@forgeax/engine-shader 0.1.28 → 0.1.29

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 (73) hide show
  1. package/NOTICE +35 -0
  2. package/README.md +39 -0
  3. package/dist/ShaderRegistry.d.ts +8 -0
  4. package/dist/ShaderRegistry.d.ts.map +1 -1
  5. package/dist/index.d.ts +6 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.mjs +312 -71
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/material/artifact-types.d.ts +12 -2
  10. package/dist/material/artifact-types.d.ts.map +1 -1
  11. package/dist/material-schemas.d.ts +5 -0
  12. package/dist/material-schemas.d.ts.map +1 -1
  13. package/package.json +5 -4
  14. package/src/ShaderRegistry.ts +10 -0
  15. package/src/__tests__/auto-exposure-graph.unit.test.ts +59 -0
  16. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -15
  17. package/src/__tests__/builtin-texture-sampling-contract.test.ts +1 -1
  18. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +2 -1
  19. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +167 -6
  20. package/src/__tests__/direct-light-layout.unit.test.ts +1 -1
  21. package/src/__tests__/lighting-punctual.unit.test.ts +29 -1
  22. package/src/__tests__/ltc-provenance.unit.test.ts +13 -2
  23. package/src/__tests__/material-builtins.unit.test.ts +1 -0
  24. package/src/__tests__/material-contract.unit.test.ts +42 -9
  25. package/src/__tests__/material-derived-builtins.integration.test.ts +18 -2
  26. package/src/__tests__/probe-lighting-composition.unit.test.ts +1 -1
  27. package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -2
  28. package/src/__tests__/scene-temporal.unit.test.ts +10 -0
  29. package/src/__tests__/shader.unit.test.ts +1 -0
  30. package/src/__tests__/sprite-variants.unit.test.ts +1 -0
  31. package/src/__tests__/ssr-artifact.unit.test.ts +141 -0
  32. package/src/__tests__/ssr-bgl.integration.test.ts +185 -0
  33. package/src/__tests__/standard-output-domain.unit.test.ts +36 -0
  34. package/src/__tests__/standard-pbr-artifact-receipt.unit.test.ts +93 -0
  35. package/src/__tests__/standard-surface-pass-evaluation.integration.test.ts +16 -4
  36. package/src/__tests__/surface-v1.unit.test.ts +6 -0
  37. package/src/__tests__/taa-resolve.unit.test.ts +20 -9
  38. package/src/__tests__/transmission-thickness-scale.unit.test.ts +6 -1
  39. package/src/__tests__/transparent-pbr.unit.test.ts +1 -1
  40. package/src/__tests__/vertex-color-variant.unit.test.ts +4 -1
  41. package/src/atmosphere-background.wgsl +3 -3
  42. package/src/auto-exposure-meter.wgsl +179 -0
  43. package/src/color-lut.wgsl +17 -0
  44. package/src/common.wgsl +7 -5
  45. package/src/default-standard-pbr-skin.wgsl +142 -40
  46. package/src/default-standard-pbr.wgsl +198 -82
  47. package/src/default_standard_surface.wgsl +56 -19
  48. package/src/fxaa.wgsl +9 -5
  49. package/src/ibl-sampling.wgsl +30 -6
  50. package/src/index.ts +189 -0
  51. package/src/lighting-punctual.wgsl +5 -6
  52. package/src/material/artifact-types.ts +109 -63
  53. package/src/material-schemas.ts +84 -8
  54. package/src/output-encoding.wgsl +10 -0
  55. package/src/pbr-temporal.wgsl +9 -1
  56. package/src/scene-temporal.wgsl +2 -0
  57. package/src/shadow-pcf.wgsl +6 -8
  58. package/src/shadow-surface.wgsl +16 -0
  59. package/src/shadow_caster.wgsl +99 -61
  60. package/src/sprite-lit.wgsl +4 -4
  61. package/src/sprite.wgsl +3 -3
  62. package/src/ssr-compose.wgsl +129 -0
  63. package/src/ssr-hiz-reduce.wgsl +53 -0
  64. package/src/ssr-hiz.wgsl +66 -0
  65. package/src/ssr-temporal.wgsl +305 -0
  66. package/src/ssr-trace.wgsl +784 -0
  67. package/src/standard-cluster.wgsl +6 -4
  68. package/src/standard-surface.wgsl +696 -0
  69. package/src/surface_v1.wgsl +6 -0
  70. package/src/taa-resolve.wgsl +222 -28
  71. package/src/tbn.wgsl +1 -1
  72. package/src/tonemap.wgsl +37 -18
  73. package/src/unlit.wgsl +3 -3
