@forgeax/engine-shader 0.1.23 → 0.1.25

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 (60) hide show
  1. package/README.md +110 -4
  2. package/dist/ShaderRegistry.d.ts.map +1 -1
  3. package/dist/index.d.ts +10 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.mjs +59 -55
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/material/artifact-registry.d.ts +1 -0
  8. package/dist/material/artifact-registry.d.ts.map +1 -1
  9. package/dist/material-schemas.d.ts +31 -0
  10. package/dist/material-schemas.d.ts.map +1 -1
  11. package/package.json +4 -4
  12. package/src/ShaderRegistry.ts +0 -17
  13. package/src/__tests__/builtin-texture-sampling-contract.test.ts +10 -1
  14. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +9 -3
  15. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +48 -166
  16. package/src/__tests__/deferred-lighting-ssao.test.ts +7 -4
  17. package/src/__tests__/ibl-irradiance.unit.test.ts +9 -0
  18. package/src/__tests__/lighting-directional.unit.test.ts +36 -0
  19. package/src/__tests__/lighting-punctual.unit.test.ts +12 -4
  20. package/src/__tests__/material-builtins.unit.test.ts +21 -3
  21. package/src/__tests__/material-contract.unit.test.ts +131 -17
  22. package/src/__tests__/physical-clearcoat.unit.test.ts +65 -0
  23. package/src/__tests__/probe-lighting-composition.unit.test.ts +3 -2
  24. package/src/__tests__/shader-manifest-transmission.unit.test.ts +8 -10
  25. package/src/__tests__/shader.unit.test.ts +52 -87
  26. package/src/__tests__/spot-cookie-composition.unit.test.ts +2 -1
  27. package/src/__tests__/standard-pbr-webgl2-interface.unit.test.ts +33 -0
  28. package/src/__tests__/standard-physical-layer.unit.test.ts +16 -0
  29. package/src/__tests__/standard-surface-pass-evaluation.integration.test.ts +38 -0
  30. package/src/__tests__/standard-webgl2-varying-budget.unit.test.ts +17 -0
  31. package/src/__tests__/surface-lighting-provenance.integration.test.ts +59 -0
  32. package/src/__tests__/surface-pass-templates.integration.test.ts +28 -0
  33. package/src/__tests__/surface-v1-negative.unit.test.ts +37 -0
  34. package/src/__tests__/surface-v1.unit.test.ts +83 -0
  35. package/src/__tests__/transmission-thickness-scale.unit.test.ts +2 -6
  36. package/src/__tests__/vertex-color-variant.unit.test.ts +16 -5
  37. package/src/default-standard-pbr-skin.wgsl +704 -192
  38. package/src/default-standard-pbr.wgsl +563 -279
  39. package/src/default_standard_surface.wgsl +88 -0
  40. package/src/hdrp-cluster-forward.wgsl +225 -0
  41. package/src/ibl-irradiance.wgsl +43 -8
  42. package/src/ibl-prefilter.wgsl +13 -3
  43. package/src/ibl-shared.wgsl +9 -2
  44. package/src/index.ts +37 -3
  45. package/src/lighting-directional.wgsl +3 -1
  46. package/src/lighting-spot-modifiers.wgsl +8 -1
  47. package/src/lighting-spot-projector.wgsl +31 -0
  48. package/src/material/artifact-registry.ts +20 -1
  49. package/src/material/artifact-types.ts +2 -2
  50. package/src/material/physical/anisotropy.wgsl +26 -0
  51. package/src/material/physical/clearcoat.wgsl +24 -0
  52. package/src/material/physical/iridescence.wgsl +23 -0
  53. package/src/material/physical/sheen.wgsl +17 -0
  54. package/src/material/standard-physical-layer.wgsl +12 -0
  55. package/src/material-schemas.ts +65 -30
  56. package/src/register-default-standard-pbr-skin.ts +2 -2
  57. package/src/shadow_caster.wgsl +80 -6
  58. package/src/standard-cluster.wgsl +1 -36
  59. package/src/surface_v1.wgsl +26 -0
  60. package/src/unlit.wgsl +12 -0
@@ -1,11 +1,34 @@
1
1
  #define_import_path forgeax_material::standard
2
+ #pragma material_slot surface
3
+ #import forgeax_material::slot::surface::{evaluate_surface}
4
+ #import forgeax_material::surface_v1::{SurfaceInput, SurfaceData}
2
5
  #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, shadowMap, shadowSampler, sampleMaterialTexture}
3
6
  #import forgeax_scene_temporal::{sceneViewZ}
7
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
8
+ #import forgeax_view::common::{spotModifierSampler, iesProfileTexture, cookieTexture, cookieMatrices}
9
+ #endif
10
+ #ifdef PROJECTOR_AVAILABLE
11
+ #ifndef EXTENDED_LIGHTING_AVAILABLE
12
+ #import forgeax_view::common::{projectorTexture, projectorSampler}
13
+ #endif
14
+ #endif
4
15
  #import forgeax_pbr::temporal::{projectPbrSceneTemporal}
5
16
  #import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
6
- #import forgeax_pbr::ibl_sampling::{sampleIblDiffuse, sampleIblSpecular, sampleReflectionProbeSpecular}
17
+ #import forgeax_pbr::ibl_sampling::{box_project, sampleIblDiffuse, sampleIblSpecular, sampleReflectionProbeSpecular}
7
18
  #import forgeax_pbr::lighting_probe::{evaluateProbeDiffuse}
8
19
  #import forgeax_pbr::tbn::{decodeTangentSpaceNormalRg, scaleTangentSpaceNormal, applyTBN}
20
+ #ifdef CLEARCOAT_AVAILABLE
21
+ #import forgeax_pbr::clearcoat::{evaluateClearcoatLayer}
22
+ #endif
23
+ #ifdef ANISOTROPY_AVAILABLE
24
+ #import forgeax_pbr::anisotropy::{evaluateAnisotropicNormal}
25
+ #endif
26
+ #ifdef SHEEN_AVAILABLE
27
+ #import forgeax_pbr::sheen::{evaluateSheenLayer}
28
+ #endif
29
+ #ifdef IRIDESCENCE_AVAILABLE
30
+ #import forgeax_pbr::iridescence::{evaluateIridescenceFresnel}
31
+ #endif
9
32
  #import forgeax_pbr::lighting_directional::{evalDirectionalNoShadow, evalDirectionalShadowFactor}
10
33
  #ifdef CLUSTER_FORWARD_AVAILABLE
11
34
  #import forgeax_standard::cluster::{evaluateStandardClusterLights, get_ssao_intensity}
@@ -21,6 +44,23 @@
21
44
  #pragma variant_axis PROJECTOR_AVAILABLE
22
45
  #pragma variant_axis REFLECTION_FALLBACK_AVAILABLE
23
46
 
47
+ // reflection_probe is a scene-projected Standard material lane. Its optional
48
+ // texture arguments stay binding-free here; the host selects the bounded table
49
+ // row and falls back to the Skylight helpers when no candidate is admitted.
50
+ // Keep this compile-time witness in the probe variant only: authored physical
51
+ // aliases intentionally flatten renderer-owned probe axes, and naga_oil must
52
+ // not resolve a probe-only helper for those roots.
53
+ #ifdef PROBE_BLEND_AVAILABLE
54
+ fn reflection_probe_sampling_contract(
55
+ worldPosition: vec3<f32>,
56
+ direction: vec3<f32>,
57
+ boxCenter: vec3<f32>,
58
+ boxExtents: vec3<f32>,
59
+ ) -> vec3<f32> {
60
+ return box_project(worldPosition, direction, boxCenter, boxExtents);
61
+ }
62
+ #endif
63
+
24
64
  // @forgeax/engine-shader - default-standard-pbr.wgsl
25
65
  // (feat-20260523-shader-template-instance-split M5 / T04).
26
66
  //
@@ -42,14 +82,13 @@
42
82
  // + metallic + roughness
43
83
  // + 4 channel selectors f32
44
84
  // + emissive vec3 + emissiveIntensity
45
- // + uv/alpha/clearcoat/specularTint = 112 B)
85
+ // + uv/alpha/specularColor = 104 B)
46
86
  // @group(1) @binding(1) baseColorSampler sampler
47
87
  // @group(1) @binding(2) baseColorTexture texture_2d<f32>
48
88
  // @group(1) @binding(3) metallicRoughnessSampler sampler
49
89
  // @group(1) @binding(4) metallicRoughnessTexture texture_2d<f32>
50
90
  // @group(1) @binding(5) normalSampler sampler
51
91
  // @group(1) @binding(6) normalTexture texture_2d<f32>
52
- // @group(1) @binding(7) specularTintSampler + @binding(8) specularTintTexture
53
92
  // @group(1) @binding(9..15) Skylight (irradiance / prefilter / brdfLut +
54
93
  // samplers) + skylight uniform (intensity)
55
94
  // @group(2) @binding(0) meshes storage (worldFromLocal mat4
@@ -89,63 +128,16 @@
89
128
  // and tolerates RGB normal maps (b is dropped, z is recomputed --
90
129
  // equivalent for unit vectors).
91
130
 
