169 lines
5.8 KiB
TypeScript
169 lines
5.8 KiB
TypeScript
import { Color } from "../engine/render/Color"
|
|
import type { CloudLayer, SkyConfig } from "../engine/render/Sky"
|
|
import type { Mesh } from "../engine/scene/Mesh"
|
|
|
|
type Corner = [number, number, number]
|
|
|
|
/** Axis-aligned solid. Blocks the player horizontally while their feet are
|
|
* below `top`; if `standable`, its `top` also counts as ground to land on. */
|
|
export type Aabb = {
|
|
minX: number
|
|
maxX: number
|
|
minZ: number
|
|
maxZ: number
|
|
top: number
|
|
standable: boolean
|
|
}
|
|
|
|
/** The playground: geometry split by texture, its collision solids, where the
|
|
* NPC stands, and the sky to draw behind it. */
|
|
export type Level = {
|
|
floor: Mesh
|
|
walls: Mesh
|
|
crate: Mesh
|
|
colliders: Aabb[]
|
|
npcPosition: { x: number; y: number; z: number }
|
|
sky: SkyConfig
|
|
}
|
|
|
|
const ARENA = 12
|
|
const WALL_HEIGHT = 4
|
|
const CRATE = { x: -2, z: -2, half: 1, top: 1 }
|
|
|
|
/** Triangle density: grid divisions per world unit, applied to every textured
|
|
* surface (floor, walls, crate). Higher = smaller triangles = each spans less
|
|
* depth = less affine texture swim, at the cost of more geometry; lower =
|
|
* chunkier, wilder PS1 warp (below ~0.5 the floor tips into the black-wedge
|
|
* degeneration). Because it scales with surface size, one value keeps the big
|
|
* floor and the little crate equally warp-free. Edit and Vite reloads. Pairs
|
|
* with the `ps1` preset's `perspectiveCorrect`, which fights the same error
|
|
* from the render side. */
|
|
const DIVISIONS_PER_UNIT = 1.3
|
|
|
|
/** The two cloud styles; swap which one the sky uses in `buildLevel`.
|
|
* `basicCumulus` is cheap flat puffs; `fancyCumulus` is the pricier
|
|
* heightfield-shaded, domain-warped version with faked volume. */
|
|
export const basicCumulus: CloudLayer = {
|
|
kind: "basic",
|
|
color: Color.rgb(248, 250, 255),
|
|
coverage: 0.5,
|
|
scale: 0.9,
|
|
speed: 0.5,
|
|
edge: 0.02,
|
|
}
|
|
|
|
export const fancyCumulus: CloudLayer = {
|
|
kind: "fancy",
|
|
color: Color.rgb(250, 251, 255),
|
|
coverage: 0.5,
|
|
scale: 0.6,
|
|
speed: 0.5,
|
|
edge: 0.02,
|
|
warp: 0.4,
|
|
relief: 7,
|
|
}
|
|
|
|
export function buildLevel(): Level {
|
|
const floor = mesh()
|
|
quadGrid(floor, [-ARENA, 0, -ARENA], [ARENA, 0, -ARENA], [ARENA, 0, ARENA], [-ARENA, 0, ARENA], 12, 12)
|
|
|
|
const walls = mesh()
|
|
const h = WALL_HEIGHT
|
|
// Inward-facing perimeter, no ceiling so the sky shows above.
|
|
quadGrid(walls, [-ARENA, 0, -ARENA], [ARENA, 0, -ARENA], [ARENA, h, -ARENA], [-ARENA, h, -ARENA], 12, 2.5)
|
|
quadGrid(walls, [ARENA, 0, ARENA], [-ARENA, 0, ARENA], [-ARENA, h, ARENA], [ARENA, h, ARENA], 12, 2.5)
|
|
quadGrid(walls, [ARENA, 0, -ARENA], [ARENA, 0, ARENA], [ARENA, h, ARENA], [ARENA, h, -ARENA], 12, 2.5)
|
|
quadGrid(walls, [-ARENA, 0, ARENA], [-ARENA, 0, -ARENA], [-ARENA, h, -ARENA], [-ARENA, h, ARENA], 12, 2.5)
|
|
|
|
const crate = mesh()
|
|
box(crate, CRATE.x, CRATE.z, CRATE.half, CRATE.top)
|
|
|
|
const colliders: Aabb[] = [
|
|
wall(-ARENA, ARENA, -ARENA, -ARENA + 1),
|
|
wall(-ARENA, ARENA, ARENA - 1, ARENA),
|
|
wall(ARENA - 1, ARENA, -ARENA, ARENA),
|
|
wall(-ARENA, -ARENA + 1, -ARENA, ARENA),
|
|
{
|
|
minX: CRATE.x - CRATE.half,
|
|
maxX: CRATE.x + CRATE.half,
|
|
minZ: CRATE.z - CRATE.half,
|
|
maxZ: CRATE.z + CRATE.half,
|
|
top: CRATE.top,
|
|
standable: true,
|
|
},
|
|
]
|
|
|
|
const sky: SkyConfig = {
|
|
zenith: Color.rgb(58, 108, 196),
|
|
horizon: Color.rgb(178, 198, 226),
|
|
sun: Color.rgb(255, 246, 214),
|
|
sunDir: { x: 0.3, y: 0.5, z: -0.8 },
|
|
sunSize: 0.04,
|
|
clouds: basicCumulus,
|
|
}
|
|
|
|
return { floor, walls, crate, colliders, npcPosition: { x: 2, y: 0, z: -1 }, sky }
|
|
}
|
|
|
|
function mesh(): Mesh {
|
|
return { vertices: [], indices: [] }
|
|
}
|
|
|
|
function wall(minX: number, maxX: number, minZ: number, maxZ: number): Aabb {
|
|
return { minX, maxX, minZ, maxZ, top: WALL_HEIGHT, standable: false }
|
|
}
|
|
|
|
/** Tessellate a quad into a grid sized by DIVISIONS_PER_UNIT, so triangle size
|
|
* (and thus affine warp) is consistent whatever the surface's scale. Corners
|
|
* run a (uv 0,0) -> b (us,0) -> c (us,vs) -> d (0,vs). */
|
|
function quadGrid(m: Mesh, a: Corner, b: Corner, c: Corner, d: Corner, us: number, vs: number): void {
|
|
const nu = divisions(a, b)
|
|
const nv = divisions(a, d)
|
|
const base = m.vertices.length
|
|
const row = nu + 1
|
|
for (let i = 0; i <= nv; i++) {
|
|
const t = i / nv
|
|
for (let j = 0; j <= nu; j++) {
|
|
const s = j / nu
|
|
const wa = (1 - s) * (1 - t)
|
|
const wb = s * (1 - t)
|
|
const wc = s * t
|
|
const wd = (1 - s) * t
|
|
m.vertices.push({
|
|
pos: {
|
|
x: a[0] * wa + b[0] * wb + c[0] * wc + d[0] * wd,
|
|
y: a[1] * wa + b[1] * wb + c[1] * wc + d[1] * wd,
|
|
z: a[2] * wa + b[2] * wb + c[2] * wc + d[2] * wd,
|
|
},
|
|
uv: { x: us * s, y: vs * t },
|
|
})
|
|
}
|
|
}
|
|
for (let i = 0; i < nv; i++) {
|
|
for (let j = 0; j < nu; j++) {
|
|
const p = base + i * row + j
|
|
m.indices.push(p, p + 1, p + row + 1, p, p + row + 1, p + row)
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Grid divisions along edge a->b, from its world length and the density. */
|
|
function divisions(a: Corner, b: Corner): number {
|
|
const length = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2])
|
|
return Math.max(1, Math.round(length * DIVISIONS_PER_UNIT))
|
|
}
|
|
|
|
/** A box centered at (cx, cz) on the floor: top face plus four sides, one uv
|
|
* tile per face. No bottom (never seen). Tessellated like every other surface,
|
|
* so it no longer warps up close. */
|
|
function box(m: Mesh, cx: number, cz: number, half: number, top: number): void {
|
|
const x0 = cx - half
|
|
const x1 = cx + half
|
|
const z0 = cz - half
|
|
const z1 = cz + half
|
|
quadGrid(m, [x0, top, z0], [x1, top, z0], [x1, top, z1], [x0, top, z1], 1, 1)
|
|
quadGrid(m, [x0, 0, z0], [x1, 0, z0], [x1, top, z0], [x0, top, z0], 1, 1)
|
|
quadGrid(m, [x1, 0, z1], [x0, 0, z1], [x0, top, z1], [x1, top, z1], 1, 1)
|
|
quadGrid(m, [x1, 0, z0], [x1, 0, z1], [x1, top, z1], [x1, top, z0], 1, 1)
|
|
quadGrid(m, [x0, 0, z1], [x0, 0, z0], [x0, top, z0], [x0, top, z1], 1, 1)
|
|
}
|