@@ -1,4 +1,11 @@
1
- import type { BindGroupLayoutDescriptor } from '@forgeax/engine-types';
1
+ import {
2
+ type BindGroupLayoutDescriptor,
3
+ derive,
4
+ type MaterialParticleInput,
5
+ type ParamSchemaEntry,
6
+ STANDARD_MATERIAL_PARAM_SCHEMA,
7
+ STANDARD_PHYSICAL_TEXTURE_FIELDS,
8
+ } from '@forgeax/engine-types';
2
9
 
3
10
  export interface MaterialShaderResourceSlot {
4
11
  readonly name: string;
@@ -49,44 +56,59 @@ export interface MaterialShaderArtifact {
49
56
  readonly bindings: readonly BindGroupLayoutDescriptor[];
50
57
  readonly deps: readonly string[];
51
58
  readonly vertexInputs: readonly Readonly<Record<string, unknown>>[];
59
+ /** Reflection-owned dynamic values supplied by a VFX renderer. */
60
+ readonly particleInputs?: readonly MaterialParticleInput[];
52
61
  readonly specializationKey?: string;
53
62
  /** Producer-owned ABI facts shared by direct and scene-index consumers. */
54
63
  readonly receipt?: MaterialShaderArtifactReceipt;
55
64
  }
56
65
 
57
- const STANDARD_PBR_TEXTURES = [
58
- 'baseColorTexture',
59
- 'metallicRoughnessTexture',
60
- 'normalTexture',
61
- 'specularColorTexture',
62
- 'emissiveTexture',
63
- 'occlusionTexture',
64
- 'transmissionTexture',
65
- 'thicknessTexture',
66
- ] as const;
66
+ /**
67
+ * Canonical Standard material schema consumed by the scene-index row.
68
+ * Physical texture handles stay in the fixed injected range (26..45); their
69
+ * numeric factors remain in this row, after the seven stable user texture
70
+ * coordinate pairs. Keeping this projection beside the receipt lets the
71
+ * shader producer and the render-side table consume one schema identity.
72
+ */
73
+ export const STANDARD_PIPELINE_PARAM_SCHEMA: readonly ParamSchemaEntry[] =
74
+ STANDARD_MATERIAL_PARAM_SCHEMA.filter(
75
+ (entry) =>
76
+ !(
77
+ entry.type.startsWith('texture') &&
78
+ STANDARD_PHYSICAL_TEXTURE_FIELDS.includes(
79
+ entry.name as (typeof STANDARD_PHYSICAL_TEXTURE_FIELDS)[number],
80
+ )
81
+ ),
82
+ );
83
+
84
+ const STANDARD_PIPELINE_DERIVED = derive(STANDARD_PIPELINE_PARAM_SCHEMA);
67
85
 
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
- 'specularColor',
83
- 'normalScale',
84
- 'transmission',
85
- 'ior',
86
- 'thickness',
87
- 'attenuationColor',
88
- 'attenuationDistance',
89
- ] as const;
86
+ function isNumericParam(entry: ParamSchemaEntry): boolean {
87
+ return (
88
+ entry.type === 'f32' ||
89
+ entry.type === 'i32' ||
90
+ entry.type === 'u32' ||
91
+ entry.type === 'vec2' ||
92
+ entry.type === 'vec3' ||
93
+ entry.type === 'vec4' ||
94
+ entry.type === 'color'
95
+ );
96
+ }
97
+
98
+ function standardPbrMaterialRowFields(): readonly string[] {
99
+ const coordinates = new Map(
100
+ STANDARD_PIPELINE_DERIVED.coordinateRecords.map((record) => [record.parameter, record]),
101
+ );
102
+ return STANDARD_PIPELINE_PARAM_SCHEMA.flatMap((entry) => {
103
+ if (isNumericParam(entry)) return [entry.name];
104
+ if (entry.type.startsWith('texture')) {
105
+ const record = coordinates.get(entry.name);
106
+ if (record === undefined) throw new Error(`missing coordinate record for ${entry.name}`);
107
+ return [record.transformMember, record.metadataMember];
108
+ }
109
+ return [];
110
+ });
111
+ }
90
112
 
