threejs(12)-shader to create smoke and water cloud effect

1. Encapsulate the water ripple effect yourself


src/main/main01.js

import * as THREE from "three";

import {<!-- --> OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import gsap from "gsap";
import * as dat from "dat.gui";
import vertexShader from "../shaders/water/vertex.glsl";
import fragmentShader from "../shaders/water/fragment.glsl";

// Goal: Set the cloud smoke effect

//Create gui object
const gui = new dat.GUI();

// console.log(THREE);
//Initialize scene
const scene = new THREE.Scene();

//Create a perspective camera
const camera = new THREE.PerspectiveCamera(
  90,
  window.innerHeight / window.innerHeight,
  0.1,
  1000
);
//Set camera position
// object3d has position, and the attribute is a 3-dimensional vector
camera.position.set(0, 0, 2);
//Update camera
camera.aspect = window.innerWidth / window.innerHeight;
//Update the camera's projection matrix
camera.updateProjectionMatrix();
scene.add(camera);

//Add auxiliary axis to help us view the 3D coordinate axis
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

const params = {<!-- -->
  uWaresFrequency: 14,
  uScale: 0.03,
  uXzScale: 1.5,
  uNoiseFrequency: 10,
  uNoiseScale: 1.5,
  uLowColor: "#ff0000",
  uHighColor: "#ffff00",
  uXspeed: 1,
  uZspeed: 1,
  uNoiseSpeed: 1,
  uOpacity: 1,
};

const shaderMaterial = new THREE.ShaderMaterial({<!-- -->
  vertexShader: vertexShader,
  fragmentShader: fragmentShader,
  side: THREE.DoubleSide,
  uniforms: {<!-- -->
    uWaresFrequency: {<!-- -->
      value: params.uWaresFrequency,
    },
    uScale: {<!-- -->
      value: params.uScale,
    },
    uNoiseFrequency: {<!-- -->
      value: params.uNoiseFrequency,
    },
    uNoiseScale: {<!-- -->
      value: params.uNoiseScale,
    },
    uXzScale: {<!-- -->
      value: params.uXzScale,
    },
    uTime: {<!-- -->
      value: params.uTime,
    },
    uLowColor: {<!-- -->
      value: new THREE.Color(params.uLowColor),
    },
    uHighColor: {<!-- -->
      value: new THREE.Color(params.uHighColor),
    },
    uXspeed: {<!-- -->
      value: params.uXspeed,
    },
    uZspeed: {<!-- -->
      value: params.uZspeed,
    },
    uNoiseSpeed: {<!-- -->
      value: params.uNoiseSpeed,
    },
    uOpacity: {<!-- -->
      value: params.uOpacity,
    },
  },
  transparent: true,
});

gui
  .add(params, "uWaresFrequency")
  .min(1)
  .max(100)
  .step(0.1)
  .onChange((value) => {<!-- -->
    shaderMaterial.uniforms.uWaresFrequency.value = value;
  });

gui
  .add(params, "uScale")
  .min(0)
  .max(0.2)
  .step(0.001)
  .onChange((value) => {<!-- -->
    shaderMaterial.uniforms.uScale.value = value;
  });

gui
  .add(params, "uNoiseFrequency")
  .min(1)
  .max(100)
  .step(0.1)
  .onChange((value) => {<!-- -->
    shaderMaterial.uniforms.uNoiseFrequency.value = value;
  });

gui
  .add(params, "uNoiseScale")
  .min(0)
  .max(5)
  .step(0.001)
  .onChange((value) => {<!-- -->
    shaderMaterial.uniforms.uNoiseScale.value = value;
  });

gui
  .add(params, "uXzScale")
  .min(0)
  .max(5)
  .step(0.1)
  .onChange((value) => {<!-- -->
    shaderMaterial.uniforms.uXzScale.value = value;
  });

gui.addColor(params, "uLowColor").onFinishChange((value) => {<!-- -->
  shaderMaterial.uniforms.uLowColor.value = new THREE.Color(value);
});
gui.addColor(params, "uHighColor").onFinishChange((value) => {<!-- -->
  shaderMaterial.uniforms.uHighColor.value = new THREE.Color(value);
});

gui
  .add(params, "uXspeed")
  .min(0)
  .max(5)
  .step(0.001)
  .onChange((value) => {<!-- -->
    shaderMaterial.uniforms.uXspeed.value = value;
  });

gui
  .add(params, "uZspeed")
  .min(0)
  .max(5)
  .step(0.001)
  .onChange((value) => {<!-- -->
    shaderMaterial.uniforms.uZspeed.value = value;
  });

gui
  .add(params, "uNoiseSpeed")
  .min(0)
  .max(5)
  .step(0.001)
  .onChange((value) => {<!-- -->
    shaderMaterial.uniforms.uNoiseSpeed.value = value;
  });

gui
  .add(params, "uOpacity")
  .min(0)
  .max(1)
  .step(0.01)
  .onChange((value) => {<!-- -->
    shaderMaterial.uniforms.uOpacity.value = value;
  });

const plane = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(1, 1, 1024, 1024),
  shaderMaterial
);
plane.rotation.x = -Math.PI / 2;

