@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
@@ -0,0 +1,26 @@
1
+ #define_import_path forgeax_pbr::anisotropy
2
+
3
+ // Engine-owned anisotropy helpers. The root contract supplies strength and
4
+ // rotation; the map (when present) supplies tangent direction in RG and a
5
+ // strength multiplier in B. Keeping the frame operation here means the
6
+ // Standard evaluator and custom Surface share one deterministic seam.
7
+ fn evaluateAnisotropicNormal(
8
+ normal : vec3<f32>,
9
+ tangent : vec4<f32>,
10
+ strength : f32,
11
+ rotation : f32,
12
+ direction : vec2<f32>,
13
+ ) -> vec3<f32> {
14
+ let frameTangent = normalize(tangent.xyz);
15
+ let handedBitangent = normalize(cross(normal, frameTangent)) * select(-1.0, 1.0, tangent.w >= 0.0);
16
+ var mapDirection = vec2<f32>(1.0, 0.0);
17
+ if (dot(direction, direction) >= 1e-6) {
18
+ mapDirection = normalize(direction);
19
+ }
20
+ let mappedTangent = normalize(frameTangent * mapDirection.x + handedBitangent * mapDirection.y);
21
+ let mappedBitangent = normalize(cross(normal, mappedTangent)) * select(-1.0, 1.0, tangent.w >= 0.0);
22
+ let axis = normalize(mappedTangent * cos(rotation) + mappedBitangent * sin(rotation));
23
+ // A bounded bent normal preserves the zero-strength isotropic path while
24
+ // moving only the base specular highlight for non-zero anisotropy.
25
+ return normalize(normal + axis * clamp(strength, -0.999, 0.999) * 0.35);
26
+ }
@@ -0,0 +1,24 @@
1
+ #define_import_path forgeax_pbr::clearcoat
2
+
3
+ // Clearcoat is the terminal physical layer. Its Fresnel term owns both the
4
+ // attenuation of lower layers and the weight of the coat radiance.
5
+ // The root supplies clearcoatRoughness and clearcoatNormalScale after their
6
+ // texture projections; this module deliberately owns only layer energy.
7
+ fn evaluateClearcoatFresnel(viewCosine : f32, clearcoatFactor : f32) -> f32 {
8
+ let cosine = clamp(viewCosine, 0.0, 1.0);
9
+ let oneMinusCosine = 1.0 - cosine;
10
+ let dielectricFresnel = 0.04 + 0.96 * oneMinusCosine * oneMinusCosine *
11
+ oneMinusCosine * oneMinusCosine * oneMinusCosine;
12
+ return clamp(clearcoatFactor, 0.0, 1.0) * dielectricFresnel;
13
+ }
14
+
15
+ fn evaluateClearcoatLayer(
16
+ baseRadiance : vec3<f32>,
17
+ coatRadiance : vec3<f32>,
18
+ viewCosine : f32,
19
+ clearcoatFactor : f32,
20
+ ) -> vec3<f32> {
21
+ let fresnel = evaluateClearcoatFresnel(viewCosine, clearcoatFactor);
22
+ let attenuatedBase = baseRadiance * (1.0 - fresnel);
23
+ return attenuatedBase + coatRadiance * fresnel;
24
+ }
@@ -0,0 +1,23 @@
1
+ #define_import_path forgeax_pbr::iridescence
2
+
3
+ // Bounded RGB thin-film Fresnel approximation. Thickness is deliberately
4
+ // not clamped to min/max order: the glTF contract permits inverted bounds and
5
+ // the caller supplies the authored interval verbatim.
6
+ fn evaluateIridescenceFresnel(
7
+ baseF0 : vec3<f32>,
8
+ strength : f32,
9
+ filmIor : f32,
10
+ thicknessNanometres : f32,
11
+ ) -> vec3<f32> {
12
+ let safeStrength = clamp(strength, 0.0, 1.0);
13
+ let safeIor = max(filmIor, 1.0);
14
+ let base = clamp((safeIor - 1.0) / (safeIor + 1.0), 0.0, 1.0);
15
+ let phase = thicknessNanometres * 0.018;
16
+ let spectral = vec3<f32>(
17
+ 0.5 + 0.5 * cos(phase),
18
+ 0.5 + 0.5 * cos(phase + 2.0943952),
19
+ 0.5 + 0.5 * cos(phase + 4.1887903),
20
+ );
21
+ let film = clamp(vec3<f32>(base) * spectral, vec3<f32>(0.0), vec3<f32>(1.0));
22
+ return clamp(mix(baseF0, film, safeStrength), vec3<f32>(0.0), vec3<f32>(1.0));
23
+ }
@@ -0,0 +1,17 @@
1
+ #define_import_path forgeax_pbr::sheen
2
+
3
+ // Charlie-like grazing lobe approximation with explicit energy compensation.
4
+ // It is evaluated below clearcoat, so the caller attenuates this result when
5
+ // the top coat Fresnel is applied.
6
+ fn evaluateSheenLayer(
7
+ baseRadiance : vec3<f32>,
8
+ sheenColor : vec3<f32>,
9
+ sheenRoughness : f32,
10
+ viewCosine : f32,
11
+ ) -> vec3<f32> {
12
+ let roughness = clamp(sheenRoughness, 0.04, 1.0);
13
+ let grazing = pow(1.0 - clamp(viewCosine, 0.0, 1.0), 2.0);
14
+ let lobe = grazing * (1.0 - 0.5 * roughness);
15
+ let energy = clamp(max(max(sheenColor.r, sheenColor.g), sheenColor.b), 0.0, 1.0);
16
+ return baseRadiance * (1.0 - energy * lobe * 0.5) + sheenColor * lobe;
17
+ }
@@ -0,0 +1,12 @@
1
+ #define_import_path forgeax_material::standard_physical_layer
2
+
3
+ // Engine-owned physical layer facts are selected from the root StandardLayerPlan.
4
+ // Surface modules only produce base SurfaceData and never own this interface.
5
+ // The Standard template owns the facts struct; this module supplies the one
6
+ // physical evaluator body when the clearcoat plan is present.
7
+ fn evaluate_standard_physical_layer() -> StandardPhysicalLayerFacts {
8
+ return StandardPhysicalLayerFacts(
9
+ clamp(material.clearcoat, 0.0, 1.0),
10
+ clamp(material.clearcoatRoughness, 0.04, 1.0),
11
+ );
12
+ }
@@ -1,4 +1,8 @@
1
1
  import type { ParamSchemaEntry } from '@forgeax/engine-types';
