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 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, ®ion, &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
}