three.js NormalMapNode 法线贴图节点完全指南:TSL 用法、属性配置与源码原理
2026/9/8 23:17:24 网站建设 项目流程

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> )

两个参数的语义如下:

参数类型说明
nodeNode.<vec3>表示法线贴图数据。通常是texture( normalTex )的采样输出(vec3 颜色值)。
scaleNodeNode.<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 中被定义为两个整数常量:

常量语义
TangentSpaceNormalMap0法线信息相对于底层表面(切线空间),随表面曲率变化,通常来自 DCC 工具烘焙的普通法线贴图;
ObjectSpaceNormalMap1法线信息相对于物体朝向(物体空间),法线值是模型自身的空间方向,不随表面切线变化。

.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 ) );

normalMapTexturetexture()包装后直接交给normalMap(),一行代码即完成法线贴图的启用。

作为对照,传统 WebGL 渲染器(非节点路径)在 WebGLPrograms.js 中通过着色器参数区分处理:normalMapObjectSpace对应normalMapType === ObjectSpaceNormalMapnormalMapTangentSpace对应切线空间,而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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询