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
+12 -8
View File
@@ -12,7 +12,7 @@ import { AIR, CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE, Faces, ID_MASK, VOID } from
import { AssetManager } from "../assets.ts";
import { ChunkWorkerPool } from "../chunk_workers.ts";
import { worldgen_mods } from "../mods.ts";
import { type FromChunkWorker, TERRAIN_VERTEX_FLOATS } from "../workers/chunk_messages.ts";
import type { FaceGroup, FromChunkWorker } from "../workers/chunk_messages.ts";
import { create_index_buffer, create_vertex_buffer, destroy_buffer, Texture } from "../renderer/mod.ts";
import { crosses_planes } from "../workers/translucent_sort.ts";
import { Camera } from "../camera.ts";
@@ -37,6 +37,9 @@ export interface Chunk {
// bumped on every mesh request so late results from older requests get ignored
mesh_version: number;
meshes: Partial<Record<RenderLayer, ChunkMesh>>;
// the lowest and highest y of its meshes, for frustum culling
min_y: number;
max_y: number;
// only for translucent meshes that have to be sorted again as the camera moves
translucent_sort?: TranslucentSort;
// blocks its generation put in neighboring chunks (leaves), as x, y, z, numeric id. kept so a neighbor that
@@ -49,6 +52,8 @@ export interface ChunkMesh {
quad_count: number;
// the translucent layer's quads sorted back to front, the others are drawn in order
index_buffer?: GPUBuffer;
// solid and cutout quads by the way they face, see FACE_GROUPS in chunk_messages.ts
groups?: FaceGroup[];
}
interface TranslucentSort {
@@ -63,8 +68,6 @@ interface TranslucentSort {
applied: number;
}
const FLOATS_PER_QUAD = 4 * TERRAIN_VERTEX_FLOATS;
export { chunk_key };
// the block being looked at and which face of it
@@ -199,6 +202,8 @@ export class ClientLevel {
generated: false,
mesh_version: 0,
meshes: {},
min_y: 0,
max_y: 0,
};
this.chunks.set(chunk_key(x, z), chunk);
return chunk;
@@ -548,16 +553,15 @@ export class ClientLevel {
}
this.delete_chunk_mesh(chunk);
chunk.min_y = message.min_y;
chunk.max_y = message.max_y;
for (const layer of RENDER_LAYERS) {
const { vertices, quad_count } = message[layer];
const { vertices, quad_count, groups } = message[layer];
if (quad_count === 0) {
continue;
}
chunk.meshes[layer] = {
vertex_buffer: create_vertex_buffer(vertices.subarray(0, quad_count * FLOATS_PER_QUAD)),
quad_count,
};
chunk.meshes[layer] = { vertex_buffer: create_vertex_buffer(vertices), quad_count, groups };
}
const translucent = message.translucent;
+36 -13
View File
@@ -1,7 +1,7 @@
import type { Camera } from "../camera.ts";
import { mat4 } from "gl-matrix";
import type { RenderLayer } from "$/common/everything_registry.ts";
import { TERRAIN_VERTEX_FLOATS } from "../workers/chunk_messages.ts";
import { TERRAIN_VERTEX_BYTES } from "../workers/chunk_messages.ts";
export let device: GPUDevice;
export let canvas: HTMLCanvasElement;
@@ -375,10 +375,16 @@ export function flush_batch() {
vert_index = 0;
}
// draws a chunk mesh made of quads (4 vertices each). without an index buffer the quads are drawn in order
// what's bound for terrain draws in the current pass, so drawing hundreds of chunks doesn't bind the same things
// again for each one. cleared whenever the pipeline changes
let terrain_binds: { slot: number; texture: GPUTexture; index_buffer: GPUBuffer } | undefined;
// draws quads first to first + quad_count of a chunk mesh (4 vertices each). without an index buffer the quads are
// drawn in order
export function draw_terrain(
layer: RenderLayer,
vertex_buffer: GPUBuffer,
first: number,
quad_count: number,
index_buffer?: GPUBuffer,
) {
@@ -386,7 +392,7 @@ export function draw_terrain(
return;
}
if (!index_buffer) {
ensure_quad_indices(quad_count);
ensure_quad_indices(first + quad_count);
index_buffer = quad_index_buffer!;
}
@@ -395,17 +401,33 @@ export function draw_terrain(
if (current_pipeline !== pipeline) {
render_pass.setPipeline(pipeline);
current_pipeline = pipeline;
terrain_binds = undefined;
}
render_pass.setBindGroup(0, uniform_bind_group, [uniform_slot * UNIFORM_SLOT_SIZE]);
render_pass.setBindGroup(1, get_texture_bind_group(current_texture));
render_pass.setBindGroup(2, lightmap_bind_group);
if (!terrain_binds) {
render_pass.setBindGroup(2, lightmap_bind_group);
}
if (terrain_binds?.slot !== uniform_slot) {
render_pass.setBindGroup(0, uniform_bind_group, [uniform_slot * UNIFORM_SLOT_SIZE]);
}
if (terrain_binds?.texture !== current_texture) {
render_pass.setBindGroup(1, get_texture_bind_group(current_texture));
}
if (terrain_binds?.index_buffer !== index_buffer) {
render_pass.setIndexBuffer(index_buffer, "uint32");
}
terrain_binds = { slot: uniform_slot, texture: current_texture, index_buffer };
render_pass.setVertexBuffer(0, vertex_buffer);
render_pass.setIndexBuffer(index_buffer, "uint32");
render_pass.drawIndexed(quad_count * 6);
render_pass.drawIndexed(quad_count * 6, 1, first * 6);
}
export function create_vertex_buffer(vertices: Float32Array): GPUBuffer {
// the camera's view and projection, for frustum culling. only meaningful in 3d mode
export function view_projection(): Readonly<Float32Array> {
return mvp as Float32Array;
}
export function create_vertex_buffer(vertices: Float32Array<ArrayBuffer> | Uint8Array<ArrayBuffer>): GPUBuffer {
const buffer = device.createBuffer({
size: Math.max(4, vertices.byteLength),
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
@@ -598,6 +620,7 @@ function ensure_pass(): GPURenderPassEncoder {
pending_color_clear = false;
pending_depth_clear = false;
current_pipeline = undefined;
terrain_binds = undefined;
apply_scissor(pass);
return pass;
@@ -762,12 +785,12 @@ function create_pipelines() {
module: terrain_module,
entryPoint: "vs_terrain",
buffers: [{
arrayStride: TERRAIN_VERTEX_FLOATS * 4,
arrayStride: TERRAIN_VERTEX_BYTES,
attributes: [
{ shaderLocation: 0, offset: 0, format: "float32x3" },
{ shaderLocation: 1, offset: 12, format: "float32x2" },
{ shaderLocation: 2, offset: 20, format: "float32x4" },
{ shaderLocation: 3, offset: 36, format: "float32x2" },
{ shaderLocation: 1, offset: 12, format: "unorm16x2" },
{ shaderLocation: 2, offset: 16, format: "unorm8x4" },
{ shaderLocation: 3, offset: 20, format: "unorm8x2" },
],
}],
};
+41
View File
@@ -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;
}
}
+69 -15
View File
@@ -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;
+24 -4
View File
@@ -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 & {
+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: {