three.js TSL 中的 NodeVarying:节点系统如何生成与管理着色器 Varying
2026/9/8 16:58:53 网站建设 项目流程

three.js TSL 中的 NodeVarying:节点系统如何生成与管理着色器 Varying

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

NodeVarying 是 three.js 节点系统(TSL,Three Shading Language)构建管线中的一个内部核心类。它在NodeBuilder构建节点树时被创建,代表最终生成的着色器中的 varying 声明;所有 varying 实例统一保存在NodeBuilder#varyings数组中。读懂 NodeVarying,就理解了 TSL 如何把“在顶点阶段计算、在片元阶段使用”的数据自动跨阶段传递,以及interpolationTypeinterpolationSamplingneedsInterpolation这几个属性如何最终落到 GLSL/WGSL 代码上。读完本篇,你能掌握 varying 从创建、声明注册到 shader 代码生成的完整生命周期,并知道如何通过varying().toVarying().setInterpolation()正确控制插值行为。

一、类定位与源码

NodeVarying 的完整源码位于 src/nodes/core/NodeVarying.js,实现非常精简——它的全部职责就是承载一个 varying 的元数据:

// src/nodes/core/NodeVarying.js class NodeVarying extends NodeVar { constructor( name, type, interpolationType = null, interpolationSampling = null ) { super( name, type ); // Whether this varying requires interpolation or not. // 可用于检查该 varying 是否可以被优化为普通变量 this.needsInterpolation = false; // 类型测试标志,readonly,默认 true this.isNodeVarying = true; // 插值类型,默认 null this.interpolationType = interpolationType; // 插值采样类型,默认 null this.interpolationSampling = interpolationSampling; } }

同时它通过 src/nodes/Nodes.js 的导出列表对外暴露(export { default as NodeVarying } from './core/NodeVarying.js'),是节点系统的公开 API 之一。

需要强调:NodeVarying 是构建期产物,而不是用户直接操作的节点。用户操作的是 VaryingNode(对应 TSL 函数varying()),NodeVarying 只是 VaryingNode 在NodeBuilder中对应的声明数据。

二、继承链:NodeVar 提供的基类属性

NodeVarying 继承自 NodeVar。NodeVar 代表“builder 最终生成的着色器变量”,由NodeBuilder#vars字典维护;而 NodeVarying 代表“跨 shader 阶段的变量”,由NodeBuilder#varyings数组维护。

父类构造签名为new NodeVar( name, type, readOnly = false, count = null ),NodeVarying 调用super( name, type )时不传后两个参数,因此从父类继承下来的属性为:

属性类型说明默认值
namestring变量在 shader 中的名字构造参数
typestring变量类型(如vec3float构造参数
readOnlyboolean只读标志,NodeVarying 调用 super 时不传false
countnumber \| null大小,NodeVarying 调用 super 时不传null
isNodeVarboolean(readonly)类型测试标志true

三、构造函数参数详解

官方文档定义的构造签名为:

new NodeVarying( name : string, type : string, interpolationType : string, interpolationSampling : string )

对照 源码构造器constructor( name, type, interpolationType = null, interpolationSampling = null ),四个参数依次为:

  • namestring):varying 的名字,最终成为 shader 中的声明名。若由节点系统自动创建而未指定名字,builder 会按nodeVarying<index>规则生成(见下文第五节)。
  • typestring):varying 的类型,例如vec3。builder 创建时若未显式传入,默认取源节点的getNodeType( builder )结果。
  • interpolationTypestring,默认null):插值类型。null表示走 shader 默认插值规则;非null时 builder 会把它映射为具体的插值限定符(GLSL 侧的映射见第六节)。
  • interpolationSamplingstring,默认null):插值采样类型(采样位置)。同样null表示使用默认采样,非null时生成对应的采样限定符。

用户通常不直接 new 这个类,而是通过 TSL 的varying()函数与.setInterpolation()链式方法设置这两个插值字段,再由 builder 转发给 NodeVarying。

四、属性说明

NodeVarying 自有四个属性:

.interpolationSampling : string

