@aura3d/rendering 1.4.5 → 1.5.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 (203) hide show
  1. package/dist/CameraFraming.d.ts +77 -0
  2. package/dist/CameraFraming.d.ts.map +1 -1
  3. package/dist/CameraFraming.js +176 -1
  4. package/dist/CameraFraming.js.map +1 -1
  5. package/dist/CascadedShadowMaps.d.ts +9 -1
  6. package/dist/CascadedShadowMaps.d.ts.map +1 -1
  7. package/dist/CascadedShadowMaps.js +68 -0
  8. package/dist/CascadedShadowMaps.js.map +1 -1
  9. package/dist/ClusteredForwardLighting.d.ts +28 -0
  10. package/dist/ClusteredForwardLighting.d.ts.map +1 -0
  11. package/dist/ClusteredForwardLighting.js +156 -0
  12. package/dist/ClusteredForwardLighting.js.map +1 -0
  13. package/dist/DepthPass.d.ts +14 -1
  14. package/dist/DepthPass.d.ts.map +1 -1
  15. package/dist/DepthPass.js +19 -4
  16. package/dist/DepthPass.js.map +1 -1
  17. package/dist/EnvironmentMapResources.d.ts +59 -1
  18. package/dist/EnvironmentMapResources.d.ts.map +1 -1
  19. package/dist/EnvironmentMapResources.js +140 -100
  20. package/dist/EnvironmentMapResources.js.map +1 -1
  21. package/dist/EnvironmentPlatform.d.ts +3 -2
  22. package/dist/EnvironmentPlatform.d.ts.map +1 -1
  23. package/dist/EnvironmentPlatform.js +99 -44
  24. package/dist/EnvironmentPlatform.js.map +1 -1
  25. package/dist/ForwardPass.d.ts +37 -0
  26. package/dist/ForwardPass.d.ts.map +1 -1
  27. package/dist/ForwardPass.js +234 -7
  28. package/dist/ForwardPass.js.map +1 -1
  29. package/dist/Geometry.d.ts +19 -0
  30. package/dist/Geometry.d.ts.map +1 -1
  31. package/dist/Geometry.js +64 -0
  32. package/dist/Geometry.js.map +1 -1
  33. package/dist/IBL.d.ts.map +1 -1
  34. package/dist/IBL.js +0 -1
  35. package/dist/IBL.js.map +1 -1
  36. package/dist/LightCollector.d.ts +5 -1
  37. package/dist/LightCollector.d.ts.map +1 -1
  38. package/dist/LightCollector.js +16 -3
  39. package/dist/LightCollector.js.map +1 -1
  40. package/dist/LightUniforms.d.ts +26 -1
  41. package/dist/LightUniforms.d.ts.map +1 -1
  42. package/dist/LightUniforms.js +140 -8
  43. package/dist/LightUniforms.js.map +1 -1
  44. package/dist/LightingRig.d.ts +77 -0
  45. package/dist/LightingRig.d.ts.map +1 -1
  46. package/dist/LightingRig.js +100 -11
  47. package/dist/LightingRig.js.map +1 -1
  48. package/dist/MeshConsolidation.d.ts +63 -0
  49. package/dist/MeshConsolidation.d.ts.map +1 -0
  50. package/dist/MeshConsolidation.js +251 -0
  51. package/dist/MeshConsolidation.js.map +1 -0
  52. package/dist/PMREM.d.ts +13 -1
  53. package/dist/PMREM.d.ts.map +1 -1
  54. package/dist/PMREM.js +18 -4
  55. package/dist/PMREM.js.map +1 -1
  56. package/dist/PostProcessPass.d.ts +24 -2
  57. package/dist/PostProcessPass.d.ts.map +1 -1
  58. package/dist/PostProcessPass.js +136 -18
  59. package/dist/PostProcessPass.js.map +1 -1
  60. package/dist/PrimitiveSubmissionAudit.d.ts +195 -0
  61. package/dist/PrimitiveSubmissionAudit.d.ts.map +1 -0
  62. package/dist/PrimitiveSubmissionAudit.js +280 -0
  63. package/dist/PrimitiveSubmissionAudit.js.map +1 -0
  64. package/dist/ProductTurntableFixtures.d.ts.map +1 -1
  65. package/dist/ProductTurntableFixtures.js +93 -13
  66. package/dist/ProductTurntableFixtures.js.map +1 -1
  67. package/dist/ReflectionProbe.d.ts +59 -0
  68. package/dist/ReflectionProbe.d.ts.map +1 -1
  69. package/dist/ReflectionProbe.js +224 -0
  70. package/dist/ReflectionProbe.js.map +1 -1
  71. package/dist/ReflectionSurfaces.d.ts +4 -2
  72. package/dist/ReflectionSurfaces.d.ts.map +1 -1
  73. package/dist/ReflectionSurfaces.js +13 -8
  74. package/dist/ReflectionSurfaces.js.map +1 -1
  75. package/dist/RenderBackend.d.ts +1 -1
  76. package/dist/RenderBackend.d.ts.map +1 -1
  77. package/dist/RenderBackend.js +5 -1
  78. package/dist/RenderBackend.js.map +1 -1
  79. package/dist/RenderDevice.d.ts +13 -2
  80. package/dist/RenderDevice.d.ts.map +1 -1
  81. package/dist/RenderDevice.js +5 -2
  82. package/dist/RenderDevice.js.map +1 -1
  83. package/dist/Renderer.d.ts +66 -3
  84. package/dist/Renderer.d.ts.map +1 -1
  85. package/dist/Renderer.js +417 -64
  86. package/dist/Renderer.js.map +1 -1
  87. package/dist/RendererPostprocessPlan.d.ts +10 -3
  88. package/dist/RendererPostprocessPlan.d.ts.map +1 -1
  89. package/dist/RendererPostprocessPlan.js +25 -5
  90. package/dist/RendererPostprocessPlan.js.map +1 -1
  91. package/dist/RendererVisualPipelineReport.d.ts +3 -2
  92. package/dist/RendererVisualPipelineReport.d.ts.map +1 -1
  93. package/dist/RendererVisualPipelineReport.js +2 -0
  94. package/dist/RendererVisualPipelineReport.js.map +1 -1
  95. package/dist/ScreenSpaceLineMaterial.d.ts +34 -0
  96. package/dist/ScreenSpaceLineMaterial.d.ts.map +1 -0
  97. package/dist/ScreenSpaceLineMaterial.js +93 -0
  98. package/dist/ScreenSpaceLineMaterial.js.map +1 -0
  99. package/dist/ShaderChunks.d.ts +7 -0
  100. package/dist/ShaderChunks.d.ts.map +1 -1
  101. package/dist/ShaderChunks.js +137 -0
  102. package/dist/ShaderChunks.js.map +1 -1
  103. package/dist/ShaderLibrary.d.ts +17 -0
  104. package/dist/ShaderLibrary.d.ts.map +1 -1
  105. package/dist/ShaderLibrary.js +940 -98
  106. package/dist/ShaderLibrary.js.map +1 -1
  107. package/dist/ShadowPass.d.ts +10 -0
  108. package/dist/ShadowPass.d.ts.map +1 -1
  109. package/dist/ShadowPass.js +8 -1
  110. package/dist/ShadowPass.js.map +1 -1
  111. package/dist/SkinnedLitMaterial.d.ts +6 -0
  112. package/dist/SkinnedLitMaterial.d.ts.map +1 -1
  113. package/dist/SkinnedLitMaterial.js +22 -8
  114. package/dist/SkinnedLitMaterial.js.map +1 -1
  115. package/dist/SkinnedUnlitMaterial.d.ts +8 -0
  116. package/dist/SkinnedUnlitMaterial.d.ts.map +1 -1
  117. package/dist/SkinnedUnlitMaterial.js +26 -9
  118. package/dist/SkinnedUnlitMaterial.js.map +1 -1
  119. package/dist/SpecularPrefilter.d.ts +42 -0
  120. package/dist/SpecularPrefilter.d.ts.map +1 -0
  121. package/dist/SpecularPrefilter.js +480 -0
  122. package/dist/SpecularPrefilter.js.map +1 -0
  123. package/dist/TerrainFixtures.d.ts +24 -0
  124. package/dist/TerrainFixtures.d.ts.map +1 -1
  125. package/dist/TerrainFixtures.js +96 -0
  126. package/dist/TerrainFixtures.js.map +1 -1
  127. package/dist/Texture.d.ts +7 -0
  128. package/dist/Texture.d.ts.map +1 -1
  129. package/dist/Texture.js +9 -0
  130. package/dist/Texture.js.map +1 -1
  131. package/dist/VertexFormat.d.ts +34 -1
  132. package/dist/VertexFormat.d.ts.map +1 -1
  133. package/dist/VertexFormat.js +61 -0
  134. package/dist/VertexFormat.js.map +1 -1
  135. package/dist/WebGL2Device.d.ts +48 -0
  136. package/dist/WebGL2Device.d.ts.map +1 -1
  137. package/dist/WebGL2Device.js +1298 -44
  138. package/dist/WebGL2Device.js.map +1 -1
  139. package/dist/WebGPUDevice.d.ts +7 -2
  140. package/dist/WebGPUDevice.d.ts.map +1 -1
  141. package/dist/WebGPUDevice.js +111 -17
  142. package/dist/WebGPUDevice.js.map +1 -1
  143. package/dist/WebGPUSkinningLimits.d.ts +14 -0
  144. package/dist/WebGPUSkinningLimits.d.ts.map +1 -0
  145. package/dist/WebGPUSkinningLimits.js +14 -0
  146. package/dist/WebGPUSkinningLimits.js.map +1 -0
  147. package/dist/cartoon/CartoonMaterialStyle.d.ts +19 -0
  148. package/dist/cartoon/CartoonMaterialStyle.d.ts.map +1 -0
  149. package/dist/cartoon/CartoonMaterialStyle.js +13 -0
  150. package/dist/cartoon/CartoonMaterialStyle.js.map +1 -0
  151. package/dist/cartoon/CartoonRenderPreset.d.ts +36 -0
  152. package/dist/cartoon/CartoonRenderPreset.d.ts.map +1 -0
  153. package/dist/cartoon/CartoonRenderPreset.js +23 -0
  154. package/dist/cartoon/CartoonRenderPreset.js.map +1 -0
  155. package/dist/cartoon/CartoonToonMaterial.d.ts +130 -0
  156. package/dist/cartoon/CartoonToonMaterial.d.ts.map +1 -0
  157. package/dist/cartoon/CartoonToonMaterial.js +303 -0
  158. package/dist/cartoon/CartoonToonMaterial.js.map +1 -0
  159. package/dist/cartoon/CartoonVisualQuality.d.ts +28 -0
  160. package/dist/cartoon/CartoonVisualQuality.d.ts.map +1 -0
  161. package/dist/cartoon/CartoonVisualQuality.js +39 -0
  162. package/dist/cartoon/CartoonVisualQuality.js.map +1 -0
  163. package/dist/cartoon/applyCartoonRenderPreset.d.ts +79 -0
  164. package/dist/cartoon/applyCartoonRenderPreset.d.ts.map +1 -0
  165. package/dist/cartoon/applyCartoonRenderPreset.js +88 -0
  166. package/dist/cartoon/applyCartoonRenderPreset.js.map +1 -0
  167. package/dist/cartoon/index.d.ts +6 -0
  168. package/dist/cartoon/index.d.ts.map +1 -0
  169. package/dist/cartoon/index.js +6 -0
  170. package/dist/cartoon/index.js.map +1 -0
  171. package/dist/index.d.ts +29 -18
  172. package/dist/index.d.ts.map +1 -1
  173. package/dist/index.js +16 -10
  174. package/dist/index.js.map +1 -1
  175. package/dist/postprocess/NativeLdrEffectLuts.d.ts +93 -0
  176. package/dist/postprocess/NativeLdrEffectLuts.d.ts.map +1 -0
  177. package/dist/postprocess/NativeLdrEffectLuts.js +202 -0
  178. package/dist/postprocess/NativeLdrEffectLuts.js.map +1 -0
  179. package/dist/production-runtime/PBRHDRPipeline.d.ts.map +1 -1
  180. package/dist/production-runtime/PBRHDRPipeline.js +3 -0
  181. package/dist/production-runtime/PBRHDRPipeline.js.map +1 -1
  182. package/dist/production-runtime/ProductionRendererTypes.d.ts +1 -0
  183. package/dist/production-runtime/ProductionRendererTypes.d.ts.map +1 -1
  184. package/dist/production-runtime/ProductionRendererTypes.js.map +1 -1
  185. package/dist/production-runtime/ProductionWebGL2Renderer.d.ts.map +1 -1
  186. package/dist/production-runtime/ProductionWebGL2Renderer.js +5 -3
  187. package/dist/production-runtime/ProductionWebGL2Renderer.js.map +1 -1
  188. package/dist/production-runtime/ProductionWebGPURenderer.d.ts.map +1 -1
  189. package/dist/production-runtime/ProductionWebGPURenderer.js +9 -5
  190. package/dist/production-runtime/ProductionWebGPURenderer.js.map +1 -1
  191. package/dist/production-runtime/TransmissionBackdropCapture.d.ts +5 -0
  192. package/dist/production-runtime/TransmissionBackdropCapture.d.ts.map +1 -1
  193. package/dist/production-runtime/TransmissionBackdropCapture.js +40 -0
  194. package/dist/production-runtime/TransmissionBackdropCapture.js.map +1 -1
  195. package/dist/production-runtime/environment/HDRLoader.d.ts +6 -0
  196. package/dist/production-runtime/environment/HDRLoader.d.ts.map +1 -1
  197. package/dist/production-runtime/environment/HDRLoader.js +19 -0
  198. package/dist/production-runtime/environment/HDRLoader.js.map +1 -1
  199. package/dist/production-runtime/index.d.ts +3 -1
  200. package/dist/production-runtime/index.d.ts.map +1 -1
  201. package/dist/production-runtime/index.js +1 -0
  202. package/dist/production-runtime/index.js.map +1 -1
  203. package/package.json +3 -3
@@ -108,6 +108,16 @@ export const DEFAULT_SKINNED_UNLIT_SHADER_NAME = "aura3d/skinned-unlit";
108
108
  export const DEFAULT_SKINNED_UNLIT_SHADER_MARKER = "@aura3d-shader:skinned-unlit";
109
109
  export const DEFAULT_SKINNED_LIT_SHADER_NAME = "aura3d/skinned-lit";
110
110
  export const DEFAULT_SKINNED_LIT_SHADER_MARKER = "@aura3d-shader:skinned-lit";
111
+ /**
112
+ * Eight-influence skinned shaders. These read a second `JOINTS_1`/`WEIGHTS_1`
113
+ * attribute set and select between uniform-array and data-texture joint palettes at
114
+ * runtime through `u_jointPaletteMode`, so a single program serves both small rigs
115
+ * and rigs above the uniform-array joint limit.
116
+ */
117
+ export const DEFAULT_SKINNED_UNLIT_EIGHT_INFLUENCE_SHADER_NAME = "aura3d/skinned-unlit-8";
118
+ export const DEFAULT_SKINNED_UNLIT_EIGHT_INFLUENCE_SHADER_MARKER = "@aura3d-shader:skinned-unlit-8";
119
+ export const DEFAULT_SKINNED_LIT_EIGHT_INFLUENCE_SHADER_NAME = "aura3d/skinned-lit-8";
120
+ export const DEFAULT_SKINNED_LIT_EIGHT_INFLUENCE_SHADER_MARKER = "@aura3d-shader:skinned-lit-8";
111
121
  export const DEFAULT_MORPH_UNLIT_SHADER_NAME = "aura3d/morph-unlit";
112
122
  export const DEFAULT_MORPH_UNLIT_SHADER_MARKER = "@aura3d-shader:morph-unlit";
113
123
  export const DEFAULT_NORMAL_MAPPED_PBR_SHADER_NAME = "aura3d/pbr-normal-map";
