Merge pull request #9244 from horizon-rust/pvp/outline-hit

🤖 Agent: block outline and hit colour stay render-only and off by default; verify and parity pass at 4adfac92b. The composter fix #9508 is merged in. pack_screen's failures came from the playhorizon runner missing 142 vanilla lang files: the files were copied there and it passes now.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-05 00:48:10 +01:00
commit 6319cb701e
13 files changed
+371 -12

No files matched your search

@@ -355,6 +355,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
outline: Vec::new(),
outline_after_terrain: false,
outline_high_contrast: false,
pvp: Default::default(),
debug_lines: &[],
debug_lines_on_top: &[],
debug_crosshair: &[],
+5 -1
View File
@@ -87,7 +87,11 @@ fn entity_light(normal: vec3<f32>, nether: bool) -> f32 {
// primed TNT flash (5) and at column overlay - 16 for 16..=31 (a creeper's
// swelling). Alpha 1 changes nothing.
fn overlay_texel(overlay: u32) -> vec4<f32> {
if overlay == 1u || overlay == 8u { return HURT_OVERLAY; }
if overlay == 1u || overlay == 8u {
// PvP & HUD's hit colour (pvp.rs `Hit::texel`) rides in colored_count.y; 0 is vanilla's.
let hit = globals.colored_count.y;
return select(HURT_OVERLAY, unpack4x8unorm(hit), hit != 0u);
}
if overlay == 5u { return vec4(1.0, 1.0, 1.0, WHITE_OVERLAY_ALPHA[15]); }
if overlay >= 16u && overlay < 32u { return vec4(1.0, 1.0, 1.0, WHITE_OVERLAY_ALPHA[overlay - 16u]); }
return vec4(1.0);
+9 -4
View File
@@ -883,6 +883,8 @@ pub struct Frame<'a> {
pub outline_after_terrain: bool,
/// `BlockOutlineRenderState.highContrast`: a thick black outline under a bright one.
pub outline_high_contrast: bool,
/// The PvP & HUD page's outline and hit colour (`pvp::Frame`; default: vanilla's).
pub pvp: crate::pvp::Frame,
/// `lines` with a colour and pixel width (`gizmos::camera_relative`):
/// the fishing line and debug gizmos, depth tested, then gizmos drawn
/// over everything.
@@ -7252,7 +7254,8 @@ impl Renderer {
},
colored_count: [
frame.colored_lights.len().min(crate::dynamic_light::MAX) as u32,
0,
// PvP & HUD's hit colour: entity.wgsl's hurt texel, 0 for vanilla's.
frame.pvp.hurt_texel,
0,
0,
],
@@ -8133,12 +8136,14 @@ impl Renderer {
line
})
};
// A custom outline (PvP & HUD) takes the bright stroke's colour, scales both widths.
let (custom, scale) = frame.pvp.outline.map_or((None, 1.0), |(c, s)| (Some(c), s));
let lines: Vec<[f32; 8]> = if frame.outline_high_contrast {
stroke(0xff00_0000, 7.0)
.chain(stroke(0xff57_ffe1, width))
stroke(0xff00_0000, 7.0 * scale)
.chain(stroke(custom.unwrap_or(0xff57_ffe1), width * scale))
.collect()
} else {
stroke(0x6600_0000, width).collect()
stroke(custom.unwrap_or(0x6600_0000), width * scale).collect()
};
self.uploads
.buffer(&mut encoder, &self.outline, 0, bytemuck::cast_slice(&lines));
@@ -517,6 +517,7 @@ impl Scene {
outline: Vec::new(),
outline_after_terrain: false,
outline_high_contrast: false,
pvp: Default::default(),
debug_lines: &[],
debug_lines_on_top: &[],
debug_crosshair: &[],
@@ -595,6 +595,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
outline: Vec::new(),
outline_after_terrain: false,
outline_high_contrast: false,
pvp: Default::default(),
debug_lines: &[],
debug_lines_on_top: &[],
debug_crosshair: &[],
@@ -438,6 +438,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
outline: Vec::new(),
outline_after_terrain: false,
outline_high_contrast: false,
pvp: Default::default(),
debug_lines: &[],
debug_lines_on_top: &[],
debug_crosshair: &[],
@@ -328,6 +328,7 @@ fn frame(hud: &HudBatch) -> Frame<'_> {
outline: Vec::new(),
outline_after_terrain: false,
outline_high_contrast: false,
pvp: Default::default(),
debug_lines: &[],
debug_lines_on_top: &[],
debug_crosshair: &[],
@@ -1447,6 +1447,7 @@ mod initial_history_tests {
outline: Vec::new(),
outline_after_terrain: false,
outline_high_contrast: false,
pvp: Default::default(),
debug_lines: &[],
debug_lines_on_top: &[],
debug_crosshair: &[],
+246 -6
View File
@@ -1,4 +1,4 @@
//! PvP and HUD options (`docs/pvp.md`): a custom crosshair. Kept in
//! PvP and HUD options (`docs/pvp.md`): a custom crosshair, block outline and hit colour. Kept in
//! `horizon-extras.txt` with the other extras ([`crate::extras::Extras::pvp`]) and shown on
//! Horizon Settings > PvP & HUD. Every option is off by default, so the game looks like
//! vanilla until the player changes one, and none of them shows anything vanilla doesn't
@@ -71,7 +71,7 @@ impl Colour {
pub fn argb(self) -> u32 {
match self {
Colour::Inverted | Colour::White => 0xFFFF_FFFF,
Colour::Red => 0xFFFF_3030,
Colour::Red => 0xFFFF_0000,
Colour::Orange => 0xFFFF_A020,
Colour::Yellow => 0xFFFF_FF40,
Colour::Green => 0xFF40_FF40,
@@ -87,6 +87,18 @@ impl Colour {
fn step(self, back: bool) -> Colour {
step(&Self::ALL, self, back)
}
/// The next solid colour (`Inverted` left out).
fn step_solid(self, back: bool) -> Colour {
step(&Self::ALL[1..], self, back)
}
/// The colour at `percent` opacity, as ARGB.
pub fn with_opacity(self, percent: u8) -> u32 {
self.argb() & 0x00FF_FFFF | alpha(percent) << 24
}
}
/// `percent` of 255, rounded.
fn alpha(percent: u8) -> u32 {
(u32::from(percent.min(100)) * 255 + 50) / 100
}
/// The next (or previous) value of `all` after `v`, wrapping.
@@ -97,11 +109,15 @@ fn step<T: Copy + PartialEq>(all: &[T], v: T, back: bool) -> T {
}
/// `v` one step up or down within `lo..=hi`, wrapping.
fn step_num(v: u8, lo: u8, hi: u8, back: bool) -> u8 {
step_by(v, (lo, hi), 1, back)
}
/// `v` `by` up or down within `lo..=hi`, wrapping.
fn step_by(v: u8, (lo, hi): (u8, u8), by: u8, back: bool) -> u8 {
match (back, v) {
(false, v) if v >= hi => lo,
(false, v) => v + 1,
(false, v) => (v + by).min(hi),
(true, v) if v <= lo => hi,
(true, v) => v - 1,
(true, v) => v.saturating_sub(by).max(lo),
}
}
@@ -339,9 +355,104 @@ impl CrosshairCache {
}
}
/// The block outline (`LevelRenderer.submitBlockOutline`). Off, vanilla's: black at 40%, or the
/// accessibility option's high contrast one.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Outline {
pub custom: bool,
/// A solid colour, at `opacity` percent.
pub colour: Colour,
pub opacity: u8,
/// Times vanilla's width (`Window.getAppropriateLineWidth`).
pub thickness: u8,
/// The block's faces tinted with this colour, lightly.
pub fill: Option<Colour>,
}
pub const OPACITIES: (u8, u8) = (10, 100);
pub const OUTLINE_THICKNESSES: (u8, u8) = (1, 4);
/// How much the fill covers the block's faces, in percent.
pub const FILL_OPACITY: u8 = 25;
impl Default for Outline {
fn default() -> Self {
Self {
custom: false,
colour: Colour::Black,
opacity: 40,
thickness: 1,
fill: None,
}
}
}
impl Outline {
/// The stroke's ARGB and its width as a multiple of vanilla's; none for vanilla's.
pub fn stroke(&self) -> Option<(u32, f32)> {
self.custom.then(|| {
(
self.colour.with_opacity(self.opacity),
f32::from(self.thickness),
)
})
}
/// The fill's ARGB, when there is one.
pub fn fill(&self) -> Option<u32> {
self.fill
.filter(|_| self.custom)
.map(|c| c.with_opacity(FILL_OPACITY))
}
}
/// The tint a hurt entity flashes (`OverlayTexture`'s red rows). Off, vanilla's: red over 30%.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Hit {
pub custom: bool,
pub colour: Colour,
/// How much of the colour covers the entity, in percent.
pub opacity: u8,
}
pub const HIT_OPACITIES: (u8, u8) = (0, 100);
impl Default for Hit {
fn default() -> Self {
Self {
custom: false,
colour: Colour::Red,
opacity: 30,
}
}
}
impl Hit {
/// The overlay texel as RGBA bytes in a little-endian u32 (WGSL's `unpack4x8unorm`), its alpha
/// what of the entity's own colour stays (`entity.fsh` mixes by it); 0 for vanilla's texel.
pub fn texel(&self) -> u32 {
if !self.custom {
return 0;
}
let [_, r, g, b] = self.colour.argb().to_be_bytes();
u32::from_le_bytes([r, g, b, (255 - alpha(self.opacity)) as u8])
}
}
/// What the renderer reads of these options each frame (`FrameInputs::pvp`); the default is
/// vanilla's look.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Frame {
/// The block outline's stroke ([`Outline::stroke`]).
pub outline: Option<(u32, f32)>,
/// The hurt overlay texel ([`Hit::texel`]), 0 for vanilla's.
pub hurt_texel: u32,
}
impl Pvp {
pub fn frame(&self) -> Frame {
Frame {
outline: self.outline.stroke(),
hurt_texel: self.hit.texel(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Pvp {
pub crosshair: Crosshair,
pub outline: Outline,
pub hit: Hit,
}
fn flag(value: &str, d: bool) -> bool {
match value {
@@ -357,6 +468,7 @@ impl Pvp {
/// One `key:value` line of `horizon-extras.txt`; false when the key isn't a PvP one.
pub fn parse_line(&mut self, key: &str, value: &str) -> bool {
let c = &mut self.crosshair;
let (o, h) = (&mut self.outline, &mut self.hit);
match key {
"crosshair" => c.custom = flag(value, c.custom),
"crosshairShape" => c.shape = Shape::parse(value).unwrap_or(c.shape),
@@ -372,6 +484,19 @@ impl Pvp {
v => Colour::parse(v).or(c.hover),
}
}
"outline" => o.custom = flag(value, o.custom),
"outlineColour" => o.colour = Colour::parse(value).unwrap_or(o.colour),
"outlineOpacity" => o.opacity = num(value, OPACITIES, o.opacity),
"outlineThickness" => o.thickness = num(value, OUTLINE_THICKNESSES, o.thickness),
"outlineFill" => {
o.fill = match value {
"off" => None,
v => Colour::parse(v).or(o.fill),
}
}
"hit" => h.custom = flag(value, h.custom),
"hitColour" => h.colour = Colour::parse(value).unwrap_or(h.colour),
"hitOpacity" => h.opacity = num(value, HIT_OPACITIES, h.opacity),
_ => return false,
}
true
@@ -393,6 +518,19 @@ impl Pvp {
c.dot,
c.hover.map_or("off", Colour::name),
);
let (o, h) = (&self.outline, &self.hit);
let _ = write!(
out,
"outline:{}\noutlineColour:{}\noutlineOpacity:{}\noutlineThickness:{}\noutlineFill:{}\nhit:{}\nhitColour:{}\nhitOpacity:{}\n",
o.custom,
o.colour.name(),
o.opacity,
o.thickness,
o.fill.map_or("off", Colour::name),
h.custom,
h.colour.name(),
h.opacity,
);
}
}
@@ -433,6 +571,15 @@ opts! {
CrosshairDot, "crosshair_dot", "Centre dot", "A dot in the middle of the crosshair.";
CrosshairHover, "crosshair_hover", "Colour on entities",
"Another colour while the crosshair is over a mob or a player you could hit, as vanilla's attack indicator knows it.";
Outline, "outline", "Custom block outline", "Changes the outline around the block you look at.";
OutlineColour, "outline_colour", "Colour", "The outline's colour. Black at 40% is vanilla's.";
OutlineOpacity, "outline_opacity", "Opacity", "How solid the outline is.";
OutlineThickness, "outline_thickness", "Thickness", "The outline's width, in times vanilla's.";
OutlineFill, "outline_fill", "Fill", "Tints the faces of the block you look at, lightly.";
Hit, "hit", "Custom hit colour",
"Changes the colour mobs and players flash when they are hurt. Red at 30% is vanilla's.";
HitColour, "hit_colour", "Colour", "The colour hurt mobs and players flash.";
HitOpacity, "hit_opacity", "Opacity", "How much of the colour covers them.";
}
/// The PvP & HUD page: headers (language keys) and options.
pub fn rows() -> Vec<Result<Opt, &'static str>> {
@@ -452,14 +599,30 @@ pub fn rows() -> Vec<Result<Opt, &'static str>> {
]
.map(Ok),
);
rows.push(Err("horizon.pvp.header.outline"));
rows.extend(
[
Outline,
OutlineColour,
OutlineOpacity,
OutlineThickness,
OutlineFill,
]
.map(Ok),
);
rows.push(Err("horizon.pvp.header.hit"));
rows.extend([Hit, HitColour, HitOpacity].map(Ok));
rows
}
impl Opt {
/// The switch this option belongs to.
pub fn parent(self) -> Option<Opt> {
use Opt::*;
match self {
Opt::Crosshair => None,
_ => Some(Opt::Crosshair),
Crosshair | Outline | Hit => None,
OutlineColour | OutlineOpacity | OutlineThickness | OutlineFill => Some(Outline),
HitColour | HitOpacity => Some(Hit),
_ => Some(Crosshair),
}
}
pub fn switch(self, p: &Pvp) -> Option<bool> {
@@ -468,6 +631,8 @@ impl Opt {
Opt::Crosshair => c.custom,
Opt::CrosshairOutline => c.outline,
Opt::CrosshairDot => c.dot,
Opt::Outline => p.outline.custom,
Opt::Hit => p.hit.custom,
_ => return None,
})
}
@@ -484,12 +649,29 @@ impl Opt {
Some(colour) => colour.label(),
None => crate::lang::get_or("Off", "Off"),
},
Opt::OutlineColour => p.outline.colour.label(),
Opt::OutlineOpacity => format!("{}%", p.outline.opacity),
Opt::OutlineThickness => format!("{}x", p.outline.thickness),
Opt::OutlineFill => match p.outline.fill {
Some(colour) => colour.label(),
None => crate::lang::get_or("Off", "Off"),
},
Opt::HitColour => p.hit.colour.label(),
Opt::HitOpacity => format!("{}%", p.hit.opacity),
_ => return None,
})
}
/// A click; `back` (right button, wheel up) goes the other way.
pub fn cycle(self, p: &mut Pvp, back: bool) {
let c = &mut p.crosshair;
let (o, h) = (&mut p.outline, &mut p.hit);
// Off, then every solid colour.
let solid_or_off = |v: Option<Colour>| {
let all = [None]
.into_iter()
.chain(Colour::ALL[1..].iter().copied().map(Some));
step(&all.collect::<Vec<_>>(), v, back)
};
match self {
Opt::Crosshair => c.custom = !c.custom,
Opt::CrosshairShape => c.shape = step(&Shape::ALL, c.shape, back),
@@ -508,6 +690,16 @@ impl Opt {
.collect();
c.hover = step(&all, c.hover, back);
}
Opt::Outline => o.custom = !o.custom,
Opt::OutlineColour => o.colour = o.colour.step_solid(back),
Opt::OutlineOpacity => o.opacity = step_by(o.opacity, OPACITIES, 10, back),
Opt::OutlineThickness => {
o.thickness = step_by(o.thickness, OUTLINE_THICKNESSES, 1, back)
}
Opt::OutlineFill => o.fill = solid_or_off(o.fill),
Opt::Hit => h.custom = !h.custom,
Opt::HitColour => h.colour = h.colour.step_solid(back),
Opt::HitOpacity => h.opacity = step_by(h.opacity, HIT_OPACITIES, 10, back),
}
}
}
@@ -664,6 +856,54 @@ mod tests {
assert!(!p.parse_line("fullbright", "true"));
}
#[test]
fn outline_and_hit_colours_default_to_vanillas() {
// Turned on with their first settings, they give vanilla's colours: the outline's
// 0x66000000 (LevelRenderer) and OverlayTexture's hurt texel 0xB2FF0000.
let o = Outline {
custom: true,
..Default::default()
};
assert_eq!(o.stroke(), Some((0x6600_0000, 1.0)));
assert_eq!(o.fill(), None);
let [r, g, b, a] = crate::overlay::HURT_TEXEL;
let hit = Hit {
custom: true,
..Default::default()
};
assert_eq!(hit.texel(), u32::from_le_bytes([r, g, b, a]));
assert_eq!(
(Outline::default().stroke(), Hit::default().texel()),
(None, 0)
);
// Fully opaque cyan covers the entity entirely: none of its own colour stays.
let cyan = Hit {
custom: true,
colour: Colour::Cyan,
opacity: 100,
};
assert_eq!(cyan.texel().to_le_bytes(), [0x40, 0xFF, 0xFF, 0]);
let mut p = Pvp::default();
Opt::Outline.cycle(&mut p, false);
Opt::OutlineFill.cycle(&mut p, false);
assert_eq!(p.outline.fill(), Some(0x40FF_FFFF), "white at 25%");
Opt::OutlineOpacity.cycle(&mut p, true);
Opt::OutlineOpacity.cycle(&mut p, true);
Opt::OutlineOpacity.cycle(&mut p, true);
Opt::OutlineOpacity.cycle(&mut p, true);
assert_eq!(p.outline.opacity, 100, "down from 10% wraps to 100%");
Opt::OutlineColour.cycle(&mut p, true);
assert_eq!(p.outline.colour, Colour::Magenta, "never Inverted");
let mut text = String::new();
p.serialize(&mut text);
let mut read = Pvp::default();
for line in text.lines() {
let (k, v) = line.split_once(':').unwrap();
assert!(read.parse_line(k, v), "{k}");
}
assert_eq!(read, p);
}
#[test]
fn every_option_and_value_has_its_english_text() {
let en = crate::lang::Language::load(&[], "en_us");
@@ -594,5 +594,23 @@
"horizon.pvp.shape.x": "X",
"horizon.pvp.shape.circle": "Circle",
"horizon.pvp.shape.square": "Square",
"horizon.pvp.shape.dot": "Dot"
"horizon.pvp.shape.dot": "Dot",
"horizon.pvp.outline": "Custom block outline",
"horizon.pvp.outline.tooltip": "Changes the outline around the block you look at.",
"horizon.pvp.outline_colour": "Colour",
"horizon.pvp.outline_colour.tooltip": "The outline's colour. Black at 40% is vanilla's.",
"horizon.pvp.outline_opacity": "Opacity",
"horizon.pvp.outline_opacity.tooltip": "How solid the outline is.",
"horizon.pvp.outline_thickness": "Thickness",
"horizon.pvp.outline_thickness.tooltip": "The outline's width, in times vanilla's.",
"horizon.pvp.outline_fill": "Fill",
"horizon.pvp.outline_fill.tooltip": "Tints the faces of the block you look at, lightly.",
"horizon.pvp.hit": "Custom hit colour",
"horizon.pvp.hit.tooltip": "Changes the colour mobs and players flash when they are hurt. Red at 30% is vanilla's.",
"horizon.pvp.hit_colour": "Colour",
"horizon.pvp.hit_colour.tooltip": "The colour hurt mobs and players flash.",
"horizon.pvp.hit_opacity": "Opacity",
"horizon.pvp.hit_opacity.tooltip": "How much of the colour covers them.",
"horizon.pvp.header.outline": "Block outline",
"horizon.pvp.header.hit": "Hit colour"
}
+3
View File
@@ -16222,6 +16222,7 @@ impl Game<'_> {
if !title {
self.end_geometry(camera, &mut entities);
}
self.outline_fill(&outline, camera, &mut entities);
let weather = if !title && self.can_have_weather() && self.settings.horizon.weather {
horizon_client::spans::time("weather", || self.weather_geometry(camera, alpha))
} else {
@@ -16309,6 +16310,7 @@ impl Game<'_> {
outline,
outline_after_terrain,
outline_high_contrast: self.settings.file.bool("highContrastBlockOutline"),
pvp: self.settings.extras.pvp.frame(),
debug_lines: &debug_lines,
debug_lines_on_top: &debug_lines_on_top,
debug_crosshair: &debug_crosshair,
@@ -21033,6 +21035,7 @@ fn loading_frame(hud: &HudBatch) -> Frame<'_> {
outline: Vec::new(),
outline_after_terrain: false,
outline_high_contrast: false,
pvp: Default::default(),
debug_lines: &[],
debug_lines_on_top: &[],
debug_crosshair: &[],
+67
View File
@@ -3,6 +3,7 @@
use super::Game;
use horizon_client::{
hud::{HudBatch, rgba},
mesher::Vertex,
pvp::{Colour, CrosshairCache},
};
@@ -45,4 +46,70 @@ impl Game<'_> {
batch.invert_since(start);
}
}
/// The block outline's fill: each box of the targeted block's shape (`getShape`, the boxes
/// the outline's edges come from) as translucent faces drawn after translucent terrain,
/// pushed out a little so they don't fight the block's own faces. Only while the outline
/// shows (`outline` is its edges).
pub(super) fn outline_fill(
&self,
outline: &[[f64; 6]],
camera: glam::DVec3,
out: &mut Vec<Vertex>,
) {
let Some(argb) = self.settings.extras.pvp.outline.fill() else {
return;
};
let Some(hit) = self.pick().filter(|_| !outline.is_empty()) else {
return;
};
let [x, y, z] = hit.pos;
let state = self.world.block(x, y, z);
let facts = self.res.facts().get(state);
let offset = match facts.outline_offset {
true => self.pack.context.tables.offset(state, x, z),
false => None,
}
.unwrap_or([0.0; 3]);
let base = [f64::from(x), f64::from(y), f64::from(z)];
let context = self.context_outline(hit.pos, state);
let boxes = context.as_slice().iter().chain(match context {
Some(_) => &[][..],
None => &facts.outline[..],
});
let uv = horizon_client::gfx::lightning::white_uv(&self.gui.atlas.font.white());
let [a, r, g, b] = argb.to_be_bytes();
let color = u32::from_le_bytes([r, g, b, a]);
const OUT: f64 = 0.002;
for shape in boxes {
let corner = |i: usize, high: bool| {
let v = if high {
shape[i + 3] + OUT
} else {
shape[i] - OUT
};
(base[i] + offset[i] + v - camera[i]) as f32
};
// Each face: the axis it faces along, its side, and its corners' other two axes.
for axis in 0..3 {
let (u, v) = ((axis + 1) % 3, (axis + 2) % 3);
for high in [false, true] {
for (hu, hv) in [(false, false), (true, false), (true, true), (false, true)] {
let mut pos = [0.0; 3];
pos[axis] = corner(axis, high);
pos[u] = corner(u, hu);
pos[v] = corner(v, hv);
out.push(Vertex {
pos,
uv,
layer: horizon_client::gfx::TRANSLUCENT_PARTICLE_OVERLAY,
color,
light: 240 | 240 << 8,
normal: [0; 4],
glint_uv: [0.0; 2],
});
}
}
}
}
}
}
+16
View File
@@ -27,3 +27,19 @@ Each feature is built from what it visibly does, never from another client's or
attack indicator reads (`crosshairPickEntity`).
- The attack indicator, the F3 axes and the spectator rules stay vanilla's; only the sprite is replaced.
- Cost: the pixels are worked out once per settings change (`CrosshairCache`); a frame adds a few quads.
## Block outline (`outline`, default off)
- Colour (solid), opacity 10-100%, thickness 1-4 times vanilla's width, and an optional fill that tints the
targeted block's faces at 25%. Turned on, its first settings are vanilla's: black at 40%, vanilla's width.
- With Accessibility's high contrast outline, the colour replaces the bright stroke and the black one stays.
- The fill draws each box of the block's shape (`getShape`, the boxes the outline's edges come from) as
translucent faces after translucent terrain, 0.002 out from the block. Only while the outline shows.
- Cost: the stroke is vanilla's draw with another colour and width; the fill adds at most a few dozen quads.
## Hit colour (`hit`, default off)
- The colour hurt mobs and players flash, and how much of it covers them (0-100%). Its first settings are
vanilla's: `OverlayTexture`'s red over 30%.
- The texel rides in the frame's uniforms (`colored_count.y`, 0 for vanilla's), so changing it costs nothing.
- Shader packs keep vanilla's red (`pack_entities::entity_color`).