Optimize renderer
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
// what the camera can see: the six planes around it, taken from its view projection matrix (gl-matrix's column major
|
||||
// order, with webgpu's 0 to 1 depth). a box is out of view when it's entirely behind one of them
|
||||
export class Frustum {
|
||||
// a, b, c, d per plane, where a * x + b * y + c * z + d >= 0 is the inside
|
||||
#planes = new Float32Array(24);
|
||||
|
||||
update(m: Readonly<Float32Array>) {
|
||||
const planes = this.#planes;
|
||||
const set = (i: number, a: number, b: number, c: number, d: number) => {
|
||||
planes[i * 4] = a;
|
||||
planes[i * 4 + 1] = b;
|
||||
planes[i * 4 + 2] = c;
|
||||
planes[i * 4 + 3] = d;
|
||||
};
|
||||
// row r of the matrix is m[r], m[4 + r], m[8 + r], m[12 + r]
|
||||
set(0, m[3] + m[0], m[7] + m[4], m[11] + m[8], m[15] + m[12]); // left
|
||||
set(1, m[3] - m[0], m[7] - m[4], m[11] - m[8], m[15] - m[12]); // right
|
||||
set(2, m[3] + m[1], m[7] + m[5], m[11] + m[9], m[15] + m[13]); // bottom
|
||||
set(3, m[3] - m[1], m[7] - m[5], m[11] - m[9], m[15] - m[13]); // top
|
||||
set(4, m[2], m[6], m[10], m[14]); // near, depth 0
|
||||
set(5, m[3] - m[2], m[7] - m[6], m[11] - m[10], m[15] - m[14]); // far
|
||||
}
|
||||
|
||||
// whether any of the box can be seen. may say yes for boxes just outside a corner, never no for one inside
|
||||
intersects_box(min_x: number, min_y: number, min_z: number, max_x: number, max_y: number, max_z: number) {
|
||||
const planes = this.#planes;
|
||||
for (let i = 0; i < 24; i += 4) {
|
||||
const a = planes[i];
|
||||
const b = planes[i + 1];
|
||||
const c = planes[i + 2];
|
||||
// the box's corner furthest along the plane's normal
|
||||
const x = a > 0 ? max_x : min_x;
|
||||
const y = b > 0 ? max_y : min_y;
|
||||
const z = c > 0 ? max_z : min_z;
|
||||
if (a * x + b * y + c * z + planes[i + 3] < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TEXTURE_SIZE } from "$/common/constants.ts";
|
||||
import { CHUNK_SIZE, ClientLevel } from "$/client/level/client_level.ts";
|
||||
import { type Chunk, CHUNK_SIZE, type ChunkMesh, ClientLevel } from "$/client/level/client_level.ts";
|
||||
import { Camera } from "$/client/camera.ts";
|
||||
import { AssetManager } from "$/client/assets.ts";
|
||||
import { get_sprite_region } from "$/client/sprites.ts";
|
||||
@@ -20,8 +20,13 @@ import {
|
||||
push_top_face,
|
||||
set_current_texture,
|
||||
Texture,
|
||||
view_projection,
|
||||
white_tex,
|
||||
} from "$/client/renderer/mod.ts";
|
||||
import type { RenderLayer } from "$/common/everything_registry.ts";
|
||||
import { FACE_GROUPS, UNALIGNED_GROUP } from "$/client/workers/chunk_messages.ts";
|
||||
import { FACE_AXIS, FACE_NORMALS } from "$/client/workers/translucent_sort.ts";
|
||||
import { Frustum } from "./frustum.ts";
|
||||
|
||||
const BREAKING_FACES = [
|
||||
push_back_face,
|
||||
@@ -34,48 +39,59 @@ const BREAKING_FACES = [
|
||||
|
||||
// draws the level, like minecraft's LevelRenderer: terrain in layers, block breaking and entities
|
||||
export class LevelRenderer {
|
||||
frustum = new Frustum();
|
||||
// chunks in view this frame, worked out once for all the layers
|
||||
#visible: Chunk[] = [];
|
||||
|
||||
// solid and cutout terrain, drawn before entities
|
||||
render_opaque(level: ClientLevel, camera: Camera) {
|
||||
level.request_meshes(camera);
|
||||
level.update_translucent_sorting(camera);
|
||||
|
||||
set_current_texture(level.image.tex);
|
||||
|
||||
this.frustum.update(view_projection());
|
||||
this.#visible.length = 0;
|
||||
for (const chunk of level.chunks.values()) {
|
||||
const mesh = chunk.meshes.solid;
|
||||
if (mesh) {
|
||||
draw_terrain("solid", mesh.vertex_buffer, mesh.quad_count);
|
||||
if (Object.keys(chunk.meshes).length > 0 && this.#in_view(chunk)) {
|
||||
this.#visible.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
for (const chunk of level.chunks.values()) {
|
||||
const mesh = chunk.meshes.cutout;
|
||||
if (mesh) {
|
||||
draw_terrain("cutout", mesh.vertex_buffer, mesh.quad_count);
|
||||
set_current_texture(level.image.tex);
|
||||
|
||||
for (const layer of ["solid", "cutout"] as const) {
|
||||
for (const chunk of this.#visible) {
|
||||
const mesh = chunk.meshes[layer];
|
||||
if (mesh) {
|
||||
draw_facing_camera(layer, mesh, camera);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// translucent terrain, drawn after entities so they show through water and glass.
|
||||
// chunks go back to front, and each chunk's quads are already sorted back to front
|
||||
render_translucent(level: ClientLevel, camera: Camera) {
|
||||
render_translucent(_level: ClientLevel, camera: Camera) {
|
||||
const distance_sq = (x: number, z: number) => {
|
||||
const dx = (x + 0.5) * CHUNK_SIZE - camera.x;
|
||||
const dz = (z + 0.5) * CHUNK_SIZE - camera.z;
|
||||
return dx * dx + dz * dz;
|
||||
};
|
||||
const chunks = [...level.chunks.values()]
|
||||
const chunks = this.#visible
|
||||
.filter((chunk) => chunk.meshes.translucent)
|
||||
.map((chunk) => ({ mesh: chunk.meshes.translucent!, distance: distance_sq(chunk.x, chunk.z) }))
|
||||
.sort((a, b) => b.distance - a.distance);
|
||||
|
||||
set_current_texture(level.image.tex);
|
||||
|
||||
for (const { mesh } of chunks) {
|
||||
draw_terrain("translucent", mesh.vertex_buffer, mesh.quad_count, mesh.index_buffer);
|
||||
draw_terrain("translucent", mesh.vertex_buffer, 0, mesh.quad_count, mesh.index_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
#in_view(chunk: Chunk) {
|
||||
const x = chunk.x * CHUNK_SIZE;
|
||||
const z = chunk.z * CHUNK_SIZE;
|
||||
return this.frustum.intersects_box(x, chunk.min_y, z, x + CHUNK_SIZE, chunk.max_y, z + CHUNK_SIZE);
|
||||
}
|
||||
|
||||
// every entity but the one the camera is in
|
||||
render_entities(level: ClientLevel, camera_entity: Entity, partial_tick: number) {
|
||||
flush_batch();
|
||||
@@ -127,6 +143,44 @@ export class LevelRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// only the face groups that can face the camera: quads facing +x can only be seen from beyond the lowest x plane any
|
||||
// of them lie on. neighboring groups that are both drawn go in one draw call
|
||||
function draw_facing_camera(layer: RenderLayer, mesh: ChunkMesh, camera: Camera) {
|
||||
const groups = mesh.groups;
|
||||
if (!groups) {
|
||||
draw_terrain(layer, mesh.vertex_buffer, 0, mesh.quad_count);
|
||||
return;
|
||||
}
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
for (let g = 0; g < FACE_GROUPS; g++) {
|
||||
const group = groups[g];
|
||||
if (group.count === 0 || !group_faces_camera(g, group.min, group.max, camera)) {
|
||||
continue;
|
||||
}
|
||||
if (group.first !== end) {
|
||||
draw_terrain(layer, mesh.vertex_buffer, start, end - start);
|
||||
start = group.first;
|
||||
}
|
||||
end = group.first + group.count;
|
||||
}
|
||||
draw_terrain(layer, mesh.vertex_buffer, start, end - start);
|
||||
}
|
||||
|
||||
export function group_faces_camera(
|
||||
group: number,
|
||||
min: number,
|
||||
max: number,
|
||||
camera: { x: number; y: number; z: number },
|
||||
) {
|
||||
if (group === UNALIGNED_GROUP) {
|
||||
return true;
|
||||
}
|
||||
const axis = FACE_AXIS[group];
|
||||
const position = axis === 0 ? camera.x : axis === 1 ? camera.y : camera.z;
|
||||
return FACE_NORMALS[group][axis] > 0 ? position > min : position < max;
|
||||
}
|
||||
|
||||
// a box body and head in the player's color
|
||||
function render_player(player: RemotePlayer, partial_tick: number) {
|
||||
const [r, g, b] = player.color;
|
||||
|
||||
Reference in New Issue
Block a user