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
|
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;
|
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`
|
`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`
|
`Tree.build` appends into shared trunk + foliage meshes), `Boulder`
|
||||||
(procedural low-poly rock: a squashed, jittered, part-buried sphere;
|
(procedural low-poly rock: a squashed, jittered, part-buried sphere;
|
||||||
`Boulder.build` appends into a shared mesh), `Bush` (cluster of small leaf
|
`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,
|
scattered (`placeMobs`; `FROG_/BEE_COUNT`, `MOB_SEED`, `MOB_REACH` — mobs move,
|
||||||
so they carry no baked colliders), then `buildChunks` bakes
|
so they carry no baked colliders), then `buildChunks` bakes
|
||||||
terrain + props into a `CHUNK_GRID` x `CHUNK_GRID` grid of `Chunk`s (each =
|
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
|
`main` frustum-culls; bushes fold into the leaf mesh, flowers get their own
|
||||||
(drawn double-sided). Trees + boulders are baked **twice** — full geometry and
|
(drawn double-sided). Trees + boulders are baked **twice** — full geometry and
|
||||||
a low-poly impostor (`barkFar/leafFar/needleFar/rockFar`, via the builders'
|
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).
|
`#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/bark/leaf/needle/rock/flower/wall/crate/npc/frog/bee`
|
- `assets/` — generated `floor/grass/bark/birch/leaf/needle/rock/flower/wall/crate/npc/frog/bee`
|
||||||
PNGs (`floor` = room stone, `grass` = outdoor ground, `bark`/`leaf`/`needle` =
|
PNGs (`floor` = room stone, `grass` = outdoor ground, `bark`/`birch`/`leaf`/`needle` =
|
||||||
tree trunk/oak/spruce, `rock` = boulders, `flower` = 2x2 bloom-color atlas,
|
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 +
|
`frog`/`bee` = mob skin atlases: frog green + eye tone; bee stripe bands +
|
||||||
head-dark + wing-pale regions).
|
head-dark + wing-pale regions).
|
||||||
Swap for real art anytime;
|
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`)
|
## 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:
|
`kind`s carry the species read purely by silhouette:
|
||||||
- **`oak`** — short tapered trunk, a couple of branches, a broad cluster of
|
- **`oak`** — short tapered trunk, a couple of branches, a broad cluster of
|
||||||
lumpy canopy `blob`s (wider than tall, bushy).
|
lumpy canopy `blob`s (wider than tall, bushy).
|
||||||
- **`spruce`** — tall thin trunk under stacked narrowing `cone` tiers pointing
|
- **`spruce`** — tall thin trunk under stacked narrowing `cone` tiers pointing
|
||||||
to a tip (taller than wide, conical).
|
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
|
`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.
|
canopy blobs (oak/birch) / tiers (spruce); `seed` gives each tree its own wobble.
|
||||||
`Tree.build` appends into caller meshes so a forest batches into 3 draw calls
|
`Tree.build` appends into caller-chosen trunk + foliage meshes, so a forest still
|
||||||
(one bark trunk mesh, oak-leaf and spruce-needle foliage meshes). `app/level.ts`
|
batches into a few draw calls (brown-bark trunks, white-birch trunks, oak/birch
|
||||||
`scatterTrees` seeds the forest; add a species by extending the union + a builder.
|
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`
|
**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
|
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/forge-sync.md
|
||||||
@.agents/rules/openspec.md
|
@.agents/rules/openspec.md
|
||||||
@.agents/rules/quality.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
|
- 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 type { Texture } from "../engine/render/Texture"
|
||||||
import barkUrl from "../assets/bark.png"
|
import barkUrl from "../assets/bark.png"
|
||||||
import beeUrl from "../assets/bee.png"
|
import beeUrl from "../assets/bee.png"
|
||||||
|
import birchUrl from "../assets/birch.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 flowerUrl from "../assets/flower.png"
|
import flowerUrl from "../assets/flower.png"
|
||||||
|
|
@ -16,6 +17,7 @@ export type Textures = {
|
||||||
floor: Texture
|
floor: Texture
|
||||||
grass: Texture
|
grass: Texture
|
||||||
bark: Texture
|
bark: Texture
|
||||||
|
birch: Texture
|
||||||
leaf: Texture
|
leaf: Texture
|
||||||
needle: Texture
|
needle: Texture
|
||||||
rock: Texture
|
rock: Texture
|
||||||
|
|
@ -29,10 +31,11 @@ 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, 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(floorUrl),
|
||||||
loadTexture(grassUrl),
|
loadTexture(grassUrl),
|
||||||
loadTexture(barkUrl),
|
loadTexture(barkUrl),
|
||||||
|
loadTexture(birchUrl),
|
||||||
loadTexture(leafUrl),
|
loadTexture(leafUrl),
|
||||||
loadTexture(needleUrl),
|
loadTexture(needleUrl),
|
||||||
loadTexture(rockUrl),
|
loadTexture(rockUrl),
|
||||||
|
|
@ -43,7 +46,7 @@ export async function loadTextures(): Promise<Textures> {
|
||||||
loadTexture(frogUrl),
|
loadTexture(frogUrl),
|
||||||
loadTexture(beeUrl),
|
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> {
|
function loadTexture(url: string): Promise<Texture> {
|
||||||
|
|
|
||||||
27
app/level.ts
27
app/level.ts
|
|
@ -35,6 +35,9 @@ export type Chunk = {
|
||||||
maxZ: number
|
maxZ: number
|
||||||
grass: Mesh
|
grass: Mesh
|
||||||
bark: 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
|
leaf: Mesh
|
||||||
needle: Mesh
|
needle: Mesh
|
||||||
rock: Mesh
|
rock: Mesh
|
||||||
|
|
@ -42,8 +45,9 @@ export type Chunk = {
|
||||||
flowers: Mesh
|
flowers: Mesh
|
||||||
/** Cheap low-poly impostors of the same trees/boulders, drawn instead of the
|
/** Cheap low-poly impostors of the same trees/boulders, drawn instead of the
|
||||||
* full meshes once the chunk is past `config.lodDistance` (renderScene). Same
|
* 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
|
barkFar: Mesh
|
||||||
|
birchBarkFar: Mesh
|
||||||
leafFar: Mesh
|
leafFar: Mesh
|
||||||
needleFar: Mesh
|
needleFar: Mesh
|
||||||
rockFar: Mesh
|
rockFar: Mesh
|
||||||
|
|
@ -231,21 +235,27 @@ function buildChunks(trees: Tree[], boulders: Boulder[], bushes: Bush[], flowers
|
||||||
const z1 = z0 + cell
|
const z1 = z0 + cell
|
||||||
const grass = mesh()
|
const grass = mesh()
|
||||||
const bark = mesh()
|
const bark = mesh()
|
||||||
|
const birchBark = mesh()
|
||||||
const leaf = mesh()
|
const leaf = mesh()
|
||||||
const needle = mesh()
|
const needle = mesh()
|
||||||
const rock = mesh()
|
const rock = mesh()
|
||||||
const flowerMesh = mesh()
|
const flowerMesh = mesh()
|
||||||
const barkFar = mesh()
|
const barkFar = mesh()
|
||||||
|
const birchBarkFar = mesh()
|
||||||
const leafFar = mesh()
|
const leafFar = mesh()
|
||||||
const needleFar = mesh()
|
const needleFar = mesh()
|
||||||
const rockFar = mesh()
|
const rockFar = mesh()
|
||||||
Terrain.patch(TERRAIN, grass, x0, z0, x1, z1, TERRAIN_SUBDIV, TERRAIN_SUBDIV, GROUND_UV)
|
Terrain.patch(TERRAIN, grass, x0, z0, x1, z1, TERRAIN_SUBDIV, TERRAIN_SUBDIV, GROUND_UV)
|
||||||
for (const tree of trees) {
|
for (const tree of trees) {
|
||||||
if (inCell(tree.position, x0, z0, x1, z1)) {
|
if (inCell(tree.position, x0, z0, x1, z1)) {
|
||||||
const foliage = tree.kind === "oak" ? leaf : needle
|
// Birch trunks go to their own white-bark mesh; oak + birch share the oak
|
||||||
const foliageFar = tree.kind === "oak" ? leafFar : needleFar
|
// leaf foliage, spruce keeps its needles.
|
||||||
Tree.build(tree, bark, foliage)
|
const trunk = tree.kind === "birch" ? birchBark : bark
|
||||||
Tree.build(tree, barkFar, foliageFar, "impostor")
|
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) {
|
for (const boulder of boulders) {
|
||||||
|
|
@ -265,11 +275,11 @@ function buildChunks(trees: Tree[], boulders: Boulder[], bushes: Bush[], flowers
|
||||||
Flower.build(flower, flowerMesh)
|
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) {
|
if (b === null) {
|
||||||
continue
|
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
|
return chunks
|
||||||
|
|
@ -320,7 +330,8 @@ function placeTrees(colliders: Aabb[]): Tree[] {
|
||||||
if (Math.max(Math.abs(x), Math.abs(z)) < TERRAIN.inner + 3) {
|
if (Math.max(Math.abs(x), Math.abs(z)) < TERRAIN.inner + 3) {
|
||||||
continue
|
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 growth = 0.08 + rand() * 0.92
|
||||||
const position = { x, y: Terrain.height(TERRAIN, x, z), z }
|
const position = { x, y: Terrain.height(TERRAIN, x, z), z }
|
||||||
trees.push({ kind, position, growth, seed: (rand() * 0xFFFFFFFF) | 0 })
|
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
|
t += c.grass.indices.length + c.flowers.indices.length
|
||||||
const far = chunkFar(c, cam.position, config.lodDistance)
|
const far = chunkFar(c, cam.position, config.lodDistance)
|
||||||
t += far
|
t += far
|
||||||
? c.barkFar.indices.length + c.leafFar.indices.length + c.needleFar.indices.length + c.rockFar.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.leaf.indices.length + c.needle.indices.length + c.rock.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
|
return (t / 3) | 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,11 +104,13 @@ export function renderBand(
|
||||||
if (chunkFar(c, camera.position, config.lodDistance)) {
|
if (chunkFar(c, camera.position, config.lodDistance)) {
|
||||||
Rasterizer.draw(fb, c.rockFar, tx.rock, viewProj, config, true, y0, y1)
|
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.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.leafFar, tx.leaf, viewProj, config, true, y0, y1)
|
||||||
Rasterizer.draw(fb, c.needleFar, tx.needle, viewProj, config, true, y0, y1)
|
Rasterizer.draw(fb, c.needleFar, tx.needle, viewProj, config, true, y0, y1)
|
||||||
} else {
|
} else {
|
||||||
Rasterizer.draw(fb, c.rock, tx.rock, viewProj, config, true, y0, y1)
|
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.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.leaf, tx.leaf, viewProj, config, true, y0, y1)
|
||||||
Rasterizer.draw(fb, c.needle, tx.needle, 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)
|
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`
|
* 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. */
|
* drives the per-tree random wobble so a forest doesn't look cloned. */
|
||||||
export type Tree = {
|
export type Tree = {
|
||||||
kind: "oak" | "spruce"
|
kind: "oak" | "spruce" | "birch"
|
||||||
/** Trunk base, sitting on the ground. */
|
/** Trunk base, sitting on the ground. */
|
||||||
position: Vec3
|
position: Vec3
|
||||||
growth: number
|
growth: number
|
||||||
|
|
@ -34,8 +34,10 @@ export namespace Tree {
|
||||||
const rand = rng(tree.seed)
|
const rand = rng(tree.seed)
|
||||||
if (tree.kind === "oak") {
|
if (tree.kind === "oak") {
|
||||||
oak(tree.position, tree.growth, rand, trunk, foliage, lod)
|
oak(tree.position, tree.growth, rand, trunk, foliage, lod)
|
||||||
} else {
|
} else if (tree.kind === "spruce") {
|
||||||
spruce(tree.position, tree.growth, rand, trunk, foliage, lod)
|
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. */
|
/** 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 {
|
function limb(mesh: Mesh, a: Vec3, b: Vec3, ra: number, rb: number, sides: number): void {
|
||||||
const axis = Vec3.normalize(Vec3.sub(b, a))
|
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]
|
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.
|
// Oak leaves: mid green, blotchy clumps with brighter highlights.
|
||||||
const leaf: Shade = (x, y) => {
|
const leaf: Shade = (x, y) => {
|
||||||
const n = noise(x, y) * 26
|
const n = noise(x, y) * 26
|
||||||
|
|
@ -249,6 +261,7 @@ 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],
|
["bark", 64, 64, bark],
|
||||||
|
["birch", 64, 64, birch],
|
||||||
["leaf", 64, 64, leaf],
|
["leaf", 64, 64, leaf],
|
||||||
["needle", 64, 64, needle],
|
["needle", 64, 64, needle],
|
||||||
["rock", 64, 64, rock],
|
["rock", 64, 64, rock],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue