@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,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CANONICAL_SKINNED_MESH_GEOMETRY_LAYOUT,
|
|
3
|
+
createBoxMeshGeometry,
|
|
4
|
+
createMeshGeometry,
|
|
5
|
+
setMeshGeometrySkinBindPose,
|
|
6
|
+
updateMeshMorph,
|
|
7
|
+
} from '@flighthq/mesh';
|
|
8
|
+
import { createMesh } from '@flighthq/scene3d';
|
|
9
|
+
import type { MeshGeometryRuntime, MeshMorph, VertexAttributeLayout } from '@flighthq/types';
|
|
10
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
11
|
+
|
|
12
|
+
import { ensureWgpuMeshUpload } from './wgpuMeshUpload';
|
|
13
|
+
import { getWgpuScene3DRuntime } from './wgpuScene3DRuntime';
|
|
14
|
+
import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
|
|
15
|
+
import { registerWgpuGpuSkinning } from './wgpuSkinPalette';
|
|
16
|
+
|
|
17
|
+
const POSITION_LAYOUT: VertexAttributeLayout = {
|
|
18
|
+
attributes: [{ byteOffset: 0, format: 'float32x3', semantic: 'position' }],
|
|
19
|
+
stride: 12,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function lastUploadedVertices(calls: readonly { name: string; args: readonly unknown[] }[]): Float32Array {
|
|
23
|
+
const writes = calls.filter((call) => call.name === 'writeBuffer');
|
|
24
|
+
const write = writes[writes.length - 2]!;
|
|
25
|
+
return new Float32Array(
|
|
26
|
+
write.args[2] as ArrayBuffer,
|
|
27
|
+
write.args[3] as number,
|
|
28
|
+
(write.args[4] as number) / Float32Array.BYTES_PER_ELEMENT,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('ensureWgpuMeshUpload', () => {
|
|
33
|
+
it('uploads vertex + index buffers and caches by geometry', () => {
|
|
34
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
35
|
+
const geometry = createBoxMeshGeometry();
|
|
36
|
+
const upload = ensureWgpuMeshUpload(state, geometry);
|
|
37
|
+
|
|
38
|
+
expect(upload).not.toBeNull();
|
|
39
|
+
expect(upload!.vertexBuffer).toBeDefined();
|
|
40
|
+
expect(upload!.indexBuffer).toBeDefined();
|
|
41
|
+
expect(upload!.indexCount).toBe(geometry.indices!.length);
|
|
42
|
+
expect(upload!.version).toBe(geometry.version);
|
|
43
|
+
expect(fake.calls.filter((c) => c.name === 'writeBuffer').length).toBeGreaterThanOrEqual(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns the cached upload without re-uploading when version is unchanged', () => {
|
|
47
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
48
|
+
const geometry = createBoxMeshGeometry();
|
|
49
|
+
const first = ensureWgpuMeshUpload(state, geometry);
|
|
50
|
+
const writesAfterFirst = fake.calls.filter((c) => c.name === 'writeBuffer').length;
|
|
51
|
+
const second = ensureWgpuMeshUpload(state, geometry);
|
|
52
|
+
expect(second).toBe(first);
|
|
53
|
+
expect(fake.calls.filter((c) => c.name === 'writeBuffer').length).toBe(writesAfterFirst);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('re-uploads when geometry.version changes', () => {
|
|
57
|
+
const { state } = makeWgpuScene3DState();
|
|
58
|
+
const geometry = createBoxMeshGeometry();
|
|
59
|
+
const first = ensureWgpuMeshUpload(state, geometry);
|
|
60
|
+
geometry.version++;
|
|
61
|
+
const second = ensureWgpuMeshUpload(state, geometry);
|
|
62
|
+
expect(second).not.toBe(first);
|
|
63
|
+
expect(second!.version).toBe(geometry.version);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('uploads captured bind-pose positions for a GPU-skinned geometry', () => {
|
|
67
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
68
|
+
registerWgpuGpuSkinning(state);
|
|
69
|
+
const vertices = new Float32Array(20);
|
|
70
|
+
vertices[0] = 9;
|
|
71
|
+
const geometry = createMeshGeometry({
|
|
72
|
+
indices: new Uint16Array([0, 0, 0]),
|
|
73
|
+
layout: CANONICAL_SKINNED_MESH_GEOMETRY_LAYOUT,
|
|
74
|
+
vertices,
|
|
75
|
+
});
|
|
76
|
+
setMeshGeometrySkinBindPose(geometry, {
|
|
77
|
+
joints: new Float32Array([0, 0, 0, 0]),
|
|
78
|
+
normals: new Float32Array([0, 1, 0]),
|
|
79
|
+
positions: new Float32Array([1, 2, 3]),
|
|
80
|
+
skinnedNormals: new Float32Array(3),
|
|
81
|
+
skinnedPositions: new Float32Array(3),
|
|
82
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const upload = ensureWgpuMeshUpload(state, geometry, true);
|
|
86
|
+
const write = fake.calls.find((call) => call.name === 'writeBuffer')!;
|
|
87
|
+
const uploadedVertices = new Float32Array(
|
|
88
|
+
write.args[2] as ArrayBuffer,
|
|
89
|
+
write.args[3] as number,
|
|
90
|
+
(write.args[4] as number) / 4,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(upload?.skinBindUploaded).toBe(true);
|
|
94
|
+
expect(Array.from(uploadedVertices.slice(0, 3))).toEqual([1, 2, 3]);
|
|
95
|
+
expect(geometry.vertices[0]).toBe(9);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('re-uploads morph-blended vertices as weights change', () => {
|
|
99
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
100
|
+
const geometry = createMeshGeometry({
|
|
101
|
+
indices: new Uint16Array([0, 0, 0]),
|
|
102
|
+
layout: POSITION_LAYOUT,
|
|
103
|
+
vertices: new Float32Array([0, 0, 0]),
|
|
104
|
+
});
|
|
105
|
+
const morph: MeshMorph = {
|
|
106
|
+
targets: [{ normalDeltas: null, positionDeltas: new Float32Array([4, 0, 0]), tangentDeltas: null }],
|
|
107
|
+
weights: new Float32Array([1]),
|
|
108
|
+
};
|
|
109
|
+
const mesh = createMesh(geometry, []);
|
|
110
|
+
mesh.morph = morph;
|
|
111
|
+
|
|
112
|
+
updateMeshMorph(mesh);
|
|
113
|
+
ensureWgpuMeshUpload(state, geometry);
|
|
114
|
+
expect(lastUploadedVertices(fake.calls)[0]).toBeCloseTo(4);
|
|
115
|
+
|
|
116
|
+
morph.weights[0] = 0.5;
|
|
117
|
+
updateMeshMorph(mesh);
|
|
118
|
+
ensureWgpuMeshUpload(state, geometry);
|
|
119
|
+
expect(lastUploadedVertices(fake.calls)[0]).toBeCloseTo(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('uploads current morphed vertices instead of a frozen skin bind pose for GPU skinning', () => {
|
|
123
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
124
|
+
registerWgpuGpuSkinning(state);
|
|
125
|
+
const geometry = createMeshGeometry({
|
|
126
|
+
indices: new Uint16Array([0, 0, 0]),
|
|
127
|
+
layout: CANONICAL_SKINNED_MESH_GEOMETRY_LAYOUT,
|
|
128
|
+
vertices: new Float32Array(20),
|
|
129
|
+
});
|
|
130
|
+
geometry.vertices[0] = 1;
|
|
131
|
+
setMeshGeometrySkinBindPose(geometry, {
|
|
132
|
+
joints: new Float32Array([0, 0, 0, 0]),
|
|
133
|
+
normals: new Float32Array([0, 1, 0]),
|
|
134
|
+
positions: new Float32Array([1, 0, 0]),
|
|
135
|
+
skinnedNormals: new Float32Array(3),
|
|
136
|
+
skinnedPositions: new Float32Array(3),
|
|
137
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
138
|
+
});
|
|
139
|
+
const morph: MeshMorph = {
|
|
140
|
+
targets: [{ normalDeltas: null, positionDeltas: new Float32Array([4, 0, 0]), tangentDeltas: null }],
|
|
141
|
+
weights: new Float32Array([1]),
|
|
142
|
+
};
|
|
143
|
+
const mesh = createMesh(geometry, []);
|
|
144
|
+
mesh.morph = morph;
|
|
145
|
+
|
|
146
|
+
updateMeshMorph(mesh);
|
|
147
|
+
ensureWgpuMeshUpload(state, geometry, true);
|
|
148
|
+
expect(lastUploadedVertices(fake.calls)[0]).toBeCloseTo(5);
|
|
149
|
+
|
|
150
|
+
morph.weights[0] = 2;
|
|
151
|
+
updateMeshMorph(mesh);
|
|
152
|
+
ensureWgpuMeshUpload(state, geometry, true);
|
|
153
|
+
expect(lastUploadedVertices(fake.calls)[0]).toBeCloseTo(9);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('mirrors the upload onto MeshGeometryRuntime.webgpuData', () => {
|
|
157
|
+
const { state } = makeWgpuScene3DState();
|
|
158
|
+
const geometry = createBoxMeshGeometry();
|
|
159
|
+
const upload = ensureWgpuMeshUpload(state, geometry);
|
|
160
|
+
const meshRuntime = geometry[EntityRuntimeKey] as MeshGeometryRuntime;
|
|
161
|
+
expect(meshRuntime.webgpuData as unknown).toBe(upload);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns null for non-indexed geometry', () => {
|
|
165
|
+
const { state } = makeWgpuScene3DState();
|
|
166
|
+
const geometry = createMeshGeometry({
|
|
167
|
+
indices: null,
|
|
168
|
+
layout: {
|
|
169
|
+
attributes: [{ byteOffset: 0, format: 'float32x3', semantic: 'position' }],
|
|
170
|
+
stride: 12,
|
|
171
|
+
},
|
|
172
|
+
vertices: new Float32Array(9),
|
|
173
|
+
});
|
|
174
|
+
expect(ensureWgpuMeshUpload(state, geometry)).toBeNull();
|
|
175
|
+
expect(getWgpuScene3DRuntime(state).uploadCache.get(geometry)).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createCamera3D } from '@flighthq/camera';
|
|
2
|
+
import { createMatrix4, setVector3 } from '@flighthq/geometry';
|
|
3
|
+
import { addNodeChild, invalidateNodeLocalTransform } from '@flighthq/node';
|
|
4
|
+
import { createParticleEmitter3D, reserveParticleEmitter3D } from '@flighthq/particleemitter';
|
|
5
|
+
import { createNode3D, Node3DKind } from '@flighthq/scene3d';
|
|
6
|
+
import type { Camera3D, ParticleEmitter3D, Scene3DLightsLike } from '@flighthq/types';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { destroyWgpuParticleEmitter3DResources, drawWgpuScene3DParticleEmitter3Ds } from './wgpuParticleEmitter3D';
|
|
10
|
+
import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
|
|
11
|
+
|
|
12
|
+
function makeCamera(): Camera3D {
|
|
13
|
+
const cam = createCamera3D({
|
|
14
|
+
far: 100,
|
|
15
|
+
near: 0.1,
|
|
16
|
+
projection: { aspect: 1, fovY: Math.PI / 3, kind: 'perspective' },
|
|
17
|
+
});
|
|
18
|
+
cam.view = createMatrix4();
|
|
19
|
+
return cam;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeLights(): Scene3DLightsLike {
|
|
23
|
+
return { ambient: null, directional: null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeEmitterWithParticles(count: number): ParticleEmitter3D {
|
|
27
|
+
const emitter = createParticleEmitter3D();
|
|
28
|
+
reserveParticleEmitter3D(emitter, count);
|
|
29
|
+
const data = emitter.data;
|
|
30
|
+
data.particleCount = count;
|
|
31
|
+
for (let i = 0; i < count; i++) {
|
|
32
|
+
const tt = i * 4;
|
|
33
|
+
data.transforms[tt] = i;
|
|
34
|
+
data.transforms[tt + 1] = i * 2;
|
|
35
|
+
data.transforms[tt + 2] = 0;
|
|
36
|
+
data.transforms[tt + 3] = 1;
|
|
37
|
+
data.positionsZ[i] = i * 3;
|
|
38
|
+
data.alphas[i] = 1;
|
|
39
|
+
const ct = i * 3;
|
|
40
|
+
data.colors[ct] = 1;
|
|
41
|
+
data.colors[ct + 1] = 1;
|
|
42
|
+
data.colors[ct + 2] = 1;
|
|
43
|
+
data.ids[i] = 0;
|
|
44
|
+
}
|
|
45
|
+
return emitter;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeAtlasEmitter(regionWidth: number, regionHeight: number): ParticleEmitter3D {
|
|
49
|
+
const emitter = makeEmitterWithParticles(1);
|
|
50
|
+
const img = document.createElement('img');
|
|
51
|
+
emitter.data.atlas = {
|
|
52
|
+
image: { source: img, width: 128, height: 128 },
|
|
53
|
+
regions: [{ id: 0, x: 0, y: 0, width: regionWidth, height: regionHeight }],
|
|
54
|
+
} as unknown as NonNullable<ParticleEmitter3D['data']['atlas']>;
|
|
55
|
+
return emitter;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findInstanceWrite(calls: { name: string; args: unknown[] }[]): Float32Array | undefined {
|
|
59
|
+
// The instance upload is the writeBuffer whose data is a Float32Array longer than the 8-float corner
|
|
60
|
+
// quad; the frame upload passes an ArrayBuffer and the corner buffer a length-8 array.
|
|
61
|
+
const call = calls.find(
|
|
62
|
+
(c) => c.name === 'writeBuffer' && c.args[2] instanceof Float32Array && (c.args[2] as Float32Array).length > 8,
|
|
63
|
+
);
|
|
64
|
+
return call?.args[2] as Float32Array | undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pipelineDescriptors(calls: { name: string; args: unknown[] }[]): Record<string, unknown>[] {
|
|
68
|
+
return calls.filter((c) => c.name === 'createRenderPipeline').map((c) => c.args[0] as Record<string, unknown>);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasTextureConstant(descriptor: Record<string, unknown>): number {
|
|
72
|
+
const fragment = descriptor['fragment'] as { constants: Record<string, number> };
|
|
73
|
+
return fragment.constants['HAS_TEXTURE'];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('destroyWgpuParticleEmitter3DResources', () => {
|
|
77
|
+
it('is a no-op when no resources were created', () => {
|
|
78
|
+
const { state } = makeWgpuScene3DState();
|
|
79
|
+
expect(() => destroyWgpuParticleEmitter3DResources(state)).not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('drops resources after a draw', () => {
|
|
83
|
+
const { state } = makeWgpuScene3DState();
|
|
84
|
+
const scene = createNode3D(Node3DKind);
|
|
85
|
+
addNodeChild(scene, makeEmitterWithParticles(1));
|
|
86
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
87
|
+
expect(() => destroyWgpuParticleEmitter3DResources(state)).not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('drawWgpuScene3DParticleEmitter3Ds', () => {
|
|
92
|
+
it('is a no-op when the scene has no particle emitter nodes', () => {
|
|
93
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
94
|
+
const scene = createNode3D(Node3DKind);
|
|
95
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
96
|
+
expect(fake.calls.some((c) => c.name === 'drawIndexed')).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('skips emitters with zero particles', () => {
|
|
100
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
101
|
+
const scene = createNode3D(Node3DKind);
|
|
102
|
+
addNodeChild(scene, createParticleEmitter3D());
|
|
103
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
104
|
+
expect(fake.calls.some((c) => c.name === 'drawIndexed')).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('issues one instanced indexed draw of 6 indices per emitter', () => {
|
|
108
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
109
|
+
const scene = createNode3D(Node3DKind);
|
|
110
|
+
addNodeChild(scene, makeEmitterWithParticles(5));
|
|
111
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
112
|
+
const draw = fake.calls.find((c) => c.name === 'drawIndexed');
|
|
113
|
+
expect(draw).toBeDefined();
|
|
114
|
+
expect(draw!.args[0]).toBe(6);
|
|
115
|
+
expect(draw!.args[1]).toBe(5);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('uploads the per-instance data to a vertex buffer', () => {
|
|
119
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
120
|
+
const scene = createNode3D(Node3DKind);
|
|
121
|
+
addNodeChild(scene, makeEmitterWithParticles(3));
|
|
122
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
123
|
+
expect(findInstanceWrite(fake.calls)).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('normalizes the particle quad to an aspect-ratio unit square, not the region pixel size', () => {
|
|
127
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
128
|
+
const scene = createNode3D(Node3DKind);
|
|
129
|
+
addNodeChild(scene, makeAtlasEmitter(64, 32));
|
|
130
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
131
|
+
const instanceData = findInstanceWrite(fake.calls)!;
|
|
132
|
+
// Instance floats [13]/[14] carry the normalized quad size for the first particle.
|
|
133
|
+
expect(instanceData[13]).toBeCloseTo(1);
|
|
134
|
+
expect(instanceData[14]).toBeCloseTo(0.5);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('compiles the textured pipeline variant for an atlas emitter', () => {
|
|
138
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
139
|
+
const scene = createNode3D(Node3DKind);
|
|
140
|
+
addNodeChild(scene, makeAtlasEmitter(64, 64));
|
|
141
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
142
|
+
expect(pipelineDescriptors(fake.calls).some((d) => hasTextureConstant(d) === 1)).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('compiles the untextured pipeline variant for an atlas-less emitter', () => {
|
|
146
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
147
|
+
const scene = createNode3D(Node3DKind);
|
|
148
|
+
addNodeChild(scene, makeEmitterWithParticles(1));
|
|
149
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
150
|
+
const descriptors = pipelineDescriptors(fake.calls);
|
|
151
|
+
expect(descriptors.length).toBe(1);
|
|
152
|
+
expect(hasTextureConstant(descriptors[0])).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('reuses the cached pipeline on a second draw', () => {
|
|
156
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
157
|
+
const scene = createNode3D(Node3DKind);
|
|
158
|
+
addNodeChild(scene, makeEmitterWithParticles(1));
|
|
159
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
160
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
161
|
+
// One variant compiled once across both draws — the shader module and pipeline are cached per state.
|
|
162
|
+
expect(pipelineDescriptors(fake.calls).length).toBe(1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('skips the node world transform for world-space particles', () => {
|
|
166
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
167
|
+
const scene = createNode3D(Node3DKind);
|
|
168
|
+
const emitter = makeEmitterWithParticles(1);
|
|
169
|
+
emitter.data.transforms[0] = 1;
|
|
170
|
+
emitter.data.transforms[1] = 2;
|
|
171
|
+
emitter.data.positionsZ[0] = 3;
|
|
172
|
+
setVector3(emitter.position, 10, 20, 30);
|
|
173
|
+
invalidateNodeLocalTransform(emitter);
|
|
174
|
+
emitter.data.worldSpace = true;
|
|
175
|
+
addNodeChild(scene, emitter);
|
|
176
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
177
|
+
const instanceData = findInstanceWrite(fake.calls)!;
|
|
178
|
+
// Already baked into world space, so the (10,20,30) node translation is not re-applied.
|
|
179
|
+
expect(instanceData[0]).toBeCloseTo(1);
|
|
180
|
+
expect(instanceData[1]).toBeCloseTo(2);
|
|
181
|
+
expect(instanceData[2]).toBeCloseTo(3);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('applies the node world transform for local-space particles', () => {
|
|
185
|
+
const { state, fake } = makeWgpuScene3DState();
|
|
186
|
+
const scene = createNode3D(Node3DKind);
|
|
187
|
+
const emitter = makeEmitterWithParticles(1);
|
|
188
|
+
emitter.data.transforms[0] = 1;
|
|
189
|
+
emitter.data.transforms[1] = 2;
|
|
190
|
+
emitter.data.positionsZ[0] = 3;
|
|
191
|
+
setVector3(emitter.position, 10, 20, 30);
|
|
192
|
+
invalidateNodeLocalTransform(emitter);
|
|
193
|
+
addNodeChild(scene, emitter);
|
|
194
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
195
|
+
const instanceData = findInstanceWrite(fake.calls)!;
|
|
196
|
+
expect(instanceData[0]).toBeCloseTo(11);
|
|
197
|
+
expect(instanceData[1]).toBeCloseTo(22);
|
|
198
|
+
expect(instanceData[2]).toBeCloseTo(33);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { WgpuPbrDefineKey } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { compileWgpuPbrPipeline, ensureWgpuPbrPipeline } from './wgpuPbrPipelineCache';
|
|
4
|
+
import { getWgpuScene3DRuntime } from './wgpuScene3DRuntime';
|
|
5
|
+
import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
|
|
6
|
+
|
|
7
|
+
function key(overrides?: Partial<WgpuPbrDefineKey>): WgpuPbrDefineKey {
|
|
8
|
+
return {
|
|
9
|
+
alphaMaskEnabled: false,
|
|
10
|
+
anisotropyEnabled: false,
|
|
11
|
+
clearcoatEnabled: false,
|
|
12
|
+
doubleSided: false,
|
|
13
|
+
hasAlphaMap: false,
|
|
14
|
+
hasBaseColorMap: false,
|
|
15
|
+
hasEmissiveMap: false,
|
|
16
|
+
hasMetallicRoughnessMap: false,
|
|
17
|
+
hasNormalMap: false,
|
|
18
|
+
hasOcclusionMap: false,
|
|
19
|
+
iridescenceEnabled: false,
|
|
20
|
+
sheenEnabled: false,
|
|
21
|
+
specularEnabled: false,
|
|
22
|
+
subsurfaceEnabled: false,
|
|
23
|
+
transmissionEnabled: false,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('compileWgpuPbrPipeline', () => {
|
|
29
|
+
it('compiles a module and builds the pipeline + material bind-group layout', () => {
|
|
30
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
31
|
+
const pipeline = compileWgpuPbrPipeline(state, key(), 'rgba16float');
|
|
32
|
+
expect(pipeline.pipeline).toBeDefined();
|
|
33
|
+
expect(pipeline.materialBindGroupLayout).toBeDefined();
|
|
34
|
+
expect(fake.calls.some((c) => c.name === 'createShaderModule')).toBe(true);
|
|
35
|
+
expect(fake.calls.some((c) => c.name === 'createRenderPipeline')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('bakes back-face culling unless the key is doubleSided', () => {
|
|
39
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
40
|
+
compileWgpuPbrPipeline(state, key(), 'bgra8unorm');
|
|
41
|
+
compileWgpuPbrPipeline(state, key({ doubleSided: true }), 'bgra8unorm');
|
|
42
|
+
const pipelineCalls = fake.calls.filter((c) => c.name === 'createRenderPipeline');
|
|
43
|
+
const culled = pipelineCalls[0].args[0] as { primitive: { cullMode: string } };
|
|
44
|
+
const doubleSided = pipelineCalls[1].args[0] as { primitive: { cullMode: string } };
|
|
45
|
+
expect(culled.primitive.cullMode).toBe('back');
|
|
46
|
+
expect(doubleSided.primitive.cullMode).toBe('none');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('ensureWgpuPbrPipeline', () => {
|
|
51
|
+
it('caches one pipeline per define key + format', () => {
|
|
52
|
+
const { state } = makeWgpuScene3DState();
|
|
53
|
+
const a = ensureWgpuPbrPipeline(state, key(), 'rgba16float');
|
|
54
|
+
const b = ensureWgpuPbrPipeline(state, key(), 'rgba16float');
|
|
55
|
+
expect(a).toBe(b);
|
|
56
|
+
expect(getWgpuScene3DRuntime(state).pipelineCache.size).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('compiles distinct variants for distinct format, standard, or extension defines', () => {
|
|
60
|
+
const { state } = makeWgpuScene3DState();
|
|
61
|
+
ensureWgpuPbrPipeline(state, key(), 'rgba16float');
|
|
62
|
+
ensureWgpuPbrPipeline(state, key(), 'bgra8unorm');
|
|
63
|
+
ensureWgpuPbrPipeline(state, key({ doubleSided: true }), 'rgba16float');
|
|
64
|
+
ensureWgpuPbrPipeline(state, key({ clearcoatEnabled: true }), 'rgba16float');
|
|
65
|
+
expect(getWgpuScene3DRuntime(state).pipelineCache.size).toBe(4);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { WgpuPbrDefineKey } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildWgpuPbrDefineKey,
|
|
5
|
+
buildWgpuPbrDefineSource,
|
|
6
|
+
getWgpuPbrModuleBody,
|
|
7
|
+
getWgpuPbrModuleSourceForKey,
|
|
8
|
+
} from './wgpuPbrPrelude';
|
|
9
|
+
import { makeWgpuSkinningAdapter } from './wgpuScene3DTestHelper';
|
|
10
|
+
|
|
11
|
+
function key(overrides?: Partial<WgpuPbrDefineKey>): WgpuPbrDefineKey {
|
|
12
|
+
return {
|
|
13
|
+
alphaMaskEnabled: false,
|
|
14
|
+
anisotropyEnabled: false,
|
|
15
|
+
clearcoatEnabled: false,
|
|
16
|
+
doubleSided: false,
|
|
17
|
+
hasAlphaMap: false,
|
|
18
|
+
hasBaseColorMap: false,
|
|
19
|
+
hasEmissiveMap: false,
|
|
20
|
+
hasMetallicRoughnessMap: false,
|
|
21
|
+
hasNormalMap: false,
|
|
22
|
+
hasOcclusionMap: false,
|
|
23
|
+
iridescenceEnabled: false,
|
|
24
|
+
sheenEnabled: false,
|
|
25
|
+
specularEnabled: false,
|
|
26
|
+
subsurfaceEnabled: false,
|
|
27
|
+
transmissionEnabled: false,
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const NONE = key();
|
|
33
|
+
const STANDARD_ALL = key({
|
|
34
|
+
alphaMaskEnabled: true,
|
|
35
|
+
doubleSided: true,
|
|
36
|
+
hasBaseColorMap: true,
|
|
37
|
+
hasEmissiveMap: true,
|
|
38
|
+
hasMetallicRoughnessMap: true,
|
|
39
|
+
hasNormalMap: true,
|
|
40
|
+
hasOcclusionMap: true,
|
|
41
|
+
});
|
|
42
|
+
const ALL = key({
|
|
43
|
+
...STANDARD_ALL,
|
|
44
|
+
anisotropyEnabled: true,
|
|
45
|
+
clearcoatEnabled: true,
|
|
46
|
+
iridescenceEnabled: true,
|
|
47
|
+
sheenEnabled: true,
|
|
48
|
+
specularEnabled: true,
|
|
49
|
+
subsurfaceEnabled: true,
|
|
50
|
+
transmissionEnabled: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('buildWgpuPbrDefineKey', () => {
|
|
54
|
+
it('produces a stable, distinct string per flag set', () => {
|
|
55
|
+
expect(buildWgpuPbrDefineKey(NONE)).toBe('--------:-------');
|
|
56
|
+
expect(buildWgpuPbrDefineKey(STANDARD_ALL)).toBe('mdbnroe-:-------');
|
|
57
|
+
expect(buildWgpuPbrDefineKey(ALL)).toBe('mdbnroe-:CSAIPUT');
|
|
58
|
+
expect(buildWgpuPbrDefineKey(key({ alphaMaskEnabled: true }))).toBe('m-------:-------');
|
|
59
|
+
expect(buildWgpuPbrDefineKey(key({ doubleSided: true }))).toBe('-d------:-------');
|
|
60
|
+
expect(buildWgpuPbrDefineKey(key({ hasBaseColorMap: true }))).toBe('--b-----:-------');
|
|
61
|
+
expect(buildWgpuPbrDefineKey(key({ hasNormalMap: true }))).toBe('---n----:-------');
|
|
62
|
+
expect(buildWgpuPbrDefineKey(key({ hasMetallicRoughnessMap: true }))).toBe('----r---:-------');
|
|
63
|
+
expect(buildWgpuPbrDefineKey(key({ hasOcclusionMap: true }))).toBe('-----o--:-------');
|
|
64
|
+
expect(buildWgpuPbrDefineKey(key({ hasEmissiveMap: true }))).toBe('------e-:-------');
|
|
65
|
+
expect(buildWgpuPbrDefineKey(key({ hasAlphaMap: true }))).toBe('-------a:-------');
|
|
66
|
+
expect(buildWgpuPbrDefineKey(key({ clearcoatEnabled: true }))).toBe('--------:C------');
|
|
67
|
+
expect(buildWgpuPbrDefineKey(key({ transmissionEnabled: true }))).toBe('--------:------T');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('is identical for identical flags (cache soundness)', () => {
|
|
71
|
+
expect(buildWgpuPbrDefineKey(key(ALL))).toBe(buildWgpuPbrDefineKey(key(ALL)));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('buildWgpuPbrDefineSource', () => {
|
|
76
|
+
it('emits a const bool flag block reflecting the key', () => {
|
|
77
|
+
const source = buildWgpuPbrDefineSource(key({ hasBaseColorMap: true, doubleSided: true }));
|
|
78
|
+
expect(source).toContain('const HAS_BASE_COLOR_MAP : bool = true;');
|
|
79
|
+
expect(source).toContain('const DOUBLE_SIDED : bool = true;');
|
|
80
|
+
expect(source).toContain('const ALPHA_MASK : bool = false;');
|
|
81
|
+
expect(source).toContain('const HAS_NORMAL_MAP : bool = false;');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('emits a const for every extension lobe', () => {
|
|
85
|
+
const all = buildWgpuPbrDefineSource(ALL);
|
|
86
|
+
expect(all).toContain('const CLEARCOAT : bool = true;');
|
|
87
|
+
expect(all).toContain('const SHEEN : bool = true;');
|
|
88
|
+
expect(all).toContain('const ANISOTROPY : bool = true;');
|
|
89
|
+
expect(all).toContain('const IRIDESCENCE : bool = true;');
|
|
90
|
+
expect(all).toContain('const SPECULAR_EXT : bool = true;');
|
|
91
|
+
expect(all).toContain('const SUBSURFACE : bool = true;');
|
|
92
|
+
expect(all).toContain('const TRANSMISSION : bool = true;');
|
|
93
|
+
|
|
94
|
+
const none = buildWgpuPbrDefineSource(NONE);
|
|
95
|
+
expect(none).toContain('const CLEARCOAT : bool = false;');
|
|
96
|
+
expect(none).toContain('const TRANSMISSION : bool = false;');
|
|
97
|
+
expect(none).toContain('const HAS_ALPHA_MAP : bool = false;');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('gates the alpha-map const off the dedicated coverage-map flag', () => {
|
|
101
|
+
expect(buildWgpuPbrDefineSource(key({ hasAlphaMap: true }))).toContain('const HAS_ALPHA_MAP : bool = true;');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('getWgpuPbrModuleBody', () => {
|
|
106
|
+
it('declares the entry points and bind-group structs', () => {
|
|
107
|
+
const body = getWgpuPbrModuleBody();
|
|
108
|
+
expect(body).toContain('fn vs_main');
|
|
109
|
+
expect(body).toContain('fn fs_main');
|
|
110
|
+
expect(body).toContain('struct Frame');
|
|
111
|
+
expect(body).toContain('struct MaterialBlock');
|
|
112
|
+
expect(body).toContain('var<uniform> frame');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('samples every standard map through its own sampler binding', () => {
|
|
116
|
+
const body = getWgpuPbrModuleBody();
|
|
117
|
+
expect(body).toContain('textureSample(baseColorTexture, baseColorSampler, in.uv)');
|
|
118
|
+
expect(body).toContain('textureSample(metallicRoughnessTexture, metallicRoughnessSampler, in.uv)');
|
|
119
|
+
expect(body).toContain('textureSample(normalTexture, normalSampler, in.uv)');
|
|
120
|
+
expect(body).toContain('textureSample(occlusionTexture, occlusionSampler, in.uv)');
|
|
121
|
+
expect(body).toContain('textureSample(emissiveTexture, emissiveSampler, in.uv)');
|
|
122
|
+
expect(body).toContain('textureSample(alphaTexture, alphaSampler, in.uv)');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('includes the extension lobe helpers behind their const flags', () => {
|
|
126
|
+
const body = getWgpuPbrModuleBody();
|
|
127
|
+
expect(body).toContain('distributionGgxAnisotropic');
|
|
128
|
+
expect(body).toContain('distributionCharlie');
|
|
129
|
+
expect(body).toContain('iridescentFresnel');
|
|
130
|
+
expect(body).toContain('if (CLEARCOAT)');
|
|
131
|
+
expect(body).toContain('if (TRANSMISSION)');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Image-based-lighting shares group(3) with shadow so PBR stays within WebGPU's required
|
|
135
|
+
// maxBindGroups minimum of 4.
|
|
136
|
+
it('declares the group(3) IBL bindings and applies the split-sum ambient term', () => {
|
|
137
|
+
const body = getWgpuPbrModuleBody();
|
|
138
|
+
expect(body).toContain('@group(3) @binding(3) var<uniform> ibl');
|
|
139
|
+
expect(body).toContain('var iblIrradiance : texture_cube<f32>');
|
|
140
|
+
expect(body).toContain('var iblPrefiltered : texture_cube<f32>');
|
|
141
|
+
expect(body).toContain('var iblBrdf : texture_2d<f32>');
|
|
142
|
+
expect(body).toContain('fn sampleIblAmbient');
|
|
143
|
+
expect(body).toContain('fresnelSchlickRoughness');
|
|
144
|
+
// split-sum specular = prefiltered * (F * brdf.x + brdf.y)
|
|
145
|
+
expect(body).toContain('prefiltered * (F * brdf.x + brdf.y)');
|
|
146
|
+
// gated ambient: IBL when enabled, else the flat ambient term.
|
|
147
|
+
expect(body).toContain('if (ibl.params.x > 0.5)');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('getWgpuPbrModuleSourceForKey', () => {
|
|
152
|
+
it('prepends the flag block to the module body', () => {
|
|
153
|
+
const k = key({ hasNormalMap: true });
|
|
154
|
+
const source = getWgpuPbrModuleSourceForKey(k);
|
|
155
|
+
expect(source.startsWith(buildWgpuPbrDefineSource(k))).toBe(true);
|
|
156
|
+
expect(source).toContain('fn fs_main');
|
|
157
|
+
expect(source).toContain('@interpolate(flat) objectAlpha : f32');
|
|
158
|
+
expect(source).toContain('alpha * in.objectAlpha');
|
|
159
|
+
expect(source).not.toContain('jointTexture');
|
|
160
|
+
expect(getWgpuPbrModuleSourceForKey(k, true, makeWgpuSkinningAdapter())).toContain('textureLoad(jointTexture');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { WgpuRenderStateRuntime } from '@flighthq/types';
|
|
2
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { getWgpuScene3DRuntime, getWgpuSkinningAdapter } from './wgpuScene3DRuntime';
|
|
5
|
+
import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
|
|
6
|
+
import { registerWgpuGpuSkinning } from './wgpuSkinPalette';
|
|
7
|
+
|
|
8
|
+
describe('getWgpuScene3DRuntime', () => {
|
|
9
|
+
it('lazily creates one runtime per state and returns the same instance', () => {
|
|
10
|
+
const { state } = makeWgpuScene3DState();
|
|
11
|
+
const a = getWgpuScene3DRuntime(state);
|
|
12
|
+
const b = getWgpuScene3DRuntime(state);
|
|
13
|
+
expect(a).toBe(b);
|
|
14
|
+
expect(a.materialRegistry).toBeInstanceOf(Map);
|
|
15
|
+
expect(a.pipelineCache).toBeInstanceOf(Map);
|
|
16
|
+
expect(a.activeMeshPipeline).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('surfaces the registry and upload cache through the header runtime slots', () => {
|
|
20
|
+
const { state } = makeWgpuScene3DState();
|
|
21
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
22
|
+
const stateRuntime = state[EntityRuntimeKey] as WgpuRenderStateRuntime;
|
|
23
|
+
expect(stateRuntime.sceneMeshMaterialRegistry).toBe(scene.materialRegistry);
|
|
24
|
+
expect(stateRuntime.sceneMeshUploadCache).toBe(scene.uploadCache);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getWgpuSkinningAdapter', () => {
|
|
29
|
+
it('returns the state-scoped opt-in adapter', () => {
|
|
30
|
+
const { state } = makeWgpuScene3DState();
|
|
31
|
+
expect(getWgpuSkinningAdapter(state)).toBeNull();
|
|
32
|
+
registerWgpuGpuSkinning(state);
|
|
33
|
+
expect(getWgpuSkinningAdapter(state)).not.toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
|
|
2
|
+
import { getWgpuScene3DTime, setWgpuScene3DTime } from './wgpuScene3DTime';
|
|
3
|
+
|
|
4
|
+
describe('getWgpuScene3DTime', () => {
|
|
5
|
+
it('defaults to zero and reads the state-scoped value', () => {
|
|
6
|
+
const { state } = makeWgpuScene3DState();
|
|
7
|
+
expect(getWgpuScene3DTime(state)).toBe(0);
|
|
8
|
+
setWgpuScene3DTime(state, 2.5);
|
|
9
|
+
expect(getWgpuScene3DTime(state)).toBe(2.5);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('setWgpuScene3DTime', () => {
|
|
14
|
+
it('does not leak across render states', () => {
|
|
15
|
+
const first = makeWgpuScene3DState().state;
|
|
16
|
+
const second = makeWgpuScene3DState().state;
|
|
17
|
+
setWgpuScene3DTime(first, 4);
|
|
18
|
+
expect(getWgpuScene3DTime(second)).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
});
|