@@ -121,6 +131,13 @@ export const DEFAULT_TEXTURED_PBR_IRIDESCENCE_TEXTURES_VARIANT = "iridescence-te
121
131
  export const DEFAULT_TEXTURED_PBR_CLEARCOAT_TRANSMISSION_VOLUME_TEXTURES_VARIANT = "clearcoat-transmission-volume-textures";
122
132
  export const DEFAULT_TEXTURED_PBR_CLEARCOAT_SPECULAR_TEXTURES_VARIANT = "clearcoat-specular-textures";
123
133
  export const DEFAULT_TEXTURED_PBR_SPECULAR_SHEEN_ANISOTROPY_IRIDESCENCE_TEXTURES_VARIANT = "specular-sheen-anisotropy-iridescence-textures";
134
+ /**
135
+ * True screen-space fat lines, equivalent in intent to Three.js `Line2`/`LineMaterial`.
136
+ * Width is specified in pixels and stays constant across distance, FOV, viewport size,
137
+ * and device pixel ratio.
138
+ */
139
+ export const DEFAULT_SCREEN_SPACE_LINE_SHADER_NAME = "aura3d/screen-space-line";
140
+ export const DEFAULT_SCREEN_SPACE_LINE_SHADER_MARKER = "@aura3d-shader:screen-space-line";
124
141
  export const DEFAULT_DEPTH_SHADER_NAME = "aura3d/depth";
125
142
  export const DEFAULT_DEPTH_SHADER_MARKER = "@aura3d-shader:depth";
126
143
  export const DEFAULT_ENVIRONMENT_BACKGROUND_SHADER_NAME = "aura3d/environment-background";
@@ -287,7 +304,12 @@ uniform float u_iridescenceThicknessMinimum;
287
304
  uniform float u_iridescenceThicknessMaximum;
288
305
  uniform float u_dispersion;
289
306
  uniform float u_lightCount;
290
- uniform vec4 u_lightData[64];
307
+ uniform vec4 u_lightData[96];
308
+ uniform float u_clusteredLightEnabled;
309
+ uniform vec2 u_clusterGridSize;
310
+ uniform vec2 u_clusterViewportSize;
311
+ uniform sampler2D u_clusterLightData;
312
+ uniform sampler2D u_clusterLightIndices;
291
313
  uniform sampler2D u_shadowMapTexture;
292
314
  uniform float u_shadowMapEnabled;
293
315
  uniform mat4 u_shadowMapMatrix;
@@ -396,8 +418,18 @@ float a3dForwardShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirectio
396
418
  vec3 receiverNormal = normalize(normal);
397
419
  vec3 receiverLightDirection = lightDirection / max(length(lightDirection), 0.0001);
398
420
  float normalDotLight = clamp(abs(dot(receiverNormal, receiverLightDirection)), 0.0, 1.0);
399
- float slopeReceiverBias = (1.0 - normalDotLight) * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
400
- float receiverDepth = projected.z * 0.5 + 0.5 - u_shadowMapBias - slopeReceiverBias;
421
+ // Slope-scaled depth bias must be evaluated per PCF sample, not once for the kernel
422
+ // centre. A sample offset N texels away on a receiver sloped relative to the light sees a
423
+ // depth difference proportional to N, so a centre-only bias under-compensates every outer
424
+ // tap and the receiver shadows itself. Scaling by each sample's own texel distance keeps
425
+ // wide kernels acne-free without inflating the constant bias into peter-panning.
426
+ // The depth gradient across one shadow texel is tan(angle between receiver normal and
427
+ // light), not (1 - N.L). The linear form collapses toward zero far faster than the real
428
+ // gradient grows, so it under-biases exactly the grazing angles that need the most
429
+ // compensation. Clamped so a near-perpendicular receiver cannot demand unbounded bias.
430
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
431
+ float slopeTexelBias = slopeTangent * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
432
+ float projectedDepth = projected.z * 0.5 + 0.5;
401
433
  float shadowed = 0.0;
402
434
  float totalWeight = 0.0;
403
435
  int sampleCount = clamp(int(u_shadowPcfSampleCount), 1, 32);
@@ -407,6 +439,8 @@ float a3dForwardShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirectio
407
439
  float weight = max(sampleData.z, 0.0);
408
440
  vec2 offset = sampleData.xy * u_shadowMapTexelSize;
409
441
  float storedDepth = texture(u_shadowMapTexture, uv + offset).r;
442
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
443
+ float receiverDepth = projectedDepth - u_shadowMapBias - slopeTexelBias * sampleTexelDistance;
410
444
  shadowed += (receiverDepth > storedDepth ? 1.0 : 0.0) * weight;
411
445
  totalWeight += weight;
412
446
  }
@@ -434,8 +468,18 @@ float a3dPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection)
434
468
  vec3 receiverNormal = normalize(normal);
435
469
  vec3 receiverLightDirection = lightDirection / max(length(lightDirection), 0.0001);
436
470
  float normalDotLight = clamp(abs(dot(receiverNormal, receiverLightDirection)), 0.0, 1.0);
437
- float slopeReceiverBias = (1.0 - normalDotLight) * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
438
- float receiverDepth = projected.z * 0.5 + 0.5 - u_pointShadowBias - slopeReceiverBias;
471
+ // Slope-scaled depth bias must be evaluated per PCF sample, not once for the kernel
472
+ // centre. A sample offset N texels away on a receiver sloped relative to the light sees a
473
+ // depth difference proportional to N, so a centre-only bias under-compensates every outer
474
+ // tap and the receiver shadows itself. Scaling by each sample's own texel distance keeps
475
+ // wide kernels acne-free without inflating the constant bias into peter-panning.
476
+ // The depth gradient across one shadow texel is tan(angle between receiver normal and
477
+ // light), not (1 - N.L). The linear form collapses toward zero far faster than the real
478
+ // gradient grows, so it under-biases exactly the grazing angles that need the most
479
+ // compensation. Clamped so a near-perpendicular receiver cannot demand unbounded bias.
480
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
481
+ float slopeTexelBias = slopeTangent * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
482
+ float projectedDepth = projected.z * 0.5 + 0.5;
439
483
  float shadowed = 0.0;
440
484
  float totalWeight = 0.0;
441
485
  int sampleCount = clamp(int(u_pointShadowPcfSampleCount), 1, 32);
@@ -445,6 +489,8 @@ float a3dPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection)
445
489
  float weight = max(sampleData.z, 0.0);
446
490
  vec2 offset = sampleData.xy * u_pointShadowTexelSize;
447
491
  float storedDepth = texture(u_pointShadowMapTexture, uv + offset).r;
492
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
493
+ float receiverDepth = projectedDepth - u_pointShadowBias - slopeTexelBias * sampleTexelDistance;
448
494
  shadowed += (receiverDepth > storedDepth ? 1.0 : 0.0) * weight;
449
495
  totalWeight += weight;
450
496
  }
@@ -492,11 +538,16 @@ void main() {
492
538
  float horizonBlend = 1.0 - abs(normal.y);
493
539
  vec3 proceduralDiffuse = mix(u_environmentGroundColor, u_environmentSkyColor, skyBlend);
494
540
  proceduralDiffuse = mix(proceduralDiffuse, u_environmentHorizonColor, clamp(horizonBlend, 0.0, 1.0) * 0.55);
495
- vec3 environmentDiffuse = mix(ambientEnvironment, proceduralDiffuse * u_environmentMapIntensity, proceduralEnvironmentWeight);
541
+ // The ambient term must be added to the procedural contribution, not replaced by it. A mix
542
+ // here discarded u_environmentColor * u_environmentIntensity entirely whenever a procedural
543
+ // map was present, which is the normal case: raising ambient intensity from 0.18 to 3.0 on
544
+ // the product-turntable kit produced a byte-identical frame. Ambient and a sky gradient are
545
+ // separate physical contributions, so they sum.
546
+ vec3 environmentDiffuse = ambientEnvironment + proceduralDiffuse * u_environmentMapIntensity * proceduralEnvironmentWeight;
496
547
  float sampledEnvironmentWeight = step(0.0001, u_environmentMapTextureEnabled * u_environmentMapTextureIntensity);
497
548
  float diffuseEnvironmentLod = max(u_environmentMapTextureMipCount - 1.0, 0.0);
498
549
  vec3 sampledDiffuse = a3dPbrDecodeEnvironmentSample(a3dPbrEnvironmentSampleRaw(normal, diffuseEnvironmentLod));
499
- environmentDiffuse = mix(environmentDiffuse, environmentDiffuse * 0.18 + sampledDiffuse * u_environmentMapTextureIntensity * 0.92, sampledEnvironmentWeight);
550
+ environmentDiffuse = mix(environmentDiffuse, ambientEnvironment + sampledDiffuse * u_environmentMapTextureIntensity, sampledEnvironmentWeight);
500
551
  vec3 reflectionDirection = reflect(-viewDirection, normal);
501
552
  float roughness = clamp(u_roughness, 0.0, 1.0);
502
553
  float reflectionBand = pow(clamp(reflectionDirection.y * 0.5 + 0.5, 0.0, 1.0), mix(18.0, 2.0, roughness));
@@ -556,14 +607,29 @@ void main() {
556
607
  vec3 refractionRadiance = (refractedEnvironment + vec3(causticEnergy)) * volumeTint * u_environmentMapTextureIntensity * transmissionAmount * fallbackEnvironmentTransmissionEnergy * mix(0.9, 0.55, roughness) * roughVolumeIorLift;
557
608
  shaded = mix(shaded, shaded * 0.72 + refractionRadiance, transmissionAmount * mix(0.08, 0.58, fallbackEnvironmentTransmissionEnergy));
558
609
  }
559
- int count = min(int(u_lightCount), 16);
560
- for (int i = 0; i < count; ++i) {
561
- int baseIndex = i * 4;
562
- vec4 colorIntensity = u_lightData[baseIndex];
563
- vec4 positionRange = u_lightData[baseIndex + 1];
564
- vec4 directionKind = u_lightData[baseIndex + 2];
565
- vec4 spotShadowLayer = u_lightData[baseIndex + 3];
610
+ ivec2 clusterTile = clamp(ivec2(floor(gl_FragCoord.xy / max(u_clusterViewportSize / u_clusterGridSize, vec2(1.0)))), ivec2(0), ivec2(u_clusterGridSize) - ivec2(1));
611
+ int clusterIndex = clusterTile.y * int(u_clusterGridSize.x) + clusterTile.x;
612
+ int count = u_clusteredLightEnabled > 0.5 ? min(int(texelFetch(u_clusterLightIndices, ivec2(0, clusterIndex), 0).g), 64) : min(int(u_lightCount), 16);
613
+ for (int i = 0; i < 64; ++i) {
614
+ if (i >= count) break;
615
+ int lightIndex = u_clusteredLightEnabled > 0.5 ? int(texelFetch(u_clusterLightIndices, ivec2(i, clusterIndex), 0).r) : i;
616
+ int baseIndex = lightIndex * 6;
617
+ vec4 colorIntensity = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(0, lightIndex), 0) : u_lightData[baseIndex];
618
+ vec4 positionRange = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(1, lightIndex), 0) : u_lightData[baseIndex + 1];
619
+ vec4 directionKind = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(2, lightIndex), 0) : u_lightData[baseIndex + 2];
620
+ vec4 spotShadowLayer = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(3, lightIndex), 0) : u_lightData[baseIndex + 3];
621
+ vec4 areaRight = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(4, lightIndex), 0) : u_lightData[baseIndex + 4];
622
+ vec4 areaUp = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(5, lightIndex), 0) : u_lightData[baseIndex + 5];
566
623
  float kind = directionKind.w;
624
+ if (kind > 2.5) {
625
+ shaded += a3dPbrRectAreaLight(
626
+ v_worldPosition, positionRange.xyz, directionKind.xyz, areaRight.xyz, areaUp.xyz,
627
+ spotShadowLayer.x, spotShadowLayer.y, positionRange.w,
628
+ normal, viewDirection, colorIntensity.rgb, colorIntensity.a,
629
+ materialBase, u_metallic, u_roughness, u_specularFactor, u_specularColorFactor
630
+ );
631
+ continue;
632
+ }
567
633
  vec3 lightDirection = -directionKind.xyz;
568
634
  float attenuation = 1.0;
569
635
  if (kind > 0.5) {
@@ -684,7 +750,12 @@ uniform float u_environmentBrdfLutEnabled;
684
750
  uniform vec3 u_emissiveColor;
685
751
  uniform float u_emissiveStrength;
686
752
  uniform float u_lightCount;
687
- uniform vec4 u_lightData[64];
753
+ uniform vec4 u_lightData[96];
754
+ uniform float u_clusteredLightEnabled;
755
+ uniform vec2 u_clusterGridSize;
756
+ uniform vec2 u_clusterViewportSize;
757
+ uniform sampler2D u_clusterLightData;
758
+ uniform sampler2D u_clusterLightIndices;
688
759
  uniform sampler2D u_shadowMapTexture;
689
760
  uniform float u_shadowMapEnabled;
690
761
  uniform mat4 u_shadowMapMatrix;
@@ -731,8 +802,18 @@ float a3dForwardShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirectio
731
802
  vec3 receiverNormal = normalize(normal);
732
803
  vec3 receiverLightDirection = lightDirection / max(length(lightDirection), 0.0001);
733
804
  float normalDotLight = clamp(abs(dot(receiverNormal, receiverLightDirection)), 0.0, 1.0);
734
- float slopeReceiverBias = (1.0 - normalDotLight) * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
735
- float receiverDepth = projected.z * 0.5 + 0.5 - u_shadowMapBias - slopeReceiverBias;
805
+ // Slope-scaled depth bias must be evaluated per PCF sample, not once for the kernel
806
+ // centre. A sample offset N texels away on a receiver sloped relative to the light sees a
807
+ // depth difference proportional to N, so a centre-only bias under-compensates every outer
808
+ // tap and the receiver shadows itself. Scaling by each sample's own texel distance keeps
809
+ // wide kernels acne-free without inflating the constant bias into peter-panning.
810
+ // The depth gradient across one shadow texel is tan(angle between receiver normal and
811
+ // light), not (1 - N.L). The linear form collapses toward zero far faster than the real
812
+ // gradient grows, so it under-biases exactly the grazing angles that need the most
813
+ // compensation. Clamped so a near-perpendicular receiver cannot demand unbounded bias.
814
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
815
+ float slopeTexelBias = slopeTangent * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
816
+ float projectedDepth = projected.z * 0.5 + 0.5;
736
817
  float shadowed = 0.0;
737
818
  float totalWeight = 0.0;
738
819
  int sampleCount = clamp(int(u_shadowPcfSampleCount), 1, 32);
@@ -742,6 +823,8 @@ float a3dForwardShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirectio
742
823
  float weight = max(sampleData.z, 0.0);
743
824
  vec2 offset = sampleData.xy * u_shadowMapTexelSize;
744
825
  float storedDepth = texture(u_shadowMapTexture, uv + offset).r;
826
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
827
+ float receiverDepth = projectedDepth - u_shadowMapBias - slopeTexelBias * sampleTexelDistance;
745
828
  shadowed += (receiverDepth > storedDepth ? 1.0 : 0.0) * weight;
746
829
  totalWeight += weight;
747
830
  }
@@ -760,7 +843,12 @@ float a3dPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection)
760
843
  vec4 rect = u_pointShadowFaceRects[faceIndex];
761
844
  vec2 uv = rect.xy + localUv * rect.zw;
762
845
  float normalDotLight = clamp(abs(dot(normalize(normal), normalize(lightDirection))), 0.0, 1.0);
763
- float receiverDepth = projected.z * 0.5 + 0.5 - u_pointShadowBias - (1.0 - normalDotLight) * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
846
+ // Same per-sample, tangent-scaled slope bias as the directional and atlas paths. These
847
+ // cubemap-face variants inline the sample fetch, but they share the identical acne
848
+ // mechanism: a centre-only, (1 - N.L)-scaled bias under-compensates every outer tap.
849
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
850
+ float slopeTexelBias = slopeTangent * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
851
+ float projectedDepth = projected.z * 0.5 + 0.5;
764
852
  float shadowed = 0.0;
