首页
/ three.js TSL ExpressionNode 深度解析:用 expression()/return/continue/discard 在节点着色器中注入原生语句

three.js TSL ExpressionNode 深度解析:用 expression()/return/continue/discard 在节点着色器中注入原生语句

2026-09-06 18:42:08作者:房伟宁

ExpressionNode 是 three.js 节点材质(Node Material / TSL)体系中的基础类之一,用于把一段"原生的着色器代码片段"(snippet)以表达式或语句的形式直接嵌入最终生成的着色器源码中,例如 returncontinuediscard 这类流程控制语句。本文以 docs/pages/ExpressionNode.html.md 为核心,结合仓库中 src/nodes/code/ExpressionNode.js 的完整实现以及 TSL 工具函数、Loop 循环、Discard 封装等真实调用链,讲解该类的作用、构造参数、属性、内部生成逻辑与实战用法。读完你不仅能准确使用 expression().discard()Return()Continue()Break(),还能理解这些高层 API 最终是如何落到一行原生 shader 代码上的。

ExpressionNode 在节点体系中的定位

ExpressionNode 的继承链为:EventDispatcher → Node → ExpressionNode,它是 three.js 节点系统中直接继承 src/nodes/core/Node.js 的"代码片段"型节点,源码位于 src/nodes/code/ExpressionNode.js(与 CodeNodeFunctionCallNodeFunctionNode 同处 code/ 目录)。

它与同目录下的 CodeNode(见 src/nodes/code/CodeNode.js)有明确分工:

  • CodeNode 表达"一整段原生代码",是 FunctionNode(定义 WGSL/GLSL 函数)的基类;
  • ExpressionNode 表达"单条内联的原生表达式或语句",粒度更小、更轻量,专用于 returncontinuediscardbreak 这类控制流语句,或者 textureDimensions(...) 这类内联内置调用。

换言之,ExpressionNode 是节点图(Node Graph)通向最终生成着色器文本(WebGL 的 GLSL 或 WebGPU 的 WGSL)的"直通门"——它的 snippet 会被原样写入生成代码。

构造函数:参数与默认值

官方文档给出的构造签名如下,与源码 constructor 完全一致:

new ExpressionNode( snippet = '', nodeType = 'void' )
参数 类型 默认值 说明
snippet string '' 要嵌入着色器的原生代码片段(native code snippet)
nodeType string 'void' 该表达式声明产生的结果节点类型

构造时,nodeType 通过 super( nodeType ) 传递给基类 Node(决定节点的类型元信息),而 snippet 被直接保存在实例属性 this.snippet 上。

两个默认值对应了 ExpressionNode 的两种基本语义:

  • nodeType'void'(默认)snippet 是一条"不产生值的语句",例如 discardreturncontinuebreak
  • nodeType 为非 void 类型(如 'float''vec3''u32'snippet 是一个"产生值的表达式",可以被嵌入到其他节点的输入位置,例如 textureDimensions( map ) 或字面量 6u

属性说明

.snippet : string

保存将要被写入着色器的原生代码片段,默认值 ''

这是该类唯一的自有数据成员(其余能力来自基类 Node)。由于片段以纯文本形式保存,使用时需要注意它最终会被插入哪一种着色器语言环境(GLSL 或 WGSL),片段本身必须与渲染后端的着色器语法匹配。

generate() 内部逻辑:void 与取值分支

理解 ExpressionNode 行为的关键在其 generate() 实现。当节点系统在编译着色器时,会为每个节点调用 generate( builder, output ),该实现根据节点类型分两条路径:

generate( builder, output ) {

    const type = this.getNodeType( builder );
    const snippet = this.snippet;

    if ( type === 'void' ) {

        // 情况一:作为语句插入当前流程块
        builder.addLineFlowCode( snippet, this );

    } else {

        // 情况二:作为值表达式返回,供上层继续格式化
        return builder.format( snippet, type, output );

    }

}

情况一(void 语句):通过 builder.addLineFlowCode( snippet, this ) 把片段追加为当前作用域内的一行代码。addLineFlowCode 位于 src/nodes/core/NodeBuilder.js,若当前处于某个条件块(如 If)的上下文中,它还会把语句登记到对应的代码块(nodeBlock),保证 discardreturn 出现在正确的大括号作用域内。此时节点不返回值,调用方通常需要把它放进语句栈(TSL 中表现为 .toStack())。

情况二(值表达式):片段被当作表达式返回,并经由 builder.format( snippet, type, output ) 做类型对齐。NodeBuilder.format()实现见 src/nodes/core/NodeBuilder.js#L3356)负责处理源类型与目标输出类型之间的转换关系(例如向量分量的 swizzle 截取、标量/向量扩展等),确保上游要求 vec3 而 snippet 声明为 vec4 之类的场景也能正确输出可编译代码。

TSL 工厂函数 expression()

类默认导出之外,源文件尾部还导出了对应的 TSL 工厂函数:

export const expression = /*@__PURE__*/ nodeProxy( ExpressionNode ).setParameterLength( 1, 2 );

它由 nodeProxy 生成,允许传 1 或 2 个参数,即:

expression( 'discard' );            // nodeType 缺省为 'void'
expression( snippet, nodeType );    // 指定类型

expression 通过 src/nodes/TSL.jssrc/nodes/tsl/TSLBase.jssrc/nodes/Nodes.js 对外统一导出,因此既可以从 three/tsl 导入 expression,也可以直接实例化 ExpressionNode 类(两种方式等价)。

实战用法一:控制流语句 return / continue / discard / break

ExpressionNode 最常见的用途是承载控制流语句,官方也明确举例 returncontinuediscard。仓库在多个高层 TSL API 中复用了它:

discardreturnsrc/nodes/utils/Discard.js

export const Discard = ( conditional ) =>
    ( conditional ? select( conditional, expression( 'discard' ) ) : expression( 'discard' ) ).toStack();

export const Return = () => expression( 'return' ).toStack();

addMethodChaining( 'discard', Discard );
  • Return() 直接返回 expression( 'return' ).toStack(),即一个 void 的 return 语句;
  • Discard( conditional? ) 支持可选条件:传条件时用 select()discard 语句包进条件块,否则是无条件 discard;
  • .discard() 通过 addMethodChaining 注册为节点方法链,因此任何节点都可写成 cond.greaterThanEqual( 1.0 ).discard() 这样的链式语法。

continuebreaksrc/nodes/utils/LoopNode.js

export const Continue = () => expression( 'continue' ).toStack();
export const Break = () => expression( 'break' ).toStack();

在 examples 中有大量真实调用可印证。例如 examples/webgpu_deferred.html 中对未命中场景丢弃片元:

resolveMaterial.colorNode = Fn( () => {

    opaqueDepthNode.greaterThanEqual( 1.0 ).discard();
    return opaqueOutputNode;

} );

compute 着色器中的提前退出见 examples/webgpu_compute_particles_fluid.html

If( mass.lessThanEqual( 0 ), () => {

    Return();

} );

其余同类佐证还包括 examples/webgpu_volume_fire.htmlexamples/jsm/tsl/display/GTAONode.jsexamples/jsm/tsl/display/SSRNode.js 等。可以看到:所有 .discard()Return()Continue()Break() 调用,底层都是 expression( '...' ) 产生的 ExpressionNode

实战用法二:带类型的值表达式

除控制流语句外,ExpressionNode 也大量用于生成"带返回值的内联表达式"。此时需要传入第二个参数 nodeType

循环变量引用src/nodes/utils/LoopNode.js 在构造循环体时,会把循环参数包成带类型的表达式节点,供循环体内访问循环变量:

const name = ( param.isNode !== true && param.name ) || this.getVarName( i );
const type = ( param.isNode !== true && param.type ) || 'int';

inputs[ name ] = expression( name, type );

WGSL 内置查询src/renderers/webgpu/nodes/WGSLNodeBuilder.jsExpressionNode 把 WGSL 内置调用包装成可缓存的值节点,例如 textureDimensions( ... )(类型 dimensionType)、textureNumLayers(...)'u32')以及 6u 立方体贴图面数常量,并常以 VarNode 包装。