2
+ import {
3
+ STANDARD_MATERIAL_PARAM_SCHEMA,
4
+ STANDARD_PHYSICAL_PARAMETER_NAMES,
5
+ } from '@forgeax/engine-types';
2
6
  import {
3
7
  createStandardPbrArtifactReceipt,
4
8
  type MaterialShaderArtifactReceipt,
@@ -7,36 +11,67 @@ import {
7
11
  export const STANDARD_PBR_ALPHA_CUTOFF_DEFAULT = 0;
8
12
 
9
13
  /** Shared material contract for the standard PBR and skinned PBR shaders. */
10
- export const DEFAULT_STANDARD_PBR_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
11
- { name: 'baseColor', type: 'color', default: [1, 1, 1, 1] },
12
- { name: 'metallic', type: 'f32', default: 0 },
13
- { name: 'roughness', type: 'f32', default: 0.5 },
14
- { name: 'metallicChannel', type: 'f32', default: 2 },
15
- { name: 'roughnessChannel', type: 'f32', default: 1 },
16
- { name: 'aoChannel', type: 'f32', default: 0 },
17
- { name: 'extraChannel', type: 'f32', default: 0 },
18
- { name: 'emissive', type: 'vec3', colorSpace: 'srgb', default: [0, 0, 0] },
19
- { name: 'emissiveIntensity', type: 'f32', default: 0 },
20
- { name: 'occlusionStrength', type: 'f32', default: 1 },
21
- { name: 'alphaCutoff', type: 'f32', default: STANDARD_PBR_ALPHA_CUTOFF_DEFAULT },
22
- { name: 'clearcoat', type: 'f32', default: 0 },
23
- { name: 'clearcoatRoughness', type: 'f32', default: 0.5 },
24
- { name: 'specularTint', type: 'vec3', colorSpace: 'srgb', default: [1, 1, 1] },
25
- { name: 'normalScale', type: 'f32', default: 1 },
26
- { name: 'transmission', type: 'f32', default: 0 },
27
- { name: 'ior', type: 'f32', default: 1.5 },
28
- { name: 'thickness', type: 'f32', default: 0 },
29
- { name: 'attenuationColor', type: 'vec3', colorSpace: 'linear', default: [1, 1, 1] },
30
- { name: 'attenuationDistance', type: 'f32' },
31
- { name: 'baseColorTexture', type: 'texture2d' },
32
- { name: 'metallicRoughnessTexture', type: 'texture2d' },
33
- { name: 'normalTexture', type: 'texture2d' },
34
- { name: 'specularTintTexture', type: 'texture2d' },
35
- { name: 'emissiveTexture', type: 'texture2d' },
36
- { name: 'occlusionTexture', type: 'texture2d' },
37
- { name: 'transmissionTexture', type: 'texture2d' },
38
- { name: 'thicknessTexture', type: 'texture2d' },
39
- ];
14
+ export const DEFAULT_STANDARD_PBR_PARAM_SCHEMA = STANDARD_MATERIAL_PARAM_SCHEMA;
15
+
16
+ export type { StandardPhysicalTextureField } from '@forgeax/engine-types';
17
+ /**
18
+ * Backward-compatible export name for callers that need the physical
19
+ * projection. Every entry is selected from the Standard root schema above;
20
+ * no independent defaults or binding inventory can drift from it.
21
+ */
22
+ /**
23
+ * Stable physical texture injection order. The order is part of the
24
+ * Standard template's resource ABI and is derived from the root schema by
25
+ * `standardPhysicalTextureFields`; render/cook must not maintain another
26
+ * field inventory.
27
+ */
28
+ export {
29
+ STANDARD_PHYSICAL_TEXTURE_FIELDS,
30
+ standardPhysicalTextureFields,
31
+ } from '@forgeax/engine-types';
32
+
33
+ export const STANDARD_PHYSICAL_LAYER_PARAM_SCHEMA: readonly ParamSchemaEntry[] =
34
+ DEFAULT_STANDARD_PBR_PARAM_SCHEMA.filter((entry) =>
35
+ STANDARD_PHYSICAL_PARAMETER_NAMES.has(entry.name),
36
+ );
37
+
38
+ /**
39
+ * Canonical static Standard entry. It deliberately contains only the base
40
+ * contract (including the specular extension) so a base-only material keeps
41
+ * its Deferred path and carries no second-stage physical resources.
42
+ */
43
+ export const STANDARD_BASE_PARAM_SCHEMA: readonly ParamSchemaEntry[] =
44
+ DEFAULT_STANDARD_PBR_PARAM_SCHEMA.filter(
45
+ (entry) =>
46
+ !STANDARD_PHYSICAL_PARAMETER_NAMES.has(entry.name) &&
47
+ ![
48
+ 'transmission',
49
+ 'thickness',
50
+ 'attenuationColor',
51
+ 'attenuationDistance',
52
+ 'transmissionTexture',
53
+ 'thicknessTexture',
54
+ ].includes(entry.name),
55
+ );
56
+
57
+ /**
58
+ * Canonical boot-time user region for the shared Standard material BGL.
59
+ *
60
+ * It is still a projection of the root schema, but unlike the author-facing
61
+ * base-only projection it retains the pre-existing transmission texture pair.
62
+ * The template reserves those two pairs before the IBL/backdrop injection so
63
+ * a base-only shader and a transmissive shader can share one boot layout;
64
+ * physical texture pairs are never included here and are appended only for a
65
+ * root contract that declares them.
66
+ */
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
+ );
40
75
 
