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:
@@ -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:
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user