@forgeax/engine-shader 0.1.21 → 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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +4 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.mjs +33019 -8
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/ltc/provenance.d.ts +17 -0
  7. package/dist/ltc/provenance.d.ts.map +1 -0
  8. package/dist/ltc/tables.d.ts +22 -0
  9. package/dist/ltc/tables.d.ts.map +1 -0
  10. package/dist/material/artifact-types.d.ts +52 -0
  11. package/dist/material/artifact-types.d.ts.map +1 -1
  12. package/dist/material-schemas.d.ts +5 -0
  13. package/dist/material-schemas.d.ts.map +1 -1
  14. package/dist/register-default-sprite-lit.d.ts +3 -6
  15. package/dist/register-default-sprite-lit.d.ts.map +1 -1
  16. package/dist/types.d.ts +2 -1
  17. package/dist/types.d.ts.map +1 -1
  18. package/package.json +4 -4
  19. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +121 -58
  20. package/src/__tests__/direct-light-layout.unit.test.ts +40 -0
  21. package/src/__tests__/ibl-irradiance.unit.test.ts +7 -4
  22. package/src/__tests__/ibl-sampling.unit.test.ts +3 -4
  23. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +8 -2
  24. package/src/__tests__/lighting-punctual.unit.test.ts +29 -2
  25. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +1 -1
  26. package/src/__tests__/ltc-provenance.unit.test.ts +33 -0
  27. package/src/__tests__/manifest.fixture.json +33 -0
  28. package/src/__tests__/material-contract.unit.test.ts +61 -1
  29. package/src/__tests__/probe-lighting-composition.unit.test.ts +38 -0
  30. package/src/__tests__/rect-area-brdf.unit.test.ts +36 -0
  31. package/src/__tests__/reflection-probe-sampling.unit.test.ts +24 -2
  32. package/src/__tests__/shader.unit.test.ts +6 -2
  33. package/src/__tests__/spot-cookie-composition.unit.test.ts +57 -0
  34. package/src/__tests__/spot-modifier-composition.unit.test.ts +19 -0
  35. package/src/__tests__/sprite-variants.unit.test.ts +4 -1
  36. package/src/brdf.wgsl +115 -1
  37. package/src/common.wgsl +53 -79
  38. package/src/default-standard-pbr-skin.wgsl +80 -23
  39. package/src/default-standard-pbr.wgsl +119 -50
  40. package/src/ibl-irradiance.wgsl +4 -1
  41. package/src/ibl-sampling.wgsl +2 -2
  42. package/src/index.ts +17 -0
  43. package/src/lighting-directional.wgsl +6 -116
  44. package/src/lighting-probe.wgsl +51 -0
  45. package/src/lighting-punctual.wgsl +21 -11
  46. package/src/lighting-rect-area.wgsl +162 -0
  47. package/src/lighting-spot-modifiers.wgsl +93 -0
  48. package/src/ltc/provenance.ts +19 -0
  49. package/src/ltc/tables.ts +2831 -0
  50. package/src/material/artifact-types.ts +137 -0
  51. package/src/material-schemas.ts +12 -0
  52. package/src/register-default-sprite-lit.ts +3 -6
  53. package/src/sprite-lit.wgsl +3 -3
  54. package/src/sprite.wgsl +1 -1
  55. package/src/standard-cluster.wgsl +94 -57
  56. package/src/types.ts +2 -1
  57. package/src/volume/volume-inject.wgsl +34 -36
  58. package/src/volume/volume-integrate.wgsl +48 -38
  59. package/dist/.tsbuildinfo +0 -1
@@ -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.
@@ -2,7 +2,7 @@
2
2
  #pragma variant_axis CLUSTER_FORWARD_AVAILABLE
3
3
  #define_import_path forgeax_material::sprite-lit
4
4
 
5
- #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture}
5
+ #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture, packSceneTemporal}
6
6
  #import forgeax_scene_temporal::{packSceneTemporalV1, sceneViewZ}
7
7
  #ifdef CLUSTER_FORWARD_AVAILABLE
