@forgeax/engine-shader 0.1.20 → 0.1.23

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 (74) hide show
  1. package/README.md +10 -0
  2. package/dist/ShaderRegistry.d.ts.map +1 -1
  3. package/dist/index.d.ts +6 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.mjs +33070 -8
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/ltc/provenance.d.ts +17 -0
  8. package/dist/ltc/provenance.d.ts.map +1 -0
  9. package/dist/ltc/tables.d.ts +22 -0
  10. package/dist/ltc/tables.d.ts.map +1 -0
  11. package/dist/material/artifact-types.d.ts +52 -0
  12. package/dist/material/artifact-types.d.ts.map +1 -1
  13. package/dist/material-schemas.d.ts +5 -0
  14. package/dist/material-schemas.d.ts.map +1 -1
  15. package/dist/register-default-sprite-lit.d.ts +3 -6
  16. package/dist/register-default-sprite-lit.d.ts.map +1 -1
  17. package/dist/types.d.ts +2 -1
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +4 -4
  20. package/src/ShaderRegistry.ts +65 -0
  21. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -4
  22. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +7 -0
  23. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +138 -30
  24. package/src/__tests__/direct-light-layout.unit.test.ts +40 -0
  25. package/src/__tests__/ibl-irradiance.unit.test.ts +7 -4
  26. package/src/__tests__/ibl-sampling.unit.test.ts +3 -4
  27. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +16 -2
  28. package/src/__tests__/lighting-punctual.unit.test.ts +61 -8
  29. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +1 -1
  30. package/src/__tests__/ltc-provenance.unit.test.ts +33 -0
  31. package/src/__tests__/manifest.fixture.json +33 -0
  32. package/src/__tests__/material-contract.unit.test.ts +61 -1
  33. package/src/__tests__/material-derived-builtins.integration.test.ts +27 -0
  34. package/src/__tests__/probe-lighting-composition.unit.test.ts +38 -0
  35. package/src/__tests__/rect-area-brdf.unit.test.ts +36 -0
  36. package/src/__tests__/reflection-probe-sampling.unit.test.ts +45 -0
  37. package/src/__tests__/scene-temporal.unit.test.ts +20 -0
  38. package/src/__tests__/shader.unit.test.ts +40 -15
  39. package/src/__tests__/shadow-pcss.unit.test.ts +131 -0
  40. package/src/__tests__/spot-cookie-composition.unit.test.ts +57 -0
  41. package/src/__tests__/spot-modifier-composition.unit.test.ts +19 -0
  42. package/src/__tests__/sprite-lit-shader.test.ts +24 -11
  43. package/src/__tests__/sprite-variants.unit.test.ts +5 -1
  44. package/src/__tests__/transmission-thickness-scale.unit.test.ts +7 -1
  45. package/src/__tests__/transparent-pbr.unit.test.ts +7 -0
  46. package/src/brdf.wgsl +115 -1
  47. package/src/common.wgsl +95 -91
  48. package/src/default-standard-pbr-skin.wgsl +108 -92
  49. package/src/default-standard-pbr.wgsl +189 -191
  50. package/src/fxaa.wgsl +27 -6
  51. package/src/ibl-irradiance.wgsl +4 -1
  52. package/src/ibl-sampling.wgsl +56 -2
  53. package/src/index.ts +23 -0
  54. package/src/lighting-directional.wgsl +161 -132
  55. package/src/lighting-probe.wgsl +51 -0
  56. package/src/lighting-punctual.wgsl +69 -29
  57. package/src/lighting-rect-area.wgsl +162 -0
  58. package/src/lighting-spot-modifiers.wgsl +93 -0
  59. package/src/ltc/provenance.ts +19 -0
  60. package/src/ltc/tables.ts +2831 -0
  61. package/src/material/artifact-types.ts +137 -0
  62. package/src/material-schemas.ts +12 -0
  63. package/src/register-default-sprite-lit.ts +3 -6
  64. package/src/scene-temporal.wgsl +14 -8
  65. package/src/shadow-pcf.wgsl +44 -1
  66. package/src/sprite-lit.wgsl +37 -53
  67. package/src/sprite.wgsl +1 -1
  68. package/src/standard-cluster.wgsl +274 -0
  69. package/src/tonemap.wgsl +8 -3
  70. package/src/types.ts +2 -1
  71. package/src/volume/volume-inject.wgsl +48 -38
  72. package/src/volume/volume-integrate.wgsl +47 -42
  73. package/dist/.tsbuildinfo +0 -1
  74. package/src/hdrp-cluster-forward.wgsl +0 -224
