@codexo/exojs 0.9.0 → 0.10.0

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.
Files changed (209) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +14 -14
  3. package/dist/esm/core/Application.d.ts +25 -6
  4. package/dist/esm/core/Application.js +42 -8
  5. package/dist/esm/core/Application.js.map +1 -1
  6. package/dist/esm/core/Perf.d.ts +23 -0
  7. package/dist/esm/core/Perf.js +49 -0
  8. package/dist/esm/core/Perf.js.map +1 -0
  9. package/dist/esm/core/Scene.d.ts +8 -8
  10. package/dist/esm/core/Scene.js +7 -7
  11. package/dist/esm/core/Scene.js.map +1 -1
  12. package/dist/esm/core/SceneManager.js +2 -2
  13. package/dist/esm/core/SceneManager.js.map +1 -1
  14. package/dist/esm/core/SceneNode.d.ts +0 -3
  15. package/dist/esm/core/SceneNode.js +0 -9
  16. package/dist/esm/core/SceneNode.js.map +1 -1
  17. package/dist/esm/core/capabilities.d.ts +2 -0
  18. package/dist/esm/core/capabilities.js +15 -0
  19. package/dist/esm/core/capabilities.js.map +1 -1
  20. package/dist/esm/core/index.d.ts +1 -0
  21. package/dist/esm/core/types.d.ts +1 -1
  22. package/dist/esm/core/utils.d.ts +12 -0
  23. package/dist/esm/core/utils.js +18 -1
  24. package/dist/esm/core/utils.js.map +1 -1
  25. package/dist/esm/index.js +14 -3
  26. package/dist/esm/index.js.map +1 -1
  27. package/dist/esm/particles/ParticleSystem.d.ts +8 -5
  28. package/dist/esm/particles/ParticleSystem.js +9 -5
  29. package/dist/esm/particles/ParticleSystem.js.map +1 -1
  30. package/dist/esm/particles/distributions/{Gradient.d.ts → ColorGradient.d.ts} +5 -5
  31. package/dist/esm/particles/distributions/{Gradient.js → ColorGradient.js} +5 -5
  32. package/dist/esm/particles/distributions/ColorGradient.js.map +1 -0
  33. package/dist/esm/particles/distributions/Distribution.d.ts +2 -2
  34. package/dist/esm/particles/distributions/index.d.ts +2 -2
  35. package/dist/esm/particles/gpu/ParticleGpuState.js +1 -1
  36. package/dist/esm/particles/modules/ColorOverLifetime.d.ts +3 -3
  37. package/dist/esm/particles/modules/ColorOverLifetime.js.map +1 -1
  38. package/dist/esm/particles/modules/ColorOverSpeed.d.ts +3 -3
  39. package/dist/esm/particles/modules/ColorOverSpeed.js.map +1 -1
  40. package/dist/esm/particles/modules/UpdateModule.d.ts +2 -2
  41. package/dist/esm/particles/modules/UpdateModule.js +1 -1
  42. package/dist/esm/particles/modules/WgslContribution.d.ts +2 -2
  43. package/dist/esm/rendering/Camera.d.ts +33 -0
  44. package/dist/esm/rendering/Camera.js +38 -0
  45. package/dist/esm/rendering/Camera.js.map +1 -0
  46. package/dist/esm/rendering/Container.d.ts +5 -24
  47. package/dist/esm/rendering/Container.js +8 -71
  48. package/dist/esm/rendering/Container.js.map +1 -1
  49. package/dist/esm/rendering/Drawable.d.ts +8 -10
  50. package/dist/esm/rendering/Drawable.js +12 -20
  51. package/dist/esm/rendering/Drawable.js.map +1 -1
  52. package/dist/esm/rendering/RenderBackend.d.ts +18 -0
  53. package/dist/esm/rendering/RenderNode.d.ts +81 -8
  54. package/dist/esm/rendering/RenderNode.js +121 -144
  55. package/dist/esm/rendering/RenderNode.js.map +1 -1
  56. package/dist/esm/rendering/RenderTarget.d.ts +13 -0
  57. package/dist/esm/rendering/RenderTarget.js +13 -0
  58. package/dist/esm/rendering/RenderTarget.js.map +1 -1
  59. package/dist/esm/rendering/RenderTargetPass.js +17 -0
  60. package/dist/esm/rendering/RenderTargetPass.js.map +1 -1
  61. package/dist/esm/rendering/RenderingContext.d.ts +87 -0
  62. package/dist/esm/rendering/RenderingContext.js +157 -0
  63. package/dist/esm/rendering/RenderingContext.js.map +1 -0
  64. package/dist/esm/rendering/TransformBuffer.d.ts +38 -0
  65. package/dist/esm/rendering/TransformBuffer.js +116 -0
  66. package/dist/esm/rendering/TransformBuffer.js.map +1 -0
  67. package/dist/esm/rendering/filters/WebGpuShaderFilter.js +5 -12
  68. package/dist/esm/rendering/filters/WebGpuShaderFilter.js.map +1 -1
  69. package/dist/esm/rendering/geometry/Geometry.d.ts +40 -0
  70. package/dist/esm/rendering/geometry/Geometry.js +228 -0
  71. package/dist/esm/rendering/geometry/Geometry.js.map +1 -0
  72. package/dist/esm/rendering/geometry/GeometryAttribute.d.ts +32 -0
  73. package/dist/esm/rendering/geometry/QuadGeometry.d.ts +5 -0
  74. package/dist/esm/rendering/gradient/Gradient.d.ts +34 -0
  75. package/dist/esm/rendering/gradient/Gradient.js +114 -0
  76. package/dist/esm/rendering/gradient/Gradient.js.map +1 -0
  77. package/dist/esm/rendering/gradient/LinearGradient.d.ts +10 -0
  78. package/dist/esm/rendering/gradient/LinearGradient.js +26 -0
  79. package/dist/esm/rendering/gradient/LinearGradient.js.map +1 -0
  80. package/dist/esm/rendering/gradient/RadialGradient.d.ts +10 -0
  81. package/dist/esm/rendering/gradient/RadialGradient.js +25 -0
  82. package/dist/esm/rendering/gradient/RadialGradient.js.map +1 -0
  83. package/dist/esm/rendering/index.d.ts +16 -2
  84. package/dist/esm/rendering/material/Material.d.ts +114 -0
  85. package/dist/esm/rendering/material/Material.js +111 -0
  86. package/dist/esm/rendering/material/Material.js.map +1 -0
  87. package/dist/esm/rendering/material/MaterialKey.d.ts +18 -0
  88. package/dist/esm/rendering/material/MaterialKey.js +82 -0
  89. package/dist/esm/rendering/material/MaterialKey.js.map +1 -0
  90. package/dist/esm/rendering/material/MeshMaterial.d.ts +16 -0
  91. package/dist/esm/rendering/material/MeshMaterial.js +21 -0
  92. package/dist/esm/rendering/material/MeshMaterial.js.map +1 -0
  93. package/dist/esm/rendering/{mesh/MeshShader.d.ts → material/ShaderSource.d.ts} +29 -62
  94. package/dist/esm/rendering/{mesh/MeshShader.js → material/ShaderSource.js} +35 -62
  95. package/dist/esm/rendering/material/ShaderSource.js.map +1 -0
  96. package/dist/esm/rendering/material/SpriteMaterial.d.ts +15 -0
  97. package/dist/esm/rendering/material/SpriteMaterial.js +20 -0
  98. package/dist/esm/rendering/material/SpriteMaterial.js.map +1 -0
  99. package/dist/esm/rendering/mesh/Mesh.d.ts +29 -12
  100. package/dist/esm/rendering/mesh/Mesh.js +122 -3
  101. package/dist/esm/rendering/mesh/Mesh.js.map +1 -1
  102. package/dist/esm/rendering/pass/RenderPassCoordinator.d.ts +63 -0
  103. package/dist/esm/rendering/pass/RenderPassDescriptor.d.ts +48 -0
  104. package/dist/esm/rendering/pass/RenderPassDescriptor.js +16 -0
  105. package/dist/esm/rendering/pass/RenderPassDescriptor.js.map +1 -0
  106. package/dist/esm/rendering/plan/RenderCommand.d.ts +67 -0
  107. package/dist/esm/rendering/plan/RenderCommand.js +94 -0
  108. package/dist/esm/rendering/plan/RenderCommand.js.map +1 -0
  109. package/dist/esm/rendering/plan/RenderEffectExecutor.d.ts +10 -0
  110. package/dist/esm/rendering/plan/RenderEffectExecutor.js +159 -0
  111. package/dist/esm/rendering/plan/RenderEffectExecutor.js.map +1 -0
  112. package/dist/esm/rendering/plan/RenderPlan.d.ts +23 -0
  113. package/dist/esm/rendering/plan/RenderPlan.js +12 -0
  114. package/dist/esm/rendering/plan/RenderPlan.js.map +1 -0
  115. package/dist/esm/rendering/plan/RenderPlanBuilder.d.ts +31 -0
  116. package/dist/esm/rendering/plan/RenderPlanBuilder.js +242 -0
  117. package/dist/esm/rendering/plan/RenderPlanBuilder.js.map +1 -0
  118. package/dist/esm/rendering/plan/RenderPlanOptimizer.d.ts +10 -0
  119. package/dist/esm/rendering/plan/RenderPlanOptimizer.js +180 -0
  120. package/dist/esm/rendering/plan/RenderPlanOptimizer.js.map +1 -0
  121. package/dist/esm/rendering/plan/RenderPlanPlayer.d.ts +9 -0
  122. package/dist/esm/rendering/plan/RenderPlanPlayer.js +56 -0
  123. package/dist/esm/rendering/plan/RenderPlanPlayer.js.map +1 -0
  124. package/dist/esm/rendering/plan/RenderScope.d.ts +70 -0
  125. package/dist/esm/rendering/plan/RenderScope.js +16 -0
  126. package/dist/esm/rendering/plan/RenderScope.js.map +1 -0
  127. package/dist/esm/rendering/plan/playRenderTree.d.ts +4 -0
  128. package/dist/esm/rendering/plan/playRenderTree.js +19 -0
  129. package/dist/esm/rendering/plan/playRenderTree.js.map +1 -0
  130. package/dist/esm/rendering/sprite/Sprite.d.ts +22 -1
  131. package/dist/esm/rendering/sprite/Sprite.js +33 -2
  132. package/dist/esm/rendering/sprite/Sprite.js.map +1 -1
  133. package/dist/esm/rendering/sprite/spriteMaterialSources.d.ts +36 -0
  134. package/dist/esm/rendering/sprite/spriteMaterialSources.js +128 -0
  135. package/dist/esm/rendering/sprite/spriteMaterialSources.js.map +1 -0
  136. package/dist/esm/rendering/text/TextStyle.d.ts +1 -1
  137. package/dist/esm/rendering/texture/DataTexture.d.ts +5 -0
  138. package/dist/esm/rendering/texture/DataTexture.js +7 -0
  139. package/dist/esm/rendering/texture/DataTexture.js.map +1 -1
  140. package/dist/esm/rendering/video/Video.d.ts +3 -7
  141. package/dist/esm/rendering/video/Video.js +3 -8
  142. package/dist/esm/rendering/video/Video.js.map +1 -1
  143. package/dist/esm/rendering/webgl2/WebGl2Backend.d.ts +40 -0
  144. package/dist/esm/rendering/webgl2/WebGl2Backend.js +303 -22
  145. package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
  146. package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.d.ts +22 -2
  147. package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js +404 -112
  148. package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js.map +1 -1
  149. package/dist/esm/rendering/webgl2/WebGl2PassCoordinator.d.ts +57 -0
  150. package/dist/esm/rendering/webgl2/WebGl2PassCoordinator.js +79 -0
  151. package/dist/esm/rendering/webgl2/WebGl2PassCoordinator.js.map +1 -0
  152. package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.d.ts +12 -0
  153. package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js +214 -58
  154. package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js.map +1 -1
  155. package/dist/esm/rendering/webgl2/WebGl2StencilClipper.d.ts +34 -0
  156. package/dist/esm/rendering/webgl2/WebGl2StencilClipper.js +169 -0
  157. package/dist/esm/rendering/webgl2/WebGl2StencilClipper.js.map +1 -0
  158. package/dist/esm/rendering/webgl2/WebGl2TextRenderer.js +4 -0
  159. package/dist/esm/rendering/webgl2/WebGl2TextRenderer.js.map +1 -1
  160. package/dist/esm/rendering/webgl2/glsl/mesh.frag.js +1 -1
  161. package/dist/esm/rendering/webgl2/glsl/mesh.vert.js +1 -1
  162. package/dist/esm/rendering/webgl2/glsl/stencil-clip.frag.js +4 -0
  163. package/dist/esm/rendering/webgl2/glsl/stencil-clip.frag.js.map +1 -0
  164. package/dist/esm/rendering/webgl2/glsl/stencil-clip.vert.js +4 -0
  165. package/dist/esm/rendering/webgl2/glsl/stencil-clip.vert.js.map +1 -0
  166. package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts +50 -0
  167. package/dist/esm/rendering/webgpu/WebGpuBackend.js +135 -19
  168. package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
  169. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js +22 -17
  170. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js.map +1 -1
  171. package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.d.ts +24 -0
  172. package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js +488 -74
  173. package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js.map +1 -1
  174. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +13 -17
  175. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
  176. package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.d.ts +141 -0
  177. package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.js +270 -0
  178. package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.js.map +1 -0
  179. package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.d.ts +16 -0
  180. package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js +335 -26
  181. package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js.map +1 -1
  182. package/dist/esm/rendering/webgpu/WebGpuStencilClipper.d.ts +57 -0
  183. package/dist/esm/rendering/webgpu/WebGpuStencilClipper.js +257 -0
  184. package/dist/esm/rendering/webgpu/WebGpuStencilClipper.js.map +1 -0
  185. package/dist/esm/rendering/webgpu/WebGpuStencilState.d.ts +14 -0
  186. package/dist/esm/rendering/webgpu/WebGpuStencilState.js +36 -0
  187. package/dist/esm/rendering/webgpu/WebGpuStencilState.js.map +1 -0
  188. package/dist/esm/rendering/webgpu/WebGpuTextRenderer.js +14 -12
  189. package/dist/esm/rendering/webgpu/WebGpuTextRenderer.js.map +1 -1
  190. package/dist/esm/rendering/webgpu/WebGpuTransformStorage.d.ts +16 -0
  191. package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js +57 -0
  192. package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js.map +1 -0
  193. package/dist/esm/resources/JsonStore.d.ts +18 -0
  194. package/dist/esm/resources/JsonStore.js +62 -0
  195. package/dist/esm/resources/JsonStore.js.map +1 -0
  196. package/dist/esm/resources/factories/ImageFactory.d.ts +14 -8
  197. package/dist/esm/resources/factories/ImageFactory.js +13 -6
  198. package/dist/esm/resources/factories/ImageFactory.js.map +1 -1
  199. package/dist/esm/resources/factories/TextureFactory.d.ts +4 -4
  200. package/dist/esm/resources/factories/TextureFactory.js +8 -4
  201. package/dist/esm/resources/factories/TextureFactory.js.map +1 -1
  202. package/dist/esm/resources/index.d.ts +1 -0
  203. package/dist/exo.esm.js +5424 -2205
  204. package/dist/exo.esm.js.map +1 -1
  205. package/package.json +30 -24
  206. package/dist/esm/particles/distributions/Gradient.js.map +0 -1
  207. package/dist/esm/rendering/mesh/MeshShader.js.map +0 -1
  208. package/dist/esm/vendor/webgl-debug.js +0 -1160
  209. package/dist/esm/vendor/webgl-debug.js.map +0 -1
