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

use std::ops::Range;

mod effects;
mod fishing;
mod hand;
mod map;
mod models;
mod moving;
mod paintings;
mod player;
mod signs;
mod vehicles;
mod weather;
pub use moving::Style;
pub(super) use player::portrait_projection;
pub use weather::Viewer;

use bytemuck::{Pod, Zeroable};
use common::entity::mob::MobKind;
use common::world::{shape, texture};
use glam::{DVec3, Mat3, Vec3};

use crate::entities::{Entities, Kind};
use crate::interaction::Interaction;
use crate::player::LocalPlayer;
use crate::world::ClientWorld;

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct Vertex {
    position: [f32; 3],
    uv: [f32; 2],
    color: u32,
    pub(super) texture: i32,
}

#[derive(Default)]
pub struct Geometry {
    pub vertices: Vec<Vertex>,
    pub map: Option<i16>,
    pub map_text: Range<u32>,
    map_transform: Option<glam::Mat4>,
    pub objects: Range<u32>,
    pub lines: Range<u32>,
    pub outline: Range<u32>,
    pub cracks: Range<u32>,
    pub hand: Range<u32>,
    pub portrait: Range<u32>,
    /// Rain and snow, blended over the translucent terrain.
    pub weather: Range<u32>,
    /// Lightning, drawn additively.
    pub bolts: Range<u32>,
    pub clouds: Range<u32>,
    /// Blocks pistons are moving.
    pub moving: Range<u32>,
    /// `GameSettings.hideGUI` also hides the hand.
    pub hide_hand: bool,
    /// Sign boards and their text.
    pub signs: Range<u32>,
    pub names: Range<u32>,
    pub names_hidden: Range<u32>,
    /// Fancy graphics draw entity shadows.
    pub fancy: bool,
    /// Players with a fishing hook out, whose rods show the cast icon.
    anglers: Vec<i32>,
    /// The shells of charged creepers, drawn additively.
    auras: Vec<Vertex>,
    /// Entity shadows, blended without writing depth.
    pub shadows: Range<u32>,
    /// The white flashes of primed TNT, drawn additively.
    pub flashes: Range<u32>,
    /// Fancy clouds first fill the depth buffer so only their outer faces show.
    pub fancy_clouds: bool,
}

/// The unit offsets towards each face, in `Facing` order.
const SIDES: [DVec3; 6] = [
    DVec3::NEG_Y,
    DVec3::Y,
    DVec3::NEG_Z,
    DVec3::Z,
    DVec3::NEG_X,
    DVec3::X,
];

/// A billboarded item icon, half a block wide.
const SPRITE: [Vec3; 4] = [
    Vec3::new(-0.25, 0.375, 0.0),
    Vec3::new(0.25, 0.375, 0.0),
    Vec3::new(0.25, -0.125, 0.0),
    Vec3::new(-0.25, -0.125, 0.0),
];

/// An opaque gray of the given brightness.
fn gray(brightness: f32) -> u32 {
    0xff000000 | ((brightness * 255.0) as u32 * 0x010101)
}

/// The rectangle of a tile in a 16 by 16 atlas.
fn tile_uv(tile: u8) -> [f32; 4] {
    let size = 15.99 / 256.0;
    [
        f32::from(tile & 15) / 16.0,
        f32::from(tile >> 4) / 16.0,
        size,
        size,
    ]
}

fn lerp(previous: f32, current: f32, partial: f32) -> f32 {
    previous + (current - previous) * partial
}

/// The interpolated progress of an arm swing.
fn swing(living: &common::entity::Living, partial: f32) -> f32 {
    living.prev_swing_progress
        + (living.swing_progress - living.prev_swing_progress).rem_euclid(1.0) * partial
}

fn brightness_at(world: &ClientWorld, p: DVec3, sky_darkness: u32) -> f32 {
    world.brightness[usize::from(world.light_level(p.x, p.y, p.z, sky_darkness))]
}

/// The corners of a box: the bottom four counterclockwise from `min`, then the top four.
fn box_corners(min: Vec3, max: Vec3) -> [Vec3; 8] {
    [
        min,
        Vec3::new(max.x, min.y, min.z),
        Vec3::new(max.x, max.y, min.z),
        Vec3::new(min.x, max.y, min.z),
        Vec3::new(min.x, min.y, max.z),
        Vec3::new(max.x, min.y, max.z),
        max,
        Vec3::new(min.x, max.y, max.z),
    ]
}

