HD fixes: rain level from the world not from rain geometry (#41), TAA history on the jittered grid (#38), vanilla cloud tint without a depth heuristic (#35), air fog once over water (#37), translucent things in front of HD water (#36), cheaper sunrise fan (#39), halos in thick air capped; mods: render.set-time-override (API level 13) so replays show their recorded time of day (#57)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 22:56:15 +03:00
committed by airdef
parent f87d2c518f
commit 0bcc4b56dc
11 changed files with 393 additions and 147 deletions
+2
View File
@@ -20,6 +20,8 @@ struct Globals {
crumbling_sprite: vec4<f32>,
// Selection outline parameters (read by terrain.wgsl).
outline_params: vec4<f32>,
// AirDef fork. x: the HD water draws the water (fs_translucent leaves vanilla's out).
hd_flags: vec4<f32>,
// Horizon Extras' dynamic lights: camera-relative position and level (dynamic_light.rs).
dynamic_lights: array<vec4<f32>, 16>,
}
+5
View File
@@ -141,6 +141,9 @@ pub struct Sky {
pub fog_end: f32,
/// `visual/cloud_fog_end_distance`.
pub cloud_fog_end: f32,
/// AirDef fork: the world's rain level (0..1), set by `apply_weather`. The HD sky reads this
/// rather than the presence of rain geometry near the camera.
pub rain: f32,
}
impl Environment {
pub fn load(root: &Path) -> Result<Self> {
@@ -305,6 +308,7 @@ impl Environment {
.base
.get("minecraft:visual/fog_end_distance")
.map_or(1024.0, |v| v[0]),
rain: 0.0,
cloud_fog_end: self
.base
.get("minecraft:visual/cloud_fog_end_distance")
@@ -315,6 +319,7 @@ impl Environment {
/// WeatherAttributes: rain then thunder layers lerped by their levels
/// (rain counts only the part not covered by thunder).
pub fn apply_weather(sky: &mut Sky, rain: f32, thunder: f32) {
sky.rain = rain.clamp(0.0, 1.0);
let grey = |c: [f32; 3]| c[0] * 0.3 + c[1] * 0.59 + c[2] * 0.11;
let lerp3 = |t: f32, a: [f32; 3], b: [f32; 3]| -> [f32; 3] {
std::array::from_fn(|i| a[i] + (b[i] - a[i]) * t)
+13 -5
View File
@@ -8370,6 +8370,14 @@ impl Game<'_> {
.collect()
}
/// The time of day the player sees: a mod's override (a replay's recorded time), else the world's.
fn shown_day_time(&self) -> i64 {
self.mods
.mods
.time_override()
.map_or(self.world.day_time, |t| t as i64)
}
fn sky_darken(&self) -> i32 {
if self.dim == Dim::Nether {
return 11;
@@ -8378,12 +8386,12 @@ impl Game<'_> {
let sky = self
.res
.end_environment()
.sample(self.world.day_time, None, None);
.sample(self.shown_day_time(), None, None);
return (15.0 - 15.0 * sky.sky_light_level).round() as i32;
}
let (rain, thunder) = self.world.weather.levels(1.0);
// Sampled once per (time, weather): every mob asks each tick.
let key = (self.world.day_time, rain.to_bits(), thunder.to_bits());
let key = (self.shown_day_time(), rain.to_bits(), thunder.to_bits());
if let Some((k, darken)) = self.sky_darken_memo.get()
&& k == key
{
@@ -8392,7 +8400,7 @@ impl Game<'_> {
let mut sky = self
.res
.environment()
.sample(self.world.day_time, None, None);
.sample(self.shown_day_time(), None, None);
horizon_client::environment::apply_weather(&mut sky, rain, thunder);
// Level.updateSkyBrightness: darkening from the sky light level.
let darken = (15.0 - 15.0 * sky.sky_light_level).round() as i32;
@@ -8898,7 +8906,7 @@ impl Game<'_> {
"first_quarter",
"waxing_gibbous",
];
let phase = phases[(self.world.day_time / 24000).rem_euclid(8) as usize];
let phase = phases[(self.shown_day_time() / 24000).rem_euclid(8) as usize];
body(
&format!("environment/celestial/moon/{phase}"),
sky.moon_angle,
@@ -11608,7 +11616,7 @@ impl Game<'_> {
);
// visual/water_fog_color: no dimension sets it, its default is #050533.
let water_fog = blend(|b| Some(b.water_fog), [0.0; 3]);
let mut sky = environment.sample(self.world.day_time, Some(biome_sky), Some(biome_fog));
let mut sky = environment.sample(self.shown_day_time(), Some(biome_sky), Some(biome_fog));
let (rain, thunder) = if self.dim == Dim::Overworld {
self.world.weather.levels(alpha as f32)
} else {
+42 -10
View File
@@ -194,6 +194,9 @@ struct Globals {
crumbling_sprite: [f32; 4],
/// Framebuffer size, selection stroke width, perspective layering scale.
outline_params: [f32; 4],
/// AirDef fork. x: 1 while the HD water draws the water (fs_translucent then leaves vanilla's
/// water out of the colour and keeps its depth).
hd_flags: [f32; 4],
/// Horizon Extras' dynamic lights: camera-relative position and level (`dynamic_light`).
dynamic_lights: [[f32; 4]; crate::dynamic_light::MAX],
}
@@ -578,7 +581,7 @@ pub struct Renderer {
hd_far: Option<(Mat4, glam::DVec3, Vec3, u32)>,
/// HD's TAA: frames jittered so far, and the last one's unjittered view-projection and camera.
taa_frame: u32,
taa_prev: Option<(Mat4, glam::DVec3)>,
taa_prev: Option<(Mat4, glam::DVec3, Mat4)>,
hd: Option<hd::Hd>,
mod_params: (Vec<f32>, f32),
/// Time the shader pack's passes on the GPU (F3, or `HORIZON_SHADER_PACK_TIMING`).
@@ -1207,17 +1210,15 @@ impl Renderer {
if sprite_arrays > 1 {
log::info!("Horizon: {layers} block textures in {sprite_arrays} arrays");
}
// AirDef fork, HD water: the sprite layers terrain.wgsl's fs_water takes for water (and
// fs_translucent clears while the HD water draws it instead).
let sprite_constants = [
("SPRITE_ARRAYS", f64::from(sprite_arrays)),
("SPRITE_SPLIT", f64::from(split)),
];
// AirDef fork, HD water: the sprite layers terrain.wgsl's fs_water takes for water.
let water_constants = [
("SPRITE_ARRAYS", f64::from(sprite_arrays)),
("SPRITE_SPLIT", f64::from(split)),
("WATER_STILL", f64::from(water_layers[0])),
("WATER_FLOW", f64::from(water_layers[1])),
];
let water_constants = sprite_constants;
let bindings = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Horizon terrain bindings"),
entries: &[
@@ -2823,7 +2824,7 @@ impl Renderer {
mip_levels,
mipmaps: mip_levels - 1,
terrain_layout: bindings,
terrain_sprites: sprite_constants.map(|(_, v)| v),
terrain_sprites: [f64::from(sprite_arrays), f64::from(split)],
sprite_sampler,
lightmap_view,
lightmap_sampler,
@@ -4049,7 +4050,7 @@ impl Renderer {
};
let taa_prev = std::mem::replace(
&mut self.taa_prev,
taa.then_some((unjittered, frame.camera)),
taa.then_some((unjittered, frame.camera, view_proj)),
);
if frame.title {
self.panorama.prepare(
@@ -4135,6 +4136,7 @@ impl Renderer {
frame.glint_ticks,
],
outline_params: outline_params(self.config.width, self.config.height),
hd_flags: [0.0; 4],
dynamic_lights: {
let mut lights = [[0.0; 4]; crate::dynamic_light::MAX];
for (to, from) in lights.iter_mut().zip(frame.dynamic_lights) {
@@ -4260,11 +4262,14 @@ impl Renderer {
taa: taa && taa_prev.is_some(),
jitter_px,
prev_view_proj: taa_prev.map_or(unjittered, |p| p.0),
prev_view_proj_jittered: taa_prev.map_or(view_proj, |p| p.2),
camera_delta: taa_prev.map_or(Vec3::ZERO, |p| (frame.camera - p.1).as_vec3()),
// 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 },
// The world's rain level (fades in and out with the weather), not the rain geometry
// near the camera: hiding the particles or standing under a roof leaves the sky alone.
rain: frame.sky.rain.clamp(0.0, 1.0) * 0.7,
fog_color,
fog,
sky: frame.skybox && !frame.end_sky && !frame.nether_lighting && !frame.underwater,
@@ -4827,7 +4832,20 @@ impl Renderer {
+ shadow_quads
+ translucent_particle_quads
+ translucent_item_quads;
let cloud_quads = if let Some(input) = &frame.clouds {
// AirDef fork: vanilla's clouds (HD's volumetric ones off) take the low sun's colour in their
// own colour, so a mountain or glass behind them does not change it.
let cloud_input = frame.clouds.as_ref().map(|c| {
if !hd_wanted || !hd_scene.sky || hd::clouds_enabled() {
return crate::clouds::Input { ..*c };
}
let [r, g, b, a] = c.color;
let [tr, tg, tb] = hd::vanilla_cloud_tint(&hd_scene);
crate::clouds::Input {
color: [r * tr, g * tg, b * tb, a],
..*c
}
});
let cloud_quads = if let Some(input) = &cloud_input {
let (rebuilt, uniform) =
crate::spans::time("clouds", || self.cloud_mesh.prepare(input));
let vertices = self.cloud_mesh.vertices.len() as u64;
@@ -5154,6 +5172,15 @@ impl Renderer {
if let Some(settings) = frame.hd {
hd.write(&self.queue, &hd_scene, frame.hd_lights, settings);
}
// vanilla's translucent water leaves the colour while the HD water draws it.
let hd_flags_at = std::mem::offset_of!(Globals, hd_flags) as u64;
let hd_flags: [f32; 4] = [if hd.water_active() { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0];
self.uploads.buffer(
&mut encoder,
&self.globals,
hd_flags_at,
bytemuck::bytes_of(&hd_flags),
);
hd.set_timing(device, &self.queue, self.pass_timing);
hd.timing_begin(device, &self.queue);
// The sun's (or moon's) shadow cascades: the world's globals swapped for the light's.
@@ -5184,6 +5211,11 @@ impl Renderer {
}
self.queue
.write_buffer(&hd.main_globals, 0, bytemuck::bytes_of(&globals));
self.queue.write_buffer(
&hd.main_globals,
hd_flags_at,
bytemuck::bytes_of(&hd_flags),
);
// Facing the light only: the nearest surface to it is always one that does.
let toward = (hd_shadow_fast() && self.sides_on).then(|| shadow_dir * 1.0e5);
for (c, cascade) in hd_cascades.iter().enumerate() {
+293 -130
View File
@@ -133,6 +133,10 @@ pub struct HdScene {
pub view_proj: Mat4,
/// Last frame's view-projection without jitter, and the camera's move since (TAA).
pub prev_view_proj: Mat4,
/// The same frame's matrix as it was drawn, jitter included. History kept on the jittered
/// pixel grid (`fs_taa`, scale 1) is looked up with this; `fs_taau` resolves the history on the
/// unjittered output grid and uses `prev_view_proj`.
pub prev_view_proj_jittered: Mat4,
pub camera_delta: glam::Vec3,
/// This frame is anti-aliased over time (the projection is jittered).
pub taa: bool,
@@ -403,31 +407,104 @@ fn fog_at(px: vec2<i32>, dist: f32) -> vec4<f32> {
return sum / total;
}
@fragment
fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
let px = vec2<i32>(in.position.xy);
let scene = textureLoad(scene_color, px, 0).rgb;
let d = textureLoad(scene_depth, px, 0);
let g = textureLoad(gbuffer_depth, px, 0);
// Water in front of the opaque world with nothing translucent over it: here the floor is lit
// and the water composite lays the surface over it (it knows the pixel by the alpha 0.5).
var water = false;
if frame.water.x > 0.5 && g > 0.0 {
let wd = textureLoad(water_depth, px, 0);
water = wd > 0.0 && wd >= g && d - wd <= d * 0.002;
}
let dd = select(d, g, water);
let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(dd, 0.000001), 1.0);
let h = frame.inverse_view_proj * ndc;
let p = h.xyz / h.w;
let view = normalize(p);
struct LightOut {
// The lit colour; alpha 0.5 marks water (the floor under it, lit without any air), 0.6 an
// object in front of the water (the scene as drawn).
@location(0) lit: vec4<f32>,
// Water: the air between the camera and the surface (scattered light, transmittance);
// in front of the water: the floor lit (rgb); else nothing.
@location(1) air: vec4<f32>,
}
// An opaque surface lit: the ambient, the sun and the mods' lights over its albedo.
fn shade_surface(px: vec2<i32>, p: vec3<f32>, water: bool) -> vec3<f32> {
let sun = frame.sun.xyz;
let sun_on = frame.sun.w;
let atmosphere = frame.sky_sun.w > 0.5;
let count = u32(frame.params.x);
let tile = ((u32(px.y) >> 4u) * u32(frame.shadow.w) + (u32(px.x) >> 4u)) * 4u;
let surface_start = tiles[tile];
let surface_all = tiles[tile + 1u] == 0xffffffffu;
let surface_count = select(tiles[tile + 1u], count, surface_all);
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_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;
// Contact shadows from the ambient occlusion: on the ambient and blocks' light fully, on
// the mods' lights by half, not on the sun or on emission.
var occlusion = 1.0;
// (The water dims and blurs the floor under it: no occlusion there.)
if frame.ambient.w > 0.0 && !water { occlusion = ao_at(px, length(p)); }
let ao_ambient = mix(1.0, occlusion, frame.ambient.w);
let ao_lights = mix(1.0, occlusion, frame.ambient.w * 0.5);
// The sky's light is bluish from above and carries the horizon's colour from the sides;
// faces turned up take more of it than the ones turned down.
var sky_tint = vec3<f32>(1.0);
if atmosphere {
let reach = smoothstep(0.25, 0.9, lm.x);
let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
let shape = mix(0.66, 1.1, smoothstep(0.1, 1.0, up));
let hue = mix(vec3<f32>(1.0), sky_light_tint(n), 0.4 * (1.0 - 0.5 * frame.sun_color.a));
sky_tint = mix(vec3<f32>(1.0), hue * shape, reach);
}
var light = max(base * sky_tint * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
// The mods' lights.
for (var k = 0u; k < surface_count; k++) {
let l = lights[select(tiles[surface_start + k], k, surface_all)];
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 * ao_lights;
}
return albedo.rgb * light + albedo.rgb * albedo.a * 1.5;
}
@fragment
fn fs_light(in: FullOut) -> LightOut {
let px = vec2<i32>(in.position.xy);
let scene = textureLoad(scene_color, px, 0).rgb;
let d = textureLoad(scene_depth, px, 0);
let g = textureLoad(gbuffer_depth, px, 0);
// Water over the opaque world (or over nothing): the water composite lays the surface over
// the floor, which is lit here with no air. Something translucent in front of the water
// (`front`: nearer than its surface) keeps the scene as drawn; the floor goes beside it.
var wd = 0.0;
var water_here = false;
if frame.water.x > 0.5 {
wd = textureLoad(water_depth, px, 0);
water_here = wd > 0.0 && (g <= 0.0 || wd >= g);
}
let front = water_here && g > 0.0 && d - wd > d * 0.002;
let water = water_here && !front;
let has_floor = water && g > 0.0;
let dd = select(d, g, has_floor);
let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(dd, 0.000001), 1.0);
let h = frame.inverse_view_proj * ndc;
let p = h.xyz / h.w;
let view = normalize(p);
// Where the air ends: the water's surface over water, else the pixel itself.
var ps = 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(wd, 0.000001), 1.0);
ps = hf.xyz / hf.w;
}
let view_s = normalize(ps);
let sun = frame.sun.xyz;
let sun_on = frame.sun.w;
let count = u32(frame.params.x);
let tile = ((u32(px.y) >> 4u) * u32(frame.shadow.w) + (u32(px.x) >> 4u)) * 4u;
let halo_start = tiles[tile + 2u];
let halo_all = tiles[tile + 3u] == 0xffffffffu;
let halo_count = select(tiles[tile + 3u], count, halo_all);
@@ -455,113 +532,81 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
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 && 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));
}
let surface = water || (d > 0.0 && abs(d - g) <= max(d, g) * 0.002);
// (Vanilla's clouds, when the volumetric ones are off, take the low sun's colour in their own
// pass: `vanilla_cloud_tint`, applied to their colour on the CPU.)
let surface = has_floor || (!water_here && 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_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;
// Contact shadows from the ambient occlusion: on the ambient and blocks' light fully, on
// the mods' lights by half, not on the sun or on emission.
var occlusion = 1.0;
// (The water dims and blurs the floor under it: no occlusion there.)
if frame.ambient.w > 0.0 && !water { occlusion = ao_at(px, length(p)); }
let ao_ambient = mix(1.0, occlusion, frame.ambient.w);
let ao_lights = mix(1.0, occlusion, frame.ambient.w * 0.5);
// The sky's light is bluish from above and carries the horizon's colour from the sides;
// faces turned up take more of it than the ones turned down.
var sky_tint = vec3<f32>(1.0);
if atmosphere {
let reach = smoothstep(0.25, 0.9, lm.x);
let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
let shape = mix(0.66, 1.1, smoothstep(0.1, 1.0, up));
let hue = mix(vec3<f32>(1.0), sky_light_tint(n), 0.4 * (1.0 - 0.5 * frame.sun_color.a));
sky_tint = mix(vec3<f32>(1.0), hue * shape, reach);
}
var light = max(base * sky_tint * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
// The mods' lights.
for (var k = 0u; k < surface_count; k++) {
let l = lights[select(tiles[surface_start + k], k, surface_all)];
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 * ao_lights;
}
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));
let haze = haze_colour(view);
c = mix(c, haze, f * frame.fog_color.a);
// Aerial perspective: far things take the horizon's colour a little before the fog does.
if atmosphere && frame.fog.x > 1.0e6 {
c = mix(c, haze, (1.0 - exp(-sph / 700.0)) * 0.5);
c = shade_surface(px, p, water);
// Vanilla's fog over it again (the water composite does it at the surface).
if !water {
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));
let haze = haze_colour(view);
c = mix(c, haze, f * frame.fog_color.a);
// Aerial perspective: far things take the horizon's colour a little before the fog does.
if atmosphere && frame.fog.x > 1.0e6 {
c = mix(c, haze, (1.0 - exp(-sph / 700.0)) * 0.5);
}
}
}
// 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);
// What lies behind a translucent thing in front of the water: the floor, lit.
var floor_lit = vec3<f32>(0.0);
if front {
let hg = frame.inverse_view_proj * vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(g, 0.000001), 1.0);
floor_lit = shade_surface(px, hg.xyz / hg.w, true);
}
// The air between the camera and the pixel (the water's surface over water): the volumetric
// fog (what it hides, what it scatters), the sun's rays between the shadows, and the halos
// round the mods' lights. Over water they go to the composite, which lays them over the
// finished water; elsewhere over the pixel here.
var air_t = 1.0;
var air_s = vec3<f32>(0.0);
let far = select(min(length(ps), frame.shadow.z), frame.shadow.z, d <= 0.0);
let density = frame.ambient.z;
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);
}
if d > 0.0 { fog_len = min(length(ps), 192.0); }
let fg = fog_at(px, fog_len);
c = c * fg.a + fg.rgb;
air_t = fg.a;
air_s = 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;
for (var i = 0; i < steps; i++) {
let t = (f32(i) + jitter) / f32(steps) * far;
acc += lit_at(view * t);
acc += lit_at(view_s * t);
}
// Henyey-Greenstein, forward scattering: brightest looking towards the sun.
let cos_t = dot(view, sun);
let cos_t = dot(view_s, 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));
air_s += frame.sun_color.rgb * sun_on * (acc / f32(steps)) * phase * (1.0 - exp(-far * density));
}
for (var k = 0u; k < halo_count; k++) {
let l = lights[select(tiles[halo_start + k], k, halo_all)];
// 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;
let t0 = dot(l.pos.xyz, view_s);
let hh = length(l.pos.xyz - view_s * 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);
// Thicker air glows more round a light, but only so far: the mist at dusk and many fires
// at once would otherwise wash the picture out.
air_s += l.color.rgb * l.color.w * (a1 - a0) / hh * 0.004 * (0.3 + min(density * 300.0, 0.9));
}
return vec4<f32>(c, select(1.0, 0.5, water));
var out: LightOut;
if water {
out.lit = vec4<f32>(c, 0.5);
out.air = vec4<f32>(air_s, air_t);
} else {
out.lit = vec4<f32>(c * air_t + air_s, select(1.0, 0.6, front));
out.air = vec4<f32>(floor_lit, 0.0);
}
return out;
}
"#;
@@ -580,6 +625,9 @@ const REFLECT_RANGE: f32 = 96.0;
@group(0) @binding(8) var lightmap: texture_2d<f32>;
@group(0) @binding(9) var linear: sampler;
//SKY10
// What the lighting left beside the lit world: over water the air in front of the surface (scattered
// light, transmittance), in front of the water the lit floor.
@group(0) @binding(11) var air_tex: texture_2d<f32>;
fn ign(px: vec2<f32>) -> f32 {
let p = px + vec2<f32>(5.588238 * frame.params.w);
@@ -761,6 +809,9 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
let px = vec2<i32>(in.position.xy);
let base = textureLoad(lit_tex, px, 0);
if base.a > 0.75 { return vec4<f32>(base.rgb, 1.0); }
// Alpha 0.6: something translucent stands in front of the water here (base is the scene).
let front = base.a > 0.55;
let aux = textureLoad(air_tex, px, 0);
let wd = textureLoad(water_depth, px, 0);
let g = textureLoad(gbuffer_depth, px, 0);
let tint = textureLoad(water_tint, px, 0);
@@ -792,23 +843,35 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
let volume_light = max(ambient * frame.ambient.x, block * frame.ambient.y) + frame.sun_color.rgb * sun_on * sunlit * 0.8;
// Refraction: where the ray through the waves meets the world, against where the flat surface
// would send it (so still water looks as vanilla's and only the waves distort).
let floor0 = position_at(in.uv, g);
let depth0 = max(length(floor0) - dist, 0.0);
let bent = refract(-v, n, 0.75) - refract(-v, geometric, 0.75);
// would send it (so still water looks as vanilla's and only the waves distort). Not where
// something translucent is in front of the water, nor onto texels that hold such a thing.
let has_floor = g > 0.0;
var uv_floor = in.uv;
var floor_z = g;
let moved = project(floor0 + bent * min(depth0, 24.0));
if moved.z > 0.0 && on_screen(moved.xy) {
let z = textureLoad(gbuffer_depth, texel(moved.xy), 0);
// Only what lies behind the water: not the bank or an entity in front of it.
if z > 0.0 && z <= wd {
uv_floor = moved.xy;
floor_z = z;
if has_floor && !front {
let floor0 = position_at(in.uv, g);
let depth0 = max(length(floor0) - dist, 0.0);
let bent = refract(-v, n, 0.75) - refract(-v, geometric, 0.75);
let moved = project(floor0 + bent * min(depth0, 24.0));
if moved.z > 0.0 && on_screen(moved.xy) {
let z = textureLoad(gbuffer_depth, texel(moved.xy), 0);
// The four texels the filter blends: none may be a pixel with a translucent thing
// in front of the water.
let marks = textureGather(3, lit_tex, linear, moved.xy);
let tainted = any((marks > vec4<f32>(0.55)) & (marks < vec4<f32>(0.75)));
// Only what lies behind the water: not the bank or an entity in front of it.
if z > 0.0 && z <= wd && !tainted {
uv_floor = moved.xy;
floor_z = z;
}
}
}
let thickness = clamp(length(position_at(uv_floor, floor_z)) - dist, 0.0, 48.0);
let behind = textureSampleLevel(lit_tex, linear, uv_floor, 0.0).rgb;
var thickness = 48.0;
if has_floor {
thickness = clamp(length(position_at(uv_floor, floor_z)) - dist, 0.0, 48.0);
}
var behind = textureSampleLevel(lit_tex, linear, uv_floor, 0.0).rgb;
if front { behind = aux.rgb; }
// A deep, dark water: the biome's colour darkened and a little greyed, so it reads as
// depth rather than as bright paint.
let biome = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
@@ -840,6 +903,13 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
if frame.sky_sun.w > 0.5 && frame.fog.x > 1.0e6 {
c = mix(c, haze, (1.0 - exp(-dist / 700.0)) * 0.5);
}
if front {
// The scene as drawn, with the water's change to the floor laid under the translucent thing.
c = base.rgb + (c - aux.rgb) * 0.7;
} else {
// The air between the camera and the surface: volumetric fog, rays, halos.
c = c * aux.a + aux.rgb;
}
return vec4<f32>(max(c, vec3<f32>(0.0)), 1.0);
}
"#;
@@ -1413,6 +1483,8 @@ pub struct Hd {
water_normal: wgpu::TextureView,
water_depth: wgpu::TextureView,
lit_water: wgpu::TextureView,
/// The lighting's second target: the air over water, the floor beside translucent things.
air: wgpu::TextureView,
water_layout: wgpu::BindGroupLayout,
water_pipeline: wgpu::RenderPipeline,
water_group: Option<((wgpu::TextureView, wgpu::Sampler), wgpu::BindGroup)>,
@@ -1849,6 +1921,7 @@ impl Hd {
FLOAT,
SAMPLER,
FLOAT,
UNFILTERED,
]),
});
let sky_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@@ -1961,15 +2034,39 @@ impl Hd {
},
alpha: wgpu::BlendComponent::REPLACE,
};
let light_pipeline = fullscreen(
device,
"HD lighting",
&light_layout,
&light,
"fs_light",
HDR,
None,
);
let light_pipeline = {
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("HD lighting"),
bind_group_layouts: &[Some(&light_layout)],
..Default::default()
});
let target = Some(wgpu::ColorTargetState {
format: HDR,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("HD lighting"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &light,
entry_point: Some("vs_full"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &light,
entry_point: Some("fs_light"),
compilation_options: Default::default(),
targets: &[target.clone(), target],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
})
};
let prefilter = fullscreen(
device,
"HD bloom prefilter",
@@ -2108,6 +2205,7 @@ impl Hd {
)
.view,
lit: target(device, "HD lit", [1, 1], HDR).view,
air: target(device, "HD air", [1, 1], HDR).view,
fx: target(device, "HD post", [1, 1], HDR).view,
water_tint: target(device, "HD water colour", [1, 1], WATER_TINT).view,
water_normal: target(device, "HD water normal", [1, 1], WATER_NORMAL).view,
@@ -2199,6 +2297,7 @@ impl Hd {
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.air = target(device, "HD air", size, HDR).view;
self.fx = target(device, "HD post", size, HDR).view;
self.water_tint = target(device, "HD water colour", size, WATER_TINT).view;
self.water_normal = target(device, "HD water normal", size, WATER_NORMAL).view;
@@ -2322,7 +2421,12 @@ impl Hd {
0.0
};
let frame = HdFrame {
prev_view_proj: scene.prev_view_proj.to_cols_array_2d(),
prev_view_proj: if self.upscaling() {
scene.prev_view_proj
} else {
scene.prev_view_proj_jittered
}
.to_cols_array_2d(),
taa: [
scene.camera_delta.x,
scene.camera_delta.y,
@@ -2685,15 +2789,28 @@ impl Hd {
return;
};
self.water_composited = false;
Self::draw(
encoder,
"HD lighting",
&self.lit,
false,
&self.light_pipeline,
group,
stamp(&mut self.timing, "hd lighting"),
);
let clear = |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,
},
})
};
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("HD lighting"),
timestamp_writes: stamp(&mut self.timing, "hd lighting"),
color_attachments: &[clear(&self.lit), clear(&self.air)],
..Default::default()
});
pass.set_pipeline(&self.light_pipeline);
pass.set_bind_group(0, group, &[]);
pass.draw(0..3, 0..1);
}
}
/// A bind group of consecutive bindings 0.., counted (`groups_last_frame`).
@@ -2987,6 +3104,7 @@ impl Hd {
wgpu::BindingResource::TextureView(lightmap.0),
wgpu::BindingResource::Sampler(&self.sampler),
wgpu::BindingResource::TextureView(&self.sky_lut),
wgpu::BindingResource::TextureView(&self.air),
],
);
self.water_group = Some((key, group));
@@ -3098,6 +3216,20 @@ pub fn light_of(scene: &HdScene) -> (glam::Vec3, f32, glam::Vec3) {
}
}
/// The factor on vanilla's cloud colour that lights the clouds with the low sun's colour at dusk
/// and dawn instead of a flat white. It is the clouds' own colour (the cloud pass gets it), not a
/// guess from the depth buffer, so what stands behind a cloud cannot change it.
pub fn vanilla_cloud_tint(scene: &HdScene) -> [f32; 3] {
let sun = scene.sun.normalize_or(glam::Vec3::Y);
let low = 1.0 - smooth(0.05, 0.5, sun.y);
let overcast = (scene.rain / 0.7).clamp(0.0, 1.0) * 0.85;
let (_, _, color) = light_of(scene);
let warm = color.lerp(glam::Vec3::ONE, 0.45) * 1.1;
let t = low * 0.7 * (1.0 - overcast);
let tint = glam::Vec3::ONE.lerp(warm, t);
[tint.x, tint.y, tint.z]
}
/// 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
@@ -3201,6 +3333,37 @@ mod tests {
assert_eq!(seen.len(), 16);
}
#[test]
fn taa_history_lookup_follows_the_jittered_grid() {
// A static point under two frames' different Halton jitters: fs_taa finds this frame's
// point in the history by the previous frame's matrix *as drawn*, which lands on the pixel
// the previous frame wrote it to; the unjittered one is off by that frame's jitter.
let size = [1920u32, 1080];
let unjittered = Mat4::perspective_rh(1.0, 16.0 / 9.0, 0.05, 1000.0);
let to_uv = |m: Mat4, p: glam::Vec3| {
let c = m * p.extend(1.0);
[c.x / c.w * 0.5 + 0.5, 0.5 - c.y / c.w * 0.5]
};
let drawn = |frame: u32| {
let j = jitter(frame, size, 16);
Mat4::from_translation(glam::Vec3::new(j[0], j[1], 0.0)) * unjittered
};
let p = glam::Vec3::new(0.0, 0.0, -20.0);
for frame in 1..16 {
let prev = drawn(frame - 1);
// The pixel (in uv) the previous frame drew the point at: what the history holds there.
let wrote = to_uv(prev, p);
let old = to_uv(unjittered, p);
let err = [
(old[0] - wrote[0]).abs() * size[0] as f32,
(old[1] - wrote[1]).abs() * size[1] as f32,
];
let pj = jitter(frame - 1, size, 16);
assert!((err[0] - pj[0].abs() * size[0] as f32 * 0.5).abs() < 1e-3);
assert!((err[1] - pj[1].abs() * size[1] as f32 * 0.5).abs() < 1e-3);
}
}
#[test]
fn jitter_covers_the_upscale_ratio() {
assert_eq!(jitter_count(1.0), 16);
+8
View File
@@ -283,6 +283,14 @@ fn vanilla_sunrise_weight(d: vec3<f32>) -> f32 {
let theta = radians(flip + 90.0);
let c = cos(theta);
let s = sin(theta);
// Above the horizon the fan reaches only the sun's side (x of the same sign as the apex's, -s)
// and rises at most alpha/3 per block of that distance (its rim stands 40*alpha high over a
// 120-block radius and the points above the horizon lie at least 120 * the share of the rim
// from the axis); anything else cannot hit it, so most of the sky skips the 16 triangles.
if d.y > 0.0 {
let side = -s * d.x;
if side <= 0.0 || d.y > alpha * (1.0 / 3.0) * side * 1.02 + 0.001 { return 0.0; }
}
var best = 0.0;
let apex = vanilla_rotate_x90(vec3<f32>(-100.0 * s, 100.0 * c, 0.0));
for (var i = 0; i < 16; i++) {
+8
View File
@@ -22,6 +22,8 @@ struct Globals {
// Atlas rect of the current destroy stage (read by entity.wgsl).
crumbling_sprite: vec4<f32>,
outline_params: vec4<f32>,
// AirDef fork. x: the HD water draws the water (fs_translucent leaves vanilla's out).
hd_flags: vec4<f32>,
// Horizon Extras' dynamic lights: camera-relative position and level (dynamic_light.rs).
dynamic_lights: array<vec4<f32>, 16>,
}
@@ -211,6 +213,12 @@ struct WaterOut {
@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; }
// The HD water composite draws the water itself: vanilla's stays out of the colour (alpha 0
// blends to nothing) but keeps its depth, which the lighting reads.
let layer = in.layer & 0x7fffu;
if globals.hd_flags.x > 0.5 && (layer == WATER_STILL || layer == WATER_FLOW) {
return vec4(0.0);
}
return apply_fog(color, in.spherical, in.cylindrical);
}
+5
View File
@@ -694,6 +694,11 @@ interface render {
/// Switches the HD lighting on or off for this mod's world, with its
/// exposure and bloom; off, the world is drawn as the game draws it.
set-lighting: func(lighting: lighting);
/// Shows the world at this time of day (AirDef fork): the sky, the sun
/// and moon, the sky light and the fog, as `day-time` ticks (24000 a day,
/// as `clock`). Only what the player sees changes: the world's own clock,
/// its saves and its rules do not. None shows the world's own time.
set-time-override: func(day-time: option<u64>);
}
/// What the game calls. Only the hooks `init` asks for are ever called.
+4 -2
View File
@@ -7,13 +7,13 @@
//! version's level so the launcher can tell before it installs.
/// The newest level this Horizon serves.
pub const LEVEL: u32 = 12;
pub const LEVEL: u32 = 13;
/// Every function the game offers mods, as `interface.function`, with its
/// level. Level 0 is the contract of Horizon 0.2.67 to 0.2.69. A new
/// function takes the next level and raises [`LEVEL`] (the `api_reference`
/// test fails until it is listed here).
pub const FUNCTIONS: [(&str, u32); 66] = [
pub const FUNCTIONS: [(&str, u32); 67] = [
("log.write", 0),
("game.get-player", 0),
("game.get-clock", 0),
@@ -82,6 +82,8 @@ pub const FUNCTIONS: [(&str, u32); 66] = [
("render.query-sections", 12),
("render.sections", 12),
("render.world-changes", 12),
// AirDef fork: a replay shows its recorded time of day.
("render.set-time-override", 13),
];
/// What the game's program carries so the launcher can read its level
+12
View File
@@ -507,6 +507,8 @@ struct State {
/// `render.set-lights` and `render.set-lighting` (AirDef fork).
lights: Vec<PointLight>,
lighting: Option<ModLighting>,
/// `render.set-time-override` (AirDef fork).
time_override: Option<u64>,
input: InputState,
browser: BrowserState,
/// Capability `storage`'s folder; made on first `set`.
@@ -749,6 +751,7 @@ impl Mods {
views: Vec::new(),
lights: Vec::new(),
lighting: None,
time_override: None,
input: InputState::default(),
browser: BrowserState::default(),
data_dir,
@@ -1334,6 +1337,12 @@ impl Mods {
.iter()
.find_map(|e| e.run.as_ref()?.store.data().lighting)
}
/// The time of day the first running mod asked to show, if any.
pub fn time_override(&self) -> Option<u64> {
self.entries
.iter()
.find_map(|e| e.run.as_ref()?.store.data().time_override)
}
/// The columns each running mod asked the ground height of, taken.
pub fn take_ground_queries(&mut self) -> Vec<(usize, Vec<(i32, i32)>)> {
self.entries
@@ -2237,6 +2246,9 @@ impl render::Host for State {
};
self.lighting = Some(lighting);
}
fn set_time_override(&mut self, day_time: Option<u64>) {
self.time_override = day_time.map(|t| t.min(1 << 40));
}
fn query_ground(&mut self, columns: Vec<(i32, i32)>) {
let room = 512usize.saturating_sub(self.ground_queries.len());
self.ground_queries.extend(columns.into_iter().take(room));
+1
View File
@@ -646,6 +646,7 @@ The HD lighting (AirDef fork): the world lit per pixel by the mods' lights, in h
| `set-views` | `func(views: list<camera-view>)` | Replaces the mod's cameras: up to 8. A `view` shape shows one; the game renders up to three a frame, in turn. |
| `set-lights` | `func(lights: list<point-light>)` | Replaces the mod's lights: up to 512, interpolated per id between the last two calls. |
| `set-lighting` | `func(lighting: lighting)` | Switches the HD lighting on or off for this mod's world, with its exposure and bloom; off, the world is drawn as the game draws it. |
| `set-time-override` | `func(day-time: option<u64>)` | Shows the world at this time of day (AirDef fork): the sky, the sun and moon, the sky light and the fog, as `day-time` ticks (24000 a day, as `clock`). Only what the player sees changes: the world's own clock, its saves and its rules do not. None shows the world's own time. |
## `events`