feat: trees

This commit is contained in:
Dan Finch 2026-08-04 13:53:59 +02:00
parent 61389f091c
commit 9aa0ec6a3f
10 changed files with 311 additions and 10 deletions

View file

@ -1,13 +1,19 @@
import type { Texture } from "../engine/render/Texture"
import barkUrl from "../assets/bark.png"
import crateUrl from "../assets/crate.png"
import floorUrl from "../assets/floor.png"
import grassUrl from "../assets/grass.png"
import leafUrl from "../assets/leaf.png"
import needleUrl from "../assets/needle.png"
import npcUrl from "../assets/npc.png"
import wallUrl from "../assets/wall.png"
export type Textures = {
floor: Texture
grass: Texture
bark: Texture
leaf: Texture
needle: Texture
wall: Texture
crate: Texture
npc: Texture
@ -15,14 +21,17 @@ export type Textures = {
/** Load every game texture up front. Call once before starting the loop. */
export async function loadTextures(): Promise<Textures> {
const [floor, grass, wall, crate, npc] = await Promise.all([
const [floor, grass, bark, leaf, needle, wall, crate, npc] = await Promise.all([
loadTexture(floorUrl),
loadTexture(grassUrl),
loadTexture(barkUrl),
loadTexture(leafUrl),
loadTexture(needleUrl),
loadTexture(wallUrl),
loadTexture(crateUrl),
loadTexture(npcUrl),
])
return { floor, grass, wall, crate, npc }
return { floor, grass, bark, leaf, needle, wall, crate, npc }
}
function loadTexture(url: string): Promise<Texture> {

View file

@ -2,6 +2,7 @@ import { Color } from "../engine/render/Color"
import type { CloudLayer, SkyConfig } from "../engine/render/Sky"
import type { Mesh } from "../engine/scene/Mesh"
import { Terrain } from "../engine/scene/Terrain"
import { Tree } from "../engine/scene/Tree"
type Corner = [number, number, number]
@ -24,6 +25,12 @@ export type Level = {
walls: Mesh
crate: Mesh
ground: Mesh
/** All tree trunks + branches (bark texture). */
trunks: Mesh
/** Oak canopies (leaf texture) and spruce foliage (needle texture), split so
* each takes its own texture in one draw call. */
oakFoliage: Mesh
spruceFoliage: Mesh
colliders: Aabb[]
npcPosition: { x: number; y: number; z: number }
terrain: Terrain
@ -57,6 +64,13 @@ const TERRAIN: Terrain = {
peakStart: 0.45,
}
/** Forest: how many trees to scatter on the grass, and the seed for their
* placement/kind/growth. Trees ring the room out to `TREE_REACH` of the world;
* each rolls oak-or-spruce and a growth 0..1 (sapling .. full grown). */
const TREE_COUNT = 500
const TREE_SEED = 0x5EED
const TREE_REACH = 0.6
/** Outdoor ground mesh resolution. A fixed grid over the whole world, so cell
* size (and cost) is set here, not by the world's size: bigger `outer` gives
* chunkier terrain, not more triangles. `GROUND_UV` sets texture tiles/unit. */
@ -136,7 +150,53 @@ export function buildLevel(): Level {
const npcPosition = { x: 2, y: 0, z: -1 }
return { floor, walls, crate, ground, colliders, npcPosition, terrain: TERRAIN, sky }
// Scatter a forest on the grass; appends into the tree meshes + trunk colliders.
const trunks = mesh()
const oakFoliage = mesh()
const spruceFoliage = mesh()
scatterTrees(trunks, oakFoliage, spruceFoliage, colliders)
return { floor, walls, crate, ground, trunks, oakFoliage, spruceFoliage, colliders, npcPosition, terrain: TERRAIN, sky }
}
/** Place `TREE_COUNT` trees around the room on walkable grass: each sits on the
* terrain, rolls oak/spruce and a growth stage, and (once past sapling size)
* drops a trunk collider so you can't walk through it. */
function scatterTrees(trunks: Mesh, oakFoliage: Mesh, spruceFoliage: Mesh, colliders: Aabb[]): void {
const rand = mulberry(TREE_SEED)
const maxDist = TERRAIN.outer * TREE_REACH
let placed = 0
for (let guard = 0; placed < TREE_COUNT && guard < TREE_COUNT * 20; guard++) {
const angle = rand() * Math.PI * 2
const dist = ARENA + 5 + rand() * (maxDist - ARENA - 5)
const x = Math.cos(angle) * dist
const z = Math.sin(angle) * dist
// Stay out of the room clearing and its flat rim.
if (Math.max(Math.abs(x), Math.abs(z)) < TERRAIN.inner + 3) {
continue
}
const kind = rand() < 0.5 ? "oak" : "spruce"
const growth = 0.08 + rand() * 0.92
const position = { x, y: Terrain.height(TERRAIN, x, z), z }
Tree.build({ kind, position, growth, seed: (rand() * 0xFFFFFFFF) | 0 }, trunks, kind === "oak" ? oakFoliage : spruceFoliage)
// Saplings are passable; grown trunks block. Square footprint, non-standable.
if (growth > 0.35) {
const r = growth * (kind === "oak" ? 0.3 : 0.2) + 0.15
colliders.push({ minX: x - r, maxX: x + r, minZ: z - r, maxZ: z + r, top: position.y + 3, standable: false })
}
placed++
}
}
/** Deterministic 0..1 generator (mulberry32) for tree placement. */
function mulberry(seed: number): () => number {
let a = seed >>> 0
return () => {
a = (a + 0x6D2B79F5) | 0
let t = Math.imul(a ^ (a >>> 15), 1 | a)
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
function mesh(): Mesh {

View file

@ -114,6 +114,9 @@ async function main(): Promise<void> {
Rasterizer.draw(fb, level.floor, textures.floor, viewProj, config)
Rasterizer.draw(fb, level.walls, textures.wall, viewProj, config)
Rasterizer.draw(fb, level.crate, textures.crate, viewProj, config)
Rasterizer.draw(fb, level.trunks, textures.bark, viewProj, config)
Rasterizer.draw(fb, level.oakFoliage, textures.leaf, viewProj, config)
Rasterizer.draw(fb, level.spruceFoliage, textures.needle, viewProj, config)
Rasterizer.draw(fb, Sprite.billboard(npc, camera), npc.texture, viewProj, config)
Framebuffer.quantize(fb, config)
present()

View file

@ -20,7 +20,7 @@ const SPEED = 6
* solid even at big multipliers. */
const RUN_MULTIPLIER = 2
const GRAVITY = 22
const JUMP_SPEED = 20
const JUMP_SPEED = 14
const NPC_RADIUS = 0.5
export namespace Player {