three.js TSL BypassNode:在节点图中执行无返回值的副作用并保留原输出
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
本篇围绕 three.js 的 TSL(Three Shading Language)节点系统中的BypassNode展开。它解决一个看似简单却高频出现的节点图问题:当某个输入位需要“有返回值”的节点,而你想在那里调用一个不返回值的函数或节点(即void类型)时,如何让副作用代码被执行、同时输出值保持来自另一个节点。读完后你将理解BypassNode的构造参数与属性、其代码生成逻辑,以及在 three.js 源码中真实的使用方式。
核心定位:为“void 调用”开辟旁路
BypassNode的官方定义是(见 BypassNode 文档):
该类会生成给定节点(callNode)的代码,但在输出上返回另一个节点(outputNode)。这可用于在需要返回值的输入处调用一个不返回值(
void类型)的方法或节点。
换句话说,在 TSL 节点图中,几乎每个节点表达式都必须“产出值”。但很多操作是纯副作用的——例如向某个 buffer 写入数据、更新 uniform、执行stack的清理、调用只负责“干活”的函数——它们没有有意义的返回值。BypassNode就是把这种调用“旁路”接进需要返回值的位置:
- callNode:真正被执行、产生副作用的节点,其代码会被生成到 shader 中;
- outputNode:该表达式最终对外呈现的值,与 callNode 的返回值无关。
文档给出的原始示例:
material.colorNode = myColor.bypass( runVoidFn() )意思是:material.colorNode最终取myColor的值,但在求值过程中会先执行runVoidFn()的副作用代码。
从源码结构看,这正是 BypassNode.js 中generate方法的行为:先构建callNode(按void类型),把生成的代码片段注入到 shader 的行内代码流中,再返回outputNode的构建结果:
generate( builder ) { const snippet = this.callNode.build( builder, 'void' ); if ( snippet !== '' ) { builder.addLineFlowCode( snippet, this ); } return this.outputNode.build( builder ); }注意两个细节:
this.callNode.build( builder, 'void' )显式按void类型构建调用节点,表明它预期该节点不返回值;- 只有当副作用代码片段非空(
snippet !== '')时,才通过 NodeBuilder 的addLineFlowCode将其加入行内代码流(line flow),避免向 shader 注入无意义的空语句。
此外,generateNodeType也直接委托给 outputNode(源码第 58-62 行):
generateNodeType( builder ) { return this.outputNode.getNodeType( builder ); }这保证了BypassNode在类型推导上“透明”——它对节点图其余部分的类型签名完全等价于outputNode本身,调用方感知不到旁路的存在。
API 参考
构造函数
new BypassNode( outputNode : Node, callNode : Node )
创建一个旁路节点。参数说明:
- outputNode:输出节点。决定该表达式对外返回的值与类型;
- callNode:调用节点。决定被执行(产生副作用)的代码。
属性
.outputNode : Node
输出节点。
.callNode : Node
调用节点。
.isBypassNode : boolean(readonly)
类型测试标志,默认为true,可用于instanceof之外的鸭子类型判断。
对应的 TSL 函数
除类本身外,BypassNode.js 文件末尾 还导出并提供两种调用方式:
export const bypass = /*@__PURE__*/ nodeProxy( BypassNode ).setParameterLength( 2 ); addMethodChaining( 'bypass', bypass );bypass( outputNode, callNode ):TSL 全局函数形式,通过nodeProxy生成BypassNode实例,参数长度固定为 2;.bypass( callNode ):通过addMethodChaining挂载的方法链形式,任意节点上都可以直接调用,这也是文档示例myColor.bypass( runVoidFn() )能成立的原因。
该模块通过 Nodes.js 与 TSL.js 统一导出,因此既可以import { BypassNode } from 'three',也可以在 TSL 上下文中直接使用bypass函数或.bypass()方法。
实战用法
基本示例:执行副作用同时保留原颜色
import { material, color, uniform } from 'three/tsl'; // 假设 setEnv() 是一个不返回值的 TSL 函数(void) material.colorNode = myColor.bypass( setEnv() ); // 上式等价于:bypass( myColor, setEnv() )生成 shader 时,setEnv()的函数调用语句会先出现在行内代码流中,而colorNode的最终赋值仍然使用myColor的值。
一个真实的源码用例:LightsNode 的 stack 清理
在 three.js 光照节点的实现中,BypassNode被用来处理“执行清理、保留输出”的场景。LightsNode.js 第 424 行:
outgoingLightNode = outgoingLightNode.bypass( builder.removeStack() );这里:
outgoingLightNode是光照计算完成后对外输出的节点(累加了总漫反射、总镜面反射光等,见 第 417-420 行);builder.removeStack()是 NodeBuilder 上用于弹出栈内所有节点引用、结束一段作用域的方法,它的返回值(被移除的StackNode)在这里并不关心;- 用
.bypass()把“弹出栈”这个副作用绑定在输出节点上,保证光照输出值不被破坏,同时清理代码仍然被生成并执行。
这与 TSL 栈机制成对出现:addStack/removeStack(NodeBuilder.js 第 1871-1906 行)用于在 shader 构建中管理临时变量的作用域,而 TSLCore.js 的setupOutput展示了典型的“入栈—求值—出栈”结构:
setupOutput( builder ) { builder.addStack(); builder.stack.outputNode = this.call( builder ); return builder.removeStack(); }可以看出BypassNode是 TSL 构建器体系中处理“执行与取值分离”这一需求的基础构件。
工作原理小结
BypassNode在代码生成阶段的完整流程:
- 节点图求值到
BypassNode时,generateNodeType将类型声明完全转交给outputNode,使外部看到的就是outputNode的类型; generate先以void类型构建callNode,得到副作用代码片段;- 若片段非空,调用
builder.addLineFlowCode( snippet, this )将副作用语句写入当前 shader 的行内代码流; - 最后返回
outputNode.build( builder )的结果,即该表达式对外暴露的值。
由此,BypassNode以极小的实现成本(整个类不到 100 行)提供了 TSL 节点图中“命令式副作用”与“声明式求值”之间的桥接:副作用代码确定会被生成,而输出值完全由你指定的outputNode决定。
相关文档
- BypassNode 文档页
- BypassNode 源码
- NodeBuilder 栈管理实现
- TSL 函数与节点代理机制
- TSL 总览文档
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考