three.js WaterMesh 完全指南:基于 WebGPU 与 TSL 的平面反射水面效果
导读
WaterMesh 是 three.js 中一个基于 TSL(Three Shading Language) 与 WebGPU 渲染管线实现的平面反射水面效果类。它继承自 Mesh,通过一张法线贴图驱动水面扰动、结合实时平面反射(Flat Mirror)与太阳高光/水色散射,以极少的代码即可在场景中铺开一片具有动态波纹与真实反射的水面。本文以 WaterMesh.html.md 文档为主体,结合其源码实现 examples/jsm/objects/WaterMesh.js 与官方示例 examples/webgpu_ocean.html 进行纵深讲解,读完你将掌握 WaterMesh 的构造方式、全部参数语义、着色器原理,以及它在实际场景中的集成与调参方法。
适用前提:WaterMesh 依赖 WebGPU 后端(
WebGPURenderer)与 TSL 节点系统,只能在支持 WebGPU 的浏览器中运行;如果使用传统WebGLRenderer,请改用其 WebGL 版本 Water(见 WaterMesh 源码注释)。
WaterMesh 是什么:一个“扁平的反射水面”
从源码结构看,WaterMesh 是一个轻量封装类:构造函数内部创建了一个 NodeMaterial,把所有水面渲染逻辑以 TSL 节点图的形式挂载到材质上,然后以 super( geometry, material ) 的方式继承自 Mesh(见 examples/jsm/objects/WaterMesh.js#L25-L37):
import { Mesh, NodeMaterial } from 'three/webgpu';
import { Fn, ... } from 'three/tsl';
class WaterMesh extends Mesh {
constructor( geometry, options ) {
const material = new NodeMaterial();
super( geometry, material );
// ... 初始化 uniform 与 TSL 着色逻辑
}
}
其核心视觉构成可以概括为四层:
- 法线扰动:对一张法线贴图做四次不同频率/偏移的采样并叠加,形成不断流动的水面法线(即“噪声”层)。
- 平面反射(Flat Mirror):通过 TSL 的
reflector()节点实时渲染一张镜面反射纹理,并按法线扰动结果进行 UV 扭曲,得到“水波中的倒影”。 - 菲涅尔混合(Fresnel Mix):根据视线与法线的夹角,在“水色散射 + 漫反射”与“镜面反射 + 太阳高光”之间混合,模拟近处透明、远处镜面的真实水体。
- 阴影扭曲接收:利用
receivedShadowPositionNode让投射到水面上的阴影随波扰动,避免出现“水面是平的”穿帮。
快速上手:最小可运行示例
由于它是 addon,必须显式导入,导入路径为 three/addons/objects/WaterMesh.js(对应仓库中的 examples/jsm/objects/WaterMesh.js):
import { WaterMesh } from 'three/addons/objects/WaterMesh.js';
在 WebGPU 渲染器下创建一片水面的最小代码如下:
// 1. 创建 WebGPU 渲染器(WaterMesh 的硬性前提)
const renderer = new THREE.WebGPURenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
// 2. 加载法线贴图并开启重复平铺(水面 UV 会大范围平铺)
const loader = new THREE.TextureLoader();
const waterNormals = loader.load( 'textures/waternormals.jpg' );
waterNormals.wrapS = waterNormals.wrapT = THREE.RepeatWrapping;
// 3. 创建几何体 + WaterMesh
const waterGeometry = new THREE.PlaneGeometry( 10000, 10000 );
const water = new WaterMesh( waterGeometry, {
waterNormals: waterNormals,
sunDirection: new THREE.Vector3(),
sunColor: 0xffffff,
waterColor: 0x001e0f,
distortionScale: 3.7
} );
// 4. 让水面水平放置并加入场景
water.rotation.x = - Math.PI / 2;
scene.add( water );
以上用法直接取自官方示例 examples/webgpu_ocean.html#L100-L118。注意两点:
- 水面的几何体通常是一个很大的
PlaneGeometry,通过rotation.x = -Math.PI/2放平; waterNormals贴图需要设置为RepeatWrapping,否则大面积水面会出现采样越界问题。
构造函数详解
new WaterMesh( geometry : BufferGeometry, options : WaterMesh~Options )
- geometry:水面的几何体,通常为
PlaneGeometry(或任何BufferGeometry)。 - options:可选配置对象,完整字段见下文“Options 配置选项”一节。所有字段都有默认值,因此
new WaterMesh( geometry )也可以直接运行。
运行前提与依赖
WaterMesh 只能在 WebGPU 渲染器下工作,代码层面需要同时引入 WebGPU 命名空间与 TSL 模块,这在官方示例的 importmap 中有明确体现(examples/webgpu_ocean.html#L29-L38):
{
"imports": {
"three": "../build/three.webgpu.js",
"three/webgpu": "../build/three.webgpu.js",
"three/tsl": "../build/three.tsl.js",
"three/addons/": "./jsm/"
}
}
TSL 水面着色还会用到 reflector() 节点(来自 src/nodes/utils/ReflectorNode.js),它是整个平面反射效果的底层实现。
Options 配置选项(完整参数表)
构造函数接受的所有选项及默认值如下,与文档 WaterMesh.html.md 一一对应,且能在源码 examples/jsm/objects/WaterMesh.js#L180-L192 的类型定义中得到验证:
| 选项 | 类型 | 默认值 | 含义 |
|---|---|---|---|
resolutionScale |
number | 0.5 |
反射纹理的分辨率缩放比例。 |
waterNormals |
Texture | null |
水面法线贴图。 |
alpha |
number | 1 |
水面整体透明度。 |
size |
number | 1 |
法线噪声的尺度(波纹疏密)。 |
sunColor |
number | Color | string | 0xffffff |
太阳(高光)颜色。 |
sunDirection |
Vector3 | (0.70707, 0.70707, 0.0) |
太阳方向(将被归一化使用)。 |
waterColor |
number | Color | string | 0x7F7F7F |
水体本身的散射颜色。 |
distortionScale |
number | 20 |
反射/阴影 UV 扭曲强度。 |
各参数在源码中的落点
在构造函数中,这些选项被分别转换为 UniformNode 或 TextureNode(examples/jsm/objects/WaterMesh.js#L54-L111):
this.resolutionScale = options.resolutionScale !== undefined ? options.resolutionScale : 0.5;
this.waterNormals = texture( options.waterNormals ); // TextureNode
this.alpha = uniform( options.alpha ?? 1.0 ); // UniformNode<float>
this.size = uniform( options.size ?? 1.0 ); // UniformNode<float>
this.sunColor = uniform( new Color( options.sunColor ?? 0xffffff ) ); // UniformNode<color>
this.sunDirection = uniform( options.sunDirection ?? new Vector3( 0.70707, 0.70707, 0.0 ) ); // UniformNode<vec3>
this.waterColor = uniform( new Color( options.waterColor ?? 0x7f7f7f ) ); // UniformNode<color>
this.distortionScale = uniform( options.distortionScale ?? 20.0 ); // UniformNode<float>
理解这一点对运行时调参至关重要:所有 Uniform 参数在运行时都可以直接通过 .value 读写。例如官方示例中通过 GUI 实时调整水面参数的方式(examples/webgpu_ocean.html#L197-L199):
folderWater.add( water.distortionScale, 'value', 0, 8, 0.1 ).name( 'distortionScale' );
folderWater.add( water.size, 'value', 0.1, 10, 0.1 ).name( 'size' );
以及根据太阳方位实时更新 sunDirection(examples/webgpu_ocean.html#L153):
water.sunDirection.value.copy( sun ).normalize();
属性(Properties)速查
创建后的 WaterMesh 实例暴露以下属性(均与文档一致):
- .alpha : UniformNode<float> — 透明度,默认
1。 - .distortionScale : UniformNode<float> — 扭曲强度,默认
20。 - .isWaterMesh : boolean(只读)— 类型检测标志,默认
true。可用于在对象列表中区分水面对象:if ( object.isWaterMesh ) { /* 是水面 */ } - .resolutionScale : number — 反射效果的分辨率缩放,默认
0.5。注意它是普通 number 而非 Uniform。 - .size : UniformNode<float> — 噪声尺度,默认
1。 - .sunColor : UniformNode<color> — 太阳颜色,默认
0xffffff。 - .sunDirection : UniformNode<vec3> — 太阳方向,默认
(0.70707, 0.70707, 0.0)。 - .waterColor : UniformNode<color> — 水色,默认
0x7f7f7f。 - .waterNormals : TextureNode — 法线贴图节点。
原理纵深:TSL 着色器是如何“画”出水面的
以下内容基于 examples/jsm/objects/WaterMesh.js#L113-L174 的源码逐段解析,属于从源码结构推断的实现细节,可用于理解参数之间的联动关系。
第一步:四层法线采样生成流动噪声
const getNoise = Fn( ( [ uv ] ) => {
const offset = time;
const uv0 = add( div( uv, 103 ), vec2( div( offset, 17 ), div( offset, 29 ) ) ).toVar();
const uv1 = div( uv, 107 ).sub( vec2( div( offset, - 19 ), div( offset, 31 ) ) ).toVar();
const uv2 = add( div( uv, vec2( 8907.0, 9803.0 ) ), vec2( div( offset, 101 ), div( offset, 97 ) ) ).toVar();
const uv3 = sub( div( uv, vec2( 1091.0, 1027.0 ) ), vec2( div( offset, 109 ), div( offset, - 113 ) ) ).toVar();
const sample0 = this.waterNormals.sample( uv0 );
const sample1 = this.waterNormals.sample( uv1 );
const sample2 = this.waterNormals.sample( uv2 );
const sample3 = this.waterNormals.sample( uv3 );
const noise = sample0.add( sample1 ).add( sample2 ).add( sample3 );
return noise.mul( 0.5 ).sub( 1 );
} );
实现要点:
- 输入 UV 为
positionWorld.xz(世界坐标的 XZ 平面)乘以this.size,因此 size 越大,波纹越稀疏;size 越小,波纹越细密; - 四组采样使用不同的除数(103/107/8907/1091 等质数)和不同的时间偏移频率(17/29/-19/31/101/97/109/-113),使得四层法线以不同速度、不同方向流动,叠加后既避免规律重复,又能形成持续的动态涌动感;
- 最终结果
* 0.5 - 1把叠加值重新映射到合理的法线取值区间。
第二步:扰动法线与视线几何
const noise = getNoise( positionWorld.xz.mul( this.size ) );
const surfaceNormal = normalize( noise.xzy.mul( 1.5, 1.0, 1.5 ) );
const worldToEye = cameraPosition.sub( positionWorld );
const eyeDirection = normalize( worldToEye );
- 取噪声的 X、Z 分量并按
(1.5, 1.0, 1.5)缩放后重组为法线方向,让水面法线主要沿 XZ 平面波动; - 计算“世界位置 → 相机”的视线方向,供后续菲涅尔与高光计算使用。
第三步:太阳高光(镜面反射高光)
const reflection = normalize( reflect( this.sunDirection.negate(), surfaceNormal ) );
const direction = max( 0.0, dot( eyeDirection, reflection ) );
const specularLight = pow( direction, 100 ).mul( this.sunColor ).mul( 2.0 );
- 将太阳方向关于扰动后的法线做反射,得到反射光线;
- 反射光线与视线方向点积的 100 次幂形成一个非常尖锐的镜面高光(指数 100 意味着光斑小而亮,类似阳光洒在水面的碎光);
- 高光强度以
sunColor * 2.0放大。
第四步:水色漫反射(散射)
const diffuseLight = max( dot( this.sunDirection, surfaceNormal ), 0.0 ).mul( this.sunColor ).mul( 0.5 );
- 太阳方向与法线的点积产生朗伯式漫反射强度,再以
sunColor * 0.5着色,为水面提供基础的受光明暗变化。
第五步:基于距离的扭曲强度
const distance = length( worldToEye );
const distortion = surfaceNormal.xz
.mul( float( 0.001 ).add( float( 1.0 ).div( distance ) ) )
.mul( this.distortionScale );
- 扭曲量由法线 XZ 分量乘以
(0.001 + 1/distance)再乘distortionScale得到; - 由于包含
1/distance,近处水面扭曲更强、远处更平缓,符合透视下水面近大远小的视觉规律,distortionScale则是全局强度的调节旋钮。
第六步:材质组装——反射、菲涅尔与透明度
material.transparent = true;
material.opacityNode = this.alpha;
material.receivedShadowPositionNode = positionWorld.add( distortion );
transparent = true与opacityNode = alpha共同实现半透明水面;receivedShadowPositionNode将接收阴影的采样位置加上扭曲量,使阴影也能随波“浮动”,增强真实感。
material.colorNode = Fn( () => {
const mirrorSampler = reflector();
mirrorSampler.uvNode = mirrorSampler.uvNode.add( distortion );
mirrorSampler.reflector.resolutionScale = this.resolutionScale;
this.add( mirrorSampler.target );
const theta = max( dot( eyeDirection, surfaceNormal ), 0.0 );
const rf0 = float( 0.02 );
const reflectance = mul( pow( float( 1.0 ).sub( theta ), 5.0 ), float( 1.0 ).sub( rf0 ) ).add( rf0 );
const scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ).mul( this.waterColor );
const albedo = mix( this.sunColor.mul( diffuseLight ).mul( 0.3 ).add( scatter ), mirrorSampler.rgb.add( specularLight ), reflectance );
return albedo;
} )();
这里有几个关键点值得展开:
reflector()是反射核心:它来自 src/nodes/utils/ReflectorNode.js,底层通过resolutionScale控制反射纹理的渲染分辨率(该参数在 r180 由resolution更名而来,源码中保留了兼容性告警,见 ReflectorNode.js#L256-L260)。因此resolutionScale = 0.5意味着反射内容以一半分辨率渲染,属于明显的性能与画质权衡项:调高更清晰但更耗性能,调低更省资源。this.add( mirrorSampler.target ):反射器会创建一个虚拟相机目标对象,WaterMesh 把它挂到自己名下,从而参与反射场景的渲染。- 菲涅尔公式:
reflectance = (1-rf0) * (1 - cosθ)^5 + rf0(Schlick 近似,rf0 = 0.02),θ 为视线与法线夹角——视线越接近水面(掠射角),反射占比越高;视线越垂直于水面,透射(水色散射)占比越高。 - 最终颜色:在“太阳漫反射弱化版 + 水色散射”与“反射纹理 + 太阳高光”之间按菲涅尔系数线性插值,得到既有通透水色、又有清晰倒影的最终像素。
与反射分辨率相关的底层约定
ReflectorNode 的构造函数默认参数为 resolutionScale = 1、generateMipmaps = false、bounces = true(src/nodes/utils/ReflectorNode.js#L224-L231),而 WaterMesh 将 resolutionScale 默认压低到 0.5,可见其设计取向是“默认偏性能、按需提高画质”。
实战:在官方 Ocean 示例中把 WaterMesh 用起来
官方示例 examples/webgpu_ocean.html 是 WaterMesh 最完整的集成范例,展示了水面与天空、后处理协同工作的完整链路:
- 场景与环境:创建
WebGPURenderer,开启ACESFilmicToneMapping,并配合SkyMesh(TSL 天空)提供环境光照(examples/webgpu_ocean.html#L64-L70)。 - 水面创建:
PlaneGeometry(10000, 10000)搭配textures/waternormals.jpg法线贴图(仓库中该贴图位于 examples/textures/waternormals.jpg),并设置RepeatWrapping。 - 太阳联动:通过
sun.setFromSphericalCoords()由仰角/方位角计算太阳方向,同时写入sky.sunPosition与water.sunDirection.value(examples/webgpu_ocean.html#L145-L163),实现“太阳位置 → 天空光照 + 水面高光”的同步。 - 后处理叠加:用
pass()捕获场景,叠加bloom()泛光(threshold=0、strength=0.1、radius=0),让水面高光与天空产生柔和的辉光(examples/webgpu_ocean.html#L84-L92)。 - 实时调参:通过 Inspector 对
water.distortionScale.value(0~8)与water.size.value(0.1~10)做范围限定的实时调整。
参考该示例,你可以把 WaterMesh 推广到湖面、泳池、游戏关卡水体等场景:只需替换 waterColor(如深色 0x001e0f 适合海水、浅色 0x99e0ff 适合泳池)、调节 distortionScale 控制波浪强度、用 sunDirection 跟随场景主光源即可。
使用注意事项
- 仅限 WebGPU:WaterMesh 依赖
three/webgpu与three/tsl模块,WebGL 环境会报错。传统 WebGL 场景请使用 Water。 - 法线贴图必须 RepeatWrapping:水面 UV 依赖大范围平铺采样,不开启会导致边界明显或采样异常。
resolutionScale是性能关键:反射是额外的场景渲染,0.5已是折中值;水面占屏面积大、对倒影清晰度要求高时可尝试提升到0.75~1,并观察帧率影响。- 透明排序:
material.transparent = true意味着水面参与透明渲染,若场景中存在其他半透明物体,可像官方示例那样调整water.renderOrder保证正确的混合顺序。
参考资料与延伸阅读
- 本文主体文档:docs/pages/WaterMesh.html.md
- 核心源码实现:examples/jsm/objects/WaterMesh.js
- 官方集成示例:examples/webgpu_ocean.html
- 反射节点底层实现:src/nodes/utils/ReflectorNode.js
- WebGL 版水面(WebGLRenderer 场景):examples/jsm/objects/Water.js
- 进阶版水面(支持折射与流向贴图,同为 WebGPU):examples/jsm/objects/Water2Mesh.js
- 水面法线贴图示例:examples/textures/waternormals.jpg
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00