@aura3d/rendering 1.3.3 → 1.4.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 (314) hide show
  1. package/dist/ForwardPass.js +7 -1
  2. package/dist/ForwardPass.js.map +1 -1
  3. package/dist/MorphTarget.d.ts +1 -0
  4. package/dist/MorphTarget.d.ts.map +1 -1
  5. package/dist/MorphTarget.js.map +1 -1
  6. package/dist/Renderer.js +7 -1
  7. package/dist/Renderer.js.map +1 -1
  8. package/package.json +38 -4
  9. package/src/ArchitecturalLightingFixtures.ts +0 -241
  10. package/src/ArchitecturalMaterialCatalog.ts +0 -130
  11. package/src/ArchitecturalMeasurementFixtures.ts +0 -155
  12. package/src/BRDFLut.ts +0 -47
  13. package/src/CameraFraming.ts +0 -200
  14. package/src/CanonicalSceneFixtures.ts +0 -95
  15. package/src/CascadedShadowMaps.ts +0 -449
  16. package/src/ColorManagement.ts +0 -127
  17. package/src/CullingFixtures.ts +0 -280
  18. package/src/DecalGeometry.ts +0 -26
  19. package/src/DepthPass.ts +0 -110
  20. package/src/EnvironmentBackgroundPass.ts +0 -194
  21. package/src/EnvironmentBackgroundResources.ts +0 -309
  22. package/src/EnvironmentLighting.ts +0 -96
  23. package/src/EnvironmentMapResources.ts +0 -1026
  24. package/src/EnvironmentPipeline.ts +0 -69
  25. package/src/EnvironmentPlatform.ts +0 -1306
  26. package/src/EnvironmentPreset.ts +0 -155
  27. package/src/Exposure.ts +0 -66
  28. package/src/ExternalParityRenderPreset.ts +0 -725
  29. package/src/ForwardPass.ts +0 -1663
  30. package/src/FrameVisualMetrics.ts +0 -301
  31. package/src/Geometry.ts +0 -461
  32. package/src/GeometryPrimitives.ts +0 -2
  33. package/src/HDRRenderPipeline.ts +0 -94
  34. package/src/IBL.ts +0 -86
  35. package/src/IndexBuffer.ts +0 -57
  36. package/src/InstancedPBRMaterial.ts +0 -186
  37. package/src/InstancedUnlitMaterial.ts +0 -60
  38. package/src/Instancing.ts +0 -19
  39. package/src/LightCollector.ts +0 -82
  40. package/src/LightUniforms.ts +0 -47
  41. package/src/LightingDebug.ts +0 -80
  42. package/src/LightingDefaults.ts +0 -79
  43. package/src/LightingRig.ts +0 -301
  44. package/src/LineGeometry.ts +0 -2
  45. package/src/Material.ts +0 -209
  46. package/src/MaterialBinding.ts +0 -131
  47. package/src/MaterialInstance.ts +0 -59
  48. package/src/MaterialPresets.ts +0 -272
  49. package/src/MorphTarget.ts +0 -123
  50. package/src/MorphTargetPlan.ts +0 -178
  51. package/src/MorphUnlitMaterial.ts +0 -60
  52. package/src/NormalMappedPBRMaterial.ts +0 -188
  53. package/src/OceanFixtures.ts +0 -318
  54. package/src/PBRLightingDefaults.ts +0 -12
  55. package/src/PBRMaterial.ts +0 -442
  56. package/src/PMREM.ts +0 -53
  57. package/src/PbrReference.ts +0 -681
  58. package/src/PostProcessPass.ts +0 -2507
  59. package/src/ProceduralTextureFixtures.ts +0 -393
  60. package/src/ProductTurntableFixtures.ts +0 -815
  61. package/src/Raycaster.ts +0 -21
  62. package/src/ReflectionProbe.ts +0 -13
  63. package/src/ReflectionSurfaces.ts +0 -219
  64. package/src/RenderBackend.ts +0 -43
  65. package/src/RenderDebugViews.ts +0 -138
  66. package/src/RenderDevice.ts +0 -836
  67. package/src/RenderGraph.ts +0 -194
  68. package/src/RenderPass.ts +0 -27
  69. package/src/RenderPipeline.ts +0 -101
  70. package/src/RenderQueue.ts +0 -12
  71. package/src/RenderState.ts +0 -12
  72. package/src/Renderer.ts +0 -2626
  73. package/src/RendererDebugOverlay.ts +0 -80
  74. package/src/RendererFeatureGates.ts +0 -150
  75. package/src/RendererPostprocessPlan.ts +0 -279
  76. package/src/RendererTiming.ts +0 -251
  77. package/src/RendererVisualPipelineReport.ts +0 -836
  78. package/src/ResourceLifecycle.ts +0 -21
  79. package/src/Sampler.ts +0 -37
  80. package/src/SceneOptimization.ts +0 -518
  81. package/src/ShaderChunks.ts +0 -393
  82. package/src/ShaderLibrary.ts +0 -2766
  83. package/src/ShaderModule.ts +0 -36
  84. package/src/ShaderPreprocessor.ts +0 -260
  85. package/src/ShaderReflection.ts +0 -201
  86. package/src/ShadowMap.ts +0 -237
  87. package/src/ShadowPass.ts +0 -198
  88. package/src/ShadowProjection.ts +0 -115
  89. package/src/SkinnedLitMaterial.ts +0 -397
  90. package/src/SkinnedUnlitMaterial.ts +0 -63
  91. package/src/SkinningBounds.ts +0 -131
  92. package/src/SpaceEnvironmentFixtures.ts +0 -200
  93. package/src/SpriteGeometry.ts +0 -21
  94. package/src/StereoCameraRig.ts +0 -127
  95. package/src/StereoEffects.ts +0 -274
  96. package/src/TerrainFixtures.ts +0 -180
  97. package/src/Texture.ts +0 -307
  98. package/src/TextureBinding.ts +0 -91
  99. package/src/TexturedPBRMaterial.ts +0 -1112
  100. package/src/TexturedUnlitMaterial.ts +0 -67
  101. package/src/ToneMapping.ts +0 -105
  102. package/src/UniformBinder.ts +0 -16
  103. package/src/UniformLayout.ts +0 -129
  104. package/src/UnlitMaterial.ts +0 -75
  105. package/src/VegetationFixtures.ts +0 -314
  106. package/src/VertexBuffer.ts +0 -120
  107. package/src/VertexFormat.ts +0 -171
  108. package/src/VoxelWorldFixtures.ts +0 -280
  109. package/src/WeatherFixtures.ts +0 -223
  110. package/src/WebGL2Device.ts +0 -2280
  111. package/src/WebGL2StateCache.ts +0 -330
  112. package/src/WebGPUDevice.ts +0 -2877
  113. package/src/WebGPURenderToTextureProof.ts +0 -139
  114. package/src/advanced-runtime/AdvancedRenderer.ts +0 -73
  115. package/src/advanced-runtime/index.ts +0 -9
  116. package/src/animation/AnimationMaterialStyle.ts +0 -32
  117. package/src/animation/AnimationRenderPreset.ts +0 -45
  118. package/src/animation/AnimationToonMaterial.ts +0 -387
  119. package/src/animation/AnimationVisualQuality.ts +0 -71
  120. package/src/animation/applyAnimationRenderPreset.ts +0 -180
  121. package/src/animation/index.ts +0 -5
  122. package/src/cinematic/BloomPass.ts +0 -26
  123. package/src/cinematic/CinematicDepthComposition.ts +0 -30
  124. package/src/cinematic/CinematicEvidence.ts +0 -123
  125. package/src/cinematic/CinematicLightingRig.ts +0 -144
  126. package/src/cinematic/CinematicMaterialPresets.ts +0 -130
  127. package/src/cinematic/CinematicPostProcess.ts +0 -83
  128. package/src/cinematic/DepthHazePass.ts +0 -30
  129. package/src/cinematic/EmissivePracticalLightSystem.ts +0 -45
  130. package/src/cinematic/FilmGrainPass.ts +0 -24
  131. package/src/cinematic/FogVolumeSystem.ts +0 -30
  132. package/src/cinematic/GlowCardSystem.ts +0 -44
  133. package/src/cinematic/RainParticleSystem.ts +0 -151
  134. package/src/cinematic/VignettePass.ts +0 -25
  135. package/src/cinematic/WetReflectionApproximation.ts +0 -27
  136. package/src/cinematic/index.ts +0 -14
  137. package/src/effects/CollisionModule.ts +0 -64
  138. package/src/effects/ColorModule.ts +0 -60
  139. package/src/effects/ForceModule.ts +0 -25
  140. package/src/effects/GPUParticleBackend.ts +0 -569
  141. package/src/effects/Particle.ts +0 -108
  142. package/src/effects/ParticleDiagnostics.ts +0 -224
  143. package/src/effects/ParticleEffectPresets.ts +0 -147
  144. package/src/effects/ParticleEmitter.ts +0 -326
  145. package/src/effects/ParticleModule.ts +0 -26
  146. package/src/effects/ParticleRenderPass.ts +0 -117
  147. package/src/effects/ParticleRenderer.ts +0 -132
  148. package/src/effects/ParticleSystem.ts +0 -333
  149. package/src/effects/SizeModule.ts +0 -56
  150. package/src/effects/TrailModule.ts +0 -72
  151. package/src/effects/VelocityModule.ts +0 -75
  152. package/src/effects/index.ts +0 -15
  153. package/src/index.ts +0 -925
  154. package/src/materials/AlphaSorting.ts +0 -30
  155. package/src/materials/MaterialExtensions.ts +0 -42
  156. package/src/materials/PhysicalMaterial.ts +0 -133
  157. package/src/materials/TransmissionPass.ts +0 -51
  158. package/src/performance/BVH.ts +0 -18
  159. package/src/performance/Batcher.ts +0 -2
  160. package/src/performance/FrustumCuller.ts +0 -9
  161. package/src/performance/LOD.ts +0 -20
  162. package/src/performance/Octree.ts +0 -10
  163. package/src/performance/RenderItemSorting.ts +0 -170
  164. package/src/performance/RendererStats.ts +0 -43
  165. package/src/performance/ResourceBudget.ts +0 -44
  166. package/src/postprocess/BloomPass.ts +0 -28
  167. package/src/postprocess/CinematicDiagnostics.ts +0 -642
  168. package/src/postprocess/ColorGradingPass.ts +0 -24
  169. package/src/postprocess/DepthOfFieldPass.ts +0 -12
  170. package/src/postprocess/EffectComposer.ts +0 -349
  171. package/src/postprocess/SSAOPass.ts +0 -20
  172. package/src/postprocess/index.ts +0 -34
  173. package/src/production-runtime/AnimationControlsPipeline.ts +0 -77
  174. package/src/production-runtime/PBRHDRPipeline.ts +0 -381
  175. package/src/production-runtime/ProductionEffectsPipeline.ts +0 -85
  176. package/src/production-runtime/ProductionRendererTypes.ts +0 -136
  177. package/src/production-runtime/ProductionRuntimeRenderer.ts +0 -172
  178. package/src/production-runtime/ProductionWebGL2Renderer.ts +0 -313
  179. package/src/production-runtime/ProductionWebGPURenderer.ts +0 -396
  180. package/src/production-runtime/TransmissionBackdropCapture.ts +0 -130
  181. package/src/production-runtime/animation/MorphTargetRenderer.ts +0 -2
  182. package/src/production-runtime/animation/SkinningRenderer.ts +0 -2
  183. package/src/production-runtime/backends/RendererBackend.ts +0 -17
  184. package/src/production-runtime/backends/WebGL2RendererBackend.ts +0 -30
  185. package/src/production-runtime/backends/WebGPURendererBackend.ts +0 -24
  186. package/src/production-runtime/backends/webgl2/WebGL2Buffer.ts +0 -2
  187. package/src/production-runtime/backends/webgl2/WebGL2Capabilities.ts +0 -2
  188. package/src/production-runtime/backends/webgl2/WebGL2RenderTarget.ts +0 -2
  189. package/src/production-runtime/backends/webgl2/WebGL2Shader.ts +0 -2
  190. package/src/production-runtime/backends/webgl2/WebGL2StateCache.ts +0 -1
  191. package/src/production-runtime/backends/webgl2/WebGL2Texture.ts +0 -2
  192. package/src/production-runtime/backends/webgpu/WebGPUBuffer.ts +0 -2
  193. package/src/production-runtime/backends/webgpu/WebGPUCapabilities.ts +0 -2
  194. package/src/production-runtime/backends/webgpu/WebGPURenderTarget.ts +0 -2
  195. package/src/production-runtime/backends/webgpu/WebGPUShader.ts +0 -2
  196. package/src/production-runtime/backends/webgpu/WebGPUTexture.ts +0 -2
  197. package/src/production-runtime/color/ColorManagement.ts +0 -3
  198. package/src/production-runtime/color/ToneMapping.ts +0 -7
  199. package/src/production-runtime/diagnostics/FrameCapture.ts +0 -2
  200. package/src/production-runtime/diagnostics/GPUCapabilities.ts +0 -2
  201. package/src/production-runtime/diagnostics/RendererStats.ts +0 -2
  202. package/src/production-runtime/environment/EnvironmentMap.ts +0 -1
  203. package/src/production-runtime/environment/HDRLoader.ts +0 -57
  204. package/src/production-runtime/environment/PMREMGenerator.ts +0 -713
  205. package/src/production-runtime/framegraph/FrameGraph.ts +0 -18
  206. package/src/production-runtime/framegraph/RenderPass.ts +0 -15
  207. package/src/production-runtime/geometry/ProjectedDecalGeometry.ts +0 -439
  208. package/src/production-runtime/index.ts +0 -127
  209. package/src/production-runtime/lights/LightManager.ts +0 -7
  210. package/src/production-runtime/lights/ShadowMapRenderer.ts +0 -5
  211. package/src/production-runtime/materials/GLTFMaterialAdapter.ts +0 -6
  212. package/src/production-runtime/materials/GLTFPBRMaterialAdapter.ts +0 -2
  213. package/src/production-runtime/materials/MaterialCompiler.ts +0 -7
  214. package/src/production-runtime/materials/MaterialTextureBindings.ts +0 -5
  215. package/src/production-runtime/materials/PBRMaterial.ts +0 -2
  216. package/src/production-runtime/materials/PBRShaderFeatures.ts +0 -2
  217. package/src/production-runtime/passes/ContactShadowPass.ts +0 -184
  218. package/src/production-runtime/passes/DepthPrepass.ts +0 -7
  219. package/src/production-runtime/passes/OpaquePass.ts +0 -7
  220. package/src/production-runtime/passes/ShadowPass.ts +0 -7
  221. package/src/production-runtime/passes/SkyboxPass.ts +0 -7
  222. package/src/production-runtime/passes/ToneMappingPass.ts +0 -7
  223. package/src/production-runtime/passes/TransparentPass.ts +0 -7
  224. package/src/production-runtime/postprocess/BloomPass.ts +0 -51
  225. package/src/production-runtime/postprocess/ColorGradingPass.ts +0 -49
  226. package/src/production-runtime/postprocess/DOFPass.ts +0 -47
  227. package/src/production-runtime/postprocess/FXAAPass.ts +0 -53
  228. package/src/production-runtime/postprocess/ProductionEffectComposer.ts +0 -78
  229. package/src/production-runtime/postprocess/ProductionPostProcessTypes.ts +0 -174
  230. package/src/production-runtime/postprocess/SSAOPass.ts +0 -45
  231. package/src/production-runtime/resources/GPUBuffer.ts +0 -2
  232. package/src/production-runtime/resources/GPUTexture.ts +0 -2
  233. package/src/production-runtime/resources/RenderTarget.ts +0 -3
  234. package/src/production-runtime/resources/ResourceCache.ts +0 -7
  235. package/src/production-runtime/scene/Camera.ts +0 -1
  236. package/src/production-runtime/scene/Lights.ts +0 -2
  237. package/src/production-runtime/scene/RenderableMesh.ts +0 -2
  238. package/src/production-runtime/scene/RenderablePrimitive.ts +0 -1
  239. package/src/production-runtime/scene/RenderableScene.ts +0 -4
  240. package/src/production-runtime/shaders/ShaderProgramLibrary.ts +0 -7
  241. package/src/production-runtime/shaders/chunks/brdf.glsl +0 -2
  242. package/src/production-runtime/shaders/chunks/ibl.glsl +0 -3
  243. package/src/production-runtime/shaders/chunks/pbr.frag.glsl +0 -47
  244. package/src/production-runtime/shaders/chunks/pbr.vert.glsl +0 -8
  245. package/src/production-runtime/shaders/chunks/shadows.glsl +0 -2
  246. package/src/production-runtime/shaders/wgsl/pbr.wgsl +0 -3
  247. package/src/production-runtime/shaders/wgsl/postprocess.wgsl +0 -2
  248. package/src/production-runtime/shaders/wgsl/skybox.wgsl +0 -2
  249. package/src/shaders/depth.frag.glsl +0 -5
  250. package/src/shaders/depth.vert.glsl +0 -8
  251. package/src/shaders/pbr-direct.frag.glsl +0 -687
  252. package/src/shaders/pbr-direct.vert.glsl +0 -18
  253. package/src/shaders/unlit.frag.glsl +0 -18
  254. package/src/shaders/unlit.vert.glsl +0 -13
  255. package/src/shadows/CascadedShadowPipeline.ts +0 -53
  256. package/src/shadows/ContactShadows.ts +0 -106
  257. package/src/shadows/ShadowDebugViews.ts +0 -46
  258. package/src/threejs-compatibility/InstancingSystem.ts +0 -17
  259. package/src/threejs-compatibility/LightingSystem.ts +0 -22
  260. package/src/threejs-compatibility/MaterialSystem.ts +0 -9
  261. package/src/threejs-compatibility/RenderTargetSystem.ts +0 -35
  262. package/src/threejs-compatibility/RendererDiagnostics.ts +0 -61
  263. package/src/threejs-compatibility/SceneRenderer.ts +0 -34
  264. package/src/threejs-compatibility/ShadowSystem.ts +0 -19
  265. package/src/threejs-compatibility/TextureSystem.ts +0 -19
  266. package/src/threejs-compatibility/ThreeCompatRenderer.ts +0 -90
  267. package/src/threejs-compatibility/TransparencySystem.ts +0 -19
  268. package/src/threejs-compatibility/index.ts +0 -29
  269. package/src/threejs-compatibility/performance/BVH.ts +0 -14
  270. package/src/threejs-compatibility/performance/FrustumCulling.ts +0 -10
  271. package/src/threejs-compatibility/performance/Instancing.ts +0 -4
  272. package/src/threejs-compatibility/performance/LODSystem.ts +0 -16
  273. package/src/threejs-compatibility/performance/OcclusionCulling.ts +0 -6
  274. package/src/threejs-compatibility/performance/RaycastAcceleration.ts +0 -12
  275. package/src/threejs-compatibility/performance/RendererProfiler.ts +0 -19
  276. package/src/threejs-compatibility/performance/TextureStreaming.ts +0 -4
  277. package/src/threejs-compatibility/performance/index.ts +0 -12
  278. package/src/threejs-compatibility/postprocess/BloomPass.ts +0 -16
  279. package/src/threejs-compatibility/postprocess/ColorGradingPass.ts +0 -16
  280. package/src/threejs-compatibility/postprocess/DepthOfFieldPass.ts +0 -21
  281. package/src/threejs-compatibility/postprocess/EffectComposer.ts +0 -14
  282. package/src/threejs-compatibility/postprocess/FXAAPass.ts +0 -15
  283. package/src/threejs-compatibility/postprocess/MotionBlurPass.ts +0 -20
  284. package/src/threejs-compatibility/postprocess/OutlinePass.ts +0 -21
  285. package/src/threejs-compatibility/postprocess/PostProcessTypes.ts +0 -169
  286. package/src/threejs-compatibility/postprocess/RenderPass.ts +0 -9
  287. package/src/threejs-compatibility/postprocess/SMAAPass.ts +0 -15
  288. package/src/threejs-compatibility/postprocess/SSAOPass.ts +0 -21
  289. package/src/threejs-compatibility/postprocess/ShaderPass.ts +0 -7
  290. package/src/threejs-compatibility/postprocess/TAAPass.ts +0 -15
  291. package/src/threejs-compatibility/postprocess/VignettePass.ts +0 -16
  292. package/src/threejs-compatibility/postprocess/index.ts +0 -15
  293. package/src/threejs-compatibility/shaders/NodeMaterial.ts +0 -21
  294. package/src/threejs-compatibility/shaders/RawShaderMaterial.ts +0 -6
  295. package/src/threejs-compatibility/shaders/ShaderDiagnostics.ts +0 -17
  296. package/src/threejs-compatibility/shaders/ShaderMaterial.ts +0 -18
  297. package/src/threejs-compatibility/shaders/ThreeCompatShaderChunks.ts +0 -6
  298. package/src/threejs-compatibility/shaders/Uniforms.ts +0 -18
  299. package/src/threejs-compatibility/shaders/index.ts +0 -9
  300. package/src/threejs-compatibility/vfx/GPUPointCloud.ts +0 -7
  301. package/src/threejs-compatibility/vfx/LineRenderer.ts +0 -13
  302. package/src/threejs-compatibility/vfx/ParticleSystem.ts +0 -23
  303. package/src/threejs-compatibility/vfx/SpriteSystem.ts +0 -14
  304. package/src/threejs-compatibility/vfx/TrailRenderer.ts +0 -8
  305. package/src/threejs-compatibility/vfx/VFXDiagnostics.ts +0 -23
  306. package/src/threejs-compatibility/vfx/index.ts +0 -9
  307. package/src/threejs-example-parity/index.ts +0 -205
  308. package/src/webgpu/WebGPUBuffer.ts +0 -1
  309. package/src/webgpu/WebGPUCompute.ts +0 -2
  310. package/src/webgpu/WebGPUPipelineCache.ts +0 -15
  311. package/src/webgpu/WebGPUPostProcess.ts +0 -2
  312. package/src/webgpu/WebGPUTexture.ts +0 -2
  313. package/tsconfig.json +0 -26
  314. package/vitest.config.ts +0 -20