765
853
  float totalWeight = 0.0;
766
854
  int sampleCount = clamp(int(u_pointShadowPcfSampleCount), 1, 32);
@@ -768,6 +856,8 @@ float a3dPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection)
768
856
  if (i >= sampleCount) break;
769
857
  vec4 sampleData = u_pointShadowPcfSamples[i];
770
858
  float weight = max(sampleData.z, 0.0);
859
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
860
+ float receiverDepth = projectedDepth - u_pointShadowBias - slopeTexelBias * sampleTexelDistance;
771
861
  shadowed += (receiverDepth > texture(u_pointShadowMapTexture, uv + sampleData.xy * u_pointShadowTexelSize).r ? 1.0 : 0.0) * weight;
772
862
  totalWeight += weight;
773
863
  }
@@ -827,11 +917,16 @@ vec3 a3dPbrEnvironmentDiffuseInput(vec3 normal) {
827
917
  float horizonBlend = 1.0 - abs(normal.y);
828
918
  vec3 proceduralDiffuse = mix(u_environmentGroundColor, u_environmentSkyColor, skyBlend);
829
919
  proceduralDiffuse = mix(proceduralDiffuse, u_environmentHorizonColor, clamp(horizonBlend, 0.0, 1.0) * 0.55);
830
- vec3 environmentDiffuse = mix(ambientEnvironment, proceduralDiffuse * u_environmentMapIntensity, proceduralEnvironmentWeight);
920
+ // The ambient term must be added to the procedural contribution, not replaced by it. A mix
921
+ // here discarded u_environmentColor * u_environmentIntensity entirely whenever a procedural
922
+ // map was present, which is the normal case: raising ambient intensity from 0.18 to 3.0 on
923
+ // the product-turntable kit produced a byte-identical frame. Ambient and a sky gradient are
924
+ // separate physical contributions, so they sum.
925
+ vec3 environmentDiffuse = ambientEnvironment + proceduralDiffuse * u_environmentMapIntensity * proceduralEnvironmentWeight;
831
926
  float sampledEnvironmentWeight = step(0.0001, u_environmentMapTextureEnabled * u_environmentMapTextureIntensity);
832
927
  float diffuseEnvironmentLod = max(u_environmentMapTextureMipCount - 1.0, 0.0);
833
928
  vec3 sampledDiffuse = a3dPbrDecodeEnvironmentSample(a3dPbrEnvironmentSampleRaw(normal, diffuseEnvironmentLod));
834
- return mix(environmentDiffuse, environmentDiffuse * 0.18 + sampledDiffuse * u_environmentMapTextureIntensity * 0.92, sampledEnvironmentWeight);
929
+ return mix(environmentDiffuse, ambientEnvironment + sampledDiffuse * u_environmentMapTextureIntensity, sampledEnvironmentWeight);
835
930
  }
836
931
  vec3 a3dPbrEnvironmentSpecularInput(vec3 normal, vec3 viewDirection, float roughness) {
837
932
  float proceduralEnvironmentWeight = step(0.0001, u_environmentMapIntensity);
@@ -852,11 +947,15 @@ vec3 a3dPbrEnvironmentSpecularInput(vec3 normal, vec3 viewDirection, float rough
852
947
  float environmentLod = clampedRoughness * max(u_environmentMapTextureMipCount - 1.0, 0.0);
853
948
  vec3 sampledSpecular = a3dPbrBoundHdrSpecularRadiance(a3dPbrDecodeEnvironmentSample(a3dPbrEnvironmentSampleRaw(reflectionDirection, environmentLod)));
854
949
  float nDotV = clamp(dot(normal, viewDirection), 0.0, 1.0);
855
- vec2 brdfLut = texture(u_environmentBrdfLutTexture, vec2(nDotV, clampedRoughness)).rg;
856
950
  sampledSpecular = a3dPbrClampSampledSpecularEdgeEnergy(sampledSpecular, nDotV, clampedRoughness);
857
951
  sampledSpecular *= u_environmentMapTextureSpecularIntensity * sampledEnvironmentWeight * mix(0.84, 0.58, clampedRoughness);
858
952
  return proceduralSpecular + sampledSpecular;
859
953
  }
954
+ vec2 a3dPbrEnvironmentBrdfInput(vec3 normal, vec3 viewDirection, float roughness) {
955
+ float nDotV = clamp(dot(normalize(normal), normalize(viewDirection)), 0.0, 1.0);
956
+ vec2 lut = texture(u_environmentBrdfLutTexture, vec2(nDotV, clamp(roughness, 0.0, 1.0))).rg;
957
+ return mix(vec2(1.0, 0.0), lut, step(0.0001, u_environmentBrdfLutEnabled));
958
+ }
860
959
  vec3 a3dPbrEncodeOutput(vec3 linearColor) {
861
960
  vec3 color = max(linearColor, vec3(0.0));
862
961
  vec3 filmic = clamp((color * (2.51 * color + 0.03)) / (color * (2.43 * color + 0.59) + 0.14), vec3(0.0), vec3(1.0));
@@ -868,25 +967,41 @@ void main() {
868
967
  if (!gl_FrontFacing) normal = -normal;
869
968
  vec3 viewDirection = normalize(u_cameraPosition - v_worldPosition);
870
969
  vec3 base = u_baseColor.rgb * v_instanceColor.rgb;
871
- vec3 shaded = u_emissiveColor * u_emissiveStrength + a3dPbrEnvironmentLight(
970
+ vec3 shaded = u_emissiveColor * u_emissiveStrength + a3dPbrEnvironmentLightSplitSum(
872
971
  normal,
873
972
  viewDirection,
874
973
  a3dPbrEnvironmentDiffuseInput(normal),
875
974
  a3dPbrEnvironmentSpecularInput(normal, viewDirection, u_roughness),
975
+ a3dPbrEnvironmentBrdfInput(normal, viewDirection, u_roughness),
876
976
  base,
877
977
  u_metallic,
878
978
  u_roughness,
879
979
  1.0,
880
980
  vec3(1.0)
881
981
  );
882
- int count = min(int(u_lightCount), 16);
883
- for (int i = 0; i < count; ++i) {
884
- int baseIndex = i * 4;
885
- vec4 colorIntensity = u_lightData[baseIndex];
886
- vec4 positionRange = u_lightData[baseIndex + 1];
887
- vec4 directionKind = u_lightData[baseIndex + 2];
888
- vec4 spotShadowLayer = u_lightData[baseIndex + 3];
982
+ ivec2 clusterTile = clamp(ivec2(floor(gl_FragCoord.xy / max(u_clusterViewportSize / u_clusterGridSize, vec2(1.0)))), ivec2(0), ivec2(u_clusterGridSize) - ivec2(1));
983
+ int clusterIndex = clusterTile.y * int(u_clusterGridSize.x) + clusterTile.x;
984
+ int count = u_clusteredLightEnabled > 0.5 ? min(int(texelFetch(u_clusterLightIndices, ivec2(0, clusterIndex), 0).g), 64) : min(int(u_lightCount), 16);
985
+ for (int i = 0; i < 64; ++i) {
986
+ if (i >= count) break;
987
+ int lightIndex = u_clusteredLightEnabled > 0.5 ? int(texelFetch(u_clusterLightIndices, ivec2(i, clusterIndex), 0).r) : i;
988
+ int baseIndex = lightIndex * 6;
989
+ vec4 colorIntensity = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(0, lightIndex), 0) : u_lightData[baseIndex];
990
+ vec4 positionRange = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(1, lightIndex), 0) : u_lightData[baseIndex + 1];
991
+ vec4 directionKind = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(2, lightIndex), 0) : u_lightData[baseIndex + 2];
992
+ vec4 spotShadowLayer = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(3, lightIndex), 0) : u_lightData[baseIndex + 3];
993
+ vec4 areaRight = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(4, lightIndex), 0) : u_lightData[baseIndex + 4];
994
+ vec4 areaUp = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(5, lightIndex), 0) : u_lightData[baseIndex + 5];
889
995
  float kind = directionKind.w;
996
+ if (kind > 2.5) {
997
+ shaded += a3dPbrRectAreaLight(
998
+ v_worldPosition, positionRange.xyz, directionKind.xyz, areaRight.xyz, areaUp.xyz,
999
+ spotShadowLayer.x, spotShadowLayer.y, positionRange.w,
1000
+ normal, viewDirection, colorIntensity.rgb, colorIntensity.a,
1001
+ base, u_metallic, u_roughness, 1.0, vec3(1.0)
1002
+ );
1003
+ continue;
1004
+ }
890
1005
  vec3 lightDirection = -directionKind.xyz;
891
1006
  float attenuation = 1.0;
892
1007
  if (kind > 0.5) {
@@ -961,23 +1076,13 @@ void main() {
961
1076
  vertex: `#version 300 es
962
1077
  // ${DEFAULT_SKINNED_UNLIT_SHADER_MARKER}
963
1078
  precision highp float;
1079
+ #include <skinning_common>
964
1080
  layout(location = 0) in vec3 a_position;
965
1081
  layout(location = 5) in vec4 a_joints;
966
1082
  layout(location = 6) in vec4 a_weights;
967
1083
  uniform mat4 u_modelViewProjection;
968
- uniform mat4 u_jointMatrices[96];
969
- uniform float u_jointCount;
970
1084
  void main() {
971
- float maxJoint = max(u_jointCount - 1.0, 0.0);
972
- int jointX = int(clamp(a_joints.x, 0.0, maxJoint));
973
- int jointY = int(clamp(a_joints.y, 0.0, maxJoint));
974
- int jointZ = int(clamp(a_joints.z, 0.0, maxJoint));
975
- int jointW = int(clamp(a_joints.w, 0.0, maxJoint));
976
- mat4 skin =
977
- u_jointMatrices[jointX] * a_weights.x +
978
- u_jointMatrices[jointY] * a_weights.y +
979
- u_jointMatrices[jointZ] * a_weights.z +
980
- u_jointMatrices[jointW] * a_weights.w;
1085
+ mat4 skin = a3dSkinMatrix4(a_joints, a_weights);
981
1086
  float weightSum = a_weights.x + a_weights.y + a_weights.z + a_weights.w;
982
1087
  vec4 skinnedPosition = weightSum > 0.0001 ? skin * vec4(a_position, 1.0) : vec4(a_position, 1.0);
983
1088
  gl_Position = u_modelViewProjection * skinnedPosition;
@@ -999,6 +1104,7 @@ void main() {
999
1104
  vertex: `#version 300 es
1000
1105
  // ${DEFAULT_SKINNED_LIT_SHADER_MARKER}
1001
1106
  precision highp float;
1107
+ #include <skinning_common>
1002
1108
  layout(location = 0) in vec3 a_position;
1003
1109
  layout(location = 1) in vec3 a_normal;
1004
1110
  layout(location = 2) in vec2 a_uv;
@@ -1012,8 +1118,6 @@ uniform mat4 u_normalMatrix;
1012
1118
  uniform vec2 u_baseColorTextureOffset;
1013
1119
  uniform vec2 u_baseColorTextureScale;
1014
1120
  uniform float u_baseColorTextureRotation;
1015
- uniform mat4 u_jointMatrices[96];
1016
- uniform float u_jointCount;
1017
1121
  out vec3 v_normal;
1018
1122
  out vec4 v_tangent;
1019
1123
  out vec3 v_worldPosition;
@@ -1026,16 +1130,7 @@ void main() {
1026
1130
  float s = sin(u_baseColorTextureRotation);
1027
1131
  v_uv = vec2(scaledUv.x * c - scaledUv.y * s, scaledUv.x * s + scaledUv.y * c) + u_baseColorTextureOffset;
1028
1132
  v_vertexColor = a_color;
1029
- float maxJoint = max(u_jointCount - 1.0, 0.0);
1030
- int jointX = int(clamp(a_joints.x, 0.0, maxJoint));
1031
- int jointY = int(clamp(a_joints.y, 0.0, maxJoint));
1032
- int jointZ = int(clamp(a_joints.z, 0.0, maxJoint));
1033
- int jointW = int(clamp(a_joints.w, 0.0, maxJoint));
1034
- mat4 skin =
1035
- u_jointMatrices[jointX] * a_weights.x +
1036
- u_jointMatrices[jointY] * a_weights.y +
1037
- u_jointMatrices[jointZ] * a_weights.z +
1038
- u_jointMatrices[jointW] * a_weights.w;
1133
+ mat4 skin = a3dSkinMatrix4(a_joints, a_weights);
1039
1134
  v_weightSum = a_weights.x + a_weights.y + a_weights.z + a_weights.w;
1040
1135
  vec4 skinnedPosition = v_weightSum > 0.0001 ? skin * vec4(a_position, 1.0) : vec4(a_position, 1.0);
1041
1136
  vec3 skinnedNormal = (v_weightSum > 0.0001 ? skin * vec4(a_normal, 0.0) : vec4(a_normal, 0.0)).xyz;
@@ -1115,7 +1210,12 @@ uniform float u_iridescenceThicknessMinimum;
1115
1210
  uniform float u_iridescenceThicknessMaximum;
1116
1211
  uniform float u_dispersion;
1117
1212
  uniform float u_lightCount;
1118
- uniform vec4 u_lightData[64];
1213
+ uniform vec4 u_lightData[96];
1214
+ uniform float u_clusteredLightEnabled;
1215
+ uniform vec2 u_clusterGridSize;
1216
+ uniform vec2 u_clusterViewportSize;
1217
+ uniform sampler2D u_clusterLightData;
1218
+ uniform sampler2D u_clusterLightIndices;
1119
1219
  uniform sampler2D u_shadowMapTexture;
1120
1220
  uniform float u_shadowMapEnabled;
1121
1221
  uniform mat4 u_shadowMapMatrix;
@@ -1217,8 +1317,18 @@ float a3dForwardShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirectio
1217
1317
  vec3 receiverNormal = normalize(normal);
1218
1318
  vec3 receiverLightDirection = lightDirection / max(length(lightDirection), 0.0001);
1219
1319
  float normalDotLight = clamp(abs(dot(receiverNormal, receiverLightDirection)), 0.0, 1.0);
1220
- float slopeReceiverBias = (1.0 - normalDotLight) * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
1221
- float receiverDepth = projected.z * 0.5 + 0.5 - u_shadowMapBias - slopeReceiverBias;
1320
+ // Slope-scaled depth bias must be evaluated per PCF sample, not once for the kernel
1321
+ // centre. A sample offset N texels away on a receiver sloped relative to the light sees a
1322
+ // depth difference proportional to N, so a centre-only bias under-compensates every outer
1323
+ // tap and the receiver shadows itself. Scaling by each sample's own texel distance keeps
1324
+ // wide kernels acne-free without inflating the constant bias into peter-panning.
1325
+ // The depth gradient across one shadow texel is tan(angle between receiver normal and
1326
+ // light), not (1 - N.L). The linear form collapses toward zero far faster than the real
1327
+ // gradient grows, so it under-biases exactly the grazing angles that need the most
1328
+ // compensation. Clamped so a near-perpendicular receiver cannot demand unbounded bias.
1329
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
1330
+ float slopeTexelBias = slopeTangent * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
1331
+ float projectedDepth = projected.z * 0.5 + 0.5;
1222
1332
  float shadowed = 0.0;
1223
1333
  float totalWeight = 0.0;
1224
1334
  int sampleCount = clamp(int(u_shadowPcfSampleCount), 1, 32);
@@ -1228,6 +1338,8 @@ float a3dForwardShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirectio
1228
1338
  float weight = max(sampleData.z, 0.0);
1229
1339
  vec2 offset = sampleData.xy * u_shadowMapTexelSize;
1230
1340
  float storedDepth = texture(u_shadowMapTexture, uv + offset).r;
1341
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
1342
+ float receiverDepth = projectedDepth - u_shadowMapBias - slopeTexelBias * sampleTexelDistance;
1231
1343
  shadowed += (receiverDepth > storedDepth ? 1.0 : 0.0) * weight;
1232
1344
  totalWeight += weight;
1233
1345
  }
@@ -1255,8 +1367,18 @@ float a3dPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection)
1255
1367
  vec3 receiverNormal = normalize(normal);
1256
1368
  vec3 receiverLightDirection = lightDirection / max(length(lightDirection), 0.0001);
1257
1369
  float normalDotLight = clamp(abs(dot(receiverNormal, receiverLightDirection)), 0.0, 1.0);
1258
- float slopeReceiverBias = (1.0 - normalDotLight) * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
1259
- float receiverDepth = projected.z * 0.5 + 0.5 - u_pointShadowBias - slopeReceiverBias;
1370
+ // Slope-scaled depth bias must be evaluated per PCF sample, not once for the kernel
1371
+ // centre. A sample offset N texels away on a receiver sloped relative to the light sees a
1372
+ // depth difference proportional to N, so a centre-only bias under-compensates every outer
1373
+ // tap and the receiver shadows itself. Scaling by each sample's own texel distance keeps
1374
+ // wide kernels acne-free without inflating the constant bias into peter-panning.
1375
+ // The depth gradient across one shadow texel is tan(angle between receiver normal and
1376
+ // light), not (1 - N.L). The linear form collapses toward zero far faster than the real
1377
+ // gradient grows, so it under-biases exactly the grazing angles that need the most
1378
+ // compensation. Clamped so a near-perpendicular receiver cannot demand unbounded bias.
1379
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
1380
+ float slopeTexelBias = slopeTangent * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
1381
+ float projectedDepth = projected.z * 0.5 + 0.5;
1260
1382
  float shadowed = 0.0;
1261
1383
  float totalWeight = 0.0;
1262
1384
  int sampleCount = clamp(int(u_pointShadowPcfSampleCount), 1, 32);
@@ -1266,6 +1388,8 @@ float a3dPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection)
1266
1388
  float weight = max(sampleData.z, 0.0);
