【06】基于Three.js下雨粒子特效的代码解析(webgpu_compute_particles_rain.html)
这段 Three.js 代码实现了包含粒子系统模拟、碰撞检测、多种 3D 物体添加以及动画渲染等丰富的功能。希望大家能从中理解相关的实现思路,运用到自己的开发实践中,进一步探索 Three.js 带来的强大 3D 开发能力。

一、引言
在Web开发中,利用Three.js可以创建出令人惊叹的3D场景和特效。本文将对一段利用粒子效果实现下雨场景的Three.js代码进行详细解析,官方示例地址:https://github.com/mrdoob/three.js/blob/master/examples/webgpu_compute_particles_rain.html这段代码实现了诸如粒子模拟、碰撞效果等一系列有趣的视觉效果,希望通过解析能帮助读者更好地理解Three.js的相关功能和使用方式。
二、代码模块导入
代码起始处通过以下导入语句引入了所需的库和模块:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
- import * as THREE from 'three';:引入 Three.js 核心库,这是整个 3D 场景搭建、渲染等操作的基础,后续像创建相机、几何体等都依赖它。
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';:导入轨道控制器,用于方便地控制相机在场景中的观察角度,比如通过鼠标拖动来旋转、缩放和平移视角。
- import Stats from 'three/addons/libs/stats.module.js';:引入性能统计模块,能够实时展示帧率等性能指标,便于开发者了解场景渲染的效率情况。
- import { GUI } from 'three/addons/libs/lil-gui.module.min.js';:导入图形用户界面模块,可创建交互控件来动态调整场景中的参数,增强场景的交互性。
- import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';:用于处理缓冲几何体的工具模块,比如合并多个几何体等操作会用到它。
三、核心功能代码解析
(一)场景与相机初始化
const { innerWidth, innerHeight } = window;
camera = new THREE.PerspectiveCamera( 60, innerWidth / innerHeight, 0.1, 110 );
camera.position.set( 40, 8, 0 );
camera.lookAt( 0, 0, 0 );
scene = new THREE.Scene();
- 首先获取浏览器窗口的宽高信息,接着创建了一个透视相机 PerspectiveCamera。其中,60 表示视角角度,宽高比根据窗口宽高动态计算,0.1 和 110 分别是近裁剪面和远裁剪面距离,设置好相机位置并让其看向原点。然后创建了一个空的 Scene 对象,后续所有的 3D 元素都会添加到这个场景中。
(二)灯光设置
const dirLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
dirLight.castShadow = true;
dirLight.position.set( 3, 17, 17 );
// 更多阴影相机参数设置
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 50;
// 等其他阴影相机边界参数设置
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.bias = - 0.01;
scene.add( dirLight );
scene.add( new THREE.AmbientLight( 0x111111 ) );
核心功能是创建了一个平行光 DirectionalLight,颜色为白色,强度 0.5,并开启阴影投射。然后细致配置了其阴影相机的相关参数,像近远裁剪面距离、阴影贴图尺寸以及阴影偏差等,来确保阴影效果良好。同时添加了一个环境光 AmbientLight,颜色较暗,作用是给整个场景提供基础、均匀的光照,避免场景出现过暗区域。
(三)粒子系统相关
1. 粒子属性数组创建
const positionBuffer = instancedArray( maxParticleCount, 'vec3' );
const velocityBuffer = instancedArray( maxParticleCount, 'vec3' );
const ripplePositionBuffer = instancedArray( maxParticleCount, 'vec3' );
const rippleTimeBuffer = instancedArray( maxParticleCount, 'vec3' );
利用 instancedArray 函数创建了多个用于存储粒子不同属性(位置、速度、涟漪位置、涟漪时间)的实例化数组,元素类型为 vec3,数量由 maxParticleCount 决定,为后续粒子模拟计算提供数据基础。
2. 粒子初始化与更新计算
const computeInit = Fn( () => {
const position = positionBuffer.element( instanceIndex );
const velocity = velocityBuffer.element( instanceIndex );
const rippleTime = rippleTimeBuffer.element( instanceIndex );
// 基于随机等逻辑初始化粒子位置、速度、涟漪时间等属性
const randX = hash( instanceIndex );
const randY = hash( instanceIndex.add( randUint() ) );
const randZ = hash( instanceIndex.add( randUint() ) );
position.x = randX.mul( 100 ).add( - 50 );
position.y = randY.mul( 25 );
position.z = randZ.mul( 100 ).add( - 50 );
velocity.y = randX.mul( - 0.04 ).add( - 0.2 );
rippleTime.x = 1000;
} )().compute( maxParticleCount );
const computeUpdate = Fn( () => {
const position = positionBuffer.element( instanceIndex );
const velocity = velocityBuffer.element( instanceIndex );
position.addAssign( velocity );
// 还有更多复杂的碰撞、涟漪相关属性更新逻辑
} );
computeParticles = computeUpdate().compute( maxParticleCount );
- computeInit 函数主要负责粒子的初始化,通过获取对应实例索引的粒子属性元素,利用随机函数(如 randUint、hash 等辅助函数)来初始化粒子的位置、速度和涟漪时间等关键属性,最后通过 compute 方法对所有粒子执行初始化计算。
- computeUpdate 函数实现粒子的更新逻辑,先简单更新粒子位置(基于速度),还有后续复杂的涉及碰撞检测(比如判断粒子是否与地面碰撞等)、涟漪效果相关属性更新的代码逻辑,最终通过 compute 方法完成所有粒子的更新计算,并将结果赋值给 computeParticles 供渲染使用。
3. 粒子材质与物体创建
const rainMaterial = new THREE.MeshBasicNodeMaterial();
rainMaterial.colorNode = uv().distance( vec2( 0.5, 0 ) ).oneMinus().mul( 3 ).exp().mul( 0.1 );
rainMaterial.vertexNode = billboarding( { position: positionBuffer.toAttribute() } );
rainMaterial.opacity = 0.2;
rainMaterial.side = THREE.DoubleSide;
rainMaterial.forceSinglePass = true;
rainMaterial.depthWrite = false;
rainMaterial.depthTest = true;
rainMaterial.transparent = true;
const rainParticles = new THREE.Mesh( new THREE.PlaneGeometry( 0.1, 2 ), rainMaterial );
rainParticles.count = instanceCount;
scene.add( rainParticles );
这里创建了雨滴粒子的材质 rainMaterial,其颜色通过纹理坐标等相关计算得出,顶点节点通过 billboarding 函数实现类似广告牌效果(让粒子始终朝向相机),同时设置了透明度、渲染面、深度相关等诸多属性来呈现出透明、双面渲染等符合雨滴特性的视觉效果。接着创建了平面几何体作为雨滴形状,结合材质创建 rainParticles 网格对象并添加到场景中,指定了粒子数量为 instanceCount。
(四)碰撞检测相关
collisionCamera = new THREE.OrthographicCamera( - 50, 50, 50, - 50, 0.1, 50 );
collisionCamera.position.y = 50;
collisionCamera.lookAt( 0, 0, 0 );
collisionCamera.layers.disableAll();
collisionCamera.layers.enable( 1 );
collisionPosRT = new THREE.RenderTarget( 1024, 1024 );
collisionPosRT.texture.type = THREE.HalfFloatType;
collisionPosRT.texture.magFilter = THREE.NearestFilter;
collisionPosRT.texture.minFilter = THREE.NearestFilter;
collisionPosRT.texture.generateMipmaps = false;
collisionPosMaterial = new THREE.MeshBasicNodeMaterial();
collisionPosMaterial.colorNode = positionWorld;
- 创建了一个正交相机 collisionCamera,用于碰撞检测相关的渲染,设置其位置、看向的目标以及通过图层管理只启用特定图层(图层 1)。
- 创建了一个渲染目标 collisionPosRT,配置其纹理属性,比如类型、过滤方式等,这个渲染目标可用于将场景渲染到纹理上以便后续做碰撞检测操作。
- 定义了一个材质 collisionPosMaterial,其颜色节点关联 positionWorld,可能是基于物体在世界空间的位置来辅助碰撞检测相关的视觉呈现(具体依赖 positionWorld 的实现逻辑)。
(五)场景中的其他物体添加
const floorGeometry = new THREE.PlaneGeometry( 1000, 1000 );
floorGeometry.rotateX( - Math.PI / 2 );
const plane = new THREE.Mesh( floorGeometry, new THREE.MeshBasicMaterial( { color: 0x050505 } ) );
scene.add( plane );
collisionBox = new THREE.Mesh( new THREE.BoxGeometry( 30, 1, 15 ), new THREE.MeshStandardMaterial() );
collisionBox.material.color.set( 0x333333 );
collisionBox.position.y = 12;
collisionBox.scale.x = 3.5;
collisionBox.layers.enable( 1 );
collisionBox.castShadow = true;
scene.add( collisionBox );
const loader = new THREE.BufferGeometryLoader();
loader.load( 'models/json/suzanne_buffergeometry.json', function ( geometry ) {
geometry.computeVertexNormals();
monkey = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial( { roughness: 1, metalness: 0 } ) );
monkey.receiveShadow = true;
monkey.scale.setScalar( 5 );
monkey.rotation.y = Math.PI / 2;
monkey.position.y = 4.5;
monkey.layers.enable( 1 );
scene.add( monkey );
} );
- 创建了一个大的平面几何体作为地面,旋转后使其平放,赋予一个深色材质并添加到场景中。
- 创建了一个长方体的碰撞箱物体,设置其材质、颜色、位置、缩放等属性,开启阴影投射并添加到特定图层(图层 1)后添加到场景中,用于模拟碰撞区域。
- 通过模型加载器加载一个特定的模型文件(此处是猴子模型),在加载回调中处理模型的顶点法线计算,然后创建对应的网格对象,配置其材质、接收阴影、缩放、旋转和位置等属性后添加到场景中。
(六)渲染与交互相关设置
clock = new THREE.Clock();
renderer = new THREE.WebGPURenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );
stats = new Stats();
document.body.appendChild( stats.dom );
controls = new OrbitControls( camera, renderer.domElement );
controls.minDistance = 5;
controls.maxDistance = 50;
controls.update();
window.addEventListener( 'resize', onWindowResize );
- 创建 Clock 对象用于跟踪时间,方便在动画循环中获取时间增量来实现基于时间的动画更新。
- 创建 WebGPURenderer 渲染器(如果支持 WebGPU),设置抗锯齿、像素比以及渲染尺寸等,指定动画循环的回调函数为 animate,将渲染器 DOM 元素添加到页面,使场景能显示出来。同时创建 Stats 对象展示性能信息,添加到页面。
- 创建轨道控制器 OrbitControls 并设置相机的最小、最大观察距离,调用 update 方法初始化。添加窗口大小改变的监听器,当窗口变化时调用 onWindowResize 函数调整相机和渲染器参数,保证场景正确显示。
(七)动画循环函数
function animate() {
stats.update();
const delta = clock.getDelta();
if (monkey) {
monkey.rotation.y += delta;
}
// 碰撞箱位置更新逻辑
collisionBoxPos.set( collisionBoxPosUI.x, collisionBoxPosUI.y, - collisionBoxPosUI.z );
collisionBox.position.lerp( collisionBoxPos, 10 * delta );
// 碰撞检测相关渲染
scene.overrideMaterial = collisionPosMaterial;
renderer.setRenderTarget( collisionPosRT );
renderer.render( scene, collisionCamera );
// 粒子计算渲染
renderer.compute( computeParticles );
// 最终场景渲染
scene.overrideMaterial = null;
renderer.setRenderTarget( null );
renderer.render( scene, camera );
}
在 animate 函数中,首先更新性能统计信息,获取时间增量 delta,利用它来更新猴子模型的旋转角度实现旋转动画效果。接着处理碰撞箱的位置更新,通过线性插值让其位置平滑变化。然后进行碰撞检测相关的渲染操作,将场景渲染到特定的渲染目标上,使用之前定义的碰撞检测相关材质等。之后执行粒子计算渲染,最后将场景以正常状态(清除覆盖材质、设置渲染目标为默认)渲染到相机对应的画面上,完成一帧的动画更新和渲染展示。
四、总结
通过对上述核心功能代码的解析,我们可以看到这段 Three.js 代码实现了包含粒子系统模拟、碰撞检测、多种 3D 物体添加以及动画渲染等丰富的功能。希望大家能从中理解相关的实现思路,运用到自己的开发实践中,进一步探索 Three.js 带来的强大 3D 开发能力。
更多推荐
所有评论(0)