@hology/webgpu-renderer 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,144 +1,144 @@
1
- # @hology/webgpu-renderer
2
-
3
- TypeScript-first WebGPU renderer scaffold aimed at teams migrating from an existing three.js codebase.
4
-
5
- The current slice keeps three scene/content classes as the authoring surface and adapts them into a small WGSL-first renderer:
6
-
7
- - `Mesh`
8
- - `SkinnedMesh`
9
- - `InstancedMesh`
10
- - `BatchedMesh`
11
- - `BufferGeometry` and `BufferAttribute`
12
- - `Texture` / `CanvasTexture` / `DataTexture`
13
- - `Material` via a custom `WGSLShaderMaterial`
14
-
15
- ## What this version does
16
-
17
- - Initializes a raw WebGPU device and canvas context.
18
- - Traverses a three `Scene`.
19
- - Skins `SkinnedMesh` content in the WGSL vertex stage from cached GPU bone palettes; CPU work is limited to one palette update per changed skeleton plus conservative bone-envelope culling.
20
- - Extracts standard meshes, instanced meshes, and batched meshes into a shared per-frame instance buffer.
21
- - Renders standard meshes, true instanced draws for `InstancedMesh`, and batched draws for `BatchedMesh` without per-draw object-uniform uploads.
22
- - Runs the frame through an explicit render graph with pass dependencies, resource reads/writes, and ordered execution.
23
- - Builds a Hi-Z depth pyramid with compute passes.
24
- - Uses compute to cull indexed opaque draws against the Hi-Z pyramid and write indirect indexed draw arguments on the GPU.
25
- - Temporally GPU-culls every instance in a `StaticDrawSet`, compacts visible source-instance indices before the static depth prepass, and builds its indirect instance counts without per-frame CPU visibility uploads.
26
- - Renders directional-light and point-light shadow maps.
27
- - Exposes a reusable `ComputeKernel` abstraction for general-purpose compute work.
28
- - Exposes both `render()` and `renderAsync()` so graph passes can opt into async `prepare()` work before execution.
29
- - Exposes `compile()` and `compileAsync()` so scenes can prewarm geometry, textures, and material pipelines before the first visible frame.
30
- - Can capture per-pass render-graph timings, with CPU timings everywhere and GPU timestamp-query timings when the device supports them.
31
- - Supports custom render-graph nodes, including fullscreen post-processing passes that can read the renderer's scene color target and write to a post-process target before presentation.
32
- - Exposes a higher-level `FrameGraphBuilder` so passes can be wired with named outputs and inputs instead of raw `GPUTextureView` plumbing.
33
- - Lets you write custom WGSL shaders and pass uniforms/textures through a material that plays the role of a WebGPU-era `ShaderMaterial`.
34
- - Supports sampled 2D, 2D-array, 3D, cube, compressed, and compressed-array texture uploads, including KTX2-friendly `CompressedTexture`, `CompressedTextureArray`, `CompressedCubeTexture`, `DataArrayTexture`, and `Data3DTexture` paths.
35
- - Ships with a demo scene that proves the pipeline works with `Mesh`, `BufferAttribute`, `Texture`, `InstancedMesh`, `BatchedMesh`, and dynamic shadows.
36
-
37
- ## What this version does not do yet
38
-
39
- - Built-in PBR or lighting materials.
40
- - GPU-driven culling for transparent draws or non-indexed draws. The current visibility path targets indexed opaque draws first.
41
- - GPU compaction into a single `multi_draw_indirect` style stream. The renderer writes one indirect indexed argument record per extracted indexed draw and then submits them individually.
42
- - Full GPU-driven aggregation for mutable `BatchedMesh` content. Dynamic batches still use the CPU extraction path; immutable batches can use `StaticDrawSet` for per-instance GPU culling and compaction.
43
- - Cascaded directional shadows, clustered lighting, point-light arrays, shadow atlases, or any of the bigger AAA-scale scheduling systems yet.
44
- - Storage textures, external textures, and the more specialized renderer-owned texture flows from three's node/WebGPU stack.
45
- - Post-processing, temporal anti-aliasing/upscaling, terrain, particles, or material-template systems.
46
-
47
- ## Install and run
48
-
49
- ```bash
50
- npm install
51
- npm run dev
52
- ```
53
-
54
- Useful scripts:
55
-
56
- - `npm run build`
57
- - `npm run typecheck`
58
- - `npm run demo:build`
59
-
60
- ## Public API
61
-
62
- ```ts
63
- import {
64
- FrameGraphBuilder,
65
- Mesh,
66
- Scene,
67
- PerspectiveCamera,
68
- BoxGeometry,
69
- WGSLShaderMaterial,
70
- WebGpuRenderer,
71
- } from "@hology/webgpu-renderer";
72
-
73
- const renderer = new WebGpuRenderer({ canvas });
74
- await renderer.initialize();
75
-
76
- const material = new WGSLShaderMaterial({
77
- vertexShader: `...`,
78
- fragmentShader: `...`,
79
- uniforms: {
80
- time: { type: "f32", value: 0 },
81
- },
82
- });
83
-
84
- const mesh = new Mesh(new BoxGeometry(1, 1, 1), material);
85
- const scene = new Scene();
86
- scene.add(mesh);
87
-
88
- const camera = new PerspectiveCamera(50, canvas.width / canvas.height, 0.1, 100);
89
- camera.position.z = 4;
90
-
91
- renderer.render(scene, camera);
92
-
93
- await renderer.renderAsync(scene, camera);
94
-
95
- renderer.compile(scene, camera);
96
- await renderer.compileAsync(scene, camera);
97
- ```
98
-
99
- `compile()` is synchronous and assumes `initialize()` has already completed. `compileAsync()` will initialize on demand if needed and uses async pipeline compilation for WGSL materials.
100
-
101
- For incremental migrations, a renderer-wide fallback can draw meshes whose source
102
- materials have not been ported yet. The source materials remain untouched, and
103
- native `WGSLShaderMaterial` instances still take precedence:
104
-
105
- ```ts
106
- const renderer = new WebGpuRenderer({
107
- canvas,
108
- fallbackMaterial: diagnosticMaterial,
109
- // Disable this for scenes whose callbacks require THREE.WebGLRenderer APIs.
110
- invokeObjectRenderCallbacks: false,
111
- });
112
- ```
113
-
114
- The fallback applies to regular, instanced, and batched mesh extraction, including
115
- static draw sets. For imported `THREE.SkinnedMesh` objects, the renderer instead
116
- uses its built-in skin-aware lit fallback automatically, so standard Three.js
117
- materials and their skin attributes work without a WGSL material per submesh. Set
118
- `fallbackSkinnedMaterial` when a project needs a custom GPU-skinned appearance.
119
- Batched meshes from both the Three.js r169 `_drawInfo` layout and newer
120
- `_instanceInfo` layout are supported.
121
-
122
- Hi-Z occlusion culling is currently optional and defaults to off while the GPU visibility path is still settling:
123
-
124
- ```ts
125
- const renderer = new WebGpuRenderer({
126
- canvas,
127
- hiZOcclusionCulling: false,
128
- sampleCount: 1,
129
- renderGraphProfiling: true,
130
- });
131
-
132
- renderer.setHiZOcclusionCullingEnabled(true);
133
- renderer.setRenderGraphProfilingEnabled(true);
134
- ```
135
-
136
- Whether Hi-Z culling is actually active also depends on device support for WebGPU indirect draws with non-zero `firstInstance`:
137
-
138
- ```ts
139
- renderer.getHiZOcclusionCullingEnabled(); // requested
140
- renderer.getHiZOcclusionCullingActive(); // requested + supported on this device
141
- renderer.getLastFrameProfile(); // latest CPU/GPU pass timings
1
+ # @hology/webgpu-renderer
2
+
3
+ TypeScript-first WebGPU renderer scaffold aimed at teams migrating from an existing three.js codebase.
4
+
5
+ The current slice keeps three scene/content classes as the authoring surface and adapts them into a small WGSL-first renderer:
6
+
7
+ - `Mesh`
8
+ - `SkinnedMesh`
9
+ - `InstancedMesh`
10
+ - `BatchedMesh`
11
+ - `BufferGeometry` and `BufferAttribute`
12
+ - `Texture` / `CanvasTexture` / `DataTexture`
13
+ - `Material` via a custom `WGSLShaderMaterial`
14
+
15
+ ## What this version does
16
+
17
+ - Initializes a raw WebGPU device and canvas context.
18
+ - Traverses a three `Scene`.
19
+ - Skins `SkinnedMesh` content in the WGSL vertex stage from cached GPU bone palettes; CPU work is limited to one palette update per changed skeleton plus conservative bone-envelope culling.
20
+ - Extracts standard meshes, instanced meshes, and batched meshes into a shared per-frame instance buffer.
21
+ - Renders standard meshes, true instanced draws for `InstancedMesh`, and batched draws for `BatchedMesh` without per-draw object-uniform uploads.
22
+ - Runs the frame through an explicit render graph with pass dependencies, resource reads/writes, and ordered execution.
23
+ - Builds a Hi-Z depth pyramid with compute passes.
24
+ - Uses compute to cull indexed opaque draws against the Hi-Z pyramid and write indirect indexed draw arguments on the GPU.
25
+ - Temporally GPU-culls every instance in a `StaticDrawSet`, compacts visible source-instance indices before the static depth prepass, and builds its indirect instance counts without per-frame CPU visibility uploads.
26
+ - Renders directional-light and point-light shadow maps.
27
+ - Exposes a reusable `ComputeKernel` abstraction for general-purpose compute work.
28
+ - Exposes both `render()` and `renderAsync()` so graph passes can opt into async `prepare()` work before execution.
29
+ - Exposes `compile()` and `compileAsync()` so scenes can prewarm geometry, textures, and material pipelines before the first visible frame.
30
+ - Can capture per-pass render-graph timings, with CPU timings everywhere and GPU timestamp-query timings when the device supports them.
31
+ - Supports custom render-graph nodes, including fullscreen post-processing passes that can read the renderer's scene color target and write to a post-process target before presentation.
32
+ - Exposes a higher-level `FrameGraphBuilder` so passes can be wired with named outputs and inputs instead of raw `GPUTextureView` plumbing.
33
+ - Lets you write custom WGSL shaders and pass uniforms/textures through a material that plays the role of a WebGPU-era `ShaderMaterial`.
34
+ - Supports sampled 2D, 2D-array, 3D, cube, compressed, and compressed-array texture uploads, including KTX2-friendly `CompressedTexture`, `CompressedTextureArray`, `CompressedCubeTexture`, `DataArrayTexture`, and `Data3DTexture` paths.
35
+ - Ships with a demo scene that proves the pipeline works with `Mesh`, `BufferAttribute`, `Texture`, `InstancedMesh`, `BatchedMesh`, and dynamic shadows.
36
+
37
+ ## What this version does not do yet
38
+
39
+ - Built-in PBR or lighting materials.
40
+ - GPU-driven culling for transparent draws or non-indexed draws. The current visibility path targets indexed opaque draws first.
41
+ - GPU compaction into a single `multi_draw_indirect` style stream. The renderer writes one indirect indexed argument record per extracted indexed draw and then submits them individually.
42
+ - Full GPU-driven aggregation for mutable `BatchedMesh` content. Dynamic batches still use the CPU extraction path; immutable batches can use `StaticDrawSet` for per-instance GPU culling and compaction.
43
+ - Cascaded directional shadows, clustered lighting, point-light arrays, shadow atlases, or any of the bigger AAA-scale scheduling systems yet.
44
+ - Storage textures, external textures, and the more specialized renderer-owned texture flows from three's node/WebGPU stack.
45
+ - Post-processing, temporal anti-aliasing/upscaling, terrain, particles, or material-template systems.
46
+
47
+ ## Install and run
48
+
49
+ ```bash
50
+ npm install
51
+ npm run dev
52
+ ```
53
+
54
+ Useful scripts:
55
+
56
+ - `npm run build`
57
+ - `npm run typecheck`
58
+ - `npm run demo:build`
59
+
60
+ ## Public API
61
+
62
+ ```ts
63
+ import {
64
+ FrameGraphBuilder,
65
+ Mesh,
66
+ Scene,
67
+ PerspectiveCamera,
68
+ BoxGeometry,
69
+ WGSLShaderMaterial,
70
+ WebGpuRenderer,
71
+ } from "@hology/webgpu-renderer";
72
+
73
+ const renderer = new WebGpuRenderer({ canvas });
74
+ await renderer.initialize();
75
+
76
+ const material = new WGSLShaderMaterial({
77
+ vertexShader: `...`,
78
+ fragmentShader: `...`,
79
+ uniforms: {
80
+ time: { type: "f32", value: 0 },
81
+ },
82
+ });
83
+
84
+ const mesh = new Mesh(new BoxGeometry(1, 1, 1), material);
85
+ const scene = new Scene();
86
+ scene.add(mesh);
87
+
88
+ const camera = new PerspectiveCamera(50, canvas.width / canvas.height, 0.1, 100);
89
+ camera.position.z = 4;
90
+
91
+ renderer.render(scene, camera);
92
+
93
+ await renderer.renderAsync(scene, camera);
94
+
95
+ renderer.compile(scene, camera);
96
+ await renderer.compileAsync(scene, camera);
97
+ ```
98
+
99
+ `compile()` is synchronous and assumes `initialize()` has already completed. `compileAsync()` will initialize on demand if needed and uses async pipeline compilation for WGSL materials.
100
+
101
+ For incremental migrations, a renderer-wide fallback can draw meshes whose source
102
+ materials have not been ported yet. The source materials remain untouched, and
103
+ native `WGSLShaderMaterial` instances still take precedence:
104
+
105
+ ```ts
106
+ const renderer = new WebGpuRenderer({
107
+ canvas,
108
+ fallbackMaterial: diagnosticMaterial,
109
+ // Disable this for scenes whose callbacks require THREE.WebGLRenderer APIs.
110
+ invokeObjectRenderCallbacks: false,
111
+ });
112
+ ```
113
+
114
+ The fallback applies to regular, instanced, and batched mesh extraction, including
115
+ static draw sets. For imported `THREE.SkinnedMesh` objects, the renderer instead
116
+ uses its built-in skin-aware lit fallback automatically, so standard Three.js
117
+ materials and their skin attributes work without a WGSL material per submesh. Set
118
+ `fallbackSkinnedMaterial` when a project needs a custom GPU-skinned appearance.
119
+ Batched meshes from both the Three.js r169 `_drawInfo` layout and newer
120
+ `_instanceInfo` layout are supported.
121
+
122
+ Hi-Z occlusion culling is currently optional and defaults to off while the GPU visibility path is still settling:
123
+
124
+ ```ts
125
+ const renderer = new WebGpuRenderer({
126
+ canvas,
127
+ hiZOcclusionCulling: false,
128
+ sampleCount: 1,
129
+ renderGraphProfiling: true,
130
+ });
131
+
132
+ renderer.setHiZOcclusionCullingEnabled(true);
133
+ renderer.setRenderGraphProfilingEnabled(true);
134
+ ```
135
+
136
+ Whether Hi-Z culling is actually active also depends on device support for WebGPU indirect draws with non-zero `firstInstance`:
137
+
138
+ ```ts
139
+ renderer.getHiZOcclusionCullingEnabled(); // requested
140
+ renderer.getHiZOcclusionCullingActive(); // requested + supported on this device
141
+ renderer.getLastFrameProfile(); // latest CPU/GPU pass timings
142
142
  renderer.getSampleCount(); // current MSAA sample count
