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 / server / src / terrain.rs

use common::JavaRandom;
use common::climate::{Climate, ClimateMap};
use common::noise::PerlinOctaves;
use common::world::chunk::{self, HEIGHT, VOLUME};
use common::world::{Block, ChunkPos};

use crate::caves::Caves;
use crate::nether::NetherGenerator;

const SEA_LEVEL: usize = 64;
const SAMPLES_XZ: usize = 5;
const SAMPLES_Y: usize = 17;
const HORIZONTAL: f64 = 684.412;

/// The terrain of one dimension.
pub enum Generator {
    Overworld(OverworldGenerator),
    Nether(NetherGenerator),
}

impl Generator {
    pub fn seed(&self) -> i64 {
        match self {
            Self::Overworld(generator) => generator.seed,
            Self::Nether(generator) => generator.seed,
        }
    }

    /// `WorldProvider.worldType`: 0 for the Overworld, -1 for the Nether.
    pub fn dimension(&self) -> i8 {
        match self {
            Self::Overworld(_) => 0,
            Self::Nether(_) => -1,
        }
    }

    /// The biomes of an area laid out x-major; the Nether is all `Hell`.
    pub fn biomes(&self, x: i32, z: i32, width: usize, depth: usize) -> Vec<common::biome::Biome> {
        match self {
            Self::Overworld(generator) => generator.climate.sample(x, z, width, depth).biomes,
            Self::Nether(_) => vec![common::biome::Biome::Hell; width * depth],
        }
    }

    /// `provideChunk`: the blocks, and for the Nether the generator random afterwards.
    pub fn generate(&self, pos: ChunkPos) -> (Box<[Block; VOLUME]>, Option<JavaRandom>) {
        match self {
            Self::Overworld(generator) => (generator.generate(pos), None),
            Self::Nether(generator) => generator.generate(pos),
        }
    }
}

/// The random `provideChunk` builds the surface with.
pub(crate) fn chunk_random(pos: ChunkPos) -> JavaRandom {
    JavaRandom::new(
        i64::from(pos.x)
            .wrapping_mul(341_873_128_712)
            .wrapping_add(i64::from(pos.z).wrapping_mul(132_897_987_541)),
    )
}

/// The limit noises and the selector blending them, which both dimensions shape terrain with.
pub(crate) struct Density {
    lower_limit: PerlinOctaves,
    upper_limit: PerlinOctaves,
    selector: PerlinOctaves,
}

impl Density {
    pub(crate) fn new(random: &mut JavaRandom) -> Self {
        Self {
            lower_limit: PerlinOctaves::new(random, 16),
            upper_limit: PerlinOctaves::new(random, 16),
            selector: PerlinOctaves::new(random, 8),
        }
    }

    /// The density at the 5x17x5 samples of a chunk, minus `offsets` of each sample column.
    pub(crate) fn field(
        &self,
        pos: ChunkPos,
        vertical: f64,
        selector_vertical: f64,
        offsets: impl Fn(usize) -> [f64; SAMPLES_Y],
    ) -> Vec<f64> {
        let origin = [f64::from(pos.x * 4), 0.0, f64::from(pos.z * 4)];
        let sample = |noise: &PerlinOctaves, horizontal, vertical| {
            let mut values = vec![0.0; SAMPLES_XZ * SAMPLES_Y * SAMPLES_XZ];
            let size = [SAMPLES_XZ, SAMPLES_Y, SAMPLES_XZ];
            noise.fill(
                &mut values,
                origin,
                size,
                [horizontal, vertical, horizontal],
            );
            values
        };
        let selector = sample(&self.selector, HORIZONTAL / 80.0, selector_vertical);
        let lower = sample(&self.lower_limit, HORIZONTAL, vertical);
        let upper = sample(&self.upper_limit, HORIZONTAL, vertical);

        let mut field = Vec::with_capacity(lower.len());
        for column in 0..SAMPLES_XZ * SAMPLES_XZ {
            for (sy, offset) in offsets(column).into_iter().enumerate() {
                let i = field.len();
                let (low, high) = (lower[i] / 512.0, upper[i] / 512.0);
                let blend = (selector[i] / 10.0 + 1.0) / 2.0;
                let mut value = if blend < 0.0 {
                    low
                } else if blend > 1.0 {
                    high
                } else {
                    low + (high - low) * blend
                };
                value -= offset;
                if sy > SAMPLES_Y - 4 {
                    let fade = f64::from((sy - (SAMPLES_Y - 4)) as f32 / 3.0);
                    value = value * (1.0 - fade) + -10.0 * fade;
                }
                field.push(value);
            }
        }
        field
    }
}