1267
1389
  vec2 offset = sampleData.xy * u_pointShadowTexelSize;
1268
1390
  float storedDepth = texture(u_pointShadowMapTexture, uv + offset).r;
1391
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
1392
+ float receiverDepth = projectedDepth - u_pointShadowBias - slopeTexelBias * sampleTexelDistance;
1269
1393
  shadowed += (receiverDepth > storedDepth ? 1.0 : 0.0) * weight;
1270
1394
  totalWeight += weight;
1271
1395
  }
@@ -1323,7 +1447,12 @@ void main() {
1323
1447
  float horizonBlend = 1.0 - abs(normal.y);
1324
1448
  vec3 proceduralDiffuse = mix(u_environmentGroundColor, u_environmentSkyColor, skyBlend);
1325
1449
  proceduralDiffuse = mix(proceduralDiffuse, u_environmentHorizonColor, clamp(horizonBlend, 0.0, 1.0) * 0.55);
1326
- vec3 environmentDiffuse = mix(ambientEnvironment, proceduralDiffuse * u_environmentMapIntensity, proceduralEnvironmentWeight);
1450
+ // The ambient term must be added to the procedural contribution, not replaced by it. A mix
1451
+ // here discarded u_environmentColor * u_environmentIntensity entirely whenever a procedural
1452
+ // map was present, which is the normal case: raising ambient intensity from 0.18 to 3.0 on
1453
+ // the product-turntable kit produced a byte-identical frame. Ambient and a sky gradient are
1454
+ // separate physical contributions, so they sum.
1455
+ vec3 environmentDiffuse = ambientEnvironment + proceduralDiffuse * u_environmentMapIntensity * proceduralEnvironmentWeight;
1327
1456
  vec3 reflectionDirection = reflect(-viewDirection, normal);
1328
1457
  float sampledEnvironmentWeight = step(0.0001, u_environmentMapTextureEnabled * u_environmentMapTextureIntensity);
1329
1458
  float diffuseEnvironmentLod = max(u_environmentMapTextureMipCount - 1.0, 0.0);
@@ -1349,7 +1478,7 @@ void main() {
1349
1478
  vec3 shaded = a3dPbrEnvironmentLightSplitSum(
1350
1479
  normal,
1351
1480
  viewDirection,
1352
- mix(environmentDiffuse, environmentDiffuse * 0.18 + sampledDiffuse * u_environmentMapTextureIntensity * 0.92, sampledEnvironmentWeight),
1481
+ mix(environmentDiffuse, ambientEnvironment + sampledDiffuse * u_environmentMapTextureIntensity, sampledEnvironmentWeight),
1353
1482
  proceduralSpecular + sampledSpecular,
1354
1483
  mix(vec2(1.0, 0.0), brdfLut, step(0.0001, u_environmentBrdfLutEnabled)),
1355
1484
  materialBase,
@@ -1358,14 +1487,516 @@ void main() {
1358
1487
  u_specularFactor,
1359
1488
  u_specularColorFactor
1360
1489
  ) * occlusion + emissive;
1361
- int count = min(int(u_lightCount), 16);
1362
- for (int i = 0; i < count; ++i) {
1363
- int baseIndex = i * 4;
1364
- vec4 colorIntensity = u_lightData[baseIndex];
1365
- vec4 positionRange = u_lightData[baseIndex + 1];
1366
- vec4 directionKind = u_lightData[baseIndex + 2];
1367
- vec4 spotShadowLayer = u_lightData[baseIndex + 3];
1490
+ ivec2 clusterTile = clamp(ivec2(floor(gl_FragCoord.xy / max(u_clusterViewportSize / u_clusterGridSize, vec2(1.0)))), ivec2(0), ivec2(u_clusterGridSize) - ivec2(1));
1491
+ int clusterIndex = clusterTile.y * int(u_clusterGridSize.x) + clusterTile.x;
1492
+ int count = u_clusteredLightEnabled > 0.5 ? min(int(texelFetch(u_clusterLightIndices, ivec2(0, clusterIndex), 0).g), 64) : min(int(u_lightCount), 16);
1493
+ for (int i = 0; i < 64; ++i) {
1494
+ if (i >= count) break;
1495
+ int lightIndex = u_clusteredLightEnabled > 0.5 ? int(texelFetch(u_clusterLightIndices, ivec2(i, clusterIndex), 0).r) : i;
1496
+ int baseIndex = lightIndex * 6;
1497
+ vec4 colorIntensity = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(0, lightIndex), 0) : u_lightData[baseIndex];
1498
+ vec4 positionRange = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(1, lightIndex), 0) : u_lightData[baseIndex + 1];
1499
+ vec4 directionKind = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(2, lightIndex), 0) : u_lightData[baseIndex + 2];
1500
+ vec4 spotShadowLayer = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(3, lightIndex), 0) : u_lightData[baseIndex + 3];
1501
+ vec4 areaRight = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(4, lightIndex), 0) : u_lightData[baseIndex + 4];
1502
+ vec4 areaUp = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(5, lightIndex), 0) : u_lightData[baseIndex + 5];
1368
1503
  float kind = directionKind.w;
