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 / player.rs

use std::f32::consts::PI;

use common::entity::flags;
use common::inventory::PlayerInventory;
use common::item::{Item, ItemKind, ItemStack};
use common::math::{cos, sin};
use common::world::texture;
use glam::{Mat4, Vec3, Vec4};

use super::{
    Geometry, lerp,
    models::{Model, Part},
};
use crate::gui::Portrait;
use crate::player::LocalPlayer;

const SKIN: i32 = crate::assets::entity_texture("mob/char.png");
const ARMOR: i32 = crate::assets::ARMOR_TEXTURES;

#[derive(Clone, Copy, Default)]
struct Joint {
    position: Vec3,
    angles: Vec3,
}

impl Joint {
    fn transform(self) -> Mat4 {
        Mat4::from_translation(self.position)
            * Mat4::from_rotation_z(self.angles.z)
            * Mat4::from_rotation_y(self.angles.y)
            * Mat4::from_rotation_x(self.angles.x)
    }

    fn part(self, min: [f32; 3], size: [f32; 3], uv: [f32; 2]) -> Part {
        Part::new(min, size, self.position.to_array(), uv).rotate(self.angles.to_array())
    }
}

struct Biped {
    head: Joint,
    body: Joint,
    arms: [Joint; 2],
    legs: [Joint; 2],
}

impl Biped {
    fn pose(entity: &common::entity::Entity, held: bool, head: [f32; 2], partial: f32) -> Self {
        let living = entity.living.as_ref().expect("living player");
        let phase = living.walk_animation_progress - living.walk_animation_speed * (1.0 - partial);
        let speed = lerp(
            living.prev_walk_animation_speed,
            living.walk_animation_speed,
            partial,
        )
        .min(1.0);
        let age = entity.ticks_existed as f32 + partial;
        let swing = super::swing(living, partial);
        let mut pose = Self {
            head: Joint {
                angles: Vec3::new(head[1].to_radians(), head[0].to_radians(), 0.0),
                ..Default::default()
            },
            body: Joint::default(),
            arms: std::array::from_fn(|i| Joint {
                position: Vec3::new(if i == 0 { -5.0 } else { 5.0 }, 2.0, 0.0),
                angles: Vec3::new(
                    cos(phase * 0.6662 + if i == 0 { PI } else { 0.0 }) * 2.0 * speed * 0.5,
                    0.0,
                    0.0,
                ),
            }),
            legs: std::array::from_fn(|i| Joint {
                position: Vec3::new(if i == 0 { -2.0 } else { 2.0 }, 12.0, 0.0),
                angles: Vec3::new(
                    cos(phase * 0.6662 + if i == 0 { 0.0 } else { PI }) * 1.4 * speed,
                    0.0,
                    0.0,
                ),
            }),
        };
        if entity.flag(flags::RIDING) {
            for i in 0..2 {
                pose.arms[i].angles.x -= PI * 0.2;
                pose.legs[i].angles.x = -PI * 0.4;
                pose.legs[i].angles.y = if i == 0 { PI * 0.1 } else { -PI * 0.1 };
            }
        }
        if held {
            pose.arms[0].angles.x = pose.arms[0].angles.x * 0.5 - PI * 0.1;
        }
        let twist = sin(swing.sqrt() * PI * 2.0) * 0.2;
        pose.body.angles.y = twist;
        for (i, arm) in pose.arms.iter_mut().enumerate() {
            let side = if i == 0 { -1.0 } else { 1.0 };
            arm.position.x = side * cos(twist) * 5.0;
            arm.position.z = -side * sin(twist) * 5.0;
            arm.angles.y = twist;
        }
        pose.arms[1].angles.x += twist;
        let mut strike = 1.0 - swing;
        strike *= strike;
        strike *= strike;
        let strike = sin((1.0 - strike) * PI);
        let head = sin(swing * PI) * -(pose.head.angles.x - 0.7) * 0.75;
        pose.arms[0].angles.x =
            (f64::from(pose.arms[0].angles.x) - (f64::from(strike) * 1.2 + f64::from(head))) as f32;
        pose.arms[0].angles.y += twist * 2.0;
        pose.arms[0].angles.z = sin(swing * PI) * -0.4;
        if entity.is_sneaking() {
            pose.body.angles.x = 0.5;
            pose.head.position.y = 1.0;
            for i in 0..2 {
                pose.arms[i].angles.x += 0.4;
                pose.legs[i].position.y = 9.0;
                pose.legs[i].position.z = 4.0;
            }
        }
        let sway = cos(age * 0.09) * 0.05 + 0.05;
        let breathe = sin(age * 0.067) * 0.05;
        pose.arms[0].angles.z += sway;
        pose.arms[1].angles.z -= sway;
        pose.arms[0].angles.x += breathe;
        pose.arms[1].angles.x -= breathe;
        pose
    }

