alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH 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
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),
]),
);
}