Optimize renderer
This commit is contained in:
@@ -6,8 +6,23 @@ import type { SortType } from "./translucent_sort.ts";
|
||||
|
||||
// messages between the main thread and the chunk workers
|
||||
|
||||
// position 3, uv 2, color 4 (directional shade and ambient occlusion, alpha), lightmap coordinates 2
|
||||
export const TERRAIN_VERTEX_FLOATS = 11;
|
||||
// a terrain vertex, 24 bytes: position as float32x3, atlas uv as unorm16x2, directional shade times ambient occlusion
|
||||
// and alpha as unorm8x4 (shade repeated in rgb), and lightmap coordinates as unorm8x2 plus two unused bytes
|
||||
export const TERRAIN_VERTEX_BYTES = 24;
|
||||
export const TERRAIN_QUAD_BYTES = 4 * TERRAIN_VERTEX_BYTES;
|
||||
|
||||
// solid and cutout quads are grouped by the way they face, so groups facing away from the camera can be skipped, like
|
||||
// sodium's block face culling. the groups follow FACE_NORMALS' order, then the quads that aren't axis aligned
|
||||
export const FACE_GROUPS = 7;
|
||||
export const UNALIGNED_GROUP = 6;
|
||||
|
||||
export interface FaceGroup {
|
||||
first: number;
|
||||
count: number;
|
||||
// the lowest and highest plane the group's quads lie on, along its axis
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export type ToChunkWorker =
|
||||
| {
|
||||
@@ -44,10 +59,12 @@ export type ToChunkWorker =
|
||||
camera: number[];
|
||||
};
|
||||
|
||||
// 4 vertices per quad
|
||||
// 4 vertices per quad, TERRAIN_VERTEX_BYTES each
|
||||
export interface LayerMesh {
|
||||
vertices: Float32Array;
|
||||
vertices: Uint8Array<ArrayBuffer>;
|
||||
quad_count: number;
|
||||
// solid and cutout only, see FACE_GROUPS. the quads are stored group after group
|
||||
groups?: FaceGroup[];
|
||||
}
|
||||
|
||||
export type FromChunkWorker =
|
||||
@@ -64,6 +81,9 @@ export type FromChunkWorker =
|
||||
chunk_x: number;
|
||||
chunk_z: number;
|
||||
version: number;
|
||||
// the lowest and highest y of the chunk's quads, for frustum culling
|
||||
min_y: number;
|
||||
max_y: number;
|
||||
solid: LayerMesh;
|
||||
cutout: LayerMesh;
|
||||
translucent: LayerMesh & {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user