Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / client / src / render / scene / models.rs

use common::entity::mob::MobKind;
use common::math::{cos, sin};
use glam::{Mat3, Mat4, Vec3};

use super::{Geometry, lerp};
use crate::entities::RemoteEntity;

const PI: f32 = std::f32::consts::PI;
const FLEECE: [[f32; 3]; 16] = [
    [1.0, 1.0, 1.0],
    [0.95, 0.7, 0.2],
    [0.9, 0.5, 0.85],
    [0.6, 0.7, 0.95],
    [0.9, 0.9, 0.2],
    [0.5, 0.8, 0.1],
    [0.95, 0.7, 0.8],
    [0.3, 0.3, 0.3],
    [0.6, 0.6, 0.6],
    [0.3, 0.6, 0.7],
    [0.7, 0.4, 0.9],
    [0.2, 0.4, 0.8],
    [0.5, 0.4, 0.3],
    [0.4, 0.5, 0.2],
    [0.8, 0.3, 0.3],
    [0.1, 0.1, 0.1],
];

pub(super) struct Part {
    min: Vec3,
    size: Vec3,
    pivot: Vec3,
    uv: [f32; 2],
    rotation: Vec3,
    dilation: f32,
    mirrored: bool,
    yxz: bool,
}

impl Part {
    pub(super) fn new(min: [f32; 3], size: [f32; 3], pivot: [f32; 3], uv: [f32; 2]) -> Self {
        Self {
            min: min.into(),
            size: size.into(),
            pivot: pivot.into(),
            uv,
            rotation: Vec3::ZERO,
            dilation: 0.0,
            mirrored: false,
            yxz: false,
        }
    }

    fn rotation(&self) -> Mat3 {
        let [x, y, z] = self.rotation.to_array();
        if self.yxz {
            Mat3::from_rotation_y(y) * Mat3::from_rotation_x(x) * Mat3::from_rotation_z(z)
        } else {
            Mat3::from_rotation_z(z) * Mat3::from_rotation_y(y) * Mat3::from_rotation_x(x)
        }
    }

    /// `ModelRenderer.postRender`: from the part's space into its model's.
    pub(super) fn transform(&self) -> Mat4 {
        Mat4::from_translation(self.pivot) * Mat4::from_mat3(self.rotation())
    }

    pub(super) fn rotate(mut self, angles: [f32; 3]) -> Self {
        self.rotation = angles.into();
        self
    }
    pub(super) fn rotate_yxz(mut self, angles: [f32; 3]) -> Self {
        self.rotation = angles.into();
        self.yxz = true;
        self
    }

    pub(super) fn inflate(mut self, dilation: f32) -> Self {
        self.dilation = dilation;
        self
    }
    pub(super) fn mirror(mut self, mirrored: bool) -> Self {
        self.mirrored = mirrored;
        self
    }
}

pub(super) struct Model<'a> {
    pub geometry: &'a mut Geometry,
    pub position: Vec3,
    pub transform: Mat3,
    pub texture: i32,
    pub color: Vec3,
    pub alpha: f32,
    /// Untextured colors drawn over each face: the red of damage, the white of a creeper's
    /// flash (`RenderLiving.getColorMultiplier`); transparent ones are skipped.
    pub overlays: [u32; 2],
}

/// `RenderHelper.enableStandardItemLighting`: ambient plus two diffuse lights.
pub(super) fn item_lighting(normal: Vec3) -> f32 {
    (0.4 + normal.dot(Vec3::new(0.2, 1.0, -0.7).normalize()).max(0.0) * 0.6
        + normal.dot(Vec3::new(-0.2, 1.0, 0.7).normalize()).max(0.0) * 0.6)
        .min(1.0)
}

/// The red tint of a hurt or dying model, as bright as the entity.
pub(super) fn hurt_overlay(hurt: bool, brightness: f32) -> [u32; 2] {
    [
        if hurt {
            0x66000000 | (brightness * 255.0) as u32
        } else {
            0
        },
        0,
    ]
}

impl<'a> Model<'a> {
    /// A plain model in `transform`'s space, as bright as given.
    pub(super) fn new(
        geometry: &'a mut Geometry,
        transform: Mat4,
        texture: i32,
        brightness: f32,
    ) -> Self {
        Self {
            geometry,
            position: transform.transform_point3(Vec3::ZERO),
            transform: Mat3::from_mat4(transform),
            texture,
            color: Vec3::splat(brightness),
            alpha: 1.0,
            overlays: [0; 2],
        }
    }

