SPACE
to play / pauseARROW RIGHT
orL
to go forwardARROW LEFT
orJ
to go backwardARROW UP
to increase volumeARROW DOWN
to decrease volumeF
to toggle fullscreenM
to toggle mute0 to 9
to go to the corresponding part of the videoSHIFT
+,
to decrease playback speedSHIFT
+.
or;
to increase playback speed
Shortcuts ⌨️
Unlock content 🔓
To get access to 93 hours of video, a members-only Discord server, subtitles, lesson resources, future updates and much more join us for only $95!
Want to learn more? 👋
That's the end of the free part 😔
To get access to 93 hours of video, a members-only Discord server and future updates, join us for only $95!
Introduction 00:00
At the end of the Raging Sea lesson, I suggested adding reflection for those who wanted to go further.
By now, you probably realized how hard this task is.
Fortunately, now that we know how basic shading works, we can apply our knowledge to the raging sea. And even better, we can re-use the light functions we prepared in the previous lesson.
Here’s the final result:
It’s also the opportunity to make the water look even more epic as if there was demonic stuff happening under the sea.
Setup 01:14
The starter is exactly the same as the Raging Sea project as we left it:
- A well-subdivided plane for the sea
- Custom shaders located in the
src/shaders/water/
folder to handle the up and down animation and the color - An instance of
lil-gui
with some tweaks to control the shader - The
vite-plugin-glsl
dependency to handle GLSL files OrbitControls
to rotate around
Tweak the initial setting 02:12
We are going to tweak our initial setting to suit what’s coming and to make the water look a little more epic as shown before.
The initial colors were chosen to highlight the tip of the waves and create contrast. This won’t be necessary because the light shading is going to add a lot of realistic and natural contrast.
Let’s change the depthColor
to #ff4000
and the surfaceColor
to #151c37
:
debugObject.depthColor = '#ff4000'
debugObject.surfaceColor = '#151c37'
We now need to update the gradient position and amplitude.
On the waterMaterial
uniforms, change the uColorOffset
to 0.925
and the uColorMultiplier
to 1
:
const waterMaterial = new THREE.ShaderMaterial({
// ...
uniforms:
{
// ...
uColorOffset: { value: 0.925 },
uColorMultiplier: { value: 1 }
}
})
It already looks a lot more epic.
Prepare the shader 04:03
Before adding any light shading, we need to prepare our shader and organize things a little.
The starter could have come prepared, but I wanted you to start from the exact end of the Raging Sea lesson.
Perlin function
Rename the cnoise
function with perlinClassic3D
and don’t forget to also change where you call it in main()
:
float perlinClassic3D(vec3 P)
{
// ...
}
void main()
{
// ...
for(float i = 1.0; i <= uSmallIterations; i++)
{
elevation -= abs(perlinClassic3D(vec3(modelPosition.xz * uSmallWavesFrequency * i, uTime * uSmallWavesSpeed)) * uSmallWavesElevation / i);
}
// ...
}
cnoise
stands for classic noise
which is correct, but not accurate enough.
Next, we are going to put it in a different file so that it doesn’t take up most of the vertex shader.
In the src/shaders/
folder, create an includes/
folder.
In the includes/
folder, create a perlinClassic3D.glsl
file and add the function accompanied by the permute
, taylorInvSqrt
and fade
functions:
// Classic Perlin 3D Noise
// by Stefan Gustavson
//
vec4 permute(vec4 x)
{
return mod(((x*34.0)+1.0)*x, 289.0);
}
vec4 taylorInvSqrt(vec4 r)
{
return 1.79284291400159 - 0.85373472095314 * r;
}
vec3 fade(vec3 t)
{
return t*t*t*(t*(t*6.0-15.0)+10.0);
}
float perlinClassic3D(vec3 P)
{
vec3 Pi0 = floor(P); // Integer part for indexing
vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
Pi0 = mod(Pi0, 289.0);
Pi1 = mod(Pi1, 289.0);
vec3 Pf0 = fract(P); // Fractional part for interpolation
vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
vec4 iy = vec4(Pi0.yy, Pi1.yy);
vec4 iz0 = Pi0.zzzz;
vec4 iz1 = Pi1.zzzz;
vec4 ixy = permute(permute(ix) + iy);
vec4 ixy0 = permute(ixy + iz0);
vec4 ixy1 = permute(ixy + iz1);
vec4 gx0 = ixy0 / 7.0;
vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
gx0 = fract(gx0);
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
vec4 sz0 = step(gz0, vec4(0.0));
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
vec4 gx1 = ixy1 / 7.0;
vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
gx1 = fract(gx1);
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
vec4 sz1 = step(gz1, vec4(0.0));
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
g000 *= norm0.x;
g010 *= norm0.y;
g100 *= norm0.z;
g110 *= norm0.w;
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
g001 *= norm1.x;
g011 *= norm1.y;
g101 *= norm1.z;
g111 *= norm1.w;
float n000 = dot(g000, Pf0);
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
float n111 = dot(g111, Pf1);
vec3 fade_xyz = fade(Pf0);
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
return 2.2 * n_xyz;
}
Back in vertex.glsl
, replace all these parts with an #include
:
#include ../includes/perlinClassic3D.glsl
void main()
{
// ...
}
Comments
Add some comments in the vertex shader to separate things a little:
void main()
{
// Base position
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
// Elevation
float elevation = sin(modelPosition.x * uBigWavesFrequency.x + uTime * uBigWavesSpeed) *
sin(modelPosition.z * uBigWavesFrequency.y + uTime * uBigWavesSpeed) *
uBigWavesElevation;
for(float i = 1.0; i <= uSmallIterations; i++)
{
elevation -= abs(perlinClassic3D(vec3(modelPosition.xz * uSmallWavesFrequency * i, uTime * uSmallWavesSpeed)) * uSmallWavesElevation / i);
}
modelPosition.y += elevation;
// Final position
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
// Varyings
vElevation = elevation;
}
Do the same in the fragment shader:
void main()
{
// Base color
float mixStrength = (vElevation + uColorOffset) * uColorMultiplier;
vec3 color = mix(uDepthColor, uSurfaceColor, mixStrength);
// Final color
gl_FragColor = vec4(color, 1.0);
#include <colorspace_fragment>
}
It might sound far-fetched, but we are going to add some code to the vertex.glsl
and it’s always good to keep things organized before it gets complex.
Smoothstep
In the fragment shader, we are going to enhance the gradient’s feel by applying a smoothstep to mixStrength
:
void main()
{
// Base color
float mixStrength = (vElevation + uColorOffset) * uColorMultiplier;
mixStrength = smoothstep(0.0, 1.0, mixStrength);
// ...
}
Tone mapping
Although we are not using any tone mapping on our renderer right now, let’s anticipate it and add the tonemapping_fragment
chunk right before the colorspace_fragment
chunk:
void main()
{
// ...
// Final color
gl_FragColor = vec4(color, 1.0);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}
How to use it 🤔
- Download the Starter pack or Final project
- Unzip it
- Open your terminal and go to the unzip folder
-
Run
npm install
to install dependencies
(if your terminal warns you about vulnerabilities, ignore it) -
Run
npm run dev
to launch the local server
(project should open on your default browser automatically) - Start coding
- The JS is located in src/script.js
- The HTML is located in src/index.html
- The CSS is located in src/style.css