Terrain generation
This commit is contained in:
+198
-33
@@ -1,46 +1,211 @@
|
||||
import { Alea, create_noise_2d } from "@paulaboks/rng";
|
||||
import { Alea, create_noise_2d, NoiseFunction2D } from "@paulaboks/rng";
|
||||
import { CHUNK_SIZE, Dimension } from "./components/dimension.ts";
|
||||
|
||||
import { Dimension } from "./components/dimension.ts";
|
||||
import { ClientWorld } from "./client_world.ts";
|
||||
type Biome =
|
||||
| "desert"
|
||||
| "plains"
|
||||
| "forest"
|
||||
| "jungle"
|
||||
| "tundra"
|
||||
| "taiga"
|
||||
| "snow"
|
||||
| "savanna"
|
||||
| "swamp";
|
||||
|
||||
export function generate(
|
||||
world: ClientWorld,
|
||||
dimension: Dimension,
|
||||
width: number,
|
||||
depth: number,
|
||||
max_height = 64,
|
||||
seed = "seed",
|
||||
) {
|
||||
const height_noise = create_noise_2d(new Alea(seed + "_height"));
|
||||
function get_biome(temp: number, moisture: number): Biome {
|
||||
if (temp > 0.6) {
|
||||
if (moisture < -0.2) {
|
||||
return "desert";
|
||||
}
|
||||
if (moisture > 0.4) {
|
||||
return "jungle";
|
||||
}
|
||||
return "savanna";
|
||||
}
|
||||
if (temp > 0) {
|
||||
if (moisture > 0.5) {
|
||||
return "swamp";
|
||||
}
|
||||
if (moisture > 0) {
|
||||
return "forest";
|
||||
}
|
||||
return "plains";
|
||||
}
|
||||
if (temp > -0.5) {
|
||||
return "taiga";
|
||||
}
|
||||
return "tundra";
|
||||
}
|
||||
|
||||
const scale = 0.05;
|
||||
function get_surface_block(biome: Biome) {
|
||||
if (biome === "desert") {
|
||||
return "bworld:sand";
|
||||
} else if (biome === "tundra") {
|
||||
return "bworld:snow";
|
||||
}
|
||||
return "bworld:grass";
|
||||
}
|
||||
|
||||
for (let z = 0; z < depth; z++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const nx = x * scale;
|
||||
const nz = z * scale;
|
||||
function biome_height_modifier(biome: Biome) {
|
||||
if (biome === "desert") {
|
||||
return 0.2;
|
||||
}
|
||||
if (biome === "plains") {
|
||||
return 0.4;
|
||||
}
|
||||
if (biome === "forest") {
|
||||
return 0.5;
|
||||
}
|
||||
if (biome === "jungle") {
|
||||
return 0.45;
|
||||
}
|
||||
if (biome === "taiga") {
|
||||
return 0.55;
|
||||
}
|
||||
if (biome === "tundra") {
|
||||
return 0.35;
|
||||
}
|
||||
if (biome === "savanna") {
|
||||
return 0.4;
|
||||
}
|
||||
if (biome === "swamp") {
|
||||
return 0.35;
|
||||
}
|
||||
|
||||
// multi-octave terrain
|
||||
let h = height_noise(nx, nz) * 1 +
|
||||
height_noise(nx * 2, nz * 2) * 0.5 +
|
||||
height_noise(nx * 4, nz * 4) * 0.25;
|
||||
return 0.4;
|
||||
}
|
||||
|
||||
h /= 1.75;
|
||||
function fractal_noise(noise: NoiseFunction2D, x: number, y: number, octaves = 2) {
|
||||
let value = 0;
|
||||
let amp = 1;
|
||||
let freq = 1;
|
||||
let max = 0;
|
||||
for (let i = 0; i < octaves; i++) {
|
||||
value += noise(x * freq, y * freq) * amp;
|
||||
max += amp;
|
||||
amp *= 0.5;
|
||||
freq *= 2;
|
||||
}
|
||||
return value / max;
|
||||
}
|
||||
|
||||
// convert noise to terrain height
|
||||
const terrain_height = Math.floor((h * 0.5 + 0.5) * max_height);
|
||||
function get_terrain_height(base: number, biome: Biome, x: number, z: number, noise: NoiseFunction2D) {
|
||||
const biomeMod = biome_height_modifier(biome);
|
||||
|
||||
for (let y = 0; y <= terrain_height; y++) {
|
||||
let id = "bworld:stone";
|
||||
const main = fractal_noise(noise, x * 0.003, z * 0.003) * 15;
|
||||
|
||||
// simple terrain layers
|
||||
if (y === terrain_height) {
|
||||
id = "bworld:leaves";
|
||||
} else if (y > terrain_height - 4) {
|
||||
id = "bworld:dirt";
|
||||
const detail = fractal_noise(noise, x * 0.01, z * 0.01) * 3;
|
||||
|
||||
return Math.floor(base + biomeMod * 20 + main + detail);
|
||||
}
|
||||
|
||||
function can_place_tree(tree_map: boolean[][], local_x: number, local_z: number) {
|
||||
const TREE_SPACING = 4;
|
||||
for (let dx = -TREE_SPACING; dx <= TREE_SPACING; dx++) {
|
||||
for (let dz = -TREE_SPACING; dz <= TREE_SPACING; dz++) {
|
||||
const nx = local_x + dx;
|
||||
const nz = local_z + dz;
|
||||
if (nx >= 0 && nx < CHUNK_SIZE && nz >= 0 && nz < CHUNK_SIZE && tree_map[nx][nz]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function place_tree(dimension: Dimension, x: number, y: number, z: number, biome: Biome) {
|
||||
const height = Math.floor(Math.random() * 3) + (biome === "jungle" ? 8 : 4);
|
||||
const trunk_block = "bworld:log";
|
||||
const leaves_block = "bworld:leaves";
|
||||
|
||||
// if (biome === "jungle") {
|
||||
// trunkBlock = "bworld:jungle_log";
|
||||
// leavesBlock = "bworld:jungle_leaves";
|
||||
// } else if (biome === "taiga") {
|
||||
// leavesBlock = "bworld:spruce_leaves";
|
||||
// }
|
||||
|
||||
for (let i = 0; i < height; i++) {
|
||||
dimension.add_block({ x, y: y + i, z, id: trunk_block });
|
||||
}
|
||||
|
||||
for (let dx = -2; dx <= 2; dx++) {
|
||||
for (let dz = -2; dz <= 2; dz++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy) <= 3) {
|
||||
dimension.add_block({
|
||||
x: x + dx,
|
||||
y: y + height + dy,
|
||||
z: z + dz,
|
||||
id: leaves_block,
|
||||
});
|
||||
}
|
||||
|
||||
dimension.add_block(world, { x, y, z, id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TREE_THRESHOLD: Record<Biome, number> = {
|
||||
forest: 0.5,
|
||||
jungle: 0.3,
|
||||
taiga: 0.6,
|
||||
plains: 0.95,
|
||||
desert: 1,
|
||||
tundra: 1,
|
||||
savanna: 0.65,
|
||||
swamp: 0.5,
|
||||
snow: 0.8,
|
||||
};
|
||||
|
||||
function should_place_tree(feature_noise: NoiseFunction2D, biome: Biome, x: number, z: number) {
|
||||
const n = feature_noise(x * 0.1, z * 0.1);
|
||||
return n > (TREE_THRESHOLD[biome] ?? 0.8);
|
||||
}
|
||||
|
||||
export function generate_chunk(dimension: Dimension, cx: number, cz: number, seed = "seed") {
|
||||
const height_noise = create_noise_2d(new Alea(seed + "_height"));
|
||||
const temp_noise = create_noise_2d(new Alea(seed + "_temp"));
|
||||
const moisture_noise = create_noise_2d(new Alea(seed + "_moisture"));
|
||||
const feature_noise = create_noise_2d(new Alea(seed + "_feature"));
|
||||
|
||||
const biome_scale = 0.003;
|
||||
const terrain_scale = 0.01;
|
||||
|
||||
const tree_map: boolean[][] = Array.from({ length: CHUNK_SIZE }, () => Array(CHUNK_SIZE).fill(false));
|
||||
|
||||
for (let x = 0; x < CHUNK_SIZE; x++) {
|
||||
for (let z = 0; z < CHUNK_SIZE; z++) {
|
||||
const wx = cx * CHUNK_SIZE + x;
|
||||
const wz = cz * CHUNK_SIZE + z;
|
||||
|
||||
const temp = temp_noise(wx * biome_scale, wz * biome_scale);
|
||||
const moisture = moisture_noise(wx * biome_scale, wz * biome_scale);
|
||||
const biome = get_biome(temp, moisture);
|
||||
|
||||
const height_noise_value = fractal_noise(height_noise, wx * terrain_scale, wz * terrain_scale);
|
||||
const base_height = (height_noise_value + 1) * 15 + 50;
|
||||
const height = get_terrain_height(base_height, biome, wx, wz, height_noise);
|
||||
|
||||
const surface_block = get_surface_block(biome);
|
||||
|
||||
for (let y = 0; y <= height; y++) {
|
||||
let block = "bworld:stone";
|
||||
if (y === height) {
|
||||
block = surface_block;
|
||||
} else if (y > height - 4) {
|
||||
block = "bworld:dirt";
|
||||
}
|
||||
|
||||
if (biome === "swamp" && y === height && Math.random() < 0.2) {
|
||||
block = "bworld:water";
|
||||
}
|
||||
|
||||
dimension.add_block({ x: wx, y, z: wz, id: block });
|
||||
}
|
||||
|
||||
if (should_place_tree(feature_noise, biome, wx, wz) && can_place_tree(tree_map, x, z)) {
|
||||
place_tree(dimension, wx, height + 1, wz, biome);
|
||||
tree_map[x][z] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user