引言
Three.js 是一个强大的 JavaScript 库,用于在网页上创建和渲染 3D 场景。本文将深入分析一段 Three.js 官网示例代码,详细解释其实现思路和主要功能代码,帮助读者更好地理解和掌握 Three.js 的应用。官网代码地址:https://github.com/mrdoob/three.js/blob/master/examples/webgpu_compute_particles_snow.html
代码整体架构
代码主要实现了一个包含粒子特效、场景物体(如地板、树、茶壶等)的 3D 场景,并进行了碰撞检测、粒子计算更新、场景渲染以及后期处理等操作。整体分为初始化、更新计算、渲染等几个主要部分。
主要功能代码解析
初始化部分
init();
async function init() {
// 相机设置
camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
camera.position.set(20, 2, 20);
camera.layers.enable(2);
camera.lookAt(0, 40, 0);
// 场景设置
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0f3c37, 5, 40);
// 灯光设置
const dirLight = new THREE.DirectionalLight(0xf9ff9b, 9);
// 灯光阴影相关设置
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 30;
//...更多阴影设置
scene.add(dirLight);
scene.add(new THREE.HemisphereLight(0x0f3c37, 0x080d10, 100));
// 碰撞相机和渲染目标设置
collisionCamera = new THREE.OrthographicCamera(-50, 50, 50, -50, 0.1, 50);
collisionCamera.position.y = 50;
collisionCamera.lookAt(0, 0, 0);
collisionCamera.layers.enable(1);
collisionPosRT = new THREE.RenderTarget(1024, 1024);
// 渲染目标纹理设置
collisionPosRT.texture.type = THREE.HalfFloatType;
//...更多纹理设置
collisionPosMaterial = new THREE.MeshBasicNodeMaterial();
collisionPosMaterial.fog = false;
collisionPosMaterial.toneMapped = false;
collisionPosMaterial.colorNode = positionWorld.y;
// 粒子相关缓冲区设置
const positionBuffer = instancedArray(maxParticleCount, 'vec3');
const scaleBuffer = instancedArray(maxParticleCount, 'vec3');
const staticPositionBuffer = instancedArray(maxParticleCount, 'vec3');
const dataBuffer = instancedArray(maxParticleCount, 'vec4');
// 粒子初始化计算
const computeInit = Fn(() => {
// 计算粒子初始位置、缩放等
const position = positionBuffer.element(instanceIndex);
const scale = scaleBuffer.element(instanceIndex);
const particleData = dataBuffer.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(500).add(3);
position.z = randZ.mul(100).add(-50);
scale.xyz = hash(instanceIndex.add(Math.random())).mul(0.8).add(0.2);
staticPositionBuffer.element(instanceIndex).assign(vec3(1000, 10000, 1000));
particleData.y = randY.mul(-0.1).add(-0.02);
particleData.x = position.x;
particleData.z = position.z;
particleData.w = randX;
})().compute(maxParticleCount);
// 场景物体添加
const geometry = new THREE.SphereGeometry(surfaceOffset, 5, 5);
const dynamicParticles = particle();
const staticParticles = particle(true);
scene.add(dynamicParticles);
scene.add(staticParticles);
const floorGeometry = new THREE.PlaneGeometry(100, 100);
floorGeometry.rotateX(-Math.PI / 2);
const plane = new THREE.Mesh(floorGeometry, new THREE.MeshStandardMaterial({
color: 0x0c1e1e,
roughness: 0.5,
metalness: 0,
transparent: true
}));
plane.material.opacityNode = positionLocal.xz.mul(0.05).distance(0).saturate().oneMinus();
scene.add(plane);
scene.add(tree());
const teapotTree = new THREE.Mesh(new TeapotGeometry(0.5, 18), new THREE.MeshBasicNodeMaterial({
color: 0xfcfb9e
}));
teapotTree.position.y = 18;
scene.add(teapotTree);
// 场景背景设置
scene.backgroundNode = screenUV.distance(0.5).mul(2).mix(color(0x0f4140), color(0x060a0d));
// 渲染器、统计工具、控制设置
renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animate);
stats = new Stats({
precision: 3,
horizontal: false
});
stats.init(renderer);
controls = new OrbitControls(camera, renderer.domElement);
// 控制参数设置
controls.target.set(0, 10, 0);
//...更多控制参数设置
controls.update();
// 后期处理设置
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode();
const vignette = screenUV.distance(0.5).mul(1.35).clamp().oneMinus();
const teapotTreePass = pass(teapotTree, camera).getTextureNode();
const teapotTreePassBlurred = gaussianBlur(teapotTreePass, vec2(1), 3);
teapotTreePassBlurred.resolution = new THREE.Vector2(0.2, 0.2);
const scenePassColorBlurred = gaussianBlur(scenePassColor);
scenePassColorBlurred.resolution = new THREE.Vector2(0.5, 0.5);
scenePassColorBlurred.directionNode = vec2(1);
let totalPass = scenePass;
totalPass = totalPass.add(scenePassColorBlurred.mul(0.1));
totalPass = totalPass.mul(vignette);
totalPass = totalPass.add(teapotTreePass.mul(10).add(teapotTreePassBlurred));
postProcessing = new THREE.PostProcessing(renderer);
postProcessing.outputNode = totalPass;
await renderer.computeAsync(computeInit);
window.addEventListener('resize', onWindowResize);
}
- 相机与场景设置:创建了透视相机
PerspectiveCamera
,设置了视角、位置、看向点等参数。同时创建了场景Scene
,并设置了雾效。 - 灯光设置:添加了方向光
DirectionalLight
和半球光HemisphereLight
,并对方向光的阴影进行了详细设置。 - 碰撞检测相关设置:创建了正交相机
OrthographicCamera
用于碰撞检测,设置了渲染目标RenderTarget
和材质MeshBasicNodeMaterial
,用于获取碰撞位置信息。 - 粒子初始化:通过
instancedArray
创建了多个粒子相关的缓冲区,用于存储粒子的位置、缩放等信息。computeInit
函数计算粒子的初始位置、缩放和其他属性。 - 场景物体添加:创建了粒子、地板、树、茶壶等物体,并添加到场景中。每个物体都有其特定的几何形状和材质设置。
- 渲染器、统计和控制设置:创建了
WebGPURenderer
渲染器,设置了色调映射、像素比等参数。同时初始化了统计工具Stats
和相机控制OrbitControls
。 - 后期处理设置:通过
pass
函数获取场景和茶壶树的纹理节点,应用高斯模糊gaussianBlur
等效果,最后通过PostProcessing
组合这些效果。
更新计算部分
const surfaceOffset = 0.2;
const speed = 0.4;
const computeUpdate = Fn(() => {
const getCoord = (pos) => pos.add(50).div(100);
const position = positionBuffer.element(instanceIndex);
const scale = scaleBuffer.element(instanceIndex);
const particleData = dataBuffer.element(instanceIndex);
const velocity = particleData.y;
const random = particleData.w;
const rippleOnSurface = texture(collisionPosRT.texture, getCoord(position.xz));
const rippleFloorArea = rippleOnSurface.y.add(scale.x.mul(surfaceOffset));
If(position.y.greaterThan(rippleFloorArea), () => {
position.x = particleData.x.add(time.mul(random.mul(random)).mul(speed).sin().mul(3));
position.z = particleData.z.add(time.mul(random).mul(speed).cos().mul(random.mul(10)));
position.y = position.y.add(velocity);
}).Else(() => {
staticPositionBuffer.element(instanceIndex).assign(position);
});
});
computeParticles = computeUpdate().compute(maxParticleCount);
computeUpdate
函数用于更新粒子的位置。通过获取碰撞位置纹理信息,判断粒子是否在某个表面之上,从而决定粒子的运动方式。如果粒子在表面之上,根据一些随机和时间相关的计算更新粒子的位置;否则将粒子位置存储到静态位置缓冲区。
渲染部分
async function animate() {
controls.update();
scene.overrideMaterial = collisionPosMaterial;
renderer.setRenderTarget(collisionPosRT);
await renderer.renderAsync(scene, collisionCamera);
await renderer.computeAsync(computeParticles);
scene.overrideMaterial = null;
renderer.setRenderTarget(null);
await postProcessing.renderAsync();
stats.update();
}
animate
函数是动画循环的核心。在每一帧中,首先更新相机控制,然后设置场景的覆盖材质,通过碰撞相机渲染场景到碰撞位置渲染目标,接着计算粒子的更新,恢复场景的正常材质,最后进行后期处理的渲染,并更新统计信息。
总结
通过对这段 Three.js 代码的详细解析,我们了解了如何创建复杂的 3D 场景,包括相机、灯光、物体的设置,如何进行粒子特效的实现,以及碰撞检测、后期处理等功能。希望本文对你有帮助!