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);
}

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

相关推荐
GIS程序媛—椰子28 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00135 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端38 分钟前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x41 分钟前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟100941 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习