three.js 下雨特效(高级版本)很干!!很难!!很详细

带深度信息的下雨特效的实现

上一篇技术文章three.js 模拟真实海洋(超详细教程,炫酷海洋) - 掘金 (juejin.cn) 发布后,获得了许多小伙伴的收藏,如果还没有看的可以去看看,效果真的很好,实现的每一步真的很清楚。

也有小伙伴让我先把效果放到文章的最前面,所以这次先看效果。

Gitee源码

阅读之前简略了解一下原理。

1. 什么是深度?

深度,深度,深度就是三维世界中的坐标点,经过 MVP(modelMatrix viewMatrix projectionMatrix) 变化后,映射到相机空间的坐标中的 z 值范围 [0,1],z越大,距离相机越远。

2. 在WEBGL获取渲染信息

WEBGL中渲染信息存在 FBO(FrameBufferObject) 中,可以创建获取,详细百度哈,这里不是重点。

3. 在 three.js 中获取渲染信息

THREE.WebGLRenderTarget() 做了很好的封装,可以帮我们给三维场景自由拍照,并且获取到信息。

了解完了?那么!开始吧!

1. 基础模板的创建就直接跳过了

2. 首先确定下雨特效所在的空间,这里用一个BOX描述这个空间。然后在中间放置一个平面,用于挡雨。

ini 复制代码
    box = new THREE.Box3(
        new THREE.Vector3(-200, 0, -200),
        new THREE.Vector3(200, 200, 200)
    );
    
    const geometry = new THREE.PlaneGeometry(100, 400)
    geometry.rotateX(-Math.PI / 2)

    const mesh = new THREE.Mesh(
        geometry,
        new THREE.MeshBasicMaterial({ side: THREE.DoubleSide })
    );
    mesh.position.y = 100
    scene.add(mesh);

是不是很简单? 效果如下:

3. 渲染深度图

ini 复制代码
    // 创建 renderTarget
    target = new THREE.WebGLRenderTarget(WIDTH, HEIGHT);
    target.texture.format = THREE.RGBFormat;
    target.texture.minFilter = THREE.NearestFilter;
    target.texture.magFilter = THREE.NearestFilter;
    target.texture.generateMipmaps = false;

    // 创建正交相机
    orthCamera = new THREE.OrthographicCamera();
    const center = new THREE.Vector3();
    box.getCenter(center);
    
    // 根据 BOX 设置正交相机的参数
    orthCamera.left = box.min.x - center.x;
    orthCamera.right = box.max.x - center.x;
    orthCamera.top = box.max.z - center.z;
    orthCamera.bottom = box.min.z - center.z;
    orthCamera.near = .1;
    orthCamera.far = box.max.y - box.min.y;
    
    // 设置正交相机的位置在 BOX 正上方
    orthCamera.position.copy(center);
    orthCamera.position.y += box.max.y - center.y;
    orthCamera.lookAt(center);

    // 更新正交相机投影矩阵和世界矩阵
    orthCamera.updateProjectionMatrix();
    orthCamera.updateWorldMatrix()

    // 创建个 helper 看看效果
    const helper = new THREE.CameraHelper(orthCamera)
    scene.add(helper);

正交相机创建完成了。待会用这个相机渲染depthScene获取深度信息。

创建 depthScene 。 Scene.overrideMaterial 如果存在,会对场景中的每一个对象,强制使用这个材质渲染,这个场景只需要获取深度信息,所以这里覆盖所有对象本身的材质。

ini 复制代码
    // 创建场景2,用于绘制深度图
    depthScene = new THREE.Scene();
    depthScene.overrideMaterial = new THREE.ShaderMaterial({
        vertexShader: `
        varying float color;

        void main() {
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          color = gl_Position.z / 2.0 + 0.5;
        }
      `,
        fragmentShader: `
        varying float color;
        
        vec4 encodeFloat2RGBA(float v)
        {
            vec4 enc = vec4(1.0, 255.0, 65025.0, 16581375.0) * v;
            enc = fract(enc);
            enc -= enc.yzww * vec4(1.0/255.0, 1.0/255.0, 1.0/255.0, 0.0);
            return enc;
        }
        void main() {
            gl_FragColor = encodeFloat2RGBA(1.0 - color);
        }
      `,
    });