/// `generateTerrain`: trilinear interpolation of the density field; `block(x, y, z, density)`
/// picks each block.
pub(crate) fn interpolate(
    density: &[f64],
    blocks: &mut [Block; VOLUME],
    block: impl Fn(usize, usize, usize, f64) -> Block,
) {
    let sample = |x: usize, z: usize, y: usize| density[(x * SAMPLES_XZ + z) * SAMPLES_Y + y];
    for cx in 0..4 {
        for cz in 0..4 {
            for cy in 0..SAMPLES_Y - 1 {
                let mut d00 = sample(cx, cz, cy);
                let mut d01 = sample(cx, cz + 1, cy);
                let mut d10 = sample(cx + 1, cz, cy);
                let mut d11 = sample(cx + 1, cz + 1, cy);
                let step00 = (sample(cx, cz, cy + 1) - d00) * 0.125;
                let step01 = (sample(cx, cz + 1, cy + 1) - d01) * 0.125;
                let step10 = (sample(cx + 1, cz, cy + 1) - d10) * 0.125;
                let step11 = (sample(cx + 1, cz + 1, cy + 1) - d11) * 0.125;
                for sy in 0..8 {
                    let y = cy * 8 + sy;
                    let (mut d0, mut d1) = (d00, d01);
                    let step_x0 = (d10 - d00) * 0.25;
                    let step_x1 = (d11 - d01) * 0.25;
                    for sx in 0..4 {
                        let x = cx * 4 + sx;
                        let mut d = d0;
                        let step_z = (d1 - d0) * 0.25;
                        for sz in 0..4 {
                            let z = cz * 4 + sz;
                            blocks[chunk::index(x, y, z)] = block(x, y, z, d);
                            d += step_z;
                        }
                        d0 += step_x0;
                        d1 += step_x1;
                    }
                    d00 += step00;
                    d01 += step01;
                    d10 += step10;
                    d11 += step11;
                }
            }
        }
    }
}

/// The noises of `replaceBlocksForBiome` per column: sand (soul sand), gravel and soil depth.
pub(crate) fn surface_noise(
    pair: &PerlinOctaves,
    depth: &PerlinOctaves,
    pos: ChunkPos,
) -> [[f64; 256]; 3] {
    const SCALE: f64 = 1.0 / 32.0;
    let (x0, z0) = (f64::from(pos.min_block_x()), f64::from(pos.min_block_z()));
    let mut noise = [[0.0; 256]; 3];
    let flat = [16, 16, 1];
    pair.fill(&mut noise[0], [x0, z0, 0.0], flat, [SCALE, SCALE, 1.0]);
    pair.fill(
        &mut noise[1],
        [x0, 109.0134, z0],
        [16, 1, 16],
        [SCALE, 1.0, SCALE],
    );
    depth.fill(&mut noise[2], [x0, z0, 0.0], flat, [SCALE * 2.0; 3]);
    noise
}

/// Port of `ChunkProviderGenerate`'s terrain stage. Immutable, so one instance
/// can generate chunks on many threads at once.
pub struct OverworldGenerator {
    seed: i64,
    pub(crate) climate: Climate,
    density: Density,
    sand_gravel: PerlinOctaves,
    soil_depth: PerlinOctaves,
    scale: PerlinOctaves,
    depth: PerlinOctaves,
    /// `mobSpawnerNoise`, which population uses to vary tree density.
    pub(crate) tree_noise: PerlinOctaves,
    caves: Caves,
}

impl OverworldGenerator {
    pub fn new(seed: i64) -> Self {
        let mut random = JavaRandom::new(seed);
        Self {
            seed,
            climate: Climate::new(seed),
            density: Density::new(&mut random),
            sand_gravel: PerlinOctaves::new(&mut random, 4),
            soil_depth: PerlinOctaves::new(&mut random, 4),
            scale: PerlinOctaves::new(&mut random, 10),
            depth: PerlinOctaves::new(&mut random, 16),
            tree_noise: PerlinOctaves::new(&mut random, 8),
            caves: Caves {
                seed,
                nether: false,
            },
        }
    }

    /// Port of `provideChunk`.
    pub fn generate(&self, pos: ChunkPos) -> Box<[Block; VOLUME]> {
        let mut random = chunk_random(pos);
        let climate = self
            .climate
            .sample(pos.min_block_x(), pos.min_block_z(), 16, 16);
        let mut blocks = chunk::empty_blocks();
        let density = self.density_field(pos, &climate);
        interpolate(&density, &mut blocks, |x, y, z, d| {
            if d > 0.0 {
                Block::STONE
            } else if y >= SEA_LEVEL {
                Block::AIR
            } else if climate.temperature[x * 16 + z] < 0.5 && y >= SEA_LEVEL - 1 {
                Block::ICE
            } else {
                Block::WATER
            }
        });
        self.replace_surface(pos, &climate, &mut random, &mut blocks);
        self.caves.carve(pos, &mut blocks);
        blocks
    }

