chunkgen 0.2.0

Minecraft world generation library
Documentation
use std::sync::LazyLock;

use crate::util::pos::ChunkPos;
use crate::world::worldgen::structure::pools::jigsaw_junction::JigsawJunction;
use crate::world::worldgen::structure::pools::structure_template_pool::Projection;
use crate::world::worldgen::structure::structure::TerrainAdjustment;
use crate::world::worldgen::structure::structure_piece::PieceKind;
use crate::world::worldgen::structure::structure_start::StructureStart;
use shulkr::util::BlockPosition;
use shulkr::util::BoundingBox;
use std::f64::consts::E;

pub const BEARD_KERNEL_RADIUS: i32 = 12;
const BEARD_KERNEL_SIZE: usize = 24;

static BEARD_KERNEL: LazyLock<[f32; BEARD_KERNEL_SIZE * BEARD_KERNEL_SIZE * BEARD_KERNEL_SIZE]> =
    LazyLock::new(|| {
        let mut kernel = [0.0f32; BEARD_KERNEL_SIZE * BEARD_KERNEL_SIZE * BEARD_KERNEL_SIZE];
        for zi in 0..BEARD_KERNEL_SIZE {
            for xi in 0..BEARD_KERNEL_SIZE {
                for yi in 0..BEARD_KERNEL_SIZE {
                    kernel[zi * BEARD_KERNEL_SIZE * BEARD_KERNEL_SIZE
                        + xi * BEARD_KERNEL_SIZE
                        + yi] = compute_beard_contribution(
                        xi as i32 - BEARD_KERNEL_RADIUS,
                        f64::from(yi as i32 - BEARD_KERNEL_RADIUS) + 0.5,
                        zi as i32 - BEARD_KERNEL_RADIUS,
                    ) as f32;
                }
            }
        }
        kernel
    });

#[derive(Debug, Clone, Copy)]
pub struct Rigid {
    pub box_: BoundingBox,
    pub terrain_adjustment: TerrainAdjustment,
    pub ground_level_delta: i32,
}

#[derive(Debug, Default)]
pub struct Beardifier {
    pieces: Vec<Rigid>,
    junctions: Vec<JigsawJunction>,
    affected_box: Option<BoundingBox>,
}

impl Beardifier {
    pub const fn empty() -> Self {
        Self {
            pieces: Vec::new(),
            junctions: Vec::new(),
            affected_box: None,
        }
    }

    pub fn for_structures_in_chunk(
        chunk_pos: ChunkPos,
        collect_starts: impl FnOnce(&mut dyn FnMut(&StructureStart)),
    ) -> Self {
        let chunk_start_block_x = chunk_pos.0.x * 16;
        let chunk_start_block_z = chunk_pos.0.y * 16;
        let mut pieces = Vec::new();
        let mut junctions = Vec::new();
        let mut any_piece_bounding_box: Option<BoundingBox> = None;

        collect_starts(&mut |start| {
            let Some(structure) = start.structure() else {
                return;
            };
            let terrain_adjustment = structure.terrain_adaptation();
            if terrain_adjustment == TerrainAdjustment::None {
                return;
            }

            for piece in start.pieces() {
                if !is_close_to_chunk(piece.bounding_box(), chunk_pos, BEARD_KERNEL_RADIUS) {
                    continue;
                }

                let PieceKind::PoolElement(pool_piece) = &piece.kind else {
                    pieces.push(Rigid {
                        box_: piece.bounding_box(),
                        terrain_adjustment,
                        ground_level_delta: 0,
                    });
                    any_piece_bounding_box = Some(include_bounding_box(
                        any_piece_bounding_box,
                        piece.bounding_box(),
                    ));
                    continue;
                };
                if pool_piece.element.projection() == Projection::Rigid {
                    pieces.push(Rigid {
                        box_: piece.bounding_box(),
                        terrain_adjustment,
                        ground_level_delta: pool_piece.ground_level_delta,
                    });
                    any_piece_bounding_box = Some(include_bounding_box(
                        any_piece_bounding_box,
                        piece.bounding_box(),
                    ));
                }

                for junction in &pool_piece.junctions {
                    let junction_x = junction.source_x;
                    let junction_z = junction.source_z;
                    if junction_x > chunk_start_block_x - BEARD_KERNEL_RADIUS
                        && junction_z > chunk_start_block_z - BEARD_KERNEL_RADIUS
                        && junction_x < chunk_start_block_x + 15 + BEARD_KERNEL_RADIUS
                        && junction_z < chunk_start_block_z + 15 + BEARD_KERNEL_RADIUS
                    {
                        junctions.push(*junction);
                        let junction_box = BoundingBox::from_point(BlockPosition::new(
                            junction_x,
                            junction.source_ground_y,
                            junction_z,
                        ));
                        any_piece_bounding_box =
                            Some(include_bounding_box(any_piece_bounding_box, junction_box));
                    }
                }
            }
        });

        let Some(any_piece_bounding_box) = any_piece_bounding_box else {
            return Self::empty();
        };

        Self {
            pieces,
            junctions,
            affected_box: Some(any_piece_bounding_box.inflated_by(BEARD_KERNEL_SIZE as i32)),
        }
    }

