首页
/ GLSLNodeBuilder:three.js WebGL2 后端中面向 GLSL 的节点着色器生成器

GLSLNodeBuilder:three.js WebGL2 后端中面向 GLSL 的节点着色器生成器

2026-09-06 19:22:21作者:宣聪麟

GLSLNodeBuilder 是 three.js 节点着色器体系(Node / TSL)中面向 GLSL 的代码生成器(NodeBuilder)。它的职责是把 NodeMaterial、计算节点等抽象出来的“节点数据流”翻译成可直接交给 WebGL2 编译执行的 GLSL ES 3.0 着色器源码,并同时产出纹理采样器、Uniform Buffer、Vertex Buffer 等绑定元数据,供渲染器创建渲染/计算管线使用。读完本文,你将完整掌握它的继承关系、四类核心数据结构、buildCode() 主流程、全部纹理采样代码生成方法、uniform 与内置变量的 GLSL 映射规则,以及它在 WebGL 回退后端中如何用 PBO/Transform Feedback 补齐 WebGL2 的能力缺口。

1. 它在 three.js 渲染架构中的位置

GLSLNodeBuilder 的类注释清晰地定义了它的定位:

A node builder targeting GLSL. This module generates GLSL shader code from node materials and also generates the respective bindings and vertex buffer definitions. These data are later used by the renderer to create render and compute pipelines for render objects.

从源码结构可以确认它属于「webgl-fallback」渲染后端。在 WebGLBackend.js 中,createNodeBuilder() 直接返回 new GLSLNodeBuilder( object, renderer ),也就是说:当渲染器需要回退到 WebGL2 环境执行节点着色器时,最终都会由 GLSLNodeBuilder 来产出 #version 300 es 的 GLSL 代码。

其继承关系与协作对象如下:

  • 继承自 NodeBuilder(所有后端代码生成器的抽象基类),并通过 super( object, renderer, new GLSLNodeParser() ) 注入 GLSL 专用的解析器。
  • GLSLNodeParser 负责把一段 GLSL 函数源码解析成 GLSLNodeFunction,从而支持“用 GLSL 书写并复用的函数节点”。
  • 生成的 uniform/绑定对象(NodeSampledTextureNodeUniformsGroupNodeUniformBuffer 等)位于 src/renderers/common/nodes/,被后端渲染器用来装配绑定组与布局。

同一套节点材质在 WebGPU 后端对应的是 TSLNodeBuilder 相关实现 一类的生成器;GLSLNodeBuilder 则是把抽象节点系统落地到 GLSL 方言的关键一环。如果想先了解节点系统的上层概念,可参考 docs/TSL.md

2. 构造函数与参数

new GLSLNodeBuilder( object : Object3D, renderer : Renderer )
参数 类型 说明
object Object3D 要生成着色器的三维对象(渲染对象或计算对象)
renderer Renderer 当前渲染器实例,用于访问后端、扩展等上下文

构造函数(对应源码 GLSLNodeBuilder.js 第 178-220 行)除了调用父类构造函数外,还会初始化四份关键数据。注意类注释中把本类标注为 @augments NodeBuilder,即它属于 NodeBuilder 子类,父类中已维护 uniforms、varyings、attributes、flowNodes 等跨后端公共状态。

3. 四个核心属性(生成器的“工作台账”)

3.1 .builtins : Object.<string, Array.<string>>

每个 shader stage('vertex''fragment''compute')使用过的 builtin 声明的数组。

this.builtins = { vertex: [], fragment: [], compute: [] };

例如 enableHardwareClipping() 会向 builtins['vertex'] 中推入 out float gl_ClipDistance[ N ]enableMultiview() 会推入 layout(num_views = 2) in

3.2 .extensions : Object.<string, Map.<string, Object>>

每个 shader stage 使用过的扩展集合,key 为扩展名,value 为 { name, behavior } 对象。enableExtension() 负责写入,getExtensions() 负责把它序列化为 #extension name : behavior 指令。

