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("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); }