Optimize renderer

This commit is contained in:
2026-09-26 11:38:32 -03:00
parent 8b549162c4
commit 58afaac821
8 changed files with 424 additions and 72 deletions
+97 -27
View File
@@ -16,13 +16,22 @@ import {
TEXTURE_SIZE,
} from "$/common/constants.ts";
import type { Texture } from "../renderer/types.ts";
import { type FromChunkWorker, TERRAIN_VERTEX_FLOATS, type ToChunkWorker } from "./chunk_messages.ts";
import {
FACE_GROUPS,
type FaceGroup,
type FromChunkWorker,
TERRAIN_QUAD_BYTES,
TERRAIN_VERTEX_BYTES,
type ToChunkWorker,
UNALIGNED_GROUP,
} from "./chunk_messages.ts";
import { generate_raw_chunk, WorldgenSetup } from "$/common/generation.ts";
import { load_worldgen } from "$/common/worldgen_loader.ts";
import { default_block_value, get_state_value } from "$/common/utils.ts";
import { bake_model, block_variant, FACE_CORNERS, find_model, type ModelJson } from "$/common/block_models.ts";
import {
choose_sort_type,
FACE_AXIS,
FACE_NORMALS,
quad_indices,
quad_planes,
@@ -42,7 +51,6 @@ import {
type TexturesInfo = Record<string, SpriteRegion>;
const FLOATS_PER_QUAD = 4 * TERRAIN_VERTEX_FLOATS;
// keeps texture lookups off the sprite's edge
const UV_PAD = 0.5;
@@ -91,6 +99,8 @@ interface MeshQuad {
face: number;
cull: number;
flush: boolean;
// the face group it goes in, see FACE_GROUPS
group: number;
shade: number;
sprite: SpriteRegion;
// 4 weights per corner
@@ -132,7 +142,7 @@ self.onmessage = async (event: MessageEvent<ToChunkWorker>) => {
case "mesh": {
region.fill(message.chunks);
region.compute(light_tables);
const { solid, cutout, translucent } = make_chunk_mesh(
const { solid, cutout, translucent, min_y, max_y } = make_chunk_mesh(
message.chunk_x,
message.chunk_z,
message.chunks[4]!,
@@ -144,6 +154,8 @@ self.onmessage = async (event: MessageEvent<ToChunkWorker>) => {
chunk_x: message.chunk_x,
chunk_z: message.chunk_z,
version: message.version,
min_y,
max_y,
solid,
cutout,
translucent,
@@ -285,28 +297,57 @@ function should_flip() {
return light(0) + light(2) > light(1) + light(3);
}
function push_quad(vertices: Float32Array, i: number, quad: MeshQuad, x: number, y: number, z: number, alpha: number) {
// quads being built, in the terrain vertex format
class QuadBuffer {
count = 0;
bytes = new Uint8Array(TERRAIN_QUAD_BYTES * 64);
f32 = new Float32Array(this.bytes.buffer);
u16 = new Uint16Array(this.bytes.buffer);
// room for one more quad
reserve() {
if ((this.count + 1) * TERRAIN_QUAD_BYTES <= this.bytes.length) return;
const bytes = new Uint8Array(this.bytes.length * 2);
bytes.set(this.bytes);
this.bytes = bytes;
this.f32 = new Float32Array(bytes.buffer);
this.u16 = new Uint16Array(bytes.buffer);
}
}
// the lowest and highest y of any quad in the chunk being meshed
let mesh_min_y = Infinity;
let mesh_max_y = -Infinity;
function push_quad(buffer: QuadBuffer, quad: MeshQuad, x: number, y: number, z: number, alpha: number) {
buffer.reserve();
const { f32, u16, bytes } = buffer;
const sprite = quad.sprite;
// starting from the second corner moves the diagonal, the winding stays the same
const first = should_flip() ? 1 : 0;
const alpha_byte = Math.round(alpha * 255);
for (let k = 0; k < 4; k++) {
const corner = (first + k) & 3;
const brightness = quad.shade * corner_ao[corner];
vertices[i++] = x + quad.positions[corner * 3];
vertices[i++] = y + quad.positions[corner * 3 + 1];
vertices[i++] = z + quad.positions[corner * 3 + 2];
vertices[i++] = atlas_u(sprite, quad.uvs[corner * 2]);
vertices[i++] = atlas_v(sprite, quad.uvs[corner * 2 + 1]);
vertices[i++] = brightness;
vertices[i++] = brightness;
vertices[i++] = brightness;
vertices[i++] = alpha;
const byte = (buffer.count * 4 + k) * TERRAIN_VERTEX_BYTES;
const vy = y + quad.positions[corner * 3 + 1];
f32[byte / 4] = x + quad.positions[corner * 3];
f32[byte / 4 + 1] = vy;
f32[byte / 4 + 2] = z + quad.positions[corner * 3 + 2];
u16[byte / 2 + 6] = Math.round(atlas_u(sprite, quad.uvs[corner * 2]) * 65535);
u16[byte / 2 + 7] = Math.round(atlas_v(sprite, quad.uvs[corner * 2 + 1]) * 65535);
const brightness = Math.round(quad.shade * corner_ao[corner] * 255);
bytes[byte + 16] = brightness;
bytes[byte + 17] = brightness;
bytes[byte + 18] = brightness;
bytes[byte + 19] = alpha_byte;
// where to read the lightmap, block light across and sky light down
vertices[i++] = (corner_block[corner] + 0.5) / 16;
vertices[i++] = (corner_sky[corner] + 0.5) / 16;
bytes[byte + 20] = Math.round((corner_block[corner] + 0.5) / 16 * 255);
bytes[byte + 21] = Math.round((corner_sky[corner] + 0.5) / 16 * 255);
if (vy < mesh_min_y) mesh_min_y = vy;
if (vy > mesh_max_y) mesh_max_y = vy;
}
return i;
buffer.count += 1;
}
// pixels of a sprite to atlas coordinates, kept off the sprite's edge
@@ -340,6 +381,7 @@ function block_quads(value: number): MeshQuad[] {
face: quad.face,
cull: quad.cull,
flush: quad.flush,
group: quad.aligned ? quad.face : UNALIGNED_GROUP,
shade: quad.shade ? FACE_SHADE[quad.face] : 1,
sprite: textures_info[quad.texture] ?? textures_info["engine:missing"],
light_weights: light_weights(quad.face, quad.positions),
@@ -408,7 +450,13 @@ function light_quad(quad: MeshQuad, index: number, y: number, face_offsets: numb
// region has to be filled and lit first
// values is the middle chunk's blocks with their states, for models that change with them
function make_chunk_mesh(chunk_x: number, chunk_z: number, values: Uint32Array, camera: number[]) {
const layers = [SOLID, CUTOUT, TRANSLUCENT].map(() => ({ vertices: new Float32Array(4096), floats: 0 }));
// solid and cutout get a buffer per face group, translucent one for everything since it's sorted instead
const opaque = [SOLID, CUTOUT].map(() =>
Array.from({ length: FACE_GROUPS }, () => ({ buffer: new QuadBuffer(), min: Infinity, max: -Infinity }))
);
const translucent_quads = new QuadBuffer();
mesh_min_y = Infinity;
mesh_max_y = -Infinity;
// for sorting the translucent quads
let centers = new Float32Array(256);
let faces = new Uint8Array(256);
@@ -426,7 +474,6 @@ function make_chunk_mesh(chunk_x: number, chunk_z: number, values: Uint32Array,
const block_info = blocks_registry[block_nid];
const layer_id = block_layers[block_nid];
const layer = layers[layer_id];
const alpha = layer_id === TRANSLUCENT ? block_info.alpha ?? 1 : 1;
const wx = chunk_x * CHUNK_SIZE + x;
@@ -443,12 +490,20 @@ function make_chunk_mesh(chunk_x: number, chunk_z: number, values: Uint32Array,
}
light_quad(quad, index, y, face_offsets);
layer.vertices = ensure_capacity(layer.vertices, layer.floats + FLOATS_PER_QUAD);
layer.floats = push_quad(layer.vertices, layer.floats, quad, wx, y, wz, alpha);
if (layer_id === TRANSLUCENT) {
if (layer_id !== TRANSLUCENT) {
const group = opaque[layer_id][quad.group];
push_quad(group.buffer, quad, wx, y, wz, alpha);
if (quad.group !== UNALIGNED_GROUP) {
const axis = FACE_AXIS[quad.face];
const plane = (axis === 0 ? wx : axis === 1 ? y : wz) + quad.positions[axis];
if (plane < group.min) group.min = plane;
if (plane > group.max) group.max = plane;
}
} else {
push_quad(translucent_quads, quad, wx, y, wz, alpha);
// sorting treats every quad as facing along an axis, rotated ones too
const q = layer.floats / FLOATS_PER_QUAD - 1;
const q = translucent_quads.count - 1;
centers = ensure_capacity(centers, (q + 1) * 3);
faces = ensure_capacity(faces, q + 1);
const p = quad.positions;
@@ -462,16 +517,31 @@ function make_chunk_mesh(chunk_x: number, chunk_z: number, values: Uint32Array,
}
}
const [solid, cutout, translucent] = layers.map((layer) => ({
vertices: layer.vertices,
quad_count: layer.floats / FLOATS_PER_QUAD,
}));
// each layer's groups one after another, in one buffer
const [solid, cutout] = opaque.map((groups) => {
const quad_count = groups.reduce((sum, group) => sum + group.buffer.count, 0);
const vertices = new Uint8Array(quad_count * TERRAIN_QUAD_BYTES);
const ranges: FaceGroup[] = [];
let first = 0;
for (const { buffer, min, max } of groups) {
vertices.set(buffer.bytes.subarray(0, buffer.count * TERRAIN_QUAD_BYTES), first * TERRAIN_QUAD_BYTES);
ranges.push({ first, count: buffer.count, min, max });
first += buffer.count;
}
return { vertices, quad_count, groups: ranges };
});
const translucent = {
vertices: translucent_quads.bytes.slice(0, translucent_quads.count * TERRAIN_QUAD_BYTES),
quad_count: translucent_quads.count,
};
const quads = { centers, faces, count: translucent.quad_count };
const sort_type = choose_sort_type(quads);
const [camera_x, camera_y, camera_z] = camera;
return {
min_y: mesh_min_y === Infinity ? 0 : mesh_min_y,
max_y: mesh_max_y === -Infinity ? 0 : mesh_max_y,
solid,
cutout,
translucent: {