8
8
  #import forgeax_standard::cluster::{evaluateStandardClusterLights}
@@ -39,11 +39,11 @@
39
39
  // 1 string change in MaterialAsset.passes[0].shader).
40
40
  //
41
41
  // Bindings (byte-identical to sprite.wgsl so the 4 BindGroupLayout chain is
42
- // 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
43
43
  // pipelineState.defaultSampler + .defaultWhiteTextureView on the host side):
44
44
  //
45
45
  // @group(0) @binding(0) view uniform
46
- // @group(0) @binding(1..2) Standard Cluster storage (selected variant)
46
+ // @group(2) @binding(3..6) Standard Cluster storage (selected variant)
47
47
  // @group(1) @binding(0) material uniform (sprite layout
48
48
  // colorTint /
49
49
  // region /
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
  //
@@ -3,50 +3,56 @@
3
3
  //
4
4
  // The cluster module owns membership and raw payload decoding only. Punctual
5
5
  // BRDF, range, cone, PCF, and fail-open behavior live in lighting-punctual.
6
+ // ProbeBlendRecord is intentionally absent: probes are not DirectLightSlot
7
+ // members and this cluster owner never performs an all-probe loop.
6
8
 
7
9
  #define_import_path forgeax_standard::cluster
8
10
 
9
11
  #import forgeax_pbr::lighting_punctual::{evalPoint, evalPointFlat, evalSpot, evalSpotFlat, evalSpotShadowed}
10
12
  #import forgeax_pbr::lighting_attenuation::{projectSpotUv}
13
+ #import forgeax_pbr::lighting_spot_modifiers::{spotModifierFactors}
14
+ #import forgeax_pbr::lighting_rect_area::{evalRectAreaLtcGgx}
11
15
  #import forgeax_view::common::{view}
12
16
  #ifdef PROJECTOR_AVAILABLE
17
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
18
+ #import forgeax_view::common::{spotModifierSampler, cookieTexture}
19
+ #else
20
+ #ifndef EXTENDED_LIGHTING_AVAILABLE
13
21
  #import forgeax_view::common::{projectorTexture, projectorSampler}
14
22
  #endif
23
+ #endif
24
+ #endif
15
25
  #ifdef POINT_SHADOW_AVAILABLE
16
26
  #import forgeax_pbr::lighting_punctual::{evalPointShadowed}
27
+ #import forgeax_view::common::{shadowParams}
17
28
  #endif
18
29
 
19
30
  #ifdef CLUSTER_FORWARD_AVAILABLE
20
31
 
21
32
  const KIND_POINT: u32 = 0u;
22
33
  const KIND_SPOT: u32 = 1u;
23
- // LightSlot.kind_and_shadow.y uses one high bit as the accepted projector
24
- // marker. The remaining bits are the shadow-atlas tile (0..3), with zero as
25
- // the explicit View-UBO lane for projector-only spots.
34
+ const KIND_RECT_AREA: u32 = 2u;
26
35
  const PROJECTOR_FLAG: i32 = 0x40000000;
27
36
  const TILE_MASK: i32 = 0x3fffffff;
28
37
 
