three.js NormalMapNode 法线贴图节点完全指南:TSL 用法、属性配置与源码原理
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
本篇技术指南聚焦于 three.js 渲染节点(Node/TSL)体系中的NormalMapNode(法线贴图节点),它负责将法线贴图(Normal Map)数据转换为参与光照计算的法线方向,是 PBR 材质细节表现的关键一环。读者将掌握normalMap()TSL 函数与material.normalNode的接入方式、node/scaleNode/normalMapType/unpackNormalMode四大属性的含义与配置,并通过源码级剖析理解从法线采样值到切线空间/物体空间法线的完整计算管线。
NormalMapNode 是什么
NormalMapNode是 three.js 节点材质系统中的一个TempNode,文档中的定位一句话即可概括:This class can be used for applying normals maps to materials.——将一张记录法线扰动信息的贴图应用到材质上,使平坦的表面产生凹凸起伏的视觉假象,从而在不增加几何体面数的情况下大幅提升细节表现。
其继承关系为:EventDispatcher → Node → TempNode → NormalMapNode,这意味着它继承了 Node 的事件分发、求值(build/setup)能力,也继承了TempNode作为可复用临时表达式的特性——同一节点可被安全地在多处引用并输出为着色器中的中间值,而不必重复构造开销(参见 TempNode 源码)。
在 TSL(Three Shading Language)代码中,它通常以如下形式出现:
material.normalNode = normalMap( texture( normalTex ) );即通过texture()采样法线贴图得到 RGB 颜色,再交给normalMap()这一 TSL 工厂函数生成NormalMapNode实例,最终挂载到材质的.normalNode插槽上。
构造函数与签名
文档给出的构造签名如下:
new NormalMapNode( node : Node.<vec3>, scaleNode : Node.<vec2> )两个参数的语义如下:
| 参数 | 类型 | 说明 |
|---|---|---|
node | Node.<vec3> | 表示法线贴图数据。通常是texture( normalTex )的采样输出(vec3 颜色值)。 |
scaleNode | Node.<vec2> | 控制法线效果的强度(intensity)。默认值为null。 |
其中scaleNode的类型标注为vec2是文档中的描述;从实际实现看,它参与对 XY 分量的缩放(见下文setup()分析),传入标量、向量或常数值皆可被 TSL 数学运算兼容。
TSL 工厂函数:normalMap()
在实际工程中,开发者很少直接new NormalMapNode(...),而是使用同文件导出的 TSL 函数normalMap。其声明位于 NormalMapNode.js 源码 尾部:
export const normalMap = /*@__PURE__*/ nodeProxy( NormalMapNode ).setParameterLength( 1, 2 );- 它由
nodeProxy()生成,支持参数数量在1~2个之间变化:只传法线贴图节点时scaleNode默认为null,与构造函数默认值保持一致; @__PURE__注解便于打包器(如 Rollup/Tree-shaking)将其作为纯表达式做死代码消除与常量提升。
由于该函数在 Nodes.js 与 TSL.js 中均有再导出,因此既可通过import { normalMap } from 'three/nodes'使用,也可通过 TSL 命名空间访问。
实例属性详解
文档列出的四个属性,在 NormalMapNode.js 构造函数中被逐一初始化:
.node : Node.
表示法线贴图数据本身,直接透传构造参数。在setup()阶段会先对它执行mul( 2.0 ).sub( 1.0 ),把存储在颜色通道[0, 1]范围内的贴图值重映射到向量方向[-1, 1]范围——这是所有法线贴图解包的第一步。
.scaleNode : Node.
控制法线效果的强度,默认null。当它为null时,法线直接使用重映射后的 XY 分量;非空时,法线的 XY 分量会乘以缩放系数再重组 Z 分量。该缩放仅作用于切平面方向(XY),不影响法线 Z 的朝向,因此数值增大表现为「更夸张的凹凸」,减小则趋于平坦。
.normalMapType : TangentSpaceNormalMap | ObjectSpaceNormalMap
法线贴图的类型,默认是TangentSpaceNormalMap。它在 constants.js 中被定义为两个整数常量:
| 常量 | 值 | 语义 |
|---|---|---|
TangentSpaceNormalMap | 0 | 法线信息相对于底层表面(切线空间),随表面曲率变化,通常来自 DCC 工具烘焙的普通法线贴图; |
ObjectSpaceNormalMap | 1 | 法线信息相对于物体朝向(物体空间),法线值是模型自身的空间方向,不随表面切线变化。 |
.unpackNormalMode : string
控制采样得到的法线贴图值如何被解包,默认是NoNormalPacking。相关常量同样定义在 constants.js:
| 常量 | 值 | 用途 |
|---|---|---|
NoNormalPacking | '' | 不做额外解包,使用标准 RGB 全通道法线(RGB = XYZ); |
NormalRGPacking | 'rg' | 法线 XY 编码在 R、G 通道,Z 由 XY 重建(RG 压缩格式,通常配合RGBE/双通道纹理节省带宽); |
NormalGAPacking | 'ga' | 法线 XY 编码在 G、A 通道,对应 DX 系常见的 BC5 双通道压缩布局。 |
从源码结构看,默认的完整 RGB 法线贴图(NoNormalPacking)无需额外分支;而'rg'/'ga'两种打包会调用 Packing.js 中的unpackNormal( xy )函数,通过vec3( xy, sqrt( saturate( 1 - dot( xy, xy ) ) ) )在单位半球上重建 Z 分量。
源码级原理:setup() 计算管线
节点的核心逻辑集中在 setup() 方法,它完整揭示了文档所述各项能力如何落地:
第一步:颜色重映射为方向向量
let normalMap = this.node.mul( 2.0 ).sub( 1.0 );将贴图采样值从颜色域映射到方向域。注意,这一映射发生在解包模式判断之前,因此'rg'压缩数据同样需要先经历本步再做 Z 重建。
第二步:按 unpackNormalMode 解包(仅切线空间)
if ( normalMapType === TangentSpaceNormalMap ) { if ( unpackNormalMode === NormalRGPacking ) { normalMap = unpackNormal( normalMap.xy ); } else if ( unpackNormalMode === NormalGAPacking ) { normalMap = unpackNormal( normalMap.yw ); } else if ( unpackNormalMode !== NoNormalPacking ) { error( `THREE.NodeMaterial: Unexpected unpack normal mode: ...` ); } }- RG 打包取
xy、GA 打包取yw,二者都通过 unpackNormal 重建缺失分量; - 遇到未知的解包模式会调用
error()并中断;物体空间贴图与压缩打包组合使用时同样会抛错(源码 L96-L100),这是合法的输入约束,因为物体空间法线通常以全 RGB 存储。
第三步:应用强度缩放(scaleNode)
if ( scaleNode !== null ) { let scale = scaleNode; if ( builder.isFlatShading() === true ) { scale = negateOnBackSide( scale ); } normalMap = vec3( normalMap.xy.mul( scale ), normalMap.z ); }- 只有
scaleNode非空才参与计算,XY 分量被缩放,Z 分量保持不变; - 一个值得注意的实现细节:当材质启用平面着色(flat shading)时,缩放值会被 FrontFacingNode.js 中的
negateOnBackSide()包裹,使背面片元的缩放自动取反,避免背面法线方向错误——这是许多手写 shader 容易遗漏的边界处理。
第四步:空间变换输出
if ( normalMapType === ObjectSpaceNormalMap ) { output = transformNormalToView( normalMap ); } else if ( normalMapType === TangentSpaceNormalMap ) { output = TBNViewMatrix.mul( normalMap ).normalize(); } else { error( `NodeMaterial: Unsupported normal map type: ...` ); output = normalView; // 回退到默认法线 }- 切线空间:法线贴图记录的是相对于表面 TBN(切线/副切线/法线)的方向,因此需通过视空间 TBN 矩阵转换。
TBNViewMatrix定义于 AccessorsUtils.js,即mat3( tangentView, bitangentView, normalView ),再执行矩阵乘法与normalize()归一化; - 物体空间:法线值本身就是物体坐标方向,直接调用
transformNormalToView()(来自 Normal.js 访问器)转换到视空间即可; - 未知类型会触发运行时错误并回退到默认视空间法线
normalView,保证着色器仍能编译而不至于黑屏。
由此可以看到,setup()把「解包 → 缩放 → 空间变换」三条管线串成一张有向无环计算图,最终返回一个vec3输出,供光照节点消费。
接入材质:material.normalNode
NormalMapNode的典型挂载点是各类基于节点的物理/标准材质。传统材质体系中,法线贴图相关字段已在 MeshStandardMaterial 等类上预置:normalMap(纹理)、normalMapType(默认TangentSpaceNormalMap)与normalScale。而在 WebGPU 节点渲染路径下,等价能力通过 TSL 暴露:
material.normalNode = normalMap( texture( normalTex ), normalScale );其中normalScale即对应传统 API 中的强度控制。这种设计使得法线贴图不再是材质上「写死的开关」,而是可以和其他 TSL 节点自由组合、条件混合的可编程片段。
在仓库的实际示例中,webgpu_lights_phong.html 展示了最简接入:
centerObject.material.normalNode = normalMap( texture( normalMapTexture ) );normalMapTexture经texture()包装后直接交给normalMap(),一行代码即完成法线贴图的启用。
作为对照,传统 WebGL 渲染器(非节点路径)在 WebGLPrograms.js 中通过着色器参数区分处理:normalMapObjectSpace对应normalMapType === ObjectSpaceNormalMap,normalMapTangentSpace对应切线空间,而packedNormalMap则额外要求纹理为 RG 双通道格式。这印证了normalMapType与压缩打包在两种渲染后端下的语义一致性。
实践要点与易错提醒
- 贴图颜色空间:法线贴图必须以线性颜色空间采样,采样纹理时应确保纹理资源的色彩管理不会将其误判为 sRGB,否则 XY 重映射后会引入系统性偏移;
- 压缩打包仅限切线空间:
NormalRGPacking/NormalGAPacking只对TangentSpaceNormalMap生效,与ObjectSpaceNormalMap组合会直接触发源码中的错误提示,设计资产时请保持类型一致; - flat shading 与背面缩放:若材质开启平面着色,务必理解源码会对
scaleNode做背面取反,自定义scaleNode表达式时不应破坏这一语义; - 强度值语义:
scaleNode作用于 XY 分量。设为0时法线退化为纯 Z(即无扰动),大于1增强凹凸,小于1(含负值反向)弱化或反转细节。
小结
NormalMapNode是节点材质体系中对传统MeshStandardMaterial.normalMap + normalScale的 TSL 化封装。通过本文可以看到:文档中看似简单的四个属性与一个工厂函数,背后对应着一条清晰且健壮的着色器计算管线——颜色域重映射、RG/GA 压缩解包(unpackNormal)、scale 强度控制(含平面着色背面修正)以及切线/物体空间到视空间的变换。掌握它,就等于掌握了 three.js 节点体系下所有表面细节类效果(法线、视差、凹凸)的通用范式。需要进一步深入时,可继续阅读其父类 TempNode、基础节点 Node,以及同目录下协同工作的 FrontFacingNode.js 与 Packing.js。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考