11.6 C
New York
Wednesday, March 5, 2025

Creating Stylized Water Results with React Three Fiber


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.

Screenshot of the game showing Maria, the main character, standing on a hill, surrounded by trees, looking at the sea.

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.

Three main inspirations: Coastal World, Elysium, and Europa.
Coastal World, Elysium and Europa

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).

A subdivided plane mesh in Blender

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

Quirky rocks made in Blender

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>
)
Models imported over a planeGeometry mesh
Wanting seamless

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

PlaneGeometry Mesh serving as water surface
I used a lightweight blue coloration to see the distinction between the planes.

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)

Terrain model with colors applied
The darkish greenish-blue on the bottom will assist us so as to add depth to the scene

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);  
}
Rocks with a moss-like effect
It provides persona to the scene—so easy, but so cozy

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 and uWaveAmplitude 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.

Intersection foam effect

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.

Frame drop
Effectively, it didn’t freeze like that, however in my thoughts, it did

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

Phil Dunphy doing a magic trick

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.

Mr. Miyagi teaching Daniel-san a lesson

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! 🎮





Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles