meat/engine/render/ChunkBuilder.ts

123 lines
3.4 KiB
TypeScript

import { STRIDE, type Mesh } from "../scene/Mesh"
import type { Chunk, Bounds3 } from "./Chunk"
import type { DrawGroup, Material } from "./Material"
export type MeshBatch = {
mesh: (material: Material) => Mesh
use: (material: Material, mesh: Mesh) => void
}
export type ChunkItem = {
position: { x: number; z: number }
bakeNear: (batch: MeshBatch) => void
bakeFar?: (batch: MeshBatch) => void
}
export type ChunkCell = {
x0: number
z0: number
x1: number
z1: number
}
export type ChunkBuilder = {
minX: number
minZ: number
maxX: number
maxZ: number
columns: number
rows: number
bakeCell: (near: MeshBatch, far: MeshBatch, cell: ChunkCell) => void
}
export namespace ChunkBuilder {
export function build(config: ChunkBuilder, items: ChunkItem[]): Chunk[] {
const width = (config.maxX - config.minX) / config.columns
const depth = (config.maxZ - config.minZ) / config.rows
const chunks: Chunk[] = []
for (let column = 0; column < config.columns; column++) {
const x0 = config.minX + column * width
const x1 = x0 + width
for (let row = 0; row < config.rows; row++) {
const z0 = config.minZ + row * depth
const z1 = z0 + depth
const nearMeshes = new Map<Material, Mesh>()
const farMeshes = new Map<Material, Mesh>()
const near = batch(nearMeshes)
const far = batch(farMeshes)
config.bakeCell(near, far, { x0, z0, x1, z1 })
for (const item of items) {
if (inCell(item.position, x0, z0, x1, z1)) {
item.bakeNear(near)
item.bakeFar?.(far)
}
}
const nearGroups = groups(nearMeshes)
const farGroups = groups(farMeshes)
const box = bounds([...nearMeshes.values(), ...farMeshes.values()])
if (box !== null) {
chunks.push({ ...box, near: nearGroups, far: farGroups })
}
}
}
return chunks
}
function batch(meshes: Map<Material, Mesh>): MeshBatch {
return {
mesh(material) {
let mesh = meshes.get(material)
if (mesh === undefined) {
mesh = { verts: [], indices: [] }
meshes.set(material, mesh)
}
return mesh
},
use(material, mesh) {
meshes.set(material, mesh)
},
}
}
function groups(meshes: Map<Material, Mesh>): DrawGroup[] {
const result: DrawGroup[] = []
for (const [material, mesh] of meshes) {
if (mesh.indices.length > 0) {
result.push({ mesh, material })
}
}
return result
}
function inCell(
position: { x: number; z: number },
x0: number,
z0: number,
x1: number,
z1: number,
): boolean {
return (
position.x >= x0 && position.x < x1 && position.z >= z0 && position.z < z1
)
}
function bounds(meshes: Mesh[]): Bounds3 | null {
let minX = Infinity
let minY = Infinity
let minZ = Infinity
let maxX = -Infinity
let maxY = -Infinity
let maxZ = -Infinity
for (const mesh of meshes) {
for (let i = 0; i < mesh.verts.length; i += STRIDE) {
minX = Math.min(minX, mesh.verts[i])
minY = Math.min(minY, mesh.verts[i + 1])
minZ = Math.min(minZ, mesh.verts[i + 2])
maxX = Math.max(maxX, mesh.verts[i])
maxY = Math.max(maxY, mesh.verts[i + 1])
maxZ = Math.max(maxZ, mesh.verts[i + 2])
}
}
return maxX < minX ? null : { minX, minY, minZ, maxX, maxY, maxZ }
}
}