On this tutorial, I’ll present you the way I created a cool, stylized water impact with clean motion for my React Three Fiber sport. I’ll stroll you thru the planning course of, how I stored the model constant, the challenges I confronted, and what went flawed alongside the way in which. We’ll cowl writing shaders, optimizing 3D property, and utilizing them effectively to create an attention-grabbing impact—all whereas sustaining good efficiency.
How the Concept Got here Up
This yr, my spouse simply turned 30, and I wished to do one thing particular for her: a sport impressed by O Conto da Ilha Desconhecida (The Story of the Unknown Island) by José Saramago, a e-book she’d been eager to learn for some time. The thought was easy: construct an open-world exploration sport in React Three Fiber, set on an island the place she may discover, discover clues, and ultimately uncover her birthday reward.

Water performs an enormous half within the sport—it’s probably the most seen factor of the setting, and for the reason that sport is ready on an island, it’s everywhere. However right here’s the problem: how do I make the water look superior with out melting the graphics card?
Type
I wished my sport to have a cartoonish, stylized aesthetic, and to realize that, I took inspiration from three foremost initiatives: Coastal World, Elysium, and Europa. My purpose was for my sport to seize that very same playful, charming vibe, so I knew precisely the route I wished to take.

Creating Property
Earlier than we dive into the water impact, we want some 3D property to begin constructing the setting. First, I created the terrain in Blender by sculpting a subdivided airplane mesh. The important thing right here is to create elevations on the heart of the mesh whereas holding the outer edges flat, sustaining their authentic place (no top change on the edges).

I additionally added some enjoyable rocks to deliver a bit of additional attraction to the scene.

You’ll discover I didn’t apply any supplies to the fashions but. That’s intentional—I’ll be dealing with the supplies straight in React Three Fiber to maintain all the things wanting constant.
Setting Up the Setting
I’ll go step-by-step so that you don’t get misplaced within the course of.
I imported the fashions I created in Blender, following this wonderful tutorial by Niccolò Fanton, revealed right here on Codrops.

Then, I positioned them over a big planeGeometry
that covers the bottom and disappears into the fog. This strategy is lighter than scaling our terrain mesh within the X and Z axes, particularly because you may wish to work with numerous subdivisions. Right here’s basic math: 4 vertices are lighter than a whole lot or extra.
const { nodes } = useGLTF("/fashions/terrain.glb")
return (
<group dispose={null}>
<mesh
geometry={nodes.airplane.geometry}
materials={nodes.airplane.materials} // We'll exchange this default Blender materials later
receiveShadow
/>
<mesh
rotation-x={-Math.PI / 2}
place={[0, -0.01, 0]} // Moved it down to forestall the visible glitch from airplane collision
materials={nodes.airplane.materials} // Utilizing the identical materials for a seamless look
receiveShadow
>
<planeGeometry args={[256, 256]} />
</mesh>
</group>
)

Subsequent, I duplicated this mesh
to function the water floor and moved it as much as the peak I wished for the water degree.