impl Geometry {
    fn quad(&mut self, points: [Vec3; 4], [u, v, w, h]: [f32; 4], texture: i32, color: u32) {
        let uv = [[u, v], [u + w, v], [u + w, v + h], [u, v + h]];
        self.quad_uv(points, uv, texture, color);
    }

    fn quad_uv(&mut self, points: [Vec3; 4], uv: [[f32; 2]; 4], texture: i32, color: u32) {
        self.vertices.extend([0, 1, 2, 0, 2, 3].map(|i| Vertex {
            position: points[i].to_array(),
            uv: uv[i],
            color,
            texture,
        }));
    }

    fn cuboid(
        &mut self,
        min: Vec3,
        max: Vec3,
        transform: impl Fn(Vec3) -> Vec3,
        textures: [u8; 6],
        brightness: [f32; 6],
    ) {
        // `RenderBlocks.render*Face` corners in the order of the texture's top-left, top-right,
        // bottom-right and bottom-left
        const FACES: [[usize; 4]; 6] = [
            [0, 1, 5, 4],
            [3, 2, 6, 7],
            [2, 3, 0, 1],
            [7, 6, 5, 4],
            [3, 7, 4, 0],
            [6, 2, 1, 5],
        ];
        let corners = box_corners(min, max).map(transform);
        for (face, indices) in FACES.into_iter().enumerate() {
            let shade = brightness[face] * [0.5, 1.0, 0.8, 0.8, 0.6, 0.6][face];
            let points = indices.map(|i| corners[i]);
            self.quad(points, tile_uv(textures[face]), 0, gray(shade));
        }
    }

    /// Laid out glyphs with the top-left corner at `origin` in `transform`'s space.
    fn glyphs(&mut self, glyphs: Vec<(u8, f32, u32)>, origin: Vec3, transform: glam::Mat4) {
        for (ch, offset, color) in glyphs {
            let (x, y) = (origin.x + offset, origin.y);
            let points = [[x, y], [x + 7.99, y], [x + 7.99, y + 7.99], [x, y + 7.99]]
                .map(|[x, y]| transform.transform_point3(Vec3::new(x, y, origin.z)));
            let uv = crate::gui::Geometry::glyph_uv(ch);
            self.quad(points, uv, crate::gui::FONT, color);
        }
    }

    fn block_item(
        &mut self,
        block: common::world::Block,
        metadata: u8,
        transform: glam::Mat4,
        brightness: f32,
    ) {
        for face in super::item::faces(block, metadata) {
            let color =
                super::item::color(super::item::tint(block, metadata), brightness * face.shade);
            let points = face.points.map(|p| transform.transform_point3(p));
            self.quad_uv(points, face.uv, 0, color);
        }
    }

    /// `TileEntityMobSpawnerRenderer`: a small spinning copy of each spawner's mob.
    fn spawner_mobs(&mut self, world: &ClientWorld, eye: DVec3, partial: f32, sky: u32) {
        let center = common::world::ChunkPos::containing(
            common::math::floor_f64(eye.x),
            common::math::floor_f64(eye.z),
        );
        for ([x, y, z], kind, _) in world.spawner_mobs_near(center, 4) {
            let corner = DVec3::new(f64::from(x), f64::from(y), f64::from(z));
            if (corner + 0.5).distance_squared(eye) >= 64.0 * 64.0 {
                continue;
            }
            let (yaw, previous) = world
                .spawner_yaws
                .get(&[x, y, z])
                .copied()
                .unwrap_or_default();
            let spin = ((previous + (yaw - previous) * f64::from(partial)) * 10.0) as f32;
            let rotation = Mat3::from_rotation_y(spin.to_radians())
                * Mat3::from_rotation_x((-30.0_f32).to_radians());
            let origin = (corner - eye).as_vec3()
                + Vec3::new(0.5, 0.4, 0.5)
                + rotation * Vec3::new(0.0, -0.4, 0.0);
            let brightness = brightness_at(world, corner + DVec3::new(0.5, 0.0, 0.5), sky);
            let start = self.vertices.len();
            let entity = crate::entities::RemoteEntity::new(0, Kind::Mob(kind), [0; 3]);
            self.mob(kind, &entity, Vec3::ZERO, partial, brightness);
            let transform = rotation * (7.0 / 16.0);
            for vertex in &mut self.vertices[start..] {
                vertex.position =
                    (origin + transform * Vec3::from_array(vertex.position)).to_array();
            }
        }
    }

