Files
benilla-octo/crates/benilla/assets/shaders/sky.wgsl
T
SamandClaude Fable 5 2ad0043523 benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:27:01 +07:00

133 lines
7.5 KiB
WebGPU Shading Language
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Sky-dome gradient — unlit. Paired with `sky.rs` (SkyExt). The five Light.dbc SkyColor stops
// (zenith→horizon) + the fog colour (row 7) are interpolated PER-FRAGMENT by the view-direction
// elevation. RE-VERIFIED mapping (WoW.exe FUN_006d0d10/FUN_006d0f50 + apitrace WoW.5 dome vertices):
// one stop per ring at GEOMETRIC elevations above the horizon —
// SkyColor0 @ 90° (zenith) … SkyColor1 @ 16.8° … SkyColor2 @ 9.8° … SkyColor3 @ 3.7° …
// SkyColor4 @ 1.8° … and the horizon (0°) and below = the FOG colour (row 7).
// So SkyColor0 dominates the whole upper sky; the gradient is crushed into the bottom ~17° and
// converges into the distance fog at the horizon. Linear interp in elevation ≈ WoW's ring Gouraud.
// SKY-PASS DEPTH (see `sky_order.rs`, "The depth law"): the dome's radius does not decide occlusion —
// the reference draws the sky FIRST and the opaque world paints over it. Forcing the far depth makes
// the dome a true backdrop: it fills only pixels no world geometry claimed, so a WDL hill drawn beyond
// the dome's shell can never be over-painted by the gradient.
#import bevy_pbr::{
forward_io::VertexOutput,
mesh_view_bindings::view,
}
/// Reverse-Z "infinitely far" — the sky pass's forced depth (`sky_order.rs`).
const SKY_FAR_DEPTH: f32 = 0.0;
struct SkyOutput {
@location(0) color: vec4<f32>,
@builtin(frag_depth) depth: f32,
}
struct SkyColors {
sky0: vec4<f32>, // zenith (90°)
sky1: vec4<f32>, // 16.8°
sky2: vec4<f32>, // 9.8°
sky3: vec4<f32>, // 3.7°
sky4: vec4<f32>, // 1.8°
fog: vec4<f32>, // horizon (0°) and below — LightIntBand row 7
warp: vec4<f32>, // x = dawn/dusk warp strength S (0 = off), y = sun azimuth (rad), zw reserved
};
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> sky: SkyColors;
// Sun-relative azimuth phase → glow factor `g` (`FUN_006d0f50` table `0xce9af8`, VERIFIED byte-exact off
// its writer `FUN_006ce210`). 6 wrap-around keyframes, piecewise-linear like the engine's table sampler,
// symmetric about phase 0.625. **Semantics (capture-reconciled — see the warp note below):** `g = +1.0`
// (phase 0.125) is the IDENTITY case `(1−g)=0` → ring left untouched, and that phase lands on the SUN
// bearing → the sun side is the bright, unwarped DBC stop. `g < 0` (trough −0.7 at phase 0.625) is the
// ANTI-sun side → it pulls the ring toward the dark apex. `phase` is fract()'d into [0,1).
fn azimuth_glow(phase: f32) -> f32 {
let p = fract(phase);
if (p < 0.125) { return mix(0.0, 1.0, (p + 0.125) / 0.25); } // wrap 0.875(g0)→1.125(g1)
else if (p < 0.375) { return mix(1.0, 0.0, (p - 0.125) / 0.25); }
else if (p < 0.5) { return mix(0.0, -0.5, (p - 0.375) / 0.125); }
else if (p < 0.625) { return mix(-0.5, -0.7, (p - 0.5) / 0.125); }
else if (p < 0.75) { return mix(-0.7, -0.5, (p - 0.625) / 0.125); }
else if (p < 0.875) { return mix(-0.5, 0.0, (p - 0.75) / 0.125); }
else { return mix(0.0, 1.0, (p - 0.875) / 0.25); } // wrap 0.875(g0)→1.125(g1)
}
// One mid-ring's warped colour for a single glow factor `g` (`FUN_006d0f50`, VERIFIED + dusk-capture
// reconciled). `g ≥ 0`: desaturate toward SkyColor1 by `(1−g)·S²` (so g=+1 on the sun bearing = identity =
// the bright raw stop). `g < 0` (anti-sun): a prepass toward SkyColor1 by `S`, then toward the apex
// SkyColor0 by `0.7·(−g)·S²` — the darkest, desaturated away side. (`S²` = the prepass-S × per-segment-S
// nesting; `0.7` is the binary const `0x7ffd7c`. All byte-verified; reproduces the WoW.10 dusk dome to
// ~2/channel.)
fn warp_one(base: vec3<f32>, g: f32, s: f32) -> vec3<f32> {
let s2 = s * s;
if (g >= 0.0) {
return mix(base, sky.sky1.rgb, (1.0 - g) * s2);
}
return mix(mix(base, sky.sky1.rgb, s), sky.sky0.rgb, 0.7 * (-g) * s2);
}
@fragment
fn fragment(in: VertexOutput) -> SkyOutput {
// Direction from the camera to this dome fragment; elevation above the world horizon.
let dir = normalize(in.world_position.xyz - view.world_position.xyz);
let elev = degrees(asin(clamp(dir.y, -1.0, 1.0))); // −90..90, 0 = horizon
// Dawn/dusk azimuthal warp (FUN_006d0f50) — only when S>0 (sunrise ~06:30 / sunset ~21:30 in
// highlightSky=1 zones; the S-curve decays to 0 by ~midnight). RE-VERIFIED + reconciled against the
// WoW.10 dusk capture. Two corrections over the old per-fragment version:
// (1) Warp ONLY the 4 mid rings (SkyColor1..4); the apex (SkyColor0) and the fog rim are UNWARPED.
// The engine's warp loop touches only ring offsets 4/8/12/16, and the capture shows the apex+rim
// uniform across azimuth. So we warp the mid-ring COLOURS here, then run the elevation gradient
// with the warped mid-rings + the *unwarped* apex/fog — the upper sky (>16.8°) interpolates the
// warped ring1 up to the unwarped apex, GL-Gouraud-faithful. (The old `ring_weight` smoothstep
// warped the whole upper sky → at night it dragged the entire dome toward the black apex.)
// (2) The lobe bearing: `+0.125` puts the SUN bearing at glow phase 0.125 (g=+1 → identity → the
// bright raw stop) and the anti-sun at 0.625 (g=−0.7 → darkest). The old `+0.625` had it 180°
// inverted — darkening the sun side and leaving the anti-sun bright. The warp does NOT brighten:
// the sunset glow IS the unwarped sun-side stop; the warp only darkens/desaturates the rest.
// Per-vertex Gouraud: the engine bakes the warp at 24 azimuth segments and interpolates between, which
// smears the genuine g=0 discontinuity (away `(1−g)·S²` vs warm prepass `S`) across one 15° segment
// rather than the hard fragment edge a smooth eval would give. We emulate that — evaluate the warp at
// the two bracketing segment phases and lerp.
var s1 = sky.sky1.rgb;
var s2c = sky.sky2.rgb;
var s3 = sky.sky3.rgb;
var s4 = sky.sky4.rgb;
let warp_s = sky.warp.x;
if (warp_s > 0.0) {
let az = fract((atan2(dir.z, dir.x) - sky.warp.y) / 6.2831853 + 0.125);
let seg = az * 24.0; // 24 azimuth segments, like the dome vertices
let s0 = floor(seg);
let f = seg - s0;
let g0 = azimuth_glow(s0 / 24.0);
let g1 = azimuth_glow((s0 + 1.0) / 24.0);
s1 = mix(warp_one(sky.sky1.rgb, g0, warp_s), warp_one(sky.sky1.rgb, g1, warp_s), f);
s2c = mix(warp_one(sky.sky2.rgb, g0, warp_s), warp_one(sky.sky2.rgb, g1, warp_s), f);
s3 = mix(warp_one(sky.sky3.rgb, g0, warp_s), warp_one(sky.sky3.rgb, g1, warp_s), f);
s4 = mix(warp_one(sky.sky4.rgb, g0, warp_s), warp_one(sky.sky4.rgb, g1, warp_s), f);
}
// Elevation gradient: warped mid-rings + UNWARPED apex (SkyColor0) and fog rim.
var col: vec3<f32>;
if (elev <= 0.0) {
col = sky.fog.rgb; // horizon and below = fog colour (row 7), unwarped
} else if (elev < 1.8) {
col = mix(sky.fog.rgb, s4, elev / 1.8);
} else if (elev < 3.7) {
col = mix(s4, s3, (elev - 1.8) / (3.7 - 1.8));
} else if (elev < 9.8) {
col = mix(s3, s2c, (elev - 3.7) / (9.8 - 3.7));
} else if (elev < 16.8) {
col = mix(s2c, s1, (elev - 9.8) / (16.8 - 9.8));
} else {
col = mix(s1, sky.sky0.rgb, (elev - 16.8) / (90.0 - 16.8)); // warped ring1 → UNWARPED apex
}
// Raw gamma out (GAMMA LANE, 0161): the reference draws the sky with GL_FRAMEBUFFER_SRGB OFF
// (CSky::Render @0x6d4940) — raw DBC bytes — which is now the whole pipeline's convention.
let rgb = col;
var out: SkyOutput;
out.color = vec4<f32>(rgb, 1.0);
out.depth = SKY_FAR_DEPTH;
return out;
}