Since this waterLevel
shall be utilized in different elements, I arrange a retailer
with Zustand to simply handle and entry the water degree all through the sport. This manner, we will tweak the values and see the modifications mirrored throughout the complete sport, which is one thing I actually get pleasure from doing!
import { create } from "zustand"
export const useStore = create((set) => ({
waterLevel: 0.9,
}))
const waterLevel = useStore((state) => state.waterLevel)
return (
<mesh rotation-x={-Math.PI / 2} position-y={waterLevel}>
<planeGeometry args={[256, 256]} />
<meshStandardMaterial coloration="lightblue" />
</mesh>
)
Clay Supplies? Not Anymore!
As talked about earlier, we’ll deal with the supplies in React Three Fiber, and now it’s time to get began. Since we’ll be writing some shaders, let’s use the Customized Shader Materials library, which permits us to increase Three.js supplies with our personal vertex and fragment shaders. It’s a lot less complicated than utilizing onBeforeCompile
and requires far much less code.
Set Up
Let’s begin with the terrain. I picked three colours to make use of right here: Sand, Grass, and Underwater. First, we apply the sand coloration as the bottom coloration for the terrain and move waterLevel
, GRASS_COLOR
, and UNDERWATER_COLOR
as uniforms to the shader.
It’s time to begin utilizing the useControls
hook from Leva so as to add a GUI, which lets us see the modifications instantly.
// Interactive coloration parameters
const { SAND_BASE_COLOR, GRASS_BASE_COLOR, UNDERWATER_BASE_COLOR } =
useControls("Terrain", {
SAND_BASE_COLOR: { worth: "#ff9900", label: "Sand" },
GRASS_BASE_COLOR: { worth: "#85a02b", label: "Grass" },
UNDERWATER_BASE_COLOR: { worth: "#118a4f", label: "Underwater" }
})
// Convert coloration hex values to Three.js Colour objects
const GRASS_COLOR = useMemo(
() => new THREE.Colour(GRASS_BASE_COLOR),
[GRASS_BASE_COLOR]
)
const UNDERWATER_COLOR = useMemo(
() => new THREE.Colour(UNDERWATER_BASE_COLOR),
[UNDERWATER_BASE_COLOR]
)
// Materials
const materialRef = useRef()
// Replace shader uniforms every time management values change
useEffect(() => {
if (!materialRef.present) return
materialRef.present.uniforms.uGrassColor.worth = GRASS_COLOR
materialRef.present.uniforms.uUnderwaterColor.worth = UNDERWATER_COLOR
materialRef.present.uniforms.uWaterLevel.worth = waterLevel
}, [
GRASS_COLOR,
UNDERWATER_COLOR,
waterLevel
])
<mesh geometry={nodes.airplane.geometry} receiveShadow>
<CustomShaderMaterial
ref={materialRef}
baseMaterial={THREE.MeshStandardMaterial}
coloration={SAND_BASE_COLOR}
vertexShader={vertexShader}
fragmentShader={fragmentShader}
uniforms={{
uTime: { worth: 0 },
uGrassColor: { worth: GRASS_COLOR },
uUnderwaterColor: { worth: UNDERWATER_COLOR },
uWaterLevel: { worth: waterLevel }
}}
/>
</mesh>
We additionally apply the UNDERWATER_COLOR
to the big planeGeometry
we created for the bottom, guaranteeing that the 2 parts mix seamlessly.
<mesh
rotation-x={-Math.PI / 2}
place={[0, -0.01, 0]}
receiveShadow
>
<planeGeometry args={[256, 256]} />
<meshStandardMaterial coloration={UNDERWATER_BASE_COLOR} />
</mesh>
Observe: As you’ll see, I exploit capitalCase
for world values that come straight from useStore
hook and UPPERCASE
for values managed by Leva.
Vertex and Fragment Shader
To date, so good. Now, let’s create the vertexShader
and fragmentShader
.
Right here we have to use the csm_
prefix as a result of Customized Shader Materials extends an current materials from Three.js. This manner, our customized varyings received’t battle with any others that may already be declared on the prolonged materials.
// Vertex Shader
various vec3 csm_vPositionW;
void foremost() {
csm_vPositionW = (modelMatrix * vec4(place, 1.0)).xyz;
}
Within the code above, we’re passing the vertex place info to the fragmentShader
through the use of a various
named csm_vPositionW
. This permits us to entry the world place of every vertex within the fragmentShader
, which shall be helpful for creating results based mostly on the vertex’s place in world house.
Within the fragmentShader
, we use uWaterLevel
as a threshold mixed with csm_vPositionW.y
, so when uWaterLevel
modifications, all the things reacts accordingly.
// Fragment Shader
various vec3 csm_vPositionW;
uniform float uWaterLevel;
uniform vec3 uGrassColor;
uniform vec3 uUnderwaterColor;
void foremost() {
// Set the present coloration as the bottom coloration
vec3 baseColor = csm_DiffuseColor.rgb;
// Darken the bottom coloration at decrease Y values to simulate moist sand
float heightFactor = smoothstep(uWaterLevel + 1.0, uWaterLevel, csm_vPositionW.y);
baseColor = combine(baseColor, baseColor * 0.5, heightFactor);
// Mix underwater coloration with base planeMesh so as to add depth to the ocean backside
float oceanFactor = smoothstep(min(uWaterLevel - 0.4, 0.2), 0.0, csm_vPositionW.y);
baseColor = combine(baseColor, uUnderwaterColor, oceanFactor);
// Add grass to the upper areas of the terrain
float grassFactor = smoothstep(uWaterLevel + 0.8, max(uWaterLevel + 1.6, 3.0), csm_vPositionW.y);
baseColor = combine(baseColor, uGrassColor, grassFactor);
// Output the ultimate coloration
csm_DiffuseColor = vec4(baseColor, 1.0);
}
What I like most about utilizing world place to tweak parts is the way it lets us create dynamic visuals, like lovely gradients that react to the setting.
For instance, we darkened the sand close to the water degree to present the impact of moist sand, which provides a pleasant contact. Plus, we added grass that grows based mostly on an offset from the water degree.
Right here’s what it appears like (I’ve hidden the water for now so we will see the outcomes extra clearly)

