带深度信息的下雨特效的实现
上一篇技术文章three.js 模拟真实海洋(超详细教程,炫酷海洋) - 掘金 (juejin.cn) 发布后,获得了许多小伙伴的收藏,如果还没有看的可以去看看,效果真的很好,实现的每一步真的很清楚。
也有小伙伴让我先把效果放到文章的最前面,所以这次先看效果。
阅读之前简略了解一下原理。
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);
}