@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.
- package/LICENSE +202 -0
- package/README.md +99 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/ShaderRegistry.d.ts +208 -0
- package/dist/ShaderRegistry.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-background.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-background.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-preetham-red.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-preetham.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-preetham.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +1 -0
- package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +2 -0
- package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +2 -0
- package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +1 -0
- package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +2 -0
- package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +1 -0
- package/dist/__tests__/deferred-lighting-ssao.test.d.ts +2 -0
- package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +1 -0
- package/dist/__tests__/fog-oracle.unit.test.d.ts +2 -0
- package/dist/__tests__/fog-oracle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +2 -0
- package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +2 -0
- package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-ssao-compile.test.d.ts +2 -0
- package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +1 -0
- package/dist/__tests__/ibl-irradiance.unit.test.d.ts +2 -0
- package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ibl-sampling.unit.test.d.ts +2 -0
- package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lighting-punctual.unit.test.d.ts +2 -0
- package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +2 -0
- package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-builtins.unit.test.d.ts +2 -0
- package/dist/__tests__/material-builtins.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/material-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-derived-builtins.integration.test.d.ts +2 -0
- package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-temporal.unit.test.d.ts +2 -0
- package/dist/__tests__/scene-temporal.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +2 -0
- package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shader.unit.test.d.ts +2 -0
- package/dist/__tests__/shader.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-shader.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-shader.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-variants.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-variants.unit.test.d.ts.map +1 -0
- package/dist/__tests__/taa-resolve.unit.test.d.ts +2 -0
- package/dist/__tests__/taa-resolve.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transparent-pbr.unit.test.d.ts +2 -0
- package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vertex-color-variant.unit.test.d.ts +2 -0
- package/dist/__tests__/vertex-color-variant.unit.test.d.ts.map +1 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +560 -0
- package/dist/index.mjs.map +1 -0
- package/dist/material/artifact-registry.d.ts +31 -0
- package/dist/material/artifact-registry.d.ts.map +1 -0
- package/dist/material/artifact-types.d.ts +13 -0
- package/dist/material/artifact-types.d.ts.map +1 -0
- package/dist/material-schemas.d.ts +8 -0
- package/dist/material-schemas.d.ts.map +1 -0
- package/dist/register-default-sprite-lit.d.ts +40 -0
- package/dist/register-default-sprite-lit.d.ts.map +1 -0
- package/dist/register-default-standard-pbr-skin.d.ts +34 -0
- package/dist/register-default-standard-pbr-skin.d.ts.map +1 -0
- package/dist/tonemap.d.ts +13 -0
- package/dist/tonemap.d.ts.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/ShaderRegistry.ts +519 -0
- package/src/__tests__/atmosphere-background.test.ts +33 -0
- package/src/__tests__/atmosphere-cubemap-orientation.test.ts +68 -0
- package/src/__tests__/atmosphere-preetham-numeric.test.ts +122 -0
- package/src/__tests__/atmosphere-preetham-red.test.ts +52 -0
- package/src/__tests__/atmosphere-preetham.test.ts +132 -0
- package/src/__tests__/atmosphere-three-r184-reference.test.ts +122 -0
- package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +31 -0
- package/src/__tests__/builtin-texture-sampling-contract.test.ts +56 -0
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +19 -0
- package/src/__tests__/deferred-lighting-ssao.test.ts +114 -0
- package/src/__tests__/fog-oracle.unit.test.ts +132 -0
- package/src/__tests__/fog-orthographic-ray.unit.test.ts +62 -0
- package/src/__tests__/fog-producer-matrix.integration.test.ts +77 -0
- package/src/__tests__/hdrp-ssao-compile.test.ts +185 -0
- package/src/__tests__/ibl-irradiance.unit.test.ts +43 -0
- package/src/__tests__/ibl-sampling.unit.test.ts +40 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +66 -0
- package/src/__tests__/manifest.fixture.json +22 -0
- package/src/__tests__/material-artifact-dedup.unit.test.ts +21 -0
- package/src/__tests__/material-builtins.unit.test.ts +111 -0
- package/src/__tests__/material-contract.unit.test.ts +79 -0
- package/src/__tests__/material-derived-builtins.integration.test.ts +30 -0
- package/src/__tests__/public-surface.unit.test.ts +9 -0
- package/src/__tests__/scene-temporal.unit.test.ts +32 -0
- package/src/__tests__/shader-registry-transaction.unit.test.ts +116 -0
- package/src/__tests__/shader.unit.test.ts +1909 -0
- package/src/__tests__/sprite-lit-shader.test.ts +240 -0
- package/src/__tests__/sprite-variants.unit.test.ts +209 -0
- package/src/__tests__/taa-resolve.unit.test.ts +32 -0
- package/src/__tests__/transparent-pbr.unit.test.ts +21 -0
- package/src/__tests__/vertex-color-variant.unit.test.ts +52 -0
- package/src/atmosphere-background.wgsl +80 -0
- package/src/atmosphere-cubemap.wgsl +72 -0
- package/src/atmosphere-ibl.wgsl +127 -0
- package/src/atmosphere-preetham.wgsl +214 -0
- package/src/bloom-blur.wgsl +89 -0
- package/src/bloom-bright.wgsl +50 -0
- package/src/bloom-composite.wgsl +54 -0
- package/src/brdf.wgsl +25 -0
- package/src/common.wgsl +523 -0
- package/src/default-standard-pbr-skin.wgsl +538 -0
- package/src/default-standard-pbr.wgsl +794 -0
- package/src/errors.ts +169 -0
- package/src/fog.wgsl +27 -0
- package/src/fxaa.wgsl +259 -0
- package/src/hdrp-cluster-forward.wgsl +186 -0
- package/src/hdrp-deferred-lighting.wgsl +69 -0
- package/src/hdrp-ssao.wgsl +174 -0
- package/src/ibl-brdf-lut.wgsl +92 -0
- package/src/ibl-equirect-to-cube.wgsl +56 -0
- package/src/ibl-irradiance.wgsl +89 -0
- package/src/ibl-prefilter.wgsl +101 -0
- package/src/ibl-sampling.wgsl +58 -0
- package/src/ibl-shared.wgsl +118 -0
- package/src/index.ts +166 -0
- package/src/lighting-directional.wgsl +471 -0
- package/src/lighting-punctual.wgsl +274 -0
- package/src/material/artifact-registry.ts +78 -0
- package/src/material/artifact-types.ts +26 -0
- package/src/material-schemas.ts +48 -0
- package/src/msdf-text.wgsl +208 -0
- package/src/pbr-temporal.wgsl +73 -0
- package/src/register-default-sprite-lit.ts +87 -0
- package/src/register-default-standard-pbr-skin.ts +74 -0
- package/src/shadow-pcf.wgsl +120 -0
- package/src/shadow_caster.wgsl +54 -0
- package/src/skybox.wgsl +75 -0
- package/src/sprite-lit.wgsl +307 -0
- package/src/sprite.wgsl +365 -0
- package/src/taa-resolve.wgsl +129 -0
- package/src/tbn.wgsl +54 -0
- package/src/tonemap.ts +13 -0
- package/src/tonemap.wgsl +177 -0
- package/src/types.ts +59 -0
- package/src/unlit.wgsl +186 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::lighting_punctual
|
|
2
|
+
|
|
3
|
+
// @forgeax/engine-shader - lighting-punctual.wgsl
|
|
4
|
+
// (feat-20260523-shader-template-instance-split M5 / T02).
|
|
5
|
+
//
|
|
6
|
+
// Point + spot light evaluators extracted from pbr.wgsl
|
|
7
|
+
// (feat-20260519-light-casters-point-spot-pbr M4 / w22 byte-equivalent
|
|
8
|
+
// extraction). Both light types share a punctual BRDF body (GGX specular +
|
|
9
|
+
// Lambertian diffuse + Three r184 squared finite-range attenuation); the
|
|
10
|
+
// only difference is that SpotLight multiplies a cone-falloff factor
|
|
11
|
+
// `smoothstep(cosOuter, cosInner, dot(l, -lightDir))` on top.
|
|
12
|
+
//
|
|
13
|
+
// charter P4 consistent abstraction: one body, two thin wrappers that each
|
|
14
|
+
// carry exactly the parameters their light type needs. evalPoint avoids the
|
|
15
|
+
// "evalPunctual(cosInner=1, cosOuter=-1, ...)" magic-value collapse pattern
|
|
16
|
+
// since `smoothstep(-1, 1, x)` is the Hermite cubic 0..1 (not a constant 1).
|
|
17
|
+
//
|
|
18
|
+
// Range attenuation (Three r184 squared finite-range authority):
|
|
19
|
+
// atten = clamp(1 - (d^2 * invR^2)^2, 0, 1)^2 / max(d^2, 1e-4)
|
|
20
|
+
// `max(d^2, 1e-4)` math safety net keeps the divisor finite when the
|
|
21
|
+
// fragment is at the light position (zero-distance NaN guard, layer 2 of
|
|
22
|
+
// the two-layer fail-fast strategy alongside the host-side bounds gate).
|
|
23
|
+
// `invRangeSquared = 0` collapses the quartic falloff to a pure 1/d^2 law.
|
|
24
|
+
//
|
|
25
|
+
// Pure-function module aside from the brdf #import; takes all light + surface
|
|
26
|
+
// parameters as args so the helper does not declare its own bindings (host
|
|
27
|
+
// material shader owns the @group(0) light buffer namespace).
|
|
28
|
+
//
|
|
29
|
+
// Exports:
|
|
30
|
+
// - evalPoint(lightPos, colorTimesIntensity, invRangeSquared, ...) -> vec3<f32>
|
|
31
|
+
// - evalSpot(lightPos, lightDir, colorTimesIntensity, cosInner, cosOuter,
|
|
32
|
+
// invRangeSquared, ...) -> vec3<f32>
|
|
33
|
+
|
|
34
|
+
#import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
|
|
35
|
+
// feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-5):
|
|
36
|
+
// spot shadow sampling reuses the shared 2D 9-tap PCF core (sample_shadow_2d)
|
|
37
|
+
// and the always-on `spotShadowMap` (binding 8) + `shadowSampler` (binding 4).
|
|
38
|
+
// `shadowSampler` is imported UNCONDITIONALLY here (spot is always-on, D-5):
|
|
39
|
+
// the point-shadow #ifdef block below must NOT re-import it (double import).
|
|
40
|
+
#import forgeax_pbr::shadow_pcf::{sample_shadow_2d}
|
|
41
|
+
#import forgeax_view::common::{spotShadowMap, shadowSampler}
|
|
42
|
+
#ifdef POINT_SHADOW_AVAILABLE
|
|
43
|
+
#import forgeax_pbr::shadow_pcf::{sample_shadow_cube_hw2x2}
|
|
44
|
+
// Pull in the @group(0) @binding(5) shadowAtlas declaration from common.wgsl so
|
|
45
|
+
// the free-identifier references in `evalPointShadowed` resolve through
|
|
46
|
+
// naga_oil's import scope. (`shadowSampler` is already imported above for the
|
|
47
|
+
// always-on spot path.)
|
|
48
|
+
#import forgeax_view::common::{shadowAtlas}
|
|
49
|
+
#endif
|
|
50
|
+
|
|
51
|
+
// Shared punctual BRDF body returning (diffuse + specular) *
|
|
52
|
+
// colorTimesIntensity * nDotL * attenuation. Cone factor is applied by the
|
|
53
|
+
// caller (evalSpot only).
|
|
54
|
+
fn evalPunctualBody(
|
|
55
|
+
lightPos : vec3<f32>,
|
|
56
|
+
colorTimesIntensity : vec3<f32>,
|
|
57
|
+
invRangeSquared : f32,
|
|
58
|
+
worldPos : vec3<f32>,
|
|
59
|
+
normal : vec3<f32>,
|
|
60
|
+
viewDir : vec3<f32>,
|
|
61
|
+
baseColor : vec3<f32>,
|
|
62
|
+
metallic : f32,
|
|
63
|
+
alphaSq : f32,
|
|
64
|
+
F0 : vec3<f32>,
|
|
65
|
+
) -> vec3<f32> {
|
|
66
|
+
let toLight = lightPos - worldPos;
|
|
67
|
+
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
68
|
+
let l = toLight / sqrt(dSquared);
|
|
69
|
+
let h = normalize(viewDir + l);
|
|
70
|
+
let nDotL = max(dot(normal, l), 0.0);
|
|
71
|
+
let nDotV = max(dot(normal, viewDir), 1e-5);
|
|
72
|
+
let nDotH = max(dot(normal, h), 0.0);
|
|
73
|
+
let vDotH = max(dot(viewDir, h), 0.0);
|
|
74
|
+
let f = f_schlick(vDotH, F0);
|
|
75
|
+
let specular = d_ggx(nDotH, alphaSq) * v_smith(nDotV, nDotL, alphaSq) * f;
|
|
76
|
+
let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);
|
|
77
|
+
let diffuse = kd * baseColor / 3.14159265;
|
|
78
|
+
let factor = max(min(1.0 - (dSquared * invRangeSquared) * (dSquared * invRangeSquared), 1.0), 0.0);
|
|
79
|
+
let attenuation = factor * factor / dSquared;
|
|
80
|
+
return (diffuse + specular) * colorTimesIntensity * nDotL * attenuation;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Omnidirectional point light: no cone factor.
|
|
84
|
+
fn evalPoint(
|
|
85
|
+
lightPos : vec3<f32>,
|
|
86
|
+
colorTimesIntensity : vec3<f32>,
|
|
87
|
+
invRangeSquared : f32,
|
|
88
|
+
worldPos : vec3<f32>,
|
|
89
|
+
normal : vec3<f32>,
|
|
90
|
+
viewDir : vec3<f32>,
|
|
91
|
+
baseColor : vec3<f32>,
|
|
92
|
+
metallic : f32,
|
|
93
|
+
alphaSq : f32,
|
|
94
|
+
F0 : vec3<f32>,
|
|
95
|
+
) -> vec3<f32> {
|
|
96
|
+
return evalPunctualBody(
|
|
97
|
+
lightPos, colorTimesIntensity, invRangeSquared,
|
|
98
|
+
worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#ifdef POINT_SHADOW_AVAILABLE
|
|
103
|
+
// Shadow-modulated omnidirectional point light: same BRDF body * shadow factor.
|
|
104
|
+
//
|
|
105
|
+
// feat-20260612-point-light-shadows-urp-hdrp M3 / T-M3-3 + M4 / T-M4-3
|
|
106
|
+
// (plan-strategy §D-1 + §D-8). Only emitted when POINT_SHADOW_AVAILABLE is
|
|
107
|
+
// true (forward path with the cube_array atlas at @group(0) binding 5).
|
|
108
|
+
// The shadow factor is reconstructed from `lightLocal` via the largest-axis
|
|
109
|
+
// projection (research L0.5 Bevy pattern); the caller passes `near` / `far`
|
|
110
|
+
// directly so both pipelines route the same constants without owning the
|
|
111
|
+
// upstream binding (URP reads them from `shadowParams[layer]` at @group(0)
|
|
112
|
+
// binding 6; HDRP unpacks them off `LightSlot.kind_and_pad.zw` per
|
|
113
|
+
// plan-strategy §D-8).
|
|
114
|
+
//
|
|
115
|
+
// Caller responsibility: gate this on `shadowAtlasLayer >= 0` so the
|
|
116
|
+
// no-shadow lights stay on the unshadowed `evalPoint` path; passing a
|
|
117
|
+
// negative layer here is undefined (the cube_array view rejects it).
|
|
118
|
+
fn evalPointShadowed(
|
|
119
|
+
lightPos : vec3<f32>,
|
|
120
|
+
colorTimesIntensity : vec3<f32>,
|
|
121
|
+
invRangeSquared : f32,
|
|
122
|
+
worldPos : vec3<f32>,
|
|
123
|
+
normal : vec3<f32>,
|
|
124
|
+
viewDir : vec3<f32>,
|
|
125
|
+
baseColor : vec3<f32>,
|
|
126
|
+
metallic : f32,
|
|
127
|
+
alphaSq : f32,
|
|
128
|
+
F0 : vec3<f32>,
|
|
129
|
+
shadowAtlasLayer : i32,
|
|
130
|
+
near : f32,
|
|
131
|
+
far : f32,
|
|
132
|
+
depthBias : f32,
|
|
133
|
+
normalBias : f32,
|
|
134
|
+
) -> vec3<f32> {
|
|
135
|
+
let lit = evalPunctualBody(
|
|
136
|
+
lightPos, colorTimesIntensity, invRangeSquared,
|
|
137
|
+
worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
|
|
138
|
+
);
|
|
139
|
+
// Fragment-to-light direction; cubemap sample uses the local-space
|
|
140
|
+
// direction (research L0.5: Bevy convention). For a right-handed world,
|
|
141
|
+
// the cubemap convention flips Z so the +Z face look direction matches.
|
|
142
|
+
let toLight = lightPos - worldPos;
|
|
143
|
+
// Cubemap sample direction is from-fragment-to-light (Bevy + LearnOpenGL),
|
|
144
|
+
// negated to fragment-from-light when reconstructing the depth ref.
|
|
145
|
+
let lightLocal = vec3<f32>(toLight.x, toLight.y, -toLight.z);
|
|
146
|
+
// Reconstruct [0,1] NDC depth from world-space distance: largest-axis
|
|
147
|
+
// projection (research L0.5). Match the per-face perspective near / far
|
|
148
|
+
// configured by buildPointShadowMatrices (PointLightShadow.nearPlane /
|
|
149
|
+
// farPlane on the host).
|
|
150
|
+
let absV = abs(vec3<f32>(toLight.x, toLight.y, toLight.z));
|
|
151
|
+
let largestAxis = max(absV.x, max(absV.y, absV.z));
|
|
152
|
+
// Perspective z-NDC reconstruction for the largest axis as the eye-space
|
|
153
|
+
// -z component (cube face look direction is the +axis the absolute value
|
|
154
|
+
// selected). z_ndc = far * (largest - near) / (largest * (far - near)).
|
|
155
|
+
let denom = max(largestAxis * (far - near), 1e-6);
|
|
156
|
+
let depthRef = clamp(far * (largestAxis - near) / denom, 0.0, 1.0);
|
|
157
|
+
let nDotL = max(dot(normal, normalize(toLight)), 0.0);
|
|
158
|
+
let shadowFactor = sample_shadow_cube_hw2x2(
|
|
159
|
+
shadowAtlas, shadowSampler, lightLocal, shadowAtlasLayer,
|
|
160
|
+
depthRef, depthBias, normalBias, nDotL,
|
|
161
|
+
);
|
|
162
|
+
return lit * shadowFactor;
|
|
163
|
+
}
|
|
164
|
+
#endif
|
|
165
|
+
|
|
166
|
+
// Cone-restricted spot light: BRDF body * smoothstep cone factor.
|
|
167
|
+
// `cosInner` / `cosOuter` are pre-computed cosines (host-side
|
|
168
|
+
// degree -> cosine conversion in extract-frame; plan-strategy D-S2).
|
|
169
|
+
fn evalSpot(
|
|
170
|
+
lightPos : vec3<f32>,
|
|
171
|
+
lightDir : vec3<f32>,
|
|
172
|
+
colorTimesIntensity : vec3<f32>,
|
|
173
|
+
cosInner : f32,
|
|
174
|
+
cosOuter : f32,
|
|
175
|
+
invRangeSquared : f32,
|
|
176
|
+
worldPos : vec3<f32>,
|
|
177
|
+
normal : vec3<f32>,
|
|
178
|
+
viewDir : vec3<f32>,
|
|
179
|
+
baseColor : vec3<f32>,
|
|
180
|
+
metallic : f32,
|
|
181
|
+
alphaSq : f32,
|
|
182
|
+
F0 : vec3<f32>,
|
|
183
|
+
) -> vec3<f32> {
|
|
184
|
+
let body = evalPunctualBody(
|
|
185
|
+
lightPos, colorTimesIntensity, invRangeSquared,
|
|
186
|
+
worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
|
|
187
|
+
);
|
|
188
|
+
let toLight = lightPos - worldPos;
|
|
189
|
+
let l = normalize(toLight);
|
|
190
|
+
let cone = smoothstep(cosOuter, cosInner, dot(l, -lightDir));
|
|
191
|
+
return body * cone;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-4 +
|
|
195
|
+
// D-5). Shadow-modulated spot light: the unshadowed `evalSpot` result times a
|
|
196
|
+
// PCF shadow factor sampled from the spot's perspective depth-atlas tile.
|
|
197
|
+
//
|
|
198
|
+
// Mirrors `evalPointShadowed`'s "shadowed wrapper + upstream gate" pattern
|
|
199
|
+
// (research Finding B3): the caller gates on `shadowAtlasTile >= 0` so
|
|
200
|
+
// no-shadow / clipped / direction-degenerate spots (tile = -1, plan D-4) stay
|
|
201
|
+
// on the unshadowed `evalSpot` path.
|
|
202
|
+
//
|
|
203
|
+
// Depth-ref reconstruction is the standard perspective `splane.z / splane.w`
|
|
204
|
+
// non-linear depth (plan-strategy D-4, godot-point-spot-shadows wiki S3.4):
|
|
205
|
+
// store-side and sample-side share the SAME perspective `lightViewProj`, so the
|
|
206
|
+
// projection's non-linearity cancels and no near/far reconstruction is needed
|
|
207
|
+
// (unlike the point cube path's largest-axis projection).
|
|
208
|
+
//
|
|
209
|
+
// Atlas tiling: the host packs up to 4 spot shadows into a 2x2 grid of one
|
|
210
|
+
// `spotShadowDepth` texture (urp-pipeline.ts). Tile N occupies quadrant
|
|
211
|
+
// (col = N % 2, row = N / 2); the [0,1] light-clip UV is scaled to a 0.5x0.5
|
|
212
|
+
// sub-rect and offset to the tile origin. PCF taps stay inside the tile by
|
|
213
|
+
// scaling the texel step to the half-resolution sub-rect.
|
|
214
|
+
//
|
|
215
|
+
// OOB / NaN gate (research Finding F1, mirrors lighting-directional.wgsl): a
|
|
216
|
+
// degenerate `lightViewProj` (near-zero spot direction) yields NaN UVs; the
|
|
217
|
+
// `>= 0 && <= 1` form is false for NaN, so the fragment returns fully lit
|
|
218
|
+
// (shadowFactor = 1.0) instead of a hard-black artifact.
|
|
219
|
+
fn evalSpotShadowed(
|
|
220
|
+
lightPos : vec3<f32>,
|
|
221
|
+
lightDir : vec3<f32>,
|
|
222
|
+
colorTimesIntensity : vec3<f32>,
|
|
223
|
+
cosInner : f32,
|
|
224
|
+
cosOuter : f32,
|
|
225
|
+
invRangeSquared : f32,
|
|
226
|
+
worldPos : vec3<f32>,
|
|
227
|
+
normal : vec3<f32>,
|
|
228
|
+
viewDir : vec3<f32>,
|
|
229
|
+
baseColor : vec3<f32>,
|
|
230
|
+
metallic : f32,
|
|
231
|
+
alphaSq : f32,
|
|
232
|
+
F0 : vec3<f32>,
|
|
233
|
+
lightViewProj : mat4x4<f32>,
|
|
234
|
+
shadowAtlasTile : i32,
|
|
235
|
+
depthBias : f32,
|
|
236
|
+
normalBias : f32,
|
|
237
|
+
) -> vec3<f32> {
|
|
238
|
+
let body = evalSpot(
|
|
239
|
+
lightPos, lightDir, colorTimesIntensity, cosInner, cosOuter, invRangeSquared,
|
|
240
|
+
worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Project the fragment into the spot's light clip space.
|
|
244
|
+
let splane = lightViewProj * vec4<f32>(worldPos, 1.0);
|
|
245
|
+
// Perspective divide; guard a zero/near-zero w (fragment behind the light or
|
|
246
|
+
// a degenerate matrix) so the OOB gate below catches it as fully lit.
|
|
247
|
+
let invW = select(1.0 / splane.w, 0.0, abs(splane.w) < 1e-6);
|
|
248
|
+
let ndcXY = splane.xy * invW;
|
|
249
|
+
let depthRef = splane.z * invW;
|
|
250
|
+
// Clip-space [-1,1] -> texture UV [0,1] with the standard Y flip.
|
|
251
|
+
let clipUv = vec2<f32>(ndcXY.x * 0.5 + 0.5, ndcXY.y * -0.5 + 0.5);
|
|
252
|
+
|
|
253
|
+
// OOB / NaN gate: outside the light frustum (or NaN from a degenerate matrix)
|
|
254
|
+
// returns fully lit. Mirrors the directional `>= 0 && <= 1` NaN-safe form.
|
|
255
|
+
if (!(clipUv.x >= 0.0 && clipUv.x <= 1.0 && clipUv.y >= 0.0 && clipUv.y <= 1.0 && depthRef <= 1.0)) {
|
|
256
|
+
return body;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Map the [0,1] light-clip UV into the spot's 2x2 atlas tile sub-rect.
|
|
260
|
+
let col = f32(shadowAtlasTile % 2);
|
|
261
|
+
let row = f32(shadowAtlasTile / 2);
|
|
262
|
+
let tileOrigin = vec2<f32>(col, row) * 0.5;
|
|
263
|
+
let atlasUv = clipUv * 0.5 + tileOrigin;
|
|
264
|
+
|
|
265
|
+
// texel step within the half-resolution sub-rect (atlas is 2x tile size).
|
|
266
|
+
let atlasDims = vec2<f32>(textureDimensions(spotShadowMap, 0));
|
|
267
|
+
let texel = vec2<f32>(1.0, 1.0) / atlasDims;
|
|
268
|
+
|
|
269
|
+
let nDotL = max(dot(normal, normalize(lightPos - worldPos)), 0.0);
|
|
270
|
+
let shadowFactor = sample_shadow_2d(
|
|
271
|
+
spotShadowMap, shadowSampler, atlasUv, texel, depthRef, normalBias, depthBias, nDotL,
|
|
272
|
+
);
|
|
273
|
+
return body * shadowFactor;
|
|
274
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
|
|
3
|
+
export interface MaterialRuntimeArtifact {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
readonly digest?: string;
|
|
7
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MaterialArtifactInspection {
|
|
11
|
+
readonly key: string;
|
|
12
|
+
readonly digest?: string;
|
|
13
|
+
readonly byteLength: number;
|
|
14
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MaterialArtifactConflictError {
|
|
18
|
+
readonly code: 'material-artifact-conflict';
|
|
19
|
+
readonly expected: string;
|
|
20
|
+
readonly hint: string;
|
|
21
|
+
readonly detail: {
|
|
22
|
+
readonly key: string;
|
|
23
|
+
readonly existingDigest?: string;
|
|
24
|
+
readonly incomingDigest?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function equalBytes(left: Uint8Array, right: Uint8Array): boolean {
|
|
29
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class MaterialArtifactRegistry {
|
|
33
|
+
readonly #artifacts = new Map<string, MaterialRuntimeArtifact>();
|
|
34
|
+
|
|
35
|
+
register(
|
|
36
|
+
artifact: MaterialRuntimeArtifact,
|
|
37
|
+
): Result<MaterialRuntimeArtifact, MaterialArtifactConflictError> {
|
|
38
|
+
const previous = this.#artifacts.get(artifact.key);
|
|
39
|
+
if (previous !== undefined) {
|
|
40
|
+
if (equalBytes(previous.bytes, artifact.bytes)) return ok(previous);
|
|
41
|
+
return err({
|
|
42
|
+
code: 'material-artifact-conflict',
|
|
43
|
+
expected: 'one immutable artifact byte sequence per specialization key',
|
|
44
|
+
hint: 're-cook the conflicting specialization and publish one artifact digest',
|
|
45
|
+
detail: {
|
|
46
|
+
key: artifact.key,
|
|
47
|
+
...(previous.digest ? { existingDigest: previous.digest } : {}),
|
|
48
|
+
...(artifact.digest ? { incomingDigest: artifact.digest } : {}),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const immutable = Object.freeze(artifact);
|
|
53
|
+
this.#artifacts.set(artifact.key, immutable);
|
|
54
|
+
return ok(immutable);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get(key: string): MaterialRuntimeArtifact | undefined {
|
|
58
|
+
return this.#artifacts.get(key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
inspect(key: string): MaterialArtifactInspection | undefined {
|
|
62
|
+
const artifact = this.#artifacts.get(key);
|
|
63
|
+
if (artifact === undefined) return undefined;
|
|
64
|
+
return {
|
|
65
|
+
key: artifact.key,
|
|
66
|
+
...(artifact.digest === undefined ? {} : { digest: artifact.digest }),
|
|
67
|
+
byteLength: artifact.bytes.byteLength,
|
|
68
|
+
...(artifact.metadata === undefined ? {} : { metadata: artifact.metadata }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function registerMaterialArtifact(
|
|
74
|
+
registry: MaterialArtifactRegistry,
|
|
75
|
+
artifact: MaterialRuntimeArtifact,
|
|
76
|
+
): Result<MaterialRuntimeArtifact, MaterialArtifactConflictError> {
|
|
77
|
+
return registry.register(artifact);
|
|
78
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BindGroupLayoutDescriptor } from '@forgeax/engine-types';
|
|
2
|
+
|
|
3
|
+
export interface MaterialShaderArtifact {
|
|
4
|
+
readonly material: string;
|
|
5
|
+
readonly pass: string;
|
|
6
|
+
readonly wgsl: string;
|
|
7
|
+
readonly layoutIdentity: string;
|
|
8
|
+
readonly bindings: readonly BindGroupLayoutDescriptor[];
|
|
9
|
+
readonly deps: readonly string[];
|
|
10
|
+
readonly vertexInputs: readonly Readonly<Record<string, unknown>>[];
|
|
11
|
+
readonly specializationKey?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isMaterialShaderArtifact(value: unknown): value is MaterialShaderArtifact {
|
|
15
|
+
if (value === null || typeof value !== 'object') return false;
|
|
16
|
+
const artifact = value as Partial<MaterialShaderArtifact>;
|
|
17
|
+
return (
|
|
18
|
+
typeof artifact.material === 'string' &&
|
|
19
|
+
typeof artifact.pass === 'string' &&
|
|
20
|
+
typeof artifact.wgsl === 'string' &&
|
|
21
|
+
typeof artifact.layoutIdentity === 'string' &&
|
|
22
|
+
Array.isArray(artifact.bindings) &&
|
|
23
|
+
Array.isArray(artifact.deps) &&
|
|
24
|
+
Array.isArray(artifact.vertexInputs)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ParamSchemaEntry } from '@forgeax/engine-types';
|
|
2
|
+
|
|
3
|
+
export const STANDARD_PBR_ALPHA_CUTOFF_DEFAULT = 0;
|
|
4
|
+
|
|
5
|
+
/** Shared material contract for the standard PBR and skinned PBR shaders. */
|
|
6
|
+
export const DEFAULT_STANDARD_PBR_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
|
|
7
|
+
{ name: 'baseColor', type: 'color', default: [1, 1, 1, 1] },
|
|
8
|
+
{ name: 'metallic', type: 'f32', default: 0 },
|
|
9
|
+
{ name: 'roughness', type: 'f32', default: 0.5 },
|
|
10
|
+
{ name: 'metallicChannel', type: 'f32', default: 2 },
|
|
11
|
+
{ name: 'roughnessChannel', type: 'f32', default: 1 },
|
|
12
|
+
{ name: 'aoChannel', type: 'f32', default: 0 },
|
|
13
|
+
{ name: 'extraChannel', type: 'f32', default: 0 },
|
|
14
|
+
{ name: 'emissive', type: 'vec3', colorSpace: 'srgb', default: [0, 0, 0] },
|
|
15
|
+
{ name: 'emissiveIntensity', type: 'f32', default: 0 },
|
|
16
|
+
{ name: 'occlusionStrength', type: 'f32', default: 1 },
|
|
17
|
+
{ name: 'alphaCutoff', type: 'f32', default: STANDARD_PBR_ALPHA_CUTOFF_DEFAULT },
|
|
18
|
+
{ name: 'clearcoat', type: 'f32', default: 0 },
|
|
19
|
+
{ name: 'clearcoatRoughness', type: 'f32', default: 0.5 },
|
|
20
|
+
{ name: 'specularTint', type: 'vec3', colorSpace: 'srgb', default: [1, 1, 1] },
|
|
21
|
+
{ name: 'normalScale', type: 'f32', default: 1 },
|
|
22
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
23
|
+
{ name: 'metallicRoughnessTexture', type: 'texture2d' },
|
|
24
|
+
{ name: 'normalTexture', type: 'texture2d' },
|
|
25
|
+
{ name: 'specularTintTexture', type: 'texture2d' },
|
|
26
|
+
{ name: 'emissiveTexture', type: 'texture2d' },
|
|
27
|
+
{ name: 'occlusionTexture', type: 'texture2d' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_UNLIT_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
|
|
31
|
+
{ name: 'baseColor', type: 'color', default: [1, 1, 1, 1] },
|
|
32
|
+
{ name: 'alphaCutoff', type: 'f32', default: 0 },
|
|
33
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_SPRITE_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
|
|
37
|
+
{ name: 'colorTint', type: 'vec4', colorSpace: 'srgb', default: [1, 1, 1, 1] },
|
|
38
|
+
{ name: 'region', type: 'vec4', default: [0, 0, 1, 1] },
|
|
39
|
+
{ name: 'pivotAndSize', type: 'vec4', default: [0.5, 0.5, 1, 1] },
|
|
40
|
+
{ name: 'slicesAndMode', type: 'vec4', default: [0, 0, 0, 0] },
|
|
41
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_MSDF_TEXT_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
|
|
45
|
+
{ name: 'tintColor', type: 'color', default: [1, 1, 1, 1] },
|
|
46
|
+
{ name: 'distanceRange', type: 'vec4', default: [4, 512, 512, 0] },
|
|
47
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
48
|
+
];
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
2
|
+
|
|
3
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, Mesh, view, meshes, sampleMaterialTexture}
|
|
4
|
+
#import forgeax_view::fog::{apply_fog}
|
|
5
|
+
|
|
6
|
+
// @forgeax/engine-shader - msdf-text.wgsl
|
|
7
|
+
// (feat-20260531-world-space-msdf-text-rendering M5 / w20).
|
|
8
|
+
//
|
|
9
|
+
// World-space MSDF text material -- the 5th MaterialAsset shader after
|
|
10
|
+
// 'unlit' / 'standard' / 'sprite'. Text entities are baked into a standard
|
|
11
|
+
// mesh entity (glyph quads in local layout space; position.xy = layout x/y,
|
|
12
|
+
// position.z = 0; uv = atlas UV) and ride the existing forward path via
|
|
13
|
+
// `materialShaderId='forgeax::msdf-text'` (D-7 -- zero new pipelineTag,
|
|
14
|
+
// reuses the 'unlit' pipeline tag + transparent bucket + premultiplied
|
|
15
|
+
// alpha blend ONE / ONE_MINUS_SRC_ALPHA).
|
|
16
|
+
//
|
|
17
|
+
// Anchors:
|
|
18
|
+
// - plan-strategy D-3 (GPU billboard; WGSL has no inverse(), camera basis
|
|
19
|
+
// reconstructed without inverse() -- Finding 5/6/7)
|
|
20
|
+
// - plan-strategy D-7 (premultiplied output vec4(tint.rgb*alpha, alpha);
|
|
21
|
+
// ONE / ONE_MINUS_SRC_ALPHA; writes hdrColor so bloom catches it -- R-7)
|
|
22
|
+
// - knowledge-base/wiki/msdf-text-rendering.md section 6 (median + screenPxRange
|
|
23
|
+
// SSOT) + section 2 (median-of-three corner preservation)
|
|
24
|
+
// - requirements AC-10 (GPU billboard not degenerate to a line) + AC-12
|
|
25
|
+
// (bloom / premultiplied) + C3 (materialShaderId path, zero pipelineTag)
|
|
26
|
+
// + C4 (billboard x pick orientation independence)
|
|
27
|
+
//
|
|
28
|
+
// Billboard construction (D-3 / Finding 5/6/7):
|
|
29
|
+
// The mesh vertex.pos.xy carries the per-glyph layout coordinate (already
|
|
30
|
+
// includes intra-line advance + multi-line \n offset baked by the layout
|
|
31
|
+
// system). The quad is expanded along the camera's right / up basis vectors
|
|
32
|
+
// in WORLD space so the text always faces the camera (Finding 7: the quad
|
|
33
|
+
// normal is the camera forward direction, so "text plane parallel to view
|
|
34
|
+
// ray" never happens -- no NaN / flip flicker). WGSL exposes no inverse()
|
|
35
|
+
// built-in (Finding 5); the camera right / up are reconstructed from the
|
|
36
|
+
// world-space anchor and `view.cameraPos` (a look-at frame) with only
|
|
37
|
+
// normalize / cross -- zero matrix inverse, zero per-frame CPU rewrite of
|
|
38
|
+
// the mesh. `anchor` = the model matrix translation column
|
|
39
|
+
// (meshes[idx].worldFromLocal column 3).
|
|
40
|
+
//
|
|
41
|
+
// Bindings (mirror sprite.wgsl / unlit.wgsl byte-for-byte so the shared
|
|
42
|
+
// 4-BindGroupLayout chain is reused without a per-pipeline BGL; the
|
|
43
|
+
// metallicRoughness / normal slots 3..6 stay declared-but-unused, bound to
|
|
44
|
+
// pipelineState defaults at the host side -- D-1 candidate b):
|
|
45
|
+
//
|
|
46
|
+
// @group(0) @binding(0) view uniform (msdf-text reads
|
|
47
|
+
// worldViewProj +
|
|
48
|
+
// cameraPos)
|
|
49
|
+
// @group(1) @binding(0) material uniform (tintColor vec4
|
|
50
|
+
// + distanceRange
|
|
51
|
+
// f32 + atlas dims)
|
|
52
|
+
// @group(1) @binding(1) baseColorSampler sampler (atlas sampler)
|
|
53
|
+
// @group(1) @binding(2) baseColorTexture texture_2d<f32> (MSDF atlas)
|
|
54
|
+
// @group(2) @binding(0) meshes storage (msdf-text reads
|
|
55
|
+
// meshes[idx]
|
|
56
|
+
// .worldFromLocal
|
|
57
|
+
// translation
|
|
58
|
+
// column = anchor)
|
|
59
|
+
|
|
60
|
+
struct Material {
|
|
61
|
+
// tintColor: per-text color multiplied onto the reconstructed coverage
|
|
62
|
+
// alpha. Maps to the `tintColor` paramSchema entry (default opaque white).
|
|
63
|
+
tintColor : vec4<f32>,
|
|
64
|
+
// distanceRange: atlas-space distance-field width (msdfgen -pxrange output,
|
|
65
|
+
// = sidecar.common.distanceRange). atlasSize.xy carries the atlas pixel
|
|
66
|
+
// dimensions so screenPxRange can convert atlas units to screen pixels.
|
|
67
|
+
// distanceRange in .x; atlasSize in .yz; .w padding (std140 vec4 align).
|
|
68
|
+
distanceRange : vec4<f32>,
|
|
69
|
+
baseColorTextureCoordinatesTransform : vec4<f32>,
|
|
70
|
+
baseColorTextureCoordinatesMetadata : vec4<f32>,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
@group(1) @binding(0) var<uniform> material : Material;
|
|
74
|
+
@group(1) @binding(1) var baseColorSampler : sampler;
|
|
75
|
+
@group(1) @binding(2) var baseColorTexture : texture_2d<f32>;
|
|
76
|
+
// Preserve filtering reflection for the bound texture passed to the helper.
|
|
77
|
+
fn materialTextureFilteringWitness() {
|
|
78
|
+
let base = baseColorTexture;
|
|
79
|
+
let baseWitness = textureSample(base, baseColorSampler, vec2<f32>(0.0));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
struct VsIn {
|
|
83
|
+
@location(0) pos : vec3<f32>,
|
|
84
|
+
@location(1) normal : vec3<f32>,
|
|
85
|
+
@location(2) uv : vec2<f32>,
|
|
86
|
+
@location(3) tangent : vec4<f32>,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
struct VsOut {
|
|
90
|
+
@builtin(position) clip : vec4<f32>,
|
|
91
|
+
@location(0) uv : vec2<f32>,
|
|
92
|
+
@location(1) worldPos : vec3<f32>,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
@vertex
|
|
96
|
+
fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
|
|
97
|
+
// anchor = the entity world-matrix translation column (D-3). The layout
|
|
98
|
+
// system bakes the glyph quads in local layout space (Y-up), so the model
|
|
99
|
+
// matrix is a pure Translate (Rotate / Scale folded into the layout) and
|
|
100
|
+
// its 4th column is the world-space anchor.
|
|
101
|
+
let model = meshes[idx].worldFromLocal;
|
|
102
|
+
let anchor = model[3].xyz;
|
|
103
|
+
// Camera look-at frame (Finding 5/6/7): forward = anchor -> camera. WGSL
|
|
104
|
+
// has no inverse() built-in; the right / up basis is reconstructed from
|
|
105
|
+
// `view.cameraPos` + the world up reference (0,1,0) with normalize / cross
|
|
106
|
+
// only -- zero matrix inverse, the quad normal is always the camera
|
|
107
|
+
// forward direction so the billboard never degenerates to a line.
|
|
108
|
+
let forward = normalize(view.cameraPos - anchor);
|
|
109
|
+
// Guard against the camera looking straight down/up the world-up axis
|
|
110
|
+
// (forward parallel to (0,1,0)): fall back to a +Z up reference so cross()
|
|
111
|
+
// stays well-conditioned. This is the only degenerate case (Finding 7) and
|
|
112
|
+
// is resolved here rather than producing a NaN basis.
|
|
113
|
+
let upRef = select(
|
|
114
|
+
vec3<f32>(0.0, 1.0, 0.0),
|
|
115
|
+
vec3<f32>(0.0, 0.0, 1.0),
|
|
116
|
+
abs(forward.y) > 0.999,
|
|
117
|
+
);
|
|
118
|
+
let right = normalize(cross(upRef, forward));
|
|
119
|
+
let up = cross(forward, right);
|
|
120
|
+
// Expand the glyph quad's local layout coordinate along the world-space
|
|
121
|
+
// right / up basis (Finding 6: billboard-rotate then translate). pos.z is
|
|
122
|
+
// 0 for every baked glyph vertex so it contributes nothing.
|
|
123
|
+
let world_pos = anchor + right * in.pos.x + up * in.pos.y;
|
|
124
|
+
var out : VsOut;
|
|
125
|
+
out.clip = view.worldViewProj * vec4<f32>(world_pos, 1.0);
|
|
126
|
+
out.uv = in.uv;
|
|
127
|
+
out.worldPos = world_pos;
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// median(R, G, B): corner-preserving MSDF reconstruction operator (wiki
|
|
132
|
+
// section 2). Picks the distance supported by two-or-more edges so sharp
|
|
133
|
+
// corners survive bilinear upscaling (median = max(min, min(max)) form).
|
|
134
|
+
fn median(r : f32, g : f32, b : f32) -> f32 {
|
|
135
|
+
return max(min(r, g), min(max(r, g), b));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// screenPxRange: converts the atlas-space distanceRange into a screen-pixel
|
|
139
|
+
// ramp width using fwidth() of the atlas UV (wiki section 3.3 / section 6). The
|
|
140
|
+
// `max(., 1.0)` floor keeps the anti-aliasing ramp at least half a pixel wide
|
|
141
|
+
// (msdfgen README: ramp < 1 px degrades back to a hard step / aliasing).
|
|
142
|
+
fn screen_px_range(uv : vec2<f32>) -> f32 {
|
|
143
|
+
let atlas_dims = material.distanceRange.yz;
|
|
144
|
+
let unit_range = vec2<f32>(material.distanceRange.x) / atlas_dims;
|
|
145
|
+
let screen_tex_size = vec2<f32>(1.0) / fwidth(uv);
|
|
146
|
+
return max(0.5 * dot(unit_range, screen_tex_size), 1.0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
150
|
+
var origin = viewParams.cameraPos;
|
|
151
|
+
var direction = normalize(worldPos - origin);
|
|
152
|
+
var rayDistance = length(worldPos - origin);
|
|
153
|
+
if (viewParams.temporalProjection.z >= 0.5) {
|
|
154
|
+
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
155
|
+
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
156
|
+
let nearPoint = nearH.xyz / nearH.w;
|
|
157
|
+
let farPoint = farH.xyz / farH.w;
|
|
158
|
+
direction = normalize(farPoint - nearPoint);
|
|
159
|
+
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
160
|
+
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
161
|
+
}
|
|
162
|
+
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// fs_main_hdr: outputs linear premultiplied alpha for the rgba16float
|
|
166
|
+
// offscreen target (D-7 / R-7). The tonemap fullscreen pass handles sRGB
|
|
167
|
+
// encoding; writing hdrColor lets the bloom bright-pass catch the text.
|
|
168
|
+
@fragment
|
|
169
|
+
fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
170
|
+
let msd = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv, material.baseColorTextureCoordinatesMetadata.zw).rgb;
|
|
171
|
+
let sd = median(msd.r, msd.g, msd.b);
|
|
172
|
+
// wiki section 3.3: opacity = clamp((sd - 0.5) * screenPxRange + 0.5, 0, 1)
|
|
173
|
+
// (linear ramp; equivalent to smoothstep(0.5 - delta, 0.5 + delta, sd) at
|
|
174
|
+
// sub-pixel error -- wiki section 3.2 form-equivalence callout).
|
|
175
|
+
let dist = (sd - 0.5) * screen_px_range(in.uv);
|
|
176
|
+
let alpha = clamp(dist + 0.5, 0.0, 1.0) * material.tintColor.a;
|
|
177
|
+
// Premultiplied output: rgb already multiplied by alpha for srcFactor=ONE /
|
|
178
|
+
// dstFactor=ONE_MINUS_SRC_ALPHA (wiki section 6 SSOT).
|
|
179
|
+
let fogged = applySceneFog(view, material.tintColor.rgb, alpha, in.worldPos);
|
|
180
|
+
return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// linear_to_srgb: per-channel IEC 61966-2-1 transfer function for the LDR
|
|
184
|
+
// bgra8unorm swap-chain target (not hardware-sRGB-encoded). Alpha is NOT
|
|
185
|
+
// encoded -- the blend equation operates on raw alpha.
|
|
186
|
+
fn linear_to_srgb(linear : f32) -> f32 {
|
|
187
|
+
let c = clamp(linear, 0.0, 1.0);
|
|
188
|
+
return select(c * 12.92, pow(c, 1.0 / 2.4) * 1.055 - 0.055, c > 0.0031308);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// fs_main: LDR variant for the bgra8unorm target. Same coverage math as the
|
|
192
|
+
// HDR variant; encodes RGB through the sRGB transfer function (the LDR target
|
|
193
|
+
// is not hardware-sRGB-encoded), alpha stays linear through the blend.
|
|
194
|
+
@fragment
|
|
195
|
+
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
196
|
+
let msd = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv, material.baseColorTextureCoordinatesMetadata.zw).rgb;
|
|
197
|
+
let sd = median(msd.r, msd.g, msd.b);
|
|
198
|
+
let dist = (sd - 0.5) * screen_px_range(in.uv);
|
|
199
|
+
let alpha = clamp(dist + 0.5, 0.0, 1.0) * material.tintColor.a;
|
|
200
|
+
let fogged = applySceneFog(view, material.tintColor.rgb, alpha, in.worldPos);
|
|
201
|
+
let premult = fogged.rgb * fogged.a;
|
|
202
|
+
return vec4<f32>(
|
|
203
|
+
linear_to_srgb(premult.r),
|
|
204
|
+
linear_to_srgb(premult.g),
|
|
205
|
+
linear_to_srgb(premult.b),
|
|
206
|
+
fogged.a,
|
|
207
|
+
);
|
|
208
|
+
}
|