    pub(super) fn part(&mut self, part: Part) {
        let mut min = part.min - Vec3::splat(part.dilation);
        let mut max = part.min + part.size + Vec3::splat(part.dilation);
        if part.mirrored {
            std::mem::swap(&mut min.x, &mut max.x);
        }
        let rotation = part.rotation();
        let corners = super::box_corners(min, max)
            .map(|p| self.position + self.transform * (rotation * p + part.pivot));
        let [x, y, z] = part.size.to_array();
        let [u, v] = part.uv;
        for (indices, uv) in [
            ([5, 1, 2, 6], [u + z + x, v + z, z, y]),
            ([0, 4, 7, 3], [u, v + z, z, y]),
            ([5, 4, 0, 1], [u + z, v, x, z]),
            ([2, 3, 7, 6], [u + z + x, v, x, z]),
            ([1, 0, 3, 2], [u + z, v + z, x, y]),
            ([4, 5, 6, 7], [u + z + x + z, v + z, x, y]),
        ] {
            let mut points = indices.map(|i| corners[i]);
            if part.mirrored {
                points.reverse();
            }
            let normal = (points[1] - points[0])
                .cross(points[2] - points[0])
                .normalize_or_zero();
            let rgb = (self.color * item_lighting(normal) * 255.0)
                .clamp(Vec3::ZERO, Vec3::splat(255.0))
                .as_uvec3();
            let [u, v, w, h] = uv;
            let mut uv = [
                (u + w - 0.1) / 64.0,
                (v + 0.1) / 32.0,
                -(w - 0.2) / 64.0,
                (h - 0.2) / 32.0,
            ];
            if part.mirrored {
                uv[1] += uv[3];
                uv[3] = -uv[3];
            }
            self.geometry.quad(
                points,
                uv,
                self.texture,
                ((self.alpha * 255.0) as u32) << 24 | rgb.x | (rgb.y << 8) | (rgb.z << 16),
            );
            for overlay in self.overlays.into_iter().filter(|c| c >> 24 != 0) {
                self.geometry.quad(points, [0.0; 4], -1, overlay);
            }
        }
    }
}

pub(super) fn angle(previous: f32, current: f32, partial: f32) -> f32 {
    previous + ((current - previous + 180.0).rem_euclid(360.0) - 180.0) * partial
}

