diff --git a/packages/model3d-viewer/src/loader.test.ts b/packages/model3d-viewer/src/loader.test.ts index a5d6cb88a..50413d648 100644 --- a/packages/model3d-viewer/src/loader.test.ts +++ b/packages/model3d-viewer/src/loader.test.ts @@ -84,6 +84,19 @@ describe('模型格式判定', () => { new TextEncoder().encode('\uFEFF{"asset":{}}'), ), ).toBe('gltf'); + // ASCII FBX(Blender 默认导出)没有 Kaydara 魔数,按开头这行认。 + expect( + resolveModel3dViewerFormatFromBytes( + new TextEncoder().encode( + '; FBX 7.4.0 project file\nFBXHeaderExtension: {', + ), + ), + ).toBe('fbx'); + expect( + resolveModel3dViewerFormatFromBytes( + new TextEncoder().encode('\n ; FBX 7.3.0 project file'), + ), + ).toBe('fbx'); expect( resolveModel3dViewerFormatFromBytes(new Uint8Array([0x00, 0x01])), ).toBeNull(); diff --git a/packages/model3d-viewer/src/loader.ts b/packages/model3d-viewer/src/loader.ts index 46707477a..f28303b8d 100644 --- a/packages/model3d-viewer/src/loader.ts +++ b/packages/model3d-viewer/src/loader.ts @@ -43,6 +43,8 @@ const MODEL3D_VIEWER_FBX_MAGIC = [ 0x4b, 0x61, 0x79, 0x64, 0x61, 0x72, 0x61, 0x20, 0x46, 0x42, 0x58, 0x20, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, ]; +/** ASCII FBX 的开头:「; FBX 7.4.0 project file」(Blender 等工具的默认导出)。 */ +const MODEL3D_VIEWER_ASCII_FBX_MAGIC = [0x3b, 0x20, 0x46, 0x42, 0x58]; const MODEL3D_VIEWER_SUPPORTED_FORMAT_TEXT = MODEL3D_VIEWER_SUPPORTED_FORMATS.join(' / '); @@ -56,18 +58,36 @@ function matchesModel3dViewerMagic( return magic.every((value, index) => bytes[index] === value); } -function isModel3dViewerJsonStart(bytes: Uint8Array) { - for (const byte of bytes) { - // BOM 与空白之后是 `{` 才当 glTF JSON:多文件 glTF 的正文就是一个 JSON 文档。 +/** 跳过 BOM 与前导空白后的正文起点;全是噪声时返回 -1。 */ +function resolveModel3dViewerContentStart(bytes: Uint8Array) { + for (let index = 0; index < bytes.length; index += 1) { + const byte = bytes[index]; if (byte === 0xef || byte === 0xbb || byte === 0xbf) { continue; } if (byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d) { continue; } - return byte === 0x7b; + return index; } - return false; + return -1; +} + +function isModel3dViewerJsonStart(bytes: Uint8Array) { + // BOM 与空白之后是 `{` 才当 glTF JSON:多文件 glTF 的正文就是一个 JSON 文档。 + const start = resolveModel3dViewerContentStart(bytes); + return start >= 0 && bytes[start] === 0x7b; +} + +/** ASCII FBX 只有「看起来像」的判据,识别不出就交给显式 format 或扩展名。 */ +function isModel3dViewerAsciiFbxStart(bytes: Uint8Array) { + const start = resolveModel3dViewerContentStart(bytes); + if (start < 0) { + return false; + } + return MODEL3D_VIEWER_ASCII_FBX_MAGIC.every( + (value, offset) => bytes[start + offset] === value, + ); } /** 声明类型里的模型格式;不是可渲染格式时返回 null。 */ @@ -96,6 +116,9 @@ export function resolveModel3dViewerFormatFromBytes( if (matchesModel3dViewerMagic(view, MODEL3D_VIEWER_FBX_MAGIC)) { return 'fbx'; } + if (isModel3dViewerAsciiFbxStart(view)) { + return 'fbx'; + } if (isModel3dViewerJsonStart(view)) { return 'gltf'; }