@aura3d/rendering 1.4.0 → 1.4.2

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 (308) hide show
  1. package/package.json +38 -4
  2. package/AGENTS.md +0 -46
  3. package/src/ArchitecturalLightingFixtures.ts +0 -241
  4. package/src/ArchitecturalMaterialCatalog.ts +0 -130
  5. package/src/ArchitecturalMeasurementFixtures.ts +0 -155
  6. package/src/BRDFLut.ts +0 -47
  7. package/src/CameraFraming.ts +0 -200
  8. package/src/CanonicalSceneFixtures.ts +0 -95
  9. package/src/CascadedShadowMaps.ts +0 -449
  10. package/src/ColorManagement.ts +0 -127
  11. package/src/CullingFixtures.ts +0 -280
  12. package/src/DecalGeometry.ts +0 -26
  13. package/src/DepthPass.ts +0 -110
  14. package/src/EnvironmentBackgroundPass.ts +0 -194
  15. package/src/EnvironmentBackgroundResources.ts +0 -309
  16. package/src/EnvironmentLighting.ts +0 -96
  17. package/src/EnvironmentMapResources.ts +0 -1026
  18. package/src/EnvironmentPipeline.ts +0 -69
  19. package/src/EnvironmentPlatform.ts +0 -1306
  20. package/src/EnvironmentPreset.ts +0 -155
  21. package/src/Exposure.ts +0 -66
  22. package/src/ExternalParityRenderPreset.ts +0 -725
  23. package/src/ForwardPass.ts +0 -1668
  24. package/src/FrameVisualMetrics.ts +0 -301
  25. package/src/Geometry.ts +0 -461
  26. package/src/GeometryPrimitives.ts +0 -2
  27. package/src/HDRRenderPipeline.ts +0 -94
  28. package/src/IBL.ts +0 -86
  29. package/src/IndexBuffer.ts +0 -57
  30. package/src/InstancedPBRMaterial.ts +0 -186
  31. package/src/InstancedUnlitMaterial.ts +0 -60
  32. package/src/Instancing.ts +0 -19
  33. package/src/LightCollector.ts +0 -82
  34. package/src/LightUniforms.ts +0 -47
  35. package/src/LightingDebug.ts +0 -80
  36. package/src/LightingDefaults.ts +0 -79
  37. package/src/LightingRig.ts +0 -301
  38. package/src/LineGeometry.ts +0 -2
  39. package/src/Material.ts +0 -209
  40. package/src/MaterialBinding.ts +0 -131
  41. package/src/MaterialInstance.ts +0 -59
  42. package/src/MaterialPresets.ts +0 -272
  43. package/src/MorphTarget.ts +0 -124
  44. package/src/MorphTargetPlan.ts +0 -178
  45. package/src/MorphUnlitMaterial.ts +0 -60
  46. package/src/NormalMappedPBRMaterial.ts +0 -188
  47. package/src/OceanFixtures.ts +0 -318
  48. package/src/PBRLightingDefaults.ts +0 -12
  49. package/src/PBRMaterial.ts +0 -442
  50. package/src/PMREM.ts +0 -53
  51. package/src/PbrReference.ts +0 -681
  52. package/src/PostProcessPass.ts +0 -2507
  53. package/src/ProceduralTextureFixtures.ts +0 -393
  54. package/src/ProductTurntableFixtures.ts +0 -815
  55. package/src/Raycaster.ts +0 -21
  56. package/src/ReflectionProbe.ts +0 -13
  57. package/src/ReflectionSurfaces.ts +0 -219
  58. package/src/RenderBackend.ts +0 -43
  59. package/src/RenderDebugViews.ts +0 -138
  60. package/src/RenderDevice.ts +0 -836
  61. package/src/RenderGraph.ts +0 -194
  62. package/src/RenderPass.ts +0 -27
  63. package/src/RenderPipeline.ts +0 -101
  64. package/src/RenderQueue.ts +0 -12
  65. package/src/RenderState.ts +0 -12
  66. package/src/Renderer.ts +0 -2631
  67. package/src/RendererDebugOverlay.ts +0 -80
  68. package/src/RendererFeatureGates.ts +0 -150
  69. package/src/RendererPostprocessPlan.ts +0 -279
  70. package/src/RendererTiming.ts +0 -251
  71. package/src/RendererVisualPipelineReport.ts +0 -836
  72. package/src/ResourceLifecycle.ts +0 -21
  73. package/src/Sampler.ts +0 -37
  74. package/src/SceneOptimization.ts +0 -518
  75. package/src/ShaderChunks.ts +0 -393
  76. package/src/ShaderLibrary.ts +0 -2766
  77. package/src/ShaderModule.ts +0 -36
  78. package/src/ShaderPreprocessor.ts +0 -260
  79. package/src/ShaderReflection.ts +0 -201
  80. package/src/ShadowMap.ts +0 -237
  81. package/src/ShadowPass.ts +0 -198
  82. package/src/ShadowProjection.ts +0 -115
  83. package/src/SkinnedLitMaterial.ts +0 -397
  84. package/src/SkinnedUnlitMaterial.ts +0 -63
  85. package/src/SkinningBounds.ts +0 -131
  86. package/src/SpaceEnvironmentFixtures.ts +0 -200
  87. package/src/SpriteGeometry.ts +0 -21
  88. package/src/StereoCameraRig.ts +0 -127
  89. package/src/StereoEffects.ts +0 -274
  90. package/src/TerrainFixtures.ts +0 -180
  91. package/src/Texture.ts +0 -307
  92. package/src/TextureBinding.ts +0 -91
  93. package/src/TexturedPBRMaterial.ts +0 -1112
  94. package/src/TexturedUnlitMaterial.ts +0 -67
  95. package/src/ToneMapping.ts +0 -105
  96. package/src/UniformBinder.ts +0 -16
  97. package/src/UniformLayout.ts +0 -129
  98. package/src/UnlitMaterial.ts +0 -75
  99. package/src/VegetationFixtures.ts +0 -314
  100. package/src/VertexBuffer.ts +0 -120
  101. package/src/VertexFormat.ts +0 -171
  102. package/src/VoxelWorldFixtures.ts +0 -280
  103. package/src/WeatherFixtures.ts +0 -223
  104. package/src/WebGL2Device.ts +0 -2280
  105. package/src/WebGL2StateCache.ts +0 -330
  106. package/src/WebGPUDevice.ts +0 -2877
  107. package/src/WebGPURenderToTextureProof.ts +0 -139
  108. package/src/advanced-runtime/AdvancedRenderer.ts +0 -73
  109. package/src/advanced-runtime/index.ts +0 -9
  110. package/src/animation/AnimationMaterialStyle.ts +0 -32
  111. package/src/animation/AnimationRenderPreset.ts +0 -45
  112. package/src/animation/AnimationToonMaterial.ts +0 -387
  113. package/src/animation/AnimationVisualQuality.ts +0 -71
  114. package/src/animation/applyAnimationRenderPreset.ts +0 -180
  115. package/src/animation/index.ts +0 -5
  116. package/src/cinematic/BloomPass.ts +0 -26
  117. package/src/cinematic/CinematicDepthComposition.ts +0 -30
  118. package/src/cinematic/CinematicEvidence.ts +0 -123
  119. package/src/cinematic/CinematicLightingRig.ts +0 -144
  120. package/src/cinematic/CinematicMaterialPresets.ts +0 -130
  121. package/src/cinematic/CinematicPostProcess.ts +0 -83
  122. package/src/cinematic/DepthHazePass.ts +0 -30
  123. package/src/cinematic/EmissivePracticalLightSystem.ts +0 -45
  124. package/src/cinematic/FilmGrainPass.ts +0 -24
  125. package/src/cinematic/FogVolumeSystem.ts +0 -30
  126. package/src/cinematic/GlowCardSystem.ts +0 -44
  127. package/src/cinematic/RainParticleSystem.ts +0 -151
  128. package/src/cinematic/VignettePass.ts +0 -25
  129. package/src/cinematic/WetReflectionApproximation.ts +0 -27
  130. package/src/cinematic/index.ts +0 -14
  131. package/src/effects/CollisionModule.ts +0 -64
  132. package/src/effects/ColorModule.ts +0 -60
  133. package/src/effects/ForceModule.ts +0 -25
  134. package/src/effects/GPUParticleBackend.ts +0 -569
  135. package/src/effects/Particle.ts +0 -108
  136. package/src/effects/ParticleDiagnostics.ts +0 -224
  137. package/src/effects/ParticleEffectPresets.ts +0 -147
  138. package/src/effects/ParticleEmitter.ts +0 -326
  139. package/src/effects/ParticleModule.ts +0 -26
  140. package/src/effects/ParticleRenderPass.ts +0 -117
  141. package/src/effects/ParticleRenderer.ts +0 -132
  142. package/src/effects/ParticleSystem.ts +0 -333
  143. package/src/effects/SizeModule.ts +0 -56
  144. package/src/effects/TrailModule.ts +0 -72
  145. package/src/effects/VelocityModule.ts +0 -75
  146. package/src/effects/index.ts +0 -15
  147. package/src/index.ts +0 -925
  148. package/src/materials/AlphaSorting.ts +0 -30
  149. package/src/materials/MaterialExtensions.ts +0 -42
  150. package/src/materials/PhysicalMaterial.ts +0 -133
  151. package/src/materials/TransmissionPass.ts +0 -51
  152. package/src/performance/BVH.ts +0 -18
  153. package/src/performance/Batcher.ts +0 -2
  154. package/src/performance/FrustumCuller.ts +0 -9
  155. package/src/performance/LOD.ts +0 -20
  156. package/src/performance/Octree.ts +0 -10
  157. package/src/performance/RenderItemSorting.ts +0 -170
  158. package/src/performance/RendererStats.ts +0 -43
  159. package/src/performance/ResourceBudget.ts +0 -44
  160. package/src/postprocess/BloomPass.ts +0 -28
  161. package/src/postprocess/CinematicDiagnostics.ts +0 -642
  162. package/src/postprocess/ColorGradingPass.ts +0 -24
  163. package/src/postprocess/DepthOfFieldPass.ts +0 -12
  164. package/src/postprocess/EffectComposer.ts +0 -349
  165. package/src/postprocess/SSAOPass.ts +0 -20
  166. package/src/postprocess/index.ts +0 -34
  167. package/src/production-runtime/AnimationControlsPipeline.ts +0 -77
  168. package/src/production-runtime/PBRHDRPipeline.ts +0 -381
  169. package/src/production-runtime/ProductionEffectsPipeline.ts +0 -85
  170. package/src/production-runtime/ProductionRendererTypes.ts +0 -136
  171. package/src/production-runtime/ProductionRuntimeRenderer.ts +0 -172
  172. package/src/production-runtime/ProductionWebGL2Renderer.ts +0 -313
  173. package/src/production-runtime/ProductionWebGPURenderer.ts +0 -396
  174. package/src/production-runtime/TransmissionBackdropCapture.ts +0 -130
  175. package/src/production-runtime/animation/MorphTargetRenderer.ts +0 -2
  176. package/src/production-runtime/animation/SkinningRenderer.ts +0 -2
  177. package/src/production-runtime/backends/RendererBackend.ts +0 -17
  178. package/src/production-runtime/backends/WebGL2RendererBackend.ts +0 -30
  179. package/src/production-runtime/backends/WebGPURendererBackend.ts +0 -24
  180. package/src/production-runtime/backends/webgl2/WebGL2Buffer.ts +0 -2
  181. package/src/production-runtime/backends/webgl2/WebGL2Capabilities.ts +0 -2
  182. package/src/production-runtime/backends/webgl2/WebGL2RenderTarget.ts +0 -2
  183. package/src/production-runtime/backends/webgl2/WebGL2Shader.ts +0 -2
  184. package/src/production-runtime/backends/webgl2/WebGL2StateCache.ts +0 -1
  185. package/src/production-runtime/backends/webgl2/WebGL2Texture.ts +0 -2
  186. package/src/production-runtime/backends/webgpu/WebGPUBuffer.ts +0 -2
  187. package/src/production-runtime/backends/webgpu/WebGPUCapabilities.ts +0 -2
  188. package/src/production-runtime/backends/webgpu/WebGPURenderTarget.ts +0 -2
  189. package/src/production-runtime/backends/webgpu/WebGPUShader.ts +0 -2
  190. package/src/production-runtime/backends/webgpu/WebGPUTexture.ts +0 -2
  191. package/src/production-runtime/color/ColorManagement.ts +0 -3
  192. package/src/production-runtime/color/ToneMapping.ts +0 -7
  193. package/src/production-runtime/diagnostics/FrameCapture.ts +0 -2
  194. package/src/production-runtime/diagnostics/GPUCapabilities.ts +0 -2
  195. package/src/production-runtime/diagnostics/RendererStats.ts +0 -2
  196. package/src/production-runtime/environment/EnvironmentMap.ts +0 -1
  197. package/src/production-runtime/environment/HDRLoader.ts +0 -57
  198. package/src/production-runtime/environment/PMREMGenerator.ts +0 -713
  199. package/src/production-runtime/framegraph/FrameGraph.ts +0 -18
  200. package/src/production-runtime/framegraph/RenderPass.ts +0 -15
  201. package/src/production-runtime/geometry/ProjectedDecalGeometry.ts +0 -439
  202. package/src/production-runtime/index.ts +0 -127
  203. package/src/production-runtime/lights/LightManager.ts +0 -7
  204. package/src/production-runtime/lights/ShadowMapRenderer.ts +0 -5
  205. package/src/production-runtime/materials/GLTFMaterialAdapter.ts +0 -6
  206. package/src/production-runtime/materials/GLTFPBRMaterialAdapter.ts +0 -2
  207. package/src/production-runtime/materials/MaterialCompiler.ts +0 -7
  208. package/src/production-runtime/materials/MaterialTextureBindings.ts +0 -5
  209. package/src/production-runtime/materials/PBRMaterial.ts +0 -2
  210. package/src/production-runtime/materials/PBRShaderFeatures.ts +0 -2
  211. package/src/production-runtime/passes/ContactShadowPass.ts +0 -184
  212. package/src/production-runtime/passes/DepthPrepass.ts +0 -7
  213. package/src/production-runtime/passes/OpaquePass.ts +0 -7
  214. package/src/production-runtime/passes/ShadowPass.ts +0 -7
  215. package/src/production-runtime/passes/SkyboxPass.ts +0 -7
  216. package/src/production-runtime/passes/ToneMappingPass.ts +0 -7
  217. package/src/production-runtime/passes/TransparentPass.ts +0 -7
  218. package/src/production-runtime/postprocess/BloomPass.ts +0 -51
  219. package/src/production-runtime/postprocess/ColorGradingPass.ts +0 -49
  220. package/src/production-runtime/postprocess/DOFPass.ts +0 -47
  221. package/src/production-runtime/postprocess/FXAAPass.ts +0 -53
  222. package/src/production-runtime/postprocess/ProductionEffectComposer.ts +0 -78
  223. package/src/production-runtime/postprocess/ProductionPostProcessTypes.ts +0 -174
  224. package/src/production-runtime/postprocess/SSAOPass.ts +0 -45
  225. package/src/production-runtime/resources/GPUBuffer.ts +0 -2
  226. package/src/production-runtime/resources/GPUTexture.ts +0 -2
  227. package/src/production-runtime/resources/RenderTarget.ts +0 -3
  228. package/src/production-runtime/resources/ResourceCache.ts +0 -7
  229. package/src/production-runtime/scene/Camera.ts +0 -1
  230. package/src/production-runtime/scene/Lights.ts +0 -2
  231. package/src/production-runtime/scene/RenderableMesh.ts +0 -2
  232. package/src/production-runtime/scene/RenderablePrimitive.ts +0 -1
  233. package/src/production-runtime/scene/RenderableScene.ts +0 -4
  234. package/src/production-runtime/shaders/ShaderProgramLibrary.ts +0 -7
  235. package/src/production-runtime/shaders/chunks/brdf.glsl +0 -2
  236. package/src/production-runtime/shaders/chunks/ibl.glsl +0 -3
  237. package/src/production-runtime/shaders/chunks/pbr.frag.glsl +0 -47
  238. package/src/production-runtime/shaders/chunks/pbr.vert.glsl +0 -8
  239. package/src/production-runtime/shaders/chunks/shadows.glsl +0 -2
  240. package/src/production-runtime/shaders/wgsl/pbr.wgsl +0 -3
  241. package/src/production-runtime/shaders/wgsl/postprocess.wgsl +0 -2
  242. package/src/production-runtime/shaders/wgsl/skybox.wgsl +0 -2
  243. package/src/shaders/depth.frag.glsl +0 -5
  244. package/src/shaders/depth.vert.glsl +0 -8
  245. package/src/shaders/pbr-direct.frag.glsl +0 -687
  246. package/src/shaders/pbr-direct.vert.glsl +0 -18
  247. package/src/shaders/unlit.frag.glsl +0 -18
  248. package/src/shaders/unlit.vert.glsl +0 -13
  249. package/src/shadows/CascadedShadowPipeline.ts +0 -53
  250. package/src/shadows/ContactShadows.ts +0 -106
  251. package/src/shadows/ShadowDebugViews.ts +0 -46
  252. package/src/threejs-compatibility/InstancingSystem.ts +0 -17
  253. package/src/threejs-compatibility/LightingSystem.ts +0 -22
  254. package/src/threejs-compatibility/MaterialSystem.ts +0 -9
  255. package/src/threejs-compatibility/RenderTargetSystem.ts +0 -35
  256. package/src/threejs-compatibility/RendererDiagnostics.ts +0 -61
  257. package/src/threejs-compatibility/SceneRenderer.ts +0 -34
  258. package/src/threejs-compatibility/ShadowSystem.ts +0 -19
  259. package/src/threejs-compatibility/TextureSystem.ts +0 -19
  260. package/src/threejs-compatibility/ThreeCompatRenderer.ts +0 -90
  261. package/src/threejs-compatibility/TransparencySystem.ts +0 -19
  262. package/src/threejs-compatibility/index.ts +0 -29
  263. package/src/threejs-compatibility/performance/BVH.ts +0 -14
  264. package/src/threejs-compatibility/performance/FrustumCulling.ts +0 -10
  265. package/src/threejs-compatibility/performance/Instancing.ts +0 -4
  266. package/src/threejs-compatibility/performance/LODSystem.ts +0 -16
  267. package/src/threejs-compatibility/performance/OcclusionCulling.ts +0 -6
  268. package/src/threejs-compatibility/performance/RaycastAcceleration.ts +0 -12
  269. package/src/threejs-compatibility/performance/RendererProfiler.ts +0 -19
  270. package/src/threejs-compatibility/performance/TextureStreaming.ts +0 -4
  271. package/src/threejs-compatibility/performance/index.ts +0 -12
  272. package/src/threejs-compatibility/postprocess/BloomPass.ts +0 -16
  273. package/src/threejs-compatibility/postprocess/ColorGradingPass.ts +0 -16
  274. package/src/threejs-compatibility/postprocess/DepthOfFieldPass.ts +0 -21
  275. package/src/threejs-compatibility/postprocess/EffectComposer.ts +0 -14
  276. package/src/threejs-compatibility/postprocess/FXAAPass.ts +0 -15
  277. package/src/threejs-compatibility/postprocess/MotionBlurPass.ts +0 -20
  278. package/src/threejs-compatibility/postprocess/OutlinePass.ts +0 -21
  279. package/src/threejs-compatibility/postprocess/PostProcessTypes.ts +0 -169
  280. package/src/threejs-compatibility/postprocess/RenderPass.ts +0 -9
  281. package/src/threejs-compatibility/postprocess/SMAAPass.ts +0 -15
  282. package/src/threejs-compatibility/postprocess/SSAOPass.ts +0 -21
  283. package/src/threejs-compatibility/postprocess/ShaderPass.ts +0 -7
  284. package/src/threejs-compatibility/postprocess/TAAPass.ts +0 -15
  285. package/src/threejs-compatibility/postprocess/VignettePass.ts +0 -16
  286. package/src/threejs-compatibility/postprocess/index.ts +0 -15
  287. package/src/threejs-compatibility/shaders/NodeMaterial.ts +0 -21
  288. package/src/threejs-compatibility/shaders/RawShaderMaterial.ts +0 -6
  289. package/src/threejs-compatibility/shaders/ShaderDiagnostics.ts +0 -17
  290. package/src/threejs-compatibility/shaders/ShaderMaterial.ts +0 -18
  291. package/src/threejs-compatibility/shaders/ThreeCompatShaderChunks.ts +0 -6
  292. package/src/threejs-compatibility/shaders/Uniforms.ts +0 -18
  293. package/src/threejs-compatibility/shaders/index.ts +0 -9
  294. package/src/threejs-compatibility/vfx/GPUPointCloud.ts +0 -7
  295. package/src/threejs-compatibility/vfx/LineRenderer.ts +0 -13
  296. package/src/threejs-compatibility/vfx/ParticleSystem.ts +0 -23
  297. package/src/threejs-compatibility/vfx/SpriteSystem.ts +0 -14
  298. package/src/threejs-compatibility/vfx/TrailRenderer.ts +0 -8
  299. package/src/threejs-compatibility/vfx/VFXDiagnostics.ts +0 -23
  300. package/src/threejs-compatibility/vfx/index.ts +0 -9
  301. package/src/threejs-example-parity/index.ts +0 -205
  302. package/src/webgpu/WebGPUBuffer.ts +0 -1
  303. package/src/webgpu/WebGPUCompute.ts +0 -2
  304. package/src/webgpu/WebGPUPipelineCache.ts +0 -15
  305. package/src/webgpu/WebGPUPostProcess.ts +0 -2
  306. package/src/webgpu/WebGPUTexture.ts +0 -2
  307. package/tsconfig.json +0 -26
  308. package/vitest.config.ts +0 -20