impl Geometry {
    pub(super) fn mob(
        &mut self,
        kind: MobKind,
        entity: &RemoteEntity,
        position: Vec3,
        partial: f32,
        brightness: f32,
    ) {
        let previous = entity.previous_animation;
        let current = entity.animation;
        let yaw = angle(previous.rotation[0], current.rotation[0], partial);
        let pitch = lerp(previous.rotation[1], current.rotation[1], partial);
        let body = angle(previous.rotation[2], current.rotation[2], partial);
        let head = [pitch.to_radians(), (yaw - body).to_radians(), 0.0];
        let limb = current.walk[0] - current.walk[1] * (1.0 - partial);
        let speed = lerp(previous.walk[1], current.walk[1], partial).min(1.0);
        let leg = cos(limb * 0.6662) * 1.4 * speed;
        let age = entity.age as f32 + partial;
        let death = if current.death > 0 {
            (((current.death as f32 + partial - 1.0) / 20.0 * 1.6).max(0.0))
                .sqrt()
                .min(1.0)
                * if kind == MobKind::Spider {
                    PI
                } else {
                    PI / 2.0
                }
        } else {
            0.0
        };
        let mut scale = Vec3::new(-1.0, -1.0, 1.0) / 16.0;
        let fuse = (previous.fuse as f32 + (current.fuse - previous.fuse) as f32 * partial) / 28.0;
        if kind == MobKind::Creeper {
            let wobble = 1.0 + sin(fuse * 100.0) * fuse * 0.01;
            let swell = fuse.clamp(0.0, 1.0).powi(4);
            scale *= Vec3::new(
                (1.0 + swell * 0.4) * wobble,
                (1.0 + swell * 0.1) / wobble,
                (1.0 + swell * 0.4) * wobble,
            );
        }
        if kind == MobKind::Ghast {
            let charge =
                lerp(previous.special.charge, current.special.charge, partial).max(0.0) / 20.0;
            let pulse = 1.0 / (charge.powi(5) * 2.0 + 1.0);
            scale *= Vec3::new(
                (8.0 + 1.0 / pulse) / 2.0,
                (8.0 + pulse) / 2.0,
                (8.0 + 1.0 / pulse) / 2.0,
            );
        }
        // `RenderGiantZombie.preRenderScale`
        if kind == MobKind::Giant {
            scale *= 6.0;
        }
        if kind == MobKind::Slime {
            let size = f32::from(entity.flags[1].max(1));
            let stretch = lerp(previous.special.stretch, current.special.stretch, partial);
            let squash = 1.0 / (stretch / (size * 0.5 + 1.0) + 1.0);
            scale *= Vec3::new(squash * size, size / squash, squash * size);
        }
        let rotation =
            Mat3::from_rotation_y((180.0 - body).to_radians()) * Mat3::from_rotation_z(death);
        let position = position + rotation * Vec3::new(0.0, 1.5078125 * (scale.y * -16.0), 0.0);
        let (position, rotation) = if kind == MobKind::Squid {
            let pitch = lerp(
                previous.special.squid_pitch,
                current.special.squid_pitch,
                partial,
            );
            let roll = lerp(
                previous.special.squid_roll,
                current.special.squid_roll,
                partial,
            );
            let rotation = Mat3::from_rotation_y((180.0 - body).to_radians())
                * Mat3::from_rotation_x(pitch.to_radians())
                * Mat3::from_rotation_y(roll.to_radians());
            (
                position - Vec3::Y * 1.5078125
                    + Vec3::Y * 0.5
                    + rotation * Vec3::Y * (1.5078125 - 1.2),
                rotation,
            )
        } else {
            (position, rotation)
        };
        let texture = match kind {
            MobKind::Pig => 11,
            MobKind::Cow => 12,
            MobKind::Sheep => 13,
            MobKind::Chicken => 14,
            MobKind::Zombie | MobKind::Giant => 15,
            MobKind::Monster => crate::assets::entity_texture("mob/char.png"),
            MobKind::Skeleton => 16,
            MobKind::Spider => 17,
            MobKind::Creeper => 18,
            MobKind::PigZombie => crate::assets::entity_texture("mob/pigzombie.png"),
            MobKind::Slime => crate::assets::entity_texture("mob/slime.png"),
            MobKind::Squid => crate::assets::entity_texture("mob/squid.png"),
            MobKind::Wolf => crate::assets::entity_texture(if entity.flags[1] & 4 != 0 {
                "mob/wolf_tame.png"
            } else if entity.flags[1] & 2 != 0 {
                "mob/wolf_angry.png"
            } else {
                "mob/wolf.png"
            }),
            MobKind::Ghast => crate::assets::entity_texture(if entity.flags[1] == 1 {
                "mob/ghast_fire.png"
            } else {
                "mob/ghast.png"
            }),
        };
        let mut model = Model {
            position,
            transform: rotation * Mat3::from_diagonal(scale),
            overlays: hurt_overlay(current.hurt > 0 || current.death > 0, brightness),
            ..Model::new(self, Mat4::IDENTITY, texture, brightness)
        };
        // `RenderCreeper.updateCreeperColorMultiplier`: flashing white faster as it swells
        if kind == MobKind::Creeper && (fuse * 10.0) as i32 % 2 != 0 {
            let alpha = ((fuse * 0.2 * 255.0) as i32).clamp(0, 255) as u32;
            model.overlays[1] = alpha << 24 | 0xffffff;
        }
        let mut held = None;
        let mut aura = None;
        match kind {
            MobKind::Pig | MobKind::Cow | MobKind::Sheep => {
                quadruped(&mut model, kind, head, leg, false);
                if kind == MobKind::Sheep && entity.flags[1] & 16 == 0 {
                    model.texture = 19;
                    model.color *= Vec3::from_array(FLEECE[usize::from(entity.flags[1] & 15)]);
                    quadruped(&mut model, kind, head, leg, true);
                } else if kind == MobKind::Pig && entity.flags[1] != 0 {
                    model.texture = 20;
                    quadruped(&mut model, kind, head, leg, true);
                }
            }
            MobKind::Wolf => wolf(&mut model, entity, head, limb, speed, partial),
            MobKind::Chicken => {
                let head = [-head[0], head[1], 0.0];
                for (min, size, uv) in [
                    ([-2.0, -6.0, -2.0], [4.0, 6.0, 3.0], [0.0, 0.0]),
                    ([-2.0, -4.0, -4.0], [4.0, 2.0, 2.0], [14.0, 0.0]),
                    ([-1.0, -2.0, -3.0], [2.0, 2.0, 2.0], [14.0, 4.0]),
                ] {
                    model.part(Part::new(min, size, [0.0, 15.0, -4.0], uv).rotate(head));
                }
                model.part(
                    Part::new(
                        [-3.0, -4.0, -3.0],
                        [6.0, 8.0, 6.0],
                        [0.0, 16.0, 0.0],
                        [0.0, 9.0],
                    )
                    .rotate([PI / 2.0, 0.0, 0.0]),
                );
                for (x, leg) in [(-2.0, leg), (1.0, -leg)] {
                    model.part(
                        Part::new(
                            [-1.0, 0.0, -3.0],
                            [3.0, 5.0, 3.0],
                            [x, 19.0, 1.0],
                            [26.0, 0.0],
                        )
                        .rotate([leg, 0.0, 0.0]),
                    );
                }
                let [previous, current] = [previous.special.flap, current.special.flap];
                let progress = lerp(previous.progress, current.progress, partial);
                let flap = (sin(progress) + 1.0) * lerp(previous.spread, current.spread, partial);
                for (x, min, roll) in [(-4.0, 0.0, flap), (4.0, -1.0, -flap)] {
                    model.part(
                        Part::new(
                            [min, 0.0, -3.0],
                            [1.0, 4.0, 6.0],
                            [x, 13.0, 0.0],
                            [24.0, 13.0],
                        )
                        .rotate([0.0, 0.0, roll]),
                    );
                }
            }
            MobKind::Zombie
            | MobKind::Skeleton
            | MobKind::PigZombie
            | MobKind::Giant
            | MobKind::Monster => {
                model
                    .part(Part::new([-4.0, -8.0, -4.0], [8.0; 3], [0.0; 3], [0.0; 2]).rotate(head));
                model.part(
                    Part::new([-4.0, -8.0, -4.0], [8.0; 3], [0.0; 3], [32.0, 0.0])
                        .rotate(head)
                        .inflate(0.5),
                );
                model.part(Part::new(
                    [-4.0, 0.0, -2.0],
                    [8.0, 12.0, 4.0],
                    [0.0; 3],
                    [16.0; 2],
                ));
                let thin = kind == MobKind::Skeleton;
                let width = if thin { 2.0 } else { 4.0 };
                let riding = entity.flags[0] & common::entity::flags::RIDING != 0;
                for (side, gait) in [(-1.0, leg), (1.0, -leg)] {
                    let legs = if riding {
                        [-PI * 0.4, -side * PI * 0.1, 0.0]
                    } else {
                        [gait, 0.0, 0.0]
                    };
                    model.part(
                        Part::new(
                            [-width / 2.0, 0.0, -width / 2.0],
                            [width, 12.0, width],
                            [side * 2.0, 12.0, 0.0],
                            [0.0, 16.0],
                        )
                        .rotate(legs)
                        .mirror(side > 0.0),
                    );
                    let arm_x = if thin || side > 0.0 { -1.0 } else { -3.0 };
                    let arm = Part::new(
                        [arm_x, -2.0, -width / 2.0],
                        [width, 12.0, width],
                        [side * 5.0, 2.0, 0.0],
                        [40.0, 16.0],
                    )
                    .rotate([
                        // `ModelBiped` swings its arms, `ModelZombie` and kin hold them out
                        if kind == MobKind::Monster {
                            cos(limb * 0.6662 + if side < 0.0 { PI } else { 0.0 }) * speed
                        } else {
                            -PI / 2.0
                        } - side * sin(age * 0.067) * 0.05,
                        if kind == MobKind::Monster {
                            0.0
                        } else {
                            side * 0.1
                        },
                        -side * (cos(age * 0.09) * 0.05 + 0.05),
                    ])
                    .mirror(side > 0.0);
                    if (thin || kind == MobKind::PigZombie) && side < 0.0 {
                        let model_space = Mat4::from_translation(model.position)
                            * Mat4::from_mat3(model.transform);
                        held = Some(model_space * arm.transform());
                    }
                    model.part(arm);
                }
            }
            MobKind::Ghast => {
                model.color = Vec3::ONE;
                model.part(Part::new([-8.0; 3], [16.0; 3], [0.0, 8.0, 0.0], [0.0; 2]));
                let mut random = common::JavaRandom::new(1660);
                for index in 0..9 {
                    let x = ((index % 3) as f32 - (index / 3 % 2) as f32 * 0.5 + 0.25 - 1.0) * 5.0;
                    let z = ((index / 3) as f32 - 1.0) * 5.0;
                    let length = (random.next_int(7) + 8) as f32;
                    model.part(
                        Part::new(
                            [-1.0, 0.0, -1.0],
                            [2.0, length, 2.0],
                            [x, 15.0, z],
                            [0.0; 2],
                        )
                        .rotate([
                            0.2 * sin(age * 0.3 + index as f32) + 0.4,
                            0.0,
                            0.0,
                        ]),
                    );
                }
            }
            MobKind::Slime => {
                model.part(Part::new(
                    [-3.0, 17.0, -3.0],
                    [6.0; 3],
                    [0.0; 3],
                    [0.0, 16.0],
                ));
                for (min, size, uv) in [
                    ([-3.25, 18.0, -3.5], [2.0; 3], [32.0, 0.0]),
                    ([1.25, 18.0, -3.5], [2.0; 3], [32.0, 4.0]),
                    ([0.0, 21.0, -3.5], [1.0; 3], [32.0, 8.0]),
                ] {
                    model.part(Part::new(min, size, [0.0; 3], uv));
                }
                model.part(Part::new([-4.0, 16.0, -4.0], [8.0; 3], [0.0; 3], [0.0; 2]));
            }
            MobKind::Squid => {
                model.part(Part::new(
                    [-6.0, -8.0, -6.0],
                    [12.0, 16.0, 12.0],
                    [0.0, 8.0, 0.0],
                    [0.0; 2],
                ));
                let tentacle = lerp(previous.special.tentacle, current.special.tentacle, partial);
                for index in 0..8 {
                    let angle = index as f64 * std::f64::consts::TAU / 8.0;
                    model.part(
                        Part::new(
                            [-1.0, 0.0, -1.0],
                            [2.0, 18.0, 2.0],
                            [angle.cos() as f32 * 5.0, 15.0, angle.sin() as f32 * 5.0],
                            [48.0, 0.0],
                        )
                        .rotate([
                            tentacle,
                            (-angle + std::f64::consts::FRAC_PI_2) as f32,
                            0.0,
                        ]),
                    );
                }
            }
            MobKind::Spider => {
                spider(&mut model, head, limb, speed);
                // `RenderSpider.setSpiderEyeBrightness`: the eyes show more in the dark, blended
                // without an alpha test
                model.texture = 23 | 1 << 17;
                model.color = Vec3::ONE;
                model.alpha = (1.0 - brightness) * 0.5;
                model.overlays = [0; 2];
                spider(&mut model, head, limb, speed);
            }
            MobKind::Creeper => {
                creeper(&mut model, head, leg, 0.0);
                // `RenderCreeper`: a charged creeper wears a crackling shell
                if entity.flags[2] != 0 {
                    aura = Some(model.geometry.vertices.len());
                    model.texture = crate::assets::entity_texture("armor/power.png");
                    model.color = Vec3::splat(0.5);
                    model.overlays = [0; 2];
                    creeper(&mut model, head, leg, 2.0);
                }
            }
        }
        // The shell scrolls its texture and is drawn additively with the flashes
        if let Some(start) = aura {
            let scroll = (entity.age as f32 + partial) * 0.01;
            self.auras
                .extend(self.vertices.drain(start..).map(|mut vertex| {
                    vertex.uv[0] += scroll;
                    vertex.uv[1] += scroll;
                    vertex
                }));
        }
        // `RenderBiped.renderEquippedItems`: skeletons hold a bow
        if let Some(arm) = held {
            let arm = arm
                * Mat4::from_scale(Vec3::splat(16.0))
                * Mat4::from_translation(Vec3::new(-1.0 / 16.0, 7.0 / 16.0, 1.0 / 16.0));
            let (transform, item) = if kind == MobKind::PigZombie {
                let transform = arm
                    * Mat4::from_translation(Vec3::new(0.0, 3.0 / 16.0, 0.0))
                    * Mat4::from_scale(Vec3::new(0.625, -0.625, 0.625))
                    * Mat4::from_rotation_x(-100_f32.to_radians())
                    * Mat4::from_rotation_y(45_f32.to_radians());
                (transform, common::item::Item::GOLDEN_SWORD)
            } else {
                let transform = arm
                    * Mat4::from_translation(Vec3::new(0.25, 3.0 / 16.0, -3.0 / 16.0))
                    * Mat4::from_scale(Vec3::splat(0.375))
                    * Mat4::from_rotation_z(60_f32.to_radians())
                    * Mat4::from_rotation_x(-90_f32.to_radians())
                    * Mat4::from_rotation_z(20_f32.to_radians());
                (transform, common::item::Item::BOW)
            };
            self.held_item(
                transform,
                common::item::ItemStack::one(item),
                brightness,
                None,
            );
        }
    }

