一、引言

在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 开发能力。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