mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81baa3e0cb | ||
|
|
21baf0dc99 |
No files matched your search
@@ -222,6 +222,7 @@ impl Game<'_> {
|
||||
hud_overlays::camera_overlays(size[0] as i32, size[1] as i32, &overlays, &mut scope);
|
||||
self.screen_fx.build(&ScreenInputs {
|
||||
sprites: &self.gui.atlas.sprites,
|
||||
flags: &self.gui.atlas.flags,
|
||||
aspect: size[0] as f32 / size[1].max(1) as f32,
|
||||
partial,
|
||||
block_overlay,
|
||||
|
||||
@@ -1921,7 +1921,7 @@ impl Renderer {
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("screen.wgsl").into()),
|
||||
});
|
||||
let screen_attributes =
|
||||
wgpu::vertex_attr_array![0=>Float32x2,1=>Float32x2,2=>Float32x4,3=>Float32x4];
|
||||
wgpu::vertex_attr_array![0=>Float32x2,1=>Float32x2,2=>Float32x4,3=>Float32x4,4=>Uint32];
|
||||
let screen_vertex = [Some(wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<crate::screen_effects::ScreenVertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
|
||||
@@ -18,11 +18,35 @@ pub struct Rect {
|
||||
pub w: u32,
|
||||
pub h: u32,
|
||||
}
|
||||
/// `TextureMetadataSection`'s `blur` and `clamp`: how vanilla samples a
|
||||
/// standalone texture (`ReloadableTexture.setSampler`: linear rather than
|
||||
/// nearest filtering, clamp-to-edge rather than repeat addressing).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct TextureFlags {
|
||||
pub blur: bool,
|
||||
pub clamp: bool,
|
||||
}
|
||||
impl TextureFlags {
|
||||
/// The `texture` section of a `.png.mcmeta` file, if it sets either flag.
|
||||
pub fn from_mcmeta(json: &str) -> Option<Self> {
|
||||
let meta: serde_json::Value = serde_json::from_str(json).ok()?;
|
||||
let section = meta.get("texture")?;
|
||||
let flag = |key| section.get(key).and_then(serde_json::Value::as_bool) == Some(true);
|
||||
let flags = Self {
|
||||
blur: flag("blur"),
|
||||
clamp: flag("clamp"),
|
||||
};
|
||||
(flags != Self::default()).then_some(flags)
|
||||
}
|
||||
}
|
||||
pub struct HudAtlas {
|
||||
pub pixels: Vec<u8>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub sprites: HashMap<String, Rect>,
|
||||
/// Sprites whose `.png.mcmeta` sets `blur` or `clamp`; the others
|
||||
/// sample as vanilla's defaults (nearest, repeat).
|
||||
pub flags: HashMap<String, TextureFlags>,
|
||||
pub fire_animations: Vec<(Rect, crate::sprites::SpriteAnimation)>,
|
||||
/// The default font (bitmap sheets in this atlas, unifont rasterized on demand).
|
||||
pub font: crate::font::Font,
|
||||
@@ -211,7 +235,7 @@ impl HudAtlas {
|
||||
let s = p.to_string_lossy();
|
||||
!s.contains("title/background") && !s.contains("realms") && !s.contains("presets")
|
||||
});
|
||||
let decode_one = |path: &PathBuf| -> Option<(String, u32, u32, Vec<u8>)> {
|
||||
let decode_one = |path: &PathBuf| -> Option<(PackedImage, Option<TextureFlags>)> {
|
||||
let name = path
|
||||
.strip_prefix(&textures)
|
||||
.ok()?
|
||||
@@ -226,11 +250,15 @@ impl HudAtlas {
|
||||
} else {
|
||||
pixels
|
||||
};
|
||||
(w <= 1024 && (h <= 512 || name.starts_with("font/"))).then_some((name, w, h, pixels))
|
||||
let flags = fs::read_to_string(path.with_extension("png.mcmeta"))
|
||||
.ok()
|
||||
.and_then(|json| TextureFlags::from_mcmeta(&json));
|
||||
(w <= 1024 && (h <= 512 || name.starts_with("font/")))
|
||||
.then_some(((name, w, h, pixels), flags))
|
||||
};
|
||||
let threads = std::thread::available_parallelism().map_or(2, |n| n.get());
|
||||
let chunk = files.len().div_ceil(threads).max(1);
|
||||
let mut images: Vec<_> = std::thread::scope(|scope| {
|
||||
let decoded: Vec<_> = std::thread::scope(|scope| {
|
||||
let handles: Vec<_> = files
|
||||
.chunks(chunk)
|
||||
.map(|part| {
|
||||
@@ -242,6 +270,14 @@ impl HudAtlas {
|
||||
.flat_map(|h| h.join().unwrap_or_default())
|
||||
.collect()
|
||||
});
|
||||
let mut flags = HashMap::new();
|
||||
let mut images: Vec<PackedImage> = Vec::with_capacity(decoded.len());
|
||||
for (image, image_flags) in decoded {
|
||||
if let Some(f) = image_flags {
|
||||
flags.insert(image.0.clone(), f);
|
||||
}
|
||||
images.push(image);
|
||||
}
|
||||
if full {
|
||||
images.extend(atlas_sprites(root, &images));
|
||||
}
|
||||
@@ -337,6 +373,7 @@ impl HudAtlas {
|
||||
width,
|
||||
height,
|
||||
sprites,
|
||||
flags,
|
||||
fire_animations,
|
||||
font,
|
||||
white,
|
||||
|
||||
@@ -1,26 +1,53 @@
|
||||
// Full-screen camera effects (screen_effects.rs): clip-space quads sampling
|
||||
// the shared 2D atlas with repeat inside each sprite rectangle.
|
||||
// Full-screen camera effects (screen_effects.rs): clip-space quads reading
|
||||
// one sprite rectangle of the shared 2D atlas as if it were its own texture,
|
||||
// with the sampler vanilla gives it (ReloadableTexture.setSampler: linear
|
||||
// when `blur`, clamp-to-edge when `clamp`, else nearest and repeat).
|
||||
@group(0) @binding(1) var atlas: texture_2d<f32>;
|
||||
@group(0) @binding(2) var atlas_sampler: sampler;
|
||||
struct Out {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
@location(1) @interpolate(flat) rect: vec4<f32>,
|
||||
@location(2) color: vec4<f32>,
|
||||
@location(3) @interpolate(flat) sampling: u32,
|
||||
}
|
||||
@vertex fn vs_screen(@location(0) pos: vec2<f32>, @location(1) uv: vec2<f32>,
|
||||
@location(2) rect: vec4<f32>, @location(3) color: vec4<f32>) -> Out {
|
||||
@location(2) rect: vec4<f32>, @location(3) color: vec4<f32>,
|
||||
@location(4) sampling: u32) -> Out {
|
||||
var out: Out;
|
||||
out.clip = vec4(pos, 0.0, 1.0);
|
||||
out.uv = uv;
|
||||
out.rect = rect;
|
||||
out.color = color;
|
||||
out.sampling = sampling;
|
||||
return out;
|
||||
}
|
||||
const SAMPLE_BLUR: u32 = 1u;
|
||||
const SAMPLE_CLAMP: u32 = 2u;
|
||||
// A texel of the sprite, addressed by repeat or clamp-to-edge. With repeat, `p` is at most one
|
||||
// texel outside the sprite (`sample` reduces the coordinate first), so wrapping is a select: no
|
||||
// integer division, which Intel GPUs emulate (it cost 0.85 ms a frame on the vignette on an HD 620).
|
||||
fn texel(origin: vec2<i32>, size: vec2<i32>, p: vec2<i32>, clamp_edge: bool) -> vec4<f32> {
|
||||
var q = select(select(p, p - size, p >= size), p + size, p < vec2(0));
|
||||
if clamp_edge { q = clamp(p, vec2(0), size - 1); }
|
||||
return textureLoad(atlas, origin + q, 0);
|
||||
}
|
||||
fn sample(in: Out) -> vec4<f32> {
|
||||
let f = fract(in.uv);
|
||||
let uv = in.rect.xy + f * (in.rect.zw - in.rect.xy);
|
||||
return textureSampleLevel(atlas, atlas_sampler, uv, 0.0) * in.color;
|
||||
let dims = vec2<f32>(textureDimensions(atlas));
|
||||
let origin = vec2<i32>(round(in.rect.xy * dims));
|
||||
let size = max(vec2<i32>(round((in.rect.zw - in.rect.xy) * dims)), vec2(1));
|
||||
let clamp_edge = (in.sampling & SAMPLE_CLAMP) != 0u;
|
||||
// Repeat: one period of the sprite, so every texel read is at most one outside it.
|
||||
let p = select(fract(in.uv), in.uv, clamp_edge) * vec2<f32>(size);
|
||||
if (in.sampling & SAMPLE_BLUR) == 0u {
|
||||
return texel(origin, size, vec2<i32>(floor(p)), clamp_edge) * in.color;
|
||||
}
|
||||
// GL_LINEAR: the four texels around the sample point, weighted.
|
||||
let c = p - 0.5;
|
||||
let i = vec2<i32>(floor(c));
|
||||
let t = c - floor(c);
|
||||
let a = mix(texel(origin, size, i, clamp_edge), texel(origin, size, i + vec2(1, 0), clamp_edge), t.x);
|
||||
let b = mix(texel(origin, size, i + vec2(0, 1), clamp_edge), texel(origin, size, i + vec2(1, 1), clamp_edge), t.x);
|
||||
return mix(a, b, t.y) * in.color;
|
||||
}
|
||||
@fragment fn fs_screen(in: Out) -> @location(0) vec4<f32> {
|
||||
let c = sample(in);
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
//! `hud_overlays::camera_overlays` (vignette, pumpkin blur, powder-snow
|
||||
//! outline, nether portal swirl). Positions are clip-space quads; textures
|
||||
//! come from the shared 2D atlas and may repeat.
|
||||
use crate::{hud::Rect, hud_layout::GuiDraw};
|
||||
use crate::{
|
||||
hud::{Rect, TextureFlags},
|
||||
hud_layout::GuiDraw,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -24,6 +27,17 @@ pub struct ScreenVertex {
|
||||
/// Atlas rectangle (u0, v0, u1, v1) of the sprite.
|
||||
pub rect: [f32; 4],
|
||||
pub color: [f32; 4],
|
||||
/// How the sprite is sampled: [`SAMPLE_BLUR`] and [`SAMPLE_CLAMP`].
|
||||
pub sampling: u32,
|
||||
}
|
||||
/// Linear filtering (`TextureMetadataSection.blur`), else nearest.
|
||||
pub const SAMPLE_BLUR: u32 = 1;
|
||||
/// Clamp-to-edge addressing (`TextureMetadataSection.clamp`), else repeat.
|
||||
pub const SAMPLE_CLAMP: u32 = 2;
|
||||
/// The sampling bits of a standalone texture's flags.
|
||||
pub fn sampling(flags: TextureFlags) -> u32 {
|
||||
let bit = |on: bool, bit: u32| if on { bit } else { 0 };
|
||||
bit(flags.blur, SAMPLE_BLUR) | bit(flags.clamp, SAMPLE_CLAMP)
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScreenQuad {
|
||||
@@ -34,6 +48,8 @@ pub struct ScreenQuad {
|
||||
/// What the camera sees this frame.
|
||||
pub struct ScreenInputs<'a> {
|
||||
pub sprites: &'a HashMap<String, Rect>,
|
||||
/// `.png.mcmeta` sampling of the standalone textures among `sprites`.
|
||||
pub flags: &'a HashMap<String, TextureFlags>,
|
||||
pub aspect: f32,
|
||||
pub partial: f32,
|
||||
/// Particle sprite of the view-blocking block around the eye.
|
||||
@@ -92,17 +108,23 @@ impl ScreenEffects {
|
||||
pub fn build(&self, input: &ScreenInputs) -> Vec<ScreenQuad> {
|
||||
let mut out = Vec::new();
|
||||
let sprite = |name: &str| input.sprites.get(name).map(|r| [r.u0, r.v0, r.u1, r.v1]);
|
||||
// Standalone textures (`ReloadableTexture.setSampler`) follow their
|
||||
// metadata; atlas sprites (block overlay, fire, portal) sample
|
||||
// nearest with repeat.
|
||||
let texture_sampling = |name: &str| input.flags.get(name).copied().map_or(0, sampling);
|
||||
let scale = hand_scale(input.aspect);
|
||||
// Screen-effect quads live at view depth 0.5 in the hand projection.
|
||||
let hand_quad = |corners: [[f32; 2]; 4],
|
||||
uv: [[f32; 2]; 4],
|
||||
rect: [f32; 4],
|
||||
color: [f32; 4]| ScreenQuad {
|
||||
color: [f32; 4],
|
||||
sampling: u32| ScreenQuad {
|
||||
vertices: std::array::from_fn(|i| ScreenVertex {
|
||||
pos: [corners[i][0] * scale[0], corners[i][1] * scale[1]],
|
||||
uv: uv[i],
|
||||
rect,
|
||||
color,
|
||||
sampling,
|
||||
}),
|
||||
blend: Blend::Alpha,
|
||||
};
|
||||
@@ -117,6 +139,7 @@ impl ScreenEffects {
|
||||
[[1.0, 1.0], [0.0, 1.0], [0.0, 0.0], [1.0, 0.0]],
|
||||
rect,
|
||||
[c, c, c, 1.0],
|
||||
0,
|
||||
));
|
||||
}
|
||||
if let Some((brightness, yaw, pitch)) = input.water
|
||||
@@ -129,6 +152,7 @@ impl ScreenEffects {
|
||||
uv,
|
||||
rect,
|
||||
[brightness, brightness, brightness, 0.1],
|
||||
texture_sampling("misc/underwater"),
|
||||
));
|
||||
}
|
||||
if input.on_fire
|
||||
@@ -150,11 +174,12 @@ impl ScreenEffects {
|
||||
[[1.0, 1.0], [0.0, 1.0], [0.0, 0.0], [1.0, 0.0]],
|
||||
rect,
|
||||
[1.0, 1.0, 1.0, alpha],
|
||||
0,
|
||||
));
|
||||
}
|
||||
}
|
||||
// Hud camera overlays: GUI-space full-screen blits (v grows downwards).
|
||||
let gui = |rect: [f32; 4], color: [f32; 4], blend: Blend| ScreenQuad {
|
||||
let gui = |rect: [f32; 4], color: [f32; 4], blend: Blend, sampling: u32| ScreenQuad {
|
||||
vertices: std::array::from_fn(|i| {
|
||||
let p = [[-1.0f32, 1.0], [1.0, 1.0], [1.0, -1.0], [-1.0, -1.0]][i];
|
||||
ScreenVertex {
|
||||
@@ -162,6 +187,7 @@ impl ScreenEffects {
|
||||
uv: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]][i],
|
||||
rect,
|
||||
color,
|
||||
sampling,
|
||||
}
|
||||
}),
|
||||
blend,
|
||||
@@ -188,7 +214,7 @@ impl ScreenEffects {
|
||||
Blend::Alpha
|
||||
};
|
||||
if let Some(rect) = sprite(texture) {
|
||||
out.push(gui(rect, rgba(*color), blend));
|
||||
out.push(gui(rect, rgba(*color), blend, texture_sampling(texture)));
|
||||
}
|
||||
}
|
||||
GuiDraw::AtlasSprite {
|
||||
@@ -197,7 +223,7 @@ impl ScreenEffects {
|
||||
..
|
||||
} => {
|
||||
if let Some(rect) = sprite(name) {
|
||||
out.push(gui(rect, rgba(*color), Blend::Alpha));
|
||||
out.push(gui(rect, rgba(*color), Blend::Alpha, 0));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -243,8 +269,10 @@ mod tests {
|
||||
},
|
||||
);
|
||||
let e = ScreenEffects::default();
|
||||
let flags = HashMap::new();
|
||||
let input = ScreenInputs {
|
||||
sprites: &sprites,
|
||||
flags: &flags,
|
||||
aspect: 1.6,
|
||||
partial: 0.0,
|
||||
block_overlay: None,
|
||||
@@ -255,5 +283,69 @@ mod tests {
|
||||
let quads = e.build(&input);
|
||||
assert_eq!(quads.len(), 1);
|
||||
assert_eq!(quads[0].blend, Blend::Vignette);
|
||||
assert_eq!(quads[0].vertices[0].sampling, 0);
|
||||
}
|
||||
#[test]
|
||||
fn standalone_textures_sample_by_their_metadata() {
|
||||
let rect = Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
};
|
||||
let sprites: HashMap<_, _> = ["misc/vignette", "misc/underwater", "block/stone"]
|
||||
.map(|name| (name.to_owned(), rect))
|
||||
.into();
|
||||
let blur = TextureFlags {
|
||||
blur: true,
|
||||
clamp: false,
|
||||
};
|
||||
let clamp = TextureFlags {
|
||||
blur: false,
|
||||
clamp: true,
|
||||
};
|
||||
let flags: HashMap<_, _> = [
|
||||
("misc/vignette".to_owned(), blur),
|
||||
("misc/underwater".to_owned(), clamp),
|
||||
// Atlas sprites ignore the texture section.
|
||||
("block/stone".to_owned(), blur),
|
||||
]
|
||||
.into();
|
||||
let quads = ScreenEffects::default().build(&ScreenInputs {
|
||||
sprites: &sprites,
|
||||
flags: &flags,
|
||||
aspect: 1.6,
|
||||
partial: 0.0,
|
||||
block_overlay: Some("block/stone".to_owned()),
|
||||
water: Some((1.0, 0.0, 0.0)),
|
||||
on_fire: false,
|
||||
gui_overlays: &[crate::hud_overlays::vignette(320, 240, 1.0, 0.0)],
|
||||
});
|
||||
let sampling: Vec<_> = quads.iter().map(|q| q.vertices[0].sampling).collect();
|
||||
assert_eq!(sampling, [0, SAMPLE_CLAMP, SAMPLE_BLUR]);
|
||||
}
|
||||
#[test]
|
||||
fn mcmeta_texture_section() {
|
||||
let parse = TextureFlags::from_mcmeta;
|
||||
assert_eq!(
|
||||
parse(r#"{"texture":{"blur":true}}"#),
|
||||
Some(TextureFlags {
|
||||
blur: true,
|
||||
clamp: false
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse(r#"{"texture":{"clamp":true,"mipmap_strategy":"mean"}}"#),
|
||||
Some(TextureFlags {
|
||||
blur: false,
|
||||
clamp: true
|
||||
})
|
||||
);
|
||||
// Defaults (false, false) or no section: the default sampler.
|
||||
assert_eq!(parse(r#"{"texture":{"blur":false}}"#), None);
|
||||
assert_eq!(parse(r#"{"animation":{}}"#), None);
|
||||
assert_eq!(parse("not json"), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user