143
143
  ```
144
144
 
@@ -169,260 +169,260 @@ manual synchronization rule applies to directional and spot-light targets whose
169
169
  `matrixWorldAutoUpdate` has been disabled.
170
170
 
171
171
  ## Static GPU Visibility
172
-
173
- For immutable environment content, snapshot the authored Three objects once and remove
174
- the source root from the dynamic scene. The static path supports indexed opaque
175
- `Mesh`, `InstancedMesh`, and `BatchedMesh` content with `WGSLShaderMaterial`:
176
-
177
- ```ts
178
- await renderer.initialize();
179
-
180
- const staticEnvironment = renderer.createStaticDrawSet(environmentRoot, {
181
- label: "environment",
182
- });
183
- scene.remove(environmentRoot);
184
- ```
185
-
186
- With Hi-Z active, the renderer keeps frozen instance data and per-instance bounds
187
- on the GPU. It first culls against the previous frame's depth pyramid, compacts the
188
- survivors, and uses those indirect commands for the static depth prepass. It then
189
- builds the current pyramid and retests only the initial rejects before forward
190
- rendering, which keeps camera motion and newly exposed geometry correct. CPU work
191
- does not scale with the static set's instance count.
192
-
193
- The first frame after creation, resize, or toggling Hi-Z is deliberately
194
- frustum-only because there is no valid depth history yet. Static transforms,
195
- membership, geometry, materials, and visibility are frozen; dispose and recreate
196
- the set after changing any of those properties. Material uniforms and textures
197
- remain live.
198
-
199
- MSAA is currently disabled by default. You can enable it with:
200
-
201
- ```ts
202
- const renderer = new WebGpuRenderer({
203
- canvas,
204
- sampleCount: 4,
205
- });
206
- ```
207
-
208
- or update it later with:
209
-
210
- ```ts
211
- renderer.setSampleCount(4);
212
- ```
213
-
214
- This first slice applies MSAA to the built-in forward pass and resolves back into the normal scene color target, while the rest of the render graph remains single-sampled.
215
-
216
- Meshes can also provide `mesh.customDepthMaterial` using `WGSLShaderMaterial`. In this slice, that override is used for directional and spot shadows so vertex-deformed casters can render correctly into the shadow map. The shared depth prepass intentionally skips those meshes for now to keep the prepass on its fast shared pipeline.
217
-
218
- Optional renderer hooks:
219
-
220
- - `renderer.setRenderGraphBuilder((graph, context) => { ... })`
221
- - `renderer.setHiZOcclusionCullingEnabled(true | false)`
222
- - `renderer.getHiZOcclusionCullingActive()`
223
- - `renderer.setRenderGraphProfilingEnabled(true | false)`
224
- - `renderer.getLastFrameProfile()`
225
- - `renderer.createComputeKernel({ ... })`
226
-
227
- Inside `setRenderGraphBuilder(...)`, the context can now:
228
-
229
- - add the built-in shadow, depth, Hi-Z, visibility, forward, and present passes
230
- - add a custom pass via `context.addCustomPass({ ... })`
231
- - choose whether the present pass samples from `sceneColor` or `postProcessColor`
232
-
233
- For a simpler authoring layer, use `FrameGraphBuilder`:
234
-
235
- ```ts
236
- renderer.setRenderGraphBuilder((_graph, context) => {
237
- const frameGraph = new FrameGraphBuilder(context);
238
- const vignetteColor = frameGraph.createColorTarget("vignetteColor");
239
-
240
- frameGraph.addStandardScenePasses();
241
- frameGraph.addFullscreenPass({
242
- name: "VignettePostPass",
243
- inputs: {
244
- sceneColor: frameGraph.sceneColor,
245
- },
246
- outputs: vignetteColor,
247
- fragmentShader: `...`,
248
- });
249
- frameGraph.present(vignetteColor);
250
- });
251
- ```
252
-
253
- This keeps the internal renderer explicit, but lets normal usage feel closer to named render targets flowing from one pass to the next.
254
-
255
- ## Shader contract
256
-
257
- The renderer prepends a small WGSL header before your shaders. Your WGSL code is expected to provide `vsMain` and `fsMain`.
258
-
259
- For porting existing three.js `ShaderMaterial` code, see [Migrating Three ShaderMaterial Shaders](docs/shader-material-migration.md). For material shaders that write multiple render targets, see [Multiple Render Targets](docs/multiple-render-targets.md). `WGSLShaderMaterial` accepts three-style uniform entries like `{ value }`, infers common WGSL types, and moves texture uniforms into texture bindings automatically.
260
-
261
- `WGSLShaderMaterial` also runs a small preprocessor before WebGPU compilation. You can use `defines` plus common GLSL-style directives such as `#define`, `#ifdef`, `#ifndef`, `#if`, `#elif`, `#else`, and `#endif` in WGSL shader strings:
262
-
263
- ```ts
264
- const material = new WGSLShaderMaterial({
265
- defines: {
266
- USE_BATCH_PARAMS: true,
267
- MAX_LIGHT_STEPS: 8,
268
- },
269
- vertexShader,
270
- fragmentShader,
271
- });
272
- ```
273
-
274
- The preprocessor is a migration helper, not native WGSL. It supports conditional compilation and simple object-like/function-like macro expansion, but it does not provide three's GLSL chunks or convert GLSL syntax to WGSL.
275
-
276
- The renderer also provides a small compatibility define set by default:
277
-
278
- - `HIGH_PRECISION`
279
- - `WEBGPU_RENDERER`
280
- - `WGSL_SHADER_MATERIAL`
281
- - `USE_BATCHING`
282
- - `USE_COLOR`
283
- - `USE_COLOR_ALPHA`
284
- - `USE_SHADOWMAP` when compiling a shadow-receiving forward variant
285
- - `DOUBLE_SIDED` or `FLIP_SIDED` from `material.side`
286
- - texture-name defines such as `USE_MAP`, `USE_NORMALMAP`, and `USE_ALPHAMAP` when matching texture bindings exist
287
- - light-count constants such as `NUM_DIR_LIGHTS`, `NUM_POINT_LIGHTS`, and `NUM_SPOT_LIGHTS`
288
-
289
- You can disable a default compatibility define per material by setting it to `false` in `defines`.
290
-
291
- Available frame resources in every shader:
292
-
293
- ```wgsl
294
- struct RendererCameraUniforms {
295
- viewProj: mat4x4<f32>,
296
- cameraPosition: vec4<f32>,
297
- viewportAndTime: vec4<f32>,
298
- view: mat4x4<f32>,
299
- projection: mat4x4<f32>,
300
- };
301
- ```
302
-
303
- Also available:
304
-
305
- - `rendererElapsedTime()` — the `timeSeconds` argument passed to `render()`, in seconds
306
- - `rendererWorldPosition(localPosition, instanceIndex)`
307
- - `rendererClipPosition(localPosition, instanceIndex)`
308
- - `rendererWorldNormal(localNormal, instanceIndex)`
309
- - `rendererObjectColor(instanceIndex)`
310
- - `rendererBatchingId(instanceIndex)`
311
- - `modelMatrix(instanceIndex)`
312
- - `modelViewMatrix(instanceIndex)`
313
- - `projectionMatrix()`
314
- - `normalMatrix(instanceIndex)`
315
- - `cameraPosition()`
316
- - `rendererDirectionalLightDirection()`
317
- - `rendererDirectionalLightColor()`
318
- - `rendererPointLightPosition()`
319
- - `rendererPointLightRange()`
320
- - `rendererPointLightColor()`
321
- - `rendererAmbientLight()`
322
- - `rendererSampleDirectionalShadow(worldPosition)`
323
- - `rendererSamplePointShadow(worldPosition)`
324
-
325
- If you define material uniforms, they are exposed as:
326
-
327
- ```wgsl
328
- @group(1) @binding(0) var<uniform> materialUniforms: MaterialUniforms;
329
- ```
330
-
331
- For example, a uniform named `time` is read as `materialUniforms.time` in WGSL rather than as a global `time` variable.
332
-
333
- For renderer-wide animation time, prefer `rendererElapsedTime()` over a material
334
- `time` uniform. The renderer uploads it once per frame through the shared scene
335
- or shadow-camera bind group, so it works for forward rendering, depth/shadow
336
- passes, and compatible `StaticDrawSet` render bundles without per-material
337
- uniform uploads or bundle rebuilds.
338
-
339
- For `BatchedMesh`, `rendererBatchingId(instanceIndex)` returns the original three.js batched instance id from `BatchedMesh.addInstance(...)`. This is the migration replacement for shaders that used three's batching id path to look up additional per-batch data from a custom texture or buffer.
340
-
341
- If you define textures like `{ albedo: texture }`, they are exposed with the correct WGSL texture type for the bound three.js texture. A regular 2D texture looks like:
342
-
343
- ```wgsl
344
- @group(1) @binding(1) var albedoSampler: sampler;
345
- @group(1) @binding(2) var albedoTexture: texture_2d<f32>;
346
- ```
347
-
348
- For array, cube, 3D, integer, or depth textures, the declaration changes automatically to the matching WGSL type such as `texture_2d_array<f32>`, `texture_cube<f32>`, or `texture_3d<f32>`.
349
-
350
- Vertex attribute locations are fixed for this first slice:
351
-
352
- - `@location(0)` position `vec3<f32>`
353
- - `@location(1)` normal `vec3<f32>`
354
- - `@location(2)` uv `vec2<f32>`
355
- - `@location(3)` color `vec4<f32>`
356
- - `@builtin(instance_index)` instance index for shared instance-buffer lookups
357
-
358
- Missing geometry attributes are auto-filled with defaults.
359
-
360
- ### GPU skinning contract
361
-
362
- `SkinnedMesh` no longer has a CPU-skinned fallback. Imported Three.js meshes use
363
- the renderer's built-in skin-aware fallback material automatically. For custom
364
- WGSL, the renderer sets `USE_SKINNING` from the draw object and supplies the two
365
- GPU vertex streams. The material only needs the conditional shader path; no
366
- editor-facing skinning setting is required:
367
-
368
- ```wgsl
369
- struct VertexInput {
370
- @location(0) position: vec3<f32>,
371
- @location(1) normal: vec3<f32>,
372
- #ifdef USE_SKINNING
373
- @location(4) skinIndex: vec4<u32>,
374
- @location(5) skinWeight: vec4<f32>,
375
- #endif
376
- @builtin(instance_index) instanceIndex: u32,
377
- };
378
-
379
- #ifdef USE_SKINNING
380
- let skinnedPosition = rendererSkinPosition(input.position, input.skinIndex, input.skinWeight);
381
- let skinnedNormal = rendererSkinNormal(input.normal, input.skinIndex, input.skinWeight);
382
- #else
383
- let skinnedPosition = input.position;
384
- let skinnedNormal = input.normal;
385
- #endif
386
- output.position = rendererClipPosition(skinnedPosition, input.instanceIndex);
387
- output.worldNormal = rendererWorldNormal(skinnedNormal, input.instanceIndex);
388
- ```
389
-
390
- The renderer shares one GPU palette per `THREE.Skeleton`, uploads it only when its bone world matrices or inverses change, and retains a per-mesh bind-matrix uniform. It also culls against a conservative envelope built from each bone's influenced vertices before a palette upload is needed.
391
-
392
- ## Demo
393
-
394
- The demo is wired directly to the source library and shows:
395
-
396
- - a regular `Mesh` using a custom `BufferAttribute` color stream
397
- - a procedural `SkinnedMesh` example with animated bones
398
- - a realistic 20-character X Bot walking crowd loaded from the supplied FBX mesh and animation
399
- - a shared texture sampled from WGSL
400
- - animated material uniforms
401
- - an `InstancedMesh`
402
- - a `BatchedMesh`
403
- - a shadow-casting directional light
404
- - a shadow-casting point light
405
- - a render graph that includes depth, Hi-Z, visibility, and forward passes
406
- - a live HUD that visualizes per-pass frame timings
407
- - an example picker with both a baseline scene and a custom vignette post-processing example
408
- - one example module per file under `demo/examples`, so new demos can be added without growing `demo/main.ts`
409
-
410
- ## Structure
411
-
412
- - `src/materials`: WGSL material and uniform packing
413
- - `src/renderer`: WebGPU renderer core, extraction, pass execution, and shader header generation
414
- - `src/rendergraph`: frame graph primitives
415
- - `src/compute`: reusable compute pipeline wrapper
416
- - `demo/examples`: one file per selectable example, plus shared demo scene helpers
417
- - `demo/ui`: small reusable demo UI helpers like the profiling HUD
418
- - `demo`: Vite demo app shell using the library exports directly
419
-
420
- ## Next steps
421
-
422
- Natural follow-ups from here:
423
-
424
- - add a three bridge layer explicitly named and documented as the migration boundary
425
- - compact visible draws into fewer GPU-driven submission streams and reduce CPU draw iteration further
426
- - add cascaded directional shadows, light arrays, and shadow atlases
427
- - add material templates and pipeline caches for larger shader families
428
- - separate extraction, render graph nodes, and resource registries into dedicated modules
172
+
173
+ For immutable environment content, snapshot the authored Three objects once and remove
174
+ the source root from the dynamic scene. The static path supports indexed opaque
175
+ `Mesh`, `InstancedMesh`, and `BatchedMesh` content with `WGSLShaderMaterial`:
176
+
177
+ ```ts
178
+ await renderer.initialize();
179
+
180
+ const staticEnvironment = renderer.createStaticDrawSet(environmentRoot, {
181
+ label: "environment",
182
+ });
183
+ scene.remove(environmentRoot);
184
+ ```
185
+
186
+ With Hi-Z active, the renderer keeps frozen instance data and per-instance bounds
187
+ on the GPU. It first culls against the previous frame's depth pyramid, compacts the
188
+ survivors, and uses those indirect commands for the static depth prepass. It then
189
+ builds the current pyramid and retests only the initial rejects before forward
190
+ rendering, which keeps camera motion and newly exposed geometry correct. CPU work
191
+ does not scale with the static set's instance count.
192
+
193
+ The first frame after creation, resize, or toggling Hi-Z is deliberately
194
+ frustum-only because there is no valid depth history yet. Static transforms,
195
+ membership, geometry, materials, and visibility are frozen; dispose and recreate
196
+ the set after changing any of those properties. Material uniforms and textures
197
+ remain live.
198
+
199
+ MSAA is currently disabled by default. You can enable it with:
200
+
201
+ ```ts
202
+ const renderer = new WebGpuRenderer({
203
+ canvas,
204
+ sampleCount: 4,
205
+ });
206
+ ```
207
+
208
+ or update it later with:
209
+
210
+ ```ts
211
+ renderer.setSampleCount(4);
212
+ ```
213
+
214
+ This first slice applies MSAA to the built-in forward pass and resolves back into the normal scene color target, while the rest of the render graph remains single-sampled.
215
+
216
+ Meshes can also provide `mesh.customDepthMaterial` using `WGSLShaderMaterial`. In this slice, that override is used for directional and spot shadows so vertex-deformed casters can render correctly into the shadow map. The shared depth prepass intentionally skips those meshes for now to keep the prepass on its fast shared pipeline.
217
+
218
+ Optional renderer hooks:
219
+
220
+ - `renderer.setRenderGraphBuilder((graph, context) => { ... })`
221
+ - `renderer.setHiZOcclusionCullingEnabled(true | false)`
222
+ - `renderer.getHiZOcclusionCullingActive()`
223
+ - `renderer.setRenderGraphProfilingEnabled(true | false)`
224
+ - `renderer.getLastFrameProfile()`
225
+ - `renderer.createComputeKernel({ ... })`
226
+
227
+ Inside `setRenderGraphBuilder(...)`, the context can now:
228
+
229
+ - add the built-in shadow, depth, Hi-Z, visibility, forward, and present passes
230
+ - add a custom pass via `context.addCustomPass({ ... })`
231
+ - choose whether the present pass samples from `sceneColor` or `postProcessColor`
232
+
233
+ For a simpler authoring layer, use `FrameGraphBuilder`:
234
+
235
+ ```ts
236
+ renderer.setRenderGraphBuilder((_graph, context) => {
237
+ const frameGraph = new FrameGraphBuilder(context);
238
+ const vignetteColor = frameGraph.createColorTarget("vignetteColor");
239
+
240
+ frameGraph.addStandardScenePasses();
241
+ frameGraph.addFullscreenPass({
242
+ name: "VignettePostPass",
243
+ inputs: {
244
+ sceneColor: frameGraph.sceneColor,
245
+ },
246
+ outputs: vignetteColor,
247
+ fragmentShader: `...`,
248
+ });
249
+ frameGraph.present(vignetteColor);
250
+ });
251
+ ```
252
+
253
+ This keeps the internal renderer explicit, but lets normal usage feel closer to named render targets flowing from one pass to the next.
254
+
255
+ ## Shader contract
256
+
257
+ The renderer prepends a small WGSL header before your shaders. Your WGSL code is expected to provide `vsMain` and `fsMain`.
258
+
259
+ For porting existing three.js `ShaderMaterial` code, see [Migrating Three ShaderMaterial Shaders](docs/shader-material-migration.md). For material shaders that write multiple render targets, see [Multiple Render Targets](docs/multiple-render-targets.md). `WGSLShaderMaterial` accepts three-style uniform entries like `{ value }`, infers common WGSL types, and moves texture uniforms into texture bindings automatically.
260
+
261
+ `WGSLShaderMaterial` also runs a small preprocessor before WebGPU compilation. You can use `defines` plus common GLSL-style directives such as `#define`, `#ifdef`, `#ifndef`, `#if`, `#elif`, `#else`, and `#endif` in WGSL shader strings:
262
+
263
+ ```ts
264
+ const material = new WGSLShaderMaterial({
265
+ defines: {
266
+ USE_BATCH_PARAMS: true,
267
+ MAX_LIGHT_STEPS: 8,
268
+ },
269
+ vertexShader,
270
+ fragmentShader,
271
+ });
272
+ ```
273
+
274
+ The preprocessor is a migration helper, not native WGSL. It supports conditional compilation and simple object-like/function-like macro expansion, but it does not provide three's GLSL chunks or convert GLSL syntax to WGSL.
275
+
276
+ The renderer also provides a small compatibility define set by default:
277
+
278
+ - `HIGH_PRECISION`
279
+ - `WEBGPU_RENDERER`
280
+ - `WGSL_SHADER_MATERIAL`
281
+ - `USE_BATCHING`
282
+ - `USE_COLOR`
283
+ - `USE_COLOR_ALPHA`
284
+ - `USE_SHADOWMAP` when compiling a shadow-receiving forward variant
285
+ - `DOUBLE_SIDED` or `FLIP_SIDED` from `material.side`
286
+ - texture-name defines such as `USE_MAP`, `USE_NORMALMAP`, and `USE_ALPHAMAP` when matching texture bindings exist
287
+ - light-count constants such as `NUM_DIR_LIGHTS`, `NUM_POINT_LIGHTS`, and `NUM_SPOT_LIGHTS`
288
+
289
+ You can disable a default compatibility define per material by setting it to `false` in `defines`.
290
+
291
+ Available frame resources in every shader:
292
+
293
+ ```wgsl
294
+ struct RendererCameraUniforms {
295
+ viewProj: mat4x4<f32>,
296
+ cameraPosition: vec4<f32>,
297
+ viewportAndTime: vec4<f32>,
298
+ view: mat4x4<f32>,
299
+ projection: mat4x4<f32>,
300
+ };
301
+ ```
302
+
303
+ Also available:
304
+
305
+ - `rendererElapsedTime()` — the `timeSeconds` argument passed to `render()`, in seconds
306
+ - `rendererWorldPosition(localPosition, instanceIndex)`
307
+ - `rendererClipPosition(localPosition, instanceIndex)`
308
+ - `rendererWorldNormal(localNormal, instanceIndex)`
309
+ - `rendererObjectColor(instanceIndex)`
310
+ - `rendererBatchingId(instanceIndex)`
311
+ - `modelMatrix(instanceIndex)`
312
+ - `modelViewMatrix(instanceIndex)`
313
+ - `projectionMatrix()`
314
+ - `normalMatrix(instanceIndex)`
315
+ - `cameraPosition()`
316
+ - `rendererDirectionalLightDirection()`
317
+ - `rendererDirectionalLightColor()`
318
+ - `rendererPointLightPosition()`
319
+ - `rendererPointLightRange()`
320
+ - `rendererPointLightColor()`
321
+ - `rendererAmbientLight()`
322
+ - `rendererSampleDirectionalShadow(worldPosition)`
323
+ - `rendererSamplePointShadow(worldPosition)`
324
+
325
+ If you define material uniforms, they are exposed as:
326
+
327
+ ```wgsl
328
+ @group(1) @binding(0) var<uniform> materialUniforms: MaterialUniforms;
329
+ ```
330
+
331
+ For example, a uniform named `time` is read as `materialUniforms.time` in WGSL rather than as a global `time` variable.
332
+
333
+ For renderer-wide animation time, prefer `rendererElapsedTime()` over a material
334
+ `time` uniform. The renderer uploads it once per frame through the shared scene
335
+ or shadow-camera bind group, so it works for forward rendering, depth/shadow
336
+ passes, and compatible `StaticDrawSet` render bundles without per-material
337
+ uniform uploads or bundle rebuilds.
338
+
339
+ For `BatchedMesh`, `rendererBatchingId(instanceIndex)` returns the original three.js batched instance id from `BatchedMesh.addInstance(...)`. This is the migration replacement for shaders that used three's batching id path to look up additional per-batch data from a custom texture or buffer.
340
+
341
+ If you define textures like `{ albedo: texture }`, they are exposed with the correct WGSL texture type for the bound three.js texture. A regular 2D texture looks like:
342
+
343
+ ```wgsl
344
+ @group(1) @binding(1) var albedoSampler: sampler;
345
+ @group(1) @binding(2) var albedoTexture: texture_2d<f32>;
346
+ ```
347
+
348
+ For array, cube, 3D, integer, or depth textures, the declaration changes automatically to the matching WGSL type such as `texture_2d_array<f32>`, `texture_cube<f32>`, or `texture_3d<f32>`.
349
+
350
+ Vertex attribute locations are fixed for this first slice:
351
+
352
+ - `@location(0)` position `vec3<f32>`
353
+ - `@location(1)` normal `vec3<f32>`
354
+ - `@location(2)` uv `vec2<f32>`
355
+ - `@location(3)` color `vec4<f32>`
356
+ - `@builtin(instance_index)` instance index for shared instance-buffer lookups
357
+
358
+ Missing geometry attributes are auto-filled with defaults.
359
+
360
+ ### GPU skinning contract
361
+
362
+ `SkinnedMesh` no longer has a CPU-skinned fallback. Imported Three.js meshes use
363
+ the renderer's built-in skin-aware fallback material automatically. For custom
364
+ WGSL, the renderer sets `USE_SKINNING` from the draw object and supplies the two
365
+ GPU vertex streams. The material only needs the conditional shader path; no
366
+ editor-facing skinning setting is required:
367
+
368
+ ```wgsl
369
+ struct VertexInput {
370
+ @location(0) position: vec3<f32>,
371
+ @location(1) normal: vec3<f32>,
372
+ #ifdef USE_SKINNING
373
+ @location(4) skinIndex: vec4<u32>,
374
+ @location(5) skinWeight: vec4<f32>,
375
+ #endif
376
+ @builtin(instance_index) instanceIndex: u32,
377
+ };
378
+
379
+ #ifdef USE_SKINNING
380
+ let skinnedPosition = rendererSkinPosition(input.position, input.skinIndex, input.skinWeight);
381
+ let skinnedNormal = rendererSkinNormal(input.normal, input.skinIndex, input.skinWeight);
382
+ #else
383
+ let skinnedPosition = input.position;
384
+ let skinnedNormal = input.normal;
385
+ #endif
386
+ output.position = rendererClipPosition(skinnedPosition, input.instanceIndex);
387
+ output.worldNormal = rendererWorldNormal(skinnedNormal, input.instanceIndex);
388
+ ```
389
+
390
+ The renderer shares one GPU palette per `THREE.Skeleton`, uploads it only when its bone world matrices or inverses change, and retains a per-mesh bind-matrix uniform. It also culls against a conservative envelope built from each bone's influenced vertices before a palette upload is needed.
391
+
392
+ ## Demo
393
+
394
+ The demo is wired directly to the source library and shows:
395
+
396
+ - a regular `Mesh` using a custom `BufferAttribute` color stream
397
+ - a procedural `SkinnedMesh` example with animated bones
398
+ - a realistic 20-character X Bot walking crowd loaded from the supplied FBX mesh and animation
399
+ - a shared texture sampled from WGSL
400
+ - animated material uniforms
401
+ - an `InstancedMesh`
402
+ - a `BatchedMesh`
403
+ - a shadow-casting directional light
404
+ - a shadow-casting point light
405
+ - a render graph that includes depth, Hi-Z, visibility, and forward passes
406
+ - a live HUD that visualizes per-pass frame timings
407
+ - an example picker with both a baseline scene and a custom vignette post-processing example
408
+ - one example module per file under `demo/examples`, so new demos can be added without growing `demo/main.ts`
409
+
410
+ ## Structure
411
+
412
+ - `src/materials`: WGSL material and uniform packing
413
+ - `src/renderer`: WebGPU renderer core, extraction, pass execution, and shader header generation
414
+ - `src/rendergraph`: frame graph primitives
415
+ - `src/compute`: reusable compute pipeline wrapper
416
+ - `demo/examples`: one file per selectable example, plus shared demo scene helpers
417
+ - `demo/ui`: small reusable demo UI helpers like the profiling HUD
418
+ - `demo`: Vite demo app shell using the library exports directly
419
+
420
+ ## Next steps
421
+
422
+ Natural follow-ups from here:
423
+
424
+ - add a three bridge layer explicitly named and documented as the migration boundary
425
+ - compact visible draws into fewer GPU-driven submission streams and reduce CPU draw iteration further
426
+ - add cascaded directional shadows, light arrays, and shadow atlases
427
+ - add material templates and pipeline caches for larger shader families
428
+ - separate extraction, render graph nodes, and resource registries into dedicated modules