ccc3e20902
Smoke: one rule by engine, no heat on trails, no trail on P1-SUN. Searchlights: two lamps a target, off with an empty sky. Halos fade before their reach and their sum is limited. Launch sites load chunks through the server thread, go off-map as a last resort, and reach 4000 blocks. Air defence: ranges x1.5, shots grow with urgency, claims released, /airdef lastchance, long shots on a big radar's tracks. Build: rust-toolchain.toml, airdef.py check, generator manifest and --check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
946 lines
43 KiB
Diff
946 lines
43 KiB
Diff
From 13e74537dd6e63482e3613e0e416de1158944dfe Mon Sep 17 00:00:00 2001
|
|
From: chexit <chexit@duck.com>
|
|
Date: Thu, 29 Oct 2026 18:38:19 +0300
|
|
Subject: [PATCH 21/40] HD sky and light: a physical atmosphere (Rayleigh, Mie,
|
|
ozone; constants and transmittance after Photon, MIT) for the sky, haze, fog
|
|
and water reflections; warmer sunlight from the atmosphere's transmittance, a
|
|
gentle sky-coloured ambient, soft PCSS-lite shadows near the player, a haze
|
|
round the sun; lighter water with fine ripples
|
|
|
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
---
|
|
crates/horizon-client/src/gfx.rs | 4 +
|
|
.../src/gfx/THIRD_PARTY_PHOTON.md | 34 ++
|
|
crates/horizon-client/src/gfx/hd.rs | 259 +++++++++++++-
|
|
crates/horizon-client/src/gfx/hd_sky.rs | 324 ++++++++++++++++++
|
|
crates/horizon-client/src/terrain.wgsl | 2 +-
|
|
5 files changed, 604 insertions(+), 19 deletions(-)
|
|
create mode 100644 crates/horizon-client/src/gfx/THIRD_PARTY_PHOTON.md
|
|
create mode 100644 crates/horizon-client/src/gfx/hd_sky.rs
|
|
|
|
diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs
|
|
index f9dab8abf..d01131433 100644
|
|
--- a/crates/horizon-client/src/gfx.rs
|
|
+++ b/crates/horizon-client/src/gfx.rs
|
|
@@ -72,6 +72,7 @@ mod cull;
|
|
mod end_sky;
|
|
mod flat;
|
|
mod hd;
|
|
+mod hd_sky;
|
|
mod hdr;
|
|
pub mod lightning;
|
|
mod lod;
|
|
@@ -4275,6 +4276,8 @@ impl Renderer {
|
|
camera.z.rem_euclid(256.0) as f32,
|
|
],
|
|
water: !frame.underwater,
|
|
+ sunrise: frame.sky.sunrise_color,
|
|
+ sky_fog_end: frame.fog.map_or(frame.distance.min(512.0), |f| f.sky_end),
|
|
};
|
|
let (shadow_dir, shadow_strength, _) = hd::light_of(&hd_scene);
|
|
let mut hd_cascades = hd::cascades(camera, shadow_dir);
|
|
@@ -5817,6 +5820,7 @@ impl Renderer {
|
|
{
|
|
// AirDef fork: lit by the mods' lights into the HD target, the mod's post pass on
|
|
// that, then bloom and the tone curve onto the screen.
|
|
+ hd.sky(&mut encoder);
|
|
hd.light(
|
|
&self.device,
|
|
&mut encoder,
|
|
diff --git a/crates/horizon-client/src/gfx/THIRD_PARTY_PHOTON.md b/crates/horizon-client/src/gfx/THIRD_PARTY_PHOTON.md
|
|
new file mode 100644
|
|
index 000000000..03d945001
|
|
--- /dev/null
|
|
+++ b/crates/horizon-client/src/gfx/THIRD_PARTY_PHOTON.md
|
|
@@ -0,0 +1,34 @@
|
|
+# Third-party code: Photon
|
|
+
|
|
+Parts of the HD renderer's atmosphere (`hd_sky.rs`) are ported from, or follow closely, the
|
|
+Photon shader pack by sixthsurge, `shaders/include/sky/atmosphere.glsl` (and the idea of the water
|
|
+waves in `shaders/include/misc/water_normal.glsl`) at commit
|
|
+26c2906ee02cc484359b835fba6993d443d45966, https://github.com/sixthsurge/photon
|
|
+
|
|
+What is taken: the air's scattering and absorption coefficients (Rayleigh, Mie, ozone), the scale
|
|
+heights, the ozone density profile, the Mie albedo and Henyey-Greenstein anisotropy, the sunlight
|
|
+colour outside the atmosphere, and the Chapman-function approximation of the sun's transmittance.
|
|
+The sky-view texture, its march, the sun's colour and everything else are our own.
|
|
+
|
|
+Photon is distributed under the MIT License:
|
|
+
|
|
+ The MIT License
|
|
+
|
|
+ Copyright © 2025 Benjamin Stott "sixthsurge"
|
|
+
|
|
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
+ and associated documentation files (the "Software"), to deal in the Software without
|
|
+ restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
|
+ distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
+ Software is furnished to do so, subject to the following conditions:
|
|
+
|
|
+ The above copyright notice and this permission notice shall be included in all copies or
|
|
+ substantial portions of the Software.
|
|
+
|
|
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
+ BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
+
|
|
+No other shader pack's code (BSL, Complementary and so on) may be used: their licences forbid it.
|
|
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
|
|
index 8070aab3c..031e9f2de 100644
|
|
--- a/crates/horizon-client/src/gfx/hd.rs
|
|
+++ b/crates/horizon-client/src/gfx/hd.rs
|
|
@@ -5,6 +5,7 @@
|
|
//! into a high-dynamic-range target; the mod's post pass runs on that; then
|
|
//! bright parts bloom and a tone curve brings it to the screen — vanilla's
|
|
//! range (up to about 0.8) left as it is, only what is brighter rolled off.
|
|
+use super::hd_sky;
|
|
use super::pack_timing::PassTiming;
|
|
use glam::Mat4;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
@@ -101,6 +102,12 @@ struct HdFrame {
|
|
water: [f32; 4],
|
|
/// rgb: the sky's colour; w: 1 when there is a sky.
|
|
sky_color: [f32; 4],
|
|
+ /// xyz: towards the sun itself (not the moon); w: 1 when the atmosphere draws the sky.
|
|
+ sky_sun: [f32; 4],
|
|
+ /// x: vanilla's sky fog end; z: how overcast the sky is (rain: 0 clear, 1 vanilla's colours).
|
|
+ sky_extra: [f32; 4],
|
|
+ /// Vanilla's sunrise colour fan (rgb, alpha), taken out of the sky before the atmosphere is put in.
|
|
+ sunrise: [f32; 4],
|
|
}
|
|
|
|
/// What the world around looks like this frame (from the renderer's globals).
|
|
@@ -131,6 +138,9 @@ pub struct HdScene {
|
|
pub camera_mod: [f32; 2],
|
|
/// The water effect may run (not underwater, not disabled).
|
|
pub water: bool,
|
|
+ /// Vanilla's sunrise fan colour and its sky fog end (to subtract vanilla's sky from the world).
|
|
+ pub sunrise: [f32; 4],
|
|
+ pub sky_fog_end: f32,
|
|
}
|
|
|
|
/// The cascades' half sizes (blocks) and shadow map size.
|
|
@@ -155,6 +165,9 @@ struct HdFrame {
|
|
view_proj: mat4x4<f32>,
|
|
water: vec4<f32>,
|
|
sky_color: vec4<f32>,
|
|
+ sky_sun: vec4<f32>,
|
|
+ sky_extra: vec4<f32>,
|
|
+ sunrise: vec4<f32>,
|
|
}
|
|
struct FullOut {
|
|
@builtin(position) position: vec4<f32>,
|
|
@@ -195,6 +208,9 @@ struct Light {
|
|
@group(0) @binding(13) var ao_tex: texture_2d<f32>;
|
|
// The water pass's depth: the nearest water surface (0 where there is none).
|
|
@group(0) @binding(14) var water_depth: texture_depth_2d;
|
|
+//SKY15
|
|
+// The pixel being lit, for the shadow filter's noise.
|
|
+var<private> frag_px: vec2<f32>;
|
|
|
|
// The occlusion at a full-resolution pixel `dist` away: the four nearest half-resolution texels
|
|
// weighted by closeness and by how near their distance is, so it doesn't bleed across edges.
|
|
@@ -239,6 +255,42 @@ fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
|
|
return shadow_taps(p, n, 1);
|
|
}
|
|
|
|
+// A point of a disc of radius 1 (Vogel's spiral), turned by `angle`.
|
|
+fn vogel(i: i32, count: i32, angle: f32) -> vec2<f32> {
|
|
+ let r = sqrt((f32(i) + 0.5) / f32(count));
|
|
+ let a = f32(i) * 2.39996323 + angle;
|
|
+ return vec2<f32>(cos(a), sin(a)) * r;
|
|
+}
|
|
+
|
|
+// The near cascade's soft shadow: a blocker search sets the penumbra's width (wider the farther
|
|
+// the shadow's caster is above the receiver), then a ring of taps turned by a per-pixel noise
|
|
+// filters it; the temporal pass averages the noise. `s`: the point in the cascade (uv, depth),
|
|
+// `texel`: the cascade's texel in blocks.
|
|
+fn soft_shadow(s: vec3<f32>, texel: f32) -> f32 {
|
|
+ let angle = ign(frag_px) * 6.2831853;
|
|
+ var blocker = 0.0;
|
|
+ var found = 0.0;
|
|
+ for (var i = 0; i < 8; i++) {
|
|
+ let o = vogel(i, 8, angle) * (6.0 / 2048.0);
|
|
+ let q = clamp(vec2<i32>((s.xy + o) * 2048.0), vec2<i32>(0), vec2<i32>(2047));
|
|
+ let z = textureLoad(shadow_map, q, 0, 0);
|
|
+ if z < s.z - 0.0006 {
|
|
+ blocker += z;
|
|
+ found += 1.0;
|
|
+ }
|
|
+ }
|
|
+ if found < 0.5 { return 1.0; }
|
|
+ // The caster's height above the receiver, in blocks (the cascade's depth spans 799 blocks).
|
|
+ let height = (s.z - blocker / found) * 799.0;
|
|
+ let radius = clamp(height * 0.18 / texel, 1.6, 8.0);
|
|
+ var lit = 0.0;
|
|
+ for (var i = 0; i < 12; i++) {
|
|
+ let o = vogel(i, 12, angle + 1.7) * (radius / 2048.0);
|
|
+ lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + o, 0, s.z - 0.0005 - 0.00005 * radius);
|
|
+ }
|
|
+ return lit / 12.0;
|
|
+}
|
|
+
|
|
// `shadow` with a (2 * r + 1) square of taps: the floor under water takes one (the water dims and
|
|
// blurs it anyway).
|
|
fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
|
|
@@ -249,13 +301,16 @@ fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
|
|
let s = cascade_at(q, c);
|
|
if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
|
|
var lit = 0.0;
|
|
+ let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
|
|
+ if r == 1 && c == 0 {
|
|
+ return mix(1.0, soft_shadow(s, texel), fade);
|
|
+ }
|
|
let step = 1.0 / 2048.0;
|
|
for (var y = -r; y <= r; y++) {
|
|
for (var x = -r; x <= r; x++) {
|
|
lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2<f32>(f32(x), f32(y)) * step, c, s.z - 0.0005);
|
|
}
|
|
}
|
|
- let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
|
|
return mix(1.0, lit / f32((2 * r + 1) * (2 * r + 1)), fade);
|
|
}
|
|
|
|
@@ -300,7 +355,31 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
|
let halo_start = tiles[tile + 2u];
|
|
let halo_all = tiles[tile + 3u] == 0xffffffffu;
|
|
let halo_count = select(tiles[tile + 3u], count, halo_all);
|
|
+ frag_px = in.position.xy;
|
|
+ let atmosphere = frame.sky_sun.w > 0.5;
|
|
var c = scene;
|
|
+ // The sky: the atmosphere's, with what the world drew over vanilla's plain sky (sun, moon,
|
|
+ // stars, clouds) kept on top.
|
|
+ if d <= 0.0 && atmosphere {
|
|
+ var kept = max(scene - vanilla_sky(view), vec3<f32>(0.0));
|
|
+ var sky = atmosphere_sky(view);
|
|
+ // The sun: vanilla's square sprite is taken out and a round disc with a soft haze round
|
|
+ // it (warm, strongest low) is drawn in its place.
|
|
+ let toward = dot(view, frame.sky_sun.xyz);
|
|
+ kept *= 1.0 - smoothstep(0.965, 0.975, toward);
|
|
+ let clear = 1.0 - frame.sky_extra.z;
|
|
+ let shine = smoothstep(-0.04, 0.02, frame.sky_sun.y) * clear;
|
|
+ let glow = max(toward, 0.0);
|
|
+ let angle = acos(clamp(toward, -1.0, 1.0));
|
|
+ sky += frame.sun_color.rgb * shine * (pow(glow, 24.0) * 0.07 + pow(glow, 400.0) * 0.4 + 6.0 * (1.0 - smoothstep(0.016, 0.022, angle)));
|
|
+ c = sky + kept;
|
|
+ }
|
|
+ // Clouds (vanilla's, drawn into the world with depth but not into the G-buffer): lit by the low
|
|
+ // sun's colour at dusk and dawn instead of vanilla's flat white.
|
|
+ if atmosphere && !water && d > 0.0 && g <= 0.0 && p.y > 8.0 && dot(p, p) > 2304.0 {
|
|
+ let low = 1.0 - smoothstep(0.05, 0.5, frame.sky_sun.y);
|
|
+ c = scene * mix(vec3<f32>(1.0), mix(frame.sun_color.rgb, vec3<f32>(1.0), 0.45) * 1.1, low * 0.7 * (1.0 - frame.sky_extra.z));
|
|
+ }
|
|
let surface = water || (d > 0.0 && abs(d - g) <= max(d, g) * 0.002);
|
|
if surface {
|
|
let n = normalize(textureLoad(normal_tex, px, 0).xyz);
|
|
@@ -323,7 +402,17 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
|
if frame.ambient.w > 0.0 && !water { occlusion = ao_at(px, length(p)); }
|
|
let ao_ambient = mix(1.0, occlusion, frame.ambient.w);
|
|
let ao_lights = mix(1.0, occlusion, frame.ambient.w * 0.5);
|
|
- var light = max(base * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
|
|
+ // The sky's light is bluish from above and carries the horizon's colour from the sides;
|
|
+ // faces turned up take more of it than the ones turned down.
|
|
+ var sky_tint = vec3<f32>(1.0);
|
|
+ if atmosphere {
|
|
+ let reach = smoothstep(0.25, 0.9, lm.x);
|
|
+ let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
|
+ let shape = mix(0.66, 1.1, smoothstep(0.1, 1.0, up));
|
|
+ let hue = mix(vec3<f32>(1.0), sky_light_tint(n), 0.4 * (1.0 - 0.5 * frame.sun_color.a));
|
|
+ sky_tint = mix(vec3<f32>(1.0), hue * shape, reach);
|
|
+ }
|
|
+ var light = max(base * sky_tint * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
|
|
// The mods' lights.
|
|
for (var k = 0u; k < surface_count; k++) {
|
|
let l = lights[select(tiles[surface_start + k], k, surface_all)];
|
|
@@ -340,7 +429,12 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
|
let sph = length(p);
|
|
let cyl = max(length(p.xz), abs(p.y));
|
|
let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
|
|
- c = mix(c, frame.fog_color.rgb, f * frame.fog_color.a);
|
|
+ let haze = haze_colour(view);
|
|
+ c = mix(c, haze, f * frame.fog_color.a);
|
|
+ // Aerial perspective: far things take the horizon's colour a little before the fog does.
|
|
+ if atmosphere && frame.fog.x > 1.0e6 {
|
|
+ c = mix(c, haze, (1.0 - exp(-sph / 700.0)) * 0.5);
|
|
+ }
|
|
}
|
|
// Light in the air: the sun's rays between the shadows, and halos round the mods' lights.
|
|
let far = select(min(length(p), frame.shadow.z), frame.shadow.z, d <= 0.0);
|
|
@@ -387,6 +481,7 @@ const REFLECT_RANGE: f32 = 96.0;
|
|
@group(0) @binding(7) var shadow_sampler: sampler_comparison;
|
|
@group(0) @binding(8) var lightmap: texture_2d<f32>;
|
|
@group(0) @binding(9) var linear: sampler;
|
|
+//SKY10
|
|
|
|
fn ign(px: vec2<f32>) -> f32 {
|
|
let p = px + vec2<f32>(5.588238 * frame.params.w);
|
|
@@ -417,7 +512,7 @@ fn texel(uv: vec2<f32>) -> vec2<i32> {
|
|
// are whole cycles per 600 s, so the sum repeats every 256 blocks and 10 minutes and holds still
|
|
// in the world as the camera moves. x, y: the vector in units of 2 pi / 256; z: the slope the
|
|
// wave adds; w: its phase.
|
|
-const WAVES = array<vec4<f32>, 10>(
|
|
+const WAVES = array<vec4<f32>, 26>(
|
|
vec4<f32>(3.0, 1.0, 0.040, 0.3),
|
|
vec4<f32>(-2.0, 5.0, 0.045, 2.1),
|
|
vec4<f32>(7.0, 2.0, 0.040, 4.4),
|
|
@@ -428,17 +523,36 @@ const WAVES = array<vec4<f32>, 10>(
|
|
vec4<f32>(9.0, 23.0, 0.030, 2.7),
|
|
vec4<f32>(-27.0, 19.0, 0.025, 4.9),
|
|
vec4<f32>(31.0, 30.0, 0.025, 1.8),
|
|
+ // Fine ripples (wavelengths of 0.7 to 3 blocks) for the many small sun sparkles: slopes small,
|
|
+ // faded out by distance and pixel size like the rest.
|
|
+ vec4<f32>(83.0, 41.0, 0.042, 0.4),
|
|
+ vec4<f32>(-97.0, 59.0, 0.042, 3.1),
|
|
+ vec4<f32>(71.0, -113.0, 0.042, 5.0),
|
|
+ vec4<f32>(131.0, 89.0, 0.040, 1.5),
|
|
+ vec4<f32>(-157.0, -103.0, 0.040, 4.2),
|
|
+ vec4<f32>(191.0, -67.0, 0.038, 2.4),
|
|
+ vec4<f32>(-223.0, 149.0, 0.038, 0.8),
|
|
+ vec4<f32>(101.0, 241.0, 0.036, 3.7),
|
|
+ vec4<f32>(-263.0, -181.0, 0.034, 5.5),
|
|
+ vec4<f32>(311.0, 127.0, 0.034, 2.0),
|
|
+ // Finer still (0.35 to 0.6 blocks): the small sparkles.
|
|
+ vec4<f32>(397.0, -263.0, 0.050, 1.1),
|
|
+ vec4<f32>(-467.0, 331.0, 0.050, 4.6),
|
|
+ vec4<f32>(521.0, 449.0, 0.045, 3.0),
|
|
+ vec4<f32>(-613.0, -199.0, 0.045, 5.9),
|
|
+ vec4<f32>(293.0, 577.0, 0.045, 0.2),
|
|
+ vec4<f32>(-701.0, 263.0, 0.040, 2.8),
|
|
);
|
|
|
|
// The gradient of the water's height at world x, z: the waves that a pixel `footprint` blocks
|
|
// wide can still resolve (finer ones fade out, or they would shimmer).
|
|
fn wave_gradient(xz: vec2<f32>, footprint: f32) -> vec2<f32> {
|
|
var grad = vec2<f32>(0.0);
|
|
- for (var i = 0; i < 10; i++) {
|
|
+ for (var i = 0; i < 26; i++) {
|
|
let w = WAVES[i];
|
|
let k = w.xy * (2.0 * PI / 256.0);
|
|
let len = length(k);
|
|
- let fade = clamp((2.0 * PI / len) / max(footprint, 0.0001) * 0.25 - 0.5, 0.0, 1.0);
|
|
+ let fade = clamp((2.0 * PI / len) / max(footprint, 0.0001) * 0.2 - 0.6, 0.0, 1.0);
|
|
let cycles = round(sqrt(9.8 * len) * 0.5 * 600.0 / (2.0 * PI));
|
|
let omega = cycles * 2.0 * PI / 600.0;
|
|
let phase = dot(k, xz) - omega * frame.water.y + w.w;
|
|
@@ -481,7 +595,9 @@ fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
|
|
// the zenith's, darker where the water lies under cover (`vis` is the sky light there).
|
|
fn sky_along(r: vec3<f32>, vis: f32) -> vec3<f32> {
|
|
var c = frame.fog_color.rgb;
|
|
- if frame.sky_color.w > 0.5 {
|
|
+ if frame.sky_sun.w > 0.5 {
|
|
+ c = atmosphere_sky(r);
|
|
+ } else if frame.sky_color.w > 0.5 {
|
|
c = mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, r.y));
|
|
}
|
|
return c * mix(0.12, 1.0, vis);
|
|
@@ -531,7 +647,7 @@ fn reflection(p: vec3<f32>, r: vec3<f32>, jitter: f32) -> vec4<f32> {
|
|
// The sun's glint on the waves (GGX, a rough lobe so it glitters instead of aliasing).
|
|
fn glint(n: vec3<f32>, v: vec3<f32>, l: vec3<f32>) -> f32 {
|
|
let h = normalize(l + v);
|
|
- let a = 0.05;
|
|
+ let a = 0.07;
|
|
let nh = max(dot(n, h), 0.0);
|
|
let d = a * a / (PI * pow(nh * nh * (a * a - 1.0) + 1.0, 2.0));
|
|
let f = 0.02 + 0.98 * pow(1.0 - max(dot(v, h), 0.0), 5.0);
|
|
@@ -565,7 +681,10 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
|
// Light on the water: vanilla's lightmap at the surface, and the sun where it reaches.
|
|
let levels = vec2<f32>(nt.a, tint.a);
|
|
let sky_reach = smoothstep(0.35, 0.95, levels.x);
|
|
- let ambient = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(levels.y, levels.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
|
|
+ var ambient = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(levels.y, levels.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
|
|
+ if frame.sky_sun.w > 0.5 {
|
|
+ ambient *= mix(vec3<f32>(1.0), sky_light_tint(vec3<f32>(0.0, 1.0, 0.0)), 0.7 * sky_reach);
|
|
+ }
|
|
let block = textureSampleLevel(lightmap, linear, vec2<f32>(levels.y * (15.0 / 16.0) + 0.5 / 16.0, 0.5 / 16.0), 0.0).rgb;
|
|
let sun = frame.sun.xyz;
|
|
let sun_on = frame.sun.w;
|
|
@@ -594,10 +713,10 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
|
// A deep, dark water: the biome's colour darkened and a little greyed, so it reads as
|
|
// depth rather than as bright paint.
|
|
let biome = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
|
|
- let colour = mix(vec3<f32>(dot(biome, vec3<f32>(0.333))), biome, 0.8) * vec3<f32>(0.55, 0.68, 0.8);
|
|
+ let colour = mix(vec3<f32>(dot(biome, vec3<f32>(0.333))), biome, 0.85) * vec3<f32>(0.92, 1.0, 1.0);
|
|
// The colours the water takes up fastest are the ones it has the least of.
|
|
- let absorb = exp(-(vec3<f32>(1.0) - colour) * 1.1 * (thickness + 0.3));
|
|
- let refracted = behind * absorb + colour * volume_light * 0.6 * (vec3<f32>(1.0) - absorb);
|
|
+ let absorb = exp(-(vec3<f32>(1.0) - colour) * 0.8 * (thickness + 0.3));
|
|
+ let refracted = behind * absorb + colour * volume_light * mix(0.85, 0.4, frame.sun_color.a) * (vec3<f32>(1.0) - absorb);
|
|
|
|
// Reflection: the screen where the ray stays on it, the sky where it leaves it or misses.
|
|
var r = reflect(-v, n);
|
|
@@ -617,7 +736,11 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
|
let sph = dist;
|
|
let cyl = max(length(p.xz), abs(p.y));
|
|
let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
|
|
- c = mix(c, frame.fog_color.rgb, f * frame.fog_color.a);
|
|
+ let haze = haze_colour(-v);
|
|
+ c = mix(c, haze, f * frame.fog_color.a);
|
|
+ if frame.sky_sun.w > 0.5 && frame.fog.x > 1.0e6 {
|
|
+ c = mix(c, haze, (1.0 - exp(-dist / 700.0)) * 0.5);
|
|
+ }
|
|
return vec4<f32>(max(c, vec3<f32>(0.0)), 1.0);
|
|
}
|
|
"#;
|
|
@@ -1200,6 +1323,14 @@ pub struct Hd {
|
|
/// The frame's translucent terrain exists (set by the renderer before `write`).
|
|
pub water_geometry: bool,
|
|
water_composited: bool,
|
|
+ /// The sky-view texture (see `hd_sky`), the pass that marches it and its bind group.
|
|
+ sky_lut: wgpu::TextureView,
|
|
+ sky_pipeline: wgpu::RenderPipeline,
|
|
+ sky_group: wgpu::BindGroup,
|
|
+ /// The sun's height the sky-view texture was marched for (it depends on nothing else), and
|
|
+ /// whether this frame's sun has moved enough to march it again.
|
|
+ sky_drawn_for: f32,
|
|
+ sky_stale: bool,
|
|
started: std::time::Instant,
|
|
/// TAA: the resolved frame, and the two histories (one read, one written, alternating).
|
|
taa: wgpu::TextureView,
|
|
@@ -1403,10 +1534,23 @@ fn ao_strength_default() -> f32 {
|
|
.unwrap_or(AO_STRENGTH)
|
|
}
|
|
|
|
+/// The shader's source: the common part, then the body with the sky-reading functions where it
|
|
+/// has a marker (`//SKY15`: the sky-view texture is binding 15, `//SKY10`: binding 10).
|
|
+fn assemble(body: &str) -> String {
|
|
+ let mut body = body.to_owned();
|
|
+ for binding in ["15", "10"] {
|
|
+ body = body.replace(
|
|
+ &format!("//SKY{binding}"),
|
|
+ &hd_sky::SKY_FUNCS.replace("SKYBIND", binding),
|
|
+ );
|
|
+ }
|
|
+ format!("{COMMON}{body}")
|
|
+}
|
|
+
|
|
fn module(device: &wgpu::Device, label: &str, body: &str) -> wgpu::ShaderModule {
|
|
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some(label),
|
|
- source: wgpu::ShaderSource::Wgsl(format!("{COMMON}{body}").into()),
|
|
+ source: wgpu::ShaderSource::Wgsl(assemble(body).into()),
|
|
})
|
|
}
|
|
|
|
@@ -1523,6 +1667,7 @@ impl Hd {
|
|
},
|
|
UNFILTERED,
|
|
DEPTH,
|
|
+ FLOAT,
|
|
]),
|
|
});
|
|
let water_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
@@ -1542,8 +1687,13 @@ impl Hd {
|
|
wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
|
|
FLOAT,
|
|
SAMPLER,
|
|
+ FLOAT,
|
|
]),
|
|
});
|
|
+ let sky_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
+ label: Some("HD sky"),
|
|
+ entries: &entries(&[UNIFORM]),
|
|
+ });
|
|
let taa_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("HD TAA"),
|
|
entries: &entries(&[UNIFORM, UNFILTERED, DEPTH, FLOAT, SAMPLER]),
|
|
@@ -1605,6 +1755,16 @@ impl Hd {
|
|
let ao = module(device, "HD ambient occlusion", AO);
|
|
let ao_blur = module(device, "HD ambient occlusion blur", AO_BLUR);
|
|
let water = module(device, "HD water", WATER);
|
|
+ let sky_module = module(device, "HD sky", hd_sky::SKY_PASS);
|
|
+ let sky_pipeline = fullscreen(
|
|
+ device,
|
|
+ "HD sky",
|
|
+ &sky_layout,
|
|
+ &sky_module,
|
|
+ "fs_sky_lut",
|
|
+ hd_sky::SKY_LUT_FORMAT,
|
|
+ None,
|
|
+ );
|
|
let water_pipeline = fullscreen(
|
|
device,
|
|
"HD water",
|
|
@@ -1691,6 +1851,21 @@ impl Hd {
|
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
+ let sky_lut = target(
|
|
+ device,
|
|
+ "HD sky",
|
|
+ hd_sky::SKY_LUT_SIZE,
|
|
+ hd_sky::SKY_LUT_FORMAT,
|
|
+ )
|
|
+ .view;
|
|
+ let sky_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
+ label: Some("HD sky"),
|
|
+ layout: &sky_layout,
|
|
+ entries: &[wgpu::BindGroupEntry {
|
|
+ binding: 0,
|
|
+ resource: frame.as_entire_binding(),
|
|
+ }],
|
|
+ });
|
|
let lights = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("HD lights"),
|
|
size: (MAX_LIGHTS * std::mem::size_of::<HdLight>()) as u64,
|
|
@@ -1788,6 +1963,11 @@ impl Hd {
|
|
water_active: false,
|
|
water_geometry: false,
|
|
water_composited: false,
|
|
+ sky_lut,
|
|
+ sky_pipeline,
|
|
+ sky_group,
|
|
+ sky_drawn_for: 2.0,
|
|
+ sky_stale: true,
|
|
started: std::time::Instant::now(),
|
|
taa: target(device, "HD TAA", [1, 1], HDR).view,
|
|
history: [
|
|
@@ -1961,6 +2141,11 @@ impl Hd {
|
|
if !self.taa_active || scene.camera_delta.length() > 64.0 {
|
|
self.history_ready = false;
|
|
}
|
|
+ let sun_height = scene.sun.normalize_or(glam::Vec3::Y).y;
|
|
+ self.sky_stale = (sun_height - self.sky_drawn_for).abs() > 0.001;
|
|
+ if self.sky_stale {
|
|
+ self.sky_drawn_for = sun_height;
|
|
+ }
|
|
let frame = HdFrame {
|
|
prev_view_proj: scene.prev_view_proj.to_cols_array_2d(),
|
|
taa: [
|
|
@@ -1982,6 +2167,17 @@ impl Hd {
|
|
scene.sky_color[2],
|
|
if scene.sky { 1.0 } else { 0.0 },
|
|
],
|
|
+ sky_sun: {
|
|
+ let sun = scene.sun.normalize_or(glam::Vec3::Y);
|
|
+ [sun.x, sun.y, sun.z, if scene.sky { 1.0 } else { 0.0 }]
|
|
+ },
|
|
+ sky_extra: [
|
|
+ scene.sky_fog_end,
|
|
+ 0.0,
|
|
+ (scene.rain / 0.7).clamp(0.0, 1.0) * 0.85,
|
|
+ 0.0,
|
|
+ ],
|
|
+ sunrise: scene.sunrise,
|
|
taa_params: [
|
|
if self.upscaling() {
|
|
UPSCALE_HISTORY
|
|
@@ -2123,6 +2319,23 @@ impl Hd {
|
|
pass.draw(0..3, 0..1);
|
|
}
|
|
|
|
+ /// Marches the sky-view texture for this frame's sun (before the lighting and the water); skipped
|
|
+ /// while the sun's height has moved less than a thousandth since the last march.
|
|
+ pub fn sky(&mut self, encoder: &mut wgpu::CommandEncoder) {
|
|
+ if !self.sky_stale {
|
|
+ return;
|
|
+ }
|
|
+ Self::draw(
|
|
+ encoder,
|
|
+ "HD sky",
|
|
+ &self.sky_lut,
|
|
+ false,
|
|
+ &self.sky_pipeline,
|
|
+ &self.sky_group,
|
|
+ stamp(&mut self.timing, "hd sky"),
|
|
+ );
|
|
+ }
|
|
+
|
|
/// Lights the world (its colour and depth) into `lit`.
|
|
pub fn light(
|
|
&mut self,
|
|
@@ -2203,6 +2416,10 @@ impl Hd {
|
|
binding: 14,
|
|
resource: wgpu::BindingResource::TextureView(&self.water_depth),
|
|
},
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 15,
|
|
+ resource: wgpu::BindingResource::TextureView(&self.sky_lut),
|
|
+ },
|
|
],
|
|
});
|
|
self.light_group = Some((key, group));
|
|
@@ -2512,6 +2729,7 @@ impl Hd {
|
|
wgpu::BindingResource::Sampler(&self.shadow_sampler),
|
|
wgpu::BindingResource::TextureView(lightmap.0),
|
|
wgpu::BindingResource::Sampler(&self.sampler),
|
|
+ wgpu::BindingResource::TextureView(&self.sky_lut),
|
|
],
|
|
);
|
|
self.water_group = Some((key, group));
|
|
@@ -2608,9 +2826,13 @@ pub fn light_of(scene: &HdScene) -> (glam::Vec3, f32, glam::Vec3) {
|
|
let clear = 1.0 - scene.rain * 0.85;
|
|
if sun.y > -0.02 {
|
|
let up = smooth(-0.02, 0.18, sun.y);
|
|
- let warm = smooth(0.0, 0.45, sun.y);
|
|
- let color = glam::Vec3::new(1.0, 0.62, 0.38).lerp(glam::Vec3::new(1.0, 0.95, 0.86), warm);
|
|
- (sun, 0.6 * up * clear, color)
|
|
+ // The atmosphere's transmittance along the sun's ray (orange when low, a clean warm white
|
|
+ // high), kept at full brightness and tempered, with a slight warm tint on top.
|
|
+ let t = hd_sky::sun_transmittance(sun.y);
|
|
+ let hue = t / t.max_element().max(1e-4);
|
|
+ let color = glam::Vec3::new(hue.x.powf(0.6), hue.y.powf(0.6), hue.z.powf(0.6))
|
|
+ * glam::Vec3::new(1.0, 0.93, 0.78);
|
|
+ (sun, 0.7 * up * clear, color)
|
|
} else {
|
|
let moon = -sun;
|
|
let up = smooth(-0.02, 0.18, moon.y);
|
|
@@ -2661,8 +2883,9 @@ mod tests {
|
|
("ao", AO),
|
|
("ao blur", AO_BLUR),
|
|
("water", WATER),
|
|
+ ("sky", hd_sky::SKY_PASS),
|
|
] {
|
|
- let source = format!("{COMMON}{body}");
|
|
+ let source = assemble(body);
|
|
let module = naga::front::wgsl::parse_str(&source).unwrap_or_else(|e| {
|
|
panic!("{name}: {}", e.emit_to_string(&source));
|
|
});
|
|
diff --git a/crates/horizon-client/src/gfx/hd_sky.rs b/crates/horizon-client/src/gfx/hd_sky.rs
|
|
new file mode 100644
|
|
index 000000000..b5c231a7b
|
|
--- /dev/null
|
|
+++ b/crates/horizon-client/src/gfx/hd_sky.rs
|
|
@@ -0,0 +1,324 @@
|
|
+//! AirDef fork: the HD renderer's atmosphere. A small sky-view texture (azimuth from the sun by
|
|
+//! elevation, plus a row of ambient samples) is marched each frame from single scattering of
|
|
+//! Rayleigh and Mie particles and ozone absorption with a cheap multiple-scattering term; the
|
|
+//! lighting and water shaders read it for the sky, the aerial-perspective haze, the water's
|
|
+//! reflection fallback and the colour of the sky's light. The sunlight's colour is the same
|
|
+//! atmosphere's transmittance at the sun's elevation (`sun_transmittance`).
|
|
+//!
|
|
+//! The coefficients, the density profiles (scale heights, the ozone layer), the Henyey-Greenstein
|
|
+//! Mie parameters and the Chapman-function transmittance come from Photon by sixthsurge (MIT
|
|
+//! licence, see THIRD_PARTY_PHOTON.md), shaders/include/sky/atmosphere.glsl at commit 26c2906.
|
|
+
|
|
+/// The sky-view texture: 128 azimuths by 64 elevations, then 8 rows for the ambient samples
|
|
+/// (row 64: texel 0 the zenith, texel 1 the horizon's average).
|
|
+pub const SKY_LUT_SIZE: [u32; 2] = [128, 72];
|
|
+pub const SKY_LUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
|
+
|
|
+/// The atmosphere's constants and the pass that marches the sky-view texture (after `COMMON`).
|
|
+pub const SKY_PASS: &str = r#"
|
|
+@group(0) @binding(0) var<uniform> frame: HdFrame;
|
|
+
|
|
+// Photon's atmosphere (sixthsurge, MIT): metres, Earth's size; the camera stands on the surface.
|
|
+const PI: f32 = 3.14159265;
|
|
+const PLANET_R: f32 = 6371000.0;
|
|
+const ATMOSPHERE_TOP: f32 = 6481000.0;
|
|
+const GROUND_R: f32 = 6370000.0;
|
|
+const SCALE_HEIGHTS: vec2<f32> = vec2<f32>(8400.0, 1250.0);
|
|
+const RAYLEIGH: vec3<f32> = vec3<f32>(8.059375432e-06, 1.671209429e-05, 4.080133294e-05);
|
|
+const MIE: vec3<f32> = vec3<f32>(1.666442358e-06, 1.812685127e-06, 1.958927896e-06);
|
|
+const OZONE: vec3<f32> = vec3<f32>(8.304280072e-07, 1.314911970e-06, 5.440679729e-08);
|
|
+const MIE_ALBEDO: f32 = 0.9;
|
|
+const MIE_G: f32 = 0.77;
|
|
+// Sunlight in space (Photon's AM0 colour) and how bright the sun and the moon shine here.
|
|
+const SUN_I: vec3<f32> = vec3<f32>(1.051, 0.985, 0.940) * 24.0;
|
|
+const MOON_I: vec3<f32> = vec3<f32>(0.62, 0.78, 1.0) * 0.07;
|
|
+// The share of the light scattered a second time, spread evenly (the haze's lift).
|
|
+const MULTI_SCATTER: f32 = 0.6;
|
|
+const STEPS: i32 = 32;
|
|
+
|
|
+// Photon's density: Rayleigh, Mie (exponential) and the ozone layer, at an altitude in metres.
|
|
+fn density_at(altitude: f32) -> vec3<f32> {
|
|
+ let rm = exp(-max(altitude, 0.0) / SCALE_HEIGHTS);
|
|
+ let km = altitude * 1e-3;
|
|
+ let o1 = 12.5 * exp(-km / 8.0);
|
|
+ let o2 = 30.0 * exp((18.0 - km) * (km - 18.0) / 80.0);
|
|
+ let o3 = 75.0 * exp((23.5 - km) * (km - 23.5) / 50.0);
|
|
+ let o4 = 50.0 * exp((30.0 - km) * (km - 30.0) / 150.0);
|
|
+ return vec3<f32>(rm, 7.428e-3 * (o1 + o2 + o3 + o4));
|
|
+}
|
|
+
|
|
+// Chapman function approximation (thetenthplanet.de/archives/4519, as Photon uses it).
|
|
+fn chapman(x: f32, cos_theta: f32) -> f32 {
|
|
+ let c = sqrt(1.5707963 * x);
|
|
+ if cos_theta >= 0.0 {
|
|
+ return c / ((c - 1.0) * cos_theta + 1.0);
|
|
+ }
|
|
+ let sin_theta = sqrt(clamp(1.0 - cos_theta * cos_theta, 0.0, 1.0));
|
|
+ return c / ((c - 1.0) * cos_theta - 1.0) + 2.0 * c * exp(min(x - x * sin_theta, 80.0)) * sqrt(sin_theta);
|
|
+}
|
|
+
|
|
+// The light reaching a point r from the planet's centre (altitude h, whose sky direction makes
|
|
+// cos mu with the up there) from the sun or the moon; 0 when the planet is in the way.
|
|
+fn light_transmittance(mu: f32, r: f32, h: f32) -> vec3<f32> {
|
|
+ if mu < 0.0 && r * r * (1.0 - mu * mu) < PLANET_R * PLANET_R { return vec3<f32>(0.0); }
|
|
+ let dens = exp(-max(h, 0.0) / SCALE_HEIGHTS);
|
|
+ var air = SCALE_HEIGHTS * dens;
|
|
+ air.x *= chapman(r / SCALE_HEIGHTS.x, mu);
|
|
+ air.y *= chapman(r / SCALE_HEIGHTS.y, mu);
|
|
+ return clamp(exp(-(RAYLEIGH * air.x + MIE * air.y + OZONE * air.x)), vec3<f32>(0.0), vec3<f32>(1.0));
|
|
+}
|
|
+
|
|
+fn rayleigh_phase(nu: f32) -> f32 {
|
|
+ return 3.0 / (16.0 * PI) * (1.0 + nu * nu);
|
|
+}
|
|
+
|
|
+fn mie_phase(nu: f32) -> f32 {
|
|
+ let g = MIE_G;
|
|
+ return (1.0 - g * g) / (4.0 * PI * pow(1.0 + g * g - 2.0 * g * nu, 1.5));
|
|
+}
|
|
+
|
|
+// The radiance the camera sees along `view` (y up) with the sun and the moon in the directions
|
|
+// given: single scattering plus a flat multiple-scattering term.
|
|
+fn march(view: vec3<f32>, sun: vec3<f32>, moon: vec3<f32>) -> vec3<f32> {
|
|
+ let b = PLANET_R * view.y;
|
|
+ let t_max = -b + sqrt(b * b + ATMOSPHERE_TOP * ATMOSPHERE_TOP - PLANET_R * PLANET_R);
|
|
+ let nu_sun = dot(view, sun);
|
|
+ let nu_moon = dot(view, moon);
|
|
+ let phase_r_sun = rayleigh_phase(nu_sun);
|
|
+ let phase_m_sun = mie_phase(nu_sun);
|
|
+ let phase_r_moon = rayleigh_phase(nu_moon);
|
|
+ let phase_m_moon = mie_phase(nu_moon);
|
|
+ var od = vec3<f32>(0.0);
|
|
+ var sum = vec3<f32>(0.0);
|
|
+ var t0 = 0.0;
|
|
+ for (var i = 0; i < STEPS; i++) {
|
|
+ let u = (f32(i) + 1.0) / f32(STEPS);
|
|
+ let t1 = t_max * u * u;
|
|
+ let dt = t1 - t0;
|
|
+ let t = 0.5 * (t0 + t1);
|
|
+ t0 = t1;
|
|
+ let r2 = PLANET_R * PLANET_R + 2.0 * PLANET_R * t * view.y + t * t;
|
|
+ let r = sqrt(r2);
|
|
+ // The altitude without the cancellation of r - planet radius.
|
|
+ let altitude = (2.0 * PLANET_R * t * view.y + t * t) / (r + PLANET_R);
|
|
+ let dens = density_at(altitude);
|
|
+ let extinction = RAYLEIGH * dens.x + MIE * dens.y + OZONE * dens.z;
|
|
+ let view_t = exp(-(od + 0.5 * extinction * dt));
|
|
+ od += extinction * dt;
|
|
+ let mu_sun = (PLANET_R * sun.y + t * nu_sun) / r;
|
|
+ let mu_moon = (PLANET_R * moon.y + t * nu_moon) / r;
|
|
+ let sun_t = light_transmittance(mu_sun, r, altitude);
|
|
+ let moon_t = light_transmittance(mu_moon, r, altitude);
|
|
+ let scatter_r = RAYLEIGH * dens.x;
|
|
+ let scatter_m = MIE * MIE_ALBEDO * dens.y;
|
|
+ let single = (scatter_r * phase_r_sun + scatter_m * phase_m_sun) * sun_t * SUN_I
|
|
+ + (scatter_r * phase_r_moon + scatter_m * phase_m_moon) * moon_t * MOON_I;
|
|
+ let multi = (scatter_r + scatter_m) * (1.0 / (4.0 * PI)) * MULTI_SCATTER
|
|
+ * (sun_t * SUN_I + moon_t * MOON_I) * 0.5;
|
|
+ sum += view_t * (single + multi) * dt;
|
|
+ }
|
|
+ return sum;
|
|
+}
|
|
+
|
|
+@fragment
|
|
+fn fs_sky_lut(in: FullOut) -> @location(0) vec4<f32> {
|
|
+ let px = vec2<i32>(in.position.xy);
|
|
+ // The texture's frame: the sun in the xy plane, the moon opposite.
|
|
+ let sin_e = clamp(frame.sky_sun.y, -1.0, 1.0);
|
|
+ let sun = vec3<f32>(sqrt(1.0 - sin_e * sin_e), sin_e, 0.0);
|
|
+ let moon = -sun;
|
|
+ // A faint night glow, so the night sky is not black.
|
|
+ let night = vec3<f32>(0.0006, 0.0011, 0.0026);
|
|
+ if px.y < 64 {
|
|
+ let phi = f32(px.x) / 127.0 * PI;
|
|
+ let v = f32(px.y) / 63.0;
|
|
+ let l = v * v * PI * 0.5;
|
|
+ let dir = vec3<f32>(cos(l) * cos(phi), sin(l), cos(l) * sin(phi));
|
|
+ return vec4<f32>(march(dir, sun, moon) + night, 1.0);
|
|
+ }
|
|
+ if px.y == 64 && px.x == 0 {
|
|
+ return vec4<f32>(march(vec3<f32>(0.0, 1.0, 0.0), sun, moon) + night, 1.0);
|
|
+ }
|
|
+ if px.y == 64 && px.x == 1 {
|
|
+ var sum = vec3<f32>(0.0);
|
|
+ for (var k = 0; k < 6; k++) {
|
|
+ let phi = (f32(k) + 0.5) / 6.0 * PI;
|
|
+ let l = 0.3;
|
|
+ sum += march(vec3<f32>(cos(l) * cos(phi), sin(l), cos(l) * sin(phi)), sun, moon);
|
|
+ }
|
|
+ return vec4<f32>(sum / 6.0 + night, 1.0);
|
|
+ }
|
|
+ return vec4<f32>(0.0);
|
|
+}
|
|
+"#;
|
|
+
|
|
+/// Reading the sky-view texture, in the lighting and the water shaders (`SKYBIND` is replaced by
|
|
+/// the binding of the texture; `frame` and the `linear` sampler come before it).
|
|
+pub const SKY_FUNCS: &str = r#"
|
|
+@group(0) @binding(SKYBIND) var sky_lut: texture_2d<f32>;
|
|
+const SKY_PI: f32 = 3.14159265;
|
|
+// How bright the sky is shown (the exposure of the physical radiance) and its gamma.
|
|
+const SKY_EXPOSURE: f32 = 0.85;
|
|
+
|
|
+// Where the direction `dir` (world, y up) lies in the sky-view texture.
|
|
+fn sky_uv(dir: vec3<f32>) -> vec2<f32> {
|
|
+ let s = frame.sky_sun.xyz;
|
|
+ let l = asin(clamp(dir.y, 0.0, 1.0));
|
|
+ var cphi = 1.0;
|
|
+ let dl = length(dir.xz);
|
|
+ let sl = length(s.xz);
|
|
+ if dl > 1e-4 && sl > 1e-4 { cphi = dot(dir.xz / dl, s.xz / sl); }
|
|
+ let u = acos(clamp(cphi, -1.0, 1.0)) / SKY_PI;
|
|
+ let v = sqrt(l / (SKY_PI * 0.5));
|
|
+ return vec2<f32>((u * 127.0 + 0.5) / 128.0, (v * 63.0 + 0.5) / 72.0);
|
|
+}
|
|
+
|
|
+// The physical sky's colour as it is shown: a soft shoulder, then the display's gamma.
|
|
+fn sky_show(radiance: vec3<f32>) -> vec3<f32> {
|
|
+ // Compressed by the brightest channel, so the hue stays as the atmosphere made it.
|
|
+ let peak = max(max(radiance.r, radiance.g), max(radiance.b, 1e-6));
|
|
+ let c = pow(radiance * ((1.0 - exp(-peak * SKY_EXPOSURE)) / peak), vec3<f32>(1.0 / 1.9));
|
|
+ // A little more colour (Photon boosts the atmosphere's saturation too).
|
|
+ // The low sun gets more, for orange and pink in place of pastel.
|
|
+ let boost = 1.2 + 0.45 * (1.0 - smoothstep(0.0, 0.35, frame.sky_sun.y));
|
|
+ return max(mix(vec3<f32>(dot(c, vec3<f32>(0.2126, 0.7152, 0.0722))), c, boost), vec3<f32>(0.0));
|
|
+}
|
|
+
|
|
+// The vanilla sky's own gradient (the overcast and the rain keep it).
|
|
+fn vanilla_gradient(dir: vec3<f32>) -> vec3<f32> {
|
|
+ return mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, max(dir.y, 0.0)));
|
|
+}
|
|
+
|
|
+// The sky along `dir`: the atmosphere, greyed towards vanilla's colours in rain.
|
|
+fn atmosphere_sky(dir: vec3<f32>) -> vec3<f32> {
|
|
+ let lin = textureSampleLevel(sky_lut, linear, sky_uv(dir), 0.0).rgb;
|
|
+ var c = sky_show(lin);
|
|
+ let overcast = frame.sky_extra.z;
|
|
+ if overcast > 0.0 { c = mix(c, vanilla_gradient(dir), overcast); }
|
|
+ return c;
|
|
+}
|
|
+
|
|
+// The colour distant things fade to: the sky just above the horizon in their direction (so the
|
|
+// haze leans towards the sun), or vanilla's fog colour where the environment sets its own fog.
|
|
+fn haze_colour(dir: vec3<f32>) -> vec3<f32> {
|
|
+ if frame.sky_sun.w < 0.5 || frame.fog.x < 1.0e6 { return frame.fog_color.rgb; }
|
|
+ let up = max(dir.y, 0.0) * 0.5 + 0.035;
|
|
+ return atmosphere_sky(normalize(vec3<f32>(dir.x, 0.0, dir.z) + vec3<f32>(0.0, up, 0.0)));
|
|
+}
|
|
+
|
|
+// The sky's light for a surface of normal n: the horizon's colour from the sides, the zenith's
|
|
+// from above, as a hue (luminance 1) to tint vanilla's sky light with.
|
|
+fn sky_light_tint(n: vec3<f32>) -> vec3<f32> {
|
|
+ let zenith = sky_show(textureLoad(sky_lut, vec2<i32>(0, 64), 0).rgb);
|
|
+ let horizon = sky_show(textureLoad(sky_lut, vec2<i32>(1, 64), 0).rgb);
|
|
+ let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
|
+ let c = mix(horizon, zenith, smoothstep(0.35, 1.0, up));
|
|
+ let luma = dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
+ return c / max(luma, 0.003);
|
|
+}
|
|
+
|
|
+// vanilla's sky (terrain.wgsl: fs_sky), to tell what the world drew on the sky pixel (the sun and
|
|
+// moon sprites, the stars, the clouds) from its plain gradient.
|
|
+fn vanilla_disc_distance(d: vec3<f32>, y: f32) -> vec2<f32> {
|
|
+ if d.y * y <= 0.0 { return vec2<f32>(-1.0); }
|
|
+ let p = d * (y / d.y);
|
|
+ let h = length(p.xz);
|
|
+ let phi = atan2(p.z, p.x);
|
|
+ let sector = SKY_PI / 4.0;
|
|
+ let delta = phi - (floor(phi / sector) * sector + sector * 0.5);
|
|
+ let edge = 512.0 * cos(sector * 0.5) / cos(delta);
|
|
+ if h > edge { return vec2<f32>(-1.0); }
|
|
+ let s = h / edge;
|
|
+ let ay = abs(y);
|
|
+ let rim = sqrt(512.0 * 512.0 + y * y);
|
|
+ return vec2<f32>(ay + s * (rim - ay), ay + s * (512.0 - ay));
|
|
+}
|
|
+fn vanilla_linear_fog(d: f32, start: f32, end: f32) -> f32 {
|
|
+ if d <= start { return 0.0; }
|
|
+ if d >= end { return 1.0; }
|
|
+ return (d - start) / (end - start);
|
|
+}
|
|
+fn vanilla_rotate_x90(v: vec3<f32>) -> vec3<f32> { return vec3<f32>(v.x, -v.z, v.y); }
|
|
+fn vanilla_sunrise_weight(d: vec3<f32>) -> f32 {
|
|
+ let alpha = frame.sunrise.a;
|
|
+ if alpha <= 0.001 { return 0.0; }
|
|
+ let flip = select(0.0, 180.0, frame.sky_sun.x > 0.0);
|
|
+ let theta = radians(flip + 90.0);
|
|
+ let c = cos(theta);
|
|
+ let s = sin(theta);
|
|
+ var best = 0.0;
|
|
+ let apex = vanilla_rotate_x90(vec3<f32>(-100.0 * s, 100.0 * c, 0.0));
|
|
+ for (var i = 0; i < 16; i++) {
|
|
+ let a0 = f32(i) * 2.0 * SKY_PI / 16.0;
|
|
+ let a1 = f32(i + 1) * 2.0 * SKY_PI / 16.0;
|
|
+ let l0 = vec3<f32>(sin(a0) * 120.0, cos(a0) * 120.0, -cos(a0) * 40.0 * alpha);
|
|
+ let l1 = vec3<f32>(sin(a1) * 120.0, cos(a1) * 120.0, -cos(a1) * 40.0 * alpha);
|
|
+ let r0 = vanilla_rotate_x90(vec3<f32>(l0.x * c - l0.y * s, l0.x * s + l0.y * c, l0.z));
|
|
+ let r1 = vanilla_rotate_x90(vec3<f32>(l1.x * c - l1.y * s, l1.x * s + l1.y * c, l1.z));
|
|
+ let e1 = r0 - apex;
|
|
+ let e2 = r1 - apex;
|
|
+ let pv = cross(d, e2);
|
|
+ let det = dot(e1, pv);
|
|
+ if abs(det) < 1e-6 { continue; }
|
|
+ let tv = -apex;
|
|
+ let u = dot(tv, pv) / det;
|
|
+ let qv = cross(tv, e1);
|
|
+ let v = dot(d, qv) / det;
|
|
+ let t = dot(e2, qv) / det;
|
|
+ if u >= 0.0 && v >= 0.0 && u + v <= 1.0 && t > 0.0 {
|
|
+ best = max(best, 1.0 - u - v);
|
|
+ }
|
|
+ }
|
|
+ return best;
|
|
+}
|
|
+fn vanilla_sky(d: vec3<f32>) -> vec3<f32> {
|
|
+ var color = frame.fog_color.rgb;
|
|
+ let dist = vanilla_disc_distance(d, 16.0);
|
|
+ if dist.x >= 0.0 {
|
|
+ let end = frame.sky_extra.x;
|
|
+ let f = max(vanilla_linear_fog(dist.x, 0.0, end), vanilla_linear_fog(dist.y, end, end));
|
|
+ color = mix(frame.sky_color.rgb, frame.fog_color.rgb, f * frame.fog_color.a);
|
|
+ }
|
|
+ let w = vanilla_sunrise_weight(d);
|
|
+ if w > 0.0 {
|
|
+ color = mix(color, frame.sunrise.rgb, w * frame.sunrise.a);
|
|
+ }
|
|
+ return color;
|
|
+}
|
|
+"#;
|
|
+
|
|
+/// The sun's light at the ground: the atmosphere's transmittance along the sun's direction
|
|
+/// (`sin_elevation`, the y of the unit vector towards it), linear, as a fraction of the light
|
|
+/// outside the air. Same Chapman model as the sky pass.
|
|
+pub fn sun_transmittance(sin_elevation: f32) -> glam::Vec3 {
|
|
+ const PLANET: f32 = 6_371_000.0;
|
|
+ const SCALE: [f32; 2] = [8400.0, 1250.0];
|
|
+ const RAYLEIGH: glam::Vec3 = glam::Vec3::new(8.059_375e-6, 1.671_209_4e-5, 4.080_133_4e-5);
|
|
+ const MIE: glam::Vec3 = glam::Vec3::new(1.666_442_4e-6, 1.812_685e-6, 1.958_927_9e-6);
|
|
+ const OZONE: glam::Vec3 = glam::Vec3::new(8.304_28e-7, 1.314_912e-6, 5.440_679_7e-8);
|
|
+ // A sun below the horizon keeps the horizon's colour (its strength is faded elsewhere).
|
|
+ let mu = sin_elevation.clamp(0.0, 1.0);
|
|
+ let chapman = |x: f32, c: f32| {
|
|
+ let k = (std::f32::consts::FRAC_PI_2 * x).sqrt();
|
|
+ k / ((k - 1.0) * c + 1.0)
|
|
+ };
|
|
+ let air = [
|
|
+ SCALE[0] * chapman(PLANET / SCALE[0], mu),
|
|
+ SCALE[1] * chapman(PLANET / SCALE[1], mu),
|
|
+ ];
|
|
+ let tau = RAYLEIGH * air[0] + MIE * air[1] + OZONE * air[0];
|
|
+ glam::Vec3::new((-tau.x).exp(), (-tau.y).exp(), (-tau.z).exp())
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::*;
|
|
+
|
|
+ #[test]
|
|
+ fn the_low_sun_is_redder_than_the_high_one() {
|
|
+ let noon = sun_transmittance(1.0);
|
|
+ let low = sun_transmittance(0.1);
|
|
+ assert!(noon.x > 0.85 && noon.z > 0.6, "{noon:?}");
|
|
+ assert!(low.z / low.x < noon.z / noon.x * 0.6, "{low:?} vs {noon:?}");
|
|
+ }
|
|
+}
|
|
diff --git a/crates/horizon-client/src/terrain.wgsl b/crates/horizon-client/src/terrain.wgsl
|
|
index e892d5a0f..0a2a6cdba 100644
|
|
--- a/crates/horizon-client/src/terrain.wgsl
|
|
+++ b/crates/horizon-client/src/terrain.wgsl
|
|
@@ -197,7 +197,7 @@ struct WaterOut {
|
|
if layer != WATER_STILL && layer != WATER_FLOW { discard; }
|
|
// The biome's colour at the texture's mean brightness, not the texture: the HD water draws
|
|
// its own surface (waves, reflections), and vanilla's ripples on top would look painted on.
|
|
- let color = vec3(0.62) * in.tint.rgb;
|
|
+ let color = vec3(0.85) * in.tint.rgb;
|
|
var out: WaterOut;
|
|
out.tint = vec4(color, clamp(in.lm.y, 0.0, 1.0));
|
|
out.normal = vec4(face_normal(in.pos) * 0.5 + 0.5, clamp(in.lm.x, 0.0, 1.0));
|
|
--
|
|
2.54.0 (Apple Git-157)
|
|
|