140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
import { TEXTURE_SIZE } from "$/common/constants.ts";
|
|
import { CHUNK_SIZE, 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,
|
|
white_tex,
|
|
} from "$/client/renderer/mod.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 {
|
|
// 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);
|
|
|
|
for (const chunk of level.chunks.values()) {
|
|
const mesh = chunk.meshes.solid;
|
|
if (mesh) {
|
|
draw_terrain("solid", mesh.vertex_buffer, mesh.quad_count);
|
|
}
|
|
}
|
|
|
|
for (const chunk of level.chunks.values()) {
|
|
const mesh = chunk.meshes.cutout;
|
|
if (mesh) {
|
|
draw_terrain("cutout", mesh.vertex_buffer, mesh.quad_count);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = [...level.chunks.values()]
|
|
.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);
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|