mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 10:39:46 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fd4e22139 | ||
|
|
ec19b6320a | ||
|
|
16e29a2774 | ||
|
|
e2afd13715 | ||
|
|
d016678e24 | ||
|
|
984b7d8635 | ||
|
|
afa60454b2 | ||
|
|
aa8592fbf9 | ||
|
|
0c77cd5898 | ||
|
|
641a5859a2 | ||
|
|
7d17f516e5 |
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);
|
||||
|
||||
@@ -21,8 +21,8 @@ use horizon_cosmetics::prefs::{self, Others};
|
||||
use horizon_cosmetics::trail::{Budget, Emitter};
|
||||
use horizon_cosmetics::{Item, Slot};
|
||||
use horizon_hooks::players::{
|
||||
Badge, CameraOverride, CameraView, Frame, Gear, Kind, Mark, NameDecoration, PlayerModel,
|
||||
PlayerModels, SpriteStore, Sprites,
|
||||
Badge, Bone, CameraOverride, CameraView, Frame, Gear, Kind, Look, Mark, Mesh, NameDecoration,
|
||||
PlayerDraw, PlayerModel, PlayerModels, Quad, SpriteStore, Sprites,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::sync::mpsc::{Receiver, channel};
|
||||
@@ -66,8 +66,8 @@ pub(super) struct Cosmetics {
|
||||
/// 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)>,
|
||||
/// Kept between players so a frame of avatars allocates none: a companion or an aura.
|
||||
poses: RefCell<Poses>,
|
||||
/// Item files from a folder (`HORIZON_COSMETICS_LOCAL`): mannequins wear them too.
|
||||
local: bool,
|
||||
/// Companions, trails and auras.
|
||||
@@ -463,14 +463,45 @@ struct Seat {
|
||||
models: Vec<PlayerModel>,
|
||||
/// The world is the window's own single-player one (`Frame::solo`).
|
||||
solo: bool,
|
||||
/// Name tag badges' sprites, by the item's address: their id (never reused) and the item,
|
||||
/// held so the address isn't reused while it is there.
|
||||
sprites: FxHashMap<usize, (u64, Arc<Item>)>,
|
||||
/// The items the hooks show, by the item's address.
|
||||
items: FxHashMap<usize, Kept>,
|
||||
last_sprite: u64,
|
||||
/// Sprites drawing missed, put in the atlas next frame.
|
||||
wanted: Vec<u64>,
|
||||
}
|
||||
|
||||
/// An item the hooks show: its sprite's id (never reused) and a mesh per bone it rides, held
|
||||
/// with the item so its address isn't reused while it is here.
|
||||
struct Kept {
|
||||
sprite: u64,
|
||||
item: Arc<Item>,
|
||||
meshes: Vec<(Bone, Arc<Mesh>)>,
|
||||
}
|
||||
|
||||
/// The hooks' model of an item's part.
|
||||
fn mesh(p: &horizon_cosmetics::Part) -> Mesh {
|
||||
let quads = p.faces.iter().map(|f| Quad::new(f.normal, f.vertices));
|
||||
Mesh::new(
|
||||
p.name.clone(),
|
||||
p.pose,
|
||||
quads.collect(),
|
||||
p.children.iter().map(mesh).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The hooks' name of a bone.
|
||||
fn bone(b: horizon_cosmetics::Bone) -> Bone {
|
||||
use horizon_cosmetics::Bone as B;
|
||||
match b {
|
||||
B::Head => Bone::Head,
|
||||
B::Body => Bone::Body,
|
||||
B::LeftArm => Bone::LeftArm,
|
||||
B::RightArm => Bone::RightArm,
|
||||
B::LeftLeg => Bone::LeftLeg,
|
||||
B::RightLeg => Bone::RightLeg,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the game tells the hooks before each call, until the settings move here (spec #14091
|
||||
/// step 5a) and the player's emote with them (step 4).
|
||||
#[derive(Default)]
|
||||
@@ -539,6 +570,10 @@ impl horizon_hooks::Hooks for Hooks {
|
||||
}
|
||||
|
||||
/// An emote pulls the camera back: it eases out over the emote's first quarter second.
|
||||
fn player_layers(&self, model: &PlayerModel, draw: &mut dyn PlayerDraw) {
|
||||
service(|s| s.player_layers(model, 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);
|
||||
@@ -664,14 +699,7 @@ impl Seat {
|
||||
if !(styles || said.marks) {
|
||||
return None;
|
||||
}
|
||||
let uuid = match m.kind {
|
||||
Kind::Own => own_uuid(m.uuid, self.solo, said.solo, || {
|
||||
horizon_client::auth::current().map(|s| s.uuid)
|
||||
}),
|
||||
Kind::Mannequin if self.local => Some(u128::from(m.id)),
|
||||
Kind::Player => m.uuid,
|
||||
_ => None,
|
||||
}?;
|
||||
let uuid = self.uuid_of(m)?;
|
||||
let mut frame = self.client.as_ref()?.frame();
|
||||
let mark = (said.marks && frame.horizon(uuid)).then_some(MARK);
|
||||
let worn = if styles { frame.worn(uuid) } else { None };
|
||||
@@ -687,42 +715,135 @@ impl Seat {
|
||||
Some(NameDecoration::new(mark, badge, top))
|
||||
}
|
||||
|
||||
/// The name tag style in `worn`: its badge's place in the atlas and its line's colour. None
|
||||
/// until the badge is in the atlas (it is put there next frame).
|
||||
fn badge(&mut self, worn: &horizon_cosmetics::Worn, sprites: &dyn Sprites) -> Option<Badge> {
|
||||
let item = worn.iter().find(|i| i.slot == Slot::NameTag)?;
|
||||
let [r, g, b] = item.tag?.line;
|
||||
/// The UUID the service knows `m` by: a player's, or a mannequin's id with local items.
|
||||
fn uuid_of(&self, m: &PlayerModel) -> Option<u128> {
|
||||
match m.kind {
|
||||
Kind::Own => own_uuid(m.uuid, self.solo, self.said.solo, || {
|
||||
horizon_client::auth::current().map(|s| s.uuid)
|
||||
}),
|
||||
Kind::Mannequin if self.local => Some(u128::from(m.id)),
|
||||
Kind::Player => m.uuid,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `item` as the hooks show it, kept from its first sight.
|
||||
fn kept(&mut self, item: &Arc<Item>) -> &Kept {
|
||||
let next = self.last_sprite + 1;
|
||||
let (id, _) = *self
|
||||
.sprites
|
||||
let kept = self
|
||||
.items
|
||||
.entry(Arc::as_ptr(item) as usize)
|
||||
.or_insert_with(|| (next, item.clone()));
|
||||
self.last_sprite = self.last_sprite.max(id);
|
||||
let Some([u0, v0, u1, v1]) = sprites.get(id) else {
|
||||
.or_insert_with(|| Kept {
|
||||
sprite: next,
|
||||
item: item.clone(),
|
||||
meshes: item
|
||||
.roots()
|
||||
.map(|(b, root)| (bone(b), Arc::new(mesh(root))))
|
||||
.collect(),
|
||||
});
|
||||
self.last_sprite = self.last_sprite.max(kept.sprite);
|
||||
kept
|
||||
}
|
||||
|
||||
/// Where `item`'s texture is in the atlas (`get` reads the crate's sprites): the top left
|
||||
/// corner of its slot. None until it is there (it is put there next frame).
|
||||
fn uv(&mut self, item: &Arc<Item>, get: impl Fn(u64) -> Option<[f32; 4]>) -> Option<[f32; 4]> {
|
||||
let id = self.kept(item).sprite;
|
||||
let Some([u0, v0, u1, v1]) = get(id) else {
|
||||
if !self.wanted.contains(&id) {
|
||||
self.wanted.push(id);
|
||||
}
|
||||
return None;
|
||||
};
|
||||
// The mark sits in the slot's top left corner.
|
||||
let u1 = u0 + (u1 - u0) * item.texture.width as f32 / 64.0;
|
||||
let v1 = v0 + (v1 - v0) * item.texture.height as f32 / 64.0;
|
||||
Some(Badge::new(
|
||||
[u0, v0, u1, v1],
|
||||
u32::from_be_bytes([0, r, g, b]),
|
||||
))
|
||||
let (w, h) = (
|
||||
item.texture.width as f32 / 64.0,
|
||||
item.texture.height as f32 / 64.0,
|
||||
);
|
||||
Some([u0, v0, u0 + (u1 - u0) * w, v0 + (v1 - v0) * h])
|
||||
}
|
||||
|
||||
/// Puts the sprites drawing missed in the atlas; forgets those of items no one wears.
|
||||
/// The name tag style in `worn`: its badge and its line's colour. None until the badge is in
|
||||
/// the atlas.
|
||||
fn badge(&mut self, worn: &horizon_cosmetics::Worn, sprites: &dyn Sprites) -> Option<Badge> {
|
||||
let item = worn.iter().find(|i| i.slot == Slot::NameTag)?;
|
||||
let [r, g, b] = item.tag?.line;
|
||||
let uv = self.uv(item, |id| sprites.get(id))?;
|
||||
Some(Badge::new(uv, u32::from_be_bytes([0, r, g, b])))
|
||||
}
|
||||
|
||||
/// Puts the sprites drawing missed in the atlas; forgets the items no one wears.
|
||||
fn put_sprites(&mut self, store: &mut dyn SpriteStore) {
|
||||
for id in self.wanted.drain(..) {
|
||||
let item = self.sprites.values().find(|(i, _)| *i == id);
|
||||
if let Some((_, item)) = item.filter(|_| !store.has(id)) {
|
||||
store.put(id, slot_image(item));
|
||||
let kept = self.items.values().find(|k| k.sprite == id);
|
||||
if let Some(kept) = kept.filter(|_| !store.has(id)) {
|
||||
store.put(id, slot_image(&kept.item));
|
||||
}
|
||||
}
|
||||
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`).
|
||||
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();
|
||||
// An emote's props: its time and weight while it plays.
|
||||
let emote = emotes::pose_playing(draw.body_pose()).and_then(|p| {
|
||||
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,
|
||||
};
|
||||
// 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).
|
||||
if emoting.is_none() && covered(m.gear, item.slot) {
|
||||
continue;
|
||||
}
|
||||
let Some(uv) = self.uv(item, |id| draw.sprite(id)) else {
|
||||
continue;
|
||||
};
|
||||
for i in 0..self.kept(item).meshes.len() {
|
||||
let (bone_of, mesh) = self.kept(item).meshes[i].clone();
|
||||
let known = bones.iter().flatten().find(|(b, _)| *b == bone_of);
|
||||
let matrix = match known {
|
||||
Some(&(_, matrix)) => matrix,
|
||||
None => {
|
||||
let Some(matrix) = draw.bone(bone_of) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(free) = bones.iter_mut().find(|b| b.is_none()) {
|
||||
*free = Some((bone_of, matrix));
|
||||
}
|
||||
matrix
|
||||
}
|
||||
};
|
||||
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);
|
||||
// Props grow in and shrink out with the emote's blend.
|
||||
let root = p.pose(&mesh.name);
|
||||
for s in &mut root[6..9] {
|
||||
*s *= w;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
self.sprites
|
||||
.retain(|_, (_, item)| Arc::strong_count(item) > 1);
|
||||
}
|
||||
|
||||
/// Once a tick: names the Horizon players in view to the emote feed and plays what it says
|
||||
@@ -922,104 +1043,47 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// The companion, trail and aura `m` wears, in the world (`camera`: where the view's camera
|
||||
/// is); the items on its bones are the hook's (`Seat::player_layers`). `gpu`: a companion
|
||||
/// goes on the GPU's skin path (the mob pass's skin frame, begun) and the bounds of what went
|
||||
/// there come back, for culling; else its vertices go to `out`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn cosmetics_geometry(
|
||||
pub(super) fn cosmetics_fx_of(
|
||||
&self,
|
||||
m: &Mob,
|
||||
own: bool,
|
||||
base: glam::Mat4,
|
||||
camera: Option<glam::DVec3>,
|
||||
camera: 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) {
|
||||
// "Name tags only": other players' styles beside their names, nothing around them.
|
||||
let bodies = own || self.settings.extras.effective_others() == Others::Shown;
|
||||
// The player's own in first person: only its companion, drawn after the drain of the
|
||||
// first-person model (`cosmetics_fx_geometry`).
|
||||
if !bodies || own && !self.own_body_in_view() {
|
||||
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;
|
||||
body_poses.reset(body);
|
||||
let data = self.res.mob_data();
|
||||
anim::setup(m, &anim::state(m, alpha), body_poses, &data.animations);
|
||||
let worn = self.worn(m, own)?;
|
||||
if horizon_client::mobs::trader::is_invisible(m) {
|
||||
return None;
|
||||
}
|
||||
let mut poses = self.cosmetics.poses.borrow_mut();
|
||||
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 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
|
||||
}
|
||||
for item in worn.iter() {
|
||||
if let Slot::Companion | Slot::Trail | Slot::Aura = item.slot {
|
||||
let at = fx::Wearer {
|
||||
m,
|
||||
own,
|
||||
key: if own { OWN } else { m.id },
|
||||
base,
|
||||
camera,
|
||||
alpha,
|
||||
light,
|
||||
};
|
||||
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 = self.cosmetics_fx(&at, item, gpu, bounds, &mut poses, out);
|
||||
}
|
||||
}
|
||||
bounds
|
||||
@@ -1398,4 +1462,124 @@ mod tests {
|
||||
service.tick(&mut view);
|
||||
assert!(view.0[3].1.is_none());
|
||||
}
|
||||
|
||||
/// 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).
|
||||
struct Canvas {
|
||||
pose: Option<Arc<dyn BodyPose>>,
|
||||
sprite: Option<[f32; 4]>,
|
||||
drawn: Vec<(String, [[f32; 4]; 4], [f32; 4], [f32; 3])>,
|
||||
}
|
||||
|
||||
const LEFT_ARM: [[f32; 4]; 4] = [[2.0, 0.0, 0.0, 0.0]; 4];
|
||||
|
||||
impl PlayerDraw for Canvas {
|
||||
fn pass(&self) -> horizon_hooks::players::Pass {
|
||||
horizon_hooks::players::Pass::World
|
||||
}
|
||||
fn ticks(&self) -> f32 {
|
||||
102.0
|
||||
}
|
||||
fn base(&self) -> [[f32; 4]; 4] {
|
||||
LEFT_ARM
|
||||
}
|
||||
fn motion(&self) -> horizon_hooks::players::Motion {
|
||||
Default::default()
|
||||
}
|
||||
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)
|
||||
}
|
||||
fn body_pose(&self) -> Option<&Arc<dyn BodyPose>> {
|
||||
self.pose.as_ref()
|
||||
}
|
||||
fn sprite(&self, _: u64) -> Option<[f32; 4]> {
|
||||
self.sprite
|
||||
}
|
||||
fn draw(
|
||||
&mut self,
|
||||
mesh: &Arc<Mesh>,
|
||||
matrix: [[f32; 4]; 4],
|
||||
look: Look,
|
||||
pose: &mut dyn FnMut(&mut dyn horizon_hooks::players::PartPoses),
|
||||
) {
|
||||
struct Rest(std::collections::HashMap<String, [f32; 9]>);
|
||||
impl horizon_hooks::players::PartPoses for Rest {
|
||||
fn pose(&mut self, name: &str) -> &mut [f32; 9] {
|
||||
self.0.entry(name.to_owned()).or_insert([0.0; 9])
|
||||
}
|
||||
}
|
||||
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,
|
||||
look.uv,
|
||||
[root[6], root[7], root[8]],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_emotes_prop_goes_on_its_bone_once_its_texture_is_in_scaled_by_the_blend() {
|
||||
let pop = CROWN
|
||||
.replace(
|
||||
r#""slot": "head", "bone": "head""#,
|
||||
r#""slot": "emote", "bone": "left_arm""#,
|
||||
)
|
||||
.replace(
|
||||
r#""uv": [0, 0]}]}]}"#,
|
||||
r#""uv": [0, 0]}]}],
|
||||
"emote": {"length": 40, "blend": 4, "tracks": [
|
||||
{"part": "right_arm", "keys": [{"t": 0}, {"t": 40}]}]}}"#,
|
||||
);
|
||||
let item = Arc::new(Item::parse(pop.as_bytes()).unwrap());
|
||||
let playing = horizon_cosmetics::Playing {
|
||||
item: item.clone(),
|
||||
start: 100.0,
|
||||
stop: None,
|
||||
};
|
||||
let (_, weight) = playing.at(102.0).unwrap();
|
||||
assert!(weight > 0.0 && weight < 1.0, "{weight}");
|
||||
let player = PlayerModel::new(9, Kind::Player, Some(5), 102, false);
|
||||
let mut view = View(vec![(player, None)]);
|
||||
emotes::set_model_playing(&mut view, 9, Some(playing));
|
||||
let mut canvas = Canvas {
|
||||
pose: view.0[0].1.clone(),
|
||||
sprite: None,
|
||||
drawn: Vec::new(),
|
||||
};
|
||||
// Others' cosmetics hidden, no service: the player's own emote still shows.
|
||||
let mut seat = Seat {
|
||||
said: Said {
|
||||
others_hidden: true,
|
||||
..Said::default()
|
||||
},
|
||||
..Seat::default()
|
||||
};
|
||||
// Its texture not in the atlas yet: nothing drawn, the sprite asked for.
|
||||
seat.player_layers(&player, &mut canvas);
|
||||
assert!(canvas.drawn.is_empty());
|
||||
assert_eq!(seat.wanted.len(), 1);
|
||||
canvas.sprite = Some([0.0, 0.0, 0.5, 0.5]);
|
||||
seat.player_layers(&player, &mut canvas);
|
||||
// A 16×16 texture: the slot's top left quarter of it.
|
||||
let quarter = 0.5 * 16.0 / 64.0;
|
||||
assert_eq!(canvas.drawn.len(), 1);
|
||||
let (_, matrix, uv, scale) = &canvas.drawn[0];
|
||||
assert_eq!((matrix, uv), (&LEFT_ARM, &[0.0, 0.0, quarter, quarter]));
|
||||
for s in scale {
|
||||
assert!((s - weight).abs() < 1e-6, "{scale:?} by {weight}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -314,8 +314,7 @@ impl Game<'_> {
|
||||
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);
|
||||
self.pose_companion(item, m, alpha, &mut poses, &model);
|
||||
self.cosmetics_model(&model, matrix, &poses, uv, light, false, None, out);
|
||||
}
|
||||
}
|
||||
@@ -280,25 +280,18 @@ impl Game<'_> {
|
||||
self.spin_attack_geometry(mob, camera, alpha as f32, packed, out);
|
||||
self.stuck_in_body_geometry(mob, mob.id as i32, camera, alpha as f32, packed, out);
|
||||
}
|
||||
// Cosmetics (game/cosmetics.rs), posed on the GPU with the mob's own jobs: culled
|
||||
// with it.
|
||||
// What crates draw on players and mannequins (`game/player_hooks.rs`: cosmetics),
|
||||
// posed on the GPU with the mob's own jobs: culled with it.
|
||||
if !layers_only
|
||||
&& super::super::cosmetics::enabled()
|
||||
&& horizon_hooks::engine::any()
|
||||
&& (is_player || &*mob.kind.id == "minecraft:mannequin")
|
||||
{
|
||||
let mark = self.skin_frame.borrow().mark();
|
||||
let base = horizon_client::mobs::render::base_transform(mob, alpha as f32, camera);
|
||||
let gpu = skinning;
|
||||
if let Some((lo, hi)) = self.cosmetics_geometry(
|
||||
mob,
|
||||
false,
|
||||
base,
|
||||
Some(camera),
|
||||
alpha as f32,
|
||||
packed,
|
||||
gpu,
|
||||
out,
|
||||
) {
|
||||
if let Some((lo, hi)) =
|
||||
self.player_layers_geometry(mob, base, camera, alpha as f32, packed, gpu, out)
|
||||
{
|
||||
skinned = Some(match skinned {
|
||||
Some((first, Some((l, h)))) => (first, Some((l.min(lo), h.max(hi)))),
|
||||
Some((first, None)) => (first, Some((lo, hi))),
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
//! registered nothing is built.
|
||||
|
||||
use super::Game;
|
||||
use horizon_client::fast_hash::FxHashMap;
|
||||
use horizon_client::join_seq::JoinList;
|
||||
use horizon_client::mobs::Mob;
|
||||
use horizon_client::mobs::anim::BodyPose;
|
||||
use horizon_client::mesher::Vertex;
|
||||
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::{
|
||||
CameraView, Frame, Gear, Kind, 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;
|
||||
|
||||
@@ -146,7 +149,386 @@ impl SpriteStore for HookStore<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// The crates' meshes as the renderer takes them, by the mesh's address (each entry holds its
|
||||
/// mesh, so the address isn't reused); render thread only.
|
||||
static MESHES: std::cell::RefCell<FxHashMap<usize, (Arc<Mesh>, Arc<Part>)>> =
|
||||
Default::default();
|
||||
}
|
||||
|
||||
/// The game's model of `mesh`, made once.
|
||||
fn part_of(mesh: &Arc<Mesh>) -> Arc<Part> {
|
||||
fn part(m: &Mesh) -> Part {
|
||||
Part {
|
||||
name: m.name.clone(),
|
||||
pose: m.pose,
|
||||
polygons: m
|
||||
.quads
|
||||
.iter()
|
||||
.map(|q| Polygon {
|
||||
normal: q.normal,
|
||||
vertices: q.vertices,
|
||||
cull_back: false,
|
||||
})
|
||||
.collect(),
|
||||
children: m.children.iter().map(part).collect(),
|
||||
}
|
||||
}
|
||||
MESHES.with_borrow_mut(|meshes| {
|
||||
let (_, part_of) = meshes
|
||||
.entry(Arc::as_ptr(mesh) as usize)
|
||||
.or_insert_with(|| (mesh.clone(), Arc::new(part(mesh))));
|
||||
part_of.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// 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| {
|
||||
m.borrow_mut()
|
||||
.retain(|_, (mesh, _)| Arc::strong_count(mesh) > 1)
|
||||
});
|
||||
}
|
||||
|
||||
/// `Poses` as a crate poses a mesh's parts.
|
||||
struct PosesOf<'a>(&'a mut Poses);
|
||||
|
||||
impl PartPoses for PosesOf<'_> {
|
||||
fn pose(&mut self, name: &str) -> &mut [f32; 9] {
|
||||
self.0.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
game: &'a Game<'g>,
|
||||
m: &'a Mob,
|
||||
pass: Pass,
|
||||
/// The model's matrix (`render::base_transform`).
|
||||
base: glam::Mat4,
|
||||
camera: glam::DVec3,
|
||||
alpha: f32,
|
||||
light: u32,
|
||||
gpu: bool,
|
||||
out: &'a mut Vec<Vertex>,
|
||||
bounds: Option<(glam::Vec3, glam::Vec3)>,
|
||||
/// The body as it is drawn, posed on the first `bone`.
|
||||
body: Option<PooledPoses>,
|
||||
poses: PooledPoses,
|
||||
}
|
||||
|
||||
impl DrawPlayer<'_, '_> {
|
||||
fn body(&self) -> &Part {
|
||||
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<'_, '_> {
|
||||
fn pass(&self) -> Pass {
|
||||
self.pass
|
||||
}
|
||||
|
||||
fn ticks(&self) -> f32 {
|
||||
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());
|
||||
let data = self.game.res.mob_data();
|
||||
let state = anim::state(self.m, self.alpha);
|
||||
anim::setup(self.m, &state, &mut poses, &data.animations);
|
||||
self.body = Some(poses);
|
||||
}
|
||||
let poses = self.body.as_ref()?;
|
||||
let at = render::part_matrix(self.body(), self.base, poses, bone.part(), 0.0)?;
|
||||
Some(at.to_cols_array_2d())
|
||||
}
|
||||
|
||||
fn body_pose(&self) -> Option<&Arc<dyn BodyPose>> {
|
||||
self.m.emote.as_ref()
|
||||
}
|
||||
|
||||
fn sprite(&self, id: u64) -> Option<[f32; 4]> {
|
||||
HookSprites(self.game).get(id)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&mut self,
|
||||
mesh: &Arc<Mesh>,
|
||||
matrix: [[f32; 4]; 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();
|
||||
let mut skin = horizon_client::mobs::skin::Skin::new(&mut models, &mut frame);
|
||||
skin.bounds = self.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) {
|
||||
self.bounds = skin.bounds;
|
||||
return;
|
||||
}
|
||||
}
|
||||
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.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn player_layers_geometry(
|
||||
&self,
|
||||
m: &Mob,
|
||||
base: glam::Mat4,
|
||||
camera: glam::DVec3,
|
||||
alpha: f32,
|
||||
light: u32,
|
||||
gpu: bool,
|
||||
out: &mut Vec<Vertex>,
|
||||
) -> Option<(glam::Vec3, glam::Vec3)> {
|
||||
debug_assert!(
|
||||
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 mut draw = DrawPlayer {
|
||||
game: self,
|
||||
m,
|
||||
pass: Pass::World,
|
||||
base,
|
||||
camera,
|
||||
alpha,
|
||||
light,
|
||||
gpu,
|
||||
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_fx_of(m, false, base, 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// What crates draw on the window's own model `m` (its vertices to `out`): in the world in
|
||||
/// third person (`Pass::World`, then the cosmetics' companion, trail and aura still drawn
|
||||
/// here), 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"
|
||||
);
|
||||
let fx = pass == Pass::World && super::cosmetics::enabled();
|
||||
if !engine::any() && !fx {
|
||||
return;
|
||||
}
|
||||
let base = render::base_transform(m, alpha, camera);
|
||||
if engine::any() {
|
||||
let model = model(m, Kind::Own, self.remote.as_ref().map(|r| r.profile()));
|
||||
let mut draw = DrawPlayer {
|
||||
game: self,
|
||||
m,
|
||||
pass,
|
||||
base,
|
||||
camera,
|
||||
alpha,
|
||||
light,
|
||||
gpu: false,
|
||||
out: &mut *out,
|
||||
bounds: None,
|
||||
body: None,
|
||||
poses: Poses::pooled(),
|
||||
};
|
||||
engine::player_layers(&model, &mut draw);
|
||||
}
|
||||
if fx {
|
||||
self.cosmetics_fx_of(m, true, base, camera, alpha, light, false, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// After every player (`game.rs`): what crates draw around the window's own model in first
|
||||
/// person, the cosmetics' fx still drawn here, 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::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::first_person(&own, &mut draw);
|
||||
}
|
||||
self.cosmetics_fx_geometry(camera, alpha, out);
|
||||
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.
|
||||
pub(super) fn client_hooks_frame(&mut self) {
|
||||
debug_assert!(
|
||||
@@ -160,6 +542,8 @@ impl Game<'_> {
|
||||
let connection = self.remote.as_ref().map_or(0, |r| r.session.serial());
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,13 @@ pub trait Hooks: Sync {
|
||||
None
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -204,6 +211,20 @@ pub mod engine {
|
||||
all().iter().find_map(|h| h.player_mark(uuid))
|
||||
}
|
||||
|
||||
/// [`Hooks::player_layers`].
|
||||
pub fn player_layers(model: &players::PlayerModel, draw: &mut dyn players::PlayerDraw) {
|
||||
for hooks in all() {
|
||||
hooks.player_layers(model, draw);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`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))
|
||||
@@ -266,6 +287,21 @@ mod tests {
|
||||
fn camera(&self, own: &players::CameraView) -> Option<players::CameraOverride> {
|
||||
Some(players::CameraOverride::new(own.age / 10.0))
|
||||
}
|
||||
/// A hat on the head, its brim turned by the model's age.
|
||||
fn player_layers(&self, _: &players::PlayerModel, draw: &mut dyn players::PlayerDraw) {
|
||||
let (Some(head), Some(uv)) = (draw.bone(players::Bone::Head), draw.sprite(1)) else {
|
||||
return;
|
||||
};
|
||||
let turn = draw.ticks() / 100.0;
|
||||
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();
|
||||
models.for_each(&mut |m| ids.push(m.id));
|
||||
@@ -330,6 +366,68 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
static HAT: std::sync::LazyLock<std::sync::Arc<players::Mesh>> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
let brim = players::Mesh::new("brim".into(), [0.0; 9], Vec::new(), Vec::new());
|
||||
std::sync::Arc::new(players::Mesh::new(
|
||||
"hat".into(),
|
||||
[0.0; 9],
|
||||
Vec::new(),
|
||||
vec![brim],
|
||||
))
|
||||
});
|
||||
|
||||
/// 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)>, Vec<f32>);
|
||||
impl players::PlayerDraw for Canvas {
|
||||
fn pass(&self) -> players::Pass {
|
||||
players::Pass::World
|
||||
}
|
||||
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])
|
||||
}
|
||||
fn body_pose(&self) -> Option<&std::sync::Arc<dyn players::BodyPose>> {
|
||||
None
|
||||
}
|
||||
fn sprite(&self, id: u64) -> Option<[f32; 4]> {
|
||||
(id == 1).then_some([0.0, 0.0, 0.5, 0.5])
|
||||
}
|
||||
fn draw(
|
||||
&mut self,
|
||||
mesh: &std::sync::Arc<players::Mesh>,
|
||||
matrix: [[f32; 4]; 4],
|
||||
look: players::Look,
|
||||
pose: &mut dyn FnMut(&mut dyn players::PartPoses),
|
||||
) {
|
||||
struct Rest([f32; 9]);
|
||||
impl players::PartPoses for Rest {
|
||||
fn pose(&mut self, _: &str) -> &mut [f32; 9] {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
let mut brim = Rest(mesh.children[0].pose);
|
||||
pose(&mut brim);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements nothing: the defaults do nothing.
|
||||
struct Silent;
|
||||
impl Hooks for Silent {}
|
||||
@@ -364,6 +462,19 @@ mod tests {
|
||||
assert_eq!(engine::player_mark(8), None);
|
||||
let camera = engine::camera(&players::CameraView::new(5.0));
|
||||
assert_eq!(camera.map(|c| c.distance), Some(0.5));
|
||||
let mut canvas = Canvas::default();
|
||||
engine::player_layers(&model(players::Kind::Player), &mut canvas);
|
||||
assert_eq!(
|
||||
canvas.0,
|
||||
[(
|
||||
"hat".to_owned(),
|
||||
[[1.0, 0.0, 0.0, 0.0]; 4],
|
||||
[0.0, 0.0, 0.5, 0.5],
|
||||
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());
|
||||
|
||||
@@ -210,3 +210,247 @@ impl CameraOverride {
|
||||
Self { distance }
|
||||
}
|
||||
}
|
||||
|
||||
/// A model a crate draws on players, as the game's own models are: a tree of parts, 16 units a
|
||||
/// block, y down. Build it once and keep the `Arc`: the engine prepares it for the GPU once, by
|
||||
/// the `Arc`'s address, while the crate holds it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct Mesh {
|
||||
/// What [`PartPoses::pose`] names it by.
|
||||
pub name: String,
|
||||
/// At rest: x, y, z, x/y/z rotation (radians), x/y/z scale.
|
||||
pub pose: [f32; 9],
|
||||
pub quads: Vec<Quad>,
|
||||
pub children: Vec<Mesh>,
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
pub fn new(name: String, pose: [f32; 9], quads: Vec<Quad>, children: Vec<Mesh>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
pose,
|
||||
quads,
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One face of a [`Mesh`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct Quad {
|
||||
pub normal: [f32; 3],
|
||||
/// Its corners: x, y, z, then u, v across the sprite (0 to 1).
|
||||
pub vertices: [[f32; 5]; 4],
|
||||
}
|
||||
|
||||
impl Quad {
|
||||
pub fn new(normal: [f32; 3], vertices: [[f32; 5]; 4]) -> Self {
|
||||
Self { normal, vertices }
|
||||
}
|
||||
}
|
||||
|
||||
/// A part of the player's body a crate draws on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Bone {
|
||||
Head,
|
||||
Body,
|
||||
RightArm,
|
||||
LeftArm,
|
||||
RightLeg,
|
||||
LeftLeg,
|
||||
}
|
||||
|
||||
impl Bone {
|
||||
/// Its name among [`BODY_PARTS`].
|
||||
pub fn part(self) -> &'static str {
|
||||
BODY_PARTS[self as usize]
|
||||
}
|
||||
}
|
||||
|
||||
/// Which picture of a player is drawn.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Pass {
|
||||
/// 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.
|
||||
pub trait PartPoses {
|
||||
/// Part `name`'s pose, to change (see [`Mesh::pose`]); a name the mesh lacks gets a pose that
|
||||
/// is not used.
|
||||
fn pose(&mut self, name: &str) -> &mut [f32; 9];
|
||||
}
|
||||
|
||||
/// Where a crate draws on a player model ([`crate::Hooks::player_layers`]). Matrices are
|
||||
/// column-major, relative to the camera, in blocks.
|
||||
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;
|
||||
/// 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` as `look` says; `pose` poses its parts first.
|
||||
fn draw(
|
||||
&mut self,
|
||||
mesh: &Arc<Mesh>,
|
||||
matrix: [[f32; 4]; 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);
|
||||
}
|
||||
@@ -48,10 +48,11 @@ pub use horizon_server_files as server_files;
|
||||
pub mod hooks {
|
||||
/// The players the game shows (`horizon_hooks::players`): the views [`Hooks::frame`] and
|
||||
/// [`Hooks::tick`] get, the pose a crate lays over a player's body, what goes by their names,
|
||||
/// the crate's sprites and the camera.
|
||||
/// the crate's sprites, the camera, and what a crate draws on them.
|
||||
pub use horizon_hooks::players::{
|
||||
BODY_PARTS, Badge, BodyPose, CameraOverride, CameraView, Frame, Gear, Kind, Mark,
|
||||
NameDecoration, PlayerModel, PlayerModels, 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