Three.js 渲染架构深度解析:从 WebGL/GLSL 到 WebGPU/TSL 的演进之路
Three.js 渲染架构的代际演进------从 WebGLRenderer 的 GLSL 静态拼接体系,到 WebGPURenderer 的节点图 + TSL 动态编译体系。要求:
- 逐层拆解两套架构的内部实现(子系统职责、Shader 编译流程、缓存策略)
- 用对比代码示例说明
onBeforeCompile与 TSL 的范式差异- 详细讲解 TSL 的三阶段构建(setup/analyze/generate)及 NodeBuilder 的代码生成机制
- 专章介绍 Compute Shader 的工作原理与使用方式
- 配合架构图(Mermaid)辅助理解,代码示例均标注来源文件
二、正文
第一章:两条路的起点------为什么需要两套渲染器?
Three.js 目前同时维护两套完整的渲染路径:
| 维度 | WebGLRenderer(传统路径) | WebGPURenderer(新路径) |
|---|---|---|
| 入口文件 | three |
three/webgpu |
| Shader 语言 | GLSL(字符串拼接) | TSL → WGSL / GLSL |
| 自定义材质 | onBeforeCompile 钩子 |
NodeMaterial + TSL 节点图 |
| Compute Shader | 不支持 | 原生支持 |
| 后端 | 仅 WebGL | WebGPU(自动 fallback WebGL 2) |
| 初始化 | 同步 | 异步 await renderer.init() |
两者并非替代关系,而是共存 :WebGLRenderer 提供最广泛的浏览器兼容性,WebGPURenderer 则面向现代 GPU 能力(Compute、存储缓冲区、MRT 等)。
第二章:传统路径------WebGLRenderer 的 20+ 子系统协作
2.1 子系统全景
WebGLRenderer 在 initGLContext() 中一次性初始化所有子系统:
js
// src/renderers/WebGLRenderer.js
extensions = new WebGLExtensions( _gl );
capabilities = new WebGLCapabilities( _gl, extensions, parameters, utils );
state = new WebGLState( _gl, extensions );
info = new WebGLInfo( _gl );
properties = new WebGLProperties();
textures = new WebGLTextures( ... );
environments = new WebGLEnvironments( _this );
attributes = new WebGLAttributes( _gl );
bindingStates = new WebGLBindingStates( _gl, attributes );
geometries = new WebGLGeometries( ... );
objects = new WebGLObjects( ... );
morphtargets = new WebGLMorphtargets( ... );
clipping = new WebGLClipping( properties );
programCache = new WebGLPrograms( _this, environments, extensions, capabilities, bindingStates, clipping );
materials = new WebGLMaterials( _this, properties );
renderLists = new WebGLRenderLists();
renderStates = new WebGLRenderStates( extensions );
background = new WebGLBackground( ... );
shadowMap = new WebGLShadowMap( ... );
uniformsGroups = new WebGLUniformsGroups( ... );
```[2](#0-1)
其中最核心的是 `programCache`(`WebGLPrograms` 实例),它负责整个 Shader 程序的生命周期管理。
#### 2.2 ShaderChunk:GLSL 的"乐高积木"
`ShaderChunk` 是一个巨大的 GLSL 片段字典,包含 100+ 个功能模块:
alphahash_fragment → Alpha 哈希透明
lights_physical_fragment → PBR 物理光照
shadowmap_pars_fragment → 阴影贴图参数
skinning_vertex → 骨骼蒙皮
morphtarget_vertex → 变形目标
fog_fragment → 雾效
...
-
(#0-2)
每个 Chunk 都是一段独立的 GLSL 字符串,通过 `#include <chunk_name>` 语法在主 Shader 中引用。
#### 2.3 ShaderLib:预定义的完整 Shader 库
`ShaderLib` 将 Chunk 组合成完整的材质 Shader。以 Lambert 材质为例:
```js
// src/renderers/shaders/ShaderLib.js
lambert: {
uniforms: mergeUniforms([
UniformsLib.common,
UniformsLib.envmap,
UniformsLib.lights,
{ emissive: { value: new Color(0x000000) } }
]),
vertexShader: ShaderChunk.meshlambert_vert,
fragmentShader: ShaderChunk.meshlambert_frag
}
```[4](#0-3)
而 `meshlambert_vert` 本身就是一段通过 `#include` 组合的 GLSL 模板:
```glsl
// src/renderers/shaders/ShaderLib/meshlambert.glsl.js
#define LAMBERT
varying vec3 vViewPosition;
#include <common>
#include <batching_pars_vertex>
#include <uv_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
// ...
void main() {
#include <uv_vertex>
#include <morphinstance_vertex>
#include <begin_vertex>
#include <project_vertex>
// ...
}
```[5](#0-4)
#### 2.4 WebGLPrograms:程序缓存的核心逻辑
每次渲染一个对象时,`getProgram()` 会被调用。它的核心逻辑是:
材质参数 → getParameters() → getProgramCacheKey() → 查缓存 → 命中则复用 / 未命中则编译新程序
-
(#0-5)
缓存键由 **50+ 个参数**拼接而成,涵盖精度、贴图类型、光源数量、阴影类型、色调映射等所有影响 Shader 变体的因素:
```js
// src/renderers/webgl/WebGLPrograms.js
function getProgramCacheKey( parameters ) {
const array = [];
array.push( parameters.shaderID );
// 数值参数:精度、光源数、阴影类型...
getProgramCacheKeyParameters( array, parameters );
// 布尔参数:instancing、envMap、skinning...
getProgramCacheKeyBooleans( array, parameters );
array.push( renderer.outputColorSpace );
array.push( parameters.customProgramCacheKey );
return array.join();
}
```[7](#0-6)
#### 2.5 WebGLProgram:GLSL 的最终编译过程
`WebGLProgram` 是实际执行 GLSL 编译的地方,它做了以下几件事:
**① 生成 `#define` 宏前缀**(根据参数决定开启哪些功能):
```js
prefixVertex = [
generatePrecision( parameters ),
'#define SHADER_TYPE ' + parameters.shaderType,
parameters.instancing ? '#define USE_INSTANCING' : '',
parameters.map ? '#define USE_MAP' : '',
parameters.skinning ? '#define USE_SKINNING' : '',
parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
// ... 50+ 个 define
].filter( filterEmptyLine ).join( '\n' );
```[8](#0-7)
**② 展开 `#include` 指令**(递归替换 ShaderChunk):
```js
// src/renderers/webgl/WebGLProgram.js
const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
function resolveIncludes( string ) {
return string.replace( includePattern, includeReplacer );
}
function includeReplacer( match, include ) {
let string = ShaderChunk[ include ]; // 从字典中取出片段
return resolveIncludes( string ); // 递归展开嵌套 include
}
```[9](#0-8)
**③ GLSL 1.0 → 3.0 兼容性转换**:
```js
prefixFragment = [
'#define varying in',
'layout(location = 0) out highp vec4 pc_fragColor;',
'#define gl_FragColor pc_fragColor',
'#define texture2D texture',
// ...
].join('\n') + '\n' + prefixFragment;
```[10](#0-9)
**④ 最终编译并链接**:
```js
const vertexGlsl = versionString + prefixVertex + vertexShader;
const fragmentGlsl = versionString + prefixFragment + fragmentShader;
const glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
const glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );
gl.attachShader( program, glVertexShader );
gl.attachShader( program, glFragmentShader );
gl.linkProgram( program );
```[11](#0-10)
#### 2.6 onBeforeCompile:用户自定义 Shader 的"外科手术"
传统路径下,用户修改内置材质 Shader 的唯一方式是 `onBeforeCompile`:
```js
// 旧方式:字符串替换,脆弱且难以维护
const material = new THREE.MeshStandardMaterial();
material.onBeforeCompile = ( shader ) => {
shader.uniforms.detailMap = { value: detailMap };
// 在特定 token 后插入代码
shader.fragmentShader = shader.fragmentShader.replace(
'#define STANDARD',
'#define STANDARD\nuniform sampler2D detailMap;'
);
shader.fragmentShader = shader.fragmentShader.replace(
'#include <map_fragment>',
'#include <map_fragment>\ndiffuseColor *= texture2D(detailMap, vMapUv * 10.0);'
);
};
```[12](#0-11)
这种方式的问题:
- 依赖字符串匹配,Three.js 内部重构后极易失效
- 多个材质模块之间无法互相通信
- 无法跨 WebGL/WebGPU 后端复用
---
### 第三章:新路径------Renderer + Backend 的统一抽象层
#### 3.1 架构总览
```mermaid
graph TD
"WebGPURenderer" --> "Renderer (common)"
"Renderer (common)" --> "Backend (abstract)"
"Backend (abstract)" --> "WebGPUBackend"
"Backend (abstract)" --> "WebGLBackend (fallback)"
"Renderer (common)" --> "NodeManager"
"Renderer (common)" --> "Pipelines"
"Renderer (common)" --> "Bindings"
"Renderer (common)" --> "RenderObjects"
"Renderer (common)" --> "Textures"
"Renderer (common)" --> "Geometries"
"NodeManager" --> "WGSLNodeBuilder (WebGPU)"
"NodeManager" --> "GLSLNodeBuilder (WebGL)"
```[13](#0-12)
#### 3.2 自动 Fallback 机制
`WebGPURenderer` 构造时会尝试创建 `WebGPUBackend`,若浏览器不支持 WebGPU,则自动降级到 `WebGLBackend`:
```js
// src/renderers/webgpu/WebGPURenderer.js
constructor( parameters = {} ) {
let BackendClass;
if ( parameters.forceWebGL ) {
BackendClass = WebGLBackend;
} else {
BackendClass = WebGPUBackend;
parameters.getFallback = () => {
warn( 'WebGPURenderer: WebGPU is not available, running under WebGL2 backend.' );
return new WebGLBackend( parameters );
};
}
const backend = new BackendClass( parameters );
super( backend, parameters ); // 传入 Renderer 基类
}
```[14](#0-13)
#### 3.3 Renderer.init():异步初始化子系统
新路径的初始化是**异步**的,因为 WebGPU 设备请求本身是异步操作:
```js
// src/renderers/common/Renderer.js
async init() {
await backend.init( this ); // 等待 GPU 设备就绪
this._nodes = new NodeManager( this, backend );
this._pipelines = new Pipelines( backend, this._nodes, this.info );
this._bindings = new Bindings( backend, this._nodes, ... );
this._objects = new RenderObjects( this, this._nodes, ... );
this._textures = new Textures( this, backend, this.info );
this._geometries = new Geometries( this._attributes, this.info );
this._renderLists = new RenderLists( this.lighting );
// ...
}
```[15](#0-14)
#### 3.4 Backend 抽象接口
`Backend` 基类定义了所有后端必须实现的抽象方法,实现了真正的后端无关性:
```js
// src/renderers/common/Backend.js
createProgram( program ) { } // 创建 Shader 程序
createRenderPipeline( renderObject, promises ) { } // 创建渲染管线
createComputePipeline( pipeline, bindings ) { } // 创建计算管线
createBindings( bindGroup, bindings, ... ) { } // 创建绑定组
compute( computeGroup, computeNode, ... ) { } // 执行计算
```[16](#0-15)
---
### 第四章:TSL------从"拼接字符串"到"组合函数"
#### 4.1 为什么需要 TSL?
TSL(Three.js Shading Language)的核心动机:
> "Creating shaders has always been an advanced step for most developers... The shader graph solution adopted today by the industry has allowed developers more focused on dynamics to create the necessary graphic effects." [17](#0-16)
**对比示例**------实现一个细节贴图叠加效果:
```js
// ❌ 旧方式(onBeforeCompile):脆弱的字符串手术
material.onBeforeCompile = ( shader ) => {
shader.uniforms.detailMap = { value: detailMap };
shader.fragmentShader = shader.fragmentShader.replace(
'#include <map_fragment>',
`#include <map_fragment>
diffuseColor *= texture2D( detailMap, vMapUv * 10.0 );`
);
};
// ✅ 新方式(TSL):声明式节点组合
import { texture, uv } from 'three/tsl';
const detail = texture( detailMap, uv().mul( 10 ) );
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = texture( colorMap ).mul( detail );
```[18](#0-17)
#### 4.2 TSL 的核心架构
所有 TSL 组件都继承自 `Node` 基类。`NodeBuilder` 是代码生成的核心,目前有两个具体实现:
- `WGSLNodeBuilder`:面向 WebGPU,生成 WGSL 代码
- `GLSLNodeBuilder`:面向 WebGL 2 fallback,生成 GLSL 代码 [19](#0-18)
```mermaid
graph LR
"TSL 节点图" --> "NodeBuilder.build()"
"NodeBuilder.build()" --> "Stage 1: setup"
"NodeBuilder.build()" --> "Stage 2: analyze"
"NodeBuilder.build()" --> "Stage 3: generate"
"Stage 3: generate" --> "buildCode()"
"buildCode()" --> "WGSLNodeBuilder → WGSL 字符串"
"buildCode()" --> "GLSLNodeBuilder → GLSL 字符串"
"WGSL 字符串" --> "WebGPU Pipeline"
"GLSL 字符串" --> "WebGL Program"
4.3 三阶段构建流程详解
NodeBuilder.build() 按顺序执行三个阶段:
js
// src/nodes/core/NodeBuilder.js
build() {
this.prebuild();
// 三个阶段依次执行
for ( const buildStage of ['setup', 'analyze', 'generate'] ) {
this.setBuildStage( buildStage );
for ( const shaderStage of shaderStages ) {
this.setShaderStage( shaderStage );
const flowNodes = this.flowNodes[ shaderStage ];
for ( const node of flowNodes ) {
if ( buildStage === 'generate' ) {
this.flowNode( node );
} else {
node.build( this );
}
}
}
}
this.buildCode(); // 第四步:生成最终 Shader 字符串
this.buildUpdateNodes(); // 注册运行时更新节点
return this;
}
```[20](#0-19)
**三个阶段的职责:**
| 阶段 | 职责 | 返回值 |
|---|---|---|
| `setup` | 构建节点树,创建子节点,返回输出节点引用 | `Node` |
| `analyze` | 分析节点依赖,决定哪些节点需要缓存为临时变量 | `null` |
| `generate` | 遍历节点树,生成 Shader 代码字符串 | `string` | [21](#0-20)
#### 4.4 GLSLNodeBuilder:生成 GLSL 代码
`GLSLNodeBuilder.buildCode()` 将节点图转换为完整的 GLSL 着色器:
```js
// src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js
buildCode() {
const shadersData = { fragment: {}, vertex: {} };
for ( const shaderStage in shadersData ) {
let flow = '// code\n\n';
flow += this.flowCode[ shaderStage ];
// 遍历流节点,拼接代码
for ( const node of flowNodes ) {
flow += `${ flowSlotData.code }\n\t`;
if ( node === mainNode ) {
if ( shaderStage === 'vertex' ) {
flow += `gl_Position = ${ flowSlotData.result };`;
} else if ( shaderStage === 'fragment' ) {
flow += `fragColor = ${ flowSlotData.result };`;
}
}
}
stageData.uniforms = this.getUniforms( shaderStage );
stageData.varyings = this.getVaryings( shaderStage );
stageData.flow = flow;
}
this.vertexShader = this._getGLSLVertexCode( shadersData.vertex );
this.fragmentShader = this._getGLSLFragmentCode( shadersData.fragment );
}
```[22](#0-21)
生成的 GLSL 顶点着色器模板:
```glsl
// 由 _getGLSLVertexCode() 生成
#version 300 es
// extensions
${shaderData.extensions}
// uniforms
${shaderData.uniforms}
// varyings
${shaderData.varyings}
// attributes
${shaderData.attributes}
void main() {
// transforms
${shaderData.transforms}
// flow(节点图生成的代码)
${shaderData.flow}
gl_PointSize = 1.0;
}
```[23](#0-22)
#### 4.5 WGSLNodeBuilder:生成 WGSL 代码
`WGSLNodeBuilder` 生成的顶点着色器模板(WGSL 语法):
```wgsl
// 由 _getWGSLVertexCode() 生成
// directives
${shaderData.directives}
// structs
${shaderData.structs}
// uniforms
${shaderData.uniforms}
// varyings
${shaderData.varyings}
var<private> varyings : VaryingsStruct;
@vertex
fn main( ${shaderData.attributes} ) -> VaryingsStruct {
// flow(节点图生成的代码)
${shaderData.flow}
return varyings;
}
```[24](#0-23)
#### 4.6 TSL 的自动优化能力
TSL 的 `analyze` 阶段会自动识别重复计算并提取为临时变量,避免 GPU 上的冗余运算。例如,如果多个节点都引用了 `positionWorld`,TSL 只会在 Shader 中计算一次,并将结果存入临时变量供后续节点复用。 [25](#0-24)
---
### 第五章:Compute Shader------GPU 通用计算的新能力
#### 5.1 什么是 Compute Shader?
Compute Shader 是一种不绑定到渲染管线的 GPU 程序,可以直接对缓冲区数据进行并行计算。典型用途:粒子物理模拟、GPU 排序、图像处理、神经网络推理等。
在 Three.js 新路径中,Compute Shader 通过 `ComputeNode` 表达:
```js
// src/nodes/gpgpu/ComputeNode.js
class ComputeNode extends Node {
constructor( computeNode, workgroupSize ) {
super( 'void' );
this.computeNode = computeNode; // 计算逻辑节点
this.workgroupSize = workgroupSize; // 工作组大小 [x, y, z]
this.count = null; // 总线程数
this.dispatchSize = null; // 调度大小
}
}
```[26](#0-25)
#### 5.2 创建 Compute Shader
TSL 提供了 `compute()` 和 `computeKernel()` 两个工厂函数:
```js
import { compute, computeKernel, instanceIndex, storage } from 'three/tsl';
import { StorageBufferAttribute } from 'three/webgpu';
// 创建 GPU 存储缓冲区
const positionBuffer = new StorageBufferAttribute( new Float32Array( count * 3 ), 3 );
// 定义计算逻辑(TSL 节点图)
const updatePositions = Fn( () => {
const i = instanceIndex;
const pos = storage( positionBuffer, 'vec3', count ).element( i );
pos.addAssign( vec3( 0, -0.01, 0 ) ); // 每帧向下移动
} );
// 创建 ComputeNode:64 个线程一组
const computeNode = compute( updatePositions(), count, [ 64 ] );
// 每帧执行
renderer.compute( computeNode );
```[27](#0-26)
#### 5.3 Compute Shader 的执行流程
```mermaid
graph TD
"renderer.compute(computeNode)" --> "Renderer._computeAsync()"
"Renderer._computeAsync()" --> "nodes.updateForCompute()"
"nodes.updateForCompute()" --> "NodeBuilder.build() → WGSL compute shader"
"Renderer._computeAsync()" --> "bindings.updateForCompute()"
"Renderer._computeAsync()" --> "pipelines.getForCompute()"
"pipelines.getForCompute()" --> "backend.createComputePipeline()"
"Renderer._computeAsync()" --> "backend.compute()"
"backend.compute()" --> "passEncoder.dispatchWorkgroups(x, y, z)"
```[28](#0-27)
#### 5.4 WebGPU 后端的 Compute 实现
在 `WebGPUBackend` 中,Compute 的执行通过 WebGPU 的 `GPUComputePassEncoder` 完成:
```js
// src/renderers/webgpu/WebGPUBackend.js
beginCompute( computeGroup ) {
// 创建命令编码器和计算通道
groupGPU.cmdEncoderGPU = device.createCommandEncoder();
groupGPU.passEncoderGPU = cmdEncoder.beginComputePass();
}
compute( computeGroup, computeNode, bindings, pipeline, dispatchSize ) {
const pipelineGPU = this.get( pipeline ).pipeline;
passEncoderGPU.setPipeline( pipelineGPU );
// 绑定资源(存储缓冲区、纹理等)
for ( const bindGroup of bindings ) {
passEncoderGPU.setBindGroup( i, bindingsData.group );
}
// 计算调度大小:ceil( count / workgroupSize )
const dispatchCount = Math.ceil( count / workgroupSize[0] );
// 发射工作组
passEncoderGPU.dispatchWorkgroups( dispatchSize[0], dispatchSize[1], dispatchSize[2] );
}
```[29](#0-28)
#### 5.5 WGSL Compute Shader 的生成模板
`WGSLNodeBuilder` 为 Compute Shader 生成如下 WGSL 代码:
```wgsl
// 由 _getWGSLComputeCode() 生成
@compute @workgroup_size( 64, 1, 1 )
fn main( @builtin(global_invocation_id) globalId: vec3<u32>,
@builtin(num_workgroups) numWorkgroups: vec3<u32> ) {
// 计算全局线程索引
instanceIndex = globalId.x
+ globalId.y * ( 64 * numWorkgroups.x )
+ globalId.z * ( 64 * numWorkgroups.x ) * ( 1 * numWorkgroups.y );
// 边界检查(当 count 为数字时自动生成)
if ( instanceIndex >= countUniform ) { return; }
// 用户定义的计算逻辑(由 TSL 节点图生成)
${shaderData.flow}
}
```[30](#0-29)
---
### 第六章:两条路的完整对比与选型建议
#### 6.1 Shader 编译流程对比
```mermaid
graph LR
subgraph "传统路径 WebGLRenderer"
A1["Material 类型"] --> B1["ShaderLib 查找"]
B1 --> C1["getParameters() 收集 50+ 参数"]
C1 --> D1["getProgramCacheKey() 生成缓存键"]
D1 --> E1{"缓存命中?"}
E1 -- "是" --> F1["复用 WebGLProgram"]
E1 -- "否" --> G1["生成 #define 前缀"]
G1 --> H1["resolveIncludes() 展开 ShaderChunk"]
H1 --> I1["gl.compileShader() + gl.linkProgram()"]
I1 --> F1
end
subgraph "新路径 WebGPURenderer"
A2["NodeMaterial + TSL 节点图"] --> B2["NodeBuilder.build()"]
B2 --> C2["setup 阶段:构建节点树"]
C2 --> D2["analyze 阶段:优化分析"]
D2 --> E2["generate 阶段:生成代码"]
E2 --> F2["buildCode()"]
F2 --> G2{"后端类型?"}
G2 -- "WebGPU" --> H2["WGSLNodeBuilder → WGSL"]
G2 -- "WebGL" --> I2["GLSLNodeBuilder → GLSL"]
H2 --> J2["device.createShaderModule()"]
I2 --> K2["gl.compileShader()"]
end
6.2 选型建议
需要最广泛的浏览器兼容性?
→ WebGLRenderer
需要 Compute Shader / 存储缓冲区 / 现代 GPU 特性?
→ WebGPURenderer(自动 fallback WebGL 2)
需要自定义材质效果?
→ 旧项目:onBeforeCompile(WebGLRenderer)
→ 新项目:TSL NodeMaterial(WebGPURenderer,推荐)
需要跨 WebGL/WebGPU 的可移植 Shader?
→ TSL(同一套代码,自动编译为 WGSL 或 GLSL)
```[31](#0-30)
---
### 第七章:实战代码示例
#### 7.1 WebGLRenderer 基础场景
```js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.position.z = 5;
// 使用 ShaderLib 中的 physical 材质
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh( new THREE.BoxGeometry(), material );
scene.add( mesh );
renderer.setAnimationLoop( () => {
mesh.rotation.y += 0.01;
renderer.render( scene, camera );
});
7.2 WebGPURenderer + TSL 动态材质
js
import * as THREE from 'three/webgpu';
import { color, positionLocal, sin, time } from 'three/tsl';
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init();
const material = new THREE.MeshStandardNodeMaterial();
// TSL 节点:颜色随时间变化
material.colorNode = color( 0x00ff00 ).mul( sin( time ).mul( 0.5 ).add( 0.5 ) );
// TSL 节点:顶点随时间波动
material.positionNode = positionLocal.add(
sin( time.add( positionLocal.y ) ).mul( 0.1 )
);
```[32](#0-31)
#### 7.3 Compute Shader:GPU 粒子物理
```js
import * as THREE from 'three/webgpu';
import { Fn, instanceIndex, storage, vec3, float } from 'three/tsl';
import { StorageBufferAttribute } from 'three/webgpu';
const COUNT = 100000;
const renderer = new THREE.WebGPURenderer();
await renderer.init();
// GPU 存储缓冲区
const positionBuffer = new StorageBufferAttribute( new Float32Array( COUNT * 3 ), 3 );
const velocityBuffer = new StorageBufferAttribute( new Float32Array( COUNT * 3 ), 3 );
// 定义 Compute Shader 逻辑(TSL)
const updateParticles = Fn( () => {
const i = instanceIndex;
const pos = storage( positionBuffer, 'vec3', COUNT ).element( i );
const vel = storage( velocityBuffer, 'vec3', COUNT ).element( i );
// 重力
vel.addAssign( vec3( 0, -0.001, 0 ) );
pos.addAssign( vel );
// 地面反弹
If( pos.y.lessThan( 0 ), () => {
pos.y.assign( 0 );
vel.y.mulAssign( -0.8 );
});
});
// 创建 ComputeNode(每次 dispatch 64 个线程)
const computeNode = compute( updateParticles(), COUNT, [ 64 ] );
// 渲染循环
renderer.setAnimationLoop( async () => {
await renderer.computeAsync( computeNode );
renderer.render( scene, camera );
});
```[33](#0-32)
---
### 第八章:迁移路径------从 GLSL 到 TSL
Three.js 还提供了 `Transpiler` 工具,可以将现有 GLSL 代码自动转换为 TSL:
```js
import Transpiler from 'three/addons/transpiler/Transpiler.js';
import { GLSLDecoder } from 'three/addons/transpiler/GLSLDecoder.js';
import { TSLEncoder } from 'three/addons/transpiler/TSLEncoder.js';
const transpiler = new Transpiler( new GLSLDecoder(), new TSLEncoder() );
const glslCode = `
vec3 myEffect( vec3 color, float time ) {
return color * sin( time );
}
`;
const tslCode = transpiler.parse( glslCode );
// 输出:const myEffect = Fn(([color, time]) => color.mul(sin(time)));
```[34](#0-33)
---
### 总结
| 特性 | WebGLRenderer | WebGPURenderer + TSL |
|---|---|---|
| Shader 复用 | 基于 50+ 参数的缓存键 | 基于节点图哈希的 Pipeline 缓存 |
| 自定义 Shader | `onBeforeCompile` 字符串替换 | TSL 节点组合,声明式 |
| 跨后端 | 仅 WebGL | WebGPU + WebGL 2 自动 fallback |
| Compute | 不支持 | `ComputeNode` + `renderer.compute()` |
| 自动优化 | 无 | 重复表达式消除、tree shaking |
| 类型安全 | 无 | TypeScript 友好 |
| 序列化 | 无 | `Node.serialize()` / `deserialize()` |
Three.js 的这次架构演进,本质上是将 Shader 的表达层从"字符串操作"提升到了"函数组合",同时通过后端抽象层实现了真正的渲染 API 无关性。对于新项目,推荐直接采用 `WebGPURenderer + TSL` 路径;对于存量项目,`Transpiler` 工具提供了平滑的迁移通道。 [35](#0-34)
### Citations
**File:** docs/llms.txt (L32-82)
```text
### 2. Choosing Between WebGLRenderer and WebGPURenderer
Three.js maintains both renderers:
**Use WebGLRenderer** (default, mature):
- Maximum browser compatibility
- Well-established, many years of development
- Most examples and tutorials use this
```js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer();
Use WebGPURenderer when you need:
- Custom shaders/materials using TSL (Three.js Shading Language)
- Compute shaders
- Advanced node-based materials
js
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer();
await renderer.init();
3. TSL (Three.js Shading Language)
When using WebGPURenderer, use TSL instead of raw GLSL for custom materials:
js
import { texture, uv, color } from 'three/tsl';
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = texture( myTexture ).mul( color( 0xff0000 ) );
TSL benefits:
- Works with both WebGL and WebGPU backends
- No string manipulation or onBeforeCompile hacks
- Type-safe, composable shader nodes
- Automatic optimization
4. NodeMaterial Classes (for WebGPU/TSL)
When using TSL, use node-based materials:
-
MeshBasicNodeMaterial
-
MeshStandardNodeMaterial
-
MeshPhysicalNodeMaterial
-
LineBasicNodeMaterial
-
SpriteNodeMaterial
File: src/renderers/WebGLRenderer.js (L420-470)
javascriptlet extensions, capabilities, state, info; let properties, textures, environments, attributes, geometries, objects; let programCache, materials, renderLists, renderStates, clipping, shadowMap; let background, morphtargets, bufferRenderer, indexedBufferRenderer; let utils, bindingStates, uniformsGroups; function initGLContext() { extensions = new WebGLExtensions( _gl ); extensions.init(); utils = new WebGLUtils( _gl, extensions ); capabilities = new WebGLCapabilities( _gl, extensions, parameters, utils ); state = new WebGLState( _gl, extensions ); if ( capabilities.reversedDepthBuffer && reversedDepthBuffer ) { state.buffers.depth.setReversed( true ); } _scratchFramebuffer = _gl.createFramebuffer(); _srcFramebuffer = _gl.createFramebuffer(); _dstFramebuffer = _gl.createFramebuffer(); info = new WebGLInfo( _gl ); properties = new WebGLProperties(); textures = new WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info ); environments = new WebGLEnvironments( _this ); attributes = new WebGLAttributes( _gl ); bindingStates = new WebGLBindingStates( _gl, attributes ); geometries = new WebGLGeometries( _gl, attributes, info, bindingStates ); objects = new WebGLObjects( _gl, geometries, attributes, bindingStates, info ); morphtargets = new WebGLMorphtargets( _gl, capabilities, textures ); clipping = new WebGLClipping( properties ); programCache = new WebGLPrograms( _this, environments, extensions, capabilities, bindingStates, clipping ); materials = new WebGLMaterials( _this, properties ); renderLists = new WebGLRenderLists(); renderStates = new WebGLRenderStates( extensions ); background = new WebGLBackground( _this, environments, state, objects, _alpha, premultipliedAlpha ); shadowMap = new WebGLShadowMap( _this, objects, capabilities ); uniformsGroups = new WebGLUniformsGroups( _gl, info, capabilities, state ); bufferRenderer = new WebGLBufferRenderer( _gl, extensions, info ); indexedBufferRenderer = new WebGLIndexedBufferRenderer( _gl, extensions, info ); info.programs = programCache.programs;
File: src/renderers/WebGLRenderer.js (L2155-2224)
javascript
function getProgram( material, scene, object ) {
if ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ...
const materialProperties = properties.get( material );
const lights = currentRenderState.state.lights;
const shadowsArray = currentRenderState.state.shadowsArray;
const lightsStateVersion = lights.state.version;
const parameters = programCache.getParameters( material, lights.state, shadowsArray, scene, object, currentRenderState.state.lightProbeGridArray );
const programCacheKey = programCache.getProgramCacheKey( parameters );
let programs = materialProperties.programs;
// always update environment and fog - changing these trigger an getProgram call, but it's possible that the program doesn't change
materialProperties.environment = ( material.isMeshStandardMaterial || material.isMeshLambertMaterial || material.isMeshPhongMaterial ) ? scene.environment : null;
materialProperties.fog = scene.fog;
const usePMREM = material.isMeshStandardMaterial || ( material.isMeshLambertMaterial && ! material.envMap ) || ( material.isMeshPhongMaterial && ! material.envMap );
materialProperties.envMap = environments.get( material.envMap || materialProperties.environment, usePMREM );
materialProperties.envMapRotation = ( materialProperties.environment !== null && material.envMap === null ) ? scene.environmentRotation : material.envMapRotation;
if ( programs === undefined ) {
// new material
material.addEventListener( 'dispose', onMaterialDispose );
programs = new Map();
materialProperties.programs = programs;
}
let program = programs.get( programCacheKey );
if ( program !== undefined ) {
// early out if program and light state is identical
if ( materialProperties.currentProgram === program && materialProperties.lightsStateVersion === lightsStateVersion ) {
updateCommonMaterialProperties( material, parameters );
return program;
}
} else {
parameters.uniforms = programCache.getUniforms( material );
// Use node builder for node materials if available
if ( _nodesHandler !== null && material.isNodeMaterial ) {
_nodesHandler.build( material, object, parameters );
}
material.onBeforeCompile( parameters, _this );
program = programCache.acquireProgram( parameters, programCacheKey );
programs.set( programCacheKey, program );
materialProperties.uniforms = parameters.uniforms;
}
File: src/renderers/shaders/ShaderChunk.js (L1-108)
javascript
import alphahash_fragment from './ShaderChunk/alphahash_fragment.glsl.js';
import alphahash_pars_fragment from './ShaderChunk/alphahash_pars_fragment.glsl.js';
import alphamap_fragment from './ShaderChunk/alphamap_fragment.glsl.js';
import alphamap_pars_fragment from './ShaderChunk/alphamap_pars_fragment.glsl.js';
import alphatest_fragment from './ShaderChunk/alphatest_fragment.glsl.js';
import alphatest_pars_fragment from './ShaderChunk/alphatest_pars_fragment.glsl.js';
import aomap_fragment from './ShaderChunk/aomap_fragment.glsl.js';
import aomap_pars_fragment from './ShaderChunk/aomap_pars_fragment.glsl.js';
import batching_pars_vertex from './ShaderChunk/batching_pars_vertex.glsl.js';
import batching_vertex from './ShaderChunk/batching_vertex.glsl.js';
import begin_vertex from './ShaderChunk/begin_vertex.glsl.js';
import beginnormal_vertex from './ShaderChunk/beginnormal_vertex.glsl.js';
import bsdfs from './ShaderChunk/bsdfs.glsl.js';
import iridescence_fragment from './ShaderChunk/iridescence_fragment.glsl.js';
import bumpmap_pars_fragment from './ShaderChunk/bumpmap_pars_fragment.glsl.js';
import clipping_planes_fragment from './ShaderChunk/clipping_planes_fragment.glsl.js';
import clipping_planes_pars_fragment from './ShaderChunk/clipping_planes_pars_fragment.glsl.js';
import clipping_planes_pars_vertex from './ShaderChunk/clipping_planes_pars_vertex.glsl.js';
import clipping_planes_vertex from './ShaderChunk/clipping_planes_vertex.glsl.js';
import color_fragment from './ShaderChunk/color_fragment.glsl.js';
import color_pars_fragment from './ShaderChunk/color_pars_fragment.glsl.js';
import color_pars_vertex from './ShaderChunk/color_pars_vertex.glsl.js';
import color_vertex from './ShaderChunk/color_vertex.glsl.js';
import common from './ShaderChunk/common.glsl.js';
import cube_uv_reflection_fragment from './ShaderChunk/cube_uv_reflection_fragment.glsl.js';
import defaultnormal_vertex from './ShaderChunk/defaultnormal_vertex.glsl.js';
import displacementmap_pars_vertex from './ShaderChunk/displacementmap_pars_vertex.glsl.js';
import displacementmap_vertex from './ShaderChunk/displacementmap_vertex.glsl.js';
import emissivemap_fragment from './ShaderChunk/emissivemap_fragment.glsl.js';
import emissivemap_pars_fragment from './ShaderChunk/emissivemap_pars_fragment.glsl.js';
import colorspace_fragment from './ShaderChunk/colorspace_fragment.glsl.js';
import colorspace_pars_fragment from './ShaderChunk/colorspace_pars_fragment.glsl.js';
import envmap_fragment from './ShaderChunk/envmap_fragment.glsl.js';
import envmap_common_pars_fragment from './ShaderChunk/envmap_common_pars_fragment.glsl.js';
import envmap_pars_fragment from './ShaderChunk/envmap_pars_fragment.glsl.js';
import envmap_pars_vertex from './ShaderChunk/envmap_pars_vertex.glsl.js';
import envmap_vertex from './ShaderChunk/envmap_vertex.glsl.js';
import fog_vertex from './ShaderChunk/fog_vertex.glsl.js';
import fog_pars_vertex from './ShaderChunk/fog_pars_vertex.glsl.js';
import fog_fragment from './ShaderChunk/fog_fragment.glsl.js';
import fog_pars_fragment from './ShaderChunk/fog_pars_fragment.glsl.js';
import gradientmap_pars_fragment from './ShaderChunk/gradientmap_pars_fragment.glsl.js';
import lightmap_pars_fragment from './ShaderChunk/lightmap_pars_fragment.glsl.js';
import lights_lambert_fragment from './ShaderChunk/lights_lambert_fragment.glsl.js';
import lights_lambert_pars_fragment from './ShaderChunk/lights_lambert_pars_fragment.glsl.js';
import lights_pars_begin from './ShaderChunk/lights_pars_begin.glsl.js';
import envmap_physical_pars_fragment from './ShaderChunk/envmap_physical_pars_fragment.glsl.js';
import lights_toon_fragment from './ShaderChunk/lights_toon_fragment.glsl.js';
import lights_toon_pars_fragment from './ShaderChunk/lights_toon_pars_fragment.glsl.js';
import lights_phong_fragment from './ShaderChunk/lights_phong_fragment.glsl.js';
import lights_phong_pars_fragment from './ShaderChunk/lights_phong_pars_fragment.glsl.js';
import lights_physical_fragment from './ShaderChunk/lights_physical_fragment.glsl.js';
import lights_physical_pars_fragment from './ShaderChunk/lights_physical_pars_fragment.glsl.js';
import lights_fragment_begin from './ShaderChunk/lights_fragment_begin.glsl.js';
import lights_fragment_maps from './ShaderChunk/lights_fragment_maps.glsl.js';
import lights_fragment_end from './ShaderChunk/lights_fragment_end.glsl.js';
import lightprobes_pars_fragment from './ShaderChunk/lightprobes_pars_fragment.glsl.js';
import logdepthbuf_fragment from './ShaderChunk/logdepthbuf_fragment.glsl.js';
import logdepthbuf_pars_fragment from './ShaderChunk/logdepthbuf_pars_fragment.glsl.js';
import logdepthbuf_pars_vertex from './ShaderChunk/logdepthbuf_pars_vertex.glsl.js';
import logdepthbuf_vertex from './ShaderChunk/logdepthbuf_vertex.glsl.js';
import map_fragment from './ShaderChunk/map_fragment.glsl.js';
import map_pars_fragment from './ShaderChunk/map_pars_fragment.glsl.js';
import map_particle_fragment from './ShaderChunk/map_particle_fragment.glsl.js';
import map_particle_pars_fragment from './ShaderChunk/map_particle_pars_fragment.glsl.js';
import metalnessmap_fragment from './ShaderChunk/metalnessmap_fragment.glsl.js';
import metalnessmap_pars_fragment from './ShaderChunk/metalnessmap_pars_fragment.glsl.js';
import morphinstance_vertex from './ShaderChunk/morphinstance_vertex.glsl.js';
import morphcolor_vertex from './ShaderChunk/morphcolor_vertex.glsl.js';
import morphnormal_vertex from './ShaderChunk/morphnormal_vertex.glsl.js';
import morphtarget_pars_vertex from './ShaderChunk/morphtarget_pars_vertex.glsl.js';
import morphtarget_vertex from './ShaderChunk/morphtarget_vertex.glsl.js';
import normal_fragment_begin from './ShaderChunk/normal_fragment_begin.glsl.js';
import normal_fragment_maps from './ShaderChunk/normal_fragment_maps.glsl.js';
import normal_pars_fragment from './ShaderChunk/normal_pars_fragment.glsl.js';
import normal_pars_vertex from './ShaderChunk/normal_pars_vertex.glsl.js';
import normal_vertex from './ShaderChunk/normal_vertex.glsl.js';
import normalmap_pars_fragment from './ShaderChunk/normalmap_pars_fragment.glsl.js';
import clearcoat_normal_fragment_begin from './ShaderChunk/clearcoat_normal_fragment_begin.glsl.js';
import clearcoat_normal_fragment_maps from './ShaderChunk/clearcoat_normal_fragment_maps.glsl.js';
import clearcoat_pars_fragment from './ShaderChunk/clearcoat_pars_fragment.glsl.js';
import iridescence_pars_fragment from './ShaderChunk/iridescence_pars_fragment.glsl.js';
import opaque_fragment from './ShaderChunk/opaque_fragment.glsl.js';
import packing from './ShaderChunk/packing.glsl.js';
import premultiplied_alpha_fragment from './ShaderChunk/premultiplied_alpha_fragment.glsl.js';
import project_vertex from './ShaderChunk/project_vertex.glsl.js';
import dithering_fragment from './ShaderChunk/dithering_fragment.glsl.js';
import dithering_pars_fragment from './ShaderChunk/dithering_pars_fragment.glsl.js';
import roughnessmap_fragment from './ShaderChunk/roughnessmap_fragment.glsl.js';
import roughnessmap_pars_fragment from './ShaderChunk/roughnessmap_pars_fragment.glsl.js';
import shadowmap_pars_fragment from './ShaderChunk/shadowmap_pars_fragment.glsl.js';
import shadowmap_pars_vertex from './ShaderChunk/shadowmap_pars_vertex.glsl.js';
import shadowmap_vertex from './ShaderChunk/shadowmap_vertex.glsl.js';
import shadowmask_pars_fragment from './ShaderChunk/shadowmask_pars_fragment.glsl.js';
import skinbase_vertex from './ShaderChunk/skinbase_vertex.glsl.js';
import skinning_pars_vertex from './ShaderChunk/skinning_pars_vertex.glsl.js';
import skinning_vertex from './ShaderChunk/skinning_vertex.glsl.js';
import skinnormal_vertex from './ShaderChunk/skinnormal_vertex.glsl.js';
import specularmap_fragment from './ShaderChunk/specularmap_fragment.glsl.js';
import specularmap_pars_fragment from './ShaderChunk/specularmap_pars_fragment.glsl.js';
import tonemapping_fragment from './ShaderChunk/tonemapping_fragment.glsl.js';
import tonemapping_pars_fragment from './ShaderChunk/tonemapping_pars_fragment.glsl.js';
import transmission_fragment from './ShaderChunk/transmission_fragment.glsl.js';
import transmission_pars_fragment from './ShaderChunk/transmission_pars_fragment.glsl.js';
import uv_pars_fragment from './ShaderChunk/uv_pars_fragment.glsl.js';
import uv_pars_vertex from './ShaderChunk/uv_pars_vertex.glsl.js';
import uv_vertex from './ShaderChunk/uv_vertex.glsl.js';
import worldpos_vertex from './ShaderChunk/worldpos_vertex.glsl.js';
File: src/renderers/shaders/ShaderLib.js (L27-50)
javascript
lambert: {
uniforms: /*@__PURE__*/ mergeUniforms( [
UniformsLib.common,
UniformsLib.specularmap,
UniformsLib.envmap,
UniformsLib.aomap,
UniformsLib.lightmap,
UniformsLib.emissivemap,
UniformsLib.bumpmap,
UniformsLib.normalmap,
UniformsLib.displacementmap,
UniformsLib.fog,
UniformsLib.lights,
{
emissive: { value: /*@__PURE__*/ new Color( 0x000000 ) },
envMapIntensity: { value: 1 }
}
] ),
vertexShader: ShaderChunk.meshlambert_vert,
fragmentShader: ShaderChunk.meshlambert_frag
},
File: src/renderers/shaders/ShaderLib/meshlambert.glsl.js (L1-50)
javascript
export const vertex = /* glsl */`
#define LAMBERT
varying vec3 vViewPosition;
#include <common>
#include <batching_pars_vertex>
#include <uv_pars_vertex>
#include <displacementmap_pars_vertex>
#include <envmap_pars_vertex>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <normal_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <shadowmap_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main() {
#include <uv_vertex>
#include <color_vertex>
#include <morphinstance_vertex>
#include <morphcolor_vertex>
#include <batching_vertex>
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinbase_vertex>
#include <skinnormal_vertex>
#include <defaultnormal_vertex>
#include <normal_vertex>
#include <begin_vertex>
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <displacementmap_vertex>
#include <project_vertex>
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
vViewPosition = - mvPosition.xyz;
#include <worldpos_vertex>
#include <envmap_vertex>
#include <shadowmap_vertex>
#include <fog_vertex>
}
File: src/renderers/webgl/WebGLPrograms.js (L393-431)
javascript
function getProgramCacheKey( parameters ) {
const array = [];
if ( parameters.shaderID ) {
array.push( parameters.shaderID );
} else {
array.push( parameters.customVertexShaderID );
array.push( parameters.customFragmentShaderID );
}
if ( parameters.defines !== undefined ) {
for ( const name in parameters.defines ) {
array.push( name );
array.push( parameters.defines[ name ] );
}
}
if ( parameters.isRawShaderMaterial === false ) {
getProgramCacheKeyParameters( array, parameters );
getProgramCacheKeyBooleans( array, parameters );
array.push( renderer.outputColorSpace );
}
array.push( parameters.customProgramCacheKey );
return array.join();
}
File: src/renderers/webgl/WebGLProgram.js (L241-276)
javascript
// Resolve Includes
const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
function resolveIncludes( string ) {
return string.replace( includePattern, includeReplacer );
}
const shaderChunkMap = new Map();
function includeReplacer( match, include ) {
let string = ShaderChunk[ include ];
if ( string === undefined ) {
const newInclude = shaderChunkMap.get( include );
if ( newInclude !== undefined ) {
string = ShaderChunk[ newInclude ];
warn( 'WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.', include, newInclude );
} else {
throw new Error( 'THREE.WebGLProgram: Can not resolve #include <' + include + '>' );
}
}
return resolveIncludes( string );
}
File: src/renderers/webgl/WebGLProgram.js (L473-592)
javascript
prefixVertex = [
generatePrecision( parameters ),
'#define SHADER_TYPE ' + parameters.shaderType,
'#define SHADER_NAME ' + parameters.shaderName,
customDefines,
parameters.extensionClipCullDistance ? '#define USE_CLIP_DISTANCE' : '',
parameters.batching ? '#define USE_BATCHING' : '',
parameters.batchingColor ? '#define USE_BATCHING_COLOR' : '',
parameters.instancing ? '#define USE_INSTANCING' : '',
parameters.instancingColor ? '#define USE_INSTANCING_COLOR' : '',
parameters.instancingMorph ? '#define USE_INSTANCING_MORPH' : '',
parameters.useFog && parameters.fog ? '#define USE_FOG' : '',
parameters.useFog && parameters.fogExp2 ? '#define FOG_EXP2' : '',
parameters.map ? '#define USE_MAP' : '',
parameters.envMap ? '#define USE_ENVMAP' : '',
parameters.envMap ? '#define ' + envMapModeDefine : '',
parameters.lightMap ? '#define USE_LIGHTMAP' : '',
parameters.aoMap ? '#define USE_AOMAP' : '',
parameters.bumpMap ? '#define USE_BUMPMAP' : '',
parameters.normalMap ? '#define USE_NORMALMAP' : '',
parameters.normalMapObjectSpace ? '#define USE_NORMALMAP_OBJECTSPACE' : '',
parameters.normalMapTangentSpace ? '#define USE_NORMALMAP_TANGENTSPACE' : '',
parameters.displacementMap ? '#define USE_DISPLACEMENTMAP' : '',
parameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',
parameters.anisotropy ? '#define USE_ANISOTROPY' : '',
parameters.anisotropyMap ? '#define USE_ANISOTROPYMAP' : '',
parameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',
parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',
parameters.iridescenceMap ? '#define USE_IRIDESCENCEMAP' : '',
parameters.iridescenceThicknessMap ? '#define USE_IRIDESCENCE_THICKNESSMAP' : '',
parameters.specularMap ? '#define USE_SPECULARMAP' : '',
parameters.specularColorMap ? '#define USE_SPECULAR_COLORMAP' : '',
parameters.specularIntensityMap ? '#define USE_SPECULAR_INTENSITYMAP' : '',
parameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',
parameters.metalnessMap ? '#define USE_METALNESSMAP' : '',
parameters.alphaMap ? '#define USE_ALPHAMAP' : '',
parameters.alphaHash ? '#define USE_ALPHAHASH' : '',
parameters.transmission ? '#define USE_TRANSMISSION' : '',
parameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',
parameters.thicknessMap ? '#define USE_THICKNESSMAP' : '',
parameters.sheenColorMap ? '#define USE_SHEEN_COLORMAP' : '',
parameters.sheenRoughnessMap ? '#define USE_SHEEN_ROUGHNESSMAP' : '',
//
parameters.mapUv ? '#define MAP_UV ' + parameters.mapUv : '',
parameters.alphaMapUv ? '#define ALPHAMAP_UV ' + parameters.alphaMapUv : '',
parameters.lightMapUv ? '#define LIGHTMAP_UV ' + parameters.lightMapUv : '',
parameters.aoMapUv ? '#define AOMAP_UV ' + parameters.aoMapUv : '',
parameters.emissiveMapUv ? '#define EMISSIVEMAP_UV ' + parameters.emissiveMapUv : '',
parameters.bumpMapUv ? '#define BUMPMAP_UV ' + parameters.bumpMapUv : '',
parameters.normalMapUv ? '#define NORMALMAP_UV ' + parameters.normalMapUv : '',
parameters.displacementMapUv ? '#define DISPLACEMENTMAP_UV ' + parameters.displacementMapUv : '',
parameters.metalnessMapUv ? '#define METALNESSMAP_UV ' + parameters.metalnessMapUv : '',
parameters.roughnessMapUv ? '#define ROUGHNESSMAP_UV ' + parameters.roughnessMapUv : '',
parameters.anisotropyMapUv ? '#define ANISOTROPYMAP_UV ' + parameters.anisotropyMapUv : '',
parameters.clearcoatMapUv ? '#define CLEARCOATMAP_UV ' + parameters.clearcoatMapUv : '',
parameters.clearcoatNormalMapUv ? '#define CLEARCOAT_NORMALMAP_UV ' + parameters.clearcoatNormalMapUv : '',
parameters.clearcoatRoughnessMapUv ? '#define CLEARCOAT_ROUGHNESSMAP_UV ' + parameters.clearcoatRoughnessMapUv : '',
parameters.iridescenceMapUv ? '#define IRIDESCENCEMAP_UV ' + parameters.iridescenceMapUv : '',
parameters.iridescenceThicknessMapUv ? '#define IRIDESCENCE_THICKNESSMAP_UV ' + parameters.iridescenceThicknessMapUv : '',
parameters.sheenColorMapUv ? '#define SHEEN_COLORMAP_UV ' + parameters.sheenColorMapUv : '',
parameters.sheenRoughnessMapUv ? '#define SHEEN_ROUGHNESSMAP_UV ' + parameters.sheenRoughnessMapUv : '',
parameters.specularMapUv ? '#define SPECULARMAP_UV ' + parameters.specularMapUv : '',
parameters.specularColorMapUv ? '#define SPECULAR_COLORMAP_UV ' + parameters.specularColorMapUv : '',
parameters.specularIntensityMapUv ? '#define SPECULAR_INTENSITYMAP_UV ' + parameters.specularIntensityMapUv : '',
parameters.transmissionMapUv ? '#define TRANSMISSIONMAP_UV ' + parameters.transmissionMapUv : '',
parameters.thicknessMapUv ? '#define THICKNESSMAP_UV ' + parameters.thicknessMapUv : '',
//
parameters.vertexTangents && parameters.flatShading === false ? '#define USE_TANGENT' : '',
parameters.vertexNormals ? '#define HAS_NORMAL' : '',
parameters.vertexColors ? '#define USE_COLOR' : '',
parameters.vertexAlphas ? '#define USE_COLOR_ALPHA' : '',
parameters.vertexUv1s ? '#define USE_UV1' : '',
parameters.vertexUv2s ? '#define USE_UV2' : '',
parameters.vertexUv3s ? '#define USE_UV3' : '',
parameters.pointsUvs ? '#define USE_POINTS_UV' : '',
parameters.flatShading ? '#define FLAT_SHADED' : '',
parameters.skinning ? '#define USE_SKINNING' : '',
parameters.morphTargets ? '#define USE_MORPHTARGETS' : '',
parameters.morphNormals && parameters.flatShading === false ? '#define USE_MORPHNORMALS' : '',
( parameters.morphColors ) ? '#define USE_MORPHCOLORS' : '',
( parameters.morphTargetsCount > 0 ) ? '#define MORPHTARGETS_TEXTURE_STRIDE ' + parameters.morphTextureStride : '',
( parameters.morphTargetsCount > 0 ) ? '#define MORPHTARGETS_COUNT ' + parameters.morphTargetsCount : '',
parameters.doubleSided ? '#define DOUBLE_SIDED' : '',
parameters.flipSided ? '#define FLIP_SIDED' : '',
parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',
parameters.sizeAttenuation ? '#define USE_SIZEATTENUATION' : '',
parameters.numLightProbes > 0 ? '#define USE_LIGHT_PROBES' : '',
File: src/renderers/webgl/WebGLProgram.js (L801-857)
javascript
if ( parameters.isRawShaderMaterial !== true ) {
// GLSL 3.0 conversion for built-in materials and ShaderMaterial
versionString = '#version 300 es\n';
prefixVertex = [
customVertexExtensions,
'#define attribute in',
'#define varying out',
'#define texture2D texture'
].join( '\n' ) + '\n' + prefixVertex;
prefixFragment = [
'#define varying in',
( parameters.glslVersion === GLSL3 ) ? '' : 'layout(location = 0) out highp vec4 pc_fragColor;',
( parameters.glslVersion === GLSL3 ) ? '' : '#define gl_FragColor pc_fragColor',
'#define gl_FragDepthEXT gl_FragDepth',
'#define texture2D texture',
'#define textureCube texture',
'#define texture2DProj textureProj',
'#define texture2DLodEXT textureLod',
'#define texture2DProjLodEXT textureProjLod',
'#define textureCubeLodEXT textureLod',
'#define texture2DGradEXT textureGrad',
'#define texture2DProjGradEXT textureProjGrad',
'#define textureCubeGradEXT textureGrad'
].join( '\n' ) + '\n' + prefixFragment;
}
const vertexGlsl = versionString + prefixVertex + vertexShader;
const fragmentGlsl = versionString + prefixFragment + fragmentShader;
// log( '*VERTEX*', vertexGlsl );
// log( '*FRAGMENT*', fragmentGlsl );
const glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
const glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );
gl.attachShader( program, glVertexShader );
gl.attachShader( program, glFragmentShader );
// Force a particular attribute to index 0.
if ( parameters.index0AttributeName !== undefined ) {
gl.bindAttribLocation( program, 0, parameters.index0AttributeName );
} else if ( parameters.morphTargets === true ) {
// programs with morphTargets displace position out of attribute 0
gl.bindAttribLocation( program, 0, 'position' );
}
gl.linkProgram( program );
File: src/materials/Material.js (L519-531)
javascript
/**
* An optional callback that is executed immediately before the shader
* program is compiled. This function is called with the shader source code
* as a parameter. Useful for the modification of built-in materials.
*
* This method can only be used when rendering with {@link WebGLRenderer}. The
* recommended approach when customizing materials is to use `WebGPURenderer` with the new
* Node Material system and [TSL](https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language).
*
* @param {{vertexShader:string,fragmentShader:string,uniforms:Object}} shaderobject - The object holds the uniforms and the vertex and fragment shader source.
* @param {WebGLRenderer} renderer - A reference to the renderer.
*/
onBeforeCompile( /* shaderobject, renderer */ ) {}
File: src/renderers/webgpu/WebGPURenderer.js (L28-103)
javascript
class WebGPURenderer extends Renderer {
/**
* WebGPURenderer options.
*
* @typedef {Object} WebGPURenderer~Options
* @property {boolean} [logarithmicDepthBuffer=false] - Whether logarithmic depth buffer is enabled or not.
* @property {boolean} [reversedDepthBuffer=false] - Whether reversed depth buffer is enabled or not.
* @property {boolean} [alpha=true] - Whether the default framebuffer (which represents the final contents of the canvas) should be transparent or opaque.
* @property {boolean} [depth=true] - Whether the default framebuffer should have a depth buffer or not.
* @property {boolean} [stencil=false] - Whether the default framebuffer should have a stencil buffer or not.
* @property {boolean} [antialias=false] - Whether MSAA as the default anti-aliasing should be enabled or not.
* @property {number} [samples=0] - When `antialias` is `true`, `4` samples are used by default. Set this parameter to any other integer value than 0 to overwrite the default.
* @property {boolean} [forceWebGL=false] - If set to `true`, the renderer uses a WebGL 2 backend no matter if WebGPU is supported or not.
* @property {boolean} [multiview=false] - If set to `true`, the renderer will use multiview during WebXR rendering if supported.
* @property {number} [outputType=undefined] - Texture type for output to canvas. By default, device's preferred format is used; other formats may incur overhead.
* @property {number} [outputBufferType=HalfFloatType] - Defines the type of output buffers. The default `HalfFloatType` is recommend for best
* quality. To save memory and bandwidth, `UnsignedByteType` might be used. This will reduce rendering quality though.
*/
/**
* Constructs a new WebGPU renderer.
*
* @param {WebGPURenderer~Options} [parameters] - The configuration parameter.
*/
constructor( parameters = {} ) {
let BackendClass;
if ( parameters.forceWebGL ) {
BackendClass = WebGLBackend;
} else {
BackendClass = WebGPUBackend;
parameters.getFallback = () => {
warn( 'WebGPURenderer: WebGPU is not available, running under WebGL2 backend.' );
return new WebGLBackend( parameters );
};
}
const backend = new BackendClass( parameters );
//super( new Proxy( backend, debugHandler ) );
super( backend, parameters );
/**
* The generic default value is overwritten with the
* standard node library for type mapping.
*
* @type {StandardNodeLibrary}
*/
this.library = new StandardNodeLibrary();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isWebGPURenderer = true;
if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {
__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );
}
}
File: src/renderers/common/Renderer.js (L754-827)
javascript
async init() {
if ( this._initPromise !== null ) {
return this._initPromise;
}
this._initPromise = new Promise( async ( resolve, reject ) => {
let backend = this.backend;
try {
await backend.init( this );
} catch ( error ) {
if ( this._getFallback !== null ) {
// try the fallback
try {
this.backend = backend = this._getFallback( error );
await backend.init( this );
} catch ( error ) {
reject( error );
return;
}
} else {
reject( error );
return;
}
}
this._nodes = new NodeManager( this, backend );
this._animation = new Animation( this, this._nodes, this.info );
this._attributes = new Attributes( backend, this.info );
this._background = new Background( this, this._nodes );
this._geometries = new Geometries( this._attributes, this.info );
this._textures = new Textures( this, backend, this.info );
this._pipelines = new Pipelines( backend, this._nodes, this.info );
this._bindings = new Bindings( backend, this._nodes, this._textures, this._attributes, this._pipelines, this.info );
this._objects = new RenderObjects( this, this._nodes, this._geometries, this._pipelines, this._bindings, this.info );
this._renderLists = new RenderLists( this.lighting );
this._bundles = new RenderBundles();
this._renderContexts = new RenderContexts( this );
//
this._animation.start();
this._initialized = true;
//
this._inspector.init();
//
resolve( this );
} );
return this._initPromise;
}
File: src/renderers/common/Renderer.js (L2779-2837)
javascript
this.inspector.beginCompute( this.backend.getTimestampUID( computeNodes ), computeNodes );
//
const backend = this.backend;
const pipelines = this._pipelines;
const bindings = this._bindings;
const nodes = this._nodes;
const computeList = Array.isArray( computeNodes ) ? computeNodes : [ computeNodes ];
if ( computeList[ 0 ] === undefined || computeList[ 0 ].isComputeNode !== true ) {
throw new Error( 'THREE.Renderer: .compute() expects a ComputeNode.' );
}
backend.beginCompute( computeNodes );
for ( const computeNode of computeList ) {
// onInit
if ( pipelines.has( computeNode ) === false ) {
const dispose = () => {
computeNode.removeEventListener( 'dispose', dispose );
pipelines.delete( computeNode );
bindings.deleteForCompute( computeNode );
nodes.delete( computeNode );
};
computeNode.addEventListener( 'dispose', dispose );
//
const onInitFn = computeNode.onInitFunction;
if ( onInitFn !== null ) {
onInitFn.call( computeNode, { renderer: this } );
}
}
nodes.updateForCompute( computeNode );
bindings.updateForCompute( computeNode );
const computeBindings = bindings.getForCompute( computeNode );
const computePipeline = pipelines.getForCompute( computeNode, computeBindings );
backend.compute( computeNodes, computeNode, computeBindings, computePipeline, dispatchSize );
}
File: src/renderers/common/Backend.js (L178-268)
javascript
compute( /*computeGroup, computeNode, computeBindings, computePipeline*/ ) { }
// program
/**
* Creates a shader program from the given programmable stage.
*
* @abstract
* @param {ProgrammableStage} program - The programmable stage.
*/
createProgram( /*program*/ ) { }
/**
* Destroys the shader program of the given programmable stage.
*
* @abstract
* @param {ProgrammableStage} program - The programmable stage.
*/
destroyProgram( /*program*/ ) { }
// bindings
/**
* Creates bindings from the given bind group definition.
*
* @abstract
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {number} cacheIndex - The cache index.
* @param {number} version - The version.
*/
createBindings( /*bindGroup, bindings, cacheIndex, version*/ ) { }
/**
* Updates the given bind group definition.
*
* @abstract
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {number} cacheIndex - The cache index.
* @param {number} version - The version.
*/
updateBindings( /*bindGroup, bindings, cacheIndex, version*/ ) { }
/**
* Updates a buffer binding.
*
* @abstract
* @param {Buffer} binding - The buffer binding to update.
*/
updateBinding( /*binding*/ ) { }
// pipeline
/**
* Creates a render pipeline for the given render object.
*
* @abstract
* @param {RenderObject} renderObject - The render object.
* @param {Array<Promise>} promises - An array of compilation promises which are used in `compileAsync()`.
*/
createRenderPipeline( /*renderObject, promises*/ ) { }
/**
* Creates a compute pipeline for the given compute node.
*
* @abstract
* @param {ComputePipeline} computePipeline - The compute pipeline.
* @param {Array<BindGroup>} bindings - The bindings.
*/
createComputePipeline( /*computePipeline, bindings*/ ) { }
// cache key
/**
* Returns `true` if the render pipeline requires an update.
*
* @abstract
* @param {RenderObject} renderObject - The render object.
* @return {boolean} Whether the render pipeline requires an update or not.
*/
needsRenderUpdate( /*renderObject*/ ) { }
/**
* Returns a cache key that is used to identify render pipelines.
*
* @abstract
* @param {RenderObject} renderObject - The render object.
* @return {string} The cache key.
*/
getRenderCacheKey( /*renderObject*/ ) { }
File: docs/TSL.md (L66-75)
markdown
## Introduction
### Why TSL?
Creating shaders has always been an advanced step for most developers; many game developers have never created GLSL code from scratch. The shader graph solution adopted today by the industry has allowed developers more focused on dynamics to create the necessary graphic effects to meet the demands of their projects.
The aim of the project is to create an easy-to-use environment for shader creation. Even if for this we need to create complexity behind it, this happened initially with `Renderer` and now with the `TSL`.
Other benefits that TSL brings besides simplifying shading creation are keeping the `renderer agnostic`, while all the complexity of a material can be imported into different modules and use `tree shaking` without breaking during the process.
File: docs/TSL.md (L80-123)
markdown
#### Old
This is how we would achieve that using `.onBeforeCompile()`:
```js
const material = new THREE.MeshStandardMaterial();
material.map = colorMap;
material.onBeforeCompile = ( shader ) => {
shader.uniforms.detailMap = { value: detailMap };
let token = '#define STANDARD';
let insert = /* glsl */`
uniform sampler2D detailMap;
`;
shader.fragmentShader = shader.fragmentShader.replace( token, token + insert );
token = '#include <map_fragment>';
insert = /* glsl */`
diffuseColor *= texture2D( detailMap, vMapUv * 10.0 );
`;
shader.fragmentShader = shader.fragmentShader.replace( token, token + insert );
};
Any simple change from this makes the code increasingly complicated using .onBeforeCompile, the result we have today in the community are countless types of parametric materials that do not communicate with each other, and that need to be updated periodically to be operating, limiting the creativity to create unique materials reusing modules in a simple way.
New
With TSL the code would look like this:
js
import { texture, uv } from 'three/tsl';
const detail = texture( detailMap, uv().mul( 10 ) );
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = texture( colorMap ).mul( detail );
**File:** docs/TSL.md (L125-130)
```markdown
`TSL` is also capable of encoding code into different outputs such as `WGSL`/`GLSL` - `WebGPU`/`WebGL`, in addition to optimizing the shader graph automatically and through codes that can be inserted within each `Node`. This allows the developer to focus on productivity and leave the graphical management part to the `Node System`.
Another important feature of a graph shader is that we will no longer need to care about the sequence in which components are created, because the `Node System` will only declare and include it once.
Let's say that you import `positionWorld` into your code, even if another component uses it, the calculations performed to obtain `position world` will only be performed once, as is the case with any other node such as: `normalWorld`, `modelPosition`, etc.
File: docs/TSL.md (L131-147)
markdown
### Architecture
All `TSL` components are extended from `Node` class. The `Node` allows it to communicate with any other, value conversions can be automatic or manual, a `Node` can receive the output value expected by the parent `Node` and modify its own output snippet. It's possible to modulate them using `tree shaking` in the shader construction process, the `Node` will have important information such as `geometry`, `material`, `renderer` as well as the `backend`, which can influence the type and value of output.
The main class responsible for creating the code is `NodeBuilder`. This class can be extended to any output programming language, so you can use TSL for a third language if you wish. Currently `NodeBuilder` has two extended classes, the `WGSLNodeBuilder` aimed at WebGPU and `GLSLNodeBuilder` aimed at WebGL2.
The build process is based on three pillars: `setup`, `analyze` and `generate`.
| | |
| -- | -- |
| `setup` | Use `TSL` to create a completely customized code for the `Node` output. The `Node` can use many others within itself, have countless inputs, but there will always be a single output. |
| `analyze` | This proccess will check the `nodes` that were created in order to create useful information for `generate` the snippet, such as the need to create or not a cache/variable for optimizing a node. |
| `generate` | An output of `string` will be returned from each `node`. Any node will also be able to create code in the flow of shader, supporting multiple lines. |
`Node` also have a native update process invoked by the `update()` function, these events be called by `frame`, `render call` and `object draw`.
It is also possible to serialize or deserialize a `Node` using `serialize()` and `deserialize()` functions.
File: src/nodes/core/NodeBuilder.js (L3086-3138)
javascript
build() {
this.prebuild();
// setup() -> stage 1: create possible new nodes and/or return an output reference node
// analyze() -> stage 2: analyze nodes to possible optimization and validation
// generate() -> stage 3: generate shader
for ( const buildStage of defaultBuildStages ) {
this.setBuildStage( buildStage );
if ( this.context.position && this.context.position.isNode ) {
this.flowNodeFromShaderStage( 'vertex', this.context.position );
}
for ( const shaderStage of shaderStages ) {
this.setShaderStage( shaderStage );
const flowNodes = this.flowNodes[ shaderStage ];
for ( const node of flowNodes ) {
if ( buildStage === 'generate' ) {
this.flowNode( node );
} else {
node.build( this );
}
}
}
}
this.setBuildStage( null );
this.setShaderStage( null );
// stage 4: build code for a specific output
this.buildCode();
this.buildUpdateNodes();
return this;
}
File: src/nodes/core/Node.js (L798-807)
javascript
/**
* This method performs the build of a node. The behavior and return value depend on the current build stage:
* - **setup**: Prepares the node and its children for the build process. This process can also create new nodes. Returns the node itself or a variant.
* - **analyze**: Analyzes the node hierarchy for optimizations in the code generation stage. Returns `null`.
* - **generate**: Generates the shader code for the node. Returns the generated shader string.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {?(string|Node)} [output=null] - Can be used to define the output type.
* @return {?(Node|string)} The result of the build process, depending on the build stage.
*/
File: src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js (L1508-1549)
javascript
_getGLSLVertexCode( shaderData ) {
return `#version 300 es
${ this.getSignature() }
// extensions
${shaderData.extensions}
// precision
${ defaultPrecisions }
// structs
${shaderData.structs}
// uniforms
${shaderData.uniforms}
// varyings
${shaderData.varyings}
// attributes
${shaderData.attributes}
// vars
${shaderData.vars}
// codes
${shaderData.codes}
void main() {
// transforms
${shaderData.transforms}
// flow
${shaderData.flow}
gl_PointSize = 1.0;
}
`;
File: src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js (L1597-1692)
javascript
/**
* Controls the code build of the shader stages.
*/
buildCode() {
const shadersData = this.material !== null ? { fragment: {}, vertex: {} } : { compute: {} };
this.sortBindingGroups();
for ( const shaderStage in shadersData ) {
let flow = '// code\n\n';
flow += this.flowCode[ shaderStage ];
const flowNodes = this.flowNodes[ shaderStage ];
const mainNode = flowNodes[ flowNodes.length - 1 ];
for ( const node of flowNodes ) {
const flowSlotData = this.getFlowData( node/*, shaderStage*/ );
const slotName = node.name;
if ( slotName ) {
if ( flow.length > 0 ) flow += '\n';
flow += `\t// flow -> ${ slotName }\n\t`;
}
flow += `${ flowSlotData.code }\n\t`;
if ( node === mainNode && shaderStage !== 'compute' ) {
flow += '// result\n\t';
if ( shaderStage === 'vertex' ) {
flow += 'gl_Position = ';
flow += `${ this.format( flowSlotData.result, mainNode.getNodeType( this ), 'vec4' ) };`;
} else if ( shaderStage === 'fragment' ) {
if ( ! node.outputNode.isOutputStructNode ) {
flow += 'fragColor = ';
flow += `${ this.format( flowSlotData.result, mainNode.getNodeType( this ), this.getOutputType() ) };`;
}
}
}
}
const stageData = shadersData[ shaderStage ];
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;
// fallbacks
if ( shaderStage === 'vertex' ) {
const ext = this.renderer.backend.extensions;
if ( this.object.isBatchedMesh && ext.has( 'WEBGL_multi_draw' ) === false ) {
stageData.uniforms += '\nuniform uint nodeUniformDrawId;\n';
}
}
}
if ( this.material !== null ) {
this.vertexShader = this._getGLSLVertexCode( shadersData.vertex );
this.fragmentShader = this._getGLSLFragmentCode( shadersData.fragment );
} else {
this.computeShader = this._getGLSLVertexCode( shadersData.compute );
}
}
File: src/renderers/webgpu/nodes/WGSLNodeBuilder.js (L2435-2466)
javascript
_getWGSLVertexCode( shaderData ) {
return `${ this.getSignature() }
// directives
${shaderData.directives}
// structs
${shaderData.structs}
// uniforms
${shaderData.uniforms}
// varyings
${shaderData.varyings}
var<private> varyings : VaryingsStruct;
// vars
${shaderData.vars}
// codes
${shaderData.codes}
@vertex
fn main( ${shaderData.attributes} ) -> VaryingsStruct {
// flow
${shaderData.flow}
return varyings;
}
`;
File: src/renderers/webgpu/nodes/WGSLNodeBuilder.js (L2514-2555)
javascript
_getWGSLComputeCode( shaderData, workgroupSize ) {
const [ workgroupSizeX, workgroupSizeY, workgroupSizeZ ] = workgroupSize;
return `${ this.getSignature() }
// directives
${ shaderData.directives }
// system
var<private> instanceIndex : u32;
// locals
${ shaderData.scopedArrays }
// structs
${ shaderData.structs }
// uniforms
${ shaderData.uniforms }
// vars
${ this.allowGlobalVariables ? shaderData.vars : '' }
// codes
${ shaderData.codes }
@compute @workgroup_size( ${ workgroupSizeX }, ${ workgroupSizeY }, ${ workgroupSizeZ } )
fn main( ${ shaderData.attributes } ) {
// local vars
${ this.allowGlobalVariables ? '' : shaderData.vars }
// system
instanceIndex = globalId.x
+ globalId.y * ( ${ workgroupSizeX } * numWorkgroups.x )
+ globalId.z * ( ${ workgroupSizeX } * numWorkgroups.x ) * ( ${ workgroupSizeY } * numWorkgroups.y );
// flow
${ shaderData.flow }
}
`;
File: src/nodes/gpgpu/ComputeNode.js (L14-111)
javascript
class ComputeNode extends Node {
static get type() {
return 'ComputeNode';
}
/**
* Constructs a new compute node.
*
* @param {Node} computeNode - The node that defines the compute shader logic.
* @param {Array<number>} workgroupSize - An array defining the X, Y, and Z dimensions of the workgroup for compute shader execution.
*/
constructor( computeNode, workgroupSize ) {
super( 'void' );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isComputeNode = true;
/**
* The node that defines the compute shader logic.
*
* @type {Node}
*/
this.computeNode = computeNode;
/**
* An array defining the X, Y, and Z dimensions of the workgroup for compute shader execution.
*
* @type {Array<number>}
* @default [ 64 ]
*/
this.workgroupSize = workgroupSize;
/**
* The total number of threads (invocations) to execute. If it is a number, it will be used
* to automatically generate bounds checking against `instanceIndex`.
*
* @type {number|Array<number>}
*/
this.count = null;
/**
* The dispatch size for workgroups on X, Y, and Z axes.
* Used directly if `count` is not provided.
*
* @type {number|Array<number>}
*/
this.dispatchSize = null;
/**
* The version of the node.
*
* @type {number}
*/
this.version = 1;
/**
* The name or label of the uniform.
*
* @type {string}
* @default ''
*/
this.name = '';
/**
* The `updateBeforeType` is set to `NodeUpdateType.OBJECT` since {@link ComputeNode#updateBefore}
* is executed once per object by default.
*
* @type {string}
* @default 'object'
*/
this.updateBeforeType = NodeUpdateType.OBJECT;
/**
* A callback executed when the compute node finishes initialization.
*
* @type {?Function}
*/
this.onInitFunction = null;
/**
* A uniform node holding the dispatch count for bounds checking.
* Created automatically when `count` is a number.
*
* @type {?UniformNode}
*/
this.countNode = null;
}
File: src/nodes/gpgpu/ComputeNode.js (L251-307)
javascript
export const computeKernel = ( node, workgroupSize = [ 64 ] ) => {
if ( workgroupSize.length === 0 || workgroupSize.length > 3 ) {
error( 'TSL: compute() workgroupSize must have 1, 2, or 3 elements', new StackTrace() );
}
for ( let i = 0; i < workgroupSize.length; i ++ ) {
const val = workgroupSize[ i ];
if ( typeof val !== 'number' || val <= 0 || ! Number.isInteger( val ) ) {
error( `TSL: compute() workgroupSize element at index [ ${ i } ] must be a positive integer`, new StackTrace() );
}
}
// Implicit fill-up to [ x, y, z ] with 1s, just like WGSL treats @workgroup_size when fewer dimensions are specified
while ( workgroupSize.length < 3 ) workgroupSize.push( 1 );
//
return new ComputeNode( nodeObject( node ), workgroupSize );
};
/**
* TSL function for creating a compute node.
*
* @tsl
* @function
* @param {Node} node - The TSL logic for the compute shader.
* @param {number|Array<number>} count - The compute count or dispatch size.
* @param {Array<number>} [workgroupSize=[64]] - The workgroup size.
* @returns {ComputeNode}
, */
export const compute = ( node, count, workgroupSize ) => {
const computeNode = computeKernel( node, workgroupSize );
if ( typeof count === 'number' ) {
computeNode.count = count;
} else {
computeNode.dispatchSize = count;
}
return computeNode;
};
File: src/renderers/webgpu/WebGPUBackend.js (L1554-1677)
javascript
beginCompute( computeGroup ) {
const groupGPU = this.get( computeGroup );
//
const label = 'computeGroup_' + computeGroup.id;
_computePassDescriptor.label = label;
_commandEncoderDescriptor.label = label;
this.initTimestampQuery( TimestampQuery.COMPUTE, this.getTimestampUID( computeGroup ), _computePassDescriptor );
groupGPU.cmdEncoderGPU = this.device.createCommandEncoder( _commandEncoderDescriptor );
groupGPU.passEncoderGPU = groupGPU.cmdEncoderGPU.beginComputePass( _computePassDescriptor );
_commandEncoderDescriptor.reset();
_computePassDescriptor.reset();
}
/**
* Executes a compute command for the given compute node.
*
* @param {Node|Array<Node>} computeGroup - The group of compute nodes of a compute call. Can be a single compute node.
* @param {Node} computeNode - The compute node.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {ComputePipeline} pipeline - The compute pipeline.
* @param {number|Array<number>|IndirectStorageBufferAttribute} [dispatchSize=null]
* - A single number representing count, or
* - An array [x, y, z] representing dispatch size, or
* - A IndirectStorageBufferAttribute for indirect dispatch size.
*/
compute( computeGroup, computeNode, bindings, pipeline, dispatchSize = null ) {
const computeNodeData = this.get( computeNode );
const { passEncoderGPU } = this.get( computeGroup );
// pipeline
const pipelineGPU = this.get( pipeline ).pipeline;
this.pipelineUtils.setPipeline( passEncoderGPU, pipelineGPU );
// bind groups
for ( let i = 0, l = bindings.length; i < l; i ++ ) {
const bindGroup = bindings[ i ];
const bindingsData = this.get( bindGroup );
passEncoderGPU.setBindGroup( i, bindingsData.group );
}
if ( dispatchSize === null ) {
dispatchSize = computeNode.dispatchSize || computeNode.count;
}
// When the dispatchSize is set with a StorageBuffer from the GPU.
if ( dispatchSize && dispatchSize.isIndirectStorageBufferAttribute ) {
const dispatchBuffer = this.get( dispatchSize ).buffer;
passEncoderGPU.dispatchWorkgroupsIndirect( dispatchBuffer, 0 );
return;
}
if ( typeof dispatchSize === 'number' ) {
// If a single number is given, we calculate the dispatch size based on the workgroup size
const count = dispatchSize;
if ( computeNodeData.dispatchSize === undefined || computeNodeData.count !== count ) {
// cache dispatch size to avoid recalculating it every time
computeNodeData.dispatchSize = [ 0, 1, 1 ];
computeNodeData.count = count;
const workgroupSize = computeNode.workgroupSize;
let size = workgroupSize[ 0 ];
for ( let i = 1; i < workgroupSize.length; i ++ )
size *= workgroupSize[ i ];
const dispatchCount = Math.ceil( count / size );
//
const maxComputeWorkgroupsPerDimension = this.device.limits.maxComputeWorkgroupsPerDimension;
dispatchSize = [ dispatchCount, 1, 1 ];
if ( dispatchCount > maxComputeWorkgroupsPerDimension ) {
dispatchSize[ 0 ] = Math.min( dispatchCount, maxComputeWorkgroupsPerDimension );
dispatchSize[ 1 ] = Math.ceil( dispatchCount / maxComputeWorkgroupsPerDimension );
}
computeNodeData.dispatchSize = dispatchSize;
}
dispatchSize = computeNodeData.dispatchSize;
}
//
passEncoderGPU.dispatchWorkgroups(
dispatchSize[ 0 ],
dispatchSize[ 1 ] || 1,
dispatchSize[ 2 ] || 1
);
File: utils/llms/build.js (L464-488)
javascript
// Custom TSL material
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = color( 0x00ff00 ).mul( sin( time ).mul( 0.5 ).add( 0.5 ) );
material.positionNode = positionLocal.add( sin( time.add( positionLocal.y ) ).mul( 0.1 ) );
// Mesh
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
// Handle resize
window.addEventListener( 'resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
} );
// Animation loop
function animate() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
controls.update();
renderer.render( scene, camera );
}
renderer.setAnimationLoop( animate );
File: docs/pages/Transpiler.html.md (L1-57)
markdown
# Transpiler
A class that transpiles shader code from one language into another.
`Transpiler` can only be used to convert GLSL into TSL right now. It is intended to support developers when they want to migrate their custom materials from the current to the new node-based material system.
## Import
Transpiler is an addon, and must be imported explicitly, see [Installation#Addons](https://threejs.org/manual/#en/installation).
```js
import Transpiler from 'three/addons/transpiler/Transpiler.js';
Constructor
new Transpiler( decoder : GLSLDecoder, encoder : TSLEncoder )
Constructs a new transpiler.
decoder
The GLSL decoder.
encoder
The TSL encoder.
Properties
.decoder : GLSLDecoder
The GLSL decoder. This component parse GLSL and produces a language-independent AST for further processing.
.encoder : TSLEncoder
The TSL encoder. It takes the AST and emits TSL code.
.linker : Linker
The linker. It processes the AST and resolves variable and function references, ensuring that all dependencies are properly linked.
Methods
.parse( source : string ) : string
Parses the given GLSL source and returns TSL syntax.
source
The GLSL source.
Returns: The TSL code.
Source
examples/jsm/transpiler/Transpiler.js
**File:** src/Three.WebGPU.js (L1-37)
```javascript
export * from './Three.Core.js';
export * from './materials/nodes/NodeMaterials.js';
export { default as WebGPURenderer } from './renderers/webgpu/WebGPURenderer.js';
export { default as WebGPUBackend } from './renderers/webgpu/WebGPUBackend.js';
export { default as WebGLBackend } from './renderers/webgl-fallback/WebGLBackend.js';
export { default as WebGLCapabilities } from './renderers/webgl-fallback/utils/WebGLCapabilities.js';
export { default as Lighting } from './renderers/common/Lighting.js';
export { default as BundleGroup } from './renderers/common/BundleGroup.js';
export { default as QuadMesh } from './renderers/common/QuadMesh.js';
export { default as PMREMGenerator } from './renderers/common/extras/PMREMGenerator.js';
export { default as RenderPipeline } from './renderers/common/RenderPipeline.js';
export { default as PostProcessing } from './renderers/common/PostProcessing.js';
export { default as ReadbackBuffer } from './renderers/common/ReadbackBuffer.js';
import * as RendererUtils from './renderers/common/RendererUtils.js';
export { RendererUtils };
export { default as CubeRenderTarget } from './renderers/common/CubeRenderTarget.js';
export { default as StorageTexture } from './renderers/common/StorageTexture.js';
export { default as Storage3DTexture } from './renderers/common/Storage3DTexture.js';
export { default as StorageArrayTexture } from './renderers/common/StorageArrayTexture.js';
export { default as StorageBufferAttribute } from './renderers/common/StorageBufferAttribute.js';
export { default as StorageInstancedBufferAttribute } from './renderers/common/StorageInstancedBufferAttribute.js';
export { default as IndirectStorageBufferAttribute } from './renderers/common/IndirectStorageBufferAttribute.js';
export { default as IESSpotLight } from './lights/webgpu/IESSpotLight.js';
export { default as ProjectorLight } from './lights/webgpu/ProjectorLight.js';
export { default as NodeLoader } from './loaders/nodes/NodeLoader.js';
export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js';
export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js';
export { default as InspectorBase } from './renderers/common/InspectorBase.js';
export { default as CanvasTarget } from './renderers/common/CanvasTarget.js';
export { default as BlendMode } from './renderers/common/BlendMode.js';
export { default as GLSLNodeBuilder } from './renderers/webgl-fallback/nodes/GLSLNodeBuilder.js';
export { default as BasicNodeLibrary } from './renderers/webgpu/nodes/BasicNodeLibrary.js';
export { ClippingGroup } from './objects/ClippingGroup.js';
export * from './nodes/Nodes.js';
import * as TSL from './nodes/TSL.js';
export { TSL };

