ccc3e20902
Smoke: one rule by engine, no heat on trails, no trail on P1-SUN. Searchlights: two lamps a target, off with an empty sky. Halos fade before their reach and their sum is limited. Launch sites load chunks through the server thread, go off-map as a last resort, and reach 4000 blocks. Air defence: ranges x1.5, shots grow with urgency, claims released, /airdef lastchance, long shots on a big radar's tracks. Build: rust-toolchain.toml, airdef.py check, generator manifest and --check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1685 lines
72 KiB
Diff
1685 lines
72 KiB
Diff
From 9ebf0031fe9ba6b7f02afb26e4fd3a1ba4ddb0d0 Mon Sep 17 00:00:00 2001
|
|
From: chexit <chexit@duck.com>
|
|
Date: Thu, 29 Oct 2026 13:59:43 +0300
|
|
Subject: [PATCH 12/40] HD lighting stage 2: sun and moon shadow cascades, the
|
|
world relit (sun by face angle and shadow, sky and block light through the
|
|
lightmap), light shafts and halos, fog over it again, a light colour grade
|
|
|
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
---
|
|
crates/horizon-client/src/entity.wgsl | 11 +-
|
|
crates/horizon-client/src/game.rs | 2 +-
|
|
crates/horizon-client/src/game/mod_models.rs | 40 +-
|
|
crates/horizon-client/src/game/mods.rs | 6 +-
|
|
crates/horizon-client/src/gfx.rs | 271 +++++++-
|
|
crates/horizon-client/src/gfx/hd.rs | 673 ++++++++++++++++---
|
|
crates/horizon-client/src/gfx/mod_post.rs | 64 +-
|
|
crates/horizon-client/src/gfx/mod_views.rs | 9 +-
|
|
crates/horizon-client/src/terrain.wgsl | 10 +
|
|
9 files changed, 957 insertions(+), 129 deletions(-)
|
|
|
|
diff --git a/crates/horizon-client/src/entity.wgsl b/crates/horizon-client/src/entity.wgsl
|
|
index 01c64def9..61bc1f92a 100644
|
|
--- a/crates/horizon-client/src/entity.wgsl
|
|
+++ b/crates/horizon-client/src/entity.wgsl
|
|
@@ -56,8 +56,9 @@ struct Out {
|
|
@location(11) glint_uv: vec2<f32>,
|
|
// 1 drops the quad's back-facing pixels (Vertex::normal w = 1), like a culling render type.
|
|
@location(12) @interpolate(flat) cull: u32,
|
|
- // AirDef fork, HD lighting's G-buffer: the camera-relative position.
|
|
+ // AirDef fork, HD lighting's G-buffer: the camera-relative position, sky and block light.
|
|
@location(13) pos: vec3<f32>,
|
|
+ @location(14) lm: vec2<f32>,
|
|
}
|
|
// Shipped light.glsl; normals are already transformed and RGBA8_SNORM packed.
|
|
fn entity_light(normal: vec3<f32>, nether: bool) -> f32 {
|
|
@@ -121,6 +122,7 @@ fn entity_vertex(position: vec3<f32>, uv: vec2<f32>,
|
|
out.cylindrical = select(max(length(position.xz), abs(position.y)), 0.0, overlay >= 2u && overlay <= 4u);
|
|
out.overlay = overlay;
|
|
out.pos = position;
|
|
+ out.lm = vec2(l.y, l.x) / 240.0;
|
|
out.glint_uv = vec2(0.0);
|
|
if overlay == 4u { out.spherical = length(position); }
|
|
return out;
|
|
@@ -269,6 +271,7 @@ fn crumbling_uv(uv: vec2<f32>, packed_origin: vec2<f32>, sprite_size: vec2<f32>,
|
|
struct EntityGBufferOut {
|
|
@location(0) albedo: vec4<f32>,
|
|
@location(1) normal: vec4<f32>,
|
|
+ @location(2) light: vec4<f32>,
|
|
}
|
|
@fragment fn fs_entity_gbuffer(in: Out, @builtin(front_facing) front: bool) -> EntityGBufferOut {
|
|
let color = textureSample(atlas, atlas_sampler, in.uv) * select(in.back_color, in.color, front);
|
|
@@ -280,8 +283,14 @@ struct EntityGBufferOut {
|
|
var out: EntityGBufferOut;
|
|
out.albedo = vec4(color.rgb, glow);
|
|
out.normal = vec4(n, 1.0);
|
|
+ out.light = vec4(clamp(in.lm, vec2(0.0), vec2(1.0)), 0.0, 1.0);
|
|
return out;
|
|
}
|
|
+// AirDef fork: entities in the sun's shadow map.
|
|
+@fragment fn fs_entity_shadow(in: Out) {
|
|
+ let a = textureSample(atlas, atlas_sampler, in.uv).a * in.color.a;
|
|
+ if a < 0.1 || in.overlay == 4u || in.overlay == 9u || in.overlay == 10u || in.overlay == 40u { discard; }
|
|
+}
|
|
|
|
// rendertype_outline.fsh: the vertex colour (the team colour; outline vertices carry no
|
|
// normal, so no cardinal light) wherever the texture is not fully clear.
|
|
diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs
|
|
index 3c97bbfb6..3a11ee283 100644
|
|
--- a/crates/horizon-client/src/game.rs
|
|
+++ b/crates/horizon-client/src/game.rs
|
|
@@ -112,10 +112,10 @@ mod mod_browser;
|
|
#[cfg(feature = "mods")]
|
|
mod mod_browser_recipes;
|
|
mod mod_input;
|
|
+mod mod_models;
|
|
#[cfg(feature = "mods")]
|
|
mod mod_recipes;
|
|
mod mod_render;
|
|
-mod mod_models;
|
|
mod mods;
|
|
mod mount_menu;
|
|
mod mounts;
|
|
diff --git a/crates/horizon-client/src/game/mod_models.rs b/crates/horizon-client/src/game/mod_models.rs
|
|
index e6dbc7953..2d1b4780a 100644
|
|
--- a/crates/horizon-client/src/game/mod_models.rs
|
|
+++ b/crates/horizon-client/src/game/mod_models.rs
|
|
@@ -65,7 +65,11 @@ impl Game<'_> {
|
|
pub(super) fn mod_hd(&self) -> Option<horizon_client::gfx::HdSettings> {
|
|
None
|
|
}
|
|
- pub(super) fn mod_lights(&self, _camera: glam::DVec3, _alpha: f64) -> Vec<horizon_client::gfx::HdLight> {
|
|
+ pub(super) fn mod_lights(
|
|
+ &self,
|
|
+ _camera: glam::DVec3,
|
|
+ _alpha: f64,
|
|
+ ) -> Vec<horizon_client::gfx::HdLight> {
|
|
Vec::new()
|
|
}
|
|
}
|
|
@@ -127,7 +131,12 @@ impl Game<'_> {
|
|
let mut lights: HashMap<(usize, u32), (PointLight, PointLight)> = HashMap::new();
|
|
for (owner, l) in self.mods.mods.lights() {
|
|
let key = (owner, l.id);
|
|
- let before = self.mods.models.lights.get(&key).map_or(*l, |(_, now)| *now);
|
|
+ let before = self
|
|
+ .mods
|
|
+ .models
|
|
+ .lights
|
|
+ .get(&key)
|
|
+ .map_or(*l, |(_, now)| *now);
|
|
lights.insert(key, (before, *l));
|
|
}
|
|
self.mods.models.lights = lights;
|
|
@@ -136,7 +145,10 @@ impl Game<'_> {
|
|
self.mods.models.views = views
|
|
.into_iter()
|
|
.map(|v| {
|
|
- let before = old.iter().find(|(_, o)| o.id == v.id).map_or(v, |(_, o)| *o);
|
|
+ let before = old
|
|
+ .iter()
|
|
+ .find(|(_, o)| o.id == v.id)
|
|
+ .map_or(v, |(_, o)| *o);
|
|
(before, v)
|
|
})
|
|
.collect();
|
|
@@ -145,12 +157,19 @@ impl Game<'_> {
|
|
/// The HD lighting a mod asked for, if any.
|
|
pub(super) fn mod_hd(&self) -> Option<horizon_client::gfx::HdSettings> {
|
|
let l = self.mods.mods.lighting()?;
|
|
- l.hd.then_some(horizon_client::gfx::HdSettings { exposure: l.exposure, bloom: l.bloom })
|
|
+ l.hd.then_some(horizon_client::gfx::HdSettings {
|
|
+ exposure: l.exposure,
|
|
+ bloom: l.bloom,
|
|
+ })
|
|
}
|
|
|
|
/// The mods' lights between their last two settings, relative to the camera; the nearest and
|
|
/// brightest first when there are too many.
|
|
- pub(super) fn mod_lights(&self, camera: DVec3, alpha: f64) -> Vec<horizon_client::gfx::HdLight> {
|
|
+ pub(super) fn mod_lights(
|
|
+ &self,
|
|
+ camera: DVec3,
|
|
+ alpha: f64,
|
|
+ ) -> Vec<horizon_client::gfx::HdLight> {
|
|
let t = alpha.clamp(0.0, 1.0);
|
|
let mut out: Vec<(f32, horizon_client::gfx::HdLight)> = self
|
|
.mods
|
|
@@ -167,7 +186,12 @@ impl Game<'_> {
|
|
}
|
|
let light = horizon_client::gfx::HdLight {
|
|
pos: [at.x as f32, at.y as f32, at.z as f32, radius],
|
|
- color: [b.color.0, b.color.1, b.color.2, a.intensity + (b.intensity - a.intensity) * t as f32],
|
|
+ color: [
|
|
+ b.color.0,
|
|
+ b.color.1,
|
|
+ b.color.2,
|
|
+ a.intensity + (b.intensity - a.intensity) * t as f32,
|
|
+ ],
|
|
};
|
|
Some((light.color[3] / (1.0 + d * d * 0.001), light))
|
|
})
|
|
@@ -266,7 +290,9 @@ impl Game<'_> {
|
|
None => (0, None, Vec::new()),
|
|
};
|
|
let mut error = None;
|
|
- let Some(renderer) = self.renderer.as_mut() else { return };
|
|
+ let Some(renderer) = self.renderer.as_mut() else {
|
|
+ return;
|
|
+ };
|
|
if version != self.mods.models.post {
|
|
self.mods.models.post = version;
|
|
error = renderer.set_mod_post(source.as_deref()).err();
|
|
diff --git a/crates/horizon-client/src/game/mods.rs b/crates/horizon-client/src/game/mods.rs
|
|
index ebdd18a24..a760c7763 100644
|
|
--- a/crates/horizon-client/src/game/mods.rs
|
|
+++ b/crates/horizon-client/src/game/mods.rs
|
|
@@ -484,7 +484,11 @@ impl Game<'_> {
|
|
known_recipes,
|
|
placing: self.mod_recipe_screen(),
|
|
creative: self.creative,
|
|
- world: self.player.is_some().then(|| self.mods.world_id.clone()).flatten(),
|
|
+ world: self
|
|
+ .player
|
|
+ .is_some()
|
|
+ .then(|| self.mods.world_id.clone())
|
|
+ .flatten(),
|
|
}
|
|
}
|
|
/// The first block along the player's view within
|
|
diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs
|
|
index be51a0289..e351763fd 100644
|
|
--- a/crates/horizon-client/src/gfx.rs
|
|
+++ b/crates/horizon-client/src/gfx.rs
|
|
@@ -71,15 +71,15 @@ mod arena;
|
|
mod cull;
|
|
mod end_sky;
|
|
mod flat;
|
|
+mod hd;
|
|
mod hdr;
|
|
pub mod lightning;
|
|
mod lod;
|
|
+mod mod_post;
|
|
+mod mod_views;
|
|
mod native_material;
|
|
mod native_pack;
|
|
mod native_post;
|
|
-mod hd;
|
|
-mod mod_post;
|
|
-mod mod_views;
|
|
pub use hd::{HdLight, HdSettings};
|
|
pub use mod_views::ModView;
|
|
mod native_sky;
|
|
@@ -563,6 +563,10 @@ pub struct Renderer {
|
|
/// AirDef fork: the HD lighting's G-buffer pipelines (terrain, entities) and its targets.
|
|
gbuffer_terrain: wgpu::RenderPipeline,
|
|
gbuffer_entity: wgpu::RenderPipeline,
|
|
+ /// The HD sun shadows: depth only, from the light, and the sections in its farther cascade.
|
|
+ shadow_terrain: wgpu::RenderPipeline,
|
|
+ shadow_entity: wgpu::RenderPipeline,
|
|
+ hd_shadow_visible: Vec<resident::Visible>,
|
|
hd: Option<hd::Hd>,
|
|
mod_params: (Vec<f32>, f32),
|
|
/// Time the shader pack's passes on the GPU (F3, or `HORIZON_SHADER_PACK_TIMING`).
|
|
@@ -1347,9 +1351,34 @@ impl Renderer {
|
|
let tri = wgpu::PrimitiveTopology::TriangleList;
|
|
// AirDef fork: the HD lighting's G-buffer (terrain.wgsl's fs_gbuffer): albedo and normal.
|
|
let gbuffer_targets = [
|
|
- Some(wgpu::ColorTargetState { format: hd::ALBEDO, blend: None, write_mask: wgpu::ColorWrites::ALL }),
|
|
- Some(wgpu::ColorTargetState { format: hd::NORMAL, blend: None, write_mask: wgpu::ColorWrites::ALL }),
|
|
+ Some(wgpu::ColorTargetState {
|
|
+ format: hd::ALBEDO,
|
|
+ blend: None,
|
|
+ write_mask: wgpu::ColorWrites::ALL,
|
|
+ }),
|
|
+ Some(wgpu::ColorTargetState {
|
|
+ format: hd::NORMAL,
|
|
+ blend: None,
|
|
+ write_mask: wgpu::ColorWrites::ALL,
|
|
+ }),
|
|
+ Some(wgpu::ColorTargetState {
|
|
+ format: hd::LIGHT_LEVELS,
|
|
+ blend: None,
|
|
+ write_mask: wgpu::ColorWrites::ALL,
|
|
+ }),
|
|
];
|
|
+ // The shadow maps: plain depth from the light, biased against acne, both faces.
|
|
+ let shadow_depth = Some(wgpu::DepthStencilState {
|
|
+ format: wgpu::TextureFormat::Depth32Float,
|
|
+ depth_write_enabled: Some(true),
|
|
+ depth_compare: Some(wgpu::CompareFunction::Less),
|
|
+ stencil: Default::default(),
|
|
+ bias: wgpu::DepthBiasState {
|
|
+ constant: 2,
|
|
+ slope_scale: 2.0,
|
|
+ clamp: 0.0,
|
|
+ },
|
|
+ });
|
|
let gbuffer_depth = Some(wgpu::DepthStencilState {
|
|
format: wgpu::TextureFormat::Depth32Float,
|
|
depth_write_enabled: Some(true),
|
|
@@ -1366,18 +1395,48 @@ impl Renderer {
|
|
compilation_options: Default::default(),
|
|
buffers: &terrain_buffers,
|
|
},
|
|
- primitive: wgpu::PrimitiveState { cull_mode: Some(wgpu::Face::Back), ..Default::default() },
|
|
+ primitive: wgpu::PrimitiveState {
|
|
+ cull_mode: Some(wgpu::Face::Back),
|
|
+ ..Default::default()
|
|
+ },
|
|
depth_stencil: gbuffer_depth.clone(),
|
|
multisample: Default::default(),
|
|
fragment: Some(wgpu::FragmentState {
|
|
module: &shader,
|
|
entry_point: Some("fs_gbuffer"),
|
|
- compilation_options: wgpu::PipelineCompilationOptions { constants: &sprite_constants, ..Default::default() },
|
|
+ compilation_options: wgpu::PipelineCompilationOptions {
|
|
+ constants: &sprite_constants,
|
|
+ ..Default::default()
|
|
+ },
|
|
targets: &gbuffer_targets,
|
|
}),
|
|
multiview_mask: None,
|
|
cache: None,
|
|
});
|
|
+ let shadow_terrain = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
+ label: Some("HD shadows terrain"),
|
|
+ layout: Some(&layout),
|
|
+ vertex: wgpu::VertexState {
|
|
+ module: &shader,
|
|
+ entry_point: Some("vs_terrain"),
|
|
+ compilation_options: Default::default(),
|
|
+ buffers: &terrain_buffers,
|
|
+ },
|
|
+ primitive: Default::default(),
|
|
+ depth_stencil: shadow_depth.clone(),
|
|
+ multisample: Default::default(),
|
|
+ fragment: Some(wgpu::FragmentState {
|
|
+ module: &shader,
|
|
+ entry_point: Some("fs_shadow"),
|
|
+ compilation_options: wgpu::PipelineCompilationOptions {
|
|
+ constants: &sprite_constants,
|
|
+ ..Default::default()
|
|
+ },
|
|
+ targets: &[],
|
|
+ }),
|
|
+ multiview_mask: None,
|
|
+ cache: None,
|
|
+ });
|
|
let solid = pipeline(
|
|
"Terrain solid",
|
|
"vs_terrain",
|
|
@@ -1996,6 +2055,27 @@ impl Renderer {
|
|
multiview_mask: None,
|
|
cache: None,
|
|
});
|
|
+ let shadow_entity = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
+ label: Some("HD shadows entities"),
|
|
+ layout: Some(&entity_layout),
|
|
+ vertex: wgpu::VertexState {
|
|
+ module: &entity_shader,
|
|
+ entry_point: Some("vs_entity"),
|
|
+ compilation_options: Default::default(),
|
|
+ buffers: &entity_vertex,
|
|
+ },
|
|
+ primitive: Default::default(),
|
|
+ depth_stencil: shadow_depth.clone(),
|
|
+ multisample: Default::default(),
|
|
+ fragment: Some(wgpu::FragmentState {
|
|
+ module: &entity_shader,
|
|
+ entry_point: Some("fs_entity_shadow"),
|
|
+ compilation_options: Default::default(),
|
|
+ targets: &[],
|
|
+ }),
|
|
+ multiview_mask: None,
|
|
+ cache: None,
|
|
+ });
|
|
let entity_pipeline = |label, cull_mode, depth_write, compare, bias, blend, clouds| {
|
|
entity_pipeline_with(
|
|
label,
|
|
@@ -2729,6 +2809,9 @@ impl Renderer {
|
|
mod_views: None,
|
|
gbuffer_terrain,
|
|
gbuffer_entity,
|
|
+ shadow_terrain,
|
|
+ shadow_entity,
|
|
+ hd_shadow_visible: Vec::new(),
|
|
hd: None,
|
|
mod_params: (Vec::new(), 0.0),
|
|
pass_timing: pack_timing::PassTiming::logging(),
|
|
@@ -3112,14 +3195,17 @@ impl Renderer {
|
|
let mut at = range.start;
|
|
if let Some(views) = &self.mod_views {
|
|
for v in batch.views.iter().filter(|v| {
|
|
- v.at >= range.start && (v.at < range.end || (v.at == range.end && range.end == last))
|
|
+ v.at >= range.start
|
|
+ && (v.at < range.end || (v.at == range.end && range.end == last))
|
|
}) {
|
|
- self.hud.draw_range(encoder, target, &self.depth, batch, size, at..v.at);
|
|
+ self.hud
|
|
+ .draw_range(encoder, target, &self.depth, batch, size, at..v.at);
|
|
views.blit(encoder, target, size, v, batch.scale);
|
|
at = v.at;
|
|
}
|
|
}
|
|
- self.hud.draw_range(encoder, target, &self.depth, batch, size, at..range.end);
|
|
+ self.hud
|
|
+ .draw_range(encoder, target, &self.depth, batch, size, at..range.end);
|
|
}
|
|
/// The mod cameras due this frame, each into its picture: sky, solid
|
|
/// terrain and entities, through the world's own pipelines with the
|
|
@@ -3215,7 +3301,9 @@ impl Renderer {
|
|
pass.draw_indexed(0..s.solid * 6, s.start as i32, instance..instance + 1);
|
|
}
|
|
for (k, v) in d.lods.iter().enumerate() {
|
|
- let Some(tile) = self.lods.get(&v.key) else { continue };
|
|
+ let Some(tile) = self.lods.get(&v.key) else {
|
|
+ continue;
|
|
+ };
|
|
let s = &tile.mesh;
|
|
if s.solid == 0 {
|
|
continue;
|
|
@@ -3520,7 +3608,11 @@ impl Renderer {
|
|
}
|
|
Some(source) => {
|
|
// Under the HD lighting it draws into the HD target, else onto the screen.
|
|
- let format = if self.mod_post_hdr { hd::HDR } else { self.config.format.remove_srgb_suffix() };
|
|
+ let format = if self.mod_post_hdr {
|
|
+ hd::HDR
|
|
+ } else {
|
|
+ self.config.format.remove_srgb_suffix()
|
|
+ };
|
|
self.mod_post = Some(mod_post::ModPost::new(&self.device, source, format)?);
|
|
self.mod_post_source = Some(source.to_owned());
|
|
}
|
|
@@ -3530,7 +3622,9 @@ impl Renderer {
|
|
/// AirDef fork: the mod post pass's parameters for the next frames.
|
|
pub fn set_mod_params(&mut self, params: &[f32], partial: f32) {
|
|
self.mod_params.0.clear();
|
|
- self.mod_params.0.extend_from_slice(¶ms[..params.len().min(mod_post::PARAMS)]);
|
|
+ self.mod_params
|
|
+ .0
|
|
+ .extend_from_slice(¶ms[..params.len().min(mod_post::PARAMS)]);
|
|
self.mod_params.1 = partial;
|
|
}
|
|
pub fn drop_shader_packs(&mut self) {
|
|
@@ -4049,6 +4143,41 @@ impl Renderer {
|
|
});
|
|
}
|
|
self.drawn_sections = visible.len() + visible_lods.len();
|
|
+ // AirDef fork: the HD sun shadows draw every section in the farther cascade's box.
|
|
+ let hd_wanted = !frame.title
|
|
+ && frame.hd.is_some()
|
|
+ && !self.pack_post.as_ref().is_some_and(|p| p.1.is_some())
|
|
+ && !self.native.as_ref().is_some_and(|n| n.post.is_some());
|
|
+ let hd_scene = hd::HdScene {
|
|
+ view_proj,
|
|
+ camera,
|
|
+ // The world's sun: vanilla turns the sky -90° about Y, so it crosses from east to west.
|
|
+ sun: Vec3::new(-sun_angle.sin(), sun_angle.cos(), 0.0),
|
|
+ dark: 1.0 - frame.sky.sky_light_factor,
|
|
+ rain: if frame.weather.is_empty() { 0.0 } else { 0.7 },
|
|
+ fog_color,
|
|
+ fog,
|
|
+ sky: frame.skybox && !frame.end_sky && !frame.nether_lighting && !frame.underwater,
|
|
+ };
|
|
+ let (shadow_dir, shadow_strength, _) = hd::light_of(&hd_scene);
|
|
+ let hd_cascades = hd::cascades(camera, shadow_dir);
|
|
+ let mut hd_shadow_visible = std::mem::take(&mut self.hd_shadow_visible);
|
|
+ hd_shadow_visible.clear();
|
|
+ if hd_wanted && shadow_strength > 0.0 {
|
|
+ let radius =
|
|
+ ((frame.detail_distance / 16.0) as i32).min((hd::CASCADES[1] / 16.0) as i32 + 2);
|
|
+ let light = Frustum {
|
|
+ planes: frustum(hd_cascades[1]),
|
|
+ };
|
|
+ self.sections.visible(
|
|
+ camera,
|
|
+ radius,
|
|
+ &light,
|
|
+ |_| true,
|
|
+ |_, _| false,
|
|
+ &mut hd_shadow_visible,
|
|
+ );
|
|
+ }
|
|
// Mods' cameras: their sections round them, all of them (no occlusion), after the shadow's.
|
|
let globals_size = std::mem::size_of::<Globals>() as u64;
|
|
let mut views_due = Vec::new();
|
|
@@ -4063,7 +4192,9 @@ impl Renderer {
|
|
for v in views.choose(device, frame.mod_views, globals_size) {
|
|
let (view_proj, view, aspect) = mod_views::matrices(v, camera, frame.depth_far);
|
|
let own = world_projection(v.fov, aspect, frame.depth_far) * view;
|
|
- let planes = Frustum { planes: frustum(own) };
|
|
+ let planes = Frustum {
|
|
+ planes: frustum(own),
|
|
+ };
|
|
let mut seen = Vec::new();
|
|
// Where distant terrain covers a column, its tile draws instead, as in the world pass.
|
|
self.sections.visible(
|
|
@@ -4105,8 +4236,13 @@ impl Renderer {
|
|
} else if frame.mod_views.is_empty() {
|
|
self.mod_views = None;
|
|
}
|
|
- let view_sections: usize = views_due.iter().map(|d| d.visible.len() + d.lods.len()).sum();
|
|
- let instances = self.drawn_sections + shadow_visible.len() + view_sections;
|
|
+ let view_sections: usize = views_due
|
|
+ .iter()
|
|
+ .map(|d| d.visible.len() + d.lods.len())
|
|
+ .sum();
|
|
+ let instances =
|
|
+ self.drawn_sections + shadow_visible.len() + view_sections + hd_shadow_visible.len();
|
|
+ let hd_shadow_first = (self.drawn_sections + shadow_visible.len() + view_sections) as u32;
|
|
if instances as u64 > self.instance_capacity {
|
|
self.instance_capacity = (instances as u64).next_power_of_two();
|
|
self.instances = self.device.create_buffer(&wgpu::BufferDescriptor {
|
|
@@ -4125,14 +4261,13 @@ impl Renderer {
|
|
.map(|v| section_instance(v.origin))
|
|
.chain(visible_lods.iter().map(|v| lod::instance(v.origin)))
|
|
.chain(shadow_visible.iter().map(|v| section_instance(v.origin)))
|
|
- .chain(
|
|
- views_due.iter().flat_map(|d| {
|
|
- d.visible
|
|
- .iter()
|
|
- .map(|v| section_instance(v.origin))
|
|
- .chain(d.lods.iter().map(|v| lod::instance(v.origin)))
|
|
- }),
|
|
- ),
|
|
+ .chain(views_due.iter().flat_map(|d| {
|
|
+ d.visible
|
|
+ .iter()
|
|
+ .map(|v| section_instance(v.origin))
|
|
+ .chain(d.lods.iter().map(|v| lod::instance(v.origin)))
|
|
+ }))
|
|
+ .chain(hd_shadow_visible.iter().map(|v| section_instance(v.origin))),
|
|
);
|
|
if !self.origins.is_empty() {
|
|
self.uploads.buffer(
|
|
@@ -4835,10 +4970,65 @@ impl Renderer {
|
|
if hd_on && let Some(size) = self.world_post.size() {
|
|
let format = self.config.format.remove_srgb_suffix();
|
|
let device = &self.device;
|
|
- let hd = self.hd.get_or_insert_with(|| hd::Hd::new(device, size, format));
|
|
+ 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));
|
|
hd.resize(device, size);
|
|
if let Some(settings) = frame.hd {
|
|
- hd.write(&self.queue, view_proj, frame.hd_lights, settings);
|
|
+ hd.write(&self.queue, &hd_scene, frame.hd_lights, settings);
|
|
+ }
|
|
+ // The sun's (or moon's) shadow cascades: the world's globals swapped for the light's.
|
|
+ if shadow_strength > 0.0 {
|
|
+ self.queue
|
|
+ .write_buffer(&hd.main_globals, 0, bytemuck::bytes_of(&globals));
|
|
+ for (c, cascade) in hd_cascades.iter().enumerate() {
|
|
+ let light = Globals {
|
|
+ view_proj: cascade.to_cols_array_2d(),
|
|
+ ..globals
|
|
+ };
|
|
+ self.queue
|
|
+ .write_buffer(&hd.shadow_globals[c], 0, bytemuck::bytes_of(&light));
|
|
+ encoder.copy_buffer_to_buffer(
|
|
+ &hd.shadow_globals[c],
|
|
+ 0,
|
|
+ &self.globals,
|
|
+ 0,
|
|
+ globals_size,
|
|
+ );
|
|
+ let box_ = Frustum {
|
|
+ planes: frustum(*cascade),
|
|
+ };
|
|
+ let mut pass = hd.shadow_pass(&mut encoder, c);
|
|
+ pass.set_bind_group(0, &self.group, &[]);
|
|
+ pass.set_pipeline(&self.shadow_terrain);
|
|
+ pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
|
+ pass.set_vertex_buffer(1, self.instances.slice(..));
|
|
+ let mut page = None;
|
|
+ for (k, v) in hd_shadow_visible.iter().enumerate() {
|
|
+ let s = self.sections.get(v.slot);
|
|
+ if s.solid == 0 {
|
|
+ continue;
|
|
+ }
|
|
+ let min = Vec3::from(v.origin);
|
|
+ if c == 0 && !box_.intersects(min, min + Vec3::splat(16.0)) {
|
|
+ continue;
|
|
+ }
|
|
+ if page != Some(s.page) {
|
|
+ pass.set_vertex_buffer(0, self.arena.buffer(s.page).slice(..));
|
|
+ page = Some(s.page);
|
|
+ }
|
|
+ let instance = hd_shadow_first + k as u32;
|
|
+ pass.draw_indexed(0..s.solid * 6, s.start as i32, instance..instance + 1);
|
|
+ }
|
|
+ if entity_quads > 0 {
|
|
+ pass.set_pipeline(&self.shadow_entity);
|
|
+ pass.set_bind_group(0, &self.entity_group, &[]);
|
|
+ pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
|
+ pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
|
|
+ }
|
|
+ }
|
|
+ encoder.copy_buffer_to_buffer(&hd.main_globals, 0, &self.globals, 0, globals_size);
|
|
}
|
|
let mut pass = hd.gbuffer_pass(&mut encoder);
|
|
pass.set_bind_group(0, &self.group, &[]);
|
|
@@ -4846,7 +5036,10 @@ impl Renderer {
|
|
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
|
pass.set_vertex_buffer(1, self.instances.slice(..));
|
|
let mut page = None;
|
|
- let sections = visible.iter().enumerate().map(|(k, v)| (k, self.sections.get(v.slot)));
|
|
+ let sections = visible
|
|
+ .iter()
|
|
+ .enumerate()
|
|
+ .map(|(k, v)| (k, self.sections.get(v.slot)));
|
|
let tiles = visible_lods
|
|
.iter()
|
|
.enumerate()
|
|
@@ -5397,11 +5590,22 @@ 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.light(&self.device, &mut encoder, (color, depth));
|
|
+ hd.light(
|
|
+ &self.device,
|
|
+ &mut encoder,
|
|
+ (color, depth),
|
|
+ (&self.lightmap_view, &self.lightmap_sampler),
|
|
+ );
|
|
match &mut self.mod_post {
|
|
Some(post) if mod_post => {
|
|
let size = self.world_post.size().unwrap_or([1, 1]);
|
|
- post.write(&self.queue, frame, size, &self.mod_params.0, self.mod_params.1);
|
|
+ post.write(
|
|
+ &self.queue,
|
|
+ frame,
|
|
+ size,
|
|
+ &self.mod_params.0,
|
|
+ self.mod_params.1,
|
|
+ );
|
|
post.run(&self.device, &mut encoder, (&hd.lit, depth), &hd.fx);
|
|
hd.finish(&self.device, &mut encoder, &hd.fx, &view_target);
|
|
}
|
|
@@ -5412,7 +5616,13 @@ impl Renderer {
|
|
&& let Some(post) = &mut self.mod_post
|
|
{
|
|
let size = self.world_post.size().unwrap_or([1, 1]);
|
|
- post.write(&self.queue, frame, size, &self.mod_params.0, self.mod_params.1);
|
|
+ post.write(
|
|
+ &self.queue,
|
|
+ frame,
|
|
+ size,
|
|
+ &self.mod_params.0,
|
|
+ self.mod_params.1,
|
|
+ );
|
|
post.run(&self.device, &mut encoder, (color, depth), &view_target);
|
|
} else if scaled.is_some() {
|
|
self.world_post.finish(&mut encoder, &view_target);
|
|
@@ -5635,6 +5845,7 @@ impl Renderer {
|
|
self.visible = visible;
|
|
self.visible_lods = visible_lods;
|
|
self.shadow_visible = shadow_visible;
|
|
+ self.hd_shadow_visible = hd_shadow_visible;
|
|
Ok(true)
|
|
}
|
|
}
|
|
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
|
|
index b40484e5e..c36c1795d 100644
|
|
--- a/crates/horizon-client/src/gfx/hd.rs
|
|
+++ b/crates/horizon-client/src/gfx/hd.rs
|
|
@@ -10,6 +10,8 @@ use glam::Mat4;
|
|
/// The G-buffer's formats.
|
|
pub const ALBEDO: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
|
pub const NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
|
+/// Sky and block light per pixel.
|
|
+pub const LIGHT_LEVELS: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
|
/// The lit world and the post pass's output.
|
|
pub const HDR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
|
/// The most lights drawn a frame.
|
|
@@ -41,15 +43,54 @@ struct HdFrame {
|
|
inverse_view_proj: [[f32; 4]; 4],
|
|
/// width, height, 1/width, 1/height.
|
|
resolution: [f32; 4],
|
|
- /// lights, exposure, bloom, -.
|
|
+ /// lights, exposure, bloom, a frame counter.
|
|
params: [f32; 4],
|
|
+ /// The shadow cascades: camera-relative position to the light's clip space.
|
|
+ cascades: [[[f32; 4]; 4]; 2],
|
|
+ /// xyz: towards the sun (or the moon at night); w: how much it lights (0 set or hidden).
|
|
+ sun: [f32; 4],
|
|
+ sun_color: [f32; 4],
|
|
+ fog_color: [f32; 4],
|
|
+ /// Environmental start, end; render-distance start, end.
|
|
+ fog: [f32; 4],
|
|
+ /// x: sky light kept in the shade; y: blocks' light; z: the air's density (rays, halos).
|
|
+ ambient: [f32; 4],
|
|
+ /// x: shadows on; y, z: the cascades' half sizes.
|
|
+ shadow: [f32; 4],
|
|
}
|
|
|
|
+/// What the world around looks like this frame (from the renderer's globals).
|
|
+#[derive(Clone, Copy, Debug)]
|
|
+pub struct HdScene {
|
|
+ pub view_proj: Mat4,
|
|
+ /// The camera in the world, for steady shadows.
|
|
+ pub camera: glam::DVec3,
|
|
+ /// Towards the sun, and the sky's darkening (0 day, 1 night).
|
|
+ pub sun: glam::Vec3,
|
|
+ pub dark: f32,
|
|
+ pub rain: f32,
|
|
+ pub fog_color: [f32; 4],
|
|
+ pub fog: [f32; 4],
|
|
+ /// The sky has a sun at all (the Overworld).
|
|
+ pub sky: bool,
|
|
+}
|
|
+
|
|
+/// The cascades' half sizes (blocks) and shadow map size.
|
|
+pub const CASCADES: [f32; 2] = [40.0, 176.0];
|
|
+pub const SHADOW_SIZE: u32 = 2048;
|
|
+
|
|
const COMMON: &str = r#"
|
|
struct HdFrame {
|
|
inverse_view_proj: mat4x4<f32>,
|
|
resolution: vec4<f32>,
|
|
params: vec4<f32>,
|
|
+ cascades: array<mat4x4<f32>, 2>,
|
|
+ sun: vec4<f32>,
|
|
+ sun_color: vec4<f32>,
|
|
+ fog_color: vec4<f32>,
|
|
+ fog: vec4<f32>,
|
|
+ ambient: vec4<f32>,
|
|
+ shadow: vec4<f32>,
|
|
}
|
|
struct FullOut {
|
|
@builtin(position) position: vec4<f32>,
|
|
@@ -77,38 +118,131 @@ struct Light {
|
|
@group(0) @binding(4) var normal_tex: texture_2d<f32>;
|
|
@group(0) @binding(5) var gbuffer_depth: texture_depth_2d;
|
|
@group(0) @binding(6) var<storage, read> lights: array<Light>;
|
|
+@group(0) @binding(7) var light_tex: texture_2d<f32>;
|
|
+@group(0) @binding(8) var shadow_map: texture_depth_2d_array;
|
|
+@group(0) @binding(9) var shadow_sampler: sampler_comparison;
|
|
+@group(0) @binding(10) var lightmap: texture_2d<f32>;
|
|
+@group(0) @binding(11) var linear: sampler;
|
|
+
|
|
+// Interleaved gradient noise: a different offset each pixel and frame, for the ray march.
|
|
+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))));
|
|
+}
|
|
+
|
|
+// Where a camera-relative point falls in cascade c: uv and depth.
|
|
+fn cascade_at(p: vec3<f32>, c: i32) -> vec3<f32> {
|
|
+ let s = frame.cascades[c] * vec4<f32>(p, 1.0);
|
|
+ return vec3<f32>(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z);
|
|
+}
|
|
+
|
|
+// The sun's (or moon's) light reaching p: 1 lit, 0 in shadow; soft-edged.
|
|
+fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
|
|
+ let dist = length(p);
|
|
+ let c = select(1, 0, dist < frame.shadow.y * 0.92);
|
|
+ let texel = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0;
|
|
+ let q = p + n * texel * 1.5;
|
|
+ let s = cascade_at(q, c);
|
|
+ if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
|
|
+ var lit = 0.0;
|
|
+ let step = 1.0 / 2048.0;
|
|
+ for (var y = -1; y <= 1; y++) {
|
|
+ for (var x = -1; x <= 1; x++) {
|
|
+ lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2<f32>(f32(x), f32(y)) * step, c, s.z - 0.0005);
|
|
+ }
|
|
+ }
|
|
+ let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
|
|
+ return mix(1.0, lit / 9.0, fade);
|
|
+}
|
|
+
|
|
+// One shadow sample, for the rays through the air.
|
|
+fn lit_at(p: vec3<f32>) -> f32 {
|
|
+ let s = cascade_at(p, 1);
|
|
+ if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
|
|
+ return textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy, 1, s.z - 0.001);
|
|
+}
|
|
+
|
|
+fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
|
|
+ if d <= start { return 0.0; }
|
|
+ if d >= end { return 1.0; }
|
|
+ return (d - start) / (end - start);
|
|
+}
|
|
|
|
@fragment
|
|
fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
|
let px = vec2<i32>(in.position.xy);
|
|
- var c = textureLoad(scene_color, px, 0).rgb;
|
|
+ let scene = textureLoad(scene_color, px, 0).rgb;
|
|
let d = textureLoad(scene_depth, px, 0);
|
|
let g = textureLoad(gbuffer_depth, px, 0);
|
|
- // Only where the G-buffer holds what the world shows (not the sky, water, particles).
|
|
- if d <= 0.0 || abs(d - g) > max(d, g) * 0.002 {
|
|
- return vec4<f32>(c, 1.0);
|
|
- }
|
|
- let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, d, 1.0);
|
|
+ let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(d, 0.000001), 1.0);
|
|
let h = frame.inverse_view_proj * ndc;
|
|
let p = h.xyz / h.w;
|
|
- let n = normalize(textureLoad(normal_tex, px, 0).xyz);
|
|
- let albedo = textureLoad(albedo_tex, px, 0);
|
|
- var light = vec3<f32>(0.0);
|
|
+ let view = normalize(p);
|
|
+ let sun = frame.sun.xyz;
|
|
+ let sun_on = frame.sun.w;
|
|
let count = u32(frame.params.x);
|
|
+ var c = scene;
|
|
+ let surface = d > 0.0 && abs(d - g) <= max(d, g) * 0.002;
|
|
+ if surface {
|
|
+ let n = normalize(textureLoad(normal_tex, px, 0).xyz);
|
|
+ let albedo = textureLoad(albedo_tex, px, 0);
|
|
+ let lm = textureLoad(light_tex, px, 0).xy;
|
|
+ // Vanilla's light (sky and blocks through the lightmap), shaded where the sun can't reach.
|
|
+ let base = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(lm.y, lm.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
|
|
+ let block = textureSampleLevel(lightmap, linear, vec2<f32>(lm.y * (15.0 / 16.0) + 0.5 / 16.0, 0.5 / 16.0), 0.0).rgb;
|
|
+ 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(p, n); } 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;
|
|
+ var light = max(base * frame.ambient.x, block * frame.ambient.y) + direct;
|
|
+ // The mods' lights.
|
|
+ for (var i = 0u; i < count; i++) {
|
|
+ let l = lights[i];
|
|
+ let to = l.pos.xyz - p;
|
|
+ let dist = length(to);
|
|
+ if dist >= l.pos.w { continue; }
|
|
+ let window = pow(clamp(1.0 - pow(dist / l.pos.w, 4.0), 0.0, 1.0), 2.0);
|
|
+ let falloff = l.color.w / (dist * dist + 1.0) * window;
|
|
+ let k = max(dot(n, to / max(dist, 0.001)) * 0.85 + 0.15, 0.0);
|
|
+ light += l.color.rgb * falloff * k;
|
|
+ }
|
|
+ c = albedo.rgb * light + albedo.rgb * albedo.a * 1.5;
|
|
+ // Vanilla's fog over it again.
|
|
+ let sph = length(p);
|
|
+ let cyl = max(length(p.xz), abs(p.y));
|
|
+ let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
|
|
+ c = mix(c, frame.fog_color.rgb, f * frame.fog_color.a);
|
|
+ }
|
|
+ // Light in the air: the sun's rays between the shadows, and halos round the mods' lights.
|
|
+ let far = select(min(length(p), frame.shadow.z), frame.shadow.z, d <= 0.0);
|
|
+ let density = frame.ambient.z;
|
|
+ if sun_on > 0.0 && density > 0.0 {
|
|
+ let steps = 14;
|
|
+ let jitter = ign(in.position.xy);
|
|
+ var acc = 0.0;
|
|
+ for (var i = 0; i < steps; i++) {
|
|
+ let t = (f32(i) + jitter) / f32(steps) * far;
|
|
+ acc += lit_at(view * t);
|
|
+ }
|
|
+ // Henyey-Greenstein, forward scattering: brightest looking towards the sun.
|
|
+ let cos_t = dot(view, sun);
|
|
+ let gk = 0.6;
|
|
+ let phase = (1.0 - gk * gk) / pow(1.0 + gk * gk - 2.0 * gk * cos_t, 1.5) * 0.08;
|
|
+ c += frame.sun_color.rgb * sun_on * (acc / f32(steps)) * phase * (1.0 - exp(-far * density));
|
|
+ }
|
|
for (var i = 0u; i < count; i++) {
|
|
let l = lights[i];
|
|
- let to = l.pos.xyz - p;
|
|
- let dist = length(to);
|
|
- if dist >= l.pos.w { continue; }
|
|
- // Inverse square, softened near the light, faded to nothing at its radius.
|
|
- let window = pow(clamp(1.0 - pow(dist / l.pos.w, 4.0), 0.0, 1.0), 2.0);
|
|
- let falloff = l.color.w / (dist * dist + 1.0) * window;
|
|
- let ndl = max(dot(n, to / max(dist, 0.001)) * 0.85 + 0.15, 0.0);
|
|
- light += l.color.rgb * falloff * ndl;
|
|
+ // Nearest point of the view ray to the light, and the glow along the ray in front of it.
|
|
+ let t0 = dot(l.pos.xyz, view);
|
|
+ let hh = length(l.pos.xyz - view * t0) + 0.5;
|
|
+ if hh > l.pos.w * 0.5 { continue; }
|
|
+ let a0 = atan((0.0 - t0) / hh);
|
|
+ let a1 = atan((far - t0) / hh);
|
|
+ c += l.color.rgb * l.color.w * (a1 - a0) / hh * 0.004 * (0.3 + density * 300.0);
|
|
}
|
|
- c += albedo.rgb * light;
|
|
- // Glowing parts (flames, flares) shine past white, for the bloom.
|
|
- c += albedo.rgb * albedo.a * 1.5;
|
|
return vec4<f32>(c, 1.0);
|
|
}
|
|
"#;
|
|
@@ -188,6 +322,10 @@ fn fs_tonemap(in: FullOut) -> @location(0) vec4<f32> {
|
|
// Very bright light bleaches towards white, as film does.
|
|
c = mix(c, vec3<f32>(rolled), clamp((peak - 2.0) / 8.0, 0.0, 0.6));
|
|
}
|
|
+ // A light grade: a touch more colour, and a gentle S round the middle grey.
|
|
+ let luma = dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
+ c = max(mix(vec3<f32>(luma), c, 1.12), vec3<f32>(0.0));
|
|
+ c = mix(c, c * c * (3.0 - 2.0 * c), 0.18);
|
|
return vec4<f32>(c, 1.0);
|
|
}
|
|
"#;
|
|
@@ -196,11 +334,20 @@ struct Target {
|
|
view: wgpu::TextureView,
|
|
}
|
|
|
|
-fn target(device: &wgpu::Device, label: &str, size: [u32; 2], format: wgpu::TextureFormat) -> Target {
|
|
+fn target(
|
|
+ device: &wgpu::Device,
|
|
+ label: &str,
|
|
+ size: [u32; 2],
|
|
+ format: wgpu::TextureFormat,
|
|
+) -> Target {
|
|
let view = device
|
|
.create_texture(&wgpu::TextureDescriptor {
|
|
label: Some(label),
|
|
- size: wgpu::Extent3d { width: size[0].max(1), height: size[1].max(1), depth_or_array_layers: 1 },
|
|
+ size: wgpu::Extent3d {
|
|
+ width: size[0].max(1),
|
|
+ height: size[1].max(1),
|
|
+ depth_or_array_layers: 1,
|
|
+ },
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: wgpu::TextureDimension::D2,
|
|
@@ -216,6 +363,15 @@ pub struct Hd {
|
|
size: [u32; 2],
|
|
pub albedo: wgpu::TextureView,
|
|
pub normal: wgpu::TextureView,
|
|
+ pub levels: wgpu::TextureView,
|
|
+ /// The shadow cascades: the array to sample, one view per layer to draw.
|
|
+ shadow_array: wgpu::TextureView,
|
|
+ shadow_layers: Vec<wgpu::TextureView>,
|
|
+ shadow_sampler: wgpu::Sampler,
|
|
+ /// The globals the shadow passes draw with (the light's view), and the camera's to put back.
|
|
+ pub shadow_globals: [wgpu::Buffer; 2],
|
|
+ pub main_globals: wgpu::Buffer,
|
|
+ frames: u32,
|
|
pub depth: wgpu::TextureView,
|
|
/// The lit world, and the post pass's output.
|
|
pub lit: wgpu::TextureView,
|
|
@@ -258,12 +414,21 @@ fn fullscreen(
|
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
label: Some(label),
|
|
layout: Some(&pipeline_layout),
|
|
- vertex: wgpu::VertexState { module, entry_point: Some("vs_full"), compilation_options: Default::default(), buffers: &[] },
|
|
+ vertex: wgpu::VertexState {
|
|
+ module,
|
|
+ entry_point: Some("vs_full"),
|
|
+ compilation_options: Default::default(),
|
|
+ buffers: &[],
|
|
+ },
|
|
fragment: Some(wgpu::FragmentState {
|
|
module,
|
|
entry_point: Some(entry),
|
|
compilation_options: Default::default(),
|
|
- targets: &[Some(wgpu::ColorTargetState { format, blend, write_mask: wgpu::ColorWrites::ALL })],
|
|
+ targets: &[Some(wgpu::ColorTargetState {
|
|
+ format,
|
|
+ blend,
|
|
+ write_mask: wgpu::ColorWrites::ALL,
|
|
+ })],
|
|
}),
|
|
primitive: Default::default(),
|
|
depth_stencil: None,
|
|
@@ -286,8 +451,11 @@ fn entries(kinds: &[wgpu::BindingType]) -> Vec<wgpu::BindGroupLayoutEntry> {
|
|
.collect()
|
|
}
|
|
|
|
-const UNIFORM: wgpu::BindingType =
|
|
- wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None };
|
|
+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 {
|
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
|
view_dimension: wgpu::TextureViewDimension::D2,
|
|
@@ -306,7 +474,12 @@ const DEPTH: wgpu::BindingType = wgpu::BindingType::Texture {
|
|
const SAMPLER: wgpu::BindingType = wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering);
|
|
|
|
impl Hd {
|
|
- pub fn new(device: &wgpu::Device, size: [u32; 2], screen: wgpu::TextureFormat) -> Self {
|
|
+ pub fn new(
|
|
+ device: &wgpu::Device,
|
|
+ size: [u32; 2],
|
|
+ screen: wgpu::TextureFormat,
|
|
+ globals_size: u64,
|
|
+ ) -> Self {
|
|
let light_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("HD lighting"),
|
|
entries: &entries(&[
|
|
@@ -321,6 +494,15 @@ impl Hd {
|
|
has_dynamic_offset: false,
|
|
min_binding_size: None,
|
|
},
|
|
+ UNFILTERED,
|
|
+ wgpu::BindingType::Texture {
|
|
+ sample_type: wgpu::TextureSampleType::Depth,
|
|
+ view_dimension: wgpu::TextureViewDimension::D2Array,
|
|
+ multisampled: false,
|
|
+ },
|
|
+ wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
|
|
+ FLOAT,
|
|
+ SAMPLER,
|
|
]),
|
|
});
|
|
let bloom_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
@@ -335,14 +517,58 @@ impl Hd {
|
|
let bloom = module(device, "HD bloom", BLOOM);
|
|
let tone = module(device, "HD tone mapping", TONEMAP);
|
|
let add = wgpu::BlendState {
|
|
- color: wgpu::BlendComponent { src_factor: wgpu::BlendFactor::One, dst_factor: wgpu::BlendFactor::One, operation: wgpu::BlendOperation::Add },
|
|
+ color: wgpu::BlendComponent {
|
|
+ src_factor: wgpu::BlendFactor::One,
|
|
+ dst_factor: wgpu::BlendFactor::One,
|
|
+ operation: wgpu::BlendOperation::Add,
|
|
+ },
|
|
alpha: wgpu::BlendComponent::REPLACE,
|
|
};
|
|
- let light_pipeline = fullscreen(device, "HD lighting", &light_layout, &light, "fs_light", HDR, None);
|
|
- let prefilter = fullscreen(device, "HD bloom prefilter", &bloom_layout, &bloom, "fs_prefilter", HDR, None);
|
|
- let down = fullscreen(device, "HD bloom down", &bloom_layout, &bloom, "fs_down", HDR, None);
|
|
- let up = fullscreen(device, "HD bloom up", &bloom_layout, &bloom, "fs_up", HDR, Some(add));
|
|
- let tone = fullscreen(device, "HD tone mapping", &tone_layout, &tone, "fs_tonemap", screen, None);
|
|
+ let light_pipeline = fullscreen(
|
|
+ device,
|
|
+ "HD lighting",
|
|
+ &light_layout,
|
|
+ &light,
|
|
+ "fs_light",
|
|
+ HDR,
|
|
+ None,
|
|
+ );
|
|
+ let prefilter = fullscreen(
|
|
+ device,
|
|
+ "HD bloom prefilter",
|
|
+ &bloom_layout,
|
|
+ &bloom,
|
|
+ "fs_prefilter",
|
|
+ HDR,
|
|
+ None,
|
|
+ );
|
|
+ let down = fullscreen(
|
|
+ device,
|
|
+ "HD bloom down",
|
|
+ &bloom_layout,
|
|
+ &bloom,
|
|
+ "fs_down",
|
|
+ HDR,
|
|
+ None,
|
|
+ );
|
|
+ let up = fullscreen(
|
|
+ device,
|
|
+ "HD bloom up",
|
|
+ &bloom_layout,
|
|
+ &bloom,
|
|
+ "fs_up",
|
|
+ HDR,
|
|
+ Some(add),
|
|
+ );
|
|
+ let tone = fullscreen(
|
|
+ device,
|
|
+ "HD tone mapping",
|
|
+ &tone_layout,
|
|
+ &tone,
|
|
+ "fs_tonemap",
|
|
+ screen,
|
|
+ None,
|
|
+ );
|
|
let frame = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("HD frame"),
|
|
size: std::mem::size_of::<HdFrame>() as u64,
|
|
@@ -363,11 +589,70 @@ impl Hd {
|
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
|
..Default::default()
|
|
});
|
|
+ let shadow_texture = device.create_texture(&wgpu::TextureDescriptor {
|
|
+ label: Some("HD shadow cascades"),
|
|
+ size: wgpu::Extent3d {
|
|
+ width: SHADOW_SIZE,
|
|
+ height: SHADOW_SIZE,
|
|
+ depth_or_array_layers: 2,
|
|
+ },
|
|
+ mip_level_count: 1,
|
|
+ sample_count: 1,
|
|
+ dimension: wgpu::TextureDimension::D2,
|
|
+ format: wgpu::TextureFormat::Depth32Float,
|
|
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
|
+ view_formats: &[],
|
|
+ });
|
|
+ let shadow_array = shadow_texture.create_view(&wgpu::TextureViewDescriptor {
|
|
+ dimension: Some(wgpu::TextureViewDimension::D2Array),
|
|
+ ..Default::default()
|
|
+ });
|
|
+ let shadow_layers = (0..2)
|
|
+ .map(|l| {
|
|
+ shadow_texture.create_view(&wgpu::TextureViewDescriptor {
|
|
+ dimension: Some(wgpu::TextureViewDimension::D2),
|
|
+ base_array_layer: l,
|
|
+ array_layer_count: Some(1),
|
|
+ ..Default::default()
|
|
+ })
|
|
+ })
|
|
+ .collect();
|
|
+ let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
+ label: Some("HD shadows"),
|
|
+ mag_filter: wgpu::FilterMode::Linear,
|
|
+ min_filter: wgpu::FilterMode::Linear,
|
|
+ compare: Some(wgpu::CompareFunction::LessEqual),
|
|
+ ..Default::default()
|
|
+ });
|
|
+ let globals = |label| {
|
|
+ device.create_buffer(&wgpu::BufferDescriptor {
|
|
+ label: Some(label),
|
|
+ size: globals_size,
|
|
+ usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
|
|
+ mapped_at_creation: false,
|
|
+ })
|
|
+ };
|
|
let mut hd = Hd {
|
|
size: [0, 0],
|
|
albedo: target(device, "HD albedo", [1, 1], ALBEDO).view,
|
|
normal: target(device, "HD normal", [1, 1], NORMAL).view,
|
|
- depth: target(device, "HD depth", [1, 1], wgpu::TextureFormat::Depth32Float).view,
|
|
+ levels: target(device, "HD light levels", [1, 1], LIGHT_LEVELS).view,
|
|
+ shadow_array,
|
|
+ shadow_layers,
|
|
+ shadow_sampler,
|
|
+ shadow_globals: [
|
|
+ globals("HD shadow globals 0"),
|
|
+ globals("HD shadow globals 1"),
|
|
+ ],
|
|
+ main_globals: globals("HD camera globals"),
|
|
+ frames: 0,
|
|
+ depth: target(
|
|
+ device,
|
|
+ "HD depth",
|
|
+ [1, 1],
|
|
+ wgpu::TextureFormat::Depth32Float,
|
|
+ )
|
|
+ .view,
|
|
lit: target(device, "HD lit", [1, 1], HDR).view,
|
|
fx: target(device, "HD post", [1, 1], HDR).view,
|
|
bloom: Vec::new(),
|
|
@@ -395,6 +680,7 @@ impl Hd {
|
|
self.size = size;
|
|
self.albedo = target(device, "HD albedo", size, ALBEDO).view;
|
|
self.normal = target(device, "HD normal", size, NORMAL).view;
|
|
+ self.levels = target(device, "HD light levels", size, LIGHT_LEVELS).view;
|
|
self.depth = target(device, "HD depth", size, wgpu::TextureFormat::Depth32Float).view;
|
|
self.lit = target(device, "HD lit", size, HDR).view;
|
|
self.fx = target(device, "HD post", size, HDR).view;
|
|
@@ -406,20 +692,77 @@ impl Hd {
|
|
.collect();
|
|
}
|
|
|
|
- /// This frame's camera, lights and settings.
|
|
- pub fn write(&mut self, queue: &wgpu::Queue, view_proj: Mat4, lights: &[HdLight], settings: HdSettings) {
|
|
+ /// This frame's camera, sun, fog, lights and settings.
|
|
+ pub fn write(
|
|
+ &mut self,
|
|
+ queue: &wgpu::Queue,
|
|
+ scene: &HdScene,
|
|
+ lights: &[HdLight],
|
|
+ settings: HdSettings,
|
|
+ ) {
|
|
let n = lights.len().min(MAX_LIGHTS);
|
|
if n > 0 {
|
|
queue.write_buffer(&self.lights, 0, bytemuck::cast_slice(&lights[..n]));
|
|
}
|
|
+ self.frames = self.frames.wrapping_add(1);
|
|
+ // The sun by day, the moon (opposite) by night, faded out round the horizon.
|
|
+ let (dir, strength, color) = light_of(scene);
|
|
+ let cascades = cascades(scene.camera, dir);
|
|
+ let night = scene.dark.clamp(0.0, 1.0);
|
|
let frame = HdFrame {
|
|
- inverse_view_proj: view_proj.inverse().to_cols_array_2d(),
|
|
- resolution: [self.size[0] as f32, self.size[1] as f32, 1.0 / self.size[0] as f32, 1.0 / self.size[1] as f32],
|
|
- params: [n as f32, settings.exposure, settings.bloom, 0.0],
|
|
+ inverse_view_proj: scene.view_proj.inverse().to_cols_array_2d(),
|
|
+ resolution: [
|
|
+ self.size[0] as f32,
|
|
+ self.size[1] as f32,
|
|
+ 1.0 / self.size[0] as f32,
|
|
+ 1.0 / self.size[1] as f32,
|
|
+ ],
|
|
+ params: [
|
|
+ n as f32,
|
|
+ settings.exposure,
|
|
+ settings.bloom,
|
|
+ (self.frames % 64) as f32,
|
|
+ ],
|
|
+ cascades: cascades.map(|m| m.to_cols_array_2d()),
|
|
+ sun: [dir.x, dir.y, dir.z, strength],
|
|
+ sun_color: [color.x, color.y, color.z, night],
|
|
+ fog_color: scene.fog_color,
|
|
+ fog: scene.fog,
|
|
+ // In the shade by day a surface keeps half its sky light; at night almost all.
|
|
+ ambient: [0.5 + 0.45 * night, 1.0, 0.0018 + scene.rain * 0.006, 0.0],
|
|
+ shadow: [
|
|
+ if strength > 0.0 { 1.0 } else { 0.0 },
|
|
+ CASCADES[0],
|
|
+ CASCADES[1],
|
|
+ 0.0,
|
|
+ ],
|
|
};
|
|
queue.write_buffer(&self.frame, 0, bytemuck::bytes_of(&frame));
|
|
}
|
|
|
|
+ /// The shadow pass of cascade `c`: its depth layer cleared, nothing else.
|
|
+ pub fn shadow_pass<'e>(
|
|
+ &self,
|
|
+ encoder: &'e mut wgpu::CommandEncoder,
|
|
+ c: usize,
|
|
+ ) -> wgpu::RenderPass<'e> {
|
|
+ encoder
|
|
+ .begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
+ label: Some("HD shadow cascade"),
|
|
+ color_attachments: &[],
|
|
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
|
+ view: &self.shadow_layers[c],
|
|
+ depth_ops: Some(wgpu::Operations {
|
|
+ load: wgpu::LoadOp::Clear(1.0),
|
|
+ store: wgpu::StoreOp::Store,
|
|
+ }),
|
|
+ stencil_ops: None,
|
|
+ }),
|
|
+ ..Default::default()
|
|
+ })
|
|
+ .forget_lifetime()
|
|
+ }
|
|
+
|
|
/// The G-buffer pass's attachments: albedo and normal cleared, its own depth.
|
|
pub fn gbuffer_pass<'e>(&self, encoder: &'e mut wgpu::CommandEncoder) -> wgpu::RenderPass<'e> {
|
|
let clear = |view| {
|
|
@@ -427,23 +770,41 @@ impl Hd {
|
|
view,
|
|
resolve_target: None,
|
|
depth_slice: None,
|
|
- ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
|
|
+ ops: wgpu::Operations {
|
|
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
|
+ store: wgpu::StoreOp::Store,
|
|
+ },
|
|
})
|
|
};
|
|
- encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
- label: Some("HD G-buffer"),
|
|
- color_attachments: &[clear(&self.albedo), clear(&self.normal)],
|
|
- depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
|
- view: &self.depth,
|
|
- depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0.0), store: wgpu::StoreOp::Store }),
|
|
- stencil_ops: None,
|
|
- }),
|
|
- ..Default::default()
|
|
- })
|
|
- .forget_lifetime()
|
|
+ encoder
|
|
+ .begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
+ label: Some("HD G-buffer"),
|
|
+ color_attachments: &[
|
|
+ clear(&self.albedo),
|
|
+ clear(&self.normal),
|
|
+ clear(&self.levels),
|
|
+ ],
|
|
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
|
+ view: &self.depth,
|
|
+ depth_ops: Some(wgpu::Operations {
|
|
+ load: wgpu::LoadOp::Clear(0.0),
|
|
+ store: wgpu::StoreOp::Store,
|
|
+ }),
|
|
+ stencil_ops: None,
|
|
+ }),
|
|
+ ..Default::default()
|
|
+ })
|
|
+ .forget_lifetime()
|
|
}
|
|
|
|
- fn draw(encoder: &mut wgpu::CommandEncoder, label: &str, to: &wgpu::TextureView, load: bool, pipeline: &wgpu::RenderPipeline, group: &wgpu::BindGroup) {
|
|
+ fn draw(
|
|
+ encoder: &mut wgpu::CommandEncoder,
|
|
+ label: &str,
|
|
+ to: &wgpu::TextureView,
|
|
+ load: bool,
|
|
+ pipeline: &wgpu::RenderPipeline,
|
|
+ group: &wgpu::BindGroup,
|
|
+ ) {
|
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
label: Some(label),
|
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
@@ -451,7 +812,11 @@ impl Hd {
|
|
resolve_target: None,
|
|
depth_slice: None,
|
|
ops: wgpu::Operations {
|
|
- load: if load { wgpu::LoadOp::Load } else { wgpu::LoadOp::Clear(wgpu::Color::BLACK) },
|
|
+ load: if load {
|
|
+ wgpu::LoadOp::Load
|
|
+ } else {
|
|
+ wgpu::LoadOp::Clear(wgpu::Color::BLACK)
|
|
+ },
|
|
store: wgpu::StoreOp::Store,
|
|
},
|
|
})],
|
|
@@ -463,21 +828,75 @@ impl Hd {
|
|
}
|
|
|
|
/// Lights the world (its colour and depth) into `lit`.
|
|
- pub fn light(&self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, world: (&wgpu::TextureView, &wgpu::TextureView)) {
|
|
+ pub fn light(
|
|
+ &self,
|
|
+ device: &wgpu::Device,
|
|
+ encoder: &mut wgpu::CommandEncoder,
|
|
+ world: (&wgpu::TextureView, &wgpu::TextureView),
|
|
+ lightmap: (&wgpu::TextureView, &wgpu::Sampler),
|
|
+ ) {
|
|
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("HD lighting"),
|
|
layout: &self.light_layout,
|
|
entries: &[
|
|
- wgpu::BindGroupEntry { binding: 0, resource: self.frame.as_entire_binding() },
|
|
- wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(world.0) },
|
|
- wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(world.1) },
|
|
- wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.albedo) },
|
|
- wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self.normal) },
|
|
- wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&self.depth) },
|
|
- wgpu::BindGroupEntry { binding: 6, resource: self.lights.as_entire_binding() },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 0,
|
|
+ resource: self.frame.as_entire_binding(),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 1,
|
|
+ resource: wgpu::BindingResource::TextureView(world.0),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 2,
|
|
+ resource: wgpu::BindingResource::TextureView(world.1),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 3,
|
|
+ resource: wgpu::BindingResource::TextureView(&self.albedo),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 4,
|
|
+ resource: wgpu::BindingResource::TextureView(&self.normal),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 5,
|
|
+ resource: wgpu::BindingResource::TextureView(&self.depth),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 6,
|
|
+ resource: self.lights.as_entire_binding(),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 7,
|
|
+ resource: wgpu::BindingResource::TextureView(&self.levels),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 8,
|
|
+ resource: wgpu::BindingResource::TextureView(&self.shadow_array),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 9,
|
|
+ resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 10,
|
|
+ resource: wgpu::BindingResource::TextureView(lightmap.0),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 11,
|
|
+ resource: wgpu::BindingResource::Sampler(lightmap.1),
|
|
+ },
|
|
],
|
|
});
|
|
- Self::draw(encoder, "HD lighting", &self.lit, false, &self.light_pipeline, &group);
|
|
+ Self::draw(
|
|
+ encoder,
|
|
+ "HD lighting",
|
|
+ &self.lit,
|
|
+ false,
|
|
+ &self.light_pipeline,
|
|
+ &group,
|
|
+ );
|
|
}
|
|
|
|
fn bloom_group(&self, device: &wgpu::Device, source: &wgpu::TextureView) -> wgpu::BindGroup {
|
|
@@ -485,36 +904,140 @@ impl Hd {
|
|
label: Some("HD bloom"),
|
|
layout: &self.bloom_layout,
|
|
entries: &[
|
|
- wgpu::BindGroupEntry { binding: 0, resource: self.frame.as_entire_binding() },
|
|
- wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(source) },
|
|
- wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.sampler) },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 0,
|
|
+ resource: self.frame.as_entire_binding(),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 1,
|
|
+ resource: wgpu::BindingResource::TextureView(source),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 2,
|
|
+ resource: wgpu::BindingResource::Sampler(&self.sampler),
|
|
+ },
|
|
],
|
|
})
|
|
}
|
|
|
|
/// Bloom from `source` (the lit world or the post pass's output), then the tone curve onto `screen`.
|
|
- pub fn finish(&self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, source: &wgpu::TextureView, screen: &wgpu::TextureView) {
|
|
+ pub fn finish(
|
|
+ &self,
|
|
+ device: &wgpu::Device,
|
|
+ encoder: &mut wgpu::CommandEncoder,
|
|
+ source: &wgpu::TextureView,
|
|
+ screen: &wgpu::TextureView,
|
|
+ ) {
|
|
// Down: the prefilter into the first level, then each level from the one above.
|
|
- Self::draw(encoder, "HD bloom prefilter", &self.bloom[0].view, false, &self.prefilter, &self.bloom_group(device, source));
|
|
+ Self::draw(
|
|
+ encoder,
|
|
+ "HD bloom prefilter",
|
|
+ &self.bloom[0].view,
|
|
+ false,
|
|
+ &self.prefilter,
|
|
+ &self.bloom_group(device, source),
|
|
+ );
|
|
for k in 1..self.bloom.len() {
|
|
let g = self.bloom_group(device, &self.bloom[k - 1].view);
|
|
- Self::draw(encoder, "HD bloom down", &self.bloom[k].view, false, &self.down, &g);
|
|
+ Self::draw(
|
|
+ encoder,
|
|
+ "HD bloom down",
|
|
+ &self.bloom[k].view,
|
|
+ false,
|
|
+ &self.down,
|
|
+ &g,
|
|
+ );
|
|
}
|
|
// Up: each level added onto the bigger one.
|
|
for k in (1..self.bloom.len()).rev() {
|
|
let g = self.bloom_group(device, &self.bloom[k].view);
|
|
- Self::draw(encoder, "HD bloom up", &self.bloom[k - 1].view, true, &self.up, &g);
|
|
+ Self::draw(
|
|
+ encoder,
|
|
+ "HD bloom up",
|
|
+ &self.bloom[k - 1].view,
|
|
+ true,
|
|
+ &self.up,
|
|
+ &g,
|
|
+ );
|
|
}
|
|
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("HD tone mapping"),
|
|
layout: &self.tone_layout,
|
|
entries: &[
|
|
- wgpu::BindGroupEntry { binding: 0, resource: self.frame.as_entire_binding() },
|
|
- wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(source) },
|
|
- wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self.bloom[0].view) },
|
|
- wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self.sampler) },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 0,
|
|
+ resource: self.frame.as_entire_binding(),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 1,
|
|
+ resource: wgpu::BindingResource::TextureView(source),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 2,
|
|
+ resource: wgpu::BindingResource::TextureView(&self.bloom[0].view),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 3,
|
|
+ resource: wgpu::BindingResource::Sampler(&self.sampler),
|
|
+ },
|
|
],
|
|
});
|
|
- Self::draw(encoder, "HD tone mapping", screen, false, &self.tone, &group);
|
|
+ Self::draw(
|
|
+ encoder,
|
|
+ "HD tone mapping",
|
|
+ screen,
|
|
+ false,
|
|
+ &self.tone,
|
|
+ &group,
|
|
+ );
|
|
+ }
|
|
+}
|
|
+
|
|
+/// The light that casts the shadows: towards the sun by day, the moon by night; how strong
|
|
+/// (0 below the horizon or without a sky) and its colour (warm low, white high, cold moon).
|
|
+pub fn light_of(scene: &HdScene) -> (glam::Vec3, f32, glam::Vec3) {
|
|
+ if !scene.sky {
|
|
+ return (glam::Vec3::Y, 0.0, glam::Vec3::ZERO);
|
|
+ }
|
|
+ let sun = scene.sun.normalize_or(glam::Vec3::Y);
|
|
+ let clear = 1.0 - scene.rain * 0.85;
|
|
+ if sun.y > -0.02 {
|
|
+ let up = smooth(-0.02, 0.18, sun.y);
|
|
+ let warm = smooth(0.0, 0.45, sun.y);
|
|
+ let color = glam::Vec3::new(1.0, 0.62, 0.38).lerp(glam::Vec3::new(1.0, 0.95, 0.86), warm);
|
|
+ (sun, 0.6 * up * clear, color)
|
|
+ } else {
|
|
+ let moon = -sun;
|
|
+ let up = smooth(-0.02, 0.18, moon.y);
|
|
+ (moon, 0.15 * up * clear, glam::Vec3::new(0.55, 0.66, 1.0))
|
|
}
|
|
}
|
|
+
|
|
+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)
|
|
+}
|
|
+
|
|
+/// The cascades' light view-projections (camera-relative), each square snapped to its texels in
|
|
+/// the world so the shadows don't crawl as the camera moves.
|
|
+pub fn cascades(camera: glam::DVec3, light: glam::Vec3) -> [Mat4; 2] {
|
|
+ let l = light.normalize_or(glam::Vec3::Y).as_dvec3();
|
|
+ let forward = -l;
|
|
+ let reference = if l.y.abs() > 0.99 {
|
|
+ glam::DVec3::Z
|
|
+ } else {
|
|
+ glam::DVec3::Y
|
|
+ };
|
|
+ let right = forward.cross(reference).normalize();
|
|
+ let up = right.cross(forward);
|
|
+ CASCADES.map(|r| {
|
|
+ let texel = f64::from(r) * 2.0 / f64::from(SHADOW_SIZE);
|
|
+ let snap = |v: f64| (v / texel).floor() * texel - v;
|
|
+ let centre = right * snap(camera.dot(right)) + up * snap(camera.dot(up));
|
|
+ let eye = centre + l * 400.0;
|
|
+ let view = glam::DMat4::look_at_rh(eye, centre, up);
|
|
+ let r = f64::from(r);
|
|
+ let projection = glam::DMat4::orthographic_rh(-r, r, -r, r, 1.0, 800.0);
|
|
+ (projection * view).as_mat4()
|
|
+ })
|
|
+}
|
|
diff --git a/crates/horizon-client/src/gfx/mod_post.rs b/crates/horizon-client/src/gfx/mod_post.rs
|
|
index 497cff854..d1f54a4bb 100644
|
|
--- a/crates/horizon-client/src/gfx/mod_post.rs
|
|
+++ b/crates/horizon-client/src/gfx/mod_post.rs
|
|
@@ -121,7 +121,11 @@ fn compile(source: &str) -> Result<naga::Module, String> {
|
|
let text = e.emit_to_string(&full);
|
|
match e.location(&full) {
|
|
Some(l) if l.line_number as usize > prelude_lines => {
|
|
- format!("line {}: {}", l.line_number as usize - prelude_lines, e.message())
|
|
+ format!(
|
|
+ "line {}: {}",
|
|
+ l.line_number as usize - prelude_lines,
|
|
+ e.message()
|
|
+ )
|
|
}
|
|
_ => text,
|
|
}
|
|
@@ -136,7 +140,11 @@ fn compile(source: &str) -> Result<naga::Module, String> {
|
|
}
|
|
|
|
impl ModPost {
|
|
- pub fn new(device: &wgpu::Device, source: &str, format: wgpu::TextureFormat) -> Result<Self, String> {
|
|
+ pub fn new(
|
|
+ device: &wgpu::Device,
|
|
+ source: &str,
|
|
+ format: wgpu::TextureFormat,
|
|
+ ) -> Result<Self, String> {
|
|
let module = compile(source)?;
|
|
let texture = |binding, sample_type| wgpu::BindGroupLayoutEntry {
|
|
binding,
|
|
@@ -232,11 +240,19 @@ impl ModPost {
|
|
}
|
|
|
|
/// This frame's camera and the mod's `params`, for a world of `size`.
|
|
- pub fn write(&self, queue: &wgpu::Queue, frame: &Frame, size: [u32; 2], params: &[f32], partial: f32) {
|
|
+ pub fn write(
|
|
+ &self,
|
|
+ queue: &wgpu::Queue,
|
|
+ frame: &Frame,
|
|
+ size: [u32; 2],
|
|
+ params: &[f32],
|
|
+ partial: f32,
|
|
+ ) {
|
|
let aspect = size[0] as f32 / size[1].max(1) as f32;
|
|
let view = crate::camera::view_matrix(frame.yaw, frame.pitch);
|
|
- let projection =
|
|
- super::world_projection(frame.fov_degrees, aspect, frame.depth_far) * frame.bob * frame.spin;
|
|
+ let projection = super::world_projection(frame.fov_degrees, aspect, frame.depth_far)
|
|
+ * frame.bob
|
|
+ * frame.spin;
|
|
let a = frame.sky.sun_angle.to_radians();
|
|
let sun = [-a.sin(), a.cos(), 0.0, 0.0];
|
|
let mut p = [[0.0f32; 4]; PARAMS / 4];
|
|
@@ -248,14 +264,24 @@ impl ModPost {
|
|
view_proj: m(projection * view),
|
|
view_inverse: m(view.inverse()),
|
|
projection_inverse: m(projection.inverse()),
|
|
- camera: [frame.camera.x as f32, frame.camera.y as f32, frame.camera.z as f32, 0.0],
|
|
+ camera: [
|
|
+ frame.camera.x as f32,
|
|
+ frame.camera.y as f32,
|
|
+ frame.camera.z as f32,
|
|
+ 0.0,
|
|
+ ],
|
|
resolution: [
|
|
size[0] as f32,
|
|
size[1] as f32,
|
|
1.0 / size[0].max(1) as f32,
|
|
1.0 / size[1].max(1) as f32,
|
|
],
|
|
- time: [self.start.elapsed().as_secs_f32() % 3600.0, partial, sun[1], 0.0],
|
|
+ time: [
|
|
+ self.start.elapsed().as_secs_f32() % 3600.0,
|
|
+ partial,
|
|
+ sun[1],
|
|
+ 0.0,
|
|
+ ],
|
|
sun,
|
|
params: p,
|
|
};
|
|
@@ -279,15 +305,29 @@ impl ModPost {
|
|
label: Some("Mod post"),
|
|
layout: &self.layout,
|
|
entries: &[
|
|
- wgpu::BindGroupEntry { binding: 0, resource: self.buffer.as_entire_binding() },
|
|
- wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(world.0) },
|
|
- wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(world.1) },
|
|
- wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self.sampler) },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 0,
|
|
+ resource: self.buffer.as_entire_binding(),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 1,
|
|
+ resource: wgpu::BindingResource::TextureView(world.0),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 2,
|
|
+ resource: wgpu::BindingResource::TextureView(world.1),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 3,
|
|
+ resource: wgpu::BindingResource::Sampler(&self.sampler),
|
|
+ },
|
|
],
|
|
});
|
|
self.group = Some(((world.0.clone(), world.1.clone()), group));
|
|
}
|
|
- let Some((_, group)) = &self.group else { return };
|
|
+ let Some((_, group)) = &self.group else {
|
|
+ return;
|
|
+ };
|
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
label: Some("Mod post"),
|
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
diff --git a/crates/horizon-client/src/gfx/mod_views.rs b/crates/horizon-client/src/gfx/mod_views.rs
|
|
index c0d886afd..aa2c5b971 100644
|
|
--- a/crates/horizon-client/src/gfx/mod_views.rs
|
|
+++ b/crates/horizon-client/src/gfx/mod_views.rs
|
|
@@ -194,7 +194,8 @@ impl Views {
|
|
views: &'a [ModView],
|
|
globals_size: u64,
|
|
) -> Vec<&'a ModView> {
|
|
- self.targets.retain(|id, _| views.iter().any(|v| v.id == *id));
|
|
+ self.targets
|
|
+ .retain(|id, _| views.iter().any(|v| v.id == *id));
|
|
for v in views {
|
|
if self.targets.get(&v.id).is_none_or(|t| t.size != v.size) {
|
|
let t = self.target(device, v.size, globals_size);
|
|
@@ -318,5 +319,9 @@ pub(super) fn matrices(v: &ModView, eye: DVec3, far: f32) -> (Mat4, Mat4, f32) {
|
|
let view = crate::camera::view_matrix(v.yaw, v.pitch);
|
|
let projection = super::world_projection(v.fov, aspect, far);
|
|
let offset = (eye - v.camera).as_vec3();
|
|
- (projection * view * Mat4::from_translation(offset), view, aspect)
|
|
+ (
|
|
+ projection * view * Mat4::from_translation(offset),
|
|
+ view,
|
|
+ aspect,
|
|
+ )
|
|
}
|
|
diff --git a/crates/horizon-client/src/terrain.wgsl b/crates/horizon-client/src/terrain.wgsl
|
|
index 293729e33..bb5c70f63 100644
|
|
--- a/crates/horizon-client/src/terrain.wgsl
|
|
+++ b/crates/horizon-client/src/terrain.wgsl
|
|
@@ -74,6 +74,8 @@ struct TerrainOut {
|
|
// AirDef fork, HD lighting's G-buffer: the camera-relative position and the unlit colour.
|
|
@location(6) pos: vec3<f32>,
|
|
@location(7) tint: vec4<f32>,
|
|
+ // Sky and block light, 0 to 1.
|
|
+ @location(8) lm: vec2<f32>,
|
|
}
|
|
// Horizon Extras' dynamic lights (dynamic_light.rs): the block light, in the vertex's units
|
|
// (16 per level), a vertex at camera-relative `pos` gets from the frame's light sources, one
|
|
@@ -132,6 +134,7 @@ fn terrain_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f3
|
|
out.cylindrical = max(length(pos.xz), abs(pos.y));
|
|
out.pos = pos;
|
|
out.tint = vec4(color.rgb, 1.0);
|
|
+ out.lm = vec2(f32(light.x), block) / 240.0;
|
|
return out;
|
|
}
|
|
fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
|
|
@@ -159,6 +162,7 @@ fn apply_fog(color: vec4<f32>, spherical: f32, cylindrical: f32) -> vec4<f32> {
|
|
struct GBufferOut {
|
|
@location(0) albedo: vec4<f32>,
|
|
@location(1) normal: vec4<f32>,
|
|
+ @location(2) light: vec4<f32>,
|
|
}
|
|
fn face_normal(pos: vec3<f32>) -> vec3<f32> {
|
|
var n = normalize(cross(dpdx(pos), dpdy(pos)));
|
|
@@ -171,8 +175,14 @@ fn face_normal(pos: vec3<f32>) -> vec3<f32> {
|
|
var out: GBufferOut;
|
|
out.albedo = vec4(color.rgb, f32(in.emission) / 15.0);
|
|
out.normal = vec4(face_normal(in.pos), 1.0);
|
|
+ out.light = vec4(clamp(in.lm, vec2(0.0), vec2(1.0)), 0.0, 1.0);
|
|
return out;
|
|
}
|
|
+// AirDef fork: the sun's shadow map — depth only, cut-out leaves leave holes.
|
|
+@fragment fn fs_shadow(in: TerrainOut) {
|
|
+ let a = sprite(in.uv, in.layer & 0x7fffu).a;
|
|
+ if a < 0.5 && (in.layer & 0x8000u) == 0u { discard; }
|
|
+}
|
|
@fragment fn fs_translucent(in: TerrainOut) -> @location(0) vec4<f32> {
|
|
let color = sprite(in.uv, in.layer & 0x7fffu) * in.color;
|
|
if color.a < 0.01 { discard; }
|
|
--
|
|
2.54.0 (Apple Git-157)
|
|
|