package/src/Renderer.ts DELETED
@@ -1,2626 +0,0 @@
1
- import {
2
- Bounds3 as SceneBounds3,
3
- Camera,
4
- DirectionalLight,
5
- Light,
6
- PointLight,
7
- Scene,
8
- SpotLight,
9
- identityMat4,
10
- invertMat4,
11
- multiplyMat4,
12
- orthographicMat4,
13
- perspectiveMat4,
14
- transformPoint,
15
- type Mat4,
16
- type Vec3,
17
- type SceneNode
18
- } from "@aura3d/scene";
19
- import type { Ray } from "@aura3d/math";
20
- import { createRenderDevice, type RenderBackendOptions } from "./RenderBackend";
21
- import { type LdrPostprocessPassDescriptor, type RenderDevice, RenderDeviceError, type RenderDeviceDiagnostics, type RenderTarget, type RenderTargetDescriptor } from "./RenderDevice";
22
- import { ENVIRONMENT_BACKGROUND_COLOR_RESOURCE, EnvironmentBackgroundPass, type EnvironmentBackgroundOptions } from "./EnvironmentBackgroundPass";
23
- import { ForwardPass, type EnvironmentLightingOptions, type ForwardEnvironmentFogOptions, type ForwardShadowMapOptions, type RenderItem, type RenderMaterial, type SkinningPaletteBinding } from "./ForwardPass";
24
- import { type CollectedLight, LightCollector } from "./LightCollector";
25
- import { Geometry } from "./Geometry";
26
- import { type MorphTargetDelta } from "./MorphTarget";
27
- import { computeSkinnedMorphTargetWeightedBounds } from "./SkinningBounds";
28
- import { RenderGraph } from "./RenderGraph";
29
- import {
30
- BloomPass,
31
- FXAAPass,
32
- ToneMappingPass,
33
- bloomFloatPixels,
34
- bloomPixels,
35
- chromaticAberrationPixels,
36
- colorGradePixels,
37
- contactShadowPixels,
38
- createDepthTextureBinding,
39
- depthOfFieldPixels,
40
- filmGrainPixels,
41
- fusedLdrPostprocessPixels,
42
- fxaaPixels,
43
- motionBlurPixels,
44
- outlinePixels,
45
- ssaoPixels,
46
- ssrPixels,
47
- taaPixels,
48
- toneMapFloatPixels,
49
- toneMapPixels,
50
- writePostProcessPixels,
51
- type BloomOptions,
52
- type ChromaticAberrationOptions,
53
- type ColorGradeOptions,
54
- type ContactShadowPostProcessOptions,
55
- type DepthTextureBinding,
56
- type DepthOfFieldOptions,
57
- type FXAAOptions,
58
- type FilmGrainOptions,
59
- type FusedLdrPostProcessPass,
60
- type FusedLdrPostProcessScratch,
61
- type MotionBlurOptions,
62
- type OutlineOptions,
63
- type SSAOOptions,
64
- type SSROptions,
65
- type TAAOptions,
66
- type ToneMappingOptions
67
- } from "./PostProcessPass";
68
- import {
69
- createRendererPostprocessPasses,
70
- createRendererPostprocessPlanDiagnostics,
71
- type RendererPostProcessPassName,
72
- type RendererPostProcessPassPlan,
73
- type RendererPostprocessPlanDiagnostics,
74
- type RendererPostprocessPlanOptions,
75
- type RendererPostprocessTargetFormat
76
- } from "./RendererPostprocessPlan";
77
- import { createDefaultShaderLibrary, type ShaderLibrary } from "./ShaderLibrary";
78
- import { ShadowMap, type ShadowFilterKernel, type ShadowMapOptions } from "./ShadowMap";
79
- import { ShadowPass } from "./ShadowPass";
80
- import { Sampler } from "./Sampler";
81
- import { TextureBinding } from "./TextureBinding";
82
- import { computePerspectiveCameraFrame, type PerspectiveCameraFrameOptions } from "./CameraFraming";
83
- import {
84
- assertRendererFeatures,
85
- createRendererFeatureReport,
86
- type RendererFeature,
87
- type RendererFeatureReport
88
- } from "./RendererFeatureGates";
89
- import { batchStaticRenderItems, type StaticBatchOptions, type StaticBatchInput } from "./SceneOptimization";
90
-
91
- export interface RendererOptions extends RenderBackendOptions {
92
- readonly width?: number;
93
- readonly height?: number;
94
- readonly clearColor?: readonly [number, number, number, number];
95
- readonly shaderLibrary?: ShaderLibrary;
96
- readonly requiredFeatures?: readonly RendererFeature[];
97
- }
98
-
99
- export interface ResizeToDisplayOptions {
100
- readonly cssWidth?: number;
101
- readonly cssHeight?: number;
102
- readonly devicePixelRatio?: number;
103
- }
104
-
105
- export interface ResizeToDisplayResult {
106
- readonly resized: boolean;
107
- readonly cssWidth: number;
108
- readonly cssHeight: number;
109
- readonly devicePixelRatio: number;
110
- readonly width: number;
111
- readonly height: number;
112
- }
113
-
114
- export interface RendererAnimationLoop {
115
- readonly running: boolean;
116
- stop(): void;
117
- }
118
-
119
- export interface RendererFrameCapture {
120
- readonly width: number;
121
- readonly height: number;
122
- readonly pixels: Uint8Array;
123
- readonly diagnostics: RenderDeviceDiagnostics;
124
- readonly capturedAt?: string;
125
- readonly renderSize?: RendererFrameCaptureRenderSize;
126
- readonly pixelHash?: string;
127
- readonly pixelDigest?: RendererFrameCapturePixelDigest;
128
- readonly pixelStats?: RendererFrameCapturePixelStats;
129
- readonly diagnosticsSummary?: RendererFrameCaptureDiagnosticsSummary;
130
- readonly metadata?: RendererFrameCaptureMetadata;
131
- }
132
-
133
- export interface RendererFrameCaptureWithMetadata extends RendererFrameCapture {
134
- readonly capturedAt: string;
135
- readonly renderSize: RendererFrameCaptureRenderSize;
136
- readonly pixelHash: string;
137
- readonly pixelDigest: RendererFrameCapturePixelDigest;
138
- readonly pixelStats: RendererFrameCapturePixelStats;
139
- readonly diagnosticsSummary: RendererFrameCaptureDiagnosticsSummary;
140
- readonly metadata: RendererFrameCaptureMetadata;
141
- }
142
-
143
- export interface RendererFrameCaptureRenderSize {
144
- readonly width: number;
145
- readonly height: number;
146
- readonly pixelCount: number;
147
- readonly byteLength: number;
148
- readonly aspectRatio: number;
149
- }
150
-
151
- export interface RendererFrameCapturePixelDigest {
152
- readonly algorithm: "sha256";
153
- readonly encoding: "hex";
154
- readonly source: "rgba8";
155
- readonly value: string;
156
- readonly byteLength: number;
157
- }
158
-
159
- export interface RendererFrameCapturePixelStats {
160
- readonly totalPixels: number;
161
- readonly opaquePixels: number;
162
- readonly opaqueRatio: number;
163
- readonly transparentPixels: number;
164
- readonly transparentRatio: number;
165
- readonly nonBlankPixels: number;
166
- readonly nonBlankRatio: number;
167
- readonly underexposedPixels: number;
168
- readonly underexposedRatio: number;
169
- readonly overexposedPixels: number;
170
- readonly overexposedRatio: number;
171
- readonly clippedChannelPixels: number;
172
- readonly clippedChannelRatio: number;
173
- readonly averageLuminance: number;
174
- readonly minLuminance: number;
175
- readonly maxLuminance: number;
176
- readonly averageAlpha: number;
177
- readonly averageRgb: readonly [number, number, number];
178
- readonly thresholds: {
179
- readonly nonBlankLuminance: number;
180
- readonly underexposedLuminance: number;
181
- readonly overexposedLuminance: number;
182
- readonly clippedChannel: number;
183
- };
184
- }
185
-
186
- export interface RendererFrameCaptureDiagnosticsSummary {
187
- readonly backend: RenderDevice["kind"];
188
- readonly renderer: string;
189
- readonly vendor: string;
190
- readonly capabilities: readonly string[];
191
- readonly limitations: readonly string[];
192
- readonly drawCalls: number;
193
- readonly resources: {
194
- readonly buffers: number;
195
- readonly shaders: number;
196
- readonly renderTargets?: number;
197
- readonly textures?: number;
198
- };
199
- readonly memoryBytes?: {
200
- readonly buffers?: number;
201
- readonly textures?: number;
202
- readonly approximateGpu?: number;
203
- };
204
- readonly scene?: {
205
- readonly submittedObjects?: number;
206
- readonly visibleObjects?: number;
207
- readonly culledObjects?: number;
208
- readonly frustumTestedObjects?: number;
209
- };
210
- readonly postprocess?: {
211
- readonly passes?: number;
212
- readonly passNames?: readonly string[];
213
- readonly targetFormat?: "rgba8" | "rgba16f" | "rgba32f";
214
- readonly renderTargets?: number;
215
- readonly textures?: number;
216
- readonly targetWidth?: number;
217
- readonly targetHeight?: number;
218
- };
219
- readonly contextLost: boolean;
220
- readonly lastError: string | null;
221
- }
222
-
223
- export interface RendererFrameCaptureMetadata {
224
- readonly capturedAt: string;
225
- readonly renderSize: RendererFrameCaptureRenderSize;
226
- readonly pixelHash: string;
227
- readonly pixelDigest: RendererFrameCapturePixelDigest;
228
- readonly pixelStats: RendererFrameCapturePixelStats;
229
- readonly diagnosticsSummary: RendererFrameCaptureDiagnosticsSummary;
230
- }
231
-
232
- export interface RenderSource {
233
- collectRenderItems?(): Iterable<RenderItem>;
234
- readonly renderItems?: Iterable<RenderItem>;
235
- readonly scene?: Scene;
236
- readonly renderTarget?: RenderTarget;
237
- readonly cameraPolicy?: RendererCameraPolicy;
238
- readonly cameraFrameBounds?: SceneBounds3 | {
239
- readonly min: readonly [number, number, number];
240
- readonly max: readonly [number, number, number];
241
- };
242
- readonly cameraFrameOptions?: PerspectiveCameraFrameOptions;
243
- readonly collectedLights?: Iterable<CollectedLight>;
244
- readonly environmentBackground?: EnvironmentBackgroundOptions | false;
245
- readonly environmentLighting?: EnvironmentLightingOptions | false;
246
- readonly environmentFog?: ForwardEnvironmentFogOptions | false;
247
- readonly shadowMap?: ForwardShadowMapOptions;
248
- readonly shadow?: RendererShadowOptions | boolean;
249
- readonly postprocess?: RendererPostProcessOptions | boolean;
250
- readonly cameraPosition?: readonly [number, number, number];
251
- readonly geometryLibrary?: RenderResourceLookup<Geometry>;
252
- readonly materialLibrary?: RenderResourceLookup<RenderMaterial>;
253
- readonly morphTargetLibrary?: RenderResourceLookup<readonly MorphTargetDelta[]>;
254
- readonly frustumCulling?: boolean;
255
- readonly staticBatching?: boolean | StaticBatchOptions;
256
- }
257
-
258
- export interface RendererInput {
259
- readonly source: RenderSource | Iterable<RenderItem> | Scene;
260
- readonly camera?: CameraLike;
261
- }
262
-
263
- export type RendererCameraPolicy = "identity" | "auto-frame" | "require";
264
-
265
- export const DEFAULT_RENDERER_AUTO_FRAME_OPTIONS: PerspectiveCameraFrameOptions = {
266
- paddingRatio: 0.14,
267
- yawRadians: -0.38,
268
- pitchRadians: -0.18,
269
- nearPadding: 0.2,
270
- farPadding: 2.4
271
- };
272
-
273
- export const DEFAULT_RENDERER_ENVIRONMENT_LIGHTING: EnvironmentLightingOptions = {
274
- color: [0.78, 0.8, 0.84],
275
- intensity: 0.42,
276
- proceduralMap: {
277
- skyColor: [0.64, 0.76, 0.94],
278
- horizonColor: [0.94, 0.82, 0.62],
279
- groundColor: [0.12, 0.13, 0.15],
280
- specularColor: [1, 0.94, 0.78],
281
- intensity: 0.5,
282
- specularIntensity: 0.82
283
- }
284
- };
285
-
286
- const DISABLED_RENDERER_ENVIRONMENT_LIGHTING: EnvironmentLightingOptions = {
287
- color: [0, 0, 0],
288
- intensity: 0,
289
- proceduralMap: {
290
- skyColor: [0, 0, 0],
291
- horizonColor: [0, 0, 0],
292
- groundColor: [0, 0, 0],
293
- specularColor: [0, 0, 0],
294
- intensity: 0,
295
- specularIntensity: 0
296
- },
297
- environmentMapIntensity: 0,
298
- environmentMapSpecularIntensity: 0
299
- };
300
-
301
- export const DEFAULT_RENDERER_DIRECT_LIGHTING = {
302
- key: {
303
- color: [1, 0.92, 0.78] as const,
304
- intensity: 2.25,
305
- direction: [0.42, -0.58, -0.7] as const
306
- },
307
- fill: {
308
- color: [0.48, 0.62, 0.9] as const,
309
- intensity: 0.55,
310
- direction: [-0.35, -0.22, -0.91] as const
311
- }
312
- } as const;
313
-
314
- export interface RendererShadowOptions extends ShadowMapOptions {
315
- readonly enabled?: boolean;
316
- readonly light?: Light;
317
- readonly lightMatrix?: Float32Array | readonly number[];
318
- readonly strength?: number;
319
- readonly slopeBias?: number;
320
- readonly texelSize?: readonly [number, number];
321
- readonly filterKernel?: ShadowFilterKernel;
322
- }
323
-
324
- export interface RendererPostProcessOptions extends RendererPostprocessPlanOptions {
325
- readonly targetFormat?: RendererPostprocessTargetFormat;
326
- }
327
-
328
- export type RenderResourceLookup<T> = ReadonlyMap<string, T> | Readonly<Record<string, T>>;
329
-
330
- interface RenderCollectionDiagnostics {
331
- submittedObjects: number;
332
- visibleObjects: number;
333
- culledObjects: number;
334
- frustumTestedObjects: number;
335
- }
336
-
337
- interface RendererPostprocessDiagnostics {
338
- readonly postprocessPasses: number;
339
- readonly postprocessPassNames: readonly RendererPostProcessPassName[];
340
- readonly postprocessTargetFormat: RendererPostprocessTargetFormat;
341
- readonly postprocessRenderTargets: number;
342
- readonly postprocessTextures: number;
343
- readonly postprocessTargetWidth: number;
344
- readonly postprocessTargetHeight: number;
345
- readonly postprocessPlan: RendererPostprocessPlanDiagnostics;
346
- }
347
-
348
- export interface CameraLike {
349
- readonly projectionMatrix?: Float32Array | readonly number[];
350
- readonly viewMatrix?: Float32Array | readonly number[];
351
- readonly viewProjectionMatrix?: Float32Array | readonly number[];
352
- resize?(width: number, height: number): void;
353
- updateCameraMatrices?(): void;
354
- }
355
-
356
- export interface ScenePickHit {
357
- readonly node: SceneNode;
358
- readonly geometry: Geometry;
359
- readonly distance: number;
360
- readonly bounds: SceneBounds3;
361
- readonly hitPoint?: readonly [number, number, number];
362
- readonly pointIndex?: number;
363
- readonly instanceIndex?: number;
364
- }
365
-
366
- export interface ScenePickOptions {
367
- readonly pointRadius?: number;
368
- readonly lineRadius?: number;
369
- }
370
-
371
- export class Renderer {
372
- public readonly device: RenderDevice;
373
- private readonly graph = new RenderGraph();
374
- private readonly shaderLibrary: ShaderLibrary;
375
- private readonly canvas?: HTMLCanvasElement | OffscreenCanvas;
376
- private width: number;
377
- private height: number;
378
- private clearColor: readonly [number, number, number, number];
379
- private disposed = false;
380
- private animationLoop: RendererAnimationLoopImpl | null = null;
381
- private readonly fusedLdrPostprocessScratch: FusedLdrPostProcessScratch = {};
382
-
383
- private constructor(device: RenderDevice, options: RendererOptions) {
384
- this.device = device;
385
- this.canvas = options.canvas;
386
- this.width = options.width ?? inferInitialCanvasDimension(options.canvas, "width");
387
- this.height = options.height ?? inferInitialCanvasDimension(options.canvas, "height");
388
- this.clearColor = options.clearColor ?? [0, 0, 0, 1];
389
- this.shaderLibrary = options.shaderLibrary ?? createDefaultShaderLibrary();
390
- this.resizeCanvas(this.width, this.height);
391
- }
392
-
393
- static async create(options: RendererOptions = {}): Promise<Renderer> {
394
- const device = await createRenderDevice(options);
395
- if (options.requiredFeatures && options.requiredFeatures.length > 0) {
396
- assertRendererFeatures(device, options.requiredFeatures);
397
- }
398
- return new Renderer(device, options);
399
- }
400
-
401
- getFeatureReport(): RendererFeatureReport {
402
- return createRendererFeatureReport(this.device);
403
- }
404
-
405
- resize(width: number, height: number): void {
406
- this.assertAlive();
407
- if (width <= 0 || height <= 0 || !Number.isInteger(width) || !Number.isInteger(height)) {
408
- throw new RenderDeviceError("Renderer dimensions must be positive integers", "INVALID_FRAME_SIZE", { width, height });
409
- }
410
- this.width = width;
411
- this.height = height;
412
- this.resizeCanvas(width, height);
413
- }
414
-
415
- resizeToDisplay(options: ResizeToDisplayOptions = {}): ResizeToDisplayResult {
416
- this.assertAlive();
417
- if (!this.canvas) {
418
- throw new RenderDeviceError("resizeToDisplay requires a canvas-backed renderer", "CANVAS_REQUIRED");
419
- }
420
- const cssWidth = options.cssWidth ?? readCanvasCssSize(this.canvas, "width");
421
- const cssHeight = options.cssHeight ?? readCanvasCssSize(this.canvas, "height");
422
- const devicePixelRatio = options.devicePixelRatio ?? globalThis.devicePixelRatio ?? 1;
423
- if (![cssWidth, cssHeight, devicePixelRatio].every(Number.isFinite) || cssWidth <= 0 || cssHeight <= 0 || devicePixelRatio <= 0) {
424
- throw new RenderDeviceError("Display size and DPR must be finite positive values", "INVALID_DISPLAY_SIZE", {
425
- cssWidth,
426
- cssHeight,
427
- devicePixelRatio
428
- });
429
- }
430
- const width = Math.max(1, Math.round(cssWidth * devicePixelRatio));
431
- const height = Math.max(1, Math.round(cssHeight * devicePixelRatio));
432
- const resized = width !== this.width || height !== this.height;
433
- if (resized) {
434
- this.resize(width, height);
435
- }
436
- return { resized, cssWidth, cssHeight, devicePixelRatio, width, height };
437
- }
438
-
439
- startAnimationLoop(callback: (timeMs: number, renderer: Renderer) => void): RendererAnimationLoop {
440
- this.assertAlive();
441
- this.animationLoop?.stop();
442
- const loop = new RendererAnimationLoopImpl(this, callback);
443
- this.animationLoop = loop;
444
- loop.start();
445
- return loop;
446
- }
447
-
448
- render(input: RendererInput): RenderDeviceDiagnostics;
449
- render(source: RenderSource | Iterable<RenderItem> | Scene, camera?: CameraLike): RenderDeviceDiagnostics;
450
- render(sourceOrInput: RendererInput | RenderSource | Iterable<RenderItem> | Scene, camera?: CameraLike): RenderDeviceDiagnostics {
451
- this.assertAlive();
452
- const { source, camera: inputCamera } = normalizeRendererInput(sourceOrInput, camera);
453
- sceneFromSource(source)?.updateWorldTransforms();
454
- const cameraPolicy = collectCameraPolicy(source);
455
- const identityPolicyIgnoresSceneCamera = inputCamera === undefined && cameraPolicy === "identity";
456
- const autoFrameOverridesSceneCamera = inputCamera === undefined && hasExplicitAutoFrameCameraPolicy(source);
457
- const resolvedCamera = resolveCamera(source, inputCamera, { width: this.width, height: this.height }, {
458
- allowSceneCamera: !identityPolicyIgnoresSceneCamera && !autoFrameOverridesSceneCamera
459
- });
460
- if (!resolvedCamera && cameraPolicy === "require") {
461
- throw new RenderDeviceError("RenderSource requires an explicit camera but none was supplied or found in the scene.", "CAMERA_REQUIRED");
462
- }
463
- let cameraViewProjection = resolvedCamera?.viewProjectionMatrix;
464
- const collectedItems = collectRenderItemsWithDiagnostics(source, cameraViewProjection, resolvedCamera?.camera);
465
- const collectionDiagnostics = collectedItems.diagnostics;
466
- let items = collectedItems.items;
467
- const lights = collectRenderLights(source);
468
- const environmentBackground = collectEnvironmentBackground(source);
469
- const environmentLighting = collectEnvironmentLighting(source);
470
- const environmentFog = collectEnvironmentFog(source);
471
- const explicitShadowMap = collectForwardShadowMap(source);
472
- const shadowOptions = collectRendererShadowOptions(source);
473
- const sourceCameraPosition = collectSourceCameraPosition(source);
474
- const explicitRenderTarget = collectRenderTarget(source);
475
- validateExplicitRenderTarget(explicitRenderTarget, this.width, this.height);
476
- let cameraPosition = sourceCameraPosition ?? resolvedCamera?.cameraPosition;
477
- if (!resolvedCamera && cameraPolicy === "auto-frame") {
478
- const autoFrame = createAutoFrameCamera(source, items, this.width, this.height);
479
- if (autoFrame) {
480
- cameraViewProjection = autoFrame.viewProjectionMatrix;
481
- items = applyViewProjection(items, autoFrame.viewProjectionMatrix);
482
- cameraPosition = sourceCameraPosition ?? autoFrame.cameraPosition;
483
- }
484
- }
485
- const postprocess = collectPostprocess(source);
486
- const ownedTargets: RenderTarget[] = [];
487
- const ownedShadowPasses: ShadowPass[] = [];
488
- this.graph.clear();
489
- let postprocessTargetFormat: Extract<RenderTargetDescriptor["format"], "rgba8" | "rgba16f" | "rgba32f"> | undefined;
490
- if (postprocess) {
491
- const format = postprocess.targetFormat ?? defaultPostprocessTargetFormat(this.device, postprocess);
492
- postprocessTargetFormat = format;
493
- if ((format === "rgba16f" || format === "rgba32f") && !this.device.info.capabilities?.includes("hdr-render-targets")) {
494
- throw new RenderDeviceError("Renderer HDR postprocess requires an HDR-capable backend", "HDR_POSTPROCESS_UNSUPPORTED", {
495
- backend: this.device.kind,
496
- format
497
- });
498
- }
499
- if ((format === "rgba16f" || format === "rgba32f") && postprocess.toneMapping === false) {
500
- throw new RenderDeviceError("Renderer HDR postprocess requires tone mapping before presentation or LDR postprocess passes", "HDR_POSTPROCESS_TONEMAPPING_REQUIRED", {
501
- backend: this.device.kind,
502
- format
503
- });
504
- }
505
- if (!this.device.presentRenderTarget) {
506
- throw new RenderDeviceError("Renderer postprocess requires a backend presentation path", "POSTPROCESS_PRESENT_UNSUPPORTED", {
507
- backend: this.device.kind
508
- });
509
- }
510
- const requiresDepthTexture = postprocessRequiresDepthTexture(postprocess);
511
- if (requiresDepthTexture && !this.device.info.capabilities?.includes("depth-textures")) {
512
- throw new RenderDeviceError("Depth-aware renderer postprocess requires a backend with sampleable depth texture render targets.", "DEPTH_POSTPROCESS_UNSUPPORTED", {
513
- backend: this.device.kind
514
- });
515
- }
516
- const forwardTarget = this.device.createRenderTarget({
517
- width: this.width,
518
- height: this.height,
519
- label: "renderer-forward-color",
520
- format,
521
- depth: requiresDepthTexture ? "texture" : true
522
- });
523
- ownedTargets.push(forwardTarget);
524
- this.device.setRenderTarget(forwardTarget);
525
- }
526
- this.device.beginFrame(this.width, this.height);
527
- try {
528
- const rendererShadowMap = explicitShadowMap ?? this.executeRendererShadowMap({
529
- shadowOptions,
530
- source,
531
- items,
532
- lights,
533
- ownedTargets,
534
- ownedShadowPasses
535
- });
536
- if (postprocess) {
537
- const forwardTarget = ownedTargets[0];
538
- if (!forwardTarget) {
539
- throw new RenderDeviceError("Renderer postprocess missing forward render target", "POSTPROCESS_TARGET_MISSING");
540
- }
541
- this.device.setRenderTarget(forwardTarget);
542
- } else {
543
- this.device.setRenderTarget(explicitRenderTarget ?? null);
544
- }
545
- this.device.clear(this.clearColor);
546
- if (environmentBackground) {
547
- this.graph.addPass(new EnvironmentBackgroundPass({
548
- ...environmentBackground,
549
- outputColorSpace: postprocess ? "linear" : environmentBackground.outputColorSpace,
550
- inverseViewProjectionMatrix: environmentBackground.inverseViewProjectionMatrix ?? invertMat4(cameraViewProjection ?? identityMat4()),
551
- shaderLibrary: this.shaderLibrary
552
- }));
553
- }
554
- this.graph.addPass(new ForwardPass({
555
- items,
556
- lights,
557
- environmentLighting,
558
- environmentFog,
559
- inputColorResource: environmentBackground ? ENVIRONMENT_BACKGROUND_COLOR_RESOURCE : undefined,
560
- shadowMap: rendererShadowMap,
561
- cameraPosition,
562
- outputColorSpace: postprocess ? "linear" : "srgb",
563
- shaderLibrary: this.shaderLibrary
564
- }));
565
- this.graph.execute({ device: this.device, width: this.width, height: this.height });
566
- if (postprocess) {
567
- this.executePostprocess(postprocess, ownedTargets, explicitRenderTarget);
568
- }
569
- } finally {
570
- this.device.endFrame();
571
- for (const shadowPass of ownedShadowPasses) {
572
- shadowPass.dispose();
573
- }
574
- for (const target of ownedTargets) {
575
- target.dispose();
576
- }
577
- }
578
- return withRendererFrameDiagnostics(this.device.getDiagnostics(), collectionDiagnostics, createPostprocessDiagnostics(postprocess, ownedTargets, this.width, this.height, {
579
- targetFormat: postprocessTargetFormat,
580
- nativeLdrPostprocess: Boolean(this.device.presentLdrPostprocess),
581
- rendererDepthAvailable: Boolean(postprocess && postprocessRequiresDepthTexture(postprocess) && this.device.info.capabilities?.includes("depth-textures"))
582
- }));
583
- }
584
-
585
- renderAsync(input: RendererInput): Promise<RenderDeviceDiagnostics>;
586
- renderAsync(source: RenderSource | Iterable<RenderItem> | Scene, camera?: CameraLike): Promise<RenderDeviceDiagnostics>;
587
- async renderAsync(sourceOrInput: RendererInput | RenderSource | Iterable<RenderItem> | Scene, camera?: CameraLike): Promise<RenderDeviceDiagnostics> {
588
- this.assertAlive();
589
- const { source, camera: inputCamera } = normalizeRendererInput(sourceOrInput, camera);
590
- sceneFromSource(source)?.updateWorldTransforms();
591
- const cameraPolicy = collectCameraPolicy(source);
592
- const identityPolicyIgnoresSceneCamera = inputCamera === undefined && cameraPolicy === "identity";
593
- const autoFrameOverridesSceneCamera = inputCamera === undefined && hasExplicitAutoFrameCameraPolicy(source);
594
- const resolvedCamera = resolveCamera(source, inputCamera, { width: this.width, height: this.height }, {
595
- allowSceneCamera: !identityPolicyIgnoresSceneCamera && !autoFrameOverridesSceneCamera
596
- });
597
- if (!resolvedCamera && cameraPolicy === "require") {
598
- throw new RenderDeviceError("RenderSource requires an explicit camera but none was supplied or found in the scene.", "CAMERA_REQUIRED");
599
- }
600
- let cameraViewProjection = resolvedCamera?.viewProjectionMatrix;
601
- const collectedItems = collectRenderItemsWithDiagnostics(source, cameraViewProjection, resolvedCamera?.camera);
602
- const collectionDiagnostics = collectedItems.diagnostics;
603
- let items = collectedItems.items;
604
- const lights = collectRenderLights(source);
605
- const environmentBackground = collectEnvironmentBackground(source);
606
- const environmentLighting = collectEnvironmentLighting(source);
607
- const environmentFog = collectEnvironmentFog(source);
608
- const explicitShadowMap = collectForwardShadowMap(source);
609
- const shadowOptions = collectRendererShadowOptions(source);
610
- const sourceCameraPosition = collectSourceCameraPosition(source);
611
- const explicitRenderTarget = collectRenderTarget(source);
612
- validateExplicitRenderTarget(explicitRenderTarget, this.width, this.height);
613
- let cameraPosition = sourceCameraPosition ?? resolvedCamera?.cameraPosition;
614
- if (!resolvedCamera && cameraPolicy === "auto-frame") {
615
- const autoFrame = createAutoFrameCamera(source, items, this.width, this.height);
616
- if (autoFrame) {
617
- cameraViewProjection = autoFrame.viewProjectionMatrix;
618
- items = applyViewProjection(items, autoFrame.viewProjectionMatrix);
619
- cameraPosition = sourceCameraPosition ?? autoFrame.cameraPosition;
620
- }
621
- }
622
- const postprocess = collectPostprocess(source);
623
- const ownedTargets: RenderTarget[] = [];
624
- const ownedShadowPasses: ShadowPass[] = [];
625
- this.graph.clear();
626
- let postprocessTargetFormat: Extract<RenderTargetDescriptor["format"], "rgba8" | "rgba16f" | "rgba32f"> | undefined;
627
- if (postprocess) {
628
- const format = postprocess.targetFormat ?? defaultPostprocessTargetFormat(this.device, postprocess);
629
- postprocessTargetFormat = format;
630
- if ((format === "rgba16f" || format === "rgba32f") && !this.device.info.capabilities?.includes("hdr-render-targets")) {
631
- throw new RenderDeviceError("Renderer HDR postprocess requires an HDR-capable backend", "HDR_POSTPROCESS_UNSUPPORTED", {
632
- backend: this.device.kind,
633
- format
634
- });
635
- }
636
- if ((format === "rgba16f" || format === "rgba32f") && postprocess.toneMapping === false) {
637
- throw new RenderDeviceError("Renderer HDR postprocess requires tone mapping before presentation or LDR postprocess passes", "HDR_POSTPROCESS_TONEMAPPING_REQUIRED", {
638
- backend: this.device.kind,
639
- format
640
- });
641
- }
642
- if (!this.device.presentRenderTarget) {
643
- throw new RenderDeviceError("Renderer postprocess requires a backend presentation path", "POSTPROCESS_PRESENT_UNSUPPORTED", {
644
- backend: this.device.kind
645
- });
646
- }
647
- const requiresDepthTexture = postprocessRequiresDepthTexture(postprocess);
648
- if (requiresDepthTexture && !this.device.info.capabilities?.includes("depth-textures")) {
649
- throw new RenderDeviceError("Depth-aware renderer postprocess requires a backend with sampleable depth texture render targets.", "DEPTH_POSTPROCESS_UNSUPPORTED", {
650
- backend: this.device.kind
651
- });
652
- }
653
- const forwardTarget = this.device.createRenderTarget({
654
- width: this.width,
655
- height: this.height,
656
- label: "renderer-forward-color",
657
- format,
658
- depth: requiresDepthTexture ? "texture" : true
659
- });
660
- ownedTargets.push(forwardTarget);
661
- this.device.setRenderTarget(forwardTarget);
662
- }
663
- this.device.beginFrame(this.width, this.height);
664
- try {
665
- const rendererShadowMap = explicitShadowMap ?? this.executeRendererShadowMap({
666
- shadowOptions,
667
- source,
668
- items,
669
- lights,
670
- ownedTargets,
671
- ownedShadowPasses
672
- });
673
- if (postprocess) {
674
- const forwardTarget = ownedTargets[0];
675
- if (!forwardTarget) {
676
- throw new RenderDeviceError("Renderer postprocess missing forward render target", "POSTPROCESS_TARGET_MISSING");
677
- }
678
- this.device.setRenderTarget(forwardTarget);
679
- } else {
680
- this.device.setRenderTarget(explicitRenderTarget ?? null);
681
- }
682
- this.device.clear(this.clearColor);
683
- if (environmentBackground) {
684
- this.graph.addPass(new EnvironmentBackgroundPass({
685
- ...environmentBackground,
686
- outputColorSpace: postprocess ? "linear" : environmentBackground.outputColorSpace,
687
- inverseViewProjectionMatrix: environmentBackground.inverseViewProjectionMatrix ?? invertMat4(cameraViewProjection ?? identityMat4()),
688
- shaderLibrary: this.shaderLibrary
689
- }));
690
- }
691
- this.graph.addPass(new ForwardPass({
692
- items,
693
- lights,
694
- environmentLighting,
695
- environmentFog,
696
- inputColorResource: environmentBackground ? ENVIRONMENT_BACKGROUND_COLOR_RESOURCE : undefined,
697
- shadowMap: rendererShadowMap,
698
- cameraPosition,
699
- outputColorSpace: postprocess ? "linear" : "srgb",
700
- shaderLibrary: this.shaderLibrary
701
- }));
702
- this.graph.execute({ device: this.device, width: this.width, height: this.height });
703
- if (postprocess) {
704
- await this.executePostprocessAsync(postprocess, ownedTargets, explicitRenderTarget);
705
- }
706
- } finally {
707
- this.device.endFrame();
708
- for (const shadowPass of ownedShadowPasses) {
709
- shadowPass.dispose();
710
- }
711
- for (const target of ownedTargets) {
712
- target.dispose();
713
- }
714
- }
715
- return withRendererFrameDiagnostics(this.device.getDiagnostics(), collectionDiagnostics, createPostprocessDiagnostics(postprocess, ownedTargets, this.width, this.height, {
716
- targetFormat: postprocessTargetFormat,
717
- nativeLdrPostprocess: Boolean(this.device.presentLdrPostprocess),
718
- rendererDepthAvailable: Boolean(postprocess && postprocessRequiresDepthTexture(postprocess) && this.device.info.capabilities?.includes("depth-textures"))
719
- }));
720
- }
721
-
722
- renderScene(scene: RenderSource | Scene, camera?: CameraLike): RenderDeviceDiagnostics {
723
- return this.render(scene, camera);
724
- }
725
-
726
- renderItems(items: Iterable<RenderItem>, camera?: CameraLike, options: Omit<RenderSource, "renderItems"> = {}): RenderDeviceDiagnostics {
727
- return this.render({ ...options, renderItems: items }, camera);
728
- }
729
-
730
- captureFrame(source?: RenderSource | Iterable<RenderItem> | Scene, camera?: CameraLike): RendererFrameCaptureWithMetadata {
731
- const diagnostics = source ? this.render(source, camera) : this.device.getDiagnostics();
732
- const pixels = this.device.readPixels(0, 0, this.width, this.height);
733
- const metadata = createRendererFrameCaptureMetadata(this.device, this.width, this.height, pixels, diagnostics);
734
- return {
735
- width: this.width,
736
- height: this.height,
737
- pixels,
738
- diagnostics,
739
- capturedAt: metadata.capturedAt,
740
- renderSize: metadata.renderSize,
741
- pixelHash: metadata.pixelHash,
742
- pixelDigest: metadata.pixelDigest,
743
- pixelStats: metadata.pixelStats,
744
- diagnosticsSummary: metadata.diagnosticsSummary,
745
- metadata
746
- };
747
- }
748
-
749
- getDiagnostics(): RenderDeviceDiagnostics {
750
- return this.device.getDiagnostics();
751
- }
752
-
753
- dispose(): void {
754
- this.animationLoop?.stop();
755
- this.animationLoop = null;
756
- this.device.dispose();
757
- this.disposed = true;
758
- }
759
-
760
- private assertAlive(): void {
761
- if (this.disposed || this.device.disposed) {
762
- throw new RenderDeviceError("Renderer is disposed", "DISPOSED_DEVICE");
763
- }
764
- }
765
-
766
- private resizeCanvas(width: number, height: number): void {
767
- if (!this.canvas) return;
768
- if (this.canvas.width !== width) {
769
- this.canvas.width = width;
770
- }
771
- if (this.canvas.height !== height) {
772
- this.canvas.height = height;
773
- }
774
- }
775
-
776
- private executePostprocess(postprocess: RendererPostProcessOptions, ownedTargets: RenderTarget[], outputTarget?: RenderTarget): void {
777
- const forwardTarget = ownedTargets[0];
778
- let current = forwardTarget;
779
- if (!current) {
780
- throw new RenderDeviceError("Renderer postprocess missing forward render target", "POSTPROCESS_TARGET_MISSING");
781
- }
782
- const passes = createRendererPostprocessPasses(postprocess);
783
- if (passes.length === 0) {
784
- if (outputTarget) {
785
- this.device.setRenderTarget(current);
786
- writePostProcessPixels(this.device, current, outputTarget, this.device.readPixels(0, 0, current.width, current.height));
787
- } else {
788
- this.device.presentRenderTarget?.(current);
789
- }
790
- return;
791
- }
792
- if (this.executeFusedLdrPostprocess(current, passes, outputTarget)) return;
793
- for (let index = 0; index < passes.length; index += 1) {
794
- const pass = passes[index]!;
795
- const nextPass = passes[index + 1];
796
- if (pass.name === "bloom" && isHdrRenderTarget(current)) {
797
- if (nextPass?.name !== "tone-mapping") {
798
- throw new RenderDeviceError("Renderer HDR bloom requires tone mapping immediately after the float bloom pass.", "HDR_BLOOM_TONEMAPPING_REQUIRED", {
799
- source: current.label
800
- });
801
- }
802
- const isCombinedLast = index + 1 === passes.length - 1;
803
- const target = isCombinedLast ? outputTarget : this.device.createRenderTarget({
804
- width: this.width,
805
- height: this.height,
806
- label: "renderer-postprocess-tone-mapping",
807
- format: "rgba8",
808
- depth: false
809
- });
810
- if (target) ownedTargets.push(target);
811
- this.device.setRenderTarget(current);
812
- const bloomed = bloomFloatPixels(
813
- this.device.readFloatPixels(0, 0, current.width, current.height),
814
- current.width,
815
- current.height,
816
- pass.options as BloomOptions
817
- );
818
- const mapped = toneMapFloatPixels(
819
- bloomed.pixels,
820
- current.width,
821
- current.height,
822
- {
823
- outputColorSpace: "srgb",
824
- ...(nextPass.options as ToneMappingOptions)
825
- }
826
- );
827
- writePostProcessPixels(this.device, current, target, mapped.pixels);
828
- if (target) current = target;
829
- index += 1;
830
- continue;
831
- }
832
- const isLast = index === passes.length - 1;
833
- const target = isLast ? outputTarget : this.device.createRenderTarget({
834
- width: this.width,
835
- height: this.height,
836
- label: `renderer-postprocess-${pass.name}`,
837
- format: "rgba8",
838
- depth: false
839
- });
840
- if (target) ownedTargets.push(target);
841
- if (pass.name === "tone-mapping") {
842
- new ToneMappingPass({
843
- source: current,
844
- target,
845
- outputColorSpace: "srgb",
846
- ...(pass.options as ToneMappingOptions)
847
- }).execute({ device: this.device, width: this.width, height: this.height });
848
- } else if (pass.name === "bloom") {
849
- new BloomPass({
850
- source: current,
851
- target,
852
- ...(pass.options as BloomOptions)
853
- }).execute({ device: this.device, width: this.width, height: this.height });
854
- } else if (pass.name === "fxaa") {
855
- new FXAAPass({
856
- source: current,
857
- target,
858
- ...(pass.options as FXAAOptions)
859
- }).execute({ device: this.device, width: this.width, height: this.height });
860
- } else {
861
- this.executePixelPostprocessPass(pass, current, target, forwardTarget);
862
- }
863
- if (target) current = target;
864
- }
865
- }
866
-
867
- private executeFusedLdrPostprocess(current: RenderTarget, passes: readonly RendererPostProcessPassPlan[], outputTarget?: RenderTarget): boolean {
868
- if (!canFuseLdrPostprocess(current, passes)) return false;
869
- if (this.device.presentLdrPostprocess) {
870
- this.device.presentLdrPostprocess(current, {
871
- passes: passes.map((pass) => ({
872
- name: pass.name,
873
- options: pass.options as Readonly<Record<string, unknown>>
874
- })) as readonly LdrPostprocessPassDescriptor[],
875
- ...(outputTarget ? { outputTarget } : {}),
876
- toneMappingDefaults: { outputColorSpace: "srgb" }
877
- });
878
- return true;
879
- }
880
- this.device.setRenderTarget(current);
881
- const pixels = fusedLdrPostprocessPixels(
882
- this.device.readPixels(0, 0, current.width, current.height),
883
- current.width,
884
- current.height,
885
- passes as readonly FusedLdrPostProcessPass[],
886
- {
887
- mutateInput: true,
888
- scratch: this.fusedLdrPostprocessScratch,
889
- toneMappingDefaults: { outputColorSpace: "srgb" }
890
- }
891
- );
892
- writePostProcessPixels(this.device, current, outputTarget, pixels);
893
- return true;
894
- }
895
-
896
- private async executePostprocessAsync(postprocess: RendererPostProcessOptions, ownedTargets: RenderTarget[], outputTarget?: RenderTarget): Promise<void> {
897
- const forwardTarget = ownedTargets[0];
898
- let current = forwardTarget;
899
- if (!current) {
900
- throw new RenderDeviceError("Renderer postprocess missing forward render target", "POSTPROCESS_TARGET_MISSING");
901
- }
902
- const passes = createRendererPostprocessPasses(postprocess);
903
- if (passes.length === 0) {
904
- if (outputTarget) {
905
- this.device.setRenderTarget(current);
906
- writePostProcessPixels(this.device, current, outputTarget, await this.readRenderTargetPixelsAsync(current));
907
- } else {
908
- this.device.presentRenderTarget?.(current);
909
- }
910
- return;
911
- }
912
- if (await this.executeFusedLdrPostprocessAsync(current, passes, outputTarget)) return;
913
- for (let index = 0; index < passes.length; index += 1) {
914
- const pass = passes[index]!;
915
- const nextPass = passes[index + 1];
916
- if (pass.name === "bloom" && isHdrRenderTarget(current)) {
917
- if (nextPass?.name !== "tone-mapping") {
918
- throw new RenderDeviceError("Renderer HDR bloom requires tone mapping immediately after the float bloom pass.", "HDR_BLOOM_TONEMAPPING_REQUIRED", {
919
- source: current.label
920
- });
921
- }
922
- const isCombinedLast = index + 1 === passes.length - 1;
923
- const target = isCombinedLast ? outputTarget : this.device.createRenderTarget({
924
- width: this.width,
925
- height: this.height,
926
- label: "renderer-postprocess-tone-mapping",
927
- format: "rgba8",
928
- depth: false
929
- });
930
- if (target) ownedTargets.push(target);
931
- const bloomed = bloomFloatPixels(
932
- await this.readRenderTargetFloatPixelsAsync(current),
933
- current.width,
934
- current.height,
935
- pass.options as BloomOptions
936
- );
937
- const mapped = toneMapFloatPixels(
938
- bloomed.pixels,
939
- current.width,
940
- current.height,
941
- {
942
- outputColorSpace: "srgb",
943
- ...(nextPass.options as ToneMappingOptions)
944
- }
945
- );
946
- writePostProcessPixels(this.device, current, target, mapped.pixels);
947
- if (target) current = target;
948
- index += 1;
949
- continue;
950
- }
951
- const isLast = index === passes.length - 1;
952
- const target = isLast ? outputTarget : this.device.createRenderTarget({
953
- width: this.width,
954
- height: this.height,
955
- label: `renderer-postprocess-${pass.name}`,
956
- format: "rgba8",
957
- depth: false
958
- });
959
- if (target) ownedTargets.push(target);
960
- if (pass.name === "tone-mapping") {
961
- const mapped = isHdrRenderTarget(current)
962
- ? toneMapFloatPixels(await this.readRenderTargetFloatPixelsAsync(current), current.width, current.height, {
963
- outputColorSpace: "srgb",
964
- ...(pass.options as ToneMappingOptions)
965
- })
966
- : toneMapPixels(await this.readRenderTargetPixelsAsync(current), current.width, current.height, {
967
- outputColorSpace: "srgb",
968
- ...(pass.options as ToneMappingOptions)
969
- });
970
- writePostProcessPixels(this.device, current, target, mapped.pixels);
971
- } else if (pass.name === "bloom") {
972
- const bloomed = bloomPixels(await this.readRenderTargetPixelsAsync(current), current.width, current.height, pass.options as BloomOptions);
973
- writePostProcessPixels(this.device, current, target, bloomed.pixels);
974
- } else if (pass.name === "fxaa") {
975
- const smoothed = fxaaPixels(await this.readRenderTargetPixelsAsync(current), current.width, current.height, pass.options as FXAAOptions);
976
- writePostProcessPixels(this.device, current, target, smoothed.pixels);
977
- } else {
978
- await this.executePixelPostprocessPassAsync(pass, current, target, forwardTarget);
979
- }
980
- if (target) current = target;
981
- }
982
- }
983
-
984
- private async executeFusedLdrPostprocessAsync(current: RenderTarget, passes: readonly RendererPostProcessPassPlan[], outputTarget?: RenderTarget): Promise<boolean> {
985
- if (!canFuseLdrPostprocess(current, passes)) return false;
986
- if (this.device.presentLdrPostprocess) {
987
- this.device.presentLdrPostprocess(current, {
988
- passes: passes.map((pass) => ({
989
- name: pass.name,
990
- options: pass.options as Readonly<Record<string, unknown>>
991
- })) as readonly LdrPostprocessPassDescriptor[],
992
- ...(outputTarget ? { outputTarget } : {}),
993
- toneMappingDefaults: { outputColorSpace: "srgb" }
994
- });
995
- return true;
996
- }
997
- const pixels = fusedLdrPostprocessPixels(
998
- await this.readRenderTargetPixelsAsync(current),
999
- current.width,
1000
- current.height,
1001
- passes as readonly FusedLdrPostProcessPass[],
1002
- {
1003
- mutateInput: true,
1004
- scratch: this.fusedLdrPostprocessScratch,
1005
- toneMappingDefaults: { outputColorSpace: "srgb" }
1006
- }
1007
- );
1008
- writePostProcessPixels(this.device, current, outputTarget, pixels);
1009
- return true;
1010
- }
1011
-
1012
- private executePixelPostprocessPass(pass: RendererPostProcessPassPlan, source: RenderTarget, target: RenderTarget | undefined, forwardTarget: RenderTarget): void {
1013
- this.device.setRenderTarget(source);
1014
- const input = this.device.readPixels(0, 0, source.width, source.height);
1015
- const rendererDepth = isDepthPostprocessPass(pass.name) && !postprocessPassHasDepth(pass.options)
1016
- ? this.readRendererOwnedDepthTexture(forwardTarget)
1017
- : undefined;
1018
- const result = pass.name === "color-grade"
1019
- ? colorGradePixels(input, source.width, source.height, pass.options as ColorGradeOptions).pixels
1020
- : pass.name === "chromatic-aberration"
1021
- ? chromaticAberrationPixels(input, source.width, source.height, pass.options as ChromaticAberrationOptions).pixels
1022
- : pass.name === "film-grain"
1023
- ? filmGrainPixels(input, source.width, source.height, pass.options as FilmGrainOptions).pixels
1024
- : pass.name === "depth-of-field"
1025
- ? depthOfFieldPixels(input, source.width, source.height, withRendererDepth(pass.options as DepthOfFieldOptions, rendererDepth)).pixels
1026
- : pass.name === "motion-blur"
1027
- ? motionBlurPixels(input, source.width, source.height, pass.options as MotionBlurOptions).pixels
1028
- : pass.name === "contact-shadow"
1029
- ? contactShadowPixels(input, source.width, source.height, withRendererDepth(pass.options as ContactShadowPostProcessOptions, rendererDepth)).pixels
1030
- : pass.name === "ssao"
1031
- ? ssaoPixels(input, source.width, source.height, withRendererDepth(pass.options as SSAOOptions, rendererDepth)).pixels
1032
- : pass.name === "ssr"
1033
- ? ssrPixels(input, source.width, source.height, withRendererDepth(pass.options as SSROptions, rendererDepth)).pixels
1034
- : pass.name === "taa"
1035
- ? taaPixels(input, source.width, source.height, pass.options as TAAOptions).pixels
1036
- : pass.name === "outline"
1037
- ? outlinePixels(input, source.width, source.height, pass.options as OutlineOptions).pixels
1038
- : undefined;
1039
- if (!result) {
1040
- throw new RenderDeviceError("Renderer postprocess pass is outside the supported renderer pass catalog", "POSTPROCESS_PASS_UNKNOWN", {
1041
- pass: pass.name
1042
- });
1043
- }
1044
- writePostProcessPixels(this.device, source, target, result);
1045
- }
1046
-
1047
- private async executePixelPostprocessPassAsync(pass: RendererPostProcessPassPlan, source: RenderTarget, target: RenderTarget | undefined, forwardTarget: RenderTarget): Promise<void> {
1048
- const input = await this.readRenderTargetPixelsAsync(source);
1049
- const rendererDepth = isDepthPostprocessPass(pass.name) && !postprocessPassHasDepth(pass.options)
1050
- ? this.readRendererOwnedDepthTexture(forwardTarget)
1051
- : undefined;
1052
- const result = pass.name === "color-grade"
1053
- ? colorGradePixels(input, source.width, source.height, pass.options as ColorGradeOptions).pixels
1054
- : pass.name === "chromatic-aberration"
1055
- ? chromaticAberrationPixels(input, source.width, source.height, pass.options as ChromaticAberrationOptions).pixels
1056
- : pass.name === "film-grain"
1057
- ? filmGrainPixels(input, source.width, source.height, pass.options as FilmGrainOptions).pixels
1058
- : pass.name === "depth-of-field"
1059
- ? depthOfFieldPixels(input, source.width, source.height, withRendererDepth(pass.options as DepthOfFieldOptions, rendererDepth)).pixels
1060
- : pass.name === "motion-blur"
1061
- ? motionBlurPixels(input, source.width, source.height, pass.options as MotionBlurOptions).pixels
1062
- : pass.name === "contact-shadow"
1063
- ? contactShadowPixels(input, source.width, source.height, withRendererDepth(pass.options as ContactShadowPostProcessOptions, rendererDepth)).pixels
1064
- : pass.name === "ssao"
1065
- ? ssaoPixels(input, source.width, source.height, withRendererDepth(pass.options as SSAOOptions, rendererDepth)).pixels
1066
- : pass.name === "ssr"
1067
- ? ssrPixels(input, source.width, source.height, withRendererDepth(pass.options as SSROptions, rendererDepth)).pixels
1068
- : pass.name === "taa"
1069
- ? taaPixels(input, source.width, source.height, pass.options as TAAOptions).pixels
1070
- : pass.name === "outline"
1071
- ? outlinePixels(input, source.width, source.height, pass.options as OutlineOptions).pixels
1072
- : undefined;
1073
- if (!result) {
1074
- throw new RenderDeviceError("Renderer postprocess pass is outside the supported renderer pass catalog", "POSTPROCESS_PASS_UNKNOWN", {
1075
- pass: pass.name
1076
- });
1077
- }
1078
- writePostProcessPixels(this.device, source, target, result);
1079
- }
1080
-
1081
- private async readRenderTargetPixelsAsync(target: RenderTarget): Promise<Uint8Array> {
1082
- this.device.setRenderTarget(target);
1083
- if (this.device.readPixelsAsync && target.colorTexture.format === "rgba8") {
1084
- return this.device.readPixelsAsync(0, 0, target.width, target.height);
1085
- }
1086
- return this.device.readPixels(0, 0, target.width, target.height);
1087
- }
1088
-
1089
- private async readRenderTargetFloatPixelsAsync(target: RenderTarget): Promise<Float32Array> {
1090
- this.device.setRenderTarget(target);
1091
- if (this.device.readFloatPixelsAsync && isHdrRenderTarget(target)) {
1092
- return this.device.readFloatPixelsAsync(0, 0, target.width, target.height);
1093
- }
1094
- return this.device.readFloatPixels(0, 0, target.width, target.height);
1095
- }
1096
-
1097
- private readRendererOwnedDepthTexture(forwardTarget: RenderTarget): DepthTextureBinding {
1098
- if (!forwardTarget.depthTexture) {
1099
- throw new RenderDeviceError("Renderer-owned depth postprocess requires the forward target to expose a depth texture.", "POSTPROCESS_DEPTH_TARGET_MISSING", {
1100
- renderTarget: forwardTarget.label
1101
- });
1102
- }
1103
- if (!this.device.readDepthPixels) {
1104
- throw new RenderDeviceError("Renderer-owned depth postprocess requires backend depth readback.", "DEPTH_READBACK_UNSUPPORTED", {
1105
- backend: this.device.kind
1106
- });
1107
- }
1108
- this.device.setRenderTarget(forwardTarget);
1109
- return createDepthTextureBinding({
1110
- label: forwardTarget.depthTexture.label,
1111
- width: forwardTarget.width,
1112
- height: forwardTarget.height,
1113
- data: this.device.readDepthPixels(0, 0, forwardTarget.width, forwardTarget.height)
1114
- });
1115
- }
1116
-
1117
- private executeRendererShadowMap(options: {
1118
- readonly shadowOptions: RendererShadowOptions | undefined;
1119
- readonly source: RenderSource | Iterable<RenderItem> | Scene;
1120
- readonly items: readonly RenderItem[];
1121
- readonly lights: readonly CollectedLight[];
1122
- readonly ownedTargets: RenderTarget[];
1123
- readonly ownedShadowPasses: ShadowPass[];
1124
- }): ForwardShadowMapOptions | undefined {
1125
- if (!options.shadowOptions || options.shadowOptions.enabled === false) {
1126
- return undefined;
1127
- }
1128
- if (!this.device.info.capabilities?.includes("render-targets")) {
1129
- throw new RenderDeviceError("Renderer-owned shadows require render targets", "SHADOW_RENDER_TARGET_UNSUPPORTED", {
1130
- backend: this.device.kind
1131
- });
1132
- }
1133
- const light = options.shadowOptions.light ?? firstShadowCastingLight(options.source, options.lights);
1134
- if (light instanceof PointLight) {
1135
- return this.executeRendererPointShadowMap({
1136
- shadowOptions: options.shadowOptions,
1137
- items: options.items,
1138
- ownedTargets: options.ownedTargets,
1139
- ownedShadowPasses: options.ownedShadowPasses,
1140
- light
1141
- });
1142
- }
1143
- if (light && !(light instanceof DirectionalLight) && !(light instanceof SpotLight)) {
1144
- throw new RenderDeviceError("Renderer-owned shadow maps require directional, spot, or point lights.", "SHADOW_LIGHT_TYPE_UNSUPPORTED", {
1145
- lightName: light.name,
1146
- lightType: light.constructor.name
1147
- });
1148
- }
1149
- const lightMatrix = options.shadowOptions.lightMatrix
1150
- ? toMat4(options.shadowOptions.lightMatrix, "shadow.lightMatrix")
1151
- : createRendererOwnedShadowMatrix(light, options.items, firstShadowCastingCollectedLight(options.lights));
1152
- const shadowMap = new ShadowMap({
1153
- size: options.shadowOptions.size,
1154
- bias: options.shadowOptions.bias,
1155
- filter: options.shadowOptions.filter,
1156
- pcfRadius: options.shadowOptions.pcfRadius,
1157
- pcfSamples: options.shadowOptions.pcfSamples,
1158
- pcfDistribution: options.shadowOptions.pcfDistribution,
1159
- label: options.shadowOptions.label ?? "renderer-shadow-map"
1160
- });
1161
- const shadowPass = new ShadowPass({
1162
- light,
1163
- casters: options.items,
1164
- shadowMap,
1165
- viewProjectionMatrix: lightMatrix,
1166
- shaderLibrary: this.shaderLibrary
1167
- });
1168
- options.ownedShadowPasses.push(shadowPass);
1169
- const result = shadowPass.execute({ device: this.device, width: this.width, height: this.height });
1170
- if (!result.rendered) {
1171
- if (result.reason === "no-light" || result.reason === "light-disabled" || result.reason === "not-shadow-casting") {
1172
- throw new RenderDeviceError("Renderer-owned shadows require an enabled shadow-casting light.", "SHADOW_LIGHT_REQUIRED", {
1173
- reason: result.reason
1174
- });
1175
- }
1176
- return undefined;
1177
- }
1178
- return shadowPass.getForwardShadowMap({
1179
- lightMatrix,
1180
- strength: options.shadowOptions.strength,
1181
- slopeBias: options.shadowOptions.slopeBias,
1182
- texelSize: options.shadowOptions.texelSize,
1183
- bias: options.shadowOptions.bias,
1184
- filterKernel: options.shadowOptions.filterKernel
1185
- }) ?? undefined;
1186
- }
1187
-
1188
- private executeRendererPointShadowMap(options: {
1189
- readonly shadowOptions: RendererShadowOptions;
1190
- readonly items: readonly RenderItem[];
1191
- readonly ownedTargets: RenderTarget[];
1192
- readonly ownedShadowPasses: ShadowPass[];
1193
- readonly light: PointLight;
1194
- }): ForwardShadowMapOptions | undefined {
1195
- if (!this.device.writeRenderTargetPixels) {
1196
- throw new RenderDeviceError("Renderer-owned point shadows require render-target pixel upload for the point-light atlas.", "POINT_SHADOW_ATLAS_UPLOAD_UNSUPPORTED", {
1197
- backend: this.device.kind
1198
- });
1199
- }
1200
- options.light.transform.updateWorld(undefined, true);
1201
- const size = options.shadowOptions.size ?? 512;
1202
- const faceMatrices = createPointShadowFaceMatrices(options.light);
1203
- const faceRects = createPointShadowFaceRects();
1204
- const shadowMapOptions = {
1205
- size,
1206
- bias: options.shadowOptions.bias,
1207
- filter: options.shadowOptions.filter,
1208
- pcfRadius: options.shadowOptions.pcfRadius,
1209
- pcfSamples: options.shadowOptions.pcfSamples,
1210
- pcfDistribution: options.shadowOptions.pcfDistribution
1211
- };
1212
- const atlasPixels = new Uint8Array(size * 3 * size * 2 * 4);
1213
- for (let face = 0; face < 6; face += 1) {
1214
- const faceMatrix = faceMatrices.slice(face * 16, face * 16 + 16);
1215
- const shadowPass = new ShadowPass({
1216
- light: options.light,
1217
- casters: options.items,
1218
- shadowMap: new ShadowMap({ ...shadowMapOptions, label: `${options.shadowOptions.label ?? "renderer-point-shadow"}-face-${face}` }),
1219
- viewProjectionMatrix: faceMatrix,
1220
- shaderLibrary: this.shaderLibrary
1221
- });
1222
- options.ownedShadowPasses.push(shadowPass);
1223
- const result = shadowPass.execute({ device: this.device, width: this.width, height: this.height });
1224
- if (!result.rendered) {
1225
- return undefined;
1226
- }
1227
- const target = shadowPass.getRenderTarget();
1228
- if (!target) {
1229
- throw new RenderDeviceError("Renderer-owned point shadow face did not expose a render target.", "POINT_SHADOW_FACE_TARGET_MISSING", { face });
1230
- }
1231
- const facePixels = readShadowFacePixels(this.device, target);
1232
- blitPointShadowFace(atlasPixels, size * 3, size * 2, facePixels, size, faceRects, face);
1233
- }
1234
- const atlasTarget = this.device.createRenderTarget({
1235
- width: size * 3,
1236
- height: size * 2,
1237
- label: `${options.shadowOptions.label ?? "renderer-point-shadow"}-atlas`,
1238
- format: "rgba8",
1239
- depth: false
1240
- });
1241
- options.ownedTargets.push(atlasTarget);
1242
- this.device.writeRenderTargetPixels(atlasTarget, atlasPixels);
1243
- const texture = new TextureBinding({
1244
- name: "u_pointShadowMapTexture",
1245
- texture: atlasTarget.colorTexture,
1246
- sampler: new Sampler({ minFilter: "nearest", magFilter: "nearest", addressU: "clamp-to-edge", addressV: "clamp-to-edge" }),
1247
- required: true
1248
- });
1249
- return {
1250
- texture: new TextureBinding({
1251
- name: "u_shadowMapTexture",
1252
- texture: atlasTarget.colorTexture,
1253
- sampler: new Sampler({ minFilter: "nearest", magFilter: "nearest", addressU: "clamp-to-edge", addressV: "clamp-to-edge" }),
1254
- required: true
1255
- }),
1256
- lightMatrix: identityMat4(),
1257
- strength: options.shadowOptions.strength,
1258
- slopeBias: options.shadowOptions.slopeBias,
1259
- texelSize: options.shadowOptions.texelSize,
1260
- bias: options.shadowOptions.bias,
1261
- filterKernel: options.shadowOptions.filterKernel,
1262
- pointLight: {
1263
- texture,
1264
- lightPosition: [options.light.transform.worldMatrix[12], options.light.transform.worldMatrix[13], options.light.transform.worldMatrix[14]],
1265
- range: options.light.range,
1266
- faceMatrices,
1267
- faceRects,
1268
- strength: options.shadowOptions.strength,
1269
- slopeBias: options.shadowOptions.slopeBias,
1270
- texelSize: [1 / Math.max(1, size * 3), 1 / Math.max(1, size * 2)],
1271
- bias: options.shadowOptions.bias,
1272
- filterKernel: options.shadowOptions.filterKernel
1273
- }
1274
- };
1275
- }
1276
- }
1277
-
1278
- export function pickSceneRenderables(
1279
- source: Pick<RenderSource, "geometryLibrary" | "morphTargetLibrary" | "scene">,
1280
- ray: Ray,
1281
- options: ScenePickOptions = {}
1282
- ): ScenePickHit | undefined {
1283
- return pickSceneRenderableHits(source, ray, options)[0];
1284
- }
1285
-
1286
- export function pickSceneRenderableHits(
1287
- source: Pick<RenderSource, "geometryLibrary" | "morphTargetLibrary" | "scene">,
1288
- ray: Ray,
1289
- options: ScenePickOptions = {}
1290
- ): readonly ScenePickHit[] {
1291
- const pointRadius = options.pointRadius;
1292
- const lineRadius = options.lineRadius;
1293
- if (pointRadius !== undefined && (!Number.isFinite(pointRadius) || pointRadius <= 0)) {
1294
- throw new RenderDeviceError("Scene point picking radius must be a finite positive number", "SCENE_PICK_RADIUS_INVALID", { pointRadius });
1295
- }
1296
- if (lineRadius !== undefined && (!Number.isFinite(lineRadius) || lineRadius <= 0)) {
1297
- throw new RenderDeviceError("Scene line picking radius must be a finite positive number", "SCENE_PICK_RADIUS_INVALID", { lineRadius });
1298
- }
1299
- const scene = source.scene;
1300
- if (!scene) {
1301
- throw new RenderDeviceError("Scene picking requires a scene", "SCENE_PICKING_SCENE_MISSING");
1302
- }
1303
- if (!source.geometryLibrary) {
1304
- throw new RenderDeviceError("Scene picking requires a geometryLibrary resource lookup", "SCENE_PICKING_RESOURCES_MISSING");
1305
- }
1306
-
1307
- scene.updateWorldTransforms();
1308
- const hits: ScenePickHit[] = [];
1309
- for (const { node, renderable } of scene.collectRenderables()) {
1310
- const geometry = lookupRenderResource(source.geometryLibrary, renderable.geometry);
1311
- if (!geometry) {
1312
- throw new RenderDeviceError("Scene pick target references missing geometry", "SCENE_PICK_GEOMETRY_MISSING", {
1313
- node: node.name,
1314
- geometry: renderable.geometry
1315
- });
1316
- }
1317
- const morphTargets = source.morphTargetLibrary ? lookupRenderResource(source.morphTargetLibrary, renderable.geometry) : undefined;
1318
- if (renderable.morphWeights.length > 0 && !morphTargets) {
1319
- throw new RenderDeviceError("Scene pick target has morph weights but no morph target resource entry", "SCENE_PICK_MORPH_TARGETS_MISSING", {
1320
- node: node.name,
1321
- geometry: renderable.geometry,
1322
- morphWeights: renderable.morphWeights.length
1323
- });
1324
- }
1325
- const bounds = renderableWorldBounds(geometry, node.transform.worldMatrix, renderable.instanceTransforms, morphTargets, renderable.morphWeights, renderable.skinning);
1326
- if (geometry.topology === "points" && options.pointRadius !== undefined) {
1327
- hits.push(...pickPoints(node, geometry, ray, node.transform.worldMatrix, renderable.instanceTransforms, options.pointRadius));
1328
- continue;
1329
- }
1330
- const pickBounds = geometry.topology === "lines" && options.lineRadius !== undefined
1331
- ? expandSceneBounds(bounds, options.lineRadius)
1332
- : bounds;
1333
- const hitPoint = ray.intersectBox(pickBounds.toMathBox());
1334
- if (hitPoint) {
1335
- hits.push({ node, geometry, bounds: pickBounds, distance: hitPoint.distanceTo(ray.origin), hitPoint: hitPoint.toArray() });
1336
- }
1337
- }
1338
- return hits.sort((left, right) => left.distance - right.distance);
1339
- }
1340
-
1341
- function pickPoints(
1342
- node: SceneNode,
1343
- geometry: Geometry,
1344
- ray: Ray,
1345
- modelMatrix: Mat4,
1346
- instanceTransforms: Float32Array | readonly number[] | undefined,
1347
- pointRadius: number
1348
- ): readonly ScenePickHit[] {
1349
- const hits: ScenePickHit[] = [];
1350
- const radiusSquared = pointRadius * pointRadius;
1351
- const transforms = instanceTransforms ? collectInstanceWorldMatrices(modelMatrix, instanceTransforms) : [modelMatrix];
1352
- for (let instanceIndex = 0; instanceIndex < transforms.length; instanceIndex += 1) {
1353
- const transform = transforms[instanceIndex]!;
1354
- for (let pointIndex = 0; pointIndex < geometry.vertexBuffer.vertexCount; pointIndex += 1) {
1355
- const position = geometry.vertexBuffer.getAttribute(pointIndex, "position");
1356
- const worldPoint = transformPoint(transform, [position[0] ?? 0, position[1] ?? 0, position[2] ?? 0]);
1357
- const distance = distanceAlongRay(ray, worldPoint);
1358
- if (distance < 0) continue;
1359
- const radialDistanceSquared = squaredDistanceToRayAt(ray, worldPoint, distance);
1360
- if (radialDistanceSquared <= radiusSquared) {
1361
- hits.push({
1362
- node,
1363
- geometry,
1364
- bounds: expandPointBounds(worldPoint, pointRadius),
1365
- distance,
1366
- hitPoint: worldPoint,
1367
- pointIndex,
1368
- instanceIndex: instanceTransforms ? instanceIndex : undefined
1369
- });
1370
- }
1371
- }
1372
- }
1373
- return hits;
1374
- }
1375
-
1376
- function collectInstanceWorldMatrices(modelMatrix: Mat4, instanceTransforms: Float32Array | readonly number[]): readonly Mat4[] {
1377
- const matrices: Mat4[] = [];
1378
- for (let offset = 0; offset < instanceTransforms.length; offset += 16) {
1379
- matrices.push(multiplyMat4(modelMatrix, toMat4(instanceTransforms.slice(offset, offset + 16), "instanceTransforms")));
1380
- }
1381
- return matrices;
1382
- }
1383
-
1384
- function distanceAlongRay(ray: Ray, point: readonly [number, number, number]): number {
1385
- return (
1386
- (point[0] - ray.origin.x) * ray.direction.x +
1387
- (point[1] - ray.origin.y) * ray.direction.y +
1388
- (point[2] - ray.origin.z) * ray.direction.z
1389
- );
1390
- }
1391
-
1392
- function squaredDistanceToRayAt(ray: Ray, point: readonly [number, number, number], distance: number): number {
1393
- const closestX = ray.origin.x + ray.direction.x * distance;
1394
- const closestY = ray.origin.y + ray.direction.y * distance;
1395
- const closestZ = ray.origin.z + ray.direction.z * distance;
1396
- const dx = point[0] - closestX;
1397
- const dy = point[1] - closestY;
1398
- const dz = point[2] - closestZ;
1399
- return dx * dx + dy * dy + dz * dz;
1400
- }
1401
-
1402
- function expandPointBounds(center: readonly [number, number, number], radius: number): SceneBounds3 {
1403
- return new SceneBounds3(
1404
- [center[0] - radius, center[1] - radius, center[2] - radius],
1405
- [center[0] + radius, center[1] + radius, center[2] + radius]
1406
- );
1407
- }
1408
-
1409
- function expandSceneBounds(bounds: SceneBounds3, radius: number): SceneBounds3 {
1410
- return new SceneBounds3(
1411
- [bounds.min[0] - radius, bounds.min[1] - radius, bounds.min[2] - radius],
1412
- [bounds.max[0] + radius, bounds.max[1] + radius, bounds.max[2] + radius]
1413
- );
1414
- }
1415
-
1416
- class RendererAnimationLoopImpl implements RendererAnimationLoop {
1417
- private requestId: number | null = null;
1418
- public running = false;
1419
-
1420
- constructor(
1421
- private readonly renderer: Renderer,
1422
- private readonly callback: (timeMs: number, renderer: Renderer) => void
1423
- ) {}
1424
-
1425
- start(): void {
1426
- if (typeof requestAnimationFrame !== "function" || typeof cancelAnimationFrame !== "function") {
1427
- throw new RenderDeviceError("Renderer animation loops require requestAnimationFrame", "ANIMATION_LOOP_UNAVAILABLE");
1428
- }
1429
- this.running = true;
1430
- this.requestId = requestAnimationFrame((timeMs) => this.tick(timeMs));
1431
- }
1432
-
1433
- stop(): void {
1434
- this.running = false;
1435
- if (this.requestId !== null) {
1436
- cancelAnimationFrame(this.requestId);
1437
- this.requestId = null;
1438
- }
1439
- }
1440
-
1441
- private tick(timeMs: number): void {
1442
- if (!this.running) {
1443
- return;
1444
- }
1445
- this.callback(timeMs, this.renderer);
1446
- if (this.running) {
1447
- this.requestId = requestAnimationFrame((nextTimeMs) => this.tick(nextTimeMs));
1448
- }
1449
- }
1450
- }
1451
-
1452
- function readCanvasCssSize(canvas: HTMLCanvasElement | OffscreenCanvas, axis: "width" | "height"): number {
1453
- if ("getBoundingClientRect" in canvas) {
1454
- const bounds = canvas.getBoundingClientRect();
1455
- const value = axis === "width" ? bounds.width : bounds.height;
1456
- if (value > 0) {
1457
- return value;
1458
- }
1459
- }
1460
- return axis === "width" ? canvas.width : canvas.height;
1461
- }
1462
-
1463
- function inferInitialCanvasDimension(canvas: HTMLCanvasElement | OffscreenCanvas | undefined, axis: "width" | "height"): number {
1464
- if (!canvas) return 1;
1465
- const cssSize = readCanvasCssSize(canvas, axis);
1466
- const dpr = Number.isFinite(globalThis.devicePixelRatio) && globalThis.devicePixelRatio > 0 ? globalThis.devicePixelRatio : 1;
1467
- const displaySize = Math.round(cssSize * dpr);
1468
- return Math.max(1, displaySize || (axis === "width" ? canvas.width : canvas.height) || 1);
1469
- }
1470
-
1471
- function collectEnvironmentLighting(source: RenderSource | Iterable<RenderItem> | Scene): EnvironmentLightingOptions | undefined {
1472
- if (source instanceof Scene || isIterable(source)) return undefined;
1473
- if (source.environmentLighting === false) return cloneEnvironmentLighting(DISABLED_RENDERER_ENVIRONMENT_LIGHTING);
1474
- if (source.environmentLighting) return source.environmentLighting;
1475
- return cloneEnvironmentLighting(DEFAULT_RENDERER_ENVIRONMENT_LIGHTING);
1476
- }
1477
-
1478
- function collectEnvironmentBackground(source: RenderSource | Iterable<RenderItem> | Scene): EnvironmentBackgroundOptions | undefined {
1479
- if (source instanceof Scene || isIterable(source) || source.environmentBackground === false) return undefined;
1480
- return source.environmentBackground;
1481
- }
1482
-
1483
- function collectEnvironmentFog(source: RenderSource | Iterable<RenderItem> | Scene): ForwardEnvironmentFogOptions | false | undefined {
1484
- if (source instanceof Scene || isIterable(source)) return undefined;
1485
- return source.environmentFog;
1486
- }
1487
-
1488
- function collectForwardShadowMap(source: RenderSource | Iterable<RenderItem> | Scene): ForwardShadowMapOptions | undefined {
1489
- return source instanceof Scene || isIterable(source) ? undefined : source.shadowMap;
1490
- }
1491
-
1492
- function collectRenderTarget(source: RenderSource | Iterable<RenderItem> | Scene): RenderTarget | undefined {
1493
- return source instanceof Scene || isIterable(source) ? undefined : source.renderTarget;
1494
- }
1495
-
1496
- function validateExplicitRenderTarget(target: RenderTarget | undefined, width: number, height: number): void {
1497
- if (!target) return;
1498
- if (target.disposed) {
1499
- throw new RenderDeviceError("Renderer renderTarget must be a live render target.", "RENDER_TARGET_DISPOSED", {
1500
- renderTarget: target.label
1501
- });
1502
- }
1503
- if (target.width !== width || target.height !== height) {
1504
- throw new RenderDeviceError("Renderer renderTarget dimensions must match the renderer viewport.", "RENDER_TARGET_VIEWPORT_MISMATCH", {
1505
- renderTarget: target.label,
1506
- targetWidth: target.width,
1507
- targetHeight: target.height,
1508
- rendererWidth: width,
1509
- rendererHeight: height
1510
- });
1511
- }
1512
- }
1513
-
1514
- function collectRendererShadowOptions(source: RenderSource | Iterable<RenderItem> | Scene): RendererShadowOptions | undefined {
1515
- if (source instanceof Scene || isIterable(source)) return undefined;
1516
- if (source.shadow === true) return {};
1517
- if (!source.shadow) return undefined;
1518
- return source.shadow;
1519
- }
1520
-
1521
- function firstShadowCastingLight(source: RenderSource | Iterable<RenderItem> | Scene, lights: readonly CollectedLight[]): Light | null {
1522
- const explicit = lights.find((light) => light.castsShadow && light.source.visible)?.source;
1523
- if (explicit) return explicit;
1524
- const scene = sceneFromSource(source);
1525
- return scene?.collectLights().find((light) => light.visible && light.castsShadow) ?? null;
1526
- }
1527
-
1528
- function firstShadowCastingCollectedLight(lights: readonly CollectedLight[]): CollectedLight | undefined {
1529
- return lights.find((light) => light.castsShadow && light.source.visible);
1530
- }
1531
-
1532
- function lightDirectionFromLight(light: Light | null): Vec3 {
1533
- if (!light) return [0, -1, -1];
1534
- const direction = light instanceof DirectionalLight ? light.getDirection() : undefined;
1535
- return normalizeVec3([
1536
- Number(direction?.[0] ?? 0),
1537
- Number(direction?.[1] ?? -1),
1538
- Number(direction?.[2] ?? -1)
1539
- ]);
1540
- }
1541
-
1542
- function createRendererOwnedShadowMatrix(light: Light | null, items: readonly RenderItem[], collectedLight: CollectedLight | undefined): Mat4 {
1543
- if (light instanceof SpotLight) {
1544
- light.transform.updateWorld(undefined, true);
1545
- const near = 0.01;
1546
- const far = Math.max(near + 0.01, light.range);
1547
- const projectionMatrix = perspectiveMat4(light.angle * 2, 1, near, far);
1548
- return multiplyMat4(projectionMatrix, light.transform.inverseWorldMatrix);
1549
- }
1550
- return createDirectionalShadowMatrix(items, collectedLight?.direction ?? lightDirectionFromLight(light));
1551
- }
1552
-
1553
- function createPointShadowFaceMatrices(light: PointLight): Float32Array {
1554
- light.transform.updateWorld(undefined, true);
1555
- const position: Vec3 = [
1556
- light.transform.worldMatrix[12],
1557
- light.transform.worldMatrix[13],
1558
- light.transform.worldMatrix[14]
1559
- ];
1560
- const projection = perspectiveMat4(Math.PI / 2, 1, 0.01, Math.max(0.02, light.range));
1561
- const faces: readonly { readonly direction: Vec3; readonly up: Vec3 }[] = [
1562
- { direction: [1, 0, 0], up: [0, -1, 0] },
1563
- { direction: [-1, 0, 0], up: [0, -1, 0] },
1564
- { direction: [0, 1, 0], up: [0, 0, 1] },
1565
- { direction: [0, -1, 0], up: [0, 0, -1] },
1566
- { direction: [0, 0, 1], up: [0, -1, 0] },
1567
- { direction: [0, 0, -1], up: [0, -1, 0] }
1568
- ];
1569
- const matrices = new Float32Array(6 * 16);
1570
- for (const [index, face] of faces.entries()) {
1571
- const view = lookAtMatrix(position, addVec3(position, face.direction), face.up);
1572
- matrices.set(multiplyMat4(projection, view), index * 16);
1573
- }
1574
- return matrices;
1575
- }
1576
-
1577
- function createPointShadowFaceRects(): Float32Array {
1578
- const rects = new Float32Array(6 * 4);
1579
- for (let face = 0; face < 6; face += 1) {
1580
- const column = face % 3;
1581
- const row = Math.floor(face / 3);
1582
- rects.set([column / 3, row / 2, 1 / 3, 1 / 2], face * 4);
1583
- }
1584
- return rects;
1585
- }
1586
-
1587
- function readShadowFacePixels(device: RenderDevice, target: RenderTarget): Uint8Array {
1588
- device.setRenderTarget(target);
1589
- if (target.depthTexture && device.readDepthPixels) {
1590
- const depth = device.readDepthPixels(0, 0, target.width, target.height);
1591
- const pixels = new Uint8Array(target.width * target.height * 4);
1592
- for (let index = 0; index < depth.length; index += 1) {
1593
- const byte = Math.max(0, Math.min(255, Math.round((depth[index] ?? 1) * 255)));
1594
- const offset = index * 4;
1595
- pixels[offset] = byte;
1596
- pixels[offset + 1] = byte;
1597
- pixels[offset + 2] = byte;
1598
- pixels[offset + 3] = 255;
1599
- }
1600
- return pixels;
1601
- }
1602
- return device.readPixels(0, 0, target.width, target.height);
1603
- }
1604
-
1605
- function blitPointShadowFace(
1606
- atlasPixels: Uint8Array,
1607
- atlasWidth: number,
1608
- atlasHeight: number,
1609
- facePixels: Uint8Array,
1610
- faceSize: number,
1611
- faceRects: Float32Array,
1612
- face: number
1613
- ): void {
1614
- const rectOffset = face * 4;
1615
- const destX = Math.round((faceRects[rectOffset] ?? 0) * atlasWidth);
1616
- const destY = Math.round((faceRects[rectOffset + 1] ?? 0) * atlasHeight);
1617
- for (let row = 0; row < faceSize; row += 1) {
1618
- const sourceOffset = row * faceSize * 4;
1619
- const targetOffset = ((destY + row) * atlasWidth + destX) * 4;
1620
- atlasPixels.set(facePixels.subarray(sourceOffset, sourceOffset + faceSize * 4), targetOffset);
1621
- }
1622
- }
1623
-
1624
- function lookAtMatrix(eye: Vec3, target: Vec3, up: Vec3): Mat4 {
1625
- const forward = normalizeVec3(subtractVec3(target, eye));
1626
- const right = normalizeVec3(crossVec3(forward, up));
1627
- const correctedUp = crossVec3(right, forward);
1628
- return [
1629
- right[0], correctedUp[0], -forward[0], 0,
1630
- right[1], correctedUp[1], -forward[1], 0,
1631
- right[2], correctedUp[2], -forward[2], 0,
1632
- -dotVec3(right, eye), -dotVec3(correctedUp, eye), dotVec3(forward, eye), 1
1633
- ];
1634
- }
1635
-
1636
- function createDirectionalShadowMatrix(items: readonly RenderItem[], lightDirection: readonly [number, number, number]): Mat4 {
1637
- const bounds = collectRenderItemBounds(items);
1638
- if (!bounds || bounds.isEmpty()) {
1639
- return identityMat4();
1640
- }
1641
- const basis = directionalLightBasis(lightDirection);
1642
- const points = boundsCorners(bounds).map((point) => projectPointToLightSpace(point, basis));
1643
- const lightBounds = boundsFromVec3(points);
1644
- const width = Math.max(0.01, lightBounds.max[0] - lightBounds.min[0]);
1645
- const height = Math.max(0.01, lightBounds.max[1] - lightBounds.min[1]);
1646
- const depth = Math.max(0.01, lightBounds.max[2] - lightBounds.min[2]);
1647
- const padding = Math.max(width, height, depth) * 0.08 + 0.05;
1648
- const viewMatrix = lightViewMatrix(basis);
1649
- const projectionMatrix = orthographicMat4(
1650
- lightBounds.min[0] - padding,
1651
- lightBounds.max[0] + padding,
1652
- lightBounds.min[1] - padding,
1653
- lightBounds.max[1] + padding,
1654
- lightBounds.min[2] - padding,
1655
- lightBounds.max[2] + padding
1656
- );
1657
- return multiplyMat4(projectionMatrix, viewMatrix);
1658
- }
1659
-
1660
- function directionalLightBasis(lightDirection: readonly [number, number, number]): {
1661
- readonly right: Vec3;
1662
- readonly up: Vec3;
1663
- readonly forward: Vec3;
1664
- } {
1665
- const forward = normalizeVec3(scaleVec3(normalizeVec3(lightDirection), -1));
1666
- const fallbackUp: Vec3 = Math.abs(forward[1]) > 0.94 ? [0, 0, 1] : [0, 1, 0];
1667
- const right = normalizeVec3(crossVec3(fallbackUp, forward));
1668
- const up = normalizeVec3(crossVec3(forward, right));
1669
- return { right, up, forward };
1670
- }
1671
-
1672
- function lightViewMatrix(basis: { readonly right: Vec3; readonly up: Vec3; readonly forward: Vec3 }): Mat4 {
1673
- return [
1674
- basis.right[0], basis.up[0], basis.forward[0], 0,
1675
- basis.right[1], basis.up[1], basis.forward[1], 0,
1676
- basis.right[2], basis.up[2], basis.forward[2], 0,
1677
- 0, 0, 0, 1
1678
- ];
1679
- }
1680
-
1681
- function projectPointToLightSpace(point: Vec3, basis: { readonly right: Vec3; readonly up: Vec3; readonly forward: Vec3 }): Vec3 {
1682
- return [dotVec3(point, basis.right), dotVec3(point, basis.up), dotVec3(point, basis.forward)];
1683
- }
1684
-
1685
- function boundsCorners(bounds: SceneBounds3): readonly Vec3[] {
1686
- return [
1687
- [bounds.min[0], bounds.min[1], bounds.min[2]],
1688
- [bounds.max[0], bounds.min[1], bounds.min[2]],
1689
- [bounds.min[0], bounds.max[1], bounds.min[2]],
1690
- [bounds.min[0], bounds.min[1], bounds.max[2]],
1691
- [bounds.max[0], bounds.max[1], bounds.min[2]],
1692
- [bounds.max[0], bounds.min[1], bounds.max[2]],
1693
- [bounds.min[0], bounds.max[1], bounds.max[2]],
1694
- [bounds.max[0], bounds.max[1], bounds.max[2]]
1695
- ];
1696
- }
1697
-
1698
- function boundsFromVec3(points: readonly Vec3[]): { readonly min: Vec3; readonly max: Vec3 } {
1699
- const min: Vec3 = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
1700
- const max: Vec3 = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
1701
- for (const point of points) {
1702
- for (let axis = 0; axis < 3; axis += 1) {
1703
- min[axis] = Math.min(min[axis]!, point[axis]!);
1704
- max[axis] = Math.max(max[axis]!, point[axis]!);
1705
- }
1706
- }
1707
- return { min, max };
1708
- }
1709
-
1710
- function normalizeVec3(value: readonly [number, number, number]): Vec3 {
1711
- const length = Math.hypot(value[0], value[1], value[2]);
1712
- if (!Number.isFinite(length) || length <= 1e-8) return [0, -1 / Math.SQRT2, -1 / Math.SQRT2];
1713
- return [value[0] / length, value[1] / length, value[2] / length];
1714
- }
1715
-
1716
- function scaleVec3(value: Vec3, amount: number): Vec3 {
1717
- return [value[0] * amount, value[1] * amount, value[2] * amount];
1718
- }
1719
-
1720
- function addVec3(left: Vec3, right: Vec3): Vec3 {
1721
- return [left[0] + right[0], left[1] + right[1], left[2] + right[2]];
1722
- }
1723
-
1724
- function subtractVec3(left: Vec3, right: Vec3): Vec3 {
1725
- return [left[0] - right[0], left[1] - right[1], left[2] - right[2]];
1726
- }
1727
-
1728
- function crossVec3(left: Vec3, right: Vec3): Vec3 {
1729
- return [
1730
- left[1] * right[2] - left[2] * right[1],
1731
- left[2] * right[0] - left[0] * right[2],
1732
- left[0] * right[1] - left[1] * right[0]
1733
- ];
1734
- }
1735
-
1736
- function dotVec3(left: Vec3, right: Vec3): number {
1737
- return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
1738
- }
1739
-
1740
- function collectPostprocess(source: RenderSource | Iterable<RenderItem> | Scene): RendererPostProcessOptions | undefined {
1741
- if (source instanceof Scene || isIterable(source)) return undefined;
1742
- if (source.postprocess === true) return {};
1743
- if (!source.postprocess) return undefined;
1744
- return source.postprocess;
1745
- }
1746
-
1747
- function normalizeRendererInput(
1748
- sourceOrInput: RendererInput | RenderSource | Iterable<RenderItem> | Scene,
1749
- camera?: CameraLike
1750
- ): RendererInput {
1751
- if (isRendererInput(sourceOrInput)) {
1752
- return { source: sourceOrInput.source, camera: camera ?? sourceOrInput.camera };
1753
- }
1754
- return { source: sourceOrInput, ...(camera ? { camera } : {}) };
1755
- }
1756
-
1757
- function isRendererInput(value: RendererInput | RenderSource | Iterable<RenderItem> | Scene): value is RendererInput {
1758
- return !(value instanceof Scene) && !isIterable(value) && isRecord(value) && "source" in value;
1759
- }
1760
-
1761
- function isRecord(value: unknown): value is Record<string, unknown> {
1762
- return typeof value === "object" && value !== null;
1763
- }
1764
-
1765
- function collectCameraPolicy(source: RenderSource | Iterable<RenderItem> | Scene): RendererCameraPolicy {
1766
- if (source instanceof Scene || isIterable(source)) return "identity";
1767
- if (source.cameraPolicy) return source.cameraPolicy;
1768
- if (source.scene && !source.renderItems && !source.collectRenderItems) return "auto-frame";
1769
- if ((source.renderItems || source.collectRenderItems) && !source.renderTarget) return "auto-frame";
1770
- return "identity";
1771
- }
1772
-
1773
- function postprocessRequiresDepthTexture(postprocess: RendererPostProcessOptions): boolean {
1774
- return Boolean(
1775
- (postprocess.depthOfField && !postprocess.depthOfField.depth) ||
1776
- (postprocess.contactShadow && !postprocess.contactShadow.depth) ||
1777
- (postprocess.ssao && !postprocess.ssao.depth) ||
1778
- (postprocess.ssr && !postprocess.ssr.depth)
1779
- );
1780
- }
1781
-
1782
- function canFuseLdrPostprocess(source: RenderTarget, passes: readonly RendererPostProcessPassPlan[]): boolean {
1783
- const sourceIsHdr = isHdrRenderTarget(source);
1784
- return passes.length > 1
1785
- && (!sourceIsHdr || passes[0]?.name === "tone-mapping")
1786
- && passes.every((pass) => pass.name === "tone-mapping" || pass.name === "color-grade" || pass.name === "fxaa")
1787
- && passes.every((pass, index) => {
1788
- const previousRank = index === 0 ? -1 : ldrFusionPassRank(passes[index - 1]!.name);
1789
- return ldrFusionPassRank(pass.name) >= previousRank;
1790
- });
1791
- }
1792
-
1793
- function ldrFusionPassRank(name: RendererPostProcessPassName): number {
1794
- if (name === "tone-mapping") return 0;
1795
- if (name === "color-grade") return 1;
1796
- if (name === "fxaa") return 2;
1797
- return Number.POSITIVE_INFINITY;
1798
- }
1799
-
1800
- function isDepthPostprocessPass(name: RendererPostProcessPassName): name is "depth-of-field" | "contact-shadow" | "ssao" | "ssr" {
1801
- return name === "depth-of-field" || name === "contact-shadow" || name === "ssao" || name === "ssr";
1802
- }
1803
-
1804
- function postprocessPassHasDepth(options: RendererPostProcessPassPlan["options"]): boolean {
1805
- return typeof options === "object" && options !== null && "depth" in options && Boolean((options as { readonly depth?: unknown }).depth);
1806
- }
1807
-
1808
- function withRendererDepth<T extends DepthOfFieldOptions | ContactShadowPostProcessOptions | SSAOOptions | SSROptions>(options: T, depth: DepthTextureBinding | undefined): T {
1809
- return depth && !options.depth ? { ...options, depth } : options;
1810
- }
1811
-
1812
- function defaultPostprocessTargetFormat(
1813
- device: RenderDevice,
1814
- postprocess: RendererPostProcessOptions
1815
- ): Extract<RenderTargetDescriptor["format"], "rgba8" | "rgba16f" | "rgba32f"> {
1816
- if (postprocess.toneMapping === false) return "rgba8";
1817
- return device.info.capabilities?.includes("hdr-render-targets") ? "rgba16f" : "rgba8";
1818
- }
1819
-
1820
- function isHdrRenderTarget(target: RenderTarget): boolean {
1821
- return target.colorTexture.format === "rgba16f" || target.colorTexture.format === "rgba32f";
1822
- }
1823
-
1824
- function collectSourceCameraPosition(source: RenderSource | Iterable<RenderItem> | Scene): readonly [number, number, number] | undefined {
1825
- if (source instanceof Scene || isIterable(source)) return undefined;
1826
- const position = source.cameraPosition;
1827
- if (position === undefined) return undefined;
1828
- if (position.length !== 3 || position.some((value) => !Number.isFinite(value))) {
1829
- throw new RenderDeviceError("RenderSource cameraPosition must contain three finite numbers", "CAMERA_POSITION_INVALID", {
1830
- cameraPosition: [...position]
1831
- });
1832
- }
1833
- return position;
1834
- }
1835
-
1836
- function collectRenderItems(
1837
- source: RenderSource | Iterable<RenderItem> | Scene,
1838
- cameraViewProjection?: Mat4,
1839
- camera?: Camera
1840
- ): readonly RenderItem[] {
1841
- return collectRenderItemsWithDiagnostics(source, cameraViewProjection, camera).items;
1842
- }
1843
-
1844
- function collectRenderItemsWithDiagnostics(
1845
- source: RenderSource | Iterable<RenderItem> | Scene,
1846
- cameraViewProjection?: Mat4,
1847
- camera?: Camera
1848
- ): { readonly items: readonly RenderItem[]; readonly diagnostics: RenderCollectionDiagnostics } {
1849
- const diagnostics = createRenderCollectionDiagnostics();
1850
- if (isIterable(source)) {
1851
- const items = applyViewProjection([...source], cameraViewProjection);
1852
- diagnostics.submittedObjects += items.length;
1853
- diagnostics.visibleObjects += items.length;
1854
- return { items, diagnostics };
1855
- }
1856
- if (source instanceof Scene) {
1857
- const items = collectSceneRenderItems(source, {}, cameraViewProjection, camera, diagnostics);
1858
- return { items, diagnostics };
1859
- }
1860
- const items: RenderItem[] = [];
1861
- if (source.scene) {
1862
- items.push(...collectSceneRenderItems(source.scene, source, cameraViewProjection, source.frustumCulling === false ? undefined : camera, diagnostics));
1863
- }
1864
- const explicitItems: RenderItem[] = [];
1865
- if (source.collectRenderItems) {
1866
- explicitItems.push(...source.collectRenderItems());
1867
- }
1868
- if (source.renderItems) {
1869
- explicitItems.push(...source.renderItems);
1870
- }
1871
- if (explicitItems.length > 0) {
1872
- const optimizedItems = applyRendererOwnedStaticBatching(source, explicitItems);
1873
- const projectedItems = applyViewProjection(optimizedItems, cameraViewProjection);
1874
- diagnostics.submittedObjects += explicitItems.length;
1875
- diagnostics.visibleObjects += explicitItems.length;
1876
- items.push(...projectedItems);
1877
- }
1878
- return { items, diagnostics };
1879
- }
1880
-
1881
- const staticBatchGeometryIds = new WeakMap<Geometry, number>();
1882
- const staticBatchMaterialIds = new WeakMap<object, number>();
1883
- let nextStaticBatchResourceId = 1;
1884
-
1885
- function applyRendererOwnedStaticBatching(source: RenderSource, items: readonly RenderItem[]): readonly RenderItem[] {
1886
- if (!source.staticBatching) return items;
1887
- const batchable: StaticBatchInput[] = [];
1888
- const passthrough: RenderItem[] = [];
1889
- for (const item of items) {
1890
- if (isStaticBatchCandidate(item)) {
1891
- batchable.push({
1892
- geometry: item.geometry,
1893
- material: item.material,
1894
- modelMatrix: item.modelMatrix ?? identityMat4(),
1895
- batchKey: staticBatchKey(item),
1896
- label: item.label
1897
- });
1898
- } else {
1899
- passthrough.push(item);
1900
- }
1901
- }
1902
- if (batchable.length === 0) return items;
1903
- const options = source.staticBatching === true ? {} : source.staticBatching;
1904
- const batched = batchStaticRenderItems(batchable, {
1905
- labelPrefix: "renderer-static-batch",
1906
- ...options
1907
- });
1908
- return [...passthrough, ...batched.renderItems];
1909
- }
1910
-
1911
- function isStaticBatchCandidate(item: RenderItem): item is RenderItem & { readonly material: RenderMaterial } {
1912
- return item.material !== undefined &&
1913
- item.drawRange === undefined &&
1914
- item.skinning === undefined &&
1915
- item.morphTargets === undefined &&
1916
- item.morphWeights === undefined &&
1917
- item.instanceTransforms === undefined &&
1918
- item.instanceColors === undefined &&
1919
- item.instanceAttributes === undefined;
1920
- }
1921
-
1922
- function staticBatchKey(item: RenderItem & { readonly material: RenderMaterial }): string {
1923
- return `${resourceId(staticBatchGeometryIds, item.geometry)}:${resourceId(staticBatchMaterialIds, item.material)}`;
1924
- }
1925
-
1926
- function resourceId<T extends object>(ids: WeakMap<T, number>, resource: T): number {
1927
- const existing = ids.get(resource);
1928
- if (existing !== undefined) return existing;
1929
- const next = nextStaticBatchResourceId++;
1930
- ids.set(resource, next);
1931
- return next;
1932
- }
1933
-
1934
- function collectSceneRenderItems(
1935
- scene: Scene,
1936
- source: Pick<RenderSource, "geometryLibrary" | "materialLibrary" | "morphTargetLibrary">,
1937
- cameraViewProjection?: Mat4,
1938
- camera?: Camera,
1939
- diagnostics?: RenderCollectionDiagnostics
1940
- ): readonly RenderItem[] {
1941
- scene.updateWorldTransforms();
1942
- const renderables = scene.collectRenderables();
1943
- if (renderables.length === 0) {
1944
- return [];
1945
- }
1946
- if (!source.geometryLibrary || !source.materialLibrary) {
1947
- throw new RenderDeviceError("Scene rendering requires geometryLibrary and materialLibrary resource lookups", "SCENE_RENDER_RESOURCES_MISSING", {
1948
- renderables: renderables.length
1949
- });
1950
- }
1951
- const items: RenderItem[] = [];
1952
- for (const { node, renderable } of renderables) {
1953
- const geometry = lookupRenderResource(source.geometryLibrary!, renderable.geometry);
1954
- const material = lookupRenderResource(source.materialLibrary!, renderable.material);
1955
- if (!geometry) {
1956
- throw new RenderDeviceError("Scene renderable references missing geometry", "SCENE_GEOMETRY_MISSING", {
1957
- node: node.name,
1958
- geometry: renderable.geometry
1959
- });
1960
- }
1961
- if (!material) {
1962
- throw new RenderDeviceError("Scene renderable references missing material", "SCENE_MATERIAL_MISSING", {
1963
- node: node.name,
1964
- material: renderable.material
1965
- });
1966
- }
1967
- const morphTargets = source.morphTargetLibrary ? lookupRenderResource(source.morphTargetLibrary, renderable.geometry) : undefined;
1968
- if (renderable.morphWeights.length > 0 && !morphTargets) {
1969
- throw new RenderDeviceError("Scene renderable has morph weights but no morph target resource entry", "SCENE_MORPH_TARGETS_MISSING", {
1970
- node: node.name,
1971
- geometry: renderable.geometry,
1972
- morphWeights: renderable.morphWeights.length
1973
- });
1974
- }
1975
- const modelMatrix = node.transform.worldMatrix;
1976
- const bounds = renderableWorldBounds(geometry, modelMatrix, renderable.instanceTransforms, morphTargets, renderable.morphWeights, renderable.skinning);
1977
- if (diagnostics) {
1978
- diagnostics.submittedObjects += 1;
1979
- if (camera) diagnostics.frustumTestedObjects += 1;
1980
- }
1981
- if (camera && !camera.frustum.intersectsBox(bounds.toMathBox())) {
1982
- if (diagnostics) diagnostics.culledObjects += 1;
1983
- continue;
1984
- }
1985
- if (diagnostics) diagnostics.visibleObjects += 1;
1986
- items.push({
1987
- geometry,
1988
- material,
1989
- label: node.name,
1990
- modelMatrix,
1991
- normalMatrix: normalMatrixFromModel(modelMatrix),
1992
- modelViewProjectionMatrix: multiplyMat4(cameraViewProjection ?? identityMat4(), modelMatrix),
1993
- ...(renderable.skinning ? { skinning: renderable.skinning } : {}),
1994
- ...(renderable.instanceTransforms ? { instanceTransforms: renderable.instanceTransforms } : {}),
1995
- ...(renderable.instanceColors ? { instanceColors: renderable.instanceColors } : {}),
1996
- ...(morphTargets && renderable.morphWeights.length > 0 ? { morphTargets, morphWeights: renderable.morphWeights } : {})
1997
- });
1998
- }
1999
- return items;
2000
- }
2001
-
2002
- function createRenderCollectionDiagnostics(): RenderCollectionDiagnostics {
2003
- return {
2004
- submittedObjects: 0,
2005
- visibleObjects: 0,
2006
- culledObjects: 0,
2007
- frustumTestedObjects: 0
2008
- };
2009
- }
2010
-
2011
- function createPostprocessDiagnostics(
2012
- postprocess: RendererPostProcessOptions | undefined,
2013
- ownedTargets: readonly RenderTarget[],
2014
- width: number,
2015
- height: number,
2016
- context: {
2017
- readonly targetFormat?: RendererPostprocessTargetFormat;
2018
- readonly nativeLdrPostprocess?: boolean;
2019
- readonly rendererDepthAvailable?: boolean;
2020
- } = {}
2021
- ): RendererPostprocessDiagnostics | undefined {
2022
- if (!postprocess) return undefined;
2023
- const passes = createRendererPostprocessPasses(postprocess);
2024
- const targetFormat = context.targetFormat ?? postprocess.targetFormat ?? "rgba8";
2025
- return {
2026
- postprocessPasses: passes.length,
2027
- postprocessPassNames: passes.map((pass) => pass.name),
2028
- postprocessTargetFormat: targetFormat,
2029
- postprocessRenderTargets: ownedTargets.length,
2030
- postprocessTextures: ownedTargets.reduce((total, target) => total + 1 + (target.depthTexture ? 1 : 0), 0),
2031
- postprocessTargetWidth: width,
2032
- postprocessTargetHeight: height,
2033
- postprocessPlan: createRendererPostprocessPlanDiagnostics(postprocess, {
2034
- sourceTargetFormat: targetFormat,
2035
- targetFormat,
2036
- nativeLdrPostprocess: context.nativeLdrPostprocess,
2037
- rendererDepthAvailable: context.rendererDepthAvailable
2038
- })
2039
- };
2040
- }
2041
-
2042
- const CAPTURE_NON_BLANK_LUMINANCE_THRESHOLD = 1 / 255;
2043
- const CAPTURE_UNDEREXPOSED_LUMINANCE_THRESHOLD = 5 / 255;
2044
- const CAPTURE_OVEREXPOSED_LUMINANCE_THRESHOLD = 250 / 255;
2045
- const CAPTURE_CLIPPED_CHANNEL_THRESHOLD = 254;
2046
-
2047
- function createRendererFrameCaptureMetadata(
2048
- device: RenderDevice,
2049
- width: number,
2050
- height: number,
2051
- pixels: Uint8Array,
2052
- diagnostics: RenderDeviceDiagnostics
2053
- ): RendererFrameCaptureMetadata {
2054
- const pixelDigest = createRendererFramePixelDigest(pixels);
2055
- return {
2056
- capturedAt: new Date().toISOString(),
2057
- renderSize: {
2058
- width,
2059
- height,
2060
- pixelCount: width * height,
2061
- byteLength: pixels.byteLength,
2062
- aspectRatio: roundCaptureMetric(height > 0 ? width / height : 0)
2063
- },
2064
- pixelHash: pixelDigest.value,
2065
- pixelDigest,
2066
- pixelStats: createRendererFramePixelStats(pixels, width, height),
2067
- diagnosticsSummary: createRendererFrameDiagnosticsSummary(device, diagnostics)
2068
- };
2069
- }
2070
-
2071
- function createRendererFramePixelDigest(pixels: Uint8Array): RendererFrameCapturePixelDigest {
2072
- return {
2073
- algorithm: "sha256",
2074
- encoding: "hex",
2075
- source: "rgba8",
2076
- value: sha256Hex(pixels),
2077
- byteLength: pixels.byteLength
2078
- };
2079
- }
2080
-
2081
- function createRendererFramePixelStats(pixels: Uint8Array, width: number, height: number): RendererFrameCapturePixelStats {
2082
- const totalPixels = Math.max(0, width * height);
2083
- const divisor = totalPixels > 0 ? totalPixels : 1;
2084
- let opaquePixels = 0;
2085
- let transparentPixels = 0;
2086
- let nonBlankPixels = 0;
2087
- let underexposedPixels = 0;
2088
- let overexposedPixels = 0;
2089
- let clippedChannelPixels = 0;
2090
- let luminanceSum = 0;
2091
- let alphaSum = 0;
2092
- let redSum = 0;
2093
- let greenSum = 0;
2094
- let blueSum = 0;
2095
- let minLuminance = totalPixels > 0 ? Number.POSITIVE_INFINITY : 0;
2096
- let maxLuminance = 0;
2097
-
2098
- for (let pixel = 0; pixel < totalPixels; pixel += 1) {
2099
- const offset = pixel * 4;
2100
- const red = pixels[offset] ?? 0;
2101
- const green = pixels[offset + 1] ?? 0;
2102
- const blue = pixels[offset + 2] ?? 0;
2103
- const alpha = pixels[offset + 3] ?? 0;
2104
- const luminance = ((0.2126 * red) + (0.7152 * green) + (0.0722 * blue)) / 255;
2105
-
2106
- redSum += red;
2107
- greenSum += green;
2108
- blueSum += blue;
2109
- alphaSum += alpha;
2110
- luminanceSum += luminance;
2111
- minLuminance = Math.min(minLuminance, luminance);
2112
- maxLuminance = Math.max(maxLuminance, luminance);
2113
-
2114
- if (alpha === 255) opaquePixels += 1;
2115
- if (alpha === 0) transparentPixels += 1;
2116
- if (alpha > 0 && luminance > CAPTURE_NON_BLANK_LUMINANCE_THRESHOLD) nonBlankPixels += 1;
2117
- if (alpha > 0 && luminance <= CAPTURE_UNDEREXPOSED_LUMINANCE_THRESHOLD) underexposedPixels += 1;
2118
- if (alpha > 0 && luminance >= CAPTURE_OVEREXPOSED_LUMINANCE_THRESHOLD) overexposedPixels += 1;
2119
- if (
2120
- alpha > 0 &&
2121
- (red >= CAPTURE_CLIPPED_CHANNEL_THRESHOLD ||
2122
- green >= CAPTURE_CLIPPED_CHANNEL_THRESHOLD ||
2123
- blue >= CAPTURE_CLIPPED_CHANNEL_THRESHOLD)
2124
- ) {
2125
- clippedChannelPixels += 1;
2126
- }
2127
- }
2128
-
2129
- return {
2130
- totalPixels,
2131
- opaquePixels,
2132
- opaqueRatio: captureRatio(opaquePixels, totalPixels),
2133
- transparentPixels,
2134
- transparentRatio: captureRatio(transparentPixels, totalPixels),
2135
- nonBlankPixels,
2136
- nonBlankRatio: captureRatio(nonBlankPixels, totalPixels),
2137
- underexposedPixels,
2138
- underexposedRatio: captureRatio(underexposedPixels, totalPixels),
2139
- overexposedPixels,
2140
- overexposedRatio: captureRatio(overexposedPixels, totalPixels),
2141
- clippedChannelPixels,
2142
- clippedChannelRatio: captureRatio(clippedChannelPixels, totalPixels),
2143
- averageLuminance: roundCaptureMetric(luminanceSum / divisor),
2144
- minLuminance: roundCaptureMetric(minLuminance),
2145
- maxLuminance: roundCaptureMetric(maxLuminance),
2146
- averageAlpha: roundCaptureMetric((alphaSum / divisor) / 255),
2147
- averageRgb: [
2148
- roundCaptureMetric((redSum / divisor) / 255),
2149
- roundCaptureMetric((greenSum / divisor) / 255),
2150
- roundCaptureMetric((blueSum / divisor) / 255)
2151
- ],
2152
- thresholds: {
2153
- nonBlankLuminance: roundCaptureMetric(CAPTURE_NON_BLANK_LUMINANCE_THRESHOLD),
2154
- underexposedLuminance: roundCaptureMetric(CAPTURE_UNDEREXPOSED_LUMINANCE_THRESHOLD),
2155
- overexposedLuminance: roundCaptureMetric(CAPTURE_OVEREXPOSED_LUMINANCE_THRESHOLD),
2156
- clippedChannel: roundCaptureMetric(CAPTURE_CLIPPED_CHANNEL_THRESHOLD / 255)
2157
- }
2158
- };
2159
- }
2160
-
2161
- function createRendererFrameDiagnosticsSummary(device: RenderDevice, diagnostics: RenderDeviceDiagnostics): RendererFrameCaptureDiagnosticsSummary {
2162
- const resources = {
2163
- buffers: diagnostics.buffers,
2164
- shaders: diagnostics.shaders,
2165
- ...(diagnostics.renderTargets !== undefined ? { renderTargets: diagnostics.renderTargets } : {}),
2166
- ...(diagnostics.textures !== undefined ? { textures: diagnostics.textures } : {})
2167
- };
2168
- const memoryBytes = {
2169
- ...(diagnostics.bufferBytes !== undefined ? { buffers: diagnostics.bufferBytes } : {}),
2170
- ...(diagnostics.textureBytes !== undefined ? { textures: diagnostics.textureBytes } : {}),
2171
- ...(diagnostics.approximateGpuMemoryBytes !== undefined ? { approximateGpu: diagnostics.approximateGpuMemoryBytes } : {})
2172
- };
2173
- const scene = {
2174
- ...(diagnostics.submittedObjects !== undefined ? { submittedObjects: diagnostics.submittedObjects } : {}),
2175
- ...(diagnostics.visibleObjects !== undefined ? { visibleObjects: diagnostics.visibleObjects } : {}),
2176
- ...(diagnostics.culledObjects !== undefined ? { culledObjects: diagnostics.culledObjects } : {}),
2177
- ...(diagnostics.frustumTestedObjects !== undefined ? { frustumTestedObjects: diagnostics.frustumTestedObjects } : {})
2178
- };
2179
- const postprocess = {
2180
- ...(diagnostics.postprocessPasses !== undefined ? { passes: diagnostics.postprocessPasses } : {}),
2181
- ...(diagnostics.postprocessPassNames !== undefined ? { passNames: diagnostics.postprocessPassNames } : {}),
2182
- ...(diagnostics.postprocessTargetFormat !== undefined ? { targetFormat: diagnostics.postprocessTargetFormat } : {}),
2183
- ...(diagnostics.postprocessRenderTargets !== undefined ? { renderTargets: diagnostics.postprocessRenderTargets } : {}),
2184
- ...(diagnostics.postprocessTextures !== undefined ? { textures: diagnostics.postprocessTextures } : {}),
2185
- ...(diagnostics.postprocessTargetWidth !== undefined ? { targetWidth: diagnostics.postprocessTargetWidth } : {}),
2186
- ...(diagnostics.postprocessTargetHeight !== undefined ? { targetHeight: diagnostics.postprocessTargetHeight } : {})
2187
- };
2188
-
2189
- return {
2190
- backend: device.kind,
2191
- renderer: device.info.renderer,
2192
- vendor: device.info.vendor,
2193
- capabilities: device.info.capabilities ?? [],
2194
- limitations: device.info.limitations ?? [],
2195
- drawCalls: diagnostics.drawCalls,
2196
- resources,
2197
- ...(Object.keys(memoryBytes).length > 0 ? { memoryBytes } : {}),
2198
- ...(Object.keys(scene).length > 0 ? { scene } : {}),
2199
- ...(Object.keys(postprocess).length > 0 ? { postprocess } : {}),
2200
- contextLost: diagnostics.contextLost,
2201
- lastError: diagnostics.lastError
2202
- };
2203
- }
2204
-
2205
- function captureRatio(count: number, total: number): number {
2206
- return roundCaptureMetric(total > 0 ? count / total : 0);
2207
- }
2208
-
2209
- function roundCaptureMetric(value: number): number {
2210
- if (!Number.isFinite(value)) return 0;
2211
- return Math.round(value * 1000000) / 1000000;
2212
- }
2213
-
2214
- const SHA256_INITIAL_HASH = [
2215
- 0x6a09e667,
2216
- 0xbb67ae85,
2217
- 0x3c6ef372,
2218
- 0xa54ff53a,
2219
- 0x510e527f,
2220
- 0x9b05688c,
2221
- 0x1f83d9ab,
2222
- 0x5be0cd19
2223
- ] as const;
2224
-
2225
- const SHA256_ROUND_CONSTANTS = [
2226
- 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
2227
- 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
2228
- 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
2229
- 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
2230
- 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
2231
- 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
2232
- 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
2233
- 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
2234
- ] as const;
2235
-
2236
- function sha256Hex(bytes: Uint8Array): string {
2237
- const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
2238
- const bitLengthHigh = Math.floor(bytes.length / 0x20000000);
2239
- const bitLengthLow = (bytes.length % 0x20000000) * 8;
2240
- const hash: number[] = [...SHA256_INITIAL_HASH];
2241
- const words = new Uint32Array(64);
2242
-
2243
- for (let block = 0; block < paddedLength; block += 64) {
2244
- for (let index = 0; index < 16; index += 1) {
2245
- const offset = block + index * 4;
2246
- words[index] = (
2247
- (sha256PaddedByte(bytes, offset, paddedLength, bitLengthHigh, bitLengthLow) << 24) |
2248
- (sha256PaddedByte(bytes, offset + 1, paddedLength, bitLengthHigh, bitLengthLow) << 16) |
2249
- (sha256PaddedByte(bytes, offset + 2, paddedLength, bitLengthHigh, bitLengthLow) << 8) |
2250
- sha256PaddedByte(bytes, offset + 3, paddedLength, bitLengthHigh, bitLengthLow)
2251
- ) >>> 0;
2252
- }
2253
-
2254
- for (let index = 16; index < 64; index += 1) {
2255
- const first = words[index - 15]!;
2256
- const second = words[index - 2]!;
2257
- const sigma0 = rightRotate32(first, 7) ^ rightRotate32(first, 18) ^ (first >>> 3);
2258
- const sigma1 = rightRotate32(second, 17) ^ rightRotate32(second, 19) ^ (second >>> 10);
2259
- words[index] = (words[index - 16]! + sigma0 + words[index - 7]! + sigma1) >>> 0;
2260
- }
2261
-
2262
- let a = hash[0]!;
2263
- let b = hash[1]!;
2264
- let c = hash[2]!;
2265
- let d = hash[3]!;
2266
- let e = hash[4]!;
2267
- let f = hash[5]!;
2268
- let g = hash[6]!;
2269
- let h = hash[7]!;
2270
-
2271
- for (let index = 0; index < 64; index += 1) {
2272
- const sigma1 = rightRotate32(e, 6) ^ rightRotate32(e, 11) ^ rightRotate32(e, 25);
2273
- const choice = (e & f) ^ (~e & g);
2274
- const temp1 = (h + sigma1 + choice + SHA256_ROUND_CONSTANTS[index]! + words[index]!) >>> 0;
2275
- const sigma0 = rightRotate32(a, 2) ^ rightRotate32(a, 13) ^ rightRotate32(a, 22);
2276
- const majority = (a & b) ^ (a & c) ^ (b & c);
2277
- const temp2 = (sigma0 + majority) >>> 0;
2278
-
2279
- h = g;
2280
- g = f;
2281
- f = e;
2282
- e = (d + temp1) >>> 0;
2283
- d = c;
2284
- c = b;
2285
- b = a;
2286
- a = (temp1 + temp2) >>> 0;
2287
- }
2288
-
2289
- hash[0] = (hash[0]! + a) >>> 0;
2290
- hash[1] = (hash[1]! + b) >>> 0;
2291
- hash[2] = (hash[2]! + c) >>> 0;
2292
- hash[3] = (hash[3]! + d) >>> 0;
2293
- hash[4] = (hash[4]! + e) >>> 0;
2294
- hash[5] = (hash[5]! + f) >>> 0;
2295
- hash[6] = (hash[6]! + g) >>> 0;
2296
- hash[7] = (hash[7]! + h) >>> 0;
2297
- }
2298
-
2299
- return hash.map((word) => word.toString(16).padStart(8, "0")).join("");
2300
- }
2301
-
2302
- function sha256PaddedByte(bytes: Uint8Array, index: number, paddedLength: number, bitLengthHigh: number, bitLengthLow: number): number {
2303
- if (index < bytes.length) return bytes[index]!;
2304
- if (index === bytes.length) return 0x80;
2305
- const lengthOffset = index - (paddedLength - 8);
2306
- if (lengthOffset < 0) return 0;
2307
- if (lengthOffset < 4) return (bitLengthHigh >>> ((3 - lengthOffset) * 8)) & 0xff;
2308
- return (bitLengthLow >>> ((7 - lengthOffset) * 8)) & 0xff;
2309
- }
2310
-
2311
- function rightRotate32(value: number, bits: number): number {
2312
- return ((value >>> bits) | (value << (32 - bits))) >>> 0;
2313
- }
2314
-
2315
- function withRendererFrameDiagnostics(
2316
- diagnostics: RenderDeviceDiagnostics,
2317
- collection: RenderCollectionDiagnostics,
2318
- postprocess?: RendererPostprocessDiagnostics
2319
- ): RenderDeviceDiagnostics {
2320
- return {
2321
- ...diagnostics,
2322
- submittedObjects: collection.submittedObjects,
2323
- visibleObjects: collection.visibleObjects,
2324
- culledObjects: collection.culledObjects,
2325
- frustumTestedObjects: collection.frustumTestedObjects,
2326
- ...(postprocess ?? {})
2327
- };
2328
- }
2329
-
2330
- function renderableWorldBounds(
2331
- geometry: Geometry,
2332
- modelMatrix: Mat4,
2333
- instanceTransforms?: Float32Array | readonly number[],
2334
- morphTargets?: readonly MorphTargetDelta[],
2335
- morphWeights?: readonly number[],
2336
- skinning?: SkinningPaletteBinding
2337
- ): SceneBounds3 {
2338
- const envelope = computeSkinnedMorphTargetWeightedBounds(geometry, skinning, morphTargets, morphWeights);
2339
- const local = new SceneBounds3(
2340
- [envelope.min[0], envelope.min[1], envelope.min[2]],
2341
- [envelope.max[0], envelope.max[1], envelope.max[2]]
2342
- );
2343
- if (!instanceTransforms) {
2344
- return local.transform(modelMatrix);
2345
- }
2346
-
2347
- let bounds = new SceneBounds3();
2348
- for (let offset = 0; offset < instanceTransforms.length; offset += 16) {
2349
- const instanceMatrix = toMat4(instanceTransforms.slice(offset, offset + 16), "instanceTransforms");
2350
- bounds = bounds.union(local.transform(multiplyMat4(modelMatrix, instanceMatrix)));
2351
- }
2352
- return bounds;
2353
- }
2354
-
2355
- function applyViewProjection(items: readonly RenderItem[], cameraViewProjection?: Mat4): readonly RenderItem[] {
2356
- const hasCameraViewProjection = cameraViewProjection !== undefined;
2357
- const viewProjection = cameraViewProjection ?? identityMat4();
2358
- return items.map((item) => {
2359
- const modelMatrix = toMat4(item.modelMatrix ?? identityMat4(), "modelMatrix", item.label);
2360
- const explicitModelViewProjection = item.modelViewProjectionMatrix
2361
- ? toMat4(item.modelViewProjectionMatrix, "modelViewProjectionMatrix", item.label)
2362
- : undefined;
2363
- return {
2364
- ...item,
2365
- modelMatrix,
2366
- normalMatrix: item.normalMatrix ? toMat4(item.normalMatrix, "normalMatrix", item.label) : normalMatrixFromModel(modelMatrix),
2367
- modelViewProjectionMatrix: hasCameraViewProjection || !explicitModelViewProjection
2368
- ? multiplyMat4(viewProjection, modelMatrix)
2369
- : explicitModelViewProjection
2370
- };
2371
- });
2372
- }
2373
-
2374
- function createAutoFrameCamera(
2375
- source: RenderSource | Iterable<RenderItem> | Scene,
2376
- items: readonly RenderItem[],
2377
- width: number,
2378
- height: number
2379
- ): { readonly viewProjectionMatrix: Mat4; readonly cameraPosition: readonly [number, number, number] } | undefined {
2380
- const bounds = collectCameraFrameBounds(source) ?? collectRenderItemBounds(items);
2381
- if (!bounds || bounds.isEmpty()) {
2382
- return undefined;
2383
- }
2384
- const frame = computePerspectiveCameraFrame(bounds, { width, height }, {
2385
- ...DEFAULT_RENDERER_AUTO_FRAME_OPTIONS,
2386
- ...collectCameraFrameOptions(source)
2387
- });
2388
- return {
2389
- viewProjectionMatrix: frame.viewProjectionMatrix,
2390
- cameraPosition: frame.cameraPosition
2391
- };
2392
- }
2393
-
2394
- function collectRenderItemBounds(items: readonly RenderItem[]): SceneBounds3 | undefined {
2395
- let bounds: SceneBounds3 | undefined;
2396
- for (const item of items) {
2397
- if (item.includeInAutoFrame === false) continue;
2398
- const itemBounds = renderableWorldBounds(item.geometry, toMat4(item.modelMatrix ?? identityMat4(), "modelMatrix", item.label), item.instanceTransforms, item.morphTargets, item.morphWeights, item.skinning);
2399
- bounds = bounds ? bounds.union(itemBounds) : itemBounds;
2400
- }
2401
- return bounds;
2402
- }
2403
-
2404
- function collectCameraFrameOptions(source: RenderSource | Iterable<RenderItem> | Scene): PerspectiveCameraFrameOptions {
2405
- if (source instanceof Scene || isIterable(source) || !source.cameraFrameOptions) return {};
2406
- return source.cameraFrameOptions;
2407
- }
2408
-
2409
- function collectCameraFrameBounds(source: RenderSource | Iterable<RenderItem> | Scene): SceneBounds3 | undefined {
2410
- if (source instanceof Scene || isIterable(source) || !source.cameraFrameBounds) return undefined;
2411
- const bounds = source.cameraFrameBounds;
2412
- const min = bounds.min;
2413
- const max = bounds.max;
2414
- const values = [...min, ...max];
2415
- if (values.length !== 6 || values.some((value) => !Number.isFinite(value))) {
2416
- throw new RenderDeviceError("RenderSource cameraFrameBounds must contain finite min/max vectors.", "CAMERA_FRAME_BOUNDS_INVALID", {
2417
- min: [...min],
2418
- max: [...max]
2419
- });
2420
- }
2421
- if (max[0] < min[0] || max[1] < min[1] || max[2] < min[2]) {
2422
- throw new RenderDeviceError("RenderSource cameraFrameBounds max must be greater than or equal to min.", "CAMERA_FRAME_BOUNDS_INVALID", {
2423
- min: [...min],
2424
- max: [...max]
2425
- });
2426
- }
2427
- return new SceneBounds3([min[0], min[1], min[2]], [max[0], max[1], max[2]]);
2428
- }
2429
-
2430
- function resolveCamera(
2431
- source: RenderSource | Iterable<RenderItem> | Scene,
2432
- camera?: CameraLike,
2433
- viewport?: { readonly width: number; readonly height: number },
2434
- options: { readonly allowSceneCamera?: boolean } = {}
2435
- ): { readonly viewProjectionMatrix: Mat4; readonly camera?: Camera; readonly cameraPosition?: readonly [number, number, number] } | undefined {
2436
- const resolved = camera ?? (options.allowSceneCamera === false ? undefined : resolveSceneCamera(source));
2437
- if (!resolved) {
2438
- return undefined;
2439
- }
2440
- const resize = maybeCameraResize(resolved);
2441
- if (viewport && resize) {
2442
- resize(viewport.width, viewport.height);
2443
- }
2444
- resolved.updateCameraMatrices?.();
2445
- if (resolved.viewProjectionMatrix) {
2446
- const viewMatrix = resolved.viewMatrix ? toMat4(resolved.viewMatrix, "viewMatrix") : undefined;
2447
- return {
2448
- viewProjectionMatrix: toMat4(resolved.viewProjectionMatrix, "viewProjectionMatrix"),
2449
- ...(resolved instanceof Camera
2450
- ? { camera: resolved, cameraPosition: cameraWorldPosition(resolved) }
2451
- : viewMatrix
2452
- ? { cameraPosition: cameraPositionFromViewMatrix(viewMatrix) }
2453
- : {})
2454
- };
2455
- }
2456
- if (resolved.projectionMatrix && resolved.viewMatrix) {
2457
- const viewMatrix = toMat4(resolved.viewMatrix, "viewMatrix");
2458
- return {
2459
- viewProjectionMatrix: multiplyMat4(toMat4(resolved.projectionMatrix, "projectionMatrix"), viewMatrix),
2460
- ...(resolved instanceof Camera
2461
- ? { camera: resolved, cameraPosition: cameraWorldPosition(resolved) }
2462
- : { cameraPosition: cameraPositionFromViewMatrix(viewMatrix) })
2463
- };
2464
- }
2465
- throw new RenderDeviceError(
2466
- "Renderer camera must expose a viewProjectionMatrix or projectionMatrix plus viewMatrix",
2467
- "CAMERA_VIEW_PROJECTION_MISSING"
2468
- );
2469
- }
2470
-
2471
- function hasExplicitAutoFrameCameraPolicy(source: RenderSource | Iterable<RenderItem> | Scene): boolean {
2472
- return !(source instanceof Scene) && !isIterable(source) && source.cameraPolicy === "auto-frame";
2473
- }
2474
-
2475
- function cameraWorldPosition(camera: Camera): readonly [number, number, number] {
2476
- const matrix = camera.transform.worldMatrix;
2477
- return [matrix[12] ?? 0, matrix[13] ?? 0, matrix[14] ?? 0];
2478
- }
2479
-
2480
- function cameraPositionFromViewMatrix(viewMatrix: Mat4): readonly [number, number, number] {
2481
- const inverseView = invertMat4(viewMatrix);
2482
- return [inverseView[12] ?? 0, inverseView[13] ?? 0, inverseView[14] ?? 0];
2483
- }
2484
-
2485
- function maybeCameraResize(camera: Camera | CameraLike): ((width: number, height: number) => void) | undefined {
2486
- const candidate = (camera as { readonly resize?: unknown }).resize;
2487
- return typeof candidate === "function" ? candidate.bind(camera) as (width: number, height: number) => void : undefined;
2488
- }
2489
-
2490
- function resolveSceneCamera(source: RenderSource | Iterable<RenderItem> | Scene): Camera | undefined {
2491
- return sceneFromSource(source)?.collectCameras()[0];
2492
- }
2493
-
2494
- function sceneFromSource(source: RenderSource | Iterable<RenderItem> | Scene): Scene | undefined {
2495
- return source instanceof Scene ? source : isIterable(source) ? undefined : source.scene;
2496
- }
2497
-
2498
- function normalMatrixFromModel(modelMatrix: Mat4): Mat4 {
2499
- const matrix = transposeMat4(invertMat4(modelMatrix));
2500
- return [
2501
- matrix[0], matrix[1], matrix[2], 0,
2502
- matrix[4], matrix[5], matrix[6], 0,
2503
- matrix[8], matrix[9], matrix[10], 0,
2504
- 0, 0, 0, 1
2505
- ];
2506
- }
2507
-
2508
- function transposeMat4(matrix: Mat4): Mat4 {
2509
- return [
2510
- matrix[0], matrix[4], matrix[8], matrix[12],
2511
- matrix[1], matrix[5], matrix[9], matrix[13],
2512
- matrix[2], matrix[6], matrix[10], matrix[14],
2513
- matrix[3], matrix[7], matrix[11], matrix[15]
2514
- ];
2515
- }
2516
-
2517
- function toMat4(value: Float32Array | readonly number[], field: string, label?: string): Mat4 {
2518
- const values = Array.from(value);
2519
- if (values.length !== 16 || !values.every(Number.isFinite)) {
2520
- throw new RenderDeviceError("Renderer matrix inputs must be finite mat4 values", "RENDERER_MATRIX_CONTRACT", {
2521
- field,
2522
- label,
2523
- scalars: values.length
2524
- });
2525
- }
2526
- return values as Mat4;
2527
- }
2528
-
2529
- function lookupRenderResource<T>(lookup: RenderResourceLookup<T>, key: string): T | undefined {
2530
- if (isReadonlyMap(lookup)) {
2531
- return lookup.get(key);
2532
- }
2533
- return lookup[key];
2534
- }
2535
-
2536
- function isReadonlyMap<T>(lookup: RenderResourceLookup<T>): lookup is ReadonlyMap<string, T> {
2537
- return typeof (lookup as ReadonlyMap<string, T>).get === "function";
2538
- }
2539
-
2540
- function collectRenderLights(source: RenderSource | Iterable<RenderItem> | Scene): readonly CollectedLight[] {
2541
- if (source instanceof Scene) {
2542
- return new LightCollector().collect(source);
2543
- }
2544
- if (isIterable(source)) {
2545
- return [];
2546
- }
2547
- if (source.collectedLights !== undefined) {
2548
- return [...source.collectedLights];
2549
- }
2550
- if (source.scene) {
2551
- const collected = new LightCollector().collect(source.scene);
2552
- if (collected.length > 0 || source.environmentLighting === false) {
2553
- return collected;
2554
- }
2555
- return createDefaultRendererDirectLights();
2556
- }
2557
- if (source.environmentLighting !== false && (source.renderItems || source.collectRenderItems)) {
2558
- return createDefaultRendererDirectLights();
2559
- }
2560
- return [];
2561
- }
2562
-
2563
- function createDefaultRendererDirectLights(): readonly CollectedLight[] {
2564
- const key = new DirectionalLight("default-renderer-key-light");
2565
- const fill = new DirectionalLight("default-renderer-fill-light");
2566
- const lights = [
2567
- { source: key, ...DEFAULT_RENDERER_DIRECT_LIGHTING.key },
2568
- { source: fill, ...DEFAULT_RENDERER_DIRECT_LIGHTING.fill }
2569
- ] as const;
2570
- return lights.map((light) => {
2571
- light.source.color = [...light.color] as [number, number, number];
2572
- light.source.intensity = light.intensity;
2573
- return {
2574
- kind: "directional" as const,
2575
- color: [...light.color] as [number, number, number],
2576
- intensity: light.intensity,
2577
- position: [0, 0, 0] as [number, number, number],
2578
- direction: normalizeDefaultLightDirection(light.direction),
2579
- range: 0,
2580
- spotAngle: 0,
2581
- penumbra: 0,
2582
- castsShadow: false,
2583
- layerMask: 0xffffffff,
2584
- source: light.source
2585
- };
2586
- });
2587
- }
2588
-
2589
- function normalizeDefaultLightDirection(direction: readonly [number, number, number]): readonly [number, number, number] {
2590
- const length = Math.hypot(direction[0], direction[1], direction[2]);
2591
- if (length <= 0 || !Number.isFinite(length)) {
2592
- return [0, 0, -1];
2593
- }
2594
- return [direction[0] / length, direction[1] / length, direction[2] / length];
2595
- }
2596
-
2597
- function cloneEnvironmentLighting(environment: EnvironmentLightingOptions): EnvironmentLightingOptions {
2598
- return {
2599
- color: [...environment.color] as [number, number, number],
2600
- intensity: environment.intensity,
2601
- ...(environment.proceduralMap
2602
- ? {
2603
- proceduralMap: {
2604
- skyColor: [...environment.proceduralMap.skyColor] as [number, number, number],
2605
- horizonColor: [...environment.proceduralMap.horizonColor] as [number, number, number],
2606
- groundColor: [...environment.proceduralMap.groundColor] as [number, number, number],
2607
- specularColor: [...environment.proceduralMap.specularColor] as [number, number, number],
2608
- intensity: environment.proceduralMap.intensity,
2609
- specularIntensity: environment.proceduralMap.specularIntensity
2610
- }
2611
- }
2612
- : {}),
2613
- ...(environment.environmentMapTexture ? { environmentMapTexture: environment.environmentMapTexture } : {}),
2614
- ...(environment.environmentCubeMapTexture ? { environmentCubeMapTexture: environment.environmentCubeMapTexture } : {}),
2615
- ...(environment.environmentMapIntensity !== undefined ? { environmentMapIntensity: environment.environmentMapIntensity } : {}),
2616
- ...(environment.environmentMapSpecularIntensity !== undefined ? { environmentMapSpecularIntensity: environment.environmentMapSpecularIntensity } : {}),
2617
- ...(environment.environmentMapRotation !== undefined ? { environmentMapRotation: environment.environmentMapRotation } : {}),
2618
- ...(environment.environmentMapMipCount !== undefined ? { environmentMapMipCount: environment.environmentMapMipCount } : {}),
2619
- ...(environment.environmentMapEncoding ? { environmentMapEncoding: environment.environmentMapEncoding } : {}),
2620
- ...(environment.environmentBrdfLutTexture ? { environmentBrdfLutTexture: environment.environmentBrdfLutTexture } : {})
2621
- };
2622
- }
2623
-
2624
- function isIterable(value: unknown): value is Iterable<RenderItem> {
2625
- return typeof (value as Iterable<RenderItem>)[Symbol.iterator] === "function";
2626
- }