three.js TSL 中 BuiltinNode 详解:直接引用内建着色器变量,支撑硬件裁剪与多视图渲染
BuiltinNode 是 three.js 节点材质系统(TSL,Three Shading Language)中的一个基础访问器节点,继承自 EventDispatcher → Node,用于在节点着色器代码中直接引用"内建着色器变量"(built-in shader variables)。这类变量不是普通的 uniform 或 attribute,而是由 GPU 驱动或渲染后端注入的着色器内置标识符——例如 OpenGL 的 gl_ClipDistance、多视图渲染的 gl_ViewID_OVR,以及 WebGPU WGSL 中通过 @builtin 声明的变量。理解它,你就能明白 three.js 的 NodeMaterial 是如何实现硬件加速顶点裁剪(hardware-accelerated vertex clipping)、以及多目相机(MultiViewCamera)矩阵选择的。
1. 类定义与继承关系
BuiltinNode 的完整实现位于 src/nodes/accessors/BuiltinNode.js,共 63 行:
import Node from '../core/Node.js';
import { nodeProxy } from '../tsl/TSLBase.js';
/**
* The node allows to set values for built-in shader variables. That is
* required for features like hardware-accelerated vertex clipping.
*
* @augments Node
*/
class BuiltinNode extends Node {
constructor( name ) {
super( 'float' );
this.name = name;
this.isBuiltinNode = true;
}
generate( /* builder */ ) {
return this.name;
}
}
export default BuiltinNode;
export const builtin = /*@__PURE__*/ nodeProxy( BuiltinNode ).setParameterLength( 1 );
从源码结构看,这个类的设计极其轻量的核心原因是:它生成的"代码片段"就是变量名本身。generate() 完全忽略 builder 参数,直接 return this.name——也就是说,builtin('gl_ClipDistance') 参与表达式运算后,最终生成的 GLSL/WGSL 文本里就原样出现 gl_ClipDistance 这个标识符,由渲染管线在着色器前注入对应的变量声明(out float gl_ClipDistance[N] 或 @builtin( clip_distances ) 等)。
构造函数与文档对应关系如下:
1.1 构造函数
new BuiltinNode( name : string ) —— 构造一个新的 builtin 节点。
| 参数 | 说明 |
|---|---|
name |
内建着色器变量的名称(字符串),即最终会原样写入生成着色器代码中的标识符 |
需要注意一个细节:构造函数内 super( 'float' ) 将节点类型默认硬编码为 'float'。从源码看,BuiltinNode 没有提供修改 nodeType 的入口,因此它的类型系统定位是"标量浮点变量访问器"。但在实际使用中,three.js 内部的调用方都是把它当作"变量引用"而非完整类型声明来用(见下文 ClippingNode 中 hw_clip_distances.element( i ) 的用法),数组访问等语义由外层表达式节点承担。
1.2 属性
.isBuiltinNode : boolean(readonly) —— 类型测试标志,用于 instanceof 之外的快速类型判别,默认值为 true。
.name : string —— 内建着色器变量的名称。该属性覆写了 Node#name(Node 基类文档)。由于 name 在节点系统中同时充当节点的缓存键,覆写它意味着同名内建变量在哈希层面被视为同一节点,可以复用生成结果。
1.3 方法
.generate( builder : NodeBuilder ) : string —— 生成该 builtin 节点的代码片段。参数 builder 为当前节点构建器(源码中未使用,参数以注释形式保留),返回值即 this.name。同样覆写了 Node#generate。
2. TSL 入口:builtin() 函数
面向 TSL 语法的用户,日常使用的不是 new BuiltinNode(...),而是文件末尾导出的 builtin 函数:
export const builtin = /*@__PURE__*/ nodeProxy( BuiltinNode ).setParameterLength( 1 );
nodeProxy( BuiltinNode )是 TSL 的通用代理机制:返回一个可被@__PURE__注解标记的函数,惰性构造节点实例,从而保证 tree-shaking 友好性;.setParameterLength( 1 )声明该函数只接受 1 个参数(即变量名);- 通过
builtin( name )调用即得到BuiltinNode实例,且可直接参与 TSL 链式运算(.element( i )、.assign( ... )等)。
该函数经 src/nodes/TSL.js(export * from './accessors/BuiltinNode.js')与 src/nodes/Nodes.js(export { default as BuiltinNode } from './accessors/BuiltinNode.js')对外导出,属于 TSL 核心访问器 API 的一部分,参见 docs/TSL.md。
3. 实战场景一:硬件加速顶点裁剪(ClippingNode)
BuiltinNode 最核心的使用场景,正是类注释中提到的 "hardware-accelerated vertex clipping"。在 src/nodes/accessors/ClippingNode.js 中,ClippingNode 提供三种裁剪作用域:default(软件 discard)、alphaToCoverage(alpha-to-coverage 抗锯齿裁剪)、hardware(硬件裁剪)。其中硬件裁剪分支的完整实现为:
setupHardwareClipping( unionPlanes, builder ) {
const numUnionPlanes = unionPlanes.length;
builder.enableHardwareClipping( numUnionPlanes );
return Fn( () => {
const clippingPlanes = uniformArray( unionPlanes ).setGroup( renderGroup );
const hw_clip_distances = builtin( builder.getClipDistance() );
Loop( numUnionPlanes, ( { i } ) => {
const plane = clippingPlanes.element( i );
const distance = positionView.dot( plane.xyz ).sub( plane.w ).negate();
hw_clip_distances.element( i ).assign( distance );
} );
} )();
}
这里 builtin( builder.getClipDistance() ) 就是 BuiltinNode 的典型用法:顶点着色器中逐个计算每个裁剪平面的距离并写入硬件变量,之后由 GPU 光栅化阶段真正丢弃越界顶点,无需片元 discard,因此称为"硬件加速"。对应的 TSL 函数入口为 hardwareClipping():
export const hardwareClipping = () => new ClippingNode( ClippingNode.HARDWARE );
3.1 两个后端对"内建变量名"的不同回答
builder.getClipDistance() 返回什么字符串,取决于当前渲染后端:
- WebGL 回退后端(src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js,第 1415 行附近):
getClipDistance() {
return 'gl_ClipDistance';
}
enableHardwareClipping( planeCount ) {
this.enableExtension( 'GL_ANGLE_clip_cull_distance', 'require' );
this.builtins[ 'vertex' ].push( `out float gl_ClipDistance[ ${ planeCount } ]` );
}
即返回 OpenGL 标准变量 gl_ClipDistance,并在顶点阶段以 out float gl_ClipDistance[N] 声明;同时强制要求 GL_ANGLE_clip_cull_distance 扩展('require' 级别)——这是该功能在 WebGL 下的适用前提。
- WebGPU 后端(src/renderers/webgpu/nodes/WGSLNodeBuilder.js,第 1659 行附近):
getClipDistance() {
return 'varyings.hw_clip_distances';
}
enableHardwareClipping( planeCount ) {
this.enableClipDistances();
this.getBuiltin( 'clip_distances', 'hw_clip_distances', `array<f32, ${ planeCount } >`, 'vertex' );
}
即返回 varying 结构体中的 hw_clip_distances 字段,底层则按 WGSL 的 @builtin( clip_distances ) 内建声明注册一个 array<f32, N> 类型的顶点输出。
也就是说,BuiltinNode 的 name 是"后端无关的抽象":同一个 ClippingNode 节点树,在两个后端上会生成不同但等价的着色器文本。这个间接层完全由 NodeBuilder 承担,BuiltinNode 只负责把名字原样带进表达式。
3.2 对照示例
仓库中还有一个手动操作该内建变量的传统着色器示例 examples/webgl_clipculldistance.html(第 34 行附近),它直接在 GLSL 里写入:
gl_ClipDistance[ 0 ] = worldPosition.x - sin( time ) * ( 0.5 );
这正是 hardwareClipping() 节点化之后的"手工版",两者操作的是同一个 GPU 内建变量,可以对照理解 BuiltinNode 生成的代码在最终着色器中呈现的样子。
4. 实战场景二:多视图渲染中的 gl_ViewID_OVR
BuiltinNode 的另一个内部使用点在 src/nodes/accessors/Camera.js。该模块为 NodeMaterial 生成 cameraProjectionMatrix、cameraViewMatrix、cameraPosition 等相机矩阵访问节点;当相机是 isMultiViewCamera(多目立体渲染,如 VR 双眼一次性绘制)时,矩阵是以"uniform 数组"形式按视角索引的,而索引本身必须使用 OVR 多视图扩展注入的内建变量:
cameraProjectionMatrix =
_cameraProjectionMatrixArray.element(
camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex
);
同样的模式在 cameraProjectionMatrixInverse、cameraViewMatrix、cameraWorldMatrix、cameraNormalMatrix、cameraPosition 六处出现(文件第 86、134、182、230、278、337 行附近)。从源码结构看,非多视图场景下索引是普通的 cameraIndex,而多视图场景下每个 GPU 线程对应不同视角,只能靠 gl_ViewID_OVR 这一内建变量区分——这也是 BuiltinNode "把驱动变量拉进 TSL 表达式"这一能力不可替代的地方。与之配套的声明注入在 src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js 的 enableMultiview() 中:
enableMultiview() {
this.enableExtension( 'GL_OVR_multiview2', 'require', 'fragment' );
this.enableExtension( 'GL_OVR_multiview2', 'require', 'vertex' );
this.builtins[ 'vertex' ].push( 'layout(num_views = 2) in' );
}
5. 与 ComputeBuiltinNode 的区分
仓库中还有一个命名相近的节点 ComputeBuiltinNode(src/nodes/gpgpu/ComputeBuiltinNode.js),用于 WebGPU compute 阶段暴露 dispatch 上下文信息(numWorkgroups、workgroupId、globalId、localId、subgroupSize 等)。两者的关系与差异:
| 对比项 | BuiltinNode | ComputeBuiltinNode |
|---|---|---|
| 定位 | 通用内建变量引用(渲染管线内建:gl_ClipDistance、gl_ViewID_OVR 等) |
compute 作用域的 WGSL 内建(@builtin( work_group_id ) 等) |
| nodeType | 固定为 'float'(构造时 super( 'float' )) |
构造时可传入(如 'uvec3'、'uint') |
| 作用域限制 | 无显式阶段检查 | generate() 中检查 builder.shaderStage === 'compute',非 compute 阶段会 warn 并退化为常量生成 |
| 缓存键 | 覆写 name,同名节点共享哈希 |
覆写 getHash(),哈希直接取内建名 |
| TSL 入口 | builtin( name ) |
numWorkgroups、workgroupId 等预置常量 |
因此,BuiltinNode 应理解为"渲染管线侧的内建变量桥",而 ComputeBuiltinNode 是"compute 侧的内建变量桥",两者互补而非替代关系。
6. 使用要点与适用前提
name必须是目标后端真实存在的着色器内建标识符:BuiltinNode.generate()不做任何校验,名字错误会在着色器编译期暴露而不是节点构建期。- 变量声明由后端注入而非节点负责:
out float gl_ClipDistance[N](WebGL,见GLSLNodeBuilder.enableHardwareClipping)与@builtin( clip_distances )(WebGPU,见WGSLNodeBuilder.enableHardwareClipping)分别由NodeBuilder在对应顶点阶段声明,BuiltinNode只负责"引用"。 - 硬件裁剪在 WebGL 下依赖
GL_ANGLE_clip_cull_distance扩展(以'require'级别启用),在不支持该扩展的驱动上应回退到clipping()(软件 discard)或clippingAlpha()(alpha-to-coverage)。 builtin()返回的节点类型为'float':若内建变量本身是数组(如gl_ClipDistance[N]),需结合.element( i )等访问节点使用,正如ClippingNode的写法。- 类型判别可用只读标志
node.isBuiltinNode === true。
7. 延伸阅读
- 节点基类 API:Node 文档(
.name、.generate的基类定义) - 裁剪节点:ClippingNode 文档、src/nodes/accessors/ClippingNode.js
- compute 内建节点:ComputeBuiltinNode 文档、src/nodes/gpgpu/ComputeBuiltinNode.js
- 手动裁剪距离示例:examples/webgl_clipculldistance.html
- TSL 总览:docs/TSL.md
- 源码入口:src/nodes/accessors/BuiltinNode.js
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 StartedRust0624
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