1504
+ if (kind > 2.5) {
1505
+ shaded += a3dPbrRectAreaLight(
1506
+ v_worldPosition, positionRange.xyz, directionKind.xyz, areaRight.xyz, areaUp.xyz,
1507
+ spotShadowLayer.x, spotShadowLayer.y, positionRange.w,
1508
+ normal, viewDirection, colorIntensity.rgb, colorIntensity.a,
1509
+ materialBase, metallic, roughness, u_specularFactor, u_specularColorFactor
1510
+ );
1511
+ continue;
1512
+ }
1513
+ vec3 lightDirection = -directionKind.xyz;
1514
+ float attenuation = 1.0;
1515
+ if (kind > 0.5) {
1516
+ vec3 toLight = positionRange.xyz - v_worldPosition;
1517
+ float distanceToLight = length(toLight);
1518
+ lightDirection = distanceToLight > 0.0001 ? toLight / distanceToLight : -directionKind.xyz;
1519
+ float range = max(positionRange.w, 0.0001);
1520
+ float rangeFalloff = clamp(1.0 - pow(distanceToLight / range, 4.0), 0.0, 1.0);
1521
+ rangeFalloff *= rangeFalloff;
1522
+ attenuation = rangeFalloff / max(distanceToLight * distanceToLight, 1.0);
1523
+ }
1524
+ if (kind > 1.5) {
1525
+ vec3 lightToFragment = normalize(v_worldPosition - positionRange.xyz);
1526
+ float cone = dot(normalize(directionKind.xyz), lightToFragment);
1527
+ float outer = cos(spotShadowLayer.x);
1528
+ float inner = cos(spotShadowLayer.x * max(1.0 - spotShadowLayer.y, 0.001));
1529
+ attenuation *= smoothstep(outer, inner, cone);
1530
+ }
1531
+ shaded += a3dPbrDirectLight(
1532
+ normal,
1533
+ viewDirection,
1534
+ lightDirection,
1535
+ colorIntensity.rgb,
1536
+ colorIntensity.a * attenuation * mix(1.0, kind > 0.5 && kind < 1.5 ? a3dPointShadowFactor(v_worldPosition, normal, lightDirection) : a3dForwardShadowFactor(v_worldPosition, normal, lightDirection), step(0.5, spotShadowLayer.z)),
1537
+ materialBase,
1538
+ metallic,
1539
+ roughness,
1540
+ u_specularFactor,
1541
+ u_specularColorFactor
1542
+ );
1543
+ }
1544
+ if (baseColor.a < u_alphaCutoff) discard;
1545
+ vec3 fogged = a3dApplyEnvironmentFog(max(shaded, vec3(0.0)), v_worldPosition);
1546
+ outColor = vec4(a3dPbrEncodeOutput(fogged), baseColor.a);
1547
+ }
1548
+ `
1549
+ });
1550
+ library.register({
1551
+ name: DEFAULT_SKINNED_UNLIT_EIGHT_INFLUENCE_SHADER_NAME,
1552
+ marker: DEFAULT_SKINNED_UNLIT_EIGHT_INFLUENCE_SHADER_MARKER,
1553
+ vertex: `#version 300 es
1554
+ // ${DEFAULT_SKINNED_UNLIT_EIGHT_INFLUENCE_SHADER_MARKER}
1555
+ precision highp float;
1556
+ #include <skinning_common>
1557
+ layout(location = 0) in vec3 a_position;
1558
+ layout(location = 5) in vec4 a_joints;
1559
+ layout(location = 6) in vec4 a_weights;
1560
+ layout(location = 8) in vec4 a_joints1;
1561
+ layout(location = 9) in vec4 a_weights1;
1562
+ uniform mat4 u_modelViewProjection;
1563
+ void main() {
1564
+ mat4 skin = a3dSkinMatrix8(a_joints, a_weights, a_joints1, a_weights1);
1565
+ float weightSum =
1566
+ a_weights.x + a_weights.y + a_weights.z + a_weights.w +
1567
+ a_weights1.x + a_weights1.y + a_weights1.z + a_weights1.w;
1568
+ vec4 skinnedPosition = weightSum > 0.0001 ? skin * vec4(a_position, 1.0) : vec4(a_position, 1.0);
1569
+ gl_Position = u_modelViewProjection * skinnedPosition;
1570
+ }
1571
+ `,
1572
+ fragment: `#version 300 es
1573
+ // ${DEFAULT_SKINNED_UNLIT_EIGHT_INFLUENCE_SHADER_MARKER}
1574
+ precision highp float;
1575
+ uniform vec4 u_baseColor;
1576
+ out vec4 outColor;
1577
+ void main() {
1578
+ outColor = u_baseColor;
1579
+ }
1580
+ `
1581
+ });
1582
+ library.register({
1583
+ name: DEFAULT_SKINNED_LIT_EIGHT_INFLUENCE_SHADER_NAME,
1584
+ marker: DEFAULT_SKINNED_LIT_EIGHT_INFLUENCE_SHADER_MARKER,
1585
+ vertex: `#version 300 es
1586
+ // ${DEFAULT_SKINNED_LIT_EIGHT_INFLUENCE_SHADER_MARKER}
1587
+ precision highp float;
1588
+ #include <skinning_common>
1589
+ layout(location = 0) in vec3 a_position;
1590
+ layout(location = 1) in vec3 a_normal;
1591
+ layout(location = 2) in vec2 a_uv;
1592
+ layout(location = 3) in vec4 a_tangent;
1593
+ layout(location = 4) in vec4 a_color;
1594
+ layout(location = 5) in vec4 a_joints;
1595
+ layout(location = 6) in vec4 a_weights;
1596
+ layout(location = 8) in vec4 a_joints1;
1597
+ layout(location = 9) in vec4 a_weights1;
1598
+ uniform mat4 u_modelViewProjection;
1599
+ uniform mat4 u_modelMatrix;
1600
+ uniform mat4 u_normalMatrix;
1601
+ uniform vec2 u_baseColorTextureOffset;
1602
+ uniform vec2 u_baseColorTextureScale;
1603
+ uniform float u_baseColorTextureRotation;
1604
+ out vec3 v_normal;
1605
+ out vec4 v_tangent;
1606
+ out vec3 v_worldPosition;
1607
+ out vec2 v_uv;
1608
+ out vec4 v_vertexColor;
1609
+ out float v_weightSum;
1610
+ void main() {
1611
+ vec2 scaledUv = a_uv * u_baseColorTextureScale;
1612
+ float c = cos(u_baseColorTextureRotation);
1613
+ float s = sin(u_baseColorTextureRotation);
1614
+ v_uv = vec2(scaledUv.x * c - scaledUv.y * s, scaledUv.x * s + scaledUv.y * c) + u_baseColorTextureOffset;
1615
+ v_vertexColor = a_color;
1616
+ mat4 skin = a3dSkinMatrix8(a_joints, a_weights, a_joints1, a_weights1);
1617
+ v_weightSum =
1618
+ a_weights.x + a_weights.y + a_weights.z + a_weights.w +
1619
+ a_weights1.x + a_weights1.y + a_weights1.z + a_weights1.w;
1620
+ vec4 skinnedPosition = v_weightSum > 0.0001 ? skin * vec4(a_position, 1.0) : vec4(a_position, 1.0);
1621
+ // Normals and tangents use the same skin matrix with w = 0 so translation is
1622
+ // excluded; renormalizing happens in the fragment stage.
1623
+ vec3 skinnedNormal = (v_weightSum > 0.0001 ? skin * vec4(a_normal, 0.0) : vec4(a_normal, 0.0)).xyz;
1624
+ vec3 skinnedTangent = (v_weightSum > 0.0001 ? skin * vec4(a_tangent.xyz, 0.0) : vec4(a_tangent.xyz, 0.0)).xyz;
1625
+ v_normal = mat3(u_normalMatrix) * skinnedNormal;
1626
+ v_tangent = vec4(mat3(u_normalMatrix) * skinnedTangent, a_tangent.w);
1627
+ v_worldPosition = (u_modelMatrix * skinnedPosition).xyz;
1628
+ gl_Position = u_modelViewProjection * skinnedPosition;
1629
+ }
1630
+ `,
1631
+ fragment: `#version 300 es
1632
+ // ${DEFAULT_SKINNED_LIT_EIGHT_INFLUENCE_SHADER_MARKER}
1633
+ precision highp float;
1634
+ #include <lighting_common>
1635
+ #include <pbr_common>
1636
+ uniform vec4 u_baseColor;
1637
+ uniform sampler2D u_baseColorTexture;
1638
+ uniform float u_baseColorTextureEnabled;
1639
+ uniform sampler2D u_normalTexture;
1640
+ uniform float u_normalTextureEnabled;
1641
+ uniform float u_normalScale;
1642
+ uniform sampler2D u_metallicRoughnessTexture;
1643
+ uniform float u_metallicRoughnessTextureEnabled;
1644
+ uniform sampler2D u_occlusionTexture;
1645
+ uniform float u_occlusionTextureEnabled;
1646
+ uniform float u_occlusionStrength;
1647
+ uniform sampler2D u_emissiveTexture;
1648
+ uniform float u_emissiveTextureEnabled;
1649
+ uniform float u_alphaCutoff;
1650
+ uniform float u_metallic;
1651
+ uniform float u_roughness;
1652
+ uniform vec3 u_environmentColor;
1653
+ uniform float u_environmentIntensity;
1654
+ uniform vec3 u_environmentSkyColor;
1655
+ uniform vec3 u_environmentHorizonColor;
1656
+ uniform vec3 u_environmentGroundColor;
1657
+ uniform vec3 u_environmentSpecularColor;
1658
+ uniform float u_environmentMapIntensity;
1659
+ uniform float u_environmentSpecularIntensity;
1660
+ uniform sampler2D u_environmentMapTexture;
1661
+ uniform samplerCube u_environmentCubeMapTexture;
1662
+ uniform float u_environmentCubeMapTextureEnabled;
1663
+ uniform float u_environmentMapTextureEnabled;
1664
+ uniform float u_environmentMapTextureIntensity;
1665
+ uniform float u_environmentMapTextureSpecularIntensity;
1666
+ uniform float u_environmentMapTextureRotation;
1667
+ uniform float u_environmentMapTextureMipCount;
1668
+ uniform float u_environmentMapTextureEncoding;
1669
+ uniform sampler2D u_environmentBrdfLutTexture;
1670
+ uniform float u_environmentBrdfLutEnabled;
1671
+ uniform vec3 u_emissiveColor;
1672
+ uniform float u_emissiveStrength;
1673
+ uniform float u_clearcoatFactor;
1674
+ uniform float u_clearcoatRoughnessFactor;
1675
+ uniform float u_transmissionFactor;
1676
+ uniform float u_diffuseTransmissionFactor;
1677
+ uniform vec3 u_diffuseTransmissionColorFactor;
1678
+ uniform float u_transmissionFallbackEnergy;
1679
+ uniform float u_volumeThicknessFactor;
1680
+ uniform float u_volumeAttenuationDistance;
1681
+ uniform vec3 u_volumeAttenuationColor;
1682
+ uniform float u_transmissionParallaxStrength;
1683
+ uniform vec3 u_transmissionParallaxBoxMin;
1684
+ uniform vec3 u_transmissionParallaxBoxMax;
1685
+ uniform float u_transmissionBounceCount;
1686
+ uniform float u_transmissionCausticStrength;
1687
+ uniform float u_ior;
1688
+ uniform float u_specularFactor;
1689
+ uniform vec3 u_specularColorFactor;
1690
+ uniform vec3 u_sheenColorFactor;
1691
+ uniform float u_sheenRoughnessFactor;
1692
+ uniform float u_anisotropyStrength;
1693
+ uniform float u_anisotropyRotation;
1694
+ uniform float u_iridescenceFactor;
1695
+ uniform float u_iridescenceIor;
1696
+ uniform float u_iridescenceThicknessMinimum;
1697
+ uniform float u_iridescenceThicknessMaximum;
1698
+ uniform float u_dispersion;
1699
+ uniform float u_lightCount;
1700
+ uniform vec4 u_lightData[96];
1701
+ uniform float u_clusteredLightEnabled;
1702
+ uniform vec2 u_clusterGridSize;
1703
+ uniform vec2 u_clusterViewportSize;
1704
+ uniform sampler2D u_clusterLightData;
1705
+ uniform sampler2D u_clusterLightIndices;
1706
+ uniform sampler2D u_shadowMapTexture;
1707
+ uniform float u_shadowMapEnabled;
1708
+ uniform mat4 u_shadowMapMatrix;
1709
+ uniform float u_shadowMapStrength;
1710
+ uniform float u_shadowMapBias;
1711
+ uniform float u_shadowMapSlopeBias;
1712
+ uniform vec2 u_shadowMapTexelSize;
1713
+ uniform float u_shadowPcfSampleCount;
1714
+ uniform vec4 u_shadowPcfSamples[32];
1715
+ uniform sampler2D u_pointShadowMapTexture;
1716
+ uniform float u_pointShadowMapEnabled;
1717
+ uniform vec3 u_pointShadowLightPosition;
1718
+ uniform float u_pointShadowRange;
1719
+ uniform mat4 u_pointShadowFaceMatrices[6];
1720
+ uniform vec4 u_pointShadowFaceRects[6];
1721
+ uniform float u_pointShadowStrength;
1722
+ uniform float u_pointShadowBias;
1723
+ uniform float u_pointShadowSlopeBias;
1724
+ uniform vec2 u_pointShadowTexelSize;
1725
+ uniform float u_pointShadowPcfSampleCount;
1726
+ uniform vec4 u_pointShadowPcfSamples[32];
1727
+ uniform float u_outputColorSpace;
1728
+ uniform vec3 u_cameraPosition;
1729
+ uniform float u_environmentFogEnabled;
1730
+ uniform float u_environmentFogMode;
1731
+ uniform vec3 u_environmentFogColor;
1732
+ uniform float u_environmentFogNear;
1733
+ uniform float u_environmentFogFar;
1734
+ uniform float u_environmentFogDensity;
1735
+ uniform float u_environmentFogHeightFalloff;
1736
+ uniform float u_environmentFogHeightReference;
1737
+ uniform float u_environmentFogMaxOpacity;
1738
+ in vec3 v_normal;
1739
+ in vec4 v_tangent;
1740
+ in vec3 v_worldPosition;
1741
+ in vec2 v_uv;
1742
+ in vec4 v_vertexColor;
1743
+ in float v_weightSum;
1744
+ out vec4 outColor;
1745
+ #include <environment_fog_common>
1746
+ vec2 a3dEnvironmentEquirectUv(vec3 direction, float rotation) {
1747
+ vec3 d = normalize(direction);
1748
+ float u = atan(d.z, d.x) / 6.28318530718 + 0.5 + rotation;
1749
+ float v = acos(clamp(d.y, -1.0, 1.0)) / 3.14159265359;
1750
+ return vec2(fract(u), clamp(v, 0.0, 1.0));
1751
+ }
1752
+ vec3 a3dEnvironmentCubeDirection(vec3 direction, float rotation) {
1753
+ float angle = rotation * 6.28318530718;
1754
+ float c = cos(angle);
1755
+ float s = sin(angle);
1756
+ vec3 d = normalize(direction);
1757
+ return normalize(vec3(c * d.x - s * d.z, d.y, s * d.x + c * d.z));
1758
+ }
1759
+ vec3 a3dPbrDecodeEnvironmentSrgb(vec3 encodedColor) {
1760
+ return max(encodedColor, vec3(0.0));
1761
+ }
1762
+ vec3 a3dPbrDecodeEnvironmentRgbe(vec4 encodedSample) {
1763
+ float exponent = encodedSample.a * 255.0;
1764
+ float scale = exponent <= 0.0 ? 0.0 : exp2(exponent - 128.0) * (255.0 / 256.0);
1765
+ return max(encodedSample.rgb * scale, vec3(0.0));
1766
+ }
1767
+ vec3 a3dPbrDecodeEnvironmentSample(vec4 encodedSample) {
1768
+ if (u_environmentMapTextureEncoding > 1.5) return max(encodedSample.rgb, vec3(0.0));
1769
+ if (u_environmentMapTextureEncoding > 0.5) return a3dPbrDecodeEnvironmentRgbe(encodedSample);
1770
+ return a3dPbrDecodeEnvironmentSrgb(encodedSample.rgb);
1771
+ }
1772
+ vec3 a3dPbrBoundHdrSpecularRadiance(vec3 radiance) {
1773
+ vec3 nonNegative = max(radiance, vec3(0.0));
1774
+ vec3 softKnee = nonNegative / (vec3(1.0) + max(nonNegative - vec3(1.0), vec3(0.0)) * 0.58);
1775
+ float maxChannel = max(max(softKnee.r, softKnee.g), softKnee.b);
1776
+ return maxChannel > 1.65 ? softKnee * (1.65 / maxChannel) : softKnee;
1777
+ }
1778
+ vec3 a3dPbrClampSampledSpecularEdgeEnergy(vec3 radiance, float nDotV, float roughness) {
1779
+ float faceOn = smoothstep(0.12, 0.55, clamp(nDotV, 0.0, 1.0));
1780
+ float roughEnergy = mix(0.6, 1.0, clamp(roughness, 0.0, 1.0));
1781
+ float edgeCap = mix(0.12, 1.2, faceOn) * roughEnergy;
1782
+ float edgeScale = mix(0.14, 0.92, faceOn);
1783
+ vec3 bounded = min(radiance * edgeScale, vec3(edgeCap));
1784
+ return max(bounded, vec3(0.0));
1785
+ }
1786
+ vec4 a3dPbrEnvironmentSampleRaw(vec3 direction, float lod) {
1787
+ vec4 equirectSample = textureLod(u_environmentMapTexture, a3dEnvironmentEquirectUv(direction, u_environmentMapTextureRotation), lod);
1788
+ vec4 cubeSample = textureLod(u_environmentCubeMapTexture, a3dEnvironmentCubeDirection(direction, u_environmentMapTextureRotation), lod);
1789
+ return mix(equirectSample, cubeSample, step(0.5, u_environmentCubeMapTextureEnabled));
1790
+ }
1791
+ vec3 a3dSkinnedPbrNormal(vec3 baseNormal, vec4 tangentFrame, vec2 uv) {
1792
+ vec3 sampled = texture(u_normalTexture, uv).xyz * 2.0 - 1.0;
1793
+ vec3 n = normalize(baseNormal);
1794
+ vec3 tangent = normalize(tangentFrame.xyz);
1795
+ vec3 bitangent = normalize(cross(n, tangent) * tangentFrame.w);
1796
+ return normalize(tangent * sampled.x * u_normalScale + bitangent * sampled.y * u_normalScale + n * max(sampled.z, 0.001));
1797
+ }
1798
+ float a3dForwardShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection) {
1799
+ if (u_shadowMapEnabled < 0.5) return 1.0;
1800
+ vec4 lightPosition = u_shadowMapMatrix * vec4(worldPosition, 1.0);
1801
+ vec3 projected = lightPosition.xyz / max(lightPosition.w, 0.0001);
1802
+ vec2 uv = projected.xy * 0.5 + 0.5;
1803
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) return 1.0;
1804
+ vec3 receiverNormal = normalize(normal);
1805
+ vec3 receiverLightDirection = lightDirection / max(length(lightDirection), 0.0001);
1806
+ float normalDotLight = clamp(abs(dot(receiverNormal, receiverLightDirection)), 0.0, 1.0);
1807
+ // Slope-scaled depth bias must be evaluated per PCF sample, not once for the kernel
1808
+ // centre. A sample offset N texels away on a receiver sloped relative to the light sees a
1809
+ // depth difference proportional to N, so a centre-only bias under-compensates every outer
1810
+ // tap and the receiver shadows itself. Scaling by each sample's own texel distance keeps
1811
+ // wide kernels acne-free without inflating the constant bias into peter-panning.
1812
+ // The depth gradient across one shadow texel is tan(angle between receiver normal and
1813
+ // light), not (1 - N.L). The linear form collapses toward zero far faster than the real
1814
+ // gradient grows, so it under-biases exactly the grazing angles that need the most
1815
+ // compensation. Clamped so a near-perpendicular receiver cannot demand unbounded bias.
1816
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
1817
+ float slopeTexelBias = slopeTangent * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
1818
+ float projectedDepth = projected.z * 0.5 + 0.5;
1819
+ float shadowed = 0.0;
1820
+ float totalWeight = 0.0;
1821
+ int sampleCount = clamp(int(u_shadowPcfSampleCount), 1, 32);
1822
+ for (int i = 0; i < 32; ++i) {
1823
+ if (i >= sampleCount) break;
1824
+ vec4 sampleData = u_shadowPcfSamples[i];
1825
+ float weight = max(sampleData.z, 0.0);
1826
+ vec2 offset = sampleData.xy * u_shadowMapTexelSize;
1827
+ float storedDepth = texture(u_shadowMapTexture, uv + offset).r;
1828
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
1829
+ float receiverDepth = projectedDepth - u_shadowMapBias - slopeTexelBias * sampleTexelDistance;
1830
+ shadowed += (receiverDepth > storedDepth ? 1.0 : 0.0) * weight;
1831
+ totalWeight += weight;
1832
+ }
1833
+ float occlusion = totalWeight > 0.0 ? shadowed / totalWeight : 0.0;
1834
+ return mix(1.0, 1.0 - occlusion, clamp(u_shadowMapStrength, 0.0, 1.0));
1835
+ }
1836
+ float a3dPointShadowFaceIndex(vec3 direction) {
1837
+ vec3 absoluteDirection = abs(direction);
1838
+ if (absoluteDirection.x >= absoluteDirection.y && absoluteDirection.x >= absoluteDirection.z) return direction.x >= 0.0 ? 0.0 : 1.0;
1839
+ if (absoluteDirection.y >= absoluteDirection.x && absoluteDirection.y >= absoluteDirection.z) return direction.y >= 0.0 ? 2.0 : 3.0;
1840
+ return direction.z >= 0.0 ? 4.0 : 5.0;
1841
+ }
1842
+ float a3dPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection) {
1843
+ if (u_pointShadowMapEnabled < 0.5) return 1.0;
1844
+ vec3 lightToFragment = worldPosition - u_pointShadowLightPosition;
1845
+ float distanceToLight = length(lightToFragment);
1846
+ if (distanceToLight > u_pointShadowRange) return 1.0;
1847
+ int faceIndex = int(a3dPointShadowFaceIndex(lightToFragment));
1848
+ vec4 lightPosition = u_pointShadowFaceMatrices[faceIndex] * vec4(worldPosition, 1.0);
1849
+ vec3 projected = lightPosition.xyz / max(lightPosition.w, 0.0001);
1850
+ vec2 localUv = projected.xy * 0.5 + 0.5;
1851
+ if (localUv.x < 0.0 || localUv.x > 1.0 || localUv.y < 0.0 || localUv.y > 1.0) return 1.0;
1852
+ vec4 rect = u_pointShadowFaceRects[faceIndex];
1853
+ vec2 uv = rect.xy + localUv * rect.zw;
1854
+ vec3 receiverNormal = normalize(normal);
1855
+ vec3 receiverLightDirection = lightDirection / max(length(lightDirection), 0.0001);
1856
+ float normalDotLight = clamp(abs(dot(receiverNormal, receiverLightDirection)), 0.0, 1.0);
1857
+ // Slope-scaled depth bias must be evaluated per PCF sample, not once for the kernel
1858
+ // centre. A sample offset N texels away on a receiver sloped relative to the light sees a
1859
+ // depth difference proportional to N, so a centre-only bias under-compensates every outer
1860
+ // tap and the receiver shadows itself. Scaling by each sample's own texel distance keeps
1861
+ // wide kernels acne-free without inflating the constant bias into peter-panning.
1862
+ // The depth gradient across one shadow texel is tan(angle between receiver normal and
1863
+ // light), not (1 - N.L). The linear form collapses toward zero far faster than the real
1864
+ // gradient grows, so it under-biases exactly the grazing angles that need the most
1865
+ // compensation. Clamped so a near-perpendicular receiver cannot demand unbounded bias.
1866
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
1867
+ float slopeTexelBias = slopeTangent * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
1868
+ float projectedDepth = projected.z * 0.5 + 0.5;
1869
+ float shadowed = 0.0;
1870
+ float totalWeight = 0.0;
1871
+ int sampleCount = clamp(int(u_pointShadowPcfSampleCount), 1, 32);
1872
+ for (int i = 0; i < 32; ++i) {
1873
+ if (i >= sampleCount) break;
1874
+ vec4 sampleData = u_pointShadowPcfSamples[i];
1875
+ float weight = max(sampleData.z, 0.0);
1876
+ vec2 offset = sampleData.xy * u_pointShadowTexelSize;
1877
+ float storedDepth = texture(u_pointShadowMapTexture, uv + offset).r;
1878
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
1879
+ float receiverDepth = projectedDepth - u_pointShadowBias - slopeTexelBias * sampleTexelDistance;
1880
+ shadowed += (receiverDepth > storedDepth ? 1.0 : 0.0) * weight;
1881
+ totalWeight += weight;
1882
+ }
1883
+ float occlusion = totalWeight > 0.0 ? shadowed / totalWeight : 0.0;
1884
+ return mix(1.0, 1.0 - occlusion, clamp(u_pointShadowStrength, 0.0, 1.0));
1885
+ }
1886
+ vec3 a3dPbrEncodeOutput(vec3 linearColor) {
1887
+ vec3 color = max(linearColor, vec3(0.0));
1888
+ vec3 filmic = clamp((color * (2.51 * color + 0.03)) / (color * (2.43 * color + 0.59) + 0.14), vec3(0.0), vec3(1.0));
1889
+ vec3 srgb = pow(filmic, vec3(1.0 / 2.2));
1890
+ return mix(color, srgb, step(0.5, u_outputColorSpace));
1891
+ }
1892
+ void main() {
1893
+ vec3 normal = normalize(v_normal);
1894
+ normal = mix(normal, a3dSkinnedPbrNormal(v_normal, v_tangent, v_uv), step(0.5, u_normalTextureEnabled));
1895
+ if (!gl_FrontFacing) normal = -normal;
1896
+ vec3 viewDirection = normalize(u_cameraPosition - v_worldPosition);
1897
+ vec4 sampledBaseColor = texture(u_baseColorTexture, v_uv);
1898
+ vec4 decodedBaseColor = vec4(a3dPbrDecodeEnvironmentSrgb(sampledBaseColor.rgb), sampledBaseColor.a);
1899
+ float baseColorTextureWeight = clamp(u_baseColorTextureEnabled, 0.0, 1.0);
1900
+ vec4 baseColor = mix(u_baseColor, u_baseColor * decodedBaseColor, baseColorTextureWeight) * v_vertexColor;
1901
+ vec4 metallicRoughnessSample = texture(u_metallicRoughnessTexture, v_uv);
1902
+ float metallic = mix(u_metallic, clamp(u_metallic * metallicRoughnessSample.b, 0.0, 1.0), step(0.5, u_metallicRoughnessTextureEnabled));
1903
+ float roughness = mix(u_roughness, clamp(u_roughness * metallicRoughnessSample.g, 0.0, 1.0), step(0.5, u_metallicRoughnessTextureEnabled));
1904
+ float occlusion = mix(1.0, mix(1.0, texture(u_occlusionTexture, v_uv).r, clamp(u_occlusionStrength, 0.0, 1.0)), step(0.5, u_occlusionTextureEnabled));
1905
+ vec3 emissive = u_emissiveColor * u_emissiveStrength * mix(vec3(1.0), a3dPbrDecodeEnvironmentSrgb(texture(u_emissiveTexture, v_uv).rgb), step(0.5, u_emissiveTextureEnabled));
1906
+ vec3 materialBase = a3dApplyAdvancedPbrLobes(
1907
+ baseColor.rgb,
1908
+ u_clearcoatFactor,
1909
+ u_clearcoatRoughnessFactor,
1910
+ u_transmissionFactor,
1911
+ u_diffuseTransmissionFactor,
1912
+ u_diffuseTransmissionColorFactor,
1913
+ u_transmissionFallbackEnergy,
1914
+ u_volumeThicknessFactor,
1915
+ u_volumeAttenuationDistance,
1916
+ u_volumeAttenuationColor,
1917
+ u_ior,
1918
+ u_specularFactor,
1919
+ u_specularColorFactor,
1920
+ u_sheenColorFactor,
1921
+ u_sheenRoughnessFactor,
1922
+ u_anisotropyStrength,
1923
+ u_anisotropyRotation,
1924
+ u_iridescenceFactor,
1925
+ u_iridescenceIor,
1926
+ u_iridescenceThicknessMinimum,
1927
+ u_iridescenceThicknessMaximum,
1928
+ u_dispersion
1929
+ );
1930
+ float environmentHemi = mix(0.35, 1.0, clamp(normal.y * 0.5 + 0.5, 0.0, 1.0));
1931
+ vec3 ambientEnvironment = u_environmentColor * u_environmentIntensity * environmentHemi;
1932
+ float proceduralEnvironmentWeight = step(0.0001, u_environmentMapIntensity);
1933
+ float skyBlend = clamp(normal.y * 0.5 + 0.5, 0.0, 1.0);
1934
+ float horizonBlend = 1.0 - abs(normal.y);
1935
+ vec3 proceduralDiffuse = mix(u_environmentGroundColor, u_environmentSkyColor, skyBlend);
1936
+ proceduralDiffuse = mix(proceduralDiffuse, u_environmentHorizonColor, clamp(horizonBlend, 0.0, 1.0) * 0.55);
1937
+ // The ambient term must be added to the procedural contribution, not replaced by it. A mix
1938
+ // here discarded u_environmentColor * u_environmentIntensity entirely whenever a procedural
1939
+ // map was present, which is the normal case: raising ambient intensity from 0.18 to 3.0 on
1940
+ // the product-turntable kit produced a byte-identical frame. Ambient and a sky gradient are
1941
+ // separate physical contributions, so they sum.
1942
+ vec3 environmentDiffuse = ambientEnvironment + proceduralDiffuse * u_environmentMapIntensity * proceduralEnvironmentWeight;
1943
+ vec3 reflectionDirection = reflect(-viewDirection, normal);
1944
+ float sampledEnvironmentWeight = step(0.0001, u_environmentMapTextureEnabled * u_environmentMapTextureIntensity);
1945
+ float diffuseEnvironmentLod = max(u_environmentMapTextureMipCount - 1.0, 0.0);
1946
+ vec3 sampledDiffuse = a3dPbrDecodeEnvironmentSample(a3dPbrEnvironmentSampleRaw(normal, diffuseEnvironmentLod));
1947
+ float clampedRoughness = clamp(roughness, 0.0, 1.0);
1948
+ float reflectionBand = pow(clamp(reflectionDirection.y * 0.5 + 0.5, 0.0, 1.0), mix(18.0, 2.0, clampedRoughness));
1949
+ float roughEnvironmentFloor = mix(0.04, 0.38, clampedRoughness);
1950
+ float proceduralSpecularResponse = max(reflectionBand, roughEnvironmentFloor);
1951
+ float horizonStripe = pow(clamp(1.0 - abs(reflectionDirection.y), 0.0, 1.0), mix(14.0, 3.0, clampedRoughness));
1952
+ float sideStripe = pow(clamp(abs(reflectionDirection.x), 0.0, 1.0), mix(10.0, 3.0, clampedRoughness)) * smoothstep(0.0, 0.72, 1.0 - abs(reflectionDirection.y));
1953
+ float overheadStripe = pow(clamp(reflectionDirection.y * 0.5 + 0.5, 0.0, 1.0), mix(22.0, 4.0, clampedRoughness));
1954
+ vec3 proceduralSpecularColor = u_environmentSpecularColor * proceduralSpecularResponse
1955
+ + u_environmentHorizonColor * horizonStripe * 0.42
1956
+ + u_environmentSkyColor * overheadStripe * 0.28
1957
+ + u_environmentGroundColor * sideStripe * 0.22;
1958
+ vec3 proceduralSpecular = proceduralSpecularColor * u_environmentSpecularIntensity * proceduralEnvironmentWeight;
1959
+ float environmentLod = clampedRoughness * max(u_environmentMapTextureMipCount - 1.0, 0.0);
1960
+ vec3 sampledSpecular = a3dPbrBoundHdrSpecularRadiance(a3dPbrDecodeEnvironmentSample(a3dPbrEnvironmentSampleRaw(reflectionDirection, environmentLod)));
1961
+ float nDotV = clamp(dot(normal, viewDirection), 0.0, 1.0);
1962
+ vec2 brdfLut = texture(u_environmentBrdfLutTexture, vec2(nDotV, clampedRoughness)).rg;
1963
+ sampledSpecular = a3dPbrClampSampledSpecularEdgeEnergy(sampledSpecular, nDotV, clampedRoughness);
1964
+ sampledSpecular *= u_environmentMapTextureSpecularIntensity * sampledEnvironmentWeight * mix(1.1, 0.85, roughness);
1965
+ vec3 shaded = a3dPbrEnvironmentLightSplitSum(
1966
+ normal,
1967
+ viewDirection,
1968
+ mix(environmentDiffuse, ambientEnvironment + sampledDiffuse * u_environmentMapTextureIntensity, sampledEnvironmentWeight),
1969
+ proceduralSpecular + sampledSpecular,
1970
+ mix(vec2(1.0, 0.0), brdfLut, step(0.0001, u_environmentBrdfLutEnabled)),
1971
+ materialBase,
1972
+ metallic,
1973
+ roughness,
1974
+ u_specularFactor,
1975
+ u_specularColorFactor
1976
+ ) * occlusion + emissive;
1977
+ ivec2 clusterTile = clamp(ivec2(floor(gl_FragCoord.xy / max(u_clusterViewportSize / u_clusterGridSize, vec2(1.0)))), ivec2(0), ivec2(u_clusterGridSize) - ivec2(1));
1978
+ int clusterIndex = clusterTile.y * int(u_clusterGridSize.x) + clusterTile.x;
1979
+ int count = u_clusteredLightEnabled > 0.5 ? min(int(texelFetch(u_clusterLightIndices, ivec2(0, clusterIndex), 0).g), 64) : min(int(u_lightCount), 16);
1980
+ for (int i = 0; i < 64; ++i) {
1981
+ if (i >= count) break;
1982
+ int lightIndex = u_clusteredLightEnabled > 0.5 ? int(texelFetch(u_clusterLightIndices, ivec2(i, clusterIndex), 0).r) : i;
1983
+ int baseIndex = lightIndex * 6;
1984
+ vec4 colorIntensity = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(0, lightIndex), 0) : u_lightData[baseIndex];
1985
+ vec4 positionRange = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(1, lightIndex), 0) : u_lightData[baseIndex + 1];
1986
+ vec4 directionKind = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(2, lightIndex), 0) : u_lightData[baseIndex + 2];
1987
+ vec4 spotShadowLayer = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(3, lightIndex), 0) : u_lightData[baseIndex + 3];
1988
+ vec4 areaRight = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(4, lightIndex), 0) : u_lightData[baseIndex + 4];
1989
+ vec4 areaUp = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(5, lightIndex), 0) : u_lightData[baseIndex + 5];
1990
+ float kind = directionKind.w;
1991
+ if (kind > 2.5) {
1992
+ shaded += a3dPbrRectAreaLight(
1993
+ v_worldPosition, positionRange.xyz, directionKind.xyz, areaRight.xyz, areaUp.xyz,
1994
+ spotShadowLayer.x, spotShadowLayer.y, positionRange.w,
1995
+ normal, viewDirection, colorIntensity.rgb, colorIntensity.a,
1996
+ materialBase, metallic, roughness, u_specularFactor, u_specularColorFactor
1997
+ );
1998
+ continue;
1999
+ }
1369
2000
  vec3 lightDirection = -directionKind.xyz;
