首页
/ three.js WaterMesh 完全指南:基于 WebGPU 与 TSL 的平面反射水面效果

three.js WaterMesh 完全指南:基于 WebGPU 与 TSL 的平面反射水面效果

2026-09-08 11:39:15作者:管翌锬

导读

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 着色逻辑
	}
}

其核心视觉构成可以概括为四层:

  1. 法线扰动:对一张法线贴图做四次不同频率/偏移的采样并叠加,形成不断流动的水面法线(即“噪声”层)。
  2. 平面反射(Flat Mirror):通过 TSL 的 reflector() 节点实时渲染一张镜面反射纹理,并按法线扰动结果进行 UV 扭曲,得到“水波中的倒影”。
  3. 菲涅尔混合(Fresnel Mix):根据视线与法线的夹角,在“水色散射 + 漫反射”与“镜面反射 + 太阳高光”之间混合,模拟近处透明、远处镜面的真实水体。
  4. 阴影扭曲接收:利用 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 扭曲强度。

各参数在源码中的落点

在构造函数中,这些选项被分别转换为 UniformNodeTextureNodeexamples/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' );

以及根据太阳方位实时更新 sunDirectionexamples/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 = trueopacityNode = 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 = 1generateMipmaps = falsebounces = truesrc/nodes/utils/ReflectorNode.js#L224-L231),而 WaterMesh 将 resolutionScale 默认压低到 0.5,可见其设计取向是“默认偏性能、按需提高画质”。

实战:在官方 Ocean 示例中把 WaterMesh 用起来

官方示例 examples/webgpu_ocean.html 是 WaterMesh 最完整的集成范例,展示了水面与天空、后处理协同工作的完整链路:

  1. 场景与环境:创建 WebGPURenderer,开启 ACESFilmicToneMapping,并配合 SkyMesh(TSL 天空)提供环境光照(examples/webgpu_ocean.html#L64-L70)。
  2. 水面创建PlaneGeometry(10000, 10000) 搭配 textures/waternormals.jpg 法线贴图(仓库中该贴图位于 examples/textures/waternormals.jpg),并设置 RepeatWrapping
  3. 太阳联动:通过 sun.setFromSphericalCoords() 由仰角/方位角计算太阳方向,同时写入 sky.sunPositionwater.sunDirection.valueexamples/webgpu_ocean.html#L145-L163),实现“太阳位置 → 天空光照 + 水面高光”的同步。
  4. 后处理叠加:用 pass() 捕获场景,叠加 bloom() 泛光(threshold=0strength=0.1radius=0),让水面高光与天空产生柔和的辉光(examples/webgpu_ocean.html#L84-L92)。
  5. 实时调参:通过 Inspector 对 water.distortionScale.value(0~8)与 water.size.value(0.1~10)做范围限定的实时调整。

参考该示例,你可以把 WaterMesh 推广到湖面、泳池、游戏关卡水体等场景:只需替换 waterColor(如深色 0x001e0f 适合海水、浅色 0x99e0ff 适合泳池)、调节 distortionScale 控制波浪强度、用 sunDirection 跟随场景主光源即可。

使用注意事项

  • 仅限 WebGPU:WaterMesh 依赖 three/webgputhree/tsl 模块,WebGL 环境会报错。传统 WebGL 场景请使用 Water
  • 法线贴图必须 RepeatWrapping:水面 UV 依赖大范围平铺采样,不开启会导致边界明显或采样异常。
  • resolutionScale 是性能关键:反射是额外的场景渲染,0.5 已是折中值;水面占屏面积大、对倒影清晰度要求高时可尝试提升到 0.75~1,并观察帧率影响。
  • 透明排序material.transparent = true 意味着水面参与透明渲染,若场景中存在其他半透明物体,可像官方示例那样调整 water.renderOrder 保证正确的混合顺序。

参考资料与延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390