@haiyue/extensions 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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/animation/Animation2DComponent.d.ts +46 -0
- package/dist/animation/Animation2DExtensionRegistry.d.ts +27 -0
- package/dist/animation/Animation2DRenderSystem.d.ts +89 -0
- package/dist/animation/Animation2DRuntime.d.ts +99 -0
- package/dist/animation/Animation2DStateMachine.d.ts +13 -0
- package/dist/animation/Animation2DStateMachineComponent.d.ts +54 -0
- package/dist/animation/Animation2DStateMachineRuntime.d.ts +30 -0
- package/dist/animation/Animation2DStateMachineSystem.d.ts +14 -0
- package/dist/animation/Animation2DStateMachineVisualRuntime.d.ts +27 -0
- package/dist/animation/Animation2DSystem.d.ts +14 -0
- package/dist/animation/AnimationAssetLoader.d.ts +9 -0
- package/dist/animation/AnimationAudioClip.d.ts +26 -0
- package/dist/animation/AnimationPathTessellator.d.ts +52 -0
- package/dist/animation/AnimationTextRasterizer.d.ts +21 -0
- package/dist/animation/AnimationVisual2D.d.ts +68 -0
- package/dist/animation/HyaAnimation2DClipAdapter.d.ts +16 -0
- package/dist/animation/Particle2DDescriptorAdapter.d.ts +5 -0
- package/dist/animation/runtime/mixer/Action.d.ts +150 -0
- package/dist/animation/runtime/mixer/Mixer.d.ts +54 -0
- package/dist/animation/runtime/mixer/PoseBuffer.d.ts +23 -0
- package/dist/animation/runtime/mixer/Sampler.d.ts +17 -0
- package/dist/animation/runtime/mixer/Types.d.ts +101 -0
- package/dist/animation/runtime/mixer/index.d.ts +6 -0
- package/dist/animation-state-machine/AnimationStateMachine.d.ts +111 -0
- package/dist/animation-state-machine/AnimationStateMachineChannels.d.ts +23 -0
- package/dist/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.d.ts +51 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.d.ts +14 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineCompiler.d.ts +109 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineController.d.ts +103 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineMixerPort.d.ts +35 -0
- package/dist/animation-state-machine/runtime/index.d.ts +7 -0
- package/dist/animation.d.ts +15 -0
- package/dist/animation.js +1171 -0
- package/dist/animation3d/Animation3DAction.d.ts +46 -0
- package/dist/animation3d/Animation3DBinding.d.ts +85 -0
- package/dist/animation3d/Animation3DClip.d.ts +20 -0
- package/dist/animation3d/Animation3DError.d.ts +13 -0
- package/dist/animation3d/Animation3DMixer.d.ts +27 -0
- package/dist/animation3d/Animation3DPose.d.ts +40 -0
- package/dist/animation3d/Animation3DPoseBuffer.d.ts +24 -0
- package/dist/animation3d/Animation3DResource.d.ts +28 -0
- package/dist/animation3d/Animation3DStateMachine.d.ts +95 -0
- package/dist/animation3d/Animation3DStateMachineRuntime.d.ts +70 -0
- package/dist/animation3d/Animation3DTrack.d.ts +19 -0
- package/dist/animation3d/hya/HyaAnimation3DRuntime.d.ts +73 -0
- package/dist/animation3d/hya/HyaAnimation3DStateMachineRuntime.d.ts +65 -0
- package/dist/animation3d/hya/HyaAnimation3DTypes.d.ts +170 -0
- package/dist/animation3d/hya/index.d.ts +4 -0
- package/dist/animation3d/index.d.ts +14 -0
- package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.d.ts +81 -0
- package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.d.ts +39 -0
- package/dist/animation3d/runtime/integration/index.d.ts +2 -0
- package/dist/animation3d/runtime/mixer/Action.d.ts +150 -0
- package/dist/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.d.ts +6 -0
- package/dist/animation3d/runtime/mixer/Mixer.d.ts +59 -0
- package/dist/animation3d/runtime/mixer/PoseBuffer.d.ts +1 -0
- package/dist/animation3d/runtime/mixer/TrackSampler.d.ts +24 -0
- package/dist/animation3d/runtime/mixer/index.d.ts +4 -0
- package/dist/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineController.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/index.d.ts +6 -0
- package/dist/animation3d.d.ts +5 -0
- package/dist/animation3d.js +1708 -0
- package/dist/benchmark.d.ts +30 -0
- package/dist/benchmark.js +119 -0
- package/dist/canvas-text/CanvasText2DRenderSystem.d.ts +24 -0
- package/dist/canvas-text/CanvasTextComponent.d.ts +18 -0
- package/dist/canvas-text.d.ts +4 -0
- package/dist/canvas-text.js +276 -0
- package/dist/chunks/2d-ui-animation-2d.generated-D4aHzFU2.js +4 -0
- package/dist/chunks/2d-ui-canvas-text-2d.generated-C0Eaej2m.js +4 -0
- package/dist/chunks/2d-ui-spine2d.generated-CUUi8xjl.js +4 -0
- package/dist/chunks/2d-ui-tilemap2d.generated-gebw4mX7.js +4 -0
- package/dist/chunks/Animation2DStateMachine-9IonguNa.js +3245 -0
- package/dist/chunks/AnimationStateMachineController-BIcAh-sT.js +1438 -0
- package/dist/chunks/GltfAnimation3DAdapter-Cu3rQQsW.js +2240 -0
- package/dist/chunks/GltfAnimationRuntime-CRHsLycN.js +631 -0
- package/dist/chunks/RenderSystem2DBase-DDT61dAu.js +227 -0
- package/dist/chunks/Spine2DRuntime-D9M9LAQR.js +3056 -0
- package/dist/chunks/SpineAssetParser-BSDmPge1.js +378 -0
- package/dist/chunks/gltfLoader-Dm22jLYB.js +2085 -0
- package/dist/chunks/pluginUtils-CvwroRMd.js +33 -0
- package/dist/experimental-gltf-worker.d.ts +4 -0
- package/dist/experimental-gltf-worker.js +138 -0
- package/dist/experimental-spine-worker.d.ts +4 -0
- package/dist/experimental-spine-worker.js +110 -0
- package/dist/gltf/GltfAccessorReader.d.ts +7 -0
- package/dist/gltf/GltfAnimation3DAdapter.d.ts +47 -0
- package/dist/gltf/GltfAnimationRuntime.d.ts +10 -0
- package/dist/gltf/GltfAssetWorkerClient.d.ts +20 -0
- package/dist/gltf/GltfCompatibilityReport.d.ts +70 -0
- package/dist/gltf/GltfConservativeBounds.d.ts +18 -0
- package/dist/gltf/GltfDracoDecoder.d.ts +4 -0
- package/dist/gltf/GltfExtensionAdapter.d.ts +54 -0
- package/dist/gltf/GltfLoaderContract.d.ts +249 -0
- package/dist/gltf/GltfLoaderErrors.d.ts +4 -0
- package/dist/gltf/GltfMaterialDescriptor.d.ts +23 -0
- package/dist/gltf/GltfMaterialEncoder.d.ts +20 -0
- package/dist/gltf/GltfMaterialLoader.d.ts +6 -0
- package/dist/gltf/GltfModelComponent.d.ts +72 -0
- package/dist/gltf/GltfModelSystem.d.ts +37 -0
- package/dist/gltf/GltfPlugin.d.ts +6 -0
- package/dist/gltf/GltfSchema.d.ts +225 -0
- package/dist/gltf/GltfUvSemanticPlanner.d.ts +33 -0
- package/dist/gltf/gltfLoader.d.ts +9 -0
- package/dist/gltf-animation3d.d.ts +3 -0
- package/dist/gltf-animation3d.js +8 -0
- package/dist/gltf-worker-runtime.d.ts +7 -0
- package/dist/gltf-worker-runtime.js +6981 -0
- package/dist/gltf.d.ts +12 -0
- package/dist/gltf.js +517 -0
- package/dist/grid/Grid2DComponent.d.ts +25 -0
- package/dist/grid.d.ts +2 -0
- package/dist/grid.js +56 -0
- package/dist/hya-state-machine.d.ts +7 -0
- package/dist/hya-state-machine.js +2934 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/internal/2d-ui-shader-artifact.js +870 -0
- package/dist/plugins/pluginUtils.d.ts +20 -0
- package/dist/plugins.d.ts +6 -0
- package/dist/shaders/generated/2d-ui-artifact.generated.d.ts +658 -0
- package/dist/spine/Spine2DComponent.d.ts +116 -0
- package/dist/spine/Spine2DGpuRenderer.d.ts +65 -0
- package/dist/spine/Spine2DRenderSystem.d.ts +27 -0
- package/dist/spine/Spine2DRuntime.d.ts +103 -0
- package/dist/spine/SpineAnimationBoneTimelines.d.ts +8 -0
- package/dist/spine/SpineAssetParser.d.ts +18 -0
- package/dist/spine/SpineAssetWorkerClient.d.ts +19 -0
- package/dist/spine/SpineAssetWorkerContract.d.ts +6 -0
- package/dist/spine/SpineAtlasParser.d.ts +21 -0
- package/dist/spine/SpineFloatBuilder.d.ts +15 -0
- package/dist/spine/SpinePathConstraintSolver.d.ts +32 -0
- package/dist/spine/SpinePlugin.d.ts +8 -0
- package/dist/spine/SpineSkeletonRuntime.d.ts +185 -0
- package/dist/spine/SpineSlotAnimation.d.ts +17 -0
- package/dist/spine/SpineTimelineRuntime.d.ts +25 -0
- package/dist/spine/SpineVertexBuilder.d.ts +61 -0
- package/dist/spine.d.ts +7 -0
- package/dist/spine.js +578 -0
- package/dist/tilemap/Tilemap2DComponent.d.ts +110 -0
- package/dist/tilemap/Tilemap2DRenderSystem.d.ts +11 -0
- package/dist/tilemap/Tilemap2DRenderer.d.ts +24 -0
- package/dist/tilemap/TilemapPlugin.d.ts +8 -0
- package/dist/tilemap.d.ts +6 -0
- package/dist/tilemap.js +535 -0
- package/dist/tween/Tween2DComponent.d.ts +70 -0
- package/dist/tween/Tween2DSystem.d.ts +12 -0
- package/dist/tween.d.ts +4 -0
- package/dist/tween.js +138 -0
- package/dist/utils/RenderSystem2DBase.d.ts +42 -0
- package/dist/utils/arrayAccess.d.ts +4 -0
- package/dist/utils/camera2d.d.ts +12 -0
- package/dist/utils/render2dGpu.d.ts +32 -0
- package/package.json +124 -0
- package/src/animation/Animation2DComponent.ts +135 -0
- package/src/animation/Animation2DExtensionRegistry.ts +52 -0
- package/src/animation/Animation2DRenderSystem.ts +1026 -0
- package/src/animation/Animation2DRuntime.ts +1326 -0
- package/src/animation/Animation2DStateMachine.ts +39 -0
- package/src/animation/Animation2DStateMachineComponent.ts +228 -0
- package/src/animation/Animation2DStateMachineRuntime.ts +141 -0
- package/src/animation/Animation2DStateMachineSystem.ts +60 -0
- package/src/animation/Animation2DStateMachineVisualRuntime.ts +282 -0
- package/src/animation/Animation2DSystem.ts +72 -0
- package/src/animation/AnimationAssetLoader.ts +84 -0
- package/src/animation/AnimationAudioClip.ts +140 -0
- package/src/animation/AnimationPathTessellator.ts +691 -0
- package/src/animation/AnimationTextRasterizer.ts +340 -0
- package/src/animation/AnimationVisual2D.ts +153 -0
- package/src/animation/HyaAnimation2DClipAdapter.ts +845 -0
- package/src/animation/Particle2DDescriptorAdapter.ts +35 -0
- package/src/animation/runtime/mixer/Action.ts +876 -0
- package/src/animation/runtime/mixer/Mixer.ts +505 -0
- package/src/animation/runtime/mixer/PoseBuffer.ts +105 -0
- package/src/animation/runtime/mixer/Sampler.ts +252 -0
- package/src/animation/runtime/mixer/Types.ts +138 -0
- package/src/animation/runtime/mixer/index.ts +31 -0
- package/src/animation-state-machine/AnimationStateMachine.ts +146 -0
- package/src/animation-state-machine/AnimationStateMachineChannels.ts +116 -0
- package/src/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.ts +197 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.ts +503 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineCompiler.ts +763 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineController.ts +987 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineMixerPort.ts +54 -0
- package/src/animation-state-machine/runtime/index.ts +47 -0
- package/src/animation.ts +33 -0
- package/src/animation3d/Animation3DAction.ts +73 -0
- package/src/animation3d/Animation3DBinding.ts +116 -0
- package/src/animation3d/Animation3DClip.ts +22 -0
- package/src/animation3d/Animation3DError.ts +34 -0
- package/src/animation3d/Animation3DMixer.ts +82 -0
- package/src/animation3d/Animation3DPose.ts +100 -0
- package/src/animation3d/Animation3DPoseBuffer.ts +85 -0
- package/src/animation3d/Animation3DResource.ts +49 -0
- package/src/animation3d/Animation3DStateMachine.ts +126 -0
- package/src/animation3d/Animation3DStateMachineRuntime.ts +191 -0
- package/src/animation3d/Animation3DTrack.ts +29 -0
- package/src/animation3d/hya/HyaAnimation3DRuntime.ts +756 -0
- package/src/animation3d/hya/HyaAnimation3DStateMachineRuntime.ts +618 -0
- package/src/animation3d/hya/HyaAnimation3DTypes.ts +168 -0
- package/src/animation3d/hya/index.ts +10 -0
- package/src/animation3d/index.ts +76 -0
- package/src/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.ts +325 -0
- package/src/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.ts +158 -0
- package/src/animation3d/runtime/integration/index.ts +9 -0
- package/src/animation3d/runtime/mixer/Action.ts +833 -0
- package/src/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.ts +26 -0
- package/src/animation3d/runtime/mixer/Mixer.ts +562 -0
- package/src/animation3d/runtime/mixer/PoseBuffer.ts +1 -0
- package/src/animation3d/runtime/mixer/TrackSampler.ts +399 -0
- package/src/animation3d/runtime/mixer/index.ts +8 -0
- package/src/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.ts +4 -0
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.ts +1 -0
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineController.ts +1 -0
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.ts +1 -0
- package/src/animation3d/runtime/state-machine/index.ts +45 -0
- package/src/animation3d.ts +5 -0
- package/src/benchmark.ts +168 -0
- package/src/canvas-text/CanvasText2DRenderSystem.ts +292 -0
- package/src/canvas-text/CanvasTextComponent.ts +47 -0
- package/src/canvas-text.ts +4 -0
- package/src/experimental-gltf-worker.ts +18 -0
- package/src/experimental-spine-worker.ts +9 -0
- package/src/gltf/GltfAccessorReader.ts +271 -0
- package/src/gltf/GltfAnimation3DAdapter.ts +680 -0
- package/src/gltf/GltfAnimationRuntime.ts +392 -0
- package/src/gltf/GltfAssetWorkerClient.ts +136 -0
- package/src/gltf/GltfCompatibilityReport.ts +214 -0
- package/src/gltf/GltfConservativeBounds.ts +219 -0
- package/src/gltf/GltfDracoDecoder.ts +251 -0
- package/src/gltf/GltfExtensionAdapter.ts +340 -0
- package/src/gltf/GltfLoaderContract.ts +333 -0
- package/src/gltf/GltfLoaderErrors.ts +59 -0
- package/src/gltf/GltfMaterialDescriptor.ts +134 -0
- package/src/gltf/GltfMaterialEncoder.ts +173 -0
- package/src/gltf/GltfMaterialLoader.ts +293 -0
- package/src/gltf/GltfModelComponent.ts +115 -0
- package/src/gltf/GltfModelSystem.ts +208 -0
- package/src/gltf/GltfPlugin.ts +84 -0
- package/src/gltf/GltfSchema.ts +229 -0
- package/src/gltf/GltfUvSemanticPlanner.ts +166 -0
- package/src/gltf/gltfLoader.ts +910 -0
- package/src/gltf-animation3d.ts +10 -0
- package/src/gltf-worker-runtime.ts +10 -0
- package/src/gltf.ts +56 -0
- package/src/grid/Grid2DComponent.ts +69 -0
- package/src/grid.ts +2 -0
- package/src/hya-state-machine.ts +19 -0
- package/src/index.ts +2 -0
- package/src/plugins/pluginUtils.ts +50 -0
- package/src/plugins.ts +6 -0
- package/src/shaders/generated/2d-ui-animation-2d.generated.wgsl +242 -0
- package/src/shaders/generated/2d-ui-artifact.generated.ts +867 -0
- package/src/shaders/generated/2d-ui-canvas-text-2d.generated.wgsl +38 -0
- package/src/shaders/generated/2d-ui-spine2d.generated.wgsl +41 -0
- package/src/shaders/generated/2d-ui-tilemap2d.generated.wgsl +36 -0
- package/src/spine/Spine2DComponent.ts +168 -0
- package/src/spine/Spine2DGpuRenderer.ts +449 -0
- package/src/spine/Spine2DRenderSystem.ts +199 -0
- package/src/spine/Spine2DRuntime.ts +1529 -0
- package/src/spine/SpineAnimationBoneTimelines.ts +30 -0
- package/src/spine/SpineAssetParser.ts +20 -0
- package/src/spine/SpineAssetWorkerClient.ts +112 -0
- package/src/spine/SpineAssetWorkerContract.ts +4 -0
- package/src/spine/SpineAtlasParser.ts +127 -0
- package/src/spine/SpineFloatBuilder.ts +66 -0
- package/src/spine/SpinePathConstraintSolver.ts +582 -0
- package/src/spine/SpinePlugin.ts +76 -0
- package/src/spine/SpineSkeletonRuntime.ts +521 -0
- package/src/spine/SpineSlotAnimation.ts +195 -0
- package/src/spine/SpineTimelineRuntime.ts +453 -0
- package/src/spine/SpineVertexBuilder.ts +686 -0
- package/src/spine.ts +7 -0
- package/src/tilemap/Tilemap2DComponent.ts +232 -0
- package/src/tilemap/Tilemap2DRenderSystem.ts +58 -0
- package/src/tilemap/Tilemap2DRenderer.ts +289 -0
- package/src/tilemap/TilemapPlugin.ts +75 -0
- package/src/tilemap.ts +6 -0
- package/src/tween/Tween2DComponent.ts +83 -0
- package/src/tween/Tween2DSystem.ts +78 -0
- package/src/tween.ts +4 -0
- package/src/types/earcut.d.ts +7 -0
- package/src/types/wgsl.d.ts +4 -0
- package/src/utils/RenderSystem2DBase.ts +164 -0
- package/src/utils/arrayAccess.ts +17 -0
- package/src/utils/camera2d.ts +68 -0
- package/src/utils/render2dGpu.ts +132 -0
- package/test/fixtures/gltf/animation-characterization.gltf +344 -0
|
@@ -0,0 +1,2085 @@
|
|
|
1
|
+
import { ColorSRGB, Entity, CartesianTransform3D, Geometry3D, Mesh3D, PbrMaterial } from '@haiyue/engine';
|
|
2
|
+
import { Transform3D } from '@haiyue/engine/components';
|
|
3
|
+
import { monotonicNow, parseAssetWorkerFirst } from '@haiyue/engine/experimental/async';
|
|
4
|
+
import { mat4 } from 'wgpu-matrix';
|
|
5
|
+
import { PBR_COMPATIBILITY_CONTRACT, getPbrTextureFormat, createMaterialFromDescriptor } from '@haiyue/engine/material';
|
|
6
|
+
import { t as throwIfGltfLoadAborted, g as gltfDataError, d as attachGltfSource, e as createAnimationClips, r as readAccessorFloat, f as readAccessorIndices, h as generateFlatNormals, i as readAccessorUnsigned, b as applyMorphWeights, u as updateSkinnedPrimitive, c as composeTrsMatrix, j as registerGltfConservativeBounds, k as readAccessorMat4 } from './GltfAnimationRuntime-CRHsLycN.js';
|
|
7
|
+
|
|
8
|
+
/** Validates the untrusted JSON envelope before the loader follows references. */
|
|
9
|
+
function isGltfAsset(value) {
|
|
10
|
+
if (!isRecord(value))
|
|
11
|
+
return false;
|
|
12
|
+
if (!isRecord(value.asset) || typeof value.asset.version !== 'string' || !value.asset.version.startsWith('2'))
|
|
13
|
+
return false;
|
|
14
|
+
const arrayFields = [
|
|
15
|
+
'buffers', 'bufferViews', 'accessors', 'meshes', 'nodes', 'scenes', 'skins',
|
|
16
|
+
'materials', 'images', 'textures', 'samplers', 'animations',
|
|
17
|
+
];
|
|
18
|
+
if (arrayFields.some(field => value[field] !== undefined && !Array.isArray(value[field])))
|
|
19
|
+
return false;
|
|
20
|
+
if (value.scene !== undefined && !isNonNegativeInteger(value.scene))
|
|
21
|
+
return false;
|
|
22
|
+
if (value.extensionsUsed !== undefined && (!Array.isArray(value.extensionsUsed) || !value.extensionsUsed.every(item => typeof item === 'string')))
|
|
23
|
+
return false;
|
|
24
|
+
if (value.extensionsRequired !== undefined && (!Array.isArray(value.extensionsRequired) || !value.extensionsRequired.every(item => typeof item === 'string')))
|
|
25
|
+
return false;
|
|
26
|
+
if (!(value.buffers ?? []).every(buffer => isRecord(buffer)
|
|
27
|
+
&& (buffer.uri === undefined || typeof buffer.uri === 'string')
|
|
28
|
+
&& (buffer.byteLength === undefined || isNonNegativeInteger(buffer.byteLength))))
|
|
29
|
+
return false;
|
|
30
|
+
if (!(value.bufferViews ?? []).every(view => isRecord(view)
|
|
31
|
+
&& isNonNegativeInteger(view.buffer)
|
|
32
|
+
&& isNonNegativeInteger(view.byteLength)
|
|
33
|
+
&& (view.byteOffset === undefined || isNonNegativeInteger(view.byteOffset))))
|
|
34
|
+
return false;
|
|
35
|
+
if (!(value.accessors ?? []).every(accessor => isRecord(accessor)
|
|
36
|
+
&& isNonNegativeInteger(accessor.count)
|
|
37
|
+
&& typeof accessor.type === 'string'
|
|
38
|
+
&& isNonNegativeInteger(accessor.componentType)
|
|
39
|
+
&& isOptionalFiniteNumberArray(accessor.min)
|
|
40
|
+
&& isOptionalFiniteNumberArray(accessor.max)
|
|
41
|
+
&& (accessor.bufferView === undefined || isNonNegativeInteger(accessor.bufferView))))
|
|
42
|
+
return false;
|
|
43
|
+
if (!(value.meshes ?? []).every(mesh => isRecord(mesh)
|
|
44
|
+
&& Array.isArray(mesh.primitives)
|
|
45
|
+
&& mesh.primitives.every(primitive => isRecord(primitive) && isNumericRecord(primitive.attributes))))
|
|
46
|
+
return false;
|
|
47
|
+
if (!(value.nodes ?? []).every(isRecord))
|
|
48
|
+
return false;
|
|
49
|
+
if (!(value.scenes ?? []).every(scene => isRecord(scene)
|
|
50
|
+
&& (scene.nodes === undefined || (Array.isArray(scene.nodes) && scene.nodes.every(isNonNegativeInteger)))))
|
|
51
|
+
return false;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function isNonNegativeInteger(value) {
|
|
58
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
59
|
+
}
|
|
60
|
+
function isNumericRecord(value) {
|
|
61
|
+
return isRecord(value) && Object.values(value).every(isNonNegativeInteger);
|
|
62
|
+
}
|
|
63
|
+
function isOptionalFiniteNumberArray(value) {
|
|
64
|
+
return value === undefined || (Array.isArray(value) && value.every(item => typeof item === 'number' && Number.isFinite(item)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Keeps the accessor evidence needed to derive conservative bounds for the
|
|
69
|
+
* current morph weights and skin matrices. Missing evidence returns null so
|
|
70
|
+
* Render3D keeps its fail-open culling behavior.
|
|
71
|
+
*/
|
|
72
|
+
class GltfConservativeBounds {
|
|
73
|
+
_base;
|
|
74
|
+
_positionTargets;
|
|
75
|
+
_current;
|
|
76
|
+
_skinUsageCache = null;
|
|
77
|
+
constructor(_base, _positionTargets) {
|
|
78
|
+
this._base = _base;
|
|
79
|
+
this._positionTargets = _positionTargets;
|
|
80
|
+
this._current = _base;
|
|
81
|
+
}
|
|
82
|
+
static fromPrimitive(gltf, primitive) {
|
|
83
|
+
const positionAccessorIndex = primitive.attributes.POSITION;
|
|
84
|
+
if (positionAccessorIndex === undefined)
|
|
85
|
+
return null;
|
|
86
|
+
const base = readVec3Bounds(gltf.accessors?.[positionAccessorIndex]);
|
|
87
|
+
if (!base)
|
|
88
|
+
return null;
|
|
89
|
+
const positionTargets = (primitive.targets ?? []).map(target => {
|
|
90
|
+
if (target.POSITION === undefined)
|
|
91
|
+
return ZERO_BOUNDS;
|
|
92
|
+
return readVec3Bounds(gltf.accessors?.[target.POSITION]);
|
|
93
|
+
});
|
|
94
|
+
return new GltfConservativeBounds(base, positionTargets);
|
|
95
|
+
}
|
|
96
|
+
updateMorphWeights(weights) {
|
|
97
|
+
const min = [...this._base.min];
|
|
98
|
+
const max = [...this._base.max];
|
|
99
|
+
for (let targetIndex = 0; targetIndex < this._positionTargets.length; targetIndex++) {
|
|
100
|
+
const weight = weights[targetIndex] ?? 0;
|
|
101
|
+
if (!Number.isFinite(weight)) {
|
|
102
|
+
this._current = null;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (weight === 0)
|
|
106
|
+
continue;
|
|
107
|
+
const target = this._positionTargets[targetIndex];
|
|
108
|
+
if (!target) {
|
|
109
|
+
this._current = null;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
113
|
+
const targetMin = target.min[axis];
|
|
114
|
+
const targetMax = target.max[axis];
|
|
115
|
+
const currentMin = min[axis];
|
|
116
|
+
const currentMax = max[axis];
|
|
117
|
+
if (targetMin === undefined || targetMax === undefined || currentMin === undefined || currentMax === undefined) {
|
|
118
|
+
this._current = null;
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const a = targetMin * weight;
|
|
122
|
+
const b = targetMax * weight;
|
|
123
|
+
min[axis] = currentMin + Math.min(a, b);
|
|
124
|
+
max[axis] = currentMax + Math.max(a, b);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this._current = { min, max };
|
|
128
|
+
return boundsToSphere(this._current);
|
|
129
|
+
}
|
|
130
|
+
getSourceBounds() {
|
|
131
|
+
return this._current ? boundsToSphere(this._current) : null;
|
|
132
|
+
}
|
|
133
|
+
getSkinnedBounds(jointMatrices, joints, weights) {
|
|
134
|
+
if (!this._current || jointMatrices.length === 0 || jointMatrices.length % 16 !== 0)
|
|
135
|
+
return null;
|
|
136
|
+
const min = [Infinity, Infinity, Infinity];
|
|
137
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
138
|
+
const jointCount = jointMatrices.length / 16;
|
|
139
|
+
let usage;
|
|
140
|
+
if (this._skinUsageCache
|
|
141
|
+
&& this._skinUsageCache.jointCount === jointCount
|
|
142
|
+
&& this._skinUsageCache.joints === joints
|
|
143
|
+
&& this._skinUsageCache.weights === weights) {
|
|
144
|
+
usage = this._skinUsageCache.usage;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
usage = collectJointUsage(jointCount, joints, weights);
|
|
148
|
+
this._skinUsageCache = { jointCount, joints, weights, usage };
|
|
149
|
+
}
|
|
150
|
+
if (!usage)
|
|
151
|
+
return null;
|
|
152
|
+
if (usage.includeSource)
|
|
153
|
+
includeBounds(min, max, this._current);
|
|
154
|
+
for (const jointIndex of usage.joints) {
|
|
155
|
+
if (!includeTransformedBounds(min, max, this._current, jointMatrices, jointIndex * 16))
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return boundsToSphere({ min, max });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function collectJointUsage(jointCount, joints, weights) {
|
|
162
|
+
if (!joints || !weights || joints.length !== weights.length || joints.length % 4 !== 0) {
|
|
163
|
+
return { joints: new Set(Array.from({ length: jointCount }, (_, index) => index)), includeSource: true };
|
|
164
|
+
}
|
|
165
|
+
const used = new Set();
|
|
166
|
+
let includeSource = false;
|
|
167
|
+
for (let vertexOffset = 0; vertexOffset < joints.length; vertexOffset += 4) {
|
|
168
|
+
let weightSum = 0;
|
|
169
|
+
for (let influence = 0; influence < 4; influence++) {
|
|
170
|
+
const offset = vertexOffset + influence;
|
|
171
|
+
const weight = weights[offset];
|
|
172
|
+
const joint = joints[offset];
|
|
173
|
+
if (weight === undefined || joint === undefined
|
|
174
|
+
|| !Number.isFinite(weight) || !Number.isInteger(joint) || joint < 0 || joint >= jointCount)
|
|
175
|
+
return null;
|
|
176
|
+
if (weight <= 0)
|
|
177
|
+
continue;
|
|
178
|
+
used.add(joint);
|
|
179
|
+
weightSum += weight;
|
|
180
|
+
}
|
|
181
|
+
if (weightSum <= 0)
|
|
182
|
+
includeSource = true;
|
|
183
|
+
}
|
|
184
|
+
if (used.size === 0)
|
|
185
|
+
includeSource = true;
|
|
186
|
+
return { joints: used, includeSource };
|
|
187
|
+
}
|
|
188
|
+
function readVec3Bounds(accessor) {
|
|
189
|
+
if (!accessor || accessor.type !== 'VEC3')
|
|
190
|
+
return null;
|
|
191
|
+
const min = accessor.min;
|
|
192
|
+
const max = accessor.max;
|
|
193
|
+
if (!min || !max || min.length !== 3 || max.length !== 3)
|
|
194
|
+
return null;
|
|
195
|
+
if (![...min, ...max].every(Number.isFinite))
|
|
196
|
+
return null;
|
|
197
|
+
const [minX, minY, minZ] = min;
|
|
198
|
+
const [maxX, maxY, maxZ] = max;
|
|
199
|
+
if (minX === undefined || minY === undefined || minZ === undefined
|
|
200
|
+
|| maxX === undefined || maxY === undefined || maxZ === undefined)
|
|
201
|
+
return null;
|
|
202
|
+
if (minX > maxX || minY > maxY || minZ > maxZ)
|
|
203
|
+
return null;
|
|
204
|
+
return {
|
|
205
|
+
min: [minX, minY, minZ],
|
|
206
|
+
max: [maxX, maxY, maxZ],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function includeTransformedBounds(outMin, outMax, bounds, matrix, offset) {
|
|
210
|
+
if (offset < 0 || offset + 15 >= matrix.length)
|
|
211
|
+
return false;
|
|
212
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
213
|
+
const x = corner & 1 ? bounds.max[0] : bounds.min[0];
|
|
214
|
+
const y = corner & 2 ? bounds.max[1] : bounds.min[1];
|
|
215
|
+
const z = corner & 4 ? bounds.max[2] : bounds.min[2];
|
|
216
|
+
const tx = (matrix[offset] ?? NaN) * x + (matrix[offset + 4] ?? NaN) * y + (matrix[offset + 8] ?? NaN) * z + (matrix[offset + 12] ?? NaN);
|
|
217
|
+
const ty = (matrix[offset + 1] ?? NaN) * x + (matrix[offset + 5] ?? NaN) * y + (matrix[offset + 9] ?? NaN) * z + (matrix[offset + 13] ?? NaN);
|
|
218
|
+
const tz = (matrix[offset + 2] ?? NaN) * x + (matrix[offset + 6] ?? NaN) * y + (matrix[offset + 10] ?? NaN) * z + (matrix[offset + 14] ?? NaN);
|
|
219
|
+
if (![tx, ty, tz].every(Number.isFinite))
|
|
220
|
+
return false;
|
|
221
|
+
outMin[0] = Math.min(outMin[0], tx);
|
|
222
|
+
outMin[1] = Math.min(outMin[1], ty);
|
|
223
|
+
outMin[2] = Math.min(outMin[2], tz);
|
|
224
|
+
outMax[0] = Math.max(outMax[0], tx);
|
|
225
|
+
outMax[1] = Math.max(outMax[1], ty);
|
|
226
|
+
outMax[2] = Math.max(outMax[2], tz);
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
function includeBounds(outMin, outMax, bounds) {
|
|
231
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
232
|
+
const currentMin = outMin[axis];
|
|
233
|
+
const currentMax = outMax[axis];
|
|
234
|
+
const boundsMin = bounds.min[axis];
|
|
235
|
+
const boundsMax = bounds.max[axis];
|
|
236
|
+
if (currentMin === undefined || currentMax === undefined || boundsMin === undefined || boundsMax === undefined)
|
|
237
|
+
continue;
|
|
238
|
+
outMin[axis] = Math.min(currentMin, boundsMin);
|
|
239
|
+
outMax[axis] = Math.max(currentMax, boundsMax);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function boundsToSphere(bounds) {
|
|
243
|
+
const center = [
|
|
244
|
+
(bounds.min[0] + bounds.max[0]) * 0.5,
|
|
245
|
+
(bounds.min[1] + bounds.max[1]) * 0.5,
|
|
246
|
+
(bounds.min[2] + bounds.max[2]) * 0.5,
|
|
247
|
+
];
|
|
248
|
+
return {
|
|
249
|
+
center,
|
|
250
|
+
radius: Math.hypot(bounds.max[0] - center[0], bounds.max[1] - center[1], bounds.max[2] - center[2]),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const ZERO_BOUNDS = Object.freeze({
|
|
254
|
+
min: Object.freeze([0, 0, 0]),
|
|
255
|
+
max: Object.freeze([0, 0, 0]),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
function createGltfCompatibilityReport(gltf, extensions, primitives, performance) {
|
|
259
|
+
const extensionEntries = extensions.map(entry => Object.freeze({ ...entry }));
|
|
260
|
+
const textures = createTextureEntries(gltf);
|
|
261
|
+
const bounds = primitives.map(input => createBoundsEntry(gltf, input));
|
|
262
|
+
const uvSemantics = primitives.map(input => Object.freeze({
|
|
263
|
+
meshIndex: input.meshIndex,
|
|
264
|
+
primitiveIndex: input.primitiveIndex,
|
|
265
|
+
capacity: input.uvSemanticPlan.capacity,
|
|
266
|
+
availableSemantics: Object.freeze([...input.uvSemanticPlan.availableSemantics]),
|
|
267
|
+
referencedSemantics: Object.freeze([...input.uvSemanticPlan.referencedSemantics]),
|
|
268
|
+
mappings: Object.freeze(input.uvSemanticPlan.mappings.map(mapping => Object.freeze({ ...mapping }))),
|
|
269
|
+
path: input.uvSemanticPlan.path,
|
|
270
|
+
}));
|
|
271
|
+
const issues = [];
|
|
272
|
+
for (const entry of extensionEntries) {
|
|
273
|
+
if (entry.support === 'supported')
|
|
274
|
+
continue;
|
|
275
|
+
issues.push(Object.freeze({
|
|
276
|
+
category: 'extension',
|
|
277
|
+
path: `gltf.extensionsUsed[${Math.max(0, (gltf.extensionsUsed ?? []).indexOf(entry.extension))}]`,
|
|
278
|
+
code: entry.support === 'partial' ? 'GLTF_EXTENSION_PARTIAL' : 'GLTF_EXTENSION_UNSUPPORTED',
|
|
279
|
+
message: entry.note,
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
for (const entry of textures) {
|
|
283
|
+
if (entry.mipmapSource !== 'unavailable')
|
|
284
|
+
continue;
|
|
285
|
+
issues.push(Object.freeze({
|
|
286
|
+
category: 'texture-mipmap',
|
|
287
|
+
path: entry.path,
|
|
288
|
+
code: 'GLTF_TEXTURE_MIPMAP_UNAVAILABLE',
|
|
289
|
+
message: entry.note,
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
for (const entry of bounds) {
|
|
293
|
+
if (entry.support !== 'fail-open')
|
|
294
|
+
continue;
|
|
295
|
+
issues.push(Object.freeze({
|
|
296
|
+
category: 'bounds',
|
|
297
|
+
path: entry.path,
|
|
298
|
+
code: 'GLTF_BOUNDS_FAIL_OPEN',
|
|
299
|
+
message: entry.reason ?? 'Dynamic bounds cannot be proven; frustum culling remains disabled.',
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
return Object.freeze({
|
|
303
|
+
status: issues.length === 0 ? 'compatible' : 'degraded',
|
|
304
|
+
extensions: Object.freeze(extensionEntries),
|
|
305
|
+
textures: Object.freeze(textures),
|
|
306
|
+
bounds: Object.freeze(bounds),
|
|
307
|
+
uvSemantics: Object.freeze(uvSemantics),
|
|
308
|
+
performance: Object.freeze({ ...performance }),
|
|
309
|
+
issues: Object.freeze(issues),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function createTextureEntries(gltf) {
|
|
313
|
+
return (gltf.textures ?? []).map((texture, textureIndex) => {
|
|
314
|
+
const basisuSource = texture.extensions?.KHR_texture_basisu?.source;
|
|
315
|
+
const imageIndex = basisuSource ?? texture.source;
|
|
316
|
+
const image = imageIndex === undefined ? undefined : gltf.images?.[imageIndex];
|
|
317
|
+
const path = `gltf.textures[${textureIndex}]`;
|
|
318
|
+
if (imageIndex === undefined || !image) {
|
|
319
|
+
return Object.freeze({
|
|
320
|
+
textureIndex,
|
|
321
|
+
imageIndex: imageIndex ?? null,
|
|
322
|
+
mipmapSource: 'unavailable',
|
|
323
|
+
path,
|
|
324
|
+
note: 'Texture has no resolvable image source, so a mipmap contract cannot be established.',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const compressed = basisuSource !== undefined || image.mimeType === 'image/ktx2';
|
|
328
|
+
return Object.freeze({
|
|
329
|
+
textureIndex,
|
|
330
|
+
imageIndex,
|
|
331
|
+
mipmapSource: compressed ? 'source-provided' : 'generated-full-chain',
|
|
332
|
+
path,
|
|
333
|
+
note: compressed
|
|
334
|
+
? 'KTX2/Basis texture uses the source-provided mip chain.'
|
|
335
|
+
: 'Ordinary image receives a generated full mip chain when loaded by PBR.',
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function createBoundsEntry(gltf, input) {
|
|
340
|
+
const path = `gltf.meshes[${input.meshIndex}].primitives[${input.primitiveIndex}]`;
|
|
341
|
+
const dynamic = input.skinRequested || (input.primitive.targets?.length ?? 0) > 0;
|
|
342
|
+
if (!dynamic) {
|
|
343
|
+
return Object.freeze({ meshIndex: input.meshIndex, primitiveIndex: input.primitiveIndex, support: 'static', path, reason: null });
|
|
344
|
+
}
|
|
345
|
+
const reason = getBoundsEvidenceFailure(gltf, input);
|
|
346
|
+
if (reason) {
|
|
347
|
+
return Object.freeze({ meshIndex: input.meshIndex, primitiveIndex: input.primitiveIndex, support: 'fail-open', path, reason });
|
|
348
|
+
}
|
|
349
|
+
return Object.freeze({
|
|
350
|
+
meshIndex: input.meshIndex,
|
|
351
|
+
primitiveIndex: input.primitiveIndex,
|
|
352
|
+
support: 'accessor-conservative',
|
|
353
|
+
path,
|
|
354
|
+
reason: null,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function getBoundsEvidenceFailure(gltf, input) {
|
|
358
|
+
const positionIndex = input.primitive.attributes.POSITION;
|
|
359
|
+
if (positionIndex === undefined || !hasVec3Bounds(gltf.accessors?.[positionIndex])) {
|
|
360
|
+
return 'Base POSITION accessor is missing a valid finite min/max pair.';
|
|
361
|
+
}
|
|
362
|
+
for (const [targetIndex, target] of (input.primitive.targets ?? []).entries()) {
|
|
363
|
+
if (target.POSITION === undefined)
|
|
364
|
+
continue;
|
|
365
|
+
if (!hasVec3Bounds(gltf.accessors?.[target.POSITION])) {
|
|
366
|
+
return `Morph target ${targetIndex} POSITION accessor is missing a valid finite min/max pair.`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (input.skinRequested && !input.geometry.skinning) {
|
|
370
|
+
return 'Skin was requested but a runtime skin contract could not be created for this primitive.';
|
|
371
|
+
}
|
|
372
|
+
if (!input.geometry.localBounds) {
|
|
373
|
+
return 'Dynamic local bounds are unavailable for the current morph/skin state.';
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
function hasVec3Bounds(accessor) {
|
|
378
|
+
if (!accessor || accessor.type !== 'VEC3' || accessor.min?.length !== 3 || accessor.max?.length !== 3)
|
|
379
|
+
return false;
|
|
380
|
+
const min = accessor.min;
|
|
381
|
+
const max = accessor.max;
|
|
382
|
+
return [...min, ...max].every(Number.isFinite)
|
|
383
|
+
&& min.every((value, axis) => {
|
|
384
|
+
const maximum = max[axis];
|
|
385
|
+
return maximum !== undefined && value <= maximum;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const clearcoatAdapter = Object.freeze({
|
|
390
|
+
extension: 'KHR_materials_clearcoat',
|
|
391
|
+
capability: Object.freeze({ support: 'supported', note: 'Imported as a native layered PBR clearcoat lobe.' }),
|
|
392
|
+
extendMaterial(context) {
|
|
393
|
+
const clearcoat = context.extensionData;
|
|
394
|
+
if (!clearcoat)
|
|
395
|
+
return null;
|
|
396
|
+
const basePath = `${context.materialPath}.extensions.KHR_materials_clearcoat`;
|
|
397
|
+
const textures = [];
|
|
398
|
+
if (clearcoat.clearcoatTexture)
|
|
399
|
+
textures.push({
|
|
400
|
+
slot: 'clearcoat',
|
|
401
|
+
textureInfo: clearcoat.clearcoatTexture,
|
|
402
|
+
path: `${basePath}.clearcoatTexture`,
|
|
403
|
+
});
|
|
404
|
+
if (clearcoat.clearcoatRoughnessTexture)
|
|
405
|
+
textures.push({
|
|
406
|
+
slot: 'clearcoatRoughness',
|
|
407
|
+
textureInfo: clearcoat.clearcoatRoughnessTexture,
|
|
408
|
+
path: `${basePath}.clearcoatRoughnessTexture`,
|
|
409
|
+
});
|
|
410
|
+
if (clearcoat.clearcoatNormalTexture)
|
|
411
|
+
textures.push({
|
|
412
|
+
slot: 'clearcoatNormal',
|
|
413
|
+
textureInfo: clearcoat.clearcoatNormalTexture,
|
|
414
|
+
path: `${basePath}.clearcoatNormalTexture`,
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
state: {
|
|
418
|
+
clearcoatFactor: clearcoat.clearcoatFactor ?? 0,
|
|
419
|
+
clearcoatRoughnessFactor: clearcoat.clearcoatRoughnessFactor ?? 0,
|
|
420
|
+
clearcoatNormalScale: clearcoat.clearcoatNormalTexture?.scale ?? 1,
|
|
421
|
+
},
|
|
422
|
+
textures: Object.freeze(textures),
|
|
423
|
+
};
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
const iorAdapter = Object.freeze({
|
|
427
|
+
extension: 'KHR_materials_ior',
|
|
428
|
+
capability: Object.freeze({ support: 'supported', note: 'Imported into the native dielectric Fresnel IOR parameter.' }),
|
|
429
|
+
extendMaterial(context) {
|
|
430
|
+
const ior = context.extensionData;
|
|
431
|
+
if (!ior)
|
|
432
|
+
return null;
|
|
433
|
+
return { state: { ior: ior.ior ?? 1.5 } };
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
const specularAdapter = Object.freeze({
|
|
437
|
+
extension: 'KHR_materials_specular',
|
|
438
|
+
capability: Object.freeze({ support: 'supported', note: 'Imported into the native dielectric specular BRDF.' }),
|
|
439
|
+
extendMaterial(context) {
|
|
440
|
+
const specular = context.extensionData;
|
|
441
|
+
if (!specular)
|
|
442
|
+
return null;
|
|
443
|
+
const basePath = `${context.materialPath}.extensions.KHR_materials_specular`;
|
|
444
|
+
const textures = [];
|
|
445
|
+
if (specular.specularTexture)
|
|
446
|
+
textures.push({
|
|
447
|
+
slot: 'specular',
|
|
448
|
+
textureInfo: specular.specularTexture,
|
|
449
|
+
path: `${basePath}.specularTexture`,
|
|
450
|
+
});
|
|
451
|
+
if (specular.specularColorTexture)
|
|
452
|
+
textures.push({
|
|
453
|
+
slot: 'specularColor',
|
|
454
|
+
textureInfo: specular.specularColorTexture,
|
|
455
|
+
path: `${basePath}.specularColorTexture`,
|
|
456
|
+
});
|
|
457
|
+
return {
|
|
458
|
+
state: {
|
|
459
|
+
specularFactor: specular.specularFactor ?? 1,
|
|
460
|
+
specularColorFactor: specular.specularColorFactor ?? [1, 1, 1],
|
|
461
|
+
},
|
|
462
|
+
textures: Object.freeze(textures),
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
const sheenAdapter = Object.freeze({
|
|
467
|
+
extension: 'KHR_materials_sheen',
|
|
468
|
+
capability: Object.freeze({ support: 'supported', note: 'Imported as a native Charlie sheen layer with direct-light and IBL energy compensation.' }),
|
|
469
|
+
extendMaterial(context) {
|
|
470
|
+
const sheen = context.extensionData;
|
|
471
|
+
if (!sheen)
|
|
472
|
+
return null;
|
|
473
|
+
const basePath = `${context.materialPath}.extensions.KHR_materials_sheen`;
|
|
474
|
+
const textures = [];
|
|
475
|
+
if (sheen.sheenColorTexture)
|
|
476
|
+
textures.push({
|
|
477
|
+
slot: 'sheenColor',
|
|
478
|
+
textureInfo: sheen.sheenColorTexture,
|
|
479
|
+
path: `${basePath}.sheenColorTexture`,
|
|
480
|
+
});
|
|
481
|
+
if (sheen.sheenRoughnessTexture)
|
|
482
|
+
textures.push({
|
|
483
|
+
slot: 'sheenRoughness',
|
|
484
|
+
textureInfo: sheen.sheenRoughnessTexture,
|
|
485
|
+
path: `${basePath}.sheenRoughnessTexture`,
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
state: {
|
|
489
|
+
sheenColorFactor: sheen.sheenColorFactor ?? [0, 0, 0],
|
|
490
|
+
sheenRoughnessFactor: sheen.sheenRoughnessFactor ?? 0,
|
|
491
|
+
},
|
|
492
|
+
textures: Object.freeze(textures),
|
|
493
|
+
};
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
const transmissionAdapter = Object.freeze({
|
|
497
|
+
extension: 'KHR_materials_transmission',
|
|
498
|
+
capability: Object.freeze({
|
|
499
|
+
support: 'supported',
|
|
500
|
+
note: 'Imported into the native screen-space scene-color path; combined Sheen keeps factor BRDF but omits Sheen texture sampling to stay within the WebGPU minimum binding budget.',
|
|
501
|
+
}),
|
|
502
|
+
extendMaterial(context) {
|
|
503
|
+
const transmission = context.extensionData;
|
|
504
|
+
if (!transmission)
|
|
505
|
+
return null;
|
|
506
|
+
const basePath = `${context.materialPath}.extensions.KHR_materials_transmission`;
|
|
507
|
+
return {
|
|
508
|
+
state: { transmissionFactor: transmission.transmissionFactor ?? 0 },
|
|
509
|
+
textures: transmission.transmissionTexture ? Object.freeze([{
|
|
510
|
+
slot: 'transmission',
|
|
511
|
+
textureInfo: transmission.transmissionTexture,
|
|
512
|
+
path: `${basePath}.transmissionTexture`,
|
|
513
|
+
}]) : Object.freeze([]),
|
|
514
|
+
};
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
const volumeAdapter = Object.freeze({
|
|
518
|
+
extension: 'KHR_materials_volume',
|
|
519
|
+
capability: Object.freeze({ support: 'supported', note: 'Imported with thickness and Beer-Lambert attenuation in the native transmission path.' }),
|
|
520
|
+
extendMaterial(context) {
|
|
521
|
+
const volume = context.extensionData;
|
|
522
|
+
if (!volume)
|
|
523
|
+
return null;
|
|
524
|
+
const basePath = `${context.materialPath}.extensions.KHR_materials_volume`;
|
|
525
|
+
return {
|
|
526
|
+
state: {
|
|
527
|
+
thicknessFactor: volume.thicknessFactor ?? 0,
|
|
528
|
+
attenuationDistance: volume.attenuationDistance ?? Infinity,
|
|
529
|
+
attenuationColor: volume.attenuationColor ?? [1, 1, 1],
|
|
530
|
+
},
|
|
531
|
+
textures: volume.thicknessTexture ? Object.freeze([{
|
|
532
|
+
slot: 'thickness',
|
|
533
|
+
textureInfo: volume.thicknessTexture,
|
|
534
|
+
path: `${basePath}.thicknessTexture`,
|
|
535
|
+
}]) : Object.freeze([]),
|
|
536
|
+
};
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
const variantsAdapter = Object.freeze({
|
|
540
|
+
extension: 'KHR_materials_variants',
|
|
541
|
+
capability: Object.freeze({ support: 'supported', note: 'Imported as native PBR material variants.' }),
|
|
542
|
+
collectMaterialVariantNames(gltf, rootExtensionData) {
|
|
543
|
+
const definitions = rootExtensionData?.variants ?? [];
|
|
544
|
+
return Object.freeze(definitions.map(variant => variant.name));
|
|
545
|
+
},
|
|
546
|
+
collectMaterialVariants(context) {
|
|
547
|
+
const definitions = context.rootExtensionData?.variants ?? [];
|
|
548
|
+
const mappings = context.primitiveExtensionData?.mappings ?? [];
|
|
549
|
+
const result = [];
|
|
550
|
+
for (const mapping of mappings) {
|
|
551
|
+
for (const variantIndex of mapping.variants) {
|
|
552
|
+
const name = definitions[variantIndex]?.name;
|
|
553
|
+
if (name)
|
|
554
|
+
result.push(Object.freeze({ name, materialIndex: mapping.material }));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return Object.freeze(result);
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
function capabilityAdapter(extension, support, note) {
|
|
561
|
+
return Object.freeze({ extension, capability: Object.freeze({ support, note }) });
|
|
562
|
+
}
|
|
563
|
+
const DEFAULT_GLTF_EXTENSION_ADAPTERS = Object.freeze([
|
|
564
|
+
capabilityAdapter('KHR_draco_mesh_compression', 'supported', 'Decoded through the configured Draco decoder.'),
|
|
565
|
+
variantsAdapter,
|
|
566
|
+
clearcoatAdapter,
|
|
567
|
+
iorAdapter,
|
|
568
|
+
specularAdapter,
|
|
569
|
+
sheenAdapter,
|
|
570
|
+
transmissionAdapter,
|
|
571
|
+
volumeAdapter,
|
|
572
|
+
capabilityAdapter('KHR_materials_anisotropy', 'partial', 'The base PBR material is imported, but anisotropy shading is not rendered.'),
|
|
573
|
+
capabilityAdapter('KHR_texture_basisu', 'supported', 'Imported through the KTX2/Basis texture path.'),
|
|
574
|
+
capabilityAdapter('KHR_texture_transform', 'supported', 'Applied per PBR texture slot in the shader.'),
|
|
575
|
+
]);
|
|
576
|
+
/** Resolves an immutable per-load adapter set; user adapters replace defaults by extension id. */
|
|
577
|
+
function resolveGltfExtensionAdapters(customAdapters = []) {
|
|
578
|
+
const adapters = new Map(DEFAULT_GLTF_EXTENSION_ADAPTERS.map(adapter => [adapter.extension, adapter]));
|
|
579
|
+
for (const adapter of customAdapters) {
|
|
580
|
+
const extension = adapter.extension.trim();
|
|
581
|
+
if (!extension)
|
|
582
|
+
throw new TypeError('GltfExtensionAdapter.extension must not be empty.');
|
|
583
|
+
adapters.set(extension, Object.freeze({
|
|
584
|
+
...adapter,
|
|
585
|
+
extension,
|
|
586
|
+
capability: Object.freeze({ ...adapter.capability }),
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
return Object.freeze([...adapters.values()]);
|
|
590
|
+
}
|
|
591
|
+
function createGltfExtensionCapabilities(adapters) {
|
|
592
|
+
return Object.freeze(Object.fromEntries(adapters.map(adapter => [adapter.extension, adapter.capability])));
|
|
593
|
+
}
|
|
594
|
+
function collectGltfMaterialExtensionPatches(gltf, material, primitive, materialPath, adapters) {
|
|
595
|
+
if (!material)
|
|
596
|
+
return Object.freeze([]);
|
|
597
|
+
const patches = [];
|
|
598
|
+
for (const adapter of adapters) {
|
|
599
|
+
if (!adapter.extendMaterial)
|
|
600
|
+
continue;
|
|
601
|
+
const extensionData = material.extensions?.[adapter.extension];
|
|
602
|
+
if (extensionData === undefined)
|
|
603
|
+
continue;
|
|
604
|
+
const patch = adapter.extendMaterial({ gltf, material, primitive, materialPath, extensionData });
|
|
605
|
+
if (patch)
|
|
606
|
+
patches.push(patch);
|
|
607
|
+
}
|
|
608
|
+
return Object.freeze(patches);
|
|
609
|
+
}
|
|
610
|
+
function collectGltfMaterialVariantReferences(gltf, primitive, adapters) {
|
|
611
|
+
const result = [];
|
|
612
|
+
for (const adapter of adapters) {
|
|
613
|
+
if (!adapter.collectMaterialVariants)
|
|
614
|
+
continue;
|
|
615
|
+
const rootExtensionData = gltf.extensions?.[adapter.extension];
|
|
616
|
+
const primitiveExtensionData = primitive.extensions?.[adapter.extension];
|
|
617
|
+
if (rootExtensionData === undefined && primitiveExtensionData === undefined)
|
|
618
|
+
continue;
|
|
619
|
+
result.push(...adapter.collectMaterialVariants({ gltf, primitive, rootExtensionData, primitiveExtensionData }));
|
|
620
|
+
}
|
|
621
|
+
return Object.freeze(result);
|
|
622
|
+
}
|
|
623
|
+
function collectGltfMaterialVariantNames(gltf, adapters) {
|
|
624
|
+
const names = new Set();
|
|
625
|
+
for (const adapter of adapters) {
|
|
626
|
+
if (!adapter.collectMaterialVariantNames)
|
|
627
|
+
continue;
|
|
628
|
+
const rootExtensionData = gltf.extensions?.[adapter.extension];
|
|
629
|
+
if (rootExtensionData === undefined)
|
|
630
|
+
continue;
|
|
631
|
+
for (const name of adapter.collectMaterialVariantNames(gltf, rootExtensionData)) {
|
|
632
|
+
if (name)
|
|
633
|
+
names.add(name);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
for (const mesh of gltf.meshes ?? []) {
|
|
637
|
+
for (const primitive of mesh.primitives) {
|
|
638
|
+
for (const variant of collectGltfMaterialVariantReferences(gltf, primitive, adapters))
|
|
639
|
+
names.add(variant.name);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return Object.freeze([...names]);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const textureStateKeys = Object.freeze({
|
|
646
|
+
baseColor: 'baseColorTexture',
|
|
647
|
+
metallicRoughness: 'metallicRoughnessTexture',
|
|
648
|
+
normal: 'normalTexture',
|
|
649
|
+
occlusion: 'occlusionTexture',
|
|
650
|
+
emissive: 'emissiveTexture',
|
|
651
|
+
clearcoat: 'clearcoatTexture',
|
|
652
|
+
clearcoatRoughness: 'clearcoatRoughnessTexture',
|
|
653
|
+
clearcoatNormal: 'clearcoatNormalTexture',
|
|
654
|
+
specular: 'specularTexture',
|
|
655
|
+
specularColor: 'specularColorTexture',
|
|
656
|
+
sheenColor: 'sheenColorTexture',
|
|
657
|
+
sheenRoughness: 'sheenRoughnessTexture',
|
|
658
|
+
transmission: 'transmissionTexture',
|
|
659
|
+
thickness: 'thicknessTexture',
|
|
660
|
+
});
|
|
661
|
+
/** Compiles glTF core material data and extension patches into one engine descriptor. */
|
|
662
|
+
function createGltfMaterialDescriptor(input) {
|
|
663
|
+
const { material } = input;
|
|
664
|
+
const pbr = material?.pbrMetallicRoughness;
|
|
665
|
+
const state = {
|
|
666
|
+
baseColor: pbr?.baseColorFactor
|
|
667
|
+
? linearBaseColorFactorToSrgb(pbr.baseColorFactor)
|
|
668
|
+
: input.defaultBaseColor ?? [1, 1, 1, 1],
|
|
669
|
+
metallic: pbr?.metallicFactor ?? 1,
|
|
670
|
+
roughness: pbr?.roughnessFactor ?? 1,
|
|
671
|
+
normalScale: material?.normalTexture?.scale ?? 1,
|
|
672
|
+
occlusionStrength: material?.occlusionTexture?.strength ?? 1,
|
|
673
|
+
emissiveFactor: material?.emissiveFactor ?? [0, 0, 0],
|
|
674
|
+
alphaMode: material?.alphaMode === 'BLEND' ? 'blend' : material?.alphaMode === 'MASK' ? 'mask' : 'opaque',
|
|
675
|
+
alphaCutoff: material?.alphaCutoff ?? 0.5,
|
|
676
|
+
doubleSided: material?.doubleSided ?? false,
|
|
677
|
+
};
|
|
678
|
+
const patches = collectGltfMaterialExtensionPatches(input.gltf, material, input.primitive, input.materialPath, input.adapters);
|
|
679
|
+
const textureMappings = {};
|
|
680
|
+
const samplers = {};
|
|
681
|
+
for (const patch of patches) {
|
|
682
|
+
Object.assign(state, patch.state);
|
|
683
|
+
}
|
|
684
|
+
for (const binding of collectGltfMaterialTextureBindings(input.gltf, material, input.primitive, input.materialPath, input.adapters)) {
|
|
685
|
+
const texture = input.resolveTexture(binding);
|
|
686
|
+
state[textureStateKeys[binding.slot]] = texture.source;
|
|
687
|
+
textureMappings[binding.slot] = texture.mapping;
|
|
688
|
+
samplers[binding.slot] = texture.sampler;
|
|
689
|
+
}
|
|
690
|
+
if (Object.keys(textureMappings).length > 0)
|
|
691
|
+
state.textureMappings = textureMappings;
|
|
692
|
+
if (Object.keys(samplers).length > 0)
|
|
693
|
+
state.samplers = samplers;
|
|
694
|
+
return Object.freeze({
|
|
695
|
+
shadingModel: 'pbr-metallic-roughness',
|
|
696
|
+
state: Object.freeze(state),
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/** Shared semantic source for preload, UV planning, and descriptor compilation. */
|
|
700
|
+
function collectGltfMaterialTextureBindings(gltf, material, primitive, materialPath, adapters) {
|
|
701
|
+
if (!material)
|
|
702
|
+
return Object.freeze([]);
|
|
703
|
+
const pbr = material.pbrMetallicRoughness;
|
|
704
|
+
const bindings = [];
|
|
705
|
+
addTexture(bindings, 'baseColor', pbr?.baseColorTexture, `${materialPath}.pbrMetallicRoughness.baseColorTexture`);
|
|
706
|
+
addTexture(bindings, 'metallicRoughness', pbr?.metallicRoughnessTexture, `${materialPath}.pbrMetallicRoughness.metallicRoughnessTexture`);
|
|
707
|
+
addTexture(bindings, 'normal', material.normalTexture, `${materialPath}.normalTexture`);
|
|
708
|
+
addTexture(bindings, 'occlusion', material.occlusionTexture, `${materialPath}.occlusionTexture`);
|
|
709
|
+
addTexture(bindings, 'emissive', material.emissiveTexture, `${materialPath}.emissiveTexture`);
|
|
710
|
+
for (const patch of collectGltfMaterialExtensionPatches(gltf, material, primitive, materialPath, adapters)) {
|
|
711
|
+
bindings.push(...patch.textures ?? []);
|
|
712
|
+
}
|
|
713
|
+
return Object.freeze(bindings);
|
|
714
|
+
}
|
|
715
|
+
function addTexture(bindings, slot, textureInfo, path) {
|
|
716
|
+
if (textureInfo)
|
|
717
|
+
bindings.push(Object.freeze({ slot, textureInfo, path }));
|
|
718
|
+
}
|
|
719
|
+
function linearBaseColorFactorToSrgb(value) {
|
|
720
|
+
return [
|
|
721
|
+
ColorSRGB.linearToSRGB(value[0]),
|
|
722
|
+
ColorSRGB.linearToSRGB(value[1]),
|
|
723
|
+
ColorSRGB.linearToSRGB(value[2]),
|
|
724
|
+
value[3],
|
|
725
|
+
];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const GLTF_UV_CHANNEL_CAPACITY = PBR_COMPATIBILITY_CONTRACT.uvChannelCapacity;
|
|
729
|
+
function planGltfUvSemantics(gltf, primitive, meshIndex, primitiveIndex, adapters = DEFAULT_GLTF_EXTENSION_ADAPTERS) {
|
|
730
|
+
const primitivePath = `gltf.meshes[${meshIndex}].primitives[${primitiveIndex}]`;
|
|
731
|
+
const availableSets = Object.keys(primitive.attributes)
|
|
732
|
+
.map(parseTextureCoordinateSemantic)
|
|
733
|
+
.filter((set) => set !== null)
|
|
734
|
+
.sort((a, b) => a - b);
|
|
735
|
+
const availableSetLookup = new Set(availableSets);
|
|
736
|
+
const references = collectTextureCoordinateReferences(gltf, primitive, adapters);
|
|
737
|
+
for (const reference of references) {
|
|
738
|
+
if (availableSetLookup.has(reference.set))
|
|
739
|
+
continue;
|
|
740
|
+
return {
|
|
741
|
+
ok: false,
|
|
742
|
+
failure: Object.freeze({
|
|
743
|
+
code: 'GLTF_UV_SEMANTIC_MISSING',
|
|
744
|
+
message: `Texture references ${reference.semantic}, but the mesh primitive does not provide that attribute.`,
|
|
745
|
+
path: reference.path,
|
|
746
|
+
context: Object.freeze({ texCoord: reference.set, semantic: reference.semantic, meshIndex, primitiveIndex }),
|
|
747
|
+
}),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const referencedSets = [...new Set(references.map(reference => reference.set))].sort((a, b) => a - b);
|
|
751
|
+
if (referencedSets.length > GLTF_UV_CHANNEL_CAPACITY) {
|
|
752
|
+
const referencedSemantics = referencedSets.map(toTextureCoordinateSemantic);
|
|
753
|
+
return {
|
|
754
|
+
ok: false,
|
|
755
|
+
failure: Object.freeze({
|
|
756
|
+
code: 'GLTF_UV_CHANNEL_CAPACITY_EXCEEDED',
|
|
757
|
+
message: `Primitive references ${referencedSets.length} texture-coordinate sets (${referencedSemantics.join(', ')}), exceeding the ${GLTF_UV_CHANNEL_CAPACITY}-channel PBR contract.`,
|
|
758
|
+
path: `${primitivePath}.attributes`,
|
|
759
|
+
context: Object.freeze({
|
|
760
|
+
meshIndex,
|
|
761
|
+
primitiveIndex,
|
|
762
|
+
capacity: GLTF_UV_CHANNEL_CAPACITY,
|
|
763
|
+
referencedSemantics: Object.freeze(referencedSemantics),
|
|
764
|
+
}),
|
|
765
|
+
}),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
const mappings = referencedSets.map((set, channel) => Object.freeze({
|
|
769
|
+
semantic: toTextureCoordinateSemantic(set),
|
|
770
|
+
set,
|
|
771
|
+
channel: channel,
|
|
772
|
+
}));
|
|
773
|
+
return {
|
|
774
|
+
ok: true,
|
|
775
|
+
plan: Object.freeze({
|
|
776
|
+
meshIndex,
|
|
777
|
+
primitiveIndex,
|
|
778
|
+
path: primitivePath,
|
|
779
|
+
capacity: GLTF_UV_CHANNEL_CAPACITY,
|
|
780
|
+
availableSemantics: Object.freeze(availableSets.map(toTextureCoordinateSemantic)),
|
|
781
|
+
referencedSemantics: Object.freeze(referencedSets.map(toTextureCoordinateSemantic)),
|
|
782
|
+
mappings: Object.freeze(mappings),
|
|
783
|
+
}),
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function getGltfUvChannel(plan, semanticSet) {
|
|
787
|
+
return plan.mappings.find(mapping => mapping.set === semanticSet)?.channel ?? null;
|
|
788
|
+
}
|
|
789
|
+
function parseTextureCoordinateSemantic(semantic) {
|
|
790
|
+
const match = /^TEXCOORD_(0|[1-9]\d*)$/.exec(semantic);
|
|
791
|
+
if (!match)
|
|
792
|
+
return null;
|
|
793
|
+
const set = Number(match[1]);
|
|
794
|
+
return Number.isSafeInteger(set) ? set : null;
|
|
795
|
+
}
|
|
796
|
+
function toTextureCoordinateSemantic(set) {
|
|
797
|
+
return `TEXCOORD_${set}`;
|
|
798
|
+
}
|
|
799
|
+
function collectTextureCoordinateReferences(gltf, primitive, adapters) {
|
|
800
|
+
const materials = new Map();
|
|
801
|
+
const baseIndex = primitive.material;
|
|
802
|
+
materials.set(baseIndex ?? null, {
|
|
803
|
+
material: baseIndex === undefined ? null : gltf.materials?.[baseIndex] ?? null,
|
|
804
|
+
path: baseIndex === undefined ? 'gltf.defaultMaterial' : `gltf.materials[${baseIndex}]`,
|
|
805
|
+
});
|
|
806
|
+
for (const variant of collectGltfMaterialVariantReferences(gltf, primitive, adapters)) {
|
|
807
|
+
if (materials.has(variant.materialIndex))
|
|
808
|
+
continue;
|
|
809
|
+
materials.set(variant.materialIndex, {
|
|
810
|
+
material: gltf.materials?.[variant.materialIndex] ?? null,
|
|
811
|
+
path: `gltf.materials[${variant.materialIndex}]`,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
const references = [];
|
|
815
|
+
for (const { material, path } of materials.values()) {
|
|
816
|
+
for (const { textureInfo, path: texturePath } of collectGltfMaterialTextureBindings(gltf, material, primitive, path, adapters)) {
|
|
817
|
+
const transform = textureInfo.extensions?.KHR_texture_transform;
|
|
818
|
+
const set = transform?.texCoord ?? textureInfo.texCoord ?? 0;
|
|
819
|
+
references.push(Object.freeze({
|
|
820
|
+
set,
|
|
821
|
+
semantic: toTextureCoordinateSemantic(set),
|
|
822
|
+
path: transform?.texCoord === undefined
|
|
823
|
+
? `${texturePath}.texCoord`
|
|
824
|
+
: `${texturePath}.extensions.KHR_texture_transform.texCoord`,
|
|
825
|
+
}));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return references;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const DEFAULT_DRACO_DECODER_SCRIPT_URL = 'draco_decoder_gltf_nodejs.js';
|
|
832
|
+
const FALLBACK_DRACO_DECODER_SCRIPT_URL = '../node_modules/draco3dgltf/draco_decoder_gltf_nodejs.js';
|
|
833
|
+
let sharedDecoderPromise = null;
|
|
834
|
+
const sharedScriptPromises = new Map();
|
|
835
|
+
/** Resolves and decodes one KHR_draco_mesh_compression primitive. */
|
|
836
|
+
async function decodeDracoPrimitive(gltf, buffers, extension, options) {
|
|
837
|
+
throwIfGltfLoadAborted(options.signal);
|
|
838
|
+
const view = gltf.bufferViews?.[extension.bufferView];
|
|
839
|
+
if (!view)
|
|
840
|
+
throw gltfDataError(`Missing Draco bufferView ${extension.bufferView}.`);
|
|
841
|
+
const source = buffers[view.buffer];
|
|
842
|
+
if (!source)
|
|
843
|
+
throw gltfDataError(`Missing Draco buffer ${view.buffer}.`);
|
|
844
|
+
const module = await resolveDracoDecoder(options);
|
|
845
|
+
throwIfGltfLoadAborted(options.signal);
|
|
846
|
+
const decoder = new module.Decoder();
|
|
847
|
+
const decoderBuffer = new module.DecoderBuffer();
|
|
848
|
+
const mesh = new module.Mesh();
|
|
849
|
+
const bytes = new Uint8Array(source, view.byteOffset ?? 0, view.byteLength);
|
|
850
|
+
decoderBuffer.Init(bytes, bytes.byteLength);
|
|
851
|
+
try {
|
|
852
|
+
const status = decoder.DecodeBufferToMesh(decoderBuffer, mesh);
|
|
853
|
+
throwIfGltfLoadAborted(options.signal);
|
|
854
|
+
if (!status.ok())
|
|
855
|
+
throw gltfDataError(`Draco decode failed: ${status.error_msg()}`);
|
|
856
|
+
const positions = readFloatAttribute(module, decoder, mesh, extension, 'POSITION', 3);
|
|
857
|
+
if (!positions)
|
|
858
|
+
throw gltfDataError('Draco primitive is missing POSITION attribute.');
|
|
859
|
+
const normals = readFloatAttribute(module, decoder, mesh, extension, 'NORMAL', 3);
|
|
860
|
+
const textureCoordinates = Object.keys(extension.attributes)
|
|
861
|
+
.map(semantic => ({ semantic, set: parseTextureCoordinateSemantic(semantic) }))
|
|
862
|
+
.filter((entry) => entry.set !== null)
|
|
863
|
+
.sort((a, b) => a.set - b.set)
|
|
864
|
+
.map(entry => {
|
|
865
|
+
const data = readFloatAttribute(module, decoder, mesh, extension, entry.semantic, 2);
|
|
866
|
+
if (!data)
|
|
867
|
+
throw gltfDataError(`Draco primitive is missing ${entry.semantic}.`);
|
|
868
|
+
return { set: entry.set, data };
|
|
869
|
+
});
|
|
870
|
+
const joints = readUnsignedAttribute(module, decoder, mesh, extension, 'JOINTS_0', 4);
|
|
871
|
+
const weights = readFloatAttribute(module, decoder, mesh, extension, 'WEIGHTS_0', 4);
|
|
872
|
+
const indices = readIndices(module, decoder, mesh);
|
|
873
|
+
throwIfGltfLoadAborted(options.signal);
|
|
874
|
+
return {
|
|
875
|
+
positions,
|
|
876
|
+
...(normals === null ? {} : { normals }),
|
|
877
|
+
textureCoordinates,
|
|
878
|
+
...(joints === null ? {} : { joints }),
|
|
879
|
+
...(weights === null ? {} : { weights }),
|
|
880
|
+
indices,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
finally {
|
|
884
|
+
module.destroy(mesh);
|
|
885
|
+
module.destroy(decoderBuffer);
|
|
886
|
+
module.destroy(decoder);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function resolveDracoDecoder(options) {
|
|
890
|
+
throwIfGltfLoadAborted(options.signal);
|
|
891
|
+
const provided = options.dracoDecoder;
|
|
892
|
+
if (provided) {
|
|
893
|
+
const module = typeof provided === 'function'
|
|
894
|
+
? await provided(options.dracoDecoderConfig)
|
|
895
|
+
: await provided;
|
|
896
|
+
throwIfGltfLoadAborted(options.signal);
|
|
897
|
+
return module;
|
|
898
|
+
}
|
|
899
|
+
if (!sharedDecoderPromise)
|
|
900
|
+
sharedDecoderPromise = loadDefaultDecoder(options.dracoDecoderConfig);
|
|
901
|
+
const module = await sharedDecoderPromise;
|
|
902
|
+
throwIfGltfLoadAborted(options.signal);
|
|
903
|
+
return module;
|
|
904
|
+
}
|
|
905
|
+
async function loadDefaultDecoder(config) {
|
|
906
|
+
let factory;
|
|
907
|
+
if (config?.scriptUrl) {
|
|
908
|
+
factory = await loadDecoderFactory(config.scriptUrl);
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
try {
|
|
912
|
+
factory = await loadDecoderFactory(DEFAULT_DRACO_DECODER_SCRIPT_URL);
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
factory = await loadDecoderFactory(FALLBACK_DRACO_DECODER_SCRIPT_URL);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return factory(config);
|
|
919
|
+
}
|
|
920
|
+
function loadDecoderFactory(scriptUrl) {
|
|
921
|
+
const existing = getGlobalDecoderFactory();
|
|
922
|
+
if (existing)
|
|
923
|
+
return Promise.resolve(existing);
|
|
924
|
+
const existingPromise = sharedScriptPromises.get(scriptUrl);
|
|
925
|
+
if (existingPromise)
|
|
926
|
+
return existingPromise;
|
|
927
|
+
const promise = new Promise((resolve, reject) => {
|
|
928
|
+
if (typeof document === 'undefined') {
|
|
929
|
+
const workerScope = globalThis;
|
|
930
|
+
if (typeof workerScope.postMessage !== 'function' || workerScope.location === undefined) {
|
|
931
|
+
reject(gltfDataError('Draco decoder is not available. Provide LoadGltfOptions.dracoDecoder in non-browser environments.'));
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
void loadWorkerDecoderFactory(scriptUrl).then(resolve, reject);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const script = document.createElement('script');
|
|
938
|
+
script.async = true;
|
|
939
|
+
script.src = scriptUrl;
|
|
940
|
+
script.onload = () => {
|
|
941
|
+
const factory = getGlobalDecoderFactory();
|
|
942
|
+
if (factory)
|
|
943
|
+
resolve(factory);
|
|
944
|
+
else
|
|
945
|
+
reject(gltfDataError(`Draco decoder script loaded without DracoDecoderModule: ${scriptUrl}`, { url: scriptUrl }));
|
|
946
|
+
};
|
|
947
|
+
script.onerror = () => reject(gltfDataError(`Failed to load Draco decoder script: ${scriptUrl}`, { url: scriptUrl }));
|
|
948
|
+
document.head.appendChild(script);
|
|
949
|
+
});
|
|
950
|
+
sharedScriptPromises.set(scriptUrl, promise);
|
|
951
|
+
promise.catch(() => sharedScriptPromises.delete(scriptUrl));
|
|
952
|
+
return promise;
|
|
953
|
+
}
|
|
954
|
+
async function loadWorkerDecoderFactory(scriptUrl) {
|
|
955
|
+
const response = await fetch(scriptUrl);
|
|
956
|
+
if (!response.ok) {
|
|
957
|
+
throw gltfDataError(`Failed to load Draco decoder script: ${scriptUrl}`, { url: scriptUrl, status: response.status });
|
|
958
|
+
}
|
|
959
|
+
const source = await response.text();
|
|
960
|
+
const factory = new Function(`${source}\nreturn typeof DracoDecoderModule === "function" ? DracoDecoderModule : null;\n//# sourceURL=${scriptUrl}`)();
|
|
961
|
+
if (typeof factory !== 'function') {
|
|
962
|
+
throw gltfDataError(`Draco decoder script loaded without DracoDecoderModule: ${scriptUrl}`, { url: scriptUrl });
|
|
963
|
+
}
|
|
964
|
+
return factory;
|
|
965
|
+
}
|
|
966
|
+
function getGlobalDecoderFactory() {
|
|
967
|
+
const globalScope = globalThis;
|
|
968
|
+
return typeof globalScope.DracoDecoderModule === 'function' ? globalScope.DracoDecoderModule : null;
|
|
969
|
+
}
|
|
970
|
+
function readUnsignedAttribute(module, decoder, mesh, extension, semantic, expectedSize) {
|
|
971
|
+
const uniqueId = extension.attributes[semantic];
|
|
972
|
+
if (uniqueId === undefined)
|
|
973
|
+
return null;
|
|
974
|
+
const attribute = decoder.GetAttributeByUniqueId(mesh, uniqueId);
|
|
975
|
+
if (!attribute)
|
|
976
|
+
throw gltfDataError(`Draco primitive is missing attribute ${semantic} with unique id ${uniqueId}.`);
|
|
977
|
+
const pointCount = mesh.num_points();
|
|
978
|
+
const decode = (ArrayType, ValuesType, read) => {
|
|
979
|
+
if (!ValuesType || !read)
|
|
980
|
+
return null;
|
|
981
|
+
const values = new ValuesType();
|
|
982
|
+
try {
|
|
983
|
+
if (!read.call(decoder, mesh, attribute, values))
|
|
984
|
+
throw gltfDataError(`Failed to decode Draco attribute ${semantic}.`);
|
|
985
|
+
if (values.size() < pointCount * expectedSize) {
|
|
986
|
+
throw gltfDataError(`Draco attribute ${semantic} returned ${values.size()} values, expected at least ${pointCount * expectedSize}.`);
|
|
987
|
+
}
|
|
988
|
+
const out = new ArrayType(pointCount * expectedSize);
|
|
989
|
+
for (let i = 0; i < out.length; i++)
|
|
990
|
+
out[i] = values.GetValue(i);
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
finally {
|
|
994
|
+
module.destroy(values);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
const u16 = decode(Uint16Array, module.DracoUInt16Array, decoder.GetAttributeUInt16ForAllPoints);
|
|
998
|
+
if (u16)
|
|
999
|
+
return u16;
|
|
1000
|
+
const u32 = decode(Uint32Array, module.DracoUInt32Array, decoder.GetAttributeUInt32ForAllPoints);
|
|
1001
|
+
if (u32)
|
|
1002
|
+
return u32;
|
|
1003
|
+
const values = readFloatAttribute(module, decoder, mesh, extension, semantic, expectedSize);
|
|
1004
|
+
if (!values)
|
|
1005
|
+
return null;
|
|
1006
|
+
const out = new Uint16Array(values.length);
|
|
1007
|
+
for (let i = 0; i < values.length; i++)
|
|
1008
|
+
out[i] = Math.max(0, Math.round(requiredFiniteValue(values, i, `Draco ${semantic}`)));
|
|
1009
|
+
return out;
|
|
1010
|
+
}
|
|
1011
|
+
function readFloatAttribute(module, decoder, mesh, extension, semantic, expectedSize) {
|
|
1012
|
+
const uniqueId = extension.attributes[semantic];
|
|
1013
|
+
if (uniqueId === undefined)
|
|
1014
|
+
return null;
|
|
1015
|
+
const attribute = decoder.GetAttributeByUniqueId(mesh, uniqueId);
|
|
1016
|
+
if (!attribute)
|
|
1017
|
+
throw gltfDataError(`Draco primitive is missing attribute ${semantic} with unique id ${uniqueId}.`);
|
|
1018
|
+
const values = new module.DracoFloat32Array();
|
|
1019
|
+
try {
|
|
1020
|
+
if (!decoder.GetAttributeFloatForAllPoints(mesh, attribute, values))
|
|
1021
|
+
throw gltfDataError(`Failed to decode Draco attribute ${semantic}.`);
|
|
1022
|
+
const pointCount = mesh.num_points();
|
|
1023
|
+
if (values.size() < pointCount * expectedSize) {
|
|
1024
|
+
throw gltfDataError(`Draco attribute ${semantic} returned ${values.size()} values, expected at least ${pointCount * expectedSize}.`);
|
|
1025
|
+
}
|
|
1026
|
+
const out = new Float32Array(pointCount * expectedSize);
|
|
1027
|
+
for (let i = 0; i < out.length; i++)
|
|
1028
|
+
out[i] = values.GetValue(i);
|
|
1029
|
+
return out;
|
|
1030
|
+
}
|
|
1031
|
+
finally {
|
|
1032
|
+
module.destroy(values);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function readIndices(module, decoder, mesh) {
|
|
1036
|
+
const faceCount = mesh.num_faces();
|
|
1037
|
+
const indices = mesh.num_points() > 65535 ? new Uint32Array(faceCount * 3) : new Uint16Array(faceCount * 3);
|
|
1038
|
+
const face = new module.DracoInt32Array();
|
|
1039
|
+
try {
|
|
1040
|
+
for (let faceIndex = 0; faceIndex < faceCount; faceIndex++) {
|
|
1041
|
+
if (!decoder.GetFaceFromMesh(mesh, faceIndex, face))
|
|
1042
|
+
throw gltfDataError(`Failed to decode Draco face ${faceIndex}.`);
|
|
1043
|
+
indices[faceIndex * 3] = face.GetValue(0);
|
|
1044
|
+
indices[faceIndex * 3 + 1] = face.GetValue(1);
|
|
1045
|
+
indices[faceIndex * 3 + 2] = face.GetValue(2);
|
|
1046
|
+
}
|
|
1047
|
+
return indices;
|
|
1048
|
+
}
|
|
1049
|
+
finally {
|
|
1050
|
+
module.destroy(face);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function requiredFiniteValue(values, index, label) {
|
|
1054
|
+
const value = values[index];
|
|
1055
|
+
if (value === undefined || !Number.isFinite(value))
|
|
1056
|
+
throw gltfDataError(`${label} is missing a finite value at index ${index}.`);
|
|
1057
|
+
return value;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async function createGltfMaterial(gltf, buffers, primitive, options, context, uvPlan) {
|
|
1061
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1062
|
+
const materialIndex = primitive.material;
|
|
1063
|
+
const gltfMaterial = materialIndex === undefined ? null : gltf.materials?.[materialIndex] ?? null;
|
|
1064
|
+
const createDescriptor = (material, materialPath) => createGltfMaterialDescriptor({
|
|
1065
|
+
gltf,
|
|
1066
|
+
material,
|
|
1067
|
+
primitive,
|
|
1068
|
+
materialPath,
|
|
1069
|
+
...(options.baseColorFactor === undefined ? {} : { defaultBaseColor: options.baseColorFactor }),
|
|
1070
|
+
adapters: context.extensionAdapters,
|
|
1071
|
+
resolveTexture: binding => ({
|
|
1072
|
+
source: createTextureSource(gltf, buffers, binding.textureInfo.index, binding.slot, context),
|
|
1073
|
+
mapping: resolveGltfTextureMapping(binding.textureInfo, primitive, binding.path, uvPlan),
|
|
1074
|
+
sampler: createSamplerDescriptor(gltf, binding.textureInfo.index),
|
|
1075
|
+
}),
|
|
1076
|
+
});
|
|
1077
|
+
const descriptor = createDescriptor(gltfMaterial, materialIndex === undefined ? 'gltf.defaultMaterial' : `gltf.materials[${materialIndex}]`);
|
|
1078
|
+
const variants = collectGltfMaterialVariantReferences(gltf, primitive, context.extensionAdapters).map(variant => ({
|
|
1079
|
+
name: variant.name,
|
|
1080
|
+
state: createDescriptor(gltf.materials?.[variant.materialIndex] ?? null, `gltf.materials[${variant.materialIndex}]`).state,
|
|
1081
|
+
}));
|
|
1082
|
+
return createMaterialFromDescriptor({ ...descriptor, variants });
|
|
1083
|
+
}
|
|
1084
|
+
function resolveGltfTextureMapping(textureInfo, primitive, path, uvPlan) {
|
|
1085
|
+
const transform = textureInfo.extensions?.KHR_texture_transform;
|
|
1086
|
+
const textureCoord = transform?.texCoord ?? textureInfo.texCoord ?? 0;
|
|
1087
|
+
const texCoordPath = transform?.texCoord === undefined
|
|
1088
|
+
? `${path}.texCoord`
|
|
1089
|
+
: `${path}.extensions.KHR_texture_transform.texCoord`;
|
|
1090
|
+
const semantic = `TEXCOORD_${textureCoord}`;
|
|
1091
|
+
if (primitive.attributes[semantic] === undefined) {
|
|
1092
|
+
throw gltfDataError(`Texture references ${semantic}, but the mesh primitive does not provide that attribute.`, { texCoord: textureCoord, semantic }, texCoordPath);
|
|
1093
|
+
}
|
|
1094
|
+
const channel = getGltfUvChannel(uvPlan, textureCoord);
|
|
1095
|
+
if (channel === null) {
|
|
1096
|
+
throw gltfDataError(`${semantic} was not assigned to a physical PBR UV channel.`, { texCoord: textureCoord, semantic, capacity: uvPlan.capacity }, texCoordPath);
|
|
1097
|
+
}
|
|
1098
|
+
return {
|
|
1099
|
+
texCoord: channel,
|
|
1100
|
+
offset: transform?.offset ?? [0, 0],
|
|
1101
|
+
rotation: transform?.rotation ?? 0,
|
|
1102
|
+
scale: transform?.scale ?? [1, 1],
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function createSamplerDescriptor(gltf, textureIndex) {
|
|
1106
|
+
const texture = gltf.textures?.[textureIndex];
|
|
1107
|
+
const sampler = texture?.sampler === undefined ? null : gltf.samplers?.[texture.sampler] ?? null;
|
|
1108
|
+
return {
|
|
1109
|
+
addressModeU: mapWrapMode(sampler?.wrapS),
|
|
1110
|
+
addressModeV: mapWrapMode(sampler?.wrapT),
|
|
1111
|
+
magFilter: mapMagFilter(sampler?.magFilter),
|
|
1112
|
+
minFilter: mapMinFilter(sampler?.minFilter),
|
|
1113
|
+
mipmapFilter: mapMipmapFilter(sampler?.minFilter),
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function mapWrapMode(mode) {
|
|
1117
|
+
switch (mode) {
|
|
1118
|
+
case 33071: return 'clamp-to-edge';
|
|
1119
|
+
case 33648: return 'mirror-repeat';
|
|
1120
|
+
case 10497:
|
|
1121
|
+
case undefined:
|
|
1122
|
+
return 'repeat';
|
|
1123
|
+
default:
|
|
1124
|
+
return 'repeat';
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function mapMagFilter(filter) {
|
|
1128
|
+
return filter === 9728 ? 'nearest' : 'linear';
|
|
1129
|
+
}
|
|
1130
|
+
function mapMinFilter(filter) {
|
|
1131
|
+
switch (filter) {
|
|
1132
|
+
case 9728:
|
|
1133
|
+
case 9984:
|
|
1134
|
+
case 9986:
|
|
1135
|
+
return 'nearest';
|
|
1136
|
+
default:
|
|
1137
|
+
return 'linear';
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function mapMipmapFilter(filter) {
|
|
1141
|
+
switch (filter) {
|
|
1142
|
+
case 9984:
|
|
1143
|
+
case 9985:
|
|
1144
|
+
return 'nearest';
|
|
1145
|
+
case 9986:
|
|
1146
|
+
case 9987:
|
|
1147
|
+
return 'linear';
|
|
1148
|
+
default:
|
|
1149
|
+
return 'linear';
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function preloadGltfTextures(gltf, buffers, options, context) {
|
|
1153
|
+
const assetManager = options.assetManager;
|
|
1154
|
+
if (!assetManager)
|
|
1155
|
+
return;
|
|
1156
|
+
const requests = new Map();
|
|
1157
|
+
for (const mesh of gltf.meshes ?? []) {
|
|
1158
|
+
for (const primitive of mesh.primitives) {
|
|
1159
|
+
const materialIndexes = new Set();
|
|
1160
|
+
if (primitive.material !== undefined)
|
|
1161
|
+
materialIndexes.add(primitive.material);
|
|
1162
|
+
for (const variant of collectGltfMaterialVariantReferences(gltf, primitive, context.extensionAdapters)) {
|
|
1163
|
+
materialIndexes.add(variant.materialIndex);
|
|
1164
|
+
}
|
|
1165
|
+
for (const materialIndex of materialIndexes) {
|
|
1166
|
+
const material = gltf.materials?.[materialIndex] ?? null;
|
|
1167
|
+
for (const binding of collectGltfMaterialTextureBindings(gltf, material, primitive, `gltf.materials[${materialIndex}]`, context.extensionAdapters)) {
|
|
1168
|
+
const reference = resolveTextureReference(gltf, buffers, binding.textureInfo.index, binding.slot, context);
|
|
1169
|
+
if (reference)
|
|
1170
|
+
requests.set(reference.requestKey, reference);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
const results = await Promise.allSettled([...requests.values()].map(async (reference) => ({
|
|
1176
|
+
reference,
|
|
1177
|
+
handle: await assetManager.loadTexture(reference.source, {
|
|
1178
|
+
cacheKey: reference.cacheKey,
|
|
1179
|
+
format: reference.format,
|
|
1180
|
+
mipmaps: 'generate',
|
|
1181
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
1182
|
+
}),
|
|
1183
|
+
})));
|
|
1184
|
+
const loaded = [];
|
|
1185
|
+
let firstError = null;
|
|
1186
|
+
for (const result of results) {
|
|
1187
|
+
if (result.status === 'fulfilled')
|
|
1188
|
+
loaded.push(result.value);
|
|
1189
|
+
else if (firstError === null)
|
|
1190
|
+
firstError = result.reason;
|
|
1191
|
+
}
|
|
1192
|
+
if (firstError !== null) {
|
|
1193
|
+
for (const { handle } of loaded)
|
|
1194
|
+
handle.release();
|
|
1195
|
+
throw firstError;
|
|
1196
|
+
}
|
|
1197
|
+
for (const { reference, handle } of loaded) {
|
|
1198
|
+
context.preloadedTextures.set(reference.requestKey, handle.value);
|
|
1199
|
+
context.assetHandles.push(handle);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function createTextureSource(gltf, buffers, textureIndex, slot, context) {
|
|
1203
|
+
const reference = resolveTextureReference(gltf, buffers, textureIndex, slot, context);
|
|
1204
|
+
if (!reference)
|
|
1205
|
+
return null;
|
|
1206
|
+
return context.preloadedTextures.get(reference.requestKey) ?? reference.source;
|
|
1207
|
+
}
|
|
1208
|
+
function resolveTextureReference(gltf, buffers, textureIndex, slot, context) {
|
|
1209
|
+
const texture = gltf.textures?.[textureIndex];
|
|
1210
|
+
const basisuSource = texture?.extensions?.KHR_texture_basisu?.source;
|
|
1211
|
+
const imageIndex = basisuSource ?? texture?.source;
|
|
1212
|
+
if (imageIndex === undefined)
|
|
1213
|
+
return null;
|
|
1214
|
+
const image = gltf.images?.[imageIndex] ?? null;
|
|
1215
|
+
if (!image)
|
|
1216
|
+
return null;
|
|
1217
|
+
const compressed = basisuSource !== undefined || image.mimeType === 'image/ktx2';
|
|
1218
|
+
const src = createImageUrl(gltf, buffers, imageIndex, image, context);
|
|
1219
|
+
if (!src)
|
|
1220
|
+
return null;
|
|
1221
|
+
const source = compressed
|
|
1222
|
+
? { kind: 'compressed-texture', type: 'texture/ktx2', src }
|
|
1223
|
+
: src;
|
|
1224
|
+
const cacheKey = createImageCacheKey(imageIndex, image, context);
|
|
1225
|
+
const format = getPbrTextureFormat(slot);
|
|
1226
|
+
return {
|
|
1227
|
+
source,
|
|
1228
|
+
cacheKey,
|
|
1229
|
+
requestKey: compressed ? `compressed:${cacheKey}` : `${format}:${cacheKey}`,
|
|
1230
|
+
format,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function createImageCacheKey(imageIndex, image, context) {
|
|
1234
|
+
if (image.uri) {
|
|
1235
|
+
const url = image.uri.startsWith('data:') || image.uri.startsWith('blob:')
|
|
1236
|
+
? image.uri
|
|
1237
|
+
: new URL(image.uri, context.baseUrl).href;
|
|
1238
|
+
return `url:${url}`;
|
|
1239
|
+
}
|
|
1240
|
+
return `gltf:${context.assetIdentity}:image:${imageIndex}`;
|
|
1241
|
+
}
|
|
1242
|
+
function createImageUrl(gltf, buffers, imageIndex, image, context) {
|
|
1243
|
+
const cached = context.imageSources.get(imageIndex);
|
|
1244
|
+
if (typeof cached === 'string')
|
|
1245
|
+
return cached;
|
|
1246
|
+
if (image.uri) {
|
|
1247
|
+
const url = image.uri.startsWith('data:') || image.uri.startsWith('blob:')
|
|
1248
|
+
? image.uri
|
|
1249
|
+
: new URL(image.uri, context.baseUrl).href;
|
|
1250
|
+
context.imageSources.set(imageIndex, url);
|
|
1251
|
+
return url;
|
|
1252
|
+
}
|
|
1253
|
+
if (image.bufferView === undefined)
|
|
1254
|
+
return null;
|
|
1255
|
+
const view = gltf.bufferViews?.[image.bufferView];
|
|
1256
|
+
if (!view)
|
|
1257
|
+
return null;
|
|
1258
|
+
const buffer = buffers[view.buffer];
|
|
1259
|
+
if (!buffer)
|
|
1260
|
+
throw gltfDataError(`Image bufferView references missing buffer ${view.buffer}.`);
|
|
1261
|
+
const bytes = new Uint8Array(buffer, view.byteOffset ?? 0, view.byteLength);
|
|
1262
|
+
const blob = new Blob([bytes.slice()], { type: image.mimeType || 'application/octet-stream' });
|
|
1263
|
+
const url = URL.createObjectURL(blob);
|
|
1264
|
+
if (!context.sharedImageSources)
|
|
1265
|
+
context.objectUrls.push(url);
|
|
1266
|
+
context.imageSources.set(imageIndex, url);
|
|
1267
|
+
return url;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const GLTF_EXTENSION_CAPABILITIES = createGltfExtensionCapabilities(DEFAULT_GLTF_EXTENSION_ADAPTERS);
|
|
1271
|
+
function isGltfParsedAsset(value) {
|
|
1272
|
+
if (!isRecord(value))
|
|
1273
|
+
return false;
|
|
1274
|
+
if (!isGltfAsset(value.gltf))
|
|
1275
|
+
return false;
|
|
1276
|
+
if (!(value.binaryChunk === null || value.binaryChunk instanceof ArrayBuffer))
|
|
1277
|
+
return false;
|
|
1278
|
+
if (!Array.isArray(value.buffers) || !value.buffers.every(buffer => buffer instanceof ArrayBuffer))
|
|
1279
|
+
return false;
|
|
1280
|
+
if (typeof value.baseUrl !== 'string')
|
|
1281
|
+
return false;
|
|
1282
|
+
if (value.geometryPayloads !== undefined && !isGeometryPayloadMatrix(value.geometryPayloads))
|
|
1283
|
+
return false;
|
|
1284
|
+
return value.metrics === undefined || isParsedAssetMetrics(value.metrics);
|
|
1285
|
+
}
|
|
1286
|
+
function isGeometryPayloadMatrix(value) {
|
|
1287
|
+
return Array.isArray(value) && value.every(mesh => Array.isArray(mesh) && mesh.every(payload => {
|
|
1288
|
+
if (payload === null)
|
|
1289
|
+
return true;
|
|
1290
|
+
if (!isRecord(payload) || !(payload.positions instanceof Float32Array))
|
|
1291
|
+
return false;
|
|
1292
|
+
if (payload.indices !== undefined && !(payload.indices instanceof Uint16Array) && !(payload.indices instanceof Uint32Array))
|
|
1293
|
+
return false;
|
|
1294
|
+
if (payload.normals !== undefined && !(payload.normals instanceof Float32Array))
|
|
1295
|
+
return false;
|
|
1296
|
+
if (!Array.isArray(payload.textureCoordinates)
|
|
1297
|
+
|| !payload.textureCoordinates.every(entry => isRecord(entry)
|
|
1298
|
+
&& typeof entry.set === 'number'
|
|
1299
|
+
&& Number.isInteger(entry.set)
|
|
1300
|
+
&& entry.set >= 0
|
|
1301
|
+
&& entry.data instanceof Float32Array))
|
|
1302
|
+
return false;
|
|
1303
|
+
if (!Array.isArray(payload.positionTargets) || !payload.positionTargets.every(target => target instanceof Float32Array))
|
|
1304
|
+
return false;
|
|
1305
|
+
return Array.isArray(payload.normalTargets) && payload.normalTargets.every(target => target instanceof Float32Array);
|
|
1306
|
+
}));
|
|
1307
|
+
}
|
|
1308
|
+
function isParsedAssetMetrics(value) {
|
|
1309
|
+
if (!isRecord(value))
|
|
1310
|
+
return false;
|
|
1311
|
+
return [
|
|
1312
|
+
'fetchMs',
|
|
1313
|
+
'parseMs',
|
|
1314
|
+
'workerParseMs',
|
|
1315
|
+
'dracoDecodeMs',
|
|
1316
|
+
'geometryPreparationMs',
|
|
1317
|
+
'sourceBytes',
|
|
1318
|
+
'workerTransferBytes',
|
|
1319
|
+
'workerTransferBufferCount',
|
|
1320
|
+
].every(key => typeof value[key] === 'number' && Number.isFinite(value[key]) && value[key] >= 0);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const sharedImageSourcesByAsset = new WeakMap();
|
|
1324
|
+
async function loadGltfModel(src, options = {}) {
|
|
1325
|
+
const loadStartedAt = nowMilliseconds();
|
|
1326
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1327
|
+
const extensionAdapters = resolveGltfExtensionAdapters(options.extensionAdapters);
|
|
1328
|
+
const extensionCapabilities = createGltfExtensionCapabilities(extensionAdapters);
|
|
1329
|
+
let parsedAssetHandle = null;
|
|
1330
|
+
let parsedAsset;
|
|
1331
|
+
const assetManager = options.assetManager ?? null;
|
|
1332
|
+
try {
|
|
1333
|
+
if (assetManager) {
|
|
1334
|
+
parsedAssetHandle = await assetManager.load(`gltf:parsed:${resolveAssetIdentity(src)}`, signal => loadParsedAssetForModel(src, options, signal), asset => disposeSharedImageSources(asset, assetManager), { signal: options.signal });
|
|
1335
|
+
parsedAsset = parsedAssetHandle.value;
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
parsedAsset = await loadParsedAssetForModel(src, options, options.signal);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
catch (error) {
|
|
1342
|
+
throw attachGltfSource(error, src);
|
|
1343
|
+
}
|
|
1344
|
+
const { gltf, buffers, baseUrl, geometryPayloads } = parsedAsset;
|
|
1345
|
+
let extensionInspection;
|
|
1346
|
+
try {
|
|
1347
|
+
extensionInspection = validateGltfExtensions(gltf, extensionCapabilities);
|
|
1348
|
+
}
|
|
1349
|
+
catch (error) {
|
|
1350
|
+
parsedAssetHandle?.release();
|
|
1351
|
+
throw attachGltfSource(error, src);
|
|
1352
|
+
}
|
|
1353
|
+
try {
|
|
1354
|
+
for (const warning of extensionInspection.warnings)
|
|
1355
|
+
options.onWarning?.(warning);
|
|
1356
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1357
|
+
}
|
|
1358
|
+
catch (error) {
|
|
1359
|
+
parsedAssetHandle?.release();
|
|
1360
|
+
throw attachGltfSource(error, src);
|
|
1361
|
+
}
|
|
1362
|
+
const animationTargets = new Map();
|
|
1363
|
+
const context = {
|
|
1364
|
+
baseUrl,
|
|
1365
|
+
assetIdentity: resolveAssetIdentity(src),
|
|
1366
|
+
objectUrls: [],
|
|
1367
|
+
imageSources: parsedAssetHandle && assetManager
|
|
1368
|
+
? getSharedImageSources(parsedAsset, assetManager)
|
|
1369
|
+
: new Map(),
|
|
1370
|
+
sharedImageSources: parsedAssetHandle !== null,
|
|
1371
|
+
preloadedTextures: new Map(),
|
|
1372
|
+
animationTargets,
|
|
1373
|
+
skinnedPrimitives: [],
|
|
1374
|
+
pendingSkinnedPrimitives: [],
|
|
1375
|
+
assetHandles: parsedAssetHandle ? [parsedAssetHandle] : [],
|
|
1376
|
+
...(geometryPayloads === undefined ? {} : { geometryPayloads }),
|
|
1377
|
+
compatibilityPrimitives: [],
|
|
1378
|
+
extensionAdapters,
|
|
1379
|
+
};
|
|
1380
|
+
const root = new Entity('glTF Model');
|
|
1381
|
+
try {
|
|
1382
|
+
let mainThreadDracoDecodeMs = 0;
|
|
1383
|
+
const geometryOptions = geometryPayloads === undefined
|
|
1384
|
+
? {
|
|
1385
|
+
...options,
|
|
1386
|
+
diagnostics: {
|
|
1387
|
+
onDracoDecode(durationMs, decodedBytes) {
|
|
1388
|
+
mainThreadDracoDecodeMs += durationMs;
|
|
1389
|
+
options.diagnostics?.onDracoDecode?.(durationMs, decodedBytes);
|
|
1390
|
+
},
|
|
1391
|
+
},
|
|
1392
|
+
}
|
|
1393
|
+
: options;
|
|
1394
|
+
let geometryPreparationMs = parsedAsset.metrics?.geometryPreparationMs ?? 0;
|
|
1395
|
+
const geometryTask = geometryPayloads === undefined
|
|
1396
|
+
? (async () => {
|
|
1397
|
+
const startedAt = nowMilliseconds();
|
|
1398
|
+
try {
|
|
1399
|
+
return await prepareGltfGeometryPayloads(gltf, buffers, geometryOptions);
|
|
1400
|
+
}
|
|
1401
|
+
finally {
|
|
1402
|
+
geometryPreparationMs = nowMilliseconds() - startedAt;
|
|
1403
|
+
}
|
|
1404
|
+
})()
|
|
1405
|
+
: Promise.resolve(geometryPayloads);
|
|
1406
|
+
let textureDecodeTranscodeUploadMs = 0;
|
|
1407
|
+
const textureTask = (async () => {
|
|
1408
|
+
const startedAt = nowMilliseconds();
|
|
1409
|
+
try {
|
|
1410
|
+
await preloadGltfTextures(gltf, buffers, options, context);
|
|
1411
|
+
}
|
|
1412
|
+
finally {
|
|
1413
|
+
textureDecodeTranscodeUploadMs = nowMilliseconds() - startedAt;
|
|
1414
|
+
}
|
|
1415
|
+
})();
|
|
1416
|
+
const [geometryResult, textureResult] = await Promise.all([
|
|
1417
|
+
settleTask(geometryTask),
|
|
1418
|
+
settleTask(textureTask),
|
|
1419
|
+
]);
|
|
1420
|
+
if (!geometryResult.ok)
|
|
1421
|
+
throw geometryResult.error;
|
|
1422
|
+
if (!textureResult.ok)
|
|
1423
|
+
throw textureResult.error;
|
|
1424
|
+
context.geometryPayloads = geometryResult.value;
|
|
1425
|
+
const instantiateStartedAt = nowMilliseconds();
|
|
1426
|
+
root.addComponent(new CartesianTransform3D());
|
|
1427
|
+
const sceneIndex = options.scene ?? gltf.scene ?? 0;
|
|
1428
|
+
const scene = gltf.scenes?.[sceneIndex] ?? gltf.scenes?.[0];
|
|
1429
|
+
const rootNodes = await Promise.all((scene?.nodes ?? []).map(nodeIndex => instantiateNode(gltf, buffers, nodeIndex, options, animationTargets, context)));
|
|
1430
|
+
for (const child of rootNodes) {
|
|
1431
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1432
|
+
if (child)
|
|
1433
|
+
root.addChild(child);
|
|
1434
|
+
}
|
|
1435
|
+
resolvePendingSkins(gltf, buffers, context);
|
|
1436
|
+
const animationClips = createAnimationClips(gltf, buffers, animationTargets, context.skinnedPrimitives);
|
|
1437
|
+
const instantiateMs = nowMilliseconds() - instantiateStartedAt;
|
|
1438
|
+
const decodedGeometryBytes = countDecodedGeometryBytes(context.compatibilityPrimitives);
|
|
1439
|
+
const parsedMetrics = parsedAsset.metrics;
|
|
1440
|
+
const dracoDecodeMs = (parsedMetrics?.dracoDecodeMs ?? 0) + mainThreadDracoDecodeMs;
|
|
1441
|
+
const compatibilityReport = createGltfCompatibilityReport(gltf, extensionInspection.report.entries, context.compatibilityPrimitives, {
|
|
1442
|
+
loadMs: nowMilliseconds() - loadStartedAt,
|
|
1443
|
+
decodedGeometryBytes,
|
|
1444
|
+
});
|
|
1445
|
+
return {
|
|
1446
|
+
root,
|
|
1447
|
+
animations: animationClips.map(clip => ({ name: clip.name, duration: clip.duration, channelCount: clip.channels.length })),
|
|
1448
|
+
animationClips,
|
|
1449
|
+
assetStats: collectAssetStats(gltf),
|
|
1450
|
+
loadMetrics: Object.freeze({
|
|
1451
|
+
timings: Object.freeze({
|
|
1452
|
+
fetchMs: parsedMetrics?.fetchMs ?? 0,
|
|
1453
|
+
workerParseMs: parsedMetrics?.workerParseMs ?? 0,
|
|
1454
|
+
dracoDecodeMs,
|
|
1455
|
+
geometryPreparationMs: geometryPayloads === undefined
|
|
1456
|
+
? Math.max(0, geometryPreparationMs - mainThreadDracoDecodeMs)
|
|
1457
|
+
: parsedMetrics?.geometryPreparationMs ?? 0,
|
|
1458
|
+
instantiateMs,
|
|
1459
|
+
textureDecodeTranscodeUploadMs,
|
|
1460
|
+
totalMs: nowMilliseconds() - loadStartedAt,
|
|
1461
|
+
}),
|
|
1462
|
+
sourceBytes: parsedMetrics?.sourceBytes ?? countUniqueBufferBytes(buffers),
|
|
1463
|
+
decodedGeometryBytes,
|
|
1464
|
+
workerTransferBytes: parsedMetrics?.workerTransferBytes ?? 0,
|
|
1465
|
+
workerTransferBufferCount: parsedMetrics?.workerTransferBufferCount ?? 0,
|
|
1466
|
+
}),
|
|
1467
|
+
objectUrls: context.objectUrls,
|
|
1468
|
+
assetHandles: context.assetHandles,
|
|
1469
|
+
materialVariants: collectGltfMaterialVariantNames(gltf, extensionAdapters),
|
|
1470
|
+
warnings: extensionInspection.warnings,
|
|
1471
|
+
extensionReport: extensionInspection.report,
|
|
1472
|
+
compatibilityReport,
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
catch (error) {
|
|
1476
|
+
disposeEntityTree(root);
|
|
1477
|
+
revokeObjectUrls(context.objectUrls);
|
|
1478
|
+
releaseAssetHandles(context.assetHandles);
|
|
1479
|
+
throw attachGltfSource(error, src);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
async function loadParsedAssetForModel(src, options, signal) {
|
|
1483
|
+
const assetWorker = options.assetWorker && !isInlineOrBlobUri(src) ? options.assetWorker : null;
|
|
1484
|
+
return parseAssetWorkerFirst({
|
|
1485
|
+
parser: { type: 'model/gltf', parse: (_input, context) => loadParsedGltfAsset(src, context.signal) },
|
|
1486
|
+
input: src,
|
|
1487
|
+
context: { ...(signal === undefined ? {} : { signal }), source: src },
|
|
1488
|
+
worker: assetWorker
|
|
1489
|
+
? (_input, context) => assetWorker.loadParsedAsset(src, {
|
|
1490
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
1491
|
+
...(options.dracoDecoderConfig === undefined ? {} : { dracoDecoderConfig: options.dracoDecoderConfig }),
|
|
1492
|
+
})
|
|
1493
|
+
: null,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
function resolveAssetIdentity(src) {
|
|
1497
|
+
try {
|
|
1498
|
+
const fallback = typeof globalThis.location?.href === 'string' ? globalThis.location.href : undefined;
|
|
1499
|
+
return fallback === undefined ? new URL(src).href : new URL(src, fallback).href;
|
|
1500
|
+
}
|
|
1501
|
+
catch {
|
|
1502
|
+
return src;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
function getSharedImageSources(asset, manager) {
|
|
1506
|
+
let byManager = sharedImageSourcesByAsset.get(asset);
|
|
1507
|
+
if (!byManager) {
|
|
1508
|
+
byManager = new WeakMap();
|
|
1509
|
+
sharedImageSourcesByAsset.set(asset, byManager);
|
|
1510
|
+
}
|
|
1511
|
+
let sources = byManager.get(manager);
|
|
1512
|
+
if (!sources) {
|
|
1513
|
+
sources = new Map();
|
|
1514
|
+
byManager.set(manager, sources);
|
|
1515
|
+
}
|
|
1516
|
+
return sources;
|
|
1517
|
+
}
|
|
1518
|
+
function disposeSharedImageSources(asset, manager) {
|
|
1519
|
+
const sources = sharedImageSourcesByAsset.get(asset)?.get(manager);
|
|
1520
|
+
if (!sources)
|
|
1521
|
+
return;
|
|
1522
|
+
for (const url of sources.values()) {
|
|
1523
|
+
if (url.startsWith('blob:'))
|
|
1524
|
+
URL.revokeObjectURL(url);
|
|
1525
|
+
}
|
|
1526
|
+
sources.clear();
|
|
1527
|
+
sharedImageSourcesByAsset.get(asset)?.delete(manager);
|
|
1528
|
+
}
|
|
1529
|
+
async function settleTask(task) {
|
|
1530
|
+
try {
|
|
1531
|
+
return { ok: true, value: await task };
|
|
1532
|
+
}
|
|
1533
|
+
catch (error) {
|
|
1534
|
+
return { ok: false, error };
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const nowMilliseconds = monotonicNow;
|
|
1538
|
+
function countDecodedGeometryBytes(primitives) {
|
|
1539
|
+
const buffers = new Set();
|
|
1540
|
+
const add = (value) => {
|
|
1541
|
+
if (value)
|
|
1542
|
+
buffers.add(value.buffer);
|
|
1543
|
+
};
|
|
1544
|
+
for (const { geometry } of primitives) {
|
|
1545
|
+
add(geometry.positions);
|
|
1546
|
+
add(geometry.normals);
|
|
1547
|
+
add(geometry.indices);
|
|
1548
|
+
add(geometry.morphBasePositions);
|
|
1549
|
+
add(geometry.morphBaseNormals);
|
|
1550
|
+
for (const value of geometry.textureCoordinates.values())
|
|
1551
|
+
add(value);
|
|
1552
|
+
for (const target of geometry.morphTargets) {
|
|
1553
|
+
add(target.positions);
|
|
1554
|
+
add(target.normals);
|
|
1555
|
+
}
|
|
1556
|
+
add(geometry.skinning?.joints);
|
|
1557
|
+
add(geometry.skinning?.weights);
|
|
1558
|
+
add(geometry.skinning?.jointMatrices);
|
|
1559
|
+
}
|
|
1560
|
+
let total = 0;
|
|
1561
|
+
for (const buffer of buffers)
|
|
1562
|
+
total += buffer.byteLength;
|
|
1563
|
+
return total;
|
|
1564
|
+
}
|
|
1565
|
+
function countUniqueBufferBytes(buffers) {
|
|
1566
|
+
let total = 0;
|
|
1567
|
+
for (const buffer of new Set(buffers))
|
|
1568
|
+
total += buffer.byteLength;
|
|
1569
|
+
return total;
|
|
1570
|
+
}
|
|
1571
|
+
function isInlineOrBlobUri(src) {
|
|
1572
|
+
return /^(data:|blob:)/i.test(src);
|
|
1573
|
+
}
|
|
1574
|
+
function disposeGltfModel(model) {
|
|
1575
|
+
disposeEntityTree(model.root);
|
|
1576
|
+
revokeObjectUrls(model.objectUrls);
|
|
1577
|
+
releaseAssetHandles(model.assetHandles);
|
|
1578
|
+
model.animations.length = 0;
|
|
1579
|
+
model.animationClips.length = 0;
|
|
1580
|
+
}
|
|
1581
|
+
function setGltfMaterialVariant(model, name) {
|
|
1582
|
+
if (name !== null && !model.materialVariants.includes(name)) {
|
|
1583
|
+
throw new RangeError(`Unknown glTF material variant "${name}".`);
|
|
1584
|
+
}
|
|
1585
|
+
visitEntityTree(model.root, entity => {
|
|
1586
|
+
const material = entity.getComponent(Mesh3D)?.material;
|
|
1587
|
+
if (!(material instanceof PbrMaterial))
|
|
1588
|
+
return;
|
|
1589
|
+
material.setVariant(name !== null && material.variantNames.includes(name) ? name : null);
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
function visitEntityTree(entity, visit) {
|
|
1593
|
+
visit(entity);
|
|
1594
|
+
for (const child of entity.children)
|
|
1595
|
+
visitEntityTree(child, visit);
|
|
1596
|
+
}
|
|
1597
|
+
function releaseAssetHandles(handles) {
|
|
1598
|
+
for (const handle of handles)
|
|
1599
|
+
handle.release();
|
|
1600
|
+
handles.length = 0;
|
|
1601
|
+
}
|
|
1602
|
+
function revokeObjectUrls(urls) {
|
|
1603
|
+
for (const url of urls)
|
|
1604
|
+
URL.revokeObjectURL(url);
|
|
1605
|
+
urls.length = 0;
|
|
1606
|
+
}
|
|
1607
|
+
function disposeEntityTree(entity) {
|
|
1608
|
+
const children = [...entity.children];
|
|
1609
|
+
for (const child of children)
|
|
1610
|
+
disposeEntityTree(child);
|
|
1611
|
+
entity.destroy();
|
|
1612
|
+
}
|
|
1613
|
+
function collectAssetStats(gltf) {
|
|
1614
|
+
return {
|
|
1615
|
+
meshCount: gltf.meshes?.length ?? 0,
|
|
1616
|
+
primitiveCount: (gltf.meshes ?? []).reduce((sum, mesh) => sum + mesh.primitives.length, 0),
|
|
1617
|
+
materialCount: gltf.materials?.length ?? 0,
|
|
1618
|
+
textureCount: gltf.textures?.length ?? 0,
|
|
1619
|
+
imageCount: gltf.images?.length ?? 0,
|
|
1620
|
+
animationCount: gltf.animations?.length ?? 0,
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
async function loadGltfAsset(src, signal) {
|
|
1624
|
+
const baseUrl = resolveBaseUrl(src);
|
|
1625
|
+
const fetchStartedAt = nowMilliseconds();
|
|
1626
|
+
const response = await fetch(src, signal ? { signal } : undefined);
|
|
1627
|
+
if (!response.ok)
|
|
1628
|
+
throw gltfDataError(`Failed to load glTF: ${response.status} ${response.statusText}`, { url: src, status: response.status }, 'gltf');
|
|
1629
|
+
throwIfGltfLoadAborted(signal);
|
|
1630
|
+
const bytes = await response.arrayBuffer();
|
|
1631
|
+
const fetchMs = nowMilliseconds() - fetchStartedAt;
|
|
1632
|
+
throwIfGltfLoadAborted(signal);
|
|
1633
|
+
const parseStartedAt = nowMilliseconds();
|
|
1634
|
+
if (isGlb(bytes)) {
|
|
1635
|
+
const parsed = parseGlb(bytes);
|
|
1636
|
+
return {
|
|
1637
|
+
...parsed,
|
|
1638
|
+
baseUrl,
|
|
1639
|
+
fetchMs,
|
|
1640
|
+
parseMs: nowMilliseconds() - parseStartedAt,
|
|
1641
|
+
sourceBytes: bytes.byteLength,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
const text = new TextDecoder().decode(bytes);
|
|
1645
|
+
const gltf = parseGltfJson(text, src, 'gltf');
|
|
1646
|
+
return {
|
|
1647
|
+
gltf,
|
|
1648
|
+
binaryChunk: null,
|
|
1649
|
+
baseUrl,
|
|
1650
|
+
fetchMs,
|
|
1651
|
+
parseMs: nowMilliseconds() - parseStartedAt,
|
|
1652
|
+
sourceBytes: bytes.byteLength,
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
async function loadParsedGltfAsset(src, signal) {
|
|
1656
|
+
const { gltf, binaryChunk, baseUrl, fetchMs: primaryFetchMs, parseMs, sourceBytes } = await loadGltfAsset(src, signal);
|
|
1657
|
+
throwIfGltfLoadAborted(signal);
|
|
1658
|
+
const buffersStartedAt = nowMilliseconds();
|
|
1659
|
+
const buffers = await loadBuffers(gltf, binaryChunk, baseUrl, signal);
|
|
1660
|
+
const bufferFetchMs = nowMilliseconds() - buffersStartedAt;
|
|
1661
|
+
return {
|
|
1662
|
+
gltf,
|
|
1663
|
+
binaryChunk,
|
|
1664
|
+
buffers,
|
|
1665
|
+
baseUrl,
|
|
1666
|
+
metrics: Object.freeze({
|
|
1667
|
+
fetchMs: primaryFetchMs + bufferFetchMs,
|
|
1668
|
+
parseMs,
|
|
1669
|
+
workerParseMs: parseMs,
|
|
1670
|
+
dracoDecodeMs: 0,
|
|
1671
|
+
geometryPreparationMs: 0,
|
|
1672
|
+
sourceBytes: sourceBytes + countExternalBufferBytes(gltf, buffers),
|
|
1673
|
+
workerTransferBytes: 0,
|
|
1674
|
+
workerTransferBufferCount: 0,
|
|
1675
|
+
}),
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function countExternalBufferBytes(gltf, buffers) {
|
|
1679
|
+
let total = 0;
|
|
1680
|
+
for (const [index, descriptor] of (gltf.buffers ?? []).entries()) {
|
|
1681
|
+
if (!descriptor.uri || descriptor.uri.startsWith('data:'))
|
|
1682
|
+
continue;
|
|
1683
|
+
total += buffers[index]?.byteLength ?? 0;
|
|
1684
|
+
}
|
|
1685
|
+
return total;
|
|
1686
|
+
}
|
|
1687
|
+
function validateGltfExtensions(gltf, capabilities) {
|
|
1688
|
+
const usedExtensions = new Set(gltf.extensionsUsed ?? []);
|
|
1689
|
+
const requiredExtensions = new Set(gltf.extensionsRequired ?? []);
|
|
1690
|
+
for (const [index, extension] of (gltf.extensionsRequired ?? []).entries()) {
|
|
1691
|
+
if (!usedExtensions.has(extension)) {
|
|
1692
|
+
throw gltfDataError(`Required glTF extension "${extension}" is missing from extensionsUsed.`, { extension }, `gltf.extensionsRequired[${index}]`);
|
|
1693
|
+
}
|
|
1694
|
+
const capability = capabilities[extension];
|
|
1695
|
+
if (capability?.support === 'supported')
|
|
1696
|
+
continue;
|
|
1697
|
+
throw gltfDataError(`Required glTF extension "${extension}" is not supported.`, { extension, support: capability?.support ?? 'unknown' }, `gltf.extensionsRequired[${index}]`);
|
|
1698
|
+
}
|
|
1699
|
+
const warnings = [];
|
|
1700
|
+
for (const [index, extension] of (gltf.extensionsUsed ?? []).entries()) {
|
|
1701
|
+
const capability = capabilities[extension];
|
|
1702
|
+
if (capability?.support === 'supported')
|
|
1703
|
+
continue;
|
|
1704
|
+
const partial = capability?.support === 'partial';
|
|
1705
|
+
warnings.push(Object.freeze({
|
|
1706
|
+
code: partial ? 'W_GLTF_PARTIAL_OPTIONAL_EXTENSION' : 'W_GLTF_UNSUPPORTED_OPTIONAL_EXTENSION',
|
|
1707
|
+
message: partial
|
|
1708
|
+
? `Optional glTF extension "${extension}" is only partially supported: ${capability.note}`
|
|
1709
|
+
: `Optional glTF extension "${extension}" is not supported and was ignored.`,
|
|
1710
|
+
extension,
|
|
1711
|
+
path: `gltf.extensionsUsed[${index}]`,
|
|
1712
|
+
}));
|
|
1713
|
+
}
|
|
1714
|
+
const entries = [...usedExtensions].map(extension => {
|
|
1715
|
+
const capability = capabilities[extension];
|
|
1716
|
+
const support = capability?.support ?? 'unsupported';
|
|
1717
|
+
return Object.freeze({
|
|
1718
|
+
extension,
|
|
1719
|
+
required: requiredExtensions.has(extension),
|
|
1720
|
+
support,
|
|
1721
|
+
disposition: support === 'supported' ? 'supported' : support === 'partial' ? 'partial' : 'ignored',
|
|
1722
|
+
note: capability?.note ?? 'No loader capability is registered; optional extension data is ignored.',
|
|
1723
|
+
});
|
|
1724
|
+
});
|
|
1725
|
+
const frozenWarnings = Object.freeze(warnings);
|
|
1726
|
+
return Object.freeze({
|
|
1727
|
+
warnings: frozenWarnings,
|
|
1728
|
+
report: Object.freeze({
|
|
1729
|
+
fullySupported: entries.every(entry => entry.support === 'supported'),
|
|
1730
|
+
entries: Object.freeze(entries),
|
|
1731
|
+
}),
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
function resolveBaseUrl(src) {
|
|
1735
|
+
try {
|
|
1736
|
+
return new URL('.', src).href;
|
|
1737
|
+
}
|
|
1738
|
+
catch {
|
|
1739
|
+
const documentBase = globalThis.document?.baseURI;
|
|
1740
|
+
const candidates = [documentBase, globalThis.location?.href, 'http://localhost/'];
|
|
1741
|
+
for (const candidate of candidates) {
|
|
1742
|
+
if (typeof candidate !== 'string')
|
|
1743
|
+
continue;
|
|
1744
|
+
try {
|
|
1745
|
+
return new URL('.', candidate).href;
|
|
1746
|
+
}
|
|
1747
|
+
catch {
|
|
1748
|
+
// about:srcdoc and other opaque origins cannot resolve relatives.
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return 'http://localhost/';
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
function isGlb(buffer) {
|
|
1755
|
+
const view = new DataView(buffer);
|
|
1756
|
+
return view.byteLength >= 12 && view.getUint32(0, true) === 0x46546c67;
|
|
1757
|
+
}
|
|
1758
|
+
function parseGlb(buffer) {
|
|
1759
|
+
if (buffer.byteLength < 12)
|
|
1760
|
+
throw gltfDataError('GLB header is truncated.', {}, 'glb.header');
|
|
1761
|
+
const view = new DataView(buffer);
|
|
1762
|
+
const version = view.getUint32(4, true);
|
|
1763
|
+
if (version !== 2)
|
|
1764
|
+
throw gltfDataError(`Unsupported GLB version: ${version}`, { version }, 'glb.header.version');
|
|
1765
|
+
let offset = 12;
|
|
1766
|
+
let gltf = null;
|
|
1767
|
+
let binaryChunk = null;
|
|
1768
|
+
while (offset + 8 <= view.byteLength) {
|
|
1769
|
+
const chunkLength = view.getUint32(offset, true);
|
|
1770
|
+
const chunkType = view.getUint32(offset + 4, true);
|
|
1771
|
+
const chunkStart = offset + 8;
|
|
1772
|
+
if (chunkStart + chunkLength > view.byteLength) {
|
|
1773
|
+
throw gltfDataError('GLB chunk exceeds the file length.', { chunkLength, offset }, `glb.chunks[${offset}]`);
|
|
1774
|
+
}
|
|
1775
|
+
const chunk = buffer.slice(chunkStart, chunkStart + chunkLength);
|
|
1776
|
+
if (chunkType === 0x4e4f534a) {
|
|
1777
|
+
gltf = parseGltfJson(new TextDecoder().decode(chunk), 'GLB', 'glb.json');
|
|
1778
|
+
}
|
|
1779
|
+
else if (chunkType === 0x004e4942) {
|
|
1780
|
+
binaryChunk = chunk;
|
|
1781
|
+
}
|
|
1782
|
+
offset = chunkStart + chunkLength;
|
|
1783
|
+
}
|
|
1784
|
+
if (!gltf)
|
|
1785
|
+
throw gltfDataError('GLB is missing JSON chunk.', {}, 'glb.json');
|
|
1786
|
+
return { gltf, binaryChunk };
|
|
1787
|
+
}
|
|
1788
|
+
function parseGltfJson(text, source, path) {
|
|
1789
|
+
let value;
|
|
1790
|
+
try {
|
|
1791
|
+
value = JSON.parse(text);
|
|
1792
|
+
}
|
|
1793
|
+
catch (error) {
|
|
1794
|
+
throw gltfDataError('glTF JSON is not valid JSON.', { url: source }, path, error);
|
|
1795
|
+
}
|
|
1796
|
+
if (!isGltfAsset(value)) {
|
|
1797
|
+
throw gltfDataError('glTF JSON does not match the supported glTF structure.', { url: source }, path);
|
|
1798
|
+
}
|
|
1799
|
+
return value;
|
|
1800
|
+
}
|
|
1801
|
+
async function loadBuffers(gltf, binaryChunk, baseUrl, signal) {
|
|
1802
|
+
return Promise.all((gltf.buffers ?? []).map(async (buffer, index) => {
|
|
1803
|
+
throwIfGltfLoadAborted(signal);
|
|
1804
|
+
if (!buffer.uri) {
|
|
1805
|
+
if (index === 0 && binaryChunk)
|
|
1806
|
+
return binaryChunk;
|
|
1807
|
+
throw gltfDataError(`glTF buffer ${index} has no uri and no GLB chunk.`);
|
|
1808
|
+
}
|
|
1809
|
+
if (buffer.uri.startsWith('data:')) {
|
|
1810
|
+
return dataUriToArrayBuffer(buffer.uri);
|
|
1811
|
+
}
|
|
1812
|
+
const response = await fetch(resolveFetchUri(buffer.uri, baseUrl), signal ? { signal } : undefined);
|
|
1813
|
+
if (!response.ok)
|
|
1814
|
+
throw gltfDataError(`Failed to load glTF buffer: ${buffer.uri}`, { uri: buffer.uri }, `gltf.buffers[${index}]`);
|
|
1815
|
+
throwIfGltfLoadAborted(signal);
|
|
1816
|
+
const result = await response.arrayBuffer();
|
|
1817
|
+
throwIfGltfLoadAborted(signal);
|
|
1818
|
+
return result;
|
|
1819
|
+
}));
|
|
1820
|
+
}
|
|
1821
|
+
function resolveFetchUri(uri, baseUrl) {
|
|
1822
|
+
if (/^(blob:|https?:\/\/)/i.test(uri))
|
|
1823
|
+
return uri;
|
|
1824
|
+
return new URL(uri, baseUrl).href;
|
|
1825
|
+
}
|
|
1826
|
+
function dataUriToArrayBuffer(uri) {
|
|
1827
|
+
const comma = uri.indexOf(',');
|
|
1828
|
+
const meta = uri.slice(0, comma);
|
|
1829
|
+
const data = uri.slice(comma + 1);
|
|
1830
|
+
if (meta.endsWith(';base64')) {
|
|
1831
|
+
const binary = atob(data);
|
|
1832
|
+
const out = new Uint8Array(binary.length);
|
|
1833
|
+
for (let i = 0; i < binary.length; i++)
|
|
1834
|
+
out[i] = binary.charCodeAt(i);
|
|
1835
|
+
return out.buffer;
|
|
1836
|
+
}
|
|
1837
|
+
return new TextEncoder().encode(decodeURIComponent(data)).buffer;
|
|
1838
|
+
}
|
|
1839
|
+
async function instantiateNode(gltf, buffers, nodeIndex, options, animationTargets, context) {
|
|
1840
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1841
|
+
const node = gltf.nodes?.[nodeIndex];
|
|
1842
|
+
if (!node)
|
|
1843
|
+
return null;
|
|
1844
|
+
const entity = new Entity(node.name || `glTF Node ${nodeIndex}`);
|
|
1845
|
+
const { transform, target } = createNodeTransform(node, entity);
|
|
1846
|
+
entity.addComponent(transform);
|
|
1847
|
+
animationTargets.set(nodeIndex, target);
|
|
1848
|
+
if (node.mesh !== undefined) {
|
|
1849
|
+
const mesh = gltf.meshes?.[node.mesh];
|
|
1850
|
+
target.weights = mesh?.weights ? [...mesh.weights] : [];
|
|
1851
|
+
const primitiveEntities = await Promise.all((mesh?.primitives ?? []).map((primitive, primitiveIndex) => createPrimitiveEntity(gltf, buffers, primitive, options, `${mesh?.name || 'Mesh'} Primitive ${primitiveIndex}`, target, context, node.skin, entity, node.mesh, primitiveIndex)));
|
|
1852
|
+
for (const primitiveEntity of primitiveEntities) {
|
|
1853
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1854
|
+
if (primitiveEntity)
|
|
1855
|
+
entity.addChild(primitiveEntity);
|
|
1856
|
+
}
|
|
1857
|
+
applyMorphWeights(target, target.weights);
|
|
1858
|
+
}
|
|
1859
|
+
const children = await Promise.all((node.children ?? []).map(childIndex => instantiateNode(gltf, buffers, childIndex, options, animationTargets, context)));
|
|
1860
|
+
for (const child of children) {
|
|
1861
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1862
|
+
if (child)
|
|
1863
|
+
entity.addChild(child);
|
|
1864
|
+
}
|
|
1865
|
+
return entity;
|
|
1866
|
+
}
|
|
1867
|
+
function createNodeTransform(node, entity) {
|
|
1868
|
+
const translation = node.translation ? [...node.translation] : [0, 0, 0];
|
|
1869
|
+
const rotation = node.rotation ? [...node.rotation] : [0, 0, 0, 1];
|
|
1870
|
+
const scale = node.scale ? [...node.scale] : [1, 1, 1];
|
|
1871
|
+
const transform = new Transform3D();
|
|
1872
|
+
if (node.matrix?.length === 16) {
|
|
1873
|
+
transform.localMatrix = new Float32Array(node.matrix);
|
|
1874
|
+
}
|
|
1875
|
+
else {
|
|
1876
|
+
transform.localMatrix = composeTrsMatrix(translation, rotation, scale);
|
|
1877
|
+
}
|
|
1878
|
+
return { transform, target: { entity, transform, translation, rotation, scale, weights: [], morphPrimitives: [] } };
|
|
1879
|
+
}
|
|
1880
|
+
async function createPrimitiveEntity(gltf, buffers, primitive, options, name, target, context, skinIndex, meshEntity, meshIndex, primitiveIndex) {
|
|
1881
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1882
|
+
if (primitive.mode !== undefined && primitive.mode !== 4)
|
|
1883
|
+
return null;
|
|
1884
|
+
const payload = context.geometryPayloads?.[meshIndex]?.[primitiveIndex]
|
|
1885
|
+
?? await createPrimitiveGeometryPayload(gltf, buffers, primitive, options);
|
|
1886
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1887
|
+
if (!payload)
|
|
1888
|
+
return null;
|
|
1889
|
+
const uvPlanResult = planGltfUvSemantics(gltf, primitive, meshIndex, primitiveIndex, context.extensionAdapters);
|
|
1890
|
+
if (!uvPlanResult.ok) {
|
|
1891
|
+
throw gltfDataError(uvPlanResult.failure.message, uvPlanResult.failure.context, uvPlanResult.failure.path);
|
|
1892
|
+
}
|
|
1893
|
+
const uvPlan = uvPlanResult.plan;
|
|
1894
|
+
const { positions, indices, normals, textureCoordinates, joints, weights, positionTargets, normalTargets } = payload;
|
|
1895
|
+
for (const mapping of uvPlan.mappings) {
|
|
1896
|
+
if (textureCoordinates.some(entry => entry.set === mapping.set))
|
|
1897
|
+
continue;
|
|
1898
|
+
throw gltfDataError(`Decoded geometry payload is missing ${mapping.semantic}.`, { meshIndex, primitiveIndex, semantic: mapping.semantic, texCoord: mapping.set }, `${uvPlan.path}.attributes.${mapping.semantic}`);
|
|
1899
|
+
}
|
|
1900
|
+
const geometry = new Geometry3D({
|
|
1901
|
+
positions,
|
|
1902
|
+
...(normals === undefined ? {} : { normals }),
|
|
1903
|
+
textureCoordinates,
|
|
1904
|
+
textureCoordinateLayout: uvPlan.mappings.map(mapping => mapping.set),
|
|
1905
|
+
...(indices === undefined ? {} : { indices }),
|
|
1906
|
+
});
|
|
1907
|
+
const conservativeBounds = GltfConservativeBounds.fromPrimitive(gltf, primitive);
|
|
1908
|
+
if (conservativeBounds)
|
|
1909
|
+
registerGltfConservativeBounds(geometry, conservativeBounds);
|
|
1910
|
+
if (positionTargets.length > 0 || normalTargets.length > 0) {
|
|
1911
|
+
geometry.setMorphTargets(new Array(Math.max(positionTargets.length, normalTargets.length)).fill(null).map((_, index) => ({
|
|
1912
|
+
...(positionTargets[index] === undefined ? {} : { positions: positionTargets[index] }),
|
|
1913
|
+
...(normalTargets[index] === undefined ? {} : { normals: normalTargets[index] }),
|
|
1914
|
+
})), target.weights);
|
|
1915
|
+
target.morphPrimitives.push({
|
|
1916
|
+
geometry,
|
|
1917
|
+
basePositions: positions.slice(),
|
|
1918
|
+
baseNormals: normals ? normals.slice() : null,
|
|
1919
|
+
positionTargets,
|
|
1920
|
+
normalTargets,
|
|
1921
|
+
});
|
|
1922
|
+
if (target.weights.length === 0)
|
|
1923
|
+
target.weights = new Array(Math.max(positionTargets.length, normalTargets.length)).fill(0);
|
|
1924
|
+
}
|
|
1925
|
+
const material = await createGltfMaterial(gltf, buffers, primitive, options, context, uvPlan);
|
|
1926
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1927
|
+
const entity = new Entity(name);
|
|
1928
|
+
entity.addComponent(new CartesianTransform3D());
|
|
1929
|
+
entity.addComponent(new Mesh3D(geometry, material));
|
|
1930
|
+
if (skinIndex !== undefined && joints && weights) {
|
|
1931
|
+
context.pendingSkinnedPrimitives.push({ skinIndex, geometry, meshEntity, joints, weights });
|
|
1932
|
+
}
|
|
1933
|
+
context.compatibilityPrimitives.push({
|
|
1934
|
+
meshIndex,
|
|
1935
|
+
primitiveIndex,
|
|
1936
|
+
primitive,
|
|
1937
|
+
geometry,
|
|
1938
|
+
skinRequested: skinIndex !== undefined,
|
|
1939
|
+
uvSemanticPlan: uvPlan,
|
|
1940
|
+
});
|
|
1941
|
+
return entity;
|
|
1942
|
+
}
|
|
1943
|
+
async function createPrimitiveGeometryPayload(gltf, buffers, primitive, options) {
|
|
1944
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1945
|
+
if (primitive.mode !== undefined && primitive.mode !== 4)
|
|
1946
|
+
return null;
|
|
1947
|
+
const dracoExtension = primitive.extensions?.KHR_draco_mesh_compression;
|
|
1948
|
+
let decoded = null;
|
|
1949
|
+
if (dracoExtension) {
|
|
1950
|
+
const decodeStartedAt = nowMilliseconds();
|
|
1951
|
+
decoded = await decodeDracoPrimitive(gltf, buffers, dracoExtension, options);
|
|
1952
|
+
options.diagnostics?.onDracoDecode?.(nowMilliseconds() - decodeStartedAt, countDracoGeometryBytes(decoded));
|
|
1953
|
+
}
|
|
1954
|
+
throwIfGltfLoadAborted(options.signal);
|
|
1955
|
+
const positionAccessor = primitive.attributes.POSITION;
|
|
1956
|
+
if (positionAccessor === undefined && !decoded?.positions)
|
|
1957
|
+
return null;
|
|
1958
|
+
const positions = decoded?.positions ?? readAccessorFloat(gltf, buffers, positionAccessor, 3);
|
|
1959
|
+
const indices = decoded?.indices ?? (primitive.indices !== undefined ? readAccessorIndices(gltf, buffers, primitive.indices) : undefined);
|
|
1960
|
+
const normals = decoded?.normals ?? (primitive.attributes.NORMAL !== undefined
|
|
1961
|
+
? readAccessorFloat(gltf, buffers, primitive.attributes.NORMAL, 3)
|
|
1962
|
+
: generateFlatNormals(positions, indices ?? null));
|
|
1963
|
+
const textureCoordinates = decoded?.textureCoordinates ?? Object.entries(primitive.attributes)
|
|
1964
|
+
.map(([semantic, accessor]) => ({ set: parseTextureCoordinateSemantic(semantic), accessor }))
|
|
1965
|
+
.filter((entry) => entry.set !== null)
|
|
1966
|
+
.sort((a, b) => a.set - b.set)
|
|
1967
|
+
.map(entry => ({ set: entry.set, data: readAccessorFloat(gltf, buffers, entry.accessor, 2) }));
|
|
1968
|
+
const joints = decoded?.joints ?? (primitive.attributes.JOINTS_0 !== undefined
|
|
1969
|
+
? readAccessorUnsigned(gltf, buffers, primitive.attributes.JOINTS_0, 4)
|
|
1970
|
+
: undefined);
|
|
1971
|
+
const weights = decoded?.weights ?? (primitive.attributes.WEIGHTS_0 !== undefined
|
|
1972
|
+
? readAccessorFloat(gltf, buffers, primitive.attributes.WEIGHTS_0, 4)
|
|
1973
|
+
: undefined);
|
|
1974
|
+
return {
|
|
1975
|
+
positions,
|
|
1976
|
+
...(indices === undefined ? {} : { indices }),
|
|
1977
|
+
...(normals === undefined ? {} : { normals }),
|
|
1978
|
+
textureCoordinates,
|
|
1979
|
+
...(joints === undefined ? {} : { joints }),
|
|
1980
|
+
...(weights === undefined ? {} : { weights }),
|
|
1981
|
+
positionTargets: (primitive.targets ?? []).map(item => item.POSITION !== undefined
|
|
1982
|
+
? readAccessorFloat(gltf, buffers, item.POSITION, 3)
|
|
1983
|
+
: new Float32Array(positions.length)),
|
|
1984
|
+
normalTargets: (primitive.targets ?? []).map(item => item.NORMAL !== undefined
|
|
1985
|
+
? readAccessorFloat(gltf, buffers, item.NORMAL, 3)
|
|
1986
|
+
: new Float32Array(normals.length)),
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
async function prepareGltfGeometryPayloads(gltf, buffers, options = {}) {
|
|
1990
|
+
return Promise.all((gltf.meshes ?? []).map(mesh => Promise.all(mesh.primitives.map(primitive => createPrimitiveGeometryPayload(gltf, buffers, primitive, options)))));
|
|
1991
|
+
}
|
|
1992
|
+
function countDracoGeometryBytes(decoded) {
|
|
1993
|
+
const buffers = new Set();
|
|
1994
|
+
const add = (value) => {
|
|
1995
|
+
if (value)
|
|
1996
|
+
buffers.add(value.buffer);
|
|
1997
|
+
};
|
|
1998
|
+
add(decoded.positions);
|
|
1999
|
+
add(decoded.indices);
|
|
2000
|
+
add(decoded.normals);
|
|
2001
|
+
add(decoded.joints);
|
|
2002
|
+
add(decoded.weights);
|
|
2003
|
+
for (const entry of decoded.textureCoordinates)
|
|
2004
|
+
add(entry.data);
|
|
2005
|
+
let total = 0;
|
|
2006
|
+
for (const buffer of buffers)
|
|
2007
|
+
total += buffer.byteLength;
|
|
2008
|
+
return total;
|
|
2009
|
+
}
|
|
2010
|
+
function resolvePendingSkins(gltf, buffers, context) {
|
|
2011
|
+
for (const pending of context.pendingSkinnedPrimitives) {
|
|
2012
|
+
const runtime = createSkinRuntime(gltf, buffers, pending, context);
|
|
2013
|
+
if (runtime) {
|
|
2014
|
+
updateSkinnedPrimitive(runtime);
|
|
2015
|
+
context.skinnedPrimitives.push(runtime);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
context.pendingSkinnedPrimitives.length = 0;
|
|
2019
|
+
}
|
|
2020
|
+
function createSkinRuntime(gltf, buffers, pending, context) {
|
|
2021
|
+
const skin = gltf.skins?.[pending.skinIndex];
|
|
2022
|
+
if (!skin || skin.joints.length === 0)
|
|
2023
|
+
return null;
|
|
2024
|
+
const jointTargets = skin.joints.map(nodeIndex => context.animationTargets.get(nodeIndex) ?? null);
|
|
2025
|
+
if (!jointTargets.some(Boolean))
|
|
2026
|
+
return null;
|
|
2027
|
+
const inverseBindMatrices = skin.inverseBindMatrices !== undefined
|
|
2028
|
+
? readAccessorMat4(gltf, buffers, skin.inverseBindMatrices)
|
|
2029
|
+
: new Array(jointTargets.length).fill(null).map(() => mat4.identity());
|
|
2030
|
+
if (inverseBindMatrices.length < jointTargets.length) {
|
|
2031
|
+
while (inverseBindMatrices.length < jointTargets.length)
|
|
2032
|
+
inverseBindMatrices.push(mat4.identity());
|
|
2033
|
+
}
|
|
2034
|
+
const jointMatrices = new Float32Array(jointTargets.length * 16);
|
|
2035
|
+
const gpuSkinAttributes = createGpuSkinAttributes(pending.joints, pending.weights, jointTargets.length);
|
|
2036
|
+
pending.geometry.setSkinning({
|
|
2037
|
+
joints: gpuSkinAttributes.joints,
|
|
2038
|
+
weights: gpuSkinAttributes.weights,
|
|
2039
|
+
jointMatrices,
|
|
2040
|
+
});
|
|
2041
|
+
return {
|
|
2042
|
+
geometry: pending.geometry,
|
|
2043
|
+
meshEntity: pending.meshEntity,
|
|
2044
|
+
jointTargets,
|
|
2045
|
+
inverseBindMatrices,
|
|
2046
|
+
jointMatrices,
|
|
2047
|
+
inverseMeshWorldScratch: mat4.identity(),
|
|
2048
|
+
jointMatrixScratch: mat4.identity(),
|
|
2049
|
+
skinMatrixScratch: mat4.identity(),
|
|
2050
|
+
lastMeshWorldVersion: -1,
|
|
2051
|
+
lastGeometryVersion: -1,
|
|
2052
|
+
lastJointWorldVersions: new Int32Array(jointTargets.length).fill(-1),
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
function createGpuSkinAttributes(joints, weights, jointCount) {
|
|
2056
|
+
const gpuJoints = new Float32Array(joints.length);
|
|
2057
|
+
const gpuWeights = new Float32Array(joints.length);
|
|
2058
|
+
for (let vertexOffset = 0; vertexOffset < joints.length; vertexOffset += 4) {
|
|
2059
|
+
let weightSum = 0;
|
|
2060
|
+
for (let influence = 0; influence < 4; influence++) {
|
|
2061
|
+
const offset = vertexOffset + influence;
|
|
2062
|
+
const jointIndex = joints[offset] ?? 0;
|
|
2063
|
+
const weight = weights[offset] ?? 0;
|
|
2064
|
+
if (jointIndex >= 0 && jointIndex < jointCount) {
|
|
2065
|
+
gpuJoints[offset] = jointIndex;
|
|
2066
|
+
gpuWeights[offset] = Number.isFinite(weight) && weight > 0 ? weight : 0;
|
|
2067
|
+
weightSum += gpuWeights[offset];
|
|
2068
|
+
}
|
|
2069
|
+
else {
|
|
2070
|
+
gpuJoints[offset] = 0;
|
|
2071
|
+
gpuWeights[offset] = 0;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
if (weightSum > 0) {
|
|
2075
|
+
for (let influence = 0; influence < 4; influence++) {
|
|
2076
|
+
const offset = vertexOffset + influence;
|
|
2077
|
+
gpuWeights[offset] = (gpuWeights[offset] ?? 0) / weightSum;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
return { joints: gpuJoints, weights: gpuWeights };
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
export { DEFAULT_GLTF_EXTENSION_ADAPTERS as D, GLTF_EXTENSION_CAPABILITIES as G, loadGltfModel as a, collectGltfMaterialVariantNames as b, collectGltfMaterialExtensionPatches as c, disposeGltfModel as d, collectGltfMaterialVariantReferences as e, isGltfParsedAsset as i, loadParsedGltfAsset as l, prepareGltfGeometryPayloads as p, resolveGltfExtensionAdapters as r, setGltfMaterialVariant as s };
|
|
2085
|
+
//# sourceMappingURL=gltfLoader-Dm22jLYB.js.map
|