Files
bworld/client/rendering/level_renderer.ts
T
2026-09-26 11:38:32 -03:00

194 lines
5.8 KiB
TypeScript

import { TEXTURE_SIZE } from "$/common/constants.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";
import type { MultiPlayerGameMode } from "$/client/game_mode.ts";
import type { Entity } from "$/client/entity/entity.ts";
import { RemotePlayer } from "$/client/entity/remote_player.ts";
import { ItemEntity } from "$/client/entity/item_entity.ts";
import { render_item_entity } from "./item_renderer.ts";
import {
draw_terrain,
flush_batch,
push_back_face,
push_bottom_face,
push_box,
push_front_face,
push_left_face,
push_right_face,
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,
push_bottom_face,
push_front_face,
push_left_face,
push_right_face,
push_top_face,
];
// 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);
this.frustum.update(view_projection());
this.#visible.length = 0;
for (const chunk of level.chunks.values()) {
if (Object.keys(chunk.meshes).length > 0 && this.#in_view(chunk)) {
this.#visible.push(chunk);
}
}
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) {
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 = 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);
for (const { mesh } of chunks) {
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();
set_current_texture(white_tex!);
for (const entity of level.entities.values()) {
if (entity !== camera_entity && entity instanceof RemotePlayer) {
render_player(entity, partial_tick);
}
}
flush_batch();
set_current_texture(level.image.tex);
for (const entity of level.entities.values()) {
if (entity instanceof ItemEntity) {
render_item_entity(entity, partial_tick);
}
}
flush_batch();
}
// the cracks on the block being broken
render_destroy_progress(game_mode: MultiPlayerGameMode) {
const block = game_mode.destroy_pos;
if (!block) {
return;
}
const progress = Math.max(0, Math.min(1, game_mode.destroy_progress / game_mode.destroy_time));
const stage = Math.round(progress * 8);
if (Number.isNaN(stage)) {
return;
}
const tex = AssetManager.instance.get<Texture>("bworld:textures");
const region = get_sprite_region(`engine:break_${stage}`);
for (const push_face of BREAKING_FACES) {
push_face(
tex,
block.x,
block.y,
block.z,
region.x * TEXTURE_SIZE,
region.y * TEXTURE_SIZE,
TEXTURE_SIZE,
TEXTURE_SIZE,
);
}
}
}
// 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;
const { x, y, z } = player.render_position(partial_tick);
// body
push_box(x - 0.3, y, z - 0.15, 0.6, 1.3, 0.3, r, g, b);
// head
push_box(x - 0.25, y + 1.3, z - 0.25, 0.5, 0.5, 0.5, 0.95, 0.8, 0.65);
}