FDNavigate back to the homepage

Creating Realistic Water Ripples - A Beginner's Guide to Reflective Effects

June 3rd, 2023 · 3 min read

In some respects this is a adaptation of another article about hdri’s and applying textures to an inner mesh, the article named Mastering Skybox Realism - Loading and Applying HDRI with Three and R3F.

This article allowed you to apply a hdri to a sphere and apply the light emitting aspects of the hdri to the objects in the scene.

The extension to this is using: cube cameras, reflective materials from drei, a queue which stores center positions and radi for a circular SDF and a really cool customShaderMaterial from Farazz - which allows a developer to easily extend material classes from three or derivatives (meaning a class which extends of the base class materials from three).

So I wanted a base to work off to create or mimic kelvin wakes. I here you say surely you can find something on shaderToy and port over? I found this to be quite complex and really hard, alittle beyond my reach as of now.

However instead of a feedback loop for ever expanding elipsises to mimic kelvin wakes, can I not just use a queue of centers and radi of an SDF? I have done something similar in another article to do with vertex displacement. You have to be aware of hardware limitations ie max sizes of arrays, but we are way under with 300 and this is more than enough to implement this basic effect.

The key take home message was that I can dynamically set a array in glsl with javascript template literals but its slow af. The reason being I couldnt get it to update with out causing rerenders which drastically reduces fps.

Sometimes I think anything which has complex orchestration is better of doing programatically with threejs core api, rather than declaritvely.

Personal opinion from 3-4 years of playing around with these kind of things and shader effects, Im not talking about basic shaderMaterials or posprocessing but things which have more than one render or off screen render targets.


This is something really cool from Farazz created which allows easy extension of core materials or already exnted materials in R3F or Three.js.

In my case I have extended MeshReflectiveMaterial from @react-three/drei (notice the import from the materials sub directory) this is because we cannot directly use a react component which this npm packaged exports from the main folder.

CustomShaderMaterial can be defined like this:

2 {(texture) => {
3 return (
4 <mesh
5 rotation={[-Math.PI / 2, 0, 0]}
6 scale={[200, 200, 200]}
7 position={[0, 1, 0]}
8 onPointerMove={(e) => {
9 handleClick(e);
10 }}
11 ref={meshRef}
12 >
13 <planeGeometry />
14 <CustomShaderMaterial
15 ref={setMatRef}
16 baseMaterial={MeshReflectorMaterial}
17 vertexShader={patchShaders(customShader.vertex)}
18 fragmentShader={patchShaders(customShader.fragment)}
19 uniforms={uniforms}
20 envMap={texture}
21 metalness={1}
22 roughness={0}
23 />
24 </mesh>
25 );
26 }}

CustomShaderMaterial expects as a minimum:



The cubeCamera essentially takes a snap shot of the surroundings onto a 2d texture and used in the way stated above, this is a gross oversimplification, but im not trying to give a 101 on cubeCameras, just a general workflow or idea how to practicvally use them. This cube map will give us a texture we can use onto the surface of a mesh.

So these 3 things are enabling the reflectivity:


We are using Pysically based material (PBR - which meshStandardMaterial and meshPhysicalMaterial) which means we can increase the metalness to give a good shine and decrease the roughness to give the appearance of less rough material and more smoothness.

Wake and dynamic arrays GLSL

I firstly attempted to recreate this shadertoy in r3f with a feedback loop for the different buffers, unfortunately I couldnt get this to work, each shadertoy seems to have different requirements with r3f setup.

One day I should be able to come up with a solution to porting which is easy and effective, until then I will have to think of different avenues!

The way I thought of, is to extend this article.

Essentially I tried to make a dyanmic array by using javascript template literals in the shaders.

1uniform vec2 positions[int(`${positions.current.length}`)]

Suffice to say this didnt work, I got to the stage where unless i had a state setting in onPointerMove and had the uniforms update based on this (which was slow), then I had to take another avenue.

The over avenue was to have a fixed length to my array, and have a queue buffer ie, one in one out, this way the shader wouldnt complain and would compile.

1let positions = new Array(200).fill(new THREE.Vector2());
2let radius = new Array(200).fill(60);
4// more code
6useFrame(() => {
7 radius = radius.map((item, index) =>
8 Math.min(item + 100 * easeCubic(index / 200), 60.0)
9 );
11 if (matRef) {
12 matRef.uniforms.radius.value = radius;
13 matRef.uniforms.positions.value = positions;
14 }

This shows a increase in radius, but the default radius have 60 so are at the maximum and dont animate, and positions have a default vec2.

And the CustomShaderMaterial from Farazz allows easily updating of uniforms as it has the vertexShader and fragmentShader + uniforms on the material instance which normal materials dont have, which makes using onBeforeCompile really complicated.

A neat solution which by all means is helping develop the shader ecosystem.

The main shader

The shader is the summation of colors having looped through the positions array and using a sdf, creating a hollow circle with two ranges.

1vec3 colorSum = vec3(csm_DiffuseColor.rgb);
2for (int i = 0; i< MAX_SIZE; i++) {
3 vec2 position = vUv; // Calculate the fragment's position in 2D
4 vec2 center = positions[i]; // Set the center of the circle
5 float radius = 1.0 / max(1.0, 60.0 - radius[i]);
7 float distance = circleSDF(position, center, radius);
9 if (distance < 0.0 && distance > -0.005) {
10 // Fragment is inside the circle
11 colorSum += colorSum * 0.06;
12 } else {
13 // Fragment is outside the circle
14 colorSum += vec3(0.0, 0.0, 0.0); // Transparent color
15 }
17 if (length(center) > 0.0) {
18 }
19 csm_DiffuseColor = vec4(colorSum, 1.0);

An important note to make is that using this library for a customShader you need to replace standard things like, gl_Position, gl_FragColor and three DiffuseColor with cms_FragColor or csm_DiffuseColor. And there are loads of ways to configure this so replacing specific bits of the shader etc etc.

The future part 2

This is a great base to work from and develop a kelvin wake algorithm along with reflective water, with a few coming tweaks this should be a cool water example. For now, have fun and watch out for the part 2 of this.

More articles from theFrontDev

Immersive Portals in Three and R3F - Unleashing the Power of Stencil Masks from Drei

Dive into the world of portals in Three.js with the powerful stencil mask functionality from Drei. Experience seamless transitions and captivating environments as you traverse these enchanting gateways. With precise control over visibility using stencil masks, create immersive and interactive scenes that transport your audience to new dimensions. Discover the endless creative possibilities and unlock a realm of storytelling potential with portals in Three.js. Step into a world where reality and imagination converge, and embark on an immersive journey like never before.

May 21st, 2023 · 2 min read

Mastering Skybox Realism - Loading and Applying HDRI with Three and R3F

Embark on a journey towards breathtaking visual fidelity as we delve into the realm of HDRI-driven skybox shaders in Three.js. In this comprehensive article, we guide you through the process of loading and seamlessly applying High Dynamic Range Images (HDRI) to create stunningly realistic skybox environments. Discover the art of harnessing the true potential of HDRI to evoke immersive atmospheres and dynamic lighting conditions. Learn the intricacies of integrating HDRI seamlessly into Three.js skybox shaders, unlocking a world of photorealistic rendering and captivating visual experiences. Join me as we explore the techniques for achieving unmatched visual realism with HDRI in Three.js skybox shaders.

May 21st, 2023 · 1 min read
© 2021–2023 theFrontDev
Link to $https://twitter.com/TheFrontDevLink to $https://github.com/Richard-ThompsonLink to $https://www.linkedin.com/in/richard-thompson-248ba3111/