@@ -1,14 +1,151 @@
1
1
  import type { BindGroupLayoutDescriptor } from '@forgeax/engine-types';
2
2
 
3
+ export interface MaterialShaderResourceSlot {
4
+ readonly name: string;
5
+ readonly parameter: string;
6
+ readonly kind: 'sampler' | 'texture';
7
+ readonly group: number;
8
+ readonly binding: number;
9
+ }
10
+
11
+ export interface MaterialShaderVertexInput {
12
+ readonly semantic: string;
13
+ readonly location: number;
14
+ readonly format: string;
15
+ }
16
+
17
+ export interface MaterialShaderArtifactReceipt {
18
+ readonly directEntry: string;
19
+ readonly sceneIndexEntry: string;
20
+ readonly materialRow: {
21
+ readonly byteLength: number;
22
+ readonly fields: readonly string[];
23
+ };
24
+ readonly resourceSlots: readonly MaterialShaderResourceSlot[];
25
+ readonly uvSets: readonly { readonly parameter: string; readonly set: number }[];
26
+ readonly vertexInputs: readonly MaterialShaderVertexInput[];
27
+ readonly alphaMask: { readonly cutoff: string; readonly source: string };
28
+ readonly skinPaletteAddress?: {
29
+ readonly group: number;
30
+ readonly binding: number;
31
+ readonly stride: number;
32
+ };
33
+ readonly reflection: {
34
+ readonly layoutIdentity: string;
35
+ readonly resourceSlots: readonly MaterialShaderResourceSlot[];
36
+ readonly vertexInputs: readonly MaterialShaderVertexInput[];
37
+ };
38
+ readonly receiptIdentity: string;
39
+ readonly generation: number;
40
+ }
41
+
3
42
  export interface MaterialShaderArtifact {
4
43
  readonly material: string;
5
44
  readonly pass: string;
6
45
  readonly wgsl: string;
7
46
  readonly layoutIdentity: string;
47
+ /** Naga-reflected UV set count used to derive clamp-to-last vertex aliases. */
48
+ readonly uvSetCount?: number;
8
49
  readonly bindings: readonly BindGroupLayoutDescriptor[];
9
50
  readonly deps: readonly string[];
10
51
  readonly vertexInputs: readonly Readonly<Record<string, unknown>>[];
11
52
  readonly specializationKey?: string;
53
+ /** Producer-owned ABI facts shared by direct and scene-index consumers. */
54
+ readonly receipt?: MaterialShaderArtifactReceipt;
55
+ }
56
+
57
+ const STANDARD_PBR_TEXTURES = [
58
+ 'baseColorTexture',
59
+ 'metallicRoughnessTexture',
60
+ 'normalTexture',
61
+ 'specularTintTexture',
62
+ 'emissiveTexture',
63
+ 'occlusionTexture',
64
+ 'transmissionTexture',
65
+ 'thicknessTexture',
66
+ ] as const;
67
+
68
+ const STANDARD_PBR_NUMERIC_FIELDS = [
69
+ 'baseColor',
70
+ 'metallic',
71
+ 'roughness',
72
+ 'metallicChannel',
73
+ 'roughnessChannel',
74
+ 'aoChannel',
75
+ 'extraChannel',
76
+ 'emissive',
77
+ 'emissiveIntensity',
78
+ 'occlusionStrength',
79
+ 'alphaCutoff',
80
+ 'clearcoat',
81
+ 'clearcoatRoughness',
82
+ 'specularTint',
83
+ 'normalScale',
84
+ 'transmission',
85
+ 'ior',
86
+ 'thickness',
87
+ 'attenuationColor',
88
+ 'attenuationDistance',
89
+ ] as const;
90
+
91
+ const STANDARD_PBR_VERTEX_INPUTS: readonly MaterialShaderVertexInput[] = [
92
+ { semantic: 'position', location: 0, format: 'float32x3' },
93
+ { semantic: 'normal', location: 1, format: 'float32x3' },
94
+ { semantic: 'uv', location: 2, format: 'float32x2' },
95
+ { semantic: 'tangent', location: 3, format: 'float32x4' },
96
+ ];
97
+
98
+ function standardPbrResourceSlots(): MaterialShaderResourceSlot[] {
99
+ return STANDARD_PBR_TEXTURES.flatMap((parameter, index) => [
100
+ {
101
+ name: `${parameter}_sampler`,
102
+ parameter,
103
+ kind: 'sampler' as const,
104
+ group: 1,
105
+ binding: index * 2 + 1,
106
+ },
107
+ {
108
+ name: parameter,
109
+ parameter,
110
+ kind: 'texture' as const,
111
+ group: 1,
112
+ binding: index * 2 + 2,
113
+ },
114
+ ]);
115
+ }
116
+
117
+ /**
118
+ * Builds the one Standard PBR ABI receipt consumed by both material lanes.
119
+ * The skinned module uses the same material producer and only adds its palette
120
+ * address; it must retain the same receipt identity for material compatibility.
121
+ */
122
+ export function createStandardPbrArtifactReceipt(skinned = false): MaterialShaderArtifactReceipt {
123
+ const resourceSlots = standardPbrResourceSlots();
124
+ const uvSets = STANDARD_PBR_TEXTURES.map((parameter) => ({ parameter, set: 0 }));
125
+ const vertexInputs = skinned
126
+ ? [
127
+ ...STANDARD_PBR_VERTEX_INPUTS,
128
+ { semantic: 'skinIndex', location: 4, format: 'uint16x4' },
129
+ { semantic: 'skinWeight', location: 5, format: 'float32x4' },
130
+ ]
131
+ : [...STANDARD_PBR_VERTEX_INPUTS];
132
+ return {
133
+ directEntry: 'vs_main',
134
+ sceneIndexEntry: 'vs_scene_index',
135
+ materialRow: { byteLength: 384, fields: [...STANDARD_PBR_NUMERIC_FIELDS] },
136
+ resourceSlots,
137
+ uvSets,
138
+ vertexInputs,
139
+ alphaMask: { cutoff: 'alphaCutoff', source: 'baseColor.a' },
140
+ ...(skinned ? { skinPaletteAddress: { group: 2, binding: 1, stride: 64 } } : {}),
141
+ reflection: {
142
+ layoutIdentity: 'standard-pbr/material-row-v1',
143
+ resourceSlots,
144
+ vertexInputs,
145
+ },
146
+ receiptIdentity: 'standard-pbr/material-row-v1',
147
+ generation: 1,
148
+ };
12
149
  }