    pub(super) fn arrow(
        &mut self,
        entity: &RemoteEntity,
        position: Vec3,
        partial: f32,
        brightness: f32,
    ) {
        let [yaw, pitch] = [0, 1].map(|i| {
            let previous = entity.previous_animation.rotation[i];
            angle(previous, entity.animation.rotation[i], partial).to_radians()
        });
        let shake = entity.shake as f32 - partial;
        let shake = if shake > 0.0 {
            (-sin(shake * 3.0) * shake).to_radians()
        } else {
            0.0
        };
        let rotation = Mat3::from_rotation_y(yaw - PI / 2.0)
            * Mat3::from_rotation_z(pitch)
            * Mat3::from_rotation_z(shake)
            * Mat3::from_rotation_x(PI / 4.0);
        let color = super::gray(brightness);
        let place = |rotation: Mat3, p: Vec3| {
            rotation * ((p + Vec3::new(-4.0, 0.0, 0.0)) * 0.05625) + position
        };
        let cap = [
            Vec3::new(-7.0, -2.0, -2.0),
            Vec3::new(-7.0, -2.0, 2.0),
            Vec3::new(-7.0, 2.0, 2.0),
            Vec3::new(-7.0, 2.0, -2.0),
        ];
        let cap_uv = [0.0, 5.0 / 32.0, 0.15625, 5.0 / 32.0];
        self.quad(cap.map(|p| place(rotation, p)), cap_uv, 22, color);
        let [a, b, c, d] = cap;
        self.quad([d, c, b, a].map(|p| place(rotation, p)), cap_uv, 22, color);
        for angle in [PI / 2.0, PI, PI * 1.5, PI * 2.0] {
            let rotation = rotation * Mat3::from_rotation_x(angle);
            let points = [
                Vec3::new(-8.0, -2.0, 0.0),
                Vec3::new(8.0, -2.0, 0.0),
                Vec3::new(8.0, 2.0, 0.0),
                Vec3::new(-8.0, 2.0, 0.0),
            ]
            .map(|p| place(rotation, p));
            self.quad(points, [0.0, 0.0, 0.5, 0.15625], 22, color);
        }
    }
}

