@codexo/exojs 0.4.0 → 0.6.2
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 +499 -163
- package/README.md +156 -141
- package/dist/esm/audio/AudioAnalyser.d.ts +0 -1
- package/dist/esm/audio/AudioAnalyser.js +0 -2
- package/dist/esm/audio/AudioAnalyser.js.map +1 -1
- package/dist/esm/core/Application.d.ts +4 -4
- package/dist/esm/core/Application.js +19 -19
- package/dist/esm/core/Application.js.map +1 -1
- package/dist/esm/core/Scene.d.ts +59 -24
- package/dist/esm/core/Scene.js +60 -18
- package/dist/esm/core/Scene.js.map +1 -1
- package/dist/esm/core/SceneManager.js +15 -9
- package/dist/esm/core/SceneManager.js.map +1 -1
- package/dist/esm/core/SceneNode.d.ts +45 -5
- package/dist/esm/core/SceneNode.js +136 -7
- package/dist/esm/core/SceneNode.js.map +1 -1
- package/dist/esm/index.js +6 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/math/index.d.ts +0 -1
- package/dist/esm/rendering/CallbackRenderPass.d.ts +3 -3
- package/dist/esm/rendering/CallbackRenderPass.js +2 -2
- package/dist/esm/rendering/CallbackRenderPass.js.map +1 -1
- package/dist/esm/rendering/Container.d.ts +10 -11
- package/dist/esm/rendering/Container.js +5 -5
- package/dist/esm/rendering/Container.js.map +1 -1
- package/dist/esm/rendering/Drawable.d.ts +2 -2
- package/dist/esm/rendering/Drawable.js +5 -5
- package/dist/esm/rendering/Drawable.js.map +1 -1
- package/dist/esm/rendering/{SceneRenderRuntime.d.ts → RenderBackend.d.ts} +21 -3
- package/dist/esm/rendering/RenderNode.d.ts +41 -5
- package/dist/esm/rendering/RenderNode.js +89 -24
- package/dist/esm/rendering/RenderNode.js.map +1 -1
- package/dist/esm/rendering/RenderPass.d.ts +2 -2
- package/dist/esm/rendering/RenderTargetPass.d.ts +3 -3
- package/dist/esm/rendering/RenderTargetPass.js +9 -9
- package/dist/esm/rendering/RenderTargetPass.js.map +1 -1
- package/dist/esm/rendering/Renderer.d.ts +3 -3
- package/dist/esm/rendering/RendererRegistry.d.ts +13 -7
- package/dist/esm/rendering/RendererRegistry.js +18 -10
- package/dist/esm/rendering/RendererRegistry.js.map +1 -1
- package/dist/esm/rendering/filters/BlurFilter.d.ts +2 -2
- package/dist/esm/rendering/filters/BlurFilter.js +5 -5
- package/dist/esm/rendering/filters/BlurFilter.js.map +1 -1
- package/dist/esm/rendering/filters/ColorFilter.d.ts +2 -2
- package/dist/esm/rendering/filters/ColorFilter.js +3 -3
- package/dist/esm/rendering/filters/ColorFilter.js.map +1 -1
- package/dist/esm/rendering/filters/Filter.d.ts +2 -2
- package/dist/esm/rendering/index.d.ts +9 -6
- package/dist/esm/rendering/mesh/Mesh.d.ts +69 -0
- package/dist/esm/rendering/mesh/Mesh.js +114 -0
- package/dist/esm/rendering/mesh/Mesh.js.map +1 -0
- package/dist/esm/rendering/primitives/Graphics.d.ts +3 -3
- package/dist/esm/rendering/primitives/Graphics.js.map +1 -1
- package/dist/esm/rendering/shader/Shader.d.ts +3 -3
- package/dist/esm/rendering/shader/Shader.js +10 -10
- package/dist/esm/rendering/text/Text.d.ts +2 -2
- package/dist/esm/rendering/text/Text.js +2 -2
- package/dist/esm/rendering/text/Text.js.map +1 -1
- package/dist/esm/rendering/texture/Sampler.d.ts +0 -3
- package/dist/esm/rendering/texture/Sampler.js +5 -7
- package/dist/esm/rendering/texture/Sampler.js.map +1 -1
- package/dist/esm/rendering/types.d.ts +4 -0
- package/dist/esm/rendering/types.js +4 -0
- package/dist/esm/rendering/types.js.map +1 -1
- package/dist/esm/rendering/video/Video.d.ts +2 -2
- package/dist/esm/rendering/video/Video.js +2 -2
- package/dist/esm/rendering/video/Video.js.map +1 -1
- package/dist/esm/rendering/webgl2/AbstractWebGl2BatchedRenderer.d.ts +2 -2
- package/dist/esm/rendering/webgl2/AbstractWebGl2BatchedRenderer.js +35 -11
- package/dist/esm/rendering/webgl2/AbstractWebGl2BatchedRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/AbstractWebGl2Renderer.d.ts +13 -13
- package/dist/esm/rendering/webgl2/AbstractWebGl2Renderer.js +20 -20
- package/dist/esm/rendering/webgl2/AbstractWebGl2Renderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/{WebGl2RenderManager.d.ts → WebGl2Backend.d.ts} +15 -12
- package/dist/esm/rendering/webgl2/{WebGl2RenderManager.js → WebGl2Backend.js} +63 -38
- package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2MaskCompositor.d.ts +31 -0
- package/dist/esm/rendering/webgl2/WebGl2MaskCompositor.js +186 -0
- package/dist/esm/rendering/webgl2/WebGl2MaskCompositor.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.d.ts +27 -0
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js +242 -0
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.d.ts +38 -7
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js +281 -90
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.d.ts +2 -2
- package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.js +15 -10
- package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2ShaderMappings.js +12 -0
- package/dist/esm/rendering/webgl2/WebGl2ShaderMappings.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2ShaderProgram.d.ts +2 -0
- package/dist/esm/rendering/webgl2/{WebGl2ShaderRuntime.js → WebGl2ShaderProgram.js} +58 -18
- package/dist/esm/rendering/webgl2/WebGl2ShaderProgram.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.d.ts +26 -7
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js +260 -62
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2VertexArrayObject.d.ts +24 -1
- package/dist/esm/rendering/webgl2/WebGl2VertexArrayObject.js +6 -2
- package/dist/esm/rendering/webgl2/WebGl2VertexArrayObject.js.map +1 -1
- package/dist/esm/rendering/webgl2/glsl/mask-compose.frag.js +4 -0
- package/dist/esm/rendering/webgl2/glsl/mask-compose.frag.js.map +1 -0
- package/dist/esm/rendering/webgl2/glsl/mask-compose.vert.js +4 -0
- package/dist/esm/rendering/webgl2/glsl/mask-compose.vert.js.map +1 -0
- package/dist/esm/rendering/webgl2/glsl/mesh.frag.js +4 -0
- package/dist/esm/rendering/webgl2/glsl/mesh.frag.js.map +1 -0
- package/dist/esm/rendering/webgl2/glsl/mesh.vert.js +4 -0
- package/dist/esm/rendering/webgl2/glsl/mesh.vert.js.map +1 -0
- package/dist/esm/rendering/webgl2/glsl/particle.vert.js +1 -1
- package/dist/esm/rendering/webgl2/glsl/sprite.frag.js +1 -1
- package/dist/esm/rendering/webgl2/glsl/sprite.vert.js +1 -1
- package/dist/esm/rendering/webgpu/AbstractWebGpuRenderer.d.ts +9 -9
- package/dist/esm/rendering/webgpu/AbstractWebGpuRenderer.js +18 -18
- package/dist/esm/rendering/webgpu/AbstractWebGpuRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/{WebGpuRenderManager.d.ts → WebGpuBackend.d.ts} +17 -14
- package/dist/esm/rendering/webgpu/{WebGpuRenderManager.js → WebGpuBackend.js} +77 -40
- package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.d.ts +37 -0
- package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js +279 -0
- package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.d.ts +40 -0
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js +439 -0
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.d.ts +2 -3
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +65 -82
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.d.ts +2 -3
- package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js +24 -25
- package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.d.ts +28 -13
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js +410 -382
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js.map +1 -1
- package/dist/esm/resources/Loader.js +5 -3
- package/dist/esm/resources/Loader.js.map +1 -1
- package/dist/exo.esm.js +3574 -1696
- package/dist/exo.esm.js.map +1 -1
- package/package.json +20 -11
- package/dist/esm/math/Transformable.d.ts +0 -47
- package/dist/esm/math/Transformable.js +0 -140
- package/dist/esm/math/Transformable.js.map +0 -1
- package/dist/esm/rendering/webgl2/WebGl2RenderManager.js.map +0 -1
- package/dist/esm/rendering/webgl2/WebGl2RendererRuntime.d.ts +0 -15
- package/dist/esm/rendering/webgl2/WebGl2ShaderRuntime.d.ts +0 -2
- package/dist/esm/rendering/webgl2/WebGl2ShaderRuntime.js.map +0 -1
- package/dist/esm/rendering/webgpu/WebGpuRenderManager.js.map +0 -1
- package/dist/esm/rendering/webgpu/WebGpuRendererRuntime.d.ts +0 -8
- package/dist/exo.esm.min.js +0 -2
- package/dist/exo.esm.min.js.map +0 -1
- package/dist/exo.global.js +0 -17328
- package/dist/exo.global.js.map +0 -1
- package/dist/exo.global.min.js +0 -2
- package/dist/exo.global.min.js.map +0 -1
|
@@ -1,137 +1,156 @@
|
|
|
1
1
|
import { AbstractWebGpuRenderer } from './AbstractWebGpuRenderer.js';
|
|
2
2
|
import { Texture } from '../texture/Texture.js';
|
|
3
3
|
import { RenderTexture } from '../texture/RenderTexture.js';
|
|
4
|
+
import { BlendModes } from '../types.js';
|
|
4
5
|
import { getWebGpuBlendState } from './WebGpuBlendState.js';
|
|
5
6
|
|
|
6
7
|
/// <reference types="@webgpu/types" />
|
|
7
|
-
const spriteShaderSource = `
|
|
8
|
-
struct ProjectionUniforms {
|
|
9
|
-
matrix: mat4x4<f32>,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
@group(0) @binding(0)
|
|
13
|
-
var<uniform> projection: ProjectionUniforms;
|
|
14
|
-
|
|
15
|
-
@group(1) @binding(0)
|
|
16
|
-
var spriteTexture0: texture_2d<f32>;
|
|
17
|
-
@group(1) @binding(1)
|
|
18
|
-
var spriteTexture1: texture_2d<f32>;
|
|
19
|
-
@group(1) @binding(2)
|
|
20
|
-
var spriteTexture2: texture_2d<f32>;
|
|
21
|
-
@group(1) @binding(3)
|
|
22
|
-
var spriteTexture3: texture_2d<f32>;
|
|
23
|
-
@group(1) @binding(4)
|
|
24
|
-
var spriteTexture4: texture_2d<f32>;
|
|
25
|
-
@group(1) @binding(5)
|
|
26
|
-
var spriteTexture5: texture_2d<f32>;
|
|
27
|
-
@group(1) @binding(6)
|
|
28
|
-
var spriteTexture6: texture_2d<f32>;
|
|
29
|
-
@group(1) @binding(7)
|
|
30
|
-
var spriteTexture7: texture_2d<f32>;
|
|
31
|
-
|
|
32
|
-
@group(1) @binding(8)
|
|
33
|
-
var spriteSampler0: sampler;
|
|
34
|
-
@group(1) @binding(9)
|
|
35
|
-
var spriteSampler1: sampler;
|
|
36
|
-
@group(1) @binding(10)
|
|
37
|
-
var spriteSampler2: sampler;
|
|
38
|
-
@group(1) @binding(11)
|
|
39
|
-
var spriteSampler3: sampler;
|
|
40
|
-
@group(1) @binding(12)
|
|
41
|
-
var spriteSampler4: sampler;
|
|
42
|
-
@group(1) @binding(13)
|
|
43
|
-
var spriteSampler5: sampler;
|
|
44
|
-
@group(1) @binding(14)
|
|
45
|
-
var spriteSampler6: sampler;
|
|
46
|
-
@group(1) @binding(15)
|
|
47
|
-
var spriteSampler7: sampler;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@location(
|
|
54
|
-
@location(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
output
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
8
|
+
const spriteShaderSource = `
|
|
9
|
+
struct ProjectionUniforms {
|
|
10
|
+
matrix: mat4x4<f32>,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
@group(0) @binding(0)
|
|
14
|
+
var<uniform> projection: ProjectionUniforms;
|
|
15
|
+
|
|
16
|
+
@group(1) @binding(0)
|
|
17
|
+
var spriteTexture0: texture_2d<f32>;
|
|
18
|
+
@group(1) @binding(1)
|
|
19
|
+
var spriteTexture1: texture_2d<f32>;
|
|
20
|
+
@group(1) @binding(2)
|
|
21
|
+
var spriteTexture2: texture_2d<f32>;
|
|
22
|
+
@group(1) @binding(3)
|
|
23
|
+
var spriteTexture3: texture_2d<f32>;
|
|
24
|
+
@group(1) @binding(4)
|
|
25
|
+
var spriteTexture4: texture_2d<f32>;
|
|
26
|
+
@group(1) @binding(5)
|
|
27
|
+
var spriteTexture5: texture_2d<f32>;
|
|
28
|
+
@group(1) @binding(6)
|
|
29
|
+
var spriteTexture6: texture_2d<f32>;
|
|
30
|
+
@group(1) @binding(7)
|
|
31
|
+
var spriteTexture7: texture_2d<f32>;
|
|
32
|
+
|
|
33
|
+
@group(1) @binding(8)
|
|
34
|
+
var spriteSampler0: sampler;
|
|
35
|
+
@group(1) @binding(9)
|
|
36
|
+
var spriteSampler1: sampler;
|
|
37
|
+
@group(1) @binding(10)
|
|
38
|
+
var spriteSampler2: sampler;
|
|
39
|
+
@group(1) @binding(11)
|
|
40
|
+
var spriteSampler3: sampler;
|
|
41
|
+
@group(1) @binding(12)
|
|
42
|
+
var spriteSampler4: sampler;
|
|
43
|
+
@group(1) @binding(13)
|
|
44
|
+
var spriteSampler5: sampler;
|
|
45
|
+
@group(1) @binding(14)
|
|
46
|
+
var spriteSampler6: sampler;
|
|
47
|
+
@group(1) @binding(15)
|
|
48
|
+
var spriteSampler7: sampler;
|
|
49
|
+
|
|
50
|
+
// Per-instance vertex layout (56 bytes per sprite). The four corners
|
|
51
|
+
// of the quad are derived from @builtin(vertex_index) 0..3 inside the
|
|
52
|
+
// vertex shader — there is no per-vertex stream.
|
|
53
|
+
struct VertexInput {
|
|
54
|
+
@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
|
+
@location(3) uvBounds: vec4<f32>, // uMin, vMin, uMax, vMax (CPU pre-swaps for flipY)
|
|
58
|
+
@location(4) color: vec4<f32>, // RGBA tint
|
|
59
|
+
@location(5) packedSlotFlags: u32, // bits 0..7 = slot, bit 8 = premultiply
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
struct VertexOutput {
|
|
63
|
+
@builtin(position) position: vec4<f32>,
|
|
64
|
+
@location(0) texcoord: vec2<f32>,
|
|
65
|
+
@location(1) color: vec4<f32>,
|
|
66
|
+
@location(2) @interpolate(flat) premultiplySample: u32,
|
|
67
|
+
@location(3) @interpolate(flat) textureSlot: u32,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
@vertex
|
|
71
|
+
fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutput {
|
|
72
|
+
var output: VertexOutput;
|
|
73
|
+
|
|
74
|
+
// vid 0..3 → corners in TL, TR, BR, BL order (matches the static index
|
|
75
|
+
// buffer [0,1,2,0,2,3] used for indexed triangle-list drawing).
|
|
76
|
+
let cornerX = ((vid + 1u) >> 1u) & 1u;
|
|
77
|
+
let cornerY = vid >> 1u;
|
|
78
|
+
|
|
79
|
+
let localX = select(input.localBounds.x, input.localBounds.z, cornerX == 1u);
|
|
80
|
+
let localY = select(input.localBounds.y, input.localBounds.w, cornerY == 1u);
|
|
81
|
+
|
|
82
|
+
let worldX = input.transformAB.x * localX + input.transformAB.y * localY + input.transformAB.z;
|
|
83
|
+
let worldY = input.transformCD.x * localX + input.transformCD.y * localY + input.transformCD.z;
|
|
84
|
+
|
|
85
|
+
output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
|
|
86
|
+
|
|
87
|
+
let u = select(input.uvBounds.x, input.uvBounds.z, cornerX == 1u);
|
|
88
|
+
let v = select(input.uvBounds.y, input.uvBounds.w, cornerY == 1u);
|
|
89
|
+
output.texcoord = vec2<f32>(u, v);
|
|
90
|
+
|
|
91
|
+
output.color = vec4(input.color.rgb * input.color.a, input.color.a);
|
|
92
|
+
output.textureSlot = input.packedSlotFlags & 0xFFu;
|
|
93
|
+
output.premultiplySample = (input.packedSlotFlags >> 8u) & 1u;
|
|
94
|
+
|
|
95
|
+
return output;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn sampleTexture(slot: u32, uv: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> {
|
|
99
|
+
switch slot {
|
|
100
|
+
case 0u: {
|
|
101
|
+
return textureSampleGrad(spriteTexture0, spriteSampler0, uv, ddx, ddy);
|
|
102
|
+
}
|
|
103
|
+
case 1u: {
|
|
104
|
+
return textureSampleGrad(spriteTexture1, spriteSampler1, uv, ddx, ddy);
|
|
105
|
+
}
|
|
106
|
+
case 2u: {
|
|
107
|
+
return textureSampleGrad(spriteTexture2, spriteSampler2, uv, ddx, ddy);
|
|
108
|
+
}
|
|
109
|
+
case 3u: {
|
|
110
|
+
return textureSampleGrad(spriteTexture3, spriteSampler3, uv, ddx, ddy);
|
|
111
|
+
}
|
|
112
|
+
case 4u: {
|
|
113
|
+
return textureSampleGrad(spriteTexture4, spriteSampler4, uv, ddx, ddy);
|
|
114
|
+
}
|
|
115
|
+
case 5u: {
|
|
116
|
+
return textureSampleGrad(spriteTexture5, spriteSampler5, uv, ddx, ddy);
|
|
117
|
+
}
|
|
118
|
+
case 6u: {
|
|
119
|
+
return textureSampleGrad(spriteTexture6, spriteSampler6, uv, ddx, ddy);
|
|
120
|
+
}
|
|
121
|
+
default: {
|
|
122
|
+
return textureSampleGrad(spriteTexture7, spriteSampler7, uv, ddx, ddy);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@fragment
|
|
128
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
129
|
+
// Compute screen-space derivatives in uniform control flow before the
|
|
130
|
+
// per-slot switch. WGSL requires textureSample (implicit LOD) to run in
|
|
131
|
+
// uniform control flow, which multi-texture batching breaks because the
|
|
132
|
+
// slot varies per fragment. textureSampleGrad takes explicit derivatives
|
|
133
|
+
// and is valid regardless of control-flow uniformity, while preserving
|
|
134
|
+
// mipmap-correct LOD when sprites use mipmapped textures.
|
|
135
|
+
let ddx = dpdx(input.texcoord);
|
|
136
|
+
let ddy = dpdy(input.texcoord);
|
|
137
|
+
let sample = sampleTexture(input.textureSlot, input.texcoord, ddx, ddy);
|
|
138
|
+
let resolvedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), input.premultiplySample == 1u);
|
|
139
|
+
|
|
140
|
+
return resolvedSample * input.color;
|
|
141
|
+
}
|
|
122
142
|
`;
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const spriteIndexCount = 6;
|
|
143
|
+
const instanceStrideBytes = 56;
|
|
144
|
+
const wordsPerInstance = instanceStrideBytes / Uint32Array.BYTES_PER_ELEMENT;
|
|
126
145
|
const projectionByteLength = 64;
|
|
127
146
|
const initialBatchCapacity = 32;
|
|
128
|
-
const wordsPerVertex = vertexStrideBytes / Uint32Array.BYTES_PER_ELEMENT;
|
|
129
147
|
const maxBatchTextures = 8;
|
|
148
|
+
const indicesPerSprite = 6;
|
|
149
|
+
// Static index buffer: two triangles forming a quad, vertex IDs 0..3 in
|
|
150
|
+
// TL/TR/BR/BL order so the WGSL `cornerX/cornerY` derivation matches.
|
|
151
|
+
const quadIndices = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
130
152
|
class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
131
|
-
_drawCalls = [];
|
|
132
|
-
_drawCallCount = 0;
|
|
133
153
|
_projectionData = new Float32Array(projectionByteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
134
|
-
_renderManager = null;
|
|
135
154
|
_device = null;
|
|
136
155
|
_shaderModule = null;
|
|
137
156
|
_uniformBindGroupLayout = null;
|
|
@@ -139,71 +158,81 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
139
158
|
_pipelineLayout = null;
|
|
140
159
|
_uniformBuffer = null;
|
|
141
160
|
_uniformBindGroup = null;
|
|
142
|
-
_vertexBuffer = null;
|
|
143
161
|
_indexBuffer = null;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
162
|
+
_instanceBuffer = null;
|
|
163
|
+
_instanceCapacity = 0;
|
|
164
|
+
_instanceData = new ArrayBuffer(0);
|
|
165
|
+
_instanceFloat32 = new Float32Array(this._instanceData);
|
|
166
|
+
_instanceUint32 = new Uint32Array(this._instanceData);
|
|
148
167
|
_pipelines = new Map();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
visibility: GPUShaderStage.VERTEX,
|
|
158
|
-
buffer: {
|
|
159
|
-
type: 'uniform',
|
|
160
|
-
},
|
|
161
|
-
}],
|
|
162
|
-
});
|
|
163
|
-
this._textureBindGroupLayout = this._device.createBindGroupLayout({
|
|
164
|
-
entries: [
|
|
165
|
-
...Array.from({ length: maxBatchTextures }, (_, index) => ({
|
|
166
|
-
binding: index,
|
|
167
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
168
|
-
texture: {
|
|
169
|
-
sampleType: 'float',
|
|
170
|
-
},
|
|
171
|
-
})),
|
|
172
|
-
...Array.from({ length: maxBatchTextures }, (_, index) => ({
|
|
173
|
-
binding: maxBatchTextures + index,
|
|
174
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
175
|
-
sampler: {
|
|
176
|
-
type: 'filtering',
|
|
177
|
-
},
|
|
178
|
-
})),
|
|
179
|
-
],
|
|
180
|
-
});
|
|
181
|
-
this._pipelineLayout = this._device.createPipelineLayout({
|
|
182
|
-
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
183
|
-
});
|
|
184
|
-
this._uniformBuffer = this._device.createBuffer({
|
|
185
|
-
size: projectionByteLength,
|
|
186
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
187
|
-
});
|
|
188
|
-
this._uniformBindGroup = this._device.createBindGroup({
|
|
189
|
-
layout: this._uniformBindGroupLayout,
|
|
190
|
-
entries: [{
|
|
191
|
-
binding: 0,
|
|
192
|
-
resource: {
|
|
193
|
-
buffer: this._uniformBuffer,
|
|
194
|
-
},
|
|
195
|
-
}],
|
|
196
|
-
});
|
|
197
|
-
this._ensureBatchCapacity(initialBatchCapacity);
|
|
168
|
+
_activeTextures = new Array(maxBatchTextures).fill(null);
|
|
169
|
+
_textureSlots = new Map();
|
|
170
|
+
_slotCount = 0;
|
|
171
|
+
_instanceCount = 0;
|
|
172
|
+
_currentBlendMode = null;
|
|
173
|
+
onConnect(backend) {
|
|
174
|
+
if (this._device) {
|
|
175
|
+
return;
|
|
198
176
|
}
|
|
177
|
+
this._device = backend.device;
|
|
178
|
+
this._shaderModule = this._device.createShaderModule({ code: spriteShaderSource });
|
|
179
|
+
this._uniformBindGroupLayout = this._device.createBindGroupLayout({
|
|
180
|
+
entries: [{
|
|
181
|
+
binding: 0,
|
|
182
|
+
visibility: GPUShaderStage.VERTEX,
|
|
183
|
+
buffer: {
|
|
184
|
+
type: 'uniform',
|
|
185
|
+
},
|
|
186
|
+
}],
|
|
187
|
+
});
|
|
188
|
+
this._textureBindGroupLayout = this._device.createBindGroupLayout({
|
|
189
|
+
entries: [
|
|
190
|
+
...Array.from({ length: maxBatchTextures }, (_, index) => ({
|
|
191
|
+
binding: index,
|
|
192
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
193
|
+
texture: {
|
|
194
|
+
sampleType: 'float',
|
|
195
|
+
},
|
|
196
|
+
})),
|
|
197
|
+
...Array.from({ length: maxBatchTextures }, (_, index) => ({
|
|
198
|
+
binding: maxBatchTextures + index,
|
|
199
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
200
|
+
sampler: {
|
|
201
|
+
type: 'filtering',
|
|
202
|
+
},
|
|
203
|
+
})),
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
this._pipelineLayout = this._device.createPipelineLayout({
|
|
207
|
+
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
208
|
+
});
|
|
209
|
+
this._uniformBuffer = this._device.createBuffer({
|
|
210
|
+
size: projectionByteLength,
|
|
211
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
212
|
+
});
|
|
213
|
+
this._uniformBindGroup = this._device.createBindGroup({
|
|
214
|
+
layout: this._uniformBindGroupLayout,
|
|
215
|
+
entries: [{
|
|
216
|
+
binding: 0,
|
|
217
|
+
resource: {
|
|
218
|
+
buffer: this._uniformBuffer,
|
|
219
|
+
},
|
|
220
|
+
}],
|
|
221
|
+
});
|
|
222
|
+
// Static index buffer for the quad. Allocated once at connect; its
|
|
223
|
+
// contents never change.
|
|
224
|
+
this._indexBuffer = this._device.createBuffer({
|
|
225
|
+
size: quadIndices.byteLength,
|
|
226
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
227
|
+
});
|
|
228
|
+
this._device.queue.writeBuffer(this._indexBuffer, 0, quadIndices.buffer, quadIndices.byteOffset, quadIndices.byteLength);
|
|
199
229
|
}
|
|
200
230
|
onDisconnect() {
|
|
201
|
-
this.
|
|
202
|
-
this._vertexBuffer?.destroy();
|
|
231
|
+
this._instanceBuffer?.destroy();
|
|
203
232
|
this._indexBuffer?.destroy();
|
|
204
233
|
this._uniformBuffer?.destroy();
|
|
205
234
|
this._pipelines.clear();
|
|
206
|
-
this.
|
|
235
|
+
this._instanceBuffer = null;
|
|
207
236
|
this._indexBuffer = null;
|
|
208
237
|
this._uniformBindGroup = null;
|
|
209
238
|
this._uniformBuffer = null;
|
|
@@ -212,94 +241,64 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
212
241
|
this._uniformBindGroupLayout = null;
|
|
213
242
|
this._shaderModule = null;
|
|
214
243
|
this._device = null;
|
|
215
|
-
this.
|
|
216
|
-
this.
|
|
217
|
-
this.
|
|
218
|
-
this.
|
|
219
|
-
this.
|
|
220
|
-
this.
|
|
244
|
+
this._backend = null;
|
|
245
|
+
this._instanceCapacity = 0;
|
|
246
|
+
this._instanceData = new ArrayBuffer(0);
|
|
247
|
+
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
248
|
+
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
249
|
+
this._instanceCount = 0;
|
|
250
|
+
this._currentBlendMode = null;
|
|
251
|
+
this._resetSlots();
|
|
221
252
|
}
|
|
222
253
|
render(sprite) {
|
|
223
|
-
const
|
|
254
|
+
const backend = this._backend;
|
|
224
255
|
const texture = sprite.texture;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
256
|
+
// Same early-out conditions as the deferred renderer used to apply.
|
|
257
|
+
if (backend === null
|
|
258
|
+
|| (!(texture instanceof Texture) && !(texture instanceof RenderTexture))
|
|
228
259
|
|| texture.width === 0
|
|
229
260
|
|| texture.height === 0
|
|
230
261
|
|| (texture instanceof Texture && texture.source === null)) {
|
|
231
262
|
return;
|
|
232
263
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
drawCall.blendMode = sprite.blendMode;
|
|
264
|
+
const blendMode = sprite.blendMode;
|
|
265
|
+
// Flush triggers: blend-mode change, instance buffer full at current
|
|
266
|
+
// capacity (we'll grow on next render), or texture-slot exhaustion.
|
|
267
|
+
const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
|
|
268
|
+
const slotExhausted = !this._textureSlots.has(texture) && this._slotCount >= maxBatchTextures;
|
|
269
|
+
if (blendModeChanged || slotExhausted) {
|
|
270
|
+
this.flush();
|
|
241
271
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
272
|
+
this._currentBlendMode = blendMode;
|
|
273
|
+
backend.setBlendMode(blendMode);
|
|
274
|
+
// Resolve / assign texture slot.
|
|
275
|
+
let slot = this._textureSlots.get(texture);
|
|
276
|
+
if (slot === undefined) {
|
|
277
|
+
slot = this._slotCount++;
|
|
278
|
+
this._textureSlots.set(texture, slot);
|
|
279
|
+
this._activeTextures[slot] = texture;
|
|
249
280
|
}
|
|
281
|
+
const premultiplySample = backend.shouldPremultiplyTextureSample(texture) ? 1 : 0;
|
|
282
|
+
const packedSlotFlags = slot | (premultiplySample << 8);
|
|
283
|
+
// Ensure capacity covers the new entry BEFORE packing — otherwise the
|
|
284
|
+
// typed-array writes in _packInstance silently fall off the end of a
|
|
285
|
+
// too-small buffer.
|
|
286
|
+
this._ensureInstanceCapacity(this._instanceCount + 1);
|
|
287
|
+
this._packInstance(sprite, texture, packedSlotFlags);
|
|
288
|
+
this._instanceCount++;
|
|
250
289
|
}
|
|
251
290
|
flush() {
|
|
252
|
-
const
|
|
291
|
+
const backend = this._backend;
|
|
253
292
|
const device = this._device;
|
|
254
293
|
const uniformBuffer = this._uniformBuffer;
|
|
255
294
|
const uniformBindGroup = this._uniformBindGroup;
|
|
256
|
-
|
|
257
|
-
const indexBuffer = this._indexBuffer;
|
|
258
|
-
if (!renderManager || !device || !uniformBuffer || !uniformBindGroup || !vertexBuffer || !indexBuffer) {
|
|
295
|
+
if (!backend || !device || !uniformBuffer || !uniformBindGroup) {
|
|
259
296
|
return;
|
|
260
297
|
}
|
|
261
|
-
if (this.
|
|
298
|
+
if (this._instanceCount === 0 && !backend.clearRequested) {
|
|
262
299
|
return;
|
|
263
300
|
}
|
|
264
|
-
|
|
265
|
-
// reasons this must happen before the render pass begins:
|
|
266
|
-
// 1. _ensureBatchCapacity destroys old buffers and creates new ones
|
|
267
|
-
// when capacity grows, so running it after setVertexBuffer /
|
|
268
|
-
// setIndexBuffer would leave the pass bound to destroyed buffers.
|
|
269
|
-
// 2. All batches are packed into the vertex buffer at distinct
|
|
270
|
-
// sprite offsets, so the buffer must hold every sprite in the
|
|
271
|
-
// flush, not just one batch worth.
|
|
272
|
-
if (this._drawCallCount > 0) {
|
|
273
|
-
this._ensureBatchCapacity(this._drawCallCount);
|
|
274
|
-
}
|
|
275
|
-
// Walk the batches once, packing each batch's vertex data into the
|
|
276
|
-
// CPU-side buffer at its own sprite-aligned offset. Each batch's
|
|
277
|
-
// metadata is recorded for the draw loop below.
|
|
278
|
-
//
|
|
279
|
-
// This replaces an earlier per-batch queue.writeBuffer(..., offset: 0)
|
|
280
|
-
// pattern where every writeBuffer targeted the same GPU offset. All
|
|
281
|
-
// writeBuffers in a frame execute before queue.submit(commandBuffer),
|
|
282
|
-
// so only the last batch's vertex data survived — which meant any
|
|
283
|
-
// flush containing more than one batch rendered every batch using
|
|
284
|
-
// the LAST batch's vertices (background vanished, sprites duplicated
|
|
285
|
-
// at wrong sizes, etc. whenever blend mode / texture slot / pipeline
|
|
286
|
-
// caused a split into multiple batches).
|
|
287
|
-
const batchPlan = [];
|
|
288
|
-
let packedSpriteCount = 0;
|
|
289
|
-
for (let start = 0; start < this._drawCallCount;) {
|
|
290
|
-
const batch = this._getBatchRange(start);
|
|
291
|
-
const spriteCount = batch.end - batch.start;
|
|
292
|
-
this._writeBatchVertexData(batch, packedSpriteCount);
|
|
293
|
-
batchPlan.push({
|
|
294
|
-
firstSprite: packedSpriteCount,
|
|
295
|
-
spriteCount,
|
|
296
|
-
blendMode: batch.blendMode,
|
|
297
|
-
textures: batch.textures,
|
|
298
|
-
});
|
|
299
|
-
packedSpriteCount += spriteCount;
|
|
300
|
-
start = batch.end;
|
|
301
|
-
}
|
|
302
|
-
const viewMatrix = renderManager.view.getTransform();
|
|
301
|
+
const viewMatrix = backend.view.getTransform();
|
|
303
302
|
this._projectionData.set([
|
|
304
303
|
viewMatrix.a, viewMatrix.c, 0, 0,
|
|
305
304
|
viewMatrix.b, viewMatrix.d, 0, 0,
|
|
@@ -309,157 +308,175 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
309
308
|
device.queue.writeBuffer(uniformBuffer, 0, this._projectionData.buffer, this._projectionData.byteOffset, this._projectionData.byteLength);
|
|
310
309
|
const encoder = device.createCommandEncoder();
|
|
311
310
|
const pass = encoder.beginRenderPass({
|
|
312
|
-
colorAttachments: [
|
|
311
|
+
colorAttachments: [backend.createColorAttachment()],
|
|
313
312
|
});
|
|
314
|
-
|
|
315
|
-
const scissor =
|
|
313
|
+
backend.stats.renderPasses++;
|
|
314
|
+
const scissor = backend.getScissorRect();
|
|
316
315
|
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
317
316
|
if (scissor !== null && !maskClipsAll) {
|
|
318
317
|
pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
319
318
|
}
|
|
320
|
-
if (this.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
319
|
+
if (this._instanceCount > 0 && !maskClipsAll && this._instanceBuffer !== null && this._indexBuffer !== null && this._currentBlendMode !== null) {
|
|
320
|
+
device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, this._instanceCount * instanceStrideBytes);
|
|
321
|
+
const pipeline = this._getPipeline(this._currentBlendMode, backend.renderTargetFormat);
|
|
322
|
+
const textureBindGroup = this._createTextureBindGroup(device, backend);
|
|
323
|
+
pass.setPipeline(pipeline);
|
|
324
324
|
pass.setBindGroup(0, uniformBindGroup);
|
|
325
|
-
pass.
|
|
326
|
-
pass.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
pass.setBindGroup(1, textureBindGroup);
|
|
332
|
-
pass.drawIndexed(plan.spriteCount * spriteIndexCount, 1, plan.firstSprite * spriteIndexCount, 0, 0);
|
|
333
|
-
renderManager.stats.batches++;
|
|
334
|
-
renderManager.stats.drawCalls++;
|
|
335
|
-
}
|
|
325
|
+
pass.setBindGroup(1, textureBindGroup);
|
|
326
|
+
pass.setVertexBuffer(0, this._instanceBuffer);
|
|
327
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16');
|
|
328
|
+
pass.drawIndexed(indicesPerSprite, this._instanceCount, 0, 0, 0);
|
|
329
|
+
backend.stats.batches++;
|
|
330
|
+
backend.stats.drawCalls++;
|
|
336
331
|
}
|
|
337
332
|
pass.end();
|
|
338
|
-
|
|
339
|
-
this.
|
|
333
|
+
backend.submit(encoder.finish());
|
|
334
|
+
this._instanceCount = 0;
|
|
335
|
+
this._resetSlots();
|
|
336
|
+
this._currentBlendMode = null;
|
|
340
337
|
}
|
|
341
338
|
destroy() {
|
|
342
339
|
this.disconnect();
|
|
343
340
|
}
|
|
344
|
-
|
|
345
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Pre-create render pipelines for every blend-mode × target-format
|
|
343
|
+
* combination this renderer can produce, asynchronously and in
|
|
344
|
+
* parallel. Called from the render manager's init path so by the time
|
|
345
|
+
* the first frame draws, all pipelines exist in cache.
|
|
346
|
+
*
|
|
347
|
+
* Without prewarm, the first draw of any new (blendMode, format)
|
|
348
|
+
* combination would fall back to the synchronous _getPipeline() path,
|
|
349
|
+
* which blocks while the WebGPU implementation compiles WGSL and
|
|
350
|
+
* sets up the pipeline state object — typically tens of milliseconds.
|
|
351
|
+
*/
|
|
352
|
+
async prewarmPipelines(formats) {
|
|
353
|
+
const device = this._device;
|
|
354
|
+
if (!device || !this._shaderModule || !this._pipelineLayout) {
|
|
346
355
|
return;
|
|
347
356
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
nextCapacity *= 2;
|
|
357
|
+
if (typeof device.createRenderPipelineAsync !== 'function') {
|
|
358
|
+
return;
|
|
351
359
|
}
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
360
|
+
const blendModes = [
|
|
361
|
+
BlendModes.Normal,
|
|
362
|
+
BlendModes.Additive,
|
|
363
|
+
BlendModes.Subtract,
|
|
364
|
+
BlendModes.Multiply,
|
|
365
|
+
BlendModes.Screen,
|
|
366
|
+
];
|
|
367
|
+
const promises = [];
|
|
368
|
+
for (const blendMode of blendModes) {
|
|
369
|
+
for (const format of formats) {
|
|
370
|
+
const pipelineKey = `${blendMode}:${format}`;
|
|
371
|
+
if (this._pipelines.has(pipelineKey)) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const promise = device
|
|
375
|
+
.createRenderPipelineAsync(this._buildPipelineDescriptor(blendMode, format))
|
|
376
|
+
.then((pipeline) => {
|
|
377
|
+
this._pipelines.set(pipelineKey, pipeline);
|
|
378
|
+
});
|
|
379
|
+
promises.push(promise);
|
|
380
|
+
}
|
|
371
381
|
}
|
|
372
|
-
|
|
373
|
-
this._vertexBuffer?.destroy();
|
|
374
|
-
this._indexBuffer?.destroy();
|
|
375
|
-
this._vertexCapacity = nextCapacity;
|
|
376
|
-
this._vertexData = vertexData;
|
|
377
|
-
this._float32View = new Float32Array(vertexData);
|
|
378
|
-
this._uint32View = new Uint32Array(vertexData);
|
|
379
|
-
this._vertexBuffer = vertexBuffer;
|
|
380
|
-
this._indexBuffer = indexBuffer;
|
|
382
|
+
await Promise.all(promises);
|
|
381
383
|
}
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
384
|
+
_packInstance(sprite, texture, packedSlotFlags) {
|
|
385
|
+
const offset = this._instanceCount * wordsPerInstance;
|
|
386
|
+
const f32 = this._instanceFloat32;
|
|
387
|
+
const u32 = this._instanceUint32;
|
|
388
|
+
const bounds = sprite.getLocalBounds();
|
|
389
|
+
f32[offset + 0] = bounds.left;
|
|
390
|
+
f32[offset + 1] = bounds.top;
|
|
391
|
+
f32[offset + 2] = bounds.right;
|
|
392
|
+
f32[offset + 3] = bounds.bottom;
|
|
393
|
+
const transform = sprite.getGlobalTransform();
|
|
394
|
+
f32[offset + 4] = transform.a;
|
|
395
|
+
f32[offset + 5] = transform.b;
|
|
396
|
+
f32[offset + 6] = transform.x;
|
|
397
|
+
f32[offset + 7] = transform.c;
|
|
398
|
+
f32[offset + 8] = transform.d;
|
|
399
|
+
f32[offset + 9] = transform.y;
|
|
400
|
+
// uvBounds: u16x4 normalised, packed into two u32 slots. The CPU
|
|
401
|
+
// applies the flipY swap so the shader stays orientation-agnostic.
|
|
402
|
+
const frame = sprite.textureFrame;
|
|
403
|
+
const texWidth = texture.width;
|
|
404
|
+
const texHeight = texture.height;
|
|
405
|
+
const uMin = ((frame.left / texWidth) * 0xFFFF) & 0xFFFF;
|
|
406
|
+
const uMax = ((frame.right / texWidth) * 0xFFFF) & 0xFFFF;
|
|
407
|
+
const vMinRaw = ((frame.top / texHeight) * 0xFFFF) & 0xFFFF;
|
|
408
|
+
const vMaxRaw = ((frame.bottom / texHeight) * 0xFFFF) & 0xFFFF;
|
|
409
|
+
const flipY = texture instanceof Texture && texture.flipY;
|
|
410
|
+
const vMin = flipY ? vMaxRaw : vMinRaw;
|
|
411
|
+
const vMax = flipY ? vMinRaw : vMaxRaw;
|
|
412
|
+
u32[offset + 10] = uMin | (vMin << 16);
|
|
413
|
+
u32[offset + 11] = uMax | (vMax << 16);
|
|
414
|
+
u32[offset + 12] = sprite.tint.toRgba();
|
|
415
|
+
u32[offset + 13] = packedSlotFlags;
|
|
416
|
+
}
|
|
417
|
+
_ensureInstanceCapacity(instanceCount) {
|
|
418
|
+
if (!this._device || instanceCount <= this._instanceCapacity) {
|
|
385
419
|
return;
|
|
386
420
|
}
|
|
387
|
-
let
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const textureSlot = batch.textureSlots.get(drawCall.texture) ?? 0;
|
|
391
|
-
const premultiplySample = renderManager.shouldPremultiplyTextureSample(drawCall.texture) ? 1 : 0;
|
|
392
|
-
const vertices = drawCall.sprite.vertices;
|
|
393
|
-
const texCoords = drawCall.sprite.texCoords;
|
|
394
|
-
for (let i = 0; i < spriteVertexCount; i++) {
|
|
395
|
-
const vertexIndex = i * 2;
|
|
396
|
-
const packedTexCoord = texCoords[i];
|
|
397
|
-
this._float32View[vertexOffset] = vertices[vertexIndex];
|
|
398
|
-
this._float32View[vertexOffset + 1] = vertices[vertexIndex + 1];
|
|
399
|
-
this._float32View[vertexOffset + 2] = (packedTexCoord & 0xFFFF) / 65535;
|
|
400
|
-
this._float32View[vertexOffset + 3] = ((packedTexCoord >>> 16) & 0xFFFF) / 65535;
|
|
401
|
-
this._uint32View[vertexOffset + 4] = drawCall.color;
|
|
402
|
-
this._uint32View[vertexOffset + 5] = premultiplySample;
|
|
403
|
-
this._uint32View[vertexOffset + 6] = textureSlot;
|
|
404
|
-
vertexOffset += wordsPerVertex;
|
|
405
|
-
}
|
|
421
|
+
let nextCapacity = Math.max(this._instanceCapacity, initialBatchCapacity);
|
|
422
|
+
while (nextCapacity < instanceCount) {
|
|
423
|
+
nextCapacity *= 2;
|
|
406
424
|
}
|
|
425
|
+
const oldData = this._instanceData;
|
|
426
|
+
// Preserve any already-packed instances. _instanceCount is bounded by
|
|
427
|
+
// the previous capacity, but oldData may be the initial 0-byte buffer
|
|
428
|
+
// — clamp to its actual byteLength to avoid out-of-range typed-array
|
|
429
|
+
// construction.
|
|
430
|
+
const carryBytes = Math.min(this._instanceCount * instanceStrideBytes, oldData.byteLength);
|
|
431
|
+
const instanceData = new ArrayBuffer(nextCapacity * instanceStrideBytes);
|
|
432
|
+
if (carryBytes > 0) {
|
|
433
|
+
new Uint8Array(instanceData).set(new Uint8Array(oldData, 0, carryBytes));
|
|
434
|
+
}
|
|
435
|
+
const instanceBuffer = this._device.createBuffer({
|
|
436
|
+
size: instanceData.byteLength,
|
|
437
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
438
|
+
});
|
|
439
|
+
this._instanceBuffer?.destroy();
|
|
440
|
+
this._instanceCapacity = nextCapacity;
|
|
441
|
+
this._instanceData = instanceData;
|
|
442
|
+
this._instanceFloat32 = new Float32Array(instanceData);
|
|
443
|
+
this._instanceUint32 = new Uint32Array(instanceData);
|
|
444
|
+
this._instanceBuffer = instanceBuffer;
|
|
407
445
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
let end = start + 1;
|
|
413
|
-
textureSlots.set(drawCall.texture, 0);
|
|
414
|
-
textures.push(drawCall.texture);
|
|
415
|
-
while (end < this._drawCallCount) {
|
|
416
|
-
const nextDrawCall = this._drawCalls[end];
|
|
417
|
-
if (nextDrawCall.blendMode !== drawCall.blendMode) {
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
|
-
if (!textureSlots.has(nextDrawCall.texture)) {
|
|
421
|
-
if (textures.length >= maxBatchTextures) {
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
textureSlots.set(nextDrawCall.texture, textures.length);
|
|
425
|
-
textures.push(nextDrawCall.texture);
|
|
446
|
+
_resetSlots() {
|
|
447
|
+
if (this._slotCount > 0) {
|
|
448
|
+
for (let i = 0; i < this._slotCount; i++) {
|
|
449
|
+
this._activeTextures[i] = null;
|
|
426
450
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
end++;
|
|
451
|
+
this._textureSlots.clear();
|
|
452
|
+
this._slotCount = 0;
|
|
431
453
|
}
|
|
432
|
-
return {
|
|
433
|
-
start,
|
|
434
|
-
end,
|
|
435
|
-
spriteCount: end - start,
|
|
436
|
-
blendMode: drawCall.blendMode,
|
|
437
|
-
textures,
|
|
438
|
-
textureSlots,
|
|
439
|
-
};
|
|
440
454
|
}
|
|
441
|
-
_createTextureBindGroup(device,
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
455
|
+
_createTextureBindGroup(device, backend) {
|
|
456
|
+
// Slots beyond the active count get the slot-0 texture as a filler so
|
|
457
|
+
// the bind-group layout always sees N valid texture views and samplers.
|
|
458
|
+
// The fragment shader's switch only ever dispatches to the active slot
|
|
459
|
+
// count, so unsampled fillers cost nothing visually.
|
|
460
|
+
const fallbackTexture = this._activeTextures[0] ?? Texture.empty;
|
|
461
|
+
const fallbackBinding = backend.getTextureBinding(fallbackTexture);
|
|
445
462
|
const resolvedBindings = new Array(maxBatchTextures);
|
|
446
|
-
for (let
|
|
447
|
-
const texture =
|
|
448
|
-
|
|
463
|
+
for (let i = 0; i < maxBatchTextures; i++) {
|
|
464
|
+
const texture = this._activeTextures[i] ?? fallbackTexture;
|
|
465
|
+
resolvedBindings[i] = texture === fallbackTexture
|
|
449
466
|
? fallbackBinding
|
|
450
|
-
:
|
|
451
|
-
resolvedBindings[index] = textureBinding;
|
|
467
|
+
: backend.getTextureBinding(texture);
|
|
452
468
|
}
|
|
453
|
-
|
|
469
|
+
const entries = [];
|
|
470
|
+
for (let i = 0; i < maxBatchTextures; i++) {
|
|
454
471
|
entries.push({
|
|
455
|
-
binding:
|
|
456
|
-
resource: resolvedBindings[
|
|
472
|
+
binding: i,
|
|
473
|
+
resource: resolvedBindings[i].view,
|
|
457
474
|
});
|
|
458
475
|
}
|
|
459
|
-
for (let
|
|
476
|
+
for (let i = 0; i < maxBatchTextures; i++) {
|
|
460
477
|
entries.push({
|
|
461
|
-
binding: maxBatchTextures +
|
|
462
|
-
resource: resolvedBindings[
|
|
478
|
+
binding: maxBatchTextures + i,
|
|
479
|
+
resource: resolvedBindings[i].sampler,
|
|
463
480
|
});
|
|
464
481
|
}
|
|
465
482
|
return device.createBindGroup({
|
|
@@ -473,35 +490,48 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
473
490
|
if (existingPipeline) {
|
|
474
491
|
return existingPipeline;
|
|
475
492
|
}
|
|
476
|
-
if (!this._device || !this._shaderModule || !this._pipelineLayout || !this.
|
|
493
|
+
if (!this._device || !this._shaderModule || !this._pipelineLayout || !this._backend) {
|
|
477
494
|
throw new Error('Renderer has to be connected first!');
|
|
478
495
|
}
|
|
479
|
-
const pipeline = this._device.createRenderPipeline(
|
|
496
|
+
const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(blendMode, format));
|
|
497
|
+
this._pipelines.set(pipelineKey, pipeline);
|
|
498
|
+
return pipeline;
|
|
499
|
+
}
|
|
500
|
+
_buildPipelineDescriptor(blendMode, format) {
|
|
501
|
+
if (!this._shaderModule || !this._pipelineLayout) {
|
|
502
|
+
throw new Error('Renderer has to be connected first!');
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
480
505
|
layout: this._pipelineLayout,
|
|
481
506
|
vertex: {
|
|
482
507
|
module: this._shaderModule,
|
|
483
508
|
entryPoint: 'vertexMain',
|
|
484
509
|
buffers: [{
|
|
485
|
-
arrayStride:
|
|
510
|
+
arrayStride: instanceStrideBytes,
|
|
511
|
+
stepMode: 'instance',
|
|
486
512
|
attributes: [{
|
|
487
513
|
shaderLocation: 0,
|
|
488
514
|
offset: 0,
|
|
489
|
-
format: '
|
|
515
|
+
format: 'float32x4',
|
|
490
516
|
}, {
|
|
491
517
|
shaderLocation: 1,
|
|
492
|
-
offset:
|
|
493
|
-
format: '
|
|
518
|
+
offset: 16,
|
|
519
|
+
format: 'float32x3',
|
|
494
520
|
}, {
|
|
495
521
|
shaderLocation: 2,
|
|
496
|
-
offset:
|
|
497
|
-
format: '
|
|
522
|
+
offset: 28,
|
|
523
|
+
format: 'float32x3',
|
|
498
524
|
}, {
|
|
499
525
|
shaderLocation: 3,
|
|
500
|
-
offset:
|
|
501
|
-
format: '
|
|
526
|
+
offset: 40,
|
|
527
|
+
format: 'unorm16x4',
|
|
502
528
|
}, {
|
|
503
529
|
shaderLocation: 4,
|
|
504
|
-
offset:
|
|
530
|
+
offset: 48,
|
|
531
|
+
format: 'unorm8x4',
|
|
532
|
+
}, {
|
|
533
|
+
shaderLocation: 5,
|
|
534
|
+
offset: 52,
|
|
505
535
|
format: 'uint32',
|
|
506
536
|
}],
|
|
507
537
|
}],
|
|
@@ -518,9 +548,7 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
518
548
|
primitive: {
|
|
519
549
|
topology: 'triangle-list',
|
|
520
550
|
},
|
|
521
|
-
}
|
|
522
|
-
this._pipelines.set(pipelineKey, pipeline);
|
|
523
|
-
return pipeline;
|
|
551
|
+
};
|
|
524
552
|
}
|
|
525
553
|
}
|
|
526
554
|
|