92
- struct Material {
93
- baseColor : vec4<f32>,
94
- metallic : f32,
95
- roughness : f32,
96
- // Channel selectors (D-8): 4 independent f32 entries split out of the
97
- // legacy `channelMap : vec4<u32>`. Each value is a small integer encoded
98
- // as f32 in {0.0, 1.0, 2.0, 3.0} indexing into {r,g,b,a}. Default glTF 2.0
99
- // packing = (metallicChannel=2, roughnessChannel=1, aoChannel=0,
100
- // extraChannel=0). The fragment casts to u32 at the pick site -- f32 is
101
- // chosen so the schema entry type aligns with the 14-tuple
102
- // MaterialParamType numeric-run packing rule (4-byte stride, 16 B span).
103
- metallicChannel : f32,
104
- roughnessChannel : f32,
105
- aoChannel : f32,
106
- extraChannel : f32,
107
- // vec3 align=16 inserts 8 implicit padding bytes after extraChannel
108
- // (offsets 40..48) so emissive lands at offset 48.
109
- emissive : vec3<f32>,
110
- emissiveIntensity : f32,
111
- occlusionStrength : f32,
112
- alphaCutoff : f32,
113
- clearcoat : f32,
114
- clearcoatRoughness : f32,
115
- specularTint : vec3<f32>,
116
- normalScale : f32,
117
- transmission : f32,
118
- ior : f32,
119
- thickness : f32,
120
- attenuationColor : vec3<f32>,
121
- attenuationDistance : f32,
122
- baseColorTextureCoordinatesTransform : vec4<f32>,
123
- baseColorTextureCoordinatesMetadata : vec4<f32>,
124
- metallicRoughnessTextureCoordinatesTransform : vec4<f32>,
125
- metallicRoughnessTextureCoordinatesMetadata : vec4<f32>,
126
- normalTextureCoordinatesTransform : vec4<f32>,
127
- normalTextureCoordinatesMetadata : vec4<f32>,
128
- specularTintTextureCoordinatesTransform : vec4<f32>,
129
- specularTintTextureCoordinatesMetadata : vec4<f32>,
130
- emissiveTextureCoordinatesTransform : vec4<f32>,
131
- emissiveTextureCoordinatesMetadata : vec4<f32>,
132
- occlusionTextureCoordinatesTransform : vec4<f32>,
133
- occlusionTextureCoordinatesMetadata : vec4<f32>,
134
- transmissionTextureCoordinatesTransform : vec4<f32>,
135
- transmissionTextureCoordinatesMetadata : vec4<f32>,
136
- thicknessTextureCoordinatesTransform : vec4<f32>,
137
- thicknessTextureCoordinatesMetadata : vec4<f32>,
138
- };
139
-
140
- @group(1) @binding(0) var<uniform> material : Material;
131
+ // The MaterialParameters struct and its binding-0 declaration are generated
132
+ // from the root ParamSchema during composition. Keeping the ABI out of this
133
+ // template prevents a second handwritten interface from drifting from the
134
+ // runtime UBO writer.
141
135
  @group(1) @binding(1) var baseColorSampler : sampler;
142
136
  @group(1) @binding(2) var baseColorTexture : texture_2d<f32>;
143
137
  @group(1) @binding(3) var metallicRoughnessSampler : sampler;
144
138
  @group(1) @binding(4) var metallicRoughnessTexture : texture_2d<f32>;
145
139
  @group(1) @binding(5) var normalSampler : sampler;
146
140
  @group(1) @binding(6) var normalTexture : texture_2d<f32>;
147
- @group(1) @binding(7) var specularTintSampler : sampler;
148
- @group(1) @binding(8) var specularTintTexture : texture_2d<f32>;
149
141
  @group(1) @binding(9) var emissiveSampler : sampler;
150
142
  @group(1) @binding(10) var emissiveTexture : texture_2d<f32>;
151
143
  @group(1) @binding(11) var occlusionSampler : sampler;
@@ -158,6 +150,46 @@ struct Material {
158
150
  @group(1) @binding(24) var transmissionBackdropSampler : sampler;
159
151
  @group(1) @binding(25) var transmissionBackdropTexture : texture_2d<f32>;
160
152
  #endif
153
+ #ifdef CLEARCOAT_TEXTURE_AVAILABLE
154
+ @group(1) @binding(26) var clearcoatSampler : sampler;
155
+ @group(1) @binding(27) var clearcoatTexture : texture_2d<f32>;
156
+ #endif
157
+ #ifdef CLEARCOAT_ROUGHNESS_TEXTURE_AVAILABLE
158
+ @group(1) @binding(28) var clearcoatRoughnessSampler : sampler;
159
+ @group(1) @binding(29) var clearcoatRoughnessTexture : texture_2d<f32>;
160
+ #endif
161
+ #ifdef CLEARCOAT_NORMAL_TEXTURE_AVAILABLE
162
+ @group(1) @binding(30) var clearcoatNormalSampler : sampler;
163
+ @group(1) @binding(31) var clearcoatNormalTexture : texture_2d<f32>;
164
+ #endif
165
+ #ifdef ANISOTROPY_TEXTURE_AVAILABLE
166
+ @group(1) @binding(32) var anisotropySampler : sampler;
167
+ @group(1) @binding(33) var anisotropyTexture : texture_2d<f32>;
168
+ #endif
169
+ #ifdef SHEEN_COLOR_TEXTURE_AVAILABLE
170
+ @group(1) @binding(34) var sheenColorSampler : sampler;
171
+ @group(1) @binding(35) var sheenColorTexture : texture_2d<f32>;
172
+ #endif
173
+ #ifdef SHEEN_ROUGHNESS_TEXTURE_AVAILABLE
174
+ @group(1) @binding(36) var sheenRoughnessSampler : sampler;
175
+ @group(1) @binding(37) var sheenRoughnessTexture : texture_2d<f32>;
176
+ #endif
177
+ #ifdef IRIDESCENCE_TEXTURE_AVAILABLE
178
+ @group(1) @binding(38) var iridescenceSampler : sampler;
179
+ @group(1) @binding(39) var iridescenceTexture : texture_2d<f32>;
180
+ #endif
181
+ #ifdef IRIDESCENCE_THICKNESS_TEXTURE_AVAILABLE
182
+ @group(1) @binding(40) var iridescenceThicknessSampler : sampler;
183
+ @group(1) @binding(41) var iridescenceThicknessTexture : texture_2d<f32>;
184
+ #endif
185
+ #ifdef SPECULAR_TEXTURE_AVAILABLE
186
+ @group(1) @binding(42) var specularTextureSampler : sampler;
187
+ @group(1) @binding(43) var specularTexture : texture_2d<f32>;
188
+ #endif
189
+ #ifdef SPECULAR_COLOR_TEXTURE_AVAILABLE
190
+ @group(1) @binding(44) var specularColorTextureSampler : sampler;
191
+ @group(1) @binding(45) var specularColorTexture : texture_2d<f32>;
192
+ #endif
161
193
 
162
194
  // Naga reflection does not retain filtering usage through the generic shared
163
195
  // helper. These compile-time-only witnesses retain the binding contract while
@@ -166,15 +198,52 @@ fn materialTextureFilteringWitness() {
166
198
  let base = baseColorTexture;
167
199
  let metallicRoughness = metallicRoughnessTexture;
168
200
  let normal = normalTexture;
169
- let specularTint = specularTintTexture;
170
201
  let emissive = emissiveTexture;
171
202
  let occlusion = occlusionTexture;
203
+ #ifdef CLEARCOAT_TEXTURE_AVAILABLE
204
+ let clearcoat = clearcoatTexture;
205
+ #endif
206
+ #ifdef CLEARCOAT_ROUGHNESS_TEXTURE_AVAILABLE
207
+ let clearcoatRoughness = clearcoatRoughnessTexture;
208
+ #endif
209
+ #ifdef CLEARCOAT_NORMAL_TEXTURE_AVAILABLE
210
+ let clearcoatNormal = clearcoatNormalTexture;
211
+ #endif
172
212
  let baseWitness = textureSample(base, baseColorSampler, vec2<f32>(0.0));
173
213
  let metallicRoughnessWitness = textureSample(metallicRoughness, metallicRoughnessSampler, vec2<f32>(0.0));
174
214
  let normalWitness = textureSample(normal, normalSampler, vec2<f32>(0.0));
175
- let specularTintWitness = textureSample(specularTint, specularTintSampler, vec2<f32>(0.0));
176
215
  let emissiveWitness = textureSample(emissive, emissiveSampler, vec2<f32>(0.0));
177
216
  let occlusionWitness = textureSample(occlusion, occlusionSampler, vec2<f32>(0.0));
217
+ #ifdef CLEARCOAT_TEXTURE_AVAILABLE
218
+ let clearcoatWitness = textureSample(clearcoat, clearcoatSampler, vec2<f32>(0.0));
219
+ #endif
220
+ #ifdef CLEARCOAT_ROUGHNESS_TEXTURE_AVAILABLE
221
+ let clearcoatRoughnessWitness = textureSample(clearcoatRoughness, clearcoatRoughnessSampler, vec2<f32>(0.0));
222
+ #endif
223
+ #ifdef CLEARCOAT_NORMAL_TEXTURE_AVAILABLE
224
+ let clearcoatNormalWitness = textureSample(clearcoatNormal, clearcoatNormalSampler, vec2<f32>(0.0));
225
+ #endif
226
+ #ifdef ANISOTROPY_TEXTURE_AVAILABLE
227
+ let anisotropyWitness = textureSample(anisotropyTexture, anisotropySampler, vec2<f32>(0.0));
228
+ #endif
229
+ #ifdef SHEEN_COLOR_TEXTURE_AVAILABLE
230
+ let sheenColorWitness = textureSample(sheenColorTexture, sheenColorSampler, vec2<f32>(0.0));
231
+ #endif
232
+ #ifdef SHEEN_ROUGHNESS_TEXTURE_AVAILABLE
233
+ let sheenRoughnessWitness = textureSample(sheenRoughnessTexture, sheenRoughnessSampler, vec2<f32>(0.0));
234
+ #endif
235
+ #ifdef IRIDESCENCE_TEXTURE_AVAILABLE
236
+ let iridescenceWitness = textureSample(iridescenceTexture, iridescenceSampler, vec2<f32>(0.0));
237
+ #endif
238
+ #ifdef IRIDESCENCE_THICKNESS_TEXTURE_AVAILABLE
239
+ let iridescenceThicknessWitness = textureSample(iridescenceThicknessTexture, iridescenceThicknessSampler, vec2<f32>(0.0));
240
+ #endif
241
+ #ifdef SPECULAR_TEXTURE_AVAILABLE
242
+ let specularWeightWitness = textureSample(specularTexture, specularTextureSampler, vec2<f32>(0.0));
243
+ #endif
244
+ #ifdef SPECULAR_COLOR_TEXTURE_AVAILABLE
245
+ let specularColorWitness = textureSample(specularColorTexture, specularColorTextureSampler, vec2<f32>(0.0));
246
+ #endif
178
247
  #ifdef TRANSMISSION_AVAILABLE
179
248
  let transmission = transmissionTexture;
180
249
  let thickness = thicknessTexture;
@@ -216,8 +285,10 @@ struct SkylightUniforms {
216
285
  @group(1) @binding(23) var<uniform> skylight : SkylightUniforms;
217
286
 
218
287
  #ifdef PROBE_BLEND_AVAILABLE
219
- // Optional consumer lane. The host must provide the retained 160B record
220
- // buffer and matching BGL before enabling this capability.
288
+ // Optional consumer lane. The host provides the retained ProbeBlendRecord
289
+ // storage for the object selected by this draw. Keeping it in group(3)
290
+ // preserves the mesh/instance ownership of groups (2)/(3) without inventing a
291
+ // second probe bind group.
221
292
  @group(3) @binding(1) var<storage, read> probeBlendRecords : array<vec4<f32>>;
222
293
  #endif
223
294
 
@@ -264,6 +335,10 @@ struct VsOut {
264
335
  // Keep mesh and instance storage vertex-only; transmission receives the
265
336
  // transform basis from the vertex stage below.
266
337
  @location(4) @interpolate(flat) transmissionBasis0 : vec4<f32>,
338
+ #else
339
+ // WebGL2 exposes only inter-stage locations 0..14. Pack the object-space
340
+ // Surface position with its cluster/CSM depth in one varying.
341
+ @location(7) positionOSAndViewZ : vec4<f32>,
267
342
  #endif
268
343
  // feat-city-glb multi-UV tiling: second UV set inter-stage varying. Uses the
269
344
  // previously-vacant @location(5) so @location(6)/(7) stay byte-stable with
@@ -273,8 +348,9 @@ struct VsOut {
273
348
  @location(9) uv3 : vec2<f32>,
274
349
  @location(10) uv4 : vec2<f32>,
275
350
  @location(11) uv5 : vec2<f32>,
276
- @location(12) uv6 : vec2<f32>,
277
- @location(13) uv7 : vec2<f32>,
351
+ // UV6/UV7 share one vec4 slot so the transmission basis tail can stay
352
+ // inside WebGL2's 14 user varying locations.
353
+ @location(12) uv6And7 : vec4<f32>,
278
354
  #ifdef VERTEX_COLOR_AVAILABLE
279
355
  @location(14) color : vec4<f32>,
280
356
  #endif
@@ -286,27 +362,23 @@ struct VsOut {
286
362
  // surfaces -- whose cluster cells were unrelated to the floor's slice 0
287
363
  // hot zone -- received zero light. Forward view_z explicitly. M5 / w19:
288
364
  // also feeds CSM cascade selection in evalDirectional.
289
- @location(7) viewZ : f32,
365
+ // Surface position and clustered/CSM view depth share one varying so the
366
+ // composed Standard shader stays within WebGL2's 14 inter-stage locations.
290
367
  #ifdef TRANSMISSION_AVAILABLE
291
- @location(15) @interpolate(flat) transmissionBasis1 : vec4<f32>,
368
+ @location(7) viewZ : f32,
369
+ @location(13) @interpolate(flat) transmissionBasis1 : vec4<f32>,
292
370
  #endif
293
371
  };
294
372
 
295
- fn pick_channel(rgba : vec4<f32>, channelIndex : u32) -> f32 {
296
- // Branch-free channel pick. WGSL has no array<f32, 4>(rgba) addressable
297
- // helper; manual switch keeps the path uniform.
298
- switch (channelIndex) {
299
- case 0u: { return rgba.r; }
300
- case 1u: { return rgba.g; }
301
- case 2u: { return rgba.b; }
302
- default: { return rgba.a; }
303
- }
304
- }
305
-
306
- // Directional lighting lives in forgeax_pbr::lighting_directional. Point,
307
- // Spot, and Rect local lights are evaluated by the Standard Cluster module
308
- // below from one indexed light_data corpus; no per-kind fixed-slot loop is
309
- // present in this material shader.
373
+ // Light evaluators live in forgeax_pbr::lighting_directional +
374
+ // forgeax_pbr::lighting_punctual (M5 / T02). evalDirectional consumes the
375
+ // host's view UBO + shadowMap (LO 3.1.3 slope-scaled-bias 3x3 PCF;
376
+ // feat-20260520-directional-light-shadow-mapping byte-equivalent). evalPoint
377
+ // / evalSpot share evalPunctualBody (GGX specular + Lambertian diffuse +
378
+ // KHR_lights_punctual quartic range attenuation;
379
+ // feat-20260519-light-casters-point-spot-pbr M4 / w22 byte-equivalent).
380
+ // Charter P4: spot light is a thin cone-multiplier on top of the punctual
381
+ // body, point light is the body unchanged -- no magic-value collapse.
310
382
 
311
383
  fn vs_main_impl(in : VsIn, meshIndex : u32, instanceIndex : u32) -> VsOut {
312
384
  // feat-20260604-instances-per-instance-transform-shader-group3-bin M1 / w4:
@@ -322,6 +394,9 @@ fn vs_main_impl(in : VsIn, meshIndex : u32, instanceIndex : u32) -> VsOut {
322
394
  let world = localToWorld * vec4<f32>(in.pos, 1.0);
323
395
  var out : VsOut;
324
396
  out.clip = view.worldViewProj * world;
397
+ #ifndef TRANSMISSION_AVAILABLE
398
+ out.positionOSAndViewZ = vec4<f32>(in.pos, sceneViewZ(out.clip, view.temporalProjection));
399
+ #endif
325
400
  out.worldPos = world.xyz;
326
401
  // Normal: derive the inverse-transpose from the world matrix columns. This
327
402
  // keeps non-uniform entity scale correct while avoiding the Mesh.normalMatrix
@@ -345,6 +420,16 @@ fn vs_main_impl(in : VsIn, meshIndex : u32, instanceIndex : u32) -> VsOut {
345
420
  // (w=0); .w handedness preserved for bitangent reconstruction in fragment.
346
421
  let worldTangentXyz = normalize((entityWorld * instanceLocal * vec4<f32>(in.tangent.xyz, 0.0)).xyz);
347
422
  out.worldTangent = vec4<f32>(worldTangentXyz, in.tangent.w);
423
+ out.uv = in.uv;
424
+ out.uv1 = in.uv1;
425
+ out.uv2 = in.uv2;
426
+ out.uv3 = in.uv3;
427
+ out.uv4 = in.uv4;
428
+ out.uv5 = in.uv5;
429
+ out.uv6And7 = vec4<f32>(in.uv6, in.uv7);
430
+ #ifdef VERTEX_COLOR_AVAILABLE
431
+ out.color = in.color;
432
+ #endif
348
433
  #ifdef TRANSMISSION_AVAILABLE
349
434
  out.transmissionBasis0 = vec4<f32>(
350
435
  localToWorld[0].x,
@@ -358,17 +443,6 @@ fn vs_main_impl(in : VsIn, meshIndex : u32, instanceIndex : u32) -> VsOut {
358
443
  localToWorld[2].x,
359
444
  localToWorld[2].y,
360
445
  );
361
- #endif
362
- out.uv = in.uv;
363
- out.uv1 = in.uv1;
364
- out.uv2 = in.uv2;
365
- out.uv3 = in.uv3;
366
- out.uv4 = in.uv4;
367
- out.uv5 = in.uv5;
368
- out.uv6 = in.uv6;
369
- out.uv7 = in.uv7;
370
- #ifdef VERTEX_COLOR_AVAILABLE
371
- out.color = in.color;
372
446
  #endif
373
447
  // feat-20260613-csm-cascaded-shadow-maps M5 / w19: the per-fragment
374
448
  // light-space position varying is gone; evalDirectional computes
@@ -378,16 +452,22 @@ fn vs_main_impl(in : VsIn, meshIndex : u32, instanceIndex : u32) -> VsOut {
378
452
  // Perspective divide on clip-space position; ndc.z retains depth-buffer value.
379
453
  let clipPos = out.clip;
380
454
  out.ndc = vec4(clipPos.xy / clipPos.w, clipPos.z / clipPos.w, localToWorld[2].z);
381
- // Standard WebGPU/GL perspective projection has clip.w = -view.z, so
382
- // view_z = -clip.w (negative in front of the camera). M4.5-followup:
383
- // ndc_position_to_cluster needs this for log-z slice mapping; passing
384
- // ndc.z (in [0,1]) instead made every fragment land in slice 0.
385
455
  // Keep the cluster depth exactly aligned with the CPU binner for both
386
456
  // perspective and off-axis orthographic projections.
457
+ #ifdef TRANSMISSION_AVAILABLE
387
458
  out.viewZ = sceneViewZ(clipPos, view.temporalProjection);
459
+ #endif
388
460
  return out;
389
461
  }
390
462
 
463
+ fn standardViewZ(in : VsOut) -> f32 {
464
+ #ifdef TRANSMISSION_AVAILABLE
465
+ return in.viewZ;
466
+ #else
467
+ return in.positionOSAndViewZ.w;
468
+ #endif
469
+ }
470
+
391
471
  @vertex
392
472
  fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
393
473
  return vs_main_impl(in, 0u, idx);
@@ -408,8 +488,8 @@ fn transformedMaterialUv(transform : vec4<f32>, metadata : vec4<f32>, in : VsOut
408
488
  if (metadata.x >= 3.0) { source = in.uv3; }
409
489
  if (metadata.x >= 4.0) { source = in.uv4; }
410
490
  if (metadata.x >= 5.0) { source = in.uv5; }
411
- if (metadata.x >= 6.0) { source = in.uv6; }
412
- if (metadata.x >= 7.0) { source = in.uv7; }
491
+ if (metadata.x >= 6.0) { source = in.uv6And7.xy; }
492
+ if (metadata.x >= 7.0) { source = in.uv6And7.zw; }
413
493
  let scaled = source * transform.zw;
414
494
  let angle = metadata.y;
415
495
  let c = cos(angle);
@@ -417,10 +497,6 @@ fn transformedMaterialUv(transform : vec4<f32>, metadata : vec4<f32>, in : VsOut
417
497
  return vec2<f32>(scaled.x * c - scaled.y * s, scaled.x * s + scaled.y * c) + transform.xy;
418
498
  }
419
499
 
420
- fn materialAlpha(baseSample : vec4<f32>) -> f32 {
421
- return material.baseColor.a * baseSample.a;
422
- }
423
-
424
500
  fn materialVertexColor(in : VsOut) -> vec4<f32> {
425
501
  #ifdef VERTEX_COLOR_AVAILABLE
426
502
  return in.color;
@@ -429,6 +505,79 @@ fn materialVertexColor(in : VsOut) -> vec4<f32> {
429
505
  #endif
430
506
  }
431
507
 
508
+ // Keep the cooked artifact content-addressable per declared capability set.
509
+ // Some capability axes only change an imported helper's resource surface; the
510
+ // witness makes that distinction explicit in the composed source as well, so
511
+ // two valid manifest keys can never alias one artifact hash.
512
+ fn standardVariantIdentity() -> f32 {
513
+ var identity = 0.0;
514
+ #ifdef STORAGE_BUFFER_AVAILABLE
515
+ identity = identity + 1.0;
516
+ #endif
517
+ #ifdef CLUSTER_FORWARD_AVAILABLE
518
+ identity = identity + 2.0;
519
+ #endif
520
+ #ifdef VERTEX_COLOR_AVAILABLE
521
+ identity = identity + 4.0;
522
+ #endif
523
+ #ifdef PROBE_BLEND_AVAILABLE
524
+ identity = identity + 8.0;
525
+ #endif
526
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
527
+ identity = identity + 16.0;
528
+ #endif
529
+ #ifdef TRANSMISSION_AVAILABLE
530
+ identity = identity + 32.0;
531
+ #endif
532
+ #ifdef DIRECTIONAL_PCSS_AVAILABLE
533
+ identity = identity + 64.0;
534
+ #endif
535
+ #ifdef PROJECTOR_AVAILABLE
536
+ identity = identity + 128.0;
537
+ #endif
538
+ #ifdef REFLECTION_FALLBACK_AVAILABLE
539
+ identity = identity + 256.0;
540
+ #endif
541
+ return identity;
542
+ }
543
+
544
+ // Standard owns the lighting and pass policy; the selected Surface owns only
545
+ // the base facts exchanged through surface_v1.
546
+ fn evaluateStandardSurface(in : VsOut, frontFacing : bool) -> SurfaceData {
547
+ let viewDirectionWS = normalize(view.cameraPos - in.worldPos);
548
+ #ifdef TRANSMISSION_AVAILABLE
549
+ let positionOS = in.worldPos;
550
+ #else
551
+ let positionOS = in.positionOSAndViewZ.xyz;
552
+ #endif
553
+ return evaluate_surface(SurfaceInput(
554
+ positionOS,
555
+ in.worldPos,
556
+ in.worldNormal,
557
+ in.worldTangent,
558
+ viewDirectionWS,
559
+ in.uv,
560
+ in.uv1,
561
+ materialVertexColor(in),
562
+ frontFacing,
563
+ ));
564
+ }
565
+
566
+ // The renderer supplies the retained ProbeBlendRecord at the per-object
567
+ // consumer boundary. This helper deliberately accepts only diffuse inputs;
568
+ // Skylight/ReflectionProbe continue to own specular.
569
+ fn composeProbeDiffuse(
570
+ shPreblend : array<vec4<f32>, 9>,
571
+ localBlendFraction : f32,
572
+ normal : vec3<f32>,
573
+ skyIrradiance : vec3<f32>,
574
+ kD : vec3<f32>,
575
+ albedo : vec3<f32>,
576
+ metallic : f32,
577
+ ) -> vec3<f32> {
578
+ return evaluateProbeDiffuse(shPreblend, localBlendFraction, normal, skyIrradiance, kD, albedo, metallic);
579
+ }
580
+
432
581
  fn finiteScalar(value : f32, fallback : f32) -> f32 {
433
582
  let bounded = clamp(value, -65504.0, 65504.0);
434
583
  return select(fallback, bounded, value == value);
@@ -456,8 +605,8 @@ fn blendLinearTransparent(
456
605
  // linearHdrColorDomain: fs_main writes linear HDR when its target is HDR.
457
606
  // toneStageInput: the value remains linear until the fullscreen tone stage.
458
607
 
459
- fn alphaTest(alpha : f32) {
460
- if (material.alphaCutoff > 0.0 && alpha <= material.alphaCutoff) {
608
+ fn alphaTestSurface(surface : SurfaceData) {
609
+ if (surface.alphaClipThreshold > 0.0 && surface.opacity <= surface.alphaClipThreshold) {
461
610
  discard;
462
611
  }
463
612
  }
@@ -472,62 +621,123 @@ struct StandardPbrOutput {
472
621
  };
473
622
 
474
623
  @fragment
475
- fn fs_main(in : VsOut) -> StandardPbrOutput {
476
- let baseUv = transformedMaterialUv(material.baseColorTextureCoordinatesTransform, material.baseColorTextureCoordinatesMetadata, in);
477
- let baseSample = sampleMaterialTexture(baseColorTexture, baseColorSampler, baseUv, material.baseColorTextureCoordinatesMetadata.zw);
478
- let vertexColor = materialVertexColor(in);
479
- alphaTest(material.baseColor.a * baseSample.a * vertexColor.a);
480
- let alpha = materialAlpha(baseSample) * vertexColor.a;
481
- let albedo = material.baseColor.rgb * baseSample.rgb * vertexColor.rgb;
482
-
483
- // Metallic-roughness texture sampling with per-field channel selectors
484
- // (D-8): glTF 2.0 default layout B=metallic, G=roughness, R=occlusion is
485
- // encoded by the host as 4 independent f32 selectors in the merged UBO
486
- // (metallicChannel/roughnessChannel/aoChannel/extraChannel). Cast to u32
487
- // at the pick_channel call site; values stay in {0,1,2,3}.
488
- let mrUv = transformedMaterialUv(material.metallicRoughnessTextureCoordinatesTransform, material.metallicRoughnessTextureCoordinatesMetadata, in);
489
- let mrSample = sampleMaterialTexture(metallicRoughnessTexture, metallicRoughnessSampler, mrUv, material.metallicRoughnessTextureCoordinatesMetadata.zw);
490
- let metallic = clamp(
491
- finiteScalar(material.metallic, 0.0) *
492
- finiteScalar(pick_channel(mrSample, u32(material.metallicChannel)), 1.0),
493
- 0.0,
494
- 1.0,
495
- );
496
- let roughnessTex = clamp(
497
- finiteScalar(pick_channel(mrSample, u32(material.roughnessChannel)), 1.0),
498
- 0.0,
499
- 1.0,
500
- );
501
-
502
- // Layer-2 fail-fast: shader internal clamp keeps D_GGX finite for
503
- // roughness=0 even if the asset somehow bypasses layer-1 register
504
- // fail-fast (plan-strategy section 5.3 + AC-02 (b)+(c)). Apply to the
505
- // material scalar prior to the channelMap-extracted texture multiplier
506
- // so the final roughness >= 0.04 floor is preserved (texture sample is
507
- // a 1.0-default white when metallicRoughnessTexture is absent, so
508
- // multiplying after the clamp keeps the floor intact).
509
- var a = max(finiteScalar(material.roughness, 0.5), 0.04);
510
- a = a * roughnessTex;
511
- a = a * a;
512
-
513
- // TBN basis composed via forgeax_pbr::tbn helpers; default fallback
514
- // (defaultNormalTextureView) RG=(128,128) -> tangent (0,0,1) -> world n
515
- // unchanged.
516
- let normalUv = transformedMaterialUv(material.normalTextureCoordinatesTransform, material.normalTextureCoordinatesMetadata, in);
517
- let normSampleRg = sampleMaterialTexture(normalTexture, normalSampler, normalUv, material.normalTextureCoordinatesMetadata.zw).rg;
518
- let normTangent = scaleTangentSpaceNormal(
519
- decodeTangentSpaceNormalRg(normSampleRg), material.normalScale,
520
- );
521
- let n = applyTBN(in.worldNormal, in.worldTangent, normTangent);
624
+ fn fs_main(in : VsOut, @builtin(front_facing) frontFacing : bool) -> StandardPbrOutput {
625
+ let _variantIdentity = standardVariantIdentity();
626
+ let surface = evaluateStandardSurface(in, frontFacing);
627
+ alphaTestSurface(surface);
628
+ let alpha = surface.opacity;
629
+ let albedo = surface.baseColor;
630
+ let metallic = clamp(finiteScalar(surface.metallic, 0.0), 0.0, 1.0);
631
+ let iblRoughness = clamp(finiteScalar(surface.roughness, 0.5), 0.04, 1.0);
632
+ let a = iblRoughness * iblRoughness;
633
+ let n = normalize(surface.normalWS);
522
634
 
523
635
  let v = normalize(view.cameraPos - in.worldPos);
524
- let specularUv = transformedMaterialUv(material.specularTintTextureCoordinatesTransform, material.specularTintTextureCoordinatesMetadata, in);
525
- let specularTint = material.specularTint * sampleMaterialTexture(
526
- specularTintTexture, specularTintSampler, specularUv, material.specularTintTextureCoordinatesMetadata.zw,
636
+ var specularColor = material.specularColor;
637
+ #ifdef SPECULAR_COLOR_TEXTURE_AVAILABLE
638
+ let specularUv = transformedMaterialUv(
639
+ material.specularColorTextureCoordinatesTransform,
640
+ material.specularColorTextureCoordinatesMetadata,
641
+ in,
642
+ );
643
+ specularColor = specularColor * sampleMaterialTexture(
644
+ specularColorTexture,
645
+ specularColorTextureSampler,
646
+ specularUv,
647
+ material.specularColorTextureCoordinatesMetadata.zw,
527
648
  ).rgb;
649
+ #endif
650
+ var specularWeight = clamp(finiteScalar(material.specular, 1.0), 0.0, 1.0);
651
+ #ifdef SPECULAR_TEXTURE_AVAILABLE
652
+ let specularWeightUv = transformedMaterialUv(
653
+ material.specularTextureCoordinatesTransform,
654
+ material.specularTextureCoordinatesMetadata,
655
+ in,
656
+ );
657
+ specularWeight = specularWeight * sampleMaterialTexture(
658
+ specularTexture,
659
+ specularTextureSampler,
660
+ specularWeightUv,
661
+ material.specularTextureCoordinatesMetadata.zw,
662
+ ).a;
663
+ #endif
528
664
  let safeIor = max(finiteScalar(material.ior, 1.5), 1.0);
529
665
  let dielectricF0 = pow((safeIor - 1.0) / (safeIor + 1.0), 2.0);
530
- let f0 = mix(vec3<f32>(dielectricF0) * specularTint, albedo, metallic);
666
+ var f0 = mix(vec3<f32>(dielectricF0) * specularColor * specularWeight, albedo, metallic);
667
+ var physicalNormal = n;
668
+ #ifdef ANISOTROPY_AVAILABLE
669
+ var anisotropyStrength = finiteScalar(material.anisotropyStrength, 0.0);
670
+ var anisotropyRotation = finiteScalar(material.anisotropyRotation, 0.0);
671
+ var anisotropyDirection = vec2<f32>(1.0, 0.0);
672
+ #ifdef ANISOTROPY_TEXTURE_AVAILABLE
673
+ let anisotropyUv = transformedMaterialUv(
674
+ material.anisotropyTextureCoordinatesTransform,
675
+ material.anisotropyTextureCoordinatesMetadata,
676
+ in,
677
+ );
678
+ let anisotropySample = sampleMaterialTexture(
679
+ anisotropyTexture,
680
+ anisotropySampler,
681
+ anisotropyUv,
682
+ material.anisotropyTextureCoordinatesMetadata.zw,
683
+ );
684
+ let encodedAnisotropyDirection = 2.0 * anisotropySample.rg - vec2<f32>(1.0, 1.0);
685
+ if (dot(encodedAnisotropyDirection, encodedAnisotropyDirection) >= 1e-6) {
686
+ anisotropyDirection = normalize(encodedAnisotropyDirection);
687
+ }
688
+ anisotropyStrength = anisotropyStrength * anisotropySample.b;
689
+ #endif
690
+ physicalNormal = evaluateAnisotropicNormal(
691
+ n,
692
+ in.worldTangent,
693
+ anisotropyStrength,
694
+ anisotropyRotation,
695
+ anisotropyDirection,
696
+ );
697
+ #endif
698
+ #ifdef IRIDESCENCE_AVAILABLE
699
+ var iridescenceStrength = finiteScalar(material.iridescence, 0.0);
700
+ var iridescenceThicknessFactor = 1.0;
701
+ #ifdef IRIDESCENCE_TEXTURE_AVAILABLE
702
+ let iridescenceUv = transformedMaterialUv(
703
+ material.iridescenceTextureCoordinatesTransform,
704
+ material.iridescenceTextureCoordinatesMetadata,
705
+ in,
706
+ );
707
+ let iridescenceSample = sampleMaterialTexture(
708
+ iridescenceTexture,
709
+ iridescenceSampler,
710
+ iridescenceUv,
711
+ material.iridescenceTextureCoordinatesMetadata.zw,
712
+ );
713
+ iridescenceStrength = iridescenceStrength * iridescenceSample.r;
714
+ #endif
715
+ #ifdef IRIDESCENCE_THICKNESS_TEXTURE_AVAILABLE
716
+ let iridescenceThicknessUv = transformedMaterialUv(
717
+ material.iridescenceThicknessTextureCoordinatesTransform,
718
+ material.iridescenceThicknessTextureCoordinatesMetadata,
719
+ in,
720
+ );
721
+ let iridescenceThicknessSample = sampleMaterialTexture(
722
+ iridescenceThicknessTexture,
723
+ iridescenceThicknessSampler,
724
+ iridescenceThicknessUv,
725
+ material.iridescenceThicknessTextureCoordinatesMetadata.zw,
726
+ );
727
+ iridescenceThicknessFactor = clamp(iridescenceThicknessSample.g, 0.0, 1.0);
728
+ #endif
729
+ let filmThickness = mix(
730
+ finiteScalar(material.iridescenceThicknessMinimum, 100.0),
731
+ finiteScalar(material.iridescenceThicknessMaximum, 400.0),
732
+ iridescenceThicknessFactor,
733
+ );
734
+ f0 = evaluateIridescenceFresnel(
735
+ f0,
736
+ iridescenceStrength,
737
+ finiteScalar(material.iridescenceIor, 1.3),
738
+ filmThickness,
739
+ );
740
+ #endif
531
741
  var diffuseAlbedo = albedo;
532
742
  #ifdef TRANSMISSION_AVAILABLE
533
743
  let transmissionUv = transformedMaterialUv(
@@ -576,16 +786,12 @@ fn fs_main(in : VsOut) -> StandardPbrOutput {
576
786
  );
577
787
  diffuseAlbedo = albedo * (1.0 - transmittedEnergy);
578
788
  #endif
579
- let coatRoughness = max(material.clearcoatRoughness, 0.04);
580
- let coatAlpha = coatRoughness * coatRoughness;
581
- let coatF = f_schlick(max(dot(n, v), 0.0), vec3<f32>(0.04)) * material.clearcoat;
582
-
583
789
  // Ambient (IBL) + 1 + N + N accumulation
584
790
  // (feat-20260520-skylight-ibl-cubemap M3 / t48 +
585
791
  // feat-20260520-directional-light-shadow-mapping +
586
792
  // feat-20260518-pbr-direct-lighting-mvp Finding 4):
587
793
  //
588
- // color = ambient(IBL) + directional(shadowed) + clustered local lights
794
+ // color = ambient(IBL) + directional(shadowed) + sum(point) + sum(spot)
589
795
  //
590
796
  // When the host Skylight bind group provides default-zero resources the IBL
591
797
  // helpers sample to vec3(0) and ambient = 0, so the shader falls through to
@@ -601,43 +807,53 @@ fn fs_main(in : VsOut) -> StandardPbrOutput {
601
807
  // split-sum mip lookup, so we re-derive the post-shader-clamp roughness
602
808
  // here (matches sampleIblSpecular's `mip = roughness * 4.0` expectation).
603
809
  //
604
- // Directional lighting is evaluated here. All local lights (Point, Spot,
605
- // Rect) are evaluated by the Standard Cluster module below, which reads
606
- // one indexed light_data corpus and never performs a fixed-slot loop.
607
- let kD = (vec3<f32>(1.0) - f_schlick(max(dot(n, v), 0.0), f0)) * (1.0 - metallic);
608
- let iblRoughness = clamp(max(finiteScalar(material.roughness, 0.5), 0.04) * roughnessTex, 0.04, 1.0);
810
+ // Direct lights (1 + N + N): one directional + N point + N spot, summed
811
+ // sequentially. LIGHT_ARRAY_MAX_SLOTS = 4 host-side bounds the loop trip
812
+ // counts. GGX BRDF helpers (d_ggx / v_smith / f_schlick from
813
+ // forgeax_pbr::brdf) run inside each evalDirectional / evalPoint / evalSpot
814
+ // so all three light types share the same microfacet specular + Lambertian
815
+ // diffuse form. The directional path additionally projects worldPos
816
+ // through the per-cascade light-space matrix in the fragment stage for
817
+ // shadow-map PCF lookup (CSM, feat-20260613).
818
+ let kD = (vec3<f32>(1.0) - f_schlick(max(dot(physicalNormal, v), 0.0), f0)) * (1.0 - metallic);
819
+ var coatRoughness = 0.04;
820
+ var coatAlpha = coatRoughness * coatRoughness;
821
+ var coatF = vec3<f32>(0.0);
822
+ #ifdef CLEARCOAT_AVAILABLE
823
+ coatRoughness = max(finiteScalar(material.clearcoatRoughness, 0.04), 0.04);
824
+ coatAlpha = coatRoughness * coatRoughness;
825
+ coatF = f_schlick(max(dot(physicalNormal, v), 0.0), vec3<f32>(0.04)) *
826
+ clamp(finiteScalar(material.clearcoat, 0.0), 0.0, 1.0);
827
+ #endif
609
828
  var irradiance = vec3<f32>(0.0);
610
829
  var specularIbl = vec3<f32>(0.0);
611
830
  if (skylight.intensity < 0.0) {
612
- // A negative intensity is the renderer-owned probe sentinel. The producer
613
- // stores -(probeIntensity + 1), so intensity=0 remains distinguishable
614
- // from the ordinary Skylight zero payload. The four color/rotation lanes
615
- // carry probe center and half extents in this mode; Skylight resources
616
- // remain unchanged and therefore preserve the ABI.
831
+ // A negative intensity is the renderer-owned probe sentinel. The four
832
+ // color/rotation lanes carry the selected probe center and half extents;
833
+ // ordinary Skylight resources remain unchanged, preserving the ABI.
617
834
  specularIbl = sampleReflectionProbeSpecular(
618
- n, v, iblRoughness, f0, in.worldPos,
835
+ physicalNormal, v, iblRoughness, f0, in.worldPos,
619
836
  vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB),
620
837
  skylight.rotation.xyz, vec4<f32>(0.0, 0.0, 0.0, 1.0),
621
838
  prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
622
839
  );
623
840
  } else {
624
- irradiance = sampleIblDiffuse(n, skylight.rotation, irradianceMap, irradianceSampler);
841
+ irradiance = sampleIblDiffuse(physicalNormal, skylight.rotation, irradianceMap, irradianceSampler);
625
842
  specularIbl = sampleIblSpecular(
626
- n, v, iblRoughness, f0,
843
+ physicalNormal, v, iblRoughness, f0,
627
844
  skylight.rotation,
628
845
  prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
629
846
  );
630
847
  }
631
- let occlusionUv = transformedMaterialUv(material.occlusionTextureCoordinatesTransform, material.occlusionTextureCoordinatesMetadata, in);
632
- let aoSample = sampleMaterialTexture(occlusionTexture, occlusionSampler, occlusionUv, material.occlusionTextureCoordinatesMetadata.zw);
633
- let ao = mix(1.0, aoSample.r, material.occlusionStrength);
848
+ let ao = surface.occlusion;
634
849
  // feat-20260612-hdrp-ssao M7 round-2: `var` (mutable) so the
635
850
  // CLUSTER_FORWARD_AVAILABLE branch below can `ambient *=` the SSAO
636
851
  // factor. The non-HDRP path leaves ambient untouched.
637
852
  let skyColor = vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB);
638
853
  let skyFactor = skyColor * skylight.intensity;
639
854
  var reflectionFallback = specularIbl * (vec3<f32>(1.0) - coatF);
640
- var ambient = (kD * irradiance * diffuseAlbedo + specularIbl) * (vec3<f32>(1.0) - coatF);
855
+ var ambient = (kD * irradiance * diffuseAlbedo + specularIbl) *
856
+ (vec3<f32>(1.0) - coatF);
641
857
  #ifdef PROBE_BLEND_AVAILABLE
642
858
  // group(3) is the per-object bind group; record zero is the object selected
643
859
  // by this draw, independent of instance count.
@@ -648,44 +864,24 @@ fn fs_main(in : VsOut) -> StandardPbrOutput {
648
864
  );
649
865
  let probeLocalBlendFraction = probeBlendRecords[0].z;
650
866
  let probeDiffuseK = kD / max(vec3<f32>(1.0 - metallic), vec3<f32>(0.0001));
651
- let probeDiffuse = evaluateProbeDiffuse(probeShPreblend, probeLocalBlendFraction, n, irradiance * skyFactor, probeDiffuseK, albedo, metallic);
652
- ambient = (probeDiffuse + specularIbl * skyFactor) * (vec3<f32>(1.0) - coatF);
653
- #endif
654
- if (material.clearcoat != 0.0) {
655
- // Keep the clearcoat lobe on the same selected environment as the base
656
- // specular lobe. Probe mode reuses the packed center/extents lanes in the
657
- // per-probe uniform; routing it through global Skylight IBL would make the
658
- // fallback a mixed-source projection and break c=1 source equivalence.
659
- var clearcoatIbl = vec3<f32>(0.0);
660
- if (skylight.intensity < 0.0) {
661
- clearcoatIbl = sampleReflectionProbeSpecular(
662
- n, v, coatRoughness, vec3<f32>(0.04), in.worldPos,
663
- vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB),
664
- skylight.rotation.xyz, vec4<f32>(0.0, 0.0, 0.0, 1.0),
665
- prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
666
- );
667
- } else {
668
- clearcoatIbl = sampleIblSpecular(
669
- n, v, coatRoughness, vec3<f32>(0.04),
670
- skylight.rotation,
671
- prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
672
- );
673
- }
674
- let clearcoatContribution = clearcoatIbl * material.clearcoat;
675
- #ifdef PROBE_BLEND_AVAILABLE
676
- if (skylight.intensity < 0.0) {
677
- ambient = ambient + clearcoatContribution;
678
- } else {
679
- ambient = ambient + clearcoatIbl * material.clearcoat * skyFactor;
680
- }
681
- #else
682
- ambient = ambient + clearcoatContribution;
867
+ let probeDiffuse = composeProbeDiffuse(probeShPreblend,
868
+ probeLocalBlendFraction,
869
+ physicalNormal,
870
+ irradiance * skyFactor,
871
+ probeDiffuseK,
872
+ diffuseAlbedo,
873
+ metallic,
874
+ );
875
+ ambient = (probeDiffuse + specularIbl * skyFactor) *
876
+ (vec3<f32>(1.0) - coatF);
683
877
  #endif
684
- reflectionFallback = reflectionFallback + clearcoatContribution;
685
- }
686
878
  #ifdef PROBE_BLEND_AVAILABLE
687
879
  ambient = ambient * ao;
688
880
  #else
881
+ // In the regular Skylight path tint/intensity are applied after sampling.
882
+ // In the probe-sentinel path intensity is encoded as -(probeIntensity + 1),
883
+ // so decode only the non-negative payload and keep zero-intensity probes
884
+ // distinct from an absent Skylight.
689
885
  if (skylight.intensity < 0.0) {
690
886
  let environmentScale = max(-skylight.intensity - 1.0, 0.0) * ao;
691
887
  ambient = ambient * environmentScale;
@@ -711,19 +907,16 @@ fn fs_main(in : VsOut) -> StandardPbrOutput {
711
907
  let ssaoFactor = textureSample(ssaoBlurredTexture, ssaoBlurredSampler, ssaoUv).r;
712
908
  // scope-amend-webgl2-ubo: intensity packed into cluster_uniform.near_far_log.w.
713
909
  let ssaoIntensity = get_ssao_intensity();
714
- let ssaoScale = mix(1.0, ssaoFactor * ao, ssaoIntensity);
715
910
  ambient *= mix(1.0, ssaoFactor * ao, ssaoIntensity);
911
+ let ssaoScale = mix(1.0, ssaoFactor * ao, ssaoIntensity);
716
912
  reflectionFallback *= ssaoScale;
717
913
  #endif
718
914
  var color = ambient;
719
915
  #ifdef TRANSMISSION_AVAILABLE
720
916
  let screenUv = in.ndc.xy * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);
721
- // Read the same renderer-owned transform tables that the vertex stage uses
722
- // instead of adding six more interstage varyings. WebGPU guarantees only 16
723
- // user-defined vertex output locations; keeping the affine conversion in
724
- // the fragment stage preserves that contract and keeps thickness in the
725
- // glTF unit-scale mesh space before measuring the refracted segment in world
726
- // metres.
917
+ // Read the affine basis forwarded by the vertex stage. This keeps the
918
+ // fragment path within the inter-stage location budget and preserves the
919
+ // scene-index vertex entry used by the GPU-driven lane.
727
920
  let localToWorld0 = in.transmissionBasis0.xyz;
728
921
  let localToWorld1 = vec3<f32>(
729
922
  in.transmissionBasis0.w,
@@ -806,30 +999,158 @@ fn fs_main(in : VsOut) -> StandardPbrOutput {
806
999
  color = color + finiteColor(transmittedBackdrop, vec3<f32>(0.0)) *
807
1000
  transmittedEnergy * beerAttenuation;
808
1001
  #endif
809
- let directionalShadow = evalDirectionalShadowFactor(n, in.worldPos, in.viewZ);
810
- let directionalBase = evalDirectionalNoShadow(n, v, diffuseAlbedo, metallic, a, f0);
1002
+ let directionalShadow = evalDirectionalShadowFactor(
1003
+ physicalNormal,
1004
+ in.worldPos,
1005
+ standardViewZ(in),
1006
+ );
1007
+ let directionalBase = evalDirectionalNoShadow(physicalNormal, v, diffuseAlbedo, metallic, a, f0);
811
1008
  color = color + directionalShadow * directionalBase;
812
- if (material.clearcoat != 0.0) {
813
- let directionalClearcoat = evalDirectionalNoShadow(
814
- n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
815
- );
816
- color = color + directionalShadow * material.clearcoat * directionalClearcoat;
817
- }
818
1009
  #ifdef CLUSTER_FORWARD_AVAILABLE
819
1010
  // NDC from vertex shader (perspective-divided clip-space, interpolated).
820
1011
  // view_z: NDC depth for cluster Z-slice lookup.
821
1012
  color = color + evaluateStandardClusterLights(
822
- in.ndc.xyz, in.viewZ, in.worldPos, n, v, diffuseAlbedo, metallic, a, f0, false,
1013
+ in.ndc.xyz, standardViewZ(in), in.worldPos, physicalNormal, v,
1014
+ diffuseAlbedo, metallic, a, f0, false,
1015
+ );
1016
+ #endif // CLUSTER_FORWARD_AVAILABLE
1017
+ // Emissive is part of the lower-energy stack and is attenuated by the
1018
+ // topcoat just like diffuse/specular radiance.
1019
+ color = color + surface.emissive;
1020
+ #ifdef SHEEN_AVAILABLE
1021
+ var sheenColor = material.sheenColor;
1022
+ var sheenRoughnessFactor = 1.0;
1023
+ #ifdef SHEEN_COLOR_TEXTURE_AVAILABLE
1024
+ let sheenColorUv = transformedMaterialUv(
1025
+ material.sheenColorTextureCoordinatesTransform,
1026
+ material.sheenColorTextureCoordinatesMetadata,
1027
+ in,
823
1028
  );
824
- if (material.clearcoat != 0.0) {
825
- color = color + material.clearcoat * evaluateStandardClusterLights(
826
- in.ndc.xyz, in.viewZ, in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04), false,
1029
+ sheenColor = sheenColor * sampleMaterialTexture(
1030
+ sheenColorTexture,
1031
+ sheenColorSampler,
1032
+ sheenColorUv,
1033
+ material.sheenColorTextureCoordinatesMetadata.zw,
1034
+ ).rgb;
1035
+ #endif
1036
+ #ifdef SHEEN_ROUGHNESS_TEXTURE_AVAILABLE
1037
+ let sheenRoughnessUv = transformedMaterialUv(
1038
+ material.sheenRoughnessTextureCoordinatesTransform,
1039
+ material.sheenRoughnessTextureCoordinatesMetadata,
1040
+ in,
1041
+ );
1042
+ sheenRoughnessFactor = sampleMaterialTexture(
1043
+ sheenRoughnessTexture,
1044
+ sheenRoughnessSampler,
1045
+ sheenRoughnessUv,
1046
+ material.sheenRoughnessTextureCoordinatesMetadata.zw,
1047
+ ).a;
1048
+ #endif
1049
+ let sheenRoughness = clamp(
1050
+ finiteScalar(material.sheenRoughness, 0.0) * sheenRoughnessFactor,
1051
+ 0.0,
1052
+ 1.0,
1053
+ );
1054
+ color = evaluateSheenLayer(color, sheenColor, sheenRoughness, dot(n, v));
1055
+ #endif
1056
+ #ifdef CLEARCOAT_AVAILABLE
1057
+ var clearcoatFactor = clamp(material.clearcoat, 0.0, 1.0);
1058
+ #ifdef CLEARCOAT_TEXTURE_AVAILABLE
1059
+ let clearcoatUv = transformedMaterialUv(material.clearcoatTextureCoordinatesTransform, material.clearcoatTextureCoordinatesMetadata, in);
1060
+ clearcoatFactor = clamp(
1061
+ material.clearcoat * sampleMaterialTexture(
1062
+ clearcoatTexture,
1063
+ clearcoatSampler,
1064
+ clearcoatUv,
1065
+ material.clearcoatTextureCoordinatesMetadata.zw,
1066
+ ).r,
1067
+ 0.0,
1068
+ 1.0,
1069
+ );
1070
+ #endif
1071
+ var clearcoatRoughnessValue = clamp(max(material.clearcoatRoughness, 0.04), 0.04, 1.0);
1072
+ #ifdef CLEARCOAT_ROUGHNESS_TEXTURE_AVAILABLE
1073
+ let clearcoatRoughnessUv = transformedMaterialUv(material.clearcoatRoughnessTextureCoordinatesTransform, material.clearcoatRoughnessTextureCoordinatesMetadata, in);
1074
+ clearcoatRoughnessValue = clamp(
1075
+ max(material.clearcoatRoughness, 0.04) * sampleMaterialTexture(
1076
+ clearcoatRoughnessTexture,
1077
+ clearcoatRoughnessSampler,
1078
+ clearcoatRoughnessUv,
1079
+ material.clearcoatRoughnessTextureCoordinatesMetadata.zw,
1080
+ ).g,
1081
+ 0.04,
1082
+ 1.0,
1083
+ );
1084
+ #endif
1085
+ var clearcoatNormalValue = in.worldNormal;
1086
+ #ifdef CLEARCOAT_NORMAL_TEXTURE_AVAILABLE
1087
+ let clearcoatNormalUv = transformedMaterialUv(material.clearcoatNormalTextureCoordinatesTransform, material.clearcoatNormalTextureCoordinatesMetadata, in);
1088
+ clearcoatNormalValue = applyTBN(
1089
+ in.worldNormal,
1090
+ in.worldTangent,
1091
+ scaleTangentSpaceNormal(
1092
+ decodeTangentSpaceNormalRg(sampleMaterialTexture(
1093
+ clearcoatNormalTexture,
1094
+ clearcoatNormalSampler,
1095
+ clearcoatNormalUv,
1096
+ material.clearcoatNormalTextureCoordinatesMetadata.zw,
1097
+ ).rg),
1098
+ material.clearcoatNormalScale,
1099
+ ),
1100
+ );
1101
+ #endif
1102
+ var clearcoatIbl = vec3<f32>(0.0);
1103
+ if (skylight.intensity < 0.0) {
1104
+ clearcoatIbl = sampleReflectionProbeSpecular(
1105
+ clearcoatNormalValue,
1106
+ v,
1107
+ clearcoatRoughnessValue,
1108
+ vec3<f32>(0.04),
1109
+ in.worldPos,
1110
+ vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB),
1111
+ skylight.rotation.xyz,
1112
+ vec4<f32>(0.0, 0.0, 0.0, 1.0),
1113
+ prefilterMap,
1114
+ prefilterSampler,
1115
+ brdfLut,
1116
+ brdfLutSampler,
1117
+ );
1118
+ } else {
1119
+ clearcoatIbl = sampleIblSpecular(
1120
+ clearcoatNormalValue,
1121
+ v,
1122
+ clearcoatRoughnessValue,
1123
+ vec3<f32>(0.04),
1124
+ skylight.rotation,
1125
+ prefilterMap,
1126
+ prefilterSampler,
1127
+ brdfLut,
1128
+ brdfLutSampler,
827
1129
  );
828
1130
  }
829
- #endif // CLUSTER_FORWARD_AVAILABLE
830
- let emissiveUv = transformedMaterialUv(material.emissiveTextureCoordinatesTransform, material.emissiveTextureCoordinatesMetadata, in);
831
- let emissiveSample = sampleMaterialTexture(emissiveTexture, emissiveSampler, emissiveUv, material.emissiveTextureCoordinatesMetadata.zw).rgb;
832
- color = color + material.emissive * material.emissiveIntensity * emissiveSample;
1131
+ let clearcoatAlpha = clearcoatRoughnessValue * clearcoatRoughnessValue;
1132
+ let clearcoatDirect = directionalShadow * evalDirectionalNoShadow(
1133
+ clearcoatNormalValue,
1134
+ v,
1135
+ vec3<f32>(0.0),
1136
+ 1.0,
1137
+ clearcoatAlpha,
1138
+ vec3<f32>(0.04),
1139
+ );
1140
+ let directionalClearcoat = clearcoatDirect;
1141
+ let clearcoatContribution = clearcoatIbl * clearcoatFactor;
1142
+ var clearcoatEnvironment = clearcoatIbl * skyColor * skylight.intensity;
1143
+ if (skylight.intensity < 0.0) {
1144
+ clearcoatEnvironment = clearcoatIbl;
1145
+ }
1146
+ color = evaluateClearcoatLayer(
1147
+ color,
1148
+ clearcoatEnvironment + directionalClearcoat,
1149
+ dot(clearcoatNormalValue, v),
1150
+ clearcoatFactor,
1151
+ );
1152
+ reflectionFallback = reflectionFallback + clearcoatContribution;
1153
+ #endif
833
1154
  var output : StandardPbrOutput;
834
1155
  output.color = vec4<f32>(color, alpha);
835
1156
  #ifdef REFLECTION_FALLBACK_AVAILABLE
@@ -865,49 +1186,18 @@ struct GBufferOutput {
865
1186
  /// pipeline's g-buffer render pass binds this entry point via the shader's
866
1187
  /// multi-entry support (passKind='deferred' selects `fs_gbuffer`).
867
1188
  @fragment
868
- fn fs_gbuffer(in : VsOut) -> GBufferOutput {
869
- let baseUv = transformedMaterialUv(
870
- material.baseColorTextureCoordinatesTransform,
871
- material.baseColorTextureCoordinatesMetadata,
872
- in,
873
- );
874
- let baseSample = sampleMaterialTexture(baseColorTexture, baseColorSampler, baseUv, material.baseColorTextureCoordinatesMetadata.zw);
875
- let vertexColor = materialVertexColor(in);
876
- alphaTest(material.baseColor.a * baseSample.a * vertexColor.a);
877
- let alpha = materialAlpha(baseSample) * vertexColor.a;
878
- let albedo = material.baseColor.rgb * baseSample.rgb * vertexColor.rgb;
879
-
880
- let mrUv = transformedMaterialUv(
881
- material.metallicRoughnessTextureCoordinatesTransform,
882
- material.metallicRoughnessTextureCoordinatesMetadata,
883
- in,
884
- );
885
- let mrSample = sampleMaterialTexture(metallicRoughnessTexture, metallicRoughnessSampler, mrUv, material.metallicRoughnessTextureCoordinatesMetadata.zw);
886
- let metallic = material.metallic * pick_channel(mrSample, u32(material.metallicChannel));
887
- let roughnessTex = pick_channel(mrSample, u32(material.roughnessChannel));
888
-
889
- var a = max(material.roughness, 0.04);
890
- a = a * roughnessTex;
891
-
892
- let normalUv = transformedMaterialUv(material.normalTextureCoordinatesTransform, material.normalTextureCoordinatesMetadata, in);
893
- let normSampleRg = sampleMaterialTexture(normalTexture, normalSampler, normalUv, material.normalTextureCoordinatesMetadata.zw).rg;
894
- let normTangent = scaleTangentSpaceNormal(
895
- decodeTangentSpaceNormalRg(normSampleRg), material.normalScale,
896
- );
897
- let n = applyTBN(in.worldNormal, in.worldTangent, normTangent);
898
-
899
- let emissiveUv = transformedMaterialUv(material.emissiveTextureCoordinatesTransform, material.emissiveTextureCoordinatesMetadata, in);
900
- let emissiveSample = sampleMaterialTexture(emissiveTexture, emissiveSampler, emissiveUv, material.emissiveTextureCoordinatesMetadata.zw).rgb;
901
- let emissive = material.emissive * material.emissiveIntensity * emissiveSample;
902
-
903
- let occlusionUv = transformedMaterialUv(material.occlusionTextureCoordinatesTransform, material.occlusionTextureCoordinatesMetadata, in);
904
- let aoSample = sampleMaterialTexture(occlusionTexture, occlusionSampler, occlusionUv, material.occlusionTextureCoordinatesMetadata.zw);
905
- let ao = mix(1.0, aoSample.r, material.occlusionStrength);
1189
+ fn fs_gbuffer(in : VsOut, @builtin(front_facing) frontFacing : bool) -> GBufferOutput {
1190
+ let surface = evaluateStandardSurface(in, frontFacing);
1191
+ alphaTestSurface(surface);
1192
+ let albedo = surface.baseColor;
1193
+ let metallic = clamp(surface.metallic, 0.0, 1.0);
1194
+ let roughness = clamp(surface.roughness, 0.04, 1.0);
1195
+ let n = normalize(surface.normalWS);
906
1196
 
907
1197
  var out : GBufferOutput;
908
- out.normal_roughness = vec4<f32>(n * 0.5 + 0.5, a);
1198
+ out.normal_roughness = vec4<f32>(n * 0.5 + 0.5, roughness);
909
1199
  out.albedo_metallic = vec4<f32>(albedo, metallic);
910
- out.emissive_ao = vec4<f32>(emissive, ao);
1200
+ out.emissive_ao = vec4<f32>(surface.emissive, surface.occlusion);
911
1201
  return out;
912
1202
  }
913
1203
 
@@ -923,7 +1213,6 @@ struct TemporalVsOut {
923
1213
  @location(7) uv7 : vec2<f32>,
924
1214
  @location(8) @interpolate(perspective) currentClip : vec4<f32>,
925
1215
  @location(9) @interpolate(perspective) previousClip : vec4<f32>,
926
- @location(10) @interpolate(flat) reactive : f32,
927
1216
  #ifdef VERTEX_COLOR_AVAILABLE
928
1217
  @location(14) color : vec4<f32>,
929
1218
  #endif
@@ -934,7 +1223,7 @@ fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32) -> TemporalVsOut {
934
1223
  let currentWorld = meshes[0].worldFromLocal *
935
1224
  instances[idx].localFromInstance * vec4<f32>(in.pos, 1.0);
936
1225
  var previousWorld = currentWorld;
937
- #ifdef STORAGE_BUFFER_AVAILABLE
1226
+ #if STORAGE_BUFFER_AVAILABLE == true
938
1227
  previousWorld = meshes[0].previousWorldFromLocal *
939
1228
  instances[idx].previousLocalFromInstance * vec4<f32>(in.pos, 1.0);
940
1229
  #endif
@@ -942,11 +1231,6 @@ fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32) -> TemporalVsOut {
942
1231
  out.currentClip = view.temporalCurrentViewProj * currentWorld;
943
1232
  out.clip = out.currentClip;
944
1233
  out.previousClip = view.temporalPreviousViewProj * previousWorld;
945
- #if STORAGE_BUFFER_AVAILABLE == true
946
- out.reactive = meshes[0].temporal.x;
947
- #else
948
- out.reactive = 1.0;
949
- #endif
950
1234
  out.uv = in.uv;
951
1235
  out.uv1 = in.uv1;
952
1236
  out.uv2 = in.uv2;
@@ -972,7 +1256,7 @@ fn temporalVertexAlpha(in : TemporalVsOut) -> f32 {
972
1256
  @fragment
973
1257
  fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
974
1258
  var reactive = 0.0;
975
- #ifdef STORAGE_BUFFER_AVAILABLE
1259
+ #if STORAGE_BUFFER_AVAILABLE == true
976
1260
  reactive = meshes[0].temporal.x;
977
1261
  #endif
978
1262
  return projectPbrSceneTemporal(
@@ -985,7 +1269,7 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
985
1269
  in.currentClip,
986
1270
  in.previousClip,
987
1271
  view.temporalProjection,
988
- in.reactive,
1272
+ reactive,
989
1273
  in.uv, in.uv1, in.uv2, in.uv3,
990
1274
  in.uv4, in.uv5, in.uv6, in.uv7,
991
1275
  );