diff --git a/pomme-client/build.rs b/pomme-client/build.rs index 07ed8e321..5efca6a9c 100644 --- a/pomme-client/build.rs +++ b/pomme-client/build.rs @@ -130,6 +130,7 @@ fn main() { ("item_entity.vert", shaderc::ShaderKind::Vertex), ("item_entity_world.vert", shaderc::ShaderKind::Vertex), ("item_entity.frag", shaderc::ShaderKind::Fragment), + ("item_outline.vert", shaderc::ShaderKind::Vertex), ("weather.vert", shaderc::ShaderKind::Vertex), ("weather.frag", shaderc::ShaderKind::Fragment), ("particle.vert", shaderc::ShaderKind::Vertex), diff --git a/pomme-client/src/app/core.rs b/pomme-client/src/app/core.rs index ca2fbd6e6..3c890e676 100644 --- a/pomme-client/src/app/core.rs +++ b/pomme-client/src/app/core.rs @@ -2228,6 +2228,7 @@ impl AppCore { color, fill_color, collision_rule, + see_friendly_invisibles, members, } => { game.scoreboard.set_team( @@ -2238,6 +2239,7 @@ impl AppCore { color, fill_color, collision_rule, + see_friendly_invisibles, members, ); } @@ -2517,7 +2519,8 @@ impl AppCore { } } if entity_type == azalea_registry::builtin::EntityKind::Item { - game.item_entity_store.spawn_item(id, position, velocity); + game.item_entity_store + .spawn_item(id, uuid, position, velocity); } } NetworkEvent::EntityMoved { diff --git a/pomme-client/src/app/phases/in_game.rs b/pomme-client/src/app/phases/in_game.rs index d29f8f2d3..1d29a130f 100644 --- a/pomme-client/src/app/phases/in_game.rs +++ b/pomme-client/src/app/phases/in_game.rs @@ -3858,12 +3858,26 @@ pub fn update_game( (pos, stage, state) }); - let spectator_outlines = crate::player::is_spectator(game.player.game_mode) - && core.input.performing_action(Action::SpectatorOutlines); - let outline = |glowing: bool, is_player: bool, uuid, player_uuid| { - use crate::entity::glowing::{appears_glowing, outline_color}; + use crate::entity::glowing::{appears_glowing, outline_color, scoreboard_name}; + use crate::renderer::pipelines::entity_renderer::Visibility; + let spectating = crate::player::is_spectator(game.player.game_mode); + let spectator_outlines = spectating && core.input.performing_action(Action::SpectatorOutlines); + let viewer = scoreboard_name(&game.tab_list, core.user.uuid, Some(core.user.uuid)); + // Both take the entity's scoreboard name lazily: it's only needed when the + // entity glows or is invisible. + let outline = |glowing: bool, is_player: bool, name: &dyn Fn() -> String| { appears_glowing(glowing, is_player, spectator_outlines) - .then(|| outline_color(&game.scoreboard, &game.tab_list, uuid, player_uuid)) + .then(|| outline_color(&game.scoreboard, &name())) + }; + // Vanilla `Entity.isInvisibleTo`, against the local player. + let visibility = |invisible: bool, name: &dyn Fn() -> String| { + if !invisible { + Visibility::Visible + } else if spectating || game.scoreboard.sees_friendly_invisible(&viewer, &name()) { + Visibility::Seen + } else { + Visibility::Hidden + } }; let mut entity_renders: Vec = if benchmark_running { @@ -3883,14 +3897,15 @@ pub fn update_game( let is_sleeping = e.sleeping_pos.is_some(); let (radius, strength) = crate::entity::shadow::of_living(e); + let name = || scoreboard_name(&game.tab_list, e.uuid, e.player_uuid); EntityRenderInfo { position: interp_pos + extras.render_offset, outline_color: outline( e.is_glowing, e.entity_type == EntityKind::Player, - e.uuid, - e.player_uuid, + &name, ), + visibility: visibility(e.is_invisible, &name), head_y_rot_deg: lerp_angle( e.prev_head_y_rot_deg, e.head_y_rot_deg, @@ -3988,7 +4003,8 @@ pub fn update_game( partial_tick, ); - let (radius, strength) = crate::entity::shadow::of_player(&game.player.attributes); + let (radius, strength) = + crate::entity::shadow::of_player(&game.player.attributes, game.player.invisible); entity_renders.push(EntityRenderInfo { position: interp_pos, head_y_rot_deg: interp_y_rot_deg, @@ -4007,12 +4023,8 @@ pub fn update_game( .min(1.0), entity_kind: EntityKind::Player, player_uuid: Some(core.user.uuid), - outline_color: outline( - game.player.glowing, - true, - core.user.uuid, - Some(core.user.uuid), - ), + outline_color: outline(game.player.glowing, true, &|| viewer.clone()), + visibility: visibility(game.player.invisible, &|| viewer.clone()), armor: armor_render_infos(game.player.inventory.armor_slots()), has_red_overlay: has_red_overlay(game.player.hurt_time, game.player.death_time), death_time: render_death_time(game.player.death_time, partial_tick), @@ -4054,6 +4066,11 @@ pub fn update_game( *gfx.renderer.camera_pivot_position(), gfx.renderer.camera_anchor(), partial_tick, + |item| { + outline(item.is_glowing, false, &|| { + scoreboard_name(&game.tab_list, item.uuid, None) + }) + }, ) }; @@ -4554,6 +4571,7 @@ fn emit_item_copies( z_size: f32, light: f32, nether_lighting: bool, + outline_color: Option, ) { use crate::renderer::pipelines::item_entity::ItemRenderInfo; use crate::util::JavaRandom; @@ -4575,6 +4593,7 @@ fn emit_item_copies( let mut push = |copy_offset: glam::Mat4| { infos.push(ItemRenderInfo { item_name: name.clone(), + outline_color, tints: tints.clone(), model_matrix: base * copy_offset * ground_transform, light, @@ -4685,6 +4704,7 @@ fn shadow_casters( entities.chain(items).collect() } +#[allow(clippy::too_many_arguments)] fn build_item_render_infos( entity_store: &crate::entity::ItemEntityStore, chunk_store: &ChunkStore, @@ -4693,6 +4713,7 @@ fn build_item_render_infos( camera_pos: glam::DVec3, anchor: glam::DVec3, partial_tick: f32, + outline: impl Fn(&crate::entity::ItemEntity) -> Option, ) -> Vec { let mut infos = Vec::new(); let nether_lighting = cardinal_light == CardinalLightType::Nether; @@ -4717,6 +4738,7 @@ fn build_item_render_infos( z_size, light, nether_lighting, + outline(item), ); } @@ -4742,6 +4764,8 @@ fn build_item_render_infos( z_size, light, nether_lighting, + // Vanilla's `ItemPickupParticle` clears its outline. + None, ); } diff --git a/pomme-client/src/entity/glowing.rs b/pomme-client/src/entity/glowing.rs index 709b54e12..02f1db888 100644 --- a/pomme-client/src/entity/glowing.rs +++ b/pomme-client/src/entity/glowing.rs @@ -11,20 +11,18 @@ pub fn appears_glowing(glowing: bool, is_player: bool, spectator_outlines: bool) glowing || spectator_outlines && is_player } -/// Vanilla `Entity.getTeamColor`: the colour of the team the entity's -/// scoreboard name belongs to (a player's name, anyone else's UUID string, -/// `getScoreboardName`), white without one. -pub fn outline_color( - scoreboard: &Scoreboard, - tab_list: &TabList, - uuid: Uuid, - player_uuid: Option, -) -> u32 { - let name = match player_uuid.and_then(|id| tab_list.players.get(&id)) { +/// Vanilla `getScoreboardName`: a player's name, anyone else's UUID string. +pub fn scoreboard_name(tab_list: &TabList, uuid: Uuid, player_uuid: Option) -> String { + match player_uuid.and_then(|id| tab_list.players.get(&id)) { Some(player) => player.name.clone(), None => uuid.to_string(), - }; - scoreboard.member_team_color(&name).unwrap_or(0xFFFFFF) + } +} + +/// Vanilla `Entity.getTeamColor`: the colour of the team the entity's +/// scoreboard name belongs to, white without one. +pub fn outline_color(scoreboard: &Scoreboard, name: &str) -> u32 { + scoreboard.member_team_color(name).unwrap_or(0xFFFFFF) } #[cfg(test)] @@ -41,12 +39,15 @@ mod tests { #[test] fn outline_color_falls_back_to_white() { - let color = outline_color( - &Scoreboard::default(), - &TabList::default(), - Uuid::nil(), - None, + assert_eq!(outline_color(&Scoreboard::default(), "anyone"), 0xFFFFFF); + } + + #[test] + fn non_players_score_by_uuid() { + let uuid = Uuid::from_u128(7); + assert_eq!( + scoreboard_name(&TabList::default(), uuid, None), + uuid.to_string() ); - assert_eq!(color, 0xFFFFFF); } } diff --git a/pomme-client/src/entity/mod.rs b/pomme-client/src/entity/mod.rs index 13b6ee91a..987f3433a 100644 --- a/pomme-client/src/entity/mod.rs +++ b/pomme-client/src/entity/mod.rs @@ -27,6 +27,8 @@ use crate::world::chunk::ChunkStore; /// `Entity.FLAG_GLOWING` (bit 6) of the shared entity flags byte (index 0). const SHARED_FLAG_GLOWING: u8 = 0x40; +/// `Entity.FLAG_INVISIBLE` (bit 5). +const SHARED_FLAG_INVISIBLE: u8 = 0x20; /// Whether a shared entity flags byte is glowing: the server's /// `isCurrentlyGlowing`, from the `Glowing` tag or the glowing effect. @@ -34,6 +36,11 @@ pub fn shared_flags_glowing(flags: u8) -> bool { flags & SHARED_FLAG_GLOWING != 0 } +/// Whether a shared entity flags byte is invisible (vanilla `isInvisible`). +pub fn shared_flags_invisible(flags: u8) -> bool { + flags & SHARED_FLAG_INVISIBLE != 0 +} + /// A scalar synched-entity-data value, forwarded raw from the wire; /// [`EntityStore::apply_entity_data`] gives it meaning per (kind, index). #[derive(Clone, Copy, Debug, PartialEq)] @@ -866,6 +873,9 @@ impl LivingEntity { } pub struct ItemEntity { + /// As a string, its scoreboard name (vanilla `getScoreboardName`), which + /// its team colour is keyed by. + pub uuid: uuid::Uuid, pub position: Position, pub prev_position: Position, pub item_name: String, @@ -928,12 +938,13 @@ impl ItemEntityStore { } } - pub fn spawn_item(&mut self, id: i32, position: Position, velocity: DVec3) { + pub fn spawn_item(&mut self, id: i32, uuid: uuid::Uuid, position: Position, velocity: DVec3) { let bob_offset = ((id as u32).wrapping_mul(2654435761)) as f32 / u32::MAX as f32 * std::f32::consts::TAU; self.items.insert( id, ItemEntity { + uuid, position, prev_position: position, item_name: String::new(), @@ -1260,10 +1271,10 @@ impl EntityStore { let kind = entity.entity_type; let index = normalize_player_index(kind, normalize_ageable_index(kind, index)); match (kind, index, value) { - // Shared entity flags byte: bit 0x08 = sprinting, 0x20 = invisible. + // Shared entity flags byte: bit 0x08 = sprinting. (_, 0, Byte(f)) => { entity.is_sprinting = f & 0x08 != 0; - entity.is_invisible = f & 0x20 != 0; + entity.is_invisible = shared_flags_invisible(f); entity.is_glowing = shared_flags_glowing(f); } (_, 9, Float(h)) => entity.health = h, @@ -1937,7 +1948,7 @@ mod tests { assert!(!store.living[&1].is_glowing); let mut items = ItemEntityStore::new(); - items.spawn_item(2, Position::default(), DVec3::ZERO); + items.spawn_item(2, uuid::Uuid::nil(), Position::default(), DVec3::ZERO); items.set_shared_flags(2, SHARED_FLAG_GLOWING); assert!(items.items[&2].is_glowing); } diff --git a/pomme-client/src/entity/shadow.rs b/pomme-client/src/entity/shadow.rs index f434d2e20..1efc887b9 100644 --- a/pomme-client/src/entity/shadow.rs +++ b/pomme-client/src/entity/shadow.rs @@ -18,29 +18,35 @@ pub struct ShadowInputs { pub puff_state: u8, } -/// A tracked mob's or player's shadow; none while it's invisible. +/// A tracked mob's or player's shadow. pub fn of_living(entity: &LivingEntity) -> (f32, f32) { - if entity.is_invisible { - return (0.0, 0.0); - } let inputs = ShadowInputs { scale: scale(&entity.attributes), is_baby: entity.is_baby, slime_size: entity.slime_size, puff_state: entity.puff_state, }; - shadow(entity.entity_type, &inputs) + unless_invisible(entity.is_invisible, entity.entity_type, &inputs) } /// The local player's shadow, from its own attributes. -pub fn of_player(attributes: &AttributeMap) -> (f32, f32) { +pub fn of_player(attributes: &AttributeMap, invisible: bool) -> (f32, f32) { let inputs = ShadowInputs { scale: scale(attributes), is_baby: false, slime_size: 0, puff_state: 0, }; - shadow(EntityKind::Player, &inputs) + unless_invisible(invisible, EntityKind::Player, &inputs) +} + +/// Vanilla `extractShadow`: no shadow while the entity is invisible. +fn unless_invisible(invisible: bool, kind: EntityKind, inputs: &ShadowInputs) -> (f32, f32) { + if invisible { + (0.0, 0.0) + } else { + shadow(kind, inputs) + } } /// Vanilla `getScale`: the `scale` attribute. diff --git a/pomme-client/src/net/mod.rs b/pomme-client/src/net/mod.rs index 796494a2d..52d8ef6f2 100644 --- a/pomme-client/src/net/mod.rs +++ b/pomme-client/src/net/mod.rs @@ -327,6 +327,7 @@ pub enum NetworkEvent { color: [f32; 4], fill_color: Option, collision_rule: crate::ui::hud::CollisionRule, + see_friendly_invisibles: bool, members: Option>, }, ScoreboardTeamMembers { diff --git a/pomme-client/src/net/scoreboard_wire.rs b/pomme-client/src/net/scoreboard_wire.rs index d24ac2736..18262b1dc 100644 --- a/pomme-client/src/net/scoreboard_wire.rs +++ b/pomme-client/src/net/scoreboard_wire.rs @@ -113,6 +113,8 @@ fn team( color, fill_color, collision_rule: collision_rule(parameters.collision_rule), + // `PlayerTeam.unpackOptions`: bit 2. + see_friendly_invisibles: parameters.options & 2 != 0, members, }) } diff --git a/pomme-client/src/player/mod.rs b/pomme-client/src/player/mod.rs index 1339781d8..0fd50b487 100644 --- a/pomme-client/src/player/mod.rs +++ b/pomme-client/src/player/mod.rs @@ -227,6 +227,8 @@ pub struct LocalPlayer { pub swimming: bool, /// The shared glowing flag. pub glowing: bool, + /// The shared invisible flag. + pub invisible: bool, /// The physical SWIMMING pose, chosen by `Player.updatePlayerPose`. pub swimming_pose: bool, pub air_supply: i32, @@ -329,6 +331,7 @@ impl LocalPlayer { under_water: false, swimming: false, glowing: false, + invisible: false, swimming_pose: false, air_supply: MAX_AIR_SUPPLY, portal_effect_intensity: 0.0, @@ -438,14 +441,15 @@ impl LocalPlayer { } } - /// The shared entity flags (bit 3 sprinting, bit 4 swimming, bit 6 - /// glowing) the server sets on the local player. + /// The shared entity flags (bit 3 sprinting, bit 4 swimming, bit 5 + /// invisible, bit 6 glowing) the server sets on the local player. // TODO: vanilla's sprint speed modifier only follows a local // `setSprinting`; this server flag also drives pomme's movement speed. pub fn sync_shared_flags(&mut self, flags: u8) { self.sprinting = flags & 0x08 != 0; self.swimming = flags & 0x10 != 0; self.glowing = crate::entity::shared_flags_glowing(flags); + self.invisible = crate::entity::shared_flags_invisible(flags); } pub fn snapshot_render_state(&mut self) { diff --git a/pomme-client/src/renderer/mod.rs b/pomme-client/src/renderer/mod.rs index cd2c69fc0..422456e65 100644 --- a/pomme-client/src/renderer/mod.rs +++ b/pomme-client/src/renderer/mod.rs @@ -1798,10 +1798,11 @@ impl Renderer { } // Vanilla blits the outline after the hand, before the GUI. - if self.entity_renderer.has_outlines() { + if self.entity_renderer.has_outlines() || self.item_entity_pipeline.has_outlines() { pass.end(); let mut outline = self.entity_outline.begin(&self.ctx.device, &encoder); self.entity_renderer.draw_outlines(&mut outline, frame); + self.item_entity_pipeline.draw_outlines(&mut outline, frame); self.entity_outline .finish(&self.ctx.device, &encoder, outline); pass = self.resume_frame_pass(&encoder, image_index); diff --git a/pomme-client/src/renderer/pipelines/entity_outline.rs b/pomme-client/src/renderer/pipelines/entity_outline.rs index 939301b11..46e783245 100644 --- a/pomme-client/src/renderer/pipelines/entity_outline.rs +++ b/pomme-client/src/renderer/pipelines/entity_outline.rs @@ -115,7 +115,9 @@ impl EntityOutline { }; pass(swap, &self.sobel, self.outline_nearest, &[]); pass(outline, &self.blur, self.swap_linear, &[1.0, 0.0]); - pass(swap, &self.blur, self.outline_linear, &[0.0, 1.0]); + // Vanilla's `(0, 1)` steps up its bottom-up GL rows; these run top + // down. The blur's taps aren't symmetric, so the sign matters. + pass(swap, &self.blur, self.outline_linear, &[0.0, -1.0]); pass(outline, &self.blit, self.swap_nearest, &[1.0; 4]); } diff --git a/pomme-client/src/renderer/pipelines/entity_renderer.rs b/pomme-client/src/renderer/pipelines/entity_renderer.rs index a38ecc6ac..b1a6bd313 100644 --- a/pomme-client/src/renderer/pipelines/entity_renderer.rs +++ b/pomme-client/src/renderer/pipelines/entity_renderer.rs @@ -194,6 +194,7 @@ pub struct EntityRenderInfo { /// The glowing outline's colour (vanilla `EntityRenderState.outlineColor`), /// when the entity draws one. pub outline_color: Option, + pub visibility: Visibility, } /// Everything inert: mob-family animation inputs zeroed, no overlays, white @@ -256,11 +257,27 @@ impl Default for EntityRenderInfo { attack_time: 0.0, skip_cull: false, outline_color: None, + visibility: Visibility::Visible, shadow: Default::default(), } } } +/// Vanilla `isInvisible` with `isInvisibleToPlayer`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Visibility { + #[default] + Visible, + /// Invisible, but the viewer sees through it: a spectator, or a teammate + /// whose team sees friendly invisibles. + Seen, + /// Invisible to the viewer. + Hidden, +} + +/// Vanilla's `0x26` alpha on a `Seen` mob's body. +const SEEN_ALPHA: f32 = 38.0 / 255.0; + /// How an overlay layer is blended. Base/baby variants are always `Opaque`. #[derive(Clone, Copy, PartialEq, Eq)] enum OverlayKind { @@ -285,6 +302,9 @@ struct MobVariant { texture: rhi::backend::Texture, texture_set: rhi::backend::BindGroup, overlay_kind: OverlayKind, + /// A layer that leaves its outline while its mob is invisible and glows + /// (vanilla `SheepWoolLayer`, `SlimeOuterLayer`). + invisible_outline: bool, /// Overlay whose part poses (pivots/rotations/scales) differ from the /// base model's, so its part transforms can't be shared with the base /// (stray/bogged clothing: humanoid ±1.9 legs over skeleton ±2.0). @@ -793,6 +813,7 @@ pub struct EntityRenderer { pipeline: rhi::backend::RenderPipeline, /// Opaque with backface culling — bat wings. culled_pipeline: rhi::backend::RenderPipeline, + translucent_culled_pipeline: rhi::backend::RenderPipeline, /// Translucent, depth-writing — slime shell. body_translucent_pipeline: rhi::backend::RenderPipeline, /// Translucent, depth-write off — spider eyes. @@ -832,6 +853,9 @@ pub(super) enum BlendMode { /// Same blend as `Translucent` but keeps depth writes (vanilla /// `entityTranslucent` vs `EYES`). TranslucentDepthWrite, + /// `TranslucentDepthWrite` with backface culling (vanilla + /// `entityTranslucentCull`): an invisible mob the viewer sees through. + TranslucentCulled, Additive, } @@ -868,6 +892,7 @@ struct VariantDef { tex_variants: &'static [&'static [&'static str]], tex_size: u32, overlay_kind: OverlayKind, + invisible_outline: bool, } struct MobDef { @@ -1086,6 +1111,14 @@ fn mob_definitions() -> Vec { tex_variants, tex_size, overlay_kind: OverlayKind::Opaque, + invisible_outline: false, + } + } + + fn outlined_when_invisible(def: VariantDef) -> VariantDef { + VariantDef { + invisible_outline: true, + ..def } } @@ -1184,13 +1217,17 @@ fn mob_definitions() -> Vec { SHEEP_WOOL_UNDERCOAT_TEX, 64, ), - opaque(entity_model::bake_sheep_wool_model(), SHEEP_WOOL_TEX, 64), + outlined_when_invisible(opaque( + entity_model::bake_sheep_wool_model(), + SHEEP_WOOL_TEX, + 64, + )), ], - baby_overlays: vec![opaque( + baby_overlays: vec![outlined_when_invisible(opaque( entity_model::bake_baby_sheep_wool_model(), SHEEP_BABY_WOOL_TEX, 64, - )], + ))], }, MobDef { kind: EntityKind::Player, @@ -1332,6 +1369,7 @@ fn mob_definitions() -> Vec { tex_variants: CREEPER_ARMOR_TEX, tex_size: 64, overlay_kind: OverlayKind::SwirlAdditive, + invisible_outline: false, }], baby_overlays: vec![], }, @@ -1372,6 +1410,7 @@ fn mob_definitions() -> Vec { tex_variants: SPIDER_EYES_TEX, tex_size: 64, overlay_kind: OverlayKind::EyesTranslucent, + invisible_outline: false, }], baby_overlays: vec![], }, @@ -1389,6 +1428,7 @@ fn mob_definitions() -> Vec { tex_variants: ENDERMAN_EYES_TEX, tex_size: 64, overlay_kind: OverlayKind::EyesTranslucent, + invisible_outline: false, }], baby_overlays: vec![], }, @@ -1406,6 +1446,7 @@ fn mob_definitions() -> Vec { tex_variants: SLIME_TEX, tex_size: 64, overlay_kind: OverlayKind::BodyTranslucent, + invisible_outline: true, }], baby_overlays: vec![], }, @@ -1502,12 +1543,14 @@ fn mob_definitions() -> Vec { tex_variants: HORSE_MARKINGS_TEX, tex_size: 64, overlay_kind: OverlayKind::BodyTranslucent, + invisible_outline: false, }], baby_overlays: vec![VariantDef { model: entity_model::bake_baby_horse_model(), tex_variants: HORSE_MARKINGS_BABY_TEX, tex_size: 64, overlay_kind: OverlayKind::BodyTranslucent, + invisible_outline: false, }], }, MobDef { @@ -1610,6 +1653,7 @@ fn mob_definitions() -> Vec { tex_variants: BAT_TEX, tex_size: 32, overlay_kind: OverlayKind::OpaqueCulled, + invisible_outline: false, }], baby: None, adult_overlays: vec![], @@ -1717,6 +1761,7 @@ impl EntityRenderer { rhi::backend::create_pipeline_layout(device, &[camera_layout, texture_layout], None); let [ + translucent_culled_pipeline, pipeline, culled_pipeline, body_translucent_pipeline, @@ -1815,6 +1860,7 @@ impl EntityRenderer { Self { pipeline, culled_pipeline, + translucent_culled_pipeline, body_translucent_pipeline, eyes_pipeline, swirl_pipeline, @@ -2260,7 +2306,7 @@ impl EntityRenderer { // part) becomes a single instanced draw. `vis`/`groups` borrow self.mobs // and are dropped at the end of this block, before the buffer write below. let mut instances = Instances::default(); - let (opaque, armor, culled, body, eyes, swirl, outline) = { + let (opaque, armor, culled, translucent, body, eyes, swirl, outline) = { let mut vis: Vec = Vec::new(); for info in entities { let Some(entry) = self.mobs.get(&info.entity_kind) else { @@ -2295,21 +2341,31 @@ impl EntityRenderer { // after its upper one. let mut opaque = VariantGroups::default(); let mut culled = VariantGroups::default(); + let mut translucent = VariantGroups::default(); + // An invisible glowing mob's parts that only leave their outline. + let mut outline_only = VariantGroups::default(); for (vi, v) in vis.iter().enumerate() { let base = v .entry .base_variant(v.info.is_baby, self.effective_variant_index(v.info)); let texture_set = self.player_texture_set(v.info, base.texture_set); - let group = if base.overlay_kind == OverlayKind::OpaqueCulled { - &mut culled - } else { - &mut opaque - }; - group.add( - base, - texture_set, - (vi, v.info.base_tint, hurt_color(v.info), [0.0, 0.0]), - ); + let member = (vi, v.info.base_tint, hurt_color(v.info), [0.0, 0.0]); + match body_route(v.info) { + Route::Draw if base.overlay_kind == OverlayKind::OpaqueCulled => { + culled.add(base, texture_set, member); + } + Route::Draw => opaque.add(base, texture_set, member), + Route::Translucent => { + let [r, g, b, a] = v.info.base_tint; + translucent.add( + base, + texture_set, + (vi, [r, g, b, a * SEEN_ALPHA], member.2, member.3), + ); + } + Route::OutlineOnly => outline_only.add(base, texture_set, member), + Route::Skip => {} + } } for slot in 0..MAX_OVERLAYS { for (vi, v) in vis.iter().enumerate() { @@ -2327,31 +2383,44 @@ impl EntityRenderer { _ => continue, }; if let Some(tint) = v.info.overlay_tints[slot] { - group.add( - overlay, - overlay.texture_set, - (vi, tint, hurt_color(v.info), [0.0, 0.0]), - ); + let member = (vi, tint, hurt_color(v.info), [0.0, 0.0]); + add_layer(v.info, overlay, member, group, &mut outline_only); } } } - let body = collect_overlays(&vis, OverlayKind::BodyTranslucent); - let eyes = collect_overlays(&vis, OverlayKind::EyesTranslucent); - let swirl = collect_overlays(&vis, OverlayKind::SwirlAdditive); + let body = collect_overlays(&vis, OverlayKind::BodyTranslucent, &mut outline_only); + let eyes = collect_overlays(&vis, OverlayKind::EyesTranslucent, &mut outline_only); + let swirl = collect_overlays(&vis, OverlayKind::SwirlAdditive, &mut outline_only); let opaque = opaque.emit(&vis, &mut instances); let armor = self.collect_armor_draws(&vis, &mut instances); let culled = culled.emit(&vis, &mut instances); + let translucent = translucent.emit(&vis, &mut instances); let body = body.emit(&vis, &mut instances); let eyes = eyes.emit(&vis, &mut instances); let swirl = swirl.emit(&vis, &mut instances); - // Eyes and the energy swirl don't affect the outline. + let outline_only = outline_only.emit(&vis, &mut instances); + // Eyes and the energy swirl don't affect the outline; vanilla's + // outline-only parts use the unculled `RenderTypes.outline`. let outline = [ - outline_records(&[&opaque, &armor, &body], &vis, &mut instances), - outline_records(&[&culled], &vis, &mut instances), + outline_records( + &[&opaque, &armor, &body, &outline_only], + &vis, + &mut instances, + ), + outline_records(&[&culled, &translucent], &vis, &mut instances), ]; - (opaque, armor, culled, body, eyes, swirl, outline) + ( + opaque, + armor, + culled, + translucent, + body, + eyes, + swirl, + outline, + ) }; // Write the instance buffer (clamped to capacity; the cap is far above any @@ -2372,6 +2441,13 @@ impl EntityRenderer { self.record_pass(pass, frame, self.pipeline, &opaque, count); self.record_pass(pass, frame, self.pipeline, &armor, count); self.record_pass(pass, frame, self.culled_pipeline, &culled, count); + self.record_pass( + pass, + frame, + self.translucent_culled_pipeline, + &translucent, + count, + ); self.record_pass(pass, frame, self.body_translucent_pipeline, &body, count); self.record_pass(pass, frame, self.eyes_pipeline, &eyes, count); self.record_pass(pass, frame, self.swirl_pipeline, &swirl, count); @@ -2432,12 +2508,9 @@ impl EntityRenderer { device: &rhi::backend::Device, targets: rhi::backend::PipelineTargets, ) { - self.pipeline.destroy(device); - self.culled_pipeline.destroy(device); - self.body_translucent_pipeline.destroy(device); - self.eyes_pipeline.destroy(device); - self.swirl_pipeline.destroy(device); + self.destroy_pipelines(device); [ + self.translucent_culled_pipeline, self.pipeline, self.culled_pipeline, self.body_translucent_pipeline, @@ -2446,6 +2519,19 @@ impl EntityRenderer { ] = create_pipelines(device, targets, self.pipeline_layout); } + fn destroy_pipelines(&self, device: &rhi::backend::Device) { + for pipeline in [ + self.translucent_culled_pipeline, + self.pipeline, + self.culled_pipeline, + self.body_translucent_pipeline, + self.eyes_pipeline, + self.swirl_pipeline, + ] { + pipeline.destroy(device); + } + } + pub fn destroy(&mut self, device: &rhi::backend::Device) { destroy_armor_assets(device, &mut self.armor_assets); @@ -2483,11 +2569,7 @@ impl EntityRenderer { free_player_skin_texture(device, skin); } - self.pipeline.destroy(device); - self.culled_pipeline.destroy(device); - self.body_translucent_pipeline.destroy(device); - self.eyes_pipeline.destroy(device); - self.swirl_pipeline.destroy(device); + self.destroy_pipelines(device); for pipeline in self.outline_pipelines { pipeline.destroy(device); } @@ -2647,7 +2729,66 @@ impl<'a> VariantGroups<'a> { } /// Group the non-opaque overlays of one kind (body / eyes / swirl) by variant. -fn collect_overlays<'a>(vis: &[VisEntity<'a>], kind: OverlayKind) -> VariantGroups<'a> { +/// Where vanilla sends a part of an entity's model. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Route { + Draw, + /// The `entityTranslucentCull` body of an invisible mob the viewer sees. + Translucent, + /// Only into the outline, for an invisible glowing mob. + OutlineOnly, + Skip, +} + +/// `LivingEntityRenderer.getRenderType` for the base model. +fn body_route(info: &EntityRenderInfo) -> Route { + match info.visibility { + Visibility::Visible => Route::Draw, + Visibility::Seen => Route::Translucent, + Visibility::Hidden if info.outline_color.is_some() => Route::OutlineOnly, + Visibility::Hidden => Route::Skip, + } +} + +/// A layer of an invisible mob: most hide (`RenderLayer. +/// coloredCutoutModelCopyLayerRender`), eyes and the energy swirl ignore it, +/// and sheep wool and the slime shell leave their outline. +fn layer_route(info: &EntityRenderInfo, kind: OverlayKind, invisible_outline: bool) -> Route { + if info.visibility == Visibility::Visible + || matches!( + kind, + OverlayKind::EyesTranslucent | OverlayKind::SwirlAdditive + ) + { + Route::Draw + } else if invisible_outline && info.outline_color.is_some() { + Route::OutlineOnly + } else { + Route::Skip + } +} + +/// Adds `layer` to `group`, or only to `outline_only`, as `layer_route` +/// sends it. +fn add_layer<'a>( + info: &EntityRenderInfo, + layer: &'a MobVariant, + member: Member, + group: &mut VariantGroups<'a>, + outline_only: &mut VariantGroups<'a>, +) { + match layer_route(info, layer.overlay_kind, layer.invisible_outline) { + Route::OutlineOnly => outline_only.add(layer, layer.texture_set, member), + Route::Skip => {} + _ => group.add(layer, layer.texture_set, member), + } +} + +fn collect_overlays<'a>( + vis: &[VisEntity<'a>], + kind: OverlayKind, + outline_only: &mut VariantGroups<'a>, +) -> VariantGroups<'a> { let mut groups = VariantGroups::default(); for (vi, v) in vis.iter().enumerate() { // Energy swirl scrolls its UVs over time (vanilla `EnergySwirlLayer`). @@ -2672,7 +2813,8 @@ fn collect_overlays<'a>(vis: &[VisEntity<'a>], kind: OverlayKind) -> VariantGrou continue; } if let Some(tint) = v.info.overlay_tints[slot] { - groups.add(overlay, overlay.texture_set, (vi, tint, overlay_color, uv)); + let member = (vi, tint, overlay_color, uv); + add_layer(v.info, overlay, member, &mut groups, outline_only); } } } @@ -2856,6 +2998,7 @@ fn build_variants( tex_variants, tex_size, overlay_kind, + invisible_outline, } = variant; // The scrolling swirl needs REPEAT wrapping; everything else clamps. let sampler = match overlay_kind { @@ -2886,6 +3029,7 @@ fn build_variants( texture, texture_set, overlay_kind, + invisible_outline, own_pivots: false, } }) @@ -2934,8 +3078,15 @@ fn create_pipelines( device: &rhi::backend::Device, targets: rhi::backend::PipelineTargets, layout: rhi::backend::PipelineLayout, -) -> [rhi::backend::RenderPipeline; 5] { +) -> [rhi::backend::RenderPipeline; 6] { [ + create_pipeline( + device, + targets, + layout, + BlendMode::TranslucentCulled, + ModelInput::Instanced, + ), create_pipeline( device, targets, @@ -3037,7 +3188,10 @@ pub(super) fn create_pipeline( targets, layout, &rhi::RenderPipelineDesc { - cull_mode: if blend == BlendMode::OpaqueCulled { + cull_mode: if matches!( + blend, + BlendMode::OpaqueCulled | BlendMode::TranslucentCulled + ) { rhi::CullMode::Back } else { rhi::CullMode::None @@ -3053,9 +3207,9 @@ pub(super) fn create_pipeline( blend: match blend { BlendMode::Opaque | BlendMode::OpaqueCulled => None, // Glowing eyes, slime shell. - BlendMode::Translucent | BlendMode::TranslucentDepthWrite => { - Some(rhi::BlendState::TRANSLUCENT) - } + BlendMode::Translucent + | BlendMode::TranslucentDepthWrite + | BlendMode::TranslucentCulled => Some(rhi::BlendState::TRANSLUCENT), // Energy swirl glow. BlendMode::Additive => Some(rhi::BlendState { color: rhi::BlendComponent { @@ -3085,10 +3239,73 @@ mod tests { use azalea_registry::builtin::ItemKind; use super::{ - ArmorDye, EquipmentClientInfoJson, armor_layer_defs, armor_render_info, - equipment_texture_key, + ArmorDye, EquipmentClientInfoJson, OverlayKind, Route, Visibility, armor_layer_defs, + armor_render_info, body_route, equipment_texture_key, layer_route, }; + fn invisible(visibility: Visibility, glowing: bool) -> EntityRenderInfo { + EntityRenderInfo { + visibility, + outline_color: glowing.then_some(0xFFFFFF), + ..Default::default() + } + } + + #[test] + fn invisible_bodies_route_like_get_render_type() { + assert_eq!( + body_route(&invisible(Visibility::Visible, false)), + Route::Draw + ); + assert_eq!( + body_route(&invisible(Visibility::Seen, true)), + Route::Translucent + ); + assert_eq!( + body_route(&invisible(Visibility::Hidden, true)), + Route::OutlineOnly + ); + assert_eq!( + body_route(&invisible(Visibility::Hidden, false)), + Route::Skip + ); + } + + #[test] + fn invisible_layers_hide_but_eyes_stay_and_wool_outlines() { + let hidden = invisible(Visibility::Hidden, true); + let seen = invisible(Visibility::Seen, false); + assert_eq!( + layer_route(&hidden, OverlayKind::Opaque, false), + Route::Skip + ); + assert_eq!(layer_route(&seen, OverlayKind::Opaque, false), Route::Skip); + assert_eq!( + layer_route(&hidden, OverlayKind::EyesTranslucent, false), + Route::Draw + ); + assert_eq!( + layer_route(&hidden, OverlayKind::SwirlAdditive, false), + Route::Draw + ); + assert_eq!( + layer_route(&hidden, OverlayKind::Opaque, true), + Route::OutlineOnly + ); + assert_eq!( + layer_route(&seen, OverlayKind::BodyTranslucent, true), + Route::Skip + ); + assert_eq!( + layer_route( + &invisible(Visibility::Visible, false), + OverlayKind::Opaque, + false + ), + Route::Draw + ); + } + #[test] fn armor_render_info_uses_equippable_slot_asset_and_dye() { let diamond = ItemStack::new(ItemKind::DiamondHelmet, 1); diff --git a/pomme-client/src/renderer/pipelines/gui_item.rs b/pomme-client/src/renderer/pipelines/gui_item.rs index 583565d8e..926a97883 100644 --- a/pomme-client/src/renderer/pipelines/gui_item.rs +++ b/pomme-client/src/renderer/pipelines/gui_item.rs @@ -114,7 +114,9 @@ impl GuiItemPipeline { display, ); - let instance = self.shared.push_item(frame, &model, 1.0, false, item_tints); + let instance = self + .shared + .push_item(frame, &model, 1.0, false, item_tints, 0); pass.set_vertex_buffer(0, buffer); pass.draw(vertex_count, 1, 0, instance); } diff --git a/pomme-client/src/renderer/pipelines/held_item.rs b/pomme-client/src/renderer/pipelines/held_item.rs index 48aee0383..90c647178 100644 --- a/pomme-client/src/renderer/pipelines/held_item.rs +++ b/pomme-client/src/renderer/pipelines/held_item.rs @@ -142,7 +142,7 @@ impl HeldItemPipeline { let model = arm * display.to_matrix(); let instance = self.shared - .push_item(frame, &model, item.light, false, &item.item_tints); + .push_item(frame, &model, item.light, false, &item.item_tints, 0); pass.set_vertex_buffer(0, buffer); pass.draw(vertex_count, 1, 0, instance); } diff --git a/pomme-client/src/renderer/pipelines/item_entity.rs b/pomme-client/src/renderer/pipelines/item_entity.rs index a4e7cdb27..b8928b482 100644 --- a/pomme-client/src/renderer/pipelines/item_entity.rs +++ b/pomme-client/src/renderer/pipelines/item_entity.rs @@ -39,6 +39,8 @@ fn pack_normal(normal: glam::Vec3) -> [i8; 4] { pub struct ItemRenderInfo { pub item_name: Arc, + /// `0xRRGGBB` while the item entity glows. + pub outline_color: Option, /// Evaluated tint palette, shared by every copy of one dropped stack. pub tints: Arc<[u32]>, pub model_matrix: Mat4, @@ -90,6 +92,8 @@ struct ItemInstance { /// The copy's slice of the tint palette. tint_base: u32, tint_count: u32, + /// The glowing outline's `0xRRGGBB`, read only by the outline pass. + outline_color: u32, } impl ItemInstance { @@ -109,11 +113,18 @@ impl ItemInstance { rhi::VertexFormat::Float32x2, rhi::VertexFormat::Uint32, rhi::VertexFormat::Uint32, + rhi::VertexFormat::Uint32, ], ), }; - fn new(model: &Mat4, light: f32, nether_lighting: bool, tints: (u32, u32)) -> Self { + fn new( + model: &Mat4, + light: f32, + nether_lighting: bool, + tints: (u32, u32), + outline_color: u32, + ) -> Self { Self { model: model.to_cols_array(), normal_matrix: glam::Mat3::from_mat4(*model) @@ -123,8 +134,50 @@ impl ItemInstance { light: [light, if nether_lighting { 1.0 } else { 0.0 }], tint_base: tints.0, tint_count: tints.1, + outline_color, + } + } +} + +/// One instanced draw: `count` copies of a mesh from instance `first`. +#[derive(Clone, Copy)] +struct Run { + buffer: rhi::backend::BufferHandle, + vertex_count: u32, + first: u32, + count: u32, +} + +/// Groups the `items` that `keep` picks into runs of adjacent copies sharing +/// a mesh; instance `i` is item `i`, and nothing is reordered. +fn runs<'a>( + items: &[ItemRenderInfo], + meshes: &'a HashMap, + keep: impl Fn(&ItemRenderInfo, &MeshEntry) -> bool, +) -> Vec { + let mut runs: Vec<(&'a MeshEntry, u32, u32)> = Vec::new(); + for (index, item) in (0..).zip(items) { + let Some(mesh) = meshes.get(&*item.item_name) else { + continue; + }; + if !keep(item, mesh) { + continue; + } + match runs.last_mut() { + Some((last, first, count)) if std::ptr::eq(*last, mesh) && *first + *count == index => { + *count += 1; + } + _ => runs.push((mesh, index, 1)), } } + runs.into_iter() + .map(|(mesh, first, count)| Run { + buffer: mesh.buffer.handle(), + vertex_count: mesh.vertex_count, + first, + count, + }) + .collect() } /// A host buffer per frame in flight, filled from the start each frame. It @@ -344,6 +397,7 @@ impl ItemPipelineShared { light: f32, nether_lighting: bool, tints: &[u32], + outline_color: u32, ) -> u32 { let tint_base = self.tints.push(frame, tints); let instance = ItemInstance::new( @@ -351,6 +405,7 @@ impl ItemPipelineShared { light, nether_lighting, (tint_base, tints.len() as u32), + outline_color, ); self.instances.push(frame, &[instance]) } @@ -376,8 +431,12 @@ pub struct ItemEntityPipeline { cutout: rhi::backend::RenderPipeline, /// Vanilla `ITEM_TRANSLUCENT`: alpha-tested and blended. translucent: rhi::backend::RenderPipeline, + /// Vanilla `OUTLINE_CULL`, which both item sheets resubmit as. + outline: rhi::backend::RenderPipeline, shared: ItemPipelineShared, meshes: HashMap, + /// The last `draw`'s glowing copies. + outline_runs: Vec, } impl ItemEntityPipeline { @@ -392,8 +451,10 @@ impl ItemEntityPipeline { Self { cutout, translucent, + outline: create_outline_pipeline(device, shared.pipeline_layout), shared, meshes: HashMap::new(), + outline_runs: Vec::new(), } } @@ -512,6 +573,7 @@ impl ItemEntityPipeline { frame: usize, items: &[ItemRenderInfo], ) { + self.outline_runs.clear(); if items.is_empty() { return; } @@ -525,37 +587,43 @@ impl ItemEntityPipeline { item.light, item.nether_lighting, &item.tints, + item.outline_color.unwrap_or(0), ); } for (pipeline, translucent) in [(self.cutout, false), (self.translucent, true)] { - // (mesh, first instance, instance count), for runs of adjacent - // items sharing a mesh. - let mut runs: Vec<(&MeshEntry, u32, u32)> = Vec::new(); - for (index, item) in (0..).zip(items) { - let Some(mesh) = self.meshes.get(&*item.item_name) else { - continue; - }; - if mesh.translucent != translucent { - continue; - } - match runs.last_mut() { - Some((last, first, count)) - if std::ptr::eq(*last, mesh) && *first + *count == index => - { - *count += 1; - } - _ => runs.push((mesh, index, 1)), - } - } - if runs.is_empty() { - continue; - } - self.shared.bind(pass, frame, pipeline); - for (mesh, first, count) in runs { - pass.set_vertex_buffer(0, mesh.buffer.handle()); - pass.draw(mesh.vertex_count, count, 0, first); - } + let runs = runs(items, &self.meshes, |_, mesh| { + mesh.translucent == translucent + }); + self.draw_runs(pass, frame, pipeline, &runs); + } + self.outline_runs = runs(items, &self.meshes, |item, _| item.outline_color.is_some()); + } + + /// Whether the last `draw` left any glowing copy to outline. + pub fn has_outlines(&self) -> bool { + !self.outline_runs.is_empty() + } + + /// The last `draw`'s glowing copies, into the outline target's pass. + pub fn draw_outlines(&self, pass: &mut rhi::backend::RenderPassEncoder, frame: usize) { + self.draw_runs(pass, frame, self.outline, &self.outline_runs); + } + + fn draw_runs( + &self, + pass: &mut rhi::backend::RenderPassEncoder, + frame: usize, + pipeline: rhi::backend::RenderPipeline, + runs: &[Run], + ) { + if runs.is_empty() { + return; + } + self.shared.bind(pass, frame, pipeline); + for run in runs { + pass.set_vertex_buffer(0, run.buffer); + pass.draw(run.vertex_count, run.count, 0, run.first); } } @@ -587,6 +655,7 @@ impl ItemEntityPipeline { pub fn destroy(&mut self, device: &rhi::backend::Device) { self.clear_meshes(device); self.destroy_pipelines(device); + self.outline.destroy(device); self.shared.destroy(device); } } @@ -910,6 +979,29 @@ fn create_world_pipelines( (world(false), world(true)) } +/// Vanilla `OUTLINE_CULL` for item quads: the silhouette in the outline +/// colour, into the outline target with no depth or blend. +fn create_outline_pipeline( + device: &rhi::backend::Device, + layout: rhi::backend::PipelineLayout, +) -> rhi::backend::RenderPipeline { + rhi::backend::create_render_pipeline( + device, + super::entity_outline::targets(), + layout, + &rhi::RenderPipelineDesc { + cull_mode: rhi::CullMode::Back, + front_face: rhi::FrontFace::Ccw, + ..rhi::RenderPipelineDesc::new( + "item outline", + shader::include_shader!("item_outline.vert"), + shader::include_shader!("entity_outline.frag"), + &[ItemVertex::BUFFER, ItemInstance::BUFFER], + ) + }, + ) +} + pub(super) fn create_pipeline_with_front_face( device: &rhi::backend::Device, targets: rhi::backend::PipelineTargets, @@ -965,7 +1057,7 @@ mod tests { let attributes = ItemInstance::BUFFER.attributes; let last = attributes.last().unwrap(); // item_instance.glsl's last location. - assert_eq!(last.location, 14); + assert_eq!(last.location, 15); assert_eq!( (last.offset + last.format.size()) as usize, size_of::() @@ -979,7 +1071,7 @@ mod tests { glam::Quat::from_rotation_y(0.7), glam::Vec3::new(1.0, 2.0, 3.0), ); - let instance = ItemInstance::new(&model, 0.8, true, (3, 2)); + let instance = ItemInstance::new(&model, 0.8, true, (3, 2), 0xFF0000); let normal_matrix = glam::Mat3::from_cols_array(&instance.normal_matrix); let (normal, tangent) = (glam::Vec3::Y, glam::Vec3::new(1.0, 0.0, 1.0)); let surface = glam::Mat3::from_mat4(model) * tangent; diff --git a/pomme-client/src/renderer/shaders/item_instance.glsl b/pomme-client/src/renderer/shaders/item_instance.glsl index 08815952a..05504d3f6 100644 --- a/pomme-client/src/renderer/shaders/item_instance.glsl +++ b/pomme-client/src/renderer/shaders/item_instance.glsl @@ -5,3 +5,4 @@ layout(location = 9) in mat3 normal_matrix; layout(location = 12) in vec2 instance_light; layout(location = 13) in uint item_tint_base; layout(location = 14) in uint item_tint_count; +layout(location = 15) in uint outline_color; diff --git a/pomme-client/src/renderer/shaders/item_outline.vert b/pomme-client/src/renderer/shaders/item_outline.vert new file mode 100644 index 000000000..321c05868 --- /dev/null +++ b/pomme-client/src/renderer/shaders/item_outline.vert @@ -0,0 +1,21 @@ +#version 450 + +// The item mesh for vanilla `rendertype_outline`: positions and the outline +// colour as the tint, for entity_outline.frag (which reads only its rgb). + +#include "camera_ubo.glsl" +#include "item_instance.glsl" +#include "item_tint.glsl" + +layout(location = 0) in vec3 position; +layout(location = 1) in vec2 tex_coords; + +layout(location = 0) out vec2 v_tex_coords; +layout(location = 1) out vec4 v_tint; + +void main() { + vec4 world_pos = model * vec4(position, 1.0); + gl_Position = view_proj * vec4(world_pos.xyz - camera_pos.xyz, 1.0); + v_tex_coords = tex_coords; + v_tint = unpack_argb(outline_color); +} diff --git a/pomme-client/src/renderer/shaders/item_tint.glsl b/pomme-client/src/renderer/shaders/item_tint.glsl index be0336f3e..ebf8e0aaf 100644 --- a/pomme-client/src/renderer/shaders/item_tint.glsl +++ b/pomme-client/src/renderer/shaders/item_tint.glsl @@ -5,11 +5,7 @@ layout(set = 2, binding = 0, std430) readonly buffer ItemTintPalette { uint item_tints[]; }; -vec4 resolve_item_tint(uint tint_index, uint base, uint count) { - if (tint_index >= count) { - return vec4(1.0); - } - uint color = item_tints[base + tint_index]; +vec4 unpack_argb(uint color) { return vec4( float((color >> 16) & 255u), float((color >> 8) & 255u), @@ -17,3 +13,10 @@ vec4 resolve_item_tint(uint tint_index, uint base, uint count) { float((color >> 24) & 255u) ) / 255.0; } + +vec4 resolve_item_tint(uint tint_index, uint base, uint count) { + if (tint_index >= count) { + return vec4(1.0); + } + return unpack_argb(item_tints[base + tint_index]); +} diff --git a/pomme-client/src/ui/hud.rs b/pomme-client/src/ui/hud.rs index 5b87666d8..91d8ebb12 100644 --- a/pomme-client/src/ui/hud.rs +++ b/pomme-client/src/ui/hud.rs @@ -90,6 +90,7 @@ pub(crate) struct ScoreboardTeam { /// menu), like vanilla's `PlayerTeam.getColor()` Optional. pub(crate) fill_color: Option, collision_rule: CollisionRule, + see_friendly_invisibles: bool, pub(crate) members: HashSet, } @@ -168,6 +169,7 @@ impl Scoreboard { color: [f32; 4], fill_color: Option, collision_rule: CollisionRule, + see_friendly_invisibles: bool, members: Option>, ) { // Vanilla ignores a parameter change for a team it doesn't know; @@ -185,6 +187,7 @@ impl Scoreboard { color, fill_color: None, collision_rule, + see_friendly_invisibles, members: HashSet::new(), }); team.display_name = display_name; @@ -193,6 +196,7 @@ impl Scoreboard { team.color = color; team.fill_color = fill_color; team.collision_rule = collision_rule; + team.see_friendly_invisibles = see_friendly_invisibles; // ADD unions its player list onto an existing team, like vanilla's // addPlayerTeam + per-player addPlayerToTeam. if let Some(members) = members { @@ -275,6 +279,13 @@ impl Scoreboard { /// Vanilla `TeamColor`: a real team chat color if the member belongs to a /// team whose formatting carries one, otherwise no owner-derived color. + /// `Entity.isInvisibleTo`'s team rule: `viewer` sees an invisible + /// `target` on its own team when the team allows it. + pub fn sees_friendly_invisible(&self, viewer: &str, target: &str) -> bool { + self.team_of(target) + .is_some_and(|(_, team)| team.see_friendly_invisibles && team.members.contains(viewer)) + } + pub fn member_team_color(&self, member: &str) -> Option { self.teams .values() @@ -1809,6 +1820,7 @@ mod tests { WHITE, None, rule, + true, Some(members.iter().map(|m| (*m).into()).collect()), ); } @@ -1844,6 +1856,34 @@ mod tests { assert!(!scoreboard.pushable_by("pusher", "target")); } + #[test] + fn teammates_see_friendly_invisibles_when_the_team_allows() { + let mut scoreboard = Scoreboard::default(); + add_team( + &mut scoreboard, + "a", + CollisionRule::Always, + &["viewer", "mate"], + ); + add_team(&mut scoreboard, "b", CollisionRule::Always, &["stranger"]); + assert!(scoreboard.sees_friendly_invisible("viewer", "mate")); + assert!(!scoreboard.sees_friendly_invisible("viewer", "stranger")); + assert!(!scoreboard.sees_friendly_invisible("loner", "mate")); + + scoreboard.set_team( + "a".into(), + Vec::new(), + Vec::new(), + Vec::new(), + WHITE, + None, + CollisionRule::Always, + false, + None, + ); + assert!(!scoreboard.sees_friendly_invisible("viewer", "mate")); + } + fn add_colored_team(scoreboard: &mut Scoreboard, name: &str, fill: Option) { scoreboard.set_team( name.into(), @@ -1853,6 +1893,7 @@ mod tests { WHITE, fill, CollisionRule::Always, + true, Some(vec!["Player".into()]), ); } diff --git a/tests/ingame/entities/glowing.toml b/tests/ingame/entities/glowing.toml index ccc7a07ac..61f17d7cb 100644 --- a/tests/ingame/entities/glowing.toml +++ b/tests/ingame/entities/glowing.toml @@ -1,6 +1,8 @@ # The glowing effect sets the shared glowing flag and draws vanilla's outline: -# white without a team, the team's colour with one, through walls, and gone -# once the effect is cleared. +# white without a team, the team's colour with one, through walls, around a +# glowing dropped item, alone for an invisible glowing mob (a sheep keeps its +# wool's outline), and gone once the effect is cleared. Invisible mobs show +# nothing else, except to a spectator, who sees them faded. [[step]] wait_ms = 2000 @@ -27,6 +29,23 @@ chat = "/fill 31 -60 -31 34 -58 -31 minecraft:stone" [[step]] chat = "/summon minecraft:pig 32.5 -60 -32.5 {NoAI:1b,Tags:[\"glow\"]}" +# A glowing dropped item, an invisible glowing pig and sheep, and an +# invisible pig that shows nothing. +[[step]] +chat = "/summon minecraft:item 27.5 -60 -28.0 {Item:{id:\"minecraft:diamond\",count:1},PickupDelay:32767,Age:-32768,Glowing:1b,Tags:[\"glow\"]}" + +[[step]] +chat = "/summon minecraft:pig 33.0 -60 -28.5 {NoAI:1b,Tags:[\"glow\",\"inv\"]}" + +[[step]] +chat = "/summon minecraft:sheep 27.5 -60 -33.5 {NoAI:1b,Tags:[\"glow\",\"inv\"]}" + +[[step]] +chat = "/summon minecraft:pig 31.5 -60 -27.5 {NoAI:1b,Tags:[\"inv\"]}" + +[[step]] +chat = "/effect give @e[tag=inv] minecraft:invisibility infinite 0 true" + [[step]] chat = "/team add glowing_red" @@ -52,6 +71,25 @@ wait_ms = 11000 [[step]] screenshot = "outlines" +# A spectator sees invisible mobs faded. +[[step]] +chat = "/gamemode spectator" + +[[step]] +press = { action = "key.toggleGui" } + +[[step]] +wait_ms = 500 + +[[step]] +screenshot = "spectator" + +[[step]] +press = { action = "key.toggleGui" } + +[[step]] +chat = "/gamemode survival" + [[step]] chat = "/effect clear @e[tag=glow]" @@ -67,6 +105,9 @@ screenshot = "cleared" [[step]] chat = "/kill @e[tag=glow]" +[[step]] +chat = "/kill @e[tag=inv]" + [[step]] chat = "/team remove glowing_red" diff --git a/tests/ingame/player/invisible_shadow.toml b/tests/ingame/player/invisible_shadow.toml new file mode 100644 index 000000000..75476dc69 --- /dev/null +++ b/tests/ingame/player/invisible_shadow.toml @@ -0,0 +1,37 @@ +# An invisible player casts no shadow, in third person too. + +[[step]] +wait_ms = 2000 + +[[step]] +chat = "/time set 6000" + +[[step]] +chat = "/tp @s ~ ~ ~ 0 60" + +[[step]] +press = { action = "key.togglePerspective" } + +[[step]] +wait_ms = 11000 + +[[step]] +screenshot = "visible" + +[[step]] +chat = "/effect give @s minecraft:invisibility infinite 0 true" + +[[step]] +wait_ms = 11000 + +[[step]] +screenshot = "invisible" + +[[step]] +chat = "/effect clear @s" + +[[step]] +press = { action = "key.togglePerspective" } + +[[step]] +press = { action = "key.togglePerspective" }