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 mod.rs
//! Builds section meshes, porting the parts of `RenderBlocks` that terrain needs.
mod block;
mod devices;
mod fluid;
mod pack;
mod plants;
mod special;
pub use pack::{GROUPS, MAX_QUADS, SectionMesh, set_atlas};
use std::sync::{Arc, LazyLock, OnceLock};
use bytemuck::{Pod, Zeroable};
use common::world::chunk::{HEIGHT, index};
use common::world::{Block, Chunk, Layer, Material, Shape};
use glam::Vec3;
use crate::world::{BiomeColors, ColumnColors};
const SIZE: i32 = 16;
const BORDER: i32 = 2;
pub(crate) const PADDED: usize = SIZE as usize + 2 * BORDER as usize;
/// A quad corner with its section-relative position. [`pack`] splits quads into the shapes,
/// lights and tints `terrain.slang` reads.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug, Default)]
pub struct Vertex {
xy: u32,
z_shade: u32,
uv: u32,
tint: u32,
/// Four light samples whose brightness the shader averages, for smooth lighting.
light: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
struct Light {
sky: u8,
block: u8,
}
impl Light {
const SKY: Self = Self { sky: 15, block: 0 };
fn max(self, other: Self) -> Self {
Self {
sky: self.sky.max(other.sky),
block: self.block.max(other.block),
}
}
/// `World.getBrightness` with a block's own light value as the minimum.
fn with_emission(self, emission: u8) -> Self {
Self {
block: self.block.max(emission),
..self
}
}
fn packed(self) -> u32 {
u32::from(self.sky) | u32::from(self.block) << 4
}
}
/// Face shading factors from `renderStandardBlockWithColorMultiplier`.
#[derive(Clone, Copy)]
enum Shade {
Top = 0,
ZSide = 1,
XSide = 2,
Bottom = 3,
/// Full brightness halfway blended in, for the glowing torches of powered repeaters.
Glow = 4,
}
const WHITE: u32 = 0xFF_FFFF;
/// A section and its lighting border, copied out of the 3×3 chunk neighborhood
pub struct MeshInput {
/// Chunks ordered by `(dz + 1) * 3 + (dx + 1)`.
pub chunks: [Arc<Chunk>; 9],
/// Tints of the center chunk's columns, see [`Self::colors`].
pub colors: Arc<OnceLock<ColumnColors>>,
pub biome_colors: Arc<BiomeColors>,
pub section_y: i32,
pub fancy: bool,
pub smooth_lighting: bool,
}
impl MeshInput {
/// Tints of the center chunk's columns, sampled by the first section that needs them.
fn colors(&self) -> &ColumnColors {
self.colors
.get_or_init(|| self.biome_colors.column(self.chunks[4].pos))
}
}
#[derive(Clone, Copy, Debug)]
pub struct Quad {
pub vertices: [Vertex; 4],
/// The block within the section that the corners are relative to.
block: [u8; 3],
/// Drawn with the alpha test regardless of its texture.
cutout: bool,
}
struct Cache {
/// World position of the section's first block.
origin: [i32; 3],
blocks: Box<[Block; PADDED * PADDED * PADDED]>,
metadata: Box<[u8; PADDED * PADDED * PADDED]>,
light: Box<[Light; PADDED * PADDED * PADDED]>,
/// [`Cache::light`] of each slot, which includes the neighbor maximum.
light_ext: Box<[Light; PADDED * PADDED * PADDED]>,
/// `RenderBlocks.renderAllFaces`: draw faces even where neighbors hide them.
all_faces: bool,
}
impl Cache {
fn build(input: &MeshInput) -> Self {
let center = input.chunks[4].pos;
let mut cache = Self {
origin: [
center.min_block_x(),
input.section_y * SIZE,
center.min_block_z(),
],
blocks: Box::new([Block::AIR; PADDED * PADDED * PADDED]),
metadata: Box::new([0; PADDED * PADDED * PADDED]),
light: Box::new([Light::default(); PADDED * PADDED * PADDED]),
light_ext: Box::new([Light::default(); PADDED * PADDED * PADDED]),
all_faces: false,
};
for x in -BORDER..SIZE + BORDER {
for z in -BORDER..SIZE + BORDER {
let chunk = &input.chunks[(((z >> 4) + 1) * 3 + (x >> 4) + 1) as usize];
let (cx, cz) = ((x & 15) as usize, (z & 15) as usize);
for y in -BORDER..SIZE + BORDER {
let i = Self::slot(x, y, z);
let world_y = cache.origin[1] + y;
if world_y < 0 {
continue;
}
if world_y >= HEIGHT as i32 {
cache.light[i] = Light::SKY;
continue;
}
let j = index(cx, world_y as usize, cz);
cache.blocks[i] = chunk.blocks().get(j);
cache.metadata[i] = chunk.metadata.get(j);
cache.light[i] = Light {
sky: chunk.sky_light.get(j),
block: chunk.block_light.get(j),
};
}
}
}
*cache.light_ext = *cache.light;
for x in 1 - BORDER..SIZE + BORDER - 1 {
for z in 1 - BORDER..SIZE + BORDER - 1 {
for y in 1 - BORDER..SIZE + BORDER - 1 {
cache.refresh_light(x, y, z);
}
}
}
cache
}
/// Port of `ChunkCache.getLightValueExt`, including the neighbor maximum for slabs, stairs
/// and farmland, stored for [`Self::light`].
fn refresh_light(&mut self, x: i32, y: i32, z: i32) {
let i = Self::slot(x, y, z);
self.light_ext[i] = if uses_neighbor_light(self.blocks[i]) {
[(0, 1, 0), (1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1)]
.into_iter()
.map(|(dx, dy, dz)| self.light[Self::slot(x + dx, y + dy, z + dz)])
.fold(Light::default(), Light::max)
} else {
self.light[i]
};
}
fn slot(x: i32, y: i32, z: i32) -> usize {
(((x + BORDER) as usize * PADDED) + (z + BORDER) as usize) * PADDED + (y + BORDER) as usize
}
fn block(&self, x: i32, y: i32, z: i32) -> Block {
self.blocks[Self::slot(x, y, z)]
}
fn metadata(&self, x: i32, y: i32, z: i32) -> u8 {
self.metadata[Self::slot(x, y, z)]
}
fn material(&self, x: i32, y: i32, z: i32) -> Material {
self.block(x, y, z).material()
}
/// Port of `ChunkCache.getLightValueExt`, see [`Self::refresh_light`].
fn light(&self, x: i32, y: i32, z: i32) -> Light {
self.light_ext[Self::slot(x, y, z)]
}
fn world(&self, [x, y, z]: [i32; 3]) -> [i32; 3] {
[self.origin[0] + x, self.origin[1] + y, self.origin[2] + z]
}
}
/// Blocks lit by their brightest neighbor in `getLightValueExt`.
fn uses_neighbor_light(block: Block) -> bool {
matches!(
block,
Block::SLAB | Block::FARMLAND | Block::OAK_STAIRS | Block::COBBLESTONE_STAIRS
)
}
impl common::world::shape::BlockView for Cache {
fn block(&self, x: i32, y: i32, z: i32) -> Block {
Cache::block(self, x, y, z)
}
fn metadata(&self, x: i32, y: i32, z: i32) -> u8 {
Cache::metadata(self, x, y, z)
}
}
/// Per block id: `isBlockOpaqueCube`, and `Block.canBlockGrass` (lets light reach around corners).
struct Tables {
opaque: [bool; 256],
passes_light: [bool; 256],
}
static TABLES: LazyLock<Tables> = LazyLock::new(|| Tables {
opaque: std::array::from_fn(|id| Block(id as u8).is_opaque_cube()),
passes_light: std::array::from_fn(|id| {
matches!(
Block(id as u8).material(),
Material::Air
| Material::Plants
| Material::Circuits
| Material::Snow
| Material::Fire
| Material::Portal
)
}),
});
/// Mirrors `isBlockOpaqueCube`, where leaves only occlude with fast graphics.
fn occludes(block: Block, fancy: bool) -> bool {
if block == Block::LEAVES {
!fancy
} else {
TABLES.opaque[usize::from(block.0)]
}
}
/// `round` for vertex attributes, clamped to `0..=65535`. Exact for these magnitudes, and
/// avoids a libm call on targets without a rounding instruction.
fn round_unit(value: f32) -> u32 {
let whole = value as i32;
let rounded = if value - whole as f32 >= 0.5 {
whole + 1
} else {
whole
};
rounded.clamp(0, 65535) as u32
}
/// The u and v ranges of a whole atlas tile, as `RenderBlocks` computes them: u0, u1, v0, v1.
fn tile(texture: u8) -> [f32; 4] {
let (u, v) = (f32::from(texture & 15) * 16.0, f32::from(texture & 0xF0));
[
u / 256.0,
(u + 15.99) / 256.0,
v / 256.0,
(v + 15.99) / 256.0,
]
}
/// 0xRRGGBB to the RGBA8 vertex tint.
fn tint(rgb: u32) -> u32 {
(rgb >> 16 & 0xFF) | (rgb & 0xFF00) | (rgb & 0xFF) << 16 | 0xFF00_0000
}
struct Builder {
origin: Vec3,
quads: Vec<Quad>,
/// `RenderBlocks.overrideBlockTexture`: the tile every quad takes its texels from instead.
overlay: Option<u8>,
}
impl Builder {
fn at([x, y, z]: [i32; 3], overlay: Option<u8>) -> Self {
Self {
origin: Vec3::new(x as f32, y as f32, z as f32),
quads: Vec::new(),
overlay,
}
}
/// Draws the last quad with the alpha test, in the same draw as an overlay on top of it.
fn mark_cutout(&mut self) {
if let Some(quad) = self.quads.last_mut() {
quad.cutout = true;
}
}
fn push(&mut self, vertices: [Vertex; 4]) {
self.quads.push(Quad {
vertices,
block: self.origin.to_array().map(|c| c as u8),
cutout: false,
});
}
/// Moves a quad's texture coordinates from its own tile into the overlay tile.
fn retexture(&self, mut corners: [(Vec3, (f32, f32)); 4]) -> [(Vec3, (f32, f32)); 4] {
if let Some(tile) = self.overlay {
let origin = |coordinate: fn(&(Vec3, (f32, f32))) -> f32| {
(corners.iter().map(coordinate).fold(f32::MAX, f32::min) * 16.0).floor() / 16.0
};
let (u0, v0) = (origin(|c| c.1.0), origin(|c| c.1.1));
let (tu, tv) = (f32::from(tile % 16) / 16.0, f32::from(tile / 16) / 16.0);
for (_, (u, v)) in &mut corners {
*u += tu - u0;
*v += tv - v0;
}
}
corners
}
fn vertex(
&self,
position: Vec3,
(u, v): (f32, f32),
light: [Light; 4],
shade: Shade,
rgb: u32,
) -> Vertex {
let p = ((self.origin + position + 8.0) * 2048.0)
.to_array()
.map(round_unit);
let light = light
.iter()
.enumerate()
.fold(0, |packed, (i, sample)| packed | sample.packed() << (8 * i));
Vertex {
xy: p[0] | p[1] << 16,
z_shade: p[2] | (shade as u32) << 16,
uv: round_unit(u * 65536.0) | round_unit(v * 65536.0) << 16,
tint: tint(rgb),
light,
}
}
/// A quad lit by a single light value.
fn quad(&mut self, corners: [(Vec3, (f32, f32)); 4], light: Light, shade: Shade, rgb: u32) {
let vertices = self
.retexture(corners)
.map(|(position, uv)| self.vertex(position, uv, [light; 4], shade, rgb));
self.push(vertices);
}
/// A quad with four light samples averaged at each corner.
fn smooth_quad(
&mut self,
corners: [(Vec3, (f32, f32)); 4],
light: [[Light; 4]; 4],
shade: Shade,
rgb: u32,
) {
let corners = self.retexture(corners);
let vertices = std::array::from_fn(|i| {
let (position, uv) = corners[i];
self.vertex(position, uv, light[i], shade, rgb)
});
self.push(vertices);
}
}
pub fn build(input: &MeshInput) -> SectionMesh {
let cache = Cache::build(input);
let mut layers = [0; 2].map(|_| Builder::at([0; 3], None));
for x in 0..SIZE {
for z in 0..SIZE {
for y in 0..SIZE {
let block = cache.block(x, y, z);
let Some(properties) = block.properties() else {
continue;
};
// A full opaque cube with every face hidden draws nothing
if properties.shape == Shape::Cube
&& properties.opaque_cube
&& common::world::shape::SIDE_OFFSETS
.iter()
.all(|[dx, dy, dz]| {
occludes(cache.block(x + dx, y + dy, z + dz), input.fancy)
})
{
continue;
}
let builder = &mut layers[match properties.layer {
Layer::Opaque => 0,
Layer::Translucent => 1,
}];
builder.origin = Vec3::new(x as f32, y as f32, z as f32);
render_block(input, &cache, builder, block, [x, y, z]);
}
}
}
let [opaque, translucent] = layers.map(|builder| builder.quads);
SectionMesh::pack(&opaque, &translucent)
}
/// `RenderBlocks.renderBlockByRenderType`.
fn render_block(
input: &MeshInput,
cache: &Cache,
builder: &mut Builder,
block: Block,
position: [i32; 3],
) {
let Some(properties) = block.properties() else {
return;
};
match properties.shape {
Shape::Cube => block::standard(input, cache, builder, block, position),
Shape::Fluid => fluid::fluid(cache, builder, block, position, input.fancy),
Shape::Cross => plants::cross(input, cache, builder, block, position),
Shape::Cactus => plants::cactus(cache, builder, block, position, input.fancy),
Shape::Torch => special::torch(cache, builder, block, position),
Shape::Bed => devices::bed(input, cache, builder, position),
Shape::Lever => devices::lever(input, cache, builder, position),
Shape::Repeater => devices::repeater(input, cache, builder, block, position),
Shape::Wire => devices::wire(cache, builder, position),
Shape::Rail => devices::rail(cache, builder, block, position),
Shape::PistonBase => devices::piston_base(input, cache, builder, block, position),
Shape::PistonExtension => {
devices::piston_extension(input, cache, builder, position, (1.0, 16.0))
}
Shape::Stairs | Shape::Fence | Shape::Door | Shape::Ladder | Shape::Crops | Shape::Fire => {
special::render(input, cache, builder, block, position)
}
Shape::None => {}
}
}
/// What `TileEntityRendererPiston` draws for a block a piston moves.
pub enum Moving {
Block(Block, u8),
/// A piston head with a rod `length` long, showing `shift` texels of the rod texture.
Head {
metadata: u8,
length: f32,
shift: f32,
},
/// A retracting piston's base, drawn without the head's quarter.
Base(Block, u8),
}
/// The geometry of a moving block at `position` within the section, with all faces and lit
/// where it is.
pub fn moving(input: &MeshInput, position: [i32; 3], part: Moving) -> Vec<Quad> {
let mut cache = Cache::build(input);
cache.all_faces = true;
let (block, metadata) = match part {
Moving::Block(block, metadata) | Moving::Base(block, metadata) => (block, metadata),
Moving::Head { metadata, .. } => (Block::PISTON_HEAD, metadata),
};
let [x, y, z] = position;
let slot = Cache::slot(x, y, z);
cache.blocks[slot] = block;
cache.metadata[slot] = metadata;
cache.refresh_light(x, y, z);
let mut builder = Builder::at(position, None);
match part {
Moving::Head { length, shift, .. } => {
devices::piston_extension(input, &cache, &mut builder, position, (length, shift))
}
_ => render_block(input, &cache, &mut builder, block, position),
}
builder.quads
}
/// `RenderBlocks.renderBlockUsingTexture`: the visible faces of the block at `position`
/// within the section, textured with `tile`.
pub fn overlay(input: &MeshInput, position: [i32; 3], tile: u8) -> Vec<Quad> {
let cache = Cache::build(input);
let [x, y, z] = position;
let block = cache.block(x, y, z);
let mut builder = Builder::at(position, Some(tile));
render_block(input, &cache, &mut builder, block, position);
builder.quads
}
impl Vertex {
/// The position within the section.
pub fn position(&self) -> Vec3 {
let xy = (self.xy & 0xFFFF, self.xy >> 16);
Vec3::new(xy.0 as f32, xy.1 as f32, (self.z_shade & 0xFFFF) as f32) / 2048.0 - 8.0
}
pub fn uv(&self) -> [f32; 2] {
[(self.uv & 0xFFFF) as f32, (self.uv >> 16) as f32].map(|v| v / 65536.0)
}
/// The RGBA8 color `terrain.slang` gives the vertex.
pub fn color(&self, brightness: &[f32; 16], sky_darkness: u32) -> u32 {
const SHADE: [f32; 4] = [1.0, 0.8, 0.6, 0.5];
let light = (0..4)
.map(|i| {
let sample = self.light >> (8 * i);
let sky = (sample & 15) as i32 - sky_darkness as i32;
let block = (sample >> 4 & 15) as i32;
brightness[sky.max(block).clamp(0, 15) as usize]
})
.sum::<f32>()
/ 4.0
* SHADE[(self.z_shade >> 16 & 3) as usize];
let light = if self.z_shade >> 18 & 1 != 0 {
(light + 1.0) * 0.5
} else {
light
};
let channel = |shift: u32| (((self.tint >> shift) & 0xFF) as f32 * light) as u32;
0xFF00_0000 | channel(0) | channel(8) << 8 | channel(16) << 16
}
}