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 / common / src / world / shape.rs

//! Block shapes: the bounds of `setBlockBounds`/`setBlockBoundsBasedOnState`, collision boxes,
//! selection boxes and ray tracing against blocks.

use glam::DVec3;

use super::{Block, TileEntity};
use crate::physics::{Aabb, BlockHit, nearest_face};

/// Read access to blocks for shape queries.
pub trait BlockView {
    fn block(&self, x: i32, y: i32, z: i32) -> Block;
    fn metadata(&self, x: i32, y: i32, z: i32) -> u8;
    fn tile_entity(&self, _x: i32, _y: i32, _z: i32) -> Option<&TileEntity> {
        None
    }
}

/// A view with `block` already at `position`, for the shape of a block about to be placed.
struct Placed<'a, V> {
    view: &'a V,
    position: [i32; 3],
    block: Block,
    metadata: u8,
}

impl<V: BlockView> BlockView for Placed<'_, V> {
    fn block(&self, x: i32, y: i32, z: i32) -> Block {
        if [x, y, z] == self.position {
            self.block
        } else {
            self.view.block(x, y, z)
        }
    }

    fn metadata(&self, x: i32, y: i32, z: i32) -> u8 {
        if [x, y, z] == self.position {
            self.metadata
        } else {
            self.view.metadata(x, y, z)
        }
    }
}

/// The collision boxes `block` would have at `position`, which entities must stay out of
/// (`World.canBlockBePlacedAt`).
pub fn placement_boxes(
    view: &impl BlockView,
    block: Block,
    metadata: u8,
    position @ [x, y, z]: [i32; 3],
) -> Vec<Aabb> {
    let placed = Placed {
        view,
        position,
        block,
        metadata,
    };
    let mut boxes = Vec::new();
    let region = selection_box(&placed, block, x, y, z);
    collision_boxes(&placed, block, x, y, z, &region, &mut boxes);
    boxes
}

/// `PistonBlockTextures.offsets*ForSide`: the unit step towards a side.
pub const SIDE_OFFSETS: [[i32; 3]; 6] = [
    [0, -1, 0],
    [0, 1, 0],
    [0, 0, -1],
    [0, 0, 1],
    [-1, 0, 0],
    [1, 0, 0],
];

/// The block a moving piston carries and how far it still is from its destination, as the
/// offset towards its facing (`getProgress(0)`, counted backwards while extending).
fn moving_piston(view: &impl BlockView, x: i32, y: i32, z: i32) -> Option<(Block, DVec3)> {
    let Some(&TileEntity::Piston {
        block,
        facing,
        last_progress,
        extending,
        ..
    }) = view.tile_entity(x, y, z)
    else {
        return None;
    };
    let block = Block(block as u8);
    if block.is_air() || block == Block::MOVING_PISTON {
        return None;
    }
    let remaining = if extending {
        1.0 - last_progress
    } else {
        last_progress
    };
    Some((block, piston_offset(facing as usize, remaining)))
}

fn piston_offset(facing: usize, remaining: f32) -> DVec3 {
    DVec3::from_array(SIDE_OFFSETS[facing.min(5)].map(|d| f64::from(d as f32 * remaining)))
}

/// `BlockPistonMoving.getAxisAlignedBB`: the collision box of `block` shifted back along the
/// piston's facing by `remaining`.
pub fn moving_collision_box(
    view: &impl BlockView,
    block: Block,
    x: i32,
    y: i32,
    z: i32,
    facing: u8,
    remaining: f32,
) -> Option<Aabb> {
    if block.is_air() || block == Block::MOVING_PISTON {
        return None;
    }
    let offset = piston_offset(usize::from(facing), remaining);
    collision_box(view, block, x, y, z).map(|b| b.offset(-offset))
}

/// Bounds from float constants, widened like the original's `float` to `double` fields.
fn bounds(min: [f32; 3], max: [f32; 3]) -> Aabb {
    Aabb::new(
        DVec3::from_array(min.map(f64::from)),
        DVec3::from_array(max.map(f64::from)),
    )
}

const FULL: Aabb = Aabb::new(DVec3::ZERO, DVec3::ONE);

fn height(h: f32) -> Aabb {
    bounds([0.0; 3], [1.0, h, 1.0])
}

fn centered(half_width: f32, h: f32) -> Aabb {
    bounds(
        [0.5 - half_width, 0.0, 0.5 - half_width],
        [0.5 + half_width, h, 0.5 + half_width],
    )
}