13
150
 
14
151
  export function isMaterialShaderArtifact(value: unknown): value is MaterialShaderArtifact {
@@ -1,4 +1,8 @@
1
1
  import type { ParamSchemaEntry } from '@forgeax/engine-types';
2
+ import {
3
+ createStandardPbrArtifactReceipt,
4
+ type MaterialShaderArtifactReceipt,
5
+ } from './material/artifact-types.js';
2
6
 
3
7
  export const STANDARD_PBR_ALPHA_CUTOFF_DEFAULT = 0;
4
8
 
@@ -34,6 +38,14 @@ export const DEFAULT_STANDARD_PBR_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
34
38
  { name: 'thicknessTexture', type: 'texture2d' },
35
39
  ];
36
40
 
41
+ /** The single producer receipt shared by direct and scene-index Standard PBR. */
42
+ export const STANDARD_PBR_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt =
43
+ createStandardPbrArtifactReceipt();
44
+
45
+ /** Skinned Standard PBR keeps the material receipt identity and adds palette ABI. */
46
+ export const STANDARD_PBR_SKIN_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt =
47
+ createStandardPbrArtifactReceipt(true);
48
+
37
49
  export const DEFAULT_UNLIT_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
38
50
  { name: 'baseColor', type: 'color', default: [1, 1, 1, 1] },
