Optimize renderer
This commit is contained in:
@@ -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`);
|
||||
});
|
||||
Reference in New Issue
Block a user