@aura3d/engine 2.0.4 → 3.0.1

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.
Files changed (824) hide show
  1. package/README.md +45 -11
  2. package/dist/animation/AnimationAction.d.ts.map +1 -1
  3. package/dist/animation/AnimationAction.js +20 -2
  4. package/dist/animation/AnimationAction.js.map +1 -1
  5. package/dist/animation/AnimationLayer.d.ts +1 -0
  6. package/dist/animation/AnimationLayer.d.ts.map +1 -1
  7. package/dist/animation/AnimationLayer.js +24 -3
  8. package/dist/animation/AnimationLayer.js.map +1 -1
  9. package/dist/animation/AnimationMixer.d.ts +7 -1
  10. package/dist/animation/AnimationMixer.d.ts.map +1 -1
  11. package/dist/animation/AnimationMixer.js +105 -34
  12. package/dist/animation/AnimationMixer.js.map +1 -1
  13. package/dist/animation/AnimationTrack.d.ts.map +1 -1
  14. package/dist/animation/AnimationTrack.js +13 -0
  15. package/dist/animation/AnimationTrack.js.map +1 -1
  16. package/dist/animation/FootIk.d.ts +19 -0
  17. package/dist/animation/FootIk.d.ts.map +1 -1
  18. package/dist/animation/FootIk.js +113 -7
  19. package/dist/animation/FootIk.js.map +1 -1
  20. package/dist/animation/HumanoidRetargeting.d.ts +35 -2
  21. package/dist/animation/HumanoidRetargeting.d.ts.map +1 -1
  22. package/dist/animation/HumanoidRetargeting.js +59 -9
  23. package/dist/animation/HumanoidRetargeting.js.map +1 -1
  24. package/dist/animation/LocomotionEvidence.d.ts +34 -0
  25. package/dist/animation/LocomotionEvidence.d.ts.map +1 -0
  26. package/dist/animation/LocomotionEvidence.js +48 -0
  27. package/dist/animation/LocomotionEvidence.js.map +1 -0
  28. package/dist/animation/LocomotionKit.d.ts +6 -0
  29. package/dist/animation/LocomotionKit.d.ts.map +1 -1
  30. package/dist/animation/LocomotionKit.js +6 -0
  31. package/dist/animation/LocomotionKit.js.map +1 -1
  32. package/dist/animation/RootMotion.d.ts +36 -0
  33. package/dist/animation/RootMotion.d.ts.map +1 -1
  34. package/dist/animation/RootMotion.js +58 -0
  35. package/dist/animation/RootMotion.js.map +1 -1
  36. package/dist/animation/SpringBones.d.ts +28 -0
  37. package/dist/animation/SpringBones.d.ts.map +1 -1
  38. package/dist/animation/SpringBones.js +24 -0
  39. package/dist/animation/SpringBones.js.map +1 -1
  40. package/dist/animation/index.d.ts +1 -0
  41. package/dist/animation/index.d.ts.map +1 -1
  42. package/dist/animation/index.js +1 -0
  43. package/dist/animation/index.js.map +1 -1
  44. package/dist/assets/GLTFAnimationRuntime.d.ts +175 -2
  45. package/dist/assets/GLTFAnimationRuntime.d.ts.map +1 -1
  46. package/dist/assets/GLTFAnimationRuntime.js +615 -5
  47. package/dist/assets/GLTFAnimationRuntime.js.map +1 -1
  48. package/dist/assets/GLTFCompressionDecoders.d.ts +1 -0
  49. package/dist/assets/GLTFCompressionDecoders.d.ts.map +1 -1
  50. package/dist/assets/GLTFCompressionDecoders.js +20 -5
  51. package/dist/assets/GLTFCompressionDecoders.js.map +1 -1
  52. package/dist/assets/GLTFExtensionSupport.js +2 -2
  53. package/dist/assets/GLTFExtensionSupport.js.map +1 -1
  54. package/dist/assets/GLTFLoader.d.ts +360 -1
  55. package/dist/assets/GLTFLoader.d.ts.map +1 -1
  56. package/dist/assets/GLTFLoader.js +218 -8
  57. package/dist/assets/GLTFLoader.js.map +1 -1
  58. package/dist/assets/KTX2BasisTextureTranscoder.d.ts +39 -0
  59. package/dist/assets/KTX2BasisTextureTranscoder.d.ts.map +1 -1
  60. package/dist/assets/KTX2BasisTextureTranscoder.js +78 -0
  61. package/dist/assets/KTX2BasisTextureTranscoder.js.map +1 -1
  62. package/dist/assets/TexturePipeline.d.ts +20 -0
  63. package/dist/assets/TexturePipeline.d.ts.map +1 -1
  64. package/dist/assets/TexturePipeline.js +20 -0
  65. package/dist/assets/TexturePipeline.js.map +1 -1
  66. package/dist/assets/TextureStreaming.d.ts +30 -0
  67. package/dist/assets/TextureStreaming.d.ts.map +1 -1
  68. package/dist/assets/TextureStreaming.js +53 -0
  69. package/dist/assets/TextureStreaming.js.map +1 -1
  70. package/dist/assets/browser-index.d.ts +7 -3
  71. package/dist/assets/browser-index.d.ts.map +1 -1
  72. package/dist/assets/browser-index.js +3 -1
  73. package/dist/assets/browser-index.js.map +1 -1
  74. package/dist/assets/gltf-runtime.d.ts +1 -1
  75. package/dist/assets/gltf-runtime.d.ts.map +1 -1
  76. package/dist/assets/gltf-runtime.js.map +1 -1
  77. package/dist/assets/index.d.ts +7 -7
  78. package/dist/assets/index.d.ts.map +1 -1
  79. package/dist/assets/index.js +3 -3
  80. package/dist/assets/index.js.map +1 -1
  81. package/dist/assets/loaders/HDRLoader.d.ts +40 -0
  82. package/dist/assets/loaders/HDRLoader.d.ts.map +1 -1
  83. package/dist/assets/loaders/HDRLoader.js +158 -0
  84. package/dist/assets/loaders/HDRLoader.js.map +1 -1
  85. package/dist/audio/AudioSource.d.ts +2 -0
  86. package/dist/audio/AudioSource.d.ts.map +1 -1
  87. package/dist/audio/AudioSource.js +7 -0
  88. package/dist/audio/AudioSource.js.map +1 -1
  89. package/dist/audio/Footsteps.d.ts +47 -0
  90. package/dist/audio/Footsteps.d.ts.map +1 -0
  91. package/dist/audio/Footsteps.js +73 -0
  92. package/dist/audio/Footsteps.js.map +1 -0
  93. package/dist/audio/GameMixer.d.ts +69 -0
  94. package/dist/audio/GameMixer.d.ts.map +1 -0
  95. package/dist/audio/GameMixer.js +141 -0
  96. package/dist/audio/GameMixer.js.map +1 -0
  97. package/dist/audio/PositionalEmitter.d.ts +95 -0
  98. package/dist/audio/PositionalEmitter.d.ts.map +1 -0
  99. package/dist/audio/PositionalEmitter.js +199 -0
  100. package/dist/audio/PositionalEmitter.js.map +1 -0
  101. package/dist/audio/index.d.ts +6 -0
  102. package/dist/audio/index.d.ts.map +1 -1
  103. package/dist/audio/index.js +3 -0
  104. package/dist/audio/index.js.map +1 -1
  105. package/dist/aura3d-cli/pull-bridge/profiles.d.ts +12 -2
  106. package/dist/aura3d-cli/pull-bridge/profiles.d.ts.map +1 -1
  107. package/dist/aura3d-cli/pull-bridge/profiles.js +35 -4
  108. package/dist/aura3d-cli/pull-bridge/profiles.js.map +1 -1
  109. package/dist/aura3d-cli/pull-bridge/provenance.d.ts +15 -1
  110. package/dist/aura3d-cli/pull-bridge/provenance.d.ts.map +1 -1
  111. package/dist/aura3d-cli/pull-bridge/provenance.js +60 -1
  112. package/dist/aura3d-cli/pull-bridge/provenance.js.map +1 -1
  113. package/dist/aura3d-cli/pull-bridge/scoring.d.ts +10 -1
  114. package/dist/aura3d-cli/pull-bridge/scoring.d.ts.map +1 -1
  115. package/dist/aura3d-cli/pull-bridge/scoring.js +9 -1
  116. package/dist/aura3d-cli/pull-bridge/scoring.js.map +1 -1
  117. package/dist/aura3d-cli/pull-bridge/search.js +2 -2
  118. package/dist/aura3d-cli/pull-bridge/search.js.map +1 -1
  119. package/dist/aura3d-cli/pull-bridge.d.ts +15 -3
  120. package/dist/aura3d-cli/pull-bridge.d.ts.map +1 -1
  121. package/dist/aura3d-cli/pull-bridge.js +68 -5
  122. package/dist/aura3d-cli/pull-bridge.js.map +1 -1
  123. package/dist/controls/ArcballControls.d.ts +92 -0
  124. package/dist/controls/ArcballControls.d.ts.map +1 -0
  125. package/dist/controls/ArcballControls.js +286 -0
  126. package/dist/controls/ArcballControls.js.map +1 -0
  127. package/dist/controls/DragControls.d.ts +13 -0
  128. package/dist/controls/DragControls.d.ts.map +1 -1
  129. package/dist/controls/DragControls.js +24 -1
  130. package/dist/controls/DragControls.js.map +1 -1
  131. package/dist/controls/FlyControls.d.ts +9 -0
  132. package/dist/controls/FlyControls.d.ts.map +1 -1
  133. package/dist/controls/FlyControls.js +20 -2
  134. package/dist/controls/FlyControls.js.map +1 -1
  135. package/dist/controls/FocusFrame.d.ts +34 -0
  136. package/dist/controls/FocusFrame.d.ts.map +1 -0
  137. package/dist/controls/FocusFrame.js +82 -0
  138. package/dist/controls/FocusFrame.js.map +1 -0
  139. package/dist/controls/HoverOutline.d.ts +46 -0
  140. package/dist/controls/HoverOutline.d.ts.map +1 -0
  141. package/dist/controls/HoverOutline.js +87 -0
  142. package/dist/controls/HoverOutline.js.map +1 -0
  143. package/dist/controls/InteractionControls.d.ts +7 -0
  144. package/dist/controls/InteractionControls.d.ts.map +1 -1
  145. package/dist/controls/InteractionControls.js +12 -1
  146. package/dist/controls/InteractionControls.js.map +1 -1
  147. package/dist/controls/MapControls.d.ts +1 -1
  148. package/dist/controls/MapControls.d.ts.map +1 -1
  149. package/dist/controls/MapControls.js +3 -3
  150. package/dist/controls/MapControls.js.map +1 -1
  151. package/dist/controls/NativeControlTypes.d.ts +17 -0
  152. package/dist/controls/NativeControlTypes.d.ts.map +1 -1
  153. package/dist/controls/NativeControlTypes.js.map +1 -1
  154. package/dist/controls/OrbitControls.d.ts +26 -3
  155. package/dist/controls/OrbitControls.d.ts.map +1 -1
  156. package/dist/controls/OrbitControls.js +48 -11
  157. package/dist/controls/OrbitControls.js.map +1 -1
  158. package/dist/controls/Picking.d.ts +15 -0
  159. package/dist/controls/Picking.d.ts.map +1 -1
  160. package/dist/controls/Picking.js +62 -23
  161. package/dist/controls/Picking.js.map +1 -1
  162. package/dist/controls/PointerLockControls.d.ts.map +1 -1
  163. package/dist/controls/PointerLockControls.js +4 -0
  164. package/dist/controls/PointerLockControls.js.map +1 -1
  165. package/dist/controls/SelectionManager.d.ts +8 -0
  166. package/dist/controls/SelectionManager.d.ts.map +1 -1
  167. package/dist/controls/SelectionManager.js +11 -0
  168. package/dist/controls/SelectionManager.js.map +1 -1
  169. package/dist/controls/TrackballControls.d.ts +7 -0
  170. package/dist/controls/TrackballControls.d.ts.map +1 -1
  171. package/dist/controls/TrackballControls.js +21 -5
  172. package/dist/controls/TrackballControls.js.map +1 -1
  173. package/dist/controls/TransformControls.d.ts +8 -0
  174. package/dist/controls/TransformControls.d.ts.map +1 -1
  175. package/dist/controls/TransformControls.js +15 -2
  176. package/dist/controls/TransformControls.js.map +1 -1
  177. package/dist/controls/index.d.ts +6 -0
  178. package/dist/controls/index.d.ts.map +1 -1
  179. package/dist/controls/index.js +3 -0
  180. package/dist/controls/index.js.map +1 -1
  181. package/dist/create-aura3d/index.js +1 -1
  182. package/dist/editor-runtime/RootEditorSurface.d.ts +65 -0
  183. package/dist/editor-runtime/RootEditorSurface.d.ts.map +1 -0
  184. package/dist/editor-runtime/RootEditorSurface.js +62 -0
  185. package/dist/editor-runtime/RootEditorSurface.js.map +1 -0
  186. package/dist/editor-runtime/index.d.ts +2 -0
  187. package/dist/editor-runtime/index.d.ts.map +1 -1
  188. package/dist/editor-runtime/index.js +1 -0
  189. package/dist/editor-runtime/index.js.map +1 -1
  190. package/dist/engine/agent-api/AnimationController.d.ts +14 -2
  191. package/dist/engine/agent-api/AnimationController.d.ts.map +1 -1
  192. package/dist/engine/agent-api/AnimationController.js +10 -1
  193. package/dist/engine/agent-api/AnimationController.js.map +1 -1
  194. package/dist/engine/agent-api/AnimationDebugOverlay.d.ts +56 -0
  195. package/dist/engine/agent-api/AnimationDebugOverlay.d.ts.map +1 -0
  196. package/dist/engine/agent-api/AnimationDebugOverlay.js +148 -0
  197. package/dist/engine/agent-api/AnimationDebugOverlay.js.map +1 -0
  198. package/dist/engine/agent-api/AnimationMixerBuilders.d.ts +41 -0
  199. package/dist/engine/agent-api/AnimationMixerBuilders.d.ts.map +1 -0
  200. package/dist/engine/agent-api/AnimationMixerBuilders.js +146 -0
  201. package/dist/engine/agent-api/AnimationMixerBuilders.js.map +1 -0
  202. package/dist/engine/agent-api/AssetDecoders.d.ts +17 -0
  203. package/dist/engine/agent-api/AssetDecoders.d.ts.map +1 -0
  204. package/dist/engine/agent-api/AssetDecoders.js +18 -0
  205. package/dist/engine/agent-api/AssetDecoders.js.map +1 -0
  206. package/dist/engine/agent-api/CameraProjection.d.ts +2 -0
  207. package/dist/engine/agent-api/CameraProjection.d.ts.map +1 -0
  208. package/dist/engine/agent-api/CameraProjection.js +2 -0
  209. package/dist/engine/agent-api/CameraProjection.js.map +1 -0
  210. package/dist/engine/agent-api/Decals.d.ts +174 -0
  211. package/dist/engine/agent-api/Decals.d.ts.map +1 -0
  212. package/dist/engine/agent-api/Decals.js +350 -0
  213. package/dist/engine/agent-api/Decals.js.map +1 -0
  214. package/dist/engine/agent-api/DeferredFrameResources.d.ts +2 -0
  215. package/dist/engine/agent-api/DeferredFrameResources.d.ts.map +1 -0
  216. package/dist/engine/agent-api/DeferredFrameResources.js +2 -0
  217. package/dist/engine/agent-api/DeferredFrameResources.js.map +1 -0
  218. package/dist/engine/agent-api/FootPlanting.d.ts +81 -0
  219. package/dist/engine/agent-api/FootPlanting.d.ts.map +1 -0
  220. package/dist/engine/agent-api/FootPlanting.js +61 -0
  221. package/dist/engine/agent-api/FootPlanting.js.map +1 -0
  222. package/dist/engine/agent-api/GameAppRuntime.d.ts +19 -0
  223. package/dist/engine/agent-api/GameAppRuntime.d.ts.map +1 -1
  224. package/dist/engine/agent-api/GameAppRuntime.js +37 -1
  225. package/dist/engine/agent-api/GameAppRuntime.js.map +1 -1
  226. package/dist/engine/agent-api/GameCameraRigs.d.ts +185 -0
  227. package/dist/engine/agent-api/GameCameraRigs.d.ts.map +1 -0
  228. package/dist/engine/agent-api/GameCameraRigs.js +419 -0
  229. package/dist/engine/agent-api/GameCameraRigs.js.map +1 -0
  230. package/dist/engine/agent-api/GameFeel.d.ts +100 -0
  231. package/dist/engine/agent-api/GameFeel.d.ts.map +1 -0
  232. package/dist/engine/agent-api/GameFeel.js +226 -0
  233. package/dist/engine/agent-api/GameFeel.js.map +1 -0
  234. package/dist/engine/agent-api/LabelTelemetry.d.ts +114 -0
  235. package/dist/engine/agent-api/LabelTelemetry.d.ts.map +1 -0
  236. package/dist/engine/agent-api/LabelTelemetry.js +130 -0
  237. package/dist/engine/agent-api/LabelTelemetry.js.map +1 -0
  238. package/dist/engine/agent-api/ModelInstanceMatrices.d.ts +2 -0
  239. package/dist/engine/agent-api/ModelInstanceMatrices.d.ts.map +1 -0
  240. package/dist/engine/agent-api/ModelInstanceMatrices.js +2 -0
  241. package/dist/engine/agent-api/ModelInstanceMatrices.js.map +1 -0
  242. package/dist/engine/agent-api/NavigationCrowds.d.ts +174 -0
  243. package/dist/engine/agent-api/NavigationCrowds.d.ts.map +1 -0
  244. package/dist/engine/agent-api/NavigationCrowds.js +176 -0
  245. package/dist/engine/agent-api/NavigationCrowds.js.map +1 -0
  246. package/dist/engine/agent-api/PhysicsRuntime.d.ts +61 -5
  247. package/dist/engine/agent-api/PhysicsRuntime.d.ts.map +1 -1
  248. package/dist/engine/agent-api/PhysicsRuntime.js +53 -21
  249. package/dist/engine/agent-api/PhysicsRuntime.js.map +1 -1
  250. package/dist/engine/agent-api/PngTexturePixels.d.ts +9 -0
  251. package/dist/engine/agent-api/PngTexturePixels.d.ts.map +1 -0
  252. package/dist/engine/agent-api/PngTexturePixels.js +53 -0
  253. package/dist/engine/agent-api/PngTexturePixels.js.map +1 -0
  254. package/dist/engine/agent-api/RootDiagnosticSnapshot.d.ts +2 -0
  255. package/dist/engine/agent-api/RootDiagnosticSnapshot.d.ts.map +1 -0
  256. package/dist/engine/agent-api/RootDiagnosticSnapshot.js +2 -0
  257. package/dist/engine/agent-api/RootDiagnosticSnapshot.js.map +1 -0
  258. package/dist/engine/agent-api/RootFrameClock.d.ts +2 -0
  259. package/dist/engine/agent-api/RootFrameClock.d.ts.map +1 -0
  260. package/dist/engine/agent-api/RootFrameClock.js +2 -0
  261. package/dist/engine/agent-api/RootFrameClock.js.map +1 -0
  262. package/dist/engine/agent-api/RootGeometry.d.ts +16 -1
  263. package/dist/engine/agent-api/RootGeometry.d.ts.map +1 -1
  264. package/dist/engine/agent-api/RootGeometry.js.map +1 -1
  265. package/dist/engine/agent-api/RootPerformanceQuality.d.ts +2 -0
  266. package/dist/engine/agent-api/RootPerformanceQuality.d.ts.map +1 -0
  267. package/dist/engine/agent-api/RootPerformanceQuality.js +2 -0
  268. package/dist/engine/agent-api/RootPerformanceQuality.js.map +1 -0
  269. package/dist/engine/agent-api/RootRuntimeSupport.d.ts +61 -0
  270. package/dist/engine/agent-api/RootRuntimeSupport.d.ts.map +1 -0
  271. package/dist/engine/agent-api/RootRuntimeSupport.js +127 -0
  272. package/dist/engine/agent-api/RootRuntimeSupport.js.map +1 -0
  273. package/dist/engine/agent-api/RuntimeNodeHandle.d.ts +28 -0
  274. package/dist/engine/agent-api/RuntimeNodeHandle.d.ts.map +1 -1
  275. package/dist/engine/agent-api/RuntimeNodeHandle.js.map +1 -1
  276. package/dist/engine/agent-api/Scatter.d.ts +18 -0
  277. package/dist/engine/agent-api/Scatter.d.ts.map +1 -0
  278. package/dist/engine/agent-api/Scatter.js +18 -0
  279. package/dist/engine/agent-api/Scatter.js.map +1 -0
  280. package/dist/engine/agent-api/TouchControlBinding.d.ts +32 -0
  281. package/dist/engine/agent-api/TouchControlBinding.d.ts.map +1 -1
  282. package/dist/engine/agent-api/TouchControlBinding.js +36 -0
  283. package/dist/engine/agent-api/TouchControlBinding.js.map +1 -1
  284. package/dist/engine/agent-api/VehicleChassis.d.ts.map +1 -1
  285. package/dist/engine/agent-api/VehicleChassis.js +76 -13
  286. package/dist/engine/agent-api/VehicleChassis.js.map +1 -1
  287. package/dist/engine/agent-api/WorldLabelRenderer.d.ts +13 -2
  288. package/dist/engine/agent-api/WorldLabelRenderer.d.ts.map +1 -1
  289. package/dist/engine/agent-api/WorldLabelRenderer.js +58 -26
  290. package/dist/engine/agent-api/WorldLabelRenderer.js.map +1 -1
  291. package/dist/engine/agent-api/index.d.ts +804 -7
  292. package/dist/engine/agent-api/index.d.ts.map +1 -1
  293. package/dist/engine/agent-api/index.js +2326 -195
  294. package/dist/engine/agent-api/index.js.map +1 -1
  295. package/dist/engine/agent-api/product-viewer-runtime.d.ts.map +1 -1
  296. package/dist/engine/game/GameAudio.d.ts +66 -0
  297. package/dist/engine/game/GameAudio.d.ts.map +1 -1
  298. package/dist/engine/game/GameAudio.js +179 -44
  299. package/dist/engine/game/GameAudio.js.map +1 -1
  300. package/dist/engine/instances-model/InstancedModel.d.ts +111 -0
  301. package/dist/engine/instances-model/InstancedModel.d.ts.map +1 -0
  302. package/dist/engine/instances-model/InstancedModel.js +108 -0
  303. package/dist/engine/instances-model/InstancedModel.js.map +1 -0
  304. package/dist/engine/material-physical/PhysicalMaterialSpec.d.ts +72 -0
  305. package/dist/engine/material-physical/PhysicalMaterialSpec.d.ts.map +1 -0
  306. package/dist/engine/material-physical/PhysicalMaterialSpec.js +95 -0
  307. package/dist/engine/material-physical/PhysicalMaterialSpec.js.map +1 -0
  308. package/dist/engine/production-runtime/GameAppRuntime.d.ts +1 -1
  309. package/dist/engine/production-runtime/GameAppRuntime.d.ts.map +1 -1
  310. package/dist/engine/production-runtime/GameRenderPreset.d.ts +76 -0
  311. package/dist/engine/production-runtime/GameRenderPreset.d.ts.map +1 -1
  312. package/dist/engine/production-runtime/GameRenderPreset.js +167 -0
  313. package/dist/engine/production-runtime/GameRenderPreset.js.map +1 -1
  314. package/dist/engine/production-runtime/RootGpuParticleWorkload.d.ts +34 -0
  315. package/dist/engine/production-runtime/RootGpuParticleWorkload.d.ts.map +1 -0
  316. package/dist/engine/production-runtime/RootGpuParticleWorkload.js +117 -0
  317. package/dist/engine/production-runtime/RootGpuParticleWorkload.js.map +1 -0
  318. package/dist/engine/production-runtime/RootRenderSourceBridge.d.ts +2 -0
  319. package/dist/engine/production-runtime/RootRenderSourceBridge.d.ts.map +1 -0
  320. package/dist/engine/production-runtime/RootRenderSourceBridge.js +2 -0
  321. package/dist/engine/production-runtime/RootRenderSourceBridge.js.map +1 -0
  322. package/dist/engine/production-runtime/TypedGLBActor.d.ts +28 -2
  323. package/dist/engine/production-runtime/TypedGLBActor.d.ts.map +1 -1
  324. package/dist/engine/production-runtime/TypedGLBActor.js +46 -1
  325. package/dist/engine/production-runtime/TypedGLBActor.js.map +1 -1
  326. package/dist/engine/production-runtime/index.d.ts +6 -4
  327. package/dist/engine/production-runtime/index.d.ts.map +1 -1
  328. package/dist/engine/production-runtime/index.js +7 -3
  329. package/dist/engine/production-runtime/index.js.map +1 -1
  330. package/dist/input/ActionMap.d.ts +40 -0
  331. package/dist/input/ActionMap.d.ts.map +1 -1
  332. package/dist/input/ActionMap.js +179 -0
  333. package/dist/input/ActionMap.js.map +1 -1
  334. package/dist/input/ComboDetector.d.ts +49 -0
  335. package/dist/input/ComboDetector.d.ts.map +1 -0
  336. package/dist/input/ComboDetector.js +102 -0
  337. package/dist/input/ComboDetector.js.map +1 -0
  338. package/dist/input/Haptics.d.ts +46 -0
  339. package/dist/input/Haptics.d.ts.map +1 -0
  340. package/dist/input/Haptics.js +67 -0
  341. package/dist/input/Haptics.js.map +1 -0
  342. package/dist/input/InputSystem.d.ts.map +1 -1
  343. package/dist/input/InputSystem.js +1 -0
  344. package/dist/input/InputSystem.js.map +1 -1
  345. package/dist/input/PointerDevice.d.ts +1 -0
  346. package/dist/input/PointerDevice.d.ts.map +1 -1
  347. package/dist/input/PointerDevice.js +1 -1
  348. package/dist/input/PointerDevice.js.map +1 -1
  349. package/dist/input/TouchLayouts.d.ts +35 -0
  350. package/dist/input/TouchLayouts.d.ts.map +1 -0
  351. package/dist/input/TouchLayouts.js +88 -0
  352. package/dist/input/TouchLayouts.js.map +1 -0
  353. package/dist/input/WebXRSessionController.d.ts +53 -0
  354. package/dist/input/WebXRSessionController.d.ts.map +1 -1
  355. package/dist/input/WebXRSessionController.js +88 -2
  356. package/dist/input/WebXRSessionController.js.map +1 -1
  357. package/dist/input/controls/OrbitControls.d.ts +44 -1
  358. package/dist/input/controls/OrbitControls.d.ts.map +1 -1
  359. package/dist/input/controls/OrbitControls.js +151 -12
  360. package/dist/input/controls/OrbitControls.js.map +1 -1
  361. package/dist/input/index.d.ts +8 -2
  362. package/dist/input/index.d.ts.map +1 -1
  363. package/dist/input/index.js +3 -0
  364. package/dist/input/index.js.map +1 -1
  365. package/dist/lean/base.d.ts +1 -1
  366. package/dist/lean/base.d.ts.map +1 -1
  367. package/dist/lean/base.js +1 -1
  368. package/dist/lean/base.js.map +1 -1
  369. package/dist/lean/game.d.ts +98 -0
  370. package/dist/lean/game.d.ts.map +1 -1
  371. package/dist/lean/game.js +192 -0
  372. package/dist/lean/game.js.map +1 -1
  373. package/dist/lean/index.js +1 -1
  374. package/dist/lean/index.js.map +1 -1
  375. package/dist/materials/GameReadyMaterialLibrary.d.ts +39 -0
  376. package/dist/materials/GameReadyMaterialLibrary.d.ts.map +1 -0
  377. package/dist/materials/GameReadyMaterialLibrary.js +253 -0
  378. package/dist/materials/GameReadyMaterialLibrary.js.map +1 -0
  379. package/dist/materials/MaterialValidation.d.ts +21 -0
  380. package/dist/materials/MaterialValidation.d.ts.map +1 -1
  381. package/dist/materials/MaterialValidation.js +105 -0
  382. package/dist/materials/MaterialValidation.js.map +1 -1
  383. package/dist/materials/browser-index.d.ts +2 -0
  384. package/dist/materials/browser-index.d.ts.map +1 -1
  385. package/dist/materials/browser-index.js +1 -0
  386. package/dist/materials/browser-index.js.map +1 -1
  387. package/dist/materials/node.d.ts +2 -2
  388. package/dist/materials/node.d.ts.map +1 -1
  389. package/dist/materials/node.js +1 -1
  390. package/dist/materials/node.js.map +1 -1
  391. package/dist/navigation-recast/index.d.ts +116 -0
  392. package/dist/navigation-recast/index.d.ts.map +1 -0
  393. package/dist/navigation-recast/index.js +247 -0
  394. package/dist/navigation-recast/index.js.map +1 -0
  395. package/dist/physics/CollisionVolumes.d.ts +59 -0
  396. package/dist/physics/CollisionVolumes.d.ts.map +1 -1
  397. package/dist/physics/CollisionVolumes.js +87 -0
  398. package/dist/physics/CollisionVolumes.js.map +1 -1
  399. package/dist/physics/PhysicsDebugDraw.d.ts +37 -0
  400. package/dist/physics/PhysicsDebugDraw.d.ts.map +1 -1
  401. package/dist/physics/PhysicsDebugDraw.js +38 -1
  402. package/dist/physics/PhysicsDebugDraw.js.map +1 -1
  403. package/dist/physics/PhysicsWorld.d.ts +15 -0
  404. package/dist/physics/PhysicsWorld.d.ts.map +1 -1
  405. package/dist/physics/PhysicsWorld.js +8 -1
  406. package/dist/physics/PhysicsWorld.js.map +1 -1
  407. package/dist/physics/solverless.d.ts +1 -1
  408. package/dist/physics/solverless.d.ts.map +1 -1
  409. package/dist/physics/solverless.js.map +1 -1
  410. package/dist/physics-rapier/HeightfieldLayout.d.ts +12 -0
  411. package/dist/physics-rapier/HeightfieldLayout.d.ts.map +1 -0
  412. package/dist/physics-rapier/HeightfieldLayout.js +19 -0
  413. package/dist/physics-rapier/HeightfieldLayout.js.map +1 -0
  414. package/dist/physics-rapier/index.d.ts +7 -1
  415. package/dist/physics-rapier/index.d.ts.map +1 -1
  416. package/dist/physics-rapier/index.js +3 -2
  417. package/dist/physics-rapier/index.js.map +1 -1
  418. package/dist/react/index.d.ts +136 -2
  419. package/dist/react/index.d.ts.map +1 -1
  420. package/dist/react/index.js +251 -10
  421. package/dist/react/index.js.map +1 -1
  422. package/dist/rendering/AtmosphereWetness.d.ts +61 -0
  423. package/dist/rendering/AtmosphereWetness.d.ts.map +1 -0
  424. package/dist/rendering/AtmosphereWetness.js +99 -0
  425. package/dist/rendering/AtmosphereWetness.js.map +1 -0
  426. package/dist/rendering/CascadedShadowMaps.d.ts.map +1 -1
  427. package/dist/rendering/CascadedShadowMaps.js +3 -1
  428. package/dist/rendering/CascadedShadowMaps.js.map +1 -1
  429. package/dist/rendering/ClusteredForwardLighting.d.ts +25 -1
  430. package/dist/rendering/ClusteredForwardLighting.d.ts.map +1 -1
  431. package/dist/rendering/ClusteredForwardLighting.js +80 -3
  432. package/dist/rendering/ClusteredForwardLighting.js.map +1 -1
  433. package/dist/rendering/DayNightSky.d.ts +63 -0
  434. package/dist/rendering/DayNightSky.d.ts.map +1 -0
  435. package/dist/rendering/DayNightSky.js +156 -0
  436. package/dist/rendering/DayNightSky.js.map +1 -0
  437. package/dist/rendering/EnvironmentPresetPack.d.ts +30 -0
  438. package/dist/rendering/EnvironmentPresetPack.d.ts.map +1 -0
  439. package/dist/rendering/EnvironmentPresetPack.js +49 -0
  440. package/dist/rendering/EnvironmentPresetPack.js.map +1 -0
  441. package/dist/rendering/ExtensionScalarAtlas.d.ts +18 -0
  442. package/dist/rendering/ExtensionScalarAtlas.d.ts.map +1 -0
  443. package/dist/rendering/ExtensionScalarAtlas.js +107 -0
  444. package/dist/rendering/ExtensionScalarAtlas.js.map +1 -0
  445. package/dist/rendering/ForwardPass.d.ts +65 -0
  446. package/dist/rendering/ForwardPass.d.ts.map +1 -1
  447. package/dist/rendering/ForwardPass.js +196 -2
  448. package/dist/rendering/ForwardPass.js.map +1 -1
  449. package/dist/rendering/InstancedPBRMaterial.d.ts +1 -0
  450. package/dist/rendering/InstancedPBRMaterial.d.ts.map +1 -1
  451. package/dist/rendering/InstancedPBRMaterial.js +3 -0
  452. package/dist/rendering/InstancedPBRMaterial.js.map +1 -1
  453. package/dist/rendering/InstancingDiagnostics.d.ts +69 -0
  454. package/dist/rendering/InstancingDiagnostics.d.ts.map +1 -0
  455. package/dist/rendering/InstancingDiagnostics.js +98 -0
  456. package/dist/rendering/InstancingDiagnostics.js.map +1 -0
  457. package/dist/rendering/LeanWebGL2Device.d.ts +265 -0
  458. package/dist/rendering/LeanWebGL2Device.d.ts.map +1 -0
  459. package/dist/rendering/LeanWebGL2Device.js +4001 -0
  460. package/dist/rendering/LeanWebGL2Device.js.map +1 -0
  461. package/dist/rendering/LightingRig.d.ts +19 -1
  462. package/dist/rendering/LightingRig.d.ts.map +1 -1
  463. package/dist/rendering/LightingRig.js +61 -1
  464. package/dist/rendering/LightingRig.js.map +1 -1
  465. package/dist/rendering/MaterialBinding.js +1 -1
  466. package/dist/rendering/MaterialBinding.js.map +1 -1
  467. package/dist/rendering/MeshConsolidation.d.ts +12 -0
  468. package/dist/rendering/MeshConsolidation.d.ts.map +1 -1
  469. package/dist/rendering/MeshConsolidation.js +28 -0
  470. package/dist/rendering/MeshConsolidation.js.map +1 -1
  471. package/dist/rendering/MorphTargetPlan.d.ts +29 -2
  472. package/dist/rendering/MorphTargetPlan.d.ts.map +1 -1
  473. package/dist/rendering/MorphTargetPlan.js +23 -4
  474. package/dist/rendering/MorphTargetPlan.js.map +1 -1
  475. package/dist/rendering/NormalMappedPBRMaterial.d.ts.map +1 -1
  476. package/dist/rendering/NormalMappedPBRMaterial.js +22 -2
  477. package/dist/rendering/NormalMappedPBRMaterial.js.map +1 -1
  478. package/dist/rendering/OceanSurface.d.ts +75 -0
  479. package/dist/rendering/OceanSurface.d.ts.map +1 -1
  480. package/dist/rendering/OceanSurface.js +211 -0
  481. package/dist/rendering/OceanSurface.js.map +1 -1
  482. package/dist/rendering/PBRMaterial.d.ts +1 -0
  483. package/dist/rendering/PBRMaterial.d.ts.map +1 -1
  484. package/dist/rendering/PBRMaterial.js +23 -2
  485. package/dist/rendering/PBRMaterial.js.map +1 -1
  486. package/dist/rendering/PbrReference.d.ts +48 -0
  487. package/dist/rendering/PbrReference.d.ts.map +1 -1
  488. package/dist/rendering/PbrReference.js +126 -0
  489. package/dist/rendering/PbrReference.js.map +1 -1
  490. package/dist/rendering/PlanarReflection.d.ts +221 -0
  491. package/dist/rendering/PlanarReflection.d.ts.map +1 -0
  492. package/dist/rendering/PlanarReflection.js +541 -0
  493. package/dist/rendering/PlanarReflection.js.map +1 -0
  494. package/dist/rendering/PostProcessPass.d.ts +32 -0
  495. package/dist/rendering/PostProcessPass.d.ts.map +1 -1
  496. package/dist/rendering/PostProcessPass.js +79 -36
  497. package/dist/rendering/PostProcessPass.js.map +1 -1
  498. package/dist/rendering/ProjectionMath.d.ts +3 -0
  499. package/dist/rendering/ProjectionMath.d.ts.map +1 -0
  500. package/dist/rendering/ProjectionMath.js +26 -0
  501. package/dist/rendering/ProjectionMath.js.map +1 -0
  502. package/dist/rendering/ReflectionSurfaces.d.ts +17 -0
  503. package/dist/rendering/ReflectionSurfaces.d.ts.map +1 -1
  504. package/dist/rendering/ReflectionSurfaces.js +103 -1
  505. package/dist/rendering/ReflectionSurfaces.js.map +1 -1
  506. package/dist/rendering/RenderDevice.d.ts +92 -0
  507. package/dist/rendering/RenderDevice.d.ts.map +1 -1
  508. package/dist/rendering/RenderDevice.js +85 -0
  509. package/dist/rendering/RenderDevice.js.map +1 -1
  510. package/dist/rendering/Renderer.d.ts +9 -0
  511. package/dist/rendering/Renderer.d.ts.map +1 -1
  512. package/dist/rendering/Renderer.js +97 -9
  513. package/dist/rendering/Renderer.js.map +1 -1
  514. package/dist/rendering/RendererPostprocessPlan.d.ts +64 -3
  515. package/dist/rendering/RendererPostprocessPlan.d.ts.map +1 -1
  516. package/dist/rendering/RendererPostprocessPlan.js +116 -12
  517. package/dist/rendering/RendererPostprocessPlan.js.map +1 -1
  518. package/dist/rendering/RendererVisualPipelineReport.d.ts.map +1 -1
  519. package/dist/rendering/RendererVisualPipelineReport.js +3 -1
  520. package/dist/rendering/RendererVisualPipelineReport.js.map +1 -1
  521. package/dist/rendering/Sampler.d.ts +29 -0
  522. package/dist/rendering/Sampler.d.ts.map +1 -1
  523. package/dist/rendering/Sampler.js +47 -0
  524. package/dist/rendering/Sampler.js.map +1 -1
  525. package/dist/rendering/ScreenSpaceReflectionPass.d.ts +43 -0
  526. package/dist/rendering/ScreenSpaceReflectionPass.d.ts.map +1 -0
  527. package/dist/rendering/ScreenSpaceReflectionPass.js +58 -0
  528. package/dist/rendering/ScreenSpaceReflectionPass.js.map +1 -0
  529. package/dist/rendering/SdfText.d.ts +250 -0
  530. package/dist/rendering/SdfText.d.ts.map +1 -0
  531. package/dist/rendering/SdfText.js +551 -0
  532. package/dist/rendering/SdfText.js.map +1 -0
  533. package/dist/rendering/ShaderChunks.d.ts.map +1 -1
  534. package/dist/rendering/ShaderChunks.js +30 -12
  535. package/dist/rendering/ShaderChunks.js.map +1 -1
  536. package/dist/rendering/ShaderLibrary.d.ts.map +1 -1
  537. package/dist/rendering/ShaderLibrary.js +601 -186
  538. package/dist/rendering/ShaderLibrary.js.map +1 -1
  539. package/dist/rendering/ShaderLibraryCore.d.ts +3 -0
  540. package/dist/rendering/ShaderLibraryCore.d.ts.map +1 -1
  541. package/dist/rendering/ShaderLibraryCore.js +73 -12
  542. package/dist/rendering/ShaderLibraryCore.js.map +1 -1
  543. package/dist/rendering/ShadowMap.d.ts +24 -0
  544. package/dist/rendering/ShadowMap.d.ts.map +1 -1
  545. package/dist/rendering/ShadowMap.js +43 -0
  546. package/dist/rendering/ShadowMap.js.map +1 -1
  547. package/dist/rendering/SkinnedLitMaterial.d.ts.map +1 -1
  548. package/dist/rendering/SkinnedLitMaterial.js +2 -0
  549. package/dist/rendering/SkinnedLitMaterial.js.map +1 -1
  550. package/dist/rendering/SkinningBounds.d.ts +10 -0
  551. package/dist/rendering/SkinningBounds.d.ts.map +1 -1
  552. package/dist/rendering/SkinningBounds.js +27 -0
  553. package/dist/rendering/SkinningBounds.js.map +1 -1
  554. package/dist/rendering/SpriteFlipbook.d.ts +60 -0
  555. package/dist/rendering/SpriteFlipbook.d.ts.map +1 -0
  556. package/dist/rendering/SpriteFlipbook.js +128 -0
  557. package/dist/rendering/SpriteFlipbook.js.map +1 -0
  558. package/dist/rendering/TemporalHistory.d.ts +37 -0
  559. package/dist/rendering/TemporalHistory.d.ts.map +1 -0
  560. package/dist/rendering/TemporalHistory.js +135 -0
  561. package/dist/rendering/TemporalHistory.js.map +1 -0
  562. package/dist/rendering/TemporalMath.d.ts +3 -0
  563. package/dist/rendering/TemporalMath.d.ts.map +1 -0
  564. package/dist/rendering/TemporalMath.js +9 -0
  565. package/dist/rendering/TemporalMath.js.map +1 -0
  566. package/dist/rendering/TerrainTiles.d.ts +120 -0
  567. package/dist/rendering/TerrainTiles.d.ts.map +1 -0
  568. package/dist/rendering/TerrainTiles.js +221 -0
  569. package/dist/rendering/TerrainTiles.js.map +1 -0
  570. package/dist/rendering/TexturedPBRMaterial.d.ts +4 -0
  571. package/dist/rendering/TexturedPBRMaterial.d.ts.map +1 -1
  572. package/dist/rendering/TexturedPBRMaterial.js +88 -5
  573. package/dist/rendering/TexturedPBRMaterial.js.map +1 -1
  574. package/dist/rendering/TexturedUnlitMaterial.d.ts.map +1 -1
  575. package/dist/rendering/TexturedUnlitMaterial.js +20 -2
  576. package/dist/rendering/TexturedUnlitMaterial.js.map +1 -1
  577. package/dist/rendering/UnlitMaterial.d.ts.map +1 -1
  578. package/dist/rendering/UnlitMaterial.js +20 -2
  579. package/dist/rendering/UnlitMaterial.js.map +1 -1
  580. package/dist/rendering/VertexFormat.d.ts +6 -0
  581. package/dist/rendering/VertexFormat.d.ts.map +1 -1
  582. package/dist/rendering/VertexFormat.js +12 -0
  583. package/dist/rendering/VertexFormat.js.map +1 -1
  584. package/dist/rendering/VolumetricFog.d.ts +55 -0
  585. package/dist/rendering/VolumetricFog.d.ts.map +1 -0
  586. package/dist/rendering/VolumetricFog.js +91 -0
  587. package/dist/rendering/VolumetricFog.js.map +1 -0
  588. package/dist/rendering/WaterSurface.d.ts +77 -0
  589. package/dist/rendering/WaterSurface.d.ts.map +1 -0
  590. package/dist/rendering/WaterSurface.js +133 -0
  591. package/dist/rendering/WaterSurface.js.map +1 -0
  592. package/dist/rendering/WebGL2Device.d.ts +73 -0
  593. package/dist/rendering/WebGL2Device.d.ts.map +1 -1
  594. package/dist/rendering/WebGL2Device.js +836 -79
  595. package/dist/rendering/WebGL2Device.js.map +1 -1
  596. package/dist/rendering/WebGPUDevice.d.ts +62 -1
  597. package/dist/rendering/WebGPUDevice.d.ts.map +1 -1
  598. package/dist/rendering/WebGPUDevice.js +844 -51
  599. package/dist/rendering/WebGPUDevice.js.map +1 -1
  600. package/dist/rendering/WebGPUExtensionAtlas.d.ts +6 -0
  601. package/dist/rendering/WebGPUExtensionAtlas.d.ts.map +1 -0
  602. package/dist/rendering/WebGPUExtensionAtlas.js +225 -0
  603. package/dist/rendering/WebGPUExtensionAtlas.js.map +1 -0
  604. package/dist/rendering/WebGPUSkinningLimits.d.ts +40 -0
  605. package/dist/rendering/WebGPUSkinningLimits.d.ts.map +1 -1
  606. package/dist/rendering/WebGPUSkinningLimits.js +32 -0
  607. package/dist/rendering/WebGPUSkinningLimits.js.map +1 -1
  608. package/dist/rendering/effects/CollisionModule.d.ts +9 -0
  609. package/dist/rendering/effects/CollisionModule.d.ts.map +1 -1
  610. package/dist/rendering/effects/CollisionModule.js +10 -0
  611. package/dist/rendering/effects/CollisionModule.js.map +1 -1
  612. package/dist/rendering/effects/ColorModule.d.ts +7 -0
  613. package/dist/rendering/effects/ColorModule.d.ts.map +1 -1
  614. package/dist/rendering/effects/ColorModule.js +23 -0
  615. package/dist/rendering/effects/ColorModule.js.map +1 -1
  616. package/dist/rendering/effects/ForceModule.d.ts +35 -0
  617. package/dist/rendering/effects/ForceModule.d.ts.map +1 -1
  618. package/dist/rendering/effects/ForceModule.js +49 -0
  619. package/dist/rendering/effects/ForceModule.js.map +1 -1
  620. package/dist/rendering/effects/GPUParticleBackend.d.ts +188 -0
  621. package/dist/rendering/effects/GPUParticleBackend.d.ts.map +1 -1
  622. package/dist/rendering/effects/GPUParticleBackend.js +710 -1
  623. package/dist/rendering/effects/GPUParticleBackend.js.map +1 -1
  624. package/dist/rendering/effects/HeightfieldModule.d.ts +58 -0
  625. package/dist/rendering/effects/HeightfieldModule.d.ts.map +1 -0
  626. package/dist/rendering/effects/HeightfieldModule.js +129 -0
  627. package/dist/rendering/effects/HeightfieldModule.js.map +1 -0
  628. package/dist/rendering/effects/LightingModule.d.ts +34 -0
  629. package/dist/rendering/effects/LightingModule.d.ts.map +1 -0
  630. package/dist/rendering/effects/LightingModule.js +54 -0
  631. package/dist/rendering/effects/LightingModule.js.map +1 -0
  632. package/dist/rendering/effects/ParticleDiagnostics.d.ts +8 -0
  633. package/dist/rendering/effects/ParticleDiagnostics.d.ts.map +1 -1
  634. package/dist/rendering/effects/ParticleDiagnostics.js +3 -0
  635. package/dist/rendering/effects/ParticleDiagnostics.js.map +1 -1
  636. package/dist/rendering/effects/ParticleModule.d.ts +8 -0
  637. package/dist/rendering/effects/ParticleModule.d.ts.map +1 -1
  638. package/dist/rendering/effects/ParticleModule.js.map +1 -1
  639. package/dist/rendering/effects/ParticleRenderer.d.ts +20 -0
  640. package/dist/rendering/effects/ParticleRenderer.d.ts.map +1 -1
  641. package/dist/rendering/effects/ParticleRenderer.js +57 -7
  642. package/dist/rendering/effects/ParticleRenderer.js.map +1 -1
  643. package/dist/rendering/effects/ParticleSystem.d.ts +45 -1
  644. package/dist/rendering/effects/ParticleSystem.d.ts.map +1 -1
  645. package/dist/rendering/effects/ParticleSystem.js +373 -4
  646. package/dist/rendering/effects/ParticleSystem.js.map +1 -1
  647. package/dist/rendering/effects/ResidentGPUParticleRenderer.d.ts +158 -0
  648. package/dist/rendering/effects/ResidentGPUParticleRenderer.d.ts.map +1 -0
  649. package/dist/rendering/effects/ResidentGPUParticleRenderer.js +576 -0
  650. package/dist/rendering/effects/ResidentGPUParticleRenderer.js.map +1 -0
  651. package/dist/rendering/effects/SizeModule.d.ts +4 -0
  652. package/dist/rendering/effects/SizeModule.d.ts.map +1 -1
  653. package/dist/rendering/effects/SizeModule.js +16 -0
  654. package/dist/rendering/effects/SizeModule.js.map +1 -1
  655. package/dist/rendering/effects/SubEmitterModule.d.ts +61 -0
  656. package/dist/rendering/effects/SubEmitterModule.d.ts.map +1 -0
  657. package/dist/rendering/effects/SubEmitterModule.js +119 -0
  658. package/dist/rendering/effects/SubEmitterModule.js.map +1 -0
  659. package/dist/rendering/effects/TrailModule.d.ts +38 -0
  660. package/dist/rendering/effects/TrailModule.d.ts.map +1 -1
  661. package/dist/rendering/effects/TrailModule.js +95 -2
  662. package/dist/rendering/effects/TrailModule.js.map +1 -1
  663. package/dist/rendering/effects/TurbulenceModule.d.ts +43 -0
  664. package/dist/rendering/effects/TurbulenceModule.d.ts.map +1 -0
  665. package/dist/rendering/effects/TurbulenceModule.js +130 -0
  666. package/dist/rendering/effects/TurbulenceModule.js.map +1 -0
  667. package/dist/rendering/effects/index.d.ts +4 -0
  668. package/dist/rendering/effects/index.d.ts.map +1 -1
  669. package/dist/rendering/effects/index.js +4 -0
  670. package/dist/rendering/effects/index.js.map +1 -1
  671. package/dist/rendering/extension-scalar-atlas.d.ts +3 -0
  672. package/dist/rendering/extension-scalar-atlas.d.ts.map +1 -0
  673. package/dist/rendering/extension-scalar-atlas.js +2 -0
  674. package/dist/rendering/extension-scalar-atlas.js.map +1 -0
  675. package/dist/rendering/index.d.ts +58 -19
  676. package/dist/rendering/index.d.ts.map +1 -1
  677. package/dist/rendering/index.js +32 -10
  678. package/dist/rendering/index.js.map +1 -1
  679. package/dist/rendering/lean/LeanProductionRenderer.d.ts.map +1 -1
  680. package/dist/rendering/lean/LeanProductionRenderer.js +2 -2
  681. package/dist/rendering/lean/LeanProductionRenderer.js.map +1 -1
  682. package/dist/rendering/lean-core-runtime.d.ts +13 -0
  683. package/dist/rendering/lean-core-runtime.d.ts.map +1 -0
  684. package/dist/rendering/lean-core-runtime.js +10 -0
  685. package/dist/rendering/lean-core-runtime.js.map +1 -0
  686. package/dist/rendering/materials/MaterialExtensions.js +3 -3
  687. package/dist/rendering/materials/MaterialExtensions.js.map +1 -1
  688. package/dist/rendering/postprocess/NativeBloomPyramid.d.ts +44 -0
  689. package/dist/rendering/postprocess/NativeBloomPyramid.d.ts.map +1 -0
  690. package/dist/rendering/postprocess/NativeBloomPyramid.js +84 -0
  691. package/dist/rendering/postprocess/NativeBloomPyramid.js.map +1 -0
  692. package/dist/rendering/postprocess/NativeLdrEffectLuts.d.ts +10 -1
  693. package/dist/rendering/postprocess/NativeLdrEffectLuts.d.ts.map +1 -1
  694. package/dist/rendering/postprocess/NativeLdrEffectLuts.js +19 -2
  695. package/dist/rendering/postprocess/NativeLdrEffectLuts.js.map +1 -1
  696. package/dist/rendering/postprocess/index.d.ts +2 -0
  697. package/dist/rendering/postprocess/index.d.ts.map +1 -1
  698. package/dist/rendering/postprocess/index.js +1 -0
  699. package/dist/rendering/postprocess/index.js.map +1 -1
  700. package/dist/rendering/production-runtime/PBRHDRPipeline.d.ts +22 -0
  701. package/dist/rendering/production-runtime/PBRHDRPipeline.d.ts.map +1 -1
  702. package/dist/rendering/production-runtime/PBRHDRPipeline.js +125 -0
  703. package/dist/rendering/production-runtime/PBRHDRPipeline.js.map +1 -1
  704. package/dist/rendering/production-runtime/ProductionRendererTypes.d.ts +1 -0
  705. package/dist/rendering/production-runtime/ProductionRendererTypes.d.ts.map +1 -1
  706. package/dist/rendering/production-runtime/ProductionRendererTypes.js.map +1 -1
  707. package/dist/rendering/production-runtime/ProductionRuntimeRenderer.d.ts +3 -0
  708. package/dist/rendering/production-runtime/ProductionRuntimeRenderer.d.ts.map +1 -1
  709. package/dist/rendering/production-runtime/ProductionRuntimeRenderer.js +8 -1
  710. package/dist/rendering/production-runtime/ProductionRuntimeRenderer.js.map +1 -1
  711. package/dist/rendering/production-runtime/ProductionWebGL2Renderer.d.ts +4 -0
  712. package/dist/rendering/production-runtime/ProductionWebGL2Renderer.d.ts.map +1 -1
  713. package/dist/rendering/production-runtime/ProductionWebGL2Renderer.js +15 -0
  714. package/dist/rendering/production-runtime/ProductionWebGL2Renderer.js.map +1 -1
  715. package/dist/rendering/production-runtime/ProductionWebGPURenderer.d.ts +58 -0
  716. package/dist/rendering/production-runtime/ProductionWebGPURenderer.d.ts.map +1 -1
  717. package/dist/rendering/production-runtime/ProductionWebGPURenderer.js +127 -0
  718. package/dist/rendering/production-runtime/ProductionWebGPURenderer.js.map +1 -1
  719. package/dist/rendering/production-runtime/framegraph/FrameGraph.d.ts +4 -2
  720. package/dist/rendering/production-runtime/framegraph/FrameGraph.d.ts.map +1 -1
  721. package/dist/rendering/production-runtime/framegraph/FrameGraph.js +62 -4
  722. package/dist/rendering/production-runtime/framegraph/FrameGraph.js.map +1 -1
  723. package/dist/rendering/production-runtime/framegraph/NativeFrameGraphBindings.d.ts +32 -0
  724. package/dist/rendering/production-runtime/framegraph/NativeFrameGraphBindings.d.ts.map +1 -0
  725. package/dist/rendering/production-runtime/framegraph/NativeFrameGraphBindings.js +111 -0
  726. package/dist/rendering/production-runtime/framegraph/NativeFrameGraphBindings.js.map +1 -0
  727. package/dist/rendering/production-runtime/framegraph/RenderPass.d.ts +23 -0
  728. package/dist/rendering/production-runtime/framegraph/RenderPass.d.ts.map +1 -1
  729. package/dist/rendering/production-runtime/framegraph/RenderPass.js +87 -1
  730. package/dist/rendering/production-runtime/framegraph/RenderPass.js.map +1 -1
  731. package/dist/rendering/production-runtime/index.d.ts +7 -4
  732. package/dist/rendering/production-runtime/index.d.ts.map +1 -1
  733. package/dist/rendering/production-runtime/index.js +4 -2
  734. package/dist/rendering/production-runtime/index.js.map +1 -1
  735. package/dist/rendering/production-runtime/passes/DepthPrepass.d.ts +18 -1
  736. package/dist/rendering/production-runtime/passes/DepthPrepass.d.ts.map +1 -1
  737. package/dist/rendering/production-runtime/passes/DepthPrepass.js +41 -2
  738. package/dist/rendering/production-runtime/passes/DepthPrepass.js.map +1 -1
  739. package/dist/rendering/production-runtime/passes/FramegraphTopology.d.ts +13 -0
  740. package/dist/rendering/production-runtime/passes/FramegraphTopology.d.ts.map +1 -0
  741. package/dist/rendering/production-runtime/passes/FramegraphTopology.js +59 -0
  742. package/dist/rendering/production-runtime/passes/FramegraphTopology.js.map +1 -0
  743. package/dist/rendering/production-runtime/passes/OpaquePass.d.ts +19 -1
  744. package/dist/rendering/production-runtime/passes/OpaquePass.d.ts.map +1 -1
  745. package/dist/rendering/production-runtime/passes/OpaquePass.js +43 -2
  746. package/dist/rendering/production-runtime/passes/OpaquePass.js.map +1 -1
  747. package/dist/rendering/production-runtime/passes/ShadowPass.d.ts +19 -1
  748. package/dist/rendering/production-runtime/passes/ShadowPass.d.ts.map +1 -1
  749. package/dist/rendering/production-runtime/passes/ShadowPass.js +45 -2
  750. package/dist/rendering/production-runtime/passes/ShadowPass.js.map +1 -1
  751. package/dist/rendering/production-runtime/passes/SkyboxPass.d.ts +17 -1
  752. package/dist/rendering/production-runtime/passes/SkyboxPass.d.ts.map +1 -1
  753. package/dist/rendering/production-runtime/passes/SkyboxPass.js +41 -2
  754. package/dist/rendering/production-runtime/passes/SkyboxPass.js.map +1 -1
  755. package/dist/rendering/production-runtime/passes/ToneMappingPass.d.ts +26 -1
  756. package/dist/rendering/production-runtime/passes/ToneMappingPass.d.ts.map +1 -1
  757. package/dist/rendering/production-runtime/passes/ToneMappingPass.js +62 -2
  758. package/dist/rendering/production-runtime/passes/ToneMappingPass.js.map +1 -1
  759. package/dist/rendering/production-runtime/passes/TransparentPass.d.ts +19 -1
  760. package/dist/rendering/production-runtime/passes/TransparentPass.d.ts.map +1 -1
  761. package/dist/rendering/production-runtime/passes/TransparentPass.js +47 -2
  762. package/dist/rendering/production-runtime/passes/TransparentPass.js.map +1 -1
  763. package/dist/rendering/reflection-surfaces.d.ts +5 -0
  764. package/dist/rendering/reflection-surfaces.d.ts.map +1 -0
  765. package/dist/rendering/reflection-surfaces.js +3 -0
  766. package/dist/rendering/reflection-surfaces.js.map +1 -0
  767. package/dist/rendering/shadows/CascadeHysteresis.d.ts +67 -0
  768. package/dist/rendering/shadows/CascadeHysteresis.d.ts.map +1 -0
  769. package/dist/rendering/shadows/CascadeHysteresis.js +104 -0
  770. package/dist/rendering/shadows/CascadeHysteresis.js.map +1 -0
  771. package/dist/rendering/shadows/ContactShadows.d.ts +52 -0
  772. package/dist/rendering/shadows/ContactShadows.d.ts.map +1 -1
  773. package/dist/rendering/shadows/ContactShadows.js +122 -0
  774. package/dist/rendering/shadows/ContactShadows.js.map +1 -1
  775. package/dist/rendering/shadows/SpotShadowMaps.d.ts +77 -0
  776. package/dist/rendering/shadows/SpotShadowMaps.d.ts.map +1 -0
  777. package/dist/rendering/shadows/SpotShadowMaps.js +106 -0
  778. package/dist/rendering/shadows/SpotShadowMaps.js.map +1 -0
  779. package/dist/rendering/webgpu/WebGPUPostProcess.d.ts +2 -0
  780. package/dist/rendering/webgpu/WebGPUPostProcess.d.ts.map +1 -1
  781. package/dist/rendering/webgpu/WebGPUPostProcess.js +1 -0
  782. package/dist/rendering/webgpu/WebGPUPostProcess.js.map +1 -1
  783. package/dist/rendering/webgpu/WebGPUPostShaders.d.ts +77 -0
  784. package/dist/rendering/webgpu/WebGPUPostShaders.d.ts.map +1 -0
  785. package/dist/rendering/webgpu/WebGPUPostShaders.js +261 -0
  786. package/dist/rendering/webgpu/WebGPUPostShaders.js.map +1 -0
  787. package/dist/rendering/webgpu/WebGPUTemporal.d.ts +17 -0
  788. package/dist/rendering/webgpu/WebGPUTemporal.d.ts.map +1 -0
  789. package/dist/rendering/webgpu/WebGPUTemporal.js +64 -0
  790. package/dist/rendering/webgpu/WebGPUTemporal.js.map +1 -0
  791. package/dist/rendering/webgpu.d.ts +6 -0
  792. package/dist/rendering/webgpu.d.ts.map +1 -1
  793. package/dist/rendering/webgpu.js +4 -0
  794. package/dist/rendering/webgpu.js.map +1 -1
  795. package/dist/scene/Scene.d.ts +8 -1
  796. package/dist/scene/Scene.d.ts.map +1 -1
  797. package/dist/scene/Scene.js +12 -2
  798. package/dist/scene/Scene.js.map +1 -1
  799. package/dist/scripting/VisualGraphContext.d.ts +31 -0
  800. package/dist/scripting/VisualGraphContext.d.ts.map +1 -1
  801. package/dist/scripting/VisualGraphExecutor.d.ts.map +1 -1
  802. package/dist/scripting/VisualGraphExecutor.js +110 -0
  803. package/dist/scripting/VisualGraphExecutor.js.map +1 -1
  804. package/dist/scripting/VisualNodeCatalog.d.ts +1 -1
  805. package/dist/scripting/VisualNodeCatalog.d.ts.map +1 -1
  806. package/dist/scripting/VisualNodeCatalog.js +20 -0
  807. package/dist/scripting/VisualNodeCatalog.js.map +1 -1
  808. package/dist/scripting/VisualScriptingRoot.d.ts +96 -0
  809. package/dist/scripting/VisualScriptingRoot.d.ts.map +1 -0
  810. package/dist/scripting/VisualScriptingRoot.js +150 -0
  811. package/dist/scripting/VisualScriptingRoot.js.map +1 -0
  812. package/dist/scripting/index.d.ts +3 -1
  813. package/dist/scripting/index.d.ts.map +1 -1
  814. package/dist/scripting/index.js +1 -0
  815. package/dist/scripting/index.js.map +1 -1
  816. package/package.json +40 -25
  817. package/templates/cinematic-scene/package.json +2 -2
  818. package/templates/cinematic-scene/src/main.ts +14 -2
  819. package/templates/mini-game/package.json +2 -2
  820. package/templates/product-viewer/package.json +2 -2
  821. package/templates/production-asset-inspector/package.json +1 -1
  822. package/templates/production-material-studio/package.json +1 -1
  823. package/templates/production-product-configurator/package.json +1 -1
  824. package/templates/production-product-viewer/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { applyRootParticleQuality, composeModelInstanceMatrices, createCameraProjection, DeferredFrameResources, getRootPerformanceBaseSize, getRootPerformanceQuality, getRootRenderSource, hasRootRenderableContent, includeRootSourceMetadata, initializeRootPerformanceQuality, readRootDiagnosticSnapshot, resolveCameraClipping, resolveRootRenderTime, setRootPerformanceQuality, supportsRootParticleQuality, validateRootPerformanceQuality } from "./RootRuntimeSupport.js";
