feat: birch
This commit is contained in:
parent
16f205babe
commit
a4490f7a20
11 changed files with 110 additions and 24 deletions
26
AGENTS.md
26
AGENTS.md
|
|
@ -72,7 +72,7 @@ rules live in `.agents/rules/*.md`.
|
|||
edge peaks. `Terrain.patch` builds one ground patch over a rectangle -- called
|
||||
per chunk, aligned so patches weld crack-free, with a hole for the room;
|
||||
`Terrain.height` is the shared ground-height sampler for the player), `Tree`
|
||||
(procedural low-poly oak/spruce geometry, sapling..full via a `growth` knob;
|
||||
(procedural low-poly oak/spruce/birch geometry, sapling..full via a `growth` knob;
|
||||
`Tree.build` appends into shared trunk + foliage meshes), `Boulder`
|
||||
(procedural low-poly rock: a squashed, jittered, part-buried sphere;
|
||||
`Boulder.build` appends into a shared mesh), `Bush` (cluster of small leaf
|
||||
|
|
@ -116,7 +116,7 @@ rules live in `.agents/rules/*.md`.
|
|||
scattered (`placeMobs`; `FROG_/BEE_COUNT`, `MOB_SEED`, `MOB_REACH` — mobs move,
|
||||
so they carry no baked colliders), then `buildChunks` bakes
|
||||
terrain + props into a `CHUNK_GRID` x `CHUNK_GRID` grid of `Chunk`s (each =
|
||||
per-texture meshes grass/bark/leaf/needle/rock/flowers + a tight AABB) that
|
||||
per-texture meshes grass/bark/birchBark/leaf/needle/rock/flowers + a tight AABB) that
|
||||
`main` frustum-culls; bushes fold into the leaf mesh, flowers get their own
|
||||
(drawn double-sided). Trees + boulders are baked **twice** — full geometry and
|
||||
a low-poly impostor (`barkFar/leafFar/needleFar/rockFar`, via the builders'
|
||||
|
|
@ -134,9 +134,9 @@ rules live in `.agents/rules/*.md`.
|
|||
`#fps` meter div (styled inline).
|
||||
- `scripts/gen-assets.ts` — procedurally draws the placeholder textures and
|
||||
writes PNGs (hand-rolled encoder via `node:zlib`). Run via `bun run assets`.
|
||||
- `assets/` — generated `floor/grass/bark/leaf/needle/rock/flower/wall/crate/npc/frog/bee`
|
||||
PNGs (`floor` = room stone, `grass` = outdoor ground, `bark`/`leaf`/`needle` =
|
||||
tree trunk/oak/spruce, `rock` = boulders, `flower` = 2x2 bloom-color atlas,
|
||||
- `assets/` — generated `floor/grass/bark/birch/leaf/needle/rock/flower/wall/crate/npc/frog/bee`
|
||||
PNGs (`floor` = room stone, `grass` = outdoor ground, `bark`/`birch`/`leaf`/`needle` =
|
||||
brown trunk / white birch trunk / oak leaf / spruce needle, `rock` = boulders, `flower` = 2x2 bloom-color atlas,
|
||||
`frog`/`bee` = mob skin atlases: frog green + eye tone; bee stripe bands +
|
||||
head-dark + wing-pale regions).
|
||||
Swap for real art anytime;
|
||||
|
|
@ -271,18 +271,24 @@ branching in the cloud shader. Cost scales with sky resolution — fine at
|
|||
|
||||
## Trees (`engine/scene/Tree.ts`)
|
||||
|
||||
Procedural low-poly geometry, faceted flat-shaded like everything else. Two
|
||||
Procedural low-poly geometry, faceted flat-shaded like everything else. Three
|
||||
`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).
|
||||
- **`birch`** — silver birch: tall, slender, near-straight trunk with an airy,
|
||||
high, slightly drooping canopy (lean silhouette). Uses a separate **white**
|
||||
`birch` bark texture (brown bark can't stand in), and shares the oak `leaf`
|
||||
foliage.
|
||||
|
||||
`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.
|
||||
canopy blobs (oak/birch) / tiers (spruce); `seed` gives each tree its own wobble.
|
||||
`Tree.build` appends into caller-chosen trunk + foliage meshes, so a forest still
|
||||
batches into a few draw calls (brown-bark trunks, white-birch trunks, oak/birch
|
||||
leaf foliage, spruce needles). `app/level.ts` `placeTrees` seeds the forest and
|
||||
rolls the species; add one by extending the union + a builder (a new bark/leaf
|
||||
look also needs its own texture + per-chunk mesh channel — see `birchBark`).
|
||||
|
||||
**Boulders** (`engine/scene/Boulder.ts`) work the same way: `Boulder.build`
|
||||
appends a squashed, per-vertex-jittered low-poly sphere (seam/pole-safe so it
|
||||
|
|
|
|||
|
|
@ -5,3 +5,6 @@
|
|||
@.agents/rules/forge-sync.md
|
||||
@.agents/rules/openspec.md
|
||||
@.agents/rules/quality.md
|
||||
|
||||
Don't nudge the user towards branching or commiting. User is perfectly capable of handling this.
|
||||
|
||||
|
|
|
|||
|
|
@ -5,3 +5,7 @@
|
|||
- all the knobs
|
||||
|
||||
|
||||
Start to make cohesive game:
|
||||
|
||||
- Split apart the "engine" and "game"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Texture } from "../engine/render/Texture"
|
||||
import barkUrl from "../assets/bark.png"
|
||||
import beeUrl from "../assets/bee.png"
|
||||
import birchUrl from "../assets/birch.png"
|
||||
import crateUrl from "../assets/crate.png"
|
||||
import floorUrl from "../assets/floor.png"
|
||||
import flowerUrl from "../assets/flower.png"
|
||||
|
|
@ -16,6 +17,7 @@ export type Textures = {
|
|||
floor: Texture
|
||||
grass: Texture
|
||||
bark: Texture
|
||||
birch: Texture
|
||||
leaf: Texture
|
||||
needle: Texture
|
||||
rock: Texture
|
||||
|
|
@ -29,10 +31,11 @@ export type Textures = {
|
|||
|
||||
/** Load every game texture up front. Call once before starting the loop. */
|
||||
export async function loadTextures(): Promise<Textures> {
|
||||
const [floor, grass, bark, leaf, needle, rock, flower, wall, crate, npc, frog, bee] = await Promise.all([
|
||||
const [floor, grass, bark, birch, leaf, needle, rock, flower, wall, crate, npc, frog, bee] = await Promise.all([
|
||||
loadTexture(floorUrl),
|
||||
loadTexture(grassUrl),
|
||||
loadTexture(barkUrl),
|
||||
loadTexture(birchUrl),
|
||||
loadTexture(leafUrl),
|
||||
loadTexture(needleUrl),
|
||||
loadTexture(rockUrl),
|
||||
|
|
@ -43,7 +46,7 @@ export async function loadTextures(): Promise<Textures> {
|
|||
loadTexture(frogUrl),
|
||||
loadTexture(beeUrl),
|
||||
])
|
||||
return { floor, grass, bark, leaf, needle, rock, flower, wall, crate, npc, frog, bee }
|
||||
return { floor, grass, bark, birch, leaf, needle, rock, flower, wall, crate, npc, frog, bee }
|
||||
}
|
||||
|
||||
function loadTexture(url: string): Promise<Texture> {
|
||||
|
|
|
|||
27
app/level.ts
27
app/level.ts
|
|
@ -35,6 +35,9 @@ export type Chunk = {
|
|||
maxZ: number
|
||||
grass: Mesh
|
||||
bark: Mesh
|
||||
/** Birch trunks (white bark texture) -- separate mesh since a draw is one mesh +
|
||||
* one texture, and silver birch bark can't share the brown oak/spruce bark. */
|
||||
birchBark: Mesh
|
||||
leaf: Mesh
|
||||
needle: Mesh
|
||||
rock: Mesh
|
||||
|
|
@ -42,8 +45,9 @@ export type Chunk = {
|
|||
flowers: Mesh
|
||||
/** Cheap low-poly impostors of the same trees/boulders, drawn instead of the
|
||||
* full meshes once the chunk is past `config.lodDistance` (renderScene). Same
|
||||
* textures as bark/leaf/needle/rock. Bushes/flowers have no far version. */
|
||||
* textures as bark/birch/leaf/needle/rock. Bushes/flowers have no far version. */
|
||||
barkFar: Mesh
|
||||
birchBarkFar: Mesh
|
||||
leafFar: Mesh
|
||||
needleFar: Mesh
|
||||
rockFar: Mesh
|
||||
|
|
@ -231,21 +235,27 @@ function buildChunks(trees: Tree[], boulders: Boulder[], bushes: Bush[], flowers
|
|||
const z1 = z0 + cell
|
||||
const grass = mesh()
|
||||
const bark = mesh()
|
||||
const birchBark = mesh()
|
||||
const leaf = mesh()
|
||||
const needle = mesh()
|
||||
const rock = mesh()
|
||||
const flowerMesh = mesh()
|
||||
const barkFar = mesh()
|
||||
const birchBarkFar = mesh()
|
||||
const leafFar = mesh()
|
||||
const needleFar = mesh()
|
||||
const rockFar = mesh()
|
||||
Terrain.patch(TERRAIN, grass, x0, z0, x1, z1, TERRAIN_SUBDIV, TERRAIN_SUBDIV, GROUND_UV)
|
||||
for (const tree of trees) {
|
||||
if (inCell(tree.position, x0, z0, x1, z1)) {
|
||||
const foliage = tree.kind === "oak" ? leaf : needle
|
||||
const foliageFar = tree.kind === "oak" ? leafFar : needleFar
|
||||
Tree.build(tree, bark, foliage)
|
||||
Tree.build(tree, barkFar, foliageFar, "impostor")
|
||||
// Birch trunks go to their own white-bark mesh; oak + birch share the oak
|
||||
// leaf foliage, spruce keeps its needles.
|
||||
const trunk = tree.kind === "birch" ? birchBark : bark
|
||||
const trunkFar = tree.kind === "birch" ? birchBarkFar : barkFar
|
||||
const foliage = tree.kind === "spruce" ? needle : leaf
|
||||
const foliageFar = tree.kind === "spruce" ? needleFar : leafFar
|
||||
Tree.build(tree, trunk, foliage)
|
||||
Tree.build(tree, trunkFar, foliageFar, "impostor")
|
||||
}
|
||||
}
|
||||
for (const boulder of boulders) {
|
||||
|
|
@ -265,11 +275,11 @@ function buildChunks(trees: Tree[], boulders: Boulder[], bushes: Bush[], flowers
|
|||
Flower.build(flower, flowerMesh)
|
||||
}
|
||||
}
|
||||
const b = bounds([grass, bark, leaf, needle, rock, flowerMesh])
|
||||
const b = bounds([grass, bark, birchBark, leaf, needle, rock, flowerMesh])
|
||||
if (b === null) {
|
||||
continue
|
||||
}
|
||||
chunks.push({ ...b, grass, bark, leaf, needle, rock, flowers: flowerMesh, barkFar, leafFar, needleFar, rockFar })
|
||||
chunks.push({ ...b, grass, bark, birchBark, leaf, needle, rock, flowers: flowerMesh, barkFar, birchBarkFar, leafFar, needleFar, rockFar })
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
|
|
@ -320,7 +330,8 @@ function placeTrees(colliders: Aabb[]): Tree[] {
|
|||
if (Math.max(Math.abs(x), Math.abs(z)) < TERRAIN.inner + 3) {
|
||||
continue
|
||||
}
|
||||
const kind = rand() < 0.5 ? "oak" : "spruce"
|
||||
const roll = rand()
|
||||
const kind = roll < 0.4 ? "oak" : roll < 0.72 ? "spruce" : "birch"
|
||||
const growth = 0.08 + rand() * 0.92
|
||||
const position = { x, y: Terrain.height(TERRAIN, x, z), z }
|
||||
trees.push({ kind, position, growth, seed: (rand() * 0xFFFFFFFF) | 0 })
|
||||
|
|
|
|||
|
|
@ -161,8 +161,8 @@ async function main(): Promise<void> {
|
|||
t += c.grass.indices.length + c.flowers.indices.length
|
||||
const far = chunkFar(c, cam.position, config.lodDistance)
|
||||
t += far
|
||||
? c.barkFar.indices.length + c.leafFar.indices.length + c.needleFar.indices.length + c.rockFar.indices.length
|
||||
: c.bark.indices.length + c.leaf.indices.length + c.needle.indices.length + c.rock.indices.length
|
||||
? c.barkFar.indices.length + c.birchBarkFar.indices.length + c.leafFar.indices.length + c.needleFar.indices.length + c.rockFar.indices.length
|
||||
: c.bark.indices.length + c.birchBark.indices.length + c.leaf.indices.length + c.needle.indices.length + c.rock.indices.length
|
||||
}
|
||||
return (t / 3) | 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,11 +104,13 @@ export function renderBand(
|
|||
if (chunkFar(c, camera.position, config.lodDistance)) {
|
||||
Rasterizer.draw(fb, c.rockFar, tx.rock, viewProj, config, true, y0, y1)
|
||||
Rasterizer.draw(fb, c.barkFar, tx.bark, viewProj, config, true, y0, y1)
|
||||
Rasterizer.draw(fb, c.birchBarkFar, tx.birch, viewProj, config, true, y0, y1)
|
||||
Rasterizer.draw(fb, c.leafFar, tx.leaf, viewProj, config, true, y0, y1)
|
||||
Rasterizer.draw(fb, c.needleFar, tx.needle, viewProj, config, true, y0, y1)
|
||||
} else {
|
||||
Rasterizer.draw(fb, c.rock, tx.rock, viewProj, config, true, y0, y1)
|
||||
Rasterizer.draw(fb, c.bark, tx.bark, viewProj, config, true, y0, y1)
|
||||
Rasterizer.draw(fb, c.birchBark, tx.birch, viewProj, config, true, y0, y1)
|
||||
Rasterizer.draw(fb, c.leaf, tx.leaf, viewProj, config, true, y0, y1)
|
||||
Rasterizer.draw(fb, c.needle, tx.needle, viewProj, config, true, y0, y1)
|
||||
Rasterizer.draw(fb, c.flowers, tx.flower, viewProj, config, false, y0, y1)
|
||||
|
|
|
|||
BIN
assets/birch.png
Normal file
BIN
assets/birch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/frog.png
BIN
assets/frog.png
Binary file not shown.
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 3.9 KiB |
|
|
@ -7,7 +7,7 @@ const TAU = Math.PI * 2
|
|||
* 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"
|
||||
kind: "oak" | "spruce" | "birch"
|
||||
/** Trunk base, sitting on the ground. */
|
||||
position: Vec3
|
||||
growth: number
|
||||
|
|
@ -34,8 +34,10 @@ export namespace Tree {
|
|||
const rand = rng(tree.seed)
|
||||
if (tree.kind === "oak") {
|
||||
oak(tree.position, tree.growth, rand, trunk, foliage, lod)
|
||||
} else {
|
||||
} else if (tree.kind === "spruce") {
|
||||
spruce(tree.position, tree.growth, rand, trunk, foliage, lod)
|
||||
} else {
|
||||
birch(tree.position, tree.growth, rand, trunk, foliage, lod)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +103,48 @@ export namespace Tree {
|
|||
}
|
||||
}
|
||||
|
||||
function birch(base: Vec3, g: number, rand: () => number, trunk: Mesh, leaves: Mesh, lod: "full" | "impostor"): void {
|
||||
// Silver birch: tall, slender, near-straight white trunk under an airy, high,
|
||||
// slightly drooping canopy of small leaf tufts -- a lean silhouette between the
|
||||
// broad oak and the conical spruce (the white bark texture does the rest).
|
||||
const h = lerp(1, 8.5, g)
|
||||
const rTrunk = lerp(0.03, 0.16, g)
|
||||
const canopyY = base.y + h * 0.75
|
||||
const blobR = h * 0.22
|
||||
if (lod === "impostor") {
|
||||
limb(trunk, base, { x: base.x, y: base.y + h * 0.9, z: base.z }, rTrunk, rTrunk * 0.5, 3)
|
||||
blob(leaves, { x: base.x, y: canopyY, z: base.z }, blobR * 1.1, rand, 4, 2)
|
||||
return
|
||||
}
|
||||
limb(trunk, base, { x: base.x, y: base.y + h * 0.88, z: base.z }, rTrunk, rTrunk * 0.35, 5)
|
||||
|
||||
const spread = h * 0.22
|
||||
// Sparse small blobs clustered high, biased downward so the crown droops.
|
||||
const blobs = 2 + 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.6) * spread,
|
||||
z: base.z + Math.sin(angle) * rad,
|
||||
}
|
||||
blob(leaves, center, blobR * (0.7 + rand() * 0.4), rand)
|
||||
}
|
||||
// Grown birches trail a few thin, near-horizontal drooping twigs.
|
||||
if (g > 0.5) {
|
||||
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: 0.6, z: Math.sin(angle) })
|
||||
const start = { x: base.x, y: base.y + h * 0.7, z: base.z }
|
||||
const end = Vec3.add(start, Vec3.scale(dir, h * 0.22))
|
||||
limb(trunk, start, end, rTrunk * 0.4, rTrunk * 0.15, 4)
|
||||
blob(leaves, end, blobR * 0.55, rand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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))
|
||||
|
|
|
|||
|
|
@ -126,6 +126,18 @@ const bark: Shade = (x, y) => {
|
|||
return [92 + n + grain + crack, 66 + n * 0.8 + grain * 0.6 + crack, 44 + n * 0.6 + crack, 255]
|
||||
}
|
||||
|
||||
// Silver birch bark: papery near-white with faint grey mottle and the dark
|
||||
// horizontal lenticel dashes that make a birch read as a birch.
|
||||
const birch: Shade = (x, y) => {
|
||||
const n = noise(x, y) * 8
|
||||
// Dark dashes: short random runs on a few rows, wrapping the trunk horizontally.
|
||||
if (y % 8 < 3 && noise(Math.floor(x / 6), Math.floor(y / 8)) > 0.3) {
|
||||
return [38 + n, 38 + n, 40 + n, 255]
|
||||
}
|
||||
const smudge = noise(Math.floor(x / 10), Math.floor(y / 10)) < -0.4 ? -22 : 0
|
||||
return [216 + n + smudge, 218 + n + smudge, 214 + n + smudge, 255]
|
||||
}
|
||||
|
||||
// Oak leaves: mid green, blotchy clumps with brighter highlights.
|
||||
const leaf: Shade = (x, y) => {
|
||||
const n = noise(x, y) * 26
|
||||
|
|
@ -249,6 +261,7 @@ const assets: Array<[string, number, number, Shade]> = [
|
|||
["floor", 64, 64, floor],
|
||||
["grass", 64, 64, grass],
|
||||
["bark", 64, 64, bark],
|
||||
["birch", 64, 64, birch],
|
||||
["leaf", 64, 64, leaf],
|
||||
["needle", 64, 64, needle],
|
||||
["rock", 64, 64, rock],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue