@aura3d/rendering 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 (374) hide show
  1. package/dist/AtmosphereWetness.d.ts +61 -0
  2. package/dist/AtmosphereWetness.d.ts.map +1 -0
  3. package/dist/AtmosphereWetness.js +99 -0
  4. package/dist/AtmosphereWetness.js.map +1 -0
  5. package/dist/CascadedShadowMaps.d.ts.map +1 -1
  6. package/dist/CascadedShadowMaps.js +3 -1
  7. package/dist/CascadedShadowMaps.js.map +1 -1
  8. package/dist/ClusteredForwardLighting.d.ts +25 -1
  9. package/dist/ClusteredForwardLighting.d.ts.map +1 -1
  10. package/dist/ClusteredForwardLighting.js +80 -3
  11. package/dist/ClusteredForwardLighting.js.map +1 -1
  12. package/dist/DayNightSky.d.ts +63 -0
  13. package/dist/DayNightSky.d.ts.map +1 -0
  14. package/dist/DayNightSky.js +156 -0
  15. package/dist/DayNightSky.js.map +1 -0
  16. package/dist/EnvironmentPresetPack.d.ts +30 -0
  17. package/dist/EnvironmentPresetPack.d.ts.map +1 -0
  18. package/dist/EnvironmentPresetPack.js +49 -0
  19. package/dist/EnvironmentPresetPack.js.map +1 -0
  20. package/dist/ExtensionScalarAtlas.d.ts +18 -0
  21. package/dist/ExtensionScalarAtlas.d.ts.map +1 -0
  22. package/dist/ExtensionScalarAtlas.js +107 -0
  23. package/dist/ExtensionScalarAtlas.js.map +1 -0
  24. package/dist/ForwardPass.d.ts +65 -0
  25. package/dist/ForwardPass.d.ts.map +1 -1
  26. package/dist/ForwardPass.js +196 -2
  27. package/dist/ForwardPass.js.map +1 -1
  28. package/dist/InstancedPBRMaterial.d.ts +1 -0
  29. package/dist/InstancedPBRMaterial.d.ts.map +1 -1
  30. package/dist/InstancedPBRMaterial.js +3 -0
  31. package/dist/InstancedPBRMaterial.js.map +1 -1
  32. package/dist/InstancingDiagnostics.d.ts +69 -0
  33. package/dist/InstancingDiagnostics.d.ts.map +1 -0
  34. package/dist/InstancingDiagnostics.js +98 -0
  35. package/dist/InstancingDiagnostics.js.map +1 -0
  36. package/dist/LeanWebGL2Device.d.ts +265 -0
  37. package/dist/LeanWebGL2Device.d.ts.map +1 -0
  38. package/dist/LeanWebGL2Device.js +4001 -0
  39. package/dist/LeanWebGL2Device.js.map +1 -0
  40. package/dist/LightingRig.d.ts +19 -1
  41. package/dist/LightingRig.d.ts.map +1 -1
  42. package/dist/LightingRig.js +61 -1
  43. package/dist/LightingRig.js.map +1 -1
  44. package/dist/MaterialBinding.js +1 -1
  45. package/dist/MaterialBinding.js.map +1 -1
  46. package/dist/MeshConsolidation.d.ts +12 -0
  47. package/dist/MeshConsolidation.d.ts.map +1 -1
  48. package/dist/MeshConsolidation.js +28 -0
  49. package/dist/MeshConsolidation.js.map +1 -1
  50. package/dist/MorphTargetPlan.d.ts +29 -2
  51. package/dist/MorphTargetPlan.d.ts.map +1 -1
  52. package/dist/MorphTargetPlan.js +23 -4
  53. package/dist/MorphTargetPlan.js.map +1 -1
  54. package/dist/NormalMappedPBRMaterial.d.ts.map +1 -1
  55. package/dist/NormalMappedPBRMaterial.js +22 -2
  56. package/dist/NormalMappedPBRMaterial.js.map +1 -1
  57. package/dist/OceanSurface.d.ts +75 -0
  58. package/dist/OceanSurface.d.ts.map +1 -1
  59. package/dist/OceanSurface.js +211 -0
  60. package/dist/OceanSurface.js.map +1 -1
  61. package/dist/PBRMaterial.d.ts +1 -0
  62. package/dist/PBRMaterial.d.ts.map +1 -1
  63. package/dist/PBRMaterial.js +23 -2
  64. package/dist/PBRMaterial.js.map +1 -1
  65. package/dist/PbrReference.d.ts +48 -0
  66. package/dist/PbrReference.d.ts.map +1 -1
  67. package/dist/PbrReference.js +126 -0
  68. package/dist/PbrReference.js.map +1 -1
  69. package/dist/PlanarReflection.d.ts +221 -0
  70. package/dist/PlanarReflection.d.ts.map +1 -0
  71. package/dist/PlanarReflection.js +541 -0
  72. package/dist/PlanarReflection.js.map +1 -0
  73. package/dist/PostProcessPass.d.ts +32 -0
  74. package/dist/PostProcessPass.d.ts.map +1 -1
  75. package/dist/PostProcessPass.js +79 -36
  76. package/dist/PostProcessPass.js.map +1 -1
  77. package/dist/ProjectionMath.d.ts +3 -0
  78. package/dist/ProjectionMath.d.ts.map +1 -0
  79. package/dist/ProjectionMath.js +26 -0
  80. package/dist/ProjectionMath.js.map +1 -0
  81. package/dist/ReflectionSurfaces.d.ts +17 -0
  82. package/dist/ReflectionSurfaces.d.ts.map +1 -1
  83. package/dist/ReflectionSurfaces.js +103 -1
  84. package/dist/ReflectionSurfaces.js.map +1 -1
  85. package/dist/RenderDevice.d.ts +92 -0
  86. package/dist/RenderDevice.d.ts.map +1 -1
  87. package/dist/RenderDevice.js +85 -0
  88. package/dist/RenderDevice.js.map +1 -1
  89. package/dist/Renderer.d.ts +9 -0
  90. package/dist/Renderer.d.ts.map +1 -1
  91. package/dist/Renderer.js +97 -9
  92. package/dist/Renderer.js.map +1 -1
  93. package/dist/RendererPostprocessPlan.d.ts +64 -3
  94. package/dist/RendererPostprocessPlan.d.ts.map +1 -1
  95. package/dist/RendererPostprocessPlan.js +116 -12
  96. package/dist/RendererPostprocessPlan.js.map +1 -1
  97. package/dist/RendererVisualPipelineReport.d.ts.map +1 -1
  98. package/dist/RendererVisualPipelineReport.js +3 -1
  99. package/dist/RendererVisualPipelineReport.js.map +1 -1
  100. package/dist/Sampler.d.ts +29 -0
  101. package/dist/Sampler.d.ts.map +1 -1
  102. package/dist/Sampler.js +47 -0
  103. package/dist/Sampler.js.map +1 -1
  104. package/dist/ScreenSpaceReflectionPass.d.ts +43 -0
  105. package/dist/ScreenSpaceReflectionPass.d.ts.map +1 -0
  106. package/dist/ScreenSpaceReflectionPass.js +58 -0
  107. package/dist/ScreenSpaceReflectionPass.js.map +1 -0
  108. package/dist/SdfText.d.ts +250 -0
  109. package/dist/SdfText.d.ts.map +1 -0
  110. package/dist/SdfText.js +551 -0
  111. package/dist/SdfText.js.map +1 -0
  112. package/dist/ShaderChunks.d.ts.map +1 -1
  113. package/dist/ShaderChunks.js +30 -12
  114. package/dist/ShaderChunks.js.map +1 -1
  115. package/dist/ShaderLibrary.d.ts.map +1 -1
  116. package/dist/ShaderLibrary.js +601 -186
  117. package/dist/ShaderLibrary.js.map +1 -1
  118. package/dist/ShaderLibraryCore.d.ts +3 -0
  119. package/dist/ShaderLibraryCore.d.ts.map +1 -1
  120. package/dist/ShaderLibraryCore.js +73 -12
  121. package/dist/ShaderLibraryCore.js.map +1 -1
  122. package/dist/ShadowMap.d.ts +24 -0
  123. package/dist/ShadowMap.d.ts.map +1 -1
  124. package/dist/ShadowMap.js +43 -0
  125. package/dist/ShadowMap.js.map +1 -1
  126. package/dist/SkinnedLitMaterial.d.ts.map +1 -1
  127. package/dist/SkinnedLitMaterial.js +2 -0
  128. package/dist/SkinnedLitMaterial.js.map +1 -1
  129. package/dist/SkinningBounds.d.ts +10 -0
  130. package/dist/SkinningBounds.d.ts.map +1 -1
  131. package/dist/SkinningBounds.js +27 -0
  132. package/dist/SkinningBounds.js.map +1 -1
  133. package/dist/SpriteFlipbook.d.ts +60 -0
  134. package/dist/SpriteFlipbook.d.ts.map +1 -0
  135. package/dist/SpriteFlipbook.js +128 -0
  136. package/dist/SpriteFlipbook.js.map +1 -0
  137. package/dist/TemporalHistory.d.ts +37 -0
  138. package/dist/TemporalHistory.d.ts.map +1 -0
  139. package/dist/TemporalHistory.js +135 -0
  140. package/dist/TemporalHistory.js.map +1 -0
  141. package/dist/TemporalMath.d.ts +3 -0
  142. package/dist/TemporalMath.d.ts.map +1 -0
  143. package/dist/TemporalMath.js +9 -0
  144. package/dist/TemporalMath.js.map +1 -0
  145. package/dist/TerrainTiles.d.ts +120 -0
  146. package/dist/TerrainTiles.d.ts.map +1 -0
  147. package/dist/TerrainTiles.js +221 -0
  148. package/dist/TerrainTiles.js.map +1 -0
  149. package/dist/TexturedPBRMaterial.d.ts +4 -0
  150. package/dist/TexturedPBRMaterial.d.ts.map +1 -1
  151. package/dist/TexturedPBRMaterial.js +88 -5
  152. package/dist/TexturedPBRMaterial.js.map +1 -1
  153. package/dist/TexturedUnlitMaterial.d.ts.map +1 -1
  154. package/dist/TexturedUnlitMaterial.js +20 -2
  155. package/dist/TexturedUnlitMaterial.js.map +1 -1
  156. package/dist/UnlitMaterial.d.ts.map +1 -1
  157. package/dist/UnlitMaterial.js +20 -2
  158. package/dist/UnlitMaterial.js.map +1 -1
  159. package/dist/VertexFormat.d.ts +6 -0
  160. package/dist/VertexFormat.d.ts.map +1 -1
  161. package/dist/VertexFormat.js +12 -0
  162. package/dist/VertexFormat.js.map +1 -1
  163. package/dist/VolumetricFog.d.ts +55 -0
  164. package/dist/VolumetricFog.d.ts.map +1 -0
  165. package/dist/VolumetricFog.js +91 -0
  166. package/dist/VolumetricFog.js.map +1 -0
  167. package/dist/WaterSurface.d.ts +77 -0
  168. package/dist/WaterSurface.d.ts.map +1 -0
  169. package/dist/WaterSurface.js +133 -0
  170. package/dist/WaterSurface.js.map +1 -0
  171. package/dist/WebGL2Device.d.ts +73 -0
  172. package/dist/WebGL2Device.d.ts.map +1 -1
  173. package/dist/WebGL2Device.js +836 -79
  174. package/dist/WebGL2Device.js.map +1 -1
  175. package/dist/WebGPUDevice.d.ts +62 -1
  176. package/dist/WebGPUDevice.d.ts.map +1 -1
  177. package/dist/WebGPUDevice.js +844 -51
  178. package/dist/WebGPUDevice.js.map +1 -1
  179. package/dist/WebGPUExtensionAtlas.d.ts +6 -0
  180. package/dist/WebGPUExtensionAtlas.d.ts.map +1 -0
  181. package/dist/WebGPUExtensionAtlas.js +225 -0
  182. package/dist/WebGPUExtensionAtlas.js.map +1 -0
  183. package/dist/WebGPUSkinningLimits.d.ts +40 -0
  184. package/dist/WebGPUSkinningLimits.d.ts.map +1 -1
  185. package/dist/WebGPUSkinningLimits.js +32 -0
  186. package/dist/WebGPUSkinningLimits.js.map +1 -1
  187. package/dist/effects/CollisionModule.d.ts +9 -0
  188. package/dist/effects/CollisionModule.d.ts.map +1 -1
  189. package/dist/effects/CollisionModule.js +10 -0
  190. package/dist/effects/CollisionModule.js.map +1 -1
  191. package/dist/effects/ColorModule.d.ts +7 -0
  192. package/dist/effects/ColorModule.d.ts.map +1 -1
  193. package/dist/effects/ColorModule.js +23 -0
  194. package/dist/effects/ColorModule.js.map +1 -1
  195. package/dist/effects/ForceModule.d.ts +35 -0
  196. package/dist/effects/ForceModule.d.ts.map +1 -1
  197. package/dist/effects/ForceModule.js +49 -0
  198. package/dist/effects/ForceModule.js.map +1 -1
  199. package/dist/effects/GPUParticleBackend.d.ts +188 -0
  200. package/dist/effects/GPUParticleBackend.d.ts.map +1 -1
  201. package/dist/effects/GPUParticleBackend.js +710 -1
  202. package/dist/effects/GPUParticleBackend.js.map +1 -1
  203. package/dist/effects/HeightfieldModule.d.ts +58 -0
  204. package/dist/effects/HeightfieldModule.d.ts.map +1 -0
  205. package/dist/effects/HeightfieldModule.js +129 -0
  206. package/dist/effects/HeightfieldModule.js.map +1 -0
  207. package/dist/effects/LightingModule.d.ts +34 -0
  208. package/dist/effects/LightingModule.d.ts.map +1 -0
  209. package/dist/effects/LightingModule.js +54 -0
  210. package/dist/effects/LightingModule.js.map +1 -0
  211. package/dist/effects/ParticleDiagnostics.d.ts +8 -0
  212. package/dist/effects/ParticleDiagnostics.d.ts.map +1 -1
  213. package/dist/effects/ParticleDiagnostics.js +3 -0
  214. package/dist/effects/ParticleDiagnostics.js.map +1 -1
  215. package/dist/effects/ParticleModule.d.ts +8 -0
  216. package/dist/effects/ParticleModule.d.ts.map +1 -1
  217. package/dist/effects/ParticleModule.js.map +1 -1
  218. package/dist/effects/ParticleRenderer.d.ts +20 -0
  219. package/dist/effects/ParticleRenderer.d.ts.map +1 -1
  220. package/dist/effects/ParticleRenderer.js +57 -7
  221. package/dist/effects/ParticleRenderer.js.map +1 -1
  222. package/dist/effects/ParticleSystem.d.ts +45 -1
  223. package/dist/effects/ParticleSystem.d.ts.map +1 -1
  224. package/dist/effects/ParticleSystem.js +373 -4
  225. package/dist/effects/ParticleSystem.js.map +1 -1
  226. package/dist/effects/ResidentGPUParticleRenderer.d.ts +158 -0
  227. package/dist/effects/ResidentGPUParticleRenderer.d.ts.map +1 -0
  228. package/dist/effects/ResidentGPUParticleRenderer.js +576 -0
  229. package/dist/effects/ResidentGPUParticleRenderer.js.map +1 -0
  230. package/dist/effects/SizeModule.d.ts +4 -0
  231. package/dist/effects/SizeModule.d.ts.map +1 -1
  232. package/dist/effects/SizeModule.js +16 -0
  233. package/dist/effects/SizeModule.js.map +1 -1
  234. package/dist/effects/SubEmitterModule.d.ts +61 -0
  235. package/dist/effects/SubEmitterModule.d.ts.map +1 -0
  236. package/dist/effects/SubEmitterModule.js +119 -0
  237. package/dist/effects/SubEmitterModule.js.map +1 -0
  238. package/dist/effects/TrailModule.d.ts +38 -0
  239. package/dist/effects/TrailModule.d.ts.map +1 -1
  240. package/dist/effects/TrailModule.js +95 -2
  241. package/dist/effects/TrailModule.js.map +1 -1
  242. package/dist/effects/TurbulenceModule.d.ts +43 -0
  243. package/dist/effects/TurbulenceModule.d.ts.map +1 -0
  244. package/dist/effects/TurbulenceModule.js +130 -0
  245. package/dist/effects/TurbulenceModule.js.map +1 -0
  246. package/dist/effects/index.d.ts +4 -0
  247. package/dist/effects/index.d.ts.map +1 -1
  248. package/dist/effects/index.js +4 -0
  249. package/dist/effects/index.js.map +1 -1
  250. package/dist/extension-scalar-atlas.d.ts +3 -0
  251. package/dist/extension-scalar-atlas.d.ts.map +1 -0
  252. package/dist/extension-scalar-atlas.js +2 -0
  253. package/dist/extension-scalar-atlas.js.map +1 -0
  254. package/dist/index.d.ts +58 -19
  255. package/dist/index.d.ts.map +1 -1
  256. package/dist/index.js +32 -10
  257. package/dist/index.js.map +1 -1
  258. package/dist/lean/LeanProductionRenderer.d.ts.map +1 -1
  259. package/dist/lean/LeanProductionRenderer.js +2 -2
  260. package/dist/lean/LeanProductionRenderer.js.map +1 -1
  261. package/dist/lean-core-runtime.d.ts +13 -0
  262. package/dist/lean-core-runtime.d.ts.map +1 -0
  263. package/dist/lean-core-runtime.js +10 -0
  264. package/dist/lean-core-runtime.js.map +1 -0
  265. package/dist/materials/MaterialExtensions.js +3 -3
  266. package/dist/materials/MaterialExtensions.js.map +1 -1
  267. package/dist/postprocess/NativeBloomPyramid.d.ts +44 -0
  268. package/dist/postprocess/NativeBloomPyramid.d.ts.map +1 -0
  269. package/dist/postprocess/NativeBloomPyramid.js +84 -0
  270. package/dist/postprocess/NativeBloomPyramid.js.map +1 -0
  271. package/dist/postprocess/NativeLdrEffectLuts.d.ts +10 -1
  272. package/dist/postprocess/NativeLdrEffectLuts.d.ts.map +1 -1
  273. package/dist/postprocess/NativeLdrEffectLuts.js +19 -2
  274. package/dist/postprocess/NativeLdrEffectLuts.js.map +1 -1
  275. package/dist/postprocess/index.d.ts +2 -0
  276. package/dist/postprocess/index.d.ts.map +1 -1
  277. package/dist/postprocess/index.js +1 -0
  278. package/dist/postprocess/index.js.map +1 -1
  279. package/dist/production-runtime/PBRHDRPipeline.d.ts +22 -0
  280. package/dist/production-runtime/PBRHDRPipeline.d.ts.map +1 -1
  281. package/dist/production-runtime/PBRHDRPipeline.js +125 -0
  282. package/dist/production-runtime/PBRHDRPipeline.js.map +1 -1
  283. package/dist/production-runtime/ProductionRendererTypes.d.ts +1 -0
  284. package/dist/production-runtime/ProductionRendererTypes.d.ts.map +1 -1
  285. package/dist/production-runtime/ProductionRendererTypes.js.map +1 -1
  286. package/dist/production-runtime/ProductionRuntimeRenderer.d.ts +3 -0
  287. package/dist/production-runtime/ProductionRuntimeRenderer.d.ts.map +1 -1
  288. package/dist/production-runtime/ProductionRuntimeRenderer.js +8 -1
  289. package/dist/production-runtime/ProductionRuntimeRenderer.js.map +1 -1
  290. package/dist/production-runtime/ProductionWebGL2Renderer.d.ts +4 -0
  291. package/dist/production-runtime/ProductionWebGL2Renderer.d.ts.map +1 -1
  292. package/dist/production-runtime/ProductionWebGL2Renderer.js +15 -0
  293. package/dist/production-runtime/ProductionWebGL2Renderer.js.map +1 -1
  294. package/dist/production-runtime/ProductionWebGPURenderer.d.ts +58 -0
  295. package/dist/production-runtime/ProductionWebGPURenderer.d.ts.map +1 -1
  296. package/dist/production-runtime/ProductionWebGPURenderer.js +127 -0
  297. package/dist/production-runtime/ProductionWebGPURenderer.js.map +1 -1
  298. package/dist/production-runtime/framegraph/FrameGraph.d.ts +4 -2
  299. package/dist/production-runtime/framegraph/FrameGraph.d.ts.map +1 -1
  300. package/dist/production-runtime/framegraph/FrameGraph.js +62 -4
  301. package/dist/production-runtime/framegraph/FrameGraph.js.map +1 -1
  302. package/dist/production-runtime/framegraph/NativeFrameGraphBindings.d.ts +32 -0
  303. package/dist/production-runtime/framegraph/NativeFrameGraphBindings.d.ts.map +1 -0
  304. package/dist/production-runtime/framegraph/NativeFrameGraphBindings.js +111 -0
  305. package/dist/production-runtime/framegraph/NativeFrameGraphBindings.js.map +1 -0
  306. package/dist/production-runtime/framegraph/RenderPass.d.ts +23 -0
  307. package/dist/production-runtime/framegraph/RenderPass.d.ts.map +1 -1
  308. package/dist/production-runtime/framegraph/RenderPass.js +87 -1
  309. package/dist/production-runtime/framegraph/RenderPass.js.map +1 -1
  310. package/dist/production-runtime/index.d.ts +7 -4
  311. package/dist/production-runtime/index.d.ts.map +1 -1
  312. package/dist/production-runtime/index.js +4 -2
  313. package/dist/production-runtime/index.js.map +1 -1
  314. package/dist/production-runtime/passes/DepthPrepass.d.ts +18 -1
  315. package/dist/production-runtime/passes/DepthPrepass.d.ts.map +1 -1
  316. package/dist/production-runtime/passes/DepthPrepass.js +41 -2
  317. package/dist/production-runtime/passes/DepthPrepass.js.map +1 -1
  318. package/dist/production-runtime/passes/FramegraphTopology.d.ts +13 -0
  319. package/dist/production-runtime/passes/FramegraphTopology.d.ts.map +1 -0
  320. package/dist/production-runtime/passes/FramegraphTopology.js +59 -0
  321. package/dist/production-runtime/passes/FramegraphTopology.js.map +1 -0
  322. package/dist/production-runtime/passes/OpaquePass.d.ts +19 -1
  323. package/dist/production-runtime/passes/OpaquePass.d.ts.map +1 -1
  324. package/dist/production-runtime/passes/OpaquePass.js +43 -2
  325. package/dist/production-runtime/passes/OpaquePass.js.map +1 -1
  326. package/dist/production-runtime/passes/ShadowPass.d.ts +19 -1
  327. package/dist/production-runtime/passes/ShadowPass.d.ts.map +1 -1
  328. package/dist/production-runtime/passes/ShadowPass.js +45 -2
  329. package/dist/production-runtime/passes/ShadowPass.js.map +1 -1
  330. package/dist/production-runtime/passes/SkyboxPass.d.ts +17 -1
  331. package/dist/production-runtime/passes/SkyboxPass.d.ts.map +1 -1
  332. package/dist/production-runtime/passes/SkyboxPass.js +41 -2
  333. package/dist/production-runtime/passes/SkyboxPass.js.map +1 -1
  334. package/dist/production-runtime/passes/ToneMappingPass.d.ts +26 -1
  335. package/dist/production-runtime/passes/ToneMappingPass.d.ts.map +1 -1
  336. package/dist/production-runtime/passes/ToneMappingPass.js +62 -2
  337. package/dist/production-runtime/passes/ToneMappingPass.js.map +1 -1
  338. package/dist/production-runtime/passes/TransparentPass.d.ts +19 -1
  339. package/dist/production-runtime/passes/TransparentPass.d.ts.map +1 -1
  340. package/dist/production-runtime/passes/TransparentPass.js +47 -2
  341. package/dist/production-runtime/passes/TransparentPass.js.map +1 -1
  342. package/dist/reflection-surfaces.d.ts +5 -0
  343. package/dist/reflection-surfaces.d.ts.map +1 -0
  344. package/dist/reflection-surfaces.js +3 -0
  345. package/dist/reflection-surfaces.js.map +1 -0
  346. package/dist/shadows/CascadeHysteresis.d.ts +67 -0
  347. package/dist/shadows/CascadeHysteresis.d.ts.map +1 -0
  348. package/dist/shadows/CascadeHysteresis.js +104 -0
  349. package/dist/shadows/CascadeHysteresis.js.map +1 -0
  350. package/dist/shadows/ContactShadows.d.ts +52 -0
  351. package/dist/shadows/ContactShadows.d.ts.map +1 -1
  352. package/dist/shadows/ContactShadows.js +122 -0
  353. package/dist/shadows/ContactShadows.js.map +1 -1
  354. package/dist/shadows/SpotShadowMaps.d.ts +77 -0
  355. package/dist/shadows/SpotShadowMaps.d.ts.map +1 -0
  356. package/dist/shadows/SpotShadowMaps.js +106 -0
  357. package/dist/shadows/SpotShadowMaps.js.map +1 -0
  358. package/dist/webgpu/WebGPUPostProcess.d.ts +2 -0
  359. package/dist/webgpu/WebGPUPostProcess.d.ts.map +1 -1
  360. package/dist/webgpu/WebGPUPostProcess.js +1 -0
  361. package/dist/webgpu/WebGPUPostProcess.js.map +1 -1
  362. package/dist/webgpu/WebGPUPostShaders.d.ts +77 -0
  363. package/dist/webgpu/WebGPUPostShaders.d.ts.map +1 -0
  364. package/dist/webgpu/WebGPUPostShaders.js +261 -0
  365. package/dist/webgpu/WebGPUPostShaders.js.map +1 -0
  366. package/dist/webgpu/WebGPUTemporal.d.ts +17 -0
  367. package/dist/webgpu/WebGPUTemporal.d.ts.map +1 -0
  368. package/dist/webgpu/WebGPUTemporal.js +64 -0
  369. package/dist/webgpu/WebGPUTemporal.js.map +1 -0
  370. package/dist/webgpu.d.ts +6 -0
  371. package/dist/webgpu.d.ts.map +1 -1
  372. package/dist/webgpu.js +4 -0
  373. package/dist/webgpu.js.map +1 -1
  374. package/package.json +15 -3
@@ -1,5 +1,9 @@
1
- import { RenderDeviceError, viewBytes } from "./RenderDevice.js";
1
+ import { WEBGPU_EXTENSION_ATLAS_WGSL, webgpuExtensionAtlasUniforms } from "./WebGPUExtensionAtlas.js";
2
+ import { RenderDeviceError, viewBytes, resolveGpuTargetOwner, spreadGpuTargetInventory } from "./RenderDevice.js";
2
3
  import { MAX_WEBGPU_SKINNING_JOINTS } from "./WebGPUSkinningLimits.js";
4
+ import { normalizeWebGPUTemporalOptions, WEBGPU_TAA_FRAGMENT, WEBGPU_TAA_OUTPUT_FRAGMENT } from "./webgpu/WebGPUTemporal.js";
5
+ import { defaultWebGPUBloomWeights, normalizeWebGPUColorGradeOptions, normalizeWebGPUBloomOptions, webgpuBlurFragment, webgpuBloomCompositeFragment, webgpuBrightExtractFragment, webgpuColorGradeFragment, webgpuFxaaFragment, WEBGPU_POST_VERTEX_WGSL } from "./webgpu/WebGPUPostShaders.js";
6
+ import { DEFAULT_DEPTH_SHADER_MARKER } from "./ShaderLibraryCore.js";
3
7
  import { reflectShaderSources } from "./ShaderReflection.js";
4
8
  import { Texture, bytesPerPixel, isCompressedTextureFormat } from "./Texture.js";
5
9
  import { isTextureBinding, TextureBinding } from "./TextureBinding.js";
@@ -22,6 +26,26 @@ const TEXTURE_USAGE = {
22
26
  TEXTURE_BINDING: 0x0004
23
27
  };
24
28
  const DEPTH_TEXTURE_FORMAT = "depth24plus";
29
+ const WEBGPU_NATIVE_COPY_FRAGMENT = `
30
+ @group(0) @binding(0) var<uniform> params: vec4<f32>;
31
+ @group(0) @binding(1) var linearSampler: sampler;
32
+ @group(0) @binding(2) var source: texture_2d<f32>;
33
+ @fragment fn fs_copy(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
34
+ return textureSample(source, linearSampler, clamp(uv, params.xy * 0.5, vec2<f32>(1.0) - params.xy * 0.5));
35
+ }`;
36
+ const WEBGPU_NATIVE_TONE_FRAGMENT = `
37
+ @group(0) @binding(0) var<uniform> params: vec4<f32>;
38
+ @group(0) @binding(1) var linearSampler: sampler;
39
+ @group(0) @binding(2) var source: texture_2d<f32>;
40
+ @fragment fn fs_tone(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
41
+ let sample = textureSample(source, linearSampler, uv);
42
+ var c = max(sample.rgb * params.x / params.w, vec3<f32>(0.0));
43
+ if (params.y > 2.5) { let toe=max(c-vec3<f32>(0.004),vec3<f32>(0.0)); c=min((toe*(6.2*toe+vec3<f32>(0.5)))/(toe*(6.2*toe+vec3<f32>(1.7))+vec3<f32>(0.06)),c*1.08); }
44
+ else if (params.y > 1.5) { c = clamp((c * (2.51 * c + vec3<f32>(0.03))) / (c * (2.43 * c + vec3<f32>(0.59)) + vec3<f32>(0.14)), vec3<f32>(0.0), vec3<f32>(1.0)); }
45
+ else if (params.y > 0.5) { c = c / (vec3<f32>(1.0) + c); }
46
+ if (params.z > 0.5) { c = select(1.055 * pow(c, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055), c * 12.92, c <= vec3<f32>(0.0031308)); }
47
+ return vec4<f32>(c, sample.a);
48
+ }`;
25
49
  class WebGPURenderBuffer {
26
50
  id;
27
51
  usage;
@@ -56,9 +80,13 @@ class WebGPUShaderProgram {
56
80
  entryPoints;
57
81
  nativeUniformLayout;
58
82
  portableBindings;
83
+ nativeColorDefaultVertex;
84
+ nativeAtlasVertex;
59
85
  disposed = false;
60
86
  renderPipelines = new Map();
61
- constructor(id, label, marker, reflection, modules, entryPoints, nativeUniformLayout, portableBindings) {
87
+ nativeColorDefaultModule;
88
+ atlasVertexModules = new Map();
89
+ constructor(id, label, marker, reflection, modules, entryPoints, nativeUniformLayout, portableBindings, nativeColorDefaultVertex, nativeAtlasVertex) {
62
90
  this.id = id;
63
91
  this.label = label;
64
92
  this.marker = marker;
@@ -67,6 +95,8 @@ class WebGPUShaderProgram {
67
95
  this.entryPoints = entryPoints;
68
96
  this.nativeUniformLayout = nativeUniformLayout;
69
97
  this.portableBindings = portableBindings;
98
+ this.nativeColorDefaultVertex = nativeColorDefaultVertex;
99
+ this.nativeAtlasVertex = nativeAtlasVertex;
70
100
  }
71
101
  dispose() {
72
102
  this.disposed = true;
@@ -157,6 +187,24 @@ export class WebGPUDevice {
157
187
  nativeMorphSubmissions = 0;
158
188
  nativeEnvironmentBindings = 0;
159
189
  nativeShadowMapBindings = 0;
190
+ nativeBloomPasses = 0;
191
+ nativeColorGradePasses = 0;
192
+ nativeFxaaPasses = 0;
193
+ nativeTaaPasses = 0;
194
+ nativeTemporalBindings = 0;
195
+ webgpuPostPipelines = new Map();
196
+ webgpuPostLinearSampler = null;
197
+ lastWebGPUBloom = null;
198
+ /**
199
+ * Uniform buffers referenced by submitted-but-maybe-unexecuted post
200
+ * passes. Destroying them synchronously after submit races the GPU (the
201
+ * reads happen later), so they retire here and are reclaimed at the start
202
+ * of the next post run — after any intervening readback fence — or on
203
+ * dispose.
204
+ */
205
+ webgpuPostRetiredUniformBuffers = [];
206
+ webgpuPostErrorScopePending = [];
207
+ webgpuPostErrors = [];
160
208
  canvasSubmissions = 0;
161
209
  buffers = new Set();
162
210
  shaders = new Set();
@@ -294,7 +342,7 @@ export class WebGPUDevice {
294
342
  this.device.createShaderModule({ label: `${sources.label}-fragment`, code: nativeSources.fragment })
295
343
  ]
296
344
  : [];
297
- const shader = new WebGPUShaderProgram(this.nextId++, sources.label, sources.marker, reflectShaderSources(sources), modules, nativeSources.entryPoints, nativeSources.uniformLayout, nativeSources.portableBindings ?? []);
345
+ const shader = new WebGPUShaderProgram(this.nextId++, sources.label, sources.marker, reflectShaderSources(sources), modules, nativeSources.entryPoints, nativeSources.uniformLayout, nativeSources.portableBindings ?? [], nativeSources.colorDefaultVertex, nativeSources.atlasVertex);
298
346
  this.shaders.add(shader);
299
347
  return shader;
300
348
  }
@@ -769,6 +817,13 @@ export class WebGPUDevice {
769
817
  this.rasterizeDraw(command, vertexBuffer);
770
818
  this.drawCalls += 1;
771
819
  }
820
+ async waitForSubmittedWork() {
821
+ this.assertAlive();
822
+ if (!this.device.queue.onSubmittedWorkDone) {
823
+ throw new RenderDeviceError("Native GPU queue completion is unavailable", "GPU_COMPLETION_UNAVAILABLE");
824
+ }
825
+ await this.device.queue.onSubmittedWorkDone();
826
+ }
772
827
  endFrame() {
773
828
  this.assertFrame();
774
829
  this.frameActive = false;
@@ -800,6 +855,11 @@ export class WebGPUDevice {
800
855
  ["nativeMorphSubmissions", this.nativeMorphSubmissions],
801
856
  ["nativeEnvironmentBindings", this.nativeEnvironmentBindings],
802
857
  ["nativeShadowMapBindings", this.nativeShadowMapBindings],
858
+ ["nativeBloomPasses", this.nativeBloomPasses],
859
+ ["nativeColorGradePasses", this.nativeColorGradePasses],
860
+ ["nativeFxaaPasses", this.nativeFxaaPasses],
861
+ ["nativeTaaPasses", this.nativeTaaPasses],
862
+ ["nativeTemporalBindings", this.nativeTemporalBindings],
803
863
  ["canvasSubmissions", this.canvasSubmissions]
804
864
  ]);
805
865
  }
@@ -825,6 +885,12 @@ export class WebGPUDevice {
825
885
  disposedShaders: [...this.shaders].filter((shader) => shader.disposed).length,
826
886
  disposedRenderTargets: [...this.renderTargets].filter((target) => target.disposed).length,
827
887
  disposedTextures: [...this.renderTargets].reduce((total, target) => total + (target.colorTexture.disposed ? 1 : 0) + (target.depthTexture?.disposed ? 1 : 0), 0),
888
+ ...spreadGpuTargetInventory(liveRenderTargets.map((target) => ({
889
+ label: target.label,
890
+ kind: "render-target",
891
+ bytes: target.colorTexture.byteLength + (target.depthTexture?.byteLength ?? 0),
892
+ owner: resolveGpuTargetOwner(target.label)
893
+ }))),
828
894
  nativeSubmissions: this.nativeSubmissions,
829
895
  nativeRenderPipelinesCreated: this.nativeRenderPipelinesCreated,
830
896
  nativeRenderPasses: this.nativeRenderPasses,
@@ -840,13 +906,407 @@ export class WebGPUDevice {
840
906
  nativeMorphSubmissions: this.nativeMorphSubmissions,
841
907
  nativeEnvironmentBindings: this.nativeEnvironmentBindings,
842
908
  nativeShadowMapBindings: this.nativeShadowMapBindings,
909
+ nativeBloomPasses: this.nativeBloomPasses,
910
+ nativeColorGradePasses: this.nativeColorGradePasses,
911
+ nativeFxaaPasses: this.nativeFxaaPasses,
843
912
  lastError: this.lastError,
844
913
  contextLost: this.contextLost
845
914
  };
846
915
  }
916
+ /**
917
+ * muse3jsparity-PRD J2 — native WebGPU bloom pyramid.
918
+ *
919
+ * Renders bright-extract -> separable-blur mip pyramid -> composite over
920
+ * the source, all as real WGSL fullscreen passes on this device. Returns a
921
+ * fresh rgba16f target owned by the caller (dispose it); every intermediate
922
+ * is disposed before return. Throws fail-closed when the native device
923
+ * cannot run fullscreen passes.
924
+ */
925
+ executeWebGPUBloom(source, options = {}) {
926
+ this.reclaimWebGPUPostUniformBuffers();
927
+ const normalized = normalizeWebGPUBloomOptions(options);
928
+ const src = this.requireRenderTarget(source);
929
+ const srcView = src.nativeView;
930
+ if (!srcView) {
931
+ throw new RenderDeviceError("WebGPU bloom source target has no native view", "NATIVE_POST_UNSUPPORTED", { label: src.label });
932
+ }
933
+ const width = src.width;
934
+ const height = src.height;
935
+ // Preserve HDR source energy even at performance quality. Bloom adds to
936
+ // scene radiance before tone mapping, so its composite is always floating point.
937
+ const halfFloat = normalized.halfFloat || src.colorTexture.format === "rgba16f" || src.colorTexture.format === "rgba32f";
938
+ const mipFormat = halfFloat ? "rgba16f" : "rgba8";
939
+ const owned = [];
940
+ const track = (target) => {
941
+ owned.push(target);
942
+ return target;
943
+ };
944
+ try {
945
+ const bright = track(this.createRenderTarget({ width, height, format: mipFormat, label: "a3d-webgpu-bloom-bright" }));
946
+ this.runWebGPUFullscreenPass({
947
+ label: "webgpu-bloom-bright",
948
+ fragment: webgpuBrightExtractFragment(),
949
+ entryPoint: "fs_bright",
950
+ uniforms: new Float32Array([normalized.threshold, normalized.knee, normalized.strength, 0]),
951
+ views: [srcView],
952
+ target: bright
953
+ });
954
+ const mips = [];
955
+ let level = bright;
956
+ let levelWidth = width;
957
+ let levelHeight = height;
958
+ for (let mip = 0; mip < normalized.mipCount; mip += 1) {
959
+ levelWidth = Math.max(1, levelWidth >> 1);
960
+ levelHeight = Math.max(1, levelHeight >> 1);
961
+ const scratch = track(this.createRenderTarget({ width: levelWidth, height: levelHeight, format: mipFormat, label: `a3d-webgpu-bloom-mip${mip}-scratch` }));
962
+ const mipTarget = track(this.createRenderTarget({ width: levelWidth, height: levelHeight, format: mipFormat, label: `a3d-webgpu-bloom-mip${mip}` }));
963
+ const levelView = this.requireRenderTarget(level).nativeView;
964
+ const scratchView = this.requireRenderTarget(scratch).nativeView;
965
+ if (!levelView || !scratchView) {
966
+ throw new RenderDeviceError("WebGPU bloom pyramid target has no native view", "NATIVE_POST_UNSUPPORTED", { mip });
967
+ }
968
+ this.runWebGPUFullscreenPass({
969
+ label: `webgpu-bloom-mip${mip}-h`,
970
+ fragment: webgpuBlurFragment(),
971
+ entryPoint: "fs_blur",
972
+ uniforms: new Float32Array([1, 0, 1 / levelWidth, 1 / levelHeight]),
973
+ views: [levelView],
974
+ target: scratch
975
+ });
976
+ this.runWebGPUFullscreenPass({
977
+ label: `webgpu-bloom-mip${mip}-v`,
978
+ fragment: webgpuBlurFragment(),
979
+ entryPoint: "fs_blur",
980
+ uniforms: new Float32Array([0, 1, 1 / levelWidth, 1 / levelHeight]),
981
+ views: [scratchView],
982
+ target: mipTarget
983
+ });
984
+ mips.push(mipTarget);
985
+ level = mipTarget;
986
+ }
987
+ const weights = defaultWebGPUBloomWeights(normalized.mipCount);
988
+ const composite = track(this.createRenderTarget({ width, height, format: "rgba16f", label: "a3d-webgpu-bloom-composite" }));
989
+ const mipViews = mips.map((mipTarget) => {
990
+ const view = this.requireRenderTarget(mipTarget).nativeView;
991
+ if (!view)
992
+ throw new RenderDeviceError("WebGPU bloom mip has no native view", "NATIVE_POST_UNSUPPORTED", {});
993
+ return view;
994
+ });
995
+ this.runWebGPUFullscreenPass({
996
+ label: "webgpu-bloom-composite",
997
+ fragment: webgpuBloomCompositeFragment({ weights, strength: normalized.strength }),
998
+ entryPoint: "fs_composite",
999
+ uniforms: new Float32Array([normalized.strength, 0, 0, 0]),
1000
+ views: [srcView, ...mipViews],
1001
+ target: composite
1002
+ });
1003
+ const bytesPerPixel = halfFloat ? 8 : 4;
1004
+ const targetBytes = width * height * bytesPerPixel
1005
+ + mips.reduce((total, mipTarget) => total + mipTarget.width * mipTarget.height * bytesPerPixel * 2, 0)
1006
+ + width * height * 8;
1007
+ this.lastWebGPUBloom = {
1008
+ mipCount: normalized.mipCount,
1009
+ halfFloat,
1010
+ targetBytes,
1011
+ compositeGain: normalized.strength,
1012
+ passes: 1 + normalized.mipCount * 2 + 1,
1013
+ executionMode: "webgpu-native-post"
1014
+ };
1015
+ this.nativeBloomPasses += this.lastWebGPUBloom.passes;
1016
+ for (const target of owned) {
1017
+ if (target !== composite)
1018
+ target.dispose();
1019
+ }
1020
+ return composite;
1021
+ }
1022
+ catch (error) {
1023
+ for (const target of owned)
1024
+ target.dispose();
1025
+ throw error;
1026
+ }
1027
+ }
1028
+ /** Last native bloom run on this device (null before the first run). */
1029
+ getWebGPUBloomDiagnostics() {
1030
+ return this.lastWebGPUBloom;
1031
+ }
1032
+ /**
1033
+ * Drains GPU validation errors observed by the native post passes.
1034
+ * Empty means every submitted post pass validated clean.
1035
+ */
1036
+ async drainWebGPUPostErrors() {
1037
+ const pending = this.webgpuPostErrorScopePending;
1038
+ this.webgpuPostErrorScopePending = [];
1039
+ await Promise.all(pending);
1040
+ const errors = [...this.webgpuPostErrors];
1041
+ this.webgpuPostErrors = [];
1042
+ return errors;
1043
+ }
1044
+ /**
1045
+ * muse3jsparity-PRD J2 — native WebGPU color grade.
1046
+ * Exposure (stops) -> saturation around luma -> contrast around middle
1047
+ * gray, one WGSL fullscreen pass into a fresh caller-owned rgba8 target.
1048
+ */
1049
+ executeWebGPUColorGrade(source, options = {}) {
1050
+ this.reclaimWebGPUPostUniformBuffers();
1051
+ const normalized = normalizeWebGPUColorGradeOptions(options);
1052
+ const src = this.requireRenderTarget(source);
1053
+ const srcView = src.nativeView;
1054
+ if (!srcView) {
1055
+ throw new RenderDeviceError("WebGPU color-grade source target has no native view", "NATIVE_POST_UNSUPPORTED", { label: src.label });
1056
+ }
1057
+ const out = this.createRenderTarget({ width: src.width, height: src.height, format: "rgba8", label: "a3d-webgpu-color-grade" });
1058
+ try {
1059
+ this.runWebGPUFullscreenPass({
1060
+ label: "webgpu-color-grade",
1061
+ fragment: webgpuColorGradeFragment(),
1062
+ entryPoint: "fs_grade",
1063
+ uniforms: new Float32Array([normalized.exposure, normalized.contrast, normalized.saturation, 0]),
1064
+ views: [srcView],
1065
+ target: out
1066
+ });
1067
+ this.nativeColorGradePasses += 1;
1068
+ return out;
1069
+ }
1070
+ catch (error) {
1071
+ out.dispose();
1072
+ throw error;
1073
+ }
1074
+ }
1075
+ /**
1076
+ * muse3jsparity-PRD J2 — native WebGPU FXAA.
1077
+ * Compact FXAA 3.11 console core (luma neighborhood, one perpendicular
1078
+ * blend tap pair; flat neighborhoods pass through untouched). Temporal
1079
+ * antialiasing has a separate GPU history contract in executeWebGPUTaa.
1080
+ */
1081
+ executeWebGPUFxaa(source) {
1082
+ this.reclaimWebGPUPostUniformBuffers();
1083
+ const src = this.requireRenderTarget(source);
1084
+ const srcView = src.nativeView;
1085
+ if (!srcView) {
1086
+ throw new RenderDeviceError("WebGPU FXAA source target has no native view", "NATIVE_POST_UNSUPPORTED", { label: src.label });
1087
+ }
1088
+ const out = this.createRenderTarget({ width: src.width, height: src.height, format: "rgba8", label: "a3d-webgpu-fxaa" });
1089
+ try {
1090
+ this.runWebGPUFullscreenPass({
1091
+ label: "webgpu-fxaa",
1092
+ fragment: webgpuFxaaFragment(),
1093
+ entryPoint: "fs_fxaa",
1094
+ uniforms: new Float32Array([1 / src.width, 1 / src.height, 0, 0]),
1095
+ views: [srcView],
1096
+ target: out
1097
+ });
1098
+ this.nativeFxaaPasses += 1;
1099
+ return out;
1100
+ }
1101
+ catch (error) {
1102
+ out.dispose();
1103
+ throw error;
1104
+ }
1105
+ }
1106
+ /** Native target-to-target chain. Unsupported passes fail before any submission. */
1107
+ presentLdrPostprocess(source, options) {
1108
+ this.assertAlive();
1109
+ const supported = new Set(["bloom", "tone-mapping", "color-grade", "fxaa", "taa"]);
1110
+ for (const pass of options.passes) {
1111
+ if (!supported.has(pass.name))
1112
+ throw new RenderDeviceError(`Native WebGPU postprocess does not support ${pass.name}`, "WEBGPU_POSTPROCESS_UNSUPPORTED", { pass: pass.name });
1113
+ }
1114
+ const owned = [];
1115
+ let current = source;
1116
+ const track = (target) => { owned.push(target); current = target; };
1117
+ try {
1118
+ for (const pass of options.passes) {
1119
+ if (pass.name === "bloom")
1120
+ track(this.executeWebGPUBloom(current, pass.options));
1121
+ else if (pass.name === "color-grade")
1122
+ track(this.executeWebGPUColorGrade(current, pass.options));
1123
+ else if (pass.name === "fxaa")
1124
+ track(this.executeWebGPUFxaa(current));
1125
+ else if (pass.name === "taa") {
1126
+ const temporal = pass.options.temporal;
1127
+ if (!temporal)
1128
+ throw new RenderDeviceError("Native WebGPU TAA requires GPU temporal bindings", "TEMPORAL_NATIVE_REQUIRED");
1129
+ track(this.executeWebGPUTaa(current, { ...temporal, ...(typeof pass.options.blend === "number" ? { blend: pass.options.blend } : {}) }));
1130
+ }
1131
+ else if (pass.name === "tone-mapping") {
1132
+ const tone = { ...options.toneMappingDefaults, ...pass.options };
1133
+ const operator = tone.operator ?? "reinhard";
1134
+ const operatorId = operator === "linear" || operator === "none" ? 0 : operator === "reinhard" ? 1 : operator === "aces" || operator === "aces-filmic" ? 2 : operator === "filmic" ? 3 : -1;
1135
+ if (operatorId < 0)
1136
+ throw new RenderDeviceError(`Unsupported native WebGPU tone operator ${String(operator)}`, "WEBGPU_POSTPROCESS_UNSUPPORTED");
1137
+ const exposure = typeof tone.exposure === "number" ? tone.exposure : 1;
1138
+ const whitePoint = typeof tone.whitePoint === "number" ? tone.whitePoint : 1;
1139
+ if (!Number.isFinite(whitePoint) || whitePoint <= 0)
1140
+ throw new RenderDeviceError("Invalid tone white point", "WEBGPU_POSTPROCESS_UNSUPPORTED");
1141
+ if (!Number.isFinite(exposure) || exposure < 0)
1142
+ throw new RenderDeviceError("Invalid tone exposure", "WEBGPU_POSTPROCESS_UNSUPPORTED");
1143
+ const target = this.createRenderTarget({ width: source.width, height: source.height, format: "rgba8", label: "a3d-webgpu-tone" });
1144
+ owned.push(target);
1145
+ this.runWebGPUFullscreenPass({ label: "webgpu-tone", fragment: WEBGPU_NATIVE_TONE_FRAGMENT, entryPoint: "fs_tone",
1146
+ uniforms: new Float32Array([exposure, operatorId, tone.outputColorSpace === "srgb" ? 1 : 0, whitePoint]),
1147
+ views: [this.requireRenderTarget(current).nativeView], target });
1148
+ current = target;
1149
+ }
1150
+ }
1151
+ const target = options.outputTarget ?? null;
1152
+ if (target === current)
1153
+ throw new RenderDeviceError("Native WebGPU presentation source/output alias", "INVALID_RESOURCE");
1154
+ this.runWebGPUFullscreenPass({ label: "webgpu-post-present", fragment: WEBGPU_NATIVE_COPY_FRAGMENT, entryPoint: "fs_copy",
1155
+ uniforms: new Float32Array([1 / source.width, 1 / source.height, 0, 0]),
1156
+ views: [this.requireRenderTarget(current).nativeView], target });
1157
+ if (!target) {
1158
+ this.activeRenderTarget = null;
1159
+ this.canvasSubmissions += 1;
1160
+ }
1161
+ }
1162
+ finally {
1163
+ for (const target of owned)
1164
+ target.dispose();
1165
+ }
1166
+ }
1167
+ /** Resolve GPU velocity/history into caller-owned historyOutput, returning scene-alpha color. */
1168
+ executeWebGPUTaa(source, options) {
1169
+ this.assertAlive();
1170
+ const [blend, depthThreshold] = normalizeWebGPUTemporalOptions(options);
1171
+ const targets = [source, options.velocity, options.history, options.historyOutput];
1172
+ if (new Set(targets).size !== targets.length)
1173
+ throw new RenderDeviceError("TAA targets must not alias", "NATIVE_POST_UNSUPPORTED");
1174
+ const native = targets.map((target) => this.requireRenderTarget(target));
1175
+ if (native.some((target) => target.width !== source.width || target.height !== source.height || !target.nativeView)) {
1176
+ throw new RenderDeviceError("TAA requires same-size native source, velocity and history targets", "NATIVE_POST_UNSUPPORTED");
1177
+ }
1178
+ if (targets.slice(1).some((target) => target.colorTexture.format !== "rgba16f")) {
1179
+ throw new RenderDeviceError("TAA velocity/history targets require rgba16f", "NATIVE_POST_UNSUPPORTED");
1180
+ }
1181
+ const out = this.createRenderTarget({ width: source.width, height: source.height, format: "rgba16f", label: "a3d-webgpu-taa-color" });
1182
+ try {
1183
+ this.runWebGPUFullscreenPass({ label: "webgpu-taa", fragment: WEBGPU_TAA_FRAGMENT, entryPoint: "fs_taa",
1184
+ uniforms: new Float32Array([1 / source.width, 1 / source.height, blend, options.historyValid ? 1 : 0, depthThreshold, 0, 0, 0]),
1185
+ views: native.slice(0, 3).map((target) => target.nativeView), target: options.historyOutput });
1186
+ this.runWebGPUFullscreenPass({ label: "webgpu-taa-output", fragment: WEBGPU_TAA_OUTPUT_FRAGMENT, entryPoint: "fs_taa_output",
1187
+ uniforms: new Float32Array([1 / source.width, 1 / source.height, 0, 0]),
1188
+ views: [native[3].nativeView, native[0].nativeView], target: out });
1189
+ this.nativeTaaPasses += 2;
1190
+ this.nativeTemporalBindings += 3;
1191
+ return out;
1192
+ }
1193
+ catch (error) {
1194
+ out.dispose();
1195
+ throw error;
1196
+ }
1197
+ }
1198
+ runWebGPUFullscreenPass(args) {
1199
+ // No assertFrame here on purpose: these are target-to-target passes with
1200
+ // their own encoders, so they run both inside a Renderer frame (scene ->
1201
+ // post chaining) and after it closed (proof harnesses that render, then
1202
+ // post, then read back).
1203
+ this.assertAlive();
1204
+ const target = args.target ? this.requireRenderTarget(args.target) : null;
1205
+ const targetView = target ? target.nativeView : this.canvasContext?.getCurrentTexture().createView();
1206
+ if (!targetView) {
1207
+ throw new RenderDeviceError("WebGPU post target has no native view", "NATIVE_POST_UNSUPPORTED", { label: target?.label ?? "canvas" });
1208
+ }
1209
+ if (!this.device.createShaderModule || !this.device.createRenderPipeline || !this.device.createCommandEncoder
1210
+ || !this.device.createSampler || !this.device.createBuffer || !this.device.createBindGroup) {
1211
+ throw new RenderDeviceError("WebGPU fullscreen post requires shader/pipeline/sampler/buffer natives", "NATIVE_POST_UNSUPPORTED", { label: args.label });
1212
+ }
1213
+ const scopeDevice = this.device;
1214
+ const scopedErrors = Boolean(scopeDevice.pushErrorScope && scopeDevice.popErrorScope);
1215
+ if (scopedErrors) {
1216
+ // Bracket everything below (module/pipeline/bind-group creation plus
1217
+ // the submit) so per-pass validation failures land here labeled.
1218
+ scopeDevice.pushErrorScope?.("validation");
1219
+ }
1220
+ const format = target ? webgpuTextureFormat(target.colorTexture.format) : this.presentationFormat;
1221
+ // Bloom quality changes the fragment binding layout (one texture per mip).
1222
+ // A label/format-only key can reuse the wrong native pipeline after a quality change.
1223
+ const cacheKey = `${args.label}:${args.entryPoint}:${format}:${args.fragment}`;
1224
+ let pipeline = this.webgpuPostPipelines.get(cacheKey);
1225
+ if (!pipeline) {
1226
+ const vertexModule = this.device.createShaderModule({ label: `${args.label}-vs`, code: WEBGPU_POST_VERTEX_WGSL });
1227
+ const fragmentModule = this.device.createShaderModule({ label: `${args.label}-fs`, code: args.fragment });
1228
+ pipeline = this.device.createRenderPipeline({
1229
+ label: `${args.label}-pipeline`,
1230
+ layout: "auto",
1231
+ vertex: { module: vertexModule, entryPoint: "vs_post" },
1232
+ fragment: { module: fragmentModule, entryPoint: args.entryPoint, targets: [{ format }] },
1233
+ primitive: { topology: "triangle-list" }
1234
+ });
1235
+ this.nativeRenderPipelinesCreated += 1;
1236
+ this.webgpuPostPipelines.set(cacheKey, pipeline);
1237
+ }
1238
+ if (!this.webgpuPostLinearSampler) {
1239
+ this.webgpuPostLinearSampler = this.device.createSampler({
1240
+ minFilter: "linear",
1241
+ magFilter: "linear",
1242
+ addressModeU: "clamp-to-edge",
1243
+ addressModeV: "clamp-to-edge"
1244
+ });
1245
+ }
1246
+ const uniformBuffer = this.device.createBuffer({
1247
+ label: `${args.label}-uniforms`,
1248
+ size: args.uniforms.byteLength,
1249
+ usage: BUFFER_USAGE.UNIFORM | BUFFER_USAGE.COPY_DST
1250
+ });
1251
+ let submitted = false;
1252
+ try {
1253
+ this.device.queue.writeBuffer(uniformBuffer, 0, args.uniforms);
1254
+ const layout = pipeline.getBindGroupLayout?.(0);
1255
+ if (!layout)
1256
+ throw new RenderDeviceError("WebGPU post pipeline has no bind-group layout", "NATIVE_POST_UNSUPPORTED", { label: args.label });
1257
+ const group = this.device.createBindGroup({
1258
+ label: `${args.label}-bind`,
1259
+ layout,
1260
+ entries: [
1261
+ { binding: 0, resource: { buffer: uniformBuffer } },
1262
+ { binding: 1, resource: this.webgpuPostLinearSampler },
1263
+ ...args.views.map((view, index) => ({ binding: 2 + index, resource: view }))
1264
+ ]
1265
+ });
1266
+ const encoder = this.device.createCommandEncoder({ label: `${args.label}-encoder` });
1267
+ const pass = encoder.beginRenderPass({
1268
+ label: `${args.label}-pass`,
1269
+ colorAttachments: [{ view: targetView, loadOp: "load", storeOp: "store" }]
1270
+ });
1271
+ pass.setPipeline(pipeline);
1272
+ pass.setBindGroup?.(0, group);
1273
+ pass.draw(3, 1, 0);
1274
+ pass.end();
1275
+ this.device.queue.submit([encoder.finish()]);
1276
+ submitted = true;
1277
+ // The pass owns every pixel (fullscreen triangle), so a later
1278
+ // readPixelsAsync must NOT flush a stale pending clear over it.
1279
+ if (target)
1280
+ target.nativeNeedsClear = false;
1281
+ this.nativeRenderPasses += 1;
1282
+ // Retired, not destroyed: the submit only enqueues; the pass reads
1283
+ // these uniforms when the GPU gets to it.
1284
+ this.webgpuPostRetiredUniformBuffers.push(uniformBuffer);
1285
+ }
1286
+ catch (error) {
1287
+ if (!submitted)
1288
+ uniformBuffer.destroy();
1289
+ throw error;
1290
+ }
1291
+ finally {
1292
+ if (scopedErrors && scopeDevice.popErrorScope) {
1293
+ const pending = scopeDevice.popErrorScope().then((info) => {
1294
+ if (info?.message)
1295
+ this.webgpuPostErrors.push(`[${args.label}] ${info.message}`);
1296
+ }, () => undefined);
1297
+ this.webgpuPostErrorScopePending.push(pending);
1298
+ }
1299
+ }
1300
+ }
1301
+ reclaimWebGPUPostUniformBuffers() {
1302
+ for (const buffer of this.webgpuPostRetiredUniformBuffers)
1303
+ buffer.destroy();
1304
+ this.webgpuPostRetiredUniformBuffers = [];
1305
+ }
847
1306
  dispose() {
848
1307
  if (this.disposed)
849
1308
  return;
1309
+ this.reclaimWebGPUPostUniformBuffers();
850
1310
  for (const buffer of this.buffers)
851
1311
  buffer.dispose();
852
1312
  for (const shader of this.shaders)
@@ -878,6 +1338,8 @@ export class WebGPUDevice {
878
1338
  throw new RenderDeviceError("No active frame", "NO_ACTIVE_FRAME");
879
1339
  }
880
1340
  }
1341
+ temporalLossListeners = new Set();
1342
+ onDeviceLost(listener) { this.temporalLossListeners.add(listener); return () => this.temporalLossListeners.delete(listener); }
881
1343
  observeDeviceLost() {
882
1344
  const lost = this.device.lost;
883
1345
  if (!lost || typeof lost.then !== "function")
@@ -886,12 +1348,24 @@ export class WebGPUDevice {
886
1348
  if (this.disposed)
887
1349
  return;
888
1350
  this.contextLost = true;
1351
+ for (const listener of this.temporalLossListeners) {
1352
+ try {
1353
+ listener();
1354
+ }
1355
+ catch { /* Continue notifying owners. */ }
1356
+ }
889
1357
  const detail = [info.reason, info.message].filter(Boolean).join(": ");
890
1358
  this.lastError = detail ? `WebGPU device lost: ${detail}` : "WebGPU device lost";
891
1359
  }).catch((error) => {
892
1360
  if (this.disposed)
893
1361
  return;
894
1362
  this.contextLost = true;
1363
+ for (const listener of this.temporalLossListeners) {
1364
+ try {
1365
+ listener();
1366
+ }
1367
+ catch { /* Continue notifying owners. */ }
1368
+ }
895
1369
  this.lastError = `WebGPU device lost promise rejected: ${error instanceof Error ? error.message : String(error)}`;
896
1370
  });
897
1371
  }
@@ -965,9 +1439,9 @@ export class WebGPUDevice {
965
1439
  const worldA = transformPosition(localA, modelMatrix);
966
1440
  const worldB = transformPosition(localB, modelMatrix);
967
1441
  const worldC = transformPosition(localC, modelMatrix);
968
- const colorA = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexA);
969
- const colorB = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexB);
970
- const colorC = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexC);
1442
+ const colorA = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexA, colorAttribute?.components);
1443
+ const colorB = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexB, colorAttribute?.components);
1444
+ const colorC = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexC, colorAttribute?.components);
971
1445
  const uvA = readVertexUv(vertexBuffer.bytes, command.vertexFormat.stride, uvAttribute?.offset, indexA);
