@codexo/exojs 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -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/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/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 +38 -0
- package/dist/esm/rendering/TransformBuffer.js +116 -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 +34 -0
- package/dist/esm/rendering/gradient/Gradient.js +114 -0
- package/dist/esm/rendering/gradient/Gradient.js.map +1 -0
- package/dist/esm/rendering/gradient/LinearGradient.d.ts +10 -0
- package/dist/esm/rendering/gradient/LinearGradient.js +26 -0
- package/dist/esm/rendering/gradient/LinearGradient.js.map +1 -0
- package/dist/esm/rendering/gradient/RadialGradient.d.ts +10 -0
- package/dist/esm/rendering/gradient/RadialGradient.js +25 -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 +67 -0
- package/dist/esm/rendering/plan/RenderCommand.js +94 -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/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 +9 -0
- package/dist/esm/rendering/plan/RenderPlanPlayer.js +56 -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/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 +36 -0
- package/dist/esm/rendering/sprite/spriteMaterialSources.js +128 -0
- package/dist/esm/rendering/sprite/spriteMaterialSources.js.map +1 -0
- package/dist/esm/rendering/text/TextStyle.d.ts +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/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 +40 -0
- package/dist/esm/rendering/webgl2/WebGl2Backend.js +303 -22
- 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/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 +12 -0
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js +214 -58
- 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.js +4 -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/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/webgpu/WebGpuBackend.d.ts +50 -0
- package/dist/esm/rendering/webgpu/WebGpuBackend.js +135 -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 +488 -74
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +13 -17
- 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 +16 -0
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js +335 -26
- 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.js +14 -12
- package/dist/esm/rendering/webgpu/WebGpuTextRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.d.ts +16 -0
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js +57 -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/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 +5424 -2205
- package/dist/exo.esm.js.map +1 -1
- package/package.json +30 -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,31 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
92
177
|
if (backend === null) {
|
|
93
178
|
throw new Error('WebGpuMeshRenderer is not connected to a backend.');
|
|
94
179
|
}
|
|
95
|
-
const customShader = mesh.
|
|
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.');
|
|
183
|
+
}
|
|
184
|
+
if (customShader !== null && backend._passCoordinator.stencilActive) {
|
|
185
|
+
// The WebGPU geometric stencil MVP supports clipping default-material
|
|
186
|
+
// Sprites and Mesh/Graphics (which select stencil-enabled pipeline
|
|
187
|
+
// variants below); a custom MeshMaterial under a Geometry clip would need
|
|
188
|
+
// its own stencil pipeline variants and is not supported yet. Throw at
|
|
189
|
+
// collection time (inside the clip scope's try) so the surrounding
|
|
190
|
+
// push/pop balances cleanly, rather than at flush time where the pop has
|
|
191
|
+
// not yet run.
|
|
192
|
+
throw new Error('WebGPU geometry stencil clipping currently supports default-material Sprites, Meshes, and Graphics. A custom-material Mesh under a Geometry clip (RenderNode.clip with a Geometry clipShape) is not supported yet. Use a Rectangle clipShape (scissor) or the WebGL2 backend instead.');
|
|
98
193
|
}
|
|
99
194
|
const vertexCount = mesh.vertexCount;
|
|
100
195
|
if (vertexCount === 0) {
|
|
101
196
|
return;
|
|
102
197
|
}
|
|
103
|
-
|
|
198
|
+
// The material owns its blend mode; the mesh's own blendMode overrides it
|
|
199
|
+
// when set away from the default (Normal). Default-path meshes keep their
|
|
200
|
+
// own blendMode verbatim.
|
|
201
|
+
const blendMode = customShader !== null && mesh.blendMode === BlendModes.Normal ? customShader.blendMode : mesh.blendMode;
|
|
104
202
|
backend.setBlendMode(blendMode);
|
|
105
203
|
const meshTexture = mesh.texture ?? Texture.white;
|
|
204
|
+
const command = backend.activeDrawCommand;
|
|
106
205
|
// backend.shouldPremultiplyTextureSample expects RenderTexture-or-Texture.
|
|
107
206
|
// Both branches are valid here. Premultiply flag is ignored by custom
|
|
108
207
|
// shaders (they handle premultiplication themselves), but we still record
|
|
@@ -123,6 +222,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
123
222
|
const drawCall = {
|
|
124
223
|
mesh,
|
|
125
224
|
customShader,
|
|
225
|
+
command,
|
|
126
226
|
blendMode,
|
|
127
227
|
texture: meshTexture,
|
|
128
228
|
premultiplySample,
|
|
@@ -151,13 +251,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
151
251
|
// Honor a pending clear with an empty pass so createColorAttachment
|
|
152
252
|
// consumes the clear-state once.
|
|
153
253
|
if (backend.clearRequested) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
colorAttachments: [backend.createColorAttachment()],
|
|
157
|
-
});
|
|
158
|
-
backend.stats.renderPasses++;
|
|
159
|
-
pass.end();
|
|
160
|
-
backend.submit(encoder.finish());
|
|
254
|
+
backend._passCoordinator.acquirePass();
|
|
255
|
+
backend._passCoordinator.endPass();
|
|
161
256
|
}
|
|
162
257
|
this._resetFrame();
|
|
163
258
|
return;
|
|
@@ -166,7 +261,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
166
261
|
// buffers, so default offsets are independent of custom offsets).
|
|
167
262
|
let defaultVertices = 0;
|
|
168
263
|
let defaultIndices = 0;
|
|
169
|
-
const customVertexCursors = new Map(); // running vertex count per
|
|
264
|
+
const customVertexCursors = new Map(); // running vertex count per material
|
|
170
265
|
const customIndexCursors = new Map();
|
|
171
266
|
for (let i = 0; i < this._drawCallCount; i++) {
|
|
172
267
|
const dc = this._drawCalls[i];
|
|
@@ -192,6 +287,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
192
287
|
// each custom-shader resource manages its own.
|
|
193
288
|
const defaultDrawCalls = this._drawCallCount - this._totalCustomDraws();
|
|
194
289
|
this._ensureUniformCapacity(defaultDrawCalls);
|
|
290
|
+
this._ensureInstancedUniformCapacity(this._drawCallCount);
|
|
195
291
|
// Phase 3: pack default-path vertex/index/uniform data.
|
|
196
292
|
const defaultUniformBytes = defaultDrawCalls * this._uniformAlignment;
|
|
197
293
|
const defaultUniformData = defaultUniformBytes > 0 ? new ArrayBuffer(defaultUniformBytes) : null;
|
|
@@ -227,8 +323,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
227
323
|
defaultUniformIndex++;
|
|
228
324
|
}
|
|
229
325
|
}
|
|
230
|
-
// Phase 3b: pack custom-path vertex/index/uniform data per
|
|
231
|
-
for (const [
|
|
326
|
+
// Phase 3b: pack custom-path vertex/index/uniform data per material.
|
|
327
|
+
for (const [material, resources] of this._customShaders) {
|
|
232
328
|
if (resources.drawCount === 0) {
|
|
233
329
|
continue;
|
|
234
330
|
}
|
|
@@ -239,7 +335,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
239
335
|
let drawCursor = 0;
|
|
240
336
|
for (let i = 0; i < this._drawCallCount; i++) {
|
|
241
337
|
const dc = this._drawCalls[i];
|
|
242
|
-
if (dc.customShader !==
|
|
338
|
+
if (dc.customShader !== material)
|
|
243
339
|
continue;
|
|
244
340
|
this._writeMeshVerticesIntoBuffer(dc.mesh, vWritten, resources.vertexFloatView, resources.vertexUintView);
|
|
245
341
|
if (dc.mesh.indices !== null) {
|
|
@@ -251,16 +347,16 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
251
347
|
}
|
|
252
348
|
}
|
|
253
349
|
// Write mesh-uniform slot (proj/trans/tint) with dynamic offset.
|
|
254
|
-
this._writeCustomMeshUniform(
|
|
350
|
+
this._writeCustomMeshUniform(material, resources, drawCursor, dc.mesh, backend);
|
|
255
351
|
vWritten += dc.vertexCount;
|
|
256
352
|
iWritten += dc.indexCount;
|
|
257
353
|
drawCursor++;
|
|
258
354
|
}
|
|
259
355
|
device.queue.writeBuffer(resources.vertexBuffer, 0, resources.vertexData, 0, resources.totalVertices * vertexStrideBytes);
|
|
260
356
|
device.queue.writeBuffer(resources.indexBuffer, 0, resources.indexData.buffer, resources.indexData.byteOffset, resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
261
|
-
// Build/refresh user uniform UBO from
|
|
262
|
-
//
|
|
263
|
-
this._uploadUserUniforms(
|
|
357
|
+
// Build/refresh user uniform UBO from the material (re-built every frame
|
|
358
|
+
// so mutations to material.uniforms.X are picked up).
|
|
359
|
+
this._uploadUserUniforms(material, resources);
|
|
264
360
|
}
|
|
265
361
|
// Phase 4: single writeBuffer per resource for the default path.
|
|
266
362
|
if (defaultVertices > 0) {
|
|
@@ -271,30 +367,66 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
271
367
|
device.queue.writeBuffer(this._uniformBuffer, 0, defaultUniformData, 0, defaultUniformBytes);
|
|
272
368
|
}
|
|
273
369
|
// Phase 5: single render pass with one drawIndexed per mesh, switching
|
|
274
|
-
// pipeline+bind groups between default and custom paths as needed.
|
|
275
|
-
|
|
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
|
-
}
|
|
370
|
+
// pipeline+bind groups between default and custom paths as needed. The
|
|
371
|
+
// coordinator owns the GPU pass (load/clear resolution, pass count and
|
|
372
|
+
// scissor are applied there) and ends + submits it below.
|
|
373
|
+
const pass = backend._passCoordinator.acquirePass().pass;
|
|
284
374
|
const renderTargetFormat = backend.renderTargetFormat;
|
|
375
|
+
// A clip scope flushes the active renderer on push/pop, so every draw call
|
|
376
|
+
// in this batch shares one stencil state — read it once. While active, the
|
|
377
|
+
// coordinator's pass carries a depth/stencil attachment, so the default and
|
|
378
|
+
// static-batch pipelines must select their stencil-enabled variants. The
|
|
379
|
+
// custom path never reaches here under a clip (render() throws at collection
|
|
380
|
+
// time), so its pipelines stay stencil-free.
|
|
381
|
+
const stencil = backend._passCoordinator.stencilActive;
|
|
285
382
|
let lastShader = null;
|
|
286
383
|
let lastBlendMode = null;
|
|
287
384
|
let lastFormat = null;
|
|
288
385
|
let lastTexture = null;
|
|
289
386
|
let defaultDrawCursor = 0;
|
|
387
|
+
let instancedDrawCursor = 0;
|
|
290
388
|
const customDrawCursors = new Map();
|
|
291
389
|
for (let i = 0; i < this._drawCallCount; i++) {
|
|
292
390
|
const dc = this._drawCalls[i];
|
|
293
391
|
if (dc.customShader === null) {
|
|
392
|
+
const batchLength = this._getStaticBatchLength(i);
|
|
393
|
+
if (batchLength >= 2) {
|
|
394
|
+
const needsPipeline = lastShader !== 'instanced' || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
|
|
395
|
+
if (needsPipeline) {
|
|
396
|
+
pass.setPipeline(this._getInstancedPipeline({ blendMode: dc.blendMode, format: renderTargetFormat, stencil }));
|
|
397
|
+
lastShader = 'instanced';
|
|
398
|
+
lastBlendMode = dc.blendMode;
|
|
399
|
+
lastFormat = renderTargetFormat;
|
|
400
|
+
lastTexture = null;
|
|
401
|
+
}
|
|
402
|
+
const maxNodeIndex = this._uploadInstancedNodeIndices(i, batchLength);
|
|
403
|
+
const storage = backend.getTransformStorageBuffer(maxNodeIndex + 1);
|
|
404
|
+
this._writeInstancedUniformSlot(instancedDrawCursor, backend, dc.premultiplySample);
|
|
405
|
+
pass.setBindGroup(0, this._getOrCreateInstancedTransformBindGroup(storage.buffer), [instancedDrawCursor * this._uniformAlignment]);
|
|
406
|
+
if (dc.texture !== lastTexture) {
|
|
407
|
+
lastTexture = dc.texture;
|
|
408
|
+
pass.setBindGroup(1, this._getTextureBindGroup(backend, dc.texture));
|
|
409
|
+
}
|
|
410
|
+
const staticGeometry = this._getOrCreateStaticGeometryEntry(dc.mesh);
|
|
411
|
+
const instanceNodeIndexBuffer = this._instancedNodeIndexBuffer;
|
|
412
|
+
if (instanceNodeIndexBuffer === null) {
|
|
413
|
+
throw new Error('Instanced node-index buffer must be initialized before drawing.');
|
|
414
|
+
}
|
|
415
|
+
pass.setVertexBuffer(0, staticGeometry.vertexBuffer);
|
|
416
|
+
pass.setVertexBuffer(1, instanceNodeIndexBuffer);
|
|
417
|
+
pass.setIndexBuffer(staticGeometry.indexBuffer, 'uint16');
|
|
418
|
+
pass.drawIndexed(staticGeometry.indexCount, batchLength);
|
|
419
|
+
backend.stats.batches++;
|
|
420
|
+
backend.stats.drawCalls++;
|
|
421
|
+
defaultDrawCursor += batchLength;
|
|
422
|
+
instancedDrawCursor++;
|
|
423
|
+
i += batchLength - 1;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
294
426
|
// ----- Default path -----
|
|
295
427
|
const needsPipeline = lastShader !== 'default' || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
|
|
296
428
|
if (needsPipeline) {
|
|
297
|
-
pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat }));
|
|
429
|
+
pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat, stencil }));
|
|
298
430
|
lastShader = 'default';
|
|
299
431
|
lastBlendMode = dc.blendMode;
|
|
300
432
|
lastFormat = renderTargetFormat;
|
|
@@ -319,7 +451,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
319
451
|
// (Spector.js, Chrome DevTools' WebGPU panel) show meaningful
|
|
320
452
|
// labels for the otherwise-anonymous mesh draws inside the
|
|
321
453
|
// batched render pass.
|
|
322
|
-
pass.pushDebugGroup('
|
|
454
|
+
pass.pushDebugGroup('MeshMaterial (custom)');
|
|
323
455
|
if (needsPipeline) {
|
|
324
456
|
pass.setPipeline(this._getOrCreateCustomPipeline(resources, dc.blendMode, renderTargetFormat));
|
|
325
457
|
lastShader = dc.customShader;
|
|
@@ -344,8 +476,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
344
476
|
backend.stats.batches++;
|
|
345
477
|
backend.stats.drawCalls++;
|
|
346
478
|
}
|
|
347
|
-
|
|
348
|
-
backend.submit(encoder.finish());
|
|
479
|
+
backend._passCoordinator.endPass();
|
|
349
480
|
this._resetFrame();
|
|
350
481
|
}
|
|
351
482
|
destroy() {
|
|
@@ -372,12 +503,20 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
372
503
|
const promises = [];
|
|
373
504
|
for (const blendMode of blendModes) {
|
|
374
505
|
for (const format of formats) {
|
|
375
|
-
|
|
506
|
+
// Prewarm only the no-clip variants; the stencil pipelines are created
|
|
507
|
+
// lazily on the first clipped draw (a rare path not worth the upfront
|
|
508
|
+
// compile cost for every blend-mode × format combination).
|
|
509
|
+
const key = meshPipelineCacheKey(blendMode, format, false);
|
|
376
510
|
if (this._pipelines.has(key))
|
|
377
511
|
continue;
|
|
378
512
|
promises.push(device.createRenderPipelineAsync(this._buildPipelineDescriptor(blendMode, format)).then(pipeline => {
|
|
379
513
|
this._pipelines.set(key, pipeline);
|
|
380
514
|
}));
|
|
515
|
+
if (!this._instancedPipelines.has(key)) {
|
|
516
|
+
promises.push(device.createRenderPipelineAsync(this._buildInstancedPipelineDescriptor(blendMode, format)).then(pipeline => {
|
|
517
|
+
this._instancedPipelines.set(key, pipeline);
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
381
520
|
}
|
|
382
521
|
}
|
|
383
522
|
await Promise.all(promises);
|
|
@@ -388,6 +527,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
388
527
|
}
|
|
389
528
|
this._device = backend.device;
|
|
390
529
|
this._shaderModule = this._device.createShaderModule({ code: meshShaderSource });
|
|
530
|
+
this._instancedShaderModule = this._device.createShaderModule({ code: instancedMeshShaderSource });
|
|
391
531
|
this._uniformBindGroupLayout = this._device.createBindGroupLayout({
|
|
392
532
|
entries: [
|
|
393
533
|
{
|
|
@@ -414,27 +554,59 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
414
554
|
this._pipelineLayout = this._device.createPipelineLayout({
|
|
415
555
|
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
416
556
|
});
|
|
557
|
+
this._instancedTransformBindGroupLayout = this._device.createBindGroupLayout({
|
|
558
|
+
entries: [
|
|
559
|
+
{
|
|
560
|
+
binding: 0,
|
|
561
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
562
|
+
buffer: { type: 'uniform', hasDynamicOffset: true },
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
binding: 1,
|
|
566
|
+
visibility: GPUShaderStage.VERTEX,
|
|
567
|
+
buffer: { type: 'read-only-storage' },
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
});
|
|
571
|
+
this._instancedPipelineLayout = this._device.createPipelineLayout({
|
|
572
|
+
bindGroupLayouts: [this._instancedTransformBindGroupLayout, this._textureBindGroupLayout],
|
|
573
|
+
});
|
|
417
574
|
}
|
|
418
575
|
onDisconnect() {
|
|
419
576
|
this.flush();
|
|
420
577
|
this._vertexBuffer?.destroy();
|
|
421
578
|
this._indexBuffer?.destroy();
|
|
422
579
|
this._uniformBuffer?.destroy();
|
|
580
|
+
this._instancedUniformBuffer?.destroy();
|
|
581
|
+
this._instancedNodeIndexBuffer?.destroy();
|
|
423
582
|
this._pipelines.clear();
|
|
583
|
+
this._instancedPipelines.clear();
|
|
424
584
|
this._textureBindGroups = new WeakMap();
|
|
585
|
+
for (const entry of this._staticGeometryCache.values()) {
|
|
586
|
+
entry.vertexBuffer.destroy();
|
|
587
|
+
entry.indexBuffer.destroy();
|
|
588
|
+
}
|
|
589
|
+
this._staticGeometryCache.clear();
|
|
425
590
|
this._vertexBuffer = null;
|
|
426
591
|
this._indexBuffer = null;
|
|
427
592
|
this._uniformBuffer = null;
|
|
428
593
|
this._uniformBindGroup = null;
|
|
594
|
+
this._instancedUniformBuffer = null;
|
|
595
|
+
this._instancedNodeIndexBuffer = null;
|
|
596
|
+
this._instancedTransformBindGroup = null;
|
|
597
|
+
this._instancedTransformStorageBuffer = null;
|
|
429
598
|
this._pipelineLayout = null;
|
|
599
|
+
this._instancedPipelineLayout = null;
|
|
430
600
|
this._textureBindGroupLayout = null;
|
|
431
601
|
this._uniformBindGroupLayout = null;
|
|
602
|
+
this._instancedTransformBindGroupLayout = null;
|
|
432
603
|
this._shaderModule = null;
|
|
433
|
-
|
|
604
|
+
this._instancedShaderModule = null;
|
|
605
|
+
// Custom materials are owned by user code (one MeshMaterial can be shared
|
|
434
606
|
// across multiple Mesh instances). Their resources are released when the
|
|
435
|
-
// user calls
|
|
607
|
+
// user calls material.destroy(), which fires our _onDispose callback. On
|
|
436
608
|
// backend disconnect we eagerly release everything to avoid GPU leaks
|
|
437
|
-
// even if the user keeps the
|
|
609
|
+
// even if the user keeps the material reference around.
|
|
438
610
|
for (const resources of this._customShaders.values()) {
|
|
439
611
|
this._releaseCustomShaderResources(resources);
|
|
440
612
|
}
|
|
@@ -445,6 +617,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
445
617
|
this._vertexBufferCapacity = 0;
|
|
446
618
|
this._indexBufferCapacity = 0;
|
|
447
619
|
this._uniformBufferCapacity = 0;
|
|
620
|
+
this._instancedUniformBufferCapacity = 0;
|
|
621
|
+
this._instancedNodeIndexBufferCapacity = 0;
|
|
622
|
+
this._instancedNodeIndexData = new Uint32Array(0);
|
|
448
623
|
}
|
|
449
624
|
// ---------------------------------------------------------------------------
|
|
450
625
|
// Default-path helpers
|
|
@@ -490,16 +665,16 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
490
665
|
}
|
|
491
666
|
}
|
|
492
667
|
_getPipeline(key) {
|
|
493
|
-
const cacheKey =
|
|
668
|
+
const cacheKey = meshPipelineCacheKey(key.blendMode, key.format, key.stencil);
|
|
494
669
|
let pipeline = this._pipelines.get(cacheKey);
|
|
495
670
|
if (!pipeline) {
|
|
496
|
-
pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(key.blendMode, key.format));
|
|
671
|
+
pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(key.blendMode, key.format, key.stencil));
|
|
497
672
|
this._pipelines.set(cacheKey, pipeline);
|
|
498
673
|
}
|
|
499
674
|
return pipeline;
|
|
500
675
|
}
|
|
501
|
-
_buildPipelineDescriptor(blendMode, format) {
|
|
502
|
-
|
|
676
|
+
_buildPipelineDescriptor(blendMode, format, stencil = false) {
|
|
677
|
+
const descriptor = {
|
|
503
678
|
layout: this._pipelineLayout,
|
|
504
679
|
vertex: {
|
|
505
680
|
module: this._shaderModule,
|
|
@@ -532,6 +707,10 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
532
707
|
cullMode: 'none',
|
|
533
708
|
},
|
|
534
709
|
};
|
|
710
|
+
if (stencil) {
|
|
711
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
712
|
+
}
|
|
713
|
+
return descriptor;
|
|
535
714
|
}
|
|
536
715
|
_getTextureBindGroup(backend, texture) {
|
|
537
716
|
let group = this._textureBindGroups.get(texture);
|
|
@@ -548,6 +727,223 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
548
727
|
}
|
|
549
728
|
return group;
|
|
550
729
|
}
|
|
730
|
+
_getStaticBatchLength(startIndex) {
|
|
731
|
+
const first = this._drawCalls[startIndex];
|
|
732
|
+
if (!this._isStaticBatchCandidate(first)) {
|
|
733
|
+
return 1;
|
|
734
|
+
}
|
|
735
|
+
let length = 1;
|
|
736
|
+
for (let i = startIndex + 1; i < this._drawCallCount; i++) {
|
|
737
|
+
const next = this._drawCalls[i];
|
|
738
|
+
if (!this._isSameStaticBatch(first, next)) {
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
length++;
|
|
742
|
+
}
|
|
743
|
+
return length;
|
|
744
|
+
}
|
|
745
|
+
_isStaticBatchCandidate(drawCall) {
|
|
746
|
+
const command = drawCall.command;
|
|
747
|
+
return drawCall.customShader === null && command?.groupIndex !== undefined && drawCall.mesh.geometry?.usage === 'static';
|
|
748
|
+
}
|
|
749
|
+
_isSameStaticBatch(left, right) {
|
|
750
|
+
if (!this._isStaticBatchCandidate(left) || !this._isStaticBatchCandidate(right)) {
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
return (left.command.groupIndex === right.command.groupIndex &&
|
|
754
|
+
left.mesh.geometry === right.mesh.geometry &&
|
|
755
|
+
left.texture === right.texture &&
|
|
756
|
+
left.blendMode === right.blendMode &&
|
|
757
|
+
left.command.material.pipelineKey === right.command.material.pipelineKey &&
|
|
758
|
+
left.command.material.bindKey === right.command.material.bindKey);
|
|
759
|
+
}
|
|
760
|
+
_uploadInstancedNodeIndices(startIndex, batchLength) {
|
|
761
|
+
this._ensureInstancedNodeIndexCapacity(batchLength);
|
|
762
|
+
let maxNodeIndex = 0;
|
|
763
|
+
for (let i = 0; i < batchLength; i++) {
|
|
764
|
+
const nodeIndex = this._drawCalls[startIndex + i].command.nodeIndex >>> 0;
|
|
765
|
+
this._instancedNodeIndexData[i] = nodeIndex;
|
|
766
|
+
if (nodeIndex > maxNodeIndex) {
|
|
767
|
+
maxNodeIndex = nodeIndex;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
this._device.queue.writeBuffer(this._instancedNodeIndexBuffer, 0, this._instancedNodeIndexData.buffer, this._instancedNodeIndexData.byteOffset, batchLength * Uint32Array.BYTES_PER_ELEMENT);
|
|
771
|
+
return maxNodeIndex;
|
|
772
|
+
}
|
|
773
|
+
_ensureInstancedNodeIndexCapacity(instanceCount) {
|
|
774
|
+
const requiredBytes = instanceCount * Uint32Array.BYTES_PER_ELEMENT;
|
|
775
|
+
if (this._instancedNodeIndexData.length < instanceCount) {
|
|
776
|
+
this._instancedNodeIndexData = new Uint32Array(Math.max(instanceCount, this._instancedNodeIndexData.length * 2 || 1));
|
|
777
|
+
}
|
|
778
|
+
if (requiredBytes > this._instancedNodeIndexBufferCapacity) {
|
|
779
|
+
this._instancedNodeIndexBuffer?.destroy();
|
|
780
|
+
this._instancedNodeIndexBufferCapacity = Math.max(requiredBytes, this._instancedNodeIndexBufferCapacity * 2 || Uint32Array.BYTES_PER_ELEMENT);
|
|
781
|
+
this._instancedNodeIndexBuffer = this._device.createBuffer({
|
|
782
|
+
size: this._instancedNodeIndexBufferCapacity,
|
|
783
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
_ensureInstancedUniformCapacity(drawCallCount) {
|
|
788
|
+
if (drawCallCount === 0) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const requiredBytes = drawCallCount * this._uniformAlignment;
|
|
792
|
+
if (requiredBytes > this._instancedUniformBufferCapacity) {
|
|
793
|
+
this._instancedUniformBuffer?.destroy();
|
|
794
|
+
this._instancedUniformBufferCapacity = Math.max(requiredBytes, this._instancedUniformBufferCapacity * 2 || this._uniformAlignment);
|
|
795
|
+
this._instancedUniformBuffer = this._device.createBuffer({
|
|
796
|
+
size: this._instancedUniformBufferCapacity,
|
|
797
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
798
|
+
});
|
|
799
|
+
this._instancedTransformBindGroup = null;
|
|
800
|
+
this._instancedTransformStorageBuffer = null;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
_writeInstancedUniformSlot(slot, backend, premultiplySample) {
|
|
804
|
+
const data = this._instancedUniformScratch;
|
|
805
|
+
const projection = backend.view.getTransform();
|
|
806
|
+
data.fill(0);
|
|
807
|
+
data[0] = projection.a;
|
|
808
|
+
data[1] = projection.b;
|
|
809
|
+
data[4] = projection.c;
|
|
810
|
+
data[5] = projection.d;
|
|
811
|
+
data[8] = projection.x;
|
|
812
|
+
data[9] = projection.y;
|
|
813
|
+
data[10] = 1;
|
|
814
|
+
data[12] = premultiplySample ? 1 : 0;
|
|
815
|
+
this._device.queue.writeBuffer(this._instancedUniformBuffer, slot * this._uniformAlignment, data.buffer, data.byteOffset, transformUniformByteLength);
|
|
816
|
+
}
|
|
817
|
+
_getOrCreateInstancedTransformBindGroup(storageBuffer) {
|
|
818
|
+
if (this._instancedTransformBindGroup !== null && this._instancedTransformStorageBuffer === storageBuffer) {
|
|
819
|
+
return this._instancedTransformBindGroup;
|
|
820
|
+
}
|
|
821
|
+
this._instancedTransformStorageBuffer = storageBuffer;
|
|
822
|
+
this._instancedTransformBindGroup = this._device.createBindGroup({
|
|
823
|
+
layout: this._instancedTransformBindGroupLayout,
|
|
824
|
+
entries: [
|
|
825
|
+
{
|
|
826
|
+
binding: 0,
|
|
827
|
+
resource: {
|
|
828
|
+
buffer: this._instancedUniformBuffer,
|
|
829
|
+
size: transformUniformByteLength,
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
binding: 1,
|
|
834
|
+
resource: {
|
|
835
|
+
buffer: storageBuffer,
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
});
|
|
840
|
+
return this._instancedTransformBindGroup;
|
|
841
|
+
}
|
|
842
|
+
_getInstancedPipeline(key) {
|
|
843
|
+
const cacheKey = meshPipelineCacheKey(key.blendMode, key.format, key.stencil);
|
|
844
|
+
let pipeline = this._instancedPipelines.get(cacheKey);
|
|
845
|
+
if (!pipeline) {
|
|
846
|
+
pipeline = this._device.createRenderPipeline(this._buildInstancedPipelineDescriptor(key.blendMode, key.format, key.stencil));
|
|
847
|
+
this._instancedPipelines.set(cacheKey, pipeline);
|
|
848
|
+
}
|
|
849
|
+
return pipeline;
|
|
850
|
+
}
|
|
851
|
+
_buildInstancedPipelineDescriptor(blendMode, format, stencil = false) {
|
|
852
|
+
const descriptor = {
|
|
853
|
+
layout: this._instancedPipelineLayout,
|
|
854
|
+
vertex: {
|
|
855
|
+
module: this._instancedShaderModule,
|
|
856
|
+
entryPoint: 'vertexMain',
|
|
857
|
+
buffers: [
|
|
858
|
+
{
|
|
859
|
+
arrayStride: vertexStrideBytes,
|
|
860
|
+
stepMode: 'vertex',
|
|
861
|
+
attributes: [
|
|
862
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x2' },
|
|
863
|
+
{ shaderLocation: 1, offset: 8, format: 'float32x2' },
|
|
864
|
+
{ shaderLocation: 2, offset: 16, format: 'unorm8x4' },
|
|
865
|
+
],
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
arrayStride: Uint32Array.BYTES_PER_ELEMENT,
|
|
869
|
+
stepMode: 'instance',
|
|
870
|
+
attributes: [{ shaderLocation: 6, offset: 0, format: 'uint32' }],
|
|
871
|
+
},
|
|
872
|
+
],
|
|
873
|
+
},
|
|
874
|
+
fragment: {
|
|
875
|
+
module: this._instancedShaderModule,
|
|
876
|
+
entryPoint: 'fragmentMain',
|
|
877
|
+
targets: [
|
|
878
|
+
{
|
|
879
|
+
format,
|
|
880
|
+
blend: getWebGpuBlendState(blendMode),
|
|
881
|
+
writeMask: GPUColorWrite.ALL,
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
},
|
|
885
|
+
primitive: {
|
|
886
|
+
topology: 'triangle-list',
|
|
887
|
+
cullMode: 'none',
|
|
888
|
+
},
|
|
889
|
+
};
|
|
890
|
+
if (stencil) {
|
|
891
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
892
|
+
}
|
|
893
|
+
return descriptor;
|
|
894
|
+
}
|
|
895
|
+
_getOrCreateStaticGeometryEntry(mesh) {
|
|
896
|
+
const geometry = mesh.geometry;
|
|
897
|
+
if (geometry?.usage !== 'static') {
|
|
898
|
+
throw new Error('Static mesh batching requires Geometry with usage="static".');
|
|
899
|
+
}
|
|
900
|
+
const existing = this._staticGeometryCache.get(geometry);
|
|
901
|
+
if (existing !== undefined) {
|
|
902
|
+
return existing;
|
|
903
|
+
}
|
|
904
|
+
const vertexData = new ArrayBuffer(mesh.vertexCount * vertexStrideBytes);
|
|
905
|
+
const vertexFloatView = new Float32Array(vertexData);
|
|
906
|
+
const vertexUintView = new Uint32Array(vertexData);
|
|
907
|
+
this._writeMeshVerticesIntoBuffer(mesh, 0, vertexFloatView, vertexUintView);
|
|
908
|
+
const indexData = new Uint16Array(mesh.indexCount);
|
|
909
|
+
if (mesh.indices !== null) {
|
|
910
|
+
indexData.set(mesh.indices, 0);
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
for (let i = 0; i < mesh.indexCount; i++) {
|
|
914
|
+
indexData[i] = i;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const vertexBuffer = this._device.createBuffer({
|
|
918
|
+
size: vertexData.byteLength,
|
|
919
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
920
|
+
});
|
|
921
|
+
const indexBuffer = this._device.createBuffer({
|
|
922
|
+
size: indexData.byteLength,
|
|
923
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
924
|
+
});
|
|
925
|
+
this._device.queue.writeBuffer(vertexBuffer, 0, vertexData, 0, vertexData.byteLength);
|
|
926
|
+
this._device.queue.writeBuffer(indexBuffer, 0, indexData.buffer, indexData.byteOffset, indexData.byteLength);
|
|
927
|
+
const disposeListener = () => {
|
|
928
|
+
const entry = this._staticGeometryCache.get(geometry);
|
|
929
|
+
if (entry === undefined) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
entry.vertexBuffer.destroy();
|
|
933
|
+
entry.indexBuffer.destroy();
|
|
934
|
+
this._staticGeometryCache.delete(geometry);
|
|
935
|
+
};
|
|
936
|
+
geometry._onDispose(disposeListener);
|
|
937
|
+
const created = {
|
|
938
|
+
geometry,
|
|
939
|
+
vertexBuffer,
|
|
940
|
+
indexBuffer,
|
|
941
|
+
indexCount: mesh.indexCount,
|
|
942
|
+
disposeListener,
|
|
943
|
+
};
|
|
944
|
+
this._staticGeometryCache.set(geometry, created);
|
|
945
|
+
return created;
|
|
946
|
+
}
|
|
551
947
|
_ensureVertexCapacity(vertexCount) {
|
|
552
948
|
const requiredBytes = vertexCount * vertexStrideBytes;
|
|
553
949
|
if (requiredBytes > this._vertexData.byteLength) {
|
|
@@ -620,19 +1016,19 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
620
1016
|
resources.totalIndices = 0;
|
|
621
1017
|
}
|
|
622
1018
|
}
|
|
623
|
-
_getOrCreateCustomShaderResources(
|
|
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;
|
|
@@ -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;
|
|
@@ -873,10 +1269,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
873
1269
|
}
|
|
874
1270
|
return group;
|
|
875
1271
|
}
|
|
876
|
-
_buildUserBindGroupLayout(device,
|
|
1272
|
+
_buildUserBindGroupLayout(device, material) {
|
|
877
1273
|
const entries = [];
|
|
878
|
-
const userUniforms = shader.uniforms;
|
|
879
|
-
Object.values(userUniforms).some(v => !isTextureUniform(v));
|
|
880
1274
|
// Binding 0 always reserved for the user UBO (even if empty), so the
|
|
881
1275
|
// bind-group layout is stable across user-uniform mutations.
|
|
882
1276
|
entries.push({
|
|
@@ -884,15 +1278,12 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
884
1278
|
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
885
1279
|
buffer: { type: 'uniform' },
|
|
886
1280
|
});
|
|
1281
|
+
const textureBindings = collectTextureBindings(material);
|
|
1282
|
+
if (textureBindings.length > maxCustomTextureSlots) {
|
|
1283
|
+
throw new Error(`Mesh material requested more than ${maxCustomTextureSlots} user texture bindings.`);
|
|
1284
|
+
}
|
|
887
1285
|
let bindingIndex = 1;
|
|
888
|
-
let
|
|
889
|
-
for (const value of Object.values(userUniforms)) {
|
|
890
|
-
if (!isTextureUniform(value)) {
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
if (textureCount >= maxCustomTextureSlots) {
|
|
894
|
-
throw new Error(`MeshShader requested more than ${maxCustomTextureSlots} user texture uniforms.`);
|
|
895
|
-
}
|
|
1286
|
+
for (let t = 0; t < textureBindings.length; t++) {
|
|
896
1287
|
entries.push({
|
|
897
1288
|
binding: bindingIndex,
|
|
898
1289
|
visibility: GPUShaderStage.FRAGMENT,
|
|
@@ -905,14 +1296,12 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
905
1296
|
sampler: { type: 'filtering' },
|
|
906
1297
|
});
|
|
907
1298
|
bindingIndex++;
|
|
908
|
-
textureCount++;
|
|
909
1299
|
}
|
|
910
1300
|
return device.createBindGroupLayout({ entries });
|
|
911
1301
|
}
|
|
912
|
-
_uploadUserUniforms(
|
|
1302
|
+
_uploadUserUniforms(material, resources) {
|
|
913
1303
|
const device = this._device;
|
|
914
|
-
const
|
|
915
|
-
const scalarValues = Object.values(uniforms).filter(v => !isTextureUniform(v));
|
|
1304
|
+
const scalarValues = collectScalarUniforms(material);
|
|
916
1305
|
// Always create a UBO (even if empty) since binding 0 of the user layout
|
|
917
1306
|
// is fixed. Min size 16 bytes to satisfy WebGPU's minimum buffer size.
|
|
918
1307
|
const slotCount = Math.max(scalarValues.length, 1);
|
|
@@ -950,16 +1339,13 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
950
1339
|
}
|
|
951
1340
|
device.queue.writeBuffer(resources.userUniformBuffer, 0, data);
|
|
952
1341
|
}
|
|
953
|
-
_buildUserBindGroup(backend,
|
|
1342
|
+
_buildUserBindGroup(backend, material, resources) {
|
|
954
1343
|
const device = this._device;
|
|
955
1344
|
const entries = [];
|
|
956
1345
|
entries.push({ binding: 0, resource: { buffer: resources.userUniformBuffer } });
|
|
957
1346
|
let bindingIndex = 1;
|
|
958
|
-
for (const
|
|
959
|
-
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
const binding = backend.getTextureBinding(value);
|
|
1347
|
+
for (const texture of collectTextureBindings(material)) {
|
|
1348
|
+
const binding = backend.getTextureBinding(texture);
|
|
963
1349
|
entries.push({ binding: bindingIndex, resource: binding.view });
|
|
964
1350
|
bindingIndex++;
|
|
965
1351
|
entries.push({ binding: bindingIndex, resource: binding.sampler });
|
|
@@ -997,6 +1383,34 @@ function isTextureUniform(value) {
|
|
|
997
1383
|
!(value instanceof Int32Array) &&
|
|
998
1384
|
!Array.isArray(value));
|
|
999
1385
|
}
|
|
1386
|
+
/** Scalar/vector/matrix uniforms (texture values excluded) in declaration order. */
|
|
1387
|
+
function collectScalarUniforms(material) {
|
|
1388
|
+
const result = [];
|
|
1389
|
+
for (const value of Object.values(material.uniforms)) {
|
|
1390
|
+
if (!isTextureUniform(value)) {
|
|
1391
|
+
result.push(value);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return result;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Texture bindings claimed by the material, in a stable order: texture-valued
|
|
1398
|
+
* entries of `uniforms` first (declaration order), then the dedicated
|
|
1399
|
+
* `textures` map (declaration order). The WGSL source must declare its
|
|
1400
|
+
* `@group(2)` texture/sampler pairs in this same order.
|
|
1401
|
+
*/
|
|
1402
|
+
function collectTextureBindings(material) {
|
|
1403
|
+
const result = [];
|
|
1404
|
+
for (const value of Object.values(material.uniforms)) {
|
|
1405
|
+
if (isTextureUniform(value)) {
|
|
1406
|
+
result.push(value);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
for (const texture of Object.values(material.textures)) {
|
|
1410
|
+
result.push(texture);
|
|
1411
|
+
}
|
|
1412
|
+
return result;
|
|
1413
|
+
}
|
|
1000
1414
|
|
|
1001
1415
|
export { WebGpuMeshRenderer };
|
|
1002
1416
|
//# sourceMappingURL=WebGpuMeshRenderer.js.map
|