Three.js TSL 中的 ConstNode 深度解析:着色器常量的类型推导与代码生成机制
ConstNode 是 Three.js 节点着色语言(TSL, Three Shading Language)中最基础的输入节点之一,负责把 JavaScript 世界的常量值(数字、布尔值、向量、矩阵、颜色等)映射为 GPU 着色器代码中的字面量。本文以 ConstNode 官方 API 文档 为骨架,结合 src/nodes/core/ConstNode.js、src/nodes/core/InputNode.js、src/nodes/core/NodeBuilder.js 和 src/nodes/tsl/TSLCore.js 的源码实现,完整讲解其构造参数、类型推导规则、着色器字符串生成逻辑,以及它在 float、vec3 等 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;
}
getValueType 是 src/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 字面量的映射规则:
- null 值兜底:
value === null时按类型生成默认值——数值型为0,bool为false,color为new Color(),各向量类型为空的Vector2/3/4; - 标量:
float走_toFloat(保证字面量带小数点,如0.5);int用Math.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);
- 向量/矩阵:通过
getTypeLength与getComponentType递归分解——vec2/vec3/vec4按x/y/z/w分量递归生成并拼成vecN( ... );mat2/3/4按elements数组递归生成(注意 mat 构造函数要求按列传入); - 无法识别的类型会抛出
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
ConvertType(src/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 )作为占位,保证错误可被定位而不是静默失败。
getConstNode(src/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/false、0~3(uint)、-1/-2(int),以及 0.5、1/3、1e-6、Math.PI、2/Math.PI 等常用浮点常量及其负值。命中缓存时复用同一个 ConstNode 实例——这既节省对象分配,也让相同常量在整个节点树中共享身份,便于下游的合并与优化。同时它兼容"传入的已经是 Node"的情况,直接透传。
另一个细节:ShaderNodeObject(即 TSL 的 nodeObject 包装入口,src/nodes/tsl/TSLCore.js#L294-L313)在解析原始值时,会先经 getValueType 判断——float、boolean 类型(以及指定了 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),三个 ConstNode 被 JoinNode 组装;若写成 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 缓存层让常用数字常量共享节点实例,
getConstNode、ConvertType、defined三个入口(src/nodes/tsl/TSLCore.js)是理解"字面量如何变成 ConstNode"的关键代码。
相关文档与源码:ConstNode API 文档、InputNode 源码、TSL 核心、NodeBuilder 源码。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00