/// A thin slab against one side, for ladders and wall signs: metadata 2 to 5 face
/// south, north, east and west.
fn against_side(meta: u8, thickness: f32, low: f32, high: f32) -> Option<Aabb> {
    Some(match meta {
        2 => bounds([0.0, low, 1.0 - thickness], [1.0, high, 1.0]),
        3 => bounds([0.0, low, 0.0], [1.0, high, thickness]),
        4 => bounds([1.0 - thickness, low, 0.0], [1.0, high, 1.0]),
        5 => bounds([0.0, low, 0.0], [thickness, high, 1.0]),
        _ => return None,
    })
}

/// `BlockDoor.getState`: the side the door panel is on.
pub fn door_state(metadata: u8) -> u8 {
    if metadata & 4 == 0 {
        metadata.wrapping_sub(1) & 3
    } else {
        metadata & 3
    }
}

/// The bounds a block has at a position, in block-local coordinates.
pub fn block_bounds(view: &impl BlockView, block: Block, x: i32, y: i32, z: i32) -> Aabb {
    let meta = || view.metadata(x, y, z);
    match block {
        Block::DANDELION | Block::ROSE => centered(0.2, 0.6),
        Block::SAPLING | Block::TALL_GRASS | Block::DEAD_BUSH => centered(0.4, 0.8),
        Block::BROWN_MUSHROOM | Block::RED_MUSHROOM => centered(0.2, 0.4),
        Block::WHEAT => centered(0.5, 0.25),
        Block::SUGAR_CANE => centered(6.0 / 16.0, 1.0),
        Block::FARMLAND => height(15.0 / 16.0),
        Block::SLAB => height(0.5),
        Block::BED => height(9.0 / 16.0),
        Block::REPEATER | Block::POWERED_REPEATER => height(2.0 / 16.0),
        Block::RAIL | Block::POWERED_RAIL | Block::DETECTOR_RAIL => {
            height(if (2..=5).contains(&meta()) { 10.0 } else { 2.0 } / 16.0)
        }
        Block::REDSTONE_WIRE => height(1.0 / 16.0),
        Block::SNOW_LAYER => height(f32::from(2 * (1 + (meta() & 7))) / 16.0),
        Block::STONE_PRESSURE_PLATE | Block::WOODEN_PRESSURE_PLATE => {
            let h = if meta() == 1 { 0.03125 } else { 1.0 / 16.0 };
            bounds([PIXEL, 0.0, PIXEL], [1.0 - PIXEL, h, 1.0 - PIXEL])
        }
        Block::CAKE => bounds(
            [cake_bites(meta()), 0.0, PIXEL],
            [1.0 - PIXEL, 0.5, 1.0 - PIXEL],
        ),
        Block::SIGN => centered(0.25, 1.0),
        Block::WALL_SIGN => {
            against_side(meta(), 2.0 / 16.0, 9.0 / 32.0, 25.0 / 32.0).unwrap_or(FULL)
        }
        Block::LADDER => against_side(meta(), 2.0 / 16.0, 0.0, 1.0).unwrap_or(FULL),
        Block::BUTTON => {
            let meta = meta();
            let depth = if meta & 8 != 0 { 1.0 } else { 2.0 } / 16.0;
            let (low, high, half) = (6.0 / 16.0, 10.0 / 16.0, 3.0 / 16.0);
            match meta & 7 {
                1 => bounds([0.0, low, 0.5 - half], [depth, high, 0.5 + half]),
                2 => bounds([1.0 - depth, low, 0.5 - half], [1.0, high, 0.5 + half]),
                3 => bounds([0.5 - half, low, 0.0], [0.5 + half, high, depth]),
                4 => bounds([0.5 - half, low, 1.0 - depth], [0.5 + half, high, 1.0]),
                _ => FULL,
            }
        }
        Block::LEVER | Block::TORCH | Block::REDSTONE_TORCH | Block::UNLIT_REDSTONE_TORCH => {
            let (w, standing) = if block == Block::LEVER {
                (3.0 / 16.0, centered(0.25, 0.6))
            } else {
                (0.15, centered(0.1, 0.6))
            };
            match meta() & 7 {
                1 => bounds([0.0, 0.2, 0.5 - w], [w * 2.0, 0.8, 0.5 + w]),
                2 => bounds([1.0 - w * 2.0, 0.2, 0.5 - w], [1.0, 0.8, 0.5 + w]),
                3 => bounds([0.5 - w, 0.2, 0.0], [0.5 + w, 0.8, w * 2.0]),
                4 => bounds([0.5 - w, 0.2, 1.0 - w * 2.0], [0.5 + w, 0.8, 1.0]),
                _ => standing,
            }
        }
        Block::WOODEN_DOOR | Block::IRON_DOOR => {
            let t = 3.0 / 16.0;
            match door_state(meta()) {
                0 => bounds([0.0; 3], [1.0, 1.0, t]),
                1 => bounds([1.0 - t, 0.0, 0.0], [1.0; 3]),
                2 => bounds([0.0, 0.0, 1.0 - t], [1.0; 3]),
                _ => bounds([0.0; 3], [t, 1.0, 1.0]),
            }
        }
        Block::TRAPDOOR => {
            let meta = meta();
            let t = 3.0 / 16.0;
            if meta & 4 == 0 {
                height(t)
            } else {
                match meta & 3 {
                    0 => bounds([0.0, 0.0, 1.0 - t], [1.0; 3]),
                    1 => bounds([0.0; 3], [1.0, 1.0, t]),
                    2 => bounds([1.0 - t, 0.0, 0.0], [1.0; 3]),
                    _ => bounds([0.0; 3], [t, 1.0, 1.0]),
                }
            }
        }
        Block::PORTAL => {
            let along_x = view.block(x - 1, y, z) == Block::PORTAL
                || view.block(x + 1, y, z) == Block::PORTAL;
            let (wx, wz) = if along_x {
                (0.5, 2.0 / 16.0)
            } else {
                (2.0 / 16.0, 0.5)
            };
            bounds([0.5 - wx, 0.0, 0.5 - wz], [0.5 + wx, 1.0, 0.5 + wz])
        }
        Block::PISTON | Block::STICKY_PISTON => {
            let meta = meta();
            if meta & 8 == 0 {
                FULL
            } else {
                piston_side(meta & 7, 0.25)
            }
        }
        // The plate is on the side it faces
        Block::PISTON_HEAD => piston_side(meta() & 7 ^ 1, 0.75),
        Block::MOVING_PISTON => match moving_piston(view, x, y, z) {
            Some((stored, offset)) => block_bounds(view, stored, x, y, z).offset(-offset),
            None => FULL,
        },
        _ => FULL,
    }
}

