首页
/ Three.js TSL 中的 ConstNode 深度解析:着色器常量的类型推导与代码生成机制

Three.js TSL 中的 ConstNode 深度解析:着色器常量的类型推导与代码生成机制

2026-09-06 15:53:55作者:伍希望

ConstNode 是 Three.js 节点着色语言(TSL, Three Shading Language)中最基础的输入节点之一,负责把 JavaScript 世界的常量值(数字、布尔值、向量、矩阵、颜色等)映射为 GPU 着色器代码中的字面量。本文以 ConstNode 官方 API 文档 为骨架,结合 src/nodes/core/ConstNode.jssrc/nodes/core/InputNode.jssrc/nodes/core/NodeBuilder.jssrc/nodes/tsl/TSLCore.js 的源码实现,完整讲解其构造参数、类型推导规则、着色器字符串生成逻辑,以及它在 floatvec3 等 TSL 便捷函数背后的实际角色,帮助你在编写自定义 TSL 材质时准确理解并控制常量的类型与生成结果。

一、类定位与继承体系

官方文档给出的继承链为:

EventDispatcher → Node → InputNode → ConstNode
  • Node:所有节点的基类,持有 nodeType、支持序列化/反序列化与 build/generate 生成流程;
  • InputNode(见 src/nodes/core/InputNode.js):数据输入节点的基类,定义了 .value(节点承载的 JS 值)、.precision(着色器精度,可取 'low'|'medium'|'high',默认 null)等公共属性,并提供 serialize/deserialize 实现——注意它在序列化时若值带有 toArray 方法(如 Vector3)会存为数组,ArrayBuffer 会转 Base64,反序列化时再通过 fromArray 还原;
  • ConstNode:在 InputNode 之上仅新增了 .isConstNode = true 标志位,以及针对"常量语义"的代码生成逻辑。

文档中的 .isConstNode : boolean (readonly) 属性即定义在 src/nodes/core/ConstNode.js#L35

constructor( value, nodeType = null ) {

    super( value, nodeType );

    /**
     * This flag can be used for type testing.
     */
    this.isConstNode = true;

}

这个标志除了用于 instanceof 之外的类型测试(if ( node.isConstNode )),还有一个实际用途:TSL 的 defined() 工具函数会遍历节点树,把 isConstNode 的节点解包为其原始 JS 值再做布尔判断,见 src/nodes/tsl/TSLCore.js#L942-L960

export function defined( value ) {

    if ( value && value.isNode ) {

        value.traverse( ( node ) => {

            if ( node.isConstNode ) {

                value = node.value;

            }

        } );

    }

    return Boolean( value );

}

这意味着 defined( x < 5.0 ) 这类表达式可以在 JS 侧直接求值出常量结果,而不必等到着色器运行。

二、构造函数:value 与 nodeType 两个参数

文档定义的构造函数签名为:

new ConstNode( value : any, nodeType : string )

value 参数

节点的 JS 值,可以是 JS 原始类型(number、boolean、string),也可以是 three.js 对象(Vector2/3/4、Matrix2/3/4、Color 等)。从 InputNode 的 JSDoc 看,甚至允许函数、数组缓冲区等,但实践中以原始值和 three.js 数学对象为主。

nodeType 参数(默认 null)

