@forgeax/engine-shader 0.1.2

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 (162) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +99 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/ShaderRegistry.d.ts +208 -0
  5. package/dist/ShaderRegistry.d.ts.map +1 -0
  6. package/dist/__tests__/atmosphere-background.test.d.ts +2 -0
  7. package/dist/__tests__/atmosphere-background.test.d.ts.map +1 -0
  8. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +2 -0
  9. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +1 -0
  10. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +2 -0
  11. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +1 -0
  12. package/dist/__tests__/atmosphere-preetham-red.test.d.ts +2 -0
  13. package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +1 -0
  14. package/dist/__tests__/atmosphere-preetham.test.d.ts +2 -0
  15. package/dist/__tests__/atmosphere-preetham.test.d.ts.map +1 -0
  16. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +2 -0
  17. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +1 -0
  18. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +2 -0
  19. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +2 -0
  21. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +1 -0
  22. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +2 -0
  23. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/deferred-lighting-ssao.test.d.ts +2 -0
  25. package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +1 -0
  26. package/dist/__tests__/fog-oracle.unit.test.d.ts +2 -0
  27. package/dist/__tests__/fog-oracle.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +2 -0
  29. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +2 -0
  31. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/hdrp-ssao-compile.test.d.ts +2 -0
  33. package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +1 -0
  34. package/dist/__tests__/ibl-irradiance.unit.test.d.ts +2 -0
  35. package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/ibl-sampling.unit.test.d.ts +2 -0
  37. package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/lighting-punctual.unit.test.d.ts +2 -0
  39. package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +2 -0
  41. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/material-builtins.unit.test.d.ts +2 -0
  43. package/dist/__tests__/material-builtins.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/material-contract.unit.test.d.ts +2 -0
  45. package/dist/__tests__/material-contract.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/material-derived-builtins.integration.test.d.ts +2 -0
  47. package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +1 -0
  48. package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
  49. package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/scene-temporal.unit.test.d.ts +2 -0
  51. package/dist/__tests__/scene-temporal.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +2 -0
  53. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/shader.unit.test.d.ts +2 -0
  55. package/dist/__tests__/shader.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/sprite-lit-shader.test.d.ts +2 -0
  57. package/dist/__tests__/sprite-lit-shader.test.d.ts.map +1 -0
  58. package/dist/__tests__/sprite-variants.unit.test.d.ts +2 -0
  59. package/dist/__tests__/sprite-variants.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/taa-resolve.unit.test.d.ts +2 -0
  61. package/dist/__tests__/taa-resolve.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/transparent-pbr.unit.test.d.ts +2 -0
  63. package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/vertex-color-variant.unit.test.d.ts +2 -0
  65. package/dist/__tests__/vertex-color-variant.unit.test.d.ts.map +1 -0
  66. package/dist/errors.d.ts +96 -0
  67. package/dist/errors.d.ts.map +1 -0
  68. package/dist/index.d.ts +44 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.mjs +560 -0
  71. package/dist/index.mjs.map +1 -0
  72. package/dist/material/artifact-registry.d.ts +31 -0
  73. package/dist/material/artifact-registry.d.ts.map +1 -0
  74. package/dist/material/artifact-types.d.ts +13 -0
  75. package/dist/material/artifact-types.d.ts.map +1 -0
  76. package/dist/material-schemas.d.ts +8 -0
  77. package/dist/material-schemas.d.ts.map +1 -0
  78. package/dist/register-default-sprite-lit.d.ts +40 -0
  79. package/dist/register-default-sprite-lit.d.ts.map +1 -0
  80. package/dist/register-default-standard-pbr-skin.d.ts +34 -0
  81. package/dist/register-default-standard-pbr-skin.d.ts.map +1 -0
  82. package/dist/tonemap.d.ts +13 -0
  83. package/dist/tonemap.d.ts.map +1 -0
  84. package/dist/types.d.ts +47 -0
  85. package/dist/types.d.ts.map +1 -0
  86. package/package.json +58 -0
  87. package/src/ShaderRegistry.ts +519 -0
  88. package/src/__tests__/atmosphere-background.test.ts +33 -0
  89. package/src/__tests__/atmosphere-cubemap-orientation.test.ts +68 -0
  90. package/src/__tests__/atmosphere-preetham-numeric.test.ts +122 -0
  91. package/src/__tests__/atmosphere-preetham-red.test.ts +52 -0
  92. package/src/__tests__/atmosphere-preetham.test.ts +132 -0
  93. package/src/__tests__/atmosphere-three-r184-reference.test.ts +122 -0
  94. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +31 -0
  95. package/src/__tests__/builtin-texture-sampling-contract.test.ts +56 -0
  96. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +19 -0
  97. package/src/__tests__/deferred-lighting-ssao.test.ts +114 -0
  98. package/src/__tests__/fog-oracle.unit.test.ts +132 -0
  99. package/src/__tests__/fog-orthographic-ray.unit.test.ts +62 -0
  100. package/src/__tests__/fog-producer-matrix.integration.test.ts +77 -0
  101. package/src/__tests__/hdrp-ssao-compile.test.ts +185 -0
  102. package/src/__tests__/ibl-irradiance.unit.test.ts +43 -0
  103. package/src/__tests__/ibl-sampling.unit.test.ts +40 -0
  104. package/src/__tests__/lighting-punctual.unit.test.ts +66 -0
  105. package/src/__tests__/manifest.fixture.json +22 -0
  106. package/src/__tests__/material-artifact-dedup.unit.test.ts +21 -0
  107. package/src/__tests__/material-builtins.unit.test.ts +111 -0
  108. package/src/__tests__/material-contract.unit.test.ts +79 -0
  109. package/src/__tests__/material-derived-builtins.integration.test.ts +30 -0
  110. package/src/__tests__/public-surface.unit.test.ts +9 -0
  111. package/src/__tests__/scene-temporal.unit.test.ts +32 -0
  112. package/src/__tests__/shader-registry-transaction.unit.test.ts +116 -0
  113. package/src/__tests__/shader.unit.test.ts +1909 -0
  114. package/src/__tests__/sprite-lit-shader.test.ts +240 -0
  115. package/src/__tests__/sprite-variants.unit.test.ts +209 -0
  116. package/src/__tests__/taa-resolve.unit.test.ts +32 -0
  117. package/src/__tests__/transparent-pbr.unit.test.ts +21 -0
  118. package/src/__tests__/vertex-color-variant.unit.test.ts +52 -0
  119. package/src/atmosphere-background.wgsl +80 -0
  120. package/src/atmosphere-cubemap.wgsl +72 -0
  121. package/src/atmosphere-ibl.wgsl +127 -0
  122. package/src/atmosphere-preetham.wgsl +214 -0
  123. package/src/bloom-blur.wgsl +89 -0
  124. package/src/bloom-bright.wgsl +50 -0
  125. package/src/bloom-composite.wgsl +54 -0
  126. package/src/brdf.wgsl +25 -0
  127. package/src/common.wgsl +523 -0
  128. package/src/default-standard-pbr-skin.wgsl +538 -0
  129. package/src/default-standard-pbr.wgsl +794 -0
  130. package/src/errors.ts +169 -0
  131. package/src/fog.wgsl +27 -0
  132. package/src/fxaa.wgsl +259 -0
  133. package/src/hdrp-cluster-forward.wgsl +186 -0
  134. package/src/hdrp-deferred-lighting.wgsl +69 -0
  135. package/src/hdrp-ssao.wgsl +174 -0
  136. package/src/ibl-brdf-lut.wgsl +92 -0
  137. package/src/ibl-equirect-to-cube.wgsl +56 -0
  138. package/src/ibl-irradiance.wgsl +89 -0
  139. package/src/ibl-prefilter.wgsl +101 -0
  140. package/src/ibl-sampling.wgsl +58 -0
  141. package/src/ibl-shared.wgsl +118 -0
  142. package/src/index.ts +166 -0
  143. package/src/lighting-directional.wgsl +471 -0
  144. package/src/lighting-punctual.wgsl +274 -0
  145. package/src/material/artifact-registry.ts +78 -0
  146. package/src/material/artifact-types.ts +26 -0
  147. package/src/material-schemas.ts +48 -0
  148. package/src/msdf-text.wgsl +208 -0
  149. package/src/pbr-temporal.wgsl +73 -0
  150. package/src/register-default-sprite-lit.ts +87 -0
  151. package/src/register-default-standard-pbr-skin.ts +74 -0
  152. package/src/shadow-pcf.wgsl +120 -0
  153. package/src/shadow_caster.wgsl +54 -0
  154. package/src/skybox.wgsl +75 -0
  155. package/src/sprite-lit.wgsl +307 -0
  156. package/src/sprite.wgsl +365 -0
  157. package/src/taa-resolve.wgsl +129 -0
  158. package/src/tbn.wgsl +54 -0
  159. package/src/tonemap.ts +13 -0
  160. package/src/tonemap.wgsl +177 -0
  161. package/src/types.ts +59 -0
  162. package/src/unlit.wgsl +186 -0
