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 mesher block.rs
//! Standard blocks: `renderStandardBlock` with both lighting modes, plus the per-block
//! bounds, textures and tints it relies on.
use common::physics::Aabb;
use common::world::shape::SIDE_OFFSETS;
use common::world::{Block, Material};
use glam::Vec3;
use super::{Builder, Cache, Light, MeshInput, Shade, WHITE, occludes};
const GRASS_SIDE: u8 = 3;
const GRASS_SIDE_OVERLAY: u8 = 38;
/// Block bounds within the unit cube, after `setBlockBoundsBasedOnState`.
#[derive(Clone, Copy)]
pub struct Bounds {
pub min: Vec3,
pub max: Vec3,
}
impl Bounds {
pub const UNIT: Self = Self {
min: Vec3::ZERO,
max: Vec3::ONE,
};
pub fn new(min: [f32; 3], max: [f32; 3]) -> Self {
Self {
min: Vec3::from_array(min),
max: Vec3::from_array(max),
}
}
pub fn with_height(height: f32) -> Self {
Self::new([0.0; 3], [1.0, height, 1.0])
}
/// The corner whose bits 4, 2 and 1 pick the maximum x, y and z.
pub fn corner(&self, bits: u8) -> Vec3 {
Vec3::select(
glam::BVec3::new(bits & 4 != 0, bits & 2 != 0, bits & 1 != 0),
self.max,
self.min,
)
}
pub fn of(aabb: &Aabb) -> Self {
Self {
min: aabb.min.as_vec3(),
max: aabb.max.as_vec3(),
}
}
}
pub fn bounds(block: Block, metadata: u8) -> Bounds {
match block {
Block::SNOW_LAYER => Bounds::with_height(f32::from(2 * (1 + (metadata & 7))) / 16.0),
Block::SLAB => Bounds::with_height(0.5),
Block::FARMLAND => Bounds::with_height(15.0 / 16.0),
_ => Bounds::UNIT,
}
}
/// `Block.getBlockTexture` for the world: the texture of a side, depending on metadata and neighbors.
pub fn texture(cache: &Cache, block: Block, [x, y, z]: [i32; 3], face: usize, fancy: bool) -> u8 {
let metadata = cache.metadata(x, y, z);
match block {
Block::GRASS => match face {
1 => 0,
0 => 2,
_ if matches!(
cache.material(x, y + 1, z),
Material::Snow | Material::BuiltSnow
) =>
{
68
}
_ => GRASS_SIDE,
},
Block::SANDSTONE => match face {
1 => 176,
0 => 208,
_ => 192,
},
Block::LOG => match (face, metadata) {
(0 | 1, _) => 21,
(_, 1) => 116,
(_, 2) => 117,
_ => 20,
},
Block::LEAVES => {
let base = if fancy { 52 } else { 53 };
if metadata & 3 == 1 { base + 80 } else { base }
}
Block::CHEST => chest_texture(cache, [x, y, z], face),
Block::LOCKED_CHEST if face > 1 => {
26 + u8::from(face == single_chest_front(cache, [x, y, z]))
}
Block::CACTUS => match face {
1 => 69,
0 => 71,
_ => 70,
},
Block::SAPLING => match metadata & 3 {
1 => 63,
2 => 79,
_ => 15,
},
Block::TALL_GRASS => match metadata {
0 => 55,
2 => 56,
_ => 39,
},
_ => common::world::texture::face(block, metadata, face).0,
}
}
/// The side of a single chest that shows its lock: away from solid blocks.
fn single_chest_front(cache: &Cache, [x, y, z]: [i32; 3]) -> usize {
let opaque = |dx: i32, dz: i32| cache.block(x + dx, y, z + dz).is_opaque_cube();
match (opaque(0, -1), opaque(0, 1), opaque(-1, 0), opaque(1, 0)) {
(.., false, true) => 4,
(.., true, false) => 5,
(false, true, ..) => 2,
_ => 3,
}
}
/// `BlockChest.getBlockTexture`: the lock faces away from solid blocks, double chests join up.
fn chest_texture(cache: &Cache, [x, y, z]: [i32; 3], face: usize) -> u8 {
const BASE: u8 = 26;
if face <= 1 {
return BASE - 1;
}
let chest = |[dx, dz]: [i32; 2]| cache.block(x + dx, y, z + dz) == Block::CHEST;
let along_z = chest([0, -1]) || chest([0, 1]);
if !along_z && !chest([-1, 0]) && !chest([1, 0]) {
return BASE + u8::from(face == single_chest_front(cache, [x, y, z]));
}
// The other half lies along the join axis, the lock faces across it
let (ends, flipped, fronts, at): (_, _, _, fn(i32, i32) -> [i32; 2]) = if along_z {
([2, 3], 4, [4, 5], |along, across| [across, along])
} else {
([4, 5], 3, [2, 3], |along, across| [along, across])
};
if ends.contains(&face) {
return BASE;
}
let first = chest(at(-1, 0));
let other = if first { -1 } else { 1 };
let mut half = if first { -1 } else { 0 };
if face == flipped {
half = -1 - half;
}
let opaque = |side: i32| {
let opaque = |[dx, dz]: [i32; 2]| cache.block(x + dx, y, z + dz).is_opaque_cube();
opaque(at(0, side)) || opaque(at(other, side))
};
let front = if opaque(1) && !opaque(-1) {
fronts[0]
} else {
fronts[1]
};
let texture = if face == front { BASE + 16 } else { BASE + 32 };
(i32::from(texture) + half) as u8
}
/// `Block.colorMultiplier` as 0xRRGGBB.
pub fn color(input: &MeshInput, cache: &Cache, block: Block, [x, y, z]: [i32; 3]) -> u32 {
let column = (z * 16 + x) as usize;
match block {
Block::GRASS => input.colors().grass[column],
Block::LEAVES => match cache.metadata(x, y, z) {
m if m & 1 == 1 => 0x61_9961,
m if m & 2 == 2 => 0x80_A755,
_ => input.colors().foliage[column],
},
Block::TALL_GRASS if cache.metadata(x, y, z) != 0 => {
let [wx, wy, wz] = cache.world([x, y, z]);
input.biome_colors.tall_grass_at(wx, wy, wz)
}
_ => WHITE,
}
}
/// `Block.shouldSideBeRendered` with its bounds rule and the overrides of standard blocks.
pub fn side_visible(
cache: &Cache,
block: Block,
[x, y, z]: [i32; 3],
face: usize,
bounds: &Bounds,
fancy: bool,
) -> bool {
if cache.all_faces {
return true;
}
let [dx, dy, dz] = SIDE_OFFSETS[face];
let neighbor = cache.block(x + dx, y + dy, z + dz);
if matches!(block, Block::ICE | Block::GLASS) && neighbor == block {
return false;
}
// `BlockPortal.shouldSideBeRendered`: only the two broad faces of the sheet
if block == Block::PORTAL {
let [nx, ny, nz] = [x + dx, y + dy, z + dz];
if neighbor == Block::PORTAL {
return false;
}
let portal = |dx: i32, dz: i32| cache.block(nx + dx, ny, nz + dz) == Block::PORTAL;
let along_x = portal(-1, 0) && !portal(-2, 0) || portal(1, 0) && !portal(2, 0);
let along_z = portal(0, -1) && !portal(0, -2) || portal(0, 1) && !portal(0, 2);
return along_x && matches!(face, 4 | 5) || along_z && matches!(face, 2 | 3);
}
if block == Block::SNOW_LAYER && face == 1 {
return true;
}
if matches!(block, Block::REPEATER | Block::POWERED_REPEATER) && face <= 1 {
return false;
}
let inset = match face {
0 => bounds.min.y > 0.0,
1 => bounds.max.y < 1.0,
2 => bounds.min.z > 0.0,
3 => bounds.max.z < 1.0,
4 => bounds.min.x > 0.0,
_ => bounds.max.x < 1.0,
};
inset || !occludes(neighbor, fancy)
}
pub fn shade(face: usize) -> Shade {
match face {
0 => Shade::Bottom,
1 => Shade::Top,
2 | 3 => Shade::ZSide,
_ => Shade::XSide,
}
}
/// Corners and UVs of `renderBottomFace` through `renderSouthFace`, in the order
/// top left, bottom left, bottom right, top right.
pub fn face_quad(face: usize, bounds: &Bounds, texture: u8) -> [(Vec3, (f32, f32)); 4] {
oriented_face_quad(face, bounds, texture, Orientation::default())
}
/// `RenderBlocks.uvRotate*` (quarter turns of a face's texture) and `flipTexture` (mirrors a
/// side's texture).
#[derive(Clone, Copy, Default)]
pub struct Orientation {
pub rotate: [u8; 6],
pub flip: bool,
}
/// [`face_quad`] with the texture turned and mirrored like the original.
pub fn oriented_face_quad(
face: usize,
bounds: &Bounds,
texture: u8,
orientation: Orientation,
) -> [(Vec3, (f32, f32)); 4] {
let t = f64::from(texture & 15) * 16.0;
let s = f64::from(texture & 0xF0);
let (min, max) = (bounds.min.as_dvec3(), bounds.max.as_dvec3());
let side = face >= 2;
// Along the texture's u axis, then across it
let (h_lo, h_hi) = if face >= 4 {
(min.z, max.z)
} else {
(min.x, max.x)
};
let (w_lo, w_hi) = if side { (min.y, max.y) } else { (min.z, max.z) };
let mut u = ((t + h_lo * 16.0) / 256.0, (t + h_hi * 16.0 - 0.01) / 256.0);
let mut v = if side {
(
(s + 16.0 - w_hi * 16.0) / 256.0,
(s + 16.0 - w_lo * 16.0 - 0.01) / 256.0,
)
} else {
((s + w_lo * 16.0) / 256.0, (s + w_hi * 16.0 - 0.01) / 256.0)
};
if side && orientation.flip {
u = (u.1, u.0);
}
let full = |base: f64| {
(
f64::from(base as f32 / 256.0),
f64::from((base as f32 + 15.99) / 256.0),
)
};
if h_lo < 0.0 || h_hi > 1.0 {
u = full(t);
}
if w_lo < 0.0 || w_hi > 1.0 {
v = full(s);
}
let (mut p, mut q, mut r, mut corner_s) = ((u.0, v.0), (u.1, v.1), (u.1, v.0), (u.0, v.1));
// East and south faces turn their rotated textures the other way around
let reversed = face == 2 || face == 5;
let rotation = match (face, orientation.rotate[face]) {
(_, 3) => 3,
(0 | 2 | 5, 1) | (1 | 3 | 4, 2) => 2,
(_, 0) => 0,
_ => 1,
};
match rotation {
1 => {
let (a, b) = ((t + w_lo * 16.0) / 256.0, (t + w_hi * 16.0) / 256.0);
let (mut c, mut d) = (
(s + 16.0 - h_hi * 16.0) / 256.0,
(s + 16.0 - h_lo * 16.0) / 256.0,
);
if reversed {
(c, d) = (d, c);
}
(p, q, r, corner_s) = ((a, d), (b, c), (a, c), (b, d));
}
2 => {
let (a, b) = (
(t + 16.0 - w_hi * 16.0) / 256.0,
(t + 16.0 - w_lo * 16.0) / 256.0,
);
let (mut c, mut d) = ((s + h_lo * 16.0) / 256.0, (s + h_hi * 16.0) / 256.0);
if reversed {
(c, d) = (d, c);
}
(p, q, r, corner_s) = ((b, c), (a, d), (b, d), (a, c));
}
3 => {
let (a, b) = (
(t + 16.0 - h_lo * 16.0) / 256.0,
(t + 16.0 - h_hi * 16.0 - 0.01) / 256.0,
);
let (c, d) = if side {
((s + w_hi * 16.0) / 256.0, (s + w_lo * 16.0 - 0.01) / 256.0)
} else {
(
(s + 16.0 - w_lo * 16.0) / 256.0,
(s + 16.0 - w_hi * 16.0 - 0.01) / 256.0,
)
};
(p, q, r, corner_s) = ((a, c), (b, d), (b, c), (a, d));
}
_ => {}
}
let uv = |(u, v): (f64, f64)| (u as f32, v as f32);
let (p, q, r, s) = (uv(p), uv(q), uv(r), uv(corner_s));
let uvs = [p, q, r, s];
QUAD_CORNERS[face].map(|(corner, uv)| (bounds.corner(corner), uvs[uv]))
}
/// Per face, the corners (bits 4, 2 and 1 pick the maximum x, y and z) and their UVs (p, q, r
/// and s) of `renderBottomFace` through `renderSouthFace`.
const QUAD_CORNERS: [[(u8, usize); 4]; 6] = [
[(0b001, 3), (0b000, 0), (0b100, 2), (0b101, 1)],
[(0b111, 1), (0b110, 2), (0b010, 0), (0b011, 3)],
[(0b010, 2), (0b110, 0), (0b100, 3), (0b000, 1)],
[(0b011, 0), (0b001, 3), (0b101, 1), (0b111, 2)],
[(0b011, 2), (0b010, 0), (0b000, 3), (0b001, 1)],
[(0b101, 3), (0b100, 1), (0b110, 2), (0b111, 0)],
];
/// `renderStandardBlock`.
pub fn standard(
input: &MeshInput,
cache: &Cache,
builder: &mut Builder,
block: Block,
position: [i32; 3],
) {
let [x, y, z] = position;
let shape = common::world::shape::block_bounds(cache, block, x, y, z);
let bounds = if matches!(
block,
Block::CAKE
| Block::PORTAL
| Block::BUTTON
| Block::TRAPDOOR
| Block::STONE_PRESSURE_PLATE
| Block::WOODEN_PRESSURE_PLATE
) {
Bounds::of(&shape)
} else {
bounds(block, cache.metadata(x, y, z))
};
standard_bounds(input, cache, builder, block, position, bounds);
}
pub fn standard_bounds(
input: &MeshInput,
cache: &Cache,
builder: &mut Builder,
block: Block,
position: [i32; 3],
bounds: Bounds,
) {
standard_oriented(input, cache, builder, block, position, bounds, [0; 6]);
}
/// [`standard_bounds`] with each face's texture turned by `rotate` quarter turns.
pub fn standard_oriented(
input: &MeshInput,
cache: &Cache,
builder: &mut Builder,
block: Block,
position: [i32; 3],
bounds: Bounds,
rotate: [u8; 6],
) {
let [x, y, z] = position;
let rgb = color(input, cache, block, position);
let emission = block.light_emission();
let own = cache.light(x, y, z).with_emission(emission);
// `renderBlockDoor` always lights each face flat
let door = matches!(block, Block::WOODEN_DOOR | Block::IRON_DOOR);
for (face, [dx, dy, dz]) in SIDE_OFFSETS.into_iter().enumerate() {
if !side_visible(cache, block, position, face, &bounds, input.fancy) {
continue;
}
let texture = texture(cache, block, position, face, input.fancy);
let flip = common::world::texture::face(block, cache.metadata(x, y, z), face).1;
let quad = oriented_face_quad(face, &bounds, texture, Orientation { rotate, flip });
let face_rgb = if face == 1 || block != Block::GRASS {
rgb
} else {
WHITE
};
let light = if input.smooth_lighting && !door {
smooth_light(cache, position, face, emission)
} else {
let inset = match face {
0 => door && bounds.min.y > 0.0,
1 => bounds.max.y != 1.0 && !block.material().is_liquid(),
2 => bounds.min.z > 0.0,
3 => bounds.max.z < 1.0,
4 => bounds.min.x > 0.0,
_ => bounds.max.x < 1.0,
};
let flat = if inset {
own
} else {
cache.light(x + dx, y + dy, z + dz).with_emission(emission)
};
[[flat; 4]; 4]
};
builder.smooth_quad(quad, light, shade(face), face_rgb);
if input.fancy && texture == GRASS_SIDE {
builder.mark_cutout();
let overlay = face_quad(face, &bounds, GRASS_SIDE_OVERLAY);
builder.smooth_quad(overlay, light, shade(face), rgb);
}
}
}
// ---- Smooth lighting ----------------------------------------
type Offset = [i32; 3];
/// A light sample of `renderStandardBlockWithAmbientOcclusion`. A diagonal corner is only
/// sampled if one of its two edge blocks lets light through, otherwise an edge stands in.
#[derive(Clone, Copy)]
enum Sample {
At(Offset),
Corner {
at: Offset,
edges: [Offset; 2],
fallback: Offset,
},
}
const fn at(x: i32, y: i32, z: i32) -> Sample {
Sample::At([x, y, z])
}
const fn corner(at: Offset, a: Offset, b: Offset, fallback: Offset) -> Sample {
Sample::Corner {
at,
edges: [a, b],
fallback,
}
}
/// Per face, the samples averaged at each corner, in the original summation order.
const SMOOTH: [[[Sample; 4]; 4]; 6] = {
let xynn = at(-1, -1, 0);
let yznn = at(0, -1, -1);
let yznp = at(0, -1, 1);
let xypn = at(1, -1, 0);
let c = at(0, -1, 0);
let nnn = corner([-1, -1, -1], [0, -1, -1], [-1, -1, 0], [-1, -1, 0]);
let nnp = corner([-1, -1, 1], [0, -1, 1], [-1, -1, 0], [-1, -1, 0]);
let pnn = corner([1, -1, -1], [0, -1, -1], [1, -1, 0], [1, -1, 0]);
let pnp = corner([1, -1, 1], [0, -1, 1], [1, -1, 0], [1, -1, 0]);
let bottom = [
[nnp, xynn, yznp, c],
[xynn, nnn, c, yznn],
[c, yznn, xypn, pnn],
[yznp, c, pnp, xypn],
];
let xynp = at(-1, 1, 0);
let xypp = at(1, 1, 0);
let yzpn = at(0, 1, -1);
let yzpp = at(0, 1, 1);
let c = at(0, 1, 0);
let npn = corner([-1, 1, -1], [0, 1, -1], [-1, 1, 0], [-1, 1, 0]);
let ppn = corner([1, 1, -1], [0, 1, -1], [1, 1, 0], [1, 1, 0]);
let npp = corner([-1, 1, 1], [0, 1, 1], [-1, 1, 0], [-1, 1, 0]);
let ppp = corner([1, 1, 1], [0, 1, 1], [1, 1, 0], [1, 1, 0]);
let top = [
[yzpp, c, ppp, xypp],
[c, yzpn, xypp, ppn],
[xynp, npn, c, yzpn],
[npp, xynp, yzpp, c],
];
let xznn = at(-1, 0, -1);
let yznn = at(0, -1, -1);
let yzpn = at(0, 1, -1);
let xzpn = at(1, 0, -1);
let c = at(0, 0, -1);
let nnn = corner([-1, -1, -1], [-1, 0, -1], [0, -1, -1], [-1, 0, -1]);
let npn = corner([-1, 1, -1], [-1, 0, -1], [0, 1, -1], [-1, 0, -1]);
let pnn = corner([1, -1, -1], [1, 0, -1], [0, -1, -1], [1, 0, -1]);
let ppn = corner([1, 1, -1], [1, 0, -1], [0, 1, -1], [1, 0, -1]);
let north = [
[xznn, npn, c, yzpn],
[c, yzpn, xzpn, ppn],
[yznn, c, pnn, xzpn],
[nnn, xznn, yznn, c],
];
let xznp = at(-1, 0, 1);
let xzpp = at(1, 0, 1);
let yznp = at(0, -1, 1);
let yzpp = at(0, 1, 1);
let c = at(0, 0, 1);
let nnp = corner([-1, -1, 1], [-1, 0, 1], [0, -1, 1], [-1, 0, 1]);
let npp = corner([-1, 1, 1], [-1, 0, 1], [0, 1, 1], [-1, 0, 1]);
let pnp = corner([1, -1, 1], [1, 0, 1], [0, -1, 1], [1, 0, 1]);
let ppp = corner([1, 1, 1], [1, 0, 1], [0, 1, 1], [1, 0, 1]);
let south = [
[xznp, npp, c, yzpp],
[nnp, xznp, yznp, c],
[yznp, c, pnp, xzpp],
[c, yzpp, xzpp, ppp],
];
let xynn = at(-1, -1, 0);
let xznn = at(-1, 0, -1);
let xznp = at(-1, 0, 1);
let xynp = at(-1, 1, 0);
let c = at(-1, 0, 0);
let nnn = corner([-1, -1, -1], [-1, 0, -1], [-1, -1, 0], [-1, 0, -1]);
let nnp = corner([-1, -1, 1], [-1, 0, 1], [-1, -1, 0], [-1, 0, 1]);
let npn = corner([-1, 1, -1], [-1, 0, -1], [-1, 1, 0], [-1, 0, -1]);
let npp = corner([-1, 1, 1], [-1, 0, 1], [-1, 1, 0], [-1, 0, 1]);
let west = [
[c, xznp, xynp, npp],
[xznn, c, npn, xynp],
[nnn, xynn, xznn, c],
[xynn, nnp, c, xznp],
];
let xypn = at(1, -1, 0);
let xzpn = at(1, 0, -1);
let xzpp = at(1, 0, 1);
let xypp = at(1, 1, 0);
let c = at(1, 0, 0);
let pnn = corner([1, -1, -1], [1, -1, 0], [1, 0, -1], [1, 0, -1]);
let pnp = corner([1, -1, 1], [1, -1, 0], [1, 0, 1], [1, 0, 1]);
let ppn = corner([1, 1, -1], [1, 1, 0], [1, 0, -1], [1, 0, -1]);
let ppp = corner([1, 1, 1], [1, 1, 0], [1, 0, 1], [1, 0, 1]);
let east = [
[xypn, pnp, c, xzpp],
[pnn, xypn, xzpn, c],
[xzpn, c, ppn, xypp],
[c, xzpp, xypp, ppp],
];
[bottom, top, north, south, west, east]
};
fn smooth_light(cache: &Cache, [x, y, z]: [i32; 3], face: usize, emission: u8) -> [[Light; 4]; 4] {
let light = |[dx, dy, dz]: Offset| cache.light(x + dx, y + dy, z + dz).with_emission(emission);
let passes_light = &super::TABLES.passes_light;
let open =
|[dx, dy, dz]: Offset| passes_light[usize::from(cache.block(x + dx, y + dy, z + dz).0)];
SMOOTH[face].map(|samples| {
samples.map(|sample| match sample {
Sample::At(offset) => light(offset),
Sample::Corner {
at,
edges: [a, b],
fallback,
} => light(if open(a) || open(b) { at } else { fallback }),
})
})
}