@forgeax/engine-fbx 0.1.4 → 0.1.7

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.
@@ -12,13 +12,10 @@ import { box3 } from '@forgeax/engine-math';
12
12
  import { AssetGuid } from '@forgeax/engine-pack/guid';
13
13
  import type {
14
14
  AnimationClipPod,
15
- AssetCodec,
16
15
  AssetRef,
17
16
  ImportedAsset,
18
17
  MaterialAsset,
19
18
  MaterialPod,
20
- MaterialTextureBindingPod,
21
- MaterialTextureValue,
22
19
  MeshAsset,
23
20
  MeshMaterialSlot,
24
21
  MeshMaterialSlotTopologyEntry,
@@ -28,7 +25,6 @@ import type {
28
25
  SkeletonPod,
29
26
  SkinPod,
30
27
  SourceOverrideMap,
31
- TextureAsset,
32
28
  TexturePod,
33
29
  } from '@forgeax/engine-types';
34
30
  import {
@@ -346,61 +342,11 @@ export function buildMeshAsset(
346
342
  };
347
343
  }
348
344
 
349
- export interface FbxDecodedTexture {
350
- readonly texture: TextureAsset;
351
- readonly bytes: Uint8Array;
352
- readonly mediaType?: string;
353
- readonly assetCodec?: AssetCodec;
354
- }
355
-
356
- function materialBindings(pod: MaterialPod): readonly MaterialTextureBindingPod[] {
357
- if (pod.textureBindings !== undefined) return pod.textureBindings;
358
- const bindings: MaterialTextureBindingPod[] = [];
359
- const legacy: readonly [MaterialTextureBindingPod['slot'], number | undefined][] = [
360
- ['baseColorTexture', pod.baseColorTextureIndex],
361
- ['metallicRoughnessTexture', pod.metallicRoughnessTextureIndex],
362
- ['normalTexture', pod.normalTextureIndex],
363
- ['specularTintTexture', pod.specularTintTextureIndex],
364
- ['emissiveTexture', pod.emissiveTextureIndex],
365
- ['occlusionTexture', pod.occlusionTextureIndex],
366
- ];
367
- for (const [slot, textureIndex] of legacy) {
368
- if (textureIndex !== undefined) bindings.push({ slot, textureIndex });
369
- }
370
- return bindings;
371
- }
372
-
373
- function buildMaterialAsset(
374
- pod: MaterialPod,
375
- guid: string,
376
- skinned = false,
377
- textureGuidByIndex: ReadonlyMap<number, string> = new Map(),
378
- ): ImportedAsset {
345
+ function buildMaterialAsset(pod: MaterialPod, guid: string, skinned = false): ImportedAsset {
379
346
  // A material consumed by a skinned mesh must select the pbr-skin shader so the
380
347
  // runtime PSO chain (LayoutKind 'pbr-skin' + 18-float vertex layout + joint
381
348
  // palette) is exercised; the render-system fail-fasts otherwise (mirror of
382
349
  // gltfImporter's `skinned` routing).
383
- const values: Record<string, NonNullable<MaterialAsset['values']>[string]> = {
384
- baseColor: pod.baseColorFactor as readonly [number, number, number, number],
385
- metallic: pod.metallicFactor,
386
- roughness: pod.roughnessFactor,
387
- };
388
- const refs: AssetRef[] = [];
389
- for (const binding of materialBindings(pod)) {
390
- const textureGuid = textureGuidByIndex.get(binding.textureIndex);
391
- if (textureGuid === undefined) continue;
392
- const textureValue: MaterialTextureValue = {
393
- // Runtime material loading interprets texture handles as indexes into
394
- // this asset's refs[]; the GUID is carried by the corresponding edge.
395
- texture: refs.length as unknown as MaterialTextureValue['texture'],
396
- ...(binding.texCoord === undefined ? {} : { coordinates: { set: binding.texCoord } }),
397
- };
398
- values[binding.slot] = textureValue;
399
- refs.push({
400
- guid: textureGuid,
401
- sourceField: { componentName: '<material>', fieldName: binding.slot },
402
- });
403
- }
404
350
  const mat: MaterialAsset = {
405
351
  kind: 'material',
406
352
  colorSpace: 'linear',
@@ -411,14 +357,18 @@ function buildMaterialAsset(
411
357
  renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
412
358
  },
413
359
  ],
414
- values,
360
+ values: {
361
+ baseColor: pod.baseColorFactor as readonly [number, number, number, number],
362
+ metallic: pod.metallicFactor,
363
+ roughness: pod.roughnessFactor,
364
+ },
415
365
  };
416
366
  return {
417
367
  guid,
418
368
  kind: 'material',
419
369
  ...(pod.name !== undefined ? { name: pod.name } : {}),
420
370
  payload: mat,
421
- refs,
371
+ refs: [],
422
372
  artifacts: {},
423
373
  };
424
374
  }
@@ -434,7 +384,7 @@ interface SceneBuildContext {
434
384
  readonly refs: readonly string[];
435
385
  /**
436
386
  * Skin GUIDs (inline strings) injected into the SceneAsset payload and the
437
- * scene envelope's refs[] so the typed scene load pulls each
387
+ * scene envelope's refs[] so the runtime recursive loadByGuid walk pulls each
438
388
  * SkinAsset on the browser-async pack-fetch path. Mirrors gltf-importer:
439
389
  * stored as GUID strings (the on-disk round-trip + parseScenePayload's
440
390
  * resolveSkinGuids accept both shapes).
@@ -528,26 +478,18 @@ function buildSceneAsset(pod: ScenePod, guid: string, ctx: SceneBuildContext): I
528
478
  };
529
479
  }
530
480
 
531
- function buildTextureAsset(
532
- pod: TexturePod,
533
- guid: string,
534
- decoded: FbxDecodedTexture,
535
- ): ImportedAsset {
481
+ // M3: TextureAsset requires decoded pixel data — deferred to M4.
482
+ // TexturePod.filePath is preserved for diagnostics during M3.
483
+ function buildTextureNote(_pod: TexturePod, _guid: string): ImportedAsset {
484
+ // Produce a minimal placeholder; real texture import (decode + upload)
485
+ // lands with M4 material parsing.
536
486
  return {
537
- guid,
487
+ guid: _guid,
538
488
  kind: 'texture',
539
- ...(pod.name !== undefined ? { name: pod.name } : {}),
540
- payload: decoded.texture,
489
+ ...(_pod.name !== undefined ? { name: _pod.name } : {}),
490
+ payload: {} as never,
541
491
  refs: [],
542
- artifacts: {
543
- body: {
544
- mediaType: decoded.mediaType?.startsWith('image/ktx2')
545
- ? decoded.mediaType
546
- : 'application/x-forgeax-rgba8',
547
- ...(decoded.assetCodec === undefined ? {} : { assetCodec: decoded.assetCodec }),
548
- bytes: decoded.bytes,
549
- },
550
- },
492
+ artifacts: {},
551
493
  };
552
494
  }
553
495
 
@@ -561,14 +503,9 @@ export function toAssetPack(params: {
561
503
  readonly animationClips: readonly AnimationClipPod[];
562
504
  readonly subAssets: readonly SubAsset[];
563
505
  readonly sourceOverrides?: SourceOverrideMap;
564
- readonly decodedTextures?: ReadonlyMap<number, FbxDecodedTexture>;
565
506
  }): readonly ImportedAsset[] {
566
507
  const assets: ImportedAsset[] = [];
567
508
  const guidOf = makeGuidResolver(params.subAssets);
568
- const textureGuidByIndex = new Map<number, string>();
569
- for (const texture of params.subAssets) {
570
- if (texture.kind === 'texture') textureGuidByIndex.set(texture.sourceIndex, texture.guid);
571
- }
572
509
  const materialGuidByIndex = new Map<number, string>();
573
510
  const materialNameByIndex = new Map<number, string>();
574
511
  const materialSourceKeyByIndex = new Map<number, string>();
@@ -642,20 +579,12 @@ export function toAssetPack(params: {
642
579
  if (!mat) continue;
643
580
  const guid = guidOf('material', i);
644
581
  // Single-mesh fixtures: any material is consumed by the skinned mesh.
645
- if (guid !== undefined) {
646
- assets.push(buildMaterialAsset(mat, guid, hasSkin, textureGuidByIndex));
647
- }
582
+ if (guid !== undefined) assets.push(buildMaterialAsset(mat, guid, hasSkin));
648
583
  }
649
584
 
650
585
  for (const tex of params.textures) {
651
586
  const guid = guidOf('texture', tex.sourceIndex);
652
- if (guid !== undefined) {
653
- const decoded = params.decodedTextures?.get(tex.sourceIndex);
654
- if (decoded === undefined) {
655
- throw new Error(`fbx texture ${tex.sourceIndex} was declared but not decoded`);
656
- }
657
- assets.push(buildTextureAsset(tex, guid, decoded));
658
- }
587
+ if (guid !== undefined) assets.push(buildTextureNote(tex, guid));
659
588
  }
660
589
 
661
590
  // Scene refs[] ordering (mirror of gltfImporter): only direct scene
@@ -1,9 +0,0 @@
1
- import { type AssetRegistry } from '@forgeax/engine-assets-runtime';
2
- import { type DdcPack } from '@forgeax/engine-import';
3
- import type { AssetDecoder, MeshAsset } from '@forgeax/engine-types';
4
- type FixtureAsset = DdcPack['assets'][number];
5
- export declare const normalizedMeshAssetDecoder: AssetDecoder<MeshAsset>;
6
- export declare function installMeshDecoder(assets: AssetRegistry): import("@forgeax/engine-types").AssetDecoderLease;
7
- export declare function createAssetRuntimeFixture(sourceAssets: readonly FixtureAsset[]): AssetRegistry;
8
- export {};
9
- //# sourceMappingURL=asset-runtime-fixture.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"asset-runtime-fixture.d.ts","sourceRoot":"","sources":["../../src/__tests__/asset-runtime-fixture.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,aAAa,EAGnB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAE,KAAK,OAAO,EAAoB,MAAM,wBAAwB,CAAC;AACxE,OAAO,KAAK,EACV,YAAY,EAEZ,SAAS,EAIV,MAAM,uBAAuB,CAAC;AAwB/B,KAAK,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AA4F9C,eAAO,MAAM,0BAA0B,EAAE,YAAY,CAAC,SAAS,CAqB9D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,qDAEvD;AAwCD,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,GAAG,aAAa,CAsD9F"}
@@ -1,251 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import {
3
- type AssetRegistry,
4
- createAssetRegistry,
5
- createCatalogSource,
6
- } from '@forgeax/engine-assets-runtime';
7
- import { meshAssetDecoder, meshAssetKind } from '@forgeax/engine-geometry';
8
- import { type DdcPack, normaliseForPack } from '@forgeax/engine-import';
9
- import type {
10
- AssetDecoder,
11
- AssetLoadError,
12
- MeshAsset,
13
- MorphTarget,
14
- Result,
15
- VertexAttributeMap,
16
- } from '@forgeax/engine-types';
17
- import { err } from '@forgeax/engine-types';
18
-
19
- const SCOPE_ID = 'fbx-test-scope';
20
- const GENERATION = 1;
21
- const PACK_DIGEST = `sha256:${'4'.repeat(64)}`;
22
- const OUTPUT_SET_DIGEST = `sha256:${'5'.repeat(64)}`;
23
- const OUTPUT_DIGEST = `sha256:${'6'.repeat(64)}`;
24
- const ATTRIBUTE_KEYS = [
25
- 'position',
26
- 'normal',
27
- 'uv',
28
- 'tangent',
29
- 'skinIndex',
30
- 'skinWeight',
31
- 'uv1',
32
- 'uv2',
33
- 'uv3',
34
- 'uv4',
35
- 'uv5',
36
- 'uv6',
37
- 'uv7',
38
- ] as const;
39
-
40
- type FixtureAsset = DdcPack['assets'][number];
41
-
42
- function sha256(bytes: Uint8Array): string {
43
- return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
44
- }
45
-
46
- function floatArray(value: unknown): Float32Array | undefined {
47
- if (value instanceof Float32Array) return value;
48
- if (value instanceof ArrayBuffer) return new Float32Array(value);
49
- if (ArrayBuffer.isView(value))
50
- return new Float32Array(Array.from(value as unknown as ArrayLike<number>));
51
- if (Array.isArray(value)) return new Float32Array(value as number[]);
52
- return undefined;
53
- }
54
-
55
- function indexArray(value: unknown): Uint16Array | Uint32Array | undefined {
56
- if (value instanceof Uint16Array || value instanceof Uint32Array) return value;
57
- if (ArrayBuffer.isView(value) || Array.isArray(value)) {
58
- const values = Array.from(value as unknown as ArrayLike<number>);
59
- return values.some((entry) => entry > 0xffff)
60
- ? new Uint32Array(values)
61
- : new Uint16Array(values);
62
- }
63
- return undefined;
64
- }
65
-
66
- function integerAttribute(value: unknown): Uint16Array | undefined {
67
- if (value instanceof Uint16Array) return value;
68
- if (Array.isArray(value) || ArrayBuffer.isView(value)) {
69
- return new Uint16Array(Array.from(value as unknown as ArrayLike<number>));
70
- }
71
- return undefined;
72
- }
73
-
74
- function morphTarget(value: unknown): MorphTarget | undefined {
75
- if (value === null || typeof value !== 'object') return undefined;
76
- const source = value as Record<string, unknown>;
77
- const position = floatArray(source.position);
78
- const normal = floatArray(source.normal);
79
- const tangent = floatArray(source.tangent);
80
- return {
81
- ...(position === undefined ? {} : { position }),
82
- ...(normal === undefined ? {} : { normal }),
83
- ...(tangent === undefined ? {} : { tangent }),
84
- };
85
- }
86
-
87
- function meshPayload(value: unknown): MeshAsset | undefined {
88
- if (value === null || typeof value !== 'object') return undefined;
89
- const source = value as Record<string, unknown>;
90
- const rawAttributes = source.attributes;
91
- if (
92
- source.kind !== 'mesh' ||
93
- floatArray(source.vertices) === undefined ||
94
- rawAttributes === null ||
95
- typeof rawAttributes !== 'object' ||
96
- !Array.isArray(source.submeshes) ||
97
- !Array.isArray(source.materialSlots)
98
- ) {
99
- return undefined;
100
- }
101
- const attributes: VertexAttributeMap = {};
102
- const sourceAttributes = rawAttributes as Record<string, unknown>;
103
- for (const key of ATTRIBUTE_KEYS) {
104
- const raw = sourceAttributes[key];
105
- if (raw === undefined) continue;
106
- const converted = key === 'skinIndex' ? integerAttribute(raw) : floatArray(raw);
107
- if (converted !== undefined) attributes[key] = converted;
108
- }
109
- const morphWeights = floatArray(source.morphWeights);
110
- const morphTargets = Array.isArray(source.morphTargets)
111
- ? source.morphTargets.flatMap((target) => {
112
- const converted = morphTarget(target);
113
- return converted === undefined ? [] : [converted];
114
- })
115
- : undefined;
116
- const vertices = floatArray(source.vertices) as Float32Array;
117
- const indices = indexArray(source.indices);
118
- const aabb = floatArray(source.aabb);
119
- return {
120
- kind: 'mesh',
121
- vertices,
122
- ...(indices === undefined ? {} : { indices }),
123
- attributes,
124
- ...(aabb === undefined ? {} : { aabb }),
125
- submeshes: source.submeshes as MeshAsset['submeshes'],
126
- materialSlots: source.materialSlots as MeshAsset['materialSlots'],
127
- ...(morphTargets === undefined ? {} : { morphTargets }),
128
- ...(morphWeights === undefined ? {} : { morphWeights }),
129
- };
130
- }
131
-
132
- export const normalizedMeshAssetDecoder: AssetDecoder<MeshAsset> = {
133
- async decode(input): Promise<Result<MeshAsset, AssetLoadError>> {
134
- const body = input.envelope.artifacts.body;
135
- if (body !== undefined) {
136
- const bytes = await input.artifacts.read(body);
137
- if (!bytes.ok) return bytes;
138
- }
139
- const payload = meshPayload(input.envelope.payload);
140
- if (payload === undefined) {
141
- return err({
142
- code: 'asset-package-invalid',
143
- expected: 'a JSON-safe mesh payload that can be owned by Geometry',
144
- hint: 'recook the mesh payload with typed vertex and attribute fields',
145
- detail: { guid: input.envelope.guid, reason: 'mesh payload normalization failed' },
146
- });
147
- }
148
- return meshAssetDecoder.decode({
149
- ...input,
150
- envelope: { ...input.envelope, payload },
151
- });
152
- },
153
- };
154
-
155
- export function installMeshDecoder(assets: AssetRegistry) {
156
- return assets.installDecoder(meshAssetKind, normalizedMeshAssetDecoder);
157
- }
158
-
159
- function row(asset: FixtureAsset, packageUrl: string) {
160
- const sourcePath = `fixtures/${asset.guid}`;
161
- return {
162
- guid: asset.guid,
163
- packageUrl,
164
- kind: asset.kind,
165
- sourcePath,
166
- revision: { rootId: 'fbx-fixture', digest: 'sha256:catalog', observedAt: 1 },
167
- publication: {
168
- schemaVersion: 'asset-publication/1' as const,
169
- sourcePath,
170
- sourceRevision: 'fbx-fixture-source',
171
- generation: GENERATION,
172
- digest: PACK_DIGEST,
173
- outputSetDigest: OUTPUT_SET_DIGEST,
174
- outputs: [
175
- {
176
- guid: asset.guid,
177
- sourceKey: asset.sourceKey ?? asset.guid,
178
- kind: asset.kind,
179
- digest: OUTPUT_DIGEST,
180
- refs: asset.refs,
181
- },
182
- ],
183
- receipt: {
184
- schemaVersion: 'asset-publication-receipt/1' as const,
185
- sourcePath,
186
- sourceRevision: 'fbx-fixture-source',
187
- inputFingerprint: 'fbx-fixture-input',
188
- outputDigest: OUTPUT_DIGEST,
189
- outputSetDigest: OUTPUT_SET_DIGEST,
190
- externalEvidence: [],
191
- },
192
- externalEvidence: [],
193
- },
194
- };
195
- }
196
-
197
- export function createAssetRuntimeFixture(sourceAssets: readonly FixtureAsset[]): AssetRegistry {
198
- const packageUrl = 'https://assets.test/fbx-fixture.pack.json';
199
- const artifactBytes = new Map<string, Uint8Array>();
200
- const runtimeAssets = sourceAssets.map((asset) => {
201
- const artifacts = Object.fromEntries(
202
- Object.entries(asset.artifacts).map(([key, body]) => {
203
- const path = `${asset.guid}/${key}.bin`;
204
- const url = new URL(path, packageUrl).toString();
205
- artifactBytes.set(url, body.bytes);
206
- return [
207
- key,
208
- {
209
- path,
210
- mediaType: body.mediaType,
211
- contentEncoding: 'identity' as const,
212
- byteLength: body.bytes.byteLength,
213
- integrity: { algorithm: 'sha256' as const, digest: sha256(body.bytes) },
214
- ...(body.assetCodec === undefined ? {} : { assetCodec: body.assetCodec }),
215
- },
216
- ];
217
- }),
218
- );
219
- return {
220
- guid: asset.guid,
221
- kind: asset.kind,
222
- ...(asset.name === undefined ? {} : { name: asset.name }),
223
- payload: normaliseForPack(asset.payload) as Record<string, unknown>,
224
- refs: asset.refs,
225
- artifacts,
226
- };
227
- });
228
- const pack = {
229
- schemaVersion: '2.0.0' as const,
230
- kind: 'internal-text-package' as const,
231
- scopeId: SCOPE_ID,
232
- generation: GENERATION,
233
- digest: PACK_DIGEST,
234
- outputSetDigest: OUTPUT_SET_DIGEST,
235
- assets: runtimeAssets,
236
- };
237
- const fetcher: typeof globalThis.fetch = async (input) => {
238
- const url = String(input);
239
- if (url === packageUrl) return new Response(JSON.stringify(pack));
240
- const bytes = artifactBytes.get(url);
241
- return bytes === undefined
242
- ? new Response('not found', { status: 404 })
243
- : new Response(bytes as unknown as BodyInit);
244
- };
245
- return createAssetRegistry({
246
- catalog: createCatalogSource({ entries: sourceAssets.map((asset) => row(asset, packageUrl)) }),
247
- fetcher,
248
- scopeId: SCOPE_ID,
249
- generation: GENERATION,
250
- });
251
- }