@@ -3,7 +3,9 @@ import { Texture } from '../texture/Texture.js';
3
3
  import { BlendModes } from '../types.js';
4
4
  import { AbstractWebGpuRenderer } from './AbstractWebGpuRenderer.js';
5
5
  import { getWebGpuBlendState } from './WebGpuBlendState.js';
6
+ import { stencilContentDepthStencilState } from './WebGpuStencilState.js';
6
7
 
8
+ /* eslint-disable max-lines */
7
9
  /// <reference types="@webgpu/types" />
8
10
  const meshShaderSource = `
9
11
  struct VertexInput {
@@ -47,6 +49,65 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
47
49
  return vec4<f32>(modulated.rgb * modulated.a, modulated.a);
48
50
  }
49
51
  `;
52
+ const instancedMeshShaderSource = `
53
+ struct VertexInput {
54
+ @location(0) position: vec2<f32>,
55
+ @location(1) texcoord: vec2<f32>,
56
+ @location(2) color: vec4<f32>,
57
+ @location(6) nodeIndex: u32,
58
+ };
59
+
60
+ struct VertexOutput {
61
+ @builtin(position) position: vec4<f32>,
62
+ @location(0) texcoord: vec2<f32>,
63
+ @location(1) color: vec4<f32>,
64
+ @location(2) tint: vec4<f32>,
65
+ @location(3) @interpolate(flat) premultiplySample: u32,
66
+ };
67
+
68
+ struct TransformSlot {
69
+ m0: vec4<f32>,
70
+ m1: vec4<f32>,
71
+ m2: vec4<f32>,
72
+ };
73
+
74
+ struct TransformUniforms {
75
+ projection: mat3x3<f32>,
76
+ flags: vec4<f32>,
77
+ };
78
+
79
+ @group(0) @binding(0) var<uniform> uniforms: TransformUniforms;
80
+ @group(0) @binding(1) var<storage, read> transforms: array<TransformSlot>;
81
+
82
+ @group(1) @binding(0) var meshTexture: texture_2d<f32>;
83
+ @group(1) @binding(1) var meshSampler: sampler;
84
+
85
+ @vertex
86
+ fn vertexMain(input: VertexInput) -> VertexOutput {
87
+ let slot = transforms[input.nodeIndex];
88
+ let world = vec3<f32>(
89
+ slot.m0.x * input.position.x + slot.m0.z * input.position.y + slot.m1.x,
90
+ slot.m0.y * input.position.x + slot.m0.w * input.position.y + slot.m1.y,
91
+ 1.0
92
+ );
93
+
94
+ var output: VertexOutput;
95
+ output.position = vec4<f32>((uniforms.projection * world).xy, 0.0, 1.0);
96
+ output.texcoord = input.texcoord;
97
+ output.color = input.color;
98
+ output.tint = slot.m2;
99
+ output.premultiplySample = u32(uniforms.flags.x);
100
+ return output;
101
+ }
102
+
103
+ @fragment
104
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
105
+ let sample = textureSample(meshTexture, meshSampler, input.texcoord);
106
+ let resolvedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), input.premultiplySample == 1u);
107
+ let modulated = resolvedSample * input.color * input.tint;
108
+ return vec4<f32>(modulated.rgb * modulated.a, modulated.a);
109
+ }
110
+ `;
50
111
  // Per-vertex layout (20 bytes): pos f32x2 + uv f32x2 + color u8x4-norm.
