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

@ -56,7 +56,9 @@ rules live in `.agents/rules/*.md`.
`Mesh` (indexed tris), `Sprite` (Y-axis billboard), `Terrain` (procedural `Mesh` (indexed tris), `Sprite` (Y-axis billboard), `Terrain` (procedural
heightfield around the room: flat clearing in the center, rolling hills, tall heightfield around the room: flat clearing in the center, rolling hills, tall
edge peaks. `Terrain.ground` builds the outdoor mesh with a hole for the room; edge peaks. `Terrain.ground` builds the outdoor mesh with a hole for the room;
`Terrain.height` is the shared ground-height sampler for the player). `Terrain.height` is the shared ground-height sampler for the player), `Tree`
(procedural low-poly oak/spruce geometry, sapling..full via a `growth` knob;
`Tree.build` appends into shared trunk + foliage meshes).
- `app/` — browser glue. - `app/` — browser glue.
- `main.ts` — game loop, input, preset switching, canvas blit, FPS meter. - `main.ts` — game loop, input, preset switching, canvas blit, FPS meter.
- `assets.ts` — load `/assets/*.png``Texture` (zero-copy; ImageData bytes - `assets.ts` — load `/assets/*.png``Texture` (zero-copy; ImageData bytes
@ -64,8 +66,10 @@ rules live in `.agents/rules/*.md`.
- `level.ts` — builds the playground: a flat stone-floored room (three thick - `level.ts` — builds the playground: a flat stone-floored room (three thick
walls via `slab`, north side open) in the center of a big grassy `Terrain` walls via `slab`, north side open) in the center of a big grassy `Terrain`
world (~20x across). world (~20x across).
Per-texture meshes incl. the outdoor grass `ground`, `Aabb` colliders, NPC Per-texture meshes incl. the outdoor grass `ground` and a scattered forest
position, `Terrain` config + `GROUND_DIVISIONS`/`GROUND_UV`, sky/cloud config. (`scatterTrees``trunks`/`oakFoliage`/`spruceFoliage`, `TREE_COUNT`/`_SEED`/
`_REACH`), `Aabb` colliders (incl. grown-tree trunks), NPC position, `Terrain`
config + `GROUND_DIVISIONS`/`GROUND_UV`, sky/cloud config.
The stone floor is lifted by `FLOOR_LIFT` (a z-bias) so it stays clean over the The stone floor is lifted by `FLOOR_LIFT` (a z-bias) so it stays clean over the
terrain skirt that laps under the room edges. Room surfaces are single flat terrain skirt that laps under the room edges. Room surfaces are single flat
quads -- no subdivision needed since texturing is perspective-correct; ground quads -- no subdivision needed since texturing is perspective-correct; ground
@ -79,8 +83,9 @@ rules live in `.agents/rules/*.md`.
`#fps` meter div (styled inline). `#fps` meter div (styled inline).
- `scripts/gen-assets.ts` — procedurally draws the placeholder textures and - `scripts/gen-assets.ts` — procedurally draws the placeholder textures and
writes PNGs (hand-rolled encoder via `node:zlib`). Run via `bun run assets`. writes PNGs (hand-rolled encoder via `node:zlib`). Run via `bun run assets`.
- `assets/` — generated `floor/grass/wall/crate/npc` PNGs (`floor` = room stone, - `assets/` — generated `floor/grass/bark/leaf/needle/wall/crate/npc` PNGs
`grass` = outdoor ground). Swap for real art anytime; (`floor` = room stone, `grass` = outdoor ground, `bark`/`leaf`/`needle` = tree
trunk/oak/spruce). Swap for real art anytime;
filenames are the contract. filenames are the contract.
- `server/` — Bun server stub. `shared/` — isomorphic slot. - `server/` — Bun server stub. `shared/` — isomorphic slot.
@ -88,7 +93,8 @@ rules live in `.agents/rules/*.md`.
`Player.update` → build `Camera``Camera.viewProjection` `Player.update` → build `Camera``Camera.viewProjection`
`Sky.render` (fills color + resets depth, replaces a clear) → `Sky.render` (fills color + resets depth, replaces a clear) →
`Rasterizer.draw` ground, floor, walls, crate (one call per texture) → `Rasterizer.draw` ground, floor, walls, crate, tree trunks, oak foliage, spruce
foliage (one call per texture) →
`Sprite.billboard(npc)` drawn via `Rasterizer.draw` `Sprite.billboard(npc)` drawn via `Rasterizer.draw`
`Framebuffer.quantize``present` (integer-scale, letterboxed blit; `Framebuffer.quantize``present` (integer-scale, letterboxed blit;
`imageSmoothingEnabled` follows `upscaleFilter`). `imageSmoothingEnabled` follows `upscaleFilter`).
@ -131,6 +137,21 @@ Both are exported presets in `app/level.ts`; the active one is set in
branching in the cloud shader. Cost scales with sky resolution — fine at branching in the cloud shader. Cost scales with sky resolution — fine at
`standard`, heavy at `clean` (mitigate: fewer fbm octaves or half-res sky). `standard`, heavy at `clean` (mitigate: fewer fbm octaves or half-res sky).
## Trees (`engine/scene/Tree.ts`)
Procedural low-poly geometry, faceted flat-shaded like everything else. Two
`kind`s carry the species read purely by silhouette:
- **`oak`** — short tapered trunk, a couple of branches, a broad cluster of
lumpy canopy `blob`s (wider than tall, bushy).
- **`spruce`** — tall thin trunk under stacked narrowing `cone` tiers pointing
to a tip (taller than wide, conical).
`growth` (0..1) runs **sapling → full grown**: it scales height/girth and adds
canopy blobs (oak) / tiers (spruce); `seed` gives each tree its own wobble.
`Tree.build` appends into caller meshes so a forest batches into 3 draw calls
(one bark trunk mesh, oak-leaf and spruce-needle foliage meshes). `app/level.ts`
`scatterTrees` seeds the forest; add a species by extending the union + a builder.
## Controls ## Controls
WASD move · **Shift** run (speed ×`RUN_MULTIPLIER` in `app/player.ts`) · mouse WASD move · **Shift** run (speed ×`RUN_MULTIPLIER` in `app/player.ts`) · mouse

View file

@ -1,13 +1,19 @@
import type { Texture } from "../engine/render/Texture" import type { Texture } from "../engine/render/Texture"
import barkUrl from "../assets/bark.png"
import crateUrl from "../assets/crate.png" import crateUrl from "../assets/crate.png"
import floorUrl from "../assets/floor.png" import floorUrl from "../assets/floor.png"
import grassUrl from "../assets/grass.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 npcUrl from "../assets/npc.png"
import wallUrl from "../assets/wall.png" import wallUrl from "../assets/wall.png"
export type Textures = { export type Textures = {
floor: Texture floor: Texture
grass: Texture grass: Texture
bark: Texture
leaf: Texture
needle: Texture
wall: Texture wall: Texture
crate: Texture crate: Texture
npc: Texture npc: Texture
@ -15,14 +21,17 @@ export type Textures = {
/** Load every game texture up front. Call once before starting the loop. */ /** Load every game texture up front. Call once before starting the loop. */
export async function loadTextures(): Promise<Textures> { 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(floorUrl),
loadTexture(grassUrl), loadTexture(grassUrl),
loadTexture(barkUrl),
loadTexture(leafUrl),
loadTexture(needleUrl),
loadTexture(wallUrl), loadTexture(wallUrl),
loadTexture(crateUrl), loadTexture(crateUrl),
loadTexture(npcUrl), loadTexture(npcUrl),
]) ])
return { floor, grass, wall, crate, npc } return { floor, grass, bark, leaf, needle, wall, crate, npc }
} }
function loadTexture(url: string): Promise<Texture> { 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 { CloudLayer, SkyConfig } from "../engine/render/Sky"
import type { Mesh } from "../engine/scene/Mesh" import type { Mesh } from "../engine/scene/Mesh"
import { Terrain } from "../engine/scene/Terrain" import { Terrain } from "../engine/scene/Terrain"
import { Tree } from "../engine/scene/Tree"
type Corner = [number, number, number] type Corner = [number, number, number]
@ -24,6 +25,12 @@ export type Level = {
walls: Mesh walls: Mesh
crate: Mesh crate: Mesh
ground: 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[] colliders: Aabb[]
npcPosition: { x: number; y: number; z: number } npcPosition: { x: number; y: number; z: number }
terrain: Terrain terrain: Terrain
@ -57,6 +64,13 @@ const TERRAIN: Terrain = {
peakStart: 0.45, 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 /** 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 * 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. */ * 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 } 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 { 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.floor, textures.floor, viewProj, config)
Rasterizer.draw(fb, level.walls, textures.wall, viewProj, config) Rasterizer.draw(fb, level.walls, textures.wall, viewProj, config)
Rasterizer.draw(fb, level.crate, textures.crate, 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) Rasterizer.draw(fb, Sprite.billboard(npc, camera), npc.texture, viewProj, config)
Framebuffer.quantize(fb, config) Framebuffer.quantize(fb, config)
present() present()

View file

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

BIN
assets/bark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
assets/leaf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
assets/needle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

181
engine/scene/Tree.ts Normal file
View file

@ -0,0 +1,181 @@
import { Vec3 } from "../math/Vec3"
import type { Mesh } from "./Mesh"
const TAU = Math.PI * 2
/** One procedural tree instance. `growth` 0..1 runs sapling -> full grown: it
* scales height and girth and adds canopy blobs (oak) / tiers (spruce). `seed`
* drives the per-tree random wobble so a forest doesn't look cloned. */
export type Tree = {
kind: "oak" | "spruce"
/** Trunk base, sitting on the ground. */
position: Vec3
growth: number
seed: number
}
/**
* Low-poly tree geometry, in the same faceted flat-shaded style as the rest of
* the world. Two silhouettes carry the species read:
* oak -- short tapered trunk, a couple of branches, a broad cluster of
* rounded canopy blobs (bushy, wider than tall).
* spruce -- tall thin trunk under stacked cones that narrow to a point
* (tiered, taller than wide).
* `build` appends into caller-owned meshes so a whole forest batches into a few
* draw calls: all trunks share one bark mesh, foliage splits oak vs spruce so
* each can carry its own leaf/needle texture.
*/
export namespace Tree {
/** Append one tree into the shared `trunk` (bark) mesh and the `foliage` mesh
* for its kind (oak leaf vs spruce needle). */
export function build(tree: Tree, trunk: Mesh, foliage: Mesh): void {
const rand = rng(tree.seed)
if (tree.kind === "oak") {
oak(tree.position, tree.growth, rand, trunk, foliage)
} else {
spruce(tree.position, tree.growth, rand, trunk, foliage)
}
}
function oak(base: Vec3, g: number, rand: () => number, trunk: Mesh, leaves: Mesh): void {
const h = lerp(0.8, 7, g)
const rTrunk = lerp(0.04, 0.32, g)
const forkY = base.y + h * 0.5
limb(trunk, base, { x: base.x, y: forkY, z: base.z }, rTrunk, rTrunk * 0.6, 5)
const blobR = h * 0.3
const spread = h * 0.32
const canopyY = base.y + h * 0.72
// Central blob plus, as it grows, a couple offset ones -> broad bushy crown.
const blobs = 1 + Math.round(g * 2)
for (let i = 0; i < blobs; i++) {
const angle = rand() * TAU
const rad = i === 0 ? 0 : spread * (0.5 + rand() * 0.5)
const center = {
x: base.x + Math.cos(angle) * rad,
y: canopyY + (rand() - 0.4) * spread,
z: base.z + Math.sin(angle) * rad,
}
blob(leaves, center, blobR * (0.7 + rand() * 0.4), rand)
}
// Grown oaks throw out a few branches, each tipped with a leaf tuft.
if (g > 0.55) {
const branches = 2 + Math.round(rand())
for (let i = 0; i < branches; i++) {
const angle = rand() * TAU
const dir = Vec3.normalize({ x: Math.cos(angle), y: 1.2, z: Math.sin(angle) })
const start = { x: base.x, y: base.y + h * 0.42, z: base.z }
const end = Vec3.add(start, Vec3.scale(dir, h * 0.3))
limb(trunk, start, end, rTrunk * 0.4, rTrunk * 0.2, 4)
blob(leaves, end, blobR * 0.6, rand)
}
}
}
function spruce(base: Vec3, g: number, rand: () => number, trunk: Mesh, needles: Mesh): void {
const h = lerp(0.6, 9, g)
const rTrunk = lerp(0.03, 0.2, g)
limb(trunk, base, { x: base.x, y: base.y + h, z: base.z }, rTrunk, rTrunk * 0.25, 5)
// Stacked cones: widest low, shrinking to a point up top -> conical tiers.
const tiers = 2 + Math.round(g * 3)
const bottom = base.y + h * 0.1
const span = h * 0.9
for (let i = 0; i < tiers; i++) {
const t = i / tiers
const y = bottom + t * span * 0.82
const radius = lerp(h * 0.3, h * 0.05, t) * (0.9 + rand() * 0.2)
const coneH = (span / tiers) * 1.9
cone(needles, { x: base.x, y, z: base.z }, coneH, radius, 6)
}
}
/** A tapered tube between two points (trunk or branch), `sides`-gonal. */
function limb(mesh: Mesh, a: Vec3, b: Vec3, ra: number, rb: number, sides: number): void {
const axis = Vec3.normalize(Vec3.sub(b, a))
const [u, v] = basis(axis)
const len = Vec3.length(Vec3.sub(b, a))
const start = mesh.vertices.length
for (let i = 0; i <= sides; i++) {
const angle = (i / sides) * TAU
const dir = Vec3.add(Vec3.scale(u, Math.cos(angle)), Vec3.scale(v, Math.sin(angle)))
const s = i / sides
mesh.vertices.push({ pos: Vec3.add(a, Vec3.scale(dir, ra)), uv: { x: s * 1.5, y: 0 } })
mesh.vertices.push({ pos: Vec3.add(b, Vec3.scale(dir, rb)), uv: { x: s * 1.5, y: len * 0.5 } })
}
for (let i = 0; i < sides; i++) {
const p = start + i * 2
mesh.indices.push(p, p + 2, p + 3, p, p + 3, p + 1)
}
}
/** A cone standing on a base ring, apex `height` above it (one spruce tier). */
function cone(mesh: Mesh, base: Vec3, height: number, radius: number, sides: number): void {
const start = mesh.vertices.length
mesh.vertices.push({ pos: { x: base.x, y: base.y + height, z: base.z }, uv: { x: 0.5, y: 0 } })
for (let i = 0; i <= sides; i++) {
const angle = (i / sides) * TAU
mesh.vertices.push({
pos: { x: base.x + Math.cos(angle) * radius, y: base.y, z: base.z + Math.sin(angle) * radius },
uv: { x: (i / sides) * 2, y: 1 },
})
}
for (let i = 0; i < sides; i++) {
mesh.indices.push(start, start + 1 + i, start + 2 + i)
}
}
/** A lumpy low-poly sphere (one oak canopy blob). Per-ring radius wobble keeps
* it organic without cracking the longitude seam. */
function blob(mesh: Mesh, center: Vec3, radius: number, rand: () => number): void {
const seg = 5
const rings = 3
const start = mesh.vertices.length
for (let r = 0; r <= rings; r++) {
const phi = (r / rings) * Math.PI
const cy = Math.cos(phi)
const cr = Math.sin(phi)
const scale = radius * (0.85 + rand() * 0.3)
for (let s = 0; s <= seg; s++) {
const theta = (s / seg) * TAU
mesh.vertices.push({
pos: {
x: center.x + cr * Math.cos(theta) * scale,
y: center.y + cy * scale,
z: center.z + cr * Math.sin(theta) * scale,
},
uv: { x: (s / seg) * 2, y: (r / rings) * 2 },
})
}
}
const row = seg + 1
for (let r = 0; r < rings; r++) {
for (let s = 0; s < seg; s++) {
const p = start + r * row + s
mesh.indices.push(p, p + 1, p + row + 1, p, p + row + 1, p + row)
}
}
}
/** Two unit vectors spanning the plane perpendicular to `axis`. */
function basis(axis: Vec3): [Vec3, Vec3] {
const ref = Math.abs(axis.y) < 0.99 ? { x: 0, y: 1, z: 0 } : { x: 1, y: 0, z: 0 }
const u = Vec3.normalize(Vec3.cross(ref, axis))
return [u, Vec3.cross(axis, u)]
}
function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t
}
/** Deterministic 0..1 generator (mulberry32) seeded per tree. */
function rng(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
}
}
}

