用Three.js搞个雨雪雾

发布于:2024-05-08 ⋅ 阅读:(18) ⋅ 点赞:(0)

许多人喜欢采用粒子来实现下雨和下雪的效果,但粒子数量过多时会影响渲染效率,我这里采用了帧缓冲,在渲染结果前面添加一层平面蒙板,只需要一个平面就能实现雨雪雾,而且有效地减少性能消耗。

image.png

1.FramebufferTexture帧缓冲贴图

将webgl渲染的结果作为一张贴图用在另外的场景元素中。

关闭renderer自动清理

  this.renderer.autoClear = false;

添加两个场景

第一个场景跟平常一样设置就行


            const tex = new THREE.TextureLoader().load(`./assets/image.jpg`);
            tex.wrapS = THREE.RepeatWrapping;
            tex.wrapT = THREE.RepeatWrapping;
            const material = new THREE.MeshStandardMaterial({
              map: tex
            });
            this.geometries = [];
            const helper = new THREE.Object3D();
            //生成50个随机建筑
            for (let i = 0; i < 50; i++) {
              const h = Math.round(Math.random() * 3) + 1;
              const x = Math.round(Math.random() * 8);
              const y = Math.round(Math.random() * 8);
              helper.position.set((x % 2 ? -1 : 1) * x, h * 0.5, (y % 2 ? -1 : 1) * y);
              const geometry = new THREE.BoxGeometry(1, h, 1);
              helper.updateWorldMatrix(true, false);
              geometry.applyMatrix4(helper.matrixWorld);
              this.geometries.push(geometry);
            }
            const mergedGeometry = BufferGeometryUtils.mergeGeometries(this.geometries, false);
            const cube = new THREE.Mesh(mergedGeometry, material);
            //接收投影
            cube.receiveShadow = true;
            //产生投影
            cube.castShadow = true;
            this.scene.add(cube);
        

为了区别第一个场景,第二个场景采用正交投影,作为接收第一个场景渲染的结果作为贴图显示。


 

          const width = this.container.offsetWidth;
          const height = this.container.offsetHeight;
         
          const sceneOrtho = new THREE.Scene();
          this.sceneOrtho = sceneOrtho;
          
          //正交相机
          const cameraOrtho = new THREE.OrthographicCamera(
            -width / 2,
            width / 2,
            height / 2,
            -height / 2,
            1,
            10
          );
          //正交相机位置
          cameraOrtho.position.z = 10;
             //正交相机范围
          cameraOrtho.left = -width / 2;
          cameraOrtho.right = width / 2;
          cameraOrtho.top = height / 2;
          cameraOrtho.bottom = -height / 2;
          this.cameraOrtho = cameraOrtho;
          //创建帧缓冲贴图  width * dpr, height * dpr 贴图大小
          const dpr = window.devicePixelRatio; 
           this.texture = new THREE.FramebufferTexture(width * dpr, height * dpr);
          const g = new THREE.PlaneGeometry(width * dpr, height * dpr);

          {//添加一个平面显示帧缓冲贴图
            const mesh = new THREE.Mesh(
              g,
              new THREE.MeshBasicMaterial({
                map: this.texture,
                transparent: true
              })
            );            
            mesh.scale.set(0.5, 0.5, 0.5); 
            mesh.position.set(-width * 0.25, height * 0.25, 0);
            sceneOrtho.add(mesh);
          }

渲染

animate() {
//清空画布
          this.renderer.clear();
          //渲染第一个场景
          this.renderer.render(this.scene, this.camera);
          if (this.texture) {
          //清空深度缓存
            this.renderer.clearDepth();
            //将第一个场景的结果渲染到帧缓冲贴图里
            //   this.vector = new THREE.Vector2();//开始坐标
            //
            this.renderer.copyFramebufferToTexture(this.vector, this.texture); 
             
            //渲染第二个场景
            this.renderer.render(this.sceneOrtho, this.cameraOrtho);
          }
          this.threeAnim = requestAnimationFrame(this.animate.bind(this));
        }

.copyFramebufferToTexture ( position : Vector2, texture : FramebufferTexture, level : Number ) 将当前WebGLFramebuffer中的像素复制到2D纹理中

  • position:开始坐标,贴图范围会根据FramebufferTexture大小和坐标决定
  • level:详细级别:级别0是基本图像级别,级别n是第n个mipmap缩减级别。

20240506_202842 00_00_00-00_00_30.gif 可以看到第一个场景同步渲染到第二个场景的帧缓冲贴图中。

2.叠加雨雪雾

在第二个场景中帧缓冲平面前面添加一个蒙板平面

帧缓冲平面大小填满容器,同样大小的蒙板平面在其前面

{//帧缓冲平面
            const mesh = new THREE.Mesh(
              g,
              new THREE.MeshBasicMaterial({
                map: this.texture,
                transparent: true
              })
            );
            mesh.position.set(0, 0, 0);
            sceneOrtho.add(mesh);
          }
          {//蒙板平面
this.material = new THREE.MeshBasicMaterial({
              color: 0xff0000,
              transparent: 0,
              opacity: 0.5
            });

            const mesh = new THREE.Mesh(g, this.material);
            mesh.position.set(0, 0, 1);
            sceneOrtho.add(mesh);
            this.weatherMesh = mesh;
          }