3.3 .transforms : Array.<Object.<string, (AttributeNode|string)>>

与 Transform Feedback(WebGL2 中用于模拟 compute 结果回读的手段)相关的数组,元素形如 { varyingName, attributeNode },由 registerTransform() 追加、getTransforms() 消费。

3.4 .uniformGroups : Object.<string, Object.<string, NodeUniformsGroup>>

API 文档对该属性的描述是:按 shader stage('vertex'/'fragment'/'compute')再按 group('render'/'frame'/'object')两层字典来管理 UBO。从当前实现看,uniformGroups 在代码中实际以 group 名(如 render/frame/object)作为 key 存储共享的 NodeUniformsGroup 实例,各 stage 通过 getBindGroupArray() 分别引用这些组(参见 getUniformFromNode 的实现),并在生成 uniform 文本时以“std140 布局的 uniform block”统一输出。理解其意图即可:它把散落的 uniform 按生命周期归组,使渲染器可以跨 stage 共享同一份 UBO

4. 主流程 buildCode():组装完整 GLSL 着色器

4.1 阶段数据收集

buildCode()(源码 第 1659 行)是控制各 shader stage 代码构建的入口。它覆盖 NodeBuilder#buildCode

const shadersData = this.material !== null ? { fragment: {}, vertex: {} } : { compute: {} };
  • 材质非空(NodeMaterial):产出 vertex + fragment 两段;
  • 材质为空:视为纯计算对象,只产出 compute 一段。

随后每个 stage 依次收集:

stageData.extensions = this.getExtensions( shaderStage );
stageData.uniforms   = this.getUniforms( shaderStage );
stageData.attributes = this.getAttributes( shaderStage );
stageData.varyings   = this.getVaryings( shaderStage );
stageData.vars       = this.getVars( shaderStage, true );
stageData.structs    = this.getStructs( shaderStage );
stageData.codes      = this.getCodes( shaderStage );
stageData.transforms = this.getTransforms( shaderStage );
stageData.flow       = flow;

flow 是由 flowNodes(节点按执行顺序形成的流图)拼装出来的主逻辑:每个节点一段缩进代码,遇到主节点时——vertex stage 会写 gl_Position = ...,fragment stage 会写 fragColor = ...(除非使用显式的输出 struct)。此外 buildCode() 还会在 BatchedMesh 但设备不支持 WEBGL_multi_draw 时自动补充 uniform uint nodeUniformDrawId; 作为回退(与 getDrawIndex() 返回的 nodeUniformDrawId 对应)。

4.2 GLSL ES 3.0 输出模板

拼装完成后的 stageData 被交给两个私有模板方法:

  • _getGLSLVertexCode()源码第 1567 行):顶点着色器模板,顺序为 #version 300 es → 签名注释 → extensions → 默认精度 → structs → uniforms → varyings → attributes → vars → codes → main(),main 中先执行 transforms 再执行 flow,最后固定 gl_Position = ...gl_PointSize = 1.0
  • _getGLSLFragmentCode()源码第 1619 行):片元着色器模板,不包含 attributes,以 flow 收尾。

关键说明:

  • 所有 shader 都强制声明默认精度 precision highp ...(float/int 及各类 sampler 全集,见源码 defaultPrecisions,第 121-142 行)。
  • compute 阶段复用顶点模板(computeShader = this._getGLSLVertexCode(...)),因为 WebGL2 后端用顶点着色器 + Transform Feedback 来模拟计算管线,gl_Position 在 compute 分支不会被写入。
  • 每段代码段上方都保留 // extensions// structs 等注释,方便开发者直接查看最终着色器排查问题。

4.3 buildFunctionCode( shaderNode ) : string

覆盖 NodeBuilder#buildFunctionCode。它把某个 ShaderNodeInternal(例如由 GLSLNodeParser 解析出来的 GLSL 函数节点)编译成一段独立的 GLSL 函数:

返回类型 函数名( 参数类型 参数名, ... ) {

	局部变量声明...

函数体流程
	return 结果;

}