package/src/errors.ts ADDED
@@ -0,0 +1,169 @@
1
+ // @forgeax/engine-shader/errors — ShaderError + factories (physically isolated from the
2
+ // same-named class in @forgeax/engine-shader-compiler, but shape-aligned 1:1 / charter
3
+ // proposition 5: consistent abstraction).
4
+ //
5
+ // Shape rules (plan-strategy §S-7 / D-R7 / OQ-2 close):
6
+ // - ShaderError extends Error, with the 5-field top-level projection
7
+ // {code, lineNum, linePos, message, hint} (MVP-2.3) + 3 internal fields
8
+ // {expected, detail, name}.
9
+ // - ShaderErrorCode is a closed 4-member union imported from @forgeax/engine-types
10
+ // (SSOT single-source policy).
11
+ //
12
+ // Physical isolation — this file does **not** import from
13
+ // @forgeax/engine-shader-compiler; the type / factories are 1:1 mirrored but
14
+ // independently implemented (guarded by the AC-06 triple-grep gate; charter
15
+ // proposition 1: progressive disclosure).
16
+
17
+ import type { ShaderErrorCode } from '@forgeax/engine-types';
18
+
19
+ export type { ShaderErrorCode };
20
+
21
+ /**
22
+ * Path-specific `detail` shape for `manifest-malformed` / `shader-not-found`.
23
+ * `reason` is supplemental prose (it does not replace `hint`).
24
+ */
25
+ export interface ShaderErrorDetail {
26
+ readonly reason?: string | undefined;
27
+ /**
28
+ * Machine-consumable list of currently-registered material-shader identifiers.
29
+ * Populated by `materialShaderNotFound()` factory so AI users can enumerate
30
+ * available shaders via `err.detail.registeredShaderIds` for autocomplete /
31
+ * fuzzy-match recovery without parsing the human-formatted `err.expected`
32
+ * string (charter P3 structured failure). `err.expected` still carries the
33
+ * pre-formatted `"ShaderRegistry has registered identifiers: [...]"` message
34
+ * for human-readable log output — the two channels are complementary.
35
+ */
36
+ readonly registeredShaderIds?: readonly string[] | undefined;
37
+ }
38
+
39
+ interface ShaderErrorInit {
40
+ readonly code: ShaderErrorCode;
41
+ readonly expected: string;
42
+ readonly hint: string;
43
+ readonly message: string;
44
+ readonly lineNum?: number | undefined;
45
+ readonly linePos?: number | undefined;
46
+ readonly detail?: ShaderErrorDetail | undefined;
47
+ }
48
+
49
+ /**
50
+ * Structured shader error (runtime path, aligned with build-time
51
+ * `@forgeax/engine-shader-compiler.ShaderError`).
52
+ *
53
+ * **5 surface fields** (MVP-2.3 top-level projection, AI-user consumption path):
54
+ * - `.code` — member of the closed `ShaderErrorCode` union (4 variants)
55
+ * - `.message` — display text (`Error` base-class field, populated at construction)
56
+ * - `.hint` — actionable recovery guidance (charter proposition 3:
57
+ * machine-readable hint > prose)
58
+ * - `.lineNum` / `.linePos` — error source location (undefined on the runtime
59
+ * path; only populated by the build-time compile-failed path; the field is
60
+ * retained here for shape alignment).
61
+ *
62
+ * **3 internal fields**:
63
+ * - `.name` = `'ShaderError'` (debug label)
64
+ * - `.expected` — expected-state description (symmetric with RhiError)
65
+ * - `.detail` — path-specific extras (`reason` prose)
66
+ *
67
+ * **Do not `new` this directly** — construct via the 2 factory helpers
68
+ * (`manifestMalformed` / `shaderNotFound`); the runtime path of this loop does
69
+ * not trigger compile-failed / init-failed (those two paths belong to the
70
+ * build-time `ShaderError` shape in `@forgeax/engine-shader-compiler`).
71
+ */
72
+ export class ShaderError extends Error {
73
+ override readonly name: 'ShaderError' = 'ShaderError';
74
+ readonly code: ShaderErrorCode;
75
+ readonly expected: string;
76
+ readonly hint: string;
77
+ readonly lineNum: number | undefined;
78
+ readonly linePos: number | undefined;
79
+ readonly detail: ShaderErrorDetail | undefined;
80
+
81
+ constructor(init: ShaderErrorInit) {
82
+ super(init.message);
83
+ this.code = init.code;
84
+ this.expected = init.expected;
85
+ this.hint = init.hint;
86
+ this.lineNum = init.lineNum;
87
+ this.linePos = init.linePos;
88
+ this.detail = init.detail;
89
+ }
90
+ }
91
+
92
+ // === factory helpers (2 variants for the runtime path) ============================
93
+
94
+ /** `manifest-malformed`: manifest.json schema validation failed (4 fields missing or JSON unparseable). */
95
+ export function manifestMalformed(args: {
96
+ readonly message: string;
97
+ readonly hint: string;
98
+ readonly reason?: string | undefined;
99
+ }): ShaderError {
100
+ return new ShaderError({
101
+ code: 'manifest-malformed',
102
+ expected: 'manifest.json parses + every entry has {hash, wgsl, glsl, bindings}',
103
+ message: args.message,
104
+ hint: args.hint,
105
+ ...(args.reason !== undefined ? { detail: { reason: args.reason } } : {}),
106
+ });
107
+ }
108
+
109
+ /** `shader-not-found`: ShaderRegistry.get(hash) hash miss. */
110
+ export function shaderNotFound(args: {
111
+ readonly hash: string;
112
+ readonly hint: string;
113
+ }): ShaderError {
114
+ return new ShaderError({
115
+ code: 'shader-not-found',
116
+ expected: `manifest.entries contains entry with hash '${args.hash}'`,
117
+ message: `ShaderRegistry: hash '${args.hash}' not present in manifest`,
118
+ hint: args.hint,
119
+ });
120
+ }
121
+
122
+ /**
123
+ * `material-shader-not-found`: `ShaderRegistry.findMaterialArtifact(identifier)`
124
+ * miss. Returned by the runtime path when a `MaterialAsset.payload.materialShader`
125
+ * `::`-routed identifier (or any registered identifier) has no matching entry
126
+ * in the registry. AI-user recovery: the `.hint` cites the
127
+ * `forgeax::default-standard-pbr` reservation + the
128
+ * `installMaterialArtifact(...)` API path so the caller can register the missing
129
+ * shader at engine boot (feat-20260523-shader-template-instance-split M5 / T05).
130
+ *
131
+ * feat-20260528-material-shader-registration-unification M3 / w12 +
132
+ * feat-20260624-sprite-lit F-P1 fix: dual-channel exposure of the registered
133
+ * identifier set. `.expected` carries the pre-formatted string
134
+ * `"ShaderRegistry has registered identifiers: [id1, id2, ...]"` for human /
135
+ * log output; `.detail.registeredShaderIds` carries the raw `readonly string[]`
136
+ * for AI users to enumerate via property access without regex-parsing the
137
+ * message (charter P3 structured failure).
138
+ */
139
+ export function materialShaderNotFound(args: {
140
+ readonly identifier: string;
141
+ readonly hint: string;
142
+ readonly expected: string[];
143
+ }): ShaderError {
144
+ return new ShaderError({
145
+ code: 'material-shader-not-found',
146
+ expected: `ShaderRegistry has registered identifiers: [${args.expected.join(', ')}]`,
147
+ message: `ShaderRegistry: material shader identifier '${args.identifier}' not registered`,
148
+ hint: args.hint,
149
+ detail: { reason: `identifier=${args.identifier}`, registeredShaderIds: args.expected },
150
+ });
151
+ }
152
+
153
+ // === Result<T, E> ====================================================================
154
+ //
155
+ // Round 3 fix-up F-P0-1: Result is re-exported from @forgeax/engine-rhi so the
156
+ // `.unwrap()` / `.unwrapOr()` method-chain surface (AGENTS.md §Adapter /
157
+ // Device IDE-friendly template + 50+ JSDoc @example anchors) is identical
158
+ // across the @forgeax/engine-rhi + @forgeax/engine-shader public surfaces (charter
159
+ // proposition 5: consistent abstraction). The previous local duplicate
160
+ // (plain field-only Result) caused TS structural-mismatch when ShaderRegistry
161
+ // values flowed into RHI-typed signatures.
162
+
163
+ export {
164
+ err,
165
+ ok,
166
+ type Result,
167
+ type ResultErr,
168
+ type ResultOk,
169
+ } from '@forgeax/engine-rhi';
package/src/fog.wgsl ADDED
@@ -0,0 +1,27 @@
1
+ #define_import_path forgeax_view::fog
2
+
3
+ // Shared analytic scene-radiance Fog. Producers provide a world-space ray;
4
+ // this module owns the density integration and linear color contract.
5
+ #import forgeax_view::common::{FogViewParams, FogRay}
6
+
7
+ const FOG_Q_EPSILON : f32 = 1e-6;
8
+ const FOG_EXP_LIMIT : f32 = 80.0;
9
+
10
+ fn fogOpticalDepth(params : FogViewParams, ray : FogRay) -> f32 {
11
+ let rho0 = params.density * exp(clamp(-params.heightFalloff * ray.origin.y, -FOG_EXP_LIMIT, FOG_EXP_LIMIT));
12
+ let q = params.heightFalloff * ray.direction.y;
13
+ var tau = rho0 * ray.distance;
14
+ if abs(q) >= FOG_Q_EPSILON {
15
+ let exponent = clamp(-q * ray.distance, -FOG_EXP_LIMIT, FOG_EXP_LIMIT);
16
+ tau = rho0 * (1.0 - exp(exponent)) / q;
17
+ }
18
+ return max(tau, 0.0);
19
+ }
20
+
21
+ fn apply_fog(params : FogViewParams, ray : FogRay, color : vec4<f32>) -> vec4<f32> {
22
+ let tau = fogOpticalDepth(params, ray);
23
+ let transmittance = exp(-tau);
24
+ let opacity = params.maxOpacity * (1.0 - transmittance);
25
+ let mixed = mix(color.rgb, params.color, opacity);
26
+ return vec4<f32>(mixed, color.a);
27
+ }
package/src/fxaa.wgsl ADDED
@@ -0,0 +1,259 @@
1
+ #define_import_path forgeax_view::fxaa
2
+
3
+ // @forgeax/engine-shader - fxaa.wgsl
4
+ // (feat-20260528-fxaa-post-processing / w4; algorithm rewritten
5
+ // feat-20260531-fxaa-shader-correctness-fix).
6
+ //
7
+ // FXAA 3.11 fragment shader -- faithful port of the canonical Simon
8
+ // Rodriguez reference (same lineage as Bevy's WGSL port), adapted to
9
+ // forgeax naga_oil conventions. Hardcoded "High" quality preset: 12-step
10
+ // edge search with compile-time step multipliers (no UBO, no #ifdef quality
11
+ // branches).
12
+ //
13
+ // Vertex stage: imports the SSOT fullscreen_triangle() from
14
+ // forgeax_view::common (single large triangle, research Finding 1).
15
+ //
16
+ // Fragment stage: computes perceptual luma via rgb2luma (sqrt of Rec.601
17
+ // weighted sum -- matches the canonical reference), performs a directional
18
+ // edge search, then blends along the edge with a sub-pixel aliasing term.
19
+ // Texture dimensions are obtained via textureDimensions() for resize-agnostic
20
+ // behavior (AC-08).
21
+ //
22
+ // SAMPLING: uses textureSampleLevel (explicit LOD 0) with a linear sampler,
23
+ // NOT textureLoad. Explicit-LOD sampling is permitted in non-uniform control
24
+ // flow (WebGPU spec 20.1 only bars *implicit*-derivative textureSample), so
25
+ // the edge-search loops are legal while still getting bilinear filtering --
26
+ // the bilinear taps at fractional UVs are what actually do the sub-texel
27
+ // blend that anti-aliases the edge.
28
+ //
29
+ // All offsets are kept in UV space: texel steps are scaled by
30
+ // inverseScreenSize (= 1/dims) before being added to a [0,1] UV. The prior
31
+ // implementation added texel-unit offsets directly to UVs (and squared
32
+ // texelSize in the sub-pixel term), sampling ~half a screen away and gouging
33
+ // holes in shaded surfaces -- the bug this rewrite fixes.
34
+ //
35
+ // Bindings (group 0):
36
+ // @binding(0) screenTexture : texture_2d<f32> -- sampled LDR input
37
+ // @binding(1) samp : sampler -- linear filterable sampler
38
+ // linearLdrColorDomain: FXAA samples and writes linear LDR values; display
39
+ // encoding happens only at the final output surface.
40
+
41
+ #import forgeax_view::common::FullscreenOutput
42
+ #import forgeax_view::common::fullscreen_triangle
43
+ #import forgeax_view::common::linearToSrgbOetf
44
+
45
+ const EDGE_THRESHOLD_MIN: f32 = 0.0312;
46
+ const EDGE_THRESHOLD_MAX: f32 = 0.125;
47
+ const SUBPIXEL_QUALITY: f32 = 0.75;
48
+ const ITERATIONS: i32 = 12;
49
+
50
+ @group(0) @binding(0) var screenTexture: texture_2d<f32>;
51
+ @group(0) @binding(1) var samp: sampler;
52
+
53
+ fn rgb2luma(rgb: vec3<f32>) -> f32 {
54
+ return sqrt(dot(rgb, vec3<f32>(0.299, 0.587, 0.114)));
55
+ }
56
+
57
+ fn sampleColor(uv: vec2<f32>) -> vec3<f32> {
58
+ return textureSampleLevel(screenTexture, samp, uv, 0.0).rgb;
59
+ }
60
+
61
+ fn sampleLuma(uv: vec2<f32>) -> f32 {
62
+ return rgb2luma(textureSampleLevel(screenTexture, samp, uv, 0.0).rgb);
63
+ }
64
+
65
+ // High-preset edge-search step multipliers (canonical FXAA 3.11 QUALITY
66
+ // table for the 12-tap variant): steps 0..4 = 1.0, then ramps to 8.0.
67
+ fn qualityStep(i: i32) -> f32 {
68
+ if i < 5 {
69
+ return 1.0;
70
+ }
71
+ if i == 5 {
72
+ return 1.5;
73
+ }
74
+ if i < 10 {
75
+ return 2.0;
76
+ }
77
+ if i == 10 {
78
+ return 4.0;
79
+ }
80
+ return 8.0;
81
+ }
82
+
83
+ @vertex
84
+ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> FullscreenOutput {
85
+ return fullscreen_triangle(vertex_index);
86
+ }
87
+
88
+ @fragment
89
+ fn fs_main(in: FullscreenOutput) -> @location(0) vec4<f32> {
90
+ let dims = vec2<f32>(textureDimensions(screenTexture));
91
+ let inverseScreenSize = 1.0 / dims;
92
+ let uv = in.uv;
93
+
94
+ let centerColor = sampleColor(uv);
95
+ let lumaCenter = rgb2luma(centerColor);
96
+
97
+ // Lumas of the 4 direct neighbours.
98
+ let lumaDown = sampleLuma(uv + vec2<f32>(0.0, 1.0) * inverseScreenSize);
99
+ let lumaUp = sampleLuma(uv + vec2<f32>(0.0, -1.0) * inverseScreenSize);
100
+ let lumaLeft = sampleLuma(uv + vec2<f32>(-1.0, 0.0) * inverseScreenSize);
101
+ let lumaRight = sampleLuma(uv + vec2<f32>(1.0, 0.0) * inverseScreenSize);
102
+
103
+ let lumaMin = min(lumaCenter, min(min(lumaDown, lumaUp), min(lumaLeft, lumaRight)));
104
+ let lumaMax = max(lumaCenter, max(max(lumaDown, lumaUp), max(lumaLeft, lumaRight)));
105
+ let lumaRange = lumaMax - lumaMin;
106
+
107
+ // Not on an edge (or in a near-uniform region): pass the centre through.
108
+ if lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX) {
109
+ return vec4<f32>(linearToSrgbOetf(centerColor), 1.0);
110
+ }
111
+
112
+ // Lumas of the 4 corners.
113
+ let lumaDownLeft = sampleLuma(uv + vec2<f32>(-1.0, 1.0) * inverseScreenSize);
114
+ let lumaUpRight = sampleLuma(uv + vec2<f32>(1.0, -1.0) * inverseScreenSize);
115
+ let lumaUpLeft = sampleLuma(uv + vec2<f32>(-1.0, -1.0) * inverseScreenSize);
116
+ let lumaDownRight = sampleLuma(uv + vec2<f32>(1.0, 1.0) * inverseScreenSize);
117
+
118
+ let lumaDownUp = lumaDown + lumaUp;
119
+ let lumaLeftRight = lumaLeft + lumaRight;
120
+
121
+ let lumaLeftCorners = lumaDownLeft + lumaUpLeft;
122
+ let lumaDownCorners = lumaDownLeft + lumaDownRight;
123
+ let lumaRightCorners = lumaDownRight + lumaUpRight;
124
+ let lumaUpCorners = lumaUpRight + lumaUpLeft;
125
+
126
+ // Estimate horizontal vs vertical gradient.
127
+ let edgeHorizontal =
128
+ abs(-2.0 * lumaLeft + lumaLeftCorners) +
129
+ abs(-2.0 * lumaCenter + lumaDownUp) * 2.0 +
130
+ abs(-2.0 * lumaRight + lumaRightCorners);
131
+ let edgeVertical =
132
+ abs(-2.0 * lumaUp + lumaUpCorners) +
133
+ abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 +
134
+ abs(-2.0 * lumaDown + lumaDownCorners);
135
+
136
+ let isHorizontal = edgeHorizontal >= edgeVertical;
137
+
138
+ // Lumas on the two sides of the edge.
139
+ let luma1 = select(lumaLeft, lumaDown, isHorizontal);
140
+ let luma2 = select(lumaRight, lumaUp, isHorizontal);
141
+ let gradient1 = luma1 - lumaCenter;
142
+ let gradient2 = luma2 - lumaCenter;
143
+
144
+ let is1Steepest = abs(gradient1) >= abs(gradient2);
145
+ let gradientScaled = 0.25 * max(abs(gradient1), abs(gradient2));
146
+
147
+ // One-texel step perpendicular to the edge, in UV space.
148
+ var stepLength = select(inverseScreenSize.x, inverseScreenSize.y, isHorizontal);
149
+
150
+ var lumaLocalAverage = 0.0;
151
+ if is1Steepest {
152
+ stepLength = -stepLength;
153
+ lumaLocalAverage = 0.5 * (luma1 + lumaCenter);
154
+ } else {
155
+ lumaLocalAverage = 0.5 * (luma2 + lumaCenter);
156
+ }
157
+
158
+ // Shift the UV by half a texel toward the edge.
159
+ var currentUv = uv;
160
+ if isHorizontal {
161
+ currentUv.y += stepLength * 0.5;
162
+ } else {
163
+ currentUv.x += stepLength * 0.5;
164
+ }
165
+
166
+ // Walk along the edge in both directions until the luma delta exceeds the
167
+ // local gradient (= we left the edge) or we run out of iterations.
168
+ let offsetStep = select(
169
+ vec2<f32>(stepLength, 0.0),
170
+ vec2<f32>(0.0, stepLength),
171
+ isHorizontal,
172
+ );
173
+ let edgeStep = select(
174
+ vec2<f32>(0.0, inverseScreenSize.y),
175
+ vec2<f32>(inverseScreenSize.x, 0.0),
176
+ isHorizontal,
177
+ );
178
+
179
+ var uv1 = currentUv - edgeStep;
180
+ var uv2 = currentUv + edgeStep;
181
+
182
+ var lumaEnd1 = sampleLuma(uv1) - lumaLocalAverage;
183
+ var lumaEnd2 = sampleLuma(uv2) - lumaLocalAverage;
184
+
185
+ var reached1 = abs(lumaEnd1) >= gradientScaled;
186
+ var reached2 = abs(lumaEnd2) >= gradientScaled;
187
+ var reachedBoth = reached1 && reached2;
188
+
189
+ if !reached1 {
190
+ uv1 -= edgeStep;
191
+ }
192
+ if !reached2 {
193
+ uv2 += edgeStep;
194
+ }
195
+
196
+ if !reachedBoth {
197
+ var i = 1;
198
+ loop {
199
+ if i >= ITERATIONS || reachedBoth {
200
+ break;
201
+ }
202
+ let q = qualityStep(i);
203
+ if !reached1 {
204
+ lumaEnd1 = sampleLuma(uv1) - lumaLocalAverage;
205
+ reached1 = abs(lumaEnd1) >= gradientScaled;
206
+ }
207
+ if !reached2 {
208
+ lumaEnd2 = sampleLuma(uv2) - lumaLocalAverage;
209
+ reached2 = abs(lumaEnd2) >= gradientScaled;
210
+ }
211
+ reachedBoth = reached1 && reached2;
212
+ if !reached1 {
213
+ uv1 -= edgeStep * q;
214
+ }
215
+ if !reached2 {
216
+ uv2 += edgeStep * q;
217
+ }
218
+ i += 1;
219
+ }
220
+ }
221
+
222
+ // Distances to each end of the edge, along the edge axis.
223
+ let distance1 = select(uv.y - uv1.y, uv.x - uv1.x, isHorizontal);
224
+ let distance2 = select(uv2.y - uv.y, uv2.x - uv.x, isHorizontal);
225
+
226
+ let isDirection1 = distance1 < distance2;
227
+ let distanceFinal = min(distance1, distance2);
228
+ let edgeThickness = distance1 + distance2;
229
+ let pixelOffsetRaw = -distanceFinal / edgeThickness + 0.5;
230
+
231
+ // Only blend if the centre luma is on the correct side of the edge --
232
+ // guards against haloing.
233
+ let isLumaCenterSmaller = lumaCenter < lumaLocalAverage;
234
+ let correctVariation1 = (lumaEnd1 < 0.0) != isLumaCenterSmaller;
235
+ let correctVariation2 = (lumaEnd2 < 0.0) != isLumaCenterSmaller;
236
+ let correctVariation = select(correctVariation2, correctVariation1, isDirection1);
237
+ let finalOffset = select(0.0, pixelOffsetRaw, correctVariation);
238
+
239
+ // Sub-pixel anti-aliasing: weight by the local 3x3 luma contrast.
240
+ let lumaAverage =
241
+ (1.0 / 12.0) *
242
+ (2.0 * (lumaDownUp + lumaLeftRight) + lumaLeftCorners + lumaRightCorners);
243
+ let subPixelOffset1 = clamp(abs(lumaAverage - lumaCenter) / lumaRange, 0.0, 1.0);
244
+ let subPixelOffset2 = (-2.0 * subPixelOffset1 + 3.0) * subPixelOffset1 * subPixelOffset1;
245
+ let subPixelOffsetFinal = subPixelOffset2 * subPixelOffset2 * SUBPIXEL_QUALITY;
246
+
247
+ let pixelOffset = max(finalOffset, subPixelOffsetFinal);
248
+
249
+ // Apply the perpendicular offset, in UV space.
250
+ var finalUv = uv;
251
+ if isHorizontal {
252
+ finalUv.y += pixelOffset * stepLength;
253
+ } else {
254
+ finalUv.x += pixelOffset * stepLength;
255
+ }
256
+
257
+ let finalColor = sampleColor(finalUv);
258
+ return vec4<f32>(linearToSrgbOetf(finalColor), 1.0);
259
+ }
@@ -0,0 +1,186 @@
1
+ // hdrp-cluster-forward.wgsl — HDRP cluster-forward punctual light evaluation.
2
+ // feat-20260608-cluster-lighting M4 / w17.
3
+ //
4
+ // The cluster module owns membership and raw payload decoding only. Punctual
5
+ // BRDF, range, cone, PCF, and fail-open behavior live in lighting-punctual.
6
+
7
+ #define_import_path forgeax_hdrp::cluster_forward
8
+
9
+ #import forgeax_pbr::lighting_punctual::{evalPoint, evalSpot, evalSpotShadowed}
10
+ #import forgeax_view::common::{view}
11
+ #ifdef POINT_SHADOW_AVAILABLE
12
+ #import forgeax_pbr::lighting_punctual::{evalPointShadowed}
13
+ #endif
14
+
15
+ #ifdef CLUSTER_FORWARD_AVAILABLE
16
+
17
+ const KIND_POINT: u32 = 0u;
18
+ const KIND_SPOT: u32 = 1u;
19
+
20
+ // LightSlot is the byte-frozen 64B std430/std140 transport contract.
21
+ // [0..2] position, [3] invRangeSquared
22
+ // [4..6] colorTimesIntensity, [7] cosInner
23
+ // [8..10] direction, [11] cosOuter
24
+ // [12] raw kind u32, [13] shadow identity i32
25
+ // [14..15] point near/far f32 bits; spot keeps these lanes zero.
26
+ struct LightSlot {
27
+ position : vec4<f32>,
28
+ color : vec4<f32>,
29
+ direction : vec4<f32>,
30
+ kind_and_shadow : vec4<u32>,
31
+ };
32
+
33
+ const LIGHTSLOT_BYTE_SIZE: u32 = 64u;
34
+
35
+ struct ClusterUniform {
36
+ grid : vec4<u32>,
37
+ near_far_log : vec4<f32>,
38
+ };
39
+
40
+ #if STORAGE_BUFFER_AVAILABLE == true
41
+ @group(2) @binding(3) var<storage, read> light_data : array<LightSlot, 256>;
42
+ @group(2) @binding(4) var<storage, read> cluster_grid : array<u32>;
43
+ @group(2) @binding(5) var<storage, read> light_index_list : array<u32>;
44
+ #else
45
+ @group(2) @binding(3) var<uniform> light_data_uniform : array<LightSlot, 128>;
46
+ #endif
47
+ @group(2) @binding(6) var<uniform> cluster_uniform : ClusterUniform;
48
+
49
+ fn get_ssao_intensity() -> f32 {
50
+ return cluster_uniform.near_far_log.w;
51
+ }
52
+
53
+ fn view_z_to_z_slice(
54
+ view_z : f32,
55
+ grid_z : u32,
56
+ near : f32,
57
+ far : f32,
58
+ log_far_over_near : f32,
59
+ ) -> u32 {
60
+ if (view_z >= -near) {
61
+ return 0u;
62
+ }
63
+ let slice = floor(log(-view_z / near) / log_far_over_near * f32(grid_z));
64
+ let u_slice = u32(slice);
65
+ if (u_slice >= grid_z) {
66
+ return grid_z - 1u;
67
+ }
68
+ return u_slice;
69
+ }
70
+
71
+ fn ndc_position_to_cluster(
72
+ ndc : vec3<f32>,
73
+ view_z : f32,
74
+ grid_x : u32,
75
+ grid_y : u32,
76
+ grid_z : u32,
77
+ near : f32,
78
+ far : f32,
79
+ log_far : f32,
80
+ ) -> vec3<u32> {
81
+ let cx = clamp(u32(floor((ndc.x * 0.5 + 0.5) * f32(grid_x))), 0u, grid_x - 1u);
82
+ let cy = clamp(u32(floor((ndc.y * 0.5 + 0.5) * f32(grid_y))), 0u, grid_y - 1u);
83
+ let cz = view_z_to_z_slice(view_z, grid_z, near, far, log_far);
84
+ return vec3(cx, cy, cz);
85
+ }
86
+
87
+ // Decode one kind first, then delegate the complete evaluation to the shared
88
+ // punctual owner. Unknown kinds are explicitly zero contribution.
89
+ fn evaluate_cluster_light(
90
+ light : LightSlot,
91
+ world_pos : vec3<f32>,
92
+ normal : vec3<f32>,
93
+ view_dir : vec3<f32>,
94
+ base_color : vec3<f32>,
95
+ metallic : f32,
96
+ alpha_sq : f32,
97
+ f0 : vec3<f32>,
98
+ ) -> vec3<f32> {
99
+ let kind = light.kind_and_shadow.x;
100
+ if (kind == KIND_POINT) {
101
+ #ifdef POINT_SHADOW_AVAILABLE
102
+ let layer = bitcast<i32>(light.kind_and_shadow.y);
103
+ if (layer >= 0) {
104
+ return evalPointShadowed(
105
+ light.position.xyz, light.color.xyz, light.position.w,
106
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
107
+ layer,
108
+ bitcast<f32>(light.kind_and_shadow.z),
109
+ bitcast<f32>(light.kind_and_shadow.w),
110
+ 0.005, 0.05,
111
+ );
112
+ }
113
+ #endif
114
+ return evalPoint(
115
+ light.position.xyz, light.color.xyz, light.position.w,
116
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
117
+ );
118
+ }
119
+ if (kind == KIND_SPOT) {
120
+ let tile = bitcast<i32>(light.kind_and_shadow.y);
121
+ if (tile >= 0) {
122
+ return evalSpotShadowed(
123
+ light.position.xyz, light.direction.xyz, light.color.xyz,
124
+ // LightSlot packs cosInner in color.w and cosOuter in direction.w;
125
+ // keep the evaluator's named cone order intact at the decode boundary.
126
+ light.color.w, light.direction.w, light.position.w,
127
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
128
+ view.spotLightViewProj[tile], tile, 0.005, 0.05,
129
+ );
130
+ }
131
+ return evalSpot(
132
+ light.position.xyz, light.direction.xyz, light.color.xyz,
133
+ light.color.w, light.direction.w, light.position.w,
134
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
135
+ );
136
+ }
137
+ return vec3<f32>(0.0);
138
+ }
139
+
140
+ fn evaluate_cluster_lights(
141
+ ndc : vec3<f32>,
142
+ view_z : f32,
143
+ world_pos : vec3<f32>,
144
+ normal : vec3<f32>,
145
+ view_dir : vec3<f32>,
146
+ base_color : vec3<f32>,
147
+ metallic : f32,
148
+ alpha_sq : f32,
149
+ f0 : vec3<f32>,
150
+ ) -> vec3<f32> {
151
+ let gx = cluster_uniform.grid.x;
152
+ let gy = cluster_uniform.grid.y;
153
+ let gz = cluster_uniform.grid.z;
154
+ let near = cluster_uniform.near_far_log.x;
155
+ let far = cluster_uniform.near_far_log.y;
156
+ let log_far = cluster_uniform.near_far_log.z;
157
+ var total_radiance = vec3<f32>(0.0);
158
+
159
+ #if STORAGE_BUFFER_AVAILABLE == true
160
+ let cluster_idx = ndc_position_to_cluster(ndc, view_z, gx, gy, gz, near, far, log_far);
161
+ let cluster_linear = cluster_idx.z * gy * gx + cluster_idx.y * gx + cluster_idx.x;
162
+ let grid_offset = cluster_linear * 2u;
163
+ let list_offset = cluster_grid[grid_offset];
164
+ let list_count = cluster_grid[grid_offset + 1u];
165
+ for (var i = 0u; i < list_count; i = i + 1u) {
166
+ let light = light_data[light_index_list[list_offset + i]];
167
+ total_radiance += evaluate_cluster_light(
168
+ light, world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
169
+ );
170
+ }
171
+ #else
172
+ let light_count = min(cluster_uniform.grid.w, 128u);
173
+ for (var i = 0u; i < 128u; i = i + 1u) {
174
+ if (i >= light_count) {
175
+ break;
176
+ }
177
+ total_radiance += evaluate_cluster_light(
178
+ light_data_uniform[i], world_pos, normal, view_dir,
179
+ base_color, metallic, alpha_sq, f0,
180
+ );
181
+ }
182
+ #endif
183
+ return total_radiance;
184
+ }
185
+
186
+ #endif // CLUSTER_FORWARD_AVAILABLE