1370
2001
  float attenuation = 1.0;
1371
2002
  if (kind > 0.5) {
@@ -1539,7 +2170,12 @@ uniform vec3 u_specularColorFactor;
1539
2170
  uniform vec3 u_sheenColorFactor;
1540
2171
  uniform float u_sheenRoughnessFactor;
1541
2172
  uniform float u_lightCount;
1542
- uniform vec4 u_lightData[64];
2173
+ uniform vec4 u_lightData[96];
2174
+ uniform float u_clusteredLightEnabled;
2175
+ uniform vec2 u_clusterGridSize;
2176
+ uniform vec2 u_clusterViewportSize;
2177
+ uniform sampler2D u_clusterLightData;
2178
+ uniform sampler2D u_clusterLightIndices;
1543
2179
  uniform sampler2D u_normalTexture;
1544
2180
  uniform float u_normalScale;
1545
2181
  uniform sampler2D u_shadowMapTexture;
@@ -1597,8 +2233,18 @@ float a3dForwardShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirectio
1597
2233
  vec3 receiverNormal = normalize(normal);
1598
2234
  vec3 receiverLightDirection = lightDirection / max(length(lightDirection), 0.0001);
1599
2235
  float normalDotLight = clamp(abs(dot(receiverNormal, receiverLightDirection)), 0.0, 1.0);
1600
- float slopeReceiverBias = (1.0 - normalDotLight) * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
1601
- float receiverDepth = projected.z * 0.5 + 0.5 - u_shadowMapBias - slopeReceiverBias;
2236
+ // Slope-scaled depth bias must be evaluated per PCF sample, not once for the kernel
2237
+ // centre. A sample offset N texels away on a receiver sloped relative to the light sees a
2238
+ // depth difference proportional to N, so a centre-only bias under-compensates every outer
2239
+ // tap and the receiver shadows itself. Scaling by each sample's own texel distance keeps
2240
+ // wide kernels acne-free without inflating the constant bias into peter-panning.
2241
+ // The depth gradient across one shadow texel is tan(angle between receiver normal and
2242
+ // light), not (1 - N.L). The linear form collapses toward zero far faster than the real
2243
+ // gradient grows, so it under-biases exactly the grazing angles that need the most
2244
+ // compensation. Clamped so a near-perpendicular receiver cannot demand unbounded bias.
2245
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
2246
+ float slopeTexelBias = slopeTangent * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
2247
+ float projectedDepth = projected.z * 0.5 + 0.5;
1602
2248
  float shadowed = 0.0;
1603
2249
  float totalWeight = 0.0;
1604
2250
  int sampleCount = clamp(int(u_shadowPcfSampleCount), 1, 32);
@@ -1608,6 +2254,8 @@ float a3dForwardShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirectio
1608
2254
  float weight = max(sampleData.z, 0.0);
1609
2255
  vec2 offset = sampleData.xy * u_shadowMapTexelSize;
1610
2256
  float storedDepth = texture(u_shadowMapTexture, uv + offset).r;
2257
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
2258
+ float receiverDepth = projectedDepth - u_shadowMapBias - slopeTexelBias * sampleTexelDistance;
1611
2259
  shadowed += (receiverDepth > storedDepth ? 1.0 : 0.0) * weight;
1612
2260
  totalWeight += weight;
1613
2261
  }
@@ -1626,7 +2274,12 @@ float a3dPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection)
1626
2274
  vec4 rect = u_pointShadowFaceRects[faceIndex];
1627
2275
  vec2 uv = rect.xy + localUv * rect.zw;
1628
2276
  float normalDotLight = clamp(abs(dot(normalize(normal), normalize(lightDirection))), 0.0, 1.0);
1629
- float receiverDepth = projected.z * 0.5 + 0.5 - u_pointShadowBias - (1.0 - normalDotLight) * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
2277
+ // Same per-sample, tangent-scaled slope bias as the directional and atlas paths. These
2278
+ // cubemap-face variants inline the sample fetch, but they share the identical acne
2279
+ // mechanism: a centre-only, (1 - N.L)-scaled bias under-compensates every outer tap.
2280
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
2281
+ float slopeTexelBias = slopeTangent * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
2282
+ float projectedDepth = projected.z * 0.5 + 0.5;
1630
2283
  float shadowed = 0.0;
1631
2284
  float totalWeight = 0.0;
1632
2285
  int sampleCount = clamp(int(u_pointShadowPcfSampleCount), 1, 32);
@@ -1634,6 +2287,8 @@ float a3dPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDirection)
1634
2287
  if (i >= sampleCount) break;
1635
2288
  vec4 sampleData = u_pointShadowPcfSamples[i];
1636
2289
  float weight = max(sampleData.z, 0.0);
2290
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
2291
+ float receiverDepth = projectedDepth - u_pointShadowBias - slopeTexelBias * sampleTexelDistance;
1637
2292
  shadowed += (receiverDepth > texture(u_pointShadowMapTexture, uv + sampleData.xy * u_pointShadowTexelSize).r ? 1.0 : 0.0) * weight;
1638
2293
  totalWeight += weight;
1639
2294
  }