原子操作(Atomic)src/nodes/gpgpu/AtomicFunctionNode.js 中原子函数片段同样通过 expression( methodSnippet, type ).toConst() 构造,供后续代码生成阶段内联。

编写你自己的表达式/语句

综合以上分析,在使用 TSL 编写自定义逻辑时可按以下模式组合:

import { Fn, If, expression } from 'three/tsl';

// 1) 值表达式:把一段原生表达式当作普通节点使用
const sizeNode = expression( 'textureDimensions( map )', 'vec2' );

// 2) 语句:配合 If/select 实现条件化 discard
material.colorNode = Fn( () => {

    uv.y.greaterThan( 0.9 ).discard();            // 隐式:expr + discard 封装

    If( depth.greaterThanEqual( 1.0 ), () => {    // 显式:if 包裹语句
        Return();
    } );

    return texture( map, uv );

} );

要点归纳:

  1. void 类型 → 语句return / continue / break / discard 的节点类型固定为 'void',必须以语句(.toStack() 或函数体内)形式出现,不能充当上游节点的值输入;
  2. 非 void 类型 → 值:当 snippet 要作为某个输入参与运算时,务必用第二个参数声明正确的节点类型,代码生成阶段的 format() 才能正确对齐目标类型;
  3. 语言一致性:snippet 是"原生代码直通",必须与最终着色器语言(WebGL 后端 GLSL / WebGPU 后端 WGSL)匹配;文档(docs/pages/ExpressionNode.html.md)称其为"native code snippet"即此意;
  4. 优先使用高层封装:日常开发建议使用 expression() 工厂以及 .discard()Discard()Return()Continue()Break() 这类既有封装,它们内部已经处理了 toStack()/条件包装等细节,行为与 three.js 内置着色器保持一致。

总结

ExpressionNode 体量虽小,却是 TSL 节点图连接"高层 JS 语义"与"底层 GLSL/WGSL 文本"的关键一环:returncontinuediscardbreak 等所有语句型操作,以及带类型的原生内联表达式,最终都汇聚为它的 snippet 字段并由 generate() 注入生成代码。理解其构造参数(snippetnodeType)、属性、void/非 void 双分支生成逻辑及其与 NodeBuilder.addLineFlowCode / format 的配合,能帮助你在编写自定义 TSL 着色器与 compute 逻辑时准确选择"语句还是值"的表达方式。

如需继续深入,可阅读同目录的 src/nodes/code/CodeNode.js(原生代码段基类)、src/nodes/code/FunctionNode.jsglslFn/wgslFn 函数定义)与 src/nodes/code/FunctionCallNode.js(函数调用),或对照 TSL 综合参考 docs/pages/TSL.html.mdDiscardReturn 等条目的说明。

登录后查看全文
热门项目推荐
相关项目推荐