    fn render(&self, model: &mut Model<'_>, dilation: f32, mask: u8) {
        if mask & 1 != 0 {
            for (uv, extra) in [([0.0; 2], 0.0), ([32.0, 0.0], 0.5)] {
                let head = self.head.part([-4.0, -8.0, -4.0], [8.0; 3], uv);
                model.part(head.inflate(dilation + extra));
            }
        }
        if mask & 2 != 0 {
            model.part(
                self.body
                    .part([-4.0, 0.0, -2.0], [8.0, 12.0, 4.0], [16.0; 2])
                    .inflate(dilation),
            );
        }
        if mask & 4 != 0 {
            for (i, arm) in self.arms.iter().enumerate() {
                model.part(
                    arm.part(
                        [if i == 0 { -3.0 } else { -1.0 }, -2.0, -2.0],
                        [4.0, 12.0, 4.0],
                        [40.0, 16.0],
                    )
                    .inflate(dilation)
                    .mirror(i == 1),
                );
            }
        }
        if mask & 8 != 0 {
            for (i, leg) in self.legs.iter().enumerate() {
                model.part(
                    leg.part([-2.0, 0.0, -2.0], [4.0, 12.0, 4.0], [0.0, 16.0])
                        .inflate(dilation)
                        .mirror(i == 1),
                );
            }
        }
    }
}