/// Full block bounds without the part facing `facing`, cut at `cut` (the base of an extended
/// piston), or only that part (a piston head, with `cut` 0.75).
fn piston_side(facing: u8, cut: f32) -> Aabb {
    let (lo, hi) = (cut, 1.0 - cut);
    match facing {
        0 => bounds([0.0, lo, 0.0], [1.0; 3]),
        1 => bounds([0.0; 3], [1.0, hi, 1.0]),
        2 => bounds([0.0, 0.0, lo], [1.0; 3]),
        3 => bounds([0.0; 3], [1.0, 1.0, hi]),
        4 => bounds([lo, 0.0, 0.0], [1.0; 3]),
        _ => bounds([0.0; 3], [hi, 1.0, 1.0]),
    }
}

/// A sixteenth of a block.
const PIXEL: f32 = 1.0 / 16.0;

/// The x where a cake with these bites eaten starts.
fn cake_bites(metadata: u8) -> f32 {
    f32::from(1 + metadata * 2) / 16.0
}

/// A cactus or cake box in world coordinates, computed in `float` like the original.
fn inset(x: i32, y: i32, z: i32, west: f32, top: f64) -> Aabb {
    Aabb::from_coords(
        f64::from(x as f32 + west),
        f64::from(y),
        f64::from(z as f32 + PIXEL),
        f64::from((x + 1) as f32 - PIXEL),
        top,
        f64::from((z + 1) as f32 - PIXEL),
    )
}