fn quadruped(model: &mut Model<'_>, kind: MobKind, head: [f32; 3], gait: f32, overlay: bool) {
    let pig = kind == MobKind::Pig;
    let cow = kind == MobKind::Cow;
    let sheep = kind == MobKind::Sheep;
    let leg_height = if pig { 6.0 } else { 12.0 };
    let dilation = if pig && overlay { 0.5 } else { 0.0 };
    let head_part = if cow {
        Part::new(
            [-4.0, -4.0, -6.0],
            [8.0, 8.0, 6.0],
            [0.0, 4.0, -8.0],
            [0.0; 2],
        )
    } else if sheep {
        Part::new(
            [-3.0, -4.0, if overlay { -4.0 } else { -6.0 }],
            [6.0, 6.0, if overlay { 6.0 } else { 8.0 }],
            [0.0, 6.0, -8.0],
            [0.0; 2],
        )
        .inflate(if overlay { 0.6 } else { 0.0 })
    } else {
        Part::new(
            [-4.0, -4.0, -8.0],
            [8.0; 3],
            [0.0, 18.0 - leg_height, -6.0],
            [0.0; 2],
        )
        .inflate(dilation)
    };
    model.part(head_part.rotate(head));
    let body = if cow {
        Part::new(
            [-6.0, -10.0, -7.0],
            [12.0, 18.0, 10.0],
            [0.0, 5.0, 2.0],
            [18.0, 4.0],
        )
    } else if sheep {
        Part::new(
            [-4.0, -10.0, -7.0],
            [8.0, 16.0, 6.0],
            [0.0, 5.0, 2.0],
            [28.0, 8.0],
        )
        .inflate(if overlay { 1.75 } else { 0.0 })
    } else {
        Part::new(
            [-5.0, -10.0, -7.0],
            [10.0, 16.0, 8.0],
            [0.0, 17.0 - leg_height, 2.0],
            [28.0, 8.0],
        )
        .inflate(dilation)
    };
    model.part(body.rotate([PI / 2.0, 0.0, 0.0]));
    let x = if cow { 4.0 } else { 3.0 };
    let front = if cow { -6.0 } else { -5.0 };
    for (x, z, gait) in [
        (-x, 7.0, gait),
        (x, 7.0, -gait),
        (-x, front, -gait),
        (x, front, gait),
    ] {
        model.part(
            Part::new(
                [-2.0, 0.0, -2.0],
                [4.0, if sheep && overlay { 6.0 } else { leg_height }, 4.0],
                [x, 24.0 - leg_height, z],
                [0.0, 16.0],
            )
            .inflate(if sheep && overlay { 0.5 } else { dilation })
            .rotate([gait, 0.0, 0.0]),
        );
    }
    if cow {
        for x in [-4.0, 3.0] {
            model.part(
                Part::new(
                    [x, -5.0, -4.0],
                    [1.0, 3.0, 1.0],
                    [0.0, 3.0, -7.0],
                    [22.0, 0.0],
                )
                .rotate(head),
            );
        }
        model.part(
            Part::new(
                [-2.0, -3.0, 0.0],
                [4.0, 6.0, 2.0],
                [0.0, 14.0, 6.0],
                [52.0, 0.0],
            )
            .rotate([PI / 2.0, 0.0, 0.0]),
        );
    }
}

