Optimize renderer
This commit is contained in:
+218
-49
@@ -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<T = unknown> {
|
||||
@@ -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<number, Chunk>();
|
||||
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<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") {
|
||||
super();
|
||||
this.world = world;
|
||||
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) {
|
||||
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<FromChunkWorker, { type: "meshed" }>) {
|
||||
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<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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user