diff --git a/build.ts b/build.ts index a559a5c..82b4796 100644 --- a/build.ts +++ b/build.ts @@ -118,7 +118,7 @@ async function build_assets() { async function build_client() { const _result = await Deno.bundle({ - entrypoints: ["./client/main.ts", "./client/workers/chunk_mesh_worker.ts"], + entrypoints: ["./client/main.ts", "./client/workers/chunk_worker.ts"], outputDir: `${BUILD_FOLDER}/client`, platform: "browser", minify: false, diff --git a/client/chunk_workers.ts b/client/chunk_workers.ts new file mode 100644 index 0000000..d82c3a2 --- /dev/null +++ b/client/chunk_workers.ts @@ -0,0 +1,39 @@ +import type { FromChunkWorker, ToChunkWorker } from "./workers/chunk_messages.ts"; + +// leave a core for the main thread, more than 4 doesnt help much +const POOL_SIZE = Math.max(1, Math.min(4, (navigator.hardwareConcurrency ?? 4) - 1)); + +export class ChunkWorkerPool { + readonly size = POOL_SIZE; + #workers: Worker[] = []; + #next = 0; + + constructor(on_message: (message: FromChunkWorker) => void) { + for (let i = 0; i < this.size; i += 1) { + const worker = new Worker(new URL("./workers/chunk_worker.js", import.meta.url), { type: "module" }); + worker.onmessage = (event: MessageEvent) => on_message(event.data); + worker.onerror = (event) => console.error("Chunk worker error:", event.message); + this.#workers.push(worker); + } + } + + broadcast(message: ToChunkWorker) { + for (const worker of this.#workers) { + worker.postMessage(message); + } + } + + // round robin, results can come back out of order + post(message: ToChunkWorker, transfer: Transferable[] = []) { + const worker = this.#workers[this.#next]; + this.#next = (this.#next + 1) % this.#workers.length; + worker.postMessage(message, transfer); + } + + terminate() { + for (const worker of this.#workers) { + worker.terminate(); + } + this.#workers = []; + } +} diff --git a/client/components/dimension.ts b/client/components/dimension.ts index d4c077f..505d858 100644 --- a/client/components/dimension.ts +++ b/client/components/dimension.ts @@ -4,10 +4,11 @@ import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry. import { AIR, CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE, Faces, ID_MASK, VOID } from "../../common/constants.ts"; import { AssetManager } from "../assets.ts"; import { ClientWorld } from "../client_world.ts"; -import { generate_chunk } from "../generation.ts"; +import { ChunkWorkerPool } from "../chunk_workers.ts"; +import type { FromChunkWorker } from "../workers/chunk_messages.ts"; import { ItemStack } from "../inventory.ts"; import { PlayerComponent } from "../player.ts"; -import { destroy_vertex_buffer, Texture } from "../renderer/mod.ts"; +import { create_vertex_buffer, destroy_vertex_buffer, Texture } from "../renderer/mod.ts"; import { Camera } from "./camera.ts"; export interface BlockData { @@ -34,16 +35,25 @@ export interface Chunk { blocks_data: BlockData[]; generated: boolean; dirty: boolean; + // bumped on every mesh request so late results from older requests get ignored + mesh_version: number; opaque_vertex_buffer?: GPUBuffer; opaque_vertex_count?: number; transparent_vertex_buffer?: GPUBuffer; transparent_vertex_count?: number; } +// numeric so looking chunks up doesnt allocate a string every time, fine for |x|, |z| < 32768 +export function chunk_key(x: number, z: number) { + return (x + 32768) * 65536 + (z + 32768); +} + +const NEIGHBOR_OFFSETS = [[-1, 0], [1, 0], [0, -1], [0, 1]] as const; + export class Dimension extends Component { world: ClientWorld; image: Texture = AssetManager.instance.get("bworld:textures"); - chunks: Chunk[] = []; + chunks = new Map(); second_timer = 0; tick_timer = 0; seed: string; @@ -51,27 +61,55 @@ export class Dimension extends Component { // blocks players changed from the generated terrain, per chunk, so they survive reloading chunks changes = new Map>(); + workers: ChunkWorkerPool; + // chunks being generated by a worker, by chunk key + pending_generation = new Map(); + constructor(world: ClientWorld, seed = "seed") { super(); this.world = world; this.seed = seed; + + this.workers = new ChunkWorkerPool((message) => this.#on_worker_message(message)); + + const blocks_registry = EverythingRegistry.get_registry("blocks"); + const block_ids: Record = {}; + blocks_registry.forEach((block, nid) => block_ids[block.id] = nid); + // sent once instead of with every mesh request + this.workers.broadcast({ + type: "init", + blocks_registry: strip_functions(blocks_registry), + block_ids, + textures_info: AssetManager.instance.get("bworld:textures_info"), + image: { width: this.image.width, height: this.image.height }, + }); } - add_chunk(x: number, z: number) { - const chunk = { + dispose() { + this.workers.terminate(); + for (const chunk of this.chunks.values()) { + this.delete_chunk_mesh(chunk); + } + this.chunks.clear(); + this.pending_generation.clear(); + } + + add_chunk(x: number, z: number, blocks: Uint32Array = new Uint32Array(CHUNK_AREA * CHUNK_HEIGHT)) { + const chunk: Chunk = { x, z, - blocks: new Uint32Array(CHUNK_AREA * CHUNK_HEIGHT), + blocks, blocks_data: [], dirty: true, generated: false, + mesh_version: 0, }; - this.chunks.push(chunk); + this.chunks.set(chunk_key(x, z), chunk); return chunk; } get_chunk(x: number, z: number) { - return this.chunks.find((chunk) => chunk.x === x && chunk.z === z); + return this.chunks.get(chunk_key(x, z)); } add_block(block: Block) { @@ -253,23 +291,107 @@ export class Dimension extends Component { } } - load_chunk(cx: number, cz: number) { - generate_chunk(this, cx, cz, this.seed); - const chunk = this.get_chunk(cx, cz); - if (chunk) { - chunk.generated = true; - chunk.dirty = true; + is_generated(cx: number, cz: number) { + return this.get_chunk(cx, cz)?.generated ?? false; + } + + is_generating(cx: number, cz: number) { + return this.pending_generation.has(chunk_key(cx, cz)); + } + + // generation happens in a worker, the chunk shows up in #on_generated + request_chunk(cx: number, cz: number) { + const key = chunk_key(cx, cz); + if (this.pending_generation.has(key) || this.chunks.get(key)?.generated) { + return; + } + this.pending_generation.set(key, { x: cx, z: cz }); + this.workers.post({ type: "generate", chunk_x: cx, chunk_z: cz, seed: this.seed }); + } + + cancel_chunk_request(cx: number, cz: number) { + this.pending_generation.delete(chunk_key(cx, cz)); + } + + unload_chunk(cx: number, cz: number) { + const key = chunk_key(cx, cz); + const chunk = this.chunks.get(key); + if (!chunk) { + return; + } + this.delete_chunk_mesh(chunk); + this.chunks.delete(key); + } + + // a chunk is only meshed once all its neighbors exist, otherwise its border faces would be wrong + // and it would have to be meshed again as each neighbor loads + can_mesh(chunk: Chunk) { + if (!chunk.generated) { + return false; + } + for (const [dx, dz] of NEIGHBOR_OFFSETS) { + if (!this.is_generated(chunk.x + dx, chunk.z + dz)) { + return false; + } + } + return true; + } + + // sends every dirty chunk that can be meshed to the workers + request_meshes() { + for (const chunk of this.chunks.values()) { + if (!chunk.dirty || !this.can_mesh(chunk)) { + continue; + } + chunk.dirty = false; + chunk.mesh_version += 1; + const padded_chunk = this.create_padded_chunk(chunk); + this.workers.post({ + type: "mesh", + chunk_x: chunk.x, + chunk_z: chunk.z, + version: chunk.mesh_version, + padded_chunk, + }, [padded_chunk.buffer]); + } + } + + #on_worker_message(message: FromChunkWorker) { + if (message.type === "generated") { + this.#on_generated(message.chunk_x, message.chunk_z, message.blocks, message.spills); + } else { + this.#on_meshed(message); + } + } + + #on_generated(cx: number, cz: number, blocks: Uint32Array, spills: Int32Array) { + const key = chunk_key(cx, cz); + // unloaded or cancelled while the worker was busy + if (!this.pending_generation.delete(key)) { + return; } - const neighbors = [ - [cx - 1, cz], - [cx + 1, cz], - [cx, cz - 1], - [cx, cz + 1], - ]; + let chunk = this.chunks.get(key); + if (chunk) { + // a placeholder made by a neighbor's tree, keep its blocks where generation left air + const existing = chunk.blocks; + for (let i = 0; i < blocks.length; i++) { + if (blocks[i] !== AIR) { + existing[i] = blocks[i]; + } + } + } else { + chunk = this.add_chunk(cx, cz, blocks); + } + chunk.generated = true; + chunk.dirty = true; - for (const [nx, nz] of neighbors) { - const neighbor = this.chunks.find((c) => c.x === nx && c.z === nz); + for (let i = 0; i < spills.length; i += 4) { + this.#set_block_raw(spills[i], spills[i + 1], spills[i + 2], spills[i + 3]); + } + + for (const [dx, dz] of NEIGHBOR_OFFSETS) { + const neighbor = this.get_chunk(cx + dx, cz + dz); if (neighbor && neighbor.generated) { neighbor.dirty = true; } @@ -283,15 +405,36 @@ export class Dimension extends Component { } } - unload_chunk(cx: number, cz: number) { - const chunk_i = this.chunks.findIndex((c) => c.x === cx && c.z === cz); - if (chunk_i === -1) { - console.warn("Tried unloading a chunk that doesn't exist"); + #on_meshed(message: Extract) { + const chunk = this.get_chunk(message.chunk_x, message.chunk_z); + // unloaded, or remeshed again since this was requested + if (!chunk || chunk.mesh_version !== message.version) { return; } - this.delete_chunk_mesh(this.chunks[chunk_i]); - this.chunks.splice(chunk_i, 1); + this.delete_chunk_mesh(chunk); + + chunk.opaque_vertex_buffer = create_vertex_buffer(message.opaque_vertices.subarray(0, message.opaque_count)); + chunk.opaque_vertex_count = message.opaque_count / 9; + + chunk.transparent_vertex_buffer = create_vertex_buffer( + message.transparent_vertices.subarray(0, message.transparent_count), + ); + chunk.transparent_vertex_count = message.transparent_count / 9; + } + + // sets a block from generation, no hooks, makes a placeholder chunk like add_block does + #set_block_raw(x: number, y: number, z: number, nid: number) { + if (y < 0 || y >= CHUNK_HEIGHT) { + return; + } + const chunk_x = Math.floor(x / CHUNK_SIZE); + const chunk_z = Math.floor(z / CHUNK_SIZE); + const chunk = this.get_chunk(chunk_x, chunk_z) ?? this.add_chunk(chunk_x, chunk_z); + const lx = x - chunk_x * CHUNK_SIZE; + const lz = z - chunk_z * CHUNK_SIZE; + chunk.blocks[y * CHUNK_AREA + lz * CHUNK_SIZE + lx] = nid; + chunk.dirty = true; } delete_chunk_mesh(chunk: Chunk) { @@ -374,31 +517,36 @@ export class Dimension extends Component { } create_padded_chunk(chunk: Chunk) { - const cx = chunk.x; - const cz = chunk.z; - const size = CHUNK_SIZE + 2; - const padded = new Uint32Array(size * size * CHUNK_HEIGHT); + const layer = size * size; + const padded = new Uint32Array(layer * CHUNK_HEIGHT); - for (let y = 0; y < CHUNK_HEIGHT; y++) { - for (let z = -1; z <= CHUNK_SIZE; z++) { - for (let x = -1; x <= CHUNK_SIZE; x++) { - let block: number; + // look the 3x3 chunks up once instead of once per border block + const around: (Uint32Array | undefined)[] = []; + for (let dz = -1; dz <= 1; dz++) { + for (let dx = -1; dx <= 1; dx++) { + around.push(this.get_chunk(chunk.x + dx, chunk.z + dz)?.blocks); + } + } - if (x >= 0 && x < CHUNK_SIZE && z >= 0 && z < CHUNK_SIZE) { - const index = y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x; - block = chunk.blocks[index] & ID_MASK; - } else { - const wx = cx * CHUNK_SIZE + x; - const wz = cz * CHUNK_SIZE + z; - block = this.get_block(wx, y, wz) ?? AIR; + for (let z = -1; z <= CHUNK_SIZE; z++) { + const dz = z < 0 ? -1 : z >= CHUNK_SIZE ? 1 : 0; + const lz = z - dz * CHUNK_SIZE; + for (let x = -1; x <= CHUNK_SIZE; x++) { + const dx = x < 0 ? -1 : x >= CHUNK_SIZE ? 1 : 0; + const lx = x - dx * CHUNK_SIZE; + const source = around[(dz + 1) * 3 + (dx + 1)]; + const source_index = lz * CHUNK_SIZE + lx; + const padded_index = (z + 1) * size + (x + 1); + + if (!source) { + for (let y = 0; y < CHUNK_HEIGHT; y++) { + padded[y * layer + padded_index] = VOID; } - - const px = x + 1; - const pz = z + 1; - const pindex = y * size * size + pz * size + px; - - padded[pindex] = block; + continue; + } + for (let y = 0; y < CHUNK_HEIGHT; y++) { + padded[y * layer + padded_index] = source[y * CHUNK_AREA + source_index] & ID_MASK; } } } @@ -406,3 +554,24 @@ export class Dimension extends Component { return padded; } } + +// functions cant be sent to workers +// deno-lint-ignore no-explicit-any +function strip_functions>(obj: T): T { + // deno-lint-ignore no-explicit-any + const out: any = Array.isArray(obj) ? [] : {}; + + for (const k in obj) { + const v = obj[k]; + + if (typeof v === "function") continue; + + if (v && typeof v === "object") { + out[k] = strip_functions(v); + } else { + out[k] = v; + } + } + + return out; +} diff --git a/client/game.ts b/client/game.ts index 91e7de5..bdd54c0 100644 --- a/client/game.ts +++ b/client/game.ts @@ -12,6 +12,7 @@ export function start_game(world: ClientWorld) { world.clear_entities(); const dimension = new Entity("dimension"); + world.dimension?.dispose(); world.dimension = new Dimension(world, world.connection?.seed); for (const [x, y, z, id] of world.connection?.initial_changes ?? []) { world.dimension.record_change(x, y, z, id); diff --git a/client/generation.ts b/client/generation.ts index 5f0f71b..a628a15 100644 --- a/client/generation.ts +++ b/client/generation.ts @@ -1,5 +1,10 @@ -import { Alea, create_noise_2d, create_noise_3d, NoiseFunction2D } from "@paulaboks/rng"; -import { CHUNK_SIZE, Dimension } from "./components/dimension.ts"; +import { Alea, create_noise_2d, create_noise_3d, NoiseFunction2D, NoiseFunction3D } from "@paulaboks/rng"; +import { CHUNK_SIZE } from "$/common/constants.ts"; + +// generation runs in a worker now, so it only needs somewhere to put blocks +export interface BlockSink { + add_block(block: { x: number; y: number; z: number; id: string }): void; +} type Biome = | "desert" @@ -129,7 +134,7 @@ function can_place_tree(tree_map: boolean[][], local_x: number, local_z: number) return true; } -function place_tree(dimension: Dimension, rng: Alea, x: number, y: number, z: number, biome: Biome) { +function place_tree(dimension: BlockSink, rng: Alea, x: number, y: number, z: number, biome: Biome) { const height = Math.floor(rng.next() * 3) + (biome === "jungle" ? 8 : 4); const trunk_block = "bworld:log"; const leaves_block = "bworld:leaves"; @@ -171,12 +176,34 @@ function should_place_tree(feature_noise: NoiseFunction2D, biome: Biome, x: numb 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 ore_noises = ORES.map((ore) => create_noise_3d(new Alea(seed + "_" + ore.id))); +interface SeedNoises { + height_noise: NoiseFunction2D; + temp_noise: NoiseFunction2D; + moisture_noise: NoiseFunction2D; + feature_noise: NoiseFunction2D; + ore_noises: NoiseFunction3D[]; +} + +// building the permutation tables is expensive, only do it once per seed +const noise_cache = new Map(); + +function get_noises(seed: string): SeedNoises { + let noises = noise_cache.get(seed); + if (!noises) { + noises = { + height_noise: create_noise_2d(new Alea(seed + "_height")), + temp_noise: create_noise_2d(new Alea(seed + "_temp")), + moisture_noise: create_noise_2d(new Alea(seed + "_moisture")), + feature_noise: create_noise_2d(new Alea(seed + "_feature")), + ore_noises: ORES.map((ore) => create_noise_3d(new Alea(seed + "_" + ore.id))), + }; + noise_cache.set(seed, noises); + } + return noises; +} + +export function generate_chunk(dimension: BlockSink, cx: number, cz: number, seed = "seed") { + const { height_noise, temp_noise, moisture_noise, feature_noise, ore_noises } = get_noises(seed); // seeded per chunk so every client generates the exact same terrain const rng = new Alea(`${seed}_chunk_${cx}_${cz}`); diff --git a/client/systems/dimension_logic.ts b/client/systems/dimension_logic.ts index 7053f3a..9e615e2 100644 --- a/client/systems/dimension_logic.ts +++ b/client/systems/dimension_logic.ts @@ -19,7 +19,7 @@ export class DimensionLogicSystem extends System { } } handle_second(world: ClientWorld, dimension: Dimension) { - for (const chunk of dimension.chunks) { + for (const chunk of dimension.chunks.values()) { for (const tickable of chunk.blocks_data) { const tile_info = EverythingRegistry.get("blocks", tickable.id); if (tile_info && tile_info.on_second) { @@ -31,7 +31,7 @@ export class DimensionLogicSystem extends System { } handle_tick(world: ClientWorld, dimension: Dimension) { - for (const chunk of dimension.chunks) { + for (const chunk of dimension.chunks.values()) { for (const tickable of chunk.blocks_data) { const tile_info = EverythingRegistry.get("blocks", tickable.id); if (tile_info && tile_info.on_tick) { diff --git a/client/systems/rendering/dimension.ts b/client/systems/rendering/dimension.ts index 35e3e48..9baae37 100644 --- a/client/systems/rendering/dimension.ts +++ b/client/systems/rendering/dimension.ts @@ -1,74 +1,17 @@ -import { EverythingRegistry } from "$/common/everything_registry.ts"; import { Chunk, Dimension } from "$/client/components/dimension.ts"; import { Camera } from "$/client/components/camera.ts"; -import { AssetManager } from "$/client/assets.ts"; -import { create_vertex_buffer, flush_buffer, set_current_texture } from "$/client/renderer/mod.ts"; +import { flush_buffer, set_current_texture } from "$/client/renderer/mod.ts"; -let gdimension: Dimension; - -const worker = new Worker(new URL("./workers/chunk_mesh_worker.js", import.meta.url), { - type: "module", -}); - -worker.onmessage = (event) => { - const { opaque_vertices, opaque_count, transparent_vertices, transparent_count, chunk_x, chunk_z } = event.data; - const chunk = gdimension.get_chunk(chunk_x, chunk_z); - if (!chunk) { - console.error("bad"); - return; - } - gdimension.delete_chunk_mesh(chunk); - chunk.dirty = false; - - chunk.opaque_vertex_buffer = create_vertex_buffer(opaque_vertices.subarray(0, opaque_count)); - chunk.opaque_vertex_count = opaque_count / 9; - - chunk.transparent_vertex_buffer = create_vertex_buffer(transparent_vertices.subarray(0, transparent_count)); - chunk.transparent_vertex_count = transparent_count / 9; -}; - -function strip_functions>(obj: T) { - const out: any = {}; - - for (const k in obj) { - const v = obj[k]; - - if (typeof v === "function") continue; - - if (v && typeof v === "object") { - out[k] = strip_functions(v); - } else { - out[k] = v; - } - } - - return out; -} - -export function render_dimension(dimension: Dimension, camera: Camera) { - gdimension = dimension; +export function render_dimension(dimension: Dimension, _camera: Camera) { + dimension.request_meshes(); set_current_texture(dimension.image.tex); - for (const chunk of dimension.chunks) { - if (chunk.dirty && chunk.generated) { - const padded_chunk = dimension.create_padded_chunk(chunk); - worker.postMessage({ - chunk_x: chunk.x, - chunk_z: chunk.z, - padded_chunk, - blocks_registry: strip_functions(EverythingRegistry.get_registry("blocks")), - textures_info: AssetManager.instance.get("bworld:textures_info"), - image: { width: dimension.image.width, height: dimension.image.height }, - }, [padded_chunk.buffer]); - chunk.dirty = false; - } - } - for (const chunk of dimension.chunks) { + for (const chunk of dimension.chunks.values()) { render_chunk_opaque(chunk); } - for (const chunk of dimension.chunks) { + for (const chunk of dimension.chunks.values()) { render_chunk_transparent(chunk); } } diff --git a/client/systems/world_generation_system.ts b/client/systems/world_generation_system.ts index 8663c0d..a32172c 100644 --- a/client/systems/world_generation_system.ts +++ b/client/systems/world_generation_system.ts @@ -18,27 +18,47 @@ export class WorldGenerationSystem extends System { const player_chunk_x = Math.floor(position.x / CHUNK_SIZE); const player_chunk_z = Math.floor(position.z / CHUNK_SIZE); - const render_distance = player_component.render_distance; + // one extra ring gets generated so the edge of the render distance has neighbors to mesh against + const load_distance = player_component.render_distance + 1; + const out_of_range = (x: number, z: number) => + Math.max(Math.abs(x - player_chunk_x), Math.abs(z - player_chunk_z)) > load_distance; - for (const chunk of dimension.chunks) { - if (Math.abs(chunk.x - player_chunk_x) > render_distance) { - dimension.unload_chunk(chunk.x, chunk.z); + // collect first, deleting from the map while iterating it skips entries + const to_unload = []; + for (const chunk of dimension.chunks.values()) { + if (out_of_range(chunk.x, chunk.z)) { + to_unload.push(chunk); } - if (Math.abs(chunk.z - player_chunk_z) > render_distance) { - dimension.unload_chunk(chunk.x, chunk.z); + } + for (const chunk of to_unload) { + dimension.unload_chunk(chunk.x, chunk.z); + } + for (const pending of [...dimension.pending_generation.values()]) { + if (out_of_range(pending.x, pending.z)) { + dimension.cancel_chunk_request(pending.x, pending.z); } } - for (let i = player_chunk_x - render_distance; i <= player_chunk_x + render_distance; i += 1) { - for (let j = player_chunk_z - render_distance; j <= player_chunk_z + render_distance; j += 1) { - const maybe_chunk = dimension.chunks.find((chunk) => chunk.x === i && chunk.z === j); - if (!maybe_chunk || !maybe_chunk.generated) { - console.log("loading chunk", i, j); - dimension.load_chunk(i, j); - // only generate one chunk per frame - return; + // dont queue up the whole area at once, so walking somewhere new gets the close chunks first + const max_in_flight = dimension.workers.size * 2; + if (dimension.pending_generation.size >= max_in_flight) { + return; + } + + const missing: { x: number; z: number; distance: number }[] = []; + for (let x = player_chunk_x - load_distance; x <= player_chunk_x + load_distance; x += 1) { + for (let z = player_chunk_z - load_distance; z <= player_chunk_z + load_distance; z += 1) { + if (!dimension.is_generated(x, z) && !dimension.is_generating(x, z)) { + const dx = x - player_chunk_x; + const dz = z - player_chunk_z; + missing.push({ x, z, distance: dx * dx + dz * dz }); } } } + missing.sort((a, b) => a.distance - b.distance); + + for (const chunk of missing.slice(0, max_in_flight - dimension.pending_generation.size)) { + dimension.request_chunk(chunk.x, chunk.z); + } } } diff --git a/client/workers/chunk_messages.ts b/client/workers/chunk_messages.ts new file mode 100644 index 0000000..cdc1480 --- /dev/null +++ b/client/workers/chunk_messages.ts @@ -0,0 +1,36 @@ +import type { BlockRegistry } from "$/common/everything_registry.ts"; +import type { SpriteRegion } from "$/common/constants.ts"; + +// messages between the main thread and the chunk workers + +export type ToChunkWorker = + | { + type: "init"; + // the blocks registry without its functions, indexed by numeric id + blocks_registry: BlockRegistry[]; + block_ids: Record; + textures_info: Record; + image: { width: number; height: number }; + } + | { type: "generate"; chunk_x: number; chunk_z: number; seed: string } + | { type: "mesh"; chunk_x: number; chunk_z: number; version: number; padded_chunk: Uint32Array }; + +export type FromChunkWorker = + | { + type: "generated"; + chunk_x: number; + chunk_z: number; + blocks: Uint32Array; + // blocks that landed in other chunks (tree leaves), flattened as x, y, z, numeric id + spills: Int32Array; + } + | { + type: "meshed"; + chunk_x: number; + chunk_z: number; + version: number; + opaque_vertices: Float32Array; + opaque_count: number; + transparent_vertices: Float32Array; + transparent_count: number; + }; diff --git a/client/workers/chunk_mesh_worker.ts b/client/workers/chunk_worker.ts similarity index 81% rename from client/workers/chunk_mesh_worker.ts rename to client/workers/chunk_worker.ts index 0a598d5..f05a6af 100644 --- a/client/workers/chunk_mesh_worker.ts +++ b/client/workers/chunk_worker.ts @@ -3,6 +3,8 @@ import type { BlockRegistry } from "$/common/everything_registry.ts"; import type { SpriteRegion } from "$/common/constants.ts"; import type { Texture } from "../renderer/types.ts"; +import type { FromChunkWorker, ToChunkWorker } from "./chunk_messages.ts"; +import { generate_chunk } from "../generation.ts"; const pad = 0.5; @@ -260,21 +262,87 @@ const FACE_PUSHING_FUNCTIONS = { right: push_right_face, } as const; -self.onmessage = (event) => { - const { chunk_x, chunk_z, padded_chunk, blocks_registry, textures_info, image } = event.data; - const [opaque_vertices, opaque_count, transparent_vertices, transparent_count] = make_chunk_mesh( +let blocks_registry: BlockRegistry[] = []; +let block_ids: Record = {}; +let textures_info: TexturesInfo = {}; +let image: Texture; + +self.onmessage = (event: MessageEvent) => { + const message = event.data; + switch (message.type) { + case "init": + blocks_registry = message.blocks_registry; + block_ids = message.block_ids; + textures_info = message.textures_info; + image = message.image as Texture; + break; + case "generate": + generate(message.chunk_x, message.chunk_z, message.seed); + break; + case "mesh": { + const [opaque_vertices, opaque_count, transparent_vertices, transparent_count] = make_chunk_mesh( + message.chunk_x, + message.chunk_z, + message.padded_chunk, + blocks_registry, + textures_info, + image, + ); + post( + { + type: "meshed", + chunk_x: message.chunk_x, + chunk_z: message.chunk_z, + version: message.version, + opaque_vertices, + opaque_count, + transparent_vertices, + transparent_count, + }, + [opaque_vertices.buffer, transparent_vertices.buffer], + ); + break; + } + } +}; + +function post(message: FromChunkWorker, transfer: Transferable[]) { + self.postMessage(message, transfer); +} + +function generate(chunk_x: number, chunk_z: number, seed: string) { + const blocks = new Uint32Array(CHUNK_SIZE * CHUNK_SIZE * CHUNK_HEIGHT); + const spills: number[] = []; + + generate_chunk( + { + add_block(block) { + const nid = block_ids[block.id]; + if (nid === undefined || block.y < 0 || block.y >= CHUNK_HEIGHT) { + return; + } + const block_chunk_x = Math.floor(block.x / CHUNK_SIZE); + const block_chunk_z = Math.floor(block.z / CHUNK_SIZE); + if (block_chunk_x !== chunk_x || block_chunk_z !== chunk_z) { + spills.push(block.x, block.y, block.z, nid); + return; + } + const lx = block.x - chunk_x * CHUNK_SIZE; + const lz = block.z - chunk_z * CHUNK_SIZE; + blocks[block.y * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx] = nid; + }, + }, chunk_x, chunk_z, - padded_chunk, - blocks_registry, - textures_info, - image, + seed, ); - self.postMessage( - { opaque_vertices, opaque_count, transparent_vertices, transparent_count, chunk_x, chunk_z }, - [opaque_vertices.buffer, transparent_vertices.buffer], - ); -}; + + const spills_array = new Int32Array(spills); + post({ type: "generated", chunk_x, chunk_z, blocks, spills: spills_array }, [ + blocks.buffer, + spills_array.buffer, + ]); +} function make_chunk_mesh( chunk_x: number,