fn creeper(model: &mut Model<'_>, head: [f32; 3], leg: f32, dilation: f32) {
    model.part(
        Part::new([-4.0, -8.0, -4.0], [8.0; 3], [0.0, 4.0, 0.0], [0.0; 2])
            .rotate(head)
            .inflate(dilation),
    );
    model.part(
        Part::new(
            [-4.0, 0.0, -2.0],
            [8.0, 12.0, 4.0],
            [0.0, 4.0, 0.0],
            [16.0; 2],
        )
        .inflate(dilation),
    );
    for (x, z, gait) in [
        (-2.0, 4.0, leg),
        (2.0, 4.0, -leg),
        (-2.0, -4.0, -leg),
        (2.0, -4.0, leg),
    ] {
        model.part(
            Part::new(
                [-2.0, 0.0, -2.0],
                [4.0, 6.0, 4.0],
                [x, 16.0, z],
                [0.0, 16.0],
            )
            .rotate([gait, 0.0, 0.0])
            .inflate(dilation),
        );
    }
}

fn spider(model: &mut Model<'_>, head: [f32; 3], limb: f32, speed: f32) {
    model
        .part(Part::new([-4.0, -4.0, -8.0], [8.0; 3], [0.0, 15.0, -3.0], [32.0, 4.0]).rotate(head));
    model.part(Part::new([-3.0; 3], [6.0; 3], [0.0, 15.0, 0.0], [0.0; 2]));
    model.part(Part::new(
        [-5.0, -4.0, -6.0],
        [10.0, 8.0, 12.0],
        [0.0, 15.0, 9.0],
        [0.0, 12.0],
    ));
    for (i, phase) in [0.0, PI, PI / 2.0, PI * 1.5].into_iter().enumerate() {
        for side in [-1.0, 1.0] {
            let base_yaw = [PI / 4.0, PI / 8.0, -PI / 8.0, -PI / 4.0][i];
            let yaw = -side * (base_yaw - cos(limb * 0.6662 * 2.0 + phase) * 0.4 * speed);
            let roll = side
                * (PI / 4.0 * if i == 0 || i == 3 { 1.0 } else { 0.74 }
                    - (sin(limb * 0.6662 + phase) * 0.4).abs() * speed);
            model.part(
                Part::new(
                    [if side < 0.0 { -15.0 } else { -1.0 }, -1.0, -1.0],
                    [16.0, 2.0, 2.0],
                    [side * 4.0, 15.0, 2.0 - i as f32],
                    [18.0, 0.0],
                )
                .rotate([0.0, yaw, roll]),
            );
        }
    }
}