image.png

添加下雨效果

顶点着色器

varying vec2 vUv;
 void main(){
vUv=uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

下雨片元着色器,

varying vec2 vUv;
uniform float iTime;//随时间变化
uniform float rainSpeed;//下雨速度
uniform float rainAngle;//下雨倾斜角度
float PI = 3.1415926;
 //随机数
float random(in vec2 uv) {
    return fract(sin(dot(uv.xy, vec2(12.9898, 78.233))) *
        43758.5453123);
}
 //噪声函数
float noise(in vec2 uv) {
    vec2 i = floor(uv);
    vec2 f = fract(uv);
    f = f * f * (3. - 2. * f);

    float lb = random(i + vec2(0., 0.));
    float rb = random(i + vec2(1., 0.));
    float lt = random(i + vec2(0., 1.));
    float rt = random(i + vec2(1., 1.));

    return mix(mix(lb, rb, f.x), mix(lt, rt, f.x), f.y);
}
//下雨效果
float rain(vec2 uv) {
    float travelTime = (iTime * 0.2) + 0.1;

    vec2 tiling = vec2(1., .01);
    vec2 offset = vec2(travelTime * 0.5 + uv.x * 0.2, travelTime * 0.2);

    vec2 st = uv * tiling + offset;

    float rain = 0.1;
    float f = fract(noise(st * 200.5) * noise(st * 125.5) * rainSpeed);
    f = clamp(pow(abs(f), 15.0) * 1.5 * (rain * rain * 125.0), 0.0, 0.25);
    return f;
}
//旋转角度
mat2 rotate2d(float angle) {
    return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
void main() {
    mat2 a = rotate2d(rainAngle * PI);
    float rain = rain(vUv * a);
    if(rain <= 0.0) {
        discard;
        return;
    }
    vec3 col = vec3(1.0);
    gl_FragColor = vec4(col, rain);

}

下雨材质

const rainMaterial = new THREE.ShaderMaterial({
              transparent: true,
              uniforms: {
                rainSpeed: { value: 3.0 },//下雨速度
                iTime: { value: 1.0 },//随时间变化
                rainAngle: { value: -0.1 }//下雨倾斜角度
              },
              vertexShader: ``,
              fragmentShader:``,
});

让雨水动起来

animate(){
//...
 if (this.material) {
              this.material.uniforms.iTime.value += 0.01;
            }
            //...
}

20240506_213142.gif

可以改变rainSpeed可调整下雨大速度大小.

添加下雪效果

顶点着色器同上,下雪的片元着色器如下,

varying vec2 vUv;
uniform float iTime;
void main() {
    float snow = 0.0;
    for(int k = 0; k < 6; k++) {
        for(int i = 0; i < 12; i++) {
            float cellSize = 2.0 + (float(i) * 3.0);
            float downSpeed = 0.3 + (sin(iTime * 0.4 + float(k + i * 20)) + 1.0) * 0.00008;
            vec2 uv = vUv + vec2(0.01 * sin((iTime + float(k * 6185)) * 0.6 + float(i)) * (5.0 / float(i)), downSpeed * (iTime + float(k * 1352)) * (1.0 / float(i)));
            vec2 uvStep = (ceil((uv) * cellSize - vec2(0.5, 0.5)) / cellSize);
            float x = fract(sin(dot(uvStep.xy, vec2(12.9898 + float(k) * 12.0, 78.233 + float(k) * 315.156))) * 43758.5453 + float(k) * 12.0) - 0.5;
            float y = fract(sin(dot(uvStep.xy, vec2(62.2364 + float(k) * 23.0, 94.674 + float(k) * 95.0))) * 62159.8432 + float(k) * 12.0) - 0.5;

            float randomMagnitude1 = sin(iTime * 2.5) * 0.7 / cellSize;
            float randomMagnitude2 = cos(iTime * 2.5) * 0.7 / cellSize;

            float d = 5.0 * distance((uvStep.xy + vec2(x * sin(y), y) * randomMagnitude1 + vec2(y, x) * randomMagnitude2), uv.xy);

            float omiVal = fract(sin(dot(uvStep.xy, vec2(32.4691, 94.615))) * 31572.1684);
            if(omiVal < 0.08 ? true : false) {
                float newd = (x + 1.0) * 0.4 * clamp(1.9 - d * (15.0 + (x * 6.3)) * (cellSize / 1.4), 0.0, 1.0);
                snow += newd;
            }
        }
    }
    gl_FragColor = vec4(1.0, 1.0, 1.0, snow);
}

20240506_213728.gif

这里必须来一句“雪花飘飘北风萧萧天地一片苍茫~”应应景

添加云雾的效果

顶点着色器同上,云雾的片元着色器如下,

float cloudDensity = 1.0; 	// overall density [0,1]
float noisiness = 0.35; 	// overall strength of the noise effect [0,1]
float speed = 0.1;			// controls the animation speed [0, 0.1 ish)
float cloudHeight = 0.5; 	// (inverse) height of the input gradient [0,...)
uniform float iTime;
uniform float fogSpeed;
uniform float fogOpacity;
varying vec2 vUv;

vec3 mod289(vec3 x) {
  return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec4 mod289(vec4 x) {
  return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec4 permute(vec4 x) {
     return mod289(((x*34.0)+1.0)*x);
}

vec4 taylorInvSqrt(vec4 r)
{
  return 1.79284291400159 - 0.85373472095314 * r;
}

float snoise(vec3 v)
  { 
  const vec2  C = vec2(1.0/6.0, 1.0/3.0) ;
  const vec4  D = vec4(0.0, 0.5, 1.0, 2.0);

// First corner
  vec3 i  = floor(v + dot(v, C.yyy) );
  vec3 x0 =   v - i + dot(i, C.xxx) ;

// Other corners
  vec3 g = step(x0.yzx, x0.xyz);
  vec3 l = 1.0 - g;
  vec3 i1 = min( g.xyz, l.zxy );
  vec3 i2 = max( g.xyz, l.zxy );

  //   x0 = x0 - 0.0 + 0.0 * C.xxx;
  //   x1 = x0 - i1  + 1.0 * C.xxx;
  //   x2 = x0 - i2  + 2.0 * C.xxx;
  //   x3 = x0 - 1.0 + 3.0 * C.xxx;
  vec3 x1 = x0 - i1 + C.xxx;
  vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
  vec3 x3 = x0 - D.yyy;      // -1.0+3.0*C.x = -0.5 = -D.y

// Permutations
  i = mod289(i); 
  vec4 p = permute( permute( permute( 
             i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
           + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) 
           + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));

// Gradients: 7x7 points over a square, mapped onto an octahedron.
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
  float n_ = 0.142857142857; // 1.0/7.0
  vec3  ns = n_ * D.wyz - D.xzx;

  vec4 j = p - 49.0 * floor(p * ns.z * ns.z);  //  mod(p,7*7)

  vec4 x_ = floor(j * ns.z);
  vec4 y_ = floor(j - 7.0 * x_ );    // mod(j,N)

  vec4 x = x_ *ns.x + ns.yyyy;
  vec4 y = y_ *ns.x + ns.yyyy;
  vec4 h = 1.0 - abs(x) - abs(y);

  vec4 b0 = vec4( x.xy, y.xy );
  vec4 b1 = vec4( x.zw, y.zw );

  //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
  //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
  vec4 s0 = floor(b0)*2.0 + 1.0;
  vec4 s1 = floor(b1)*2.0 + 1.0;
  vec4 sh = -step(h, vec4(0.0));

  vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
  vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;

  vec3 p0 = vec3(a0.xy,h.x);
  vec3 p1 = vec3(a0.zw,h.y);
  vec3 p2 = vec3(a1.xy,h.z);
  vec3 p3 = vec3(a1.zw,h.w);

//Normalise gradients
  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
  p0 *= norm.x;
  p1 *= norm.y;
  p2 *= norm.z;
  p3 *= norm.w;

// Mix final noise value
  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
  m = m * m;
  return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), 
                                dot(p2,x2), dot(p3,x3) ) );
}

/// Cloud stuff:
const float maximum = 1.0/1.0 + 1.0/2.0 + 1.0/3.0 + 1.0/4.0 + 1.0/5.0 + 1.0/6.0 + 1.0/7.0 + 1.0/8.0;
// Fractal Brownian motion, or something that passes for it anyway: range [-1, 1]
float fBm(vec3 uv)
{
    float sum = 0.0;
    for (int i = 0; i < 8; ++i) {
        float f = float(i+1);
        sum += snoise(uv*f) / f;
    }
    return sum / maximum;
}

// Simple vertical gradient:
float gradient(vec2 uv) {
 	return (1.0 - uv.y * uv.y * cloudHeight);   
}

void main() {
    vec2 uv = vUv;
    vec3 p = vec3(uv, iTime * speed);
    vec3 offset = vec3(sin(iTime * fogSpeed*3.0), sin(iTime * fogSpeed), sin(iTime * fogSpeed*2.0));
    vec2 duv = vec2(fBm(p), fBm(p + offset)) * noisiness;
    float q = gradient(uv + duv) * cloudDensity;
    if(q <= 0.0) {
        discard;
        return;
    }
    gl_FragColor = vec4(vec3(1.0), q * fogOpacity);
}

刮风起雾材质

const fogMaterial = new THREE.ShaderMaterial({
              transparent: true,
              uniforms: {
                iTime: { value: 1.0 },//随时间变化
                fogOpacity: { value: 1.0 },//雾气透明度
                fogSpeed: { value: 1.0 }//雾运动的速度
              },
              vertexShader: ``,
              fragmentShader:``,
});

20240506_215659.gif

可以通过fogOpacity调整雾气透明度和fogSpeed调整雾气运动速度。

Github地址

https://github.com/xiaolidan00/my-earth 20240506_220449.gif


网站公告

今日签到

点亮在社区的每一天
去签到