51
112
  // Default-shader path bakes the (view * globalTransform) into position so the
52
113
  // vertex shader stays branchless and uniform-free except for the per-mesh tint.
@@ -55,29 +116,53 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
55
116
  const vertexStrideBytes = 20;
56
117
  const wordsPerVertex = vertexStrideBytes / 4;
57
118
  const tintByteLength = 32; // vec4 tint + vec4 flags (only flags.x used)
119
+ const transformUniformByteLength = 64; // mat3x3<f32> (48B) + vec4<f32> flags (16B)
58
120
  // Custom-shader uniform layout:
59
121
  // mat3x3<f32> projection — 48 bytes (3 vec3 columns padded to vec4 in WGSL)
60
122
  // mat3x3<f32> translation — 48 bytes
61
123
  // vec4<f32> tint — 16 bytes
62
124
  // Total: 112 bytes; aligned up to 256 for dynamic offset.
63
125
  const customMeshUniformBytes = 112;
126
+ /**
127
+ * Cache key for the default + instanced (static-batch) pipeline maps. The
128
+ * stencil dimension keeps the clip and no-clip variants distinct, mirroring the
129
+ * sprite renderer: a stencil pipeline carries depth/stencil state and is only
130
+ * valid in a pass with the matching attachment, so the two are never
131
+ * interchangeable.
132
+ */
133
+ function meshPipelineCacheKey(blendMode, format, stencil) {
134
+ return `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
135
+ }
64
136
  const meshUniformAlignment = 256;
65
137
  const maxCustomTextureSlots = 7; // user texture uniforms; group 2 binding 1..N
66
138
  class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
67
139
  _combinedTransform = new Matrix();
68
140
  _drawCalls = [];
69
141
  _pipelines = new Map();
142
+ _instancedPipelines = new Map();
143
+ _staticGeometryCache = new Map();
70
144
  _textureBindGroups = new WeakMap();
71
145
  _customShaders = new Map();
72
146
  _device = null;
73
147
  _shaderModule = null;
148
+ _instancedShaderModule = null;
74
149
  _uniformBindGroupLayout = null;
150
+ _instancedTransformBindGroupLayout = null;
75
151
  _textureBindGroupLayout = null;
76
152
  _pipelineLayout = null;
153
+ _instancedPipelineLayout = null;
77
154
  _vertexBuffer = null;
78
155
  _indexBuffer = null;
79
156
  _uniformBuffer = null;
80
157
  _uniformBindGroup = null;
158
+ _instancedUniformBuffer = null;
159
+ _instancedUniformBufferCapacity = 0;
160
+ _instancedUniformScratch = new Float32Array(transformUniformByteLength / Float32Array.BYTES_PER_ELEMENT);
161
+ _instancedNodeIndexBuffer = null;
162
+ _instancedNodeIndexBufferCapacity = 0;
163
+ _instancedNodeIndexData = new Uint32Array(0);
164
+ _instancedTransformBindGroup = null;
165
+ _instancedTransformStorageBuffer = null;
81
166
  _uniformAlignment = 256;
82
167
  _vertexBufferCapacity = 0;
83
168
  _indexBufferCapacity = 0;
@@ -92,17 +177,31 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
92
177
  if (backend === null) {
93
178
  throw new Error('WebGpuMeshRenderer is not connected to a backend.');
94
179
  }
95
- const customShader = mesh.shader;
96
- if (customShader !== null && customShader.wgsl === null) {
97
- throw new Error('MeshShader has no `wgsl` source; cannot render through the WebGPU backend.');
180
+ const customShader = mesh.material;
181
+ if (customShader !== null && customShader.shader.wgsl === null) {
182
+ throw new Error('Mesh material shader has no `wgsl` source; cannot render through the WebGPU backend.');
183
+ }
184
+ if (customShader !== null && backend._passCoordinator.stencilActive) {
185
+ // The WebGPU geometric stencil MVP supports clipping default-material
186
+ // Sprites and Mesh/Graphics (which select stencil-enabled pipeline
187
+ // variants below); a custom MeshMaterial under a Geometry clip would need
188
+ // its own stencil pipeline variants and is not supported yet. Throw at
189
+ // collection time (inside the clip scope's try) so the surrounding
190
+ // push/pop balances cleanly, rather than at flush time where the pop has
191
+ // not yet run.
192
+ throw new Error('WebGPU geometry stencil clipping currently supports default-material Sprites, Meshes, and Graphics. A custom-material Mesh under a Geometry clip (RenderNode.clip with a Geometry clipShape) is not supported yet. Use a Rectangle clipShape (scissor) or the WebGL2 backend instead.');
98
193
  }
99
194
  const vertexCount = mesh.vertexCount;
100
195
  if (vertexCount === 0) {
101
196
  return;
102
197
  }
103
- const blendMode = mesh.blendMode;
198
+ // The material owns its blend mode; the mesh's own blendMode overrides it
199
+ // when set away from the default (Normal). Default-path meshes keep their
200
+ // own blendMode verbatim.
201
+ const blendMode = customShader !== null && mesh.blendMode === BlendModes.Normal ? customShader.blendMode : mesh.blendMode;
104
202
  backend.setBlendMode(blendMode);
105
203
  const meshTexture = mesh.texture ?? Texture.white;
204
+ const command = backend.activeDrawCommand;
106
205
  // backend.shouldPremultiplyTextureSample expects RenderTexture-or-Texture.
107
206
  // Both branches are valid here. Premultiply flag is ignored by custom
108
207
  // shaders (they handle premultiplication themselves), but we still record
@@ -123,6 +222,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
123
222
  const drawCall = {
124
223
  mesh,
125
224
  customShader,
225
+ command,
126
226
  blendMode,
127
227
  texture: meshTexture,
128
228
  premultiplySample,
@@ -151,13 +251,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
151
251
  // Honor a pending clear with an empty pass so createColorAttachment
152
252
  // consumes the clear-state once.
153
253
  if (backend.clearRequested) {
154
- const encoder = device.createCommandEncoder();
155
- const pass = encoder.beginRenderPass({
156
- colorAttachments: [backend.createColorAttachment()],
157
- });
158
- backend.stats.renderPasses++;
159
- pass.end();
160
- backend.submit(encoder.finish());
254
+ backend._passCoordinator.acquirePass();
255
+ backend._passCoordinator.endPass();
161
256
  }
162
257
  this._resetFrame();
163
258
  return;
@@ -166,7 +261,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
166
261
  // buffers, so default offsets are independent of custom offsets).
167
262
  let defaultVertices = 0;
168
263
  let defaultIndices = 0;
169
- const customVertexCursors = new Map(); // running vertex count per shader
264
+ const customVertexCursors = new Map(); // running vertex count per material
170
265
  const customIndexCursors = new Map();
171
266
  for (let i = 0; i < this._drawCallCount; i++) {
172
267
  const dc = this._drawCalls[i];
@@ -192,6 +287,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
192
287
  // each custom-shader resource manages its own.
193
288
  const defaultDrawCalls = this._drawCallCount - this._totalCustomDraws();
194
289
  this._ensureUniformCapacity(defaultDrawCalls);
290
+ this._ensureInstancedUniformCapacity(this._drawCallCount);
195
291
  // Phase 3: pack default-path vertex/index/uniform data.
196
292
  const defaultUniformBytes = defaultDrawCalls * this._uniformAlignment;
197
293
  const defaultUniformData = defaultUniformBytes > 0 ? new ArrayBuffer(defaultUniformBytes) : null;
@@ -227,8 +323,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
227
323
  defaultUniformIndex++;
228
324
  }
229
325
  }
230
- // Phase 3b: pack custom-path vertex/index/uniform data per shader.
231
- for (const [shader, resources] of this._customShaders) {
326
+ // Phase 3b: pack custom-path vertex/index/uniform data per material.
327
+ for (const [material, resources] of this._customShaders) {
232
328
  if (resources.drawCount === 0) {
233
329
  continue;
234
330
  }
@@ -239,7 +335,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
239
335
  let drawCursor = 0;
240
336
  for (let i = 0; i < this._drawCallCount; i++) {
241
337
  const dc = this._drawCalls[i];
242
- if (dc.customShader !== shader)
338
+ if (dc.customShader !== material)
243
339
  continue;
244
340
  this._writeMeshVerticesIntoBuffer(dc.mesh, vWritten, resources.vertexFloatView, resources.vertexUintView);
245
341
  if (dc.mesh.indices !== null) {
@@ -251,16 +347,16 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
251
347
  }
252
348
  }
253
349
  // Write mesh-uniform slot (proj/trans/tint) with dynamic offset.
254
- this._writeCustomMeshUniform(shader, resources, drawCursor, dc.mesh, backend);
350
+ this._writeCustomMeshUniform(material, resources, drawCursor, dc.mesh, backend);
255
351
  vWritten += dc.vertexCount;
256
352
  iWritten += dc.indexCount;
257
353
  drawCursor++;
258
354
  }
259
355
  device.queue.writeBuffer(resources.vertexBuffer, 0, resources.vertexData, 0, resources.totalVertices * vertexStrideBytes);
260
356
  device.queue.writeBuffer(resources.indexBuffer, 0, resources.indexData.buffer, resources.indexData.byteOffset, resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT);
261
- // Build/refresh user uniform UBO from shader.uniforms (re-built every
262
- // frame so mutations to shader.uniforms.X are picked up).
263
- this._uploadUserUniforms(shader, resources);
357
+ // Build/refresh user uniform UBO from the material (re-built every frame
358
+ // so mutations to material.uniforms.X are picked up).
359
+ this._uploadUserUniforms(material, resources);
264
360
  }
265
361
  // Phase 4: single writeBuffer per resource for the default path.
266
362
  if (defaultVertices > 0) {
@@ -271,30 +367,66 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
271
367
  device.queue.writeBuffer(this._uniformBuffer, 0, defaultUniformData, 0, defaultUniformBytes);
272
368
  }
273
369
  // Phase 5: single render pass with one drawIndexed per mesh, switching
274
- // pipeline+bind groups between default and custom paths as needed.
275
- const encoder = device.createCommandEncoder({ label: 'WebGpuMeshRenderer' });
276
- const pass = encoder.beginRenderPass({
277
- colorAttachments: [backend.createColorAttachment()],
278
- label: 'WebGpuMeshRenderer pass',
279
- });
280
- backend.stats.renderPasses++;
281
- if (scissor !== null) {
282
- pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
283
- }
370
+ // pipeline+bind groups between default and custom paths as needed. The
371
+ // coordinator owns the GPU pass (load/clear resolution, pass count and
372
+ // scissor are applied there) and ends + submits it below.
373
+ const pass = backend._passCoordinator.acquirePass().pass;
284
374
  const renderTargetFormat = backend.renderTargetFormat;
375
+ // A clip scope flushes the active renderer on push/pop, so every draw call
376
+ // in this batch shares one stencil state — read it once. While active, the
377
+ // coordinator's pass carries a depth/stencil attachment, so the default and
378
+ // static-batch pipelines must select their stencil-enabled variants. The
379
+ // custom path never reaches here under a clip (render() throws at collection
380
+ // time), so its pipelines stay stencil-free.
381
+ const stencil = backend._passCoordinator.stencilActive;
285
382
  let lastShader = null;
286
383
  let lastBlendMode = null;
287
384
  let lastFormat = null;
288
385
  let lastTexture = null;
289
386
  let defaultDrawCursor = 0;
387
+ let instancedDrawCursor = 0;
290
388
  const customDrawCursors = new Map();
291
389
  for (let i = 0; i < this._drawCallCount; i++) {
292
390
  const dc = this._drawCalls[i];
293
391
  if (dc.customShader === null) {
392
+ const batchLength = this._getStaticBatchLength(i);
393
+ if (batchLength >= 2) {
394
+ const needsPipeline = lastShader !== 'instanced' || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
395
+ if (needsPipeline) {
396
+ pass.setPipeline(this._getInstancedPipeline({ blendMode: dc.blendMode, format: renderTargetFormat, stencil }));
397
+ lastShader = 'instanced';
398
+ lastBlendMode = dc.blendMode;
399
+ lastFormat = renderTargetFormat;
400
+ lastTexture = null;
401
+ }
402
+ const maxNodeIndex = this._uploadInstancedNodeIndices(i, batchLength);
403
+ const storage = backend.getTransformStorageBuffer(maxNodeIndex + 1);
404
+ this._writeInstancedUniformSlot(instancedDrawCursor, backend, dc.premultiplySample);
405
+ pass.setBindGroup(0, this._getOrCreateInstancedTransformBindGroup(storage.buffer), [instancedDrawCursor * this._uniformAlignment]);
406
+ if (dc.texture !== lastTexture) {
407
+ lastTexture = dc.texture;
408
+ pass.setBindGroup(1, this._getTextureBindGroup(backend, dc.texture));
409
+ }
410
+ const staticGeometry = this._getOrCreateStaticGeometryEntry(dc.mesh);
411
+ const instanceNodeIndexBuffer = this._instancedNodeIndexBuffer;
412
+ if (instanceNodeIndexBuffer === null) {
413
+ throw new Error('Instanced node-index buffer must be initialized before drawing.');
414
+ }
415
+ pass.setVertexBuffer(0, staticGeometry.vertexBuffer);
416
+ pass.setVertexBuffer(1, instanceNodeIndexBuffer);
417
+ pass.setIndexBuffer(staticGeometry.indexBuffer, 'uint16');
418
+ pass.drawIndexed(staticGeometry.indexCount, batchLength);
419
+ backend.stats.batches++;
420
+ backend.stats.drawCalls++;
421
+ defaultDrawCursor += batchLength;
422
+ instancedDrawCursor++;
423
+ i += batchLength - 1;
424
+ continue;
425
+ }
294
426
  // ----- Default path -----
295
427
  const needsPipeline = lastShader !== 'default' || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
296
428
  if (needsPipeline) {
297
- pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat }));
429
+ pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat, stencil }));
298
430
  lastShader = 'default';
299
431
  lastBlendMode = dc.blendMode;
300
432
  lastFormat = renderTargetFormat;
@@ -319,7 +451,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
319
451
  // (Spector.js, Chrome DevTools' WebGPU panel) show meaningful
320
452
  // labels for the otherwise-anonymous mesh draws inside the
321
453
  // batched render pass.
322
- pass.pushDebugGroup('MeshShader (custom)');
454
+ pass.pushDebugGroup('MeshMaterial (custom)');
323
455
  if (needsPipeline) {
324
456
  pass.setPipeline(this._getOrCreateCustomPipeline(resources, dc.blendMode, renderTargetFormat));
325
457
  lastShader = dc.customShader;
@@ -344,8 +476,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
344
476
  backend.stats.batches++;
345
477
  backend.stats.drawCalls++;
346
478
  }
347
- pass.end();
348
- backend.submit(encoder.finish());
479
+ backend._passCoordinator.endPass();
349
480
  this._resetFrame();
350
481
  }
351
482
  destroy() {
@@ -372,12 +503,20 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
372
503
  const promises = [];
373
504
  for (const blendMode of blendModes) {
374
505
  for (const format of formats) {
375
- const key = `${blendMode}:${format}`;
506
+ // Prewarm only the no-clip variants; the stencil pipelines are created
507
+ // lazily on the first clipped draw (a rare path not worth the upfront
508
+ // compile cost for every blend-mode × format combination).
509
+ const key = meshPipelineCacheKey(blendMode, format, false);
376
510
  if (this._pipelines.has(key))
377
511
  continue;
378
512
  promises.push(device.createRenderPipelineAsync(this._buildPipelineDescriptor(blendMode, format)).then(pipeline => {
379
513
  this._pipelines.set(key, pipeline);
380
514
  }));
515
+ if (!this._instancedPipelines.has(key)) {
516
+ promises.push(device.createRenderPipelineAsync(this._buildInstancedPipelineDescriptor(blendMode, format)).then(pipeline => {
517
+ this._instancedPipelines.set(key, pipeline);
518
+ }));
519
+ }
381
520
  }
382
521
  }
383
522
  await Promise.all(promises);
@@ -388,6 +527,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
388
527
  }
389
528
  this._device = backend.device;
390
529
  this._shaderModule = this._device.createShaderModule({ code: meshShaderSource });
530
+ this._instancedShaderModule = this._device.createShaderModule({ code: instancedMeshShaderSource });
391
531
  this._uniformBindGroupLayout = this._device.createBindGroupLayout({
392
532
  entries: [
393
533
  {
@@ -414,27 +554,59 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
414
554
  this._pipelineLayout = this._device.createPipelineLayout({
415
555
  bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
416
556
  });
557
+ this._instancedTransformBindGroupLayout = this._device.createBindGroupLayout({
558
+ entries: [
559
+ {
560
+ binding: 0,
561
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
562
+ buffer: { type: 'uniform', hasDynamicOffset: true },
563
+ },
564
+ {
565
+ binding: 1,
566
+ visibility: GPUShaderStage.VERTEX,
567
+ buffer: { type: 'read-only-storage' },
568
+ },
569
+ ],
570
+ });
571
+ this._instancedPipelineLayout = this._device.createPipelineLayout({
572
+ bindGroupLayouts: [this._instancedTransformBindGroupLayout, this._textureBindGroupLayout],
573
+ });
417
574
  }
418
575
  onDisconnect() {
419
576
  this.flush();
420
577
  this._vertexBuffer?.destroy();
421
578
  this._indexBuffer?.destroy();
422
579
  this._uniformBuffer?.destroy();
580
+ this._instancedUniformBuffer?.destroy();
581
+ this._instancedNodeIndexBuffer?.destroy();
423
582
  this._pipelines.clear();
583
+ this._instancedPipelines.clear();
424
584
  this._textureBindGroups = new WeakMap();
585
+ for (const entry of this._staticGeometryCache.values()) {
586
+ entry.vertexBuffer.destroy();
587
+ entry.indexBuffer.destroy();
588
+ }
589
+ this._staticGeometryCache.clear();
425
590
  this._vertexBuffer = null;
426
591
  this._indexBuffer = null;
427
592
  this._uniformBuffer = null;
428
593
  this._uniformBindGroup = null;
594
+ this._instancedUniformBuffer = null;
595
+ this._instancedNodeIndexBuffer = null;
596
+ this._instancedTransformBindGroup = null;
597
+ this._instancedTransformStorageBuffer = null;
429
598
  this._pipelineLayout = null;
599
+ this._instancedPipelineLayout = null;
430
600
  this._textureBindGroupLayout = null;
431
601
  this._uniformBindGroupLayout = null;
602
+ this._instancedTransformBindGroupLayout = null;
432
603
  this._shaderModule = null;
433
- // Custom shaders are owned by user code (one MeshShader can be shared
604
+ this._instancedShaderModule = null;
605
+ // Custom materials are owned by user code (one MeshMaterial can be shared
434
606
  // across multiple Mesh instances). Their resources are released when the
435
- // user calls shader.destroy(), which fires our _onDispose callback. On
607
+ // user calls material.destroy(), which fires our _onDispose callback. On
436
608
  // backend disconnect we eagerly release everything to avoid GPU leaks
437
- // even if the user keeps the shader reference around.
609
+ // even if the user keeps the material reference around.
438
610
  for (const resources of this._customShaders.values()) {
439
611
  this._releaseCustomShaderResources(resources);
440
612
  }
@@ -445,6 +617,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
445
617
  this._vertexBufferCapacity = 0;
446
618
  this._indexBufferCapacity = 0;
447
619
  this._uniformBufferCapacity = 0;
620
+ this._instancedUniformBufferCapacity = 0;
621
+ this._instancedNodeIndexBufferCapacity = 0;
622
+ this._instancedNodeIndexData = new Uint32Array(0);
448
623
  }
449
624
  // ---------------------------------------------------------------------------
450
625
  // Default-path helpers
@@ -490,16 +665,16 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
490
665
  }
491
666
  }
492
667
  _getPipeline(key) {
493
- const cacheKey = `${key.blendMode}:${key.format}`;
668
+ const cacheKey = meshPipelineCacheKey(key.blendMode, key.format, key.stencil);
494
669
  let pipeline = this._pipelines.get(cacheKey);
495
670
  if (!pipeline) {
496
- pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(key.blendMode, key.format));
671
+ pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(key.blendMode, key.format, key.stencil));
497
672
  this._pipelines.set(cacheKey, pipeline);
498
673
  }
499
674
  return pipeline;
500
675
  }
501
- _buildPipelineDescriptor(blendMode, format) {
502
- return {
676
+ _buildPipelineDescriptor(blendMode, format, stencil = false) {
677
+ const descriptor = {
503
678
  layout: this._pipelineLayout,
504
679
  vertex: {
505
680
  module: this._shaderModule,
@@ -532,6 +707,10 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
532
707
  cullMode: 'none',
533
708
  },
534
709
  };
710
+ if (stencil) {
711
+ descriptor.depthStencil = stencilContentDepthStencilState();
712
+ }
713
+ return descriptor;
535
714
  }
536
715
  _getTextureBindGroup(backend, texture) {
537
716
  let group = this._textureBindGroups.get(texture);
@@ -548,6 +727,223 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
548
727
  }
549
728
  return group;
550
729
  }
730
+ _getStaticBatchLength(startIndex) {
731
+ const first = this._drawCalls[startIndex];
732
+ if (!this._isStaticBatchCandidate(first)) {
733
+ return 1;
734
+ }
735
+ let length = 1;
736
+ for (let i = startIndex + 1; i < this._drawCallCount; i++) {
737
+ const next = this._drawCalls[i];
738
+ if (!this._isSameStaticBatch(first, next)) {
739
+ break;
740
+ }
741
+ length++;
742
+ }
743
+ return length;
744
+ }
745
+ _isStaticBatchCandidate(drawCall) {
746
+ const command = drawCall.command;
747
+ return drawCall.customShader === null && command?.groupIndex !== undefined && drawCall.mesh.geometry?.usage === 'static';
748
+ }
749
+ _isSameStaticBatch(left, right) {
750
+ if (!this._isStaticBatchCandidate(left) || !this._isStaticBatchCandidate(right)) {
751
+ return false;
752
+ }
753
+ return (left.command.groupIndex === right.command.groupIndex &&
754
+ left.mesh.geometry === right.mesh.geometry &&
755
+ left.texture === right.texture &&
756
+ left.blendMode === right.blendMode &&
757
+ left.command.material.pipelineKey === right.command.material.pipelineKey &&
758
+ left.command.material.bindKey === right.command.material.bindKey);
759
+ }
760
+ _uploadInstancedNodeIndices(startIndex, batchLength) {
761
+ this._ensureInstancedNodeIndexCapacity(batchLength);
762
+ let maxNodeIndex = 0;
763
+ for (let i = 0; i < batchLength; i++) {
764
+ const nodeIndex = this._drawCalls[startIndex + i].command.nodeIndex >>> 0;
765
+ this._instancedNodeIndexData[i] = nodeIndex;
766
+ if (nodeIndex > maxNodeIndex) {
767
+ maxNodeIndex = nodeIndex;
768
+ }
769
+ }
770
+ this._device.queue.writeBuffer(this._instancedNodeIndexBuffer, 0, this._instancedNodeIndexData.buffer, this._instancedNodeIndexData.byteOffset, batchLength * Uint32Array.BYTES_PER_ELEMENT);
771
+ return maxNodeIndex;
772
+ }
773
+ _ensureInstancedNodeIndexCapacity(instanceCount) {
774
+ const requiredBytes = instanceCount * Uint32Array.BYTES_PER_ELEMENT;
775
+ if (this._instancedNodeIndexData.length < instanceCount) {
776
+ this._instancedNodeIndexData = new Uint32Array(Math.max(instanceCount, this._instancedNodeIndexData.length * 2 || 1));
777
+ }
778
+ if (requiredBytes > this._instancedNodeIndexBufferCapacity) {
779
+ this._instancedNodeIndexBuffer?.destroy();
780
+ this._instancedNodeIndexBufferCapacity = Math.max(requiredBytes, this._instancedNodeIndexBufferCapacity * 2 || Uint32Array.BYTES_PER_ELEMENT);
781
+ this._instancedNodeIndexBuffer = this._device.createBuffer({
782
+ size: this._instancedNodeIndexBufferCapacity,
783
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
784
+ });
785
+ }
786
+ }
787
+ _ensureInstancedUniformCapacity(drawCallCount) {
788
+ if (drawCallCount === 0) {
789
+ return;
790
+ }
791
+ const requiredBytes = drawCallCount * this._uniformAlignment;
792
+ if (requiredBytes > this._instancedUniformBufferCapacity) {
793
+ this._instancedUniformBuffer?.destroy();
794
+ this._instancedUniformBufferCapacity = Math.max(requiredBytes, this._instancedUniformBufferCapacity * 2 || this._uniformAlignment);
795
+ this._instancedUniformBuffer = this._device.createBuffer({
796
+ size: this._instancedUniformBufferCapacity,
797
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
798
+ });
799
+ this._instancedTransformBindGroup = null;
800
+ this._instancedTransformStorageBuffer = null;
801
+ }
802
+ }
803
+ _writeInstancedUniformSlot(slot, backend, premultiplySample) {
804
+ const data = this._instancedUniformScratch;
805
+ const projection = backend.view.getTransform();
806
+ data.fill(0);
807
+ data[0] = projection.a;
808
+ data[1] = projection.b;
809
+ data[4] = projection.c;
810
+ data[5] = projection.d;
811
+ data[8] = projection.x;
812
+ data[9] = projection.y;
813
+ data[10] = 1;
814
+ data[12] = premultiplySample ? 1 : 0;
815
+ this._device.queue.writeBuffer(this._instancedUniformBuffer, slot * this._uniformAlignment, data.buffer, data.byteOffset, transformUniformByteLength);
816
+ }
817
+ _getOrCreateInstancedTransformBindGroup(storageBuffer) {
818
+ if (this._instancedTransformBindGroup !== null && this._instancedTransformStorageBuffer === storageBuffer) {
819
+ return this._instancedTransformBindGroup;
820
+ }
821
+ this._instancedTransformStorageBuffer = storageBuffer;
822
+ this._instancedTransformBindGroup = this._device.createBindGroup({
823
+ layout: this._instancedTransformBindGroupLayout,
824
+ entries: [
825
+ {
826
+ binding: 0,
827
+ resource: {
828
+ buffer: this._instancedUniformBuffer,
829
+ size: transformUniformByteLength,
830
+ },
831
+ },
832
+ {
833
+ binding: 1,
834
+ resource: {
835
+ buffer: storageBuffer,
836
+ },
837
+ },
838
+ ],
839
+ });
840
+ return this._instancedTransformBindGroup;
841
+ }
842
+ _getInstancedPipeline(key) {
843
+ const cacheKey = meshPipelineCacheKey(key.blendMode, key.format, key.stencil);
844
+ let pipeline = this._instancedPipelines.get(cacheKey);
845
+ if (!pipeline) {
846
+ pipeline = this._device.createRenderPipeline(this._buildInstancedPipelineDescriptor(key.blendMode, key.format, key.stencil));
847
+ this._instancedPipelines.set(cacheKey, pipeline);
848
+ }
849
+ return pipeline;
850
+ }
851
+ _buildInstancedPipelineDescriptor(blendMode, format, stencil = false) {
852
+ const descriptor = {
853
+ layout: this._instancedPipelineLayout,
854
+ vertex: {
855
+ module: this._instancedShaderModule,
856
+ entryPoint: 'vertexMain',
857
+ buffers: [
858
+ {
859
+ arrayStride: vertexStrideBytes,
860
+ stepMode: 'vertex',
861
+ attributes: [
862
+ { shaderLocation: 0, offset: 0, format: 'float32x2' },
863
+ { shaderLocation: 1, offset: 8, format: 'float32x2' },
864
+ { shaderLocation: 2, offset: 16, format: 'unorm8x4' },
865
+ ],
866
+ },
867
+ {
868
+ arrayStride: Uint32Array.BYTES_PER_ELEMENT,
869
+ stepMode: 'instance',
870
+ attributes: [{ shaderLocation: 6, offset: 0, format: 'uint32' }],
871
+ },
872
+ ],
873
+ },
874
+ fragment: {
875
+ module: this._instancedShaderModule,
876
+ entryPoint: 'fragmentMain',
877
+ targets: [
878
+ {
879
+ format,
880
+ blend: getWebGpuBlendState(blendMode),
881
+ writeMask: GPUColorWrite.ALL,
882
+ },
883
+ ],
884
+ },
885
+ primitive: {
886
+ topology: 'triangle-list',
887
+ cullMode: 'none',
888
+ },
889
+ };
890
+ if (stencil) {
891
+ descriptor.depthStencil = stencilContentDepthStencilState();
892
+ }
893
+ return descriptor;
894
+ }
895
+ _getOrCreateStaticGeometryEntry(mesh) {
896
+ const geometry = mesh.geometry;
897
+ if (geometry?.usage !== 'static') {
898
+ throw new Error('Static mesh batching requires Geometry with usage="static".');
899
+ }
900
+ const existing = this._staticGeometryCache.get(geometry);
901
+ if (existing !== undefined) {
902
+ return existing;
903
+ }
904
+ const vertexData = new ArrayBuffer(mesh.vertexCount * vertexStrideBytes);
905
+ const vertexFloatView = new Float32Array(vertexData);
906
+ const vertexUintView = new Uint32Array(vertexData);
907
+ this._writeMeshVerticesIntoBuffer(mesh, 0, vertexFloatView, vertexUintView);
908
+ const indexData = new Uint16Array(mesh.indexCount);
909
+ if (mesh.indices !== null) {
910
+ indexData.set(mesh.indices, 0);
911
+ }
912
+ else {
913
+ for (let i = 0; i < mesh.indexCount; i++) {
914
+ indexData[i] = i;
915
+ }
916
+ }
917
+ const vertexBuffer = this._device.createBuffer({
918
+ size: vertexData.byteLength,
919
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
920
+ });
921
+ const indexBuffer = this._device.createBuffer({
922
+ size: indexData.byteLength,
923
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
924
+ });
925
+ this._device.queue.writeBuffer(vertexBuffer, 0, vertexData, 0, vertexData.byteLength);
926
+ this._device.queue.writeBuffer(indexBuffer, 0, indexData.buffer, indexData.byteOffset, indexData.byteLength);
927
+ const disposeListener = () => {
928
+ const entry = this._staticGeometryCache.get(geometry);
929
+ if (entry === undefined) {
930
+ return;
931
+ }
932
+ entry.vertexBuffer.destroy();
933
+ entry.indexBuffer.destroy();
934
+ this._staticGeometryCache.delete(geometry);
935
+ };
936
+ geometry._onDispose(disposeListener);
937
+ const created = {
938
+ geometry,
939
+ vertexBuffer,
940
+ indexBuffer,
941
+ indexCount: mesh.indexCount,
942
+ disposeListener,
943
+ };
944
+ this._staticGeometryCache.set(geometry, created);
945
+ return created;
946
+ }
551
947
  _ensureVertexCapacity(vertexCount) {
552
948
  const requiredBytes = vertexCount * vertexStrideBytes;
553
949
  if (requiredBytes > this._vertexData.byteLength) {
@@ -620,19 +1016,19 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
620
1016
  resources.totalIndices = 0;
621
1017
  }
622
1018
  }
623
- _getOrCreateCustomShaderResources(shader) {
624
- let resources = this._customShaders.get(shader);
1019
+ _getOrCreateCustomShaderResources(material) {
1020
+ let resources = this._customShaders.get(material);
625
1021
  if (resources !== undefined) {
626
1022
  return resources;
627
1023
  }
628
1024
  if (this._device === null) {
629
1025
  throw new Error('WebGpuMeshRenderer is not connected to a backend.');
630
1026
  }
631
- if (shader.wgsl === null) {
632
- throw new Error('MeshShader has no `wgsl` source; cannot render through the WebGPU backend.');
1027
+ if (material.shader.wgsl === null) {
1028
+ throw new Error('Mesh material shader has no `wgsl` source; cannot render through the WebGPU backend.');
633
1029
  }
634
1030
  const device = this._device;
635
- const shaderModule = device.createShaderModule({ code: shader.wgsl });
1031
+ const shaderModule = device.createShaderModule({ code: material.shader.wgsl });
636
1032
  const meshUniformLayout = device.createBindGroupLayout({
637
1033
  entries: [
638
1034
  {
@@ -648,7 +1044,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
648
1044
  { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
649
1045
  ],
650
1046
  });
651
- const userLayout = this._buildUserBindGroupLayout(device, shader);
1047
+ const userLayout = this._buildUserBindGroupLayout(device, material);
652
1048
  const pipelineLayout = device.createPipelineLayout({
653
1049
  bindGroupLayouts: [meshUniformLayout, meshTextureLayout, userLayout],
654
1050
  });
@@ -687,13 +1083,13 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
687
1083
  totalVertices: 0,
688
1084
  totalIndices: 0,
689
1085
  };
690
- this._customShaders.set(shader, resources);
691
- // When the user calls shader.destroy(), evict and release.
692
- shader._onDispose(() => {
693
- const r = this._customShaders.get(shader);
1086
+ this._customShaders.set(material, resources);
1087
+ // When the user calls material.destroy(), evict and release.
1088
+ material._onDispose(() => {
1089
+ const r = this._customShaders.get(material);
694
1090
  if (r !== undefined) {
695
1091
  this._releaseCustomShaderResources(r);
696
- this._customShaders.delete(shader);
1092
+ this._customShaders.delete(material);
697
1093
  }
698
1094
  });
699
1095
  return resources;
@@ -764,7 +1160,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
764
1160
  uintView[targetIndex + 4] = colors !== null ? colors[i] : 0xffffffff;
765
1161
  }
766
1162
  }
767
- _writeCustomMeshUniform(_shader, resources, drawCursor, mesh, backend) {
1163
+ _writeCustomMeshUniform(_material, resources, drawCursor, mesh, backend) {
768
1164
  // Layout: mat3x3 projection (48B) + mat3x3 translation (48B) + vec4 tint (16B) = 112B.
769
1165
  // WGSL mat3x3 stores 3 vec3 columns padded to vec4 alignment.
770
1166
  const slotBytes = meshUniformAlignment;
@@ -873,10 +1269,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
873
1269
  }
874
1270
  return group;
875
1271
  }
876
- _buildUserBindGroupLayout(device, shader) {
1272
+ _buildUserBindGroupLayout(device, material) {
877
1273
  const entries = [];
878
- const userUniforms = shader.uniforms;
879
- Object.values(userUniforms).some(v => !isTextureUniform(v));
880
1274
  // Binding 0 always reserved for the user UBO (even if empty), so the
881
1275
  // bind-group layout is stable across user-uniform mutations.
882
1276
  entries.push({
@@ -884,15 +1278,12 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
884
1278
  visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
885
1279
  buffer: { type: 'uniform' },
886
1280
  });
1281
+ const textureBindings = collectTextureBindings(material);
1282
+ if (textureBindings.length > maxCustomTextureSlots) {
1283
+ throw new Error(`Mesh material requested more than ${maxCustomTextureSlots} user texture bindings.`);
1284
+ }
887
1285
  let bindingIndex = 1;
888
- let textureCount = 0;
889
- for (const value of Object.values(userUniforms)) {
890
- if (!isTextureUniform(value)) {
891
- continue;
892
- }
893
- if (textureCount >= maxCustomTextureSlots) {
894
- throw new Error(`MeshShader requested more than ${maxCustomTextureSlots} user texture uniforms.`);
895
- }
1286
+ for (let t = 0; t < textureBindings.length; t++) {
896
1287
  entries.push({
897
1288
  binding: bindingIndex,
898
1289
  visibility: GPUShaderStage.FRAGMENT,
@@ -905,14 +1296,12 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
905
1296
  sampler: { type: 'filtering' },
906
1297
  });
907
1298
  bindingIndex++;
908
- textureCount++;
909
1299
  }
910
1300
  return device.createBindGroupLayout({ entries });
911
1301
  }
912
- _uploadUserUniforms(_shader, resources) {
1302
+ _uploadUserUniforms(material, resources) {
913
1303
  const device = this._device;
914
- const uniforms = _shader.uniforms;
915
- const scalarValues = Object.values(uniforms).filter(v => !isTextureUniform(v));
1304
+ const scalarValues = collectScalarUniforms(material);
916
1305
  // Always create a UBO (even if empty) since binding 0 of the user layout
917
1306
  // is fixed. Min size 16 bytes to satisfy WebGPU's minimum buffer size.
918
1307
  const slotCount = Math.max(scalarValues.length, 1);
@@ -950,16 +1339,13 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
950
1339
  }
951
1340
  device.queue.writeBuffer(resources.userUniformBuffer, 0, data);
952
1341
  }
953
- _buildUserBindGroup(backend, shader, resources) {
1342
+ _buildUserBindGroup(backend, material, resources) {
954
1343
  const device = this._device;
955
1344
  const entries = [];
956
1345
  entries.push({ binding: 0, resource: { buffer: resources.userUniformBuffer } });
957
1346
  let bindingIndex = 1;
958
- for (const value of Object.values(shader.uniforms)) {
959
- if (!isTextureUniform(value)) {
960
- continue;
961
- }
962
- const binding = backend.getTextureBinding(value);
1347
+ for (const texture of collectTextureBindings(material)) {
1348
+ const binding = backend.getTextureBinding(texture);
963
1349
  entries.push({ binding: bindingIndex, resource: binding.view });
964
1350
  bindingIndex++;
965
1351
  entries.push({ binding: bindingIndex, resource: binding.sampler });
@@ -997,6 +1383,34 @@ function isTextureUniform(value) {
997
1383
  !(value instanceof Int32Array) &&
998
1384
  !Array.isArray(value));
999
1385
  }
1386
+ /** Scalar/vector/matrix uniforms (texture values excluded) in declaration order. */
1387
+ function collectScalarUniforms(material) {
1388
+ const result = [];
1389
+ for (const value of Object.values(material.uniforms)) {
1390
+ if (!isTextureUniform(value)) {
1391
+ result.push(value);
1392
+ }
1393
+ }
1394
+ return result;
1395
+ }
1396
+ /**
1397
+ * Texture bindings claimed by the material, in a stable order: texture-valued
1398
+ * entries of `uniforms` first (declaration order), then the dedicated
1399
+ * `textures` map (declaration order). The WGSL source must declare its
1400
+ * `@group(2)` texture/sampler pairs in this same order.
1401
+ */
1402
+ function collectTextureBindings(material) {
1403
+ const result = [];
1404
+ for (const value of Object.values(material.uniforms)) {
1405
+ if (isTextureUniform(value)) {
1406
+ result.push(value);
1407
+ }
1408
+ }
1409
+ for (const texture of Object.values(material.textures)) {
1410
+ result.push(texture);
1411
+ }
1412
+ return result;
1413
+ }
1000
1414
 
1001
1415
  export { WebGpuMeshRenderer };
1002
1416
  //# sourceMappingURL=WebGpuMeshRenderer.js.map