获取到正交相机渲染的贴图了,贴图里的颜色代表深度哦。

ini 复制代码
    // 在这一步之后的渲染,渲染信息将会保存在target中,这里的信息是绘制深度的场景和正交相机。
    renderer.setRenderTarget(target);
    depthScene.children = [plane];
    renderer.render(depthScene, orthCamera);
    renderer.setRenderTarget(null);

4. 深度图有了,雨呢?先创建几何体吧。

雨水用 Mesh 创建,为什么不用 Points 呢? Points 创建的点会永远朝向我们哦。在上方会穿帮的。

创建几何体,如下图,单个雨滴,是不是很简单,我们创建 6000 个吧。

ini 复制代码
 const geometry = new THREE.BufferGeometry();

    const vertices = [];
    const poses = [];
    const uvs = [];
    const indices = [];

    for (let i = 0; i < 6000; i++) {
        const pos = new THREE.Vector3();
        pos.x = Math.random() * (box.max.x - box.min.x) + box.min.x;
        pos.y = Math.random() * (box.max.y - box.min.y) + box.min.y;
        pos.z = Math.random() * (box.max.z - box.min.z) + box.min.z;

        const height = (box.max.y - box.min.y) / 15;
        const width = height / 50;

        vertices.push(
            pos.x + width,
            pos.y + height,
            pos.z,
            pos.x - width,
            pos.y + height,
            pos.z,
            pos.x - width,
            pos.y,
            pos.z,
            pos.x + width,
            pos.y,
            pos.z
        );

        poses.push(
            pos.x,
            pos.y,
            pos.z,
            pos.x,
            pos.y,
            pos.z,
            pos.x,
            pos.y,
            pos.z,
            pos.x,
            pos.y,
            pos.z
        );

        uvs.push(1, 1, 0, 1, 0, 0, 1, 0);

        indices.push(
            i * 4 + 0,
            i * 4 + 1,
            i * 4 + 2,
            i * 4 + 0,
            i * 4 + 2,
            i * 4 + 3
        );
    }

    geometry.setAttribute(
        "position",
        new THREE.BufferAttribute(new Float32Array(vertices), 3)
    );
    geometry.setAttribute(
        "pos",
        new THREE.BufferAttribute(new Float32Array(poses), 3)
    );
    geometry.setAttribute(
        "uv",
        new THREE.BufferAttribute(new Float32Array(uvs), 2)
    );
    geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));

材质创建

需要雨滴在水平方向上,永远朝向相机。所以我们将相机位置传入着色器。