@@ -1693,11 +2348,16 @@ vec3 a3dPbrEnvironmentDiffuseInput(vec3 normal) {
1693
2348
  float horizonBlend = 1.0 - abs(normal.y);
1694
2349
  vec3 proceduralDiffuse = mix(u_environmentGroundColor, u_environmentSkyColor, skyBlend);
1695
2350
  proceduralDiffuse = mix(proceduralDiffuse, u_environmentHorizonColor, clamp(horizonBlend, 0.0, 1.0) * 0.55);
1696
- vec3 environmentDiffuse = mix(ambientEnvironment, proceduralDiffuse * u_environmentMapIntensity, proceduralEnvironmentWeight);
2351
+ // The ambient term must be added to the procedural contribution, not replaced by it. A mix
2352
+ // here discarded u_environmentColor * u_environmentIntensity entirely whenever a procedural
2353
+ // map was present, which is the normal case: raising ambient intensity from 0.18 to 3.0 on
2354
+ // the product-turntable kit produced a byte-identical frame. Ambient and a sky gradient are
2355
+ // separate physical contributions, so they sum.
2356
+ vec3 environmentDiffuse = ambientEnvironment + proceduralDiffuse * u_environmentMapIntensity * proceduralEnvironmentWeight;
1697
2357
  float sampledEnvironmentWeight = step(0.0001, u_environmentMapTextureEnabled * u_environmentMapTextureIntensity);
1698
2358
  float diffuseEnvironmentLod = max(u_environmentMapTextureMipCount - 1.0, 0.0);
1699
2359
  vec3 sampledDiffuse = a3dPbrDecodeEnvironmentSample(a3dPbrEnvironmentSampleRaw(normal, diffuseEnvironmentLod));
1700
- return mix(environmentDiffuse, environmentDiffuse * 0.18 + sampledDiffuse * u_environmentMapTextureIntensity * 0.92, sampledEnvironmentWeight);
2360
+ return mix(environmentDiffuse, ambientEnvironment + sampledDiffuse * u_environmentMapTextureIntensity, sampledEnvironmentWeight);
1701
2361
  }
1702
2362
  vec3 a3dPbrEnvironmentSpecularInput(vec3 normal, vec3 viewDirection, float roughness) {
1703
2363
  float proceduralEnvironmentWeight = step(0.0001, u_environmentMapIntensity);
@@ -1711,11 +2371,15 @@ vec3 a3dPbrEnvironmentSpecularInput(vec3 normal, vec3 viewDirection, float rough
1711
2371
  float environmentLod = clampedRoughness * max(u_environmentMapTextureMipCount - 1.0, 0.0);
1712
2372
  vec3 sampledSpecular = a3dPbrBoundHdrSpecularRadiance(a3dPbrDecodeEnvironmentSample(a3dPbrEnvironmentSampleRaw(reflectionDirection, environmentLod)));
1713
2373
  float nDotV = clamp(dot(normal, viewDirection), 0.0, 1.0);
1714
- vec2 brdfLut = texture(u_environmentBrdfLutTexture, vec2(nDotV, clampedRoughness)).rg;
1715
2374
  sampledSpecular = a3dPbrClampSampledSpecularEdgeEnergy(sampledSpecular, nDotV, clampedRoughness);
1716
2375
  sampledSpecular *= u_environmentMapTextureSpecularIntensity * sampledEnvironmentWeight * mix(0.84, 0.58, clampedRoughness);
1717
2376
  return proceduralSpecular + sampledSpecular;
1718
2377
  }
2378
+ vec2 a3dPbrEnvironmentBrdfInput(vec3 normal, vec3 viewDirection, float roughness) {
2379
+ float nDotV = clamp(dot(normalize(normal), normalize(viewDirection)), 0.0, 1.0);
2380
+ vec2 lut = texture(u_environmentBrdfLutTexture, vec2(nDotV, clamp(roughness, 0.0, 1.0))).rg;
2381
+ return mix(vec2(1.0, 0.0), lut, step(0.0001, u_environmentBrdfLutEnabled));
2382
+ }
1719
2383
  vec3 a3dPbrEncodeOutput(vec3 linearColor) {
1720
2384
  vec3 color = max(linearColor, vec3(0.0));
1721
2385
  vec3 filmic = clamp((color * (2.51 * color + 0.03)) / (color * (2.43 * color + 0.59) + 0.14), vec3(0.0), vec3(1.0));
@@ -1727,25 +2391,41 @@ void main() {
1727
2391
  if (!gl_FrontFacing) mappedNormal = -mappedNormal;
1728
2392
  vec3 viewDirection = normalize(u_cameraPosition - v_worldPosition);
1729
2393
  vec3 materialBase = u_baseColor.rgb * v_vertexColor.rgb;
1730
- vec3 shaded = u_emissiveColor * u_emissiveStrength + a3dPbrEnvironmentLight(
2394
+ vec3 shaded = u_emissiveColor * u_emissiveStrength + a3dPbrEnvironmentLightSplitSum(
1731
2395
  mappedNormal,
1732
2396
  viewDirection,
1733
2397
  a3dPbrEnvironmentDiffuseInput(mappedNormal),
1734
2398
  a3dPbrEnvironmentSpecularInput(mappedNormal, viewDirection, u_roughness),
2399
+ a3dPbrEnvironmentBrdfInput(mappedNormal, viewDirection, u_roughness),
1735
2400
  materialBase,
1736
2401
  u_metallic,
1737
2402
  u_roughness,
1738
2403
  u_specularFactor,
1739
2404
  u_specularColorFactor
1740
2405
  );
1741
- int count = min(int(u_lightCount), 16);
1742
- for (int i = 0; i < count; ++i) {
1743
- int baseIndex = i * 4;
1744
- vec4 colorIntensity = u_lightData[baseIndex];
1745
- vec4 positionRange = u_lightData[baseIndex + 1];
1746
- vec4 directionKind = u_lightData[baseIndex + 2];
1747
- vec4 spotShadowLayer = u_lightData[baseIndex + 3];
2406
+ ivec2 clusterTile = clamp(ivec2(floor(gl_FragCoord.xy / max(u_clusterViewportSize / u_clusterGridSize, vec2(1.0)))), ivec2(0), ivec2(u_clusterGridSize) - ivec2(1));
2407
+ int clusterIndex = clusterTile.y * int(u_clusterGridSize.x) + clusterTile.x;
2408
+ int count = u_clusteredLightEnabled > 0.5 ? min(int(texelFetch(u_clusterLightIndices, ivec2(0, clusterIndex), 0).g), 64) : min(int(u_lightCount), 16);
2409
+ for (int i = 0; i < 64; ++i) {
2410
+ if (i >= count) break;
2411
+ int lightIndex = u_clusteredLightEnabled > 0.5 ? int(texelFetch(u_clusterLightIndices, ivec2(i, clusterIndex), 0).r) : i;
2412
+ int baseIndex = lightIndex * 6;
2413
+ vec4 colorIntensity = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(0, lightIndex), 0) : u_lightData[baseIndex];
2414
+ vec4 positionRange = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(1, lightIndex), 0) : u_lightData[baseIndex + 1];
2415
+ vec4 directionKind = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(2, lightIndex), 0) : u_lightData[baseIndex + 2];
2416
+ vec4 spotShadowLayer = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(3, lightIndex), 0) : u_lightData[baseIndex + 3];
2417
+ vec4 areaRight = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(4, lightIndex), 0) : u_lightData[baseIndex + 4];
2418
+ vec4 areaUp = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(5, lightIndex), 0) : u_lightData[baseIndex + 5];
1748
2419
  float kind = directionKind.w;
2420
+ if (kind > 2.5) {
2421
+ shaded += a3dPbrRectAreaLight(
2422
+ v_worldPosition, positionRange.xyz, directionKind.xyz, areaRight.xyz, areaUp.xyz,
2423
+ spotShadowLayer.x, spotShadowLayer.y, positionRange.w,
2424
+ mappedNormal, viewDirection, colorIntensity.rgb, colorIntensity.a,
2425
+ materialBase, u_metallic, u_roughness, u_specularFactor, u_specularColorFactor
2426
+ );
2427
+ continue;
2428
+ }
1749
2429
  vec3 lightDirection = -directionKind.xyz;
1750
2430
  float attenuation = 1.0;
