@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
|
@@ -3,7 +3,9 @@ import { Texture } from '../texture/Texture.js';
|
|
|
3
3
|
import { BlendModes } from '../types.js';
|
|
4
4
|
import { AbstractWebGpuRenderer } from './AbstractWebGpuRenderer.js';
|
|
5
5
|
import { getWebGpuBlendState } from './WebGpuBlendState.js';
|
|
6
|
+
import { stencilContentDepthStencilState } from './WebGpuStencilState.js';
|
|
6
7
|
|
|
8
|
+
/* eslint-disable max-lines */
|
|
7
9
|
/// <reference types="@webgpu/types" />
|
|
8
10
|
const meshShaderSource = `
|
|
9
11
|
struct VertexInput {
|
|
@@ -47,6 +49,65 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
|
47
49
|
return vec4<f32>(modulated.rgb * modulated.a, modulated.a);
|
|
48
50
|
}
|
|
49
51
|
`;
|
|
52
|
+
const instancedMeshShaderSource = `
|
|
53
|
+
struct VertexInput {
|
|
54
|
+
@location(0) position: vec2<f32>,
|
|
55
|
+
@location(1) texcoord: vec2<f32>,
|
|
56
|
+
@location(2) color: vec4<f32>,
|
|
57
|
+
@location(6) nodeIndex: u32,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
struct VertexOutput {
|
|
61
|
+
@builtin(position) position: vec4<f32>,
|
|
62
|
+
@location(0) texcoord: vec2<f32>,
|
|
63
|
+
@location(1) color: vec4<f32>,
|
|
64
|
+
@location(2) tint: vec4<f32>,
|
|
65
|
+
@location(3) @interpolate(flat) premultiplySample: u32,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
struct TransformSlot {
|
|
69
|
+
m0: vec4<f32>,
|
|
70
|
+
m1: vec4<f32>,
|
|
71
|
+
m2: vec4<f32>,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
struct TransformUniforms {
|
|
75
|
+
projection: mat3x3<f32>,
|
|
76
|
+
flags: vec4<f32>,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
@group(0) @binding(0) var<uniform> uniforms: TransformUniforms;
|
|
80
|
+
@group(0) @binding(1) var<storage, read> transforms: array<TransformSlot>;
|
|
81
|
+
|
|
82
|
+
@group(1) @binding(0) var meshTexture: texture_2d<f32>;
|
|
83
|
+
@group(1) @binding(1) var meshSampler: sampler;
|
|
84
|
+
|
|
85
|
+
@vertex
|
|
86
|
+
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
87
|
+
let slot = transforms[input.nodeIndex];
|
|
88
|
+
let world = vec3<f32>(
|
|
89
|
+
slot.m0.x * input.position.x + slot.m0.z * input.position.y + slot.m1.x,
|
|
90
|
+
slot.m0.y * input.position.x + slot.m0.w * input.position.y + slot.m1.y,
|
|
91
|
+
1.0
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
var output: VertexOutput;
|
|
95
|
+
output.position = vec4<f32>((uniforms.projection * world).xy, 0.0, 1.0);
|
|
96
|
+
output.texcoord = input.texcoord;
|
|
97
|
+
output.color = input.color;
|
|
98
|
+
output.tint = slot.m2;
|
|
99
|
+
output.premultiplySample = u32(uniforms.flags.x);
|
|
100
|
+
return output;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@fragment
|
|
104
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
105
|
+
let sample = textureSample(meshTexture, meshSampler, input.texcoord);
|
|
106
|
+
let resolvedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), input.premultiplySample == 1u);
|
|
107
|
+
let modulated = resolvedSample * input.color * input.tint;
|
|
108
|
+
return vec4<f32>(modulated.rgb * modulated.a, modulated.a);
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
50
111
|
// Per-vertex layout (20 bytes): pos f32x2 + uv f32x2 + color u8x4-norm.
|
|
51
112
|
// Default-shader path bakes the (view * globalTransform) into position so the
|
|
52
113
|
// vertex shader stays branchless and uniform-free except for the per-mesh tint.
|
|
@@ -55,29 +116,53 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
|
55
116
|
const vertexStrideBytes = 20;
|
|
56
117
|
const wordsPerVertex = vertexStrideBytes / 4;
|
|
57
118
|
const tintByteLength = 32; // vec4 tint + vec4 flags (only flags.x used)
|
|
119
|
+
const transformUniformByteLength = 64; // mat3x3<f32> (48B) + vec4<f32> flags (16B)
|
|
58
120
|
// Custom-shader uniform layout:
|
|
59
121
|
// mat3x3<f32> projection — 48 bytes (3 vec3 columns padded to vec4 in WGSL)
|
|
60
122
|
// mat3x3<f32> translation — 48 bytes
|
|
61
123
|
// vec4<f32> tint — 16 bytes
|
|
62
124
|
// Total: 112 bytes; aligned up to 256 for dynamic offset.
|
|
63
125
|
const customMeshUniformBytes = 112;
|
|
126
|
+
/**
|
|
127
|
+
* Cache key for the default + instanced (static-batch) pipeline maps. The
|
|
128
|
+
* stencil dimension keeps the clip and no-clip variants distinct, mirroring the
|
|
129
|
+
* sprite renderer: a stencil pipeline carries depth/stencil state and is only
|
|
130
|
+
* valid in a pass with the matching attachment, so the two are never
|
|
131
|
+
* interchangeable.
|
|
132
|
+
*/
|
|
133
|
+
function meshPipelineCacheKey(blendMode, format, stencil) {
|
|
134
|
+
return `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
|
|
135
|
+
}
|
|
64
136
|
const meshUniformAlignment = 256;
|
|
65
137
|
const maxCustomTextureSlots = 7; // user texture uniforms; group 2 binding 1..N
|
|
66
138
|
class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
67
139
|
_combinedTransform = new Matrix();
|
|
68
140
|
_drawCalls = [];
|
|
69
141
|
_pipelines = new Map();
|
|
142
|
+
_instancedPipelines = new Map();
|
|
143
|
+
_staticGeometryCache = new Map();
|
|
70
144
|
_textureBindGroups = new WeakMap();
|
|
71
145
|
_customShaders = new Map();
|
|
72
146
|
_device = null;
|
|
73
147
|
_shaderModule = null;
|
|
148
|
+
_instancedShaderModule = null;
|
|
74
149
|
_uniformBindGroupLayout = null;
|
|
150
|
+
_instancedTransformBindGroupLayout = null;
|
|
75
151
|
_textureBindGroupLayout = null;
|
|
76
152
|
_pipelineLayout = null;
|
|
153
|
+
_instancedPipelineLayout = null;
|
|
77
154
|
_vertexBuffer = null;
|
|
78
155
|
_indexBuffer = null;
|
|
79
156
|
_uniformBuffer = null;
|
|
80
157
|
_uniformBindGroup = null;
|
|
158
|
+
_instancedUniformBuffer = null;
|
|
159
|
+
_instancedUniformBufferCapacity = 0;
|
|
160
|
+
_instancedUniformScratch = new Float32Array(transformUniformByteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
161
|
+
_instancedNodeIndexBuffer = null;
|
|
162
|
+
_instancedNodeIndexBufferCapacity = 0;
|
|
163
|
+
_instancedNodeIndexData = new Uint32Array(0);
|
|
164
|
+
_instancedTransformBindGroup = null;
|
|
165
|
+
_instancedTransformStorageBuffer = null;
|
|
81
166
|
_uniformAlignment = 256;
|
|
82
167
|
_vertexBufferCapacity = 0;
|
|
83
168
|
_indexBufferCapacity = 0;
|
|
@@ -92,17 +177,21 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
92
177
|
if (backend === null) {
|
|
93
178
|
throw new Error('WebGpuMeshRenderer is not connected to a backend.');
|
|
94
179
|
}
|
|
95
|
-
const customShader = mesh.
|
|
96
|
-
if (customShader !== null && customShader.wgsl === null) {
|
|
97
|
-
throw new Error('
|
|
180
|
+
const customShader = mesh.material;
|
|
181
|
+
if (customShader !== null && customShader.shader.wgsl === null) {
|
|
182
|
+
throw new Error('Mesh material shader has no `wgsl` source; cannot render through the WebGPU backend.');
|
|
98
183
|
}
|
|
99
184
|
const vertexCount = mesh.vertexCount;
|
|
100
185
|
if (vertexCount === 0) {
|
|
101
186
|
return;
|
|
102
187
|
}
|
|
103
|
-
|
|
188
|
+
// The material owns its blend mode; the mesh's own blendMode overrides it
|
|
189
|
+
// when set away from the default (Normal). Default-path meshes keep their
|
|
190
|
+
// own blendMode verbatim.
|
|
191
|
+
const blendMode = customShader !== null && mesh.blendMode === BlendModes.Normal ? customShader.blendMode : mesh.blendMode;
|
|
104
192
|
backend.setBlendMode(blendMode);
|
|
105
193
|
const meshTexture = mesh.texture ?? Texture.white;
|
|
194
|
+
const command = backend.activeDrawCommand;
|
|
106
195
|
// backend.shouldPremultiplyTextureSample expects RenderTexture-or-Texture.
|
|
107
196
|
// Both branches are valid here. Premultiply flag is ignored by custom
|
|
108
197
|
// shaders (they handle premultiplication themselves), but we still record
|
|
@@ -123,6 +212,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
123
212
|
const drawCall = {
|
|
124
213
|
mesh,
|
|
125
214
|
customShader,
|
|
215
|
+
command,
|
|
126
216
|
blendMode,
|
|
127
217
|
texture: meshTexture,
|
|
128
218
|
premultiplySample,
|
|
@@ -151,13 +241,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
151
241
|
// Honor a pending clear with an empty pass so createColorAttachment
|
|
152
242
|
// consumes the clear-state once.
|
|
153
243
|
if (backend.clearRequested) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
colorAttachments: [backend.createColorAttachment()],
|
|
157
|
-
});
|
|
158
|
-
backend.stats.renderPasses++;
|
|
159
|
-
pass.end();
|
|
160
|
-
backend.submit(encoder.finish());
|
|
244
|
+
backend._passCoordinator.acquirePass();
|
|
245
|
+
backend._passCoordinator.endPass();
|
|
161
246
|
}
|
|
162
247
|
this._resetFrame();
|
|
163
248
|
return;
|
|
@@ -166,7 +251,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
166
251
|
// buffers, so default offsets are independent of custom offsets).
|
|
167
252
|
let defaultVertices = 0;
|
|
168
253
|
let defaultIndices = 0;
|
|
169
|
-
const customVertexCursors = new Map(); // running vertex count per
|
|
254
|
+
const customVertexCursors = new Map(); // running vertex count per material
|
|
170
255
|
const customIndexCursors = new Map();
|
|
171
256
|
for (let i = 0; i < this._drawCallCount; i++) {
|
|
172
257
|
const dc = this._drawCalls[i];
|
|
@@ -192,6 +277,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
192
277
|
// each custom-shader resource manages its own.
|
|
193
278
|
const defaultDrawCalls = this._drawCallCount - this._totalCustomDraws();
|
|
194
279
|
this._ensureUniformCapacity(defaultDrawCalls);
|
|
280
|
+
this._ensureInstancedUniformCapacity(this._drawCallCount);
|
|
195
281
|
// Phase 3: pack default-path vertex/index/uniform data.
|
|
196
282
|
const defaultUniformBytes = defaultDrawCalls * this._uniformAlignment;
|
|
197
283
|
const defaultUniformData = defaultUniformBytes > 0 ? new ArrayBuffer(defaultUniformBytes) : null;
|
|
@@ -211,13 +297,17 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
211
297
|
this._packedIndexData[start + j] = j;
|
|
212
298
|
}
|
|
213
299
|
}
|
|
214
|
-
// Pack tint+flags for default path.
|
|
300
|
+
// Pack tint+flags for default path. Color RGB channels are 0..255; the
|
|
301
|
+
// shader multiplies the sampled texel by this tint, so normalize to
|
|
302
|
+
// 0..1 (matching TransformBuffer and the WebGL2 mesh shader). Leaving
|
|
303
|
+
// them at 0..255 scales every non-zero texel channel past 1.0, which
|
|
304
|
+
// clamps intermediate colors (gradients, photos) to full saturation.
|
|
215
305
|
if (defaultUniformF32 !== null) {
|
|
216
306
|
const offsetWords = (defaultUniformIndex * this._uniformAlignment) / Float32Array.BYTES_PER_ELEMENT;
|
|
217
307
|
const tint = dc.mesh.tint;
|
|
218
|
-
defaultUniformF32[offsetWords + 0] = tint.r;
|
|
219
|
-
defaultUniformF32[offsetWords + 1] = tint.g;
|
|
220
|
-
defaultUniformF32[offsetWords + 2] = tint.b;
|
|
308
|
+
defaultUniformF32[offsetWords + 0] = tint.r / 255;
|
|
309
|
+
defaultUniformF32[offsetWords + 1] = tint.g / 255;
|
|
310
|
+
defaultUniformF32[offsetWords + 2] = tint.b / 255;
|
|
221
311
|
defaultUniformF32[offsetWords + 3] = tint.a;
|
|
222
312
|
defaultUniformF32[offsetWords + 4] = dc.premultiplySample ? 1 : 0;
|
|
223
313
|
defaultUniformF32[offsetWords + 5] = 0;
|
|
@@ -227,8 +317,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
227
317
|
defaultUniformIndex++;
|
|
228
318
|
}
|
|
229
319
|
}
|
|
230
|
-
// Phase 3b: pack custom-path vertex/index/uniform data per
|
|
231
|
-
for (const [
|
|
320
|
+
// Phase 3b: pack custom-path vertex/index/uniform data per material.
|
|
321
|
+
for (const [material, resources] of this._customShaders) {
|
|
232
322
|
if (resources.drawCount === 0) {
|
|
233
323
|
continue;
|
|
234
324
|
}
|
|
@@ -239,7 +329,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
239
329
|
let drawCursor = 0;
|
|
240
330
|
for (let i = 0; i < this._drawCallCount; i++) {
|
|
241
331
|
const dc = this._drawCalls[i];
|
|
242
|
-
if (dc.customShader !==
|
|
332
|
+
if (dc.customShader !== material)
|
|
243
333
|
continue;
|
|
244
334
|
this._writeMeshVerticesIntoBuffer(dc.mesh, vWritten, resources.vertexFloatView, resources.vertexUintView);
|
|
245
335
|
if (dc.mesh.indices !== null) {
|
|
@@ -251,50 +341,85 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
251
341
|
}
|
|
252
342
|
}
|
|
253
343
|
// Write mesh-uniform slot (proj/trans/tint) with dynamic offset.
|
|
254
|
-
this._writeCustomMeshUniform(
|
|
344
|
+
this._writeCustomMeshUniform(material, resources, drawCursor, dc.mesh, backend);
|
|
255
345
|
vWritten += dc.vertexCount;
|
|
256
346
|
iWritten += dc.indexCount;
|
|
257
347
|
drawCursor++;
|
|
258
348
|
}
|
|
259
349
|
device.queue.writeBuffer(resources.vertexBuffer, 0, resources.vertexData, 0, resources.totalVertices * vertexStrideBytes);
|
|
260
|
-
device.queue.writeBuffer(resources.indexBuffer, 0, resources.indexData.buffer, resources.indexData.byteOffset, resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
261
|
-
// Build/refresh user uniform UBO from
|
|
262
|
-
//
|
|
263
|
-
this._uploadUserUniforms(
|
|
350
|
+
device.queue.writeBuffer(resources.indexBuffer, 0, resources.indexData.buffer, resources.indexData.byteOffset, (resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT + 3) & -4);
|
|
351
|
+
// Build/refresh user uniform UBO from the material (re-built every frame
|
|
352
|
+
// so mutations to material.uniforms.X are picked up).
|
|
353
|
+
this._uploadUserUniforms(material, resources);
|
|
264
354
|
}
|
|
265
355
|
// Phase 4: single writeBuffer per resource for the default path.
|
|
266
356
|
if (defaultVertices > 0) {
|
|
267
357
|
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, defaultVertices * vertexStrideBytes);
|
|
268
|
-
device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, defaultIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
358
|
+
device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, (defaultIndices * Uint16Array.BYTES_PER_ELEMENT + 3) & -4);
|
|
269
359
|
}
|
|
270
360
|
if (defaultUniformData !== null) {
|
|
271
361
|
device.queue.writeBuffer(this._uniformBuffer, 0, defaultUniformData, 0, defaultUniformBytes);
|
|
272
362
|
}
|
|
273
363
|
// Phase 5: single render pass with one drawIndexed per mesh, switching
|
|
274
|
-
// pipeline+bind groups between default and custom paths as needed.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
label: 'WebGpuMeshRenderer pass',
|
|
279
|
-
});
|
|
280
|
-
backend.stats.renderPasses++;
|
|
281
|
-
if (scissor !== null) {
|
|
282
|
-
pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
283
|
-
}
|
|
364
|
+
// pipeline+bind groups between default and custom paths as needed. The
|
|
365
|
+
// coordinator owns the GPU pass (load/clear resolution, pass count and
|
|
366
|
+
// scissor are applied there) and ends + submits it below.
|
|
367
|
+
const pass = backend._passCoordinator.acquirePass().pass;
|
|
284
368
|
const renderTargetFormat = backend.renderTargetFormat;
|
|
369
|
+
// A clip scope flushes the active renderer on push/pop, so every draw call
|
|
370
|
+
// in this batch shares one stencil state — read it once. While active, the
|
|
371
|
+
// coordinator's pass carries a depth/stencil attachment, so the default,
|
|
372
|
+
// static-batch, and custom-material pipelines must all select their
|
|
373
|
+
// stencil-enabled variants to match it.
|
|
374
|
+
const stencil = backend._passCoordinator.stencilActive;
|
|
285
375
|
let lastShader = null;
|
|
286
376
|
let lastBlendMode = null;
|
|
287
377
|
let lastFormat = null;
|
|
288
378
|
let lastTexture = null;
|
|
289
379
|
let defaultDrawCursor = 0;
|
|
380
|
+
let instancedDrawCursor = 0;
|
|
290
381
|
const customDrawCursors = new Map();
|
|
291
382
|
for (let i = 0; i < this._drawCallCount; i++) {
|
|
292
383
|
const dc = this._drawCalls[i];
|
|
293
384
|
if (dc.customShader === null) {
|
|
385
|
+
const batchLength = this._getStaticBatchLength(i);
|
|
386
|
+
if (batchLength >= 2) {
|
|
387
|
+
const needsPipeline = lastShader !== 'instanced' || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
|
|
388
|
+
if (needsPipeline) {
|
|
389
|
+
pass.setPipeline(this._getInstancedPipeline({ blendMode: dc.blendMode, format: renderTargetFormat, stencil }));
|
|
390
|
+
lastShader = 'instanced';
|
|
391
|
+
lastBlendMode = dc.blendMode;
|
|
392
|
+
lastFormat = renderTargetFormat;
|
|
393
|
+
lastTexture = null;
|
|
394
|
+
}
|
|
395
|
+
const maxNodeIndex = this._uploadInstancedNodeIndices(i, batchLength);
|
|
396
|
+
const storage = backend.getTransformStorageBuffer(maxNodeIndex + 1);
|
|
397
|
+
this._writeInstancedUniformSlot(instancedDrawCursor, backend, dc.premultiplySample);
|
|
398
|
+
pass.setBindGroup(0, this._getOrCreateInstancedTransformBindGroup(storage.buffer), [instancedDrawCursor * this._uniformAlignment]);
|
|
399
|
+
if (dc.texture !== lastTexture) {
|
|
400
|
+
lastTexture = dc.texture;
|
|
401
|
+
pass.setBindGroup(1, this._getTextureBindGroup(backend, dc.texture));
|
|
402
|
+
}
|
|
403
|
+
const staticGeometry = this._getOrCreateStaticGeometryEntry(dc.mesh);
|
|
404
|
+
const instanceNodeIndexBuffer = this._instancedNodeIndexBuffer;
|
|
405
|
+
if (instanceNodeIndexBuffer === null) {
|
|
406
|
+
throw new Error('Instanced node-index buffer must be initialized before drawing.');
|
|
407
|
+
}
|
|
408
|
+
pass.setVertexBuffer(0, staticGeometry.vertexBuffer);
|
|
409
|
+
pass.setVertexBuffer(1, instanceNodeIndexBuffer);
|
|
410
|
+
pass.setIndexBuffer(staticGeometry.indexBuffer, 'uint16');
|
|
411
|
+
pass.drawIndexed(staticGeometry.indexCount, batchLength);
|
|
412
|
+
backend.stats.batches++;
|
|
413
|
+
backend.stats.drawCalls++;
|
|
414
|
+
defaultDrawCursor += batchLength;
|
|
415
|
+
instancedDrawCursor++;
|
|
416
|
+
i += batchLength - 1;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
294
419
|
// ----- Default path -----
|
|
295
420
|
const needsPipeline = lastShader !== 'default' || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
|
|
296
421
|
if (needsPipeline) {
|
|
297
|
-
pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat }));
|
|
422
|
+
pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat, stencil }));
|
|
298
423
|
lastShader = 'default';
|
|
299
424
|
lastBlendMode = dc.blendMode;
|
|
300
425
|
lastFormat = renderTargetFormat;
|
|
@@ -319,9 +444,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
319
444
|
// (Spector.js, Chrome DevTools' WebGPU panel) show meaningful
|
|
320
445
|
// labels for the otherwise-anonymous mesh draws inside the
|
|
321
446
|
// batched render pass.
|
|
322
|
-
pass.pushDebugGroup('
|
|
447
|
+
pass.pushDebugGroup('MeshMaterial (custom)');
|
|
323
448
|
if (needsPipeline) {
|
|
324
|
-
pass.setPipeline(this._getOrCreateCustomPipeline(resources, dc.blendMode, renderTargetFormat));
|
|
449
|
+
pass.setPipeline(this._getOrCreateCustomPipeline(resources, dc.blendMode, renderTargetFormat, stencil));
|
|
325
450
|
lastShader = dc.customShader;
|
|
326
451
|
lastBlendMode = dc.blendMode;
|
|
327
452
|
lastFormat = renderTargetFormat;
|
|
@@ -344,8 +469,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
344
469
|
backend.stats.batches++;
|
|
345
470
|
backend.stats.drawCalls++;
|
|
346
471
|
}
|
|
347
|
-
|
|
348
|
-
backend.submit(encoder.finish());
|
|
472
|
+
backend._passCoordinator.endPass();
|
|
349
473
|
this._resetFrame();
|
|
350
474
|
}
|
|
351
475
|
destroy() {
|
|
@@ -372,12 +496,20 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
372
496
|
const promises = [];
|
|
373
497
|
for (const blendMode of blendModes) {
|
|
374
498
|
for (const format of formats) {
|
|
375
|
-
|
|
499
|
+
// Prewarm only the no-clip variants; the stencil pipelines are created
|
|
500
|
+
// lazily on the first clipped draw (a rare path not worth the upfront
|
|
501
|
+
// compile cost for every blend-mode × format combination).
|
|
502
|
+
const key = meshPipelineCacheKey(blendMode, format, false);
|
|
376
503
|
if (this._pipelines.has(key))
|
|
377
504
|
continue;
|
|
378
505
|
promises.push(device.createRenderPipelineAsync(this._buildPipelineDescriptor(blendMode, format)).then(pipeline => {
|
|
379
506
|
this._pipelines.set(key, pipeline);
|
|
380
507
|
}));
|
|
508
|
+
if (!this._instancedPipelines.has(key)) {
|
|
509
|
+
promises.push(device.createRenderPipelineAsync(this._buildInstancedPipelineDescriptor(blendMode, format)).then(pipeline => {
|
|
510
|
+
this._instancedPipelines.set(key, pipeline);
|
|
511
|
+
}));
|
|
512
|
+
}
|
|
381
513
|
}
|
|
382
514
|
}
|
|
383
515
|
await Promise.all(promises);
|
|
@@ -388,6 +520,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
388
520
|
}
|
|
389
521
|
this._device = backend.device;
|
|
390
522
|
this._shaderModule = this._device.createShaderModule({ code: meshShaderSource });
|
|
523
|
+
this._instancedShaderModule = this._device.createShaderModule({ code: instancedMeshShaderSource });
|
|
391
524
|
this._uniformBindGroupLayout = this._device.createBindGroupLayout({
|
|
392
525
|
entries: [
|
|
393
526
|
{
|
|
@@ -414,27 +547,59 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
414
547
|
this._pipelineLayout = this._device.createPipelineLayout({
|
|
415
548
|
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
416
549
|
});
|
|
550
|
+
this._instancedTransformBindGroupLayout = this._device.createBindGroupLayout({
|
|
551
|
+
entries: [
|
|
552
|
+
{
|
|
553
|
+
binding: 0,
|
|
554
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
555
|
+
buffer: { type: 'uniform', hasDynamicOffset: true },
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
binding: 1,
|
|
559
|
+
visibility: GPUShaderStage.VERTEX,
|
|
560
|
+
buffer: { type: 'read-only-storage' },
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
});
|
|
564
|
+
this._instancedPipelineLayout = this._device.createPipelineLayout({
|
|
565
|
+
bindGroupLayouts: [this._instancedTransformBindGroupLayout, this._textureBindGroupLayout],
|
|
566
|
+
});
|
|
417
567
|
}
|
|
418
568
|
onDisconnect() {
|
|
419
569
|
this.flush();
|
|
420
570
|
this._vertexBuffer?.destroy();
|
|
421
571
|
this._indexBuffer?.destroy();
|
|
422
572
|
this._uniformBuffer?.destroy();
|
|
573
|
+
this._instancedUniformBuffer?.destroy();
|
|
574
|
+
this._instancedNodeIndexBuffer?.destroy();
|
|
423
575
|
this._pipelines.clear();
|
|
576
|
+
this._instancedPipelines.clear();
|
|
424
577
|
this._textureBindGroups = new WeakMap();
|
|
578
|
+
for (const entry of this._staticGeometryCache.values()) {
|
|
579
|
+
entry.vertexBuffer.destroy();
|
|
580
|
+
entry.indexBuffer.destroy();
|
|
581
|
+
}
|
|
582
|
+
this._staticGeometryCache.clear();
|
|
425
583
|
this._vertexBuffer = null;
|
|
426
584
|
this._indexBuffer = null;
|
|
427
585
|
this._uniformBuffer = null;
|
|
428
586
|
this._uniformBindGroup = null;
|
|
587
|
+
this._instancedUniformBuffer = null;
|
|
588
|
+
this._instancedNodeIndexBuffer = null;
|
|
589
|
+
this._instancedTransformBindGroup = null;
|
|
590
|
+
this._instancedTransformStorageBuffer = null;
|
|
429
591
|
this._pipelineLayout = null;
|
|
592
|
+
this._instancedPipelineLayout = null;
|
|
430
593
|
this._textureBindGroupLayout = null;
|
|
431
594
|
this._uniformBindGroupLayout = null;
|
|
595
|
+
this._instancedTransformBindGroupLayout = null;
|
|
432
596
|
this._shaderModule = null;
|
|
433
|
-
|
|
597
|
+
this._instancedShaderModule = null;
|
|
598
|
+
// Custom materials are owned by user code (one MeshMaterial can be shared
|
|
434
599
|
// across multiple Mesh instances). Their resources are released when the
|
|
435
|
-
// user calls
|
|
600
|
+
// user calls material.destroy(), which fires our _onDispose callback. On
|
|
436
601
|
// backend disconnect we eagerly release everything to avoid GPU leaks
|
|
437
|
-
// even if the user keeps the
|
|
602
|
+
// even if the user keeps the material reference around.
|
|
438
603
|
for (const resources of this._customShaders.values()) {
|
|
439
604
|
this._releaseCustomShaderResources(resources);
|
|
440
605
|
}
|
|
@@ -445,6 +610,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
445
610
|
this._vertexBufferCapacity = 0;
|
|
446
611
|
this._indexBufferCapacity = 0;
|
|
447
612
|
this._uniformBufferCapacity = 0;
|
|
613
|
+
this._instancedUniformBufferCapacity = 0;
|
|
614
|
+
this._instancedNodeIndexBufferCapacity = 0;
|
|
615
|
+
this._instancedNodeIndexData = new Uint32Array(0);
|
|
448
616
|
}
|
|
449
617
|
// ---------------------------------------------------------------------------
|
|
450
618
|
// Default-path helpers
|
|
@@ -490,16 +658,16 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
490
658
|
}
|
|
491
659
|
}
|
|
492
660
|
_getPipeline(key) {
|
|
493
|
-
const cacheKey =
|
|
661
|
+
const cacheKey = meshPipelineCacheKey(key.blendMode, key.format, key.stencil);
|
|
494
662
|
let pipeline = this._pipelines.get(cacheKey);
|
|
495
663
|
if (!pipeline) {
|
|
496
|
-
pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(key.blendMode, key.format));
|
|
664
|
+
pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(key.blendMode, key.format, key.stencil));
|
|
497
665
|
this._pipelines.set(cacheKey, pipeline);
|
|
498
666
|
}
|
|
499
667
|
return pipeline;
|
|
500
668
|
}
|
|
501
|
-
_buildPipelineDescriptor(blendMode, format) {
|
|
502
|
-
|
|
669
|
+
_buildPipelineDescriptor(blendMode, format, stencil = false) {
|
|
670
|
+
const descriptor = {
|
|
503
671
|
layout: this._pipelineLayout,
|
|
504
672
|
vertex: {
|
|
505
673
|
module: this._shaderModule,
|
|
@@ -532,6 +700,10 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
532
700
|
cullMode: 'none',
|
|
533
701
|
},
|
|
534
702
|
};
|
|
703
|
+
if (stencil) {
|
|
704
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
705
|
+
}
|
|
706
|
+
return descriptor;
|
|
535
707
|
}
|
|
536
708
|
_getTextureBindGroup(backend, texture) {
|
|
537
709
|
let group = this._textureBindGroups.get(texture);
|
|
@@ -548,6 +720,227 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
548
720
|
}
|
|
549
721
|
return group;
|
|
550
722
|
}
|
|
723
|
+
_getStaticBatchLength(startIndex) {
|
|
724
|
+
const first = this._drawCalls[startIndex];
|
|
725
|
+
if (!this._isStaticBatchCandidate(first)) {
|
|
726
|
+
return 1;
|
|
727
|
+
}
|
|
728
|
+
let length = 1;
|
|
729
|
+
for (let i = startIndex + 1; i < this._drawCallCount; i++) {
|
|
730
|
+
const next = this._drawCalls[i];
|
|
731
|
+
if (!this._isSameStaticBatch(first, next)) {
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
length++;
|
|
735
|
+
}
|
|
736
|
+
return length;
|
|
737
|
+
}
|
|
738
|
+
_isStaticBatchCandidate(drawCall) {
|
|
739
|
+
const command = drawCall.command;
|
|
740
|
+
return drawCall.customShader === null && command?.groupIndex !== undefined && drawCall.mesh.geometry?.usage === 'static';
|
|
741
|
+
}
|
|
742
|
+
_isSameStaticBatch(left, right) {
|
|
743
|
+
if (!this._isStaticBatchCandidate(left) || !this._isStaticBatchCandidate(right)) {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
return (left.command.groupIndex === right.command.groupIndex &&
|
|
747
|
+
left.mesh.geometry === right.mesh.geometry &&
|
|
748
|
+
left.texture === right.texture &&
|
|
749
|
+
left.blendMode === right.blendMode &&
|
|
750
|
+
left.command.material.pipelineKey === right.command.material.pipelineKey &&
|
|
751
|
+
left.command.material.bindKey === right.command.material.bindKey);
|
|
752
|
+
}
|
|
753
|
+
_uploadInstancedNodeIndices(startIndex, batchLength) {
|
|
754
|
+
this._ensureInstancedNodeIndexCapacity(batchLength);
|
|
755
|
+
let maxNodeIndex = 0;
|
|
756
|
+
for (let i = 0; i < batchLength; i++) {
|
|
757
|
+
const nodeIndex = this._drawCalls[startIndex + i].command.nodeIndex >>> 0;
|
|
758
|
+
this._instancedNodeIndexData[i] = nodeIndex;
|
|
759
|
+
if (nodeIndex > maxNodeIndex) {
|
|
760
|
+
maxNodeIndex = nodeIndex;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
this._device.queue.writeBuffer(this._instancedNodeIndexBuffer, 0, this._instancedNodeIndexData.buffer, this._instancedNodeIndexData.byteOffset, batchLength * Uint32Array.BYTES_PER_ELEMENT);
|
|
764
|
+
return maxNodeIndex;
|
|
765
|
+
}
|
|
766
|
+
_ensureInstancedNodeIndexCapacity(instanceCount) {
|
|
767
|
+
const requiredBytes = instanceCount * Uint32Array.BYTES_PER_ELEMENT;
|
|
768
|
+
if (this._instancedNodeIndexData.length < instanceCount) {
|
|
769
|
+
this._instancedNodeIndexData = new Uint32Array(Math.max(instanceCount, this._instancedNodeIndexData.length * 2 || 1));
|
|
770
|
+
}
|
|
771
|
+
if (requiredBytes > this._instancedNodeIndexBufferCapacity) {
|
|
772
|
+
this._instancedNodeIndexBuffer?.destroy();
|
|
773
|
+
this._instancedNodeIndexBufferCapacity = Math.max(requiredBytes, this._instancedNodeIndexBufferCapacity * 2 || Uint32Array.BYTES_PER_ELEMENT);
|
|
774
|
+
this._instancedNodeIndexBuffer = this._device.createBuffer({
|
|
775
|
+
size: this._instancedNodeIndexBufferCapacity,
|
|
776
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
_ensureInstancedUniformCapacity(drawCallCount) {
|
|
781
|
+
if (drawCallCount === 0) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const requiredBytes = drawCallCount * this._uniformAlignment;
|
|
785
|
+
if (requiredBytes > this._instancedUniformBufferCapacity) {
|
|
786
|
+
this._instancedUniformBuffer?.destroy();
|
|
787
|
+
this._instancedUniformBufferCapacity = Math.max(requiredBytes, this._instancedUniformBufferCapacity * 2 || this._uniformAlignment);
|
|
788
|
+
this._instancedUniformBuffer = this._device.createBuffer({
|
|
789
|
+
size: this._instancedUniformBufferCapacity,
|
|
790
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
791
|
+
});
|
|
792
|
+
this._instancedTransformBindGroup = null;
|
|
793
|
+
this._instancedTransformStorageBuffer = null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
_writeInstancedUniformSlot(slot, backend, premultiplySample) {
|
|
797
|
+
const data = this._instancedUniformScratch;
|
|
798
|
+
const projection = backend.view.getTransform();
|
|
799
|
+
data.fill(0);
|
|
800
|
+
data[0] = projection.a;
|
|
801
|
+
data[1] = projection.b;
|
|
802
|
+
data[4] = projection.c;
|
|
803
|
+
data[5] = projection.d;
|
|
804
|
+
data[8] = projection.x;
|
|
805
|
+
data[9] = projection.y;
|
|
806
|
+
data[10] = 1;
|
|
807
|
+
data[12] = premultiplySample ? 1 : 0;
|
|
808
|
+
this._device.queue.writeBuffer(this._instancedUniformBuffer, slot * this._uniformAlignment, data.buffer, data.byteOffset, transformUniformByteLength);
|
|
809
|
+
}
|
|
810
|
+
_getOrCreateInstancedTransformBindGroup(storageBuffer) {
|
|
811
|
+
if (this._instancedTransformBindGroup !== null && this._instancedTransformStorageBuffer === storageBuffer) {
|
|
812
|
+
return this._instancedTransformBindGroup;
|
|
813
|
+
}
|
|
814
|
+
this._instancedTransformStorageBuffer = storageBuffer;
|
|
815
|
+
this._instancedTransformBindGroup = this._device.createBindGroup({
|
|
816
|
+
layout: this._instancedTransformBindGroupLayout,
|
|
817
|
+
entries: [
|
|
818
|
+
{
|
|
819
|
+
binding: 0,
|
|
820
|
+
resource: {
|
|
821
|
+
buffer: this._instancedUniformBuffer,
|
|
822
|
+
size: transformUniformByteLength,
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
binding: 1,
|
|
827
|
+
resource: {
|
|
828
|
+
buffer: storageBuffer,
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
],
|
|
832
|
+
});
|
|
833
|
+
return this._instancedTransformBindGroup;
|
|
834
|
+
}
|
|
835
|
+
_getInstancedPipeline(key) {
|
|
836
|
+
const cacheKey = meshPipelineCacheKey(key.blendMode, key.format, key.stencil);
|
|
837
|
+
let pipeline = this._instancedPipelines.get(cacheKey);
|
|
838
|
+
if (!pipeline) {
|
|
839
|
+
pipeline = this._device.createRenderPipeline(this._buildInstancedPipelineDescriptor(key.blendMode, key.format, key.stencil));
|
|
840
|
+
this._instancedPipelines.set(cacheKey, pipeline);
|
|
841
|
+
}
|
|
842
|
+
return pipeline;
|
|
843
|
+
}
|
|
844
|
+
_buildInstancedPipelineDescriptor(blendMode, format, stencil = false) {
|
|
845
|
+
const descriptor = {
|
|
846
|
+
layout: this._instancedPipelineLayout,
|
|
847
|
+
vertex: {
|
|
848
|
+
module: this._instancedShaderModule,
|
|
849
|
+
entryPoint: 'vertexMain',
|
|
850
|
+
buffers: [
|
|
851
|
+
{
|
|
852
|
+
arrayStride: vertexStrideBytes,
|
|
853
|
+
stepMode: 'vertex',
|
|
854
|
+
attributes: [
|
|
855
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x2' },
|
|
856
|
+
{ shaderLocation: 1, offset: 8, format: 'float32x2' },
|
|
857
|
+
{ shaderLocation: 2, offset: 16, format: 'unorm8x4' },
|
|
858
|
+
],
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
arrayStride: Uint32Array.BYTES_PER_ELEMENT,
|
|
862
|
+
stepMode: 'instance',
|
|
863
|
+
attributes: [{ shaderLocation: 6, offset: 0, format: 'uint32' }],
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
},
|
|
867
|
+
fragment: {
|
|
868
|
+
module: this._instancedShaderModule,
|
|
869
|
+
entryPoint: 'fragmentMain',
|
|
870
|
+
targets: [
|
|
871
|
+
{
|
|
872
|
+
format,
|
|
873
|
+
blend: getWebGpuBlendState(blendMode),
|
|
874
|
+
writeMask: GPUColorWrite.ALL,
|
|
875
|
+
},
|
|
876
|
+
],
|
|
877
|
+
},
|
|
878
|
+
primitive: {
|
|
879
|
+
topology: 'triangle-list',
|
|
880
|
+
cullMode: 'none',
|
|
881
|
+
},
|
|
882
|
+
};
|
|
883
|
+
if (stencil) {
|
|
884
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
885
|
+
}
|
|
886
|
+
return descriptor;
|
|
887
|
+
}
|
|
888
|
+
_getOrCreateStaticGeometryEntry(mesh) {
|
|
889
|
+
const geometry = mesh.geometry;
|
|
890
|
+
if (geometry?.usage !== 'static') {
|
|
891
|
+
throw new Error('Static mesh batching requires Geometry with usage="static".');
|
|
892
|
+
}
|
|
893
|
+
const existing = this._staticGeometryCache.get(geometry);
|
|
894
|
+
if (existing !== undefined) {
|
|
895
|
+
return existing;
|
|
896
|
+
}
|
|
897
|
+
const vertexData = new ArrayBuffer(mesh.vertexCount * vertexStrideBytes);
|
|
898
|
+
const vertexFloatView = new Float32Array(vertexData);
|
|
899
|
+
const vertexUintView = new Uint32Array(vertexData);
|
|
900
|
+
this._writeMeshVerticesIntoBuffer(mesh, 0, vertexFloatView, vertexUintView);
|
|
901
|
+
// Allocate one extra element when indexCount is odd so the GPU buffer and
|
|
902
|
+
// writeBuffer byte count can be rounded up to 4 without a buffer overread.
|
|
903
|
+
const indexData = new Uint16Array(mesh.indexCount + (mesh.indexCount & 1));
|
|
904
|
+
if (mesh.indices !== null) {
|
|
905
|
+
indexData.set(mesh.indices, 0);
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
for (let i = 0; i < mesh.indexCount; i++) {
|
|
909
|
+
indexData[i] = i;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const indexByteLen = mesh.indexCount * Uint16Array.BYTES_PER_ELEMENT;
|
|
913
|
+
const alignedIndexByteLen = (indexByteLen + 3) & -4;
|
|
914
|
+
const vertexBuffer = this._device.createBuffer({
|
|
915
|
+
size: vertexData.byteLength,
|
|
916
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
917
|
+
});
|
|
918
|
+
const indexBuffer = this._device.createBuffer({
|
|
919
|
+
size: alignedIndexByteLen,
|
|
920
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
921
|
+
});
|
|
922
|
+
this._device.queue.writeBuffer(vertexBuffer, 0, vertexData, 0, vertexData.byteLength);
|
|
923
|
+
this._device.queue.writeBuffer(indexBuffer, 0, indexData.buffer, indexData.byteOffset, alignedIndexByteLen);
|
|
924
|
+
const disposeListener = () => {
|
|
925
|
+
const entry = this._staticGeometryCache.get(geometry);
|
|
926
|
+
if (entry === undefined) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
entry.vertexBuffer.destroy();
|
|
930
|
+
entry.indexBuffer.destroy();
|
|
931
|
+
this._staticGeometryCache.delete(geometry);
|
|
932
|
+
};
|
|
933
|
+
geometry._onDispose(disposeListener);
|
|
934
|
+
const created = {
|
|
935
|
+
geometry,
|
|
936
|
+
vertexBuffer,
|
|
937
|
+
indexBuffer,
|
|
938
|
+
indexCount: mesh.indexCount,
|
|
939
|
+
disposeListener,
|
|
940
|
+
};
|
|
941
|
+
this._staticGeometryCache.set(geometry, created);
|
|
942
|
+
return created;
|
|
943
|
+
}
|
|
551
944
|
_ensureVertexCapacity(vertexCount) {
|
|
552
945
|
const requiredBytes = vertexCount * vertexStrideBytes;
|
|
553
946
|
if (requiredBytes > this._vertexData.byteLength) {
|
|
@@ -566,13 +959,16 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
566
959
|
}
|
|
567
960
|
}
|
|
568
961
|
_ensureIndexCapacity(indexCount) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
962
|
+
// GPUQueue.writeBuffer requires the byte count to be a multiple of 4.
|
|
963
|
+
// Round up: odd Uint16 counts (e.g. a 3-index triangle) would otherwise
|
|
964
|
+
// produce 6-byte writes which the WebGPU validation layer rejects.
|
|
965
|
+
const requiredBytes = (indexCount * Uint16Array.BYTES_PER_ELEMENT + 3) & -4;
|
|
966
|
+
if (this._packedIndexData.length * Uint16Array.BYTES_PER_ELEMENT < requiredBytes) {
|
|
967
|
+
this._packedIndexData = new Uint16Array(Math.max(requiredBytes / Uint16Array.BYTES_PER_ELEMENT, this._packedIndexData.length === 0 ? 2 : this._packedIndexData.length * 2));
|
|
572
968
|
}
|
|
573
969
|
if (requiredBytes > this._indexBufferCapacity) {
|
|
574
970
|
this._indexBuffer?.destroy();
|
|
575
|
-
this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ?
|
|
971
|
+
this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ? 4 : this._indexBufferCapacity * 2);
|
|
576
972
|
this._indexBuffer = this._device.createBuffer({
|
|
577
973
|
size: this._indexBufferCapacity,
|
|
578
974
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
@@ -620,19 +1016,19 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
620
1016
|
resources.totalIndices = 0;
|
|
621
1017
|
}
|
|
622
1018
|
}
|
|
623
|
-
_getOrCreateCustomShaderResources(
|
|
624
|
-
let resources = this._customShaders.get(
|
|
1019
|
+
_getOrCreateCustomShaderResources(material) {
|
|
1020
|
+
let resources = this._customShaders.get(material);
|
|
625
1021
|
if (resources !== undefined) {
|
|
626
1022
|
return resources;
|
|
627
1023
|
}
|
|
628
1024
|
if (this._device === null) {
|
|
629
1025
|
throw new Error('WebGpuMeshRenderer is not connected to a backend.');
|
|
630
1026
|
}
|
|
631
|
-
if (shader.wgsl === null) {
|
|
632
|
-
throw new Error('
|
|
1027
|
+
if (material.shader.wgsl === null) {
|
|
1028
|
+
throw new Error('Mesh material shader has no `wgsl` source; cannot render through the WebGPU backend.');
|
|
633
1029
|
}
|
|
634
1030
|
const device = this._device;
|
|
635
|
-
const shaderModule = device.createShaderModule({ code: shader.wgsl });
|
|
1031
|
+
const shaderModule = device.createShaderModule({ code: material.shader.wgsl });
|
|
636
1032
|
const meshUniformLayout = device.createBindGroupLayout({
|
|
637
1033
|
entries: [
|
|
638
1034
|
{
|
|
@@ -648,7 +1044,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
648
1044
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
649
1045
|
],
|
|
650
1046
|
});
|
|
651
|
-
const userLayout = this._buildUserBindGroupLayout(device,
|
|
1047
|
+
const userLayout = this._buildUserBindGroupLayout(device, material);
|
|
652
1048
|
const pipelineLayout = device.createPipelineLayout({
|
|
653
1049
|
bindGroupLayouts: [meshUniformLayout, meshTextureLayout, userLayout],
|
|
654
1050
|
});
|
|
@@ -687,13 +1083,13 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
687
1083
|
totalVertices: 0,
|
|
688
1084
|
totalIndices: 0,
|
|
689
1085
|
};
|
|
690
|
-
this._customShaders.set(
|
|
691
|
-
// When the user calls
|
|
692
|
-
|
|
693
|
-
const r = this._customShaders.get(
|
|
1086
|
+
this._customShaders.set(material, resources);
|
|
1087
|
+
// When the user calls material.destroy(), evict and release.
|
|
1088
|
+
material._onDispose(() => {
|
|
1089
|
+
const r = this._customShaders.get(material);
|
|
694
1090
|
if (r !== undefined) {
|
|
695
1091
|
this._releaseCustomShaderResources(r);
|
|
696
|
-
this._customShaders.delete(
|
|
1092
|
+
this._customShaders.delete(material);
|
|
697
1093
|
}
|
|
698
1094
|
});
|
|
699
1095
|
return resources;
|
|
@@ -716,14 +1112,14 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
716
1112
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
717
1113
|
});
|
|
718
1114
|
}
|
|
719
|
-
// Index buffer
|
|
720
|
-
const indexBytes = resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT;
|
|
721
|
-
if (resources.indexData.length <
|
|
722
|
-
resources.indexData = new Uint16Array(Math.max(
|
|
1115
|
+
// Index buffer — capacity must be 4-byte aligned for GPUQueue.writeBuffer.
|
|
1116
|
+
const indexBytes = (resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT + 3) & -4;
|
|
1117
|
+
if (resources.indexData.length * Uint16Array.BYTES_PER_ELEMENT < indexBytes) {
|
|
1118
|
+
resources.indexData = new Uint16Array(Math.max(indexBytes / Uint16Array.BYTES_PER_ELEMENT, resources.indexData.length * 2));
|
|
723
1119
|
}
|
|
724
1120
|
if (indexBytes > resources.indexBufferCapacity) {
|
|
725
1121
|
resources.indexBuffer?.destroy();
|
|
726
|
-
resources.indexBufferCapacity = Math.max(indexBytes, resources.indexBufferCapacity * 2 ||
|
|
1122
|
+
resources.indexBufferCapacity = Math.max(indexBytes, resources.indexBufferCapacity * 2 || 4);
|
|
727
1123
|
resources.indexBuffer = device.createBuffer({
|
|
728
1124
|
size: resources.indexBufferCapacity,
|
|
729
1125
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
@@ -764,7 +1160,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
764
1160
|
uintView[targetIndex + 4] = colors !== null ? colors[i] : 0xffffffff;
|
|
765
1161
|
}
|
|
766
1162
|
}
|
|
767
|
-
_writeCustomMeshUniform(
|
|
1163
|
+
_writeCustomMeshUniform(_material, resources, drawCursor, mesh, backend) {
|
|
768
1164
|
// Layout: mat3x3 projection (48B) + mat3x3 translation (48B) + vec4 tint (16B) = 112B.
|
|
769
1165
|
// WGSL mat3x3 stores 3 vec3 columns padded to vec4 alignment.
|
|
770
1166
|
const slotBytes = meshUniformAlignment;
|
|
@@ -809,19 +1205,24 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
809
1205
|
data[off + 10] = 1;
|
|
810
1206
|
data[off + 11] = 0;
|
|
811
1207
|
off += 12;
|
|
812
|
-
// tint (vec4)
|
|
1208
|
+
// tint (vec4). RGB are 0..255; normalize to 0..1 for the shader multiply
|
|
1209
|
+
// (u_mesh.tint is documented as 0..1, matching the default path above).
|
|
813
1210
|
const tint = mesh.tint;
|
|
814
|
-
data[off + 0] = tint.r;
|
|
815
|
-
data[off + 1] = tint.g;
|
|
816
|
-
data[off + 2] = tint.b;
|
|
1211
|
+
data[off + 0] = tint.r / 255;
|
|
1212
|
+
data[off + 1] = tint.g / 255;
|
|
1213
|
+
data[off + 2] = tint.b / 255;
|
|
817
1214
|
data[off + 3] = tint.a;
|
|
818
1215
|
this._device.queue.writeBuffer(resources.meshUniformBuffer, drawCursor * slotBytes, data);
|
|
819
1216
|
}
|
|
820
|
-
_getOrCreateCustomPipeline(resources, blendMode, format) {
|
|
821
|
-
|
|
1217
|
+
_getOrCreateCustomPipeline(resources, blendMode, format, stencil) {
|
|
1218
|
+
// The stencil dimension keeps the clip and no-clip variants distinct,
|
|
1219
|
+
// mirroring the default and static-batch caches: a stencil pipeline carries
|
|
1220
|
+
// depth/stencil state and is only valid in a pass with the matching
|
|
1221
|
+
// attachment, so the two are never interchangeable.
|
|
1222
|
+
const cacheKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
|
|
822
1223
|
let pipeline = resources.pipelines.get(cacheKey);
|
|
823
1224
|
if (pipeline === undefined) {
|
|
824
|
-
|
|
1225
|
+
const descriptor = {
|
|
825
1226
|
layout: resources.pipelineLayout,
|
|
826
1227
|
vertex: {
|
|
827
1228
|
module: resources.shaderModule,
|
|
@@ -853,7 +1254,14 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
853
1254
|
topology: 'triangle-list',
|
|
854
1255
|
cullMode: 'none',
|
|
855
1256
|
},
|
|
856
|
-
}
|
|
1257
|
+
};
|
|
1258
|
+
// While a geometric clip is active the coordinator's pass carries a
|
|
1259
|
+
// depth/stencil attachment; the content pipeline must test stencil ==
|
|
1260
|
+
// reference and leave depth/stencil otherwise inert to match it.
|
|
1261
|
+
if (stencil) {
|
|
1262
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
1263
|
+
}
|
|
1264
|
+
pipeline = this._device.createRenderPipeline(descriptor);
|
|
857
1265
|
resources.pipelines.set(cacheKey, pipeline);
|
|
858
1266
|
}
|
|
859
1267
|
return pipeline;
|
|
@@ -873,10 +1281,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
873
1281
|
}
|
|
874
1282
|
return group;
|
|
875
1283
|
}
|
|
876
|
-
_buildUserBindGroupLayout(device,
|
|
1284
|
+
_buildUserBindGroupLayout(device, material) {
|
|
877
1285
|
const entries = [];
|
|
878
|
-
const userUniforms = shader.uniforms;
|
|
879
|
-
Object.values(userUniforms).some(v => !isTextureUniform(v));
|
|
880
1286
|
// Binding 0 always reserved for the user UBO (even if empty), so the
|
|
881
1287
|
// bind-group layout is stable across user-uniform mutations.
|
|
882
1288
|
entries.push({
|
|
@@ -884,15 +1290,12 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
884
1290
|
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
885
1291
|
buffer: { type: 'uniform' },
|
|
886
1292
|
});
|
|
1293
|
+
const textureBindings = collectTextureBindings(material);
|
|
1294
|
+
if (textureBindings.length > maxCustomTextureSlots) {
|
|
1295
|
+
throw new Error(`Mesh material requested more than ${maxCustomTextureSlots} user texture bindings.`);
|
|
1296
|
+
}
|
|
887
1297
|
let bindingIndex = 1;
|
|
888
|
-
let
|
|
889
|
-
for (const value of Object.values(userUniforms)) {
|
|
890
|
-
if (!isTextureUniform(value)) {
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
if (textureCount >= maxCustomTextureSlots) {
|
|
894
|
-
throw new Error(`MeshShader requested more than ${maxCustomTextureSlots} user texture uniforms.`);
|
|
895
|
-
}
|
|
1298
|
+
for (let t = 0; t < textureBindings.length; t++) {
|
|
896
1299
|
entries.push({
|
|
897
1300
|
binding: bindingIndex,
|
|
898
1301
|
visibility: GPUShaderStage.FRAGMENT,
|
|
@@ -905,14 +1308,12 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
905
1308
|
sampler: { type: 'filtering' },
|
|
906
1309
|
});
|
|
907
1310
|
bindingIndex++;
|
|
908
|
-
textureCount++;
|
|
909
1311
|
}
|
|
910
1312
|
return device.createBindGroupLayout({ entries });
|
|
911
1313
|
}
|
|
912
|
-
_uploadUserUniforms(
|
|
1314
|
+
_uploadUserUniforms(material, resources) {
|
|
913
1315
|
const device = this._device;
|
|
914
|
-
const
|
|
915
|
-
const scalarValues = Object.values(uniforms).filter(v => !isTextureUniform(v));
|
|
1316
|
+
const scalarValues = collectScalarUniforms(material);
|
|
916
1317
|
// Always create a UBO (even if empty) since binding 0 of the user layout
|
|
917
1318
|
// is fixed. Min size 16 bytes to satisfy WebGPU's minimum buffer size.
|
|
918
1319
|
const slotCount = Math.max(scalarValues.length, 1);
|
|
@@ -950,16 +1351,13 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
950
1351
|
}
|
|
951
1352
|
device.queue.writeBuffer(resources.userUniformBuffer, 0, data);
|
|
952
1353
|
}
|
|
953
|
-
_buildUserBindGroup(backend,
|
|
1354
|
+
_buildUserBindGroup(backend, material, resources) {
|
|
954
1355
|
const device = this._device;
|
|
955
1356
|
const entries = [];
|
|
956
1357
|
entries.push({ binding: 0, resource: { buffer: resources.userUniformBuffer } });
|
|
957
1358
|
let bindingIndex = 1;
|
|
958
|
-
for (const
|
|
959
|
-
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
const binding = backend.getTextureBinding(value);
|
|
1359
|
+
for (const texture of collectTextureBindings(material)) {
|
|
1360
|
+
const binding = backend.getTextureBinding(texture);
|
|
963
1361
|
entries.push({ binding: bindingIndex, resource: binding.view });
|
|
964
1362
|
bindingIndex++;
|
|
965
1363
|
entries.push({ binding: bindingIndex, resource: binding.sampler });
|
|
@@ -997,6 +1395,34 @@ function isTextureUniform(value) {
|
|
|
997
1395
|
!(value instanceof Int32Array) &&
|
|
998
1396
|
!Array.isArray(value));
|
|
999
1397
|
}
|
|
1398
|
+
/** Scalar/vector/matrix uniforms (texture values excluded) in declaration order. */
|
|
1399
|
+
function collectScalarUniforms(material) {
|
|
1400
|
+
const result = [];
|
|
1401
|
+
for (const value of Object.values(material.uniforms)) {
|
|
1402
|
+
if (!isTextureUniform(value)) {
|
|
1403
|
+
result.push(value);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return result;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Texture bindings claimed by the material, in a stable order: texture-valued
|
|
1410
|
+
* entries of `uniforms` first (declaration order), then the dedicated
|
|
1411
|
+
* `textures` map (declaration order). The WGSL source must declare its
|
|
1412
|
+
* `@group(2)` texture/sampler pairs in this same order.
|
|
1413
|
+
*/
|
|
1414
|
+
function collectTextureBindings(material) {
|
|
1415
|
+
const result = [];
|
|
1416
|
+
for (const value of Object.values(material.uniforms)) {
|
|
1417
|
+
if (isTextureUniform(value)) {
|
|
1418
|
+
result.push(value);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
for (const texture of Object.values(material.textures)) {
|
|
1422
|
+
result.push(texture);
|
|
1423
|
+
}
|
|
1424
|
+
return result;
|
|
1425
|
+
}
|
|
1000
1426
|
|
|
1001
1427
|
export { WebGpuMeshRenderer };
|
|
1002
1428
|
//# sourceMappingURL=WebGpuMeshRenderer.js.map
|