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 / 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 }),
        })
    })
}