@aura3d/engine 1.5.1 → 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 -83
- 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/SpatialAnchoring.d.ts +47 -0
- package/dist/engine/agent-api/SpatialAnchoring.d.ts.map +1 -1
- package/dist/engine/agent-api/SpatialAnchoring.js +46 -0
- package/dist/engine/agent-api/SpatialAnchoring.js.map +1 -1
- 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 +240 -14
- package/dist/engine/agent-api/index.d.ts.map +1 -1
- package/dist/engine/agent-api/index.js +1435 -344
- 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/CameraFraming.d.ts +77 -0
- package/dist/rendering/CameraFraming.d.ts.map +1 -1
- package/dist/rendering/CameraFraming.js +176 -1
- package/dist/rendering/CameraFraming.js.map +1 -1
- 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 +28 -4
- package/dist/rendering/Renderer.d.ts.map +1 -1
- package/dist/rendering/Renderer.js +34 -12
- 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 +28 -35
- package/dist/rendering/index.d.ts.map +1 -1
- package/dist/rendering/index.js +36 -20
- 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 +243 -127
- 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)
|
|
@@ -1062,6 +1153,50 @@ export const camera = {
|
|
|
1062
1153
|
easing: options.easing ?? "easeInOut",
|
|
1063
1154
|
captureTime: options.captureTime
|
|
1064
1155
|
}),
|
|
1156
|
+
/**
|
|
1157
|
+
* A parallel-projection camera: no foreshortening, so equal world lengths
|
|
1158
|
+
* occupy equal screen lengths wherever they sit in depth.
|
|
1159
|
+
*
|
|
1160
|
+
* Reach for this when the projection is part of the meaning rather than a
|
|
1161
|
+
* stylistic choice — CAD and engineering views, floor plans, technical
|
|
1162
|
+
* diagrams, sprite and texture bakes, chart axes, and product shots that must
|
|
1163
|
+
* read as measurable. A long-lens perspective camera approximates it but
|
|
1164
|
+
* still converges, which is visible on long straight edges.
|
|
1165
|
+
*/
|
|
1166
|
+
orthographic: (options = {}) => ({
|
|
1167
|
+
mode: "orthographic",
|
|
1168
|
+
position: options.position ?? [0, 1.4, 4],
|
|
1169
|
+
target: options.target ?? [0, 0.8, 0],
|
|
1170
|
+
orthographicSize: options.orthographicSize ?? 1.4
|
|
1171
|
+
}),
|
|
1172
|
+
/**
|
|
1173
|
+
* The conventional isometric view: an orthographic camera on a 45-degree
|
|
1174
|
+
* azimuth and a ~35.264-degree elevation.
|
|
1175
|
+
*
|
|
1176
|
+
* That elevation is `atan(1 / sqrt(2))`, the angle at which the three world
|
|
1177
|
+
* axes project to equal screen lengths and 120 degrees apart. Authoring it as
|
|
1178
|
+
* a preset matters because the value is not memorable and an approximation
|
|
1179
|
+
* such as 30 or 45 degrees produces the subtly-wrong grid alignment that
|
|
1180
|
+
* isometric tile art immediately reveals.
|
|
1181
|
+
*/
|
|
1182
|
+
isometric: (options = {}) => {
|
|
1183
|
+
const target = options.target ?? [0, 0, 0];
|
|
1184
|
+
const distance = options.distance ?? 12;
|
|
1185
|
+
const elevation = Math.atan(1 / Math.SQRT2);
|
|
1186
|
+
const azimuth = Math.PI / 4;
|
|
1187
|
+
const horizontal = Math.cos(elevation) * distance;
|
|
1188
|
+
return {
|
|
1189
|
+
mode: "isometric",
|
|
1190
|
+
target,
|
|
1191
|
+
distance,
|
|
1192
|
+
orthographicSize: options.orthographicSize ?? 6,
|
|
1193
|
+
position: options.position ?? [
|
|
1194
|
+
target[0] + Math.sin(azimuth) * horizontal,
|
|
1195
|
+
target[1] + Math.sin(elevation) * distance,
|
|
1196
|
+
target[2] + Math.cos(azimuth) * horizontal
|
|
1197
|
+
]
|
|
1198
|
+
};
|
|
1199
|
+
},
|
|
1065
1200
|
autoFrame: (options = {}) => {
|
|
1066
1201
|
const bounds = options.bounds ?? { min: [-1, 0, -1], max: [1, 1.6, 1] };
|
|
1067
1202
|
const center = options.target ?? [
|
|
@@ -1562,7 +1697,7 @@ const rendererQualityProfiles = {
|
|
|
1562
1697
|
kind: "aura-renderer-quality-profile",
|
|
1563
1698
|
id: "safe-basic",
|
|
1564
1699
|
label: "Safe Basic",
|
|
1565
|
-
rendererMode: "
|
|
1700
|
+
rendererMode: "production",
|
|
1566
1701
|
status: "supported",
|
|
1567
1702
|
antialiasing: "msaa",
|
|
1568
1703
|
pixelRatio: 1,
|
|
@@ -1571,7 +1706,7 @@ const rendererQualityProfiles = {
|
|
|
1571
1706
|
requestedFeatures: ["typed models", "primitives", "basic particles", "base-color textures", "runtime nodes"],
|
|
1572
1707
|
supportedInRoot: ["typed models", "primitives", "basic particles", "base-color textures", "runtime nodes"],
|
|
1573
1708
|
blockedInRoot: ["production PBR parity", "skinned animation mixer", "native WebGPU compute", "postprocess pass chain"],
|
|
1574
|
-
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."
|
|
1575
1710
|
},
|
|
1576
1711
|
production: {
|
|
1577
1712
|
kind: "aura-renderer-quality-profile",
|
|
@@ -1640,8 +1775,6 @@ function analyzeProductionBridgeEligibility(nodes) {
|
|
|
1640
1775
|
const primitiveCount = nodes.filter((node) => node.kind === "primitive").length;
|
|
1641
1776
|
const effectCount = nodes.filter((node) => node.kind === "effect").length;
|
|
1642
1777
|
const reasons = [];
|
|
1643
|
-
if (typedModelCount < 1)
|
|
1644
|
-
reasons.push("production bridge requires at least one typed GLB from generated aura-assets");
|
|
1645
1778
|
if (unsafeModelCount > 0)
|
|
1646
1779
|
reasons.push("unsafeModelUrl and remote/raw model URLs are blocked from the production bridge");
|
|
1647
1780
|
if (inlineModelCount > 0)
|
|
@@ -1704,10 +1837,10 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
|
|
|
1704
1837
|
if (postprocessRequested && runtime?.mounted && !runtimePostprocess?.pixelBacked)
|
|
1705
1838
|
warnings.push("postprocess was requested but the runtime composer did not initialize a pixel-backed pass");
|
|
1706
1839
|
if (rendererSelection.mode === "production" && !runtime?.mounted && productionEligibility.eligible) {
|
|
1707
|
-
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.`);
|
|
1708
1841
|
}
|
|
1709
1842
|
if (rendererSelection.mode === "production" && !productionEligibility.eligible) {
|
|
1710
|
-
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("; ")}.`);
|
|
1711
1844
|
}
|
|
1712
1845
|
if (rendererSelection.profile.status === "fallback-only") {
|
|
1713
1846
|
warnings.push(`Renderer profile "${rendererSelection.profile.id}" is a request profile; public claims require route-specific runtime diagnostics and screenshot proof.`);
|
|
@@ -1793,6 +1926,7 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
|
|
|
1793
1926
|
requestedPasses,
|
|
1794
1927
|
actualPasses: runtimePostprocess?.actualPasses ?? [],
|
|
1795
1928
|
fallbackPasses: runtimePostprocess?.fallbackPasses ?? [],
|
|
1929
|
+
targetFormat: runtimePostprocess?.targetFormat,
|
|
1796
1930
|
evidence: postprocessEvidence
|
|
1797
1931
|
},
|
|
1798
1932
|
runtime: {
|
|
@@ -1800,7 +1934,13 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
|
|
|
1800
1934
|
backend: runtime?.backend ?? "scene-plan",
|
|
1801
1935
|
postprocessVerified: runtimePostprocess?.pixelBacked ?? false,
|
|
1802
1936
|
passNames: runtimePostprocess?.actualPasses ?? [],
|
|
1803
|
-
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 ?? []
|
|
1804
1944
|
},
|
|
1805
1945
|
environment: {
|
|
1806
1946
|
enabled: environmentStatus.enabled,
|
|
@@ -2210,6 +2350,7 @@ function createRuntimeScenePhysics(snapshot) {
|
|
|
2210
2350
|
const physicsNodes = snapshot.nodes.filter((node) => (node.kind === "model" || node.kind === "primitive") && Boolean(node.physics));
|
|
2211
2351
|
if (physicsNodes.length === 0)
|
|
2212
2352
|
return undefined;
|
|
2353
|
+
// WS-2.2: solver resolved through the on-demand cache rather than a static import.
|
|
2213
2354
|
const world = new PhysicsWorld({ gravity: [0, -9.81, 0], fixedDelta: 1 / 60, enableSleeping: true });
|
|
2214
2355
|
const stepper = new PhysicsStepper(world.fixedDelta);
|
|
2215
2356
|
const debugDraw = new PhysicsDebugDraw();
|
|
@@ -2610,6 +2751,8 @@ export const prefabs = {
|
|
|
2610
2751
|
...makeCityCrosswalk("zebra crosswalk west", -0.72, 0, "eastWest"),
|
|
2611
2752
|
...makeCityCrosswalk("zebra crosswalk east", 0.72, 0, "eastWest")
|
|
2612
2753
|
];
|
|
2754
|
+
const cityTowerTransforms = [];
|
|
2755
|
+
const cityTowerColors = [];
|
|
2613
2756
|
const xSlots = [-4.25, -2.58, -0.95, 1.45, 3.3];
|
|
2614
2757
|
const zSlots = [-4, -1.45, 1.3, 3.65];
|
|
2615
2758
|
for (let index = 0; index < blocks; index += 1) {
|
|
@@ -2621,15 +2764,19 @@ export const prefabs = {
|
|
|
2621
2764
|
const color = night
|
|
2622
2765
|
? (index % 3 === 0 ? "#1e293b" : index % 3 === 1 ? "#2d3340" : "#172233")
|
|
2623
2766
|
: (index % 3 === 0 ? "#8ea2aa" : index % 3 === 1 ? "#b89b72" : "#668094");
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
material: material.pbr({ color, roughness: 0.68, metallic: 0.06 })
|
|
2627
|
-
}).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);
|
|
2628
2769
|
if (options.litWindows !== false) {
|
|
2629
2770
|
nodes.push(...makeBuildingWindowRows(x, z, height, index, timeOfDay));
|
|
2630
2771
|
}
|
|
2631
2772
|
nodes.push(...makeBuildingDetails(x, z, height, index, timeOfDay));
|
|
2632
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());
|
|
2633
2780
|
const lampPositions = [
|
|
2634
2781
|
[-1.15, 0, 0.85], [1.15, 0, 0.85], [-1.15, 0, -0.85], [1.15, 0, -0.85],
|
|
2635
2782
|
[-3.85, 0, -2.05], [-2.95, 0, 2.05], [2.05, 0, -2.05], [3.05, 0, 2.05],
|
|
@@ -2725,7 +2872,9 @@ export const prefabs = {
|
|
|
2725
2872
|
const rimMaterial = material.emissive({ color: "#dbeafe", emissive: "#93c5fd", emissiveIntensity: 0.18, opacity: 0.42 });
|
|
2726
2873
|
const nodes = [
|
|
2727
2874
|
primitives.plane({ name: "seamless matte product hero floor", material: floorMaterial }).position(0, -0.018, -0.62).scale([3.6, 1, 2.6]).toJSON(),
|
|
2728
|
-
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(),
|
|
2729
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(),
|
|
2730
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(),
|
|
2731
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(),
|
|
@@ -3327,7 +3476,7 @@ function createMiniGolfStateController() {
|
|
|
3327
3476
|
const worldSnapshot = world.snapshot();
|
|
3328
3477
|
return {
|
|
3329
3478
|
physicsBackend: worldSnapshot.backend.active,
|
|
3330
|
-
deterministicReplayId: `mini-golf-
|
|
3479
|
+
deterministicReplayId: `mini-golf-rapier-v2-${shots}-${score}-${collisions}-${resets}`,
|
|
3331
3480
|
replayFrame: worldSnapshot.stats.steps,
|
|
3332
3481
|
captureTime: Number((worldSnapshot.stats.steps * world.fixedDelta).toFixed(3)),
|
|
3333
3482
|
shots,
|
|
@@ -3434,187 +3583,6 @@ export const games = {
|
|
|
3434
3583
|
export function collectGameRuntimeEvidence(app, options = {}) {
|
|
3435
3584
|
return collectGameRuntimeEvidenceV105(app, options);
|
|
3436
3585
|
}
|
|
3437
|
-
function createGameInputController(options) {
|
|
3438
|
-
const actions = options.actions;
|
|
3439
|
-
const axes = options.axes ?? {};
|
|
3440
|
-
const bufferMs = options.bufferMs ?? 120;
|
|
3441
|
-
const activeBindings = new Set();
|
|
3442
|
-
const activeActionOverrides = new Set();
|
|
3443
|
-
const previousHeld = new Map();
|
|
3444
|
-
const currentHeld = new Map();
|
|
3445
|
-
const pressedEdges = new Set();
|
|
3446
|
-
const releasedEdges = new Set();
|
|
3447
|
-
const lastPressedAt = new Map();
|
|
3448
|
-
const replayEvents = [];
|
|
3449
|
-
let frame = 0;
|
|
3450
|
-
let time = 0;
|
|
3451
|
-
let latestSnapshot = {
|
|
3452
|
-
kind: "aura-game-input-snapshot",
|
|
3453
|
-
frame,
|
|
3454
|
-
time,
|
|
3455
|
-
activeBindings: [],
|
|
3456
|
-
actions: {}
|
|
3457
|
-
};
|
|
3458
|
-
const resolveHeld = (action) => {
|
|
3459
|
-
if (activeActionOverrides.has(action))
|
|
3460
|
-
return true;
|
|
3461
|
-
const bindings = actions[action] ?? [];
|
|
3462
|
-
return bindings.some((binding) => activeBindings.has(binding));
|
|
3463
|
-
};
|
|
3464
|
-
const record = (type, binding) => {
|
|
3465
|
-
replayEvents.push({ frame, time, type, binding });
|
|
3466
|
-
};
|
|
3467
|
-
const pressBinding = (binding, shouldRecord = true) => {
|
|
3468
|
-
activeBindings.add(binding);
|
|
3469
|
-
if (actions[binding])
|
|
3470
|
-
activeActionOverrides.add(binding);
|
|
3471
|
-
if (shouldRecord)
|
|
3472
|
-
record("press", binding);
|
|
3473
|
-
};
|
|
3474
|
-
const releaseBinding = (binding, shouldRecord = true) => {
|
|
3475
|
-
activeBindings.delete(binding);
|
|
3476
|
-
activeActionOverrides.delete(binding);
|
|
3477
|
-
if (shouldRecord)
|
|
3478
|
-
record("release", binding);
|
|
3479
|
-
};
|
|
3480
|
-
const toSnapshot = () => {
|
|
3481
|
-
const actionStates = {};
|
|
3482
|
-
const nowMs = time * 1000;
|
|
3483
|
-
for (const action of Object.keys(actions)) {
|
|
3484
|
-
const held = currentHeld.get(action) ?? false;
|
|
3485
|
-
actionStates[action] = {
|
|
3486
|
-
pressed: pressedEdges.has(action),
|
|
3487
|
-
held,
|
|
3488
|
-
released: releasedEdges.has(action),
|
|
3489
|
-
buffered: pressedEdges.has(action) || nowMs - (lastPressedAt.get(action) ?? Number.NEGATIVE_INFINITY) <= bufferMs,
|
|
3490
|
-
value: held ? 1 : 0
|
|
3491
|
-
};
|
|
3492
|
-
}
|
|
3493
|
-
return {
|
|
3494
|
-
kind: "aura-game-input-snapshot",
|
|
3495
|
-
frame,
|
|
3496
|
-
time,
|
|
3497
|
-
activeBindings: [...activeBindings].sort(),
|
|
3498
|
-
actions: actionStates
|
|
3499
|
-
};
|
|
3500
|
-
};
|
|
3501
|
-
const update = (dt = 1 / 60) => {
|
|
3502
|
-
frame += 1;
|
|
3503
|
-
time += Math.max(0, dt);
|
|
3504
|
-
pressedEdges.clear();
|
|
3505
|
-
releasedEdges.clear();
|
|
3506
|
-
for (const action of Object.keys(actions)) {
|
|
3507
|
-
const held = resolveHeld(action);
|
|
3508
|
-
const wasHeld = previousHeld.get(action) ?? false;
|
|
3509
|
-
currentHeld.set(action, held);
|
|
3510
|
-
if (held && !wasHeld) {
|
|
3511
|
-
pressedEdges.add(action);
|
|
3512
|
-
lastPressedAt.set(action, time * 1000);
|
|
3513
|
-
}
|
|
3514
|
-
if (!held && wasHeld)
|
|
3515
|
-
releasedEdges.add(action);
|
|
3516
|
-
previousHeld.set(action, held);
|
|
3517
|
-
}
|
|
3518
|
-
latestSnapshot = toSnapshot();
|
|
3519
|
-
return latestSnapshot;
|
|
3520
|
-
};
|
|
3521
|
-
const target = options.target ?? (typeof window !== "undefined" ? window : undefined);
|
|
3522
|
-
const onKeyDown = (event) => {
|
|
3523
|
-
const keyboard = event;
|
|
3524
|
-
if (keyboard.repeat)
|
|
3525
|
-
return;
|
|
3526
|
-
if (keyboard.code)
|
|
3527
|
-
pressBinding(keyboard.code);
|
|
3528
|
-
if (keyboard.key && keyboard.key !== keyboard.code)
|
|
3529
|
-
pressBinding(keyboard.key);
|
|
3530
|
-
};
|
|
3531
|
-
const onKeyUp = (event) => {
|
|
3532
|
-
const keyboard = event;
|
|
3533
|
-
if (keyboard.code)
|
|
3534
|
-
releaseBinding(keyboard.code);
|
|
3535
|
-
if (keyboard.key && keyboard.key !== keyboard.code)
|
|
3536
|
-
releaseBinding(keyboard.key);
|
|
3537
|
-
};
|
|
3538
|
-
if (options.autoListen !== false && target?.addEventListener) {
|
|
3539
|
-
target.addEventListener("keydown", onKeyDown);
|
|
3540
|
-
target.addEventListener("keyup", onKeyUp);
|
|
3541
|
-
}
|
|
3542
|
-
return {
|
|
3543
|
-
kind: "aura-game-input-plan",
|
|
3544
|
-
actions,
|
|
3545
|
-
axes,
|
|
3546
|
-
bufferMs,
|
|
3547
|
-
update,
|
|
3548
|
-
snapshot() {
|
|
3549
|
-
return latestSnapshot;
|
|
3550
|
-
},
|
|
3551
|
-
pressed(action) {
|
|
3552
|
-
return pressedEdges.has(action);
|
|
3553
|
-
},
|
|
3554
|
-
held(action) {
|
|
3555
|
-
return currentHeld.get(action) ?? resolveHeld(action);
|
|
3556
|
-
},
|
|
3557
|
-
released(action) {
|
|
3558
|
-
return releasedEdges.has(action);
|
|
3559
|
-
},
|
|
3560
|
-
buffered(action, windowMs = bufferMs) {
|
|
3561
|
-
return pressedEdges.has(action) || time * 1000 - (lastPressedAt.get(action) ?? Number.NEGATIVE_INFINITY) <= windowMs;
|
|
3562
|
-
},
|
|
3563
|
-
axis(name, negativeAction, positiveAction) {
|
|
3564
|
-
const binding = axes[name];
|
|
3565
|
-
const negative = negativeAction ?? binding?.negative;
|
|
3566
|
-
const positive = positiveAction ?? binding?.positive;
|
|
3567
|
-
if (!negative && !positive)
|
|
3568
|
-
return this.held(name) ? 1 : 0;
|
|
3569
|
-
return (positive && this.held(positive) ? 1 : 0) - (negative && this.held(negative) ? 1 : 0);
|
|
3570
|
-
},
|
|
3571
|
-
press(binding) {
|
|
3572
|
-
pressBinding(binding);
|
|
3573
|
-
},
|
|
3574
|
-
release(binding) {
|
|
3575
|
-
releaseBinding(binding);
|
|
3576
|
-
},
|
|
3577
|
-
setAction(action, held) {
|
|
3578
|
-
if (held) {
|
|
3579
|
-
activeActionOverrides.add(action);
|
|
3580
|
-
record("press", action);
|
|
3581
|
-
}
|
|
3582
|
-
else {
|
|
3583
|
-
activeActionOverrides.delete(action);
|
|
3584
|
-
record("release", action);
|
|
3585
|
-
}
|
|
3586
|
-
},
|
|
3587
|
-
recorded() {
|
|
3588
|
-
return [...replayEvents];
|
|
3589
|
-
},
|
|
3590
|
-
replay(events) {
|
|
3591
|
-
activeBindings.clear();
|
|
3592
|
-
activeActionOverrides.clear();
|
|
3593
|
-
for (const event of events) {
|
|
3594
|
-
if (event.type === "press")
|
|
3595
|
-
pressBinding(event.binding, false);
|
|
3596
|
-
else
|
|
3597
|
-
releaseBinding(event.binding, false);
|
|
3598
|
-
}
|
|
3599
|
-
return update(0);
|
|
3600
|
-
},
|
|
3601
|
-
clearReplay() {
|
|
3602
|
-
replayEvents.length = 0;
|
|
3603
|
-
},
|
|
3604
|
-
dispose() {
|
|
3605
|
-
if (target?.removeEventListener) {
|
|
3606
|
-
target.removeEventListener("keydown", onKeyDown);
|
|
3607
|
-
target.removeEventListener("keyup", onKeyUp);
|
|
3608
|
-
}
|
|
3609
|
-
activeBindings.clear();
|
|
3610
|
-
activeActionOverrides.clear();
|
|
3611
|
-
previousHeld.clear();
|
|
3612
|
-
currentHeld.clear();
|
|
3613
|
-
pressedEdges.clear();
|
|
3614
|
-
releasedEdges.clear();
|
|
3615
|
-
}
|
|
3616
|
-
};
|
|
3617
|
-
}
|
|
3618
3586
|
export function createAuraGameRules(options = {}) {
|
|
3619
3587
|
return {
|
|
3620
3588
|
kind: "aura-game-rules",
|
|
@@ -5341,12 +5309,16 @@ function cityScene(options = {}) {
|
|
|
5341
5309
|
.camera(cityCameraPreset(options.cameraPreset ?? "overview", timeOfDay));
|
|
5342
5310
|
}
|
|
5343
5311
|
function collectCityInstancingPlan(nodes) {
|
|
5344
|
-
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);
|
|
5345
5316
|
const windows = names.filter((name) => name.includes("window column")).length;
|
|
5346
5317
|
const props = names.filter((name) => name.includes("bench") || name.includes("tree") || name.includes("sign") || name.includes("car body") || name.includes("traffic signal")).length;
|
|
5347
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;
|
|
5348
5319
|
const lights = names.filter((name) => name.includes("street lamp") || name.includes("headlight") || name.includes("lamp glow") || name.includes("city glow")).length;
|
|
5349
5320
|
const groupsList = [
|
|
5321
|
+
nativeInstanceNodes.some((node) => node.name?.includes("city tower")) ? "city towers" : "",
|
|
5350
5322
|
windows >= 8 ? "window columns" : "",
|
|
5351
5323
|
props >= 6 ? "street props" : "",
|
|
5352
5324
|
roadMarkings >= 8 ? "road markings" : "",
|
|
@@ -5354,13 +5326,15 @@ function collectCityInstancingPlan(nodes) {
|
|
|
5354
5326
|
].filter(Boolean);
|
|
5355
5327
|
return {
|
|
5356
5328
|
kind: "aura-city-instancing-plan",
|
|
5357
|
-
rendererPath: "
|
|
5329
|
+
rendererPath: "productionRuntimeNativeInstancing",
|
|
5358
5330
|
windows,
|
|
5359
5331
|
props,
|
|
5360
5332
|
roadMarkings,
|
|
5361
5333
|
lights,
|
|
5334
|
+
nativeInstanceGroups: nativeInstanceNodes.length,
|
|
5335
|
+
nativeInstances,
|
|
5362
5336
|
groups: groupsList,
|
|
5363
|
-
instanced:
|
|
5337
|
+
instanced: nativeInstanceNodes.length > 0 && nativeInstances > 0
|
|
5364
5338
|
};
|
|
5365
5339
|
}
|
|
5366
5340
|
function changedCityNodeNames(previous, next) {
|
|
@@ -5373,7 +5347,11 @@ function changedCityNodeNames(previous, next) {
|
|
|
5373
5347
|
function validateCityVisualQA(nodes, options = {}) {
|
|
5374
5348
|
const flattened = groups.flatten(nodes);
|
|
5375
5349
|
const names = flattened.map((node) => "name" in node ? node.name ?? "" : "");
|
|
5376
|
-
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);
|
|
5377
5355
|
const windows = names.filter((name) => name.includes("window column")).length;
|
|
5378
5356
|
const streets = names.filter((name) => name.includes("road") || name.includes("street") || name.includes("avenue")).length;
|
|
5379
5357
|
const crosswalks = names.filter((name) => name.includes("crosswalk")).length;
|
|
@@ -5901,7 +5879,7 @@ function createSceneKitLazyLoadingPlan(id) {
|
|
|
5901
5879
|
systems.push({ system, trigger, loadedByDefault: false, evidence });
|
|
5902
5880
|
};
|
|
5903
5881
|
if (id === "physicsPlayground" || id === "miniGolf") {
|
|
5904
|
-
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");
|
|
5905
5883
|
}
|
|
5906
5884
|
if (id === "productViewer") {
|
|
5907
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");
|
|
@@ -6171,10 +6149,11 @@ export const promptRecipes = {
|
|
|
6171
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]))
|
|
6172
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]))
|
|
6173
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))
|
|
6174
|
-
.add(lights.ambient({ intensity: 0.
|
|
6152
|
+
.add(lights.ambient({ intensity: 0.22, color: "#b4c9e8" }))
|
|
6175
6153
|
.add(lights.point({ name: "hard cyan rim", position: [-2.35, 2.65, 0.85], color: "#38d6ff", intensity: 3.25 }))
|
|
6176
6154
|
.add(lights.point({ name: "warm practical key", position: [2.35, 1.7, -0.25], color: "#ffd08a", intensity: 1.6 }))
|
|
6177
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 }))
|
|
6178
6157
|
.add(effects.rain({ intensity: 0.46, color: "#c3e6ff" }))
|
|
6179
6158
|
.add(effects.fog({ density: 0.08, color: "#32435a" }))
|
|
6180
6159
|
.add(effects.bloom({ intensity: 0.36, color: "#6edfff" }))
|
|
@@ -6675,10 +6654,137 @@ export function createAuraApp(target, options) {
|
|
|
6675
6654
|
let disposed = false;
|
|
6676
6655
|
let animationHandle = 0;
|
|
6677
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
|
+
};
|
|
6678
6712
|
let lastTime = 0;
|
|
6679
6713
|
let mountRevision = 0;
|
|
6680
6714
|
let canvasRuntimePhysics;
|
|
6681
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);
|
|
6682
6788
|
const frameCallbacks = new Set();
|
|
6683
6789
|
let runtimePaused = options.autoStart === false;
|
|
6684
6790
|
let runtimeFrame = 0;
|
|
@@ -6731,7 +6837,16 @@ export function createAuraApp(target, options) {
|
|
|
6731
6837
|
}
|
|
6732
6838
|
diagnosticsState.evidence = collectAuraSceneEvidence(renderSnapshot);
|
|
6733
6839
|
diagnosticsState.fps = Math.round(1000 / delta);
|
|
6734
|
-
|
|
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);
|
|
6735
6850
|
if (canvas)
|
|
6736
6851
|
diagnosticsState.renderSize = [canvas.width, canvas.height];
|
|
6737
6852
|
overlay?.update();
|
|
@@ -6749,24 +6864,87 @@ export function createAuraApp(target, options) {
|
|
|
6749
6864
|
productionController = undefined;
|
|
6750
6865
|
lastTime = 0;
|
|
6751
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
|
+
}
|
|
6752
6895
|
const backend = shouldUseProductionRenderer ? "webgl2" : canvas ? "canvas2d" : "headless";
|
|
6753
6896
|
resetDiagnosticsForCurrentScene(backend);
|
|
6754
6897
|
canvasRuntimePhysics = shouldUseProductionRenderer ? undefined : createRuntimeScenePhysics(renderSnapshot);
|
|
6755
6898
|
overlay?.update();
|
|
6756
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;
|
|
6757
6916
|
if (shouldUseProductionRenderer && canvas) {
|
|
6758
|
-
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)
|
|
6759
6929
|
.then((controller) => {
|
|
6760
6930
|
if (disposed || revision !== mountRevision) {
|
|
6931
|
+
productionMountPending = false;
|
|
6932
|
+
settleMount();
|
|
6761
6933
|
controller.dispose();
|
|
6762
6934
|
return;
|
|
6763
6935
|
}
|
|
6764
6936
|
productionController = controller;
|
|
6937
|
+
productionMountPending = false;
|
|
6938
|
+
attachDeviceListeners(controller);
|
|
6939
|
+
settleMount();
|
|
6765
6940
|
markRouteReady(snapshot, diagnosticsState);
|
|
6766
6941
|
})
|
|
6767
6942
|
.catch((error) => {
|
|
6768
6943
|
if (disposed || revision !== mountRevision)
|
|
6769
6944
|
return;
|
|
6945
|
+
productionMountPending = false;
|
|
6946
|
+
productionMountFailed = true;
|
|
6947
|
+
settleMount();
|
|
6770
6948
|
diagnosticsState.backend = "webgl2";
|
|
6771
6949
|
diagnosticsState.errors.push(productionRenderErrorMessage(error));
|
|
6772
6950
|
overlay?.update();
|
|
@@ -6778,7 +6956,7 @@ export function createAuraApp(target, options) {
|
|
|
6778
6956
|
markRouteReady(snapshot, diagnosticsState);
|
|
6779
6957
|
};
|
|
6780
6958
|
mountCurrentScene();
|
|
6781
|
-
|
|
6959
|
+
const app = {
|
|
6782
6960
|
canvas,
|
|
6783
6961
|
get scene() {
|
|
6784
6962
|
return snapshot;
|
|
@@ -6790,9 +6968,24 @@ export function createAuraApp(target, options) {
|
|
|
6790
6968
|
snapshot = normalizeSceneSnapshot(nextScene);
|
|
6791
6969
|
renderSnapshot = flattenSceneSnapshot(snapshot);
|
|
6792
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);
|
|
6793
6978
|
mountCurrentScene();
|
|
6794
6979
|
},
|
|
6795
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
|
+
},
|
|
6796
6989
|
get runtime() {
|
|
6797
6990
|
return {
|
|
6798
6991
|
paused: runtimePaused,
|
|
@@ -6819,23 +7012,112 @@ export function createAuraApp(target, options) {
|
|
|
6819
7012
|
pause() {
|
|
6820
7013
|
runtimePaused = true;
|
|
6821
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
|
+
},
|
|
6822
7034
|
resume() {
|
|
6823
7035
|
runtimePaused = false;
|
|
6824
7036
|
if (!animationHandle && !productionController && options.autoStart !== false && typeof requestAnimationFrame !== "undefined") {
|
|
6825
7037
|
animationHandle = requestAnimationFrame(render);
|
|
6826
7038
|
}
|
|
6827
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
|
+
},
|
|
6828
7063
|
step(dt = 1 / 60) {
|
|
6829
7064
|
const seconds = Math.max(0, dt);
|
|
6830
7065
|
runRuntimeFrame(seconds, "manual");
|
|
6831
7066
|
canvasRuntimePhysics?.step(seconds);
|
|
6832
7067
|
const previousPaused = runtimePaused;
|
|
6833
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;
|
|
6834
7085
|
if (productionController) {
|
|
6835
|
-
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();
|
|
6836
7110
|
}
|
|
6837
7111
|
else {
|
|
6838
|
-
|
|
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;
|
|
6839
7121
|
}
|
|
6840
7122
|
runtimePaused = previousPaused;
|
|
6841
7123
|
},
|
|
@@ -6859,6 +7141,16 @@ export function createAuraApp(target, options) {
|
|
|
6859
7141
|
},
|
|
6860
7142
|
dispose() {
|
|
6861
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();
|
|
6862
7154
|
if (animationHandle && typeof cancelAnimationFrame !== "undefined")
|
|
6863
7155
|
cancelAnimationFrame(animationHandle);
|
|
6864
7156
|
for (const controller of ownedInputControllers)
|
|
@@ -6866,9 +7158,77 @@ export function createAuraApp(target, options) {
|
|
|
6866
7158
|
ownedInputControllers.clear();
|
|
6867
7159
|
productionController?.dispose();
|
|
6868
7160
|
overlay?.dispose();
|
|
7161
|
+
unregisterAuraApp(app);
|
|
6869
7162
|
}
|
|
6870
7163
|
};
|
|
7164
|
+
registerAuraApp(app);
|
|
7165
|
+
return app;
|
|
6871
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
|
+
};
|
|
6872
7232
|
export function createGameApp(target, options) {
|
|
6873
7233
|
const { input, loop, runtimeEvidence, ...appOptions } = options;
|
|
6874
7234
|
const app = createAuraApp(target, { ...appOptions, autoStart: false });
|
|
@@ -7124,6 +7484,14 @@ function worldLabelsFromSnapshot(snapshot, runtimeNodes) {
|
|
|
7124
7484
|
fontSize: Math.max(11, Math.min(26, Math.round((node.size ?? 0.34) * 46))),
|
|
7125
7485
|
leader: node.leader === true,
|
|
7126
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,
|
|
7127
7495
|
...(node.label === "hud" && node.screenAnchor ? { screenAnchor: node.screenAnchor } : {}),
|
|
7128
7496
|
...(node.label === "hud" && !node.screenAnchor ? { screenAnchor: "top-left" } : {})
|
|
7129
7497
|
};
|
|
@@ -7143,15 +7511,154 @@ function createSceneLabelLayer(canvas, snapshot) {
|
|
|
7143
7511
|
return undefined;
|
|
7144
7512
|
return createWorldLabelLayer({ container: canvas.parentElement ?? document.body, canvas });
|
|
7145
7513
|
}
|
|
7146
|
-
|
|
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) {
|
|
7147
7630
|
const renderableNode = snapshot.nodes.find(isWebGLRenderableNode);
|
|
7148
7631
|
if (!renderableNode) {
|
|
7149
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().");
|
|
7150
7633
|
}
|
|
7151
7634
|
const renderer = await createProductionSceneRenderer(canvas, snapshot, options.renderer, runtimeNodes);
|
|
7152
7635
|
diagnosticsState.renderer = renderer.diagnostics;
|
|
7153
|
-
const
|
|
7636
|
+
const sceneWantsFrames = shouldContinuouslyRender(snapshot);
|
|
7154
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);
|
|
7155
7662
|
let disposed = false;
|
|
7156
7663
|
let animationHandle = 0;
|
|
7157
7664
|
let lastTime = 0;
|
|
@@ -7162,7 +7669,21 @@ async function startProductionRender(canvas, snapshot, diagnosticsState, options
|
|
|
7162
7669
|
lastTime = time;
|
|
7163
7670
|
if (!isPaused())
|
|
7164
7671
|
beforeRender?.(delta / 1000, "raf");
|
|
7165
|
-
|
|
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);
|
|
7166
7687
|
diagnosticsState.backend = renderer.backend;
|
|
7167
7688
|
diagnosticsState.fps = diagnosticsState.fps || 60;
|
|
7168
7689
|
diagnosticsState.drawCalls = drawCalls;
|
|
@@ -7173,28 +7694,66 @@ async function startProductionRender(canvas, snapshot, diagnosticsState, options
|
|
|
7173
7694
|
// Reproject every frame against the renderer's own camera so labels track
|
|
7174
7695
|
// their anchors while the camera moves.
|
|
7175
7696
|
labelLayer.setLabels(worldLabelsFromSnapshot(snapshot, runtimeNodes));
|
|
7176
|
-
|
|
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));
|
|
7177
7704
|
diagnosticsState.labels = labelLayer.snapshot();
|
|
7178
7705
|
}
|
|
7179
7706
|
overlay?.update();
|
|
7180
|
-
if (
|
|
7707
|
+
if ((sceneWantsFrames || requiresFrames()) && options.autoStart !== false && typeof requestAnimationFrame !== "undefined") {
|
|
7181
7708
|
animationHandle = requestAnimationFrame(renderFrame);
|
|
7182
7709
|
}
|
|
7183
7710
|
};
|
|
7184
7711
|
renderFrame();
|
|
7185
7712
|
return {
|
|
7186
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;
|
|
7187
7724
|
renderFrame(time);
|
|
7725
|
+
lastTime = previousLastTime;
|
|
7188
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,
|
|
7189
7731
|
dispose() {
|
|
7190
7732
|
disposed = true;
|
|
7191
7733
|
if (animationHandle && typeof cancelAnimationFrame !== "undefined")
|
|
7192
7734
|
cancelAnimationFrame(animationHandle);
|
|
7735
|
+
resizeObserver?.disconnect();
|
|
7736
|
+
if (typeof window !== "undefined")
|
|
7737
|
+
window.removeEventListener("resize", resizeRenderer);
|
|
7193
7738
|
labelLayer?.dispose();
|
|
7194
7739
|
renderer.dispose();
|
|
7195
7740
|
}
|
|
7196
7741
|
};
|
|
7197
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
|
+
*/
|
|
7198
7757
|
function shouldContinuouslyRender(snapshot) {
|
|
7199
7758
|
if (snapshot.timeline?.mode === "loop")
|
|
7200
7759
|
return true;
|
|
@@ -7218,9 +7777,7 @@ async function createProductionSceneRenderer(canvas, snapshot, rendererOptions,
|
|
|
7218
7777
|
const flattened = groups.flatten(snapshot.nodes);
|
|
7219
7778
|
const eligibility = analyzeProductionBridgeEligibility(flattened);
|
|
7220
7779
|
if (!eligibility.eligible) {
|
|
7221
|
-
|
|
7222
|
-
`Production bridge skipped: ${eligibility.reasons.join("; ")}.`
|
|
7223
|
-
], 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.`);
|
|
7224
7781
|
}
|
|
7225
7782
|
try {
|
|
7226
7783
|
return await createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOptions, runtimeNodes);
|
|
@@ -7233,7 +7790,57 @@ async function createProductionSceneRenderer(canvas, snapshot, rendererOptions,
|
|
|
7233
7790
|
}
|
|
7234
7791
|
let cachedProductionRuntimeFallbackLights;
|
|
7235
7792
|
function createProductionRuntimeEnvironment(snapshot) {
|
|
7236
|
-
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() ?? "" : "");
|
|
7237
7844
|
const category = resolveRendererSceneCategory(snapshot, names);
|
|
7238
7845
|
const preset = category === "city-day" ? "daylight"
|
|
7239
7846
|
: category === "city-night" ? "evening"
|
|
@@ -7262,7 +7869,7 @@ function createProductionRuntimeEnvironmentFog(snapshot) {
|
|
|
7262
7869
|
return false;
|
|
7263
7870
|
const density = clampNumber(fog.density ?? 0.12, 0, 1);
|
|
7264
7871
|
const intensity = clampNumber(fog.intensity ?? 0.5, 0, 1);
|
|
7265
|
-
const color =
|
|
7872
|
+
const color = colorToLinearRgb(fog.color ?? "#9fb7d9");
|
|
7266
7873
|
return {
|
|
7267
7874
|
// Exponential-squared reads as depth haze rather than a hard linear band, which
|
|
7268
7875
|
// matches what the public helper documents.
|
|
@@ -7276,16 +7883,8 @@ function createProductionRuntimeEnvironmentFog(snapshot) {
|
|
|
7276
7883
|
}
|
|
7277
7884
|
function createProductionRuntimePostprocess(snapshot) {
|
|
7278
7885
|
const nodes = groups.flatten(snapshot.nodes);
|
|
7279
|
-
const names = nodes.map((node) => "name" in node ? node.name?.toLowerCase() ?? "" : "");
|
|
7280
|
-
const category = resolveRendererSceneCategory(snapshot, names);
|
|
7281
7886
|
const authoredBloom = nodes.find((node) => node.kind === "effect" && node.effect === "bloom");
|
|
7282
|
-
const
|
|
7283
|
-
if (!("material" in node) || !node.material)
|
|
7284
|
-
return false;
|
|
7285
|
-
return Boolean(node.material.emissive) && (node.material.emissiveIntensity ?? 1) > 0;
|
|
7286
|
-
}).length;
|
|
7287
|
-
const darkScene = ["neon", "city-night", "space", "game"].includes(category);
|
|
7288
|
-
const bloomRequested = Boolean(authoredBloom) || (darkScene && emissiveSubjects > 0);
|
|
7887
|
+
const bloomRequested = Boolean(authoredBloom);
|
|
7289
7888
|
// Root previously advertised `ssao` in `requestedPasses` whenever a scene added
|
|
7290
7889
|
// effects.ambientOcclusion() or effects.contactOcclusion(), but never submitted
|
|
7291
7890
|
// an `ssao` option to the renderer. The advertised pass could therefore never
|
|
@@ -7294,20 +7893,15 @@ function createProductionRuntimePostprocess(snapshot) {
|
|
|
7294
7893
|
const authoredAmbientOcclusion = nodes.find((node) => node.kind === "effect" && node.effect === "ambient-occlusion");
|
|
7295
7894
|
const authoredContactOcclusion = nodes.find((node) => node.kind === "effect" && node.effect === "contact-occlusion");
|
|
7296
7895
|
const authoredOcclusion = authoredAmbientOcclusion ?? authoredContactOcclusion;
|
|
7297
|
-
const exposure = category === "city-day" ? 1.02
|
|
7298
|
-
: category === "material" || category === "product" ? 1.08
|
|
7299
|
-
: category === "space" ? 1.18
|
|
7300
|
-
: 1.12;
|
|
7301
|
-
const saturation = category === "material" || category === "product" ? 1.01
|
|
7302
|
-
: category === "city-day" ? 1.04
|
|
7303
|
-
: 1.1;
|
|
7304
7896
|
return {
|
|
7305
|
-
|
|
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",
|
|
7306
7900
|
...(bloomRequested ? {
|
|
7307
7901
|
bloom: {
|
|
7308
|
-
threshold: clampNumber(authoredBloom?.threshold ??
|
|
7309
|
-
intensity: clampNumber(authoredBloom?.intensity ??
|
|
7310
|
-
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)))
|
|
7311
7905
|
}
|
|
7312
7906
|
} : {}),
|
|
7313
7907
|
...(authoredOcclusion ? {
|
|
@@ -7323,22 +7917,11 @@ function createProductionRuntimePostprocess(snapshot) {
|
|
|
7323
7917
|
}
|
|
7324
7918
|
} : {}),
|
|
7325
7919
|
toneMapping: {
|
|
7326
|
-
exposure,
|
|
7327
|
-
whitePoint:
|
|
7328
|
-
operator: "
|
|
7920
|
+
exposure: 1,
|
|
7921
|
+
whitePoint: 1,
|
|
7922
|
+
operator: "aces",
|
|
7329
7923
|
inputColorSpace: "linear",
|
|
7330
7924
|
outputColorSpace: "srgb"
|
|
7331
|
-
},
|
|
7332
|
-
colorGrade: {
|
|
7333
|
-
contrast: category === "material" || category === "product" ? 1.06 : 1.1,
|
|
7334
|
-
saturation,
|
|
7335
|
-
vibrance: darkScene ? 0.14 : 0.06,
|
|
7336
|
-
vignette: category === "game" || category === "neon" ? 0.1 : 0.04,
|
|
7337
|
-
sharpening: category === "material" || category === "product" ? 0.28 : 0.2
|
|
7338
|
-
},
|
|
7339
|
-
fxaa: {
|
|
7340
|
-
edgeThreshold: category === "game" ? 0.07 : 0.09,
|
|
7341
|
-
subpixelBlend: category === "material" || category === "product" ? 0.48 : 0.58
|
|
7342
7925
|
}
|
|
7343
7926
|
};
|
|
7344
7927
|
}
|
|
@@ -7403,7 +7986,8 @@ function createProductionRuntimePostprocessObservation(diagnostics) {
|
|
|
7403
7986
|
contactOcclusionReceiver: actualPasses.includes("contact-shadow"),
|
|
7404
7987
|
pixelBacked,
|
|
7405
7988
|
actualPasses,
|
|
7406
|
-
fallbackPasses: pixelBacked ? [] : ["direct-render"]
|
|
7989
|
+
fallbackPasses: pixelBacked ? [] : ["direct-render"],
|
|
7990
|
+
targetFormat: diagnostics.postprocessTargetFormat
|
|
7407
7991
|
};
|
|
7408
7992
|
}
|
|
7409
7993
|
function createProductionRuntimeCollectedLights(snapshot) {
|
|
@@ -7424,7 +8008,7 @@ function createProductionRuntimeCollectedLights(snapshot) {
|
|
|
7424
8008
|
}
|
|
7425
8009
|
function createProductionRuntimeLightDescriptors(node, authoredIndex) {
|
|
7426
8010
|
const name = node.name?.trim() || `aura-authored-${node.light}-${authoredIndex + 1}`;
|
|
7427
|
-
const color =
|
|
8011
|
+
const color = colorToLinearRgb(node.color ?? "#ffffff");
|
|
7428
8012
|
const intensity = nonNegativeFinite(node.intensity);
|
|
7429
8013
|
const position = productionRuntimeLightPosition(node);
|
|
7430
8014
|
const direction = productionRuntimeLightDirection(node, position);
|
|
@@ -7701,17 +8285,40 @@ function multiplyRgb(color, tint) {
|
|
|
7701
8285
|
async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOptions, runtimeNodes) {
|
|
7702
8286
|
const flattened = groups.flatten(snapshot.nodes);
|
|
7703
8287
|
const modelNodes = flattened.filter((node) => isRenderableModelNode(node) && createAssetProvenance(node.asset).source === "typed-aura-assets-manifest");
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
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
|
+
: [];
|
|
7715
8322
|
const primitiveEntries = createProductionRuntimePrimitiveEntries(flattened);
|
|
7716
8323
|
const productionRenderer = await ProductionRuntimeRenderer.create({
|
|
7717
8324
|
canvas,
|
|
@@ -7720,7 +8327,9 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
|
|
|
7720
8327
|
backend: "webgl2",
|
|
7721
8328
|
antialias: true,
|
|
7722
8329
|
preserveDrawingBuffer: true,
|
|
7723
|
-
|
|
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)
|
|
7724
8333
|
});
|
|
7725
8334
|
let latestDeviceDiagnostics = productionRenderer.getDiagnostics();
|
|
7726
8335
|
let latestFeatures = productionRenderer.getFeatures();
|
|
@@ -7761,7 +8370,15 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
|
|
|
7761
8370
|
.filter((feature) => feature.state !== "supported")
|
|
7762
8371
|
.map((feature) => `Production runtime feature ${feature.id} is ${feature.state}: ${feature.detail}`),
|
|
7763
8372
|
...runtimeWarnings
|
|
7764
|
-
]
|
|
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
|
+
}))
|
|
7765
8382
|
}, rendererOptions);
|
|
7766
8383
|
return {
|
|
7767
8384
|
get backend() {
|
|
@@ -7781,19 +8398,35 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
|
|
|
7781
8398
|
viewProjection(time) {
|
|
7782
8399
|
return createViewProjection(snapshot, canvas.width / Math.max(1, canvas.height), time, runtimeNodes);
|
|
7783
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
|
+
},
|
|
7784
8413
|
dispose() {
|
|
7785
8414
|
productionRenderer.dispose();
|
|
7786
8415
|
for (const { actor } of actorEntries)
|
|
7787
8416
|
actor.pipeline.dispose();
|
|
7788
|
-
for (const {
|
|
7789
|
-
geometry
|
|
7790
|
-
|
|
8417
|
+
for (const { resources } of primitiveEntries) {
|
|
8418
|
+
for (const { geometry, material } of resources) {
|
|
8419
|
+
geometry.dispose();
|
|
8420
|
+
material.dispose();
|
|
8421
|
+
}
|
|
7791
8422
|
}
|
|
7792
8423
|
}
|
|
7793
8424
|
};
|
|
7794
8425
|
}
|
|
7795
8426
|
function createProductionRuntimeRendererInput(snapshot, canvas, actorEntries, primitiveEntries, time, runtimeNodes, runtimeWarnings, environmentLighting, collectedLights) {
|
|
7796
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);
|
|
7797
8430
|
for (const entry of actorEntries) {
|
|
7798
8431
|
const currentState = resolveProductionActorRuntimeState(entry, runtimeNodes);
|
|
7799
8432
|
const currentNode = currentState.node;
|
|
@@ -7810,16 +8443,16 @@ function createProductionRuntimeRendererInput(snapshot, canvas, actorEntries, pr
|
|
|
7810
8443
|
const currentState = resolveProductionPrimitiveRuntimeState(entry, runtimeNodes);
|
|
7811
8444
|
if (!currentState.visible)
|
|
7812
8445
|
continue;
|
|
8446
|
+
const resource = selectProductionPrimitiveResource(entry, currentState.node, cameraPosition);
|
|
7813
8447
|
items.push({
|
|
7814
|
-
geometry:
|
|
7815
|
-
material:
|
|
7816
|
-
modelMatrix: createModelMatrix(currentState.node,
|
|
8448
|
+
geometry: resource.geometry,
|
|
8449
|
+
material: resource.material,
|
|
8450
|
+
modelMatrix: createModelMatrix(currentState.node, resource.bounds, false, time),
|
|
7817
8451
|
label: currentState.node.name ?? `aura-primitive-${currentState.node.primitive}`,
|
|
7818
|
-
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) } : {})
|
|
7819
8454
|
});
|
|
7820
8455
|
}
|
|
7821
|
-
const viewProjectionMatrix = createViewProjection(snapshot, canvas.width / Math.max(1, canvas.height), time, runtimeNodes);
|
|
7822
|
-
const cameraPosition = resolveCameraEye(snapshot, snapshot.camera, time, runtimeNodes);
|
|
7823
8456
|
const source = {
|
|
7824
8457
|
collectRenderItems: () => items,
|
|
7825
8458
|
cameraPolicy: "require",
|
|
@@ -7836,18 +8469,57 @@ function createProductionRuntimeRendererInput(snapshot, canvas, actorEntries, pr
|
|
|
7836
8469
|
return {
|
|
7837
8470
|
source,
|
|
7838
8471
|
camera: cameraLike,
|
|
7839
|
-
metadata: createProductionRuntimeMetadata(actorEntries)
|
|
8472
|
+
metadata: createProductionRuntimeMetadata(actorEntries, primitiveEntries)
|
|
7840
8473
|
};
|
|
7841
8474
|
}
|
|
7842
8475
|
function createProductionRuntimePrimitiveEntries(nodes) {
|
|
7843
8476
|
return nodes
|
|
7844
8477
|
.filter((node) => node.kind === "primitive")
|
|
7845
|
-
.map((node) => ({
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
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;
|
|
7851
8523
|
}
|
|
7852
8524
|
function resolveProductionPrimitiveRuntimeState(entry, runtimeNodes) {
|
|
7853
8525
|
const runtimeId = entry.node.runtime?.id;
|
|
@@ -7867,11 +8539,91 @@ function resolveProductionPrimitiveRuntimeState(entry, runtimeNodes) {
|
|
|
7867
8539
|
visible: runtimeSnapshot.visible
|
|
7868
8540
|
};
|
|
7869
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
|
+
*/
|
|
7870
8572
|
function createProductionPrimitiveMaterial(node) {
|
|
7871
8573
|
const materialSpec = node.material;
|
|
7872
|
-
const baseColor =
|
|
8574
|
+
const baseColor = colorToLinearRgba(materialSpec?.color ?? materialSpec?.emissive ?? "#d7dee8");
|
|
7873
8575
|
const opacity = clamp01(materialSpec?.opacity ?? baseColor[3] ?? 1);
|
|
7874
|
-
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
|
+
}
|
|
7875
8627
|
return new PBRMaterial({
|
|
7876
8628
|
name: `a3d-production-primitive-${node.primitive}${node.name ? `-${node.name}` : ""}`,
|
|
7877
8629
|
baseColor: [baseColor[0], baseColor[1], baseColor[2], opacity],
|
|
@@ -7879,9 +8631,22 @@ function createProductionPrimitiveMaterial(node) {
|
|
|
7879
8631
|
roughness: clamp01(materialSpec?.roughness ?? 0.58),
|
|
7880
8632
|
emissiveColor,
|
|
7881
8633
|
emissiveStrength: Math.max(0, materialSpec?.emissiveIntensity ?? (materialSpec?.emissive ? 1.35 : 0)),
|
|
7882
|
-
clearcoatFactor:
|
|
8634
|
+
clearcoatFactor: clearcoat,
|
|
7883
8635
|
clearcoatRoughnessFactor: clamp01(materialSpec?.clearcoatRoughness ?? 0.34),
|
|
7884
|
-
|
|
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,
|
|
7885
8650
|
renderState: {
|
|
7886
8651
|
blend: opacity < 0.999,
|
|
7887
8652
|
depthWrite: opacity >= 0.999,
|
|
@@ -7890,9 +8655,18 @@ function createProductionPrimitiveMaterial(node) {
|
|
|
7890
8655
|
});
|
|
7891
8656
|
}
|
|
7892
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.");
|
|
7893
8662
|
return createProductionGeometryFromPrimitiveMesh(createProductionPrimitiveMesh(node.primitive));
|
|
7894
8663
|
}
|
|
7895
|
-
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.");
|
|
7896
8670
|
return createProductionPrimitiveMesh(primitive).bounds;
|
|
7897
8671
|
}
|
|
7898
8672
|
function createProductionPrimitiveMesh(primitive) {
|
|
@@ -7908,6 +8682,42 @@ function createProductionPrimitiveMesh(primitive) {
|
|
|
7908
8682
|
return createCylinderGeometry();
|
|
7909
8683
|
return createPlaneGeometry();
|
|
7910
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
|
+
}
|
|
7911
8721
|
function createProductionGeometryFromPrimitiveMesh(mesh) {
|
|
7912
8722
|
const vertexCount = mesh.positions.length / 3;
|
|
7913
8723
|
const vertices = new VertexBuffer(VertexFormat.P3N3, vertexCount);
|
|
@@ -8005,16 +8815,18 @@ function createRuntimeEvidenceFromTypedGLBActor(node, actor, renderItems) {
|
|
|
8005
8815
|
morphRenderItemCount: renderItems.filter((item) => item.morphTargets && item.morphTargets.length > 0).length
|
|
8006
8816
|
});
|
|
8007
8817
|
}
|
|
8008
|
-
function createProductionRuntimeMetadata(actorEntries) {
|
|
8818
|
+
function createProductionRuntimeMetadata(actorEntries, primitiveEntries) {
|
|
8009
8819
|
const metadata = actorEntries.map((entry) => entry.actor.pipeline.metadata);
|
|
8010
8820
|
const primary = metadata[0];
|
|
8821
|
+
const primitiveCount = primitiveEntries.length;
|
|
8822
|
+
const typedPrimitiveCount = sumProductionMetadata(metadata, "primitiveCount");
|
|
8011
8823
|
return {
|
|
8012
|
-
assetId: metadata.length === 1 ? primary?.assetId ?? "typed-glb" : `typed-glb-batch-${metadata.length}`,
|
|
8013
|
-
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",
|
|
8014
8826
|
...(metadata.length === 1 && primary?.assetName ? { assetName: primary.assetName } : metadata.length > 1 ? { assetName: "Aura3D typed GLB scene batch" } : {}),
|
|
8015
|
-
meshCount: sumProductionMetadata(metadata, "meshCount"),
|
|
8016
|
-
primitiveCount:
|
|
8017
|
-
materialCount: sumProductionMetadata(metadata, "materialCount"),
|
|
8827
|
+
meshCount: sumProductionMetadata(metadata, "meshCount") + primitiveCount,
|
|
8828
|
+
primitiveCount: typedPrimitiveCount + primitiveCount,
|
|
8829
|
+
materialCount: sumProductionMetadata(metadata, "materialCount") + primitiveCount,
|
|
8018
8830
|
textureCount: sumProductionMetadata(metadata, "textureCount"),
|
|
8019
8831
|
imageCount: sumProductionMetadata(metadata, "imageCount"),
|
|
8020
8832
|
animationCount: sumProductionMetadata(metadata, "animationCount"),
|
|
@@ -8136,6 +8948,47 @@ function colorToClearColor(color) {
|
|
|
8136
8948
|
}
|
|
8137
8949
|
return [0.02, 0.025, 0.035, 1];
|
|
8138
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
|
+
}
|
|
8139
8992
|
function colorWithAlpha(color, alpha) {
|
|
8140
8993
|
const [r, g, b] = colorToClearColor(color);
|
|
8141
8994
|
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${Math.min(1, Math.max(0, alpha))})`;
|
|
@@ -8386,13 +9239,57 @@ async function createWebGLSceneRenderer(canvas, snapshot, rendererOptions, runti
|
|
|
8386
9239
|
fallbackPasses: hasRuntimePostProcessEffects(requestedEffectNodes) ? ["webgl2-direct-render"] : []
|
|
8387
9240
|
},
|
|
8388
9241
|
warnings: [
|
|
8389
|
-
"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.",
|
|
8390
9243
|
...runtimeWarnings
|
|
8391
9244
|
]
|
|
8392
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);
|
|
8393
9279
|
return {
|
|
8394
9280
|
backend: "webgl2",
|
|
8395
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
|
+
},
|
|
8396
9293
|
render(time) {
|
|
8397
9294
|
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
8398
9295
|
gl.clearColor(background[0], background[1], background[2], background[3]);
|
|
@@ -8417,6 +9314,17 @@ async function createWebGLSceneRenderer(canvas, snapshot, rendererOptions, runti
|
|
|
8417
9314
|
gl.uniform1f(program.uniforms.metallic, primitiveEntry.metallic ?? 0);
|
|
8418
9315
|
gl.uniform1f(program.uniforms.roughness, primitiveEntry.roughness ?? 0.72);
|
|
8419
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);
|
|
8420
9328
|
gl.bindBuffer(gl.ARRAY_BUFFER, primitiveEntry.position);
|
|
8421
9329
|
gl.enableVertexAttribArray(program.attributes.position);
|
|
8422
9330
|
gl.vertexAttribPointer(program.attributes.position, 3, gl.FLOAT, false, 0, 0);
|
|
@@ -8497,6 +9405,11 @@ async function createWebGLSceneRenderer(canvas, snapshot, rendererOptions, runti
|
|
|
8497
9405
|
return createViewProjection(snapshot, canvas.width / Math.max(1, canvas.height), time, runtimeNodes);
|
|
8498
9406
|
},
|
|
8499
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();
|
|
8500
9413
|
for (const modelEntry of models) {
|
|
8501
9414
|
const textures = new Set();
|
|
8502
9415
|
for (const primitiveEntry of modelEntry.primitives) {
|
|
@@ -8629,7 +9542,13 @@ function createWebGLModel(gl, node, modelData) {
|
|
|
8629
9542
|
bounds: modelData.bounds,
|
|
8630
9543
|
color: colorToRgb(node.material?.color ?? "#8fb4ff"),
|
|
8631
9544
|
normalizeToUnit: shouldNormalizeModelNode(node),
|
|
8632
|
-
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) => {
|
|
8633
9552
|
const position = createBuffer(gl, gl.ARRAY_BUFFER, primitiveEntry.positions);
|
|
8634
9553
|
const normal = createBuffer(gl, gl.ARRAY_BUFFER, primitiveEntry.normals);
|
|
8635
9554
|
const uv = primitiveEntry.uvs ? createBuffer(gl, gl.ARRAY_BUFFER, primitiveEntry.uvs) : undefined;
|
|
@@ -8786,7 +9705,24 @@ function createWebGLPrimitiveModel(gl, node) {
|
|
|
8786
9705
|
indexType: gl.UNSIGNED_SHORT,
|
|
8787
9706
|
metallic: clamp01(node.material?.metallic ?? node.material?.metalness ?? 0),
|
|
8788
9707
|
roughness: clamp01(node.material?.roughness ?? 0.72),
|
|
8789
|
-
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)
|
|
8790
9726
|
}]
|
|
8791
9727
|
};
|
|
8792
9728
|
}
|
|
@@ -8992,7 +9928,73 @@ uniform int u_useEmissiveTexture;
|
|
|
8992
9928
|
uniform float u_metallic;
|
|
8993
9929
|
uniform float u_roughness;
|
|
8994
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;
|
|
8995
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
|
+
|
|
8996
9998
|
void main() {
|
|
8997
9999
|
vec3 normal = normalize(v_normal);
|
|
8998
10000
|
vec3 lightDirection = normalize(u_lightDirection);
|
|
@@ -9018,6 +10020,85 @@ void main() {
|
|
|
9018
10020
|
vec3 reflection = environment * specularColor * (fresnel * 0.62 + (1.0 - roughness) * 0.22);
|
|
9019
10021
|
vec3 emissive = u_emissive * emissiveTexel;
|
|
9020
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
|
+
|
|
9021
10102
|
color = color / (color + vec3(1.0));
|
|
9022
10103
|
outColor = vec4(pow(color, vec3(1.0 / 2.2)), texel.a);
|
|
9023
10104
|
}`);
|
|
@@ -9047,7 +10128,19 @@ void main() {
|
|
|
9047
10128
|
useEmissiveTexture: requiredUniform(gl, program, "u_useEmissiveTexture"),
|
|
9048
10129
|
metallic: requiredUniform(gl, program, "u_metallic"),
|
|
9049
10130
|
roughness: requiredUniform(gl, program, "u_roughness"),
|
|
9050
|
-
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")
|
|
9051
10144
|
};
|
|
9052
10145
|
return {
|
|
9053
10146
|
program,
|
|
@@ -9242,6 +10335,7 @@ function createGltfRuntimeNodes(json) {
|
|
|
9242
10335
|
const rotate = node.rotation ?? [0, 0, 0, 1];
|
|
9243
10336
|
const scale = node.scale ?? [1, 1, 1];
|
|
9244
10337
|
return {
|
|
10338
|
+
name: node.name ?? "",
|
|
9245
10339
|
children: node.children ?? [],
|
|
9246
10340
|
baseTranslation: [translate[0] ?? 0, translate[1] ?? 0, translate[2] ?? 0],
|
|
9247
10341
|
baseRotation: normalizeQuaternion([rotate[0] ?? 0, rotate[1] ?? 0, rotate[2] ?? 0, rotate[3] ?? 1]),
|
|
@@ -9455,7 +10549,10 @@ function createSphereGeometry() {
|
|
|
9455
10549
|
for (let column = 0; column < columns; column += 1) {
|
|
9456
10550
|
const a = row * (columns + 1) + column;
|
|
9457
10551
|
const b = a + columns + 1;
|
|
9458
|
-
|
|
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);
|
|
9459
10556
|
}
|
|
9460
10557
|
}
|
|
9461
10558
|
return {
|
|
@@ -9799,9 +10896,13 @@ function createViewProjection(snapshot, aspect, time, runtimeNodes) {
|
|
|
9799
10896
|
const cameraSpec = snapshot.camera;
|
|
9800
10897
|
const target = resolveCameraTarget(snapshot, cameraSpec, runtimeNodes);
|
|
9801
10898
|
const eye = resolveCameraEye(snapshot, cameraSpec, time, runtimeNodes);
|
|
9802
|
-
const view =
|
|
9803
|
-
const
|
|
9804
|
-
|
|
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);
|
|
10902
|
+
const projection = isOrthographicCameraMode(cameraSpec.mode)
|
|
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));
|
|
9805
10906
|
}
|
|
9806
10907
|
function createModelMatrix(node, bounds, normalizeToUnit, time = 0) {
|
|
9807
10908
|
const extent = [
|
|
@@ -9877,26 +10978,8 @@ function primitiveSize(node) {
|
|
|
9877
10978
|
return [node.size, node.size, node.size];
|
|
9878
10979
|
return node.size ?? [1, 1, 1];
|
|
9879
10980
|
}
|
|
9880
|
-
function
|
|
9881
|
-
|
|
9882
|
-
const nf = 1 / (near - far);
|
|
9883
|
-
return new Float32Array([
|
|
9884
|
-
f / aspect, 0, 0, 0,
|
|
9885
|
-
0, f, 0, 0,
|
|
9886
|
-
0, 0, (far + near) * nf, -1,
|
|
9887
|
-
0, 0, 2 * far * near * nf, 0
|
|
9888
|
-
]);
|
|
9889
|
-
}
|
|
9890
|
-
function lookAt(eye, target, up) {
|
|
9891
|
-
const z = normalize3([eye[0] - target[0], eye[1] - target[1], eye[2] - target[2]]);
|
|
9892
|
-
const x = normalize3(cross3(up, z));
|
|
9893
|
-
const y = cross3(z, x);
|
|
9894
|
-
return new Float32Array([
|
|
9895
|
-
x[0], y[0], z[0], 0,
|
|
9896
|
-
x[1], y[1], z[1], 0,
|
|
9897
|
-
x[2], y[2], z[2], 0,
|
|
9898
|
-
-dot3(x, eye), -dot3(y, eye), -dot3(z, eye), 1
|
|
9899
|
-
]);
|
|
10981
|
+
export function isOrthographicCameraMode(mode) {
|
|
10982
|
+
return mode === "orthographic" || mode === "isometric";
|
|
9900
10983
|
}
|
|
9901
10984
|
function multiply4(a, b) {
|
|
9902
10985
|
const output = new Float32Array(16);
|
|
@@ -9976,16 +11059,6 @@ function normalize3(value) {
|
|
|
9976
11059
|
const length = Math.hypot(value[0], value[1], value[2]) || 1;
|
|
9977
11060
|
return [value[0] / length, value[1] / length, value[2] / length];
|
|
9978
11061
|
}
|
|
9979
|
-
function cross3(a, b) {
|
|
9980
|
-
return [
|
|
9981
|
-
a[1] * b[2] - a[2] * b[1],
|
|
9982
|
-
a[2] * b[0] - a[0] * b[2],
|
|
9983
|
-
a[0] * b[1] - a[1] * b[0]
|
|
9984
|
-
];
|
|
9985
|
-
}
|
|
9986
|
-
function dot3(a, b) {
|
|
9987
|
-
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
9988
|
-
}
|
|
9989
11062
|
function mix3(a, b, t) {
|
|
9990
11063
|
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
9991
11064
|
}
|
|
@@ -10214,7 +11287,23 @@ function collectGeneratedCodeWarnings(snapshot) {
|
|
|
10214
11287
|
}
|
|
10215
11288
|
return warnings;
|
|
10216
11289
|
}
|
|
10217
|
-
|
|
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) {
|
|
10218
11307
|
if (!canvas)
|
|
10219
11308
|
return 0;
|
|
10220
11309
|
const context = canvas.getContext("2d");
|
|
@@ -10569,6 +11658,8 @@ function markRouteState(status, snapshot, diagnostics) {
|
|
|
10569
11658
|
if (typeof document !== "undefined") {
|
|
10570
11659
|
document.body.dataset.aura3dReady = status === "ready" ? "true" : "error";
|
|
10571
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;
|
|
10572
11663
|
}
|
|
10573
11664
|
if (typeof window !== "undefined") {
|
|
10574
11665
|
window.__AURA3D_ROUTE_READY__ = {
|