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 如何把“在顶点阶段计算、在片元阶段使用”的数据自动跨阶段传递,以及interpolationType、interpolationSampling、needsInterpolation这几个属性如何最终落到 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 )时不传后两个参数,因此从父类继承下来的属性为:
| 属性 | 类型 | 说明 | 默认值 |
|---|---|---|---|
name | string | 变量在 shader 中的名字 | 构造参数 |
type | string | 变量类型(如vec3、float) | 构造参数 |
readOnly | boolean | 只读标志,NodeVarying 调用 super 时不传 | false |
count | number \| null | 大小,NodeVarying 调用 super 时不传 | null |
isNodeVar | boolean(readonly) | 类型测试标志 | true |
三、构造函数参数详解
官方文档定义的构造签名为:
new NodeVarying( name : string, type : string, interpolationType : string, interpolationSampling : string )对照 源码构造器constructor( name, type, interpolationType = null, interpolationSampling = null ),四个参数依次为:
- name(
string):varying 的名字,最终成为 shader 中的声明名。若由节点系统自动创建而未指定名字,builder 会按nodeVarying<index>规则生成(见下文第五节)。 - type(
string):varying 的类型,例如vec3。builder 创建时若未显式传入,默认取源节点的getNodeType( builder )结果。 - interpolationType(
string,默认null):插值类型。null表示走 shader 默认插值规则;非null时 builder 会把它映射为具体的插值限定符(GLSL 侧的映射见第六节)。 - interpolationSampling(
string,默认null):插值采样类型(采样位置)。同样null表示使用默认采样,非null时生成对应的采样限定符。
用户通常不直接 new 这个类,而是通过 TSL 的varying()函数与.setInterpolation()链式方法设置这两个插值字段,再由 builder 转发给 NodeVarying。
四、属性说明
NodeVarying 自有四个属性:
.interpolationSampling : string
varying 数据的插值采样类型。默认null。对应 WGSL 的@interpolate(type, sampling)中第二个参数(如sample、centroid、either)。
.interpolationType : string
varying 数据的插值类型。默认null。对应 WGSL@interpolate()的第一个参数(如flat、perspective、linear);在 GLSL 侧会被翻译为smooth、noperspective等限定符。
.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; }type与sampling应使用 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; }从这段实现可以确认文档所述的两点事实:
- 数组维护:
NodeBuilder构造函数中this.varyings = [](NodeBuilder.js#L344-L349),每次创建都push进去,与文档“An array of node varyings is maintained inNodeBuilder#varyings”一致; - 懒创建 + 缓存:同一 varying 节点在多个阶段被访问时,先查
nodeData[subBuildVarying]缓存,未命中才new NodeVarying(...),保证一次构建只产生一个实例;未命名时自动命名为nodeVarying<index>。
创建后立即调用 registerDeclaration 注册进当前 shader 阶段的声明表,并带自动重名/保留字处理:若名字冲突或是保留关键字,会依次尝试baseName_1、baseName_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;随后:- 若
needsInterpolation为true:- 有
interpolationType时,查 interpolationTypeMap 与 interpolationModeMap 后生成`${interpolationType} ${sampling} out ${type} ${name};`; - 没有
interpolationType时走启发式:类型名包含int、uv、iv的自动加flat前缀;
- 有
- 若
needsInterpolation为false,不生成 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→ GLSLsmooth,LINEAR→noperspective,采样模式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 构建中的完整生命周期为:
- 用户书写:
varying( node, 'vFoo' )/node.toVarying( 'vFoo' ),可选.setInterpolation( type, sampling )设置插值(VaryingNode.js#L198-L220); - setup 阶段:
VaryingNode#setupVarying调用 NodeBuilder#getVaryingFromNode,首次访问时new NodeVarying( name, type, interpolationType, interpolationSampling ),push 进builder.varyings并注册声明;fragment 阶段引用时置needsInterpolation = true; - generate 阶段:vertex 阶段生成赋值语句(或 WGSL 结构体写入
varyings.<name>),fragment 阶段把源计算流回放到 vertex 阶段; - 声明输出: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 )。 - 优化语义:
needsInterpolation为false时 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),仅供参考