fn wolf(
    model: &mut Model<'_>,
    entity: &RemoteEntity,
    head: [f32; 3],
    limb: f32,
    speed: f32,
    partial: f32,
) {
    let previous = entity.previous_animation.special;
    let current = entity.animation.special;
    let interest = lerp(previous.interest, current.interest, partial);
    let shake = lerp(previous.shake, current.shake, partial);
    let shake_angle = |offset: f32| {
        let phase = ((shake + offset) / 1.8).clamp(0.0, 1.0);
        sin(phase * PI) * sin(phase * PI * 11.0) * 0.15 * PI
    };
    if current.wet {
        model.color *= 0.75 + shake / 2.0 * 0.25;
    }
    let head = [head[0], head[1], interest * 0.15 * PI + shake_angle(0.0)];
    for (min, size, x, uv) in [
        ([-3.0, -3.0, -2.0], [6.0, 6.0, 4.0], -1.0, [0.0, 0.0]),
        ([-3.0, -5.0, 0.0], [2.0, 2.0, 1.0], -1.0, [16.0, 14.0]),
        ([1.0, -5.0, 0.0], [2.0, 2.0, 1.0], -1.0, [16.0, 14.0]),
        ([-2.0, 0.0, -5.0], [3.0, 3.0, 4.0], -0.5, [0.0, 10.0]),
    ] {
        model.part(Part::new(min, size, [x, 13.5, -7.0], uv).rotate_yxz(head));
    }
    let sitting = entity.flags[1] & 1 != 0;
    let (body, mane, rear) = if sitting {
        (
            ([0.0, 18.0, 0.0], 0.25),
            ([-1.0, 16.0, -3.0], 0.4),
            [-1.0, 21.0, 6.0],
        )
    } else {
        (
            ([0.0, 14.0, 2.0], 0.5),
            ([-1.0, 14.0, -3.0], 0.5),
            [-1.0, 12.0, 8.0],
        )
    };
    for (min, size, (pivot, pitch), uv, roll) in [
        (
            [-4.0, -2.0, -3.0],
            [6.0, 9.0, 6.0],
            body,
            [18.0, 14.0],
            -0.16,
        ),
        (
            [-4.0, -3.0, -3.0],
            [8.0, 6.0, 7.0],
            mane,
            [21.0, 0.0],
            -0.08,
        ),
    ] {
        model.part(Part::new(min, size, pivot, uv).rotate([PI * pitch, 0.0, shake_angle(roll)]));
    }
    for index in 0..4 {
        let left = index % 2 == 0;
        let front = index >= 2;
        let gait =
            cos(limb * 0.6662 + if index == 1 || index == 2 { PI } else { 0.0 }) * 1.4 * speed;
        let (x, y, z, pitch) = if sitting && front {
            (if left { -2.49 } else { 0.51 }, 17.0, -4.0, PI * 1.85)
        } else if sitting {
            (if left { -2.5 } else { 0.5 }, 22.0, 2.0, PI * 1.5)
        } else {
            (
                if left { -2.5 } else { 0.5 },
                16.0,
                if front { -4.0 } else { 7.0 },
                gait,
            )
        };
        model.part(
            Part::new([-1.0, 0.0, -1.0], [2.0, 8.0, 2.0], [x, y, z], [0.0, 18.0])
                .rotate([pitch, 0.0, 0.0]),
        );
    }
    let angry = entity.flags[1] & 2 != 0;
    let tail = if angry {
        PI * 0.49
    } else if entity.flags[1] & 4 != 0 {
        (0.55 - (20.0 - f32::from(entity.flags[2])) * 0.02) * PI
    } else {
        PI * 0.2
    };
    model.part(
        Part::new([-1.0, 0.0, -1.0], [2.0, 8.0, 2.0], rear, [9.0, 18.0]).rotate_yxz([
            tail,
            if angry {
                0.0
            } else {
                cos(limb * 0.6662) * 1.4 * speed
            },
            shake_angle(-0.2),
        ]),
    );
}