首页
/ three.js TSL 中 BuiltinNode 详解:直接引用内建着色器变量,支撑硬件裁剪与多视图渲染

three.js TSL 中 BuiltinNode 详解:直接引用内建着色器变量,支撑硬件裁剪与多视图渲染

2026-09-06 12:21:07作者:范垣楠Rhoda

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 内部的调用方都是把它当作"变量引用"而非完整类型声明来用(见下文 ClippingNodehw_clip_distances.element( i ) 的用法),数组访问等语义由外层表达式节点承担。

1.2 属性

.isBuiltinNode : boolean(readonly) —— 类型测试标志,用于 instanceof 之外的快速类型判别,默认值为 true

.name : string —— 内建着色器变量的名称。该属性覆写了 Node#nameNode 基类文档)。由于 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.jsexport * from './accessors/BuiltinNode.js')与 src/nodes/Nodes.jsexport { 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() 返回什么字符串,取决于当前渲染后端:

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 下的适用前提。

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> 类型的顶点输出。

也就是说,BuiltinNodename 是"后端无关的抽象":同一个 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 生成 cameraProjectionMatrixcameraViewMatrixcameraPosition 等相机矩阵访问节点;当相机是 isMultiViewCamera(多目立体渲染,如 VR 双眼一次性绘制)时,矩阵是以"uniform 数组"形式按视角索引的,而索引本身必须使用 OVR 多视图扩展注入的内建变量:

cameraProjectionMatrix =
	_cameraProjectionMatrixArray.element(
		camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex
	);

同样的模式在 cameraProjectionMatrixInversecameraViewMatrixcameraWorldMatrixcameraNormalMatrixcameraPosition 六处出现(文件第 86、134、182、230、278、337 行附近)。从源码结构看,非多视图场景下索引是普通的 cameraIndex,而多视图场景下每个 GPU 线程对应不同视角,只能靠 gl_ViewID_OVR 这一内建变量区分——这也是 BuiltinNode "把驱动变量拉进 TSL 表达式"这一能力不可替代的地方。与之配套的声明注入在 src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.jsenableMultiview() 中:

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 的区分

仓库中还有一个命名相近的节点 ComputeBuiltinNodesrc/nodes/gpgpu/ComputeBuiltinNode.js),用于 WebGPU compute 阶段暴露 dispatch 上下文信息(numWorkgroupsworkgroupIdglobalIdlocalIdsubgroupSize 等)。两者的关系与差异:

对比项 BuiltinNode ComputeBuiltinNode
定位 通用内建变量引用(渲染管线内建:gl_ClipDistancegl_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 ) numWorkgroupsworkgroupId 等预置常量

因此,BuiltinNode 应理解为"渲染管线侧的内建变量桥",而 ComputeBuiltinNode 是"compute 侧的内建变量桥",两者互补而非替代关系。

6. 使用要点与适用前提

  1. name 必须是目标后端真实存在的着色器内建标识符BuiltinNode.generate() 不做任何校验,名字错误会在着色器编译期暴露而不是节点构建期。
  2. 变量声明由后端注入而非节点负责out float gl_ClipDistance[N](WebGL,见 GLSLNodeBuilder.enableHardwareClipping)与 @builtin( clip_distances )(WebGPU,见 WGSLNodeBuilder.enableHardwareClipping)分别由 NodeBuilder 在对应顶点阶段声明,BuiltinNode 只负责"引用"。
  3. 硬件裁剪在 WebGL 下依赖 GL_ANGLE_clip_cull_distance 扩展(以 'require' 级别启用),在不支持该扩展的驱动上应回退到 clipping()(软件 discard)或 clippingAlpha()(alpha-to-coverage)。
  4. builtin() 返回的节点类型为 'float':若内建变量本身是数组(如 gl_ClipDistance[N]),需结合 .element( i ) 等访问节点使用,正如 ClippingNode 的写法。
  5. 类型判别可用只读标志 node.isBuiltinNode === true

7. 延伸阅读

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