@codexo/exojs 0.9.0 → 0.11.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.
- package/CHANGELOG.md +127 -0
- package/README.md +14 -14
- package/dist/esm/core/Application.d.ts +25 -6
- package/dist/esm/core/Application.js +42 -8
- package/dist/esm/core/Application.js.map +1 -1
- package/dist/esm/core/Perf.d.ts +23 -0
- package/dist/esm/core/Perf.js +49 -0
- package/dist/esm/core/Perf.js.map +1 -0
- package/dist/esm/core/Scene.d.ts +8 -8
- package/dist/esm/core/Scene.js +7 -7
- package/dist/esm/core/Scene.js.map +1 -1
- package/dist/esm/core/SceneManager.js +2 -2
- package/dist/esm/core/SceneManager.js.map +1 -1
- package/dist/esm/core/SceneNode.d.ts +0 -3
- package/dist/esm/core/SceneNode.js +0 -9
- package/dist/esm/core/SceneNode.js.map +1 -1
- package/dist/esm/core/capabilities.d.ts +2 -0
- package/dist/esm/core/capabilities.js +15 -0
- package/dist/esm/core/capabilities.js.map +1 -1
- package/dist/esm/core/dev.d.ts +21 -0
- package/dist/esm/core/dev.js +18 -0
- package/dist/esm/core/dev.js.map +1 -0
- package/dist/esm/core/index.d.ts +1 -0
- package/dist/esm/core/types.d.ts +1 -1
- package/dist/esm/core/utils.d.ts +12 -0
- package/dist/esm/core/utils.js +18 -1
- package/dist/esm/core/utils.js.map +1 -1
- package/dist/esm/index.js +14 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/particles/ParticleSystem.d.ts +8 -5
- package/dist/esm/particles/ParticleSystem.js +9 -5
- package/dist/esm/particles/ParticleSystem.js.map +1 -1
- package/dist/esm/particles/distributions/{Gradient.d.ts → ColorGradient.d.ts} +5 -5
- package/dist/esm/particles/distributions/{Gradient.js → ColorGradient.js} +5 -5
- package/dist/esm/particles/distributions/ColorGradient.js.map +1 -0
- package/dist/esm/particles/distributions/Distribution.d.ts +2 -2
- package/dist/esm/particles/distributions/index.d.ts +2 -2
- package/dist/esm/particles/gpu/ParticleGpuState.js +1 -1
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.d.ts +2 -2
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.js +5 -1
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.js.map +1 -1
- package/dist/esm/particles/modules/ColorOverLifetime.d.ts +3 -3
- package/dist/esm/particles/modules/ColorOverLifetime.js.map +1 -1
- package/dist/esm/particles/modules/ColorOverSpeed.d.ts +3 -3
- package/dist/esm/particles/modules/ColorOverSpeed.js.map +1 -1
- package/dist/esm/particles/modules/UpdateModule.d.ts +2 -2
- package/dist/esm/particles/modules/UpdateModule.js +1 -1
- package/dist/esm/particles/modules/WgslContribution.d.ts +2 -2
- package/dist/esm/rendering/Camera.d.ts +33 -0
- package/dist/esm/rendering/Camera.js +38 -0
- package/dist/esm/rendering/Camera.js.map +1 -0
- package/dist/esm/rendering/Container.d.ts +5 -24
- package/dist/esm/rendering/Container.js +8 -71
- package/dist/esm/rendering/Container.js.map +1 -1
- package/dist/esm/rendering/Drawable.d.ts +8 -10
- package/dist/esm/rendering/Drawable.js +12 -20
- package/dist/esm/rendering/Drawable.js.map +1 -1
- package/dist/esm/rendering/RenderBackend.d.ts +18 -0
- package/dist/esm/rendering/RenderNode.d.ts +81 -8
- package/dist/esm/rendering/RenderNode.js +121 -144
- package/dist/esm/rendering/RenderNode.js.map +1 -1
- package/dist/esm/rendering/RenderTarget.d.ts +13 -0
- package/dist/esm/rendering/RenderTarget.js +13 -0
- package/dist/esm/rendering/RenderTarget.js.map +1 -1
- package/dist/esm/rendering/RenderTargetPass.js +17 -0
- package/dist/esm/rendering/RenderTargetPass.js.map +1 -1
- package/dist/esm/rendering/RenderingContext.d.ts +87 -0
- package/dist/esm/rendering/RenderingContext.js +157 -0
- package/dist/esm/rendering/RenderingContext.js.map +1 -0
- package/dist/esm/rendering/TransformBuffer.d.ts +82 -0
- package/dist/esm/rendering/TransformBuffer.js +180 -0
- package/dist/esm/rendering/TransformBuffer.js.map +1 -0
- package/dist/esm/rendering/filters/WebGpuShaderFilter.js +5 -12
- package/dist/esm/rendering/filters/WebGpuShaderFilter.js.map +1 -1
- package/dist/esm/rendering/geometry/Geometry.d.ts +40 -0
- package/dist/esm/rendering/geometry/Geometry.js +228 -0
- package/dist/esm/rendering/geometry/Geometry.js.map +1 -0
- package/dist/esm/rendering/geometry/GeometryAttribute.d.ts +32 -0
- package/dist/esm/rendering/geometry/QuadGeometry.d.ts +5 -0
- package/dist/esm/rendering/gradient/Gradient.d.ts +67 -0
- package/dist/esm/rendering/gradient/Gradient.js +160 -0
- package/dist/esm/rendering/gradient/Gradient.js.map +1 -0
- package/dist/esm/rendering/gradient/LinearGradient.d.ts +18 -0
- package/dist/esm/rendering/gradient/LinearGradient.js +49 -0
- package/dist/esm/rendering/gradient/LinearGradient.js.map +1 -0
- package/dist/esm/rendering/gradient/RadialGradient.d.ts +18 -0
- package/dist/esm/rendering/gradient/RadialGradient.js +44 -0
- package/dist/esm/rendering/gradient/RadialGradient.js.map +1 -0
- package/dist/esm/rendering/index.d.ts +16 -2
- package/dist/esm/rendering/material/Material.d.ts +114 -0
- package/dist/esm/rendering/material/Material.js +111 -0
- package/dist/esm/rendering/material/Material.js.map +1 -0
- package/dist/esm/rendering/material/MaterialKey.d.ts +18 -0
- package/dist/esm/rendering/material/MaterialKey.js +82 -0
- package/dist/esm/rendering/material/MaterialKey.js.map +1 -0
- package/dist/esm/rendering/material/MeshMaterial.d.ts +16 -0
- package/dist/esm/rendering/material/MeshMaterial.js +21 -0
- package/dist/esm/rendering/material/MeshMaterial.js.map +1 -0
- package/dist/esm/rendering/{mesh/MeshShader.d.ts → material/ShaderSource.d.ts} +29 -62
- package/dist/esm/rendering/{mesh/MeshShader.js → material/ShaderSource.js} +35 -62
- package/dist/esm/rendering/material/ShaderSource.js.map +1 -0
- package/dist/esm/rendering/material/SpriteMaterial.d.ts +15 -0
- package/dist/esm/rendering/material/SpriteMaterial.js +20 -0
- package/dist/esm/rendering/material/SpriteMaterial.js.map +1 -0
- package/dist/esm/rendering/mesh/Mesh.d.ts +29 -12
- package/dist/esm/rendering/mesh/Mesh.js +122 -3
- package/dist/esm/rendering/mesh/Mesh.js.map +1 -1
- package/dist/esm/rendering/pass/RenderPassCoordinator.d.ts +63 -0
- package/dist/esm/rendering/pass/RenderPassDescriptor.d.ts +48 -0
- package/dist/esm/rendering/pass/RenderPassDescriptor.js +16 -0
- package/dist/esm/rendering/pass/RenderPassDescriptor.js.map +1 -0
- package/dist/esm/rendering/plan/RenderCommand.d.ts +86 -0
- package/dist/esm/rendering/plan/RenderCommand.js +127 -0
- package/dist/esm/rendering/plan/RenderCommand.js.map +1 -0
- package/dist/esm/rendering/plan/RenderEffectExecutor.d.ts +10 -0
- package/dist/esm/rendering/plan/RenderEffectExecutor.js +159 -0
- package/dist/esm/rendering/plan/RenderEffectExecutor.js.map +1 -0
- package/dist/esm/rendering/plan/RenderInstruction.d.ts +51 -0
- package/dist/esm/rendering/plan/RenderInstruction.js +45 -0
- package/dist/esm/rendering/plan/RenderInstruction.js.map +1 -0
- package/dist/esm/rendering/plan/RenderPlan.d.ts +23 -0
- package/dist/esm/rendering/plan/RenderPlan.js +12 -0
- package/dist/esm/rendering/plan/RenderPlan.js.map +1 -0
- package/dist/esm/rendering/plan/RenderPlanBuilder.d.ts +31 -0
- package/dist/esm/rendering/plan/RenderPlanBuilder.js +242 -0
- package/dist/esm/rendering/plan/RenderPlanBuilder.js.map +1 -0
- package/dist/esm/rendering/plan/RenderPlanOptimizer.d.ts +10 -0
- package/dist/esm/rendering/plan/RenderPlanOptimizer.js +180 -0
- package/dist/esm/rendering/plan/RenderPlanOptimizer.js.map +1 -0
- package/dist/esm/rendering/plan/RenderPlanPlayer.d.ts +13 -0
- package/dist/esm/rendering/plan/RenderPlanPlayer.js +107 -0
- package/dist/esm/rendering/plan/RenderPlanPlayer.js.map +1 -0
- package/dist/esm/rendering/plan/RenderScope.d.ts +70 -0
- package/dist/esm/rendering/plan/RenderScope.js +16 -0
- package/dist/esm/rendering/plan/RenderScope.js.map +1 -0
- package/dist/esm/rendering/plan/playRenderTree.d.ts +4 -0
- package/dist/esm/rendering/plan/playRenderTree.js +19 -0
- package/dist/esm/rendering/plan/playRenderTree.js.map +1 -0
- package/dist/esm/rendering/primitives/Graphics.d.ts +70 -5
- package/dist/esm/rendering/primitives/Graphics.js +172 -14
- package/dist/esm/rendering/primitives/Graphics.js.map +1 -1
- package/dist/esm/rendering/sprite/Sprite.d.ts +22 -1
- package/dist/esm/rendering/sprite/Sprite.js +33 -2
- package/dist/esm/rendering/sprite/Sprite.js.map +1 -1
- package/dist/esm/rendering/sprite/spriteMaterialSources.d.ts +41 -0
- package/dist/esm/rendering/sprite/spriteMaterialSources.js +149 -0
- package/dist/esm/rendering/sprite/spriteMaterialSources.js.map +1 -0
- package/dist/esm/rendering/text/BitmapText.d.ts +2 -0
- package/dist/esm/rendering/text/BitmapText.js +8 -1
- package/dist/esm/rendering/text/BitmapText.js.map +1 -1
- package/dist/esm/rendering/text/BmFont.js +3 -0
- package/dist/esm/rendering/text/BmFont.js.map +1 -1
- package/dist/esm/rendering/text/GlyphSdf.d.ts +14 -0
- package/dist/esm/rendering/text/GlyphSdf.js +41 -11
- package/dist/esm/rendering/text/GlyphSdf.js.map +1 -1
- package/dist/esm/rendering/text/TextStyle.d.ts +6 -1
- package/dist/esm/rendering/text/TextStyle.js +1 -1
- package/dist/esm/rendering/text/TextStyle.js.map +1 -1
- package/dist/esm/rendering/texture/DataTexture.d.ts +5 -0
- package/dist/esm/rendering/texture/DataTexture.js +7 -0
- package/dist/esm/rendering/texture/DataTexture.js.map +1 -1
- package/dist/esm/rendering/texture/RenderTexture.js.map +1 -1
- package/dist/esm/rendering/video/Video.d.ts +3 -7
- package/dist/esm/rendering/video/Video.js +3 -8
- package/dist/esm/rendering/video/Video.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2Backend.d.ts +62 -0
- package/dist/esm/rendering/webgl2/WebGl2Backend.js +352 -21
- package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.d.ts +22 -2
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js +404 -112
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.d.ts +8 -0
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js +8 -0
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2PassCoordinator.d.ts +57 -0
- package/dist/esm/rendering/webgl2/WebGl2PassCoordinator.js +79 -0
- package/dist/esm/rendering/webgl2/WebGl2PassCoordinator.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.d.ts +14 -0
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js +257 -78
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2StencilClipper.d.ts +34 -0
- package/dist/esm/rendering/webgl2/WebGl2StencilClipper.js +169 -0
- package/dist/esm/rendering/webgl2/WebGl2StencilClipper.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2TextRenderer.d.ts +7 -0
- package/dist/esm/rendering/webgl2/WebGl2TextRenderer.js +11 -0
- package/dist/esm/rendering/webgl2/WebGl2TextRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/glsl/mesh.frag.js +1 -1
- package/dist/esm/rendering/webgl2/glsl/mesh.vert.js +1 -1
- package/dist/esm/rendering/webgl2/glsl/sprite.vert.js +1 -1
- package/dist/esm/rendering/webgl2/glsl/stencil-clip.frag.js +4 -0
- package/dist/esm/rendering/webgl2/glsl/stencil-clip.frag.js.map +1 -0
- package/dist/esm/rendering/webgl2/glsl/stencil-clip.vert.js +4 -0
- package/dist/esm/rendering/webgl2/glsl/stencil-clip.vert.js.map +1 -0
- package/dist/esm/rendering/webgl2/glsl/text-color.frag.js +1 -1
- package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts +63 -0
- package/dist/esm/rendering/webgpu/WebGpuBackend.js +180 -19
- package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js +22 -17
- package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.d.ts +24 -0
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js +524 -98
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.d.ts +7 -0
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +24 -22
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.d.ts +141 -0
- package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.js +270 -0
- package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.js.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.d.ts +25 -1
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js +430 -76
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuStencilClipper.d.ts +57 -0
- package/dist/esm/rendering/webgpu/WebGpuStencilClipper.js +257 -0
- package/dist/esm/rendering/webgpu/WebGpuStencilClipper.js.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuStencilState.d.ts +14 -0
- package/dist/esm/rendering/webgpu/WebGpuStencilState.js +36 -0
- package/dist/esm/rendering/webgpu/WebGpuStencilState.js.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuTextRenderer.d.ts +7 -0
- package/dist/esm/rendering/webgpu/WebGpuTextRenderer.js +30 -19
- package/dist/esm/rendering/webgpu/WebGpuTextRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.d.ts +48 -0
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js +103 -0
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js.map +1 -0
- package/dist/esm/resources/JsonStore.d.ts +18 -0
- package/dist/esm/resources/JsonStore.js +62 -0
- package/dist/esm/resources/JsonStore.js.map +1 -0
- package/dist/esm/resources/Loader.js +1 -1
- package/dist/esm/resources/Loader.js.map +1 -1
- package/dist/esm/resources/factories/ImageFactory.d.ts +14 -8
- package/dist/esm/resources/factories/ImageFactory.js +13 -6
- package/dist/esm/resources/factories/ImageFactory.js.map +1 -1
- package/dist/esm/resources/factories/TextureFactory.d.ts +4 -4
- package/dist/esm/resources/factories/TextureFactory.js +8 -4
- package/dist/esm/resources/factories/TextureFactory.js.map +1 -1
- package/dist/esm/resources/index.d.ts +1 -0
- package/dist/exo.esm.js +6326 -2350
- package/dist/exo.esm.js.map +1 -1
- package/package.json +34 -24
- package/dist/esm/particles/distributions/Gradient.js.map +0 -1
- package/dist/esm/rendering/mesh/MeshShader.js.map +0 -1
- package/dist/esm/vendor/webgl-debug.js +0 -1160
- package/dist/esm/vendor/webgl-debug.js.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { spriteVertexWgsl } from '../sprite/spriteMaterialSources.js';
|
|
1
2
|
import { RenderTexture } from '../texture/RenderTexture.js';
|
|
2
3
|
import { Texture } from '../texture/Texture.js';
|
|
3
4
|
import { BlendModes } from '../types.js';
|
|
4
5
|
import { AbstractWebGpuRenderer } from './AbstractWebGpuRenderer.js';
|
|
5
6
|
import { getWebGpuBlendState } from './WebGpuBlendState.js';
|
|
7
|
+
import { stencilContentDepthStencilState } from './WebGpuStencilState.js';
|
|
6
8
|
|
|
7
9
|
/// <reference types="@webgpu/types" />
|
|
8
10
|
const spriteShaderSource = `
|
|
@@ -10,8 +12,16 @@ struct ProjectionUniforms {
|
|
|
10
12
|
matrix: mat4x4<f32>,
|
|
11
13
|
};
|
|
12
14
|
|
|
15
|
+
struct TransformSlot {
|
|
16
|
+
m0: vec4<f32>,
|
|
17
|
+
m1: vec4<f32>,
|
|
18
|
+
m2: vec4<f32>,
|
|
19
|
+
};
|
|
20
|
+
|
|
13
21
|
@group(0) @binding(0)
|
|
14
22
|
var<uniform> projection: ProjectionUniforms;
|
|
23
|
+
@group(0) @binding(1)
|
|
24
|
+
var<storage, read> transforms: array<TransformSlot>;
|
|
15
25
|
|
|
16
26
|
@group(1) @binding(0)
|
|
17
27
|
var spriteTexture0: texture_2d<f32>;
|
|
@@ -47,16 +57,17 @@ var spriteSampler6: sampler;
|
|
|
47
57
|
@group(1) @binding(15)
|
|
48
58
|
var spriteSampler7: sampler;
|
|
49
59
|
|
|
50
|
-
// Per-instance vertex layout (
|
|
60
|
+
// Per-instance vertex layout (36 bytes per sprite). The four corners
|
|
51
61
|
// of the quad are derived from @builtin(vertex_index) 0..3 inside the
|
|
52
|
-
// vertex shader — there is no per-vertex stream.
|
|
62
|
+
// vertex shader — there is no per-vertex stream. The world transform is
|
|
63
|
+
// fetched from the shared transform storage buffer keyed by nodeIndex
|
|
64
|
+
// instead of being packed inline.
|
|
53
65
|
struct VertexInput {
|
|
54
66
|
@location(0) localBounds: vec4<f32>, // left, top, right, bottom (local space)
|
|
55
|
-
@location(1) transformAB: vec3<f32>, // first row of 2D affine
|
|
56
|
-
@location(2) transformCD: vec3<f32>, // second row of 2D affine
|
|
57
67
|
@location(3) uvBounds: vec4<f32>, // uMin, vMin, uMax, vMax (CPU pre-swaps for flipY)
|
|
58
68
|
@location(4) color: vec4<f32>, // RGBA tint
|
|
59
69
|
@location(5) packedSlotFlags: u32, // bits 0..7 = slot, bit 8 = premultiply
|
|
70
|
+
@location(6) nodeIndex: u32, // row into the shared transform storage buffer
|
|
60
71
|
};
|
|
61
72
|
|
|
62
73
|
struct VertexOutput {
|
|
@@ -79,8 +90,12 @@ fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutp
|
|
|
79
90
|
let localX = select(input.localBounds.x, input.localBounds.z, cornerX == 1u);
|
|
80
91
|
let localY = select(input.localBounds.y, input.localBounds.w, cornerY == 1u);
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
// Fetch this instance's world transform from the shared storage buffer,
|
|
94
|
+
// keyed by nodeIndex: m0 = (a, b, c, d), m1 = (tx, ty, 0, 0). (m2 carries the
|
|
95
|
+
// node tint, unused here — the sprite keeps its own per-instance color.)
|
|
96
|
+
let slot = transforms[input.nodeIndex];
|
|
97
|
+
let worldX = slot.m0.x * localX + slot.m0.y * localY + slot.m1.x;
|
|
98
|
+
let worldY = slot.m0.z * localX + slot.m0.w * localY + slot.m1.y;
|
|
84
99
|
|
|
85
100
|
output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
|
|
86
101
|
|
|
@@ -140,11 +155,12 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
|
140
155
|
return resolvedSample * input.color;
|
|
141
156
|
}
|
|
142
157
|
`;
|
|
143
|
-
const instanceStrideBytes =
|
|
158
|
+
const instanceStrideBytes = 36;
|
|
144
159
|
const wordsPerInstance = instanceStrideBytes / Uint32Array.BYTES_PER_ELEMENT;
|
|
145
160
|
const projectionByteLength = 64;
|
|
146
161
|
const initialBatchCapacity = 32;
|
|
147
162
|
const maxBatchTextures = 8;
|
|
163
|
+
const maxCustomTextureSlots = 7; // user texture uniforms; group(2) binding 1..N
|
|
148
164
|
const indicesPerSprite = 6;
|
|
149
165
|
// Static index buffer: two triangles forming a quad, vertex IDs 0..3 in
|
|
150
166
|
// TL/TR/BR/BL order so the WGSL `cornerX/cornerY` derivation matches.
|
|
@@ -157,7 +173,10 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
157
173
|
_textureBindGroupLayout = null;
|
|
158
174
|
_pipelineLayout = null;
|
|
159
175
|
_uniformBuffer = null;
|
|
160
|
-
|
|
176
|
+
// group(0) bind group = projection UBO + shared transform storage buffer.
|
|
177
|
+
// Recreated whenever the storage buffer identity changes (capacity growth).
|
|
178
|
+
_transformBindGroup = null;
|
|
179
|
+
_transformStorageBuffer = null;
|
|
161
180
|
_indexBuffer = null;
|
|
162
181
|
_instanceBuffer = null;
|
|
163
182
|
_instanceCapacity = 0;
|
|
@@ -169,7 +188,16 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
169
188
|
_textureSlots = new Map();
|
|
170
189
|
_slotCount = 0;
|
|
171
190
|
_instanceCount = 0;
|
|
191
|
+
// Highest transform-storage row referenced by the pending batch; drives the
|
|
192
|
+
// minimum row count uploaded for the storage buffer at flush time.
|
|
193
|
+
_maxNodeIndex = 0;
|
|
172
194
|
_currentBlendMode = null;
|
|
195
|
+
// Custom-material state. Per-material pipelines/bind groups are cached; the
|
|
196
|
+
// current batch's material/base-texture decide when to flush.
|
|
197
|
+
_customMaterials = new Map();
|
|
198
|
+
_customBaseTextureLayout = null;
|
|
199
|
+
_currentMaterial = null;
|
|
200
|
+
_currentBaseTexture = null;
|
|
173
201
|
onConnect(backend) {
|
|
174
202
|
if (this._device) {
|
|
175
203
|
return;
|
|
@@ -185,6 +213,13 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
185
213
|
type: 'uniform',
|
|
186
214
|
},
|
|
187
215
|
},
|
|
216
|
+
{
|
|
217
|
+
binding: 1,
|
|
218
|
+
visibility: GPUShaderStage.VERTEX,
|
|
219
|
+
buffer: {
|
|
220
|
+
type: 'read-only-storage',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
188
223
|
],
|
|
189
224
|
});
|
|
190
225
|
this._textureBindGroupLayout = this._device.createBindGroupLayout({
|
|
@@ -208,21 +243,20 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
208
243
|
this._pipelineLayout = this._device.createPipelineLayout({
|
|
209
244
|
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
210
245
|
});
|
|
246
|
+
// Single base-texture layout for the custom-material path (group 1).
|
|
247
|
+
this._customBaseTextureLayout = this._device.createBindGroupLayout({
|
|
248
|
+
entries: [
|
|
249
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
250
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
251
|
+
],
|
|
252
|
+
});
|
|
211
253
|
this._uniformBuffer = this._device.createBuffer({
|
|
212
254
|
size: projectionByteLength,
|
|
213
255
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
214
256
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
binding: 0,
|
|
220
|
-
resource: {
|
|
221
|
-
buffer: this._uniformBuffer,
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
],
|
|
225
|
-
});
|
|
257
|
+
// The group(0) bind group also binds the shared transform storage buffer,
|
|
258
|
+
// whose identity changes when its capacity grows — so it is built lazily in
|
|
259
|
+
// flush() once the active storage buffer is known.
|
|
226
260
|
// Static index buffer for the quad. Allocated once at connect; its
|
|
227
261
|
// contents never change.
|
|
228
262
|
this._indexBuffer = this._device.createBuffer({
|
|
@@ -235,12 +269,21 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
235
269
|
this._instanceBuffer?.destroy();
|
|
236
270
|
this._indexBuffer?.destroy();
|
|
237
271
|
this._uniformBuffer?.destroy();
|
|
272
|
+
// Custom materials are owned by user code (one SpriteMaterial can be shared
|
|
273
|
+
// across many sprites); their resources are released when the user calls
|
|
274
|
+
// material.destroy(). On disconnect we eagerly release to avoid GPU leaks.
|
|
275
|
+
for (const resources of this._customMaterials.values()) {
|
|
276
|
+
this._releaseCustomResources(resources);
|
|
277
|
+
}
|
|
278
|
+
this._customMaterials.clear();
|
|
238
279
|
this._pipelines.clear();
|
|
239
280
|
this._instanceBuffer = null;
|
|
240
281
|
this._indexBuffer = null;
|
|
241
|
-
this.
|
|
282
|
+
this._transformBindGroup = null;
|
|
283
|
+
this._transformStorageBuffer = null;
|
|
242
284
|
this._uniformBuffer = null;
|
|
243
285
|
this._pipelineLayout = null;
|
|
286
|
+
this._customBaseTextureLayout = null;
|
|
244
287
|
this._textureBindGroupLayout = null;
|
|
245
288
|
this._uniformBindGroupLayout = null;
|
|
246
289
|
this._shaderModule = null;
|
|
@@ -251,7 +294,10 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
251
294
|
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
252
295
|
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
253
296
|
this._instanceCount = 0;
|
|
297
|
+
this._maxNodeIndex = 0;
|
|
254
298
|
this._currentBlendMode = null;
|
|
299
|
+
this._currentMaterial = null;
|
|
300
|
+
this._currentBaseTexture = null;
|
|
255
301
|
this._resetSlots();
|
|
256
302
|
}
|
|
257
303
|
render(sprite) {
|
|
@@ -265,15 +311,33 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
265
311
|
(texture instanceof Texture && texture.source === null)) {
|
|
266
312
|
return;
|
|
267
313
|
}
|
|
314
|
+
const material = sprite.material;
|
|
315
|
+
// The transform lives in the shared storage buffer, keyed by the draw
|
|
316
|
+
// command's stable nodeIndex (already packed at the draw-command boundary).
|
|
317
|
+
// A direct, non-plan `backend.draw(sprite)` has no command — push the
|
|
318
|
+
// sprite's transform into the buffer and use the freshly-allocated slot.
|
|
319
|
+
const command = backend.activeDrawCommand;
|
|
320
|
+
const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
|
|
321
|
+
if (material === null) {
|
|
322
|
+
this._renderDefault(sprite, texture, backend, nodeIndex);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
this._renderCustom(sprite, texture, material, backend, nodeIndex);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Default multi-texture path: rotate the base texture through 8 slots. */
|
|
329
|
+
_renderDefault(sprite, texture, backend, nodeIndex) {
|
|
268
330
|
const blendMode = sprite.blendMode;
|
|
269
|
-
// Flush triggers: blend-mode change,
|
|
270
|
-
//
|
|
331
|
+
// Flush triggers: blend-mode change, texture-slot exhaustion, or a custom
|
|
332
|
+
// batch still in flight that must drain first.
|
|
271
333
|
const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
|
|
272
334
|
const slotExhausted = !this._textureSlots.has(texture) && this._slotCount >= maxBatchTextures;
|
|
273
|
-
|
|
335
|
+
const materialSwitch = this._currentMaterial !== null && this._instanceCount > 0;
|
|
336
|
+
if (blendModeChanged || slotExhausted || materialSwitch) {
|
|
274
337
|
this.flush();
|
|
275
338
|
}
|
|
276
339
|
this._currentBlendMode = blendMode;
|
|
340
|
+
this._currentMaterial = null;
|
|
277
341
|
backend.setBlendMode(blendMode);
|
|
278
342
|
// Resolve / assign texture slot.
|
|
279
343
|
let slot = this._textureSlots.get(texture);
|
|
@@ -288,15 +352,38 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
288
352
|
// typed-array writes in _packInstance silently fall off the end of a
|
|
289
353
|
// too-small buffer.
|
|
290
354
|
this._ensureInstanceCapacity(this._instanceCount + 1);
|
|
291
|
-
this._packInstance(sprite, texture, packedSlotFlags);
|
|
355
|
+
this._packInstance(sprite, texture, packedSlotFlags, nodeIndex);
|
|
356
|
+
this._instanceCount++;
|
|
357
|
+
}
|
|
358
|
+
/** Custom-material path: single base texture on group(1), instanced. */
|
|
359
|
+
_renderCustom(sprite, texture, material, backend, nodeIndex) {
|
|
360
|
+
if (material.shader.wgsl === null) {
|
|
361
|
+
throw new Error('SpriteMaterial shader has no `wgsl` source; cannot render through the WebGPU backend.');
|
|
362
|
+
}
|
|
363
|
+
// The material owns its blend mode; the sprite's own blendMode overrides it
|
|
364
|
+
// when set away from the default (Normal).
|
|
365
|
+
const blendMode = sprite.blendMode === BlendModes.Normal ? material.blendMode : sprite.blendMode;
|
|
366
|
+
const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
|
|
367
|
+
const materialChanged = this._currentMaterial !== null && material !== this._currentMaterial;
|
|
368
|
+
const textureChanged = this._currentBaseTexture !== null && texture !== this._currentBaseTexture;
|
|
369
|
+
const modeSwitch = this._currentMaterial === null && this._instanceCount > 0;
|
|
370
|
+
if (blendModeChanged || materialChanged || textureChanged || modeSwitch) {
|
|
371
|
+
this.flush();
|
|
372
|
+
}
|
|
373
|
+
this._currentBlendMode = blendMode;
|
|
374
|
+
this._currentMaterial = material;
|
|
375
|
+
this._currentBaseTexture = texture;
|
|
376
|
+
backend.setBlendMode(blendMode);
|
|
377
|
+
// textureSlot word is unused by custom fragments (base binds to group(1)).
|
|
378
|
+
this._ensureInstanceCapacity(this._instanceCount + 1);
|
|
379
|
+
this._packInstance(sprite, texture, 0, nodeIndex);
|
|
292
380
|
this._instanceCount++;
|
|
293
381
|
}
|
|
294
382
|
flush() {
|
|
295
383
|
const backend = this._backend;
|
|
296
384
|
const device = this._device;
|
|
297
385
|
const uniformBuffer = this._uniformBuffer;
|
|
298
|
-
|
|
299
|
-
if (!backend || !device || !uniformBuffer || !uniformBindGroup) {
|
|
386
|
+
if (!backend || !device || !uniformBuffer) {
|
|
300
387
|
return;
|
|
301
388
|
}
|
|
302
389
|
if (this._instanceCount === 0 && !backend.clearRequested) {
|
|
@@ -305,34 +392,66 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
305
392
|
const viewMatrix = backend.view.getTransform();
|
|
306
393
|
this._projectionData.set([viewMatrix.a, viewMatrix.c, 0, 0, viewMatrix.b, viewMatrix.d, 0, 0, 0, 0, 1, 0, viewMatrix.x, viewMatrix.y, 0, viewMatrix.z]);
|
|
307
394
|
device.queue.writeBuffer(uniformBuffer, 0, this._projectionData.buffer, this._projectionData.byteOffset, this._projectionData.byteLength);
|
|
308
|
-
const encoder = device.createCommandEncoder();
|
|
309
|
-
const pass = encoder.beginRenderPass({
|
|
310
|
-
colorAttachments: [backend.createColorAttachment()],
|
|
311
|
-
});
|
|
312
|
-
backend.stats.renderPasses++;
|
|
313
395
|
const scissor = backend.getScissorRect();
|
|
314
396
|
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
397
|
+
// The coordinator owns the GPU pass: it opens the encoder + render pass
|
|
398
|
+
// (load/clear resolution, pass count and scissor are applied there) and
|
|
399
|
+
// ends + submits it below.
|
|
400
|
+
const pass = backend._passCoordinator.acquirePass().pass;
|
|
318
401
|
if (this._instanceCount > 0 && !maskClipsAll && this._instanceBuffer !== null && this._indexBuffer !== null && this._currentBlendMode !== null) {
|
|
319
402
|
device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, this._instanceCount * instanceStrideBytes);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
403
|
+
// Resolve the shared transform storage buffer (rows uploaded up to the
|
|
404
|
+
// max nodeIndex referenced by this batch) and bind it alongside the
|
|
405
|
+
// projection UBO on group(0). Both the default and custom programs fetch
|
|
406
|
+
// the world transform from it via nodeIndex.
|
|
407
|
+
const storage = backend.getTransformStorageBuffer(this._maxNodeIndex + 1);
|
|
408
|
+
const transformBindGroup = this._getOrCreateTransformBindGroup(device, uniformBuffer, storage.buffer);
|
|
409
|
+
const material = this._currentMaterial;
|
|
410
|
+
const stencil = backend._passCoordinator.stencilActive;
|
|
411
|
+
if (material === null) {
|
|
412
|
+
const pipeline = this._getPipeline(this._currentBlendMode, backend.renderTargetFormat, stencil);
|
|
413
|
+
const textureBindGroup = this._createTextureBindGroup(device, backend);
|
|
414
|
+
pass.setPipeline(pipeline);
|
|
415
|
+
pass.setBindGroup(0, transformBindGroup);
|
|
416
|
+
pass.setBindGroup(1, textureBindGroup);
|
|
417
|
+
pass.setVertexBuffer(0, this._instanceBuffer);
|
|
418
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16');
|
|
419
|
+
pass.drawIndexed(indicesPerSprite, this._instanceCount, 0, 0, 0);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
pass.pushDebugGroup('SpriteMaterial (custom)');
|
|
423
|
+
this._drawCustomBatch(pass, device, backend, material, transformBindGroup, stencil);
|
|
424
|
+
pass.popDebugGroup();
|
|
425
|
+
}
|
|
328
426
|
backend.stats.batches++;
|
|
329
427
|
backend.stats.drawCalls++;
|
|
330
428
|
}
|
|
331
|
-
|
|
332
|
-
backend.submit(encoder.finish());
|
|
429
|
+
backend._passCoordinator.endPass();
|
|
333
430
|
this._instanceCount = 0;
|
|
431
|
+
this._maxNodeIndex = 0;
|
|
334
432
|
this._resetSlots();
|
|
335
433
|
this._currentBlendMode = null;
|
|
434
|
+
this._currentMaterial = null;
|
|
435
|
+
this._currentBaseTexture = null;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Build (or reuse) the group(0) bind group pairing the fixed projection UBO
|
|
439
|
+
* with the shared transform storage buffer. Cached against the storage buffer
|
|
440
|
+
* identity, which changes only when its capacity grows.
|
|
441
|
+
*/
|
|
442
|
+
_getOrCreateTransformBindGroup(device, uniformBuffer, storageBuffer) {
|
|
443
|
+
if (this._transformBindGroup !== null && this._transformStorageBuffer === storageBuffer) {
|
|
444
|
+
return this._transformBindGroup;
|
|
445
|
+
}
|
|
446
|
+
this._transformStorageBuffer = storageBuffer;
|
|
447
|
+
this._transformBindGroup = device.createBindGroup({
|
|
448
|
+
layout: this._uniformBindGroupLayout,
|
|
449
|
+
entries: [
|
|
450
|
+
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
451
|
+
{ binding: 1, resource: { buffer: storageBuffer } },
|
|
452
|
+
],
|
|
453
|
+
});
|
|
454
|
+
return this._transformBindGroup;
|
|
336
455
|
}
|
|
337
456
|
destroy() {
|
|
338
457
|
this.disconnect();
|
|
@@ -380,24 +499,18 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
380
499
|
}
|
|
381
500
|
await Promise.all(promises);
|
|
382
501
|
}
|
|
383
|
-
_packInstance(sprite, texture, packedSlotFlags) {
|
|
502
|
+
_packInstance(sprite, texture, packedSlotFlags, nodeIndex) {
|
|
384
503
|
const offset = this._instanceCount * wordsPerInstance;
|
|
385
504
|
const f32 = this._instanceFloat32;
|
|
386
505
|
const u32 = this._instanceUint32;
|
|
506
|
+
// localBounds: left, top, right, bottom (words 0..3, offset 0)
|
|
387
507
|
const bounds = sprite.getLocalBounds();
|
|
388
508
|
f32[offset + 0] = bounds.left;
|
|
389
509
|
f32[offset + 1] = bounds.top;
|
|
390
510
|
f32[offset + 2] = bounds.right;
|
|
391
511
|
f32[offset + 3] = bounds.bottom;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
f32[offset + 5] = transform.b;
|
|
395
|
-
f32[offset + 6] = transform.x;
|
|
396
|
-
f32[offset + 7] = transform.c;
|
|
397
|
-
f32[offset + 8] = transform.d;
|
|
398
|
-
f32[offset + 9] = transform.y;
|
|
399
|
-
// uvBounds: u16x4 normalised, packed into two u32 slots. The CPU
|
|
400
|
-
// applies the flipY swap so the shader stays orientation-agnostic.
|
|
512
|
+
// uvBounds: u16x4 normalised, packed into two u32 slots (words 4,5, offset
|
|
513
|
+
// 16). The CPU applies the flipY swap so the shader stays orientation-agnostic.
|
|
401
514
|
const frame = sprite.textureFrame;
|
|
402
515
|
const texWidth = texture.width;
|
|
403
516
|
const texHeight = texture.height;
|
|
@@ -408,10 +521,18 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
408
521
|
const flipY = texture instanceof Texture && texture.flipY;
|
|
409
522
|
const vMin = flipY ? vMaxRaw : vMinRaw;
|
|
410
523
|
const vMax = flipY ? vMinRaw : vMaxRaw;
|
|
411
|
-
u32[offset +
|
|
412
|
-
u32[offset +
|
|
413
|
-
|
|
414
|
-
u32[offset +
|
|
524
|
+
u32[offset + 4] = uMin | (vMin << 16);
|
|
525
|
+
u32[offset + 5] = uMax | (vMax << 16);
|
|
526
|
+
// color (u8x4 packed) at word 6 (offset 24)
|
|
527
|
+
u32[offset + 6] = sprite.tint.toRgba();
|
|
528
|
+
// packedSlotFlags (u32) at word 7 (offset 28)
|
|
529
|
+
u32[offset + 7] = packedSlotFlags;
|
|
530
|
+
// nodeIndex (u32) at word 8 (offset 32) — row into the shared transform buffer.
|
|
531
|
+
const node = nodeIndex >>> 0;
|
|
532
|
+
u32[offset + 8] = node;
|
|
533
|
+
if (node > this._maxNodeIndex) {
|
|
534
|
+
this._maxNodeIndex = node;
|
|
535
|
+
}
|
|
415
536
|
}
|
|
416
537
|
_ensureInstanceCapacity(instanceCount) {
|
|
417
538
|
if (!this._device || instanceCount <= this._instanceCapacity) {
|
|
@@ -481,8 +602,8 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
481
602
|
entries,
|
|
482
603
|
});
|
|
483
604
|
}
|
|
484
|
-
_getPipeline(blendMode, format) {
|
|
485
|
-
const pipelineKey = `${blendMode}:${format}`;
|
|
605
|
+
_getPipeline(blendMode, format, stencil) {
|
|
606
|
+
const pipelineKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
|
|
486
607
|
const existingPipeline = this._pipelines.get(pipelineKey);
|
|
487
608
|
if (existingPipeline) {
|
|
488
609
|
return existingPipeline;
|
|
@@ -490,15 +611,15 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
490
611
|
if (!this._device || !this._shaderModule || !this._pipelineLayout || !this._backend) {
|
|
491
612
|
throw new Error('Renderer has to be connected first!');
|
|
492
613
|
}
|
|
493
|
-
const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(blendMode, format));
|
|
614
|
+
const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(blendMode, format, stencil));
|
|
494
615
|
this._pipelines.set(pipelineKey, pipeline);
|
|
495
616
|
return pipeline;
|
|
496
617
|
}
|
|
497
|
-
_buildPipelineDescriptor(blendMode, format) {
|
|
618
|
+
_buildPipelineDescriptor(blendMode, format, stencil = false) {
|
|
498
619
|
if (!this._shaderModule || !this._pipelineLayout) {
|
|
499
620
|
throw new Error('Renderer has to be connected first!');
|
|
500
621
|
}
|
|
501
|
-
|
|
622
|
+
const descriptor = {
|
|
502
623
|
layout: this._pipelineLayout,
|
|
503
624
|
vertex: {
|
|
504
625
|
module: this._shaderModule,
|
|
@@ -513,29 +634,24 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
513
634
|
offset: 0,
|
|
514
635
|
format: 'float32x4',
|
|
515
636
|
},
|
|
516
|
-
{
|
|
517
|
-
shaderLocation: 1,
|
|
518
|
-
offset: 16,
|
|
519
|
-
format: 'float32x3',
|
|
520
|
-
},
|
|
521
|
-
{
|
|
522
|
-
shaderLocation: 2,
|
|
523
|
-
offset: 28,
|
|
524
|
-
format: 'float32x3',
|
|
525
|
-
},
|
|
526
637
|
{
|
|
527
638
|
shaderLocation: 3,
|
|
528
|
-
offset:
|
|
639
|
+
offset: 16,
|
|
529
640
|
format: 'unorm16x4',
|
|
530
641
|
},
|
|
531
642
|
{
|
|
532
643
|
shaderLocation: 4,
|
|
533
|
-
offset:
|
|
644
|
+
offset: 24,
|
|
534
645
|
format: 'unorm8x4',
|
|
535
646
|
},
|
|
536
647
|
{
|
|
537
648
|
shaderLocation: 5,
|
|
538
|
-
offset:
|
|
649
|
+
offset: 28,
|
|
650
|
+
format: 'uint32',
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
shaderLocation: 6,
|
|
654
|
+
offset: 32,
|
|
539
655
|
format: 'uint32',
|
|
540
656
|
},
|
|
541
657
|
],
|
|
@@ -557,7 +673,245 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
557
673
|
topology: 'triangle-list',
|
|
558
674
|
},
|
|
559
675
|
};
|
|
676
|
+
if (stencil) {
|
|
677
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
678
|
+
}
|
|
679
|
+
return descriptor;
|
|
680
|
+
}
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
// Custom-material path
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
_drawCustomBatch(pass, device, backend, material, transformBindGroup, stencil) {
|
|
685
|
+
const resources = this._getOrCreateCustomResources(material, device);
|
|
686
|
+
const baseTexture = this._currentBaseTexture ?? Texture.empty;
|
|
687
|
+
// Re-built every frame so mutations to material.uniforms.X are picked up.
|
|
688
|
+
this._uploadUserUniforms(material, resources, device);
|
|
689
|
+
const pipeline = this._getOrCreateCustomPipeline(resources, this._currentBlendMode, backend.renderTargetFormat, stencil, device);
|
|
690
|
+
pass.setPipeline(pipeline);
|
|
691
|
+
pass.setBindGroup(0, transformBindGroup);
|
|
692
|
+
pass.setBindGroup(1, this._getCustomBaseTextureBindGroup(resources, backend, baseTexture, device));
|
|
693
|
+
pass.setBindGroup(2, this._buildUserBindGroup(material, resources, backend, device));
|
|
694
|
+
pass.setVertexBuffer(0, this._instanceBuffer);
|
|
695
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16');
|
|
696
|
+
pass.drawIndexed(indicesPerSprite, this._instanceCount, 0, 0, 0);
|
|
697
|
+
}
|
|
698
|
+
_getOrCreateCustomResources(material, device) {
|
|
699
|
+
const existing = this._customMaterials.get(material);
|
|
700
|
+
if (existing !== undefined) {
|
|
701
|
+
return existing;
|
|
702
|
+
}
|
|
703
|
+
const wgsl = material.shader.wgsl;
|
|
704
|
+
if (wgsl === null) {
|
|
705
|
+
throw new Error('SpriteMaterial shader has no `wgsl` source; cannot render through the WebGPU backend.');
|
|
706
|
+
}
|
|
707
|
+
// The engine owns the vertex stage: prepend the canonical sprite vertex
|
|
708
|
+
// module (VertexInput/VertexOutput, group(0) projection + transform storage,
|
|
709
|
+
// group(1) base texture + sampler) to the material's fragment WGSL.
|
|
710
|
+
const shaderModule = device.createShaderModule({ code: `${spriteVertexWgsl}\n${wgsl}` });
|
|
711
|
+
const userLayout = this._buildUserBindGroupLayout(device, material);
|
|
712
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
713
|
+
bindGroupLayouts: [this._uniformBindGroupLayout, this._customBaseTextureLayout, userLayout],
|
|
714
|
+
});
|
|
715
|
+
const resources = {
|
|
716
|
+
shaderModule,
|
|
717
|
+
userLayout,
|
|
718
|
+
pipelineLayout,
|
|
719
|
+
pipelines: new Map(),
|
|
720
|
+
userUniformBuffer: null,
|
|
721
|
+
userUniformBufferCapacity: 0,
|
|
722
|
+
baseTextureBindGroups: new WeakMap(),
|
|
723
|
+
};
|
|
724
|
+
this._customMaterials.set(material, resources);
|
|
725
|
+
material._onDispose(() => {
|
|
726
|
+
const stored = this._customMaterials.get(material);
|
|
727
|
+
if (stored !== undefined) {
|
|
728
|
+
this._releaseCustomResources(stored);
|
|
729
|
+
this._customMaterials.delete(material);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
return resources;
|
|
733
|
+
}
|
|
734
|
+
_getOrCreateCustomPipeline(resources, blendMode, format, stencil, device) {
|
|
735
|
+
const cacheKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
|
|
736
|
+
const existing = resources.pipelines.get(cacheKey);
|
|
737
|
+
if (existing !== undefined) {
|
|
738
|
+
return existing;
|
|
739
|
+
}
|
|
740
|
+
const descriptor = {
|
|
741
|
+
layout: resources.pipelineLayout,
|
|
742
|
+
vertex: {
|
|
743
|
+
module: resources.shaderModule,
|
|
744
|
+
entryPoint: 'vertexMain',
|
|
745
|
+
buffers: [
|
|
746
|
+
{
|
|
747
|
+
arrayStride: instanceStrideBytes,
|
|
748
|
+
stepMode: 'instance',
|
|
749
|
+
attributes: [
|
|
750
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x4' },
|
|
751
|
+
{ shaderLocation: 3, offset: 16, format: 'unorm16x4' },
|
|
752
|
+
{ shaderLocation: 4, offset: 24, format: 'unorm8x4' },
|
|
753
|
+
{ shaderLocation: 5, offset: 28, format: 'uint32' },
|
|
754
|
+
{ shaderLocation: 6, offset: 32, format: 'uint32' },
|
|
755
|
+
],
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
},
|
|
759
|
+
fragment: {
|
|
760
|
+
module: resources.shaderModule,
|
|
761
|
+
entryPoint: 'fragmentMain',
|
|
762
|
+
targets: [
|
|
763
|
+
{
|
|
764
|
+
format,
|
|
765
|
+
blend: getWebGpuBlendState(blendMode),
|
|
766
|
+
writeMask: GPUColorWrite.ALL,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
},
|
|
770
|
+
primitive: {
|
|
771
|
+
topology: 'triangle-list',
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
if (stencil) {
|
|
775
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
776
|
+
}
|
|
777
|
+
const pipeline = device.createRenderPipeline(descriptor);
|
|
778
|
+
resources.pipelines.set(cacheKey, pipeline);
|
|
779
|
+
return pipeline;
|
|
780
|
+
}
|
|
781
|
+
_getCustomBaseTextureBindGroup(resources, backend, texture, device) {
|
|
782
|
+
const existing = resources.baseTextureBindGroups.get(texture);
|
|
783
|
+
if (existing !== undefined) {
|
|
784
|
+
return existing;
|
|
785
|
+
}
|
|
786
|
+
const binding = backend.getTextureBinding(texture);
|
|
787
|
+
const group = device.createBindGroup({
|
|
788
|
+
layout: this._customBaseTextureLayout,
|
|
789
|
+
entries: [
|
|
790
|
+
{ binding: 0, resource: binding.view },
|
|
791
|
+
{ binding: 1, resource: binding.sampler },
|
|
792
|
+
],
|
|
793
|
+
});
|
|
794
|
+
resources.baseTextureBindGroups.set(texture, group);
|
|
795
|
+
return group;
|
|
796
|
+
}
|
|
797
|
+
_buildUserBindGroupLayout(device, material) {
|
|
798
|
+
const entries = [];
|
|
799
|
+
// Binding 0 always reserved for the user UBO (even if empty), so the layout
|
|
800
|
+
// is stable across user-uniform mutations.
|
|
801
|
+
entries.push({
|
|
802
|
+
binding: 0,
|
|
803
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
804
|
+
buffer: { type: 'uniform' },
|
|
805
|
+
});
|
|
806
|
+
const textureBindings = collectTextureBindings(material);
|
|
807
|
+
if (textureBindings.length > maxCustomTextureSlots) {
|
|
808
|
+
throw new Error(`SpriteMaterial requested more than ${maxCustomTextureSlots} user texture bindings.`);
|
|
809
|
+
}
|
|
810
|
+
let bindingIndex = 1;
|
|
811
|
+
for (let t = 0; t < textureBindings.length; t++) {
|
|
812
|
+
entries.push({ binding: bindingIndex, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } });
|
|
813
|
+
bindingIndex++;
|
|
814
|
+
entries.push({ binding: bindingIndex, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } });
|
|
815
|
+
bindingIndex++;
|
|
816
|
+
}
|
|
817
|
+
return device.createBindGroupLayout({ entries });
|
|
818
|
+
}
|
|
819
|
+
_uploadUserUniforms(material, resources, device) {
|
|
820
|
+
const scalarValues = collectScalarUniforms(material);
|
|
821
|
+
// Always create a UBO (even if empty) since binding 0 of the user layout is
|
|
822
|
+
// fixed. Min size 16 bytes to satisfy WebGPU's minimum buffer size.
|
|
823
|
+
const slotCount = Math.max(scalarValues.length, 1);
|
|
824
|
+
const bufferBytes = slotCount * 16;
|
|
825
|
+
if (resources.userUniformBuffer === null || resources.userUniformBufferCapacity < bufferBytes) {
|
|
826
|
+
resources.userUniformBuffer?.destroy();
|
|
827
|
+
resources.userUniformBufferCapacity = bufferBytes;
|
|
828
|
+
resources.userUniformBuffer = device.createBuffer({
|
|
829
|
+
size: bufferBytes,
|
|
830
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
const data = new Float32Array(bufferBytes / 4);
|
|
834
|
+
let slot = 0;
|
|
835
|
+
for (const value of scalarValues) {
|
|
836
|
+
const baseFloatIndex = slot * 4;
|
|
837
|
+
if (typeof value === 'number') {
|
|
838
|
+
data[baseFloatIndex] = value;
|
|
839
|
+
}
|
|
840
|
+
else if (value instanceof Float32Array) {
|
|
841
|
+
data.set(value, baseFloatIndex);
|
|
842
|
+
}
|
|
843
|
+
else if (value instanceof Int32Array) {
|
|
844
|
+
for (let i = 0; i < value.length; i++) {
|
|
845
|
+
data[baseFloatIndex + i] = value[i];
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
const arr = value;
|
|
850
|
+
for (let i = 0; i < arr.length; i++) {
|
|
851
|
+
data[baseFloatIndex + i] = arr[i];
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
slot++;
|
|
855
|
+
}
|
|
856
|
+
device.queue.writeBuffer(resources.userUniformBuffer, 0, data);
|
|
857
|
+
}
|
|
858
|
+
_buildUserBindGroup(material, resources, backend, device) {
|
|
859
|
+
const entries = [];
|
|
860
|
+
entries.push({ binding: 0, resource: { buffer: resources.userUniformBuffer } });
|
|
861
|
+
let bindingIndex = 1;
|
|
862
|
+
for (const texture of collectTextureBindings(material)) {
|
|
863
|
+
const binding = backend.getTextureBinding(texture);
|
|
864
|
+
entries.push({ binding: bindingIndex, resource: binding.view });
|
|
865
|
+
bindingIndex++;
|
|
866
|
+
entries.push({ binding: bindingIndex, resource: binding.sampler });
|
|
867
|
+
bindingIndex++;
|
|
868
|
+
}
|
|
869
|
+
return device.createBindGroup({ layout: resources.userLayout, entries });
|
|
870
|
+
}
|
|
871
|
+
_releaseCustomResources(resources) {
|
|
872
|
+
resources.userUniformBuffer?.destroy();
|
|
873
|
+
resources.pipelines.clear();
|
|
874
|
+
resources.userUniformBuffer = null;
|
|
875
|
+
resources.userUniformBufferCapacity = 0;
|
|
876
|
+
resources.baseTextureBindGroups = new WeakMap();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function isTextureUniform(value) {
|
|
880
|
+
return (typeof value === 'object' &&
|
|
881
|
+
value !== null &&
|
|
882
|
+
'width' in value &&
|
|
883
|
+
'height' in value &&
|
|
884
|
+
!(value instanceof Float32Array) &&
|
|
885
|
+
!(value instanceof Int32Array) &&
|
|
886
|
+
!Array.isArray(value));
|
|
887
|
+
}
|
|
888
|
+
/** Scalar/vector/matrix uniforms (texture values excluded) in declaration order. */
|
|
889
|
+
function collectScalarUniforms(material) {
|
|
890
|
+
const result = [];
|
|
891
|
+
for (const value of Object.values(material.uniforms)) {
|
|
892
|
+
if (!isTextureUniform(value)) {
|
|
893
|
+
result.push(value);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Texture bindings claimed by the material, in a stable order: texture-valued
|
|
900
|
+
* entries of `uniforms` first (declaration order), then the dedicated
|
|
901
|
+
* `textures` map. The WGSL source must declare its `@group(2)` texture/sampler
|
|
902
|
+
* pairs in this same order.
|
|
903
|
+
*/
|
|
904
|
+
function collectTextureBindings(material) {
|
|
905
|
+
const result = [];
|
|
906
|
+
for (const value of Object.values(material.uniforms)) {
|
|
907
|
+
if (isTextureUniform(value)) {
|
|
908
|
+
result.push(value);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
for (const texture of Object.values(material.textures)) {
|
|
912
|
+
result.push(texture);
|
|
560
913
|
}
|
|
914
|
+
return result;
|
|
561
915
|
}
|
|
562
916
|
|
|
563
917
|
export { WebGpuSpriteRenderer };
|