【11】深入解析 Three.js 中的 GPGPU水面模拟示例代码(webgl_gpgpu_water.html)
探秘Three.js:官方GPGPU水面模拟示例深度解析💧
(零基础友好版 | 附快速集成指南)
对于刚接触Three.js的开发者来说,GPGPU(通用计算着色器)听起来可能很复杂,但其实这个水面模拟示例的核心逻辑可以拆解成简单易懂的模块!今天我们就从零基础视角解析这个示例,还会教你如何快速把效果集成到自己的项目里🌊

一、先搞懂:这个示例到底在做什么?
简单来说,这个示例实现了:
✅ 用GPU模拟真实的水面波动(比如丢石子的涟漪效果)
✅ 鼠标点按/拖动时,水面会跟着产生波纹
✅ 小黄鸭能"浮"在水面上,跟着水波晃动
✅ 水面有反光、折射,看起来像真的水
你可以把它想象成:用代码做了一个"电子水池",还能伸手进去搅水玩~
二、核心模块拆解(零基础也能懂)
1. 基础环境搭建:先搭好"舞台"
就像演戏要先搭舞台一样,Three.js做3D效果也需要先准备基础环境:
// 创建相机(相当于你的眼睛) camera =newTHREE.PerspectiveCamera(75, 窗口宽/窗口高,0.1,1000);// 创建场景(放所有3D物体的"盒子") scene =newTHREE.Scene();// 创建渲染器(负责把3D场景画到网页上) renderer =newTHREE.WebGLRenderer({ antialias:true});// 把渲染结果放到网页里 document.body.appendChild(renderer.domElement);零基础小贴士:这三行是Three.js的"标配",几乎所有项目都要写,记住就好!
2. GPGPU水面计算:让水"动"起来
这里是核心,但我们不用懂复杂的GPU原理,只需要知道:
👉 用GPUComputationRenderer帮我们算水面波动
👉 用"高度图"(一张看不见的图片)记录每个位置的水面高度
👉 着色器(Shader)负责计算波纹怎么扩散
// 初始化GPU计算工具(128x128是计算精度,数字越大效果越好但越卡) gpuCompute =newGPUComputationRenderer(128,128, renderer);// 创建高度图(存水面高度的"秘密图片")const heightmap = gpuCompute.createTexture();// 添加计算任务:让着色器处理水面波动 heightmapVariable = gpuCompute.addVariable('heightmap', 着色器代码, heightmap);零基础小贴士:你可以把GPU计算理解成"找了个超级计算器专门算水面",比CPU算得快10倍!
3. 水面渲染:把计算结果画出来
有了水面高度数据,还要用"材质"把水画出来:
// 自定义水面材质(继承Three.js自带的材质)classWaterMaterialextendsTHREE.MeshStandardMaterial{onBeforeCompile(shader){// 关键:根据高度图让顶点"凸起",形成波浪 shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>',`float height = texture2D(heightmap, uv).x; vec3 transformed = vec3(position.x, position.y + height, position.z);`);}}// 创建水面(用平面几何体+水面材质)const waterGeometry =newTHREE.PlaneGeometry(6,6);// 6x6是水面大小const waterMesh =newTHREE.Mesh(waterGeometry,newWaterMaterial()); waterMesh.rotation.x =-Math.PI/2;// 把平面旋转成水平(默认是垂直的) scene.add(waterMesh);零基础小贴士:PlaneGeometry是"平面",旋转后就成了水面;材质负责让它看起来像水~
4. 鼠标交互:用鼠标"搅水"
想要鼠标点水时产生波纹,只需要两步:
👉 检测鼠标点在水面的哪个位置
👉 把位置传给GPU计算着色器
// 鼠标按下时记录位置 document.addEventListener('pointerdown',()=>{ mousedown =true;});// 鼠标移动时计算波纹functionraycast(){if(mousedown){// 用射线检测鼠标点到的水面位置 raycaster.setFromCamera(mouseCoords, camera);const intersects = raycaster.intersectObject(waterMesh);if(intersects.length >0){// 把鼠标位置传给GPU,让这里产生波纹 heightmapVariable.material.uniforms.mousePos.value = intersects[0].point;}}}零基础小贴士:射线检测就像"从鼠标位置发射一道激光",看激光打到哪个物体上~
5. 小黄鸭漂浮:让物体跟着水动
小黄鸭能浮在水面,核心是:
👉 读取水面在小黄鸭位置的高度
👉 把小黄鸭的Y坐标(上下)设成水面高度
functionupdateDuck(){// 读取水面在鸭子位置的高度const waterHeight =getWaterHeight(duck.position.x, duck.position.z);// 让鸭子跟着水面高度走 duck.position.y = waterHeight;}零基础小贴士:就像你站在蹦床上,蹦床凹下去你也会跟着下去~
三、快速集成到你的项目(复制即用)
下面是精简版代码,直接复制到HTML文件就能运行:
<!DOCTYPEhtml><html><head><title>我的水面效果</title><style>body{margin: 0;}</style></head><body><scripttype="importmap">{"imports":{"three":"https://unpkg.com/[email protected]/build/three.module.js","three/addons/":"https://unpkg.com/[email protected]/examples/jsm/"}}</script><scripttype="module">import*asTHREEfrom'three';import{ GPUComputationRenderer }from'three/addons/misc/GPUComputationRenderer.js';// 1. 基础环境const scene =newTHREE.Scene();const camera =newTHREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight,0.1,1000);const renderer =newTHREE.WebGLRenderer({ antialias:true}); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); camera.position.set(0,2,4);// 2. 初始化GPU计算const gpuCompute =newGPUComputationRenderer(128,128, renderer);const heightmap = gpuCompute.createTexture();// 填充初始高度(随便填点噪声让水面有初始波动)const data = heightmap.image.data;for(let i =0; i < data.length; i +=4){ data[i]= Math.random()*0.1;// 初始高度}// 3. 水面计算着色器(核心:波纹扩散公式)const heightmapShader =` uniform vec2 mousePos; uniform float mouseSize; void main() { vec2 uv = gl_FragCoord.xy / resolution.xy; vec4 prev = texture2D(heightmap, uv); // 简单波纹公式(不用懂,复制就行) float dist = length(uv * 6 - mousePos); float wave = dist < mouseSize ? 0.1 : 0.0; gl_FragColor = vec4(prev.x * 0.95 + wave, 0, 0, 1); } `;const heightmapVariable = gpuCompute.addVariable('heightmap', heightmapShader, heightmap); gpuCompute.init();// 4. 创建水面classWaterMaterialextendsTHREE.MeshStandardMaterial{constructor(){super({ color:0x9bd2ec, metalness:0.9, roughness:0});}onBeforeCompile(shader){ shader.uniforms.heightmap ={ value:null}; shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>',`float h = texture2D(heightmap, uv).x; vec3 transformed = vec3(position.x, position.y + h, position.z);`);}}const waterGeo =newTHREE.PlaneGeometry(6,6,128,128);const waterMat =newWaterMaterial();const waterMesh =newTHREE.Mesh(waterGeo, waterMat); waterMesh.rotation.x =-Math.PI/2; scene.add(waterMesh);// 5. 鼠标交互const raycaster =newTHREE.Raycaster();const mouse =newTHREE.Vector2();let mousedown =false; window.addEventListener('pointerdown',()=> mousedown =true); window.addEventListener('pointerup',()=> mousedown =false); window.addEventListener('pointermove',(e)=>{ mouse.x =(e.clientX / window.innerWidth)*2-1; mouse.y =-(e.clientY / window.innerHeight)*2+1;});// 6. 动画循环functionanimate(){requestAnimationFrame(animate);// 计算水面 gpuCompute.compute(); waterMat.heightmap = gpuCompute.getCurrentRenderTarget(heightmapVariable).texture;// 鼠标交互if(mousedown){ raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObject(waterMesh);if(intersects.length >0){const p = intersects[0].point; heightmapVariable.material.uniforms.mousePos.value = p; heightmapVariable.material.uniforms.mouseSize.value =0.5;}} renderer.render(scene, camera);}animate();</script></body></html>四、零基础避坑指南
- 运行方式:必须用本地服务器(比如VSCode的Live Server插件),直接双击HTML文件会报错!
- 性能问题:如果卡,把
128改成64(计算精度降低,效果稍差但更流畅) - 水面颜色:改
0x9bd2ec就能换颜色(比如0x00ffff是浅蓝色) - 波纹大小:改
mouseSize.value = 0.5里的0.5,数字越大波纹范围越广
五、总结
其实Three.js的GPGPU水面效果并没有想象中难,核心就是:
👉 用GPU算水面高度 → 用材质画水面 → 加交互让水"活"起来
哪怕你是零基础,只要复制上面的代码,改改参数(比如水面大小、颜色、波纹强度),就能快速做出自己的水面效果!后续可以再慢慢深入学习着色器和物理公式~🚀