1751
2431
  if (kind > 0.5) {
@@ -1778,13 +2458,13 @@ void main() {
1778
2458
  name: DEFAULT_TEXTURED_PBR_SHADER_NAME,
1779
2459
  marker: DEFAULT_TEXTURED_PBR_SHADER_MARKER,
1780
2460
  variants: [
1781
- { name: DEFAULT_TEXTURED_PBR_CLEARCOAT_TEXTURES_VARIANT, defines: { A3D_PBR_CLEARCOAT_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true } },
1782
- { name: DEFAULT_TEXTURED_PBR_TRANSMISSION_VOLUME_TEXTURES_VARIANT, defines: { A3D_PBR_TRANSMISSION_VOLUME_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true } },
1783
- { name: DEFAULT_TEXTURED_PBR_SPECULAR_SHEEN_ANISOTROPY_TEXTURES_VARIANT, defines: { A3D_PBR_SPECULAR_TEXTURES: true, A3D_PBR_SPECULAR_SHEEN_ANISOTROPY_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true } },
1784
- { name: DEFAULT_TEXTURED_PBR_IRIDESCENCE_TEXTURES_VARIANT, defines: { A3D_PBR_IRIDESCENCE_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true } },
1785
- { name: DEFAULT_TEXTURED_PBR_CLEARCOAT_TRANSMISSION_VOLUME_TEXTURES_VARIANT, defines: { A3D_PBR_CLEARCOAT_TEXTURES: true, A3D_PBR_TRANSMISSION_VOLUME_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true } },
1786
- { name: DEFAULT_TEXTURED_PBR_CLEARCOAT_SPECULAR_TEXTURES_VARIANT, defines: { A3D_PBR_CLEARCOAT_TEXTURES: true, A3D_PBR_SPECULAR_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true } },
1787
- { name: DEFAULT_TEXTURED_PBR_SPECULAR_SHEEN_ANISOTROPY_IRIDESCENCE_TEXTURES_VARIANT, defines: { A3D_PBR_SPECULAR_TEXTURES: true, A3D_PBR_SPECULAR_SHEEN_ANISOTROPY_TEXTURES: true, A3D_PBR_IRIDESCENCE_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true } }
2461
+ { name: DEFAULT_TEXTURED_PBR_CLEARCOAT_TEXTURES_VARIANT, defines: { A3D_PBR_CLEARCOAT_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true, A3D_PBR_DISABLE_CLUSTERED_LIGHTING: true } },
2462
+ { name: DEFAULT_TEXTURED_PBR_TRANSMISSION_VOLUME_TEXTURES_VARIANT, defines: { A3D_PBR_TRANSMISSION_VOLUME_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true, A3D_PBR_DISABLE_CLUSTERED_LIGHTING: true } },
2463
+ { name: DEFAULT_TEXTURED_PBR_SPECULAR_SHEEN_ANISOTROPY_TEXTURES_VARIANT, defines: { A3D_PBR_SPECULAR_TEXTURES: true, A3D_PBR_SPECULAR_SHEEN_ANISOTROPY_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true, A3D_PBR_DISABLE_CLUSTERED_LIGHTING: true } },
2464
+ { name: DEFAULT_TEXTURED_PBR_IRIDESCENCE_TEXTURES_VARIANT, defines: { A3D_PBR_IRIDESCENCE_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true, A3D_PBR_DISABLE_CLUSTERED_LIGHTING: true } },
2465
+ { name: DEFAULT_TEXTURED_PBR_CLEARCOAT_TRANSMISSION_VOLUME_TEXTURES_VARIANT, defines: { A3D_PBR_CLEARCOAT_TEXTURES: true, A3D_PBR_TRANSMISSION_VOLUME_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true, A3D_PBR_DISABLE_CLUSTERED_LIGHTING: true } },
2466
+ { name: DEFAULT_TEXTURED_PBR_CLEARCOAT_SPECULAR_TEXTURES_VARIANT, defines: { A3D_PBR_CLEARCOAT_TEXTURES: true, A3D_PBR_SPECULAR_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true, A3D_PBR_DISABLE_CLUSTERED_LIGHTING: true } },
2467
+ { name: DEFAULT_TEXTURED_PBR_SPECULAR_SHEEN_ANISOTROPY_IRIDESCENCE_TEXTURES_VARIANT, defines: { A3D_PBR_SPECULAR_TEXTURES: true, A3D_PBR_SPECULAR_SHEEN_ANISOTROPY_TEXTURES: true, A3D_PBR_IRIDESCENCE_TEXTURES: true, A3D_PBR_DISABLE_TRANSMISSION_BACKDROP: true, A3D_PBR_DISABLE_CLUSTERED_LIGHTING: true } }
1788
2468
  ],
1789
2469
  vertex: `#version 300 es
1790
2470
  // ${DEFAULT_TEXTURED_PBR_SHADER_MARKER}
@@ -1880,7 +2560,14 @@ uniform float u_iridescenceThicknessMinimum;
1880
2560
  uniform float u_iridescenceThicknessMaximum;
1881
2561
  uniform float u_dispersion;
1882
2562
  uniform float u_lightCount;
1883
- uniform vec4 u_lightData[64];
2563
+ uniform vec4 u_lightData[96];
2564
+ #ifndef A3D_PBR_DISABLE_CLUSTERED_LIGHTING
2565
+ uniform float u_clusteredLightEnabled;
2566
+ uniform vec2 u_clusterGridSize;
2567
+ uniform vec2 u_clusterViewportSize;
2568
+ uniform sampler2D u_clusterLightData;
2569
+ uniform sampler2D u_clusterLightIndices;
2570
+ #endif
1884
2571
  uniform sampler2D u_shadowMapTexture;
1885
2572
  uniform float u_shadowMapEnabled;
1886
2573
  uniform mat4 u_shadowMapMatrix;
@@ -2147,8 +2834,18 @@ float a3dTexturedPbrShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDire
2147
2834
  vec3 receiverNormal = normalize(normal);
2148
2835
  vec3 receiverLightDirection = lightDirection / max(length(lightDirection), 0.0001);
2149
2836
  float normalDotLight = clamp(abs(dot(receiverNormal, receiverLightDirection)), 0.0, 1.0);
2150
- float slopeReceiverBias = (1.0 - normalDotLight) * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
2151
- float receiverDepth = projected.z * 0.5 + 0.5 - u_shadowMapBias - slopeReceiverBias;
2837
+ // Slope-scaled depth bias must be evaluated per PCF sample, not once for the kernel
2838
+ // centre. A sample offset N texels away on a receiver sloped relative to the light sees a
2839
+ // depth difference proportional to N, so a centre-only bias under-compensates every outer
2840
+ // tap and the receiver shadows itself. Scaling by each sample's own texel distance keeps
2841
+ // wide kernels acne-free without inflating the constant bias into peter-panning.
2842
+ // The depth gradient across one shadow texel is tan(angle between receiver normal and
2843
+ // light), not (1 - N.L). The linear form collapses toward zero far faster than the real
2844
+ // gradient grows, so it under-biases exactly the grazing angles that need the most
2845
+ // compensation. Clamped so a near-perpendicular receiver cannot demand unbounded bias.
2846
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
2847
+ float slopeTexelBias = slopeTangent * u_shadowMapSlopeBias * max(u_shadowMapTexelSize.x, u_shadowMapTexelSize.y);
2848
+ float projectedDepth = projected.z * 0.5 + 0.5;
2152
2849
  float shadowed = 0.0;
2153
2850
  float totalWeight = 0.0;
2154
2851
  int sampleCount = clamp(int(u_shadowPcfSampleCount), 1, 32);
@@ -2158,6 +2855,8 @@ float a3dTexturedPbrShadowFactor(vec3 worldPosition, vec3 normal, vec3 lightDire
2158
2855
  float weight = max(sampleData.z, 0.0);
2159
2856
  vec2 offset = sampleData.xy * u_shadowMapTexelSize;
2160
2857
  float storedDepth = texture(u_shadowMapTexture, uv + offset).r;
2858
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
2859
+ float receiverDepth = projectedDepth - u_shadowMapBias - slopeTexelBias * sampleTexelDistance;
2161
2860
  shadowed += (receiverDepth > storedDepth ? 1.0 : 0.0) * weight;
2162
2861
  totalWeight += weight;
2163
2862
  }
@@ -2176,7 +2875,12 @@ float a3dTexturedPbrPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 ligh
2176
2875
  vec4 rect = u_pointShadowFaceRects[faceIndex];
2177
2876
  vec2 uv = rect.xy + localUv * rect.zw;
2178
2877
  float normalDotLight = clamp(abs(dot(normalize(normal), normalize(lightDirection))), 0.0, 1.0);
2179
- float receiverDepth = projected.z * 0.5 + 0.5 - u_pointShadowBias - (1.0 - normalDotLight) * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
2878
+ // Same per-sample, tangent-scaled slope bias as the directional and atlas paths. These
2879
+ // cubemap-face variants inline the sample fetch, but they share the identical acne
2880
+ // mechanism: a centre-only, (1 - N.L)-scaled bias under-compensates every outer tap.
2881
+ float slopeTangent = min(sqrt(max(1.0 - normalDotLight * normalDotLight, 0.0)) / max(normalDotLight, 0.05), 8.0);
2882
+ float slopeTexelBias = slopeTangent * u_pointShadowSlopeBias * max(u_pointShadowTexelSize.x, u_pointShadowTexelSize.y);
2883
+ float projectedDepth = projected.z * 0.5 + 0.5;
2180
2884
  float shadowed = 0.0;
2181
2885
  float totalWeight = 0.0;
2182
2886
  int sampleCount = clamp(int(u_pointShadowPcfSampleCount), 1, 32);
@@ -2184,6 +2888,8 @@ float a3dTexturedPbrPointShadowFactor(vec3 worldPosition, vec3 normal, vec3 ligh
2184
2888
  if (i >= sampleCount) break;
2185
2889
  vec4 sampleData = u_pointShadowPcfSamples[i];
2186
2890
  float weight = max(sampleData.z, 0.0);
2891
+ float sampleTexelDistance = max(1.0, length(sampleData.xy));
2892
+ float receiverDepth = projectedDepth - u_pointShadowBias - slopeTexelBias * sampleTexelDistance;
2187
2893
  shadowed += (receiverDepth > texture(u_pointShadowMapTexture, uv + sampleData.xy * u_pointShadowTexelSize).r ? 1.0 : 0.0) * weight;
2188
2894
  totalWeight += weight;
2189
2895
  }
@@ -2229,11 +2935,16 @@ vec3 a3dTexturedPbrEnvironmentDiffuseInput(vec3 normal) {
2229
2935
  float horizonBlend = 1.0 - abs(normal.y);
2230
2936
  vec3 proceduralDiffuse = mix(u_environmentGroundColor, u_environmentSkyColor, skyBlend);
2231
2937
  proceduralDiffuse = mix(proceduralDiffuse, u_environmentHorizonColor, clamp(horizonBlend, 0.0, 1.0) * 0.55);
2232
- vec3 environmentDiffuse = mix(ambientEnvironment, proceduralDiffuse * u_environmentMapIntensity, proceduralEnvironmentWeight);
2938
+ // The ambient term must be added to the procedural contribution, not replaced by it. A mix
2939
+ // here discarded u_environmentColor * u_environmentIntensity entirely whenever a procedural
2940
+ // map was present, which is the normal case: raising ambient intensity from 0.18 to 3.0 on
2941
+ // the product-turntable kit produced a byte-identical frame. Ambient and a sky gradient are
2942
+ // separate physical contributions, so they sum.
2943
+ vec3 environmentDiffuse = ambientEnvironment + proceduralDiffuse * u_environmentMapIntensity * proceduralEnvironmentWeight;
2233
2944
  float sampledEnvironmentWeight = step(0.0001, u_environmentMapTextureEnabled * u_environmentMapTextureIntensity);
2234
2945
  float diffuseEnvironmentLod = max(u_environmentMapTextureMipCount - 1.0, 0.0);
2235
2946
  vec3 sampledDiffuse = a3dTexturedPbrDecodeEnvironmentSample(a3dTexturedPbrEnvironmentSampleRaw(normal, diffuseEnvironmentLod));
2236
- return mix(environmentDiffuse, environmentDiffuse * 0.18 + sampledDiffuse * u_environmentMapTextureIntensity * 0.92, sampledEnvironmentWeight);
2947
+ return mix(environmentDiffuse, ambientEnvironment + sampledDiffuse * u_environmentMapTextureIntensity, sampledEnvironmentWeight);
2237
2948
  }
2238
2949
  vec3 a3dTexturedPbrEnvironmentSpecularInput(vec3 normal, vec3 viewDirection, float roughness) {
2239
2950
  float materialEnvironmentSpecularScale = clamp(u_materialEnvironmentSpecularScale, 0.0, 1.0);
@@ -2257,7 +2968,6 @@ vec3 a3dTexturedPbrEnvironmentSpecularInput(vec3 normal, vec3 viewDirection, flo
2257
2968
  float sampledEnvironmentWeight = step(0.0001, u_environmentMapTextureEnabled * u_environmentMapTextureSpecularIntensity);
2258
2969
  float environmentLod = clampedRoughness * max(u_environmentMapTextureMipCount - 1.0, 0.0);
2259
2970
  vec3 sampledSpecular = a3dTexturedPbrBoundHdrSpecularRadiance(a3dTexturedPbrDecodeEnvironmentSample(a3dTexturedPbrEnvironmentSampleRaw(reflectionDirection, environmentLod)));
2260
- vec2 brdfLut = texture(u_environmentBrdfLutTexture, vec2(nDotV, clampedRoughness)).rg;
2261
2971
  sampledSpecular = a3dTexturedPbrClampSampledSpecularEdgeEnergy(sampledSpecular, nDotV, clampedRoughness);
2262
2972
  sampledSpecular *= u_environmentMapTextureSpecularIntensity * sampledEnvironmentWeight * mix(0.84, 0.58, clampedRoughness);
2263
2973
  float proceduralSpecularScale = materialFiniteSpecularScale * mix(0.34, 1.0, faceOnSpecularGate);
@@ -2570,14 +3280,43 @@ void main() {
2570
3280
  #endif
2571
3281
  shaded = mix(shaded, shaded * 0.72 + texturedRefractionRadiance, texturedTransmissionAmount * mix(0.08, 0.58, texturedFallbackEnvironmentTransmissionEnergy));
2572
3282
  }
3283
+ #ifndef A3D_PBR_DISABLE_CLUSTERED_LIGHTING
3284
+ ivec2 clusterTile = clamp(ivec2(floor(gl_FragCoord.xy / max(u_clusterViewportSize / u_clusterGridSize, vec2(1.0)))), ivec2(0), ivec2(u_clusterGridSize) - ivec2(1));
3285
+ int clusterIndex = clusterTile.y * int(u_clusterGridSize.x) + clusterTile.x;
3286
+ int count = u_clusteredLightEnabled > 0.5 ? min(int(texelFetch(u_clusterLightIndices, ivec2(0, clusterIndex), 0).g), 64) : min(int(u_lightCount), 16);
3287
+ #else
2573
3288
  int count = min(int(u_lightCount), 16);
2574
- for (int i = 0; i < count; ++i) {
2575
- int baseIndex = i * 4;
3289
+ #endif
3290
+ for (int i = 0; i < 64; ++i) {
3291
+ if (i >= count) break;
3292
+ #ifndef A3D_PBR_DISABLE_CLUSTERED_LIGHTING
3293
+ int lightIndex = u_clusteredLightEnabled > 0.5 ? int(texelFetch(u_clusterLightIndices, ivec2(i, clusterIndex), 0).r) : i;
3294
+ int baseIndex = lightIndex * 6;
3295
+ vec4 colorIntensity = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(0, lightIndex), 0) : u_lightData[baseIndex];
3296
+ vec4 positionRange = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(1, lightIndex), 0) : u_lightData[baseIndex + 1];
3297
+ vec4 directionKind = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(2, lightIndex), 0) : u_lightData[baseIndex + 2];
3298
+ vec4 spotShadowLayer = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(3, lightIndex), 0) : u_lightData[baseIndex + 3];
3299
+ vec4 areaRight = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(4, lightIndex), 0) : u_lightData[baseIndex + 4];
3300
+ vec4 areaUp = u_clusteredLightEnabled > 0.5 ? texelFetch(u_clusterLightData, ivec2(5, lightIndex), 0) : u_lightData[baseIndex + 5];
3301
+ #else
3302
+ int baseIndex = i * 6;
2576
3303
  vec4 colorIntensity = u_lightData[baseIndex];
2577
3304
  vec4 positionRange = u_lightData[baseIndex + 1];
2578
3305
  vec4 directionKind = u_lightData[baseIndex + 2];
2579
3306
  vec4 spotShadowLayer = u_lightData[baseIndex + 3];
3307
+ vec4 areaRight = u_lightData[baseIndex + 4];
3308
+ vec4 areaUp = u_lightData[baseIndex + 5];
3309
+ #endif
2580
3310
  float kind = directionKind.w;
3311
+ if (kind > 2.5) {
3312
+ shaded += a3dPbrRectAreaLight(
3313
+ v_worldPosition, positionRange.xyz, directionKind.xyz, areaRight.xyz, areaUp.xyz,
3314
+ spotShadowLayer.x, spotShadowLayer.y, positionRange.w,
3315
+ mappedNormal, viewDirection, colorIntensity.rgb, colorIntensity.a,
3316
+ base, metallic, roughness, specular, specularColor
3317
+ ) * directTextureOcclusion;
3318
+ continue;
3319
+ }
2581
3320
  vec3 lightDirection = -directionKind.xyz;
2582
3321
  float attenuation = 1.0;
2583
3322
  if (kind > 0.5) {
@@ -2703,6 +3442,109 @@ void main() {
2703
3442
  vec3 color = a3dBackgroundDecode(encodedSample) * max(u_environmentBackgroundIntensity, 0.0);
2704
3443
  outColor = vec4(a3dBackgroundEncodeOutput(color), 1.0);
2705
3444
  }
3445
+ `
3446
+ });
3447
+ library.register({
3448
+ name: DEFAULT_SCREEN_SPACE_LINE_SHADER_NAME,
3449
+ marker: DEFAULT_SCREEN_SPACE_LINE_SHADER_MARKER,
3450
+ vertex: `#version 300 es
3451
+ // ${DEFAULT_SCREEN_SPACE_LINE_SHADER_MARKER}
3452
+ precision highp float;
3453
+ layout(location = 0) in vec3 a_position;
3454
+ layout(location = 10) in vec3 a_lineStart;
3455
+ layout(location = 11) in vec3 a_lineEnd;
3456
+ layout(location = 12) in vec2 a_lineCorner;
3457
+ layout(location = 13) in float a_lineDistance;
3458
+ uniform mat4 u_modelViewProjection;
3459
+ /** Line width in CSS pixels. Multiplied by DPR to reach device pixels. */
3460
+ uniform float u_lineWidth;
3461
+ /** Device-pixel resolution of the render target. */
3462
+ uniform vec2 u_lineResolution;
3463
+ uniform float u_linePixelRatio;
3464
+ /** 0 = butt caps, 1 = square caps extended by half a width past each endpoint. */
3465
+ uniform float u_lineSquareCaps;
3466
+ out vec2 v_lineCorner;
3467
+ out float v_lineDistance;
3468
+ out float v_halfWidthPixels;
3469
+
3470
+ void main() {
3471
+ // a_position holds this vertex's anchor endpoint in world space. It is referenced
3472
+ // with zero weight so the attribute is not optimized out of the linked program:
3473
+ // bounds computation, frustum culling, and the material's attribute contract all
3474
+ // depend on the geometry declaring a real position stream.
3475
+ vec3 anchorWorld = a_position;
3476
+ vec4 clipStart = u_modelViewProjection * vec4(a_lineStart + anchorWorld * 0.0, 1.0);
3477
+ vec4 clipEnd = u_modelViewProjection * vec4(a_lineEnd, 1.0);
3478
+
3479
+ // Guard against a zero or negative w: a vertex behind the eye would otherwise
3480
+ // produce a wildly wrong NDC position and a smeared quad.
3481
+ float wStart = max(abs(clipStart.w), 1e-5);
3482
+ float wEnd = max(abs(clipEnd.w), 1e-5);
3483
+
3484
+ // Project to device pixels. Expansion happens here, so the resulting width is a
3485
+ // pixel quantity and is therefore independent of depth, FOV, and viewport size.
3486
+ vec2 pixelStart = (clipStart.xy / wStart) * 0.5 * u_lineResolution;
3487
+ vec2 pixelEnd = (clipEnd.xy / wEnd) * 0.5 * u_lineResolution;
3488
+
3489
+ vec2 pixelDirection = pixelEnd - pixelStart;
3490
+ float pixelLength = length(pixelDirection);
3491
+ // A segment that projects to a single pixel has no screen direction; pick an
3492
+ // arbitrary axis so the cap still renders instead of collapsing.
3493
+ vec2 direction = pixelLength > 1e-4 ? pixelDirection / pixelLength : vec2(1.0, 0.0);
3494
+ vec2 normal = vec2(-direction.y, direction.x);
3495
+
3496
+ float halfWidth = max(u_lineWidth, 0.0) * max(u_linePixelRatio, 0.0001) * 0.5;
3497
+ v_halfWidthPixels = halfWidth;
3498
+
3499
+ float along = a_lineCorner.y;
3500
+ vec2 anchorPixel = mix(pixelStart, pixelEnd, along);
3501
+ // Square caps push the quad outward along the segment direction so the stroke
3502
+ // extends half a width past each endpoint, matching Three.js LineMaterial behaviour.
3503
+ float capExtension = u_lineSquareCaps > 0.5 ? halfWidth : 0.0;
3504
+ vec2 capOffset = direction * (along > 0.5 ? capExtension : -capExtension);
3505
+ vec2 pixelPosition = anchorPixel + normal * (a_lineCorner.x * halfWidth) + capOffset;
3506
+
3507
+ // Back to clip space, preserving the anchor endpoint's own depth so the line
3508
+ // depth-sorts against the rest of the scene correctly.
3509
+ vec4 anchorClip = along > 0.5 ? clipEnd : clipStart;
3510
+ float anchorW = along > 0.5 ? wEnd : wStart;
3511
+ vec2 ndc = pixelPosition / (0.5 * u_lineResolution);
3512
+ gl_Position = vec4(ndc * anchorW, anchorClip.z, anchorClip.w);
3513
+
3514
+ v_lineCorner = a_lineCorner;
3515
+ v_lineDistance = a_lineDistance;
3516
+ }
3517
+ `,
3518
+ fragment: `#version 300 es
3519
+ // ${DEFAULT_SCREEN_SPACE_LINE_SHADER_MARKER}
3520
+ precision highp float;
3521
+ uniform vec4 u_baseColor;
3522
+ /** Dash period in world units. Zero disables dashing. */
3523
+ uniform float u_lineDashSize;
3524
+ uniform float u_lineGapSize;
3525
+ uniform float u_lineDashOffset;
3526
+ /** 0 = butt/square, 1 = round caps and joins. */
3527
+ uniform float u_lineRoundCaps;
3528
+ in vec2 v_lineCorner;
3529
+ in float v_lineDistance;
3530
+ in float v_halfWidthPixels;
3531
+ out vec4 outColor;
3532
+
3533
+ void main() {
3534
+ if (u_lineDashSize > 0.0) {
3535
+ float period = u_lineDashSize + max(u_lineGapSize, 0.0);
3536
+ float phase = mod(v_lineDistance + u_lineDashOffset, period);
3537
+ // Discard the gap portion of each dash period.
3538
+ if (phase > u_lineDashSize) discard;
3539
+ }
3540
+ if (u_lineRoundCaps > 0.5) {
3541
+ // Round caps: reject fragments outside the unit half-disc at each end. The
3542
+ // corner's x component already spans -1..1 across the stroke width.
3543
+ float acrossStroke = v_lineCorner.x;
3544
+ if (abs(acrossStroke) > 1.0) discard;
3545
+ }
3546
+ outColor = u_baseColor;
3547
+ }
2706
3548
  `
2707
3549
  });
2708
3550
  library.register({