@forgeax/engine-runtime 0.1.20 → 0.1.21
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/README.md +102 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts +2 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -30
- package/src/__tests__/active-camera.unit.test.ts +6 -8
- package/src/__tests__/asset-registry.recursive.spec.ts +1 -1
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
- package/src/__tests__/components.test-d.ts +10 -3
- package/src/__tests__/components.unit.test.ts +13 -7
- package/src/__tests__/create-renderer-assembly.integration.test.ts +24 -0
- package/src/__tests__/create-renderer-gpu-pass-timing.unit.test.ts +32 -0
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +66 -0
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +9 -1
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
- package/src/__tests__/errors.unit.test.ts +51 -20
- package/src/__tests__/extract-frames-merge.test.ts +48 -40
- package/src/__tests__/extract-skin-mismatch.unit.test.ts +6 -2
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +13 -0
- package/src/__tests__/geometry.unit.test.ts +7 -10
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +2 -1
- package/src/__tests__/light-snapshot.test-d.ts +8 -0
- package/src/__tests__/lights-preservation.fixup.test.ts +127 -0
- package/src/__tests__/lights.unit.test.ts +75 -2235
- package/src/__tests__/pbr-pipeline.unit.test.ts +6 -1
- package/src/__tests__/pipeline.unit.test.ts +48 -202
- package/src/__tests__/point-light-shadow.browser.test.ts +208 -83
- package/src/__tests__/point-light-shadow.unit.test.ts +61 -4
- package/src/__tests__/render-error-exhaustive.test-d.ts +63 -9
- package/src/__tests__/render-system-mega.test.ts +30 -41
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +4 -1
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +4 -1
- package/src/__tests__/render-system-skin-bg.unit.test.ts +14 -9
- package/src/__tests__/renderer-recover.unit.test.ts +8 -0
- package/src/__tests__/renderer-surface.unit.test.ts +229 -3
- package/src/__tests__/shadow-csm-atlas.test.ts +41 -0
- package/src/__tests__/shadow-csm-component.test.ts +31 -24
- package/src/__tests__/shadow-csm-extract.test.ts +156 -47
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +72 -0
- package/src/__tests__/shadow-csm-shader.dawn.test.ts +19 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +41 -45
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +85 -20
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +3 -1
- package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
- package/src/__tests__/ssao-bgl.test.ts +10 -16
- package/src/__tests__/ssao-passes.test.ts +19 -2
- package/src/__tests__/systems.unit.test.ts +44 -31
- package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
- package/src/createRenderer.ts +1 -0
- package/src/index.ts +1 -2
|
@@ -171,52 +171,26 @@ function simulateMerge(
|
|
|
171
171
|
}
|
|
172
172
|
directionalCount += f.lights.directionalCount;
|
|
173
173
|
}
|
|
174
|
+
const firstDirectionalLights = frames.find(
|
|
175
|
+
(frame) => frame.lights.directional !== undefined,
|
|
176
|
+
)?.lights;
|
|
174
177
|
const lights: ExtractedLights = {
|
|
175
178
|
directional,
|
|
176
179
|
directionalCount,
|
|
177
180
|
point,
|
|
178
181
|
spot,
|
|
179
|
-
lightViewProj:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
directional !== undefined
|
|
189
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.cascadeCount
|
|
190
|
-
: undefined,
|
|
191
|
-
cascadeBlend:
|
|
192
|
-
directional !== undefined
|
|
193
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.cascadeBlend
|
|
194
|
-
: undefined,
|
|
195
|
-
shadowMapSize:
|
|
196
|
-
directional !== undefined
|
|
197
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.shadowMapSize
|
|
198
|
-
: undefined,
|
|
199
|
-
depthBias:
|
|
200
|
-
directional !== undefined
|
|
201
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.depthBias
|
|
202
|
-
: undefined,
|
|
203
|
-
normalBias:
|
|
204
|
-
directional !== undefined
|
|
205
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.normalBias
|
|
206
|
-
: undefined,
|
|
207
|
-
pcfKernelSize:
|
|
208
|
-
directional !== undefined
|
|
209
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.pcfKernelSize
|
|
210
|
-
: undefined,
|
|
182
|
+
lightViewProj: firstDirectionalLights?.lightViewProj,
|
|
183
|
+
splitPlanes: firstDirectionalLights?.splitPlanes,
|
|
184
|
+
cascadeCount: firstDirectionalLights?.cascadeCount,
|
|
185
|
+
cascadeBlend: firstDirectionalLights?.cascadeBlend,
|
|
186
|
+
shadowMapSize: firstDirectionalLights?.shadowMapSize,
|
|
187
|
+
depthBias: firstDirectionalLights?.depthBias,
|
|
188
|
+
normalBias: firstDirectionalLights?.normalBias,
|
|
189
|
+
directionalShadowQuality: firstDirectionalLights?.directionalShadowQuality,
|
|
190
|
+
directionalShadowError: firstDirectionalLights?.directionalShadowError,
|
|
211
191
|
pointShadow: frames.flatMap((f) => f.lights.pointShadow),
|
|
212
|
-
directionalCsmConfig:
|
|
213
|
-
|
|
214
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.directionalCsmConfig
|
|
215
|
-
: undefined,
|
|
216
|
-
directionalCsmDirection:
|
|
217
|
-
directional !== undefined
|
|
218
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.directionalCsmDirection
|
|
219
|
-
: undefined,
|
|
192
|
+
directionalCsmConfig: firstDirectionalLights?.directionalCsmConfig,
|
|
193
|
+
directionalCsmDirection: firstDirectionalLights?.directionalCsmDirection,
|
|
220
194
|
};
|
|
221
195
|
|
|
222
196
|
// AC-05: singletons from owner world only
|
|
@@ -433,6 +407,36 @@ describe('extractFrames merge semantics (m2-t1, AC-04/05/06)', () => {
|
|
|
433
407
|
expect(frameB.lights.directionalCount).toBe(1);
|
|
434
408
|
});
|
|
435
409
|
|
|
410
|
+
it('AC-04: first directional owner error is preserved across the merge', () => {
|
|
411
|
+
const worldA = new World();
|
|
412
|
+
worldA
|
|
413
|
+
.spawn(
|
|
414
|
+
{ component: Transform, data: identityTransform() },
|
|
415
|
+
{
|
|
416
|
+
component: DirectionalLight,
|
|
417
|
+
data: {
|
|
418
|
+
direction: [0, -1, 0],
|
|
419
|
+
shadowFilter: 4,
|
|
420
|
+
shadowAngularRadius: 999,
|
|
421
|
+
maxPenumbraTexels: 32,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
.unwrap();
|
|
426
|
+
|
|
427
|
+
const worldB = makeWorldWithDirectionalLight();
|
|
428
|
+
const frameA = extractFrame(worldA, prepareExtractContext(worldA));
|
|
429
|
+
const frameB = extractFrame(worldB, prepareExtractContext(worldB));
|
|
430
|
+
|
|
431
|
+
expect(frameA.lights.directionalShadowError?.code).toBe('shadow-invalid-config');
|
|
432
|
+
expect(frameB.lights.directionalShadowError).toBeUndefined();
|
|
433
|
+
const merged = simulateMerge([frameA, frameB], 0);
|
|
434
|
+
expect(merged.lights.directionalShadowError?.detail).toMatchObject({
|
|
435
|
+
field: 'shadowAngularRadius',
|
|
436
|
+
actual: 999,
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
436
440
|
// ── AC-05: singleton resources from owner only ─────────────────────────────
|
|
437
441
|
|
|
438
442
|
it('AC-05: skylight is taken from owner world only, non-owner ignored', () => {
|
|
@@ -624,6 +628,9 @@ describe('extractFrames cross-world directional CSM (bug-20260710)', () => {
|
|
|
624
628
|
}
|
|
625
629
|
// splitPlanes reach the component shadowDistance on the last cascade.
|
|
626
630
|
expect(frame.lights.splitPlanes).toBeDefined();
|
|
631
|
+
expect(frame.lights.splitPlanes).toHaveLength(16);
|
|
632
|
+
expect(frame.lights.splitPlanes?.[1]).toBeGreaterThan(0);
|
|
633
|
+
expect(frame.lights.splitPlanes?.[2]).toBeGreaterThan(0);
|
|
627
634
|
});
|
|
628
635
|
|
|
629
636
|
it('single world (camera + light together) still yields non-zero lightViewProj', () => {
|
|
@@ -652,5 +659,6 @@ describe('extractFrames cross-world directional CSM (bug-20260710)', () => {
|
|
|
652
659
|
for (let i = 0; i < (frame.lights.cascadeCount ?? 0); i++) {
|
|
653
660
|
expect(isNonZeroMat(lvp?.[i])).toBe(true);
|
|
654
661
|
}
|
|
662
|
+
expect(frame.lights.splitPlanes).toHaveLength(16);
|
|
655
663
|
});
|
|
656
664
|
});
|
|
@@ -276,7 +276,7 @@ describe('render-system-extract Skin / pbr-skin mismatch (AC-07 / w19)', () => {
|
|
|
276
276
|
expect(frame.renderables).toHaveLength(0);
|
|
277
277
|
});
|
|
278
278
|
|
|
279
|
-
it('(c) Skin + pbr-skin material
|
|
279
|
+
it('(c) Skin + pbr-skin material without a palette allocator -> renderable and dispatch skipped', () => {
|
|
280
280
|
const world = new World();
|
|
281
281
|
const assets = makeAssetRegistry();
|
|
282
282
|
const meshHandle = registerSkinnedMesh(world);
|
|
@@ -288,9 +288,13 @@ describe('render-system-extract Skin / pbr-skin mismatch (AC-07 / w19)', () => {
|
|
|
288
288
|
|
|
289
289
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
290
290
|
|
|
291
|
-
extractFrame(world, prepareExtractContext(world, { assets }));
|
|
291
|
+
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
292
292
|
|
|
293
293
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
294
|
+
expect(frame.renderables).toHaveLength(0);
|
|
295
|
+
expect(frame.dispatch.some((entry) => entry.materialShaderId === 'forgeax::pbr-skin')).toBe(
|
|
296
|
+
false,
|
|
297
|
+
);
|
|
294
298
|
});
|
|
295
299
|
|
|
296
300
|
it('(d) D-5 continue semantics: bad-skin entity skipped does NOT abort sibling extract', () => {
|
|
@@ -25,4 +25,17 @@ describe('frame targets shadow removal', () => {
|
|
|
25
25
|
|
|
26
26
|
expect(resolveShadowMapSize(internals as never, lights as never)).toBe(1024);
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
it('does not resolve a Directional atlas for the disabled topology sentinel', () => {
|
|
30
|
+
const internals = {
|
|
31
|
+
device: { limits: { maxTextureDimension2D: 2048 } },
|
|
32
|
+
};
|
|
33
|
+
const lights = {
|
|
34
|
+
directional: 'disabled',
|
|
35
|
+
shadowMapSize: 2048,
|
|
36
|
+
cascadeCount: undefined,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(resolveShadowMapSize(internals as never, lights as never)).toBeUndefined();
|
|
40
|
+
});
|
|
28
41
|
});
|
|
@@ -2390,20 +2390,17 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
2390
2390
|
spies.drawIndexed.mockClear();
|
|
2391
2391
|
drawPublished(renderer, world as WorldType);
|
|
2392
2392
|
|
|
2393
|
-
//
|
|
2394
|
-
//
|
|
2393
|
+
// Mesh indexed draws carry the Instances cardinality. Fullscreen graph
|
|
2394
|
+
// draws are a separate topology concern and intentionally use one
|
|
2395
|
+
// instance; do not mix those two contracts in one equality assertion.
|
|
2395
2396
|
const indexedCalls = spies.drawIndexed.mock.calls as Array<
|
|
2396
2397
|
[number, number, number, number, number]
|
|
2397
2398
|
>;
|
|
2398
2399
|
const drawCalls = spies.draw.mock.calls as Array<[number, number, number, number]>;
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
for (const ic of allInstanceCounts) {
|
|
2404
|
-
expect(ic).toBe(first);
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2400
|
+
expect(indexedCalls.length).toBeGreaterThan(0);
|
|
2401
|
+
expect(indexedCalls.map((call) => call[1])).toEqual(expect.arrayContaining([4]));
|
|
2402
|
+
expect(indexedCalls.every((call) => call[1] === 4)).toBe(true);
|
|
2403
|
+
expect(drawCalls.every((call) => call[1] === 1)).toBe(true);
|
|
2407
2404
|
expect(errors).toEqual([]);
|
|
2408
2405
|
});
|
|
2409
2406
|
|
|
@@ -57,13 +57,14 @@ const emptyLights: ExtractedLights = {
|
|
|
57
57
|
directionalCount: 0,
|
|
58
58
|
point: [],
|
|
59
59
|
spot: [],
|
|
60
|
+
directionalShadowError: undefined,
|
|
60
61
|
lightViewProj: undefined,
|
|
61
62
|
splitPlanes: undefined,
|
|
62
63
|
cascadeCount: undefined,
|
|
63
64
|
cascadeBlend: undefined,
|
|
64
65
|
depthBias: undefined,
|
|
65
66
|
normalBias: undefined,
|
|
66
|
-
|
|
67
|
+
directionalShadowQuality: undefined,
|
|
67
68
|
} as unknown as ExtractedLights;
|
|
68
69
|
|
|
69
70
|
/** Capture the single writeViewUbo payload as a Float32Array. */
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// covers any .test-d.ts file in any package).
|
|
22
22
|
|
|
23
23
|
import { expectTypeOf, test } from 'vitest';
|
|
24
|
+
import type { DirectionalShadowQuality } from '../../../render/src/components/directional-shadow-filter';
|
|
25
|
+
import type { LightValidationError } from '../../../render/src/components/light-helpers';
|
|
24
26
|
import type {
|
|
25
27
|
DirectionalLightSnapshot,
|
|
26
28
|
ExtractedLights,
|
|
@@ -81,4 +83,10 @@ test('ExtractedLights three-bucket shape', () => {
|
|
|
81
83
|
expectTypeOf<ExtractedLights['directionalCount']>().toEqualTypeOf<number>();
|
|
82
84
|
expectTypeOf<ExtractedLights['point']>().toEqualTypeOf<readonly PointLightSnapshot[]>();
|
|
83
85
|
expectTypeOf<ExtractedLights['spot']>().toEqualTypeOf<readonly SpotLightSnapshot[]>();
|
|
86
|
+
expectTypeOf<ExtractedLights['directionalShadowQuality']>().toEqualTypeOf<
|
|
87
|
+
DirectionalShadowQuality | undefined
|
|
88
|
+
>();
|
|
89
|
+
expectTypeOf<ExtractedLights['directionalShadowError']>().toEqualTypeOf<
|
|
90
|
+
LightValidationError | undefined
|
|
91
|
+
>();
|
|
84
92
|
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { vec3 } from '@forgeax/engine-math';
|
|
3
|
+
import { Camera, PointLight, SpotLight } from '@forgeax/engine-render';
|
|
4
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
computeInvRangeSquared,
|
|
8
|
+
degToCos,
|
|
9
|
+
validatePointLightShadowData,
|
|
10
|
+
validateSpotLightData,
|
|
11
|
+
} from '../../../render/src/components/light-helpers';
|
|
12
|
+
import {
|
|
13
|
+
packLightSlot,
|
|
14
|
+
packPointLight,
|
|
15
|
+
packSpotLight,
|
|
16
|
+
} from '../../../render/src/light-buffer-layout';
|
|
17
|
+
import { buildPbrViewBglEntries } from '../../../render/src/pbr-pipeline';
|
|
18
|
+
import type {
|
|
19
|
+
PointLightSnapshot,
|
|
20
|
+
SpotLightSnapshot,
|
|
21
|
+
} from '../../../render/src/render-system-extract';
|
|
22
|
+
import { extractFrame, prepareExtractContext } from '../../../render/src/render-system-extract';
|
|
23
|
+
|
|
24
|
+
function pointSnapshot(): PointLightSnapshot {
|
|
25
|
+
return {
|
|
26
|
+
kind: 'point',
|
|
27
|
+
position: vec3.create(1, 2, 3),
|
|
28
|
+
color: vec3.create(2, 1, 0.5),
|
|
29
|
+
intensity: 2,
|
|
30
|
+
invRangeSquared: 0.25,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function spotSnapshot(): SpotLightSnapshot {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'spot',
|
|
37
|
+
position: vec3.create(1, 2, 3),
|
|
38
|
+
direction: vec3.create(0, -1, 0),
|
|
39
|
+
color: vec3.create(2, 1, 0.5),
|
|
40
|
+
intensity: 2,
|
|
41
|
+
invRangeSquared: 0.25,
|
|
42
|
+
cosInner: degToCos(15),
|
|
43
|
+
cosOuter: degToCos(30),
|
|
44
|
+
castShadow: false,
|
|
45
|
+
lightViewProj: undefined,
|
|
46
|
+
mapSize: 512,
|
|
47
|
+
nearPlane: 0.1,
|
|
48
|
+
farPlane: 25,
|
|
49
|
+
shadowAtlasTile: -1,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('Point/Spot and shared-light regression guard', () => {
|
|
54
|
+
it('keeps extract facts for point range and spot cone in the shared light path', () => {
|
|
55
|
+
const world = new World();
|
|
56
|
+
const pointSpawn = world.spawn(
|
|
57
|
+
{ component: Transform, data: { pos: [1, 2, 3] } },
|
|
58
|
+
{ component: PointLight, data: { range: 2 } },
|
|
59
|
+
);
|
|
60
|
+
const spotSpawn = world.spawn(
|
|
61
|
+
{ component: Transform, data: { pos: [4, 5, 6] } },
|
|
62
|
+
{
|
|
63
|
+
component: SpotLight,
|
|
64
|
+
data: { direction: [0, -1, 0], range: 4, innerConeDeg: 15, outerConeDeg: 30 },
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
world.spawn(
|
|
68
|
+
{ component: Transform, data: {} },
|
|
69
|
+
{ component: Camera, data: { fov: Math.PI / 4, aspect: 1, near: 0.1, far: 100 } },
|
|
70
|
+
);
|
|
71
|
+
expect(pointSpawn.ok, JSON.stringify(pointSpawn)).toBe(true);
|
|
72
|
+
expect(spotSpawn.ok, JSON.stringify(spotSpawn)).toBe(true);
|
|
73
|
+
|
|
74
|
+
const lights = extractFrame(world, prepareExtractContext(world)).lights;
|
|
75
|
+
expect(lights.point).toHaveLength(1);
|
|
76
|
+
expect(lights.point[0]?.invRangeSquared).toBeCloseTo(0.25, 6);
|
|
77
|
+
expect(lights.spot).toHaveLength(1);
|
|
78
|
+
expect(lights.spot[0]?.cosInner).toBeCloseTo(degToCos(15), 6);
|
|
79
|
+
expect(lights.spot[0]?.cosOuter).toBeCloseTo(degToCos(30), 6);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('keeps attenuation and cone helpers as finite, shared semantic owners', () => {
|
|
83
|
+
expect(computeInvRangeSquared(2)).toBeCloseTo(0.25, 8);
|
|
84
|
+
expect(computeInvRangeSquared(0)).toBe(1e8);
|
|
85
|
+
expect(computeInvRangeSquared(Number.POSITIVE_INFINITY)).toBe(0);
|
|
86
|
+
expect(degToCos(0)).toBeCloseTo(1, 8);
|
|
87
|
+
expect(degToCos(60)).toBeCloseTo(0.5, 8);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('keeps Point/Spot packing and the shared LightSlot discriminant intact', () => {
|
|
91
|
+
const point = pointSnapshot();
|
|
92
|
+
const spot = spotSnapshot();
|
|
93
|
+
const pointPacked = packPointLight(point);
|
|
94
|
+
const spotPacked = packSpotLight(spot);
|
|
95
|
+
const pointSlot = packLightSlot(point, { shadowAtlasLayer: 2, near: 0.1, far: 20 });
|
|
96
|
+
const spotSlot = packLightSlot(spot);
|
|
97
|
+
|
|
98
|
+
expect(pointPacked.byteLength).toBe(32);
|
|
99
|
+
expect(spotPacked.byteLength).toBe(80);
|
|
100
|
+
expect(new Uint32Array(pointSlot.buffer)[12]).toBe(0);
|
|
101
|
+
expect(new Int32Array(pointSlot.buffer)[13]).toBe(2);
|
|
102
|
+
expect(new Uint32Array(spotSlot.buffer)[12]).toBe(1);
|
|
103
|
+
expect(new Int32Array(spotSlot.buffer)[13]).toBe(-1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('keeps shared PBR view bindings for both storage and uniform light routes', () => {
|
|
107
|
+
const storageBindings = buildPbrViewBglEntries({ storageBuffer: true });
|
|
108
|
+
const uniformBindings = buildPbrViewBglEntries({ storageBuffer: false });
|
|
109
|
+
expect(storageBindings.map((entry) => entry.binding)).toEqual(
|
|
110
|
+
uniformBindings.map((entry) => entry.binding),
|
|
111
|
+
);
|
|
112
|
+
expect(storageBindings[1]?.buffer?.type).toBe('read-only-storage');
|
|
113
|
+
expect(storageBindings[2]?.buffer?.type).toBe('read-only-storage');
|
|
114
|
+
expect(uniformBindings[1]?.buffer?.type).toBe('uniform');
|
|
115
|
+
expect(uniformBindings[2]?.buffer?.type).toBe('uniform');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('keeps Point/Spot pcfKernelSize validation independent of Directional quality', () => {
|
|
119
|
+
expect(
|
|
120
|
+
validatePointLightShadowData({ mapSize: 512, nearPlane: 0.1, farPlane: 20, pcfKernelSize: 5 })
|
|
121
|
+
.ok,
|
|
122
|
+
).toBe(true);
|
|
123
|
+
expect(validateSpotLightData({ direction: [0, -1, 0], pcfKernelSize: 3 }).ok).toBe(true);
|
|
124
|
+
expect(validatePointLightShadowData({ pcfKernelSize: 2 }).ok).toBe(false);
|
|
125
|
+
expect(validateSpotLightData({ direction: [0, -1, 0], pcfKernelSize: 2 }).ok).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|