We will apply the same impact to the rocks, however this time utilizing a inexperienced coloration to simulate moss rising on them.
various vec3 csm_vPositionW;
uniform float uWaterLevel;
uniform vec3 uMossColor;
void foremost() {
// Set the present coloration as the bottom coloration
vec3 baseColor = csm_DiffuseColor.rgb;
// Paint decrease Y with a distinct coloration to simulate moss
float mossFactor = smoothstep(uWaterLevel + 0.3, uWaterLevel - 0.05, csm_vPositionW.y);
baseColor = combine(baseColor, uMossColor, mossFactor);
// Output the ultimate coloration
csm_DiffuseColor = vec4(baseColor, 1.0);
}

Water, Lastly
It was excessive time so as to add the water, ensuring it appears nice and feels interactive.
Animating Water Circulation
To begin, I wished the water to slowly transfer up and down, like mild tidal waves. To realize this, we move a couple of values to the vertexShader
as uniforms:
uTime
to animate the vertices based mostly on the time handed (this permits us to create steady movement)uWaveSpeed
anduWaveAmplitude
to manage the velocity and measurement of the wave motion.
Let’s do it step-by-step
1. Arrange the values globally in useStore
, as it is going to be helpful later.
// useStore.js
import { create } from "zustand"
export const useStore = create((set) => ({
waterLevel: 0.9,
waveSpeed: 1.2,
waveAmplitude: 0.1
}))
2. Add Leva controls to see the modifications reside
// International states
const waterLevel = useStore((state) => state.waterLevel)
const waveSpeed = useStore((state) => state.waveSpeed)
const waveAmplitude = useStore((state) => state.waveAmplitude)
// Interactive water parameters
const {
COLOR_BASE_NEAR, WATER_LEVEL, WAVE_SPEED, WAVE_AMPLITUDE
} = useControls("Water", {
COLOR_BASE_NEAR: { worth: "#00fccd", label: "Close to" },
WATER_LEVEL: { worth: waterLevel, min: 0.5, max: 5.0, step: 0.1, label: "Water Stage" },
WAVE_SPEED: { worth: waveSpeed, min: 0.5, max: 2.0, step: 0.1, label: "Wave Velocity" },
WAVE_AMPLITUDE: { worth: waveAmplitude, min: 0.05, max: 0.5, step: 0.05, label: "Wave Amplitude" },
})
3. Add the uniforms to the Customized Shader Materials
<CustomShaderMaterial
ref={materialRef}
baseMaterial={THREE.MeshStandardMaterial}
vertexShader={vertexShader}
fragmentShader={fragmentShader}
uniforms={{
uTime: { worth: 0 },
uWaveSpeed: { worth: WAVE_SPEED },
uWaveAmplitude: { worth: WAVE_AMPLITUDE }
}}
coloration={COLOR_BASE_NEAR}
clear
opacity={0.4}
/>
4. Deal with the worth updates
// Replace shader uniforms every time management values change
useEffect(() => {
if (!materialRef.present) return
materialRef.present.uniforms.uWaveSpeed.worth = WAVE_SPEED
materialRef.present.uniforms.uWaveAmplitude.worth = WAVE_AMPLITUDE
}, [WAVE_SPEED, WAVE_AMPLITUDE])
// Replace shader time
useFrame(({ clock }) => {
if (!materialRef.present) return
materialRef.present.uniforms.uTime.worth = clock.getElapsedTime()
})
5. Don’t overlook to replace the worldwide values so different elements can share the identical settings
// Replace world states
useEffect(() => {
useStore.setState(() => ({
waterLevel: WATER_LEVEL,
waveSpeed: WAVE_SPEED,
waveAmplitude: WAVE_AMPLITUDE
}))
}, [WAVE_SPEED, WAVE_AMPLITUDE, WATER_LEVEL])
Then, within the vertexShader
, we use these values to maneuver all of the vertices up and down. Shifting vertices within the vertexShader
is normally sooner than animating it with useFrame
as a result of it runs straight on the GPU, which is significantly better suited to dealing with these sorts of duties.
various vec2 csm_vUv;
uniform float uTime;
uniform float uWaveSpeed;
uniform float uWaveAmplitude;
void foremost() {
// Ship the uv coordinates to fragmentShader
csm_vUv = uv;
// Modify the y place based mostly on sine perform, oscillating up and down over time
float sineOffset = sin(uTime * uWaveSpeed) * uWaveAmplitude;
// Apply the sine offset to the y part of the place
vec3 modifiedPosition = place;
modifiedPosition.z += sineOffset; // z used as y as a result of factor is rotated
csm_Position = modifiedPosition;
}
Crafting the Water Floor
At this level, I wished to present my water the identical look as Coastal World, with foam-like spots and a wave sample that had a cartoonish really feel. Plus, the sample wanted to maneuver to make it really feel like actual water!
I spent a while fascinated about tips on how to obtain this with out sacrificing efficiency. Utilizing a texture map was out of the query, since we’re working with a big airplane mesh. Utilizing an enormous texture would have been too heavy and sure resulted in blurry sample edges.
Happily, I got here throughout this wonderful article by the Merci Michel group (the unimaginable creators of Coastal World) explaining how they dealt with it there. So my purpose was to try to recreate that impact with my very own twist.
Primarily, it’s a mixture of Perlin noise*, smoothStep
, sine capabilities, and lots of creativity!
* Perlin noise is a clean, random noise utilized in graphics to create natural-looking patterns, like terrain or clouds, with softer, extra natural transitions than common random noise.
Let’s break it down to grasp it higher:
First, I added two new uniforms to my shader: uTextureSize
and uColorFar
. These allow us to management the feel’s dimensions and create the impact of the ocean coloration altering the additional it’s from the digicam.
By now, you need to be accustomed to creating controls utilizing Leva and passing them as uniforms. Let’s bounce proper into the shader.
various vec2 csm_vUv;
uniform float uTime;
uniform vec3 uColorNear;
uniform vec3 uColorFar;
uniform float uTextureSize;
vec3 mod289(vec3 x) { return x - ground(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - ground(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
float snoise(vec2 v) {
...
// The Perlin noise code is a bit prolonged, so I’ve omitted it right here.
// You'll find the total code by the wizard Patricio Gonzalez Vivo at
// https://thebookofshaders.com/edit.php#11/lava-lamp.frag
}
The important thing right here is utilizing the smoothStep
perform to extract a small vary of grey from our Perlin noise texture. This provides us a wave-like sample. We will mix these values in all types of how to create totally different results.
First, the fundamental strategy:
// Generate noise for the bottom texture
float noiseBase = snoise(csm_vUv);
// Normalize the values
vec3 colorWaves = noiseBase * 0.5 + 0.5;
// Apply smoothstep for wave thresholding
vec3 waveEffect = 1.0 - (smoothstep(0.53, 0.532, colorWaves) + smoothstep(0.5, 0.49, colorWaves));
Now, with all the consequences mixed:
void foremost() {
// Set the present coloration as the bottom coloration.
vec3 finalColor = csm_FragColor.rgb;
// Set an preliminary alpha worth
vec3 alpha = vec3(1.0);
// Invert texture measurement
float textureSize = 100.0 - uTextureSize;
// Generate noise for the bottom texture
float noiseBase = snoise(csm_vUv * (textureSize * 2.8) + sin(uTime * 0.3));
noiseBase = noiseBase * 0.5 + 0.5;
vec3 colorBase = vec3(noiseBase);
// Calculate foam impact utilizing smoothstep and thresholding
vec3 foam = smoothstep(0.08, 0.001, colorBase);
foam = step(0.5, foam); // binary step to create foam impact
// Generate further noise for waves
float noiseWaves = snoise(csm_vUv * textureSize + sin(uTime * -0.1));
noiseWaves = noiseWaves * 0.5 + 0.5;
vec3 colorWaves = vec3(noiseWaves);
// Apply smoothstep for wave thresholding
// Threshold for waves oscillates between 0.6 and 0.61
float threshold = 0.6 + 0.01 * sin(uTime * 2.0);
vec3 waveEffect = 1.0 - (smoothstep(threshold + 0.03, threshold + 0.032, colorWaves) +
smoothstep(threshold, threshold - 0.01, colorWaves));
// Binary step to extend the wave sample thickness
waveEffect = step(0.5, waveEffect);
// Mix wave and foam results
vec3 combinedEffect = min(waveEffect + foam, 1.0);
// Making use of a gradient based mostly on distance
float vignette = size(csm_vUv - 0.5) * 1.5;
vec3 baseEffect = smoothstep(0.1, 0.3, vec3(vignette));
vec3 baseColor = combine(finalColor, uColorFar, baseEffect);
combinedEffect = min(waveEffect + foam, 1.0);
combinedEffect = combine(combinedEffect, vec3(0.0), baseEffect);
// Pattern foam to take care of fixed alpha of 1.0
vec3 foamEffect = combine(foam, vec3(0.0), baseEffect);
// Set the ultimate coloration
finalColor = (1.0 - combinedEffect) * baseColor + combinedEffect;
// Managing the alpha based mostly on the gap
alpha = combine(vec3(0.2), vec3(1.0), foamEffect);
alpha = combine(alpha, vec3(1.0), vignette + 0.5);
// Output the ultimate coloration
csm_FragColor = vec4(finalColor, alpha);
}
The key right here is to use the uTime
uniform to our Perlin noise texture, then use a sin
perform to make it transfer forwards and backwards, creating that flowing water impact.
// We use uTime to make the Perlin noise texture transfer
float noiseWaves = snoise(csm_vUv * textureSize + sin(uTime * -0.1));
...
// We will additionally use uTime to make the sample form dynamic
float threshold = 0.6 + 0.01 * sin(uTime * 2.0);
vec3 waveEffect = 1.0 - (smoothstep(threshold + 0.03, threshold + 0.032, colorWaves) +
smoothstep(threshold, threshold - 0.01, colorWaves));
However we nonetheless want the ultimate contact: the intersection foam impact. That is the white, foamy texture that seems the place the water touches different objects, like rocks or the shore.

Failed Foam Impact
After performing some analysis, I discovered this fancy answer that makes use of a RenderTarget
and depthMaterial
to create the froth impact (which, as I later realized, is a go-to answer for results just like the one I used to be aiming for).
Right here’s a breakdown of this strategy: the RenderTarget
captures the depth of the scene, and the depthMaterial
applies that depth knowledge to generate foam the place the water meets different objects. It appeared like the right method to obtain the visible impact I had in thoughts.
However after implementing it, I shortly realized that whereas the impact seemed nice, the efficiency was unhealthy.

The difficulty right here is that it’s computationally costly—rendering the scene to an offscreen buffer and calculating depth requires additional GPU passes. On high of that, it doesn’t work properly with clear supplies, which precipitated issues in my scene.
So, after testing it and seeing the efficiency drop, I needed to rethink the strategy and provide you with a brand new answer.
It’s all an Phantasm

Anybody who’s into sport improvement is aware of that lots of the cool results we see on display screen are literally intelligent illusions. And this one isn’t any totally different.
I used to be watching Bruno Simon’s Devlog when he talked about the right answer: portray a white stripe precisely on the water degree on each object in touch with the water (truly, he used a gradient, however I personally choose stripes). Later, I spotted that Coastal World, as talked about within the article, does the very same factor. However on the time I learn the article, I wasn’t fairly ready to soak up that information.

So, I ended up utilizing the identical perform I wrote to maneuver the water vertices within the vertexShader
and utilized it to the terrain and rocks fragment shaders with some tweaks.
Step-by-Step Course of:
1. First, I added foamDepth
to our world retailer alongside the waterLevel
, waveSpeed
, and waveAmplitude
, as a result of these values have to be accessible throughout all parts within the scene.
import { create } from "zustand"
export const useStore = create((set) => ({
waterLevel: 0.9,
waveSpeed: 1.2,
waveAmplitude: 0.1,
foamDepth: 0.05,
}))
2. Then, we move these uniforms to the fragmentShader
of every materials that wants the froth impact.
// International states
const waterLevel = useStore((state) => state.waterLevel)
const waveSpeed = useStore((state) => state.waveSpeed)
const waveAmplitude = useStore((state) => state.waveAmplitude)
const foamDepth = useStore((state) => state.foamDepth)
...
<CustomShaderMaterial
...
uniforms={{
uTime: { worth: 0 },
uWaterLevel: { worth: waterLevel },
uWaveSpeed: { worth: waveSpeed },
uWaveAmplitude: { worth: waveAmplitude }
uFoamDepth: { worth: foamDepth },
...
}}
/>
3. Lastly, on the finish of our fragmentShader
, we add the capabilities that draw the stripe, as you possibly can see above.
The way it works:
1. First, we synchronize the water motion utilizing uWaterLevel
, uWaterSpeed
, uWaterAmplitude
, and uTime
.
// Foam Impact
// Get the y place based mostly on sine perform, oscillating up and down over time
float sineOffset = sin(uTime * uWaveSpeed) * uWaveAmplitude;
// The present dynamic water top
float currentWaterHeight = uWaterLevel + sineOffset;
2. Then, we use smoothStep
to create a white stripe with a thickness of uFoamDepth
based mostly on csm_vPositionW.y
. It’s the identical strategy we used for the moist sand, moss and grass, however now it’s in movement.
float stripe = smoothstep(currentWaterHeight + 0.01, currentWaterHeight - 0.01, csm_vPositionW.y)
- smoothstep(currentWaterHeight + uFoamDepth + 0.01, currentWaterHeight + uFoamDepth - 0.01, csm_vPositionW.y);
vec3 stripeColor = vec3(1.0, 1.0, 1.0); // White stripe
// Apply the froth strip to baseColor
vec3 finalColor = combine(baseColor - stripe, stripeColor, stripe);
// Output the ultimate coloration
csm_DiffuseColor = vec4(finalColor, 1.0);
And that’s it! Now we will tweak the values to get one of the best visuals.
You Thought I Was Accomplished? Not Fairly But!
I believed it will be enjoyable so as to add some sound to the expertise. So, I picked two audio recordsdata, one with the sound of waves crashing and one other with birds singing. Certain, perhaps there aren’t any birds on such a distant island, however the vibe felt proper.
For the sound, I used the PositionalAudio
library from Drei, which is superior for including 3D sound to the scene. With it, we will place the audio precisely the place we wish it to come back from, creating an immersive expertise.
<group place={[0, 0, 0]}>
<PositionalAudio
autoplay
loop
url="/sounds/waves.mp3"
distance={50}
/>
</group>
<group place={[-65, 35, -55]}>
<PositionalAudio
autoplay
loop
url="/sounds/birds.mp3"
distance={30}
/>
</group>
And voilà!
Now, it’s necessary to notice that browsers don’t allow us to play audio till the consumer interacts with the web page. So, to deal with that, I added a world state audioEnabled
to handle the audio, and created a button to allow sound when the consumer clicks on it.
import { create } from "zustand"
export const useStore = create((set) => ({
...
audioEnabled: false,
setAudioEnabled: (enabled) => set(() => ({ audioEnabled: enabled })),
setReady: (prepared) => set(() => ({ prepared: prepared }))
}))
const audioEnabled = useStore((state) => state.audioEnabled)
const setAudioEnabled = useStore((state) => state.setAudioEnabled)
const handleSound = () => {
setAudioEnabled(!audioEnabled)
}
return <button onClick={() => handleSound()}>Allow sound</button>
const audioEnabled = useStore((state) => state.audioEnabled)
return (
audioEnabled && (
<>
<group place={[0, 0, 0]}>
<PositionalAudio
autoplay
loop
url="/sounds/waves.mp3"
distance={50}
/>
</group>
<group place={[-65, 35, -55]}>
<PositionalAudio
autoplay
loop
url="/sounds/birds.mp3"
distance={30}
/>
</group>
</>
)
)
Then, I used a mixture of CSS animations to make all the things come collectively.
Conclusion
And that’s it! On this tutorial, I’ve walked you thru the steps I took to create a novel water impact for my sport, which I made as a birthday reward for my spouse. In the event you’re interested in her response, you possibly can test it out on my Instagram, and be happy to attempt the reside model of the sport.
This challenge provides you a strong basis for creating stylized water and shaders in React Three Fiber, and you should use it as a base to experiment and construct much more advanced environments. Whether or not you’re engaged on a private challenge or diving into one thing greater, the methods I’ve shared could be tailored and expanded to fit your wants.
When you have any questions or suggestions, I’d love to listen to from you! Thanks for following alongside, and pleased coding! 🎮