39
51
  { name: 'alphaCutoff', type: 'f32', default: 0 },
@@ -56,12 +56,9 @@ export interface SpriteLitCaps {
56
56
  * compiled by @forgeax/engine-vite-plugin-shader at build time and surfaced
57
57
  * in manifest.json. The caller reads it from the manifest entries.
58
58
  *
59
- * `caps.storageBuffer` switches the runtime BGL buffer types for
60
- * @group(0) bindings 1+2 (pointLightsBuffer / spotLightsBuffer):
61
- * `false` -> uniform fallback (AC-10 WebGL2 path); `true` -> read-only-storage.
62
- * The BGL itself is built downstream in runtime/pbr-pipeline.ts (sprite-lit
63
- * reuses pbr-view / pbr-mesh-array / pbr-instances unchanged; AC-07
64
- * BGL byte-identical).
59
+ * `caps.storageBuffer` is retained for the shared mesh/instance layout
60
+ * selection. Sprite-lit local lighting reads the same Standard Cluster group
61
+ * as PBR; the view group has no direct point/spot arrays.
65
62
  *
66
63
  * @param registry ShaderRegistry instance (engine boot path).
67
64
  * @param composedWgsl Post-naga_oil composed WGSL source string.
@@ -27,17 +27,23 @@ fn sceneTemporalUv(clip : vec4<f32>) -> vec2<f32> {
27
27
  return vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
28
28
  }
29
29
 
30
- fn sceneTemporalViewDepth(clip : vec4<f32>, temporalProjection : vec4<f32>) -> f32 {
31
- let perspectiveDepth = max(clip.w, 0.0);
30
+ // Signed view-space Z shared by temporal reconstruction and Standard Cluster
31
+ // lookup. Both CPU and WGSL binners consume the negative camera-space depth:
32
+ // perspective uses -clip.w, while orthographic depth is reconstructed from
33
+ // the projection's near/far payload rather than Euclidean world distance.
34
+ fn sceneViewZ(clip : vec4<f32>, temporalProjection : vec4<f32>) -> f32 {
32
35
  let ndcDepth = clip.z / max(abs(clip.w), 1e-6);
33
- let orthographicDepth = temporalProjection.x +
34
- ndcDepth * (temporalProjection.y - temporalProjection.x);
35
- let viewDepth = select(
36
- perspectiveDepth,
37
- max(orthographicDepth, 0.0),
36
+ let orthographicViewZ = -(temporalProjection.x +
37
+ ndcDepth * (temporalProjection.y - temporalProjection.x));
38
+ return select(
39
+ -clip.w,
40
+ orthographicViewZ,
38
41
  temporalProjection.z >= 0.5,
39
42
  );
40
- return log2(1.0 + viewDepth);
43
+ }
44
+
45
+ fn sceneTemporalViewDepth(clip : vec4<f32>, temporalProjection : vec4<f32>) -> f32 {
46
+ return log2(1.0 + max(-sceneViewZ(clip, temporalProjection), 0.0));
41
47
  }
42
48
 
43
49
  fn packSceneTemporalV1(
@@ -20,6 +20,49 @@
20
20
  //
21
21
  // Return: 1.0 = fully lit, 0.0 = fully shadowed.
22
22
 
23
+ // === Directional PCSS sampling primitives ====================================
24
+ //
25
+ // These functions deliberately accept tile bounds from their caller. They own
26
+ // only integer addressing, raw depth, comparison sampling, and the shared
27
+ // receiver-bias equation; cascade selection, profile selection, blocker
28
+ // averaging, penumbra sizing, and cascade blending remain Directional policy.
29
+
30
+ fn shadow_clamp_texel_to_tile(
31
+ texel : vec2<i32>,
32
+ tileOrigin : vec2<i32>,
33
+ tileSize : vec2<i32>,
34
+ inset : i32,
35
+ ) -> vec2<i32> {
36
+ let tileMin = tileOrigin + vec2<i32>(inset);
37
+ let tileMax = tileOrigin + max(vec2<i32>(inset), tileSize - vec2<i32>(1 + inset));
38
+ return min(tileMax, max(tileMin, texel));
39
+ }
40
+
41
+ fn shadow_load_raw_depth(
42
+ shadowMap : texture_depth_2d,
43
+ texel : vec2<i32>,
44
+ ) -> f32 {
45
+ return textureLoad(shadowMap, texel, 0);
46
+ }
47
+
48
+ fn shadow_sample_compare(
49
+ shadowMap : texture_depth_2d,
50
+ shadowSampler: sampler_comparison,
51
+ uv : vec2<f32>,
52
+ depthRef : f32,
53
+ ) -> f32 {
54
+ return textureSampleCompareLevel(shadowMap, shadowSampler, uv, depthRef);
55
+ }
56
+
57
+ fn shadow_biased_receiver_depth(
58
+ receiverDepth : f32,
59
+ normalBias : f32,
60
+ depthBias : f32,
61
+ nDotL : f32,
62
+ ) -> f32 {
63
+ return receiverDepth - max(normalBias * (1.0 - nDotL), depthBias);
64
+ }
65
+
23
66
  // 3x3 integer offset table for PCF kernel (9 taps).
24
67
  // Extracted from lighting-directional.wgsl inline loop (T-M2-4);
25
68
  // `sample_shadow_2d` walks all 9 taps; `sample_shadow_cube_hw2x2` does NOT
@@ -99,7 +142,7 @@ fn sample_shadow_2d_kernel(
99
142
  // `shadowAtlas` is @group(0) @binding(5) texture_depth_cube_array.
100
143
  // `shadowSampler` is @group(0) @binding(4) sampler_comparison (shared with
101
144
  // directional shadows — same sampler type, no dimension distinction).
102
- // `lightLocal` is the fragment-to-light direction vector in the cubemap's
145
+ // `lightLocal` is the light-to-fragment direction vector in the cubemap's
103
146
  // coordinate system (Bevy convention: left-handed cubemap; caller applies
104
147
  // flip_z = vec3(1,1,-1) to convert right-hand world to left-hand cubemap).
105
148
  // `layer` is the cube_array layer index (i32; 0..3 for 4 shadow-casting
@@ -1,8 +1,12 @@
1
1
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
2
+ #pragma variant_axis CLUSTER_FORWARD_AVAILABLE
2
3
  #define_import_path forgeax_material::sprite-lit
3
4
 
4
- #import forgeax_view::common::{View, Mesh, InstanceData, PointLight, SpotLight, view, meshes, instances, pointLightsBuffer, spotLightsBuffer, sampleMaterialTexture}
5
- #import forgeax_scene_temporal::{packSceneTemporalV1}
5
+ #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture, packSceneTemporal}
6
+ #import forgeax_scene_temporal::{packSceneTemporalV1, sceneViewZ}
7
+ #ifdef CLUSTER_FORWARD_AVAILABLE
8
+ #import forgeax_standard::cluster::{evaluateStandardClusterLights}
9
+ #endif
6
10
 
7
11
  // @forgeax/engine-shader - sprite-lit.wgsl
8
12
  // (tweak-20260701-sprite-lit-flat-default-drop-ndotl-for-2d).
@@ -26,7 +30,8 @@
26
30
  // - no shadow receiver / caster
27
31
  // - no Godot-style light_mode blends
28
32
  // - no IBL ambient
29
- // - no HDRP cluster-forward path
33
+ // - Standard clustered punctual lights are enabled by the
34
+ // CLUSTER_FORWARD_AVAILABLE variant.
30
35
  //
31
36
  // >>> AI user error self-recovery hint:
32
37
  // sprite-lit needs at least one light in scene; for unlit sprites use
@@ -34,12 +39,11 @@
34
39
  // 1 string change in MaterialAsset.passes[0].shader).
35
40
  //
36
41
  // Bindings (byte-identical to sprite.wgsl so the 4 BindGroupLayout chain is
37
- // reused without a per-pipeline BGL; the 4 PBR-unused entries 3..6 bind
42
+ // reused without a per-pipeline BGL; the PBR-unused entries 3..6 bind
38
43
  // pipelineState.defaultSampler + .defaultWhiteTextureView on the host side):
39
44
  //
40
45
  // @group(0) @binding(0) view uniform
41
- // @group(0) @binding(1) pointLightsBuffer storage / uniform (variant)
42
- // @group(0) @binding(2) spotLightsBuffer storage / uniform (variant)
46
+ // @group(2) @binding(3..6) Standard Cluster storage (selected variant)
43
47
  // @group(1) @binding(0) material uniform (sprite layout
44
48
  // colorTint /
45
49
  // region /
@@ -80,16 +84,17 @@ struct VsIn {
80
84
  @location(3) tangent : vec4<f32>,
81
85
  };
82
86
 
83
- // VsOut carries the atlas UV plus the per-fragment world-space position.
84
- // The world position is emitted from the vertex stage (already computed
85
- // there for `out.clip`), so PointLight / SpotLight attenuation gets an
86
- // exact per-fragment worldPos through interpolation. This replaces the
87
- // earlier fragment-side reconstruction path, which collapsed under
88
- // multi-instance draws and could not handle the 9-slice quad layout.
87
+ // VsOut carries the atlas UV plus the per-fragment world-space position and
88
+ // the Standard cluster coordinates produced by the vertex stage. Fragment
89
+ // `@builtin(position)` is framebuffer-space after rasterization, so it is not
90
+ // a valid source for NDC cluster lookup; keep the perspective divide here and
91
+ // interpolate the result alongside worldPos.
89
92
  struct VsOut {
90
93
  @builtin(position) clip : vec4<f32>,
91
94
  @location(0) uv_atlas : vec2<f32>,
92
95
  @location(1) worldPos : vec3<f32>,
96
+ @location(2) ndc : vec3<f32>,
97
+ @location(3) viewZ : f32,
93
98
  };
94
99
 
95
100
  struct SpriteVertex {
@@ -155,6 +160,11 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index)
155
160
  out.clip = view.worldViewProj * world;
156
161
  out.uv_atlas = vertex.uvAtlas;
157
162
  out.worldPos = world.xyz;
163
+ let clipPos = out.clip;
164
+ out.ndc = clipPos.xyz / clipPos.w;
165
+ // Keep the cluster depth exactly aligned with the CPU binner for both
166
+ // perspective and off-axis orthographic projections.
167
+ out.viewZ = sceneViewZ(clipPos, view.temporalProjection);
158
168
  return out;
159
169
  }
160
170
 
@@ -174,50 +184,24 @@ fn spriteLitDirectional(albedo : vec3<f32>) -> vec3<f32> {
174
184
  return albedo * view.lightColor;
175
185
  }
176
186
 
177
- // Flat point-light contribution. Applies the KHR-lights-punctual smooth-
178
- // window range attenuation only. `attenuation` is 0 outside the light's
179
- // range (`invRangeSquared`) and 1 at the light center; the reciprocal-square
180
- // term gives the physical falloff. URP cap = 4 point lights per pass.
181
- fn spriteLitPoint(p : PointLight, worldPos : vec3<f32>, albedo : vec3<f32>) -> vec3<f32> {
182
- let toLight = p.position - worldPos;
183
- let dSquared = max(dot(toLight, toLight), 1e-4);
184
- let factor = 1.0 - (dSquared * p.invRangeSquared) * (dSquared * p.invRangeSquared);
185
- let attenuation = max(min(factor, 1.0), 0.0) / dSquared;
186
- return albedo * p.colorTimesIntensity * attenuation;
187
- }
188
-
189
- // Flat spot-light contribution. Same range attenuation as spriteLitPoint,
190
- // modulated by a smoothstep cone factor (host-side degree -> cosine
191
- // conversion). Spot direction only shapes the cone; it does not project
192
- // onto a surface normal. URP cap = 4 spot lights per pass.
193
- fn spriteLitSpot(s : SpotLight, worldPos : vec3<f32>, albedo : vec3<f32>) -> vec3<f32> {
194
- let toLight = s.position - worldPos;
195
- let dSquared = max(dot(toLight, toLight), 1e-4);
196
- let l = toLight / sqrt(dSquared);
197
- let factor = 1.0 - (dSquared * s.invRangeSquared) * (dSquared * s.invRangeSquared);
198
- let attenuation = max(min(factor, 1.0), 0.0) / dSquared;
199
- let cone = smoothstep(s.cosOuter, s.cosInner, dot(l, -s.direction));
200
- return albedo * s.colorTimesIntensity * attenuation * cone;
201
- }
202
-
203
187
  // Shared shading body used by both fs_main (LDR) and fs_main_hdr (HDR).
204
188
  // The two entry points differ only in their tail-end clamp + srgb encode
205
189
  // step (LDR clamp / HDR pass-through boundary).
206
- fn spriteLitShadeAccum(albedo : vec3<f32>, worldPos : vec3<f32>) -> vec3<f32> {
190
+ fn spriteLitShadeAccum(
191
+ albedo : vec3<f32>,
192
+ worldPos : vec3<f32>,
193
+ ndc : vec3<f32>,
194
+ viewZ : f32,
195
+ ) -> vec3<f32> {
207
196
  // Directional contribution (1 light, View UBO).
208
197
  var lit = spriteLitDirectional(albedo);
209
- // Point-light contribution (URP cap = 4; PointLightsArray.count).
210
- let pointCount = pointLightsBuffer.count;
211
- for (var i : u32 = 0u; i < pointCount; i = i + 1u) {
212
- let p = pointLightsBuffer.slots[i];
213
- lit = lit + spriteLitPoint(p, worldPos, albedo);
214
- }
215
- // Spot-light contribution (URP cap = 4).
216
- let spotCount = spotLightsBuffer.count;
217
- for (var i : u32 = 0u; i < spotCount; i = i + 1u) {
218
- let s = spotLightsBuffer.slots[i];
219
- lit = lit + spriteLitSpot(s, worldPos, albedo);
220
- }
198
+ #ifdef CLUSTER_FORWARD_AVAILABLE
199
+ let viewDir = normalize(view.cameraPos - worldPos);
200
+ lit = lit + evaluateStandardClusterLights(
201
+ ndc, viewZ, worldPos, vec3<f32>(0.0, 0.0, 1.0),
202
+ viewDir, albedo, 0.0, 1.0, vec3<f32>(0.04), true,
203
+ );
204
+ #endif
221
205
  return lit;
222
206
  }
223
207
 
@@ -225,7 +209,7 @@ fn spriteLitShadeAccum(albedo : vec3<f32>, worldPos : vec3<f32>) -> vec3<f32> {
225
209
  fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
226
210
  let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
227
211
  let albedo4 = texel * material.colorTint;
228
- let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
212
+ let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos, in.ndc, in.viewZ);
229
213
  // Strict clamp 0..1 before premultiplied alpha multiply keeps the LDR
230
214
  // path bounded even when a scene stacks many lights.
231
215
  let lit_rgba = clamp(vec4<f32>(lit, albedo4.a), vec4<f32>(0.0), vec4<f32>(1.0));
@@ -243,7 +227,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
243
227
  fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
244
228
  let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
245
229
  let albedo4 = texel * material.colorTint;
246
- let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
230
+ let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos, in.ndc, in.viewZ);
247
231
  // HDR variant: do NOT clamp the lit output to [0, 1]; let the tonemap
248
232
  // pass absorb HDR values > 1. Alpha stays clamped because the premult
249
233
  // math requires alpha in [0, 1].
package/src/sprite.wgsl CHANGED
@@ -152,7 +152,7 @@ fn resolveSpriteVertex(in : VsIn, idx : u32, vertex_index : u32) -> SpriteVertex
152
152
  // pivotAndSize.zw scaled the local quad AND the world matrix also scaled
153
153
  // it, producing the scale^2 visual size debt that research F-4 flagged.
154
154
  // Post-w11 there is one and only one scale source: the entity's
155
- // Transform.world. The mesh's built-in vertex.pos is NOT consumed (sprite
155
+ // GlobalTransform.world. The mesh's built-in vertex.pos is NOT consumed (sprite
156
156
  // geometry is fully re-derived from uv + pivot) so HANDLE_QUAD's mesh.pos
157
157
  // remains a placeholder driving vertex count (4) and topology only.
158
158
  //