scene.add(plane);

//Initialize renderer
const renderer = new THREE.WebGLRenderer({<!-- --> alpha: true });

//Set the rendering size
renderer.setSize(window.innerWidth, window.innerHeight);

// Monitor screen size changes and set the rendering size
window.addEventListener("resize", () => {<!-- -->
  // console.log("resize");
  //Update camera
  camera.aspect = window.innerWidth / window.innerHeight;
  //Update the camera's projection matrix
  camera.updateProjectionMatrix();

  //Update renderer
  renderer.setSize(window.innerWidth, window.innerHeight);
  //Set the pixel ratio of the renderer
  renderer.setPixelRatio(window.devicePixelRatio);
});

//Add renderer to body
document.body.appendChild(renderer.domElement);

//Initialize controller
const controls = new OrbitControls(camera, renderer.domElement);
//Set controller damping
controls.enableDamping = true;

const clock = new THREE.Clock();
function animate(t) {<!-- -->
  const elapsedTime = clock.getElapsedTime();
  shaderMaterial.uniforms.uTime.value = elapsedTime;
  requestAnimationFrame(animate);
  // Use the renderer to render the camera to see the content of this scene rendered.
  renderer.render(scene, camera);
}

animate();

src/shaders/water/fragment.glsl

precision lowp float;

uniform vec3 uHighColor;
uniform vec3 uLowColor;
varying float vElevation;
uniform float uOpacity;

void main(){<!-- -->
    float a = (vElevation + 1.0)/2.0;
    vec3 color = mix(uLowColor,uHighColor,a);
    gl_FragColor = vec4(color,uOpacity);
}

src/shaders/water/vertex.glsl

precision lowp float;
uniform float uWaresFrequency;
uniform float uScale;
uniform float uNoiseFrequency;
uniform float uNoiseScale;
uniform float uXzScale;
uniform float uTime;
uniform float uXspeed;
uniform float uZspeed;
uniform float uNoiseSpeed;

//The calculated height is passed to the fragment shader
varying float vElevation;

// random function
float random (vec2 st) {<!-- -->
    return fract(sin(dot(st.xy,vec2(12.9898,78.233)))*43758.5453123);
}

// rotation function
vec2 rotate(vec2 uv, float rotation, vec2 mid)
{<!-- -->
    return vec2(
      cos(rotation) * (uv.x - mid.x) + sin(rotation) * (uv.y - mid.y) + mid.x,
      cos(rotation) * (uv.y - mid.y) - sin(rotation) * (uv.x - mid.x) + mid.y
    );
}

// noise function
float noise (in vec2 _st) {<!-- -->
    vec2 i = floor(_st);
    vec2 f = fract(_st);

    // Four corners in 2D of a tile
    float a = random(i);
    float b = random(i + vec2(1.0, 0.0));
    float c = random(i + vec2(0.0, 1.0));
    float d = random(i + vec2(1.0, 1.0));

    vec2 u = f * f * (3.0 - 2.0 * f);

    return mix(a, b, u.x) +
            (c - a)* u.y * (1.0 - u.x) +
            (d - b) * u.x * u.y;
}


// Classic Perlin 2D Noise
// by Stefan Gustavson
//
vec4 permute(vec4 x)
{<!-- -->
    return mod(((x*34.0) + 1.0)*x, 289.0);
}

vec2 fade(vec2 t)
{<!-- -->
    return t*t*t*(t*(t*6.0-15.0) + 10.0);
}

float cnoise(vec2 P)
{<!-- -->
    vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
    vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
    Pi = mod(Pi, 289.0); // To avoid truncation effects in permutation
    vec4 ix = Pi.xzxz;
    vec4 iy = Pi.yyww;
    vec4 fx = Pf.xzxz;
    vec4 fy = Pf.yyww;
    vec4 i = permute(permute(ix) + iy);
    vec4 gx = 2.0 * fract(i * 0.0243902439) - 1.0; // 1/41 = 0.024...
    vec4 gy = abs(gx) - 0.5;
    vec4 tx = floor(gx + 0.5);
    gx = gx - tx;
    vec2 g00 = vec2(gx.x,gy.x);
    vec2 g10 = vec2(gx.y,gy.y);
    vec2 g01 = vec2(gx.z,gy.z);
    vec2 g11 = vec2(gx.w,gy.w);
    vec4 norm = 1.79284291400159 - 0.85373472095314 * vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
    g00 *= norm.x;
    g01 *= norm.y;
    g10 *= norm.z;
    g11 *= norm.w;
    float n00 = dot(g00, vec2(fx.x, fy.x));
    float n10 = dot(g10, vec2(fx.y, fy.y));
    float n01 = dot(g01, vec2(fx.z, fy.z));
    float n11 = dot(g11, vec2(fx.w, fy.w));
    vec2 fade_xy = fade(Pf.xy);
    vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
    float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
    return 2.3 * n_xy;
}


