引言
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 场景,包括相机、灯光、物体的设置,如何进行粒子特效的实现,以及碰撞检测、后期处理等功能。希望本文对你有帮助!