1
2
  /*
2
3
  * WS-2.2 — module-scope physics values come from the SOLVERLESS entry.
3
4
  *
@@ -22,10 +23,11 @@ import { PhysicsDebugDraw, PhysicsStepper, ScenePhysicsBridge, Shape as PhysicsS
22
23
  * of which arrived just to reach one class.
23
24
  */
24
25
  import { PhysicsWorld } from "../../physics/world.js";
25
- import { createExternalParityEnvironmentLighting, Geometry, IndexBuffer, InstancedPBRMaterial, PBRMaterial, ProductionRuntimeRenderer, srgbToLinearChannel, VertexBuffer, VertexFormat } from "../../rendering/index.js";
26
+ import { createBeamDescriptor, createDayNightSky, createDualProbeEnvironmentLightingResources, createExternalParityEnvironmentLighting, createProductionEnvironmentLightingResources, createProductionPbrHdrPipelineFromRadiance, createSdfFontAtlas, createSdfTextQuadMesh, createSpotShadowProjection, createWaterSurface, createWeatherState, describeSdfTextPixelBacking, describeWetMaterial, layoutSdfText, rasterizeSdfTextLabelImage, resolveFlipbookUv, resolveSdfTextFrameOpacity, resolveVolumetricFog, selectSpotShadowAtlasTier, sampleOceanFixture, Geometry, IndexBuffer, InstancedPBRMaterial, PBRMaterial, ProductionRuntimeRenderer, resolveSamplerAnisotropy, Sampler, srgbToLinearChannel, Texture, TexturedPBRMaterial, VertexBuffer, VertexFormat } from "../../rendering/index.js";
26
27
  import { DirectionalLight, PointLight, SpotLight } from "../../scene/index.js";
27
- import { lookAtMat4, multiplyMat4, orthographicMat4, perspectiveMat4 } from "../../scene/MathTypes.js";
28
+ import { identityMat4, lookAtMat4, multiplyMat4 } from "../../scene/MathTypes.js";
28
29
  import { AURA_NORMALIZED_MODEL_MAX_DIMENSION, boundsFromAsset, boundsHeight, boundsMaxDimension, boundsSize } from "./SceneGroundingUtils.js";
30
+ import { collectLabelTelemetry, labelTelemetryRoleFor, summarizeTextBuckets } from "./LabelTelemetry.js";
29
31
  import { createWorldLabelLayer } from "./WorldLabelRenderer.js";
30
32
  import { createAuraText3DGeometry, defineAuraCustomGeometry, selectAuraRootLodLevel } from "./RootGeometry.js";
31
33
  export * from "./RootGeometry.js";
@@ -49,8 +51,18 @@ export * from "./PhysicsRuntime.js";
49
51
  */
50
52
  export { buildMeshBVH, createMeshSurfaceQuery, raycastMesh } from "../../physics/solverless.js";
51
53
  import { createPhysicsRuntime } from "./PhysicsRuntime.js";
54
+ import { gameCameraRigs } from "./GameCameraRigs.js";
55
+ import { gameFeelBuilders } from "./GameFeel.js";
52
56
  export * from "./FocusSelection.js";
53
57
  export * from "./WorldLabelRenderer.js";
58
+ export * from "./GameCameraRigs.js";
59
+ export * from "./GameFeel.js";
60
+ export * from "./LabelTelemetry.js";
61
+ export * from "./NavigationCrowds.js";
62
+ export * from "./FootPlanting.js";
63
+ export * from "./Decals.js";
64
+ export * from "./Scatter.js";
65
+ export * from "./AssetDecoders.js";
54
66
  export * from "./VehicleChassis.js";
55
67
  export * from "./VehicleDriverAi.js";
56
68
  /**
@@ -83,7 +95,16 @@ export * from "./WebCodecsFrameEncoder.js";
83
95
  */
84
96
  export { GameInspector, createGameInspector } from "./GameInspector.js";
85
97
  import { createGameInspector } from "./GameInspector.js";
98
+ import { createPhysicalMaterialSpec } from "../material-physical/PhysicalMaterialSpec.js";
99
+ import { createInstancedModelNode } from "../instances-model/InstancedModel.js";
100
+ import { resolveWrinkleMapStrength, warnOnInstancingFallback } from "../../rendering/index.js";
86
101
  export { createGameAudio } from "../game/GameAudio.js";
102
+ export { PositionalEmitter, FootstepPlayer, createGameMixer, attachFocusPolicy, computeDistanceAttenuation, computeDopplerShift, resolveOcclusion } from "../../audio/index.js";
103
+ export { ComboDetector, createTouchLayoutPreset, probeHaptics, playHaptic } from "../../input/index.js";
104
+ export { attachVisualScriptingGraph, createVisualScriptingGraph, listVisualScriptingNodeCatalog } from "../../scripting/index.js";
105
+ export { createRootEditorSurface } from "../../editor-runtime/index.js";
106
+ import { attachVisualScriptingGraph as attachVisualScriptingGraphFn, createVisualScriptingGraph as createVisualScriptingGraphFn, listVisualScriptingNodeCatalog as listVisualScriptingNodeCatalogFn } from "../../scripting/index.js";
107
+ import { createRootEditorSurface as createRootEditorSurfaceFn } from "../../editor-runtime/index.js";
87
108
  export * from "./RenderProgressTracker.js";
88
109
  export * from "./AudioVisemeAnalyzer.js";
89
110
  export * from "./ExternalPhonemeAnalyzer.js";
@@ -114,6 +135,7 @@ export * from "./EpisodeTemplates.js";
114
135
  export * from "./AnimationMotionQuality.js";
115
136
  export * from "./AnimationRouteProof.js";
116
137
  export * from "./AnimationEpisodePackage.js";
138
+ import { evaluateDistancePrioritizedMipResidency } from "../../assets/browser-index.js";
117
139
  import { createFrameLoop } from "./FrameLoop.js";
118
140
  import { createCombatWorld, createGameCameraDirector, createGameEffects, applyGameCombatEventsToRuntime, createGameAccessibilityFocus, createGameAccessibilityLabel, createGameAccessibilityRuntimeSettings, createGameEventLog, createGameHighContrastSource, createGameHudBindings, createGameHudComboBinding, createGameHudCheckpointBinding, createGameHudDebugToggleBinding, createGameHudEventLogBinding, createGameHudHealthBinding, createGameHudLivesBinding, createGameHudMeterBinding, createGameHudObjectiveBinding, createGameHudRoundBinding, createGameHudScoreBinding, createGameHudSnapshot, createGameHudTimerBinding, createGameHudValueBinding, createGameBoxCollider, createGameCapsuleCollider, createGameColliderDebugGeometry, createGameCombatDebugGeometry, createGameCollisionWorld, createGamePlanarCollisionWorld, createGameDebugOverlayData, createGameDebugSceneNodes, createGameHitboxDebugGeometry, createGameSimulation, exportGameInputReplay, createGameFighting2DRules, createGameInput, createGameInputReplay, createGameInputReplayDriver, importGameInputReplay, createGameJumpAssist, createGameKinematicBody, createGamePauseControlsSource, createGameRectCollider, createGameReducedFlashSource, createGameReducedMotionSource, createGameSphereCollider, createGameTouchControlLayout, gameColliderAabb, gameColliders, gameEffectPresets, gameGuardboxes, gameHitboxes, gameHurtboxes, gameInputReplayEventsAt, gamePushboxes, runGameSimulation, gameTriggerVolumes } from "./GameRuntime.js";
119
141
  import { collectGameRuntimeEvidence as collectGameRuntimeEvidenceV105 } from "./GameEvidence.js";
@@ -153,6 +175,8 @@ export * from "./GameAssetValidation.js";
153
175
  export * from "./CharacterAssembly.js";
154
176
  export * from "./AssetEvidence.js";
155
177
  export * from "./AnimationController.js";
178
+ export * from "./AnimationMixerBuilders.js";
179
+ export * from "./AnimationDebugOverlay.js";
156
180
  export { gameAssetValidation, quaterniusGameReadyFighterValidationContract, validateQuaterniusGameReadyFighterAsset } from "./GameAssetValidation.js";
157
181
  export { createAnimationController } from "./AnimationController.js";
158
182
  export * from "./PromptAnimationContract.js";
@@ -244,7 +268,8 @@ export function model(asset, options = {}) {
244
268
  targetMaxDimension: options.targetMaxDimension,
245
269
  targetLength: options.targetLength,
246
270
  physics: options.physics,
247
- hiddenNodeNames: options.hiddenNodeNames
271
+ hiddenNodeNames: options.hiddenNodeNames,
272
+ wrinkle: options.wrinkle
248
273
  });
249
274
  }
