Shader packs: gbuffers_hand_water for translucent held items

The hand's quads from a translucent sheet (stained glass, ice...) go after the opaque ones in
its buffer and draw after deferred with gbuffers_hand_water (or its fallbacks: gbuffers_hand,
textured_lit, textured, basic), blended, as translucent terrain does; through gbuffers_hand
they came out solid.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
SenjinandClaude Opus 5.5 committed 2026-11-06 18:04:31 +01:00
1 parent 41f822b659
commit 7b3be8d0b9
3 files changed
+272 -14

No files matched your search

+112 -11
View File
@@ -54,8 +54,12 @@ pub struct PackHand {
slots: wgpu::Buffer,
pub slot_group: wgpu::BindGroup,
stride: u64,
/// This frame's runs: first quad, quads, and the dynamic offset of their slot.
/// This frame's runs: first quad, quads, and the dynamic offset of their slot. The opaque
/// quads come first in the buffer, then the translucent ones (`translucent_runs`), which a
/// pack's `gbuffers_hand_water` draws after deferred.
pub runs: Vec<(u32, u32, u32)>,
pub translucent_runs: Vec<(u32, u32, u32)>,
order: Vec<Vertex>,
}
impl PackHand {
@@ -120,6 +124,8 @@ impl PackHand {
slot_group,
stride,
runs: Vec::new(),
translucent_runs: Vec::new(),
order: Vec::new(),
}
}
@@ -166,17 +172,25 @@ impl PackHand {
) {
self.quads = (hand.len() / 4) as u32;
self.runs.clear();
self.translucent_runs.clear();
if self.quads == 0 {
return;
}
let split = split(hand, items);
self.order = split.order;
let mut contents = Vec::new();
for (start, quads, id) in runs(self.quads, items) {
let slot = (self.runs.len() as u64).min(SLOTS - 1);
if slot as usize == self.runs.len() {
contents.extend_from_slice(&draw_slot(id));
for (i, &(first, quads, id, translucent)) in split.runs.iter().enumerate() {
let slot = (i as u64).min(SLOTS - 1);
if slot == i as u64 {
contents.extend_from_slice(&draw_slot(id, translucent));
contents.resize(((slot + 1) * self.stride) as usize, 0);
}
self.runs.push((start, quads, (slot * self.stride) as u32));
let run = (first, quads, (slot * self.stride) as u32);
if translucent {
self.translucent_runs.push(run);
} else {
self.runs.push(run);
}
}
queue.write_buffer(&self.slots, 0, &contents);
if self.quads > self.capacity {
@@ -185,7 +199,7 @@ impl PackHand {
Self::vertex_buffer(device, &self.layout, &self.uniform, self.capacity);
self.indices = super::pack_entities::quad_indices(device, self.capacity);
}
queue.write_buffer(&self.vertices, 0, bytemuck::cast_slice(hand));
queue.write_buffer(&self.vertices, 0, bytemuck::cast_slice(&self.order));
let matrices = [projection, projection.inverse()].map(|m| m.to_cols_array());
queue.write_buffer(&self.uniform, 0, bytemuck::cast_slice(&matrices));
}
@@ -198,11 +212,62 @@ impl PackHand {
/// The hand's runs a frame keeps slots for.
const SLOTS: u64 = 8;
/// `HzDraw` of a hand run: `renderStage` the solid hand's, `blockEntityId` -1 as outside block
/// entities, and `currentRenderedItemId`.
fn draw_slot(id: i32) -> [u8; SLOT as usize] {
/// The hand's quads reordered, opaque first and translucent after (each in their order), and
/// their runs: first quad, quads, `currentRenderedItemId`, translucent.
struct Split {
order: Vec<Vertex>,
runs: Vec<(u32, u32, i32, bool)>,
}
fn split(hand: &[Vertex], items: &[(u32, i32)]) -> Split {
let quads = (hand.len() / 4) as u32;
let mut ids = vec![0; quads as usize];
for (start, n, id) in runs(quads, items) {
ids[start as usize..(start + n) as usize].fill(id);
}
let kind = |q: usize| translucent_quad(&hand[q * 4..q * 4 + 4]);
let opaque: Vec<usize> = (0..quads as usize).filter(|&q| !kind(q)).collect();
let clear: Vec<usize> = (0..quads as usize).filter(|&q| kind(q)).collect();
let mut out = Split {
order: Vec::with_capacity(hand.len()),
runs: Vec::new(),
};
for &q in opaque.iter().chain(&clear) {
out.order.extend_from_slice(&hand[q * 4..q * 4 + 4]);
}
for (translucent, list, base) in [(false, &opaque, 0), (true, &clear, opaque.len())] {
let mut i = 0;
while i < list.len() {
let id = ids[list[i]];
let mut n = 1;
while i + n < list.len() && ids[list[i + n]] == id {
n += 1;
}
out.runs
.push(((base + i) as u32, n as u32, id, translucent));
i += n;
}
}
out
}
/// A held item's quad drawn from a translucent sheet (`item_render`: its overlay layer).
fn translucent_quad(quad: &[Vertex]) -> bool {
matches!(
quad[0].layer & 0xffff,
super::TRANSLUCENT_ITEM_OVERLAY | super::TRANSLUCENT_GLINT_ITEM_OVERLAY
)
}
/// `HzDraw` of a hand run: `renderStage` the solid or translucent hand's, `blockEntityId` -1 as
/// outside block entities, and `currentRenderedItemId`.
fn draw_slot(id: i32, translucent: bool) -> [u8; SLOT as usize] {
let mut bytes = [0u8; SLOT as usize];
let stage = horizon_shaders::render_stage("HAND_SOLID") as i32;
let stage = horizon_shaders::render_stage(if translucent {
"HAND_TRANSLUCENT"
} else {
"HAND_SOLID"
}) as i32;
bytes[0..4].copy_from_slice(&stage.to_le_bytes());
bytes[8..12].copy_from_slice(&(-1i32).to_le_bytes());
bytes[12..16].copy_from_slice(&id.to_le_bytes());
@@ -233,6 +298,42 @@ fn runs(quads: u32, items: &[(u32, i32)]) -> Vec<(u32, u32, i32)> {
mod tests {
use super::*;
/// The arm, then an item whose middle quad is translucent, then a second item: the opaque
/// quads keep their order first, the translucent one comes after them, and each run keeps
/// its item's id.
#[test]
fn translucent_quads_go_after_the_opaque_ones() {
let quad = |layer: u32, tag: f32| {
[Vertex {
layer,
pos: [tag, 0.0, 0.0],
..bytemuck::Zeroable::zeroed()
}; 4]
};
let glass = super::super::TRANSLUCENT_ITEM_OVERLAY;
let hand: Vec<Vertex> = [
quad(0, 0.),
quad(0, 1.),
quad(glass, 2.),
quad(0, 3.),
quad(0, 4.),
]
.concat();
// The arm is quad 0; item 7 quads 1..4; item 9 quad 4.
let split = split(&hand, &[(4, 7), (16, 9)]);
let order: Vec<f32> = split.order.chunks(4).map(|q| q[0].pos[0]).collect();
assert_eq!(order, [0., 1., 3., 4., 2.]);
assert_eq!(
split.runs,
[
(0, 1, 0, false),
(1, 2, 7, false),
(3, 1, 9, false),
(4, 1, 7, true)
]
);
}
/// Each item's quads draw with its id; geometry before the first item is the arm's (0).
#[test]
fn runs_split_the_hand_by_item() {
@@ -593,6 +593,8 @@ pub struct PackPost {
/// What draws the first-person hand (`gbuffers_hand` or its fallback), after the opaque
/// world, and its vertices.
hand: Option<(Pass, super::pack_hand::PackHand)>,
/// `gbuffers_hand_water` (or its fallback): the hand's translucent quads, after deferred.
hand_water: Option<Pass>,
/// The pre-translucent copy: `depthtex1`, or `depthtex2` without a shader hand.
depth1: bool,
/// A program after the hand reads `depthtex2`: it is copied before the hand.
@@ -2465,6 +2467,20 @@ impl PackPost {
};
build(p, Some(&g)).map(|pass| (pass, geometry))
});
// Translucent held items, drawn from the hand's own buffer.
let hand_water = post.hand_water.and_then(|p| {
let (_, geometry) = hand.as_ref()?;
let g = Geometry {
buffers: &entity_buffers,
table: Some(&geometry.layout),
depth_write: true,
atlas: world.celestial.clone(),
material_maps: false,
ids: None,
cull: false,
};
build(p, Some(&g))
});
let weather = post
.weather
.zip(weather_geometry.as_ref())
@@ -2585,6 +2601,9 @@ impl PackPost {
if let Some((h, _)) = &hand {
notes.push(format!("the hand draws with {}", h.name));
}
if let Some(h) = &hand_water {
notes.push(format!("translucent held items draw with {}", h.name));
}
if let Some([w, _]) = &weather {
notes.push(format!("rain and snow draw with {}", w.name));
}
@@ -2638,6 +2657,7 @@ impl PackPost {
.chain(&lines)
.chain(weather.iter().flatten())
.chain(hand.iter().map(|h| &h.0))
.chain(&hand_water)
};
let shadow_targets = shadow_formats.map(|f| {
let colors = every_pass().fold(0u16, |m, p| {
@@ -2691,6 +2711,7 @@ impl PackPost {
.chain(&water)
.chain(weather.iter().take(1).flatten())
.chain(hand.iter().map(|h| &h.0))
.chain(&hand_water)
.find(touched)
.map(|_| true)
.or_else(|| passes.iter().find(touched).map(|_| true));
@@ -2870,6 +2891,7 @@ impl PackPost {
weather_draw: None,
weather_generation: 0,
hand,
hand_water,
depth1,
depth2,
shadow_targets,
@@ -3838,8 +3860,14 @@ impl PackPost {
rp.set_bind_group(1, &geometry.group, &[]);
rp.set_vertex_buffer(0, geometry.vertices());
rp.set_index_buffer(geometry.indices.slice(..), wgpu::IndexFormat::Uint32);
// The arm and each held item: their own `currentRenderedItemId`.
for &(first, quads, offset) in &geometry.runs {
// The arm and each held item: their own `currentRenderedItemId`. Translucent
// quads wait for `gbuffers_hand_water` when the pack has one.
let late = self.hand_water.is_some();
let runs = geometry
.runs
.iter()
.chain(geometry.translucent_runs.iter().filter(|_| !late));
for &(first, quads, offset) in runs {
if pass.draw {
rp.set_bind_group(2, &geometry.slot_group, &[offset]);
}
@@ -3966,9 +3994,53 @@ impl PackPost {
terrain,
);
self.draw_entities(device, queue, encoder, size, true);
self.draw_hand_water(device, queue, encoder, size);
self.draw_weather(device, queue, encoder, size);
}
/// The hand's translucent quads (uploaded by `draw_hand` this frame), with
/// `gbuffers_hand_water`.
fn draw_hand_water(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
size: [u32; 2],
) {
let Some(mut pass) = self.hand_water.take() else {
return;
};
let hand = self.hand.take();
if let Some((_, geometry)) = hand
.as_ref()
.filter(|(_, g)| g.quads > 0 && !g.translucent_runs.is_empty())
{
let group = self.bind(device, queue, encoder, &mut pass, size);
{
let mut rp = self.begin(encoder, &pass);
rp.set_pipeline(pass.render());
rp.set_bind_group(0, &group, &[]);
if pass.buffers
&& let Some(g) = &self.buffers.group
{
rp.set_bind_group(4, g, &[]);
}
rp.set_bind_group(1, &geometry.group, &[]);
rp.set_vertex_buffer(0, geometry.vertices());
rp.set_index_buffer(geometry.indices.slice(..), wgpu::IndexFormat::Uint32);
for &(first, quads, offset) in &geometry.translucent_runs {
if pass.draw {
rp.set_bind_group(2, &geometry.slot_group, &[offset]);
}
rp.draw_indexed(first * 6..(first + quads) * 6, 0, 0..1);
}
}
self.mips_valid &= !pass.written;
}
self.hand = hand;
self.hand_water = Some(pass);
}
/// The rain, then the snow, `set_weather` gave for this frame, with `gbuffers_weather`.
fn draw_weather(
&mut self,
@@ -4144,6 +4216,7 @@ impl PackPost {
.chain(&mut self.lines)
.chain(self.weather.iter_mut().flatten())
.chain(self.hand.iter_mut().map(|h| &mut h.0))
.chain(&mut self.hand_water)
.chain(&mut self.shadow_terrain)
.chain(&mut self.shadow_water)
.chain(&mut self.shadow_comp);
+85 -1
View File
@@ -185,6 +185,15 @@ const HAND: [&str; 4] = [
"gbuffers_basic",
];
/// `gbuffers_hand_water` and its fallbacks (2.3): translucent held items, after deferred.
const HAND_WATER: [&str; 5] = [
"gbuffers_hand_water",
"gbuffers_hand",
"gbuffers_textured_lit",
"gbuffers_textured",
"gbuffers_basic",
];
/// `gbuffers_weather` and its fallbacks (2.3).
const WEATHER: [&str; 4] = [
"gbuffers_weather",
@@ -460,6 +469,8 @@ pub struct Post {
pub rain_depth: bool,
/// What draws the first-person hand and held items.
pub hand: Option<PostProgram>,
/// What draws their translucent parts (stained glass, ice...), after deferred and blended.
pub hand_water: Option<PostProgram>,
/// `clouds=off`: no clouds at all, neither the game's nor the pack's program for them.
pub clouds_off: bool,
/// Some program reads a shadow buffer, or `shadow.enabled=true`: the shadow pass runs (2.5).
@@ -664,6 +675,7 @@ pub fn load_post(
.get("rain.depth")
.is_none_or(|v| v.trim() != "false"),
hand: None,
hand_water: None,
clouds_off: false,
shadow_pass: false,
shadow: None,
@@ -877,6 +889,15 @@ pub fn load_post(
p.blend = blend(properties, &p.name).flatten();
p
});
// Translucent held items draw after deferred as translucent terrain does, and blend.
let hand_water = inputs
.hand
.as_ref()
.and_then(|i| gbuffers(&HAND_WATER, Group::Translucent, i))
.map(|mut p| {
p.blend = blend(properties, &p.name).unwrap_or(Some(TRANSLUCENT_BLEND));
p
});
if let Some(terrain) = &inputs.terrain {
// Cutout terrain's alpha test, unless `alphaTest.*` says otherwise (3.2): alpha above
// 0.5, which `alphaTestRef` reads. Iris 1.11.7 with probe packs of ours (#9307), through
@@ -986,6 +1007,7 @@ pub fn load_post(
loader.post.lines = lines;
loader.post.weather = weather;
loader.post.hand = hand;
loader.post.hand_water = hand_water;
let flag = |key: &str| properties.get(key).map(|v| v.trim() == "true");
loader.post.translucent_late = flag("separateEntityDraws").unwrap_or(false);
// `after` when the pack has a deferred program, else `mixed`; ours are all translucent, so
@@ -1031,6 +1053,7 @@ fn load_shadow(
&post.clouds,
&post.weather,
&post.hand,
&post.hand_water,
];
let reads = (post.programs.iter())
.chain(gbuffers.into_iter().flatten())
@@ -2909,6 +2932,57 @@ blend.composite1.colortex3 = ONE ONE ONE ONE
);
}
/// A pack's own `gbuffers_hand_water` draws the translucent held items: after deferred,
/// blended, beside its opaque `gbuffers_hand`.
#[test]
fn translucent_held_items_draw_with_gbuffers_hand_water() {
let vsh = "#version 120\nvarying vec2 uv;\nvoid main() {\n gl_Position = ftransform();\n uv = gl_MultiTexCoord0.xy;\n}\n";
let fsh = "#version 120\nuniform sampler2D texture;\nvarying vec2 uv;\nvoid main() {\n gl_FragData[0] = texture2D(texture, uv);\n}\n";
let pack = Pack::from_zip_bytes(
"P.zip",
zip(&[
("shaders/gbuffers_hand.vsh", vsh),
("shaders/gbuffers_hand.fsh", fsh),
("shaders/gbuffers_hand_water.vsh", vsh),
("shaders/gbuffers_hand_water.fsh", fsh),
]),
)
.unwrap();
let hand = crate::GbuffersInputs {
declarations: "layout(location = 0) in vec3 hz_v_pos;".into(),
body: " hz_Vertex = vec4(hz_v_pos, 1.0);\n hz_Color = vec4(1.0);\n".into(),
..Default::default()
};
let inputs = GameInputs {
hand: Some(hand),
..Default::default()
};
let post = load_post(
&pack,
"",
&Properties::default(),
&Macros::standard(),
&inputs,
);
let (opaque, translucent) = (post.hand.unwrap(), post.hand_water.unwrap());
assert_eq!(
(opaque.name.as_str(), opaque.blend),
("gbuffers_hand", None)
);
assert_eq!(
(
translucent.name.as_str(),
translucent.group,
translucent.blend
),
(
"gbuffers_hand_water",
Group::Translucent,
Some(TRANSLUCENT_BLEND)
)
);
}
#[test]
fn water_falls_back_to_terrain_and_blends() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/packs/terrain-test");
@@ -2984,12 +3058,22 @@ blend.composite1.colortex3 = ONE ONE ONE ONE
(clouds.name.as_str(), clouds.blend),
("gbuffers_textured", Some(TRANSLUCENT_BLEND))
);
// The hand falls back to gbuffers_textured and does not blend.
// The hand falls back to gbuffers_textured and does not blend; its translucent items
// fall back to the same program, after deferred and blended.
let hand = post.hand.as_ref().unwrap();
assert_eq!(
(hand.name.as_str(), hand.group, hand.blend),
("gbuffers_textured", Group::Gbuffers, None)
);
let hand_water = post.hand_water.as_ref().unwrap();
assert_eq!(
(hand_water.name.as_str(), hand_water.group, hand_water.blend),
(
"gbuffers_textured",
Group::Translucent,
Some(TRANSLUCENT_BLEND)
)
);
// Weather falls back to gbuffers_textured, after deferred, blended.
let weather = post.weather.as_ref().unwrap();
assert_eq!(