@flighthq/scene3d-wgpu 0.2.1-next.840.857425c
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/dist/anisotropyPbrWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/anisotropyPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/anisotropyPbrWgpuMeshMaterialRenderer.js +52 -0
- package/dist/anisotropyPbrWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/blinnPhongWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/blinnPhongWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/blinnPhongWgpuMeshMaterialRenderer.js +66 -0
- package/dist/blinnPhongWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/clearcoatPbrWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/clearcoatPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/clearcoatPbrWgpuMeshMaterialRenderer.js +52 -0
- package/dist/clearcoatPbrWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/customShaderWgpuMeshMaterialRenderer.d.ts +7 -0
- package/dist/customShaderWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/customShaderWgpuMeshMaterialRenderer.js +225 -0
- package/dist/customShaderWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/depthWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/depthWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/depthWgpuMeshMaterialRenderer.js +42 -0
- package/dist/depthWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/drawWgpuScene3D.d.ts +4 -0
- package/dist/drawWgpuScene3D.d.ts.map +1 -0
- package/dist/drawWgpuScene3D.js +171 -0
- package/dist/drawWgpuScene3D.js.map +1 -0
- package/dist/emissiveWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/emissiveWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/emissiveWgpuMeshMaterialRenderer.js +51 -0
- package/dist/emissiveWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/enableWgpuScene3DCustomShaderGuards.d.ts +5 -0
- package/dist/enableWgpuScene3DCustomShaderGuards.d.ts.map +1 -0
- package/dist/enableWgpuScene3DCustomShaderGuards.js +56 -0
- package/dist/enableWgpuScene3DCustomShaderGuards.js.map +1 -0
- package/dist/enableWgpuScene3DForwardLightSelectionGuards.d.ts +4 -0
- package/dist/enableWgpuScene3DForwardLightSelectionGuards.d.ts.map +1 -0
- package/dist/enableWgpuScene3DForwardLightSelectionGuards.js +17 -0
- package/dist/enableWgpuScene3DForwardLightSelectionGuards.js.map +1 -0
- package/dist/explainWgpuScene3DForwardLightSelection.d.ts +3 -0
- package/dist/explainWgpuScene3DForwardLightSelection.d.ts.map +1 -0
- package/dist/explainWgpuScene3DForwardLightSelection.js +18 -0
- package/dist/explainWgpuScene3DForwardLightSelection.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/iridescencePbrWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/iridescencePbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/iridescencePbrWgpuMeshMaterialRenderer.js +60 -0
- package/dist/iridescencePbrWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/lambertWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/lambertWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/lambertWgpuMeshMaterialRenderer.js +61 -0
- package/dist/lambertWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/matcapWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/matcapWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/matcapWgpuMeshMaterialRenderer.js +54 -0
- package/dist/matcapWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/normalWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/normalWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/normalWgpuMeshMaterialRenderer.js +47 -0
- package/dist/normalWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/phongWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/phongWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/phongWgpuMeshMaterialRenderer.js +63 -0
- package/dist/phongWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/prepareWgpuScene3DForwardLights.d.ts +3 -0
- package/dist/prepareWgpuScene3DForwardLights.d.ts.map +1 -0
- package/dist/prepareWgpuScene3DForwardLights.js +110 -0
- package/dist/prepareWgpuScene3DForwardLights.js.map +1 -0
- package/dist/registerStandardPbrWgpuMaterial.d.ts +3 -0
- package/dist/registerStandardPbrWgpuMaterial.d.ts.map +1 -0
- package/dist/registerStandardPbrWgpuMaterial.js +11 -0
- package/dist/registerStandardPbrWgpuMaterial.js.map +1 -0
- package/dist/shadedWgpuMeshMaterialRenderer.d.ts +11 -0
- package/dist/shadedWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/shadedWgpuMeshMaterialRenderer.js +47 -0
- package/dist/shadedWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/sheenPbrWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/sheenPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/sheenPbrWgpuMeshMaterialRenderer.js +58 -0
- package/dist/sheenPbrWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/specularGlossinessPbrWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/specularGlossinessPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/specularGlossinessPbrWgpuMeshMaterialRenderer.js +126 -0
- package/dist/specularGlossinessPbrWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/specularPbrWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/specularPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/specularPbrWgpuMeshMaterialRenderer.js +65 -0
- package/dist/specularPbrWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/standardPbrWgpuMeshMaterialRenderer.d.ts +9 -0
- package/dist/standardPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/standardPbrWgpuMeshMaterialRenderer.js +174 -0
- package/dist/standardPbrWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/subsurfacePbrWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/subsurfacePbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/subsurfacePbrWgpuMeshMaterialRenderer.js +68 -0
- package/dist/subsurfacePbrWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/toonWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/toonWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/toonWgpuMeshMaterialRenderer.js +71 -0
- package/dist/toonWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/transmissionVolumePbrWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/transmissionVolumePbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/transmissionVolumePbrWgpuMeshMaterialRenderer.js +74 -0
- package/dist/transmissionVolumePbrWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/unlitWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/unlitWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/unlitWgpuMeshMaterialRenderer.js +57 -0
- package/dist/unlitWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/vertexColorWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/vertexColorWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/vertexColorWgpuMeshMaterialRenderer.js +55 -0
- package/dist/vertexColorWgpuMeshMaterialRenderer.js.map +1 -0
- package/dist/wgpuClassicPrelude.d.ts +8 -0
- package/dist/wgpuClassicPrelude.d.ts.map +1 -0
- package/dist/wgpuClassicPrelude.js +319 -0
- package/dist/wgpuClassicPrelude.js.map +1 -0
- package/dist/wgpuCustomMaterialAbi.d.ts +3 -0
- package/dist/wgpuCustomMaterialAbi.d.ts.map +1 -0
- package/dist/wgpuCustomMaterialAbi.js +5 -0
- package/dist/wgpuCustomMaterialAbi.js.map +1 -0
- package/dist/wgpuDebugPrelude.d.ts +7 -0
- package/dist/wgpuDebugPrelude.d.ts.map +1 -0
- package/dist/wgpuDebugPrelude.js +133 -0
- package/dist/wgpuDebugPrelude.js.map +1 -0
- package/dist/wgpuEnvironmentCube.d.ts +4 -0
- package/dist/wgpuEnvironmentCube.d.ts.map +1 -0
- package/dist/wgpuEnvironmentCube.js +66 -0
- package/dist/wgpuEnvironmentCube.js.map +1 -0
- package/dist/wgpuEnvironmentIblBake.d.ts +4 -0
- package/dist/wgpuEnvironmentIblBake.d.ts.map +1 -0
- package/dist/wgpuEnvironmentIblBake.js +458 -0
- package/dist/wgpuEnvironmentIblBake.js.map +1 -0
- package/dist/wgpuEnvironmentSkybox.d.ts +3 -0
- package/dist/wgpuEnvironmentSkybox.d.ts.map +1 -0
- package/dist/wgpuEnvironmentSkybox.js +159 -0
- package/dist/wgpuEnvironmentSkybox.js.map +1 -0
- package/dist/wgpuMatcapPrelude.d.ts +7 -0
- package/dist/wgpuMatcapPrelude.d.ts.map +1 -0
- package/dist/wgpuMatcapPrelude.js +134 -0
- package/dist/wgpuMatcapPrelude.js.map +1 -0
- package/dist/wgpuMeshMaterialRegistry.d.ts +5 -0
- package/dist/wgpuMeshMaterialRegistry.d.ts.map +1 -0
- package/dist/wgpuMeshMaterialRegistry.js +29 -0
- package/dist/wgpuMeshMaterialRegistry.js.map +1 -0
- package/dist/wgpuMeshPipeline.d.ts +43 -0
- package/dist/wgpuMeshPipeline.d.ts.map +1 -0
- package/dist/wgpuMeshPipeline.js +1007 -0
- package/dist/wgpuMeshPipeline.js.map +1 -0
- package/dist/wgpuMeshUpload.d.ts +3 -0
- package/dist/wgpuMeshUpload.d.ts.map +1 -0
- package/dist/wgpuMeshUpload.js +58 -0
- package/dist/wgpuMeshUpload.js.map +1 -0
- package/dist/wgpuParticleEmitter3D.d.ts +4 -0
- package/dist/wgpuParticleEmitter3D.d.ts.map +1 -0
- package/dist/wgpuParticleEmitter3D.js +403 -0
- package/dist/wgpuParticleEmitter3D.js.map +1 -0
- package/dist/wgpuPbrPipelineCache.d.ts +4 -0
- package/dist/wgpuPbrPipelineCache.d.ts.map +1 -0
- package/dist/wgpuPbrPipelineCache.js +52 -0
- package/dist/wgpuPbrPipelineCache.js.map +1 -0
- package/dist/wgpuPbrPrelude.d.ts +7 -0
- package/dist/wgpuPbrPrelude.d.ts.map +1 -0
- package/dist/wgpuPbrPrelude.js +551 -0
- package/dist/wgpuPbrPrelude.js.map +1 -0
- package/dist/wgpuScene3DRuntime.d.ts +5 -0
- package/dist/wgpuScene3DRuntime.d.ts.map +1 -0
- package/dist/wgpuScene3DRuntime.js +81 -0
- package/dist/wgpuScene3DRuntime.js.map +1 -0
- package/dist/wgpuScene3DTestHelper.d.ts +14 -0
- package/dist/wgpuScene3DTestHelper.d.ts.map +1 -0
- package/dist/wgpuScene3DTestHelper.js +121 -0
- package/dist/wgpuScene3DTestHelper.js.map +1 -0
- package/dist/wgpuScene3DTime.d.ts +4 -0
- package/dist/wgpuScene3DTime.d.ts.map +1 -0
- package/dist/wgpuScene3DTime.js +11 -0
- package/dist/wgpuScene3DTime.js.map +1 -0
- package/dist/wgpuShadedModifierSnippet.d.ts +4 -0
- package/dist/wgpuShadedModifierSnippet.d.ts.map +1 -0
- package/dist/wgpuShadedModifierSnippet.js +18 -0
- package/dist/wgpuShadedModifierSnippet.js.map +1 -0
- package/dist/wgpuShadedPrelude.d.ts +16 -0
- package/dist/wgpuShadedPrelude.d.ts.map +1 -0
- package/dist/wgpuShadedPrelude.js +625 -0
- package/dist/wgpuShadedPrelude.js.map +1 -0
- package/dist/wgpuShadowMap.d.ts +4 -0
- package/dist/wgpuShadowMap.d.ts.map +1 -0
- package/dist/wgpuShadowMap.js +155 -0
- package/dist/wgpuShadowMap.js.map +1 -0
- package/dist/wgpuSkinPalette.d.ts +7 -0
- package/dist/wgpuSkinPalette.d.ts.map +1 -0
- package/dist/wgpuSkinPalette.js +173 -0
- package/dist/wgpuSkinPalette.js.map +1 -0
- package/dist/wgpuToonPrelude.d.ts +8 -0
- package/dist/wgpuToonPrelude.d.ts.map +1 -0
- package/dist/wgpuToonPrelude.js +192 -0
- package/dist/wgpuToonPrelude.js.map +1 -0
- package/dist/wgpuUnlitPrelude.d.ts +9 -0
- package/dist/wgpuUnlitPrelude.d.ts.map +1 -0
- package/dist/wgpuUnlitPrelude.js +142 -0
- package/dist/wgpuUnlitPrelude.js.map +1 -0
- package/dist/wgpuWireframePrelude.d.ts +6 -0
- package/dist/wgpuWireframePrelude.d.ts.map +1 -0
- package/dist/wgpuWireframePrelude.js +77 -0
- package/dist/wgpuWireframePrelude.js.map +1 -0
- package/dist/wgpuWireframeUpload.d.ts +3 -0
- package/dist/wgpuWireframeUpload.d.ts.map +1 -0
- package/dist/wgpuWireframeUpload.js +68 -0
- package/dist/wgpuWireframeUpload.js.map +1 -0
- package/dist/wireframeWgpuMeshMaterialRenderer.d.ts +4 -0
- package/dist/wireframeWgpuMeshMaterialRenderer.d.ts.map +1 -0
- package/dist/wireframeWgpuMeshMaterialRenderer.js +69 -0
- package/dist/wireframeWgpuMeshMaterialRenderer.js.map +1 -0
- package/package.json +57 -0
- package/src/anisotropyPbrWgpuMeshMaterialRenderer.test.ts +90 -0
- package/src/blinnPhongWgpuMeshMaterialRenderer.test.ts +82 -0
- package/src/clearcoatPbrWgpuMeshMaterialRenderer.test.ts +90 -0
- package/src/customShaderWgpuMeshMaterialRenderer.test.ts +207 -0
- package/src/depthWgpuMeshMaterialRenderer.test.ts +69 -0
- package/src/drawWgpuScene3D.test.ts +247 -0
- package/src/emissiveWgpuMeshMaterialRenderer.test.ts +64 -0
- package/src/enableWgpuScene3DCustomShaderGuards.test.ts +73 -0
- package/src/enableWgpuScene3DForwardLightSelectionGuards.test.ts +39 -0
- package/src/explainWgpuScene3DForwardLightSelection.test.ts +15 -0
- package/src/iridescencePbrWgpuMeshMaterialRenderer.test.ts +90 -0
- package/src/lambertWgpuMeshMaterialRenderer.test.ts +77 -0
- package/src/matcapWgpuMeshMaterialRenderer.test.ts +69 -0
- package/src/normalWgpuMeshMaterialRenderer.test.ts +69 -0
- package/src/phongWgpuMeshMaterialRenderer.test.ts +79 -0
- package/src/prepareWgpuScene3DForwardLights.test.ts +84 -0
- package/src/registerStandardPbrWgpuMaterial.test.ts +15 -0
- package/src/shadedWgpuMeshMaterialRenderer.test.ts +63 -0
- package/src/sheenPbrWgpuMeshMaterialRenderer.test.ts +87 -0
- package/src/specularGlossinessPbrWgpuMeshMaterialRenderer.test.ts +100 -0
- package/src/specularPbrWgpuMeshMaterialRenderer.test.ts +90 -0
- package/src/standardPbrWgpuMeshMaterialRenderer.test.ts +172 -0
- package/src/subsurfacePbrWgpuMeshMaterialRenderer.test.ts +90 -0
- package/src/toonWgpuMeshMaterialRenderer.test.ts +89 -0
- package/src/transmissionVolumePbrWgpuMeshMaterialRenderer.test.ts +97 -0
- package/src/unlitWgpuMeshMaterialRenderer.test.ts +94 -0
- package/src/vertexColorWgpuMeshMaterialRenderer.test.ts +62 -0
- package/src/wgpuClassicPrelude.test.ts +176 -0
- package/src/wgpuDebugPrelude.test.ts +79 -0
- package/src/wgpuEnvironmentCube.test.ts +86 -0
- package/src/wgpuEnvironmentIblBake.test.ts +79 -0
- package/src/wgpuEnvironmentSkybox.test.ts +56 -0
- package/src/wgpuMatcapPrelude.test.ts +70 -0
- package/src/wgpuMeshMaterialRegistry.test.ts +55 -0
- package/src/wgpuMeshPipeline.test.ts +844 -0
- package/src/wgpuMeshUpload.test.ts +177 -0
- package/src/wgpuParticleEmitter3D.test.ts +200 -0
- package/src/wgpuPbrPipelineCache.test.ts +67 -0
- package/src/wgpuPbrPrelude.test.ts +162 -0
- package/src/wgpuScene3DRuntime.test.ts +35 -0
- package/src/wgpuScene3DTime.test.ts +20 -0
- package/src/wgpuShadedModifierSnippet.test.ts +33 -0
- package/src/wgpuShadedPrelude.test.ts +394 -0
- package/src/wgpuShadowMap.test.ts +169 -0
- package/src/wgpuSkinPalette.test.ts +88 -0
- package/src/wgpuToonPrelude.test.ts +93 -0
- package/src/wgpuUnlitPrelude.test.ts +102 -0
- package/src/wgpuWireframePrelude.test.ts +54 -0
- package/src/wgpuWireframeUpload.test.ts +36 -0
- package/src/wireframeWgpuMeshMaterialRenderer.test.ts +69 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
import { getCamera3DViewProjectionMatrix4 } from '@flighthq/camera';
|
|
2
|
+
import { createMatrix3, createMatrix4, getMatrix4Position, inverseMatrix4 } from '@flighthq/geometry';
|
|
3
|
+
import { hasImageResourcePixels } from '@flighthq/image';
|
|
4
|
+
import { bindWgpuImageResourceTexture, getWgpuRenderStateRuntime, getWgpuSampler } from '@flighthq/render-wgpu';
|
|
5
|
+
import { getTextureUvMatrix, hasTextureUvTransform } from '@flighthq/texture';
|
|
6
|
+
import { MAX_FORWARD_LIGHTS, SCENE_LIGHT_HEMISPHERE_OFFSET, SCENE_LIGHT_HEMISPHERE_STRIDE, SCENE_LIGHT_POINT_OFFSET, SCENE_LIGHT_POINT_STRIDE, SCENE_LIGHT_SPOT_OFFSET, SCENE_LIGHT_SPOT_STRIDE, } from '@flighthq/types';
|
|
7
|
+
import { ensureWgpuMeshUpload } from './wgpuMeshUpload';
|
|
8
|
+
import { getWgpuScene3DRuntime } from './wgpuScene3DRuntime';
|
|
9
|
+
// Sets the family's pipeline active for the bind→draw handoff, binds it, and binds the shared Frame
|
|
10
|
+
// bind group at group(0). A family's bind() calls this after selecting its pipeline + writing the
|
|
11
|
+
// Frame uniform; draw() reads scene.activeMeshPipeline back. Mirrors scene-gl's beginGlMeshDraw.
|
|
12
|
+
export function beginWgpuMeshDraw(state, pipeline) {
|
|
13
|
+
const stateRuntime = getWgpuRenderStateRuntime(state);
|
|
14
|
+
const pass = stateRuntime.renderPass;
|
|
15
|
+
if (pass === null)
|
|
16
|
+
return;
|
|
17
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
18
|
+
scene.activeMeshPipeline = pipeline;
|
|
19
|
+
pass.setPipeline(pipeline.pipeline);
|
|
20
|
+
pass.setBindGroup(0, scene.frameBindGroup);
|
|
21
|
+
// Lit families that PCF-sample the directional shadow map carry a group(3) shadow layout; bind the
|
|
22
|
+
// shared shadow-sample group (the real depth map when drawWgpuScene3DShadowMap ran this frame, else a
|
|
23
|
+
// 1x1 dummy gated off by the shadow uniform). Non-lit families have no group(3) and skip this.
|
|
24
|
+
if (pipeline.hasPbrSampleGroup) {
|
|
25
|
+
pass.setBindGroup(3, ensureWgpuPbrSampleBindGroup(state));
|
|
26
|
+
}
|
|
27
|
+
else if (pipeline.hasShadowGroup) {
|
|
28
|
+
pass.setBindGroup(3, ensureWgpuShadowSampleBindGroup(state));
|
|
29
|
+
}
|
|
30
|
+
// Legacy IBL-only layouts are still supported for callers that provide one directly.
|
|
31
|
+
if (pipeline.hasIblGroup) {
|
|
32
|
+
pass.setBindGroup(4, ensureWgpuIblSampleBindGroup(state));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Builds a material bind group in the shared classic/PBR entry layout: the uniform buffer at binding 0,
|
|
36
|
+
// the sampler at binding 1, then each resolved map view at binding 2 + i. Both the classic and standard-
|
|
37
|
+
// PBR binders share this shape, so the same builder + the same rebuild check (below) cover both.
|
|
38
|
+
export function buildWgpuMaterialBindGroup(state, layout, buffer, sampler, views) {
|
|
39
|
+
const entries = [
|
|
40
|
+
{ binding: 0, resource: { buffer } },
|
|
41
|
+
{ binding: 1, resource: sampler },
|
|
42
|
+
];
|
|
43
|
+
for (let i = 0; i < views.length; i++)
|
|
44
|
+
entries.push({ binding: 2 + i, resource: views[i] });
|
|
45
|
+
return state.device.createBindGroup({ layout, entries });
|
|
46
|
+
}
|
|
47
|
+
// Per-map material bind group: uniform at binding 0, N samplers at 1..N, then N matching texture
|
|
48
|
+
// views at N+1..2N. Keeping the two equal-length arrays parallel makes each WGSL map select its own
|
|
49
|
+
// Texture.sampler while preserving a fixed layout for absent maps via placeholder views/default samplers.
|
|
50
|
+
export function buildWgpuPerMapMaterialBindGroup(state, layout, buffer, samplers, views) {
|
|
51
|
+
const count = views.length;
|
|
52
|
+
const entries = [{ binding: 0, resource: { buffer } }];
|
|
53
|
+
for (let i = 0; i < count; i++)
|
|
54
|
+
entries.push({ binding: 1 + i, resource: samplers[i] });
|
|
55
|
+
for (let i = 0; i < count; i++)
|
|
56
|
+
entries.push({ binding: 1 + count + i, resource: views[i] });
|
|
57
|
+
return state.device.createBindGroup({ layout, entries });
|
|
58
|
+
}
|
|
59
|
+
// Builds a render pipeline for a family: compiles its WGSL module, and lays out [shared Frame, shared
|
|
60
|
+
// Draw, family Material] over the canonical 48-byte PBR vertex. Depth-stencil is depth24plus-stencil8,
|
|
61
|
+
// compare 'less', depth-write on (the scene pass owns depth; stencil inert); culling is back-face
|
|
62
|
+
// unless doubleSided. The family passes its own materialBindGroupLayout + entry points (default
|
|
63
|
+
// vs_main/fs_main). A lit family that PCF-samples the directional shadow map passes the shared
|
|
64
|
+
// group(3) `shadowBindGroupLayout` (ensureWgpuShadowSampleLayout), which extends the pipeline layout to
|
|
65
|
+
// [Frame, Draw, Material, Shadow] and flags the pipeline so beginWgpuMeshDraw binds group(3). A custom
|
|
66
|
+
// family may instead pass `extraBindGroupLayout`; it occupies group(3), but the family binds it itself.
|
|
67
|
+
export function createWgpuMeshPipeline(state, options) {
|
|
68
|
+
const device = state.device;
|
|
69
|
+
const layouts = ensureWgpuScene3DLayouts(state);
|
|
70
|
+
const skinning = getWgpuScene3DRuntime(state).skinningAdapter;
|
|
71
|
+
const bindGroupLayouts = [
|
|
72
|
+
layouts.frameBindGroupLayout,
|
|
73
|
+
options.skinned && skinning !== null ? skinning.getDrawLayout(state) : layouts.drawBindGroupLayout,
|
|
74
|
+
options.materialBindGroupLayout,
|
|
75
|
+
];
|
|
76
|
+
if (options.extraBindGroupLayout !== undefined) {
|
|
77
|
+
bindGroupLayouts.push(options.extraBindGroupLayout);
|
|
78
|
+
}
|
|
79
|
+
if (options.extraBindGroupLayout === undefined && options.pbrSampleBindGroupLayout !== undefined) {
|
|
80
|
+
bindGroupLayouts.push(options.pbrSampleBindGroupLayout);
|
|
81
|
+
}
|
|
82
|
+
else if (options.extraBindGroupLayout === undefined) {
|
|
83
|
+
// Group order is positional: shadow (group 3) then IBL (group 4). Prefer pbrSampleBindGroupLayout
|
|
84
|
+
// for new PBR pipelines so they fit WebGPU's minimum maxBindGroups=4.
|
|
85
|
+
if (options.shadowBindGroupLayout !== undefined)
|
|
86
|
+
bindGroupLayouts.push(options.shadowBindGroupLayout);
|
|
87
|
+
if (options.iblBindGroupLayout !== undefined)
|
|
88
|
+
bindGroupLayouts.push(options.iblBindGroupLayout);
|
|
89
|
+
}
|
|
90
|
+
const layout = device.createPipelineLayout({ bindGroupLayouts });
|
|
91
|
+
const pipeline = device.createRenderPipeline({
|
|
92
|
+
layout,
|
|
93
|
+
vertex: {
|
|
94
|
+
module: options.module,
|
|
95
|
+
entryPoint: 'vs_main',
|
|
96
|
+
buffers: options.skinned && skinning !== null ? skinning.vertexBufferLayouts : VERTEX_BUFFER_LAYOUTS,
|
|
97
|
+
},
|
|
98
|
+
fragment: {
|
|
99
|
+
module: options.module,
|
|
100
|
+
entryPoint: 'fs_main',
|
|
101
|
+
targets: [
|
|
102
|
+
{
|
|
103
|
+
format: options.format,
|
|
104
|
+
blend: options.blended
|
|
105
|
+
? {
|
|
106
|
+
color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
107
|
+
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
108
|
+
}
|
|
109
|
+
: undefined,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
primitive: {
|
|
114
|
+
topology: options.topology ?? 'triangle-list',
|
|
115
|
+
frontFace: 'ccw',
|
|
116
|
+
cullMode: options.doubleSided ? 'none' : 'back',
|
|
117
|
+
},
|
|
118
|
+
depthStencil: { format: DEPTH_STENCIL_FORMAT, depthWriteEnabled: !options.blended, depthCompare: 'less' },
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
hasIblGroup: options.iblBindGroupLayout !== undefined,
|
|
122
|
+
hasPbrSampleGroup: options.pbrSampleBindGroupLayout !== undefined,
|
|
123
|
+
hasShadowGroup: options.shadowBindGroupLayout !== undefined,
|
|
124
|
+
materialBindGroupLayout: options.materialBindGroupLayout,
|
|
125
|
+
pipeline,
|
|
126
|
+
skinned: options.skinned === true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// The shared per-draw tail for every mesh-material family: ring-allocates + writes the Draw uniform
|
|
130
|
+
// (world + normal matrix) for the proxy, lazily uploads the geometry's GPU buffers (cached by
|
|
131
|
+
// geometry.version), binds the dynamic-offset Draw group at group(1) + the vertex/index buffers, and
|
|
132
|
+
// issues the indexed draw over the proxy's subset. A family's draw() reads scene.activeMeshPipeline (set
|
|
133
|
+
// by beginWgpuMeshDraw) before calling this. Mirrors scene-gl's drawGlMeshSubset.
|
|
134
|
+
export function drawWgpuMeshSubset(state, proxy, geometry) {
|
|
135
|
+
const stateRuntime = getWgpuRenderStateRuntime(state);
|
|
136
|
+
const pass = stateRuntime.renderPass;
|
|
137
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
138
|
+
if (pass === null || scene.activeMeshPipeline === null)
|
|
139
|
+
return;
|
|
140
|
+
const subset = proxy.subset;
|
|
141
|
+
if (subset.indexCount === 0)
|
|
142
|
+
return;
|
|
143
|
+
const upload = ensureWgpuMeshUpload(state, geometry, scene.activeMeshPipeline.skinned);
|
|
144
|
+
if (upload === null || upload.indexBuffer === null)
|
|
145
|
+
return;
|
|
146
|
+
const drawBindGroup = writeWgpuDrawUniform(state, proxy);
|
|
147
|
+
const activePipeline = scene.activeMeshPipeline;
|
|
148
|
+
const jointMatrices = proxy.jointMatrices ?? null;
|
|
149
|
+
const skinning = scene.skinningAdapter;
|
|
150
|
+
const boundDrawGroup = activePipeline.skinned && jointMatrices !== null && skinning !== null
|
|
151
|
+
? skinning.getDrawBindGroup(state, jointMatrices)
|
|
152
|
+
: drawBindGroup;
|
|
153
|
+
_dynamicOffsets[0] = scene.pendingDrawOffset;
|
|
154
|
+
pass.setBindGroup(1, boundDrawGroup, _dynamicOffsets);
|
|
155
|
+
pass.setVertexBuffer(0, upload.vertexBuffer);
|
|
156
|
+
pass.setIndexBuffer(upload.indexBuffer, upload.indexFormat);
|
|
157
|
+
pass.drawIndexed(subset.indexCount, 1, subset.indexOffset, 0, 0);
|
|
158
|
+
}
|
|
159
|
+
// Resolves the shared Frame bind group, creating it from the shared Frame layout + Frame buffer on
|
|
160
|
+
// first use. Every family pipeline declares the same group(0) layout, so this one bind group is valid
|
|
161
|
+
// for all of them.
|
|
162
|
+
export function ensureWgpuFrameBindGroup(state) {
|
|
163
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
164
|
+
if (scene.frameBuffer === null) {
|
|
165
|
+
scene.frameBuffer = state.device.createBuffer({
|
|
166
|
+
size: FRAME_UNIFORM_BYTES,
|
|
167
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (scene.frameBindGroup === null) {
|
|
171
|
+
scene.frameBindGroup = state.device.createBindGroup({
|
|
172
|
+
layout: ensureWgpuScene3DLayouts(state).frameBindGroupLayout,
|
|
173
|
+
entries: [{ binding: 0, resource: { buffer: scene.frameBuffer } }],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return scene.frameBindGroup;
|
|
177
|
+
}
|
|
178
|
+
// Resolves the shared IBL sample bind group a caller binds when its pipeline carries the
|
|
179
|
+
// legacy standalone IBL layout — the WGSL counterpart of scene-gl's IBL texture-unit + u_ibl* uniform binds in
|
|
180
|
+
// bindGlMeshLightBlock. Lazily creates the IBL uniform buffer (enabled/intensity/maxMip), a filtering
|
|
181
|
+
// sampler, and 1x1 dummy cube + 2D-LUT textures for the no-IBL case, then rewrites the uniform every call
|
|
182
|
+
// (from scene.ibl) and rebuilds the bind group only when the bound irradiance view changes (present ↔
|
|
183
|
+
// absent). A scene with no baked environment still renders: the dummy views are bound and the shader's
|
|
184
|
+
// `enabled < 0.5` gate keeps them unsampled — mirroring GL's u_iblEnabled = 0 placeholder path.
|
|
185
|
+
export function ensureWgpuIblSampleBindGroup(state) {
|
|
186
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
187
|
+
const device = state.device;
|
|
188
|
+
if (scene.iblUniformBuffer === null) {
|
|
189
|
+
scene.iblUniformBuffer = device.createBuffer({
|
|
190
|
+
size: IBL_SAMPLE_UNIFORM_BYTES,
|
|
191
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (scene.iblSampler === null) {
|
|
195
|
+
// Trilinear so the prefiltered specular cube's roughness mip chain (textureSampleLevel) filters.
|
|
196
|
+
scene.iblSampler = device.createSampler({
|
|
197
|
+
magFilter: 'linear',
|
|
198
|
+
minFilter: 'linear',
|
|
199
|
+
mipmapFilter: 'linear',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (scene.iblDummyCubeView === null) {
|
|
203
|
+
// 1x1 cube + 1x1 2D bound when no IBL is baked this frame; never sampled (the enabled flag gates them
|
|
204
|
+
// off), they only satisfy the texture_cube / texture_2d slots so the draw is valid.
|
|
205
|
+
scene.iblDummyCubeTexture = device.createTexture({
|
|
206
|
+
size: [1, 1, 6],
|
|
207
|
+
format: IBL_DUMMY_FORMAT,
|
|
208
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
209
|
+
});
|
|
210
|
+
scene.iblDummyCubeView = scene.iblDummyCubeTexture.createView({ dimension: 'cube' });
|
|
211
|
+
scene.iblDummyLutTexture = device.createTexture({
|
|
212
|
+
size: [1, 1, 1],
|
|
213
|
+
format: IBL_DUMMY_FORMAT,
|
|
214
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
215
|
+
});
|
|
216
|
+
scene.iblDummyLutView = scene.iblDummyLutTexture.createView();
|
|
217
|
+
}
|
|
218
|
+
const ibl = scene.ibl;
|
|
219
|
+
const u = _iblSampleScratch;
|
|
220
|
+
if (ibl !== null) {
|
|
221
|
+
u[0] = 1; // enabled
|
|
222
|
+
u[1] = ibl.intensity;
|
|
223
|
+
u[2] = ibl.prefilteredMipCount - 1; // maxMip (roughness 1.0)
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
u[0] = 0;
|
|
227
|
+
u[1] = 1;
|
|
228
|
+
u[2] = 0;
|
|
229
|
+
}
|
|
230
|
+
u[3] = 0;
|
|
231
|
+
device.queue.writeBuffer(scene.iblUniformBuffer, 0, u.buffer, 0, IBL_SAMPLE_UNIFORM_BYTES);
|
|
232
|
+
const irradianceView = ibl !== null ? ibl.irradianceCubeView : scene.iblDummyCubeView;
|
|
233
|
+
const prefilteredView = ibl !== null ? ibl.prefilteredCubeView : scene.iblDummyCubeView;
|
|
234
|
+
const brdfView = ibl !== null ? ibl.brdfLutView : scene.iblDummyLutView;
|
|
235
|
+
if (scene.iblSampleBindGroup === null || scene.iblSampleCubeView !== irradianceView) {
|
|
236
|
+
scene.iblSampleBindGroup = device.createBindGroup({
|
|
237
|
+
layout: ensureWgpuIblSampleLayout(state),
|
|
238
|
+
entries: [
|
|
239
|
+
{ binding: 0, resource: { buffer: scene.iblUniformBuffer } },
|
|
240
|
+
{ binding: 1, resource: irradianceView },
|
|
241
|
+
{ binding: 2, resource: prefilteredView },
|
|
242
|
+
{ binding: 3, resource: brdfView },
|
|
243
|
+
{ binding: 4, resource: scene.iblSampler },
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
scene.iblSampleCubeView = irradianceView;
|
|
247
|
+
}
|
|
248
|
+
return scene.iblSampleBindGroup;
|
|
249
|
+
}
|
|
250
|
+
// Resolves the shared standalone IBL sample bind-group layout (uniform enabled/intensity/maxMip, a diffuse
|
|
251
|
+
// irradiance cube, a prefiltered specular cube, a 2D BRDF LUT, and a filtering sampler), created once per
|
|
252
|
+
// state. Lit PBR pipelines pass this to createWgpuMeshPipeline; the shared bind group built by
|
|
253
|
+
// ensureWgpuIblSampleBindGroup targets it, so one IBL bind group serves every lit PBR pipeline variant.
|
|
254
|
+
export function ensureWgpuIblSampleLayout(state) {
|
|
255
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
256
|
+
if (scene.iblSampleLayout === null) {
|
|
257
|
+
scene.iblSampleLayout = state.device.createBindGroupLayout({
|
|
258
|
+
entries: [
|
|
259
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
260
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float', viewDimension: 'cube' } },
|
|
261
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float', viewDimension: 'cube' } },
|
|
262
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
263
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return scene.iblSampleLayout;
|
|
268
|
+
}
|
|
269
|
+
// Fetches (or on first use creates) the per-material binding cached under `key`, rebuilding its bind
|
|
270
|
+
// group ONLY when the primary sampler or a resolved map view differs from the cached set. The hot path
|
|
271
|
+
// (an unchanged material re-bound every frame) allocates nothing: the caller fills a REUSED `views`
|
|
272
|
+
// scratch, this compares it against the binding's owned view array in place, and returns the cached
|
|
273
|
+
// bind group untouched. On a create OR a rebuild — both change events, not steady state —
|
|
274
|
+
// buildWgpuMaterialBindGroup (re)allocates the GPUBindGroupEntry array; the binding's own view-array
|
|
275
|
+
// copy is allocated only on create (or a view-count change), and a rebuild reuses the buffer and
|
|
276
|
+
// overwrites the owned view array in place. This is the shared no-hidden-allocation cache both the
|
|
277
|
+
// classic and standard-PBR binders route through. The caller still writes the uniform buffer + stashes
|
|
278
|
+
// the uv transform after this returns.
|
|
279
|
+
export function ensureWgpuMaterialBinding(state, key, layout, uniformByteSize, sampler, views) {
|
|
280
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
281
|
+
let binding = scene.materialBindGroups.get(key);
|
|
282
|
+
if (binding === undefined) {
|
|
283
|
+
const buffer = state.device.createBuffer({
|
|
284
|
+
size: uniformByteSize,
|
|
285
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
286
|
+
});
|
|
287
|
+
binding = {
|
|
288
|
+
bindGroup: buildWgpuMaterialBindGroup(state, layout, buffer, sampler, views),
|
|
289
|
+
buffer,
|
|
290
|
+
sampler,
|
|
291
|
+
views: views.slice(),
|
|
292
|
+
};
|
|
293
|
+
scene.materialBindGroups.set(key, binding);
|
|
294
|
+
}
|
|
295
|
+
else if (isWgpuMaterialBindGroupRebuildNeeded(binding, sampler, views)) {
|
|
296
|
+
binding.bindGroup = buildWgpuMaterialBindGroup(state, layout, binding.buffer, sampler, views);
|
|
297
|
+
binding.sampler = sampler;
|
|
298
|
+
// Overwrite the binding-owned view array in place — never alias the reused caller scratch, which the
|
|
299
|
+
// next material's bind will overwrite.
|
|
300
|
+
const cached = binding.views;
|
|
301
|
+
if (cached === undefined || cached.length !== views.length) {
|
|
302
|
+
binding.views = views.slice();
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
for (let i = 0; i < views.length; i++)
|
|
306
|
+
cached[i] = views[i];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return binding;
|
|
310
|
+
}
|
|
311
|
+
// Resolves the combined PBR sample bind group. WebGPU's required maxBindGroups minimum is 4, so PBR
|
|
312
|
+
// cannot afford separate shadow group(3) and IBL group(4). This layout packs both into group(3).
|
|
313
|
+
export function ensureWgpuPbrSampleBindGroup(state) {
|
|
314
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
315
|
+
const device = state.device;
|
|
316
|
+
if (scene.shadowUniformBuffer === null) {
|
|
317
|
+
scene.shadowUniformBuffer = device.createBuffer({
|
|
318
|
+
size: SHADOW_SAMPLE_UNIFORM_BYTES,
|
|
319
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (scene.shadowComparisonSampler === null) {
|
|
323
|
+
scene.shadowComparisonSampler = device.createSampler({ compare: 'less-equal' });
|
|
324
|
+
}
|
|
325
|
+
if (scene.shadowDummyView === null) {
|
|
326
|
+
scene.shadowDummyTexture = device.createTexture({
|
|
327
|
+
size: [1, 1, 1],
|
|
328
|
+
format: SHADOW_DEPTH_FORMAT,
|
|
329
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
330
|
+
});
|
|
331
|
+
scene.shadowDummyView = scene.shadowDummyTexture.createView();
|
|
332
|
+
}
|
|
333
|
+
if (scene.iblUniformBuffer === null) {
|
|
334
|
+
scene.iblUniformBuffer = device.createBuffer({
|
|
335
|
+
size: IBL_SAMPLE_UNIFORM_BYTES,
|
|
336
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (scene.iblSampler === null) {
|
|
340
|
+
scene.iblSampler = device.createSampler({
|
|
341
|
+
magFilter: 'linear',
|
|
342
|
+
minFilter: 'linear',
|
|
343
|
+
mipmapFilter: 'linear',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (scene.iblDummyCubeView === null) {
|
|
347
|
+
scene.iblDummyCubeTexture = device.createTexture({
|
|
348
|
+
size: [1, 1, 6],
|
|
349
|
+
format: IBL_DUMMY_FORMAT,
|
|
350
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
351
|
+
});
|
|
352
|
+
scene.iblDummyCubeView = scene.iblDummyCubeTexture.createView({ dimension: 'cube' });
|
|
353
|
+
scene.iblDummyLutTexture = device.createTexture({
|
|
354
|
+
size: [1, 1, 1],
|
|
355
|
+
format: IBL_DUMMY_FORMAT,
|
|
356
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
357
|
+
});
|
|
358
|
+
scene.iblDummyLutView = scene.iblDummyLutTexture.createView();
|
|
359
|
+
}
|
|
360
|
+
const shadow = scene.shadow;
|
|
361
|
+
const s = _shadowSampleScratch;
|
|
362
|
+
if (shadow !== null) {
|
|
363
|
+
const m = shadow.matrix.m;
|
|
364
|
+
for (let i = 0; i < 16; i++)
|
|
365
|
+
s[i] = m[i];
|
|
366
|
+
s[16] = 1;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
for (let i = 0; i < 16; i++)
|
|
370
|
+
s[i] = 0;
|
|
371
|
+
s[0] = 1;
|
|
372
|
+
s[5] = 1;
|
|
373
|
+
s[10] = 1;
|
|
374
|
+
s[15] = 1;
|
|
375
|
+
s[16] = 0;
|
|
376
|
+
}
|
|
377
|
+
s[17] = 0;
|
|
378
|
+
s[18] = 0;
|
|
379
|
+
s[19] = 0;
|
|
380
|
+
device.queue.writeBuffer(scene.shadowUniformBuffer, 0, s.buffer, 0, SHADOW_SAMPLE_UNIFORM_BYTES);
|
|
381
|
+
const ibl = scene.ibl;
|
|
382
|
+
const u = _iblSampleScratch;
|
|
383
|
+
if (ibl !== null) {
|
|
384
|
+
u[0] = 1;
|
|
385
|
+
u[1] = ibl.intensity;
|
|
386
|
+
u[2] = ibl.prefilteredMipCount - 1;
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
u[0] = 0;
|
|
390
|
+
u[1] = 1;
|
|
391
|
+
u[2] = 0;
|
|
392
|
+
}
|
|
393
|
+
u[3] = 0;
|
|
394
|
+
device.queue.writeBuffer(scene.iblUniformBuffer, 0, u.buffer, 0, IBL_SAMPLE_UNIFORM_BYTES);
|
|
395
|
+
const shadowView = shadow !== null ? shadow.depthView : scene.shadowDummyView;
|
|
396
|
+
const irradianceView = ibl !== null ? ibl.irradianceCubeView : scene.iblDummyCubeView;
|
|
397
|
+
const prefilteredView = ibl !== null ? ibl.prefilteredCubeView : scene.iblDummyCubeView;
|
|
398
|
+
const brdfView = ibl !== null ? ibl.brdfLutView : scene.iblDummyLutView;
|
|
399
|
+
if (scene.pbrSampleBindGroup === null ||
|
|
400
|
+
scene.pbrSampleShadowView !== shadowView ||
|
|
401
|
+
scene.pbrSampleIblCubeView !== irradianceView) {
|
|
402
|
+
scene.pbrSampleBindGroup = device.createBindGroup({
|
|
403
|
+
layout: ensureWgpuPbrSampleLayout(state),
|
|
404
|
+
entries: [
|
|
405
|
+
{ binding: 0, resource: { buffer: scene.shadowUniformBuffer } },
|
|
406
|
+
{ binding: 1, resource: shadowView },
|
|
407
|
+
{ binding: 2, resource: scene.shadowComparisonSampler },
|
|
408
|
+
{ binding: 3, resource: { buffer: scene.iblUniformBuffer } },
|
|
409
|
+
{ binding: 4, resource: irradianceView },
|
|
410
|
+
{ binding: 5, resource: prefilteredView },
|
|
411
|
+
{ binding: 6, resource: brdfView },
|
|
412
|
+
{ binding: 7, resource: scene.iblSampler },
|
|
413
|
+
],
|
|
414
|
+
});
|
|
415
|
+
scene.pbrSampleShadowView = shadowView;
|
|
416
|
+
scene.pbrSampleIblCubeView = irradianceView;
|
|
417
|
+
}
|
|
418
|
+
return scene.pbrSampleBindGroup;
|
|
419
|
+
}
|
|
420
|
+
export function ensureWgpuPbrSampleLayout(state) {
|
|
421
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
422
|
+
if (scene.pbrSampleLayout === null) {
|
|
423
|
+
scene.pbrSampleLayout = state.device.createBindGroupLayout({
|
|
424
|
+
entries: [
|
|
425
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
426
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
|
|
427
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
|
|
428
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
429
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float', viewDimension: 'cube' } },
|
|
430
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float', viewDimension: 'cube' } },
|
|
431
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
432
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return scene.pbrSampleLayout;
|
|
437
|
+
}
|
|
438
|
+
export function ensureWgpuPerMapMaterialBinding(state, key, layout, uniformByteSize, samplers, views) {
|
|
439
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
440
|
+
let binding = scene.materialBindGroups.get(key);
|
|
441
|
+
if (binding === undefined) {
|
|
442
|
+
const buffer = state.device.createBuffer({
|
|
443
|
+
size: uniformByteSize,
|
|
444
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
445
|
+
});
|
|
446
|
+
binding = {
|
|
447
|
+
bindGroup: buildWgpuPerMapMaterialBindGroup(state, layout, buffer, samplers, views),
|
|
448
|
+
buffer,
|
|
449
|
+
samplers: samplers.slice(),
|
|
450
|
+
views: views.slice(),
|
|
451
|
+
};
|
|
452
|
+
scene.materialBindGroups.set(key, binding);
|
|
453
|
+
}
|
|
454
|
+
else if (wgpuPerMapMaterialBindGroupNeedsRebuild(binding, samplers, views)) {
|
|
455
|
+
binding.bindGroup = buildWgpuPerMapMaterialBindGroup(state, layout, binding.buffer, samplers, views);
|
|
456
|
+
overwriteIdentityCache(binding, 'samplers', samplers);
|
|
457
|
+
overwriteIdentityCache(binding, 'views', views);
|
|
458
|
+
}
|
|
459
|
+
return binding;
|
|
460
|
+
}
|
|
461
|
+
// The one-time opaque-white 1x1 RGBA texture view bound to a family's map slots in the untextured
|
|
462
|
+
// path, so a material bind-group layout that declares texture slots can be satisfied without uploading
|
|
463
|
+
// real maps. Shared across families (cached on the scene runtime).
|
|
464
|
+
export function ensureWgpuPlaceholderTextureView(state) {
|
|
465
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
466
|
+
let view = scene.placeholderView;
|
|
467
|
+
if (view === null) {
|
|
468
|
+
const texture = state.device.createTexture({
|
|
469
|
+
size: [1, 1, 1],
|
|
470
|
+
format: 'rgba8unorm',
|
|
471
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
472
|
+
});
|
|
473
|
+
state.device.queue.writeTexture({ texture }, WHITE_PIXEL, { bytesPerRow: 4 }, [1, 1, 1]);
|
|
474
|
+
view = texture.createView();
|
|
475
|
+
scene.placeholderView = view;
|
|
476
|
+
}
|
|
477
|
+
return view;
|
|
478
|
+
}
|
|
479
|
+
// Resolves the shared group(0) Frame + group(1) Draw bind-group layouts, creating them once per state.
|
|
480
|
+
// group(0) is a single uniform visible to both stages (camera + lights); group(1) is a dynamic-offset
|
|
481
|
+
// uniform visible to the vertex scene2d (per-draw world + normal matrix).
|
|
482
|
+
export function ensureWgpuScene3DLayouts(state) {
|
|
483
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
484
|
+
if (scene.frameBindGroupLayout === null || scene.drawBindGroupLayout === null) {
|
|
485
|
+
const device = state.device;
|
|
486
|
+
scene.frameBindGroupLayout = device.createBindGroupLayout({
|
|
487
|
+
entries: [
|
|
488
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
scene.drawBindGroupLayout = device.createBindGroupLayout({
|
|
492
|
+
entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform', hasDynamicOffset: true } }],
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
return { drawBindGroupLayout: scene.drawBindGroupLayout, frameBindGroupLayout: scene.frameBindGroupLayout };
|
|
496
|
+
}
|
|
497
|
+
// Resolves a compiled pipeline for a string cache key, compiling it via the factory on first use and
|
|
498
|
+
// caching it on the scene runtime's per-state pipelineCache. Every family routes its pipeline through
|
|
499
|
+
// this one cache; the key is namespaced by family + define key + color format (for example
|
|
500
|
+
// `unlit:bgra8unorm|-c-`), so families and feature/format variants compile at most once and never
|
|
501
|
+
// collide. Mirrors scene-gl's ensureGlScene3DProgram.
|
|
502
|
+
export function ensureWgpuScene3DPipeline(state, key, compile) {
|
|
503
|
+
const runtime = getWgpuScene3DRuntime(state);
|
|
504
|
+
const blended = runtime.activeBlendedRun;
|
|
505
|
+
const skinned = runtime.activeSkinnedRun;
|
|
506
|
+
const variantKey = `${key}|${blended ? 'blend' : 'opaque'}|${skinned ? 'skin' : 'rigid'}`;
|
|
507
|
+
let pipeline = runtime.pipelineCache.get(variantKey);
|
|
508
|
+
if (pipeline === undefined) {
|
|
509
|
+
pipeline = compile(blended, skinned);
|
|
510
|
+
runtime.pipelineCache.set(variantKey, pipeline);
|
|
511
|
+
}
|
|
512
|
+
return pipeline;
|
|
513
|
+
}
|
|
514
|
+
// Resolves the shared group(3) shadow-sample bind group a lit family binds when its pipeline carries the
|
|
515
|
+
// shadow layout — the WGSL counterpart of scene-gl's shadow texture-unit + u_shadow* uniform binds in
|
|
516
|
+
// bindGlMeshLightBlock. Lazily creates the shadow uniform buffer (light matrix + enabled flag), the
|
|
517
|
+
// comparison sampler ('less-equal', matching the GL PCF's `current <= closest`), and a 1x1 dummy depth
|
|
518
|
+
// texture for the no-shadow case, then rewrites the uniform every call (matrix + enabled from
|
|
519
|
+
// scene.shadow) and rebuilds the bind group only when the bound depth view changes (present ↔ absent).
|
|
520
|
+
// A shadow-less scene still renders: the dummy view is bound and the shader's `enabled < 0.5` early-out
|
|
521
|
+
// keeps it unsampled — mirroring GL's u_shadowEnabled = 0 path.
|
|
522
|
+
export function ensureWgpuShadowSampleBindGroup(state) {
|
|
523
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
524
|
+
const device = state.device;
|
|
525
|
+
if (scene.shadowUniformBuffer === null) {
|
|
526
|
+
scene.shadowUniformBuffer = device.createBuffer({
|
|
527
|
+
size: SHADOW_SAMPLE_UNIFORM_BYTES,
|
|
528
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (scene.shadowComparisonSampler === null) {
|
|
532
|
+
scene.shadowComparisonSampler = device.createSampler({ compare: 'less-equal' });
|
|
533
|
+
}
|
|
534
|
+
if (scene.shadowDummyView === null) {
|
|
535
|
+
// A 1x1 sampleable depth texture bound when no shadow map exists this frame; never actually sampled
|
|
536
|
+
// (the enabled flag gates it off), it only satisfies the group(3) texture_depth_2d slot.
|
|
537
|
+
scene.shadowDummyTexture = device.createTexture({
|
|
538
|
+
size: [1, 1, 1],
|
|
539
|
+
format: SHADOW_DEPTH_FORMAT,
|
|
540
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
541
|
+
});
|
|
542
|
+
scene.shadowDummyView = scene.shadowDummyTexture.createView();
|
|
543
|
+
}
|
|
544
|
+
const shadow = scene.shadow;
|
|
545
|
+
const s = _shadowSampleScratch;
|
|
546
|
+
if (shadow !== null) {
|
|
547
|
+
const m = shadow.matrix.m;
|
|
548
|
+
for (let i = 0; i < 16; i++)
|
|
549
|
+
s[i] = m[i];
|
|
550
|
+
s[16] = 1; // enabled
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
for (let i = 0; i < 16; i++)
|
|
554
|
+
s[i] = 0;
|
|
555
|
+
s[0] = 1;
|
|
556
|
+
s[5] = 1;
|
|
557
|
+
s[10] = 1;
|
|
558
|
+
s[15] = 1; // identity
|
|
559
|
+
s[16] = 0; // disabled
|
|
560
|
+
}
|
|
561
|
+
s[17] = 0;
|
|
562
|
+
s[18] = 0;
|
|
563
|
+
s[19] = 0;
|
|
564
|
+
device.queue.writeBuffer(scene.shadowUniformBuffer, 0, s.buffer, 0, SHADOW_SAMPLE_UNIFORM_BYTES);
|
|
565
|
+
const view = shadow !== null ? shadow.depthView : scene.shadowDummyView;
|
|
566
|
+
if (scene.shadowSampleBindGroup === null || scene.shadowSampleView !== view) {
|
|
567
|
+
scene.shadowSampleBindGroup = device.createBindGroup({
|
|
568
|
+
layout: ensureWgpuShadowSampleLayout(state),
|
|
569
|
+
entries: [
|
|
570
|
+
{ binding: 0, resource: { buffer: scene.shadowUniformBuffer } },
|
|
571
|
+
{ binding: 1, resource: view },
|
|
572
|
+
{ binding: 2, resource: scene.shadowComparisonSampler },
|
|
573
|
+
],
|
|
574
|
+
});
|
|
575
|
+
scene.shadowSampleView = view;
|
|
576
|
+
}
|
|
577
|
+
return scene.shadowSampleBindGroup;
|
|
578
|
+
}
|
|
579
|
+
// Resolves the shared group(3) shadow-sample bind-group layout (uniform light matrix + enabled flag, a
|
|
580
|
+
// depth texture, and a comparison sampler), created once per state. Lit pipelines pass this to
|
|
581
|
+
// createWgpuMeshPipeline; the shared bind group built by ensureWgpuShadowSampleBindGroup targets it, so
|
|
582
|
+
// one shadow bind group serves every lit family's pipeline (pbr today).
|
|
583
|
+
export function ensureWgpuShadowSampleLayout(state) {
|
|
584
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
585
|
+
if (scene.shadowSampleLayout === null) {
|
|
586
|
+
scene.shadowSampleLayout = state.device.createBindGroupLayout({
|
|
587
|
+
entries: [
|
|
588
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
589
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
|
|
590
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
|
|
591
|
+
],
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return scene.shadowSampleLayout;
|
|
595
|
+
}
|
|
596
|
+
// Selects the ONE GPUSampler a material bind group uses, from its PRIMARY map's full sampler descriptor
|
|
597
|
+
// (the diffuse map for classic, the base-color map for PBR): wrap (a tiling repeat/mirror-repeat map
|
|
598
|
+
// gets the matching cached sampler so setTextureUvScale tiles), min/mag filter, a mip filter when the
|
|
599
|
+
// map requests a mip chain (paired with the mip chain generateWgpuMipmaps builds on upload), and
|
|
600
|
+
// anisotropy. A null/absent primary map falls back to the shared clamp sampler.
|
|
601
|
+
//
|
|
602
|
+
// SHARED-PRIMARY-SAMPLER CONTRACT (a deliberate wgpu↔GL divergence): the group(2) material layout carries
|
|
603
|
+
// a single sampler at binding 1 that the WGSL samples EVERY map (diffuse/specular/normal/alpha; base-
|
|
604
|
+
// color/metallic-roughness/normal/occlusion/emissive/alpha) through. So a non-primary map's per-Texture
|
|
605
|
+
// sampler is NOT honored on wgpu — only the primary map's. scene-gl, by contrast, binds each map with
|
|
606
|
+
// its own `map.sampler` (bindGlImageResourceTexture). This keeps the wgpu bind group to one sampler; a
|
|
607
|
+
// per-map-sampler wgpu path (one sampler binding per map) is a chartered later change, not a bug. The
|
|
608
|
+
// invalidation cache tracks this one sampler accordingly (see isWgpuMaterialBindGroupRebuildNeeded).
|
|
609
|
+
// Because a GPUSampler is immutable and baked into the cached bind group, this reads the descriptor at
|
|
610
|
+
// bind-group creation — the same lifetime as the resolved texture views.
|
|
611
|
+
export function getWgpuMaterialSampler(state, texture) {
|
|
612
|
+
if (texture === null)
|
|
613
|
+
return getWgpuRenderStateRuntime(state).linearSampler;
|
|
614
|
+
const sampler = texture.sampler;
|
|
615
|
+
const filter = sampler.magFilter.startsWith('nearest') ? 'nearest' : 'linear';
|
|
616
|
+
const useMips = sampler.mipmaps && sampler.minFilter !== 'linear' && sampler.minFilter !== 'nearest';
|
|
617
|
+
const mipmapFilter = useMips
|
|
618
|
+
? sampler.minFilter.endsWith('nearest')
|
|
619
|
+
? 'nearest'
|
|
620
|
+
: 'linear'
|
|
621
|
+
: undefined;
|
|
622
|
+
return getWgpuSampler(state, filter, sampler.wrapU, sampler.wrapV, mipmapFilter, sampler.anisotropy);
|
|
623
|
+
}
|
|
624
|
+
// Produces the distinct HAS_SKIN vertex variant. The palette stays in group(1), beside Draw, so lit
|
|
625
|
+
// pipelines still fit WebGPU's minimum four bind groups. Rigid source carries neither skin attributes
|
|
626
|
+
// nor the palette binding.
|
|
627
|
+
export function getWgpuMeshPreludeWgsl(skinned, skinning = null) {
|
|
628
|
+
return skinned && skinning !== null ? skinning.extendMeshPrelude(WGPU_MESH_PRELUDE_WGSL) : WGPU_MESH_PRELUDE_WGSL;
|
|
629
|
+
}
|
|
630
|
+
// Whether a cached material binding must rebuild its GPUBindGroup because a freshly-resolved view or the
|
|
631
|
+
// primary sampler no longer matches what it was built from. `resolveWgpuMaterialTextureView` is the
|
|
632
|
+
// invalidation seam: a map swap, an unready→ready transition, a ready→ready image replacement, or an
|
|
633
|
+
// ImageResource version bump each yield a different view identity, so identity comparison alone catches
|
|
634
|
+
// every live material-map mutation with no parallel epoch bookkeeping. `sampler` is the shared primary
|
|
635
|
+
// sampler (getWgpuMaterialSampler of the diffuse/base-color map), so a change to the primary map's
|
|
636
|
+
// sampler trips a rebuild; a non-primary map's per-Texture sampler is not bound and does not (the
|
|
637
|
+
// shared-primary-sampler contract). A binding with no cached views always rebuilds. Allocation-free —
|
|
638
|
+
// callers pass a reused scratch as `views`.
|
|
639
|
+
export function isWgpuMaterialBindGroupRebuildNeeded(binding, sampler, views) {
|
|
640
|
+
if (binding.sampler !== sampler)
|
|
641
|
+
return true;
|
|
642
|
+
const cached = binding.views;
|
|
643
|
+
if (cached === undefined || cached.length !== views.length)
|
|
644
|
+
return true;
|
|
645
|
+
for (let i = 0; i < views.length; i++) {
|
|
646
|
+
if (cached[i] !== views[i])
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
// True when a material map texture is present AND carries GPU-uploadable pixels — an element or a data-only
|
|
652
|
+
// generated Surface. Families call this to decide the `has*Map` define flag — the textured pipeline variant
|
|
653
|
+
// compiles only when the map can actually be sampled, so an empty texture renders the untextured path.
|
|
654
|
+
// Mirrors the `map !== null && map.image !== null && hasImageResourcePixels(map.image)` guard in the GL renderers.
|
|
655
|
+
export function isWgpuTextureReady(texture) {
|
|
656
|
+
return texture !== null && texture.image !== null && hasImageResourcePixels(texture.image);
|
|
657
|
+
}
|
|
658
|
+
// Resolves the GPUTextureView a family binds into a material map slot: the real uploaded view when the
|
|
659
|
+
// texture carries pixels (cached per state by render-wgpu's resource texture cache), otherwise the shared
|
|
660
|
+
// 1x1 opaque-white placeholder so the bind-group layout's texture slot is always satisfied. The single
|
|
661
|
+
// texture-resolution seam every scene-wgpu family routes its maps through — the WGSL mirror of scene-gl's
|
|
662
|
+
// `bindGlImageResourceTexture(state, map.image)` / unbound-attribute fallback.
|
|
663
|
+
export function resolveWgpuMaterialTextureView(state, texture) {
|
|
664
|
+
if (texture !== null && texture.image !== null && hasImageResourcePixels(texture.image)) {
|
|
665
|
+
// Request a mip chain when the map's sampler asks for mipmaps, so getWgpuMaterialSampler's mip
|
|
666
|
+
// filter has levels to sample; the shared placeholder and 2D path stay single-level.
|
|
667
|
+
return bindWgpuImageResourceTexture(state, texture.image, texture.sampler.mipmaps).view;
|
|
668
|
+
}
|
|
669
|
+
return ensureWgpuPlaceholderTextureView(state);
|
|
670
|
+
}
|
|
671
|
+
// Stashes a material's primary-texture uv transform for the next writeWgpuDrawUniform to fold into the
|
|
672
|
+
// shared Draw uniform. A family's bind() calls this with its base/diffuse map; writeWgpuDrawUniform
|
|
673
|
+
// consumes and resets the stash so a following draw whose family does not stash gets the untiled uv.
|
|
674
|
+
// @flighthq/texture composes the KHR transform column-major, the layout WGSL reads, matching the CPU
|
|
675
|
+
// transformTextureUv reference. A null / identity / unbound texture leaves the stash at identity (the
|
|
676
|
+
// vs_main multiply then reproduces the raw uv).
|
|
677
|
+
export function stashWgpuUvTransform(state, texture) {
|
|
678
|
+
const out = getWgpuScene3DRuntime(state).pendingUvTransform;
|
|
679
|
+
if (texture === null || ('image' in texture && texture.image === null) || !hasTextureUvTransform(texture)) {
|
|
680
|
+
resetWgpuUvTransformStash(out);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
getTextureUvMatrix(scratchUvMatrix, texture);
|
|
684
|
+
const m = scratchUvMatrix.m;
|
|
685
|
+
for (let i = 0; i < 9; i++)
|
|
686
|
+
out[i] = m[i];
|
|
687
|
+
}
|
|
688
|
+
export function wgpuPerMapMaterialBindGroupNeedsRebuild(binding, samplers, views) {
|
|
689
|
+
if (samplers.length !== views.length)
|
|
690
|
+
return true;
|
|
691
|
+
const cachedSamplers = binding.samplers;
|
|
692
|
+
const cachedViews = binding.views;
|
|
693
|
+
if (cachedSamplers === undefined ||
|
|
694
|
+
cachedViews === undefined ||
|
|
695
|
+
cachedSamplers.length !== samplers.length ||
|
|
696
|
+
cachedViews.length !== views.length) {
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
for (let i = 0; i < views.length; i++) {
|
|
700
|
+
if (cachedSamplers[i] !== samplers[i] || cachedViews[i] !== views[i])
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
function overwriteIdentityCache(binding, key, values) {
|
|
706
|
+
const cached = binding[key];
|
|
707
|
+
if (cached === undefined || cached.length !== values.length) {
|
|
708
|
+
binding[key] = values.slice();
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
for (let i = 0; i < values.length; i++)
|
|
712
|
+
cached[i] = values[i];
|
|
713
|
+
}
|
|
714
|
+
// Allocates a draw slot from the render-state's uniform ring buffer, writes the Draw uniform (world
|
|
715
|
+
// mat4x4f + normal mat3x3f padded to std140) into it, records the slot's byte offset on the scene
|
|
716
|
+
// runtime (the draw path passes it as the bind group's dynamic offset), and returns the shared
|
|
717
|
+
// dynamic-offset Draw bind group. Reusing the render-state ring keeps each subset draw to one ring
|
|
718
|
+
// slot, not a fresh buffer; submitWgpuRenderPass uploads the used ring region before submit. Mirrors
|
|
719
|
+
// the per-draw model/normal upload in scene-gl's drawGlMeshSubset.
|
|
720
|
+
export function writeWgpuDrawUniform(state, proxy) {
|
|
721
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
722
|
+
const stateRuntime = getWgpuRenderStateRuntime(state);
|
|
723
|
+
if (scene.drawBindGroup === null) {
|
|
724
|
+
scene.drawBindGroup = state.device.createBindGroup({
|
|
725
|
+
layout: ensureWgpuScene3DLayouts(state).drawBindGroupLayout,
|
|
726
|
+
entries: [{ binding: 0, resource: { buffer: stateRuntime.uniformBuffer, size: DRAW_UNIFORM_BYTES } }],
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
const offset = stateRuntime.uniformOffset;
|
|
730
|
+
const floatOffset = offset / 4;
|
|
731
|
+
const u = stateRuntime.uniformData;
|
|
732
|
+
const world = proxy.worldMatrix.m;
|
|
733
|
+
for (let i = 0; i < 16; i++)
|
|
734
|
+
u[floatOffset + i] = world[i];
|
|
735
|
+
// mat3x3f normal matrix: three vec3 columns each padded to vec4 (std140) → floats 16..27.
|
|
736
|
+
const n = proxy.normalMatrix.m;
|
|
737
|
+
u[floatOffset + 16] = n[0];
|
|
738
|
+
u[floatOffset + 17] = n[1];
|
|
739
|
+
u[floatOffset + 18] = n[2];
|
|
740
|
+
u[floatOffset + 19] = 0;
|
|
741
|
+
u[floatOffset + 20] = n[3];
|
|
742
|
+
u[floatOffset + 21] = n[4];
|
|
743
|
+
u[floatOffset + 22] = n[5];
|
|
744
|
+
u[floatOffset + 23] = 0;
|
|
745
|
+
u[floatOffset + 24] = n[6];
|
|
746
|
+
u[floatOffset + 25] = n[7];
|
|
747
|
+
u[floatOffset + 26] = n[8];
|
|
748
|
+
u[floatOffset + 27] = 0;
|
|
749
|
+
// mat3x3f uv transform: three vec3 columns each padded to vec4 (std140) → floats 28..39. The stash
|
|
750
|
+
// (set by a family's bind() via stashWgpuUvTransform) is already column-major and PERSISTS across
|
|
751
|
+
// draws — read, never reset here. drawWgpuScene3D binds once per material then draws many meshes, so the
|
|
752
|
+
// transform must survive every draw under one bind, mirroring the persistent GL u_uvTransform uniform.
|
|
753
|
+
// Every family's bind stashes authoritatively (its map, or identity for non-texturing families), so
|
|
754
|
+
// switching materials always re-establishes the correct value and no stale transform leaks forward.
|
|
755
|
+
const uv = scene.pendingUvTransform;
|
|
756
|
+
u[floatOffset + 28] = uv[0];
|
|
757
|
+
u[floatOffset + 29] = uv[1];
|
|
758
|
+
u[floatOffset + 30] = uv[2];
|
|
759
|
+
u[floatOffset + 31] = 0;
|
|
760
|
+
u[floatOffset + 32] = uv[3];
|
|
761
|
+
u[floatOffset + 33] = uv[4];
|
|
762
|
+
u[floatOffset + 34] = uv[5];
|
|
763
|
+
u[floatOffset + 35] = 0;
|
|
764
|
+
u[floatOffset + 36] = uv[6];
|
|
765
|
+
u[floatOffset + 37] = uv[7];
|
|
766
|
+
u[floatOffset + 38] = uv[8];
|
|
767
|
+
u[floatOffset + 39] = 0;
|
|
768
|
+
u[floatOffset + 40] = proxy.alpha ?? 1;
|
|
769
|
+
u[floatOffset + 41] = 0;
|
|
770
|
+
u[floatOffset + 42] = 0;
|
|
771
|
+
u[floatOffset + 43] = 0;
|
|
772
|
+
scene.pendingDrawOffset = offset;
|
|
773
|
+
stateRuntime.uniformOffset += stateRuntime.uniformStride;
|
|
774
|
+
return scene.drawBindGroup;
|
|
775
|
+
}
|
|
776
|
+
// The shared WGSL prelude every family module prepends after its const-flag block: the Frame + Draw
|
|
777
|
+
// uniform structs and their group(0)/group(1) bindings, the VertexOutput, the vs_main entry, and the
|
|
778
|
+
// srgbToLinear helper. A family appends its own group(2) Material block + fs_main. Keeping the Frame/
|
|
779
|
+
// Draw structs here keeps them in lockstep with writeWgpuFrameUniform / writeWgpuDrawUniform. Mirrors
|
|
780
|
+
// scene-gl's shared vertex body + GL_MESH_LIGHT_BLOCK_GLSL.
|
|
781
|
+
export const WGPU_MESH_PRELUDE_WGSL = /* wgsl */ `
|
|
782
|
+
const PI : f32 = 3.14159265359;
|
|
783
|
+
const MAX_FORWARD_LIGHTS : u32 = 4u;
|
|
784
|
+
|
|
785
|
+
struct Frame {
|
|
786
|
+
viewProjection : mat4x4f,
|
|
787
|
+
cameraPosition : vec4f,
|
|
788
|
+
lightDirection : vec4f, // xyz = directional light travel direction; w = directionalCount
|
|
789
|
+
directionalRadiance : vec4f, // rgb = linear premultiplied radiance
|
|
790
|
+
ambientRadiance : vec4f, // rgb = linear premultiplied radiance; w = ambientCount
|
|
791
|
+
view : mat4x4f, // camera view matrix; rotates world normals into view space (matcap)
|
|
792
|
+
// Punctual light arrays — layout mirrors Scene3DLightBlock.data (packScene3DLightBlock).
|
|
793
|
+
// point[i] = pointLights[i*2+0]={pos.xyz,range}, [i*2+1]={radiance.rgb,invSqrRange}
|
|
794
|
+
// spot[i] = spotLights[i*4+0..1] as point, [i*4+2]={dir.xyz,_}, [i*4+3]={cosInner,cosOuter,_,_}
|
|
795
|
+
// hemisphere[i] = hemisphereLights[i*3+0]={sky.rgb,_}, [i*3+1]={ground.rgb,_}, [i*3+2]={up.xyz,_}
|
|
796
|
+
pointLights : array<vec4f, 8>, // MAX_FORWARD_LIGHTS * 2
|
|
797
|
+
spotLights : array<vec4f, 16>, // MAX_FORWARD_LIGHTS * 4
|
|
798
|
+
hemisphereLights : array<vec4f, 12>, // MAX_FORWARD_LIGHTS * 3
|
|
799
|
+
punctualCounts : vec4f, // x = pointCount, y = spotCount, z = hemisphereCount
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
struct Draw {
|
|
803
|
+
world : mat4x4f,
|
|
804
|
+
normalMatrix : mat3x3f,
|
|
805
|
+
uvTransform : mat3x3f, // KHR_texture_transform of the material's primary map (identity when unused)
|
|
806
|
+
params : vec4f, // x = resolved object alpha
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
@group(0) @binding(0) var<uniform> frame : Frame;
|
|
810
|
+
@group(1) @binding(0) var<uniform> draw : Draw;
|
|
811
|
+
|
|
812
|
+
struct VertexOutput {
|
|
813
|
+
@builtin(position) clipPosition : vec4f,
|
|
814
|
+
@location(0) worldPosition : vec3f,
|
|
815
|
+
@location(1) worldNormal : vec3f,
|
|
816
|
+
@location(2) worldTangent : vec4f,
|
|
817
|
+
@location(3) uv : vec2f,
|
|
818
|
+
@location(4) @interpolate(flat) objectAlpha : f32,
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
@vertex fn vs_main(
|
|
822
|
+
@location(0) position : vec3f,
|
|
823
|
+
@location(1) normal : vec3f,
|
|
824
|
+
@location(2) tangent : vec4f,
|
|
825
|
+
@location(3) uv : vec2f,
|
|
826
|
+
) -> VertexOutput {
|
|
827
|
+
var out : VertexOutput;
|
|
828
|
+
var localPosition = vec4f(position, 1.0);
|
|
829
|
+
var localNormal = normal;
|
|
830
|
+
var localTangent = tangent.xyz;
|
|
831
|
+
let world = draw.world * localPosition;
|
|
832
|
+
out.worldPosition = world.xyz;
|
|
833
|
+
out.clipPosition = frame.viewProjection * world;
|
|
834
|
+
out.worldNormal = draw.normalMatrix * localNormal;
|
|
835
|
+
out.worldTangent = vec4f(draw.normalMatrix * localTangent, tangent.w);
|
|
836
|
+
// Apply the material's KHR_texture_transform to the uv. draw.uvTransform is identity for an untiled
|
|
837
|
+
// material (writeWgpuDrawUniform's default), so this is a no-op there — applied unconditionally rather
|
|
838
|
+
// than behind a pipeline const because this vs_main is shared by every family (classic/unlit/toon/
|
|
839
|
+
// matcap/debug/wireframe) and a const would have to thread through all of them; a per-vertex mat3
|
|
840
|
+
// multiply is negligible. The scene-gl mirror gates the equivalent branch via its #ifdef variant.
|
|
841
|
+
out.uv = (draw.uvTransform * vec3f(uv, 1.0)).xy;
|
|
842
|
+
out.objectAlpha = draw.params.x;
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// sRgb albedo texels are gamma-encoded; decode to linear before lighting.
|
|
847
|
+
fn srgbToLinear(c : vec3f) -> vec3f {
|
|
848
|
+
let lo = c / 12.92;
|
|
849
|
+
let hi = pow((c + vec3f(0.055)) / 1.055, vec3f(2.4));
|
|
850
|
+
return select(lo, hi, c > vec3f(0.04045));
|
|
851
|
+
}
|
|
852
|
+
`;
|
|
853
|
+
// Writes the per-frame Frame uniform (camera view-projection + world position + the packed light
|
|
854
|
+
// block) into the scene runtime's Frame buffer and ensures the Frame bind group exists. The light
|
|
855
|
+
// block layout matches Scene3DLightBlock.data: directional { direction.xyz @0, radiance.rgb @4 } then
|
|
856
|
+
// ambient { radiance.rgb @8 }; the presence counts go into the lightDirection.w / ambientRadiance.w
|
|
857
|
+
// lanes the shader branches on. Camera3D world position is the translation of the inverse view matrix.
|
|
858
|
+
// Punctual light arrays (point/spot/hemisphere) follow the camera view matrix, mirroring the packed
|
|
859
|
+
// layout from Scene3DLightBlock.data; a final vec4f carries the three punctual counts. Shared by every
|
|
860
|
+
// family — lighting-independent families simply ignore the light lanes.
|
|
861
|
+
export function writeWgpuFrameUniform(state, camera, lights) {
|
|
862
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
863
|
+
let binding = scene.frameBindings.get(lights);
|
|
864
|
+
if (binding === undefined) {
|
|
865
|
+
const buffer = state.device.createBuffer({
|
|
866
|
+
size: FRAME_UNIFORM_BYTES,
|
|
867
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
868
|
+
});
|
|
869
|
+
const bindGroup = state.device.createBindGroup({
|
|
870
|
+
layout: ensureWgpuScene3DLayouts(state).frameBindGroupLayout,
|
|
871
|
+
entries: [{ binding: 0, resource: { buffer } }],
|
|
872
|
+
});
|
|
873
|
+
binding = { bindGroup, buffer };
|
|
874
|
+
scene.frameBindings.set(lights, binding);
|
|
875
|
+
}
|
|
876
|
+
// beginWgpuMeshDraw binds these current aliases immediately after this write. Distinct light blocks
|
|
877
|
+
// retain distinct buffers until command submission, so later binds cannot rewrite earlier draws.
|
|
878
|
+
scene.frameBuffer = binding.buffer;
|
|
879
|
+
scene.frameBindGroup = binding.bindGroup;
|
|
880
|
+
const f = _frameScratch;
|
|
881
|
+
const aspect = camera.projection.kind === 'perspective' ? camera.projection.aspect : 1;
|
|
882
|
+
getCamera3DViewProjectionMatrix4(scratchViewProjection, camera, aspect !== 0 ? aspect : 1);
|
|
883
|
+
// Camera projection matrices use OpenGL's [-1, 1] NDC-Z convention. WebGPU clips Z to [0, 1],
|
|
884
|
+
// so left-multiply the view-projection by the depth correction z' = 0.5z + 0.5w. In column-major
|
|
885
|
+
// storage this replaces the Z row while preserving X, Y, and W. Shadow-map WGSL applies its own
|
|
886
|
+
// equivalent correction and deliberately does not consume this frame uniform.
|
|
887
|
+
const sourceVp = scratchViewProjection.m;
|
|
888
|
+
const webGpuVp = scratchWebGpuViewProjection.m;
|
|
889
|
+
webGpuVp.set(sourceVp);
|
|
890
|
+
webGpuVp[2] = 0.5 * (sourceVp[2] + sourceVp[3]);
|
|
891
|
+
webGpuVp[6] = 0.5 * (sourceVp[6] + sourceVp[7]);
|
|
892
|
+
webGpuVp[10] = 0.5 * (sourceVp[10] + sourceVp[11]);
|
|
893
|
+
webGpuVp[14] = 0.5 * (sourceVp[14] + sourceVp[15]);
|
|
894
|
+
const vp = webGpuVp;
|
|
895
|
+
for (let i = 0; i < 16; i++)
|
|
896
|
+
f[i] = vp[i];
|
|
897
|
+
inverseMatrix4(scratchInverseView, camera.view);
|
|
898
|
+
getMatrix4Position(scratchCameraPosition, scratchInverseView);
|
|
899
|
+
f[16] = scratchCameraPosition.x;
|
|
900
|
+
f[17] = scratchCameraPosition.y;
|
|
901
|
+
f[18] = scratchCameraPosition.z;
|
|
902
|
+
f[19] = 0;
|
|
903
|
+
const data = lights.data;
|
|
904
|
+
f[20] = data[0];
|
|
905
|
+
f[21] = data[1];
|
|
906
|
+
f[22] = data[2];
|
|
907
|
+
f[23] = lights.directionalCount;
|
|
908
|
+
f[24] = data[4];
|
|
909
|
+
f[25] = data[5];
|
|
910
|
+
f[26] = data[6];
|
|
911
|
+
f[27] = 0;
|
|
912
|
+
f[28] = data[8];
|
|
913
|
+
f[29] = data[9];
|
|
914
|
+
f[30] = data[10];
|
|
915
|
+
f[31] = lights.ambientCount;
|
|
916
|
+
// Camera3D view matrix (floats 32..47): used by matcap to rotate the world-space normal into view
|
|
917
|
+
// space. Lighting-independent families ignore these lanes; the cost is one extra mat4 per frame.
|
|
918
|
+
const view = camera.view.m;
|
|
919
|
+
for (let i = 0; i < 16; i++)
|
|
920
|
+
f[32 + i] = view[i];
|
|
921
|
+
// Punctual light arrays (floats 48..) — the point/spot/hemisphere slices from Scene3DLightBlock.data
|
|
922
|
+
// (identical packed layout), followed by a counts vec4f. Families that shade punctual lights read
|
|
923
|
+
// these; others simply ignore the trailing data.
|
|
924
|
+
const pointFloats = SCENE_LIGHT_POINT_STRIDE * MAX_FORWARD_LIGHTS;
|
|
925
|
+
for (let i = 0; i < pointFloats; i++)
|
|
926
|
+
f[FRAME_POINT_OFFSET + i] = data[SCENE_LIGHT_POINT_OFFSET + i];
|
|
927
|
+
const spotFloats = SCENE_LIGHT_SPOT_STRIDE * MAX_FORWARD_LIGHTS;
|
|
928
|
+
for (let i = 0; i < spotFloats; i++)
|
|
929
|
+
f[FRAME_SPOT_OFFSET + i] = data[SCENE_LIGHT_SPOT_OFFSET + i];
|
|
930
|
+
const hemisphereFloats = SCENE_LIGHT_HEMISPHERE_STRIDE * MAX_FORWARD_LIGHTS;
|
|
931
|
+
for (let i = 0; i < hemisphereFloats; i++)
|
|
932
|
+
f[FRAME_HEMISPHERE_OFFSET + i] = data[SCENE_LIGHT_HEMISPHERE_OFFSET + i];
|
|
933
|
+
f[FRAME_COUNTS_OFFSET] = lights.pointCount;
|
|
934
|
+
f[FRAME_COUNTS_OFFSET + 1] = lights.spotCount;
|
|
935
|
+
f[FRAME_COUNTS_OFFSET + 2] = lights.hemisphereCount;
|
|
936
|
+
f[FRAME_COUNTS_OFFSET + 3] = 0;
|
|
937
|
+
state.device.queue.writeBuffer(binding.buffer, 0, f.buffer, 0, FRAME_UNIFORM_BYTES);
|
|
938
|
+
}
|
|
939
|
+
// Frame uniform float offsets for the punctual light arrays — the byte offset within the Frame buffer
|
|
940
|
+
// where each punctual array begins, used by writeWgpuFrameUniform to copy the packed data from
|
|
941
|
+
// Scene3DLightBlock.data into the right Frame buffer position. All offsets in FLOATS (multiply by 4
|
|
942
|
+
// for bytes). The head block (viewProjection + cameraPosition + directional + ambient + view) is 48
|
|
943
|
+
// floats, followed by point → spot → hemisphere → counts.
|
|
944
|
+
const FRAME_POINT_OFFSET = 48;
|
|
945
|
+
const FRAME_SPOT_OFFSET = FRAME_POINT_OFFSET + SCENE_LIGHT_POINT_STRIDE * MAX_FORWARD_LIGHTS;
|
|
946
|
+
const FRAME_HEMISPHERE_OFFSET = FRAME_SPOT_OFFSET + SCENE_LIGHT_SPOT_STRIDE * MAX_FORWARD_LIGHTS;
|
|
947
|
+
const FRAME_COUNTS_OFFSET = FRAME_HEMISPHERE_OFFSET + SCENE_LIGHT_HEMISPHERE_STRIDE * MAX_FORWARD_LIGHTS;
|
|
948
|
+
// Frame uniform: mat4x4f viewProjection (64) + vec4f cameraPosition (16) + vec4f lightDirection (16)
|
|
949
|
+
// + vec4f directionalRadiance (16) + vec4f ambientRadiance (16) + mat4x4f view (64) + point lights
|
|
950
|
+
// (8 * 16 = 128) + spot lights (16 * 16 = 256) + hemisphere lights (12 * 16 = 192) + counts vec4f
|
|
951
|
+
// (16) = 784 bytes / 196 floats.
|
|
952
|
+
const FRAME_UNIFORM_BYTES = (FRAME_COUNTS_OFFSET + 4) * 4;
|
|
953
|
+
// Draw uniform: mat4x4f world (64) + mat3x3f normalMatrix as 3 padded vec4 (48) + mat3x3f uvTransform as
|
|
954
|
+
// 3 padded vec4 (48) + vec4f params (16, x = resolved object alpha) = 176; the ring buffer rounds the
|
|
955
|
+
// per-slot stride up to the device's minUniformBufferOffsetAlignment.
|
|
956
|
+
const DRAW_UNIFORM_BYTES = 176;
|
|
957
|
+
// Writes the column-major identity mat3 into a uv-transform stash buffer, the untiled default.
|
|
958
|
+
function resetWgpuUvTransformStash(out) {
|
|
959
|
+
out[0] = 1;
|
|
960
|
+
out[1] = 0;
|
|
961
|
+
out[2] = 0;
|
|
962
|
+
out[3] = 0;
|
|
963
|
+
out[4] = 1;
|
|
964
|
+
out[5] = 0;
|
|
965
|
+
out[6] = 0;
|
|
966
|
+
out[7] = 0;
|
|
967
|
+
out[8] = 1;
|
|
968
|
+
}
|
|
969
|
+
// Row-major uv matrix composed per stash, transposed into the column-major pendingUvTransform buffer.
|
|
970
|
+
const scratchUvMatrix = createMatrix3();
|
|
971
|
+
// The depth-stencil format the scene pass uses, matching render-wgpu's main-canvas / effect-target
|
|
972
|
+
// depth attachment.
|
|
973
|
+
const DEPTH_STENCIL_FORMAT = 'depth24plus-stencil8';
|
|
974
|
+
// The sampleable depth format the directional shadow map (and its 1x1 no-shadow dummy) use. depth32float
|
|
975
|
+
// is bindable as a texture_depth_2d for the lit PCF comparison; drawWgpuScene3DShadowMap renders into it.
|
|
976
|
+
export const SHADOW_DEPTH_FORMAT = 'depth32float';
|
|
977
|
+
// Shadow-sample uniform: mat4x4f light matrix (64) + vec4f params (16, x = enabled) = 80 bytes / 20 floats.
|
|
978
|
+
const SHADOW_SAMPLE_UNIFORM_BYTES = 80;
|
|
979
|
+
// IBL-sample uniform: vec4f params (16, x = enabled, y = intensity, z = maxMip) = 16 bytes / 4 floats.
|
|
980
|
+
const IBL_SAMPLE_UNIFORM_BYTES = 16;
|
|
981
|
+
// The 1x1 no-IBL dummy cube + LUT format. A plain filterable 8-bit format satisfies the IBL `float`
|
|
982
|
+
// texture slots (the dummies are never sampled — the IBL uniform's enabled flag gates them off).
|
|
983
|
+
const IBL_DUMMY_FORMAT = 'rgba8unorm';
|
|
984
|
+
// Opaque-white 1x1 RGBA pixel for the shared placeholder map texture (untextured path).
|
|
985
|
+
const WHITE_PIXEL = new Uint8Array([255, 255, 255, 255]);
|
|
986
|
+
// The canonical interleaved 48-byte PBR vertex: position(float32x3) @0, normal(float32x3) @12,
|
|
987
|
+
// tangent(float32x4) @24, uv0(float32x2) @40. Matches the @location slots in the WGSL vs_main.
|
|
988
|
+
const VERTEX_BUFFER_LAYOUTS = [
|
|
989
|
+
{
|
|
990
|
+
arrayStride: 48,
|
|
991
|
+
attributes: [
|
|
992
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
|
|
993
|
+
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
|
|
994
|
+
{ shaderLocation: 2, offset: 24, format: 'float32x4' },
|
|
995
|
+
{ shaderLocation: 3, offset: 40, format: 'float32x2' },
|
|
996
|
+
],
|
|
997
|
+
},
|
|
998
|
+
];
|
|
999
|
+
const scratchViewProjection = createMatrix4();
|
|
1000
|
+
const scratchWebGpuViewProjection = createMatrix4();
|
|
1001
|
+
const scratchInverseView = createMatrix4();
|
|
1002
|
+
const scratchCameraPosition = { x: 0, y: 0, z: 0 };
|
|
1003
|
+
const _frameScratch = new Float32Array(FRAME_UNIFORM_BYTES / 4);
|
|
1004
|
+
const _dynamicOffsets = new Uint32Array(1);
|
|
1005
|
+
const _shadowSampleScratch = new Float32Array(SHADOW_SAMPLE_UNIFORM_BYTES / 4);
|
|
1006
|
+
const _iblSampleScratch = new Float32Array(IBL_SAMPLE_UNIFORM_BYTES / 4);
|
|
1007
|
+
//# sourceMappingURL=wgpuMeshPipeline.js.map
|