250
275
  export function unsafeModelUrl(url, options = {}) {
@@ -337,15 +362,102 @@ export const instances = {
337
362
  cylinder: (options) => instancedPrimitive("cylinder", options),
338
363
  capsule: (options) => instancedPrimitive("capsule", options),
339
364
  torus: (options) => instancedPrimitive("torus", options),
340
- custom: (spec, options) => instancedPrimitive("custom", { ...options, geometry: defineAuraCustomGeometry(spec) })
365
+ custom: (spec, options) => instancedPrimitive("custom", { ...options, geometry: defineAuraCustomGeometry(spec) }),
366
+ /**
367
+ * P2 instanced GLB models (muse3jsparity-PRD): one draw class for repeated
368
+ * static models. Skinned actors fall back with a D1 warning at mount (E1
369
+ * owns rigs); explicitly unaware materials warn at build.
370
+ */
371
+ model: (asset, options) => {
372
+ const built = createInstancedModelNode({
373
+ asset,
374
+ ...(options.name !== undefined ? { name: options.name } : {}),
375
+ transforms: options.transforms.map((transform) => ({
376
+ ...(transform.position !== undefined ? { position: [...transform.position] } : {}),
377
+ ...(transform.rotation !== undefined ? { rotation: [...transform.rotation] } : {}),
378
+ ...(transform.scale !== undefined
379
+ ? { scale: typeof transform.scale === "number" ? transform.scale : [...transform.scale] }
380
+ : {})
381
+ })),
382
+ ...(options.colors ? { colors: [...options.colors] } : {}),
383
+ ...(options.lod ? { lod: options.lod } : {}),
384
+ materialInstancingAware: options.instancingAware ?? true,
385
+ ...(options.maxInstancesPerDraw !== undefined ? { maxInstancesPerDraw: options.maxInstancesPerDraw } : {}),
386
+ ...(options.material?.name !== undefined ? { materialName: options.material.name } : {})
387
+ });
388
+ const base = model(asset, options).toJSON();
389
+ return new AuraNodeBuilder({
390
+ ...base,
391
+ instances: built.node.instances.map((transform) => ({
392
+ ...(transform.position ? { position: [...transform.position] } : {}),
393
+ ...(transform.rotation ? { rotation: [...transform.rotation] } : {}),
394
+ ...(transform.scale !== undefined ? { scale: transform.scale } : {})
395
+ })),
396
+ // Safe: the builder only accepts AuraColor strings above, so no RGB tuples reach the node.
397
+ ...(built.node.instanceColors ? { instanceColors: [...built.node.instanceColors] } : {}),
398
+ ...(built.node.instanceLod ? { instanceLod: built.node.instanceLod } : {}),
399
+ instanceCulling: {
400
+ instanceCount: built.node.instanceCulling.instanceCount,
401
+ centroid: [...built.node.instanceCulling.centroid],
402
+ boundingRadius: built.node.instanceCulling.boundingRadius,
403
+ cullable: true
404
+ },
405
+ ...(built.diagnostics.fallbackWarning ? { instancedModelWarning: built.diagnostics.fallbackWarning.diagnostic } : {})
406
+ });
407
+ }
408
+ };
409
+ export const visualScripting = {
410
+ graph: createVisualScriptingGraphFn,
411
+ attach: attachVisualScriptingGraphFn,
412
+ catalog: listVisualScriptingNodeCatalogFn
413
+ };
414
+ const rootEditorSurface = createRootEditorSurfaceFn();
415
+ export const editor = {
416
+ undo: rootEditorSurface.undo,
417
+ redo: rootEditorSurface.redo,
418
+ gizmo: rootEditorSurface.attachGizmo,
419
+ playMode: { enter: rootEditorSurface.enterPlayMode, exit: rootEditorSurface.exitPlayMode },
420
+ outliner: rootEditorSurface.describeOutliner,
421
+ capabilityLabel: "editor"
341
422
  };
342
423
  export const geometry = {
343
424
  define: defineAuraCustomGeometry,
344
425
  custom: (spec, options = {}) => primitive("custom", { ...options, geometry: defineAuraCustomGeometry(spec) })
345
426
  };
427
+ let cachedSdfFontAtlas;
428
+ function rootSdfFontAtlas() {
429
+ // Baked once per session (~500ms default res); the caller never bakes per frame.
430
+ cachedSdfFontAtlas ??= createSdfFontAtlas();
431
+ return cachedSdfFontAtlas;
432
+ }
346
433
  export function text3D(text, options = {}) {
347
434
  const built = createAuraText3DGeometry(text, options);
348
- return primitive("custom", { ...options, geometry: built.geometry, text3D: { text: built.text, glyphCount: built.glyphCount, unsupportedCharacters: built.unsupportedCharacters, method: built.method } });
435
+ if ((options.backend ?? "extruded-mesh") === "extruded-mesh") {
436
+ return primitive("custom", { ...options, geometry: built.geometry, text3D: { text: built.text, glyphCount: built.glyphCount, unsupportedCharacters: built.unsupportedCharacters, method: built.method } });
437
+ }
438
+ // G1 SDF backend (muse3jsparity-PRD): the layout validates fail-loud and is
439
+ // recorded on the descriptor with the full author intent (size, spacing,
440
+ // style, occlusion policy) so the production bridge can replay the sampler
441
+ // at mount. The extruded mesh stays as the diagnosed fallback geometry.
442
+ const layout = layoutSdfText(text, rootSdfFontAtlas(), { size: options.size ?? 1, letterSpacing: options.letterSpacing, style: options.sdfStyle });
443
+ return primitive("custom", {
444
+ ...options,
445
+ geometry: built.geometry,
446
+ text3D: {
447
+ text: built.text,
448
+ glyphCount: built.glyphCount,
449
+ unsupportedCharacters: [...new Set([...built.unsupportedCharacters, ...layout.unsupportedCharacters])],
450
+ method: "sdf-atlas-quad",
451
+ backend: "sdf",
452
+ sdfQuadCount: layout.quads.length,
453
+ sdfWidthWorld: layout.widthWorld,
454
+ sdfHeightWorld: layout.heightWorld,
455
+ sdfSize: options.size ?? 1,
456
+ ...(options.letterSpacing === undefined ? {} : { sdfLetterSpacing: options.letterSpacing }),
457
+ ...(options.sdfStyle === undefined ? {} : { sdfStyle: options.sdfStyle }),
458
+ sdfOcclusion: options.sdfOcclusion ?? "dim"
459
+ }
460
+ });
349
461
  }
350
462
  export function distanceLod(options) {
351
463
  if (options.levels.length === 0)
@@ -397,6 +509,13 @@ function proceduralTexture(texture, options = {}) {
397
509
  colorB: options.colorB
398
510
  };
399
511
  }
512
+ const PHYSICAL_SPEC_KEYS = [
513
+ "color", "roughness", "metallic", "metalness", "clearcoat", "clearcoatRoughness",
514
+ "sheen", "sheenColor", "sheenRoughness", "iridescence", "iridescenceIOR",
515
+ "iridescenceThicknessRange", "anisotropy", "anisotropyRotation", "transmission",
516
+ "thickness", "attenuationColor", "attenuationDistance", "ior", "specularIntensity",
517
+ "specularColor"
518
+ ];
400
519
  export const material = {
401
520
  pbr: (options = {}) => ({
402
521
  color: "#d7dee8",
@@ -405,7 +524,29 @@ export const material = {
405
524
  metalness: options.metalness ?? options.metallic ?? 0,
406
525
  ...options
407
526
  }),
408
- physical: (options = {}) => material.pbr(options),
527
+ physical: (options = {}) => {
528
+ // P3 (muse3jsparity-PRD): the sync factory stays scalar (C1 decision —
529
+ // no renderer change); extension params validate through the physical
530
+ // matrix and every non-supported request rides the spec as a warning.
531
+ const defined = PHYSICAL_SPEC_KEYS.reduce((picked, key) => {
532
+ const value = options[key];
533
+ if (value !== undefined)
534
+ picked[key] = value;
535
+ return picked;
536
+ }, {});
537
+ const result = createPhysicalMaterialSpec(defined);
538
+ const normalized = PHYSICAL_SPEC_KEYS.reduce((picked, key) => {
539
+ const value = result.spec[key];
540
+ if (value !== undefined && options[key] !== undefined)
541
+ picked[key] = value;
542
+ return picked;
543
+ }, {});
544
+ return {
545
+ ...material.pbr(options),
546
+ ...normalized,
547
+ ...(result.boundedWarnings.length > 0 ? { physicalWarnings: [...result.boundedWarnings] } : {})
548
+ };
549
+ },
409
550
  emissive: (options = {}) => ({
410
551
  color: options.color ?? "#111827",
411
552
  emissive: options.emissive ?? options.color ?? "#38d6ff",
@@ -699,33 +840,41 @@ const materialCapabilityCatalog = [
699
840
  {
700
841
  id: "base-color-texture",
701
842
  label: "Base-color texture",
702
- rootSafeApi: "partial",
843
+ rootSafeApi: "supported",
703
844
  productionRuntime: "supported",
704
- evidence: "Imported GLB baseColorTexture is sampled by the root WebGL path, but the root material contract currently keeps texture support partial until controlled texture on/off pixels are retained.",
845
+ evidence: "Root textured-PBR upgrade resolves AuraAssetRef texture urls to sRGB baseColor textures with controlled texture on/off pixel deltas (tests/browser/root-textured-c1.spec.ts, tests/reports/root-textured-c1/c1-probe.json).",
705
846
  claimRule: "Can describe typed texture metadata and rendered textured assets; do not claim full texture-material parity without controlled root pixels."
706
847
  },
707
848
  {
708
849
  id: "metallic-roughness",
709
850
  label: "Metallic/roughness",
710
- rootSafeApi: "partial",
851
+ rootSafeApi: "supported",
711
852
  productionRuntime: "supported",
712
- evidence: "Root material specs expose values, but production-style material response needs route-specific pixel proof.",
853
+ evidence: "Root textured-PBR upgrade composites roughnessMap/metalnessMap to the glTF metallic-roughness convention with scalar fallbacks and controlled browser pixel proof (tests/browser/root-textured-c1.spec.ts).",
713
854
  claimRule: "Do not claim full PBR parity without browser evidence."
714
855
  },
715
856
  {
716
857
  id: "normal-map",
717
858
  label: "Normal maps",
718
- rootSafeApi: "partial",
859
+ rootSafeApi: "supported",
719
860
  productionRuntime: "supported",
720
- evidence: "Root specs carry normal/procedural-normal intent; production shader parity is not guaranteed.",
861
+ evidence: "Root textured-PBR upgrade binds linear normal maps with tangent-space sampling over authored TBN and controlled browser pixel proof (tests/browser/root-textured-c1.spec.ts).",
862
+ claimRule: "Claim only with material-specific screenshot evidence."
863
+ },
864
+ {
865
+ id: "occlusion-map",
866
+ label: "Occlusion maps",
867
+ rootSafeApi: "supported",
868
+ productionRuntime: "supported",
869
+ evidence: "Root textured-PBR upgrade binds linear occlusionMap textures (R channel) with occlusionStrength and controlled browser pixel proof (tests/browser/root-textured-c1.spec.ts).",
721
870
  claimRule: "Claim only with material-specific screenshot evidence."
722
871
  },
723
872
  {
724
873
  id: "emissive",
725
874
  label: "Emissive",
726
- rootSafeApi: "partial",
875
+ rootSafeApi: "supported",
727
876
  productionRuntime: "supported",
728
- evidence: "Root specs and effects expose emissive color; pixel-backed bloom is reported separately.",
877
+ evidence: "Root textured-PBR upgrade binds sRGB emissiveMap textures multiplied by the emissive factor with controlled browser pixel proof (tests/browser/root-textured-c1.spec.ts); pixel-backed bloom is reported separately.",
729
878
  claimRule: "Distinguish emissive material from postprocess bloom."
730
879
  },
731
880
  {
@@ -779,9 +928,9 @@ const materialCapabilityCatalog = [
779
928
  {
780
929
  id: "hdr-ibl",
781
930
  label: "HDR/IBL",
782
- rootSafeApi: "partial",
931
+ rootSafeApi: "supported",
783
932
  productionRuntime: "supported",
784
- evidence: "Root environment nodes request lighting intent; environment prefiltering is not proven in the root path.",
933
+ evidence: "Root environments.hdri resolves authored Radiance HDR through the HDR-to-cubemap-to-GGX-to-BRDF-LUT chain with iblPixelBacked diagnostics and controlled browser pixel deltas (tests/browser/root-ibl-b3.spec.ts, tests/reports/root-ibl-b3/b3-probe.json).",
785
934
  claimRule: "Root HDR/IBL claims require diagnostics and browser pixels."
786
935
  },
787
936
  {
@@ -805,7 +954,9 @@ function createMaterialCapabilityDiagnostics(input) {
805
954
  requested.add("metallic-roughness");
806
955
  if (spec.normal)
807
956
  requested.add("normal-map");
808
- if (spec.emissive || spec.emissiveIntensity !== undefined)
957
+ if (spec.occlusionMap || spec.occlusionStrength !== undefined)
958
+ requested.add("occlusion-map");
959
+ if (spec.emissive || spec.emissiveIntensity !== undefined || spec.emissiveMap)
809
960
  requested.add("emissive");
810
961
  if (spec.opacity !== undefined && spec.opacity < 1)
811
962
  requested.add("alpha");
@@ -829,6 +980,11 @@ function createMaterialCapabilityDiagnostics(input) {
829
980
  if (partialRequestedFeatures.length > 0) {
830
981
  warnings.push(`Partial root material features requested: ${partialRequestedFeatures.join(", ")}. Capture route pixels before claiming production material quality.`);
831
982
  }
983
+ for (const spec of specs) {
984
+ for (const physicalWarning of spec.physicalWarnings ?? []) {
985
+ warnings.push(`Physical material extension bounded: ${physicalWarning}`);
986
+ }
987
+ }
832
988
  if (unsupportedRequestedFeatures.length > 0) {
833
989
  warnings.push(`Metadata/internal material features requested: ${unsupportedRequestedFeatures.join(", ")}. Do not claim rendered support from root createAuraApp alone.`);
834
990
  }
@@ -1038,7 +1194,8 @@ export const lights = {
1038
1194
  name: options.name,
1039
1195
  position: options.position ?? [3, 4, 3],
1040
1196
  intensity: options.intensity ?? 1.5,
1041
- color: options.color ?? "#ffffff"
1197
+ color: options.color ?? "#ffffff",
1198
+ shadow: options.shadow
1042
1199
  }),
1043
1200
  point: (options = {}) => new AuraNodeBuilder({
1044
1201
  kind: "light",
@@ -1048,6 +1205,20 @@ export const lights = {
1048
1205
  intensity: options.intensity ?? 2,
1049
1206
  color: options.color ?? "#ffffff"
1050
1207
  }),
1208
+ spot: (options = {}) => new AuraNodeBuilder({
1209
+ kind: "light",
1210
+ light: "spot",
1211
+ name: options.name ?? "spot light",
1212
+ position: options.position ?? [0, 4, 0],
1213
+ target: options.target,
1214
+ angle: options.angle ?? Math.PI / 6,
1215
+ penumbra: options.penumbra ?? 0.4,
1216
+ distance: options.distance ?? 12,
1217
+ decay: options.decay,
1218
+ intensity: options.intensity ?? 8,
1219
+ color: options.color ?? "#ffffff",
1220
+ shadow: options.shadow
1221
+ }),
1051
1222
  studio: (options = {}) => new AuraNodeBuilder({
1052
1223
  kind: "light",
1053
1224
  light: "studio",
@@ -1100,6 +1271,7 @@ export const lights = {
1100
1271
  export const camera = {
1101
1272
  perspective: (options = {}) => ({
1102
1273
  mode: "perspective",
1274
+ ...resolveCameraClipping(options),
1103
1275
  position: options.position ?? [0, 1.4, 4],
1104
1276
  target: options.target ?? [0, 0.8, 0],
1105
1277
  fov: options.fov ?? 45
@@ -1109,6 +1281,7 @@ export const camera = {
1109
1281
  const target = options.target ?? [0, 0.8, 0];
1110
1282
  return {
1111
1283
  mode: "orbit",
1284
+ ...resolveCameraClipping(options),
1112
1285
  distance,
1113
1286
  target,
1114
1287
  position: options.position ?? [
@@ -1121,6 +1294,7 @@ export const camera = {
1121
1294
  },
1122
1295
  dolly: (options) => ({
1123
1296
  mode: "dolly",
1297
+ ...resolveCameraClipping(options),
1124
1298
  from: options.from,
1125
1299
  to: options.to,
1126
1300
  target: options.target ?? [0, 0.8, 0],
@@ -1130,6 +1304,7 @@ export const camera = {
1130
1304
  }),
1131
1305
  follow: (options) => ({
1132
1306
  mode: "follow",
1307
+ ...resolveCameraClipping(options),
1133
1308
  targetNode: options.targetNode,
1134
1309
  distance: options.distance ?? 5,
1135
1310
  position: options.position,
@@ -1145,6 +1320,7 @@ export const camera = {
1145
1320
  }),
1146
1321
  path: (options) => ({
1147
1322
  mode: "path",
1323
+ ...resolveCameraClipping(options),
1148
1324
  from: options.from,
1149
1325
  to: options.to,
1150
1326
  target: options.target ?? [0, 0.8, 0],
@@ -1155,6 +1331,7 @@ export const camera = {
1155
1331
  }),
1156
1332
  flythrough: (options = {}) => ({
1157
1333
  mode: "flythrough",
1334
+ ...resolveCameraClipping(options),
1158
1335
  from: options.from ?? [0, 0.36, 1.6],
1159
1336
  to: options.to ?? [0, 0.36, -4.4],
1160
1337
  target: options.target ?? [0, 0.28, -5.8],
@@ -1175,6 +1352,7 @@ export const camera = {
1175
1352
  */
1176
1353
  orthographic: (options = {}) => ({
1177
1354
  mode: "orthographic",
1355
+ ...resolveCameraClipping(options),
1178
1356
  position: options.position ?? [0, 1.4, 4],
1179
1357
  target: options.target ?? [0, 0.8, 0],
1180
1358
  orthographicSize: options.orthographicSize ?? 1.4
@@ -1197,6 +1375,7 @@ export const camera = {
1197
1375
  const horizontal = Math.cos(elevation) * distance;
1198
1376
  return {
1199
1377
  mode: "isometric",
1378
+ ...resolveCameraClipping(options),
1200
1379
  target,
1201
1380
  distance,
1202
1381
  orthographicSize: options.orthographicSize ?? 6,
@@ -1257,8 +1436,10 @@ export const camera = {
1257
1436
  solar: () => camera.orbit({ target: [0, 0, 0], distance: 7.2, fov: 46 }),
1258
1437
  humanoid: () => camera.perspective({ position: [1.2, 1.12, 3.45], target: [0, 0.78, -0.55], fov: 36 }),
1259
1438
  miniGolf: () => camera.follow({ targetNode: "white physics golf ball", distance: 4.2, fov: 48 }),
1260
- neon: () => camera.flythrough({ from: [0, 0.36, 1.6], to: [0, 0.36, -5.8], target: [0, 0.26, -6.8], fov: 54, captureTime: 0.16 })
1439
+ neon: () => camera.flythrough({ from: [0, 0.36, 1.6], to: [0, 0.36, -5.8], target: [0, 0.26, -6.8], fov: 54, captureTime: 0.16 }),
1440
+ ...gameCameraRigs
1261
1441
  };
1442
+ export const gameFeel = { create: gameFeelBuilders.create, hitStopDefaults: gameFeelBuilders.hitStopDefaults };
1262
1443
  function resolveFrameAssetRenderScale(bounds, options) {
1263
1444
  const size = boundsSize(bounds);
1264
1445
  const height = Math.max(0.001, boundsHeight(bounds));
@@ -1314,7 +1495,10 @@ export const effects = {
1314
1495
  radius: options.radius ?? 0.38,
1315
1496
  threshold: options.threshold ?? 0.7,
1316
1497
  antiBlowout,
1317
- maxIntensity
1498
+ maxIntensity,
1499
+ ...(options.quality !== undefined ? { quality: options.quality } : {}),
1500
+ ...(options.softKnee !== undefined ? { softKnee: options.softKnee } : {}),
1501
+ ...(options.shoulder !== undefined ? { shoulder: options.shoulder } : {})
1318
1502
  });
1319
1503
  },
1320
1504
  cinematicBloom: (options = {}) => effects.bloom({
@@ -1323,7 +1507,10 @@ export const effects = {
1323
1507
  radius: options.radius ?? 0.42,
1324
1508
  threshold: options.threshold ?? 0.72,
1325
1509
  antiBlowout: options.antiBlowout ?? true,
1326
- maxIntensity: options.maxIntensity ?? 0.92
1510
+ maxIntensity: options.maxIntensity ?? 0.92,
1511
+ ...(options.quality !== undefined ? { quality: options.quality } : {}),
1512
+ ...(options.softKnee !== undefined ? { softKnee: options.softKnee } : {}),
1513
+ ...(options.shoulder !== undefined ? { shoulder: options.shoulder } : {})
1327
1514
  }),
1328
1515
  neonBloom: (options = {}) => effects.bloom({
1329
1516
  intensity: options.intensity ?? 0.72,
@@ -1331,12 +1518,27 @@ export const effects = {
1331
1518
  radius: options.radius ?? 0.48,
1332
1519
  threshold: options.threshold ?? 0.68,
1333
1520
  antiBlowout: options.antiBlowout ?? true,
1334
- maxIntensity: options.maxIntensity ?? 0.92
1521
+ maxIntensity: options.maxIntensity ?? 0.92,
1522
+ ...(options.quality !== undefined ? { quality: options.quality } : {}),
1523
+ ...(options.softKnee !== undefined ? { softKnee: options.softKnee } : {}),
1524
+ ...(options.shoulder !== undefined ? { shoulder: options.shoulder } : {})
1335
1525
  }),
1336
- volumetricFog: (options = {}) => effects.fog({
1526
+ /**
1527
+ * A5 volumetric fog (muse3jsparity-PRD): builds a DISTINCT "volumetric-fog"
1528
+ * node (never plain fog) that submits the depth-aware inscatter pass plus
1529
+ * forward GPU height-fog terms. quality "off" keeps forward exp2 fog only.
1530
+ */
1531
+ volumetricFog: (options = {}) => new AuraNodeBuilder({
1532
+ kind: "effect",
1533
+ effect: "volumetric-fog",
1534
+ name: options.name ?? "volumetric fog inscatter",
1337
1535
  density: options.density ?? 0.18,
1338
1536
  color: options.color ?? "#6f84b9",
1339
- intensity: options.intensity ?? 0.7
1537
+ intensity: options.intensity ?? 0.7,
1538
+ ...(options.volumetricQuality ? { volumetricQuality: options.volumetricQuality } : {}),
1539
+ ...(options.lightPosition ? { lightPosition: options.lightPosition } : {}),
1540
+ ...(options.heightFalloff !== undefined ? { heightFalloff: options.heightFalloff } : {}),
1541
+ ...(options.heightReference !== undefined ? { heightReference: options.heightReference } : {})
1340
1542
  }),
1341
1543
  depthFog: (options = {}) => effects.fog({
1342
1544
  density: options.density ?? 0.14,
@@ -1361,6 +1563,82 @@ export const effects = {
1361
1563
  density: options.density ?? 0.7,
1362
1564
  color: options.color ?? "#020617"
1363
1565
  }),
1566
+ /**
1567
+ * Root color-grade node (muse3jsparity-PRD A3). contrast/saturation execute
1568
+ * natively; exposure/shadows/highlights/lut are recorded on the node and
1569
+ * warned (no native grade target yet — never silently accepted).
1570
+ */
1571
+ colorGrade: (options = {}) => new AuraNodeBuilder({
1572
+ kind: "effect",
1573
+ effect: "color-grade",
1574
+ name: options.name ?? "color grade",
1575
+ intensity: options.intensity ?? 1,
1576
+ exposure: options.exposure ?? 1,
1577
+ contrast: options.contrast ?? 1,
1578
+ saturation: options.saturation ?? 1,
1579
+ ...(options.shadows !== undefined ? { shadows: options.shadows } : {}),
1580
+ ...(options.highlights !== undefined ? { highlights: options.highlights } : {}),
1581
+ ...(options.lut !== undefined ? { lut: options.lut } : {})
1582
+ }),
1583
+ /**
1584
+ * Root anti-alias node (muse3jsparity-PRD A3). `fxaa` executes natively;
1585
+ * `off` submits nothing; `taa` uses renderer-owned velocity/history for opaque
1586
+ * rigid triangles. Unsupported deforming/transparent geometry emits a named warning.
1587
+ */
1588
+ antiAlias: (options = {}) => new AuraNodeBuilder({
1589
+ kind: "effect",
1590
+ effect: "anti-alias",
1591
+ name: options.name ?? "anti alias",
1592
+ mode: options.mode ?? "fxaa",
1593
+ intensity: options.intensity ?? 1
1594
+ }),
1595
+ /**
1596
+ * Root outline node (muse3jsparity-PRD A3): native outline pass, width in
1597
+ * pixels clamped to the device range 1-6 by the bridge.
1598
+ */
1599
+ outline: (options = {}) => new AuraNodeBuilder({
1600
+ kind: "effect",
1601
+ effect: "outline",
1602
+ name: options.name ?? "outline",
1603
+ color: options.color ?? "#ff9822",
1604
+ width: options.width ?? 3,
1605
+ threshold: options.threshold ?? 0.12,
1606
+ intensity: options.intensity ?? 0.9
1607
+ }),
1608
+ /**
1609
+ * Root screen-space reflections node (muse3jsparity-PRD A3): submits the
1610
+ * native SSR pass against renderer-owned depth where available.
1611
+ */
1612
+ screenSpaceReflections: (options = {}) => new AuraNodeBuilder({
1613
+ kind: "effect",
1614
+ effect: "screen-space-reflections",
1615
+ name: options.name ?? "screen space reflections",
1616
+ intensity: options.intensity ?? 0.9
1617
+ }),
1618
+ /**
1619
+ * Root depth-of-field node (muse3jsparity-PRD A3). focus is a
1620
+ * linear-distance fraction (0 = near plane, 1 = far plane); aperture widens
1621
+ * the focus transition band (game-tuned control, not a physical f-stop).
1622
+ */
1623
+ depthOfField: (options = {}) => new AuraNodeBuilder({
1624
+ kind: "effect",
1625
+ effect: "depth-of-field",
1626
+ name: options.name ?? "depth of field",
1627
+ focus: options.focus ?? 0.02,
1628
+ aperture: options.aperture ?? 0.35,
1629
+ maxBlur: options.maxBlur ?? 4,
1630
+ intensity: options.intensity ?? 1
1631
+ }),
1632
+ /**
1633
+ * Root motion-blur node: renderer-owned GPU velocity for opaque rigid geometry.
1634
+ * Unsupported deforming/transparent/instanced geometry emits a named warning.
1635
+ */
1636
+ motionBlur: (options = {}) => new AuraNodeBuilder({
1637
+ kind: "effect",
1638
+ effect: "motion-blur",
1639
+ name: options.name ?? "motion blur",
1640
+ intensity: options.intensity ?? 0.5
1641
+ }),
1364
1642
  rain: (options = {}) => new AuraNodeBuilder({
1365
1643
  kind: "effect",
1366
1644
  effect: "rain",
@@ -1373,6 +1651,62 @@ export const effects = {
1373
1651
  splashes: options.splashes ?? true,
1374
1652
  mist: options.mist ?? true
1375
1653
  }),
1654
+ snow: (options = {}) => new AuraNodeBuilder({
1655
+ kind: "effect",
1656
+ effect: "snow",
1657
+ intensity: options.intensity ?? 0.4,
1658
+ density: options.density ?? 0.68,
1659
+ color: options.color ?? "#e8f1ff",
1660
+ speed: options.speed ?? 1,
1661
+ wind: options.wind ?? [-0.85, -1.1, -0.22],
1662
+ particleCount: options.particleCount,
1663
+ splashes: options.splashes ?? false,
1664
+ mist: options.mist ?? true
1665
+ }),
1666
+ /**
1667
+ * Flipbook explosion/muzzle-flash sprite sheet (muse3jsparity-PRD D4):
1668
+ * recorded with validated sheet geometry, but withheld — root has no
1669
+ * native sprite-sheet sampler yet, so no pass is submitted.
1670
+ */
1671
+ flipbook: (options = {}) => {
1672
+ const columns = options.spriteColumns ?? 4;
1673
+ const rows = options.spriteRows ?? 4;
1674
+ resolveFlipbookUv(0, columns, rows);
1675
+ return new AuraNodeBuilder({
1676
+ kind: "effect",
1677
+ effect: "flipbook-sprite",
1678
+ name: options.name ?? "flipbook explosion sprite",
1679
+ intensity: options.intensity ?? 1,
1680
+ color: options.color ?? "#ffb347",
1681
+ spriteColumns: columns,
1682
+ spriteRows: rows,
1683
+ frameRate: options.frameRate ?? 24
1684
+ });
1685
+ },
1686
+ /**
1687
+ * Additive thick light beam / fence strip (muse3jsparity-PRD D4): recorded
1688
+ * with a validated quad-strip descriptor, but withheld — root has no native
1689
+ * beam target yet, so no pass is submitted.
1690
+ */
1691
+ beam: (options = {}) => {
1692
+ const descriptor = createBeamDescriptor({
1693
+ from: options.from ?? [0, 1, 0],
1694
+ to: options.to ?? [0, 1, -4],
1695
+ ...(options.widthWorld !== undefined ? { widthWorld: options.widthWorld } : {}),
1696
+ ...(options.segmentCount !== undefined ? { segmentCount: options.segmentCount } : {})
1697
+ });
1698
+ return new AuraNodeBuilder({
1699
+ kind: "effect",
1700
+ effect: "light-beam",
1701
+ name: options.name ?? "additive light beam",
1702
+ intensity: options.intensity ?? 0.9,
1703
+ color: options.color ?? "#9fd8ff",
1704
+ from: [...descriptor.from],
1705
+ to: [...descriptor.to],
1706
+ widthWorld: descriptor.widthWorld,
1707
+ segmentCount: descriptor.segmentCount
1708
+ });
1709
+ },
1376
1710
  particles: (options = {}) => new AuraNodeBuilder({
1377
1711
  kind: "effect",
1378
1712
  effect: "particles",
@@ -1398,6 +1732,161 @@ export const effects = {
1398
1732
  noise: options.noise ?? 0.22
1399
1733
  })
1400
1734
  };
1735
+ /**
1736
+ * D3 atmosphere builders (PRD D3 boxes 1-2). ADDITIVE root surface over the
1737
+ * pure descriptors in `@aura3d/rendering` (`DayNightSky.ts`,
1738
+ * `AtmosphereWetness.ts`, `Weather.ts`).
1739
+ *
1740
+ * Every builder below composes pre-existing node kinds only (primitive,
1741
+ * light, effect); no existing builder is modified. Precipitation pixels in
1742
+ * the default production path come from weather-state-driven primitive
1743
+ * streaks/flakes, because the production bridge does not pixel-back `rain`,
1744
+ * `snow`, or `particles` effect passes (they render only in the safe-basic
1745
+ * fallback and the Canvas2D diagnostic path). The matching effect node is
1746
+ * still declared so diagnostics report the request.
1747
+ */
1748
+ export const sky = {
1749
+ /** Time-of-day sky: background key, sun/moon discs, stars, 2D-noise clouds, key light. */
1750
+ dayNight: (options = {}) => {
1751
+ const state = createDayNightSky(options);
1752
+ const nodes = [];
1753
+ const sunUp = state.sun.elevationRadians > -0.05;
1754
+ const moonUp = state.moon.elevationRadians > -0.05;
1755
+ if (sunUp) {
1756
+ nodes.push(primitives.sphere({ name: "d3 sun disc", material: material.emissive({ color: state.sun.color, emissive: state.sun.color, emissiveIntensity: 2.2, roughness: 0.8 }) })
1757
+ .position(-6 * Math.cos(state.sun.azimuthRadians), 1 + 5 * Math.sin(state.sun.azimuthRadians), -7).scale(0.85).toJSON());
1758
+ }
1759
+ if (moonUp) {
1760
+ nodes.push(primitives.sphere({ name: "d3 moon disc", material: material.emissive({ color: state.moon.color, emissive: state.moon.color, emissiveIntensity: 1.4, roughness: 0.8 }) })
1761
+ .position(-6 * Math.cos(state.moon.azimuthRadians), 1 + 5 * Math.sin(state.moon.azimuthRadians), -7).scale(0.6).toJSON());
1762
+ }
1763
+ const starLimit = Math.max(0, Math.min(120, options.starLimit ?? 48));
1764
+ const visibleStars = state.stars.filter((star) => star.brightness > 0.05 && star.y > 0).slice(0, starLimit);
1765
+ for (const [index, star] of visibleStars.entries()) {
1766
+ nodes.push(primitives.sphere({ name: `d3 night star ${index}`, material: material.emissive({ color: "#dbeafe", emissive: "#bfdbfe", emissiveIntensity: 1.8, roughness: 0.9 }) })
1767
+ .position(star.x * 7, star.y * 4 + 0.6, -6.5).scale(0.028 + star.size * 2).toJSON());
1768
+ }
1769
+ const cloudLimit = Math.max(0, Math.min(48, options.cloudLimit ?? 12));
1770
+ const cloudTint = state.dayFactor > 0.5 ? "#f1f5f9" : state.dayFactor > 0.2 ? "#b6a6a6" : "#1e293b";
1771
+ for (const [index, cell] of state.clouds.slice(0, cloudLimit).entries()) {
1772
+ nodes.push(primitives.sphere({ name: `d3 noise cloud ${index}`, material: material.pbr({ color: cloudTint, roughness: 1, metallic: 0 }) })
1773
+ .position(cell.x * 6, cell.y * 3 + 1.4, -5.5).scale([cell.radius * 1.7, cell.radius * 0.5, cell.radius * 0.8]).toJSON());
1774
+ }
1775
+ nodes.push(lights.directional({
1776
+ name: sunUp ? "d3 sun key light" : "d3 moon key light",
1777
+ position: sunUp ? [-4, 5, 2] : [3, 4, -1],
1778
+ color: sunUp ? state.sun.color : state.moon.color,
1779
+ intensity: sunUp ? 0.6 + state.dayFactor * 1.2 : 0.35
1780
+ }).toJSON());
1781
+ return { nodes, background: state.dayFactor > 0.5 ? state.horizonColor : state.nightFactor > 0.6 ? "#020617" : state.horizonColor, dayFactor: state.dayFactor, visibleStarCount: visibleStars.length };
1782
+ }
1783
+ };
1784
+ export const weather = {
1785
+ /** Rain/snow declaration + weather-state-driven primitive streaks/flakes for production-path pixels. */
1786
+ precipitation: (options) => {
1787
+ const state = createWeatherState({
1788
+ type: options.type,
1789
+ seed: options.seed ?? 0xd3e7,
1790
+ elapsedSeconds: options.elapsedSeconds ?? 1.2,
1791
+ maxVisualDrops: 400
1792
+ });
1793
+ const snowing = options.type === "snow";
1794
+ const intensity = snowing ? Math.max(0.2, state.snowIntensity) : Math.max(0.2, state.rainIntensity);
1795
+ const nodes = [
1796
+ (snowing ? effects.snow({ intensity, color: "#e8f1ff" }) : effects.rain({ intensity, color: "#bcd7ff" })).toJSON()
1797
+ ];
1798
+ const limit = Math.max(0, Math.min(160, options.streakLimit ?? (snowing ? 70 : 90)));
1799
+ const drops = state.visualDrops.slice(0, limit);
1800
+ drops.forEach((drop, index) => {
1801
+ const x = Math.max(-3.4, Math.min(3.4, drop.x * 2.4));
1802
+ const y = Math.max(0.15, Math.min(3.4, drop.y + 1.6));
1803
+ const z = Math.max(-4.5, Math.min(2.5, drop.z * 2.4));
1804
+ if (snowing) {
1805
+ nodes.push(primitives.sphere({ name: `d3 snow flake ${index}`, material: material.pbr({ color: "#eef4ff", roughness: 0.85, metallic: 0 }) })
1806
+ .position(x, y, z).scale(0.035).toJSON());
1807
+ }
1808
+ else {
1809
+ nodes.push(primitives.box({ name: `d3 rain streak ${index}`, material: material.pbr({ color: "#bcd7ff", roughness: 0.35, metallic: 0 }) })
1810
+ .position(x, y, z).scale([0.014, Math.max(0.08, drop.length * 2.4), 0.014]).toJSON());
1811
+ }
1812
+ });
1813
+ return { nodes, dropCount: drops.length, wetness: state.wetness };
1814
+ },
1815
+ /** Ground slab with wetness darkening + puddle discs mapped from WeatherPuddlePatch. */
1816
+ wetGround: (options = {}) => {
1817
+ const probe = describeWetMaterial({
1818
+ type: options.type ?? "rain",
1819
+ dryColor: options.dryColor ?? "#5b6b4f",
1820
+ dryRoughness: options.dryRoughness ?? 0.9,
1821
+ seed: options.seed ?? 0xd3e7
1822
+ });
1823
+ const size = Math.max(2, Math.min(14, options.size ?? 8));
1824
+ const nodes = [
1825
+ primitives.box({ name: "d3 weather ground slab", material: material.pbr({ color: probe.response.albedoColor, roughness: probe.response.roughness, metallic: 0 }) })
1826
+ .position(0, -0.02, 0).scale([size, 0.04, size]).toJSON()
1827
+ ];
1828
+ probe.weather.puddlePatches.forEach((patch, index) => {
1829
+ nodes.push(primitives.cylinder({ name: `d3 puddle disc ${index}`, material: material.pbr({ color: "#16283a", roughness: 0.05, metallic: 0.1 }) })
1830
+ .position(patch.x * 2.4, 0.005, patch.z * 2.4).scale([Math.max(0.06, patch.radius * 3), 0.006, Math.max(0.06, patch.radius * 3)]).toJSON());
1831
+ });
1832
+ return {
1833
+ nodes,
1834
+ wetness: probe.response.wetness,
1835
+ albedoColor: probe.response.albedoColor,
1836
+ roughness: probe.response.roughness,
1837
+ puddleCount: probe.weather.puddlePatches.length
1838
+ };
1839
+ },
1840
+ /** Lightning-flash light hook: intensity is 0 unless type is thunderstorm. */
1841
+ lightning: (options = { type: "thunderstorm" }) => {
1842
+ const probe = describeWetMaterial({ type: options.type, elapsedSeconds: options.elapsedSeconds ?? 0.4, seed: options.seed ?? 0xd3e7 });
1843
+ return {
1844
+ nodes: probe.flash.intensity > 0
1845
+ ? [lights.directional({ name: "d3 lightning flash", color: "#dbeafe", intensity: 1 + probe.flash.intensity * 4, position: [2, 6, -3] }).toJSON()]
1846
+ : [],
1847
+ intensity: probe.flash.intensity
1848
+ };
1849
+ }
1850
+ };
1851
+ /**
1852
+ * D3 water surface builder (PRD D3 box 3). ADDITIVE root surface over
1853
+ * `createWaterSurface` in `@aura3d/rendering`.
1854
+ *
1855
+ * Rendered material = layered opaque depth-tinted bands (bounded refraction
1856
+ * look) + fresnel-baked sky tint + white shore-foam discs mapped from
1857
+ * `OceanFoamPatch` + boat + fading wake trail. Buoyancy queries stay on the
1858
+ * fixture (`sampleOceanFixture().buoyancy`); this builder creates no planar
1859
+ * reflection/refraction targets (B4 dependency, see
1860
+ * WATER_SURFACE_PLANAR_DEPENDENCY).
1861
+ */
1862
+ export const water = {
1863
+ surface: (options = {}) => {
1864
+ const state = createWaterSurface({ preset: options.preset, seed: options.seed, boat: options.boat });
1865
+ const nodes = [];
1866
+ const bandSpan = 4 / state.bands.length;
1867
+ state.bands.forEach((band, index) => {
1868
+ nodes.push(primitives.box({ name: `d3 water depth band ${index}`, material: material.pbr({ color: band.color, roughness: 0.12, metallic: 0.05 }) })
1869
+ .position(0, 0.02 + index * 0.0012, -4.5 + bandSpan * (index + 0.5)).scale([9, 0.024, bandSpan + 0.01]).toJSON());
1870
+ });
1871
+ nodes.push(primitives.box({ name: "d3 shoreline sand", material: material.pbr({ color: "#cbb37f", roughness: 0.95, metallic: 0 }) })
1872
+ .position(0, 0, 0.9).scale([9, 0.03, 2.6]).toJSON());
1873
+ for (const [index, patch] of state.foam.entries()) {
1874
+ nodes.push(primitives.sphere({ name: `d3 shore foam ${index}`, material: material.pbr({ color: "#f4fafd", roughness: 0.55, metallic: 0 }) })
1875
+ .position(Math.max(-4.2, Math.min(4.2, patch.x * 3)), 0.045, -0.42 + (index % 3) * 0.12).scale([Math.max(0.08, patch.radius * 6), 0.02, Math.max(0.05, patch.radius * 3)]).toJSON());
1876
+ }
1877
+ if (options.withBoatHull !== false && options.boat) {
1878
+ nodes.push(primitives.box({ name: "d3 wake boat hull", material: material.pbr({ color: "#7c3f21", roughness: 0.7, metallic: 0 }) })
1879
+ .position(options.boat.x ?? 0, 0.12, options.boat.z ?? -2).scale([0.34, 0.16, 0.9]).toJSON());
1880
+ }
1881
+ state.wake.forEach((segment, index) => {
1882
+ nodes.push(primitives.box({ name: `d3 boat wake ${index}`, material: material.pbr({ color: "#cfeaf7", roughness: 0.3, metallic: 0 }) })
1883
+ .position(segment.x * 2, 0.038, segment.z - 1.2).scale([segment.width, 0.012, 0.3]).toJSON());
1884
+ });
1885
+ return { nodes, bandCount: state.bands.length, foamCount: state.foam.length, wakeActive: state.wakeActive, wakeSegmentCount: state.wake.length };
1886
+ },
1887
+ /** Fixture-side buoyancy telemetry passthrough (no physics implemented here). */
1888
+ buoyancy: (options = {}) => sampleOceanFixture({ preset: options.preset ?? "moderate", seed: options.seed ?? 0xaa7e5 }).buoyancy
1889
+ };
1401
1890
  export const interactions = {
1402
1891
  orbit: (options = {}) => new AuraNodeBuilder({
1403
1892
  kind: "interaction",
@@ -1648,7 +2137,24 @@ export const environments = {
1648
2137
  if (materialClass === "product")
1649
2138
  return environments.productHero();
1650
2139
  return environments.studio();
1651
- }
2140
+ },
2141
+ /**
2142
+ * B3 root HDRI environment (muse3jsparity-PRD). The `.hdr` asset resolves
2143
+ * post-mount: first frames render the honest studio procedural fallback,
2144
+ * then the HDR→cubemap→GGX-prefilter→BRDF-LUT chain swaps in and
2145
+ * `iblPixelBacked` flips true in diagnostics. Fetch/parse failures keep the
2146
+ * fallback and warn — never a black scene, never a silent swap.
2147
+ */
2148
+ hdri: (options) => new AuraNodeBuilder({
2149
+ kind: "environment",
2150
+ environment: "hdri",
2151
+ name: options.name ?? "hdri ibl environment",
2152
+ intensity: options.intensity ?? 1,
2153
+ texture: options.texture,
2154
+ ...(options.reflectionTexture ? { reflectionTexture: options.reflectionTexture } : {}),
2155
+ ...(options.color ? { color: options.color } : {}),
2156
+ ...(options.rotation !== undefined ? { rotation: options.rotation } : {})
2157
+ })
1652
2158
  };
1653
2159
  const rendererColorManagementPreset = {
1654
2160
  kind: "aura-renderer-color-management",
@@ -1774,6 +2280,7 @@ function normalizeCreateAppRendererOptions(options) {
1774
2280
  mode,
1775
2281
  fallback: options?.fallback ?? "safe-basic",
1776
2282
  qualityProfile: profile.id,
2283
+ textureBudgetBytes: normalizeTextureBudgetBytes(options?.textureBudgetBytes),
1777
2284
  profile
1778
2285
  };
1779
2286
  }
@@ -1817,17 +2324,42 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
1817
2324
  const sceneCategory = resolveRendererSceneCategory(snapshot, names);
1818
2325
  const exposure = sceneExposurePresets[sceneCategory];
1819
2326
  const bloom = flattened.find((node) => node.kind === "effect" && node.effect === "bloom");
1820
- const fog = flattened.find((node) => node.kind === "effect" && node.effect === "fog");
2327
+ const fog = flattened.find((node) => node.kind === "effect" && (node.effect === "fog" || node.effect === "volumetric-fog"));
1821
2328
  const environment = flattened.find((node) => node.kind === "environment");
1822
2329
  const contactShadows = names.filter((name) => name.includes("contact shadow") || name.includes("footprint") || name.includes("glow pool")).length;
1823
2330
  const ambientOcclusion = flattened.some((node) => node.kind === "effect" && node.effect === "ambient-occlusion");
1824
2331
  const contactOcclusion = flattened.some((node) => node.kind === "effect" && node.effect === "contact-occlusion") || contactShadows > 0;
2332
+ // Authored temporal intent is separate from device-observed execution.
2333
+ // Missing submissions remain visible without claiming an unmounted scene rendered.
2334
+ const colorGradeNode = flattened.find((node) => node.kind === "effect" && node.effect === "color-grade");
2335
+ const antiAliasNode = flattened.find((node) => node.kind === "effect" && node.effect === "anti-alias");
2336
+ const outlineNode = flattened.find((node) => node.kind === "effect" && node.effect === "outline");
2337
+ const ssrNode = flattened.find((node) => node.kind === "effect" && node.effect === "screen-space-reflections");
2338
+ const dofNode = flattened.find((node) => node.kind === "effect" && node.effect === "depth-of-field");
2339
+ const motionBlurNode = flattened.find((node) => node.kind === "effect" && node.effect === "motion-blur");
2340
+ const flipbookNode = flattened.find((node) => node.kind === "effect" && node.effect === "flipbook-sprite");
2341
+ const beamNode = flattened.find((node) => node.kind === "effect" && node.effect === "light-beam");
2342
+ const sdfTextRequested = flattened.some((node) => node.kind === "primitive" && node.text3D?.backend === "sdf");
2343
+ const fxaaRequested = (antiAliasNode?.mode ?? "fxaa") === "fxaa" && Boolean(antiAliasNode);
2344
+ const taaRequested = (antiAliasNode?.mode ?? "fxaa") === "taa";
2345
+ const taaWithheld = taaRequested && Boolean(runtime?.mounted) && !(runtime?.postprocess?.actualPasses ?? []).includes("taa");
2346
+ const motionBlurWithheld = Boolean(motionBlurNode) && Boolean(runtime?.mounted) && !(runtime?.postprocess?.actualPasses ?? []).includes("motion-blur");
1825
2347
  const runtimePostprocess = runtime?.postprocess;
1826
2348
  const runtimePasses = runtimePostprocess?.actualPasses ?? [];
1827
- const postprocessRequested = Boolean(bloom) || ambientOcclusion || contactOcclusion || runtimePasses.length > 0;
2349
+ const postprocessRequested = Boolean(bloom) || ambientOcclusion || contactOcclusion
2350
+ || Boolean(colorGradeNode) || Boolean(antiAliasNode) || Boolean(outlineNode)
2351
+ || Boolean(ssrNode) || Boolean(dofNode) || Boolean(motionBlurNode) || runtimePasses.length > 0;
1828
2352
  const requestedPasses = runtimePasses.length > 0
1829
2353
  ? runtimePasses
1830
- : requestedRendererPostProcessPasses(Boolean(bloom), ambientOcclusion, contactOcclusion);
2354
+ : requestedRendererPostProcessPasses(Boolean(bloom), ambientOcclusion, contactOcclusion, {
2355
+ colorGrade: Boolean(colorGradeNode),
2356
+ fxaa: fxaaRequested,
2357
+ outline: Boolean(outlineNode),
2358
+ ssr: Boolean(ssrNode),
2359
+ depthOfField: Boolean(dofNode),
2360
+ motionBlurWithheld: Boolean(motionBlurNode),
2361
+ taaWithheld: taaRequested
2362
+ });
1831
2363
  const runtimeStatus = !postprocessRequested
1832
2364
  ? "disabled"
1833
2365
  : runtime?.mounted
@@ -1842,6 +2374,28 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
1842
2374
  warnings.push("scene has no contact shadow or contact-occlusion grounding cue");
1843
2375
  if (bloom && (bloom.intensity ?? 0) > 0.95 && bloom.antiBlowout !== true)
1844
2376
  warnings.push("bloom is high without anti-blowout safeguards");
2377
+ if (motionBlurWithheld)
2378
+ warnings.push("TEMPORAL_INPUTS_UNAVAILABLE: motion-blur has no native submission");
2379
+ if (flipbookNode)
2380
+ warnings.push("flipbook-sprite is recorded but withheld: root has no native sprite-sheet sampler yet, so no flipbook pass is submitted");
2381
+ if (beamNode)
2382
+ warnings.push("light-beam is recorded but withheld: root has no native beam target yet, so no beam pass is submitted");
2383
+ // G1: the baked-SDF sampler mounts synchronously on the production bridge.
2384
+ // Warn only when the SDF request cannot be pixel-backed (non-production
2385
+ // mount, or the sampler fell back to the extruded mesh at runtime).
2386
+ if (sdfTextRequested && (!runtime?.mounted || !(runtime.text?.textPixelBacked ?? false))) {
2387
+ warnings.push(runtime?.mounted
2388
+ ? `text3D sdf backend fell back to the extruded mesh (${runtime.text?.reason ?? "sampler failed"}); textPixelBacked stays false`
2389
+ : "text3D sdf backend is recorded but unmounted: the production bridge samples the SDF atlas at mount, so textPixelBacked stays false until render");
2390
+ }
2391
+ if (taaWithheld)
2392
+ warnings.push("TEMPORAL_INPUTS_UNAVAILABLE: taa has no native submission");
2393
+ if (colorGradeNode && (colorGradeNode.exposure ?? 1) !== 1)
2394
+ warnings.push("color-grade exposure is recorded but has no native grade target yet; contrast/saturation execute");
2395
+ if (colorGradeNode && (colorGradeNode.shadows !== undefined || colorGradeNode.highlights !== undefined))
2396
+ warnings.push("color-grade shadows/highlights are recorded but have no native grade target yet; contrast/saturation execute");
2397
+ if (colorGradeNode?.lut !== undefined)
2398
+ warnings.push("color-grade lut is recorded but LUT samplers are not bound yet; the lut is ignored");
1845
2399
  if (postprocessRequested && !runtime?.mounted)
1846
2400
  warnings.push("renderer diagnostics are a scene plan only; call createAuraApp(...).diagnostics() after the first render for pixel-backed pass status");
1847
2401
  if (postprocessRequested && runtime?.mounted && !runtimePostprocess?.pixelBacked)
@@ -1891,7 +2445,8 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
1891
2445
  intensity: bloom?.intensity ?? 0,
1892
2446
  threshold: bloom?.threshold ?? 0,
1893
2447
  radius: bloom?.radius ?? 0,
1894
- antiBlowout: bloom?.antiBlowout ?? true
2448
+ antiBlowout: bloom?.antiBlowout ?? true,
2449
+ ...(bloom?.quality !== undefined ? { quality: bloom.quality } : {})
1895
2450
  },
1896
2451
  shadows: {
1897
2452
  // Never a source-authored `true`. When the runtime is mounted this is the
@@ -1905,8 +2460,10 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
1905
2460
  label: runtime?.shadow?.label,
1906
2461
  nativeShadowMapBindings: runtime?.shadow?.nativeShadowMapBindings ?? 0,
1907
2462
  shadowRenderTargetsAllocated: runtime?.shadow?.shadowRenderTargetsAllocated ?? 0,
2463
+ observed: runtime?.mounted ? runtime.shadow?.observed ?? null : null,
1908
2464
  contactShadows,
1909
- mapType: "pcf-soft"
2465
+ mapType: "pcf-soft",
2466
+ ...(runtime?.shadow?.spot === undefined ? {} : { spot: runtime.shadow.spot })
1910
2467
  },
1911
2468
  occlusion: {
1912
2469
  enabled: ambientOcclusion || contactOcclusion,
@@ -1921,7 +2478,9 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
1921
2478
  fog: {
1922
2479
  enabled: Boolean(fog),
1923
2480
  density: fog?.density ?? 0,
1924
- preset: fog ? ((fog.intensity ?? 0) > 0.65 ? "volumetric" : "depth") : "none"
2481
+ // A5: a volumetric-fog node labels "volumetric" because it submits the
2482
+ // distinct inscatter pass; plain fog keeps the legacy intensity label.
2483
+ preset: fog ? (fog.effect === "volumetric-fog" || (fog.intensity ?? 0) > 0.65 ? "volumetric" : "depth") : "none"
1925
2484
  },
1926
2485
  postprocess: {
1927
2486
  enabled: runtimePostprocess?.pixelBacked ?? false,
@@ -1937,6 +2496,7 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
1937
2496
  actualPasses: runtimePostprocess?.actualPasses ?? [],
1938
2497
  fallbackPasses: runtimePostprocess?.fallbackPasses ?? [],
1939
2498
  targetFormat: runtimePostprocess?.targetFormat,
2499
+ executionMode: runtimePostprocess?.executionMode ?? "unknown",
1940
2500
  evidence: postprocessEvidence
1941
2501
  },
1942
2502
  runtime: {
@@ -1946,17 +2506,45 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
1946
2506
  passNames: runtimePostprocess?.actualPasses ?? [],
1947
2507
  warnings: runtime?.warnings ?? [],
1948
2508
  nativeInstancedSubmissions: runtime?.deviceDiagnostics?.nativeInstancedSubmissions ?? 0,
2509
+ nativeTemporalPasses: runtime?.deviceDiagnostics?.nativeTemporalPasses ?? 0,
2510
+ nativeTemporalBindings: runtime?.deviceDiagnostics?.nativeTemporalBindings ?? 0,
1949
2511
  submittedObjects: runtime?.deviceDiagnostics?.submittedObjects ?? 0,
1950
2512
  visibleObjects: runtime?.deviceDiagnostics?.visibleObjects ?? 0,
1951
2513
  culledObjects: runtime?.deviceDiagnostics?.culledObjects ?? 0,
1952
2514
  frustumTestedObjects: runtime?.deviceDiagnostics?.frustumTestedObjects ?? 0,
1953
- lodSelections: runtime?.lodSelections ?? []
2515
+ bloom: runtime?.deviceDiagnostics?.bloom ?? null,
2516
+ samplerAnisotropyUploads: runtime?.deviceDiagnostics?.samplerAnisotropyUploads ?? 0,
2517
+ maxTextureAnisotropy: runtime?.deviceDiagnostics?.maxTextureAnisotropy ?? 1,
2518
+ lodSelections: runtime?.lodSelections ?? [],
2519
+ texturedMaterials: runtime?.texturedMaterials ?? []
1954
2520
  },
1955
2521
  environment: {
1956
2522
  enabled: environmentStatus.enabled,
1957
2523
  preset: environmentStatus.preset,
1958
2524
  intensity: environmentStatus.intensity,
1959
- evidence: environmentStatus.evidence
2525
+ evidence: environmentStatus.evidence,
2526
+ // B3: pass through the HDRI upgrade observation; the unmounted default
2527
+ // reports not-pixel-backed so static diagnostics never claim HDRI proof.
2528
+ iblPixelBacked: environmentStatus.iblPixelBacked ?? false,
2529
+ hdriStatus: environmentStatus.hdriStatus ?? "none",
2530
+ ...(environmentStatus.dualProbe === undefined ? {} : { dualProbe: environmentStatus.dualProbe }),
2531
+ ...(environmentStatus.hdriRotation === undefined ? {} : { hdriRotation: environmentStatus.hdriRotation })
2532
+ },
2533
+ text: runtime?.text ?? {
2534
+ sdfTexts: flattened.filter((node) => node.kind === "primitive" && node.text3D?.backend === "sdf").length,
2535
+ textPixelBacked: false,
2536
+ quadCount: 0,
2537
+ lastOpacity: 1,
2538
+ reason: "SDF text requested in the scene graph but no runtime has mounted yet"
2539
+ },
2540
+ textures: runtime?.textures ?? {
2541
+ budgetBytes: normalizeTextureBudgetBytes(rendererOptions?.textureBudgetBytes),
2542
+ usedBytes: 0,
2543
+ requestedBytes: 0,
2544
+ overBudget: false,
2545
+ overBudgetBytes: 0,
2546
+ residentEntries: 0,
2547
+ evictedEntries: []
1960
2548
  },
1961
2549
  antialiasing: rendererQualityPresets.screenshot.antialiasing,
1962
2550
  screenshotQuality: rendererQualityPresets.screenshot,
@@ -1964,14 +2552,31 @@ function createRendererDiagnosticReport(snapshot, runtime, rendererOptions) {
1964
2552
  warnings
1965
2553
  };
1966
2554
  }
1967
- function requestedRendererPostProcessPasses(hasBloom, ambientOcclusion, contactOcclusion) {
1968
- if (!hasBloom && !ambientOcclusion && !contactOcclusion)
2555
+ function requestedRendererPostProcessPasses(hasBloom, ambientOcclusion, contactOcclusion, extra = {}) {
2556
+ if (!hasBloom && !ambientOcclusion && !contactOcclusion && !extra.colorGrade && !extra.fxaa
2557
+ && !extra.outline && !extra.ssr && !extra.depthOfField && !extra.motionBlurWithheld && !extra.taaWithheld)
1969
2558
  return [];
1970
2559
  const passes = ["render"];
1971
2560
  if (ambientOcclusion || contactOcclusion)
1972
2561
  passes.push("ssao");
1973
2562
  if (hasBloom)
1974
2563
  passes.push("bloom");
2564
+ if (extra.colorGrade)
2565
+ passes.push("color-grade");
2566
+ if (extra.ssr)
2567
+ passes.push("ssr");
2568
+ if (extra.depthOfField)
2569
+ passes.push("depth-of-field");
2570
+ if (extra.outline)
2571
+ passes.push("outline");
2572
+ if (extra.fxaa)
2573
+ passes.push("fxaa");
2574
+ // Withheld intents stay visible in the request list with an explicit suffix
2575
+ // so requested-vs-actual never silently agrees on an unexecuted pass.
2576
+ if (extra.motionBlurWithheld)
2577
+ passes.push("motion-blur");
2578
+ if (extra.taaWithheld)
2579
+ passes.push("taa");
1975
2580
  passes.push("output");
1976
2581
  return passes;
1977
2582
  }
@@ -2032,6 +2637,7 @@ export class AuraSceneBuilder {
2032
2637
  return this;
2033
2638
  }
2034
2639
  camera(next) {
2640
+ resolveCameraClipping(next);
2035
2641
  this.cameraSpec = next;
2036
2642
  return this;
2037
2643
  }
@@ -6384,6 +6990,16 @@ export function createRuntimeNodeImportedAssetEvidence(input) {
6384
6990
  renderItemCount: input.renderItemCount ?? 0,
6385
6991
  skinnedRenderItemCount: input.skinnedRenderItemCount ?? 0,
6386
6992
  morphRenderItemCount: input.morphRenderItemCount ?? 0,
6993
+ ...(input.lastMaterialTracksApplied !== undefined ? { lastMaterialTracksApplied: input.lastMaterialTracksApplied } : {}),
6994
+ ...(input.lastLightTracksApplied !== undefined ? { lastLightTracksApplied: input.lastLightTracksApplied } : {}),
6995
+ ...(input.lastFootPlantingGroundedFeet !== undefined ? { lastFootPlantingGroundedFeet: input.lastFootPlantingGroundedFeet } : {}),
6996
+ ...(input.lastFootPlantingTargetError !== undefined ? { lastFootPlantingTargetError: input.lastFootPlantingTargetError } : {}),
6997
+ ...(input.lastFootPlantingHipOffset !== undefined ? { lastFootPlantingHipOffset: input.lastFootPlantingHipOffset } : {}),
6998
+ ...(input.lastFootPlantingMissingLegs !== undefined ? { lastFootPlantingMissingLegs: [...input.lastFootPlantingMissingLegs] } : {}),
6999
+ ...(input.lastFootPlantingDeformation !== undefined ? { lastFootPlantingDeformation: input.lastFootPlantingDeformation.map(leg => ({ ...leg })) } : {}),
7000
+ ...(input.lastFootPlantingSurfaces !== undefined ? { lastFootPlantingSurfaces: input.lastFootPlantingSurfaces.map(point => ({ ...point, worldPosition: [...point.worldPosition] })) } : {}),
7001
+ ...(input.lastFootPlantingFeet !== undefined ? { lastFootPlantingFeet: input.lastFootPlantingFeet.map(foot => ({ ...foot, worldPosition: [...foot.worldPosition] })) } : {}),
7002
+ ...(input.footPlantingConfigured !== undefined ? { footPlantingConfigured: input.footPlantingConfigured } : {}),
6387
7003
  diagnostics
6388
7004
  };
6389
7005
  }
@@ -6660,11 +7276,26 @@ export function createAuraApp(target, options) {
6660
7276
  const canvas = resolveCanvas(target);
6661
7277
  if (canvas) {
6662
7278
  configureCanvas(canvas, options.pixelRatio ?? rendererSelection.profile.pixelRatio ?? devicePixelRatioSafe(), options.resize ?? true);
7279
+ initializeRootPerformanceQuality(canvas);
7280
+ // Apply a caller's device/capture budget before the production controller
7281
+ // derives its first backing size, LODs, particle pool and shadow target.
7282
+ // Runtime mutations remain guarded by app.setPerformanceQuality().
7283
+ if (options.performanceQuality)
7284
+ setRootPerformanceQuality(canvas, options.performanceQuality);
6663
7285
  }
6664
7286
  const overlay = canvas && shouldRenderOverlay(options.diagnostics, snapshot) ? createDiagnosticsOverlay(canvas, diagnosticsState) : undefined;
6665
7287
  let disposed = false;
6666
7288
  let animationHandle = 0;
6667
7289
  let productionController;
7290
+ let pendingAsyncSteps = 0;
7291
+ let asyncSteps = Promise.resolve();
7292
+ const assertMutableFrame = () => {
7293
+ if (disposed)
7294
+ throw new Error("Aura3D app is disposed.");
7295
+ if (pendingAsyncSteps > 0 || productionController?.busy?.()) {
7296
+ throw new Error("Aura3D frame submission is pending; await stepAsync() before synchronous mutation.");
7297
+ }
7298
+ };
6668
7299
  /** WS-2.9: true from the moment a WebGL mount starts until the controller arrives or fails. */
6669
7300
  let productionMountPending = false;
6670
7301
  /**
@@ -6685,6 +7316,7 @@ export function createAuraApp(target, options) {
6685
7316
  * any time and on any scene.
6686
7317
  */
6687
7318
  let productionMountSettled = Promise.resolve();
7319
+ let productionMountTask = Promise.resolve();
6688
7320
  /** Lets `dispose()` settle an in-flight mount; see the note there. */
6689
7321
  let settleMountForDispose = () => undefined;
6690
7322
  /*
@@ -6749,7 +7381,9 @@ export function createAuraApp(target, options) {
6749
7381
  appPhysicsWorldInstance ??= new PhysicsWorld({
6750
7382
  gravity: options.physics?.gravity ? [...options.physics.gravity] : [0, -9.81, 0],
6751
7383
  fixedDelta: 1 / 60,
6752
- enableSleeping: true
7384
+ enableSleeping: true,
7385
+ ...(options.physics?.seed === undefined ? {} : { seed: options.physics.seed }),
7386
+ ...(options.physics?.continuousCollision ? { continuousCollision: options.physics.continuousCollision } : {})
6753
7387
  });
6754
7388
  return appPhysicsWorldInstance;
6755
7389
  };
@@ -6804,6 +7438,8 @@ export function createAuraApp(target, options) {
6804
7438
  let runtimeAlpha = 0;
6805
7439
  const ownedInputControllers = new Set();
6806
7440
  const runRuntimeFrame = (dt, source) => {
7441
+ if (disposed)
7442
+ return;
6807
7443
  runtimeFrame += 1;
6808
7444
  runtimeTime += dt;
6809
7445
  runtimeAlpha = runtimeFixedDt > 0 ? Math.max(0, Math.min(1, (dt % runtimeFixedDt) / runtimeFixedDt)) : 0;
@@ -6821,7 +7457,7 @@ export function createAuraApp(target, options) {
6821
7457
  for (const callback of [...frameCallbacks])
6822
7458
  callback(frame);
6823
7459
  };
6824
- const shouldUseProductionRendererForCurrentScene = () => Boolean(canvas && renderSnapshot.nodes.some(isWebGLRenderableNode) && typeof window !== "undefined");
7460
+ const shouldUseProductionRendererForCurrentScene = () => Boolean(canvas && hasRootRenderableContent(canvas, renderSnapshot.nodes.some(isWebGLRenderableNode)) && typeof window !== "undefined");
6825
7461
  const resetDiagnosticsForCurrentScene = (backend) => {
6826
7462
  const fresh = createInitialDiagnostics(renderSnapshot, options.renderer);
6827
7463
  diagnosticsState.backend = backend;
@@ -6886,7 +7522,7 @@ export function createAuraApp(target, options) {
6886
7522
  * So the two cases are separated. A renderable scene with no usable canvas now raises a diagnosable
6887
7523
  * error naming the cause, instead of drawing something plausible.
6888
7524
  */
6889
- const declaresRenderableContent = renderSnapshot.nodes.some(isWebGLRenderableNode);
7525
+ const declaresRenderableContent = hasRootRenderableContent(canvas, renderSnapshot.nodes.some(isWebGLRenderableNode));
6890
7526
  /*
6891
7527
  * Scoped to the case that is actually a lie: a canvas WAS supplied, so the caller is looking at
6892
7528
  * pixels, and the scene has renderable content — but WebGL declined it, so those pixels would be a
@@ -6925,7 +7561,7 @@ export function createAuraApp(target, options) {
6925
7561
  : Promise.resolve();
6926
7562
  settleMountForDispose = settleMount;
6927
7563
  if (shouldUseProductionRenderer && canvas) {
6928
- void startProductionRender(canvas, renderSnapshot, diagnosticsState, options, overlay, runRuntimeFrame, () => runtimePaused, runtimeNodes,
7564
+ productionMountTask = startProductionRender(canvas, renderSnapshot, diagnosticsState, options, overlay, runRuntimeFrame, () => runtimePaused, runtimeNodes,
6929
7565
  // A route that registered a frame callback, or that has bodies to simulate, needs
6930
7566
  // frames regardless of what the scene declares.
6931
7567
  /*
@@ -6939,7 +7575,6 @@ export function createAuraApp(target, options) {
6939
7575
  () => runtimeTime * 1000)
6940
7576
  .then((controller) => {
6941
7577
  if (disposed || revision !== mountRevision) {
6942
- productionMountPending = false;
6943
7578
  settleMount();
6944
7579
  controller.dispose();
6945
7580
  return;
@@ -6951,6 +7586,7 @@ export function createAuraApp(target, options) {
6951
7586
  markRouteReady(snapshot, diagnosticsState);
6952
7587
  })
6953
7588
  .catch((error) => {
7589
+ settleMount();
6954
7590
  if (disposed || revision !== mountRevision)
6955
7591
  return;
6956
7592
  productionMountPending = false;
@@ -6975,7 +7611,18 @@ export function createAuraApp(target, options) {
6975
7611
  get backend() {
6976
7612
  return diagnosticsState.backend;
6977
7613
  },
7614
+ setPerformanceQuality(settings) {
7615
+ assertMutableFrame();
7616
+ const quality = validateRootPerformanceQuality(settings);
7617
+ if (!canvas || !productionController?.setPerformanceQuality)
7618
+ throw new Error("PERFORMANCE_QUALITY_UNSUPPORTED: await a production renderer mount before changing quality.");
7619
+ if (quality.particleScale !== 1 && !supportsRootParticleQuality(canvas))
7620
+ throw new Error("PERFORMANCE_PARTICLE_OWNER_UNAVAILABLE: this root workload has no adaptive native particle owner.");
7621
+ applyRootParticleQuality(canvas, quality.particleScale);
7622
+ productionController.setPerformanceQuality(quality);
7623
+ },
6978
7624
  setScene(nextScene) {
7625
+ assertMutableFrame();
6979
7626
  snapshot = normalizeSceneSnapshot(nextScene);
6980
7627
  renderSnapshot = flattenSceneSnapshot(snapshot);
6981
7628
  runtimeNodes.reset(renderSnapshot);
@@ -7022,6 +7669,10 @@ export function createAuraApp(target, options) {
7022
7669
  },
7023
7670
  pause() {
7024
7671
  runtimePaused = true;
7672
+ // Stop the production requestAnimationFrame owner as well as simulation.
7673
+ // Holding the clock alone still submitted the same expensive frame forever.
7674
+ productionController?.pause?.();
7675
+ productionController?.resetTemporalHistory?.("pause");
7025
7676
  },
7026
7677
  resetRuntimeClock() {
7027
7678
  /*
@@ -7044,6 +7695,8 @@ export function createAuraApp(target, options) {
7044
7695
  },
7045
7696
  resume() {
7046
7697
  runtimePaused = false;
7698
+ productionController?.resetTemporalHistory?.("resume");
7699
+ productionController?.resume?.();
7047
7700
  if (!animationHandle && !productionController && options.autoStart !== false && typeof requestAnimationFrame !== "undefined") {
7048
7701
  animationHandle = requestAnimationFrame(render);
7049
7702
  }
@@ -7072,11 +7725,17 @@ export function createAuraApp(target, options) {
7072
7725
  return productionController?.deviceLost?.() ?? false;
7073
7726
  },
7074
7727
  advance(dt = 1 / 60) {
7728
+ assertMutableFrame();
7075
7729
  const seconds = Math.max(0, dt);
7076
7730
  runRuntimeFrame(seconds, "manual");
7077
7731
  canvasRuntimePhysics?.step(seconds);
7732
+ // Imported GLB animation, consumed root motion and foot planting live in the
7733
+ // production actor bridge. They are simulation state, so `advance()` must
7734
+ // update them even when no GPU frame is presented.
7735
+ productionController?.update?.(runtimeTime * 1000);
7078
7736
  },
7079
7737
  step(dt = 1 / 60) {
7738
+ assertMutableFrame();
7080
7739
  const seconds = Math.max(0, dt);
7081
7740
  app.advance(seconds);
7082
7741
  const previousPaused = runtimePaused;
@@ -7136,7 +7795,38 @@ export function createAuraApp(target, options) {
7136
7795
  }
7137
7796
  runtimePaused = previousPaused;
7138
7797
  },
7798
+ stepAsync(dt = 1 / 60) {
7799
+ if (!Number.isFinite(dt) || dt < 0)
7800
+ return Promise.reject(new Error("Aura3D stepAsync requires a finite non-negative delta."));
7801
+ if (disposed)
7802
+ return Promise.reject(new Error("Aura3D app is disposed."));
7803
+ pendingAsyncSteps += 1;
7804
+ const next = asyncSteps.then(async () => {
7805
+ await productionMountSettled;
7806
+ if (disposed)
7807
+ throw new Error("Aura3D app is disposed.");
7808
+ const controller = productionController;
7809
+ if (productionMountFailed || !controller?.renderAsync) {
7810
+ throw new Error(`Aura3D asynchronous submission unavailable: ${diagnosticsState.errors.join("; ") || "a production renderer is required"}`);
7811
+ }
7812
+ await controller.renderAsync(() => {
7813
+ runRuntimeFrame(dt, "manual");
7814
+ canvasRuntimePhysics?.step(dt);
7815
+ return runtimeTime * 1000;
7816
+ });
7817
+ }).finally(() => { pendingAsyncSteps -= 1; });
7818
+ asyncSteps = next.catch(() => undefined);
7819
+ return next;
7820
+ },
7821
+ async disposeAsync() {
7822
+ const controller = productionController;
7823
+ app.dispose();
7824
+ await asyncSteps;
7825
+ await productionMountTask;
7826
+ await controller?.whenIdle?.();
7827
+ },
7139
7828
  diagnostics() {
7829
+ diagnosticsState.renderer = readRootDiagnosticSnapshot(diagnosticsState.renderer, productionController?.diagnostics, disposed, pendingAsyncSteps > 0 || Boolean(productionController?.busy?.()));
7140
7830
  return snapshotDiagnostics(diagnosticsState);
7141
7831
  },
7142
7832
  evidence(evidenceOptions = {}) {
@@ -7476,6 +8166,7 @@ function worldLabelsFromSnapshot(snapshot, runtimeNodes) {
7476
8166
  return {
7477
8167
  id: node.name ?? `${node.label}-${index + 1}`,
7478
8168
  text: node.text,
8169
+ role: labelTelemetryRoleFor(node.label),
7479
8170
  anchor: [0, 0, 0],
7480
8171
  offscreenPolicy: "hide"
7481
8172
  };
@@ -7489,6 +8180,7 @@ function worldLabelsFromSnapshot(snapshot, runtimeNodes) {
7489
8180
  return {
7490
8181
  id: node.name ?? `${node.label}-${index + 1}`,
7491
8182
  text: node.text,
8183
+ role: labelTelemetryRoleFor(node.label),
7492
8184
  anchor: [position[0], position[1], position[2]],
7493
8185
  leaderAnchor: [anchor[0], anchor[1], anchor[2]],
7494
8186
  screenOffset: separated ? [0, 0] : [0, -26],
@@ -7642,8 +8334,10 @@ requiresFrames = () => false,
7642
8334
  * wall-clock time. Without it, `pause()` froze the simulation but not time-driven rendering.
7643
8335
  */
7644
8336
  pausedRenderTime = () => 0) {
7645
- const renderableNode = snapshot.nodes.find(isWebGLRenderableNode);
7646
- if (!renderableNode) {
8337
+ // Use the same ownership rule as initial mount and setScene. A compatibility
8338
+ // source continues supplying arena/fighter geometry when public nodes are
8339
+ // temporarily absent (for example the crowd-hidden negative control).
8340
+ if (!hasRootRenderableContent(canvas, snapshot.nodes.some(isWebGLRenderableNode))) {
7647
8341
  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().");
7648
8342
  }
7649
8343
  const renderer = await createProductionSceneRenderer(canvas, snapshot, options.renderer, runtimeNodes);
@@ -7652,14 +8346,34 @@ pausedRenderTime = () => 0) {
7652
8346
  const labelLayer = createSceneLabelLayer(canvas, snapshot);
7653
8347
  const rendererSelection = normalizeCreateAppRendererOptions(options.renderer);
7654
8348
  const pixelRatio = options.pixelRatio ?? rendererSelection.profile.pixelRatio ?? devicePixelRatioSafe();
8349
+ let disposed = false;
8350
+ let animationHandle = 0;
8351
+ let lastTime = 0;
8352
+ let pendingFrames = 0;
8353
+ let frameTail = Promise.resolve();
8354
+ const baseBackingSize = getRootPerformanceBaseSize(canvas);
8355
+ let resourcesReleased = false;
8356
+ const releaseResources = () => {
8357
+ if (resourcesReleased)
8358
+ return;
8359
+ resourcesReleased = true;
8360
+ labelLayer?.dispose();
8361
+ renderer.dispose();
8362
+ };
7655
8363
  const resizeRenderer = () => {
8364
+ if (disposed)
8365
+ return;
8366
+ if (pendingFrames > 0) {
8367
+ resourceOwner.requestResize();
8368
+ return;
8369
+ }
7656
8370
  if (options.resize === false)
7657
8371
  return;
7658
8372
  const parentRect = canvas.parentElement?.getBoundingClientRect();
7659
8373
  const cssWidth = parentRect?.width || canvas.clientWidth || (typeof window !== "undefined" ? window.innerWidth : 960) || 960;
7660
8374
  const cssHeight = parentRect?.height || canvas.clientHeight || (typeof window !== "undefined" ? window.innerHeight : 540) || 540;
7661
- const width = Math.max(320, Math.round(cssWidth * pixelRatio));
7662
- const height = Math.max(220, Math.round(cssHeight * pixelRatio));
8375
+ const width = Math.max(1, Math.round(Math.max(320, cssWidth * pixelRatio) * (getRootPerformanceQuality(canvas)?.resolutionScale ?? 1)));
8376
+ const height = Math.max(1, Math.round(Math.max(220, cssHeight * pixelRatio) * (getRootPerformanceQuality(canvas)?.resolutionScale ?? 1)));
7663
8377
  if (canvas.width === width && canvas.height === height)
7664
8378
  return;
7665
8379
  canvas.style.width = `${cssWidth}px`;
@@ -7668,21 +8382,19 @@ pausedRenderTime = () => 0) {
7668
8382
  canvas.height = height;
7669
8383
  renderer.resize?.(width, height);
7670
8384
  };
8385
+ const resourceOwner = new DeferredFrameResources(releaseResources, resizeRenderer);
7671
8386
  const resizeObserver = options.resize !== false && typeof ResizeObserver !== "undefined" && canvas.parentElement
7672
8387
  ? new ResizeObserver(resizeRenderer)
7673
8388
  : undefined;
7674
8389
  resizeObserver?.observe(canvas.parentElement);
7675
8390
  if (options.resize !== false && typeof window !== "undefined")
7676
8391
  window.addEventListener("resize", resizeRenderer);
7677
- let disposed = false;
7678
- let animationHandle = 0;
7679
- let lastTime = 0;
7680
- const renderFrame = (time = performanceNow()) => {
8392
+ const renderFrame = (time = performanceNow(), submittedDrawCalls, explicit = false) => {
7681
8393
  if (disposed)
7682
8394
  return;
7683
8395
  const delta = lastTime > 0 ? Math.max(1, time - lastTime) : 16.67;
7684
8396
  lastTime = time;
7685
- if (!isPaused())
8397
+ if (!explicit && !isPaused())
7686
8398
  beforeRender?.(delta / 1000, "raf");
7687
8399
  /*
7688
8400
  * A paused app must not keep advancing time-driven rendering.
@@ -7697,8 +8409,8 @@ pausedRenderTime = () => 0) {
7697
8409
  * `runtimeTime` and renders explicitly, so stepping still produces new frames — what stops is
7698
8410
  * time passing on its own.
7699
8411
  */
7700
- const renderTime = isPaused() ? pausedRenderTime() : time;
7701
- const drawCalls = renderer.render(renderTime);
8412
+ const renderTime = resolveRootRenderTime(time, explicit, isPaused, pausedRenderTime);
8413
+ const drawCalls = submittedDrawCalls ?? renderer.render(renderTime);
7702
8414
  diagnosticsState.backend = renderer.backend;
7703
8415
  diagnosticsState.fps = diagnosticsState.fps || 60;
7704
8416
  diagnosticsState.drawCalls = drawCalls;
@@ -7717,14 +8429,100 @@ pausedRenderTime = () => 0) {
7717
8429
  labelLayer.setOcclusionTest(createSceneLabelOcclusionTest(snapshot, resolveCameraFrame(snapshot, snapshot.camera, renderTime, runtimeNodes).eye, runtimeNodes));
7718
8430
  labelLayer.update(renderer.viewProjection(renderTime));
7719
8431
  diagnosticsState.labels = labelLayer.snapshot();
8432
+ const labelNodes = groups.flatten(snapshot.nodes).filter((node) => node.kind === "label");
8433
+ diagnosticsState.labelTelemetry = collectLabelTelemetry(labelNodes, diagnosticsState.labels);
8434
+ diagnosticsState.textBuckets = summarizeTextBuckets({
8435
+ accessibleDom: labelNodes.filter((node) => node.label === "hud").length,
8436
+ worldAnchoredPlaced: diagnosticsState.labels.filter((label) => label.visible).length,
8437
+ sdfTexts: groups.flatten(snapshot.nodes).filter((node) => node.kind === "primitive" && node.text3D?.backend === "sdf").length
8438
+ });
7720
8439
  }
7721
8440
  overlay?.update();
7722
- if ((sceneWantsFrames || requiresFrames()) && options.autoStart !== false && typeof requestAnimationFrame !== "undefined") {
7723
- animationHandle = requestAnimationFrame(renderFrame);
8441
+ if (!explicit && !isPaused() && (sceneWantsFrames || requiresFrames()) && options.autoStart !== false && typeof requestAnimationFrame !== "undefined") {
8442
+ animationHandle = requestAnimationFrame(scheduleFrame);
7724
8443
  }
7725
8444
  };
7726
- renderFrame();
8445
+ const submitAsync = (clock, automatic = false) => {
8446
+ if (disposed)
8447
+ return Promise.reject(new Error("Aura3D renderer is disposed."));
8448
+ if (!renderer.renderAsync)
8449
+ return Promise.reject(new Error("Aura3D asynchronous submission requires the production renderer."));
8450
+ pendingFrames += 1;
8451
+ const submitted = resourceOwner.run(() => frameTail.then(async () => {
8452
+ if (disposed)
8453
+ throw new Error("Aura3D renderer is disposed.");
8454
+ const time = clock();
8455
+ if (automatic) {
8456
+ const delta = lastTime > 0 ? Math.max(1, time - lastTime) : 16.67;
8457
+ lastTime = time;
8458
+ if (!isPaused())
8459
+ beforeRender?.(delta / 1000, "raf");
8460
+ }
8461
+ // User frame callbacks can dispose the app. Do not submit a new frame after
8462
+ // that callback; pending ownership still defers cleanup until this settles.
8463
+ if (disposed)
8464
+ throw new Error("Aura3D renderer is disposed.");
8465
+ const renderTime = resolveRootRenderTime(time, !automatic, isPaused, pausedRenderTime);
8466
+ const drawCalls = await renderer.renderAsync(renderTime);
8467
+ if (!disposed) {
8468
+ const previousLastTime = lastTime;
8469
+ renderFrame(renderTime, drawCalls, true);
8470
+ lastTime = previousLastTime;
8471
+ }
8472
+ }).finally(() => { pendingFrames -= 1; }));
8473
+ frameTail = submitted.catch(() => undefined);
8474
+ return submitted;
8475
+ };
8476
+ const scheduleFrame = (time) => {
8477
+ animationHandle = 0;
8478
+ if (disposed)
8479
+ return;
8480
+ if (options.frameMode === "async") {
8481
+ void submitAsync(() => time, true).then(() => {
8482
+ if (!disposed && !isPaused() && (sceneWantsFrames || requiresFrames()) && options.autoStart !== false && typeof requestAnimationFrame !== "undefined") {
8483
+ animationHandle = requestAnimationFrame(scheduleFrame);
8484
+ }
8485
+ }).catch((error) => {
8486
+ diagnosticsState.errors.push(productionRenderErrorMessage(error));
8487
+ overlay?.update();
8488
+ });
8489
+ }
8490
+ else if (pendingFrames > 0) {
8491
+ // A manual async frame owns the resources. Retry without advancing simulation.
8492
+ // A pause cancels automatic retries; the explicit owner completes independently.
8493
+ if (!isPaused())
8494
+ animationHandle = requestAnimationFrame(scheduleFrame);
8495
+ }
8496
+ else
8497
+ renderFrame(time);
8498
+ };
8499
+ try {
8500
+ if (options.frameMode === "async") {
8501
+ await submitAsync(() => performanceNow(), true);
8502
+ if (!isPaused() && (sceneWantsFrames || requiresFrames()) && options.autoStart !== false && typeof requestAnimationFrame !== "undefined") {
8503
+ animationHandle = requestAnimationFrame(scheduleFrame);
8504
+ }
8505
+ }
8506
+ else
8507
+ renderFrame();
8508
+ }
8509
+ catch (error) {
8510
+ // A failed first submission has no returned controller to dispose its resources.
8511
+ disposed = true;
8512
+ resizeObserver?.disconnect();
8513
+ if (typeof window !== "undefined")
8514
+ window.removeEventListener("resize", resizeRenderer);
8515
+ releaseResources();
8516
+ throw error;
8517
+ }
7727
8518
  return {
8519
+ update(time) {
8520
+ if (disposed)
8521
+ throw new Error("Aura3D renderer is disposed.");
8522
+ if (pendingFrames > 0)
8523
+ throw new Error("Aura3D async frame is pending; await it before advancing renderer state.");
8524
+ renderer.update?.(time);
8525
+ },
7728
8526
  render(time = performanceNow()) {
7729
8527
  /*
7730
8528
  * Preserve `lastTime` across an explicit render.
@@ -7735,10 +8533,50 @@ pausedRenderTime = () => 0) {
7735
8533
  * different clock — a visible jump. Same defect the canvas2d path had; fixed identically so
7736
8534
  * the two render paths cannot disagree about what `step` does.
7737
8535
  */
8536
+ if (pendingFrames > 0)
8537
+ throw new Error("Aura3D async frame is pending; await it before synchronous rendering.");
7738
8538
  const previousLastTime = lastTime;
7739
- renderFrame(time);
8539
+ renderFrame(time, undefined, true);
7740
8540
  lastTime = previousLastTime;
7741
8541
  },
8542
+ setPerformanceQuality(settings) {
8543
+ if (disposed || pendingFrames > 0)
8544
+ throw new Error("PERFORMANCE_QUALITY_BUSY: await the active frame before changing quality.");
8545
+ setRootPerformanceQuality(canvas, settings);
8546
+ if (options.resize !== false)
8547
+ resizeRenderer();
8548
+ else {
8549
+ const width = Math.max(1, Math.round(baseBackingSize.width * settings.resolutionScale));
8550
+ const height = Math.max(1, Math.round(baseBackingSize.height * settings.resolutionScale));
8551
+ canvas.width = width;
8552
+ canvas.height = height;
8553
+ renderer.resize?.(width, height);
8554
+ }
8555
+ renderer.resetTemporalHistory?.("performance-quality");
8556
+ },
8557
+ diagnostics: () => disposed || pendingFrames > 0 ? undefined : renderer.diagnostics,
8558
+ renderAsync: (clock) => submitAsync(clock),
8559
+ busy: () => pendingFrames > 0,
8560
+ whenIdle: () => frameTail,
8561
+ resetTemporalHistory(reason) {
8562
+ // Reset after the active submission, before the next one can consume history.
8563
+ if (pendingFrames > 0)
8564
+ frameTail = frameTail.then(() => { if (!disposed)
8565
+ renderer.resetTemporalHistory?.(reason); });
8566
+ else
8567
+ renderer.resetTemporalHistory?.(reason);
8568
+ },
8569
+ pause() {
8570
+ if (animationHandle && typeof cancelAnimationFrame !== "undefined")
8571
+ cancelAnimationFrame(animationHandle);
8572
+ animationHandle = 0;
8573
+ },
8574
+ resume() {
8575
+ if (disposed || animationHandle || options.autoStart === false || typeof requestAnimationFrame === "undefined")
8576
+ return;
8577
+ if (sceneWantsFrames || requiresFrames())
8578
+ animationHandle = requestAnimationFrame(scheduleFrame);
8579
+ },
7742
8580
  // WS-2.6 — forward device-loss subscription from whichever renderer backs this controller.
7743
8581
  onDeviceLost: renderer.onDeviceLost ? (listener) => renderer.onDeviceLost(listener) : undefined,
7744
8582
  onDeviceRestored: renderer.onDeviceRestored ? (listener) => renderer.onDeviceRestored(listener) : undefined,
@@ -7750,8 +8588,7 @@ pausedRenderTime = () => 0) {
7750
8588
  resizeObserver?.disconnect();
7751
8589
  if (typeof window !== "undefined")
7752
8590
  window.removeEventListener("resize", resizeRenderer);
7753
- labelLayer?.dispose();
7754
- renderer.dispose();
8591
+ resourceOwner.dispose();
7755
8592
  }
7756
8593
  };
7757
8594
  }
@@ -7814,16 +8651,37 @@ function createProductionRuntimeEnvironment(snapshot) {
7814
8651
  "product-hero": "softbox",
7815
8652
  "night-cinematic": "evening",
7816
8653
  "metal-studio": "exhibit",
7817
- "glass-studio": "softbox"
8654
+ "glass-studio": "softbox",
8655
+ // B3: HDRI first frames render the honest studio procedural fallback;
8656
+ // the post-mount upgrade swaps in the HDR chain (hdriUrl below).
8657
+ "hdri": "studio"
7818
8658
  };
7819
8659
  const preset = presetByEnvironment[authoredEnvironment.environment];
7820
8660
  const bundle = createExternalParityEnvironmentLighting(preset);
7821
8661
  const intensity = nonNegativeFinite(authoredEnvironment.intensity);
7822
8662
  const proceduralMap = bundle.lighting.proceduralMap;
8663
+ const hdriUrl = authoredEnvironment.environment === "hdri"
8664
+ && typeof authoredEnvironment.texture === "object"
8665
+ && authoredEnvironment.texture.kind === "aura-asset-ref"
8666
+ ? authoredEnvironment.texture.url ?? undefined
8667
+ : undefined;
8668
+ const hdriReflectionUrl = authoredEnvironment.environment === "hdri"
8669
+ && typeof authoredEnvironment.reflectionTexture === "object"
8670
+ && authoredEnvironment.reflectionTexture.kind === "aura-asset-ref"
8671
+ ? authoredEnvironment.reflectionTexture.url ?? undefined
8672
+ : undefined;
8673
+ const hdriRotation = authoredEnvironment.environment === "hdri" && authoredEnvironment.rotation !== undefined
8674
+ ? authoredEnvironment.rotation
8675
+ : undefined;
7823
8676
  return {
7824
8677
  preset: authoredEnvironment.environment,
7825
8678
  intensity,
7826
- evidence: `production bridge submitted the authored ${authoredEnvironment.environment} environment through generated HDR ${preset} lighting`,
8679
+ evidence: hdriUrl
8680
+ ? `production bridge renders the studio procedural fallback until the authored HDRI ${hdriUrl}${hdriReflectionUrl ? ` (reflection ${hdriReflectionUrl})` : ""} resolves through the HDR chain`
8681
+ : `production bridge submitted the authored ${authoredEnvironment.environment} environment through generated HDR ${preset} lighting`,
8682
+ ...(hdriUrl ? { hdriUrl } : {}),
8683
+ ...(hdriReflectionUrl ? { hdriReflectionUrl } : {}),
8684
+ ...(hdriRotation !== undefined ? { hdriRotation } : {}),
7827
8685
  lighting: {
7828
8686
  ...bundle.lighting,
7829
8687
  color: authoredEnvironment.color ? colorToLinearRgb(authoredEnvironment.color) : bundle.lighting.color,
@@ -7878,14 +8736,16 @@ function createProductionRuntimeEnvironment(snapshot) {
7878
8736
  * changed exactly zero pixels through `createAuraApp`. Returning `false` when no
7879
8737
  * fog node is authored keeps the unfogged path unchanged.
7880
8738
  */
7881
- function createProductionRuntimeEnvironmentFog(snapshot) {
7882
- const fog = groups.flatten(snapshot.nodes).find((node) => node.kind === "effect" && node.effect === "fog");
8739
+ function createProductionRuntimeEnvironmentFog(snapshot, lights = [], renderWidth = 1280, renderHeight = 720) {
8740
+ const nodes = groups.flatten(snapshot.nodes);
8741
+ const volumetric = nodes.find((node) => node.kind === "effect" && node.effect === "volumetric-fog");
8742
+ const fog = volumetric ?? nodes.find((node) => node.kind === "effect" && node.effect === "fog");
7883
8743
  if (!fog)
7884
8744
  return false;
7885
8745
  const density = clampNumber(fog.density ?? 0.12, 0, 1);
7886
8746
  const intensity = clampNumber(fog.intensity ?? 0.5, 0, 1);
7887
8747
  const color = colorToLinearRgb(fog.color ?? "#9fb7d9");
7888
- return {
8748
+ const base = {
7889
8749
  // Exponential-squared reads as depth haze rather than a hard linear band, which
7890
8750
  // matches what the public helper documents.
7891
8751
  mode: "exponential-squared",
@@ -7895,10 +8755,53 @@ function createProductionRuntimeEnvironmentFog(snapshot) {
7895
8755
  density,
7896
8756
  maxOpacity: clampNumber(0.25 + intensity * 0.55, 0, 0.92)
7897
8757
  };
7898
- }
7899
- function createProductionRuntimePostprocess(snapshot) {
8758
+ if (!volumetric)
8759
+ return base;
8760
+ // A5: the volumetric-fog node drives the GPU forward inscatter terms from
8761
+ // the dominant collected light; quality "off" zeroes the forward term and
8762
+ // keeps this exp2 base exactly.
8763
+ const resolved = resolveVolumetricFog({
8764
+ density: volumetric.density,
8765
+ intensity: volumetric.intensity,
8766
+ ...(volumetric.lightPosition ? { lightPosition: volumetric.lightPosition } : {}),
8767
+ ...(volumetric.heightFalloff !== undefined ? { heightFalloff: volumetric.heightFalloff } : {}),
8768
+ ...(volumetric.heightReference !== undefined ? { heightReference: volumetric.heightReference } : {}),
8769
+ ...(volumetric.volumetricQuality ? { quality: volumetric.volumetricQuality } : {})
8770
+ }, lights, renderWidth, renderHeight);
8771
+ return {
8772
+ ...base,
8773
+ density: clampNumber(volumetric.density ?? 0.18, 0, 1),
8774
+ volumetricIntensity: resolved.forward.volumetricIntensity,
8775
+ ...(resolved.forward.volumetricLightDirection ? { volumetricLightDirection: resolved.forward.volumetricLightDirection } : {}),
8776
+ volumetricLightColor: resolved.forward.volumetricLightColor,
8777
+ ...(resolved.forward.heightFalloff !== undefined ? { heightFalloff: resolved.forward.heightFalloff } : {}),
8778
+ ...(resolved.forward.heightReference !== undefined ? { heightReference: resolved.forward.heightReference } : {})
8779
+ };
8780
+ }
8781
+ const productionTemporalSceneKeys = new WeakMap();
8782
+ let productionTemporalSceneSequence = 0;
8783
+ export function resolveNativeBloomRadius(authoredRadius) {
8784
+ const radius = authoredRadius ?? 0.38;
8785
+ const normalizedKernel = radius <= 1 ? Math.round(radius * 8) : Math.round(radius);
8786
+ return Math.max(1, Math.min(4, normalizedKernel));
8787
+ }
8788
+ function createProductionRuntimePostprocess(snapshot, lights = [], renderWidth = 1280, renderHeight = 720, temporalSupported = true) {
7900
8789
  const nodes = groups.flatten(snapshot.nodes);
7901
8790
  const authoredBloom = nodes.find((node) => node.kind === "effect" && node.effect === "bloom");
8791
+ // A5: the DISTINCT volumetric-fog node submits the depth-aware inscatter
8792
+ // pass (renderer-owned depth attaches automatically); quality "off" (or no
8793
+ // node) submits nothing and the scene keeps forward exp2 fog only.
8794
+ const authoredVolumetricFog = nodes.find((node) => node.kind === "effect" && node.effect === "volumetric-fog");
8795
+ const volumetricPass = authoredVolumetricFog
8796
+ ? resolveVolumetricFog({
8797
+ density: authoredVolumetricFog.density,
8798
+ intensity: authoredVolumetricFog.intensity,
8799
+ ...(authoredVolumetricFog.lightPosition ? { lightPosition: authoredVolumetricFog.lightPosition } : {}),
8800
+ ...(authoredVolumetricFog.heightFalloff !== undefined ? { heightFalloff: authoredVolumetricFog.heightFalloff } : {}),
8801
+ ...(authoredVolumetricFog.heightReference !== undefined ? { heightReference: authoredVolumetricFog.heightReference } : {}),
8802
+ ...(authoredVolumetricFog.volumetricQuality ? { quality: authoredVolumetricFog.volumetricQuality } : {})
8803
+ }, lights, renderWidth, renderHeight).pass
8804
+ : null;
7902
8805
  const bloomRequested = Boolean(authoredBloom);
7903
8806
  // Root previously advertised `ssao` in `requestedPasses` whenever a scene added
7904
8807
  // effects.ambientOcclusion() or effects.contactOcclusion(), but never submitted
@@ -7908,15 +8811,45 @@ function createProductionRuntimePostprocess(snapshot) {
7908
8811
  const authoredAmbientOcclusion = nodes.find((node) => node.kind === "effect" && node.effect === "ambient-occlusion");
7909
8812
  const authoredContactOcclusion = nodes.find((node) => node.kind === "effect" && node.effect === "contact-occlusion");
7910
8813
  const authoredOcclusion = authoredAmbientOcclusion ?? authoredContactOcclusion;
8814
+ // The renderer owns temporal GPU inputs. Root checks actual drawable
8815
+ // geometry before requesting them; unsupported scenes retain ordinary output.
8816
+ const authoredColorGrade = nodes.find((node) => node.kind === "effect" && node.effect === "color-grade");
8817
+ const authoredAntiAlias = nodes.find((node) => node.kind === "effect" && node.effect === "anti-alias");
8818
+ const authoredOutline = nodes.find((node) => node.kind === "effect" && node.effect === "outline");
8819
+ const authoredSsr = nodes.find((node) => node.kind === "effect" && node.effect === "screen-space-reflections");
8820
+ const authoredDof = nodes.find((node) => node.kind === "effect" && node.effect === "depth-of-field");
8821
+ const authoredMotionBlur = nodes.find((node) => node.kind === "effect" && node.effect === "motion-blur");
8822
+ const temporalRequested = temporalSupported && (Boolean(authoredMotionBlur) || authoredAntiAlias?.mode === "taa");
8823
+ let sceneKey = productionTemporalSceneKeys.get(snapshot);
8824
+ if (!sceneKey) {
8825
+ sceneKey = `root-scene-${++productionTemporalSceneSequence}`;
8826
+ productionTemporalSceneKeys.set(snapshot, sceneKey);
8827
+ }
8828
+ const fxaaRequested = (authoredAntiAlias?.mode ?? "fxaa") === "fxaa";
8829
+ const outlineChannels = colorToRgba(authoredOutline?.color ?? "#ff9822");
7911
8830
  return {
7912
8831
  // Tone mapping requires unclamped linear input. RGBA8 quantized dark clear
7913
8832
  // colors and clipped highlights before ACES, which produced washed-out output.
7914
8833
  targetFormat: "rgba16f",
8834
+ ...(temporalRequested ? {
8835
+ temporal: { sceneKey },
8836
+ ...(authoredMotionBlur ? { motionBlur: { samples: 8, scale: clampNumber(authoredMotionBlur.intensity ?? .5, 0, 2) } } : {}),
8837
+ ...(authoredAntiAlias?.mode === "taa" ? { taa: { blend: .9 } } : {})
8838
+ } : {}),
7915
8839
  ...(bloomRequested ? {
7916
8840
  bloom: {
7917
8841
  threshold: clampNumber(authoredBloom?.threshold ?? 0.78, 0, 1),
7918
8842
  intensity: clampNumber(authoredBloom?.intensity ?? 0.3, 0, 2),
7919
- radius: Math.max(1, Math.min(4, Math.round(authoredBloom?.radius ?? 2)))
8843
+ // Public bloom radius follows the normalized UnrealBloomPass-style
8844
+ // control used by routes and prefabs (normally 0..1), while the native
8845
+ // separable blur consumes an integer pixel kernel. Preserve legacy
8846
+ // explicit pixel kernels above 1 and spread normalized values across
8847
+ // 1..4 so common authored values such as 0.22/0.38 do not all collapse
8848
+ // to a one-pixel blur after rounding.
8849
+ radius: resolveNativeBloomRadius(authoredBloom?.radius),
8850
+ ...(authoredBloom?.quality !== undefined ? { quality: authoredBloom.quality } : {}),
8851
+ ...(authoredBloom?.softKnee !== undefined ? { softKnee: authoredBloom.softKnee } : {}),
8852
+ ...(authoredBloom?.shoulder !== undefined ? { shoulder: authoredBloom.shoulder } : {})
7920
8853
  }
7921
8854
  } : {}),
7922
8855
  ...(authoredOcclusion ? {
@@ -7937,7 +8870,41 @@ function createProductionRuntimePostprocess(snapshot) {
7937
8870
  operator: "aces",
7938
8871
  inputColorSpace: "linear",
7939
8872
  outputColorSpace: "srgb"
7940
- }
8873
+ },
8874
+ ...(authoredColorGrade ? {
8875
+ colorGrade: {
8876
+ contrast: clampNumber(authoredColorGrade.contrast ?? 1, 0, 3),
8877
+ saturation: clampNumber(authoredColorGrade.saturation ?? 1, 0, 3)
8878
+ }
8879
+ } : {}),
8880
+ ...(authoredOutline ? {
8881
+ outline: {
8882
+ color: [
8883
+ Math.round(outlineChannels[0] * 255),
8884
+ Math.round(outlineChannels[1] * 255),
8885
+ Math.round(outlineChannels[2] * 255),
8886
+ 255
8887
+ ],
8888
+ width: Math.max(1, Math.min(6, Math.round(authoredOutline.width ?? 3))),
8889
+ threshold: clampNumber(authoredOutline.threshold ?? 0.12, 0, 4),
8890
+ opacity: clampNumber(authoredOutline.intensity ?? 0.9, 0, 1)
8891
+ }
8892
+ } : {}),
8893
+ ...(fxaaRequested && authoredAntiAlias ? { fxaa: {} } : {}),
8894
+ ...(authoredSsr ? {
8895
+ ssr: {
8896
+ intensity: clampNumber(authoredSsr.intensity ?? 0.9, 0, 2),
8897
+ maxDistance: 18
8898
+ }
8899
+ } : {}),
8900
+ ...(authoredDof ? {
8901
+ depthOfField: {
8902
+ focusDepth: clampNumber(authoredDof.focus ?? 0.02, 0, 1),
8903
+ focusRange: clampNumber(0.02 + clampNumber(authoredDof.aperture ?? 0.35, 0, 1) * 0.3, 0.001, 1),
8904
+ maxRadius: Math.max(0, Math.min(8, Math.round(authoredDof.maxBlur ?? 4)))
8905
+ }
8906
+ } : {}),
8907
+ ...(volumetricPass ? { volumetricLight: volumetricPass } : {})
7941
8908
  };
7942
8909
  }
7943
8910
  function createProductionRuntimeShadowOptions(snapshot, collectedLights) {
@@ -7956,11 +8923,11 @@ function createProductionRuntimeShadowOptions(snapshot, collectedLights) {
7956
8923
  }, 1);
7957
8924
  const shadowCaster = collectedLights.find((light) => light.castsShadow);
7958
8925
  const size = sceneRadius > 30 ? 4096 : sceneRadius > 10 ? 2048 : 1024;
7959
- const texelWorldSize = (sceneRadius * 2) / size;
8926
+ const tuning = resolveProductionRuntimeShadowTuning(shadowCaster?.kind, size, sceneRadius);
7960
8927
  return {
7961
8928
  enabled: Boolean(shadowCaster),
7962
8929
  size,
7963
- bias: clampNumber(texelWorldSize * 0.55, 0.00035, 0.004),
8930
+ ...tuning,
7964
8931
  strength: category === "city-day" ? 0.38
7965
8932
  : category === "material" || category === "product" ? 0.24
7966
8933
  : 0.32,
@@ -7970,6 +8937,30 @@ function createProductionRuntimeShadowOptions(snapshot, collectedLights) {
7970
8937
  label: `aura3d-root-production-${category}-${size}px-shadow-map`
7971
8938
  };
7972
8939
  }
8940
+ /**
8941
+ * Shadow bias is compared in normalized light-depth space. Directional maps use
8942
+ * an orthographic projection, so their existing scene-radius/texel heuristic is
8943
+ * world-linear. A spot map is perspective: applying that world-space value at
8944
+ * the far end of its cone erased legitimate caster/receiver separation (Aura
8945
+ * Clash measured 0.00147 normalized depth versus the old 0.004 constant bias),
8946
+ * and the default slope term could erase the remainder on a vertical receiver.
8947
+ * Keep the established directional/point policy intact and use a bounded
8948
+ * fraction of one normalized spot-map texel for both perspective terms.
8949
+ */
8950
+ export function resolveProductionRuntimeShadowTuning(kind, size, sceneRadius) {
8951
+ if (!Number.isInteger(size) || size <= 0)
8952
+ throw new RangeError("Shadow-map size must be a positive integer.");
8953
+ if (!Number.isFinite(sceneRadius) || sceneRadius <= 0)
8954
+ throw new RangeError("Shadow scene radius must be finite and positive.");
8955
+ if (kind === "spot") {
8956
+ return {
8957
+ bias: clampNumber(0.15 / size, 0.00005, 0.00035),
8958
+ slopeBias: 0.12
8959
+ };
8960
+ }
8961
+ const texelWorldSize = (sceneRadius * 2) / size;
8962
+ return { bias: clampNumber(texelWorldSize * 0.55, 0.00035, 0.004) };
8963
+ }
7973
8964
  /**
7974
8965
  * Device-observed shadow state. `requested` comes from the submitted shadow
7975
8966
  * options, but `mapRendered`/`mapSampled` come only from what the device
@@ -7977,11 +8968,12 @@ function createProductionRuntimeShadowOptions(snapshot, collectedLights) {
7977
8968
  * sample it. This is what stops the root report from publishing an unconditional
7978
8969
  * `shadows.enabled: true`.
7979
8970
  */
7980
- function createProductionRuntimeShadowObservation(shadowOptions, diagnostics) {
8971
+ function createProductionRuntimeShadowObservation(shadowOptions, diagnostics, observed) {
7981
8972
  const nativeShadowMapBindings = diagnostics.nativeShadowMapBindings ?? 0;
7982
- const mapRendered = Boolean(shadowOptions.enabled) && (diagnostics.shadowRenderTargetsAllocated ?? 0) > 0;
8973
+ const mapRendered = Boolean(shadowOptions.enabled) && observed !== null && (diagnostics.shadowRenderTargetsAllocated ?? 0) > 0;
7983
8974
  return {
7984
8975
  requested: Boolean(shadowOptions.enabled),
8976
+ observed,
7985
8977
  mapRendered,
7986
8978
  mapSampled: mapRendered && nativeShadowMapBindings > 0,
7987
8979
  mapSize: shadowOptions.size,
@@ -7990,6 +8982,79 @@ function createProductionRuntimeShadowObservation(shadowOptions, diagnostics) {
7990
8982
  shadowRenderTargetsAllocated: diagnostics.shadowRenderTargetsAllocated ?? 0
7991
8983
  };
7992
8984
  }
8985
+ /**
8986
+ * G1 SDF text observation (muse3jsparity-PRD): per-resource fail-closed gate
8987
+ * (`describeSdfTextPixelBacking`) ANDed across mounted SDF resources. Layout
8988
+ * alone never backs pixels; lastOpacity is the live LOD x occlusion proof.
8989
+ */
8990
+ function createProductionTextObservation(entries) {
8991
+ const sdf = entries.flatMap((entry) => entry.resources.filter((resource) => resource.sdfText !== null));
8992
+ if (sdf.length === 0) {
8993
+ return {
8994
+ sdfTexts: 0,
8995
+ textPixelBacked: false,
8996
+ quadCount: 0,
8997
+ lastOpacity: 1,
8998
+ reason: "no SDF text nodes mounted"
8999
+ };
9000
+ }
9001
+ const gates = sdf.map((resource) => describeSdfTextPixelBacking({
9002
+ atlasUploaded: resource.textureStatus === "textured" && resource.texturedMaterial !== null,
9003
+ quadsSubmitted: resource.sdfText.lastSubmitted,
9004
+ quadCount: resource.sdfText.quadCount
9005
+ }));
9006
+ const backed = gates.filter((gate) => gate.textPixelBacked).length;
9007
+ const quadCount = sdf.reduce((total, resource) => total + (resource.sdfText?.quadCount ?? 0), 0);
9008
+ const lastOpacity = Math.min(...sdf.map((resource) => resource.sdfText?.lastOpacity ?? 1));
9009
+ return {
9010
+ sdfTexts: sdf.length,
9011
+ textPixelBacked: backed === sdf.length,
9012
+ quadCount,
9013
+ lastOpacity,
9014
+ reason: backed === sdf.length
9015
+ ? `${backed}/${sdf.length} SDF quad sets sampled from uploaded label textures (${quadCount} quads)`
9016
+ : `${backed}/${sdf.length} SDF quad sets backed: ${gates.filter((gate) => !gate.textPixelBacked).map((gate) => gate.reason).join("; ")}`
9017
+ };
9018
+ }
9019
+ /**
9020
+ * M2 texture streaming observation (muse3jsparity-PRD): funds the
9021
+ * post-upgrade texture table (SDF label images + C1 upgrades) against the
9022
+ * budget with distance-prioritized residency. Empty table = nothing to fund.
9023
+ */
9024
+ function createProductionTexturesObservation(entries, cameraEye, budgetBytes) {
9025
+ const budget = normalizeTextureBudgetBytes(budgetBytes);
9026
+ const table = entries.flatMap((entry, entryIndex) => entry.resources
9027
+ .filter((resource) => resource.textureMipBytes.length > 0)
9028
+ .map((resource, resourceIndex) => {
9029
+ const position = entry.node.position ?? [0, 0, 0];
9030
+ return {
9031
+ id: `${entry.node.name ?? `primitive-${entryIndex}`}#${resourceIndex}`,
9032
+ mipBytesCoarseToFine: resource.textureMipBytes,
9033
+ distanceMeters: Math.hypot(cameraEye[0] - position[0], cameraEye[1] - position[1], cameraEye[2] - position[2])
9034
+ };
9035
+ }));
9036
+ if (table.length === 0) {
9037
+ return {
9038
+ budgetBytes: budget,
9039
+ usedBytes: 0,
9040
+ requestedBytes: 0,
9041
+ overBudget: false,
9042
+ overBudgetBytes: 0,
9043
+ residentEntries: 0,
9044
+ evictedEntries: []
9045
+ };
9046
+ }
9047
+ const residency = describeTextureStreamingResidency(table, budget);
9048
+ return {
9049
+ budgetBytes: budget,
9050
+ usedBytes: residency.usedBytes,
9051
+ requestedBytes: residency.requestedBytes,
9052
+ overBudget: residency.overBudget,
9053
+ overBudgetBytes: residency.overBudgetBytes,
9054
+ residentEntries: residency.residents.filter((resident) => resident.residentLevels > 0).length,
9055
+ evictedEntries: [...residency.evicted]
9056
+ };
9057
+ }
7993
9058
  function createProductionRuntimePostprocessObservation(diagnostics) {
7994
9059
  const actualPasses = diagnostics.postprocessPassNames ?? [];
7995
9060
  const pixelBacked = actualPasses.length > 0 && diagnostics.lastError === null;
@@ -8002,7 +9067,8 @@ function createProductionRuntimePostprocessObservation(diagnostics) {
8002
9067
  pixelBacked,
8003
9068
  actualPasses,
8004
9069
  fallbackPasses: pixelBacked ? [] : ["direct-render"],
8005
- targetFormat: diagnostics.postprocessTargetFormat
9070
+ targetFormat: diagnostics.postprocessTargetFormat,
9071
+ executionMode: diagnostics.postprocessPlan?.executionMode ?? "unknown"
8006
9072
  };
8007
9073
  }
8008
9074
  function createProductionRuntimeCollectedLights(snapshot) {
@@ -8010,17 +9076,36 @@ function createProductionRuntimeCollectedLights(snapshot) {
8010
9076
  const descriptors = authoredLights.flatMap((node, index) => createProductionRuntimeLightDescriptors(node, index));
8011
9077
  if (descriptors.length === 0)
8012
9078
  return createProductionRuntimeFallbackLights();
8013
- const shadowCasterIndex = descriptors.reduce((selected, descriptor, index) => {
8014
- if (selected < 0)
8015
- return index;
8016
- const current = descriptors[selected];
8017
- if (descriptor.shadowPriority !== current.shadowPriority) {
8018
- return descriptor.shadowPriority > current.shadowPriority ? index : selected;
8019
- }
8020
- return descriptor.intensity > current.intensity ? index : selected;
8021
- }, -1);
9079
+ const shadowCasterIndex = resolveProductionShadowCasterIndex(descriptors);
8022
9080
  return descriptors.map((descriptor, index) => createProductionRuntimeCollectedLight(descriptor, index === shadowCasterIndex));
8023
9081
  }
9082
+ /**
9083
+ * N1 caster selection (pure, unit-tested): an explicit `shadow: true`
9084
+ * request wins the single caster slot over unrequested lights; ties and
9085
+ * all-unrequested scenes keep the legacy priority-then-intensity order.
9086
+ */
9087
+ export function resolveProductionShadowCasterIndex(descriptors) {
9088
+ const explicitlyRequested = descriptors
9089
+ .map((descriptor, index) => ({ descriptor, index }))
9090
+ .filter(({ descriptor }) => descriptor.shadowRequested);
9091
+ // An authored false is scene-level opt-out when no light is explicitly
9092
+ // requested. Otherwise another unspecified light would silently take the
9093
+ // legacy caster slot and make `shadow: false` visually ineffective.
9094
+ if (explicitlyRequested.length === 0 && descriptors.some((descriptor) => descriptor.shadowDisabled === true)) {
9095
+ return -1;
9096
+ }
9097
+ const candidates = explicitlyRequested.length > 0
9098
+ ? explicitlyRequested
9099
+ : descriptors.map((descriptor, index) => ({ descriptor, index }));
9100
+ return candidates.reduce((selected, candidate) => {
9101
+ if (selected === undefined)
9102
+ return candidate;
9103
+ if (candidate.descriptor.shadowPriority !== selected.descriptor.shadowPriority) {
9104
+ return candidate.descriptor.shadowPriority > selected.descriptor.shadowPriority ? candidate : selected;
9105
+ }
9106
+ return candidate.descriptor.intensity > selected.descriptor.intensity ? candidate : selected;
9107
+ }, undefined)?.index ?? -1;
9108
+ }
8024
9109
  function createProductionRuntimeLightDescriptors(node, authoredIndex) {
8025
9110
  const name = node.name?.trim() || `aura-authored-${node.light}-${authoredIndex + 1}`;
8026
9111
  const color = colorToLinearRgb(node.color ?? "#ffffff");
@@ -8044,6 +9129,8 @@ function createProductionRuntimeLightDescriptors(node, authoredIndex) {
8044
9129
  spotAngle: 0,
8045
9130
  penumbra: 0,
8046
9131
  shadowPriority: 3,
9132
+ shadowRequested: node.shadow === true,
9133
+ shadowDisabled: node.shadow === false,
8047
9134
  authoredLight: node.light
8048
9135
  }];
8049
9136
  }
@@ -8059,12 +9146,34 @@ function createProductionRuntimeLightDescriptors(node, authoredIndex) {
8059
9146
  spotAngle: 0,
8060
9147
  penumbra: 0,
8061
9148
  shadowPriority: 1,
9149
+ shadowRequested: false,
8062
9150
  authoredLight: node.light
8063
9151
  }];
8064
9152
  }
8065
9153
  if (node.light === "studio") {
8066
9154
  return createProductionRuntimeStudioLightDescriptors(node, name, color, intensity, position);
8067
9155
  }
9156
+ if (node.light === "spot") {
9157
+ // N1 (muse3jsparity-PRD): authored spot with an explicit cone. The aim
9158
+ // target wins over lookAt; the direction feeds the B1 spot shadow path
9159
+ // and the A5 volumetric dominant-light selection.
9160
+ const spotTarget = node.target ?? node.lookAt ?? [0, 0.75, 0];
9161
+ return [{
9162
+ kind: "spot",
9163
+ name,
9164
+ color,
9165
+ intensity,
9166
+ position,
9167
+ direction: normalize3([spotTarget[0] - position[0], spotTarget[1] - position[1], spotTarget[2] - position[2]]),
9168
+ range: Math.max(1, node.distance ?? 12),
9169
+ spotAngle: clampNumber(node.angle ?? Math.PI / 6, 0.08, Math.PI / 2 - 0.01),
9170
+ penumbra: clampNumber(node.penumbra ?? 0.4, 0, 1),
9171
+ shadowPriority: 2,
9172
+ shadowRequested: node.shadow === true,
9173
+ shadowDisabled: node.shadow === false,
9174
+ authoredLight: node.light
9175
+ }];
9176
+ }
8068
9177
  const width = positiveFinite(node.width, node.light === "softbox" ? 2.4 : 2.2)
8069
9178
  * productionRuntimeLightAxisScale(node.scale, 0);
8070
9179
  const height = positiveFinite(node.height, node.light === "softbox" ? 1.6 : 1.2)
@@ -8083,6 +9192,7 @@ function createProductionRuntimeLightDescriptors(node, authoredIndex) {
8083
9192
  spotAngle: clampNumber(Math.atan2(halfDiagonal, targetDistance), 0.08, Math.PI / 2 - 0.01),
8084
9193
  penumbra: node.light === "softbox" ? 0.78 : 0.42,
8085
9194
  shadowPriority: 2,
9195
+ shadowRequested: false,
8086
9196
  authoredLight: node.light,
8087
9197
  authoredWidth: width,
8088
9198
  authoredHeight: height
@@ -8112,6 +9222,7 @@ function createProductionRuntimeStudioLightDescriptors(node, name, color, intens
8112
9222
  spotAngle: 0,
8113
9223
  penumbra: 0,
8114
9224
  shadowPriority: 3,
9225
+ shadowRequested: false,
8115
9226
  authoredLight: node.light
8116
9227
  },
8117
9228
  {
@@ -8125,6 +9236,7 @@ function createProductionRuntimeStudioLightDescriptors(node, name, color, intens
8125
9236
  spotAngle: 0,
8126
9237
  penumbra: 0,
8127
9238
  shadowPriority: 0,
9239
+ shadowRequested: false,
8128
9240
  authoredLight: node.light
8129
9241
  },
8130
9242
  {
@@ -8138,6 +9250,7 @@ function createProductionRuntimeStudioLightDescriptors(node, name, color, intens
8138
9250
  spotAngle: 0,
8139
9251
  penumbra: 0,
8140
9252
  shadowPriority: 0,
9253
+ shadowRequested: false,
8141
9254
  authoredLight: node.light
8142
9255
  }
8143
9256
  ];
@@ -8157,6 +9270,7 @@ function createProductionRuntimeFallbackLights() {
8157
9270
  spotAngle: 0,
8158
9271
  penumbra: 0,
8159
9272
  shadowPriority: 3,
9273
+ shadowRequested: false,
8160
9274
  authoredLight: "fallback"
8161
9275
  },
8162
9276
  {
@@ -8170,6 +9284,7 @@ function createProductionRuntimeFallbackLights() {
8170
9284
  spotAngle: 0,
8171
9285
  penumbra: 0,
8172
9286
  shadowPriority: 0,
9287
+ shadowRequested: false,
8173
9288
  authoredLight: "fallback"
8174
9289
  },
8175
9290
  {
@@ -8183,6 +9298,7 @@ function createProductionRuntimeFallbackLights() {
8183
9298
  spotAngle: 0,
8184
9299
  penumbra: 0,
8185
9300
  shadowPriority: 0,
9301
+ shadowRequested: false,
8186
9302
  authoredLight: "fallback"
8187
9303
  }
8188
9304
  ];
@@ -8342,6 +9458,7 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
8342
9458
  height: canvas.height,
8343
9459
  backend: "webgl2",
8344
9460
  antialias: true,
9461
+ ...(getRootRenderSource(canvas) ? { errorCheckMode: "frame" } : {}),
8345
9462
  preserveDrawingBuffer: true,
8346
9463
  // Background colors are display intent. Pre-invert the renderer's coupled
8347
9464
  // matrix-fitted ACES transform so presentation preserves that authored color.
@@ -8349,53 +9466,153 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
8349
9466
  });
8350
9467
  let latestDeviceDiagnostics = productionRenderer.getDiagnostics();
8351
9468
  let latestFeatures = productionRenderer.getFeatures();
9469
+ // M2 streaming distances measure against the live camera eye; refreshed
9470
+ // every render so residency follows the camera instead of mount intent.
9471
+ let latestCameraEye = resolveCameraFrame(snapshot, snapshot.camera, 0, runtimeNodes).eye;
8352
9472
  const runtimeWarnings = new Set();
9473
+ // C1 textured upgrade: fire-and-forget after mount. Scalar first frames stay
9474
+ // fast; outcomes land in textureUpgradeWarnings, which render() never clears
9475
+ // (per-frame runtimeWarnings are rebuilt every frame).
9476
+ const textureUpgradeWarnings = new Set();
9477
+ void upgradeProductionPrimitiveTextures(primitiveEntries, (message) => {
9478
+ textureUpgradeWarnings.add(message);
9479
+ }, Number(canvas.getContext("webgl2")?.getParameter(WebGL2RenderingContext.MAX_TEXTURE_SIZE) ?? 4096)).catch((error) => {
9480
+ textureUpgradeWarnings.add(`textured upgrade pass failed (${error instanceof Error ? error.message : String(error)}); scalar materials retained`);
9481
+ });
8353
9482
  const productionEnvironment = createProductionRuntimeEnvironment(snapshot);
9483
+ // B3 HDRI upgrade: fire-and-forget after mount. First frames render the
9484
+ // studio procedural fallback; the HDR chain swaps the lighting object the
9485
+ // render closure reads every frame. Outcomes land in hdriWarnings, which
9486
+ // render() never clears (per-frame runtimeWarnings are rebuilt every frame).
9487
+ let currentEnvironmentLighting = productionEnvironment.lighting;
9488
+ const hdriWarnings = new Set();
9489
+ const hdriState = { status: productionEnvironment.hdriUrl ? "pending" : "none" };
9490
+ let disposeHdriEnvironment = null;
9491
+ if (flattened.some((node) => node.kind === "environment" && node.environment === "hdri" && node.texture?.kind !== "aura-asset-ref")) {
9492
+ hdriWarnings.add("hdri environment has no texture asset ref; studio procedural fallback retained");
9493
+ }
9494
+ if (productionEnvironment.hdriUrl) {
9495
+ const hdriUrl = productionEnvironment.hdriUrl;
9496
+ const hdriReflectionUrl = productionEnvironment.hdriReflectionUrl;
9497
+ const hdriIntensity = productionEnvironment.intensity;
9498
+ const hdriRotation = productionEnvironment.hdriRotation;
9499
+ void upgradeProductionEnvironmentHdri(hdriUrl, hdriIntensity, hdriReflectionUrl, hdriRotation)
9500
+ .then((result) => {
9501
+ disposeHdriEnvironment?.();
9502
+ disposeHdriEnvironment = result.dispose;
9503
+ currentEnvironmentLighting = result.lighting;
9504
+ hdriState.status = "ready";
9505
+ hdriState.maxLinearValue = result.maxLinearValue;
9506
+ hdriState.specularMipCount = result.specularMipCount;
9507
+ hdriState.dualProbe = result.dualProbe;
9508
+ })
9509
+ .catch((error) => {
9510
+ hdriState.status = "fallback";
9511
+ hdriWarnings.add(`HDRI upgrade failed for ${hdriUrl} (${error instanceof Error ? error.message : String(error)}); studio procedural fallback retained`);
9512
+ });
9513
+ }
8354
9514
  const productionRuntimeLights = createProductionRuntimeCollectedLights(snapshot);
8355
9515
  const authoredLightNodes = flattened.filter((node) => node.kind === "light");
8356
9516
  const authoredDirectLightNodes = authoredLightNodes.filter((node) => node.light !== "ambient");
8357
9517
  const authoredAmbientLightCount = authoredLightNodes.length - authoredDirectLightNodes.length;
8358
9518
  const authoredAreaProxyCount = authoredDirectLightNodes.filter((node) => node.light === "rect" || node.light === "softbox").length;
8359
- const buildDiagnostics = () => createRendererDiagnosticReport(snapshot, {
8360
- mounted: true,
8361
- backend: "production-runtime",
8362
- postprocess: createProductionRuntimePostprocessObservation(latestDeviceDiagnostics),
8363
- shadow: createProductionRuntimeShadowObservation(createProductionRuntimeShadowOptions(snapshot, productionRuntimeLights), latestDeviceDiagnostics),
8364
- environment: {
8365
- enabled: true,
8366
- preset: productionEnvironment.preset,
8367
- intensity: productionEnvironment.intensity,
8368
- evidence: productionEnvironment.evidence
8369
- },
8370
- warnings: [
8371
- `Production runtime bridge active with ${actorEntries.length} typed GLB actor${actorEntries.length === 1 ? "" : "s"} and ${primitiveEntries.length} Aura primitive${primitiveEntries.length === 1 ? "" : "s"} on ${productionRenderer.backend}.`,
8372
- ...(authoredDirectLightNodes.length === 0
8373
- ? ["Production runtime direct-light fallback active: the scene has no authored directional, point, studio, rect, or softbox light."]
8374
- : [`Production runtime derived ${productionRuntimeLights.length} collected direct light${productionRuntimeLights.length === 1 ? "" : "s"} from ${authoredDirectLightNodes.length} authored scene light${authoredDirectLightNodes.length === 1 ? "" : "s"}.`]),
8375
- ...(authoredAmbientLightCount > 0
8376
- ? [`${authoredAmbientLightCount} authored ambient light${authoredAmbientLightCount === 1 ? "" : "s"} remain environment-lighting intent and are not mislabeled as direct CollectedLight entries.`]
8377
- : []),
8378
- ...(authoredAreaProxyCount > 0
8379
- ? [`${authoredAreaProxyCount} authored rect/softbox light${authoredAreaProxyCount === 1 ? "" : "s"} use bounded spot-light proxies whose cone and range derive from authored width and height; this does not claim physical area-light shading.`]
8380
- : []),
8381
- ...(flattened.some((node) => node.kind === "effect")
8382
- ? ["Effect nodes are requested in the scene graph; production bridge diagnostics report them, but unsupported postprocess/effect passes remain non-pixel-backed until the runtime feature reports support."]
8383
- : []),
8384
- ...(productionRenderer.backendSelection.fallback ? [`Production runtime backend fallback: ${productionRenderer.backendSelection.reason}`] : []),
8385
- ...latestFeatures
8386
- .filter((feature) => feature.state !== "supported")
8387
- .map((feature) => `Production runtime feature ${feature.id} is ${feature.state}: ${feature.detail}`),
8388
- ...runtimeWarnings
8389
- ],
8390
- deviceDiagnostics: latestDeviceDiagnostics,
8391
- lodSelections: primitiveEntries
8392
- .filter((entry) => Boolean(entry.node.lod?.levels.length))
8393
- .map((entry) => ({
8394
- nodeName: entry.node.name ?? "unnamed distance LOD",
8395
- levelIndex: entry.currentLodIndex,
8396
- levelName: entry.resources[entry.currentLodIndex]?.name ?? `level-${entry.currentLodIndex}`
8397
- }))
8398
- }, rendererOptions);
9519
+ // N1 + G1 + M2 observations refresh on every diagnostics read from the
9520
+ // live entries/device state — never cached intent.
9521
+ const buildDiagnostics = () => {
9522
+ const shadowObservation = createProductionRuntimeShadowObservation({ ...createProductionRuntimeShadowOptions(snapshot, productionRuntimeLights), ...(getRootPerformanceQuality(canvas) ? { size: getRootPerformanceQuality(canvas).shadowSize } : {}) }, latestDeviceDiagnostics, productionRenderer.getShadowEvidence());
9523
+ const spotCaster = productionRuntimeLights.find((light) => light.castsShadow);
9524
+ const spotAngle = spotCaster?.kind === "spot" ? spotCaster.spotAngle : undefined;
9525
+ const texturesObservation = createProductionTexturesObservation(primitiveEntries, latestCameraEye, rendererOptions?.textureBudgetBytes ?? normalizeTextureBudgetBytes(undefined));
9526
+ const streamingWarnings = texturesObservation.overBudget
9527
+ ? [`texture streaming over budget by ${texturesObservation.overBudgetBytes} bytes `
9528
+ + `(${texturesObservation.residentEntries} resident, `
9529
+ + `${texturesObservation.evictedEntries.length} evicted: ${texturesObservation.evictedEntries.join(", ") || "none"}); `
9530
+ + `raise renderer.textureBudgetBytes above ${texturesObservation.requestedBytes} bytes or move textures closer`]
9531
+ : [];
9532
+ return createRendererDiagnosticReport(snapshot, {
9533
+ mounted: true,
9534
+ backend: "production-runtime",
9535
+ postprocess: createProductionRuntimePostprocessObservation(latestDeviceDiagnostics),
9536
+ shadow: {
9537
+ ...shadowObservation,
9538
+ spot: describeProductionSpotShadow({
9539
+ requested: flattened.some((node) => node.kind === "light" && node.light === "spot" && node.shadow === true),
9540
+ casterIsSpot: spotCaster?.kind === "spot",
9541
+ ...(spotCaster === undefined ? {} : { casterName: spotCaster.source.name }),
9542
+ ...(spotAngle === undefined ? {} : { angle: spotAngle }),
9543
+ ...(spotCaster?.kind === "spot" ? { penumbra: spotCaster.penumbra, range: spotCaster.range } : {}),
9544
+ mapRendered: shadowObservation.mapRendered,
9545
+ mapSampled: shadowObservation.mapSampled
9546
+ })
9547
+ },
9548
+ text: createProductionTextObservation(primitiveEntries),
9549
+ textures: texturesObservation,
9550
+ environment: {
9551
+ enabled: true,
9552
+ preset: productionEnvironment.preset,
9553
+ intensity: productionEnvironment.intensity,
9554
+ evidence: hdriState.status === "ready"
9555
+ ? `${productionEnvironment.evidence} — HDRI chain ready${hdriState.dualProbe ? " (dual-probe: illumination diffuse + reflection specular)" : " (single probe)"} (maxLinear ${hdriState.maxLinearValue}, ${hdriState.specularMipCount} specular mips)`
9556
+ : productionEnvironment.evidence,
9557
+ // B3 iblPixelBacked: true only after the HDR chain swaps the live
9558
+ // lighting object. Procedural first frames and fallbacks never claim it.
9559
+ iblPixelBacked: hdriState.status === "ready",
9560
+ hdriStatus: hdriState.status,
9561
+ ...(hdriState.dualProbe === undefined ? {} : { dualProbe: hdriState.dualProbe }),
9562
+ ...(productionEnvironment.hdriRotation === undefined ? {} : { hdriRotation: productionEnvironment.hdriRotation })
9563
+ },
9564
+ warnings: [
9565
+ `Production runtime bridge active with ${actorEntries.length} typed GLB actor${actorEntries.length === 1 ? "" : "s"} and ${primitiveEntries.length} Aura primitive${primitiveEntries.length === 1 ? "" : "s"} on ${productionRenderer.backend}.`,
9566
+ ...(authoredDirectLightNodes.length === 0
9567
+ ? ["Production runtime direct-light fallback active: the scene has no authored directional, point, studio, rect, or softbox light."]
9568
+ : [`Production runtime derived ${productionRuntimeLights.length} collected direct light${productionRuntimeLights.length === 1 ? "" : "s"} from ${authoredDirectLightNodes.length} authored scene light${authoredDirectLightNodes.length === 1 ? "" : "s"}.`]),
9569
+ ...(authoredAmbientLightCount > 0
9570
+ ? [`${authoredAmbientLightCount} authored ambient light${authoredAmbientLightCount === 1 ? "" : "s"} remain environment-lighting intent and are not mislabeled as direct CollectedLight entries.`]
9571
+ : []),
9572
+ ...(authoredAreaProxyCount > 0
9573
+ ? [`${authoredAreaProxyCount} authored rect/softbox light${authoredAreaProxyCount === 1 ? "" : "s"} use bounded spot-light proxies whose cone and range derive from authored width and height; this does not claim physical area-light shading.`]
9574
+ : []),
9575
+ ...(flattened.some((node) => node.kind === "effect")
9576
+ ? ["Effect nodes are requested in the scene graph; production bridge diagnostics report them, but unsupported postprocess/effect passes remain non-pixel-backed until the runtime feature reports support."]
9577
+ : []),
9578
+ ...(productionRenderer.backendSelection.fallback ? [`Production runtime backend fallback: ${productionRenderer.backendSelection.reason}`] : []),
9579
+ ...latestFeatures
9580
+ .filter((feature) => feature.state !== "supported")
9581
+ .map((feature) => `Production runtime feature ${feature.id} is ${feature.state}: ${feature.detail}`),
9582
+ ...runtimeWarnings,
9583
+ ...textureUpgradeWarnings,
9584
+ ...streamingWarnings,
9585
+ ...hdriWarnings
9586
+ ],
9587
+ deviceDiagnostics: latestDeviceDiagnostics,
9588
+ lodSelections: primitiveEntries
9589
+ .filter((entry) => Boolean(entry.node.lod?.levels.length))
9590
+ .map((entry) => ({
9591
+ nodeName: entry.node.name ?? "unnamed distance LOD",
9592
+ levelIndex: entry.currentLodIndex,
9593
+ levelName: entry.resources[entry.currentLodIndex]?.name ?? `level-${entry.currentLodIndex}`
9594
+ })),
9595
+ texturedMaterials: primitiveEntries.flatMap((entry) => entry.resources.map((resource) => ({
9596
+ nodeName: entry.node.name ?? `aura-primitive-${entry.node.primitive}`,
9597
+ levelName: resource.name,
9598
+ status: resource.textureStatus,
9599
+ slots: resource.textureSlots,
9600
+ pixelBacked: resource.textureStatus === "textured" && resource.texturedMaterial !== null,
9601
+ warnings: resource.textureWarnings
9602
+ })))
9603
+ }, rendererOptions);
9604
+ };
9605
+ let preparedFrame;
9606
+ const buildFrame = (time) => {
9607
+ runtimeWarnings.clear();
9608
+ latestCameraEye = resolveCameraFrame(snapshot, snapshot.camera, time, runtimeNodes).eye;
9609
+ return createProductionRuntimeRendererInput(snapshot, canvas, actorEntries, primitiveEntries, time, runtimeNodes, runtimeWarnings, currentEnvironmentLighting, productionRuntimeLights);
9610
+ };
9611
+ const takePreparedFrame = (time) => {
9612
+ const prepared = preparedFrame?.time === time ? preparedFrame.input : undefined;
9613
+ preparedFrame = undefined;
9614
+ return prepared ?? buildFrame(time);
9615
+ };
8399
9616
  return {
8400
9617
  get backend() {
8401
9618
  return productionRenderer.backend;
@@ -8403,17 +9620,34 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
8403
9620
  get diagnostics() {
8404
9621
  return buildDiagnostics();
8405
9622
  },
9623
+ update(time) {
9624
+ // Building the frame applies controller-bound actor clips, root-motion
9625
+ // consumption, foot IK, morphs and imported-asset evidence. Discarding the
9626
+ // returned input deliberately avoids a GPU/device submission.
9627
+ preparedFrame = { time, input: buildFrame(time) };
9628
+ },
8406
9629
  render(time) {
8407
- runtimeWarnings.clear();
8408
- const input = createProductionRuntimeRendererInput(snapshot, canvas, actorEntries, primitiveEntries, time, runtimeNodes, runtimeWarnings, productionEnvironment.lighting, productionRuntimeLights);
9630
+ const input = takePreparedFrame(time);
8409
9631
  const result = productionRenderer.renderInteractiveFrame(input);
8410
9632
  latestDeviceDiagnostics = result.diagnostics;
9633
+ getRootRenderSource(canvas)?.onFrame?.(latestDeviceDiagnostics, [...(input.source.collectRenderItems?.() ?? [])]);
9634
+ latestFeatures = result.features;
9635
+ return latestDeviceDiagnostics.drawCalls;
9636
+ },
9637
+ async renderAsync(time) {
9638
+ const input = takePreparedFrame(time);
9639
+ const result = await productionRenderer.renderInteractiveFrameAsync(input);
9640
+ latestDeviceDiagnostics = result.diagnostics;
9641
+ getRootRenderSource(canvas)?.onFrame?.(latestDeviceDiagnostics, [...(input.source.collectRenderItems?.() ?? [])]);
8411
9642
  latestFeatures = result.features;
8412
9643
  return latestDeviceDiagnostics.drawCalls;
8413
9644
  },
8414
9645
  viewProjection(time) {
8415
9646
  return createViewProjection(snapshot, canvas.width / Math.max(1, canvas.height), time, runtimeNodes);
8416
9647
  },
9648
+ resetTemporalHistory(reason) {
9649
+ productionRenderer.resetTemporalHistory(reason);
9650
+ },
8417
9651
  resize(width, height) {
8418
9652
  productionRenderer.resize(width, height);
8419
9653
  },
@@ -8427,69 +9661,185 @@ async function createProductionRuntimeSceneRenderer(canvas, snapshot, rendererOp
8427
9661
  return productionRenderer.deviceLost();
8428
9662
  },
8429
9663
  dispose() {
9664
+ disposeHdriEnvironment?.();
8430
9665
  productionRenderer.dispose();
8431
9666
  for (const { actor } of actorEntries)
8432
9667
  actor.dispose();
8433
9668
  for (const { resources } of primitiveEntries) {
8434
- for (const { geometry, material } of resources) {
9669
+ for (const { geometry, material, texturedMaterial, textureDisposer } of resources) {
9670
+ textureDisposer?.();
8435
9671
  geometry.dispose();
8436
9672
  material.dispose();
9673
+ texturedMaterial?.dispose();
8437
9674
  }
8438
9675
  }
8439
9676
  }
8440
9677
  };
8441
9678
  }
8442
9679
  function createProductionRuntimeRendererInput(snapshot, canvas, actorEntries, primitiveEntries, time, runtimeNodes, runtimeWarnings, environmentLighting, collectedLights) {
8443
- const items = [];
9680
+ const compatibility = getRootRenderSource(canvas);
9681
+ const attachedItems = compatibility ? [...(compatibility.source.collectRenderItems?.() ?? compatibility.source.renderItems ?? [])] : [];
9682
+ const items = [...attachedItems];
8444
9683
  const viewProjectionMatrix = createViewProjection(snapshot, canvas.width / Math.max(1, canvas.height), time, runtimeNodes);
8445
9684
  const cameraPosition = resolveCameraFrame(snapshot, snapshot.camera, time, runtimeNodes).eye;
8446
- for (const entry of actorEntries) {
8447
- const currentState = resolveProductionActorRuntimeState(entry, runtimeNodes);
8448
- const currentNode = currentState.node;
9685
+ for (const [actorIndex, entry] of actorEntries.entries()) {
9686
+ let currentState = resolveProductionActorRuntimeState(entry, runtimeNodes);
9687
+ let currentNode = currentState.node;
8449
9688
  if (currentNode.visible === false)
8450
9689
  continue;
8451
- applyProductionActorAnimation(entry, currentNode, currentState.animationBinding, time, runtimeWarnings);
9690
+ let modelMatrix = [...createModelMatrix(currentNode, productionActorModelBounds(currentNode.asset, entry.actor), shouldNormalizeModelNode(currentNode), time)];
9691
+ // The foot-planting post-pass solves in the same world space this matrix draws into;
9692
+ // refresh its matrix before the clip plays so the solve uses this frame, not the last.
9693
+ applyProductionActorFootPlanting(entry, currentState.animationBinding, modelMatrix, runtimeWarnings);
9694
+ if (currentState.animationPose) {
9695
+ try {
9696
+ entry.actor.applyRetargetedPose(currentState.animationPose, currentState.animationPoseTime ?? time);
9697
+ entry.rootMotionCursors = undefined;
9698
+ }
9699
+ catch (error) {
9700
+ runtimeWarnings.add(`Typed GLB actor "${entry.actor.id}" failed to apply bound pose: ${productionRenderErrorMessage(error)}`);
9701
+ }
9702
+ }
9703
+ else
9704
+ applyProductionActorAnimation(entry, currentNode, currentState.animationBinding, time, runtimeWarnings, modelMatrix, () => {
9705
+ currentState = resolveProductionActorRuntimeState(entry, runtimeNodes);
9706
+ currentNode = currentState.node;
9707
+ modelMatrix = [...createModelMatrix(currentNode, productionActorModelBounds(currentNode.asset, entry.actor), shouldNormalizeModelNode(currentNode), time)];
9708
+ applyProductionActorFootPlanting(entry, currentState.animationBinding, modelMatrix, runtimeWarnings);
9709
+ });
8452
9710
  applyProductionActorMorphTargets(entry, currentState.morphTargets, runtimeWarnings);
8453
- const modelMatrix = [...createModelMatrix(currentNode, productionActorModelBounds(currentNode.asset, entry.actor), shouldNormalizeModelNode(currentNode), time)];
8454
- const actorItems = entry.actor.collectRenderItems({ modelMatrix });
8455
- items.push(...actorItems.map((item) => ({ ...item, castShadow: currentNode.castShadow })));
9711
+ // Wrinkle detail (E1 face-rig demo): resolve morph weights through the model's hook.
9712
+ // Absent hook (or empty weights) resolves to 0 = today's rendering exactly.
9713
+ const wrinkleStrength = currentNode.wrinkle
9714
+ ? resolveWrinkleMapStrength(currentState.morphTargets ?? {}, currentNode.wrinkle)
9715
+ : undefined;
9716
+ const actorItems = entry.actor.collectRenderItems({
9717
+ modelMatrix,
9718
+ ...(wrinkleStrength === undefined ? {} : { wrinkleStrength })
9719
+ });
9720
+ // P2 (muse3jsparity-PRD): model instances attach exactly like primitive
9721
+ // nodes. Skinned actors cannot instance (D1 matrix) — warn once and draw
9722
+ // single instead of silently dropping copies.
9723
+ const modelInstances = currentNode.instances;
9724
+ let modelInstanceAttach = {};
9725
+ if (modelInstances && modelInstances.length > 0) {
9726
+ if (actorItems.some((item) => item.skinning)) {
9727
+ warnOnInstancingFallback({
9728
+ material: currentNode.name ?? currentNode.asset.id,
9729
+ requestedInstances: modelInstances.length,
9730
+ drawnBatches: Math.max(1, actorItems.length),
9731
+ reason: "skinned-palette-overflow-cpu-fallback",
9732
+ onWarning: (message) => runtimeWarnings.add(message)
9733
+ });
9734
+ }
9735
+ else {
9736
+ modelInstanceAttach = {
9737
+ instanceTransforms: createProductionModelInstanceTransforms(modelInstances, currentNode, productionActorModelBounds(currentNode.asset, entry.actor), time),
9738
+ ...(currentNode.instanceColors
9739
+ ? { instanceColors: createProductionInstanceColors(currentNode.instanceColors, modelInstances.length) }
9740
+ : {})
9741
+ };
9742
+ }
9743
+ }
9744
+ if (currentNode.instancedModelWarning)
9745
+ runtimeWarnings.add(currentNode.instancedModelWarning);
9746
+ // A GLB mesh has its own authored local/world transform. Native instance
9747
+ // transforms must apply the complete normalized node placement BEFORE that
9748
+ // mesh transform, exactly like an individually mounted actor. The previous
9749
+ // base(actorRoot * mesh) * instance(actorRoot) order normalized twice and
9750
+ // rotated/scaled the instance translations in the mesh's coordinate system.
9751
+ const localActorItems = modelInstanceAttach.instanceTransforms
9752
+ ? entry.actor.collectRenderItems({ modelMatrix: identityMat4(), ...(wrinkleStrength === undefined ? {} : { wrinkleStrength }) })
9753
+ : undefined;
9754
+ items.push(...actorItems.map((item, itemIndex) => {
9755
+ const nativeMatrices = modelInstanceAttach.instanceTransforms;
9756
+ const localItem = localActorItems?.[itemIndex];
9757
+ if (nativeMatrices && !localItem)
9758
+ throw new Error("Instanced GLB mesh collection changed within one frame");
9759
+ return { ...item, label: `actor-${actorIndex}:mesh-${itemIndex}:${item.label ?? "mesh"}`, castShadow: currentNode.castShadow, ...modelInstanceAttach,
9760
+ ...(nativeMatrices && localItem ? {
9761
+ modelMatrix: identityMat4(),
9762
+ instanceTransforms: composeModelInstanceMatrices(nativeMatrices, localItem.modelMatrix ?? identityMat4())
9763
+ } : {}) };
9764
+ }));
8456
9765
  attachProductionActorEvidence(currentNode, entry.actor, actorItems, runtimeNodes);
8457
9766
  }
8458
- for (const entry of primitiveEntries) {
9767
+ // G1 SDF occlusion test, built once per frame only when SDF quads exist
9768
+ // (same cost argument as WS-2.7: one bbox per node, negligible beside render).
9769
+ const sdfEntries = primitiveEntries.filter((entry) => entry.resources.some((resource) => resource.sdfText !== null));
9770
+ const sdfOcclusionTest = sdfEntries.length > 0
9771
+ ? createSceneLabelOcclusionTest(snapshot, cameraPosition, runtimeNodes)
9772
+ : undefined;
9773
+ for (const [primitiveIndex, entry] of primitiveEntries.entries()) {
8459
9774
  const currentState = resolveProductionPrimitiveRuntimeState(entry, runtimeNodes);
8460
9775
  if (!currentState.visible)
8461
9776
  continue;
8462
- const resource = selectProductionPrimitiveResource(entry, currentState.node, cameraPosition);
9777
+ const resource = selectProductionPrimitiveResource(entry, currentState.node, cameraPosition, getRootPerformanceQuality(canvas)?.lodBias ?? 1);
9778
+ if (resource.sdfText && resource.texturedMaterial) {
9779
+ // G1 per-frame SDF opacity: LOD fade from the live camera distance
9780
+ // times the scene occlusion policy, written to the quad material.
9781
+ // Occlusion "hide" skips submission (no pixels, no backing claim).
9782
+ const position = currentState.node.position ?? [0, 0, 0];
9783
+ const distance = Math.hypot(cameraPosition[0] - position[0], cameraPosition[1] - position[1], cameraPosition[2] - position[2]);
9784
+ const frame = resolveSdfTextFrameOpacity({
9785
+ distance,
9786
+ ...(resource.sdfText.lodFadeNear === undefined ? {} : { lodFadeNear: resource.sdfText.lodFadeNear }),
9787
+ ...(resource.sdfText.lodFadeFar === undefined ? {} : { lodFadeFar: resource.sdfText.lodFadeFar }),
9788
+ occluded: sdfOcclusionTest?.(position) ?? false,
9789
+ occlusionPolicy: resource.sdfText.occlusionPolicy
9790
+ });
9791
+ resource.sdfText.lastOpacity = frame.opacity;
9792
+ resource.sdfText.lastVisible = frame.visible;
9793
+ resource.sdfText.lastSubmitted = frame.visible;
9794
+ if (!frame.visible)
9795
+ continue;
9796
+ const base = resource.texturedMaterial.getParameter("baseColor");
9797
+ const rgb = Array.isArray(base) && base.length >= 3
9798
+ ? [base[0] ?? 1, base[1] ?? 1, base[2] ?? 1]
9799
+ : [1, 1, 1];
9800
+ resource.texturedMaterial.setParameter("baseColor", [rgb[0], rgb[1], rgb[2], frame.opacity]);
9801
+ }
8463
9802
  items.push({
8464
9803
  geometry: resource.geometry,
8465
- material: resource.material,
9804
+ material: resource.texturedMaterial ?? resource.material,
8466
9805
  modelMatrix: createModelMatrix(currentState.node, resource.bounds, false, time),
8467
- label: currentState.node.name ?? `aura-primitive-${currentState.node.primitive}`,
9806
+ label: `primitive-${primitiveIndex}:${resource.name}:${currentState.node.name ?? currentState.node.primitive}`,
8468
9807
  castShadow: currentState.node.castShadow,
8469
9808
  includeInAutoFrame: false,
8470
9809
  ...(currentState.node.instances ? { instanceTransforms: createProductionInstanceTransforms(currentState.node.instances, currentState.node), instanceColors: createProductionInstanceColors(currentState.node.instanceColors, currentState.node.instances.length) } : {})
8471
9810
  });
8472
9811
  }
9812
+ const unsupportedTemporal = items.find(item => {
9813
+ const base = item.material && "baseMaterial" in item.material ? item.material.baseMaterial : item.material;
9814
+ return Boolean(item.skinning || item.morphTargets?.length || item.instanceTransforms?.length || item.geometry.topology !== "triangles" || base?.renderState.blend);
9815
+ });
9816
+ const temporalRequested = groups.flatten(snapshot.nodes).some(node => node.kind === "effect" && (node.effect === "motion-blur" || (node.effect === "anti-alias" && node.mode === "taa")));
9817
+ for (const warning of runtimeWarnings)
9818
+ if (warning.startsWith("TEMPORAL_UNSUPPORTED_GEOMETRY:"))
9819
+ runtimeWarnings.delete(warning);
9820
+ if (temporalRequested && unsupportedTemporal)
9821
+ runtimeWarnings.add(`TEMPORAL_UNSUPPORTED_GEOMETRY: ${unsupportedTemporal.label}; requires opaque rigid noninstanced triangles`);
8473
9822
  const source = {
9823
+ ...(compatibility?.source ?? {}),
8474
9824
  collectRenderItems: () => items,
8475
- cameraPolicy: "require",
8476
- staticBatching: true,
9825
+ cameraPolicy: compatibility?.source.cameraPolicy ?? "require",
9826
+ staticBatching: !temporalRequested,
8477
9827
  frustumCulling: true,
8478
- collectedLights,
8479
- environmentLighting,
9828
+ collectedLights: [...collectedLights, ...(compatibility?.source.collectedLights ?? [])],
9829
+ environmentLighting: compatibility?.source.environmentLighting ?? environmentLighting,
8480
9830
  // The production runtime owns the pixel-backed HDR target and pass chain for
8481
9831
  // routes that request effects. The diagnostics are device-observed, so a
8482
9832
  // compositor failure is reported as fallback rather than claimed as a pass.
8483
- postprocess: createProductionRuntimePostprocess(snapshot),
8484
- shadow: createProductionRuntimeShadowOptions(snapshot, collectedLights),
8485
- environmentFog: createProductionRuntimeEnvironmentFog(snapshot),
8486
- cameraPosition
9833
+ postprocess: compatibility?.source.postprocess ?? createProductionRuntimePostprocess(snapshot, collectedLights, canvas.width, canvas.height, !unsupportedTemporal),
9834
+ shadow: { ...createProductionRuntimeShadowOptions(snapshot, collectedLights), ...(getRootPerformanceQuality(canvas) ? { size: getRootPerformanceQuality(canvas).shadowSize } : {}) },
9835
+ environmentFog: compatibility?.source.environmentFog ?? createProductionRuntimeEnvironmentFog(snapshot, collectedLights, canvas.width, canvas.height),
9836
+ ...(compatibility?.source.cameraPolicy === "auto-frame" ? {} : { cameraPosition })
8487
9837
  };
8488
9838
  const cameraLike = { viewProjectionMatrix };
8489
9839
  return {
8490
9840
  source,
8491
- camera: cameraLike,
8492
- metadata: createProductionRuntimeMetadata(actorEntries, primitiveEntries)
9841
+ ...(compatibility?.source.cameraPolicy === "auto-frame" ? {} : { camera: cameraLike }),
9842
+ metadata: includeRootSourceMetadata(createProductionRuntimeMetadata(actorEntries, primitiveEntries), attachedItems)
8493
9843
  };
8494
9844
  }
8495
9845
  function createProductionRuntimePrimitiveEntries(nodes) {
@@ -8497,10 +9847,610 @@ function createProductionRuntimePrimitiveEntries(nodes) {
8497
9847
  .filter((node) => node.kind === "primitive")
8498
9848
  .map((node) => ({ node, resources: createProductionPrimitiveResources(node), currentLodIndex: 0 }));
8499
9849
  }
9850
+ function blankProductionPrimitiveTextureState() {
9851
+ return { texturedMaterial: null, textureStatus: "none", textureSlots: [], textureWarnings: [], sdfText: null, textureBytes: 0, textureMipBytes: [] };
9852
+ }
9853
+ /**
9854
+ * C1 texture intent classification (pure, unit-tested). Asset refs resolve to
9855
+ * fetchable urls; procedural inputs have no rasterizer and are reported so
9856
+ * the caller can warn instead of silently dropping them.
9857
+ */
9858
+ const ROOT_EXTENSION_TEXTURE_SLOTS = ["clearcoat", "clearcoatRoughness", "clearcoatNormal", "sheenColor", "sheenRoughness", "iridescence", "iridescenceThickness", "anisotropy"];
9859
+ export function createProductionPrimitiveTextureIntent(materialSpec) {
9860
+ const refUrl = (input) => input && typeof input === "object" && input.kind === "aura-asset-ref"
9861
+ ? input.url ?? undefined
9862
+ : undefined;
9863
+ const procedurals = [];
9864
+ const noteProcedural = (slot, input) => {
9865
+ if (input && typeof input === "object" && input.kind === "aura-procedural-texture") {
9866
+ procedurals.push(`${slot}:${input.texture}`);
9867
+ }
9868
+ };
9869
+ noteProcedural("normal", materialSpec?.normal);
9870
+ noteProcedural("roughnessMap", materialSpec?.roughnessMap);
9871
+ noteProcedural("metalnessMap", materialSpec?.metalnessMap);
9872
+ noteProcedural("occlusionMap", materialSpec?.occlusionMap);
9873
+ noteProcedural("emissiveMap", materialSpec?.emissiveMap);
9874
+ const baseColorUrl = refUrl(materialSpec?.texture);
9875
+ const normalUrl = refUrl(materialSpec?.normal);
9876
+ const roughnessUrl = refUrl(materialSpec?.roughnessMap);
9877
+ const metalnessUrl = refUrl(materialSpec?.metalnessMap);
9878
+ const occlusionUrl = refUrl(materialSpec?.occlusionMap);
9879
+ const emissiveUrl = refUrl(materialSpec?.emissiveMap);
9880
+ const extensionMaps = Object.fromEntries(ROOT_EXTENSION_TEXTURE_SLOTS.flatMap((slot) => {
9881
+ const input = materialSpec?.[`${slot}Map`];
9882
+ if (input === undefined)
9883
+ return [];
9884
+ if (!input || typeof input !== "object" || input.kind !== "aura-asset-ref" || input.type !== "texture") {
9885
+ throw new AuraRuntimeError("unsupported-texture", `${slot}Map requires a typed texture asset reference`);
9886
+ }
9887
+ if (!["png", "jpg", "jpeg", "webp"].includes(input.format) || !input.url) {
9888
+ throw new AuraRuntimeError("unsupported-texture", `${slot}Map requires a fetchable png, jpg, jpeg or webp texture`);
9889
+ }
9890
+ const texCoord = materialSpec?.texCoords?.[slot] ?? 0;
9891
+ if (texCoord !== 0 && texCoord !== 1)
9892
+ throw new RangeError(`${slot} texCoord must be 0 or 1`);
9893
+ const transform = materialSpec?.texTransforms?.[slot];
9894
+ if (transform && [...(transform.offset ?? [0, 0]), ...(transform.scale ?? [1, 1]), transform.rotation ?? 0].some((value) => !Number.isFinite(value))) {
9895
+ throw new RangeError(`${slot} texture transform must contain finite values`);
9896
+ }
9897
+ return [[slot, { url: input.url, colorSpace: slot === "sheenColor" ? "srgb" : "linear", texCoord, ...(transform ? { transform } : {}) }]];
9898
+ }));
9899
+ return {
9900
+ ...(baseColorUrl ? { baseColorUrl } : {}),
9901
+ ...(normalUrl ? { normalUrl } : {}),
9902
+ ...(roughnessUrl ? { roughnessUrl } : {}),
9903
+ ...(metalnessUrl ? { metalnessUrl } : {}),
9904
+ ...(occlusionUrl ? { occlusionUrl } : {}),
9905
+ ...(emissiveUrl ? { emissiveUrl } : {}),
9906
+ ...(Object.keys(extensionMaps).length ? { extensionMaps } : {}),
9907
+ proceduralInputs: procedurals
9908
+ };
9909
+ }
9910
+ /**
9911
+ * C1 metallic-roughness compositing (pure, unit-tested). glTF convention:
9912
+ * R = occlusion (unused here, forced to 255), G = roughness, B = metallic.
9913
+ * Missing channels fall back to the scalar spec values.
9914
+ */
9915
+ const productionPrimitiveBitmapPixels = new WeakMap();
9916
+ async function loadProductionPrimitiveBitmap(url) {
9917
+ if (typeof fetch !== "function" || typeof createImageBitmap !== "function") {
9918
+ throw new Error("texture fetch requires fetch + createImageBitmap (browser production mount)");
9919
+ }
9920
+ const response = await fetch(url);
9921
+ if (!response.ok)
9922
+ throw new Error(`texture fetch failed with status ${response.status} for ${url}`);
9923
+ const blob = await response.blob();
9924
+ const bitmap = await createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" });
9925
+ try {
9926
+ const { decodePngTexturePixels } = await import("./PngTexturePixels.js");
9927
+ const pixels = await decodePngTexturePixels(new Uint8Array(await blob.arrayBuffer()));
9928
+ if (pixels)
9929
+ productionPrimitiveBitmapPixels.set(bitmap, pixels);
9930
+ return bitmap;
9931
+ }
9932
+ catch (error) {
9933
+ bitmap.close();
9934
+ throw error;
9935
+ }
9936
+ }
9937
+ function bitmapRgbaPixels(bitmap) {
9938
+ const decoded = productionPrimitiveBitmapPixels.get(bitmap);
9939
+ if (decoded)
9940
+ return decoded;
9941
+ if (typeof document === "undefined")
9942
+ throw new Error("texture compositing requires a DOM canvas");
9943
+ const canvas = document.createElement("canvas");
9944
+ canvas.width = bitmap.width;
9945
+ canvas.height = bitmap.height;
9946
+ const context = canvas.getContext("2d", { willReadFrequently: true });
9947
+ if (!context)
9948
+ throw new Error("2d canvas unavailable for texture compositing");
9949
+ context.drawImage(bitmap, 0, 0);
9950
+ const image = context.getImageData(0, 0, bitmap.width, bitmap.height);
9951
+ return { width: bitmap.width, height: bitmap.height, data: new Uint8Array(image.data) };
9952
+ }
9953
+ /**
9954
+ * C1 post-mount textured upgrade (muse3jsparity-PRD). Runs fire-and-forget
9955
+ * after mount: scalar first frames stay fast and honest, and every outcome —
9956
+ * textured, fallback, or skipped — is recorded on the resource with warnings.
9957
+ * Procedural inputs have no rasterizer: recorded + warned, never faked.
9958
+ */
9959
+ export async function upgradeProductionPrimitiveTextures(entries, warn, maxTextureSize = 4096) {
9960
+ for (const entry of entries) {
9961
+ for (const resource of entry.resources) {
9962
+ const spec = resource.materialSpec;
9963
+ const intent = createProductionPrimitiveTextureIntent(spec);
9964
+ for (const procedural of intent.proceduralInputs) {
9965
+ // Static channel (collectGeneratedCodeWarnings) already warns; record
9966
+ // per-resource without duplicating into runtime warnings.
9967
+ resource.textureWarnings.push(`procedural texture ${procedural} on "${resource.name}" has no rasterizer; recorded only, scalar material retained`);
9968
+ }
9969
+ const urls = [intent.baseColorUrl, intent.normalUrl, intent.roughnessUrl, intent.metalnessUrl, intent.occlusionUrl, intent.emissiveUrl, ...Object.values(intent.extensionMaps ?? {}).map((map) => map.url)].filter((url) => typeof url === "string" && url.length > 0);
9970
+ if (urls.length === 0)
9971
+ continue;
9972
+ if (resource.textureStatus !== "none")
9973
+ continue;
9974
+ resource.textureStatus = "pending";
9975
+ try {
9976
+ await upgradeProductionPrimitiveResource(resource, resource.sourceNode, spec, intent, maxTextureSize);
9977
+ }
9978
+ catch (error) {
9979
+ resource.textureStatus = "fallback";
9980
+ const message = `textured upgrade failed for "${resource.name}" (${error instanceof Error ? error.message : String(error)}); scalar material retained`;
9981
+ resource.textureWarnings.push(message);
9982
+ warn(message);
9983
+ }
9984
+ }
9985
+ }
9986
+ }
9987
+ /**
9988
+ * B3 post-mount HDRI upgrade (muse3jsparity-PRD). Fetches a Radiance `.hdr`
9989
+ * asset, runs the HDR→cubemap→GGX-prefilter→BRDF-LUT chain, and returns the
9990
+ * live lighting object plus its disposal. Throws on fetch/parse failure so
9991
+ * the caller keeps the honest procedural fallback and warns.
9992
+ */
9993
+ export async function upgradeProductionEnvironmentHdri(url, intensity, reflectionUrl, rotation) {
9994
+ if (typeof fetch !== "function") {
9995
+ throw new Error("HDRI upgrade requires fetch (browser production mount)");
9996
+ }
9997
+ const fetchRadiance = async (assetUrl) => {
9998
+ const response = await fetch(assetUrl);
9999
+ if (!response.ok)
10000
+ throw new Error(`HDRI fetch failed with status ${response.status} for ${assetUrl}`);
10001
+ const bytes = new Uint8Array(await response.arrayBuffer());
10002
+ if (bytes.length === 0)
10003
+ throw new Error(`HDRI asset is empty for ${assetUrl}`);
10004
+ return bytes;
10005
+ };
10006
+ const clampIntensity = Math.max(0, intensity);
10007
+ const illumination = createProductionPbrHdrPipelineFromRadiance(await fetchRadiance(url), {
10008
+ id: "root-hdri-environment",
10009
+ label: "Root HDRI environment",
10010
+ intensity: clampIntensity,
10011
+ ...(rotation !== undefined ? { rotation } : {})
10012
+ });
10013
+ if (!illumination.diagnostics.realRadianceHdr)
10014
+ throw new Error(`HDRI asset did not parse as Radiance HDR for ${url}`);
10015
+ if (!reflectionUrl) {
10016
+ const resources = createProductionEnvironmentLightingResources(illumination);
10017
+ return {
10018
+ lighting: resources.lighting,
10019
+ dispose: resources.dispose,
10020
+ maxLinearValue: illumination.diagnostics.maxLinearValue,
10021
+ specularMipCount: illumination.diagnostics.specularMipCount,
10022
+ dualProbe: false
10023
+ };
10024
+ }
10025
+ const reflection = createProductionPbrHdrPipelineFromRadiance(await fetchRadiance(reflectionUrl), {
10026
+ id: "root-hdri-reflection-environment",
10027
+ label: "Root HDRI reflection environment",
10028
+ intensity: clampIntensity,
10029
+ ...(rotation !== undefined ? { rotation } : {})
10030
+ });
10031
+ if (!reflection.diagnostics.realRadianceHdr)
10032
+ throw new Error(`HDRI reflection asset did not parse as Radiance HDR for ${reflectionUrl}`);
10033
+ const dual = createDualProbeEnvironmentLightingResources({ illumination, reflection });
10034
+ return {
10035
+ lighting: dual.lighting,
10036
+ dispose: dual.dispose,
10037
+ maxLinearValue: Math.max(illumination.diagnostics.maxLinearValue, reflection.diagnostics.maxLinearValue),
10038
+ specularMipCount: reflection.diagnostics.specularMipCount,
10039
+ dualProbe: true
10040
+ };
10041
+ }
10042
+ async function upgradeProductionPrimitiveResource(resource, node, spec, intent, maxTextureSize) {
10043
+ const fail = (message) => new Error(message);
10044
+ if (!resource.geometry.vertexBuffer.format.hasAttribute("uv")) {
10045
+ throw fail(`"${resource.name}" geometry carries no uv set; textured upgrade needs generated uvs`);
10046
+ }
10047
+ const scalars = resolveProductionPrimitiveScalars(node);
10048
+ const extensionEntries = Object.entries(intent.extensionMaps ?? {});
10049
+ const needsScalarAtlas = extensionEntries.some(([slot]) => slot.startsWith("clearcoat"))
10050
+ && extensionEntries.some(([slot]) => slot.startsWith("sheen") || slot === "anisotropy")
10051
+ && extensionEntries.some(([slot]) => slot.startsWith("iridescence"));
10052
+ if (extensionEntries.some(([, map]) => map.texCoord === 1) && !resource.geometry.vertexBuffer.format.hasAttribute("uv1")) {
10053
+ throw fail(`"${resource.name}" geometry carries no uv1 set requested by extension texture`);
10054
+ }
10055
+ const ownedBitmaps = [];
10056
+ const ownedTextures = [];
10057
+ const bitmapLoads = new Map();
10058
+ const load = (url) => {
10059
+ let pending = bitmapLoads.get(url);
10060
+ if (!pending) {
10061
+ pending = loadProductionPrimitiveBitmap(url).then((bitmap) => {
10062
+ ownedBitmaps.push(bitmap);
10063
+ return bitmap;
10064
+ });
10065
+ bitmapLoads.set(url, pending);
10066
+ }
10067
+ return pending;
10068
+ };
10069
+ const own = (texture) => { ownedTextures.push(texture); return texture; };
10070
+ const disposeTextures = () => {
10071
+ for (const texture of ownedTextures)
10072
+ texture.dispose();
10073
+ for (const bitmap of ownedBitmaps)
10074
+ bitmap.close();
10075
+ ownedTextures.length = 0;
10076
+ ownedBitmaps.length = 0;
10077
+ };
10078
+ try {
10079
+ const baseResults = await Promise.allSettled([
10080
+ intent.baseColorUrl ? load(intent.baseColorUrl) : Promise.resolve(undefined),
10081
+ intent.normalUrl ? load(intent.normalUrl) : Promise.resolve(undefined),
10082
+ intent.roughnessUrl ? load(intent.roughnessUrl) : Promise.resolve(undefined),
10083
+ intent.metalnessUrl ? load(intent.metalnessUrl) : Promise.resolve(undefined),
10084
+ intent.occlusionUrl ? load(intent.occlusionUrl) : Promise.resolve(undefined),
10085
+ intent.emissiveUrl ? load(intent.emissiveUrl) : Promise.resolve(undefined)
10086
+ ]);
10087
+ const failure = baseResults.find((result) => result.status === "rejected");
10088
+ if (failure?.status === "rejected")
10089
+ throw failure.reason;
10090
+ const [baseColorSource, normalSource, roughnessSource, metalnessSource, occlusionSource, emissiveSource] = baseResults.map((result) => result.status === "fulfilled" ? result.value : undefined);
10091
+ const slots = [];
10092
+ const extensionOptions = {};
10093
+ const extensionSources = [];
10094
+ const atlasPixels = {};
10095
+ for (const [slot, map] of extensionEntries) {
10096
+ const source = await load(map.url);
10097
+ extensionSources.push(source);
10098
+ if (needsScalarAtlas && ["clearcoat", "clearcoatRoughness", "sheenRoughness", "iridescence", "iridescenceThickness"].includes(slot)) {
10099
+ Object.assign(atlasPixels, { [slot]: bitmapRgbaPixels(source) });
10100
+ }
10101
+ extensionOptions[`${slot}Texture`] = own(new Texture({ width: source.width, height: source.height, source, colorSpace: map.colorSpace, label: `${resource.name}-${slot}` }));
10102
+ if (map.transform)
10103
+ extensionOptions[`${slot}TextureTransform`] = { ...map.transform };
10104
+ slots.push(slot);
10105
+ }
10106
+ if (needsScalarAtlas) {
10107
+ const { createExtensionScalarAtlas } = await import("../../rendering/extension-scalar-atlas.js");
10108
+ const atlas = createExtensionScalarAtlas(atlasPixels, maxTextureSize);
10109
+ own(atlas.texture);
10110
+ extensionOptions.extensionScalarAtlas = atlas;
10111
+ }
10112
+ const baseColorTexture = baseColorSource
10113
+ ? own(new Texture({ width: baseColorSource.width, height: baseColorSource.height, source: baseColorSource, colorSpace: "srgb", label: `${resource.name}-basecolor` }))
10114
+ : undefined;
10115
+ if (baseColorTexture)
10116
+ slots.push("baseColor");
10117
+ const normalTexture = normalSource
10118
+ ? own(new Texture({ width: normalSource.width, height: normalSource.height, source: normalSource, colorSpace: "linear", label: `${resource.name}-normal` }))
10119
+ : undefined;
10120
+ if (normalTexture)
10121
+ slots.push("normal");
10122
+ let metallicRoughnessTexture;
10123
+ if (roughnessSource ?? metalnessSource) {
10124
+ const base = roughnessSource ?? metalnessSource;
10125
+ const size = { width: base.width, height: base.height };
10126
+ const readPixels = (bitmap) => {
10127
+ if (!bitmap)
10128
+ return undefined;
10129
+ const decoded = bitmapRgbaPixels(bitmap);
10130
+ if (decoded.width !== size.width || decoded.height !== size.height) {
10131
+ const canvas = document.createElement("canvas");
10132
+ canvas.width = size.width;
10133
+ canvas.height = size.height;
10134
+ const context = canvas.getContext("2d", { willReadFrequently: true });
10135
+ if (!context)
10136
+ throw fail("2d canvas unavailable for texture compositing");
10137
+ context.drawImage(bitmap, 0, 0, size.width, size.height);
10138
+ return new Uint8Array(context.getImageData(0, 0, size.width, size.height).data);
10139
+ }
10140
+ return decoded.data;
10141
+ };
10142
+ const composited = compositeMetallicRoughnessPixels(readPixels(roughnessSource), readPixels(metalnessSource), size.width * size.height, spec?.roughness ?? 0.58, spec?.metallic ?? spec?.metalness ?? 0);
10143
+ metallicRoughnessTexture = own(new Texture({ width: size.width, height: size.height, data: composited, colorSpace: "linear", label: `${resource.name}-metallicroughness` }));
10144
+ slots.push("metallicRoughness");
10145
+ }
10146
+ const occlusionTexture = occlusionSource
10147
+ ? own(new Texture({ width: occlusionSource.width, height: occlusionSource.height, source: occlusionSource, colorSpace: "linear", label: `${resource.name}-occlusion` }))
10148
+ : undefined;
10149
+ if (occlusionTexture)
10150
+ slots.push("occlusion");
10151
+ const emissiveTexture = emissiveSource
10152
+ ? own(new Texture({ width: emissiveSource.width, height: emissiveSource.height, source: emissiveSource, colorSpace: "srgb", label: `${resource.name}-emissive` }))
10153
+ : undefined;
10154
+ if (emissiveTexture)
10155
+ slots.push("emissive");
10156
+ if (slots.length === 0)
10157
+ throw fail(`no texture resolved for "${resource.name}"`);
10158
+ // M2 streaming table: resident bytes from the decoded sources (base +
10159
+ // full mip-chain estimate) with the chain keyed off the largest source.
10160
+ const loadedDims = [baseColorSource, normalSource, roughnessSource, metalnessSource, occlusionSource, emissiveSource, ...extensionSources]
10161
+ .filter((source) => source !== undefined)
10162
+ .map((source) => ({ width: source.width, height: source.height }));
10163
+ const loadedBaseBytes = loadedDims.reduce((total, dims) => total + dims.width * dims.height * 4, 0);
10164
+ const largestDims = loadedDims.reduce((largest, dims) => (dims.width * dims.height > largest.width * largest.height ? dims : largest), { width: 1, height: 1 });
10165
+ resource.textureBytes = Math.round(loadedBaseBytes * (4 / 3));
10166
+ resource.textureMipBytes = mipChainBytesCoarseToFine(largestDims.width, largestDims.height);
10167
+ const texCoords = spec?.texCoords;
10168
+ // C3: every root textured slot shares one capability-gated sampler request
10169
+ // (default 8x where supported; the renderer clamps to the device maximum).
10170
+ const textureSampler = new Sampler({
10171
+ maxAnisotropy: resolveSamplerAnisotropy({ desired: spec?.textureAnisotropy }).applied
10172
+ });
10173
+ for (const [slot] of extensionEntries)
10174
+ extensionOptions[`${slot}Sampler`] = textureSampler;
10175
+ resource.texturedMaterial = new TexturedPBRMaterial({
10176
+ name: `a3d-production-textured-primitive-${resource.name}`,
10177
+ baseColor: [scalars.baseColor[0], scalars.baseColor[1], scalars.baseColor[2], scalars.opacity],
10178
+ metallic: clamp01(spec?.metallic ?? spec?.metalness ?? 0),
10179
+ roughness: clamp01(spec?.roughness ?? 0.58),
10180
+ emissiveColor: spec?.emissive
10181
+ ? [scalars.emissiveColor[0], scalars.emissiveColor[1], scalars.emissiveColor[2]]
10182
+ : emissiveTexture ? [1, 1, 1] : [scalars.emissiveColor[0], scalars.emissiveColor[1], scalars.emissiveColor[2]],
10183
+ emissiveStrength: Math.max(0, spec?.emissiveIntensity ?? (spec?.emissive || emissiveTexture ? 1.35 : 0)),
10184
+ occlusionStrength: clamp01(spec?.occlusionStrength ?? 1),
10185
+ clearcoatFactor: scalars.clearcoat,
10186
+ clearcoatRoughnessFactor: clamp01(spec?.clearcoatRoughness ?? 0.34),
10187
+ clearcoatNormalScale: Math.max(0, spec?.clearcoatNormalScale ?? 1),
10188
+ ...extensionOptions,
10189
+ sheenColorFactor: [scalars.sheenColor[0], scalars.sheenColor[1], scalars.sheenColor[2]],
10190
+ sheenRoughnessFactor: clamp01(spec?.sheenRoughness ?? 0.3),
10191
+ anisotropyStrength: scalars.anisotropy,
10192
+ anisotropyRotation: spec?.anisotropyRotation ?? 0,
10193
+ iridescenceFactor: scalars.iridescence,
10194
+ iridescenceIor: Math.max(1, spec?.iridescenceIOR ?? 1.3),
10195
+ ...(scalars.thicknessRange ? { iridescenceThicknessMinimum: scalars.thicknessRange[0], iridescenceThicknessMaximum: scalars.thicknessRange[1] } : {}),
10196
+ transmissionFactor: scalars.transmission,
10197
+ ...(spec?.thickness === undefined ? {} : { volumeThicknessFactor: Math.max(0, spec.thickness) }),
10198
+ ...(spec?.ior === undefined ? {} : { ior: Math.max(1, spec.ior) }),
10199
+ ...(spec?.attenuationColor ? { volumeAttenuationColor: colorToLinearRgb(spec.attenuationColor) } : {}),
10200
+ ...(spec?.attenuationDistance === undefined ? {} : { volumeAttenuationDistance: Math.max(0, spec.attenuationDistance) }),
10201
+ environmentIntensity: scalars.environmentIntensity,
10202
+ envMapIntensity: scalars.envMapIntensity,
10203
+ renderState: {
10204
+ blend: scalars.opacity < 0.999,
10205
+ depthWrite: scalars.opacity >= 0.999,
10206
+ cullMode: node.primitive === "plane" || scalars.opacity < 0.999 ? "none" : "back"
10207
+ },
10208
+ ...(baseColorTexture ? { baseColorTexture, baseColorSampler: textureSampler } : {}),
10209
+ ...(normalTexture ? { normalTexture, normalSampler: textureSampler, normalScale: spec?.normalScale ?? 1 } : {}),
10210
+ ...(metallicRoughnessTexture ? { metallicRoughnessTexture, metallicRoughnessSampler: textureSampler } : {}),
10211
+ ...(occlusionTexture ? { occlusionTexture, occlusionSampler: textureSampler } : {}),
10212
+ ...(emissiveTexture ? { emissiveTexture, emissiveSampler: textureSampler } : {}),
10213
+ ...(spec?.texTransforms?.baseColor ? { baseColorTextureTransform: { ...spec.texTransforms.baseColor } } : {}),
10214
+ ...(spec?.texTransforms?.normal ? { normalTextureTransform: { ...spec.texTransforms.normal } } : {}),
10215
+ ...(spec?.texTransforms?.metallicRoughness ? { metallicRoughnessTextureTransform: { ...spec.texTransforms.metallicRoughness } } : {}),
10216
+ ...(spec?.texTransforms?.occlusion ? { occlusionTextureTransform: { ...spec.texTransforms.occlusion } } : {}),
10217
+ ...(spec?.texTransforms?.emissive ? { emissiveTextureTransform: { ...spec.texTransforms.emissive } } : {}),
10218
+ ...((texCoords?.baseColor ?? 0) > 0 || (texCoords?.normal ?? 0) > 0 || (texCoords?.metallicRoughness ?? 0) > 0 || (texCoords?.occlusion ?? 0) > 0 || (texCoords?.emissive ?? 0) > 0 || extensionEntries.length > 0
10219
+ ? {
10220
+ textureTexCoords: {
10221
+ ...Object.fromEntries(extensionEntries.map(([slot, map]) => [slot, map.texCoord])),
10222
+ ...(baseColorTexture && (texCoords?.baseColor ?? 0) > 0 ? { baseColor: 1 } : {}),
10223
+ ...(normalTexture && (texCoords?.normal ?? 0) > 0 ? { normal: 1 } : {}),
10224
+ ...(metallicRoughnessTexture && (texCoords?.metallicRoughness ?? 0) > 0 ? { metallicRoughness: 1 } : {}),
10225
+ ...(occlusionTexture && (texCoords?.occlusion ?? 0) > 0 ? { occlusion: 1 } : {}),
10226
+ ...(emissiveTexture && (texCoords?.emissive ?? 0) > 0 ? { emissive: 1 } : {})
10227
+ }
10228
+ }
10229
+ : {})
10230
+ });
10231
+ if (resource.material.disposed) {
10232
+ resource.texturedMaterial.dispose();
10233
+ resource.texturedMaterial = null;
10234
+ throw fail("primitive disposed while texture upgrade was pending");
10235
+ }
10236
+ resource.textureDisposer = disposeTextures;
10237
+ resource.textureStatus = "textured";
10238
+ resource.textureSlots = slots;
10239
+ }
10240
+ catch (error) {
10241
+ disposeTextures();
10242
+ throw error;
10243
+ }
10244
+ }
10245
+ export function compositeMetallicRoughnessPixels(rough, metal, pixelCount, roughnessScalar, metalnessScalar) {
10246
+ const out = new Uint8Array(pixelCount * 4);
10247
+ const fallbackG = Math.round(clamp01(roughnessScalar) * 255);
10248
+ const fallbackB = Math.round(clamp01(metalnessScalar) * 255);
10249
+ for (let pixel = 0; pixel < pixelCount; pixel += 1) {
10250
+ const base = pixel * 4;
10251
+ out[base] = 255;
10252
+ out[base + 1] = rough ? rough[base + 1] ?? fallbackG : fallbackG;
10253
+ out[base + 2] = metal ? metal[base + 2] ?? fallbackB : fallbackB;
10254
+ out[base + 3] = 255;
10255
+ }
10256
+ return out;
10257
+ }
10258
+ /**
10259
+ * G1 SDF text resource (muse3jsparity-PRD): replays the recorded descriptor
10260
+ * through the atlas sampler at mount, uploads the label image as a native
10261
+ * texture, and submits atlas-derived quads. Returns null (extruded fallback)
10262
+ * with a warning when the sampler cannot run — never a silent mesh swap.
10263
+ */
10264
+ function createSdfTextPrimitiveResource(node, warn) {
10265
+ const text = node.text3D;
10266
+ if (!text || text.backend !== "sdf")
10267
+ return null;
10268
+ try {
10269
+ const layout = layoutSdfText(text.text, rootSdfFontAtlas(), {
10270
+ size: text.sdfSize ?? 1,
10271
+ ...(text.sdfLetterSpacing === undefined ? {} : { letterSpacing: text.sdfLetterSpacing }),
10272
+ ...(text.sdfStyle === undefined ? {} : { style: text.sdfStyle })
10273
+ });
10274
+ const style = layout.style;
10275
+ const styled = text.sdfStyle !== undefined && (style.outlineWidthEm > 0 || style.glowRadiusEm > 0 || style.shadowOffsetEm !== undefined);
10276
+ const image = rasterizeSdfTextLabelImage(layout, rootSdfFontAtlas(), {
10277
+ texelsPerWorldUnit: 64,
10278
+ ...(styled
10279
+ ? {
10280
+ outline: [1, 0.85, 0.6, 1],
10281
+ glow: [0.5, 0.8, 1, 1],
10282
+ shadow: [0, 0, 0, 1]
10283
+ }
10284
+ : {})
10285
+ });
10286
+ const mesh = createSdfTextQuadMesh(layout, image);
10287
+ const vertexCount = mesh.vertexCount;
10288
+ const vertices = new VertexBuffer(VertexFormat.P3N3T4T2T2, vertexCount);
10289
+ for (let index = 0; index < vertexCount; index += 1) {
10290
+ vertices.setAttribute(index, "position", [mesh.positions[index * 3] ?? 0, mesh.positions[index * 3 + 1] ?? 0, 0]);
10291
+ vertices.setAttribute(index, "normal", [0, 0, 1]);
10292
+ vertices.setAttribute(index, "tangent", [1, 0, 0, 1]);
10293
+ vertices.setAttribute(index, "uv", [mesh.uvs[index * 2] ?? 0, mesh.uvs[index * 2 + 1] ?? 0]);
10294
+ vertices.setAttribute(index, "uv1", [mesh.uvs[index * 2] ?? 0, mesh.uvs[index * 2 + 1] ?? 0]);
10295
+ }
10296
+ const geometry = new Geometry(vertices, new IndexBuffer(mesh.indices, vertexCount), "triangles", { min: [mesh.min[0], mesh.min[1], mesh.min[2]], max: [mesh.max[0], mesh.max[1], mesh.max[2]] });
10297
+ const opacity = clamp01(node.material?.opacity ?? 1);
10298
+ const labelTexture = new Texture({
10299
+ width: image.width,
10300
+ height: image.height,
10301
+ data: image.data,
10302
+ colorSpace: "srgb",
10303
+ label: `${node.name ?? "sdf-text"}-sdf-label`
10304
+ });
10305
+ const sampler = new Sampler({ maxAnisotropy: resolveSamplerAnisotropy({ desired: node.material?.textureAnisotropy }).applied });
10306
+ const texturedMaterial = new TexturedPBRMaterial({
10307
+ name: `a3d-production-sdf-text-${node.name ?? "label"}`,
10308
+ baseColor: [1, 1, 1, opacity],
10309
+ metallic: 0,
10310
+ roughness: 0.9,
10311
+ emissiveColor: [0.92, 0.92, 0.92],
10312
+ emissiveStrength: 0.9,
10313
+ occlusionStrength: 0,
10314
+ environmentIntensity: 0,
10315
+ renderState: { blend: true, depthWrite: false, cullMode: "none" },
10316
+ baseColorTexture: labelTexture,
10317
+ baseColorSampler: sampler,
10318
+ emissiveTexture: labelTexture,
10319
+ emissiveSampler: sampler
10320
+ });
10321
+ const mipBytes = mipChainBytesCoarseToFine(image.width, image.height);
10322
+ return {
10323
+ geometry,
10324
+ material: createProductionPrimitiveMaterial(node),
10325
+ bounds: { min: [mesh.min[0], mesh.min[1], mesh.min[2]], max: [mesh.max[0], mesh.max[1], mesh.max[2]] },
10326
+ name: node.name ?? "sdf-text",
10327
+ materialSpec: node.material,
10328
+ sourceNode: node,
10329
+ ...blankProductionPrimitiveTextureState(),
10330
+ texturedMaterial,
10331
+ textureStatus: "textured",
10332
+ textureSlots: ["baseColor", "emissive"],
10333
+ textureWarnings: [],
10334
+ sdfText: {
10335
+ quadCount: mesh.quadCount,
10336
+ imageBytes: image.width * image.height * 4,
10337
+ ...(style.lodFadeNear === undefined ? {} : { lodFadeNear: style.lodFadeNear }),
10338
+ ...(style.lodFadeFar === undefined ? {} : { lodFadeFar: style.lodFadeFar }),
10339
+ occlusionPolicy: (text.sdfOcclusion ?? "dim"),
10340
+ lastOpacity: opacity,
10341
+ lastVisible: true,
10342
+ lastSubmitted: false
10343
+ },
10344
+ textureBytes: Math.round(image.width * image.height * 4 * (4 / 3)),
10345
+ textureMipBytes: mipBytes
10346
+ };
10347
+ }
10348
+ catch (error) {
10349
+ warn(`SDF text sampler failed for "${node.name ?? "sdf-text"}" (${error instanceof Error ? error.message : String(error)}); extruded mesh fallback retained`);
10350
+ return null;
10351
+ }
10352
+ }
10353
+ /**
10354
+ * M2 mip-chain byte estimate (pure, unit-tested): full chain from the base
10355
+ * level, coarse-to-fine, RGBA8. Matches the GPU residency the bridge funds.
10356
+ */
10357
+ export function mipChainBytesCoarseToFine(width, height) {
10358
+ if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
10359
+ throw new Error("Mip-chain bytes require positive integer dimensions.");
10360
+ }
10361
+ const levels = [];
10362
+ let w = width;
10363
+ let h = height;
10364
+ while (true) {
10365
+ levels.unshift(w * h * 4);
10366
+ if (w === 1 && h === 1)
10367
+ break;
10368
+ w = Math.max(1, Math.floor(w / 2));
10369
+ h = Math.max(1, Math.floor(h / 2));
10370
+ }
10371
+ return levels;
10372
+ }
10373
+ /** M2 streaming budget normalization (pure, unit-tested): default 256 MiB, fail-closed. */
10374
+ export function normalizeTextureBudgetBytes(value) {
10375
+ if (value === undefined)
10376
+ return 256 * 1024 * 1024;
10377
+ if (!Number.isFinite(value) || value <= 0) {
10378
+ throw new Error("Aura3D textureBudgetBytes must be a positive byte count.");
10379
+ }
10380
+ return Math.floor(value);
10381
+ }
10382
+ /**
10383
+ * M2 streaming residency from the post-upgrade texture table (pure,
10384
+ * unit-tested): distance-prioritized mip funding against the budget with
10385
+ * over-budget telemetry for the unfunded tail.
10386
+ */
10387
+ export function describeTextureStreamingResidency(table, budgetBytes) {
10388
+ const candidates = table.map((entry) => ({
10389
+ id: entry.id,
10390
+ mipBytesCoarseToFine: entry.mipBytesCoarseToFine,
10391
+ distanceMeters: entry.distanceMeters
10392
+ }));
10393
+ return evaluateDistancePrioritizedMipResidency(candidates, normalizeTextureBudgetBytes(budgetBytes));
10394
+ }
10395
+ /**
10396
+ * N1 spot shadow observation (pure, unit-tested): cone + atlas tier from the
10397
+ * authored spot, pixel-backing gated on the device-observed map signals with
10398
+ * the spot as caster. No signal, no claim.
10399
+ */
10400
+ export function describeProductionSpotShadow(input) {
10401
+ if (!input.requested || !input.casterIsSpot || input.angle === undefined) {
10402
+ return {
10403
+ requested: input.requested,
10404
+ casterIsSpot: input.casterIsSpot,
10405
+ ...(input.casterName === undefined ? {} : { casterName: input.casterName }),
10406
+ spotPixelBacked: false,
10407
+ reason: !input.requested
10408
+ ? "no authored spot requested a shadow map"
10409
+ : !input.casterIsSpot
10410
+ ? "shadow requested but the caster slot went to a non-spot light"
10411
+ : "spot caster has no cone description"
10412
+ };
10413
+ }
10414
+ // Tier selection validates the cone fail-closed (RangeError on bad angle);
10415
+ // the full projection matrix composes in-device from this cone + range.
10416
+ const tier = selectSpotShadowAtlasTier(input.angle);
10417
+ createSpotShadowProjection(input.angle, Math.max(1, input.range ?? 12));
10418
+ const backed = input.mapRendered && input.mapSampled;
10419
+ return {
10420
+ requested: true,
10421
+ casterIsSpot: true,
10422
+ ...(input.casterName === undefined ? {} : { casterName: input.casterName }),
10423
+ angle: input.angle,
10424
+ ...(input.penumbra === undefined ? {} : { penumbra: input.penumbra }),
10425
+ range: Math.max(1, input.range ?? 12),
10426
+ atlasResolution: tier.resolution,
10427
+ atlasReason: tier.reason,
10428
+ spotPixelBacked: backed,
10429
+ reason: backed
10430
+ ? `spot shadow map rendered and sampled (${tier.resolution}px ${tier.reason})`
10431
+ : "spot caster selected but the device shows no rendered+sampled shadow map yet"
10432
+ };
10433
+ }
8500
10434
  function createProductionPrimitiveResources(node) {
10435
+ const sdfWarnings = [];
10436
+ const sdfResource = createSdfTextPrimitiveResource(node, (message) => { sdfWarnings.push(message); });
8501
10437
  const levels = node.lod?.levels;
8502
10438
  if (!levels?.length) {
8503
- return [{ geometry: createProductionPrimitiveGeometry(node), material: createProductionPrimitiveMaterial(node), bounds: primitiveGeometryBounds(node), name: node.name ?? node.primitive }];
10439
+ if (sdfResource)
10440
+ return [sdfResource];
10441
+ const fallback = {
10442
+ geometry: createProductionPrimitiveGeometry(node),
10443
+ material: createProductionPrimitiveMaterial(node),
10444
+ bounds: primitiveGeometryBounds(node),
10445
+ name: node.name ?? node.primitive,
10446
+ materialSpec: node.material,
10447
+ sourceNode: node,
10448
+ ...blankProductionPrimitiveTextureState()
10449
+ };
10450
+ // Sampler failures stay visible on the fallback resource (surfaced via
10451
+ // texturedMaterials diagnostics), never swallowed by the mesh swap.
10452
+ fallback.textureWarnings.push(...sdfWarnings);
10453
+ return [fallback];
8504
10454
  }
8505
10455
  return levels.map((level, index) => {
8506
10456
  if (!level.primitive && !level.geometry)
@@ -8512,18 +10462,40 @@ function createProductionPrimitiveResources(node) {
8512
10462
  material: level.material ?? node.material,
8513
10463
  lod: undefined
8514
10464
  };
8515
- return { geometry: createProductionPrimitiveGeometry(levelNode), material: createProductionPrimitiveMaterial(levelNode), bounds: primitiveGeometryBounds(levelNode), name: level.name };
10465
+ const levelWarnings = [];
10466
+ const levelSdf = createSdfTextPrimitiveResource(levelNode, (message) => { levelWarnings.push(message); });
10467
+ if (levelSdf)
10468
+ return { ...levelSdf, name: level.name };
10469
+ const levelFallback = {
10470
+ geometry: createProductionPrimitiveGeometry(levelNode),
10471
+ material: createProductionPrimitiveMaterial(levelNode),
10472
+ bounds: primitiveGeometryBounds(levelNode),
10473
+ name: level.name,
10474
+ materialSpec: levelNode.material,
10475
+ sourceNode: levelNode,
10476
+ ...blankProductionPrimitiveTextureState()
10477
+ };
10478
+ levelFallback.textureWarnings.push(...levelWarnings);
10479
+ return levelFallback;
8516
10480
  });
8517
10481
  }
8518
- function selectProductionPrimitiveResource(entry, node, cameraPosition) {
10482
+ function selectProductionPrimitiveResource(entry, node, cameraPosition, lodBias = 1) {
8519
10483
  if (!node.lod?.levels.length)
8520
10484
  return entry.resources[0];
8521
10485
  const position = node.position ?? [0, 0, 0];
8522
10486
  const distance = Math.hypot(cameraPosition[0] - position[0], cameraPosition[1] - position[1], cameraPosition[2] - position[2]);
8523
- const selection = selectAuraRootLodLevel(distance, node.lod.levels, entry.currentLodIndex, node.lod.hysteresis ?? 0);
10487
+ const selection = selectAuraRootLodLevel(distance * lodBias, node.lod.levels, entry.currentLodIndex, node.lod.hysteresis ?? 0);
8524
10488
  entry.currentLodIndex = selection.levelIndex;
8525
10489
  return entry.resources[selection.levelIndex] ?? entry.resources[entry.resources.length - 1];
8526
10490
  }
10491
+ function createProductionModelInstanceTransforms(transforms, node, bounds, time) {
10492
+ const matrices = new Float32Array(transforms.length * 16);
10493
+ transforms.forEach((transform, index) => {
10494
+ const localNode = { ...node, ...transform };
10495
+ matrices.set(createModelMatrix(localNode, bounds, shouldNormalizeModelNode(node), time), index * 16);
10496
+ });
10497
+ return matrices;
10498
+ }
8527
10499
  function createProductionInstanceTransforms(transforms, node) {
8528
10500
  const matrices = new Float32Array(transforms.length * 16);
8529
10501
  transforms.forEach((transform, index) => {
@@ -8559,37 +10531,7 @@ function resolveProductionPrimitiveRuntimeState(entry, runtimeNodes) {
8559
10531
  visible: runtimeSnapshot.visible
8560
10532
  };
8561
10533
  }
8562
- /**
8563
- * Build the production `PBRMaterial` for a primitive node.
8564
- *
8565
- * ## WS-2.1a — the parameter drop this function used to be
8566
- *
8567
- * Until 1.6 this forwarded `clearcoat` and **nothing else** from the extended material surface.
8568
- * `AuraMaterialSpec` accepts `sheen`, `sheenRoughness`, `sheenColor`, `iridescence`,
8569
- * `iridescenceIOR`, `iridescenceThicknessRange`, `anisotropy`, `anisotropyRotation`, `transmission`,
8570
- * `thickness`, `ior`, `attenuationColor` and `attenuationDistance`; `PBRMaterial` accepts a uniform
8571
- * for every one of them and binds them all. The bridge between the two dropped them on the floor.
8572
- *
8573
- * How that presented, measured by `tools/material-structural-parity` before the fix:
8574
- *
8575
- * ```
8576
- * material.pbr({ ..., sheen: 1 }) -> byte-identical frame to sheen: 0
8577
- * material.pbr({ ..., iridescence: 1 }) -> byte-identical frame to iridescence: 0
8578
- * material.pbr({ ..., clearcoat: 1 }) -> byte-identical frame to clearcoat: 0
8579
- * ```
8580
- *
8581
- * `clearcoat` was *forwarded* and still produced an identical frame, which pins the second, separate
8582
- * defect: `a3dPbrExtensionEnvironmentLight` in `ShaderChunks.ts` adds its lobes to
8583
- * `specularRadiance`, a term that is zero without an environment map. Forwarding alone is necessary
8584
- * and not sufficient, so `environmentIntensity` now also has a floor when an extension factor is
8585
- * present — otherwise a developer setting `clearcoat: 1` on a scene with no environment sees nothing
8586
- * and has no way to find out why.
8587
- *
8588
- * The reason this survived so long is worth stating: three of the four lobes are scalar
8589
- * approximations rather than real BRDFs, and **nobody could tell**, because the parameters never
8590
- * arrived. The plumbing defect concealed the shading defect.
8591
- */
8592
- function createProductionPrimitiveMaterial(node) {
10534
+ function resolveProductionPrimitiveScalars(node) {
8593
10535
  const materialSpec = node.material;
8594
10536
  const baseColor = colorToLinearRgba(materialSpec?.color ?? materialSpec?.emissive ?? "#d7dee8");
8595
10537
  const opacity = clamp01(materialSpec?.opacity ?? baseColor[3] ?? 1);
@@ -8620,6 +10562,17 @@ function createProductionPrimitiveMaterial(node) {
8620
10562
  const environmentIntensity = declaresExtension
8621
10563
  ? Math.max(0.35, requestedEnvironmentIntensity)
8622
10564
  : requestedEnvironmentIntensity;
10565
+ // B3: the sampled env-map scale defaults to 1 (no look change) and honors
10566
+ // an explicitly authored envMapIntensity, including 0 (killed response).
10567
+ const envMapIntensity = materialSpec?.envMapIntensity === undefined ? 1 : Math.max(0, materialSpec.envMapIntensity);
10568
+ return {
10569
+ materialSpec, baseColor, opacity, emissiveColor, clearcoat, sheen, iridescence,
10570
+ anisotropy, transmission, thicknessRange, sheenColor, declaresExtension, environmentIntensity, envMapIntensity
10571
+ };
10572
+ }
10573
+ function createProductionPrimitiveMaterial(node) {
10574
+ const materialSpec = node.material;
10575
+ const { baseColor, opacity, emissiveColor, clearcoat, sheenColor, declaresExtension, transmission, thicknessRange, sheen, iridescence, anisotropy, environmentIntensity, envMapIntensity } = resolveProductionPrimitiveScalars(node);
8623
10576
  /*
8624
10577
  * Native instance submission requires the renderer's instanced shader contract. The ordinary
8625
10578
  * PBR shader intentionally has no instance matrix inputs and ForwardPass therefore expands it.
@@ -8637,6 +10590,7 @@ function createProductionPrimitiveMaterial(node) {
8637
10590
  emissiveColor,
8638
10591
  emissiveStrength: Math.max(0, materialSpec?.emissiveIntensity ?? (materialSpec?.emissive ? 1.35 : 0)),
8639
10592
  environmentIntensity,
10593
+ envMapIntensity,
8640
10594
  renderState: {
8641
10595
  blend: opacity < 0.999,
8642
10596
  depthWrite: opacity >= 0.999,
@@ -8667,6 +10621,7 @@ function createProductionPrimitiveMaterial(node) {
8667
10621
  ...(materialSpec?.attenuationColor ? { volumeAttenuationColor: colorToLinearRgb(materialSpec.attenuationColor) } : {}),
8668
10622
  ...(materialSpec?.attenuationDistance === undefined ? {} : { volumeAttenuationDistance: Math.max(0, materialSpec.attenuationDistance) }),
8669
10623
  environmentIntensity,
10624
+ envMapIntensity,
8670
10625
  renderState: {
8671
10626
  blend: opacity < 0.999,
8672
10627
  depthWrite: opacity >= 0.999,
@@ -8689,6 +10644,12 @@ function primitiveGeometryBounds(nodeOrPrimitive) {
8689
10644
  throw new Error("Aura3D custom primitive requires geometry.");
8690
10645
  return createProductionPrimitiveMesh(primitive).bounds;
8691
10646
  }
10647
+ function tilingSecondUnwrap(uvs) {
10648
+ const uv1s = new Float32Array(uvs.length);
10649
+ for (let i = 0; i < uvs.length; i += 1)
10650
+ uv1s[i] = (uvs[i] ?? 0) * 2;
10651
+ return uv1s;
10652
+ }
8692
10653
  function createProductionPrimitiveMesh(primitive) {
8693
10654
  if (primitive === "sphere")
8694
10655
  return createSphereGeometry();
@@ -8740,11 +10701,24 @@ function calculateCustomGeometryNormals(spec) {
8740
10701
  }
8741
10702
  function createProductionGeometryFromPrimitiveMesh(mesh) {
8742
10703
  const vertexCount = mesh.positions.length / 3;
8743
- const vertices = new VertexBuffer(VertexFormat.P3N3, vertexCount);
10704
+ const textured = Boolean(mesh.uvs && mesh.uvs.length === vertexCount * 2
10705
+ && mesh.tangents && mesh.tangents.length === vertexCount * 4);
10706
+ const vertices = new VertexBuffer(textured ? VertexFormat.P3N3T4T2T2 : VertexFormat.P3N3, vertexCount);
8744
10707
  for (let index = 0; index < vertexCount; index += 1) {
8745
10708
  const base = index * 3;
8746
10709
  vertices.setAttribute(index, "position", [mesh.positions[base] ?? 0, mesh.positions[base + 1] ?? 0, mesh.positions[base + 2] ?? 0]);
8747
10710
  vertices.setAttribute(index, "normal", [mesh.normals[base] ?? 0, mesh.normals[base + 1] ?? 1, mesh.normals[base + 2] ?? 0]);
10711
+ if (textured) {
10712
+ const uvBase = index * 2;
10713
+ const tangentBase = index * 4;
10714
+ vertices.setAttribute(index, "tangent", [
10715
+ mesh.tangents[tangentBase] ?? 1, mesh.tangents[tangentBase + 1] ?? 0,
10716
+ mesh.tangents[tangentBase + 2] ?? 0, mesh.tangents[tangentBase + 3] ?? 1
10717
+ ]);
10718
+ vertices.setAttribute(index, "uv", [mesh.uvs[uvBase] ?? 0, mesh.uvs[uvBase + 1] ?? 0]);
10719
+ const uv1 = mesh.uv1s && mesh.uv1s.length === vertexCount * 2 ? mesh.uv1s : mesh.uvs;
10720
+ vertices.setAttribute(index, "uv1", [uv1[uvBase] ?? 0, uv1[uvBase + 1] ?? 0]);
10721
+ }
8748
10722
  }
8749
10723
  return new Geometry(vertices, new IndexBuffer(Array.from(mesh.indices), vertexCount), "triangles", mesh.bounds);
8750
10724
  }
@@ -8766,10 +10740,12 @@ function resolveProductionActorRuntimeState(entry, runtimeNodes) {
8766
10740
  animation: runtimeSnapshot.animation ?? entry.node.animation
8767
10741
  },
8768
10742
  animationBinding: runtimeSnapshot.animationBinding,
10743
+ animationPose: runtimeSnapshot.animationPose,
10744
+ animationPoseTime: runtimeSnapshot.animationPoseBinding?.localTime ?? runtimeSnapshot.animationPoseBinding?.captureTime,
8769
10745
  ...(Object.keys(morphTargets).length > 0 ? { morphTargets } : {})
8770
10746
  };
8771
10747
  }
8772
- function applyProductionActorAnimation(entry, node, animationBinding, time, runtimeWarnings) {
10748
+ function applyProductionActorAnimation(entry, node, animationBinding, time, runtimeWarnings, modelMatrix, refreshAfterMovement) {
8773
10749
  const animation = node.animation;
8774
10750
  if (!animation?.clip || isModelTransformAnimationClip(animation.clip))
8775
10751
  return;
@@ -8779,12 +10755,84 @@ function applyProductionActorAnimation(entry, node, animationBinding, time, runt
8779
10755
  return;
8780
10756
  }
8781
10757
  try {
8782
- entry.actor.playClip(clipName, resolveProductionActorAnimationSeconds(animation, animationBinding, time));
10758
+ const binding = animationBinding?.rootMotion;
10759
+ if (binding && animationBinding) {
10760
+ const observed = animationBinding.rootMotionSamples ?? [{
10761
+ playbackId: animationBinding.playbackId ?? clipName, clipName,
10762
+ time: animationBinding.rootMotionTime ?? 0, weight: 1,
10763
+ loop: animationBinding.loop ?? animation.loop ?? true, additive: false
10764
+ }];
10765
+ if (observed.length === 0) {
10766
+ entry.rootMotionCursors = undefined;
10767
+ return;
10768
+ }
10769
+ const cursors = entry.rootMotionCursors ?? new Map();
10770
+ const samples = observed.map(sample => {
10771
+ if (!Number.isFinite(sample.time) || sample.time < 0)
10772
+ throw new Error("Root motion requires a finite nonnegative unwrapped controller time.");
10773
+ const previous = cursors.get(sample.playbackId);
10774
+ return { clipName: sample.clipName, target: binding.target,
10775
+ fromTime: previous === undefined ? 0 : Math.min(previous, sample.time),
10776
+ toTime: sample.time, weight: sample.weight, loop: sample.loop, additive: sample.additive };
10777
+ });
10778
+ const commitCursors = () => { entry.rootMotionCursors = new Map(observed.map(sample => [sample.playbackId, sample.time])); };
10779
+ const result = entry.actor.playRootMotionClips(samples, {
10780
+ worldFromLocal: [...modelMatrix],
10781
+ move: requested => {
10782
+ if (samples.every(sample => sample.fromTime === sample.toTime))
10783
+ return [0, 0, 0];
10784
+ const accepted = binding.move(requested);
10785
+ // Advance cursors before pose/IK work: retrying a failed pose cannot move twice.
10786
+ commitCursors();
10787
+ refreshAfterMovement();
10788
+ return accepted;
10789
+ }
10790
+ });
10791
+ commitCursors();
10792
+ binding.onSample?.(result.motion);
10793
+ }
10794
+ else {
10795
+ entry.rootMotionCursors = undefined;
10796
+ entry.actor.playClip(clipName, resolveProductionActorAnimationSeconds(animation, animationBinding, time));
10797
+ }
8783
10798
  }
8784
10799
  catch (error) {
8785
10800
  runtimeWarnings.add(`Typed GLB actor "${entry.actor.id}" failed to apply clip "${clipName}": ${productionRenderErrorMessage(error)}`);
8786
10801
  }
8787
10802
  }
10803
+ /**
10804
+ * Applies controller-bound foot planting (E2) to the typed GLB actor. The resolved config
10805
+ * is refreshed every frame with the actor's live model matrix: time-animated model nodes
10806
+ * (float/orbit/turntable) move, and the post-pass solves in world space, so a stale matrix
10807
+ * would plant feet in yesterday's frame. Foot-lock state survives the refresh — the runtime
10808
+ * keeps its rig across matrix-only updates and resets it only when the leg set, ground, or
10809
+ * solve parameters change.
10810
+ */
10811
+ function applyProductionActorFootPlanting(entry, animationBinding, modelMatrix, runtimeWarnings) {
10812
+ const footPlanting = animationBinding?.footPlanting;
10813
+ try {
10814
+ entry.actor.animation.setFootPlanting((footPlanting === undefined
10815
+ ? undefined
10816
+ : { ...footPlanting, legs: footPlanting.legs.map(leg => ({ ...leg, ...(leg.contactPhase ? { contact: leg.contactPhase(animationBinding?.localTime ?? 0) } : {}) })), ...spreadFootPlantingWorldMatrix(modelMatrix) }));
10817
+ }
10818
+ catch (error) {
10819
+ runtimeWarnings.add(`Typed GLB actor "${entry.actor.id}" failed to apply foot planting: ${productionRenderErrorMessage(error)}`);
10820
+ }
10821
+ }
10822
+ /**
10823
+ * Packages the actor model matrix as the post-pass `worldFromLocal`. The matrix the
10824
+ * renderer draws with (column-major 16, model-local → world) is exactly the space the
10825
+ * foot-planting ground speaks, so the same array threads both paths. Returns {} when the
10826
+ * matrix is missing or malformed so the pass falls back to actor-local solving.
10827
+ */
10828
+ function spreadFootPlantingWorldMatrix(modelMatrix) {
10829
+ if (!modelMatrix || modelMatrix.length !== 16)
10830
+ return {};
10831
+ const worldFromLocal = Array.from(modelMatrix, (component) => Number(component));
10832
+ if (worldFromLocal.some((component) => !Number.isFinite(component)))
10833
+ return {};
10834
+ return { worldFromLocal };
10835
+ }
8788
10836
  function applyProductionActorMorphTargets(entry, morphTargets, runtimeWarnings) {
8789
10837
  if (!morphTargets || Object.keys(morphTargets).length === 0)
8790
10838
  return;
@@ -8832,7 +10880,17 @@ function createRuntimeEvidenceFromTypedGLBActor(node, actor, renderItems) {
8832
10880
  },
8833
10881
  renderItemCount: renderItems.length,
8834
10882
  skinnedRenderItemCount: renderItems.filter((item) => item.skinning).length,
8835
- morphRenderItemCount: renderItems.filter((item) => item.morphTargets && item.morphTargets.length > 0).length
10883
+ morphRenderItemCount: renderItems.filter((item) => item.morphTargets && item.morphTargets.length > 0).length,
10884
+ lastMaterialTracksApplied: actorEvidence.lastMaterialTracksApplied,
10885
+ lastLightTracksApplied: actorEvidence.lastLightTracksApplied,
10886
+ lastFootPlantingGroundedFeet: actorEvidence.lastFootPlantingGroundedFeet,
10887
+ lastFootPlantingTargetError: actorEvidence.lastFootPlantingTargetError,
10888
+ lastFootPlantingHipOffset: actorEvidence.lastFootPlantingHipOffset,
10889
+ lastFootPlantingMissingLegs: [...actorEvidence.lastFootPlantingMissingLegs],
10890
+ lastFootPlantingFeet: actorEvidence.lastFootPlantingFeet,
10891
+ lastFootPlantingSurfaces: actorEvidence.lastFootPlantingSurfaces,
10892
+ lastFootPlantingDeformation: actorEvidence.lastFootPlantingDeformation,
10893
+ footPlantingConfigured: actorEvidence.footPlantingConfigured
8836
10894
  });
8837
10895
  }
8838
10896
  function createProductionRuntimeMetadata(actorEntries, primitiveEntries) {
@@ -9277,7 +11335,9 @@ function collectRuntimeEffectNodes(snapshot) {
9277
11335
  return groups.flatten(snapshot.nodes).filter((node) => node.kind === "effect");
9278
11336
  }
9279
11337
  function hasRuntimePostProcessEffects(effectNodes) {
9280
- return effectNodes.some((node) => node.effect === "bloom" || node.effect === "ambient-occlusion" || node.effect === "contact-occlusion");
11338
+ return effectNodes.some((node) => node.effect === "bloom" || node.effect === "ambient-occlusion" || node.effect === "contact-occlusion"
11339
+ || node.effect === "color-grade" || node.effect === "anti-alias" || node.effect === "outline"
11340
+ || node.effect === "screen-space-reflections" || node.effect === "depth-of-field" || node.effect === "motion-blur");
9281
11341
  }
9282
11342
  async function createWebGLSceneRenderer(canvas, snapshot, rendererOptions, runtimeWarnings = [], runtimeNodes) {
9283
11343
  const gl = canvas.getContext("webgl2", { antialias: true, preserveDrawingBuffer: true });
@@ -9313,7 +11373,8 @@ async function createWebGLSceneRenderer(canvas, snapshot, rendererOptions, runti
9313
11373
  contactOcclusionReceiver: false,
9314
11374
  pixelBacked: false,
9315
11375
  actualPasses: [],
9316
- fallbackPasses: hasRuntimePostProcessEffects(requestedEffectNodes) ? ["webgl2-direct-render"] : []
11376
+ fallbackPasses: hasRuntimePostProcessEffects(requestedEffectNodes) ? ["webgl2-direct-render"] : [],
11377
+ executionMode: "none"
9317
11378
  },
9318
11379
  warnings: [
9319
11380
  "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.",
@@ -10549,6 +12610,8 @@ function textureRef(texture, uvs, key) {
10549
12610
  return { [key]: texture.index };
10550
12611
  }
10551
12612
  function createPlaneGeometry() {
12613
+ const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
12614
+ const tangents = new Float32Array([1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1]);
10552
12615
  return {
10553
12616
  positions: new Float32Array([
10554
12617
  -0.5, 0, -0.5,
@@ -10569,10 +12632,21 @@ function createPlaneGeometry() {
10569
12632
  // up-normal downward. The plane then faced away from every overhead light and
10570
12633
  // received no direct lighting or visible cast shadows.
10571
12634
  indices: new Uint16Array([0, 2, 1, 0, 3, 2]),
10572
- bounds: { min: [-0.5, 0, -0.5], max: [0.5, 0, 0.5] }
12635
+ bounds: { min: [-0.5, 0, -0.5], max: [0.5, 0, 0.5] },
12636
+ uvs,
12637
+ uv1s: tilingSecondUnwrap(uvs),
12638
+ tangents
10573
12639
  };
10574
12640
  }
10575
12641
  function createBoxGeometry() {
12642
+ // One full 0-1 unwrap per face, matching the 4-vertex face order below.
12643
+ const faceUvs = [0, 0, 1, 0, 1, 1, 0, 1];
12644
+ const uvs = new Float32Array([...faceUvs, ...faceUvs, ...faceUvs, ...faceUvs, ...faceUvs, ...faceUvs]);
12645
+ // Analytic tangents along each face's +u direction (face order +z,-z,+y,-y,+x,-x).
12646
+ const faceTangents = [
12647
+ [1, 0, 0, 1], [-1, 0, 0, 1], [1, 0, 0, 1], [1, 0, 0, 1], [0, 0, -1, 1], [0, 0, 1, 1]
12648
+ ];
12649
+ const tangents = new Float32Array(faceTangents.flatMap((tangent) => [...tangent, ...tangent, ...tangent, ...tangent]));
10576
12650
  const positions = new Float32Array([
10577
12651
  -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
10578
12652
  0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5,
@@ -10600,7 +12674,10 @@ function createBoxGeometry() {
10600
12674
  16, 17, 18, 16, 18, 19,
10601
12675
  20, 21, 22, 20, 22, 23
10602
12676
  ]),
10603
- bounds: { min: [-0.5, -0.5, -0.5], max: [0.5, 0.5, 0.5] }
12677
+ bounds: { min: [-0.5, -0.5, -0.5], max: [0.5, 0.5, 0.5] },
12678
+ uvs,
12679
+ uv1s: tilingSecondUnwrap(uvs),
12680
+ tangents
10604
12681
  };
10605
12682
  }
10606
12683
  function createSphereGeometry() {
@@ -10608,6 +12685,8 @@ function createSphereGeometry() {
10608
12685
  const columns = 16;
10609
12686
  const positions = [];
10610
12687
  const normals = [];
12688
+ const uvs = [];
12689
+ const tangents = [];
10611
12690
  const indices = [];
10612
12691
  for (let row = 0; row <= rows; row += 1) {
10613
12692
  const v = row / rows;
@@ -10620,6 +12699,8 @@ function createSphereGeometry() {
10620
12699
  const z = Math.sin(theta) * Math.sin(phi);
10621
12700
  positions.push(x * 0.5, y * 0.5, z * 0.5);
10622
12701
  normals.push(x, y, z);
12702
+ uvs.push(u, 1 - v);
12703
+ tangents.push(-Math.sin(phi), 0, Math.cos(phi), 1);
10623
12704
  }
10624
12705
  }
10625
12706
  for (let row = 0; row < rows; row += 1) {
@@ -10632,11 +12713,15 @@ function createSphereGeometry() {
10632
12713
  indices.push(a, a + 1, b, b, a + 1, b + 1);
10633
12714
  }
10634
12715
  }
12716
+ const sphereUvs = new Float32Array(uvs);
10635
12717
  return {
10636
12718
  positions: new Float32Array(positions),
10637
12719
  normals: new Float32Array(normals),
10638
12720
  indices: new Uint16Array(indices),
10639
- bounds: { min: [-0.5, -0.5, -0.5], max: [0.5, 0.5, 0.5] }
12721
+ bounds: { min: [-0.5, -0.5, -0.5], max: [0.5, 0.5, 0.5] },
12722
+ uvs: sphereUvs,
12723
+ uv1s: tilingSecondUnwrap(sphereUvs),
12724
+ tangents: new Float32Array(tangents)
10640
12725
  };
10641
12726
  }
10642
12727
  function createCylinderGeometry() {
@@ -10973,11 +13058,7 @@ function createViewProjection(snapshot, aspect, time, runtimeNodes) {
10973
13058
  const cameraSpec = snapshot.camera;
10974
13059
  const { target, eye } = resolveCameraFrame(snapshot, cameraSpec, time, runtimeNodes);
10975
13060
  const view = lookAtMat4([...eye], [...target], [0, 1, 0]);
10976
- const halfHeight = Math.max(1e-4, cameraSpec.orthographicSize ?? 1.4);
10977
- const halfWidth = Math.max(1e-4, halfHeight * aspect);
10978
- const projection = isOrthographicCameraMode(cameraSpec.mode)
10979
- ? orthographicMat4(-halfWidth, halfWidth, -halfHeight, halfHeight, 0.05, 100)
10980
- : perspectiveMat4(((cameraSpec.fov ?? 45) * Math.PI) / 180, aspect, 0.05, 100);
13061
+ const projection = createCameraProjection(cameraSpec, aspect);
10981
13062
  return new Float32Array(multiplyMat4(projection, view));
10982
13063
  }
10983
13064
  function createModelMatrix(node, bounds, normalizeToUnit, time = 0) {
@@ -11050,6 +13131,10 @@ function isModelTransformAnimationClip(clip) {
11050
13131
  return clip === "turntable" || clip === "float" || clip === "orbit";
11051
13132
  }
11052
13133
  function primitiveSize(node) {
13134
+ // SDF quad positions are already authored in world units by layoutSdfText.
13135
+ // Applying the public text size again here shrinks/grows the GPU quad twice.
13136
+ if (node.text3D?.backend === "sdf")
13137
+ return [1, 1, 1];
11053
13138
  if (typeof node.size === "number")
11054
13139
  return [node.size, node.size, node.size];
11055
13140
  return node.size ?? [1, 1, 1];
@@ -11370,7 +13455,9 @@ function snapshotDiagnostics(value) {
11370
13455
  renderer: value.renderer,
11371
13456
  warnings: [...value.warnings],
11372
13457
  errors: [...value.errors],
11373
- ...(value.labels ? { labels: value.labels } : {})
13458
+ ...(value.labels ? { labels: value.labels } : {}),
13459
+ ...(value.labelTelemetry ? { labelTelemetry: value.labelTelemetry } : {}),
13460
+ ...(value.textBuckets ? { textBuckets: value.textBuckets } : {})
11374
13461
  };
11375
13462
  }
11376
13463
  function collectGeneratedCodeWarnings(snapshot) {
@@ -11381,6 +13468,17 @@ function collectGeneratedCodeWarnings(snapshot) {
11381
13468
  if (!snapshot.nodes.some((node) => node.kind === "interaction")) {
11382
13469
  warnings.push("Scene has no interactions. Suggested fix: add interactions.orbit() for product/viewer scenes.");
11383
13470
  }
13471
+ const flatNodes = groups.flatten(snapshot.nodes);
13472
+ // muse3jsparity-PRD C1: procedural texture inputs have no rasterizer. The
13473
+ // intent is compile-time known, so warn here; fetch outcomes stay dynamic.
13474
+ for (const node of flatNodes) {
13475
+ if (node.kind !== "primitive" || !node.material)
13476
+ continue;
13477
+ const label = node.name ?? `aura-primitive-${node.primitive}`;
13478
+ for (const procedural of createProductionPrimitiveTextureIntent(node.material).proceduralInputs) {
13479
+ warnings.push(`procedural texture ${procedural} on "${label}" has no rasterizer; recorded only, scalar material retained`);
13480
+ }
13481
+ }
11384
13482
  for (const node of snapshot.nodes) {
11385
13483
  if (node.kind === "model" && createAssetProvenance(node.asset).source === "unsafe-url") {
11386
13484
  warnings.push(`Model uses unsafeModelUrl("${node.asset.url}"). Suggested fix: run assets add and use typed assets.`);
@@ -11544,6 +13642,16 @@ function drawEffect(context, width, height, node, time) {
11544
13642
  context.fillStyle = toAlphaColor(node.color ?? "#9fb7d9", node.density ?? 0.12);
11545
13643
  context.fillRect(0, height * 0.2, width, height * 0.8);
11546
13644
  }
13645
+ if (node.effect === "volumetric-fog") {
13646
+ context.fillStyle = toAlphaColor(node.color ?? "#6f84b9", node.density ?? 0.18);
13647
+ context.fillRect(0, height * 0.2, width, height * 0.8);
13648
+ const anchor = node.lightPosition ?? [0.5, 0.18];
13649
+ const glow = context.createRadialGradient(width * anchor[0], height * anchor[1], 8, width * anchor[0], height * anchor[1], width * 0.3);
13650
+ glow.addColorStop(0, toAlphaColor(node.color ?? "#fff3d6", (node.intensity ?? 0.7) * 0.35));
13651
+ glow.addColorStop(1, "rgba(255,255,255,0)");
13652
+ context.fillStyle = glow;
13653
+ context.fillRect(0, 0, width, height);
13654
+ }
11547
13655
  if (node.effect === "bloom") {
11548
13656
  const gradient = context.createRadialGradient(width * 0.5, height * 0.45, 20, width * 0.5, height * 0.45, width * 0.46);
11549
13657
  gradient.addColorStop(0, toAlphaColor(node.color ?? "#ffffff", (node.intensity ?? 0.35) * 0.3));
@@ -11596,6 +13704,29 @@ function drawEffect(context, width, height, node, time) {
11596
13704
  }
11597
13705
  }
11598
13706
  }
13707
+ if (node.effect === "snow") {
13708
+ const density = Math.max(0.2, Math.min(1.6, node.density ?? node.intensity ?? 0.68));
13709
+ const intensity = Math.max(0.1, Math.min(1.4, node.intensity ?? 0.4));
13710
+ const color = node.color ?? "#e8f1ff";
13711
+ if (node.mist !== false) {
13712
+ context.fillStyle = toAlphaColor(color, Math.min(0.1, 0.03 + intensity * 0.04));
13713
+ context.fillRect(0, height * 0.2, width, height * 0.72);
13714
+ }
13715
+ const drift = (node.wind?.[0] ?? -0.85) * 0.02;
13716
+ for (let layer = 0; layer < 3; layer += 1) {
13717
+ const count = Math.round((layer === 2 ? 30 : 52) * density);
13718
+ const size = layer === 2 ? 3.1 : layer === 1 ? 2.2 : 1.4;
13719
+ context.fillStyle = toAlphaColor(color, Math.min(0.85, 0.34 + intensity * (0.3 - layer * 0.06)));
13720
+ for (let i = 0; i < count; i += 1) {
13721
+ const sway = Math.sin(time * 0.0011 * (node.speed ?? 1) + i * 1.7 + layer) * (6 + layer * 5);
13722
+ const x = (((i * 61 + layer * 197 + time * 0.008 * (node.speed ?? 1) * drift * 60) % width) + width) % width + sway * 0.14;
13723
+ const y = ((i * 97 + layer * 131 + time * 0.028 * (node.speed ?? 1) * (1 + layer * 0.3)) % (height * 0.86)) + height * 0.06;
13724
+ context.beginPath();
13725
+ context.arc(x, y, size, 0, Math.PI * 2);
13726
+ context.fill();
13727
+ }
13728
+ }
13729
+ }
11599
13730
  if (node.effect === "particles") {
11600
13731
  const isFountain = node.emitter === "fountain";
11601
13732
  // Fountain cap matches prefabs.particleFountain's 2400 maximum so rendered counts never undercut the scene JSON.