    pub const fn is_empty(&self) -> bool {
        self.affected_box.is_none()
    }

    pub const fn affected_box(&self) -> Option<BoundingBox> {
        self.affected_box
    }

    pub fn compute(&self, block_x: i32, block_y: i32, block_z: i32) -> f64 {
        f64::from(self.sample_value(block_x, block_y, block_z))
    }

    pub fn sample_value(&self, block_x: i32, block_y: i32, block_z: i32) -> f32 {
        match self.affected_box {
            Some(b) if b.is_inside(BlockPosition::new(block_x, block_y, block_z)) => {
                self.sample_value_unchecked(block_x, block_y, block_z)
            }
            _ => 0.0,
        }
    }

    pub fn sample_value_unchecked(&self, block_x: i32, block_y: i32, block_z: i32) -> f32 {
        let mut noise_value = 0.0f32;

        for rigid in &self.pieces {
            let box_ = rigid.box_;
            let dx = 0.max((box_.min_x() - block_x).max(block_x - box_.max_x()));
            let dz = 0.max((box_.min_z() - block_z).max(block_z - box_.max_z()));
            let ground_y = box_.min_y() + rigid.ground_level_delta;
            let dy_to_ground = block_y - ground_y;
            let dy = match rigid.terrain_adjustment {
                TerrainAdjustment::None => 0,
                TerrainAdjustment::Bury | TerrainAdjustment::BeardThin => dy_to_ground,
                TerrainAdjustment::BeardBox => {
                    0.max((ground_y - block_y).max(block_y - box_.max_y()))
                }
                TerrainAdjustment::Encapsulate => {
                    0.max((box_.min_y() - block_y).max(block_y - box_.max_y()))
                }
            };
            noise_value += match rigid.terrain_adjustment {
                TerrainAdjustment::None => 0.0,
                TerrainAdjustment::Bury => {
                    get_bury_contribution(dx as f32, dy as f32 / 2.0, dz as f32)
                }
                TerrainAdjustment::BeardThin | TerrainAdjustment::BeardBox => {
                    get_beard_contribution(dx, dy, dz, dy_to_ground) * 0.8
                }
                TerrainAdjustment::Encapsulate => {
                    get_bury_contribution(dx as f32 / 2.0, dy as f32 / 2.0, dz as f32 / 2.0) * 0.8
                }
            };
        }

        for junction in &self.junctions {
            let dx = block_x - junction.source_x;
            let dy = block_y - junction.source_ground_y;
            let dz = block_z - junction.source_z;
            noise_value += get_beard_contribution(dx, dy, dz, dy) * 0.4;
        }

        noise_value
    }
}

const fn is_close_to_chunk(box_: BoundingBox, chunk_pos: ChunkPos, distance: i32) -> bool {
    let min_x = chunk_pos.0.x * 16;
    let min_z = chunk_pos.0.y * 16;
    box_.intersects_xz(
        min_x - distance,
        min_z - distance,
        min_x + 15 + distance,
        min_z + 15 + distance,
    )
}

fn include_bounding_box(encompassing: Option<BoundingBox>, new_box: BoundingBox) -> BoundingBox {
    encompassing.map_or(new_box, |existing| existing.encapsulate_box(new_box))
}

fn get_bury_contribution(dx: f32, dy: f32, dz: f32) -> f32 {
    let distance_sq = dx * dx + dy * dy + dz * dz;
    if distance_sq >= 36.0 {
        0.0
    } else {
        1.0 - (f64::from(distance_sq).sqrt() as f32) / 6.0
    }
}

fn get_beard_contribution(dx: i32, dy: i32, dz: i32, y_to_ground: i32) -> f32 {
    let xi = dx + BEARD_KERNEL_RADIUS;
    let yi = dy + BEARD_KERNEL_RADIUS;
    let zi = dz + BEARD_KERNEL_RADIUS;
    if !is_in_kernel_range(xi) || !is_in_kernel_range(yi) || !is_in_kernel_range(zi) {
        return 0.0;
    }

    let dy_with_offset = y_to_ground as f32 + 0.5;
    let distance_sqr =
        dx as f32 * dx as f32 + dy_with_offset * dy_with_offset + dz as f32 * dz as f32;
    let value = -dy_with_offset * (fast_inv_sqrt(f64::from(distance_sqr / 2.0)) as f32) / 2.0;
    value
        * BEARD_KERNEL[zi as usize * BEARD_KERNEL_SIZE * BEARD_KERNEL_SIZE
            + xi as usize * BEARD_KERNEL_SIZE
            + yi as usize]
}

const fn is_in_kernel_range(value: i32) -> bool {
    value >= 0 && value < BEARD_KERNEL_SIZE as i32
}

fn compute_beard_contribution(dx: i32, dy: f64, dz: i32) -> f64 {
    let distance_sqr = f64::from(dx) * f64::from(dx) + dy * dy + f64::from(dz) * f64::from(dz);
    E.powf(-distance_sqr / 16.0)
}

fn fast_inv_sqrt(x: f64) -> f64 {
    let x_half = 0.5 * x;
    let bits = 6_910_469_410_427_058_090_i64 - (x.to_bits() as i64 >> 1);
    let approx = f64::from_bits(bits as u64);
    approx * (1.5 - x_half * approx * approx)
}