显式着色器类型。关键点在于默认值为 null,此时节点会尝试从 value 推导类型。这一行为由 InputNode 的 generateNodeType 实现(见 src/nodes/core/InputNode.js#L54-L64):

generateNodeType( /*builder*/ ) {

    if ( this.nodeType === null ) {

        return getValueType( this.value );

    }

    return this.nodeType;

}

getValueTypesrc/nodes/core/NodeUtils.js 中的工具函数,根据 JS 值映射出 GLSL 类型:整数映射为 float 还是 int 等细节由其内部规则决定。这带来一个常见陷阱——在 TSL 中直接 new ConstNode( 1 ) 得到的类型未必是你预期的 int

TSL 源码本身就体现了这种"显式传类型"的用法。src/nodes/tsl/TSLCore.js#L859-L863 在构建常量缓存时,对无符号/有符号整数都显式指定了 nodeType:

const uints = [ 0, 1, 2, 3 ];
const ints = [ - 1, - 2 ];

const uintsCacheMap = new Map();
for ( const uint of uints ) uintsCacheMap.set( uint, new ConstNode( uint, 'uint' ) );

const intsCacheMap = new Map( [ ...uintsCacheMap ].map( el => new ConstNode( el.value, 'int' ) ) );
for ( const int of ints ) intsCacheMap.set( int, new ConstNode( int, 'int' ) );

同样,数组下标访问也依赖显式类型的常量节点,如 src/nodes/tsl/TSLCore.js#L228 中的 new ArrayElementNode( this, new ConstNode( i, 'uint' ) )——用 uint 常量作为索引,保证生成 arr[ 2u ] 这类合法的 GLSL 索引。

三、核心方法:generateConst 与 generate

.generateConst( builder ) : string

文档描述:"Generates the shader string of the value with the current node builder." 源码实现只有两行(见 src/nodes/core/ConstNode.js#L45-L49):

generateConst( builder ) {

    return builder.generateConst( this.getNodeType( builder ), this.value );

}

即:先通过 getNodeType( builder ) 确定最终类型(显式 nodeType 或从 value 推导的类型),再委托给 NodeBuilder.generateConst( type, value ) 完成真正的字符串化。

NodeBuilder 侧的实现(src/nodes/core/NodeBuilder.js#L1410-L1459)值得逐段理解,它定义了所有常量到 GLSL 字面量的映射规则:

  1. null 值兜底value === null 时按类型生成默认值——数值型为 0boolfalsecolornew Color(),各向量类型为空的 Vector2/3/4
  2. 标量
    • float_toFloat(保证字面量带小数点,如 0.5);
    • intMath.round 取整输出,如 5
    • uint 输出 u 后缀,如 5u,且负数一律输出 0u(GLSL 无符号数不允许负字面量);
    • bool 输出 true / false
    • color 输出为 vec3( r, g, b )getType( 'color' ) 返回 vec3,见 src/nodes/core/NodeBuilder.js#L1468-L1474);
  3. 向量/矩阵:通过 getTypeLengthgetComponentType 递归分解——vec2/vec3/vec4x/y/z/w 分量递归生成并拼成 vecN( ... )mat2/3/4elements 数组递归生成(注意 mat 构造函数要求按列传入);
  4. 无法识别的类型会抛出 THREE.NodeBuilder: Type 'xxx' not found in generate constant attempt.

generate( builder, output ):数值型的特判

文档未列出但源码中实际存在 generate 方法(src/nodes/core/ConstNode.js#L51-L63):

generate( builder, output ) {

    const type = this.getNodeType( builder );

    if ( _regNum.test( type ) && _regNum.test( output ) ) {

        return builder.generateConst( output, this.value );

    }

    return builder.format( this.generateConst( builder ), type, output );

}

文件顶部的 _regNum = /float|u?int/ 只匹配数值类型。逻辑是:

  • 源类型与目标输出类型都是数值float/int/uint 之间):直接以 output 类型重新生成常量字面量,避免生成 float( 5u ) 这类冗余转换;
  • 其他情况(如向量到向量的宽度差异、类型族不同):先生成本类型的常量字符串,再交给 builder.format( value, type, output ) 完成类型转换封装。

这就是 float( x )vec3( color ) 等 TSL 隐式转换在常量节点上的落地路径。

四、TSL 入口:float / int / uint / bool / vecN 与常量缓存

对绝大多数使用者而言,不会直接 new ConstNode,而是通过 TSL 导出的类型转换函数创建常量节点。src/nodes/tsl/TSLCore.js#L1213-L1230 中的导出全部由 ConvertType 工厂生成:

export const float = new ConvertType( 'float', cacheMaps.float );
export const int   = new ConvertType( 'int',   cacheMaps.ints  );
export const uint  = new ConvertType( 'uint',  cacheMaps.uint  );
export const bool  = new ConvertType( 'bool',  cacheMaps.bool  );

export const vec2 = new ConvertType( 'vec2' );
export const vec3 = new ConvertType( 'vec3' );
export const vec4 = new ConvertType( 'vec4' );
// ... ivec2 / uvec2 / ivec3 / uvec3 / uvec4 / ivec4 / uvec4

ConvertTypesrc/nodes/tsl/TSLCore.js#L891-L938)的行为:

  • 单参数:调用 getConstNode( param, type ) 创建 ConstNode;若推导出的类型与目标类型不一致(例如 float( 5 ) 传入了整数),会自动包一层 ConvertNode 做显式转换;
  • 多参数:把每个参数转为 ConstNode 后交给 JoinNode 拼装,例如 vec3( 1.0, 2.0, 3.0 )
  • undefined 参数:报错并返回 new ConstNode( 0, type ) 作为占位,保证错误可被定位而不是静默失败。

getConstNodesrc/nodes/tsl/TSLCore.js#L873-L889)则是一层常量缓存

const getConstNode = ( value, type ) => {

    if ( constNodesCacheMap.has( value ) ) {

        return constNodesCacheMap.get( value );

    } else if ( value.isNode === true ) {

        return value;

    } else {

        return new ConstNode( value, type );

    }

};

缓存表在模块加载时预建(src/nodes/tsl/TSLCore.js#L851-L871),覆盖高频值:true/false0~3(uint)、-1/-2(int),以及 0.51/31e-6Math.PI2/Math.PI 等常用浮点常量及其负值。命中缓存时复用同一个 ConstNode 实例——这既节省对象分配,也让相同常量在整个节点树中共享身份,便于下游的合并与优化。同时它兼容"传入的已经是 Node"的情况,直接透传。

另一个细节:ShaderNodeObject(即 TSL 的 nodeObject 包装入口,src/nodes/tsl/TSLCore.js#L294-L313)在解析原始值时,会先经 getValueType 判断——floatboolean 类型(以及指定了 altType 的场景)一律走 getConstNode 转成 ConstNode。也就是说,在 TSL 表达式里书写的任何数字字面量,最终几乎都会落成一个 ConstNode

五、实用示例

直接构造 ConstNode

import * as THREE from 'three';
import { ConstNode } from 'three/addons/nodes/Nodes.js';

// 显式指定类型,避免依赖推导
const offset = new ConstNode( 1.5, 'float' );     // -> 1.5
const index  = new ConstNode( 2, 'uint' );        // -> 2u
const tint   = new ConstNode( new THREE.Color( 0.2, 0.4, 0.8 ) ); // 推导为 color

console.log( tint.nodeType ); // 'color'

通过 TSL 类型函数(推荐写法)

import { vec3, float, int, bool } from 'three/addons/nodes/TSL.js';

const baseColor  = vec3( 0.1, 0.2, 0.3 );  // JoinNode<vec3> 包裹三个 float 常量
const strength   = float( 2.0 );          // ConstNode<float>
const count      = int( 8 );              // ConstNode<int>
const enabled    = bool( true );         // ConstNode<bool>

// 常量可参与任意 TSL 表达式,最终由 NodeBuilder 生成 GLSL 字面量
const result = vec3( 1.0, 2.0, 3.0 ).add( float( 0.5 ) );

从源码结构看,vec3( 1.0, 2.0, 3.0 ) 会命中 float 缓存表(1.0 等值预建在 floatsCacheMap),三个 ConstNodeJoinNode 组装;若写成 vec3( 1.5 + 0.5, 2.0, 3.0 ) 这类无法预计算的形式,则 1.5 + 0.5 会走 TSL 的加法节点而非常量折叠,这是两种写法的本质区别。

生成结果对照

以 NodeBuilder 的规则推演几个典型生成结果:

构造 生成的 GLSL 字面量
new ConstNode( 2, 'uint' ) 2u
new ConstNode( -1, 'int' ) -1
new ConstNode( 1e-6 ) 0.000001_toFloat 格式化)
new ConstNode( new THREE.Vector3( 1, 2, 3 ) ) vec3( 1.0, 2.0, 3.0 )
new ConstNode( new THREE.Color( 1, 0, 0.5 ) ) vec3( 1.0, 0.0, 0.5 )

六、小结与延伸阅读

  • ConstNode 本身很薄:它只负责"持值 + 标记 + 委托生成",真正的 GLSL 字面量规则集中在 NodeBuilder.generateConst,理解生成结果应同时阅读两处;
  • 类型是常量的第一公民nodeType 留空时的推导规则决定了 1 到底是 float 还是 int,涉及整数语义(索引、位运算)时应显式传 'int'/'uint',TSL 内部的缓存构造也全部采用显式类型;
  • 数值间生成会特判免转换generate 中的 _regNum 短路让 float/int/uint 互转直接重写字面量,这是常量节点生成路径与一般节点路径的重要差异;
  • TSL 缓存层让常用数字常量共享节点实例,getConstNodeConvertTypedefined 三个入口(src/nodes/tsl/TSLCore.js)是理解"字面量如何变成 ConstNode"的关键代码。

相关文档与源码:ConstNode API 文档InputNode 源码TSL 核心NodeBuilder 源码

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