    pub fn build(
        &mut self,
        entities: &Entities,
        world: &ClientWorld,
        player: &LocalPlayer,
        interaction: &Interaction,
        partial: f32,
        sky_darkness: u32,
    ) {
        self.vertices.clear();
        self.anglers = entities
            .0
            .values()
            .filter_map(|e| match e.kind {
                Kind::Hook(owner) => owner,
                _ => None,
            })
            .collect();
        self.map = None;
        self.map_text = 0..0;
        self.map_transform = None;
        self.portrait = 0..0;
        self.weather = 0..0;
        self.bolts = 0..0;
        self.clouds = 0..0;
        self.moving = 0..0;
        let mut flashes = Vec::new();
        let mut shadows = Vec::new();
        let eye = player.eye(partial);
        let view_yaw = lerp(player.entity.prev_yaw, player.entity.yaw, partial);
        let billboard = Mat3::from_rotation_y(-player.entity.yaw.to_radians())
            * Mat3::from_rotation_x(player.entity.pitch.to_radians());
        if (player.third_person || player.entity.sleep.sleeping) && player.started {
            let pos = player
                .entity
                .prev_pos
                .lerp(player.entity.pos, f64::from(partial));
            let feet = pos - DVec3::Y * f64::from(player.entity.y_offset);
            let height = f64::from(player.entity.height) * 0.66;
            if self.fancy {
                shadows.push((feet, player.entity.height, 0.5, 1.0));
            }
            if player.entity.fire > 0 || player.entity.flag(common::entity::flags::ON_FIRE) {
                self.entity_fire((feet - eye).as_vec3(), (0.6, 1.8, 0.0), view_yaw);
            }
            self.player_model(
                &player.entity,
                &interaction.inventory,
                partial,
                (feet - eye).as_vec3(),
                brightness_at(world, feet + DVec3::Y * height, sky_darkness),
                None,
                player.bed_direction(world),
            );
        }
        for (&id, entity) in &entities.0 {
            let position = entity.previous.lerp(entity.position, f64::from(partial));
            let (mut width, mut height, offset, shadow) = effects::dimensions(entity.kind);
            if matches!(entity.kind, Kind::Mob(MobKind::Slime)) {
                width *= f32::from(entity.flags[1].max(1));
                height = width;
            }
            // `Entity.getEntityBrightness` samples two thirds up the body
            let sample = match entity.kind {
                Kind::Mob(_) | Kind::Player => f64::from(height) * 0.66,
                _ => 0.0,
            };
            let brightness = brightness_at(world, position + DVec3::Y * sample, sky_darkness);
            let relative = (position - eye).as_vec3();
            if self.fancy {
                // `RenderItem.shadowOpaque`
                let opaque = if matches!(entity.kind, Kind::Item(_)) {
                    0.75
                } else {
                    1.0
                };
                shadows.push((position, height, shadow, opaque));
            }
            if entity.flags[0] & common::entity::flags::ON_FIRE != 0 {
                self.entity_fire(relative, (width, height, offset), view_yaw);
            }
            match entity.kind {
                Kind::Player => {
                    if let Some(remote) = &entity.player {
                        let bed = remote.entity.sleep.bed.map(|[x, y, z]| {
                            usize::from(shape::BlockView::metadata(world, x, y, z) & 3)
                        });
                        // `RenderPlayer.renderPlayer` lowers other sneaking players
                        let sneak = if remote.entity.is_sneaking() {
                            0.125
                        } else {
                            0.0
                        };
                        self.player_model(
                            &remote.entity,
                            &remote.inventory,
                            partial,
                            relative - Vec3::Y * sneak,
                            brightness,
                            None,
                            bed,
                        );
                    }
                }
                Kind::Mob(kind) => self.mob(kind, entity, relative, partial, brightness),
                Kind::Painting(painting) => self.painting(painting, relative, world, sky_darkness),
                Kind::Vehicle(kind) => {
                    self.vehicle(world, kind, entity, relative, partial, brightness)
                }
                Kind::Hook(_) => self.hook(relative, &billboard),
                Kind::Arrow => self.arrow(entity, relative, partial, brightness),
                Kind::Tnt => {
                    let swelling = if entity.animation.fuse < 10 {
                        1.0 + (1.0 - (entity.animation.fuse as f32 - partial + 1.0) / 10.0)
                            .clamp(0.0, 1.0)
                            .powi(4)
                            * 0.3
                    } else {
                        1.0
                    };
                    self.cuboid(
                        Vec3::splat(-0.5),
                        Vec3::splat(0.5),
                        |p| p * swelling + relative,
                        std::array::from_fn(|side| {
                            texture::face(common::world::Block::TNT, 0, side).0
                        }),
                        [brightness; 6],
                    );
                    if entity.animation.fuse / 5 % 2 == 0 {
                        let fuse = entity.animation.fuse as f32 - partial + 1.0;
                        flashes.push((relative, swelling, (1.0 - fuse / 100.0) * 0.8));
                    }
                }
                Kind::Falling(block) => {
                    // `RenderBlocks.renderBlockFallingSand`: each face is at least as bright as
                    // its neighbor cell
                    let cell = position.floor();
                    let center = brightness_at(world, cell, sky_darkness);
                    self.cuboid(
                        Vec3::splat(-0.5),
                        Vec3::splat(0.5),
                        |p| p + relative,
                        std::array::from_fn(|side| texture::face(block, 0, side).0),
                        SIDES.map(|side| {
                            brightness_at(world, cell + side, sky_darkness).max(center)
                        }),
                    );
                }
                Kind::Thrown(_) | Kind::Fireball => {
                    let (item, scale) = match entity.kind {
                        Kind::Thrown(item) => (item, 1.0),
                        _ => (common::item::Item::SNOWBALL, 4.0),
                    };
                    let tile = item.properties().map_or(0, |p| p.icon);
                    let points = SPRITE.map(|p| billboard * p * scale + relative);
                    self.quad(points, tile_uv(tile), crate::gui::ITEMS, gray(brightness));
                }
                Kind::Item(stack) => {
                    let phase = (id as f32 * 0.618034).fract() * std::f32::consts::TAU;
                    let age = entity.age as f32 + partial;
                    let bob = (age / 10.0 + phase).sin() * 0.1 + 0.1;
                    let position = relative + Vec3::Y * bob;
                    let copies = match stack.count {
                        21.. => 4,
                        6.. => 3,
                        2.. => 2,
                        _ => 1,
                    };
                    let mut random = common::JavaRandom::new(187);
                    for i in 0..copies {
                        let mut scatter = |spread: f32| {
                            if i == 0 {
                                Vec3::ZERO
                            } else {
                                Vec3::from_array(std::array::from_fn(|_| {
                                    (random.next_float() * 2.0 - 1.0) * spread
                                }))
                            }
                        };
                        if let Some(block) = stack.item.block()
                            && super::item::in_3d(block)
                        {
                            let rotation = glam::Mat4::from_rotation_y(age / 20.0 + phase);
                            let scale = if block.properties().is_some_and(|p| {
                                p.normal || p.shape == common::world::Shape::PistonBase
                            }) || block == common::world::Block::SLAB
                            {
                                0.25
                            } else {
                                0.5
                            };
                            let transform = glam::Mat4::from_translation(position)
                                * rotation
                                * glam::Mat4::from_scale(Vec3::splat(scale))
                                * glam::Mat4::from_translation(scatter(0.2 / scale));
                            self.block_item(block, stack.damage as u8, transform, brightness);
                        } else {
                            let (atlas, tile) = stack.item.block().map_or_else(
                                || (crate::gui::ITEMS, super::item::icon(stack)),
                                |block| (0, texture::inventory_face(block, stack.damage as u8, 0)),
                            );
                            let rotation = Mat3::from_rotation_y(-player.entity.yaw.to_radians());
                            let offset = scatter(0.3) * 0.5 + position;
                            let points = SPRITE.map(|p| rotation * p + offset);
                            self.quad(points, tile_uv(tile), atlas, gray(brightness));
                        }
                    }
                }
            }
        }
        self.spawner_mobs(world, eye, partial, sky_darkness);
        for particle in &world.particles.particles {
            let position = particle
                .previous
                .lerp(particle.position, f64::from(partial));
            let look = particle.appearance(partial);
            let world_light = brightness_at(world, position, sky_darkness);
            let light = look.light.apply(world_light);
            let center = (position - eye).as_vec3();
            if let Some(alpha) = look.flat_alpha {
                let half = look.size;
                let shade = (world_light * 255.0) as u32;
                self.quad(
                    [
                        center + Vec3::new(-half, 0.0, -half),
                        center + Vec3::new(half, 0.0, -half),
                        center + Vec3::new(half, 0.0, half),
                        center + Vec3::new(-half, 0.0, half),
                    ],
                    look.uv,
                    look.texture,
                    (((alpha * 255.0) as u32) << 24) | (shade * 0x010101),
                );
                continue;
            }
            let color = (particle.color * light * 255.0).as_uvec3();
            let right = billboard.x_axis * look.size;
            let up = billboard.y_axis * look.size;
            self.quad(
                [
                    center - right + up,
                    center + right + up,
                    center + right - up,
                    center - right - up,
                ],
                look.uv,
                look.texture,
                0xff000000 | color.x | (color.y << 8) | (color.z << 16),
            );
        }
        self.objects = 0..self.vertices.len() as u32;
        let start = self.vertices.len() as u32;
        self.fishing_lines(entities, player, partial);
        self.lines = start..self.vertices.len() as u32;
        for (relative, swelling, alpha) in flashes {
            let start = self.vertices.len();
            self.cuboid(
                Vec3::splat(-0.5),
                Vec3::splat(0.5),
                |p| p * swelling + relative,
                [0; 6],
                [1.0; 6],
            );
            let color = ((alpha.clamp(0.0, 1.0) * 255.0) as u32) << 24 | 0xffffff;
            for vertex in &mut self.vertices[start..] {
                vertex.texture = -1;
                vertex.color = color;
            }
        }
        self.vertices.append(&mut self.auras);
        self.flashes = self.lines.end..self.vertices.len() as u32;
        for (position, height, size, opaque) in shadows {
            self.shadow(world, position, eye, (height, size, opaque), sky_darkness);
        }
        self.shadows = self.flashes.end..self.vertices.len() as u32;
        self.outline = self.shadows.end..self.shadows.end;
        self.cracks = self.shadows.end..self.shadows.end;
        if interaction.screen.is_none()
            && let Some(hit) = interaction.shown
        {
            let block = shape::BlockView::block(world, hit.x, hit.y, hit.z);
            let bounds = shape::selection_box(world, block, hit.x, hit.y, hit.z)
                .inflate(DVec3::splat(0.002));
            let min = (bounds.min - eye).as_vec3();
            let max = (bounds.max - eye).as_vec3();
            for axis in 0..3 {
                let other = [(axis + 1) % 3, (axis + 2) % 3];
                for a in [false, true] {
                    for b in [false, true] {
                        let mut start = min;
                        start[other[0]] = if a { max[other[0]] } else { min[other[0]] };
                        start[other[1]] = if b { max[other[1]] } else { min[other[1]] };
                        let mut end = start;
                        end[axis] = max[axis];
                        let direction = (end - start).normalize_or_zero();
                        let width =
                            direction.cross((start + end) * 0.5).normalize_or_zero() * 0.0015;
                        self.quad(
                            [start - width, start + width, end + width, end - width],
                            [0.0; 4],
                            -1,
                            0x66000000,
                        );
                    }
                }
            }
            self.outline.end = self.vertices.len() as u32;
            self.cracks = self.outline.end..self.outline.end;
            let damage = interaction.damage(partial);
            let column = common::world::ChunkPos::containing(hit.x, hit.z);
            if damage > 0.0
                && let Some(input) = world.mesh_input(column, hit.y >> 4, self.fancy, false)
            {
                let tile = 240 + (damage * 10.0).min(9.0) as u8;
                let origin = DVec3::new(
                    f64::from(column.min_block_x()),
                    f64::from(hit.y & !15),
                    f64::from(column.min_block_z()),
                ) - eye;
                let quads = crate::render::mesher::overlay(
                    &input,
                    [hit.x & 15, hit.y & 15, hit.z & 15],
                    tile,
                );
                for quad in &quads {
                    let quad = &quad.vertices;
                    let points = quad.each_ref().map(|vertex| {
                        let position = (origin + vertex.position().as_dvec3()).as_vec3();
                        // `glPolygonOffset(-3, -3)`: drawn just in front of the block
                        position - position.normalize_or_zero() * 0.002
                    });
                    self.quad_uv(points, quad.each_ref().map(|v| v.uv()), 0, 0xffffffff);
                }
                self.cracks.end = self.vertices.len() as u32;
            }
        }
        self.hand = self.vertices.len() as u32..self.vertices.len() as u32;
        if !player.third_person && !player.entity.sleep.sleeping && player.started {
            let brightness = brightness_at(world, eye, sky_darkness);
            if !self.hide_hand && player.entity.living.as_ref().is_some_and(|l| l.health > 0) {
                self.first_person(player, interaction, world, partial, brightness);
            }
            self.hazard_overlays(player, world, brightness);
        }
        self.hand.end = self.vertices.len() as u32;
    }
}