mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge remote-tracking branch 'origin/feat/dynamic-lights' into integ/w1
This commit is contained in:
commit
ae482a72e9
9 files changed
+301
-11
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user