Three.js 中 GPGPU 水面模拟示例代码深度解析
对于刚接触 Three.js 的开发者来说,GPGPU(通用计算着色器)听起来可能很复杂,但其实这个水面模拟示例的核心逻辑可以拆解成简单易懂的模块!今天我们就从基础视角解析这个示例,还会教你如何快速把效果集成到自己的项目里。
一、先搞懂:这个示例到底在做什么?
简单来说,这个示例实现了:
- 用 GPU 模拟真实的水面波动(比如丢石子的涟漪效果)
- 鼠标点按/拖动时,水面会跟着产生波纹
- 物体能'浮'在水面上,跟着水波晃动
- 水面有反光、折射,看起来像真的水
你可以把它想象成:用代码做了一个'电子水池',还能伸手进去搅水玩~
二、核心模块拆解
1. 基础环境搭建:先搭好'舞台'
就像演戏要先搭舞台一样,Three.js 做 3D 效果也需要先准备基础环境:
// 创建相机(相当于你的眼睛)
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 创建场景(放所有 3D 物体的'盒子')
scene = new THREE.Scene();
// 创建渲染器(负责把 3D 场景画到网页上)
renderer = new THREE.WebGLRenderer({ antialias: true });
// 把渲染结果放到网页里
document.body.appendChild(renderer.domElement);
这三行是 Three.js 的'标配',几乎所有项目都要写,记住就好!
2. GPGPU 水面计算:让水'动'起来
这里是核心,但我们不用懂复杂的 GPU 原理,只需要知道:
- 用
GPUComputationRenderer帮我们算水面波动 - 用'高度图'(一张看不见的图片)记录每个位置的水面高度
- 着色器(Shader)负责计算波纹怎么扩散
// 初始化 GPU 计算工具(128x128 是计算精度,数字越大效果越好但越卡)
gpuCompute = new GPUComputationRenderer(128, 128, renderer);
// 创建高度图(存水面高度的'秘密图片')
const heightmap = gpuCompute.createTexture();
// 添加计算任务:让着色器处理水面波动
heightmapVariable = gpuCompute.addVariable('heightmap', shaderCode, heightmap);
你可以把 GPU 计算理解成'找了个超级计算器专门算水面',比 CPU 算得快 10 倍!
3. 水面渲染:把计算结果画出来
有了水面高度数据,还要用'材质'把水画出来:
// 自定义水面材质(继承 Three.js 自带的材质)
class WaterMaterial extends THREE.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 = new THREE.PlaneGeometry(6, 6);
const waterMesh = new THREE.Mesh(waterGeometry, new WaterMaterial());
waterMesh.rotation.x = -Math.PI / 2; // 把平面旋转成水平(默认是垂直的)
scene.add(waterMesh);
PlaneGeometry 是'平面',旋转后就成了水面;材质负责让它看起来像水~
4. 鼠标交互:用鼠标'搅水'
想要鼠标点水时产生波纹,只需要两步:
- 检测鼠标点在水面的哪个位置
- 把位置传给 GPU 计算着色器
// 鼠标按下时记录位置
document.addEventListener('pointerdown', () => { mousedown = true; });
// 鼠标移动时计算波纹
function raycast() {
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 坐标(上下)设成水面高度
function updateDuck() {
// 读取水面在鸭子位置的高度
const waterHeight = getWaterHeight(duck.position.x, duck.position.z);
// 让鸭子跟着水面高度走
duck.position.y = waterHeight;
}
就像你站在蹦床上,蹦床凹下去你也会跟着下去~
三、快速集成指南
下面是精简版代码,直接复制到 HTML 文件就能运行:
<!DOCTYPE html>
<html>
<head>
<title>我的水面效果</title>
<style>body{margin: 0;}</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { GPUComputationRenderer } from 'three/addons/misc/GPUComputationRenderer.js';
// 1. 基础环境
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.set(0, 2, 4);
// 2. 初始化 GPU 计算
const gpuCompute = new GPUComputationRenderer(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. 创建水面
class WaterMaterial extends THREE.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 = new THREE.PlaneGeometry(6, 6, 128, 128);
const waterMat = new WaterMaterial();
const waterMesh = new THREE.Mesh(waterGeo, waterMat);
waterMesh.rotation.x = -Math.PI / 2;
scene.add(waterMesh);
// 5. 鼠标交互
const raycaster = new THREE.Raycaster();
const mouse = new THREE.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. 动画循环
function animate() {
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 算水面高度 → 用材质画水面 → 加交互让水'活'起来
哪怕你是零基础,只要复制上面的代码,改改参数(比如水面大小、颜色、波纹强度),就能快速做出自己的水面效果!后续可以再慢慢深入学习着色器和物理公式。