29
- // LightSlot is the byte-frozen 64B std430/std140 transport contract.
30
- // [0..2] position, [3] invRangeSquared
31
- // [4..6] colorTimesIntensity, [7] cosInner
32
- // [8..10] direction, [11] cosOuter
33
- // [12] raw kind u32, [13] shadow identity i32
34
- // [14..15] point near/far f32 bits; spot keeps these lanes zero.
35
- struct LightSlot {
36
- position : vec4<f32>,
37
- color : vec4<f32>,
38
- direction : vec4<f32>,
39
- kind_and_shadow : vec4<u32>,
38
+ // DirectLightSlot is the byte-frozen 80B std430/std140 transport contract.
39
+ // The named rows match the host packer; metadata carries kind and identities.
40
+ struct DirectLightSlot {
41
+ position : vec4<f32>,
42
+ colorTimesIntensity : vec4<f32>,
43
+ direction : vec4<f32>,
44
+ auxiliary : vec4<f32>,
45
+ metadata : vec4<u32>,
40
46
  };
41
47
 
42
- const LIGHTSLOT_BYTE_SIZE: u32 = 64u;
48
+ const DIRECT_LIGHT_SLOT_BYTE_SIZE: u32 = 80u;
43
49
 
44
50
  struct ClusterUniform {
45
51
  grid : vec4<u32>,
46
52
  near_far_log : vec4<f32>,
47
53
  };
48
54
 
49
- @group(2) @binding(3) var<storage, read> light_data : array<LightSlot, 256>;
55
+ @group(2) @binding(3) var<storage, read> light_data : array<DirectLightSlot, 256>;
50
56
  @group(2) @binding(4) var<storage, read> cluster_grid : array<u32>;
51
57
  @group(2) @binding(5) var<storage, read> light_index_list : array<u32>;
52
58
  @group(2) @binding(6) var<uniform> cluster_uniform : ClusterUniform;
@@ -91,19 +97,27 @@ fn ndc_position_to_cluster(
91
97
 
92
98
  // Surface SpotLight projector sampling is owned by the same clustered light
93
99
  // evaluator as the analytic cone/range contribution. The host marks the
94
- // selected projector in the LightSlot flag and writes its matrix to the
100
+ // selected projector in the DirectLightSlot flag and writes its matrix to the
95
101
  // corresponding View lane (lane 0 for projector-only); no second
96
102
  // material-local sampling loop or resource owner is allowed.
97
103
  fn sampleStandardSpotProjector(
98
104
  lightViewProj : mat4x4<f32>,
99
105
  world_pos : vec3<f32>,
106
+ metadata : vec4<u32>,
100
107
  ) -> vec3<f32> {
101
108
  #ifdef PROJECTOR_AVAILABLE
102
109
  let uv = projectSpotUv(lightViewProj, world_pos);
103
110
  if (!(uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0)) {
104
111
  return vec3<f32>(1.0);
105
112
  }
113
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
114
+ if (metadata.w == 0xffffffffu) {
115
+ return vec3<f32>(1.0);
116
+ }
117
+ return textureSampleLevel(cookieTexture, spotModifierSampler, uv, metadata.w, 0.0).rgb;
118
+ #else
106
119
  return textureSampleLevel(projectorTexture, projectorSampler, uv, 0.0).rgb;
120
+ #endif
107
121
  #else
108
122
  return vec3<f32>(1.0);
109
123
  #endif
@@ -112,7 +126,7 @@ fn sampleStandardSpotProjector(
112
126
  // Decode one kind first, then delegate the complete evaluation to the shared
113
127
  // punctual owner. Unknown kinds are explicitly zero contribution.
114
128
  fn evaluate_cluster_light(
115
- light : LightSlot,
129
+ light : DirectLightSlot,
116
130
  world_pos : vec3<f32>,
117
131
  normal : vec3<f32>,
118
132
  view_dir : vec3<f32>,
@@ -122,78 +136,101 @@ fn evaluate_cluster_light(
122
136
  f0 : vec3<f32>,
123
137
  flat_2d : bool,
124
138
  ) -> vec3<f32> {
125
- let kind = light.kind_and_shadow.x;
139
+ let kind = light.metadata.x;
126
140
  if (kind == KIND_POINT) {
127
141
  if (flat_2d) {
128
142
  return evalPointFlat(
129
- light.position.xyz, light.color.xyz, light.position.w,
143
+ light.position.xyz, light.colorTimesIntensity.xyz, light.position.w,
130
144
  world_pos, base_color,
131
145
  );
132
146
  }
133
147
  #ifdef POINT_SHADOW_AVAILABLE
134
- let layer = bitcast<i32>(light.kind_and_shadow.y);
148
+ let layer = bitcast<i32>(light.metadata.y);
135
149
  if (layer >= 0) {
150
+ let shadow = shadowParams[layer];
136
151
  return evalPointShadowed(
137
- light.position.xyz, light.color.xyz, light.position.w,
152
+ light.position.xyz, light.colorTimesIntensity.xyz, light.position.w,
138
153
  world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
139
154
  layer,
140
- bitcast<f32>(light.kind_and_shadow.z),
141
- bitcast<f32>(light.kind_and_shadow.w),
155
+ shadow.x,
156
+ shadow.y,
142
157
  0.005, 0.05,
143
158
  );
144
159
  }
145
160
  #endif
146
161
  return evalPoint(
147
- light.position.xyz, light.color.xyz, light.position.w,
162
+ light.position.xyz, light.colorTimesIntensity.xyz, light.position.w,
148
163
  world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
149
164
  );
150
165
  }
151
166
  if (kind == KIND_SPOT) {
152
167
  if (flat_2d) {
153
168
  return evalSpotFlat(
154
- light.position.xyz, light.direction.xyz, light.color.xyz,
155
- light.color.w, light.direction.w, light.position.w,
169
+ light.position.xyz, light.direction.xyz, light.colorTimesIntensity.xyz,
170
+ light.colorTimesIntensity.w, light.direction.w, light.position.w,
156
171
  world_pos, base_color,
157
172
  );
158
173
  }
159
- let encoded_tile = bitcast<i32>(light.kind_and_shadow.y);
160
- // The unshadowed sentinel is -1. A signed bitwise test against the
161
- // projector flag would misclassify that sentinel because -1 has every bit
162
- // set, so admit the flag only from the non-negative encoded domain.
174
+ let modifierFactors = spotModifierFactors(
175
+ light.position.xyz,
176
+ light.direction.xyz,
177
+ light.direction.w,
178
+ world_pos,
179
+ light.auxiliary.w,
180
+ light.metadata,
181
+ );
182
+ let modifier = vec3<f32>(modifierFactors.x) * modifierFactors.yzw;
183
+ let encoded_tile = bitcast<i32>(light.metadata.y);
184
+ // The optional projector marker shares the non-negative shadow identity
185
+ // lane. The direct-light packer keeps the lower tile bits intact.
186
+ var projector_selected = false;
187
+ var tile : i32 = encoded_tile;
163
188
  if (encoded_tile >= 0 && (encoded_tile & PROJECTOR_FLAG) != 0) {
164
- let tile = encoded_tile & TILE_MASK;
165
- if (tile < 4) {
166
- let projector = sampleStandardSpotProjector(view.spotLightViewProj[tile], world_pos);
167
- return evalSpotShadowed(
168
- light.position.xyz, light.direction.xyz, light.color.xyz,
169
- // LightSlot packs cosInner in color.w and cosOuter in direction.w;
170
- // keep the evaluator's named cone order intact at the decode boundary.
171
- light.color.w, light.direction.w, light.position.w,
172
- world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
173
- view.spotLightViewProj[tile], tile, 0.005, 0.05,
174
- bitcast<f32>(light.kind_and_shadow.w),
175
- bitcast<f32>(light.kind_and_shadow.z),
176
- ) * projector;
177
- }
178
- return vec3<f32>(0.0);
189
+ projector_selected = true;
190
+ tile = encoded_tile & TILE_MASK;
179
191
  }
180
- if (encoded_tile >= 0 && encoded_tile < 4) {
181
- let tile = encoded_tile;
192
+ if (tile >= 0 && tile < 4) {
182
193
  return evalSpotShadowed(
183
- light.position.xyz, light.direction.xyz, light.color.xyz,
184
- // LightSlot packs cosInner in color.w and cosOuter in direction.w;
194
+ light.position.xyz, light.direction.xyz, light.colorTimesIntensity.xyz,
195
+ // DirectLightSlot packs cosInner in color.w and cosOuter in direction.w;
185
196
  // keep the evaluator's named cone order intact at the decode boundary.
186
- light.color.w, light.direction.w, light.position.w,
187
- world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
197
+ light.colorTimesIntensity.w, light.direction.w, light.position.w,
198
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
188
199
  view.spotLightViewProj[tile], tile, 0.005, 0.05,
189
- bitcast<f32>(light.kind_and_shadow.w),
190
- bitcast<f32>(light.kind_and_shadow.z),
191
- );
200
+ 3.0,
201
+ 1.0,
202
+ ) * modifier
203
+ #ifdef PROJECTOR_AVAILABLE
204
+ * select(
205
+ vec3<f32>(1.0),
206
+ sampleStandardSpotProjector(view.spotLightViewProj[tile], world_pos, light.metadata),
207
+ projector_selected,
208
+ )
209
+ #endif
210
+ ;
192
211
  }
193
212
  return evalSpot(
194
- light.position.xyz, light.direction.xyz, light.color.xyz,
195
- light.color.w, light.direction.w, light.position.w,
213
+ light.position.xyz, light.direction.xyz, light.colorTimesIntensity.xyz,
214
+ light.colorTimesIntensity.w, light.direction.w, light.position.w,
196
215
  world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
216
+ ) * modifier;
217
+ }
218
+ if (kind == KIND_RECT_AREA) {
219
+ return evalRectAreaLtcGgx(
220
+ light.position.xyz,
221
+ light.colorTimesIntensity.xyz,
222
+ light.auxiliary.xyz,
223
+ light.direction.xyz,
224
+ light.colorTimesIntensity.w,
225
+ light.direction.w,
226
+ light.position.w,
227
+ world_pos,
228
+ normal,
229
+ view_dir,
230
+ base_color,
231
+ metallic,
232
+ alpha_sq,
233
+ f0,
197
234
  );
198
235
  }
199
236
  return vec3<f32>(0.0);
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  * no longer carry a bindingLayout sidecar (§Change stance: no v1/v2
18
18
  * dual-path, no incremental drop window).
19
19
  */
20
+ import type { MaterialShaderArtifactReceipt } from './material/artifact-types.js';
20
21
  export interface MaterialShaderManifestVariant {
21
22
  readonly definesKey: string;
22
23
  readonly defines: Record<string, boolean>;
@@ -33,7 +34,7 @@ export interface MaterialShaderManifestVariant {
33
34
  *
34
35
  * For Engine.create() internal use only; AI users never see this type.
35
36
  */
36
- export interface MaterialShaderManifestEntry {
37
+ export interface MaterialShaderManifestEntry extends Partial<MaterialShaderArtifactReceipt> {
37
38
  readonly identifier: string;
38
39
  readonly sourcePath: string;
39
40
  readonly composedWgsl: string;
@@ -1,6 +1,7 @@
1
1
  #define_import_path forgeax_view::volume_inject
2
2
  #import forgeax_pbr::shadow_pcf::{sample_shadow_2d_kernel}
3
3
  #import forgeax_pbr::lighting_attenuation::{projectSpotUv}
4
+ #import forgeax_view::common::{DirectLightSlot}
4
5
  // forgeax_pbr::lighting_punctual exports the surface-equivalent
5
6
  // evalVolumePoint and evalVolumeSpot contract; this stage consumes their
6
7
  // shared projector and shadow visibility facts without a second light owner.
@@ -29,41 +30,18 @@ struct VolumeParams {
29
30
  optics : vec4<f32>,
30
31
  };
31
32
 
32
- struct SpotLight {
33
- position : vec3<f32>,
34
- invRangeSquared : f32,
35
- colorTimesIntensity : vec3<f32>,
36
- cosInner : f32,
37
- direction : vec3<f32>,
38
- cosOuter : f32,
39
- depthBias : f32,
40
- normalBias : f32,
41
- pcfKernelSize : f32,
42
- shadowAtlasTile : i32,
43
- shadowIntensity : f32,
44
- };
45
-
46
- struct SpotLightsArray {
47
- count : u32,
48
- slots : array<SpotLight, 4>,
49
- };
50
-
51
- struct PointLight {
52
- position : vec3<f32>,
53
- invRangeSquared : f32,
54
- colorTimesIntensity : vec3<f32>,
55
- shadowAtlasLayer : i32,
56
- };
33
+ const DIRECT_LIGHT_METADATA_SENTINEL : u32 = 0xffffffffu;
34
+ const DIRECT_LIGHT_TILE_MASK : u32 = 0x3fffffffu;
57
35
 
58
- struct PointLightsArray {
59
- count : u32,
60
- slots : array<PointLight, 4>,
36
+ struct ClusterUniform {
37
+ grid : vec4<u32>,
38
+ near_far_log : vec4<f32>,
61
39
  };
62
40
 
63
41
  @group(0) @binding(5) var<uniform> volume_params : VolumeParams;
64
42
  @group(0) @binding(6) var volume_froxel : texture_storage_2d_array<rgba8unorm, write>;
65
- @group(0) @binding(7) var<storage, read> spotLightsBuffer : SpotLightsArray;
66
- @group(0) @binding(8) var<storage, read> pointLightsBuffer : PointLightsArray;
43
+ @group(0) @binding(7) var<storage, read> light_data : array<DirectLightSlot, 256>;
44
+ @group(0) @binding(8) var<uniform> cluster_uniform : ClusterUniform;
67
45
  @group(0) @binding(0) var<uniform> view : View;
68
46
 
69
47
  fn reconstruct_world(uv : vec2<f32>, depth : f32) -> vec3<f32> {
@@ -161,12 +139,32 @@ fn volume_cascade(world_position : vec3<f32>, view_depth : f32) -> f32 {
161
139
  );
162
140
  }
163
141
 
142
+ fn shadowAtlasTile(light : DirectLightSlot) -> i32 {
143
+ let encoded = light.metadata.y;
144
+ if (encoded == DIRECT_LIGHT_METADATA_SENTINEL) { return -1; }
145
+ // Projector-selected spots carry the marker in bit 30 and retain the
146
+ // selected shadow tile in the lower 30 bits. A projector-only spot uses
147
+ // tile zero, matching the surface projector contract.
148
+ let tile = encoded & DIRECT_LIGHT_TILE_MASK;
149
+ if (tile >= 4u) { return -1; }
150
+ return i32(tile);
151
+ }
152
+
153
+ fn selected_cluster_light_slot(value : f32) -> i32 {
154
+ let slot = i32(round(value));
155
+ if (slot < 0) { return -1; }
156
+ let index = u32(slot);
157
+ if (index >= cluster_uniform.grid.w || index >= 256u) { return -1; }
158
+ return slot;
159
+ }
160
+
164
161
  fn spot_shadow_visibility_at(world_position : vec3<f32>) -> f32 {
165
- let slot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
166
- if (slot >= spotLightsBuffer.count) { return 0.0; }
167
- let light = spotLightsBuffer.slots[slot];
168
- if (light.shadowAtlasTile < 0) { return 1.0; }
169
- let tile = u32(light.shadowAtlasTile);
162
+ let slot = selected_cluster_light_slot(volume_params.light_color.w);
163
+ if (slot < 0) { return 0.0; }
164
+ let light = light_data[u32(slot)];
165
+ let encodedTile = shadowAtlasTile(light);
166
+ if (encodedTile < 0) { return 1.0; }
167
+ let tile = u32(encodedTile);
170
168
  let projectorUv = projectSpotUv(view.spotLightViewProj[tile], world_position);
171
169
  // The accepted projector tuple is sampled with textureSampleLevel by the
172
170
  // surface and volume owners when a projector texture is present.
@@ -186,7 +184,7 @@ fn spot_shadow_visibility_at(world_position : vec3<f32>) -> f32 {
186
184
  let shadow_size = vec2<f32>(textureDimensions(shadowMap));
187
185
  return sample_shadow_2d_kernel(
188
186
  shadowMap, shadowSampler, atlas_uv, 1.0 / shadow_size, projected.z,
189
- light.normalBias, light.depthBias, 1.0, light.pcfKernelSize,
187
+ light.auxiliary.y, light.auxiliary.x, 1.0, 3.0,
190
188
  );
191
189
  }
192
190