函数签名来自节点 layout 的输入输出类型映射,参数类型通过 this.getType() 转换成 GLSL 类型名。该机制让“JS 里用字符串写 GLSL 函数”与节点系统无缝衔接。

5. 纹理采样代码生成方法族(核心输出)

这是 GLSLNodeBuilder 最密集的功能区:把抽象纹理节点翻译成具体的 GLSL 采样/读取内建函数。以下方法的完整实现在 GLSLNodeBuilder.js 第 576-851 行,公共参数含义如下:

  • texture:纹理对象,用于判断深度纹理、立方体、数组纹理等形态;
  • textureProperty:shader 中纹理 uniform 的名称;
  • uvSnippet / depthSnippet / offsetSnippet:纹理坐标、0 起始的数组层索引、纹素偏移的 GLSL 片段;
  • 各方法专属的 bias/level/grad/compare/gather/flipY 片段:mip 偏置、显式 mip 层、显式梯度、深度比较参考值、采集通道、是否翻转 Y。

5.1 各方法 → GLSL 内建函数对照

方法 用途 生成的 GLSL(示意)
.generateTexture() 常规采样/读取 texture(map, uv) / textureOffset(map, uv, off);深度纹理取 .x
.generateTextureBias() 带 mip 偏置采样 texture(map, uv, bias) / textureOffset(map, uv, off, bias)
.generateTextureLevel() 显式 mip 层采样 textureLod(map, uv, level) / textureLodOffset(...)
.generateTextureGrad() 显式梯度采样 textureGrad(map, uv, dPdx, dPdy) / textureGradOffset(...)
.generateTextureLoad() 不滤波的单纹素读取 texelFetch(map, ivec, level) / texelFetchOffset(...)
.generateTextureCompare() 阴影深度纹理比较采样 fragment 阶段:texture(sampler2DShadow, vec3(uv, ref)) 等;其他阶段直接报错
.generateTextureGather() 一次取 4 个相邻纹素 内部 polyfill:tsl_textureGather(...)
.generateTextureGatherCompare() 4 纹素上的深度比较 内部 polyfill:tsl_textureGatherCompare(...)

关键实现细节:

  • 深度纹理约定:凡 texture.isDepthTexture === true,采样结果都要追加 .x;比较采样只在 'fragment' stage 允许,否则抛出 WebGPURenderer: THREE.DepthTexture.compareFunction() does not support ${stage} shader.(源码 第 781 行)。
  • 立方体贴图阴影:比较采样对 CubeTexture 使用 texture(map, vec4(方向, 参考值));数组纹理则拼成 vec4(uv, depth, ref)
  • WebGL2 没有原生 textureGather:因此该族方法通过 _include() 注入 GLSL polyfill 函数,如 tsl_textureGather(见源码第 15-29 行的 glslPolyfills),它用四次 textureLod 手动重建 2×2 纹素结果,并依据 flipY 处理 Y 轴翻转(WebGL 纹理方向约定,isFlipY() 恒为 true)。
  • 显式 mip 层采样时若传入 depthSnippet,UV 会被包装成 vec3(uv, depth),从而把 2D 数组纹理映射到正确语法。
  • .generateTextureLoad() 对 storage buffer 的 PBO 读取也复用:见下文第 8 节。

5.2 采样器类型的自动推导(getUniforms)

getUniforms()第 859 行)根据 uniform 的类型与纹理形态生成对应 sampler 声明:

场景 生成的 GLSL 声明
普通纹理 uniform sampler2D name;
3D 纹理(非数组) uniform sampler3D name;
数组纹理(含 DataArray/压缩数组) uniform sampler2DArray name;
compareFunction 且按比较采样 uniform sampler2DShadow / sampler2DArrayShadow;
立方体贴图 / 深度立方体贴图 uniform samplerCube / samplerCubeShadow;
整数数据纹理(IntType/UnsignedIntType 前缀 i / u(如 usampler2D
Buffer uniform 具名块:bufferName { type name[count]; };
归组 uniform(render/frame/object 组) layout( std140 ) uniform 组名 { ... };

普通 uniform 会补上 uniform 关键字;节点上设置了 precisionlow/medium/high)时,会用 precisionLib{ low:'lowp', medium:'mediump', high:'highp' })映射为 GLSL 精度限定符拼在类型前。归组 uniform 通过 _getGLSLUniformStruct() 统一输出为 std140 布局的 uniform block,保证跨 stage 的字节布局一致。

