@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,69 @@
|
|
|
1
|
+
#define_import_path forgeax_view::hdrp_deferred_lighting
|
|
2
|
+
|
|
3
|
+
#import forgeax_view::common::FullscreenOutput
|
|
4
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay}
|
|
5
|
+
#import forgeax_view::common::fullscreen_triangle
|
|
6
|
+
#import forgeax_view::fog::{apply_fog}
|
|
7
|
+
|
|
8
|
+
// HDRP deferred lighting producer. The lighting pass consumes the material
|
|
9
|
+
// G-buffer and current-frame depth, reconstructs the fragment world position,
|
|
10
|
+
// then applies the shared analytic Fog exactly once to linear radiance.
|
|
11
|
+
|
|
12
|
+
@group(0) @binding(0) var<uniform> view : View;
|
|
13
|
+
|
|
14
|
+
@group(1) @binding(0) var gbufferAlbedo : texture_2d<f32>;
|
|
15
|
+
@group(1) @binding(1) var gbufferSampler : sampler;
|
|
16
|
+
@group(1) @binding(2) var<uniform> deferredParams : vec4<f32>;
|
|
17
|
+
@group(1) @binding(3) var sceneDepth : texture_depth_2d;
|
|
18
|
+
@group(1) @binding(4) var depthSampler : sampler;
|
|
19
|
+
@group(1) @binding(5) var gbufferNormal : texture_2d<f32>;
|
|
20
|
+
@group(1) @binding(6) var gbufferEmissive : texture_2d<f32>;
|
|
21
|
+
|
|
22
|
+
@vertex
|
|
23
|
+
fn vs_main(@builtin(vertex_index) vertex_index : u32) -> FullscreenOutput {
|
|
24
|
+
return fullscreen_triangle(vertex_index);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn reconstructWorldPosition(uv : vec2<f32>, depth : f32) -> vec3<f32> {
|
|
28
|
+
let clip = vec4<f32>(uv * 2.0 - vec2<f32>(1.0), depth, 1.0);
|
|
29
|
+
let worldH = view.inverseViewProj * clip;
|
|
30
|
+
return worldH.xyz / worldH.w;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn fogDeferredRadiance(color : vec4<f32>, worldPos : vec3<f32>) -> vec4<f32> {
|
|
34
|
+
var origin = view.cameraPos;
|
|
35
|
+
var direction = normalize(worldPos - origin);
|
|
36
|
+
var rayDistance = length(worldPos - origin);
|
|
37
|
+
if (view.temporalProjection.z >= 0.5) {
|
|
38
|
+
let nearH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
39
|
+
let farH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
40
|
+
let nearPoint = nearH.xyz / nearH.w;
|
|
41
|
+
let farPoint = farH.xyz / farH.w;
|
|
42
|
+
direction = normalize(farPoint - nearPoint);
|
|
43
|
+
origin = worldPos - direction * dot(worldPos - view.cameraPos, direction);
|
|
44
|
+
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
45
|
+
}
|
|
46
|
+
return apply_fog(view.fog, FogRay(origin, direction, rayDistance), color);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@fragment
|
|
50
|
+
fn fs_lighting(in : FullscreenOutput) -> @location(0) vec4<f32> {
|
|
51
|
+
// The lighting pass runs before the forward lane and shares its cleared
|
|
52
|
+
// scene target. A far-depth sample is an empty G-buffer pixel; discard it
|
|
53
|
+
// so deferred lighting preserves the existing clear/background contract.
|
|
54
|
+
// Writing a synthetic material value here would cover forward-only scenes
|
|
55
|
+
// (and change the HDRP baseline even when no deferred material is present).
|
|
56
|
+
let depth = textureSampleLevel(sceneDepth, depthSampler, in.uv, 0);
|
|
57
|
+
if (depth >= 0.999999) {
|
|
58
|
+
discard;
|
|
59
|
+
}
|
|
60
|
+
let material = textureSampleLevel(gbufferAlbedo, gbufferSampler, in.uv, 0);
|
|
61
|
+
let normalSample = textureSampleLevel(gbufferNormal, gbufferSampler, in.uv, 0);
|
|
62
|
+
let emissiveSample = textureSampleLevel(gbufferEmissive, gbufferSampler, in.uv, 0);
|
|
63
|
+
let normal = normalize(normalSample.xyz * 2.0 - vec3<f32>(1.0));
|
|
64
|
+
let light = max(dot(normal, normalize(-view.lightDir)), 0.0);
|
|
65
|
+
let direct = material.rgb * view.lightColor * light * clamp(emissiveSample.a, 0.0, 1.0);
|
|
66
|
+
let radiance = vec4<f32>(direct + emissiveSample.rgb, material.a);
|
|
67
|
+
let worldPos = reconstructWorldPosition(in.uv, depth);
|
|
68
|
+
return fogDeferredRadiance(radiance, worldPos);
|
|
69
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// hdrp-ssao.wgsl — HDRP Screen-Space Ambient Occlusion.
|
|
2
|
+
// feat-20260612-hdrp-ssao M2 / w8.
|
|
3
|
+
//
|
|
4
|
+
// Two fullscreen fragment passes following LO 5.9:
|
|
5
|
+
// fs_ssao_calc — 64-sample hemisphere SSAO with range check, writes R8 scalar.
|
|
6
|
+
// fs_ssao_blur — 4x4 box blur (16 tap), reads ssaoRaw, writes R8 scalar.
|
|
7
|
+
//
|
|
8
|
+
// plan-strategy D-1: SSAO owns its uniform group (@group(2)) with view +
|
|
9
|
+
// projection + inverseProjection (3 mat4, 192 B UBO). Does NOT read View UBO.
|
|
10
|
+
// plan-strategy D-2: exactly 2 pass, no H/V split.
|
|
11
|
+
// requirements OOS-3: 4x4 box blur only; no depth-aware/bilateral.
|
|
12
|
+
// requirements OOS-4: no depth-reconstruction path for normals; reads normal from
|
|
13
|
+
// g-buffer RT0 (packed world-space normal, 0.5*n+0.5 in rgba16f).
|
|
14
|
+
//
|
|
15
|
+
// BGL layout (@group(0), SSAO-dedicated; w37 expansion for D-A/D-D + dawn fix):
|
|
16
|
+
// @binding(0) var<uniform> ssao_uniform : SsaoUniform (256 B UBO)
|
|
17
|
+
// @binding(1) var<uniform> ssao_kernel : array<vec4<f32>,64> (1024 B UBO)
|
|
18
|
+
// @binding(2) var ssao_noise_texture : texture_2d<f32> (4x4 rgba32float)
|
|
19
|
+
// @binding(3) var ssao_noise_sampler : sampler (filtering, for noise tile)
|
|
20
|
+
// @binding(4) var gbuffer_normal : texture_2d<f32> (RT0, packed world normal)
|
|
21
|
+
// @binding(5) var hdr_depth : texture_depth_2d (hardware depth)
|
|
22
|
+
// @binding(6) var ssao_depth_sampler : sampler (non-filtering, for hdr_depth)
|
|
23
|
+
// @binding(7) var ssaoRaw : texture_2d<f32> (half-res calc output, blur input)
|
|
24
|
+
// @binding(8) var ssaoSampler : sampler (filtering, for ssaoRaw)
|
|
25
|
+
//
|
|
26
|
+
// Sampler split (w37 dawn-blocker fix): WebGPU validation requires depth
|
|
27
|
+
// textures (sampleType=depth) to be sampled with non-filtering or comparison
|
|
28
|
+
// samplers, not filtering. Pre-w37 the BGL had a single `filtering` sampler at
|
|
29
|
+
// binding 3 paired with hdr_depth at binding 5, which crashed every HDRP PSO
|
|
30
|
+
// build on dawn. ssao_depth_sampler at binding 6 is the non-filtering sampler
|
|
31
|
+
// dedicated to hdr_depth; ssao_noise_sampler at binding 3 stays filtering for
|
|
32
|
+
// the float noise/normal textures.
|
|
33
|
+
//
|
|
34
|
+
// fs_ssao_calc: reads g-buffer normal + hdrDepth, writes half-res R8.
|
|
35
|
+
// fs_ssao_blur: reads ssaoRaw (half-res R8), writes half-res R8 (D-D fix:
|
|
36
|
+
// pre-w37 blur erroneously read gbuffer_normal — copy-paste typo from
|
|
37
|
+
// fs_ssao_calc).
|
|
38
|
+
//
|
|
39
|
+
// Vertex stage: both passes use fullscreen_triangle from common.wgsl.
|
|
40
|
+
//
|
|
41
|
+
// Research F1-F6 (LO 5.9 GLSL -> WGSL translation), KB ref:
|
|
42
|
+
// .forgeax-harness/knowledge-base/references/repos/learnopengl/src/5.advanced_lighting/9.ssao/
|
|
43
|
+
|
|
44
|
+
#define_import_path forgeax_hdrp::ssao
|
|
45
|
+
|
|
46
|
+
#import forgeax_view::common::{fullscreen_triangle, FullscreenOutput}
|
|
47
|
+
|
|
48
|
+
// ── SSAO uniform (3 mat4 + vec4 intensityPad, 256 B, D-1 + D-C) ─────────────
|
|
49
|
+
//
|
|
50
|
+
// intensityPad carries the lighting intensity plus the calc radius/bias.
|
|
51
|
+
// The lighting shader reads intensity from `cluster_uniform.near_far_log.w`
|
|
52
|
+
// (HDRP unified BGL @group(2) @binding(6)); the calc shader reads radius and
|
|
53
|
+
// bias here. Keeping the values in this existing vec4 preserves the one-shot
|
|
54
|
+
// 256 B UBO write and avoids a second parameter binding.
|
|
55
|
+
|
|
56
|
+
struct SsaoUniform {
|
|
57
|
+
view : mat4x4<f32>, // world -> view
|
|
58
|
+
projection : mat4x4<f32>, // view -> clip
|
|
59
|
+
inverseProjection : mat4x4<f32>, // NDC -> view
|
|
60
|
+
intensityPad : vec4<f32>, // x = intensity, y = radius, z = bias
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ── SSAO binding declarations (@group(2)) ───────────────────────────────────
|
|
64
|
+
|
|
65
|
+
@group(0) @binding(0) var<uniform> ssao_uniform : SsaoUniform;
|
|
66
|
+
@group(0) @binding(1) var<uniform> ssao_kernel : array<vec4<f32>, 64>;
|
|
67
|
+
@group(0) @binding(2) var ssao_noise_texture : texture_2d<f32>;
|
|
68
|
+
@group(0) @binding(3) var ssao_noise_sampler : sampler;
|
|
69
|
+
@group(0) @binding(4) var gbuffer_normal : texture_2d<f32>;
|
|
70
|
+
@group(0) @binding(5) var hdr_depth : texture_depth_2d;
|
|
71
|
+
@group(0) @binding(6) var ssao_depth_sampler : sampler;
|
|
72
|
+
@group(0) @binding(7) var ssaoRaw : texture_2d<f32>;
|
|
73
|
+
@group(0) @binding(8) var ssaoSampler : sampler;
|
|
74
|
+
|
|
75
|
+
// ── vertex: fullscreen triangle (SSOT in common.wgsl) ───────────────────────
|
|
76
|
+
|
|
77
|
+
struct SsaoVsOut {
|
|
78
|
+
@builtin(position) position : vec4<f32>,
|
|
79
|
+
@location(0) uv : vec2<f32>,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
@vertex
|
|
83
|
+
fn vs_ssao(@builtin(vertex_index) vertex_index : u32) -> SsaoVsOut {
|
|
84
|
+
let ft = fullscreen_triangle(vertex_index);
|
|
85
|
+
var out : SsaoVsOut;
|
|
86
|
+
out.position = ft.position;
|
|
87
|
+
out.uv = ft.uv;
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── fs_ssao_calc: 64-sample hemisphere SSAO (LO 5.9) ────────────────────────
|
|
92
|
+
|
|
93
|
+
@fragment
|
|
94
|
+
fn fs_ssao_calc(in : SsaoVsOut) -> @location(0) f32 {
|
|
95
|
+
// Reconstruct view-space position from depth + NDC.
|
|
96
|
+
let depth = textureSampleLevel(hdr_depth, ssao_depth_sampler, in.uv, 0);
|
|
97
|
+
// NDC reconstruction: screen-space xy in [-1,1], depth in [0,1].
|
|
98
|
+
let ndc = vec4<f32>(in.uv * 2.0 - 1.0, depth, 1.0);
|
|
99
|
+
// Transform NDC -> view via inverse projection.
|
|
100
|
+
var viewPosH = ssao_uniform.inverseProjection * ndc;
|
|
101
|
+
viewPosH = viewPosH / viewPosH.w;
|
|
102
|
+
let viewPos = viewPosH.xyz;
|
|
103
|
+
|
|
104
|
+
// Read and unpack world-space normal from g-buffer RT0.
|
|
105
|
+
let packedNormal = textureSample(gbuffer_normal, ssao_noise_sampler, in.uv).rgb;
|
|
106
|
+
let worldNormal = normalize(packedNormal * 2.0 - 1.0);
|
|
107
|
+
|
|
108
|
+
// Rotate world normal to view-space.
|
|
109
|
+
let viewNormal = normalize((ssao_uniform.view * vec4<f32>(worldNormal, 0.0)).xyz);
|
|
110
|
+
|
|
111
|
+
// Read noise for per-pixel TBN rotation.
|
|
112
|
+
let noiseScale = vec2<f32>(textureDimensions(ssao_noise_texture, 0)) / 4.0;
|
|
113
|
+
// Actually: scale factor is screenDim / noiseDim. Use a fixed factor for half-res.
|
|
114
|
+
// The noise texture is 4x4 and tiled via REPEAT sampling.
|
|
115
|
+
let screenDim = vec2<f32>(textureDimensions(gbuffer_normal, 0));
|
|
116
|
+
let noiseUV = in.uv * screenDim / 4.0;
|
|
117
|
+
let randomVec = normalize(textureSample(ssao_noise_texture, ssao_noise_sampler, noiseUV).xyz);
|
|
118
|
+
|
|
119
|
+
// TBN: Gram-Schmidt orthonormalization (tangent-space -> view-space).
|
|
120
|
+
let tangent = normalize(randomVec - viewNormal * dot(randomVec, viewNormal));
|
|
121
|
+
let bitangent = cross(viewNormal, tangent);
|
|
122
|
+
let TBN = mat3x3<f32>(tangent, bitangent, viewNormal);
|
|
123
|
+
|
|
124
|
+
// SSAO parameters are install-time config values projected by the host.
|
|
125
|
+
let radius = ssao_uniform.intensityPad.y;
|
|
126
|
+
let bias = ssao_uniform.intensityPad.z;
|
|
127
|
+
|
|
128
|
+
var occlusion = 0.0;
|
|
129
|
+
for (var i = 0u; i < 64u; i = i + 1u) {
|
|
130
|
+
// Tangent-space sample -> view-space via TBN.
|
|
131
|
+
let sampleTangent = ssao_kernel[i].xyz;
|
|
132
|
+
var sampleView = TBN * sampleTangent;
|
|
133
|
+
sampleView = viewPos + sampleView * radius;
|
|
134
|
+
|
|
135
|
+
// Project sample to screen.
|
|
136
|
+
var offset = ssao_uniform.projection * vec4<f32>(sampleView, 1.0);
|
|
137
|
+
offset = offset / offset.w;
|
|
138
|
+
offset = offset * 0.5 + 0.5;
|
|
139
|
+
|
|
140
|
+
// Sample depth at the projected screen location.
|
|
141
|
+
let sampleDepth = textureSampleLevel(hdr_depth, ssao_depth_sampler, offset.xy, 0);
|
|
142
|
+
// Reconstruct view-space z of the sampled fragment (at offset.xy).
|
|
143
|
+
var sampledViewPosH = ssao_uniform.inverseProjection * vec4<f32>(offset.xy * 2.0 - 1.0, sampleDepth, 1.0);
|
|
144
|
+
sampledViewPosH = sampledViewPosH / sampledViewPosH.w;
|
|
145
|
+
let sampledViewZ = sampledViewPosH.z;
|
|
146
|
+
|
|
147
|
+
// Range check: smoothstep based on distance along view-z axis.
|
|
148
|
+
let rangeCheck = smoothstep(0.0, 1.0, radius / abs(viewPos.z - sampledViewZ));
|
|
149
|
+
let sampleContrib = select(0.0, 1.0, sampledViewZ >= sampleView.z + bias);
|
|
150
|
+
occlusion += sampleContrib * rangeCheck;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
occlusion = 1.0 - (occlusion / 64.0);
|
|
154
|
+
return occlusion;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── fs_ssao_blur: 4x4 box blur (LO 5.9, OOS-3) ─────────────────────────────
|
|
158
|
+
|
|
159
|
+
@fragment
|
|
160
|
+
fn fs_ssao_blur(in : SsaoVsOut) -> @location(0) f32 {
|
|
161
|
+
// D-D fix: blur reads ssaoRaw (half-res calc output), not gbuffer_normal.
|
|
162
|
+
// Pre-w37 the dimensions + sample texture both incorrectly named
|
|
163
|
+
// gbuffer_normal — copy-paste typo from fs_ssao_calc that produced no
|
|
164
|
+
// occlusion at all (the blur ran a 4x4 average over packed world normals).
|
|
165
|
+
let texelSize = 1.0 / vec2<f32>(textureDimensions(ssaoRaw, 0));
|
|
166
|
+
var result = 0.0;
|
|
167
|
+
for (var x = -2; x < 2; x = x + 1) {
|
|
168
|
+
for (var y = -2; y < 2; y = y + 1) {
|
|
169
|
+
let offset = vec2<f32>(f32(x), f32(y)) * texelSize;
|
|
170
|
+
result += textureSample(ssaoRaw, ssaoSampler, in.uv + offset).r;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return result / 16.0;
|
|
174
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::ibl_brdf_lut
|
|
2
|
+
|
|
3
|
+
// @forgeax/engine-shader - ibl-brdf-lut.wgsl
|
|
4
|
+
// (feat-20260520-skylight-ibl-cubemap M3 / t46).
|
|
5
|
+
//
|
|
6
|
+
// BRDF integration LUT bake (Karis 2013 split-sum, second factor).
|
|
7
|
+
// One-shot fullscreen pass into a 256x256 RG16F target. Output channels:
|
|
8
|
+
// R = sum (1 - Fc) * G * (V . H) / (N . H * N . V)
|
|
9
|
+
// G = sum Fc * G * (V . H) / (N . H * N . V)
|
|
10
|
+
// where Fc = (1 - V . H)^5. This LUT is consumed at runtime by
|
|
11
|
+
// sampleIblSpecular() in ibl-sampling.wgsl.
|
|
12
|
+
//
|
|
13
|
+
// Zero @group(1) bindings -- the LUT is computed entirely from screen-space
|
|
14
|
+
// position + Hammersley sequence + shared math. AC-06 / plan D-10 fixes
|
|
15
|
+
// BRDF_LUT_SIZE = 256u as the SSOT constant; the host (IblPipelineCache)
|
|
16
|
+
// reads the same constant via a TS mirror.
|
|
17
|
+
//
|
|
18
|
+
// Entries: fullscreen_vs + brdfLutBake_fs.
|
|
19
|
+
|
|
20
|
+
#import forgeax_pbr::ibl_shared::{hammersley, importanceSampleGGX, iblGeometrySmith}
|
|
21
|
+
|
|
22
|
+
struct VsOut {
|
|
23
|
+
@builtin(position) clip: vec4<f32>,
|
|
24
|
+
@location(0) worldPos: vec3<f32>,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// AC-06 / plan D-10: BRDF LUT resolution. Both the bake fragment and the
|
|
28
|
+
// caller (IblPipelineCache.runIblPrecompute) align on this SSOT constant.
|
|
29
|
+
const BRDF_LUT_SIZE: u32 = 256u;
|
|
30
|
+
const BRDF_LUT_SAMPLE_COUNT: u32 = 1024u;
|
|
31
|
+
|
|
32
|
+
@vertex
|
|
33
|
+
fn fullscreen_vs(@builtin(vertex_index) vi: u32) -> VsOut {
|
|
34
|
+
// Fullscreen triangle covering NDC [-1,1]^2.
|
|
35
|
+
var x: f32 = -1.0;
|
|
36
|
+
var y: f32 = -1.0;
|
|
37
|
+
if (vi == 1u) {
|
|
38
|
+
x = 3.0;
|
|
39
|
+
}
|
|
40
|
+
if (vi == 2u) {
|
|
41
|
+
y = 3.0;
|
|
42
|
+
}
|
|
43
|
+
var out: VsOut;
|
|
44
|
+
out.clip = vec4<f32>(x, y, 0.0, 1.0);
|
|
45
|
+
out.worldPos = vec3<f32>(0.0); // unused
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@fragment
|
|
50
|
+
fn brdfLutBake_fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
|
51
|
+
// Screen-space framebuffer position maps to (NdotV, roughness) in [0, 1]^2.
|
|
52
|
+
// Caller renders a fullscreen quad at BRDF_LUT_SIZE x BRDF_LUT_SIZE viewport
|
|
53
|
+
// (256x256 per plan D-10 / AC-06). pos.x / pos.y are framebuffer pixel
|
|
54
|
+
// coordinates in [0.5, BRDF_LUT_SIZE - 0.5].
|
|
55
|
+
let denom = f32(BRDF_LUT_SIZE - 1u);
|
|
56
|
+
let NdotV = clamp(pos.x / denom, 0.0, 1.0);
|
|
57
|
+
let roughness = clamp(pos.y / denom, 0.0, 1.0);
|
|
58
|
+
|
|
59
|
+
// Reconstruct V from NdotV in canonical frame: N = (0, 0, 1).
|
|
60
|
+
let V = vec3<f32>(
|
|
61
|
+
sqrt(clamp(1.0 - NdotV * NdotV, 0.0, 1.0)),
|
|
62
|
+
0.0,
|
|
63
|
+
NdotV,
|
|
64
|
+
);
|
|
65
|
+
let N = vec3<f32>(0.0, 0.0, 1.0);
|
|
66
|
+
|
|
67
|
+
var A: f32 = 0.0;
|
|
68
|
+
var B: f32 = 0.0;
|
|
69
|
+
|
|
70
|
+
for (var i: u32 = 0u; i < BRDF_LUT_SAMPLE_COUNT; i = i + 1u) {
|
|
71
|
+
let Xi = hammersley(i, BRDF_LUT_SAMPLE_COUNT);
|
|
72
|
+
let H = importanceSampleGGX(Xi, N, roughness);
|
|
73
|
+
let L = normalize(2.0 * dot(V, H) * H - V);
|
|
74
|
+
|
|
75
|
+
let NdotL = max(L.z, 0.0);
|
|
76
|
+
if (NdotL > 0.0) {
|
|
77
|
+
let NdotH = max(H.z, 0.0);
|
|
78
|
+
let VdotH = max(dot(V, H), 0.0);
|
|
79
|
+
|
|
80
|
+
let G = iblGeometrySmith(NdotV, NdotL, roughness);
|
|
81
|
+
let GVis = (G * VdotH) / max(NdotH * NdotV, 1e-5);
|
|
82
|
+
let Fc = pow(max(1.0 - VdotH, 0.0), 5.0);
|
|
83
|
+
|
|
84
|
+
A += (1.0 - Fc) * GVis;
|
|
85
|
+
B += Fc * GVis;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
A = A / f32(BRDF_LUT_SAMPLE_COUNT);
|
|
89
|
+
B = B / f32(BRDF_LUT_SAMPLE_COUNT);
|
|
90
|
+
|
|
91
|
+
return vec4<f32>(A, B, 0.0, 1.0);
|
|
92
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::ibl_equirect_to_cube
|
|
2
|
+
|
|
3
|
+
// @forgeax/engine-shader - ibl-equirect-to-cube.wgsl
|
|
4
|
+
// (feat-20260520-skylight-ibl-cubemap M3 / t43).
|
|
5
|
+
//
|
|
6
|
+
// Equirectangular HDRI -> cubemap projection pass.
|
|
7
|
+
// Per LearnOpenGL §6.2.1: render a unit cube from each of the 6 face views,
|
|
8
|
+
// fragment shader maps the world-space direction to equirect UV and samples
|
|
9
|
+
// the input HDRI texture.
|
|
10
|
+
//
|
|
11
|
+
// @group(0) = per-face viewProj uniform.
|
|
12
|
+
// @group(1) = equirect texture_2d + sampler. NOTE: this is the ONLY ibl-*
|
|
13
|
+
// module that binds a texture_2d at @group(1); the irradiance /
|
|
14
|
+
// prefilter modules bind texture_cube at the same slot, which
|
|
15
|
+
// is why the round-1 single-file design collided.
|
|
16
|
+
//
|
|
17
|
+
// Entries:
|
|
18
|
+
// - cubemap_vs (shared VS used by irradiance + prefilter as well;
|
|
19
|
+
// each module redeclares its own copy because WGSL
|
|
20
|
+
// modules cannot cross-import entry points)
|
|
21
|
+
// - equirectToCube_fs
|
|
22
|
+
|
|
23
|
+
#import forgeax_pbr::ibl_shared::{sampleSphericalMap}
|
|
24
|
+
|
|
25
|
+
struct CubemapVsIn {
|
|
26
|
+
@location(0) pos: vec3<f32>,
|
|
27
|
+
};
|
|
28
|
+
struct CubemapVsOut {
|
|
29
|
+
@builtin(position) clip: vec4<f32>,
|
|
30
|
+
@location(0) worldPos: vec3<f32>,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
struct CubemapFaceUniforms {
|
|
34
|
+
viewProj: mat4x4<f32>,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
@group(0) @binding(0) var<uniform> faceUniforms: CubemapFaceUniforms;
|
|
38
|
+
|
|
39
|
+
@group(1) @binding(0) var equirectTexture: texture_2d<f32>;
|
|
40
|
+
@group(1) @binding(1) var equirectSamplerS: sampler;
|
|
41
|
+
|
|
42
|
+
@vertex
|
|
43
|
+
fn cubemap_vs(in0: CubemapVsIn) -> CubemapVsOut {
|
|
44
|
+
var out: CubemapVsOut;
|
|
45
|
+
out.clip = faceUniforms.viewProj * vec4<f32>(in0.pos, 1.0);
|
|
46
|
+
out.worldPos = in0.pos;
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@fragment
|
|
51
|
+
fn equirectToCube_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
|
|
52
|
+
let dir = normalize(in0.worldPos);
|
|
53
|
+
let uv = sampleSphericalMap(dir);
|
|
54
|
+
let color = textureSample(equirectTexture, equirectSamplerS, uv);
|
|
55
|
+
return vec4<f32>(color.rgb, 1.0);
|
|
56
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::ibl_irradiance
|
|
2
|
+
|
|
3
|
+
// @forgeax/engine-shader - ibl-irradiance.wgsl
|
|
4
|
+
// (feat-20260520-skylight-ibl-cubemap M3 / t44).
|
|
5
|
+
//
|
|
6
|
+
// Diffuse irradiance convolution. Per LearnOpenGL §6.2.2: hemisphere
|
|
7
|
+
// Riemann sum (sampleDelta = 0.025) integrates the env cubemap to produce
|
|
8
|
+
// the convolved irradiance cubemap consumed by sampleIblDiffuse() at
|
|
9
|
+
// runtime.
|
|
10
|
+
//
|
|
11
|
+
// @group(0) = per-face viewProj uniform.
|
|
12
|
+
// @group(1) = env cubemap (texture_cube<f32>) + sampler. This is the same
|
|
13
|
+
// slot the prefilter module uses for its env cube, BUT because
|
|
14
|
+
// round-2 keeps each ibl-* module physically separate, the WGSL
|
|
15
|
+
// (group, binding) global-uniqueness rule applies per module
|
|
16
|
+
// rather than across the family.
|
|
17
|
+
//
|
|
18
|
+
// Entries: cubemap_vs + irradianceConvolve_fs.
|
|
19
|
+
|
|
20
|
+
#import forgeax_pbr::ibl_shared::{PI}
|
|
21
|
+
|
|
22
|
+
struct CubemapVsIn {
|
|
23
|
+
@location(0) pos: vec3<f32>,
|
|
24
|
+
};
|
|
25
|
+
struct CubemapVsOut {
|
|
26
|
+
@builtin(position) clip: vec4<f32>,
|
|
27
|
+
@location(0) worldPos: vec3<f32>,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
struct CubemapFaceUniforms {
|
|
31
|
+
viewProj: mat4x4<f32>,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
@group(0) @binding(0) var<uniform> faceUniforms: CubemapFaceUniforms;
|
|
35
|
+
|
|
36
|
+
@group(1) @binding(0) var envCube: texture_cube<f32>;
|
|
37
|
+
@group(1) @binding(1) var envSamplerS: sampler;
|
|
38
|
+
|
|
39
|
+
const IRRADIANCE_SAMPLE_DELTA: f32 = 0.025;
|
|
40
|
+
|
|
41
|
+
@vertex
|
|
42
|
+
fn cubemap_vs(in0: CubemapVsIn) -> CubemapVsOut {
|
|
43
|
+
var out: CubemapVsOut;
|
|
44
|
+
out.clip = faceUniforms.viewProj * vec4<f32>(in0.pos, 1.0);
|
|
45
|
+
out.worldPos = in0.pos;
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@fragment
|
|
50
|
+
fn irradianceConvolve_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
|
|
51
|
+
let N = normalize(in0.worldPos);
|
|
52
|
+
let up0 = select(
|
|
53
|
+
vec3<f32>(0.0, 1.0, 0.0),
|
|
54
|
+
vec3<f32>(1.0, 0.0, 0.0),
|
|
55
|
+
abs(N.y) > 0.999,
|
|
56
|
+
);
|
|
57
|
+
let right = normalize(cross(up0, N));
|
|
58
|
+
let up = normalize(cross(N, right));
|
|
59
|
+
|
|
60
|
+
var irradiance = vec3<f32>(0.0);
|
|
61
|
+
var nrSamples: f32 = 0.0;
|
|
62
|
+
|
|
63
|
+
var phi: f32 = 0.0;
|
|
64
|
+
while (phi < 2.0 * PI) {
|
|
65
|
+
var theta: f32 = 0.0;
|
|
66
|
+
while (theta < 0.5 * PI) {
|
|
67
|
+
let tangentSample = vec3<f32>(
|
|
68
|
+
sin(theta) * cos(phi),
|
|
69
|
+
sin(theta) * sin(phi),
|
|
70
|
+
cos(theta),
|
|
71
|
+
);
|
|
72
|
+
let sampleVec = tangentSample.x * right +
|
|
73
|
+
tangentSample.y * up +
|
|
74
|
+
tangentSample.z * N;
|
|
75
|
+
|
|
76
|
+
let sampleColor = textureSampleLevel(
|
|
77
|
+
envCube, envSamplerS, sampleVec, 0.0,
|
|
78
|
+
).rgb;
|
|
79
|
+
irradiance += sampleColor * cos(theta) * sin(theta);
|
|
80
|
+
nrSamples += 1.0;
|
|
81
|
+
theta += IRRADIANCE_SAMPLE_DELTA;
|
|
82
|
+
}
|
|
83
|
+
phi += IRRADIANCE_SAMPLE_DELTA;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// IRRADIANCE_PAYLOAD_E_TIMES_PI: the stored payload is E * pi.
|
|
87
|
+
irradiance = PI * irradiance / max(nrSamples, 1.0);
|
|
88
|
+
return vec4<f32>(irradiance, 1.0);
|
|
89
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::ibl_prefilter
|
|
2
|
+
|
|
3
|
+
// @forgeax/engine-shader - ibl-prefilter.wgsl
|
|
4
|
+
// (feat-20260520-skylight-ibl-cubemap M3 / t45).
|
|
5
|
+
//
|
|
6
|
+
// GGX specular prefilter pass (Karis 2013 split-sum). For each roughness
|
|
7
|
+
// mip level, importance-sample the environment cubemap with SAMPLE_COUNT
|
|
8
|
+
// Hammersley samples; the result is the prefiltered radiance cube
|
|
9
|
+
// consumed by sampleIblSpecular() at runtime.
|
|
10
|
+
//
|
|
11
|
+
// @group(0) layout (dual uniform -- the (b3) compose test in the round-2
|
|
12
|
+
// dawn suite exercises this ordering):
|
|
13
|
+
// binding(0) = CubemapFaceUniforms { viewProj } (per-face)
|
|
14
|
+
// binding(1) = PrefilterUniforms { roughness, faceSize, _pad0, _pad1 }
|
|
15
|
+
// @group(1) = env cubemap + sampler. Same shape as ibl-irradiance, but
|
|
16
|
+
// physically isolated -- both bind @binding(0) to a texture_cube
|
|
17
|
+
// inside *their own* module without colliding because round-2
|
|
18
|
+
// ships each module as a separate file.
|
|
19
|
+
//
|
|
20
|
+
// Entries: cubemap_vs + prefilterEnv_fs.
|
|
21
|
+
|
|
22
|
+
#import forgeax_pbr::ibl_shared::{PI, hammersley, importanceSampleGGX, iblDGGX}
|
|
23
|
+
|
|
24
|
+
struct CubemapVsIn {
|
|
25
|
+
@location(0) pos: vec3<f32>,
|
|
26
|
+
};
|
|
27
|
+
struct CubemapVsOut {
|
|
28
|
+
@builtin(position) clip: vec4<f32>,
|
|
29
|
+
@location(0) worldPos: vec3<f32>,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
struct CubemapFaceUniforms {
|
|
33
|
+
viewProj: mat4x4<f32>,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
struct PrefilterUniforms {
|
|
37
|
+
roughness: f32,
|
|
38
|
+
faceSize: f32,
|
|
39
|
+
// Keep the uniform block 16-byte aligned on WebGL2 downlevel backends.
|
|
40
|
+
_pad0: f32,
|
|
41
|
+
_pad1: f32,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
@group(0) @binding(0) var<uniform> faceUniforms: CubemapFaceUniforms;
|
|
45
|
+
@group(0) @binding(1) var<uniform> prefUniforms: PrefilterUniforms;
|
|
46
|
+
|
|
47
|
+
@group(1) @binding(0) var envCube: texture_cube<f32>;
|
|
48
|
+
@group(1) @binding(1) var envSamplerS: sampler;
|
|
49
|
+
|
|
50
|
+
const PREFILTER_SAMPLE_COUNT: u32 = 1024u;
|
|
51
|
+
|
|
52
|
+
@vertex
|
|
53
|
+
fn cubemap_vs(in0: CubemapVsIn) -> CubemapVsOut {
|
|
54
|
+
var out: CubemapVsOut;
|
|
55
|
+
out.clip = faceUniforms.viewProj * vec4<f32>(in0.pos, 1.0);
|
|
56
|
+
out.worldPos = in0.pos;
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@fragment
|
|
61
|
+
fn prefilterEnv_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
|
|
62
|
+
let roughness = prefUniforms.roughness;
|
|
63
|
+
// V = R = N (cubemap capture from origin assumption).
|
|
64
|
+
let N = normalize(in0.worldPos);
|
|
65
|
+
let V = N;
|
|
66
|
+
|
|
67
|
+
var prefilteredColor = vec3<f32>(0.0);
|
|
68
|
+
var totalWeight: f32 = 0.0;
|
|
69
|
+
|
|
70
|
+
for (var i: u32 = 0u; i < PREFILTER_SAMPLE_COUNT; i = i + 1u) {
|
|
71
|
+
let Xi = hammersley(i, PREFILTER_SAMPLE_COUNT);
|
|
72
|
+
let H = importanceSampleGGX(Xi, N, roughness);
|
|
73
|
+
let L = normalize(2.0 * dot(V, H) * H - V);
|
|
74
|
+
|
|
75
|
+
let NdotL = max(dot(N, L), 0.0);
|
|
76
|
+
if (NdotL > 0.0) {
|
|
77
|
+
// Mip level from solid-angle PDF ratio.
|
|
78
|
+
let D0 = iblDGGX(max(dot(N, H), 0.0), roughness);
|
|
79
|
+
let NdotH0 = max(dot(N, H), 0.0);
|
|
80
|
+
let HdotV = max(dot(H, V), 0.0);
|
|
81
|
+
let pdf = D0 * NdotH0 / (4.0 * HdotV) + 0.0001;
|
|
82
|
+
|
|
83
|
+
let resolution: f32 = 512.0;
|
|
84
|
+
let saTexel = 4.0 * PI / (6.0 * resolution * resolution);
|
|
85
|
+
let saSample = 1.0 / (f32(PREFILTER_SAMPLE_COUNT) * pdf + 0.0001);
|
|
86
|
+
|
|
87
|
+
let mipLevel = select(
|
|
88
|
+
0.5 * log2(saSample / saTexel),
|
|
89
|
+
0.0,
|
|
90
|
+
roughness == 0.0,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
prefilteredColor += textureSampleLevel(
|
|
94
|
+
envCube, envSamplerS, L, mipLevel,
|
|
95
|
+
).rgb * NdotL;
|
|
96
|
+
totalWeight += NdotL;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
prefilteredColor = prefilteredColor / max(totalWeight, 0.001);
|
|
100
|
+
return vec4<f32>(prefilteredColor, 1.0);
|
|
101
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::ibl_sampling
|
|
2
|
+
|
|
3
|
+
// @forgeax/engine-shader - ibl-sampling.wgsl
|
|
4
|
+
// (feat-20260520-skylight-ibl-cubemap M3 / t47).
|
|
5
|
+
//
|
|
6
|
+
// Runtime IBL sampling helpers consumed by pbr.wgsl. Each helper takes the
|
|
7
|
+
// texture + sampler as function arguments rather than declaring its own
|
|
8
|
+
// @group/@binding, so the host (pbr.wgsl's @group(1) material BGL,
|
|
9
|
+
// Skylight resources at @binding(7..13) per D-5 round-4) owns the
|
|
10
|
+
// binding layout and this module composes cleanly anywhere.
|
|
11
|
+
//
|
|
12
|
+
// Zero @group/@binding declarations -- this is the symmetric counterpart
|
|
13
|
+
// to ibl-shared.wgsl for runtime sampling code.
|
|
14
|
+
//
|
|
15
|
+
// Exports:
|
|
16
|
+
// - sampleIblDiffuse(N, irradianceMap, irradianceSampler)
|
|
17
|
+
// - sampleIblSpecular(N, V, roughness, F0, prefilterMap, prefilterSampler,
|
|
18
|
+
// brdfLut, brdfLutSampler)
|
|
19
|
+
|
|
20
|
+
#import forgeax_pbr::ibl_shared::{PI, fresnelSchlickRoughness, inverseRotateEnvironment}
|
|
21
|
+
|
|
22
|
+
// Sample pre-convolved irradiance from the irradiance cubemap.
|
|
23
|
+
// Y is negated to compensate for WebGPU's top-left texture origin vs the
|
|
24
|
+
// OpenGL convention used during equirect-to-cube render passes.
|
|
25
|
+
fn sampleIblDiffuse(
|
|
26
|
+
normal: vec3<f32>,
|
|
27
|
+
rotation: vec4<f32>,
|
|
28
|
+
irradianceMap: texture_cube<f32>,
|
|
29
|
+
irradianceSampler: sampler,
|
|
30
|
+
) -> vec3<f32> {
|
|
31
|
+
let rotated = inverseRotateEnvironment(normal, rotation);
|
|
32
|
+
let dir = vec3<f32>(rotated.x, -rotated.y, rotated.z);
|
|
33
|
+
let irradianceEOverPi = textureSample(irradianceMap, irradianceSampler, dir).rgb;
|
|
34
|
+
return irradianceEOverPi / PI;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Split-sum specular IBL: prefiltered env * (F0 * scale + bias).
|
|
38
|
+
fn sampleIblSpecular(
|
|
39
|
+
normal: vec3<f32>,
|
|
40
|
+
view: vec3<f32>,
|
|
41
|
+
roughness: f32,
|
|
42
|
+
F0: vec3<f32>,
|
|
43
|
+
rotation: vec4<f32>,
|
|
44
|
+
prefilterMap: texture_cube<f32>,
|
|
45
|
+
prefilterSampler: sampler,
|
|
46
|
+
brdfLut: texture_2d<f32>,
|
|
47
|
+
brdfLutSampler: sampler,
|
|
48
|
+
) -> vec3<f32> {
|
|
49
|
+
let NdotV = max(dot(normal, view), 0.001);
|
|
50
|
+
let R = reflect(-view, normal);
|
|
51
|
+
let rotated = inverseRotateEnvironment(R, rotation);
|
|
52
|
+
let Rflip = vec3<f32>(rotated.x, -rotated.y, rotated.z);
|
|
53
|
+
let mip = roughness * 4.0;
|
|
54
|
+
let prefilteredColor = textureSampleLevel(prefilterMap, prefilterSampler, Rflip, mip).rgb;
|
|
55
|
+
let envBRDF = textureSample(brdfLut, brdfLutSampler, vec2<f32>(NdotV, roughness)).rg;
|
|
56
|
+
let F = fresnelSchlickRoughness(NdotV, F0, roughness);
|
|
57
|
+
return prefilteredColor * (F * envBRDF.r + envBRDF.g);
|
|
58
|
+
}
|