972
1446
  const uvB = readVertexUv(vertexBuffer.bytes, command.vertexFormat.stride, uvAttribute?.offset, indexB);
973
1447
  const uvC = readVertexUv(vertexBuffer.bytes, command.vertexFormat.stride, uvAttribute?.offset, indexC);
@@ -980,15 +1454,15 @@ export class WebGPUDevice {
980
1454
  const indexB = indices[offset + 1];
981
1455
  const a = transformPosition(readLocal(indexA), matrix);
982
1456
  const b = transformPosition(readLocal(indexB), matrix);
983
- const colorA = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexA);
984
- const colorB = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexB);
1457
+ const colorA = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexA, colorAttribute?.components);
1458
+ const colorB = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexB, colorAttribute?.components);
985
1459
  rasterizeLine(this.activeRenderTarget, a, b, color, colorA, colorB, depthTest, depthWrite);
986
1460
  }
987
1461
  }
988
1462
  else {
989
1463
  for (const index of indices) {
990
1464
  const point = transformPosition(readLocal(index), matrix);
991
- const vertexColor = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, index);
1465
+ const vertexColor = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, index, colorAttribute?.components);
992
1466
  rasterizePoint(this.activeRenderTarget, point, multiplyColor(color, vertexColor), depthTest, depthWrite);
