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
+135
View File
@@ -0,0 +1,135 @@
// the renderer skips chunks outside the view and faces pointing away from the camera, but must never skip one it
// would have shown
import { assert, assertEquals } from "@std/assert";
import { mat4 } from "gl-matrix";
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { Frustum } from "$/client/rendering/frustum.ts";
import { group_faces_camera } from "$/client/rendering/level_renderer.ts";
import { FACE_GROUPS, type LayerMesh, TERRAIN_VERTEX_BYTES } from "$/client/workers/chunk_messages.ts";
import { test_game } from "./helpers.ts";
// the matrix the renderer makes for a camera, see update_camera in client/renderer/core.ts
function camera_matrix(x: number, y: number, z: number, yaw: number, pitch: number) {
const proj = mat4.perspectiveZO(mat4.create(), Math.PI / 3, 16 / 9, 0.1, 1000);
const view = mat4.create();
mat4.rotateX(view, view, -pitch);
mat4.rotateY(view, view, -yaw);
mat4.translate(view, view, [-x, -y, -z]);
return mat4.multiply(mat4.create(), proj, view) as Float32Array;
}
Deno.test("the frustum keeps what's in front of the camera and drops what's behind or beside it", () => {
const frustum = new Frustum();
// yaw 0 looks toward -z
frustum.update(camera_matrix(0, 70, 0, 0, 0));
assert(frustum.intersects_box(-8, 60, -40, 8, 80, -24), "straight ahead");
assert(frustum.intersects_box(-1, 0, -1, 1, 256, 1), "the chunk the camera is in");
assert(!frustum.intersects_box(-8, 60, 24, 8, 80, 40), "behind");
assert(!frustum.intersects_box(200, 60, -40, 216, 80, -24), "far off to the side");
assert(!frustum.intersects_box(-8, 60, -1100, 8, 80, -1090), "past the far plane");
});
// real chunks from the chunk worker, meshed the way the client does it
async function mesh_chunks(radius: number) {
const { game } = await test_game("mods", undefined, "culling-seed");
const blocks = EverythingRegistry.get_registry<BlockRegistry>("blocks");
const block_ids: Record<string, number> = {};
blocks.forEach((b, nid) => block_ids[b.id] = nid);
const worker = new Worker(new URL("../client/workers/chunk_worker.ts", import.meta.url).href, { type: "module" });
// deno-lint-ignore no-explicit-any
const replies = new Map<string, (message: any) => void>();
worker.onmessage = (e) => {
const key = `${e.data.type} ${e.data.chunk_x} ${e.data.chunk_z}`;
replies.get(key)?.(e.data);
replies.delete(key);
};
// deno-lint-ignore no-explicit-any
const ask = (message: any, reply: string) =>
// deno-lint-ignore no-explicit-any
new Promise<any>((resolve) => {
replies.set(`${reply} ${message.chunk_x} ${message.chunk_z}`, resolve);
worker.postMessage(message);
});
worker.postMessage({
type: "init",
blocks_registry: JSON.parse(JSON.stringify(blocks)),
block_ids,
models: {},
textures_info: { "engine:missing": { x: 0, y: 0 } },
image: { width: 1024, height: 1024 },
worldgen_scripts: [],
ores: game.recipes.ores,
});
const generated = new Map<string, Uint32Array>();
for (let x = -radius - 1; x <= radius + 1; x++) {
for (let z = -radius - 1; z <= radius + 1; z++) {
const reply = await ask({ type: "generate", chunk_x: x, chunk_z: z, seed: "culling-seed" }, "generated");
generated.set(`${x},${z}`, reply.blocks);
}
}
const meshes = [];
for (let x = -radius; x <= radius; x++) {
for (let z = -radius; z <= radius; z++) {
const chunks = [];
for (let dz = -1; dz <= 1; dz++) {
for (let dx = -1; dx <= 1; dx++) chunks.push(generated.get(`${x + dx},${z + dz}`)!.slice());
}
meshes.push(
await ask({ type: "mesh", chunk_x: x, chunk_z: z, version: 1, chunks, camera: [0, 0, 0] }, "meshed"),
);
}
}
worker.terminate();
return meshes;
}
// each quad's corners, read back out of the vertex format
function quad_corners(mesh: LayerMesh, quad: number) {
const f32 = new Float32Array(mesh.vertices.buffer, mesh.vertices.byteOffset, mesh.vertices.byteLength / 4);
return [0, 1, 2, 3].map((k) => {
const i = ((quad * 4 + k) * TERRAIN_VERTEX_BYTES) / 4;
return [f32[i], f32[i + 1], f32[i + 2]];
});
}
Deno.test("face groups only skip quads that face away from the camera", async () => {
const meshes = await mesh_chunks(1);
const cameras = [[8, 70, 8], [-20, 120, 30], [24, 20, -5], [8, 300, 8], [0.5, 64.5, 15.5]];
let checked = 0;
for (const message of meshes) {
for (const layer of ["solid", "cutout"] as const) {
const mesh: LayerMesh = message[layer];
if (mesh.quad_count === 0) continue;
assertEquals(mesh.groups!.length, FACE_GROUPS);
assertEquals(mesh.groups!.reduce((sum, g) => sum + g.count, 0), mesh.quad_count);
for (const [cx, cy, cz] of cameras) {
for (const [g, group] of mesh.groups!.entries()) {
if (group_faces_camera(g, group.min, group.max, { x: cx, y: cy, z: cz })) continue;
// a skipped group: every quad in it faces away from the camera, or is edge on
for (let q = group.first; q < group.first + group.count; q++) {
const [a, b, , d] = quad_corners(mesh, q);
const e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
const e2 = [d[0] - a[0], d[1] - a[1], d[2] - a[2]];
const n = [
e1[1] * e2[2] - e1[2] * e2[1],
e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0],
];
const facing = n[0] * (cx - a[0]) + n[1] * (cy - a[1]) + n[2] * (cz - a[2]);
assert(facing <= 1e-4, `group ${g} skipped a quad facing the camera at ${cx},${cy},${cz}`);
checked++;
}
}
}
}
// the bounds hold every quad
for (const layer of ["solid", "cutout", "translucent"] as const) {
const mesh: LayerMesh = message[layer];
for (let q = 0; q < mesh.quad_count; q++) {
for (const [, y] of quad_corners(mesh, q)) assert(y >= message.min_y && y <= message.max_y);
}
}
}
assert(checked > 1000, `only checked ${checked} quads`);
});