91
113
  const STANDARD_PBR_VERTEX_INPUTS: readonly MaterialShaderVertexInput[] = [
92
114
  { semantic: 'position', location: 0, format: 'float32x3' },
@@ -96,22 +118,18 @@ const STANDARD_PBR_VERTEX_INPUTS: readonly MaterialShaderVertexInput[] = [
96
118
  ];
97
119
 
98
120
  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
- ]);
121
+ return STANDARD_PIPELINE_DERIVED.resourceBindings.flatMap((resource) => {
122
+ if (resource.kind !== 'sampler' && resource.kind !== 'texture') return [];
123
+ return [
124
+ {
125
+ name: resource.name,
126
+ parameter: resource.parameter ?? resource.name,
127
+ kind: resource.kind,
128
+ group: 1,
129
+ binding: resource.binding,
130
+ },
131
+ ];
132
+ });
115
133
  }
116
134
 
117
135
  /**
@@ -119,38 +137,65 @@ function standardPbrResourceSlots(): MaterialShaderResourceSlot[] {
119
137
  * The skinned module uses the same material producer and only adds its palette
120
138
  * address; it must retain the same receipt identity for material compatibility.
121
139
  */
122
- export function createStandardPbrArtifactReceipt(skinned = false): MaterialShaderArtifactReceipt {
140
+ export function createStandardPbrArtifactReceipt(
141
+ skinned = false,
142
+ vertexColorAvailable = false,
143
+ ): MaterialShaderArtifactReceipt {
123
144
  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];
145
+ const uvSets = STANDARD_PIPELINE_DERIVED.coordinateRecords.map((record) => ({
146
+ parameter: record.parameter,
147
+ set: 0,
148
+ }));
149
+ const vertexInputs = [
150
+ ...STANDARD_PBR_VERTEX_INPUTS,
151
+ ...(skinned
152
+ ? [
153
+ { semantic: 'skinIndex', location: 4, format: 'uint16x4' },
154
+ { semantic: 'skinWeight', location: 5, format: 'float32x4' },
155
+ ]
156
+ : []),
157
+ ...(vertexColorAvailable ? [{ semantic: 'color', location: 13, format: 'float32x4' }] : []),
158
+ ];
159
+ const layoutIdentity = `standard-pbr/material-row-v2${vertexColorAvailable ? '/vertex-color' : ''}`;
132
160
  return {
133
161
  directEntry: 'vs_main',
134
162
  sceneIndexEntry: 'vs_scene_index',
135
- materialRow: { byteLength: 384, fields: [...STANDARD_PBR_NUMERIC_FIELDS] },
163
+ materialRow: {
164
+ byteLength: STANDARD_PIPELINE_DERIVED.totalBytes,
165
+ fields: [...standardPbrMaterialRowFields()],
166
+ },
136
167
  resourceSlots,
137
168
  uvSets,
138
169
  vertexInputs,
139
170
  alphaMask: { cutoff: 'alphaCutoff', source: 'baseColor.a' },
140
171
  ...(skinned ? { skinPaletteAddress: { group: 2, binding: 1, stride: 64 } } : {}),
141
172
  reflection: {
142
- layoutIdentity: 'standard-pbr/material-row-v1',
173
+ layoutIdentity,
143
174
  resourceSlots,
144
175
  vertexInputs,
145
176
  },
146
- receiptIdentity: 'standard-pbr/material-row-v1',
147
- generation: 1,
177
+ receiptIdentity: layoutIdentity,
178
+ generation: 2,
148
179
  };
149
180
  }
150
181
 
151
182
  export function isMaterialShaderArtifact(value: unknown): value is MaterialShaderArtifact {
152
183
  if (value === null || typeof value !== 'object') return false;
153
184
  const artifact = value as Partial<MaterialShaderArtifact>;
185
+ const particleInputs = artifact.particleInputs;
186
+ const particleInputsValid =
187
+ particleInputs === undefined ||
188
+ (Array.isArray(particleInputs) &&
189
+ particleInputs.every(
190
+ (input) =>
191
+ input !== null &&
192
+ typeof input === 'object' &&
193
+ typeof input.name === 'string' &&
194
+ typeof input.type === 'string' &&
195
+ typeof input.visibility === 'string' &&
196
+ Number.isInteger(input.lane) &&
197
+ input.lane >= 0,
198
+ ));
154
199
  return (
155
200
  typeof artifact.material === 'string' &&
156
201
  typeof artifact.pass === 'string' &&
@@ -158,6 +203,7 @@ export function isMaterialShaderArtifact(value: unknown): value is MaterialShade
158
203
  typeof artifact.layoutIdentity === 'string' &&
159
204
  Array.isArray(artifact.bindings) &&
160
205
  Array.isArray(artifact.deps) &&
161
- Array.isArray(artifact.vertexInputs)
206
+ Array.isArray(artifact.vertexInputs) &&
207
+ particleInputsValid
162
208
  );
163
209
  }