6. 顶点输入、varying、struct 的输出

6.1 .getAttributes( shaderStage ) : string

vertex/compute 阶段把属性数组逐条序列化为带绑定位置的顶点输入:

layout( location = 0 ) in vec3 position;
layout( location = 1 ) in vec2 uv;

compute 阶段同样会写 in 属性(因为 WebGL 回退把计算放到顶点阶段执行,几何数据来自绑定的顶点缓冲)。

6.2 .getVaryings( shaderStage ) : string

  • vertex/compute 写 out,fragment 写 in
  • 需要插值的 varying 依据 interpolationTypeMap 映射(perspective → smoothlinear → noperspective),必要时附加 centroid
  • 整型类 varying(类型含 int/uv/iv)自动加 flat 限定符,避免逐像素插值;
  • 不需要插值的 varying 在 vertex 阶段生成同名局部变量;
  • 最后把 this.builtins[stage] 中的内置声明原样拼接(第 1220-1224 行)。

6.3 .getStructs( shaderStage ).getStructMembers( struct )

非输出 struct 被序列化为标准 struct 名 { 成员类型 成员名; ... };;带 output 标记的 struct 成员则转成 layout( location = N ) out ... 的多渲染目标输出。fragment 阶段若最终没有显式输出 struct,会自动补一句:

layout( location = 0 ) out <输出类型> fragColor;

.getOutputStructName() 对本类返回空字符串(注释明确写着 “Not relevant for GLSL”,即 GLSL 不需要 struct 命名的输出封装——而 WebGPU 端需要)。

6.4 .getTypeFromAttribute( attribute ) : string

把 BufferAttribute 映射为 GLSL 类型。它对整型属性做了 WebGL2 特有的纠正:GLSL ES 3.0 中非高精度整型顶点属性默认是浮点解释,因此只有当底层数组确实是 Uint32Array/Int32Array才保留 i/u 前缀,否则去掉前缀以匹配实际数据(源码 第 1028-1049 行)。

7. uniform 与 binding 的生成

7.1 .getUniformFromNode( node, type, shaderStage, name = null ) : NodeUniform

文档称其为“最重要的方法之一”:为给定 uniform 节点生成匹配的绑定实例。源码实现(第 1766 行起)根据 type 分发创建不同绑定对象并压入对应绑定组:

type 创建/获取的绑定实例 说明
texture NodeSampledTexture 2D 采样纹理绑定
cubeTexture / cubeDepthTexture NodeSampledCubeTexture 立方体纹理绑定
texture3D NodeSampledTexture3D 3D 纹理绑定
buffer NodeUniformBuffer 具名 buffer 绑定,名称自动改写为 buffer${ node.id }
其余标量/向量 NodeUniformsGroup + getNodeUniform() 归入组,按名称去重,避免跨 stage 重复添加

创建结果缓存在 nodeData 的 uniformGPU 上,同一节点不会被重复生成绑定。这些绑定对象随后被 renderer 用于创建绑定组(bind groups)与布局,因此 GLSLNodeBuilder 输出的不只是字符串,还有运行期真正需要的资源元数据。

7.2 .getPropertyName( node, shaderStage ) : string

非纹理/非 buffer 的 NodeUniform 直接返回其名称;其余走父类 super.getPropertyName()(保持与既有 NodeBuilder 一致的命名缓存逻辑)。

8. storage buffer 的 PBO 回退方案

