恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
three.js TSL 节点核心基类解析:TempNode 的缓存管理与去重机制
首页
资讯中心
/
three.js TSL 节点核心基类解析:TempNode 的缓存管理与去重机制
three.js TSL 节点核心基类解析:TempNode 的缓存管理与去重机制
发布时间:2026/9/10 1:30:00
three.js TSL 节点核心基类解析TempNode 的缓存管理与去重机制【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsTempNode 是 three.js 节点材质Node Material / TSL体系中最重要的中间层基类之一它继承自 Node同时又是 MathNode、OperatorNode、ArrayNode 等大量具体节点类型的公共父类。它的核心使命只有一句话——通过缓存管理把“被多次引用的节点计算结果”提升为临时变量从而在生成的着色器代码中杜绝重复计算。读完本文你将理解 TSL 生成着色器的三次构建流水线、usageCount 计数机制、临时变量的申请与复用逻辑并能据此写出对生成代码效率有清晰预期的自定义节点。TempNode 在节点体系中的位置TSLThree Shading Language把材质、光照、后处理等一切着色逻辑抽象为一棵节点树。所有节点的根基类是Node而Node本身继承自 three.js 的事件分发器EventDispatcher。本文主角TempNode处于继承链的中间层EventDispatcher → Node → TempNode → 具体节点从 src/nodes/core/TempNode.js 的实现看TempNode继承Node后并没有增加复杂的自身状态而是重写了build()方法并补充了一个判定方法hasDependencies()。文档描述它“作为许多其他节点类型的基类”是字面属实的在 src/nodes/ 目录中直接extends TempNode的类型横跨多个功能模块例如核心类ArrayNode对应 TSL 的array()、AssignNode赋值节点数学类MathNode、OperatorNode、BitcastNode显示/材质类BumpMapNode、NormalMapNode、ToneMappingNode、ColorSpaceNode、RenderOutputNode、PassNode后处理通道其他FunctionCallNode函数调用、VelocityNode速度访问器、SubgroupFunctionNodeGPU 计算子组等。也就是说绝大多数“会对子节点做一次运算并产出新值”的节点都选择以 TempNode 为基类这正是因为它内置了“表达式去重”的通用能力。为什么需要临时变量重复计算的代价在用户书写的节点树中同一个表达式节点经常会被多处引用。例如一张法线贴图的计算结果既参与漫反射光照又参与高光反射。如果不做任何处理生成 GLSL / WGSL 时每个引用点都会内联一次完整表达式导致// 未去重的示意代码重复计算三遍 vec3 n normalize( texture( map, uv ).xyz * 2.0 - 1.0 ); // 片段 A 内联 ... vec3 n normalize( texture( map, uv ).xyz * 2.0 - 1.0 ); // 片段 B 再次内联重复计算不仅拖慢 GPU还会让代码体积膨胀、可读性下降。TempNode 的思路与编译器常见的“公共子表达式消除”一致把复杂表达式先赋给一个临时变量之后所有引用点都直接使用变量名// 使用 TempNode 缓存后的示意代码 nodeVar0 normalize( texture( map, uv ).xyz * 2.0 - 1.0 ); vec3 n nodeVar0;构造器与属性构造器new TempNode( nodeType null )nodeType是节点的输出类型比如float、vec3。它会被透传给基类 Node 的构造器最终存储在this.nodeType上。默认值为null表示“类型在构建阶段由实际子节点推导”。TempNode自身并未覆盖nodeType的语义真正消费它的是Node基类的getNodeType()/generateNodeType()等类型推导逻辑因此继承 TempNode 的自定义节点只需沿用这套类型约定即可。注意TempNode并没有显式定义nodeType属性——从源码看它只是把构造参数原样交给super( nodeType )可推断属性本身由 Node 基类声明并赋默认值null。这与文档中“nodeType 默认是 null”的说明完全吻合。静态类型与类型标记static get type() { return TempNode; }与 three.js 其他对象一致节点系统通过constructor.type暴露类名便于序列化例如 NodeLoader 反序列化 JSON 时按type实例化对应类。TempNode的type是TempNode而继承它的子类各自声明了自己的type。.isTempNode : boolean只读this.isTempNode true;该布尔标记在构造时被写死为true语义与其他 three.js 对象如isNode、isMesh一致——它不是用来读的运行时值而是用来做鸭子类型判断的廉价通道function isTempNode( obj ) { return obj ! null obj ! undefined obj.isTempNode true; }相比instanceof这种标记方式在跨模块引用、压缩混淆后依然可靠。核心方法 hasDependencies判定是否“被多次使用”hasDependencies( builder ) { return builder.getDataFromNode( this ).usageCount 1; }builder是 NodeBuilder着色器构建器实例。getDataFromNode( this )会取出与当前节点关联的内部数据缓存其中的usageCount字段记录了该节点在整个着色器构建过程中被引用的次数。关键在于这个计数是谁、在何时累加的。在 Node 基类的 analyze 方法中每次节点进入“analyze分析”构建阶段都会先调用const usageCount builder.increaseUsage( this );而NodeBuilder.increaseUsage()见 src/nodes/core/NodeBuilder.js的实现为nodeData.usageCount nodeData.usageCount undefined ? 1 : nodeData.usageCount 1;即首次记录为 1之后每次再被引用都加 1。因此hasDependencies()返回true就等价于“该节点在节点图中被引用了不止一次存在多个依赖方”。同时NodeBuilder还会区分读引用readUsageCount与赋值写上下文中的引用writeUsageCount为赋值语义AssignNode留出信息。小结hasDependencies(builder)返回值语义 “该节点相对其他节点是否有多于一个依赖”。文档中“Returns: A flag that indicates if there is more than one dependency to other nodes.”与源码逐字对应。build() 重写三次构建流水线中的去重注入build()是节点的总入口TempNode重写它来插入缓存逻辑。要理解这段代码先要明白 Node.build() 划分的三个构建阶段setup准备节点与子节点可能创建新节点返回节点对象analyze分析节点层级统计 usageCount去重依据在此产生generate真正生成着色器代码字符串。TempNode只介入第三个阶段generatebuild( builder, output ) { const buildStage builder.getBuildStage(); if ( buildStage generate ) { const type builder.getVectorType( this.getNodeType( builder, output ) ); const nodeData builder.getDataFromNode( this ); // 命中缓存直接格式化返回已生成的临时变量名 if ( nodeData.propertyName ! undefined ) { return builder.format( nodeData.propertyName, type, output ); } // 未命中缓存且值得缓存 else if ( type ! void output ! void this.hasDependencies( builder ) ) { const snippet super.build( builder, type ); // ① 先生成内联表达式 const nodeVar builder.getVarFromNode( this, null, type ); // ② 申请临时变量 const propertyName builder.getPropertyName( nodeVar ); builder.addLineFlowCode( ${propertyName} ${snippet}, this ); // ③ 写入赋值流代码 nodeData.snippet snippet; nodeData.propertyName propertyName; // ④ 登记缓存 return builder.format( nodeData.propertyName, type, output ); } } return super.build( builder, output ); // 其他阶段或无需缓存时走默认路径 }逐一拆解这段核心逻辑① 生成内联表达式。super.build( builder, type )先让父类逻辑按目标类型type生成一串可内联的着色器表达式snippet。此时尚未产生任何副作用。② 申请临时变量。builder.getVarFromNode()在 NodeBuilder 中为节点分配一个NodeVar。自动生成的变量名遵循nodeVar0、nodeVar1… 的递增命名规则常量场景则用nodeConst0…其声明会注册进当前 shader 阶段的变量表最终统一出现在生成的着色器代码中。③ 写入赋值流代码。builder.addLineFlowCode( nodeVar0 snippet, this )生成形如nodeVar0 ...;的赋值行并插入到当前代码流flow code的合适位置。由于引用点众多这一步实际由构建器在“流代码”阶段统一输出确保赋值发生在使用之前。④ 登记缓存并返回。把生成结果写入nodeData.snippet与nodeData.propertyName后本次调用返回builder.format( propertyName, type, output )——即引用方拿到的是变量名本身。后续命中。当该节点第二次、第三次被引用并再次进入 generate 阶段时第一个分支propertyName ! undefined直接命中返回格式化的变量名不再重复生成表达式。边界条件。缓存只对type ! void output ! void生效void 类型没有可供复用的“值”输出为 void例如纯副作用节点同样没有提升为临时变量的意义此时会退回super.build()的默认内联路径。此外从 Node 基类 build 的流程可知节点构建会记录buildStages若某节点在 generate 阶段被直接请求而其父阶段setup/analyze尚未执行构建器会自动补跑保证usageCount在去重判断前已统计到位。源码定位与完整阅读路径本文引用的核心实现集中在一个短文件里建议对照阅读TempNode 实现源码约 88 行含完整的hasDependencies与build重写Node 基类源码负责 setup/analyze/generate 三阶段调度、getShared节点共享与analyze中的increaseUsage调用NodeBuilder 源码提供getDataFromNode、increaseUsage、getVarFromNode、getPropertyName、addLineFlowCode等基础设施NodeVar 源码临时变量数据结构。对应的 API 参考文档与本文档同源生成位于 Node.html.md、NodeBuilder.html.md、NodeVar.html.md。派生类中的实际运用以 ArrayNode 为例观察继承 TempNode 的类能直观感受“临时变量”机制的适用范围。以 ArrayNode 为例它把一组节点组合成着色器数组用户代码通常写成const colors array( [ vec3( 1, 0, 0 ), vec3( 0, 1, 0 ), vec3( 0, 0, 1 ) ] ); const redColor colors.element( 0 ); // ElementNode 引用 colorsarray()定义在 ArrayNode.js 的 TSL 工厂函数内部会new ArrayNode( null, count, values )。当colors被.element()、后续计算多处引用时它的usageCount会大于 1于是 TempNode 的build()把整段数组初始化表达式提升为一次赋值其余引用点只读取变量避免数组字面量被反复展开。类似的MathNode加减乘除、三角函数等一旦在某材质中参与多处输出其计算结果也会被缓存为临时变量——这正是数学表达式节点普遍继承 TempNode 的直接原因。使用注意事项与扩展建议绝大多数场景无需直接实例化。TSL 用户通过tsl语法add(),mul(),normalize()等间接构建节点树TempNode的缓存行为自动生效无需手工干预自定义节点时优先继承 TempNode。若你实现的自定义表达式节点“把若干输入算出一个可复用的值”继承 TempNode 即可免费获得去重缓存只需要求这类表达式无副作用且幂等——被提升为临时变量意味着它只会执行一次与global/getShared区分。Node 基类还有另一套“节点共享”机制global标记 getShared()面向“同一节点对象在场景中只应声明一次”的场景如 AttributeNode而 TempNode 的去重面向“表达式值避免重复计算”。两者关注点不同但可协同类型测试。判断一个对象是否为临时节点可用只读标记obj.isTempNode true。总结TempNode 用极简的接口一个类型标记、一个判定方法、一次build()重写为 three.js TSL 节点系统注入了公共子表达式消除能力analyze 阶段由NodeBuilder.increaseUsage()累计引用次数generate 阶段由hasDependencies()判断是否值得缓存命中后通过getVarFromNode()addLineFlowCode()把表达式沉淀为命名临时变量并登记到nodeData使后续所有引用点只读写变量名。理解这条链路也就掌握了 TSL 生成高效着色器代码的一个关键支柱——无论你是在排查生成代码冗余还是在编写自己的材质节点都能精准预判哪些表达式会被“计算一次、复用多次”。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考