@@ -4,6 +4,7 @@ import {
4
4
  STANDARD_PHYSICAL_PARAMETER_NAMES,
5
5
  } from '@forgeax/engine-types';
6
6
  import {
7
+ STANDARD_PIPELINE_PARAM_SCHEMA as CANONICAL_STANDARD_PIPELINE_PARAM_SCHEMA,
7
8
  createStandardPbrArtifactReceipt,
8
9
  type MaterialShaderArtifactReceipt,
9
10
  } from './material/artifact-types.js';
@@ -13,6 +14,24 @@ export const STANDARD_PBR_ALPHA_CUTOFF_DEFAULT = 0;
13
14
  /** Shared material contract for the standard PBR and skinned PBR shaders. */
14
15
  export const DEFAULT_STANDARD_PBR_PARAM_SCHEMA = STANDARD_MATERIAL_PARAM_SCHEMA;
15
16
 
17
+ /** Numeric WGSL override ID, stable across Naga symbol mangling. */
18
+ export const STANDARD_TEXTURE_MASK_OVERRIDE = '64000';
19
+
20
+ const standardTextureBits: ReadonlyMap<string, number> = new Map(
21
+ DEFAULT_STANDARD_PBR_PARAM_SCHEMA.filter((entry) => entry.type === 'texture2d').map(
22
+ (entry, index) => [entry.name, 2 ** index],
23
+ ),
24
+ );
25
+
26
+ /** Presence follows the authored contract, never asynchronous GPU residency. */
27
+ export function standardTextureMask(schema: readonly ParamSchemaEntry[]): number {
28
+ let mask = 0;
29
+ for (const entry of schema) {
30
+ if (entry.type === 'texture2d') mask |= standardTextureBits.get(entry.name) ?? 0;
31
+ }
32
+ return mask;
33
+ }
34
+
16
35
  export type { StandardPhysicalTextureField } from '@forgeax/engine-types';
17
36
  /**
18
37
  * Backward-compatible export name for callers that need the physical
@@ -64,14 +83,71 @@ export const STANDARD_BASE_PARAM_SCHEMA: readonly ParamSchemaEntry[] =
64
83
  * physical texture pairs are never included here and are appended only for a
65
84
  * root contract that declares them.
66
85
  */
