diff --git a/crates/horizon-client-render/src/gfx/pack_hand.rs b/crates/horizon-client-render/src/gfx/pack_hand.rs index dfe70ce8d2..3fd69da32f 100644 --- a/crates/horizon-client-render/src/gfx/pack_hand.rs +++ b/crates/horizon-client-render/src/gfx/pack_hand.rs @@ -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, } 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, + 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 = (0..quads as usize).filter(|&q| !kind(q)).collect(); + let clear: Vec = (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 = [ + 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 = 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() { diff --git a/crates/horizon-client-render/src/gfx/pack_post.rs b/crates/horizon-client-render/src/gfx/pack_post.rs index 411204e903..d784f501a4 100644 --- a/crates/horizon-client-render/src/gfx/pack_post.rs +++ b/crates/horizon-client-render/src/gfx/pack_post.rs @@ -614,6 +614,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, /// 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. @@ -2586,6 +2588,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()) @@ -2706,6 +2722,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)); } @@ -2759,6 +2778,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| { @@ -2812,6 +2832,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)); @@ -2991,6 +3012,7 @@ impl PackPost { weather_draw: None, weather_generation: 0, hand, + hand_water, depth1, depth2, shadow_targets, @@ -3969,8 +3991,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]); } @@ -4097,9 +4125,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, @@ -4275,6 +4347,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); diff --git a/crates/horizon-shaders/src/post.rs b/crates/horizon-shaders/src/post.rs index 5590bfcc9d..71ab2af0b3 100644 --- a/crates/horizon-shaders/src/post.rs +++ b/crates/horizon-shaders/src/post.rs @@ -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", @@ -467,6 +476,8 @@ pub struct Post { pub rain_depth: bool, /// What draws the first-person hand and held items. pub hand: Option, + /// What draws their translucent parts (stained glass, ice...), after deferred and blended. + pub hand_water: Option, /// `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). @@ -671,6 +682,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, @@ -889,6 +901,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 @@ -998,6 +1019,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 @@ -1043,6 +1065,7 @@ fn load_shadow( &post.clouds, &post.weather, &post.hand, + &post.hand_water, ]; // A program's compute stages count as its other stages do: Iris 1.11.7 drew the shadow map // for a probe pack of ours whose only reader of it was `prepare.csh` (#10490). @@ -2986,6 +3009,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"); @@ -3072,12 +3146,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!(