WebGL2 无法像 WebGPU 那样把 storage buffer 直接暴露给片元/顶点着色器(supports.storageBuffer 在本类中为 false,见源码第 107-110 行)。GLSLNodeBuilder 采用的替代方案是 Pixel Buffer Object(PBO),把 storage buffer 的数据重新包装成一张纹理,用“采纹素”代替“读 buffer”:

  • .setupPBO( storageBufferNode )第 377 行):把属性数组按 itemSize 与数据类型映射成合适像素格式(如 RGBAIntegerFormat + UnsignedIntType),并扩充到近似正方形的宽高(width 取 2 的幂),构造一张 DataTexture(标记 isPBOTexture = true),最终通过 getUniformFromNode() 注册为纹理 uniform。
  • .generatePBO( storageArrayElementNode ) : string第 483 行):把“storage 数组的某个元素”的读写翻译成 PBO 纹理的 texelFetch 访问:用 textureSize(...) 取宽度,把线性索引拆成 ivec2( index % width, index / width ),按 itemSize 截取对应通道,返回可作为变量使用的属性名。

像素格式的推断来自常量表(src/constants.js 中的 RedFormat/RGFormat/RGBFormat/RGBAFormat 及其整型变体、FloatType 等)。若你在代码里对 storage buffer 做 attribute.array 之类的假设,注意此路径会替换为扩容后的数组并附加 pbo/pboNode 元数据。

9. 内置变量(builtins)的 GLSL 映射

这组 getter 把节点系统里抽象的内置量映射为 GLSL 内建变量:

方法 返回的 GLSL 备注
.getVertexIndex() uint( gl_VertexID ) 覆盖父类
.getInstanceIndex() uint( gl_InstanceID ) 覆盖父类;compute 语境下代表“在工作组网格内线性化的调用索引”
.getInvocationLocalIndex() uint( gl_InstanceID ) % workgroupSize 由对象的 workgroupSize 计算
.getDrawIndex() uint( gl_DrawID )nodeUniformDrawId 设备支持 WEBGL_multi_draw 时用前者,否则回退为 uniform(配合 buildCode() 注入的兜底声明)
.getFrontFacing() gl_FrontFacing 覆盖父类
.getFragCoord() gl_FragCoord.xy 覆盖父类
.getFragDepth() gl_FragDepth
.getClipDistance() gl_ClipDistance 需要 WEBGL_clip_cull_distance 支持
.getSubgroupSize() / .getInvocationSubgroupIndex() / .getSubgroupIndex() 抛错 WebGL 后端不支持 subgroup,报错信息明确提示
.getOutputStructName() '' GLSL 无输出 struct 概念

注意 .getInvocationLocalIndex() 依赖 this.object.workgroupSize,只有计算节点配置了工作组尺寸才有意义。

10. 方法名解析、类型转换与能力探测

10.1 .getMethod( method ) 与 GLSL 方法映射表

节点系统里很多操作以通用名字出现,GLSLNodeBuilder 通过 glslMethods 表(源码第 81-99 行)解析为 GLSL 内建函数名:

  • equals → equal
  • 位转换:bitcast_float_int → floatBitsToIntbitcast_int_float → intBitsToFloatbitcast_float_uint → floatBitsToUintbitcast_uint_float → uintBitsToFloat,而 uint↔int 两种方向因 GLSL 无直接内建,映射到自定义 polyfill tsl_bitcast_uint_to_int / tsl_bitcast_int_to_uint
  • 打包:floatpack_snorm_2x16 → packSnorm2x16floatpack_unorm_2x16 → packUnorm2x16floatpack_float16_2x16 → packHalf2x16floatpack_snorm_4x8 → tsl_packSnorm4x8(4×8 无原生函数,走 polyfill);
  • 解包:unpackSnorm2x16 / unpackUnorm2x16 / unpackHalf2x16 以及 4×8 的 polyfill。

.getBitcastMethod( type, inputType )bitcast_${inputType}_${type} 交给 getMethod() 解析;.getFloatPackingMethod( encoding, layout = '2x16' ).getFloatUnpackingMethod( ... ) 同理,生成 floatpack_${encoding}_${layout} / floatunpack_... 的方法名。需要 polyfill 时 _include() 会先把对应的 CodeNode 构建并注入代码段头部。

