import { Mat4 as Matrix, type Mat4 } from "../math/Mat4" import type { Vec2 } from "../math/Vec2" import type { Vec3 } from "../math/Vec3" import type { Camera } from "../scene/Camera" import { Sprite } from "../scene/Sprite" import { Framebuffer, type Framebuffer as Frame } from "./Framebuffer" import { Frustum } from "./Frustum" import type { DrawGroup, Material } from "./Material" import { Rasterizer } from "./Rasterizer" import type { RenderConfig } from "./RenderConfig" import { Sky, type SkyConfig } from "./Sky" import { Chunk, type Chunk as RenderChunk } from "./Chunk" export type Billboard = { position: Vec3 size: Vec2 material: Material } export type RenderPrototype = { readonly groups: readonly DrawGroup[] readonly radius: number readonly minY: number readonly maxY: number } export type RenderTransform = { x: number y: number z: number heading: number scale: number } export type RenderInstance = RenderTransform & { prototype: number } /** Clone-safe render projection of a live level. Contains no behavior callbacks or * game-content registries, so workers need only engine code. */ export type RenderScene = { readonly chunks: readonly RenderChunk[] readonly staticGroups: readonly DrawGroup[] readonly billboards: readonly Billboard[] readonly prototypes: readonly RenderPrototype[] readonly maxInstances: number readonly sky: SkyConfig } export namespace RenderScene { export function visibleChunks( scene: RenderScene, viewProjection: Mat4, ): number[] { const frustum = Frustum.fromViewProj(viewProjection) const visible: number[] = [] for (let i = 0; i < scene.chunks.length; i++) { const chunk = scene.chunks[i] if ( Frustum.intersectsAabb( frustum, chunk.minX, chunk.minY, chunk.minZ, chunk.maxX, chunk.maxY, chunk.maxZ, ) ) { visible.push(i) } } return visible } export function visibleInstances( scene: RenderScene, instances: RenderInstance[], viewProjection: Mat4, ): RenderInstance[] { const frustum = Frustum.fromViewProj(viewProjection) const visible: RenderInstance[] = [] for (const instance of instances) { const prototype = scene.prototypes[instance.prototype] if (prototype === undefined) { continue } const radius = prototype.radius * instance.scale if ( Frustum.intersectsAabb( frustum, instance.x - radius, instance.y + prototype.minY * instance.scale, instance.z - radius, instance.x + radius, instance.y + prototype.maxY * instance.scale, instance.z + radius, ) ) { visible.push(instance) } } return visible } export function renderBand( framebuffer: Frame, scene: RenderScene, camera: Camera, viewProjection: Mat4, visible: number[], instances: RenderInstance[], config: RenderConfig, skyStep: number, time: number, y0: number, y1: number, ): void { Sky.render(framebuffer, camera, scene.sky, time, skyStep, y0, y1) drawGroups(framebuffer, scene.staticGroups, viewProjection, config, y0, y1) for (const index of visible) { const chunk = scene.chunks[index] const groups = Chunk.isFar(chunk, camera.position, config.lodDistance) ? chunk.far : chunk.near drawGroups(framebuffer, groups, viewProjection, config, y0, y1) } for (const billboard of scene.billboards) { const sprite = { position: billboard.position, size: billboard.size, texture: billboard.material.texture, } Rasterizer.draw( framebuffer, Sprite.billboard(sprite, camera), billboard.material.texture, viewProjection, config, billboard.material.cull, y0, y1, ) } for (const instance of instances) { const prototype = scene.prototypes[instance.prototype] if (prototype === undefined) { continue } const modelViewProjection = Matrix.multiply( viewProjection, Matrix.compose( instance.x, instance.y, instance.z, instance.heading, instance.scale, ), ) drawGroups( framebuffer, prototype.groups, modelViewProjection, config, y0, y1, ) } Framebuffer.quantize(framebuffer, config, y0, y1) } export function triangleCount( scene: RenderScene, visible: number[], instances: RenderInstance[], eye: Vec3, lodDistance: number, ): number { let indices = groupIndices(scene.staticGroups) + scene.billboards.length * 6 for (const index of visible) { const chunk = scene.chunks[index] indices += groupIndices( Chunk.isFar(chunk, eye, lodDistance) ? chunk.far : chunk.near, ) } for (const instance of instances) { const prototype = scene.prototypes[instance.prototype] if (prototype !== undefined) { indices += groupIndices(prototype.groups) } } return (indices / 3) | 0 } function drawGroups( framebuffer: Frame, groups: readonly DrawGroup[], matrix: Mat4, config: RenderConfig, y0: number, y1: number, ): void { for (const group of groups) { Rasterizer.draw( framebuffer, group.mesh, group.material.texture, matrix, config, group.material.cull, y0, y1, ) } } function groupIndices(groups: readonly DrawGroup[]): number { let count = 0 for (const group of groups) { count += group.mesh.indices.length } return count } }