varying 数据的插值采样类型。默认null。对应 WGSL 的@interpolate(type, sampling)中第二个参数(如samplecentroideither)。

.interpolationType : string

varying 数据的插值类型。默认null。对应 WGSL@interpolate()的第一个参数(如flatperspectivelinear);在 GLSL 侧会被翻译为smoothnoperspective等限定符。

.isNodeVarying : boolean (readonly)

类型测试标志,默认为true。builder 生成属性名时用它识别 varying 节点——例如 WGSLNodeBuilder#getPropertyName 中判断node.isNodeVarying === true && node.needsInterpolation === true后,在 vertex 阶段返回varyings.<name>前缀,使写入落到 WGSL 的VaryingsStruct上。

.needsInterpolation : boolean

该 varying 是否需要插值,默认false。文档中特别说明:此属性可用于检查该 varying 是否可以被优化为普通变量(variable)。这一点是理解整个 varying 管线的关键:

  • 当一个数据只在单个 shader 阶段使用(例如只由 fragment 读取、但顶点阶段从未“消费”它),构建时needsInterpolation保持false,builder 会把它降级生成一个普通 shader 变量,而不是真正跨阶段的插值 varying,从而减少寄存器与插值开销;
  • 一旦被 fragment 阶段引用,就会被置为true,正式生成跨阶段声明。

置位逻辑在 VaryingNode#setupVarying 中:

setupVarying( builder ) { const properties = builder.getNodeProperties( this ); let varying = properties.varying; if ( varying === undefined ) { const name = this.name; const type = this.getNodeType( builder ); const interpolationType = this.interpolationType; const interpolationSampling = this.interpolationSampling; properties.varying = varying = builder.getVaryingFromNode( this, name, type, interpolationType, interpolationSampling ); properties.node = subBuild( this.node, 'VERTEX' ); } // this property can be used to check if the varying can be optimized for a variable varying.needsInterpolation || ( varying.needsInterpolation = ( builder.shaderStage === 'fragment' ) ); return varying; }

注意第 140 行的短路赋值:只有当构建器当前处于fragment阶段时才把needsInterpolation置真。这意味着“顶点阶段计算、片元阶段读取”这一使用模式才会触发真正的插值 varying 生成。

五、创建链路:从 VaryingNode 到 NodeVarying

5.1 用户侧 API:varying()、toVarying()、vertexStage()

VaryingNode 的源码注释给出了标准用法:

const positionLocal = positionGeometry.toVarying( 'vPositionLocal' );

VaryingNode 文件末尾 同时导出三个 TSL 入口:

// TSL 函数:创建 varying 节点 export const varying = /*@__PURE__*/ nodeProxy( VaryingNode ).setParameterLength( 1, 2 ); // 语义别名:把节点的计算“钉”在 vertex 阶段 export const vertexStage = ( node ) => varying( node ); addMethodChaining( 'toVarying', varying ); addMethodChaining( 'toVertexStage', vertexStage );

varying(node, name)node.toVarying(name)node.toVertexStage()三种写法等价。VaryingNode 构造器还会把源节点包一层subBuild( node, 'VERTEX' ),表示“源节点在顶点阶段子构建中执行”。

设置插值的方式是链式方法 setInterpolation:

setInterpolation( type, sampling = null ) { this.interpolationType = type; this.interpolationSampling = sampling; return this; }

typesampling应使用 three.js 暴露的InterpolationSamplingType/InterpolationSamplingMode常量。仓库示例 examples/webgpu_centroid_sampling.html 中有真实用法:

testUV.setInterpolation( type, sampling ); // ... testUV.setInterpolation( THREE.InterpolationSamplingType.PERSPECTIVE, THREE.InterpolationSamplingMode.SAMPLE ); testUV.setInterpolation( THREE.InterpolationSamplingType.PERSPECTIVE, THREE.InterpolationSamplingMode.CENTROID );

而在 examples/jsm/generators/city/SkyscraperGenerator.js 中可以看到 flat 插值的典型场景——逐面片的面 id 绝不能被光栅器插值,否则按面片取整后比较会失败:

const partId = varying( attribute( 'partId', 'float' ) ) .setInterpolation( InterpolationSamplingType.FLAT, InterpolationSamplingMode.EITHER );

另一个toVarying()的实际用例见 examples/webgpu_compute_cloth.html:把变换到视图空间的法线提升到顶点阶段计算(material.normalNode = transformNormalToView( normal ).toVarying()),避免在片元阶段重复计算。

5.2 builder 侧:getVaryingFromNode()

VaryingNode 在 setup 阶段把创建请求交给 builder。核心方法 NodeBuilder#getVaryingFromNode:

getVaryingFromNode( node, name = null, type = node.getNodeType( this ), interpolationType = null, interpolationSampling = null ) { const nodeData = this.getDataFromNode( node, 'any' ); const subBuildVarying = this.getSubBuildProperty( 'varying', nodeData.subBuilds ); let nodeVarying = nodeData[ subBuildVarying ]; if ( nodeVarying === undefined ) { const varyings = this.varyings; const index = varyings.length; if ( name === null ) name = 'nodeVarying' + index; // 自动命名 // 支持子构建(subBuild)场景下的命名前缀 if ( subBuildVarying !== 'varying' ) { name = this.getSubBuildProperty( name, nodeData.subBuilds ); } // nodeVarying = new NodeVarying( name, type, interpolationType, interpolationSampling ); varyings.push( nodeVarying ); this.registerDeclaration( nodeVarying ); nodeData[ subBuildVarying ] = nodeVarying; } return nodeVarying; }

从这段实现可以确认文档所述的两点事实:

  1. 数组维护NodeBuilder构造函数中this.varyings = [](NodeBuilder.js#L344-L349),每次创建都push进去,与文档“An array of node varyings is maintained inNodeBuilder#varyings”一致;
  2. 懒创建 + 缓存:同一 varying 节点在多个阶段被访问时,先查nodeData[subBuildVarying]缓存,未命中才new NodeVarying(...),保证一次构建只产生一个实例;未命名时自动命名为nodeVarying<index>

创建后立即调用 registerDeclaration 注册进当前 shader 阶段的声明表,并带自动重名/保留字处理:若名字冲突或是保留关键字,会依次尝试baseName_1baseName_2……并打印TSL: Declaration name ... is a reserved keyword or already in use. Renamed to ...的警告。这也是为什么用户给 varying 起的名字可能与最终 shader 中的名字略有差异。

此外 PropertyNode#generate 中也有第二条进入路径:node.varing标记为true的属性节点会调用builder.getVaryingFromNode( this, this.name )并强制nodeVar.needsInterpolation = true

5.3 generate:顶点写、片元读

VaryingNode#generate 决定了这个 varying 在两个阶段各自生成什么代码:

  • vertex 阶段:构建源节点代码并输出propertyName = <snippet>;,即把计算结果写入 varying;
  • fragment 阶段:调用builder.flowNodeFromShaderStage( NodeShaderStage.VERTEX, ... ),把源节点的计算流“拉”到顶点阶段执行,当前阶段只返回 varying 的属性名供读取。

这就是 TSL 中“一个节点,跨阶段求值”的底层机制。

六、从元数据到 shader 代码:两种 builder 的生成策略

NodeVarying 的四个字段最终由两个 NodeBuilder 消费并翻译为具体着色语言。

6.1 GLSL(WebGL fallback):interpolationTypeMap

GLSLNodeBuilder#getVaryings 逐条遍历this.varyings,按阶段分别生成out/in声明:

  • vertex / compute 阶段compute阶段会强制varying.needsInterpolation = true;随后:
    • needsInterpolationtrue
      • interpolationType时,查 interpolationTypeMap 与 interpolationModeMap 后生成`${interpolationType} ${sampling} out ${type} ${name};`
      • 没有interpolationType时走启发式:类型名包含intuviv的自动加flat前缀;
    • needsInterpolationfalse不生成 varying,而是直接生成一条普通变量声明(源码注释:generate variable (no varying required))——这正是needsInterpolation优化在 GLSL 侧的体现。
  • fragment 阶段:只对needsInterpolation === true的 varying 生成对应的in声明,规则与 vertex 对称。

映射表本身定义了 TSL 插值常量到 GLSL 限定符的对应关系:

// src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js const interpolationTypeMap = { perspective: 'smooth', linear: 'noperspective' }; const interpolationModeMap = { 'centroid': 'centroid' };

InterpolationSamplingType.PERSPECTIVE→ GLSLsmoothLINEARnoperspective,采样模式centroid原样透传;未命中的取值按原字符串直接拼入声明。

6.2 WGSL(WebGPU):@location 与 @interpolate

WGSLNodeBuilder#getVaryings 生成 WGSL 的 varying 参数/结构体字段:

  • needsInterpolation === true的 varying:分配递增的@location( N )编号,并拼出@interpolate( <type>[, <sampling> ] )修饰;若未显式指定插值但类型以int/uint/ivec/uvec开头,自动补@interpolate(flat, either)(整数 varying 不可插值,这是语义要求而非优化);
  • needsInterpolation === false且阶段为vertex的 varying:不调用插值通道,而是 push 进vars[ shaderStage ],作为普通私有变量参与编译——同样是“优化为 variable”的策略;
  • vertex 阶段最终把全部声明组装成struct VaryingsStruct_getWGSLStruct),主函数形如fn main( attributes ) -> VaryingsStruct { ... return varyings; }(见 WGSL 模板),fragment 阶段则以fn main( ${varyings} )接收。

写入路径的命名同样依赖 NodeVarying 字段:getPropertyName 对isNodeVarying && needsInterpolation的节点在 vertex 阶段返回varyings.<name>,使顶点代码的赋值落到结构体成员上。

七、完整生命周期小结

综合上述源码,一个 varying 在 TSL 构建中的完整生命周期为:

  1. 用户书写varying( node, 'vFoo' )/node.toVarying( 'vFoo' ),可选.setInterpolation( type, sampling )设置插值(VaryingNode.js#L198-L220);
  2. setup 阶段VaryingNode#setupVarying调用 NodeBuilder#getVaryingFromNode,首次访问时new NodeVarying( name, type, interpolationType, interpolationSampling ),push 进builder.varyings并注册声明;fragment 阶段引用时置needsInterpolation = true
  3. generate 阶段:vertex 阶段生成赋值语句(或 WGSL 结构体写入varyings.<name>),fragment 阶段把源计算流回放到 vertex 阶段;
  4. 声明输出:GLSLNodeBuilder#getVaryings / WGSLNodeBuilder#getVaryings 把name/type/interpolationType/interpolationSampling翻译成out/in声明、@location+@interpolate修饰,或按needsInterpolation === false降级为普通变量。

八、使用注意与边界

  • NodeVarying 不要手动构造:它由 builder 在构建期创建并缓存,手动 new 出来的实例不会进入builder.varyings,也不会被注册为声明,不会出现在最终 shader 中;需要跨阶段数据时请走varying()/toVarying()
  • 名字冲突会自动重命名registerDeclaration会处理保留字与重名,重命名后仅打印警告(NodeBuilder.js#L2271-L2299),若你在调试中比对生成的 shader 源码,注意这一点。
  • 整数类型 varying 的 flat 处理:GLSL 侧按类型名启发式推断flat,WGSL 侧按正则(int|uint|ivec|uvec)推断并显式生成@interpolate(flat, either);如需精确控制,建议仍显式调用setInterpolation( InterpolationSamplingType.FLAT, InterpolationSamplingMode.EITHER )
  • 优化语义needsInterpolationfalse时 varying 会被降级为普通变量,这在 WGSL vertex 阶段表现为跳过@location分配、改走vars,在 GLSL 阶段表现为仅生成一条无out/in的变量声明。

参考路径

  • 核心实现:src/nodes/core/NodeVarying.js、src/nodes/core/NodeVar.js
  • 创建与注册:src/nodes/core/NodeBuilder.js、src/nodes/core/PropertyNode.js
  • 节点 API:src/nodes/core/VaryingNode.js
  • 代码生成:src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js、src/renderers/webgpu/nodes/WGSLNodeBuilder.js
  • 官方文档页:docs/pages/NodeVarying.html.md

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询