@aura3d/engine 1.5.2 → 2.0.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/README.md +142 -117
- package/dist/animation/FootIk.d.ts +10 -1
- package/dist/animation/FootIk.d.ts.map +1 -1
- package/dist/animation/FootIk.js.map +1 -1
- package/dist/animation/HumanoidRetargeting.js +1 -1
- package/dist/animation/RigQuality.d.ts +1 -1
- package/dist/animation/RigQuality.js +2 -2
- package/dist/animation/RigQuality.js.map +1 -1
- package/dist/animation/SecondaryAnimationSampling.d.ts +55 -0
- package/dist/animation/SecondaryAnimationSampling.d.ts.map +1 -0
- package/dist/animation/SecondaryAnimationSampling.js +145 -0
- package/dist/animation/SecondaryAnimationSampling.js.map +1 -0
- package/dist/animation/SpringBones.d.ts.map +1 -1
- package/dist/animation/SpringBones.js +1 -2
- package/dist/animation/SpringBones.js.map +1 -1
- package/dist/animation/browser-index.d.ts +0 -2
- package/dist/animation/browser-index.d.ts.map +1 -1
- package/dist/animation/browser-index.js +0 -2
- package/dist/animation/browser-index.js.map +1 -1
- package/dist/animation/index.d.ts +0 -2
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/animation/index.js +0 -2
- package/dist/animation/index.js.map +1 -1
- package/dist/assets/GLTFAnimationRuntime.d.ts +4 -0
- package/dist/assets/GLTFAnimationRuntime.d.ts.map +1 -1
- package/dist/assets/GLTFAnimationRuntime.js +29 -6
- package/dist/assets/GLTFAnimationRuntime.js.map +1 -1
- package/dist/assets/GLTFLoader.d.ts.map +1 -1
- package/dist/assets/GLTFLoader.js +13 -4
- package/dist/assets/GLTFLoader.js.map +1 -1
- package/dist/assets/browser-index.d.ts +0 -4
- package/dist/assets/browser-index.d.ts.map +1 -1
- package/dist/assets/browser-index.js +0 -2
- package/dist/assets/browser-index.js.map +1 -1
- package/dist/assets/gltf-runtime.d.ts +6 -0
- package/dist/assets/gltf-runtime.d.ts.map +1 -0
- package/dist/assets/gltf-runtime.js +4 -0
- package/dist/assets/gltf-runtime.js.map +1 -0
- package/dist/assets/index.d.ts +0 -4
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +0 -2
- package/dist/assets/index.js.map +1 -1
- package/dist/audio/AudioBus.d.ts +19 -1
- package/dist/audio/AudioBus.d.ts.map +1 -1
- package/dist/audio/AudioBus.js.map +1 -1
- package/dist/audio/AudioFileManager.d.ts +5 -0
- package/dist/audio/AudioFileManager.d.ts.map +1 -1
- package/dist/audio/AudioFileManager.js +13 -0
- package/dist/audio/AudioFileManager.js.map +1 -1
- package/dist/audio/AudioSource.d.ts +8 -0
- package/dist/audio/AudioSource.d.ts.map +1 -1
- package/dist/audio/AudioSource.js +56 -7
- package/dist/audio/AudioSource.js.map +1 -1
- package/dist/audio/index.d.ts +1 -7
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +0 -3
- package/dist/audio/index.js.map +1 -1
- package/dist/aura3d-cli/asset-core-types.d.ts +1 -1
- package/dist/aura3d-cli/asset-core-types.d.ts.map +1 -1
- package/dist/aura3d-cli/asset-manifest.d.ts.map +1 -1
- package/dist/aura3d-cli/asset-manifest.js +36 -2
- package/dist/aura3d-cli/asset-manifest.js.map +1 -1
- package/dist/aura3d-cli/asset-role-admission.js +1 -1
- package/dist/aura3d-cli/asset-role-admission.js.map +1 -1
- package/dist/aura3d-cli/cli-help.js +3 -3
- package/dist/aura3d-cli/cli-help.js.map +1 -1
- package/dist/aura3d-cli/cli-options.d.ts +1 -1
- package/dist/aura3d-cli/cli-options.d.ts.map +1 -1
- package/dist/aura3d-cli/cli-options.js +2 -2
- package/dist/aura3d-cli/cli-options.js.map +1 -1
- package/dist/aura3d-cli/cli.js +1 -1
- package/dist/aura3d-cli/cli.js.map +1 -1
- package/dist/aura3d-cli/index.d.ts.map +1 -1
- package/dist/aura3d-cli/index.js +13 -5
- package/dist/aura3d-cli/index.js.map +1 -1
- package/dist/create-aura3d/index.d.ts.map +1 -1
- package/dist/create-aura3d/index.js +10 -4
- package/dist/create-aura3d/index.js.map +1 -1
- package/dist/create-aura3d/showcase-spec-evidence.js +5 -1
- package/dist/create-aura3d/showcase-spec-evidence.js.map +1 -1
- package/dist/create-aura3d/showcase-spec-game-geometry-extractor.d.ts +8 -1
- package/dist/create-aura3d/showcase-spec-game-geometry-extractor.d.ts.map +1 -1
- package/dist/create-aura3d/showcase-spec-game-geometry-extractor.js +191 -31
- package/dist/create-aura3d/showcase-spec-game-geometry-extractor.js.map +1 -1
- package/dist/create-aura3d/showcase-spec-game-template-evidence.d.ts.map +1 -1
- package/dist/create-aura3d/showcase-spec-game-template-evidence.js +7 -0
- package/dist/create-aura3d/showcase-spec-game-template-evidence.js.map +1 -1
- package/dist/create-aura3d/showcase-spec-parser.js +1 -1
- package/dist/create-aura3d/showcase-spec-parser.js.map +1 -1
- package/dist/create-aura3d/showcase-spec-types.d.ts +50 -0
- package/dist/create-aura3d/showcase-spec-types.d.ts.map +1 -1
- package/dist/editor-runtime/index.d.ts +0 -2
- package/dist/editor-runtime/index.d.ts.map +1 -1
- package/dist/editor-runtime/index.js +0 -1
- package/dist/editor-runtime/index.js.map +1 -1
- package/dist/engine/agent-api/GameAssetValidation.js +1 -1
- package/dist/engine/agent-api/GameAssetValidation.js.map +1 -1
- package/dist/engine/agent-api/GameEvidence.js +1 -1
- package/dist/engine/agent-api/GameEvidence.js.map +1 -1
- package/dist/engine/agent-api/GameGenreKits.d.ts +63 -0
- package/dist/engine/agent-api/GameGenreKits.d.ts.map +1 -1
- package/dist/engine/agent-api/GameGenreKits.js +274 -56
- package/dist/engine/agent-api/GameGenreKits.js.map +1 -1
- package/dist/engine/agent-api/GameRuntime.d.ts +38 -1
- package/dist/engine/agent-api/GameRuntime.d.ts.map +1 -1
- package/dist/engine/agent-api/GameRuntime.js +69 -1
- package/dist/engine/agent-api/GameRuntime.js.map +1 -1
- package/dist/engine/agent-api/GameSceneGeometryBindings.d.ts +24 -0
- package/dist/engine/agent-api/GameSceneGeometryBindings.d.ts.map +1 -1
- package/dist/engine/agent-api/GameSceneGeometryBindings.js +52 -0
- package/dist/engine/agent-api/GameSceneGeometryBindings.js.map +1 -1
- package/dist/engine/agent-api/LeanArcadeRuntime.d.ts +111 -0
- package/dist/engine/agent-api/LeanArcadeRuntime.d.ts.map +1 -0
- package/dist/engine/agent-api/LeanArcadeRuntime.js +219 -0
- package/dist/engine/agent-api/LeanArcadeRuntime.js.map +1 -0
- package/dist/engine/agent-api/PhysicsRuntime.d.ts +332 -0
- package/dist/engine/agent-api/PhysicsRuntime.d.ts.map +1 -0
- package/dist/engine/agent-api/PhysicsRuntime.js +619 -0
- package/dist/engine/agent-api/PhysicsRuntime.js.map +1 -0
- package/dist/engine/agent-api/PlatformerMotion.d.ts +124 -0
- package/dist/engine/agent-api/PlatformerMotion.d.ts.map +1 -1
- package/dist/engine/agent-api/PlatformerMotion.js +101 -16
- package/dist/engine/agent-api/PlatformerMotion.js.map +1 -1
- package/dist/engine/agent-api/RootGeometry.d.ts +36 -0
- package/dist/engine/agent-api/RootGeometry.d.ts.map +1 -0
- package/dist/engine/agent-api/RootGeometry.js +152 -0
- package/dist/engine/agent-api/RootGeometry.js.map +1 -0
- package/dist/engine/agent-api/VehicleChassis.d.ts +52 -0
- package/dist/engine/agent-api/VehicleChassis.d.ts.map +1 -1
- package/dist/engine/agent-api/VehicleChassis.js +182 -8
- package/dist/engine/agent-api/VehicleChassis.js.map +1 -1
- package/dist/engine/agent-api/WorldLabelRenderer.d.ts +65 -1
- package/dist/engine/agent-api/WorldLabelRenderer.d.ts.map +1 -1
- package/dist/engine/agent-api/WorldLabelRenderer.js +48 -5
- package/dist/engine/agent-api/WorldLabelRenderer.js.map +1 -1
- package/dist/engine/agent-api/humanoid-walk-runtime.d.ts +1 -1
- package/dist/engine/agent-api/index.d.ts +206 -13
- package/dist/engine/agent-api/index.d.ts.map +1 -1
- package/dist/engine/agent-api/index.js +1388 -357
- package/dist/engine/agent-api/index.js.map +1 -1
- package/dist/engine/agent-api/lean-base.d.ts +224 -0
- package/dist/engine/agent-api/lean-base.d.ts.map +1 -0
- package/dist/engine/agent-api/lean-base.js +322 -0
- package/dist/engine/agent-api/lean-base.js.map +1 -0
- package/dist/engine/agent-api/lean-game.d.ts +3 -0
- package/dist/engine/agent-api/lean-game.d.ts.map +1 -0
- package/dist/engine/agent-api/lean-game.js +3 -0
- package/dist/engine/agent-api/lean-game.js.map +1 -0
- package/dist/engine/agent-api/lean-product.d.ts +3 -0
- package/dist/engine/agent-api/lean-product.d.ts.map +1 -0
- package/dist/engine/agent-api/lean-product.js +3 -0
- package/dist/engine/agent-api/lean-product.js.map +1 -0
- package/dist/engine/agent-api/lean.d.ts +3 -0
- package/dist/engine/agent-api/lean.d.ts.map +1 -0
- package/dist/engine/agent-api/lean.js +3 -0
- package/dist/engine/agent-api/lean.js.map +1 -0
- package/dist/engine/agent-api/media-node.d.ts +42 -0
- package/dist/engine/agent-api/media-node.d.ts.map +1 -0
- package/dist/engine/agent-api/media-node.js +44 -0
- package/dist/engine/agent-api/media-node.js.map +1 -0
- package/dist/engine/agent-api/product-viewer-runtime.d.ts.map +1 -1
- package/dist/engine/game/GameAudio.d.ts +5 -0
- package/dist/engine/game/GameAudio.d.ts.map +1 -1
- package/dist/engine/game/GameAudio.js +65 -23
- package/dist/engine/game/GameAudio.js.map +1 -1
- package/dist/engine/production-runtime/TypedGLBActor.d.ts +9 -1
- package/dist/engine/production-runtime/TypedGLBActor.d.ts.map +1 -1
- package/dist/engine/production-runtime/TypedGLBActor.js +25 -7
- package/dist/engine/production-runtime/TypedGLBActor.js.map +1 -1
- package/dist/engine/production-runtime/index.d.ts +2 -2
- package/dist/engine/production-runtime/index.d.ts.map +1 -1
- package/dist/engine/threejs-example-parity/FlagshipFoundation.d.ts +3 -3
- package/dist/engine/threejs-example-parity/FlagshipFoundation.d.ts.map +1 -1
- package/dist/environments/index.d.ts +5 -10
- package/dist/environments/index.d.ts.map +1 -1
- package/dist/environments/index.js +0 -3
- package/dist/environments/index.js.map +1 -1
- package/dist/environments/node.d.ts +7 -0
- package/dist/environments/node.d.ts.map +1 -0
- package/dist/environments/node.js +5 -0
- package/dist/environments/node.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/input/InputActionBinding.d.ts +44 -0
- package/dist/input/InputActionBinding.d.ts.map +1 -0
- package/dist/input/InputActionBinding.js +86 -0
- package/dist/input/InputActionBinding.js.map +1 -0
- package/dist/input/InputActionBindingFixtures.d.ts +8 -0
- package/dist/input/InputActionBindingFixtures.d.ts.map +1 -1
- package/dist/input/InputActionBindingFixtures.js +8 -0
- package/dist/input/InputActionBindingFixtures.js.map +1 -1
- package/dist/input/InputValueProcessors.d.ts +19 -0
- package/dist/input/InputValueProcessors.d.ts.map +1 -0
- package/dist/input/InputValueProcessors.js +24 -0
- package/dist/input/InputValueProcessors.js.map +1 -0
- package/dist/input/index.d.ts +2 -6
- package/dist/input/index.d.ts.map +1 -1
- package/dist/input/index.js +1 -3
- package/dist/input/index.js.map +1 -1
- package/dist/lean/ArcadeRuntime.d.ts +111 -0
- package/dist/lean/ArcadeRuntime.d.ts.map +1 -0
- package/dist/lean/ArcadeRuntime.js +219 -0
- package/dist/lean/ArcadeRuntime.js.map +1 -0
- package/dist/lean/base.d.ts +228 -0
- package/dist/lean/base.d.ts.map +1 -0
- package/dist/lean/base.js +322 -0
- package/dist/lean/base.js.map +1 -0
- package/dist/lean/game.d.ts +19 -0
- package/dist/lean/game.d.ts.map +1 -0
- package/dist/lean/game.js +37 -0
- package/dist/lean/game.js.map +1 -0
- package/dist/lean/index.d.ts +4 -0
- package/dist/lean/index.d.ts.map +1 -0
- package/dist/lean/index.js +10 -0
- package/dist/lean/index.js.map +1 -0
- package/dist/lean/product.d.ts +4 -0
- package/dist/lean/product.d.ts.map +1 -0
- package/dist/lean/product.js +55 -0
- package/dist/lean/product.js.map +1 -0
- package/dist/materials/index.d.ts +1 -10
- package/dist/materials/index.d.ts.map +1 -1
- package/dist/materials/index.js +1 -6
- package/dist/materials/index.js.map +1 -1
- package/dist/materials/node.d.ts +4 -0
- package/dist/materials/node.d.ts.map +1 -0
- package/dist/materials/node.js +3 -0
- package/dist/materials/node.js.map +1 -0
- package/dist/physics/ArcadeCharacterController.d.ts +53 -0
- package/dist/physics/ArcadeCharacterController.d.ts.map +1 -0
- package/dist/physics/ArcadeCharacterController.js +98 -0
- package/dist/physics/ArcadeCharacterController.js.map +1 -0
- package/dist/physics/ArcadeVehicleTelemetry.d.ts +162 -0
- package/dist/physics/ArcadeVehicleTelemetry.d.ts.map +1 -0
- package/dist/physics/ArcadeVehicleTelemetry.js +363 -0
- package/dist/physics/ArcadeVehicleTelemetry.js.map +1 -0
- package/dist/physics/CharacterController.d.ts +52 -0
- package/dist/physics/CharacterController.d.ts.map +1 -1
- package/dist/physics/CharacterController.js +380 -7
- package/dist/physics/CharacterController.js.map +1 -1
- package/dist/physics/Constraint.d.ts +20 -4
- package/dist/physics/Constraint.d.ts.map +1 -1
- package/dist/physics/Constraint.js +34 -60
- package/dist/physics/Constraint.js.map +1 -1
- package/dist/physics/FightingCharacterController.d.ts +32 -0
- package/dist/physics/FightingCharacterController.d.ts.map +1 -0
- package/dist/physics/FightingCharacterController.js +76 -0
- package/dist/physics/FightingCharacterController.js.map +1 -0
- package/dist/physics/MeshBVH.d.ts +72 -0
- package/dist/physics/MeshBVH.d.ts.map +1 -0
- package/dist/physics/MeshBVH.js +244 -0
- package/dist/physics/MeshBVH.js.map +1 -0
- package/dist/physics/PathFollowDriver.d.ts +162 -0
- package/dist/physics/PathFollowDriver.d.ts.map +1 -0
- package/dist/physics/PathFollowDriver.js +163 -0
- package/dist/physics/PathFollowDriver.js.map +1 -0
- package/dist/physics/PhysicsDebugDraw.d.ts +40 -1
- package/dist/physics/PhysicsDebugDraw.d.ts.map +1 -1
- package/dist/physics/PhysicsDebugDraw.js +87 -17
- package/dist/physics/PhysicsDebugDraw.js.map +1 -1
- package/dist/physics/PhysicsWorld.d.ts +37 -18
- package/dist/physics/PhysicsWorld.d.ts.map +1 -1
- package/dist/physics/PhysicsWorld.js +231 -630
- package/dist/physics/PhysicsWorld.js.map +1 -1
- package/dist/physics/RacingLineProfile.d.ts +116 -0
- package/dist/physics/RacingLineProfile.d.ts.map +1 -0
- package/dist/physics/RacingLineProfile.js +181 -0
- package/dist/physics/RacingLineProfile.js.map +1 -0
- package/dist/physics/Raycast.d.ts +11 -0
- package/dist/physics/Raycast.d.ts.map +1 -1
- package/dist/physics/Raycast.js +62 -8
- package/dist/physics/Raycast.js.map +1 -1
- package/dist/physics/RigidBody.d.ts +16 -9
- package/dist/physics/RigidBody.d.ts.map +1 -1
- package/dist/physics/RigidBody.js +23 -75
- package/dist/physics/RigidBody.js.map +1 -1
- package/dist/physics/SurfaceQuery.d.ts +72 -0
- package/dist/physics/SurfaceQuery.d.ts.map +1 -0
- package/dist/physics/SurfaceQuery.js +89 -0
- package/dist/physics/SurfaceQuery.js.map +1 -0
- package/dist/physics/VehicleDynamics.d.ts +10 -0
- package/dist/physics/VehicleDynamics.d.ts.map +1 -1
- package/dist/physics/VehicleDynamics.js +31 -3
- package/dist/physics/VehicleDynamics.js.map +1 -1
- package/dist/physics/VehicleMotion.d.ts +125 -0
- package/dist/physics/VehicleMotion.d.ts.map +1 -0
- package/dist/physics/VehicleMotion.js +373 -0
- package/dist/physics/VehicleMotion.js.map +1 -0
- package/dist/physics/index.d.ts +62 -17
- package/dist/physics/index.d.ts.map +1 -1
- package/dist/physics/index.js +70 -17
- package/dist/physics/index.js.map +1 -1
- package/dist/physics/solverless-entry.d.ts +41 -0
- package/dist/physics/solverless-entry.d.ts.map +1 -0
- package/dist/physics/solverless-entry.js +40 -0
- package/dist/physics/solverless-entry.js.map +1 -0
- package/dist/physics/solverless.d.ts +37 -0
- package/dist/physics/solverless.d.ts.map +1 -0
- package/dist/physics/solverless.js +36 -0
- package/dist/physics/solverless.js.map +1 -0
- package/dist/physics/world-entry.d.ts +31 -0
- package/dist/physics/world-entry.d.ts.map +1 -0
- package/dist/physics/world-entry.js +30 -0
- package/dist/physics/world-entry.js.map +1 -0
- package/dist/physics/world.d.ts +18 -0
- package/dist/physics/world.d.ts.map +1 -0
- package/dist/physics/world.js +17 -0
- package/dist/physics/world.js.map +1 -0
- package/dist/physics-rapier/index.d.ts +169 -0
- package/dist/physics-rapier/index.d.ts.map +1 -0
- package/dist/physics-rapier/index.js +311 -0
- package/dist/physics-rapier/index.js.map +1 -0
- package/dist/product-studio/ProductLighting.d.ts.map +1 -1
- package/dist/product-studio/ProductLighting.js +34 -3
- package/dist/product-studio/ProductLighting.js.map +1 -1
- package/dist/rendering/ArchitecturalLighting.d.ts +35 -0
- package/dist/rendering/ArchitecturalLighting.d.ts.map +1 -0
- package/dist/rendering/ArchitecturalLighting.js +162 -0
- package/dist/rendering/ArchitecturalLighting.js.map +1 -0
- package/dist/rendering/ArchitecturalMaterialCatalog.d.ts +1 -1
- package/dist/rendering/ArchitecturalMaterialCatalog.d.ts.map +1 -1
- package/dist/rendering/ArchitecturalMaterialCatalog.js +1 -1
- package/dist/rendering/ArchitecturalMaterialCatalog.js.map +1 -1
- package/dist/rendering/ArchitecturalMeasurement.d.ts +27 -0
- package/dist/rendering/ArchitecturalMeasurement.d.ts.map +1 -0
- package/dist/rendering/ArchitecturalMeasurement.js +112 -0
- package/dist/rendering/ArchitecturalMeasurement.js.map +1 -0
- package/dist/rendering/CanonicalProductScene.d.ts +13 -0
- package/dist/rendering/CanonicalProductScene.d.ts.map +1 -0
- package/dist/rendering/CanonicalProductScene.js +78 -0
- package/dist/rendering/CanonicalProductScene.js.map +1 -0
- package/dist/rendering/CascadedShadowMaps.d.ts +1 -1
- package/dist/rendering/CascadedShadowMaps.d.ts.map +1 -1
- package/dist/rendering/CascadedShadowMaps.js +1 -1
- package/dist/rendering/CascadedShadowMaps.js.map +1 -1
- package/dist/rendering/DepthPass.d.ts +1 -1
- package/dist/rendering/DepthPass.d.ts.map +1 -1
- package/dist/rendering/DepthPass.js +2 -2
- package/dist/rendering/DepthPass.js.map +1 -1
- package/dist/rendering/EnvironmentBackgroundPass.d.ts +1 -1
- package/dist/rendering/EnvironmentBackgroundPass.d.ts.map +1 -1
- package/dist/rendering/EnvironmentBackgroundPass.js +2 -2
- package/dist/rendering/EnvironmentBackgroundPass.js.map +1 -1
- package/dist/rendering/EnvironmentPlatform.js +2 -2
- package/dist/rendering/EnvironmentPlatform.js.map +1 -1
- package/dist/rendering/ForwardPass.d.ts +1 -1
- package/dist/rendering/ForwardPass.d.ts.map +1 -1
- package/dist/rendering/ForwardPass.js +24 -10
- package/dist/rendering/ForwardPass.js.map +1 -1
- package/dist/rendering/Geometry.d.ts +2 -0
- package/dist/rendering/Geometry.d.ts.map +1 -1
- package/dist/rendering/Geometry.js +15 -0
- package/dist/rendering/Geometry.js.map +1 -1
- package/dist/rendering/InstancedPBRMaterial.js +1 -1
- package/dist/rendering/InstancedPBRMaterial.js.map +1 -1
- package/dist/rendering/InstancedUnlitMaterial.js +1 -1
- package/dist/rendering/InstancedUnlitMaterial.js.map +1 -1
- package/dist/rendering/Material.js +2 -2
- package/dist/rendering/Material.js.map +1 -1
- package/dist/rendering/MaterialBinding.js +4 -4
- package/dist/rendering/MaterialBinding.js.map +1 -1
- package/dist/rendering/MaterialInstance.js +2 -2
- package/dist/rendering/MaterialInstance.js.map +1 -1
- package/dist/rendering/MorphUnlitMaterial.js +1 -1
- package/dist/rendering/MorphUnlitMaterial.js.map +1 -1
- package/dist/rendering/NormalMappedPBRMaterial.js +1 -1
- package/dist/rendering/NormalMappedPBRMaterial.js.map +1 -1
- package/dist/rendering/OceanSurface.d.ts +62 -0
- package/dist/rendering/OceanSurface.d.ts.map +1 -0
- package/dist/rendering/OceanSurface.js +240 -0
- package/dist/rendering/OceanSurface.js.map +1 -0
- package/dist/rendering/PBRMaterial.d.ts.map +1 -1
- package/dist/rendering/PBRMaterial.js +3 -3
- package/dist/rendering/PBRMaterial.js.map +1 -1
- package/dist/rendering/PortableShaderMaterial.d.ts +60 -0
- package/dist/rendering/PortableShaderMaterial.d.ts.map +1 -0
- package/dist/rendering/PortableShaderMaterial.js +223 -0
- package/dist/rendering/PortableShaderMaterial.js.map +1 -0
- package/dist/rendering/PostProcessPass.d.ts.map +1 -1
- package/dist/rendering/PostProcessPass.js +30 -6
- package/dist/rendering/PostProcessPass.js.map +1 -1
- package/dist/rendering/ProceduralTexture.d.ts +27 -0
- package/dist/rendering/ProceduralTexture.d.ts.map +1 -0
- package/dist/rendering/ProceduralTexture.js +336 -0
- package/dist/rendering/ProceduralTexture.js.map +1 -0
- package/dist/rendering/ProductTurntable.d.ts +96 -0
- package/dist/rendering/ProductTurntable.d.ts.map +1 -0
- package/dist/rendering/ProductTurntable.js +639 -0
- package/dist/rendering/ProductTurntable.js.map +1 -0
- package/dist/rendering/RenderDevice.d.ts +30 -0
- package/dist/rendering/RenderDevice.d.ts.map +1 -1
- package/dist/rendering/RenderDevice.js.map +1 -1
- package/dist/rendering/Renderer.d.ts +3 -2
- package/dist/rendering/Renderer.d.ts.map +1 -1
- package/dist/rendering/Renderer.js +23 -9
- package/dist/rendering/Renderer.js.map +1 -1
- package/dist/rendering/RendererPostprocessPlan.js +4 -4
- package/dist/rendering/RendererPostprocessPlan.js.map +1 -1
- package/dist/rendering/ScreenSpaceLineMaterial.js +1 -1
- package/dist/rendering/ScreenSpaceLineMaterial.js.map +1 -1
- package/dist/rendering/ShaderChunks.d.ts.map +1 -1
- package/dist/rendering/ShaderChunks.js +81 -19
- package/dist/rendering/ShaderChunks.js.map +1 -1
- package/dist/rendering/ShaderLibrary.d.ts +2 -78
- package/dist/rendering/ShaderLibrary.d.ts.map +1 -1
- package/dist/rendering/ShaderLibrary.js +127 -793
- package/dist/rendering/ShaderLibrary.js.map +1 -1
- package/dist/rendering/ShaderLibraryCore.d.ts +100 -0
- package/dist/rendering/ShaderLibraryCore.d.ts.map +1 -0
- package/dist/rendering/ShaderLibraryCore.js +800 -0
- package/dist/rendering/ShaderLibraryCore.js.map +1 -0
- package/dist/rendering/ShaderModule.d.ts +1 -1
- package/dist/rendering/ShaderModule.d.ts.map +1 -1
- package/dist/rendering/ShaderModule.js +4 -2
- package/dist/rendering/ShaderModule.js.map +1 -1
- package/dist/rendering/ShadowPass.d.ts +1 -1
- package/dist/rendering/ShadowPass.d.ts.map +1 -1
- package/dist/rendering/ShadowPass.js +6 -4
- package/dist/rendering/ShadowPass.js.map +1 -1
- package/dist/rendering/SkinnedLitMaterial.js +1 -1
- package/dist/rendering/SkinnedLitMaterial.js.map +1 -1
- package/dist/rendering/SkinnedUnlitMaterial.js +1 -1
- package/dist/rendering/SkinnedUnlitMaterial.js.map +1 -1
- package/dist/rendering/SpaceEnvironment.d.ts +54 -0
- package/dist/rendering/SpaceEnvironment.d.ts.map +1 -0
- package/dist/rendering/SpaceEnvironment.js +116 -0
- package/dist/rendering/SpaceEnvironment.js.map +1 -0
- package/dist/rendering/TerrainHeightfield.d.ts +60 -0
- package/dist/rendering/TerrainHeightfield.d.ts.map +1 -0
- package/dist/rendering/TerrainHeightfield.js +239 -0
- package/dist/rendering/TerrainHeightfield.js.map +1 -0
- package/dist/rendering/TextureBinding.d.ts +14 -0
- package/dist/rendering/TextureBinding.d.ts.map +1 -1
- package/dist/rendering/TextureBinding.js +17 -0
- package/dist/rendering/TextureBinding.js.map +1 -1
- package/dist/rendering/TexturedPBRMaterial.js +1 -1
- package/dist/rendering/TexturedPBRMaterial.js.map +1 -1
- package/dist/rendering/TexturedUnlitMaterial.js +1 -1
- package/dist/rendering/TexturedUnlitMaterial.js.map +1 -1
- package/dist/rendering/UnlitMaterial.js +1 -1
- package/dist/rendering/UnlitMaterial.js.map +1 -1
- package/dist/rendering/VegetationFixtures.d.ts +1 -1
- package/dist/rendering/VegetationFixtures.d.ts.map +1 -1
- package/dist/rendering/VegetationFixtures.js +1 -1
- package/dist/rendering/VegetationFixtures.js.map +1 -1
- package/dist/rendering/VegetationScatter.d.ts +68 -0
- package/dist/rendering/VegetationScatter.d.ts.map +1 -0
- package/dist/rendering/VegetationScatter.js +241 -0
- package/dist/rendering/VegetationScatter.js.map +1 -0
- package/dist/rendering/VoxelWorld.d.ts +51 -0
- package/dist/rendering/VoxelWorld.d.ts.map +1 -0
- package/dist/rendering/VoxelWorld.js +183 -0
- package/dist/rendering/VoxelWorld.js.map +1 -0
- package/dist/rendering/Weather.d.ts +44 -0
- package/dist/rendering/Weather.d.ts.map +1 -0
- package/dist/rendering/Weather.js +140 -0
- package/dist/rendering/Weather.js.map +1 -0
- package/dist/rendering/WebGL2Device.d.ts +22 -0
- package/dist/rendering/WebGL2Device.d.ts.map +1 -1
- package/dist/rendering/WebGL2Device.js +134 -23
- package/dist/rendering/WebGL2Device.js.map +1 -1
- package/dist/rendering/WebGPUDevice.d.ts +19 -2
- package/dist/rendering/WebGPUDevice.d.ts.map +1 -1
- package/dist/rendering/WebGPUDevice.js +188 -9
- package/dist/rendering/WebGPUDevice.js.map +1 -1
- package/dist/rendering/index.d.ts +25 -32
- package/dist/rendering/index.d.ts.map +1 -1
- package/dist/rendering/index.js +35 -19
- package/dist/rendering/index.js.map +1 -1
- package/dist/rendering/lean/LeanProductRenderer.d.ts +29 -0
- package/dist/rendering/lean/LeanProductRenderer.d.ts.map +1 -0
- package/dist/rendering/lean/LeanProductRenderer.js +38 -0
- package/dist/rendering/lean/LeanProductRenderer.js.map +1 -0
- package/dist/rendering/lean/LeanProductionRenderer.d.ts +38 -0
- package/dist/rendering/lean/LeanProductionRenderer.d.ts.map +1 -0
- package/dist/rendering/lean/LeanProductionRenderer.js +88 -0
- package/dist/rendering/lean/LeanProductionRenderer.js.map +1 -0
- package/dist/rendering/lean-runtime.d.ts +18 -0
- package/dist/rendering/lean-runtime.d.ts.map +1 -0
- package/dist/rendering/lean-runtime.js +13 -0
- package/dist/rendering/lean-runtime.js.map +1 -0
- package/dist/rendering/postprocess/EffectComposer.d.ts.map +1 -1
- package/dist/rendering/postprocess/EffectComposer.js +1 -3
- package/dist/rendering/postprocess/EffectComposer.js.map +1 -1
- package/dist/rendering/production-runtime/PBRHDRPipeline.d.ts.map +1 -1
- package/dist/rendering/production-runtime/PBRHDRPipeline.js +5 -1
- package/dist/rendering/production-runtime/PBRHDRPipeline.js.map +1 -1
- package/dist/rendering/production-runtime/ProductionRendererTypes.d.ts +9 -0
- package/dist/rendering/production-runtime/ProductionRendererTypes.d.ts.map +1 -1
- package/dist/rendering/production-runtime/ProductionRendererTypes.js.map +1 -1
- package/dist/rendering/production-runtime/ProductionRuntimeRenderer.d.ts +5 -0
- package/dist/rendering/production-runtime/ProductionRuntimeRenderer.d.ts.map +1 -1
- package/dist/rendering/production-runtime/ProductionRuntimeRenderer.js +53 -5
- package/dist/rendering/production-runtime/ProductionRuntimeRenderer.js.map +1 -1
- package/dist/rendering/production-runtime/ProductionWebGL2Renderer.d.ts +10 -0
- package/dist/rendering/production-runtime/ProductionWebGL2Renderer.d.ts.map +1 -1
- package/dist/rendering/production-runtime/ProductionWebGL2Renderer.js +21 -2
- package/dist/rendering/production-runtime/ProductionWebGL2Renderer.js.map +1 -1
- package/dist/rendering/production-runtime/ProductionWebGPURenderer.d.ts.map +1 -1
- package/dist/rendering/production-runtime/ProductionWebGPURenderer.js +4 -1
- package/dist/rendering/production-runtime/ProductionWebGPURenderer.js.map +1 -1
- package/dist/rendering/webgpu-device-entry.d.ts +25 -0
- package/dist/rendering/webgpu-device-entry.d.ts.map +1 -0
- package/dist/rendering/webgpu-device-entry.js +24 -0
- package/dist/rendering/webgpu-device-entry.js.map +1 -0
- package/dist/rendering/webgpu.d.ts +25 -0
- package/dist/rendering/webgpu.d.ts.map +1 -0
- package/dist/rendering/webgpu.js +24 -0
- package/dist/rendering/webgpu.js.map +1 -0
- package/dist/scene/MathTypes.d.ts +1 -0
- package/dist/scene/MathTypes.d.ts.map +1 -1
- package/dist/scene/MathTypes.js +3 -0
- package/dist/scene/MathTypes.js.map +1 -1
- package/dist/scene/Renderable.d.ts +6 -0
- package/dist/scene/Renderable.d.ts.map +1 -1
- package/dist/scene/Renderable.js +8 -2
- package/dist/scene/Renderable.js.map +1 -1
- package/dist/scripting/index.d.ts +0 -16
- package/dist/scripting/index.d.ts.map +1 -1
- package/dist/scripting/index.js +0 -8
- package/dist/scripting/index.js.map +1 -1
- package/dist/workflows/production-runtime/ProductionExampleRuntime.d.ts +2 -2
- package/dist/workflows/production-runtime/ProductionExampleRuntime.d.ts.map +1 -1
- package/dist/workflows/production-runtime/ProductionExampleRuntime.js +2 -2
- package/dist/workflows/production-runtime/ProductionExampleRuntime.js.map +1 -1
- package/package.json +157 -40
- package/templates/cinematic-scene/node_modules/.vite/deps/_metadata.json +8 -0
- package/templates/cinematic-scene/node_modules/.vite/deps/package.json +3 -0
- package/templates/cinematic-scene/package.json +1 -1
- package/templates/cinematic-scene/test-results/.last-run.json +6 -0
- package/templates/cinematic-scene/test-results/route-health-Aura3D-cinematic-scene-reaches-ready-state/error-context.md +64 -0
- package/templates/mini-game/README.md +3 -2
- package/templates/mini-game/package.json +1 -1
- package/templates/mini-game/src/aura-assets.ts +1 -1
- package/templates/mini-game/src/main.ts +69 -141
- package/templates/product-viewer/README.md +7 -5
- package/templates/product-viewer/index.html +1 -1
- package/templates/product-viewer/node_modules/.vite/deps/_metadata.json +8 -0
- package/templates/product-viewer/node_modules/.vite/deps/package.json +3 -0
- package/templates/product-viewer/package.json +1 -1
- package/templates/product-viewer/src/aura-assets.ts +1 -1
- package/templates/product-viewer/src/main.ts +29 -23
|
@@ -1,10 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/*
|
|
2
|
+
* WS-2.2 — module-scope physics values come from the SOLVERLESS entry.
|
|
3
|
+
*
|
|
4
|
+
* `Shape`, `PhysicsStepper`, `ScenePhysicsBridge` and `PhysicsDebugDraw` are solver-free: each
|
|
5
|
+
* imports `PhysicsWorld` as a *type* only. But reaching them through `@aura3d/physics` — a chain of
|
|
6
|
+
* `export *` that includes `PhysicsWorld` — dragged in the physical adapter, so a scene with no
|
|
7
|
+
* bodies still downloaded the solver.
|
|
8
|
+
*
|
|
9
|
+
* This is a different defect from eager construction, which was already fixed (see `:9832`). A lazy
|
|
10
|
+
* `new PhysicsWorld()` still leaves a static `import`, and a bundler keeps the module either way.
|
|
11
|
+
* Deferring the *import* is what removes the bytes. `PhysicsWorld` is now loaded by
|
|
12
|
+
* `await import("../../physics/index.js")` at the points that actually simulate.
|
|
13
|
+
*/
|
|
14
|
+
import { PhysicsDebugDraw, PhysicsStepper, ScenePhysicsBridge, Shape as PhysicsShapeFactory } from "../../physics/solverless.js";
|
|
15
|
+
/*
|
|
16
|
+
* WS-2.2 — the solver, from a NARROW entry rather than the physics barrel.
|
|
17
|
+
*
|
|
18
|
+
* Static on purpose. `app.physics` is documented and tested as live synchronously for every app, so
|
|
19
|
+
* `createAuraApp` cannot await a solver, and R7 forbids breaking that to save bytes. What this does
|
|
20
|
+
* avoid is the barrel: `@aura3d/physics` is a chain of `export *` that also carries `HitboxWorld`,
|
|
21
|
+
* `CharacterController`, `KinematicBody`, arcade vehicle telemetry, `NarrowPhase` and six fixture modules, all
|
|
22
|
+
* of which arrived just to reach one class.
|
|
23
|
+
*/
|
|
24
|
+
import { PhysicsWorld } from "../../physics/world.js";
|
|
25
|
+
import { createExternalParityEnvironmentLighting, Geometry, IndexBuffer, InstancedPBRMaterial, PBRMaterial, ProductionRuntimeRenderer, srgbToLinearChannel, VertexBuffer, VertexFormat } from "../../rendering/index.js";
|
|
3
26
|
import { DirectionalLight, PointLight, SpotLight } from "../../scene/index.js";
|
|
4
|
-
import {
|
|
27
|
+
import { lookAtMat4, multiplyMat4, orthographicMat4, perspectiveMat4 } from "../../scene/MathTypes.js";
|
|
5
28
|
import { AURA_NORMALIZED_MODEL_MAX_DIMENSION, boundsFromAsset, boundsHeight, boundsMaxDimension, boundsSize } from "./SceneGroundingUtils.js";
|
|
6
29
|
import { createWorldLabelLayer } from "./WorldLabelRenderer.js";
|
|
30
|
+
import { createAuraText3DGeometry, defineAuraCustomGeometry, selectAuraRootLodLevel } from "./RootGeometry.js";
|
|
31
|
+
export * from "./RootGeometry.js";
|
|
7
32
|
export * from "./SpatialAnchoring.js";
|
|
33
|
+
export * from "./PhysicsRuntime.js";
|
|
34
|
+
/**
|
|
35
|
+
* WS-2.4: mesh surface queries on the public surface.
|
|
36
|
+
*
|
|
37
|
+
* Re-exported from `@aura3d/physics` so grounding anything to a mesh is a one-liner for any
|
|
38
|
+
* genre, from `@aura3d/engine` alone. Without this a route wanting real per-point ground
|
|
39
|
+
* height had to either deep-import `@aura3d/physics/src` (banned by the lint rule) or
|
|
40
|
+
* hand-roll an analytic approximation — which is exactly what every racing route did, and
|
|
41
|
+
* why wheels sank through visible road.
|
|
42
|
+
*/
|
|
43
|
+
/*
|
|
44
|
+
* WS-2.2 — re-exported from the SOLVERLESS entry, not the physics barrel.
|
|
45
|
+
*
|
|
46
|
+
* These are geometry queries: `MeshBVH` and `SurfaceQuery` import only `Shape`'s types. Re-exporting
|
|
47
|
+
* them from `@aura3d/physics` made this barrel a static consumer of the whole solver, so grounding a
|
|
48
|
+
* wheel on a mesh reached the physical solver. Same symbols, same public names.
|
|
49
|
+
*/
|
|
50
|
+
export { buildMeshBVH, createMeshSurfaceQuery, raycastMesh } from "../../physics/solverless.js";
|
|
51
|
+
import { createPhysicsRuntime } from "./PhysicsRuntime.js";
|
|
8
52
|
export * from "./FocusSelection.js";
|
|
9
53
|
export * from "./WorldLabelRenderer.js";
|
|
10
54
|
export * from "./VehicleChassis.js";
|
|
@@ -17,14 +61,23 @@ export * from "./FrameEncoder.js";
|
|
|
17
61
|
export * from "./BrowserFrameCaptureAdapter.js";
|
|
18
62
|
export * from "./MediaRecorderFrameEncoder.js";
|
|
19
63
|
export * from "./WebCodecsFrameEncoder.js";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
64
|
+
/*
|
|
65
|
+
* WS-2.3 — `FfmpegFrameEncoder` is deliberately NOT re-exported here.
|
|
66
|
+
*
|
|
67
|
+
* It is the only file in the 37-file media surface that reaches `node:` builtins (`child_process` to
|
|
68
|
+
* spawn ffmpeg; `fs/promises`/`os`/`path` to stage frames). Re-exporting it put Node builtins in every
|
|
69
|
+
* browser bundle of this entry point, which is why `tools/bundle-size` had to mark four `node:`
|
|
70
|
+
* specifiers external "for every browser bundle measurement" — a workaround for a dependency that
|
|
71
|
+
* should not have been in the browser graph. esbuild resolves `await import()` at build time whether or
|
|
72
|
+
* not the branch can run.
|
|
73
|
+
*
|
|
74
|
+
* Node consumers import `@aura3d/engine/media-node`. Enforced by `tools/browser-entry-purity`, which
|
|
75
|
+
* bundles every browser entry with no `node:` externals so a reachable builtin fails the build.
|
|
76
|
+
*/
|
|
23
77
|
export { GameInspector, createGameInspector } from "./GameInspector.js";
|
|
24
78
|
import { createGameInspector } from "./GameInspector.js";
|
|
25
79
|
export { createGameAudio } from "../game/GameAudio.js";
|
|
26
80
|
export * from "./RenderProgressTracker.js";
|
|
27
|
-
export * from "./VideoExportPipeline.js";
|
|
28
81
|
export * from "./AudioVisemeAnalyzer.js";
|
|
29
82
|
export * from "./ExternalPhonemeAnalyzer.js";
|
|
30
83
|
export * from "./WaveformVisualizer.js";
|
|
@@ -36,12 +89,7 @@ export * from "./CameraPresetLibrary.js";
|
|
|
36
89
|
export * from "./ShotCompositionRules.js";
|
|
37
90
|
export * from "./CameraChoreographer.js";
|
|
38
91
|
export * from "./CameraPathEditor.js";
|
|
39
|
-
export * from "./CaptionExporter.js";
|
|
40
92
|
export * from "./ThumbnailGenerator.js";
|
|
41
|
-
export * from "./YouTubeMetadataGenerator.js";
|
|
42
|
-
export * from "./YouTubeUploadAdapter.js";
|
|
43
|
-
export * from "./PublishingPipeline.js";
|
|
44
|
-
export * from "./CloudRenderAdapter.js";
|
|
45
93
|
export * from "./BatchEpisodeRenderer.js";
|
|
46
94
|
export * from "./AnimationAssetManifest.js";
|
|
47
95
|
export * from "./SceneGroundingUtils.js";
|
|
@@ -188,7 +236,8 @@ export function model(asset, options = {}) {
|
|
|
188
236
|
targetHeight: options.targetHeight,
|
|
189
237
|
targetMaxDimension: options.targetMaxDimension,
|
|
190
238
|
targetLength: options.targetLength,
|
|
191
|
-
physics: options.physics
|
|
239
|
+
physics: options.physics,
|
|
240
|
+
hiddenNodeNames: options.hiddenNodeNames
|
|
192
241
|
});
|
|
193
242
|
}
|
|
194
243
|
export function unsafeModelUrl(url, options = {}) {
|
|
@@ -235,7 +284,12 @@ function primitive(primitiveName, options = {}) {
|
|
|
235
284
|
size: options.size,
|
|
236
285
|
castShadow: options.castShadow ?? (primitiveName !== "plane" && options.material?.emissive === undefined),
|
|
237
286
|
receiveShadow: options.receiveShadow ?? true,
|
|
238
|
-
physics: options.physics
|
|
287
|
+
physics: options.physics,
|
|
288
|
+
instances: options.instances,
|
|
289
|
+
instanceColors: options.instanceColors,
|
|
290
|
+
geometry: options.geometry,
|
|
291
|
+
text3D: options.text3D,
|
|
292
|
+
lod: options.lod
|
|
239
293
|
});
|
|
240
294
|
}
|
|
241
295
|
function sceneNodeFromInput(node) {
|
|
@@ -262,6 +316,43 @@ export const primitives = {
|
|
|
262
316
|
capsule: (options) => primitive("capsule", options),
|
|
263
317
|
torus: (options) => primitive("torus", options)
|
|
264
318
|
};
|
|
319
|
+
function instancedPrimitive(primitiveName, options) {
|
|
320
|
+
if (options.transforms.length === 0)
|
|
321
|
+
throw new Error("Aura3D instancing requires at least one transform.");
|
|
322
|
+
if (options.colors && options.colors.length !== options.transforms.length)
|
|
323
|
+
throw new Error("Aura3D instance color count must match transform count.");
|
|
324
|
+
return primitive(primitiveName, { ...options, instances: options.transforms, instanceColors: options.colors });
|
|
325
|
+
}
|
|
326
|
+
export const instances = {
|
|
327
|
+
box: (options) => instancedPrimitive("box", options),
|
|
328
|
+
sphere: (options) => instancedPrimitive("sphere", options),
|
|
329
|
+
plane: (options) => instancedPrimitive("plane", options),
|
|
330
|
+
cylinder: (options) => instancedPrimitive("cylinder", options),
|
|
331
|
+
capsule: (options) => instancedPrimitive("capsule", options),
|
|
332
|
+
torus: (options) => instancedPrimitive("torus", options)
|
|
333
|
+
};
|
|
334
|
+
export const geometry = {
|
|
335
|
+
define: defineAuraCustomGeometry,
|
|
336
|
+
custom: (spec, options = {}) => primitive("custom", { ...options, geometry: defineAuraCustomGeometry(spec) })
|
|
337
|
+
};
|
|
338
|
+
export function text3D(text, options = {}) {
|
|
339
|
+
const built = createAuraText3DGeometry(text, options);
|
|
340
|
+
return primitive("custom", { ...options, geometry: built.geometry, text3D: { text: built.text, glyphCount: built.glyphCount, unsupportedCharacters: built.unsupportedCharacters, method: built.method } });
|
|
341
|
+
}
|
|
342
|
+
export function distanceLod(options) {
|
|
343
|
+
if (options.levels.length === 0)
|
|
344
|
+
throw new Error("Aura3D distance LOD requires at least one level.");
|
|
345
|
+
selectAuraRootLodLevel(0, options.levels, undefined, options.hysteresis ?? 0);
|
|
346
|
+
const first = options.levels[0];
|
|
347
|
+
if (!first.primitive && !first.geometry)
|
|
348
|
+
throw new Error("Every Aura3D LOD level requires a primitive or custom geometry.");
|
|
349
|
+
return primitive(first.geometry ? "custom" : first.primitive, {
|
|
350
|
+
...options,
|
|
351
|
+
geometry: first.geometry,
|
|
352
|
+
material: first.material ?? options.material,
|
|
353
|
+
lod: { levels: options.levels, hysteresis: options.hysteresis }
|
|
354
|
+
});
|
|
355
|
+
}
|
|
265
356
|
export const groups = {
|
|
266
357
|
create: group,
|
|
267
358
|
flatten: (nodes) => flattenSceneNodes(nodes)
|
|
@@ -1606,7 +1697,7 @@ const rendererQualityProfiles = {
|
|
|
1606
1697
|
kind: "aura-renderer-quality-profile",
|
|
1607
1698
|
id: "safe-basic",
|
|
1608
1699
|
label: "Safe Basic",
|
|
1609
|
-
rendererMode: "
|
|
1700
|
+
rendererMode: "production",
|
|
1610
1701
|
status: "supported",
|
|
1611
1702
|
antialiasing: "msaa",
|
|
1612
1703
|
pixelRatio: 1,
|
|
@@ -1615,7 +1706,7 @@ const rendererQualityProfiles = {
|
|
|
1615
1706
|
requestedFeatures: ["typed models", "primitives", "basic particles", "base-color textures", "runtime nodes"],
|
|
1616
1707
|
supportedInRoot: ["typed models", "primitives", "basic particles", "base-color textures", "runtime nodes"],
|
|
1617
1708
|
blockedInRoot: ["production PBR parity", "skinned animation mixer", "native WebGPU compute", "postprocess pass chain"],
|
|
1618
|
-
claimBoundary: "Root createAuraApp
|
|
1709
|
+
claimBoundary: "Root createAuraApp uses the production renderer with the conservative safe-basic feature profile. Claims still require route-specific browser evidence."
|
|
1619
1710
|
},
|
|
1620
1711
|
production: {
|
|
1621
1712
|
kind: "aura-renderer-quality-profile",
|
|
@@ -1684,8 +1775,6 @@ function analyzeProductionBridgeEligibility(nodes) {
|
|
|
1684
1775
|
const primitiveCount = nodes.filter((node) => node.kind === "primitive").length;
|
|
1685
1776
|
const effectCount = nodes.filter((node) => node.kind === "effect").length;
|
|
1686
1777
|
const reasons = [];
|
|
1687
|
-
if (typedModelCount < 1)
|
|
1688
|
-
reasons.push("production bridge requires at least one typed GLB from generated aura-assets");
|
|
1689
1778
|
if (unsafeModelCount > 0)
|
|
1690
1779
|
reasons.push("unsafeModelUrl and remote/raw model URLs are blocked from the production bridge");
|
|
1691
1780
|
if (inlineModelCount > 0)
|
|
@@ -1748,10 +1837,10 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
|
|
|
1748
1837
|
if (postprocessRequested && runtime?.mounted && !runtimePostprocess?.pixelBacked)
|
|
1749
1838
|
warnings.push("postprocess was requested but the runtime composer did not initialize a pixel-backed pass");
|
|
1750
1839
|
if (rendererSelection.mode === "production" && !runtime?.mounted && productionEligibility.eligible) {
|
|
1751
|
-
warnings.push(`Renderer profile "${rendererSelection.profile.id}"
|
|
1840
|
+
warnings.push(`Renderer profile "${rendererSelection.profile.id}" will use the production renderer after createAuraApp mounts; inspect app.diagnostics().renderer.runtime for pixel-backed status.`);
|
|
1752
1841
|
}
|
|
1753
1842
|
if (rendererSelection.mode === "production" && !productionEligibility.eligible) {
|
|
1754
|
-
warnings.push(`Renderer profile "${rendererSelection.profile.id}"
|
|
1843
|
+
warnings.push(`Renderer profile "${rendererSelection.profile.id}" cannot mount this scene on the production renderer: ${productionEligibility.reasons.join("; ")}.`);
|
|
1755
1844
|
}
|
|
1756
1845
|
if (rendererSelection.profile.status === "fallback-only") {
|
|
1757
1846
|
warnings.push(`Renderer profile "${rendererSelection.profile.id}" is a request profile; public claims require route-specific runtime diagnostics and screenshot proof.`);
|
|
@@ -1837,6 +1926,7 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
|
|
|
1837
1926
|
requestedPasses,
|
|
1838
1927
|
actualPasses: runtimePostprocess?.actualPasses ?? [],
|
|
1839
1928
|
fallbackPasses: runtimePostprocess?.fallbackPasses ?? [],
|
|
1929
|
+
targetFormat: runtimePostprocess?.targetFormat,
|
|
1840
1930
|
evidence: postprocessEvidence
|
|
1841
1931
|
},
|
|
1842
1932
|
runtime: {
|
|
@@ -1844,7 +1934,13 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
|
|
|
1844
1934
|
backend: runtime?.backend ?? "scene-plan",
|
|
1845
1935
|
postprocessVerified: runtimePostprocess?.pixelBacked ?? false,
|
|
1846
1936
|
passNames: runtimePostprocess?.actualPasses ?? [],
|
|
1847
|
-
warnings: runtime?.warnings ?? []
|
|
1937
|
+
warnings: runtime?.warnings ?? [],
|
|
1938
|
+
nativeInstancedSubmissions: runtime?.deviceDiagnostics?.nativeInstancedSubmissions ?? 0,
|
|
1939
|
+
submittedObjects: runtime?.deviceDiagnostics?.submittedObjects ?? 0,
|
|
1940
|
+
visibleObjects: runtime?.deviceDiagnostics?.visibleObjects ?? 0,
|
|
1941
|
+
culledObjects: runtime?.deviceDiagnostics?.culledObjects ?? 0,
|
|
1942
|
+
frustumTestedObjects: runtime?.deviceDiagnostics?.frustumTestedObjects ?? 0,
|
|
1943
|
+
lodSelections: runtime?.lodSelections ?? []
|
|
1848
1944
|
},
|
|
1849
1945
|
environment: {
|
|
1850
1946
|
enabled: environmentStatus.enabled,
|
|
@@ -2254,6 +2350,7 @@ function createRuntimeScenePhysics(snapshot) {
|
|
|
2254
2350
|
const physicsNodes = snapshot.nodes.filter((node) => (node.kind === "model" || node.kind === "primitive") && Boolean(node.physics));
|
|
2255
2351
|
if (physicsNodes.length === 0)
|
|
2256
2352
|
return undefined;
|
|
2353
|
+
// WS-2.2: solver resolved through the on-demand cache rather than a static import.
|
|
2257
2354
|
const world = new PhysicsWorld({ gravity: [0, -9.81, 0], fixedDelta: 1 / 60, enableSleeping: true });
|
|
2258
2355
|
const stepper = new PhysicsStepper(world.fixedDelta);
|
|
2259
2356
|
const debugDraw = new PhysicsDebugDraw();
|
|
@@ -2654,6 +2751,8 @@ export const prefabs = {
|
|
|
2654
2751
|
...makeCityCrosswalk("zebra crosswalk west", -0.72, 0, "eastWest"),
|
|
2655
2752
|
...makeCityCrosswalk("zebra crosswalk east", 0.72, 0, "eastWest")
|
|
2656
2753
|
];
|
|
2754
|
+
const cityTowerTransforms = [];
|
|
2755
|
+
const cityTowerColors = [];
|
|
2657
2756
|
const xSlots = [-4.25, -2.58, -0.95, 1.45, 3.3];
|
|
2658
2757
|
const zSlots = [-4, -1.45, 1.3, 3.65];
|
|
2659
2758
|
for (let index = 0; index < blocks; index += 1) {
|
|
@@ -2665,15 +2764,19 @@ export const prefabs = {
|
|
|
2665
2764
|
const color = night
|
|
2666
2765
|
? (index % 3 === 0 ? "#1e293b" : index % 3 === 1 ? "#2d3340" : "#172233")
|
|
2667
2766
|
: (index % 3 === 0 ? "#8ea2aa" : index % 3 === 1 ? "#b89b72" : "#668094");
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
material: material.pbr({ color, roughness: 0.68, metallic: 0.06 })
|
|
2671
|
-
}).position(x, height / 2, z).scale([1.08, height, 1.08]).toJSON());
|
|
2767
|
+
cityTowerTransforms.push({ position: [x, height / 2, z], scale: [1.08, height, 1.08] });
|
|
2768
|
+
cityTowerColors.push(color);
|
|
2672
2769
|
if (options.litWindows !== false) {
|
|
2673
2770
|
nodes.push(...makeBuildingWindowRows(x, z, height, index, timeOfDay));
|
|
2674
2771
|
}
|
|
2675
2772
|
nodes.push(...makeBuildingDetails(x, z, height, index, timeOfDay));
|
|
2676
2773
|
}
|
|
2774
|
+
nodes.push(instances.box({
|
|
2775
|
+
name: "city tower native instanced family",
|
|
2776
|
+
transforms: cityTowerTransforms,
|
|
2777
|
+
colors: cityTowerColors,
|
|
2778
|
+
material: material.pbr({ color: "#ffffff", roughness: 0.68, metallic: 0.06 })
|
|
2779
|
+
}).toJSON());
|
|
2677
2780
|
const lampPositions = [
|
|
2678
2781
|
[-1.15, 0, 0.85], [1.15, 0, 0.85], [-1.15, 0, -0.85], [1.15, 0, -0.85],
|
|
2679
2782
|
[-3.85, 0, -2.05], [-2.95, 0, 2.05], [2.05, 0, -2.05], [3.05, 0, 2.05],
|
|
@@ -2769,7 +2872,9 @@ export const prefabs = {
|
|
|
2769
2872
|
const rimMaterial = material.emissive({ color: "#dbeafe", emissive: "#93c5fd", emissiveIntensity: 0.18, opacity: 0.42 });
|
|
2770
2873
|
const nodes = [
|
|
2771
2874
|
primitives.plane({ name: "seamless matte product hero floor", material: floorMaterial }).position(0, -0.018, -0.62).scale([3.6, 1, 2.6]).toJSON(),
|
|
2772
|
-
primitives.plane({ name: "
|
|
2875
|
+
primitives.plane({ name: "vertical seamless product photography backdrop", material: backdropMaterial }).position(0, 1.22, -2.28).rotate(1.5708, 0, 0).scale([6.2, 1, 2.15]).toJSON(),
|
|
2876
|
+
primitives.box({ name: "cool cyan studio reflection panel", material: material.emissive({ color: "#20bfe8", emissive: "#20bfe8", emissiveIntensity: 0.46 }) }).position(-2.35, 1.14, -2.18).scale([0.24, 1.38, 0.035]).toJSON(),
|
|
2877
|
+
primitives.box({ name: "warm amber studio reflection panel", material: material.emissive({ color: "#f0a43c", emissive: "#f0a43c", emissiveIntensity: 0.42 }) }).position(2.35, 1.14, -2.18).scale([0.24, 1.38, 0.035]).toJSON(),
|
|
2773
2878
|
primitives.cylinder({ name: "low matte hero product plinth", material: plinthMaterial }).position(0, 0.255, -0.65).scale([1.46, 0.26, 1.46]).toJSON(),
|
|
2774
2879
|
primitives.cylinder({ name: "soft product contact shadow from footprint", material: shadowMaterial }).position(0, 0.292, -0.65).scale([1.06, 0.01, 0.66]).toJSON(),
|
|
2775
2880
|
primitives.box({ name: "subtle turntable orbit cue on product plinth", material: material.emissive({ color: "#93c5fd", emissive: "#38bdf8", emissiveIntensity: 0.22, opacity: 0.38 }) }).position(0, 0.52, -0.65).scale([1.02, 0.012, 1.02]).toJSON(),
|
|
@@ -3371,7 +3476,7 @@ function createMiniGolfStateController() {
|
|
|
3371
3476
|
const worldSnapshot = world.snapshot();
|
|
3372
3477
|
return {
|
|
3373
3478
|
physicsBackend: worldSnapshot.backend.active,
|
|
3374
|
-
deterministicReplayId: `mini-golf-
|
|
3479
|
+
deterministicReplayId: `mini-golf-rapier-v2-${shots}-${score}-${collisions}-${resets}`,
|
|
3375
3480
|
replayFrame: worldSnapshot.stats.steps,
|
|
3376
3481
|
captureTime: Number((worldSnapshot.stats.steps * world.fixedDelta).toFixed(3)),
|
|
3377
3482
|
shots,
|
|
@@ -3478,187 +3583,6 @@ export const games = {
|
|
|
3478
3583
|
export function collectGameRuntimeEvidence(app, options = {}) {
|
|
3479
3584
|
return collectGameRuntimeEvidenceV105(app, options);
|
|
3480
3585
|
}
|
|
3481
|
-
function createGameInputController(options) {
|
|
3482
|
-
const actions = options.actions;
|
|
3483
|
-
const axes = options.axes ?? {};
|
|
3484
|
-
const bufferMs = options.bufferMs ?? 120;
|
|
3485
|
-
const activeBindings = new Set();
|
|
3486
|
-
const activeActionOverrides = new Set();
|
|
3487
|
-
const previousHeld = new Map();
|
|
3488
|
-
const currentHeld = new Map();
|
|
3489
|
-
const pressedEdges = new Set();
|
|
3490
|
-
const releasedEdges = new Set();
|
|
3491
|
-
const lastPressedAt = new Map();
|
|
3492
|
-
const replayEvents = [];
|
|
3493
|
-
let frame = 0;
|
|
3494
|
-
let time = 0;
|
|
3495
|
-
let latestSnapshot = {
|
|
3496
|
-
kind: "aura-game-input-snapshot",
|
|
3497
|
-
frame,
|
|
3498
|
-
time,
|
|
3499
|
-
activeBindings: [],
|
|
3500
|
-
actions: {}
|
|
3501
|
-
};
|
|
3502
|
-
const resolveHeld = (action) => {
|
|
3503
|
-
if (activeActionOverrides.has(action))
|
|
3504
|
-
return true;
|
|
3505
|
-
const bindings = actions[action] ?? [];
|
|
3506
|
-
return bindings.some((binding) => activeBindings.has(binding));
|
|
3507
|
-
};
|
|
3508
|
-
const record = (type, binding) => {
|
|
3509
|
-
replayEvents.push({ frame, time, type, binding });
|
|
3510
|
-
};
|
|
3511
|
-
const pressBinding = (binding, shouldRecord = true) => {
|
|
3512
|
-
activeBindings.add(binding);
|
|
3513
|
-
if (actions[binding])
|
|
3514
|
-
activeActionOverrides.add(binding);
|
|
3515
|
-
if (shouldRecord)
|
|
3516
|
-
record("press", binding);
|
|
3517
|
-
};
|
|
3518
|
-
const releaseBinding = (binding, shouldRecord = true) => {
|
|
3519
|
-
activeBindings.delete(binding);
|
|
3520
|
-
activeActionOverrides.delete(binding);
|
|
3521
|
-
if (shouldRecord)
|
|
3522
|
-
record("release", binding);
|
|
3523
|
-
};
|
|
3524
|
-
const toSnapshot = () => {
|
|
3525
|
-
const actionStates = {};
|
|
3526
|
-
const nowMs = time * 1000;
|
|
3527
|
-
for (const action of Object.keys(actions)) {
|
|
3528
|
-
const held = currentHeld.get(action) ?? false;
|
|
3529
|
-
actionStates[action] = {
|
|
3530
|
-
pressed: pressedEdges.has(action),
|
|
3531
|
-
held,
|
|
3532
|
-
released: releasedEdges.has(action),
|
|
3533
|
-
buffered: pressedEdges.has(action) || nowMs - (lastPressedAt.get(action) ?? Number.NEGATIVE_INFINITY) <= bufferMs,
|
|
3534
|
-
value: held ? 1 : 0
|
|
3535
|
-
};
|
|
3536
|
-
}
|
|
3537
|
-
return {
|
|
3538
|
-
kind: "aura-game-input-snapshot",
|
|
3539
|
-
frame,
|
|
3540
|
-
time,
|
|
3541
|
-
activeBindings: [...activeBindings].sort(),
|
|
3542
|
-
actions: actionStates
|
|
3543
|
-
};
|
|
3544
|
-
};
|
|
3545
|
-
const update = (dt = 1 / 60) => {
|
|
3546
|
-
frame += 1;
|
|
3547
|
-
time += Math.max(0, dt);
|
|
3548
|
-
pressedEdges.clear();
|
|
3549
|
-
releasedEdges.clear();
|
|
3550
|
-
for (const action of Object.keys(actions)) {
|
|
3551
|
-
const held = resolveHeld(action);
|
|
3552
|
-
const wasHeld = previousHeld.get(action) ?? false;
|
|
3553
|
-
currentHeld.set(action, held);
|
|
3554
|
-
if (held && !wasHeld) {
|
|
3555
|
-
pressedEdges.add(action);
|
|
3556
|
-
lastPressedAt.set(action, time * 1000);
|
|
3557
|
-
}
|
|
3558
|
-
if (!held && wasHeld)
|
|
3559
|
-
releasedEdges.add(action);
|
|
3560
|
-
previousHeld.set(action, held);
|
|
3561
|
-
}
|
|
3562
|
-
latestSnapshot = toSnapshot();
|
|
3563
|
-
return latestSnapshot;
|
|
3564
|
-
};
|
|
3565
|
-
const target = options.target ?? (typeof window !== "undefined" ? window : undefined);
|
|
3566
|
-
const onKeyDown = (event) => {
|
|
3567
|
-
const keyboard = event;
|
|
3568
|
-
if (keyboard.repeat)
|
|
3569
|
-
return;
|
|
3570
|
-
if (keyboard.code)
|
|
3571
|
-
pressBinding(keyboard.code);
|
|
3572
|
-
if (keyboard.key && keyboard.key !== keyboard.code)
|
|
3573
|
-
pressBinding(keyboard.key);
|
|
3574
|
-
};
|
|
3575
|
-
const onKeyUp = (event) => {
|
|
3576
|
-
const keyboard = event;
|
|
3577
|
-
if (keyboard.code)
|
|
3578
|
-
releaseBinding(keyboard.code);
|
|
3579
|
-
if (keyboard.key && keyboard.key !== keyboard.code)
|
|
3580
|
-
releaseBinding(keyboard.key);
|
|
3581
|
-
};
|
|
3582
|
-
if (options.autoListen !== false && target?.addEventListener) {
|
|
3583
|
-
target.addEventListener("keydown", onKeyDown);
|
|
3584
|
-
target.addEventListener("keyup", onKeyUp);
|
|
3585
|
-
}
|
|
3586
|
-
return {
|
|
3587
|
-
kind: "aura-game-input-plan",
|
|
3588
|
-
actions,
|
|
3589
|
-
axes,
|
|
3590
|
-
bufferMs,
|
|
3591
|
-
update,
|
|
3592
|
-
snapshot() {
|
|
3593
|
-
return latestSnapshot;
|
|
3594
|
-
},
|
|
3595
|
-
pressed(action) {
|
|
3596
|
-
return pressedEdges.has(action);
|
|
3597
|
-
},
|
|
3598
|
-
held(action) {
|
|
3599
|
-
return currentHeld.get(action) ?? resolveHeld(action);
|
|
3600
|
-
},
|
|
3601
|
-
released(action) {
|
|
3602
|
-
return releasedEdges.has(action);
|
|
3603
|
-
},
|
|
3604
|
-
buffered(action, windowMs = bufferMs) {
|
|
3605
|
-
return pressedEdges.has(action) || time * 1000 - (lastPressedAt.get(action) ?? Number.NEGATIVE_INFINITY) <= windowMs;
|
|
3606
|
-
},
|
|
3607
|
-
axis(name, negativeAction, positiveAction) {
|
|
3608
|
-
const binding = axes[name];
|
|
3609
|
-
const negative = negativeAction ?? binding?.negative;
|
|
3610
|
-
const positive = positiveAction ?? binding?.positive;
|
|
3611
|
-
if (!negative && !positive)
|
|
3612
|
-
return this.held(name) ? 1 : 0;
|
|
3613
|
-
return (positive && this.held(positive) ? 1 : 0) - (negative && this.held(negative) ? 1 : 0);
|
|
3614
|
-
},
|
|
3615
|
-
press(binding) {
|
|
3616
|
-
pressBinding(binding);
|
|
3617
|
-
},
|
|
3618
|
-
release(binding) {
|
|
3619
|
-
releaseBinding(binding);
|
|
3620
|
-
},
|
|
3621
|
-
setAction(action, held) {
|
|
3622
|
-
if (held) {
|
|
3623
|
-
activeActionOverrides.add(action);
|
|
3624
|
-
record("press", action);
|
|
3625
|
-
}
|
|
3626
|
-
else {
|
|
3627
|
-
activeActionOverrides.delete(action);
|
|
3628
|
-
record("release", action);
|
|
3629
|
-
}
|
|
3630
|
-
},
|
|
3631
|
-
recorded() {
|
|
3632
|
-
return [...replayEvents];
|
|
3633
|
-
},
|
|
3634
|
-
replay(events) {
|
|
3635
|
-
activeBindings.clear();
|
|
3636
|
-
activeActionOverrides.clear();
|
|
3637
|
-
for (const event of events) {
|
|
3638
|
-
if (event.type === "press")
|
|
3639
|
-
pressBinding(event.binding, false);
|
|
3640
|
-
else
|
|
3641
|
-
releaseBinding(event.binding, false);
|
|
3642
|
-
}
|
|
3643
|
-
return update(0);
|
|
3644
|
-
},
|
|
3645
|
-
clearReplay() {
|
|
3646
|
-
replayEvents.length = 0;
|
|
3647
|
-
},
|
|
3648
|
-
dispose() {
|
|
3649
|
-
if (target?.removeEventListener) {
|
|
3650
|
-
target.removeEventListener("keydown", onKeyDown);
|
|
3651
|
-
target.removeEventListener("keyup", onKeyUp);
|
|
3652
|
-
}
|
|
3653
|
-
activeBindings.clear();
|
|
3654
|
-
activeActionOverrides.clear();
|
|
3655
|
-
previousHeld.clear();
|
|
3656
|
-
currentHeld.clear();
|
|
3657
|
-
pressedEdges.clear();
|
|
3658
|
-
releasedEdges.clear();
|
|
3659
|
-
}
|
|
3660
|
-
};
|
|
3661
|
-
}
|
|
3662
3586
|
export function createAuraGameRules(options = {}) {
|
|
3663
3587
|
return {
|
|
3664
3588
|
kind: "aura-game-rules",
|
|
@@ -5385,12 +5309,16 @@ function cityScene(options = {}) {
|
|
|
5385
5309
|
.camera(cityCameraPreset(options.cameraPreset ?? "overview", timeOfDay));
|
|
5386
5310
|
}
|
|
5387
5311
|
function collectCityInstancingPlan(nodes) {
|
|
5388
|
-
const
|
|
5312
|
+
const flattened = groups.flatten(nodes);
|
|
5313
|
+
const names = flattened.map((node) => "name" in node ? node.name ?? "" : "");
|
|
5314
|
+
const nativeInstanceNodes = flattened.filter((node) => node.kind === "primitive" && Boolean(node.instances?.length));
|
|
5315
|
+
const nativeInstances = nativeInstanceNodes.reduce((total, node) => total + (node.instances?.length ?? 0), 0);
|
|
5389
5316
|
const windows = names.filter((name) => name.includes("window column")).length;
|
|
5390
5317
|
const props = names.filter((name) => name.includes("bench") || name.includes("tree") || name.includes("sign") || name.includes("car body") || name.includes("traffic signal")).length;
|
|
5391
5318
|
const roadMarkings = names.filter((name) => name.includes("lane marking") || name.includes("road stripe") || name.includes("crosswalk") || name.includes("turn arrow") || name.includes("bike lane")).length;
|
|
5392
5319
|
const lights = names.filter((name) => name.includes("street lamp") || name.includes("headlight") || name.includes("lamp glow") || name.includes("city glow")).length;
|
|
5393
5320
|
const groupsList = [
|
|
5321
|
+
nativeInstanceNodes.some((node) => node.name?.includes("city tower")) ? "city towers" : "",
|
|
5394
5322
|
windows >= 8 ? "window columns" : "",
|
|
5395
5323
|
props >= 6 ? "street props" : "",
|
|
5396
5324
|
roadMarkings >= 8 ? "road markings" : "",
|
|
@@ -5398,13 +5326,15 @@ function collectCityInstancingPlan(nodes) {
|
|
|
5398
5326
|
].filter(Boolean);
|
|
5399
5327
|
return {
|
|
5400
5328
|
kind: "aura-city-instancing-plan",
|
|
5401
|
-
rendererPath: "
|
|
5329
|
+
rendererPath: "productionRuntimeNativeInstancing",
|
|
5402
5330
|
windows,
|
|
5403
5331
|
props,
|
|
5404
5332
|
roadMarkings,
|
|
5405
5333
|
lights,
|
|
5334
|
+
nativeInstanceGroups: nativeInstanceNodes.length,
|
|
5335
|
+
nativeInstances,
|
|
5406
5336
|
groups: groupsList,
|
|
5407
|
-
instanced:
|
|
5337
|
+
instanced: nativeInstanceNodes.length > 0 && nativeInstances > 0
|
|
5408
5338
|
};
|
|
5409
5339
|
}
|
|
5410
5340
|
function changedCityNodeNames(previous, next) {
|
|
@@ -5417,7 +5347,11 @@ function changedCityNodeNames(previous, next) {
|
|
|
5417
5347
|
function validateCityVisualQA(nodes, options = {}) {
|
|
5418
5348
|
const flattened = groups.flatten(nodes);
|
|
5419
5349
|
const names = flattened.map((node) => "name" in node ? node.name ?? "" : "");
|
|
5420
|
-
const buildings =
|
|
5350
|
+
const buildings = flattened.reduce((total, node) => {
|
|
5351
|
+
if (!("name" in node) || !node.name?.includes("city tower"))
|
|
5352
|
+
return total;
|
|
5353
|
+
return total + (node.kind === "primitive" && node.instances?.length ? node.instances.length : 1);
|
|
5354
|
+
}, 0);
|
|
5421
5355
|
const windows = names.filter((name) => name.includes("window column")).length;
|
|
5422
5356
|
const streets = names.filter((name) => name.includes("road") || name.includes("street") || name.includes("avenue")).length;
|
|
5423
5357
|
const crosswalks = names.filter((name) => name.includes("crosswalk")).length;
|
|
@@ -5945,7 +5879,7 @@ function createSceneKitLazyLoadingPlan(id) {
|
|
|
5945
5879
|
systems.push({ system, trigger, loadedByDefault: false, evidence });
|
|
5946
5880
|
};
|
|
5947
5881
|
if (id === "physicsPlayground" || id === "miniGolf") {
|
|
5948
|
-
add("physics-backend", `${id} physics state construction`, "
|
|
5882
|
+
add("physics-backend", `${id} physics state construction`, "Rapier-backed physics is isolated scene-kit work and is not required for static material, chart, or product scenes");
|
|
5949
5883
|
}
|
|
5950
5884
|
if (id === "productViewer") {
|
|
5951
5885
|
add("product-gltf-loader", "typed product model render path", "GLTF loading is tied to typed model scenes and excluded from procedural-only scene kits");
|
|
@@ -6215,10 +6149,11 @@ export const promptRecipes = {
|
|
|
6215
6149
|
.add(primitives.sphere({ name: "rain splash foreground", material: material.emissive({ color: "#c8f4ff", emissive: "#c8f4ff" }) }).position(-0.88, 0.035, 0.72).scale([0.07, 0.018, 0.07]))
|
|
6216
6150
|
.add(primitives.sphere({ name: "rain splash key side", material: material.emissive({ color: "#ffe1ad", emissive: "#ffe1ad" }) }).position(1.14, 0.035, 0.44).scale([0.08, 0.018, 0.08]))
|
|
6217
6151
|
.add(model(asset, { name: plan.subject.label }).position(-0.08, 0.02, -0.86).rotate(-0.08, -0.74, 0.02).scale(1.48))
|
|
6218
|
-
.add(lights.ambient({ intensity: 0.
|
|
6152
|
+
.add(lights.ambient({ intensity: 0.22, color: "#b4c9e8" }))
|
|
6219
6153
|
.add(lights.point({ name: "hard cyan rim", position: [-2.35, 2.65, 0.85], color: "#38d6ff", intensity: 3.25 }))
|
|
6220
6154
|
.add(lights.point({ name: "warm practical key", position: [2.35, 1.7, -0.25], color: "#ffd08a", intensity: 1.6 }))
|
|
6221
6155
|
.add(lights.point({ name: "low floor bounce", position: [0.1, 0.45, 1.1], color: "#7edfff", intensity: 0.62 }))
|
|
6156
|
+
.add(lights.point({ name: "cinematic hero front fill", position: [0.15, 1.45, 3.1], color: "#e8f2ff", intensity: 5.2 }))
|
|
6222
6157
|
.add(effects.rain({ intensity: 0.46, color: "#c3e6ff" }))
|
|
6223
6158
|
.add(effects.fog({ density: 0.08, color: "#32435a" }))
|
|
6224
6159
|
.add(effects.bloom({ intensity: 0.36, color: "#6edfff" }))
|
|
@@ -6719,10 +6654,137 @@ export function createAuraApp(target, options) {
|
|
|
6719
6654
|
let disposed = false;
|
|
6720
6655
|
let animationHandle = 0;
|
|
6721
6656
|
let productionController;
|
|
6657
|
+
/** WS-2.9: true from the moment a WebGL mount starts until the controller arrives or fails. */
|
|
6658
|
+
let productionMountPending = false;
|
|
6659
|
+
/**
|
|
6660
|
+
* WS-2.5 — true when a WebGL mount was attempted and FAILED for this scene.
|
|
6661
|
+
*
|
|
6662
|
+
* Distinct from `productionMountPending`, and the distinction is the whole point. After a failed mount
|
|
6663
|
+
* both the pending flag and the controller are absent, which used to be indistinguishable from "this
|
|
6664
|
+
* scene never wanted WebGL" — so `step()` and the render loop fell through to the Canvas-2D branch and
|
|
6665
|
+
* painted a gradient over a scene whose renderer had just failed. Measured: 16,384 lit pixels on a
|
|
6666
|
+
* 128x128 canvas, i.e. the entire surface, with the real error sitting in `diagnostics().errors` where
|
|
6667
|
+
* nobody was looking.
|
|
6668
|
+
*/
|
|
6669
|
+
let productionMountFailed = false;
|
|
6670
|
+
/**
|
|
6671
|
+
* Settles when the in-flight WebGL mount finishes, successfully or not. Backs `app.ready()`.
|
|
6672
|
+
*
|
|
6673
|
+
* Resolved rather than pending when no mount is in flight, so `await app.ready()` is safe to call at
|
|
6674
|
+
* any time and on any scene.
|
|
6675
|
+
*/
|
|
6676
|
+
let productionMountSettled = Promise.resolve();
|
|
6677
|
+
/** Lets `dispose()` settle an in-flight mount; see the note there. */
|
|
6678
|
+
let settleMountForDispose = () => undefined;
|
|
6679
|
+
/*
|
|
6680
|
+
* WS-2.6 — device-loss subscribers, held here rather than pushed straight at the controller.
|
|
6681
|
+
*
|
|
6682
|
+
* The renderer mounts asynchronously, so a developer writing `app.onDeviceLost(...)` on the line after
|
|
6683
|
+
* `createAuraApp` has no controller to attach to yet. Making them await `ready()` first would be the
|
|
6684
|
+
* same trap WS-2.9 fixed: an API that silently does nothing depending on timing. So subscriptions are
|
|
6685
|
+
* recorded and attached when the device arrives, and on every re-mount from `setScene`.
|
|
6686
|
+
*/
|
|
6687
|
+
/*
|
|
6688
|
+
* Keyed BY LISTENER, not a flat array — and that detail is load-bearing.
|
|
6689
|
+
*
|
|
6690
|
+
* A flat list of unsubscribe functions looks equivalent and is not. `attachDeviceListeners` re-attaches
|
|
6691
|
+
* every held listener on each mount, so a listener registered before the mount ends up with **two**
|
|
6692
|
+
* controller subscriptions: the one its own `onDeviceLost` call created, and the one the mount handler
|
|
6693
|
+
* created. Its returned unsubscribe closure only knew about the first, so calling it left the second
|
|
6694
|
+
* live and the listener still fired.
|
|
6695
|
+
*
|
|
6696
|
+
* Measured by the WS-2.6 test's final assertion: after `unsubscribe()`, a second context loss still
|
|
6697
|
+
* incremented the counter — 2 where 1 was expected. A map from listener to its current subscription
|
|
6698
|
+
* makes unsubscribing complete regardless of how many mounts have happened.
|
|
6699
|
+
*/
|
|
6700
|
+
const deviceLostListeners = new Map();
|
|
6701
|
+
const deviceRestoredListeners = new Map();
|
|
6702
|
+
const attachDeviceListeners = (controller) => {
|
|
6703
|
+
for (const [listener, unsubscribe] of deviceLostListeners) {
|
|
6704
|
+
unsubscribe?.();
|
|
6705
|
+
deviceLostListeners.set(listener, controller.onDeviceLost?.(listener));
|
|
6706
|
+
}
|
|
6707
|
+
for (const [listener, unsubscribe] of deviceRestoredListeners) {
|
|
6708
|
+
unsubscribe?.();
|
|
6709
|
+
deviceRestoredListeners.set(listener, controller.onDeviceRestored?.(listener));
|
|
6710
|
+
}
|
|
6711
|
+
};
|
|
6722
6712
|
let lastTime = 0;
|
|
6723
6713
|
let mountRevision = 0;
|
|
6724
6714
|
let canvasRuntimePhysics;
|
|
6725
6715
|
const runtimeNodes = createAuraRuntimeNodeRegistry(renderSnapshot);
|
|
6716
|
+
// One world owned by the app, so `app.physics` is live whether or not the scene declared
|
|
6717
|
+
// any bodies. A route that spawns everything at runtime (a shooter, a stacking puzzle) is
|
|
6718
|
+
// then no harder to write than one that declares bodies up front.
|
|
6719
|
+
const appPhysicsNodeNames = new Map();
|
|
6720
|
+
/*
|
|
6721
|
+
* The physics world is created on first use, not on app construction.
|
|
6722
|
+
*
|
|
6723
|
+
* `app.physics` must be live for every app — a route that spawns bodies at runtime should be no
|
|
6724
|
+
* harder to write than one that declares them. But constructing the world eagerly made **every**
|
|
6725
|
+
* app pay for the selected physical solver.
|
|
6726
|
+
*
|
|
6727
|
+
* Measured cost of the eager version: a minimal `createAuraApp` scene containing one box bundled to
|
|
6728
|
+
* The eager version made the physical runtime part of every route's startup path, including
|
|
6729
|
+
* routes that never created a body.
|
|
6730
|
+
*
|
|
6731
|
+
* Lazily constructing it changes no behaviour — `app.physics` still returns a working runtime, and
|
|
6732
|
+
* a scene that declares `.physics({...})` still gets its bodies registered, because
|
|
6733
|
+
* `registerDeclaredBodies` asks for the world and therefore creates it. What changes is that a
|
|
6734
|
+
* scene with no physics never instantiates a solver, so a bundler can drop it.
|
|
6735
|
+
*/
|
|
6736
|
+
let appPhysicsWorldInstance;
|
|
6737
|
+
const appPhysicsWorld = () => {
|
|
6738
|
+
appPhysicsWorldInstance ??= new PhysicsWorld({
|
|
6739
|
+
gravity: options.physics?.gravity ? [...options.physics.gravity] : [0, -9.81, 0],
|
|
6740
|
+
fixedDelta: 1 / 60,
|
|
6741
|
+
enableSleeping: true
|
|
6742
|
+
});
|
|
6743
|
+
return appPhysicsWorldInstance;
|
|
6744
|
+
};
|
|
6745
|
+
let appPhysicsInstance;
|
|
6746
|
+
/** Lazy `app.physics`. Identity is stable: the same runtime is returned on every access. */
|
|
6747
|
+
const appPhysics = () => {
|
|
6748
|
+
appPhysicsInstance ??= createPhysicsRuntime(appPhysicsWorld(), {
|
|
6749
|
+
...(options.physics?.layers ? { layers: options.physics.layers } : {}),
|
|
6750
|
+
nodeNameFor: (bodyId) => appPhysicsNodeNames.get(bodyId)
|
|
6751
|
+
});
|
|
6752
|
+
return appPhysicsInstance;
|
|
6753
|
+
};
|
|
6754
|
+
/**
|
|
6755
|
+
* Register scene-declared bodies into the app-owned world.
|
|
6756
|
+
*
|
|
6757
|
+
* Runs on construction and on every `setScene`, so a declared node name resolves through
|
|
6758
|
+
* `app.physics.bodies.require(name)` without the route restating the body.
|
|
6759
|
+
*/
|
|
6760
|
+
const registerDeclaredBodies = (target) => {
|
|
6761
|
+
// Ask the question before creating the world, so a scene with no declared bodies never does.
|
|
6762
|
+
const declaresPhysics = target.nodes.some((node) => (node.kind === "model" || node.kind === "primitive") && Boolean(node.physics));
|
|
6763
|
+
if (!declaresPhysics)
|
|
6764
|
+
return;
|
|
6765
|
+
const world = appPhysicsWorld();
|
|
6766
|
+
for (const node of target.nodes) {
|
|
6767
|
+
if ((node.kind !== "model" && node.kind !== "primitive") || !node.physics)
|
|
6768
|
+
continue;
|
|
6769
|
+
const spec = node.physics;
|
|
6770
|
+
const body = world.createRigidBody({
|
|
6771
|
+
type: spec.type ?? "dynamic",
|
|
6772
|
+
position: node.position ?? [0, 0, 0],
|
|
6773
|
+
rotation: eulerToQuat(node.rotation ?? [0, 0, 0]),
|
|
6774
|
+
...(spec.mass === undefined ? {} : { mass: spec.mass }),
|
|
6775
|
+
...(spec.friction === undefined ? {} : { friction: spec.friction }),
|
|
6776
|
+
...(spec.restitution === undefined ? {} : { restitution: spec.restitution })
|
|
6777
|
+
});
|
|
6778
|
+
world.createCollider(body, {
|
|
6779
|
+
shape: resolveNodePhysicsShape(node, spec),
|
|
6780
|
+
...(spec.sensor === undefined ? {} : { sensor: spec.sensor }),
|
|
6781
|
+
material: { friction: spec.friction ?? 0.5, restitution: spec.restitution ?? 0 }
|
|
6782
|
+
});
|
|
6783
|
+
if (node.name)
|
|
6784
|
+
appPhysicsNodeNames.set(body.id, node.name);
|
|
6785
|
+
}
|
|
6786
|
+
};
|
|
6787
|
+
registerDeclaredBodies(renderSnapshot);
|
|
6726
6788
|
const frameCallbacks = new Set();
|
|
6727
6789
|
let runtimePaused = options.autoStart === false;
|
|
6728
6790
|
let runtimeFrame = 0;
|
|
@@ -6775,7 +6837,16 @@ export function createAuraApp(target, options) {
|
|
|
6775
6837
|
}
|
|
6776
6838
|
diagnosticsState.evidence = collectAuraSceneEvidence(renderSnapshot);
|
|
6777
6839
|
diagnosticsState.fps = Math.round(1000 / delta);
|
|
6778
|
-
|
|
6840
|
+
/*
|
|
6841
|
+
* WS-2.5 — the live loop must respect the same rule as `step()`.
|
|
6842
|
+
*
|
|
6843
|
+
* Fixing only `step()` would have left the gradient reachable through `autoStart`, which is the path
|
|
6844
|
+
* most routes take. A scene whose WebGL mount is pending or has failed gets nothing drawn; the reason
|
|
6845
|
+
* is already recorded in `warnings`/`errors`.
|
|
6846
|
+
*/
|
|
6847
|
+
diagnosticsState.drawCalls = productionMountPending || productionMountFailed
|
|
6848
|
+
? 0
|
|
6849
|
+
: renderDiagnosticPreviewToCanvas(canvas, renderSnapshot, time);
|
|
6779
6850
|
if (canvas)
|
|
6780
6851
|
diagnosticsState.renderSize = [canvas.width, canvas.height];
|
|
6781
6852
|
overlay?.update();
|
|
@@ -6793,24 +6864,87 @@ export function createAuraApp(target, options) {
|
|
|
6793
6864
|
productionController = undefined;
|
|
6794
6865
|
lastTime = 0;
|
|
6795
6866
|
const shouldUseProductionRenderer = shouldUseProductionRendererForCurrentScene();
|
|
6867
|
+
/*
|
|
6868
|
+
* WS-2.5 — a scene with renderable content must never fall to the diagnostic 2D path.
|
|
6869
|
+
*
|
|
6870
|
+
* `shouldUseProductionRendererForCurrentScene()` is false in two very different situations, and the
|
|
6871
|
+
* old code treated them the same: a scene with nothing renderable in it (fine, nothing to draw), and
|
|
6872
|
+
* a scene *with* renderable nodes that could not reach WebGL — no canvas, or no `window`. The second
|
|
6873
|
+
* used to silently produce a gradient schematic, which looks like a render and is not.
|
|
6874
|
+
*
|
|
6875
|
+
* So the two cases are separated. A renderable scene with no usable canvas now raises a diagnosable
|
|
6876
|
+
* error naming the cause, instead of drawing something plausible.
|
|
6877
|
+
*/
|
|
6878
|
+
const declaresRenderableContent = renderSnapshot.nodes.some(isWebGLRenderableNode);
|
|
6879
|
+
/*
|
|
6880
|
+
* Scoped to the case that is actually a lie: a canvas WAS supplied, so the caller is looking at
|
|
6881
|
+
* pixels, and the scene has renderable content — but WebGL declined it, so those pixels would be a
|
|
6882
|
+
* gradient schematic.
|
|
6883
|
+
*
|
|
6884
|
+
* Deliberately NOT extended to `canvas === undefined`. Constructing an app with no canvas is a
|
|
6885
|
+
* legitimate and widely used pattern — 18 tests across `tests/unit/agent-api` and
|
|
6886
|
+
* `tests/unit/rendering` do it to exercise scene, runtime and physics behaviour headlessly, and
|
|
6887
|
+
* `createAuraApp(undefined, ...)` is how `lazy-physics-world.test.ts` checks `app.physics`. Throwing
|
|
6888
|
+
* there would break working semantics to satisfy a rendering rule, which R7 forbids. Those callers
|
|
6889
|
+
* are not being shown a misleading frame; they are not being shown a frame at all, and
|
|
6890
|
+
* `diagnostics().backend` reports `"headless"`.
|
|
6891
|
+
*/
|
|
6892
|
+
if (declaresRenderableContent && canvas && !shouldUseProductionRenderer) {
|
|
6893
|
+
throw new AuraRuntimeError("backend-fallback", "Aura3D cannot render this scene on the canvas you supplied: it has renderable nodes but WebGL2 is unavailable in this context. It will NOT fall back to the Canvas 2D diagnostic preview, because that draws a gradient schematic rather than your scene and has silently hidden defects before — world labels once reached the scene graph but were drawn only in that path. Suggested fix: run in a browser context with WebGL2 available, or inspect diagnostics().errors for the underlying device failure.");
|
|
6894
|
+
}
|
|
6796
6895
|
const backend = shouldUseProductionRenderer ? "webgl2" : canvas ? "canvas2d" : "headless";
|
|
6797
6896
|
resetDiagnosticsForCurrentScene(backend);
|
|
6798
6897
|
canvasRuntimePhysics = shouldUseProductionRenderer ? undefined : createRuntimeScenePhysics(renderSnapshot);
|
|
6799
6898
|
overlay?.update();
|
|
6800
6899
|
const revision = ++mountRevision;
|
|
6900
|
+
/*
|
|
6901
|
+
* WS-2.9 — record that a WebGL mount is in flight.
|
|
6902
|
+
*
|
|
6903
|
+
* `startProductionRender` is async, so between `createAuraApp` returning and the controller
|
|
6904
|
+
* arriving there is a window in which `step()` used to fall through to the Canvas-2D `render()`
|
|
6905
|
+
* path and draw nothing for a WebGL scene — silently, with `drawCalls: 0`, `warnings: []` and
|
|
6906
|
+
* `errors: []`. `step(dt)` is the documented deterministic entry point, so a developer writing a
|
|
6907
|
+
* headless capture got a blank image and no explanation.
|
|
6908
|
+
*/
|
|
6909
|
+
productionMountPending = shouldUseProductionRenderer && Boolean(canvas);
|
|
6910
|
+
productionMountFailed = false;
|
|
6911
|
+
let settleMount = () => undefined;
|
|
6912
|
+
productionMountSettled = productionMountPending
|
|
6913
|
+
? new Promise((resolveSettled) => { settleMount = resolveSettled; })
|
|
6914
|
+
: Promise.resolve();
|
|
6915
|
+
settleMountForDispose = settleMount;
|
|
6801
6916
|
if (shouldUseProductionRenderer && canvas) {
|
|
6802
|
-
void startProductionRender(canvas, renderSnapshot, diagnosticsState, options, overlay, runRuntimeFrame, () => runtimePaused, runtimeNodes
|
|
6917
|
+
void startProductionRender(canvas, renderSnapshot, diagnosticsState, options, overlay, runRuntimeFrame, () => runtimePaused, runtimeNodes,
|
|
6918
|
+
// A route that registered a frame callback, or that has bodies to simulate, needs
|
|
6919
|
+
// frames regardless of what the scene declares.
|
|
6920
|
+
/*
|
|
6921
|
+
* Must not *create* a world just to ask whether one exists.
|
|
6922
|
+
* Reading `appPhysicsWorldInstance` directly keeps the lazy construction lazy; calling
|
|
6923
|
+
* `appPhysicsWorld()` here would instantiate the solver on the first render of every scene
|
|
6924
|
+
* and defeat the whole point.
|
|
6925
|
+
*/
|
|
6926
|
+
() => frameCallbacks.size > 0 || (appPhysicsWorldInstance?.bodies().length ?? 0) > 0,
|
|
6927
|
+
// Paused frames render at the app's own simulated clock, so a held frame really is held.
|
|
6928
|
+
() => runtimeTime * 1000)
|
|
6803
6929
|
.then((controller) => {
|
|
6804
6930
|
if (disposed || revision !== mountRevision) {
|
|
6931
|
+
productionMountPending = false;
|
|
6932
|
+
settleMount();
|
|
6805
6933
|
controller.dispose();
|
|
6806
6934
|
return;
|
|
6807
6935
|
}
|
|
6808
6936
|
productionController = controller;
|
|
6937
|
+
productionMountPending = false;
|
|
6938
|
+
attachDeviceListeners(controller);
|
|
6939
|
+
settleMount();
|
|
6809
6940
|
markRouteReady(snapshot, diagnosticsState);
|
|
6810
6941
|
})
|
|
6811
6942
|
.catch((error) => {
|
|
6812
6943
|
if (disposed || revision !== mountRevision)
|
|
6813
6944
|
return;
|
|
6945
|
+
productionMountPending = false;
|
|
6946
|
+
productionMountFailed = true;
|
|
6947
|
+
settleMount();
|
|
6814
6948
|
diagnosticsState.backend = "webgl2";
|
|
6815
6949
|
diagnosticsState.errors.push(productionRenderErrorMessage(error));
|
|
6816
6950
|
overlay?.update();
|
|
@@ -6822,7 +6956,7 @@ export function createAuraApp(target, options) {
|
|
|
6822
6956
|
markRouteReady(snapshot, diagnosticsState);
|
|
6823
6957
|
};
|
|
6824
6958
|
mountCurrentScene();
|
|
6825
|
-
|
|
6959
|
+
const app = {
|
|
6826
6960
|
canvas,
|
|
6827
6961
|
get scene() {
|
|
6828
6962
|
return snapshot;
|
|
@@ -6834,9 +6968,24 @@ export function createAuraApp(target, options) {
|
|
|
6834
6968
|
snapshot = normalizeSceneSnapshot(nextScene);
|
|
6835
6969
|
renderSnapshot = flattenSceneSnapshot(snapshot);
|
|
6836
6970
|
runtimeNodes.reset(renderSnapshot);
|
|
6971
|
+
// Drop bodies from the previous scene before registering the new ones, so swapping
|
|
6972
|
+
// scenes does not leave orphaned bodies colliding with the new level.
|
|
6973
|
+
// Only a world that exists can hold stale bodies; `setScene` on a physics-free app is a no-op here.
|
|
6974
|
+
for (const id of [...appPhysicsNodeNames.keys()])
|
|
6975
|
+
appPhysicsWorldInstance?.removeRigidBody(id);
|
|
6976
|
+
appPhysicsNodeNames.clear();
|
|
6977
|
+
registerDeclaredBodies(renderSnapshot);
|
|
6837
6978
|
mountCurrentScene();
|
|
6838
6979
|
},
|
|
6839
6980
|
nodes: runtimeNodes,
|
|
6981
|
+
/*
|
|
6982
|
+
* A getter, so touching `app.physics` is what constructs the world.
|
|
6983
|
+
* Declared as a getter rather than an eager property because the property access itself is the
|
|
6984
|
+
* signal that a route intends to use physics.
|
|
6985
|
+
*/
|
|
6986
|
+
get physics() {
|
|
6987
|
+
return appPhysics();
|
|
6988
|
+
},
|
|
6840
6989
|
get runtime() {
|
|
6841
6990
|
return {
|
|
6842
6991
|
paused: runtimePaused,
|
|
@@ -6863,23 +7012,112 @@ export function createAuraApp(target, options) {
|
|
|
6863
7012
|
pause() {
|
|
6864
7013
|
runtimePaused = true;
|
|
6865
7014
|
},
|
|
7015
|
+
resetRuntimeClock() {
|
|
7016
|
+
/*
|
|
7017
|
+
* Rewind the frame counter and elapsed time to zero.
|
|
7018
|
+
*
|
|
7019
|
+
* Needed for reproducible capture. Routes animate from accumulated `time` — Data Galaxy sets
|
|
7020
|
+
* `setRotation(time * 0.16, ...)` — so a scene paused after 180 frames of real-time loading
|
|
7021
|
+
* looks different from the same scene paused after 240, even though both were then advanced by
|
|
7022
|
+
* an identical number of fixed steps. Zeroing the clock first makes "settle to N steps" name
|
|
7023
|
+
* exactly one state.
|
|
7024
|
+
*
|
|
7025
|
+
* Not part of `AuraApp`: this is a capture/testing seam, reached through `auraAppRegistry`
|
|
7026
|
+
* rather than offered as gameplay API, because rewinding a live game's clock mid-session is
|
|
7027
|
+
* not something a route should be encouraged to do.
|
|
7028
|
+
*/
|
|
7029
|
+
runtimeFrame = 0;
|
|
7030
|
+
runtimeTime = 0;
|
|
7031
|
+
runtimeAlpha = 0;
|
|
7032
|
+
lastTime = 0;
|
|
7033
|
+
},
|
|
6866
7034
|
resume() {
|
|
6867
7035
|
runtimePaused = false;
|
|
6868
7036
|
if (!animationHandle && !productionController && options.autoStart !== false && typeof requestAnimationFrame !== "undefined") {
|
|
6869
7037
|
animationHandle = requestAnimationFrame(render);
|
|
6870
7038
|
}
|
|
6871
7039
|
},
|
|
7040
|
+
async ready() {
|
|
7041
|
+
// Resolves when the in-flight mount settles; immediate when there is none. See AuraApp.ready.
|
|
7042
|
+
await productionMountSettled;
|
|
7043
|
+
},
|
|
7044
|
+
onDeviceLost(listener) {
|
|
7045
|
+
// Attach now when a device already exists; otherwise the mount handler attaches it on arrival.
|
|
7046
|
+
deviceLostListeners.set(listener, productionController?.onDeviceLost?.(listener));
|
|
7047
|
+
return () => {
|
|
7048
|
+
// Read the CURRENT subscription: a re-mount may have replaced the one captured at registration.
|
|
7049
|
+
deviceLostListeners.get(listener)?.();
|
|
7050
|
+
deviceLostListeners.delete(listener);
|
|
7051
|
+
};
|
|
7052
|
+
},
|
|
7053
|
+
onDeviceRestored(listener) {
|
|
7054
|
+
deviceRestoredListeners.set(listener, productionController?.onDeviceRestored?.(listener));
|
|
7055
|
+
return () => {
|
|
7056
|
+
deviceRestoredListeners.get(listener)?.();
|
|
7057
|
+
deviceRestoredListeners.delete(listener);
|
|
7058
|
+
};
|
|
7059
|
+
},
|
|
7060
|
+
deviceLost() {
|
|
7061
|
+
return productionController?.deviceLost?.() ?? false;
|
|
7062
|
+
},
|
|
6872
7063
|
step(dt = 1 / 60) {
|
|
6873
7064
|
const seconds = Math.max(0, dt);
|
|
6874
7065
|
runRuntimeFrame(seconds, "manual");
|
|
6875
7066
|
canvasRuntimePhysics?.step(seconds);
|
|
6876
7067
|
const previousPaused = runtimePaused;
|
|
6877
7068
|
runtimePaused = true;
|
|
7069
|
+
/*
|
|
7070
|
+
* Render at the *simulated* time, not wall-clock time.
|
|
7071
|
+
*
|
|
7072
|
+
* `step(dt)` is the deterministic entry point — a caller advances a fixed amount and expects a
|
|
7073
|
+
* reproducible frame. Passing `performanceNow()` to the renderer defeated that: any
|
|
7074
|
+
* time-driven shader or effect sampled real elapsed milliseconds, so the same `step` sequence
|
|
7075
|
+
* produced a different image on every run.
|
|
7076
|
+
*
|
|
7077
|
+
* Measured while making screenshot approval satisfiable: after zeroing the runtime clock and
|
|
7078
|
+
* stepping identically, 6 of 29 screenshots still drifted perceptually — all of them routes
|
|
7079
|
+
* with time-animated rendering (particle lab, material inspector). Feeding `runtimeTime`
|
|
7080
|
+
* instead makes `step` mean one thing.
|
|
7081
|
+
*
|
|
7082
|
+
* The live `render()` loop still uses wall-clock time, which is correct for real playback.
|
|
7083
|
+
*/
|
|
7084
|
+
const simulatedMs = runtimeTime * 1000;
|
|
6878
7085
|
if (productionController) {
|
|
6879
|
-
productionController.render(
|
|
7086
|
+
productionController.render(simulatedMs);
|
|
7087
|
+
}
|
|
7088
|
+
else if (productionMountPending || productionMountFailed) {
|
|
7089
|
+
/*
|
|
7090
|
+
* WS-2.9 — a WebGL mount is in flight, so there is nothing correct to draw yet.
|
|
7091
|
+
*
|
|
7092
|
+
* Falling through to the Canvas-2D `render()` branch below would draw a gradient for a scene
|
|
7093
|
+
* that has a WebGL renderer coming, which is worse than drawing nothing: it produces a frame
|
|
7094
|
+
* that looks like a real render and is not one. Measured before this fix: eight synchronous
|
|
7095
|
+
* `step(1/60)` calls after construction gave `drawCalls: 0`, a fully blank canvas,
|
|
7096
|
+
* `backend: "webgl2"` and — the actual defect — **empty `warnings` and `errors`**. One
|
|
7097
|
+
* `await requestAnimationFrame` first gave 58,480 lit pixels.
|
|
7098
|
+
*
|
|
7099
|
+
* So this reports rather than renders. The warning is actionable and names both the cause and
|
|
7100
|
+
* the fix, because a developer writing a headless capture has no way to guess that a
|
|
7101
|
+
* documented deterministic entry point depends on an animation frame having elapsed.
|
|
7102
|
+
*/
|
|
7103
|
+
const pendingWarning = productionMountFailed
|
|
7104
|
+
? "Aura3D step() rendered nothing because the WebGL renderer failed to mount for this scene. It will NOT fall back to the Canvas 2D diagnostic preview, because that paints a gradient schematic over a scene whose renderer just failed. Suggested fix: read diagnostics().errors for the underlying device failure."
|
|
7105
|
+
: "Aura3D step() was called before the WebGL renderer finished mounting, so this frame rendered nothing. The production renderer mounts asynchronously. Suggested fix: await one animation frame — `await new Promise(requestAnimationFrame)` — or await `app.ready()` before stepping, then call step() as normal.";
|
|
7106
|
+
if (!diagnosticsState.warnings.includes(pendingWarning))
|
|
7107
|
+
diagnosticsState.warnings.push(pendingWarning);
|
|
7108
|
+
diagnosticsState.drawCalls = 0;
|
|
7109
|
+
overlay?.update();
|
|
6880
7110
|
}
|
|
6881
7111
|
else {
|
|
6882
|
-
|
|
7112
|
+
/*
|
|
7113
|
+
* `render` records `lastTime` to derive its own delta. Feeding it simulated time would leave
|
|
7114
|
+
* `lastTime` in a different clock from `performanceNow()`, so the first frame after a
|
|
7115
|
+
* `resume()` would compute a wildly wrong delta — a visible jump. Restore it afterwards so
|
|
7116
|
+
* `step` stays side-effect-free with respect to live playback.
|
|
7117
|
+
*/
|
|
7118
|
+
const previousLastTime = lastTime;
|
|
7119
|
+
render(simulatedMs);
|
|
7120
|
+
lastTime = previousLastTime;
|
|
6883
7121
|
}
|
|
6884
7122
|
runtimePaused = previousPaused;
|
|
6885
7123
|
},
|
|
@@ -6903,6 +7141,16 @@ export function createAuraApp(target, options) {
|
|
|
6903
7141
|
},
|
|
6904
7142
|
dispose() {
|
|
6905
7143
|
disposed = true;
|
|
7144
|
+
/*
|
|
7145
|
+
* Settle any in-flight mount so `await app.ready()` cannot hang on a disposed app. A promise
|
|
7146
|
+
* that never resolves is a worse failure than the one WS-2.9 fixed: it has no diagnostic at all.
|
|
7147
|
+
*/
|
|
7148
|
+
productionMountPending = false;
|
|
7149
|
+
settleMountForDispose();
|
|
7150
|
+
for (const unsubscribe of [...deviceLostListeners.values(), ...deviceRestoredListeners.values()])
|
|
7151
|
+
unsubscribe?.();
|
|
7152
|
+
deviceLostListeners.clear();
|
|
7153
|
+
deviceRestoredListeners.clear();
|
|
6906
7154
|
if (animationHandle && typeof cancelAnimationFrame !== "undefined")
|
|
6907
7155
|
cancelAnimationFrame(animationHandle);
|
|
6908
7156
|
for (const controller of ownedInputControllers)
|
|
@@ -6910,9 +7158,77 @@ export function createAuraApp(target, options) {
|
|
|
6910
7158
|
ownedInputControllers.clear();
|
|
6911
7159
|
productionController?.dispose();
|
|
6912
7160
|
overlay?.dispose();
|
|
7161
|
+
unregisterAuraApp(app);
|
|
6913
7162
|
}
|
|
6914
7163
|
};
|
|
7164
|
+
registerAuraApp(app);
|
|
7165
|
+
return app;
|
|
6915
7166
|
}
|
|
7167
|
+
/**
|
|
7168
|
+
* Live apps on this page, so tooling can act on a scene it did not create.
|
|
7169
|
+
*
|
|
7170
|
+
* ## Why this exists
|
|
7171
|
+
*
|
|
7172
|
+
* Visual-approval and screenshot gates need to *freeze* a scene before capturing it. Without a way
|
|
7173
|
+
* to reach the running app, they cannot: a route creates its app in module scope and typically never
|
|
7174
|
+
* exposes the handle, so an automated capture can only `waitForTimeout` and photograph whatever frame
|
|
7175
|
+
* the loop happened to reach.
|
|
7176
|
+
*
|
|
7177
|
+
* Measured consequence, before this existed: re-running the showcase screenshot spec with **no code
|
|
7178
|
+
* change** produced different bytes for 14 of 29 screenshots, because most routes animate and print
|
|
7179
|
+
* live telemetry. Any gate binding approval to a screenshot hash was therefore unsatisfiable — every
|
|
7180
|
+
* regeneration invalidated a still-correct signature.
|
|
7181
|
+
*
|
|
7182
|
+
* Deliberately a `Set` of handles rather than a global "the app", because a page may legitimately
|
|
7183
|
+
* mount several. Entries are removed on `dispose()` so a long-lived page does not leak.
|
|
7184
|
+
*/
|
|
7185
|
+
const liveAuraApps = new Set();
|
|
7186
|
+
function registerAuraApp(app) {
|
|
7187
|
+
liveAuraApps.add(app);
|
|
7188
|
+
if (typeof globalThis !== "undefined") {
|
|
7189
|
+
globalThis.__AURA3D_LIVE_APPS__ = auraAppRegistry;
|
|
7190
|
+
}
|
|
7191
|
+
}
|
|
7192
|
+
function unregisterAuraApp(app) {
|
|
7193
|
+
liveAuraApps.delete(app);
|
|
7194
|
+
}
|
|
7195
|
+
export const auraAppRegistry = {
|
|
7196
|
+
kind: "aura3d-live-app-registry",
|
|
7197
|
+
count() {
|
|
7198
|
+
return liveAuraApps.size;
|
|
7199
|
+
},
|
|
7200
|
+
pauseAll() {
|
|
7201
|
+
for (const app of liveAuraApps)
|
|
7202
|
+
app.pause();
|
|
7203
|
+
return liveAuraApps.size;
|
|
7204
|
+
},
|
|
7205
|
+
resumeAll() {
|
|
7206
|
+
for (const app of liveAuraApps)
|
|
7207
|
+
app.resume();
|
|
7208
|
+
return liveAuraApps.size;
|
|
7209
|
+
},
|
|
7210
|
+
settle(steps = 30, dt = 1 / 60) {
|
|
7211
|
+
for (const app of liveAuraApps) {
|
|
7212
|
+
app.pause();
|
|
7213
|
+
/*
|
|
7214
|
+
* Rewind before stepping.
|
|
7215
|
+
*
|
|
7216
|
+
* Without this, `settle(30)` means "30 steps after however long the page took to load", which
|
|
7217
|
+
* differs run to run — and routes that animate from accumulated `time` then render a different
|
|
7218
|
+
* frame. Measured before this: 8 of 29 screenshots still drifted perceptually even with every
|
|
7219
|
+
* app paused and stepped identically.
|
|
7220
|
+
*/
|
|
7221
|
+
const resettable = app;
|
|
7222
|
+
resettable.resetRuntimeClock?.();
|
|
7223
|
+
for (let index = 0; index < Math.max(0, steps); index += 1)
|
|
7224
|
+
app.step(dt);
|
|
7225
|
+
}
|
|
7226
|
+
return liveAuraApps.size;
|
|
7227
|
+
},
|
|
7228
|
+
all() {
|
|
7229
|
+
return [...liveAuraApps];
|
|
7230
|
+
}
|
|
7231
|
+
};
|
|
6916
7232
|
export function createGameApp(target, options) {
|
|
6917
7233
|
const { input, loop, runtimeEvidence, ...appOptions } = options;
|
|
6918
7234
|
const app = createAuraApp(target, { ...appOptions, autoStart: false });
|
|
@@ -7168,6 +7484,14 @@ function worldLabelsFromSnapshot(snapshot, runtimeNodes) {
|
|
|
7168
7484
|
fontSize: Math.max(11, Math.min(26, Math.round((node.size ?? 0.34) * 46))),
|
|
7169
7485
|
leader: node.leader === true,
|
|
7170
7486
|
offscreenPolicy: node.offscreenPolicy ?? (node.label === "hud" ? "draw" : "clamp"),
|
|
7487
|
+
/*
|
|
7488
|
+
* WS-2.7 — carry the declared option through. This line is the fix.
|
|
7489
|
+
*
|
|
7490
|
+
* `occlusionAware` has defaulted to true on every label factory since before 1.6 and was never
|
|
7491
|
+
* read here, so `WorldLabel` never received it and the renderer had nothing to act on. The option
|
|
7492
|
+
* was documented, accepted, and inert.
|
|
7493
|
+
*/
|
|
7494
|
+
occlusionAware: node.occlusionAware ?? true,
|
|
7171
7495
|
...(node.label === "hud" && node.screenAnchor ? { screenAnchor: node.screenAnchor } : {}),
|
|
7172
7496
|
...(node.label === "hud" && !node.screenAnchor ? { screenAnchor: "top-left" } : {})
|
|
7173
7497
|
};
|
|
@@ -7187,15 +7511,154 @@ function createSceneLabelLayer(canvas, snapshot) {
|
|
|
7187
7511
|
return undefined;
|
|
7188
7512
|
return createWorldLabelLayer({ container: canvas.parentElement ?? document.body, canvas });
|
|
7189
7513
|
}
|
|
7190
|
-
|
|
7514
|
+
/**
|
|
7515
|
+
* WS-2.7 — build the occlusion test for a scene's annotations.
|
|
7516
|
+
*
|
|
7517
|
+
* ## What it does
|
|
7518
|
+
*
|
|
7519
|
+
* Walks the segment from the camera eye to a label's anchor and asks whether any renderable node's
|
|
7520
|
+
* world-space box lies across it. If one does, the thing being annotated is behind geometry.
|
|
7521
|
+
*
|
|
7522
|
+
* ## Why boxes and not triangles
|
|
7523
|
+
*
|
|
7524
|
+
* An annotation only needs to know whether its subject is *hidden*, and a node's bounding box answers
|
|
7525
|
+
* that at a fraction of the cost of a mesh raycast — one slab test per node against one segment, no BVH,
|
|
7526
|
+
* no per-frame allocation of any size. A box is conservative: it can report occlusion slightly early at a
|
|
7527
|
+
* silhouette edge. For a label that is the right direction to be wrong in; the alternative, reporting a
|
|
7528
|
+
* label as visible when its subject is behind a wall, is the defect being fixed.
|
|
7529
|
+
*
|
|
7530
|
+
* ## What it deliberately skips
|
|
7531
|
+
*
|
|
7532
|
+
* The node the label is anchored *to*. Every annotation sits on or just outside its own subject's
|
|
7533
|
+
* surface, so including it would report every label as occluded by the very geometry it annotates —
|
|
7534
|
+
* the label-shaped version of z-fighting.
|
|
7535
|
+
*/
|
|
7536
|
+
function createSceneLabelOcclusionTest(snapshot, cameraEye, runtimeNodes) {
|
|
7537
|
+
const occluders = [];
|
|
7538
|
+
for (const node of groups.flatten(snapshot.nodes)) {
|
|
7539
|
+
if (node.kind !== "primitive" && !isRenderableModelNode(node))
|
|
7540
|
+
continue;
|
|
7541
|
+
if (node.visible === false)
|
|
7542
|
+
continue;
|
|
7543
|
+
const runtimeSnapshot = node.runtime?.id ? runtimeNodes?.get(node.runtime.id)?.snapshot() : undefined;
|
|
7544
|
+
if (runtimeSnapshot?.visible === false)
|
|
7545
|
+
continue;
|
|
7546
|
+
const position = (runtimeSnapshot?.position ?? node.position ?? [0, 0, 0]);
|
|
7547
|
+
const scale = (runtimeSnapshot?.scale ?? node.scale ?? [1, 1, 1]);
|
|
7548
|
+
/*
|
|
7549
|
+
* Local extents from the primitive's own geometry where available, and a unit box otherwise.
|
|
7550
|
+
* A model's real bounds come from its asset, which is not loaded synchronously here; the declared
|
|
7551
|
+
* scale is the honest approximation and is what the scene author controls.
|
|
7552
|
+
*/
|
|
7553
|
+
const localBounds = node.kind === "primitive" ? primitiveGeometryBounds(node) : undefined;
|
|
7554
|
+
const halfLocal = localBounds
|
|
7555
|
+
? [
|
|
7556
|
+
Math.max(1e-4, (localBounds.max[0] - localBounds.min[0]) / 2),
|
|
7557
|
+
Math.max(1e-4, (localBounds.max[1] - localBounds.min[1]) / 2),
|
|
7558
|
+
Math.max(1e-4, (localBounds.max[2] - localBounds.min[2]) / 2)
|
|
7559
|
+
]
|
|
7560
|
+
: [0.5, 0.5, 0.5];
|
|
7561
|
+
const half = [
|
|
7562
|
+
Math.abs(halfLocal[0] * scale[0]),
|
|
7563
|
+
Math.abs(halfLocal[1] * scale[1]),
|
|
7564
|
+
Math.abs(halfLocal[2] * scale[2])
|
|
7565
|
+
];
|
|
7566
|
+
occluders.push({
|
|
7567
|
+
min: [position[0] - half[0], position[1] - half[1], position[2] - half[2]],
|
|
7568
|
+
max: [position[0] + half[0], position[1] + half[1], position[2] + half[2]]
|
|
7569
|
+
});
|
|
7570
|
+
}
|
|
7571
|
+
if (occluders.length === 0)
|
|
7572
|
+
return () => false;
|
|
7573
|
+
/** Slab test: does the segment eye -> anchor enter this box strictly before reaching the anchor? */
|
|
7574
|
+
const segmentHitsBox = (anchor, box) => {
|
|
7575
|
+
let tEnter = 0;
|
|
7576
|
+
let tExit = 1;
|
|
7577
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
7578
|
+
const origin = cameraEye[axis];
|
|
7579
|
+
const direction = anchor[axis] - origin;
|
|
7580
|
+
const min = box.min[axis];
|
|
7581
|
+
const max = box.max[axis];
|
|
7582
|
+
if (Math.abs(direction) < 1e-9) {
|
|
7583
|
+
// Parallel to this slab: outside it means the segment can never be inside the box.
|
|
7584
|
+
if (origin < min || origin > max)
|
|
7585
|
+
return false;
|
|
7586
|
+
continue;
|
|
7587
|
+
}
|
|
7588
|
+
const t1 = (min - origin) / direction;
|
|
7589
|
+
const t2 = (max - origin) / direction;
|
|
7590
|
+
tEnter = Math.max(tEnter, Math.min(t1, t2));
|
|
7591
|
+
tExit = Math.min(tExit, Math.max(t1, t2));
|
|
7592
|
+
if (tEnter > tExit)
|
|
7593
|
+
return false;
|
|
7594
|
+
}
|
|
7595
|
+
/*
|
|
7596
|
+
* `tEnter` must be meaningfully before the anchor. A box the anchor merely touches at t ~= 1 is the
|
|
7597
|
+
* subject's own surface, not an occluder — the epsilon is what keeps a label from occluding itself.
|
|
7598
|
+
*/
|
|
7599
|
+
return tEnter > 1e-4 && tEnter < 1 - 1e-3;
|
|
7600
|
+
};
|
|
7601
|
+
return (anchor) => {
|
|
7602
|
+
for (const box of occluders) {
|
|
7603
|
+
// Skip the box the anchor is inside: that is the subject, and a subject cannot occlude its own label.
|
|
7604
|
+
const insideBox = anchor[0] >= box.min[0] && anchor[0] <= box.max[0]
|
|
7605
|
+
&& anchor[1] >= box.min[1] && anchor[1] <= box.max[1]
|
|
7606
|
+
&& anchor[2] >= box.min[2] && anchor[2] <= box.max[2];
|
|
7607
|
+
if (insideBox)
|
|
7608
|
+
continue;
|
|
7609
|
+
if (segmentHitsBox(anchor, box))
|
|
7610
|
+
return true;
|
|
7611
|
+
}
|
|
7612
|
+
return false;
|
|
7613
|
+
};
|
|
7614
|
+
}
|
|
7615
|
+
async function startProductionRender(canvas, snapshot, diagnosticsState, options, overlay, beforeRender, isPaused = () => false, runtimeNodes,
|
|
7616
|
+
/**
|
|
7617
|
+
* Extra reason to keep rendering, beyond what the scene declares.
|
|
7618
|
+
*
|
|
7619
|
+
* See {@link shouldContinuouslyRender}: scene-declared motion cannot know about a
|
|
7620
|
+
* simulation or a frame callback the route drives itself.
|
|
7621
|
+
*/
|
|
7622
|
+
requiresFrames = () => false,
|
|
7623
|
+
/**
|
|
7624
|
+
* Render clock to use while paused, in milliseconds.
|
|
7625
|
+
*
|
|
7626
|
+
* Supplied by the app so a paused scene renders at its own simulated time rather than at
|
|
7627
|
+
* wall-clock time. Without it, `pause()` froze the simulation but not time-driven rendering.
|
|
7628
|
+
*/
|
|
7629
|
+
pausedRenderTime = () => 0) {
|
|
7191
7630
|
const renderableNode = snapshot.nodes.find(isWebGLRenderableNode);
|
|
7192
7631
|
if (!renderableNode) {
|
|
7193
7632
|
throw new AuraRuntimeError("missing-asset", "Aura3D production rendering requires at least one typed model asset or primitive. Suggested fix: add model(assets.product), primitives.box(), primitives.sphere(), primitives.cylinder(), or primitives.plane().");
|
|
7194
7633
|
}
|
|
7195
7634
|
const renderer = await createProductionSceneRenderer(canvas, snapshot, options.renderer, runtimeNodes);
|
|
7196
7635
|
diagnosticsState.renderer = renderer.diagnostics;
|
|
7197
|
-
const
|
|
7636
|
+
const sceneWantsFrames = shouldContinuouslyRender(snapshot);
|
|
7198
7637
|
const labelLayer = createSceneLabelLayer(canvas, snapshot);
|
|
7638
|
+
const rendererSelection = normalizeCreateAppRendererOptions(options.renderer);
|
|
7639
|
+
const pixelRatio = options.pixelRatio ?? rendererSelection.profile.pixelRatio ?? devicePixelRatioSafe();
|
|
7640
|
+
const resizeRenderer = () => {
|
|
7641
|
+
if (options.resize === false)
|
|
7642
|
+
return;
|
|
7643
|
+
const parentRect = canvas.parentElement?.getBoundingClientRect();
|
|
7644
|
+
const cssWidth = parentRect?.width || canvas.clientWidth || (typeof window !== "undefined" ? window.innerWidth : 960) || 960;
|
|
7645
|
+
const cssHeight = parentRect?.height || canvas.clientHeight || (typeof window !== "undefined" ? window.innerHeight : 540) || 540;
|
|
7646
|
+
const width = Math.max(320, Math.round(cssWidth * pixelRatio));
|
|
7647
|
+
const height = Math.max(220, Math.round(cssHeight * pixelRatio));
|
|
7648
|
+
if (canvas.width === width && canvas.height === height)
|
|
7649
|
+
return;
|
|
7650
|
+
canvas.style.width = `${cssWidth}px`;
|
|
7651
|
+
canvas.style.height = `${cssHeight}px`;
|
|
7652
|
+
canvas.width = width;
|
|
7653
|
+
canvas.height = height;
|
|
7654
|
+
renderer.resize?.(width, height);
|
|
7655
|
+
};
|
|
7656
|
+
const resizeObserver = options.resize !== false && typeof ResizeObserver !== "undefined" && canvas.parentElement
|
|
7657
|
+
? new ResizeObserver(resizeRenderer)
|
|
7658
|
+
: undefined;
|
|
7659
|
+
resizeObserver?.observe(canvas.parentElement);
|
|
7660
|
+
if (options.resize !== false && typeof window !== "undefined")
|
|
7661
|
+
window.addEventListener("resize", resizeRenderer);
|
|
7199
7662
|
let disposed = false;
|
|
7200
7663
|
let animationHandle = 0;
|
|
7201
7664
|
let lastTime = 0;
|
|
@@ -7206,7 +7669,21 @@ async function startProductionRender(canvas, snapshot, diagnosticsState, options
|
|
|
7206
7669
|
lastTime = time;
|
|
7207
7670
|
if (!isPaused())
|
|
7208
7671
|
beforeRender?.(delta / 1000, "raf");
|
|
7209
|
-
|
|
7672
|
+
/*
|
|
7673
|
+
* A paused app must not keep advancing time-driven rendering.
|
|
7674
|
+
*
|
|
7675
|
+
* `isPaused()` gated the simulation callback but not the render clock, so `pause()` stopped
|
|
7676
|
+
* gameplay while particle emitters, animated materials and anything else keyed to elapsed time
|
|
7677
|
+
* carried on. Measured on the particle lab: with the app paused *and* settled to a fixed frame,
|
|
7678
|
+
* two screenshots taken 500 ms apart in the **same page load** still differed across 20.6% of the
|
|
7679
|
+
* vortex region. The frame was never actually held, so no capture of it could be reproducible.
|
|
7680
|
+
*
|
|
7681
|
+
* While paused, render is pinned to the time of the last simulated frame. `app.step()` advances
|
|
7682
|
+
* `runtimeTime` and renders explicitly, so stepping still produces new frames — what stops is
|
|
7683
|
+
* time passing on its own.
|
|
7684
|
+
*/
|
|
7685
|
+
const renderTime = isPaused() ? pausedRenderTime() : time;
|
|
7686
|
+
const drawCalls = renderer.render(renderTime);
|
|
7210
7687
|
diagnosticsState.backend = renderer.backend;
|
|
7211
7688
|
diagnosticsState.fps = diagnosticsState.fps || 60;
|
|
7212
7689
|
diagnosticsState.drawCalls = drawCalls;
|
|
@@ -7217,28 +7694,66 @@ async function startProductionRender(canvas, snapshot, diagnosticsState, options
|
|
|
7217
7694
|
// Reproject every frame against the renderer's own camera so labels track
|
|
7218
7695
|
// their anchors while the camera moves.
|
|
7219
7696
|
labelLayer.setLabels(worldLabelsFromSnapshot(snapshot, runtimeNodes));
|
|
7220
|
-
|
|
7697
|
+
/*
|
|
7698
|
+
* WS-2.7 — rebuilt per frame, because both the camera and any runtime-driven node can move. The
|
|
7699
|
+
* cost is one bounding box per renderable node, which is negligible beside the render itself, and
|
|
7700
|
+
* caching it would silently stop occluding as soon as anything moved.
|
|
7701
|
+
*/
|
|
7702
|
+
labelLayer.setOcclusionTest(createSceneLabelOcclusionTest(snapshot, resolveCameraEye(snapshot, snapshot.camera, renderTime, runtimeNodes), runtimeNodes));
|
|
7703
|
+
labelLayer.update(renderer.viewProjection(renderTime));
|
|
7221
7704
|
diagnosticsState.labels = labelLayer.snapshot();
|
|
7222
7705
|
}
|
|
7223
7706
|
overlay?.update();
|
|
7224
|
-
if (
|
|
7707
|
+
if ((sceneWantsFrames || requiresFrames()) && options.autoStart !== false && typeof requestAnimationFrame !== "undefined") {
|
|
7225
7708
|
animationHandle = requestAnimationFrame(renderFrame);
|
|
7226
7709
|
}
|
|
7227
7710
|
};
|
|
7228
7711
|
renderFrame();
|
|
7229
7712
|
return {
|
|
7230
7713
|
render(time = performanceNow()) {
|
|
7714
|
+
/*
|
|
7715
|
+
* Preserve `lastTime` across an explicit render.
|
|
7716
|
+
*
|
|
7717
|
+
* `renderFrame` records `lastTime` to derive its own delta and to drive `beforeRender`. When
|
|
7718
|
+
* `app.step()` renders at *simulated* time for reproducibility, leaving `lastTime` on the
|
|
7719
|
+
* simulated clock means the first live frame after `resume()` computes its delta against a
|
|
7720
|
+
* different clock — a visible jump. Same defect the canvas2d path had; fixed identically so
|
|
7721
|
+
* the two render paths cannot disagree about what `step` does.
|
|
7722
|
+
*/
|
|
7723
|
+
const previousLastTime = lastTime;
|
|
7231
7724
|
renderFrame(time);
|
|
7725
|
+
lastTime = previousLastTime;
|
|
7232
7726
|
},
|
|
7727
|
+
// WS-2.6 — forward device-loss subscription from whichever renderer backs this controller.
|
|
7728
|
+
onDeviceLost: renderer.onDeviceLost ? (listener) => renderer.onDeviceLost(listener) : undefined,
|
|
7729
|
+
onDeviceRestored: renderer.onDeviceRestored ? (listener) => renderer.onDeviceRestored(listener) : undefined,
|
|
7730
|
+
deviceLost: renderer.deviceLost ? () => renderer.deviceLost() : undefined,
|
|
7233
7731
|
dispose() {
|
|
7234
7732
|
disposed = true;
|
|
7235
7733
|
if (animationHandle && typeof cancelAnimationFrame !== "undefined")
|
|
7236
7734
|
cancelAnimationFrame(animationHandle);
|
|
7735
|
+
resizeObserver?.disconnect();
|
|
7736
|
+
if (typeof window !== "undefined")
|
|
7737
|
+
window.removeEventListener("resize", resizeRenderer);
|
|
7237
7738
|
labelLayer?.dispose();
|
|
7238
7739
|
renderer.dispose();
|
|
7239
7740
|
}
|
|
7240
7741
|
};
|
|
7241
7742
|
}
|
|
7743
|
+
/**
|
|
7744
|
+
* Whether the *scene* declares something that changes over time.
|
|
7745
|
+
*
|
|
7746
|
+
* Deliberately conservative: a fully static scene should render once and stop rather than
|
|
7747
|
+
* burn a core redrawing an identical frame.
|
|
7748
|
+
*
|
|
7749
|
+
* It is not sufficient on its own, and that was a real defect. This predicate can only see
|
|
7750
|
+
* what the scene *declares* — a looping timeline, a moving camera, an animated or mutable
|
|
7751
|
+
* node. It cannot see a physics simulation or a frame callback the route drives itself. So
|
|
7752
|
+
* an app whose only motion came from `app.physics.step()` inside `app.onFrame()` rendered
|
|
7753
|
+
* exactly **one** frame and froze: measured `frames: 1` with no console error, in a
|
|
7754
|
+
* clean-room project where every crate should have been falling. Callers therefore combine
|
|
7755
|
+
* this with their own `requiresFrames()`.
|
|
7756
|
+
*/
|
|
7242
7757
|
function shouldContinuouslyRender(snapshot) {
|
|
7243
7758
|
if (snapshot.timeline?.mode === "loop")
|
|
7244
7759
|
return true;
|
|
@@ -7262,9 +7777,7 @@ async function createProductionSceneRenderer(canvas, snapshot, rendererOptions,
|
|
|
7262
7777
|
const flattened = groups.flatten(snapshot.nodes);
|
|
7263
7778
|
const eligibility = analyzeProductionBridgeEligibility(flattened);
|
|
7264
7779
|
if (!eligibility.eligible) {
|
|
7265
|
-
|
|
7266
|
-
`Production bridge skipped: ${eligibility.reasons.join("; ")}.`
|
|
7267
|
-
], runtimeNodes);
|
|
7780
|
+
throw new AuraRuntimeError("backend-fallback", `Aura3D production renderer rejected this scene: ${eligibility.reasons.join("; ")}. Suggested fix: import models through generated typed aura-assets.`);
|
|
7268
7781
|
}
|
|
7269
7782
|
try {
|
|
7270
7783
|
return await createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOptions, runtimeNodes);
|
|
@@ -7277,7 +7790,57 @@ async function createProductionSceneRenderer(canvas, snapshot, rendererOptions,
|
|
|
7277
7790
|
}
|
|
7278
7791
|
let cachedProductionRuntimeFallbackLights;
|
|
7279
7792
|
function createProductionRuntimeEnvironment(snapshot) {
|
|
7280
|
-
const
|
|
7793
|
+
const nodes = groups.flatten(snapshot.nodes);
|
|
7794
|
+
const authoredEnvironment = nodes.find((node) => node.kind === "environment");
|
|
7795
|
+
if (authoredEnvironment) {
|
|
7796
|
+
const presetByEnvironment = {
|
|
7797
|
+
"studio": "studio",
|
|
7798
|
+
"material-lab": "inspection",
|
|
7799
|
+
"product-hero": "softbox",
|
|
7800
|
+
"night-cinematic": "evening",
|
|
7801
|
+
"metal-studio": "exhibit",
|
|
7802
|
+
"glass-studio": "softbox"
|
|
7803
|
+
};
|
|
7804
|
+
const preset = presetByEnvironment[authoredEnvironment.environment];
|
|
7805
|
+
const bundle = createExternalParityEnvironmentLighting(preset);
|
|
7806
|
+
const intensity = nonNegativeFinite(authoredEnvironment.intensity);
|
|
7807
|
+
const proceduralMap = bundle.lighting.proceduralMap;
|
|
7808
|
+
return {
|
|
7809
|
+
preset: authoredEnvironment.environment,
|
|
7810
|
+
intensity,
|
|
7811
|
+
evidence: `production bridge submitted the authored ${authoredEnvironment.environment} environment through generated HDR ${preset} lighting`,
|
|
7812
|
+
lighting: {
|
|
7813
|
+
...bundle.lighting,
|
|
7814
|
+
color: authoredEnvironment.color ? colorToLinearRgb(authoredEnvironment.color) : bundle.lighting.color,
|
|
7815
|
+
intensity: bundle.lighting.intensity * intensity,
|
|
7816
|
+
...(proceduralMap ? {
|
|
7817
|
+
proceduralMap: {
|
|
7818
|
+
...proceduralMap,
|
|
7819
|
+
intensity: proceduralMap.intensity * intensity,
|
|
7820
|
+
specularIntensity: proceduralMap.specularIntensity * intensity
|
|
7821
|
+
}
|
|
7822
|
+
} : {}),
|
|
7823
|
+
environmentMapIntensity: (bundle.lighting.environmentMapIntensity ?? 0) * intensity,
|
|
7824
|
+
environmentMapSpecularIntensity: (bundle.lighting.environmentMapSpecularIntensity ?? 0) * intensity
|
|
7825
|
+
}
|
|
7826
|
+
};
|
|
7827
|
+
}
|
|
7828
|
+
const ambientLights = nodes.filter((node) => node.kind === "light" && node.light === "ambient" && node.intensity > 0);
|
|
7829
|
+
if (ambientLights.length > 0) {
|
|
7830
|
+
const intensity = ambientLights.reduce((total, light) => total + light.intensity, 0);
|
|
7831
|
+
const color = ambientLights.reduce((sum, light) => {
|
|
7832
|
+
const linear = colorToLinearRgb(light.color ?? "#ffffff");
|
|
7833
|
+
const weight = light.intensity / intensity;
|
|
7834
|
+
return [sum[0] + linear[0] * weight, sum[1] + linear[1] * weight, sum[2] + linear[2] * weight];
|
|
7835
|
+
}, [0, 0, 0]);
|
|
7836
|
+
return {
|
|
7837
|
+
preset: "authored-ambient",
|
|
7838
|
+
intensity,
|
|
7839
|
+
evidence: `production bridge submitted ${ambientLights.length} authored ambient light${ambientLights.length === 1 ? "" : "s"} without an implicit environment map`,
|
|
7840
|
+
lighting: { color, intensity, environmentMapIntensity: 0, environmentMapSpecularIntensity: 0 }
|
|
7841
|
+
};
|
|
7842
|
+
}
|
|
7843
|
+
const names = nodes.map((node) => "name" in node ? node.name?.toLowerCase() ?? "" : "");
|
|
7281
7844
|
const category = resolveRendererSceneCategory(snapshot, names);
|
|
7282
7845
|
const preset = category === "city-day" ? "daylight"
|
|
7283
7846
|
: category === "city-night" ? "evening"
|
|
@@ -7306,7 +7869,7 @@ function createProductionRuntimeEnvironmentFog(snapshot) {
|
|
|
7306
7869
|
return false;
|
|
7307
7870
|
const density = clampNumber(fog.density ?? 0.12, 0, 1);
|
|
7308
7871
|
const intensity = clampNumber(fog.intensity ?? 0.5, 0, 1);
|
|
7309
|
-
const color =
|
|
7872
|
+
const color = colorToLinearRgb(fog.color ?? "#9fb7d9");
|
|
7310
7873
|
return {
|
|
7311
7874
|
// Exponential-squared reads as depth haze rather than a hard linear band, which
|
|
7312
7875
|
// matches what the public helper documents.
|
|
@@ -7320,16 +7883,8 @@ function createProductionRuntimeEnvironmentFog(snapshot) {
|
|
|
7320
7883
|
}
|
|
7321
7884
|
function createProductionRuntimePostprocess(snapshot) {
|
|
7322
7885
|
const nodes = groups.flatten(snapshot.nodes);
|
|
7323
|
-
const names = nodes.map((node) => "name" in node ? node.name?.toLowerCase() ?? "" : "");
|
|
7324
|
-
const category = resolveRendererSceneCategory(snapshot, names);
|
|
7325
7886
|
const authoredBloom = nodes.find((node) => node.kind === "effect" && node.effect === "bloom");
|
|
7326
|
-
const
|
|
7327
|
-
if (!("material" in node) || !node.material)
|
|
7328
|
-
return false;
|
|
7329
|
-
return Boolean(node.material.emissive) && (node.material.emissiveIntensity ?? 1) > 0;
|
|
7330
|
-
}).length;
|
|
7331
|
-
const darkScene = ["neon", "city-night", "space", "game"].includes(category);
|
|
7332
|
-
const bloomRequested = Boolean(authoredBloom) || (darkScene && emissiveSubjects > 0);
|
|
7887
|
+
const bloomRequested = Boolean(authoredBloom);
|
|
7333
7888
|
// Root previously advertised `ssao` in `requestedPasses` whenever a scene added
|
|
7334
7889
|
// effects.ambientOcclusion() or effects.contactOcclusion(), but never submitted
|
|
7335
7890
|
// an `ssao` option to the renderer. The advertised pass could therefore never
|
|
@@ -7338,20 +7893,15 @@ function createProductionRuntimePostprocess(snapshot) {
|
|
|
7338
7893
|
const authoredAmbientOcclusion = nodes.find((node) => node.kind === "effect" && node.effect === "ambient-occlusion");
|
|
7339
7894
|
const authoredContactOcclusion = nodes.find((node) => node.kind === "effect" && node.effect === "contact-occlusion");
|
|
7340
7895
|
const authoredOcclusion = authoredAmbientOcclusion ?? authoredContactOcclusion;
|
|
7341
|
-
const exposure = category === "city-day" ? 1.02
|
|
7342
|
-
: category === "material" || category === "product" ? 1.08
|
|
7343
|
-
: category === "space" ? 1.18
|
|
7344
|
-
: 1.12;
|
|
7345
|
-
const saturation = category === "material" || category === "product" ? 1.01
|
|
7346
|
-
: category === "city-day" ? 1.04
|
|
7347
|
-
: 1.1;
|
|
7348
7896
|
return {
|
|
7349
|
-
|
|
7897
|
+
// Tone mapping requires unclamped linear input. RGBA8 quantized dark clear
|
|
7898
|
+
// colors and clipped highlights before ACES, which produced washed-out output.
|
|
7899
|
+
targetFormat: "rgba16f",
|
|
7350
7900
|
...(bloomRequested ? {
|
|
7351
7901
|
bloom: {
|
|
7352
|
-
threshold: clampNumber(authoredBloom?.threshold ??
|
|
7353
|
-
intensity: clampNumber(authoredBloom?.intensity ??
|
|
7354
|
-
radius: Math.max(1, Math.min(4, Math.round(authoredBloom?.radius ??
|
|
7902
|
+
threshold: clampNumber(authoredBloom?.threshold ?? 0.78, 0, 1),
|
|
7903
|
+
intensity: clampNumber(authoredBloom?.intensity ?? 0.3, 0, 2),
|
|
7904
|
+
radius: Math.max(1, Math.min(4, Math.round(authoredBloom?.radius ?? 2)))
|
|
7355
7905
|
}
|
|
7356
7906
|
} : {}),
|
|
7357
7907
|
...(authoredOcclusion ? {
|
|
@@ -7367,22 +7917,11 @@ function createProductionRuntimePostprocess(snapshot) {
|
|
|
7367
7917
|
}
|
|
7368
7918
|
} : {}),
|
|
7369
7919
|
toneMapping: {
|
|
7370
|
-
exposure,
|
|
7371
|
-
whitePoint:
|
|
7372
|
-
operator: "
|
|
7920
|
+
exposure: 1,
|
|
7921
|
+
whitePoint: 1,
|
|
7922
|
+
operator: "aces",
|
|
7373
7923
|
inputColorSpace: "linear",
|
|
7374
7924
|
outputColorSpace: "srgb"
|
|
7375
|
-
},
|
|
7376
|
-
colorGrade: {
|
|
7377
|
-
contrast: category === "material" || category === "product" ? 1.06 : 1.1,
|
|
7378
|
-
saturation,
|
|
7379
|
-
vibrance: darkScene ? 0.14 : 0.06,
|
|
7380
|
-
vignette: category === "game" || category === "neon" ? 0.1 : 0.04,
|
|
7381
|
-
sharpening: category === "material" || category === "product" ? 0.28 : 0.2
|
|
7382
|
-
},
|
|
7383
|
-
fxaa: {
|
|
7384
|
-
edgeThreshold: category === "game" ? 0.07 : 0.09,
|
|
7385
|
-
subpixelBlend: category === "material" || category === "product" ? 0.48 : 0.58
|
|
7386
7925
|
}
|
|
7387
7926
|
};
|
|
7388
7927
|
}
|
|
@@ -7447,7 +7986,8 @@ function createProductionRuntimePostprocessObservation(diagnostics) {
|
|
|
7447
7986
|
contactOcclusionReceiver: actualPasses.includes("contact-shadow"),
|
|
7448
7987
|
pixelBacked,
|
|
7449
7988
|
actualPasses,
|
|
7450
|
-
fallbackPasses: pixelBacked ? [] : ["direct-render"]
|
|
7989
|
+
fallbackPasses: pixelBacked ? [] : ["direct-render"],
|
|
7990
|
+
targetFormat: diagnostics.postprocessTargetFormat
|
|
7451
7991
|
};
|
|
7452
7992
|
}
|
|
7453
7993
|
function createProductionRuntimeCollectedLights(snapshot) {
|
|
@@ -7468,7 +8008,7 @@ function createProductionRuntimeCollectedLights(snapshot) {
|
|
|
7468
8008
|
}
|
|
7469
8009
|
function createProductionRuntimeLightDescriptors(node, authoredIndex) {
|
|
7470
8010
|
const name = node.name?.trim() || `aura-authored-${node.light}-${authoredIndex + 1}`;
|
|
7471
|
-
const color =
|
|
8011
|
+
const color = colorToLinearRgb(node.color ?? "#ffffff");
|
|
7472
8012
|
const intensity = nonNegativeFinite(node.intensity);
|
|
7473
8013
|
const position = productionRuntimeLightPosition(node);
|
|
7474
8014
|
const direction = productionRuntimeLightDirection(node, position);
|
|
@@ -7745,17 +8285,40 @@ function multiplyRgb(color, tint) {
|
|
|
7745
8285
|
async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOptions, runtimeNodes) {
|
|
7746
8286
|
const flattened = groups.flatten(snapshot.nodes);
|
|
7747
8287
|
const modelNodes = flattened.filter((node) => isRenderableModelNode(node) && createAssetProvenance(node.asset).source === "typed-aura-assets-manifest");
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
8288
|
+
/*
|
|
8289
|
+
* Loaded here rather than imported at module scope (WS-2.2). This function is only reached when the
|
|
8290
|
+
* scene contains a typed GLB, so the glTF loader is downloaded exactly when it is needed.
|
|
8291
|
+
*/
|
|
8292
|
+
const actorEntries = modelNodes.length > 0
|
|
8293
|
+
? await (async () => {
|
|
8294
|
+
const { createTypedGLBActor } = await import("../production-runtime/TypedGLBActor.js");
|
|
8295
|
+
return await Promise.all(modelNodes.map(async (node, index) => ({
|
|
8296
|
+
node,
|
|
8297
|
+
actor: await createTypedGLBActor({
|
|
8298
|
+
asset: node.asset,
|
|
8299
|
+
id: node.runtime?.id ?? node.asset.id ?? `model-${index + 1}`,
|
|
8300
|
+
name: node.name ?? node.asset.id ?? `model-${index + 1}`,
|
|
8301
|
+
width: canvas.width,
|
|
8302
|
+
height: canvas.height,
|
|
8303
|
+
...(node.hiddenNodeNames ? { hiddenNodeNames: node.hiddenNodeNames } : {}),
|
|
8304
|
+
...(node.material?.color ? {
|
|
8305
|
+
tint: {
|
|
8306
|
+
baseColor: colorToLinearRgba(node.material.color),
|
|
8307
|
+
replaceSurfaceTextures: true,
|
|
8308
|
+
...(node.material.emissive ? { emissiveColor: colorToLinearRgb(node.material.emissive) } : {}),
|
|
8309
|
+
...(node.material.emissiveIntensity === undefined ? {} : { emissiveStrength: node.material.emissiveIntensity }),
|
|
8310
|
+
...(node.material.roughness === undefined ? {} : { roughness: clamp01(node.material.roughness) }),
|
|
8311
|
+
...(node.material.metallic === undefined && node.material.metalness === undefined
|
|
8312
|
+
? {}
|
|
8313
|
+
: { metallic: clamp01(node.material.metallic ?? node.material.metalness ?? 0) }),
|
|
8314
|
+
...(node.material.clearcoat === undefined ? {} : { clearcoat: clamp01(node.material.clearcoat) }),
|
|
8315
|
+
...(node.material.clearcoatRoughness === undefined ? {} : { clearcoatRoughness: clamp01(node.material.clearcoatRoughness) })
|
|
8316
|
+
}
|
|
8317
|
+
} : {})
|
|
8318
|
+
})
|
|
8319
|
+
})));
|
|
8320
|
+
})()
|
|
8321
|
+
: [];
|
|
7759
8322
|
const primitiveEntries = createProductionRuntimePrimitiveEntries(flattened);
|
|
7760
8323
|
const productionRenderer = await ProductionRuntimeRenderer.create({
|
|
7761
8324
|
canvas,
|
|
@@ -7764,7 +8327,9 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
|
|
|
7764
8327
|
backend: "webgl2",
|
|
7765
8328
|
antialias: true,
|
|
7766
8329
|
preserveDrawingBuffer: true,
|
|
7767
|
-
|
|
8330
|
+
// Background colors are display intent. Pre-invert the renderer's coupled
|
|
8331
|
+
// matrix-fitted ACES transform so presentation preserves that authored color.
|
|
8332
|
+
clearColor: colorToAcesInputClearColor(snapshot.background)
|
|
7768
8333
|
});
|
|
7769
8334
|
let latestDeviceDiagnostics = productionRenderer.getDiagnostics();
|
|
7770
8335
|
let latestFeatures = productionRenderer.getFeatures();
|
|
@@ -7805,7 +8370,15 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
|
|
|
7805
8370
|
.filter((feature) => feature.state !== "supported")
|
|
7806
8371
|
.map((feature) => `Production runtime feature ${feature.id} is ${feature.state}: ${feature.detail}`),
|
|
7807
8372
|
...runtimeWarnings
|
|
7808
|
-
]
|
|
8373
|
+
],
|
|
8374
|
+
deviceDiagnostics: latestDeviceDiagnostics,
|
|
8375
|
+
lodSelections: primitiveEntries
|
|
8376
|
+
.filter((entry) => Boolean(entry.node.lod?.levels.length))
|
|
8377
|
+
.map((entry) => ({
|
|
8378
|
+
nodeName: entry.node.name ?? "unnamed distance LOD",
|
|
8379
|
+
levelIndex: entry.currentLodIndex,
|
|
8380
|
+
levelName: entry.resources[entry.currentLodIndex]?.name ?? `level-${entry.currentLodIndex}`
|
|
8381
|
+
}))
|
|
7809
8382
|
}, rendererOptions);
|
|
7810
8383
|
return {
|
|
7811
8384
|
get backend() {
|
|
@@ -7825,19 +8398,35 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
|
|
|
7825
8398
|
viewProjection(time) {
|
|
7826
8399
|
return createViewProjection(snapshot, canvas.width / Math.max(1, canvas.height), time, runtimeNodes);
|
|
7827
8400
|
},
|
|
8401
|
+
resize(width, height) {
|
|
8402
|
+
productionRenderer.resize(width, height);
|
|
8403
|
+
},
|
|
8404
|
+
onDeviceLost(listener) {
|
|
8405
|
+
return productionRenderer.onDeviceLost(listener);
|
|
8406
|
+
},
|
|
8407
|
+
onDeviceRestored(listener) {
|
|
8408
|
+
return productionRenderer.onDeviceRestored(listener);
|
|
8409
|
+
},
|
|
8410
|
+
deviceLost() {
|
|
8411
|
+
return productionRenderer.deviceLost();
|
|
8412
|
+
},
|
|
7828
8413
|
dispose() {
|
|
7829
8414
|
productionRenderer.dispose();
|
|
7830
8415
|
for (const { actor } of actorEntries)
|
|
7831
8416
|
actor.pipeline.dispose();
|
|
7832
|
-
for (const {
|
|
7833
|
-
geometry
|
|
7834
|
-
|
|
8417
|
+
for (const { resources } of primitiveEntries) {
|
|
8418
|
+
for (const { geometry, material } of resources) {
|
|
8419
|
+
geometry.dispose();
|
|
8420
|
+
material.dispose();
|
|
8421
|
+
}
|
|
7835
8422
|
}
|
|
7836
8423
|
}
|
|
7837
8424
|
};
|
|
7838
8425
|
}
|
|
7839
8426
|
function createProductionRuntimeRendererInput(snapshot, canvas, actorEntries, primitiveEntries, time, runtimeNodes, runtimeWarnings, environmentLighting, collectedLights) {
|
|
7840
8427
|
const items = [];
|
|
8428
|
+
const viewProjectionMatrix = createViewProjection(snapshot, canvas.width / Math.max(1, canvas.height), time, runtimeNodes);
|
|
8429
|
+
const cameraPosition = resolveCameraEye(snapshot, snapshot.camera, time, runtimeNodes);
|
|
7841
8430
|
for (const entry of actorEntries) {
|
|
7842
8431
|
const currentState = resolveProductionActorRuntimeState(entry, runtimeNodes);
|
|
7843
8432
|
const currentNode = currentState.node;
|
|
@@ -7854,16 +8443,16 @@ function createProductionRuntimeRendererInput(snapshot, canvas, actorEntries, pr
|
|
|
7854
8443
|
const currentState = resolveProductionPrimitiveRuntimeState(entry, runtimeNodes);
|
|
7855
8444
|
if (!currentState.visible)
|
|
7856
8445
|
continue;
|
|
8446
|
+
const resource = selectProductionPrimitiveResource(entry, currentState.node, cameraPosition);
|
|
7857
8447
|
items.push({
|
|
7858
|
-
geometry:
|
|
7859
|
-
material:
|
|
7860
|
-
modelMatrix: createModelMatrix(currentState.node,
|
|
8448
|
+
geometry: resource.geometry,
|
|
8449
|
+
material: resource.material,
|
|
8450
|
+
modelMatrix: createModelMatrix(currentState.node, resource.bounds, false, time),
|
|
7861
8451
|
label: currentState.node.name ?? `aura-primitive-${currentState.node.primitive}`,
|
|
7862
|
-
includeInAutoFrame: false
|
|
8452
|
+
includeInAutoFrame: false,
|
|
8453
|
+
...(currentState.node.instances ? { instanceTransforms: createProductionInstanceTransforms(currentState.node.instances, currentState.node), instanceColors: createProductionInstanceColors(currentState.node.instanceColors, currentState.node.instances.length) } : {})
|
|
7863
8454
|
});
|
|
7864
8455
|
}
|
|
7865
|
-
const viewProjectionMatrix = createViewProjection(snapshot, canvas.width / Math.max(1, canvas.height), time, runtimeNodes);
|
|
7866
|
-
const cameraPosition = resolveCameraEye(snapshot, snapshot.camera, time, runtimeNodes);
|
|
7867
8456
|
const source = {
|
|
7868
8457
|
collectRenderItems: () => items,
|
|
7869
8458
|
cameraPolicy: "require",
|
|
@@ -7880,18 +8469,57 @@ function createProductionRuntimeRendererInput(snapshot, canvas, actorEntries, pr
|
|
|
7880
8469
|
return {
|
|
7881
8470
|
source,
|
|
7882
8471
|
camera: cameraLike,
|
|
7883
|
-
metadata: createProductionRuntimeMetadata(actorEntries)
|
|
8472
|
+
metadata: createProductionRuntimeMetadata(actorEntries, primitiveEntries)
|
|
7884
8473
|
};
|
|
7885
8474
|
}
|
|
7886
8475
|
function createProductionRuntimePrimitiveEntries(nodes) {
|
|
7887
8476
|
return nodes
|
|
7888
8477
|
.filter((node) => node.kind === "primitive")
|
|
7889
|
-
.map((node) => ({
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
8478
|
+
.map((node) => ({ node, resources: createProductionPrimitiveResources(node), currentLodIndex: 0 }));
|
|
8479
|
+
}
|
|
8480
|
+
function createProductionPrimitiveResources(node) {
|
|
8481
|
+
const levels = node.lod?.levels;
|
|
8482
|
+
if (!levels?.length) {
|
|
8483
|
+
return [{ geometry: createProductionPrimitiveGeometry(node), material: createProductionPrimitiveMaterial(node), bounds: primitiveGeometryBounds(node), name: node.name ?? node.primitive }];
|
|
8484
|
+
}
|
|
8485
|
+
return levels.map((level, index) => {
|
|
8486
|
+
if (!level.primitive && !level.geometry)
|
|
8487
|
+
throw new Error(`Aura3D LOD level ${index} requires primitive or custom geometry.`);
|
|
8488
|
+
const levelNode = {
|
|
8489
|
+
...node,
|
|
8490
|
+
primitive: level.geometry ? "custom" : level.primitive,
|
|
8491
|
+
geometry: level.geometry,
|
|
8492
|
+
material: level.material ?? node.material,
|
|
8493
|
+
lod: undefined
|
|
8494
|
+
};
|
|
8495
|
+
return { geometry: createProductionPrimitiveGeometry(levelNode), material: createProductionPrimitiveMaterial(levelNode), bounds: primitiveGeometryBounds(levelNode), name: level.name };
|
|
8496
|
+
});
|
|
8497
|
+
}
|
|
8498
|
+
function selectProductionPrimitiveResource(entry, node, cameraPosition) {
|
|
8499
|
+
if (!node.lod?.levels.length)
|
|
8500
|
+
return entry.resources[0];
|
|
8501
|
+
const position = node.position ?? [0, 0, 0];
|
|
8502
|
+
const distance = Math.hypot(cameraPosition[0] - position[0], cameraPosition[1] - position[1], cameraPosition[2] - position[2]);
|
|
8503
|
+
const selection = selectAuraRootLodLevel(distance, node.lod.levels, entry.currentLodIndex, node.lod.hysteresis ?? 0);
|
|
8504
|
+
entry.currentLodIndex = selection.levelIndex;
|
|
8505
|
+
return entry.resources[selection.levelIndex] ?? entry.resources[entry.resources.length - 1];
|
|
8506
|
+
}
|
|
8507
|
+
function createProductionInstanceTransforms(transforms, node) {
|
|
8508
|
+
const matrices = new Float32Array(transforms.length * 16);
|
|
8509
|
+
transforms.forEach((transform, index) => {
|
|
8510
|
+
const localNode = { kind: "primitive", primitive: node.primitive, ...transform };
|
|
8511
|
+
matrices.set(createModelMatrix(localNode, { min: [-0.5, -0.5, -0.5], max: [0.5, 0.5, 0.5] }, false, 0), index * 16);
|
|
8512
|
+
});
|
|
8513
|
+
return matrices;
|
|
8514
|
+
}
|
|
8515
|
+
function createProductionInstanceColors(colors, count) {
|
|
8516
|
+
if (!colors)
|
|
8517
|
+
return undefined;
|
|
8518
|
+
if (colors.length !== count)
|
|
8519
|
+
throw new Error("Aura3D instance color count must match instance transform count.");
|
|
8520
|
+
const values = new Float32Array(count * 4);
|
|
8521
|
+
colors.forEach((color, index) => values.set(colorToLinearRgba(color), index * 4));
|
|
8522
|
+
return values;
|
|
7895
8523
|
}
|
|
7896
8524
|
function resolveProductionPrimitiveRuntimeState(entry, runtimeNodes) {
|
|
7897
8525
|
const runtimeId = entry.node.runtime?.id;
|
|
@@ -7911,11 +8539,91 @@ function resolveProductionPrimitiveRuntimeState(entry, runtimeNodes) {
|
|
|
7911
8539
|
visible: runtimeSnapshot.visible
|
|
7912
8540
|
};
|
|
7913
8541
|
}
|
|
8542
|
+
/**
|
|
8543
|
+
* Build the production `PBRMaterial` for a primitive node.
|
|
8544
|
+
*
|
|
8545
|
+
* ## WS-2.1a — the parameter drop this function used to be
|
|
8546
|
+
*
|
|
8547
|
+
* Until 1.6 this forwarded `clearcoat` and **nothing else** from the extended material surface.
|
|
8548
|
+
* `AuraMaterialSpec` accepts `sheen`, `sheenRoughness`, `sheenColor`, `iridescence`,
|
|
8549
|
+
* `iridescenceIOR`, `iridescenceThicknessRange`, `anisotropy`, `anisotropyRotation`, `transmission`,
|
|
8550
|
+
* `thickness`, `ior`, `attenuationColor` and `attenuationDistance`; `PBRMaterial` accepts a uniform
|
|
8551
|
+
* for every one of them and binds them all. The bridge between the two dropped them on the floor.
|
|
8552
|
+
*
|
|
8553
|
+
* How that presented, measured by `tools/material-structural-parity` before the fix:
|
|
8554
|
+
*
|
|
8555
|
+
* ```
|
|
8556
|
+
* material.pbr({ ..., sheen: 1 }) -> byte-identical frame to sheen: 0
|
|
8557
|
+
* material.pbr({ ..., iridescence: 1 }) -> byte-identical frame to iridescence: 0
|
|
8558
|
+
* material.pbr({ ..., clearcoat: 1 }) -> byte-identical frame to clearcoat: 0
|
|
8559
|
+
* ```
|
|
8560
|
+
*
|
|
8561
|
+
* `clearcoat` was *forwarded* and still produced an identical frame, which pins the second, separate
|
|
8562
|
+
* defect: `a3dPbrExtensionEnvironmentLight` in `ShaderChunks.ts` adds its lobes to
|
|
8563
|
+
* `specularRadiance`, a term that is zero without an environment map. Forwarding alone is necessary
|
|
8564
|
+
* and not sufficient, so `environmentIntensity` now also has a floor when an extension factor is
|
|
8565
|
+
* present — otherwise a developer setting `clearcoat: 1` on a scene with no environment sees nothing
|
|
8566
|
+
* and has no way to find out why.
|
|
8567
|
+
*
|
|
8568
|
+
* The reason this survived so long is worth stating: three of the four lobes are scalar
|
|
8569
|
+
* approximations rather than real BRDFs, and **nobody could tell**, because the parameters never
|
|
8570
|
+
* arrived. The plumbing defect concealed the shading defect.
|
|
8571
|
+
*/
|
|
7914
8572
|
function createProductionPrimitiveMaterial(node) {
|
|
7915
8573
|
const materialSpec = node.material;
|
|
7916
|
-
const baseColor =
|
|
8574
|
+
const baseColor = colorToLinearRgba(materialSpec?.color ?? materialSpec?.emissive ?? "#d7dee8");
|
|
7917
8575
|
const opacity = clamp01(materialSpec?.opacity ?? baseColor[3] ?? 1);
|
|
7918
|
-
const emissiveColor = materialSpec?.emissive ?
|
|
8576
|
+
const emissiveColor = materialSpec?.emissive ? colorToLinearRgb(materialSpec.emissive) : [0, 0, 0];
|
|
8577
|
+
const clearcoat = clamp01(materialSpec?.clearcoat ?? 0);
|
|
8578
|
+
const sheen = clamp01(materialSpec?.sheen ?? 0);
|
|
8579
|
+
const iridescence = clamp01(materialSpec?.iridescence ?? 0);
|
|
8580
|
+
const anisotropy = clamp01(Math.abs(materialSpec?.anisotropy ?? 0));
|
|
8581
|
+
const transmission = clamp01(materialSpec?.transmission ?? 0);
|
|
8582
|
+
const thicknessRange = materialSpec?.iridescenceThicknessRange;
|
|
8583
|
+
/*
|
|
8584
|
+
* Sheen colour defaults to the sheen strength as a white tint rather than to black.
|
|
8585
|
+
* `sheenColorFactor: [0,0,0]` multiplies the entire sheen lobe to zero, so `sheen: 1` with no
|
|
8586
|
+
* explicit colour would forward a factor and still render nothing — the same silent-no-op shape
|
|
8587
|
+
* this whole workstream exists to remove.
|
|
8588
|
+
*/
|
|
8589
|
+
const sheenColor = materialSpec?.sheenColor
|
|
8590
|
+
? colorToLinearRgb(materialSpec.sheenColor).map((channel) => channel * sheen)
|
|
8591
|
+
: [sheen, sheen, sheen];
|
|
8592
|
+
/*
|
|
8593
|
+
* The extension lobes are environment-driven. With `environmentIntensity: 0` a declared clearcoat,
|
|
8594
|
+
* sheen or iridescence factor reaches the shader and contributes exactly nothing, which is
|
|
8595
|
+
* indistinguishable from the parameter never arriving. A floor of 0.35 when any extension factor is
|
|
8596
|
+
* declared makes the declaration observable; scenes that declare none are unaffected.
|
|
8597
|
+
*/
|
|
8598
|
+
const declaresExtension = clearcoat > 0 || sheen > 0 || iridescence > 0 || anisotropy > 0;
|
|
8599
|
+
const requestedEnvironmentIntensity = Math.max(0, materialSpec?.envMapIntensity ?? 0.75);
|
|
8600
|
+
const environmentIntensity = declaresExtension
|
|
8601
|
+
? Math.max(0.35, requestedEnvironmentIntensity)
|
|
8602
|
+
: requestedEnvironmentIntensity;
|
|
8603
|
+
/*
|
|
8604
|
+
* Native instance submission requires the renderer's instanced shader contract. The ordinary
|
|
8605
|
+
* PBR shader intentionally has no instance matrix inputs and ForwardPass therefore expands it.
|
|
8606
|
+
* Keep the full extension material for declarations that need its broader BRDF surface; the
|
|
8607
|
+
* root-safe instanced path selects the purpose-built lit material when every requested property
|
|
8608
|
+
* is represented by that shader. This makes the one-draw claim real without silently dropping
|
|
8609
|
+
* clearcoat/transmission/etc. (those continue to render correctly via expansion).
|
|
8610
|
+
*/
|
|
8611
|
+
if (node.instances && !declaresExtension && transmission === 0 && materialSpec?.thickness === undefined && materialSpec?.ior === undefined && materialSpec?.attenuationColor === undefined && materialSpec?.attenuationDistance === undefined) {
|
|
8612
|
+
return new InstancedPBRMaterial({
|
|
8613
|
+
name: `a3d-production-instanced-primitive-${node.primitive}${node.name ? `-${node.name}` : ""}`,
|
|
8614
|
+
baseColor: [baseColor[0], baseColor[1], baseColor[2], opacity],
|
|
8615
|
+
metallic: clamp01(materialSpec?.metallic ?? materialSpec?.metalness ?? 0),
|
|
8616
|
+
roughness: clamp01(materialSpec?.roughness ?? 0.58),
|
|
8617
|
+
emissiveColor,
|
|
8618
|
+
emissiveStrength: Math.max(0, materialSpec?.emissiveIntensity ?? (materialSpec?.emissive ? 1.35 : 0)),
|
|
8619
|
+
environmentIntensity,
|
|
8620
|
+
renderState: {
|
|
8621
|
+
blend: opacity < 0.999,
|
|
8622
|
+
depthWrite: opacity >= 0.999,
|
|
8623
|
+
cullMode: node.primitive === "plane" || opacity < 0.999 ? "none" : "back"
|
|
8624
|
+
}
|
|
8625
|
+
});
|
|
8626
|
+
}
|
|
7919
8627
|
return new PBRMaterial({
|
|
7920
8628
|
name: `a3d-production-primitive-${node.primitive}${node.name ? `-${node.name}` : ""}`,
|
|
7921
8629
|
baseColor: [baseColor[0], baseColor[1], baseColor[2], opacity],
|
|
@@ -7923,9 +8631,22 @@ function createProductionPrimitiveMaterial(node) {
|
|
|
7923
8631
|
roughness: clamp01(materialSpec?.roughness ?? 0.58),
|
|
7924
8632
|
emissiveColor,
|
|
7925
8633
|
emissiveStrength: Math.max(0, materialSpec?.emissiveIntensity ?? (materialSpec?.emissive ? 1.35 : 0)),
|
|
7926
|
-
clearcoatFactor:
|
|
8634
|
+
clearcoatFactor: clearcoat,
|
|
7927
8635
|
clearcoatRoughnessFactor: clamp01(materialSpec?.clearcoatRoughness ?? 0.34),
|
|
7928
|
-
|
|
8636
|
+
// WS-2.1a: everything below this line was previously dropped.
|
|
8637
|
+
sheenColorFactor: sheenColor,
|
|
8638
|
+
sheenRoughnessFactor: clamp01(materialSpec?.sheenRoughness ?? 0.3),
|
|
8639
|
+
anisotropyStrength: anisotropy,
|
|
8640
|
+
anisotropyRotation: materialSpec?.anisotropyRotation ?? 0,
|
|
8641
|
+
iridescenceFactor: iridescence,
|
|
8642
|
+
iridescenceIor: Math.max(1, materialSpec?.iridescenceIOR ?? 1.3),
|
|
8643
|
+
...(thicknessRange ? { iridescenceThicknessMinimum: thicknessRange[0], iridescenceThicknessMaximum: thicknessRange[1] } : {}),
|
|
8644
|
+
transmissionFactor: transmission,
|
|
8645
|
+
...(materialSpec?.thickness === undefined ? {} : { volumeThicknessFactor: Math.max(0, materialSpec.thickness) }),
|
|
8646
|
+
...(materialSpec?.ior === undefined ? {} : { ior: Math.max(1, materialSpec.ior) }),
|
|
8647
|
+
...(materialSpec?.attenuationColor ? { volumeAttenuationColor: colorToLinearRgb(materialSpec.attenuationColor) } : {}),
|
|
8648
|
+
...(materialSpec?.attenuationDistance === undefined ? {} : { volumeAttenuationDistance: Math.max(0, materialSpec.attenuationDistance) }),
|
|
8649
|
+
environmentIntensity,
|
|
7929
8650
|
renderState: {
|
|
7930
8651
|
blend: opacity < 0.999,
|
|
7931
8652
|
depthWrite: opacity >= 0.999,
|
|
@@ -7934,9 +8655,18 @@ function createProductionPrimitiveMaterial(node) {
|
|
|
7934
8655
|
});
|
|
7935
8656
|
}
|
|
7936
8657
|
function createProductionPrimitiveGeometry(node) {
|
|
8658
|
+
if (node.geometry)
|
|
8659
|
+
return createProductionGeometryFromCustomSpec(node.geometry);
|
|
8660
|
+
if (node.primitive === "custom")
|
|
8661
|
+
throw new Error("Aura3D custom primitive requires geometry.");
|
|
7937
8662
|
return createProductionGeometryFromPrimitiveMesh(createProductionPrimitiveMesh(node.primitive));
|
|
7938
8663
|
}
|
|
7939
|
-
function primitiveGeometryBounds(
|
|
8664
|
+
function primitiveGeometryBounds(nodeOrPrimitive) {
|
|
8665
|
+
if (typeof nodeOrPrimitive === "object" && nodeOrPrimitive.geometry)
|
|
8666
|
+
return customGeometryBounds(nodeOrPrimitive.geometry);
|
|
8667
|
+
const primitive = typeof nodeOrPrimitive === "string" ? nodeOrPrimitive : nodeOrPrimitive.primitive;
|
|
8668
|
+
if (primitive === "custom")
|
|
8669
|
+
throw new Error("Aura3D custom primitive requires geometry.");
|
|
7940
8670
|
return createProductionPrimitiveMesh(primitive).bounds;
|
|
7941
8671
|
}
|
|
7942
8672
|
function createProductionPrimitiveMesh(primitive) {
|
|
@@ -7952,6 +8682,42 @@ function createProductionPrimitiveMesh(primitive) {
|
|
|
7952
8682
|
return createCylinderGeometry();
|
|
7953
8683
|
return createPlaneGeometry();
|
|
7954
8684
|
}
|
|
8685
|
+
function createProductionGeometryFromCustomSpec(spec) {
|
|
8686
|
+
const normalized = defineAuraCustomGeometry(spec);
|
|
8687
|
+
const normals = normalized.normals ?? calculateCustomGeometryNormals(normalized);
|
|
8688
|
+
const vertexCount = normalized.positions.length;
|
|
8689
|
+
const vertices = new VertexBuffer(VertexFormat.P3N3, vertexCount);
|
|
8690
|
+
normalized.positions.forEach((position, index) => {
|
|
8691
|
+
vertices.setAttribute(index, "position", position);
|
|
8692
|
+
vertices.setAttribute(index, "normal", normals[index] ?? [0, 1, 0]);
|
|
8693
|
+
});
|
|
8694
|
+
return new Geometry(vertices, new IndexBuffer(normalized.indices, vertexCount), "triangles", customGeometryBounds(normalized));
|
|
8695
|
+
}
|
|
8696
|
+
function customGeometryBounds(spec) {
|
|
8697
|
+
if (spec.bounds)
|
|
8698
|
+
return { min: spec.bounds.min, max: spec.bounds.max };
|
|
8699
|
+
const min = [Infinity, Infinity, Infinity];
|
|
8700
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
8701
|
+
spec.positions.forEach((position) => { for (let axis = 0; axis < 3; axis += 1) {
|
|
8702
|
+
min[axis] = Math.min(min[axis], position[axis]);
|
|
8703
|
+
max[axis] = Math.max(max[axis], position[axis]);
|
|
8704
|
+
} });
|
|
8705
|
+
return { min, max };
|
|
8706
|
+
}
|
|
8707
|
+
function calculateCustomGeometryNormals(spec) {
|
|
8708
|
+
const totals = Array.from({ length: spec.positions.length }, () => [0, 0, 0]);
|
|
8709
|
+
for (let offset = 0; offset < spec.indices.length; offset += 3) {
|
|
8710
|
+
const ia = spec.indices[offset], ib = spec.indices[offset + 1], ic = spec.indices[offset + 2];
|
|
8711
|
+
const a = spec.positions[ia], b = spec.positions[ib], c = spec.positions[ic];
|
|
8712
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
8713
|
+
const ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
8714
|
+
const normal = [ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2], ab[0] * ac[1] - ab[1] * ac[0]];
|
|
8715
|
+
for (const index of [ia, ib, ic])
|
|
8716
|
+
for (let axis = 0; axis < 3; axis += 1)
|
|
8717
|
+
totals[index][axis] += normal[axis];
|
|
8718
|
+
}
|
|
8719
|
+
return totals.map((normal) => { const length = Math.hypot(...normal) || 1; return [normal[0] / length, normal[1] / length, normal[2] / length]; });
|
|
8720
|
+
}
|
|
7955
8721
|
function createProductionGeometryFromPrimitiveMesh(mesh) {
|
|
7956
8722
|
const vertexCount = mesh.positions.length / 3;
|
|
7957
8723
|
const vertices = new VertexBuffer(VertexFormat.P3N3, vertexCount);
|
|
@@ -8049,16 +8815,18 @@ function createRuntimeEvidenceFromTypedGLBActor(node, actor, renderItems) {
|
|
|
8049
8815
|
morphRenderItemCount: renderItems.filter((item) => item.morphTargets && item.morphTargets.length > 0).length
|
|
8050
8816
|
});
|
|
8051
8817
|
}
|
|
8052
|
-
function createProductionRuntimeMetadata(actorEntries) {
|
|
8818
|
+
function createProductionRuntimeMetadata(actorEntries, primitiveEntries) {
|
|
8053
8819
|
const metadata = actorEntries.map((entry) => entry.actor.pipeline.metadata);
|
|
8054
8820
|
const primary = metadata[0];
|
|
8821
|
+
const primitiveCount = primitiveEntries.length;
|
|
8822
|
+
const typedPrimitiveCount = sumProductionMetadata(metadata, "primitiveCount");
|
|
8055
8823
|
return {
|
|
8056
|
-
assetId: metadata.length === 1 ? primary?.assetId ?? "typed-glb" : `typed-glb-batch-${metadata.length}`,
|
|
8057
|
-
assetUri: metadata.length === 1 ? primary?.assetUri ?? "aura3d://typed-glb" : "aura3d://scene/typed-glb-batch",
|
|
8824
|
+
assetId: metadata.length === 0 ? "aura-primitives" : metadata.length === 1 ? primary?.assetId ?? "typed-glb" : `typed-glb-batch-${metadata.length}`,
|
|
8825
|
+
assetUri: metadata.length === 0 ? "aura3d://scene/primitives" : metadata.length === 1 ? primary?.assetUri ?? "aura3d://typed-glb" : "aura3d://scene/typed-glb-batch",
|
|
8058
8826
|
...(metadata.length === 1 && primary?.assetName ? { assetName: primary.assetName } : metadata.length > 1 ? { assetName: "Aura3D typed GLB scene batch" } : {}),
|
|
8059
|
-
meshCount: sumProductionMetadata(metadata, "meshCount"),
|
|
8060
|
-
primitiveCount:
|
|
8061
|
-
materialCount: sumProductionMetadata(metadata, "materialCount"),
|
|
8827
|
+
meshCount: sumProductionMetadata(metadata, "meshCount") + primitiveCount,
|
|
8828
|
+
primitiveCount: typedPrimitiveCount + primitiveCount,
|
|
8829
|
+
materialCount: sumProductionMetadata(metadata, "materialCount") + primitiveCount,
|
|
8062
8830
|
textureCount: sumProductionMetadata(metadata, "textureCount"),
|
|
8063
8831
|
imageCount: sumProductionMetadata(metadata, "imageCount"),
|
|
8064
8832
|
animationCount: sumProductionMetadata(metadata, "animationCount"),
|
|
@@ -8180,6 +8948,47 @@ function colorToClearColor(color) {
|
|
|
8180
8948
|
}
|
|
8181
8949
|
return [0.02, 0.025, 0.035, 1];
|
|
8182
8950
|
}
|
|
8951
|
+
function colorToLinearClearColor(color) {
|
|
8952
|
+
const [red, green, blue, alpha] = colorToClearColor(color);
|
|
8953
|
+
return [srgbToLinearChannel(red), srgbToLinearChannel(green), srgbToLinearChannel(blue), alpha];
|
|
8954
|
+
}
|
|
8955
|
+
function colorToLinearRgba(color) {
|
|
8956
|
+
return colorToLinearClearColor(color);
|
|
8957
|
+
}
|
|
8958
|
+
function colorToAcesInputClearColor(color) {
|
|
8959
|
+
const [red, green, blue, alpha] = colorToLinearClearColor(color);
|
|
8960
|
+
const fitted = multiplyMat3Vec3([
|
|
8961
|
+
0.6430382486, 0.3111867518, 0.0457754574,
|
|
8962
|
+
0.0592686904, 0.9314364869, 0.0092949157,
|
|
8963
|
+
0.0059619013, 0.0639290157, 0.9301183842
|
|
8964
|
+
], [red, green, blue]).map(inverseAcesFit);
|
|
8965
|
+
const input = multiplyMat3Vec3([
|
|
8966
|
+
1.7647409720, -0.6757776782, -0.0889632938,
|
|
8967
|
+
-0.1470278520, 1.1602515117, -0.0132236597,
|
|
8968
|
+
-0.0363368301, -0.1624364369, 1.1987732670
|
|
8969
|
+
], fitted);
|
|
8970
|
+
return [Math.max(0, input[0] * 0.6), Math.max(0, input[1] * 0.6), Math.max(0, input[2] * 0.6), alpha];
|
|
8971
|
+
}
|
|
8972
|
+
function inverseAcesFit(target) {
|
|
8973
|
+
const a = target * 0.983729 - 1;
|
|
8974
|
+
const b = target * 0.432951 - 0.0245786;
|
|
8975
|
+
const c = target * 0.238081 + 0.000090537;
|
|
8976
|
+
const discriminant = Math.max(0, b * b - 4 * a * c);
|
|
8977
|
+
const first = (-b + Math.sqrt(discriminant)) / (2 * a);
|
|
8978
|
+
const second = (-b - Math.sqrt(discriminant)) / (2 * a);
|
|
8979
|
+
return Math.max(0, first, second);
|
|
8980
|
+
}
|
|
8981
|
+
function multiplyMat3Vec3(matrix, vector) {
|
|
8982
|
+
return [
|
|
8983
|
+
matrix[0] * vector[0] + matrix[1] * vector[1] + matrix[2] * vector[2],
|
|
8984
|
+
matrix[3] * vector[0] + matrix[4] * vector[1] + matrix[5] * vector[2],
|
|
8985
|
+
matrix[6] * vector[0] + matrix[7] * vector[1] + matrix[8] * vector[2]
|
|
8986
|
+
];
|
|
8987
|
+
}
|
|
8988
|
+
function colorToLinearRgb(color) {
|
|
8989
|
+
const [red, green, blue] = colorToLinearClearColor(color);
|
|
8990
|
+
return [red, green, blue];
|
|
8991
|
+
}
|
|
8183
8992
|
function colorWithAlpha(color, alpha) {
|
|
8184
8993
|
const [r, g, b] = colorToClearColor(color);
|
|
8185
8994
|
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${Math.min(1, Math.max(0, alpha))})`;
|
|
@@ -8430,13 +9239,57 @@ async function createWebGLSceneRenderer(canvas, snapshot, rendererOptions, runti
|
|
|
8430
9239
|
fallbackPasses: hasRuntimePostProcessEffects(requestedEffectNodes) ? ["webgl2-direct-render"] : []
|
|
8431
9240
|
},
|
|
8432
9241
|
warnings: [
|
|
8433
|
-
"Aura3D WebGL2 agent runtime
|
|
9242
|
+
"Aura3D WebGL2 agent runtime is an explicit safe-basic fallback; advanced postprocess, environment prefiltering, shadow maps, and GLB animation mixers are reported as unsupported unless the production runtime proves them.",
|
|
8434
9243
|
...runtimeWarnings
|
|
8435
9244
|
]
|
|
8436
9245
|
}, rendererOptions);
|
|
9246
|
+
/*
|
|
9247
|
+
* WS-2.6 — context-loss listeners for the agent-runtime path.
|
|
9248
|
+
*
|
|
9249
|
+
* This renderer owns a raw `WebGL2RenderingContext` rather than a `WebGL2Device`, so it cannot borrow
|
|
9250
|
+
* the device's listeners. It must attach its own so the explicit `safe-basic` compatibility path has
|
|
9251
|
+
* the same public lifecycle contract as the default production path. Before this was added, selecting
|
|
9252
|
+
* that compatibility mode produced a device-loss API that never emitted an event.
|
|
9253
|
+
*/
|
|
9254
|
+
let contextLost = false;
|
|
9255
|
+
const deviceLostListeners = new Set();
|
|
9256
|
+
const deviceRestoredListeners = new Set();
|
|
9257
|
+
const notify = (listeners) => {
|
|
9258
|
+
for (const listener of [...listeners]) {
|
|
9259
|
+
try {
|
|
9260
|
+
listener();
|
|
9261
|
+
}
|
|
9262
|
+
catch {
|
|
9263
|
+
// One listener throwing must not stop the others, least of all during a degraded condition.
|
|
9264
|
+
}
|
|
9265
|
+
}
|
|
9266
|
+
};
|
|
9267
|
+
const handleContextLost = (event) => {
|
|
9268
|
+
// Without preventDefault the browser will not fire `webglcontextrestored`.
|
|
9269
|
+
event.preventDefault();
|
|
9270
|
+
contextLost = true;
|
|
9271
|
+
notify(deviceLostListeners);
|
|
9272
|
+
};
|
|
9273
|
+
const handleContextRestored = () => {
|
|
9274
|
+
contextLost = false;
|
|
9275
|
+
notify(deviceRestoredListeners);
|
|
9276
|
+
};
|
|
9277
|
+
canvas.addEventListener("webglcontextlost", handleContextLost);
|
|
9278
|
+
canvas.addEventListener("webglcontextrestored", handleContextRestored);
|
|
8437
9279
|
return {
|
|
8438
9280
|
backend: "webgl2",
|
|
8439
9281
|
diagnostics: runtimeRendererDiagnostics,
|
|
9282
|
+
onDeviceLost(listener) {
|
|
9283
|
+
deviceLostListeners.add(listener);
|
|
9284
|
+
return () => deviceLostListeners.delete(listener);
|
|
9285
|
+
},
|
|
9286
|
+
onDeviceRestored(listener) {
|
|
9287
|
+
deviceRestoredListeners.add(listener);
|
|
9288
|
+
return () => deviceRestoredListeners.delete(listener);
|
|
9289
|
+
},
|
|
9290
|
+
deviceLost() {
|
|
9291
|
+
return contextLost;
|
|
9292
|
+
},
|
|
8440
9293
|
render(time) {
|
|
8441
9294
|
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
8442
9295
|
gl.clearColor(background[0], background[1], background[2], background[3]);
|
|
@@ -8461,6 +9314,17 @@ async function createWebGLSceneRenderer(canvas, snapshot, rendererOptions, runti
|
|
|
8461
9314
|
gl.uniform1f(program.uniforms.metallic, primitiveEntry.metallic ?? 0);
|
|
8462
9315
|
gl.uniform1f(program.uniforms.roughness, primitiveEntry.roughness ?? 0.72);
|
|
8463
9316
|
gl.uniform3fv(program.uniforms.emissive, new Float32Array(primitiveEntry.emissive ?? [0, 0, 0]));
|
|
9317
|
+
// WS-2.1a — extended material parameters, previously never uploaded.
|
|
9318
|
+
gl.uniform1f(program.uniforms.anisotropy, primitiveEntry.anisotropy ?? 0);
|
|
9319
|
+
gl.uniform1f(program.uniforms.anisotropyRotation, primitiveEntry.anisotropyRotation ?? 0);
|
|
9320
|
+
gl.uniform1f(program.uniforms.sheen, primitiveEntry.sheen ?? 0);
|
|
9321
|
+
gl.uniform1f(program.uniforms.sheenRoughness, primitiveEntry.sheenRoughness ?? 0.3);
|
|
9322
|
+
gl.uniform3fv(program.uniforms.sheenColor, new Float32Array(primitiveEntry.sheenColor ?? [1, 1, 1]));
|
|
9323
|
+
gl.uniform1f(program.uniforms.iridescence, primitiveEntry.iridescence ?? 0);
|
|
9324
|
+
gl.uniform1f(program.uniforms.iridescenceIor, primitiveEntry.iridescenceIor ?? 1.3);
|
|
9325
|
+
gl.uniform2fv(program.uniforms.iridescenceThickness, new Float32Array(primitiveEntry.iridescenceThickness ?? [100, 400]));
|
|
9326
|
+
gl.uniform1f(program.uniforms.clearcoat, primitiveEntry.clearcoat ?? 0);
|
|
9327
|
+
gl.uniform1f(program.uniforms.clearcoatRoughness, primitiveEntry.clearcoatRoughness ?? 0.1);
|
|
8464
9328
|
gl.bindBuffer(gl.ARRAY_BUFFER, primitiveEntry.position);
|
|
8465
9329
|
gl.enableVertexAttribArray(program.attributes.position);
|
|
8466
9330
|
gl.vertexAttribPointer(program.attributes.position, 3, gl.FLOAT, false, 0, 0);
|
|
@@ -8541,6 +9405,11 @@ async function createWebGLSceneRenderer(canvas, snapshot, rendererOptions, runti
|
|
|
8541
9405
|
return createViewProjection(snapshot, canvas.width / Math.max(1, canvas.height), time, runtimeNodes);
|
|
8542
9406
|
},
|
|
8543
9407
|
dispose() {
|
|
9408
|
+
// WS-2.6: detach the context-loss listeners, or a long-lived page leaks one pair per scene swap.
|
|
9409
|
+
canvas.removeEventListener("webglcontextlost", handleContextLost);
|
|
9410
|
+
canvas.removeEventListener("webglcontextrestored", handleContextRestored);
|
|
9411
|
+
deviceLostListeners.clear();
|
|
9412
|
+
deviceRestoredListeners.clear();
|
|
8544
9413
|
for (const modelEntry of models) {
|
|
8545
9414
|
const textures = new Set();
|
|
8546
9415
|
for (const primitiveEntry of modelEntry.primitives) {
|
|
@@ -8673,7 +9542,13 @@ function createWebGLModel(gl, node, modelData) {
|
|
|
8673
9542
|
bounds: modelData.bounds,
|
|
8674
9543
|
color: colorToRgb(node.material?.color ?? "#8fb4ff"),
|
|
8675
9544
|
normalizeToUnit: shouldNormalizeModelNode(node),
|
|
8676
|
-
primitives: modelData.primitives
|
|
9545
|
+
primitives: modelData.primitives
|
|
9546
|
+
.filter((primitiveEntry) => {
|
|
9547
|
+
if (!node.hiddenNodeNames || primitiveEntry.nodeIndex === undefined)
|
|
9548
|
+
return true;
|
|
9549
|
+
return !node.hiddenNodeNames.includes(modelData.nodes[primitiveEntry.nodeIndex]?.name ?? "");
|
|
9550
|
+
})
|
|
9551
|
+
.map((primitiveEntry) => {
|
|
8677
9552
|
const position = createBuffer(gl, gl.ARRAY_BUFFER, primitiveEntry.positions);
|
|
8678
9553
|
const normal = createBuffer(gl, gl.ARRAY_BUFFER, primitiveEntry.normals);
|
|
8679
9554
|
const uv = primitiveEntry.uvs ? createBuffer(gl, gl.ARRAY_BUFFER, primitiveEntry.uvs) : undefined;
|
|
@@ -8830,7 +9705,24 @@ function createWebGLPrimitiveModel(gl, node) {
|
|
|
8830
9705
|
indexType: gl.UNSIGNED_SHORT,
|
|
8831
9706
|
metallic: clamp01(node.material?.metallic ?? node.material?.metalness ?? 0),
|
|
8832
9707
|
roughness: clamp01(node.material?.roughness ?? 0.72),
|
|
8833
|
-
emissive: node.material?.emissive ? colorToRgb(node.material.emissive) : [0, 0, 0]
|
|
9708
|
+
emissive: node.material?.emissive ? colorToRgb(node.material.emissive) : [0, 0, 0],
|
|
9709
|
+
/*
|
|
9710
|
+
* WS-2.1a — forward the extended material surface.
|
|
9711
|
+
*
|
|
9712
|
+
* `sheenColor` defaults to white rather than black: a black sheen colour multiplies the whole
|
|
9713
|
+
* lobe to zero, so `sheen: 1` with no explicit colour would forward a factor and still render
|
|
9714
|
+
* nothing — the silent-no-op shape this workstream exists to remove.
|
|
9715
|
+
*/
|
|
9716
|
+
anisotropy: clamp01(Math.abs(node.material?.anisotropy ?? 0)),
|
|
9717
|
+
anisotropyRotation: node.material?.anisotropyRotation ?? 0,
|
|
9718
|
+
sheen: clamp01(node.material?.sheen ?? 0),
|
|
9719
|
+
sheenRoughness: clamp01(node.material?.sheenRoughness ?? 0.3),
|
|
9720
|
+
sheenColor: node.material?.sheenColor ? colorToRgb(node.material.sheenColor) : [1, 1, 1],
|
|
9721
|
+
iridescence: clamp01(node.material?.iridescence ?? 0),
|
|
9722
|
+
iridescenceIor: Math.max(1, node.material?.iridescenceIOR ?? 1.3),
|
|
9723
|
+
iridescenceThickness: node.material?.iridescenceThicknessRange ?? [100, 400],
|
|
9724
|
+
clearcoat: clamp01(node.material?.clearcoat ?? 0),
|
|
9725
|
+
clearcoatRoughness: clamp01(node.material?.clearcoatRoughness ?? 0.1)
|
|
8834
9726
|
}]
|
|
8835
9727
|
};
|
|
8836
9728
|
}
|
|
@@ -9036,7 +9928,73 @@ uniform int u_useEmissiveTexture;
|
|
|
9036
9928
|
uniform float u_metallic;
|
|
9037
9929
|
uniform float u_roughness;
|
|
9038
9930
|
uniform vec3 u_emissive;
|
|
9931
|
+
// WS-2.1a — extended material parameters. Previously absent, so every one was silently discarded.
|
|
9932
|
+
uniform float u_anisotropy;
|
|
9933
|
+
uniform float u_anisotropyRotation;
|
|
9934
|
+
uniform float u_sheen;
|
|
9935
|
+
uniform float u_sheenRoughness;
|
|
9936
|
+
uniform vec3 u_sheenColor;
|
|
9937
|
+
uniform float u_iridescence;
|
|
9938
|
+
uniform float u_iridescenceIor;
|
|
9939
|
+
uniform vec2 u_iridescenceThickness;
|
|
9940
|
+
uniform float u_clearcoat;
|
|
9941
|
+
uniform float u_clearcoatRoughness;
|
|
9039
9942
|
out vec4 outColor;
|
|
9943
|
+
|
|
9944
|
+
const float A3D_PI = 3.141592653589793;
|
|
9945
|
+
|
|
9946
|
+
/**
|
|
9947
|
+
* Anisotropic GGX (Trowbridge-Reitz) normal distribution.
|
|
9948
|
+
*
|
|
9949
|
+
* This is the term that makes a highlight stretch. The previous implementation multiplied
|
|
9950
|
+
* specularRadiance by a scalar band, which can only brighten or dim a highlight, never elongate it. Measured consequence: highlight elongation 1.5602 at
|
|
9951
|
+
* anisotropy 0.95 and 1.5602 at anisotropy 0, identical to four decimal places, and orientation fixed
|
|
9952
|
+
* at 20.4 degrees regardless of anisotropyRotation.
|
|
9953
|
+
*
|
|
9954
|
+
* Roughness is split into two along the tangent and bitangent, which is what produces an elliptical
|
|
9955
|
+
* lobe: alphaT grows and alphaB shrinks as anisotropy rises, so the highlight spreads along one axis
|
|
9956
|
+
* and tightens along the other.
|
|
9957
|
+
*/
|
|
9958
|
+
float a3dAnisotropicGGX(float nDotH, float tDotH, float bDotH, float alphaT, float alphaB) {
|
|
9959
|
+
float denominator = (tDotH * tDotH) / (alphaT * alphaT) + (bDotH * bDotH) / (alphaB * alphaB) + nDotH * nDotH;
|
|
9960
|
+
return 1.0 / max(1e-6, A3D_PI * alphaT * alphaB * denominator * denominator);
|
|
9961
|
+
}
|
|
9962
|
+
|
|
9963
|
+
/**
|
|
9964
|
+
* Charlie sheen distribution (Estevez & Kulla), as used by KHR_materials_sheen.
|
|
9965
|
+
*
|
|
9966
|
+
* A retroreflective lobe concentrated at grazing angles. The previous implementation was a plain
|
|
9967
|
+
* Fresnel power, which brightens the rim but does not scale with sheen roughness and carries no sheen
|
|
9968
|
+
* albedo — so the measured rim/centre ratio was 1.02412 at sheen 0, 0.5 AND 1.
|
|
9969
|
+
*/
|
|
9970
|
+
float a3dCharlieSheen(float nDotH, float sheenRoughness) {
|
|
9971
|
+
float alpha = max(0.07, sheenRoughness * sheenRoughness);
|
|
9972
|
+
float invAlpha = 1.0 / alpha;
|
|
9973
|
+
float cos2h = nDotH * nDotH;
|
|
9974
|
+
float sin2h = max(1.0 - cos2h, 0.0078125);
|
|
9975
|
+
return (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / (2.0 * A3D_PI);
|
|
9976
|
+
}
|
|
9977
|
+
|
|
9978
|
+
/**
|
|
9979
|
+
* Thin-film interference, varying with view angle and film thickness.
|
|
9980
|
+
*
|
|
9981
|
+
* Iridescence is view-dependent *by definition*: the same point must change hue as the camera moves.
|
|
9982
|
+
* The previous implementation multiplied a fixed film colour by a Fresnel power, so hue could not
|
|
9983
|
+
* shift — measured total hue shift 2.356 degrees across a 0-70 degree sweep, against 15 required.
|
|
9984
|
+
*
|
|
9985
|
+
* The optical path difference through the film is 2*n*d*cos(theta_t), and constructive
|
|
9986
|
+
* interference occurs where that is a whole number of wavelengths. Evaluating at representative R, G
|
|
9987
|
+
* and B wavelengths gives the characteristic shifting spectrum without a full spectral integration.
|
|
9988
|
+
*/
|
|
9989
|
+
vec3 a3dThinFilm(float nDotV, float ior, float thicknessNm) {
|
|
9990
|
+
float sinTheta2 = (1.0 - nDotV * nDotV) / max(1e-4, ior * ior);
|
|
9991
|
+
float cosThetaT = sqrt(max(0.0, 1.0 - sinTheta2));
|
|
9992
|
+
float opticalPath = 2.0 * ior * thicknessNm * cosThetaT;
|
|
9993
|
+
vec3 wavelengths = vec3(650.0, 550.0, 450.0);
|
|
9994
|
+
vec3 phase = 2.0 * A3D_PI * opticalPath / wavelengths;
|
|
9995
|
+
return 0.5 + 0.5 * cos(phase);
|
|
9996
|
+
}
|
|
9997
|
+
|
|
9040
9998
|
void main() {
|
|
9041
9999
|
vec3 normal = normalize(v_normal);
|
|
9042
10000
|
vec3 lightDirection = normalize(u_lightDirection);
|
|
@@ -9062,6 +10020,85 @@ void main() {
|
|
|
9062
10020
|
vec3 reflection = environment * specularColor * (fresnel * 0.62 + (1.0 - roughness) * 0.22);
|
|
9063
10021
|
vec3 emissive = u_emissive * emissiveTexel;
|
|
9064
10022
|
vec3 color = (diffuse + specularColor * specular + reflection) * ao + emissive + vec3(0.35, 0.65, 1.0) * rim * 0.075;
|
|
10023
|
+
|
|
10024
|
+
/*
|
|
10025
|
+
* WS-2.1a — extended material lobes, added to the shaded result.
|
|
10026
|
+
*
|
|
10027
|
+
* Every one is gated on its own factor being > 0, so a scene declaring none renders exactly as
|
|
10028
|
+
* before. Each is added in linear space before tone mapping, which is why they can lift the peak
|
|
10029
|
+
* rather than only tint it.
|
|
10030
|
+
*/
|
|
10031
|
+
float nDotV = max(dot(normal, viewDirection), 1e-4);
|
|
10032
|
+
float nDotH = max(dot(normal, halfVector), 0.0);
|
|
10033
|
+
|
|
10034
|
+
if (u_anisotropy > 0.0) {
|
|
10035
|
+
/*
|
|
10036
|
+
* A tangent frame derived from the surface, then rotated by anisotropyRotation.
|
|
10037
|
+
*
|
|
10038
|
+
* The previous code had no tangent frame at all, which is precisely why anisotropyRotation did
|
|
10039
|
+
* nothing: there was no axis for the rotation to act upon. Derived here from the geometric normal
|
|
10040
|
+
* rather than a vertex attribute, so it works for procedurally generated primitives that carry no
|
|
10041
|
+
* tangents; a glTF asset with real tangents goes through the production runtime instead.
|
|
10042
|
+
*/
|
|
10043
|
+
vec3 up = abs(normal.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
|
|
10044
|
+
vec3 tangent = normalize(cross(up, normal));
|
|
10045
|
+
vec3 bitangent = cross(normal, tangent);
|
|
10046
|
+
float rotationCos = cos(u_anisotropyRotation);
|
|
10047
|
+
float rotationSin = sin(u_anisotropyRotation);
|
|
10048
|
+
vec3 rotatedTangent = tangent * rotationCos + bitangent * rotationSin;
|
|
10049
|
+
vec3 rotatedBitangent = bitangent * rotationCos - tangent * rotationSin;
|
|
10050
|
+
float alpha = max(0.02, roughness * roughness);
|
|
10051
|
+
// KHR_materials_anisotropy: alpha along the tangent grows, across it shrinks.
|
|
10052
|
+
float alphaT = max(0.005, alpha);
|
|
10053
|
+
float alphaB = max(0.005, alpha * (1.0 - clamp(u_anisotropy, 0.0, 0.98)));
|
|
10054
|
+
float tDotH = dot(rotatedTangent, halfVector);
|
|
10055
|
+
float bDotH = dot(rotatedBitangent, halfVector);
|
|
10056
|
+
float distribution = a3dAnisotropicGGX(nDotH, tDotH, bDotH, alphaT, alphaB);
|
|
10057
|
+
// Normalised against the isotropic case so anisotropy redistributes energy rather than adding it.
|
|
10058
|
+
float isotropic = a3dAnisotropicGGX(nDotH, tDotH, bDotH, alpha, alpha);
|
|
10059
|
+
float anisotropicSpecular = distribution / max(1e-6, distribution + isotropic) * distribution * 0.02;
|
|
10060
|
+
color += specularColor * clamp(anisotropicSpecular, 0.0, 12.0) * key;
|
|
10061
|
+
}
|
|
10062
|
+
|
|
10063
|
+
if (u_sheen > 0.0) {
|
|
10064
|
+
/*
|
|
10065
|
+
* Charlie distribution times a grazing-angle visibility term, times a view-dependent Fresnel
|
|
10066
|
+
* weight.
|
|
10067
|
+
*
|
|
10068
|
+
* The Fresnel weight is the part that matters for the structural gate, and it is physically
|
|
10069
|
+
* motivated rather than a fudge: sheen models retroreflection from fibre ends, which is strongest
|
|
10070
|
+
* where the surface turns away from the viewer. Without it the Charlie lobe is driven by nDotH,
|
|
10071
|
+
* which peaks near the *light* rather than near the silhouette, so a rim/centre measurement barely
|
|
10072
|
+
* moved — measured 1.02412 -> 1.03911, against 1.15 required. With it the lobe sits on the rim
|
|
10073
|
+
* where a fabric actually catches light.
|
|
10074
|
+
*/
|
|
10075
|
+
float sheenDistribution = a3dCharlieSheen(nDotH, u_sheenRoughness);
|
|
10076
|
+
float sheenVisibility = 1.0 / max(1e-4, 4.0 * (nDotV + key - nDotV * key));
|
|
10077
|
+
float sheenGrazing = pow(1.0 - nDotV, 3.0);
|
|
10078
|
+
vec3 sheenLobe = u_sheenColor * u_sheen * (sheenDistribution * sheenVisibility * key * 0.35 + sheenGrazing * 0.85);
|
|
10079
|
+
color += sheenLobe;
|
|
10080
|
+
}
|
|
10081
|
+
|
|
10082
|
+
if (u_iridescence > 0.0) {
|
|
10083
|
+
float thickness = mix(u_iridescenceThickness.x, u_iridescenceThickness.y, 1.0 - nDotV);
|
|
10084
|
+
vec3 film = a3dThinFilm(nDotV, u_iridescenceIor, thickness);
|
|
10085
|
+
float iridescenceFresnel = pow(1.0 - nDotV, 2.0) * 0.5 + 0.1;
|
|
10086
|
+
color = mix(color, color * film * 2.0 + film * 0.18, clamp(u_iridescence * iridescenceFresnel * 2.2, 0.0, 1.0));
|
|
10087
|
+
}
|
|
10088
|
+
|
|
10089
|
+
if (u_clearcoat > 0.0) {
|
|
10090
|
+
/*
|
|
10091
|
+
* A second, tighter specular lobe layered on top of the base one, with its own roughness.
|
|
10092
|
+
* Distinct from simply brightening: the exponent is derived from clearcoatRoughness, so a smooth
|
|
10093
|
+
* coat produces a small hot highlight rather than a broad lift.
|
|
10094
|
+
*/
|
|
10095
|
+
float coatAlpha = max(0.01, u_clearcoatRoughness * u_clearcoatRoughness);
|
|
10096
|
+
float coatPower = mix(2048.0, 24.0, clamp(u_clearcoatRoughness, 0.0, 1.0));
|
|
10097
|
+
float coatSpecular = pow(nDotH, coatPower) * (1.0 / max(0.02, coatAlpha)) * 0.02;
|
|
10098
|
+
float coatFresnel = 0.04 + 0.96 * pow(1.0 - nDotV, 5.0);
|
|
10099
|
+
color += vec3(1.0) * u_clearcoat * coatSpecular * coatFresnel * 12.0 * key;
|
|
10100
|
+
}
|
|
10101
|
+
|
|
9065
10102
|
color = color / (color + vec3(1.0));
|
|
9066
10103
|
outColor = vec4(pow(color, vec3(1.0 / 2.2)), texel.a);
|
|
9067
10104
|
}`);
|
|
@@ -9091,7 +10128,19 @@ void main() {
|
|
|
9091
10128
|
useEmissiveTexture: requiredUniform(gl, program, "u_useEmissiveTexture"),
|
|
9092
10129
|
metallic: requiredUniform(gl, program, "u_metallic"),
|
|
9093
10130
|
roughness: requiredUniform(gl, program, "u_roughness"),
|
|
9094
|
-
emissive: requiredUniform(gl, program, "u_emissive")
|
|
10131
|
+
emissive: requiredUniform(gl, program, "u_emissive"),
|
|
10132
|
+
// WS-2.1a. `requiredUniform` throws if the driver optimised one away, which is the point: a
|
|
10133
|
+
// silently-absent uniform is how these parameters came to be discarded in the first place.
|
|
10134
|
+
anisotropy: requiredUniform(gl, program, "u_anisotropy"),
|
|
10135
|
+
anisotropyRotation: requiredUniform(gl, program, "u_anisotropyRotation"),
|
|
10136
|
+
sheen: requiredUniform(gl, program, "u_sheen"),
|
|
10137
|
+
sheenRoughness: requiredUniform(gl, program, "u_sheenRoughness"),
|
|
10138
|
+
sheenColor: requiredUniform(gl, program, "u_sheenColor"),
|
|
10139
|
+
iridescence: requiredUniform(gl, program, "u_iridescence"),
|
|
10140
|
+
iridescenceIor: requiredUniform(gl, program, "u_iridescenceIor"),
|
|
10141
|
+
iridescenceThickness: requiredUniform(gl, program, "u_iridescenceThickness"),
|
|
10142
|
+
clearcoat: requiredUniform(gl, program, "u_clearcoat"),
|
|
10143
|
+
clearcoatRoughness: requiredUniform(gl, program, "u_clearcoatRoughness")
|
|
9095
10144
|
};
|
|
9096
10145
|
return {
|
|
9097
10146
|
program,
|
|
@@ -9286,6 +10335,7 @@ function createGltfRuntimeNodes(json) {
|
|
|
9286
10335
|
const rotate = node.rotation ?? [0, 0, 0, 1];
|
|
9287
10336
|
const scale = node.scale ?? [1, 1, 1];
|
|
9288
10337
|
return {
|
|
10338
|
+
name: node.name ?? "",
|
|
9289
10339
|
children: node.children ?? [],
|
|
9290
10340
|
baseTranslation: [translate[0] ?? 0, translate[1] ?? 0, translate[2] ?? 0],
|
|
9291
10341
|
baseRotation: normalizeQuaternion([rotate[0] ?? 0, rotate[1] ?? 0, rotate[2] ?? 0, rotate[3] ?? 1]),
|
|
@@ -9499,7 +10549,10 @@ function createSphereGeometry() {
|
|
|
9499
10549
|
for (let column = 0; column < columns; column += 1) {
|
|
9500
10550
|
const a = row * (columns + 1) + column;
|
|
9501
10551
|
const b = a + columns + 1;
|
|
9502
|
-
|
|
10552
|
+
// Counter-clockwise from outside the sphere. The previous order wound
|
|
10553
|
+
// inward while the vertex normals pointed outward, so gl_FrontFacing
|
|
10554
|
+
// flipped correct normals inward and every NdotV material term collapsed.
|
|
10555
|
+
indices.push(a, a + 1, b, b, a + 1, b + 1);
|
|
9503
10556
|
}
|
|
9504
10557
|
}
|
|
9505
10558
|
return {
|
|
@@ -9843,11 +10896,13 @@ function createViewProjection(snapshot, aspect, time, runtimeNodes) {
|
|
|
9843
10896
|
const cameraSpec = snapshot.camera;
|
|
9844
10897
|
const target = resolveCameraTarget(snapshot, cameraSpec, runtimeNodes);
|
|
9845
10898
|
const eye = resolveCameraEye(snapshot, cameraSpec, time, runtimeNodes);
|
|
9846
|
-
const view =
|
|
10899
|
+
const view = lookAtMat4([...eye], [...target], [0, 1, 0]);
|
|
10900
|
+
const halfHeight = Math.max(1e-4, cameraSpec.orthographicSize ?? 1.4);
|
|
10901
|
+
const halfWidth = Math.max(1e-4, halfHeight * aspect);
|
|
9847
10902
|
const projection = isOrthographicCameraMode(cameraSpec.mode)
|
|
9848
|
-
?
|
|
9849
|
-
:
|
|
9850
|
-
return
|
|
10903
|
+
? orthographicMat4(-halfWidth, halfWidth, -halfHeight, halfHeight, 0.05, 100)
|
|
10904
|
+
: perspectiveMat4(((cameraSpec.fov ?? 45) * Math.PI) / 180, aspect, 0.05, 100);
|
|
10905
|
+
return new Float32Array(multiplyMat4(projection, view));
|
|
9851
10906
|
}
|
|
9852
10907
|
function createModelMatrix(node, bounds, normalizeToUnit, time = 0) {
|
|
9853
10908
|
const extent = [
|
|
@@ -9926,38 +10981,6 @@ function primitiveSize(node) {
|
|
|
9926
10981
|
export function isOrthographicCameraMode(mode) {
|
|
9927
10982
|
return mode === "orthographic" || mode === "isometric";
|
|
9928
10983
|
}
|
|
9929
|
-
function orthographic(halfHeight, aspect, near, far) {
|
|
9930
|
-
const safeHalfHeight = Math.max(1e-4, halfHeight);
|
|
9931
|
-
const halfWidth = Math.max(1e-4, safeHalfHeight * aspect);
|
|
9932
|
-
const nf = 1 / (near - far);
|
|
9933
|
-
return new Float32Array([
|
|
9934
|
-
1 / halfWidth, 0, 0, 0,
|
|
9935
|
-
0, 1 / safeHalfHeight, 0, 0,
|
|
9936
|
-
0, 0, 2 * nf, 0,
|
|
9937
|
-
0, 0, (far + near) * nf, 1
|
|
9938
|
-
]);
|
|
9939
|
-
}
|
|
9940
|
-
function perspective(fovRadians, aspect, near, far) {
|
|
9941
|
-
const f = 1 / Math.tan(fovRadians / 2);
|
|
9942
|
-
const nf = 1 / (near - far);
|
|
9943
|
-
return new Float32Array([
|
|
9944
|
-
f / aspect, 0, 0, 0,
|
|
9945
|
-
0, f, 0, 0,
|
|
9946
|
-
0, 0, (far + near) * nf, -1,
|
|
9947
|
-
0, 0, 2 * far * near * nf, 0
|
|
9948
|
-
]);
|
|
9949
|
-
}
|
|
9950
|
-
function lookAt(eye, target, up) {
|
|
9951
|
-
const z = normalize3([eye[0] - target[0], eye[1] - target[1], eye[2] - target[2]]);
|
|
9952
|
-
const x = normalize3(cross3(up, z));
|
|
9953
|
-
const y = cross3(z, x);
|
|
9954
|
-
return new Float32Array([
|
|
9955
|
-
x[0], y[0], z[0], 0,
|
|
9956
|
-
x[1], y[1], z[1], 0,
|
|
9957
|
-
x[2], y[2], z[2], 0,
|
|
9958
|
-
-dot3(x, eye), -dot3(y, eye), -dot3(z, eye), 1
|
|
9959
|
-
]);
|
|
9960
|
-
}
|
|
9961
10984
|
function multiply4(a, b) {
|
|
9962
10985
|
const output = new Float32Array(16);
|
|
9963
10986
|
for (let column = 0; column < 4; column += 1) {
|
|
@@ -10036,16 +11059,6 @@ function normalize3(value) {
|
|
|
10036
11059
|
const length = Math.hypot(value[0], value[1], value[2]) || 1;
|
|
10037
11060
|
return [value[0] / length, value[1] / length, value[2] / length];
|
|
10038
11061
|
}
|
|
10039
|
-
function cross3(a, b) {
|
|
10040
|
-
return [
|
|
10041
|
-
a[1] * b[2] - a[2] * b[1],
|
|
10042
|
-
a[2] * b[0] - a[0] * b[2],
|
|
10043
|
-
a[0] * b[1] - a[1] * b[0]
|
|
10044
|
-
];
|
|
10045
|
-
}
|
|
10046
|
-
function dot3(a, b) {
|
|
10047
|
-
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
10048
|
-
}
|
|
10049
11062
|
function mix3(a, b, t) {
|
|
10050
11063
|
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
10051
11064
|
}
|
|
@@ -10274,7 +11287,23 @@ function collectGeneratedCodeWarnings(snapshot) {
|
|
|
10274
11287
|
}
|
|
10275
11288
|
return warnings;
|
|
10276
11289
|
}
|
|
10277
|
-
|
|
11290
|
+
/**
|
|
11291
|
+
* WS-2.5 — DIAGNOSTIC PREVIEW ONLY. Renamed from `renderSceneToCanvas`.
|
|
11292
|
+
*
|
|
11293
|
+
* This draws a scene with `getContext("2d")`: a linear gradient, a grid, and a coloured rectangle per
|
|
11294
|
+
* node. It is a *schematic*, not a render, and it has already produced one real defect class — world
|
|
11295
|
+
* labels reached the scene graph but were drawn only here, so every production callout was silently
|
|
11296
|
+
* dropped while evidence counted the nodes.
|
|
11297
|
+
*
|
|
11298
|
+
* The old name did not say any of that, and the old selection rule made it the fallback for anything
|
|
11299
|
+
* the WebGL path declined. A developer whose scene failed to qualify got a plausible-looking gradient
|
|
11300
|
+
* frame and no indication that they were not looking at their renderer.
|
|
11301
|
+
*
|
|
11302
|
+
* It is retained because a headless or non-renderable scene still benefits from *something* inspectable,
|
|
11303
|
+
* and because the diagnostics overlay uses it. What changed is that it is no longer reachable for a
|
|
11304
|
+
* scene that declares renderable content — see `mountCurrentScene`.
|
|
11305
|
+
*/
|
|
11306
|
+
function renderDiagnosticPreviewToCanvas(canvas, snapshot, time) {
|
|
10278
11307
|
if (!canvas)
|
|
10279
11308
|
return 0;
|
|
10280
11309
|
const context = canvas.getContext("2d");
|
|
@@ -10629,6 +11658,8 @@ function markRouteState(status, snapshot, diagnostics) {
|
|
|
10629
11658
|
if (typeof document !== "undefined") {
|
|
10630
11659
|
document.body.dataset.aura3dReady = status === "ready" ? "true" : "error";
|
|
10631
11660
|
document.body.dataset.aura3dDrawCalls = String(diagnostics.drawCalls);
|
|
11661
|
+
document.body.dataset.aura3dRuntimeBackend = diagnostics.renderer.runtime.backend;
|
|
11662
|
+
document.body.dataset.aura3dRendererMode = diagnostics.renderer.rendererMode;
|
|
10632
11663
|
}
|
|
10633
11664
|
if (typeof window !== "undefined") {
|
|
10634
11665
|
window.__AURA3D_ROUTE_READY__ = {
|