impl Geometry {
    #[allow(clippy::too_many_arguments)]
    pub(super) fn player_model(
        &mut self,
        entity: &common::entity::Entity,
        inventory: &PlayerInventory,
        partial: f32,
        position: Vec3,
        brightness: f32,
        rotation: Option<[f32; 3]>,
        bed: Option<usize>,
    ) {
        let Some(living) = &entity.living else {
            return;
        };
        let [yaw, pitch, body] = rotation.unwrap_or([
            lerp(entity.prev_yaw, entity.yaw, partial),
            lerp(entity.prev_pitch, entity.pitch, partial),
            lerp(
                living.prev_render_yaw_offset,
                living.render_yaw_offset,
                partial,
            ),
        ]);
        let pose = Biped::pose(
            entity,
            inventory.held().is_some(),
            [yaw - body, pitch],
            partial,
        );
        let death = if living.death_time > 0 {
            ((living.death_time as f32 + partial - 1.0) / 20.0 * 1.6)
                .max(0.0)
                .sqrt()
                .min(1.0)
                * PI
                / 2.0
        } else {
            0.0
        };
        let corpse = match bed {
            Some(direction) => {
                let (offset, orientation) = [
                    (Vec3::new(0.0, 0.0, -1.8), 90.0_f32),
                    (Vec3::new(1.8, 0.0, 0.0), 0.0),
                    (Vec3::new(0.0, 0.0, 1.8), 270.0),
                    (Vec3::new(-1.8, 0.0, 0.0), 180.0),
                ][direction];
                Mat4::from_translation(position + offset)
                    * Mat4::from_rotation_y(orientation.to_radians())
                    * Mat4::from_rotation_z(PI / 2.0)
                    * Mat4::from_rotation_y(270_f32.to_radians())
            }
            None => {
                Mat4::from_translation(position)
                    * Mat4::from_rotation_y((180.0 - body).to_radians())
                    * Mat4::from_rotation_z(death)
            }
        };
        let transform = corpse
            * Mat4::from_scale(Vec3::new(-0.9375, -0.9375, 0.9375))
            * Mat4::from_translation(Vec3::new(0.0, -1.5078125, 0.0))
            * Mat4::from_scale(Vec3::splat(1.0 / 16.0));
        let hurt = living.hurt_time > 0 || living.death_time > 0;
        let mut model = Model {
            overlays: super::models::hurt_overlay(hurt, brightness),
            ..Model::new(self, transform, SKIN, brightness)
        };
        pose.render(&mut model, 0.0, 15);
        for (pass, mask) in [1, 6, 10, 8].into_iter().enumerate() {
            let Some(stack) = inventory.armor[3 - pass] else {
                continue;
            };
            let Some(ItemKind::Armor { render, .. }) = stack.item.properties().map(|p| p.kind)
            else {
                continue;
            };
            model.texture = ARMOR + i32::from(render) * 2 + i32::from(pass == 2);
            pose.render(&mut model, if pass == 2 { 0.5 } else { 1.0 }, mask);
        }
        if let Some(helmet) = inventory.armor[3]
            && helmet.item.block().is_some()
        {
            let transform = transform
                * pose.head.transform()
                * Mat4::from_scale(Vec3::splat(16.0))
                * Mat4::from_translation(Vec3::new(0.0, -0.25, 0.0))
                * Mat4::from_rotation_y(PI)
                * Mat4::from_scale(Vec3::new(0.625, -0.625, 0.625));
            self.held_item(transform, helmet, brightness, None);
        }
        if let Some(stack) = inventory.held() {
            let mut transform = transform
                * pose.arms[0].transform()
                * Mat4::from_scale(Vec3::splat(16.0))
                * Mat4::from_translation(Vec3::new(-1.0 / 16.0, 7.0 / 16.0, 1.0 / 16.0));
            if item_in_3d(stack) {
                transform *= Mat4::from_translation(Vec3::new(0.0, 3.0 / 16.0, -5.0 / 16.0))
                    * Mat4::from_rotation_x(20_f32.to_radians())
                    * Mat4::from_rotation_y(45_f32.to_radians())
                    * Mat4::from_scale(Vec3::new(0.375, -0.375, 0.375));
            } else if stack.item.properties().is_some_and(|p| p.full_3d) {
                if stack.item == Item::FISHING_ROD {
                    transform *= Mat4::from_rotation_z(PI)
                        * Mat4::from_translation(Vec3::new(0.0, -0.125, 0.0));
                }
                transform *= 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());
            } else {
                transform *= 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(-PI / 2.0)
                    * Mat4::from_rotation_z(20_f32.to_radians());
            }
            self.held_item(transform, stack, brightness, Some(entity.id));
        }
    }

    pub fn names(
        &mut self,
        entities: &crate::entities::Entities,
        viewer: &LocalPlayer,
        font: &crate::gui::Geometry,
        partial: f32,
        hidden: bool,
    ) {
        let eye = viewer.eye(partial);
        let yaw = lerp(viewer.entity.prev_yaw, viewer.entity.yaw, partial);
        let pitch = lerp(viewer.entity.prev_pitch, viewer.entity.pitch, partial);
        let mut ranges = [0..0, 0..0];
        for (pass, range) in ranges.iter_mut().enumerate() {
            range.start = self.vertices.len() as u32;
            for remote in entities.0.values().filter(|_| !hidden) {
                let Some(player) = &remote.player else {
                    continue;
                };
                let sneaking = player.entity.is_sneaking();
                let position = remote.previous.lerp(remote.position, f64::from(partial));
                if (sneaking && pass == 0)
                    || position.distance_squared(viewer.entity.pos)
                        >= if sneaking { 1024.0 } else { 4096.0 }
                {
                    continue;
                }
                let height = if player.entity.sleep.sleeping {
                    0.8
                } else if sneaking {
                    2.05 - 0.125
                } else {
                    2.3
                };
                let transform =
                    Mat4::from_translation((position - eye).as_vec3() + Vec3::Y * height)
                        * Mat4::from_rotation_y(-yaw.to_radians())
                        * Mat4::from_rotation_x(pitch.to_radians())
                        * Mat4::from_scale(Vec3::new(-1.6 / 60.0, -1.6 / 60.0, 1.6 / 60.0));
                let faint = pass == 0 || sneaking;
                let (glyphs, width) =
                    font.layout(&player.name, if faint { 0x20ffffff } else { 0xffffffff });
                let left = -(width / 2.0).floor();
                if faint {
                    self.quad(
                        [
                            [-1.0 + left, -1.0],
                            [-1.0 + left, 8.0],
                            [-left + 1.0, 8.0],
                            [-left + 1.0, -1.0],
                        ]
                        .map(|[x, y]| transform.transform_point3(Vec3::new(x, y, 0.0))),
                        [0.0; 4],
                        -1,
                        0x40000000,
                    );
                }
                self.glyphs(glyphs, Vec3::new(left, 0.0, 0.0), transform);
            }
            range.end = self.vertices.len() as u32;
        }
        [self.names_hidden, self.names] = ranges;
    }

    pub fn build_portrait(
        &mut self,
        player: &LocalPlayer,
        inventory: &PlayerInventory,
        portrait: Option<Portrait>,
    ) {
        let start = self.vertices.len() as u32;
        self.portrait = start..start;
        let Some(Portrait { origin, cursor, .. }) = portrait else {
            return;
        };
        let horizontal = ((origin[0] - cursor[0]) / 40.0).atan();
        let vertical = ((origin[1] - 50.0 - cursor[1]) / 40.0).atan();
        self.player_model(
            &player.entity,
            inventory,
            1.0,
            Vec3::ZERO,
            1.0,
            Some([horizontal * 40.0, -vertical * 20.0, horizontal * 20.0]),
            None,
        );
        let transform = Mat4::from_translation(Vec3::new(origin[0], origin[1], 0.0))
            * Mat4::from_scale(Vec3::new(30.0, -30.0, 30.0))
            * Mat4::from_rotation_x((-vertical * 20.0).to_radians());
        for vertex in &mut self.vertices[start as usize..] {
            vertex.position = transform
                .transform_point3(Vec3::from_array(vertex.position))
                .to_array();
        }
        self.portrait.end = self.vertices.len() as u32;
    }

    /// `holder` is the entity holding the item: a fishing rod with its hook out looks cast.
    pub(super) fn held_item(
        &mut self,
        mut transform: Mat4,
        stack: ItemStack,
        brightness: f32,
        holder: Option<i32>,
    ) {
        let cast = stack.item == common::item::Item::FISHING_ROD
            && holder.is_some_and(|id| self.anglers.contains(&id));
        if item_in_3d(stack) {
            let block = stack.item.block().expect("block item");
            self.block_item(block, stack.damage as u8, transform, brightness);
        } else {
            transform *= Mat4::from_translation(Vec3::new(0.0, -0.3, 0.0))
                * Mat4::from_scale(Vec3::splat(1.5))
                * Mat4::from_rotation_y(50_f32.to_radians())
                * Mat4::from_rotation_z(335_f32.to_radians())
                * Mat4::from_translation(Vec3::new(-15.0 / 16.0, -1.0 / 16.0, 0.0));
            let (atlas, icon) = stack.item.block().map_or_else(
                || {
                    let icon = super::super::item::icon(stack);
                    (crate::gui::ITEMS, if cast { icon + 16 } else { icon })
                },
                |block| (0, texture::inventory_face(block, stack.damage as u8, 0)),
            );
            self.item_sprite(transform, atlas, icon, brightness);
        }
    }
}

fn item_in_3d(stack: ItemStack) -> bool {
    stack.item.block().is_some_and(super::super::item::in_3d)
}

pub(crate) fn portrait_projection([width, height]: [f32; 2]) -> Mat4 {
    Mat4::from_cols(
        Vec4::new(2.0 / width, 0.0, 0.0, 0.0),
        Vec4::new(0.0, -2.0 / height, 0.0, 0.0),
        Vec4::new(0.0, 0.0, 1.0 / 200.0, 0.0),
        Vec4::new(-1.0, 1.0, 0.5, 1.0),
    )
}