mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 10:39:46 -10:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e138304d2e | ||
|
|
4be9343eca | ||
|
|
03fd6705a1 | ||
|
|
a5a60598e4 | ||
|
|
0fd4e22139 | ||
|
|
ec19b6320a | ||
|
|
3d9df204e9 | ||
|
|
984b7d8635 | ||
|
|
afa60454b2 |
No files matched your search
@@ -6773,8 +6773,8 @@ impl Game<'_> {
|
||||
.extend(out.drain(player_start..));
|
||||
}
|
||||
self.mark_motion(out.len(), 0);
|
||||
// Companions in first person, trails and auras (game/cosmetics/fx.rs).
|
||||
self.cosmetics_fx_geometry(camera, alpha, &mut out);
|
||||
// What crates draw around players: first person, see-through (game/player_hooks.rs).
|
||||
self.players_after_geometry(camera, alpha, &mut out);
|
||||
self.mark_entity(out.len(), "minecraft:evoker_fangs");
|
||||
self.fangs_geometry(camera, alpha, &mut out);
|
||||
self.block_item_display_geometry(camera, alpha, &mut out);
|
||||
|
||||
@@ -13,18 +13,12 @@ mod fx;
|
||||
|
||||
use super::Game;
|
||||
use horizon_client::fast_hash::FxHashMap;
|
||||
use horizon_client::mesher::Vertex;
|
||||
use horizon_client::mobs::anim::Poses;
|
||||
use horizon_client::mobs::{Mob, Part, Polygon};
|
||||
use horizon_cosmetics::follow::Follow;
|
||||
use horizon_cosmetics::prefs::{self, Others};
|
||||
use horizon_cosmetics::trail::{Budget, Emitter};
|
||||
use horizon_cosmetics::{Item, Slot};
|
||||
use horizon_hooks::players::{
|
||||
Badge, Bone, CameraOverride, CameraView, Frame, Gear, Kind, Mark, Mesh, NameDecoration,
|
||||
PlayerDraw, PlayerModel, PlayerModels, Quad, SpriteStore, Sprites,
|
||||
Badge, Bone, CameraOverride, CameraView, Frame, Gear, Kind, Look, Mark, Mesh, NameDecoration,
|
||||
Pass, PlayerDraw, PlayerModel, PlayerModels, Quad, SpriteStore, Sprites,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::sync::mpsc::{Receiver, channel};
|
||||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -32,46 +26,10 @@ use std::time::{Duration, Instant};
|
||||
/// Horizon's mark before a Horizon player's name, over their name tag and in the player list.
|
||||
const MARK: Mark = &horizon_cosmetics::mark::MARK;
|
||||
|
||||
/// An item as the renderer takes it.
|
||||
struct Baked {
|
||||
item: Arc<Item>,
|
||||
/// A model per bone it rides (a shirt's sleeves are on the arms).
|
||||
models: Vec<(horizon_cosmetics::Bone, Arc<Part>)>,
|
||||
/// Its texture's sprite (a 64×64 skin slot of the atlas).
|
||||
location: String,
|
||||
/// The frame it was last marked drawn in (its slot is kept).
|
||||
marked: Cell<u32>,
|
||||
}
|
||||
|
||||
impl Baked {
|
||||
fn new(item: &Arc<Item>, frame: u32) -> Self {
|
||||
Self {
|
||||
item: item.clone(),
|
||||
models: item
|
||||
.roots()
|
||||
.map(|(bone, root)| (bone, Arc::new(part(root))))
|
||||
.collect(),
|
||||
location: format!("cosmetics/{:x}", Arc::as_ptr(item) as usize),
|
||||
marked: Cell::new(frame.wrapping_sub(1)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Cosmetics {
|
||||
/// The hooks' client (`Seat`), taken once it started.
|
||||
client: Option<Arc<horizon_cosmetics::Cosmetics>>,
|
||||
/// By the item's address; each entry holds its item, so the address isn't reused.
|
||||
baked: RefCell<FxHashMap<usize, Baked>>,
|
||||
/// Items drawn whose texture isn't in the atlas yet.
|
||||
uploads: RefCell<Vec<Arc<Item>>>,
|
||||
frame: Cell<u32>,
|
||||
/// Kept between players so a frame of avatars allocates none: the wearer's body, an item.
|
||||
poses: RefCell<(Poses, Poses)>,
|
||||
/// Item files from a folder (`HORIZON_COSMETICS_LOCAL`): mannequins wear them too.
|
||||
local: bool,
|
||||
/// Companions, trails and auras.
|
||||
fx: RefCell<Fx>,
|
||||
/// Emotes: those the wheel offers and the one playing (`cosmetics/emotes.rs`).
|
||||
emotes: emotes::Emotes,
|
||||
/// The emote wheel (`cosmetics/emote_wheel.rs`).
|
||||
@@ -130,49 +88,6 @@ fn load_owned_emotes(
|
||||
)
|
||||
}
|
||||
|
||||
/// The key of the player's own model in `Fx`'s maps (others go by entity id).
|
||||
const OWN: u32 = u32::MAX;
|
||||
/// Full light, for what glows.
|
||||
const FULL_LIGHT: u32 = 240 | 240 << 8;
|
||||
|
||||
/// What companions and trails keep between frames, per wearer (marked with the frame they were
|
||||
/// last drawn in; the others are dropped), and the frame's trails and auras, drawn after the
|
||||
/// players (`cosmetics_fx_geometry`).
|
||||
#[derive(Default)]
|
||||
struct Fx {
|
||||
companions: FxHashMap<u32, (Follow, u32)>,
|
||||
trails: FxHashMap<u32, (Emitter, u32)>,
|
||||
budget: Budget,
|
||||
clock: Option<(Instant, Instant)>,
|
||||
/// Ticks since the first frame, and the last frame's length in seconds.
|
||||
now: f64,
|
||||
dt: f32,
|
||||
/// Trails and auras: see-through.
|
||||
clear: Vec<Vertex>,
|
||||
}
|
||||
|
||||
impl Fx {
|
||||
/// A new frame: the clock moves on and what no frame drew since the last one goes.
|
||||
fn frame(&mut self, frame: u32) {
|
||||
let now = Instant::now();
|
||||
let (start, last) = self.clock.get_or_insert((now, now));
|
||||
self.dt = now.duration_since(*last).as_secs_f32().min(0.25);
|
||||
self.now = now.duration_since(*start).as_secs_f64() * 20.0;
|
||||
*last = now;
|
||||
let seen = |mark: u32| frame.wrapping_sub(mark) <= 1;
|
||||
self.companions.retain(|_, (_, mark)| seen(*mark));
|
||||
let budget = &mut self.budget;
|
||||
self.trails.retain(|_, (e, mark)| {
|
||||
let keep = seen(*mark);
|
||||
if !keep {
|
||||
e.clear(budget);
|
||||
}
|
||||
keep
|
||||
});
|
||||
self.clear.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the signed-in account owns a cosmetic: the answer kept from the last run first
|
||||
/// (`prefs::owned`), then the service's, asked once a run and retried after a failure like Show my
|
||||
/// cosmetics. Not known yet, or no session: not owning.
|
||||
@@ -334,15 +249,11 @@ impl Cosmetics {
|
||||
/// choice, or by owning none). Hidden, the service is never asked about them.
|
||||
fn worn_of(
|
||||
client: &horizon_cosmetics::Cosmetics,
|
||||
extras: &horizon_client::extras::Extras,
|
||||
said: &Said,
|
||||
own: bool,
|
||||
uuid: impl FnOnce() -> Option<u128>,
|
||||
) -> Option<horizon_cosmetics::Worn> {
|
||||
let shown = if own {
|
||||
extras.cosmetics_mine
|
||||
} else {
|
||||
extras.effective_others() != Others::Hidden
|
||||
};
|
||||
let shown = if own { said.mine } else { !said.others_hidden };
|
||||
if !shown {
|
||||
return None;
|
||||
}
|
||||
@@ -369,24 +280,6 @@ fn own_uuid(
|
||||
/// How often a looping emote is said again (the service keeps one a minute).
|
||||
const LOOP_AGAIN: Duration = Duration::from_secs(30);
|
||||
|
||||
/// The game's model of an item's part.
|
||||
fn part(p: &horizon_cosmetics::Part) -> Part {
|
||||
Part {
|
||||
name: p.name.clone(),
|
||||
pose: p.pose,
|
||||
polygons: p
|
||||
.faces
|
||||
.iter()
|
||||
.map(|f| Polygon {
|
||||
normal: f.normal,
|
||||
vertices: f.vertices,
|
||||
cull_back: false,
|
||||
})
|
||||
.collect(),
|
||||
children: p.children.iter().map(part).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The texture in the top-left corner of a skin slot's 64×64 RGBA.
|
||||
fn slot_image(item: &Item) -> Vec<u8> {
|
||||
let t = &item.texture;
|
||||
@@ -468,6 +361,8 @@ struct Seat {
|
||||
last_sprite: u64,
|
||||
/// Sprites drawing missed, put in the atlas next frame.
|
||||
wanted: Vec<u64>,
|
||||
/// Companions, trails and auras (`cosmetics/fx.rs`).
|
||||
fx: fx::Fx,
|
||||
}
|
||||
|
||||
/// An item the hooks show: its sprite's id (never reused) and a mesh per bone it rides, held
|
||||
@@ -514,6 +409,8 @@ struct Said {
|
||||
others_hidden: bool,
|
||||
/// Horizon's mark by Horizon players' names.
|
||||
marks: bool,
|
||||
/// Other players' trails and auras are hidden.
|
||||
hide_fx: bool,
|
||||
/// Cosmetics in single player: the signed-in account's on the player's own model.
|
||||
solo: bool,
|
||||
/// My emote pulled the camera back: when it started (the own model's age).
|
||||
@@ -524,6 +421,19 @@ struct Said {
|
||||
first: Option<Arc<Item>>,
|
||||
}
|
||||
|
||||
impl Said {
|
||||
/// What the game's settings say.
|
||||
fn settings(&mut self, extras: &horizon_client::extras::Extras) {
|
||||
self.mine = extras.cosmetics_mine;
|
||||
self.listed = extras.horizon_listed;
|
||||
self.others_shown = extras.effective_others() == Others::Shown;
|
||||
self.others_hidden = extras.effective_others() == Others::Hidden;
|
||||
self.marks = extras.horizon_marks;
|
||||
self.hide_fx = extras.cosmetics_hide_fx;
|
||||
self.solo = extras.cosmetics_solo;
|
||||
}
|
||||
}
|
||||
|
||||
/// One [`Seat`] per splitscreen player.
|
||||
static SERVICES: Mutex<Vec<Seat>> = Mutex::new(Vec::new());
|
||||
|
||||
@@ -546,6 +456,7 @@ impl horizon_hooks::Hooks for Hooks {
|
||||
service(|s| {
|
||||
s.solo = frame.solo;
|
||||
s.frame();
|
||||
s.fx.frame();
|
||||
s.put_sprites(sprites);
|
||||
});
|
||||
}
|
||||
@@ -574,6 +485,11 @@ impl horizon_hooks::Hooks for Hooks {
|
||||
service(|s| s.player_layers(model, draw));
|
||||
}
|
||||
|
||||
/// The player's own companion in first person.
|
||||
fn first_person(&self, own: &PlayerModel, draw: &mut dyn PlayerDraw) {
|
||||
service(|s| s.first_person(own, draw));
|
||||
}
|
||||
|
||||
fn camera(&self, own: &CameraView) -> Option<CameraOverride> {
|
||||
let start = service(|s| s.said.pull)?;
|
||||
let t = ((own.age - start) / 5.0).clamp(0.0, 1.0);
|
||||
@@ -782,15 +698,14 @@ impl Seat {
|
||||
self.items.retain(|_, k| Arc::strong_count(&k.item) > 1);
|
||||
}
|
||||
|
||||
/// The cosmetics `m` wears, on its bones, and an emote's props (companions, trails and auras
|
||||
/// are still the game's: `cosmetics_geometry`).
|
||||
/// The cosmetics `m` wears, on its bones and around it in the world (companions, trails,
|
||||
/// auras: `cosmetics/fx.rs`), and an emote's props.
|
||||
fn player_layers(&mut self, m: &PlayerModel, draw: &mut dyn PlayerDraw) {
|
||||
if m.invisible {
|
||||
return;
|
||||
}
|
||||
let said = &self.said;
|
||||
let own = m.kind == Kind::Own;
|
||||
let shown = if own { said.mine } else { !said.others_hidden };
|
||||
// "Name tags only": other players' styles beside their names, nothing on their bodies.
|
||||
let bodies = own || said.others_shown;
|
||||
let ticks = draw.ticks();
|
||||
@@ -799,17 +714,22 @@ impl Seat {
|
||||
let (t, w) = p.at(ticks)?;
|
||||
(p.item.vertices > 0).then(|| (p.item.clone(), t, w))
|
||||
});
|
||||
let worn = match (&self.client, self.uuid_of(m)) {
|
||||
(Some(client), Some(uuid)) if shown && bodies => client.frame().worn(uuid),
|
||||
_ => None,
|
||||
};
|
||||
let client = self.client.as_ref().filter(|_| bodies);
|
||||
let worn = client.and_then(|c| worn_of(c, &self.said, own, || self.uuid_of(m)));
|
||||
// Items on one bone (a back item, a shoulder pet and a shirt ride the body) share its
|
||||
// matrix: the head, the body and the two arms at most.
|
||||
let mut bones: [Option<(Bone, [[f32; 4]; 4])>; 4] = [None; 4];
|
||||
let worn_items = worn.iter().flat_map(|w| w.iter()).map(|i| (i, None));
|
||||
let emote_items = emote.iter().map(|(i, t, w)| (i, Some((*t, *w))));
|
||||
for (item, emoting) in worn_items.chain(emote_items) {
|
||||
// Covered by gear, or drawn by no bone (a name tag style, companions, trails, auras).
|
||||
// On no bone, around the model in the world.
|
||||
if let (Slot::Companion | Slot::Trail | Slot::Aura, None) = (item.slot, emoting) {
|
||||
if draw.pass() == Pass::World {
|
||||
self.fx(m, item, draw);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Covered by gear, or drawn by no bone (a name tag style).
|
||||
if emoting.is_none() && covered(m.gear, item.slot) {
|
||||
continue;
|
||||
}
|
||||
@@ -831,7 +751,7 @@ impl Seat {
|
||||
matrix
|
||||
}
|
||||
};
|
||||
draw.draw(&mesh, matrix, uv, &mut |p| {
|
||||
draw.draw(&mesh, matrix, Look::new(uv), &mut |p| {
|
||||
item.animate(ticks, |name, pose| *p.pose(name) = pose);
|
||||
if let (Some((t, w)), Some(e)) = (emoting, &item.emote) {
|
||||
e.pose_props(t, |n| item.rest(n), |n, pose| *p.pose(n) = pose);
|
||||
@@ -846,6 +766,19 @@ impl Seat {
|
||||
}
|
||||
}
|
||||
|
||||
/// The window's own companion while its body is out of view.
|
||||
fn first_person(&mut self, m: &PlayerModel, draw: &mut dyn PlayerDraw) {
|
||||
let Some(client) = self.client.as_ref().filter(|_| !m.invisible) else {
|
||||
return;
|
||||
};
|
||||
let Some(worn) = worn_of(client, &self.said, true, || self.uuid_of(m)) else {
|
||||
return;
|
||||
};
|
||||
if let Some(item) = worn.iter().find(|i| i.slot == Slot::Companion) {
|
||||
self.fx(m, item, draw);
|
||||
}
|
||||
}
|
||||
|
||||
/// Once a tick: names the Horizon players in view to the emote feed and plays what it says
|
||||
/// they play on their models; one that moved ends theirs here at once. Others' emotes show
|
||||
/// with "Other players' cosmetics" on Shown only; otherwise nothing is asked.
|
||||
@@ -945,12 +878,7 @@ impl Game<'_> {
|
||||
let pull = self.cosmetics.emotes.pull();
|
||||
service(|s| {
|
||||
let said = &mut s.said;
|
||||
said.mine = extras.cosmetics_mine;
|
||||
said.listed = extras.horizon_listed;
|
||||
said.others_shown = extras.effective_others() == Others::Shown;
|
||||
said.others_hidden = extras.effective_others() == Others::Hidden;
|
||||
said.marks = extras.horizon_marks;
|
||||
said.solo = extras.cosmetics_solo;
|
||||
said.settings(extras);
|
||||
said.pull = pull;
|
||||
if said.playing.as_ref().map(|(id, l)| (id.as_str(), *l)) != playing {
|
||||
said.playing = playing.map(|(id, l)| (id.to_owned(), l));
|
||||
@@ -961,36 +889,16 @@ impl Game<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Once a frame, after the hooks: takes the service's client for drawing and puts the
|
||||
/// textures the last frame wanted into the atlas.
|
||||
/// Once a frame, after the hooks: what the game reads of them (the settings' owner flag,
|
||||
/// whether items come from a folder).
|
||||
pub(super) fn cosmetics_frame(&mut self) {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
let c = &mut self.cosmetics;
|
||||
let owns = service(|s| {
|
||||
if c.client.is_none() {
|
||||
c.client = s.client.clone();
|
||||
c.local = s.local;
|
||||
}
|
||||
s.owner.owns
|
||||
});
|
||||
let (owns, local) = service(|s| (s.owner.owns, s.local));
|
||||
// Read every frame: the settings button shows it, and it is never saved.
|
||||
self.settings.extras.cosmetics_owner = owns;
|
||||
c.frame.set(c.frame.get().wrapping_add(1));
|
||||
c.fx.get_mut().frame(c.frame.get());
|
||||
let uploads = std::mem::take(&mut *c.uploads.borrow_mut());
|
||||
for item in uploads {
|
||||
let location = format!("cosmetics/{:x}", Arc::as_ptr(&item) as usize);
|
||||
if !self.gui.atlas.sprites.contains_key(&location) {
|
||||
self.store_skin(location, slot_image(&item));
|
||||
}
|
||||
}
|
||||
// Items no player wears any more.
|
||||
self.cosmetics
|
||||
.baked
|
||||
.borrow_mut()
|
||||
.retain(|_, b| Arc::strong_count(&b.item) > 1);
|
||||
self.cosmetics.local = local;
|
||||
}
|
||||
|
||||
/// Asks the service which emotes the account owns, for the wheel: when it opens, at most
|
||||
@@ -1016,217 +924,6 @@ impl Game<'_> {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// What `m` wears, if cosmetics are shown for it: `own` for the player's own model.
|
||||
fn worn(&self, m: &Mob, own: bool) -> Option<horizon_cosmetics::Worn> {
|
||||
let client = self.cosmetics.client.as_ref()?;
|
||||
worn_of(client, &self.settings.extras, own, || {
|
||||
self.cosmetics_uuid(m, own)
|
||||
})
|
||||
}
|
||||
|
||||
/// The UUID the service knows `m` by: a player's, or a mannequin's id with local items.
|
||||
fn cosmetics_uuid(&self, m: &Mob, own: bool) -> Option<u128> {
|
||||
if own {
|
||||
own_uuid(
|
||||
self.remote.as_ref().map(|r| r.profile()),
|
||||
self.integrated.is_some(),
|
||||
self.settings.extras.cosmetics_solo,
|
||||
|| horizon_client::auth::current().map(|s| s.uuid),
|
||||
)
|
||||
} else if &*m.kind.id == "minecraft:player" {
|
||||
self.remote_player_uuid(m.id)
|
||||
} else if self.cosmetics.local && &*m.kind.id == "minecraft:mannequin" {
|
||||
Some(u128::from(m.id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The cosmetics `m` wears, on its bones. `gpu`: posed on the GPU (the mob pass's skin
|
||||
/// frame, begun); the bounds of what went there come back, for culling. Else the vertices
|
||||
/// go to `out`. `camera`: where the world view's camera is; None in a picture of the player
|
||||
/// (the inventory), which shows no companion, trail or aura.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn cosmetics_geometry(
|
||||
&self,
|
||||
m: &Mob,
|
||||
own: bool,
|
||||
base: glam::Mat4,
|
||||
camera: Option<glam::DVec3>,
|
||||
alpha: f32,
|
||||
light: u32,
|
||||
gpu: bool,
|
||||
out: &mut Vec<horizon_client::mesher::Vertex>,
|
||||
) -> Option<(glam::Vec3, glam::Vec3)> {
|
||||
use horizon_client::mobs::{anim, render};
|
||||
let worn = self.worn(m, own);
|
||||
// An emote's props: its time and weight while it plays.
|
||||
let ticks = m.age as f32 + alpha;
|
||||
let emote = emotes::playing(m).and_then(|p| {
|
||||
let (t, w) = p.at(ticks)?;
|
||||
(p.item.vertices > 0).then_some((&p.item, t, w))
|
||||
});
|
||||
if worn.is_none() && emote.is_none() || horizon_client::mobs::trader::is_invisible(m) {
|
||||
return None;
|
||||
}
|
||||
// The avatar's body, posed as it is drawn.
|
||||
let (key, _) = render::body(m);
|
||||
let body = m.kind.layer(&key).unwrap_or(&m.kind.model);
|
||||
let mut scratch = self.cosmetics.poses.borrow_mut();
|
||||
let (body_poses, poses) = &mut *scratch;
|
||||
// Others' items on the body are the hook's (`Seat::player_layers`): their companions,
|
||||
// trails and auras alone are drawn here, on no bone.
|
||||
if own {
|
||||
body_poses.reset(body);
|
||||
let data = self.res.mob_data();
|
||||
anim::setup(m, &anim::state(m, alpha), body_poses, &data.animations);
|
||||
}
|
||||
let mut bounds = None;
|
||||
// Items on one bone (a back item, a shoulder pet and a shirt ride the body) share its
|
||||
// matrix: the head, the body and the two arms at most.
|
||||
let mut bones: [Option<(horizon_cosmetics::Bone, glam::Mat4)>; 4] = [None; 4];
|
||||
// "Name tags only": other players' styles beside their names, nothing on their bodies.
|
||||
let bodies = own || self.settings.extras.effective_others() == Others::Shown;
|
||||
let worn_items = worn.iter().flat_map(|w| w.iter()).map(|i| (i, None));
|
||||
let emote_items = emote.map(|(i, t, w)| (i, Some((t, w))));
|
||||
for (item, emoting) in worn_items.chain(emote_items) {
|
||||
if emoting.is_none() && !bodies {
|
||||
continue;
|
||||
}
|
||||
if let (Some(camera), Slot::Companion | Slot::Trail | Slot::Aura) = (camera, item.slot)
|
||||
{
|
||||
// The player's own in first person: only its companion, drawn after the drain
|
||||
// of the first-person model (`cosmetics_fx_geometry`).
|
||||
if !(own && !self.own_body_in_view()) {
|
||||
let at = fx::Wearer {
|
||||
m,
|
||||
own,
|
||||
key: if own { OWN } else { m.id },
|
||||
base,
|
||||
camera,
|
||||
alpha,
|
||||
light,
|
||||
};
|
||||
bounds = self.cosmetics_fx(&at, item, gpu, bounds, poses, out);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if !own {
|
||||
continue;
|
||||
}
|
||||
if emoting.is_none() && covered(super::player_hooks::gear(m), item.slot) {
|
||||
continue;
|
||||
}
|
||||
let Some((models, uv)) = self.cosmetics_baked(item) else {
|
||||
continue;
|
||||
};
|
||||
for (bone_of, model) in &models {
|
||||
let known = bones.iter().flatten().find(|(b, _)| b == bone_of);
|
||||
let bone = match known {
|
||||
Some(&(_, matrix)) => matrix,
|
||||
None => {
|
||||
let Some(matrix) =
|
||||
render::part_matrix(body, base, body_poses, bone_of.part(), 0.0)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Some(free) = bones.iter_mut().find(|b| b.is_none()) {
|
||||
*free = Some((*bone_of, matrix));
|
||||
}
|
||||
matrix
|
||||
}
|
||||
};
|
||||
poses.reset(model);
|
||||
item.animate(ticks, |name, pose| *poses.get(name) = pose);
|
||||
if let (Some((t, w)), Some(e)) = (emoting, &item.emote) {
|
||||
e.pose_props(t, |n| item.rest(n), |n, pose| *poses.get(n) = pose);
|
||||
// Props grow in and shrink out with the emote's blend.
|
||||
let root = poses.get(&model.name);
|
||||
for s in &mut root[6..9] {
|
||||
*s *= w;
|
||||
}
|
||||
}
|
||||
bounds = self.cosmetics_model(model, bone, poses, uv, light, gpu, bounds, out);
|
||||
}
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
/// An item's models and its texture's corners in the atlas, once its texture is there (it is
|
||||
/// asked for meanwhile).
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn cosmetics_baked(
|
||||
&self,
|
||||
item: &Arc<Item>,
|
||||
) -> Option<(Vec<(horizon_cosmetics::Bone, Arc<Part>)>, [f32; 4])> {
|
||||
let frame = self.cosmetics.frame.get();
|
||||
let mut baked = self.cosmetics.baked.borrow_mut();
|
||||
let b = baked
|
||||
.entry(Arc::as_ptr(item) as usize)
|
||||
.or_insert_with(|| Baked::new(item, frame));
|
||||
let Some(rect) = self.gui.atlas.sprites.get(&b.location) else {
|
||||
self.cosmetics.uploads.borrow_mut().push(item.clone());
|
||||
return None;
|
||||
};
|
||||
if b.marked.replace(frame) != frame {
|
||||
self.skin_downloads.borrow_mut().drawn(&b.location);
|
||||
}
|
||||
// The texture fills the top-left of its 64×64 slot.
|
||||
let (w, h) = (
|
||||
item.texture.width as f32 / 64.0,
|
||||
item.texture.height as f32 / 64.0,
|
||||
);
|
||||
let uv = [
|
||||
rect.u0,
|
||||
rect.v0,
|
||||
rect.u0 + (rect.u1 - rect.u0) * w,
|
||||
rect.v0 + (rect.v1 - rect.v0) * h,
|
||||
];
|
||||
Some((b.models.clone(), uv))
|
||||
}
|
||||
|
||||
/// One model at `matrix`, posed by `poses`: on the GPU's skin path when `gpu` (the bounds
|
||||
/// grow to hold it), else its vertices go to `out`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn cosmetics_model(
|
||||
&self,
|
||||
model: &Arc<Part>,
|
||||
matrix: glam::Mat4,
|
||||
poses: &horizon_client::mobs::anim::Poses,
|
||||
uv: [f32; 4],
|
||||
light: u32,
|
||||
gpu: bool,
|
||||
bounds: Option<(glam::Vec3, glam::Vec3)>,
|
||||
out: &mut Vec<Vertex>,
|
||||
) -> Option<(glam::Vec3, glam::Vec3)> {
|
||||
if gpu {
|
||||
let mut models = self.skin_models.borrow_mut();
|
||||
let mut frame = self.skin_frame.borrow_mut();
|
||||
let mut skin = horizon_client::mobs::skin::Skin::new(&mut models, &mut frame);
|
||||
skin.bounds = bounds;
|
||||
let paint = horizon_client::mobs::skin::Paint {
|
||||
uv,
|
||||
scroll: 0.0,
|
||||
color: 0xFFFF_FFFF,
|
||||
light,
|
||||
overlay: 0,
|
||||
};
|
||||
if skin.draw(model, matrix, poses, paint) {
|
||||
return skin.bounds;
|
||||
}
|
||||
}
|
||||
horizon_client::mobs::render_model(
|
||||
model,
|
||||
matrix,
|
||||
&|p: &Part| poses.pose(p),
|
||||
uv,
|
||||
light,
|
||||
0,
|
||||
out,
|
||||
);
|
||||
bounds
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1293,6 +990,13 @@ mod tests {
|
||||
|
||||
const OTHER: u128 = 0x1234;
|
||||
|
||||
/// What `extras` tell the hooks.
|
||||
fn said_of(extras: &Extras) -> Said {
|
||||
let mut said = Said::default();
|
||||
said.settings(extras);
|
||||
said
|
||||
}
|
||||
|
||||
/// What another player wears as `extras` shows it, drawn frame after frame for up to
|
||||
/// `patience` (the service answers on its own thread), and what was asked of the service.
|
||||
fn other_player(
|
||||
@@ -1310,7 +1014,7 @@ mod tests {
|
||||
let client = horizon_cosmetics::Cosmetics::new(service.clone(), None);
|
||||
let deadline = Instant::now() + patience;
|
||||
let worn = loop {
|
||||
let worn = worn_of(&client, extras, false, || Some(OTHER));
|
||||
let worn = worn_of(&client, &said_of(extras), false, || Some(OTHER));
|
||||
if worn.is_some() || Instant::now() >= deadline {
|
||||
break worn;
|
||||
}
|
||||
@@ -1371,7 +1075,7 @@ mod tests {
|
||||
let uuid = || own_uuid(Some(OFFLINE), solo, extras.cosmetics_solo, || Some(ACCOUNT));
|
||||
let deadline = Instant::now() + patience;
|
||||
let worn = loop {
|
||||
let worn = worn_of(&client, &extras, true, uuid);
|
||||
let worn = worn_of(&client, &said_of(&extras), true, uuid);
|
||||
if worn.is_some() || Instant::now() >= deadline {
|
||||
break worn;
|
||||
}
|
||||
@@ -1529,10 +1233,16 @@ mod tests {
|
||||
|
||||
/// A player model as `player_layers` draws on it: one bone, the sprites it is given; logs
|
||||
/// each draw (mesh, matrix, uv, its root's scale once posed).
|
||||
#[derive(Default)]
|
||||
struct Canvas {
|
||||
pose: Option<Arc<dyn BodyPose>>,
|
||||
sprite: Option<[f32; 4]>,
|
||||
drawn: Vec<(String, [[f32; 4]; 4], [f32; 4], [f32; 3])>,
|
||||
/// The model's matrix (`LEFT_ARM` if None) and how it moves.
|
||||
base: Option<glam::Mat4>,
|
||||
motion: horizon_hooks::players::Motion,
|
||||
/// Each draw's look: see-through alpha and light.
|
||||
looks: Vec<(Option<f32>, horizon_hooks::players::Light)>,
|
||||
}
|
||||
|
||||
const LEFT_ARM: [[f32; 4]; 4] = [[2.0, 0.0, 0.0, 0.0]; 4];
|
||||
@@ -1544,6 +1254,22 @@ mod tests {
|
||||
fn ticks(&self) -> f32 {
|
||||
102.0
|
||||
}
|
||||
fn base(&self) -> [[f32; 4]; 4] {
|
||||
self.base.map_or(LEFT_ARM, |b| b.to_cols_array_2d())
|
||||
}
|
||||
fn motion(&self) -> horizon_hooks::players::Motion {
|
||||
self.motion
|
||||
}
|
||||
fn view(&self) -> horizon_hooks::players::View {
|
||||
Default::default()
|
||||
}
|
||||
fn particles(
|
||||
&mut self,
|
||||
_: [f32; 4],
|
||||
_: &[horizon_hooks::players::Particle],
|
||||
_: horizon_hooks::players::Light,
|
||||
) {
|
||||
}
|
||||
fn bone(&mut self, bone: Bone) -> Option<[[f32; 4]; 4]> {
|
||||
(bone == Bone::LeftArm).then_some(LEFT_ARM)
|
||||
}
|
||||
@@ -1557,7 +1283,7 @@ mod tests {
|
||||
&mut self,
|
||||
mesh: &Arc<Mesh>,
|
||||
matrix: [[f32; 4]; 4],
|
||||
uv: [f32; 4],
|
||||
look: Look,
|
||||
pose: &mut dyn FnMut(&mut dyn horizon_hooks::players::PartPoses),
|
||||
) {
|
||||
struct Rest(std::collections::HashMap<String, [f32; 9]>);
|
||||
@@ -1569,8 +1295,13 @@ mod tests {
|
||||
let mut rest = Rest([(mesh.name.clone(), mesh.pose)].into());
|
||||
pose(&mut rest);
|
||||
let root = rest.0[&mesh.name];
|
||||
self.drawn
|
||||
.push((mesh.name.clone(), matrix, uv, [root[6], root[7], root[8]]));
|
||||
self.looks.push((look.see_through, look.light));
|
||||
self.drawn.push((
|
||||
mesh.name.clone(),
|
||||
matrix,
|
||||
look.uv,
|
||||
[root[6], root[7], root[8]],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1600,8 +1331,7 @@ mod tests {
|
||||
emotes::set_model_playing(&mut view, 9, Some(playing));
|
||||
let mut canvas = Canvas {
|
||||
pose: view.0[0].1.clone(),
|
||||
sprite: None,
|
||||
drawn: Vec::new(),
|
||||
..Canvas::default()
|
||||
};
|
||||
// Others' cosmetics hidden, no service: the player's own emote still shows.
|
||||
let mut seat = Seat {
|
||||
@@ -1626,4 +1356,47 @@ mod tests {
|
||||
assert!((s - weight).abs() < 1e-6, "{scale:?} by {weight}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_aura_goes_see_through_at_the_feet_unless_swimming_far_or_hidden() {
|
||||
use horizon_hooks::players::{Light, Motion};
|
||||
let ring = CROWN
|
||||
.replace(r#""slot": "head", "bone": "head""#, r#""slot": "aura""#)
|
||||
// At the feet: an aura reaches at most 6 pixels above them.
|
||||
.replace(r#""attach": {"offset": [0, -18, 0]},"#, "")
|
||||
.replace(
|
||||
r#""uv": [0, 0]}]}]}"#,
|
||||
r#""uv": [0, 0]}]}], "aura": {"at": "feet", "alpha": 0.5}}"#,
|
||||
);
|
||||
let ring = Arc::new(Item::parse(ring.as_bytes()).unwrap());
|
||||
let at = glam::Vec3::new(3.0, 0.0, 4.0);
|
||||
let mut canvas = Canvas {
|
||||
sprite: Some([0.0, 0.0, 0.5, 0.5]),
|
||||
base: Some(glam::Mat4::from_translation(at)),
|
||||
..Canvas::default()
|
||||
};
|
||||
let other = PlayerModel::new(9, Kind::Player, Some(5), 102, false);
|
||||
let own = PlayerModel::new(1, Kind::Own, None, 102, false);
|
||||
let mut seat = Seat::default();
|
||||
seat.fx(&other, &ring, &mut canvas);
|
||||
// Half see-through, lit as the wearer, 24 pixels under the neck.
|
||||
assert_eq!(canvas.looks, [(Some(0.5), Light::Model)]);
|
||||
let feet = glam::Mat4::from_cols_array_2d(&canvas.drawn[0].1).w_axis;
|
||||
assert_eq!(feet.truncate(), at + glam::Vec3::new(0.0, 1.5, 0.0));
|
||||
// Swimming, it would tip over with the body: none.
|
||||
canvas.motion = Motion::default().poses(false, false, false, true);
|
||||
seat.fx(&other, &ring, &mut canvas);
|
||||
canvas.motion = Motion::default();
|
||||
// Past 48 blocks: none.
|
||||
canvas.base = Some(glam::Mat4::from_translation(glam::Vec3::X * 49.0));
|
||||
seat.fx(&other, &ring, &mut canvas);
|
||||
assert_eq!(canvas.looks.len(), 1);
|
||||
// Others' trails and auras hidden: theirs go, the player's own stays.
|
||||
canvas.base = Some(glam::Mat4::from_translation(at));
|
||||
seat.said.hide_fx = true;
|
||||
seat.fx(&other, &ring, &mut canvas);
|
||||
assert_eq!(canvas.looks.len(), 1);
|
||||
seat.fx(&own, &ring, &mut canvas);
|
||||
assert_eq!(canvas.looks.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -71,11 +71,6 @@ pub(in crate::game) fn pose_playing(pose: Option<&Arc<dyn BodyPose>>) -> Option<
|
||||
any.downcast_ref::<EmotePose>().map(|e| &e.0)
|
||||
}
|
||||
|
||||
/// The emote `m` plays, if any.
|
||||
pub(in crate::game) fn playing(m: &Mob) -> Option<&Playing> {
|
||||
pose_playing(m.emote.as_ref())
|
||||
}
|
||||
|
||||
/// The pose that plays `next` where `now` is laid, unless it plays that already (None: keep
|
||||
/// it; a new one allocates).
|
||||
fn replaced(
|
||||
|
||||
@@ -1,47 +1,78 @@
|
||||
//! Companions, trails and auras ([docs/cosmetics.md](../../../../../docs/cosmetics.md)): worn
|
||||
//! like the other cosmetics but on no bone. A companion floats beside its wearer and follows with
|
||||
//! a lag (`horizon_cosmetics::follow`), drawn with the wearer; a trail's particles
|
||||
//! (`horizon_cosmetics::trail`) and an aura blend, so they go after every player in one
|
||||
//! see-through span (`cosmetics_fx_geometry`). Nothing of them is an entity, collides or is sent
|
||||
//! to a server.
|
||||
//! (`horizon_cosmetics::trail`) and an aura blend, so they are drawn see-through after every
|
||||
//! player (`PlayerDraw::particles`, `Look::see_through`). Nothing of them is an entity, collides
|
||||
//! or is sent to a server.
|
||||
|
||||
use super::{FULL_LIGHT, OWN};
|
||||
use crate::game::Game;
|
||||
use super::Seat;
|
||||
use glam::{DVec3, Mat4, Vec3};
|
||||
use horizon_client::mesher::Vertex;
|
||||
use horizon_client::mobs::anim::Poses;
|
||||
use horizon_client::mobs::{Mob, Part};
|
||||
use horizon_client::fast_hash::FxHashMap;
|
||||
use horizon_cosmetics::follow::{self, Follow};
|
||||
use horizon_cosmetics::trail::Emitter;
|
||||
use horizon_cosmetics::trail::{Budget, Emitter};
|
||||
use horizon_cosmetics::{Item, Slot};
|
||||
use horizon_hooks::players::{Kind, Light, Look, Particle, Pass, PlayerDraw, PlayerModel};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
/// How far from the camera (blocks) a wearer's companion, trail and aura are drawn.
|
||||
const FAR: f32 = 48.0;
|
||||
|
||||
/// Who wears it, as drawn this frame.
|
||||
pub(super) struct Wearer<'a> {
|
||||
pub m: &'a Mob,
|
||||
/// The player's own model.
|
||||
pub own: bool,
|
||||
/// `OWN`, or the entity id.
|
||||
pub key: u32,
|
||||
/// The wearer's model matrix (`render::base_transform`), relative to `camera`.
|
||||
pub base: Mat4,
|
||||
pub camera: DVec3,
|
||||
pub alpha: f32,
|
||||
pub light: u32,
|
||||
/// The key of the player's own model in `Fx`'s maps (others go by entity id).
|
||||
const OWN: u32 = u32::MAX;
|
||||
|
||||
/// What companions and trails keep between frames, per wearer (marked with the frame they were
|
||||
/// last drawn in; the others are dropped).
|
||||
#[derive(Default)]
|
||||
pub(super) struct Fx {
|
||||
companions: FxHashMap<u32, (Follow, u32)>,
|
||||
trails: FxHashMap<u32, (Emitter, u32)>,
|
||||
budget: Budget,
|
||||
clock: Option<(Instant, Instant)>,
|
||||
/// Ticks since the first frame, and the last frame's length in seconds.
|
||||
now: f64,
|
||||
dt: f32,
|
||||
frame: u32,
|
||||
/// Kept so a frame of trails allocates none.
|
||||
particles: Vec<Particle>,
|
||||
}
|
||||
|
||||
/// `m`'s head yaw this frame (degrees).
|
||||
fn head_yaw(m: &Mob, alpha: f32) -> f32 {
|
||||
let turn = horizon_client::mobs::living::wrap(m.head_yaw - m.previous_head_yaw);
|
||||
m.previous_head_yaw + turn * alpha
|
||||
}
|
||||
impl Fx {
|
||||
/// A new frame: the clock moves on and what no frame drew since the last one goes.
|
||||
pub(super) fn frame(&mut self) {
|
||||
self.frame = self.frame.wrapping_add(1);
|
||||
let frame = self.frame;
|
||||
let now = Instant::now();
|
||||
let (start, last) = self.clock.get_or_insert((now, now));
|
||||
self.dt = now.duration_since(*last).as_secs_f32().min(0.25);
|
||||
self.now = now.duration_since(*start).as_secs_f64() * 20.0;
|
||||
*last = now;
|
||||
let seen = |mark: u32| frame.wrapping_sub(mark) <= 1;
|
||||
self.companions.retain(|_, (_, mark)| seen(*mark));
|
||||
let budget = &mut self.budget;
|
||||
self.trails.retain(|_, (e, mark)| {
|
||||
let keep = seen(*mark);
|
||||
if !keep {
|
||||
e.clear(budget);
|
||||
}
|
||||
keep
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether `m` lies in a bed.
|
||||
fn asleep(m: &Mob) -> bool {
|
||||
m.extra.avatar_pose == horizon_client::pose::Pose::Sleeping
|
||||
/// The companion of wearer `key` moved toward `target` (once a frame, however many times it
|
||||
/// is drawn).
|
||||
fn follow(&mut self, key: u32, target: DVec3, yaw: f32, lag: f32) -> Follow {
|
||||
let (frame, dt) = (self.frame, self.dt);
|
||||
let (f, mark) = self
|
||||
.companions
|
||||
.entry(key)
|
||||
.or_insert((Follow::at(target, yaw), frame));
|
||||
if *mark != frame {
|
||||
f.step(target, yaw, lag, dt);
|
||||
*mark = frame;
|
||||
}
|
||||
*f
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a companion floats: `at` (relative to the camera) facing `yaw`, at the wearer's scale,
|
||||
@@ -52,270 +83,144 @@ fn companion_matrix(at: Vec3, yaw: f32, scale: f32) -> Mat4 {
|
||||
* Mat4::from_scale(Vec3::new(-scale, -scale, scale))
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// A companion, trail or aura `at` wears: a companion goes where the wearer's other items go
|
||||
/// (`gpu` and `out`, `bounds` grown), trails and auras to the frame's see-through vertices.
|
||||
/// Other players' trails and auras stay hidden with "Hide other players' trails and auras".
|
||||
pub(super) fn cosmetics_fx(
|
||||
&self,
|
||||
at: &Wearer,
|
||||
item: &Arc<Item>,
|
||||
gpu: bool,
|
||||
bounds: Option<(Vec3, Vec3)>,
|
||||
poses: &mut Poses,
|
||||
out: &mut Vec<Vertex>,
|
||||
) -> Option<(Vec3, Vec3)> {
|
||||
if !at.own && item.slot.is_effect() && self.settings.extras.cosmetics_hide_fx {
|
||||
return bounds;
|
||||
/// Full light for what glows, else `otherwise`.
|
||||
fn glowing(glow: bool, otherwise: Light) -> Light {
|
||||
if glow { Light::Full } else { otherwise }
|
||||
}
|
||||
|
||||
impl Seat {
|
||||
/// A companion, trail or aura `m` wears, around its model as `draw` draws it. Other players'
|
||||
/// trails and auras stay hidden with "Hide other players' trails and auras".
|
||||
pub(super) fn fx(&mut self, m: &PlayerModel, item: &Arc<Item>, draw: &mut dyn PlayerDraw) {
|
||||
let own = m.kind == Kind::Own;
|
||||
if !own && item.slot.is_effect() && self.said.hide_fx {
|
||||
return;
|
||||
}
|
||||
// Past `FAR` a companion, trail or aura is a few pixels across for the cost of a model:
|
||||
// not drawn, and a trail that isn't drawn doesn't spawn (its particles go back to the
|
||||
// budget).
|
||||
if at.base.w_axis.truncate().length_squared() > FAR * FAR {
|
||||
return bounds;
|
||||
let base = Mat4::from_cols_array_2d(&draw.base());
|
||||
if base.w_axis.truncate().length_squared() > FAR * FAR {
|
||||
return;
|
||||
}
|
||||
let Some(uv) = self.uv(item, |id| draw.sprite(id)) else {
|
||||
return;
|
||||
};
|
||||
let key = if own { OWN } else { m.id };
|
||||
match item.slot {
|
||||
Slot::Companion => {
|
||||
let Some((model, uv, matrix)) = self.companion_at(at, item, None) else {
|
||||
return bounds;
|
||||
};
|
||||
self.pose_companion(item, at.m, at.alpha, poses, &model);
|
||||
let glow = item.companion.as_ref().is_some_and(|c| c.glow);
|
||||
let light = if glow { FULL_LIGHT } else { at.light };
|
||||
self.cosmetics_model(&model, matrix, poses, uv, light, gpu, bounds, out)
|
||||
}
|
||||
Slot::Trail => {
|
||||
self.trail(at, item);
|
||||
bounds
|
||||
}
|
||||
Slot::Aura => {
|
||||
self.aura(at, item, poses);
|
||||
bounds
|
||||
}
|
||||
_ => bounds,
|
||||
Slot::Companion => self.companion(key, item, uv, base, draw),
|
||||
Slot::Trail => self.trail(key, item, uv, draw),
|
||||
Slot::Aura => self.aura(item, uv, base, draw),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The companion's model, texture and matrix this frame, its follow moved on once a frame.
|
||||
/// `first_person`: the camera's forward and left, for the player's own in first person: the
|
||||
/// companion keeps out of the middle of the view.
|
||||
fn companion_at(
|
||||
&self,
|
||||
at: &Wearer,
|
||||
/// The companion where it floats, its follow moved on once a frame. In first person it keeps
|
||||
/// out of the middle of the view, lit where it floats.
|
||||
fn companion(
|
||||
&mut self,
|
||||
key: u32,
|
||||
item: &Arc<Item>,
|
||||
first_person: Option<(Vec3, Vec3)>,
|
||||
) -> Option<(Arc<Part>, [f32; 4], Mat4)> {
|
||||
let c = item.companion.as_ref()?;
|
||||
let (models, uv) = self.cosmetics_baked(item)?;
|
||||
let model = models.first()?.1.clone();
|
||||
let scale = at.base.x_axis.truncate().length();
|
||||
let target = at.base.transform_point3(Vec3::from(c.anchor) / 16.0);
|
||||
let head = at
|
||||
.base
|
||||
.transform_point3(Vec3::from(horizon_cosmetics::HEAD) / 16.0);
|
||||
let yaw = head_yaw(at.m, at.alpha);
|
||||
let follow = self.companion_follow(at.key, at.camera + target.as_dvec3(), yaw, c.lag);
|
||||
uv: [f32; 4],
|
||||
base: Mat4,
|
||||
draw: &mut dyn PlayerDraw,
|
||||
) {
|
||||
let Some(c) = item.companion.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some((_, mesh)) = self.kept(item).meshes.first().cloned() else {
|
||||
return;
|
||||
};
|
||||
let first_person = draw.pass() == Pass::FirstPerson;
|
||||
let (motion, view) = (draw.motion(), draw.view());
|
||||
let camera = DVec3::from(view.camera);
|
||||
let scale = base.x_axis.truncate().length();
|
||||
let target = base.transform_point3(Vec3::from(c.anchor) / 16.0);
|
||||
let head = base.transform_point3(Vec3::from(horizon_cosmetics::HEAD) / 16.0);
|
||||
let follow = self
|
||||
.fx
|
||||
.follow(key, camera + target.as_dvec3(), motion.head_yaw, c.lag);
|
||||
let clearance = (horizon_cosmetics::HEAD_CLEARANCE + c.reach) / 16.0 * scale;
|
||||
let mut rel = (follow.pos - at.camera).as_vec3();
|
||||
if let Some((forward, left)) = first_person {
|
||||
rel = follow::out_of_view(rel, forward, left);
|
||||
let mut rel = (follow.pos - camera).as_vec3();
|
||||
if first_person {
|
||||
rel = follow::out_of_view(rel, Vec3::from(view.forward), Vec3::from(view.left));
|
||||
}
|
||||
rel = follow::clear_of(rel, head, clearance);
|
||||
Some((model, uv, companion_matrix(rel, follow.yaw, scale)))
|
||||
}
|
||||
|
||||
/// The companion of wearer `key` moved toward `target` (once a frame, however many times it
|
||||
/// is drawn).
|
||||
fn companion_follow(&self, key: u32, target: DVec3, yaw: f32, lag: f32) -> Follow {
|
||||
let frame = self.cosmetics.frame.get();
|
||||
let mut fx = self.cosmetics.fx.borrow_mut();
|
||||
let dt = fx.dt;
|
||||
let (f, mark) = fx
|
||||
.companions
|
||||
.entry(key)
|
||||
.or_insert((Follow::at(target, yaw), frame));
|
||||
if *mark != frame {
|
||||
f.step(target, yaw, lag, dt);
|
||||
*mark = frame;
|
||||
}
|
||||
*f
|
||||
}
|
||||
|
||||
/// The companion's animations, and its `look` part turned with the wearer's pitch.
|
||||
fn pose_companion(&self, item: &Item, m: &Mob, alpha: f32, poses: &mut Poses, model: &Part) {
|
||||
poses.reset(model);
|
||||
item.animate(m.age as f32 + alpha, |name, pose| *poses.get(name) = pose);
|
||||
if let Some(look) = item.companion.as_ref().and_then(|c| c.look.as_deref()) {
|
||||
let pitch = m.previous_head_pitch + (m.head_pitch - m.previous_head_pitch) * alpha;
|
||||
poses.get(look)[3] += (pitch * 0.6).clamp(-30.0, 30.0).to_radians();
|
||||
}
|
||||
let matrix = companion_matrix(rel, follow.yaw, scale).to_cols_array_2d();
|
||||
let near = if first_person {
|
||||
Light::AtPlace
|
||||
} else {
|
||||
Light::Model
|
||||
};
|
||||
let look = Look::new(uv).lit(glowing(c.glow, near));
|
||||
let ticks = draw.ticks();
|
||||
// Its animations, and its `look` part turned with the wearer's pitch.
|
||||
let pitch = (motion.head_pitch * 0.6).clamp(-30.0, 30.0).to_radians();
|
||||
draw.draw(&mesh, matrix, look, &mut |p| {
|
||||
item.animate(ticks, |name, pose| *p.pose(name) = pose);
|
||||
if let Some(part) = c.look.as_deref() {
|
||||
p.pose(part)[3] += pitch;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The trail's particles: spawned once a frame by the distance walked (not while sneaking or
|
||||
/// asleep), facing the camera.
|
||||
fn trail(&self, at: &Wearer, item: &Arc<Item>) {
|
||||
fn trail(&mut self, key: u32, item: &Arc<Item>, uv: [f32; 4], draw: &mut dyn PlayerDraw) {
|
||||
let Some(t) = &item.trail else {
|
||||
return;
|
||||
};
|
||||
let Some((_, uv)) = self.cosmetics_baked(item) else {
|
||||
return;
|
||||
};
|
||||
let m = at.m;
|
||||
let a = f64::from(at.alpha);
|
||||
let feet = DVec3::new(
|
||||
m.previous.x + (m.position.x - m.previous.x) * a,
|
||||
m.previous.y + (m.position.y - m.previous.y) * a,
|
||||
m.previous.z + (m.position.z - m.previous.z) * a,
|
||||
);
|
||||
let moves = !m.extra.crouching && !asleep(m);
|
||||
let q = self.camera_orientation();
|
||||
let (right, up) = (q * Vec3::X, q * Vec3::Y);
|
||||
let light = if t.glow { FULL_LIGHT } else { at.light };
|
||||
let frame = self.cosmetics.frame.get();
|
||||
let mut fx = self.cosmetics.fx.borrow_mut();
|
||||
let super::Fx {
|
||||
let (motion, view) = (draw.motion(), draw.view());
|
||||
let moves = !motion.crouching && !motion.asleep;
|
||||
let Fx {
|
||||
trails,
|
||||
budget,
|
||||
now,
|
||||
clear,
|
||||
frame,
|
||||
particles,
|
||||
..
|
||||
} = &mut *fx;
|
||||
} = &mut self.fx;
|
||||
let (e, mark) = trails
|
||||
.entry(at.key)
|
||||
.entry(key)
|
||||
.or_insert_with(|| (Emitter::default(), frame.wrapping_sub(1)));
|
||||
if *mark != frame {
|
||||
e.step(t, feet, moves, *now, budget);
|
||||
*mark = frame;
|
||||
}
|
||||
for quad in e.quads(t, *now, at.camera, right, up) {
|
||||
let color = ((quad.alpha.clamp(0.0, 1.0) * 255.0) as u32) << 24 | 0x00FF_FFFF;
|
||||
for (corner, [u, v]) in quad.corners.iter().zip(quad.uv) {
|
||||
clear.push(Vertex {
|
||||
pos: corner.to_array(),
|
||||
uv: [uv[0] + (uv[2] - uv[0]) * u, uv[1] + (uv[3] - uv[1]) * v],
|
||||
layer: 0,
|
||||
color,
|
||||
light,
|
||||
// Unlit: a particle keeps its colour whichever way it faces.
|
||||
normal: [0, 0, 0, 0],
|
||||
glint_uv: [0.0, 0.0],
|
||||
});
|
||||
}
|
||||
if *mark != *frame {
|
||||
e.step(t, DVec3::from(motion.feet), moves, *now, budget);
|
||||
*mark = *frame;
|
||||
}
|
||||
let (camera, right, up) = (
|
||||
DVec3::from(view.camera),
|
||||
Vec3::from(view.right),
|
||||
Vec3::from(view.up),
|
||||
);
|
||||
particles.clear();
|
||||
particles.extend(
|
||||
e.quads(t, *now, camera, right, up)
|
||||
.map(|q| Particle::new(q.corners.map(|c| c.to_array()), q.uv, q.alpha)),
|
||||
);
|
||||
draw.particles(uv, particles, glowing(t.glow, Light::Model));
|
||||
}
|
||||
|
||||
/// The aura at the wearer's feet, see-through. None while they fly, swim or sleep: it would
|
||||
/// tip over with the body.
|
||||
fn aura(&self, at: &Wearer, item: &Arc<Item>, poses: &mut Poses) {
|
||||
let (m, Some(a)) = (at.m, &item.aura) else {
|
||||
fn aura(&mut self, item: &Arc<Item>, uv: [f32; 4], base: Mat4, draw: &mut dyn PlayerDraw) {
|
||||
let Some(a) = &item.aura else {
|
||||
return;
|
||||
};
|
||||
if m.extra.fall_flying || asleep(m) || m.extra.swim.amount(at.alpha) > 0.0 {
|
||||
let motion = draw.motion();
|
||||
if motion.flying || motion.asleep || motion.swimming {
|
||||
return;
|
||||
}
|
||||
let Some((models, uv)) = self.cosmetics_baked(item) else {
|
||||
return;
|
||||
};
|
||||
let Some((_, model)) = models.first() else {
|
||||
let Some((_, mesh)) = self.kept(item).meshes.first().cloned() else {
|
||||
return;
|
||||
};
|
||||
// The model's origin is the middle of the feet: 24 pixels under the neck.
|
||||
let matrix = at.base * Mat4::from_translation(Vec3::new(0.0, 24.0 / 16.0, 0.0));
|
||||
poses.reset(model);
|
||||
item.animate(m.age as f32 + at.alpha, |name, pose| {
|
||||
*poses.get(name) = pose
|
||||
let matrix = base * Mat4::from_translation(Vec3::new(0.0, 24.0 / 16.0, 0.0));
|
||||
let look = Look::new(uv)
|
||||
.lit(glowing(a.glow, Light::Model))
|
||||
.see_through(a.alpha);
|
||||
let ticks = draw.ticks();
|
||||
draw.draw(&mesh, matrix.to_cols_array_2d(), look, &mut |p| {
|
||||
item.animate(ticks, |name, pose| *p.pose(name) = pose)
|
||||
});
|
||||
let light = if a.glow { FULL_LIGHT } else { at.light };
|
||||
let mut fx = self.cosmetics.fx.borrow_mut();
|
||||
let start = fx.clear.len();
|
||||
horizon_client::mobs::render_model(
|
||||
model,
|
||||
matrix,
|
||||
&|p: &Part| poses.pose(p),
|
||||
uv,
|
||||
light,
|
||||
0,
|
||||
&mut fx.clear,
|
||||
);
|
||||
let color = ((a.alpha * 255.0) as u32) << 24 | 0x00FF_FFFF;
|
||||
for v in &mut fx.clear[start..] {
|
||||
v.color = color;
|
||||
// Seen through, a face turned away would add to the near one (and a card's two
|
||||
// faces would double it): `entity.wgsl` drops back faces, so it is `alpha` at most.
|
||||
v.normal[3] = horizon_client::mesher::SHADED_CULL_W;
|
||||
}
|
||||
}
|
||||
|
||||
/// After every player: the player's own companion in first person (out of the middle of the
|
||||
/// view, after the first-person model went to the shadow pass only), then the frame's trails
|
||||
/// and auras in one see-through span.
|
||||
pub(in crate::game) fn cosmetics_fx_geometry(
|
||||
&self,
|
||||
camera: DVec3,
|
||||
alpha: f64,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
if !super::enabled() {
|
||||
return;
|
||||
}
|
||||
if !self.own_body_in_view()
|
||||
&& let Some(m) = self.player_model.as_ref()
|
||||
{
|
||||
self.mark_entity(out.len(), "minecraft:player");
|
||||
self.own_companion_first_person(m, camera, alpha as f32, out);
|
||||
}
|
||||
let fx = self.cosmetics.fx.borrow();
|
||||
if !fx.clear.is_empty() {
|
||||
self.mark_translucent_entity(out.len(), "minecraft:player");
|
||||
out.extend_from_slice(&fx.clear);
|
||||
}
|
||||
}
|
||||
|
||||
fn own_companion_first_person(
|
||||
&self,
|
||||
m: &Mob,
|
||||
camera: DVec3,
|
||||
alpha: f32,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
if horizon_client::mobs::trader::is_invisible(m) || self.own_invisible() {
|
||||
return;
|
||||
}
|
||||
let Some(worn) = self.worn(m, true) else {
|
||||
return;
|
||||
};
|
||||
let Some(item) = worn.iter().find(|i| i.slot == Slot::Companion) else {
|
||||
return;
|
||||
};
|
||||
let at = Wearer {
|
||||
m,
|
||||
own: true,
|
||||
key: OWN,
|
||||
base: horizon_client::mobs::render::base_transform(m, alpha, camera),
|
||||
camera,
|
||||
alpha,
|
||||
light: FULL_LIGHT,
|
||||
};
|
||||
let (yaw, pitch) = (self.yaw.to_radians(), self.pitch.to_radians());
|
||||
let forward = Vec3::new(
|
||||
-yaw.sin() * pitch.cos(),
|
||||
-pitch.sin(),
|
||||
yaw.cos() * pitch.cos(),
|
||||
);
|
||||
let left = Vec3::new(yaw.cos(), 0.0, yaw.sin());
|
||||
let Some((model, uv, matrix)) = self.companion_at(&at, item, Some((forward, left))) else {
|
||||
return;
|
||||
};
|
||||
// Lit where it floats.
|
||||
let p = (camera + matrix.w_axis.truncate().as_dvec3()).floor();
|
||||
let light = self.world.light(p.x as i32, p.y as i32, p.z as i32);
|
||||
let mut light = (u32::from(light >> 4) * 16) | ((u32::from(light & 15) * 16) << 8);
|
||||
if item.companion.as_ref().is_some_and(|c| c.glow) {
|
||||
light = FULL_LIGHT;
|
||||
}
|
||||
let mut poses = self.cosmetics.poses.borrow_mut();
|
||||
let poses = &mut poses.1;
|
||||
self.pose_companion(item, m, alpha, poses, &model);
|
||||
self.cosmetics_model(&model, matrix, poses, uv, light, false, None, out);
|
||||
}
|
||||
}
|
||||
@@ -800,19 +800,8 @@ impl Game<'_> {
|
||||
packed,
|
||||
out,
|
||||
);
|
||||
if super::cosmetics::enabled() {
|
||||
let base = horizon_client::mobs::render::base_transform(m, alpha as f32, camera);
|
||||
self.cosmetics_geometry(
|
||||
m,
|
||||
true,
|
||||
base,
|
||||
Some(camera),
|
||||
alpha as f32,
|
||||
packed,
|
||||
false,
|
||||
out,
|
||||
);
|
||||
}
|
||||
let world = horizon_hooks::players::Pass::World;
|
||||
self.own_layers_geometry(m, world, camera, alpha as f32, packed, out);
|
||||
self.player_elytra_geometry(m, chest, camera, alpha as f32, packed, out);
|
||||
self.spin_attack_geometry(m, camera, alpha as f32, packed, out);
|
||||
self.shoulder_parrots_geometry(m, camera, alpha as f32, packed, out);
|
||||
@@ -926,10 +915,8 @@ impl Game<'_> {
|
||||
240 | 240 << 8,
|
||||
&mut verts,
|
||||
);
|
||||
if super::cosmetics::enabled() {
|
||||
let base = horizon_client::mobs::render::base_transform(&m, 1.0, camera);
|
||||
self.cosmetics_geometry(&m, true, base, None, 1.0, 240 | 240 << 8, false, &mut verts);
|
||||
}
|
||||
let picture = horizon_hooks::players::Pass::Picture;
|
||||
self.own_layers_geometry(&m, picture, camera, 1.0, 240 | 240 << 8, &mut verts);
|
||||
// `WingsLayer` draws through `RenderTypes.armorCutoutNoCull`: the
|
||||
// wings' faces show from both sides (the elytra texture is empty on
|
||||
// the face that looks at the body).
|
||||
|
||||
@@ -11,8 +11,8 @@ use horizon_client::mobs::anim::{self, BodyPose, PooledPoses, Poses};
|
||||
use horizon_client::mobs::{Mob, Part, Polygon, render};
|
||||
use horizon_hooks::engine;
|
||||
use horizon_hooks::players::{
|
||||
Bone, CameraView, Frame, Gear, Kind, Mesh, PartPoses, Pass, PlayerDraw, PlayerModel,
|
||||
PlayerModels, SpriteStore, Sprites,
|
||||
Bone, CameraView, Frame, Gear, Kind, Light, Look, Mesh, Motion, PartPoses, Particle, Pass,
|
||||
PlayerDraw, PlayerModel, PlayerModels, SpriteStore, Sprites, View,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -182,6 +182,14 @@ fn part_of(mesh: &Arc<Mesh>) -> Arc<Part> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Full light, packed as the vertices carry it.
|
||||
const FULL: u32 = 240 | 240 << 8;
|
||||
|
||||
thread_local! {
|
||||
/// What crates draw see-through this frame, added after every player.
|
||||
static SEE_THROUGH: std::cell::RefCell<Vec<Vertex>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// Forgets the meshes no crate holds any more (once a frame).
|
||||
fn forget_meshes() {
|
||||
let _ = MESHES.try_with(|m| {
|
||||
@@ -199,7 +207,7 @@ impl PartPoses for PosesOf<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A player model drawn in the world pass, as a crate draws on it: posed on the GPU's skin path
|
||||
/// A player model drawn, as a crate draws on it: posed on the GPU's skin path
|
||||
/// when `gpu` (the mob pass's skin frame, begun; the bounds of what went there grow, for
|
||||
/// culling), else its vertices go to `out`.
|
||||
struct DrawPlayer<'a, 'g> {
|
||||
@@ -208,6 +216,7 @@ struct DrawPlayer<'a, 'g> {
|
||||
pass: Pass,
|
||||
/// The model's matrix (`render::base_transform`).
|
||||
base: glam::Mat4,
|
||||
camera: glam::DVec3,
|
||||
alpha: f32,
|
||||
light: u32,
|
||||
gpu: bool,
|
||||
@@ -223,6 +232,19 @@ impl DrawPlayer<'_, '_> {
|
||||
let (key, _) = render::body(self.m);
|
||||
self.m.kind.layer(&key).unwrap_or(&self.m.kind.model)
|
||||
}
|
||||
|
||||
/// `light` packed; `at`: the place `Light::AtPlace` reads, relative to the camera.
|
||||
fn packed(&self, light: Light, at: glam::Vec3) -> u32 {
|
||||
match light {
|
||||
Light::Full => FULL,
|
||||
Light::AtPlace => {
|
||||
let p = (self.camera + at.as_dvec3()).floor();
|
||||
let l = self.game.world.light(p.x as i32, p.y as i32, p.z as i32);
|
||||
(u32::from(l >> 4) * 16) | ((u32::from(l & 15) * 16) << 8)
|
||||
}
|
||||
_ => self.light,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerDraw for DrawPlayer<'_, '_> {
|
||||
@@ -234,7 +256,52 @@ impl PlayerDraw for DrawPlayer<'_, '_> {
|
||||
self.m.age as f32 + self.alpha
|
||||
}
|
||||
|
||||
fn base(&self) -> [[f32; 4]; 4] {
|
||||
self.base.to_cols_array_2d()
|
||||
}
|
||||
|
||||
fn motion(&self) -> Motion {
|
||||
let (m, a) = (self.m, self.alpha);
|
||||
let t = f64::from(a);
|
||||
let (p, n) = (m.previous, m.position);
|
||||
let feet = [
|
||||
p.x + (n.x - p.x) * t,
|
||||
p.y + (n.y - p.y) * t,
|
||||
p.z + (n.z - p.z) * t,
|
||||
];
|
||||
let turn = horizon_client::mobs::living::wrap(m.head_yaw - m.previous_head_yaw);
|
||||
let yaw = m.previous_head_yaw + turn * a;
|
||||
let pitch = m.previous_head_pitch + (m.head_pitch - m.previous_head_pitch) * a;
|
||||
let asleep = m.extra.avatar_pose == horizon_client::pose::Pose::Sleeping;
|
||||
let swimming = m.extra.swim.amount(a) > 0.0;
|
||||
Motion::new(feet, yaw, pitch).poses(
|
||||
m.extra.crouching,
|
||||
asleep,
|
||||
m.extra.fall_flying,
|
||||
swimming,
|
||||
)
|
||||
}
|
||||
|
||||
fn view(&self) -> View {
|
||||
let q = self.game.camera_orientation();
|
||||
let (right, up) = (
|
||||
(q * glam::Vec3::X).to_array(),
|
||||
(q * glam::Vec3::Y).to_array(),
|
||||
);
|
||||
let (yaw, pitch) = (self.game.yaw.to_radians(), self.game.pitch.to_radians());
|
||||
let forward = [
|
||||
-yaw.sin() * pitch.cos(),
|
||||
-pitch.sin(),
|
||||
yaw.cos() * pitch.cos(),
|
||||
];
|
||||
let left = [yaw.cos(), 0.0, yaw.sin()];
|
||||
View::new(self.camera.to_array(), right, up).looking(forward, left)
|
||||
}
|
||||
|
||||
fn bone(&mut self, bone: Bone) -> Option<[[f32; 4]; 4]> {
|
||||
if self.pass == Pass::FirstPerson {
|
||||
return None;
|
||||
}
|
||||
if self.body.is_none() {
|
||||
let mut poses = Poses::pooled();
|
||||
poses.reset(self.body());
|
||||
@@ -260,13 +327,30 @@ impl PlayerDraw for DrawPlayer<'_, '_> {
|
||||
&mut self,
|
||||
mesh: &Arc<Mesh>,
|
||||
matrix: [[f32; 4]; 4],
|
||||
uv: [f32; 4],
|
||||
look: Look,
|
||||
pose: &mut dyn FnMut(&mut dyn PartPoses),
|
||||
) {
|
||||
let model = part_of(mesh);
|
||||
let matrix = glam::Mat4::from_cols_array_2d(&matrix);
|
||||
let (uv, light) = (look.uv, self.packed(look.light, matrix.w_axis.truncate()));
|
||||
self.poses.reset(&model);
|
||||
pose(&mut PosesOf(&mut self.poses));
|
||||
let poses = &self.poses;
|
||||
if let Some(alpha) = look.see_through {
|
||||
let color = ((alpha.clamp(0.0, 1.0) * 255.0) as u32) << 24 | 0x00FF_FFFF;
|
||||
let pose_of = |p: &Part| poses.pose(p);
|
||||
SEE_THROUGH.with_borrow_mut(|out| {
|
||||
let start = out.len();
|
||||
horizon_client::mobs::render_model(&model, matrix, &pose_of, uv, light, 0, out);
|
||||
for v in &mut out[start..] {
|
||||
v.color = color;
|
||||
// Seen through, a face turned away would add to the near one: `entity.wgsl`
|
||||
// drops back faces, so it is `alpha` at most.
|
||||
v.normal[3] = horizon_client::mesher::SHADED_CULL_W;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if self.gpu {
|
||||
let mut models = self.game.skin_models.borrow_mut();
|
||||
let mut frame = self.game.skin_frame.borrow_mut();
|
||||
@@ -276,30 +360,43 @@ impl PlayerDraw for DrawPlayer<'_, '_> {
|
||||
uv,
|
||||
scroll: 0.0,
|
||||
color: 0xFFFF_FFFF,
|
||||
light: self.light,
|
||||
light,
|
||||
overlay: 0,
|
||||
};
|
||||
if skin.draw(&model, matrix, &self.poses, paint) {
|
||||
if skin.draw(&model, matrix, poses, paint) {
|
||||
self.bounds = skin.bounds;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let poses = &self.poses;
|
||||
horizon_client::mobs::render_model(
|
||||
&model,
|
||||
matrix,
|
||||
&|p: &Part| poses.pose(p),
|
||||
uv,
|
||||
self.light,
|
||||
0,
|
||||
self.out,
|
||||
);
|
||||
let pose_of = |p: &Part| poses.pose(p);
|
||||
horizon_client::mobs::render_model(&model, matrix, &pose_of, uv, light, 0, self.out);
|
||||
}
|
||||
|
||||
fn particles(&mut self, uv: [f32; 4], particles: &[Particle], light: Light) {
|
||||
let light = self.packed(light, glam::Vec3::ZERO);
|
||||
SEE_THROUGH.with_borrow_mut(|out| {
|
||||
for q in particles {
|
||||
let color = ((q.alpha.clamp(0.0, 1.0) * 255.0) as u32) << 24 | 0x00FF_FFFF;
|
||||
for (corner, [u, v]) in q.corners.iter().zip(q.uv) {
|
||||
out.push(Vertex {
|
||||
pos: *corner,
|
||||
uv: [uv[0] + (uv[2] - uv[0]) * u, uv[1] + (uv[3] - uv[1]) * v],
|
||||
layer: 0,
|
||||
color,
|
||||
light,
|
||||
// Unlit: a particle keeps its colour whichever way it faces.
|
||||
normal: [0, 0, 0, 0],
|
||||
glint_uv: [0.0, 0.0],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// What crates draw on player model `m` in the world pass (`creatures.rs`), then the
|
||||
/// cosmetics still drawn here: see [`DrawPlayer`]. The bounds of what went to the GPU.
|
||||
/// What crates draw on player model `m` in the world pass (`creatures.rs`): see
|
||||
/// [`DrawPlayer`]. The bounds of what went to the GPU.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn player_layers_geometry(
|
||||
&self,
|
||||
@@ -315,33 +412,102 @@ impl Game<'_> {
|
||||
self.serving.is_none(),
|
||||
"a server thread calls no client hook"
|
||||
);
|
||||
let kind = kind(m)?;
|
||||
let mut bounds = None;
|
||||
if engine::any() {
|
||||
let model = model(m, kind, self.remote_player_uuid(m.id));
|
||||
let kind = kind(m).filter(|_| engine::any())?;
|
||||
let model = model(m, kind, self.remote_player_uuid(m.id));
|
||||
let mut draw = DrawPlayer {
|
||||
game: self,
|
||||
m,
|
||||
pass: Pass::World,
|
||||
base,
|
||||
camera,
|
||||
alpha,
|
||||
light,
|
||||
gpu,
|
||||
out,
|
||||
bounds: None,
|
||||
body: None,
|
||||
poses: Poses::pooled(),
|
||||
};
|
||||
engine::player_layers(&model, &mut draw);
|
||||
draw.bounds
|
||||
}
|
||||
|
||||
/// What crates draw on the window's own model `m` (its vertices to `out`): in the world in
|
||||
/// third person (`Pass::World`), or in the inventory's picture of it (`Pass::Picture`).
|
||||
/// `camera`: the world view's camera, or the picture's.
|
||||
pub(super) fn own_layers_geometry(
|
||||
&self,
|
||||
m: &Mob,
|
||||
pass: Pass,
|
||||
camera: glam::DVec3,
|
||||
alpha: f32,
|
||||
light: u32,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
debug_assert!(
|
||||
self.serving.is_none(),
|
||||
"a server thread calls no client hook"
|
||||
);
|
||||
if !engine::any() {
|
||||
return;
|
||||
}
|
||||
let model = model(m, Kind::Own, self.remote.as_ref().map(|r| r.profile()));
|
||||
let mut draw = DrawPlayer {
|
||||
game: self,
|
||||
m,
|
||||
pass,
|
||||
base: render::base_transform(m, alpha, camera),
|
||||
camera,
|
||||
alpha,
|
||||
light,
|
||||
gpu: false,
|
||||
out,
|
||||
bounds: None,
|
||||
body: None,
|
||||
poses: Poses::pooled(),
|
||||
};
|
||||
engine::player_layers(&model, &mut draw);
|
||||
}
|
||||
|
||||
/// After every player (`game.rs`): what crates draw around the window's own model in first
|
||||
/// person, then what was drawn see-through, in one span.
|
||||
pub(super) fn players_after_geometry(
|
||||
&self,
|
||||
camera: glam::DVec3,
|
||||
alpha: f64,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
if engine::any()
|
||||
&& !self.own_body_in_view()
|
||||
&& let Some(m) = self.player_model.as_ref()
|
||||
{
|
||||
let own = model(m, Kind::Own, self.remote.as_ref().map(|r| r.profile()));
|
||||
let own = own.looks(own.gear, own.invisible || self.own_invisible());
|
||||
self.mark_entity(out.len(), "minecraft:player");
|
||||
let mut draw = DrawPlayer {
|
||||
game: self,
|
||||
m,
|
||||
pass: Pass::World,
|
||||
base,
|
||||
alpha,
|
||||
light,
|
||||
gpu,
|
||||
pass: Pass::FirstPerson,
|
||||
base: render::base_transform(m, alpha as f32, camera),
|
||||
camera,
|
||||
alpha: alpha as f32,
|
||||
light: FULL,
|
||||
gpu: false,
|
||||
out: &mut *out,
|
||||
bounds: None,
|
||||
body: None,
|
||||
poses: Poses::pooled(),
|
||||
};
|
||||
engine::player_layers(&model, &mut draw);
|
||||
bounds = draw.bounds;
|
||||
}
|
||||
let worn = super::cosmetics::enabled()
|
||||
.then(|| self.cosmetics_geometry(m, false, base, Some(camera), alpha, light, gpu, out))
|
||||
.flatten();
|
||||
match (bounds, worn) {
|
||||
(Some((l, h)), Some((lo, hi))) => Some((l.min(lo), h.max(hi))),
|
||||
(b, w) => b.or(w),
|
||||
engine::first_person(&own, &mut draw);
|
||||
}
|
||||
let _ = SEE_THROUGH.try_with(|v| {
|
||||
let mut v = v.borrow_mut();
|
||||
if !v.is_empty() {
|
||||
self.mark_translucent_entity(out.len(), "minecraft:player");
|
||||
out.extend_from_slice(&v);
|
||||
v.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Once a frame, before drawing.
|
||||
@@ -358,6 +524,7 @@ impl Game<'_> {
|
||||
let frame = Frame::new(connection, self.integrated.is_some());
|
||||
engine::frame(&frame, &mut HookStore(self));
|
||||
forget_meshes();
|
||||
let _ = SEE_THROUGH.try_with(|v| v.borrow_mut().clear());
|
||||
self.cosmetics_frame();
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,10 @@ pub trait Hooks: Sync {
|
||||
/// Draws on a player model, after its vanilla layers, each time it is drawn.
|
||||
fn player_layers(&self, _model: &players::PlayerModel, _draw: &mut dyn players::PlayerDraw) {}
|
||||
|
||||
/// Draws around the window's own model while its body is out of view (first person), once a
|
||||
/// frame after the first-person model: [`players::Pass::FirstPerson`].
|
||||
fn first_person(&self, _own: &players::PlayerModel, _draw: &mut dyn players::PlayerDraw) {}
|
||||
|
||||
/// The resolver of `friend:` addresses this crate offers ([`tunnel`]); the first hook that
|
||||
/// offers one is used. Asked on the network thread when joining and on the game thread when
|
||||
/// an address is shown, never per frame.
|
||||
@@ -214,6 +218,13 @@ pub mod engine {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::first_person`].
|
||||
pub fn first_person(own: &players::PlayerModel, draw: &mut dyn players::PlayerDraw) {
|
||||
for hooks in all() {
|
||||
hooks.first_person(own, draw);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::camera`].
|
||||
pub fn camera(own: &players::CameraView) -> Option<players::CameraOverride> {
|
||||
all().iter().find_map(|h| h.camera(own))
|
||||
@@ -282,7 +293,14 @@ mod tests {
|
||||
return;
|
||||
};
|
||||
let turn = draw.ticks() / 100.0;
|
||||
draw.draw(&HAT, head, uv, &mut |p| p.pose("brim")[4] = turn);
|
||||
let look = players::Look::new(uv);
|
||||
draw.draw(&HAT, head, look, &mut |p| p.pose("brim")[4] = turn);
|
||||
}
|
||||
/// A spark at the eyes, as bright as the head turns.
|
||||
fn first_person(&self, _: &players::PlayerModel, draw: &mut dyn players::PlayerDraw) {
|
||||
let alpha = draw.motion().head_yaw / 90.0;
|
||||
let spark = players::Particle::new([[0.0; 3]; 4], [[0.0; 2]; 4], alpha);
|
||||
draw.particles([0.0; 4], &[spark], players::Light::Full);
|
||||
}
|
||||
fn tick(&self, models: &mut dyn players::PlayerModels) {
|
||||
let mut ids = Vec::new();
|
||||
@@ -359,9 +377,10 @@ mod tests {
|
||||
))
|
||||
});
|
||||
|
||||
/// A player drawn at age 50, its head where the camera is; logs each draw.
|
||||
/// A player drawn at age 50, its head where the camera is, turned 45 degrees; logs each draw
|
||||
/// and its particles' alphas.
|
||||
#[derive(Default)]
|
||||
struct Canvas(Vec<(String, [[f32; 4]; 4], [f32; 4], f32)>);
|
||||
struct Canvas(Vec<(String, [[f32; 4]; 4], [f32; 4], f32)>, Vec<f32>);
|
||||
impl players::PlayerDraw for Canvas {
|
||||
fn pass(&self) -> players::Pass {
|
||||
players::Pass::World
|
||||
@@ -369,6 +388,15 @@ mod tests {
|
||||
fn ticks(&self) -> f32 {
|
||||
50.0
|
||||
}
|
||||
fn base(&self) -> [[f32; 4]; 4] {
|
||||
[[1.0, 0.0, 0.0, 0.0]; 4]
|
||||
}
|
||||
fn motion(&self) -> players::Motion {
|
||||
players::Motion::new([0.0; 3], 45.0, 0.0)
|
||||
}
|
||||
fn view(&self) -> players::View {
|
||||
players::View::default()
|
||||
}
|
||||
fn bone(&mut self, bone: players::Bone) -> Option<[[f32; 4]; 4]> {
|
||||
(bone == players::Bone::Head).then_some([[1.0, 0.0, 0.0, 0.0]; 4])
|
||||
}
|
||||
@@ -382,7 +410,7 @@ mod tests {
|
||||
&mut self,
|
||||
mesh: &std::sync::Arc<players::Mesh>,
|
||||
matrix: [[f32; 4]; 4],
|
||||
uv: [f32; 4],
|
||||
look: players::Look,
|
||||
pose: &mut dyn FnMut(&mut dyn players::PartPoses),
|
||||
) {
|
||||
struct Rest([f32; 9]);
|
||||
@@ -393,7 +421,10 @@ mod tests {
|
||||
}
|
||||
let mut brim = Rest(mesh.children[0].pose);
|
||||
pose(&mut brim);
|
||||
self.0.push((mesh.name.clone(), matrix, uv, brim.0[4]));
|
||||
self.0.push((mesh.name.clone(), matrix, look.uv, brim.0[4]));
|
||||
}
|
||||
fn particles(&mut self, _: [f32; 4], particles: &[players::Particle], _: players::Light) {
|
||||
self.1.extend(particles.iter().map(|p| p.alpha));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,6 +473,8 @@ mod tests {
|
||||
0.5
|
||||
)]
|
||||
);
|
||||
engine::first_person(&model(players::Kind::Own), &mut canvas);
|
||||
assert_eq!(canvas.1, [0.5]);
|
||||
let mut models = Two(Default::default());
|
||||
engine::tick(&mut models);
|
||||
assert!(models.body_pose(3).is_some() && models.body_pose(7).is_some());
|
||||
|
||||
@@ -274,8 +274,149 @@ impl Bone {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Pass {
|
||||
/// In the world, seen by the camera.
|
||||
/// In the world, seen by the camera (the window's own model too, in third person).
|
||||
World,
|
||||
/// The inventory's picture of the window's own model.
|
||||
Picture,
|
||||
/// The window's own model while its body is out of view (first person): no bone, only what
|
||||
/// floats around it ([`crate::Hooks::first_person`]).
|
||||
FirstPerson,
|
||||
}
|
||||
|
||||
/// How a player model moves this frame ([`PlayerDraw::motion`]), with the frame's part of a tick.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct Motion {
|
||||
/// Where its feet are, in the world.
|
||||
pub feet: [f64; 3],
|
||||
/// Where its head turns (degrees, the game's yaw and pitch).
|
||||
pub head_yaw: f32,
|
||||
pub head_pitch: f32,
|
||||
pub crouching: bool,
|
||||
/// Lies in a bed.
|
||||
pub asleep: bool,
|
||||
/// Flies with an elytra.
|
||||
pub flying: bool,
|
||||
/// Swims (any part of the swimming pose).
|
||||
pub swimming: bool,
|
||||
}
|
||||
|
||||
impl Motion {
|
||||
pub fn new(feet: [f64; 3], head_yaw: f32, head_pitch: f32) -> Self {
|
||||
Self {
|
||||
feet,
|
||||
head_yaw,
|
||||
head_pitch,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// With its poses: crouching, asleep, flying, swimming.
|
||||
pub fn poses(self, crouching: bool, asleep: bool, flying: bool, swimming: bool) -> Self {
|
||||
Self {
|
||||
crouching,
|
||||
asleep,
|
||||
flying,
|
||||
swimming,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The camera a player model is drawn for ([`PlayerDraw::view`]).
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct View {
|
||||
/// Where it is, in the world: matrices and [`Particle`] corners are relative to it.
|
||||
pub camera: [f64; 3],
|
||||
/// Its right and up, for what faces it.
|
||||
pub right: [f32; 3],
|
||||
pub up: [f32; 3],
|
||||
/// Where the window's player looks, and their level left (first person).
|
||||
pub forward: [f32; 3],
|
||||
pub left: [f32; 3],
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(camera: [f64; 3], right: [f32; 3], up: [f32; 3]) -> Self {
|
||||
Self {
|
||||
camera,
|
||||
right,
|
||||
up,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// With where the window's player looks and their level left.
|
||||
pub fn looking(self, forward: [f32; 3], left: [f32; 3]) -> Self {
|
||||
Self {
|
||||
forward,
|
||||
left,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How a mesh or particles are lit.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Light {
|
||||
/// As the player model is.
|
||||
Model,
|
||||
/// Fully lit: it glows.
|
||||
Full,
|
||||
/// By the world where the mesh's origin is (as particles: like [`Light::Model`]).
|
||||
AtPlace,
|
||||
}
|
||||
|
||||
/// How [`PlayerDraw::draw`] draws a mesh.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct Look {
|
||||
/// The sprite part it is textured by (u0, v0, u1, v1).
|
||||
pub uv: [f32; 4],
|
||||
pub light: Light,
|
||||
/// Drawn see-through at this alpha, after every player, its back faces dropped.
|
||||
pub see_through: Option<f32>,
|
||||
}
|
||||
|
||||
impl Look {
|
||||
/// Opaque, lit as the model.
|
||||
pub fn new(uv: [f32; 4]) -> Self {
|
||||
Self {
|
||||
uv,
|
||||
light: Light::Model,
|
||||
see_through: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lit(self, light: Light) -> Self {
|
||||
Self { light, ..self }
|
||||
}
|
||||
|
||||
pub fn see_through(self, alpha: f32) -> Self {
|
||||
Self {
|
||||
see_through: Some(alpha),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A quad that faces the camera ([`PlayerDraw::particles`]).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct Particle {
|
||||
/// Its corners, relative to the camera.
|
||||
pub corners: [[f32; 3]; 4],
|
||||
/// Each corner's place across the sprite part (0 to 1).
|
||||
pub uv: [[f32; 2]; 4],
|
||||
pub alpha: f32,
|
||||
}
|
||||
|
||||
impl Particle {
|
||||
pub fn new(corners: [[f32; 3]; 4], uv: [[f32; 2]; 4], alpha: f32) -> Self {
|
||||
Self { corners, uv, alpha }
|
||||
}
|
||||
}
|
||||
|
||||
/// The poses of a [`Mesh`]'s parts for one [`PlayerDraw::draw`], each at rest to begin with.
|
||||
@@ -291,19 +432,25 @@ pub trait PlayerDraw {
|
||||
fn pass(&self) -> Pass;
|
||||
/// The model's age in ticks, with the frame's part of a tick.
|
||||
fn ticks(&self) -> f32;
|
||||
/// Where `bone` is as the body is drawn this frame; None if the model has no such part.
|
||||
/// The model's own matrix, its origin at the neck (y down, 16 units a block).
|
||||
fn base(&self) -> [[f32; 4]; 4];
|
||||
fn motion(&self) -> Motion;
|
||||
fn view(&self) -> View;
|
||||
/// Where `bone` is as the body is drawn this frame; None if the model has no such part (or
|
||||
/// no body is drawn: [`Pass::FirstPerson`]).
|
||||
fn bone(&mut self, bone: Bone) -> Option<[[f32; 4]; 4]>;
|
||||
/// The pose laid over the model's body ([`PlayerModels::set_body_pose`]), if any.
|
||||
fn body_pose(&self) -> Option<&Arc<dyn BodyPose>>;
|
||||
/// As [`Sprites::get`].
|
||||
fn sprite(&self, id: u64) -> Option<[f32; 4]>;
|
||||
/// Draws `mesh` at `matrix`, textured by the sprite part `uv` (u0, v0, u1, v1) and lit as the
|
||||
/// model is; `pose` poses its parts first.
|
||||
/// Draws `mesh` at `matrix` as `look` says; `pose` poses its parts first.
|
||||
fn draw(
|
||||
&mut self,
|
||||
mesh: &Arc<Mesh>,
|
||||
matrix: [[f32; 4]; 4],
|
||||
uv: [f32; 4],
|
||||
look: Look,
|
||||
pose: &mut dyn FnMut(&mut dyn PartPoses),
|
||||
);
|
||||
/// Draws `particles`, textured by the sprite part `uv`, see-through after every player.
|
||||
fn particles(&mut self, uv: [f32; 4], particles: &[Particle], light: Light);
|
||||
}
|
||||
@@ -50,9 +50,9 @@ pub mod hooks {
|
||||
/// [`Hooks::tick`] get, the pose a crate lays over a player's body, what goes by their names,
|
||||
/// the crate's sprites, the camera, and what a crate draws on them.
|
||||
pub use horizon_hooks::players::{
|
||||
BODY_PARTS, Badge, BodyPose, Bone, CameraOverride, CameraView, Frame, Gear, Kind, Mark,
|
||||
Mesh, NameDecoration, PartPoses, Pass, PlayerDraw, PlayerModel, PlayerModels, Quad,
|
||||
SpriteStore, Sprites,
|
||||
BODY_PARTS, Badge, BodyPose, Bone, CameraOverride, CameraView, Frame, Gear, Kind, Light,
|
||||
Look, Mark, Mesh, Motion, NameDecoration, PartPoses, Particle, Pass, PlayerDraw,
|
||||
PlayerModel, PlayerModels, Quad, SpriteStore, Sprites, View,
|
||||
};
|
||||
pub use horizon_hooks::{Hooks, Started, TooLate, register, seat, tunnel};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user