three.js TSL 中 BumpMapNode 深度解析:基于屏幕空间梯度的凹凸贴图节点实现
three.js TSL 中 BumpMapNode 深度解析基于屏幕空间梯度的凹凸贴图节点实现【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js本文以 three.js 官方文档页 BumpMapNode 为骨架结合 src/nodes/display/BumpMapNode.js 的源码实现系统讲解该节点如何把一张高度图转换为 view 空间的法线扰动。读完后你将掌握bumpMap()TSL 函数的构造参数与默认行为、将凹凸贴图挂到节点材质normalNode的标准写法、Wireframe 下自动降级的机制以及 Mikkelsen 屏幕空间梯度算法在 Node 系统中的具体落地方式。文档定位与继承体系官方文档对BumpMapNode的定义是This class can be used for applying bump maps to materials.此类可用于向材质应用凹凸贴图。其继承链为EventDispatcher → Node → TempNode → BumpMapNode继承自 TempNode 意味着它具备临时变量缓存能力当同一个BumpMapNode实例在材质中不止一处被引用时usageCount 1编译阶段会先把求值结果写入一个临时属性再让所有引用点复用该属性避免重复计算。这对凹凸贴图这类每片元都要采样纹理并做多次屏幕空间微分的开销型节点尤为重要。节点输出的类型是vec3构造时super( vec3 )即它最终产出的是扰动后的法线向量而非标量高度——这也是为什么文档给出的用法是把它赋给material.normalNode而不是颜色类属性。基本用法与构造参数文档给出的标准示例material.normalNode bumpMap( texture( bumpTex ) );其中texture( bumpTex )把经典Texture包装成 TSL 的TextureNodebumpMap( ... )是 BumpMapNode.js 末尾通过nodeProxy( BumpMapNode ).setParameterLength( 1, 2 )导出的 TSL 函数对应BumpMapNode类material.normalNode是 NodeMaterial 上的节点属性用于覆盖默认的normalMap/normalScale或bumpMap/bumpScale法线推导逻辑。构造函数与参数new BumpMapNode( textureNode, scaleNode null )参数类型说明默认值textureNodeNodefloat凹凸图高度图数据注意期望是float 节点即高度值而非 RGB 颜色必填scaleNodeNodefloat控制凹凸效果的强度对应经典材质中的bumpScale语义null两个属性在类中均为一等成员可随时修改.textureNode : Nodefloat— 表示凹凸图数据.scaleNode : Nodefloat— 控制凹凸强度默认null。一个关键细节scaleNode默认虽为null但setup()中会做const bumpScale this.scaleNode ! null ? this.scaleNode : 1;见 BumpMapNode.js#L91-L107即缺省强度为 1。如果想动态调节强度scaleNode传一个 float 节点即可例如仓库示例 webgpu_materials_envmaps_bpcem.html 中的写法wallMat.normalNode bumpMap( texture( bumpTex ), float( 5 ) );也可以直接复用材质的经典属性让bumpScale保持数据驱动MaterialNode会自动生成对应的 float 节点见下文与经典 bumpMap/bumpScale 属性的桥接。高度图输入为什么是 floattextureNode的类型标注为Nodefloat是有实际含义的实现里对纹理的三次采样结果都会先经过float( ... )转换BumpMapNode.js#L16-L21即只取标量高度值。当从texture()包装的完整纹理采样得到vec4时后续乘法和差分仍按 float 语义参与运算而系统内部桥接路径则会显式取红色通道见后文。实践中建议高度图使用灰度数据且纹理应是非颜色数据与经典material.bumpMap文档中纹理须为NoColorSpace的要求一致。底层原理Mikkelsen 屏幕空间梯度法BumpMapNode的实现直接引用了 Morten S. Mikkelsen 的论文Bump Mapping Unparametrized Surfaces on the GPU源码第 8-9 行注释标明出处由两个 TSL 函数构成1.dHdxy_fwd前向差分求屏幕空间高度梯度const dHdxy_fwd Fn( ( { textureNode, bumpScale } ) { // Its used to preserve the same TextureNode instance const sampleTexture ( callback ) textureNode.isolate().context( { getUV: ( texNode ) callback( texNode.uvNode || uv() ), forceUVContext: true } ); const Hll float( sampleTexture( ( uvNode ) uvNode ) ); return vec2( float( sampleTexture( ( uvNode ) uvNode.add( uvNode.dFdx() ) ) ).sub( Hll ), float( sampleTexture( ( uvNode ) uvNode.add( uvNode.dFdy() ) ) ).sub( Hll ) ).mul( bumpScale ); } );BumpMapNode.js#L11-L23其逻辑与经典 WebGL 着色器中的dHdxy_fwd()见 bumpmap_pars_fragment.glsl.js完全同构但做了一次 Node 化改造Hll是当前片元 UV 处采样的基准高度沿 UV 的屏幕空间导数dFdx()/dFdy()偏移后再采样两次做前向差分得到高度对屏幕坐标 x、y 的导数dHdxy最后整体乘以bumpScale让强度在这里统一进入梯度sampleTexture中用isolate()保住同一个TextureNode实例并通过getUV上下文统一 UV 来源若节点本身携带uvNode就用它否则回退到内置uv()——这保证了自定义 UV 流程如材质上不同 texture 的channel配置不会被破坏。2.perturbNormalArb把高度梯度变换为法线扰动const perturbNormalArb Fn( ( inputs ) { const { surf_pos, surf_norm, dHdxy } inputs; // normalize is done to ensure that the bump map looks the same regardless of the textures scale const vSigmaX surf_pos.dFdx().normalize(); const vSigmaY surf_pos.dFdy().normalize(); const vN surf_norm; // normalized const R1 vSigmaY.cross( vN ); const R2 vN.cross( vSigmaX ); const fDet vSigmaX.dot( R1 ).mul( faceDirection ); const vGrad fDet.sign().mul( dHdxy.x.mul( R1 ).add( dHdxy.y.mul( R2 ) ) ); return fDet.abs().mul( surf_norm ).sub( vGrad ).normalize(); } );BumpMapNode.js#L27-L45逐行对应论文方法用视点位置surf_pos的屏幕空间导数dFdx/dFdy得到两个切向方向vSigmaX/vSigmaYnormalize是为了让凹凸效果与纹理 scale 无关这一点源码注释也写明了由vSigmaY × vN和vN × vSigmaX构造两个参考向量R1、R2fDet vSigmaX · R1 × faceDirection是屏幕三角形面积因子其中faceDirectionFrontFacingNode.js#L75在前向片元为1、背向片元为-1由此自动处理DoubleSide材质的背面法线方向问题最终输出normalize( abs(fDet) * vN - vGrad )即原法线减去高度梯度贡献项后重新归一化得到扰动后的 view 空间法线。这里的surf_pos与surf_norm由setup()注入分别为 positionView 与 normalView——所以BumpMapNode的输入必须是 view 空间语义的几何量输出天然就是可直接替换normalNode的法线。Wireframe 下的自动降级setup( builder )中还有一段重要的防御性逻辑// Screen-space derivatives are unreliable on thin lines, so the bump // effect is disabled for wireframe rendering. if ( builder.material.wireframe true ) return normalView;BumpMapNode.js#L91-L107屏幕空间导数dFdx/dFdy在线条wireframe/line渲染上是不可靠的——线段的屏幕三角形退化差分没有意义。因此当材质开启wireframe时节点直接退化为未扰动的normalView凹凸效果被安全禁用。这一点与经典 WebGL 渲染管线的行为完全一致WebGLPrograms.js 中const HAS_BUMPMAP !! material.bumpMap material.wireframe false;两条渲染路径对同一场景的视觉表现保持对齐。与经典 bumpMap / bumpScale 属性的桥接BumpMapNode并非只能手工创建。在节点材质体系中材质经典的bumpMap/bumpScale属性会被自动编译成该节点。在 MaterialNode.js#L233-L254 中法线作用域的推导顺序为} else if ( scope MaterialNode.NORMAL ) { if ( material.normalMap ) { node normalMap( this.getTexture( normal ), this.getCache( normalScale, vec2 ) ); // ... 法线贴图格式相关处理 } else if ( material.bumpMap ) { node bumpMap( this.getTexture( bump ).r, this.getFloat( bumpScale ) ); } else { node normalView; } }这段代码说明三件事优先级normalMap优先于bumpMap二者只取其一都没有则回退到normalView几何法线。这与 NodeMaterial 文档中normalNode的默认值要么是normalMap * normalScale要么是bumpMap * bumpScale要么是normalView的描述完全吻合显式取红通道桥接路径传入的是this.getTexture( bump ).r即纹理的红色通道作为高度——这正是凹凸图是非颜色灰度数据这一约定的代码级落实bumpScale 是数据驱动的this.getFloat( bumpScale )会生成跟踪材质属性变化的 float 节点运行时修改material.bumpScale不需要重建节点图。因此如果你的项目使用NodeMaterial或MeshStandardMaterialNode等直接写经典的material.bumpMap tex; material.bumpScale 2;即可底层走的正是BumpMapNode这条 Mikkelsen 路径。实战程序化高度场驱动凹凸TSL 节点图的优势在于textureNode可以是任意 float 节点不限于纹理采样。仓库示例 webgpu_geometry_loft.html 中有大量程序化高度场 bumpMap的组合import { bumpMap, mx_noise_float, mx_worley_noise_float, positionLocal, uv, sin } from three/tsl; // 基于空间噪声的凹凸 porcelain.normalNode bumpMap( mx_noise_float( positionLocal.mul( 2 ) ).mul( 0.05 ) ); // 基于 UV 周期性纹理的凹凸 shellMaterial.normalNode bumpMap( sin( uv().x.mul( 480 ) ).mul( 0.02 ) ); // 组合多个噪声场 capMaterial.normalNode bumpMap( warts.mul( 0.08 ).sub( gills.mul( 0.015 ) ) );此外 webgpu_tsl_earth.html 中用高度采样节点globeMaterial.normalNode bumpMap( bumpElevation );为地球模型生成地形凹凸。这些用法共同表明BumpMapNode的输入既可以是texture( ... )包装的灰度图也可以是noise、sin、位置函数等任意程序化 float 表达式节点内部只关心每片元拿到一个高度值 UV 导数这一契约。与 NormalMapNode 的取舍同一个目录下的 NormalMapNode.js 处理的是法线贴图RGB 直接编码切线空间法线偏移需要normalScale且区分 OpenGL/ tangent 两种约定而BumpMapNode处理的是高度图单通道标量靠屏幕空间微分现场求梯度。选择上可以遵循有现成法线贴图、或需要精确的切线空间控制 →normalMap只有灰度高度数据法线编辑器导出的高度、程序化噪声、地形高程图→bumpMap两者在节点材质中互斥且normalMap优先见上文MaterialNode的推导顺序。小结要点说明继承EventDispatcher → Node → TempNode → BumpMapNode输出vec3法线构造new BumpMapNode( textureNode: Nodefloat, scaleNode: Nodefloat null )强度缺省scaleNode为null时按1处理算法Mikkelsen 屏幕空间前向差分dHdxy_fwdperturbNormalArb与 WebGL 管线bumpmap_pars_fragment同构Wireframe自动降级为normalView与WebGLPrograms的HAS_BUMPMAP判断行为一致属性桥接经典bumpMap/bumpScale由MaterialNode自动编译为bumpMap( 纹理.r, bumpScale )normalMap存在时优先灵活输入高度可以是纹理采样也可以是任意程序化 float 节点噪声、UV 函数等核心参考文件src/nodes/display/BumpMapNode.js节点实现、src/nodes/accessors/MaterialNode.js属性桥接、src/nodes/core/TempNode.js临时变量缓存机制、src/renderers/shaders/ShaderChunk/bumpmap_pars_fragment.glsl.js经典 WebGL 管线的同源算法、docs/pages/BumpMapNode.html.md官方 API 文档。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考