Skip to main content

yog_body/
model.rs

1//! A `.ymodel` made ready to draw: its bones in order, each cube baked into
2//! the quads vanilla's `ModelPart.Cube` makes (same corners, same box-UV,
3//! same mirroring) — so a model drawn here looks as one drawn by the game.
4
5use crate::format::{CubeDef, ModelFile};
6use crate::math::{Mat4, Vec3, rad};
7
8/// One four-cornered face: corners, their UVs (0–1), its normal.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Quad {
11    pub pos: [Vec3; 4],
12    pub uv: [[f32; 2]; 4],
13    pub normal: Vec3,
14}
15
16impl Quad {
17    /// This quad moved by `m`.
18    pub fn transformed(&self, m: &Mat4) -> Quad {
19        Quad {
20            pos: self.pos.map(|p| m.transform_point(p)),
21            uv: self.uv,
22            normal: m.transform_normal(self.normal),
23        }
24    }
25
26    /// The middle of its corners.
27    pub fn center(&self) -> Vec3 {
28        self.pos.iter().fold(Vec3::ZERO, |sum, &p| sum + p) * (1.0 / self.pos.len() as f32)
29    }
30}
31
32/// A bone, ready.
33#[derive(Debug, Clone)]
34pub struct Bone {
35    pub name: String,
36    /// Index of its parent in [`Model::bones`] (always before it).
37    pub parent: Option<usize>,
38    /// Pixels.
39    pub pivot: Vec3,
40    /// Radians.
41    pub rotation: Vec3,
42    /// Its cubes' faces, in its own space (pixels).
43    pub quads: Vec<Quad>,
44    pub locators: Vec<(String, Vec3)>,
45}
46
47/// A model, ready.
48#[derive(Debug, Clone)]
49pub struct Model {
50    /// The texture as the host loads it (`ns:textures/….png`), or the
51    /// video shown instead (`video:<id>`, which the runtime swaps for its frame).
52    pub texture: String,
53    pub bones: Vec<Bone>,
54}
55
56impl Model {
57    /// Builds `file`. Errors name what's wrong (an unknown or later
58    /// parent, a repeated name).
59    pub fn new(file: &ModelFile) -> Result<Model, String> {
60        let texture = match &file.video {
61            Some(v) => format!(
62                "{}{}{v}",
63                yog_ids::image_scheme::VIDEO,
64                yog_ids::sep::NAMESPACE
65            ),
66            None => texture_resource(&file.texture),
67        };
68        let [tw, th] = file.texture_size.map(|n| n.max(1) as f32);
69        let mut bones: Vec<Bone> = Vec::with_capacity(file.bones.len());
70        for b in &file.bones {
71            if bones.iter().any(|o| o.name == b.name) {
72                return Err(format!("bone `{}` comes twice", b.name));
73            }
74            let parent = match &b.parent {
75                None => None,
76                Some(p) => Some(bones.iter().position(|o| &o.name == p).ok_or_else(|| {
77                    format!("bone `{}`: its parent `{p}` must come before it", b.name)
78                })?),
79            };
80            bones.push(Bone {
81                name: b.name.clone(),
82                parent,
83                pivot: Vec3::from_array(b.pivot),
84                rotation: rad(Vec3::from_array(b.rotation)),
85                quads: b.cubes.iter().flat_map(|c| cube(c, tw, th)).collect(),
86                locators: b
87                    .locators
88                    .0
89                    .iter()
90                    .map(|(n, p)| (n.clone(), Vec3::from_array(*p)))
91                    .collect(),
92            });
93        }
94        Ok(Model { texture, bones })
95    }
96
97    pub fn bone(&self, name: &str) -> Option<usize> {
98        self.bones.iter().position(|b| b.name == name)
99    }
100}
101
102/// The texture as the host loads it: `<ns>:textures/<path>.png`.
103pub fn texture_resource(id: &str) -> String {
104    let (ns, path) = yog_ids::split(id);
105    yog_ids::join(ns, &yog_ids::texture_file(path))
106}
107
108/// `ModelPart.Cube`'s six faces.
109pub fn cube(c: &CubeDef, tex_w: f32, tex_h: f32) -> [Quad; 6] {
110    let [x, y, z] = c.origin;
111    let [w, h, d] = c.size;
112    let [u, v] = c.uv;
113    let g = c.inflate;
114    let (mut x0, y0, z0) = (x - g, y - g, z - g);
115    let (mut x1, y1, z1) = (x + w + g, y + h + g, z + d + g);
116    if c.mirror {
117        std::mem::swap(&mut x0, &mut x1);
118    }
119    let p = |x, y, z| Vec3::new(x, y, z);
120    let (v7, v0, v1, v2) = (p(x0, y0, z0), p(x1, y0, z0), p(x1, y1, z0), p(x0, y1, z0));
121    let (v3, v4, v5, v6) = (p(x0, y0, z1), p(x1, y0, z1), p(x1, y1, z1), p(x0, y1, z1));
122    let (u0, u1, u2, u3, u4, u5) = (
123        u,
124        u + d,
125        u + d + w,
126        u + d + w + w,
127        u + d + w + d,
128        u + d + w + d + w,
129    );
130    let (t0, t1, t2) = (v, v + d, v + d + h);
131    let face = |corners: [Vec3; 4], ua: f32, va: f32, ub: f32, vb: f32, normal: Vec3| {
132        let mut pos = corners;
133        let mut uv = [
134            [ub / tex_w, va / tex_h],
135            [ua / tex_w, va / tex_h],
136            [ua / tex_w, vb / tex_h],
137            [ub / tex_w, vb / tex_h],
138        ];
139        let mut n = normal;
140        if c.mirror {
141            pos.reverse();
142            uv.reverse();
143            n.x = -n.x;
144        }
145        Quad { pos, uv, normal: n }
146    };
147    [
148        face([v4, v3, v7, v0], u1, t0, u2, t1, Vec3::new(0.0, -1.0, 0.0)),
149        face([v1, v2, v6, v5], u2, t1, u3, t0, Vec3::new(0.0, 1.0, 0.0)),
150        face([v7, v3, v6, v2], u0, t1, u1, t2, Vec3::new(-1.0, 0.0, 0.0)),
151        face([v0, v7, v2, v1], u1, t1, u2, t2, Vec3::new(0.0, 0.0, -1.0)),
152        face([v4, v0, v1, v5], u2, t1, u4, t2, Vec3::new(1.0, 0.0, 0.0)),
153        face([v3, v4, v5, v6], u4, t1, u5, t2, Vec3::new(0.0, 0.0, 1.0)),
154    ]
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn wing() -> CubeDef {
162        CubeDef {
163            origin: [-10.0, 0.0, 0.0],
164            size: [10.0, 20.0, 2.0],
165            uv: [22.0, 0.0],
166            inflate: 1.0,
167            mirror: false,
168        }
169    }
170
171    #[test]
172    fn a_cube_is_vanillas() {
173        let q = cube(&wing(), 64.0, 32.0);
174        // DOWN: v4, v3, v7, v0 with UVs from (d, 0) to (d + w, d).
175        assert_eq!(q[0].pos[0], Vec3::new(1.0, -1.0, 3.0));
176        assert_eq!(q[0].uv[0], [34.0 / 64.0, 0.0]);
177        assert_eq!(q[0].uv[1], [24.0 / 64.0, 0.0]);
178        assert_eq!(q[3].normal, Vec3::new(0.0, 0.0, -1.0));
179    }
180
181    #[test]
182    fn a_mirrored_cube_turns_its_faces() {
183        let mut c = wing();
184        c.mirror = true;
185        let q = cube(&c, 64.0, 32.0);
186        assert_eq!(q[2].normal, Vec3::new(1.0, 0.0, 0.0));
187        assert_eq!(q[0].pos[3], Vec3::new(-11.0, -1.0, 3.0));
188    }
189
190    #[test]
191    fn a_video_stands_for_the_texture() {
192        let file = yog_data::from_text::<ModelFile>(
193            r#"{ texture: "@texture:m:screen", video: "@video:m:intro", bones: [] }"#,
194        )
195        .unwrap()
196        .value;
197        assert_eq!(Model::new(&file).unwrap().texture, "video:m:intro");
198    }
199
200    #[test]
201    fn a_texture_is_loaded_from_its_resource() {
202        assert_eq!(
203            texture_resource("minecraft:entity/elytra"),
204            "minecraft:textures/entity/elytra.png"
205        );
206    }
207}