993
1467
  }
994
1468
  }
@@ -1002,7 +1476,27 @@ export class WebGPUDevice {
1002
1476
  if (!hasNativeRenderPipeline(this.device))
1003
1477
  return;
1004
1478
  const shader = this.requireShader(command.shader);
1005
- const vertexModule = shader.modules[0];
1479
+ // GL supplies an opaque white default for an absent vertex color. Native
1480
+ // WGSL inputs must instead match the actual vertex buffer layout.
1481
+ const useDefaultColor = shader.nativeColorDefaultVertex && !command.vertexFormat?.attributes.some(attribute => attribute.semantic === "color");
1482
+ if (useDefaultColor && !shader.nativeColorDefaultModule) {
1483
+ shader.nativeColorDefaultModule = this.device.createShaderModule?.({ label: `${shader.label}-vertex-default-color`, code: shader.nativeColorDefaultVertex });
1484
+ }
1485
+ let vertexModule = useDefaultColor ? shader.nativeColorDefaultModule : shader.modules[0];
1486
+ if (shader.nativeAtlasVertex) {
1487
+ const hasColor = command.vertexFormat?.attributes.some(attribute => attribute.shaderLocation === 4) ?? false;
1488
+ const hasUv1 = command.vertexFormat?.attributes.some(attribute => attribute.shaderLocation === 7) ?? false;
1489
+ const key = `${hasColor}:${hasUv1}`;
1490
+ if (!shader.atlasVertexModules.has(key)) {
1491
+ let source = shader.nativeAtlasVertex;
1492
+ if (!hasColor)
1493
+ source = source.replace(", @location(4) color: vec4<f32>", "").replace("output.color = color;", "output.color = vec4<f32>(1.0);");
1494
+ if (!hasUv1)
1495
+ source = source.replace(", @location(7) uv1: vec2<f32>", "").replace("output.uv1 = uv1;", "output.uv1 = vec2<f32>(0.0);");
1496
+ shader.atlasVertexModules.set(key, this.device.createShaderModule?.({ label: `${shader.label}-atlas-vertex-${key}`, code: source }));
1497
+ }
1498
+ vertexModule = shader.atlasVertexModules.get(key);
1499
+ }
1006
1500
  const fragmentModule = shader.modules[1] ?? vertexModule;
1007
1501
  if (!vertexModule || !fragmentModule)
1008
1502
  return;
@@ -1062,12 +1556,19 @@ export class WebGPUDevice {
1062
1556
  const uniformBuffer = usesNativeDrawUniforms(shader.nativeUniformLayout) ? this.createNativeDrawUniformBuffer(command) : null;
1063
1557
  const sampledTexture = shader.nativeUniformLayout === "generated-texture" ? this.createNativeSampledTextureBinding(uniformBaseColorTextureBinding(command.uniforms)) : null;
1064
1558
  const materialTextures = usesNativePbrTextureBindings(shader.nativeUniformLayout) ? this.createNativePbrTextureBindings(command) : null;
1559
+ const atlasResources = shader.nativeUniformLayout === "generated-atlas-pbr" ? this.createNativeAtlasBindings(command) : null;
1560
+ if (shader.nativeUniformLayout === "generated-atlas-pbr" && !atlasResources) {
1561
+ uniformBuffer?.destroy();
1562
+ return;
1563
+ }
1065
1564
  if (shader.nativeUniformLayout === "generated-texture" && !sampledTexture) {
1066
1565
  uniformBuffer?.destroy();
1566
+ atlasResources?.buffer.destroy();
1067
1567
  return;
1068
1568
  }
1069
1569
  if (usesNativePbrTextureBindings(shader.nativeUniformLayout) && !materialTextures) {
1070
1570
  uniformBuffer?.destroy();
1571
+ atlasResources?.buffer.destroy();
1071
1572
  return;
1072
1573
  }
1073
1574
  if (shader.nativeUniformLayout === "portable" && !portableResources)
@@ -1078,6 +1579,7 @@ export class WebGPUDevice {
1078
1579
  layout: pipeline.getBindGroupLayout(0),
1079
1580
  entries: portableResources?.entries ?? [
1080
1581
  { binding: 0, resource: { buffer: uniformBuffer } },
1582
+ ...(atlasResources?.entries ?? []),
1081
1583
  ...(sampledTexture ? [
1082
1584
  { binding: 1, resource: sampledTexture.sampler },
1083
1585
  { binding: 2, resource: sampledTexture.view }
@@ -1098,7 +1600,9 @@ export class WebGPUDevice {
1098
1600
  { binding: 13, resource: materialTextures.occlusion.sampler },
1099
1601
  { binding: 14, resource: materialTextures.occlusion.view },
1100
1602
  { binding: 15, resource: materialTextures.clusterLightData.view },
1101
- { binding: 16, resource: materialTextures.clusterLightIndices.view }
1603
+ { binding: 16, resource: materialTextures.clusterLightIndices.view },
1604
+ { binding: 17, resource: materialTextures.spotShadow.sampler },
1605
+ { binding: 18, resource: materialTextures.spotShadow.view }
1102
1606
  ] : [])
1103
1607
  ]
1104
1608
  })
@@ -1132,6 +1636,7 @@ export class WebGPUDevice {
1132
1636
  if (!pass.setIndexBuffer || !pass.drawIndexed) {
1133
1637
  pass.end();
1134
1638
  uniformBuffer?.destroy();
1639
+ atlasResources?.buffer.destroy();
1135
1640
  portableResources?.buffer.destroy();
1136
1641
  this.lastError = "Native WebGPU indexed draw skipped because the render-pass encoder does not expose indexed draw APIs.";
1137
1642
  return;
@@ -1148,6 +1653,7 @@ export class WebGPUDevice {
1148
1653
  if (this.activeRenderTarget)
1149
1654
  this.activeRenderTarget.nativeNeedsClear = false;
1150
1655
  uniformBuffer?.destroy();
1656
+ atlasResources?.buffer.destroy();
1151
1657
  portableResources?.buffer.destroy();
1152
1658
  this.nativeSubmissions += 1;
1153
1659
  if (sampledTexture)
@@ -1162,7 +1668,7 @@ export class WebGPUDevice {
1162
1668
  this.nativePassthroughSubmissions += 1;
1163
1669
  this.nativeTextureBindings += portableResources?.actualTextureCount ?? 0;
1164
1670
  }
1165
- if (shader.nativeUniformLayout === "generated-pbr" || shader.nativeUniformLayout === "generated-textured-pbr" || shader.nativeUniformLayout === "generated-instanced-pbr")
1671
+ if (shader.nativeUniformLayout === "generated-pbr" || shader.nativeUniformLayout === "generated-atlas-pbr" || shader.nativeUniformLayout === "generated-textured-pbr" || shader.nativeUniformLayout === "generated-instanced-pbr")
1166
1672
  this.nativePbrSubmissions += 1;
1167
1673
  if (shader.nativeUniformLayout === "generated-instanced-pbr")
1168
1674
  this.nativeInstancedSubmissions += 1;
@@ -1170,6 +1676,7 @@ export class WebGPUDevice {
1170
1676
  this.nativeSkinnedSubmissions += 1;
1171
1677
  if (shader.nativeUniformLayout === "generated-morph-unlit")
1172
1678
  this.nativeMorphSubmissions += 1;
1679
+ this.nativeTextureBindings += atlasResources?.actualTextureCount ?? 0;
1173
1680
  if (materialTextures?.actualBaseColor)
1174
1681
  this.nativeTextureBindings += 1;
1175
1682
  if (materialTextures?.actualNormal)
@@ -1184,6 +1691,8 @@ export class WebGPUDevice {
1184
1691
  this.nativeEnvironmentBindings += 1;
1185
1692
  if (materialTextures?.actualShadow)
1186
1693
  this.nativeShadowMapBindings += 1;
1694
+ if (materialTextures?.actualSpotShadow)
1695
+ this.nativeShadowMapBindings += 1;
1187
1696
  if (!this.activeRenderTarget)
1188
1697
  this.canvasSubmissions += 1;
1189
1698
  }
@@ -1221,6 +1730,20 @@ export class WebGPUDevice {
1221
1730
  if (!binding?.texture || binding.texture.disposed || !binding.validate().ok || !hasNativeSampledTextureBinding(this.device))
1222
1731
  return null;
1223
1732
  const texture = binding.texture;
1733
+ // J2 zero-copy shadow path: a render target's native color texture is
1734
+ // created with TEXTURE_BINDING usage, so a shadow map rendered natively
1735
+ // (ShadowPass "encoded-color-depth") samples directly — no CPU readback
1736
+ // round-trip. Same-queue submission order guarantees the depth writes
1737
+ // are visible to the later lit pass.
1738
+ for (const target of this.renderTargets) {
1739
+ if (target.disposed || target.sampleCount !== 1 || target.colorTexture !== texture || !target.nativeView)
1740
+ continue;
1741
+ return {
1742
+ view: target.nativeView,
1743
+ sampler: this.device.createSampler(webgpuSamplerDescriptor(binding.sampler)),
1744
+ actual: true
1745
+ };
1746
+ }
1224
1747
  let resource = this.nativeSampledTextures.get(texture);
1225
1748
  if (!resource) {
1226
1749
  if (texture.source) {
@@ -1290,6 +1813,7 @@ export class WebGPUDevice {
1290
1813
  const environmentBinding = uniformTextureBinding(command.uniforms, "u_environmentMapTexture");
1291
1814
  const brdfBinding = uniformTextureBinding(command.uniforms, "u_environmentBrdfLutTexture");
1292
1815
  const shadowBinding = uniformTextureBinding(command.uniforms, "u_shadowMapTexture");
1816
+ const spotShadowBinding = uniformTextureBinding(command.uniforms, "u_spotShadowMapTexture");
1293
1817
  const normalBinding = uniformTextureBinding(command.uniforms, "u_normalTexture");
1294
1818
  const metallicRoughnessBinding = uniformTextureBinding(command.uniforms, "u_metallicRoughnessTexture");
1295
1819
  const occlusionBinding = uniformTextureBinding(command.uniforms, "u_occlusionTexture");
@@ -1299,18 +1823,20 @@ export class WebGPUDevice {
1299
1823
  const environment = this.createNativeSampledTextureBinding(environmentBinding) ?? this.createNativeFallbackSampledTextureBinding("environment-srgb", [92, 116, 156, 255], "srgb");
1300
1824
  const brdf = this.createNativeSampledTextureBinding(brdfBinding) ?? this.createNativeFallbackSampledTextureBinding("brdf-linear", [180, 180, 255, 255], "linear");
1301
1825
  const shadow = this.createNativeSampledTextureBinding(shadowBinding) ?? this.createNativeFallbackSampledTextureBinding("shadow-lit-linear", [255, 255, 255, 255], "linear");
1826
+ const spotShadow = this.createNativeSampledTextureBinding(spotShadowBinding) ?? this.createNativeFallbackSampledTextureBinding("spot-shadow-lit-linear", [255, 255, 255, 255], "linear");
1302
1827
  const normal = this.createNativeSampledTextureBinding(normalBinding) ?? this.createNativeFallbackSampledTextureBinding("flat-normal-linear", [128, 128, 255, 255], "linear");
1303
1828
  const metallicRoughness = this.createNativeSampledTextureBinding(metallicRoughnessBinding) ?? this.createNativeFallbackSampledTextureBinding("metallic-roughness-linear", [255, 255, 255, 255], "linear");
1304
1829
  const occlusion = this.createNativeSampledTextureBinding(occlusionBinding) ?? this.createNativeFallbackSampledTextureBinding("occlusion-linear", [255, 255, 255, 255], "linear");
1305
1830
  const clusterLightData = this.createNativeSampledTextureBinding(clusterLightDataBinding) ?? this.createNativeFallbackSampledTextureBinding("cluster-light-data-linear", [0, 0, 0, 0], "linear");
1306
1831
  const clusterLightIndices = this.createNativeSampledTextureBinding(clusterLightIndicesBinding) ?? this.createNativeFallbackSampledTextureBinding("cluster-light-indices-linear", [0, 0, 0, 0], "linear");
1307
- if (!base || !environment || !brdf || !shadow || !normal || !metallicRoughness || !occlusion || !clusterLightData || !clusterLightIndices)
1832
+ if (!base || !environment || !brdf || !shadow || !spotShadow || !normal || !metallicRoughness || !occlusion || !clusterLightData || !clusterLightIndices)
1308
1833
  return null;
1309
1834
  return {
1310
1835
  base,
1311
1836
  environment,
1312
1837
  brdf,
1313
1838
  shadow,
1839
+ spotShadow,
1314
1840
  normal,
1315
1841
  metallicRoughness,
1316
1842
  occlusion,
@@ -1320,6 +1846,7 @@ export class WebGPUDevice {
1320
1846
  actualEnvironment: environmentBinding !== null && environment.actual,
1321
1847
  actualBrdf: brdfBinding !== null && brdf.actual,
1322
1848
  actualShadow: shadowBinding !== null && shadow.actual,
1849
+ actualSpotShadow: spotShadowBinding !== null && spotShadow.actual,
1323
1850
  actualNormal: normalBinding !== null && normal.actual,
1324
1851
  actualMetallicRoughness: metallicRoughnessBinding !== null && metallicRoughness.actual,
1325
1852
  actualOcclusion: occlusionBinding !== null && occlusion.actual
@@ -1355,8 +1882,10 @@ export class WebGPUDevice {
1355
1882
  createNativeDrawUniformBuffer(command) {
1356
1883
  if (!this.device.createBindGroup)
1357
1884
  return null;
1358
- // 188 floats for the scalar/matrix fields + 96 joint matrices (16 floats each) appended at 188.
1359
- const data = new Float32Array(188 + MAX_WEBGPU_SKINNING_JOINTS * 16);
1885
+ // 188 floats for the scalar/matrix fields + 96 joint matrices (16 floats each) appended at 188
1886
+ // + J2 native shadow projections (312 floats at 1724: directional matrix/params/PCF, then spot)
1887
+ // + model matrix (16 floats at 2036) for instanced world composition.
1888
+ const data = new Float32Array(2052);
1360
1889
  const baseColorBinding = uniformBaseColorTextureBinding(command.uniforms);
1361
1890
  const environmentBinding = uniformTextureBinding(command.uniforms, "u_environmentMapTexture");
1362
1891
  const normalBinding = uniformTextureBinding(command.uniforms, "u_normalTexture");
@@ -1408,6 +1937,37 @@ export class WebGPUDevice {
1408
1937
  for (let index = 0; index < MAX_WEBGPU_SKINNING_JOINTS; index += 1) {
1409
1938
  data.set(jointMatrices[index] ?? identityMatrix(), 188 + index * 16);
1410
1939
  }
1940
+ // J2 native shadow projections (WGSL parity with a3dForwardShadowFactor /
1941
+ // a3dSpotShadowFactor). Identity matrices + zero enabled flags keep every
1942
+ // shadow factor at exactly 1.0 (lit) when the ForwardPass gates stay shut.
1943
+ data.set(uniformMat4(command.uniforms?.get("u_shadowMapMatrix")) ?? identityMatrix(), 1724);
1944
+ const shadowTexel = uniformVec2(command.uniforms?.get("u_shadowMapTexelSize")) ?? [1, 1];
1945
+ data.set([
1946
+ uniformNumber(command.uniforms?.get("u_shadowMapBias"), 0.001),
1947
+ uniformNumber(command.uniforms?.get("u_shadowMapSlopeBias"), 1),
1948
+ shadowTexel[0],
1949
+ shadowTexel[1]
1950
+ ], 1740);
1951
+ data.set([uniformNumber(command.uniforms?.get("u_shadowPcfSampleCount"), 1), 0, 0, 0], 1744);
1952
+ setNativeShadowPcfSamples(data, command.uniforms?.get("u_shadowPcfSamples"), 1748);
1953
+ data.set(uniformMat4(command.uniforms?.get("u_spotShadowMatrix")) ?? identityMatrix(), 1876);
1954
+ const spotLightPosition = uniformVec3(command.uniforms?.get("u_spotShadowLightPosition")) ?? [0, 0, 0];
1955
+ data.set([spotLightPosition[0], spotLightPosition[1], spotLightPosition[2], uniformNumber(command.uniforms?.get("u_spotShadowRange"), 1)], 1892);
1956
+ const spotLightDirection = uniformVec3(command.uniforms?.get("u_spotShadowLightDirection")) ?? [0, -1, 0];
1957
+ data.set([spotLightDirection[0], spotLightDirection[1], spotLightDirection[2], 0], 1896);
1958
+ data.set([
1959
+ uniformNumber(command.uniforms?.get("u_spotShadowMapEnabled"), 0),
1960
+ uniformNumber(command.uniforms?.get("u_spotShadowStrength"), 0.65),
1961
+ uniformNumber(command.uniforms?.get("u_spotShadowBias"), 0.001),
1962
+ uniformNumber(command.uniforms?.get("u_spotShadowSlopeBias"), 1)
1963
+ ], 1900);
1964
+ const spotTexel = uniformVec2(command.uniforms?.get("u_spotShadowTexelSize")) ?? [1, 1];
1965
+ const spotCone = uniformVec2(command.uniforms?.get("u_spotShadowCone")) ?? [Math.PI / 4, 0];
1966
+ data.set([spotTexel[0], spotTexel[1], uniformNumber(command.uniforms?.get("u_spotShadowPcfSampleCount"), 1), spotCone[0]], 1904);
1967
+ setNativeShadowPcfSamples(data, command.uniforms?.get("u_spotShadowPcfSamples"), 1908);
1968
+ // Model matrix for the instanced vertex world composition
1969
+ // (u_modelMatrix * instanceMatrix * position, GLSL parity).
1970
+ data.set(modelMatrix, 2036);
1411
1971
  const morphDeltas = command.uniforms?.get("u_morphPositionDeltas");
1412
1972
  const morphNumbers = morphDeltas instanceof Float32Array || Array.isArray(morphDeltas) ? Array.from(morphDeltas).slice(0, 32) : [];
1413
1973
  if (morphNumbers.length > 0)
@@ -1420,6 +1980,36 @@ export class WebGPUDevice {
1420
1980
  this.device.queue.writeBuffer(buffer, 0, data);
1421
1981
  return buffer;
1422
1982
  }
1983
+ createNativeAtlasBindings(command) {
1984
+ const definitions = [
1985
+ ["u_extensionScalarAtlas", 20, 0, "linear"],
1986
+ ["u_clearcoatNormalTexture", 22, 21, "linear"],
1987
+ ["u_sheenColorTexture", 24, 23, "srgb"],
1988
+ ["u_anisotropyTexture", 26, 25, "linear"],
1989
+ ["u_emissiveTexture", 28, 27, "srgb"]
1990
+ ];
1991
+ const entries = [];
1992
+ let actualTextureCount = 0;
1993
+ for (const [name, textureIndex, samplerIndex, colorSpace] of definitions) {
1994
+ const actualTexture = this.createNativeSampledTextureBinding(uniformTextureBinding(command.uniforms, name));
1995
+ if (textureIndex === 20 && !actualTexture)
1996
+ throw new RenderDeviceError("Native extension atlas requires a resolved scalar atlas texture", "TEXTURE_BINDING_MISSING");
1997
+ const texture = actualTexture
1998
+ ?? this.createNativeFallbackSampledTextureBinding(`atlas-${name}`, name.includes("Normal") ? [128, 128, 255, 255] : [255, 255, 255, 255], colorSpace);
1999
+ if (!texture)
2000
+ return null;
2001
+ if (texture.actual)
2002
+ actualTextureCount += 1;
2003
+ entries.push({ binding: textureIndex, resource: texture.view });
2004
+ if (samplerIndex)
2005
+ entries.push({ binding: samplerIndex, resource: texture.sampler });
2006
+ }
2007
+ const data = webgpuExtensionAtlasUniforms(command.uniforms);
2008
+ const buffer = this.device.createBuffer({ label: "extension-atlas-uniforms", size: data.byteLength, usage: BUFFER_USAGE.UNIFORM | BUFFER_USAGE.COPY_DST });
2009
+ this.device.queue.writeBuffer(buffer, 0, data);
2010
+ entries.push({ binding: 19, resource: { buffer } });
2011
+ return { buffer, entries, actualTextureCount };
2012
+ }
1423
2013
  createNativePortableBindings(command, bindings) {
1424
2014
  if (!this.device.createBindGroup)
1425
2015
  return null;
@@ -1726,6 +2316,7 @@ function usesNativeDrawUniforms(layout) {
1726
2316
  }
1727
2317
  function usesNativePbrTextureBindings(layout) {
1728
2318
  return layout === "generated-pbr" ||
2319
+ layout === "generated-atlas-pbr" ||
1729
2320
  layout === "generated-textured-pbr" ||
1730
2321
  layout === "generated-instanced-pbr";
1731
2322
  }
@@ -1806,6 +2397,15 @@ function uniformVec2(value) {
1806
2397
  ? [components[0], components[1]]
1807
2398
  : null;
1808
2399
  }
2400
+ function setNativeShadowPcfSamples(data, value, offset) {
2401
+ const numbers = value instanceof Float32Array || Array.isArray(value) ? Array.from(value) : [];
2402
+ for (let index = 0; index < 32; index += 1) {
2403
+ const quad = numbers.slice(index * 4, index * 4 + 4);
2404
+ // Default single center tap (weight 1) so an enabled shadow map without
2405
+ // an explicit kernel still occludes instead of dividing by zero weight.
2406
+ data.set(quad.length === 4 && quad.every(Number.isFinite) ? quad : [0, 0, 1, 0], offset + index * 4);
2407
+ }
2408
+ }
1809
2409
  function uniformMat4Array(value, count) {
1810
2410
  const numbers = value instanceof Float32Array || Array.isArray(value) ? Array.from(value) : [];
1811
2411
  return Array.from({ length: count }, (_, index) => {
@@ -1972,7 +2572,7 @@ function jointPaletteFor(uniforms) {
1972
2572
  const count = Math.min(MAX_WEBGPU_SKINNING_JOINTS, Math.floor(length / 16));
1973
2573
  return count > 0 ? uniformMat4Array(value, count) : [];
1974
2574
  }
1975
- function readVertexColor(bytes, stride, offset, vertexIndex) {
2575
+ function readVertexColor(bytes, stride, offset, vertexIndex, components = 4) {
1976
2576
  if (offset === undefined)
1977
2577
  return [1, 1, 1, 1];
1978
2578
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
@@ -1981,7 +2581,7 @@ function readVertexColor(bytes, stride, offset, vertexIndex) {
1981
2581
  view.getFloat32(base, true),
1982
2582
  view.getFloat32(base + 4, true),
1983
2583
  view.getFloat32(base + 8, true),
1984
- view.getFloat32(base + 12, true)
2584
+ components >= 4 ? view.getFloat32(base + 12, true) : 1
1985
2585
  ];
1986
2586
  }
1987
2587
  function readVertexUv(bytes, stride, offset, vertexIndex) {
@@ -2166,7 +2766,9 @@ function createNativeShaderSources(sources) {
2166
2766
  return { ...nativeInstancedPbrShader(vertexEntry, fragmentEntry, sources.marker), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "generated-instanced-pbr" };
2167
2767
  }
2168
2768
  if (sources.marker.includes("pbr-textured")) {
2169
- return { ...nativeTexturedPbrShader(vertexEntry, fragmentEntry, sources.marker, nativeShaderUsesTangent(sources.vertex)), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "generated-textured-pbr" };
2769
+ const atlas = /uniform\s+sampler2D\s+u_extensionScalarAtlas/.test(sources.fragment);
2770
+ const native = nativeTexturedPbrShader(vertexEntry, fragmentEntry, sources.marker, nativeShaderUsesTangent(sources.vertex), atlas);
2771
+ return { ...native, ...(atlas ? { atlasVertex: native.vertex } : {}), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: atlas ? "generated-atlas-pbr" : "generated-textured-pbr" };
2170
2772
  }
2171
2773
  if (sources.marker.includes("pbr-direct")) {
2172
2774
  return sources.fragment.includes("u_baseColorTexture")
@@ -2180,22 +2782,100 @@ function createNativeShaderSources(sources) {
2180
2782
  return { ...nativeMorphUnlitShader(vertexEntry, fragmentEntry, sources.marker), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "generated-morph-unlit" };
2181
2783
  }
2182
2784
  if (/sampler2D/.test(sources.fragment) && /layout\s*\(\s*location\s*=\s*2\s*\)\s*in\s+vec2/.test(sources.vertex)) {
2785
+ const colorInput = /layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s*in\s+(?:lowp\s+|mediump\s+|highp\s+)?vec([34])\s+(?:a_color|color)\s*;/.exec(sources.vertex);
2786
+ const colorParameter = colorInput ? `, @location(${colorInput[1]}) vertexColor: vec${colorInput[2]}<f32>` : "";
2787
+ const drawUniforms = `struct DrawUniforms {
2788
+ modelViewProjection: mat4x4<f32>,
2789
+ color: vec4<f32>,
2790
+ };
2791
+ @group(0) @binding(0) var<uniform> u_draw: DrawUniforms;`;
2792
+ const vertexOutput = `struct VertexOutput {
2793
+ @builtin(position) position: vec4<f32>,
2794
+ @location(0) uv: vec2<f32>,
2795
+ ${colorInput ? "@location(1) color: vec4<f32>," : ""}
2796
+ };`;
2797
+ const vertex = `// ${sources.marker}
2798
+ ${drawUniforms}
2799
+ ${vertexOutput}
2800
+ @vertex
2801
+ fn ${vertexEntry}(@location(0) position: vec3<f32>, @location(2) uv: vec2<f32>${colorParameter}) -> VertexOutput {
2802
+ var output: VertexOutput;
2803
+ let clipPosition = u_draw.modelViewProjection * vec4<f32>(position, 1.0);
2804
+ output.position = vec4<f32>(clipPosition.x, clipPosition.y, clipPosition.z * 0.5 + clipPosition.w * 0.5, clipPosition.w);
2805
+ output.uv = uv;
2806
+ ${colorInput ? `output.color = ${colorInput[2] === "3" ? "vec4<f32>(vertexColor, 1.0)" : "vertexColor"};` : ""}
2807
+ return output;
2808
+ }
2809
+ `;
2183
2810
  return {
2184
- vertex: `// ${sources.marker}\nstruct DrawUniforms {\n modelViewProjection: mat4x4<f32>,\n color: vec4<f32>,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;\n\n@vertex\nfn ${vertexEntry}(@location(0) position: vec3<f32>, @location(2) uv: vec2<f32>) -> VertexOutput {\n var output: VertexOutput;\n let clipPosition = u_draw.modelViewProjection * vec4<f32>(position, 1.0);
2185
- output.position = vec4<f32>(clipPosition.x, clipPosition.y, clipPosition.z * 0.5 + clipPosition.w * 0.5, clipPosition.w);\n output.uv = uv;\n return output;\n}\n`,
2186
- fragment: `// ${sources.marker}\nstruct DrawUniforms {\n modelViewProjection: mat4x4<f32>,\n color: vec4<f32>,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;\n@group(0) @binding(1) var u_textureSampler: sampler;\n@group(0) @binding(2) var u_texture: texture_2d<f32>;\n\n@fragment\nfn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {\n return u_draw.color * textureSample(u_texture, u_textureSampler, input.uv);\n}\n`,
2811
+ vertex,
2812
+ colorDefaultVertex: colorInput ? vertex.replace(colorParameter, "").replace(/output\.color = [^;]+;/, "output.color = vec4<f32>(1.0);") : undefined,
2813
+ fragment: `// ${sources.marker}
2814
+ ${drawUniforms}
2815
+ ${vertexOutput}
2816
+ @group(0) @binding(1) var u_textureSampler: sampler;
2817
+ @group(0) @binding(2) var u_texture: texture_2d<f32>;
2818
+ @fragment
2819
+ fn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {
2820
+ return u_draw.color * textureSample(u_texture, u_textureSampler, input.uv)${colorInput ? " * input.color" : ""};
2821
+ }
2822
+ `,
2187
2823
  entryPoints: [vertexEntry, fragmentEntry],
2188
2824
  uniformLayout: "generated-texture"
2189
2825
  };
2190
2826
  }
2827
+ // Basic GLSL programs (including unlit primitives and particle sprites)
2828
+ // declare their color input explicitly. Preserve that location and component
2829
+ // count; RGB inputs use opaque alpha, while RGBA alpha modulates the material.
2830
+ // Depth programs intentionally do not consume material/vertex color.
2831
+ const colorInput = sources.marker.includes(DEFAULT_DEPTH_SHADER_MARKER) ? null
2832
+ : /layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s*in\s+(?:lowp\s+|mediump\s+|highp\s+)?vec([34])\s+(?:a_color|color)\s*;/.exec(sources.vertex);
2833
+ const colorParameter = colorInput ? `, @location(${colorInput[1]}) vertexColor: vec${colorInput[2]}<f32>` : "";
2834
+ const vertexOutput = `struct VertexOutput {
2835
+ @builtin(position) position: vec4<f32>,
2836
+ ${colorInput ? "@location(0) color: vec4<f32>," : ""}
2837
+ };`;
2838
+ const drawUniforms = `struct DrawUniforms {
2839
+ modelViewProjection: mat4x4<f32>,
2840
+ color: vec4<f32>,
2841
+ };
2842
+ @group(0) @binding(0) var<uniform> u_draw: DrawUniforms;`;
2843
+ const vertex = `// ${sources.marker}
2844
+ ${drawUniforms}
2845
+ ${vertexOutput}
2846
+ @vertex
2847
+ fn ${vertexEntry}(@location(0) position: vec3<f32>${colorParameter}) -> VertexOutput {
2848
+ var output: VertexOutput;
2849
+ let clipPosition = u_draw.modelViewProjection * vec4<f32>(position, 1.0);
2850
+ output.position = vec4<f32>(clipPosition.x, clipPosition.y, clipPosition.z * 0.5 + clipPosition.w * 0.5, clipPosition.w);
2851
+ ${colorInput ? `output.color = ${colorInput[2] === "3" ? "vec4<f32>(vertexColor, 1.0)" : "vertexColor"};` : ""}
2852
+ return output;
2853
+ }
2854
+ `;
2191
2855
  return {
2192
- vertex: `// ${sources.marker}\nstruct DrawUniforms {\n modelViewProjection: mat4x4<f32>,\n color: vec4<f32>,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;\n\n@vertex\nfn ${vertexEntry}(@location(0) position: vec3<f32>) -> VertexOutput {\n var output: VertexOutput;\n let clipPosition = u_draw.modelViewProjection * vec4<f32>(position, 1.0);
2193
- output.position = vec4<f32>(clipPosition.x, clipPosition.y, clipPosition.z * 0.5 + clipPosition.w * 0.5, clipPosition.w);\n return output;\n}\n`,
2194
- fragment: `// ${sources.marker}\nstruct DrawUniforms {\n modelViewProjection: mat4x4<f32>,\n color: vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;\n\n@fragment\nfn ${fragmentEntry}() -> @location(0) vec4<f32> {\n return u_draw.color;\n}\n`,
2856
+ vertex,
2857
+ colorDefaultVertex: colorInput ? vertex.replace(colorParameter, "").replace(/output\.color = [^;]+;/, "output.color = vec4<f32>(1.0);") : undefined,
2858
+ fragment: sources.marker.includes(DEFAULT_DEPTH_SHADER_MARKER)
2859
+ ? nativeDepthFragment(sources.marker, fragmentEntry)
2860
+ : `// ${sources.marker}
2861
+ ${drawUniforms}
2862
+ ${vertexOutput}
2863
+ @fragment
2864
+ fn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {
2865
+ return u_draw.color${colorInput ? " * input.color" : ""};
2866
+ }
2867
+ `,
2195
2868
  entryPoints: [vertexEntry, fragmentEntry],
2196
2869
  uniformLayout: "generated-basic"
2197
2870
  };
2198
2871
  }
2872
+ function nativeDepthFragment(marker, fragmentEntry) {
2873
+ // J2 native depth encoding: the GLSL depth program writes gl_FragCoord.z
2874
+ // into an rgba8 shadow target ("encoded-color-depth"). The framebuffer
2875
+ // depth @builtin(position).z is already 0..1 in WebGPU, so packing it
2876
+ // samples identically to the GL path through the projective PCF reader.
2877
+ return `// ${marker}\nstruct DrawUniforms {\n modelViewProjection: mat4x4<f32>,\n color: vec4<f32>,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;\n\n@fragment\nfn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {\n return vec4<f32>(input.position.z, input.position.z, input.position.z, 1.0);\n}\n`;
2878
+ }
2199
2879
  const PORTABLE_BINDINGS_MARKER = "/* @aura3d-bindings */";
2200
2880
  function injectPortableBindings(source, bindings) {
2201
2881
  if (!source.includes(PORTABLE_BINDINGS_MARKER)) {
@@ -2299,6 +2979,23 @@ function nativeUniformStruct() {
2299
2979
  // 96-joint skinning palette (WebGL2 \`u_jointMatrices[96]\` parity). Appended at the struct end so
2300
2980
  // the existing field offsets are unchanged.
2301
2981
  joints: array<mat4x4<f32>, 96>,
2982
+ // J2 native shadow projections (float offsets 1724+, after the 96-joint
2983
+ // palette). Directional block mirrors GLSL \`a3dForwardShadowFactor\`;
2984
+ // spot block mirrors \`a3dSpotShadowFactor\` (cone/range pre-check inside
2985
+ // the helper). Appended here so no existing field offset moves.
2986
+ shadowMapMatrix: mat4x4<f32>,
2987
+ shadowParams0: vec4<f32>,
2988
+ shadowParams1: vec4<f32>,
2989
+ shadowPcf: array<vec4<f32>, 32>,
2990
+ spotShadowMatrix: mat4x4<f32>,
2991
+ spotShadowParams0: vec4<f32>,
2992
+ spotShadowParams1: vec4<f32>,
2993
+ spotShadowParams2: vec4<f32>,
2994
+ spotShadowParams3: vec4<f32>,
2995
+ spotShadowPcf: array<vec4<f32>, 32>,
2996
+ // Model matrix for GLSL-parity world composition on the instanced path
2997
+ // (\`u_modelMatrix * instanceMatrix * position\`; float offset 2036).
2998
+ modelMatrix: mat4x4<f32>,
2302
2999
  };
2303
3000
 
2304
3001
  @group(0) @binding(0) var<uniform> u_draw: DrawUniforms;
@@ -2311,7 +3008,7 @@ fn a3dWebGPUClipPosition(clipPosition: vec4<f32>) -> vec4<f32> {
2311
3008
  function nativeShaderUsesTangent(vertexSource) {
2312
3009
  return /\ba_tangent\b/.test(vertexSource) || /layout\s*\(\s*location\s*=\s*3\s*\)\s*in\s+vec4/.test(vertexSource);
2313
3010
  }
2314
- function nativePbrFragmentPrelude(marker) {
3011
+ function nativePbrFragmentPrelude(marker, atlas = false) {
2315
3012
  return `// ${marker}
2316
3013
  ${nativeUniformStruct()}
2317
3014
  @group(0) @binding(1) var u_baseSampler: sampler;
@@ -2330,6 +3027,10 @@ ${nativeUniformStruct()}
2330
3027
  @group(0) @binding(14) var u_occlusionTexture: texture_2d<f32>;
2331
3028
  @group(0) @binding(15) var u_clusterLightData: texture_2d<f32>;
2332
3029
  @group(0) @binding(16) var u_clusterLightIndices: texture_2d<f32>;
3030
+ @group(0) @binding(17) var u_spotShadowSampler: sampler;
3031
+ @group(0) @binding(18) var u_spotShadowTexture: texture_2d<f32>;
3032
+
3033
+ ${atlas ? WEBGPU_EXTENSION_ATLAS_WGSL : ""}
2333
3034
 
2334
3035
  struct VertexOutput {
2335
3036
  @builtin(position) position: vec4<f32>,
@@ -2337,6 +3038,7 @@ struct VertexOutput {
2337
3038
  @location(1) uv: vec2<f32>,
2338
3039
  @location(2) worldPosition: vec3<f32>,
2339
3040
  @location(3) tangent: vec4<f32>,
3041
+ ${atlas ? "@location(4) uv1: vec2<f32>, @location(5) color: vec4<f32>," : ""}
2340
3042
  };
2341
3043
 
2342
3044
  fn fresnelSchlick(cosTheta: f32, f0: vec3<f32>) -> vec3<f32> {
@@ -2349,14 +3051,16 @@ fn fresnelSchlickRoughness(cosTheta: f32, f0: vec3<f32>, roughness: f32) -> vec3
2349
3051
  }
2350
3052
 
2351
3053
  fn ggxDistribution(nDotH: f32, roughness: f32) -> f32 {
2352
- let a = max(roughness * roughness, 0.045);
3054
+ let a = ${atlas ? "max(roughness, 0.045) * max(roughness, 0.045)" : "max(roughness * roughness, 0.045)"};
2353
3055
  let a2 = a * a;
2354
- let denom = max((nDotH * nDotH) * (a2 - 1.0) + 1.0, 0.001);
2355
- return a2 / max(3.14159265 * denom * denom, 0.001);
3056
+ // At roughness .045 the true denominator before squaring is ~4.1e-6.
3057
+ // A .001/.00001 floor destroys the normalized low-roughness GGX lobe.
3058
+ let denom = max((nDotH * nDotH) * (a2 - 1.0) + 1.0, ${atlas ? "0.00000001" : "0.001"});
3059
+ return a2 / max(3.14159265 * denom * denom, ${atlas ? "0.0000000000000001" : "0.001"});
2356
3060
  }
2357
3061
 
2358
3062
  fn ggxVisibilitySmithCorrelated(nDotV: f32, nDotL: f32, roughness: f32) -> f32 {
2359
- let a = max(roughness * roughness, 0.045);
3063
+ let a = ${atlas ? "max(roughness, 0.045) * max(roughness, 0.045)" : "max(roughness * roughness, 0.045)"};
2360
3064
  let a2 = a * a;
2361
3065
  let lambdaV = nDotL * sqrt(max((nDotV - a2 * nDotV) * nDotV + a2, 0.00001));
2362
3066
  let lambdaL = nDotV * sqrt(max((nDotL - a2 * nDotL) * nDotL + a2, 0.00001));
@@ -2421,18 +3125,79 @@ fn perturbNormal(normalInput: vec3<f32>, tangentFrame: vec4<f32>, normalSample:
2421
3125
  return normalize(tangent * mapped.x + bitangent * mapped.y + n * max(mapped.z, 0.001));
2422
3126
  }
2423
3127
 
2424
- fn shadePbr(normalInput: vec3<f32>, tangentFrame: vec4<f32>, uv: vec2<f32>, worldPosition: vec3<f32>, fragmentPosition: vec2<f32>) -> vec4<f32> {
3128
+ fn a3dNativeShadowFactor(worldPosition: vec3<f32>, normal: vec3<f32>, lightDirection: vec3<f32>, useSpot: bool, shadowSampler: sampler, shadowTexture: texture_2d<f32>) -> f32 {
3129
+ var shadowMatrix = u_draw.shadowMapMatrix;
3130
+ var bias = u_draw.shadowParams0.x;
3131
+ var slopeBias = u_draw.shadowParams0.y;
3132
+ var texelSize = u_draw.shadowParams0.zw;
3133
+ var strength = u_draw.flags.z;
3134
+ var sampleCount = i32(u_draw.shadowParams1.x + 0.5);
3135
+ var receiverLightDirection = lightDirection;
3136
+ if (useSpot) {
3137
+ if (u_draw.spotShadowParams2.x < 0.5) { return 1.0; }
3138
+ let toFragment = worldPosition - u_draw.spotShadowParams0.xyz;
3139
+ let distanceToLight = max(length(toFragment), 0.0001);
3140
+ if (distanceToLight > u_draw.spotShadowParams0.w) { return 1.0; }
3141
+ let coneAxis = u_draw.spotShadowParams1.xyz / max(length(u_draw.spotShadowParams1.xyz), 0.0001);
3142
+ if (dot(toFragment / distanceToLight, coneAxis) < cos(u_draw.spotShadowParams3.w)) { return 1.0; }
3143
+ receiverLightDirection = coneAxis * -1.0;
3144
+ shadowMatrix = u_draw.spotShadowMatrix;
3145
+ bias = u_draw.spotShadowParams2.z;
3146
+ slopeBias = u_draw.spotShadowParams2.w;
3147
+ texelSize = u_draw.spotShadowParams3.xy;
3148
+ strength = u_draw.spotShadowParams2.y;
3149
+ sampleCount = i32(u_draw.spotShadowParams3.z + 0.5);
3150
+ }
3151
+ let lightPosition = shadowMatrix * vec4<f32>(worldPosition, 1.0);
3152
+ let projected = lightPosition.xyz / max(lightPosition.w, 0.0001);
3153
+ let uv = projected.xy * 0.5 + vec2<f32>(0.5, 0.5);
3154
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { return 1.0; }
3155
+ // Native render targets store framebuffer row 0 at the top while the CPU
3156
+ // projection math addresses texels GL-style (row 0 at the bottom), so the
3157
+ // sample V must be mirrored to land on the texel the projection means.
3158
+ // Display and readback paths already account for the row order, which is
3159
+ // why screenshots and readPixels match GL while direct sampling does not.
3160
+ let sampleUv = vec2<f32>(uv.x, 1.0 - uv.y);
3161
+ let receiverNormal = normalize(normal);
3162
+ let shadowLightDirection = receiverLightDirection / max(length(receiverLightDirection), 0.0001);
3163
+ let normalDotLight = clamp(abs(dot(receiverNormal, shadowLightDirection)), 0.0, 1.0);
3164
+ // Shared bias-table policy (GLSL parity): per-sample tangent-scaled slope
3165
+ // bias — a centre-only bias under-compensates outer PCF taps on sloped
3166
+ // receivers, so each tap scales by its own texel distance.
3167
+ let slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
3168
+ let slopeTexelBias = slopeTangent * slopeBias * max(texelSize.x, texelSize.y);
3169
+ let projectedDepth = projected.z * 0.5 + 0.5;
3170
+ var shadowed = 0.0;
3171
+ var totalWeight = 0.0;
3172
+ let taps = clamp(sampleCount, 1, 32);
3173
+ for (var i = 0; i < 32; i = i + 1) {
3174
+ if (i >= taps) { break; }
3175
+ var sampleData = u_draw.shadowPcf[i];
3176
+ if (useSpot) { sampleData = u_draw.spotShadowPcf[i]; }
3177
+ let weight = max(sampleData.z, 0.0);
3178
+ let offset = sampleData.xy * texelSize;
3179
+ let storedDepth = textureSampleLevel(shadowTexture, shadowSampler, sampleUv + offset, 0.0).r;
3180
+ let sampleTexelDistance = max(1.0, length(sampleData.xy));
3181
+ let receiverDepth = projectedDepth - bias - slopeTexelBias * sampleTexelDistance;
3182
+ shadowed = shadowed + select(0.0, 1.0, receiverDepth > storedDepth) * weight;
3183
+ totalWeight = totalWeight + weight;
3184
+ }
3185
+ let occlusion = select(0.0, shadowed / totalWeight, totalWeight > 0.0);
3186
+ return mix(1.0, 1.0 - occlusion, clamp(strength, 0.0, 1.0));
3187
+ }
3188
+
3189
+ fn shadePbr(normalInput: vec3<f32>, tangentFrame: vec4<f32>, uv: vec2<f32>, worldPosition: vec3<f32>, fragmentPosition: vec2<f32>${atlas ? ", uv1: vec2<f32>, vertexColor: vec4<f32>" : ""}) -> vec4<f32> {
2425
3190
  var normal = normalize(normalInput);
2426
3191
  if (u_draw.morph0.x > 0.5) {
2427
- normal = perturbNormal(normal, tangentFrame, textureSample(u_normalTexture, u_normalSampler, uv).rgb, u_draw.morph0.y);
3192
+ normal = perturbNormal(normal, tangentFrame, textureSample(u_normalTexture, u_normalSampler, ${atlas ? "atlasUv(1u, uv, uv1)" : "uv"}).rgb, u_draw.morph0.y);
2428
3193
  }
2429
3194
  let viewDirection = normalize(u_draw.camera.xyz - worldPosition);
2430
3195
  let lightDirection = normalize(vec3<f32>(0.36, 0.52, 0.78));
2431
3196
  let halfVector = normalize(lightDirection + viewDirection);
2432
- var baseColor = u_draw.color.rgb;
2433
- var materialAlpha = u_draw.color.a;
3197
+ var baseColor = u_draw.color.rgb${atlas ? " * vertexColor.rgb" : ""};
3198
+ var materialAlpha = u_draw.color.a${atlas ? " * vertexColor.a" : ""};
2434
3199
  if (u_draw.flags.x > 0.5) {
2435
- let baseSample = textureSample(u_baseTexture, u_baseSampler, uv);
3200
+ let baseSample = textureSample(u_baseTexture, u_baseSampler, ${atlas ? "atlasUv(0u, uv, uv1)" : "uv"});
2436
3201
  baseColor = baseColor * baseSample.rgb;
2437
3202
  materialAlpha = materialAlpha * baseSample.a;
2438
3203
  }
@@ -2441,16 +3206,14 @@ fn shadePbr(normalInput: vec3<f32>, tangentFrame: vec4<f32>, uv: vec2<f32>, worl
2441
3206
  let productBodyGate = productPropBodyGate(sourceProductBaseColor, u_draw.materialFlags.z) * (1.0 - productOrangeGate * 0.92);
2442
3207
  let productSurfaceGate = clamp(productBodyGate + productOrangeGate * 0.72, 0.0, 1.0);
2443
3208
  baseColor = productPropAlbedo(sourceProductBaseColor, u_draw.materialFlags.z);
2444
- if (materialAlpha < u_draw.material.x) {
2445
- discard;
2446
- }
3209
+ ${atlas ? "" : "if (materialAlpha < u_draw.material.x) { discard; }"}
2447
3210
  let transmission = clamp(max(u_draw.material.y, u_draw.material.z), 0.0, 1.0);
2448
- let metallicRoughnessSample = textureSample(u_metallicRoughnessTexture, u_metallicRoughnessSampler, uv);
3211
+ let metallicRoughnessSample = textureSample(u_metallicRoughnessTexture, u_metallicRoughnessSampler, ${atlas ? "atlasUv(2u, uv, uv1)" : "uv"});
2449
3212
  let metallicRoughnessEnabled = step(0.5, u_draw.materialFlags.x);
2450
3213
  let occlusionEnabled = step(0.5, u_draw.materialFlags.y);
2451
3214
  let sampledMetallic = clamp(u_draw.params.x * metallicRoughnessSample.b, 0.0, 1.0);
2452
3215
  let sampledRoughness = clamp(u_draw.params.y * metallicRoughnessSample.g, 0.045, 1.0);
2453
- let sampledOcclusion = mix(1.0, textureSample(u_occlusionTexture, u_occlusionSampler, uv).r, clamp(u_draw.morph0.z, 0.0, 1.0));
3216
+ let sampledOcclusion = mix(1.0, textureSample(u_occlusionTexture, u_occlusionSampler, ${atlas ? "atlasUv(3u, uv, uv1)" : "uv"}).r, clamp(u_draw.morph0.z, 0.0, 1.0));
2454
3217
  let metallic = mix(clamp(u_draw.params.x, 0.0, 1.0), sampledMetallic, metallicRoughnessEnabled);
2455
3218
  let roughness = mix(clamp(u_draw.params.y, 0.045, 1.0), sampledRoughness, metallicRoughnessEnabled);
2456
3219
  let occlusion = mix(1.0, sampledOcclusion, occlusionEnabled);
@@ -2460,16 +3223,23 @@ fn shadePbr(normalInput: vec3<f32>, tangentFrame: vec4<f32>, uv: vec2<f32>, worl
2460
3223
  let nDotV = max(dot(normal, viewDirection), 0.001);
2461
3224
  let nDotH = max(dot(normal, halfVector), 0.001);
2462
3225
  let vDotH = max(dot(viewDirection, halfVector), 0.001);
2463
- let f0 = mix(vec3<f32>(0.04, 0.04, 0.04), baseColor, metallic);
3226
+ let dielectricF0 = ${atlas ? "vec3<f32>(pow((u_atlas.substrate.x-1.0)/(u_atlas.substrate.x+1.0),2.0)) * clamp(u_atlas.specularColor.rgb,vec3<f32>(0.0),vec3<f32>(1.0)) * clamp(u_atlas.substrate.y,0.0,1.0)" : "vec3<f32>(0.04, 0.04, 0.04)"};
3227
+ let f0 = mix(dielectricF0, baseColor, metallic);
2464
3228
  let fresnel = fresnelSchlick(vDotH, f0);
2465
3229
  let distribution = ggxDistribution(nDotH, roughness);
2466
3230
  let visibility = ggxVisibilitySmithCorrelated(nDotV, nDotL, roughness);
2467
3231
  let lDotH = max(dot(lightDirection, halfVector), 0.0);
2468
- let kd = (vec3<f32>(1.0, 1.0, 1.0) - fresnel) * (1.0 - metallic);
3232
+ ${atlas ? `// The film replaces the substrate Fresnel in the dielectric energy split.
3233
+ // Retaining the old kd while adding film specular creates or loses energy.
3234
+ let filmFresnel = atlasIridescenceFresnel(f0, vDotH, uv, uv1);
3235
+ let filmMaximum = max(max(filmFresnel.r, filmFresnel.g), filmFresnel.b);
3236
+ let diffuseFresnel = select(fresnel, vec3<f32>(filmMaximum), u_atlas.iridescence.y > 0.0);
3237
+ let kd = (vec3<f32>(1.0) - diffuseFresnel) * (1.0 - metallic);` : "let kd = (vec3<f32>(1.0, 1.0, 1.0) - fresnel) * (1.0 - metallic);"}
2469
3238
  let specular = fresnel * distribution * visibility;
2470
3239
  let diffuse = kd * baseColor * diffuseBurley(nDotV, nDotL, lDotH, roughness) / 3.14159265;
2471
3240
  var environment = baseColor * u_draw.params.z * (0.28 + 0.72 * clamp(normal.y * 0.5 + 0.5, 0.0, 1.0)) * occlusion;
2472
3241
  var environmentSpecularContribution = vec3<f32>(0.0, 0.0, 0.0);
3242
+ var environmentExtensionRadiance = vec3<f32>(0.0);
2473
3243
  if (u_draw.flags.w > 0.5) {
2474
3244
  let reflectionDirection = reflect(-viewDirection, normal);
2475
3245
  let diffuseUv = vec2<f32>(fract(atan2(normal.z, normal.x) / 6.2831853 + 0.5), acos(clamp(normal.y, -1.0, 1.0)) / 3.14159265);
@@ -2480,15 +3250,20 @@ fn shadePbr(normalInput: vec3<f32>, tangentFrame: vec4<f32>, uv: vec2<f32>, worl
2480
3250
  let brdf = textureSampleLevel(u_brdfTexture, u_brdfSampler, vec2<f32>(nDotV, roughness), 0.0).rg;
2481
3251
  let environmentFresnel = fresnelSchlickRoughness(nDotV, f0, roughness);
2482
3252
  let environmentDiffuse = (vec3<f32>(1.0, 1.0, 1.0) - environmentFresnel) * (1.0 - metallic) * diffuseEnv * baseColor * u_draw.params.w * occlusion;
2483
- let environmentSpecular = specularEnv * (f0 * brdf.x + vec3<f32>(brdf.y, brdf.y, brdf.y)) * max(u_draw.reserved0.w, 0.0);
3253
+ ${atlas ? "let environmentF0 = atlasIridescenceEnvironmentF0(f0, nDotV, uv, uv1);" : "let environmentF0 = f0;"}
3254
+ let environmentSpecular = specularEnv * (environmentF0 * brdf.x + vec3<f32>(brdf.y, brdf.y, brdf.y)) * max(u_draw.reserved0.w, 0.0);
2484
3255
  environmentSpecularContribution = environmentSpecular;
3256
+ environmentExtensionRadiance = specularEnv * max(u_draw.reserved0.w, 0.0);
2485
3257
  environment = environment + environmentDiffuse + environmentSpecular;
2486
3258
  }
3259
+ // J2 native shadow projections: directional PCF mirrors GLSL
3260
+ // a3dForwardShadowFactor; spot PCF mirrors a3dSpotShadowFactor. Both
3261
+ // default to 1.0 (lit) when disabled or outside the shadow frustum.
2487
3262
  var shadow = 1.0;
2488
3263
  if (u_draw.flags.y > 0.5) {
2489
- let shadowDepth = textureSampleLevel(u_shadowTexture, u_shadowSampler, vec2<f32>(0.5, 0.5), 0.0).r;
2490
- shadow = mix(1.0, shadowDepth, clamp(u_draw.flags.z, 0.0, 1.0));
3264
+ shadow = a3dNativeShadowFactor(worldPosition, normal, lightDirection, false, u_shadowSampler, u_shadowTexture);
2491
3265
  }
3266
+ shadow = shadow * a3dNativeShadowFactor(worldPosition, normal, lightDirection, true, u_spotShadowSampler, u_spotShadowTexture);
2492
3267
  var clusteredDirect = vec3<f32>(0.0, 0.0, 0.0);
2493
3268
  if (u_draw.materialFlags.w > 0.5) {
2494
3269
  let indexDimensions = textureDimensions(u_clusterLightIndices);
@@ -2518,7 +3293,8 @@ fn shadePbr(normalInput: vec3<f32>, tangentFrame: vec4<f32>, uv: vec2<f32>, worl
2518
3293
  }
2519
3294
  }
2520
3295
  let legacyDirect = (diffuse + specular) * nDotL * 2.25 * shadow;
2521
- let litOpaqueLinearColor = environment + select(legacyDirect, clusteredDirect, u_draw.materialFlags.w > 0.5);
3296
+ let litOpaqueLinearColor = environment + select(legacyDirect, clusteredDirect, u_draw.materialFlags.w > 0.5)
3297
+ ${atlas ? "+ atlasExtensionLighting(normal, tangentFrame, viewDirection, lightDirection, uv, uv1, shadow, environmentExtensionRadiance, f0, roughness) + u_atlas.emissive.rgb * u_atlas.factors.y * mix(vec3<f32>(1.0), textureSample(u_emissiveTexture, u_emissiveSampler, atlasUv(4u, uv, uv1)).rgb, u_atlas.maps[4].control.z)" : ""};
2522
3298
  let smoothedBodyColor = mix(baseColor, vec3<f32>(1.0, 0.88, 0.012), productBodyGate * 0.32);
2523
3299
  let smoothedBeakColor = mix(baseColor, vec3<f32>(1.0, 0.24, 0.018), productOrangeGate * 0.82);
2524
3300
  let softBodyProduct = smoothedBodyColor * (1.5 + 0.06 * nDotL) + specular * nDotL * 0.018;
@@ -2528,6 +3304,7 @@ fn shadePbr(normalInput: vec3<f32>, tangentFrame: vec4<f32>, uv: vec2<f32>, worl
2528
3304
  let transmittedTint = baseColor * u_draw.params.w * (0.18 + 0.42 * clamp(1.0 - roughness, 0.0, 1.0)) + environmentSpecularContribution * 1.35;
2529
3305
  let linearColor = mix(opaqueLinearColor, opaqueLinearColor * 0.22 + transmittedTint, transmission);
2530
3306
  let outputAlpha = mix(materialAlpha, min(materialAlpha, 0.22), transmission);
3307
+ ${atlas ? "if (materialAlpha < u_draw.material.x) { discard; }" : ""}
2531
3308
  return vec4<f32>(encodePbrOutput(linearColor), outputAlpha);
2532
3309
  }
2533
3310
  `;
@@ -2564,7 +3341,7 @@ fn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {
2564
3341
  `
2565
3342
  };
2566
3343
  }
2567
- function nativeTexturedPbrShader(vertexEntry, fragmentEntry, marker, usesTangent) {
3344
+ function nativeTexturedPbrShader(vertexEntry, fragmentEntry, marker, usesTangent, atlas = false) {
2568
3345
  return {
2569
3346
  vertex: `// ${marker}
2570
3347
  ${nativeUniformStruct()}
@@ -2574,24 +3351,26 @@ struct VertexOutput {
2574
3351
  @location(1) uv: vec2<f32>,
2575
3352
  @location(2) worldPosition: vec3<f32>,
2576
3353
  @location(3) tangent: vec4<f32>,
3354
+ ${atlas ? "@location(4) uv1: vec2<f32>, @location(5) color: vec4<f32>," : ""}
2577
3355
  };
2578
3356
 
2579
3357
  @vertex
2580
- fn ${vertexEntry}(@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) uv: vec2<f32>${usesTangent ? ", @location(3) tangent: vec4<f32>" : ""}) -> VertexOutput {
3358
+ fn ${vertexEntry}(@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) uv: vec2<f32>${usesTangent ? ", @location(3) tangent: vec4<f32>" : ""}${atlas ? ", @location(4) color: vec4<f32>, @location(7) uv1: vec2<f32>" : ""}) -> VertexOutput {
2581
3359
  var output: VertexOutput;
2582
3360
  let worldPosition = (u_draw.instance0 * vec4<f32>(position, 1.0)).xyz;
2583
3361
  output.position = a3dWebGPUClipPosition(u_draw.modelViewProjection * vec4<f32>(position, 1.0));
2584
3362
  output.normal = normalize((u_draw.normalMatrix * vec4<f32>(normal, 0.0)).xyz);
2585
3363
  output.uv = uv;
3364
+ ${atlas ? "output.uv1 = uv1; output.color = color;" : ""}
2586
3365
  output.worldPosition = worldPosition;
2587
3366
  ${usesTangent ? "output.tangent = vec4<f32>(normalize((u_draw.normalMatrix * vec4<f32>(tangent.xyz, 0.0)).xyz), tangent.w);" : "output.tangent = vec4<f32>(1.0, 0.0, 0.0, 1.0);"}
2588
3367
  return output;
2589
3368
  }
2590
3369
  `,
2591
- fragment: `${nativePbrFragmentPrelude(marker)}
3370
+ fragment: `${nativePbrFragmentPrelude(marker, atlas)}
2592
3371
  @fragment
2593
3372
  fn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {
2594
- return shadePbr(input.normal, input.tangent, input.uv, input.worldPosition, input.position.xy);
3373
+ return shadePbr(input.normal, input.tangent, input.uv, input.worldPosition, input.position.xy${atlas ? ", input.uv1, input.color" : ""});
2595
3374
  }
2596
3375
  `
2597
3376
  };
@@ -2615,13 +3394,27 @@ fn instanceMatrix(index: u32) -> mat4x4<f32> {
2615
3394
  return u_draw.instance0;
2616
3395
  }
2617
3396
 
3397
+ fn a3dInstanceNormalMatrix(model: mat4x4<f32>) -> mat3x3<f32> {
3398
+ // GLSL parity: v_normal = u_normalMatrix * transpose(inverse(mat3(instance))) * a_normal.
3399
+ let m = mat3x3<f32>(model[0].xyz, model[1].xyz, model[2].xyz);
3400
+ let adj0 = cross(m[1], m[2]);
3401
+ let adj1 = cross(m[2], m[0]);
3402
+ let adj2 = cross(m[0], m[1]);
3403
+ let det = dot(m[0], adj0);
3404
+ let safeDet = select(det, 1.0, abs(det) < 0.00001);
3405
+ return mat3x3<f32>(
3406
+ vec3<f32>(adj0.x, adj1.x, adj2.x) / safeDet,
3407
+ vec3<f32>(adj0.y, adj1.y, adj2.y) / safeDet,
3408
+ vec3<f32>(adj0.z, adj1.z, adj2.z) / safeDet);
3409
+ }
3410
+
2618
3411
  @vertex
2619
3412
  fn ${vertexEntry}(@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
2620
3413
  var output: VertexOutput;
2621
3414
  let model = instanceMatrix(instanceIndex);
2622
- let worldPosition = (model * vec4<f32>(position, 1.0)).xyz;
3415
+ let worldPosition = (u_draw.modelMatrix * model * vec4<f32>(position, 1.0)).xyz;
2623
3416
  output.position = a3dWebGPUClipPosition(u_draw.modelViewProjection * model * vec4<f32>(position, 1.0));
2624
- output.normal = normalize((model * vec4<f32>(normal, 0.0)).xyz);
3417
+ output.normal = normalize((u_draw.normalMatrix * vec4<f32>(a3dInstanceNormalMatrix(model) * normal, 0.0)).xyz);
2625
3418
  output.uv = vec2<f32>(0.5, 0.5);
2626
3419
  output.worldPosition = worldPosition;
2627
3420
  output.tangent = vec4<f32>(1.0, 0.0, 0.0, 1.0);