@@ -1,1668 +0,0 @@
1
- import { Geometry } from "./Geometry";
2
- import { type CollectedLight } from "./LightCollector";
3
- import { LightUniforms } from "./LightUniforms";
4
- import { Material, type RenderState } from "./Material";
5
- import { MaterialBinding } from "./MaterialBinding";
6
- import { MaterialInstance } from "./MaterialInstance";
7
- import { applyMorphTargets, type MorphTargetDelta } from "./MorphTarget";
8
- import { type DrawCommand, type InstanceVertexAttribute, type RenderBuffer, type RenderDevice, RenderDeviceError, type RenderShaderProgram, type UniformValue } from "./RenderDevice";
9
- import { RenderPipeline } from "./RenderPipeline";
10
- import { BaseRenderPass, type RenderPassContext } from "./RenderPass";
11
- import { ShaderModule } from "./ShaderModule";
12
- import { createDefaultShaderLibrary, type ShaderLibrary } from "./ShaderLibrary";
13
- import { createShadowFilterKernel, type ShadowFilterKernel } from "./ShadowMap";
14
- import { TextureBinding } from "./TextureBinding";
15
- import { UnlitMaterial } from "./UnlitMaterial";
16
- import { sortRenderQueueItems } from "./performance/RenderItemSorting";
17
-
18
- export interface RenderItem {
19
- readonly geometry: Geometry;
20
- readonly material?: RenderMaterial;
21
- readonly label?: string;
22
- readonly drawRange?: RenderItemDrawRange;
23
- readonly includeInAutoFrame?: boolean;
24
- readonly modelMatrix?: Float32Array | readonly number[];
25
- readonly normalMatrix?: Float32Array | readonly number[];
26
- readonly modelViewProjectionMatrix?: Float32Array | readonly number[];
27
- readonly skinning?: SkinningPaletteBinding;
28
- readonly morphTargets?: readonly MorphTargetDelta[];
29
- readonly morphWeights?: readonly number[];
30
- readonly instanceTransforms?: Float32Array | readonly number[];
31
- readonly instanceColors?: Float32Array | readonly number[];
32
- readonly instanceAttributes?: readonly RenderItemInstanceAttribute[];
33
- readonly boundingBoxCenter?: readonly [number, number, number];
34
- }
35
-
36
- export interface RenderItemDrawRange {
37
- readonly start: number;
38
- readonly count: number;
39
- }
40
-
41
- export interface RenderItemInstanceAttribute {
42
- readonly shaderName: string;
43
- readonly components: 1 | 2 | 3 | 4;
44
- readonly data: Float32Array | readonly number[];
45
- readonly normalized?: boolean;
46
- readonly divisor?: number;
47
- }
48
-
49
- export type RenderMaterial = Material | MaterialInstance;
50
-
51
- export interface SkinningPaletteBinding {
52
- readonly jointCount: number;
53
- readonly matrices: Float32Array;
54
- }
55
-
56
- export const MAX_GPU_MORPH_VERTICES = 64;
57
- export const MAX_GPU_MORPH_TARGETS = 4;
58
- export const MAX_GPU_INSTANCES = 64;
59
-
60
- export interface ForwardPassOptions {
61
- readonly items: readonly RenderItem[];
62
- readonly lights?: readonly CollectedLight[];
63
- readonly environmentLighting?: EnvironmentLightingOptions;
64
- readonly environmentFog?: ForwardEnvironmentFogOptions | false;
65
- readonly inputColorResource?: string;
66
- readonly shadowMap?: ForwardShadowMapOptions;
67
- readonly cameraPosition?: readonly [number, number, number];
68
- readonly outputColorSpace?: "linear" | "srgb";
69
- readonly shaderLibrary?: ShaderLibrary;
70
- }
71
-
72
- export interface EnvironmentLightingOptions {
73
- readonly color: readonly [number, number, number];
74
- readonly intensity: number;
75
- readonly proceduralMap?: ProceduralEnvironmentMapLightingOptions;
76
- readonly environmentMapTexture?: TextureBinding;
77
- readonly environmentCubeMapTexture?: TextureBinding;
78
- readonly environmentMapIntensity?: number;
79
- readonly environmentMapSpecularIntensity?: number;
80
- readonly environmentMapRotation?: number;
81
- readonly environmentMapMipCount?: number;
82
- readonly environmentMapEncoding?: "srgb" | "rgbe" | "linear";
83
- readonly environmentBrdfLutTexture?: TextureBinding;
84
- }
85
-
86
- export interface ProceduralEnvironmentMapLightingOptions {
87
- readonly skyColor: readonly [number, number, number];
88
- readonly horizonColor: readonly [number, number, number];
89
- readonly groundColor: readonly [number, number, number];
90
- readonly specularColor: readonly [number, number, number];
91
- readonly intensity: number;
92
- readonly specularIntensity: number;
93
- }
94
-
95
- export type ForwardEnvironmentFogMode = "linear" | "exponential" | "exponential-squared";
96
-
97
- export interface ForwardEnvironmentFogOptions {
98
- readonly mode: ForwardEnvironmentFogMode;
99
- readonly color: readonly [number, number, number];
100
- readonly near: number;
101
- readonly far: number;
102
- readonly density: number;
103
- readonly heightFalloff?: number;
104
- readonly heightReference?: number;
105
- readonly maxOpacity?: number;
106
- }
107
-
108
- export interface ForwardShadowMapOptions {
109
- readonly texture: TextureBinding;
110
- readonly lightMatrix: Float32Array | readonly number[];
111
- readonly strength?: number;
112
- readonly bias?: number;
113
- readonly slopeBias?: number;
114
- readonly texelSize?: readonly [number, number];
115
- readonly filterKernel?: ShadowFilterKernel;
116
- readonly pointLight?: ForwardPointShadowMapOptions;
117
- }
118
-
119
- export interface ForwardPointShadowMapOptions {
120
- readonly texture: TextureBinding;
121
- readonly lightPosition: readonly [number, number, number];
122
- readonly range: number;
123
- readonly faceMatrices: Float32Array | readonly number[];
124
- readonly faceRects: Float32Array | readonly number[];
125
- readonly strength?: number;
126
- readonly bias?: number;
127
- readonly slopeBias?: number;
128
- readonly texelSize?: readonly [number, number];
129
- readonly filterKernel?: ShadowFilterKernel;
130
- }
131
-
132
- type Mat4 = [
133
- number, number, number, number,
134
- number, number, number, number,
135
- number, number, number, number,
136
- number, number, number, number
137
- ];
138
-
139
- const MAX_FORWARD_SHADOW_PCF_SAMPLES = 32;
140
- const DEFAULT_FORWARD_SHADOW_FILTER_KERNEL = createShadowFilterKernel({ filter: "pcf", pcfRadius: 1, pcfSamples: 9 });
141
- const INSTANCE_MATRIX_ATTRIBUTE_NAMES = [
142
- "a_instanceMatrix0",
143
- "a_instanceMatrix1",
144
- "a_instanceMatrix2",
145
- "a_instanceMatrix3"
146
- ] as const;
147
-
148
- export class ForwardPass extends BaseRenderPass {
149
- private static readonly shaderCaches = new WeakMap<RenderDevice, WeakMap<ShaderLibrary, Map<string, ShaderModule>>>();
150
- private readonly materialBinding = new MaterialBinding();
151
- private readonly shaderLibrary: ShaderLibrary;
152
- private readonly skinningPaletteUploads = new SkinningPaletteUploadManager();
153
-
154
- constructor(private readonly options: ForwardPassOptions) {
155
- super("forward", options.inputColorResource ? [options.inputColorResource] : [], ["color"]);
156
- this.shaderLibrary = options.shaderLibrary ?? createDefaultShaderLibrary();
157
- }
158
-
159
- execute(context: RenderPassContext): void {
160
- this.skinningPaletteUploads.beginFrame();
161
- for (const item of sortForwardRenderItems(this.options.items, this.options.cameraPosition)) {
162
- this.drawItem(context.device, item);
163
- }
164
- }
165
-
166
- private drawItem(device: RenderDevice, item: RenderItem): void {
167
- const material = item.material ?? new UnlitMaterial();
168
- const baseMaterial = getBaseMaterial(material);
169
- this.applyLightUniforms(material);
170
- const shader = this.getShader(baseMaterial, device);
171
- if (item.instanceTransforms && baseMaterial.renderState.cullMode !== "none" && instancedItemNeedsPerInstanceCullState(item)) {
172
- for (const expanded of expandInstancedRenderItem(item)) {
173
- this.drawItem(device, expanded);
174
- }
175
- return;
176
- }
177
- if (item.instanceTransforms && !supportsInstanceAttributes(shader) && instanceTransformCount(item) > MAX_GPU_INSTANCES) {
178
- for (const batch of splitInstanceTransforms(item.instanceTransforms)) {
179
- this.drawItem(device, { ...item, instanceTransforms: batch });
180
- }
181
- return;
182
- }
183
- if (item.instanceTransforms && !supportsInstanceAttributes(shader) && !supportsInstanceUniforms(shader)) {
184
- for (const expanded of expandInstancedRenderItem(item)) {
185
- this.drawItem(device, expanded);
186
- }
187
- return;
188
- }
189
- const binding = this.materialBinding.bind(material, shader);
190
- const uniforms = new Map<string, UniformValue>(binding.uniforms);
191
- applyEnvironmentLightingUniforms(this.options.environmentLighting, item, shader, uniforms);
192
- applyEnvironmentFogUniforms(this.options.environmentFog, item, shader, uniforms);
193
- applyForwardShadowMapUniforms(this.options.shadowMap, item, shader, uniforms);
194
- applyOutputColorSpaceUniform(this.options.outputColorSpace ?? "srgb", item, shader, uniforms);
195
- applyCameraUniforms(this.options.cameraPosition, item, shader, uniforms);
196
- applyAlphaCutoffUniform(item, shader, uniforms);
197
- applyTransformUniforms(item, shader, uniforms);
198
- if (item.skinning) {
199
- this.skinningPaletteUploads.bind(item, item.skinning, baseMaterial, shader, uniforms);
200
- }
201
- const instanceBinding = item.instanceTransforms ? applyInstanceBinding(device, item, shader, uniforms) : { count: 1 };
202
- const gpuMorph = item.morphTargets || item.morphWeights ? applyGpuMorphUniforms(item, shader, uniforms) : false;
203
- const geometry = gpuMorph ? item.geometry : resolveRenderGeometry(item);
204
- validateMaterialGeometryContract(item, baseMaterial, geometry);
205
- try {
206
- const vertexBuffer = geometry.vertexBuffer.upload(device);
207
- const indexBuffer = geometry.indexBuffer?.upload(device);
208
- const drawRange = resolveDrawRange(geometry, item.drawRange);
209
- const pipeline = new RenderPipeline({
210
- label: item.label ?? baseMaterial.name,
211
- shader,
212
- vertexFormat: geometry.vertexBuffer.format,
213
- topology: geometry.topology,
214
- renderState: renderStateForItem(baseMaterial.renderState, item),
215
- requiredAttributes: baseMaterial.requiredAttributes
216
- });
217
- const command: DrawCommand = pipeline.createDrawCommand({
218
- label: item.label,
219
- vertexBuffer,
220
- vertexCount: indexBuffer !== undefined ? geometry.vertexBuffer.vertexCount : drawRange.count,
221
- ...(indexBuffer === undefined && drawRange.start > 0 ? { firstVertex: drawRange.start } : {}),
222
- uniforms,
223
- ...(item.instanceTransforms ? { instanceCount: instanceBinding.count } : {}),
224
- ...(instanceBinding.attributes ? { instanceAttributes: instanceBinding.attributes } : {})
225
- });
226
- if (indexBuffer !== undefined) {
227
- Object.assign(command, {
228
- indexBuffer,
229
- indexType: geometry.indexBuffer?.type,
230
- indexCount: drawRange.count,
231
- ...(drawRange.start > 0 ? { firstIndex: drawRange.start } : {})
232
- });
233
- }
234
- device.draw(command);
235
- } finally {
236
- for (const buffer of instanceBinding.buffers ?? []) buffer.dispose();
237
- if (geometry !== item.geometry) {
238
- geometry.dispose();
239
- }
240
- }
241
- }
242
-
243
- private applyLightUniforms(material: RenderMaterial): void {
244
- const baseMaterial = getBaseMaterial(material);
245
- if (!baseMaterial.requiredUniforms.includes("u_lightCount") || !baseMaterial.requiredUniforms.includes("u_lightData")) {
246
- return;
247
- }
248
- const packed = LightUniforms.pack(this.options.lights ?? []);
249
- if (material instanceof MaterialInstance) {
250
- material.setOverride("u_lightCount", packed.lightCount);
251
- material.setOverride("u_lightData", packed.data);
252
- } else {
253
- material.setParameter("u_lightCount", packed.lightCount);
254
- material.setParameter("u_lightData", packed.data);
255
- }
256
- }
257
-
258
- private getShader(material: Material, device: RenderDevice): RenderShaderProgram {
259
- const cacheKey = shaderCacheKey(material);
260
- const shaderCache = getForwardPassShaderCache(device, this.shaderLibrary);
261
- let module = shaderCache.get(cacheKey);
262
- if (!module) {
263
- module = material.shaderVariant
264
- ? ShaderModule.fromLibraryVariant(this.shaderLibrary, material.shaderKey, material.shaderVariant)
265
- : ShaderModule.fromLibrary(this.shaderLibrary, material.shaderKey);
266
- shaderCache.set(cacheKey, module);
267
- }
268
- return module.compile(device);
269
- }
270
- }
271
-
272
- class SkinningPaletteUploadManager {
273
- private static readonly validatedGeometryJointCounts = new WeakMap<Geometry, Set<number>>();
274
- private submissions = 0;
275
- private jointsUploaded = 0;
276
- private maxJointCount = 0;
277
-
278
- beginFrame(): void {
279
- this.submissions = 0;
280
- this.jointsUploaded = 0;
281
- this.maxJointCount = 0;
282
- }
283
-
284
- bind(
285
- item: RenderItem,
286
- skinning: SkinningPaletteBinding,
287
- material: Material,
288
- shader: RenderShaderProgram,
289
- uniforms: Map<string, UniformValue>
290
- ): void {
291
- applySkinningUniforms(skinning, material, shader, uniforms);
292
- const validatedJointCounts = SkinningPaletteUploadManager.validatedGeometryJointCounts.get(item.geometry) ?? new Set<number>();
293
- if (!validatedJointCounts.has(skinning.jointCount)) {
294
- validateSkinningGeometryContract(item, skinning);
295
- validatedJointCounts.add(skinning.jointCount);
296
- SkinningPaletteUploadManager.validatedGeometryJointCounts.set(item.geometry, validatedJointCounts);
297
- }
298
- this.submissions += 1;
299
- this.jointsUploaded += skinning.jointCount;
300
- this.maxJointCount = Math.max(this.maxJointCount, skinning.jointCount);
301
- }
302
- }
303
-
304
- function getForwardPassShaderCache(device: RenderDevice, library: ShaderLibrary): Map<string, ShaderModule> {
305
- let libraryCaches = ForwardPass["shaderCaches"].get(device);
306
- if (!libraryCaches) {
307
- libraryCaches = new WeakMap<ShaderLibrary, Map<string, ShaderModule>>();
308
- ForwardPass["shaderCaches"].set(device, libraryCaches);
309
- }
310
- let shaderCache = libraryCaches.get(library);
311
- if (!shaderCache) {
312
- shaderCache = new Map<string, ShaderModule>();
313
- libraryCaches.set(library, shaderCache);
314
- }
315
- return shaderCache;
316
- }
317
-
318
- function applyOutputColorSpaceUniform(
319
- outputColorSpace: "linear" | "srgb",
320
- item: RenderItem,
321
- shader: RenderShaderProgram,
322
- uniforms: Map<string, UniformValue>
323
- ): void {
324
- if (!shader.reflection.uniforms.has("u_outputColorSpace")) return;
325
- if (outputColorSpace !== "linear" && outputColorSpace !== "srgb") {
326
- throw new RenderDeviceError("Forward pass outputColorSpace must be linear or srgb", "FORWARD_OUTPUT_COLOR_SPACE_CONTRACT", {
327
- label: item.label,
328
- outputColorSpace
329
- });
330
- }
331
- uniforms.set("u_outputColorSpace", outputColorSpace === "srgb" ? 1 : 0);
332
- }
333
-
334
- function applyEnvironmentFogUniforms(
335
- fog: ForwardEnvironmentFogOptions | false | undefined,
336
- item: RenderItem,
337
- shader: RenderShaderProgram,
338
- uniforms: Map<string, UniformValue>
339
- ): void {
340
- const requiredUniforms = [
341
- "u_environmentFogEnabled",
342
- "u_environmentFogMode",
343
- "u_environmentFogColor",
344
- "u_environmentFogNear",
345
- "u_environmentFogFar",
346
- "u_environmentFogDensity",
347
- "u_environmentFogHeightFalloff",
348
- "u_environmentFogHeightReference",
349
- "u_environmentFogMaxOpacity"
350
- ];
351
- if (!requiredUniforms.every((uniform) => shader.reflection.uniforms.has(uniform))) {
352
- return;
353
- }
354
- if (!fog) {
355
- uniforms.set("u_environmentFogEnabled", 0);
356
- uniforms.set("u_environmentFogMode", 1);
357
- uniforms.set("u_environmentFogColor", [0, 0, 0]);
358
- uniforms.set("u_environmentFogNear", 0);
359
- uniforms.set("u_environmentFogFar", 1);
360
- uniforms.set("u_environmentFogDensity", 0);
361
- uniforms.set("u_environmentFogHeightFalloff", 0);
362
- uniforms.set("u_environmentFogHeightReference", 0);
363
- uniforms.set("u_environmentFogMaxOpacity", 1);
364
- return;
365
- }
366
- const color = Array.from(fog.color);
367
- const heightFalloff = fog.heightFalloff ?? 0;
368
- const heightReference = fog.heightReference ?? 0;
369
- const maxOpacity = fog.maxOpacity ?? 1;
370
- if (color.length !== 3 || !color.every((component) => Number.isFinite(component) && component >= 0 && component <= 1)) {
371
- throw new RenderDeviceError("Forward environment fog color must contain three finite linear RGB values in [0, 1]", "FORWARD_ENVIRONMENT_FOG_CONTRACT", {
372
- label: item.label,
373
- color
374
- });
375
- }
376
- if (!Number.isFinite(fog.near) || !Number.isFinite(fog.far) || fog.far <= fog.near) {
377
- throw new RenderDeviceError("Forward environment fog requires finite far greater than near", "FORWARD_ENVIRONMENT_FOG_CONTRACT", {
378
- label: item.label,
379
- near: fog.near,
380
- far: fog.far
381
- });
382
- }
383
- if (!Number.isFinite(fog.density) || fog.density < 0) {
384
- throw new RenderDeviceError("Forward environment fog density must be finite and non-negative", "FORWARD_ENVIRONMENT_FOG_CONTRACT", {
385
- label: item.label,
386
- density: fog.density
387
- });
388
- }
389
- if (!Number.isFinite(heightFalloff) || heightFalloff < 0) {
390
- throw new RenderDeviceError("Forward environment fog heightFalloff must be finite and non-negative", "FORWARD_ENVIRONMENT_FOG_CONTRACT", {
391
- label: item.label,
392
- heightFalloff
393
- });
394
- }
395
- if (!Number.isFinite(heightReference)) {
396
- throw new RenderDeviceError("Forward environment fog heightReference must be finite", "FORWARD_ENVIRONMENT_FOG_CONTRACT", {
397
- label: item.label,
398
- heightReference
399
- });
400
- }
401
- if (!Number.isFinite(maxOpacity) || maxOpacity < 0 || maxOpacity > 1) {
402
- throw new RenderDeviceError("Forward environment fog maxOpacity must be finite in [0, 1]", "FORWARD_ENVIRONMENT_FOG_CONTRACT", {
403
- label: item.label,
404
- maxOpacity
405
- });
406
- }
407
- uniforms.set("u_environmentFogEnabled", 1);
408
- uniforms.set("u_environmentFogMode", fogModeUniform(fog.mode, item.label));
409
- uniforms.set("u_environmentFogColor", color);
410
- uniforms.set("u_environmentFogNear", fog.near);
411
- uniforms.set("u_environmentFogFar", fog.far);
412
- uniforms.set("u_environmentFogDensity", fog.density);
413
- uniforms.set("u_environmentFogHeightFalloff", heightFalloff);
414
- uniforms.set("u_environmentFogHeightReference", heightReference);
415
- uniforms.set("u_environmentFogMaxOpacity", maxOpacity);
416
- }
417
-
418
- function fogModeUniform(mode: ForwardEnvironmentFogMode, label: string | undefined): number {
419
- switch (mode) {
420
- case "linear":
421
- return 1;
422
- case "exponential":
423
- return 2;
424
- case "exponential-squared":
425
- return 3;
426
- default:
427
- throw new RenderDeviceError("Forward environment fog mode must be linear, exponential, or exponential-squared", "FORWARD_ENVIRONMENT_FOG_CONTRACT", {
428
- label,
429
- mode
430
- });
431
- }
432
- }
433
-
434
- function validateMaterialGeometryContract(item: RenderItem, material: Material, geometry: Geometry): void {
435
- const missing = material.requiredAttributes.filter((attribute) => !geometryHasAttribute(geometry, attribute));
436
- if (missing.length === 0) {
437
- return;
438
- }
439
- throw new RenderDeviceError("Render item geometry is missing attributes required by its material", "RENDER_ITEM_GEOMETRY_MATERIAL_CONTRACT", {
440
- label: item.label,
441
- material: material.name,
442
- topology: geometry.topology,
443
- vertexFormat: geometry.vertexBuffer.format.attributes.map((attribute) => attribute.shaderName),
444
- missingAttributes: missing
445
- });
446
- }
447
-
448
- function resolveDrawRange(geometry: Geometry, range: RenderItemDrawRange | undefined): { readonly start: number; readonly count: number } {
449
- const available = geometry.indexBuffer?.count ?? geometry.vertexBuffer.vertexCount;
450
- if (!range) {
451
- return { start: 0, count: available };
452
- }
453
- if (!Number.isInteger(range.start) || range.start < 0 || !Number.isInteger(range.count) || range.count <= 0) {
454
- throw new RenderDeviceError("Render item drawRange must contain non-negative integer start and positive integer count", "RENDER_ITEM_DRAW_RANGE_INVALID", {
455
- start: range.start,
456
- count: range.count,
457
- available
458
- });
459
- }
460
- if (range.start + range.count > available) {
461
- throw new RenderDeviceError("Render item drawRange exceeds geometry draw count", "RENDER_ITEM_DRAW_RANGE_INVALID", {
462
- start: range.start,
463
- count: range.count,
464
- available
465
- });
466
- }
467
- return range;
468
- }
469
-
470
- function geometryHasAttribute(geometry: Geometry, attribute: string): boolean {
471
- return geometry.vertexBuffer.format.attributes.some((candidate) => candidate.semantic === attribute || candidate.shaderName === attribute);
472
- }
473
-
474
- function shaderCacheKey(material: Material): string {
475
- return material.shaderVariant ? `${material.shaderKey}:${material.shaderVariant}` : material.shaderKey;
476
- }
477
-
478
- function supportsInstanceUniforms(shader: RenderShaderProgram): boolean {
479
- return shader.reflection.uniforms.has("u_instanceMatrices") && shader.reflection.uniforms.has("u_instanceCount");
480
- }
481
-
482
- function supportsInstanceAttributes(shader: RenderShaderProgram): boolean {
483
- return shader.reflection.uniforms.has("u_instanceAttributeMode") &&
484
- shader.reflection.uniforms.has("u_instanceCount") &&
485
- INSTANCE_MATRIX_ATTRIBUTE_NAMES.every((name) => shader.reflection.attributes.has(name));
486
- }
487
-
488
- function renderStateForItem(renderState: RenderState, item: RenderItem): RenderState {
489
- if (renderState.cullMode === "none") return renderState;
490
- const modelMatrix = toMat4Values(item.modelMatrix ?? identityMatrix(), "modelMatrix", item.label);
491
- if (!hasNegativeHandedness(modelMatrix)) return renderState;
492
- return {
493
- ...renderState,
494
- cullMode: renderState.cullMode === "back" ? "front" : "back"
495
- };
496
- }
497
-
498
- function instancedItemNeedsPerInstanceCullState(item: RenderItem): boolean {
499
- const source = validateInstanceTransformSource(item);
500
- const baseModel = toMat4Values(item.modelMatrix ?? identityMatrix(), "modelMatrix", item.label);
501
- const handedness = new Set<boolean>();
502
- for (let offset = 0; offset < source.length; offset += 16) {
503
- const instanceMatrix = mat4FromArrayLike(source, offset);
504
- handedness.add(hasNegativeHandedness(multiplyMat4(baseModel, instanceMatrix)));
505
- }
506
- return handedness.has(true);
507
- }
508
-
509
- function expandInstancedRenderItem(item: RenderItem): readonly RenderItem[] {
510
- const source = validateInstanceTransformSource(item);
511
- const baseModel = toMat4Values(item.modelMatrix ?? identityMatrix(), "modelMatrix", item.label);
512
- const baseModelViewProjection = toMat4Values(item.modelViewProjectionMatrix ?? baseModel, "modelViewProjectionMatrix", item.label);
513
- const { instanceTransforms: _instanceTransforms, normalMatrix: _normalMatrix, ...baseItem } = item;
514
- const expanded: RenderItem[] = [];
515
- for (let offset = 0; offset < source.length; offset += 16) {
516
- const index = offset / 16;
517
- const instanceMatrix = mat4FromArrayLike(source, offset);
518
- const modelMatrix = multiplyMat4(baseModel, instanceMatrix);
519
- expanded.push({
520
- ...baseItem,
521
- label: item.label ? `${item.label}#instance-${index}` : undefined,
522
- modelMatrix: new Float32Array(modelMatrix),
523
- modelViewProjectionMatrix: new Float32Array(multiplyMat4(baseModelViewProjection, instanceMatrix)),
524
- normalMatrix: new Float32Array(normalMatrixFromModel(modelMatrix))
525
- });
526
- }
527
- return expanded;
528
- }
529
-
530
- function multiplyMat4(left: Mat4, right: Mat4): Mat4 {
531
- const out = new Array(16).fill(0) as Mat4;
532
- for (let column = 0; column < 4; column += 1) {
533
- for (let row = 0; row < 4; row += 1) {
534
- out[column * 4 + row] =
535
- left[0 * 4 + row] * right[column * 4 + 0] +
536
- left[1 * 4 + row] * right[column * 4 + 1] +
537
- left[2 * 4 + row] * right[column * 4 + 2] +
538
- left[3 * 4 + row] * right[column * 4 + 3];
539
- }
540
- }
541
- return out;
542
- }
543
-
544
- function normalMatrixFromModel(modelMatrix: Mat4): Mat4 {
545
- let matrix: Mat4;
546
- try {
547
- matrix = transposeMat4(invertMat4(modelMatrix));
548
- } catch {
549
- matrix = [...identityMatrix()] as Mat4;
550
- }
551
- return [
552
- matrix[0], matrix[1], matrix[2], 0,
553
- matrix[4], matrix[5], matrix[6], 0,
554
- matrix[8], matrix[9], matrix[10], 0,
555
- 0, 0, 0, 1
556
- ];
557
- }
558
-
559
- function hasNegativeHandedness(matrix: Mat4): boolean {
560
- const determinant =
561
- matrix[0] * (matrix[5] * matrix[10] - matrix[9] * matrix[6]) -
562
- matrix[4] * (matrix[1] * matrix[10] - matrix[9] * matrix[2]) +
563
- matrix[8] * (matrix[1] * matrix[6] - matrix[5] * matrix[2]);
564
- return Number.isFinite(determinant) && determinant < -1e-8;
565
- }
566
-
567
- function transposeMat4(matrix: Mat4): Mat4 {
568
- return [
569
- matrix[0], matrix[4], matrix[8], matrix[12],
570
- matrix[1], matrix[5], matrix[9], matrix[13],
571
- matrix[2], matrix[6], matrix[10], matrix[14],
572
- matrix[3], matrix[7], matrix[11], matrix[15]
573
- ];
574
- }
575
-
576
- function invertMat4(matrix: Mat4): Mat4 {
577
- const [
578
- a00, a01, a02, a03,
579
- a10, a11, a12, a13,
580
- a20, a21, a22, a23,
581
- a30, a31, a32, a33
582
- ] = matrix;
583
- const b00 = a00 * a11 - a01 * a10;
584
- const b01 = a00 * a12 - a02 * a10;
585
- const b02 = a00 * a13 - a03 * a10;
586
- const b03 = a01 * a12 - a02 * a11;
587
- const b04 = a01 * a13 - a03 * a11;
588
- const b05 = a02 * a13 - a03 * a12;
589
- const b06 = a20 * a31 - a21 * a30;
590
- const b07 = a20 * a32 - a22 * a30;
591
- const b08 = a20 * a33 - a23 * a30;
592
- const b09 = a21 * a32 - a22 * a31;
593
- const b10 = a21 * a33 - a23 * a31;
594
- const b11 = a22 * a33 - a23 * a32;
595
- const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
596
- if (Math.abs(det) <= 1e-12 || !Number.isFinite(det)) {
597
- throw new RenderDeviceError("Instance transform fallback requires invertible model matrices", "INSTANCING_CONTRACT");
598
- }
599
- const invDet = 1 / det;
600
- return [
601
- (a11 * b11 - a12 * b10 + a13 * b09) * invDet,
602
- (a02 * b10 - a01 * b11 - a03 * b09) * invDet,
603
- (a31 * b05 - a32 * b04 + a33 * b03) * invDet,
604
- (a22 * b04 - a21 * b05 - a23 * b03) * invDet,
605
- (a12 * b08 - a10 * b11 - a13 * b07) * invDet,
606
- (a00 * b11 - a02 * b08 + a03 * b07) * invDet,
607
- (a32 * b02 - a30 * b05 - a33 * b01) * invDet,
608
- (a20 * b05 - a22 * b02 + a23 * b01) * invDet,
609
- (a10 * b10 - a11 * b08 + a13 * b06) * invDet,
610
- (a01 * b08 - a00 * b10 - a03 * b06) * invDet,
611
- (a30 * b04 - a31 * b02 + a33 * b00) * invDet,
612
- (a21 * b02 - a20 * b04 - a23 * b00) * invDet,
613
- (a11 * b07 - a10 * b09 - a12 * b06) * invDet,
614
- (a00 * b09 - a01 * b07 + a02 * b06) * invDet,
615
- (a31 * b01 - a30 * b03 - a32 * b00) * invDet,
616
- (a20 * b03 - a21 * b01 + a22 * b00) * invDet
617
- ];
618
- }
619
-
620
- function applyForwardShadowMapUniforms(
621
- shadowMap: ForwardShadowMapOptions | undefined,
622
- item: RenderItem,
623
- shader: RenderShaderProgram,
624
- uniforms: Map<string, UniformValue>
625
- ): void {
626
- const requiredUniforms = [
627
- "u_shadowMapTexture",
628
- "u_shadowMapEnabled",
629
- "u_shadowMapMatrix",
630
- "u_shadowMapStrength",
631
- "u_shadowMapBias",
632
- "u_shadowMapSlopeBias",
633
- "u_shadowMapTexelSize",
634
- "u_shadowPcfSampleCount",
635
- "u_shadowPcfSamples"
636
- ];
637
- if (!requiredUniforms.every((uniform) => shader.reflection.uniforms.has(uniform))) {
638
- return;
639
- }
640
- applyForwardPointShadowMapUniforms(shadowMap?.pointLight, item, shader, uniforms);
641
- if (!shadowMap) {
642
- uniforms.set("u_shadowMapTexture", new TextureBinding({ name: "u_shadowMapTexture", required: false }));
643
- uniforms.set("u_shadowMapEnabled", 0);
644
- uniforms.set("u_shadowMapMatrix", new Float32Array(identityMatrix()));
645
- uniforms.set("u_shadowMapStrength", 0);
646
- uniforms.set("u_shadowMapBias", 0);
647
- uniforms.set("u_shadowMapSlopeBias", 0);
648
- uniforms.set("u_shadowMapTexelSize", [1, 1]);
649
- uniforms.set("u_shadowPcfSampleCount", 1);
650
- uniforms.set("u_shadowPcfSamples", new Float32Array(MAX_FORWARD_SHADOW_PCF_SAMPLES * 4));
651
- return;
652
- }
653
- const validation = shadowMap.texture.validate();
654
- if (!validation.ok) {
655
- throw new RenderDeviceError("Forward shadow-map texture binding validation failed", "FORWARD_SHADOW_MAP_CONTRACT", {
656
- label: item.label,
657
- diagnostics: validation.diagnostics
658
- });
659
- }
660
- const strength = shadowMap.strength ?? 0.65;
661
- const bias = shadowMap.bias ?? 0.001;
662
- const slopeBias = shadowMap.slopeBias ?? 1;
663
- const texelSize = shadowMap.texelSize ?? [
664
- 1 / Math.max(1, shadowMap.texture.texture?.width ?? 1),
665
- 1 / Math.max(1, shadowMap.texture.texture?.height ?? 1)
666
- ];
667
- if (!Number.isFinite(strength) || strength < 0 || strength > 1) {
668
- throw new RenderDeviceError("Forward shadow-map strength must be finite in [0, 1]", "FORWARD_SHADOW_MAP_CONTRACT", {
669
- label: item.label,
670
- strength
671
- });
672
- }
673
- if (!Number.isFinite(bias) || bias < 0) {
674
- throw new RenderDeviceError("Forward shadow-map bias must be finite and non-negative", "FORWARD_SHADOW_MAP_CONTRACT", {
675
- label: item.label,
676
- bias
677
- });
678
- }
679
- if (!Number.isFinite(slopeBias) || slopeBias < 0) {
680
- throw new RenderDeviceError("Forward shadow-map slopeBias must be finite and non-negative", "FORWARD_SHADOW_MAP_CONTRACT", {
681
- label: item.label,
682
- slopeBias
683
- });
684
- }
685
- if (texelSize.length !== 2 || !texelSize.every((value) => Number.isFinite(value) && value > 0)) {
686
- throw new RenderDeviceError("Forward shadow-map texelSize must contain two finite positive values", "FORWARD_SHADOW_MAP_CONTRACT", {
687
- label: item.label,
688
- texelSize: Array.from(texelSize)
689
- });
690
- }
691
- const filterKernel = shadowMap.filterKernel ?? DEFAULT_FORWARD_SHADOW_FILTER_KERNEL;
692
- const pcfSamples = packForwardShadowPcfSamples(filterKernel, item.label);
693
- uniforms.set("u_shadowMapTexture", shadowMap.texture);
694
- uniforms.set("u_shadowMapEnabled", 1);
695
- uniforms.set("u_shadowMapMatrix", toMat4Uniform(shadowMap.lightMatrix, "shadowMap.lightMatrix", item.label));
696
- uniforms.set("u_shadowMapStrength", strength);
697
- uniforms.set("u_shadowMapBias", bias);
698
- uniforms.set("u_shadowMapSlopeBias", slopeBias);
699
- uniforms.set("u_shadowMapTexelSize", texelSize);
700
- uniforms.set("u_shadowPcfSampleCount", filterKernel.samples.length);
701
- uniforms.set("u_shadowPcfSamples", pcfSamples);
702
- }
703
-
704
- function applyForwardPointShadowMapUniforms(
705
- pointShadowMap: ForwardPointShadowMapOptions | undefined,
706
- item: RenderItem,
707
- shader: RenderShaderProgram,
708
- uniforms: Map<string, UniformValue>
709
- ): void {
710
- const requiredUniforms = [
711
- "u_pointShadowMapTexture",
712
- "u_pointShadowMapEnabled",
713
- "u_pointShadowLightPosition",
714
- "u_pointShadowRange",
715
- "u_pointShadowFaceMatrices",
716
- "u_pointShadowFaceRects",
717
- "u_pointShadowStrength",
718
- "u_pointShadowBias",
719
- "u_pointShadowSlopeBias",
720
- "u_pointShadowTexelSize",
721
- "u_pointShadowPcfSampleCount",
722
- "u_pointShadowPcfSamples"
723
- ];
724
- if (!requiredUniforms.every((uniform) => shader.reflection.uniforms.has(uniform))) {
725
- return;
726
- }
727
- if (!pointShadowMap) {
728
- uniforms.set("u_pointShadowMapTexture", new TextureBinding({ name: "u_pointShadowMapTexture", required: false }));
729
- uniforms.set("u_pointShadowMapEnabled", 0);
730
- uniforms.set("u_pointShadowLightPosition", [0, 0, 0]);
731
- uniforms.set("u_pointShadowRange", 1);
732
- uniforms.set("u_pointShadowFaceMatrices", new Float32Array(6 * 16));
733
- uniforms.set("u_pointShadowFaceRects", new Float32Array(6 * 4));
734
- uniforms.set("u_pointShadowStrength", 0);
735
- uniforms.set("u_pointShadowBias", 0);
736
- uniforms.set("u_pointShadowSlopeBias", 0);
737
- uniforms.set("u_pointShadowTexelSize", [1, 1]);
738
- uniforms.set("u_pointShadowPcfSampleCount", 1);
739
- uniforms.set("u_pointShadowPcfSamples", new Float32Array(MAX_FORWARD_SHADOW_PCF_SAMPLES * 4));
740
- return;
741
- }
742
- const validation = pointShadowMap.texture.validate();
743
- if (!validation.ok) {
744
- throw new RenderDeviceError("Forward point-shadow atlas texture binding validation failed", "FORWARD_POINT_SHADOW_MAP_CONTRACT", {
745
- label: item.label,
746
- diagnostics: validation.diagnostics
747
- });
748
- }
749
- if (pointShadowMap.lightPosition.length !== 3 || !isFiniteArrayLike(pointShadowMap.lightPosition)) {
750
- throw new RenderDeviceError("Forward point-shadow lightPosition must contain three finite values", "FORWARD_POINT_SHADOW_MAP_CONTRACT", {
751
- label: item.label,
752
- lightPosition: pointShadowMap.lightPosition
753
- });
754
- }
755
- if (!Number.isFinite(pointShadowMap.range) || pointShadowMap.range <= 0) {
756
- throw new RenderDeviceError("Forward point-shadow range must be finite and positive", "FORWARD_POINT_SHADOW_MAP_CONTRACT", {
757
- label: item.label,
758
- range: pointShadowMap.range
759
- });
760
- }
761
- const faceMatrices = toFloat32Array(pointShadowMap.faceMatrices, 6 * 16, "pointShadowMap.faceMatrices", item.label);
762
- const faceRects = toFloat32Array(pointShadowMap.faceRects, 6 * 4, "pointShadowMap.faceRects", item.label);
763
- validatePointShadowFaceRects(faceRects, item.label);
764
- const strength = pointShadowMap.strength ?? 0.65;
765
- const bias = pointShadowMap.bias ?? 0.001;
766
- const slopeBias = pointShadowMap.slopeBias ?? 1;
767
- const texelSize = pointShadowMap.texelSize ?? [
768
- 1 / Math.max(1, pointShadowMap.texture.texture?.width ?? 1),
769
- 1 / Math.max(1, pointShadowMap.texture.texture?.height ?? 1)
770
- ];
771
- if (!Number.isFinite(strength) || strength < 0 || strength > 1) {
772
- throw new RenderDeviceError("Forward point-shadow strength must be finite in [0, 1]", "FORWARD_POINT_SHADOW_MAP_CONTRACT", {
773
- label: item.label,
774
- strength
775
- });
776
- }
777
- if (!Number.isFinite(bias) || bias < 0) {
778
- throw new RenderDeviceError("Forward point-shadow bias must be finite and non-negative", "FORWARD_POINT_SHADOW_MAP_CONTRACT", {
779
- label: item.label,
780
- bias
781
- });
782
- }
783
- if (!Number.isFinite(slopeBias) || slopeBias < 0) {
784
- throw new RenderDeviceError("Forward point-shadow slopeBias must be finite and non-negative", "FORWARD_POINT_SHADOW_MAP_CONTRACT", {
785
- label: item.label,
786
- slopeBias
787
- });
788
- }
789
- if (texelSize.length !== 2 || !texelSize.every((value) => Number.isFinite(value) && value > 0)) {
790
- throw new RenderDeviceError("Forward point-shadow texelSize must contain two finite positive values", "FORWARD_POINT_SHADOW_MAP_CONTRACT", {
791
- label: item.label,
792
- texelSize: Array.from(texelSize)
793
- });
794
- }
795
- const filterKernel = pointShadowMap.filterKernel ?? DEFAULT_FORWARD_SHADOW_FILTER_KERNEL;
796
- uniforms.set("u_pointShadowMapTexture", pointShadowMap.texture);
797
- uniforms.set("u_pointShadowMapEnabled", 1);
798
- uniforms.set("u_pointShadowLightPosition", pointShadowMap.lightPosition);
799
- uniforms.set("u_pointShadowRange", pointShadowMap.range);
800
- uniforms.set("u_pointShadowFaceMatrices", faceMatrices);
801
- uniforms.set("u_pointShadowFaceRects", faceRects);
802
- uniforms.set("u_pointShadowStrength", strength);
803
- uniforms.set("u_pointShadowBias", bias);
804
- uniforms.set("u_pointShadowSlopeBias", slopeBias);
805
- uniforms.set("u_pointShadowTexelSize", texelSize);
806
- uniforms.set("u_pointShadowPcfSampleCount", filterKernel.samples.length);
807
- uniforms.set("u_pointShadowPcfSamples", packForwardShadowPcfSamples(filterKernel, item.label));
808
- }
809
-
810
- function toFloat32Array(values: Float32Array | readonly number[], expectedLength: number, name: string, label: string | undefined): Float32Array {
811
- const source = values instanceof Float32Array ? values : new Float32Array(values);
812
- if (source.length !== expectedLength || !isFiniteArrayLike(source)) {
813
- throw new RenderDeviceError(`${name} must contain ${expectedLength} finite values`, "FORWARD_POINT_SHADOW_MAP_CONTRACT", {
814
- label,
815
- length: source.length,
816
- expectedLength
817
- });
818
- }
819
- return source;
820
- }
821
-
822
- function validatePointShadowFaceRects(faceRects: Float32Array, label: string | undefined): void {
823
- for (let offset = 0; offset < faceRects.length; offset += 4) {
824
- const rect = [faceRects[offset], faceRects[offset + 1], faceRects[offset + 2], faceRects[offset + 3]];
825
- if (!rect.every((value) => Number.isFinite(value) && value >= 0 && value <= 1) || rect[2]! <= 0 || rect[3]! <= 0) {
826
- throw new RenderDeviceError("Forward point-shadow face rects must be normalized atlas rectangles", "FORWARD_POINT_SHADOW_MAP_CONTRACT", {
827
- label,
828
- face: offset / 4,
829
- rect
830
- });
831
- }
832
- }
833
- }
834
-
835
- function packForwardShadowPcfSamples(filterKernel: ShadowFilterKernel, label: string | undefined): Float32Array {
836
- if (filterKernel.samples.length < 1 || filterKernel.samples.length > MAX_FORWARD_SHADOW_PCF_SAMPLES) {
837
- throw new RenderDeviceError("Forward shadow-map PCF kernel must contain 1 to 32 samples", "FORWARD_SHADOW_MAP_CONTRACT", {
838
- label,
839
- samples: filterKernel.samples.length
840
- });
841
- }
842
- const packed = new Float32Array(MAX_FORWARD_SHADOW_PCF_SAMPLES * 4);
843
- let weightSum = 0;
844
- for (const [index, sample] of filterKernel.samples.entries()) {
845
- if (![sample.x, sample.y, sample.weight].every(Number.isFinite) || sample.weight < 0) {
846
- throw new RenderDeviceError("Forward shadow-map PCF samples must contain finite offsets and non-negative weights", "FORWARD_SHADOW_MAP_CONTRACT", {
847
- label,
848
- sample
849
- });
850
- }
851
- const offset = index * 4;
852
- packed[offset] = sample.x;
853
- packed[offset + 1] = sample.y;
854
- packed[offset + 2] = sample.weight;
855
- weightSum += sample.weight;
856
- }
857
- if (weightSum <= 0) {
858
- throw new RenderDeviceError("Forward shadow-map PCF sample weights must sum to a positive value", "FORWARD_SHADOW_MAP_CONTRACT", {
859
- label,
860
- weightSum
861
- });
862
- }
863
- return packed;
864
- }
865
-
866
- function applyEnvironmentLightingUniforms(
867
- environment: EnvironmentLightingOptions | undefined,
868
- item: RenderItem,
869
- shader: RenderShaderProgram,
870
- uniforms: Map<string, UniformValue>
871
- ): void {
872
- if (!environment) {
873
- clearProceduralEnvironmentMapUniforms(shader, uniforms);
874
- clearSampledEnvironmentMapUniforms(shader, uniforms);
875
- clearEnvironmentBrdfLutUniforms(shader, uniforms);
876
- return;
877
- }
878
- if (!shader.reflection.uniforms.has("u_environmentColor") || !shader.reflection.uniforms.has("u_environmentIntensity")) {
879
- return;
880
- }
881
- const color = environment.color;
882
- if (color.length !== 3 || !isFiniteColor(color)) {
883
- throw new RenderDeviceError("Environment lighting color must contain three finite values in [0, 1]", "ENVIRONMENT_LIGHTING_CONTRACT", {
884
- label: item.label,
885
- color
886
- });
887
- }
888
- if (!Number.isFinite(environment.intensity) || environment.intensity < 0) {
889
- throw new RenderDeviceError("Environment lighting intensity must be finite and non-negative", "ENVIRONMENT_LIGHTING_CONTRACT", {
890
- label: item.label,
891
- intensity: environment.intensity
892
- });
893
- }
894
- uniforms.set("u_environmentColor", color);
895
- uniforms.set("u_environmentIntensity", environment.intensity);
896
- applyProceduralEnvironmentMapUniforms(environment.proceduralMap, item, shader, uniforms);
897
- applySampledEnvironmentMapUniforms(environment, item, shader, uniforms);
898
- }
899
-
900
- function applyProceduralEnvironmentMapUniforms(
901
- proceduralMap: ProceduralEnvironmentMapLightingOptions | undefined,
902
- item: RenderItem,
903
- shader: RenderShaderProgram,
904
- uniforms: Map<string, UniformValue>
905
- ): void {
906
- if (!proceduralMap) {
907
- clearProceduralEnvironmentMapUniforms(shader, uniforms);
908
- return;
909
- }
910
- const requiredUniforms = [
911
- "u_environmentSkyColor",
912
- "u_environmentHorizonColor",
913
- "u_environmentGroundColor",
914
- "u_environmentSpecularColor",
915
- "u_environmentMapIntensity",
916
- "u_environmentSpecularIntensity"
917
- ];
918
- if (!requiredUniforms.every((uniform) => shader.reflection.uniforms.has(uniform))) {
919
- return;
920
- }
921
- const skyColor = toEnvironmentColor(proceduralMap.skyColor, "proceduralMap.skyColor", item.label);
922
- const horizonColor = toEnvironmentColor(proceduralMap.horizonColor, "proceduralMap.horizonColor", item.label);
923
- const groundColor = toEnvironmentColor(proceduralMap.groundColor, "proceduralMap.groundColor", item.label);
924
- const specularColor = toEnvironmentColor(proceduralMap.specularColor, "proceduralMap.specularColor", item.label);
925
- if (!Number.isFinite(proceduralMap.intensity) || proceduralMap.intensity < 0) {
926
- throw new RenderDeviceError("Procedural environment map intensity must be finite and non-negative", "ENVIRONMENT_LIGHTING_CONTRACT", {
927
- label: item.label,
928
- intensity: proceduralMap.intensity
929
- });
930
- }
931
- if (!Number.isFinite(proceduralMap.specularIntensity) || proceduralMap.specularIntensity < 0) {
932
- throw new RenderDeviceError("Procedural environment map specularIntensity must be finite and non-negative", "ENVIRONMENT_LIGHTING_CONTRACT", {
933
- label: item.label,
934
- specularIntensity: proceduralMap.specularIntensity
935
- });
936
- }
937
- uniforms.set("u_environmentSkyColor", skyColor);
938
- uniforms.set("u_environmentHorizonColor", horizonColor);
939
- uniforms.set("u_environmentGroundColor", groundColor);
940
- uniforms.set("u_environmentSpecularColor", specularColor);
941
- uniforms.set("u_environmentMapIntensity", proceduralMap.intensity);
942
- uniforms.set("u_environmentSpecularIntensity", proceduralMap.specularIntensity);
943
- }
944
-
945
- function clearProceduralEnvironmentMapUniforms(
946
- shader: RenderShaderProgram,
947
- uniforms: Map<string, UniformValue>
948
- ): void {
949
- if (shader.reflection.uniforms.has("u_environmentMapIntensity")) {
950
- uniforms.set("u_environmentMapIntensity", 0);
951
- }
952
- if (shader.reflection.uniforms.has("u_environmentSpecularIntensity")) {
953
- uniforms.set("u_environmentSpecularIntensity", 0);
954
- }
955
- }
956
-
957
- function applySampledEnvironmentMapUniforms(
958
- environment: EnvironmentLightingOptions,
959
- item: RenderItem,
960
- shader: RenderShaderProgram,
961
- uniforms: Map<string, UniformValue>
962
- ): void {
963
- const environmentMapTexture = environment.environmentMapTexture;
964
- const environmentCubeMapTexture = environment.environmentCubeMapTexture;
965
- if (!environmentMapTexture && !environmentCubeMapTexture) {
966
- clearSampledEnvironmentMapUniforms(shader, uniforms);
967
- clearEnvironmentBrdfLutUniforms(shader, uniforms);
968
- return;
969
- }
970
- const requiredUniforms = [
971
- "u_environmentMapTexture",
972
- "u_environmentMapTextureEnabled",
973
- "u_environmentMapTextureIntensity",
974
- "u_environmentMapTextureSpecularIntensity",
975
- "u_environmentMapTextureRotation",
976
- "u_environmentMapTextureMipCount"
977
- ];
978
- if (!requiredUniforms.every((uniform) => shader.reflection.uniforms.has(uniform))) {
979
- return;
980
- }
981
- if (environmentMapTexture) {
982
- validateEnvironmentTextureBinding("Environment map texture", environmentMapTexture, "2d", item);
983
- }
984
- if (environmentCubeMapTexture) {
985
- validateEnvironmentTextureBinding("Environment cube map texture", environmentCubeMapTexture, "cube", item);
986
- }
987
- const intensity = environment.environmentMapIntensity ?? 1;
988
- const specularIntensity = environment.environmentMapSpecularIntensity ?? 0.5;
989
- const rotation = environment.environmentMapRotation ?? 0;
990
- const mipCount = environment.environmentMapMipCount ?? 1;
991
- const encoding = environment.environmentMapEncoding ?? "srgb";
992
- if (!Number.isFinite(intensity) || intensity < 0) {
993
- throw new RenderDeviceError("Sampled environment map intensity must be finite and non-negative", "ENVIRONMENT_LIGHTING_CONTRACT", {
994
- label: item.label,
995
- intensity
996
- });
997
- }
998
- if (!Number.isFinite(specularIntensity) || specularIntensity < 0) {
999
- throw new RenderDeviceError("Sampled environment map specular intensity must be finite and non-negative", "ENVIRONMENT_LIGHTING_CONTRACT", {
1000
- label: item.label,
1001
- specularIntensity
1002
- });
1003
- }
1004
- if (!Number.isFinite(rotation)) {
1005
- throw new RenderDeviceError("Sampled environment map rotation must be finite", "ENVIRONMENT_LIGHTING_CONTRACT", {
1006
- label: item.label,
1007
- rotation
1008
- });
1009
- }
1010
- if (!Number.isInteger(mipCount) || mipCount < 1) {
1011
- throw new RenderDeviceError("Sampled environment map mip count must be a positive integer", "ENVIRONMENT_LIGHTING_CONTRACT", {
1012
- label: item.label,
1013
- mipCount
1014
- });
1015
- }
1016
- if (encoding !== "srgb" && encoding !== "rgbe" && encoding !== "linear") {
1017
- throw new RenderDeviceError("Sampled environment map encoding must be srgb, rgbe, or linear", "ENVIRONMENT_LIGHTING_CONTRACT", {
1018
- label: item.label,
1019
- encoding
1020
- });
1021
- }
1022
- uniforms.set(
1023
- "u_environmentMapTexture",
1024
- environmentMapTexture ?? new TextureBinding({ name: "u_environmentMapTexture", required: false, expectedDimension: "2d" })
1025
- );
1026
- if (shader.reflection.uniforms.has("u_environmentCubeMapTexture")) {
1027
- uniforms.set(
1028
- "u_environmentCubeMapTexture",
1029
- environmentCubeMapTexture ?? new TextureBinding({ name: "u_environmentCubeMapTexture", required: false, expectedDimension: "cube" })
1030
- );
1031
- }
1032
- uniforms.set("u_environmentMapTextureEnabled", 1);
1033
- if (shader.reflection.uniforms.has("u_environmentCubeMapTextureEnabled")) {
1034
- uniforms.set("u_environmentCubeMapTextureEnabled", environmentCubeMapTexture ? 1 : 0);
1035
- }
1036
- uniforms.set("u_environmentMapTextureIntensity", intensity);
1037
- uniforms.set("u_environmentMapTextureSpecularIntensity", specularIntensity);
1038
- uniforms.set("u_environmentMapTextureRotation", rotation);
1039
- uniforms.set("u_environmentMapTextureMipCount", mipCount);
1040
- if (shader.reflection.uniforms.has("u_environmentMapTextureEncoding")) {
1041
- uniforms.set("u_environmentMapTextureEncoding", encoding === "rgbe" ? 1 : encoding === "linear" ? 2 : 0);
1042
- }
1043
- applyEnvironmentBrdfLutUniforms(environment, item, shader, uniforms);
1044
- }
1045
-
1046
- function clearSampledEnvironmentMapUniforms(
1047
- shader: RenderShaderProgram,
1048
- uniforms: Map<string, UniformValue>
1049
- ): void {
1050
- if (shader.reflection.uniforms.has("u_environmentMapTexture")) {
1051
- uniforms.set("u_environmentMapTexture", new TextureBinding({ name: "u_environmentMapTexture", required: false, expectedDimension: "2d" }));
1052
- }
1053
- if (shader.reflection.uniforms.has("u_environmentCubeMapTexture")) {
1054
- uniforms.set("u_environmentCubeMapTexture", new TextureBinding({ name: "u_environmentCubeMapTexture", required: false, expectedDimension: "cube" }));
1055
- }
1056
- if (shader.reflection.uniforms.has("u_environmentMapTextureEnabled")) {
1057
- uniforms.set("u_environmentMapTextureEnabled", 0);
1058
- }
1059
- if (shader.reflection.uniforms.has("u_environmentCubeMapTextureEnabled")) {
1060
- uniforms.set("u_environmentCubeMapTextureEnabled", 0);
1061
- }
1062
- if (shader.reflection.uniforms.has("u_environmentMapTextureIntensity")) {
1063
- uniforms.set("u_environmentMapTextureIntensity", 0);
1064
- }
1065
- if (shader.reflection.uniforms.has("u_environmentMapTextureSpecularIntensity")) {
1066
- uniforms.set("u_environmentMapTextureSpecularIntensity", 0);
1067
- }
1068
- if (shader.reflection.uniforms.has("u_environmentMapTextureEncoding")) {
1069
- uniforms.set("u_environmentMapTextureEncoding", 0);
1070
- }
1071
- }
1072
-
1073
- function validateEnvironmentTextureBinding(
1074
- label: string,
1075
- binding: TextureBinding,
1076
- expectedDimension: "2d" | "cube",
1077
- item: RenderItem
1078
- ): void {
1079
- const validation = binding.validate();
1080
- if (!validation.ok) {
1081
- throw new RenderDeviceError(`${label} binding validation failed`, "ENVIRONMENT_LIGHTING_CONTRACT", {
1082
- label: item.label,
1083
- diagnostics: validation.diagnostics
1084
- });
1085
- }
1086
- if (binding.texture?.dimension !== expectedDimension) {
1087
- throw new RenderDeviceError(`${label} must bind a ${expectedDimension} texture`, "ENVIRONMENT_LIGHTING_CONTRACT", {
1088
- label: item.label,
1089
- texture: binding.name,
1090
- expectedDimension,
1091
- actualDimension: binding.texture?.dimension ?? null
1092
- });
1093
- }
1094
- }
1095
-
1096
- function applyEnvironmentBrdfLutUniforms(
1097
- environment: EnvironmentLightingOptions,
1098
- item: RenderItem,
1099
- shader: RenderShaderProgram,
1100
- uniforms: Map<string, UniformValue>
1101
- ): void {
1102
- if (!environment.environmentBrdfLutTexture) {
1103
- clearEnvironmentBrdfLutUniforms(shader, uniforms);
1104
- return;
1105
- }
1106
- if (!shader.reflection.uniforms.has("u_environmentBrdfLutTexture") || !shader.reflection.uniforms.has("u_environmentBrdfLutEnabled")) {
1107
- return;
1108
- }
1109
- const validation = environment.environmentBrdfLutTexture.validate();
1110
- if (!validation.ok) {
1111
- throw new RenderDeviceError("Environment BRDF LUT texture binding validation failed", "ENVIRONMENT_LIGHTING_CONTRACT", {
1112
- label: item.label,
1113
- diagnostics: validation.diagnostics
1114
- });
1115
- }
1116
- uniforms.set("u_environmentBrdfLutTexture", environment.environmentBrdfLutTexture);
1117
- uniforms.set("u_environmentBrdfLutEnabled", 1);
1118
- }
1119
-
1120
- function clearEnvironmentBrdfLutUniforms(
1121
- shader: RenderShaderProgram,
1122
- uniforms: Map<string, UniformValue>
1123
- ): void {
1124
- if (shader.reflection.uniforms.has("u_environmentBrdfLutTexture")) {
1125
- uniforms.set("u_environmentBrdfLutTexture", new TextureBinding({ name: "u_environmentBrdfLutTexture", required: false }));
1126
- }
1127
- if (shader.reflection.uniforms.has("u_environmentBrdfLutEnabled")) {
1128
- uniforms.set("u_environmentBrdfLutEnabled", 0);
1129
- }
1130
- }
1131
-
1132
- function toEnvironmentColor(value: readonly number[], field: string, label?: string): readonly number[] {
1133
- if (value.length !== 3 || !isFiniteColor(value)) {
1134
- throw new RenderDeviceError("Procedural environment map colors must contain three finite values in [0, 1]", "ENVIRONMENT_LIGHTING_CONTRACT", {
1135
- label,
1136
- field,
1137
- color: value
1138
- });
1139
- }
1140
- return value;
1141
- }
1142
-
1143
- function applyCameraUniforms(
1144
- cameraPosition: readonly [number, number, number] | undefined,
1145
- item: RenderItem,
1146
- shader: RenderShaderProgram,
1147
- uniforms: Map<string, UniformValue>
1148
- ): void {
1149
- if (!shader.reflection.uniforms.has("u_cameraPosition")) return;
1150
- const position = cameraPosition ?? [0, 0, 1];
1151
- if (position.length !== 3 || position.some((value) => !Number.isFinite(value))) {
1152
- throw new RenderDeviceError("Forward pass camera position must be a finite vec3", "FORWARD_CAMERA_CONTRACT", {
1153
- label: item.label,
1154
- cameraPosition: [...position]
1155
- });
1156
- }
1157
- uniforms.set("u_cameraPosition", [...position]);
1158
- }
1159
-
1160
- function applyAlphaCutoffUniform(
1161
- item: RenderItem,
1162
- shader: RenderShaderProgram,
1163
- uniforms: Map<string, UniformValue>
1164
- ): void {
1165
- if (!shader.reflection.uniforms.has("u_alphaCutoff") || uniforms.has("u_alphaCutoff")) return;
1166
- uniforms.set("u_alphaCutoff", 0);
1167
- }
1168
-
1169
- function applyTransformUniforms(
1170
- item: RenderItem,
1171
- shader: RenderShaderProgram,
1172
- uniforms: Map<string, UniformValue>
1173
- ): void {
1174
- const modelMatrix = shader.reflection.uniforms.has("u_modelMatrix") || shader.reflection.uniforms.has("u_normalMatrix")
1175
- ? toMat4Uniform(item.modelMatrix ?? identityMatrix(), "modelMatrix", item.label)
1176
- : undefined;
1177
- if (item.modelViewProjectionMatrix && shader.reflection.uniforms.has("u_modelViewProjection")) {
1178
- uniforms.set("u_modelViewProjection", toMat4Uniform(item.modelViewProjectionMatrix, "modelViewProjectionMatrix", item.label));
1179
- }
1180
- if (modelMatrix && shader.reflection.uniforms.has("u_modelMatrix")) {
1181
- uniforms.set("u_modelMatrix", modelMatrix);
1182
- }
1183
- if (shader.reflection.uniforms.has("u_normalMatrix")) {
1184
- uniforms.set(
1185
- "u_normalMatrix",
1186
- item.normalMatrix
1187
- ? toMat4Uniform(item.normalMatrix, "normalMatrix", item.label)
1188
- : new Float32Array(normalMatrixFromModel(toMat4Values(modelMatrix ?? identityMatrix(), "modelMatrix", item.label)))
1189
- );
1190
- }
1191
- }
1192
-
1193
- function toMat4Uniform(value: Float32Array | readonly number[], field: string, label?: string): Float32Array {
1194
- if (value.length !== 16 || !isFiniteArrayLike(value)) {
1195
- throw new RenderDeviceError("Render item transform uniforms must be finite mat4 values", "RENDER_ITEM_TRANSFORM_CONTRACT", {
1196
- label,
1197
- field,
1198
- scalars: value.length
1199
- });
1200
- }
1201
- return value instanceof Float32Array ? value : new Float32Array(value);
1202
- }
1203
-
1204
- function toMat4Values(value: Float32Array | readonly number[], field: string, label?: string): Mat4 {
1205
- if (value.length !== 16 || !isFiniteArrayLike(value)) {
1206
- throw new RenderDeviceError("Render item transform uniforms must be finite mat4 values", "RENDER_ITEM_TRANSFORM_CONTRACT", {
1207
- label,
1208
- field,
1209
- scalars: value.length
1210
- });
1211
- }
1212
- return mat4FromArrayLike(value, 0);
1213
- }
1214
-
1215
- function identityMatrix(): readonly number[] {
1216
- return [
1217
- 1, 0, 0, 0,
1218
- 0, 1, 0, 0,
1219
- 0, 0, 1, 0,
1220
- 0, 0, 0, 1
1221
- ];
1222
- }
1223
-
1224
- function getBaseMaterial(material: RenderMaterial): Material {
1225
- return material instanceof MaterialInstance ? material.baseMaterial : material;
1226
- }
1227
-
1228
- function sortForwardRenderItems(
1229
- items: readonly RenderItem[],
1230
- cameraPosition: readonly [number, number, number] | undefined
1231
- ): readonly RenderItem[] {
1232
- return sortRenderQueueItems(items.map((item) => ({
1233
- item,
1234
- bucket: isTransparentRenderItem(item) ? "transparent" : isTransmissionRenderItem(item) ? "transmission" : "opaque",
1235
- depth: cameraPosition ? distanceSquaredFromCamera(item, cameraPosition) : 0,
1236
- pipelineKey: renderItemPipelineKey(item),
1237
- batchKey: renderItemPipelineKey(item),
1238
- instanceCount: item.instanceTransforms ? instanceTransformCount(item) : 1
1239
- }))).items;
1240
- }
1241
-
1242
- function isTransparentRenderItem(item: RenderItem): boolean {
1243
- const material = item.material ?? new UnlitMaterial();
1244
- return getBaseMaterial(material).renderState.blend;
1245
- }
1246
-
1247
- function materialNumericParameter(material: Material, name: string, fallback: number): number {
1248
- const value = material.getParameter(name);
1249
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1250
- }
1251
-
1252
- function isTransmissionRenderItem(item: RenderItem): boolean {
1253
- const material = item.material ?? new UnlitMaterial();
1254
- const baseMaterial = getBaseMaterial(material);
1255
- if (baseMaterial.renderState.blend) return false;
1256
- const transmissionFactor = materialNumericParameter(baseMaterial, "u_transmissionFactor", 0);
1257
- const diffuseTransmissionFactor = materialNumericParameter(baseMaterial, "u_diffuseTransmissionFactor", 0);
1258
- const volumeThicknessFactor = materialNumericParameter(baseMaterial, "u_volumeThicknessFactor", 0);
1259
- return transmissionFactor > 0.001 || diffuseTransmissionFactor > 0.001 || volumeThicknessFactor > 0.001;
1260
- }
1261
-
1262
- function distanceSquaredFromCamera(item: RenderItem, cameraPosition: readonly [number, number, number]): number {
1263
- const center = item.boundingBoxCenter;
1264
- const tx = center?.[0] ?? item.modelMatrix?.[12] ?? 0;
1265
- const ty = center?.[1] ?? item.modelMatrix?.[13] ?? 0;
1266
- const tz = center?.[2] ?? item.modelMatrix?.[14] ?? 0;
1267
- const x = tx - cameraPosition[0];
1268
- const y = ty - cameraPosition[1];
1269
- const z = tz - cameraPosition[2];
1270
- return x * x + y * y + z * z;
1271
- }
1272
-
1273
- function renderItemPipelineKey(item: RenderItem): string {
1274
- const material = item.material ?? new UnlitMaterial();
1275
- const baseMaterial = getBaseMaterial(material);
1276
- const state = baseMaterial.renderState;
1277
- return `${baseMaterial.name}|${state.depthTest ? "dt" : "ndt"}|${state.depthWrite ? "dw" : "ndw"}|${state.cullMode}|${state.blend ? "blend" : "opaque"}`;
1278
- }
1279
-
1280
- function instanceTransformCount(item: RenderItem): number {
1281
- return validateInstanceTransformSource(item).length / 16;
1282
- }
1283
-
1284
- function splitInstanceTransforms(transforms: Float32Array | readonly number[]): Float32Array[] {
1285
- const source = transforms instanceof Float32Array ? transforms : new Float32Array(transforms);
1286
- const batches: Float32Array[] = [];
1287
- for (let offset = 0; offset < source.length; offset += MAX_GPU_INSTANCES * 16) {
1288
- batches.push(source.subarray(offset, Math.min(source.length, offset + MAX_GPU_INSTANCES * 16)));
1289
- }
1290
- return batches;
1291
- }
1292
-
1293
- function applyInstanceUniforms(
1294
- item: RenderItem,
1295
- shader: RenderShaderProgram,
1296
- uniforms: Map<string, UniformValue>
1297
- ): number {
1298
- if (!shader.reflection.uniforms.has("u_instanceMatrices") || !shader.reflection.uniforms.has("u_instanceCount")) {
1299
- throw new RenderDeviceError("Instanced render items require a shader with instance matrix uniforms", "INSTANCING_SHADER_CONTRACT", {
1300
- label: item.label
1301
- });
1302
- }
1303
- const source = validateInstanceTransformSource(item);
1304
- const instanceCount = source.length / 16;
1305
- if (instanceCount > MAX_GPU_INSTANCES) {
1306
- throw new RenderDeviceError("Instanced shader path exceeds the supported uniform instance count", "GPU_INSTANCE_LIMIT", {
1307
- label: item.label,
1308
- instanceCount,
1309
- maxInstances: MAX_GPU_INSTANCES
1310
- });
1311
- }
1312
- const packed = new Float32Array(MAX_GPU_INSTANCES * 16);
1313
- packed.set(source);
1314
- uniforms.set("u_instanceMatrices", packed);
1315
- uniforms.set("u_instanceCount", instanceCount);
1316
- uniforms.set("u_instanceAttributeMode", 0);
1317
- return instanceCount;
1318
- }
1319
-
1320
- function applyInstanceBinding(
1321
- device: RenderDevice,
1322
- item: RenderItem,
1323
- shader: RenderShaderProgram,
1324
- uniforms: Map<string, UniformValue>
1325
- ): { readonly count: number; readonly attributes?: readonly InstanceVertexAttribute[]; readonly buffers?: readonly RenderBuffer[] } {
1326
- const source = validateInstanceTransformSource(item);
1327
- const instanceCount = source.length / 16;
1328
- const extraBindings = createExtraInstanceAttributeBindings(device, item, instanceCount);
1329
- if (supportsInstanceAttributes(shader) && instanceCount > MAX_GPU_INSTANCES) {
1330
- const data = source instanceof Float32Array ? source : new Float32Array(source);
1331
- const buffer = device.createBuffer("vertex", data.byteLength, data);
1332
- uniforms.set("u_instanceCount", instanceCount);
1333
- uniforms.set("u_instanceAttributeMode", 1);
1334
- return {
1335
- count: instanceCount,
1336
- buffers: [buffer, ...extraBindings.buffers],
1337
- attributes: [
1338
- ...INSTANCE_MATRIX_ATTRIBUTE_NAMES.map((shaderName, column) => ({
1339
- buffer,
1340
- shaderName,
1341
- components: 4 as const,
1342
- offset: column * 16,
1343
- stride: 64,
1344
- divisor: 1
1345
- })),
1346
- ...extraBindings.attributes
1347
- ]
1348
- };
1349
- }
1350
- return {
1351
- count: applyInstanceUniforms(item, shader, uniforms),
1352
- ...(extraBindings.attributes.length > 0 ? { attributes: extraBindings.attributes } : {}),
1353
- ...(extraBindings.buffers.length > 0 ? { buffers: extraBindings.buffers } : {})
1354
- };
1355
- }
1356
-
1357
- function createExtraInstanceAttributeBindings(
1358
- device: RenderDevice,
1359
- item: RenderItem,
1360
- instanceCount: number
1361
- ): { readonly attributes: readonly InstanceVertexAttribute[]; readonly buffers: readonly RenderBuffer[] } {
1362
- const descriptors: RenderItemInstanceAttribute[] = [];
1363
- if (item.instanceColors) {
1364
- descriptors.push({
1365
- shaderName: "a_instanceColor",
1366
- components: 4,
1367
- data: item.instanceColors
1368
- });
1369
- }
1370
- if (item.instanceAttributes) descriptors.push(...item.instanceAttributes);
1371
- if (descriptors.length === 0) return { attributes: [], buffers: [] };
1372
-
1373
- const attributes: InstanceVertexAttribute[] = [];
1374
- const buffers: RenderBuffer[] = [];
1375
- for (const descriptor of descriptors) {
1376
- validateInstanceAttributeDescriptor(descriptor, instanceCount, item.label);
1377
- const data = descriptor.data instanceof Float32Array ? descriptor.data : new Float32Array(descriptor.data);
1378
- const stride = descriptor.components * 4;
1379
- const buffer = device.createBuffer("vertex", data.byteLength, data);
1380
- buffers.push(buffer);
1381
- attributes.push({
1382
- buffer,
1383
- shaderName: descriptor.shaderName,
1384
- components: descriptor.components,
1385
- offset: 0,
1386
- stride,
1387
- normalized: descriptor.normalized ?? false,
1388
- divisor: descriptor.divisor ?? 1
1389
- });
1390
- }
1391
- return { attributes, buffers };
1392
- }
1393
-
1394
- function validateInstanceAttributeDescriptor(descriptor: RenderItemInstanceAttribute, instanceCount: number, label: string | undefined): void {
1395
- if (!descriptor.shaderName.trim()) {
1396
- throw new RenderDeviceError("Instance attribute shaderName is required", "INSTANCE_ATTRIBUTE_CONTRACT", { label });
1397
- }
1398
- if (![1, 2, 3, 4].includes(descriptor.components)) {
1399
- throw new RenderDeviceError("Instance attribute components must be 1, 2, 3, or 4", "INSTANCE_ATTRIBUTE_CONTRACT", {
1400
- label,
1401
- shaderName: descriptor.shaderName,
1402
- components: descriptor.components
1403
- });
1404
- }
1405
- const data = descriptor.data instanceof Float32Array ? descriptor.data : new Float32Array(descriptor.data);
1406
- if (data.length !== instanceCount * descriptor.components || !Array.from(data).every(Number.isFinite)) {
1407
- throw new RenderDeviceError("Instance attribute data must contain finite values for every instance", "INSTANCE_ATTRIBUTE_CONTRACT", {
1408
- label,
1409
- shaderName: descriptor.shaderName,
1410
- components: descriptor.components,
1411
- dataLength: data.length,
1412
- expectedLength: instanceCount * descriptor.components
1413
- });
1414
- }
1415
- }
1416
-
1417
- function resolveRenderGeometry(item: RenderItem): Geometry {
1418
- if (item.morphTargets === undefined && item.morphWeights === undefined) {
1419
- return item.geometry;
1420
- }
1421
- if (!item.morphTargets || !item.morphWeights) {
1422
- throw new RenderDeviceError("Morph render items require both morphTargets and morphWeights", "MORPH_TARGET_CONTRACT", {
1423
- label: item.label,
1424
- targetCount: item.morphTargets?.length ?? 0,
1425
- weightCount: item.morphWeights?.length ?? 0
1426
- });
1427
- }
1428
- return applyMorphTargets(item.geometry, item.morphTargets, item.morphWeights);
1429
- }
1430
-
1431
- function applyGpuMorphUniforms(
1432
- item: RenderItem,
1433
- shader: RenderShaderProgram,
1434
- uniforms: Map<string, UniformValue>
1435
- ): boolean {
1436
- if (
1437
- !shader.reflection.uniforms.has("u_morphPositionDeltas") ||
1438
- !shader.reflection.uniforms.has("u_morphWeights") ||
1439
- !shader.reflection.uniforms.has("u_morphTargetCount")
1440
- ) {
1441
- return false;
1442
- }
1443
- if (!item.morphTargets || !item.morphWeights) {
1444
- throw new RenderDeviceError("Morph render items require both morphTargets and morphWeights", "MORPH_TARGET_CONTRACT", {
1445
- label: item.label,
1446
- targetCount: item.morphTargets?.length ?? 0,
1447
- weightCount: item.morphWeights?.length ?? 0
1448
- });
1449
- }
1450
- if (item.morphTargets.length !== item.morphWeights.length) {
1451
- throw new RenderDeviceError("Morph target count must match morph weight count", "MORPH_TARGET_CONTRACT", {
1452
- label: item.label,
1453
- targetCount: item.morphTargets.length,
1454
- weightCount: item.morphWeights.length
1455
- });
1456
- }
1457
- // Counts beyond the uniform fast-path capacity are not an error: fall back to the CPU morph
1458
- // (resolveRenderGeometry -> applyMorphTargets), which is unlimited and morphs normals + tangents
1459
- // so lighting follows the deformation. The texture-backed GPU plan (createMorphTargetPlan) packs
1460
- // the same data for the texture path; see MorphTargetPlan.ts.
1461
- if (item.morphTargets.length > MAX_GPU_MORPH_TARGETS || item.geometry.vertexBuffer.vertexCount > MAX_GPU_MORPH_VERTICES) {
1462
- return false;
1463
- }
1464
- const packed = new Float32Array(MAX_GPU_MORPH_TARGETS * MAX_GPU_MORPH_VERTICES * 4);
1465
- const packedNormals = new Float32Array(MAX_GPU_MORPH_TARGETS * MAX_GPU_MORPH_VERTICES * 4);
1466
- const weights = new Float32Array(MAX_GPU_MORPH_TARGETS);
1467
- for (let targetIndex = 0; targetIndex < item.morphTargets.length; targetIndex += 1) {
1468
- const target = item.morphTargets[targetIndex]!;
1469
- if (!target.positions || target.positions.length < item.geometry.vertexBuffer.vertexCount) {
1470
- throw new RenderDeviceError("GPU morph shader path requires position deltas for every source vertex", "GPU_MORPH_TARGET_CONTRACT", {
1471
- label: item.label,
1472
- targetIndex,
1473
- vertexCount: item.geometry.vertexBuffer.vertexCount,
1474
- deltaCount: target.positions?.length ?? 0
1475
- });
1476
- }
1477
- const weight = item.morphWeights[targetIndex] ?? 0;
1478
- if (!Number.isFinite(weight)) {
1479
- throw new RenderDeviceError("GPU morph weights must be finite", "GPU_MORPH_TARGET_CONTRACT", {
1480
- label: item.label,
1481
- targetIndex,
1482
- weight
1483
- });
1484
- }
1485
- weights[targetIndex] = weight;
1486
- for (let vertex = 0; vertex < item.geometry.vertexBuffer.vertexCount; vertex += 1) {
1487
- const delta = target.positions[vertex]!;
1488
- if (delta.length !== 3 || !Number.isFinite(delta[0]) || !Number.isFinite(delta[1]) || !Number.isFinite(delta[2])) {
1489
- throw new RenderDeviceError("GPU morph position deltas must be finite vec3 values", "GPU_MORPH_TARGET_CONTRACT", {
1490
- label: item.label,
1491
- targetIndex,
1492
- vertex
1493
- });
1494
- }
1495
- const offset = (targetIndex * MAX_GPU_MORPH_VERTICES + vertex) * 4;
1496
- packed[offset] = delta[0];
1497
- packed[offset + 1] = delta[1];
1498
- packed[offset + 2] = delta[2];
1499
- const normal = target.normals?.[vertex];
1500
- if (normal && normal.length === 3) {
1501
- packedNormals[offset] = normal[0];
1502
- packedNormals[offset + 1] = normal[1];
1503
- packedNormals[offset + 2] = normal[2];
1504
- }
1505
- }
1506
- }
1507
- uniforms.set("u_morphPositionDeltas", packed);
1508
- uniforms.set("u_morphWeights", weights);
1509
- uniforms.set("u_morphTargetCount", item.morphTargets.length);
1510
- // Normal deltas are uploaded only when the bound shader declares the uniform (lit morph variants);
1511
- // the default unlit morph shader ignores them.
1512
- if (shader.reflection.uniforms.has("u_morphNormalDeltas")) {
1513
- uniforms.set("u_morphNormalDeltas", packedNormals);
1514
- }
1515
- return true;
1516
- }
1517
-
1518
- function applySkinningUniforms(
1519
- skinning: SkinningPaletteBinding,
1520
- material: Material,
1521
- shader: RenderShaderProgram,
1522
- uniforms: Map<string, UniformValue>
1523
- ): void {
1524
- if (!shader.reflection.uniforms.has("u_jointMatrices") || !shader.reflection.uniforms.has("u_jointCount")) {
1525
- throw new RenderDeviceError("Skinned render item requires a shader with joint palette uniforms", "SKINNING_SHADER_CONTRACT", {
1526
- material: material.name
1527
- });
1528
- }
1529
- if (!Number.isInteger(skinning.jointCount) || skinning.jointCount <= 0 || skinning.jointCount > 96) {
1530
- throw new RenderDeviceError("Skinning jointCount must be an integer in [1, 96]", "INVALID_SKINNING_PALETTE", {
1531
- jointCount: skinning.jointCount
1532
- });
1533
- }
1534
- if (skinning.matrices.length !== skinning.jointCount * 16) {
1535
- throw new RenderDeviceError("Skinning matrix palette length must equal jointCount * 16", "INVALID_SKINNING_PALETTE", {
1536
- jointCount: skinning.jointCount,
1537
- matrixScalars: skinning.matrices.length
1538
- });
1539
- }
1540
- if (!isFiniteArrayLike(skinning.matrices)) {
1541
- throw new RenderDeviceError("Skinning matrix palette must contain finite values", "INVALID_SKINNING_PALETTE", {
1542
- jointCount: skinning.jointCount
1543
- });
1544
- }
1545
- uniforms.set("u_jointCount", skinning.jointCount);
1546
- uniforms.set("u_jointMatrices", skinning.matrices);
1547
- }
1548
-
1549
- function validateSkinningGeometryContract(item: RenderItem, skinning: SkinningPaletteBinding): void {
1550
- const format = item.geometry.vertexBuffer.format;
1551
- if (!format.hasAttribute("joints") || !format.hasAttribute("weights")) {
1552
- throw new RenderDeviceError("Skinned render item geometry must include joints and weights attributes", "SKINNING_GEOMETRY_CONTRACT", {
1553
- label: item.label,
1554
- jointCount: skinning.jointCount,
1555
- vertexFormat: format.attributes.map((attribute) => attribute.shaderName),
1556
- missingAttributes: [
1557
- ...(format.hasAttribute("joints") ? [] : ["a_joints"]),
1558
- ...(format.hasAttribute("weights") ? [] : ["a_weights"])
1559
- ]
1560
- });
1561
- }
1562
-
1563
- const jointsAttribute = format.getAttribute("joints");
1564
- const weightsAttribute = format.getAttribute("weights");
1565
- if (jointsAttribute.components !== 4 || weightsAttribute.components !== 4) {
1566
- throw new RenderDeviceError("Skinned render item geometry must use four joint and four weight influences per vertex", "SKINNING_GEOMETRY_CONTRACT", {
1567
- label: item.label,
1568
- jointCount: skinning.jointCount,
1569
- jointComponents: jointsAttribute.components,
1570
- weightComponents: weightsAttribute.components
1571
- });
1572
- }
1573
-
1574
- for (let vertex = 0; vertex < item.geometry.vertexBuffer.vertexCount; vertex += 1) {
1575
- const joints = item.geometry.vertexBuffer.getAttribute(vertex, "joints");
1576
- const weights = item.geometry.vertexBuffer.getAttribute(vertex, "weights");
1577
- let weightSum = 0;
1578
- for (let influence = 0; influence < 4; influence += 1) {
1579
- const joint = joints[influence] ?? 0;
1580
- const weight = weights[influence] ?? 0;
1581
- if (!Number.isFinite(weight) || weight < 0) {
1582
- throw new RenderDeviceError("Skinned render item weights must be finite non-negative values", "SKINNING_GEOMETRY_CONTRACT", {
1583
- label: item.label,
1584
- jointCount: skinning.jointCount,
1585
- vertex,
1586
- influence,
1587
- weight
1588
- });
1589
- }
1590
- if (!Number.isFinite(joint) || !Number.isInteger(joint) || joint < 0) {
1591
- throw new RenderDeviceError("Skinned render item joints must be finite non-negative integer indices", "SKINNING_GEOMETRY_CONTRACT", {
1592
- label: item.label,
1593
- jointCount: skinning.jointCount,
1594
- vertex,
1595
- influence,
1596
- joint
1597
- });
1598
- }
1599
- if (weight > 0 && joint >= skinning.jointCount) {
1600
- throw new RenderDeviceError("Skinned render item joint indices must be within the uploaded skinning palette", "SKINNING_GEOMETRY_CONTRACT", {
1601
- label: item.label,
1602
- jointCount: skinning.jointCount,
1603
- vertex,
1604
- influence,
1605
- joint,
1606
- weight
1607
- });
1608
- }
1609
- weightSum += weight;
1610
- }
1611
- if (weightSum <= 0) {
1612
- throw new RenderDeviceError("Skinned render item weights must sum to a positive value", "SKINNING_GEOMETRY_CONTRACT", {
1613
- label: item.label,
1614
- jointCount: skinning.jointCount,
1615
- vertex,
1616
- weightSum
1617
- });
1618
- }
1619
- if (Math.abs(weightSum - 1) > 0.02) {
1620
- throw new RenderDeviceError("Skinned render item weights must be normalized before GPU skinning", "SKINNING_GEOMETRY_CONTRACT", {
1621
- label: item.label,
1622
- jointCount: skinning.jointCount,
1623
- vertex,
1624
- weightSum
1625
- });
1626
- }
1627
- }
1628
- }
1629
-
1630
- function validateInstanceTransformSource(item: RenderItem): Float32Array | readonly number[] {
1631
- const source = item.instanceTransforms ?? [];
1632
- if (source.length === 0 || source.length % 16 !== 0) {
1633
- throw new RenderDeviceError("Instance transforms must contain one or more mat4 values", "INSTANCING_CONTRACT", {
1634
- label: item.label,
1635
- scalars: source.length
1636
- });
1637
- }
1638
- if (!isFiniteArrayLike(source)) {
1639
- throw new RenderDeviceError("Instance transforms must contain finite mat4 values", "INSTANCING_CONTRACT", {
1640
- label: item.label
1641
- });
1642
- }
1643
- return source;
1644
- }
1645
-
1646
- function isFiniteArrayLike(values: ArrayLike<number>): boolean {
1647
- for (let index = 0; index < values.length; index += 1) {
1648
- if (!Number.isFinite(values[index])) return false;
1649
- }
1650
- return true;
1651
- }
1652
-
1653
- function isFiniteColor(values: ArrayLike<number>): boolean {
1654
- for (let index = 0; index < values.length; index += 1) {
1655
- const channel = values[index];
1656
- if (!Number.isFinite(channel) || channel < 0 || channel > 1) return false;
1657
- }
1658
- return true;
1659
- }
1660
-
1661
- function mat4FromArrayLike(values: ArrayLike<number>, offset: number): Mat4 {
1662
- return [
1663
- values[offset]!, values[offset + 1]!, values[offset + 2]!, values[offset + 3]!,
1664
- values[offset + 4]!, values[offset + 5]!, values[offset + 6]!, values[offset + 7]!,
1665
- values[offset + 8]!, values[offset + 9]!, values[offset + 10]!, values[offset + 11]!,
1666
- values[offset + 12]!, values[offset + 13]!, values[offset + 14]!, values[offset + 15]!
1667
- ];
1668
- }