Optimize renderer
This commit is contained in:
@@ -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
@@ -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" },
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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