    /// Port of `initializeNoiseField`.
    fn density_field(&self, pos: ChunkPos, climate: &ClimateMap) -> Vec<f64> {
        let (x, z) = (pos.x * 4, pos.z * 4);
        let size = [SAMPLES_XZ, SAMPLES_XZ];
        let mut scale = [0.0; SAMPLES_XZ * SAMPLES_XZ];
        let mut depth = [0.0; SAMPLES_XZ * SAMPLES_XZ];
        self.scale.fill_2d(&mut scale, x, z, size, [1.121, 1.121]);
        self.depth.fill_2d(&mut depth, x, z, size, [200.0, 200.0]);
        self.density
            .field(pos, HORIZONTAL, HORIZONTAL / 160.0, |column| {
                let stride = 16 / SAMPLES_XZ;
                let column_x = column / SAMPLES_XZ * stride + stride / 2;
                let column_z = column % SAMPLES_XZ * stride + stride / 2;
                let temperature = climate.temperature[column_x * 16 + column_z];
                let wetness = climate.rainfall[column_x * 16 + column_z] * temperature;
                let dryness = (1.0 - wetness) * (1.0 - wetness);
                let dryness = dryness * dryness;
                let mut scale = ((scale[column] + 256.0) / 512.0 * (1.0 - dryness)).min(1.0);
                let mut depth = depth[column] / 8000.0;
                if depth < 0.0 {
                    depth = -depth * 0.3;
                }
                depth = depth * 3.0 - 2.0;
                if depth < 0.0 {
                    depth = (depth / 2.0).max(-1.0) / 1.4 / 2.0;
                    scale = 0.0;
                } else {
                    depth = depth.min(1.0) / 8.0;
                }
                let scale = scale.max(0.0) + 0.5;
                let center = SAMPLES_Y as f64 / 2.0 + depth * SAMPLES_Y as f64 / 16.0 * 4.0;
                std::array::from_fn(|sy| {
                    let offset = (sy as f64 - center) * 12.0 / scale;
                    if offset < 0.0 { offset * 4.0 } else { offset }
                })
            })
    }

    /// Port of `replaceBlocksForBiome`: topsoil, beaches, sandstone and bedrock.
    fn replace_surface(
        &self,
        pos: ChunkPos,
        climate: &ClimateMap,
        random: &mut JavaRandom,
        blocks: &mut [Block; VOLUME],
    ) {
        let [sand, gravel, soil] = surface_noise(&self.sand_gravel, &self.soil_depth, pos);
        for z in 0..16 {
            for x in 0..16 {
                let column = x * 16 + z;
                let biome = climate.biomes[column];
                let has_sand = sand[column] + random.next_double() * 0.2 > 0.0;
                let has_gravel = gravel[column] + random.next_double() * 0.2 > 3.0;
                let soil_depth = (soil[column] / 3.0 + 3.0 + random.next_double() * 0.25) as i32;
                let mut remaining = -1;
                let mut top = biome.top_block();
                let mut filler = biome.filler_block();

                for y in (0..HEIGHT).rev() {
                    let index = chunk::index(x, y, z);
                    if y as i32 <= random.next_int(5) {
                        blocks[index] = Block::BEDROCK;
                        continue;
                    }
                    let current = blocks[index];
                    if current.is_air() {
                        remaining = -1;
                    } else if current == Block::STONE {
                        if remaining == -1 {
                            if soil_depth <= 0 {
                                (top, filler) = (Block::AIR, Block::STONE);
                            } else if (SEA_LEVEL - 4..=SEA_LEVEL + 1).contains(&y) {
                                (top, filler) = (biome.top_block(), biome.filler_block());
                                if has_gravel {
                                    (top, filler) = (Block::AIR, Block::GRAVEL);
                                }
                                if has_sand {
                                    (top, filler) = (Block::SAND, Block::SAND);
                                }
                            }
                            if y < SEA_LEVEL && top.is_air() {
                                top = Block::WATER;
                            }
                            remaining = soil_depth;
                            blocks[index] = if y >= SEA_LEVEL - 1 { top } else { filler };
                        } else if remaining > 0 {
                            remaining -= 1;
                            blocks[index] = filler;
                            if remaining == 0 && filler == Block::SAND {
                                remaining = random.next_int(4);
                                filler = Block::SANDSTONE;
                            }
                        }
                    }
                }
            }
        }
    }
}