ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

three.js 节点着色器系统的变量基石:全面解析 VarNode 与 TSL 的 toVar / Var 变量声明机制

three.js 节点着色器系统的变量基石:全面解析 VarNode 与 TSL 的 toVar / Var 变量声明机制 three.js 节点着色器系统的变量基石全面解析 VarNode 与 TSL 的 toVar / Var 变量声明机制【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js导读VarNode 是 three.js 基于 TSLThree Shading Language构建的节点化着色器系统中代表着色器变量的核心节点类继承自EventDispatcher → Node。在日常开发中开发者几乎不会直接new VarNode()而是通过sampleDepth( uvNode ).toVar( depth )这类 TSL 链式语法把一段需要被重复引用或便于调试的节点表达式提升为着色器中真正可复用的命名变量。本文以 VarNode 官方文档 为骨架结合 VarNode.js 源码 与 TSL.md系统讲解 VarNode 的构造参数、属性标志、Intent意图机制以及Var/Const/VarIntent三个工厂函数在 shader 生成阶段的底层行为帮助你写出更高效、更可读的 TSL 材质代码。一、VarNode 是什么从表达式到着色器变量的桥梁在 TSL 中绝大多数节点表达式的求值结果默认会被内联到生成的 GLSL / WGSL 着色器中。但某些昂贵或会被多处使用的子表达式例如深度采样、法线变换结果如果反复内联会造成冗余计算也让生成的着色器代码难以阅读。VarNode 正是为解决这一问题而存在的节点类型。从源码类定义可见其定位与继承关系*Inheritance: EventDispatcher → Node → VarNode对应源码为 src/nodes/core/VarNode.js其中类声明如下class VarNode extends Node { static get type() { return VarNode; } // ... }其官方文档给出的典型用法是从既有节点创建变量const depth sampleDepth( uvNode ).toVar( depth );也就是说sampleDepth( uvNode )首先产生一个节点对象.toVar()再将其包装为一个VarNode从而在生成的着色器中以命名变量这里是depth的形式存在供后续其他节点引用。VarNode 在项目中的广泛使用虽然使用者很少直接感知到 VarNode 的存在但它深度渗透在 three.js 内部。仅以src目录为例toVar()/Var()/toConst()被大量模块引用例如材质侧Line2NodeMaterial.js、MeshPhysicalNodeMaterial.js、NodeMaterial.js光照与着色模型PhysicalLightingModel.js、ShadowMaskModel.js、VolumetricLightingModel.js数学函数与噪声MaterialXNoise.js、OperatorNode.js这印证了官方文档中Variables are created from existing nodes like the following这一描述任何已有节点都可以被包装成变量节点它是构建复杂 TSL 材质时的通用基础设施。二、构造函数与参数说明new VarNode( node : Node, name : string, readOnly : boolean )构造一个新的变量节点。对应源码见 VarNode.jsconstructor( node, name null, readOnly false ) { super(); this.node node; this.name name; this.global true; this.isVarNode true; this.readOnly readOnly; this.parents true; this.intent false; }三个构造参数的核心含义如下| 参数 | 类型 | 默认值 | 说明 | | -- | -- | -- | -- | |node|Node| 无必填 | 需要为其创建变量的原始节点它是变量要承载的表达式来源 | |name|string|null| 变量在着色器中的名字若不提供节点系统会自动生成详见下文自动命名 | |readOnly|boolean|false| 只读标志置为true时该变量在合适场景下会被声明为常量而非可变变量 |自动命名机制当name传入null时真正决定变量名的工作发生在构建阶段。在 NodeBuilder.js 的getVarFromNode()中if ( name null ) { name ( readOnly ? nodeConst : nodeVar ) id; }也就是说普通变量toVar自动命名为nodeVar0、nodeVar1……常量toConst即readOnly true自动命名为nodeConst0、nodeConst1……这一机制保证即使你完全不关心命名也不会出现变量名冲突并且同类变量共享独立的计数器源码中的idNS readOnly ? _const : _var。三、属性详解VarNode 在基类 Node 之上覆盖并新增了一系列标志位理解它们对判断节点在构建管线中的行为至关重要。.global : boolean —— 默认true覆盖 Node 默认值基类 Node.js 中global默认是false而 VarNode 将其置为true。该属性关系到节点缓存是否可跨当前 shader-stage 复用见基类中的isGlobal()说明对应文档链接 Node#global。对于变量节点而言global true意味着它所声明的变量可以在当前着色阶段内被当作全局缓存引用。.intent : boolean —— 默认false意图标志用于表示该节点仅表达一种变量使用意图而非真实声明的变量详见第四节Intent 机制。.isVarNode : boolean只读—— 默认true类型测试标志。与 three.js 其他类一样可通过node.isVarNode true快速判断节点是否为 VarNode从而避免instanceof的额外开销或跨副本问题。.name : string —— 默认null着色器中变量的名字。若为null由节点系统自动生成见上文的nodeVar/nodeConst命名规则覆盖自基类 Node 的name属性对应 Node#name。注意基类默认值为空字符串而 VarNode 显式使用null来触发自动命名分支。.node : Node被包装的原始节点是 VarNode 的核心数据来源。几乎 VarNode 的所有类型派生行为都委托给它——例如源码中的getElementType()、generateNodeType()、getArrayCount()、getMemberType()都直接转发给this.node。.parents : boolean —— 默认true告知节点系统该节点需要父节点。其含义可结合文档描述Add this flag to the node system to indicate that this node require parents理解对应覆盖了 Node#parents。它影响节点在依赖图与作用域栈中的挂载方式toVar工厂在源码中都会先调用.toStack()见下文工厂函数小节。.readOnly : boolean —— 默认false只读标志。它并非简单的标签而是会显著改变generate()阶段的代码生成策略详见第六节的 readOnly 深入分析。四、Intent意图机制方法与使用场景Intent 是 VarNode 中比较精巧的设计。它在三个方法中实现对应源码 VarNode.jssetIntent( value ) { this.intent value; return this; } isIntent( builder ) { const data builder.getDataFromNode( this ); if ( data.forceDeclaration true ) return false; return this.intent; } getIntent() { return this.intent; }各方法说明| 方法 | 参数 | 返回 | 作用 | | -- | -- | -- | -- | |setIntent( value : boolean )|value要设置的意图标志 |VarNode支持链式调用 | 设置意图标志 | |isIntent( builder : NodeBuilder )|builder节点构建器 |boolean| 判断该节点当前是否作为意图使用当构建器记录中forceDeclaration true声明被强制时返回false| |getIntent()| 无 |boolean| 直接返回意图标志 |意图节点解决了什么问题文档中的原话是Its useful for assigning variables without needing creating a new variable node.在需要给变量赋值时不必再新建一个变量节点。理解这一点需要结合.toVarIntent()的典型应用。当你要把一个表达式的值赋给已存在的变量而非新声明变量时如果使用toVar会强制生成一条新的变量声明而使用意图模式节点系统会知道这里只表达赋值意图从而在合适情况下直接输出目标变量的赋值语句避免多余的声明。isIntent()中forceDeclaration的检查很重要当构建流程判定必须先有声明例如变量同时被读写、或处于循环/代码块作用域内需要先入栈时意图会被降级为真实声明。意图在构建阶段的分流在build()方法VarNode.js中当isIntent( builder )为真且当前并非赋值场景isAssign( builder ) ! true时节点不会走super.build()而是直接把内部node的构建结果原样返回if ( this.isIntent( builder ) ) { if ( this.isAssign( builder ) ! true ) { return this.node.build( ...params ); } } return super.build( ...params );这印证了意图节点不真正声明变量而是代表一种赋值/使用意向的设计目的。五、核心工厂函数与链式方法在文档正文之外VarNode.js 还导出了三个 TSL 工厂函数并注册了三个链式方法。它们才是日常 TSL 编程的真实入口// 工厂函数供 Fn 使用 export const Var ( node, name null ) createVar( node, name ).toStack(); export const Const ( node, name null ) createVar( node, name, true ).toStack(); export const VarIntent ( node ) createVar( node ).setIntent( true ).toStack(); // 链式方法 addMethodChaining( toVar, Var ); addMethodChaining( toConst, Const ); addMethodChaining( toVarIntent, VarIntent );其中createVar是通过nodeProxy( VarNode )生成的代理工厂。整理成表格如下| 工厂函数 | 链式方法 | 参数 | 说明 | | -- | -- | -- | -- | |Var( node, name null )|.toVar( name )|node任意节点name可选变量名 | 将节点转换为着色器中可复用的变量 | |Const( node, name null )|.toConst( name )| 同上 | 将节点转换为只读常量内部以readOnly true构造 | |VarIntent( node )|.toVarIntent()|node任意节点 | 创建变量意图节点用于赋值场景而无需新建变量 |三者在构造后都会额外调用.toStack()——通过addMethodChaining(toStack, Stack)见 TSLCore.js把变量挂入当前作用域栈确保声明出现在正确的位置。对应地TSL.md 的 Variables 章节给出了精简清单| Name | Description | | -- | -- | |.toVar( node, name null )或Var( node, name null )| Converts a node into a reusable variable in the shader. | |.toConst( node, name null )或Const( node, name null )| Converts a node into an inline constant. | |property( type, name null )| Declares a property but does not assign an initial value. |同时文档强调name 是可选的置null时系统自动生成手动创建变量、常量或属性有助于手工优化着色器图、方便调试例如让最终生成的代码中出现语义化变量名depth而不是一长串内联表达式。典型示例const uvScaled uv().mul( 10 ).toVar(); material.colorNode texture( map, uvScaled );这里uv().mul(10)的结果被提取为变量uvScaled后续采样直接引用变量避免在多次使用时重复计算缩放后的 UV。六、代码生成阶段generate的深入行为变量声明的真正产出发生在generate()方法中VarNode.js。该方法揭示了文档参数背后不为人知的细节1. void 类型约束const nodeType this.getNodeType( builder ); if ( nodeType void ) { if ( this.isIntent( builder ) ! true ) { error( TSL: .toVar() can not be used with void type., this.stackTrace ); } const snippet node.build( builder ); return snippet; }对无返回值void的表达式调用.toVar()会直接抛出错误TSL: .toVar() can not be used with void type.这是新手常见的坑。仅当节点处于意图模式下才被容忍此时直接透传内部节点的构建结果。2. readOnly 与确定性determinism文档中 readOnly 默认false的表象之下generate()中隐藏着更精细的策略const isWebGPUBackend renderer.backend.isWebGPUBackend true; let isDeterministic false; let shouldTreatAsReadOnly false; if ( readOnly ) { isDeterministic builder.isDeterministic( node ); shouldTreatAsReadOnly isWebGPUBackend ? readOnly : isDeterministic; }WebGPU 后端只要构造时readOnly为真即Const/.toConst()一律按只读处理WebGL 后端仅当节点同时被判定为确定性isDeterministic即可安全重复求值时才按只读处理。随后生成的声明前缀也因此区分if ( shouldTreatAsReadOnly ) { if ( isWebGPUBackend ) { declarationPrefix isDeterministic ? const ${ propertyName } : let ${ propertyName }; } else { const count node.getArrayCount( builder ); declarationPrefix const ${ builder.getVar( nodeVar.type, propertyName, count ) }; } }可见在 WebGPU 后端非确定性节点会降级为let而 WebGL 后端在只读场景一律生成const声明GLSL 的 const 要求编译期常量因此需要确定性前提若后端为 WebGL 却要求严格只读则shouldTreatAsReadOnly会在非确定性时被否决。最终变量通过builder.addLineFlowCode(${ declarationPrefix } ${ snippet }, this )以一行赋值语句写入生成代码并返回变量名propertyName供后续节点引用。3. 作用域栈与循环 / 代码块处理build()中有一段专门处理变量位于循环nodeLoop或代码块nodeBlock上下文的逻辑VarNode.js。对于带循环的内部 shader 函数调用会把变量标记forceDeclaration true这也解释了isIntent()为什么需要检查该标志并将声明addToStackBefore( this )提前到栈的正确位置防止变量在循环体内被重复声明。4. 共享节点短路build()开头还会通过getShared( builder )判断若当前节点并非共享实例this ! refNode则直接委托给共享节点的build()保证同一 VarNode 在依赖图中只生成一次声明。七、VarNode 与 Node 基类的协作最后值得强调 VarNode 与 Node.js 的协作边界。从源码与文档的 Overrides 标注可以看出VarNode 是有选择地覆盖基类行为| 属性 / 行为 | Node 基类默认 | VarNode 覆盖值 | 意义 | | -- | -- | -- | -- | |name|空字符串 |null触发自动命名 | 见第二节自动命名机制 | |global|false|true| 变量声明可在当前 shader 阶段内全局缓存复用 | |parents| — |true| 声明节点需要父作用域栈配合.toStack() | |isVarNode| 无 |true| 类型快速判别 |继承关系决定了 VarNode 天然具备 Node 的全部基础设施事件分发、类型系统、build/analyze/generate生命周期等而它只对与变量化强相关的属性做了覆写保持了最小的定制面。结语VarNode 是理解 three.js TSL 代码生成机制的绝佳切片从.toVar()的链式入口、Var/Const/VarIntent三个工厂到intent标志对赋值场景的优化再到generate()中跨 WebGL / WebGPU 后端、readOnly 与确定性交织的声明策略每一个设计都直接决定最终着色器的质量与正确性。当你下次编写 TSL 材质时无论是用toVar复用昂贵的子表达式、用toConst固化常量还是用toVarIntent表达赋值意图本质都是在与 VarNode 打交道。若想进一步深入可直接阅读 VarNode 完整实现、跟踪 NodeBuilder 的变量注册逻辑或参考 TSL 文档的 Variables 章节 与 Node 基类文档 对照学习。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表