@forgeax/engine-runtime 0.1.21 → 0.1.23
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 +3 -3
- package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
- package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -1
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.mjs +3 -2
- package/dist/index.mjs.map +1 -1
- package/dist/sprite-param-values.d.ts +1 -1
- package/package.json +30 -30
- package/src/__tests__/animation-graph-asset-union.test-d.ts +2 -0
- package/src/__tests__/asset-brand-and-name-retirement.test.ts +4 -3
- package/src/__tests__/asset-registry-scene.test-d.ts +1 -0
- package/src/__tests__/asset.unit.test.ts +5 -2
- package/src/__tests__/collect-transient-roundtrip.test.ts +14 -3
- package/src/__tests__/components.test-d.ts +9 -6
- package/src/__tests__/components.unit.test.ts +23 -19
- package/src/__tests__/errors.unit.test.ts +3 -0
- package/src/__tests__/extract-frames-merge.test.ts +3 -0
- package/src/__tests__/helpers/register-runtime-components.ts +9 -1
- package/src/__tests__/helpers/register-scene-components.ts +9 -2
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +8 -10
- package/src/__tests__/light-snapshot.test-d.ts +27 -1
- package/src/__tests__/lights-preservation.fixup.test.ts +19 -21
- package/src/__tests__/lights.unit.test.ts +2076 -1
- package/src/__tests__/loader-name-survival.test.ts +8 -9
- package/src/__tests__/package-promotion.test.ts +7 -11
- package/src/__tests__/pbr-pipeline.unit.test.ts +16 -16
- package/src/__tests__/pipeline.unit.test.ts +20 -24
- package/src/__tests__/point-light-shadow.browser.test.ts +1 -0
- package/src/__tests__/point-light-shadow.unit.test.ts +18 -98
- package/src/__tests__/propagate-transforms.dawn.test.ts +15 -16
- package/src/__tests__/record-worldid-isolation.test.ts +16 -32
- package/src/__tests__/render-error-exhaustive.test-d.ts +10 -0
- package/src/__tests__/render-system-extract.test.ts +5 -5
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +1 -0
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +1 -0
- package/src/__tests__/renderer-surface.unit.test.ts +0 -2
- package/src/__tests__/resolveName-unified-entry.test.ts +6 -6
- package/src/__tests__/resolveName.test.ts +14 -2
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +1 -0
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +1 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +3 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +15 -4
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +1 -1
- package/src/__tests__/skin-parented-double-transform.test.ts +5 -5
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +1 -6
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -13
- package/src/__tests__/ssao-bgl.test.ts +2 -1
- package/src/__tests__/systems.unit.test.ts +25 -174
- package/src/__tests__/template-game-default-api-contract.test.ts +45 -23
- package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +10 -4
- package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +2 -2
- package/src/collect-scene-asset.ts +1 -1
- package/src/components/__tests__/transform-vec-schema.test.ts +2 -2
- package/src/createRenderer.ts +1 -0
- package/src/sprite-param-values.ts +1 -1
- package/dist/.tsbuildinfo +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -26,37 +26,37 @@
|
|
|
26
26
|
"LICENSE"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@forgeax/engine-animation": "0.1.
|
|
30
|
-
"@forgeax/engine-assets-runtime": "0.1.
|
|
31
|
-
"@forgeax/engine-audio-webaudio": "0.1.
|
|
32
|
-
"@forgeax/engine-codec": "0.1.
|
|
33
|
-
"@forgeax/engine-debug-draw": "0.1.
|
|
34
|
-
"@forgeax/engine-ecs": "0.1.
|
|
35
|
-
"@forgeax/engine-font": "0.1.
|
|
36
|
-
"@forgeax/engine-geometry": "0.1.
|
|
37
|
-
"@forgeax/engine-graphics-extras": "0.1.
|
|
38
|
-
"@forgeax/engine-image": "0.1.
|
|
39
|
-
"@forgeax/engine-input": "0.1.
|
|
40
|
-
"@forgeax/engine-math": "0.1.
|
|
41
|
-
"@forgeax/engine-pack": "0.1.
|
|
42
|
-
"@forgeax/engine-profiler": "0.1.
|
|
43
|
-
"@forgeax/engine-render": "0.1.
|
|
44
|
-
"@forgeax/engine-render-graph": "0.1.
|
|
45
|
-
"@forgeax/engine-rhi": "0.1.
|
|
46
|
-
"@forgeax/engine-rhi-null": "0.1.
|
|
47
|
-
"@forgeax/engine-rhi-webgpu": "0.1.
|
|
48
|
-
"@forgeax/engine-rhi-wgpu": "0.1.
|
|
49
|
-
"@forgeax/engine-scene": "0.1.
|
|
50
|
-
"@forgeax/engine-shader": "0.1.
|
|
51
|
-
"@forgeax/engine-skinning": "0.1.
|
|
52
|
-
"@forgeax/engine-types": "0.1.
|
|
53
|
-
"@forgeax/engine-vfx": "0.1.
|
|
29
|
+
"@forgeax/engine-animation": "0.1.23",
|
|
30
|
+
"@forgeax/engine-assets-runtime": "0.1.23",
|
|
31
|
+
"@forgeax/engine-audio-webaudio": "0.1.23",
|
|
32
|
+
"@forgeax/engine-codec": "0.1.23",
|
|
33
|
+
"@forgeax/engine-debug-draw": "0.1.23",
|
|
34
|
+
"@forgeax/engine-ecs": "0.1.23",
|
|
35
|
+
"@forgeax/engine-font": "0.1.23",
|
|
36
|
+
"@forgeax/engine-geometry": "0.1.23",
|
|
37
|
+
"@forgeax/engine-graphics-extras": "0.1.23",
|
|
38
|
+
"@forgeax/engine-image": "0.1.23",
|
|
39
|
+
"@forgeax/engine-input": "0.1.23",
|
|
40
|
+
"@forgeax/engine-math": "0.1.23",
|
|
41
|
+
"@forgeax/engine-pack": "0.1.23",
|
|
42
|
+
"@forgeax/engine-profiler": "0.1.23",
|
|
43
|
+
"@forgeax/engine-render": "0.1.23",
|
|
44
|
+
"@forgeax/engine-render-graph": "0.1.23",
|
|
45
|
+
"@forgeax/engine-rhi": "0.1.23",
|
|
46
|
+
"@forgeax/engine-rhi-null": "0.1.23",
|
|
47
|
+
"@forgeax/engine-rhi-webgpu": "0.1.23",
|
|
48
|
+
"@forgeax/engine-rhi-wgpu": "0.1.23",
|
|
49
|
+
"@forgeax/engine-scene": "0.1.23",
|
|
50
|
+
"@forgeax/engine-shader": "0.1.23",
|
|
51
|
+
"@forgeax/engine-skinning": "0.1.23",
|
|
52
|
+
"@forgeax/engine-types": "0.1.23",
|
|
53
|
+
"@forgeax/engine-vfx": "0.1.23"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@forgeax/engine-import": "0.1.
|
|
57
|
-
"@forgeax/engine-naga": "0.1.
|
|
58
|
-
"@forgeax/engine-state": "0.1.
|
|
59
|
-
"@forgeax/engine-vite-plugin-shader": "0.1.
|
|
56
|
+
"@forgeax/engine-import": "0.1.23",
|
|
57
|
+
"@forgeax/engine-naga": "0.1.23",
|
|
58
|
+
"@forgeax/engine-state": "0.1.23",
|
|
59
|
+
"@forgeax/engine-vite-plugin-shader": "0.1.23",
|
|
60
60
|
"@types/node": "^20.14.0"
|
|
61
61
|
},
|
|
62
62
|
"forgeax": {
|
|
@@ -68,6 +68,8 @@ describe('AnimationGraph POD in the Asset union (M2 / w12)', () => {
|
|
|
68
68
|
return 'video';
|
|
69
69
|
case 'particle-effect':
|
|
70
70
|
return 'particle-effect';
|
|
71
|
+
case 'ies-profile':
|
|
72
|
+
return 'ies-profile';
|
|
71
73
|
default: {
|
|
72
74
|
const _exhaustive: never = a;
|
|
73
75
|
return _exhaustive;
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// AC-05: storedNameOf retired; per-GUID stored name lives on the envelope and
|
|
4
4
|
// feeds resolveName's `storedName` argument. resolveName's three-arg
|
|
5
|
-
//
|
|
6
|
-
// no-package '' fallback /
|
|
5
|
+
// display-name derivation (stored-name precedence / package basename
|
|
6
|
+
// fallback / no-package '' fallback / promotion freeze) must remain
|
|
7
|
+
// coherent.
|
|
7
8
|
// AC-06: the 14-arm assetBrand switch retired for the ASSET_BRAND Record
|
|
8
9
|
// table. The table maps every Asset.kind to its brand, and the two
|
|
9
10
|
// consumption points (instantiate -> allocSharedRef brand arg;
|
|
@@ -19,7 +20,7 @@ function makeReg(): AssetRegistry {
|
|
|
19
20
|
|
|
20
21
|
const sampler = { kind: 'sampler' as const };
|
|
21
22
|
|
|
22
|
-
// ── AC-05: resolveName
|
|
23
|
+
// ── AC-05: resolveName remains coherent after storedNameOf retirement ──
|
|
23
24
|
|
|
24
25
|
describe('AC-05 resolveName regression (storedNameOf retired, name on envelope)', () => {
|
|
25
26
|
it('single-asset package -> basename(path), no stored name needed', () => {
|
|
@@ -784,6 +784,7 @@ function _makeStubGPU(): unknown {
|
|
|
784
784
|
'ui',
|
|
785
785
|
'audio',
|
|
786
786
|
'particle-effect',
|
|
787
|
+
'ies-profile',
|
|
787
788
|
] as const;
|
|
788
789
|
|
|
789
790
|
const UNREGISTERED_STUBS = ['shader'] as const;
|
|
@@ -795,13 +796,15 @@ function _makeStubGPU(): unknown {
|
|
|
795
796
|
for (const kind of REGISTERED_KINDS) {
|
|
796
797
|
expect(reg.get(kind), `expected loader for kind '${kind}'`).toBeDefined();
|
|
797
798
|
}
|
|
798
|
-
expect(reg.registeredKinds()).toHaveLength(
|
|
799
|
+
expect(reg.registeredKinds()).toHaveLength(18);
|
|
800
|
+
expect(reg.get('ies-profile'), 'ies-profile is wired internally').toBeDefined();
|
|
799
801
|
});
|
|
800
802
|
|
|
801
803
|
it('default set includes audio, video, and UI', () => {
|
|
802
804
|
const reg = new LoaderRegistry();
|
|
803
805
|
wireDefaultLoaders(reg);
|
|
804
|
-
expect(reg.registeredKinds()).toHaveLength(
|
|
806
|
+
expect(reg.registeredKinds()).toHaveLength(18);
|
|
807
|
+
expect(reg.get('ies-profile'), 'ies-profile is wired internally').toBeDefined();
|
|
805
808
|
expect(reg.get('video'), 'video is wired internally (graphics-extras)').toBeDefined();
|
|
806
809
|
expect(reg.get('ui'), 'ui is wired internally (engine-ui)').toBeDefined();
|
|
807
810
|
expect(reg.get('audio'), 'audio is an ordinary default asset kind').toBeDefined();
|
|
@@ -14,9 +14,15 @@ import * as SceneOwner from '@forgeax/engine-scene';
|
|
|
14
14
|
|
|
15
15
|
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
16
16
|
import type { EntityHandle } from '@forgeax/engine-ecs';
|
|
17
|
-
import { World } from '@forgeax/engine-ecs';
|
|
17
|
+
import { componentDefinition, World } from '@forgeax/engine-ecs';
|
|
18
18
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
ChildOf,
|
|
21
|
+
Children,
|
|
22
|
+
GlobalTransform,
|
|
23
|
+
propagateTransforms,
|
|
24
|
+
Transform,
|
|
25
|
+
} from '@forgeax/engine-scene';
|
|
20
26
|
import type { LocalEntityId, SceneAsset } from '@forgeax/engine-types';
|
|
21
27
|
import { describe, expect, it } from 'vitest';
|
|
22
28
|
import { rootsToSceneAsset } from '../collect-scene-asset';
|
|
@@ -26,7 +32,7 @@ import { registerSceneComponents } from './helpers/register-scene-components';
|
|
|
26
32
|
// Read the resolved world mat4 (column-major 16 floats) from the Transform
|
|
27
33
|
// world column array view.
|
|
28
34
|
function worldOf(world: World, entity: EntityHandle): Float32Array {
|
|
29
|
-
return world.get(entity,
|
|
35
|
+
return world.get(entity, GlobalTransform).unwrap().world;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
function makeRegistry(): AssetRegistry {
|
|
@@ -210,6 +216,7 @@ describe('w1 — AC-04 transient world roundtrip numeric equivalence', () => {
|
|
|
210
216
|
for (const ent of collected.value.entities) {
|
|
211
217
|
const t = (ent.components as Record<string, Record<string, unknown>>).Transform;
|
|
212
218
|
if (t !== undefined) expect('world' in t).toBe(false);
|
|
219
|
+
expect((ent.components as Record<string, unknown>).GlobalTransform).toBeUndefined();
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
// Round-trip into a fresh world.
|
|
@@ -266,4 +273,8 @@ describe('w1 — AC-04 transient world roundtrip numeric equivalence', () => {
|
|
|
266
273
|
expect(wDst[13] as number).toBeCloseTo(4, 5);
|
|
267
274
|
expect(wDst[14] as number).toBeCloseTo(5, 5);
|
|
268
275
|
});
|
|
276
|
+
|
|
277
|
+
it('declares GlobalTransform transient and reconstructs it from the authored pair', () => {
|
|
278
|
+
expect(componentDefinition(GlobalTransform).policy.transient).toBe(true);
|
|
279
|
+
});
|
|
269
280
|
});
|
|
@@ -16,18 +16,20 @@
|
|
|
16
16
|
|
|
17
17
|
import type { InputShapeOf, SchemaOf, ShapeOf } from '@forgeax/engine-ecs';
|
|
18
18
|
import type { Camera, DirectionalLight, MeshFilter, MeshRenderer } from '@forgeax/engine-render';
|
|
19
|
-
import type { ChildOf, Transform } from '@forgeax/engine-scene';
|
|
19
|
+
import type { ChildOf, GlobalTransform, Transform } from '@forgeax/engine-scene';
|
|
20
20
|
import type { Handle } from '@forgeax/engine-types';
|
|
21
21
|
import { describe, expectTypeOf, it } from 'vitest';
|
|
22
22
|
|
|
23
23
|
describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeOf', () => {
|
|
24
|
-
it('Transform data shape has 3 local inline-array fields
|
|
24
|
+
it('Transform data shape has 3 local inline-array fields', () => {
|
|
25
25
|
type Data = ShapeOf<SchemaOf<typeof Transform>>;
|
|
26
|
-
expectTypeOf<keyof Data>().toEqualTypeOf<'pos' | 'quat' | 'scale'
|
|
26
|
+
expectTypeOf<keyof Data>().toEqualTypeOf<'pos' | 'quat' | 'scale'>();
|
|
27
27
|
expectTypeOf<Data['pos']>().toEqualTypeOf<Float32Array>();
|
|
28
28
|
expectTypeOf<Data['quat']>().toEqualTypeOf<Float32Array>();
|
|
29
29
|
expectTypeOf<Data['scale']>().toEqualTypeOf<Float32Array>();
|
|
30
|
-
|
|
30
|
+
type GlobalData = ShapeOf<SchemaOf<typeof GlobalTransform>>;
|
|
31
|
+
expectTypeOf<keyof GlobalData>().toEqualTypeOf<'world'>();
|
|
32
|
+
expectTypeOf<GlobalData['world']>().toEqualTypeOf<Float32Array>();
|
|
31
33
|
// Write side widens `array<f32, N>` to also accept plain number[] literals
|
|
32
34
|
// (InputShapeOf asymmetry): `pos: [1, 2, 3]` spawns without a
|
|
33
35
|
// Float32Array wrapper at the call site.
|
|
@@ -190,12 +192,13 @@ describe('w7 type-level - component name literal types are preserved', () => {
|
|
|
190
192
|
// ────────────────────────────────────────────────────────────────────────────
|
|
191
193
|
|
|
192
194
|
describe('w15 AC-04 type constraint — ShapeOf derivation from field-payload', () => {
|
|
193
|
-
it(
|
|
195
|
+
it('ShapeOf<SchemaOf<typeof Transform>> yields local Float32Array views', () => {
|
|
194
196
|
type T = ShapeOf<SchemaOf<typeof Transform>>;
|
|
195
197
|
expectTypeOf<T['pos']>().toEqualTypeOf<Float32Array>();
|
|
196
198
|
expectTypeOf<T['quat']>().toEqualTypeOf<Float32Array>();
|
|
197
199
|
expectTypeOf<T['scale']>().toEqualTypeOf<Float32Array>();
|
|
198
|
-
|
|
200
|
+
type GlobalData = ShapeOf<SchemaOf<typeof GlobalTransform>>;
|
|
201
|
+
expectTypeOf<GlobalData['world']>().toEqualTypeOf<Float32Array>();
|
|
199
202
|
});
|
|
200
203
|
|
|
201
204
|
it("ShapeOf<SchemaOf<typeof Camera>> yields 21 number fields ('f32' -> number) + autoAspect boolean ('bool' -> boolean)", () => {
|
|
@@ -65,7 +65,13 @@ import {
|
|
|
65
65
|
Tilemap,
|
|
66
66
|
} from '@forgeax/engine-render/authoring';
|
|
67
67
|
import * as SceneOwner from '@forgeax/engine-scene';
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
ChildOf,
|
|
70
|
+
Children,
|
|
71
|
+
GlobalTransform,
|
|
72
|
+
propagateTransforms,
|
|
73
|
+
Transform,
|
|
74
|
+
} from '@forgeax/engine-scene';
|
|
69
75
|
import type { Handle, LocalEntityId, SceneAsset, SceneEntity } from '@forgeax/engine-types';
|
|
70
76
|
import { toShared } from '@forgeax/engine-types';
|
|
71
77
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
@@ -150,17 +156,17 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
150
156
|
// abstraction: components are flat-scalar SoA columns, not nested objects).
|
|
151
157
|
|
|
152
158
|
describe('w7 - 5 component schemas register through defineComponent', () => {
|
|
153
|
-
it('Transform has
|
|
159
|
+
it('Transform has the three authored local array fields', () => {
|
|
154
160
|
expect(Transform.name).toBe('Transform');
|
|
155
|
-
expect(Object.keys(componentSchema(Transform)).length).toBe(
|
|
161
|
+
expect(Object.keys(componentSchema(Transform)).length).toBe(3);
|
|
156
162
|
// pos: inline [x, y, z]
|
|
157
163
|
expect(componentSchema(Transform).pos).toBe('array<f32, 3>');
|
|
158
164
|
// quat: inline [x, y, z, w] quaternion
|
|
159
165
|
expect(componentSchema(Transform).quat).toBe('array<f32, 4>');
|
|
160
166
|
// scale: inline [x, y, z]
|
|
161
167
|
expect(componentSchema(Transform).scale).toBe('array<f32, 3>');
|
|
162
|
-
|
|
163
|
-
expect(componentSchema(
|
|
168
|
+
expect(GlobalTransform.name).toBe('GlobalTransform');
|
|
169
|
+
expect(componentSchema(GlobalTransform).world).toBe('array<f32, 16>');
|
|
164
170
|
});
|
|
165
171
|
|
|
166
172
|
it('MeshFilter has 1 shared<MeshAsset> field (assetHandle; M5 / w14)', () => {
|
|
@@ -414,9 +420,10 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
414
420
|
// file keeps an integration-shape spawn smoke for the
|
|
415
421
|
// archetype-with-Children path.
|
|
416
422
|
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
423
|
+
// The resolved world transform is the `GlobalTransform.world` mat4 column;
|
|
424
|
+
// Transform's generic ECS requirement materializes that carrier on normal
|
|
425
|
+
// spawn/add paths. Propagation + hierarchy compose coverage lives in
|
|
426
|
+
// render-system-extract.test.ts (GlobalTransform.world parent x child).
|
|
420
427
|
//
|
|
421
428
|
// charter mapping: proposition 2 (Bevy ChildOf/Children industry analog) +
|
|
422
429
|
// proposition 3 (machine-readable schema > prose: grep componentSchema(ChildOf) /
|
|
@@ -435,14 +442,10 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
435
442
|
expect((componentSchema(Children) as Record<string, unknown>).entities).toBe('array<entity>');
|
|
436
443
|
});
|
|
437
444
|
|
|
438
|
-
it('
|
|
439
|
-
|
|
440
|
-
// separate GlobalTransform component. The 3 local-TRS inline array
|
|
441
|
-
// columns (pos/quat/scale) plus the `world: array<f32, 16>` resolved
|
|
442
|
-
// mat4 are the full shape.
|
|
443
|
-
const tKeys = Object.keys(componentSchema(Transform));
|
|
445
|
+
it('GlobalTransform carries the resolved world mat4 column', () => {
|
|
446
|
+
const tKeys = Object.keys(componentSchema(GlobalTransform));
|
|
444
447
|
expect(tKeys).toContain('world');
|
|
445
|
-
expect(componentSchema(
|
|
448
|
+
expect(componentSchema(GlobalTransform).world).toBe('array<f32, 16>');
|
|
446
449
|
expect(componentSchema(Transform).pos).toBe('array<f32, 3>');
|
|
447
450
|
expect(componentSchema(Transform).quat).toBe('array<f32, 4>');
|
|
448
451
|
expect(componentSchema(Transform).scale).toBe('array<f32, 3>');
|
|
@@ -605,7 +608,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
605
608
|
//
|
|
606
609
|
// Two orthogonal guarantees after ChildOf gained its `relationship` block:
|
|
607
610
|
// AC-25 reader path unchanged -- propagateTransforms still walks ChildOf.parent
|
|
608
|
-
// upward and composes child.
|
|
611
|
+
// upward and composes child.GlobalTransform.world = parent.world x
|
|
609
612
|
// compose(child local). The migration must NOT alter this output.
|
|
610
613
|
// AC-25 mirror auto-maintained -- spawning / removing / reparenting ChildOf
|
|
611
614
|
// now auto-updates the parent's Children.entities list (the OOS-10 manual-
|
|
@@ -637,9 +640,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
637
640
|
const r = propagateTransforms(world);
|
|
638
641
|
expect(r.ok).toBe(true);
|
|
639
642
|
|
|
640
|
-
// feat-20260601: the resolved world transform lives on
|
|
643
|
+
// feat-20260601: the resolved world transform lives on GlobalTransform.world
|
|
641
644
|
// (a 16-float column-major mat4); the translation column is m[12,13,14].
|
|
642
|
-
const t = world.get(child,
|
|
645
|
+
const t = world.get(child, GlobalTransform);
|
|
643
646
|
expect(t.ok).toBe(true);
|
|
644
647
|
if (!t.ok) return;
|
|
645
648
|
const w = t.value.world;
|
|
@@ -2415,6 +2418,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
2415
2418
|
const world = new World();
|
|
2416
2419
|
const e = world.spawn({ component: Transform, data: {} }).unwrap();
|
|
2417
2420
|
const row = world.get(e, Transform).unwrap();
|
|
2421
|
+
const global = world.get(e, GlobalTransform).unwrap();
|
|
2418
2422
|
|
|
2419
2423
|
// local pos/quat/scale inline array columns at identity.
|
|
2420
2424
|
expect(Array.from(row.pos)).toEqual([0, 0, 0]);
|
|
@@ -2423,7 +2427,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
2423
2427
|
|
|
2424
2428
|
// world array<f32,16> resolves to a Float32Array view of 16 contiguous
|
|
2425
2429
|
// floats. Default fill is the identity mat4 (column-major).
|
|
2426
|
-
const w =
|
|
2430
|
+
const w = global.world as Float32Array;
|
|
2427
2431
|
expect(w).toBeInstanceOf(Float32Array);
|
|
2428
2432
|
expect(w.length).toBe(16);
|
|
2429
2433
|
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
@@ -547,6 +547,8 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
547
547
|
return 'points lines budget exceeded';
|
|
548
548
|
case 'points-lines-prepare-failed':
|
|
549
549
|
return 'points lines prepare failed';
|
|
550
|
+
case 'light-resource-unavailable':
|
|
551
|
+
return 'light resource unavailable';
|
|
550
552
|
case 'transmission-capability-missing':
|
|
551
553
|
return 'transmission capability missing';
|
|
552
554
|
case 'projector-binding-failed':
|
|
@@ -719,6 +721,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
719
721
|
return 'ok';
|
|
720
722
|
case 'points-lines-prepare-failed':
|
|
721
723
|
return 'ok';
|
|
724
|
+
case 'light-resource-unavailable':
|
|
722
725
|
case 'transmission-capability-missing':
|
|
723
726
|
return 'ok';
|
|
724
727
|
case 'projector-binding-failed':
|
|
@@ -161,11 +161,13 @@ function simulateMerge(
|
|
|
161
161
|
// AC-04: lights — point[]/spot[] concat; directional first-hit in worlds[] order; directionalCount sum
|
|
162
162
|
const point: PointLightSnapshot[] = [];
|
|
163
163
|
const spot: SpotLightSnapshot[] = [];
|
|
164
|
+
const rect: ExtractedLights['rect'][number][] = [];
|
|
164
165
|
let directional: DirectionalLightSnapshot | undefined;
|
|
165
166
|
let directionalCount = 0;
|
|
166
167
|
for (const f of frames) {
|
|
167
168
|
for (const p of f.lights.point) point.push(p);
|
|
168
169
|
for (const s of f.lights.spot) spot.push(s);
|
|
170
|
+
for (const r of f.lights.rect) rect.push(r);
|
|
169
171
|
if (directional === undefined && f.lights.directional !== undefined) {
|
|
170
172
|
directional = f.lights.directional;
|
|
171
173
|
}
|
|
@@ -179,6 +181,7 @@ function simulateMerge(
|
|
|
179
181
|
directionalCount,
|
|
180
182
|
point,
|
|
181
183
|
spot,
|
|
184
|
+
rect,
|
|
182
185
|
lightViewProj: firstDirectionalLights?.lightViewProj,
|
|
183
186
|
splitPlanes: firstDirectionalLights?.splitPlanes,
|
|
184
187
|
cascadeCount: firstDirectionalLights?.cascadeCount,
|
|
@@ -24,7 +24,14 @@ import {
|
|
|
24
24
|
TileLayer,
|
|
25
25
|
Tilemap,
|
|
26
26
|
} from '@forgeax/engine-render/authoring';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
ChildOf,
|
|
29
|
+
Children,
|
|
30
|
+
GlobalTransform,
|
|
31
|
+
MorphWeights,
|
|
32
|
+
Name,
|
|
33
|
+
Transform,
|
|
34
|
+
} from '@forgeax/engine-scene';
|
|
28
35
|
|
|
29
36
|
const RUNTIME_COMPONENTS: readonly Component[] = [
|
|
30
37
|
Camera,
|
|
@@ -51,6 +58,7 @@ const RUNTIME_COMPONENTS: readonly Component[] = [
|
|
|
51
58
|
TileLayer,
|
|
52
59
|
Tilemap,
|
|
53
60
|
Transform,
|
|
61
|
+
GlobalTransform,
|
|
54
62
|
VideoPlayer,
|
|
55
63
|
Visibility,
|
|
56
64
|
];
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { Component, World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { SceneInstance } from '@forgeax/engine-render';
|
|
3
|
-
import { ChildOf, Children, Transform } from '@forgeax/engine-scene';
|
|
3
|
+
import { ChildOf, Children, GlobalTransform, Transform } from '@forgeax/engine-scene';
|
|
4
4
|
|
|
5
5
|
/** Register the scene vocabulary explicitly for an isolated test World. */
|
|
6
6
|
export function registerSceneComponents(world: World, extras: readonly Component[] = []): void {
|
|
7
|
-
for (const component of [
|
|
7
|
+
for (const component of [
|
|
8
|
+
SceneInstance,
|
|
9
|
+
ChildOf,
|
|
10
|
+
Children,
|
|
11
|
+
Transform,
|
|
12
|
+
GlobalTransform,
|
|
13
|
+
...extras,
|
|
14
|
+
]) {
|
|
8
15
|
world.components.register(component).unwrap();
|
|
9
16
|
}
|
|
10
17
|
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// lock the four light GPU pack paths' byte output so the collapse is proven
|
|
8
8
|
// byte-neutral end to end:
|
|
9
9
|
// 1. View UBO (DirectionalLight direction/color lanes) -> writeViewUbo
|
|
10
|
-
// 2. PointLightBuffer (color lanes) ->
|
|
11
|
-
// 3. SpotLightBuffer (direction + color lanes) ->
|
|
10
|
+
// 2. PointLightBuffer (color lanes) -> packDirectLightSlot
|
|
11
|
+
// 3. SpotLightBuffer (direction + color lanes) -> packDirectLightSlot
|
|
12
12
|
// 4. Skylight uniform/ambient (color destructure) -> SkylightSnapshot
|
|
13
13
|
// (Camera clear pass is M3, not here.)
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ import { vec3 } from '@forgeax/engine-math';
|
|
|
16
16
|
import type { Buffer, RhiQueue } from '@forgeax/engine-rhi';
|
|
17
17
|
import { ok } from '@forgeax/engine-rhi';
|
|
18
18
|
import { describe, expect, it } from 'vitest';
|
|
19
|
-
import {
|
|
19
|
+
import { packDirectLightSlot } from '../../../render/src/light-buffer-layout';
|
|
20
20
|
import { writeViewUbo } from '../../../render/src/record/view-ubo';
|
|
21
21
|
import type { CameraSnapshot } from '../../../render/src/render-contract';
|
|
22
22
|
import type {
|
|
@@ -103,7 +103,7 @@ describe('w7 -- light GPU pack byte-neutral (AC-11)', () => {
|
|
|
103
103
|
expect(payload[22]).toBeCloseTo(0.7, 5);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
it('
|
|
106
|
+
it('packDirectLightSlot writes Point color into slots 4..6 byte-for-byte (80 B)', () => {
|
|
107
107
|
const snap: PointLightSnapshot = {
|
|
108
108
|
kind: 'point',
|
|
109
109
|
position: vec3.create(1.5, -2.25, 0.125),
|
|
@@ -111,14 +111,14 @@ describe('w7 -- light GPU pack byte-neutral (AC-11)', () => {
|
|
|
111
111
|
intensity: 2,
|
|
112
112
|
invRangeSquared: 0.04,
|
|
113
113
|
};
|
|
114
|
-
const out =
|
|
115
|
-
expect(out.byteLength).toBe(
|
|
114
|
+
const out = packDirectLightSlot(snap);
|
|
115
|
+
expect(out.byteLength).toBe(80);
|
|
116
116
|
expect(out[4]).toBeCloseTo(0.4, 6);
|
|
117
117
|
expect(out[5]).toBeCloseTo(0.5, 6);
|
|
118
118
|
expect(out[6]).toBeCloseTo(0.6, 6);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
it('
|
|
121
|
+
it('packDirectLightSlot writes Spot color slots 4..6 + direction slots 8..10 byte-for-byte (80 B)', () => {
|
|
122
122
|
const snap: SpotLightSnapshot = {
|
|
123
123
|
kind: 'spot',
|
|
124
124
|
position: vec3.create(0, 5, 0),
|
|
@@ -135,9 +135,7 @@ describe('w7 -- light GPU pack byte-neutral (AC-11)', () => {
|
|
|
135
135
|
farPlane: 50,
|
|
136
136
|
shadowAtlasTile: -1,
|
|
137
137
|
};
|
|
138
|
-
const out =
|
|
139
|
-
// SpotLight carries projector receiver controls and the shadow intensity
|
|
140
|
-
// tail in the current std430 payload.
|
|
138
|
+
const out = packDirectLightSlot(snap);
|
|
141
139
|
expect(out.byteLength).toBe(80);
|
|
142
140
|
expect(out[4]).toBeCloseTo(0.3, 6);
|
|
143
141
|
expect(out[5]).toBeCloseTo(0.4, 6);
|
|
@@ -23,11 +23,16 @@
|
|
|
23
23
|
import { expectTypeOf, test } from 'vitest';
|
|
24
24
|
import type { DirectionalShadowQuality } from '../../../render/src/components/directional-shadow-filter';
|
|
25
25
|
import type { LightValidationError } from '../../../render/src/components/light-helpers';
|
|
26
|
+
import type {
|
|
27
|
+
DirectLightSlotKind,
|
|
28
|
+
DirectLightSnapshot,
|
|
29
|
+
} from '../../../render/src/light-buffer-layout';
|
|
26
30
|
import type {
|
|
27
31
|
DirectionalLightSnapshot,
|
|
28
32
|
ExtractedLights,
|
|
29
33
|
LightSnapshot,
|
|
30
34
|
PointLightSnapshot,
|
|
35
|
+
RectAreaDirectLightSnapshot,
|
|
31
36
|
SpotLightSnapshot,
|
|
32
37
|
} from '../../../render/src/render-system-extract';
|
|
33
38
|
|
|
@@ -46,6 +51,8 @@ function consumeSnapshot(s: LightSnapshot): number {
|
|
|
46
51
|
return s.invRangeSquared;
|
|
47
52
|
case 'spot':
|
|
48
53
|
return s.cosInner + s.cosOuter;
|
|
54
|
+
case 'rect-area':
|
|
55
|
+
return s.halfWidth + s.halfHeight;
|
|
49
56
|
default:
|
|
50
57
|
return assertNever(s);
|
|
51
58
|
}
|
|
@@ -53,7 +60,7 @@ function consumeSnapshot(s: LightSnapshot): number {
|
|
|
53
60
|
|
|
54
61
|
test('LightSnapshot is a discriminated union on kind', () => {
|
|
55
62
|
expectTypeOf<LightSnapshot>().toEqualTypeOf<
|
|
56
|
-
DirectionalLightSnapshot | PointLightSnapshot | SpotLightSnapshot
|
|
63
|
+
DirectionalLightSnapshot | PointLightSnapshot | SpotLightSnapshot | RectAreaDirectLightSnapshot
|
|
57
64
|
>();
|
|
58
65
|
// Smoke that the consumer compiles (run-time noop; compile-time exhaustive).
|
|
59
66
|
void consumeSnapshot;
|
|
@@ -83,6 +90,25 @@ test('ExtractedLights three-bucket shape', () => {
|
|
|
83
90
|
expectTypeOf<ExtractedLights['directionalCount']>().toEqualTypeOf<number>();
|
|
84
91
|
expectTypeOf<ExtractedLights['point']>().toEqualTypeOf<readonly PointLightSnapshot[]>();
|
|
85
92
|
expectTypeOf<ExtractedLights['spot']>().toEqualTypeOf<readonly SpotLightSnapshot[]>();
|
|
93
|
+
expectTypeOf<ExtractedLights['rect']>().toEqualTypeOf<readonly RectAreaDirectLightSnapshot[]>();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
function consumeDirectLight(snapshot: DirectLightSnapshot): number {
|
|
97
|
+
switch (snapshot.kind) {
|
|
98
|
+
case 'point':
|
|
99
|
+
return snapshot.invRangeSquared;
|
|
100
|
+
case 'spot':
|
|
101
|
+
return snapshot.cosInner + snapshot.cosOuter;
|
|
102
|
+
case 'rect-area':
|
|
103
|
+
return snapshot.halfWidth + snapshot.halfHeight;
|
|
104
|
+
default:
|
|
105
|
+
return assertNever(snapshot);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test('DirectLightSlot kind is closed and includes only finite direct lights', () => {
|
|
110
|
+
expectTypeOf<DirectLightSlotKind>().toEqualTypeOf<'point' | 'spot' | 'rect-area'>();
|
|
111
|
+
void consumeDirectLight;
|
|
86
112
|
expectTypeOf<ExtractedLights['directionalShadowQuality']>().toEqualTypeOf<
|
|
87
113
|
DirectionalShadowQuality | undefined
|
|
88
114
|
>();
|
|
@@ -9,11 +9,7 @@ import {
|
|
|
9
9
|
validatePointLightShadowData,
|
|
10
10
|
validateSpotLightData,
|
|
11
11
|
} from '../../../render/src/components/light-helpers';
|
|
12
|
-
import {
|
|
13
|
-
packLightSlot,
|
|
14
|
-
packPointLight,
|
|
15
|
-
packSpotLight,
|
|
16
|
-
} from '../../../render/src/light-buffer-layout';
|
|
12
|
+
import { packDirectLightSlot } from '../../../render/src/light-buffer-layout';
|
|
17
13
|
import { buildPbrViewBglEntries } from '../../../render/src/pbr-pipeline';
|
|
18
14
|
import type {
|
|
19
15
|
PointLightSnapshot,
|
|
@@ -87,32 +83,34 @@ describe('Point/Spot and shared-light regression guard', () => {
|
|
|
87
83
|
expect(degToCos(60)).toBeCloseTo(0.5, 8);
|
|
88
84
|
});
|
|
89
85
|
|
|
90
|
-
it('keeps Point/Spot packing
|
|
86
|
+
it('keeps Point/Spot packing in the shared DirectLightSlot owner', () => {
|
|
91
87
|
const point = pointSnapshot();
|
|
92
88
|
const spot = spotSnapshot();
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const pointSlot = packLightSlot(point, { shadowAtlasLayer: 2, near: 0.1, far: 20 });
|
|
96
|
-
const spotSlot = packLightSlot(spot);
|
|
89
|
+
const pointSlot = packDirectLightSlot({ ...point, shadowAtlasLayer: 2 });
|
|
90
|
+
const spotSlot = packDirectLightSlot(spot);
|
|
97
91
|
|
|
98
|
-
expect(
|
|
99
|
-
expect(
|
|
100
|
-
expect(new Uint32Array(pointSlot.buffer)[
|
|
101
|
-
expect(new
|
|
102
|
-
expect(new Uint32Array(spotSlot.buffer)[
|
|
103
|
-
expect(new
|
|
92
|
+
expect(pointSlot.byteLength).toBe(80);
|
|
93
|
+
expect(spotSlot.byteLength).toBe(80);
|
|
94
|
+
expect(new Uint32Array(pointSlot.buffer)[16]).toBe(0);
|
|
95
|
+
expect(new Uint32Array(pointSlot.buffer)[17]).toBe(2);
|
|
96
|
+
expect(new Uint32Array(spotSlot.buffer)[16]).toBe(1);
|
|
97
|
+
expect(new Uint32Array(spotSlot.buffer)[17]).toBe(0xffffffff);
|
|
104
98
|
});
|
|
105
99
|
|
|
106
|
-
it('keeps
|
|
100
|
+
it('keeps the PBR view layout independent from cluster light storage capability', () => {
|
|
107
101
|
const storageBindings = buildPbrViewBglEntries({ storageBuffer: true });
|
|
108
102
|
const uniformBindings = buildPbrViewBglEntries({ storageBuffer: false });
|
|
109
103
|
expect(storageBindings.map((entry) => entry.binding)).toEqual(
|
|
110
104
|
uniformBindings.map((entry) => entry.binding),
|
|
111
105
|
);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
// Local lights are consumed through the cluster payload in group(2). The
|
|
107
|
+
// shared view group therefore has no legacy direct-light storage/uniform
|
|
108
|
+
// entries at bindings 1/2, and its shape cannot drift with the capability
|
|
109
|
+
// route selected for the cluster buffers.
|
|
110
|
+
expect(storageBindings.find((entry) => entry.binding === 1)).toBeUndefined();
|
|
111
|
+
expect(storageBindings.find((entry) => entry.binding === 2)).toBeUndefined();
|
|
112
|
+
expect(uniformBindings.find((entry) => entry.binding === 1)).toBeUndefined();
|
|
113
|
+
expect(uniformBindings.find((entry) => entry.binding === 2)).toBeUndefined();
|
|
116
114
|
});
|
|
117
115
|
|
|
118
116
|
it('keeps Point/Spot pcfKernelSize validation independent of Directional quality', () => {
|