nightshade 0.8.2

A cross-platform data-oriented game engine.
Documentation
use super::components::{NoiseConfig, NoiseType, TerrainConfig};
use crate::ecs::mesh::components::{Mesh, Vertex};
use nalgebra_glm::{Vec3, vec3};
use noise::{Billow, Fbm, MultiFractal, NoiseFn, Perlin, RidgedMulti, Simplex};

pub fn sample_terrain_height(x: f32, z: f32, config: &TerrainConfig) -> f32 {
    sample_noise(x as f64, z as f64, &config.noise) as f32 * config.height_scale
}

pub struct TerrainGenerationResult {
    pub mesh: Mesh,
    pub heights: Vec<f32>,
    pub min_height: f32,
    pub max_height: f32,
}

pub fn generate_terrain_mesh(config: &TerrainConfig) -> TerrainGenerationResult {
    let vertex_count = (config.resolution_x * config.resolution_z) as usize;
    let mut vertices = Vec::with_capacity(vertex_count);
    let mut heights = Vec::with_capacity(vertex_count);

    let step_x = config.width / (config.resolution_x - 1) as f32;
    let step_z = config.depth / (config.resolution_z - 1) as f32;
    let half_width = config.width / 2.0;
    let half_depth = config.depth / 2.0;

    let mut min_height = f32::MAX;
    let mut max_height = f32::MIN;

    for z in 0..config.resolution_z {
        for x in 0..config.resolution_x {
            let world_x = x as f32 * step_x - half_width;
            let world_z = z as f32 * step_z - half_depth;

            let height = sample_noise(world_x as f64, world_z as f64, &config.noise) as f32
                * config.height_scale;

            min_height = min_height.min(height);
            max_height = max_height.max(height);

            heights.push(height);

            let u = (x as f32 / (config.resolution_x - 1) as f32) * config.uv_scale[0];
            let v = (z as f32 / (config.resolution_z - 1) as f32) * config.uv_scale[1];

            vertices.push(Vertex::with_tex_coords(
                vec3(world_x, height, world_z),
                vec3(0.0, 1.0, 0.0),
                [u, v],
            ));
        }
    }

    let mut indices =
        Vec::with_capacity(((config.resolution_x - 1) * (config.resolution_z - 1) * 6) as usize);

    for z in 0..(config.resolution_z - 1) {
        for x in 0..(config.resolution_x - 1) {
            let top_left = z * config.resolution_x + x;
            let top_right = top_left + 1;
            let bottom_left = (z + 1) * config.resolution_x + x;
            let bottom_right = bottom_left + 1;

            indices.push(top_left);
            indices.push(bottom_left);
            indices.push(top_right);

            indices.push(top_right);
            indices.push(bottom_left);
            indices.push(bottom_right);
        }
    }

    calculate_terrain_normals(
        &mut vertices,
        &indices,
        config.resolution_x,
        config.resolution_z,
    );

    TerrainGenerationResult {
        mesh: Mesh::new(vertices, indices),
        heights,
        min_height,
        max_height,
    }
}

fn sample_noise(x: f64, z: f64, config: &NoiseConfig) -> f64 {
    let scaled_x = x * config.frequency;
    let scaled_z = z * config.frequency;

    match config.noise_type {
        NoiseType::Perlin => {
            let noise = Fbm::<Perlin>::new(config.seed)
                .set_octaves(config.octaves)
                .set_lacunarity(config.lacunarity)
                .set_persistence(config.persistence);
            noise.get([scaled_x, scaled_z])
        }
        NoiseType::Simplex => {
            let noise = Fbm::<Simplex>::new(config.seed)
                .set_octaves(config.octaves)
                .set_lacunarity(config.lacunarity)
                .set_persistence(config.persistence);
            noise.get([scaled_x, scaled_z])
        }
        NoiseType::Billow => {
            let noise = Billow::<Perlin>::new(config.seed)
                .set_octaves(config.octaves)
                .set_lacunarity(config.lacunarity)
                .set_persistence(config.persistence);
            noise.get([scaled_x, scaled_z])
        }
        NoiseType::RidgedMulti => {
            let noise = RidgedMulti::<Perlin>::new(config.seed)
                .set_octaves(config.octaves)
                .set_lacunarity(config.lacunarity);
            noise.get([scaled_x, scaled_z])
        }
    }
}

fn calculate_terrain_normals(
    vertices: &mut [Vertex],
    indices: &[u32],
    resolution_x: u32,
    resolution_z: u32,
) {
    let vertex_count = (resolution_x * resolution_z) as usize;
    let mut normals: Vec<Vec3> = vec![Vec3::zeros(); vertex_count];

    for triangle in indices.chunks(3) {
        let i0 = triangle[0] as usize;
        let i1 = triangle[1] as usize;
        let i2 = triangle[2] as usize;

        let v0 = vec3(
            vertices[i0].position[0],
            vertices[i0].position[1],
            vertices[i0].position[2],
        );
        let v1 = vec3(
            vertices[i1].position[0],
            vertices[i1].position[1],
            vertices[i1].position[2],
        );
        let v2 = vec3(
            vertices[i2].position[0],
            vertices[i2].position[1],
            vertices[i2].position[2],
        );

        let edge1 = v1 - v0;
        let edge2 = v2 - v0;
        let face_normal = nalgebra_glm::cross(&edge1, &edge2);

        normals[i0] += face_normal;
        normals[i1] += face_normal;
        normals[i2] += face_normal;
    }

    for (index, vertex) in vertices.iter_mut().enumerate() {
        let normal = nalgebra_glm::normalize(&normals[index]);
        vertex.normal = [normal.x, normal.y, normal.z];
    }
}