@aura3d/assets 1.0.6
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/README.md +17 -0
- package/dist/AdvancedAssetCorpus.d.ts +124 -0
- package/dist/AdvancedAssetCorpus.d.ts.map +1 -0
- package/dist/AdvancedAssetCorpus.js +266 -0
- package/dist/AdvancedAssetCorpus.js.map +1 -0
- package/dist/AssetBundleCacheFixtures.d.ts +62 -0
- package/dist/AssetBundleCacheFixtures.d.ts.map +1 -0
- package/dist/AssetBundleCacheFixtures.js +222 -0
- package/dist/AssetBundleCacheFixtures.js.map +1 -0
- package/dist/AssetCache.d.ts +21 -0
- package/dist/AssetCache.d.ts.map +1 -0
- package/dist/AssetCache.js +54 -0
- package/dist/AssetCache.js.map +1 -0
- package/dist/AssetCompatibility.d.ts +54 -0
- package/dist/AssetCompatibility.d.ts.map +1 -0
- package/dist/AssetCompatibility.js +122 -0
- package/dist/AssetCompatibility.js.map +1 -0
- package/dist/AssetCorpus.d.ts +88 -0
- package/dist/AssetCorpus.d.ts.map +1 -0
- package/dist/AssetCorpus.js +215 -0
- package/dist/AssetCorpus.js.map +1 -0
- package/dist/AssetDependencyGraph.d.ts +12 -0
- package/dist/AssetDependencyGraph.d.ts.map +1 -0
- package/dist/AssetDependencyGraph.js +66 -0
- package/dist/AssetDependencyGraph.js.map +1 -0
- package/dist/AssetHandle.d.ts +26 -0
- package/dist/AssetHandle.d.ts.map +1 -0
- package/dist/AssetHandle.js +53 -0
- package/dist/AssetHandle.js.map +1 -0
- package/dist/AssetImportPreflight.d.ts +43 -0
- package/dist/AssetImportPreflight.d.ts.map +1 -0
- package/dist/AssetImportPreflight.js +117 -0
- package/dist/AssetImportPreflight.js.map +1 -0
- package/dist/AssetInspection.d.ts +183 -0
- package/dist/AssetInspection.d.ts.map +1 -0
- package/dist/AssetInspection.js +397 -0
- package/dist/AssetInspection.js.map +1 -0
- package/dist/AssetLoader.d.ts +26 -0
- package/dist/AssetLoader.d.ts.map +1 -0
- package/dist/AssetLoader.js +12 -0
- package/dist/AssetLoader.js.map +1 -0
- package/dist/AssetManager.d.ts +37 -0
- package/dist/AssetManager.d.ts.map +1 -0
- package/dist/AssetManager.js +161 -0
- package/dist/AssetManager.js.map +1 -0
- package/dist/AssetRegistry.d.ts +10 -0
- package/dist/AssetRegistry.d.ts.map +1 -0
- package/dist/AssetRegistry.js +34 -0
- package/dist/AssetRegistry.js.map +1 -0
- package/dist/AssetRenderDefaults.d.ts +13 -0
- package/dist/AssetRenderDefaults.d.ts.map +1 -0
- package/dist/AssetRenderDefaults.js +23 -0
- package/dist/AssetRenderDefaults.js.map +1 -0
- package/dist/AudioLoader.d.ts +17 -0
- package/dist/AudioLoader.d.ts.map +1 -0
- package/dist/AudioLoader.js +23 -0
- package/dist/AudioLoader.js.map +1 -0
- package/dist/BlenderExportValidation.d.ts +64 -0
- package/dist/BlenderExportValidation.d.ts.map +1 -0
- package/dist/BlenderExportValidation.js +143 -0
- package/dist/BlenderExportValidation.js.map +1 -0
- package/dist/CarConceptMaterialStability.d.ts +28 -0
- package/dist/CarConceptMaterialStability.d.ts.map +1 -0
- package/dist/CarConceptMaterialStability.js +1308 -0
- package/dist/CarConceptMaterialStability.js.map +1 -0
- package/dist/EXRLoader.d.ts +2 -0
- package/dist/EXRLoader.d.ts.map +1 -0
- package/dist/EXRLoader.js +2 -0
- package/dist/EXRLoader.js.map +1 -0
- package/dist/ExternalParityGLTFCorpus.d.ts +39 -0
- package/dist/ExternalParityGLTFCorpus.d.ts.map +1 -0
- package/dist/ExternalParityGLTFCorpus.js +32 -0
- package/dist/ExternalParityGLTFCorpus.js.map +1 -0
- package/dist/GLTFAnimationRuntime.d.ts +243 -0
- package/dist/GLTFAnimationRuntime.d.ts.map +1 -0
- package/dist/GLTFAnimationRuntime.js +954 -0
- package/dist/GLTFAnimationRuntime.js.map +1 -0
- package/dist/GLTFCompressionDecoders.d.ts +42 -0
- package/dist/GLTFCompressionDecoders.d.ts.map +1 -0
- package/dist/GLTFCompressionDecoders.js +99 -0
- package/dist/GLTFCompressionDecoders.js.map +1 -0
- package/dist/GLTFExtensionSupport.d.ts +33 -0
- package/dist/GLTFExtensionSupport.d.ts.map +1 -0
- package/dist/GLTFExtensionSupport.js +85 -0
- package/dist/GLTFExtensionSupport.js.map +1 -0
- package/dist/GLTFLoader.d.ts +361 -0
- package/dist/GLTFLoader.d.ts.map +1 -0
- package/dist/GLTFLoader.js +2735 -0
- package/dist/GLTFLoader.js.map +1 -0
- package/dist/GLTFRenderResources.d.ts +162 -0
- package/dist/GLTFRenderResources.d.ts.map +1 -0
- package/dist/GLTFRenderResources.js +1684 -0
- package/dist/GLTFRenderResources.js.map +1 -0
- package/dist/HDRLoader.d.ts +2 -0
- package/dist/HDRLoader.d.ts.map +1 -0
- package/dist/HDRLoader.js +2 -0
- package/dist/HDRLoader.js.map +1 -0
- package/dist/ImageLoader.d.ts +15 -0
- package/dist/ImageLoader.d.ts.map +1 -0
- package/dist/ImageLoader.js +36 -0
- package/dist/ImageLoader.js.map +1 -0
- package/dist/ImportPipeline.d.ts +39 -0
- package/dist/ImportPipeline.d.ts.map +1 -0
- package/dist/ImportPipeline.js +105 -0
- package/dist/ImportPipeline.js.map +1 -0
- package/dist/KTX2BasisTextureTranscoder.d.ts +9 -0
- package/dist/KTX2BasisTextureTranscoder.d.ts.map +1 -0
- package/dist/KTX2BasisTextureTranscoder.js +178 -0
- package/dist/KTX2BasisTextureTranscoder.js.map +1 -0
- package/dist/LoadContext.d.ts +20 -0
- package/dist/LoadContext.d.ts.map +1 -0
- package/dist/LoadContext.js +42 -0
- package/dist/LoadContext.js.map +1 -0
- package/dist/MaterialLoader.d.ts +17 -0
- package/dist/MaterialLoader.d.ts.map +1 -0
- package/dist/MaterialLoader.js +138 -0
- package/dist/MaterialLoader.js.map +1 -0
- package/dist/MeshOptimization.d.ts +20 -0
- package/dist/MeshOptimization.d.ts.map +1 -0
- package/dist/MeshOptimization.js +65 -0
- package/dist/MeshOptimization.js.map +1 -0
- package/dist/OBJLoader.d.ts +13 -0
- package/dist/OBJLoader.d.ts.map +1 -0
- package/dist/OBJLoader.js +492 -0
- package/dist/OBJLoader.js.map +1 -0
- package/dist/SceneAnalysisFixtures.d.ts +100 -0
- package/dist/SceneAnalysisFixtures.d.ts.map +1 -0
- package/dist/SceneAnalysisFixtures.js +331 -0
- package/dist/SceneAnalysisFixtures.js.map +1 -0
- package/dist/SceneLoader.d.ts +27 -0
- package/dist/SceneLoader.d.ts.map +1 -0
- package/dist/SceneLoader.js +40 -0
- package/dist/SceneLoader.js.map +1 -0
- package/dist/ShaderLoader.d.ts +12 -0
- package/dist/ShaderLoader.d.ts.map +1 -0
- package/dist/ShaderLoader.js +18 -0
- package/dist/ShaderLoader.js.map +1 -0
- package/dist/TextureLoader.d.ts +16 -0
- package/dist/TextureLoader.d.ts.map +1 -0
- package/dist/TextureLoader.js +17 -0
- package/dist/TextureLoader.js.map +1 -0
- package/dist/TexturePipeline.d.ts +24 -0
- package/dist/TexturePipeline.d.ts.map +1 -0
- package/dist/TexturePipeline.js +64 -0
- package/dist/TexturePipeline.js.map +1 -0
- package/dist/TextureStreaming.d.ts +10 -0
- package/dist/TextureStreaming.d.ts.map +1 -0
- package/dist/TextureStreaming.js +11 -0
- package/dist/TextureStreaming.js.map +1 -0
- package/dist/WorkerAssetJobs.d.ts +18 -0
- package/dist/WorkerAssetJobs.d.ts.map +1 -0
- package/dist/WorkerAssetJobs.js +37 -0
- package/dist/WorkerAssetJobs.js.map +1 -0
- package/dist/advanced-gallery/index.d.ts +3 -0
- package/dist/advanced-gallery/index.d.ts.map +1 -0
- package/dist/advanced-gallery/index.js +2 -0
- package/dist/advanced-gallery/index.js.map +1 -0
- package/dist/asset-corpus/GLTFSceneLoader.d.ts +3 -0
- package/dist/asset-corpus/GLTFSceneLoader.d.ts.map +1 -0
- package/dist/asset-corpus/GLTFSceneLoader.js +2 -0
- package/dist/asset-corpus/GLTFSceneLoader.js.map +1 -0
- package/dist/asset-corpus/ProductionAssetCorpus.d.ts +89 -0
- package/dist/asset-corpus/ProductionAssetCorpus.d.ts.map +1 -0
- package/dist/asset-corpus/ProductionAssetCorpus.js +116 -0
- package/dist/asset-corpus/ProductionAssetCorpus.js.map +1 -0
- package/dist/asset-corpus/ProductionAssetPipeline.d.ts +3 -0
- package/dist/asset-corpus/ProductionAssetPipeline.d.ts.map +1 -0
- package/dist/asset-corpus/ProductionAssetPipeline.js +3 -0
- package/dist/asset-corpus/ProductionAssetPipeline.js.map +1 -0
- package/dist/asset-corpus/ProductionGLTFRenderPipeline.d.ts +65 -0
- package/dist/asset-corpus/ProductionGLTFRenderPipeline.d.ts.map +1 -0
- package/dist/asset-corpus/ProductionGLTFRenderPipeline.js +110 -0
- package/dist/asset-corpus/ProductionGLTFRenderPipeline.js.map +1 -0
- package/dist/asset-corpus/ProductionHDRTextureLoader.d.ts +8 -0
- package/dist/asset-corpus/ProductionHDRTextureLoader.d.ts.map +1 -0
- package/dist/asset-corpus/ProductionHDRTextureLoader.js +2 -0
- package/dist/asset-corpus/ProductionHDRTextureLoader.js.map +1 -0
- package/dist/asset-corpus/ProductionKTX2TextureLoader.d.ts +7 -0
- package/dist/asset-corpus/ProductionKTX2TextureLoader.d.ts.map +1 -0
- package/dist/asset-corpus/ProductionKTX2TextureLoader.js +2 -0
- package/dist/asset-corpus/ProductionKTX2TextureLoader.js.map +1 -0
- package/dist/asset-corpus/ProductionTextureLoader.d.ts +7 -0
- package/dist/asset-corpus/ProductionTextureLoader.d.ts.map +1 -0
- package/dist/asset-corpus/ProductionTextureLoader.js +2 -0
- package/dist/asset-corpus/ProductionTextureLoader.js.map +1 -0
- package/dist/asset-corpus/index.d.ts +10 -0
- package/dist/asset-corpus/index.d.ts.map +1 -0
- package/dist/asset-corpus/index.js +8 -0
- package/dist/asset-corpus/index.js.map +1 -0
- package/dist/browser-index.d.ts +48 -0
- package/dist/browser-index.d.ts.map +1 -0
- package/dist/browser-index.js +26 -0
- package/dist/browser-index.js.map +1 -0
- package/dist/createRenderableScene.d.ts +23 -0
- package/dist/createRenderableScene.d.ts.map +1 -0
- package/dist/createRenderableScene.js +94 -0
- package/dist/createRenderableScene.js.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/loadRenderableAsset.d.ts +16 -0
- package/dist/loadRenderableAsset.d.ts.map +1 -0
- package/dist/loadRenderableAsset.js +50 -0
- package/dist/loadRenderableAsset.js.map +1 -0
- package/dist/loaders/CubeTextureLoader.d.ts +4 -0
- package/dist/loaders/CubeTextureLoader.d.ts.map +1 -0
- package/dist/loaders/CubeTextureLoader.js +9 -0
- package/dist/loaders/CubeTextureLoader.js.map +1 -0
- package/dist/loaders/EXRLoader.d.ts +4 -0
- package/dist/loaders/EXRLoader.d.ts.map +1 -0
- package/dist/loaders/EXRLoader.js +10 -0
- package/dist/loaders/EXRLoader.js.map +1 -0
- package/dist/loaders/HDRLoader.d.ts +4 -0
- package/dist/loaders/HDRLoader.d.ts.map +1 -0
- package/dist/loaders/HDRLoader.js +7 -0
- package/dist/loaders/HDRLoader.js.map +1 -0
- package/dist/loaders/KTX2Loader.d.ts +4 -0
- package/dist/loaders/KTX2Loader.d.ts.map +1 -0
- package/dist/loaders/KTX2Loader.js +7 -0
- package/dist/loaders/KTX2Loader.js.map +1 -0
- package/dist/loaders/LoaderDiagnostics.d.ts +18 -0
- package/dist/loaders/LoaderDiagnostics.d.ts.map +1 -0
- package/dist/loaders/LoaderDiagnostics.js +18 -0
- package/dist/loaders/LoaderDiagnostics.js.map +1 -0
- package/dist/loaders/MTLLoader.d.ts +10 -0
- package/dist/loaders/MTLLoader.d.ts.map +1 -0
- package/dist/loaders/MTLLoader.js +14 -0
- package/dist/loaders/MTLLoader.js.map +1 -0
- package/dist/loaders/OBJLoader.d.ts +12 -0
- package/dist/loaders/OBJLoader.d.ts.map +1 -0
- package/dist/loaders/OBJLoader.js +16 -0
- package/dist/loaders/OBJLoader.js.map +1 -0
- package/dist/loaders/TextureLoader.d.ts +6 -0
- package/dist/loaders/TextureLoader.d.ts.map +1 -0
- package/dist/loaders/TextureLoader.js +11 -0
- package/dist/loaders/TextureLoader.js.map +1 -0
- package/dist/loaders/ThreeCompatGLTFLoader.d.ts +10 -0
- package/dist/loaders/ThreeCompatGLTFLoader.d.ts.map +1 -0
- package/dist/loaders/ThreeCompatGLTFLoader.js +13 -0
- package/dist/loaders/ThreeCompatGLTFLoader.js.map +1 -0
- package/dist/loaders/index.d.ts +15 -0
- package/dist/loaders/index.d.ts.map +1 -0
- package/dist/loaders/index.js +10 -0
- package/dist/loaders/index.js.map +1 -0
- package/dist/threejs-compatibility/ThreeCompatAssetProvenance.d.ts +34 -0
- package/dist/threejs-compatibility/ThreeCompatAssetProvenance.d.ts.map +1 -0
- package/dist/threejs-compatibility/ThreeCompatAssetProvenance.js +16 -0
- package/dist/threejs-compatibility/ThreeCompatAssetProvenance.js.map +1 -0
- package/dist/threejs-compatibility/ThreeCompatAssetRegistry.d.ts +29 -0
- package/dist/threejs-compatibility/ThreeCompatAssetRegistry.d.ts.map +1 -0
- package/dist/threejs-compatibility/ThreeCompatAssetRegistry.js +33 -0
- package/dist/threejs-compatibility/ThreeCompatAssetRegistry.js.map +1 -0
- package/dist/threejs-example-parity/index.d.ts +20 -0
- package/dist/threejs-example-parity/index.d.ts.map +1 -0
- package/dist/threejs-example-parity/index.js +218 -0
- package/dist/threejs-example-parity/index.js.map +1 -0
- package/package.json +23 -0
- package/src/AdvancedAssetCorpus.ts +454 -0
- package/src/AssetBundleCacheFixtures.ts +290 -0
- package/src/AssetCache.ts +76 -0
- package/src/AssetCompatibility.ts +224 -0
- package/src/AssetCorpus.ts +329 -0
- package/src/AssetDependencyGraph.ts +83 -0
- package/src/AssetHandle.ts +74 -0
- package/src/AssetImportPreflight.ts +179 -0
- package/src/AssetInspection.ts +706 -0
- package/src/AssetLoader.ts +36 -0
- package/src/AssetManager.ts +211 -0
- package/src/AssetRegistry.ts +44 -0
- package/src/AssetRenderDefaults.ts +36 -0
- package/src/AudioLoader.ts +41 -0
- package/src/BlenderExportValidation.ts +227 -0
- package/src/CarConceptMaterialStability.ts +1405 -0
- package/src/EXRLoader.ts +1 -0
- package/src/ExternalParityGLTFCorpus.ts +65 -0
- package/src/GLTFAnimationRuntime.ts +1221 -0
- package/src/GLTFCompressionDecoders.ts +190 -0
- package/src/GLTFExtensionSupport.ts +138 -0
- package/src/GLTFLoader.ts +4001 -0
- package/src/GLTFRenderResources.ts +2105 -0
- package/src/HDRLoader.ts +1 -0
- package/src/ImageLoader.ts +52 -0
- package/src/ImportPipeline.ts +148 -0
- package/src/KTX2BasisTextureTranscoder.ts +224 -0
- package/src/LoadContext.ts +60 -0
- package/src/MaterialLoader.ts +166 -0
- package/src/MeshOptimization.ts +93 -0
- package/src/OBJLoader.ts +551 -0
- package/src/SceneAnalysisFixtures.ts +438 -0
- package/src/SceneLoader.ts +62 -0
- package/src/ShaderLoader.ts +30 -0
- package/src/TextureLoader.ts +30 -0
- package/src/TexturePipeline.ts +96 -0
- package/src/TextureStreaming.ts +20 -0
- package/src/WorkerAssetJobs.ts +56 -0
- package/src/advanced-gallery/index.ts +18 -0
- package/src/asset-corpus/GLTFSceneLoader.ts +9 -0
- package/src/asset-corpus/ProductionAssetCorpus.ts +230 -0
- package/src/asset-corpus/ProductionAssetPipeline.ts +5 -0
- package/src/asset-corpus/ProductionGLTFRenderPipeline.ts +182 -0
- package/src/asset-corpus/ProductionHDRTextureLoader.ts +2 -0
- package/src/asset-corpus/ProductionKTX2TextureLoader.ts +2 -0
- package/src/asset-corpus/ProductionTextureLoader.ts +2 -0
- package/src/asset-corpus/index.ts +29 -0
- package/src/browser-index.ts +224 -0
- package/src/createRenderableScene.ts +128 -0
- package/src/index.ts +317 -0
- package/src/loadRenderableAsset.ts +67 -0
- package/src/loaders/CubeTextureLoader.ts +8 -0
- package/src/loaders/EXRLoader.ts +10 -0
- package/src/loaders/HDRLoader.ts +7 -0
- package/src/loaders/KTX2Loader.ts +7 -0
- package/src/loaders/LoaderDiagnostics.ts +36 -0
- package/src/loaders/MTLLoader.ts +20 -0
- package/src/loaders/OBJLoader.ts +24 -0
- package/src/loaders/TextureLoader.ts +14 -0
- package/src/loaders/ThreeCompatGLTFLoader.ts +19 -0
- package/src/loaders/index.ts +14 -0
- package/src/threejs-compatibility/ThreeCompatAssetProvenance.ts +51 -0
- package/src/threejs-compatibility/ThreeCompatAssetRegistry.ts +61 -0
- package/src/threejs-example-parity/index.ts +262 -0
- package/tests/assets.test.ts +311 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,2735 @@
|
|
|
1
|
+
import { PointLight, composeMat4, decomposeMat4, invertMat4, multiplyMat4, Renderable, Scene, SpotLight } from "@aura3d/scene";
|
|
2
|
+
import { AnimationClip, AnimationTrack, Skeleton } from "@aura3d/animation";
|
|
3
|
+
import { evaluateGLTFExtensionSupport } from "./GLTFExtensionSupport.js";
|
|
4
|
+
const DEFAULT_GLTF_MATERIAL_NAME = "default-material";
|
|
5
|
+
const RUNTIME_MATERIAL_KEY_MARKER = "#aura3d-runtime:";
|
|
6
|
+
const DISPOSE_GLTF_ASSET = Symbol("disposeGLTFAsset");
|
|
7
|
+
const MAX_RENDERABLE_SKIN_JOINTS = 96;
|
|
8
|
+
function createSpecularFromSpecularGlossiness(extension) {
|
|
9
|
+
if (!extension)
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
factor: 1,
|
|
13
|
+
colorFactor: extension.specularFactor,
|
|
14
|
+
colorTexture: extension.specularGlossinessTexture
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export class GLTFLoader {
|
|
18
|
+
options;
|
|
19
|
+
type = "gltf";
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
canLoad(request) {
|
|
24
|
+
return /\.(?:gltf|glb)(?:\?.*)?$/i.test(request.url) || request.url.startsWith("data:model/gltf");
|
|
25
|
+
}
|
|
26
|
+
async load(request, context) {
|
|
27
|
+
context.throwIfAborted(request.url);
|
|
28
|
+
const document = await loadDocument(request);
|
|
29
|
+
let json = document.json;
|
|
30
|
+
if (json.asset?.version && !json.asset.version.startsWith("2.")) {
|
|
31
|
+
throw new Error(`Unsupported glTF version: ${json.asset.version}`);
|
|
32
|
+
}
|
|
33
|
+
const extensionSupport = evaluateGLTFExtensionSupport(json.extensionsUsed, json.extensionsRequired);
|
|
34
|
+
const unsupportedRequiredExtensions = extensionSupport.unsupportedRequired;
|
|
35
|
+
if (unsupportedRequiredExtensions.length > 0) {
|
|
36
|
+
throw new Error(`Unsupported required glTF extensions: ${unsupportedRequiredExtensions.join(", ")}`);
|
|
37
|
+
}
|
|
38
|
+
const rawBuffers = await Promise.all((json.buffers ?? []).map((buffer, index) => loadBuffer(buffer, index, document, request)));
|
|
39
|
+
const prepared = await prepareBufferViews(json, rawBuffers, this.options.meshoptDecoder);
|
|
40
|
+
const buffers = prepared.buffers;
|
|
41
|
+
json = prepared.json;
|
|
42
|
+
const accessorCache = createGLTFAccessorReadCache();
|
|
43
|
+
const meshQuantizationEnabled = usesGLTFExtension(json, "KHR_mesh_quantization");
|
|
44
|
+
const images = createImageAssets(json, buffers);
|
|
45
|
+
const textures = createTextureAssets(json);
|
|
46
|
+
const materials = createMaterialAssets(json, textures);
|
|
47
|
+
const materialVariants = createMaterialVariantAssets(json);
|
|
48
|
+
const scenes = createSceneAssets(json);
|
|
49
|
+
const defaultScene = resolveDefaultSceneIndex(json);
|
|
50
|
+
const cameras = createCameraAssets(json);
|
|
51
|
+
const lights = createLightAssets(json);
|
|
52
|
+
const skins = createSkinAssets(json, buffers, accessorCache);
|
|
53
|
+
const nodeInstanceTransforms = createNodeInstanceTransforms(json, buffers, accessorCache);
|
|
54
|
+
const meshes = await Promise.all((json.meshes ?? []).flatMap((mesh, meshIndex) => {
|
|
55
|
+
validateMeshDescriptor(mesh, meshIndex);
|
|
56
|
+
return mesh.primitives.map(async (primitive, primitiveIndex) => {
|
|
57
|
+
validatePrimitiveDescriptor(primitive, meshIndex, primitiveIndex);
|
|
58
|
+
validatePrimitiveAttributeAccessors(json, primitive, meshQuantizationEnabled, meshIndex, primitiveIndex);
|
|
59
|
+
const positionAccessorIndex = primitive.attributes.POSITION;
|
|
60
|
+
if (positionAccessorIndex === undefined) {
|
|
61
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} is missing POSITION`);
|
|
62
|
+
}
|
|
63
|
+
const dracoPrimitive = await decodeDracoPrimitive(json, buffers, primitive, meshIndex, primitiveIndex, this.options.dracoDecoder);
|
|
64
|
+
const positions = dracoPrimitive?.attributes.POSITION ?? readAccessor(json, buffers, positionAccessorIndex, accessorCache);
|
|
65
|
+
const normals = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "NORMAL", accessorCache);
|
|
66
|
+
const texcoordSets = readTexcoordSets(json, buffers, primitive, positions.length, meshIndex, primitiveIndex, dracoPrimitive, accessorCache);
|
|
67
|
+
const texcoords = texcoordSets[0] ?? [];
|
|
68
|
+
const tangents = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "TANGENT", accessorCache);
|
|
69
|
+
const colors = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "COLOR_0", accessorCache);
|
|
70
|
+
const joints = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "JOINTS_0", accessorCache);
|
|
71
|
+
const weights = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "WEIGHTS_0", accessorCache);
|
|
72
|
+
const indices = dracoPrimitive
|
|
73
|
+
? dracoPrimitive.indices?.map((index) => [index])
|
|
74
|
+
: primitive.indices === undefined ? undefined : readAccessor(json, buffers, primitive.indices, accessorCache);
|
|
75
|
+
validateAttributeCount("NORMAL", normals, positions.length, meshIndex, primitiveIndex);
|
|
76
|
+
validateAttributeCount("TANGENT", tangents, positions.length, meshIndex, primitiveIndex);
|
|
77
|
+
validateAttributeCount("COLOR_0", colors, positions.length, meshIndex, primitiveIndex);
|
|
78
|
+
validateAttributeCount("JOINTS_0", joints, positions.length, meshIndex, primitiveIndex);
|
|
79
|
+
validateAttributeCount("WEIGHTS_0", weights, positions.length, meshIndex, primitiveIndex);
|
|
80
|
+
const primitiveMaterialIndex = resolvePrimitiveMaterialIndex(primitive, materials, meshIndex, primitiveIndex);
|
|
81
|
+
const primitiveMaterial = primitiveMaterialIndex === undefined ? undefined : materials[primitiveMaterialIndex];
|
|
82
|
+
const primitiveMaterialVariants = resolvePrimitiveMaterialVariants(primitive, materialVariants, materials, meshIndex, primitiveIndex);
|
|
83
|
+
validateMaterialTexCoordSets(primitiveMaterial, texcoordSets, meshIndex, primitiveIndex);
|
|
84
|
+
const typedPositions = positions.map((position) => [position[0] ?? 0, position[1] ?? 0, position[2] ?? 0]);
|
|
85
|
+
const typedNormals = normals.map((normal) => [normal[0] ?? 0, normal[1] ?? 0, normal[2] ?? 0]);
|
|
86
|
+
const typedTangents = tangents.map((tangent) => [tangent[0] ?? 0, tangent[1] ?? 0, tangent[2] ?? 0, tangent[3] ?? 1]);
|
|
87
|
+
const typedColors = colors.map((color) => [color[0] ?? 1, color[1] ?? 1, color[2] ?? 1, color[3] ?? 1]);
|
|
88
|
+
const typedJoints = joints.map((joint) => [joint[0] ?? 0, joint[1] ?? 0, joint[2] ?? 0, joint[3] ?? 0]);
|
|
89
|
+
const typedWeights = weights.map((weight) => normalizeSkinWeights([
|
|
90
|
+
weight[0] ?? 0,
|
|
91
|
+
weight[1] ?? 0,
|
|
92
|
+
weight[2] ?? 0,
|
|
93
|
+
weight[3] ?? 0
|
|
94
|
+
], meshIndex, primitiveIndex));
|
|
95
|
+
const morphTargets = readMorphTargets(json, buffers, primitive, positions.length, meshIndex, primitiveIndex, accessorCache);
|
|
96
|
+
const resolvedPrimitive = resolvePrimitiveMode(primitive, indices?.map((index) => index[0] ?? 0), positions.length, meshIndex, primitiveIndex);
|
|
97
|
+
const geometry = createGeometryAsset(typedPositions, resolvedPrimitive.indices);
|
|
98
|
+
return {
|
|
99
|
+
name: meshPrimitiveName(mesh, meshIndex, primitiveIndex),
|
|
100
|
+
sourceMeshIndex: meshIndex,
|
|
101
|
+
primitiveIndex,
|
|
102
|
+
topology: resolvedPrimitive.topology,
|
|
103
|
+
geometry,
|
|
104
|
+
positions: typedPositions,
|
|
105
|
+
normals: typedNormals,
|
|
106
|
+
texcoords,
|
|
107
|
+
texcoordSets,
|
|
108
|
+
tangents: typedTangents,
|
|
109
|
+
colors: typedColors,
|
|
110
|
+
joints: typedJoints,
|
|
111
|
+
weights: typedWeights,
|
|
112
|
+
morphTargets,
|
|
113
|
+
morphWeights: resolveMorphWeights(mesh, meshIndex, primitiveIndex, morphTargets.length),
|
|
114
|
+
indices: resolvedPrimitive.indices,
|
|
115
|
+
material: materialName(primitiveMaterial, primitiveMaterialIndex),
|
|
116
|
+
materialIndex: primitiveMaterialIndex,
|
|
117
|
+
materialVariants: primitiveMaterialVariants,
|
|
118
|
+
skinIndex: findSkinForMesh(json, meshIndex)
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}));
|
|
122
|
+
const animations = createAnimationClips(json, buffers, accessorCache);
|
|
123
|
+
const loadProfile = createGLTFLoaderLoadProfileDiagnostics(document, buffers, images, accessorCache);
|
|
124
|
+
const loaderDiagnostics = createGLTFLoaderDiagnostics(json, {
|
|
125
|
+
images,
|
|
126
|
+
textures,
|
|
127
|
+
materials,
|
|
128
|
+
meshes,
|
|
129
|
+
animations,
|
|
130
|
+
skins
|
|
131
|
+
}, loadProfile);
|
|
132
|
+
let disposed = false;
|
|
133
|
+
const assertAlive = () => {
|
|
134
|
+
if (disposed) {
|
|
135
|
+
throw new Error(`glTF asset ${request.url} has been disposed`);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const asset = {
|
|
139
|
+
url: request.url,
|
|
140
|
+
get disposed() {
|
|
141
|
+
return disposed;
|
|
142
|
+
},
|
|
143
|
+
loaderDiagnostics,
|
|
144
|
+
images,
|
|
145
|
+
textures,
|
|
146
|
+
materials,
|
|
147
|
+
materialVariants,
|
|
148
|
+
scenes,
|
|
149
|
+
defaultScene,
|
|
150
|
+
meshes,
|
|
151
|
+
cameras,
|
|
152
|
+
lights,
|
|
153
|
+
skins,
|
|
154
|
+
animations,
|
|
155
|
+
createScene: (options) => {
|
|
156
|
+
assertAlive();
|
|
157
|
+
return createScene(json, meshes, cameras, lights, skins, nodeInstanceTransforms, materialVariants, options);
|
|
158
|
+
},
|
|
159
|
+
toJSON: () => {
|
|
160
|
+
assertAlive();
|
|
161
|
+
return serializeGLTFAsset(request.url, loaderDiagnostics, images, textures, materials, materialVariants, scenes, defaultScene, meshes, cameras, lights, skins, animations);
|
|
162
|
+
},
|
|
163
|
+
[DISPOSE_GLTF_ASSET]: () => {
|
|
164
|
+
disposed = true;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
return asset;
|
|
168
|
+
}
|
|
169
|
+
dispose(asset) {
|
|
170
|
+
asset[DISPOSE_GLTF_ASSET]?.();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function createGLTFAccessorReadCache() {
|
|
174
|
+
return {
|
|
175
|
+
rowsByAccessor: new Map(),
|
|
176
|
+
dataViews: new Map(),
|
|
177
|
+
profile: {
|
|
178
|
+
accessorReadCount: 0,
|
|
179
|
+
uniqueAccessorReadCount: 0,
|
|
180
|
+
accessorCacheHitCount: 0,
|
|
181
|
+
accessorRowsRead: 0,
|
|
182
|
+
accessorValuesRead: 0,
|
|
183
|
+
accessorReadMs: 0,
|
|
184
|
+
dataViewCacheEntries: 0,
|
|
185
|
+
largestAccessors: []
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function createGLTFLoaderLoadProfileDiagnostics(document, buffers, images, accessorCache) {
|
|
190
|
+
const profile = accessorCache.profile;
|
|
191
|
+
const embeddedImageBytes = images.reduce((sum, image) => sum + (image.data?.byteLength ?? 0), 0);
|
|
192
|
+
return {
|
|
193
|
+
schemaVersion: "gltf-loader-load-profile",
|
|
194
|
+
documentBytes: document.byteLength,
|
|
195
|
+
binaryChunkBytes: document.binaryChunk?.byteLength ?? 0,
|
|
196
|
+
bufferBytes: buffers.reduce((sum, buffer) => sum + buffer.byteLength, 0),
|
|
197
|
+
imageBytes: embeddedImageBytes,
|
|
198
|
+
embeddedImageBytes,
|
|
199
|
+
externalImageCount: images.filter((image) => Boolean(image.uri) && !image.uri.startsWith("data:")).length,
|
|
200
|
+
bufferViewImageCount: images.filter((image) => image.data !== undefined).length,
|
|
201
|
+
accessorReadCount: profile.accessorReadCount,
|
|
202
|
+
uniqueAccessorReadCount: profile.uniqueAccessorReadCount,
|
|
203
|
+
accessorCacheHitCount: profile.accessorCacheHitCount,
|
|
204
|
+
accessorRowsRead: profile.accessorRowsRead,
|
|
205
|
+
accessorValuesRead: profile.accessorValuesRead,
|
|
206
|
+
accessorReadMs: roundDiagnosticMs(profile.accessorReadMs),
|
|
207
|
+
dataViewCacheEntries: profile.dataViewCacheEntries,
|
|
208
|
+
largestAccessors: [...profile.largestAccessors]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function roundDiagnosticMs(value) {
|
|
212
|
+
return Math.round(value * 1000) / 1000;
|
|
213
|
+
}
|
|
214
|
+
function nowMs() {
|
|
215
|
+
const performance = globalThis.performance;
|
|
216
|
+
return performance && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
217
|
+
}
|
|
218
|
+
function createGeometryAsset(positions, indices) {
|
|
219
|
+
const min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
220
|
+
const max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
221
|
+
for (const position of positions) {
|
|
222
|
+
min[0] = Math.min(min[0], position[0]);
|
|
223
|
+
min[1] = Math.min(min[1], position[1]);
|
|
224
|
+
min[2] = Math.min(min[2], position[2]);
|
|
225
|
+
max[0] = Math.max(max[0], position[0]);
|
|
226
|
+
max[1] = Math.max(max[1], position[1]);
|
|
227
|
+
max[2] = Math.max(max[2], position[2]);
|
|
228
|
+
}
|
|
229
|
+
return { vertexCount: positions.length, indexCount: indices?.length ?? 0, bounds: { min, max } };
|
|
230
|
+
}
|
|
231
|
+
function createGLTFLoaderDiagnostics(json, asset, loadProfile) {
|
|
232
|
+
const extensionsUsed = [...new Set([...(json.extensionsUsed ?? []), ...(json.extensionsRequired ?? [])])].sort();
|
|
233
|
+
const extensionSupport = evaluateGLTFExtensionSupport(json.extensionsUsed, json.extensionsRequired);
|
|
234
|
+
const materialFeatures = [...new Set(asset.materials.flatMap(materialFeatureNames))].sort();
|
|
235
|
+
const textureSlots = [...new Set(asset.materials.flatMap(materialTextureSlotNames))].sort();
|
|
236
|
+
const unsupportedFeatures = collectUnsupportedFeatureDiagnostics(json);
|
|
237
|
+
const compression = {
|
|
238
|
+
draco: usesGLTFExtension(json, "KHR_draco_mesh_compression"),
|
|
239
|
+
meshopt: usesGLTFMeshoptExtension(json),
|
|
240
|
+
ktx2Basis: usesGLTFExtension(json, "KHR_texture_basisu") || asset.images.some((image) => /\.ktx2(?:[?#]|$)/i.test(image.uri ?? "") || image.mimeType === "image/ktx2")
|
|
241
|
+
};
|
|
242
|
+
const features = new Set([
|
|
243
|
+
"gltf",
|
|
244
|
+
...extensionsUsed.map((extension) => `extension:${extension}`),
|
|
245
|
+
...materialFeatures.map((feature) => `material:${feature}`),
|
|
246
|
+
...textureSlots.map((slot) => `texture-slot:${slot}`)
|
|
247
|
+
]);
|
|
248
|
+
if (asset.meshes.length > 0)
|
|
249
|
+
features.add("mesh");
|
|
250
|
+
if (asset.meshes.some((mesh) => mesh.geometry.indexCount > 0))
|
|
251
|
+
features.add("indexed-geometry");
|
|
252
|
+
if (asset.meshes.some((mesh) => mesh.normals.length > 0))
|
|
253
|
+
features.add("normals");
|
|
254
|
+
if (asset.meshes.some((mesh) => mesh.tangents.length > 0))
|
|
255
|
+
features.add("tangents");
|
|
256
|
+
if (asset.meshes.some((mesh) => mesh.colors.length > 0))
|
|
257
|
+
features.add("vertex-colors");
|
|
258
|
+
if (asset.meshes.some((mesh) => mesh.texcoordSets.some((set) => set.length > 0)))
|
|
259
|
+
features.add("texcoords");
|
|
260
|
+
if (asset.meshes.some((mesh) => mesh.texcoordSets.length > 1))
|
|
261
|
+
features.add("multi-uv");
|
|
262
|
+
if (asset.meshes.some((mesh) => mesh.morphTargets.length > 0))
|
|
263
|
+
features.add("morph-targets");
|
|
264
|
+
if (asset.meshes.some((mesh) => mesh.skinIndex !== undefined || mesh.joints.length > 0 || mesh.weights.length > 0))
|
|
265
|
+
features.add("skinning");
|
|
266
|
+
if (asset.skins.some((skin) => skin.joints.length > MAX_RENDERABLE_SKIN_JOINTS))
|
|
267
|
+
features.add("skinning-palette-limit-fallback");
|
|
268
|
+
if ((json.skins ?? []).some((skin) => skin.inverseBindMatrices === undefined))
|
|
269
|
+
features.add("skinning-default-inverse-bind-matrices");
|
|
270
|
+
for (const unsupportedFeature of unsupportedFeatures)
|
|
271
|
+
features.add(`unsupported:${unsupportedFeature}`);
|
|
272
|
+
if (asset.animations.length > 0)
|
|
273
|
+
features.add("animations");
|
|
274
|
+
if (asset.skins.length > 0)
|
|
275
|
+
features.add("skins");
|
|
276
|
+
if (asset.textures.length > 0)
|
|
277
|
+
features.add("textures");
|
|
278
|
+
if (compression.draco)
|
|
279
|
+
features.add("compression:draco");
|
|
280
|
+
if (compression.meshopt)
|
|
281
|
+
features.add("compression:meshopt");
|
|
282
|
+
if (compression.ktx2Basis)
|
|
283
|
+
features.add("compression:ktx2-basis");
|
|
284
|
+
return {
|
|
285
|
+
schemaVersion: "gltf-loader-diagnostics",
|
|
286
|
+
features: [...features].sort(),
|
|
287
|
+
extensionsUsed,
|
|
288
|
+
extensionsRequired: [...(json.extensionsRequired ?? [])].sort(),
|
|
289
|
+
unsupportedExtensions: extensionSupport.notAcceptedUsed,
|
|
290
|
+
extensionSupport,
|
|
291
|
+
unsupportedFeatures,
|
|
292
|
+
meshCount: asset.meshes.length,
|
|
293
|
+
primitiveCount: asset.meshes.length,
|
|
294
|
+
vertexCount: asset.meshes.reduce((sum, mesh) => sum + mesh.geometry.vertexCount, 0),
|
|
295
|
+
indexCount: asset.meshes.reduce((sum, mesh) => sum + mesh.geometry.indexCount, 0),
|
|
296
|
+
materialCount: asset.materials.length,
|
|
297
|
+
textureCount: asset.textures.length,
|
|
298
|
+
imageCount: asset.images.length,
|
|
299
|
+
animationCount: asset.animations.length,
|
|
300
|
+
skinCount: asset.skins.length,
|
|
301
|
+
morphTargetCount: asset.meshes.reduce((sum, mesh) => sum + mesh.morphTargets.length, 0),
|
|
302
|
+
materialFeatures,
|
|
303
|
+
textureSlots,
|
|
304
|
+
compression,
|
|
305
|
+
...(loadProfile ? { loadProfile } : {})
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function collectUnsupportedFeatureDiagnostics(json) {
|
|
309
|
+
const unsupported = new Set();
|
|
310
|
+
for (const mesh of json.meshes ?? []) {
|
|
311
|
+
for (const primitive of mesh.primitives) {
|
|
312
|
+
if (primitive.attributes.JOINTS_1 !== undefined || primitive.attributes.WEIGHTS_1 !== undefined) {
|
|
313
|
+
unsupported.add("skinning-extra-influences:JOINTS_1/WEIGHTS_1");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return [...unsupported].sort();
|
|
318
|
+
}
|
|
319
|
+
function materialFeatureNames(material) {
|
|
320
|
+
return [
|
|
321
|
+
material.unlit ? "unlit" : undefined,
|
|
322
|
+
material.metallicFactor > 0 ? "metallic" : undefined,
|
|
323
|
+
material.roughnessFactor < 1 ? "roughness" : undefined,
|
|
324
|
+
material.emissiveFactor.some((value) => value > 0) || material.emissiveStrength !== 1 ? "emissive" : undefined,
|
|
325
|
+
material.alphaMode !== "OPAQUE" ? `alpha-${material.alphaMode.toLowerCase()}` : undefined,
|
|
326
|
+
material.doubleSided ? "double-sided" : undefined,
|
|
327
|
+
material.normalTexture ? "normal-texture" : undefined,
|
|
328
|
+
material.occlusionTexture ? "occlusion-texture" : undefined,
|
|
329
|
+
material.clearcoat ? "clearcoat" : undefined,
|
|
330
|
+
material.transmission ? "transmission" : undefined,
|
|
331
|
+
material.diffuseTransmission ? "diffuse-transmission" : undefined,
|
|
332
|
+
material.volume ? "volume" : undefined,
|
|
333
|
+
material.ior !== undefined ? "ior" : undefined,
|
|
334
|
+
material.specular ? "specular" : undefined,
|
|
335
|
+
material.sheen ? "sheen" : undefined,
|
|
336
|
+
material.anisotropy ? "anisotropy" : undefined,
|
|
337
|
+
material.iridescence ? "iridescence" : undefined,
|
|
338
|
+
material.dispersion !== undefined ? "dispersion" : undefined,
|
|
339
|
+
material.pbrSpecularGlossiness ? "pbr-specular-glossiness" : undefined
|
|
340
|
+
].filter((feature) => feature !== undefined);
|
|
341
|
+
}
|
|
342
|
+
function materialTextureSlotNames(material) {
|
|
343
|
+
return [
|
|
344
|
+
material.baseColorTexture ? "base-color" : undefined,
|
|
345
|
+
material.metallicRoughnessTexture ? "metallic-roughness" : undefined,
|
|
346
|
+
material.normalTexture ? "normal" : undefined,
|
|
347
|
+
material.occlusionTexture ? "occlusion" : undefined,
|
|
348
|
+
material.emissiveTexture ? "emissive" : undefined,
|
|
349
|
+
material.clearcoat?.texture ? "clearcoat" : undefined,
|
|
350
|
+
material.clearcoat?.roughnessTexture ? "clearcoat-roughness" : undefined,
|
|
351
|
+
material.clearcoat?.normalTexture ? "clearcoat-normal" : undefined,
|
|
352
|
+
material.transmission?.texture ? "transmission" : undefined,
|
|
353
|
+
material.diffuseTransmission?.texture ? "diffuse-transmission" : undefined,
|
|
354
|
+
material.diffuseTransmission?.colorTexture ? "diffuse-transmission-color" : undefined,
|
|
355
|
+
material.volume?.thicknessTexture ? "volume-thickness" : undefined,
|
|
356
|
+
material.specular?.texture ? "specular" : undefined,
|
|
357
|
+
material.specular?.colorTexture ? "specular-color" : undefined,
|
|
358
|
+
material.sheen?.colorTexture ? "sheen-color" : undefined,
|
|
359
|
+
material.sheen?.roughnessTexture ? "sheen-roughness" : undefined,
|
|
360
|
+
material.anisotropy?.texture ? "anisotropy" : undefined,
|
|
361
|
+
material.iridescence?.texture ? "iridescence" : undefined,
|
|
362
|
+
material.iridescence?.thicknessTexture ? "iridescence-thickness" : undefined
|
|
363
|
+
].filter((slot) => slot !== undefined);
|
|
364
|
+
}
|
|
365
|
+
function validateAttributeCount(semantic, values, vertexCount, meshIndex, primitiveIndex) {
|
|
366
|
+
if (values.length > 0 && values.length !== vertexCount) {
|
|
367
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} ${semantic} count mismatch`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function validateMeshDescriptor(mesh, meshIndex) {
|
|
371
|
+
if (!Array.isArray(mesh.primitives) || mesh.primitives.length === 0) {
|
|
372
|
+
throw new Error(`glTF mesh ${meshIndex} primitives must be a non-empty array`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function validatePrimitiveDescriptor(primitive, meshIndex, primitiveIndex) {
|
|
376
|
+
if (!isObjectRecord(primitive.attributes) || Array.isArray(primitive.attributes)) {
|
|
377
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} attributes must be an object`);
|
|
378
|
+
}
|
|
379
|
+
for (const [semantic, accessorIndex] of Object.entries(primitive.attributes)) {
|
|
380
|
+
if (!Number.isInteger(accessorIndex) || accessorIndex < 0) {
|
|
381
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} attribute ${semantic} accessor must be a non-negative integer`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (primitive.indices !== undefined && (!Number.isInteger(primitive.indices) || primitive.indices < 0)) {
|
|
385
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} indices accessor must be a non-negative integer`);
|
|
386
|
+
}
|
|
387
|
+
if (primitive.mode !== undefined && (!Number.isInteger(primitive.mode) || primitive.mode < 0)) {
|
|
388
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} mode must be a non-negative integer`);
|
|
389
|
+
}
|
|
390
|
+
const variantExtension = primitive.extensions?.KHR_materials_variants;
|
|
391
|
+
if (variantExtension !== undefined && (!Array.isArray(variantExtension.mappings) || variantExtension.mappings.length === 0)) {
|
|
392
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants.mappings must be a non-empty array`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function readTexcoordSets(json, buffers, primitive, vertexCount, meshIndex, primitiveIndex, decodedPrimitive, accessorCache) {
|
|
396
|
+
const sets = [];
|
|
397
|
+
for (const [semantic, accessorIndex] of Object.entries(primitive.attributes)) {
|
|
398
|
+
const match = /^TEXCOORD_(\d+)$/.exec(semantic);
|
|
399
|
+
if (!match)
|
|
400
|
+
continue;
|
|
401
|
+
const setIndex = Number(match[1]);
|
|
402
|
+
if (!Number.isInteger(setIndex)) {
|
|
403
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} ${semantic} has invalid texture coordinate set index`);
|
|
404
|
+
}
|
|
405
|
+
const values = decodedPrimitive?.attributes[semantic] ?? readAccessor(json, buffers, accessorIndex, accessorCache);
|
|
406
|
+
validateAttributeCount(semantic, values, vertexCount, meshIndex, primitiveIndex);
|
|
407
|
+
sets[setIndex] = values.map((texcoord) => [texcoord[0] ?? 0, texcoord[1] ?? 0]);
|
|
408
|
+
}
|
|
409
|
+
const normalized = [];
|
|
410
|
+
for (let index = 0; index < sets.length; index += 1) {
|
|
411
|
+
normalized[index] = sets[index] ?? [];
|
|
412
|
+
}
|
|
413
|
+
return normalized;
|
|
414
|
+
}
|
|
415
|
+
function readPrimitiveAttribute(json, buffers, primitive, decodedPrimitive, semantic, accessorCache) {
|
|
416
|
+
const decoded = decodedPrimitive?.attributes[semantic];
|
|
417
|
+
if (decoded)
|
|
418
|
+
return normalizeDecodedPrimitiveAttribute(json, primitive, semantic, decoded);
|
|
419
|
+
return readOptionalAccessor(json, buffers, primitive.attributes[semantic], accessorCache);
|
|
420
|
+
}
|
|
421
|
+
function normalizeDecodedPrimitiveAttribute(json, primitive, semantic, rows) {
|
|
422
|
+
const copied = rows.map((row) => [...row]);
|
|
423
|
+
const accessorIndex = primitive.attributes[semantic];
|
|
424
|
+
const accessor = accessorIndex === undefined ? undefined : json.accessors?.[accessorIndex];
|
|
425
|
+
if (!accessor?.normalized || !semantic.startsWith("COLOR_")) {
|
|
426
|
+
return copied;
|
|
427
|
+
}
|
|
428
|
+
const normalizer = normalizedComponentScale(accessor.componentType);
|
|
429
|
+
if (!normalizer) {
|
|
430
|
+
return copied;
|
|
431
|
+
}
|
|
432
|
+
const needsNormalization = copied.some((row) => row.some((value) => Math.abs(value) > 1));
|
|
433
|
+
if (!needsNormalization) {
|
|
434
|
+
return copied.map((row) => row.map(clampColorChannel));
|
|
435
|
+
}
|
|
436
|
+
return copied.map((row) => row.map((value) => clampColorChannel(normalizer(value))));
|
|
437
|
+
}
|
|
438
|
+
function normalizedComponentScale(componentType) {
|
|
439
|
+
if (componentType === 5120)
|
|
440
|
+
return (value) => Math.max(value / 127, -1);
|
|
441
|
+
if (componentType === 5121)
|
|
442
|
+
return (value) => value / 255;
|
|
443
|
+
if (componentType === 5122)
|
|
444
|
+
return (value) => Math.max(value / 32767, -1);
|
|
445
|
+
if (componentType === 5123)
|
|
446
|
+
return (value) => value / 65535;
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
function clampColorChannel(value) {
|
|
450
|
+
return Math.max(0, Math.min(1, value));
|
|
451
|
+
}
|
|
452
|
+
async function decodeDracoPrimitive(json, buffers, primitive, meshIndex, primitiveIndex, dracoDecoder) {
|
|
453
|
+
const extension = primitive.extensions?.KHR_draco_mesh_compression;
|
|
454
|
+
if (!extension)
|
|
455
|
+
return undefined;
|
|
456
|
+
if (!usesGLTFExtension(json, "KHR_draco_mesh_compression")) {
|
|
457
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} uses KHR_draco_mesh_compression but the extension is not declared`);
|
|
458
|
+
}
|
|
459
|
+
if (!dracoDecoder) {
|
|
460
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression requires a dracoDecoder`);
|
|
461
|
+
}
|
|
462
|
+
validateDracoDescriptor(extension, meshIndex, primitiveIndex);
|
|
463
|
+
const { view, buffer } = getBufferView(json, buffers, extension.bufferView);
|
|
464
|
+
const start = view.byteOffset ?? 0;
|
|
465
|
+
const source = new Uint8Array(buffer, start, view.byteLength);
|
|
466
|
+
const decoded = await dracoDecoder(source, {
|
|
467
|
+
meshIndex,
|
|
468
|
+
primitiveIndex,
|
|
469
|
+
bufferViewIndex: extension.bufferView,
|
|
470
|
+
attributes: { ...extension.attributes }
|
|
471
|
+
});
|
|
472
|
+
validateDecodedDracoPrimitive(decoded, primitive, meshIndex, primitiveIndex);
|
|
473
|
+
return decoded;
|
|
474
|
+
}
|
|
475
|
+
function validateDracoDescriptor(extension, meshIndex, primitiveIndex) {
|
|
476
|
+
if (!Number.isInteger(extension.bufferView) || extension.bufferView < 0) {
|
|
477
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression bufferView must be a non-negative integer`);
|
|
478
|
+
}
|
|
479
|
+
if (!isObjectRecord(extension.attributes) || Array.isArray(extension.attributes)) {
|
|
480
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression attributes must be an object`);
|
|
481
|
+
}
|
|
482
|
+
if (!Number.isInteger(extension.attributes.POSITION) || extension.attributes.POSITION < 0) {
|
|
483
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression requires POSITION attribute mapping`);
|
|
484
|
+
}
|
|
485
|
+
for (const [semantic, attributeId] of Object.entries(extension.attributes)) {
|
|
486
|
+
if (!Number.isInteger(attributeId) || attributeId < 0) {
|
|
487
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression attribute ${semantic} id must be a non-negative integer`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function validateDecodedDracoPrimitive(decoded, primitive, meshIndex, primitiveIndex) {
|
|
492
|
+
if (!isObjectRecord(decoded.attributes) || Array.isArray(decoded.attributes)) {
|
|
493
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoder must return attributes`);
|
|
494
|
+
}
|
|
495
|
+
const positions = decoded.attributes.POSITION;
|
|
496
|
+
if (!Array.isArray(positions) || positions.length === 0) {
|
|
497
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoder must return POSITION rows`);
|
|
498
|
+
}
|
|
499
|
+
for (const [semantic, rows] of Object.entries(decoded.attributes)) {
|
|
500
|
+
if (primitive.attributes[semantic] === undefined) {
|
|
501
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoded unexpected attribute ${semantic}`);
|
|
502
|
+
}
|
|
503
|
+
if (!Array.isArray(rows) || rows.length !== positions.length) {
|
|
504
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoded ${semantic} count must match POSITION count`);
|
|
505
|
+
}
|
|
506
|
+
for (const [rowIndex, row] of rows.entries()) {
|
|
507
|
+
if (!Array.isArray(row) || row.some((value) => !Number.isFinite(value))) {
|
|
508
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoded ${semantic} row ${rowIndex} must contain finite values`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (decoded.indices !== undefined) {
|
|
513
|
+
for (const index of decoded.indices) {
|
|
514
|
+
if (!Number.isInteger(index) || index < 0 || index >= positions.length) {
|
|
515
|
+
throw new RangeError(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoded index ${index} is outside vertex count ${positions.length}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function validateMaterialTexCoordSets(material, texcoordSets, meshIndex, primitiveIndex) {
|
|
521
|
+
if (!material)
|
|
522
|
+
return;
|
|
523
|
+
for (const textureInfo of materialTextureInfos(material)) {
|
|
524
|
+
if (textureInfo.texCoord > 0 && (texcoordSets[textureInfo.texCoord]?.length ?? 0) === 0) {
|
|
525
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} material ${material.name} references missing TEXCOORD_${textureInfo.texCoord}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function materialTextureInfos(material) {
|
|
530
|
+
return [
|
|
531
|
+
material.baseColorTexture,
|
|
532
|
+
material.metallicRoughnessTexture,
|
|
533
|
+
material.normalTexture,
|
|
534
|
+
material.occlusionTexture,
|
|
535
|
+
material.emissiveTexture,
|
|
536
|
+
material.clearcoat?.texture,
|
|
537
|
+
material.clearcoat?.roughnessTexture,
|
|
538
|
+
material.clearcoat?.normalTexture,
|
|
539
|
+
material.transmission?.texture,
|
|
540
|
+
material.diffuseTransmission?.texture,
|
|
541
|
+
material.diffuseTransmission?.colorTexture,
|
|
542
|
+
material.volume?.thicknessTexture,
|
|
543
|
+
material.specular?.texture,
|
|
544
|
+
material.specular?.colorTexture,
|
|
545
|
+
material.sheen?.colorTexture,
|
|
546
|
+
material.sheen?.roughnessTexture,
|
|
547
|
+
material.anisotropy?.texture,
|
|
548
|
+
material.iridescence?.texture,
|
|
549
|
+
material.iridescence?.thicknessTexture
|
|
550
|
+
].filter((info) => info !== undefined);
|
|
551
|
+
}
|
|
552
|
+
function resolvePrimitiveMode(primitive, rawIndices, vertexCount, meshIndex, primitiveIndex) {
|
|
553
|
+
const mode = primitive.mode ?? 4;
|
|
554
|
+
const source = rawIndices ?? [...Array(vertexCount).keys()];
|
|
555
|
+
validatePrimitiveIndices(source, vertexCount, meshIndex, primitiveIndex);
|
|
556
|
+
switch (mode) {
|
|
557
|
+
case 1:
|
|
558
|
+
if (source.length % 2 !== 0)
|
|
559
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} LINES index count must be even`);
|
|
560
|
+
return { topology: "lines", indices: rawIndices };
|
|
561
|
+
case 2:
|
|
562
|
+
return { topology: "lines", indices: expandLineLoop(source) };
|
|
563
|
+
case 3:
|
|
564
|
+
return { topology: "lines", indices: expandLineStrip(source) };
|
|
565
|
+
case 4:
|
|
566
|
+
if (source.length % 3 !== 0)
|
|
567
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} TRIANGLES index count must be a multiple of 3`);
|
|
568
|
+
return { topology: "triangles", indices: rawIndices };
|
|
569
|
+
case 5:
|
|
570
|
+
return { topology: "triangles", indices: expandTriangleStrip(source) };
|
|
571
|
+
case 6:
|
|
572
|
+
return { topology: "triangles", indices: expandTriangleFan(source) };
|
|
573
|
+
case 0:
|
|
574
|
+
return { topology: "points", indices: rawIndices };
|
|
575
|
+
default:
|
|
576
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} has unsupported primitive mode ${mode}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function validatePrimitiveIndices(indices, vertexCount, meshIndex, primitiveIndex) {
|
|
580
|
+
for (const index of indices) {
|
|
581
|
+
if (!Number.isInteger(index) || index < 0 || index >= vertexCount) {
|
|
582
|
+
throw new RangeError(`glTF mesh ${meshIndex} primitive ${primitiveIndex} index ${index} is outside vertex count ${vertexCount}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function expandLineStrip(indices) {
|
|
587
|
+
const expanded = [];
|
|
588
|
+
for (let index = 0; index < indices.length - 1; index += 1) {
|
|
589
|
+
expanded.push(indices[index], indices[index + 1]);
|
|
590
|
+
}
|
|
591
|
+
return expanded;
|
|
592
|
+
}
|
|
593
|
+
function expandLineLoop(indices) {
|
|
594
|
+
if (indices.length < 2)
|
|
595
|
+
return [];
|
|
596
|
+
const expanded = [...expandLineStrip(indices)];
|
|
597
|
+
expanded.push(indices[indices.length - 1], indices[0]);
|
|
598
|
+
return expanded;
|
|
599
|
+
}
|
|
600
|
+
function expandTriangleStrip(indices) {
|
|
601
|
+
const expanded = [];
|
|
602
|
+
for (let index = 0; index < indices.length - 2; index += 1) {
|
|
603
|
+
if (index % 2 === 0) {
|
|
604
|
+
expanded.push(indices[index], indices[index + 1], indices[index + 2]);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
expanded.push(indices[index + 1], indices[index], indices[index + 2]);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return expanded;
|
|
611
|
+
}
|
|
612
|
+
function expandTriangleFan(indices) {
|
|
613
|
+
const expanded = [];
|
|
614
|
+
for (let index = 1; index < indices.length - 1; index += 1) {
|
|
615
|
+
expanded.push(indices[0], indices[index], indices[index + 1]);
|
|
616
|
+
}
|
|
617
|
+
return expanded;
|
|
618
|
+
}
|
|
619
|
+
function readMorphTargets(json, buffers, primitive, vertexCount, meshIndex, primitiveIndex, accessorCache) {
|
|
620
|
+
if (primitive.targets === undefined) {
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
if (!Array.isArray(primitive.targets)) {
|
|
624
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} targets must be an array`);
|
|
625
|
+
}
|
|
626
|
+
return primitive.targets.map((target, targetIndex) => {
|
|
627
|
+
validateMorphTargetDescriptor(target, meshIndex, primitiveIndex, targetIndex);
|
|
628
|
+
const positions = readOptionalAccessor(json, buffers, target.POSITION, accessorCache);
|
|
629
|
+
const normals = readOptionalAccessor(json, buffers, target.NORMAL, accessorCache);
|
|
630
|
+
const tangents = readOptionalAccessor(json, buffers, target.TANGENT, accessorCache);
|
|
631
|
+
if (positions.length > 0 && positions.length !== vertexCount) {
|
|
632
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} POSITION count mismatch`);
|
|
633
|
+
}
|
|
634
|
+
if (normals.length > 0 && normals.length !== vertexCount) {
|
|
635
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} NORMAL count mismatch`);
|
|
636
|
+
}
|
|
637
|
+
if (tangents.length > 0 && tangents.length !== vertexCount) {
|
|
638
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} TANGENT count mismatch`);
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
positions: positions.map((position) => [position[0] ?? 0, position[1] ?? 0, position[2] ?? 0]),
|
|
642
|
+
normals: normals.map((normal) => [normal[0] ?? 0, normal[1] ?? 0, normal[2] ?? 0]),
|
|
643
|
+
tangents: tangents.map((tangent) => [tangent[0] ?? 0, tangent[1] ?? 0, tangent[2] ?? 0])
|
|
644
|
+
};
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function validateMorphTargetDescriptor(target, meshIndex, primitiveIndex, targetIndex) {
|
|
648
|
+
if (!isObjectRecord(target) || Array.isArray(target)) {
|
|
649
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} must be an object`);
|
|
650
|
+
}
|
|
651
|
+
for (const key of Object.keys(target)) {
|
|
652
|
+
if (key !== "POSITION" && key !== "NORMAL" && key !== "TANGENT") {
|
|
653
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} attribute ${key} is unsupported`);
|
|
654
|
+
}
|
|
655
|
+
const accessorIndex = target[key];
|
|
656
|
+
if (!Number.isInteger(accessorIndex) || accessorIndex < 0) {
|
|
657
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} ${key} accessor must be a non-negative integer`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function resolveMorphWeights(mesh, meshIndex, primitiveIndex, targetCount) {
|
|
662
|
+
if (mesh.weights === undefined) {
|
|
663
|
+
return Array.from({ length: targetCount }, () => 0);
|
|
664
|
+
}
|
|
665
|
+
if (!Array.isArray(mesh.weights)) {
|
|
666
|
+
throw new Error(`glTF mesh ${meshIndex} weights must be an array`);
|
|
667
|
+
}
|
|
668
|
+
if (mesh.weights.length !== targetCount) {
|
|
669
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} weights count must match morph target count`);
|
|
670
|
+
}
|
|
671
|
+
if (mesh.weights.some((weight) => !Number.isFinite(weight))) {
|
|
672
|
+
throw new Error(`glTF mesh ${meshIndex} weights must contain finite numbers`);
|
|
673
|
+
}
|
|
674
|
+
return [...mesh.weights];
|
|
675
|
+
}
|
|
676
|
+
function createImageAssets(json, buffers) {
|
|
677
|
+
const avifEnabled = usesGLTFExtension(json, "EXT_texture_avif");
|
|
678
|
+
const webpEnabled = usesGLTFExtension(json, "EXT_texture_webp");
|
|
679
|
+
const basisuEnabled = usesGLTFExtension(json, "KHR_texture_basisu");
|
|
680
|
+
return (json.images ?? []).map((image, index) => {
|
|
681
|
+
if (image.uri && image.bufferView !== undefined) {
|
|
682
|
+
throw new Error(`glTF image ${index} cannot define both uri and bufferView`);
|
|
683
|
+
}
|
|
684
|
+
if (!image.uri && image.bufferView === undefined) {
|
|
685
|
+
throw new Error(`glTF image ${index} must define uri or bufferView`);
|
|
686
|
+
}
|
|
687
|
+
if (image.bufferView !== undefined && !image.mimeType) {
|
|
688
|
+
throw new Error(`glTF image ${index} with bufferView requires mimeType`);
|
|
689
|
+
}
|
|
690
|
+
validateImageMimeType(image, index, avifEnabled, webpEnabled, basisuEnabled);
|
|
691
|
+
return {
|
|
692
|
+
name: image.name ?? `image-${index}`,
|
|
693
|
+
uri: image.uri,
|
|
694
|
+
mimeType: image.mimeType,
|
|
695
|
+
data: image.bufferView === undefined ? undefined : readBufferViewBytes(json, buffers, image.bufferView)
|
|
696
|
+
};
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
function validateImageMimeType(image, index, avifEnabled, webpEnabled, basisuEnabled) {
|
|
700
|
+
if (image.mimeType !== undefined && !isSupportedImageMimeType(image.mimeType, avifEnabled, webpEnabled, basisuEnabled)) {
|
|
701
|
+
throw new Error(`glTF image ${index} mimeType must be ${supportedImageMimeTypeMessage(avifEnabled, webpEnabled, basisuEnabled)}`);
|
|
702
|
+
}
|
|
703
|
+
if (image.uri?.startsWith("data:")) {
|
|
704
|
+
const separator = image.uri.indexOf(",");
|
|
705
|
+
if (separator < 0) {
|
|
706
|
+
throw new Error(`glTF image ${index} data uri must include a comma separator`);
|
|
707
|
+
}
|
|
708
|
+
const mediaType = image.uri.slice(5, separator).split(";")[0]?.toLowerCase();
|
|
709
|
+
if (!isSupportedImageMimeType(mediaType, avifEnabled, webpEnabled, basisuEnabled)) {
|
|
710
|
+
throw new Error(`glTF image ${index} data uri media type must be ${supportedImageMimeTypeMessage(avifEnabled, webpEnabled, basisuEnabled)}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function isSupportedImageMimeType(mimeType, avifEnabled, webpEnabled, basisuEnabled) {
|
|
715
|
+
return mimeType === "image/png" || mimeType === "image/jpeg" || (avifEnabled && mimeType === "image/avif") || (webpEnabled && mimeType === "image/webp") || (basisuEnabled && mimeType === "image/ktx2");
|
|
716
|
+
}
|
|
717
|
+
function supportedImageMimeTypeMessage(avifEnabled, webpEnabled, basisuEnabled) {
|
|
718
|
+
return ["image/png", "image/jpeg", ...(avifEnabled ? ["image/avif"] : []), ...(webpEnabled ? ["image/webp"] : []), ...(basisuEnabled ? ["image/ktx2"] : [])].join(", or ");
|
|
719
|
+
}
|
|
720
|
+
function createTextureAssets(json) {
|
|
721
|
+
return (json.textures ?? []).map((texture, index) => {
|
|
722
|
+
const source = validateTextureDescriptor(json, texture, index);
|
|
723
|
+
const sampler = texture.sampler;
|
|
724
|
+
return {
|
|
725
|
+
name: texture.name ?? `texture-${index}`,
|
|
726
|
+
source,
|
|
727
|
+
sampler,
|
|
728
|
+
...(sampler === undefined ? {} : { samplerDescriptor: createSamplerAsset(json.samplers[sampler], sampler) })
|
|
729
|
+
};
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
function validateTextureDescriptor(json, texture, index) {
|
|
733
|
+
const avifSource = texture.extensions?.EXT_texture_avif?.source;
|
|
734
|
+
if (avifSource !== undefined && !usesGLTFExtension(json, "EXT_texture_avif")) {
|
|
735
|
+
throw new Error(`glTF texture ${index} uses EXT_texture_avif but the extension is not declared`);
|
|
736
|
+
}
|
|
737
|
+
const webpSource = texture.extensions?.EXT_texture_webp?.source;
|
|
738
|
+
if (webpSource !== undefined && !usesGLTFExtension(json, "EXT_texture_webp")) {
|
|
739
|
+
throw new Error(`glTF texture ${index} uses EXT_texture_webp but the extension is not declared`);
|
|
740
|
+
}
|
|
741
|
+
const basisuSource = texture.extensions?.KHR_texture_basisu?.source;
|
|
742
|
+
if (basisuSource !== undefined && !usesGLTFExtension(json, "KHR_texture_basisu")) {
|
|
743
|
+
throw new Error(`glTF texture ${index} uses KHR_texture_basisu but the extension is not declared`);
|
|
744
|
+
}
|
|
745
|
+
const source = basisuSource ?? avifSource ?? webpSource ?? texture.source;
|
|
746
|
+
if (source === undefined) {
|
|
747
|
+
throw new Error(`glTF texture ${index} references missing image source undefined`);
|
|
748
|
+
}
|
|
749
|
+
if (!Number.isInteger(source) || source < 0) {
|
|
750
|
+
throw new Error(`glTF texture ${index} source must be a non-negative integer`);
|
|
751
|
+
}
|
|
752
|
+
if (!json.images?.[source]) {
|
|
753
|
+
throw new Error(`glTF texture ${index} references missing image source ${source}`);
|
|
754
|
+
}
|
|
755
|
+
if (texture.sampler !== undefined) {
|
|
756
|
+
if (!Number.isInteger(texture.sampler) || texture.sampler < 0) {
|
|
757
|
+
throw new Error(`glTF texture ${index} sampler must be a non-negative integer`);
|
|
758
|
+
}
|
|
759
|
+
if (!json.samplers?.[texture.sampler]) {
|
|
760
|
+
throw new Error(`glTF texture ${index} references missing sampler ${texture.sampler}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return source;
|
|
764
|
+
}
|
|
765
|
+
function createSamplerAsset(sampler, samplerIndex) {
|
|
766
|
+
return {
|
|
767
|
+
minFilter: minFilterFromGLTF(sampler.minFilter, samplerIndex),
|
|
768
|
+
magFilter: magFilterFromGLTF(sampler.magFilter, samplerIndex),
|
|
769
|
+
addressU: addressModeFromGLTF(sampler.wrapS, samplerIndex, "wrapS"),
|
|
770
|
+
addressV: addressModeFromGLTF(sampler.wrapT, samplerIndex, "wrapT")
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function minFilterFromGLTF(value, samplerIndex) {
|
|
774
|
+
if (value === undefined)
|
|
775
|
+
return "linear-mipmap-linear";
|
|
776
|
+
if (value === 9728)
|
|
777
|
+
return "nearest";
|
|
778
|
+
if (value === 9729)
|
|
779
|
+
return "linear";
|
|
780
|
+
if (value === 9984)
|
|
781
|
+
return "nearest-mipmap-nearest";
|
|
782
|
+
if (value === 9985)
|
|
783
|
+
return "linear-mipmap-nearest";
|
|
784
|
+
if (value === 9986)
|
|
785
|
+
return "nearest-mipmap-linear";
|
|
786
|
+
if (value === 9987)
|
|
787
|
+
return "linear-mipmap-linear";
|
|
788
|
+
throw new Error(`glTF sampler ${samplerIndex} minFilter has unsupported value ${value}`);
|
|
789
|
+
}
|
|
790
|
+
function magFilterFromGLTF(value, samplerIndex) {
|
|
791
|
+
if (value === undefined)
|
|
792
|
+
return "linear";
|
|
793
|
+
if (value === 9728)
|
|
794
|
+
return "nearest";
|
|
795
|
+
if (value === 9729)
|
|
796
|
+
return "linear";
|
|
797
|
+
throw new Error(`glTF sampler ${samplerIndex} magFilter has unsupported value ${value}`);
|
|
798
|
+
}
|
|
799
|
+
function addressModeFromGLTF(value, samplerIndex, field) {
|
|
800
|
+
if (value === undefined || value === 10497)
|
|
801
|
+
return "repeat";
|
|
802
|
+
if (value === 33071)
|
|
803
|
+
return "clamp-to-edge";
|
|
804
|
+
if (value === 33648)
|
|
805
|
+
return "mirror-repeat";
|
|
806
|
+
throw new Error(`glTF sampler ${samplerIndex} ${field} has unsupported value ${value}`);
|
|
807
|
+
}
|
|
808
|
+
function createSceneAssets(json) {
|
|
809
|
+
return (json.scenes ?? []).map((scene, index) => {
|
|
810
|
+
return {
|
|
811
|
+
name: scene.name ?? `scene-${index}`,
|
|
812
|
+
nodeIndices: [...(scene.nodes ?? [])]
|
|
813
|
+
};
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function resolveDefaultSceneIndex(json) {
|
|
817
|
+
return json.scene ?? 0;
|
|
818
|
+
}
|
|
819
|
+
function createMaterialAssets(json, textures) {
|
|
820
|
+
return (json.materials ?? []).map((material, index) => {
|
|
821
|
+
const pbr = material.pbrMetallicRoughness;
|
|
822
|
+
const pbrSpecularGlossiness = resolvePBRSpecularGlossiness(material, textures, index).pbrSpecularGlossiness;
|
|
823
|
+
const specular = resolveSpecular(material, textures, index).specular ?? createSpecularFromSpecularGlossiness(pbrSpecularGlossiness);
|
|
824
|
+
return {
|
|
825
|
+
name: material.name ?? `material-${index}`,
|
|
826
|
+
unlit: material.extensions?.KHR_materials_unlit !== undefined,
|
|
827
|
+
baseColorFactor: pbrSpecularGlossiness?.diffuseFactor ?? resolveColor4(pbr?.baseColorFactor, [1, 1, 1, 1], `material ${index} baseColorFactor`),
|
|
828
|
+
baseColorTexture: pbrSpecularGlossiness?.diffuseTexture ?? resolveTextureInfo(textures, pbr?.baseColorTexture, `material ${index} baseColorTexture`),
|
|
829
|
+
metallicFactor: pbrSpecularGlossiness ? 0 : resolveUnit(pbr?.metallicFactor, 1, `material ${index} metallicFactor`),
|
|
830
|
+
roughnessFactor: pbrSpecularGlossiness ? 1 - pbrSpecularGlossiness.glossinessFactor : resolveUnit(pbr?.roughnessFactor, 1, `material ${index} roughnessFactor`),
|
|
831
|
+
metallicRoughnessTexture: pbrSpecularGlossiness
|
|
832
|
+
? undefined
|
|
833
|
+
: resolveTextureInfo(textures, pbr?.metallicRoughnessTexture, `material ${index} metallicRoughnessTexture`),
|
|
834
|
+
normalTexture: withDefaultScale(resolveTextureInfo(textures, material.normalTexture, `material ${index} normalTexture`), material.normalTexture?.scale),
|
|
835
|
+
occlusionTexture: withDefaultStrength(resolveTextureInfo(textures, material.occlusionTexture, `material ${index} occlusionTexture`), material.occlusionTexture?.strength),
|
|
836
|
+
emissiveTexture: resolveTextureInfo(textures, material.emissiveTexture, `material ${index} emissiveTexture`),
|
|
837
|
+
emissiveFactor: resolveColor3(material.emissiveFactor, [0, 0, 0], `material ${index} emissiveFactor`),
|
|
838
|
+
emissiveStrength: resolveEmissiveStrength(material, index),
|
|
839
|
+
...(resolveClearcoat(material, textures, index)),
|
|
840
|
+
...(resolveTransmission(material, textures, index)),
|
|
841
|
+
...(resolveDiffuseTransmission(material, textures, pbrSpecularGlossiness, index)),
|
|
842
|
+
...(resolveVolume(material, textures, index)),
|
|
843
|
+
...(resolveIOR(material, index)),
|
|
844
|
+
...(specular ? { specular } : {}),
|
|
845
|
+
...(pbrSpecularGlossiness ? { pbrSpecularGlossiness } : {}),
|
|
846
|
+
...(resolveSheen(material, textures, index)),
|
|
847
|
+
...(resolveAnisotropy(material, textures, index)),
|
|
848
|
+
...(resolveIridescence(material, textures, index)),
|
|
849
|
+
...(resolveDispersion(material, index)),
|
|
850
|
+
alphaMode: resolveAlphaMode(material.alphaMode, index),
|
|
851
|
+
alphaCutoff: resolveUnit(material.alphaCutoff, 0.5, `material ${index} alphaCutoff`),
|
|
852
|
+
doubleSided: material.doubleSided ?? false
|
|
853
|
+
};
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
function createMaterialVariantAssets(json) {
|
|
857
|
+
if (!usesGLTFExtension(json, "KHR_materials_variants"))
|
|
858
|
+
return [];
|
|
859
|
+
const variants = json.extensions?.KHR_materials_variants?.variants;
|
|
860
|
+
if (!Array.isArray(variants) || variants.length === 0) {
|
|
861
|
+
throw new Error("glTF KHR_materials_variants.variants must be a non-empty array when the extension is declared");
|
|
862
|
+
}
|
|
863
|
+
return variants.map((variant, index) => {
|
|
864
|
+
if (!isObjectRecord(variant)) {
|
|
865
|
+
throw new Error(`glTF KHR_materials_variants variant ${index} must be an object`);
|
|
866
|
+
}
|
|
867
|
+
return { name: typeof variant.name === "string" && variant.name.length > 0 ? variant.name : `variant-${index}` };
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
function resolvePrimitiveMaterialVariants(primitive, variants, materials, meshIndex, primitiveIndex) {
|
|
871
|
+
const mappings = primitive.extensions?.KHR_materials_variants?.mappings;
|
|
872
|
+
if (!mappings)
|
|
873
|
+
return [];
|
|
874
|
+
if (variants.length === 0) {
|
|
875
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} uses KHR_materials_variants but the extension is not declared`);
|
|
876
|
+
}
|
|
877
|
+
const resolved = [];
|
|
878
|
+
for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex += 1) {
|
|
879
|
+
const mapping = mappings[mappingIndex];
|
|
880
|
+
if (!Number.isInteger(mapping.material) || mapping.material < 0) {
|
|
881
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} material must be a non-negative integer`);
|
|
882
|
+
}
|
|
883
|
+
const material = materials[mapping.material];
|
|
884
|
+
if (!material) {
|
|
885
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} references missing material ${mapping.material}`);
|
|
886
|
+
}
|
|
887
|
+
if (!Array.isArray(mapping.variants) || mapping.variants.length === 0) {
|
|
888
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} variants must be a non-empty array`);
|
|
889
|
+
}
|
|
890
|
+
for (const variantIndex of mapping.variants) {
|
|
891
|
+
if (!Number.isInteger(variantIndex) || variantIndex < 0) {
|
|
892
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} variant must be a non-negative integer`);
|
|
893
|
+
}
|
|
894
|
+
const variant = variants[variantIndex];
|
|
895
|
+
if (!variant) {
|
|
896
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} references missing variant ${variantIndex}`);
|
|
897
|
+
}
|
|
898
|
+
resolved.push({
|
|
899
|
+
variantIndex,
|
|
900
|
+
variant: variant.name,
|
|
901
|
+
materialIndex: mapping.material,
|
|
902
|
+
material: material.name
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return resolved;
|
|
907
|
+
}
|
|
908
|
+
function resolveAlphaMode(value, materialIndex) {
|
|
909
|
+
if (value === undefined)
|
|
910
|
+
return "OPAQUE";
|
|
911
|
+
if (value === "OPAQUE" || value === "MASK" || value === "BLEND")
|
|
912
|
+
return value;
|
|
913
|
+
throw new Error(`glTF material ${materialIndex} alphaMode must be OPAQUE, MASK, or BLEND`);
|
|
914
|
+
}
|
|
915
|
+
function resolveTextureInfo(textures, info, label) {
|
|
916
|
+
if (!info)
|
|
917
|
+
return undefined;
|
|
918
|
+
if (!Number.isInteger(info.index) || info.index < 0) {
|
|
919
|
+
throw new Error(`glTF ${label} index must be a non-negative integer`);
|
|
920
|
+
}
|
|
921
|
+
const texture = textures[info.index];
|
|
922
|
+
if (!texture) {
|
|
923
|
+
throw new Error(`glTF ${label} references missing texture ${info.index}`);
|
|
924
|
+
}
|
|
925
|
+
const transform = info.extensions?.KHR_texture_transform;
|
|
926
|
+
const texCoord = transform?.texCoord ?? info.texCoord ?? 0;
|
|
927
|
+
if (!Number.isInteger(texCoord) || texCoord < 0) {
|
|
928
|
+
throw new RangeError(`glTF ${label} texCoord must be a non-negative integer`);
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
texture: info.index,
|
|
932
|
+
image: texture.source,
|
|
933
|
+
texCoord,
|
|
934
|
+
...(transform ? { transform: resolveTextureTransform(transform) } : {})
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function usesGLTFExtension(json, extension) {
|
|
938
|
+
return Boolean(json.extensionsRequired?.includes(extension) || json.extensionsUsed?.includes(extension));
|
|
939
|
+
}
|
|
940
|
+
function usesGLTFMeshoptExtension(json) {
|
|
941
|
+
return GLTF_MESHOPT_EXTENSION_NAMES.some((extension) => usesGLTFExtension(json, extension));
|
|
942
|
+
}
|
|
943
|
+
const GLTF_MESHOPT_EXTENSION_NAMES = ["EXT_meshopt_compression", "KHR_meshopt_compression"];
|
|
944
|
+
function validatePrimitiveAttributeAccessors(json, primitive, meshQuantizationEnabled, meshIndex, primitiveIndex) {
|
|
945
|
+
for (const [semantic, accessorIndex] of Object.entries(primitive.attributes)) {
|
|
946
|
+
if (semantic !== "POSITION" && semantic !== "NORMAL" && semantic !== "TANGENT") {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const accessor = json.accessors?.[accessorIndex];
|
|
950
|
+
if (!accessor) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (!isAccessorComponentType(accessor.componentType)) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
validateQuantizedAttributeComponentType(accessor.componentType, semantic, meshQuantizationEnabled, meshIndex, primitiveIndex);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function validateQuantizedAttributeComponentType(componentType, semantic, meshQuantizationEnabled, meshIndex, primitiveIndex) {
|
|
960
|
+
if (componentType === 5126) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (!meshQuantizationEnabled) {
|
|
964
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} ${semantic} accessor uses non-FLOAT componentType ${componentType}; declare KHR_mesh_quantization to import quantized attributes`);
|
|
965
|
+
}
|
|
966
|
+
if (componentType === 5120 || componentType === 5121 || componentType === 5122 || componentType === 5123) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} ${semantic} accessor componentType ${componentType} is unsupported for KHR_mesh_quantization`);
|
|
970
|
+
}
|
|
971
|
+
function normalizeSkinWeights(weights, meshIndex, primitiveIndex) {
|
|
972
|
+
if (weights.some((weight) => !Number.isFinite(weight) || weight < 0)) {
|
|
973
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} WEIGHTS_0 must contain finite non-negative values`);
|
|
974
|
+
}
|
|
975
|
+
const sum = weights[0] + weights[1] + weights[2] + weights[3];
|
|
976
|
+
if (sum <= 0) {
|
|
977
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} WEIGHTS_0 must sum to a positive value`);
|
|
978
|
+
}
|
|
979
|
+
if (Math.abs(sum - 1) <= 1e-5)
|
|
980
|
+
return weights;
|
|
981
|
+
return [
|
|
982
|
+
weights[0] / sum,
|
|
983
|
+
weights[1] / sum,
|
|
984
|
+
weights[2] / sum,
|
|
985
|
+
weights[3] / sum
|
|
986
|
+
];
|
|
987
|
+
}
|
|
988
|
+
function createCameraAssets(json) {
|
|
989
|
+
return (json.cameras ?? []).map((camera, index) => {
|
|
990
|
+
if (camera.type === "perspective") {
|
|
991
|
+
if (!camera.perspective) {
|
|
992
|
+
throw new Error(`glTF camera ${index} perspective camera requires perspective data`);
|
|
993
|
+
}
|
|
994
|
+
const yfov = resolveCameraPositive(camera.perspective.yfov, `camera ${index} perspective.yfov`);
|
|
995
|
+
if (yfov >= Math.PI) {
|
|
996
|
+
throw new RangeError(`glTF camera ${index} perspective.yfov must be less than PI`);
|
|
997
|
+
}
|
|
998
|
+
const znear = resolveCameraPositive(camera.perspective.znear, `camera ${index} perspective.znear`);
|
|
999
|
+
const zfar = camera.perspective.zfar === undefined
|
|
1000
|
+
? 1000
|
|
1001
|
+
: resolveCameraPositive(camera.perspective.zfar, `camera ${index} perspective.zfar`);
|
|
1002
|
+
if (zfar <= znear) {
|
|
1003
|
+
throw new RangeError(`glTF camera ${index} perspective.zfar must be greater than znear`);
|
|
1004
|
+
}
|
|
1005
|
+
const aspectRatio = resolveCameraPositive(camera.perspective.aspectRatio ?? 1, `camera ${index} perspective.aspectRatio`);
|
|
1006
|
+
return {
|
|
1007
|
+
name: camera.name ?? `camera-${index}`,
|
|
1008
|
+
type: "perspective",
|
|
1009
|
+
perspective: { aspectRatio, yfov, znear, zfar }
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
if (camera.type === "orthographic") {
|
|
1013
|
+
if (!camera.orthographic) {
|
|
1014
|
+
throw new Error(`glTF camera ${index} orthographic camera requires orthographic data`);
|
|
1015
|
+
}
|
|
1016
|
+
const xmag = resolveCameraPositive(camera.orthographic.xmag, `camera ${index} orthographic.xmag`);
|
|
1017
|
+
const ymag = resolveCameraPositive(camera.orthographic.ymag, `camera ${index} orthographic.ymag`);
|
|
1018
|
+
const znear = resolveCameraPositive(camera.orthographic.znear, `camera ${index} orthographic.znear`);
|
|
1019
|
+
const zfar = resolveCameraPositive(camera.orthographic.zfar, `camera ${index} orthographic.zfar`);
|
|
1020
|
+
if (zfar <= znear) {
|
|
1021
|
+
throw new RangeError(`glTF camera ${index} orthographic.zfar must be greater than znear`);
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
name: camera.name ?? `camera-${index}`,
|
|
1025
|
+
type: "orthographic",
|
|
1026
|
+
orthographic: { xmag, ymag, znear, zfar }
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
throw new Error(`glTF camera ${index} has unsupported type ${String(camera.type)}`);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
function resolveCameraPositive(value, label) {
|
|
1033
|
+
const resolved = value;
|
|
1034
|
+
if (resolved === undefined || !Number.isFinite(resolved) || resolved <= 0) {
|
|
1035
|
+
throw new RangeError(`glTF ${label} must be finite and positive`);
|
|
1036
|
+
}
|
|
1037
|
+
return resolved;
|
|
1038
|
+
}
|
|
1039
|
+
function createLightAssets(json) {
|
|
1040
|
+
return (json.extensions?.KHR_lights_punctual?.lights ?? []).map((light, index) => {
|
|
1041
|
+
const color = resolveColor3(light.color, [1, 1, 1], `light ${index} color`);
|
|
1042
|
+
const intensity = resolveNonNegative(light.intensity, 1, `light ${index} intensity`);
|
|
1043
|
+
if (light.type !== "directional" && light.type !== "point" && light.type !== "spot") {
|
|
1044
|
+
throw new Error(`glTF light ${index} has unsupported type ${String(light.type)}`);
|
|
1045
|
+
}
|
|
1046
|
+
if (light.type === "directional") {
|
|
1047
|
+
return { name: light.name ?? `light-${index}`, type: "directional", color, intensity };
|
|
1048
|
+
}
|
|
1049
|
+
const range = resolvePositive(light.range, 10, `light ${index} range`);
|
|
1050
|
+
if (light.type === "point") {
|
|
1051
|
+
return { name: light.name ?? `light-${index}`, type: "point", color, intensity, range };
|
|
1052
|
+
}
|
|
1053
|
+
const outerConeAngle = resolveConeAngle(light.spot?.outerConeAngle, Math.PI / 4, `light ${index} spot.outerConeAngle`, false);
|
|
1054
|
+
const innerConeAngle = resolveConeAngle(light.spot?.innerConeAngle, 0, `light ${index} spot.innerConeAngle`, true);
|
|
1055
|
+
if (innerConeAngle > outerConeAngle) {
|
|
1056
|
+
throw new RangeError(`glTF light ${index} spot.innerConeAngle must be less than or equal to outerConeAngle`);
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
name: light.name ?? `light-${index}`,
|
|
1060
|
+
type: "spot",
|
|
1061
|
+
color,
|
|
1062
|
+
intensity,
|
|
1063
|
+
range,
|
|
1064
|
+
spot: { innerConeAngle, outerConeAngle }
|
|
1065
|
+
};
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
function resolveConeAngle(value, fallback, label, allowZero) {
|
|
1069
|
+
const resolved = value ?? fallback;
|
|
1070
|
+
const valid = allowZero ? resolved >= 0 : resolved > 0;
|
|
1071
|
+
if (!Number.isFinite(resolved) || !valid || resolved >= Math.PI / 2) {
|
|
1072
|
+
const lower = allowZero ? "[0" : "(0";
|
|
1073
|
+
throw new RangeError(`glTF ${label} must be finite and within ${lower}, PI / 2)`);
|
|
1074
|
+
}
|
|
1075
|
+
return resolved;
|
|
1076
|
+
}
|
|
1077
|
+
function resolveEmissiveStrength(material, materialIndex) {
|
|
1078
|
+
const strength = material.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1;
|
|
1079
|
+
if (!Number.isFinite(strength) || strength < 0) {
|
|
1080
|
+
throw new RangeError(`glTF material ${materialIndex} KHR_materials_emissive_strength.emissiveStrength must be finite and non-negative`);
|
|
1081
|
+
}
|
|
1082
|
+
return strength;
|
|
1083
|
+
}
|
|
1084
|
+
function resolveClearcoat(material, textures, materialIndex) {
|
|
1085
|
+
const extension = material.extensions?.KHR_materials_clearcoat;
|
|
1086
|
+
if (!extension)
|
|
1087
|
+
return {};
|
|
1088
|
+
return {
|
|
1089
|
+
clearcoat: {
|
|
1090
|
+
factor: resolveUnit(extension.clearcoatFactor, 0, `material ${materialIndex} KHR_materials_clearcoat.clearcoatFactor`),
|
|
1091
|
+
texture: resolveTextureInfo(textures, extension.clearcoatTexture, `material ${materialIndex} KHR_materials_clearcoat.clearcoatTexture`),
|
|
1092
|
+
roughnessFactor: resolveUnit(extension.clearcoatRoughnessFactor, 0, `material ${materialIndex} KHR_materials_clearcoat.clearcoatRoughnessFactor`),
|
|
1093
|
+
roughnessTexture: resolveTextureInfo(textures, extension.clearcoatRoughnessTexture, `material ${materialIndex} KHR_materials_clearcoat.clearcoatRoughnessTexture`),
|
|
1094
|
+
normalTexture: withDefaultScale(resolveTextureInfo(textures, extension.clearcoatNormalTexture, `material ${materialIndex} KHR_materials_clearcoat.clearcoatNormalTexture`), extension.clearcoatNormalTexture?.scale)
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function resolveTransmission(material, textures, materialIndex) {
|
|
1099
|
+
const extension = material.extensions?.KHR_materials_transmission;
|
|
1100
|
+
if (!extension)
|
|
1101
|
+
return {};
|
|
1102
|
+
return {
|
|
1103
|
+
transmission: {
|
|
1104
|
+
factor: resolveUnit(extension.transmissionFactor, 0, `material ${materialIndex} KHR_materials_transmission.transmissionFactor`),
|
|
1105
|
+
texture: resolveTextureInfo(textures, extension.transmissionTexture, `material ${materialIndex} KHR_materials_transmission.transmissionTexture`)
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function resolveDiffuseTransmission(material, textures, pbrSpecularGlossiness, materialIndex) {
|
|
1110
|
+
const extension = material.extensions?.KHR_materials_diffuse_transmission;
|
|
1111
|
+
if (!extension)
|
|
1112
|
+
return {};
|
|
1113
|
+
if (material.extensions?.KHR_materials_unlit !== undefined) {
|
|
1114
|
+
throw new Error(`glTF material ${materialIndex} KHR_materials_diffuse_transmission must not be combined with KHR_materials_unlit`);
|
|
1115
|
+
}
|
|
1116
|
+
if (pbrSpecularGlossiness) {
|
|
1117
|
+
throw new Error(`glTF material ${materialIndex} KHR_materials_diffuse_transmission must not be combined with KHR_materials_pbrSpecularGlossiness`);
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
diffuseTransmission: {
|
|
1121
|
+
factor: resolveUnit(extension.diffuseTransmissionFactor, 0, `material ${materialIndex} KHR_materials_diffuse_transmission.diffuseTransmissionFactor`),
|
|
1122
|
+
texture: resolveTextureInfo(textures, extension.diffuseTransmissionTexture, `material ${materialIndex} KHR_materials_diffuse_transmission.diffuseTransmissionTexture`),
|
|
1123
|
+
colorFactor: resolveColor3(extension.diffuseTransmissionColorFactor, [1, 1, 1], `material ${materialIndex} KHR_materials_diffuse_transmission.diffuseTransmissionColorFactor`),
|
|
1124
|
+
colorTexture: resolveTextureInfo(textures, extension.diffuseTransmissionColorTexture, `material ${materialIndex} KHR_materials_diffuse_transmission.diffuseTransmissionColorTexture`)
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function resolveVolume(material, textures, materialIndex) {
|
|
1129
|
+
const extension = material.extensions?.KHR_materials_volume;
|
|
1130
|
+
if (!extension)
|
|
1131
|
+
return {};
|
|
1132
|
+
return {
|
|
1133
|
+
volume: {
|
|
1134
|
+
thicknessFactor: resolveNonNegative(extension.thicknessFactor, 0, `material ${materialIndex} KHR_materials_volume.thicknessFactor`),
|
|
1135
|
+
thicknessTexture: resolveTextureInfo(textures, extension.thicknessTexture, `material ${materialIndex} KHR_materials_volume.thicknessTexture`),
|
|
1136
|
+
attenuationDistance: resolvePositive(extension.attenuationDistance, Number.POSITIVE_INFINITY, `material ${materialIndex} KHR_materials_volume.attenuationDistance`),
|
|
1137
|
+
attenuationColor: resolveColor3(extension.attenuationColor, [1, 1, 1], `material ${materialIndex} KHR_materials_volume.attenuationColor`)
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function resolveIOR(material, materialIndex) {
|
|
1142
|
+
const extension = material.extensions?.KHR_materials_ior;
|
|
1143
|
+
if (!extension)
|
|
1144
|
+
return {};
|
|
1145
|
+
const ior = extension.ior ?? 1.5;
|
|
1146
|
+
if (!Number.isFinite(ior) || ior < 1) {
|
|
1147
|
+
throw new RangeError(`glTF material ${materialIndex} KHR_materials_ior.ior must be finite and at least 1`);
|
|
1148
|
+
}
|
|
1149
|
+
return { ior };
|
|
1150
|
+
}
|
|
1151
|
+
function resolveSpecular(material, textures, materialIndex) {
|
|
1152
|
+
const extension = material.extensions?.KHR_materials_specular;
|
|
1153
|
+
if (!extension)
|
|
1154
|
+
return {};
|
|
1155
|
+
return {
|
|
1156
|
+
specular: {
|
|
1157
|
+
factor: resolveUnit(extension.specularFactor, 1, `material ${materialIndex} KHR_materials_specular.specularFactor`),
|
|
1158
|
+
texture: resolveTextureInfo(textures, extension.specularTexture, `material ${materialIndex} KHR_materials_specular.specularTexture`),
|
|
1159
|
+
colorFactor: resolveNonNegativeColor3(extension.specularColorFactor, [1, 1, 1], `material ${materialIndex} KHR_materials_specular.specularColorFactor`),
|
|
1160
|
+
colorTexture: resolveTextureInfo(textures, extension.specularColorTexture, `material ${materialIndex} KHR_materials_specular.specularColorTexture`)
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function resolvePBRSpecularGlossiness(material, textures, materialIndex) {
|
|
1165
|
+
const extension = material.extensions?.KHR_materials_pbrSpecularGlossiness;
|
|
1166
|
+
if (!extension)
|
|
1167
|
+
return {};
|
|
1168
|
+
return {
|
|
1169
|
+
pbrSpecularGlossiness: {
|
|
1170
|
+
diffuseFactor: resolveColor4(extension.diffuseFactor, [1, 1, 1, 1], `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.diffuseFactor`),
|
|
1171
|
+
diffuseTexture: resolveTextureInfo(textures, extension.diffuseTexture, `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.diffuseTexture`),
|
|
1172
|
+
specularFactor: resolveColor3(extension.specularFactor, [1, 1, 1], `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.specularFactor`),
|
|
1173
|
+
glossinessFactor: resolveUnit(extension.glossinessFactor, 1, `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.glossinessFactor`),
|
|
1174
|
+
specularGlossinessTexture: resolveTextureInfo(textures, extension.specularGlossinessTexture, `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.specularGlossinessTexture`)
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function resolveSheen(material, textures, materialIndex) {
|
|
1179
|
+
const extension = material.extensions?.KHR_materials_sheen;
|
|
1180
|
+
if (!extension)
|
|
1181
|
+
return {};
|
|
1182
|
+
return {
|
|
1183
|
+
sheen: {
|
|
1184
|
+
colorFactor: resolveColor3(extension.sheenColorFactor, [0, 0, 0], `material ${materialIndex} KHR_materials_sheen.sheenColorFactor`),
|
|
1185
|
+
colorTexture: resolveTextureInfo(textures, extension.sheenColorTexture, `material ${materialIndex} KHR_materials_sheen.sheenColorTexture`),
|
|
1186
|
+
roughnessFactor: resolveUnit(extension.sheenRoughnessFactor, 0, `material ${materialIndex} KHR_materials_sheen.sheenRoughnessFactor`),
|
|
1187
|
+
roughnessTexture: resolveTextureInfo(textures, extension.sheenRoughnessTexture, `material ${materialIndex} KHR_materials_sheen.sheenRoughnessTexture`)
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
function resolveAnisotropy(material, textures, materialIndex) {
|
|
1192
|
+
const extension = material.extensions?.KHR_materials_anisotropy;
|
|
1193
|
+
if (!extension)
|
|
1194
|
+
return {};
|
|
1195
|
+
return {
|
|
1196
|
+
anisotropy: {
|
|
1197
|
+
strength: resolveUnit(extension.anisotropyStrength, 0, `material ${materialIndex} KHR_materials_anisotropy.anisotropyStrength`),
|
|
1198
|
+
rotation: resolveFiniteNumber(extension.anisotropyRotation, 0, `material ${materialIndex} KHR_materials_anisotropy.anisotropyRotation`),
|
|
1199
|
+
texture: resolveTextureInfo(textures, extension.anisotropyTexture, `material ${materialIndex} KHR_materials_anisotropy.anisotropyTexture`)
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function resolveIridescence(material, textures, materialIndex) {
|
|
1204
|
+
const extension = material.extensions?.KHR_materials_iridescence;
|
|
1205
|
+
if (!extension)
|
|
1206
|
+
return {};
|
|
1207
|
+
const thicknessMinimum = resolveNonNegative(extension.iridescenceThicknessMinimum, 100, `material ${materialIndex} KHR_materials_iridescence.iridescenceThicknessMinimum`);
|
|
1208
|
+
const thicknessMaximum = resolveNonNegative(extension.iridescenceThicknessMaximum, 400, `material ${materialIndex} KHR_materials_iridescence.iridescenceThicknessMaximum`);
|
|
1209
|
+
if (thicknessMaximum < thicknessMinimum) {
|
|
1210
|
+
throw new RangeError(`glTF material ${materialIndex} KHR_materials_iridescence.iridescenceThicknessMaximum must be greater than or equal to iridescenceThicknessMinimum`);
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
iridescence: {
|
|
1214
|
+
factor: resolveUnit(extension.iridescenceFactor, 0, `material ${materialIndex} KHR_materials_iridescence.iridescenceFactor`),
|
|
1215
|
+
texture: resolveTextureInfo(textures, extension.iridescenceTexture, `material ${materialIndex} KHR_materials_iridescence.iridescenceTexture`),
|
|
1216
|
+
ior: resolveIridescenceIOR(extension.iridescenceIor, materialIndex),
|
|
1217
|
+
thicknessMinimum,
|
|
1218
|
+
thicknessMaximum,
|
|
1219
|
+
thicknessTexture: resolveTextureInfo(textures, extension.iridescenceThicknessTexture, `material ${materialIndex} KHR_materials_iridescence.iridescenceThicknessTexture`)
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
function resolveIridescenceIOR(value, materialIndex) {
|
|
1224
|
+
const ior = value ?? 1.3;
|
|
1225
|
+
if (!Number.isFinite(ior) || ior < 1 || ior > 3) {
|
|
1226
|
+
throw new RangeError(`glTF material ${materialIndex} KHR_materials_iridescence.iridescenceIor must be finite and within [1, 3]`);
|
|
1227
|
+
}
|
|
1228
|
+
return ior;
|
|
1229
|
+
}
|
|
1230
|
+
function resolveDispersion(material, materialIndex) {
|
|
1231
|
+
const extension = material.extensions?.KHR_materials_dispersion;
|
|
1232
|
+
if (!extension)
|
|
1233
|
+
return {};
|
|
1234
|
+
return {
|
|
1235
|
+
dispersion: resolveNonNegative(extension.dispersion, 0, `material ${materialIndex} KHR_materials_dispersion.dispersion`)
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function resolveTextureTransform(transform) {
|
|
1239
|
+
return {
|
|
1240
|
+
offset: resolveFiniteVec2(transform.offset, [0, 0], "KHR_texture_transform.offset"),
|
|
1241
|
+
scale: resolveFiniteVec2(transform.scale, [1, 1], "KHR_texture_transform.scale"),
|
|
1242
|
+
rotation: resolveFiniteNumber(transform.rotation, 0, "KHR_texture_transform.rotation")
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function withDefaultScale(info, scale) {
|
|
1246
|
+
return info ? { ...info, scale: resolveNonNegative(scale, 1, "normalTexture.scale") } : undefined;
|
|
1247
|
+
}
|
|
1248
|
+
function withDefaultStrength(info, strength) {
|
|
1249
|
+
return info ? { ...info, strength: resolveUnit(strength, 1, "occlusionTexture.strength") } : undefined;
|
|
1250
|
+
}
|
|
1251
|
+
function resolveColor4(value, fallback, label) {
|
|
1252
|
+
const channels = value ?? fallback;
|
|
1253
|
+
if (channels.length !== 4 || channels.some((channel) => !Number.isFinite(channel) || channel < 0 || channel > 1)) {
|
|
1254
|
+
throw new RangeError(`glTF ${label} must contain four finite values in [0, 1]`);
|
|
1255
|
+
}
|
|
1256
|
+
return [channels[0], channels[1], channels[2], channels[3]];
|
|
1257
|
+
}
|
|
1258
|
+
function resolveColor3(value, fallback, label) {
|
|
1259
|
+
const channels = value ?? fallback;
|
|
1260
|
+
if (channels.length !== 3 || channels.some((channel) => !Number.isFinite(channel) || channel < 0 || channel > 1)) {
|
|
1261
|
+
throw new RangeError(`glTF ${label} must contain three finite values in [0, 1]`);
|
|
1262
|
+
}
|
|
1263
|
+
return [channels[0], channels[1], channels[2]];
|
|
1264
|
+
}
|
|
1265
|
+
function resolveNonNegativeColor3(value, fallback, label) {
|
|
1266
|
+
const channels = value ?? fallback;
|
|
1267
|
+
if (channels.length !== 3 || channels.some((channel) => !Number.isFinite(channel) || channel < 0)) {
|
|
1268
|
+
throw new RangeError(`glTF ${label} must contain three finite non-negative values`);
|
|
1269
|
+
}
|
|
1270
|
+
return [channels[0], channels[1], channels[2]];
|
|
1271
|
+
}
|
|
1272
|
+
function resolveUnit(value, fallback, label) {
|
|
1273
|
+
const resolved = value ?? fallback;
|
|
1274
|
+
if (!Number.isFinite(resolved) || resolved < 0 || resolved > 1) {
|
|
1275
|
+
throw new RangeError(`glTF ${label} must be finite and within [0, 1]`);
|
|
1276
|
+
}
|
|
1277
|
+
return resolved;
|
|
1278
|
+
}
|
|
1279
|
+
function resolveNonNegative(value, fallback, label) {
|
|
1280
|
+
const resolved = value ?? fallback;
|
|
1281
|
+
if (!Number.isFinite(resolved) || resolved < 0) {
|
|
1282
|
+
throw new RangeError(`glTF ${label} must be finite and non-negative`);
|
|
1283
|
+
}
|
|
1284
|
+
return resolved;
|
|
1285
|
+
}
|
|
1286
|
+
function resolvePositive(value, fallback, label) {
|
|
1287
|
+
const resolved = value ?? fallback;
|
|
1288
|
+
if (resolved === Number.POSITIVE_INFINITY)
|
|
1289
|
+
return resolved;
|
|
1290
|
+
if (!Number.isFinite(resolved) || resolved <= 0) {
|
|
1291
|
+
throw new RangeError(`glTF ${label} must be finite and positive`);
|
|
1292
|
+
}
|
|
1293
|
+
return resolved;
|
|
1294
|
+
}
|
|
1295
|
+
function resolveFiniteNumber(value, fallback, label) {
|
|
1296
|
+
const resolved = value ?? fallback;
|
|
1297
|
+
if (!Number.isFinite(resolved)) {
|
|
1298
|
+
throw new RangeError(`glTF ${label} must be finite`);
|
|
1299
|
+
}
|
|
1300
|
+
return resolved;
|
|
1301
|
+
}
|
|
1302
|
+
function resolveFiniteVec2(value, fallback, label) {
|
|
1303
|
+
const resolved = value ?? fallback;
|
|
1304
|
+
if (resolved.length !== 2 || resolved.some((channel) => !Number.isFinite(channel))) {
|
|
1305
|
+
throw new RangeError(`glTF ${label} must contain two finite values`);
|
|
1306
|
+
}
|
|
1307
|
+
return [resolved[0], resolved[1]];
|
|
1308
|
+
}
|
|
1309
|
+
function createSkinAssets(json, buffers, accessorCache) {
|
|
1310
|
+
if (!json.skins || json.skins.length === 0)
|
|
1311
|
+
return [];
|
|
1312
|
+
const nodeWorldMatrices = computeGLTFNodeWorldMatrices(json);
|
|
1313
|
+
const nodeNameForIndex = createGLTFNodeNameResolver(json);
|
|
1314
|
+
return (json.skins ?? []).map((skin, skinIndex) => {
|
|
1315
|
+
if (skin.joints.length === 0) {
|
|
1316
|
+
throw new Error(`glTF skin ${skinIndex} requires at least one joint`);
|
|
1317
|
+
}
|
|
1318
|
+
validateSkinDescriptor(json, skin, skinIndex);
|
|
1319
|
+
const parentByNode = parentIndexByNode(json);
|
|
1320
|
+
const inverseBindMatrices = skin.inverseBindMatrices === undefined
|
|
1321
|
+
? []
|
|
1322
|
+
: readAccessor(json, buffers, skin.inverseBindMatrices, accessorCache).map(toMat4);
|
|
1323
|
+
if (inverseBindMatrices.length > 0 && inverseBindMatrices.length !== skin.joints.length) {
|
|
1324
|
+
throw new Error(`glTF skin ${skinIndex} inverseBindMatrices count must match joints count`);
|
|
1325
|
+
}
|
|
1326
|
+
const jointSet = new Set(skin.joints);
|
|
1327
|
+
const orderedJoints = orderSkinJoints(json, skin.joints);
|
|
1328
|
+
const boneIndexByNode = new Map();
|
|
1329
|
+
for (let index = 0; index < orderedJoints.length; index += 1) {
|
|
1330
|
+
boneIndexByNode.set(orderedJoints[index], index);
|
|
1331
|
+
}
|
|
1332
|
+
const skeleton = new Skeleton(orderedJoints.map((nodeIndex) => {
|
|
1333
|
+
const node = json.nodes?.[nodeIndex];
|
|
1334
|
+
if (!node) {
|
|
1335
|
+
throw new Error(`glTF skin ${skinIndex} references missing joint node ${nodeIndex}`);
|
|
1336
|
+
}
|
|
1337
|
+
const parentNode = parentByNode.get(nodeIndex);
|
|
1338
|
+
const parentIndex = parentNode !== undefined && jointSet.has(parentNode) ? boneIndexByNode.get(parentNode) ?? -1 : -1;
|
|
1339
|
+
const transform = resolveNodeTransform(node, nodeIndex);
|
|
1340
|
+
return {
|
|
1341
|
+
name: nodeNameForIndex(nodeIndex),
|
|
1342
|
+
parentIndex,
|
|
1343
|
+
translation: transform.translation,
|
|
1344
|
+
rotation: transform.rotation,
|
|
1345
|
+
scale: transform.scale,
|
|
1346
|
+
inverseBindMatrix: inverseBindMatrices[skin.joints.indexOf(nodeIndex)] ?? identityMat4()
|
|
1347
|
+
};
|
|
1348
|
+
}));
|
|
1349
|
+
return {
|
|
1350
|
+
name: skin.name ?? `skin-${skinIndex}`,
|
|
1351
|
+
joints: skin.joints,
|
|
1352
|
+
jointNames: skin.joints.map((nodeIndex) => nodeNameForIndex(nodeIndex)),
|
|
1353
|
+
inverseBindMatrices: skin.joints.map((nodeIndex) => inverseBindMatrices[skin.joints.indexOf(nodeIndex)] ?? identityMat4()),
|
|
1354
|
+
skeletonRoot: skin.skeleton,
|
|
1355
|
+
skeleton,
|
|
1356
|
+
jointBindMatrices: skin.joints.map((nodeIndex) => multiplyLoaderMat4(nodeWorldMatrices.get(nodeIndex) ?? identityMat4(), inverseBindMatrices[skin.joints.indexOf(nodeIndex)] ?? identityMat4())),
|
|
1357
|
+
...(skin.joints.length <= MAX_RENDERABLE_SKIN_JOINTS ? { skinningPalette: buildGLTFSkinningPalette(skeleton, orderedJoints, skin.joints) } : {})
|
|
1358
|
+
};
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
function computeGLTFNodeWorldMatrices(json) {
|
|
1362
|
+
const nodes = json.nodes ?? [];
|
|
1363
|
+
const parents = parentIndexByNode(json);
|
|
1364
|
+
const matrices = new Map();
|
|
1365
|
+
const visiting = new Set();
|
|
1366
|
+
const resolve = (nodeIndex) => {
|
|
1367
|
+
const existing = matrices.get(nodeIndex);
|
|
1368
|
+
if (existing)
|
|
1369
|
+
return existing;
|
|
1370
|
+
if (visiting.has(nodeIndex)) {
|
|
1371
|
+
throw new Error(`glTF node graph contains a cycle at node ${nodeIndex}`);
|
|
1372
|
+
}
|
|
1373
|
+
const node = nodes[nodeIndex];
|
|
1374
|
+
if (!node)
|
|
1375
|
+
return identityMat4();
|
|
1376
|
+
visiting.add(nodeIndex);
|
|
1377
|
+
const transform = resolveNodeTransform(node, nodeIndex);
|
|
1378
|
+
const local = composeLoaderMat4(transform.translation, transform.rotation, transform.scale);
|
|
1379
|
+
const parent = parents.get(nodeIndex);
|
|
1380
|
+
const world = parent === undefined ? local : multiplyLoaderMat4(resolve(parent), local);
|
|
1381
|
+
matrices.set(nodeIndex, world);
|
|
1382
|
+
visiting.delete(nodeIndex);
|
|
1383
|
+
return world;
|
|
1384
|
+
};
|
|
1385
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
1386
|
+
resolve(index);
|
|
1387
|
+
}
|
|
1388
|
+
return matrices;
|
|
1389
|
+
}
|
|
1390
|
+
function buildGLTFSkinningPalette(skeleton, orderedJoints, gltfJoints) {
|
|
1391
|
+
const orderedPalette = skeleton.matrixPalette();
|
|
1392
|
+
const paletteByNode = new Map();
|
|
1393
|
+
for (let index = 0; index < orderedJoints.length; index += 1) {
|
|
1394
|
+
paletteByNode.set(orderedJoints[index], orderedPalette[index]);
|
|
1395
|
+
}
|
|
1396
|
+
const matrices = new Float32Array(gltfJoints.length * 16);
|
|
1397
|
+
for (let index = 0; index < gltfJoints.length; index += 1) {
|
|
1398
|
+
const matrix = paletteByNode.get(gltfJoints[index]);
|
|
1399
|
+
if (!matrix) {
|
|
1400
|
+
throw new Error(`glTF skin references missing ordered joint ${gltfJoints[index]}`);
|
|
1401
|
+
}
|
|
1402
|
+
matrices.set(matrix, index * 16);
|
|
1403
|
+
}
|
|
1404
|
+
return { jointCount: gltfJoints.length, matrices };
|
|
1405
|
+
}
|
|
1406
|
+
function validateSkinDescriptor(json, skin, skinIndex) {
|
|
1407
|
+
const seen = new Set();
|
|
1408
|
+
for (const joint of skin.joints) {
|
|
1409
|
+
if (!Number.isInteger(joint) || joint < 0) {
|
|
1410
|
+
throw new Error(`glTF skin ${skinIndex} joint indices must be non-negative integers`);
|
|
1411
|
+
}
|
|
1412
|
+
if (seen.has(joint)) {
|
|
1413
|
+
throw new Error(`glTF skin ${skinIndex} contains duplicate joint ${joint}`);
|
|
1414
|
+
}
|
|
1415
|
+
seen.add(joint);
|
|
1416
|
+
}
|
|
1417
|
+
if (skin.skeleton !== undefined && (!Number.isInteger(skin.skeleton) || skin.skeleton < 0 || !json.nodes?.[skin.skeleton])) {
|
|
1418
|
+
throw new Error(`glTF skin ${skinIndex} references missing skeleton node ${skin.skeleton}`);
|
|
1419
|
+
}
|
|
1420
|
+
if (skin.inverseBindMatrices !== undefined && (!Number.isInteger(skin.inverseBindMatrices) || skin.inverseBindMatrices < 0)) {
|
|
1421
|
+
throw new Error(`glTF skin ${skinIndex} inverseBindMatrices must reference a non-negative accessor index`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function createNodeInstanceTransforms(json, buffers, accessorCache) {
|
|
1425
|
+
const transforms = new Map();
|
|
1426
|
+
for (const [nodeIndex, node] of (json.nodes ?? []).entries()) {
|
|
1427
|
+
const extension = node.extensions?.EXT_mesh_gpu_instancing;
|
|
1428
|
+
if (!extension)
|
|
1429
|
+
continue;
|
|
1430
|
+
if (!isObjectRecord(extension.attributes) || Array.isArray(extension.attributes)) {
|
|
1431
|
+
throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing.attributes must be an object`);
|
|
1432
|
+
}
|
|
1433
|
+
const allowed = new Set(["TRANSLATION", "ROTATION", "SCALE"]);
|
|
1434
|
+
for (const semantic of Object.keys(extension.attributes)) {
|
|
1435
|
+
if (!allowed.has(semantic)) {
|
|
1436
|
+
throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing attribute ${semantic} is unsupported`);
|
|
1437
|
+
}
|
|
1438
|
+
const accessorIndex = extension.attributes[semantic];
|
|
1439
|
+
if (!Number.isInteger(accessorIndex) || accessorIndex < 0) {
|
|
1440
|
+
throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing ${semantic} accessor must be a non-negative integer`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
const translations = readOptionalAccessor(json, buffers, extension.attributes.TRANSLATION, accessorCache);
|
|
1444
|
+
const rotations = readOptionalAccessor(json, buffers, extension.attributes.ROTATION, accessorCache);
|
|
1445
|
+
const scales = readOptionalAccessor(json, buffers, extension.attributes.SCALE, accessorCache);
|
|
1446
|
+
const instanceCount = firstPositiveLength([translations.length, rotations.length, scales.length]);
|
|
1447
|
+
if (instanceCount === 0) {
|
|
1448
|
+
throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing requires TRANSLATION, ROTATION, or SCALE attributes`);
|
|
1449
|
+
}
|
|
1450
|
+
validateInstanceAttributeCount("TRANSLATION", translations.length, instanceCount, nodeIndex);
|
|
1451
|
+
validateInstanceAttributeCount("ROTATION", rotations.length, instanceCount, nodeIndex);
|
|
1452
|
+
validateInstanceAttributeCount("SCALE", scales.length, instanceCount, nodeIndex);
|
|
1453
|
+
const packed = new Float32Array(instanceCount * 16);
|
|
1454
|
+
for (let instanceIndex = 0; instanceIndex < instanceCount; instanceIndex += 1) {
|
|
1455
|
+
const translation = toInstanceVec3(translations[instanceIndex], [0, 0, 0], `node ${nodeIndex} EXT_mesh_gpu_instancing TRANSLATION`);
|
|
1456
|
+
const rotation = toInstanceQuat(rotations[instanceIndex], [0, 0, 0, 1], `node ${nodeIndex} EXT_mesh_gpu_instancing ROTATION`);
|
|
1457
|
+
const scale = toInstanceVec3(scales[instanceIndex], [1, 1, 1], `node ${nodeIndex} EXT_mesh_gpu_instancing SCALE`);
|
|
1458
|
+
packed.set(composeMat4(translation, rotation, scale), instanceIndex * 16);
|
|
1459
|
+
}
|
|
1460
|
+
transforms.set(nodeIndex, packed);
|
|
1461
|
+
}
|
|
1462
|
+
return transforms;
|
|
1463
|
+
}
|
|
1464
|
+
function firstPositiveLength(lengths) {
|
|
1465
|
+
return lengths.find((length) => length > 0) ?? 0;
|
|
1466
|
+
}
|
|
1467
|
+
function validateInstanceAttributeCount(semantic, count, instanceCount, nodeIndex) {
|
|
1468
|
+
if (count > 0 && count !== instanceCount) {
|
|
1469
|
+
throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing ${semantic} count must match instance count`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function toInstanceVec3(row, fallback, label) {
|
|
1473
|
+
if (!row)
|
|
1474
|
+
return fallback;
|
|
1475
|
+
if (row.length < 3 || row.slice(0, 3).some((value) => !Number.isFinite(value))) {
|
|
1476
|
+
throw new Error(`glTF ${label} must contain three finite values`);
|
|
1477
|
+
}
|
|
1478
|
+
return [row[0], row[1], row[2]];
|
|
1479
|
+
}
|
|
1480
|
+
function toInstanceQuat(row, fallback, label) {
|
|
1481
|
+
if (!row)
|
|
1482
|
+
return fallback;
|
|
1483
|
+
if (row.length < 4 || row.slice(0, 4).some((value) => !Number.isFinite(value))) {
|
|
1484
|
+
throw new Error(`glTF ${label} must contain four finite values`);
|
|
1485
|
+
}
|
|
1486
|
+
return [row[0], row[1], row[2], row[3]];
|
|
1487
|
+
}
|
|
1488
|
+
function createAnimationClips(json, buffers, accessorCache) {
|
|
1489
|
+
const nodeNameForIndex = createGLTFNodeNameResolver(json);
|
|
1490
|
+
return (json.animations ?? []).map((animation, animationIndex) => {
|
|
1491
|
+
const tracks = animation.channels.flatMap((channel, channelIndex) => {
|
|
1492
|
+
const sampler = animation.samplers[channel.sampler];
|
|
1493
|
+
if (!sampler) {
|
|
1494
|
+
throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} references missing sampler ${channel.sampler}`);
|
|
1495
|
+
}
|
|
1496
|
+
if (isOptionalAnimationPointerChannel(json, channel)) {
|
|
1497
|
+
return [];
|
|
1498
|
+
}
|
|
1499
|
+
if (channel.target.node === undefined || !json.nodes?.[channel.target.node]) {
|
|
1500
|
+
throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} references missing target node ${channel.target.node ?? "undefined"}`);
|
|
1501
|
+
}
|
|
1502
|
+
const interpolation = resolveAnimationInterpolation(sampler.interpolation, animationIndex, channelIndex);
|
|
1503
|
+
const path = resolveAnimationTargetPath(channel.target.path, animationIndex, channelIndex);
|
|
1504
|
+
const times = readAccessor(json, buffers, sampler.input, accessorCache).map((row) => row[0] ?? 0);
|
|
1505
|
+
const output = readAccessor(json, buffers, sampler.output, accessorCache);
|
|
1506
|
+
const values = animationKeyframesForSampler(path, times, output, interpolation, animationIndex, channelIndex, morphWeightCountForAnimationTarget(json, channel.target.node));
|
|
1507
|
+
const valueType = valueTypeForAnimationPath(path);
|
|
1508
|
+
return [new AnimationTrack({
|
|
1509
|
+
target: `${nodeNameForIndex(channel.target.node)}.${path}`,
|
|
1510
|
+
valueType,
|
|
1511
|
+
keyframes: values
|
|
1512
|
+
})];
|
|
1513
|
+
});
|
|
1514
|
+
return new AnimationClip({ name: animation.name ?? `animation-${animationIndex}`, tracks });
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
function isOptionalAnimationPointerChannel(json, channel) {
|
|
1518
|
+
return channel.target.path === "pointer" &&
|
|
1519
|
+
json.extensionsUsed?.includes("KHR_animation_pointer") === true &&
|
|
1520
|
+
json.extensionsRequired?.includes("KHR_animation_pointer") !== true &&
|
|
1521
|
+
typeof channel.target.extensions?.KHR_animation_pointer?.pointer === "string";
|
|
1522
|
+
}
|
|
1523
|
+
function resolveAnimationInterpolation(value, animationIndex, channelIndex) {
|
|
1524
|
+
if (value === undefined || value === "LINEAR" || value === "STEP" || value === "CUBICSPLINE") {
|
|
1525
|
+
return value ?? "LINEAR";
|
|
1526
|
+
}
|
|
1527
|
+
throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} interpolation ${value} is unsupported`);
|
|
1528
|
+
}
|
|
1529
|
+
function resolveAnimationTargetPath(path, animationIndex, channelIndex) {
|
|
1530
|
+
if (path === "translation" || path === "rotation" || path === "scale" || path === "weights") {
|
|
1531
|
+
return path;
|
|
1532
|
+
}
|
|
1533
|
+
throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} target path ${path} is unsupported`);
|
|
1534
|
+
}
|
|
1535
|
+
function animationKeyframesForSampler(path, times, output, interpolation, animationIndex, channelIndex, morphWeightCount) {
|
|
1536
|
+
const normalizedOutput = path === "weights" ? normalizeWeightAnimationOutput(times, output, interpolation, animationIndex, channelIndex, morphWeightCount) : output;
|
|
1537
|
+
if (interpolation !== "CUBICSPLINE") {
|
|
1538
|
+
if (times.length !== normalizedOutput.length) {
|
|
1539
|
+
throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} input/output count mismatch`);
|
|
1540
|
+
}
|
|
1541
|
+
const samples = sanitizeAnimationSamples(times, normalizedOutput, 1);
|
|
1542
|
+
const mode = interpolation === "STEP" ? "step" : "linear";
|
|
1543
|
+
return samples.times.map((time, index) => ({
|
|
1544
|
+
time,
|
|
1545
|
+
value: animationValueForPath(path, samples.output[index]),
|
|
1546
|
+
interpolation: mode
|
|
1547
|
+
}));
|
|
1548
|
+
}
|
|
1549
|
+
if (normalizedOutput.length !== times.length * 3) {
|
|
1550
|
+
throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} CUBICSPLINE output count must be three times input count`);
|
|
1551
|
+
}
|
|
1552
|
+
const samples = sanitizeAnimationSamples(times, normalizedOutput, 3);
|
|
1553
|
+
return samples.times.map((time, index) => {
|
|
1554
|
+
const offset = index * 3;
|
|
1555
|
+
return {
|
|
1556
|
+
time,
|
|
1557
|
+
inTangent: animationValueForPath(path, samples.output[offset]),
|
|
1558
|
+
value: animationValueForPath(path, samples.output[offset + 1]),
|
|
1559
|
+
outTangent: animationValueForPath(path, samples.output[offset + 2]),
|
|
1560
|
+
interpolation: "cubicspline"
|
|
1561
|
+
};
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
function sanitizeAnimationSamples(times, output, rowsPerSample) {
|
|
1565
|
+
const cleanTimes = [];
|
|
1566
|
+
const cleanOutput = [];
|
|
1567
|
+
let previous = -Number.MAX_VALUE;
|
|
1568
|
+
for (let index = 0; index < times.length; index += 1) {
|
|
1569
|
+
const time = times[index] ?? 0;
|
|
1570
|
+
if (!Number.isFinite(time) || time < 0 || time <= previous) {
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
cleanTimes.push(time);
|
|
1574
|
+
for (let row = 0; row < rowsPerSample; row += 1) {
|
|
1575
|
+
const value = output[index * rowsPerSample + row];
|
|
1576
|
+
if (value !== undefined)
|
|
1577
|
+
cleanOutput.push(value);
|
|
1578
|
+
}
|
|
1579
|
+
previous = time;
|
|
1580
|
+
}
|
|
1581
|
+
return cleanTimes.length > 0 ? { times: cleanTimes, output: cleanOutput } : { times, output };
|
|
1582
|
+
}
|
|
1583
|
+
function morphWeightCountForAnimationTarget(json, nodeIndex) {
|
|
1584
|
+
const node = nodeIndex === undefined ? undefined : json.nodes?.[nodeIndex];
|
|
1585
|
+
const mesh = node?.mesh === undefined ? undefined : json.meshes?.[node.mesh];
|
|
1586
|
+
if (!mesh)
|
|
1587
|
+
return undefined;
|
|
1588
|
+
if (mesh.weights && mesh.weights.length > 0)
|
|
1589
|
+
return mesh.weights.length;
|
|
1590
|
+
return Math.max(0, ...mesh.primitives.map((primitive) => primitive.targets?.length ?? 0)) || undefined;
|
|
1591
|
+
}
|
|
1592
|
+
function normalizeWeightAnimationOutput(times, output, interpolation, animationIndex, channelIndex, morphWeightCount) {
|
|
1593
|
+
const samplesPerTime = interpolation === "CUBICSPLINE" ? 3 : 1;
|
|
1594
|
+
const expectedRows = times.length * samplesPerTime;
|
|
1595
|
+
if (output.length === expectedRows)
|
|
1596
|
+
return output;
|
|
1597
|
+
if (times.length === 0 || output.length % expectedRows !== 0) {
|
|
1598
|
+
return output;
|
|
1599
|
+
}
|
|
1600
|
+
const width = morphWeightCount ?? (output.length / expectedRows);
|
|
1601
|
+
if (!Number.isInteger(width) || width <= 0 || output.length !== expectedRows * width) {
|
|
1602
|
+
throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} weights output count does not match morph target count`);
|
|
1603
|
+
}
|
|
1604
|
+
const grouped = [];
|
|
1605
|
+
for (let sampleIndex = 0; sampleIndex < expectedRows; sampleIndex += 1) {
|
|
1606
|
+
const values = [];
|
|
1607
|
+
for (let component = 0; component < width; component += 1) {
|
|
1608
|
+
values.push(output[sampleIndex * width + component]?.[0] ?? 0);
|
|
1609
|
+
}
|
|
1610
|
+
grouped.push(values);
|
|
1611
|
+
}
|
|
1612
|
+
return grouped;
|
|
1613
|
+
}
|
|
1614
|
+
async function loadDocument(request) {
|
|
1615
|
+
const binary = request.url.startsWith("data:model/gltf-binary") || /\.glb(?:\?.*)?$/i.test(request.url);
|
|
1616
|
+
if (request.url.startsWith("data:")) {
|
|
1617
|
+
const data = decodeDataUri(request.url);
|
|
1618
|
+
if (binary) {
|
|
1619
|
+
return parseGLB(data, request.url);
|
|
1620
|
+
}
|
|
1621
|
+
const text = new TextDecoder().decode(data);
|
|
1622
|
+
return { json: JSON.parse(text), url: request.url, byteLength: data.byteLength };
|
|
1623
|
+
}
|
|
1624
|
+
if (typeof fetch !== "function") {
|
|
1625
|
+
throw new Error("GLTFLoader requires fetch for non-data URLs");
|
|
1626
|
+
}
|
|
1627
|
+
const response = await fetch(request.url, { signal: request.signal });
|
|
1628
|
+
if (!response.ok) {
|
|
1629
|
+
throw new Error(`glTF request failed with ${response.status}`);
|
|
1630
|
+
}
|
|
1631
|
+
if (binary) {
|
|
1632
|
+
return parseGLB(await readResponseBytes(response, request.url, "document", request), request.url);
|
|
1633
|
+
}
|
|
1634
|
+
const bytes = await readResponseBytes(response, request.url, "document", request);
|
|
1635
|
+
return { json: JSON.parse(new TextDecoder().decode(bytes)), url: request.url, byteLength: bytes.byteLength };
|
|
1636
|
+
}
|
|
1637
|
+
async function loadBuffer(buffer, index, document, request) {
|
|
1638
|
+
validateBufferDescriptor(index, buffer);
|
|
1639
|
+
if (!buffer.uri) {
|
|
1640
|
+
if (index === 0 && document.binaryChunk) {
|
|
1641
|
+
if (buffer.byteLength > document.binaryChunk.byteLength) {
|
|
1642
|
+
throw new Error(`glTF buffer 0 declares ${buffer.byteLength} bytes but GLB BIN chunk has ${document.binaryChunk.byteLength}`);
|
|
1643
|
+
}
|
|
1644
|
+
return buffer.byteLength === document.binaryChunk.byteLength
|
|
1645
|
+
? document.binaryChunk
|
|
1646
|
+
: document.binaryChunk.slice(0, buffer.byteLength);
|
|
1647
|
+
}
|
|
1648
|
+
throw new Error(`glTF buffer ${index} is missing a uri and no GLB BIN chunk is available`);
|
|
1649
|
+
}
|
|
1650
|
+
if (buffer.uri.startsWith("data:")) {
|
|
1651
|
+
return validateLoadedBufferLength(index, buffer, decodeBufferDataUri(index, buffer.uri));
|
|
1652
|
+
}
|
|
1653
|
+
if (typeof fetch !== "function") {
|
|
1654
|
+
throw new Error("GLTFLoader requires fetch for external buffers");
|
|
1655
|
+
}
|
|
1656
|
+
const url = new URL(buffer.uri, document.url).toString();
|
|
1657
|
+
const response = await fetch(url, { signal: request.signal });
|
|
1658
|
+
if (!response.ok) {
|
|
1659
|
+
throw new Error(`glTF buffer request failed with ${response.status}`);
|
|
1660
|
+
}
|
|
1661
|
+
return validateLoadedBufferLength(index, buffer, await readResponseBytes(response, url, "buffer", request));
|
|
1662
|
+
}
|
|
1663
|
+
function validateBufferDescriptor(index, buffer) {
|
|
1664
|
+
if (!Number.isInteger(buffer.byteLength) || buffer.byteLength < 0) {
|
|
1665
|
+
throw new Error(`glTF buffer ${index} byteLength must be a non-negative integer`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function decodeBufferDataUri(index, uri) {
|
|
1669
|
+
const separator = uri.indexOf(",");
|
|
1670
|
+
if (separator < 0) {
|
|
1671
|
+
throw new Error(`glTF buffer ${index} data uri must include a comma separator`);
|
|
1672
|
+
}
|
|
1673
|
+
const header = uri.slice(5, separator).toLowerCase();
|
|
1674
|
+
const mediaType = header.split(";")[0] ?? "";
|
|
1675
|
+
if (mediaType && mediaType !== "application/octet-stream" && mediaType !== "application/gltf-buffer") {
|
|
1676
|
+
throw new Error(`glTF buffer ${index} data uri media type must be application/octet-stream or application/gltf-buffer`);
|
|
1677
|
+
}
|
|
1678
|
+
const payload = uri.slice(separator + 1);
|
|
1679
|
+
if (header.endsWith(";base64")) {
|
|
1680
|
+
return decodeBufferBase64DataUri(index, payload);
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
return new TextEncoder().encode(decodeURIComponent(payload)).buffer;
|
|
1684
|
+
}
|
|
1685
|
+
catch (error) {
|
|
1686
|
+
throw new Error(`glTF buffer ${index} data uri payload must be valid URI encoding: ${error instanceof Error ? error.message : String(error)}`);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function decodeBufferBase64DataUri(index, payload) {
|
|
1690
|
+
let decodedPayload;
|
|
1691
|
+
try {
|
|
1692
|
+
decodedPayload = decodeURIComponent(payload);
|
|
1693
|
+
}
|
|
1694
|
+
catch (error) {
|
|
1695
|
+
throw new Error(`glTF buffer ${index} data uri base64 payload must be valid URI encoding: ${error instanceof Error ? error.message : String(error)}`);
|
|
1696
|
+
}
|
|
1697
|
+
const compactPayload = decodedPayload.replace(/\s+/g, "");
|
|
1698
|
+
const paddingIndex = compactPayload.indexOf("=");
|
|
1699
|
+
const paddingIsTerminal = paddingIndex < 0 || /^={1,2}$/.test(compactPayload.slice(paddingIndex));
|
|
1700
|
+
if (compactPayload.length % 4 === 1 ||
|
|
1701
|
+
!paddingIsTerminal ||
|
|
1702
|
+
!/^[A-Za-z0-9+/]*={0,2}$/.test(compactPayload)) {
|
|
1703
|
+
throw new Error(`glTF buffer ${index} data uri base64 payload is malformed`);
|
|
1704
|
+
}
|
|
1705
|
+
let binary;
|
|
1706
|
+
try {
|
|
1707
|
+
binary = atob(compactPayload);
|
|
1708
|
+
}
|
|
1709
|
+
catch (error) {
|
|
1710
|
+
throw new Error(`glTF buffer ${index} data uri base64 payload is malformed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1711
|
+
}
|
|
1712
|
+
const bytes = new Uint8Array(binary.length);
|
|
1713
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
1714
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1715
|
+
}
|
|
1716
|
+
return bytes.buffer;
|
|
1717
|
+
}
|
|
1718
|
+
function validateLoadedBufferLength(index, buffer, data) {
|
|
1719
|
+
if (data.byteLength !== buffer.byteLength) {
|
|
1720
|
+
throw new Error(`glTF buffer ${index} declares ${buffer.byteLength} bytes but loaded ${data.byteLength}`);
|
|
1721
|
+
}
|
|
1722
|
+
return data;
|
|
1723
|
+
}
|
|
1724
|
+
async function prepareBufferViews(json, buffers, meshoptDecoder) {
|
|
1725
|
+
const bufferViews = json.bufferViews ?? [];
|
|
1726
|
+
const compressedViewEntries = bufferViews
|
|
1727
|
+
.map((view, index) => ({ view, index, meshopt: resolveMeshoptCompressionExtension(view) }))
|
|
1728
|
+
.filter((entry) => entry.meshopt !== undefined);
|
|
1729
|
+
if (compressedViewEntries.length === 0) {
|
|
1730
|
+
return { json, buffers };
|
|
1731
|
+
}
|
|
1732
|
+
if (!usesGLTFMeshoptExtension(json)) {
|
|
1733
|
+
throw new Error("glTF uses meshopt-compressed bufferViews but meshopt extension is not declared");
|
|
1734
|
+
}
|
|
1735
|
+
if (!meshoptDecoder) {
|
|
1736
|
+
throw new Error("glTF meshopt compression requires a meshoptDecoder");
|
|
1737
|
+
}
|
|
1738
|
+
const preparedBuffers = [...buffers];
|
|
1739
|
+
const preparedBufferViews = [...bufferViews];
|
|
1740
|
+
for (const { index, meshopt } of compressedViewEntries) {
|
|
1741
|
+
const { name, extension } = meshopt;
|
|
1742
|
+
validateMeshoptDescriptor(json, buffers, index, name, extension);
|
|
1743
|
+
const sourceBuffer = buffers[extension.buffer];
|
|
1744
|
+
const byteOffset = extension.byteOffset ?? 0;
|
|
1745
|
+
const source = new Uint8Array(sourceBuffer, byteOffset, extension.byteLength);
|
|
1746
|
+
const descriptor = {
|
|
1747
|
+
bufferViewIndex: index,
|
|
1748
|
+
byteStride: extension.byteStride,
|
|
1749
|
+
count: extension.count,
|
|
1750
|
+
mode: extension.mode,
|
|
1751
|
+
filter: extension.filter ?? "NONE"
|
|
1752
|
+
};
|
|
1753
|
+
const decoded = toOwnedArrayBuffer(await meshoptDecoder(source, descriptor));
|
|
1754
|
+
const expectedByteLength = extension.count * extension.byteStride;
|
|
1755
|
+
if (decoded.byteLength !== expectedByteLength) {
|
|
1756
|
+
throw new Error(`glTF bufferView ${index} EXT_meshopt_compression decoded ${decoded.byteLength} bytes but expected ${expectedByteLength}`);
|
|
1757
|
+
}
|
|
1758
|
+
const decodedBufferIndex = preparedBuffers.length;
|
|
1759
|
+
preparedBuffers.push(decoded);
|
|
1760
|
+
preparedBufferViews[index] = {
|
|
1761
|
+
buffer: decodedBufferIndex,
|
|
1762
|
+
byteOffset: 0,
|
|
1763
|
+
byteLength: decoded.byteLength,
|
|
1764
|
+
...(extension.mode === "ATTRIBUTES" ? { byteStride: extension.byteStride } : {})
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
return {
|
|
1768
|
+
json: { ...json, bufferViews: preparedBufferViews },
|
|
1769
|
+
buffers: preparedBuffers
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
function resolveMeshoptCompressionExtension(view) {
|
|
1773
|
+
for (const name of GLTF_MESHOPT_EXTENSION_NAMES) {
|
|
1774
|
+
const extension = view.extensions?.[name];
|
|
1775
|
+
if (extension)
|
|
1776
|
+
return { name, extension };
|
|
1777
|
+
}
|
|
1778
|
+
return undefined;
|
|
1779
|
+
}
|
|
1780
|
+
function validateMeshoptDescriptor(json, buffers, bufferViewIndex, extensionName, extension) {
|
|
1781
|
+
if (!Number.isInteger(extension.buffer) || extension.buffer < 0 || !json.buffers?.[extension.buffer] || !buffers[extension.buffer]) {
|
|
1782
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} buffer must reference an existing buffer`);
|
|
1783
|
+
}
|
|
1784
|
+
if (extension.byteOffset !== undefined && (!Number.isInteger(extension.byteOffset) || extension.byteOffset < 0)) {
|
|
1785
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} byteOffset must be a non-negative integer`);
|
|
1786
|
+
}
|
|
1787
|
+
if (!Number.isInteger(extension.byteLength) || extension.byteLength < 0) {
|
|
1788
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} byteLength must be a non-negative integer`);
|
|
1789
|
+
}
|
|
1790
|
+
if (!Number.isInteger(extension.byteStride) || extension.byteStride <= 0) {
|
|
1791
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} byteStride must be a positive integer`);
|
|
1792
|
+
}
|
|
1793
|
+
if (!Number.isInteger(extension.count) || extension.count < 0) {
|
|
1794
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} count must be a non-negative integer`);
|
|
1795
|
+
}
|
|
1796
|
+
if (extension.mode !== "ATTRIBUTES" && extension.mode !== "TRIANGLES" && extension.mode !== "INDICES") {
|
|
1797
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} mode ${String(extension.mode)} is unsupported`);
|
|
1798
|
+
}
|
|
1799
|
+
if (extension.filter !== undefined &&
|
|
1800
|
+
extension.filter !== "NONE" &&
|
|
1801
|
+
extension.filter !== "OCTAHEDRAL" &&
|
|
1802
|
+
extension.filter !== "QUATERNION" &&
|
|
1803
|
+
extension.filter !== "EXPONENTIAL" &&
|
|
1804
|
+
extension.filter !== "COLOR") {
|
|
1805
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} filter ${String(extension.filter)} is unsupported`);
|
|
1806
|
+
}
|
|
1807
|
+
const sourceBuffer = buffers[extension.buffer];
|
|
1808
|
+
const sourceStart = extension.byteOffset ?? 0;
|
|
1809
|
+
const sourceEnd = sourceStart + extension.byteLength;
|
|
1810
|
+
if (sourceEnd > sourceBuffer.byteLength) {
|
|
1811
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} source range exceeds buffer ${extension.buffer}`);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function toOwnedArrayBuffer(value) {
|
|
1815
|
+
if (value instanceof ArrayBuffer) {
|
|
1816
|
+
return value.slice(0);
|
|
1817
|
+
}
|
|
1818
|
+
const copy = new Uint8Array(value.byteLength);
|
|
1819
|
+
copy.set(value);
|
|
1820
|
+
return copy.buffer;
|
|
1821
|
+
}
|
|
1822
|
+
async function readResponseBytes(response, url, phase, request) {
|
|
1823
|
+
const totalBytes = parseContentLength(response.headers?.get("content-length"));
|
|
1824
|
+
const reader = response.body?.getReader();
|
|
1825
|
+
if (!reader) {
|
|
1826
|
+
const buffer = await response.arrayBuffer();
|
|
1827
|
+
request.onProgress?.({ url, phase: "complete", loadedBytes: buffer.byteLength, ...(totalBytes === undefined ? {} : { totalBytes }) });
|
|
1828
|
+
return buffer;
|
|
1829
|
+
}
|
|
1830
|
+
const chunks = [];
|
|
1831
|
+
let loadedBytes = 0;
|
|
1832
|
+
while (true) {
|
|
1833
|
+
if (request.signal?.aborted) {
|
|
1834
|
+
throw request.signal.reason instanceof Error ? request.signal.reason : new Error(`glTF request aborted for ${url}`);
|
|
1835
|
+
}
|
|
1836
|
+
const { done, value } = await reader.read();
|
|
1837
|
+
if (done)
|
|
1838
|
+
break;
|
|
1839
|
+
chunks.push(value);
|
|
1840
|
+
loadedBytes += value.byteLength;
|
|
1841
|
+
request.onProgress?.({ url, phase, loadedBytes, ...(totalBytes === undefined ? {} : { totalBytes }) });
|
|
1842
|
+
}
|
|
1843
|
+
const buffer = new Uint8Array(loadedBytes);
|
|
1844
|
+
let offset = 0;
|
|
1845
|
+
for (const chunk of chunks) {
|
|
1846
|
+
buffer.set(chunk, offset);
|
|
1847
|
+
offset += chunk.byteLength;
|
|
1848
|
+
}
|
|
1849
|
+
request.onProgress?.({ url, phase: "complete", loadedBytes, ...(totalBytes === undefined ? {} : { totalBytes }) });
|
|
1850
|
+
return buffer.buffer;
|
|
1851
|
+
}
|
|
1852
|
+
function parseContentLength(value) {
|
|
1853
|
+
if (!value)
|
|
1854
|
+
return undefined;
|
|
1855
|
+
const parsed = Number(value);
|
|
1856
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
1857
|
+
}
|
|
1858
|
+
function decodeDataUri(uri) {
|
|
1859
|
+
const [header = "", payload = ""] = uri.split(",", 2);
|
|
1860
|
+
if (header.endsWith(";base64")) {
|
|
1861
|
+
let binary;
|
|
1862
|
+
try {
|
|
1863
|
+
binary = atob(payload);
|
|
1864
|
+
}
|
|
1865
|
+
catch (error) {
|
|
1866
|
+
throw new Error(`Invalid base64 data URI payload: ${error instanceof Error ? error.message : String(error)}`);
|
|
1867
|
+
}
|
|
1868
|
+
const bytes = new Uint8Array(binary.length);
|
|
1869
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
1870
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1871
|
+
}
|
|
1872
|
+
return bytes.buffer;
|
|
1873
|
+
}
|
|
1874
|
+
return new TextEncoder().encode(decodeURIComponent(payload)).buffer;
|
|
1875
|
+
}
|
|
1876
|
+
const GLB_MAGIC = 0x46546c67;
|
|
1877
|
+
const GLB_VERSION = 2;
|
|
1878
|
+
const GLB_CHUNK_JSON = 0x4e4f534a;
|
|
1879
|
+
const GLB_CHUNK_BIN = 0x004e4942;
|
|
1880
|
+
function parseGLB(data, url) {
|
|
1881
|
+
const view = new DataView(data);
|
|
1882
|
+
if (data.byteLength < 20) {
|
|
1883
|
+
throw new Error("GLB file is too small");
|
|
1884
|
+
}
|
|
1885
|
+
const magic = view.getUint32(0, true);
|
|
1886
|
+
const version = view.getUint32(4, true);
|
|
1887
|
+
const declaredLength = view.getUint32(8, true);
|
|
1888
|
+
if (magic !== GLB_MAGIC) {
|
|
1889
|
+
throw new Error("Invalid GLB magic");
|
|
1890
|
+
}
|
|
1891
|
+
if (version !== GLB_VERSION) {
|
|
1892
|
+
throw new Error(`Unsupported GLB version: ${version}`);
|
|
1893
|
+
}
|
|
1894
|
+
if (declaredLength !== data.byteLength) {
|
|
1895
|
+
throw new Error(`GLB length mismatch: header ${declaredLength}, actual ${data.byteLength}`);
|
|
1896
|
+
}
|
|
1897
|
+
let offset = 12;
|
|
1898
|
+
let json;
|
|
1899
|
+
let binaryChunk;
|
|
1900
|
+
while (offset < data.byteLength) {
|
|
1901
|
+
if (offset + 8 > data.byteLength) {
|
|
1902
|
+
throw new Error("Malformed GLB chunk header");
|
|
1903
|
+
}
|
|
1904
|
+
const chunkLength = view.getUint32(offset, true);
|
|
1905
|
+
const chunkType = view.getUint32(offset + 4, true);
|
|
1906
|
+
const chunkStart = offset + 8;
|
|
1907
|
+
const chunkEnd = chunkStart + chunkLength;
|
|
1908
|
+
if (chunkEnd > data.byteLength) {
|
|
1909
|
+
throw new Error("Malformed GLB chunk length");
|
|
1910
|
+
}
|
|
1911
|
+
const chunk = data.slice(chunkStart, chunkEnd);
|
|
1912
|
+
if (chunkType === GLB_CHUNK_JSON) {
|
|
1913
|
+
const text = new TextDecoder().decode(chunk).replace(/[\u0000\s]+$/u, "");
|
|
1914
|
+
json = JSON.parse(text);
|
|
1915
|
+
}
|
|
1916
|
+
else if (chunkType === GLB_CHUNK_BIN) {
|
|
1917
|
+
binaryChunk = chunk;
|
|
1918
|
+
}
|
|
1919
|
+
offset = chunkEnd;
|
|
1920
|
+
}
|
|
1921
|
+
if (!json) {
|
|
1922
|
+
throw new Error("GLB is missing a JSON chunk");
|
|
1923
|
+
}
|
|
1924
|
+
return { json, binaryChunk, url, byteLength: data.byteLength };
|
|
1925
|
+
}
|
|
1926
|
+
function readAccessor(json, buffers, accessorIndex, accessorCache) {
|
|
1927
|
+
if (accessorCache)
|
|
1928
|
+
accessorCache.profile.accessorReadCount += 1;
|
|
1929
|
+
const cached = accessorCache?.rowsByAccessor.get(accessorIndex);
|
|
1930
|
+
if (cached && accessorCache) {
|
|
1931
|
+
accessorCache.profile.accessorCacheHitCount++;
|
|
1932
|
+
return cached;
|
|
1933
|
+
}
|
|
1934
|
+
const startedAt = nowMs();
|
|
1935
|
+
const accessor = json.accessors?.[accessorIndex];
|
|
1936
|
+
if (!accessor) {
|
|
1937
|
+
throw new Error(`Missing glTF accessor ${accessorIndex}`);
|
|
1938
|
+
}
|
|
1939
|
+
validateAccessorShape(accessorIndex, accessor);
|
|
1940
|
+
const componentCount = componentCountForType(accessor.type);
|
|
1941
|
+
const componentBytes = componentByteSize(accessor.componentType);
|
|
1942
|
+
const rowByteLength = componentCount * componentBytes;
|
|
1943
|
+
validateAccessorDescriptor(accessorIndex, accessor, rowByteLength);
|
|
1944
|
+
const output = Array.from({ length: accessor.count }, () => new Array(componentCount).fill(0));
|
|
1945
|
+
if (accessor.bufferView !== undefined) {
|
|
1946
|
+
const { view, buffer } = getBufferView(json, buffers, accessor.bufferView);
|
|
1947
|
+
const dataView = dataViewForBuffer(buffer, accessorCache);
|
|
1948
|
+
const stride = view.byteStride ?? rowByteLength;
|
|
1949
|
+
if (stride < rowByteLength) {
|
|
1950
|
+
throw new Error(`glTF accessor ${accessorIndex} byteStride ${stride} is smaller than element size ${rowByteLength}`);
|
|
1951
|
+
}
|
|
1952
|
+
const offset = (view.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
1953
|
+
validateAccessorBounds(accessorIndex, buffer, view, offset, accessor.count, stride, rowByteLength);
|
|
1954
|
+
for (let item = 0; item < accessor.count; item += 1) {
|
|
1955
|
+
const baseOffset = offset + item * stride;
|
|
1956
|
+
for (let component = 0; component < componentCount; component += 1) {
|
|
1957
|
+
output[item][component] = readComponent(dataView, baseOffset + component * componentBytes, accessor.componentType, accessor.normalized ?? false);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (accessor.sparse) {
|
|
1962
|
+
applySparseAccessor(json, buffers, accessorIndex, accessor, output, componentCount, componentBytes, accessorCache);
|
|
1963
|
+
}
|
|
1964
|
+
if (accessorCache) {
|
|
1965
|
+
recordAccessorRead(accessorCache, accessorIndex, accessor, componentCount, output.length, nowMs() - startedAt);
|
|
1966
|
+
accessorCache.rowsByAccessor.set(accessorIndex, output);
|
|
1967
|
+
}
|
|
1968
|
+
return output;
|
|
1969
|
+
}
|
|
1970
|
+
function dataViewForBuffer(buffer, accessorCache) {
|
|
1971
|
+
if (!accessorCache)
|
|
1972
|
+
return new DataView(buffer);
|
|
1973
|
+
const existing = accessorCache.dataViews.get(buffer);
|
|
1974
|
+
if (existing)
|
|
1975
|
+
return existing;
|
|
1976
|
+
const view = new DataView(buffer);
|
|
1977
|
+
accessorCache.dataViews.set(buffer, view);
|
|
1978
|
+
accessorCache.profile.dataViewCacheEntries = accessorCache.dataViews.size;
|
|
1979
|
+
return view;
|
|
1980
|
+
}
|
|
1981
|
+
function recordAccessorRead(accessorCache, accessorIndex, accessor, componentCount, rowCount, elapsedMs) {
|
|
1982
|
+
const profile = accessorCache.profile;
|
|
1983
|
+
const byteLength = rowCount * componentCount * componentByteSize(accessor.componentType);
|
|
1984
|
+
profile.uniqueAccessorReadCount++;
|
|
1985
|
+
profile.accessorRowsRead += rowCount;
|
|
1986
|
+
profile.accessorValuesRead += rowCount * componentCount;
|
|
1987
|
+
profile.accessorReadMs += elapsedMs;
|
|
1988
|
+
profile.largestAccessors.push({
|
|
1989
|
+
accessorIndex,
|
|
1990
|
+
count: accessor.count,
|
|
1991
|
+
type: accessor.type,
|
|
1992
|
+
componentType: accessor.componentType,
|
|
1993
|
+
componentCount,
|
|
1994
|
+
byteLength,
|
|
1995
|
+
...(accessor.bufferView !== undefined ? { bufferView: accessor.bufferView } : {})
|
|
1996
|
+
});
|
|
1997
|
+
profile.largestAccessors.sort((left, right) => right.byteLength - left.byteLength);
|
|
1998
|
+
if (profile.largestAccessors.length > 8) {
|
|
1999
|
+
profile.largestAccessors.length = 8;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
function validateAccessorShape(accessorIndex, accessor) {
|
|
2003
|
+
if (!isAccessorType(accessor.type)) {
|
|
2004
|
+
throw new Error(`glTF accessor ${accessorIndex} type ${String(accessor.type)} is unsupported`);
|
|
2005
|
+
}
|
|
2006
|
+
if (!isAccessorComponentType(accessor.componentType)) {
|
|
2007
|
+
throw new Error(`glTF accessor ${accessorIndex} componentType ${String(accessor.componentType)} is unsupported`);
|
|
2008
|
+
}
|
|
2009
|
+
if (accessor.bufferView !== undefined && (!Number.isInteger(accessor.bufferView) || accessor.bufferView < 0)) {
|
|
2010
|
+
throw new Error(`glTF accessor ${accessorIndex} bufferView must be a non-negative integer`);
|
|
2011
|
+
}
|
|
2012
|
+
if (accessor.sparse) {
|
|
2013
|
+
validateSparseAccessorShape(accessorIndex, accessor.sparse);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
function validateAccessorDescriptor(accessorIndex, accessor, rowByteLength) {
|
|
2017
|
+
if (!Number.isInteger(accessor.count) || accessor.count < 0) {
|
|
2018
|
+
throw new Error(`glTF accessor ${accessorIndex} count must be a non-negative integer`);
|
|
2019
|
+
}
|
|
2020
|
+
if (accessor.byteOffset !== undefined && (!Number.isInteger(accessor.byteOffset) || accessor.byteOffset < 0)) {
|
|
2021
|
+
throw new Error(`glTF accessor ${accessorIndex} byteOffset must be a non-negative integer`);
|
|
2022
|
+
}
|
|
2023
|
+
if (accessor.sparse) {
|
|
2024
|
+
if (!Number.isInteger(accessor.sparse.count) || accessor.sparse.count < 0) {
|
|
2025
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse count must be a non-negative integer`);
|
|
2026
|
+
}
|
|
2027
|
+
if (accessor.sparse.count > accessor.count) {
|
|
2028
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse count exceeds accessor count`);
|
|
2029
|
+
}
|
|
2030
|
+
if (accessor.sparse.indices.byteOffset !== undefined && (!Number.isInteger(accessor.sparse.indices.byteOffset) || accessor.sparse.indices.byteOffset < 0)) {
|
|
2031
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse indices byteOffset must be a non-negative integer`);
|
|
2032
|
+
}
|
|
2033
|
+
if (accessor.sparse.values.byteOffset !== undefined && (!Number.isInteger(accessor.sparse.values.byteOffset) || accessor.sparse.values.byteOffset < 0)) {
|
|
2034
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse values byteOffset must be a non-negative integer`);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (rowByteLength <= 0) {
|
|
2038
|
+
throw new Error(`glTF accessor ${accessorIndex} has invalid element size`);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
function validateSparseAccessorShape(accessorIndex, sparse) {
|
|
2042
|
+
const record = sparse;
|
|
2043
|
+
if (!isObjectRecord(record.indices)) {
|
|
2044
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse indices must be an object`);
|
|
2045
|
+
}
|
|
2046
|
+
if (!isObjectRecord(record.values)) {
|
|
2047
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse values must be an object`);
|
|
2048
|
+
}
|
|
2049
|
+
if (!isSparseIndexComponentType(sparse.indices.componentType)) {
|
|
2050
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse indices componentType ${String(sparse.indices.componentType)} is unsupported`);
|
|
2051
|
+
}
|
|
2052
|
+
if (!Number.isInteger(sparse.indices.bufferView) || sparse.indices.bufferView < 0) {
|
|
2053
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse indices bufferView must be a non-negative integer`);
|
|
2054
|
+
}
|
|
2055
|
+
if (!Number.isInteger(sparse.values.bufferView) || sparse.values.bufferView < 0) {
|
|
2056
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse values bufferView must be a non-negative integer`);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
function isObjectRecord(value) {
|
|
2060
|
+
return typeof value === "object" && value !== null;
|
|
2061
|
+
}
|
|
2062
|
+
function applySparseAccessor(json, buffers, accessorIndex, accessor, output, componentCount, componentBytes, accessorCache) {
|
|
2063
|
+
const sparse = accessor.sparse;
|
|
2064
|
+
if (!sparse || sparse.count === 0) {
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
const { view: indexView, buffer: indexBuffer } = getBufferView(json, buffers, sparse.indices.bufferView);
|
|
2068
|
+
const indexDataView = dataViewForBuffer(indexBuffer, accessorCache);
|
|
2069
|
+
const indexComponentBytes = componentByteSize(sparse.indices.componentType);
|
|
2070
|
+
const indexStart = (indexView.byteOffset ?? 0) + (sparse.indices.byteOffset ?? 0);
|
|
2071
|
+
validateRawRange(`glTF accessor ${accessorIndex} sparse indices`, indexBuffer, indexView, indexStart, sparse.count * indexComponentBytes);
|
|
2072
|
+
const { view: valueView, buffer: valueBuffer } = getBufferView(json, buffers, sparse.values.bufferView);
|
|
2073
|
+
const valueDataView = dataViewForBuffer(valueBuffer, accessorCache);
|
|
2074
|
+
const valueStart = (valueView.byteOffset ?? 0) + (sparse.values.byteOffset ?? 0);
|
|
2075
|
+
const rowByteLength = componentCount * componentBytes;
|
|
2076
|
+
validateRawRange(`glTF accessor ${accessorIndex} sparse values`, valueBuffer, valueView, valueStart, sparse.count * rowByteLength);
|
|
2077
|
+
for (let item = 0; item < sparse.count; item += 1) {
|
|
2078
|
+
const targetIndex = readComponent(indexDataView, indexStart + item * indexComponentBytes, sparse.indices.componentType, false);
|
|
2079
|
+
if (!Number.isInteger(targetIndex) || targetIndex < 0 || targetIndex >= accessor.count) {
|
|
2080
|
+
throw new Error(`glTF accessor ${accessorIndex} sparse index ${targetIndex} is out of range`);
|
|
2081
|
+
}
|
|
2082
|
+
const valueOffset = valueStart + item * rowByteLength;
|
|
2083
|
+
for (let component = 0; component < componentCount; component += 1) {
|
|
2084
|
+
output[targetIndex][component] = readComponent(valueDataView, valueOffset + component * componentBytes, accessor.componentType, accessor.normalized ?? false);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
function getBufferView(json, buffers, bufferViewIndex) {
|
|
2089
|
+
if (!Number.isInteger(bufferViewIndex) || bufferViewIndex < 0) {
|
|
2090
|
+
throw new Error(`glTF bufferView ${String(bufferViewIndex)} must be a non-negative integer`);
|
|
2091
|
+
}
|
|
2092
|
+
const view = json.bufferViews?.[bufferViewIndex];
|
|
2093
|
+
if (!view) {
|
|
2094
|
+
throw new Error(`Missing glTF bufferView ${bufferViewIndex}`);
|
|
2095
|
+
}
|
|
2096
|
+
validateBufferViewDescriptor(bufferViewIndex, view);
|
|
2097
|
+
const buffer = buffers[view.buffer];
|
|
2098
|
+
if (!buffer) {
|
|
2099
|
+
throw new Error(`Missing glTF buffer ${view.buffer}`);
|
|
2100
|
+
}
|
|
2101
|
+
const start = view.byteOffset ?? 0;
|
|
2102
|
+
const end = start + view.byteLength;
|
|
2103
|
+
if (end > buffer.byteLength) {
|
|
2104
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} exceeds buffer ${view.buffer}`);
|
|
2105
|
+
}
|
|
2106
|
+
return { view, buffer };
|
|
2107
|
+
}
|
|
2108
|
+
function validateBufferViewDescriptor(bufferViewIndex, view) {
|
|
2109
|
+
if (!Number.isInteger(view.buffer) || view.buffer < 0) {
|
|
2110
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} buffer must be a non-negative integer`);
|
|
2111
|
+
}
|
|
2112
|
+
if (view.byteOffset !== undefined && (!Number.isInteger(view.byteOffset) || view.byteOffset < 0)) {
|
|
2113
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} byteOffset must be a non-negative integer`);
|
|
2114
|
+
}
|
|
2115
|
+
if (!Number.isInteger(view.byteLength) || view.byteLength < 0) {
|
|
2116
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} byteLength must be a non-negative integer`);
|
|
2117
|
+
}
|
|
2118
|
+
if (view.byteStride !== undefined &&
|
|
2119
|
+
(!Number.isInteger(view.byteStride) || view.byteStride < 4 || view.byteStride > 252 || view.byteStride % 4 !== 0)) {
|
|
2120
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} byteStride must be a 4-byte aligned integer between 4 and 252`);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
function validateAccessorBounds(accessorIndex, buffer, view, start, count, stride, rowByteLength) {
|
|
2124
|
+
const byteLength = count === 0 ? 0 : (count - 1) * stride + rowByteLength;
|
|
2125
|
+
validateRawRange(`glTF accessor ${accessorIndex}`, buffer, view, start, byteLength);
|
|
2126
|
+
}
|
|
2127
|
+
function validateRawRange(label, buffer, view, start, byteLength) {
|
|
2128
|
+
if (!Number.isInteger(start) || start < 0 || !Number.isInteger(byteLength) || byteLength < 0) {
|
|
2129
|
+
throw new Error(`${label} byte range must be non-negative integers`);
|
|
2130
|
+
}
|
|
2131
|
+
const viewStart = view.byteOffset ?? 0;
|
|
2132
|
+
const viewEnd = viewStart + view.byteLength;
|
|
2133
|
+
const end = start + byteLength;
|
|
2134
|
+
if (end > viewEnd || end > buffer.byteLength) {
|
|
2135
|
+
throw new Error(`${label} exceeds its glTF bufferView`);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
function readOptionalAccessor(json, buffers, accessorIndex, accessorCache) {
|
|
2139
|
+
return accessorIndex === undefined ? [] : readAccessor(json, buffers, accessorIndex, accessorCache);
|
|
2140
|
+
}
|
|
2141
|
+
function readBufferViewBytes(json, buffers, bufferViewIndex) {
|
|
2142
|
+
const { view, buffer } = getBufferView(json, buffers, bufferViewIndex);
|
|
2143
|
+
const start = view.byteOffset ?? 0;
|
|
2144
|
+
const end = start + view.byteLength;
|
|
2145
|
+
return buffer.slice(start, end);
|
|
2146
|
+
}
|
|
2147
|
+
function isAccessorType(type) {
|
|
2148
|
+
return type === "SCALAR" || type === "VEC2" || type === "VEC3" || type === "VEC4" || type === "MAT4";
|
|
2149
|
+
}
|
|
2150
|
+
function isAccessorComponentType(type) {
|
|
2151
|
+
return type === 5120 || type === 5121 || type === 5122 || type === 5123 || type === 5125 || type === 5126;
|
|
2152
|
+
}
|
|
2153
|
+
function isSparseIndexComponentType(type) {
|
|
2154
|
+
return type === 5121 || type === 5123 || type === 5125;
|
|
2155
|
+
}
|
|
2156
|
+
function componentCountForType(type) {
|
|
2157
|
+
return type === "SCALAR" ? 1 : type === "VEC2" ? 2 : type === "VEC3" ? 3 : type === "VEC4" ? 4 : 16;
|
|
2158
|
+
}
|
|
2159
|
+
function componentByteSize(type) {
|
|
2160
|
+
if (type === 5120 || type === 5121)
|
|
2161
|
+
return 1;
|
|
2162
|
+
if (type === 5122 || type === 5123)
|
|
2163
|
+
return 2;
|
|
2164
|
+
return 4;
|
|
2165
|
+
}
|
|
2166
|
+
function readComponent(view, byteOffset, type, normalized) {
|
|
2167
|
+
if (type === 5120) {
|
|
2168
|
+
const value = view.getInt8(byteOffset);
|
|
2169
|
+
return normalized ? Math.max(value / 127, -1) : value;
|
|
2170
|
+
}
|
|
2171
|
+
if (type === 5121) {
|
|
2172
|
+
const value = view.getUint8(byteOffset);
|
|
2173
|
+
return normalized ? value / 255 : value;
|
|
2174
|
+
}
|
|
2175
|
+
if (type === 5122) {
|
|
2176
|
+
const value = view.getInt16(byteOffset, true);
|
|
2177
|
+
return normalized ? Math.max(value / 32767, -1) : value;
|
|
2178
|
+
}
|
|
2179
|
+
if (type === 5126)
|
|
2180
|
+
return view.getFloat32(byteOffset, true);
|
|
2181
|
+
if (type === 5125)
|
|
2182
|
+
return view.getUint32(byteOffset, true);
|
|
2183
|
+
const value = view.getUint16(byteOffset, true);
|
|
2184
|
+
return normalized ? value / 65535 : value;
|
|
2185
|
+
}
|
|
2186
|
+
function resolvePrimitiveMaterialIndex(primitive, materials, meshIndex, primitiveIndex) {
|
|
2187
|
+
if (primitive.material === undefined) {
|
|
2188
|
+
return undefined;
|
|
2189
|
+
}
|
|
2190
|
+
if (!Number.isInteger(primitive.material) || primitive.material < 0) {
|
|
2191
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} material must be a non-negative integer`);
|
|
2192
|
+
}
|
|
2193
|
+
if (!materials[primitive.material]) {
|
|
2194
|
+
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} references missing material ${primitive.material}`);
|
|
2195
|
+
}
|
|
2196
|
+
return primitive.material;
|
|
2197
|
+
}
|
|
2198
|
+
function materialName(material, index) {
|
|
2199
|
+
return index === undefined ? DEFAULT_GLTF_MATERIAL_NAME : material?.name ?? `material-${index}`;
|
|
2200
|
+
}
|
|
2201
|
+
function meshPrimitiveName(mesh, meshIndex, primitiveIndex) {
|
|
2202
|
+
const base = mesh.name ?? `mesh-${meshIndex}`;
|
|
2203
|
+
return mesh.primitives.length === 1 ? base : `${base}-primitive-${primitiveIndex}`;
|
|
2204
|
+
}
|
|
2205
|
+
function resolveNodeTransform(node, nodeIndex) {
|
|
2206
|
+
if (node.matrix !== undefined) {
|
|
2207
|
+
if (node.translation !== undefined || node.rotation !== undefined || node.scale !== undefined) {
|
|
2208
|
+
throw new Error(`glTF node ${nodeIndex} cannot combine matrix with translation, rotation, or scale`);
|
|
2209
|
+
}
|
|
2210
|
+
const decomposed = decomposeMat4(resolveNodeMatrix(node.matrix, nodeIndex));
|
|
2211
|
+
return {
|
|
2212
|
+
translation: decomposed.position,
|
|
2213
|
+
rotation: decomposed.rotation,
|
|
2214
|
+
scale: decomposed.scale
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
return {
|
|
2218
|
+
translation: resolveVec3(node.translation, [0, 0, 0], `node ${nodeIndex} translation`),
|
|
2219
|
+
rotation: resolveQuat(node.rotation, [0, 0, 0, 1], `node ${nodeIndex} rotation`),
|
|
2220
|
+
scale: resolveVec3(node.scale, [1, 1, 1], `node ${nodeIndex} scale`)
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
function resolveNodeMatrix(matrix, nodeIndex) {
|
|
2224
|
+
if (matrix.length !== 16 || matrix.some((value) => !Number.isFinite(value))) {
|
|
2225
|
+
throw new RangeError(`glTF node ${nodeIndex} matrix must contain 16 finite values`);
|
|
2226
|
+
}
|
|
2227
|
+
return [
|
|
2228
|
+
matrix[0], matrix[1], matrix[2], matrix[3],
|
|
2229
|
+
matrix[4], matrix[5], matrix[6], matrix[7],
|
|
2230
|
+
matrix[8], matrix[9], matrix[10], matrix[11],
|
|
2231
|
+
matrix[12], matrix[13], matrix[14], matrix[15]
|
|
2232
|
+
];
|
|
2233
|
+
}
|
|
2234
|
+
function resolveVec3(value, fallback, label) {
|
|
2235
|
+
const resolved = value ?? fallback;
|
|
2236
|
+
if (resolved.length !== 3 || resolved.some((component) => !Number.isFinite(component))) {
|
|
2237
|
+
throw new RangeError(`glTF ${label} must contain three finite values`);
|
|
2238
|
+
}
|
|
2239
|
+
return [resolved[0], resolved[1], resolved[2]];
|
|
2240
|
+
}
|
|
2241
|
+
function resolveQuat(value, fallback, label) {
|
|
2242
|
+
const resolved = value ?? fallback;
|
|
2243
|
+
if (resolved.length !== 4 || resolved.some((component) => !Number.isFinite(component))) {
|
|
2244
|
+
throw new RangeError(`glTF ${label} must contain four finite values`);
|
|
2245
|
+
}
|
|
2246
|
+
return [resolved[0], resolved[1], resolved[2], resolved[3]];
|
|
2247
|
+
}
|
|
2248
|
+
function createScene(json, meshes, cameras, lights, skins, nodeInstanceTransforms, materialVariants, options = {}) {
|
|
2249
|
+
const sceneIndex = resolveSelectedSceneIndex(json, options);
|
|
2250
|
+
validateSceneGraph(json, sceneIndex);
|
|
2251
|
+
validateSceneMaterialVariant(materialVariants, options.materialVariant);
|
|
2252
|
+
const materialKeyForRenderable = createRuntimeMaterialKeyResolver(json, sceneIndex, meshes, skins, nodeInstanceTransforms, options.materialVariant);
|
|
2253
|
+
const nodeWorldMatrices = computeGLTFNodeWorldMatrices(json);
|
|
2254
|
+
const nodeNameForIndex = createGLTFNodeNameResolver(json);
|
|
2255
|
+
const scene = new Scene();
|
|
2256
|
+
const nodes = new Map();
|
|
2257
|
+
const createNode = (index) => {
|
|
2258
|
+
const source = json.nodes?.[index];
|
|
2259
|
+
if (!source) {
|
|
2260
|
+
throw new Error(`Missing glTF node ${index}`);
|
|
2261
|
+
}
|
|
2262
|
+
const existing = nodes.get(index);
|
|
2263
|
+
if (existing) {
|
|
2264
|
+
return existing;
|
|
2265
|
+
}
|
|
2266
|
+
const node = createSceneNodeForGLTFNode(scene, source, index, nodeNameForIndex(index), cameras, lights);
|
|
2267
|
+
const transform = resolveNodeTransform(source, index);
|
|
2268
|
+
node.transform.setPosition(transform.translation[0], transform.translation[1], transform.translation[2]);
|
|
2269
|
+
node.transform.setRotation(transform.rotation[0], transform.rotation[1], transform.rotation[2], transform.rotation[3]);
|
|
2270
|
+
node.transform.setScale(transform.scale[0], transform.scale[1], transform.scale[2]);
|
|
2271
|
+
if (source.mesh !== undefined) {
|
|
2272
|
+
attachMeshPrimitives(scene, node, source.mesh, index, meshes, skins, source.skin, nodeWorldMatrices.get(index), nodeInstanceTransforms.get(index), materialKeyForRenderable);
|
|
2273
|
+
}
|
|
2274
|
+
nodes.set(index, node);
|
|
2275
|
+
for (const childIndex of source.children ?? []) {
|
|
2276
|
+
node.addChild(createNode(childIndex));
|
|
2277
|
+
}
|
|
2278
|
+
return node;
|
|
2279
|
+
};
|
|
2280
|
+
const sceneDefinition = json.scenes?.[sceneIndex];
|
|
2281
|
+
for (const nodeIndex of sceneDefinition?.nodes ?? []) {
|
|
2282
|
+
scene.root.addChild(createNode(nodeIndex));
|
|
2283
|
+
}
|
|
2284
|
+
return scene;
|
|
2285
|
+
}
|
|
2286
|
+
function resolveSelectedSceneIndex(json, options) {
|
|
2287
|
+
if (options.sceneIndex !== undefined && options.sceneName !== undefined) {
|
|
2288
|
+
throw new Error("glTF scene selection cannot specify both sceneIndex and sceneName");
|
|
2289
|
+
}
|
|
2290
|
+
if (options.sceneName !== undefined) {
|
|
2291
|
+
const sceneIndex = (json.scenes ?? []).findIndex((scene, index) => (scene.name ?? `scene-${index}`) === options.sceneName);
|
|
2292
|
+
if (sceneIndex < 0) {
|
|
2293
|
+
throw new Error(`glTF scene named ${options.sceneName} is not defined`);
|
|
2294
|
+
}
|
|
2295
|
+
return sceneIndex;
|
|
2296
|
+
}
|
|
2297
|
+
if (options.sceneIndex !== undefined) {
|
|
2298
|
+
if (!Number.isInteger(options.sceneIndex) || options.sceneIndex < 0 || !json.scenes?.[options.sceneIndex]) {
|
|
2299
|
+
throw new Error(`glTF scene ${options.sceneIndex} is missing`);
|
|
2300
|
+
}
|
|
2301
|
+
return options.sceneIndex;
|
|
2302
|
+
}
|
|
2303
|
+
return resolveDefaultSceneIndex(json);
|
|
2304
|
+
}
|
|
2305
|
+
function validateSceneMaterialVariant(materialVariants, selectedVariant) {
|
|
2306
|
+
if (selectedVariant === undefined)
|
|
2307
|
+
return;
|
|
2308
|
+
if (!materialVariants.some((variant) => variant.name === selectedVariant)) {
|
|
2309
|
+
throw new Error(`glTF material variant ${selectedVariant} is not defined`);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
function validateSceneGraph(json, sceneIndex = resolveDefaultSceneIndex(json)) {
|
|
2313
|
+
const nodeDefinitions = json.nodes ?? [];
|
|
2314
|
+
if (json.scenes && (!Number.isInteger(sceneIndex) || sceneIndex < 0 || !json.scenes[sceneIndex]))
|
|
2315
|
+
throw new Error(`glTF scene ${sceneIndex} is missing`);
|
|
2316
|
+
const parentByNode = new Map();
|
|
2317
|
+
for (const [nodeIndex, node] of nodeDefinitions.entries()) {
|
|
2318
|
+
for (const childIndex of node.children ?? []) {
|
|
2319
|
+
if (!Number.isInteger(childIndex) || childIndex < 0 || !nodeDefinitions[childIndex]) {
|
|
2320
|
+
throw new Error(`glTF node ${nodeIndex} references missing child node ${childIndex}`);
|
|
2321
|
+
}
|
|
2322
|
+
const existingParent = parentByNode.get(childIndex);
|
|
2323
|
+
if (existingParent !== undefined) {
|
|
2324
|
+
throw new Error(`glTF node ${childIndex} has multiple parents: ${existingParent} and ${nodeIndex}`);
|
|
2325
|
+
}
|
|
2326
|
+
parentByNode.set(childIndex, nodeIndex);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const visiting = new Set();
|
|
2330
|
+
const visited = new Set();
|
|
2331
|
+
const visit = (nodeIndex) => {
|
|
2332
|
+
if (visiting.has(nodeIndex)) {
|
|
2333
|
+
throw new Error(`glTF node graph contains a cycle at node ${nodeIndex}`);
|
|
2334
|
+
}
|
|
2335
|
+
if (visited.has(nodeIndex)) {
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
visiting.add(nodeIndex);
|
|
2339
|
+
for (const childIndex of nodeDefinitions[nodeIndex]?.children ?? []) {
|
|
2340
|
+
visit(childIndex);
|
|
2341
|
+
}
|
|
2342
|
+
visiting.delete(nodeIndex);
|
|
2343
|
+
visited.add(nodeIndex);
|
|
2344
|
+
};
|
|
2345
|
+
for (let nodeIndex = 0; nodeIndex < nodeDefinitions.length; nodeIndex += 1) {
|
|
2346
|
+
visit(nodeIndex);
|
|
2347
|
+
}
|
|
2348
|
+
for (const [rootListIndex, rootNodeIndex] of (json.scenes?.[sceneIndex]?.nodes ?? []).entries()) {
|
|
2349
|
+
if (!Number.isInteger(rootNodeIndex) || rootNodeIndex < 0 || !nodeDefinitions[rootNodeIndex]) {
|
|
2350
|
+
throw new Error(`glTF scene ${sceneIndex} root ${rootListIndex} references missing node ${rootNodeIndex}`);
|
|
2351
|
+
}
|
|
2352
|
+
if (parentByNode.has(rootNodeIndex)) {
|
|
2353
|
+
throw new Error(`glTF scene ${sceneIndex} root ${rootNodeIndex} is also a child node`);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
function attachMeshPrimitives(scene, node, meshIndex, nodeIndex, meshes, skins, nodeSkinIndex, nodeWorldMatrix, instanceTransforms, materialKeyForRenderable) {
|
|
2358
|
+
if (!Number.isInteger(meshIndex) || meshIndex < 0) {
|
|
2359
|
+
throw new RangeError(`glTF node ${nodeIndex} mesh must be a non-negative integer`);
|
|
2360
|
+
}
|
|
2361
|
+
const primitives = meshes.filter((mesh) => mesh.sourceMeshIndex === meshIndex);
|
|
2362
|
+
if (primitives.length === 0) {
|
|
2363
|
+
throw new Error(`glTF node ${nodeIndex} references missing mesh ${meshIndex}`);
|
|
2364
|
+
}
|
|
2365
|
+
if (primitives.length === 1) {
|
|
2366
|
+
const mesh = primitives[0];
|
|
2367
|
+
const skinning = skinningForMesh(mesh, skins, nodeSkinIndex, nodeWorldMatrix);
|
|
2368
|
+
scene.addRenderable(node, new Renderable({
|
|
2369
|
+
geometry: mesh.name,
|
|
2370
|
+
material: materialKeyForRenderable?.(mesh, runtimeMaterialContract(skinning, instanceTransforms)) ?? mesh.material,
|
|
2371
|
+
...(skinning ? { skinning } : {}),
|
|
2372
|
+
morphWeights: mesh.morphWeights,
|
|
2373
|
+
...(instanceTransforms ? { instanceTransforms } : {})
|
|
2374
|
+
}));
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
for (const mesh of primitives) {
|
|
2378
|
+
const skinning = skinningForMesh(mesh, skins, nodeSkinIndex, nodeWorldMatrix);
|
|
2379
|
+
const primitiveNode = scene.createNode(mesh.name);
|
|
2380
|
+
primitiveNode.transform.setPosition(0, 0, 0);
|
|
2381
|
+
primitiveNode.transform.setRotation(0, 0, 0, 1);
|
|
2382
|
+
primitiveNode.transform.setScale(1, 1, 1);
|
|
2383
|
+
scene.addRenderable(primitiveNode, new Renderable({
|
|
2384
|
+
geometry: mesh.name,
|
|
2385
|
+
material: materialKeyForRenderable?.(mesh, runtimeMaterialContract(skinning, instanceTransforms)) ?? mesh.material,
|
|
2386
|
+
...(skinning ? { skinning } : {}),
|
|
2387
|
+
morphWeights: mesh.morphWeights,
|
|
2388
|
+
...(instanceTransforms ? { instanceTransforms } : {})
|
|
2389
|
+
}));
|
|
2390
|
+
node.addChild(primitiveNode);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
function skinningForMesh(mesh, skins, nodeSkinIndex, nodeWorldMatrix) {
|
|
2394
|
+
const skinIndex = nodeSkinIndex;
|
|
2395
|
+
if (skinIndex === undefined)
|
|
2396
|
+
return undefined;
|
|
2397
|
+
if (mesh.joints.length === 0 || mesh.weights.length === 0) {
|
|
2398
|
+
throw new Error(`glTF mesh ${mesh.name} references skin ${skinIndex} but is missing JOINTS_0 or WEIGHTS_0 attributes`);
|
|
2399
|
+
}
|
|
2400
|
+
const skin = skins[skinIndex];
|
|
2401
|
+
if (!skin) {
|
|
2402
|
+
throw new Error(`glTF mesh ${mesh.name} references missing skin ${skinIndex}`);
|
|
2403
|
+
}
|
|
2404
|
+
if (skin.joints.length > MAX_RENDERABLE_SKIN_JOINTS) {
|
|
2405
|
+
return undefined;
|
|
2406
|
+
}
|
|
2407
|
+
if (!nodeWorldMatrix)
|
|
2408
|
+
return skin.skinningPalette;
|
|
2409
|
+
const inverseMeshWorld = invertLoaderMat4(nodeWorldMatrix);
|
|
2410
|
+
const matrices = new Float32Array(skin.jointBindMatrices.length * 16);
|
|
2411
|
+
for (let index = 0; index < skin.jointBindMatrices.length; index += 1) {
|
|
2412
|
+
matrices.set(multiplyLoaderMat4(inverseMeshWorld, skin.jointBindMatrices[index]), index * 16);
|
|
2413
|
+
}
|
|
2414
|
+
return { jointCount: skin.jointBindMatrices.length, matrices };
|
|
2415
|
+
}
|
|
2416
|
+
function composeLoaderMat4(translation, rotation, scale) {
|
|
2417
|
+
return composeMat4([...translation], [...rotation], [...scale]);
|
|
2418
|
+
}
|
|
2419
|
+
function multiplyLoaderMat4(left, right) {
|
|
2420
|
+
return multiplyMat4([...left], [...right]);
|
|
2421
|
+
}
|
|
2422
|
+
function invertLoaderMat4(matrix) {
|
|
2423
|
+
return invertMat4([...matrix]);
|
|
2424
|
+
}
|
|
2425
|
+
export function gltfRuntimeMaterialKey(material, contract) {
|
|
2426
|
+
const signature = runtimeMaterialSignature(contract);
|
|
2427
|
+
return signature === "base" ? material : `${material}${RUNTIME_MATERIAL_KEY_MARKER}${signature}`;
|
|
2428
|
+
}
|
|
2429
|
+
export function parseGLTFRuntimeMaterialKey(key) {
|
|
2430
|
+
const markerIndex = key.lastIndexOf(RUNTIME_MATERIAL_KEY_MARKER);
|
|
2431
|
+
if (markerIndex < 0)
|
|
2432
|
+
return { material: key, contract: {} };
|
|
2433
|
+
const signature = key.slice(markerIndex + RUNTIME_MATERIAL_KEY_MARKER.length);
|
|
2434
|
+
const contract = runtimeMaterialContractFromSignature(signature);
|
|
2435
|
+
if (!contract)
|
|
2436
|
+
return { material: key, contract: {} };
|
|
2437
|
+
return {
|
|
2438
|
+
material: key.slice(0, markerIndex),
|
|
2439
|
+
contract
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
function createRuntimeMaterialKeyResolver(json, sceneIndex, meshes, skins, nodeInstanceTransforms, materialVariant) {
|
|
2443
|
+
const signaturesByMaterial = new Map();
|
|
2444
|
+
const register = (material, contract) => {
|
|
2445
|
+
const signatures = signaturesByMaterial.get(material) ?? new Set();
|
|
2446
|
+
signatures.add(runtimeMaterialSignature(contract));
|
|
2447
|
+
signaturesByMaterial.set(material, signatures);
|
|
2448
|
+
};
|
|
2449
|
+
const visit = (nodeIndex) => {
|
|
2450
|
+
const node = json.nodes?.[nodeIndex];
|
|
2451
|
+
if (!node)
|
|
2452
|
+
return;
|
|
2453
|
+
if (node.mesh !== undefined) {
|
|
2454
|
+
const instanceTransforms = nodeInstanceTransforms.get(nodeIndex);
|
|
2455
|
+
for (const mesh of meshes.filter((entry) => entry.sourceMeshIndex === node.mesh)) {
|
|
2456
|
+
const skinning = skinningForMesh(mesh, skins, node.skin);
|
|
2457
|
+
register(materialForVariant(mesh, materialVariant), runtimeMaterialContract(skinning, instanceTransforms));
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
for (const childIndex of node.children ?? [])
|
|
2461
|
+
visit(childIndex);
|
|
2462
|
+
};
|
|
2463
|
+
for (const nodeIndex of json.scenes?.[sceneIndex]?.nodes ?? [])
|
|
2464
|
+
visit(nodeIndex);
|
|
2465
|
+
return (mesh, contract) => {
|
|
2466
|
+
const material = materialForVariant(mesh, materialVariant);
|
|
2467
|
+
const signatures = signaturesByMaterial.get(material);
|
|
2468
|
+
return signatures && signatures.size > 1 ? gltfRuntimeMaterialKey(material, contract) : material;
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
function runtimeMaterialContract(skinning, instanceTransforms) {
|
|
2472
|
+
return {
|
|
2473
|
+
...(skinning ? { skinned: true } : {}),
|
|
2474
|
+
...(instanceTransforms ? { instanced: true } : {})
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
function runtimeMaterialSignature(contract) {
|
|
2478
|
+
if (contract.skinned && contract.instanced)
|
|
2479
|
+
return "skinned+instanced";
|
|
2480
|
+
if (contract.skinned)
|
|
2481
|
+
return "skinned";
|
|
2482
|
+
if (contract.instanced)
|
|
2483
|
+
return "instanced";
|
|
2484
|
+
return "base";
|
|
2485
|
+
}
|
|
2486
|
+
function runtimeMaterialContractFromSignature(signature) {
|
|
2487
|
+
if (signature === "skinned+instanced")
|
|
2488
|
+
return { skinned: true, instanced: true };
|
|
2489
|
+
if (signature === "skinned")
|
|
2490
|
+
return { skinned: true };
|
|
2491
|
+
if (signature === "instanced")
|
|
2492
|
+
return { instanced: true };
|
|
2493
|
+
if (signature === "base")
|
|
2494
|
+
return {};
|
|
2495
|
+
return undefined;
|
|
2496
|
+
}
|
|
2497
|
+
function materialForVariant(mesh, materialVariant) {
|
|
2498
|
+
if (materialVariant === undefined)
|
|
2499
|
+
return mesh.material;
|
|
2500
|
+
return mesh.materialVariants.find((mapping) => mapping.variant === materialVariant)?.material ?? mesh.material;
|
|
2501
|
+
}
|
|
2502
|
+
function createSceneNodeForGLTFNode(scene, source, nodeIndex, nodeName, cameras, lights) {
|
|
2503
|
+
if (source.camera !== undefined && source.extensions?.KHR_lights_punctual?.light !== undefined) {
|
|
2504
|
+
throw new Error(`glTF node ${nodeIndex} cannot combine camera and KHR_lights_punctual light`);
|
|
2505
|
+
}
|
|
2506
|
+
let node;
|
|
2507
|
+
if (source.camera !== undefined) {
|
|
2508
|
+
node = createCameraNodeForGLTFNode(scene, source, nodeIndex, nodeName, cameras);
|
|
2509
|
+
applyGLTFNodeMetadata(node, source, nodeIndex);
|
|
2510
|
+
return node;
|
|
2511
|
+
}
|
|
2512
|
+
const lightIndex = source.extensions?.KHR_lights_punctual?.light;
|
|
2513
|
+
if (lightIndex === undefined) {
|
|
2514
|
+
node = scene.createNode(nodeName);
|
|
2515
|
+
applyGLTFNodeMetadata(node, source, nodeIndex);
|
|
2516
|
+
return node;
|
|
2517
|
+
}
|
|
2518
|
+
if (!Number.isInteger(lightIndex) || lightIndex < 0) {
|
|
2519
|
+
throw new RangeError(`glTF node ${nodeIndex} KHR_lights_punctual.light must be a non-negative integer`);
|
|
2520
|
+
}
|
|
2521
|
+
const light = lights[lightIndex];
|
|
2522
|
+
if (!light) {
|
|
2523
|
+
throw new Error(`glTF node ${nodeIndex} references missing punctual light ${lightIndex}`);
|
|
2524
|
+
}
|
|
2525
|
+
const lightNode = scene.createLight(light.type, nodeName);
|
|
2526
|
+
lightNode.color = [...light.color];
|
|
2527
|
+
lightNode.intensity = light.intensity;
|
|
2528
|
+
if (lightNode instanceof PointLight) {
|
|
2529
|
+
lightNode.range = light.range ?? 10;
|
|
2530
|
+
}
|
|
2531
|
+
else if (lightNode instanceof SpotLight) {
|
|
2532
|
+
lightNode.range = light.range ?? 10;
|
|
2533
|
+
lightNode.angle = light.spot?.outerConeAngle ?? Math.PI / 4;
|
|
2534
|
+
lightNode.penumbra = spotPenumbra(light.spot?.innerConeAngle ?? 0, lightNode.angle);
|
|
2535
|
+
}
|
|
2536
|
+
applyGLTFNodeMetadata(lightNode, source, nodeIndex);
|
|
2537
|
+
return lightNode;
|
|
2538
|
+
}
|
|
2539
|
+
function applyGLTFNodeMetadata(node, source, nodeIndex) {
|
|
2540
|
+
node.userData.gltfNodeIndex = nodeIndex;
|
|
2541
|
+
if (!source.extras)
|
|
2542
|
+
return;
|
|
2543
|
+
node.userData.gltfExtras = { ...source.extras };
|
|
2544
|
+
for (const [key, value] of Object.entries(source.extras)) {
|
|
2545
|
+
if (key.startsWith("a3d_"))
|
|
2546
|
+
node.userData[key] = value;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
function createCameraNodeForGLTFNode(scene, source, nodeIndex, nodeName, cameras) {
|
|
2550
|
+
const cameraIndex = source.camera;
|
|
2551
|
+
if (!Number.isInteger(cameraIndex) || cameraIndex === undefined || cameraIndex < 0) {
|
|
2552
|
+
throw new RangeError(`glTF node ${nodeIndex} camera must be a non-negative integer`);
|
|
2553
|
+
}
|
|
2554
|
+
const camera = cameras[cameraIndex];
|
|
2555
|
+
if (!camera) {
|
|
2556
|
+
throw new Error(`glTF node ${nodeIndex} references missing camera ${cameraIndex}`);
|
|
2557
|
+
}
|
|
2558
|
+
if (camera.type === "perspective") {
|
|
2559
|
+
const perspective = camera.perspective;
|
|
2560
|
+
return scene.createPerspectiveCamera({
|
|
2561
|
+
name: nodeName,
|
|
2562
|
+
fovYRadians: perspective.yfov,
|
|
2563
|
+
aspect: perspective.aspectRatio,
|
|
2564
|
+
near: perspective.znear,
|
|
2565
|
+
far: perspective.zfar
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
const orthographic = camera.orthographic;
|
|
2569
|
+
return scene.createOrthographicCamera({
|
|
2570
|
+
name: nodeName,
|
|
2571
|
+
left: -orthographic.xmag / 2,
|
|
2572
|
+
right: orthographic.xmag / 2,
|
|
2573
|
+
bottom: -orthographic.ymag / 2,
|
|
2574
|
+
top: orthographic.ymag / 2,
|
|
2575
|
+
near: orthographic.znear,
|
|
2576
|
+
far: orthographic.zfar
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
function spotPenumbra(innerConeAngle, outerConeAngle) {
|
|
2580
|
+
if (outerConeAngle <= 0)
|
|
2581
|
+
return 0;
|
|
2582
|
+
return Math.max(0, Math.min(1, 1 - innerConeAngle / outerConeAngle));
|
|
2583
|
+
}
|
|
2584
|
+
function createGLTFNodeNameResolver(json) {
|
|
2585
|
+
const bases = (json.nodes ?? []).map((node, index) => typeof node.name === "string" && node.name.trim().length > 0 ? node.name : `node-${index}`);
|
|
2586
|
+
const totals = new Map();
|
|
2587
|
+
for (const base of bases) {
|
|
2588
|
+
totals.set(base, (totals.get(base) ?? 0) + 1);
|
|
2589
|
+
}
|
|
2590
|
+
const seen = new Map();
|
|
2591
|
+
const names = bases.map((base) => {
|
|
2592
|
+
const count = seen.get(base) ?? 0;
|
|
2593
|
+
seen.set(base, count + 1);
|
|
2594
|
+
return (totals.get(base) ?? 0) > 1 && count > 0 ? `${base}_${count}` : base;
|
|
2595
|
+
});
|
|
2596
|
+
return (nodeIndex) => {
|
|
2597
|
+
const name = names[nodeIndex];
|
|
2598
|
+
if (name === undefined) {
|
|
2599
|
+
throw new Error(`glTF node ${nodeIndex} is missing`);
|
|
2600
|
+
}
|
|
2601
|
+
return name;
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
function findSkinForMesh(json, meshIndex) {
|
|
2605
|
+
const nodeIndex = (json.nodes ?? []).findIndex((node) => node.mesh === meshIndex && node.skin !== undefined);
|
|
2606
|
+
if (nodeIndex < 0)
|
|
2607
|
+
return undefined;
|
|
2608
|
+
return json.nodes?.[nodeIndex]?.skin;
|
|
2609
|
+
}
|
|
2610
|
+
function parentIndexByNode(json) {
|
|
2611
|
+
const parents = new Map();
|
|
2612
|
+
for (const [parentIndex, node] of (json.nodes ?? []).entries()) {
|
|
2613
|
+
for (const childIndex of node.children ?? []) {
|
|
2614
|
+
parents.set(childIndex, parentIndex);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
return parents;
|
|
2618
|
+
}
|
|
2619
|
+
function orderSkinJoints(json, joints) {
|
|
2620
|
+
const jointSet = new Set(joints);
|
|
2621
|
+
const ordered = [];
|
|
2622
|
+
const visited = new Set();
|
|
2623
|
+
const parents = parentIndexByNode(json);
|
|
2624
|
+
const visit = (joint) => {
|
|
2625
|
+
if (visited.has(joint))
|
|
2626
|
+
return;
|
|
2627
|
+
const parent = parents.get(joint);
|
|
2628
|
+
if (parent !== undefined && jointSet.has(parent))
|
|
2629
|
+
visit(parent);
|
|
2630
|
+
if (!json.nodes?.[joint]) {
|
|
2631
|
+
throw new Error(`glTF skin references missing joint node ${joint}`);
|
|
2632
|
+
}
|
|
2633
|
+
visited.add(joint);
|
|
2634
|
+
ordered.push(joint);
|
|
2635
|
+
};
|
|
2636
|
+
for (const joint of joints)
|
|
2637
|
+
visit(joint);
|
|
2638
|
+
return ordered;
|
|
2639
|
+
}
|
|
2640
|
+
function valueTypeForAnimationPath(path) {
|
|
2641
|
+
if (path === "rotation")
|
|
2642
|
+
return "quaternion";
|
|
2643
|
+
if (path === "translation" || path === "scale")
|
|
2644
|
+
return "vector3";
|
|
2645
|
+
return "number-array";
|
|
2646
|
+
}
|
|
2647
|
+
function animationValueForPath(path, row) {
|
|
2648
|
+
if (path === "rotation")
|
|
2649
|
+
return toQuat(row);
|
|
2650
|
+
if (path === "translation" || path === "scale")
|
|
2651
|
+
return toVec3(row);
|
|
2652
|
+
return [...row];
|
|
2653
|
+
}
|
|
2654
|
+
function toVec3(row) {
|
|
2655
|
+
return [row[0] ?? 0, row[1] ?? 0, row[2] ?? 0];
|
|
2656
|
+
}
|
|
2657
|
+
function toQuat(row) {
|
|
2658
|
+
return [row[0] ?? 0, row[1] ?? 0, row[2] ?? 0, row[3] ?? 1];
|
|
2659
|
+
}
|
|
2660
|
+
function toMat4(row) {
|
|
2661
|
+
if (row.length !== 16) {
|
|
2662
|
+
throw new Error(`glTF MAT4 accessor row has ${row.length} components`);
|
|
2663
|
+
}
|
|
2664
|
+
return [
|
|
2665
|
+
row[0], row[1], row[2], row[3],
|
|
2666
|
+
row[4], row[5], row[6], row[7],
|
|
2667
|
+
row[8], row[9], row[10], row[11],
|
|
2668
|
+
row[12], row[13], row[14], row[15]
|
|
2669
|
+
];
|
|
2670
|
+
}
|
|
2671
|
+
function identityMat4() {
|
|
2672
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
2673
|
+
}
|
|
2674
|
+
function serializeGLTFAsset(url, loaderDiagnostics, images, textures, materials, materialVariants, scenes, defaultScene, meshes, cameras, lights, skins, animations) {
|
|
2675
|
+
return {
|
|
2676
|
+
url,
|
|
2677
|
+
loaderDiagnostics,
|
|
2678
|
+
images: images.map((image) => ({
|
|
2679
|
+
name: image.name,
|
|
2680
|
+
...(image.uri ? { uri: image.uri } : {}),
|
|
2681
|
+
...(image.mimeType ? { mimeType: image.mimeType } : {}),
|
|
2682
|
+
...(image.data ? { byteLength: image.data.byteLength } : {})
|
|
2683
|
+
})),
|
|
2684
|
+
textures: textures.map((texture) => ({ ...texture })),
|
|
2685
|
+
materials: materials.map((material) => ({ ...material })),
|
|
2686
|
+
materialVariants: materialVariants.map((variant) => ({ ...variant })),
|
|
2687
|
+
scenes: scenes.map((scene) => ({ name: scene.name, nodeIndices: [...scene.nodeIndices] })),
|
|
2688
|
+
defaultScene,
|
|
2689
|
+
meshes: meshes.map((mesh) => ({
|
|
2690
|
+
...mesh,
|
|
2691
|
+
positions: mesh.positions.map((position) => [...position]),
|
|
2692
|
+
normals: mesh.normals.map((normal) => [...normal]),
|
|
2693
|
+
texcoords: mesh.texcoords.map((texcoord) => [...texcoord]),
|
|
2694
|
+
texcoordSets: mesh.texcoordSets.map((set) => set.map((texcoord) => [...texcoord])),
|
|
2695
|
+
tangents: mesh.tangents.map((tangent) => [...tangent]),
|
|
2696
|
+
colors: mesh.colors.map((color) => [...color]),
|
|
2697
|
+
joints: mesh.joints.map((joint) => [...joint]),
|
|
2698
|
+
weights: mesh.weights.map((weight) => [...weight]),
|
|
2699
|
+
morphTargets: mesh.morphTargets.map((target) => ({
|
|
2700
|
+
positions: target.positions.map((position) => [...position]),
|
|
2701
|
+
normals: target.normals.map((normal) => [...normal]),
|
|
2702
|
+
tangents: target.tangents.map((tangent) => [...tangent])
|
|
2703
|
+
})),
|
|
2704
|
+
morphWeights: [...mesh.morphWeights],
|
|
2705
|
+
indices: mesh.indices ? [...mesh.indices] : undefined
|
|
2706
|
+
})),
|
|
2707
|
+
cameras: cameras.map((camera) => ({
|
|
2708
|
+
...camera,
|
|
2709
|
+
...(camera.perspective ? { perspective: { ...camera.perspective } } : {}),
|
|
2710
|
+
...(camera.orthographic ? { orthographic: { ...camera.orthographic } } : {})
|
|
2711
|
+
})),
|
|
2712
|
+
lights: lights.map((light) => ({
|
|
2713
|
+
...light,
|
|
2714
|
+
color: [...light.color],
|
|
2715
|
+
...(light.spot ? { spot: { ...light.spot } } : {})
|
|
2716
|
+
})),
|
|
2717
|
+
skins: skins.map((skin) => ({
|
|
2718
|
+
name: skin.name,
|
|
2719
|
+
joints: [...skin.joints],
|
|
2720
|
+
jointNames: [...skin.jointNames],
|
|
2721
|
+
inverseBindMatrices: skin.inverseBindMatrices.map((matrix) => [...matrix]),
|
|
2722
|
+
...(skin.skeletonRoot !== undefined ? { skeletonRoot: skin.skeletonRoot } : {}),
|
|
2723
|
+
bones: skin.skeleton.bones.map((bone) => ({
|
|
2724
|
+
name: bone.name,
|
|
2725
|
+
parentIndex: bone.parentIndex,
|
|
2726
|
+
translation: [...bone.translation],
|
|
2727
|
+
rotation: [...bone.rotation],
|
|
2728
|
+
scale: [...bone.scale],
|
|
2729
|
+
inverseBindMatrix: [...bone.inverseBindMatrix]
|
|
2730
|
+
}))
|
|
2731
|
+
})),
|
|
2732
|
+
animations: animations.map((animation) => animation.toJSON())
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
//# sourceMappingURL=GLTFLoader.js.map
|