10.2 .getTernary( condSnippet, ifSnippet, elseSnippet )

把通用三元操作直接生成为 GLSL 原生写法:条件 ? 分支A : 分支B

10.3 能力探测 .isAvailable( name ) : boolean

先查静态表 supports,默认只有两项:swizzleAssign: true(GLSL 支持向量分量写回)、storageBuffer: false。未知能力则通过 WebGL 扩展探测,结果带缓存:

  • float32FilterableOES_texture_float_linear
  • clipDistanceWEBGL_clip_cull_distance

10.4 .isFlipY().needsToWorkingColorSpace( texture )

  • .isFlipY() 恒返回 true:GLSL/WebGL 语境下纹理数据统一按 Y 轴翻转约定处理(覆盖父类)。
  • .needsToWorkingColorSpace() 只在视频纹理且 colorSpace 非 NoColorSpace 时返回 true,表示需要在着色器端手动做工作色彩空间转换。

10.5 保留字保护

类内还维护了一份庞大的 glslReservedKeywords(关键字、未来保留字、类型名,甚至 main),用于在生成变量名时避开 GLSL 保留字,保证输出代码可编译。

11. 扩展开关:硬裁剪、多视图与 Transform Feedback

11.1 .enableExtension( name, behavior, shaderStage = this.shaderStage )

按 stage 写入扩展表;.getExtensions( shaderStage ) 负责输出形如 #extension GL_ANGLE_multi_draw : require 的指令(BatchedMesh 且支持 WEBGL_multi_draw 时 vertex 阶段会被自动 enable)。

11.2 .enableHardwareClipping( planeCount )

启用 GL_ANGLE_clip_cull_distance 并在 vertex 内建中加入 out float gl_ClipDistance[ planeCount ],用 GPU 硬件做剪裁平面剔除(相比软件裁剪更高效)。

11.3 .enableMultiview()

面向 XR/多视图渲染:在 fragment 与 vertex 两阶段同时启用 GL_OVR_multiview2,并在 vertex 内建中加入 layout(num_views = 2) in

11.4 Transform Feedback:.registerTransform() / .getTransforms( shaderStage )

  • .registerTransform( varyingName, attributeNode ){ varyingName, attributeNode } 追加到 this.transforms
  • .getTransforms() 遍历该数组,为每个有属性名的注册项生成赋值语句 varyingName = attributeName; 插到 main() 的开头。

这正对应 compute 回退链路:WebGL2 后端把计算当作一次“顶点处理 + 结果捕获”的过程,捕获哪些数据由注册的 varying 决定,捕获结果随后作为 attributes/vertex buffer 定义回传给渲染器——呼应了类的总述中“generates the respective bindings and vertex buffer definitions”一句。

12. 总结:如何阅读与调试

GLSLNodeBuilder 位于 src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js,共约 1860 行,是 three.js 节点体系在 WebGL2 上的“最后一公里”编译器。通读时建议按四层理解:

  1. 数据层builtinsextensionstransformsuniformGroups 四张表记录“用过哪些东西”;
  2. 装配层buildCode() 调用 getAttributes/getUniforms/getVaryings/getStructs/getExtensions/getTransforms 等 getter,把各 stage 的文本片段填入 GLSL ES 3.0 模板;
  3. 语义层generateTexture* 一族负责纹理采样语法,getUniformFromNode 一族负责把 uniform 节点变成渲染器可用的绑定实例;
  4. 回退层:PBO 纹理模拟 storage buffer、Transform Feedback 模拟 compute、polyfill 补全 textureGather 与 4×8 打包/位转换等 WebGL2 缺失能力。

若你需要在 WebGL2 下排查节点材质的着色器问题,可以直接把 WebGLBackend 生成的 vertexShader/fragmentShader 文本打印出来对照上面的模板注释段(// extensions// structs// flow 等)逐段定位。相关后续阅读:NodeBuilder 基类文档docs/TSL.md

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