Optimize renderer
This commit is contained in:
@@ -118,7 +118,7 @@ async function build_assets() {
|
|||||||
|
|
||||||
async function build_client() {
|
async function build_client() {
|
||||||
const _result = await Deno.bundle({
|
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`,
|
outputDir: `${BUILD_FOLDER}/client`,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
minify: false,
|
minify: false,
|
||||||
|
|||||||
@@ -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<FromChunkWorker>) => 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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
+217
-48
@@ -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 { AIR, CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE, Faces, ID_MASK, VOID } from "../../common/constants.ts";
|
||||||
import { AssetManager } from "../assets.ts";
|
import { AssetManager } from "../assets.ts";
|
||||||
import { ClientWorld } from "../client_world.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 { ItemStack } from "../inventory.ts";
|
||||||
import { PlayerComponent } from "../player.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";
|
import { Camera } from "./camera.ts";
|
||||||
|
|
||||||
export interface BlockData<T = unknown> {
|
export interface BlockData<T = unknown> {
|
||||||
@@ -34,16 +35,25 @@ export interface Chunk {
|
|||||||
blocks_data: BlockData[];
|
blocks_data: BlockData[];
|
||||||
generated: boolean;
|
generated: boolean;
|
||||||
dirty: 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_buffer?: GPUBuffer;
|
||||||
opaque_vertex_count?: number;
|
opaque_vertex_count?: number;
|
||||||
transparent_vertex_buffer?: GPUBuffer;
|
transparent_vertex_buffer?: GPUBuffer;
|
||||||
transparent_vertex_count?: number;
|
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 {
|
export class Dimension extends Component {
|
||||||
world: ClientWorld;
|
world: ClientWorld;
|
||||||
image: Texture = AssetManager.instance.get("bworld:textures");
|
image: Texture = AssetManager.instance.get("bworld:textures");
|
||||||
chunks: Chunk[] = [];
|
chunks = new Map<number, Chunk>();
|
||||||
second_timer = 0;
|
second_timer = 0;
|
||||||
tick_timer = 0;
|
tick_timer = 0;
|
||||||
seed: string;
|
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
|
// blocks players changed from the generated terrain, per chunk, so they survive reloading chunks
|
||||||
changes = new Map<string, Map<string, BlockChange>>();
|
changes = new Map<string, Map<string, BlockChange>>();
|
||||||
|
|
||||||
|
workers: ChunkWorkerPool;
|
||||||
|
// chunks being generated by a worker, by chunk key
|
||||||
|
pending_generation = new Map<number, { x: number; z: number }>();
|
||||||
|
|
||||||
constructor(world: ClientWorld, seed = "seed") {
|
constructor(world: ClientWorld, seed = "seed") {
|
||||||
super();
|
super();
|
||||||
this.world = world;
|
this.world = world;
|
||||||
this.seed = seed;
|
this.seed = seed;
|
||||||
|
|
||||||
|
this.workers = new ChunkWorkerPool((message) => this.#on_worker_message(message));
|
||||||
|
|
||||||
|
const blocks_registry = EverythingRegistry.get_registry<BlockRegistry>("blocks");
|
||||||
|
const block_ids: Record<string, number> = {};
|
||||||
|
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) {
|
dispose() {
|
||||||
const chunk = {
|
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,
|
x,
|
||||||
z,
|
z,
|
||||||
blocks: new Uint32Array(CHUNK_AREA * CHUNK_HEIGHT),
|
blocks,
|
||||||
blocks_data: [],
|
blocks_data: [],
|
||||||
dirty: true,
|
dirty: true,
|
||||||
generated: false,
|
generated: false,
|
||||||
|
mesh_version: 0,
|
||||||
};
|
};
|
||||||
this.chunks.push(chunk);
|
this.chunks.set(chunk_key(x, z), chunk);
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_chunk(x: number, z: number) {
|
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) {
|
add_block(block: Block) {
|
||||||
@@ -253,23 +291,107 @@ export class Dimension extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load_chunk(cx: number, cz: number) {
|
is_generated(cx: number, cz: number) {
|
||||||
generate_chunk(this, cx, cz, this.seed);
|
return this.get_chunk(cx, cz)?.generated ?? false;
|
||||||
const chunk = this.get_chunk(cx, cz);
|
|
||||||
if (chunk) {
|
|
||||||
chunk.generated = true;
|
|
||||||
chunk.dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const neighbors = [
|
is_generating(cx: number, cz: number) {
|
||||||
[cx - 1, cz],
|
return this.pending_generation.has(chunk_key(cx, cz));
|
||||||
[cx + 1, cz],
|
}
|
||||||
[cx, cz - 1],
|
|
||||||
[cx, cz + 1],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [nx, nz] of neighbors) {
|
// generation happens in a worker, the chunk shows up in #on_generated
|
||||||
const neighbor = this.chunks.find((c) => c.x === nx && c.z === nz);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (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) {
|
if (neighbor && neighbor.generated) {
|
||||||
neighbor.dirty = true;
|
neighbor.dirty = true;
|
||||||
}
|
}
|
||||||
@@ -283,15 +405,36 @@ export class Dimension extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unload_chunk(cx: number, cz: number) {
|
#on_meshed(message: Extract<FromChunkWorker, { type: "meshed" }>) {
|
||||||
const chunk_i = this.chunks.findIndex((c) => c.x === cx && c.z === cz);
|
const chunk = this.get_chunk(message.chunk_x, message.chunk_z);
|
||||||
if (chunk_i === -1) {
|
// unloaded, or remeshed again since this was requested
|
||||||
console.warn("Tried unloading a chunk that doesn't exist");
|
if (!chunk || chunk.mesh_version !== message.version) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.delete_chunk_mesh(this.chunks[chunk_i]);
|
this.delete_chunk_mesh(chunk);
|
||||||
this.chunks.splice(chunk_i, 1);
|
|
||||||
|
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) {
|
delete_chunk_mesh(chunk: Chunk) {
|
||||||
@@ -374,31 +517,36 @@ export class Dimension extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create_padded_chunk(chunk: Chunk) {
|
create_padded_chunk(chunk: Chunk) {
|
||||||
const cx = chunk.x;
|
|
||||||
const cz = chunk.z;
|
|
||||||
|
|
||||||
const size = CHUNK_SIZE + 2;
|
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++) {
|
// look the 3x3 chunks up once instead of once per border block
|
||||||
for (let z = -1; z <= CHUNK_SIZE; z++) {
|
const around: (Uint32Array | undefined)[] = [];
|
||||||
for (let x = -1; x <= CHUNK_SIZE; x++) {
|
for (let dz = -1; dz <= 1; dz++) {
|
||||||
let block: number;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const px = x + 1;
|
for (let z = -1; z <= CHUNK_SIZE; z++) {
|
||||||
const pz = z + 1;
|
const dz = z < 0 ? -1 : z >= CHUNK_SIZE ? 1 : 0;
|
||||||
const pindex = y * size * size + pz * size + px;
|
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);
|
||||||
|
|
||||||
padded[pindex] = block;
|
if (!source) {
|
||||||
|
for (let y = 0; y < CHUNK_HEIGHT; y++) {
|
||||||
|
padded[y * layer + padded_index] = VOID;
|
||||||
|
}
|
||||||
|
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;
|
return padded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// functions cant be sent to workers
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
function strip_functions<T extends Record<string, any>>(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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export function start_game(world: ClientWorld) {
|
|||||||
world.clear_entities();
|
world.clear_entities();
|
||||||
|
|
||||||
const dimension = new Entity("dimension");
|
const dimension = new Entity("dimension");
|
||||||
|
world.dimension?.dispose();
|
||||||
world.dimension = new Dimension(world, world.connection?.seed);
|
world.dimension = new Dimension(world, world.connection?.seed);
|
||||||
for (const [x, y, z, id] of world.connection?.initial_changes ?? []) {
|
for (const [x, y, z, id] of world.connection?.initial_changes ?? []) {
|
||||||
world.dimension.record_change(x, y, z, id);
|
world.dimension.record_change(x, y, z, id);
|
||||||
|
|||||||
+36
-9
@@ -1,5 +1,10 @@
|
|||||||
import { Alea, create_noise_2d, create_noise_3d, NoiseFunction2D } from "@paulaboks/rng";
|
import { Alea, create_noise_2d, create_noise_3d, NoiseFunction2D, NoiseFunction3D } from "@paulaboks/rng";
|
||||||
import { CHUNK_SIZE, Dimension } from "./components/dimension.ts";
|
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 =
|
type Biome =
|
||||||
| "desert"
|
| "desert"
|
||||||
@@ -129,7 +134,7 @@ function can_place_tree(tree_map: boolean[][], local_x: number, local_z: number)
|
|||||||
return true;
|
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 height = Math.floor(rng.next() * 3) + (biome === "jungle" ? 8 : 4);
|
||||||
const trunk_block = "bworld:log";
|
const trunk_block = "bworld:log";
|
||||||
const leaves_block = "bworld:leaves";
|
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);
|
return n > (TREE_THRESHOLD[biome] ?? 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generate_chunk(dimension: Dimension, cx: number, cz: number, seed = "seed") {
|
interface SeedNoises {
|
||||||
const height_noise = create_noise_2d(new Alea(seed + "_height"));
|
height_noise: NoiseFunction2D;
|
||||||
const temp_noise = create_noise_2d(new Alea(seed + "_temp"));
|
temp_noise: NoiseFunction2D;
|
||||||
const moisture_noise = create_noise_2d(new Alea(seed + "_moisture"));
|
moisture_noise: NoiseFunction2D;
|
||||||
const feature_noise = create_noise_2d(new Alea(seed + "_feature"));
|
feature_noise: NoiseFunction2D;
|
||||||
const ore_noises = ORES.map((ore) => create_noise_3d(new Alea(seed + "_" + ore.id)));
|
ore_noises: NoiseFunction3D[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// building the permutation tables is expensive, only do it once per seed
|
||||||
|
const noise_cache = new Map<string, SeedNoises>();
|
||||||
|
|
||||||
|
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
|
// seeded per chunk so every client generates the exact same terrain
|
||||||
const rng = new Alea(`${seed}_chunk_${cx}_${cz}`);
|
const rng = new Alea(`${seed}_chunk_${cx}_${cz}`);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class DimensionLogicSystem extends System {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
handle_second(world: ClientWorld, dimension: Dimension) {
|
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) {
|
for (const tickable of chunk.blocks_data) {
|
||||||
const tile_info = EverythingRegistry.get<BlockRegistry>("blocks", tickable.id);
|
const tile_info = EverythingRegistry.get<BlockRegistry>("blocks", tickable.id);
|
||||||
if (tile_info && tile_info.on_second) {
|
if (tile_info && tile_info.on_second) {
|
||||||
@@ -31,7 +31,7 @@ export class DimensionLogicSystem extends System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handle_tick(world: ClientWorld, dimension: Dimension) {
|
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) {
|
for (const tickable of chunk.blocks_data) {
|
||||||
const tile_info = EverythingRegistry.get<BlockRegistry>("blocks", tickable.id);
|
const tile_info = EverythingRegistry.get<BlockRegistry>("blocks", tickable.id);
|
||||||
if (tile_info && tile_info.on_tick) {
|
if (tile_info && tile_info.on_tick) {
|
||||||
|
|||||||
@@ -1,74 +1,17 @@
|
|||||||
import { EverythingRegistry } from "$/common/everything_registry.ts";
|
|
||||||
import { Chunk, Dimension } from "$/client/components/dimension.ts";
|
import { Chunk, Dimension } from "$/client/components/dimension.ts";
|
||||||
import { Camera } from "$/client/components/camera.ts";
|
import { Camera } from "$/client/components/camera.ts";
|
||||||
import { AssetManager } from "$/client/assets.ts";
|
import { flush_buffer, set_current_texture } from "$/client/renderer/mod.ts";
|
||||||
import { create_vertex_buffer, flush_buffer, set_current_texture } from "$/client/renderer/mod.ts";
|
|
||||||
|
|
||||||
let gdimension: Dimension;
|
export function render_dimension(dimension: Dimension, _camera: Camera) {
|
||||||
|
dimension.request_meshes();
|
||||||
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<T extends Record<string, any>>(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;
|
|
||||||
|
|
||||||
set_current_texture(dimension.image.tex);
|
set_current_texture(dimension.image.tex);
|
||||||
|
|
||||||
for (const chunk of dimension.chunks) {
|
for (const chunk of dimension.chunks.values()) {
|
||||||
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) {
|
|
||||||
render_chunk_opaque(chunk);
|
render_chunk_opaque(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const chunk of dimension.chunks) {
|
for (const chunk of dimension.chunks.values()) {
|
||||||
render_chunk_transparent(chunk);
|
render_chunk_transparent(chunk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,27 +18,47 @@ export class WorldGenerationSystem extends System {
|
|||||||
const player_chunk_x = Math.floor(position.x / CHUNK_SIZE);
|
const player_chunk_x = Math.floor(position.x / CHUNK_SIZE);
|
||||||
const player_chunk_z = Math.floor(position.z / 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) {
|
// collect first, deleting from the map while iterating it skips entries
|
||||||
if (Math.abs(chunk.x - player_chunk_x) > render_distance) {
|
const to_unload = [];
|
||||||
|
for (const chunk of dimension.chunks.values()) {
|
||||||
|
if (out_of_range(chunk.x, chunk.z)) {
|
||||||
|
to_unload.push(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const chunk of to_unload) {
|
||||||
dimension.unload_chunk(chunk.x, chunk.z);
|
dimension.unload_chunk(chunk.x, chunk.z);
|
||||||
}
|
}
|
||||||
if (Math.abs(chunk.z - player_chunk_z) > render_distance) {
|
for (const pending of [...dimension.pending_generation.values()]) {
|
||||||
dimension.unload_chunk(chunk.x, chunk.z);
|
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) {
|
// dont queue up the whole area at once, so walking somewhere new gets the close chunks first
|
||||||
for (let j = player_chunk_z - render_distance; j <= player_chunk_z + render_distance; j += 1) {
|
const max_in_flight = dimension.workers.size * 2;
|
||||||
const maybe_chunk = dimension.chunks.find((chunk) => chunk.x === i && chunk.z === j);
|
if (dimension.pending_generation.size >= max_in_flight) {
|
||||||
if (!maybe_chunk || !maybe_chunk.generated) {
|
|
||||||
console.log("loading chunk", i, j);
|
|
||||||
dimension.load_chunk(i, j);
|
|
||||||
// only generate one chunk per frame
|
|
||||||
return;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string, number>;
|
||||||
|
textures_info: Record<string, SpriteRegion>;
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
import type { BlockRegistry } from "$/common/everything_registry.ts";
|
import type { BlockRegistry } from "$/common/everything_registry.ts";
|
||||||
import type { SpriteRegion } from "$/common/constants.ts";
|
import type { SpriteRegion } from "$/common/constants.ts";
|
||||||
import type { Texture } from "../renderer/types.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;
|
const pad = 0.5;
|
||||||
|
|
||||||
@@ -260,22 +262,88 @@ const FACE_PUSHING_FUNCTIONS = {
|
|||||||
right: push_right_face,
|
right: push_right_face,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
self.onmessage = (event) => {
|
let blocks_registry: BlockRegistry[] = [];
|
||||||
const { chunk_x, chunk_z, padded_chunk, blocks_registry, textures_info, image } = event.data;
|
let block_ids: Record<string, number> = {};
|
||||||
|
let textures_info: TexturesInfo = {};
|
||||||
|
let image: Texture;
|
||||||
|
|
||||||
|
self.onmessage = (event: MessageEvent<ToChunkWorker>) => {
|
||||||
|
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(
|
const [opaque_vertices, opaque_count, transparent_vertices, transparent_count] = make_chunk_mesh(
|
||||||
chunk_x,
|
message.chunk_x,
|
||||||
chunk_z,
|
message.chunk_z,
|
||||||
padded_chunk,
|
message.padded_chunk,
|
||||||
blocks_registry,
|
blocks_registry,
|
||||||
textures_info,
|
textures_info,
|
||||||
image,
|
image,
|
||||||
);
|
);
|
||||||
self.postMessage(
|
post(
|
||||||
{ opaque_vertices, opaque_count, transparent_vertices, transparent_count, chunk_x, chunk_z },
|
{
|
||||||
|
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],
|
[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,
|
||||||
|
seed,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
function make_chunk_mesh(
|
||||||
chunk_x: number,
|
chunk_x: number,
|
||||||
chunk_z: number,
|
chunk_z: number,
|
||||||
Reference in New Issue
Block a user