HD: Photon-style sunsets (Rec. 2020 atmosphere, sun tint, blue hour; MIT), volumetric clouds with ground shadows instead of vanilla's in HD, volumetric fog with sun shafts and morning mist (HORIZON_HD_CLOUDS=0, HORIZON_HD_VOLFOG=0)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 21:41:48 +03:00
committed by airdef
parent a8b85116f4
commit a8235704e4
6 changed files with 1448 additions and 53 deletions
+20 -4
View File
@@ -72,7 +72,9 @@ mod cull;
mod end_sky;
mod flat;
mod hd;
mod hd_noise;
mod hd_sky;
mod hd_volumes;
mod hdr;
pub mod lightning;
mod lod;
@@ -4278,6 +4280,17 @@ impl Renderer {
water: !frame.underwater,
sunrise: frame.sky.sunrise_color,
sky_fog_end: frame.fog.map_or(frame.distance.min(512.0), |f| f.sky_end),
// AirDef fork: volumetric clouds stand where vanilla's would (option on, Overworld).
clouds: frame.clouds.is_some() && hd::clouds_enabled(),
camera_world: [
camera.x.rem_euclid(65536.0) as f32,
camera.z.rem_euclid(65536.0) as f32,
camera.y as f32,
],
seconds: frame
.clouds
.as_ref()
.map_or(0.0, |c| ((c.time as f64 + c.partial_tick) / 20.0) as f32),
};
let (shadow_dir, shadow_strength, _) = hd::light_of(&hd_scene);
let mut hd_cascades = hd::cascades(camera, shadow_dir);
@@ -5071,6 +5084,7 @@ impl Renderer {
}
}
let pack_reads_world = pack_post || native_post || mod_post || hd_on;
let hd_volumetric_clouds = hd_on && hd::clouds_enabled();
// The pack's program reads the vertices this frame's draws bind.
let pack_draws_terrain = pack_terrain && flat_frame == self.pack_terrain_flat;
// The shadow camera's lists, drawn by the pack's shadow programs.
@@ -5128,9 +5142,9 @@ impl Renderer {
let format = self.config.format.remove_srgb_suffix();
let device = &self.device;
let globals_size = std::mem::size_of::<Globals>() as u64;
let hd = self
.hd
.get_or_insert_with(|| hd::Hd::new(device, size, format, globals_size));
let hd = self.hd.get_or_insert_with(|| {
hd::Hd::new(device, &self.queue, size, format, globals_size)
});
hd.resize(device, size, [self.config.width, self.config.height]);
// Translucent terrain on screen: the water pass has something to draw.
hd.water_geometry = match self.flat.as_ref().filter(|_| flat_frame) {
@@ -5691,7 +5705,8 @@ impl Renderer {
0..1,
);
}
if cloud_quads > 0 && !pack_draws_clouds {
// AirDef fork: HD draws the clouds volumetrically (in the lighting) instead.
if cloud_quads > 0 && !pack_draws_clouds && !hd_volumetric_clouds {
pass.set_pipeline(&self.cloud_pipeline);
pass.set_bind_group(0, &self.entity_group, &[]);
pass.set_bind_group(1, &self.cloud_group, &[]);
@@ -5821,6 +5836,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.volumes(&self.device, &mut encoder, depth);
hd.light(
&self.device,
&mut encoder,
@@ -1,14 +1,26 @@
# 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
Photon shader pack by sixthsurge, `shaders/include/sky/atmosphere.glsl`, `shaders/include/lighting/colors/light_color.glsl`, `shaders/include/utility/phase_functions.glsl`, `shaders/include/sky/clouds/{common,cumulus}.glsl`, `shaders/include/lighting/cloud_shadows.glsl`, `shaders/include/fog/air_fog_vl.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.
What is taken from `atmosphere.glsl`: the air's scattering and absorption coefficients (Rayleigh, Mie,
ozone) moved to Rec. 2020 as Photon does (the march runs in that space, the result goes back to Rec. 709),
the scale heights, the ozone density profile, the Mie albedo, the Klein-Nishina Mie phase and the
depolarised Rayleigh phase (`phase_functions.glsl`), the sunlight colour outside the atmosphere, and the
Chapman-function approximation of the sun's transmittance. From `light_color.glsl`: `get_sun_tint` and
the blue-hour / sunset exposure lift (`sun_light_tint`, `sun_tint`), and `pulse`. The sky-view texture and
its march are our own.
The volumetric clouds and fog (`hd_volumes.rs`, `hd_noise.rs` for the CPU-made noise textures) port
from `include/sky/clouds/common.glsl` and `cumulus.glsl` (the density shaping with the egg shape and
edge sharpening, Worley erosion, the single and multiple scattering phase functions, the powder effect,
the eight-octave multiple-scattering loop, the light march with doubling steps, the transmittance remap,
the aerial perspective), `include/lighting/cloud_shadows.glsl` (the cloud shadow map, here a lookup into a
small map) and `include/fog/air_fog_vl.glsl` (the phase mix of two Henyey-Greenstein lobes, the evening
glow). The 3D Worley and 2D noise textures are generated on our side; the ray march, the passes, the
reprojection and the integration are our own.
Photon is distributed under the MIT License:
+317 -28
View File
@@ -6,6 +6,8 @@
//! 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;
pub use super::hd_volumes::clouds_enabled;
use super::hd_volumes::{self, HdVolumes};
use super::pack_timing::PassTiming;
use glam::Mat4;
use std::sync::atomic::{AtomicU64, Ordering};
@@ -108,6 +110,18 @@ struct HdFrame {
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],
/// x: the cloud slab's base height relative to the camera; y: its thickness; z: 1 when the
/// volumetric clouds are drawn; w: seconds (the wind).
clouds: [f32; 4],
/// xy: the camera's x and z in the world modulo 65536; z: extra coverage (rain); w: 1 when the
/// clouds' history is usable.
cloud_cam: [f32; 4],
/// xyz: the direction the clouds are lit from (the sun, or the moon at night).
cloud_dir: [f32; 4],
/// rgb: the colour of that light at the clouds; w: its strength.
cloud_sun: [f32; 4],
/// x: 1 when the volumetric fog is drawn; y: the camera's height in the world; z: the morning mist.
vfog: [f32; 4],
}
/// What the world around looks like this frame (from the renderer's globals).
@@ -141,6 +155,11 @@ pub struct HdScene {
/// 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 volumetric clouds are drawn (vanilla's option is on, in the Overworld).
pub clouds: bool,
/// The camera's world position, x and z modulo 65536 (precision), and y; seconds of game time.
pub camera_world: [f32; 3],
pub seconds: f32,
}
/// The cascades' half sizes (blocks) and shadow map size.
@@ -168,6 +187,11 @@ struct HdFrame {
sky_sun: vec4<f32>,
sky_extra: vec4<f32>,
sunrise: vec4<f32>,
clouds: vec4<f32>,
cloud_cam: vec4<f32>,
cloud_dir: vec4<f32>,
cloud_sun: vec4<f32>,
vfog: vec4<f32>,
}
struct FullOut {
@builtin(position) position: vec4<f32>,
@@ -209,6 +233,13 @@ struct Light {
// 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 volumetric fog (half resolution: light in rgb, transmittance in a), the distance it was marched
// to, the volumetric clouds (half resolution: light, transmittance) and the clouds' shadow map.
@group(0) @binding(16) var fog_tex: texture_2d<f32>;
@group(0) @binding(17) var fog_dist_tex: texture_2d<f32>;
@group(0) @binding(18) var cloud_tex: texture_2d<f32>;
@group(0) @binding(19) var cloud_shadow_tex: texture_2d<f32>;
const CLOUD_SHADOW_R: f32 = 1200.0;
// The pixel being lit, for the shadow filter's noise.
var<private> frag_px: vec2<f32>;
@@ -327,6 +358,51 @@ fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
return (d - start) / (end - start);
}
// The share of the sunlight the clouds let through at a camera-relative point: the shadow map
// holds, for the sun's way down through the slab from a point of its base, the optical depth
// (Photon's cloud shadow map, as a lookup rather than a pass).
fn cloud_shadow(p: vec3<f32>) -> f32 {
if frame.clouds.z < 0.5 { return 1.0; }
let l = frame.cloud_dir.xyz;
if l.y < 0.1 { return 1.0; }
let t = (frame.clouds.x - p.y) / l.y;
if t <= 0.0 { return 1.0; }
let q = p.xz + l.xz * t;
let uv = q / (2.0 * CLOUD_SHADOW_R) + vec2<f32>(0.5);
if any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0)) { return 1.0; }
let od = textureSampleLevel(cloud_shadow_tex, linear, uv, 0.0).r;
// Fade out as the sun gets low (the shadow then falls far from the cloud).
let fade = smoothstep(0.1, 0.25, l.y);
return mix(1.0, mix(1.0, exp(-od * 0.9), 0.9), fade);
}
// The volumetric fog at a full-resolution pixel `dist` away: the four nearest half-resolution
// texels weighted by closeness and by how near their march's length is (so it does not bleed over edges).
fn fog_at(px: vec2<i32>, dist: f32) -> vec4<f32> {
let size = vec2<i32>(textureDimensions(fog_tex));
let f = vec2<f32>(px) * 0.5 - vec2<f32>(0.25);
let base = vec2<i32>(floor(f));
let t = f - floor(f);
var sum = vec4<f32>(0.0);
var total = 0.0;
for (var j = 0; j < 2; j++) {
for (var i = 0; i < 2; i++) {
let q = clamp(base + vec2<i32>(i, j), vec2<i32>(0), size - 1);
let s = textureLoad(fog_tex, q, 0);
let sd = textureLoad(fog_dist_tex, q, 0).x;
let bw = select(1.0 - t.x, t.x, i == 1) * select(1.0 - t.y, t.y, j == 1);
let dz = (sd - dist) / (0.06 * dist + 0.4);
let w = bw / (1.0 + dz * dz * dz * dz);
sum += s * w;
total += w;
}
}
if total < 0.0001 {
return textureLoad(fog_tex, clamp(px / 2, vec2<i32>(0), size - 1), 0);
}
return sum / total;
}
@fragment
fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
let px = vec2<i32>(in.position.xy);
@@ -373,10 +449,15 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
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;
// The volumetric clouds over the sky and the sun.
if frame.clouds.z > 0.5 {
let cl = textureSampleLevel(cloud_tex, linear, in.uv, 0.0);
c = c * cl.a + cl.rgb;
}
}
// 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 {
if atmosphere && frame.clouds.z < 0.5 && !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));
}
@@ -391,7 +472,7 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
let sky_reach = smoothstep(0.35, 0.95, lm.x);
var s = 1.0;
let ndl = dot(n, sun);
if sun_on > 0.0 && ndl > 0.0 && sky_reach > 0.0 { s = shadow_taps(p, n, select(1, 0, water)); } else { s = 0.0; }
if sun_on > 0.0 && ndl > 0.0 && sky_reach > 0.0 { s = shadow_taps(p, n, select(1, 0, water)) * cloud_shadow(p); } else { s = 0.0; }
// Shadowed sky light (ambient) keeps vanilla's look, blocks' light stays as is; the sun adds
// direct light by the face's angle.
let direct = frame.sun_color.rgb * sun_on * max(ndl, 0.0) * s * sky_reach;
@@ -436,10 +517,27 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
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.
// The volumetric fog: the light the air scatters towards the camera (shafts through the trees,
// mist, haze), and how much of the pixel it hides.
let far = select(min(length(p), frame.shadow.z), frame.shadow.z, d <= 0.0);
let density = frame.ambient.z;
if sun_on > 0.0 && density > 0.0 {
let volumetric = frame.vfog.x > 0.5 && atmosphere;
if volumetric {
var fog_len = 192.0;
if d > 0.0 {
var pf = p;
if water {
let hf = frame.inverse_view_proj * vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(d, 0.000001), 1.0);
pf = hf.xyz / hf.w;
}
fog_len = min(length(pf), 192.0);
}
let fg = fog_at(px, fog_len);
c = c * fg.a + fg.rgb;
}
// Light in the air (without the volumetric fog): the sun's rays between the shadows; always the
// halos round the mods' lights.
if !volumetric && sun_on > 0.0 && density > 0.0 {
let steps = 14;
let jitter = ign(in.position.xy);
var acc = 0.0;
@@ -1185,11 +1283,11 @@ fn fs_taau(in: FullOut) -> TaaOut {
}
"#;
struct Target {
view: wgpu::TextureView,
pub(super) struct Target {
pub(super) view: wgpu::TextureView,
}
fn target(
pub(super) fn target(
device: &wgpu::Device,
label: &str,
size: [u32; 2],
@@ -1324,6 +1422,9 @@ pub struct Hd {
/// The frame's translucent terrain exists (set by the renderer before `write`).
pub water_geometry: bool,
water_composited: bool,
/// The volumetric clouds and the fog run this frame.
clouds_active: bool,
fog_active: 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,
@@ -1346,7 +1447,9 @@ pub struct Hd {
bloom: Vec<Target>,
/// Bind groups, rebuilt only when their views change: `resize` drops all of them, the
/// lighting and the source-reading ones are keyed by the views they read.
light_group: Option<(LightKey, wgpu::BindGroup)>,
light_group: [Option<(LightKey, wgpu::BindGroup)>; 2],
/// The volumetric clouds and fog (see `hd_volumes`).
volumes: HdVolumes,
prefilter_group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
tone_group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
/// Reading bloom level k - 1 (for the down pass into level k, index k - 1) and level k (for
@@ -1537,25 +1640,76 @@ fn ao_strength_default() -> f32 {
/// 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 {
pub(super) fn assemble(body: &str) -> String {
let mut body = body.to_owned();
for binding in ["15", "10"] {
// Highest number first: `//SKY1` must not eat the start of `//SKY15`.
for binding in (0..32).rev() {
body = body.replace(
&format!("//SKY{binding}"),
&hd_sky::SKY_FUNCS.replace("SKYBIND", binding),
&hd_sky::SKY_FUNCS.replace("SKYBIND", &binding.to_string()),
);
}
format!("{COMMON}{body}")
}
fn module(device: &wgpu::Device, label: &str, body: &str) -> wgpu::ShaderModule {
pub(super) fn module(device: &wgpu::Device, label: &str, body: &str) -> wgpu::ShaderModule {
// Development: with `HORIZON_HD_SHADER_DIR` set, a shader's body is read from
// `<dir>/<label>.wgsl` (written there from the built-in one when missing), so it can be tuned
// without rebuilding.
let body = match std::env::var("HORIZON_HD_SHADER_DIR") {
Ok(dir) => {
let path = std::path::Path::new(&dir).join(format!("{}.wgsl", label.replace(' ', "_")));
match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(_) => {
let _ = std::fs::write(&path, body);
body.to_owned()
}
}
}
Err(_) => body.to_owned(),
};
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(label),
source: wgpu::ShaderSource::Wgsl(assemble(body).into()),
source: wgpu::ShaderSource::Wgsl(assemble(&body).into()),
})
}
fn fullscreen(
/// One fullscreen pass over `to` (cleared), with the given pipeline and group.
pub(super) fn stamped_pass(
encoder: &mut wgpu::CommandEncoder,
label: &str,
to: &[&wgpu::TextureView],
pipeline: &wgpu::RenderPipeline,
group: &wgpu::BindGroup,
timestamp_writes: Option<wgpu::RenderPassTimestampWrites<'_>>,
) {
let attachments: Vec<_> = to
.iter()
.map(|view| {
Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})
})
.collect();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(label),
timestamp_writes,
color_attachments: &attachments,
..Default::default()
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, group, &[]);
pass.draw(0..3, 0..1);
}
pub(super) fn fullscreen(
device: &wgpu::Device,
label: &str,
layout: &wgpu::BindGroupLayout,
@@ -1596,7 +1750,7 @@ fn fullscreen(
})
}
fn entries(kinds: &[wgpu::BindingType]) -> Vec<wgpu::BindGroupLayoutEntry> {
pub(super) fn entries(kinds: &[wgpu::BindingType]) -> Vec<wgpu::BindGroupLayoutEntry> {
kinds
.iter()
.enumerate()
@@ -1609,31 +1763,33 @@ fn entries(kinds: &[wgpu::BindingType]) -> Vec<wgpu::BindGroupLayoutEntry> {
.collect()
}
const UNIFORM: wgpu::BindingType = wgpu::BindingType::Buffer {
pub(super) const UNIFORM: wgpu::BindingType = wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
};
const FLOAT: wgpu::BindingType = wgpu::BindingType::Texture {
pub(super) const FLOAT: wgpu::BindingType = wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
};
const UNFILTERED: wgpu::BindingType = wgpu::BindingType::Texture {
pub(super) const UNFILTERED: wgpu::BindingType = wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
};
const DEPTH: wgpu::BindingType = wgpu::BindingType::Texture {
pub(super) const DEPTH: wgpu::BindingType = wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
};
const SAMPLER: wgpu::BindingType = wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering);
pub(super) const SAMPLER: wgpu::BindingType =
wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering);
impl Hd {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
size: [u32; 2],
screen: wgpu::TextureFormat,
globals_size: u64,
@@ -1669,6 +1825,10 @@ impl Hd {
UNFILTERED,
DEPTH,
FLOAT,
FLOAT,
UNFILTERED,
FLOAT,
FLOAT,
]),
});
let water_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@@ -1924,6 +2084,7 @@ impl Hd {
mapped_at_creation: false,
})
};
let volumes = HdVolumes::new(device, queue);
let mut hd = Hd {
size: [0, 0],
out: [0, 0],
@@ -1964,6 +2125,8 @@ impl Hd {
water_active: false,
water_geometry: false,
water_composited: false,
clouds_active: false,
fog_active: false,
sky_lut,
sky_pipeline,
sky_group,
@@ -1983,7 +2146,8 @@ impl Hd {
taau_pipeline,
taa_groups: None,
bloom: Vec::new(),
light_group: None,
light_group: [None, None],
volumes,
prefilter_group: None,
tone_group: None,
down_groups: Vec::new(),
@@ -2085,7 +2249,8 @@ impl Hd {
})
.collect();
// Every group names a view that was just replaced.
self.light_group = None;
self.light_group = [None, None];
self.volumes.resize(device, size);
self.prefilter_group = None;
self.tone_group = None;
self.down_groups = (1..self.bloom.len())
@@ -2123,7 +2288,7 @@ impl Hd {
);
queue.write_buffer(&self.tiles, 0, bytemuck::cast_slice(&self.tile_words));
self.frames = self.frames.wrapping_add(1);
let made = self.groups_made.load(Ordering::Relaxed);
let made = self.groups_made.load(Ordering::Relaxed) + self.volumes.groups_made;
self.groups_last_frame = made - self.groups_seen;
self.groups_seen = made;
if PassTiming::logging() && self.frames.is_multiple_of(240) {
@@ -2147,6 +2312,15 @@ impl Hd {
if self.sky_stale {
self.sky_drawn_for = sun_height;
}
self.clouds_active = scene.sky && scene.clouds;
self.fog_active = scene.sky && hd_volumes::fog_enabled();
let (cloud_dir, cloud_colour, cloud_strength) = cloud_light(scene);
// The morning mist: thick before the sun is up, gone by the time it is high.
let mist = if sun_dir_x(scene) > 0.0 {
smooth(-0.5, -0.1, sun_height) * (1.0 - smooth(0.05, 0.5, sun_height))
} else {
0.0
};
let frame = HdFrame {
prev_view_proj: scene.prev_view_proj.to_cols_array_2d(),
taa: [
@@ -2179,6 +2353,38 @@ impl Hd {
0.0,
],
sunrise: scene.sunrise,
clouds: [
hd_volumes::CLOUD_BASE - scene.camera_world[2],
hd_volumes::CLOUD_THICKNESS,
if self.clouds_active { 1.0 } else { 0.0 },
scene.seconds.rem_euclid(300_000.0),
],
cloud_cam: [
scene.camera_world[0],
scene.camera_world[1],
(scene.rain / 0.7).clamp(0.0, 1.0) * 0.18,
if self.volumes.history_ready()
&& self.taa_active
&& scene.camera_delta.length() < 64.0
{
1.0
} else {
0.0
},
],
cloud_dir: [cloud_dir.x, cloud_dir.y, cloud_dir.z, 0.0],
cloud_sun: [
cloud_colour.x,
cloud_colour.y,
cloud_colour.z,
cloud_strength,
],
vfog: [
if self.fog_active { 1.0 } else { 0.0 },
scene.camera_world[2],
mist,
0.0,
],
taa_params: [
if self.upscaling() {
UPSCALE_HISTORY
@@ -2337,6 +2543,36 @@ impl Hd {
);
}
/// The volumetric clouds (with their shadow map) and the fog, before the lighting reads them.
pub fn volumes(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
depth: &wgpu::TextureView,
) {
if self.clouds_active {
self.volumes
.prepare_clouds(device, &self.frame, &self.sky_lut);
self.volumes
.draw_cloud_shadow(encoder, stamp(&mut self.timing, "hd cloud shadows"));
self.volumes
.draw_clouds(encoder, stamp(&mut self.timing, "hd clouds"));
} else {
self.volumes.reset_history();
}
if self.fog_active {
self.volumes.draw_fog(
device,
encoder,
&self.frame,
depth,
&self.shadow_array,
&self.sky_lut,
stamp(&mut self.timing, "hd fog"),
);
}
}
/// Lights the world (its colour and depth) into `lit`.
pub fn light(
&mut self,
@@ -2351,7 +2587,11 @@ impl Hd {
lightmap.0.clone(),
lightmap.1.clone(),
);
if self.light_group.as_ref().is_none_or(|(k, _)| *k != key) {
let flip = self.volumes.flip_index();
if self.light_group[flip]
.as_ref()
.is_none_or(|(k, _)| *k != key)
{
self.groups_made.fetch_add(1, Ordering::Relaxed);
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("HD lighting"),
@@ -2421,11 +2661,27 @@ impl Hd {
binding: 15,
resource: wgpu::BindingResource::TextureView(&self.sky_lut),
},
wgpu::BindGroupEntry {
binding: 16,
resource: wgpu::BindingResource::TextureView(&self.volumes.fog),
},
wgpu::BindGroupEntry {
binding: 17,
resource: wgpu::BindingResource::TextureView(&self.volumes.fog_dist),
},
wgpu::BindGroupEntry {
binding: 18,
resource: wgpu::BindingResource::TextureView(self.volumes.cloud_view()),
},
wgpu::BindGroupEntry {
binding: 19,
resource: wgpu::BindingResource::TextureView(&self.volumes.cloud_shadow),
},
],
});
self.light_group = Some((key, group));
self.light_group[flip] = Some((key, group));
}
let Some((_, group)) = &self.light_group else {
let Some((_, group)) = &self.light_group[flip] else {
return;
};
self.water_composited = false;
@@ -2832,7 +3088,8 @@ pub fn light_of(scene: &HdScene) -> (glam::Vec3, f32, glam::Vec3) {
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);
* glam::Vec3::new(1.0, 0.93, 0.78)
* hd_sky::sun_tint(sun.y);
(sun, 0.7 * up * clear, color)
} else {
let moon = -sun;
@@ -2841,6 +3098,32 @@ pub fn light_of(scene: &HdScene) -> (glam::Vec3, f32, glam::Vec3) {
}
}
/// The x of the sun's direction: positive in the morning, negative in the evening.
fn sun_dir_x(scene: &HdScene) -> f32 {
scene.sun.normalize_or(glam::Vec3::Y).x
}
/// What lights the clouds: the direction, the colour and the strength. The sun's colour from the
/// atmosphere's transmittance (as `light_of`, with Photon's evening tint), kept lighting the clouds
/// a little past the sunset (the clouds stand high), the moon's cold light at night.
pub fn cloud_light(scene: &HdScene) -> (glam::Vec3, glam::Vec3, f32) {
let sun = scene.sun.normalize_or(glam::Vec3::Y);
let clear = 1.0 - scene.rain * 0.85;
if sun.y > -0.2 {
let t = hd_sky::sun_transmittance(sun.y.max(0.01));
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))
* hd_sky::sun_tint(sun.y);
(sun, color, smooth(-0.2, 0.08, sun.y) * clear)
} else {
(
-sun,
glam::Vec3::new(0.55, 0.66, 1.0),
0.22 * smooth(-0.2, -0.45, sun.y) * clear,
)
}
}
fn smooth(a: f32, b: f32, x: f32) -> f32 {
let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
@@ -2885,7 +3168,13 @@ mod tests {
("ao blur", AO_BLUR),
("water", WATER),
("sky", hd_sky::SKY_PASS),
] {
]
.into_iter()
.chain(
hd_volumes::shader_sources()
.iter()
.map(|(n, b)| (*n, b.as_str())),
) {
let source = assemble(body);
let module = naga::front::wgsl::parse_str(&source).unwrap_or_else(|e| {
panic!("{name}: {}", e.emit_to_string(&source));
+163
View File
@@ -0,0 +1,163 @@
//! AirDef fork: the noise textures of the HD volumetric clouds and fog, made once on the CPU: a
//! tileable 2D one (cloud coverage and shape) and a tileable 3D one (Worley detail, as Photon's
//! clouds read from a precomputed 3D Worley texture).
pub const NOISE2_SIZE: u32 = 256;
pub const NOISE3_SIZE: u32 = 64;
fn hash(mut x: u32) -> u32 {
x ^= x >> 16;
x = x.wrapping_mul(0x7feb_352d);
x ^= x >> 15;
x = x.wrapping_mul(0x846c_a68b);
x ^ (x >> 16)
}
fn hash3(x: i32, y: i32, z: i32, seed: u32) -> u32 {
hash(
(x as u32)
.wrapping_mul(0x8da6_b343)
.wrapping_add((y as u32).wrapping_mul(0xd816_3841))
.wrapping_add((z as u32).wrapping_mul(0xcb1a_b31f))
.wrapping_add(seed.wrapping_mul(0x9e37_79b9)),
)
}
fn unit(h: u32) -> f32 {
(h >> 8) as f32 / 16_777_216.0
}
/// Gradient noise on a lattice of `period` cells that wraps, in 0..1 (0.5 on average).
fn perlin2(x: f32, y: f32, period: i32, seed: u32) -> f32 {
let (xi, yi) = (x.floor(), y.floor());
let (fx, fy) = (x - xi, y - yi);
let grad = |ix: i32, iy: i32, dx: f32, dy: f32| {
let h = hash3(ix.rem_euclid(period), iy.rem_euclid(period), 0, seed);
let a = unit(h) * std::f32::consts::TAU;
a.cos() * dx + a.sin() * dy
};
let fade = |t: f32| t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
let (u, v) = (fade(fx), fade(fy));
let (x0, y0) = (xi as i32, yi as i32);
let a = grad(x0, y0, fx, fy);
let b = grad(x0 + 1, y0, fx - 1.0, fy);
let c = grad(x0, y0 + 1, fx, fy - 1.0);
let d = grad(x0 + 1, y0 + 1, fx - 1.0, fy - 1.0);
let top = a + (b - a) * u;
let bottom = c + (d - c) * u;
(0.5 + (top + (bottom - top) * v) * 0.8).clamp(0.0, 1.0)
}
fn fbm2(x: f32, y: f32, base: i32, octaves: u32, seed: u32) -> f32 {
let (mut sum, mut amp, mut norm) = (0.0, 1.0, 0.0);
let mut period = base;
for o in 0..octaves {
let s = period as f32;
sum += perlin2(x * s, y * s, period, seed + o) * amp;
norm += amp;
amp *= 0.5;
period *= 2;
}
sum / norm
}
/// 1 - the distance to the nearest of the points of a wrapping 3D grid of `cells` cells (one jittered
/// point per cell), so a billowy 0..1 field.
fn worley3(x: f32, y: f32, z: f32, cells: i32, seed: u32) -> f32 {
let (px, py, pz) = (x * cells as f32, y * cells as f32, z * cells as f32);
let (ix, iy, iz) = (px.floor() as i32, py.floor() as i32, pz.floor() as i32);
let mut best = 4.0f32;
for dz in -1..=1 {
for dy in -1..=1 {
for dx in -1..=1 {
let (cx, cy, cz) = (ix + dx, iy + dy, iz + dz);
let h = hash3(
cx.rem_euclid(cells),
cy.rem_euclid(cells),
cz.rem_euclid(cells),
seed,
);
let fx = cx as f32 + unit(h);
let fy = cy as f32 + unit(hash(h ^ 0x68e3_1da4));
let fz = cz as f32 + unit(hash(h ^ 0xb529_7a4d));
let d = (fx - px).powi(2) + (fy - py).powi(2) + (fz - pz).powi(2);
best = best.min(d);
}
}
}
(1.0 - best.sqrt()).clamp(0.0, 1.0)
}
fn byte(v: f32) -> u8 {
(v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
}
/// The 2D noise, RGBA8, `NOISE2_SIZE` squared, wrapping: r the coverage (large features), g the
/// shape (medium ones, with more contrast), b a mid-frequency detail, a a fine one.
pub fn noise2() -> Vec<u8> {
let n = NOISE2_SIZE as usize;
let mut out = Vec::with_capacity(n * n * 4);
for j in 0..n {
for i in 0..n {
let (u, v) = (i as f32 / n as f32, j as f32 / n as f32);
let cover = fbm2(u, v, 2, 3, 11);
let shape = fbm2(u, v, 4, 5, 23);
// Gradient noise clusters near 0.5: stretch it so the thresholds have something to cut.
let stretch = |x: f32| ((x - 0.5) * 1.8 + 0.5).clamp(0.0, 1.0);
out.extend_from_slice(&[
byte(stretch(cover)),
byte(stretch(shape)),
byte(fbm2(u, v, 8, 4, 31)),
byte(fbm2(u, v, 16, 3, 41)),
]);
}
}
out
}
/// The 3D noise, RGBA8, `NOISE3_SIZE` cubed, wrapping: r a Worley fbm (large billows), g a finer
/// one, b a smooth value for warping, a the finest Worley.
pub fn noise3() -> Vec<u8> {
let n = NOISE3_SIZE as usize;
let mut out = Vec::with_capacity(n * n * n * 4);
for k in 0..n {
for j in 0..n {
for i in 0..n {
let (x, y, z) = (
(i as f32 + 0.5) / n as f32,
(j as f32 + 0.5) / n as f32,
(k as f32 + 0.5) / n as f32,
);
let r = worley3(x, y, z, 4, 1) * 0.625
+ worley3(x, y, z, 8, 2) * 0.25
+ worley3(x, y, z, 16, 3) * 0.125;
let g = worley3(x, y, z, 8, 4) * 0.625
+ worley3(x, y, z, 16, 5) * 0.25
+ worley3(x, y, z, 32, 6) * 0.125;
let b = perlin2(x * 4.0, y * 4.0 + z * 2.0, 4, 7) * 0.5
+ perlin2(y * 4.0, z * 4.0 + x * 2.0, 4, 8) * 0.5;
let a = worley3(x, y, z, 16, 9);
out.extend_from_slice(&[byte(r), byte(g), byte(b), byte(a)]);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noise_has_the_right_size_and_wraps() {
let t = noise2();
assert_eq!(t.len(), (NOISE2_SIZE * NOISE2_SIZE * 4) as usize);
// The lattice wraps: the value just before the edge is near the one at the start.
let a = fbm2(0.0, 0.3, 4, 5, 23);
let b = fbm2(1.0, 0.3, 4, 5, 23);
assert!((a - b).abs() < 1e-4, "{a} {b}");
let w = worley3(0.0, 0.2, 0.4, 4, 1);
let w1 = worley3(1.0, 0.2, 0.4, 4, 1);
assert!((w - w1).abs() < 1e-4);
}
}
+74 -16
View File
@@ -24,9 +24,17 @@ 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);
// Photon marches in Rec. 2020 (its working colour space): the coefficients are the Rec. 709 ones
// moved to it, and the radiance goes back to Rec. 709 at the end. This keeps the sunset's reds and
// purples apart where the narrower primaries would mix them into yellow and green.
const RAYLEIGH: vec3<f32> = vec3<f32>(1.232851742e-05, 1.638796144e-05, 3.814816455e-05);
const MIE: vec3<f32> = vec3<f32>(1.727386213e-06, 1.804192789e-06, 1.941423374e-06);
const OZONE: vec3<f32> = vec3<f32>(9.563766055e-07, 1.267105338e-06, 1.780727031e-07);
const REC2020_TO_709 = mat3x3<f32>(
vec3<f32>(1.66034406, -0.12437868, -0.01811272),
vec3<f32>(-0.58757158, 1.13283707, -0.10058379),
vec3<f32>(-0.07288024, -0.00835868, 1.11862127));
const MIE_KN_ENERGY: f32 = 3000.0;
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.
@@ -68,13 +76,41 @@ fn light_transmittance(mu: f32, r: f32, h: f32) -> vec3<f32> {
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);
// Photon's Rayleigh phase with the depolarisation per channel (the sky's slight tint).
fn rayleigh_phase(nu: f32) -> vec3<f32> {
let depolarization = vec3<f32>(2.786, 2.842, 2.899) * 1e-2;
let gamma = depolarization / (vec3<f32>(2.0) - depolarization);
let k = 3.0 / (16.0 * PI * (vec3<f32>(1.0) + 2.0 * gamma));
return k * ((vec3<f32>(1.0) + 3.0 * gamma) + (vec3<f32>(1.0) - gamma) * nu * nu);
}
// Photon's Klein-Nishina phase for the aerosols ("far closer to an actual aerosol phase function
// than Henyey-Greenstein"): a tight, bright glow round the sun.
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));
let e = MIE_KN_ENERGY;
return e / (2.0 * PI * (e - e * nu + 1.0) * log(2.0 * e + 1.0));
}
fn linear_step(a: f32, b: f32, x: f32) -> f32 {
return clamp((x - a) / (b - a), 0.0, 1.0);
}
// Photon's pulse: 1 at the centre, 0 at the width, smooth between.
fn pulse(x: f32, centre: f32, width: f32) -> f32 {
let t = abs(x - centre) / width;
if t > 1.0 { return 0.0; }
return 1.0 - t * t * (3.0 - 2.0 * t);
}
// The sunlight's tint and extra strength round sunrise and sunset, from Photon's
// get_sun_tint / get_sun_exposure (light_color.glsl): pinkish when the sun is low, violet at the
// blue hour, and a strong lift then so the twilight sky does not go dark. `y`: the sun's height.
fn sun_light_tint(y: f32) -> vec3<f32> {
let blue_hour = linear_step(0.05, 1.0, exp(-190.0 * (y + 0.09604) * (y + 0.09604)));
let p = pulse(y, 0.17, 0.40);
let morning_evening = mix(vec3<f32>(1.0), vec3<f32>(1.05, 0.84, 0.93) * 1.2, p * p);
let blue_tint = mix(vec3<f32>(1.0), vec3<f32>(0.95, 0.80, 1.0), blue_hour);
let low = 1.0 - smoothstep(0.0, 0.3, abs(y));
return morning_evening * blue_tint * (1.0 + 0.5 * low + 40.0 * blue_hour);
}
// The radiance the camera sees along `view` (y up) with the sun and the moon in the directions
@@ -84,6 +120,7 @@ fn march(view: vec3<f32>, sun: vec3<f32>, moon: vec3<f32>) -> vec3<f32> {
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 sun_i = SUN_I * sun_light_tint(frame.sky_sun.y);
let phase_r_sun = rayleigh_phase(nu_sun);
let phase_m_sun = mie_phase(nu_sun);
let phase_r_moon = rayleigh_phase(nu_moon);
@@ -111,13 +148,13 @@ fn march(view: vec3<f32>, sun: vec3<f32>, moon: vec3<f32>) -> vec3<f32> {
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
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;
* (sun_t * sun_i + moon_t * MOON_I) * 0.5;
sum += view_t * (single + multi) * dt;
}
return sum;
return max(REC2020_TO_709 * sum, vec3<f32>(0.0));
}
@fragment
@@ -180,7 +217,7 @@ fn sky_show(radiance: vec3<f32>) -> vec3<f32> {
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));
let boost = 1.2 + 0.8 * (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));
}
@@ -293,9 +330,10 @@ fn vanilla_sky(d: vec3<f32>) -> vec3<f32> {
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);
// The same Rec. 2020 coefficients as the sky pass; the result is moved back to Rec. 709.
const RAYLEIGH: glam::Vec3 = glam::Vec3::new(1.232_851_7e-5, 1.638_796_1e-5, 3.814_816_5e-5);
const MIE: glam::Vec3 = glam::Vec3::new(1.727_386_2e-6, 1.804_192_8e-6, 1.941_423_4e-6);
const OZONE: glam::Vec3 = glam::Vec3::new(9.563_766e-7, 1.267_105_3e-6, 1.780_727e-7);
// 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| {
@@ -307,7 +345,27 @@ pub fn sun_transmittance(sin_elevation: f32) -> glam::Vec3 {
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())
let t = glam::Vec3::new((-tau.x).exp(), (-tau.y).exp(), (-tau.z).exp());
let to_709 = glam::Mat3::from_cols(
glam::Vec3::new(1.660_344, -0.124_378_68, -0.018_112_72),
glam::Vec3::new(-0.587_571_6, 1.132_837, -0.100_583_79),
glam::Vec3::new(-0.072_880_24, -0.008_358_68, 1.118_621_3),
);
(to_709 * t).max(glam::Vec3::ZERO)
}
/// Photon's tint of the sunlight (`get_sun_tint`, light_color.glsl): pink when the sun is low, violet
/// at the blue hour. `y`: the sun's height (the y of the unit vector towards it).
pub fn sun_tint(y: f32) -> glam::Vec3 {
let blue_hour =
(((-190.0 * (y + 0.096_04) * (y + 0.096_04)).exp() - 0.05) / 0.95).clamp(0.0, 1.0);
let pulse = {
let t = ((y - 0.17).abs() / 0.40).min(1.0);
1.0 - t * t * (3.0 - 2.0 * t)
};
let evening = glam::Vec3::ONE.lerp(glam::Vec3::new(1.05, 0.84, 0.93) * 1.2, pulse * pulse);
let blue = glam::Vec3::ONE.lerp(glam::Vec3::new(0.95, 0.80, 1.0), blue_hour);
evening * blue
}
#[cfg(test)]
@@ -318,7 +376,7 @@ mod tests {
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!(noon.x > 0.8 && noon.z > 0.6, "{noon:?}");
assert!(low.z / low.x < noon.z / noon.x * 0.6, "{low:?} vs {noon:?}");
}
}
+857
View File
@@ -0,0 +1,857 @@
//! AirDef fork: the HD renderer's volumetric clouds and volumetric fog.
//!
//! Clouds: a raymarched cumulus slab (about vanilla's cloud height), lit by the sun with a light march
//! and Photon's multiple-scattering approximation, drawn at half resolution with a jittered march
//! and reprojected history; plus a small map of the clouds' shadows for the ground. Fog: a half
//! resolution march from the camera to the surface through height fog, sampling the sun's shadow
//! cascades for the light shafts. `fs_light` composites both (see `hd.rs`).
//!
//! The cloud shaping and lighting (altitude shaping, the powder effect, the phase functions, the
//! multiple-scattering octaves, the light march with growing steps) and the fog's phase mix and
//! evening glow are ported from Photon by sixthsurge (MIT, see THIRD_PARTY_PHOTON.md), shaders/include/sky/clouds/{common,cumulus}.glsl, shaders/include/lighting/cloud_shadows.glsl and shaders/include/fog/air_fog_vl.glsl at commit 26c2906.
use super::hd::{DEPTH, FLOAT, SAMPLER, UNIFORM, entries, module, stamped_pass, target};
use super::hd_noise;
pub const CLOUD_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub const SHADOW_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R16Float;
pub const FOG_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub const FOG_DIST_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R16Float;
/// The cloud shadow map's side in texels, and its half size in blocks (the same in `fs_light`).
pub const SHADOW_SIZE: u32 = 256;
pub const SHADOW_RADIUS: f32 = 1200.0;
/// Vanilla's cloud base height; the volumetric layer stands on it.
pub const CLOUD_BASE: f32 = 192.33;
pub const CLOUD_THICKNESS: f32 = 72.0;
/// Volumetric clouds are on unless `HORIZON_HD_CLOUDS=0`.
pub fn clouds_enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var("HORIZON_HD_CLOUDS").map_or(true, |v| v != "0"))
}
/// Volumetric fog is on unless `HORIZON_HD_VOLFOG=0`.
pub fn fog_enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var("HORIZON_HD_VOLFOG").map_or(true, |v| v != "0"))
}
/// Shared by the cloud passes: the bindings, the density of the slab, the light march and the
/// phase functions. Bindings: 0 frame, 1 3D noise, 2 2D noise, 3 clamped sampler, 4 repeating
/// sampler, 5 the sky texture.
const CLOUD_COMMON: &str = r#"
@group(0) @binding(0) var<uniform> frame: HdFrame;
@group(0) @binding(1) var noise3: texture_3d<f32>;
@group(0) @binding(2) var noise2: texture_2d<f32>;
@group(0) @binding(3) var linear: sampler;
@group(0) @binding(4) var repeat_s: sampler;
//SKY5
const CPI: f32 = 3.14159265;
// Extinction per block at full density, how fast the wind blows (blocks per second).
const CLOUD_EXT: f32 = 0.22;
const CLOUD_WIND: f32 = 3.0;
fn linear_step(a: f32, b: f32, x: f32) -> f32 {
return clamp((x - a) / (b - a), 0.0, 1.0);
}
fn dampen(x: f32) -> f32 {
let t = clamp(x, 0.0, 1.0);
return t * (2.0 - t);
}
// Where a camera-relative point lies in the world's cloud pattern (the wind moves the pattern).
fn cloud_xz(pos: vec3<f32>) -> vec2<f32> {
return vec2<f32>(pos.x + frame.cloud_cam.x, pos.z + frame.cloud_cam.y)
+ vec2<f32>(0.8, 0.6) * (CLOUD_WIND * frame.clouds.w);
}
// The cloud density at a camera-relative point (0 outside the slab), in 0..1. Photon's cumulus
// shaping: a coverage field and a shape field cut by a threshold, an egg shape over the altitude,
// Worley erosion (large and small) and sharper edges at the top than at the bottom.
fn cloud_density(pos: vec3<f32>, detail: f32) -> f32 {
let h = (pos.y - frame.clouds.x) / frame.clouds.y;
if h <= 0.0 || h >= 1.0 { return 0.0; }
let w = cloud_xz(pos);
let cover = textureSampleLevel(noise2, repeat_s, w / 16000.0, 0.0).r;
let shape = textureSampleLevel(noise2, repeat_s, w / 3000.0, 0.0).g;
let cu = mix(0.56, 0.80, cover) + frame.cloud_cam.z;
var d = linear_step(1.0 - cu, 1.0, shape);
// Carve an egg shape, thin out the bottom.
d -= smoothstep(0.2, 1.0, h) * 0.6;
d *= smoothstep(0.0, 0.2, h);
if d < 0.002 { return 0.0; }
if detail > 0.0 {
let p3 = vec3<f32>(w.x, pos.y, w.y);
let warp = (textureSampleLevel(noise3, repeat_s, p3 / 1100.0, 0.0).b - 0.5) * 0.4;
let w0 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 420.0 + vec3<f32>(warp), 0.0).r;
let w1 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 90.0 + vec3<f32>(warp * 3.0), 0.0).g;
let fade = 0.20 * smoothstep(0.85, 1.0, 1.0 - h) - 0.35 * smoothstep(0.05, 0.5, h) + 0.6;
d -= 0.55 * w0 * w0 * dampen(1.0 - d);
d -= 0.45 * w1 * w1 * dampen(1.0 - d) * fade;
}
d = max(d, 0.0);
d = 1.0 - pow(1.0 - d, mix(2.2, 5.0, h));
d *= 0.1 + 0.9 * smoothstep(0.2, 0.7, h);
return d;
}
// The optical depth from `p` along `dir` through the slab: a few steps that double in length
// (Photon's light march), the sample dithered inside its step.
fn cloud_light_depth(p: vec3<f32>, dir: vec3<f32>, steps: i32, dither: f32, detail: f32) -> f32 {
var len = 0.1 * frame.clouds.y / f32(steps);
var t = 0.0;
var od = 0.0;
for (var i = 0; i < steps; i++) {
len *= 2.0;
od += cloud_density(p + dir * (t + len * dither), detail) * len;
t += len;
}
return od;
}
"#;
const CLOUD: &str = r#"
@group(0) @binding(6) var history: texture_2d<f32>;
fn ign(px: vec2<f32>) -> f32 {
let p = px + vec2<f32>(5.588238 * frame.params.w);
return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
}
fn hg(c: f32, g: f32) -> f32 {
let gg = g * g;
return (1.0 - gg) / (4.0 * CPI * pow(1.0 + gg - 2.0 * g * c, 1.5));
}
fn klein_nishina(nu: f32, e: f32) -> f32 {
return e / (2.0 * CPI * (e - e * nu + 1.0) * log(2.0 * e + 1.0));
}
// Photon's single scattering phase: a glow close to the sun, a forward lobe and a back lobe.
fn phase_single(c: f32) -> f32 {
let a = klein_nishina(c, 2600.0);
let b = hg(c, 0.8);
return 0.8 * max(a, b) + 0.2 * hg(c, -0.2);
}
fn phase_multi(c: f32, g: vec3<f32>) -> f32 {
return 0.65 * hg(c, g.x) + 0.10 * hg(c, g.y) + 0.25 * hg(c, -g.z);
}
fn powder_effect(density: f32, c: f32) -> f32 {
let p = CPI * density / (density + 0.15);
let k = c * 0.5 + 0.5;
return mix(p, 1.0, 0.8 * k * k);
}
// Photon's multiple scattering octaves: each one scatters a part of the light again with a wider
// phase, less extinction and the powder effect easing off. x: sunlight, y: skylight.
fn cloud_scatter(
density: f32,
light_od: f32,
sky_od: f32,
ground_od: f32,
ext: f32,
sca: f32,
step_t: f32,
cos_theta: f32,
bounced: f32,
) -> vec2<f32> {
var s = vec2<f32>(0.0);
var scatter_amount = sca;
var extinct_amount = ext;
let integral = (1.0 - step_t) / ext;
var powder = powder_effect(density, cos_theta);
var phase = phase_single(cos_theta);
var phase_g = pow(vec3<f32>(0.6, 0.9, 0.3), vec3<f32>(1.0 + light_od));
let iso = 1.0 / (4.0 * CPI);
for (var i = 0; i < 4; i++) {
s.x += scatter_amount * exp(-extinct_amount * light_od) * phase;
s.x += scatter_amount * exp(-extinct_amount * ground_od) * iso * bounced;
s.y += scatter_amount * exp(-extinct_amount * sky_od) * iso;
let lift = pow(clamp(sca / 0.1, 0.0, 1.0), 0.33);
scatter_amount *= 0.55 * mix(lift, 1.0, cos_theta * 0.5 + 0.5) * powder;
extinct_amount *= 0.4;
phase_g *= 0.8;
powder = mix(powder, sqrt(powder), 0.5);
phase = phase_multi(cos_theta, phase_g);
}
return s * integral;
}
@fragment
fn fs_cloud(in: FullOut) -> @location(0) vec4<f32> {
let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, 0.5, 1.0);
let hh = frame.inverse_view_proj * ndc;
let dir = normalize(hh.xyz / hh.w);
var cur = vec4<f32>(0.0, 0.0, 0.0, 1.0);
var hit_t = 900.0;
let y0 = frame.clouds.x;
let y1 = y0 + frame.clouds.y;
if abs(dir.y) > 0.004 {
let ta = y0 / dir.y;
let tb = y1 / dir.y;
let lo = max(min(ta, tb), 0.0);
let hi = min(max(ta, tb), 4200.0);
if hi > lo {
let dither = ign(in.position.xy);
let n = clamp(i32(ceil((hi - lo) / 14.0)), 8, 26);
let step_len = (hi - lo) / f32(n);
let light = frame.cloud_dir.xyz;
let cos_theta = dot(dir, light);
let overcast = frame.sky_extra.z;
let ext = CLOUD_EXT * (1.0 - 0.25 * overcast);
let sca = ext;
let bounced = 0.4 * max(light.y, 0.0) / CPI;
var scatter = vec2<f32>(0.0);
var t_acc = 1.0;
var dist_sum = 0.0;
var weight_sum = 0.0;
for (var i = 0; i < n; i++) {
if t_acc < 0.075 { break; }
let t = lo + step_len * (f32(i) + dither);
let pos = dir * t;
var d = cloud_density(pos, 1.0);
if d < 0.002 { continue; }
d *= 1.0 - smoothstep(0.8, 1.0, t / 4200.0);
let step_od = d * ext * step_len;
let step_t = exp(-step_od);
let h = clamp((pos.y - y0) / frame.clouds.y, 0.0, 1.0);
let hash = fract(dither * 7.13 + f32(i) * 0.618);
let light_od = cloud_light_depth(pos, light, 3, hash, 1.0);
let sky_od = cloud_light_depth(pos, vec3<f32>(0.0, 1.0, 0.0), 1, 1.0 - hash, 0.0);
// A guess of the optical depth to the ground from the altitude and this density.
let ground_od = mix(d, 1.0, clamp(h * 2.0 - 1.0, 0.0, 1.0)) * h * frame.clouds.y;
scatter += cloud_scatter(d, light_od, sky_od, ground_od, ext, sca, step_t, cos_theta, bounced) * t_acc;
t_acc *= step_t;
dist_sum += t * d;
weight_sum += d;
}
if weight_sum > 0.0 { hit_t = dist_sum / weight_sum; }
// The sunlight's colour at this hour (Photon: the light's colour from the transmittance),
// the sky's own light from the zenith and the horizon.
let sun_light = frame.cloud_sun.rgb * frame.cloud_sun.w * (1.0 - overcast);
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 ambient = mix(horizon, zenith, 0.55);
var colour = scatter.x * sun_light * 3.2 + scatter.y * ambient * 5.0;
// The remap of Photon: the transmittance's floor is 0.
let trans = linear_step(0.075, 1.0, t_acc);
// Aerial perspective: far clouds fade into the haze.
let haze = haze_colour(dir);
let air = exp(-hit_t / 3600.0);
colour = mix((1.0 - trans) * haze, colour, air);
cur = vec4<f32>(colour, trans);
}
}
// Reproject the history by where the clouds are (rotation and the camera's step) and blend.
if frame.cloud_cam.w > 0.5 {
let hp = frame.prev_view_proj * vec4<f32>(dir * hit_t + frame.taa.xyz, 1.0);
if hp.w > 0.0 {
let huv = vec2<f32>(hp.x / hp.w * 0.5 + 0.5, 0.5 - hp.y / hp.w * 0.5);
if all(huv > vec2<f32>(0.0)) && all(huv < vec2<f32>(1.0)) {
let old = textureSampleLevel(history, linear, huv, 0.0);
// Not too much of a history that disagrees by a lot (a gust, a fast turn).
let diff = length(old.rgb - cur.rgb) + abs(old.a - cur.a);
let keep = 0.9 * (1.0 - smoothstep(0.35, 1.2, diff));
cur = mix(cur, old, keep);
}
}
}
return cur;
}
"#;
const CLOUD_SHADOW: &str = r#"
@fragment
fn fs_cloud_shadow(in: FullOut) -> @location(0) vec4<f32> {
// A square of the cloud base plane round the camera; a texel holds the optical depth of the
// sunlight's way through the slab from that point.
let q = (in.uv - vec2<f32>(0.5)) * (2.0 * SHADOW_R);
let light = frame.cloud_dir.xyz;
let ly = max(light.y, 0.15);
let base = vec3<f32>(q.x, frame.clouds.x + 0.5, q.y);
let len = frame.clouds.y / ly;
var od = 0.0;
for (var i = 0; i < 4; i++) {
let t = (f32(i) + 0.5) / 4.0 * len;
od += cloud_density(base + light * t, 0.0);
}
od *= CLOUD_EXT * len / 4.0 * (1.0 - 0.25 * frame.sky_extra.z);
return vec4<f32>(od, 0.0, 0.0, 1.0);
}
"#;
/// The volumetric fog: bindings 0 frame, 1 the world's depth, 2 and 3 the shadow cascades and
/// their sampler, 4 the 3D noise, 5 the repeating sampler, 6 the clamped sampler, 7 the sky
/// texture. Targets: the in-scattered light (rgb) with the transmittance (a), and the distance the
/// ray was marched (for the depth-aware upsample in `fs_light`).
const FOG: &str = r#"
@group(0) @binding(0) var<uniform> frame: HdFrame;
@group(0) @binding(1) var scene_depth: texture_depth_2d;
@group(0) @binding(2) var shadow_map: texture_depth_2d_array;
@group(0) @binding(3) var shadow_sampler: sampler_comparison;
@group(0) @binding(4) var noise3: texture_3d<f32>;
@group(0) @binding(5) var repeat_s: sampler;
@group(0) @binding(6) var linear: sampler;
//SKY7
const FOG_MAX: f32 = 192.0;
const FOG_STEPS: i32 = 18;
struct FogOut {
@location(0) fog: vec4<f32>,
@location(1) dist: vec4<f32>,
}
fn ign(px: vec2<f32>) -> f32 {
let p = px + vec2<f32>(5.588238 * frame.params.w);
return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
}
// Henyey-Greenstein, normalised to 1 for an isotropic one (so the light is its strength times it).
fn hgn(c: f32, g: f32) -> f32 {
let gg = g * g;
return (1.0 - gg) / pow(1.0 + gg - 2.0 * g * c, 1.5);
}
// The sun's light reaching a camera-relative point: 0 in shadow, 1 lit (the near cascade close
// by, the far one beyond; outside both, lit).
fn sun_visible(p: vec3<f32>) -> f32 {
let near = length(p) < frame.shadow.y * 0.9;
let c = select(1, 0, near);
let s = frame.cascades[c] * vec4<f32>(p, 1.0);
let uv = vec3<f32>(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z);
if any(uv.xy < vec2<f32>(0.0)) || any(uv.xy > vec2<f32>(1.0)) || uv.z > 1.0 { return 1.0; }
return textureSampleCompareLevel(shadow_map, shadow_sampler, uv.xy, c, uv.z - select(0.0012, 0.0007, near));
}
// The air's extinction per block at a camera-relative point: a haze that thins with height above
// the sea, a morning mist hugging the ground, rain, patches from a 3D noise that drifts; none
// deep underground.
fn fog_sigma(p: vec3<f32>) -> f32 {
let wy = p.y + frame.vfog.y;
let h = wy - 62.0;
var s = 0.0016 * exp(clamp(-h / 42.0, -6.0, 1.3));
s += frame.vfog.z * 0.011 * exp(clamp(-h / 15.0, -6.0, 1.0));
s += frame.sky_extra.z * 0.003;
let w = vec3<f32>(p.x + frame.cloud_cam.x, wy, p.z + frame.cloud_cam.y)
+ vec3<f32>(0.5, 0.0, 0.35) * frame.clouds.w;
let n = textureSampleLevel(noise3, repeat_s, w / 170.0, 0.0).b;
let patchy = 0.35 + 1.3 * smoothstep(0.25, 0.75, n);
// Fade out far below the sea's height: caves keep their own dark air.
return s * patchy * smoothstep(28.0, 58.0, wy);
}
@fragment
fn fs_fog(in: FullOut) -> FogOut {
// The nearest of the four world pixels this texel stands for (so edges keep the near depth).
let size = vec2<i32>(frame.resolution.xy);
let base = vec2<i32>(in.position.xy) * 2;
var d = 0.0;
var best = base;
for (var k = 0; k < 4; k++) {
let q = min(base + vec2<i32>(k & 1, k >> 1), size - 1);
let z = textureLoad(scene_depth, q, 0);
if z > d {
d = z;
best = q;
}
}
let uv = (vec2<f32>(best) + 0.5) * frame.resolution.zw;
let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, max(d, 0.000001), 1.0);
let h = frame.inverse_view_proj * ndc;
let p = h.xyz / h.w;
let dir = normalize(p);
let len = select(FOG_MAX, min(length(p), FOG_MAX), d > 0.0);
var out: FogOut;
out.dist = vec4<f32>(len, 0.0, 0.0, 0.0);
if frame.vfog.x < 0.5 {
out.fog = vec4<f32>(0.0, 0.0, 0.0, 1.0);
return out;
}
let sun_on = frame.sun.w;
let sun_light = frame.sun_color.rgb * sun_on;
let cos_v = dot(dir, frame.sun.xyz);
// Photon's mix of two phase functions for the aerosols, as its air fog does.
let phase = 0.7 * hgn(cos_v, 0.55) + 0.3 * hgn(cos_v, -0.2);
let haze = haze_colour(dir);
let jitter = ign(in.position.xy);
var trans = 1.0;
var light = vec3<f32>(0.0);
var lit_sum = 0.0;
for (var i = 0; i < FOG_STEPS; i++) {
let u0 = (f32(i) + jitter) / f32(FOG_STEPS);
let u1 = (f32(i) + 1.0 + jitter) / f32(FOG_STEPS);
let f0 = 0.5 * u0 + 0.5 * u0 * u0;
let f1 = 0.5 * u1 + 0.5 * u1 * u1;
let t = len * f0;
let ds = len * (f1 - f0);
let pos = dir * (t + 0.5 * ds);
let sigma = fog_sigma(pos);
let a = 1.0 - exp(-sigma * ds);
var vis = 1.0;
if sun_on > 0.0 { vis = sun_visible(pos); }
lit_sum += vis;
// The sky's light fills the shade less than the open.
let ambient = haze * (0.2 + 0.8 * (0.35 + 0.65 * vis));
light += trans * a * (sun_light * vis * phase * FOG_SUN + ambient * FOG_AMBIENT);
trans *= 1.0 - a;
}
// Photon brightens the fog a little in the early morning and the evening.
let sy = frame.sky_sun.y + 0.02;
let glow = 1.0 + 2.0 * linear_step(0.05, 1.0, exp(-300.0 * sy * sy));
out.fog = vec4<f32>(light * glow, trans);
return out;
}
const FOG_SUN: f32 = 1.1;
const FOG_AMBIENT: f32 = 0.9;
fn linear_step(a: f32, b: f32, x: f32) -> f32 {
return clamp((x - a) / (b - a), 0.0, 1.0);
}
"#;
fn cloud_body(extra: &str) -> String {
format!("{CLOUD_COMMON}{extra}")
}
/// The cloud and fog targets, noise, pipelines and bind groups.
pub struct HdVolumes {
noise3: wgpu::TextureView,
noise2: wgpu::TextureView,
repeat: wgpu::Sampler,
clamp: wgpu::Sampler,
shadow_sampler: wgpu::Sampler,
/// The clouds' half-resolution target and its history (read one, write the other).
pub clouds: [wgpu::TextureView; 2],
flip: usize,
history_ready: bool,
pub cloud_shadow: wgpu::TextureView,
pub fog: wgpu::TextureView,
pub fog_dist: wgpu::TextureView,
cloud_pipeline: wgpu::RenderPipeline,
shadow_pipeline: wgpu::RenderPipeline,
fog_layout: wgpu::BindGroupLayout,
fog_pipeline: wgpu::RenderPipeline,
cloud_layout: wgpu::BindGroupLayout,
shadow_layout: wgpu::BindGroupLayout,
cloud_groups: Option<[wgpu::BindGroup; 2]>,
shadow_group: Option<wgpu::BindGroup>,
fog_group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
/// Bind groups made so far (the renderer sums them into its per-frame count).
pub groups_made: u64,
}
const D3: wgpu::BindingType = wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D3,
multisampled: false,
};
impl HdVolumes {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
let upload = |label: &str, dimension, size: wgpu::Extent3d, data: Vec<u8>| {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size,
mip_level_count: 1,
sample_count: 1,
dimension,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
tex.as_image_copy(),
&data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(size.width * 4),
rows_per_image: Some(size.height),
},
size,
);
tex.create_view(&Default::default())
};
let n2 = hd_noise::NOISE2_SIZE;
let n3 = hd_noise::NOISE3_SIZE;
let noise2 = upload(
"HD noise 2D",
wgpu::TextureDimension::D2,
wgpu::Extent3d {
width: n2,
height: n2,
depth_or_array_layers: 1,
},
hd_noise::noise2(),
);
let noise3 = upload(
"HD noise 3D",
wgpu::TextureDimension::D3,
wgpu::Extent3d {
width: n3,
height: n3,
depth_or_array_layers: n3,
},
hd_noise::noise3(),
);
let sampler = |address| {
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("HD volumes"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
address_mode_u: address,
address_mode_v: address,
address_mode_w: address,
..Default::default()
})
};
let repeat = sampler(wgpu::AddressMode::Repeat);
let clamp = sampler(wgpu::AddressMode::ClampToEdge);
let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("HD fog shadows"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
compare: Some(wgpu::CompareFunction::LessEqual),
..Default::default()
});
let cloud_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HD clouds"),
entries: &entries(&[UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT, FLOAT]),
});
let shadow_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HD cloud shadows"),
entries: &entries(&[UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT]),
});
let fog_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HD fog"),
entries: &entries(&[
UNIFORM,
DEPTH,
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2Array,
multisampled: false,
},
wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
D3,
SAMPLER,
SAMPLER,
FLOAT,
]),
});
let cloud_src = format!("{}{CLOUD}", cloud_body(""));
let shadow_src = format!(
"const SHADOW_R: f32 = {SHADOW_RADIUS:?};\n{}{CLOUD_SHADOW}",
cloud_body("")
);
let cloud_module = module(device, "HD clouds", &cloud_src);
let shadow_module = module(device, "HD cloud shadows", &shadow_src);
let fog_module = module(device, "HD fog", FOG);
let cloud_pipeline = super::hd::fullscreen(
device,
"HD clouds",
&cloud_layout,
&cloud_module,
"fs_cloud",
CLOUD_FORMAT,
None,
);
let shadow_pipeline = super::hd::fullscreen(
device,
"HD cloud shadows",
&shadow_layout,
&shadow_module,
"fs_cloud_shadow",
SHADOW_FORMAT,
None,
);
let fog_pipeline = {
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("HD fog"),
bind_group_layouts: &[Some(&fog_layout)],
..Default::default()
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("HD fog"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &fog_module,
entry_point: Some("vs_full"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &fog_module,
entry_point: Some("fs_fog"),
compilation_options: Default::default(),
targets: &[
Some(wgpu::ColorTargetState {
format: FOG_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: FOG_DIST_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
})
};
let one = |label, size, format| target(device, label, size, format).view;
Self {
noise3,
noise2,
repeat,
clamp,
shadow_sampler,
clouds: [
one("HD clouds", [1, 1], CLOUD_FORMAT),
one("HD clouds", [1, 1], CLOUD_FORMAT),
],
flip: 0,
history_ready: false,
cloud_shadow: one(
"HD cloud shadows",
[SHADOW_SIZE, SHADOW_SIZE],
SHADOW_FORMAT,
),
fog: one("HD fog", [1, 1], FOG_FORMAT),
fog_dist: one("HD fog distance", [1, 1], FOG_DIST_FORMAT),
cloud_pipeline,
shadow_pipeline,
fog_layout,
fog_pipeline,
cloud_layout,
shadow_layout,
cloud_groups: None,
shadow_group: None,
fog_group: None,
groups_made: 0,
}
}
/// New targets for a world of `size`; the clouds and the fog are half of it.
pub fn resize(&mut self, device: &wgpu::Device, size: [u32; 2]) {
let half = [size[0].div_ceil(2).max(1), size[1].div_ceil(2).max(1)];
let one = |label, format| target(device, label, half, format).view;
self.clouds = [
one("HD clouds", CLOUD_FORMAT),
one("HD clouds", CLOUD_FORMAT),
];
self.fog = one("HD fog", FOG_FORMAT);
self.fog_dist = one("HD fog distance", FOG_DIST_FORMAT);
self.history_ready = false;
self.cloud_groups = None;
self.shadow_group = None;
self.fog_group = None;
}
/// Whether the clouds' history can be reprojected this frame (and the renderer's flag for the shader).
pub fn history_ready(&self) -> bool {
self.history_ready
}
pub fn reset_history(&mut self) {
self.history_ready = false;
}
pub fn flip_index(&self) -> usize {
self.flip
}
/// The target the clouds were written to last: what `fs_light` reads.
pub fn cloud_view(&self) -> &wgpu::TextureView {
&self.clouds[self.flip]
}
fn group(
&mut self,
device: &wgpu::Device,
label: &str,
layout: &wgpu::BindGroupLayout,
resources: &[wgpu::BindingResource<'_>],
) -> wgpu::BindGroup {
self.groups_made += 1;
let entries: Vec<_> = resources
.iter()
.enumerate()
.map(|(i, r)| wgpu::BindGroupEntry {
binding: i as u32,
resource: r.clone(),
})
.collect();
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout,
entries: &entries,
})
}
/// Makes the cloud passes' bind groups when they are missing (after a resize).
pub fn prepare_clouds(
&mut self,
device: &wgpu::Device,
frame: &wgpu::Buffer,
sky_lut: &wgpu::TextureView,
) {
if self.shadow_group.is_none() {
let layout = self.shadow_layout.clone();
let (n3, n2, clamp, repeat) = (
self.noise3.clone(),
self.noise2.clone(),
self.clamp.clone(),
self.repeat.clone(),
);
let group = self.group(
device,
"HD cloud shadows",
&layout,
&[
wgpu::BindingResource::Buffer(frame.as_entire_buffer_binding()),
wgpu::BindingResource::TextureView(&n3),
wgpu::BindingResource::TextureView(&n2),
wgpu::BindingResource::Sampler(&clamp),
wgpu::BindingResource::Sampler(&repeat),
wgpu::BindingResource::TextureView(sky_lut),
],
);
self.shadow_group = Some(group);
}
if self.cloud_groups.is_none() {
let layout = self.cloud_layout.clone();
let (n3, n2, clamp, repeat) = (
self.noise3.clone(),
self.noise2.clone(),
self.clamp.clone(),
self.repeat.clone(),
);
let hist = self.clouds.clone();
let groups = [0, 1].map(|read| {
self.group(
device,
"HD clouds",
&layout,
&[
wgpu::BindingResource::Buffer(frame.as_entire_buffer_binding()),
wgpu::BindingResource::TextureView(&n3),
wgpu::BindingResource::TextureView(&n2),
wgpu::BindingResource::Sampler(&clamp),
wgpu::BindingResource::Sampler(&repeat),
wgpu::BindingResource::TextureView(sky_lut),
wgpu::BindingResource::TextureView(&hist[read]),
],
)
});
self.cloud_groups = Some(groups);
}
}
/// The cloud shadow map.
pub fn draw_cloud_shadow(
&mut self,
encoder: &mut wgpu::CommandEncoder,
stamp: Option<wgpu::RenderPassTimestampWrites<'_>>,
) {
if let Some(group) = &self.shadow_group {
stamped_pass(
encoder,
"HD cloud shadows",
&[&self.cloud_shadow],
&self.shadow_pipeline,
group,
stamp,
);
}
}
/// The clouds (half resolution, with history).
pub fn draw_clouds(
&mut self,
encoder: &mut wgpu::CommandEncoder,
stamp: Option<wgpu::RenderPassTimestampWrites<'_>>,
) {
if let Some(groups) = &self.cloud_groups {
let (read, write) = (self.flip, 1 - self.flip);
stamped_pass(
encoder,
"HD clouds",
&[&self.clouds[write]],
&self.cloud_pipeline,
&groups[read],
stamp,
);
self.flip = write;
self.history_ready = true;
}
}
/// The fog's march from the camera to the world's depth.
pub fn draw_fog(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
frame: &wgpu::Buffer,
depth: &wgpu::TextureView,
shadow_array: &wgpu::TextureView,
sky_lut: &wgpu::TextureView,
stamp: Option<wgpu::RenderPassTimestampWrites<'_>>,
) {
if self.fog_group.as_ref().is_none_or(|(v, _)| v != depth) {
let layout = self.fog_layout.clone();
let (n3, clamp, repeat, cmp) = (
self.noise3.clone(),
self.clamp.clone(),
self.repeat.clone(),
self.shadow_sampler.clone(),
);
let group = self.group(
device,
"HD fog",
&layout,
&[
wgpu::BindingResource::Buffer(frame.as_entire_buffer_binding()),
wgpu::BindingResource::TextureView(depth),
wgpu::BindingResource::TextureView(shadow_array),
wgpu::BindingResource::Sampler(&cmp),
wgpu::BindingResource::TextureView(&n3),
wgpu::BindingResource::Sampler(&repeat),
wgpu::BindingResource::Sampler(&clamp),
wgpu::BindingResource::TextureView(sky_lut),
],
);
self.fog_group = Some((depth.clone(), group));
}
if let Some((_, group)) = &self.fog_group {
stamped_pass(
encoder,
"HD fog",
&[&self.fog, &self.fog_dist],
&self.fog_pipeline,
group,
stamp,
);
}
}
}
#[cfg(test)]
pub fn shader_sources() -> Vec<(&'static str, String)> {
vec![
("clouds", format!("{}{CLOUD}", cloud_body(""))),
(
"cloud shadows",
format!(
"const SHADOW_R: f32 = 1200.0;\n{}{CLOUD_SHADOW}",
cloud_body("")
),
),
("fog", FOG.to_owned()),
]
}