/// `getCollisionBoundingBoxFromPool`: the box an entity collides with, in world coordinates.
pub fn collision_box(view: &impl BlockView, block: Block, x: i32, y: i32, z: i32) -> Option<Aabb> {
    let origin = DVec3::new(f64::from(x), f64::from(y), f64::from(z));
    let world = |b: Aabb| Some(b.offset(origin));
    let world_top = |top: f64| {
        Some(Aabb::new(
            origin,
            DVec3::new(origin.x + 1.0, top, origin.z + 1.0),
        ))
    };
    match block {
        Block::AIR
        | Block::FLOWING_WATER
        | Block::WATER
        | Block::FLOWING_LAVA
        | Block::LAVA
        | Block::SAPLING
        | Block::TALL_GRASS
        | Block::DEAD_BUSH
        | Block::DANDELION
        | Block::ROSE
        | Block::BROWN_MUSHROOM
        | Block::RED_MUSHROOM
        | Block::WHEAT
        | Block::SUGAR_CANE
        | Block::FIRE
        | Block::TORCH
        | Block::REDSTONE_TORCH
        | Block::UNLIT_REDSTONE_TORCH
        | Block::REDSTONE_WIRE
        | Block::RAIL
        | Block::POWERED_RAIL
        | Block::DETECTOR_RAIL
        | Block::LEVER
        | Block::BUTTON
        | Block::STONE_PRESSURE_PLATE
        | Block::WOODEN_PRESSURE_PLATE
        | Block::SIGN
        | Block::WALL_SIGN
        | Block::PORTAL
        | Block::WEB => None,
        Block::MOVING_PISTON => {
            let (stored, offset) = moving_piston(view, x, y, z)?;
            collision_box(view, stored, x, y, z).map(|b| b.offset(-offset))
        }
        Block::SNOW_LAYER if view.metadata(x, y, z) & 7 < 3 => None,
        Block::SNOW_LAYER => world_top(f64::from(y as f32 + 0.5)),
        Block::FENCE => world_top(f64::from(y as f32 + 1.5)),
        Block::SOUL_SAND => world_top(f64::from((y + 1) as f32 - 0.125)),
        Block::CACTUS => Some(inset(x, y, z, PIXEL, f64::from((y + 1) as f32 - PIXEL))),
        Block::CAKE => {
            let top = f64::from(y as f32 + 0.5 - PIXEL);
            Some(inset(x, y, z, cake_bites(view.metadata(x, y, z)), top))
        }
        Block::FARMLAND
        | Block::PISTON
        | Block::STICKY_PISTON
        | Block::OAK_STAIRS
        | Block::COBBLESTONE_STAIRS => world(FULL),
        _ => world(block_bounds(view, block, x, y, z)),
    }
}

/// `getCollidingBoundingBoxes`: collision boxes of a block that intersect `region`.
pub fn collision_boxes(
    view: &impl BlockView,
    block: Block,
    x: i32,
    y: i32,
    z: i32,
    region: &Aabb,
    out: &mut Vec<Aabb>,
) {
    let origin = DVec3::new(f64::from(x), f64::from(y), f64::from(z));
    let mut add = |b: Aabb| {
        let b = b.offset(origin);
        if region.intersects(&b) {
            out.push(b);
        }
    };
    match block {
        Block::OAK_STAIRS | Block::COBBLESTONE_STAIRS => {
            let (lower, upper) = match view.metadata(x, y, z) {
                0 => (
                    bounds([0.0; 3], [0.5, 0.5, 1.0]),
                    bounds([0.5, 0.0, 0.0], [1.0; 3]),
                ),
                1 => (
                    bounds([0.0; 3], [0.5, 1.0, 1.0]),
                    bounds([0.5, 0.0, 0.0], [1.0, 0.5, 1.0]),
                ),
                2 => (
                    bounds([0.0; 3], [1.0, 0.5, 0.5]),
                    bounds([0.0, 0.0, 0.5], [1.0; 3]),
                ),
                3 => (
                    bounds([0.0; 3], [1.0, 1.0, 0.5]),
                    bounds([0.0, 0.0, 0.5], [1.0, 0.5, 1.0]),
                ),
                _ => return,
            };
            add(lower);
            add(upper);
        }
        Block::PISTON_HEAD => {
            let (plate, rod) = match view.metadata(x, y, z) & 7 {
                0 => (
                    height(0.25),
                    bounds(
                        [6.0 / 16.0, 0.25, 6.0 / 16.0],
                        [10.0 / 16.0, 1.0, 10.0 / 16.0],
                    ),
                ),
                1 => (
                    bounds([0.0, 0.75, 0.0], [1.0; 3]),
                    bounds(
                        [6.0 / 16.0, 0.0, 6.0 / 16.0],
                        [10.0 / 16.0, 0.75, 10.0 / 16.0],
                    ),
                ),
                2 => (
                    bounds([0.0; 3], [1.0, 1.0, 0.25]),
                    bounds([0.25, 6.0 / 16.0, 0.25], [0.75, 10.0 / 16.0, 1.0]),
                ),
                3 => (
                    bounds([0.0, 0.0, 0.75], [1.0; 3]),
                    bounds([0.25, 6.0 / 16.0, 0.0], [0.75, 10.0 / 16.0, 0.75]),
                ),
                4 => (
                    bounds([0.0; 3], [0.25, 1.0, 1.0]),
                    bounds([6.0 / 16.0, 0.25, 0.25], [10.0 / 16.0, 0.75, 1.0]),
                ),
                5 => (
                    bounds([0.75, 0.0, 0.0], [1.0; 3]),
                    bounds([0.0, 6.0 / 16.0, 0.25], [0.75, 10.0 / 16.0, 0.75]),
                ),
                _ => return,
            };
            add(plate);
            add(rod);
        }
        _ => {
            if let Some(b) = collision_box(view, block, x, y, z)
                && region.intersects(&b)
            {
                out.push(b);
            }
        }
    }
}

