@woosh/meep-engine 3.29.0 → 3.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/bundle-worker-image-decoder.js +1 -1
- package/build/bundle-worker-terrain.js +1 -1
- package/package.json +1 -1
- package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/BinaryBuffer.js +14 -2
- package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/BVH.js +14 -1
- package/src/core/cache/FrequencySketch.d.ts +3 -1
- package/src/core/cache/FrequencySketch.d.ts.map +1 -1
- package/src/core/cache/FrequencySketch.js +10 -4
- package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -1
- package/src/core/cache/wtinylfu/CacheWTinylfu.js +24 -2
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts +20 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js +69 -4
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts +48 -18
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +262 -70
- package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts +25 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.js +228 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts +13 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.js +10 -128
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.js +11 -1
- package/src/core/graph/mn_graph_collapse_weighted_edge.js +6 -6
- package/src/core/math/lookup/ParameterLookupTable.d.ts.map +1 -1
- package/src/core/math/lookup/ParameterLookupTable.js +10 -5
- package/src/core/math/noise/sdnoise.d.ts.map +1 -1
- package/src/core/math/noise/sdnoise.js +16 -10
- package/src/core/model/object/ImmutableObjectPool.d.ts.map +1 -1
- package/src/core/model/object/ImmutableObjectPool.js +17 -7
- package/src/core/process/executor/ConcurrentExecutor.d.ts +1 -5
- package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
- package/src/core/process/executor/ConcurrentExecutor.js +95 -1
- package/src/core/process/task/task_find_dependency_cycle.d.ts +17 -0
- package/src/core/process/task/task_find_dependency_cycle.d.ts.map +1 -0
- package/src/core/process/task/task_find_dependency_cycle.js +81 -0
- package/src/core/wfc/WFCCellChoice.d.ts +17 -0
- package/src/core/wfc/WFCCellChoice.d.ts.map +1 -0
- package/src/core/wfc/WFCCellChoice.js +36 -0
- package/src/core/wfc/WFCLattice.d.ts +105 -0
- package/src/core/wfc/WFCLattice.d.ts.map +1 -0
- package/src/core/wfc/WFCLattice.js +235 -0
- package/src/core/wfc/WFCRuleTable.d.ts +172 -0
- package/src/core/wfc/WFCRuleTable.d.ts.map +1 -0
- package/src/core/wfc/WFCRuleTable.js +334 -0
- package/src/core/wfc/WFCState.d.ts +273 -0
- package/src/core/wfc/WFCState.d.ts.map +1 -0
- package/src/core/wfc/WFCState.js +552 -0
- package/src/core/wfc/WFCStatus.d.ts +11 -0
- package/src/core/wfc/WFCStatus.d.ts.map +1 -0
- package/src/core/wfc/WFCStatus.js +28 -0
- package/src/core/wfc/WFCTileWeights.d.ts +48 -0
- package/src/core/wfc/WFCTileWeights.d.ts.map +1 -0
- package/src/core/wfc/WFCTileWeights.js +86 -0
- package/src/core/wfc/WFC_UNRESOLVED.d.ts +12 -0
- package/src/core/wfc/WFC_UNRESOLVED.d.ts.map +1 -0
- package/src/core/wfc/WFC_UNRESOLVED.js +11 -0
- package/src/core/wfc/wfc_estimate_bytes.d.ts +24 -0
- package/src/core/wfc/wfc_estimate_bytes.d.ts.map +1 -0
- package/src/core/wfc/wfc_estimate_bytes.js +47 -0
- package/src/core/wfc/wfc_rule_table_tile_components.d.ts +27 -0
- package/src/core/wfc/wfc_rule_table_tile_components.d.ts.map +1 -0
- package/src/core/wfc/wfc_rule_table_tile_components.js +80 -0
- package/src/core/wfc/wfc_solve.d.ts +21 -0
- package/src/core/wfc/wfc_solve.d.ts.map +1 -0
- package/src/core/wfc/wfc_solve.js +154 -0
- package/src/core/wfc/wfc_state_apply_boundary.d.ts +18 -0
- package/src/core/wfc/wfc_state_apply_boundary.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_apply_boundary.js +61 -0
- package/src/core/wfc/wfc_state_assign.d.ts +11 -0
- package/src/core/wfc/wfc_state_assign.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_assign.js +36 -0
- package/src/core/wfc/wfc_state_ban_bits.d.ts +17 -0
- package/src/core/wfc/wfc_state_ban_bits.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_ban_bits.js +81 -0
- package/src/core/wfc/wfc_state_collapse.d.ts +12 -0
- package/src/core/wfc/wfc_state_collapse.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_collapse.js +73 -0
- package/src/core/wfc/wfc_state_flush_dirty.d.ts +12 -0
- package/src/core/wfc/wfc_state_flush_dirty.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_flush_dirty.js +39 -0
- package/src/core/wfc/wfc_state_propagate.d.ts +14 -0
- package/src/core/wfc/wfc_state_propagate.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_propagate.js +155 -0
- package/src/core/wfc/wfc_state_read.d.ts +11 -0
- package/src/core/wfc/wfc_state_read.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_read.js +38 -0
- package/src/core/wfc/wfc_state_restrict.d.ts +18 -0
- package/src/core/wfc/wfc_state_restrict.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_restrict.js +39 -0
- package/src/core/wfc/wfc_state_set_eligible.d.ts +24 -0
- package/src/core/wfc/wfc_state_set_eligible.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_set_eligible.js +49 -0
- package/src/core/wfc/wfc_state_undo_to.d.ts +13 -0
- package/src/core/wfc/wfc_state_undo_to.d.ts.map +1 -0
- package/src/core/wfc/wfc_state_undo_to.js +84 -0
- package/src/core/wfc/wfc_words_per_cell.d.ts +11 -0
- package/src/core/wfc/wfc_words_per_cell.d.ts.map +1 -0
- package/src/core/wfc/wfc_words_per_cell.js +16 -0
- package/src/engine/ecs/EntityComponentDataset.d.ts +8 -0
- package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
- package/src/engine/ecs/EntityComponentDataset.js +37 -14
- package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts +8 -1
- package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts.map +1 -1
- package/src/engine/ecs/fow/FogOfWarRevealerSystem.js +37 -2
- package/src/engine/ecs/guid/UUID.d.ts.map +1 -1
- package/src/engine/ecs/guid/UUID.js +45 -17
- package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.d.ts.map +1 -1
- package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +3 -1
- package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +366 -360
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.js +24 -10
- package/src/engine/grid/grid2transform/GridPosition2TransformSystem.d.ts.map +1 -1
- package/src/engine/grid/grid2transform/GridPosition2TransformSystem.js +24 -5
- package/src/engine/interpolation/Interpoland.d.ts +7 -1
- package/src/engine/interpolation/Interpoland.d.ts.map +1 -1
- package/src/engine/interpolation/Interpoland.js +8 -1
- package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -1
- package/src/engine/interpolation/InterpolationSystem.js +3 -0
- package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +5 -3
- package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -1
- package/src/engine/interpolation/TransformPoseSerializationAdapter.js +14 -3
- package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -1
- package/src/engine/interpolation/pose_interpoland.js +2 -0
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +15 -10
- package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothWorld.js +17 -0
- package/src/engine/physics/mls-mpm/MLS_MPM.d.ts.map +1 -1
- package/src/engine/physics/mls-mpm/MLS_MPM.js +14 -0
- package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts +2 -1
- package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +1 -1
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts +1 -0
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts +1 -0
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts +2 -1
- package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +12 -3
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts +1 -0
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +1 -0
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
- package/src/engine/sound/sopra/runtime/EventInstance.js +4 -1
- package/src/format/image/avif/av1/decode/decode_coefficients.d.ts.map +1 -1
- package/src/format/image/avif/av1/decode/decode_coefficients.js +248 -259
- package/src/format/image/avif/av1/decode/read_golomb.d.ts +21 -0
- package/src/format/image/avif/av1/decode/read_golomb.d.ts.map +1 -0
- package/src/format/image/avif/av1/decode/read_golomb.js +49 -0
- package/src/format/image/avif/av1/filter/loop_restoration.js +461 -451
- package/src/format/image/avif/av1/util/round2.d.ts +19 -2
- package/src/format/image/avif/av1/util/round2.d.ts.map +1 -1
- package/src/format/image/avif/av1/util/round2.js +51 -27
- package/src/format/image/avif/heif/parse_avif_file.js +24 -0
- package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
- package/src/shade/renderer/buffer/table/GPUTypedTable.js +14 -0
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +46 -3
- package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +91 -1
- package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +0 -13
- package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +0 -15
- package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +5 -5
- package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts +11 -1
- package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.js +11 -1
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +18 -9
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.js +20 -26
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +1 -3
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.js +2 -5
- package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +14 -1
- package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.js +12 -3
- package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.d.ts.map +1 -1
- package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.js +7 -1
- package/src/shade/renderer/lightmap/GPULightMap.d.ts.map +1 -1
- package/src/shade/renderer/lightmap/GPULightMap.js +349 -336
- package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
- package/src/shade/renderer/loader/gltf/load_gltf.js +1137 -1128
- package/src/shade/renderer/particles/DESIGN.md +14 -5
- package/src/shade/renderer/particles/GPUParticleSystem.d.ts +25 -10
- package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
- package/src/shade/renderer/particles/GPUParticleSystem.js +182 -38
- package/src/shade/renderer/particles/graph/groups/simplex_reference.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/groups/simplex_reference.js +16 -10
- package/src/shade/renderer/particles/graph_particles.d.ts +4 -2
- package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph_particles.js +8 -2
- package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
- package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +8 -2
- package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -1
- package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +10 -4
- package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts +31 -0
- package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts.map +1 -0
- package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.js +62 -0
- package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +13 -0
- package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
- package/src/shade/renderer/particles/vm/ParticleVMReference.js +95 -17
- package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts +21 -0
- package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts.map +1 -0
- package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js +28 -0
- package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +19 -2
- package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +19 -2
- package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
- package/src/shade/renderer/shader/type/WebGPUStruct.js +12 -0
- package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts +8 -2
- package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts.map +1 -1
- package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +41 -6
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2383 -2356
- package/src/view/tooltip/DomTooltipManager.d.ts +7 -0
- package/src/view/tooltip/DomTooltipManager.d.ts.map +1 -1
- package/src/view/tooltip/DomTooltipManager.js +22 -0
- package/src/view/tooltip/DomTooltipObserver.d.ts.map +1 -1
- package/src/view/tooltip/DomTooltipObserver.js +13 -3
- package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.js +0 -45
|
@@ -1,1128 +1,1137 @@
|
|
|
1
|
-
import { assert } from "../../../../core/assert.js";
|
|
2
|
-
import { HashMap } from "../../../../core/collection/map/HashMap.js";
|
|
3
|
-
import { HashSet } from "../../../../core/collection/set/HashSet.js";
|
|
4
|
-
import { Color } from "../../../../core/color/Color.js";
|
|
5
|
-
import { Quaternion } from "../../../../core/geom/Quaternion.js";
|
|
6
|
-
import Vector3 from "../../../../core/geom/Vector3.js";
|
|
7
|
-
import { clamp } from "../../../../core/math/clamp.js";
|
|
8
|
-
import { clamp01 } from "../../../../core/math/clamp01.js";
|
|
9
|
-
import { computeObjectHash } from "../../../../core/math/hash/computeObjectHash.js";
|
|
10
|
-
import { inverseLerp } from "../../../../core/math/inverseLerp.js";
|
|
11
|
-
import { objectDeepEquals } from "../../../../core/model/object/objectDeepEquals.js";
|
|
12
|
-
import { AnimationInterpolationKind } from "../../../../engine/animation/clip/AnimationInterpolationKind.js";
|
|
13
|
-
import { curve_from_track_data } from "../../../../engine/animation/clip/curve_from_track_data.js";
|
|
14
|
-
import { AnimationCurve } from "../../../../engine/animation/curve/AnimationCurve.js";
|
|
15
|
-
import { t64_look_rotation } from "../../../../engine/ecs/transform/t64_look_rotation.js";
|
|
16
|
-
import { t64_transform_direction } from "../../../../engine/ecs/transform/t64_transform_direction.js";
|
|
17
|
-
import { meshopt_optimizeVertexCache } from "../../../../engine/graphics/geometry/optimization/VertexCacheOptimizer.js";
|
|
18
|
-
import { ShadeAnimationChannel } from "../../animation/ShadeAnimationChannel.js";
|
|
19
|
-
import { ShadeAnimationClip } from "../../animation/ShadeAnimationClip.js";
|
|
20
|
-
import { Skin } from "../../animation/Skin.js";
|
|
21
|
-
import { Attribute } from "../../geometry/Attribute.js";
|
|
22
|
-
import { Geometry, GeometryFlags } from "../../geometry/Geometry.js";
|
|
23
|
-
import { meshlet_geometry_build_from_geometry } from "../../geometry/meshlet_geometry_build_from_geometry.js";
|
|
24
|
-
import { StandardAttributes } from "../../geometry/StandardAttributes.js";
|
|
25
|
-
import { normalize_attribute } from "../../geometry/util/normalize_attribute.js";
|
|
26
|
-
import { DirectionalLight } from "../../light/model/DirectionalLight.js";
|
|
27
|
-
import { PointLight } from "../../light/model/PointLight.js";
|
|
28
|
-
import { SpotLight } from "../../light/model/SpotLight.js";
|
|
29
|
-
|
|
30
|
-
import { StandardShadeMaterial } from "../../material/StandardShadeMaterial.js";
|
|
31
|
-
import { Node3DProperty } from "../../object_property/Node3DProperty.js";
|
|
32
|
-
import { ShadeTexture } from "../../texture/ShadeTexture.js";
|
|
33
|
-
import { ShadeImage } from "../../texture/source/ShadeImage.js";
|
|
34
|
-
import { SceneBundle } from "../SceneBundle.js";
|
|
35
|
-
import { SceneNode } from "../SceneNode.js";
|
|
36
|
-
import { coalesce_array_duplicates } from "../../../../format/scene/gltf/coalesce_array_duplicates.js";
|
|
37
|
-
import { GLTF_COMPONENT_TYPE } from "../../../../format/scene/gltf/gltf_component_type.js";
|
|
38
|
-
import { gltf_create_material } from "./gltf_create_material.js";
|
|
39
|
-
import { gltf_create_texture } from "./gltf_create_texture.js";
|
|
40
|
-
import { GLTF_PRIMITIVE_MODE, GLTF_PRIMITIVE_MODE_DEFAULT } from "../../../../format/scene/gltf/GLTF_PRIMITIVE_MODE.js";
|
|
41
|
-
import { gltf_read_accessor } from "../../../../format/scene/gltf/gltf_read_accessor.js";
|
|
42
|
-
import { gltf_texture_image_index, gltf_used_image_indices } from "../../../../format/scene/gltf/gltf_used_image_indices.js";
|
|
43
|
-
import { TinyGltf } from "./tiny-gltf.js";
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Somewhere to hand `canonicalize_quaternion_signs` a rest rotation it can read by component name —
|
|
48
|
-
* a `Transform64` addresses its rotation by index, and the function's contract is named components.
|
|
49
|
-
*
|
|
50
|
-
* @type {Quaternion}
|
|
51
|
-
*/
|
|
52
|
-
const SCRATCH_REST_ROTATION = new Quaternion();
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Somewhere to read a light's facing before flipping it.
|
|
56
|
-
*
|
|
57
|
-
* @type {Vector3}
|
|
58
|
-
*/
|
|
59
|
-
const SCRATCH_DIRECTION = new Vector3();
|
|
60
|
-
const OPTIMIZE_GEOMETRIES = false;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @see https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html
|
|
64
|
-
* @param {string} id
|
|
65
|
-
*/
|
|
66
|
-
function gltf_attribute_id_to_standard(id) {
|
|
67
|
-
switch (id) {
|
|
68
|
-
case "POSITION":
|
|
69
|
-
return StandardAttributes.Position;
|
|
70
|
-
case "NORMAL":
|
|
71
|
-
return StandardAttributes.Normal;
|
|
72
|
-
case "TEXCOORD_0":
|
|
73
|
-
return StandardAttributes.TextureCoordinates0;
|
|
74
|
-
case "TEXCOORD_1":
|
|
75
|
-
return StandardAttributes.TextureCoordinates1;
|
|
76
|
-
case "TANGENT":
|
|
77
|
-
return StandardAttributes.Tangent;
|
|
78
|
-
case "COLOR_0":
|
|
79
|
-
return StandardAttributes.Color;
|
|
80
|
-
case "JOINTS_0":
|
|
81
|
-
return StandardAttributes.SkinningJoints;
|
|
82
|
-
case "WEIGHTS_0":
|
|
83
|
-
return StandardAttributes.SkinningWeights;
|
|
84
|
-
default:
|
|
85
|
-
return id;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Map glTF `animation.channels[].target.path` to {@link Node3DProperty}.
|
|
91
|
-
* Returns `-1` for paths we don't yet drive (morph-target `weights`),
|
|
92
|
-
* so the caller can skip those channels without throwing.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} path
|
|
95
|
-
* @returns {number}
|
|
96
|
-
*/
|
|
97
|
-
function gltf_animation_path_to_property(path) {
|
|
98
|
-
switch (path) {
|
|
99
|
-
case "translation":
|
|
100
|
-
return Node3DProperty.Translation;
|
|
101
|
-
case "rotation":
|
|
102
|
-
return Node3DProperty.Rotation;
|
|
103
|
-
case "scale":
|
|
104
|
-
return Node3DProperty.Scale;
|
|
105
|
-
default:
|
|
106
|
-
// "weights" (morph targets) lands here; we don't yet have
|
|
107
|
-
// morph-target support in the renderer.
|
|
108
|
-
return -1;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Number of scalar components per keyframe value for a given Node3D
|
|
114
|
-
* property — translation/scale = 3 (vec3), rotation = 4 (quaternion
|
|
115
|
-
* x/y/z/w).
|
|
116
|
-
*
|
|
117
|
-
* @param {number} property one of {@link Node3DProperty}
|
|
118
|
-
* @returns {number}
|
|
119
|
-
*/
|
|
120
|
-
function node3d_property_component_count(property) {
|
|
121
|
-
switch (property) {
|
|
122
|
-
case Node3DProperty.Translation:
|
|
123
|
-
case Node3DProperty.Scale:
|
|
124
|
-
return 3;
|
|
125
|
-
case Node3DProperty.Rotation:
|
|
126
|
-
return 4;
|
|
127
|
-
default:
|
|
128
|
-
throw new Error(`Unsupported animated property ${property}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Walk a flat keyframe-values buffer for a rotation channel and flip
|
|
134
|
-
* the sign of any keyframe whose val-quaternion has negative dot
|
|
135
|
-
* product with the previous keyframe's val-quaternion. The same flip
|
|
136
|
-
* is applied to that keyframe's in/out tangents in CUBICSPLINE mode
|
|
137
|
-
* so the cubic remains internally consistent.
|
|
138
|
-
*
|
|
139
|
-
* The flip preserves the rotation (q and -q encode the same
|
|
140
|
-
* orientation) while ensuring per-component interpolation between
|
|
141
|
-
* consecutive keyframes follows the short arc instead of passing
|
|
142
|
-
* through a near-zero quaternion mid-segment.
|
|
143
|
-
*
|
|
144
|
-
* The curve's leading sign is anchored against `rest_rotation` —
|
|
145
|
-
* the target node's authored local rest rotation: when the first
|
|
146
|
-
* keyframe's val-quat has negative dot product with the rest
|
|
147
|
-
* quaternion, the whole first keyframe block is negated (and the
|
|
148
|
-
* consecutive-keyframe pass propagates that sign down the curve).
|
|
149
|
-
* Every rotation curve targeting one node aligns against the same
|
|
150
|
-
* reference, so weighted multi-clip blends accumulate
|
|
151
|
-
* hemisphere-consistent contributions instead of cancelling toward
|
|
152
|
-
* a near-zero quaternion. Residual limitation: the anchor is the
|
|
153
|
-
* FIRST keyframe only — a curve that swings more than 90° away
|
|
154
|
-
* from the rest rotation mid-clip can still contribute opposite
|
|
155
|
-
* signs to another clip at those times; fixing that would need
|
|
156
|
-
* apply-time alignment, which the atomic pose accumulator cannot
|
|
157
|
-
* do order-independently.
|
|
158
|
-
*
|
|
159
|
-
* Mutates `values` in place. Safe to call on an output array that's
|
|
160
|
-
* a fresh copy of the source accessor (which is what
|
|
161
|
-
* {@link read_accessor_as_float32_array} produces).
|
|
162
|
-
*
|
|
163
|
-
* Exported for spec coverage; not part of the loader's public API.
|
|
164
|
-
*
|
|
165
|
-
* @param {Float32Array} values flat values: STEP/LINEAR uses 4
|
|
166
|
-
* floats/keyframe (x, y, z, w); CUBICSPLINE uses 12 (inX, inY,
|
|
167
|
-
* inZ, inW, valX, valY, valZ, valW, outX, outY, outZ, outW).
|
|
168
|
-
* @param {number} key_count
|
|
169
|
-
* @param {number} interpolation one of {@link AnimationInterpolationKind}
|
|
170
|
-
* @param {{x:number, y:number, z:number, w:number}} [rest_rotation]
|
|
171
|
-
* target node's authored local rest rotation; omitted → the
|
|
172
|
-
* leading sign is kept as authored.
|
|
173
|
-
*/
|
|
174
|
-
export function canonicalize_quaternion_signs(values, key_count, interpolation, rest_rotation) {
|
|
175
|
-
const is_cubic = interpolation === AnimationInterpolationKind.CubicTangents;
|
|
176
|
-
const stride = is_cubic ? 12 : 4;
|
|
177
|
-
// Offset into a keyframe block at which the val-quat sits.
|
|
178
|
-
// CUBICSPLINE has [in, val, out]; the val starts at offset 4 (i.e.
|
|
179
|
-
// after the 4-float in-tangent). STEP/LINEAR has just val at 0.
|
|
180
|
-
const val_offset = is_cubic ? 4 : 0;
|
|
181
|
-
|
|
182
|
-
if (key_count >= 1 && rest_rotation !== undefined) {
|
|
183
|
-
const rest_dot =
|
|
184
|
-
values[val_offset + 0] * rest_rotation.x +
|
|
185
|
-
values[val_offset + 1] * rest_rotation.y +
|
|
186
|
-
values[val_offset + 2] * rest_rotation.z +
|
|
187
|
-
values[val_offset + 3] * rest_rotation.w;
|
|
188
|
-
|
|
189
|
-
if (rest_dot < 0) {
|
|
190
|
-
// Negate the whole first keyframe block (value +
|
|
191
|
-
// tangents); the loop below carries the flip through
|
|
192
|
-
// the remaining keyframes.
|
|
193
|
-
for (let i = 0; i < stride; i++) {
|
|
194
|
-
values[i] = -values[i];
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (let k = 1; k < key_count; k++) {
|
|
200
|
-
const prev_base = (k - 1) * stride + val_offset;
|
|
201
|
-
const cur_base = k * stride + val_offset;
|
|
202
|
-
|
|
203
|
-
const dot =
|
|
204
|
-
values[prev_base + 0] * values[cur_base + 0] +
|
|
205
|
-
values[prev_base + 1] * values[cur_base + 1] +
|
|
206
|
-
values[prev_base + 2] * values[cur_base + 2] +
|
|
207
|
-
values[prev_base + 3] * values[cur_base + 3];
|
|
208
|
-
|
|
209
|
-
if (dot < 0) {
|
|
210
|
-
// Flip the whole keyframe block (value + tangents).
|
|
211
|
-
const start = k * stride;
|
|
212
|
-
const end = start + stride;
|
|
213
|
-
for (let i = start; i < end; i++) {
|
|
214
|
-
values[i] = -values[i];
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Map glTF interpolation strings to the engine's enum. Defaults to
|
|
222
|
-
* LINEAR when missing — the glTF spec's default.
|
|
223
|
-
*
|
|
224
|
-
* @param {string|undefined} interpolation
|
|
225
|
-
* @returns {number} one of {@link AnimationInterpolationKind}
|
|
226
|
-
*/
|
|
227
|
-
function gltf_interpolation_to_kind(interpolation) {
|
|
228
|
-
switch (interpolation) {
|
|
229
|
-
case "STEP":
|
|
230
|
-
return AnimationInterpolationKind.Discrete;
|
|
231
|
-
case "CUBICSPLINE":
|
|
232
|
-
return AnimationInterpolationKind.CubicTangents;
|
|
233
|
-
case "LINEAR":
|
|
234
|
-
case undefined:
|
|
235
|
-
return AnimationInterpolationKind.Linear;
|
|
236
|
-
default:
|
|
237
|
-
throw new Error(`Unsupported interpolation '${interpolation}'`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Input can be a .glb or .gltf file.
|
|
243
|
-
*
|
|
244
|
-
* Does not support most compression GLTF extensions such as draco, basis etc.
|
|
245
|
-
*
|
|
246
|
-
* Returns a {@link SceneBundle} that groups the parsed scene roots
|
|
247
|
-
* with any skins and animation clips extracted from the same asset.
|
|
248
|
-
* Callers add `bundle.scenes` to their scene, then pass the bundle's
|
|
249
|
-
* `skins` / `clips` to the scene's animation manager (e.g. via
|
|
250
|
-
* {@link GPUAnimationManager#register_skin} and
|
|
251
|
-
* {@link GPUAnimationManager#register_clip}) once the bundle has been
|
|
252
|
-
* compiled into a prefab and instantiated, so that the joints and targets
|
|
253
|
-
* are entities holding rows.
|
|
254
|
-
*
|
|
255
|
-
* @param {string} url un-resolved — the asset manager resolves network paths
|
|
256
|
-
* @param {Object} options
|
|
257
|
-
* @param {AssetManager} options.assetManager where the container, its buffers and its images come
|
|
258
|
-
* from. Required: a glTF is several files, and which of them are already in memory is not this
|
|
259
|
-
* parser's business to know.
|
|
260
|
-
* @param {AssetRequestScope} [options.scope] the load these requests belong to
|
|
261
|
-
* @param {Map<string, File|Blob|ArrayBuffer>} [options.fileMap] Map of relative paths to file data.
|
|
262
|
-
* When provided, buffer and image URIs in the GLTF are resolved from this map before the asset
|
|
263
|
-
* framework is asked. Enables multi-file GLTF loading from drag-and-drop.
|
|
264
|
-
* @param {Set<GPUTextureFormat>} [options.texture_support] what the device can sample, from
|
|
265
|
-
* `gpu_texture_compression_support(device)`. Only a `KHR_texture_basisu` image reads it, to choose
|
|
266
|
-
* what to transcode into; omitted, the loader targets the best format the transcoder writes.
|
|
267
|
-
* @return {Promise<SceneBundle>}
|
|
268
|
-
*/
|
|
269
|
-
export function load_gltf(url, { assetManager, scope, fileMap, texture_support } = {}) {
|
|
270
|
-
// TODO
|
|
271
|
-
// apply some checks to the model, things like non-uniform scales, that can be fixed potentially
|
|
272
|
-
// see https://www.youtube.com/watch?v=FJRO5wFTEc8 for common ideas
|
|
273
|
-
// check for inverted normals
|
|
274
|
-
// check for duplicated vertices
|
|
275
|
-
// check for overlapping faces
|
|
276
|
-
|
|
277
|
-
async function process(gltf) {
|
|
278
|
-
// console.log(gltf);
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* @type {{type:string, componentType:number, count:number, bufferView:number, byteOffset:number, normalized:boolean, min?:number[], max?:number}[]}
|
|
282
|
-
*/
|
|
283
|
-
const accessors = gltf.accessors;
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* `aabb` is six numbers, `[x0, y0, z0, x1, y1, z1]`, in world space — and is absent on a node
|
|
287
|
-
* that bounds nothing, meaning neither it nor anything below it declared any.
|
|
288
|
-
*
|
|
289
|
-
* {{aabb?:Float64Array, mesh:number, worldMatrix:Float32Array}[]}
|
|
290
|
-
*/
|
|
291
|
-
const nodes = gltf.nodes;
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
*
|
|
295
|
-
* @type {{aabb:Float64Array, primitives:({attributes:Object<number>,indices:number, material:number, mode:number}[])}[]}
|
|
296
|
-
*/
|
|
297
|
-
const meshes = gltf.meshes;
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
*
|
|
301
|
-
* @type {{}[]}
|
|
302
|
-
*/
|
|
303
|
-
const scenes = gltf.scenes;
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Engine Node3Ds indexed by glTF node index — populated lazily
|
|
307
|
-
* by {@link process_node} as the scene-graph recursion visits
|
|
308
|
-
* each node. Used afterwards to resolve skin joint references
|
|
309
|
-
* and animation channel targets (both of which point at glTF
|
|
310
|
-
* node indices, not at any in-memory object).
|
|
311
|
-
*
|
|
312
|
-
* @type {Node3D[]}
|
|
313
|
-
*/
|
|
314
|
-
const record_index_of = new Int32Array((gltf.nodes ?? []).length).fill(-1);
|
|
315
|
-
|
|
316
|
-
// what the file becomes: nodes in pre-order, as the walk below produces them
|
|
317
|
-
const bundle = new SceneBundle();
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Queue of pending `skin.meshes` pushes. process_node() can't
|
|
321
|
-
* resolve `node.skin → Skin` directly because skins[] is built
|
|
322
|
-
* *after* the recursion completes (the joint lookup needs
|
|
323
|
-
* node3d_by_index fully populated). So we enqueue the produced
|
|
324
|
-
* SkinnedMeshes here and drain into the matching skin once the
|
|
325
|
-
* walk finishes.
|
|
326
|
-
*
|
|
327
|
-
* @type {{ skin_index: number, meshes: Mesh[] }[]}
|
|
328
|
-
*/
|
|
329
|
-
const skin_mesh_pushes = [];
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Primitive converted to JSON string serves as key
|
|
333
|
-
* @type {Map<string, MeshletGeometry>}
|
|
334
|
-
*/
|
|
335
|
-
const geometry_cache = new Map();
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
*
|
|
339
|
-
* @type {HashSet<MeshletGeometry>}
|
|
340
|
-
*/
|
|
341
|
-
const geometry_cache_secondary = new HashSet();
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Indexed as `gltf.images` is, and populated only where an image was actually fetched.
|
|
345
|
-
*
|
|
346
|
-
* Built from {@link gltf_used_image_indices} rather than by mapping the whole list, because
|
|
347
|
-
* that is the same function the fetch is filtered by: the two cannot drift into decoding one
|
|
348
|
-
* set and sampling another, and there is no slot here for an image that was never brought in.
|
|
349
|
-
*
|
|
350
|
-
* @type {ShadeImage[]}
|
|
351
|
-
*/
|
|
352
|
-
const mapped_images = [];
|
|
353
|
-
|
|
354
|
-
for (const index of gltf_used_image_indices(gltf)) {
|
|
355
|
-
const source = gltf.images[index];
|
|
356
|
-
|
|
357
|
-
/*
|
|
358
|
-
A `KHR_texture_basisu` image arrives already transcoded — the container reader ran in
|
|
359
|
-
the parser, where the bytes were, and what came back is texels in a device format. Every
|
|
360
|
-
other image is an `ImageBitmap` the browser decoded. Below this line the two are the same
|
|
361
|
-
thing: a `ShadeImage`, which is what §3.1 bought.
|
|
362
|
-
*/
|
|
363
|
-
mapped_images[index] = source.isShadeTexelData === true
|
|
364
|
-
? ShadeImage.fromTexelData(source)
|
|
365
|
-
: ShadeImage.fromImageBitmap(source);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* used when no material is specified
|
|
370
|
-
* @type {StandardShadeMaterial}
|
|
371
|
-
*/
|
|
372
|
-
const default_material = new StandardShadeMaterial();
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
*
|
|
376
|
-
* @type {ShadeTexture[]}
|
|
377
|
-
*/
|
|
378
|
-
const mapped_textures = (gltf.textures ?? []).map(gltf_texture => gltf_create_texture(
|
|
379
|
-
gltf_texture,
|
|
380
|
-
/*
|
|
381
|
-
The same function the fetch was filtered by. Asking it twice is the point: this is the
|
|
382
|
-
half that decides what to *sample*, and the half in `gltf_used_image_indices` decides
|
|
383
|
-
what to *fetch*. When those two disagreed, every model in the game's level failed to
|
|
384
|
-
load — see the note at the top of that file.
|
|
385
|
-
*/
|
|
386
|
-
mapped_images[gltf_texture_image_index(gltf_texture)],
|
|
387
|
-
gltf.samplers
|
|
388
|
-
));
|
|
389
|
-
|
|
390
|
-
coalesce_array_duplicates(mapped_textures);
|
|
391
|
-
|
|
392
|
-
// account for materials missing
|
|
393
|
-
const gltf_materials = gltf.materials ?? [];
|
|
394
|
-
|
|
395
|
-
const mapped_materials = gltf_materials.map(gltf_mat => gltf_create_material(gltf_mat, mapped_textures));
|
|
396
|
-
|
|
397
|
-
// dedupe materials
|
|
398
|
-
coalesce_array_duplicates(mapped_materials);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* One accessor as an attribute in the shape the renderer stores it.
|
|
403
|
-
*
|
|
404
|
-
* The read itself — stride, sparse overlay, a view-less accessor's zeroes — belongs to the
|
|
405
|
-
* format and is done by {@link gltf_read_accessor}; what is left here is the engine's own
|
|
406
|
-
* question of which storage type an attribute of this name is kept in.
|
|
407
|
-
*
|
|
408
|
-
* @param {number} accessor_index
|
|
409
|
-
* @param {string} name
|
|
410
|
-
* @returns {Attribute}
|
|
411
|
-
*/
|
|
412
|
-
function buffer_to_attribute(accessor_index, name) {
|
|
413
|
-
const data = gltf_read_accessor(gltf, accessor_index);
|
|
414
|
-
|
|
415
|
-
const raw_attribute = Attribute.from(data.values, data.components, name);
|
|
416
|
-
|
|
417
|
-
raw_attribute.spec.normalized = data.normalized;
|
|
418
|
-
|
|
419
|
-
return normalize_attribute(raw_attribute);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
*
|
|
424
|
-
* @type {HashMap<Object, Attribute>}
|
|
425
|
-
*/
|
|
426
|
-
const attribute_cache = new HashMap({
|
|
427
|
-
keyEqualityFunction: objectDeepEquals,
|
|
428
|
-
keyHashFunction: computeObjectHash
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
*
|
|
433
|
-
* @param {number} attribute_accessor
|
|
434
|
-
* @param {string} name
|
|
435
|
-
* @returns {Attribute}
|
|
436
|
-
*/
|
|
437
|
-
function obtain_attribute(attribute_accessor, name) {
|
|
438
|
-
const key = {
|
|
439
|
-
attribute_accessor, name
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
let attribute = attribute_cache.get(key);
|
|
443
|
-
|
|
444
|
-
if (attribute === undefined) {
|
|
445
|
-
attribute = buffer_to_attribute(attribute_accessor, name);
|
|
446
|
-
attribute_cache.set(key, attribute);
|
|
447
|
-
} else {
|
|
448
|
-
// console.warn('attribute reuse')
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return attribute;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* A FLOAT-componentType accessor's values as a flat `Float32Array`. Used for animation
|
|
456
|
-
* sampler inputs/outputs (times, values) and the skin inverse-bind matrices, where
|
|
457
|
-
* downstream code wants a stride-free flat buffer.
|
|
458
|
-
*
|
|
459
|
-
* @param {number} accessor_index
|
|
460
|
-
* @returns {Float32Array}
|
|
461
|
-
*/
|
|
462
|
-
function read_accessor_as_float32_array(accessor_index) {
|
|
463
|
-
const accessor = accessors[accessor_index];
|
|
464
|
-
|
|
465
|
-
assert.equal(
|
|
466
|
-
accessor.componentType, GLTF_COMPONENT_TYPE.FLOAT,
|
|
467
|
-
`accessor[${accessor_index}].componentType must be FLOAT for read_accessor_as_float32_array`
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
return gltf_read_accessor(gltf, accessor_index).values;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
*
|
|
475
|
-
* @returns {MeshletGeometry}
|
|
476
|
-
*/
|
|
477
|
-
function primitive_to_geometry(primitive) {
|
|
478
|
-
const geometry = new Geometry();
|
|
479
|
-
|
|
480
|
-
const mode = primitive.mode ?? GLTF_PRIMITIVE_MODE_DEFAULT;
|
|
481
|
-
|
|
482
|
-
if (mode !== GLTF_PRIMITIVE_MODE.TRIANGLES) {
|
|
483
|
-
throw new Error('Unsupported draw method');
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
for (const attribute_key in primitive.attributes) {
|
|
487
|
-
|
|
488
|
-
const name = gltf_attribute_id_to_standard(attribute_key);
|
|
489
|
-
|
|
490
|
-
if (geometry.getAttribute(name) !== undefined) {
|
|
491
|
-
console.warn(`attribute '${name}' already exists, skipping. Primitive.attributes = ${JSON.stringify(primitive.attributes)}`)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* @type {number}
|
|
496
|
-
*/
|
|
497
|
-
const attribute_accessor = primitive.attributes[attribute_key];
|
|
498
|
-
|
|
499
|
-
let attribute;
|
|
500
|
-
try {
|
|
501
|
-
attribute = obtain_attribute(attribute_accessor, name);
|
|
502
|
-
} catch (e) {
|
|
503
|
-
console.warn(`Failed to parse attribute '${name}'. Skipping`, e);
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
geometry.addAttribute(attribute);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (attribute_key === "POSITION") {
|
|
511
|
-
// The spec mandates POSITION accessors carry min/max,
|
|
512
|
-
// so we can adopt them as the bounding box without
|
|
513
|
-
// walking the vertex data. The sphere is derived from
|
|
514
|
-
// the box inside meshlet_geometry_build_from_geometry,
|
|
515
|
-
// so we just clear the dirty flag here.
|
|
516
|
-
const accessor = accessors[attribute_accessor];
|
|
517
|
-
|
|
518
|
-
if (accessor.min !== undefined && accessor.max !== undefined) {
|
|
519
|
-
geometry.bounding_box.set([
|
|
520
|
-
...accessor.min,
|
|
521
|
-
...accessor.max
|
|
522
|
-
]);
|
|
523
|
-
|
|
524
|
-
geometry.clearFlag(GeometryFlags.BoundsNeedUpdate);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (primitive.indices !== undefined) {
|
|
530
|
-
|
|
531
|
-
geometry.index = obtain_attribute(primitive.indices, 'index');
|
|
532
|
-
|
|
533
|
-
// geometry_fix_index(geometry.index, geometry.getVertexCount());
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// vertex optimize
|
|
538
|
-
if (geometry.index !== null && OPTIMIZE_GEOMETRIES) {
|
|
539
|
-
meshopt_optimizeVertexCache(geometry.index.data, geometry.index.data, geometry.index.count, geometry.getVertexCount());
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return meshlet_geometry_build_from_geometry(geometry);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function obtain_geometry_by_primitive(primitive) {
|
|
546
|
-
const key = JSON.stringify(primitive);
|
|
547
|
-
|
|
548
|
-
let geometry = geometry_cache.get(key);
|
|
549
|
-
|
|
550
|
-
if (geometry === undefined) {
|
|
551
|
-
geometry = primitive_to_geometry(primitive)
|
|
552
|
-
|
|
553
|
-
const secondary_cached = geometry_cache_secondary.get(geometry);
|
|
554
|
-
|
|
555
|
-
if (secondary_cached !== undefined) {
|
|
556
|
-
// found an equivalent geometry in the secondary cache, use it instead
|
|
557
|
-
geometry = secondary_cached;
|
|
558
|
-
// console.warn("secondary reuse");
|
|
559
|
-
} else {
|
|
560
|
-
geometry_cache_secondary.add(geometry);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
geometry_cache.set(key, geometry);
|
|
564
|
-
} else {
|
|
565
|
-
// console.warn('geometry reuse');
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return geometry;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Build the renderable mesh(es) for one glTF node.
|
|
574
|
-
*
|
|
575
|
-
* A glTF mesh is a list of primitives sharing a name and an
|
|
576
|
-
* `aabb`, and each primitive becomes one engine-side {@link Mesh}.
|
|
577
|
-
* When the owning glTF node references a skin, every primitive is
|
|
578
|
-
* instead constructed as a {@link SkinnedMesh} so the renderer
|
|
579
|
-
* routes it through a per-instance geometry clone for deformation.
|
|
580
|
-
*
|
|
581
|
-
* @param {number} mesh_index
|
|
582
|
-
* @param {{}} node
|
|
583
|
-
* @param {boolean} is_skinned
|
|
584
|
-
* @returns {Mesh[]}
|
|
585
|
-
*/
|
|
586
|
-
function process_mesh(mesh_index) {
|
|
587
|
-
|
|
588
|
-
const mesh = meshes[mesh_index];
|
|
589
|
-
|
|
590
|
-
const primitives = mesh.primitives;
|
|
591
|
-
|
|
592
|
-
const primitive_count = primitives.length;
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* @type {{geometry: MeshletGeometry, material: StandardShadeMaterial, name: string}[]}
|
|
596
|
-
*/
|
|
597
|
-
const result = [];
|
|
598
|
-
|
|
599
|
-
for (let i = 0; i < primitive_count; i++) {
|
|
600
|
-
const primitive = primitives[i];
|
|
601
|
-
|
|
602
|
-
// build geometry
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* @type {MeshletGeometry}
|
|
606
|
-
*/
|
|
607
|
-
let geometry;
|
|
608
|
-
|
|
609
|
-
try {
|
|
610
|
-
geometry = obtain_geometry_by_primitive(primitive);
|
|
611
|
-
} catch (e) {
|
|
612
|
-
console.error(`Failed to obtain geometry for primitive [${i}] ${primitive}. `, e);
|
|
613
|
-
console.log('Ignoring mesh');
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (primitive_count === 1 && mesh.name !== undefined) {
|
|
618
|
-
geometry.name = mesh.name;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const material_id = primitive.material;
|
|
622
|
-
|
|
623
|
-
let material;
|
|
624
|
-
|
|
625
|
-
if (material_id !== undefined) {
|
|
626
|
-
material = mapped_materials[material_id];
|
|
627
|
-
|
|
628
|
-
if (material === undefined) {
|
|
629
|
-
console.warn(`specified material ${material_id} does not exist. Using default.`);
|
|
630
|
-
|
|
631
|
-
material = default_material;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
} else {
|
|
635
|
-
// no material specified, use default
|
|
636
|
-
material = default_material;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
result.push({ geometry, material, name: mesh.name ?? "" });
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return result;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
*
|
|
647
|
-
* @param {number} light_index
|
|
648
|
-
* @param {{}} node
|
|
649
|
-
* @returns {AbstractLight}
|
|
650
|
-
*/
|
|
651
|
-
function process_light(light_index, node) {
|
|
652
|
-
|
|
653
|
-
// SPEC: https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual/README.md
|
|
654
|
-
|
|
655
|
-
const gltf_light = gltf.extensions.KHR_lights_punctual.lights[light_index];
|
|
656
|
-
|
|
657
|
-
// parse color
|
|
658
|
-
const color = new Color();
|
|
659
|
-
|
|
660
|
-
if (gltf_light.color !== undefined) {
|
|
661
|
-
color.fromArray(gltf_light.color);
|
|
662
|
-
} else {
|
|
663
|
-
color.setRGB(1, 1, 1);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const intensity = gltf_light.intensity ?? 1;
|
|
667
|
-
|
|
668
|
-
const name = gltf_light.name ?? "";
|
|
669
|
-
|
|
670
|
-
let light;
|
|
671
|
-
|
|
672
|
-
const type = gltf_light.type;
|
|
673
|
-
|
|
674
|
-
switch (type) {
|
|
675
|
-
case "directional":
|
|
676
|
-
light = new DirectionalLight();
|
|
677
|
-
break;
|
|
678
|
-
case "point":
|
|
679
|
-
light = new PointLight();
|
|
680
|
-
light.distance = gltf_light.range ?? 1e20; // default light radius
|
|
681
|
-
break;
|
|
682
|
-
case "spot":
|
|
683
|
-
light = new SpotLight();
|
|
684
|
-
|
|
685
|
-
const gltf_spot = gltf_light.spot;
|
|
686
|
-
|
|
687
|
-
const outerCone = gltf_spot?.outerConeAngle ?? (Math.PI / 4); // default PI/4
|
|
688
|
-
const innerCone = gltf_spot?.innerConeAngle ?? 0;
|
|
689
|
-
|
|
690
|
-
light.angle = clamp(
|
|
691
|
-
outerCone,
|
|
692
|
-
0,
|
|
693
|
-
Math.PI / 2
|
|
694
|
-
);
|
|
695
|
-
|
|
696
|
-
light.penumbra = clamp01(
|
|
697
|
-
inverseLerp(light.angle, 0, innerCone)
|
|
698
|
-
);
|
|
699
|
-
|
|
700
|
-
light.distance = gltf_light.range ?? 1e20;
|
|
701
|
-
// TODO rotation is messed up?
|
|
702
|
-
break;
|
|
703
|
-
default:
|
|
704
|
-
throw new Error(`Unsupported light type ${type}`);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
light.color.copy(color);
|
|
708
|
-
light.intensity = intensity;
|
|
709
|
-
light.name = name;
|
|
710
|
-
|
|
711
|
-
// check for light radius
|
|
712
|
-
light.radius = gltf_light.extras?.radius ?? 0;
|
|
713
|
-
|
|
714
|
-
light.near_clip_distance = gltf_light.extras?.near_clip_distance ?? light.radius;
|
|
715
|
-
|
|
716
|
-
light.casts_shadow = true;
|
|
717
|
-
|
|
718
|
-
return light;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
// one
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
.
|
|
1095
|
-
|
|
1096
|
-
;
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
* @
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1
|
+
import { assert } from "../../../../core/assert.js";
|
|
2
|
+
import { HashMap } from "../../../../core/collection/map/HashMap.js";
|
|
3
|
+
import { HashSet } from "../../../../core/collection/set/HashSet.js";
|
|
4
|
+
import { Color } from "../../../../core/color/Color.js";
|
|
5
|
+
import { Quaternion } from "../../../../core/geom/Quaternion.js";
|
|
6
|
+
import Vector3 from "../../../../core/geom/Vector3.js";
|
|
7
|
+
import { clamp } from "../../../../core/math/clamp.js";
|
|
8
|
+
import { clamp01 } from "../../../../core/math/clamp01.js";
|
|
9
|
+
import { computeObjectHash } from "../../../../core/math/hash/computeObjectHash.js";
|
|
10
|
+
import { inverseLerp } from "../../../../core/math/inverseLerp.js";
|
|
11
|
+
import { objectDeepEquals } from "../../../../core/model/object/objectDeepEquals.js";
|
|
12
|
+
import { AnimationInterpolationKind } from "../../../../engine/animation/clip/AnimationInterpolationKind.js";
|
|
13
|
+
import { curve_from_track_data } from "../../../../engine/animation/clip/curve_from_track_data.js";
|
|
14
|
+
import { AnimationCurve } from "../../../../engine/animation/curve/AnimationCurve.js";
|
|
15
|
+
import { t64_look_rotation } from "../../../../engine/ecs/transform/t64_look_rotation.js";
|
|
16
|
+
import { t64_transform_direction } from "../../../../engine/ecs/transform/t64_transform_direction.js";
|
|
17
|
+
import { meshopt_optimizeVertexCache } from "../../../../engine/graphics/geometry/optimization/VertexCacheOptimizer.js";
|
|
18
|
+
import { ShadeAnimationChannel } from "../../animation/ShadeAnimationChannel.js";
|
|
19
|
+
import { ShadeAnimationClip } from "../../animation/ShadeAnimationClip.js";
|
|
20
|
+
import { Skin } from "../../animation/Skin.js";
|
|
21
|
+
import { Attribute } from "../../geometry/Attribute.js";
|
|
22
|
+
import { Geometry, GeometryFlags } from "../../geometry/Geometry.js";
|
|
23
|
+
import { meshlet_geometry_build_from_geometry } from "../../geometry/meshlet_geometry_build_from_geometry.js";
|
|
24
|
+
import { StandardAttributes } from "../../geometry/StandardAttributes.js";
|
|
25
|
+
import { normalize_attribute } from "../../geometry/util/normalize_attribute.js";
|
|
26
|
+
import { DirectionalLight } from "../../light/model/DirectionalLight.js";
|
|
27
|
+
import { PointLight } from "../../light/model/PointLight.js";
|
|
28
|
+
import { SpotLight } from "../../light/model/SpotLight.js";
|
|
29
|
+
|
|
30
|
+
import { StandardShadeMaterial } from "../../material/StandardShadeMaterial.js";
|
|
31
|
+
import { Node3DProperty } from "../../object_property/Node3DProperty.js";
|
|
32
|
+
import { ShadeTexture } from "../../texture/ShadeTexture.js";
|
|
33
|
+
import { ShadeImage } from "../../texture/source/ShadeImage.js";
|
|
34
|
+
import { SceneBundle } from "../SceneBundle.js";
|
|
35
|
+
import { SceneNode } from "../SceneNode.js";
|
|
36
|
+
import { coalesce_array_duplicates } from "../../../../format/scene/gltf/coalesce_array_duplicates.js";
|
|
37
|
+
import { GLTF_COMPONENT_TYPE } from "../../../../format/scene/gltf/gltf_component_type.js";
|
|
38
|
+
import { gltf_create_material } from "./gltf_create_material.js";
|
|
39
|
+
import { gltf_create_texture } from "./gltf_create_texture.js";
|
|
40
|
+
import { GLTF_PRIMITIVE_MODE, GLTF_PRIMITIVE_MODE_DEFAULT } from "../../../../format/scene/gltf/GLTF_PRIMITIVE_MODE.js";
|
|
41
|
+
import { gltf_read_accessor } from "../../../../format/scene/gltf/gltf_read_accessor.js";
|
|
42
|
+
import { gltf_texture_image_index, gltf_used_image_indices } from "../../../../format/scene/gltf/gltf_used_image_indices.js";
|
|
43
|
+
import { TinyGltf } from "./tiny-gltf.js";
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Somewhere to hand `canonicalize_quaternion_signs` a rest rotation it can read by component name —
|
|
48
|
+
* a `Transform64` addresses its rotation by index, and the function's contract is named components.
|
|
49
|
+
*
|
|
50
|
+
* @type {Quaternion}
|
|
51
|
+
*/
|
|
52
|
+
const SCRATCH_REST_ROTATION = new Quaternion();
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Somewhere to read a light's facing before flipping it.
|
|
56
|
+
*
|
|
57
|
+
* @type {Vector3}
|
|
58
|
+
*/
|
|
59
|
+
const SCRATCH_DIRECTION = new Vector3();
|
|
60
|
+
const OPTIMIZE_GEOMETRIES = false;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @see https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html
|
|
64
|
+
* @param {string} id
|
|
65
|
+
*/
|
|
66
|
+
function gltf_attribute_id_to_standard(id) {
|
|
67
|
+
switch (id) {
|
|
68
|
+
case "POSITION":
|
|
69
|
+
return StandardAttributes.Position;
|
|
70
|
+
case "NORMAL":
|
|
71
|
+
return StandardAttributes.Normal;
|
|
72
|
+
case "TEXCOORD_0":
|
|
73
|
+
return StandardAttributes.TextureCoordinates0;
|
|
74
|
+
case "TEXCOORD_1":
|
|
75
|
+
return StandardAttributes.TextureCoordinates1;
|
|
76
|
+
case "TANGENT":
|
|
77
|
+
return StandardAttributes.Tangent;
|
|
78
|
+
case "COLOR_0":
|
|
79
|
+
return StandardAttributes.Color;
|
|
80
|
+
case "JOINTS_0":
|
|
81
|
+
return StandardAttributes.SkinningJoints;
|
|
82
|
+
case "WEIGHTS_0":
|
|
83
|
+
return StandardAttributes.SkinningWeights;
|
|
84
|
+
default:
|
|
85
|
+
return id;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Map glTF `animation.channels[].target.path` to {@link Node3DProperty}.
|
|
91
|
+
* Returns `-1` for paths we don't yet drive (morph-target `weights`),
|
|
92
|
+
* so the caller can skip those channels without throwing.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} path
|
|
95
|
+
* @returns {number}
|
|
96
|
+
*/
|
|
97
|
+
function gltf_animation_path_to_property(path) {
|
|
98
|
+
switch (path) {
|
|
99
|
+
case "translation":
|
|
100
|
+
return Node3DProperty.Translation;
|
|
101
|
+
case "rotation":
|
|
102
|
+
return Node3DProperty.Rotation;
|
|
103
|
+
case "scale":
|
|
104
|
+
return Node3DProperty.Scale;
|
|
105
|
+
default:
|
|
106
|
+
// "weights" (morph targets) lands here; we don't yet have
|
|
107
|
+
// morph-target support in the renderer.
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Number of scalar components per keyframe value for a given Node3D
|
|
114
|
+
* property — translation/scale = 3 (vec3), rotation = 4 (quaternion
|
|
115
|
+
* x/y/z/w).
|
|
116
|
+
*
|
|
117
|
+
* @param {number} property one of {@link Node3DProperty}
|
|
118
|
+
* @returns {number}
|
|
119
|
+
*/
|
|
120
|
+
function node3d_property_component_count(property) {
|
|
121
|
+
switch (property) {
|
|
122
|
+
case Node3DProperty.Translation:
|
|
123
|
+
case Node3DProperty.Scale:
|
|
124
|
+
return 3;
|
|
125
|
+
case Node3DProperty.Rotation:
|
|
126
|
+
return 4;
|
|
127
|
+
default:
|
|
128
|
+
throw new Error(`Unsupported animated property ${property}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Walk a flat keyframe-values buffer for a rotation channel and flip
|
|
134
|
+
* the sign of any keyframe whose val-quaternion has negative dot
|
|
135
|
+
* product with the previous keyframe's val-quaternion. The same flip
|
|
136
|
+
* is applied to that keyframe's in/out tangents in CUBICSPLINE mode
|
|
137
|
+
* so the cubic remains internally consistent.
|
|
138
|
+
*
|
|
139
|
+
* The flip preserves the rotation (q and -q encode the same
|
|
140
|
+
* orientation) while ensuring per-component interpolation between
|
|
141
|
+
* consecutive keyframes follows the short arc instead of passing
|
|
142
|
+
* through a near-zero quaternion mid-segment.
|
|
143
|
+
*
|
|
144
|
+
* The curve's leading sign is anchored against `rest_rotation` —
|
|
145
|
+
* the target node's authored local rest rotation: when the first
|
|
146
|
+
* keyframe's val-quat has negative dot product with the rest
|
|
147
|
+
* quaternion, the whole first keyframe block is negated (and the
|
|
148
|
+
* consecutive-keyframe pass propagates that sign down the curve).
|
|
149
|
+
* Every rotation curve targeting one node aligns against the same
|
|
150
|
+
* reference, so weighted multi-clip blends accumulate
|
|
151
|
+
* hemisphere-consistent contributions instead of cancelling toward
|
|
152
|
+
* a near-zero quaternion. Residual limitation: the anchor is the
|
|
153
|
+
* FIRST keyframe only — a curve that swings more than 90° away
|
|
154
|
+
* from the rest rotation mid-clip can still contribute opposite
|
|
155
|
+
* signs to another clip at those times; fixing that would need
|
|
156
|
+
* apply-time alignment, which the atomic pose accumulator cannot
|
|
157
|
+
* do order-independently.
|
|
158
|
+
*
|
|
159
|
+
* Mutates `values` in place. Safe to call on an output array that's
|
|
160
|
+
* a fresh copy of the source accessor (which is what
|
|
161
|
+
* {@link read_accessor_as_float32_array} produces).
|
|
162
|
+
*
|
|
163
|
+
* Exported for spec coverage; not part of the loader's public API.
|
|
164
|
+
*
|
|
165
|
+
* @param {Float32Array} values flat values: STEP/LINEAR uses 4
|
|
166
|
+
* floats/keyframe (x, y, z, w); CUBICSPLINE uses 12 (inX, inY,
|
|
167
|
+
* inZ, inW, valX, valY, valZ, valW, outX, outY, outZ, outW).
|
|
168
|
+
* @param {number} key_count
|
|
169
|
+
* @param {number} interpolation one of {@link AnimationInterpolationKind}
|
|
170
|
+
* @param {{x:number, y:number, z:number, w:number}} [rest_rotation]
|
|
171
|
+
* target node's authored local rest rotation; omitted → the
|
|
172
|
+
* leading sign is kept as authored.
|
|
173
|
+
*/
|
|
174
|
+
export function canonicalize_quaternion_signs(values, key_count, interpolation, rest_rotation) {
|
|
175
|
+
const is_cubic = interpolation === AnimationInterpolationKind.CubicTangents;
|
|
176
|
+
const stride = is_cubic ? 12 : 4;
|
|
177
|
+
// Offset into a keyframe block at which the val-quat sits.
|
|
178
|
+
// CUBICSPLINE has [in, val, out]; the val starts at offset 4 (i.e.
|
|
179
|
+
// after the 4-float in-tangent). STEP/LINEAR has just val at 0.
|
|
180
|
+
const val_offset = is_cubic ? 4 : 0;
|
|
181
|
+
|
|
182
|
+
if (key_count >= 1 && rest_rotation !== undefined) {
|
|
183
|
+
const rest_dot =
|
|
184
|
+
values[val_offset + 0] * rest_rotation.x +
|
|
185
|
+
values[val_offset + 1] * rest_rotation.y +
|
|
186
|
+
values[val_offset + 2] * rest_rotation.z +
|
|
187
|
+
values[val_offset + 3] * rest_rotation.w;
|
|
188
|
+
|
|
189
|
+
if (rest_dot < 0) {
|
|
190
|
+
// Negate the whole first keyframe block (value +
|
|
191
|
+
// tangents); the loop below carries the flip through
|
|
192
|
+
// the remaining keyframes.
|
|
193
|
+
for (let i = 0; i < stride; i++) {
|
|
194
|
+
values[i] = -values[i];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (let k = 1; k < key_count; k++) {
|
|
200
|
+
const prev_base = (k - 1) * stride + val_offset;
|
|
201
|
+
const cur_base = k * stride + val_offset;
|
|
202
|
+
|
|
203
|
+
const dot =
|
|
204
|
+
values[prev_base + 0] * values[cur_base + 0] +
|
|
205
|
+
values[prev_base + 1] * values[cur_base + 1] +
|
|
206
|
+
values[prev_base + 2] * values[cur_base + 2] +
|
|
207
|
+
values[prev_base + 3] * values[cur_base + 3];
|
|
208
|
+
|
|
209
|
+
if (dot < 0) {
|
|
210
|
+
// Flip the whole keyframe block (value + tangents).
|
|
211
|
+
const start = k * stride;
|
|
212
|
+
const end = start + stride;
|
|
213
|
+
for (let i = start; i < end; i++) {
|
|
214
|
+
values[i] = -values[i];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Map glTF interpolation strings to the engine's enum. Defaults to
|
|
222
|
+
* LINEAR when missing — the glTF spec's default.
|
|
223
|
+
*
|
|
224
|
+
* @param {string|undefined} interpolation
|
|
225
|
+
* @returns {number} one of {@link AnimationInterpolationKind}
|
|
226
|
+
*/
|
|
227
|
+
function gltf_interpolation_to_kind(interpolation) {
|
|
228
|
+
switch (interpolation) {
|
|
229
|
+
case "STEP":
|
|
230
|
+
return AnimationInterpolationKind.Discrete;
|
|
231
|
+
case "CUBICSPLINE":
|
|
232
|
+
return AnimationInterpolationKind.CubicTangents;
|
|
233
|
+
case "LINEAR":
|
|
234
|
+
case undefined:
|
|
235
|
+
return AnimationInterpolationKind.Linear;
|
|
236
|
+
default:
|
|
237
|
+
throw new Error(`Unsupported interpolation '${interpolation}'`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Input can be a .glb or .gltf file.
|
|
243
|
+
*
|
|
244
|
+
* Does not support most compression GLTF extensions such as draco, basis etc.
|
|
245
|
+
*
|
|
246
|
+
* Returns a {@link SceneBundle} that groups the parsed scene roots
|
|
247
|
+
* with any skins and animation clips extracted from the same asset.
|
|
248
|
+
* Callers add `bundle.scenes` to their scene, then pass the bundle's
|
|
249
|
+
* `skins` / `clips` to the scene's animation manager (e.g. via
|
|
250
|
+
* {@link GPUAnimationManager#register_skin} and
|
|
251
|
+
* {@link GPUAnimationManager#register_clip}) once the bundle has been
|
|
252
|
+
* compiled into a prefab and instantiated, so that the joints and targets
|
|
253
|
+
* are entities holding rows.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} url un-resolved — the asset manager resolves network paths
|
|
256
|
+
* @param {Object} options
|
|
257
|
+
* @param {AssetManager} options.assetManager where the container, its buffers and its images come
|
|
258
|
+
* from. Required: a glTF is several files, and which of them are already in memory is not this
|
|
259
|
+
* parser's business to know.
|
|
260
|
+
* @param {AssetRequestScope} [options.scope] the load these requests belong to
|
|
261
|
+
* @param {Map<string, File|Blob|ArrayBuffer>} [options.fileMap] Map of relative paths to file data.
|
|
262
|
+
* When provided, buffer and image URIs in the GLTF are resolved from this map before the asset
|
|
263
|
+
* framework is asked. Enables multi-file GLTF loading from drag-and-drop.
|
|
264
|
+
* @param {Set<GPUTextureFormat>} [options.texture_support] what the device can sample, from
|
|
265
|
+
* `gpu_texture_compression_support(device)`. Only a `KHR_texture_basisu` image reads it, to choose
|
|
266
|
+
* what to transcode into; omitted, the loader targets the best format the transcoder writes.
|
|
267
|
+
* @return {Promise<SceneBundle>}
|
|
268
|
+
*/
|
|
269
|
+
export function load_gltf(url, { assetManager, scope, fileMap, texture_support } = {}) {
|
|
270
|
+
// TODO
|
|
271
|
+
// apply some checks to the model, things like non-uniform scales, that can be fixed potentially
|
|
272
|
+
// see https://www.youtube.com/watch?v=FJRO5wFTEc8 for common ideas
|
|
273
|
+
// check for inverted normals
|
|
274
|
+
// check for duplicated vertices
|
|
275
|
+
// check for overlapping faces
|
|
276
|
+
|
|
277
|
+
async function process(gltf) {
|
|
278
|
+
// console.log(gltf);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @type {{type:string, componentType:number, count:number, bufferView:number, byteOffset:number, normalized:boolean, min?:number[], max?:number}[]}
|
|
282
|
+
*/
|
|
283
|
+
const accessors = gltf.accessors;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* `aabb` is six numbers, `[x0, y0, z0, x1, y1, z1]`, in world space — and is absent on a node
|
|
287
|
+
* that bounds nothing, meaning neither it nor anything below it declared any.
|
|
288
|
+
*
|
|
289
|
+
* {{aabb?:Float64Array, mesh:number, worldMatrix:Float32Array}[]}
|
|
290
|
+
*/
|
|
291
|
+
const nodes = gltf.nodes;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
*
|
|
295
|
+
* @type {{aabb:Float64Array, primitives:({attributes:Object<number>,indices:number, material:number, mode:number}[])}[]}
|
|
296
|
+
*/
|
|
297
|
+
const meshes = gltf.meshes;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
*
|
|
301
|
+
* @type {{}[]}
|
|
302
|
+
*/
|
|
303
|
+
const scenes = gltf.scenes;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Engine Node3Ds indexed by glTF node index — populated lazily
|
|
307
|
+
* by {@link process_node} as the scene-graph recursion visits
|
|
308
|
+
* each node. Used afterwards to resolve skin joint references
|
|
309
|
+
* and animation channel targets (both of which point at glTF
|
|
310
|
+
* node indices, not at any in-memory object).
|
|
311
|
+
*
|
|
312
|
+
* @type {Node3D[]}
|
|
313
|
+
*/
|
|
314
|
+
const record_index_of = new Int32Array((gltf.nodes ?? []).length).fill(-1);
|
|
315
|
+
|
|
316
|
+
// what the file becomes: nodes in pre-order, as the walk below produces them
|
|
317
|
+
const bundle = new SceneBundle();
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Queue of pending `skin.meshes` pushes. process_node() can't
|
|
321
|
+
* resolve `node.skin → Skin` directly because skins[] is built
|
|
322
|
+
* *after* the recursion completes (the joint lookup needs
|
|
323
|
+
* node3d_by_index fully populated). So we enqueue the produced
|
|
324
|
+
* SkinnedMeshes here and drain into the matching skin once the
|
|
325
|
+
* walk finishes.
|
|
326
|
+
*
|
|
327
|
+
* @type {{ skin_index: number, meshes: Mesh[] }[]}
|
|
328
|
+
*/
|
|
329
|
+
const skin_mesh_pushes = [];
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Primitive converted to JSON string serves as key
|
|
333
|
+
* @type {Map<string, MeshletGeometry>}
|
|
334
|
+
*/
|
|
335
|
+
const geometry_cache = new Map();
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
*
|
|
339
|
+
* @type {HashSet<MeshletGeometry>}
|
|
340
|
+
*/
|
|
341
|
+
const geometry_cache_secondary = new HashSet();
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Indexed as `gltf.images` is, and populated only where an image was actually fetched.
|
|
345
|
+
*
|
|
346
|
+
* Built from {@link gltf_used_image_indices} rather than by mapping the whole list, because
|
|
347
|
+
* that is the same function the fetch is filtered by: the two cannot drift into decoding one
|
|
348
|
+
* set and sampling another, and there is no slot here for an image that was never brought in.
|
|
349
|
+
*
|
|
350
|
+
* @type {ShadeImage[]}
|
|
351
|
+
*/
|
|
352
|
+
const mapped_images = [];
|
|
353
|
+
|
|
354
|
+
for (const index of gltf_used_image_indices(gltf)) {
|
|
355
|
+
const source = gltf.images[index];
|
|
356
|
+
|
|
357
|
+
/*
|
|
358
|
+
A `KHR_texture_basisu` image arrives already transcoded — the container reader ran in
|
|
359
|
+
the parser, where the bytes were, and what came back is texels in a device format. Every
|
|
360
|
+
other image is an `ImageBitmap` the browser decoded. Below this line the two are the same
|
|
361
|
+
thing: a `ShadeImage`, which is what §3.1 bought.
|
|
362
|
+
*/
|
|
363
|
+
mapped_images[index] = source.isShadeTexelData === true
|
|
364
|
+
? ShadeImage.fromTexelData(source)
|
|
365
|
+
: ShadeImage.fromImageBitmap(source);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* used when no material is specified
|
|
370
|
+
* @type {StandardShadeMaterial}
|
|
371
|
+
*/
|
|
372
|
+
const default_material = new StandardShadeMaterial();
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
*
|
|
376
|
+
* @type {ShadeTexture[]}
|
|
377
|
+
*/
|
|
378
|
+
const mapped_textures = (gltf.textures ?? []).map(gltf_texture => gltf_create_texture(
|
|
379
|
+
gltf_texture,
|
|
380
|
+
/*
|
|
381
|
+
The same function the fetch was filtered by. Asking it twice is the point: this is the
|
|
382
|
+
half that decides what to *sample*, and the half in `gltf_used_image_indices` decides
|
|
383
|
+
what to *fetch*. When those two disagreed, every model in the game's level failed to
|
|
384
|
+
load — see the note at the top of that file.
|
|
385
|
+
*/
|
|
386
|
+
mapped_images[gltf_texture_image_index(gltf_texture)],
|
|
387
|
+
gltf.samplers
|
|
388
|
+
));
|
|
389
|
+
|
|
390
|
+
coalesce_array_duplicates(mapped_textures);
|
|
391
|
+
|
|
392
|
+
// account for materials missing
|
|
393
|
+
const gltf_materials = gltf.materials ?? [];
|
|
394
|
+
|
|
395
|
+
const mapped_materials = gltf_materials.map(gltf_mat => gltf_create_material(gltf_mat, mapped_textures));
|
|
396
|
+
|
|
397
|
+
// dedupe materials
|
|
398
|
+
coalesce_array_duplicates(mapped_materials);
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* One accessor as an attribute in the shape the renderer stores it.
|
|
403
|
+
*
|
|
404
|
+
* The read itself — stride, sparse overlay, a view-less accessor's zeroes — belongs to the
|
|
405
|
+
* format and is done by {@link gltf_read_accessor}; what is left here is the engine's own
|
|
406
|
+
* question of which storage type an attribute of this name is kept in.
|
|
407
|
+
*
|
|
408
|
+
* @param {number} accessor_index
|
|
409
|
+
* @param {string} name
|
|
410
|
+
* @returns {Attribute}
|
|
411
|
+
*/
|
|
412
|
+
function buffer_to_attribute(accessor_index, name) {
|
|
413
|
+
const data = gltf_read_accessor(gltf, accessor_index);
|
|
414
|
+
|
|
415
|
+
const raw_attribute = Attribute.from(data.values, data.components, name);
|
|
416
|
+
|
|
417
|
+
raw_attribute.spec.normalized = data.normalized;
|
|
418
|
+
|
|
419
|
+
return normalize_attribute(raw_attribute);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
*
|
|
424
|
+
* @type {HashMap<Object, Attribute>}
|
|
425
|
+
*/
|
|
426
|
+
const attribute_cache = new HashMap({
|
|
427
|
+
keyEqualityFunction: objectDeepEquals,
|
|
428
|
+
keyHashFunction: computeObjectHash
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
*
|
|
433
|
+
* @param {number} attribute_accessor
|
|
434
|
+
* @param {string} name
|
|
435
|
+
* @returns {Attribute}
|
|
436
|
+
*/
|
|
437
|
+
function obtain_attribute(attribute_accessor, name) {
|
|
438
|
+
const key = {
|
|
439
|
+
attribute_accessor, name
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
let attribute = attribute_cache.get(key);
|
|
443
|
+
|
|
444
|
+
if (attribute === undefined) {
|
|
445
|
+
attribute = buffer_to_attribute(attribute_accessor, name);
|
|
446
|
+
attribute_cache.set(key, attribute);
|
|
447
|
+
} else {
|
|
448
|
+
// console.warn('attribute reuse')
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return attribute;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* A FLOAT-componentType accessor's values as a flat `Float32Array`. Used for animation
|
|
456
|
+
* sampler inputs/outputs (times, values) and the skin inverse-bind matrices, where
|
|
457
|
+
* downstream code wants a stride-free flat buffer.
|
|
458
|
+
*
|
|
459
|
+
* @param {number} accessor_index
|
|
460
|
+
* @returns {Float32Array}
|
|
461
|
+
*/
|
|
462
|
+
function read_accessor_as_float32_array(accessor_index) {
|
|
463
|
+
const accessor = accessors[accessor_index];
|
|
464
|
+
|
|
465
|
+
assert.equal(
|
|
466
|
+
accessor.componentType, GLTF_COMPONENT_TYPE.FLOAT,
|
|
467
|
+
`accessor[${accessor_index}].componentType must be FLOAT for read_accessor_as_float32_array`
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
return gltf_read_accessor(gltf, accessor_index).values;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
*
|
|
475
|
+
* @returns {MeshletGeometry}
|
|
476
|
+
*/
|
|
477
|
+
function primitive_to_geometry(primitive) {
|
|
478
|
+
const geometry = new Geometry();
|
|
479
|
+
|
|
480
|
+
const mode = primitive.mode ?? GLTF_PRIMITIVE_MODE_DEFAULT;
|
|
481
|
+
|
|
482
|
+
if (mode !== GLTF_PRIMITIVE_MODE.TRIANGLES) {
|
|
483
|
+
throw new Error('Unsupported draw method');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
for (const attribute_key in primitive.attributes) {
|
|
487
|
+
|
|
488
|
+
const name = gltf_attribute_id_to_standard(attribute_key);
|
|
489
|
+
|
|
490
|
+
if (geometry.getAttribute(name) !== undefined) {
|
|
491
|
+
console.warn(`attribute '${name}' already exists, skipping. Primitive.attributes = ${JSON.stringify(primitive.attributes)}`)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* @type {number}
|
|
496
|
+
*/
|
|
497
|
+
const attribute_accessor = primitive.attributes[attribute_key];
|
|
498
|
+
|
|
499
|
+
let attribute;
|
|
500
|
+
try {
|
|
501
|
+
attribute = obtain_attribute(attribute_accessor, name);
|
|
502
|
+
} catch (e) {
|
|
503
|
+
console.warn(`Failed to parse attribute '${name}'. Skipping`, e);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
geometry.addAttribute(attribute);
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
if (attribute_key === "POSITION") {
|
|
511
|
+
// The spec mandates POSITION accessors carry min/max,
|
|
512
|
+
// so we can adopt them as the bounding box without
|
|
513
|
+
// walking the vertex data. The sphere is derived from
|
|
514
|
+
// the box inside meshlet_geometry_build_from_geometry,
|
|
515
|
+
// so we just clear the dirty flag here.
|
|
516
|
+
const accessor = accessors[attribute_accessor];
|
|
517
|
+
|
|
518
|
+
if (accessor.min !== undefined && accessor.max !== undefined) {
|
|
519
|
+
geometry.bounding_box.set([
|
|
520
|
+
...accessor.min,
|
|
521
|
+
...accessor.max
|
|
522
|
+
]);
|
|
523
|
+
|
|
524
|
+
geometry.clearFlag(GeometryFlags.BoundsNeedUpdate);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (primitive.indices !== undefined) {
|
|
530
|
+
|
|
531
|
+
geometry.index = obtain_attribute(primitive.indices, 'index');
|
|
532
|
+
|
|
533
|
+
// geometry_fix_index(geometry.index, geometry.getVertexCount());
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
// vertex optimize
|
|
538
|
+
if (geometry.index !== null && OPTIMIZE_GEOMETRIES) {
|
|
539
|
+
meshopt_optimizeVertexCache(geometry.index.data, geometry.index.data, geometry.index.count, geometry.getVertexCount());
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return meshlet_geometry_build_from_geometry(geometry);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function obtain_geometry_by_primitive(primitive) {
|
|
546
|
+
const key = JSON.stringify(primitive);
|
|
547
|
+
|
|
548
|
+
let geometry = geometry_cache.get(key);
|
|
549
|
+
|
|
550
|
+
if (geometry === undefined) {
|
|
551
|
+
geometry = primitive_to_geometry(primitive)
|
|
552
|
+
|
|
553
|
+
const secondary_cached = geometry_cache_secondary.get(geometry);
|
|
554
|
+
|
|
555
|
+
if (secondary_cached !== undefined) {
|
|
556
|
+
// found an equivalent geometry in the secondary cache, use it instead
|
|
557
|
+
geometry = secondary_cached;
|
|
558
|
+
// console.warn("secondary reuse");
|
|
559
|
+
} else {
|
|
560
|
+
geometry_cache_secondary.add(geometry);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
geometry_cache.set(key, geometry);
|
|
564
|
+
} else {
|
|
565
|
+
// console.warn('geometry reuse');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return geometry;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Build the renderable mesh(es) for one glTF node.
|
|
574
|
+
*
|
|
575
|
+
* A glTF mesh is a list of primitives sharing a name and an
|
|
576
|
+
* `aabb`, and each primitive becomes one engine-side {@link Mesh}.
|
|
577
|
+
* When the owning glTF node references a skin, every primitive is
|
|
578
|
+
* instead constructed as a {@link SkinnedMesh} so the renderer
|
|
579
|
+
* routes it through a per-instance geometry clone for deformation.
|
|
580
|
+
*
|
|
581
|
+
* @param {number} mesh_index
|
|
582
|
+
* @param {{}} node
|
|
583
|
+
* @param {boolean} is_skinned
|
|
584
|
+
* @returns {Mesh[]}
|
|
585
|
+
*/
|
|
586
|
+
function process_mesh(mesh_index) {
|
|
587
|
+
|
|
588
|
+
const mesh = meshes[mesh_index];
|
|
589
|
+
|
|
590
|
+
const primitives = mesh.primitives;
|
|
591
|
+
|
|
592
|
+
const primitive_count = primitives.length;
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* @type {{geometry: MeshletGeometry, material: StandardShadeMaterial, name: string}[]}
|
|
596
|
+
*/
|
|
597
|
+
const result = [];
|
|
598
|
+
|
|
599
|
+
for (let i = 0; i < primitive_count; i++) {
|
|
600
|
+
const primitive = primitives[i];
|
|
601
|
+
|
|
602
|
+
// build geometry
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* @type {MeshletGeometry}
|
|
606
|
+
*/
|
|
607
|
+
let geometry;
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
geometry = obtain_geometry_by_primitive(primitive);
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.error(`Failed to obtain geometry for primitive [${i}] ${primitive}. `, e);
|
|
613
|
+
console.log('Ignoring mesh');
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (primitive_count === 1 && mesh.name !== undefined) {
|
|
618
|
+
geometry.name = mesh.name;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const material_id = primitive.material;
|
|
622
|
+
|
|
623
|
+
let material;
|
|
624
|
+
|
|
625
|
+
if (material_id !== undefined) {
|
|
626
|
+
material = mapped_materials[material_id];
|
|
627
|
+
|
|
628
|
+
if (material === undefined) {
|
|
629
|
+
console.warn(`specified material ${material_id} does not exist. Using default.`);
|
|
630
|
+
|
|
631
|
+
material = default_material;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
} else {
|
|
635
|
+
// no material specified, use default
|
|
636
|
+
material = default_material;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
result.push({ geometry, material, name: mesh.name ?? "" });
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
*
|
|
647
|
+
* @param {number} light_index
|
|
648
|
+
* @param {{}} node
|
|
649
|
+
* @returns {AbstractLight}
|
|
650
|
+
*/
|
|
651
|
+
function process_light(light_index, node) {
|
|
652
|
+
|
|
653
|
+
// SPEC: https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual/README.md
|
|
654
|
+
|
|
655
|
+
const gltf_light = gltf.extensions.KHR_lights_punctual.lights[light_index];
|
|
656
|
+
|
|
657
|
+
// parse color
|
|
658
|
+
const color = new Color();
|
|
659
|
+
|
|
660
|
+
if (gltf_light.color !== undefined) {
|
|
661
|
+
color.fromArray(gltf_light.color);
|
|
662
|
+
} else {
|
|
663
|
+
color.setRGB(1, 1, 1);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const intensity = gltf_light.intensity ?? 1;
|
|
667
|
+
|
|
668
|
+
const name = gltf_light.name ?? "";
|
|
669
|
+
|
|
670
|
+
let light;
|
|
671
|
+
|
|
672
|
+
const type = gltf_light.type;
|
|
673
|
+
|
|
674
|
+
switch (type) {
|
|
675
|
+
case "directional":
|
|
676
|
+
light = new DirectionalLight();
|
|
677
|
+
break;
|
|
678
|
+
case "point":
|
|
679
|
+
light = new PointLight();
|
|
680
|
+
light.distance = gltf_light.range ?? 1e20; // default light radius
|
|
681
|
+
break;
|
|
682
|
+
case "spot":
|
|
683
|
+
light = new SpotLight();
|
|
684
|
+
|
|
685
|
+
const gltf_spot = gltf_light.spot;
|
|
686
|
+
|
|
687
|
+
const outerCone = gltf_spot?.outerConeAngle ?? (Math.PI / 4); // default PI/4
|
|
688
|
+
const innerCone = gltf_spot?.innerConeAngle ?? 0;
|
|
689
|
+
|
|
690
|
+
light.angle = clamp(
|
|
691
|
+
outerCone,
|
|
692
|
+
0,
|
|
693
|
+
Math.PI / 2
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
light.penumbra = clamp01(
|
|
697
|
+
inverseLerp(light.angle, 0, innerCone)
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
light.distance = gltf_light.range ?? 1e20;
|
|
701
|
+
// TODO rotation is messed up?
|
|
702
|
+
break;
|
|
703
|
+
default:
|
|
704
|
+
throw new Error(`Unsupported light type ${type}`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
light.color.copy(color);
|
|
708
|
+
light.intensity = intensity;
|
|
709
|
+
light.name = name;
|
|
710
|
+
|
|
711
|
+
// check for light radius
|
|
712
|
+
light.radius = gltf_light.extras?.radius ?? 0;
|
|
713
|
+
|
|
714
|
+
light.near_clip_distance = gltf_light.extras?.near_clip_distance ?? light.radius;
|
|
715
|
+
|
|
716
|
+
light.casts_shadow = true;
|
|
717
|
+
|
|
718
|
+
return light;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Declared ahead of `process_node`, which pushes into it, because the scene walk that calls
|
|
723
|
+
* `process_node` runs before the point where this used to be declared — so the push landed
|
|
724
|
+
* in the `const`'s temporal dead zone and threw `ReferenceError: Cannot access
|
|
725
|
+
* 'punctual_lights' before initialization`. Inside the `try` that wraps `process_light`,
|
|
726
|
+
* whose `catch` logs "Failed to process light" and moves on: so every KHR_lights_punctual
|
|
727
|
+
* light in every glTF was dropped, and the console said the light had failed to parse.
|
|
728
|
+
* @type {{light: import("../../light/model/Light.js").Light, node: number}[]}
|
|
729
|
+
*/
|
|
730
|
+
const punctual_lights = [];
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
*
|
|
734
|
+
* @param {Node3D} parent
|
|
735
|
+
* @param {number} node_index
|
|
736
|
+
*/
|
|
737
|
+
function process_node(parent, node_index) {
|
|
738
|
+
const node = nodes[node_index];
|
|
739
|
+
|
|
740
|
+
// glTF "skin" attaches at the node, not the mesh: every primitive of the referenced
|
|
741
|
+
// mesh deforms against this skin. Resolved later, against `skins[]`, which is built
|
|
742
|
+
// after the walk — the joints may be visited *after* this node — so the primitives
|
|
743
|
+
// produced here are queued and pushed into `skin.meshes` once the walk finishes.
|
|
744
|
+
const skin_index = typeof node.skin === "number" ? node.skin : -1;
|
|
745
|
+
|
|
746
|
+
const record = new SceneNode();
|
|
747
|
+
|
|
748
|
+
record.parent = parent;
|
|
749
|
+
|
|
750
|
+
if (node.name !== undefined) {
|
|
751
|
+
record.name = node.name;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const t = record.local;
|
|
755
|
+
|
|
756
|
+
if (node.matrix !== undefined) {
|
|
757
|
+
t.fromMatrix(node.matrix);
|
|
758
|
+
} else {
|
|
759
|
+
if (node.translation !== undefined) {
|
|
760
|
+
const v = node.translation;
|
|
761
|
+
t.setTranslation(v[0], v[1], v[2]);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (node.rotation !== undefined) {
|
|
765
|
+
const q = node.rotation;
|
|
766
|
+
t.setRotation(q[0], q[1], q[2], q[3]);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (node.scale !== undefined) {
|
|
770
|
+
const v = node.scale;
|
|
771
|
+
t.setScale(v[0], v[1], v[2]);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
t.updateMatrix();
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const index = bundle.add_node(record);
|
|
778
|
+
|
|
779
|
+
// joint refs, channel targets and light anchors resolve through this once the
|
|
780
|
+
// recursion unwinds
|
|
781
|
+
record_index_of[node_index] = index;
|
|
782
|
+
|
|
783
|
+
if (node.extensions !== undefined && node.extensions.KHR_lights_punctual !== undefined) {
|
|
784
|
+
const light_index = node.extensions.KHR_lights_punctual.light;
|
|
785
|
+
|
|
786
|
+
try {
|
|
787
|
+
// a light is not a node: the node is where it is, and the bundle carries the pair
|
|
788
|
+
const punctual = process_light(light_index, node);
|
|
789
|
+
|
|
790
|
+
if (punctual.isDirectionalLight === true || punctual.isSpotLight === true) {
|
|
791
|
+
// glTF lights shine along their local -Z; engine convention is forward =
|
|
792
|
+
// shining direction. The direction is read off the matrix, which the
|
|
793
|
+
// record keeps current.
|
|
794
|
+
t64_transform_direction(SCRATCH_DIRECTION, t, 0, 0, 1);
|
|
795
|
+
t64_look_rotation(
|
|
796
|
+
t,
|
|
797
|
+
-SCRATCH_DIRECTION.x, -SCRATCH_DIRECTION.y, -SCRATCH_DIRECTION.z,
|
|
798
|
+
0, 1, 0
|
|
799
|
+
);
|
|
800
|
+
t.updateMatrix();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
punctual_lights.push({ light: punctual, node: index });
|
|
804
|
+
} catch (e) {
|
|
805
|
+
console.error("Failed to process light ", light_index, " ", e);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (typeof node.mesh === "number") {
|
|
810
|
+
const primitives = process_mesh(node.mesh);
|
|
811
|
+
|
|
812
|
+
/** @type {number[]} the records that draw this mesh's primitives */
|
|
813
|
+
const produced = [];
|
|
814
|
+
|
|
815
|
+
if (primitives.length === 1) {
|
|
816
|
+
// the node is the primitive. Its name wins over the mesh's, which is what the
|
|
817
|
+
// pose query names parts by; the geometry keeps the mesh's own
|
|
818
|
+
const primitive = primitives[0];
|
|
819
|
+
|
|
820
|
+
record.geometry = primitive.geometry;
|
|
821
|
+
record.material = primitive.material;
|
|
822
|
+
|
|
823
|
+
if (node.name === undefined) {
|
|
824
|
+
record.name = primitive.name;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
produced.push(index);
|
|
828
|
+
} else {
|
|
829
|
+
// one child per primitive, each named after the mesh, at the node's own place
|
|
830
|
+
for (let i = 0; i < primitives.length; i++) {
|
|
831
|
+
const primitive = primitives[i];
|
|
832
|
+
const child = new SceneNode();
|
|
833
|
+
|
|
834
|
+
child.parent = index;
|
|
835
|
+
child.name = primitive.name;
|
|
836
|
+
child.geometry = primitive.geometry;
|
|
837
|
+
child.material = primitive.material;
|
|
838
|
+
|
|
839
|
+
produced.push(bundle.add_node(child));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (skin_index !== -1 && produced.length > 0) {
|
|
844
|
+
skin_mesh_pushes.push({
|
|
845
|
+
skin_index,
|
|
846
|
+
meshes: produced,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (node.children !== undefined) {
|
|
852
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
853
|
+
process_node(index, node.children[i]);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
for (let i = 0; i < scenes.length; i++) {
|
|
859
|
+
const scene = scenes[i];
|
|
860
|
+
|
|
861
|
+
// a root per glTF scene, unnamed and at the identity — what the prefab compiler folds
|
|
862
|
+
const scene_root = bundle.add_node(new SceneNode());
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* @type {number[]}
|
|
866
|
+
*/
|
|
867
|
+
const scene_nodes = scene.nodes;
|
|
868
|
+
const node_count = scene_nodes.length;
|
|
869
|
+
|
|
870
|
+
for (let i = 0; i < node_count; i++) {
|
|
871
|
+
process_node(scene_root, scene_nodes[i]);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ── Skins ──────────────────────────────────────────────────
|
|
876
|
+
//
|
|
877
|
+
// Build {@link Skin} objects from `gltf.skins[]`. Joints
|
|
878
|
+
// resolve to node indices through `record_index_of` (fully
|
|
879
|
+
// populated by the recursion above). Inverse-bind matrices
|
|
880
|
+
// come from a single MAT4 accessor — `joint_count * 16` f32s
|
|
881
|
+
// laid out column-major, copied into a flat Float32Array that
|
|
882
|
+
// the manager can subarray-view at registration time.
|
|
883
|
+
const skins = (gltf.skins ?? []).map((gltf_skin, skin_index) => {
|
|
884
|
+
const joint_indices = gltf_skin.joints;
|
|
885
|
+
const joint_count = joint_indices.length;
|
|
886
|
+
|
|
887
|
+
const joint_nodes = new Array(joint_count);
|
|
888
|
+
for (let i = 0; i < joint_count; i++) {
|
|
889
|
+
const joint_node_index = joint_indices[i];
|
|
890
|
+
const joint_node = record_index_of[joint_node_index];
|
|
891
|
+
|
|
892
|
+
assert.notEqual(
|
|
893
|
+
joint_node, -1,
|
|
894
|
+
`skin[${skin_index}].joints[${i}] references unknown node index ${joint_node_index}`
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
joint_nodes[i] = joint_node;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Inverse-bind matrices: glTF stores them MAT4-typed
|
|
901
|
+
// (column-major, 16 f32s per joint). When the accessor is
|
|
902
|
+
// omitted, the spec says every joint's IBM is identity —
|
|
903
|
+
// we materialise that here so the manager always sees a
|
|
904
|
+
// ready-to-use flat array.
|
|
905
|
+
let inverse_bind_matrices;
|
|
906
|
+
if (gltf_skin.inverseBindMatrices !== undefined) {
|
|
907
|
+
inverse_bind_matrices = read_accessor_as_float32_array(gltf_skin.inverseBindMatrices);
|
|
908
|
+
} else {
|
|
909
|
+
inverse_bind_matrices = new Float32Array(joint_count * 16);
|
|
910
|
+
for (let i = 0; i < joint_count; i++) {
|
|
911
|
+
inverse_bind_matrices[i * 16 + 0] = 1;
|
|
912
|
+
inverse_bind_matrices[i * 16 + 5] = 1;
|
|
913
|
+
inverse_bind_matrices[i * 16 + 10] = 1;
|
|
914
|
+
inverse_bind_matrices[i * 16 + 15] = 1;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return Skin.from({
|
|
919
|
+
name: gltf_skin.name ?? "",
|
|
920
|
+
joints: joint_nodes,
|
|
921
|
+
inverse_bind_matrices,
|
|
922
|
+
meshes: [],
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// Drain the deferred skin.meshes pushes captured during the
|
|
927
|
+
// node walk.
|
|
928
|
+
for (let i = 0; i < skin_mesh_pushes.length; i++) {
|
|
929
|
+
const { skin_index, meshes: skin_meshes } = skin_mesh_pushes[i];
|
|
930
|
+
skins[skin_index].meshes.push(...skin_meshes);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ── Animation clips ───────────────────────────────────────
|
|
934
|
+
//
|
|
935
|
+
// One glTF animation → one {@link ShadeAnimationClip}. Each
|
|
936
|
+
// channel becomes a {@link ShadeAnimationChannel}; per-component
|
|
937
|
+
// curves are built from the sampler's input (times) / output
|
|
938
|
+
// (values) accessors via the engine's curve_from_track_data
|
|
939
|
+
// helpers, which already handle STEP / LINEAR / CUBICSPLINE.
|
|
940
|
+
const clips = [];
|
|
941
|
+
|
|
942
|
+
const gltf_animations = gltf.animations ?? [];
|
|
943
|
+
for (let a = 0; a < gltf_animations.length; a++) {
|
|
944
|
+
const gltf_animation = gltf_animations[a];
|
|
945
|
+
|
|
946
|
+
const channels = [];
|
|
947
|
+
|
|
948
|
+
for (let c = 0; c < gltf_animation.channels.length; c++) {
|
|
949
|
+
const gltf_channel = gltf_animation.channels[c];
|
|
950
|
+
const target = gltf_channel.target;
|
|
951
|
+
|
|
952
|
+
const property = gltf_animation_path_to_property(target.path);
|
|
953
|
+
if (property === -1) {
|
|
954
|
+
// Morph-target weights or other unsupported paths.
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (target.node === undefined) {
|
|
959
|
+
// Channels without a node target are a glTF
|
|
960
|
+
// extension hook and not actionable here.
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const target_node = record_index_of[target.node];
|
|
965
|
+
|
|
966
|
+
assert.notEqual(
|
|
967
|
+
target_node, -1,
|
|
968
|
+
`animations[${a}].channels[${c}].target.node ${target.node} not resolved`
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
const sampler = gltf_animation.samplers[gltf_channel.sampler];
|
|
972
|
+
const times = read_accessor_as_float32_array(sampler.input);
|
|
973
|
+
const values = read_accessor_as_float32_array(sampler.output);
|
|
974
|
+
const interpolation = gltf_interpolation_to_kind(sampler.interpolation);
|
|
975
|
+
const component_count = node3d_property_component_count(property);
|
|
976
|
+
|
|
977
|
+
// Canonicalize quaternion signs on rotation channels.
|
|
978
|
+
//
|
|
979
|
+
// glTF stores rotation keyframes as raw (x, y, z, w)
|
|
980
|
+
// quaternions, but `q` and `-q` represent the same
|
|
981
|
+
// orientation. Authoring tools (notably Mixamo) often
|
|
982
|
+
// alternate signs across consecutive keyframes when a
|
|
983
|
+
// bone passes through 180°. Per-component interpolation
|
|
984
|
+
// between sign-mismatched keyframes — especially
|
|
985
|
+
// CUBICSPLINE — passes through near-zero quaternions
|
|
986
|
+
// mid-segment; the apply shader's `normalize(value)`
|
|
987
|
+
// call then produces an effectively-identity quaternion
|
|
988
|
+
// for one frame, which reads as a one-frame snap to
|
|
989
|
+
// bind pose.
|
|
990
|
+
//
|
|
991
|
+
// Fix: flip the sign of every keyframe whose val-quat
|
|
992
|
+
// has negative dot product with the prior keyframe's
|
|
993
|
+
// val-quat. For CUBICSPLINE, flip the in/out tangents
|
|
994
|
+
// alongside the value so the cubic stays consistent.
|
|
995
|
+
// The leading sign is anchored against the target
|
|
996
|
+
// node's authored rest rotation so all clips driving
|
|
997
|
+
// one node land in the same hemisphere — required
|
|
998
|
+
// for weighted multi-clip blends, where the pose
|
|
999
|
+
// accumulator sums raw components across clips.
|
|
1000
|
+
// No-op when the property isn't Rotation, or when the
|
|
1001
|
+
// data was already canonicalized at export time.
|
|
1002
|
+
if (property === Node3DProperty.Rotation && times.length >= 1) {
|
|
1003
|
+
// `canonicalize_quaternion_signs` reads the rest rotation by component name,
|
|
1004
|
+
// which a `Transform64` does not offer — hand it a quaternion instead
|
|
1005
|
+
const rest = bundle.nodes[target_node].local;
|
|
1006
|
+
|
|
1007
|
+
SCRATCH_REST_ROTATION.set(
|
|
1008
|
+
rest.rotation_x,
|
|
1009
|
+
rest.rotation_y,
|
|
1010
|
+
rest.rotation_z,
|
|
1011
|
+
rest.rotation_w
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
canonicalize_quaternion_signs(
|
|
1015
|
+
values, times.length, interpolation,
|
|
1016
|
+
SCRATCH_REST_ROTATION
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Build the per-axis curves object. component_count
|
|
1021
|
+
// is property-driven (3 for Translation/Scale, 4 for
|
|
1022
|
+
// Rotation); slots beyond what the property uses are
|
|
1023
|
+
// omitted. The keys align with the GPU track's
|
|
1024
|
+
// (x, y, z, w) vec4f output — see ShadeAnimationChannel.
|
|
1025
|
+
const axis_keys = ['x', 'y', 'z', 'w'];
|
|
1026
|
+
const curves = {};
|
|
1027
|
+
for (let comp = 0; comp < component_count; comp++) {
|
|
1028
|
+
const curve = new AnimationCurve();
|
|
1029
|
+
curve_from_track_data(curve, interpolation, values, times, component_count, comp);
|
|
1030
|
+
curves[axis_keys[comp]] = curve;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
channels.push(ShadeAnimationChannel.from({
|
|
1034
|
+
target: target_node,
|
|
1035
|
+
property,
|
|
1036
|
+
curves,
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (channels.length === 0) {
|
|
1041
|
+
// All channels were unsupported / unresolved — skip
|
|
1042
|
+
// the clip entirely so we don't emit a zero-channel
|
|
1043
|
+
// record the manager would reject.
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const clip = ShadeAnimationClip.from({
|
|
1048
|
+
name: gltf_animation.name ?? "",
|
|
1049
|
+
channels,
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
// Default optimization pass: strip channels that
|
|
1053
|
+
// evaluate to the identity transform and individual
|
|
1054
|
+
// per-axis curves that are constant at identity, then
|
|
1055
|
+
// key-reduce the survivors. glTF exporters often emit
|
|
1056
|
+
// redundant bind-pose channels (every joint gets a
|
|
1057
|
+
// translation channel even when only rotation is
|
|
1058
|
+
// animated), so this typically trims a sizeable
|
|
1059
|
+
// fraction of the channel + curve count. Behaviour
|
|
1060
|
+
// unchanged — applying the optimised clip produces
|
|
1061
|
+
// the same poses.
|
|
1062
|
+
//
|
|
1063
|
+
// Stats are computed by diffing channel + total-curve
|
|
1064
|
+
// counts before and after; the methods don't carry
|
|
1065
|
+
// them in their return values.
|
|
1066
|
+
const before_channels = clip.channels.length;
|
|
1067
|
+
let before_curves = 0;
|
|
1068
|
+
for (let i = 0; i < clip.channels.length; i++) {
|
|
1069
|
+
before_curves += clip.channels[i].curve_count;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
clip.optimize(target => bundle.nodes[target].local);
|
|
1073
|
+
|
|
1074
|
+
const after_channels = clip.channels.length;
|
|
1075
|
+
let after_curves = 0;
|
|
1076
|
+
for (let i = 0; i < clip.channels.length; i++) {
|
|
1077
|
+
after_curves += clip.channels[i].curve_count;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const removed_channels = before_channels - after_channels;
|
|
1081
|
+
const removed_curves = before_curves - after_curves;
|
|
1082
|
+
if (removed_channels > 0 || removed_curves > 0) {
|
|
1083
|
+
// console.log(
|
|
1084
|
+
// `[load_gltf] optimised clip '${clip.name}': ` +
|
|
1085
|
+
// `removed ${removed_channels} track(s), ` +
|
|
1086
|
+
// `${removed_curves} curve(s)`
|
|
1087
|
+
// );
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
clips.push(clip);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// console.profileEnd('parse-gltf');
|
|
1094
|
+
// console.timeEnd('parse-gltf');
|
|
1095
|
+
|
|
1096
|
+
bundle.skins = skins;
|
|
1097
|
+
bundle.clips = clips;
|
|
1098
|
+
bundle.lights = punctual_lights;
|
|
1099
|
+
return bundle;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return load_gltf_document(url, { assetManager, scope, fileMap, texture_support })
|
|
1103
|
+
.then(process)
|
|
1104
|
+
// .then(prepare_lightmap)
|
|
1105
|
+
;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* The same file, parsed and with its dependencies in memory, **without** being turned into anything
|
|
1110
|
+
* a renderer draws: the glTF's own JSON alongside `buffers` as `Uint8Array`s and `images` as
|
|
1111
|
+
* `ImageBitmap`s — or, for a `KHR_texture_basisu` image, as a `ShadeTexelData` already in a device
|
|
1112
|
+
* format, because no browser decodes a KTX2 container into a bitmap.
|
|
1113
|
+
*
|
|
1114
|
+
* The half of {@link load_gltf} that is not about pictures. A glTF carries more than a scene to draw
|
|
1115
|
+
* — collision hulls, navigation surfaces, spawn markers, whatever a tool reads accessors for — and
|
|
1116
|
+
* those consumers want the accessors and the bytes behind them, not a {@link SceneBundle} they would
|
|
1117
|
+
* have to take apart again.
|
|
1118
|
+
*
|
|
1119
|
+
* @param {string} url un-resolved — the asset manager resolves network paths
|
|
1120
|
+
* @param {Object} options
|
|
1121
|
+
* @param {AssetManager} options.assetManager
|
|
1122
|
+
* @param {AssetRequestScope} [options.scope]
|
|
1123
|
+
* @param {Map<string, File|Blob|ArrayBuffer>} [options.fileMap]
|
|
1124
|
+
* @param {Set<GPUTextureFormat>} [options.texture_support] see {@link load_gltf}
|
|
1125
|
+
* @returns {Promise<TinyGltf>}
|
|
1126
|
+
*/
|
|
1127
|
+
export function load_gltf_document(url, { assetManager, scope, fileMap, texture_support } = {}) {
|
|
1128
|
+
const gltf = new TinyGltf(assetManager, scope, texture_support);
|
|
1129
|
+
|
|
1130
|
+
if (fileMap) {
|
|
1131
|
+
gltf.fileMap = fileMap;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
return gltf.loadFromUrl(url);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
|