41
76
  /** The single producer receipt shared by direct and scene-index Standard PBR. */
42
77
  export const STANDARD_PBR_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt =
@@ -13,7 +13,7 @@
13
13
  // - R-10: paramSchema reuses default-standard-pbr schema (same params)
14
14
 
15
15
  import type { ShaderRegistry } from './index.js';
16
- import { DEFAULT_STANDARD_PBR_PARAM_SCHEMA } from './material-schemas.js';
16
+ import { STANDARD_PIPELINE_PARAM_SCHEMA } from './material-schemas.js';
17
17
 
18
18
  /**
19
19
  * paramSchema for forgeax::pbr-skin — mirrors default-standard-pbr 8 fields.
@@ -69,6 +69,6 @@ export function registerDefaultStandardPbrSkin(
69
69
  void caps;
70
70
  registry.installMaterialArtifact(RESERVED_ID, {
71
71
  source: composedWgsl,
72
- paramSchema: DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
72
+ paramSchema: STANDARD_PIPELINE_PARAM_SCHEMA,
73
73
  });
74
74
  }
@@ -1,10 +1,16 @@
1
1
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
2
2
  #pragma variant_axis SKINNING_DISABLED
3
+ #pragma material_slot surface
4
+ #define_import_path forgeax::default-shadow-caster
5
+ #import forgeax_material::slot::surface::{evaluate_surface}
6
+ #import forgeax_material::surface_v1::{SurfaceInput, SurfaceData}
7
+ #import forgeax_material::parameters::{material}
3
8
 
4
9
  // @forgeax/engine-shader shadow_caster.wgsl
5
10
  // feat-20260520-directional-light-shadow-mapping M1c / w9 (D-9 / AC-09):
6
- // vertex-only depth pass for directional shadow map. No fragment stage --
7
- // the GPU writes depth automatically from gl_Position.z (depth32float RT).
11
+ // depth pass for directional shadow map. The fragment stage evaluates the
12
+ // selected Standard Surface so opacity/alpha-clip stays identical to Forward
13
+ // and Deferred (depth32float is still the only render target).
8
14
  //
9
15
  // feat-20260613-csm-cascaded-shadow-maps M5 / w28: per-cascade
10
16
  // lightViewProj selection. Each cascade pass writes a different
@@ -29,12 +35,25 @@
29
35
 
30
36
  struct VsInput {
31
37
  @location(0) position : vec3<f32>,
38
+ @location(1) normal : vec3<f32>,
39
+ @location(2) uv : vec2<f32>,
40
+ @location(3) tangent : vec4<f32>,
32
41
  #if SKINNING_DISABLED == false
33
42
  @location(4) skinIndex : vec4<u32>,
34
43
  @location(5) skinWeight : vec4<f32>,
35
44
  #endif
36
45
  };
37
46
 
47
+ struct VsOut {
48
+ @builtin(position) clip : vec4<f32>,
49
+ @location(0) positionOS : vec3<f32>,
50
+ @location(1) positionWS : vec3<f32>,
51
+ @location(2) normalWS : vec3<f32>,
52
+ @location(3) tangentWS : vec4<f32>,
53
+ @location(4) surfaceUv : vec2<f32>,
54
+ @location(5) vertexColor : vec4<f32>,
55
+ };
56
+
38
57
  #if SKINNING_DISABLED == false
39
58
  #if STORAGE_BUFFER_AVAILABLE == true
40
59
  @group(2) @binding(1) var<storage, read> palette : array<mat4x4<f32>>;
@@ -53,7 +72,7 @@ fn _cascadeLightViewProj(layer : u32) -> mat4x4<f32> {
53
72
  }
54
73
 
55
74
  @vertex
56
- fn vs_main(in : VsInput, @builtin(instance_index) idx : u32) -> @builtin(position) vec4<f32> {
75
+ fn vs_main(in : VsInput, @builtin(instance_index) idx : u32) -> VsOut {
57
76
  #if SKINNING_DISABLED == false
58
77
  let skinMatrix = palette[in.skinIndex.x] * in.skinWeight.x +
59
78
  palette[in.skinIndex.y] * in.skinWeight.y +
@@ -63,8 +82,14 @@ fn vs_main(in : VsInput, @builtin(instance_index) idx : u32) -> @builtin(positio
63
82
  // produce world-space positions. Applying meshes[0].worldFromLocal again
64
83
  // would double-transform a parented skinned entity.
65
84
  let worldPos = skinMatrix * vec4<f32>(in.position, 1.0);
85
+ let worldNormal = normalize((skinMatrix * vec4<f32>(in.normal, 0.0)).xyz);
86
+ let worldTangent = normalize((skinMatrix * vec4<f32>(in.tangent.xyz, 0.0)).xyz);
66
87
  #else
67
- let worldPos = meshes[0].worldFromLocal * instances[idx].localFromInstance * vec4<f32>(in.position, 1.0);
88
+ let instanceLocal = instances[idx].localFromInstance;
89
+ let worldMatrix = meshes[0].worldFromLocal * instanceLocal;
90
+ let worldPos = worldMatrix * vec4<f32>(in.position, 1.0);
91
+ let worldNormal = normalize((worldMatrix * vec4<f32>(in.normal, 0.0)).xyz);
92
+ let worldTangent = normalize((worldMatrix * vec4<f32>(in.tangent.xyz, 0.0)).xyz);
68
93
  #endif
69
94
  // feat-20260625-spot-light-shadow-mapping M2 / w10 (D-1): spot shadow passes
70
95
  // set `isSpot = 1u` and write their perspective matrix into
@@ -72,8 +97,57 @@ fn vs_main(in : VsInput, @builtin(instance_index) idx : u32) -> @builtin(positio
72
97
  // `view.lightViewProj_A..D` via `index`. Routing on the discriminant keeps
73
98
  // the spot matrix out of the directional View UBO (no same-frame contention).
74
99
  if (shadowCasterCascade.isSpot == 1u) {
75
- return shadowCasterCascade.spotLightViewProj * worldPos;
100
+ var out : VsOut;
101
+ out.clip = shadowCasterCascade.spotLightViewProj * worldPos;
102
+ out.positionOS = in.position;
103
+ out.positionWS = worldPos.xyz;
104
+ out.normalWS = worldNormal;
105
+ out.tangentWS = vec4<f32>(worldTangent, in.tangent.w);
106
+ out.surfaceUv = in.uv;
107
+ out.vertexColor = vec4<f32>(1.0);
108
+ return out;
76
109
  }
77
110
  let lvp = _cascadeLightViewProj(shadowCasterCascade.index);
78
- return lvp * worldPos;
111
+ var out : VsOut;
112
+ out.clip = lvp * worldPos;
113
+ out.positionOS = in.position;
114
+ out.positionWS = worldPos.xyz;
115
+ out.normalWS = worldNormal;
116
+ out.tangentWS = vec4<f32>(worldTangent, in.tangent.w);
117
+ out.surfaceUv = in.uv;
118
+ out.vertexColor = vec4<f32>(1.0);
119
+ return out;
120
+ }
121
+
122
+ fn evaluateShadowSurface(in : VsOut, frontFacing : bool) -> SurfaceData {
123
+ let viewDirectionWS = normalize(view.cameraPos - in.positionWS);
124
+ return evaluate_surface(SurfaceInput(
125
+ in.positionOS,
126
+ in.positionWS,
127
+ in.normalWS,
128
+ in.tangentWS,
129
+ viewDirectionWS,
130
+ in.surfaceUv,
131
+ in.surfaceUv,
132
+ in.vertexColor,
133
+ frontFacing,
134
+ ));
135
+ }
136
+
137
+ fn alphaTestShadowSurface(surface : SurfaceData) {
138
+ if (surface.alphaClipThreshold > 0.0 && surface.opacity <= surface.alphaClipThreshold) {
139
+ discard;
140
+ }
141
+ }
142
+
143
+ // The depth-only path evaluates the selected Surface so alpha-clip ownership
144
+ // remains shared with Forward and Deferred without mutating vertex data.
145
+ @fragment
146
+ fn fs_shadow(in : VsOut, @builtin(front_facing) frontFacing : bool) {
147
+ alphaTestShadowSurface(evaluateShadowSurface(in, frontFacing));
148
+ }
149
+
150
+ @fragment
151
+ fn fs_main(in : VsOut, @builtin(front_facing) frontFacing : bool) {
152
+ alphaTestShadowSurface(evaluateShadowSurface(in, frontFacing));
79
153
  }
@@ -9,18 +9,11 @@
9
9
  #define_import_path forgeax_standard::cluster
10
10
 
11
11
  #import forgeax_pbr::lighting_punctual::{evalPoint, evalPointFlat, evalSpot, evalSpotFlat, evalSpotShadowed}
12
- #import forgeax_pbr::lighting_attenuation::{projectSpotUv}
13
12
  #import forgeax_pbr::lighting_spot_modifiers::{spotModifierFactors}
14
13
  #import forgeax_pbr::lighting_rect_area::{evalRectAreaLtcGgx}
15
14
  #import forgeax_view::common::{view}
16
15
  #ifdef PROJECTOR_AVAILABLE
17
- #ifdef EXTENDED_LIGHTING_AVAILABLE
18
- #import forgeax_view::common::{spotModifierSampler, cookieTexture}
19
- #else
20
- #ifndef EXTENDED_LIGHTING_AVAILABLE
21
- #import forgeax_view::common::{projectorTexture, projectorSampler}
22
- #endif
23
- #endif
16
+ #import forgeax_pbr::lighting_spot_projector::{sampleStandardSpotProjector}
24
17
  #endif
25
18
  #ifdef POINT_SHADOW_AVAILABLE
26
19
  #import forgeax_pbr::lighting_punctual::{evalPointShadowed}
@@ -95,34 +88,6 @@ fn ndc_position_to_cluster(
95
88
  return vec3(cx, cy, cz);
96
89
  }
97
90
 
98
- // Surface SpotLight projector sampling is owned by the same clustered light
99
- // evaluator as the analytic cone/range contribution. The host marks the
100
- // selected projector in the DirectLightSlot flag and writes its matrix to the
101
- // corresponding View lane (lane 0 for projector-only); no second
102
- // material-local sampling loop or resource owner is allowed.
103
- fn sampleStandardSpotProjector(
104
- lightViewProj : mat4x4<f32>,
105
- world_pos : vec3<f32>,
106
- metadata : vec4<u32>,
107
- ) -> vec3<f32> {
108
- #ifdef PROJECTOR_AVAILABLE
109
- let uv = projectSpotUv(lightViewProj, world_pos);
110
- if (!(uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0)) {
111
- return vec3<f32>(1.0);
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
119
- return textureSampleLevel(projectorTexture, projectorSampler, uv, 0.0).rgb;
120
- #endif
121
- #else
122
- return vec3<f32>(1.0);
123
- #endif
124
- }
125
-
126
91
  // Decode one kind first, then delegate the complete evaluation to the shared
127
92
  // punctual owner. Unknown kinds are explicitly zero contribution.
128
93
  fn evaluate_cluster_light(
@@ -0,0 +1,26 @@
1
+ #define_import_path forgeax_material::surface_v1
2
+
3
+ // Engine-owned input ABI for authored Standard material surfaces.
4
+ struct SurfaceInput {
5
+ positionOS : vec3<f32>,
6
+ positionWS : vec3<f32>,
7
+ geometricNormalWS : vec3<f32>,
8
+ tangentWS : vec4<f32>,
9
+ viewDirectionWS : vec3<f32>,
10
+ uv0 : vec2<f32>,
11
+ uv1 : vec2<f32>,
12
+ vertexColor : vec4<f32>,
13
+ frontFacing : bool,
14
+ };
15
+
16
+ // Surface output is consumed by the Standard BRDF and pass family.
17
+ struct SurfaceData {
18
+ baseColor : vec3<f32>,
19
+ normalWS : vec3<f32>,
20
+ metallic : f32,
21
+ roughness : f32,
22
+ emissive : vec3<f32>,
23
+ occlusion : f32,
24
+ opacity : f32,
25
+ alphaClipThreshold : f32,
26
+ };
package/src/unlit.wgsl CHANGED
@@ -111,6 +111,18 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
111
111
  return vec4<f32>(material.baseColor.rgb * texSample.rgb * vertexColor.rgb, alpha);
112
112
  }
113
113
 
114
+ // Depth-only shadow variant. Keep alpha clipping identical to the color path,
115
+ // but return no color target because the shadow pass has a depth attachment only.
116
+ @fragment
117
+ fn fs_shadow(in : VsOut) {
118
+ let texSample = sampleMaterialTextureLinear(baseColorTexture, baseColorSampler, in.uv, material.baseColorTextureCoordinatesMetadata.zw);
119
+ let vertexColor = materialVertexColor(in);
120
+ let alpha = material.baseColor.a * texSample.a * vertexColor.a;
121
+ if (material.alphaCutoff > 0.0 && alpha < material.alphaCutoff) {
122
+ discard;
123
+ }
124
+ }
125
+
114
126
  struct TemporalVsOut {
115
127
  @builtin(position) clip : vec4<f32>,
116
128
  @location(0) uv : vec2<f32>,