@forgeax/engine-gltf 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +204 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts +2 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-test-helpers.d.ts +5 -0
- package/dist/__tests__/bridge-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/bridge.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge.unit.test.d.ts.map +1 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts +2 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts.map +1 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts +2 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts +2 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-importer.test.d.ts +2 -0
- package/dist/__tests__/gltf-importer.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts +2 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts +2 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts +2 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts +2 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts +2 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.integration.test.d.ts +2 -0
- package/dist/__tests__/morph-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.unit.test.d.ts +2 -0
- package/dist/__tests__/morph-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key.unit.test.d.ts +2 -0
- package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
- package/dist/accessor/decode-accessor.d.ts +75 -0
- package/dist/accessor/decode-accessor.d.ts.map +1 -0
- package/dist/accessor/decode-color.d.ts +15 -0
- package/dist/accessor/decode-color.d.ts.map +1 -0
- package/dist/accessor/index.d.ts +3 -0
- package/dist/accessor/index.d.ts.map +1 -0
- package/dist/bridge.d.ts +103 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/check-extensions.d.ts +29 -0
- package/dist/check-extensions.d.ts.map +1 -0
- package/dist/cli-gltf.d.ts +8 -0
- package/dist/cli-gltf.d.ts.map +1 -0
- package/dist/cli-gltf.mjs +2435 -0
- package/dist/cli-gltf.mjs.map +1 -0
- package/dist/data-uri.d.ts +6 -0
- package/dist/data-uri.d.ts.map +1 -0
- package/dist/errors.d.ts +201 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/extract-image-bytes.d.ts +42 -0
- package/dist/extract-image-bytes.d.ts.map +1 -0
- package/dist/gltf-importer.d.ts +21 -0
- package/dist/gltf-importer.d.ts.map +1 -0
- package/dist/image-color-space.d.ts +35 -0
- package/dist/image-color-space.d.ts.map +1 -0
- package/dist/importer-entry.d.ts +10 -0
- package/dist/importer-entry.d.ts.map +1 -0
- package/dist/importer-entry.mjs +3378 -0
- package/dist/importer-entry.mjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3673 -0
- package/dist/index.mjs.map +1 -0
- package/dist/meshopt-decode.d.ts +39 -0
- package/dist/meshopt-decode.d.ts.map +1 -0
- package/dist/node-file-entry.d.ts +14 -0
- package/dist/node-file-entry.d.ts.map +1 -0
- package/dist/node-file-entry.mjs +1946 -0
- package/dist/node-file-entry.mjs.map +1 -0
- package/dist/node-path.d.ts +14 -0
- package/dist/node-path.d.ts.map +1 -0
- package/dist/parse-animation.d.ts +70 -0
- package/dist/parse-animation.d.ts.map +1 -0
- package/dist/parse-glb-chunks.d.ts +25 -0
- package/dist/parse-glb-chunks.d.ts.map +1 -0
- package/dist/parse-glb.d.ts +3 -0
- package/dist/parse-glb.d.ts.map +1 -0
- package/dist/parse-gltf-header.d.ts +18 -0
- package/dist/parse-gltf-header.d.ts.map +1 -0
- package/dist/parse-gltf.d.ts +263 -0
- package/dist/parse-gltf.d.ts.map +1 -0
- package/dist/parse-skin.d.ts +70 -0
- package/dist/parse-skin.d.ts.map +1 -0
- package/dist/reimport-reuse-meta.d.ts +48 -0
- package/dist/reimport-reuse-meta.d.ts.map +1 -0
- package/dist/serialize-meta.d.ts +2 -0
- package/dist/serialize-meta.d.ts.map +1 -0
- package/dist/source-key.d.ts +44 -0
- package/dist/source-key.d.ts.map +1 -0
- package/dist/sub-asset-key.d.ts +12 -0
- package/dist/sub-asset-key.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +2 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/dist/transform.d.ts +37 -0
- package/dist/transform.d.ts.map +1 -0
- package/package.json +92 -0
- package/src/__tests__/animation-target-errors.unit.test.ts +51 -0
- package/src/__tests__/bridge-material-samplers.unit.test.ts +67 -0
- package/src/__tests__/bridge-material-values.unit.test.ts +106 -0
- package/src/__tests__/bridge-mixed-color.unit.test.ts +100 -0
- package/src/__tests__/bridge-sampler-required.test-d.ts +59 -0
- package/src/__tests__/bridge-skin-shader-route.unit.test.ts +42 -0
- package/src/__tests__/bridge-skin-stride.unit.test.ts +115 -0
- package/src/__tests__/bridge-test-helpers.ts +9 -0
- package/src/__tests__/bridge.unit.test.ts +1227 -0
- package/src/__tests__/cli-gltf.integration.test.ts +122 -0
- package/src/__tests__/color-0-accessor.unit.test.ts +174 -0
- package/src/__tests__/data-uri-surface.unit.test.ts +26 -0
- package/src/__tests__/extension-admission.unit.test.ts +19 -0
- package/src/__tests__/fixtures/color-0/float-vec3.gltf +13 -0
- package/src/__tests__/fixtures/color-0/interleaved-vec3.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ubyte.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ushort.gltf +6 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +152 -0
- package/src/__tests__/gltf-error-migration.test.ts +147 -0
- package/src/__tests__/gltf-error-policy-owner.unit.test.ts +185 -0
- package/src/__tests__/gltf-importer.test.ts +12 -0
- package/src/__tests__/gltf-local-artifacts.test.ts +11 -0
- package/src/__tests__/gltf.unit.test.ts +2975 -0
- package/src/__tests__/malformed-buffer-data-uri.integration.test.ts +140 -0
- package/src/__tests__/material-missing-uv.unit.test.ts +29 -0
- package/src/__tests__/material-pack-refs.integration.test.ts +58 -0
- package/src/__tests__/material-per-slot-ir.unit.test.ts +90 -0
- package/src/__tests__/material-texture-transform.unit.test.ts +105 -0
- package/src/__tests__/material-unused-uv.unit.test.ts +44 -0
- package/src/__tests__/meshopt-decode.unit.test.ts +87 -0
- package/src/__tests__/meshopt-falsifier.unit.test.ts +92 -0
- package/src/__tests__/meshopt-mode-filter-owner.test-d.ts +79 -0
- package/src/__tests__/meshopt-real-matrix.integration.test.ts +370 -0
- package/src/__tests__/morph-import.integration.test.ts +115 -0
- package/src/__tests__/morph-import.unit.test.ts +62 -0
- package/src/__tests__/parse-decode.unit.test.ts +919 -0
- package/src/__tests__/parse-gltf-multi-uv.test.ts +238 -0
- package/src/__tests__/parse-gltf-skin-attrs.unit.test.ts +191 -0
- package/src/__tests__/parse-gltf.unit.test.ts +133 -0
- package/src/__tests__/pick-e2e.integration.test.ts +238 -0
- package/src/__tests__/public-surface.unit.test.ts +10 -0
- package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
- package/src/__tests__/source-key-producer.integration.test.ts +171 -0
- package/src/__tests__/source-key.unit.test.ts +159 -0
- package/src/accessor/decode-accessor.ts +309 -0
- package/src/accessor/decode-color.ts +128 -0
- package/src/accessor/index.ts +15 -0
- package/src/bridge.ts +928 -0
- package/src/check-extensions.ts +85 -0
- package/src/cli-gltf.ts +339 -0
- package/src/data-uri.ts +29 -0
- package/src/errors.ts +386 -0
- package/src/extract-image-bytes.ts +343 -0
- package/src/gltf-importer.ts +976 -0
- package/src/image-color-space.ts +104 -0
- package/src/importer-entry.ts +24 -0
- package/src/index.ts +148 -0
- package/src/meshopt-decode.ts +200 -0
- package/src/node-file-entry.ts +98 -0
- package/src/node-path.ts +41 -0
- package/src/parse-animation.ts +286 -0
- package/src/parse-glb-chunks.ts +167 -0
- package/src/parse-glb.ts +16 -0
- package/src/parse-gltf-header.ts +58 -0
- package/src/parse-gltf.ts +1731 -0
- package/src/parse-skin.ts +238 -0
- package/src/reimport-reuse-meta.ts +134 -0
- package/src/serialize-meta.ts +24 -0
- package/src/source-key.ts +106 -0
- package/src/sub-asset-key.ts +61 -0
- package/src/to-asset-pack.ts +8 -0
- package/src/transform.ts +117 -0
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeax/engine-gltf",
|
|
3
|
+
"version": "0.0.0-dev.8d955ade1c79",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"description": "Runtime glTF 2.0 importer (Tier-B subset: scenes + nodes + meshes (POSITION + INDICES) + UnlitMaterial.baseColor + Camera.perspective). Pure-function pipeline parseGlb / parseGltf / toAssetPack consumed by build-time CLI (forgeax-engine-remote-gltf import) writing <source>.meta.json (external-asset-package; assetType=gltf) for runtime loadByGuid<SceneAsset>.",
|
|
9
|
+
"bin": {
|
|
10
|
+
"forgeax-engine-remote-gltf": "./dist/cli-gltf.mjs"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./node": {
|
|
18
|
+
"types": "./dist/node-file-entry.d.ts",
|
|
19
|
+
"import": "./dist/node-file-entry.mjs"
|
|
20
|
+
},
|
|
21
|
+
"./importer": {
|
|
22
|
+
"types": "./dist/importer-entry.d.ts",
|
|
23
|
+
"import": "./dist/importer-entry.mjs"
|
|
24
|
+
},
|
|
25
|
+
"./cli-gltf": {
|
|
26
|
+
"types": "./dist/cli-gltf.d.ts",
|
|
27
|
+
"node": "./dist/cli-gltf.mjs",
|
|
28
|
+
"default": null
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/index.mjs",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@forgeax/engine-animation": "0.0.0-dev.8d955ade1c79",
|
|
42
|
+
"@forgeax/engine-geometry": "0.0.0-dev.8d955ade1c79",
|
|
43
|
+
"@forgeax/engine-import": "0.0.0-dev.8d955ade1c79",
|
|
44
|
+
"@forgeax/engine-math": "0.0.0-dev.8d955ade1c79",
|
|
45
|
+
"@forgeax/engine-pack": "0.0.0-dev.8d955ade1c79",
|
|
46
|
+
"@forgeax/engine-scene": "0.0.0-dev.8d955ade1c79",
|
|
47
|
+
"@forgeax/engine-types": "0.0.0-dev.8d955ade1c79",
|
|
48
|
+
"@webgpu/types": "^0.1.71",
|
|
49
|
+
"meshoptimizer": "^0.20.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@forgeax/engine-assets-runtime": "0.0.0-dev.8d955ade1c79",
|
|
53
|
+
"@forgeax/engine-picking": "0.0.0-dev.8d955ade1c79",
|
|
54
|
+
"@forgeax/engine-render": "0.0.0-dev.8d955ade1c79",
|
|
55
|
+
"@forgeax/engine-runtime": "0.0.0-dev.8d955ade1c79",
|
|
56
|
+
"@types/node": "^20.14.0"
|
|
57
|
+
},
|
|
58
|
+
"forgeax": {
|
|
59
|
+
"metrics": {
|
|
60
|
+
"bundle-size": {
|
|
61
|
+
"enabled": true,
|
|
62
|
+
"path": "dist/index.mjs",
|
|
63
|
+
"compression": "gzip",
|
|
64
|
+
"expected": 5865,
|
|
65
|
+
"baseline": {
|
|
66
|
+
"source": ".forgeax-harness/forgeax-loop/feat-20260515-gltf-loader-via-asset-system/plan-strategy.md#R3",
|
|
67
|
+
"threshold": 102400
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"fps": {
|
|
71
|
+
"enabled": false,
|
|
72
|
+
"reason": "build-time importer pure-function pipeline; no runtime canvas. fps reported by hello-gltf app (M5)."
|
|
73
|
+
},
|
|
74
|
+
"bench": {
|
|
75
|
+
"enabled": false,
|
|
76
|
+
"reason": "Tier-B importer is one-shot build-time work (not a hot path); bench focus stays on @forgeax/engine-math."
|
|
77
|
+
},
|
|
78
|
+
"gate": {
|
|
79
|
+
"enabled": false,
|
|
80
|
+
"reason": "no per-package binary gate; runtime parsing is consumed by @forgeax/engine-runtime + cli-gltf only, no separate enforcement scope."
|
|
81
|
+
},
|
|
82
|
+
"spike-report": {
|
|
83
|
+
"enabled": false,
|
|
84
|
+
"reason": "not a spike package."
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"scripts": {
|
|
89
|
+
"build": "tsup",
|
|
90
|
+
"test": "vitest run"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { gltfErr } from '../errors';
|
|
4
|
+
import { buildNodeParentMap, resolveNamedNodePath } from '../node-path';
|
|
5
|
+
|
|
6
|
+
describe('glTF animation target paths', () => {
|
|
7
|
+
const nodes = [
|
|
8
|
+
{ name: 'Root', children: [1] },
|
|
9
|
+
{ name: 'Hip', children: [] },
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
it('derives a stable full-path ID from a source node index', () => {
|
|
13
|
+
const parents = buildNodeParentMap(nodes);
|
|
14
|
+
const path = resolveNamedNodePath(nodes, parents, 1);
|
|
15
|
+
expect(path.ok).toBe(true);
|
|
16
|
+
if (!path.ok) return;
|
|
17
|
+
expect(path.value).toEqual(['Root', 'Hip']);
|
|
18
|
+
expect(deriveAnimationTargetId(path.value)).toBe('a95da0ec669189f98273e8f86d8ad9f2');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('uses one stable code with a reason discriminator', () => {
|
|
22
|
+
const error = gltfErr('gltf-animation-target-invalid', {
|
|
23
|
+
reason: 'name-missing',
|
|
24
|
+
animationIndex: 0,
|
|
25
|
+
channelIndex: 1,
|
|
26
|
+
nodeIndex: 1,
|
|
27
|
+
});
|
|
28
|
+
expect(error.code).toBe('gltf-animation-target-invalid');
|
|
29
|
+
expect(error.detail.reason).toBe('name-missing');
|
|
30
|
+
expect(error.hint.length).toBeGreaterThan(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('rejects cyclic node ancestry instead of looping', () => {
|
|
34
|
+
const cyclicNodes = [
|
|
35
|
+
{ name: 'A', children: [1] },
|
|
36
|
+
{ name: 'B', children: [0] },
|
|
37
|
+
] as const;
|
|
38
|
+
const path = resolveNamedNodePath(cyclicNodes, buildNodeParentMap(cyclicNodes), 0);
|
|
39
|
+
|
|
40
|
+
expect(path).toEqual({
|
|
41
|
+
ok: false,
|
|
42
|
+
reason: 'hierarchy-cycle',
|
|
43
|
+
nodeIndex: 0,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('does not reject unnamed nodes that are not animation targets', () => {
|
|
48
|
+
const parents = buildNodeParentMap([{ children: [] }]);
|
|
49
|
+
expect(parents.size).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Handle } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { toMaterialAsset } from '../bridge.js';
|
|
4
|
+
import type { GltfMaterialIr } from '../parse-gltf.js';
|
|
5
|
+
|
|
6
|
+
describe('glTF material sampler mapping', () => {
|
|
7
|
+
it('maps BLEND to straight-alpha state without depth writes', () => {
|
|
8
|
+
const asset = toMaterialAsset({
|
|
9
|
+
baseColorFactor: [1, 1, 1, 0.4],
|
|
10
|
+
metallicFactor: 0,
|
|
11
|
+
roughnessFactor: 1,
|
|
12
|
+
alphaMode: 'BLEND',
|
|
13
|
+
} as unknown as GltfMaterialIr);
|
|
14
|
+
const state = asset.passes?.[0]?.renderState as {
|
|
15
|
+
blend?: {
|
|
16
|
+
color?: { srcFactor?: string; dstFactor?: string };
|
|
17
|
+
alpha?: { srcFactor?: string; dstFactor?: string };
|
|
18
|
+
};
|
|
19
|
+
depthWriteEnabled?: boolean;
|
|
20
|
+
queue?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
expect(state.queue).toBe(3000);
|
|
24
|
+
expect(state.depthWriteEnabled).toBe(false);
|
|
25
|
+
expect(state.blend).toMatchObject({
|
|
26
|
+
color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha' },
|
|
27
|
+
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('maps each texture slot to its own sampler GUID', () => {
|
|
32
|
+
const asset = toMaterialAsset(
|
|
33
|
+
{
|
|
34
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
35
|
+
metallicFactor: 1,
|
|
36
|
+
roughnessFactor: 1,
|
|
37
|
+
baseColorTexture: { texture: 0, sampler: 10 },
|
|
38
|
+
metallicRoughnessTexture: { texture: 1, sampler: 11 },
|
|
39
|
+
normalTexture: { texture: 2, sampler: 12 },
|
|
40
|
+
occlusionTexture: { texture: 3, sampler: 13 },
|
|
41
|
+
emissiveTexture: { texture: 4, sampler: 14 },
|
|
42
|
+
} as unknown as GltfMaterialIr,
|
|
43
|
+
{
|
|
44
|
+
textureHandles: new Map([
|
|
45
|
+
[0, 100],
|
|
46
|
+
[1, 101],
|
|
47
|
+
[2, 102],
|
|
48
|
+
[3, 103],
|
|
49
|
+
[4, 104],
|
|
50
|
+
]) as unknown as ReadonlyMap<number, Handle<'TextureAsset', 'shared'>>,
|
|
51
|
+
samplerHandles: new Map([
|
|
52
|
+
[10, 200],
|
|
53
|
+
[11, 201],
|
|
54
|
+
[12, 202],
|
|
55
|
+
[13, 203],
|
|
56
|
+
[14, 204],
|
|
57
|
+
]) as unknown as ReadonlyMap<number, Handle<'SamplerAsset', 'shared'>>,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
const values = asset.values as Record<string, { sampler?: unknown }>;
|
|
61
|
+
expect(values.baseColorTexture?.sampler).toBe(200);
|
|
62
|
+
expect(values.metallicRoughnessTexture?.sampler).toBe(201);
|
|
63
|
+
expect(values.normalTexture?.sampler).toBe(202);
|
|
64
|
+
expect(values.occlusionTexture?.sampler).toBe(203);
|
|
65
|
+
expect(values.emissiveTexture?.sampler).toBe(204);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Handle } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { toMaterialAsset } from '../bridge.js';
|
|
4
|
+
import type { GltfMaterialIr } from '../parse-gltf.js';
|
|
5
|
+
|
|
6
|
+
describe('glTF material bridge values', () => {
|
|
7
|
+
it('preserves all four base-color channels for factor and texture composition', () => {
|
|
8
|
+
const factor = [0.5, 0.75, 0.25, 0.4] as const;
|
|
9
|
+
const material = toMaterialAsset({
|
|
10
|
+
baseColorFactor: factor,
|
|
11
|
+
metallicFactor: 0.5,
|
|
12
|
+
roughnessFactor: 0.7,
|
|
13
|
+
baseColorTexture: { texture: 0 },
|
|
14
|
+
} as unknown as GltfMaterialIr);
|
|
15
|
+
|
|
16
|
+
expect(material.values).toMatchObject({ baseColor: factor });
|
|
17
|
+
expect(material.values).not.toHaveProperty('baseColorAlpha');
|
|
18
|
+
expect(material.values).not.toHaveProperty('textureAlphaFactor');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
['default', undefined, 0.5],
|
|
23
|
+
['explicit', 0.73, 0.73],
|
|
24
|
+
['zero', 0, 0],
|
|
25
|
+
['one', 1, 1],
|
|
26
|
+
] as const)('projects MASK %s cutoff into the material contract', (_name, authored, expected) => {
|
|
27
|
+
const material = toMaterialAsset({
|
|
28
|
+
baseColorFactor: [1, 1, 1, 0.5],
|
|
29
|
+
metallicFactor: 0,
|
|
30
|
+
roughnessFactor: 1,
|
|
31
|
+
alphaMode: 'MASK',
|
|
32
|
+
...(authored === undefined ? {} : { alphaCutoff: authored }),
|
|
33
|
+
} as unknown as GltfMaterialIr);
|
|
34
|
+
|
|
35
|
+
expect(material.values?.alphaCutoff).toBe(expected);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('projects five structured texture slots into standard-root values', () => {
|
|
39
|
+
const material = toMaterialAsset(
|
|
40
|
+
{
|
|
41
|
+
baseColorFactor: [0.8, 0.2, 0.1, 1],
|
|
42
|
+
metallicFactor: 0.5,
|
|
43
|
+
roughnessFactor: 0.7,
|
|
44
|
+
baseColorTexture: { texture: 0, sampler: 10, texCoord: 1 },
|
|
45
|
+
metallicRoughnessTexture: { texture: 1, sampler: 11, texCoord: 2 },
|
|
46
|
+
normalTexture: { texture: 2, sampler: 12, texCoord: 3, scale: 0.6 },
|
|
47
|
+
occlusionTexture: { texture: 3, sampler: 13, texCoord: 4, strength: 0.8 },
|
|
48
|
+
emissiveTexture: { texture: 4, sampler: 14, texCoord: 5 },
|
|
49
|
+
} as unknown as GltfMaterialIr,
|
|
50
|
+
{
|
|
51
|
+
textureHandles: new Map([
|
|
52
|
+
[0, 100],
|
|
53
|
+
[1, 101],
|
|
54
|
+
[2, 102],
|
|
55
|
+
[3, 103],
|
|
56
|
+
[4, 104],
|
|
57
|
+
]) as unknown as ReadonlyMap<number, Handle<'TextureAsset', 'shared'>>,
|
|
58
|
+
samplerHandles: new Map([
|
|
59
|
+
[10, 200],
|
|
60
|
+
[11, 201],
|
|
61
|
+
[12, 202],
|
|
62
|
+
[13, 203],
|
|
63
|
+
[14, 204],
|
|
64
|
+
]) as unknown as ReadonlyMap<number, Handle<'SamplerAsset', 'shared'>>,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(material.passes?.[0]?.program.module).toBe('forgeax::default-standard-pbr');
|
|
69
|
+
expect(material.colorSpace).toBe('linear');
|
|
70
|
+
expect(material.values).toMatchObject({
|
|
71
|
+
baseColor: [0.8, 0.2, 0.1, 1],
|
|
72
|
+
metallic: 0.5,
|
|
73
|
+
roughness: 0.7,
|
|
74
|
+
baseColorTexture: { texture: 100, sampler: 200, coordinates: { set: 1 } },
|
|
75
|
+
metallicRoughnessTexture: { texture: 101, sampler: 201, coordinates: { set: 2 } },
|
|
76
|
+
normalTexture: { texture: 102, sampler: 202, coordinates: { set: 3 }, normalScale: 0.6 },
|
|
77
|
+
occlusionTexture: {
|
|
78
|
+
texture: 103,
|
|
79
|
+
sampler: 203,
|
|
80
|
+
coordinates: { set: 4 },
|
|
81
|
+
occlusionStrength: 0.8,
|
|
82
|
+
},
|
|
83
|
+
emissiveTexture: { texture: 104, sampler: 204, coordinates: { set: 5 } },
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('keeps identity texture authoring compact', () => {
|
|
88
|
+
const material = toMaterialAsset(
|
|
89
|
+
{
|
|
90
|
+
baseColorTexture: { texture: 0 },
|
|
91
|
+
metallicFactor: 0.5,
|
|
92
|
+
roughnessFactor: 0.5,
|
|
93
|
+
} as unknown as GltfMaterialIr,
|
|
94
|
+
{
|
|
95
|
+
textureHandles: new Map([[0, 100]]) as unknown as ReadonlyMap<
|
|
96
|
+
number,
|
|
97
|
+
Handle<'TextureAsset', 'shared'>
|
|
98
|
+
>,
|
|
99
|
+
samplerHandles: new Map(),
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(material.values?.baseColorTexture).toEqual({ texture: 100 });
|
|
104
|
+
expect(material.values?.baseColorTexture).not.toHaveProperty('coordinates');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { MeshAsset } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { meshIrToMeshAsset as bridgeMeshIrToMeshAsset } from '../bridge.js';
|
|
4
|
+
import type { GltfMeshIr } from '../parse-gltf.js';
|
|
5
|
+
import { unwrapMeshAsset as meshIrToMeshAsset } from './bridge-test-helpers.js';
|
|
6
|
+
|
|
7
|
+
type ColorMeshIr = GltfMeshIr & { readonly colors0?: Float32Array };
|
|
8
|
+
|
|
9
|
+
function primitive(vertexCount: number, colors0?: Float32Array): ColorMeshIr {
|
|
10
|
+
const positions = new Float32Array(vertexCount * 3);
|
|
11
|
+
for (let vertex = 0; vertex < vertexCount; vertex++) {
|
|
12
|
+
positions[vertex * 3 + 0] = vertex;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
positions,
|
|
16
|
+
indices: new Uint16Array(vertexCount === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3]),
|
|
17
|
+
materialIndex: 0,
|
|
18
|
+
meshIndex: 0,
|
|
19
|
+
...(colors0 === undefined ? {} : { colors0 }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function color(asset: MeshAsset): Float32Array | undefined {
|
|
24
|
+
return asset.attributes.color as Float32Array | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('glTF bridge COLOR_0 mixed primitive contract', () => {
|
|
28
|
+
it('takes the attributes union and fills absent primitive vertices with white', () => {
|
|
29
|
+
const colored = primitive(3, new Float32Array([1, 0.5, 0, 1, 0, 1, 0.5, 1, 0.25, 0, 1, 1]));
|
|
30
|
+
const plain = primitive(3);
|
|
31
|
+
const asset = meshIrToMeshAsset([colored, plain]);
|
|
32
|
+
expect(color(asset)).toEqual(
|
|
33
|
+
new Float32Array([
|
|
34
|
+
1, 0.5, 0, 1, 0, 1, 0.5, 1, 0.25, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
35
|
+
]),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it.each([
|
|
40
|
+
['all colored', [primitive(3, new Float32Array(12)), primitive(4, new Float32Array(16))]],
|
|
41
|
+
['all plain', [primitive(3), primitive(4)]],
|
|
42
|
+
[
|
|
43
|
+
'skin and UV combination',
|
|
44
|
+
[
|
|
45
|
+
{
|
|
46
|
+
...primitive(3, new Float32Array(12)),
|
|
47
|
+
joints0: new Uint16Array(12),
|
|
48
|
+
weights0: new Float32Array(12),
|
|
49
|
+
texcoord0: new Float32Array(6),
|
|
50
|
+
texcoord1: new Float32Array(6),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
],
|
|
54
|
+
] as const)('uses one canonical packer for %s', (_name, prims) => {
|
|
55
|
+
const asset = meshIrToMeshAsset(prims);
|
|
56
|
+
const attributes = asset.attributes as Record<string, unknown>;
|
|
57
|
+
expect(attributes.position).toBeInstanceOf(Float32Array);
|
|
58
|
+
expect(attributes.color === undefined || attributes.color instanceof Float32Array).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('keeps a missing color stream unallocated for a plain mesh', () => {
|
|
62
|
+
const asset = meshIrToMeshAsset([primitive(3)]);
|
|
63
|
+
expect(color(asset)).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns closed, typed bridge failures before publishing malformed meshes', () => {
|
|
67
|
+
const empty = bridgeMeshIrToMeshAsset([]);
|
|
68
|
+
expect(empty.ok).toBe(false);
|
|
69
|
+
if (!empty.ok) expect(empty.error.detail).toEqual({ reason: 'empty-input', primitiveCount: 0 });
|
|
70
|
+
|
|
71
|
+
const morphMismatch = bridgeMeshIrToMeshAsset([
|
|
72
|
+
{ ...primitive(3), morphTargets: [] },
|
|
73
|
+
{ ...primitive(3), morphTargets: [{ position: new Float32Array(9) }] },
|
|
74
|
+
]);
|
|
75
|
+
expect(morphMismatch.ok).toBe(false);
|
|
76
|
+
if (!morphMismatch.ok)
|
|
77
|
+
expect(morphMismatch.error.detail).toMatchObject({
|
|
78
|
+
reason: 'morph-count-mismatch',
|
|
79
|
+
expectedTargetCount: 0,
|
|
80
|
+
actualTargetCount: 1,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const colorMismatch = bridgeMeshIrToMeshAsset([primitive(3, new Float32Array(4))]);
|
|
84
|
+
expect(colorMismatch.ok).toBe(false);
|
|
85
|
+
if (!colorMismatch.ok)
|
|
86
|
+
expect(colorMismatch.error.detail).toMatchObject({
|
|
87
|
+
reason: 'color-cardinality',
|
|
88
|
+
semantic: 'COLOR_0',
|
|
89
|
+
expectedLength: 12,
|
|
90
|
+
actualLength: 4,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const layoutMismatch = bridgeMeshIrToMeshAsset([
|
|
94
|
+
{ ...primitive(3), positions: new Float32Array(4) },
|
|
95
|
+
]);
|
|
96
|
+
expect(layoutMismatch.ok).toBe(false);
|
|
97
|
+
if (!layoutMismatch.ok)
|
|
98
|
+
expect(layoutMismatch.error.detail).toMatchObject({ reason: 'layout-invalid' });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// bridge-sampler-required.test-d.ts - type-level assertions for
|
|
2
|
+
// toMaterialAsset (pass-based, feat-20260526-material-asset-multipass-renderstate M4 / w31).
|
|
3
|
+
//
|
|
4
|
+
// toMaterialAsset no longer takes a shadingModel parameter; it accepts
|
|
5
|
+
// GltfMaterialIr + optional MaterialBridgeContext and returns MaterialAsset.
|
|
6
|
+
|
|
7
|
+
import type { Handle } from '@forgeax/engine-types';
|
|
8
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
9
|
+
import type { MaterialBridgeContext } from '../bridge.js';
|
|
10
|
+
import { toMaterialAsset } from '../bridge.js';
|
|
11
|
+
import type { GltfMaterialIr } from '../parse-gltf.js';
|
|
12
|
+
|
|
13
|
+
const matIr: GltfMaterialIr = {
|
|
14
|
+
name: 'test-material',
|
|
15
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
16
|
+
metallicFactor: 0,
|
|
17
|
+
roughnessFactor: 0.5,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const textureHandles = new Map<number, Handle<'TextureAsset', 'shared'>>();
|
|
21
|
+
const samplerHandles = new Map<number, Handle<'SamplerAsset', 'shared'>>();
|
|
22
|
+
|
|
23
|
+
describe('toMaterialAsset — pass-based type-level', () => {
|
|
24
|
+
it('positive: with undefined ctx compiles', () => {
|
|
25
|
+
const result = toMaterialAsset(matIr, undefined);
|
|
26
|
+
expectTypeOf(result).toMatchTypeOf<ReturnType<typeof toMaterialAsset>>();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('positive: without ctx compiles', () => {
|
|
30
|
+
const result = toMaterialAsset(matIr);
|
|
31
|
+
expectTypeOf(result).toMatchTypeOf<ReturnType<typeof toMaterialAsset>>();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('positive: with both textureHandles and samplerHandles compiles', () => {
|
|
35
|
+
const result = toMaterialAsset(matIr, {
|
|
36
|
+
textureHandles,
|
|
37
|
+
samplerHandles,
|
|
38
|
+
});
|
|
39
|
+
expectTypeOf(result).toMatchTypeOf<ReturnType<typeof toMaterialAsset>>();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('positive: with textureHandles only compiles', () => {
|
|
43
|
+
const result = toMaterialAsset(matIr, { textureHandles });
|
|
44
|
+
expectTypeOf(result).toMatchTypeOf<ReturnType<typeof toMaterialAsset>>();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('type-level: MaterialBridgeContext has optional samplerHandles', () => {
|
|
48
|
+
const ctx: MaterialBridgeContext = { textureHandles };
|
|
49
|
+
expectTypeOf(ctx).toMatchTypeOf<MaterialBridgeContext>();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('type-level: toMaterialAsset is callable as a function', () => {
|
|
53
|
+
expectTypeOf(toMaterialAsset).toBeCallableWith(matIr);
|
|
54
|
+
expectTypeOf(toMaterialAsset).toBeCallableWith(matIr, {
|
|
55
|
+
textureHandles,
|
|
56
|
+
samplerHandles,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// bridge-skin-shader-route.unit.test.ts (feat-20260611 M4 / w17-a).
|
|
2
|
+
//
|
|
3
|
+
// Covers cooker-side auto-route: toMaterialAsset emits
|
|
4
|
+
// `forgeax::pbr-skin` when MaterialBridgeContext.skinned === true,
|
|
5
|
+
// falls back to `forgeax::default-standard-pbr` otherwise. The
|
|
6
|
+
// gltf-importer is the only call site that has the per-mesh skin
|
|
7
|
+
// presence info; routing here keeps shader choice content-driven
|
|
8
|
+
// without exposing a runtime auto-resolve (Q4) — the runtime
|
|
9
|
+
// SkinMaterialMismatchError fail-fast in render-system-extract is
|
|
10
|
+
// the reverse-direction safety net for misauthored content.
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { toMaterialAsset } from '../bridge.js';
|
|
14
|
+
import type { GltfMaterialIr } from '../parse-gltf.js';
|
|
15
|
+
|
|
16
|
+
function makeMatIr(): GltfMaterialIr {
|
|
17
|
+
return {
|
|
18
|
+
baseColorFactor: [1, 1, 1, 1] as const,
|
|
19
|
+
metallicFactor: 0.0,
|
|
20
|
+
roughnessFactor: 1.0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('toMaterialAsset shader routing (w17-a)', () => {
|
|
25
|
+
it('skinned: true -> passes[0].shader === forgeax::pbr-skin', () => {
|
|
26
|
+
const mat = makeMatIr();
|
|
27
|
+
const asset = toMaterialAsset(mat, { skinned: true });
|
|
28
|
+
expect(asset.passes?.[0]?.program.module).toBe('forgeax::pbr-skin');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('skinned: false -> passes[0].shader === forgeax::default-standard-pbr', () => {
|
|
32
|
+
const mat = makeMatIr();
|
|
33
|
+
const asset = toMaterialAsset(mat, { skinned: false });
|
|
34
|
+
expect(asset.passes?.[0]?.program.module).toBe('forgeax::default-standard-pbr');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('no flag (back-compat) -> passes[0].shader === forgeax::default-standard-pbr', () => {
|
|
38
|
+
const mat = makeMatIr();
|
|
39
|
+
const asset = toMaterialAsset(mat);
|
|
40
|
+
expect(asset.passes?.[0]?.program.module).toBe('forgeax::default-standard-pbr');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// bridge-skin-stride.unit.test.ts (feat-20260611 M2 / w9 + w10).
|
|
2
|
+
//
|
|
3
|
+
// Covers AC-08: meshIrToMeshAsset per-MeshAsset stride decision.
|
|
4
|
+
// - w9: skinned + unskinned primitives sharing one glTF mesh both walk the
|
|
5
|
+
// 18-float stride; unskinned primitive's skin slots are zero-filled.
|
|
6
|
+
// - w10: a single unskinned primitive stays on the legacy 12-float stride
|
|
7
|
+
// (OOS-9 / AC-04 — learn-render unskinned demos must not regress).
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import type { GltfMeshIr } from '../parse-gltf.js';
|
|
11
|
+
import { unwrapMeshAsset as meshIrToMeshAsset } from './bridge-test-helpers.js';
|
|
12
|
+
|
|
13
|
+
function makeUnskinnedPrim(meshIndex: number, vertexCount: number): GltfMeshIr {
|
|
14
|
+
return {
|
|
15
|
+
meshIndex,
|
|
16
|
+
materialIndex: null,
|
|
17
|
+
positions: new Float32Array(vertexCount * 3),
|
|
18
|
+
indices: new Uint16Array(Array.from({ length: vertexCount }, (_, i) => i)),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeSkinnedPrim(meshIndex: number, vertexCount: number): GltfMeshIr {
|
|
23
|
+
const joints0 = new Uint16Array(vertexCount * 4);
|
|
24
|
+
const weights0 = new Float32Array(vertexCount * 4);
|
|
25
|
+
for (let v = 0; v < vertexCount; v++) {
|
|
26
|
+
joints0[v * 4 + 0] = v;
|
|
27
|
+
joints0[v * 4 + 1] = v + 1;
|
|
28
|
+
joints0[v * 4 + 2] = v + 2;
|
|
29
|
+
joints0[v * 4 + 3] = v + 3;
|
|
30
|
+
weights0[v * 4 + 0] = 0.4;
|
|
31
|
+
weights0[v * 4 + 1] = 0.3;
|
|
32
|
+
weights0[v * 4 + 2] = 0.2;
|
|
33
|
+
weights0[v * 4 + 3] = 0.1;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
meshIndex,
|
|
37
|
+
materialIndex: null,
|
|
38
|
+
positions: new Float32Array(vertexCount * 3),
|
|
39
|
+
indices: new Uint16Array(Array.from({ length: vertexCount }, (_, i) => i)),
|
|
40
|
+
joints0,
|
|
41
|
+
weights0,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('meshIrToMeshAsset stride decision (AC-08)', () => {
|
|
46
|
+
it('skinned + unskinned primitives co-located -> 18F unified, unskinned skin slots zero-filled', () => {
|
|
47
|
+
const skinned = makeSkinnedPrim(0, 3);
|
|
48
|
+
const unskinned = makeUnskinnedPrim(0, 2);
|
|
49
|
+
const asset = meshIrToMeshAsset([skinned, unskinned]);
|
|
50
|
+
|
|
51
|
+
const totalVerts = 3 + 2;
|
|
52
|
+
expect(asset.vertices.length / 18).toBe(totalVerts);
|
|
53
|
+
expect(asset.attributes.skinIndex).toBeInstanceOf(Uint16Array);
|
|
54
|
+
expect(asset.attributes.skinWeight).toBeInstanceOf(Float32Array);
|
|
55
|
+
|
|
56
|
+
const stride = 18;
|
|
57
|
+
// Layout per deriveVertexBufferLayout (72-byte stride):
|
|
58
|
+
// pos[0..2] norm[3..5] uv[6..7] tangent[8..11]
|
|
59
|
+
// skinIndex (uint16x4) at float[12..13] (8 bytes)
|
|
60
|
+
// skinWeight (float32x4) at float[14..17] (16 bytes)
|
|
61
|
+
const interleavedU16 = new Uint16Array(asset.vertices.buffer);
|
|
62
|
+
for (let v = 0; v < 3; v++) {
|
|
63
|
+
const u16Base = (v * stride + 12) * 2;
|
|
64
|
+
expect(interleavedU16[u16Base + 0]).toBe(v);
|
|
65
|
+
expect(interleavedU16[u16Base + 1]).toBe(v + 1);
|
|
66
|
+
expect(interleavedU16[u16Base + 2]).toBe(v + 2);
|
|
67
|
+
expect(interleavedU16[u16Base + 3]).toBe(v + 3);
|
|
68
|
+
expect(asset.vertices[v * stride + 14]).toBeCloseTo(0.4, 5);
|
|
69
|
+
expect(asset.vertices[v * stride + 15]).toBeCloseTo(0.3, 5);
|
|
70
|
+
expect(asset.vertices[v * stride + 16]).toBeCloseTo(0.2, 5);
|
|
71
|
+
expect(asset.vertices[v * stride + 17]).toBeCloseTo(0.1, 5);
|
|
72
|
+
}
|
|
73
|
+
// Unskinned primitive (vertex 3..4) skin slots must be all zero.
|
|
74
|
+
for (let v = 3; v < totalVerts; v++) {
|
|
75
|
+
const u16Base = (v * stride + 12) * 2;
|
|
76
|
+
expect(interleavedU16[u16Base + 0]).toBe(0);
|
|
77
|
+
expect(interleavedU16[u16Base + 1]).toBe(0);
|
|
78
|
+
expect(interleavedU16[u16Base + 2]).toBe(0);
|
|
79
|
+
expect(interleavedU16[u16Base + 3]).toBe(0);
|
|
80
|
+
expect(asset.vertices[v * stride + 14]).toBe(0);
|
|
81
|
+
expect(asset.vertices[v * stride + 15]).toBe(0);
|
|
82
|
+
expect(asset.vertices[v * stride + 16]).toBe(0);
|
|
83
|
+
expect(asset.vertices[v * stride + 17]).toBe(0);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('meshIrToMeshAsset pure-unskinned path (AC-04 / AC-08 / OOS-9)', () => {
|
|
89
|
+
it('all primitives unskinned -> 12F stride preserved, no skinIndex/skinWeight in attributes', () => {
|
|
90
|
+
const unskinned = makeUnskinnedPrim(0, 4);
|
|
91
|
+
const asset = meshIrToMeshAsset([unskinned]);
|
|
92
|
+
|
|
93
|
+
expect(asset.vertices.length / 12).toBe(4);
|
|
94
|
+
expect(asset.attributes.skinIndex).toBeUndefined();
|
|
95
|
+
expect(asset.attributes.skinWeight).toBeUndefined();
|
|
96
|
+
// Position / normal / uv / tangent attributes still populated.
|
|
97
|
+
expect(asset.attributes.position).toBeInstanceOf(Float32Array);
|
|
98
|
+
expect(asset.attributes.normal).toBeInstanceOf(Float32Array);
|
|
99
|
+
expect(asset.attributes.uv).toBeInstanceOf(Float32Array);
|
|
100
|
+
expect(asset.attributes.tangent).toBeInstanceOf(Float32Array);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('glTF bridge uses geometry-owned packing', () => {
|
|
105
|
+
it('does not own a parallel 12F/18F stride or uv count calculation', () => {
|
|
106
|
+
const asset = meshIrToMeshAsset([makeUnskinnedPrim(0, 4)]);
|
|
107
|
+
expect(asset.vertices.length / 12).toBe(4);
|
|
108
|
+
expect(asset.attributes.position).toBeInstanceOf(Float32Array);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('keeps no-color producer output free of a synthetic color stream', () => {
|
|
112
|
+
const asset = meshIrToMeshAsset([makeUnskinnedPrim(0, 4)]);
|
|
113
|
+
expect(asset.attributes.color).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MeshAsset } from '@forgeax/engine-types';
|
|
2
|
+
import { meshIrToMeshAsset } from '../bridge.js';
|
|
3
|
+
|
|
4
|
+
/** Test-only unwrap for fixtures whose validity is the subject under test. */
|
|
5
|
+
export function unwrapMeshAsset(...args: Parameters<typeof meshIrToMeshAsset>): MeshAsset {
|
|
6
|
+
const result = meshIrToMeshAsset(...args);
|
|
7
|
+
if (!result.ok) throw result.error;
|
|
8
|
+
return result.value;
|
|
9
|
+
}
|