ini 复制代码
    material = new THREE.MeshBasicMaterial({
        transparent: true,
        opacity: 0.8,
        depthWrite: false,
    });

    material.onBeforeCompile = function (shader, renderer) {
        const getFoot = `
            attribute vec3 pos;
            uniform float top;
            uniform float bottom;
            uniform float time;
            uniform mat4 cameraMatrix;
            varying float depth;
            varying vec2 depthUv;
            #include <common>
            float angle(float x, float y){
                return atan(y, x);  
            }
            
            // 计算更新过后的顶点坐标的偏移
            vec2 getFoot(vec2 camera,vec2 _n_pos,vec2 pos){
                vec2 position;

                float distanceLen = distance(pos, _n_pos);

                float a = angle(camera.x - _n_pos.x, camera.y - _n_pos.y);

                pos.x > _n_pos.x ? a -= 0.785 : a += 0.785; 

                position.x = cos(a) * distanceLen;
                position.y = sin(a) * distanceLen;
                
                return position + _n_pos;
            }
            `;
        const begin_vertex = `

            float height = top - bottom;

            vec3 _n_pos = vec3(pos.x, pos.y- height/30.,pos.z);

            vec2 foot = getFoot(vec2(cameraPosition.x, cameraPosition.z),  vec2(_n_pos.x, _n_pos.z), vec2(position.x, position.z));
            
            
            // 模拟雨滴下落位置。 Bottom -> Bottom + Height 是雨滴下落空间。
            float y = _n_pos.y - bottom - height * fract(time);
            y += y < 0.0 ? height : 0.0;
     
            // 雨滴下落的百分比,即是雨滴的深度。 [0,1] 空间
            depth = (1.0 - y / height) ;
    
            // 更新顶点位置
            y += bottom;
            y += position.y - _n_pos.y;
            vec3 transformed = vec3( foot.x, y, foot.y );
            
            // 将顶点坐标与正交相机的投影矩阵的逆矩阵进行运算,得到顶点坐标在 [-1,1] 三维空间的数据。
            vec4 cameraDepth = cameraMatrix * vec4(transformed, 1.0);
            
            // 采样 uv
            depthUv = cameraDepth.xy/2.0 + 0.5;
            `;

        const depth_vary = `
            uniform sampler2D tDepth;
            uniform float opacity;
            varying float depth;
            varying vec2 depthUv;

            float decodeRGBA2Float(vec4 rgba)
            {
                return dot(rgba, vec4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 16581375.0));
            }
            `;

        const depth_frag = `
        
            // 对比深度值,如果深度值满足关系,不进行渲染。
            if(1.0 - depth < decodeRGBA2Float(texture2D( tDepth, depthUv ))) discard;
            vec4 diffuseColor = vec4( diffuse, opacity );
            `
        shader.vertexShader = shader.vertexShader.replace(
            "#include <common>",
            getFoot
        );
        shader.vertexShader = shader.vertexShader.replace(
            "#include <begin_vertex>",
            begin_vertex
        );
        shader.fragmentShader = shader.fragmentShader.replace('uniform float opacity;', depth_vary)
        shader.fragmentShader = shader.fragmentShader.replace('vec4 diffuseColor = vec4( diffuse, opacity );', depth_frag)

        shader.uniforms.cameraPosition = {
            value: new THREE.Vector3(0, 200, 0)
        }
        shader.uniforms.top = {
            value: box.max.y
        }
        shader.uniforms.bottom = {
            value: box.min.y
        }
        shader.uniforms.time = {
            value: 0
        }

        shader.uniforms.cameraMatrix = {
            value: new THREE.Matrix4()
        }
        shader.uniforms.tDepth = {
            value: target.texture
        }
        material.uniforms = shader.uniforms;
    };

正交相机拍的深度图类似这样。其中的颜色由顶点的 Z 值决定

最后就是执行渲染了,如果需要动态获取深度图(例如,模型撑着雨伞在移动),可以在动画帧中执行哦,我下面注释了。

ini 复制代码
function render() {
    time = clock.getElapsedTime() / 2;

    if (material.uniforms) {
        material.uniforms.cameraPosition.value = camera.position;
        material.uniforms.time.value = time;

        material.uniforms.cameraMatrix.value = new THREE.Matrix4().multiplyMatrices(
            orthCamera.projectionMatrix,
            orthCamera.matrixWorldInverse
        );

        // renderer.setRenderTarget(target);
        // renderer.render(depthScene, orthCamera);
        // renderer.setRenderTarget(null);
        // material.uniforms.tDepth.value = target.texture;
    }

    renderer.render(scene, camera);
}

到此为止。下班。给个收藏给个赞!!!

相关推荐
ytttr8734 分钟前
5G毫米波射频前端设计:从GaN功放到混合信号集成方案
前端·5g·生成对抗网络
水鳜鱼肥6 分钟前
Github Spark 革新应用,重构未来
前端·人工智能
前端李二牛30 分钟前
现代CSS属性兼容性问题及解决方案
前端·css
贰月不是腻月1 小时前
凭什么说我是邪修?
前端
中等生1 小时前
一文搞懂 JavaScript 原型和原型链
前端·javascript
前端李二牛1 小时前
现代化图片组件设计思路与实现方案
前端·html
黑椒牛肉焖饭1 小时前
web第一次作业
前端·javascript·html
一枚前端小能手1 小时前
Vue3 开发中的5个实用小技巧
前端
Sawtone1 小时前
shadcn/ui:我到底是不是组件库啊😭图文 + 多个场景案例详解 shadcn + tailwind 颠覆性组件开发,小伙伴直呼高端
前端·面试
柏成1 小时前
qiankun 微前端框架🐳
前端·javascript·vue.js