/// `getSelectedBoundingBoxFromPool`: the outline drawn around the targeted block.
pub fn selection_box(view: &impl BlockView, block: Block, x: i32, y: i32, z: i32) -> Aabb {
    let origin = DVec3::new(f64::from(x), f64::from(y), f64::from(z));
    match block {
        Block::CACTUS => inset(x, y, z, PIXEL, origin.y + 1.0),
        Block::CAKE => {
            let top = f64::from(y as f32 + 0.5);
            inset(x, y, z, cake_bites(view.metadata(x, y, z)), top)
        }
        _ => block_bounds(view, block, x, y, z).offset(origin),
    }
}

/// `Block.canCollideCheck`: whether a ray stops at the block.
fn ray_stops(block: Block, meta: u8, liquids: bool) -> bool {
    match block {
        Block::FIRE => false,
        _ if block.material().is_liquid() => liquids && meta == 0,
        _ => true,
    }
}

/// `Block.collisionRayTrace`.
pub fn ray_trace_block(
    view: &impl BlockView,
    block: Block,
    x: i32,
    y: i32,
    z: i32,
    start: DVec3,
    end: DVec3,
) -> Option<BlockHit> {
    let b = block_bounds(view, block, x, y, z);
    let origin = DVec3::new(f64::from(x), f64::from(y), f64::from(z));
    let (point, side) = nearest_face(start - origin, end - origin, b.min, b.max)?;
    Some(BlockHit {
        x,
        y,
        z,
        side,
        point: point + origin,
    })
}

/// `World.rayTraceBlocks_do_do`: walks the blocks along a segment, at most 200 steps.
pub fn ray_trace(
    view: &impl BlockView,
    start: DVec3,
    end: DVec3,
    liquids: bool,
    solid_only: bool,
) -> Option<BlockHit> {
    if start.is_nan() || end.is_nan() {
        return None;
    }
    let floor = |v: f64| crate::math::floor_f64(v);
    let target = [floor(end.x), floor(end.y), floor(end.z)];
    let mut cell = [floor(start.x), floor(start.y), floor(start.z)];
    let mut pos = start;
    let test = |cell: [i32; 3], pos: DVec3| {
        let [x, y, z] = cell;
        let block = view.block(x, y, z);
        let meta = view.metadata(x, y, z);
        let collides =
            !solid_only || block.is_air() || collision_box(view, block, x, y, z).is_some();
        (collides && !block.is_air() && ray_stops(block, meta, liquids))
            .then(|| ray_trace_block(view, block, x, y, z, pos, end))
            .flatten()
    };
    if let Some(hit) = test(cell, pos) {
        return Some(hit);
    }
    for _ in 0..=200 {
        if pos.is_nan() || cell == target {
            return None;
        }
        let mut boundary = [999.0; 3];
        let mut stepping = [true; 3];
        for axis in 0..3 {
            if target[axis] > cell[axis] {
                boundary[axis] = f64::from(cell[axis]) + 1.0;
            } else if target[axis] < cell[axis] {
                boundary[axis] = f64::from(cell[axis]);
            } else {
                stepping[axis] = false;
            }
        }
        let d = end - pos;
        let t: [f64; 3] = std::array::from_fn(|axis| {
            if stepping[axis] {
                (boundary[axis] - pos[axis]) / d[axis]
            } else {
                999.0
            }
        });
        let face = if t[0] < t[1] && t[0] < t[2] {
            pos = DVec3::new(boundary[0], pos.y + d.y * t[0], pos.z + d.z * t[0]);
            if target[0] > cell[0] { 4 } else { 5 }
        } else if t[1] < t[2] {
            pos = DVec3::new(pos.x + d.x * t[1], boundary[1], pos.z + d.z * t[1]);
            if target[1] > cell[1] { 0 } else { 1 }
        } else {
            pos = DVec3::new(pos.x + d.x * t[2], pos.y + d.y * t[2], boundary[2]);
            if target[2] > cell[2] { 2 } else { 3 }
        };
        cell = [floor(pos.x), floor(pos.y), floor(pos.z)];
        match face {
            5 => cell[0] -= 1,
            1 => cell[1] -= 1,
            3 => cell[2] -= 1,
            _ => {}
        }
        if let Some(hit) = test(cell, pos) {
            return Some(hit);
        }
    }
    None
}