@carbonenginejs/runtime-resource 0.1.0
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/FORMAT-PROVENANCE.md +135 -0
- package/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +241 -0
- package/dist/CjsMotherLode.js +56 -0
- package/dist/CjsMotherLode.js.map +1 -0
- package/dist/CjsResMan.js +740 -0
- package/dist/CjsResMan.js.map +1 -0
- package/dist/CjsResManQueue.js +214 -0
- package/dist/CjsResManQueue.js.map +1 -0
- package/dist/CjsResource.js +366 -0
- package/dist/CjsResource.js.map +1 -0
- package/dist/CjsResourceSource.js +59 -0
- package/dist/CjsResourceSource.js.map +1 -0
- package/dist/_virtual/_rollupPluginBabelHelpers.js +153 -0
- package/dist/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
- package/dist/format/CjsFormat.js +178 -0
- package/dist/format/CjsFormat.js.map +1 -0
- package/dist/format/CjsResourceProbe.js +269 -0
- package/dist/format/CjsResourceProbe.js.map +1 -0
- package/dist/format/payloadContract.js +173 -0
- package/dist/format/payloadContract.js.map +1 -0
- package/dist/formats/black/CjsBlackFormat.js +272 -0
- package/dist/formats/black/CjsBlackFormat.js.map +1 -0
- package/dist/formats/black/core/CjsBlackBinaryReader.js +153 -0
- package/dist/formats/black/core/CjsBlackBinaryReader.js.map +1 -0
- package/dist/formats/black/core/CjsBlackPropertyReaders.js +382 -0
- package/dist/formats/black/core/CjsBlackPropertyReaders.js.map +1 -0
- package/dist/formats/black/core/CjsBlackReader.js +760 -0
- package/dist/formats/black/core/CjsBlackReader.js.map +1 -0
- package/dist/formats/black/core/CjsBlackSchemaRegistry.js +433 -0
- package/dist/formats/black/core/CjsBlackSchemaRegistry.js.map +1 -0
- package/dist/formats/black/core/black-schema-v1-2026-07-11.json.js +4 -0
- package/dist/formats/black/core/black-schema-v1-2026-07-11.json.js.map +1 -0
- package/dist/formats/black/core/blackConstants.js +7 -0
- package/dist/formats/black/core/blackConstants.js.map +1 -0
- package/dist/formats/black/core/blackDefinitions.js +10 -0
- package/dist/formats/black/core/blackDefinitions.js.map +1 -0
- package/dist/formats/black/core/blackEnums.js +6 -0
- package/dist/formats/black/core/blackEnums.js.map +1 -0
- package/dist/formats/black/core/blackSchema.js +3 -0
- package/dist/formats/black/core/blackSchema.js.map +1 -0
- package/dist/formats/black/core/blackVersion.js +22 -0
- package/dist/formats/black/core/blackVersion.js.map +1 -0
- package/dist/formats/black/core/helpers.js +182 -0
- package/dist/formats/black/core/helpers.js.map +1 -0
- package/dist/formats/black/core/schema.js +4 -0
- package/dist/formats/black/core/schema.js.map +1 -0
- package/dist/formats/black/index.js +2 -0
- package/dist/formats/black/index.js.map +1 -0
- package/dist/formats/bnk/CjsBnkFormat.js +245 -0
- package/dist/formats/bnk/CjsBnkFormat.js.map +1 -0
- package/dist/formats/bnk/core/helpers.js +354 -0
- package/dist/formats/bnk/core/helpers.js.map +1 -0
- package/dist/formats/bnk/core/soundbanksInfo.js +246 -0
- package/dist/formats/bnk/core/soundbanksInfo.js.map +1 -0
- package/dist/formats/bnk/index.js +3 -0
- package/dist/formats/bnk/index.js.map +1 -0
- package/dist/formats/cmf/CjsCmfFormat.js +491 -0
- package/dist/formats/cmf/CjsCmfFormat.js.map +1 -0
- package/dist/formats/cmf/core/binary.js +118 -0
- package/dist/formats/cmf/core/binary.js.map +1 -0
- package/dist/formats/cmf/core/buffers.js +233 -0
- package/dist/formats/cmf/core/buffers.js.map +1 -0
- package/dist/formats/cmf/core/constants.js +47 -0
- package/dist/formats/cmf/core/constants.js.map +1 -0
- package/dist/formats/cmf/core/gr2Anim.js +453 -0
- package/dist/formats/cmf/core/gr2Anim.js.map +1 -0
- package/dist/formats/cmf/core/helpers.js +287 -0
- package/dist/formats/cmf/core/helpers.js.map +1 -0
- package/dist/formats/cmf/core/pack.js +276 -0
- package/dist/formats/cmf/core/pack.js.map +1 -0
- package/dist/formats/cmf/core/schema.js +364 -0
- package/dist/formats/cmf/core/schema.js.map +1 -0
- package/dist/formats/cmf/core/shared.js +268 -0
- package/dist/formats/cmf/core/shared.js.map +1 -0
- package/dist/formats/cmf/core/writer.js +517 -0
- package/dist/formats/cmf/core/writer.js.map +1 -0
- package/dist/formats/cmf/index.js +2 -0
- package/dist/formats/cmf/index.js.map +1 -0
- package/dist/formats/dds/CjsDdsFormat.js +187 -0
- package/dist/formats/dds/CjsDdsFormat.js.map +1 -0
- package/dist/formats/dds/core/helpers.js +812 -0
- package/dist/formats/dds/core/helpers.js.map +1 -0
- package/dist/formats/dds/index.js +2 -0
- package/dist/formats/dds/index.js.map +1 -0
- package/dist/formats/fbx/CjsFbxFormat.js +260 -0
- package/dist/formats/fbx/CjsFbxFormat.js.map +1 -0
- package/dist/formats/fbx/core/helpers.js +3901 -0
- package/dist/formats/fbx/core/helpers.js.map +1 -0
- package/dist/formats/fbx/index.js +2 -0
- package/dist/formats/fbx/index.js.map +1 -0
- package/dist/formats/flac/CjsFlacFormat.js +81 -0
- package/dist/formats/flac/CjsFlacFormat.js.map +1 -0
- package/dist/formats/flac/core/helpers.js +295 -0
- package/dist/formats/flac/core/helpers.js.map +1 -0
- package/dist/formats/flac/index.js +2 -0
- package/dist/formats/flac/index.js.map +1 -0
- package/dist/formats/gif/CjsGifFormat.js +81 -0
- package/dist/formats/gif/CjsGifFormat.js.map +1 -0
- package/dist/formats/gif/core/helpers.js +360 -0
- package/dist/formats/gif/core/helpers.js.map +1 -0
- package/dist/formats/gif/index.js +2 -0
- package/dist/formats/gif/index.js.map +1 -0
- package/dist/formats/gltf/CjsGltfFormat.js +284 -0
- package/dist/formats/gltf/CjsGltfFormat.js.map +1 -0
- package/dist/formats/gltf/core/helpers.js +292 -0
- package/dist/formats/gltf/core/helpers.js.map +1 -0
- package/dist/formats/gltf/core/json.js +79 -0
- package/dist/formats/gltf/core/json.js.map +1 -0
- package/dist/formats/gltf/core/parser.js +666 -0
- package/dist/formats/gltf/core/parser.js.map +1 -0
- package/dist/formats/gltf/core/targets.js +163 -0
- package/dist/formats/gltf/core/targets.js.map +1 -0
- package/dist/formats/gltf/index.js +2 -0
- package/dist/formats/gltf/index.js.map +1 -0
- package/dist/formats/index.js +23 -0
- package/dist/formats/index.js.map +1 -0
- package/dist/formats/jpeg/CjsJpegFormat.js +200 -0
- package/dist/formats/jpeg/CjsJpegFormat.js.map +1 -0
- package/dist/formats/jpeg/core/helpers.js +350 -0
- package/dist/formats/jpeg/core/helpers.js.map +1 -0
- package/dist/formats/jpeg/core/jpeg.js +377 -0
- package/dist/formats/jpeg/core/jpeg.js.map +1 -0
- package/dist/formats/jpeg/index.js +2 -0
- package/dist/formats/jpeg/index.js.map +1 -0
- package/dist/formats/mp3/CjsMp3Format.js +186 -0
- package/dist/formats/mp3/CjsMp3Format.js.map +1 -0
- package/dist/formats/mp3/core/helpers.js +338 -0
- package/dist/formats/mp3/core/helpers.js.map +1 -0
- package/dist/formats/mp3/index.js +2 -0
- package/dist/formats/mp3/index.js.map +1 -0
- package/dist/formats/mp4/CjsMp4Format.js +185 -0
- package/dist/formats/mp4/CjsMp4Format.js.map +1 -0
- package/dist/formats/mp4/core/helpers.js +449 -0
- package/dist/formats/mp4/core/helpers.js.map +1 -0
- package/dist/formats/mp4/index.js +2 -0
- package/dist/formats/mp4/index.js.map +1 -0
- package/dist/formats/obj/CjsObjFormat.js +247 -0
- package/dist/formats/obj/CjsObjFormat.js.map +1 -0
- package/dist/formats/obj/core/helpers.js +573 -0
- package/dist/formats/obj/core/helpers.js.map +1 -0
- package/dist/formats/obj/core/json.js +64 -0
- package/dist/formats/obj/core/json.js.map +1 -0
- package/dist/formats/obj/core/parser.js +321 -0
- package/dist/formats/obj/core/parser.js.map +1 -0
- package/dist/formats/obj/index.js +2 -0
- package/dist/formats/obj/index.js.map +1 -0
- package/dist/formats/ogg/CjsOggFormat.js +82 -0
- package/dist/formats/ogg/CjsOggFormat.js.map +1 -0
- package/dist/formats/ogg/core/helpers.js +387 -0
- package/dist/formats/ogg/core/helpers.js.map +1 -0
- package/dist/formats/ogg/core/imdct.js +178 -0
- package/dist/formats/ogg/core/imdct.js.map +1 -0
- package/dist/formats/ogg/core/vorbis.js +999 -0
- package/dist/formats/ogg/core/vorbis.js.map +1 -0
- package/dist/formats/ogg/index.js +2 -0
- package/dist/formats/ogg/index.js.map +1 -0
- package/dist/formats/png/CjsPngFormat.js +189 -0
- package/dist/formats/png/CjsPngFormat.js.map +1 -0
- package/dist/formats/png/core/helpers.js +583 -0
- package/dist/formats/png/core/helpers.js.map +1 -0
- package/dist/formats/png/index.js +2 -0
- package/dist/formats/png/index.js.map +1 -0
- package/dist/formats/red/CjsRedFormat.js +247 -0
- package/dist/formats/red/CjsRedFormat.js.map +1 -0
- package/dist/formats/red/core/CjsRedReader.js +201 -0
- package/dist/formats/red/core/CjsRedReader.js.map +1 -0
- package/dist/formats/red/core/blackDefinitions.js +3 -0
- package/dist/formats/red/core/blackDefinitions.js.map +1 -0
- package/dist/formats/red/core/helpers.js +136 -0
- package/dist/formats/red/core/helpers.js.map +1 -0
- package/dist/formats/red/core/redGraph.js +68 -0
- package/dist/formats/red/core/redGraph.js.map +1 -0
- package/dist/formats/red/core/schema.js +4 -0
- package/dist/formats/red/core/schema.js.map +1 -0
- package/dist/formats/red/index.js +2 -0
- package/dist/formats/red/index.js.map +1 -0
- package/dist/formats/stl/CjsStlFormat.js +304 -0
- package/dist/formats/stl/CjsStlFormat.js.map +1 -0
- package/dist/formats/stl/core/helpers.js +257 -0
- package/dist/formats/stl/core/helpers.js.map +1 -0
- package/dist/formats/stl/core/json.js +51 -0
- package/dist/formats/stl/core/json.js.map +1 -0
- package/dist/formats/stl/core/stl.js +544 -0
- package/dist/formats/stl/core/stl.js.map +1 -0
- package/dist/formats/stl/core/targets.js +163 -0
- package/dist/formats/stl/core/targets.js.map +1 -0
- package/dist/formats/stl/index.js +2 -0
- package/dist/formats/stl/index.js.map +1 -0
- package/dist/formats/tga/CjsTgaFormat.js +186 -0
- package/dist/formats/tga/CjsTgaFormat.js.map +1 -0
- package/dist/formats/tga/core/helpers.js +446 -0
- package/dist/formats/tga/core/helpers.js.map +1 -0
- package/dist/formats/tga/index.js +2 -0
- package/dist/formats/tga/index.js.map +1 -0
- package/dist/formats/wav/CjsWavFormat.js +186 -0
- package/dist/formats/wav/CjsWavFormat.js.map +1 -0
- package/dist/formats/wav/core/helpers.js +328 -0
- package/dist/formats/wav/core/helpers.js.map +1 -0
- package/dist/formats/wav/index.js +2 -0
- package/dist/formats/wav/index.js.map +1 -0
- package/dist/formats/webm/CjsWebmFormat.js +185 -0
- package/dist/formats/webm/CjsWebmFormat.js.map +1 -0
- package/dist/formats/webm/core/helpers.js +537 -0
- package/dist/formats/webm/core/helpers.js.map +1 -0
- package/dist/formats/webm/index.js +2 -0
- package/dist/formats/webm/index.js.map +1 -0
- package/dist/formats/webp/CjsWebpFormat.js +80 -0
- package/dist/formats/webp/CjsWebpFormat.js.map +1 -0
- package/dist/formats/webp/core/helpers.js +214 -0
- package/dist/formats/webp/core/helpers.js.map +1 -0
- package/dist/formats/webp/index.js +2 -0
- package/dist/formats/webp/index.js.map +1 -0
- package/dist/formats/wem/CjsWemFormat.js +197 -0
- package/dist/formats/wem/CjsWemFormat.js.map +1 -0
- package/dist/formats/wem/core/bitStream.js +259 -0
- package/dist/formats/wem/core/bitStream.js.map +1 -0
- package/dist/formats/wem/core/codebookLibrary.js +164 -0
- package/dist/formats/wem/core/codebookLibrary.js.map +1 -0
- package/dist/formats/wem/core/helpers.js +336 -0
- package/dist/formats/wem/core/helpers.js.map +1 -0
- package/dist/formats/wem/core/packedCodebooksAotuv603.js +30 -0
- package/dist/formats/wem/core/packedCodebooksAotuv603.js.map +1 -0
- package/dist/formats/wem/core/wemToOgg.js +485 -0
- package/dist/formats/wem/core/wemToOgg.js.map +1 -0
- package/dist/formats/wem/index.js +2 -0
- package/dist/formats/wem/index.js.map +1 -0
- package/dist/formats/yaml/CjsYamlFormat.js +77 -0
- package/dist/formats/yaml/CjsYamlFormat.js.map +1 -0
- package/dist/formats/yaml/core/CjsYamlReader.js +304 -0
- package/dist/formats/yaml/core/CjsYamlReader.js.map +1 -0
- package/dist/formats/yaml/core/helpers.js +160 -0
- package/dist/formats/yaml/core/helpers.js.map +1 -0
- package/dist/formats/yaml/index.js +2 -0
- package/dist/formats/yaml/index.js.map +1 -0
- package/dist/generated/resources/AudioGeometryResData.js +47 -0
- package/dist/generated/resources/AudioGeometryResData.js.map +1 -0
- package/dist/generated/resources/CmfVertexReader.js +46 -0
- package/dist/generated/resources/CmfVertexReader.js.map +1 -0
- package/dist/generated/resources/GStateBindingCallbackData.js +31 -0
- package/dist/generated/resources/GStateBindingCallbackData.js.map +1 -0
- package/dist/generated/resources/MeshDecalData.js +37 -0
- package/dist/generated/resources/MeshDecalData.js.map +1 -0
- package/dist/generated/resources/MeshDecalLodData.js +34 -0
- package/dist/generated/resources/MeshDecalLodData.js.map +1 -0
- package/dist/generated/resources/Tr2AsyncSave.js +46 -0
- package/dist/generated/resources/Tr2AsyncSave.js.map +1 -0
- package/dist/generated/resources/Tr2CmfContents.js +37 -0
- package/dist/generated/resources/Tr2CmfContents.js.map +1 -0
- package/dist/generated/resources/Tr2GrannyIntersectionResult.js +58 -0
- package/dist/generated/resources/Tr2GrannyIntersectionResult.js.map +1 -0
- package/dist/generated/resources/Tr2LoadPrepareFence.js +37 -0
- package/dist/generated/resources/Tr2LoadPrepareFence.js.map +1 -0
- package/dist/generated/resources/Tr2MaterialArea.js +34 -0
- package/dist/generated/resources/Tr2MaterialArea.js.map +1 -0
- package/dist/generated/resources/Tr2MaterialMesh.js +31 -0
- package/dist/generated/resources/Tr2MaterialMesh.js.map +1 -0
- package/dist/generated/resources/Tr2MaterialRes.js +34 -0
- package/dist/generated/resources/Tr2MaterialRes.js.map +1 -0
- package/dist/generated/resources/Tr2ShaderPermutation.js +43 -0
- package/dist/generated/resources/Tr2ShaderPermutation.js.map +1 -0
- package/dist/generated/resources/Tr2TextureLodManager.js +60 -0
- package/dist/generated/resources/Tr2TextureLodManager.js.map +1 -0
- package/dist/generated/resources/Tr2TextureLodUpdateRequest.js +37 -0
- package/dist/generated/resources/Tr2TextureLodUpdateRequest.js.map +1 -0
- package/dist/generated/resources/Tr2TexturePackChannel.js +37 -0
- package/dist/generated/resources/Tr2TexturePackChannel.js.map +1 -0
- package/dist/generated/resources/Tr2TexturePipeline.js +44 -0
- package/dist/generated/resources/Tr2TexturePipeline.js.map +1 -0
- package/dist/generated/resources/Tr2TexturePipelineParams.js +34 -0
- package/dist/generated/resources/Tr2TexturePipelineParams.js.map +1 -0
- package/dist/generated/resources/Tr2TexturePipelineStepCompress.js +40 -0
- package/dist/generated/resources/Tr2TexturePipelineStepCompress.js.map +1 -0
- package/dist/generated/resources/Tr2TexturePipelineStepLimitSize.js +34 -0
- package/dist/generated/resources/Tr2TexturePipelineStepLimitSize.js.map +1 -0
- package/dist/generated/resources/Tr2TexturePipelineStepLoad.js +31 -0
- package/dist/generated/resources/Tr2TexturePipelineStepLoad.js.map +1 -0
- package/dist/generated/resources/Tr2TexturePipelineStepPack.js +43 -0
- package/dist/generated/resources/Tr2TexturePipelineStepPack.js.map +1 -0
- package/dist/generated/resources/TriGeometryResAreaData.js +59 -0
- package/dist/generated/resources/TriGeometryResAreaData.js.map +1 -0
- package/dist/generated/resources/TriGeometryResJointData.js +38 -0
- package/dist/generated/resources/TriGeometryResJointData.js.map +1 -0
- package/dist/generated/resources/TriGeometryResLodData.js +88 -0
- package/dist/generated/resources/TriGeometryResLodData.js.map +1 -0
- package/dist/generated/resources/TriGeometryResMeshData.js +63 -0
- package/dist/generated/resources/TriGeometryResMeshData.js.map +1 -0
- package/dist/generated/resources/TriGeometryResSkeletonData.js +34 -0
- package/dist/generated/resources/TriGeometryResSkeletonData.js.map +1 -0
- package/dist/generated/resources/TriJointBinding.js +38 -0
- package/dist/generated/resources/TriJointBinding.js.map +1 -0
- package/dist/generated/resources/TriMorphTargetGeometryConstants.js +46 -0
- package/dist/generated/resources/TriMorphTargetGeometryConstants.js.map +1 -0
- package/dist/generated/resources/TriRtGeometryConstants.js +88 -0
- package/dist/generated/resources/TriRtGeometryConstants.js.map +1 -0
- package/dist/generated/resources/enums.js +18 -0
- package/dist/generated/resources/enums.js.map +1 -0
- package/dist/generated/resources/index.js +32 -0
- package/dist/generated/resources/index.js.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/resourcePath.js +18 -0
- package/dist/resourcePath.js.map +1 -0
- package/dist/resourceStates.js +9 -0
- package/dist/resourceStates.js.map +1 -0
- package/dist/resources/Tr2EffectRes.js +71 -0
- package/dist/resources/Tr2EffectRes.js.map +1 -0
- package/dist/resources/Tr2GrannyStateRes.js +44 -0
- package/dist/resources/Tr2GrannyStateRes.js.map +1 -0
- package/dist/resources/Tr2ImageRes.js +114 -0
- package/dist/resources/Tr2ImageRes.js.map +1 -0
- package/dist/resources/Tr2LightProfileRes.js +40 -0
- package/dist/resources/Tr2LightProfileRes.js.map +1 -0
- package/dist/resources/TriGeometryRes.js +216 -0
- package/dist/resources/TriGeometryRes.js.map +1 -0
- package/dist/resources/TriGrannyRes.js +43 -0
- package/dist/resources/TriGrannyRes.js.map +1 -0
- package/dist/resources/TriTextureRes.js +304 -0
- package/dist/resources/TriTextureRes.js.map +1 -0
- package/dist/resources/resourceBoundary.js +47 -0
- package/dist/resources/resourceBoundary.js.map +1 -0
- package/dist/texture/CjsTextureArrayRes.js +406 -0
- package/dist/texture/CjsTextureArrayRes.js.map +1 -0
- package/dist/texture/CjsTextureParameterProxy.js +133 -0
- package/dist/texture/CjsTextureParameterProxy.js.map +1 -0
- package/format-notices/black/LICENSE +21 -0
- package/format-notices/black/NOTICE +47 -0
- package/format-notices/bnk/LICENSE +21 -0
- package/format-notices/bnk/NOTICE +17 -0
- package/format-notices/cmf/LICENSE +21 -0
- package/format-notices/cmf/NOTICE +36 -0
- package/format-notices/dds/LICENSE +21 -0
- package/format-notices/dds/NOTICE +14 -0
- package/format-notices/fbx/LICENSE +21 -0
- package/format-notices/fbx/NOTICE +14 -0
- package/format-notices/flac/LICENSE +21 -0
- package/format-notices/flac/NOTICE +14 -0
- package/format-notices/gif/LICENSE +21 -0
- package/format-notices/gif/NOTICE +14 -0
- package/format-notices/gltf/LICENSE +21 -0
- package/format-notices/gltf/NOTICE +27 -0
- package/format-notices/jpeg/LICENSE +21 -0
- package/format-notices/jpeg/NOTICE +14 -0
- package/format-notices/mp3/LICENSE +21 -0
- package/format-notices/mp3/NOTICE +14 -0
- package/format-notices/mp4/LICENSE +21 -0
- package/format-notices/mp4/NOTICE +14 -0
- package/format-notices/obj/LICENSE +21 -0
- package/format-notices/obj/NOTICE +26 -0
- package/format-notices/ogg/LICENSE +21 -0
- package/format-notices/ogg/NOTICE +28 -0
- package/format-notices/png/LICENSE +21 -0
- package/format-notices/png/NOTICE +14 -0
- package/format-notices/red/LICENSE +21 -0
- package/format-notices/red/NOTICE +31 -0
- package/format-notices/stl/LICENSE +21 -0
- package/format-notices/stl/NOTICE +21 -0
- package/format-notices/tga/LICENSE +21 -0
- package/format-notices/tga/NOTICE +14 -0
- package/format-notices/wav/LICENSE +21 -0
- package/format-notices/wav/NOTICE +14 -0
- package/format-notices/webm/LICENSE +21 -0
- package/format-notices/webm/NOTICE +14 -0
- package/format-notices/webp/LICENSE +21 -0
- package/format-notices/webp/NOTICE +14 -0
- package/format-notices/wem/LICENSE +57 -0
- package/format-notices/wem/NOTICE +33 -0
- package/format-notices/yaml/LICENSE +21 -0
- package/format-notices/yaml/NOTICE +44 -0
- package/package.json +51 -0
- package/resource-lifecycle.md +487 -0
|
@@ -0,0 +1,3901 @@
|
|
|
1
|
+
import { generateTangents, generateBiNormals, generateNormals } from '@carbonenginejs/core-math/mesh';
|
|
2
|
+
|
|
3
|
+
const OUTPUT_CMF = "cmf";
|
|
4
|
+
const OUTPUT_GR2 = "gr2";
|
|
5
|
+
const OUTPUT_RAW = "raw";
|
|
6
|
+
const OUTPUT_JSON = "json";
|
|
7
|
+
const OUTPUT_FBX_JSON = "fbxJson";
|
|
8
|
+
const GR2_CLASS_KEYS = Object.freeze(["Root", "Mesh", "BoneBinding", "IndexGroup", "MorphTarget", "Model", "Skeleton", "Bone", "Animation", "TrackGroup", "TransformTrack", "Curve"]);
|
|
9
|
+
const CMF_CLASS_KEYS = Object.freeze(["Root", "Section", "Metadata", "MetadataEntry", "Mesh", "IndexGroup", "VertexElement", "MeshLod", "MeshArea", "LodMeshArea", "BoneBinding", "MorphTargets", "MorphTarget", "LodMorphTarget", "AudioOcclusionMesh", "Skeleton", "BoneMask", "BoneWeight", "Animation", "AnimationChannel", "AnimationCurve"]);
|
|
10
|
+
const CLASS_KEYS = Object.freeze(Array.from(new Set([...GR2_CLASS_KEYS, ...CMF_CLASS_KEYS])));
|
|
11
|
+
const DEFAULT_VALUES = Object.freeze({
|
|
12
|
+
emit: OUTPUT_RAW,
|
|
13
|
+
inputType: "fbx",
|
|
14
|
+
source: "",
|
|
15
|
+
flipV: true,
|
|
16
|
+
classes: Object.freeze({}),
|
|
17
|
+
maxBytes: 256 * 1024 * 1024,
|
|
18
|
+
maxNodes: 250000,
|
|
19
|
+
maxDepth: 128,
|
|
20
|
+
maxProperties: 4096,
|
|
21
|
+
maxArrayLength: 20000000
|
|
22
|
+
});
|
|
23
|
+
const BINARY_SIGNATURE = "Kaydara FBX Binary \u0000\u001a\u0000";
|
|
24
|
+
const BINARY_HEADER_SIZE = 27;
|
|
25
|
+
const NODE_HEADER_32_SIZE = 13;
|
|
26
|
+
const NODE_HEADER_64_SIZE = 25;
|
|
27
|
+
const ASCII_PREFIX_LENGTH = 8192;
|
|
28
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
29
|
+
const TEXT_DECODER = new TextDecoder("utf-8");
|
|
30
|
+
const OPTION_KEYS = new Set(["emit", "inputType", "source", "flipV", "classes", "maxBytes", "maxNodes", "maxDepth", "maxProperties", "maxArrayLength"]);
|
|
31
|
+
const DEFLATE_CODE_LENGTH_ORDER = Object.freeze([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
32
|
+
const DEFLATE_LENGTH_BASE = Object.freeze([3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258]);
|
|
33
|
+
const DEFLATE_LENGTH_EXTRA = Object.freeze([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0]);
|
|
34
|
+
const DEFLATE_DISTANCE_BASE = Object.freeze([1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577]);
|
|
35
|
+
const DEFLATE_DISTANCE_EXTRA = Object.freeze([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13]);
|
|
36
|
+
const ADLER32_MOD = 65521;
|
|
37
|
+
const TRIANGULATION_EPSILON = 1e-10;
|
|
38
|
+
const IDENTITY_MATRIX4 = Object.freeze([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
39
|
+
const DEFAULT_ROTATION_ORDER = "XYZ";
|
|
40
|
+
const FBX_ROTATION_ORDERS = Object.freeze(["XYZ", "XZY", "YZX", "YXZ", "ZXY", "ZYX"]);
|
|
41
|
+
const FBX_TICKS_PER_SECOND = 46186158000;
|
|
42
|
+
const CMF_VERTEX_CHANNELS = Object.freeze([["position", "Position", 3], ["normal", "Normal", 3], ["tangent", "Tangent", 3], ["binormal", "Binormal", 3], ["texcoord0", "TexCoord", 2, 0], ["texcoord1", "TexCoord", 2, 1], ["color0", "Color", 4, 0], ["blendIndice", "BoneIndices", 4, 0, "UInt8"], ["blendWeight", "BoneWeights", 4, 0]]);
|
|
43
|
+
const FEATURE_SCAN_ROOT_NODES = Object.freeze(["GlobalSettings", "Objects", "Connections"]);
|
|
44
|
+
const SUPPORTED_GEOMETRY_CLASSES = new Set(["", "Mesh", "Shape"]);
|
|
45
|
+
const SUPPORTED_LAYER_ELEMENTS = new Set(["LayerElementNormal", "LayerElementTangent", "LayerElementBinormal", "LayerElementUV", "LayerElementColor", "LayerElementMaterial"]);
|
|
46
|
+
const SUPPORTED_LAYER_MAPPINGS = new Set(["ByPolygonVertex", "ByControlPoint", "ByVertice", "ByVertex", "ByPolygon", "AllSame"]);
|
|
47
|
+
const SUPPORTED_LAYER_REFERENCES = new Set(["Direct", "IndexToDirect", "Index"]);
|
|
48
|
+
const SUPPORTED_MATERIAL_MAPPINGS = new Set(["AllSame", "ByPolygon"]);
|
|
49
|
+
let fixedDeflateTables = null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalize reader values.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} [base] Base values.
|
|
55
|
+
* @param {object} [options] Override values.
|
|
56
|
+
* @param {string} [readerName] Reader name for errors.
|
|
57
|
+
* @returns {object} Normalized values.
|
|
58
|
+
*/
|
|
59
|
+
function normalizeValues(base = DEFAULT_VALUES, options = {}, readerName = "CjsFbxFormat") {
|
|
60
|
+
if (!options || typeof options !== "object") {
|
|
61
|
+
throw new TypeError(`${readerName}: options must be an object`);
|
|
62
|
+
}
|
|
63
|
+
for (const key of Object.keys(options)) {
|
|
64
|
+
if (!OPTION_KEYS.has(key)) {
|
|
65
|
+
throw new TypeError(`${readerName}: unknown option ${JSON.stringify(key)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const baseValues = {
|
|
69
|
+
...DEFAULT_VALUES,
|
|
70
|
+
...(base || {})
|
|
71
|
+
};
|
|
72
|
+
const values = {
|
|
73
|
+
...baseValues,
|
|
74
|
+
...(options || {})
|
|
75
|
+
};
|
|
76
|
+
values.inputType = values.inputType ? String(values.inputType).replace(/^\./u, "").toLowerCase() : "fbx";
|
|
77
|
+
values.emit = normalizeEmit(values.emit, readerName);
|
|
78
|
+
values.maxBytes = normalizeIntegerLimit(values.maxBytes, "maxBytes", readerName);
|
|
79
|
+
values.maxNodes = normalizeIntegerLimit(values.maxNodes, "maxNodes", readerName);
|
|
80
|
+
values.maxDepth = normalizeIntegerLimit(values.maxDepth, "maxDepth", readerName);
|
|
81
|
+
values.maxProperties = normalizeIntegerLimit(values.maxProperties, "maxProperties", readerName);
|
|
82
|
+
values.maxArrayLength = normalizeIntegerLimit(values.maxArrayLength, "maxArrayLength", readerName);
|
|
83
|
+
values.flipV = normalizeBooleanOption(values.flipV, "flipV", readerName);
|
|
84
|
+
values.classes = hasOwn(options, "classes") ? mergeClasses(baseValues.classes || {}, options.classes, readerName) : normalizeClasses(baseValues.classes || {}, readerName);
|
|
85
|
+
if ((values.emit === OUTPUT_GR2 || values.emit === OUTPUT_CMF) && !hasClasses(values.classes)) {
|
|
86
|
+
throw new TypeError(`${readerName}: emit "${values.emit}" requires explicit classes`);
|
|
87
|
+
}
|
|
88
|
+
return values;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate a node class key.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} key Class key.
|
|
95
|
+
* @param {string} [readerName] Reader name for errors.
|
|
96
|
+
* @returns {string} The validated key.
|
|
97
|
+
*/
|
|
98
|
+
function validateClassKey(key, readerName = "CjsFbxFormat") {
|
|
99
|
+
if (!CLASS_KEYS.includes(key)) {
|
|
100
|
+
throw new Error(`${readerName}: unknown class key ${JSON.stringify(key)}; expected one of ${CLASS_KEYS.join(", ")}`);
|
|
101
|
+
}
|
|
102
|
+
return key;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate a node class constructor.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} key Class key.
|
|
109
|
+
* @param {Function} Class Constructor.
|
|
110
|
+
* @param {string} [readerName] Reader name for errors.
|
|
111
|
+
* @returns {Function} The validated constructor.
|
|
112
|
+
*/
|
|
113
|
+
function validateClass(key, Class, readerName = "CjsFbxFormat") {
|
|
114
|
+
validateClassKey(key, readerName);
|
|
115
|
+
if (typeof Class !== "function") {
|
|
116
|
+
throw new TypeError(`${readerName}: class ${key} must be a constructor`);
|
|
117
|
+
}
|
|
118
|
+
return Class;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Normalize an emit target.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} emit Emit target.
|
|
125
|
+
* @param {string} readerName Reader name for errors.
|
|
126
|
+
* @returns {string} Normalized emit target.
|
|
127
|
+
*/
|
|
128
|
+
function normalizeEmit(emit, readerName) {
|
|
129
|
+
if (emit === undefined || emit === null) {
|
|
130
|
+
return OUTPUT_RAW;
|
|
131
|
+
}
|
|
132
|
+
if (emit === OUTPUT_JSON) {
|
|
133
|
+
return OUTPUT_FBX_JSON;
|
|
134
|
+
}
|
|
135
|
+
if ([OUTPUT_RAW, OUTPUT_FBX_JSON, OUTPUT_GR2, OUTPUT_CMF].includes(emit)) {
|
|
136
|
+
return emit;
|
|
137
|
+
}
|
|
138
|
+
throw new TypeError(`${readerName}: unknown emit value ${JSON.stringify(emit)}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Convert supported binary inputs to bytes.
|
|
143
|
+
*
|
|
144
|
+
* @param {Uint8Array|ArrayBuffer|DataView} input Binary input.
|
|
145
|
+
* @returns {Uint8Array} Byte view.
|
|
146
|
+
*/
|
|
147
|
+
function toBytes(input) {
|
|
148
|
+
if (input instanceof Uint8Array) {
|
|
149
|
+
return input;
|
|
150
|
+
}
|
|
151
|
+
if (input instanceof ArrayBuffer) {
|
|
152
|
+
return new Uint8Array(input);
|
|
153
|
+
}
|
|
154
|
+
if (ArrayBuffer.isView(input)) {
|
|
155
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
156
|
+
}
|
|
157
|
+
throw new TypeError("FBX input must be Uint8Array, ArrayBuffer, or DataView");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Read FBX bytes with normalized values.
|
|
162
|
+
*
|
|
163
|
+
* @param {Uint8Array|ArrayBuffer|DataView} input FBX bytes.
|
|
164
|
+
* @param {object} values Normalized reader values.
|
|
165
|
+
* @returns {object} Raw or debug payload.
|
|
166
|
+
*/
|
|
167
|
+
function readWithValues(input, values = DEFAULT_VALUES) {
|
|
168
|
+
const bytes = toBytes(input);
|
|
169
|
+
if (values.emit === OUTPUT_RAW) {
|
|
170
|
+
const metadata = inspectWithValues(bytes, values);
|
|
171
|
+
return {
|
|
172
|
+
payloadType: "raw",
|
|
173
|
+
sourceFormat: "fbx",
|
|
174
|
+
metadata,
|
|
175
|
+
bytes
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (values.emit === OUTPUT_FBX_JSON) {
|
|
179
|
+
return parseWithValues(bytes, values);
|
|
180
|
+
}
|
|
181
|
+
if (values.emit === OUTPUT_GR2) {
|
|
182
|
+
return readGr2WithValues(bytes, values);
|
|
183
|
+
}
|
|
184
|
+
return readCmfWithValues(bytes, values);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Inspect FBX bytes.
|
|
189
|
+
*
|
|
190
|
+
* @param {Uint8Array|ArrayBuffer|DataView} input FBX bytes.
|
|
191
|
+
* @param {object} values Normalized reader values.
|
|
192
|
+
* @returns {object} Lightweight FBX metadata.
|
|
193
|
+
*/
|
|
194
|
+
function inspectWithValues(input, values = DEFAULT_VALUES) {
|
|
195
|
+
const bytes = toBytes(input);
|
|
196
|
+
const detected = inspectBytes(bytes);
|
|
197
|
+
return {
|
|
198
|
+
payloadType: "geometry",
|
|
199
|
+
mediaTypes: ["geometry"],
|
|
200
|
+
sourceFormat: "fbx",
|
|
201
|
+
byteLength: bytes.byteLength,
|
|
202
|
+
source: values.source || "",
|
|
203
|
+
...detected
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parse FBX bytes into a browser-safe debug document.
|
|
209
|
+
*
|
|
210
|
+
* @param {Uint8Array|ArrayBuffer|DataView} input FBX bytes.
|
|
211
|
+
* @param {object} values Normalized reader values.
|
|
212
|
+
* @returns {object} Parsed FBX document.
|
|
213
|
+
*/
|
|
214
|
+
function parseWithValues(input, values = DEFAULT_VALUES) {
|
|
215
|
+
return {
|
|
216
|
+
...parseDocumentWithValues(input, values),
|
|
217
|
+
payloadType: OUTPUT_FBX_JSON
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Read static FBX mesh data into GR2-shaped caller classes.
|
|
223
|
+
*
|
|
224
|
+
* @param {Uint8Array|ArrayBuffer|DataView} input FBX bytes.
|
|
225
|
+
* @param {object} values Normalized reader values.
|
|
226
|
+
* @returns {object} Hydrated GR2-shaped root.
|
|
227
|
+
*/
|
|
228
|
+
function readGr2WithValues(input, values = DEFAULT_VALUES) {
|
|
229
|
+
const document = parseGr2DocumentWithValues(input, values);
|
|
230
|
+
return hydrateGr2(buildGr2FromFbxNodes(document.nodes, values), values.classes, {
|
|
231
|
+
source: values.source
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Read static FBX mesh data into CMF-shaped caller classes.
|
|
237
|
+
*
|
|
238
|
+
* @param {Uint8Array|ArrayBuffer|DataView} input FBX bytes.
|
|
239
|
+
* @param {object} values Normalized reader values.
|
|
240
|
+
* @returns {object} Hydrated CMF-shaped root.
|
|
241
|
+
*/
|
|
242
|
+
function readCmfWithValues(input, values = DEFAULT_VALUES) {
|
|
243
|
+
const document = parseGr2DocumentWithValues(input, values);
|
|
244
|
+
return hydrateCmf(buildCmfFromShared(buildGr2FromFbxNodes(document.nodes, values)), values.classes, {
|
|
245
|
+
source: values.source
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function parseDocumentWithValues(input, values = DEFAULT_VALUES) {
|
|
249
|
+
const bytes = toBytes(input);
|
|
250
|
+
if (bytes.byteLength > values.maxBytes) {
|
|
251
|
+
throw new Error(`fbx: input exceeds maxBytes (${values.maxBytes})`);
|
|
252
|
+
}
|
|
253
|
+
const metadata = inspectWithValues(bytes, values);
|
|
254
|
+
if (metadata.encoding === "binary") {
|
|
255
|
+
return parseBinaryDocument(bytes, metadata, values);
|
|
256
|
+
}
|
|
257
|
+
if (metadata.encoding === "ascii") {
|
|
258
|
+
return parseAsciiDocument(bytes, metadata, values);
|
|
259
|
+
}
|
|
260
|
+
const error = new Error("fbx: unrecognized FBX data");
|
|
261
|
+
error.code = "CJS_FORMAT_UNRECOGNIZED";
|
|
262
|
+
error.sourceFormat = "fbx";
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
function parseGr2DocumentWithValues(input, values) {
|
|
266
|
+
const bytes = toBytes(input);
|
|
267
|
+
if (bytes.byteLength > values.maxBytes) {
|
|
268
|
+
throw new Error(`fbx: input exceeds maxBytes (${values.maxBytes})`);
|
|
269
|
+
}
|
|
270
|
+
const metadata = inspectWithValues(bytes, values);
|
|
271
|
+
if (metadata.encoding === "binary") {
|
|
272
|
+
return {
|
|
273
|
+
...metadata,
|
|
274
|
+
nodes: readBinaryTargetNodes(bytes, metadata, values, new Set(["GlobalSettings", "Objects", "Connections"]))
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (metadata.encoding === "ascii") {
|
|
278
|
+
return {
|
|
279
|
+
...metadata,
|
|
280
|
+
nodes: readAsciiTargetNodes(bytes, values, new Set(["GlobalSettings", "Objects", "Connections"]))
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const error = new Error("fbx: unrecognized FBX data");
|
|
284
|
+
error.code = "CJS_FORMAT_UNRECOGNIZED";
|
|
285
|
+
error.sourceFormat = "fbx";
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Report FBX support for the current implementation.
|
|
291
|
+
*
|
|
292
|
+
* @param {Uint8Array|ArrayBuffer|DataView} input FBX bytes.
|
|
293
|
+
* @param {object} values Normalized reader values.
|
|
294
|
+
* @returns {object} Support report.
|
|
295
|
+
*/
|
|
296
|
+
function isSupportedWithValues(input, values = DEFAULT_VALUES) {
|
|
297
|
+
try {
|
|
298
|
+
const metadata = inspectWithValues(input, values);
|
|
299
|
+
const recognized = metadata.encoding !== "";
|
|
300
|
+
const diagnostics = recognized ? inspectFbxFeatureSupport(input, values, metadata) : {
|
|
301
|
+
warnings: [],
|
|
302
|
+
errors: []
|
|
303
|
+
};
|
|
304
|
+
return {
|
|
305
|
+
format: "fbx",
|
|
306
|
+
source: values.source || "buffer",
|
|
307
|
+
supported: recognized ? "partial" : "none",
|
|
308
|
+
confidence: recognized ? metadata.confidence : 0,
|
|
309
|
+
preferred: recognized ? "gr2" : "",
|
|
310
|
+
reason: recognized ? "FBX document parsing plus basic static/skinned/morphed GR2/CMF output and basic CMF animation curves are available; full CarbonEngine-equivalent import is not implemented yet." : "Unrecognized FBX data.",
|
|
311
|
+
metadata,
|
|
312
|
+
variants: [{
|
|
313
|
+
kind: "document",
|
|
314
|
+
codec: metadata.encoding || "unknown",
|
|
315
|
+
supported: recognized
|
|
316
|
+
}, {
|
|
317
|
+
kind: "gr2",
|
|
318
|
+
codec: "gr2-geometry",
|
|
319
|
+
supported: recognized,
|
|
320
|
+
reason: recognized ? "Basic static/skinned/morphed geometry class output is available." : "Unrecognized FBX data."
|
|
321
|
+
}, {
|
|
322
|
+
kind: "cmf",
|
|
323
|
+
codec: "cmf-geometry-animation",
|
|
324
|
+
supported: recognized,
|
|
325
|
+
reason: recognized ? "Basic static/skinned/morphed geometry and basic morph/bone animation class output are available." : "Unrecognized FBX data."
|
|
326
|
+
}],
|
|
327
|
+
warnings: diagnostics.warnings,
|
|
328
|
+
errors: diagnostics.errors
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return {
|
|
332
|
+
format: "fbx",
|
|
333
|
+
source: values.source || "buffer",
|
|
334
|
+
supported: "none",
|
|
335
|
+
confidence: 0,
|
|
336
|
+
preferred: "",
|
|
337
|
+
reason: error.message,
|
|
338
|
+
metadata: null,
|
|
339
|
+
variants: [],
|
|
340
|
+
warnings: [],
|
|
341
|
+
errors: [error.message]
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function inspectFbxFeatureSupport(input, values, metadata) {
|
|
346
|
+
try {
|
|
347
|
+
const bytes = toBytes(input),
|
|
348
|
+
targetNames = new Set(FEATURE_SCAN_ROOT_NODES),
|
|
349
|
+
nodes = metadata.encoding === "binary" ? readBinaryTargetNodes(bytes, metadata, values, targetNames) : readAsciiTargetNodes(bytes, values, targetNames);
|
|
350
|
+
return collectFbxFeatureSupport(nodes);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return {
|
|
353
|
+
warnings: [],
|
|
354
|
+
errors: [error.message]
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function collectFbxFeatureSupport(nodes) {
|
|
359
|
+
const warnings = [],
|
|
360
|
+
objectsNode = findFirstNode(nodes, "Objects");
|
|
361
|
+
if (!objectsNode) {
|
|
362
|
+
return {
|
|
363
|
+
warnings: ["fbx: no Objects node; runtime GR2/CMF output requires mesh Geometry nodes."],
|
|
364
|
+
errors: []
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const objectIndex = indexObjectNodes(objectsNode),
|
|
368
|
+
connections = readConnections(findFirstNode(nodes, "Connections"));
|
|
369
|
+
collectGeometryFeatureWarnings(objectIndex, warnings);
|
|
370
|
+
collectLayerElementFeatureWarnings(objectIndex, warnings);
|
|
371
|
+
collectSkinFeatureWarnings(objectIndex, connections, warnings);
|
|
372
|
+
collectBlendShapeFeatureWarnings(objectIndex, connections, warnings);
|
|
373
|
+
return {
|
|
374
|
+
warnings,
|
|
375
|
+
errors: []
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function collectGeometryFeatureWarnings(objectIndex, warnings) {
|
|
379
|
+
for (const geometry of objectIndex.list.filter(entry => entry.nodeName === "Geometry")) {
|
|
380
|
+
if (!SUPPORTED_GEOMETRY_CLASSES.has(geometry.className)) {
|
|
381
|
+
addWarning(warnings, `fbx: Geometry ${objectLabel(geometry)} class ${JSON.stringify(geometry.className)} is not imported by GR2/CMF output.`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (geometry.className === "Shape") {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!findFirstNode(geometry.node.children, "Vertices")) {
|
|
388
|
+
addWarning(warnings, `fbx: Geometry ${objectLabel(geometry)} is missing Vertices and cannot be imported as a mesh.`);
|
|
389
|
+
}
|
|
390
|
+
if (!findFirstNode(geometry.node.children, "PolygonVertexIndex")) {
|
|
391
|
+
addWarning(warnings, `fbx: Geometry ${objectLabel(geometry)} is missing PolygonVertexIndex and cannot be imported as a mesh.`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function collectLayerElementFeatureWarnings(objectIndex, warnings) {
|
|
396
|
+
for (const geometry of objectIndex.list.filter(entry => entry.nodeName === "Geometry" && (!entry.className || entry.className === "Mesh"))) {
|
|
397
|
+
for (const layer of geometry.node.children.filter(child => child.name.startsWith("LayerElement"))) {
|
|
398
|
+
if (!SUPPORTED_LAYER_ELEMENTS.has(layer.name)) {
|
|
399
|
+
addWarning(warnings, `fbx: ${layer.name} on Geometry ${objectLabel(geometry)} is ignored by GR2/CMF output.`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (layer.name === "LayerElementMaterial") {
|
|
403
|
+
collectMaterialLayerFeatureWarnings(geometry, layer, warnings);
|
|
404
|
+
} else {
|
|
405
|
+
collectVertexLayerFeatureWarnings(geometry, layer, warnings);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function collectVertexLayerFeatureWarnings(geometry, layer, warnings) {
|
|
411
|
+
const mapping = String(readFirstChildProperty(layer, "MappingInformationType") || "ByPolygonVertex"),
|
|
412
|
+
reference = String(readFirstChildProperty(layer, "ReferenceInformationType") || "Direct");
|
|
413
|
+
if (!SUPPORTED_LAYER_MAPPINGS.has(mapping)) {
|
|
414
|
+
addWarning(warnings, `fbx: ${layer.name} on Geometry ${objectLabel(geometry)} uses unsupported MappingInformationType ${JSON.stringify(mapping)}.`);
|
|
415
|
+
}
|
|
416
|
+
if (!SUPPORTED_LAYER_REFERENCES.has(reference)) {
|
|
417
|
+
addWarning(warnings, `fbx: ${layer.name} on Geometry ${objectLabel(geometry)} uses unsupported ReferenceInformationType ${JSON.stringify(reference)}.`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function collectMaterialLayerFeatureWarnings(geometry, layer, warnings) {
|
|
421
|
+
const mapping = String(readFirstChildProperty(layer, "MappingInformationType") || "AllSame");
|
|
422
|
+
if (!SUPPORTED_MATERIAL_MAPPINGS.has(mapping)) {
|
|
423
|
+
addWarning(warnings, `fbx: LayerElementMaterial on Geometry ${objectLabel(geometry)} uses unsupported MappingInformationType ${JSON.stringify(mapping)}.`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function collectSkinFeatureWarnings(objectIndex, connections, warnings) {
|
|
427
|
+
for (const geometry of objectIndex.list.filter(entry => entry.nodeName === "Geometry" && (!entry.className || entry.className === "Mesh"))) {
|
|
428
|
+
const skins = connectedChildObjects(geometry, objectIndex, connections, "Deformer").filter(isSkinDeformer);
|
|
429
|
+
if (skins.length > 1) {
|
|
430
|
+
addWarning(warnings, `fbx: Geometry ${objectLabel(geometry)} has ${skins.length} Skin deformers; GR2/CMF output imports only the first skin.`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const skins = objectIndex.list.filter(entry => entry.nodeName === "Deformer" && isSkinDeformer(entry));
|
|
434
|
+
for (const skin of skins) {
|
|
435
|
+
const clusters = connectedChildObjects(skin, objectIndex, connections, "Deformer").filter(isClusterDeformer);
|
|
436
|
+
for (const cluster of clusters) {
|
|
437
|
+
const bone = clusterBone(cluster, objectIndex, connections),
|
|
438
|
+
indexes = readChildArray(cluster.node, "Indexes") || [],
|
|
439
|
+
weights = readChildArray(cluster.node, "Weights") || [];
|
|
440
|
+
if (!bone) {
|
|
441
|
+
addWarning(warnings, `fbx: Skin cluster ${objectLabel(cluster)} is skipped because it has no linked bone Model.`);
|
|
442
|
+
} else if (!indexes.length) {
|
|
443
|
+
addWarning(warnings, `fbx: Skin cluster ${objectLabel(cluster)} is skipped because it has no Indexes.`);
|
|
444
|
+
} else if (!hasPositiveClusterWeight(indexes, weights)) {
|
|
445
|
+
addWarning(warnings, `fbx: Skin cluster ${objectLabel(cluster)} is skipped because it has no positive Weights.`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (hasControlPointInfluenceOverflow(buildSkinClusterRecords(clusters, objectIndex, connections))) {
|
|
449
|
+
addWarning(warnings, `fbx: Skin ${objectLabel(skin)} has control points with more than four positive bone influences; GR2/CMF output keeps the strongest four and renormalizes them.`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Check whether an indexed FBX object is a Skin deformer.
|
|
456
|
+
*
|
|
457
|
+
* @param {object} entry Indexed object entry.
|
|
458
|
+
* @returns {boolean} True when the entry is a Skin deformer.
|
|
459
|
+
*/
|
|
460
|
+
function isSkinDeformer(entry) {
|
|
461
|
+
return entry?.className === "Skin" || entry?.fullName?.startsWith("Deformer::Skin") || entry?.name === "Skin";
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check whether an indexed FBX object is a Cluster deformer.
|
|
466
|
+
*
|
|
467
|
+
* @param {object} entry Indexed object entry.
|
|
468
|
+
* @returns {boolean} True when the entry is a Cluster deformer.
|
|
469
|
+
*/
|
|
470
|
+
function isClusterDeformer(entry) {
|
|
471
|
+
return entry?.className === "Cluster" || entry?.fullName?.startsWith("SubDeformer::") || entry?.name?.includes("Cluster");
|
|
472
|
+
}
|
|
473
|
+
function hasControlPointInfluenceOverflow(records) {
|
|
474
|
+
const controlPointInfluences = buildControlPointInfluences(records);
|
|
475
|
+
for (const influences of controlPointInfluences.values()) {
|
|
476
|
+
if (influences.filter(influence => influence.weight > 0).length > 4) {
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
function collectBlendShapeFeatureWarnings(objectIndex, connections, warnings) {
|
|
483
|
+
const channels = objectIndex.list.filter(entry => isBlendShapeChannel(entry));
|
|
484
|
+
for (const channel of channels) {
|
|
485
|
+
const shapes = readBlendShapeChannelShapes(channel, objectIndex, connections);
|
|
486
|
+
if (isUnsupportedBlendShapeInBetween(channel, shapes)) {
|
|
487
|
+
addWarning(warnings, `fbx: BlendShapeChannel ${objectLabel(channel)} is skipped because in-between morph interpolation is not implemented.`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function addWarning(warnings, message) {
|
|
492
|
+
if (!warnings.includes(message)) {
|
|
493
|
+
warnings.push(message);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function objectLabel(entry) {
|
|
497
|
+
return JSON.stringify(entry?.name || entry?.fullName || entry?.key || "");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Inspect raw bytes without normalized options.
|
|
502
|
+
*
|
|
503
|
+
* @param {Uint8Array} bytes FBX bytes.
|
|
504
|
+
* @returns {object} Detected metadata.
|
|
505
|
+
*/
|
|
506
|
+
function inspectBytes(bytes) {
|
|
507
|
+
if (isBinaryFBX(bytes)) {
|
|
508
|
+
return inspectBinaryBytes(bytes);
|
|
509
|
+
}
|
|
510
|
+
const text = decodeAsciiPrefix(bytes, ASCII_PREFIX_LENGTH);
|
|
511
|
+
const ascii = isAsciiFBXText(text);
|
|
512
|
+
return {
|
|
513
|
+
encoding: ascii ? "ascii" : "",
|
|
514
|
+
version: ascii ? readAsciiVersion(text) : 0,
|
|
515
|
+
signature: ascii ? "FBX ASCII" : "",
|
|
516
|
+
rootNodeCount: ascii ? countAsciiRootNodes(text) : 0,
|
|
517
|
+
rootNodeNames: ascii ? collectAsciiRootNames(text) : [],
|
|
518
|
+
confidence: ascii ? 0.85 : 0
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Test whether bytes look like FBX.
|
|
524
|
+
*
|
|
525
|
+
* @param {Uint8Array|ArrayBuffer|DataView} input Candidate bytes.
|
|
526
|
+
* @returns {boolean} True when the input appears to be FBX.
|
|
527
|
+
*/
|
|
528
|
+
function isFBX(input) {
|
|
529
|
+
const bytes = toBytes(input);
|
|
530
|
+
return isBinaryFBX(bytes) || isAsciiFBXText(decodeAsciiPrefix(bytes, ASCII_PREFIX_LENGTH));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Convert format output into JSON-compatible debug data.
|
|
535
|
+
*
|
|
536
|
+
* @param {any} value Format output.
|
|
537
|
+
* @returns {any} JSON-compatible value.
|
|
538
|
+
*/
|
|
539
|
+
function toJsonValue(value) {
|
|
540
|
+
if (value instanceof Uint8Array) {
|
|
541
|
+
return {
|
|
542
|
+
byteLength: value.byteLength
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (ArrayBuffer.isView(value)) {
|
|
546
|
+
return {
|
|
547
|
+
type: value.constructor.name,
|
|
548
|
+
length: value.length
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (Array.isArray(value)) {
|
|
552
|
+
return value.map(toJsonValue);
|
|
553
|
+
}
|
|
554
|
+
if (value && typeof value === "object") {
|
|
555
|
+
const output = {};
|
|
556
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
557
|
+
output[key] = toJsonValue(entry);
|
|
558
|
+
}
|
|
559
|
+
return output;
|
|
560
|
+
}
|
|
561
|
+
return value;
|
|
562
|
+
}
|
|
563
|
+
function inspectBinaryBytes(bytes) {
|
|
564
|
+
const version = readU32LE(bytes, 23);
|
|
565
|
+
const headerSize = getBinaryNodeHeaderSize(version);
|
|
566
|
+
const rootNodeNames = [];
|
|
567
|
+
let rootNodeCount = 0;
|
|
568
|
+
let offset = BINARY_HEADER_SIZE;
|
|
569
|
+
while (offset + headerSize <= bytes.byteLength && !isNullRecord(bytes, offset, headerSize)) {
|
|
570
|
+
const header = readBinaryNodeHeader(bytes, offset, version);
|
|
571
|
+
if (!header || header.endOffset <= offset || header.endOffset > bytes.byteLength) {
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
rootNodeNames.push(readString(bytes, header.nameOffset, header.nameLength));
|
|
575
|
+
rootNodeCount++;
|
|
576
|
+
offset = header.endOffset;
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
encoding: "binary",
|
|
580
|
+
version,
|
|
581
|
+
signature: "Kaydara FBX Binary",
|
|
582
|
+
rootNodeCount,
|
|
583
|
+
rootNodeNames,
|
|
584
|
+
confidence: 1
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function parseBinaryDocument(bytes, metadata, values) {
|
|
588
|
+
const nodes = [];
|
|
589
|
+
const headerSize = getBinaryNodeHeaderSize(metadata.version);
|
|
590
|
+
let offset = BINARY_HEADER_SIZE;
|
|
591
|
+
let nodeCount = 0;
|
|
592
|
+
while (offset + headerSize <= bytes.byteLength) {
|
|
593
|
+
if (isNullRecord(bytes, offset, headerSize)) {
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
const result = readBinaryNode(bytes, offset, metadata.version, values, 0);
|
|
597
|
+
nodes.push(result.node);
|
|
598
|
+
nodeCount += result.count;
|
|
599
|
+
if (nodeCount > values.maxNodes) {
|
|
600
|
+
throw new Error(`fbx: node count exceeds maxNodes (${values.maxNodes})`);
|
|
601
|
+
}
|
|
602
|
+
offset = result.offset;
|
|
603
|
+
}
|
|
604
|
+
const document = {
|
|
605
|
+
...metadata,
|
|
606
|
+
payloadType: OUTPUT_FBX_JSON,
|
|
607
|
+
nodes,
|
|
608
|
+
nodeCount,
|
|
609
|
+
rootNodeCount: nodes.length,
|
|
610
|
+
rootNodeNames: nodes.map(node => node.name)
|
|
611
|
+
};
|
|
612
|
+
document.root = buildFbxRoot(document.nodes);
|
|
613
|
+
return document;
|
|
614
|
+
}
|
|
615
|
+
function readBinaryNode(bytes, offset, version, values, depth) {
|
|
616
|
+
if (depth > values.maxDepth) {
|
|
617
|
+
throw new Error(`fbx: node depth exceeds maxDepth (${values.maxDepth})`);
|
|
618
|
+
}
|
|
619
|
+
const header = readBinaryNodeHeader(bytes, offset, version);
|
|
620
|
+
if (!header) {
|
|
621
|
+
throw new Error(`fbx: invalid node header at offset ${offset}`);
|
|
622
|
+
}
|
|
623
|
+
if (header.endOffset > bytes.byteLength) {
|
|
624
|
+
throw new Error(`fbx: node ${header.name} extends beyond end of file`);
|
|
625
|
+
}
|
|
626
|
+
if (header.propertyCount > values.maxProperties) {
|
|
627
|
+
throw new Error(`fbx: node ${header.name} exceeds maxProperties (${values.maxProperties})`);
|
|
628
|
+
}
|
|
629
|
+
let cursor = header.propertiesOffset;
|
|
630
|
+
const properties = [];
|
|
631
|
+
const propertyTypes = [];
|
|
632
|
+
for (let i = 0; i < header.propertyCount; i++) {
|
|
633
|
+
const result = readBinaryProperty(bytes, cursor, values);
|
|
634
|
+
properties.push(result.value);
|
|
635
|
+
propertyTypes.push(result.type);
|
|
636
|
+
cursor = result.offset;
|
|
637
|
+
}
|
|
638
|
+
const propertyEnd = header.propertiesOffset + header.propertyListLength;
|
|
639
|
+
if (cursor > propertyEnd) {
|
|
640
|
+
throw new Error(`fbx: property list for node ${header.name} overflowed`);
|
|
641
|
+
}
|
|
642
|
+
cursor = propertyEnd;
|
|
643
|
+
const children = [];
|
|
644
|
+
let count = 1;
|
|
645
|
+
const headerSize = getBinaryNodeHeaderSize(version);
|
|
646
|
+
while (cursor + headerSize <= header.endOffset) {
|
|
647
|
+
if (isNullRecord(bytes, cursor, headerSize)) {
|
|
648
|
+
cursor += headerSize;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
if (cursor >= header.endOffset) {
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
const child = readBinaryNode(bytes, cursor, version, values, depth + 1);
|
|
655
|
+
if (child.offset > header.endOffset) {
|
|
656
|
+
throw new Error(`fbx: child node for ${header.name} overflowed`);
|
|
657
|
+
}
|
|
658
|
+
children.push(child.node);
|
|
659
|
+
count += child.count;
|
|
660
|
+
cursor = child.offset;
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
offset: header.endOffset,
|
|
664
|
+
count,
|
|
665
|
+
node: {
|
|
666
|
+
name: header.name,
|
|
667
|
+
properties,
|
|
668
|
+
propertyTypes,
|
|
669
|
+
children
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function readBinaryNodeHeader(bytes, offset, version) {
|
|
674
|
+
const large = version >= 7500;
|
|
675
|
+
const headerSize = getBinaryNodeHeaderSize(version);
|
|
676
|
+
if (offset + headerSize > bytes.byteLength) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
let cursor = offset;
|
|
680
|
+
const endOffset = large ? readU64LEAsNumber(bytes, cursor) : readU32LE(bytes, cursor);
|
|
681
|
+
cursor += large ? 8 : 4;
|
|
682
|
+
const propertyCount = large ? readU64LEAsNumber(bytes, cursor) : readU32LE(bytes, cursor);
|
|
683
|
+
cursor += large ? 8 : 4;
|
|
684
|
+
const propertyListLength = large ? readU64LEAsNumber(bytes, cursor) : readU32LE(bytes, cursor);
|
|
685
|
+
cursor += large ? 8 : 4;
|
|
686
|
+
const nameLength = bytes[cursor++];
|
|
687
|
+
const nameOffset = cursor;
|
|
688
|
+
if (endOffset === 0 && propertyCount === 0 && propertyListLength === 0 && nameLength === 0) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
if (!Number.isSafeInteger(endOffset) || !Number.isSafeInteger(propertyCount) || !Number.isSafeInteger(propertyListLength)) {
|
|
692
|
+
throw new Error("fbx: 64-bit node offsets exceed JavaScript safe integer range");
|
|
693
|
+
}
|
|
694
|
+
if (nameOffset + nameLength > bytes.byteLength) {
|
|
695
|
+
throw new Error(`fbx: node name at offset ${offset} is truncated`);
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
endOffset,
|
|
699
|
+
propertyCount,
|
|
700
|
+
propertyListLength,
|
|
701
|
+
nameLength,
|
|
702
|
+
nameOffset,
|
|
703
|
+
name: readString(bytes, nameOffset, nameLength),
|
|
704
|
+
propertiesOffset: nameOffset + nameLength
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function readBinaryProperty(bytes, offset, values) {
|
|
708
|
+
ensureRange(bytes, offset, 1, "fbx: property type is truncated");
|
|
709
|
+
const type = String.fromCharCode(bytes[offset]);
|
|
710
|
+
let cursor = offset + 1;
|
|
711
|
+
switch (type) {
|
|
712
|
+
case "Y":
|
|
713
|
+
ensureRange(bytes, cursor, 2, "fbx: int16 property is truncated");
|
|
714
|
+
return {
|
|
715
|
+
type,
|
|
716
|
+
value: readI16LE(bytes, cursor),
|
|
717
|
+
offset: cursor + 2
|
|
718
|
+
};
|
|
719
|
+
case "C":
|
|
720
|
+
ensureRange(bytes, cursor, 1, "fbx: bool property is truncated");
|
|
721
|
+
return {
|
|
722
|
+
type,
|
|
723
|
+
value: bytes[cursor] !== 0,
|
|
724
|
+
offset: cursor + 1
|
|
725
|
+
};
|
|
726
|
+
case "I":
|
|
727
|
+
ensureRange(bytes, cursor, 4, "fbx: int32 property is truncated");
|
|
728
|
+
return {
|
|
729
|
+
type,
|
|
730
|
+
value: readI32LE(bytes, cursor),
|
|
731
|
+
offset: cursor + 4
|
|
732
|
+
};
|
|
733
|
+
case "F":
|
|
734
|
+
ensureRange(bytes, cursor, 4, "fbx: float32 property is truncated");
|
|
735
|
+
return {
|
|
736
|
+
type,
|
|
737
|
+
value: readF32LE(bytes, cursor),
|
|
738
|
+
offset: cursor + 4
|
|
739
|
+
};
|
|
740
|
+
case "D":
|
|
741
|
+
ensureRange(bytes, cursor, 8, "fbx: float64 property is truncated");
|
|
742
|
+
return {
|
|
743
|
+
type,
|
|
744
|
+
value: readF64LE(bytes, cursor),
|
|
745
|
+
offset: cursor + 8
|
|
746
|
+
};
|
|
747
|
+
case "L":
|
|
748
|
+
ensureRange(bytes, cursor, 8, "fbx: int64 property is truncated");
|
|
749
|
+
return {
|
|
750
|
+
type,
|
|
751
|
+
value: readI64LEValue(bytes, cursor),
|
|
752
|
+
offset: cursor + 8
|
|
753
|
+
};
|
|
754
|
+
case "R":
|
|
755
|
+
return readBinaryBlobProperty(bytes, cursor, type, "bytes");
|
|
756
|
+
case "S":
|
|
757
|
+
return readBinaryBlobProperty(bytes, cursor, type, "string");
|
|
758
|
+
case "f":
|
|
759
|
+
case "d":
|
|
760
|
+
case "i":
|
|
761
|
+
case "l":
|
|
762
|
+
case "b":
|
|
763
|
+
case "c":
|
|
764
|
+
return readBinaryArrayProperty(bytes, cursor, type, values);
|
|
765
|
+
default:
|
|
766
|
+
throw new Error(`fbx: unsupported property type ${JSON.stringify(type)} at offset ${offset}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function readBinaryBlobProperty(bytes, offset, type, kind) {
|
|
770
|
+
ensureRange(bytes, offset, 4, `fbx: ${kind} property length is truncated`);
|
|
771
|
+
const length = readU32LE(bytes, offset);
|
|
772
|
+
const dataOffset = offset + 4;
|
|
773
|
+
const endOffset = dataOffset + length;
|
|
774
|
+
ensureRange(bytes, dataOffset, length, `fbx: ${kind} property is truncated`);
|
|
775
|
+
return {
|
|
776
|
+
type,
|
|
777
|
+
value: kind === "string" ? readString(bytes, dataOffset, length) : bytes.subarray(dataOffset, endOffset),
|
|
778
|
+
offset: endOffset
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function readBinaryArrayProperty(bytes, offset, type, values) {
|
|
782
|
+
ensureRange(bytes, offset, 12, "fbx: array property header is truncated");
|
|
783
|
+
const length = readU32LE(bytes, offset);
|
|
784
|
+
const encoding = readU32LE(bytes, offset + 4);
|
|
785
|
+
const byteLength = readU32LE(bytes, offset + 8);
|
|
786
|
+
const dataOffset = offset + 12;
|
|
787
|
+
const endOffset = dataOffset + byteLength;
|
|
788
|
+
ensureRange(bytes, dataOffset, byteLength, "fbx: array property is truncated");
|
|
789
|
+
if (length > values.maxArrayLength) {
|
|
790
|
+
throw new Error(`fbx: array property exceeds maxArrayLength (${values.maxArrayLength})`);
|
|
791
|
+
}
|
|
792
|
+
if (encoding === 0) {
|
|
793
|
+
return {
|
|
794
|
+
type,
|
|
795
|
+
value: readUncompressedArray(bytes, dataOffset, type, length, byteLength),
|
|
796
|
+
offset: endOffset
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (encoding === 1) {
|
|
800
|
+
const expectedByteLength = getArrayElementSize(type) * length;
|
|
801
|
+
const expanded = inflateZlib(bytes.subarray(dataOffset, endOffset), expectedByteLength);
|
|
802
|
+
return {
|
|
803
|
+
type,
|
|
804
|
+
value: readUncompressedArray(expanded, 0, type, length, expectedByteLength),
|
|
805
|
+
offset: endOffset
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
type,
|
|
810
|
+
value: {
|
|
811
|
+
arrayType: type,
|
|
812
|
+
length,
|
|
813
|
+
encoding,
|
|
814
|
+
byteOffset: dataOffset,
|
|
815
|
+
byteLength,
|
|
816
|
+
compressed: true
|
|
817
|
+
},
|
|
818
|
+
offset: endOffset
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function readUncompressedArray(bytes, offset, type, length, byteLength) {
|
|
822
|
+
const expected = getArrayElementSize(type) * length;
|
|
823
|
+
if (byteLength < expected) {
|
|
824
|
+
throw new Error(`fbx: array property ${type} is truncated`);
|
|
825
|
+
}
|
|
826
|
+
if (type === "f") {
|
|
827
|
+
const value = new Float32Array(length);
|
|
828
|
+
for (let i = 0; i < length; i++) {
|
|
829
|
+
value[i] = readF32LE(bytes, offset + i * 4);
|
|
830
|
+
}
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
833
|
+
if (type === "d") {
|
|
834
|
+
const value = new Float64Array(length);
|
|
835
|
+
for (let i = 0; i < length; i++) {
|
|
836
|
+
value[i] = readF64LE(bytes, offset + i * 8);
|
|
837
|
+
}
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
840
|
+
if (type === "i") {
|
|
841
|
+
const value = new Int32Array(length);
|
|
842
|
+
for (let i = 0; i < length; i++) {
|
|
843
|
+
value[i] = readI32LE(bytes, offset + i * 4);
|
|
844
|
+
}
|
|
845
|
+
return value;
|
|
846
|
+
}
|
|
847
|
+
if (type === "l") {
|
|
848
|
+
const value = [];
|
|
849
|
+
for (let i = 0; i < length; i++) {
|
|
850
|
+
value.push(readI64LEValue(bytes, offset + i * 8));
|
|
851
|
+
}
|
|
852
|
+
return value;
|
|
853
|
+
}
|
|
854
|
+
const value = [];
|
|
855
|
+
for (let i = 0; i < length; i++) {
|
|
856
|
+
value.push(bytes[offset + i] !== 0);
|
|
857
|
+
}
|
|
858
|
+
return value;
|
|
859
|
+
}
|
|
860
|
+
function inflateZlib(source, expectedLength) {
|
|
861
|
+
if (source.byteLength < 6) {
|
|
862
|
+
throw new Error("fbx: compressed array zlib stream is truncated");
|
|
863
|
+
}
|
|
864
|
+
const cmf = source[0],
|
|
865
|
+
flg = source[1];
|
|
866
|
+
if ((cmf & 0x0f) !== 8 || cmf >> 4 > 7 || ((cmf << 8) + flg) % 31 !== 0) {
|
|
867
|
+
throw new Error("fbx: compressed array has invalid zlib header");
|
|
868
|
+
}
|
|
869
|
+
if (flg & 0x20) {
|
|
870
|
+
throw new Error("fbx: compressed array uses a preset zlib dictionary");
|
|
871
|
+
}
|
|
872
|
+
const expectedAdler = readU32BE(source, source.byteLength - 4),
|
|
873
|
+
output = inflateDeflate(source.subarray(2, source.byteLength - 4), expectedLength),
|
|
874
|
+
actualAdler = adler32(output);
|
|
875
|
+
if (actualAdler !== expectedAdler) {
|
|
876
|
+
throw new Error("fbx: compressed array zlib checksum mismatch");
|
|
877
|
+
}
|
|
878
|
+
return output;
|
|
879
|
+
}
|
|
880
|
+
function inflateDeflate(source, expectedLength) {
|
|
881
|
+
const reader = new DeflateBitReader(source),
|
|
882
|
+
state = {
|
|
883
|
+
output: new Uint8Array(expectedLength),
|
|
884
|
+
offset: 0
|
|
885
|
+
};
|
|
886
|
+
let finalBlock = false;
|
|
887
|
+
while (!finalBlock) {
|
|
888
|
+
finalBlock = reader.readBits(1) === 1;
|
|
889
|
+
const blockType = reader.readBits(2);
|
|
890
|
+
if (blockType === 0) {
|
|
891
|
+
inflateStoredBlock(reader, state);
|
|
892
|
+
} else if (blockType === 1) {
|
|
893
|
+
const tables = getFixedDeflateTables();
|
|
894
|
+
inflateHuffmanBlock(reader, state, tables.literalLength, tables.distance);
|
|
895
|
+
} else if (blockType === 2) {
|
|
896
|
+
const tables = readDynamicDeflateTables(reader);
|
|
897
|
+
inflateHuffmanBlock(reader, state, tables.literalLength, tables.distance);
|
|
898
|
+
} else {
|
|
899
|
+
throw new Error("fbx: compressed array uses reserved deflate block type");
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (state.offset !== expectedLength) {
|
|
903
|
+
throw new Error("fbx: compressed array decoded to unexpected length");
|
|
904
|
+
}
|
|
905
|
+
return state.output;
|
|
906
|
+
}
|
|
907
|
+
function inflateStoredBlock(reader, state) {
|
|
908
|
+
reader.alignByte();
|
|
909
|
+
const length = reader.readAlignedU16LE(),
|
|
910
|
+
inverseLength = reader.readAlignedU16LE();
|
|
911
|
+
if (((length ^ 0xffff) & 0xffff) !== inverseLength) {
|
|
912
|
+
throw new Error("fbx: compressed array stored block length mismatch");
|
|
913
|
+
}
|
|
914
|
+
for (let i = 0; i < length; i++) {
|
|
915
|
+
writeInflatedByte(state, reader.readAlignedByte());
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function inflateHuffmanBlock(reader, state, literalLengthTable, distanceTable) {
|
|
919
|
+
for (;;) {
|
|
920
|
+
const symbol = literalLengthTable.decode(reader);
|
|
921
|
+
if (symbol < 256) {
|
|
922
|
+
writeInflatedByte(state, symbol);
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (symbol === 256) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const lengthIndex = symbol - 257;
|
|
929
|
+
if (lengthIndex < 0 || lengthIndex >= DEFLATE_LENGTH_BASE.length) {
|
|
930
|
+
throw new Error("fbx: compressed array has invalid deflate length symbol");
|
|
931
|
+
}
|
|
932
|
+
const length = DEFLATE_LENGTH_BASE[lengthIndex] + reader.readBits(DEFLATE_LENGTH_EXTRA[lengthIndex]),
|
|
933
|
+
distanceSymbol = distanceTable.decode(reader);
|
|
934
|
+
if (distanceSymbol < 0 || distanceSymbol >= DEFLATE_DISTANCE_BASE.length) {
|
|
935
|
+
throw new Error("fbx: compressed array has invalid deflate distance symbol");
|
|
936
|
+
}
|
|
937
|
+
const distance = DEFLATE_DISTANCE_BASE[distanceSymbol] + reader.readBits(DEFLATE_DISTANCE_EXTRA[distanceSymbol]);
|
|
938
|
+
copyInflatedBytes(state, distance, length);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function getFixedDeflateTables() {
|
|
942
|
+
if (!fixedDeflateTables) {
|
|
943
|
+
const literalLengths = new Array(288).fill(0);
|
|
944
|
+
for (let symbol = 0; symbol <= 143; symbol++) {
|
|
945
|
+
literalLengths[symbol] = 8;
|
|
946
|
+
}
|
|
947
|
+
for (let symbol = 144; symbol <= 255; symbol++) {
|
|
948
|
+
literalLengths[symbol] = 9;
|
|
949
|
+
}
|
|
950
|
+
for (let symbol = 256; symbol <= 279; symbol++) {
|
|
951
|
+
literalLengths[symbol] = 7;
|
|
952
|
+
}
|
|
953
|
+
for (let symbol = 280; symbol <= 287; symbol++) {
|
|
954
|
+
literalLengths[symbol] = 8;
|
|
955
|
+
}
|
|
956
|
+
fixedDeflateTables = {
|
|
957
|
+
literalLength: new DeflateHuffmanTable(literalLengths),
|
|
958
|
+
distance: new DeflateHuffmanTable(new Array(32).fill(5))
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
return fixedDeflateTables;
|
|
962
|
+
}
|
|
963
|
+
function readDynamicDeflateTables(reader) {
|
|
964
|
+
const literalLengthCount = reader.readBits(5) + 257,
|
|
965
|
+
distanceCount = reader.readBits(5) + 1,
|
|
966
|
+
codeLengthCount = reader.readBits(4) + 4,
|
|
967
|
+
codeLengthLengths = new Array(19).fill(0);
|
|
968
|
+
for (let i = 0; i < codeLengthCount; i++) {
|
|
969
|
+
codeLengthLengths[DEFLATE_CODE_LENGTH_ORDER[i]] = reader.readBits(3);
|
|
970
|
+
}
|
|
971
|
+
const codeLengthTable = new DeflateHuffmanTable(codeLengthLengths),
|
|
972
|
+
totalCount = literalLengthCount + distanceCount,
|
|
973
|
+
lengths = [];
|
|
974
|
+
while (lengths.length < totalCount) {
|
|
975
|
+
const symbol = codeLengthTable.decode(reader);
|
|
976
|
+
if (symbol <= 15) {
|
|
977
|
+
lengths.push(symbol);
|
|
978
|
+
} else if (symbol === 16) {
|
|
979
|
+
if (!lengths.length) {
|
|
980
|
+
throw new Error("fbx: compressed array repeats missing deflate code length");
|
|
981
|
+
}
|
|
982
|
+
repeatCodeLength(lengths, lengths[lengths.length - 1], reader.readBits(2) + 3, totalCount);
|
|
983
|
+
} else if (symbol === 17) {
|
|
984
|
+
repeatCodeLength(lengths, 0, reader.readBits(3) + 3, totalCount);
|
|
985
|
+
} else if (symbol === 18) {
|
|
986
|
+
repeatCodeLength(lengths, 0, reader.readBits(7) + 11, totalCount);
|
|
987
|
+
} else {
|
|
988
|
+
throw new Error("fbx: compressed array has invalid code length symbol");
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
literalLength: new DeflateHuffmanTable(lengths.slice(0, literalLengthCount)),
|
|
993
|
+
distance: new DeflateHuffmanTable(lengths.slice(literalLengthCount))
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function repeatCodeLength(lengths, value, count, totalCount) {
|
|
997
|
+
if (lengths.length + count > totalCount) {
|
|
998
|
+
throw new Error("fbx: compressed array code lengths overflow");
|
|
999
|
+
}
|
|
1000
|
+
for (let i = 0; i < count; i++) {
|
|
1001
|
+
lengths.push(value);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function writeInflatedByte(state, value) {
|
|
1005
|
+
if (state.offset >= state.output.byteLength) {
|
|
1006
|
+
throw new Error("fbx: compressed array decoded past expected length");
|
|
1007
|
+
}
|
|
1008
|
+
state.output[state.offset++] = value & 0xff;
|
|
1009
|
+
}
|
|
1010
|
+
function copyInflatedBytes(state, distance, length) {
|
|
1011
|
+
if (distance <= 0 || distance > state.offset) {
|
|
1012
|
+
throw new Error("fbx: compressed array has invalid deflate distance");
|
|
1013
|
+
}
|
|
1014
|
+
for (let i = 0; i < length; i++) {
|
|
1015
|
+
writeInflatedByte(state, state.output[state.offset - distance]);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function adler32(bytes) {
|
|
1019
|
+
let a = 1,
|
|
1020
|
+
b = 0;
|
|
1021
|
+
for (const byte of bytes) {
|
|
1022
|
+
a += byte;
|
|
1023
|
+
b += a;
|
|
1024
|
+
a %= ADLER32_MOD;
|
|
1025
|
+
b %= ADLER32_MOD;
|
|
1026
|
+
}
|
|
1027
|
+
return (b << 16 >>> 0 | a) >>> 0;
|
|
1028
|
+
}
|
|
1029
|
+
class DeflateBitReader {
|
|
1030
|
+
constructor(bytes) {
|
|
1031
|
+
this.bytes = bytes;
|
|
1032
|
+
this.byteOffset = 0;
|
|
1033
|
+
this.bitBuffer = 0;
|
|
1034
|
+
this.bitCount = 0;
|
|
1035
|
+
}
|
|
1036
|
+
readBits(count) {
|
|
1037
|
+
let value = 0,
|
|
1038
|
+
shift = 0,
|
|
1039
|
+
remaining = count;
|
|
1040
|
+
while (remaining > 0) {
|
|
1041
|
+
if (this.bitCount === 0) {
|
|
1042
|
+
if (this.byteOffset >= this.bytes.byteLength) {
|
|
1043
|
+
throw new Error("fbx: compressed array deflate stream is truncated");
|
|
1044
|
+
}
|
|
1045
|
+
this.bitBuffer = this.bytes[this.byteOffset++];
|
|
1046
|
+
this.bitCount = 8;
|
|
1047
|
+
}
|
|
1048
|
+
const take = Math.min(remaining, this.bitCount);
|
|
1049
|
+
value |= (this.bitBuffer & (1 << take) - 1) << shift;
|
|
1050
|
+
this.bitBuffer >>>= take;
|
|
1051
|
+
this.bitCount -= take;
|
|
1052
|
+
shift += take;
|
|
1053
|
+
remaining -= take;
|
|
1054
|
+
}
|
|
1055
|
+
return value;
|
|
1056
|
+
}
|
|
1057
|
+
alignByte() {
|
|
1058
|
+
this.bitBuffer = 0;
|
|
1059
|
+
this.bitCount = 0;
|
|
1060
|
+
}
|
|
1061
|
+
readAlignedByte() {
|
|
1062
|
+
if (this.byteOffset >= this.bytes.byteLength) {
|
|
1063
|
+
throw new Error("fbx: compressed array stored block is truncated");
|
|
1064
|
+
}
|
|
1065
|
+
return this.bytes[this.byteOffset++];
|
|
1066
|
+
}
|
|
1067
|
+
readAlignedU16LE() {
|
|
1068
|
+
const low = this.readAlignedByte(),
|
|
1069
|
+
high = this.readAlignedByte();
|
|
1070
|
+
return low | high << 8;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
class DeflateHuffmanTable {
|
|
1074
|
+
constructor(lengths) {
|
|
1075
|
+
this.tables = [];
|
|
1076
|
+
this.maxBits = 0;
|
|
1077
|
+
const counts = new Array(16).fill(0);
|
|
1078
|
+
for (const length of lengths) {
|
|
1079
|
+
if (length < 0 || length > 15) {
|
|
1080
|
+
throw new Error("fbx: compressed array has invalid deflate code length");
|
|
1081
|
+
}
|
|
1082
|
+
if (length) {
|
|
1083
|
+
counts[length]++;
|
|
1084
|
+
this.maxBits = Math.max(this.maxBits, length);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
let code = 0;
|
|
1088
|
+
const nextCodes = new Array(16).fill(0);
|
|
1089
|
+
for (let bits = 1; bits <= 15; bits++) {
|
|
1090
|
+
code = code + counts[bits - 1] << 1;
|
|
1091
|
+
nextCodes[bits] = code;
|
|
1092
|
+
}
|
|
1093
|
+
for (let symbol = 0; symbol < lengths.length; symbol++) {
|
|
1094
|
+
const length = lengths[symbol];
|
|
1095
|
+
if (!length) {
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
const reversed = reverseBits(nextCodes[length], length);
|
|
1099
|
+
nextCodes[length]++;
|
|
1100
|
+
if (!this.tables[length]) {
|
|
1101
|
+
this.tables[length] = [];
|
|
1102
|
+
}
|
|
1103
|
+
this.tables[length][reversed] = symbol;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
decode(reader) {
|
|
1107
|
+
let code = 0;
|
|
1108
|
+
for (let length = 1; length <= this.maxBits; length++) {
|
|
1109
|
+
code |= reader.readBits(1) << length - 1;
|
|
1110
|
+
const table = this.tables[length];
|
|
1111
|
+
if (table && table[code] !== undefined) {
|
|
1112
|
+
return table[code];
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
throw new Error("fbx: compressed array has invalid deflate code");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function reverseBits(value, length) {
|
|
1119
|
+
let reversed = 0;
|
|
1120
|
+
for (let i = 0; i < length; i++) {
|
|
1121
|
+
reversed = reversed << 1 | value & 1;
|
|
1122
|
+
value >>>= 1;
|
|
1123
|
+
}
|
|
1124
|
+
return reversed;
|
|
1125
|
+
}
|
|
1126
|
+
function parseAsciiDocument(bytes, metadata, values) {
|
|
1127
|
+
const text = TEXT_DECODER.decode(bytes);
|
|
1128
|
+
const nodes = [];
|
|
1129
|
+
const stack = [{
|
|
1130
|
+
children: nodes
|
|
1131
|
+
}];
|
|
1132
|
+
let nodeCount = 0;
|
|
1133
|
+
for (const rawLine of text.split(/\r?\n/u)) {
|
|
1134
|
+
const line = stripAsciiComment(rawLine).trim();
|
|
1135
|
+
if (!line) {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
if (line.startsWith("}")) {
|
|
1139
|
+
if (stack.length > 1) {
|
|
1140
|
+
stack.pop();
|
|
1141
|
+
}
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
const node = parseAsciiLine(line, values);
|
|
1145
|
+
if (!node) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
stack[stack.length - 1].children.push(node);
|
|
1149
|
+
nodeCount++;
|
|
1150
|
+
if (nodeCount > values.maxNodes) {
|
|
1151
|
+
throw new Error(`fbx: node count exceeds maxNodes (${values.maxNodes})`);
|
|
1152
|
+
}
|
|
1153
|
+
if (node.opensBlock) {
|
|
1154
|
+
if (stack.length >= values.maxDepth) {
|
|
1155
|
+
throw new Error(`fbx: node depth exceeds maxDepth (${values.maxDepth})`);
|
|
1156
|
+
}
|
|
1157
|
+
delete node.opensBlock;
|
|
1158
|
+
stack.push(node);
|
|
1159
|
+
} else {
|
|
1160
|
+
delete node.opensBlock;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const document = {
|
|
1164
|
+
...metadata,
|
|
1165
|
+
payloadType: OUTPUT_FBX_JSON,
|
|
1166
|
+
nodes,
|
|
1167
|
+
nodeCount,
|
|
1168
|
+
rootNodeCount: nodes.length,
|
|
1169
|
+
rootNodeNames: nodes.map(node => node.name)
|
|
1170
|
+
};
|
|
1171
|
+
document.root = buildFbxRoot(document.nodes);
|
|
1172
|
+
return document;
|
|
1173
|
+
}
|
|
1174
|
+
function readBinaryTargetNodes(bytes, metadata, values, targetNames) {
|
|
1175
|
+
const nodes = [];
|
|
1176
|
+
const headerSize = getBinaryNodeHeaderSize(metadata.version);
|
|
1177
|
+
let offset = BINARY_HEADER_SIZE;
|
|
1178
|
+
let nodeCount = 0;
|
|
1179
|
+
while (offset + headerSize <= bytes.byteLength) {
|
|
1180
|
+
if (isNullRecord(bytes, offset, headerSize)) {
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
const header = readBinaryNodeHeader(bytes, offset, metadata.version);
|
|
1184
|
+
if (!header) {
|
|
1185
|
+
throw new Error(`fbx: invalid root node header at offset ${offset}`);
|
|
1186
|
+
}
|
|
1187
|
+
if (header.endOffset <= offset || header.endOffset > bytes.byteLength) {
|
|
1188
|
+
throw new Error(`fbx: invalid root node ${header.name}`);
|
|
1189
|
+
}
|
|
1190
|
+
if (targetNames.has(header.name)) {
|
|
1191
|
+
const result = readBinaryNode(bytes, offset, metadata.version, values, 0);
|
|
1192
|
+
nodes.push(result.node);
|
|
1193
|
+
nodeCount += result.count;
|
|
1194
|
+
if (nodeCount > values.maxNodes) {
|
|
1195
|
+
throw new Error(`fbx: node count exceeds maxNodes (${values.maxNodes})`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
offset = header.endOffset;
|
|
1199
|
+
}
|
|
1200
|
+
return nodes;
|
|
1201
|
+
}
|
|
1202
|
+
function readAsciiTargetNodes(bytes, values, targetNames) {
|
|
1203
|
+
const text = TEXT_DECODER.decode(bytes);
|
|
1204
|
+
const nodes = [];
|
|
1205
|
+
const stack = [{
|
|
1206
|
+
children: nodes,
|
|
1207
|
+
collecting: true
|
|
1208
|
+
}];
|
|
1209
|
+
let nodeCount = 0;
|
|
1210
|
+
for (const rawLine of text.split(/\r?\n/u)) {
|
|
1211
|
+
const line = stripAsciiComment(rawLine).trim();
|
|
1212
|
+
if (!line) {
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
if (line.startsWith("}")) {
|
|
1216
|
+
if (stack.length > 1) {
|
|
1217
|
+
stack.pop();
|
|
1218
|
+
}
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
const node = parseAsciiLine(line, values);
|
|
1222
|
+
if (!node) {
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
nodeCount++;
|
|
1226
|
+
if (nodeCount > values.maxNodes) {
|
|
1227
|
+
throw new Error(`fbx: node count exceeds maxNodes (${values.maxNodes})`);
|
|
1228
|
+
}
|
|
1229
|
+
const isRoot = stack.length === 1;
|
|
1230
|
+
const parent = stack[stack.length - 1];
|
|
1231
|
+
const collecting = isRoot ? targetNames.has(node.name) : parent.collecting;
|
|
1232
|
+
if (collecting) {
|
|
1233
|
+
parent.children.push(node);
|
|
1234
|
+
}
|
|
1235
|
+
if (node.opensBlock) {
|
|
1236
|
+
if (stack.length >= values.maxDepth) {
|
|
1237
|
+
throw new Error(`fbx: node depth exceeds maxDepth (${values.maxDepth})`);
|
|
1238
|
+
}
|
|
1239
|
+
delete node.opensBlock;
|
|
1240
|
+
stack.push(collecting ? {
|
|
1241
|
+
children: node.children,
|
|
1242
|
+
collecting: true
|
|
1243
|
+
} : {
|
|
1244
|
+
children: [],
|
|
1245
|
+
collecting: false
|
|
1246
|
+
});
|
|
1247
|
+
} else {
|
|
1248
|
+
delete node.opensBlock;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return nodes;
|
|
1252
|
+
}
|
|
1253
|
+
function buildGr2FromFbxNodes(nodes, values) {
|
|
1254
|
+
const objectsNode = findFirstNode(nodes, "Objects");
|
|
1255
|
+
if (!objectsNode) {
|
|
1256
|
+
throw new Error("fbx: GR2 output requires an Objects node");
|
|
1257
|
+
}
|
|
1258
|
+
const objectIndex = indexObjectNodes(objectsNode),
|
|
1259
|
+
connections = readConnections(findFirstNode(nodes, "Connections")),
|
|
1260
|
+
sceneTransform = buildSceneTransform(findFirstNode(nodes, "GlobalSettings")),
|
|
1261
|
+
modelWorldCache = new Map(),
|
|
1262
|
+
skeletonContext = buildSkeletonContext(objectIndex, connections, sceneTransform, modelWorldCache),
|
|
1263
|
+
meshes = [];
|
|
1264
|
+
for (const geometry of objectIndex.list.filter(entry => entry.nodeName === "Geometry" && (!entry.className || entry.className === "Mesh"))) {
|
|
1265
|
+
const owners = connectedParentObjects(geometry, objectIndex, connections, "Model");
|
|
1266
|
+
for (const owner of owners.length ? owners : [null]) {
|
|
1267
|
+
meshes.push(buildGr2MeshFromGeometry(geometry, owner, objectIndex, connections, sceneTransform, modelWorldCache, skeletonContext, values));
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (!meshes.length) {
|
|
1271
|
+
throw new Error("fbx: no supported Geometry mesh nodes found");
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
grannyFileFormatRevision: 0,
|
|
1275
|
+
grannyFileSource: values.source || "memory",
|
|
1276
|
+
meshes,
|
|
1277
|
+
models: buildGr2Models(skeletonContext.skeletons, meshes),
|
|
1278
|
+
skeletons: skeletonContext.skeletons.map(skeleton => skeleton.cmfSkeleton),
|
|
1279
|
+
animations: [],
|
|
1280
|
+
cmfAnimations: readCmfAnimations(objectIndex, connections, meshes, skeletonContext, sceneTransform)
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function buildGr2MeshFromGeometry(geometry, owner, objectIndex, connections, sceneTransform, modelWorldCache, skeletonContext, values) {
|
|
1284
|
+
const controlPoints = requireChildArray(geometry.node, "Vertices"),
|
|
1285
|
+
polygonVertexIndex = requireChildArray(geometry.node, "PolygonVertexIndex");
|
|
1286
|
+
if (controlPoints.length % 3 !== 0) {
|
|
1287
|
+
throw new Error(`fbx: geometry ${JSON.stringify(geometry.name || geometry.key)} has invalid Vertices length`);
|
|
1288
|
+
}
|
|
1289
|
+
const decoded = decodePolygonCorners(controlPoints, polygonVertexIndex, geometry),
|
|
1290
|
+
meshTransform = buildMeshGeometryToWorld(owner, objectIndex, connections, modelWorldCache),
|
|
1291
|
+
positions = transformPoints(decoded.positions, meshTransform, sceneTransform),
|
|
1292
|
+
normal = transformDirections(readLayerElementChannel(geometry.node, "LayerElementNormal", 0, ["Normals"], ["NormalsIndex", "NormalIndex"], 3, decoded, "Normals"), meshTransform, sceneTransform),
|
|
1293
|
+
explicitTangent = transformDirections(readLayerElementChannel(geometry.node, "LayerElementTangent", 0, ["Tangents"], ["TangentsIndex", "TangentIndex"], 3, decoded, "Tangents"), meshTransform, sceneTransform),
|
|
1294
|
+
explicitBinormal = transformDirections(readLayerElementChannel(geometry.node, "LayerElementBinormal", 0, ["Binormals"], ["BinormalsIndex", "BinormalIndex"], 3, decoded, "Binormals"), meshTransform, sceneTransform),
|
|
1295
|
+
texcoord0 = readUvLayerElementChannel(geometry.node, 0, decoded, values),
|
|
1296
|
+
texcoord1 = readUvLayerElementChannel(geometry.node, 1, decoded, values),
|
|
1297
|
+
color0 = readLayerElementChannel(geometry.node, "LayerElementColor", 0, ["Colors"], ["ColorIndex", "ColorsIndex", "ColorIndices"], 4, decoded, "Colors"),
|
|
1298
|
+
polygonTriangles = triangulatePolygonList(decoded.polygons, decoded.positions),
|
|
1299
|
+
triangleFaces = flattenPolygonTriangles(polygonTriangles),
|
|
1300
|
+
tangent = explicitTangent.length ? explicitTangent : generateBaseTangents(positions, normal, texcoord0, triangleFaces),
|
|
1301
|
+
binormal = explicitBinormal.length ? explicitBinormal : generateBaseBinormals(normal, tangent),
|
|
1302
|
+
materialNames = readPolygonMaterialNames(geometry, owner, objectIndex, connections, decoded.polygons.length),
|
|
1303
|
+
indices = buildIndexGroups(polygonTriangles, materialNames),
|
|
1304
|
+
skin = readSkinning(geometry, objectIndex, connections, decoded, positions, sceneTransform, modelWorldCache, skeletonContext),
|
|
1305
|
+
morphTargets = readMorphTargets(geometry, owner, objectIndex, connections, decoded, controlPoints, meshTransform, sceneTransform, positions, triangleFaces, {
|
|
1306
|
+
normal,
|
|
1307
|
+
tangent,
|
|
1308
|
+
binormal,
|
|
1309
|
+
texcoord0
|
|
1310
|
+
}),
|
|
1311
|
+
bounds = computeBounds(positions);
|
|
1312
|
+
return {
|
|
1313
|
+
name: meshNameFromFbx(geometry, owner),
|
|
1314
|
+
morphTargets,
|
|
1315
|
+
minBounds: bounds.minBounds,
|
|
1316
|
+
maxBounds: bounds.maxBounds,
|
|
1317
|
+
boneBindings: skin.boneBindings,
|
|
1318
|
+
vertex: createGr2Vertex(positions, {
|
|
1319
|
+
tangent,
|
|
1320
|
+
normal,
|
|
1321
|
+
texcoord0,
|
|
1322
|
+
texcoord1,
|
|
1323
|
+
binormal,
|
|
1324
|
+
color0,
|
|
1325
|
+
blendIndice: skin.blendIndice,
|
|
1326
|
+
blendWeight: skin.blendWeight
|
|
1327
|
+
}),
|
|
1328
|
+
indices,
|
|
1329
|
+
skeleton: skin.skeleton
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function readSkinning(geometry, objectIndex, connections, decoded, positions, sceneTransform, modelWorldCache, skeletonContext) {
|
|
1333
|
+
const skin = connectedChildObjects(geometry, objectIndex, connections, "Deformer").find(isSkinDeformer);
|
|
1334
|
+
if (!skin) {
|
|
1335
|
+
return emptySkinning();
|
|
1336
|
+
}
|
|
1337
|
+
const clusters = connectedChildObjects(skin, objectIndex, connections, "Deformer").filter(isClusterDeformer);
|
|
1338
|
+
const clusterRecords = buildSkinClusterRecords(clusters, objectIndex, connections);
|
|
1339
|
+
if (!clusterRecords.length) {
|
|
1340
|
+
return emptySkinning();
|
|
1341
|
+
}
|
|
1342
|
+
const boneKeys = clusterRecords.map(record => record.boneKey),
|
|
1343
|
+
controlPointInfluences = buildControlPointInfluences(clusterRecords),
|
|
1344
|
+
outputInfluences = decoded.corners.map(corner => normalizeInfluences(controlPointInfluences.get(corner.controlPointIndex) || [])),
|
|
1345
|
+
blendIndice = [],
|
|
1346
|
+
blendWeight = [],
|
|
1347
|
+
boneBindings = buildBoneBindingsFromInfluences(clusterRecords, outputInfluences, positions, objectIndex, connections, sceneTransform, modelWorldCache),
|
|
1348
|
+
skeleton = findSkinSkeletonIndex(boneKeys, skeletonContext);
|
|
1349
|
+
for (const influences of outputInfluences) {
|
|
1350
|
+
for (let i = 0; i < 4; i++) {
|
|
1351
|
+
blendIndice.push(influences[i]?.boneIndex ?? 0);
|
|
1352
|
+
}
|
|
1353
|
+
for (let i = 0; i < 4; i++) {
|
|
1354
|
+
blendWeight.push(influences[i]?.weight ?? 0);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return {
|
|
1358
|
+
boneBindings,
|
|
1359
|
+
blendIndice,
|
|
1360
|
+
blendWeight,
|
|
1361
|
+
boneKeys,
|
|
1362
|
+
skeleton
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function emptySkinning() {
|
|
1366
|
+
return {
|
|
1367
|
+
boneBindings: [],
|
|
1368
|
+
blendIndice: [],
|
|
1369
|
+
blendWeight: [],
|
|
1370
|
+
boneKeys: [],
|
|
1371
|
+
skeleton: null
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Generate base mesh tangents when normal and UV channels are available.
|
|
1377
|
+
*
|
|
1378
|
+
* @param {number[]} positions Expanded position channel.
|
|
1379
|
+
* @param {number[]} normal Expanded normal channel.
|
|
1380
|
+
* @param {number[]} texcoord0 Expanded primary UV channel.
|
|
1381
|
+
* @param {number[]} triangleFaces Triangulated index buffer.
|
|
1382
|
+
* @returns {number[]} Expanded tangent channel.
|
|
1383
|
+
*/
|
|
1384
|
+
function generateBaseTangents(positions, normal, texcoord0, triangleFaces) {
|
|
1385
|
+
if (!hasArrayValues(normal) || !hasArrayValues(texcoord0)) {
|
|
1386
|
+
return [];
|
|
1387
|
+
}
|
|
1388
|
+
return cleanNumericArray(generateTangents(positions, normal, texcoord0, triangleFaces));
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Generate base mesh binormals when normals and tangents are available.
|
|
1393
|
+
*
|
|
1394
|
+
* @param {number[]} normal Expanded normal channel.
|
|
1395
|
+
* @param {number[]} tangent Expanded tangent channel.
|
|
1396
|
+
* @returns {number[]} Expanded binormal channel.
|
|
1397
|
+
*/
|
|
1398
|
+
function generateBaseBinormals(normal, tangent) {
|
|
1399
|
+
if (!hasArrayValues(normal) || !hasArrayValues(tangent)) {
|
|
1400
|
+
return [];
|
|
1401
|
+
}
|
|
1402
|
+
return cleanNumericArray(generateBiNormals(normal, tangent));
|
|
1403
|
+
}
|
|
1404
|
+
function readMorphTargets(geometry, owner, objectIndex, connections, decoded, controlPoints, meshTransform, sceneTransform, basePositions, triangleFaces, baseVertex) {
|
|
1405
|
+
const blendShapes = connectedChildObjects(geometry, objectIndex, connections, "Deformer").filter(deformer => deformer.className === "BlendShape" || deformer.fullName.startsWith("Deformer::BlendShape") || deformer.name.includes("BlendShape"));
|
|
1406
|
+
const targets = [];
|
|
1407
|
+
for (const blendShape of blendShapes) {
|
|
1408
|
+
const channels = connectedChildObjects(blendShape, objectIndex, connections, "Deformer").filter(deformer => deformer.className === "BlendShapeChannel" || deformer.fullName.startsWith("SubDeformer::") || deformer.name.includes("BlendShapeChannel"));
|
|
1409
|
+
for (const channel of channels) {
|
|
1410
|
+
const shapes = readBlendShapeChannelShapes(channel, objectIndex, connections);
|
|
1411
|
+
if (isUnsupportedBlendShapeInBetween(channel, shapes)) {
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
for (const shape of shapes) {
|
|
1415
|
+
const target = buildMorphTarget(shape, channel, geometry, owner, decoded, controlPoints, meshTransform, sceneTransform, basePositions, triangleFaces, baseVertex);
|
|
1416
|
+
if (target) {
|
|
1417
|
+
targets.push(target);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return targets;
|
|
1423
|
+
}
|
|
1424
|
+
function buildMorphTarget(shape, channel, geometry, owner, decoded, controlPoints, meshTransform, sceneTransform, basePositions, triangleFaces, baseVertex) {
|
|
1425
|
+
const indexes = readChildArray(shape.node, "Indexes") || [],
|
|
1426
|
+
vertices = readChildArray(shape.node, "Vertices") || [];
|
|
1427
|
+
if (!indexes.length || !vertices.length) {
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
if (vertices.length % 3 !== 0) {
|
|
1431
|
+
throw new Error(`fbx: morph target ${JSON.stringify(shape.name || shape.key)} has invalid Vertices length`);
|
|
1432
|
+
}
|
|
1433
|
+
if (vertices.length / 3 < indexes.length) {
|
|
1434
|
+
throw new Error(`fbx: morph target ${JSON.stringify(shape.name || shape.key)} has fewer Vertices than Indexes`);
|
|
1435
|
+
}
|
|
1436
|
+
const controlTargetPositions = controlPoints.slice();
|
|
1437
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
1438
|
+
const controlPointIndex = integerNumber(indexes[i], "Indexes"),
|
|
1439
|
+
targetOffset = i * 3,
|
|
1440
|
+
controlOffset = controlPointIndex * 3;
|
|
1441
|
+
if (controlOffset < 0 || controlOffset + 2 >= controlTargetPositions.length) {
|
|
1442
|
+
throw new Error(`fbx: morph target ${JSON.stringify(shape.name || shape.key)} has Indexes value out of range`);
|
|
1443
|
+
}
|
|
1444
|
+
controlTargetPositions[controlOffset] = finiteNumber(controlPoints[controlOffset], "Vertices") + finiteNumber(vertices[targetOffset], "Vertices");
|
|
1445
|
+
controlTargetPositions[controlOffset + 1] = finiteNumber(controlPoints[controlOffset + 1], "Vertices") + finiteNumber(vertices[targetOffset + 1], "Vertices");
|
|
1446
|
+
controlTargetPositions[controlOffset + 2] = finiteNumber(controlPoints[controlOffset + 2], "Vertices") + finiteNumber(vertices[targetOffset + 2], "Vertices");
|
|
1447
|
+
}
|
|
1448
|
+
const expanded = [];
|
|
1449
|
+
for (const corner of decoded.corners) {
|
|
1450
|
+
const source = corner.controlPointIndex * 3;
|
|
1451
|
+
expanded.push(controlTargetPositions[source], controlTargetPositions[source + 1], controlTargetPositions[source + 2]);
|
|
1452
|
+
}
|
|
1453
|
+
const positions = transformPoints(expanded, meshTransform, sceneTransform),
|
|
1454
|
+
name = shape.name || channel.name || "morph_target",
|
|
1455
|
+
vertexChannels = buildMorphTargetVertexChannels(name, geometry, owner, decoded, meshTransform, sceneTransform, positions, triangleFaces, baseVertex);
|
|
1456
|
+
return {
|
|
1457
|
+
name,
|
|
1458
|
+
dataIsDeltas: false,
|
|
1459
|
+
maxDisplacement: computeMaxDisplacement(basePositions, positions),
|
|
1460
|
+
vertex: createGr2Vertex(positions, vertexChannels)
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function buildMorphTargetVertexChannels(name, geometry, owner, decoded, meshTransform, sceneTransform, positions, triangleFaces, baseVertex) {
|
|
1464
|
+
let normal = readMorphTargetCustomNormals(name, geometry, owner, decoded, meshTransform, sceneTransform);
|
|
1465
|
+
if (!normal.length && hasArrayValues(baseVertex.normal)) {
|
|
1466
|
+
normal = cleanNumericArray(generateNormals(positions, triangleFaces));
|
|
1467
|
+
}
|
|
1468
|
+
const tangent = normal.length && hasArrayValues(baseVertex.tangent) && hasArrayValues(baseVertex.texcoord0) ? cleanNumericArray(generateTangents(positions, normal, baseVertex.texcoord0, triangleFaces)) : [];
|
|
1469
|
+
const binormal = normal.length && tangent.length && hasArrayValues(baseVertex.binormal) ? cleanNumericArray(generateBiNormals(normal, tangent)) : [];
|
|
1470
|
+
return {
|
|
1471
|
+
normal,
|
|
1472
|
+
tangent,
|
|
1473
|
+
binormal
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function readMorphTargetCustomNormals(name, geometry, owner, decoded, meshTransform, sceneTransform) {
|
|
1477
|
+
const encoded = readMorphTargetCustomNormalString(name, owner, geometry);
|
|
1478
|
+
if (!encoded) {
|
|
1479
|
+
return [];
|
|
1480
|
+
}
|
|
1481
|
+
return transformDirections(decodeBase64Float32Vector3Array(encoded, decoded.corners.length, `morph target custom normals ${JSON.stringify(name)}`), meshTransform, sceneTransform);
|
|
1482
|
+
}
|
|
1483
|
+
function readMorphTargetCustomNormalString(name, owner, geometry) {
|
|
1484
|
+
const propertyName = `bsNormals_${name}`;
|
|
1485
|
+
for (const entry of [owner, geometry]) {
|
|
1486
|
+
const value = readProperties70(entry?.node).properties[propertyName]?.value;
|
|
1487
|
+
if (typeof value === "string") {
|
|
1488
|
+
return value;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return "";
|
|
1492
|
+
}
|
|
1493
|
+
function decodeBase64Float32Vector3Array(encoded, vertexCount, feature) {
|
|
1494
|
+
const bytes = decodeBase64Bytes(encoded, feature),
|
|
1495
|
+
expectedByteLength = vertexCount * 3 * 4;
|
|
1496
|
+
if (bytes.byteLength !== expectedByteLength) {
|
|
1497
|
+
throw new Error(`fbx: ${feature} byte length ${bytes.byteLength} does not match expected ${expectedByteLength}`);
|
|
1498
|
+
}
|
|
1499
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength),
|
|
1500
|
+
values = [];
|
|
1501
|
+
for (let offset = 0; offset < bytes.byteLength; offset += 4) {
|
|
1502
|
+
values.push(finiteNumber(view.getFloat32(offset, true), feature));
|
|
1503
|
+
}
|
|
1504
|
+
return values;
|
|
1505
|
+
}
|
|
1506
|
+
function decodeBase64Bytes(encoded, feature) {
|
|
1507
|
+
const bytes = [];
|
|
1508
|
+
let buffer = 0,
|
|
1509
|
+
bits = 0;
|
|
1510
|
+
for (const char of String(encoded)) {
|
|
1511
|
+
if (/\s/u.test(char)) {
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
if (char === "=") {
|
|
1515
|
+
break;
|
|
1516
|
+
}
|
|
1517
|
+
const value = BASE64_ALPHABET.indexOf(char);
|
|
1518
|
+
if (value === -1) {
|
|
1519
|
+
throw new Error(`fbx: ${feature} contains invalid base64 data`);
|
|
1520
|
+
}
|
|
1521
|
+
buffer = buffer << 6 | value;
|
|
1522
|
+
bits += 6;
|
|
1523
|
+
if (bits >= 8) {
|
|
1524
|
+
bits -= 8;
|
|
1525
|
+
bytes.push(buffer >> bits & 0xff);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return Uint8Array.from(bytes);
|
|
1529
|
+
}
|
|
1530
|
+
function computeMaxDisplacement(basePositions, targetPositions) {
|
|
1531
|
+
let maxDisplacement = 0;
|
|
1532
|
+
for (let i = 0; i < Math.min(basePositions.length, targetPositions.length); i += 3) {
|
|
1533
|
+
maxDisplacement = Math.max(maxDisplacement, Math.hypot(targetPositions[i] - basePositions[i], targetPositions[i + 1] - basePositions[i + 1], targetPositions[i + 2] - basePositions[i + 2]));
|
|
1534
|
+
}
|
|
1535
|
+
return cleanFloat(maxDisplacement);
|
|
1536
|
+
}
|
|
1537
|
+
function readCmfAnimations(objectIndex, connections, meshes, skeletonContext, sceneTransform) {
|
|
1538
|
+
const morphTargetNames = buildMorphTargetNameSet(meshes),
|
|
1539
|
+
boneTargets = buildCmfBoneTargetMap(skeletonContext);
|
|
1540
|
+
const animations = [];
|
|
1541
|
+
for (const stack of objectIndex.list.filter(entry => entry.nodeName === "AnimationStack")) {
|
|
1542
|
+
const animation = readCmfAnimation(stack, objectIndex, connections, morphTargetNames, boneTargets, sceneTransform);
|
|
1543
|
+
if (animation) {
|
|
1544
|
+
animations.push(animation);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return animations;
|
|
1548
|
+
}
|
|
1549
|
+
function buildMorphTargetNameSet(meshes) {
|
|
1550
|
+
const names = new Set();
|
|
1551
|
+
for (const mesh of meshes) {
|
|
1552
|
+
for (const target of mesh.morphTargets ?? []) {
|
|
1553
|
+
if (target.name) {
|
|
1554
|
+
names.add(target.name);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return names;
|
|
1559
|
+
}
|
|
1560
|
+
function buildCmfBoneTargetMap(skeletonContext) {
|
|
1561
|
+
const targets = new Map();
|
|
1562
|
+
for (const skeleton of skeletonContext.skeletons) {
|
|
1563
|
+
for (let i = 0; i < skeleton.boneKeys.length; i++) {
|
|
1564
|
+
targets.set(skeleton.boneKeys[i], {
|
|
1565
|
+
name: skeleton.cmfSkeleton.bones[i] || `bone_${i}`,
|
|
1566
|
+
isRoot: skeleton.cmfSkeleton.parents[i] === 0xffffffff
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return targets;
|
|
1571
|
+
}
|
|
1572
|
+
function readCmfAnimation(stack, objectIndex, connections, morphTargetNames, boneTargets, sceneTransform) {
|
|
1573
|
+
const layers = connectedChildObjects(stack, objectIndex, connections, "AnimationLayer"),
|
|
1574
|
+
curveNodeParents = layers.length ? layers : [stack],
|
|
1575
|
+
curveNodes = uniqueEntries(curveNodeParents.flatMap(parent => connectedChildObjects(parent, objectIndex, connections, "AnimationCurveNode"))),
|
|
1576
|
+
records = [];
|
|
1577
|
+
for (const curveNode of curveNodes) {
|
|
1578
|
+
records.push(...readMorphAnimationRecords(curveNode, objectIndex, connections, morphTargetNames));
|
|
1579
|
+
records.push(...readRootPropertyMorphAnimationRecords(curveNode, objectIndex, connections, morphTargetNames, boneTargets));
|
|
1580
|
+
const boneRecord = readBoneAnimationRecord(curveNode, objectIndex, connections, boneTargets, sceneTransform);
|
|
1581
|
+
if (boneRecord) {
|
|
1582
|
+
records.push(boneRecord);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (!records.length) {
|
|
1586
|
+
return null;
|
|
1587
|
+
}
|
|
1588
|
+
const stackTimeSpan = readAnimationStackTimeSpan(stack, records);
|
|
1589
|
+
const channels = [],
|
|
1590
|
+
curves = [];
|
|
1591
|
+
let duration = 0;
|
|
1592
|
+
for (const record of records) {
|
|
1593
|
+
const knots = record.curve.ticks.map(tick => cleanFloat((tick - stackTimeSpan.startTick) / FBX_TICKS_PER_SECOND)),
|
|
1594
|
+
values = record.curve.values.map(cleanFloat),
|
|
1595
|
+
curveIndex = curves.length;
|
|
1596
|
+
if (values.length !== knots.length * record.valueDimension) {
|
|
1597
|
+
throw new Error(`fbx: animation curve for ${JSON.stringify(record.target)} has mismatched value dimension`);
|
|
1598
|
+
}
|
|
1599
|
+
for (const knot of knots) {
|
|
1600
|
+
duration = Math.max(duration, knot);
|
|
1601
|
+
}
|
|
1602
|
+
curves.push({
|
|
1603
|
+
valueDimension: record.valueDimension,
|
|
1604
|
+
interpolation: "Linear",
|
|
1605
|
+
knotType: "Float32",
|
|
1606
|
+
valueType: "Float32",
|
|
1607
|
+
knotCount: knots.length,
|
|
1608
|
+
knots: packFloat32Values(knots),
|
|
1609
|
+
values: packFloat32Values(values)
|
|
1610
|
+
});
|
|
1611
|
+
channels.push({
|
|
1612
|
+
target: record.target,
|
|
1613
|
+
targetType: record.targetType,
|
|
1614
|
+
curveIndex
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
return {
|
|
1618
|
+
name: stack.name || "fbx_animation",
|
|
1619
|
+
channels,
|
|
1620
|
+
curves,
|
|
1621
|
+
duration: cleanFloat(stackTimeSpan.duration ?? duration)
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
function readAnimationStackTimeSpan(stack, records) {
|
|
1625
|
+
const properties = readProperties70(stack.node).properties,
|
|
1626
|
+
start = propertyNumber(properties, "LocalStart", propertyNumber(properties, "ReferenceStart", null)),
|
|
1627
|
+
stop = propertyNumber(properties, "LocalStop", propertyNumber(properties, "ReferenceStop", null));
|
|
1628
|
+
if (start !== null && stop !== null && stop >= start) {
|
|
1629
|
+
return {
|
|
1630
|
+
startTick: start,
|
|
1631
|
+
duration: (stop - start) / FBX_TICKS_PER_SECOND
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
let startTick = Infinity;
|
|
1635
|
+
for (const record of records) {
|
|
1636
|
+
for (const tick of record.curve.ticks) {
|
|
1637
|
+
startTick = Math.min(startTick, tick);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return {
|
|
1641
|
+
startTick,
|
|
1642
|
+
duration: null
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
function readMorphAnimationRecords(curveNode, objectIndex, connections, morphTargetNames) {
|
|
1646
|
+
const targets = readMorphAnimationTargets(curveNode, objectIndex, connections, morphTargetNames),
|
|
1647
|
+
curve = readMorphAnimationCurve(curveNode, objectIndex, connections);
|
|
1648
|
+
if (!targets.length || !curve) {
|
|
1649
|
+
return [];
|
|
1650
|
+
}
|
|
1651
|
+
return targets.map(target => ({
|
|
1652
|
+
target,
|
|
1653
|
+
targetType: "MorphTarget",
|
|
1654
|
+
valueDimension: 1,
|
|
1655
|
+
curve
|
|
1656
|
+
}));
|
|
1657
|
+
}
|
|
1658
|
+
function readRootPropertyMorphAnimationRecords(curveNode, objectIndex, connections, morphTargetNames, boneTargets) {
|
|
1659
|
+
const targetConnection = connections.list.find(connection => connection.relation === "OP" && connection.childKey === curveNode.key && !isBoneAnimationProperty(connection.property) && boneTargets.get(connection.parentKey)?.isRoot && isBoneModel(objectIndex.byId[connection.parentKey]) && isUserDefinedNumericProperty(objectIndex.byId[connection.parentKey], connection.property));
|
|
1660
|
+
if (!targetConnection) {
|
|
1661
|
+
return [];
|
|
1662
|
+
}
|
|
1663
|
+
const target = String(targetConnection.property || "");
|
|
1664
|
+
if (!target || morphTargetNames.size && !morphTargetNames.has(target)) {
|
|
1665
|
+
return [];
|
|
1666
|
+
}
|
|
1667
|
+
const curve = readAnyScalarAnimationCurve(curveNode, objectIndex, connections, 1);
|
|
1668
|
+
return curve ? [{
|
|
1669
|
+
target,
|
|
1670
|
+
targetType: "MorphTarget",
|
|
1671
|
+
valueDimension: 1,
|
|
1672
|
+
curve
|
|
1673
|
+
}] : [];
|
|
1674
|
+
}
|
|
1675
|
+
function readMorphAnimationTargets(curveNode, objectIndex, connections, morphTargetNames) {
|
|
1676
|
+
const targets = [];
|
|
1677
|
+
for (const connection of connections.list) {
|
|
1678
|
+
if (connection.relation !== "OP" || connection.childKey !== curveNode.key || !isMorphAnimationProperty(connection.property)) {
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
const target = objectIndex.byId[connection.parentKey];
|
|
1682
|
+
if (!isBlendShapeChannel(target)) {
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
for (const name of readBlendShapeChannelMorphNames(target, objectIndex, connections)) {
|
|
1686
|
+
if (!morphTargetNames.size || morphTargetNames.has(name)) {
|
|
1687
|
+
targets.push(name);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return Array.from(new Set(targets));
|
|
1692
|
+
}
|
|
1693
|
+
function readBlendShapeChannelMorphNames(channel, objectIndex, connections) {
|
|
1694
|
+
const shapes = readBlendShapeChannelShapes(channel, objectIndex, connections);
|
|
1695
|
+
if (isUnsupportedBlendShapeInBetween(channel, shapes)) {
|
|
1696
|
+
return [];
|
|
1697
|
+
}
|
|
1698
|
+
const names = shapes.map(shape => shape.name || channel.name).filter(Boolean);
|
|
1699
|
+
return names.length ? names : [channel.name || "morph_target"];
|
|
1700
|
+
}
|
|
1701
|
+
function readBlendShapeChannelShapes(channel, objectIndex, connections) {
|
|
1702
|
+
return connectedChildObjects(channel, objectIndex, connections, "Geometry").filter(shape => shape.className === "Shape" || shape.fullName.startsWith("Geometry::Shape") || shape.name);
|
|
1703
|
+
}
|
|
1704
|
+
function readBlendShapeChannelFullWeights(channel) {
|
|
1705
|
+
return readChildArray(channel.node, "FullWeights") || [];
|
|
1706
|
+
}
|
|
1707
|
+
function isUnsupportedBlendShapeInBetween(channel, shapes) {
|
|
1708
|
+
// Multiple shapes/weights under one FBX channel are in-between targets, not independent sliders.
|
|
1709
|
+
return shapes.length > 1 || readBlendShapeChannelFullWeights(channel).length > 1;
|
|
1710
|
+
}
|
|
1711
|
+
function readMorphAnimationCurve(curveNode, objectIndex, connections) {
|
|
1712
|
+
return readAnyScalarAnimationCurve(curveNode, objectIndex, connections, 0.01, isMorphAnimationProperty);
|
|
1713
|
+
}
|
|
1714
|
+
function readAnyScalarAnimationCurve(curveNode, objectIndex, connections, scale, predicate = null) {
|
|
1715
|
+
const curves = connections.list.filter(connection => connection.parentKey === curveNode.key).map(connection => ({
|
|
1716
|
+
connection,
|
|
1717
|
+
curve: objectIndex.byId[connection.childKey]
|
|
1718
|
+
})).filter(item => item.curve?.nodeName === "AnimationCurve");
|
|
1719
|
+
const match = predicate ? curves.find(item => predicate(item.connection.property)) || curves[0] : curves.find(item => animationCurveAxis(item.connection.property) === 0) || curves[0];
|
|
1720
|
+
return match ? readScalarAnimationCurve(match.curve, scale) : null;
|
|
1721
|
+
}
|
|
1722
|
+
function readScalarAnimationCurve(curve, scale) {
|
|
1723
|
+
const ticks = readChildArray(curve.node, "KeyTime") || [],
|
|
1724
|
+
keyValueFloat = readChildArray(curve.node, "KeyValueFloat"),
|
|
1725
|
+
keyValue = readChildArray(curve.node, "KeyValue"),
|
|
1726
|
+
valueFeature = hasArrayValues(keyValueFloat) ? "KeyValueFloat" : "KeyValue",
|
|
1727
|
+
values = hasArrayValues(keyValueFloat) ? keyValueFloat : hasArrayValues(keyValue) ? keyValue : [];
|
|
1728
|
+
if (!ticks.length || !values.length) {
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
if (ticks.length !== values.length) {
|
|
1732
|
+
throw new Error(`fbx: animation curve ${JSON.stringify(curve.name || curve.key)} has mismatched KeyTime and ${valueFeature} lengths`);
|
|
1733
|
+
}
|
|
1734
|
+
const pairs = ticks.map((tick, index) => ({
|
|
1735
|
+
tick: finiteNumber(tick, "KeyTime"),
|
|
1736
|
+
value: finiteNumber(values[index], valueFeature) * scale
|
|
1737
|
+
})).sort((a, b) => a.tick - b.tick);
|
|
1738
|
+
return {
|
|
1739
|
+
ticks: pairs.map(pair => pair.tick),
|
|
1740
|
+
values: pairs.map(pair => pair.value)
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
function readBoneAnimationRecord(curveNode, objectIndex, connections, boneTargets, sceneTransform) {
|
|
1744
|
+
const targetConnection = connections.list.find(connection => connection.relation === "OP" && connection.childKey === curveNode.key && isBoneAnimationProperty(connection.property) && boneTargets.has(connection.parentKey) && isBoneModel(objectIndex.byId[connection.parentKey]));
|
|
1745
|
+
if (!targetConnection) {
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
const bone = objectIndex.byId[targetConnection.parentKey],
|
|
1749
|
+
target = boneTargets.get(bone.key),
|
|
1750
|
+
properties = readProperties70(bone.node).properties,
|
|
1751
|
+
property = normalizeBoneAnimationProperty(targetConnection.property);
|
|
1752
|
+
if (!property) {
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1755
|
+
const vectorCurve = readVectorAnimationCurve(curveNode, objectIndex, connections, animationCurveNodeFallback(curveNode, boneAnimationFallback(properties, property)));
|
|
1756
|
+
if (!vectorCurve) {
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
if (property === "position") {
|
|
1760
|
+
return {
|
|
1761
|
+
target: target.name,
|
|
1762
|
+
targetType: "BonePosition",
|
|
1763
|
+
valueDimension: 3,
|
|
1764
|
+
curve: {
|
|
1765
|
+
ticks: vectorCurve.ticks,
|
|
1766
|
+
values: packBonePositionValues(vectorCurve.values, sceneTransform, target.isRoot)
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
if (property === "rotation") {
|
|
1771
|
+
return {
|
|
1772
|
+
target: target.name,
|
|
1773
|
+
targetType: "BoneRotation",
|
|
1774
|
+
valueDimension: 4,
|
|
1775
|
+
curve: {
|
|
1776
|
+
ticks: vectorCurve.ticks,
|
|
1777
|
+
values: packBoneRotationValues(bone, vectorCurve.values, sceneTransform, target.isRoot)
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
return {
|
|
1782
|
+
target: target.name,
|
|
1783
|
+
targetType: "BoneScale",
|
|
1784
|
+
valueDimension: 3,
|
|
1785
|
+
curve: {
|
|
1786
|
+
ticks: vectorCurve.ticks,
|
|
1787
|
+
values: vectorCurve.values.map(cleanFloat)
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
function isBoneAnimationProperty(property) {
|
|
1792
|
+
return !!normalizeBoneAnimationProperty(property);
|
|
1793
|
+
}
|
|
1794
|
+
function normalizeBoneAnimationProperty(property) {
|
|
1795
|
+
const text = String(property || "").replace(/\s+/gu, "").toLowerCase();
|
|
1796
|
+
if (text.includes("translation") || text === "t") {
|
|
1797
|
+
return "position";
|
|
1798
|
+
}
|
|
1799
|
+
if (text.includes("rotation") || text === "r") {
|
|
1800
|
+
return "rotation";
|
|
1801
|
+
}
|
|
1802
|
+
if (text.includes("scaling") || text.includes("scale") || text === "s") {
|
|
1803
|
+
return "scale";
|
|
1804
|
+
}
|
|
1805
|
+
return "";
|
|
1806
|
+
}
|
|
1807
|
+
function boneAnimationFallback(properties, property) {
|
|
1808
|
+
if (property === "position") {
|
|
1809
|
+
return propertyVector3(properties, "Lcl Translation", [0, 0, 0]);
|
|
1810
|
+
}
|
|
1811
|
+
if (property === "rotation") {
|
|
1812
|
+
return propertyVector3(properties, "Lcl Rotation", [0, 0, 0]);
|
|
1813
|
+
}
|
|
1814
|
+
return propertyVector3(properties, "Lcl Scaling", [1, 1, 1]);
|
|
1815
|
+
}
|
|
1816
|
+
function animationCurveNodeFallback(curveNode, fallback) {
|
|
1817
|
+
const properties = readProperties70(curveNode.node).properties,
|
|
1818
|
+
axes = ["d|X", "d|Y", "d|Z"];
|
|
1819
|
+
return axes.map((axis, index) => propertyNumber(properties, axis, fallback[index]));
|
|
1820
|
+
}
|
|
1821
|
+
function readVectorAnimationCurve(curveNode, objectIndex, connections, fallback) {
|
|
1822
|
+
const components = [null, null, null];
|
|
1823
|
+
for (const connection of connections.list.filter(item => item.parentKey === curveNode.key)) {
|
|
1824
|
+
const curve = objectIndex.byId[connection.childKey],
|
|
1825
|
+
axis = animationCurveAxis(connection.property) ?? animationCurveAxis(curve?.name);
|
|
1826
|
+
if (curve?.nodeName !== "AnimationCurve" || axis === null) {
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
components[axis] = readScalarAnimationCurve(curve, 1);
|
|
1830
|
+
}
|
|
1831
|
+
if (components.every(component => !component)) {
|
|
1832
|
+
return null;
|
|
1833
|
+
}
|
|
1834
|
+
const ticks = uniqueSortedTicks(components);
|
|
1835
|
+
const values = [];
|
|
1836
|
+
for (const tick of ticks) {
|
|
1837
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
1838
|
+
values.push(sampleScalarAnimationCurve(components[axis], tick, fallback[axis]));
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
return {
|
|
1842
|
+
ticks,
|
|
1843
|
+
values
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
function animationCurveAxis(property) {
|
|
1847
|
+
const text = String(property || "").trim().toLowerCase();
|
|
1848
|
+
if (/(^|[|._])x$/u.test(text)) {
|
|
1849
|
+
return 0;
|
|
1850
|
+
}
|
|
1851
|
+
if (/(^|[|._])y$/u.test(text)) {
|
|
1852
|
+
return 1;
|
|
1853
|
+
}
|
|
1854
|
+
if (/(^|[|._])z$/u.test(text)) {
|
|
1855
|
+
return 2;
|
|
1856
|
+
}
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
function uniqueSortedTicks(components) {
|
|
1860
|
+
const ticks = new Set();
|
|
1861
|
+
for (const component of components) {
|
|
1862
|
+
for (const tick of component?.ticks ?? []) {
|
|
1863
|
+
ticks.add(tick);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return Array.from(ticks).sort((a, b) => a - b);
|
|
1867
|
+
}
|
|
1868
|
+
function sampleScalarAnimationCurve(curve, tick, fallback) {
|
|
1869
|
+
if (!curve) {
|
|
1870
|
+
return fallback;
|
|
1871
|
+
}
|
|
1872
|
+
const {
|
|
1873
|
+
ticks,
|
|
1874
|
+
values
|
|
1875
|
+
} = curve;
|
|
1876
|
+
if (tick <= ticks[0]) {
|
|
1877
|
+
return values[0];
|
|
1878
|
+
}
|
|
1879
|
+
if (tick >= ticks[ticks.length - 1]) {
|
|
1880
|
+
return values[values.length - 1];
|
|
1881
|
+
}
|
|
1882
|
+
for (let i = 1; i < ticks.length; i++) {
|
|
1883
|
+
if (tick > ticks[i]) {
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
if (tick === ticks[i]) {
|
|
1887
|
+
return values[i];
|
|
1888
|
+
}
|
|
1889
|
+
const amount = (tick - ticks[i - 1]) / (ticks[i] - ticks[i - 1]);
|
|
1890
|
+
return values[i - 1] + (values[i] - values[i - 1]) * amount;
|
|
1891
|
+
}
|
|
1892
|
+
return fallback;
|
|
1893
|
+
}
|
|
1894
|
+
function packBonePositionValues(values, sceneTransform, isRoot) {
|
|
1895
|
+
const output = [];
|
|
1896
|
+
for (let i = 0; i < values.length; i += 3) {
|
|
1897
|
+
const position = isRoot ? transformScenePoint([values[i], values[i + 1], values[i + 2]], sceneTransform) : [values[i] * sceneTransform.scale, values[i + 1] * sceneTransform.scale, values[i + 2] * sceneTransform.scale];
|
|
1898
|
+
output.push(...cleanVector3(position));
|
|
1899
|
+
}
|
|
1900
|
+
return output;
|
|
1901
|
+
}
|
|
1902
|
+
function packBoneRotationValues(bone, values, sceneTransform, isRoot) {
|
|
1903
|
+
const properties = readProperties70(bone.node).properties,
|
|
1904
|
+
output = [];
|
|
1905
|
+
for (let i = 0; i < values.length; i += 3) {
|
|
1906
|
+
const matrix = composeModelTransform(properties, {
|
|
1907
|
+
translation: [0, 0, 0],
|
|
1908
|
+
rotation: [values[i], values[i + 1], values[i + 2]],
|
|
1909
|
+
scale: [1, 1, 1]
|
|
1910
|
+
}),
|
|
1911
|
+
transformed = isRoot ? transformMatrixScene(matrix, sceneTransform) : matrix,
|
|
1912
|
+
rotation = decomposeMatrix4(transformed).rotation;
|
|
1913
|
+
output.push(...rotation);
|
|
1914
|
+
}
|
|
1915
|
+
return output;
|
|
1916
|
+
}
|
|
1917
|
+
function isBlendShapeChannel(entry) {
|
|
1918
|
+
return !!entry && entry.nodeName === "Deformer" && (entry.className === "BlendShapeChannel" || entry.fullName.startsWith("SubDeformer::") || entry.name.includes("BlendShapeChannel"));
|
|
1919
|
+
}
|
|
1920
|
+
function isMorphAnimationProperty(property) {
|
|
1921
|
+
return String(property || "").toLowerCase().includes("deformpercent");
|
|
1922
|
+
}
|
|
1923
|
+
function isUserDefinedNumericProperty(entry, property) {
|
|
1924
|
+
if (!entry || !property) {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
const definition = readProperties70(entry.node).properties[property];
|
|
1928
|
+
if (!definition || !String(definition.flags || "").includes("U")) {
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
return typeof definition.value === "number";
|
|
1932
|
+
}
|
|
1933
|
+
function uniqueEntries(entries) {
|
|
1934
|
+
const seen = new Set(),
|
|
1935
|
+
unique = [];
|
|
1936
|
+
for (const entry of entries) {
|
|
1937
|
+
if (!entry || seen.has(entry.key)) {
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
seen.add(entry.key);
|
|
1941
|
+
unique.push(entry);
|
|
1942
|
+
}
|
|
1943
|
+
return unique;
|
|
1944
|
+
}
|
|
1945
|
+
function packFloat32Values(values) {
|
|
1946
|
+
const bytes = new Uint8Array(values.length * 4),
|
|
1947
|
+
view = new DataView(bytes.buffer);
|
|
1948
|
+
values.forEach((value, index) => view.setFloat32(index * 4, value, true));
|
|
1949
|
+
return Array.from(bytes);
|
|
1950
|
+
}
|
|
1951
|
+
function buildSkinClusterRecords(clusters, objectIndex, connections) {
|
|
1952
|
+
const records = [];
|
|
1953
|
+
for (const cluster of clusters) {
|
|
1954
|
+
const bone = clusterBone(cluster, objectIndex, connections),
|
|
1955
|
+
indexes = readChildArray(cluster.node, "Indexes") || [],
|
|
1956
|
+
weights = readChildArray(cluster.node, "Weights") || [];
|
|
1957
|
+
if (!bone || !indexes.length || !hasPositiveClusterWeight(indexes, weights)) {
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
records.push({
|
|
1961
|
+
cluster,
|
|
1962
|
+
bone,
|
|
1963
|
+
boneKey: bone.key,
|
|
1964
|
+
boneName: bone.name || cluster.name || `bone_${records.length}`,
|
|
1965
|
+
indexes,
|
|
1966
|
+
weights
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
return records;
|
|
1970
|
+
}
|
|
1971
|
+
function hasPositiveClusterWeight(indexes, weights) {
|
|
1972
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
1973
|
+
if (finiteNumber(weights[i] ?? 0, "Weights") > 0) {
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return false;
|
|
1978
|
+
}
|
|
1979
|
+
function buildControlPointInfluences(records) {
|
|
1980
|
+
const controlPointInfluences = new Map();
|
|
1981
|
+
records.forEach((record, boneIndex) => {
|
|
1982
|
+
const indexes = record.indexes,
|
|
1983
|
+
weights = record.weights;
|
|
1984
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
1985
|
+
const controlPointIndex = integerNumber(indexes[i], "Indexes"),
|
|
1986
|
+
weight = Math.max(0, finiteNumber(weights[i] ?? 0, "Weights")),
|
|
1987
|
+
list = controlPointInfluences.get(controlPointIndex) || [];
|
|
1988
|
+
if (weight <= 0) {
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
list.push({
|
|
1992
|
+
boneIndex,
|
|
1993
|
+
boneKey: record.boneKey,
|
|
1994
|
+
boneName: record.boneName,
|
|
1995
|
+
weight
|
|
1996
|
+
});
|
|
1997
|
+
controlPointInfluences.set(controlPointIndex, list);
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
return controlPointInfluences;
|
|
2001
|
+
}
|
|
2002
|
+
function normalizeInfluences(influences) {
|
|
2003
|
+
const best = influences.filter(influence => influence.weight > 0).sort((a, b) => b.weight - a.weight).slice(0, 4);
|
|
2004
|
+
const total = best.reduce((sum, influence) => sum + influence.weight, 0);
|
|
2005
|
+
if (total <= 0) {
|
|
2006
|
+
return [];
|
|
2007
|
+
}
|
|
2008
|
+
return best.map(influence => ({
|
|
2009
|
+
...influence,
|
|
2010
|
+
weight: cleanFloat(influence.weight / total)
|
|
2011
|
+
}));
|
|
2012
|
+
}
|
|
2013
|
+
function buildBoneBindingsFromInfluences(records, outputInfluences, positions, objectIndex, connections, sceneTransform, modelWorldCache) {
|
|
2014
|
+
return records.map((record, boneIndex) => {
|
|
2015
|
+
const name = record.boneName || `bone_${boneIndex}`,
|
|
2016
|
+
bindingMatrix = buildBoneBindingMatrix(record, objectIndex, connections, sceneTransform, modelWorldCache),
|
|
2017
|
+
bindingPositions = [];
|
|
2018
|
+
for (let vertexIndex = 0; vertexIndex < outputInfluences.length; vertexIndex++) {
|
|
2019
|
+
if (!outputInfluences[vertexIndex].some(influence => influence.boneIndex === boneIndex && influence.weight > 0)) {
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
const offset = vertexIndex * 3;
|
|
2023
|
+
bindingPositions.push(...transformPointMatrix4([positions[offset], positions[offset + 1], positions[offset + 2]], bindingMatrix));
|
|
2024
|
+
}
|
|
2025
|
+
const bounds = computeBounds(bindingPositions);
|
|
2026
|
+
return {
|
|
2027
|
+
name,
|
|
2028
|
+
minBounds: bounds.minBounds,
|
|
2029
|
+
maxBounds: bounds.maxBounds
|
|
2030
|
+
};
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
function buildBoneBindingMatrix(record, objectIndex, connections, sceneTransform, modelWorldCache) {
|
|
2034
|
+
return invertMatrix4(transformMatrixScene(buildMeshGeometryToWorld(record.bone, objectIndex, connections, modelWorldCache), sceneTransform));
|
|
2035
|
+
}
|
|
2036
|
+
function clusterBone(cluster, objectIndex, connections) {
|
|
2037
|
+
return connectedChildObjects(cluster, objectIndex, connections, "Model")[0] || null;
|
|
2038
|
+
}
|
|
2039
|
+
function findSkinSkeletonIndex(boneKeys, skeletonContext) {
|
|
2040
|
+
if (!skeletonContext) {
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
let skeletonIndex = null;
|
|
2044
|
+
for (const key of boneKeys) {
|
|
2045
|
+
if (!key) {
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
const index = skeletonContext.boneToSkeletonIndex.get(key);
|
|
2049
|
+
if (index === undefined) {
|
|
2050
|
+
return null;
|
|
2051
|
+
}
|
|
2052
|
+
if (skeletonIndex === null) {
|
|
2053
|
+
skeletonIndex = index;
|
|
2054
|
+
} else if (skeletonIndex !== index) {
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
return skeletonIndex;
|
|
2059
|
+
}
|
|
2060
|
+
function buildSkeletonContext(objectIndex, connections, sceneTransform, modelWorldCache) {
|
|
2061
|
+
const roots = objectIndex.list.filter(isBoneModel).filter(bone => !connectedParentObjects(bone, objectIndex, connections, "Model").some(isBoneModel)),
|
|
2062
|
+
bindPoseMatrices = readBindPoseMatrices(objectIndex, sceneTransform),
|
|
2063
|
+
skeletons = [],
|
|
2064
|
+
boneToSkeletonIndex = new Map(),
|
|
2065
|
+
visited = new Set();
|
|
2066
|
+
for (const root of roots) {
|
|
2067
|
+
if (visited.has(root.key)) {
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
const skeleton = buildSkeleton(root, objectIndex, connections, sceneTransform, modelWorldCache, bindPoseMatrices, visited);
|
|
2071
|
+
if (!skeleton.cmfSkeleton.bones.length) {
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
const skeletonIndex = skeletons.length;
|
|
2075
|
+
skeletons.push(skeleton);
|
|
2076
|
+
for (const key of skeleton.boneKeys) {
|
|
2077
|
+
boneToSkeletonIndex.set(key, skeletonIndex);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return {
|
|
2081
|
+
skeletons,
|
|
2082
|
+
boneToSkeletonIndex
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
function buildSkeleton(root, objectIndex, connections, sceneTransform, modelWorldCache, bindPoseMatrices, visited) {
|
|
2086
|
+
const records = [];
|
|
2087
|
+
const importBone = (bone, parentIndex) => {
|
|
2088
|
+
if (!bone || visited.has(bone.key)) {
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
visited.add(bone.key);
|
|
2092
|
+
const localMatrix = buildBoneLocalMatrix(bone, parentIndex, objectIndex, connections, sceneTransform, modelWorldCache),
|
|
2093
|
+
transform = decomposeMatrix4(localMatrix),
|
|
2094
|
+
recordIndex = records.length;
|
|
2095
|
+
records.push({
|
|
2096
|
+
bone,
|
|
2097
|
+
parentIndex,
|
|
2098
|
+
localMatrix,
|
|
2099
|
+
transform
|
|
2100
|
+
});
|
|
2101
|
+
for (const child of connectedChildObjects(bone, objectIndex, connections, "Model").filter(isBoneModel)) {
|
|
2102
|
+
importBone(child, recordIndex);
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
importBone(root, -1);
|
|
2106
|
+
const name = root.name || "fbx_skeleton",
|
|
2107
|
+
invBindTransforms = buildInvBindTransforms(records, bindPoseMatrices);
|
|
2108
|
+
return {
|
|
2109
|
+
name,
|
|
2110
|
+
boneKeys: records.map(record => record.bone.key),
|
|
2111
|
+
gr2Skeleton: {
|
|
2112
|
+
name,
|
|
2113
|
+
bones: records.map(record => buildGr2Bone(record))
|
|
2114
|
+
},
|
|
2115
|
+
cmfSkeleton: {
|
|
2116
|
+
name,
|
|
2117
|
+
bones: records.map(record => record.bone.name || "bone"),
|
|
2118
|
+
parents: records.map(record => record.parentIndex === -1 ? 0xffffffff : record.parentIndex),
|
|
2119
|
+
restTransforms: records.map(record => ({
|
|
2120
|
+
position: record.transform.position,
|
|
2121
|
+
rotation: record.transform.rotation,
|
|
2122
|
+
scale: record.transform.scale
|
|
2123
|
+
})),
|
|
2124
|
+
invBindTransforms,
|
|
2125
|
+
boneMasks: buildCmfBoneMasks(records)
|
|
2126
|
+
}
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
/**
|
|
2131
|
+
* Build CMF bone masks from user-defined numeric bone properties.
|
|
2132
|
+
*
|
|
2133
|
+
* @param {object[]} records Imported skeleton bone records.
|
|
2134
|
+
* @returns {object[]} CMF bone masks.
|
|
2135
|
+
*/
|
|
2136
|
+
function buildCmfBoneMasks(records) {
|
|
2137
|
+
const masks = [],
|
|
2138
|
+
byName = new Map();
|
|
2139
|
+
records.forEach((record, index) => {
|
|
2140
|
+
const properties = readProperties70(record.bone.node).properties;
|
|
2141
|
+
for (const [name, definition] of Object.entries(properties)) {
|
|
2142
|
+
if (!isUserDefinedNumericPropertyDefinition(definition)) {
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
let mask = byName.get(name);
|
|
2146
|
+
if (!mask) {
|
|
2147
|
+
mask = {
|
|
2148
|
+
name,
|
|
2149
|
+
weights: []
|
|
2150
|
+
};
|
|
2151
|
+
byName.set(name, mask);
|
|
2152
|
+
masks.push(mask);
|
|
2153
|
+
}
|
|
2154
|
+
mask.weights.push({
|
|
2155
|
+
index,
|
|
2156
|
+
weight: cleanFloat(clamp01(definition.value))
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
return masks;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Check whether a Properties70 definition should become a CMF bone-mask weight.
|
|
2165
|
+
*
|
|
2166
|
+
* @param {object} definition Parsed Properties70 definition.
|
|
2167
|
+
* @returns {boolean} True when the property is user-defined and numeric.
|
|
2168
|
+
*/
|
|
2169
|
+
function isUserDefinedNumericPropertyDefinition(definition) {
|
|
2170
|
+
return !!definition && String(definition.flags || "").includes("U") && typeof definition.value === "number";
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/**
|
|
2174
|
+
* Clamp a numeric value to the unit interval.
|
|
2175
|
+
*
|
|
2176
|
+
* @param {number} value Input value.
|
|
2177
|
+
* @returns {number} Clamped value.
|
|
2178
|
+
*/
|
|
2179
|
+
function clamp01(value) {
|
|
2180
|
+
return Math.min(1, Math.max(0, finiteNumber(value, "bone mask weight")));
|
|
2181
|
+
}
|
|
2182
|
+
function buildGr2Models(skeletons, meshes) {
|
|
2183
|
+
return skeletons.map((skeleton, skeletonIndex) => ({
|
|
2184
|
+
name: skeleton.name,
|
|
2185
|
+
skeleton: skeleton.gr2Skeleton,
|
|
2186
|
+
meshBindings: meshes.map((mesh, meshIndex) => mesh.skeleton === skeletonIndex ? meshIndex : -1).filter(meshIndex => meshIndex !== -1)
|
|
2187
|
+
}));
|
|
2188
|
+
}
|
|
2189
|
+
function buildGr2Bone(record) {
|
|
2190
|
+
const transform = record.transform;
|
|
2191
|
+
return {
|
|
2192
|
+
name: record.bone.name || "bone",
|
|
2193
|
+
parentIndex: record.parentIndex,
|
|
2194
|
+
flag: 7,
|
|
2195
|
+
position: transform.position,
|
|
2196
|
+
orientation: transform.rotation,
|
|
2197
|
+
scaleShear: [transform.scale[0], 0, 0, 0, transform.scale[1], 0, 0, 0, transform.scale[2]]
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
function buildBoneLocalMatrix(bone, parentIndex, objectIndex, connections, sceneTransform, modelWorldCache) {
|
|
2201
|
+
if (parentIndex === -1) {
|
|
2202
|
+
return transformMatrixScene(buildModelTransformState(bone, objectIndex, connections, modelWorldCache).nodeToWorld, sceneTransform);
|
|
2203
|
+
}
|
|
2204
|
+
return scaleMatrixTranslation(buildModelLocalTransform(bone), sceneTransform.scale);
|
|
2205
|
+
}
|
|
2206
|
+
function buildInvBindTransforms(records, bindPoseMatrices) {
|
|
2207
|
+
const poseMatrices = records.map(record => bindPoseMatrices.get(record.bone.key));
|
|
2208
|
+
if (poseMatrices.every(Boolean)) {
|
|
2209
|
+
return poseMatrices.map(matrix => cleanMatrix4(invertMatrix4(matrix)));
|
|
2210
|
+
}
|
|
2211
|
+
const invBindTransforms = [];
|
|
2212
|
+
for (const record of records) {
|
|
2213
|
+
const parentInvTransform = record.parentIndex === -1 ? IDENTITY_MATRIX4 : invBindTransforms[record.parentIndex],
|
|
2214
|
+
invLocalTransform = invertMatrix4(record.localMatrix);
|
|
2215
|
+
invBindTransforms.push(cleanMatrix4(multiplyMatrix4(parentInvTransform, invLocalTransform)));
|
|
2216
|
+
}
|
|
2217
|
+
return invBindTransforms;
|
|
2218
|
+
}
|
|
2219
|
+
function readBindPoseMatrices(objectIndex, sceneTransform) {
|
|
2220
|
+
const matrices = new Map();
|
|
2221
|
+
for (const pose of objectIndex.list.filter(entry => entry.nodeName === "Pose")) {
|
|
2222
|
+
const type = String(readFirstChildProperty(pose.node, "Type") || pose.className || pose.name || "");
|
|
2223
|
+
if (!/bindpose/iu.test(type)) {
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
for (const poseNode of pose.node.children.filter(child => child.name === "PoseNode")) {
|
|
2227
|
+
const nodeId = readFirstChildProperty(poseNode, "Node"),
|
|
2228
|
+
matrix = readChildArray(poseNode, "Matrix");
|
|
2229
|
+
if (nodeId === undefined || !Array.isArray(matrix) || matrix.length !== 16) {
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
matrices.set(toObjectKey(nodeId), transformMatrixScene(matrix, sceneTransform));
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return matrices;
|
|
2236
|
+
}
|
|
2237
|
+
function isBoneModel(entry) {
|
|
2238
|
+
return entry?.nodeName === "Model" && (entry.className === "LimbNode" || /LimbNode/iu.test(entry.fullName));
|
|
2239
|
+
}
|
|
2240
|
+
function buildSceneTransform(globalSettingsNode) {
|
|
2241
|
+
const properties = readProperties70(globalSettingsNode).properties;
|
|
2242
|
+
const unitScaleFactor = propertyNumber(properties, "UnitScaleFactor", null);
|
|
2243
|
+
return {
|
|
2244
|
+
scale: unitScaleFactor === null ? 1 : unitScaleFactor * 0.01,
|
|
2245
|
+
right: axisVector(propertyNumber(properties, "CoordAxis", 0), propertyNumber(properties, "CoordAxisSign", 1)),
|
|
2246
|
+
up: axisVector(propertyNumber(properties, "UpAxis", 1), propertyNumber(properties, "UpAxisSign", 1)),
|
|
2247
|
+
forward: axisVector(propertyNumber(properties, "FrontAxis", 2), propertyNumber(properties, "FrontAxisSign", 1))
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
function buildMeshGeometryToWorld(owner, objectIndex, connections, modelWorldCache) {
|
|
2251
|
+
if (!owner) {
|
|
2252
|
+
return IDENTITY_MATRIX4;
|
|
2253
|
+
}
|
|
2254
|
+
return multiplyMatrix4(buildModelTransformState(owner, objectIndex, connections, modelWorldCache).nodeToWorld, buildModelGeometricTransform(owner));
|
|
2255
|
+
}
|
|
2256
|
+
function buildModelTransformState(model, objectIndex, connections, cache) {
|
|
2257
|
+
if (cache.has(model.key)) {
|
|
2258
|
+
return cache.get(model.key);
|
|
2259
|
+
}
|
|
2260
|
+
const local = buildModelLocalState(model),
|
|
2261
|
+
parents = connectedParentObjects(model, objectIndex, connections, "Model"),
|
|
2262
|
+
parent = parents[0],
|
|
2263
|
+
parentState = parent ? buildModelTransformState(parent, objectIndex, connections, cache) : null,
|
|
2264
|
+
state = parentState ? composeInheritedModelState(local, parentState) : {
|
|
2265
|
+
nodeToWorld: local.matrix,
|
|
2266
|
+
unscaledNodeToWorld: local.unscaledMatrix,
|
|
2267
|
+
inheritScale: local.scale
|
|
2268
|
+
};
|
|
2269
|
+
cache.set(model.key, state);
|
|
2270
|
+
return state;
|
|
2271
|
+
}
|
|
2272
|
+
function buildModelLocalTransform(model) {
|
|
2273
|
+
const properties = readProperties70(model.node).properties;
|
|
2274
|
+
return composeModelTransform(properties);
|
|
2275
|
+
}
|
|
2276
|
+
function buildModelLocalState(model) {
|
|
2277
|
+
const properties = readProperties70(model.node).properties,
|
|
2278
|
+
translation = propertyVector3(properties, "Lcl Translation", [0, 0, 0]),
|
|
2279
|
+
scale = propertyVector3(properties, "Lcl Scaling", [1, 1, 1]);
|
|
2280
|
+
return {
|
|
2281
|
+
properties,
|
|
2282
|
+
translation,
|
|
2283
|
+
scale,
|
|
2284
|
+
inheritType: propertyNumber(properties, "InheritType", 1),
|
|
2285
|
+
matrix: composeModelTransform(properties, {
|
|
2286
|
+
translation,
|
|
2287
|
+
scale
|
|
2288
|
+
}),
|
|
2289
|
+
unscaledMatrix: composeModelTransform(properties, {
|
|
2290
|
+
translation,
|
|
2291
|
+
scale: [1, 1, 1]
|
|
2292
|
+
})
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
function composeInheritedModelState(local, parentState) {
|
|
2296
|
+
const inheritType = integerNumber(local.inheritType, "InheritType");
|
|
2297
|
+
if (inheritType === 0 || inheritType === 2) {
|
|
2298
|
+
const translation = multiplyVector3(local.translation, parentState.inheritScale),
|
|
2299
|
+
scale = inheritType === 0 ? multiplyVector3(local.scale, parentState.inheritScale) : local.scale,
|
|
2300
|
+
matrix = composeModelTransform(local.properties, {
|
|
2301
|
+
translation,
|
|
2302
|
+
scale
|
|
2303
|
+
}),
|
|
2304
|
+
unscaledMatrix = composeModelTransform(local.properties, {
|
|
2305
|
+
translation,
|
|
2306
|
+
scale: [1, 1, 1]
|
|
2307
|
+
});
|
|
2308
|
+
return {
|
|
2309
|
+
nodeToWorld: multiplyMatrix4(parentState.unscaledNodeToWorld, matrix),
|
|
2310
|
+
unscaledNodeToWorld: multiplyMatrix4(parentState.unscaledNodeToWorld, unscaledMatrix),
|
|
2311
|
+
inheritScale: scale
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
return {
|
|
2315
|
+
nodeToWorld: multiplyMatrix4(parentState.nodeToWorld, local.matrix),
|
|
2316
|
+
unscaledNodeToWorld: multiplyMatrix4(parentState.unscaledNodeToWorld, local.unscaledMatrix),
|
|
2317
|
+
inheritScale: multiplyVector3(parentState.inheritScale, local.scale)
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
function buildModelGeometricTransform(model) {
|
|
2321
|
+
const properties = readProperties70(model.node).properties;
|
|
2322
|
+
return composeSimpleTransform(propertyVector3(properties, "GeometricTranslation", [0, 0, 0]), propertyVector3(properties, "GeometricRotation", [0, 0, 0]), propertyVector3(properties, "GeometricScaling", [1, 1, 1]), DEFAULT_ROTATION_ORDER);
|
|
2323
|
+
}
|
|
2324
|
+
function composeModelTransform(properties, overrides = {}) {
|
|
2325
|
+
const rotationOrder = propertyRotationOrder(properties),
|
|
2326
|
+
rotationPivot = propertyVector3(properties, "RotationPivot", [0, 0, 0]),
|
|
2327
|
+
scalingPivot = propertyVector3(properties, "ScalingPivot", [0, 0, 0]);
|
|
2328
|
+
return multiplyMatrix4List([translationMatrix4(overrides.translation ?? propertyVector3(properties, "Lcl Translation", [0, 0, 0])), translationMatrix4(propertyVector3(properties, "RotationOffset", [0, 0, 0])), translationMatrix4(rotationPivot), rotationMatrix4(propertyVector3(properties, "PreRotation", [0, 0, 0]), rotationOrder), rotationMatrix4(overrides.rotation ?? propertyVector3(properties, "Lcl Rotation", [0, 0, 0]), rotationOrder), inverseRotationMatrix4(propertyVector3(properties, "PostRotation", [0, 0, 0]), rotationOrder), translationMatrix4(negateVector3(rotationPivot)), translationMatrix4(propertyVector3(properties, "ScalingOffset", [0, 0, 0])), translationMatrix4(scalingPivot), scaleMatrix4(overrides.scale ?? propertyVector3(properties, "Lcl Scaling", [1, 1, 1])), translationMatrix4(negateVector3(scalingPivot))]);
|
|
2329
|
+
}
|
|
2330
|
+
function composeSimpleTransform(translation, rotationDegrees, scale, rotationOrder) {
|
|
2331
|
+
return multiplyMatrix4(translationMatrix4(translation), multiplyMatrix4(rotationMatrix4(rotationDegrees, rotationOrder), scaleMatrix4(scale)));
|
|
2332
|
+
}
|
|
2333
|
+
function transformPoints(values, matrix, sceneTransform) {
|
|
2334
|
+
const output = [];
|
|
2335
|
+
for (let i = 0; i < values.length; i += 3) {
|
|
2336
|
+
const point = transformPointMatrix4([values[i], values[i + 1], values[i + 2]], matrix);
|
|
2337
|
+
output.push(...transformScenePoint(point, sceneTransform));
|
|
2338
|
+
}
|
|
2339
|
+
return output;
|
|
2340
|
+
}
|
|
2341
|
+
function transformDirections(values, matrix, sceneTransform, useNormalMatrix) {
|
|
2342
|
+
if (!values.length) {
|
|
2343
|
+
return values;
|
|
2344
|
+
}
|
|
2345
|
+
const normalMatrix = normalMatrix3(matrix) ,
|
|
2346
|
+
output = [];
|
|
2347
|
+
for (let i = 0; i < values.length; i += 3) {
|
|
2348
|
+
const vector = transformVectorMatrix3([values[i], values[i + 1], values[i + 2]], normalMatrix) ;
|
|
2349
|
+
output.push(...normalizeVector3(transformSceneVector(vector, sceneTransform)));
|
|
2350
|
+
}
|
|
2351
|
+
return output;
|
|
2352
|
+
}
|
|
2353
|
+
function transformScenePoint(point, sceneTransform) {
|
|
2354
|
+
const vector = transformSceneVector(point, sceneTransform);
|
|
2355
|
+
return vector.map(value => value * sceneTransform.scale);
|
|
2356
|
+
}
|
|
2357
|
+
function transformSceneVector(vector, sceneTransform) {
|
|
2358
|
+
return [dotVector3(vector, sceneTransform.right), dotVector3(vector, sceneTransform.up), dotVector3(vector, sceneTransform.forward)];
|
|
2359
|
+
}
|
|
2360
|
+
function transformMatrixScene(matrix, sceneTransform) {
|
|
2361
|
+
const column0 = transformSceneVector([matrix[0], matrix[4], matrix[8]], sceneTransform),
|
|
2362
|
+
column1 = transformSceneVector([matrix[1], matrix[5], matrix[9]], sceneTransform),
|
|
2363
|
+
column2 = transformSceneVector([matrix[2], matrix[6], matrix[10]], sceneTransform),
|
|
2364
|
+
translation = transformScenePoint([matrix[3], matrix[7], matrix[11]], sceneTransform);
|
|
2365
|
+
return cleanMatrix4([column0[0], column1[0], column2[0], translation[0], column0[1], column1[1], column2[1], translation[1], column0[2], column1[2], column2[2], translation[2], 0, 0, 0, 1]);
|
|
2366
|
+
}
|
|
2367
|
+
function scaleMatrixTranslation(matrix, scale) {
|
|
2368
|
+
const output = matrix.slice();
|
|
2369
|
+
output[3] *= scale;
|
|
2370
|
+
output[7] *= scale;
|
|
2371
|
+
output[11] *= scale;
|
|
2372
|
+
return cleanMatrix4(output);
|
|
2373
|
+
}
|
|
2374
|
+
function decomposeMatrix4(matrix) {
|
|
2375
|
+
const scale = [Math.hypot(matrix[0], matrix[4], matrix[8]) || 1, Math.hypot(matrix[1], matrix[5], matrix[9]) || 1, Math.hypot(matrix[2], matrix[6], matrix[10]) || 1],
|
|
2376
|
+
rotationMatrix = [matrix[0] / scale[0], matrix[1] / scale[1], matrix[2] / scale[2], matrix[4] / scale[0], matrix[5] / scale[1], matrix[6] / scale[2], matrix[8] / scale[0], matrix[9] / scale[1], matrix[10] / scale[2]];
|
|
2377
|
+
return {
|
|
2378
|
+
position: cleanVector3([matrix[3], matrix[7], matrix[11]]),
|
|
2379
|
+
rotation: matrixToQuaternion(rotationMatrix),
|
|
2380
|
+
scale: cleanVector3(scale)
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
function matrixToQuaternion(matrix) {
|
|
2384
|
+
const m00 = matrix[0],
|
|
2385
|
+
m01 = matrix[1],
|
|
2386
|
+
m02 = matrix[2],
|
|
2387
|
+
m10 = matrix[3],
|
|
2388
|
+
m11 = matrix[4],
|
|
2389
|
+
m12 = matrix[5],
|
|
2390
|
+
m20 = matrix[6],
|
|
2391
|
+
m21 = matrix[7],
|
|
2392
|
+
m22 = matrix[8],
|
|
2393
|
+
trace = m00 + m11 + m22;
|
|
2394
|
+
let x, y, z, w;
|
|
2395
|
+
if (trace > 0) {
|
|
2396
|
+
const s = Math.sqrt(trace + 1) * 2;
|
|
2397
|
+
w = 0.25 * s;
|
|
2398
|
+
x = (m21 - m12) / s;
|
|
2399
|
+
y = (m02 - m20) / s;
|
|
2400
|
+
z = (m10 - m01) / s;
|
|
2401
|
+
} else if (m00 > m11 && m00 > m22) {
|
|
2402
|
+
const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
|
|
2403
|
+
w = (m21 - m12) / s;
|
|
2404
|
+
x = 0.25 * s;
|
|
2405
|
+
y = (m01 + m10) / s;
|
|
2406
|
+
z = (m02 + m20) / s;
|
|
2407
|
+
} else if (m11 > m22) {
|
|
2408
|
+
const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
|
|
2409
|
+
w = (m02 - m20) / s;
|
|
2410
|
+
x = (m01 + m10) / s;
|
|
2411
|
+
y = 0.25 * s;
|
|
2412
|
+
z = (m12 + m21) / s;
|
|
2413
|
+
} else {
|
|
2414
|
+
const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
|
|
2415
|
+
w = (m10 - m01) / s;
|
|
2416
|
+
x = (m02 + m20) / s;
|
|
2417
|
+
y = (m12 + m21) / s;
|
|
2418
|
+
z = 0.25 * s;
|
|
2419
|
+
}
|
|
2420
|
+
return normalizeQuaternion([x, y, z, w]);
|
|
2421
|
+
}
|
|
2422
|
+
function propertyVector3(properties, name, fallback) {
|
|
2423
|
+
const value = properties[name]?.value;
|
|
2424
|
+
if (Array.isArray(value)) {
|
|
2425
|
+
return [finiteNumber(value[0] ?? fallback[0], name), finiteNumber(value[1] ?? fallback[1], name), finiteNumber(value[2] ?? fallback[2], name)];
|
|
2426
|
+
}
|
|
2427
|
+
return fallback;
|
|
2428
|
+
}
|
|
2429
|
+
function propertyNumber(properties, name, fallback) {
|
|
2430
|
+
const value = properties[name]?.value;
|
|
2431
|
+
if (value === undefined || value === null || Array.isArray(value)) {
|
|
2432
|
+
return fallback;
|
|
2433
|
+
}
|
|
2434
|
+
return finiteNumber(value, name);
|
|
2435
|
+
}
|
|
2436
|
+
function propertyRotationOrder(properties) {
|
|
2437
|
+
const value = propertyNumber(properties, "RotationOrder", null);
|
|
2438
|
+
if (value === null) {
|
|
2439
|
+
return DEFAULT_ROTATION_ORDER;
|
|
2440
|
+
}
|
|
2441
|
+
return FBX_ROTATION_ORDERS[value] || DEFAULT_ROTATION_ORDER;
|
|
2442
|
+
}
|
|
2443
|
+
function axisVector(axis, sign) {
|
|
2444
|
+
const vector = [0, 0, 0];
|
|
2445
|
+
const index = Math.max(0, Math.min(2, integerNumber(axis, "Axis")));
|
|
2446
|
+
vector[index] = sign < 0 ? -1 : 1;
|
|
2447
|
+
return vector;
|
|
2448
|
+
}
|
|
2449
|
+
function translationMatrix4(translation) {
|
|
2450
|
+
return [1, 0, 0, translation[0], 0, 1, 0, translation[1], 0, 0, 1, translation[2], 0, 0, 0, 1];
|
|
2451
|
+
}
|
|
2452
|
+
function scaleMatrix4(scale) {
|
|
2453
|
+
return [scale[0], 0, 0, 0, 0, scale[1], 0, 0, 0, 0, scale[2], 0, 0, 0, 0, 1];
|
|
2454
|
+
}
|
|
2455
|
+
function rotationMatrix4(rotationDegrees, order) {
|
|
2456
|
+
let matrix = IDENTITY_MATRIX4;
|
|
2457
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
2458
|
+
const axis = order[i];
|
|
2459
|
+
const radians = degreesToRadians(rotationDegrees[axisIndex(axis)]);
|
|
2460
|
+
matrix = multiplyMatrix4(matrix, axisRotationMatrix4(axis, radians));
|
|
2461
|
+
}
|
|
2462
|
+
return matrix;
|
|
2463
|
+
}
|
|
2464
|
+
function inverseRotationMatrix4(rotationDegrees, order) {
|
|
2465
|
+
let matrix = IDENTITY_MATRIX4;
|
|
2466
|
+
for (const axis of order) {
|
|
2467
|
+
const radians = degreesToRadians(-rotationDegrees[axisIndex(axis)]);
|
|
2468
|
+
matrix = multiplyMatrix4(matrix, axisRotationMatrix4(axis, radians));
|
|
2469
|
+
}
|
|
2470
|
+
return matrix;
|
|
2471
|
+
}
|
|
2472
|
+
function multiplyMatrix4List(matrices) {
|
|
2473
|
+
return matrices.reduce((matrix, next) => multiplyMatrix4(matrix, next), IDENTITY_MATRIX4);
|
|
2474
|
+
}
|
|
2475
|
+
function negateVector3(vector) {
|
|
2476
|
+
return [-vector[0], -vector[1], -vector[2]];
|
|
2477
|
+
}
|
|
2478
|
+
function multiplyVector3(a, b) {
|
|
2479
|
+
return [a[0] * b[0], a[1] * b[1], a[2] * b[2]];
|
|
2480
|
+
}
|
|
2481
|
+
function axisIndex(axis) {
|
|
2482
|
+
return axis === "X" ? 0 : axis === "Y" ? 1 : 2;
|
|
2483
|
+
}
|
|
2484
|
+
function axisRotationMatrix4(axis, radians) {
|
|
2485
|
+
const c = Math.cos(radians),
|
|
2486
|
+
s = Math.sin(radians);
|
|
2487
|
+
switch (axis) {
|
|
2488
|
+
case "X":
|
|
2489
|
+
return [1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1];
|
|
2490
|
+
case "Y":
|
|
2491
|
+
return [c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1];
|
|
2492
|
+
default:
|
|
2493
|
+
return [c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
function multiplyMatrix4(a, b) {
|
|
2497
|
+
const output = new Array(16).fill(0);
|
|
2498
|
+
for (let row = 0; row < 4; row++) {
|
|
2499
|
+
for (let column = 0; column < 4; column++) {
|
|
2500
|
+
for (let k = 0; k < 4; k++) {
|
|
2501
|
+
output[row * 4 + column] += a[row * 4 + k] * b[k * 4 + column];
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return output;
|
|
2506
|
+
}
|
|
2507
|
+
function invertMatrix4(matrix) {
|
|
2508
|
+
const a = [matrix.slice(0, 4), matrix.slice(4, 8), matrix.slice(8, 12), matrix.slice(12, 16)],
|
|
2509
|
+
inverse = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]];
|
|
2510
|
+
for (let column = 0; column < 4; column++) {
|
|
2511
|
+
let pivotRow = column;
|
|
2512
|
+
for (let row = column + 1; row < 4; row++) {
|
|
2513
|
+
if (Math.abs(a[row][column]) > Math.abs(a[pivotRow][column])) {
|
|
2514
|
+
pivotRow = row;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
const pivot = a[pivotRow][column];
|
|
2518
|
+
if (Math.abs(pivot) < Number.EPSILON) {
|
|
2519
|
+
throw new Error("fbx: transform matrix is not invertible");
|
|
2520
|
+
}
|
|
2521
|
+
if (pivotRow !== column) {
|
|
2522
|
+
[a[column], a[pivotRow]] = [a[pivotRow], a[column]];
|
|
2523
|
+
[inverse[column], inverse[pivotRow]] = [inverse[pivotRow], inverse[column]];
|
|
2524
|
+
}
|
|
2525
|
+
for (let i = 0; i < 4; i++) {
|
|
2526
|
+
a[column][i] /= pivot;
|
|
2527
|
+
inverse[column][i] /= pivot;
|
|
2528
|
+
}
|
|
2529
|
+
for (let row = 0; row < 4; row++) {
|
|
2530
|
+
if (row === column) {
|
|
2531
|
+
continue;
|
|
2532
|
+
}
|
|
2533
|
+
const factor = a[row][column];
|
|
2534
|
+
for (let i = 0; i < 4; i++) {
|
|
2535
|
+
a[row][i] -= factor * a[column][i];
|
|
2536
|
+
inverse[row][i] -= factor * inverse[column][i];
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
return cleanMatrix4(inverse.flat());
|
|
2541
|
+
}
|
|
2542
|
+
function transformPointMatrix4(point, matrix) {
|
|
2543
|
+
return [matrix[0] * point[0] + matrix[1] * point[1] + matrix[2] * point[2] + matrix[3], matrix[4] * point[0] + matrix[5] * point[1] + matrix[6] * point[2] + matrix[7], matrix[8] * point[0] + matrix[9] * point[1] + matrix[10] * point[2] + matrix[11]];
|
|
2544
|
+
}
|
|
2545
|
+
function normalMatrix3(matrix) {
|
|
2546
|
+
const a00 = matrix[0],
|
|
2547
|
+
a01 = matrix[1],
|
|
2548
|
+
a02 = matrix[2],
|
|
2549
|
+
a10 = matrix[4],
|
|
2550
|
+
a11 = matrix[5],
|
|
2551
|
+
a12 = matrix[6],
|
|
2552
|
+
a20 = matrix[8],
|
|
2553
|
+
a21 = matrix[9],
|
|
2554
|
+
a22 = matrix[10],
|
|
2555
|
+
b01 = a22 * a11 - a12 * a21,
|
|
2556
|
+
b11 = -a22 * a10 + a12 * a20,
|
|
2557
|
+
b21 = a21 * a10 - a11 * a20,
|
|
2558
|
+
determinant = a00 * b01 + a01 * b11 + a02 * b21;
|
|
2559
|
+
if (Math.abs(determinant) < Number.EPSILON) {
|
|
2560
|
+
throw new Error("fbx: model transform is not invertible");
|
|
2561
|
+
}
|
|
2562
|
+
const inverseDeterminant = 1 / determinant;
|
|
2563
|
+
return [b01 * inverseDeterminant, (-a22 * a01 + a02 * a21) * inverseDeterminant, (a12 * a01 - a02 * a11) * inverseDeterminant, b11 * inverseDeterminant, (a22 * a00 - a02 * a20) * inverseDeterminant, (-a12 * a00 + a02 * a10) * inverseDeterminant, b21 * inverseDeterminant, (-a21 * a00 + a01 * a20) * inverseDeterminant, (a11 * a00 - a01 * a10) * inverseDeterminant];
|
|
2564
|
+
}
|
|
2565
|
+
function transformVectorMatrix3(vector, matrix) {
|
|
2566
|
+
return [matrix[0] * vector[0] + matrix[3] * vector[1] + matrix[6] * vector[2], matrix[1] * vector[0] + matrix[4] * vector[1] + matrix[7] * vector[2], matrix[2] * vector[0] + matrix[5] * vector[1] + matrix[8] * vector[2]];
|
|
2567
|
+
}
|
|
2568
|
+
function normalizeVector3(vector) {
|
|
2569
|
+
const length = Math.hypot(vector[0], vector[1], vector[2]);
|
|
2570
|
+
if (length === 0) {
|
|
2571
|
+
return [0, 0, 0];
|
|
2572
|
+
}
|
|
2573
|
+
return vector.map(value => cleanFloat(value / length));
|
|
2574
|
+
}
|
|
2575
|
+
function normalizeQuaternion(quaternion) {
|
|
2576
|
+
const length = Math.hypot(quaternion[0], quaternion[1], quaternion[2], quaternion[3]);
|
|
2577
|
+
if (length === 0) {
|
|
2578
|
+
return [0, 0, 0, 1];
|
|
2579
|
+
}
|
|
2580
|
+
return quaternion.map(value => cleanFloat(value / length));
|
|
2581
|
+
}
|
|
2582
|
+
function dotVector3(a, b) {
|
|
2583
|
+
return cleanFloat(a[0] * b[0] + a[1] * b[1] + a[2] * b[2]);
|
|
2584
|
+
}
|
|
2585
|
+
function degreesToRadians(value) {
|
|
2586
|
+
return value * Math.PI / 180;
|
|
2587
|
+
}
|
|
2588
|
+
function cleanFloat(value) {
|
|
2589
|
+
return Math.abs(value) < 1e-12 ? 0 : value;
|
|
2590
|
+
}
|
|
2591
|
+
function cleanVector3(vector) {
|
|
2592
|
+
return vector.map(cleanFloat);
|
|
2593
|
+
}
|
|
2594
|
+
function cleanNumericArray(values) {
|
|
2595
|
+
return Array.from(values, cleanFloat);
|
|
2596
|
+
}
|
|
2597
|
+
function hasArrayValues(values) {
|
|
2598
|
+
return (Array.isArray(values) || ArrayBuffer.isView(values)) && values.length > 0;
|
|
2599
|
+
}
|
|
2600
|
+
function cleanMatrix4(matrix) {
|
|
2601
|
+
return matrix.map(cleanFloat);
|
|
2602
|
+
}
|
|
2603
|
+
function indexObjectNodes(objectsNode) {
|
|
2604
|
+
const byId = {};
|
|
2605
|
+
const byType = {};
|
|
2606
|
+
const list = [];
|
|
2607
|
+
for (const node of objectsNode.children) {
|
|
2608
|
+
if (!node.properties.length) {
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
const summary = readObjectEntry(node);
|
|
2612
|
+
const entry = {
|
|
2613
|
+
...summary,
|
|
2614
|
+
node
|
|
2615
|
+
};
|
|
2616
|
+
list.push(entry);
|
|
2617
|
+
byId[entry.key] = entry;
|
|
2618
|
+
pushIndex(byType, entry.nodeName, entry);
|
|
2619
|
+
}
|
|
2620
|
+
return {
|
|
2621
|
+
list,
|
|
2622
|
+
byId,
|
|
2623
|
+
byType
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
function connectedParentObjects(entry, objectIndex, connections, nodeName) {
|
|
2627
|
+
return (connections.parentsByChild[entry.key] || []).map(key => objectIndex.byId[key]).filter(parent => parent && parent.nodeName === nodeName);
|
|
2628
|
+
}
|
|
2629
|
+
function connectedChildObjects(entry, objectIndex, connections, nodeName) {
|
|
2630
|
+
if (!entry) {
|
|
2631
|
+
return [];
|
|
2632
|
+
}
|
|
2633
|
+
return (connections.childrenByParent[entry.key] || []).map(key => objectIndex.byId[key]).filter(child => child && child.nodeName === nodeName);
|
|
2634
|
+
}
|
|
2635
|
+
function requireChildArray(node, childName) {
|
|
2636
|
+
const values = readChildArray(node, childName);
|
|
2637
|
+
if (!values) {
|
|
2638
|
+
throw new Error(`fbx: geometry is missing ${childName}`);
|
|
2639
|
+
}
|
|
2640
|
+
return values;
|
|
2641
|
+
}
|
|
2642
|
+
function readChildArray(node, childName) {
|
|
2643
|
+
const child = findFirstNode(node.children, childName);
|
|
2644
|
+
return child ? readNodeArray(child, childName) : null;
|
|
2645
|
+
}
|
|
2646
|
+
function readNodeArray(node, feature) {
|
|
2647
|
+
for (const property of node.properties) {
|
|
2648
|
+
const values = arrayPropertyToNumbers(property, feature);
|
|
2649
|
+
if (values) {
|
|
2650
|
+
return values;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
const asciiArray = findFirstNode(node.children, "a");
|
|
2654
|
+
return asciiArray ? asciiArray.properties.map(value => finiteNumber(value, feature)) : null;
|
|
2655
|
+
}
|
|
2656
|
+
function arrayPropertyToNumbers(property, feature) {
|
|
2657
|
+
if (!property) {
|
|
2658
|
+
return null;
|
|
2659
|
+
}
|
|
2660
|
+
if (property.compressed) {
|
|
2661
|
+
const error = new Error(`fbx: GR2 output cannot read compressed ${feature} arrays yet`);
|
|
2662
|
+
error.code = "CJS_FORMAT_OUTPUT_UNSUPPORTED";
|
|
2663
|
+
error.sourceFormat = "fbx";
|
|
2664
|
+
error.emit = OUTPUT_GR2;
|
|
2665
|
+
error.feature = "compressed-array";
|
|
2666
|
+
throw error;
|
|
2667
|
+
}
|
|
2668
|
+
if (ArrayBuffer.isView(property)) {
|
|
2669
|
+
return Array.from(property, value => finiteNumber(value, feature));
|
|
2670
|
+
}
|
|
2671
|
+
if (Array.isArray(property)) {
|
|
2672
|
+
return property.map(value => finiteNumber(value, feature));
|
|
2673
|
+
}
|
|
2674
|
+
return null;
|
|
2675
|
+
}
|
|
2676
|
+
function decodePolygonCorners(controlPoints, polygonVertexIndex, geometry) {
|
|
2677
|
+
const controlPointCount = controlPoints.length / 3,
|
|
2678
|
+
positions = [],
|
|
2679
|
+
corners = [],
|
|
2680
|
+
polygons = [];
|
|
2681
|
+
let polygon = [];
|
|
2682
|
+
for (const rawValue of polygonVertexIndex) {
|
|
2683
|
+
const raw = integerNumber(rawValue, "PolygonVertexIndex");
|
|
2684
|
+
const end = raw < 0;
|
|
2685
|
+
const controlPointIndex = end ? -raw - 1 : raw;
|
|
2686
|
+
if (controlPointIndex < 0 || controlPointIndex >= controlPointCount) {
|
|
2687
|
+
throw new Error(`fbx: geometry ${JSON.stringify(geometry.name || geometry.key)} has PolygonVertexIndex ${raw} out of range`);
|
|
2688
|
+
}
|
|
2689
|
+
const source = controlPointIndex * 3;
|
|
2690
|
+
const outputIndex = positions.length / 3;
|
|
2691
|
+
const polygonIndex = polygons.length;
|
|
2692
|
+
positions.push(finiteNumber(controlPoints[source], "Vertices"), finiteNumber(controlPoints[source + 1], "Vertices"), finiteNumber(controlPoints[source + 2], "Vertices"));
|
|
2693
|
+
corners.push({
|
|
2694
|
+
outputIndex,
|
|
2695
|
+
controlPointIndex,
|
|
2696
|
+
polygonIndex,
|
|
2697
|
+
polygonCornerIndex: polygon.length
|
|
2698
|
+
});
|
|
2699
|
+
polygon.push(outputIndex);
|
|
2700
|
+
if (end) {
|
|
2701
|
+
if (polygon.length < 3) {
|
|
2702
|
+
throw new Error(`fbx: geometry ${JSON.stringify(geometry.name || geometry.key)} has a polygon with fewer than three vertices`);
|
|
2703
|
+
}
|
|
2704
|
+
polygons.push(polygon);
|
|
2705
|
+
polygon = [];
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
if (polygon.length) {
|
|
2709
|
+
throw new Error(`fbx: geometry ${JSON.stringify(geometry.name || geometry.key)} has an unterminated polygon`);
|
|
2710
|
+
}
|
|
2711
|
+
return {
|
|
2712
|
+
positions,
|
|
2713
|
+
corners,
|
|
2714
|
+
polygons
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
function readLayerElementChannel(geometryNode, layerName, layerIndex, valueNames, indexNames, elementSize, decoded, feature) {
|
|
2718
|
+
const layer = findLayerElementNode(geometryNode, layerName, layerIndex);
|
|
2719
|
+
if (!layer) {
|
|
2720
|
+
return [];
|
|
2721
|
+
}
|
|
2722
|
+
const values = readFirstChildArray(layer, valueNames);
|
|
2723
|
+
if (!values) {
|
|
2724
|
+
return [];
|
|
2725
|
+
}
|
|
2726
|
+
if (values.length % elementSize !== 0) {
|
|
2727
|
+
throw new Error(`fbx: ${feature} array length is not divisible by ${elementSize}`);
|
|
2728
|
+
}
|
|
2729
|
+
const indices = readFirstChildArray(layer, indexNames) || [],
|
|
2730
|
+
mapping = String(readFirstChildProperty(layer, "MappingInformationType") || "ByPolygonVertex"),
|
|
2731
|
+
reference = String(readFirstChildProperty(layer, "ReferenceInformationType") || "Direct"),
|
|
2732
|
+
output = [];
|
|
2733
|
+
for (const corner of decoded.corners) {
|
|
2734
|
+
const mappedIndex = resolveLayerMappingIndex(mapping, corner, feature),
|
|
2735
|
+
directIndex = resolveLayerReferenceIndex(reference, indices, mappedIndex, feature);
|
|
2736
|
+
appendLayerElement(output, values, directIndex, elementSize, feature);
|
|
2737
|
+
}
|
|
2738
|
+
return output;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
/**
|
|
2742
|
+
* Read an FBX UV layer and apply the configured V-axis convention.
|
|
2743
|
+
*
|
|
2744
|
+
* @param {object} geometryNode Geometry node.
|
|
2745
|
+
* @param {number} layerIndex UV layer index.
|
|
2746
|
+
* @param {object} decoded Decoded polygon corner state.
|
|
2747
|
+
* @param {object} values Normalized reader values.
|
|
2748
|
+
* @returns {number[]} Expanded UV channel.
|
|
2749
|
+
*/
|
|
2750
|
+
function readUvLayerElementChannel(geometryNode, layerIndex, decoded, values) {
|
|
2751
|
+
const texcoord = readLayerElementChannel(geometryNode, "LayerElementUV", layerIndex, ["UV"], ["UVIndex", "UVIndices"], 2, decoded, "UV");
|
|
2752
|
+
return values.flipV ? flipTexcoordV(texcoord) : texcoord;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
/**
|
|
2756
|
+
* Flip the V coordinate of an expanded UV channel.
|
|
2757
|
+
*
|
|
2758
|
+
* @param {number[]} texcoord Expanded UV channel.
|
|
2759
|
+
* @returns {number[]} Flipped UV channel.
|
|
2760
|
+
*/
|
|
2761
|
+
function flipTexcoordV(texcoord) {
|
|
2762
|
+
const flipped = texcoord.slice();
|
|
2763
|
+
for (let i = 1; i < flipped.length; i += 2) {
|
|
2764
|
+
flipped[i] = 1 - flipped[i];
|
|
2765
|
+
}
|
|
2766
|
+
return flipped;
|
|
2767
|
+
}
|
|
2768
|
+
function findLayerElementNode(geometryNode, layerName, layerIndex) {
|
|
2769
|
+
const layers = geometryNode.children.filter(child => child.name === layerName);
|
|
2770
|
+
return layers.find((child, index) => {
|
|
2771
|
+
const id = child.properties.length ? Number(child.properties[0]) : index;
|
|
2772
|
+
return Number.isFinite(id) && id === layerIndex;
|
|
2773
|
+
}) || layers[layerIndex] || null;
|
|
2774
|
+
}
|
|
2775
|
+
function readFirstChildArray(node, childNames) {
|
|
2776
|
+
for (const childName of childNames) {
|
|
2777
|
+
const values = readChildArray(node, childName);
|
|
2778
|
+
if (values) {
|
|
2779
|
+
return values;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
return null;
|
|
2783
|
+
}
|
|
2784
|
+
function resolveLayerMappingIndex(mapping, corner, feature) {
|
|
2785
|
+
switch (mapping) {
|
|
2786
|
+
case "ByPolygonVertex":
|
|
2787
|
+
return corner.outputIndex;
|
|
2788
|
+
case "ByControlPoint":
|
|
2789
|
+
case "ByVertice":
|
|
2790
|
+
case "ByVertex":
|
|
2791
|
+
return corner.controlPointIndex;
|
|
2792
|
+
case "ByPolygon":
|
|
2793
|
+
return corner.polygonIndex;
|
|
2794
|
+
case "AllSame":
|
|
2795
|
+
return 0;
|
|
2796
|
+
default:
|
|
2797
|
+
throw new Error(`fbx: unsupported ${feature} MappingInformationType ${JSON.stringify(mapping)}`);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
function resolveLayerReferenceIndex(reference, indices, mappedIndex, feature) {
|
|
2801
|
+
switch (reference) {
|
|
2802
|
+
case "Direct":
|
|
2803
|
+
return mappedIndex;
|
|
2804
|
+
case "IndexToDirect":
|
|
2805
|
+
case "Index":
|
|
2806
|
+
{
|
|
2807
|
+
if (mappedIndex >= indices.length) {
|
|
2808
|
+
throw new Error(`fbx: ${feature} index array is too short`);
|
|
2809
|
+
}
|
|
2810
|
+
return integerNumber(indices[mappedIndex], feature);
|
|
2811
|
+
}
|
|
2812
|
+
default:
|
|
2813
|
+
throw new Error(`fbx: unsupported ${feature} ReferenceInformationType ${JSON.stringify(reference)}`);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
function appendLayerElement(output, values, directIndex, elementSize, feature) {
|
|
2817
|
+
const offset = directIndex * elementSize;
|
|
2818
|
+
if (directIndex < 0 || offset + elementSize > values.length) {
|
|
2819
|
+
throw new Error(`fbx: ${feature} direct index ${directIndex} is out of range`);
|
|
2820
|
+
}
|
|
2821
|
+
for (let i = 0; i < elementSize; i++) {
|
|
2822
|
+
output.push(finiteNumber(values[offset + i], feature));
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
function buildIndexGroups(polygonTriangles, materialNames) {
|
|
2826
|
+
const groups = [];
|
|
2827
|
+
const groupByName = new Map();
|
|
2828
|
+
let maxIndex = 0;
|
|
2829
|
+
for (let polygonIndex = 0; polygonIndex < polygonTriangles.length; polygonIndex++) {
|
|
2830
|
+
const triangles = polygonTriangles[polygonIndex],
|
|
2831
|
+
name = materialNames[polygonIndex] || "default",
|
|
2832
|
+
group = getIndexGroup(groups, groupByName, name);
|
|
2833
|
+
for (const triangle of triangles) {
|
|
2834
|
+
group.faces.push(triangle[0], triangle[1], triangle[2]);
|
|
2835
|
+
maxIndex = Math.max(maxIndex, triangle[0], triangle[1], triangle[2]);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
const bytesPerIndex = maxIndex > 0xffff ? 4 : 2;
|
|
2839
|
+
for (const group of groups) {
|
|
2840
|
+
group.bytesPerIndex = bytesPerIndex;
|
|
2841
|
+
}
|
|
2842
|
+
return groups;
|
|
2843
|
+
}
|
|
2844
|
+
function triangulatePolygonList(polygons, positions) {
|
|
2845
|
+
return polygons.map(polygon => triangulatePolygon(polygon, positions));
|
|
2846
|
+
}
|
|
2847
|
+
function triangulatePolygon(polygon, positions) {
|
|
2848
|
+
if (polygon.length === 3) {
|
|
2849
|
+
return [polygon.slice()];
|
|
2850
|
+
}
|
|
2851
|
+
const projected = projectPolygonTo2D(polygon, positions);
|
|
2852
|
+
if (!projected) {
|
|
2853
|
+
return triangulatePolygonFan(polygon);
|
|
2854
|
+
}
|
|
2855
|
+
const area = signedArea2D(projected);
|
|
2856
|
+
if (Math.abs(area) <= TRIANGULATION_EPSILON) {
|
|
2857
|
+
return triangulatePolygonFan(polygon);
|
|
2858
|
+
}
|
|
2859
|
+
if (isConvexPolygon2D(projected, area)) {
|
|
2860
|
+
return triangulatePolygonFan(polygon);
|
|
2861
|
+
}
|
|
2862
|
+
const triangles = [],
|
|
2863
|
+
remaining = polygon.map((value, index) => index);
|
|
2864
|
+
let guard = polygon.length * polygon.length;
|
|
2865
|
+
while (remaining.length > 3 && guard-- > 0) {
|
|
2866
|
+
let clipped = false;
|
|
2867
|
+
for (let index = 0; index < remaining.length; index++) {
|
|
2868
|
+
const previous = remaining[(index + remaining.length - 1) % remaining.length],
|
|
2869
|
+
current = remaining[index],
|
|
2870
|
+
next = remaining[(index + 1) % remaining.length];
|
|
2871
|
+
if (!isEar2D(previous, current, next, remaining, projected, area)) {
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
triangles.push([polygon[previous], polygon[current], polygon[next]]);
|
|
2875
|
+
remaining.splice(index, 1);
|
|
2876
|
+
clipped = true;
|
|
2877
|
+
break;
|
|
2878
|
+
}
|
|
2879
|
+
if (!clipped) {
|
|
2880
|
+
return triangulatePolygonFan(polygon);
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
if (remaining.length === 3) {
|
|
2884
|
+
triangles.push([polygon[remaining[0]], polygon[remaining[1]], polygon[remaining[2]]]);
|
|
2885
|
+
}
|
|
2886
|
+
return triangles.length ? triangles : triangulatePolygonFan(polygon);
|
|
2887
|
+
}
|
|
2888
|
+
function flattenPolygonTriangles(polygonTriangles) {
|
|
2889
|
+
const faces = [];
|
|
2890
|
+
for (const triangles of polygonTriangles) {
|
|
2891
|
+
for (const triangle of triangles) {
|
|
2892
|
+
faces.push(triangle[0], triangle[1], triangle[2]);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
return faces;
|
|
2896
|
+
}
|
|
2897
|
+
function triangulatePolygonFan(polygon) {
|
|
2898
|
+
const triangles = [];
|
|
2899
|
+
for (let i = 1; i < polygon.length - 1; i++) {
|
|
2900
|
+
triangles.push([polygon[0], polygon[i], polygon[i + 1]]);
|
|
2901
|
+
}
|
|
2902
|
+
return triangles;
|
|
2903
|
+
}
|
|
2904
|
+
function projectPolygonTo2D(polygon, positions) {
|
|
2905
|
+
const normal = computePolygonNormal(polygon, positions);
|
|
2906
|
+
const absX = Math.abs(normal[0]),
|
|
2907
|
+
absY = Math.abs(normal[1]),
|
|
2908
|
+
absZ = Math.abs(normal[2]);
|
|
2909
|
+
if (Math.max(absX, absY, absZ) <= TRIANGULATION_EPSILON) {
|
|
2910
|
+
return null;
|
|
2911
|
+
}
|
|
2912
|
+
const dropAxis = absX > absY && absX > absZ ? 0 : absY > absZ ? 1 : 2;
|
|
2913
|
+
return polygon.map(index => {
|
|
2914
|
+
const offset = index * 3;
|
|
2915
|
+
if (dropAxis === 0) {
|
|
2916
|
+
return [positions[offset + 1], positions[offset + 2]];
|
|
2917
|
+
}
|
|
2918
|
+
if (dropAxis === 1) {
|
|
2919
|
+
return [positions[offset], positions[offset + 2]];
|
|
2920
|
+
}
|
|
2921
|
+
return [positions[offset], positions[offset + 1]];
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
function computePolygonNormal(polygon, positions) {
|
|
2925
|
+
const normal = [0, 0, 0];
|
|
2926
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
2927
|
+
const current = polygon[i] * 3,
|
|
2928
|
+
next = polygon[(i + 1) % polygon.length] * 3,
|
|
2929
|
+
x0 = positions[current],
|
|
2930
|
+
y0 = positions[current + 1],
|
|
2931
|
+
z0 = positions[current + 2],
|
|
2932
|
+
x1 = positions[next],
|
|
2933
|
+
y1 = positions[next + 1],
|
|
2934
|
+
z1 = positions[next + 2];
|
|
2935
|
+
normal[0] += (y0 - y1) * (z0 + z1);
|
|
2936
|
+
normal[1] += (z0 - z1) * (x0 + x1);
|
|
2937
|
+
normal[2] += (x0 - x1) * (y0 + y1);
|
|
2938
|
+
}
|
|
2939
|
+
return normal;
|
|
2940
|
+
}
|
|
2941
|
+
function signedArea2D(points) {
|
|
2942
|
+
let area = 0;
|
|
2943
|
+
for (let i = 0; i < points.length; i++) {
|
|
2944
|
+
const current = points[i],
|
|
2945
|
+
next = points[(i + 1) % points.length];
|
|
2946
|
+
area += current[0] * next[1] - next[0] * current[1];
|
|
2947
|
+
}
|
|
2948
|
+
return area * 0.5;
|
|
2949
|
+
}
|
|
2950
|
+
function isEar2D(previous, current, next, remaining, points, area) {
|
|
2951
|
+
const cross = cross2D(points[previous], points[current], points[next]);
|
|
2952
|
+
if (area > 0 ? cross <= TRIANGULATION_EPSILON : cross >= -TRIANGULATION_EPSILON) {
|
|
2953
|
+
return false;
|
|
2954
|
+
}
|
|
2955
|
+
for (const pointIndex of remaining) {
|
|
2956
|
+
if (pointIndex === previous || pointIndex === current || pointIndex === next) {
|
|
2957
|
+
continue;
|
|
2958
|
+
}
|
|
2959
|
+
if (pointInTriangle2D(points[pointIndex], points[previous], points[current], points[next])) {
|
|
2960
|
+
return false;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
return true;
|
|
2964
|
+
}
|
|
2965
|
+
function isConvexPolygon2D(points, area) {
|
|
2966
|
+
for (let i = 0; i < points.length; i++) {
|
|
2967
|
+
const cross = cross2D(points[i], points[(i + 1) % points.length], points[(i + 2) % points.length]);
|
|
2968
|
+
if (Math.abs(cross) <= TRIANGULATION_EPSILON) {
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
if (area > 0 ? cross < -TRIANGULATION_EPSILON : cross > TRIANGULATION_EPSILON) {
|
|
2972
|
+
return false;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
return true;
|
|
2976
|
+
}
|
|
2977
|
+
function cross2D(a, b, c) {
|
|
2978
|
+
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
2979
|
+
}
|
|
2980
|
+
function pointInTriangle2D(point, a, b, c) {
|
|
2981
|
+
const ab = cross2D(a, b, point),
|
|
2982
|
+
bc = cross2D(b, c, point),
|
|
2983
|
+
ca = cross2D(c, a, point),
|
|
2984
|
+
hasNegative = ab < -TRIANGULATION_EPSILON || bc < -TRIANGULATION_EPSILON || ca < -TRIANGULATION_EPSILON,
|
|
2985
|
+
hasPositive = ab > TRIANGULATION_EPSILON || bc > TRIANGULATION_EPSILON || ca > TRIANGULATION_EPSILON;
|
|
2986
|
+
return !(hasNegative && hasPositive);
|
|
2987
|
+
}
|
|
2988
|
+
function getIndexGroup(groups, groupByName, name) {
|
|
2989
|
+
let group = groupByName.get(name);
|
|
2990
|
+
if (!group) {
|
|
2991
|
+
group = {
|
|
2992
|
+
name,
|
|
2993
|
+
bytesPerIndex: 2,
|
|
2994
|
+
faces: []
|
|
2995
|
+
};
|
|
2996
|
+
groupByName.set(name, group);
|
|
2997
|
+
groups.push(group);
|
|
2998
|
+
}
|
|
2999
|
+
return group;
|
|
3000
|
+
}
|
|
3001
|
+
function readPolygonMaterialNames(geometry, owner, objectIndex, connections, polygonCount) {
|
|
3002
|
+
const materialSlots = readMaterialSlotNames(geometry, owner, objectIndex, connections);
|
|
3003
|
+
if (!materialSlots.length) {
|
|
3004
|
+
return new Array(polygonCount).fill("default");
|
|
3005
|
+
}
|
|
3006
|
+
const layer = findFirstNode(geometry.node.children, "LayerElementMaterial");
|
|
3007
|
+
if (!layer) {
|
|
3008
|
+
return new Array(polygonCount).fill(materialSlots[0] || "default");
|
|
3009
|
+
}
|
|
3010
|
+
const mapping = String(readFirstChildProperty(layer, "MappingInformationType") || "AllSame"),
|
|
3011
|
+
materials = readChildArray(layer, "Materials") || [],
|
|
3012
|
+
names = [];
|
|
3013
|
+
for (let i = 0; i < polygonCount; i++) {
|
|
3014
|
+
const slot = mapping === "ByPolygon" ? integerNumber(materials[i] ?? 0, "Materials") : integerNumber(materials[0] ?? 0, "Materials");
|
|
3015
|
+
names.push(slot >= 0 ? materialSlots[slot] || `material_${slot}` : "default");
|
|
3016
|
+
}
|
|
3017
|
+
return names;
|
|
3018
|
+
}
|
|
3019
|
+
function readMaterialSlotNames(geometry, owner, objectIndex, connections) {
|
|
3020
|
+
const materials = [...connectedChildObjects(owner, objectIndex, connections, "Material"), ...connectedChildObjects(geometry, objectIndex, connections, "Material")];
|
|
3021
|
+
const seen = new Set();
|
|
3022
|
+
const names = [];
|
|
3023
|
+
for (const material of materials) {
|
|
3024
|
+
if (seen.has(material.key)) {
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
seen.add(material.key);
|
|
3028
|
+
names.push(material.name || `material_${names.length}`);
|
|
3029
|
+
}
|
|
3030
|
+
return names;
|
|
3031
|
+
}
|
|
3032
|
+
function meshNameFromFbx(geometry, owner) {
|
|
3033
|
+
return owner?.name || geometry.name || "fbx_mesh";
|
|
3034
|
+
}
|
|
3035
|
+
function createGr2Vertex(position, channels = {}) {
|
|
3036
|
+
return {
|
|
3037
|
+
position,
|
|
3038
|
+
blendIndice: channels.blendIndice || [],
|
|
3039
|
+
tangent: channels.tangent || [],
|
|
3040
|
+
normal: channels.normal || [],
|
|
3041
|
+
texcoord0: channels.texcoord0 || [],
|
|
3042
|
+
texcoord1: channels.texcoord1 || [],
|
|
3043
|
+
color0: channels.color0 || [],
|
|
3044
|
+
binormal: channels.binormal || [],
|
|
3045
|
+
blendWeight: channels.blendWeight || []
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
function computeBounds(positions) {
|
|
3049
|
+
if (!positions.length) {
|
|
3050
|
+
return {
|
|
3051
|
+
minBounds: [0, 0, 0],
|
|
3052
|
+
maxBounds: [0, 0, 0]
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
const minBounds = [positions[0], positions[1], positions[2]],
|
|
3056
|
+
maxBounds = [positions[0], positions[1], positions[2]];
|
|
3057
|
+
for (let i = 3; i < positions.length; i += 3) {
|
|
3058
|
+
for (let c = 0; c < 3; c++) {
|
|
3059
|
+
const value = positions[i + c];
|
|
3060
|
+
if (value < minBounds[c]) {
|
|
3061
|
+
minBounds[c] = value;
|
|
3062
|
+
}
|
|
3063
|
+
if (value > maxBounds[c]) {
|
|
3064
|
+
maxBounds[c] = value;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
return {
|
|
3069
|
+
minBounds,
|
|
3070
|
+
maxBounds
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
function hydrateGr2(root, classes, hydrationOptions = {}) {
|
|
3074
|
+
const hydrationClasses = createHydrationClasses(classes, hydrationOptions);
|
|
3075
|
+
return hydrate("Root", {
|
|
3076
|
+
grannyFileFormatRevision: root.grannyFileFormatRevision,
|
|
3077
|
+
grannyFileSource: root.grannyFileSource,
|
|
3078
|
+
meshes: root.meshes.map(mesh => hydrate("Mesh", {
|
|
3079
|
+
name: mesh.name,
|
|
3080
|
+
morphTargets: mesh.morphTargets.map(target => hydrate("MorphTarget", target, hydrationClasses)),
|
|
3081
|
+
minBounds: mesh.minBounds,
|
|
3082
|
+
maxBounds: mesh.maxBounds,
|
|
3083
|
+
boneBindings: mesh.boneBindings.map(binding => hydrate("BoneBinding", binding, hydrationClasses)),
|
|
3084
|
+
vertex: mesh.vertex,
|
|
3085
|
+
indices: mesh.indices.map(group => hydrate("IndexGroup", group, hydrationClasses))
|
|
3086
|
+
}, hydrationClasses)),
|
|
3087
|
+
models: root.models.map(model => hydrateGr2Model(model, hydrationClasses)),
|
|
3088
|
+
animations: root.animations
|
|
3089
|
+
}, hydrationClasses, hydrationOptions);
|
|
3090
|
+
}
|
|
3091
|
+
function hydrateGr2Model(model, classes) {
|
|
3092
|
+
return hydrate("Model", {
|
|
3093
|
+
...model,
|
|
3094
|
+
skeleton: hydrateGr2Skeleton(model.skeleton, classes)
|
|
3095
|
+
}, classes);
|
|
3096
|
+
}
|
|
3097
|
+
function hydrateGr2Skeleton(skeleton, classes) {
|
|
3098
|
+
return hydrate("Skeleton", {
|
|
3099
|
+
...skeleton,
|
|
3100
|
+
bones: skeleton.bones.map(bone => hydrate("Bone", bone, classes))
|
|
3101
|
+
}, classes);
|
|
3102
|
+
}
|
|
3103
|
+
function buildCmfFromShared(root) {
|
|
3104
|
+
return {
|
|
3105
|
+
version: 1,
|
|
3106
|
+
metadata: buildCmfMetadata(root),
|
|
3107
|
+
meshes: (root.meshes ?? []).map(mesh => buildCmfMesh(mesh)),
|
|
3108
|
+
skeletons: root.skeletons ?? [],
|
|
3109
|
+
animations: root.cmfAnimations ?? []
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
function buildCmfMetadata(root) {
|
|
3113
|
+
const source = String(root.grannyFileSource || ""),
|
|
3114
|
+
entries = [];
|
|
3115
|
+
if (source && source !== "memory") {
|
|
3116
|
+
entries.push({
|
|
3117
|
+
key: "source",
|
|
3118
|
+
value: source
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
entries.push({
|
|
3122
|
+
key: "sourceFormat",
|
|
3123
|
+
value: "fbx"
|
|
3124
|
+
}, {
|
|
3125
|
+
key: "generator",
|
|
3126
|
+
value: "CjsFbxFormat"
|
|
3127
|
+
});
|
|
3128
|
+
return {
|
|
3129
|
+
entries
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
function buildCmfMesh(mesh) {
|
|
3133
|
+
const vertex = mesh.vertex ?? {},
|
|
3134
|
+
indices = mesh.indices ?? [],
|
|
3135
|
+
boneBindings = (mesh.boneBindings ?? []).map(binding => buildCmfBoneBinding(binding)),
|
|
3136
|
+
morphTargets = buildCmfMorphTargets(mesh),
|
|
3137
|
+
areas = indices.map(group => buildCmfMeshArea(group, mesh, morphTargets)),
|
|
3138
|
+
stride = estimateVertexStride(vertex),
|
|
3139
|
+
vertexCount = stride === 0 ? 0 : Math.floor((vertex.position ?? []).length / 3);
|
|
3140
|
+
return {
|
|
3141
|
+
name: mesh.name ?? "",
|
|
3142
|
+
decl: buildCmfDecl(vertex),
|
|
3143
|
+
lods: [{
|
|
3144
|
+
vb: {
|
|
3145
|
+
index: 1,
|
|
3146
|
+
offset: 0,
|
|
3147
|
+
size: vertexCount * stride,
|
|
3148
|
+
stride
|
|
3149
|
+
},
|
|
3150
|
+
ib: {
|
|
3151
|
+
index: 2,
|
|
3152
|
+
offset: 0,
|
|
3153
|
+
size: totalIndexCount(indices) * cmfBytesPerIndex(indices),
|
|
3154
|
+
stride: cmfBytesPerIndex(indices)
|
|
3155
|
+
},
|
|
3156
|
+
areas: indices.map((group, index) => ({
|
|
3157
|
+
firstElement: firstTriangle(indices, index),
|
|
3158
|
+
elementCount: Math.floor((group.faces ?? []).length / 3)
|
|
3159
|
+
})),
|
|
3160
|
+
morphTargets: morphTargets.lods,
|
|
3161
|
+
threshold: 0xffffffff,
|
|
3162
|
+
vertex,
|
|
3163
|
+
indices
|
|
3164
|
+
}],
|
|
3165
|
+
areas,
|
|
3166
|
+
boneBindings,
|
|
3167
|
+
morphTargets: {
|
|
3168
|
+
decl: morphTargets.decl,
|
|
3169
|
+
targets: morphTargets.targets
|
|
3170
|
+
},
|
|
3171
|
+
uvDensities: [],
|
|
3172
|
+
bounds: cmfBounds(mesh),
|
|
3173
|
+
audioOcclusionMesh: {
|
|
3174
|
+
vertices: [],
|
|
3175
|
+
indices: [],
|
|
3176
|
+
bounds: {
|
|
3177
|
+
min: [0, 0, 0],
|
|
3178
|
+
max: [0, 0, 0]
|
|
3179
|
+
}
|
|
3180
|
+
},
|
|
3181
|
+
topology: "TriangleList",
|
|
3182
|
+
skeleton: mesh.skeleton ?? null,
|
|
3183
|
+
vertex,
|
|
3184
|
+
indices
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
function buildCmfMeshArea(group, mesh, morphTargets) {
|
|
3188
|
+
const bones = buildCmfAreaBones(group, mesh.vertex ?? {});
|
|
3189
|
+
return {
|
|
3190
|
+
name: group.name ?? "",
|
|
3191
|
+
bounds: cmfAreaBounds(group, mesh),
|
|
3192
|
+
bones,
|
|
3193
|
+
affectedByBones: bones.length > 0,
|
|
3194
|
+
affectedByMorphTargets: isCmfAreaAffectedByMorphTargets(group, mesh.vertex ?? {}, morphTargets.lods)
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
function buildCmfAreaBones(group, vertex) {
|
|
3198
|
+
const boneIndices = vertex.blendIndice ?? [],
|
|
3199
|
+
boneWeights = vertex.blendWeight ?? [],
|
|
3200
|
+
hasWeights = hasArrayValues(boneWeights),
|
|
3201
|
+
bones = new Set();
|
|
3202
|
+
if (!hasArrayValues(boneIndices)) {
|
|
3203
|
+
return [];
|
|
3204
|
+
}
|
|
3205
|
+
for (const vertexIndex of group.faces ?? []) {
|
|
3206
|
+
const offset = vertexIndex * 4;
|
|
3207
|
+
for (let i = 0; i < 4; i++) {
|
|
3208
|
+
const weight = hasWeights ? boneWeights[offset + i] ?? 0 : i === 0 ? 1 : 0;
|
|
3209
|
+
if (weight > 0) {
|
|
3210
|
+
bones.add(boneIndices[offset + i] ?? 0);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
return Array.from(bones);
|
|
3215
|
+
}
|
|
3216
|
+
function isCmfAreaAffectedByMorphTargets(group, baseVertex, morphLods) {
|
|
3217
|
+
const basePositions = baseVertex.position ?? [];
|
|
3218
|
+
for (const lod of morphLods) {
|
|
3219
|
+
const positions = lod.vertex?.position ?? [];
|
|
3220
|
+
for (const vertexIndex of group.faces ?? []) {
|
|
3221
|
+
const offset = vertexIndex * 3;
|
|
3222
|
+
if (positions[offset] !== basePositions[offset] || positions[offset + 1] !== basePositions[offset + 1] || positions[offset + 2] !== basePositions[offset + 2]) {
|
|
3223
|
+
return true;
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
return false;
|
|
3228
|
+
}
|
|
3229
|
+
function buildCmfMorphTargets(mesh) {
|
|
3230
|
+
const targets = mesh.morphTargets ?? [];
|
|
3231
|
+
if (!targets.length) {
|
|
3232
|
+
return {
|
|
3233
|
+
decl: [],
|
|
3234
|
+
targets: [],
|
|
3235
|
+
lods: []
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
const targetVertices = targets.map(target => target.vertex ?? {}),
|
|
3239
|
+
decl = buildCmfDeclFromVertices(targetVertices),
|
|
3240
|
+
stride = estimateStrideFromDecl(decl);
|
|
3241
|
+
return {
|
|
3242
|
+
decl,
|
|
3243
|
+
targets: targets.map(target => ({
|
|
3244
|
+
name: target.name ?? "",
|
|
3245
|
+
maxDisplacement: target.maxDisplacement ?? computeMaxDisplacement(mesh.vertex?.position ?? [], target.vertex?.position ?? [])
|
|
3246
|
+
})),
|
|
3247
|
+
lods: targets.map(target => {
|
|
3248
|
+
const morphVertex = target.vertex ?? {},
|
|
3249
|
+
vertexCount = Math.floor((morphVertex.position ?? []).length / 3);
|
|
3250
|
+
return {
|
|
3251
|
+
vb: {
|
|
3252
|
+
index: 0,
|
|
3253
|
+
offset: 0,
|
|
3254
|
+
size: vertexCount * stride,
|
|
3255
|
+
stride
|
|
3256
|
+
},
|
|
3257
|
+
vertex: normalizeCmfVertexForDecl(morphVertex, decl, vertexCount)
|
|
3258
|
+
};
|
|
3259
|
+
})
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
function buildCmfBoneBinding(binding) {
|
|
3263
|
+
return {
|
|
3264
|
+
name: binding.name ?? "",
|
|
3265
|
+
bounds: {
|
|
3266
|
+
min: binding.minBounds ?? binding.bounds?.min ?? [0, 0, 0],
|
|
3267
|
+
max: binding.maxBounds ?? binding.bounds?.max ?? [0, 0, 0]
|
|
3268
|
+
}
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
function buildCmfDecl(vertex) {
|
|
3272
|
+
return buildCmfDeclFromVertices([vertex]);
|
|
3273
|
+
}
|
|
3274
|
+
function buildCmfDeclFromVertices(vertices) {
|
|
3275
|
+
const decl = [];
|
|
3276
|
+
let offset = 0;
|
|
3277
|
+
for (const channel of CMF_VERTEX_CHANNELS) {
|
|
3278
|
+
const [name, usage, elementCount, usageIndex = 0, type = "Float32"] = channel;
|
|
3279
|
+
if (!vertices.some(vertex => hasArrayValues(vertex[name]))) {
|
|
3280
|
+
continue;
|
|
3281
|
+
}
|
|
3282
|
+
decl.push({
|
|
3283
|
+
usage,
|
|
3284
|
+
usageIndex,
|
|
3285
|
+
type,
|
|
3286
|
+
elementCount,
|
|
3287
|
+
offset
|
|
3288
|
+
});
|
|
3289
|
+
offset += elementCount * cmfElementTypeSize(type);
|
|
3290
|
+
}
|
|
3291
|
+
return decl;
|
|
3292
|
+
}
|
|
3293
|
+
function normalizeCmfVertexForDecl(vertex, decl, vertexCount) {
|
|
3294
|
+
const normalized = {
|
|
3295
|
+
...vertex
|
|
3296
|
+
};
|
|
3297
|
+
for (const channel of CMF_VERTEX_CHANNELS) {
|
|
3298
|
+
const [name, usage, elementCount, usageIndex = 0] = channel;
|
|
3299
|
+
if (!decl.some(element => element.usage === usage && element.usageIndex === usageIndex)) {
|
|
3300
|
+
continue;
|
|
3301
|
+
}
|
|
3302
|
+
if (!hasArrayValues(normalized[name])) {
|
|
3303
|
+
normalized[name] = new Array(vertexCount * elementCount).fill(0);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
return normalized;
|
|
3307
|
+
}
|
|
3308
|
+
function estimateVertexStride(vertex) {
|
|
3309
|
+
return estimateStrideFromDecl(buildCmfDecl(vertex));
|
|
3310
|
+
}
|
|
3311
|
+
function estimateStrideFromDecl(decl) {
|
|
3312
|
+
return decl.reduce((stride, element) => Math.max(stride, element.offset + element.elementCount * cmfElementTypeSize(element.type)), 0);
|
|
3313
|
+
}
|
|
3314
|
+
function cmfElementTypeSize(type) {
|
|
3315
|
+
return type === "Float32" ? 4 : type.includes("16") ? 2 : 1;
|
|
3316
|
+
}
|
|
3317
|
+
function totalIndexCount(indices) {
|
|
3318
|
+
return indices.reduce((total, group) => total + (group.faces?.length ?? 0), 0);
|
|
3319
|
+
}
|
|
3320
|
+
function cmfBytesPerIndex(indices) {
|
|
3321
|
+
return indices.some(group => group.bytesPerIndex === 4 || (group.faces ?? []).some(index => index > 0xffff)) ? 4 : 2;
|
|
3322
|
+
}
|
|
3323
|
+
function firstTriangle(indices, areaIndex) {
|
|
3324
|
+
let first = 0;
|
|
3325
|
+
for (let i = 0; i < areaIndex; i++) {
|
|
3326
|
+
first += Math.floor((indices[i].faces ?? []).length / 3);
|
|
3327
|
+
}
|
|
3328
|
+
return first;
|
|
3329
|
+
}
|
|
3330
|
+
function cmfBounds(mesh) {
|
|
3331
|
+
return {
|
|
3332
|
+
min: mesh.minBounds ?? [0, 0, 0],
|
|
3333
|
+
max: mesh.maxBounds ?? [0, 0, 0]
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
function cmfAreaBounds(group, mesh) {
|
|
3337
|
+
const positions = mesh.vertex?.position ?? [],
|
|
3338
|
+
areaPositions = [];
|
|
3339
|
+
for (const vertexIndex of group.faces ?? []) {
|
|
3340
|
+
const offset = vertexIndex * 3;
|
|
3341
|
+
if (offset + 2 < positions.length) {
|
|
3342
|
+
areaPositions.push(positions[offset], positions[offset + 1], positions[offset + 2]);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
const bounds = computeBounds(areaPositions);
|
|
3346
|
+
return {
|
|
3347
|
+
min: bounds.minBounds,
|
|
3348
|
+
max: bounds.maxBounds
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
function hydrateCmf(root, classes, hydrationOptions = {}) {
|
|
3352
|
+
const hydrationClasses = createHydrationClasses(classes, hydrationOptions);
|
|
3353
|
+
return hydrate("Root", {
|
|
3354
|
+
...root,
|
|
3355
|
+
metadata: root.metadata ? hydrateCmfMetadata(root.metadata, hydrationClasses) : null,
|
|
3356
|
+
meshes: root.meshes.map(mesh => hydrateCmfMesh(mesh, hydrationClasses)),
|
|
3357
|
+
skeletons: root.skeletons.map(skeleton => hydrateCmfSkeleton(skeleton, hydrationClasses)),
|
|
3358
|
+
animations: root.animations.map(animation => hydrateCmfAnimation(animation, hydrationClasses))
|
|
3359
|
+
}, hydrationClasses, hydrationOptions);
|
|
3360
|
+
}
|
|
3361
|
+
function hydrateCmfMetadata(metadata, classes) {
|
|
3362
|
+
return hydrate("Metadata", {
|
|
3363
|
+
entries: (metadata.entries ?? []).map(entry => hydrate("MetadataEntry", entry, classes))
|
|
3364
|
+
}, classes);
|
|
3365
|
+
}
|
|
3366
|
+
function hydrateCmfMesh(mesh, classes) {
|
|
3367
|
+
return hydrate("Mesh", {
|
|
3368
|
+
...mesh,
|
|
3369
|
+
decl: mesh.decl.map(element => hydrate("VertexElement", element, classes)),
|
|
3370
|
+
lods: mesh.lods.map(lod => hydrate("MeshLod", {
|
|
3371
|
+
...lod,
|
|
3372
|
+
areas: lod.areas.map(area => hydrate("LodMeshArea", area, classes)),
|
|
3373
|
+
morphTargets: lod.morphTargets.map(target => hydrate("LodMorphTarget", target, classes))
|
|
3374
|
+
}, classes)),
|
|
3375
|
+
areas: mesh.areas.map(area => hydrate("MeshArea", area, classes)),
|
|
3376
|
+
boneBindings: mesh.boneBindings.map(binding => hydrate("BoneBinding", binding, classes)),
|
|
3377
|
+
morphTargets: hydrate("MorphTargets", {
|
|
3378
|
+
decl: mesh.morphTargets.decl.map(element => hydrate("VertexElement", element, classes)),
|
|
3379
|
+
targets: mesh.morphTargets.targets.map(target => hydrate("MorphTarget", target, classes))
|
|
3380
|
+
}, classes),
|
|
3381
|
+
audioOcclusionMesh: hydrate("AudioOcclusionMesh", mesh.audioOcclusionMesh, classes)
|
|
3382
|
+
}, classes);
|
|
3383
|
+
}
|
|
3384
|
+
function hydrateCmfSkeleton(skeleton, classes) {
|
|
3385
|
+
return hydrate("Skeleton", {
|
|
3386
|
+
...skeleton,
|
|
3387
|
+
boneMasks: skeleton.boneMasks.map(mask => hydrate("BoneMask", {
|
|
3388
|
+
...mask,
|
|
3389
|
+
weights: mask.weights.map(weight => hydrate("BoneWeight", weight, classes))
|
|
3390
|
+
}, classes))
|
|
3391
|
+
}, classes);
|
|
3392
|
+
}
|
|
3393
|
+
function hydrateCmfAnimation(animation, classes) {
|
|
3394
|
+
return hydrate("Animation", {
|
|
3395
|
+
...animation,
|
|
3396
|
+
channels: animation.channels.map(channel => hydrate("AnimationChannel", channel, classes)),
|
|
3397
|
+
curves: animation.curves.map(curve => hydrate("AnimationCurve", curve, classes))
|
|
3398
|
+
}, classes);
|
|
3399
|
+
}
|
|
3400
|
+
function hydrate(type, fields, classes, hydrationOptions = {}) {
|
|
3401
|
+
const Class = classes?.[type];
|
|
3402
|
+
if (!Class) {
|
|
3403
|
+
return fields;
|
|
3404
|
+
}
|
|
3405
|
+
const options = Object.keys(hydrationOptions).length > 0 ? hydrationOptions : classes?.__hydrationOptions || {};
|
|
3406
|
+
return populate(new Class(), fields, options);
|
|
3407
|
+
}
|
|
3408
|
+
function populate(instance, fields, hydrationOptions = {}) {
|
|
3409
|
+
if (!instance || typeof instance.SetValues !== "function") {
|
|
3410
|
+
throw new TypeError("CjsFbxFormat class population requires classes to implement SetValues(values)");
|
|
3411
|
+
}
|
|
3412
|
+
instance.SetValues(fields, {
|
|
3413
|
+
...hydrationOptions,
|
|
3414
|
+
skipUpdate: true,
|
|
3415
|
+
skipEvents: true
|
|
3416
|
+
});
|
|
3417
|
+
return instance;
|
|
3418
|
+
}
|
|
3419
|
+
function createHydrationClasses(classes, hydrationOptions) {
|
|
3420
|
+
const map = Object.create(classes || null);
|
|
3421
|
+
Object.defineProperty(map, "__hydrationOptions", {
|
|
3422
|
+
value: hydrationOptions,
|
|
3423
|
+
enumerable: false
|
|
3424
|
+
});
|
|
3425
|
+
return map;
|
|
3426
|
+
}
|
|
3427
|
+
function buildFbxRoot(nodes) {
|
|
3428
|
+
const headerNode = findFirstNode(nodes, "FBXHeaderExtension");
|
|
3429
|
+
const globalSettingsNode = findFirstNode(nodes, "GlobalSettings");
|
|
3430
|
+
const definitionsNode = findFirstNode(nodes, "Definitions");
|
|
3431
|
+
const objectsNode = findFirstNode(nodes, "Objects");
|
|
3432
|
+
const connectionsNode = findFirstNode(nodes, "Connections");
|
|
3433
|
+
return {
|
|
3434
|
+
rootNodeNames: nodes.map(node => node.name),
|
|
3435
|
+
header: readHeader(headerNode),
|
|
3436
|
+
globalSettings: readProperties70(globalSettingsNode),
|
|
3437
|
+
definitions: readDefinitions(definitionsNode),
|
|
3438
|
+
objects: readObjects(objectsNode),
|
|
3439
|
+
connections: readConnections(connectionsNode)
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
function readHeader(node) {
|
|
3443
|
+
if (!node) {
|
|
3444
|
+
return {};
|
|
3445
|
+
}
|
|
3446
|
+
return {
|
|
3447
|
+
fbxHeaderVersion: readFirstChildProperty(node, "FBXHeaderVersion"),
|
|
3448
|
+
fbxVersion: readFirstChildProperty(node, "FBXVersion"),
|
|
3449
|
+
creator: readFirstChildProperty(node, "Creator")
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
function readDefinitions(node) {
|
|
3453
|
+
if (!node) {
|
|
3454
|
+
return {
|
|
3455
|
+
version: 0,
|
|
3456
|
+
count: 0,
|
|
3457
|
+
objectTypes: []
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
return {
|
|
3461
|
+
version: readFirstChildProperty(node, "Version") || 0,
|
|
3462
|
+
count: readFirstChildProperty(node, "Count") || 0,
|
|
3463
|
+
objectTypes: node.children.filter(child => child.name === "ObjectType").map(readObjectType)
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
function readObjectType(node) {
|
|
3467
|
+
return {
|
|
3468
|
+
type: node.properties[0] || "",
|
|
3469
|
+
count: readFirstChildProperty(node, "Count") || 0
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
function readObjects(node) {
|
|
3473
|
+
const byId = {};
|
|
3474
|
+
const byType = {};
|
|
3475
|
+
const list = [];
|
|
3476
|
+
if (!node) {
|
|
3477
|
+
return {
|
|
3478
|
+
count: 0,
|
|
3479
|
+
list,
|
|
3480
|
+
byId,
|
|
3481
|
+
byType
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
for (const child of node.children) {
|
|
3485
|
+
if (child.properties.length === 0) {
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3488
|
+
const entry = readObjectEntry(child);
|
|
3489
|
+
list.push(entry);
|
|
3490
|
+
byId[entry.key] = entry;
|
|
3491
|
+
if (!byType[entry.nodeName]) {
|
|
3492
|
+
byType[entry.nodeName] = [];
|
|
3493
|
+
}
|
|
3494
|
+
byType[entry.nodeName].push(entry.key);
|
|
3495
|
+
}
|
|
3496
|
+
return {
|
|
3497
|
+
count: list.length,
|
|
3498
|
+
list,
|
|
3499
|
+
byId,
|
|
3500
|
+
byType
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
function readObjectEntry(node) {
|
|
3504
|
+
const id = node.properties[0];
|
|
3505
|
+
const fullName = node.properties[1] || "";
|
|
3506
|
+
const className = node.properties[2] || "";
|
|
3507
|
+
return {
|
|
3508
|
+
id,
|
|
3509
|
+
key: toObjectKey(id),
|
|
3510
|
+
nodeName: node.name,
|
|
3511
|
+
name: getShortObjectName(fullName),
|
|
3512
|
+
fullName,
|
|
3513
|
+
className,
|
|
3514
|
+
propertyCount: node.properties.length,
|
|
3515
|
+
childCount: node.children.length,
|
|
3516
|
+
attributeNames: node.children.map(child => child.name)
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
function readConnections(node) {
|
|
3520
|
+
const list = [];
|
|
3521
|
+
const parentsByChild = {};
|
|
3522
|
+
const childrenByParent = {};
|
|
3523
|
+
if (node) {
|
|
3524
|
+
for (const child of node.children) {
|
|
3525
|
+
if (child.name !== "C") {
|
|
3526
|
+
continue;
|
|
3527
|
+
}
|
|
3528
|
+
const entry = {
|
|
3529
|
+
relation: child.properties[0] || "",
|
|
3530
|
+
childId: child.properties[1],
|
|
3531
|
+
parentId: child.properties[2],
|
|
3532
|
+
property: child.properties[3] || ""
|
|
3533
|
+
};
|
|
3534
|
+
entry.childKey = toObjectKey(entry.childId);
|
|
3535
|
+
entry.parentKey = toObjectKey(entry.parentId);
|
|
3536
|
+
list.push(entry);
|
|
3537
|
+
pushIndex(parentsByChild, entry.childKey, entry.parentKey);
|
|
3538
|
+
pushIndex(childrenByParent, entry.parentKey, entry.childKey);
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
return {
|
|
3542
|
+
count: list.length,
|
|
3543
|
+
list,
|
|
3544
|
+
parentsByChild,
|
|
3545
|
+
childrenByParent
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
function readProperties70(node) {
|
|
3549
|
+
const properties = {};
|
|
3550
|
+
const properties70 = node ? findFirstNode(node.children, "Properties70") : null;
|
|
3551
|
+
if (!properties70) {
|
|
3552
|
+
return {
|
|
3553
|
+
properties
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
for (const child of properties70.children) {
|
|
3557
|
+
if (child.name !== "P" || child.properties.length === 0) {
|
|
3558
|
+
continue;
|
|
3559
|
+
}
|
|
3560
|
+
const key = child.properties[0];
|
|
3561
|
+
properties[key] = {
|
|
3562
|
+
type: child.properties[1] || "",
|
|
3563
|
+
label: child.properties[2] || "",
|
|
3564
|
+
flags: child.properties[3] || "",
|
|
3565
|
+
value: getProperty70Value(child.properties)
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
return {
|
|
3569
|
+
properties
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
function getProperty70Value(properties) {
|
|
3573
|
+
if (properties.length <= 5) {
|
|
3574
|
+
return properties[4];
|
|
3575
|
+
}
|
|
3576
|
+
return properties.slice(4);
|
|
3577
|
+
}
|
|
3578
|
+
function readFirstChildProperty(node, childName) {
|
|
3579
|
+
const child = findFirstNode(node.children, childName);
|
|
3580
|
+
return child ? child.properties[0] : undefined;
|
|
3581
|
+
}
|
|
3582
|
+
function findFirstNode(nodes, name) {
|
|
3583
|
+
return nodes.find(node => node.name === name) || null;
|
|
3584
|
+
}
|
|
3585
|
+
function getShortObjectName(fullName) {
|
|
3586
|
+
if (typeof fullName !== "string") {
|
|
3587
|
+
return "";
|
|
3588
|
+
}
|
|
3589
|
+
const fbxClassMarker = fullName.indexOf("\u0000\u0001"),
|
|
3590
|
+
objectName = fbxClassMarker === -1 ? fullName : fullName.slice(0, fbxClassMarker),
|
|
3591
|
+
namespaceMarker = objectName.lastIndexOf("::");
|
|
3592
|
+
return namespaceMarker === -1 ? objectName : objectName.slice(namespaceMarker + 2);
|
|
3593
|
+
}
|
|
3594
|
+
function pushIndex(index, key, value) {
|
|
3595
|
+
if (!index[key]) {
|
|
3596
|
+
index[key] = [];
|
|
3597
|
+
}
|
|
3598
|
+
index[key].push(value);
|
|
3599
|
+
}
|
|
3600
|
+
function toObjectKey(value) {
|
|
3601
|
+
return value === undefined || value === null ? "" : String(value);
|
|
3602
|
+
}
|
|
3603
|
+
function parseAsciiLine(line, values) {
|
|
3604
|
+
const match = /^([A-Za-z_][\w.]*)\s*:\s*(.*)$/u.exec(line);
|
|
3605
|
+
if (!match) {
|
|
3606
|
+
return null;
|
|
3607
|
+
}
|
|
3608
|
+
let body = match[2].trim();
|
|
3609
|
+
const opensBlock = body.endsWith("{");
|
|
3610
|
+
if (opensBlock) {
|
|
3611
|
+
body = body.slice(0, -1).trim();
|
|
3612
|
+
}
|
|
3613
|
+
const properties = body ? parseAsciiProperties(body) : [];
|
|
3614
|
+
if (properties.length > values.maxProperties) {
|
|
3615
|
+
throw new Error(`fbx: ASCII node ${match[1]} exceeds maxProperties (${values.maxProperties})`);
|
|
3616
|
+
}
|
|
3617
|
+
return {
|
|
3618
|
+
name: match[1],
|
|
3619
|
+
properties,
|
|
3620
|
+
propertyTypes: [],
|
|
3621
|
+
children: [],
|
|
3622
|
+
opensBlock
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
function parseAsciiProperties(body) {
|
|
3626
|
+
return splitAsciiProperties(body).map(parseAsciiValue);
|
|
3627
|
+
}
|
|
3628
|
+
function splitAsciiProperties(body) {
|
|
3629
|
+
const parts = [];
|
|
3630
|
+
let current = "";
|
|
3631
|
+
let quoted = false;
|
|
3632
|
+
let escaped = false;
|
|
3633
|
+
for (let i = 0; i < body.length; i++) {
|
|
3634
|
+
const char = body[i];
|
|
3635
|
+
if (escaped) {
|
|
3636
|
+
current += char;
|
|
3637
|
+
escaped = false;
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
if (char === "\\") {
|
|
3641
|
+
current += char;
|
|
3642
|
+
escaped = true;
|
|
3643
|
+
continue;
|
|
3644
|
+
}
|
|
3645
|
+
if (char === "\"") {
|
|
3646
|
+
quoted = !quoted;
|
|
3647
|
+
current += char;
|
|
3648
|
+
continue;
|
|
3649
|
+
}
|
|
3650
|
+
if (char === "," && !quoted) {
|
|
3651
|
+
parts.push(current.trim());
|
|
3652
|
+
current = "";
|
|
3653
|
+
continue;
|
|
3654
|
+
}
|
|
3655
|
+
current += char;
|
|
3656
|
+
}
|
|
3657
|
+
if (current.trim() || body.endsWith(",")) {
|
|
3658
|
+
parts.push(current.trim());
|
|
3659
|
+
}
|
|
3660
|
+
return parts;
|
|
3661
|
+
}
|
|
3662
|
+
function parseAsciiValue(value) {
|
|
3663
|
+
if (/^".*"$/u.test(value)) {
|
|
3664
|
+
return value.slice(1, -1).replace(/\\"/gu, "\"").replace(/\\\\/gu, "\\");
|
|
3665
|
+
}
|
|
3666
|
+
if (/^(true|false)$/iu.test(value)) {
|
|
3667
|
+
return value.toLowerCase() === "true";
|
|
3668
|
+
}
|
|
3669
|
+
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/iu.test(value)) {
|
|
3670
|
+
return Number(value);
|
|
3671
|
+
}
|
|
3672
|
+
return value;
|
|
3673
|
+
}
|
|
3674
|
+
function stripAsciiComment(line) {
|
|
3675
|
+
let quoted = false;
|
|
3676
|
+
let escaped = false;
|
|
3677
|
+
for (let i = 0; i < line.length; i++) {
|
|
3678
|
+
const char = line[i];
|
|
3679
|
+
if (escaped) {
|
|
3680
|
+
escaped = false;
|
|
3681
|
+
continue;
|
|
3682
|
+
}
|
|
3683
|
+
if (char === "\\") {
|
|
3684
|
+
escaped = true;
|
|
3685
|
+
continue;
|
|
3686
|
+
}
|
|
3687
|
+
if (char === "\"") {
|
|
3688
|
+
quoted = !quoted;
|
|
3689
|
+
continue;
|
|
3690
|
+
}
|
|
3691
|
+
if (char === ";" && !quoted) {
|
|
3692
|
+
return line.slice(0, i);
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
return line;
|
|
3696
|
+
}
|
|
3697
|
+
function isBinaryFBX(bytes) {
|
|
3698
|
+
if (bytes.byteLength < BINARY_HEADER_SIZE) {
|
|
3699
|
+
return false;
|
|
3700
|
+
}
|
|
3701
|
+
for (let i = 0; i < BINARY_SIGNATURE.length; i++) {
|
|
3702
|
+
if (bytes[i] !== BINARY_SIGNATURE.charCodeAt(i)) {
|
|
3703
|
+
return false;
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
return true;
|
|
3707
|
+
}
|
|
3708
|
+
function isAsciiFBXText(text) {
|
|
3709
|
+
return /(^|\n)\s*FBXHeaderExtension\s*:/u.test(text) || /(^|\n)\s*Objects\s*:/u.test(text) || /;\s*FBX\s+\d+\.\d+\.\d+/iu.test(text);
|
|
3710
|
+
}
|
|
3711
|
+
function countAsciiRootNodes(text) {
|
|
3712
|
+
return collectAsciiRootNames(text).length;
|
|
3713
|
+
}
|
|
3714
|
+
function collectAsciiRootNames(text) {
|
|
3715
|
+
const names = [];
|
|
3716
|
+
let depth = 0;
|
|
3717
|
+
for (const rawLine of text.split(/\r?\n/u)) {
|
|
3718
|
+
const line = stripAsciiComment(rawLine).trim();
|
|
3719
|
+
if (!line) {
|
|
3720
|
+
continue;
|
|
3721
|
+
}
|
|
3722
|
+
if (line.startsWith("}")) {
|
|
3723
|
+
depth = Math.max(depth - 1, 0);
|
|
3724
|
+
continue;
|
|
3725
|
+
}
|
|
3726
|
+
const match = /^([A-Za-z_][\w.]*)\s*:/u.exec(line);
|
|
3727
|
+
if (match && depth === 0) {
|
|
3728
|
+
names.push(match[1]);
|
|
3729
|
+
}
|
|
3730
|
+
if (line.endsWith("{")) {
|
|
3731
|
+
depth++;
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
return names;
|
|
3735
|
+
}
|
|
3736
|
+
function readAsciiVersion(text) {
|
|
3737
|
+
const direct = /FBXVersion\s*:\s*(\d+)/u.exec(text);
|
|
3738
|
+
if (direct) {
|
|
3739
|
+
return Number(direct[1]);
|
|
3740
|
+
}
|
|
3741
|
+
const comment = /;\s*FBX\s+(\d+)\.(\d+)\.(\d+)/iu.exec(text);
|
|
3742
|
+
if (!comment) {
|
|
3743
|
+
return 0;
|
|
3744
|
+
}
|
|
3745
|
+
return Number(`${comment[1]}${comment[2].padStart(2, "0")}${comment[3].padStart(2, "0")}`);
|
|
3746
|
+
}
|
|
3747
|
+
function decodeAsciiPrefix(bytes, length) {
|
|
3748
|
+
let text = "";
|
|
3749
|
+
const count = Math.min(bytes.byteLength, length);
|
|
3750
|
+
for (let i = 0; i < count; i++) {
|
|
3751
|
+
const byte = bytes[i];
|
|
3752
|
+
if (byte === 0) {
|
|
3753
|
+
break;
|
|
3754
|
+
}
|
|
3755
|
+
text += String.fromCharCode(byte);
|
|
3756
|
+
}
|
|
3757
|
+
return text;
|
|
3758
|
+
}
|
|
3759
|
+
function getBinaryNodeHeaderSize(version) {
|
|
3760
|
+
return version >= 7500 ? NODE_HEADER_64_SIZE : NODE_HEADER_32_SIZE;
|
|
3761
|
+
}
|
|
3762
|
+
function isNullRecord(bytes, offset, length) {
|
|
3763
|
+
if (offset + length > bytes.byteLength) {
|
|
3764
|
+
return false;
|
|
3765
|
+
}
|
|
3766
|
+
for (let i = 0; i < length; i++) {
|
|
3767
|
+
if (bytes[offset + i] !== 0) {
|
|
3768
|
+
return false;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
return true;
|
|
3772
|
+
}
|
|
3773
|
+
function getArrayElementSize(type) {
|
|
3774
|
+
if (type === "d" || type === "l") {
|
|
3775
|
+
return 8;
|
|
3776
|
+
}
|
|
3777
|
+
if (type === "f" || type === "i") {
|
|
3778
|
+
return 4;
|
|
3779
|
+
}
|
|
3780
|
+
return 1;
|
|
3781
|
+
}
|
|
3782
|
+
function normalizeIntegerLimit(value, fieldName, readerName) {
|
|
3783
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
3784
|
+
throw new TypeError(`${readerName}: ${fieldName} must be a positive integer`);
|
|
3785
|
+
}
|
|
3786
|
+
return value;
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
/**
|
|
3790
|
+
* Normalize a boolean reader option.
|
|
3791
|
+
*
|
|
3792
|
+
* @param {boolean|0|1} value Option value.
|
|
3793
|
+
* @param {string} fieldName Option name.
|
|
3794
|
+
* @param {string} readerName Reader name for errors.
|
|
3795
|
+
* @returns {boolean} Normalized boolean.
|
|
3796
|
+
*/
|
|
3797
|
+
function normalizeBooleanOption(value, fieldName, readerName) {
|
|
3798
|
+
if (typeof value === "boolean") {
|
|
3799
|
+
return value;
|
|
3800
|
+
}
|
|
3801
|
+
if (value === 0 || value === 1) {
|
|
3802
|
+
return Boolean(value);
|
|
3803
|
+
}
|
|
3804
|
+
throw new TypeError(`${readerName}: ${fieldName} must be a boolean`);
|
|
3805
|
+
}
|
|
3806
|
+
function hasOwn(value, key) {
|
|
3807
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
3808
|
+
}
|
|
3809
|
+
function normalizeClasses(classes, readerName) {
|
|
3810
|
+
if (!classes) {
|
|
3811
|
+
return {};
|
|
3812
|
+
}
|
|
3813
|
+
if (typeof classes !== "object" || Array.isArray(classes)) {
|
|
3814
|
+
throw new TypeError(`${readerName}: classes must be an object map`);
|
|
3815
|
+
}
|
|
3816
|
+
const normalized = {};
|
|
3817
|
+
for (const [key, Class] of Object.entries(classes)) {
|
|
3818
|
+
validateClass(key, Class, readerName);
|
|
3819
|
+
normalized[key] = Class;
|
|
3820
|
+
}
|
|
3821
|
+
return normalized;
|
|
3822
|
+
}
|
|
3823
|
+
function mergeClasses(base, classes, readerName) {
|
|
3824
|
+
if (!classes || typeof classes !== "object" || Array.isArray(classes)) {
|
|
3825
|
+
throw new TypeError(`${readerName}: classes must be an object map`);
|
|
3826
|
+
}
|
|
3827
|
+
const normalized = normalizeClasses(base, readerName);
|
|
3828
|
+
for (const [key, Class] of Object.entries(classes)) {
|
|
3829
|
+
if (Class === null || Class === undefined) {
|
|
3830
|
+
validateClassKey(key, readerName);
|
|
3831
|
+
delete normalized[key];
|
|
3832
|
+
continue;
|
|
3833
|
+
}
|
|
3834
|
+
validateClass(key, Class, readerName);
|
|
3835
|
+
normalized[key] = Class;
|
|
3836
|
+
}
|
|
3837
|
+
return normalized;
|
|
3838
|
+
}
|
|
3839
|
+
function hasClasses(classes) {
|
|
3840
|
+
return !!classes && Object.values(classes).some(Class => typeof Class === "function");
|
|
3841
|
+
}
|
|
3842
|
+
function finiteNumber(value, feature) {
|
|
3843
|
+
const number = Number(value);
|
|
3844
|
+
if (!Number.isFinite(number)) {
|
|
3845
|
+
throw new Error(`fbx: ${feature} contains a non-finite number`);
|
|
3846
|
+
}
|
|
3847
|
+
return number;
|
|
3848
|
+
}
|
|
3849
|
+
function integerNumber(value, feature) {
|
|
3850
|
+
const number = finiteNumber(value, feature);
|
|
3851
|
+
if (!Number.isInteger(number)) {
|
|
3852
|
+
throw new Error(`fbx: ${feature} contains a non-integer value`);
|
|
3853
|
+
}
|
|
3854
|
+
return number;
|
|
3855
|
+
}
|
|
3856
|
+
function ensureRange(bytes, offset, length, message) {
|
|
3857
|
+
if (offset + length > bytes.byteLength) {
|
|
3858
|
+
throw new Error(message);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
function readString(bytes, offset, length) {
|
|
3862
|
+
ensureRange(bytes, offset, length, "fbx: string is truncated");
|
|
3863
|
+
return TEXT_DECODER.decode(bytes.subarray(offset, offset + length));
|
|
3864
|
+
}
|
|
3865
|
+
function readU32LE(bytes, offset) {
|
|
3866
|
+
return (bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] * 0x1000000) >>> 0;
|
|
3867
|
+
}
|
|
3868
|
+
function readU32BE(bytes, offset) {
|
|
3869
|
+
return (bytes[offset] << 24 >>> 0 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
|
|
3870
|
+
}
|
|
3871
|
+
function readI16LE(bytes, offset) {
|
|
3872
|
+
const value = bytes[offset] | bytes[offset + 1] << 8;
|
|
3873
|
+
return value & 0x8000 ? value - 0x10000 : value;
|
|
3874
|
+
}
|
|
3875
|
+
function readI32LE(bytes, offset) {
|
|
3876
|
+
return bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24;
|
|
3877
|
+
}
|
|
3878
|
+
function readU64LEAsNumber(bytes, offset) {
|
|
3879
|
+
const low = readU32LE(bytes, offset);
|
|
3880
|
+
const high = readU32LE(bytes, offset + 4);
|
|
3881
|
+
return high * 0x100000000 + low;
|
|
3882
|
+
}
|
|
3883
|
+
function readI64LEValue(bytes, offset) {
|
|
3884
|
+
const low = readU32LE(bytes, offset);
|
|
3885
|
+
const highUnsigned = readU32LE(bytes, offset + 4);
|
|
3886
|
+
const negative = !!(highUnsigned & 0x80000000);
|
|
3887
|
+
let value = highUnsigned * 0x100000000 + low;
|
|
3888
|
+
if (negative) {
|
|
3889
|
+
value -= 0x10000000000000000;
|
|
3890
|
+
}
|
|
3891
|
+
return Number.isSafeInteger(value) ? value : String(value);
|
|
3892
|
+
}
|
|
3893
|
+
function readF32LE(bytes, offset) {
|
|
3894
|
+
return new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getFloat32(0, true);
|
|
3895
|
+
}
|
|
3896
|
+
function readF64LE(bytes, offset) {
|
|
3897
|
+
return new DataView(bytes.buffer, bytes.byteOffset + offset, 8).getFloat64(0, true);
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
export { CLASS_KEYS, CMF_CLASS_KEYS, DEFAULT_VALUES, GR2_CLASS_KEYS, OUTPUT_CMF, OUTPUT_FBX_JSON, OUTPUT_GR2, OUTPUT_JSON, OUTPUT_RAW, inspectBytes, inspectWithValues, isFBX, isSupportedWithValues, normalizeEmit, normalizeValues, parseWithValues, readCmfWithValues, readGr2WithValues, readWithValues, toBytes, toJsonValue, validateClass, validateClassKey };
|
|
3901
|
+
//# sourceMappingURL=helpers.js.map
|