67
- export const STANDARD_PIPELINE_PARAM_SCHEMA: readonly ParamSchemaEntry[] =
68
- DEFAULT_STANDARD_PBR_PARAM_SCHEMA.filter(
69
- // The canonical Standard UBO keeps every numeric root coordinate,
70
- // including the physical-layer tail. Physical map pairs are engine-owned
71
- // resources and are injected after the stable user region.
72
- (entry) =>
73
- !(STANDARD_PHYSICAL_PARAMETER_NAMES.has(entry.name) && entry.type.startsWith('texture')),
74
- );
86
+ // Keep the runtime schema export on the receipt producer's canonical
87
+ // projection. The shader artifact, GPU Scene row, and material bind-group
88
+ // builder must not each filter the Standard root independently.
89
+ export const STANDARD_PIPELINE_PARAM_SCHEMA = CANONICAL_STANDARD_PIPELINE_PARAM_SCHEMA;
90
+
91
+ /**
92
+ * Fixed material contract consumed by the VFX mesh adapters.
93
+ *
94
+ * `standard-surface.wgsl` is an authored surface rather than the generated
95
+ * Standard root, so its Material block must be projected explicitly when a
96
+ * particle shader is published. Keep the projection derived from the
97
+ * canonical Standard vocabulary wherever possible; only the surface-owned
98
+ * `specularTint` scalar and texture alias are local to this contract. The
99
+ * order is the WGSL ABI: numeric fields first (384 B), followed by eight
100
+ * sampler/texture pairs (bindings 1..16), then the shared IBL injection at
101
+ * bindings 17..23.
102
+ */
103
+ const PARTICLE_SURFACE_NUMERIC_FIELDS = [
104
+ 'baseColor',
105
+ 'metallic',
106
+ 'roughness',
107
+ 'metallicChannel',
108
+ 'roughnessChannel',
109
+ 'aoChannel',
110
+ 'extraChannel',
111
+ 'emissive',
112
+ 'emissiveIntensity',
113
+ 'occlusionStrength',
114
+ 'alphaCutoff',
115
+ 'clearcoat',
116
+ 'clearcoatRoughness',
117
+ 'specularTint',
118
+ 'normalScale',
119
+ 'transmission',
120
+ 'ior',
121
+ 'thickness',
122
+ 'attenuationColor',
123
+ 'attenuationDistance',
124
+ ] as const;
125
+
126
+ const PARTICLE_SURFACE_TEXTURE_FIELDS = [
127
+ 'baseColorTexture',
128
+ 'metallicRoughnessTexture',
129
+ 'normalTexture',
130
+ 'specularTintTexture',
131
+ 'emissiveTexture',
132
+ 'occlusionTexture',
133
+ 'transmissionTexture',
134
+ 'thicknessTexture',
135
+ ] as const;
136
+
137
+ function particleSurfaceSchemaEntry(name: string): ParamSchemaEntry {
138
+ const entry = DEFAULT_STANDARD_PBR_PARAM_SCHEMA.find((candidate) => candidate.name === name);
139
+ if (entry !== undefined) return entry;
140
+ if (name === 'specularTint') {
141
+ return { name, type: 'vec3', colorSpace: 'srgb', default: [1, 1, 1] };
142
+ }
143
+ if (name === 'specularTintTexture') return { name, type: 'texture2d' };
144
+ throw new Error(`Particle Standard Surface schema field is missing: ${name}`);
145
+ }
146
+
147
+ export const PARTICLE_MESH_SURFACE_PARAM_SCHEMA: readonly ParamSchemaEntry[] = Object.freeze([
148
+ ...PARTICLE_SURFACE_NUMERIC_FIELDS.map(particleSurfaceSchemaEntry),
149
+ ...PARTICLE_SURFACE_TEXTURE_FIELDS.map(particleSurfaceSchemaEntry),
150
+ ]);
75
151
 
76
152
  /** The single producer receipt shared by direct and scene-index Standard PBR. */
77
153
  export const STANDARD_PBR_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt =
@@ -0,0 +1,10 @@
1
+ #define_import_path forgeax_view::output_encoding
2
+
3
+ // The single explicit output encoding owner for raw surface writes.
4
+ #import forgeax_view::common::{linearToSrgbOetf}
5
+
6
+ // Convert linear-LDR RGB once and preserve the source alpha channel exactly.
7
+ fn encodeOutput(linearColor : vec3<f32>, alpha : f32) -> vec4<f32> {
8
+ let encoded = linearToSrgbOetf(linearColor);
9
+ return vec4<f32>(encoded, alpha);
10
+ }
@@ -43,10 +43,13 @@ fn resolvePbrTemporalReactive(
43
43
  fn projectPbrSceneTemporal(
44
44
  baseColorAlpha : f32,
45
45
  alphaCutoff : f32,
46
+ #ifdef BASE_COLOR_TEXTURE_AVAILABLE
47
+ baseColorTextureEnabled : bool,
46
48
  baseColorTexture : texture_2d<f32>,
47
49
  baseColorSampler : sampler,
48
50
  transform : vec4<f32>,
49
51
  metadata : vec4<f32>,
52
+ #endif
50
53
  currentClip : vec4<f32>,
51
54
  previousClip : vec4<f32>,
52
55
  temporalProjection : vec4<f32>,
@@ -60,6 +63,9 @@ fn projectPbrSceneTemporal(
60
63
  uv6 : vec2<f32>,
61
64
  uv7 : vec2<f32>,
62
65
  ) -> vec4<f32> {
66
+ var baseSample = vec4<f32>(1.0);
67
+ #ifdef BASE_COLOR_TEXTURE_AVAILABLE
68
+ if (baseColorTextureEnabled) {
63
69
  let baseUv = transformedPbrTemporalUv(
64
70
  transform,
65
71
  metadata,
@@ -72,12 +78,14 @@ fn projectPbrSceneTemporal(
72
78
  uv6,
73
79
  uv7,
74
80
  );
75
- let baseSample = sampleMaterialTexture(
81
+ baseSample = sampleMaterialTexture(
76
82
  baseColorTexture,
77
83
  baseColorSampler,
78
84
  baseUv,
79
85
  metadata.zw,
80
86
  );
87
+ }
88
+ #endif
81
89
  if (alphaCutoff > 0.0 && baseColorAlpha * baseSample.a <= alphaCutoff) {
82
90
  discard;
83
91
  }
@@ -2,6 +2,8 @@
2
2
 
3
3
  // The sole shader interpretation of forgeax::scene-data::temporal-v1.
4
4
  // The packed target is rgba16float at render resolution and is sampled only.
5
+ // Coverage uses the main (possibly jittered) raster/depth lattice. Motion UV
6
+ // values themselves exclude jitter; an unjittered consumer offsets its read.
5
7
 
6
8
  const SCENE_DATA_TEMPORAL_V1_CLEAR : vec4<f32> = vec4<f32>(0.0, 0.0, -1.0, 1.0);
7
9
 
@@ -142,9 +142,9 @@ fn sample_shadow_2d_kernel(
142
142
  // `shadowAtlas` is @group(0) @binding(5) texture_depth_cube_array.
143
143
  // `shadowSampler` is @group(0) @binding(4) sampler_comparison (shared with
144
144
  // directional shadows — same sampler type, no dimension distinction).
145
- // `lightLocal` is the light-to-fragment direction vector in the cubemap's
146
- // coordinate system (Bevy convention: left-handed cubemap; caller applies
147
- // flip_z = vec3(1,1,-1) to convert right-hand world to left-hand cubemap).
145
+ // `lightLocal` is the raw light-to-fragment direction vector in the cubemap's
146
+ // coordinate system. evalPointShadowed and buildPointShadowMatrices share the
147
+ // CubeCamera face directions and clip-space-X reflection convention.
148
148
  // `layer` is the cube_array layer index (i32; 0..3 for 4 shadow-casting
149
149
  // point lights; sentinel -1 must be gated upstream — this function assumes
150
150
  // a valid layer).
@@ -170,11 +170,9 @@ fn sample_shadow_cube_hw2x2(
170
170
  depthBias : f32,
171
171
  nDotL : f32,
172
172
  ) -> f32 {
173
- // Slope-scaled bias: same formula as 2D path, shared SSOT. feat-20260621 M3 /
174
- // m3-t5: param names aligned to D-1 (normalBias = slope coefficient,
175
- // depthBias = floor). Pure rename -- each positional arg keeps its prior role.
176
- let bias = max(normalBias * (1.0 - nDotL), depthBias / 1000.0);
177
- let adjustedDepth = depthRef - bias;
173
+ // The public depthBias is a normalized depth floor, as for the directional
174
+ // receiver. Do not shrink it by 1000: that leaves front-lit faces uncovered.
175
+ let adjustedDepth = shadow_biased_receiver_depth(depthRef, normalBias, depthBias, nDotL);
178
176
 
179
177
  // Single textureSampleCompareLevel call: the comparison sampler resolves a
180
178
  // 2x2 PCF neighborhood at the silicon level; clamp-to-edge address mode
@@ -0,0 +1,16 @@
1
+ #define_import_path forgeax_shadow::surface
2
+ #import forgeax_view::common::{View, ShadowCasterCascade, view, shadowCasterCascade}
3
+
4
+ // All geometry producers use the shadow owner's selected light view. Point
5
+ // faces supply their face view through View; spots use the caster uniform.
6
+ fn projectShadowPosition(worldPosition: vec4<f32>) -> vec4<f32> {
7
+ if (shadowCasterCascade.isSpot == 1u) {
8
+ return shadowCasterCascade.spotLightViewProj * worldPosition;
9
+ }
10
+ switch shadowCasterCascade.index {
11
+ case 0u: { return view.lightViewProj_A * worldPosition; }
12
+ case 1u: { return view.lightViewProj_B * worldPosition; }
13
+ case 2u: { return view.lightViewProj_C * worldPosition; }
14
+ default: { return view.lightViewProj_D * worldPosition; }
15
+ }
16
+ }