Merge pull request #1162 from ludovic111/feat/dynamic-lights

🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-10-22 12:51:34 +02:00
commit c16b41ab31
9 files changed
+301 -11

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
//! Horizon Extras' dynamic lights (docs/extras.md): a torch, lantern or other light source held by
//! a player or mob, dropped as an item, or an entity on fire lights the blocks and entities
//! around it. Not in vanilla; off by default.
//!
//! Client-only and at render time: the light engine, chunk meshes and saves never see it. Each
//! frame the game picks the [`MAX`] sources nearest the camera; terrain.wgsl and entity.wgsl
//! raise a vertex's block light to `level - distance` (one level per block, as block light falls
//! off) when that is brighter. Off, the list is empty and the shaders skip the loop.
use crate::blocks::BlockFacts;
use glam::DVec3;
use horizon_world::registry::BlockRegistry;
use std::collections::HashMap;
/// Lights the shaders read: `Globals.dynamic_lights` in gfx.rs, terrain.wgsl and entity.wgsl.
pub const MAX: usize = 16;
/// What an entity on fire gives off, as a lit campfire or fire block does.
pub const BURNING: u8 = 15;
/// Sources farther than this from the camera light nothing it can see well.
const REACH: f64 = 96.0;
/// How brightly each item glows when held or dropped.
#[derive(Debug, Default)]
pub struct Luminance(HashMap<String, u8>);
impl Luminance {
/// A block item glows like its block's default state (`getLightEmission`): torches 14,
/// lanterns and glowstone 15, a lit redstone torch 7; a furnace's item, unlit, not at all.
pub fn new(facts: &BlockFacts, registry: &BlockRegistry) -> Self {
let mut map = HashMap::new();
for (id, state) in facts.states.iter().enumerate() {
if state.emission == 0 || *state.item != *state.name {
continue;
}
if registry.default_state(&state.name).map(|d| d.0 as usize) == Some(id) {
map.insert(state.item.to_string(), state.emission);
}
}
// A bucket of lava glows as lava does.
map.insert("minecraft:lava_bucket".into(), 15);
Self(map)
}
pub fn of(&self, item: &str) -> u8 {
self.0.get(item).copied().unwrap_or(0)
}
#[cfg(test)]
fn from_pairs(pairs: &[(&str, u8)]) -> Self {
Self(pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect())
}
}
/// One light this frame: where it is (world space) and its level, 1 to 15.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Source {
pub at: DVec3,
pub level: u8,
}
/// The [`MAX`] sources nearest `camera` within reach, as the shaders read them: camera-relative
/// position and level. Overwrites `out`.
pub fn select(sources: &[Source], camera: DVec3, out: &mut Vec<[f32; 4]>) {
out.clear();
let key = |s: &Source| s.at.distance_squared(camera);
let mut near: Vec<&Source> = sources
.iter()
.filter(|s| s.level > 0 && key(s) <= REACH * REACH)
.collect();
near.sort_by(|a, b| key(a).total_cmp(&key(b)));
out.extend(near.into_iter().take(MAX).map(|s| {
let p = (s.at - camera).as_vec3();
[p.x, p.y, p.z, f32::from(s.level.min(15))]
}));
}
/// The block light a vertex at `pos` (camera-relative) gets from `lights`, 0 to 15: the WGSL
/// `dynamic_block_light` in Rust, for tests.
pub fn level_at(pos: [f32; 3], lights: &[[f32; 4]]) -> f32 {
lights.iter().fold(0.0f32, |level, l| {
let d =
((pos[0] - l[0]).powi(2) + (pos[1] - l[1]).powi(2) + (pos[2] - l[2]).powi(2)).sqrt();
level.max(l[3] - d)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nearest_sources_win() {
let camera = DVec3::new(100.0, 64.0, 100.0);
let mut sources: Vec<Source> = (0..40)
.map(|i| Source {
at: camera + DVec3::new(f64::from(i), 0.0, 0.0),
level: 14,
})
.collect();
sources.push(Source {
at: camera + DVec3::new(0.5, 0.0, 0.0),
level: 0,
});
sources.push(Source {
at: camera + DVec3::new(0.0, 0.0, 200.0),
level: 15,
});
let mut out = Vec::new();
select(&sources, camera, &mut out);
assert_eq!(out.len(), MAX);
assert_eq!(out[0], [0.0, 0.0, 0.0, 14.0]);
assert_eq!(out[MAX - 1][0], (MAX - 1) as f32);
select(&[], camera, &mut out);
assert!(out.is_empty());
}
#[test]
fn light_falls_off_a_level_per_block() {
let torch = [[0.0, 0.0, 0.0, 14.0]];
assert_eq!(level_at([0.0, 0.0, 0.0], &torch), 14.0);
assert_eq!(level_at([3.0, 4.0, 0.0], &torch), 9.0);
assert_eq!(level_at([20.0, 0.0, 0.0], &torch), 0.0);
let two = [[0.0, 0.0, 0.0, 14.0], [10.0, 0.0, 0.0, 15.0]];
assert_eq!(level_at([8.0, 0.0, 0.0], &two), 13.0);
assert_eq!(level_at([0.0; 3], &[]), 0.0);
}
#[test]
fn item_luminance() {
let l = Luminance::from_pairs(&[("minecraft:torch", 14)]);
assert_eq!(l.of("minecraft:torch"), 14);
assert_eq!(l.of("minecraft:stone"), 0);
}
}
+16 -2
View File
@@ -14,11 +14,13 @@ struct Globals {
// Atlas rects (u0, v0, width, height) of end_sky.png and end_portal.png.
end_portal_sprites: array<vec4<f32>, 2>,
// x: vanilla's GameTime ((game time % 24000 + partial tick) / 24000)
// y: Nether cardinal lighting.
// y: Nether cardinal lighting, z: how many dynamic_lights are set.
time: vec4<f32>,
crumbling_sprite: vec4<f32>,
// Selection outline parameters (read by terrain.wgsl).
outline_params: vec4<f32>,
// Horizon Extras' dynamic lights: camera-relative position and level (dynamic_light.rs).
dynamic_lights: array<vec4<f32>, 16>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var atlas: texture_2d<f32>;
@@ -58,13 +60,25 @@ fn overlay_texel(overlay: u32) -> vec4<f32> {
if overlay >= 16u && overlay < 32u { return vec4(1.0, 1.0, 1.0, WHITE_OVERLAY_ALPHA[overlay - 16u]); }
return vec4(1.0);
}
// 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
// level less per block. time.z of them are set: none when the extra is off, so no loop runs.
fn dynamic_block_light(pos: vec3<f32>) -> f32 {
let count = min(u32(globals.time.z), 16u);
var level = 0.0;
for (var i = 0u; i < count; i++) {
let l = globals.dynamic_lights[i];
level = max(level, l.w - distance(pos, l.xyz));
}
return level * 16.0;
}
fn entity_vertex(position: vec3<f32>, uv: vec2<f32>,
overlay: u32, color: vec4<f32>, light: vec4<u32>, normal: vec4<f32>) -> Out {
var out: Out;
out.clip = globals.view_proj * vec4(position, 1.0);
out.proj = vec4(out.clip.xy * 0.5 + out.clip.w * 0.5, out.clip.zw);
out.uv = uv;
let l = vec2(f32(light.y), f32(light.x));
let l = vec2(max(f32(light.y), dynamic_block_light(position)), f32(light.x));
let lm = textureSampleLevel(lightmap, lightmap_sampler,
clamp(l / 256.0 + 0.5 / 16.0, vec2(0.5 / 16.0), vec2(15.5 / 16.0)), 0.0);
// Celestial bodies, clouds, beacon beams, End portals and breaking cracks are unlit.
+21 -4
View File
@@ -69,6 +69,8 @@ impl Corner {
#[derive(Clone, Debug, PartialEq)]
pub struct Extras {
pub fullbright: bool,
/// Held, dropped and burning light sources light their surroundings (`dynamic_light`).
pub dynamic_lights: bool,
pub zoom: bool,
/// A vanilla key name (`key.keyboard.z`).
pub zoom_key: String,
@@ -110,6 +112,7 @@ impl Default for Extras {
fn default() -> Self {
Self {
fullbright: false,
dynamic_lights: false,
zoom: false,
zoom_key: "key.keyboard.z".into(),
zoom_level: 4,
@@ -158,6 +161,7 @@ impl Extras {
};
match key.trim() {
"fullbright" => e.fullbright = flag(e.fullbright),
"dynamicLights" => e.dynamic_lights = flag(e.dynamic_lights),
"zoom" => e.zoom = flag(e.zoom),
"zoomKey" if value.starts_with("key.") => e.zoom_key = value.to_owned(),
"zoomLevel" => {
@@ -201,8 +205,9 @@ impl Extras {
}
pub fn serialize(&self) -> String {
format!(
"fullbright:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\n",
"fullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\n",
bool_text(self.fullbright),
bool_text(self.dynamic_lights),
bool_text(self.zoom),
self.zoom_key,
self.zoom_level,
@@ -423,6 +428,7 @@ impl Cps {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Item {
Fullbright,
DynamicLights,
Zoom,
ZoomKey,
ZoomLevel,
@@ -452,6 +458,7 @@ pub fn rows() -> Vec<Result<Item, &'static str>> {
vec![
Err("Lighting"),
Ok(Fullbright),
Ok(DynamicLights),
Err("Zoom"),
Ok(Zoom),
Ok(ZoomKey),
@@ -501,6 +508,7 @@ impl Item {
pub fn caption(self) -> &'static str {
match self {
Item::Fullbright => "Fullbright",
Item::DynamicLights => "Dynamic lights",
Item::Zoom => "Zoom",
Item::ZoomKey => "Zoom key",
Item::ZoomLevel => "Starting zoom",
@@ -533,7 +541,8 @@ impl Item {
KeystrokesCorner | KeystrokesCps => Keystrokes,
MouseTweaksWheel => MouseTweaks,
SortKey => Sort,
Fullbright | Zoom | Minimap | Fps | Keystrokes | MouseTweaks | Sort | AutoReconnect => {
Fullbright | DynamicLights | Zoom | Minimap | Fps | Keystrokes | MouseTweaks | Sort
| AutoReconnect => {
return None;
}
})
@@ -542,6 +551,9 @@ impl Item {
pub fn description(self) -> &'static str {
match self {
Item::Fullbright => "Show blocks and entities at full light, even in dark areas.",
Item::DynamicLights => {
"Torches and other light sources held, dropped or on fire light the blocks around them."
}
Item::Zoom => "Hold the zoom key to zoom; the wheel changes it while held.",
Item::ZoomKey => "Click, then press a key. Escape unbinds it.",
Item::ZoomLevel => "The zoom the key starts at.",
@@ -581,7 +593,7 @@ impl Item {
use crate::graphics_style::Cost;
match self {
Item::Fullbright | Item::Zoom | Item::Fps | Item::Keystrokes => Some(Cost::None),
Item::Minimap => Some(Cost::Low),
Item::Minimap | Item::DynamicLights => Some(Cost::Low),
Item::AutoReconnect | Item::MouseTweaks | Item::Sort => Some(Cost::None),
_ => None,
}
@@ -590,6 +602,7 @@ impl Item {
pub fn switch(self, e: &Extras) -> Option<bool> {
Some(match self {
Item::Fullbright => e.fullbright,
Item::DynamicLights => e.dynamic_lights,
Item::Zoom => e.zoom,
Item::ZoomSmooth => e.zoom_smooth,
Item::ZoomSensitivity => e.zoom_sensitivity,
@@ -632,6 +645,7 @@ impl Item {
pub fn cycle(self, e: &mut Extras, back: bool) {
match self {
Item::Fullbright => e.fullbright = !e.fullbright,
Item::DynamicLights => e.dynamic_lights = !e.dynamic_lights,
Item::Zoom => e.zoom = !e.zoom,
Item::ZoomSmooth => e.zoom_smooth = !e.zoom_smooth,
Item::ZoomSensitivity => e.zoom_sensitivity = !e.zoom_sensitivity,
@@ -677,7 +691,7 @@ mod tests {
#[test]
fn everything_is_off_by_default_and_round_trips() {
let d = Extras::default();
assert!(!d.fullbright);
assert!(!d.fullbright && !d.dynamic_lights);
assert!(!d.zoom && !d.fps && !d.keystrokes && !d.minimap && !d.auto_reconnect);
assert!(!d.mouse_tweaks && !d.sort);
assert_eq!(Extras::parse(""), d);
@@ -685,6 +699,9 @@ mod tests {
let mut e = d.clone();
Item::Fullbright.cycle(&mut e, false);
assert_eq!(Item::Fullbright.switch(&e), Some(true));
Item::DynamicLights.cycle(&mut e, false);
assert_eq!(Item::DynamicLights.switch(&e), Some(true));
assert_eq!(Extras::parse(&e.serialize()), e);
e.zoom = true;
e.zoom_key = "key.keyboard.c".into();
e.zoom_level = 6;
+14
View File
@@ -38,6 +38,7 @@ mod deflect;
mod dispense_fixture;
mod dispenser;
mod dragon;
mod dynamic_lights;
mod enchant_effects;
mod end;
mod entity_culling;
@@ -1184,6 +1185,10 @@ struct Game<'a> {
renderer: Option<Renderer>,
/// CPU geometry storage retained across frames, including empty/title frames.
entity_vertices: Vec<Vertex>,
/// Horizon Extras' dynamic lights this frame (`dynamic_lights.rs`), and how brightly each
/// item glows (built the first time the extra is on).
dynamic_lights: Vec<[f32; 4]>,
luminance: Option<horizon_client::dynamic_light::Luminance>,
/// Name tags drawn over everything (`RenderPipelines.TEXT_SEE_THROUGH`).
see_through_vertices: Vec<Vertex>,
started: Instant,
@@ -9665,6 +9670,11 @@ impl Game<'_> {
.then(|| self.border_draw(camera.to_array(), alpha as f32))
.flatten();
let see_through = std::mem::take(&mut self.see_through_vertices);
if title {
self.dynamic_lights.clear();
} else {
self.gather_dynamic_lights(camera, alpha);
}
let frame = Frame {
shader: self.shader_inputs(camera, alpha as f32),
nether_lighting: self.dim == Dim::Nether,
@@ -9722,6 +9732,7 @@ impl Game<'_> {
color: sky.cloud_color,
}),
entities: &entities,
dynamic_lights: &self.dynamic_lights,
additive: &additive,
see_through: &see_through,
celestial: if self.settings.horizon.sky || title {
@@ -11312,6 +11323,8 @@ impl<'a> Game<'a> {
window: None,
renderer: None,
entity_vertices: Vec::new(),
dynamic_lights: Vec::new(),
luminance: None,
see_through_vertices: Vec::new(),
health_hud: hud_layout::HealthHud::default(),
started: now,
@@ -12185,6 +12198,7 @@ fn loading_frame(hud: &HudBatch) -> Frame<'_> {
hud,
nether_lighting: false,
entities: &[],
dynamic_lights: &[],
clouds: None,
additive: &[],
see_through: &[],
@@ -0,0 +1,67 @@
//! Horizon Extras' dynamic lights (`horizon_client::dynamic_light`): the light sources near the
//! camera this frame. Nothing is gathered while the extra is off.
use super::{Game, shield};
use glam::DVec3;
use horizon_client::dynamic_light::{self, BURNING, Luminance, Source};
impl Game<'_> {
/// Fills `dynamic_lights` for the frame: the player's and mobs' held items and fire, and
/// glowing dropped items, the nearest [`dynamic_light::MAX`] of them.
pub(super) fn gather_dynamic_lights(&mut self, camera: DVec3, alpha: f64) {
let mut out = std::mem::take(&mut self.dynamic_lights);
out.clear();
if !self.settings.extras.dynamic_lights {
self.dynamic_lights = out;
return;
}
if self.luminance.is_none() {
self.luminance = Some(Luminance::new(self.res.facts(), self.pack.assets.registry));
}
let lum = self.luminance.as_ref().expect("built above");
let held = |item: Option<&str>| item.map_or(0, |i| lum.of(i));
let mut sources = Vec::new();
if let Some(p) = &self.player
&& !p.spectator
{
let feet = self
.previous_position
.lerp(p.position, self.clock.camera_fraction());
let level = held(self.stack_in(self.selected).map(|s| &*s.item))
.max(held(self.stack_in(shield::OFFHAND_SLOT).map(|s| &*s.item)))
.max(if p.fire_ticks > 0 { BURNING } else { 0 });
// About where the hands are.
sources.push(Source {
at: DVec3::new(feet.x, feet.y + 1.2, feet.z),
level,
});
}
for m in self.mobs.iter().filter(|m| !m.removed) {
let level = held(m.extra.main_hand.as_deref())
.max(held(m.extra.off_hand.as_deref()))
.max(if m.fire_ticks > 0 { BURNING } else { 0 });
if level == 0 {
continue;
}
let at = m.previous.lerp(m.position, alpha);
sources.push(Source {
at: DVec3::new(at.x, at.y + f64::from(m.height()) * 0.6, at.z),
level,
});
}
for item in &self.dropped {
let level = lum
.of(&item.stack.item)
.max(if item.fire_ticks > 0 { BURNING } else { 0 });
if level == 0 {
continue;
}
let at = item.previous.lerp(item.position, alpha);
sources.push(Source {
at: DVec3::new(at.x, at.y + 0.25, at.z),
level,
});
}
dynamic_light::select(&sources, camera, &mut out);
self.dynamic_lights = out;
}
}
+14 -2
View File
@@ -107,13 +107,16 @@ struct Globals {
fog_ends: [f32; 4],
/// Atlas rects (u0, v0, width, height) of end_sky.png and end_portal.png.
end_portal_sprites: [[f32; 4]; 2],
/// x: vanilla's `GameTime` uniform; y: nether cardinal lighting, zw unused.
/// x: vanilla's `GameTime` uniform; y: nether cardinal lighting; z: how many
/// `dynamic_lights` are set; w unused.
time: [f32; 4],
/// Atlas rect of a destroy stage (entity.wgsl takes only its size: every stage's is the same,
/// and each crack carries its own stage's origin).
crumbling_sprite: [f32; 4],
/// Framebuffer size, selection stroke width, perspective layering scale.
outline_params: [f32; 4],
/// Horizon Extras' dynamic lights: camera-relative position and level (`dynamic_light`).
dynamic_lights: [[f32; 4]; crate::dynamic_light::MAX],
}
pub type SectionKey = (i32, i32, i32);
struct SectionGpu {
@@ -186,6 +189,8 @@ pub struct Frame<'a> {
pub nether_lighting: bool,
/// Camera-relative entity geometry textured from the shared atlas.
pub entities: &'a [Vertex],
/// Horizon Extras' dynamic lights, nearest first (`dynamic_light::select`); empty when off.
pub dynamic_lights: &'a [[f32; 4]],
/// Cloud mesh inputs; geometry and GPU storage are retained by the renderer.
pub clouds: Option<crate::clouds::Input<'a>>,
/// Additive translucent geometry after the entities (lightning, dragon rays).
@@ -2908,10 +2913,17 @@ impl Renderer {
time: [
frame.shader_game_time,
f32::from(frame.nether_lighting),
0.0,
frame.dynamic_lights.len().min(crate::dynamic_light::MAX) as f32,
0.0,
],
outline_params: outline_params(self.config.width, self.config.height),
dynamic_lights: {
let mut lights = [[0.0; 4]; crate::dynamic_light::MAX];
for (to, from) in lights.iter_mut().zip(frame.dynamic_lights) {
*to = *from;
}
lights
},
};
self.uploads
.buffer(&mut encoder, &self.globals, 0, bytemuck::bytes_of(&globals));
+1
View File
@@ -51,6 +51,7 @@ pub mod datapack;
pub mod debug_chart;
pub mod debug_keys;
pub mod debug_overlay;
pub mod dynamic_light;
pub mod effects;
pub mod enchant;
pub mod end_crystal;
+18 -2
View File
@@ -16,11 +16,14 @@ struct Globals {
fog_ends: vec4<f32>,
// Atlas rects (u0, v0, width, height) of end_sky.png and end_portal.png.
end_portal_sprites: array<vec4<f32>, 2>,
// x: vanilla's GameTime ((game time % 24000 + partial tick) / 24000)
// x: vanilla's GameTime ((game time % 24000 + partial tick) / 24000), z: how many
// dynamic_lights are set
time: vec4<f32>,
// Atlas rect of the current destroy stage (read by entity.wgsl).
crumbling_sprite: vec4<f32>,
outline_params: vec4<f32>,
// Horizon Extras' dynamic lights: camera-relative position and level (dynamic_light.rs).
dynamic_lights: array<vec4<f32>, 16>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var sprites: texture_2d_array<f32>;
@@ -67,6 +70,18 @@ struct TerrainOut {
@location(3) spherical: f32,
@location(4) cylindrical: 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
// level less per block. time.z of them are set: none when the extra is off, so no loop runs.
fn dynamic_block_light(pos: vec3<f32>) -> f32 {
let count = min(u32(globals.time.z), 16u);
var level = 0.0;
for (var i = 0u; i < count; i++) {
let l = globals.dynamic_lights[i];
level = max(level, l.w - distance(pos, l.xyz));
}
return level * 16.0;
}
fn sample_lightmap(sky: f32, block: f32) -> vec4<f32> {
let uv = vec2(block, sky) * 16.0;
return textureSampleLevel(lightmap, lightmap_sampler,
@@ -105,7 +120,8 @@ fn terrain_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f3
out.clip = globals.view_proj * vec4(pos, 1.0);
out.uv = uv;
out.layer = layer;
out.color = color * sample_lightmap(f32(light.x) / 16.0, f32(light.y) / 16.0);
let block = max(f32(light.y), dynamic_block_light(pos));
out.color = color * sample_lightmap(f32(light.x) / 16.0, block / 16.0);
out.spherical = length(pos);
out.cylindrical = max(length(pos.xz), abs(pos.y));
return out;
+17 -1
View File
@@ -1,6 +1,6 @@
# Horizon extras
Optional conveniences built into the client: fullbright, zoom, a minimap, an FPS counter, a
Optional conveniences built into the client: fullbright, dynamic lights, zoom, a minimap, an FPS counter, a
keystroke overlay, auto-reconnect, Mouse Tweaks-style inventory controls and
inventory sorting. **Every one is off by default**: a fresh
install looks and behaves exactly like vanilla 26.3, and turning an extra off
@@ -12,6 +12,7 @@ again restores vanilla's behaviour completely.
|---|---|
| Settings, parsing and saving | `crates/horizon-client/src/extras.rs` |
| Zoom curve, FPS and click counters | `extras.rs` (pure logic, unit tested) |
| Dynamic lights: item glow, nearest sources | `crates/horizon-client/src/dynamic_light.rs` (unit tested), `game/dynamic_lights.rs`, `terrain.wgsl` and `entity.wgsl` (`dynamic_block_light`) |
| HUD drawing (FPS, keystrokes, minimap frame) | `crates/horizon-client/src/extras_hud.rs` |
| Minimap colours, tiles and image | `crates/horizon-client/src/minimap.rs` |
| Reconnect policy and backoff | `crates/horizon-client/src/reconnect.rs` |
@@ -45,6 +46,21 @@ Horizon category in Controls, and adding one would put a non-vanilla mapping int
- Turning it off restores the normal lightmap using the current brightness,
time of day and effects. The brightness slider keeps its normal range.
### Dynamic lights (`dynamicLights`, default off)
- A light source held by a player or mob (main or off hand), dropped as an item, or an entity on
fire lights the blocks, entities and hand around it, as LambDynamicLights and OptiFine do. Not
in vanilla; the idea is ported, not a mod's code (issue #814).
- An item glows like its block's default state (`getLightEmission`): torches 14, lanterns,
glowstone, sea lanterns and jack o'lanterns 15, soul torches 10, a redstone torch 7; a lava bucket
15. An entity on fire gives 15.
- Client-only, at render time: the light engine, meshes, saves and other players never see it.
Each frame the game picks the 16 sources nearest the camera (within 96 blocks,
`dynamic_light::select`); terrain.wgsl and entity.wgsl raise each vertex's block light to the
source's level minus its distance in blocks when that is brighter, then light it through the
lightmap as usual. The light doesn't stop at walls.
- Off: nothing is gathered and the shaders' loop runs zero times (the count is 0), so it costs
nothing measurable (numbers in the pull request, #814).
### Zoom (`zoom`, default off)
- Hold the zoom key (default **Z**, unused by every vanilla 26.3 key mapping; C
is `key.saveToolbarActivator`) to zoom. The key does nothing while zoom is off.