void main(){<!-- -->
    vec4 modelPosition = modelMatrix * vec4(position,1.0);

    float elevation = sin(modelPosition.x*uWaresFrequency + uTime*uXspeed)*sin(modelPosition.z*uWaresFrequency*uXzScale + uTime*uZspeed);

    elevation + = -abs(cnoise(vec2(modelPosition.xz*uNoiseFrequency + uTime*uNoiseSpeed))) *uNoiseScale;
    
    vElevation = elevation;
    
    elevation *= uScale;

    

    modelPosition.y + = elevation;

    gl_Position = projectionMatrix * viewMatrix *modelPosition;
}

2. Use the official water method

src/main/main.js

import * as THREE from "three";

import {<!-- --> OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import gsap from "gsap";
import * as dat from "dat.gui";

import {<!-- --> RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import {<!-- --> GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

//Import water
import {<!-- --> Water } from "three/examples/jsm/objects/Water2";

// Goal: get to know shaders

//Create gui object
const gui = new dat.GUI();

// console.log(THREE);
//Initialize scene
const scene = new THREE.Scene();

//Create a perspective camera
const camera = new THREE.PerspectiveCamera(
  90,
  window.innerHeight / window.innerHeight,
  0.1,
  1000
);
//Set camera position
// object3d has position, and the attribute is a 3-dimensional vector
camera.position.set(5, 5, 5);
//Update camera
camera.aspect = window.innerWidth / window.innerHeight;
//Update the camera's projection matrix
camera.updateProjectionMatrix();
scene.add(camera);

//Add auxiliary axis to help us view the 3D coordinate axis
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// const water = new Water(new THREE.PlaneBufferGeometry(1, 1, 1024, 1024), {<!-- -->
// color: "#ffffff",
// scale: 1,
// flowDirection: new THREE.Vector2(1, 1),
// textureHeight: 1024,
// textureWidth: 1024,
// });
// water.rotation.x = -Math.PI / 2;

// scene.add(water);

//Load scene background
const rgbeLoader = new RGBELoader();
rgbeLoader.loadAsync("./assets/050.hdr").then((texture) => {<!-- -->
  texture.mapping = THREE.EquirectangularReflectionMapping;
  scene.background = texture;
  scene.environment = texture;
});

// load bathtub
const gltfLoader = new GLTFLoader();
gltfLoader.load("./assets/model/yugang.glb", (gltf) => {<!-- -->
  console.log(gltf);
  const yugang = gltf.scene.children[0];
  yugang.material.side = THREE.DoubleSide;

  const waterGeometry = gltf.scene.children[1].geometry;
  const water = new Water(waterGeometry, {<!-- -->
    color: "#ffffff",
    scale: 1,
    flowDirection: new THREE.Vector2(1, 1),
    textureHeight: 1024,
    textureWidth: 1024,
  });

  scene.add(water);
  scene.add(yugang);
});

const light = new THREE.AmbientLight(0xffffff); // soft white light
light.intensity = 10;
scene.add(light);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
scene.add(directionalLight);

//Initialize renderer
const renderer = new THREE.WebGLRenderer({<!-- --> alpha: true, antialias: true });
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;

//Set the rendering size
renderer.setSize(window.innerWidth, window.innerHeight);

// Monitor screen size changes and set the rendering size
window.addEventListener("resize", () => {<!-- -->
  // console.log("resize");
  //Update camera
  camera.aspect = window.innerWidth / window.innerHeight;
  //Update the camera's projection matrix
  camera.updateProjectionMatrix();

  //Update renderer
  renderer.setSize(window.innerWidth, window.innerHeight);
  //Set the pixel ratio of the renderer
  renderer.setPixelRatio(window.devicePixelRatio);
});

//Add renderer to body
document.body.appendChild(renderer.domElement);

//Initialize controller
const controls = new OrbitControls(camera, renderer.domElement);
//Set controller damping
controls.enableDamping = true;

const clock = new THREE.Clock();
function animate(t) {<!-- -->
  const elapsedTime = clock.getElapsedTime();
  requestAnimationFrame(animate);
  // Use the renderer to render the camera to see the content of this scene rendered.
  renderer.render(scene, camera);
}

animate();