View file

@ -118,6 +118,30 @@ const grass: Shade = (x, y) => {
return [56 + n * 0.7 + patch * 0.5, 116 + n + blade + patch, 50 + n * 0.5 + patch * 0.4, 255] return [56 + n * 0.7 + patch * 0.5, 116 + n + blade + patch, 50 + n * 0.5 + patch * 0.4, 255]
} }
// Tree bark: brown with vertical grain and the odd dark crack.
const bark: Shade = (x, y) => {
const grain = Math.sin(x * 0.6 + noise(0, y) * 2) * 10
const n = noise(x, y) * 14
const crack = noise(Math.floor(x / 6), y) < -0.45 ? -24 : 0
return [92 + n + grain + crack, 66 + n * 0.8 + grain * 0.6 + crack, 44 + n * 0.6 + crack, 255]
}
// Oak leaves: mid green, blotchy clumps with brighter highlights.
const leaf: Shade = (x, y) => {
const n = noise(x, y) * 26
const clump = noise(Math.floor(x / 5), Math.floor(y / 5)) * 22
const hi = noise(x, y) > 0.6 ? 22 : 0
return [58 + n * 0.5 + clump * 0.4, 120 + n + clump + hi, 42 + n * 0.4 + clump * 0.3, 255]
}
// Spruce needles: darker, cooler blue-green, finer streaky grain.
const needle: Shade = (x, y) => {
const n = noise(x, y) * 18
const streak = noise(x, y * 2) * 10
const clump = noise(Math.floor(x / 6), Math.floor(y / 6)) * 12
return [40 + n * 0.4 + clump * 0.3, 84 + n + streak + clump, 58 + n * 0.5 + clump * 0.4, 255]
}
const wall: Shade = (x, y) => { const wall: Shade = (x, y) => {
const row = Math.floor(y / 16) const row = Math.floor(y / 16)
const bx = (x + (row % 2) * 16) % 32 const bx = (x + (row % 2) * 16) % 32
@ -172,6 +196,9 @@ const npc: Shade = (x, y) => {
const assets: Array<[string, number, number, Shade]> = [ const assets: Array<[string, number, number, Shade]> = [
["floor", 64, 64, floor], ["floor", 64, 64, floor],
["grass", 64, 64, grass], ["grass", 64, 64, grass],
["bark", 64, 64, bark],
["leaf", 64, 64, leaf],
["needle", 64, 64, needle],
["wall", 64, 64, wall], ["wall", 64, 64, wall],
["crate", 64, 64, crate], ["crate", 64, 64, crate],
["npc", 48, 64, npc], ["npc", 48, 64, npc],