feat: foilage
This commit is contained in:
parent
67cd54fe33
commit
6fb46573da
9 changed files with 270 additions and 19 deletions
|
|
@ -2,6 +2,7 @@ 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 flowerUrl from "../assets/flower.png"
|
||||
import grassUrl from "../assets/grass.png"
|
||||
import leafUrl from "../assets/leaf.png"
|
||||
import needleUrl from "../assets/needle.png"
|
||||
|
|
@ -16,6 +17,7 @@ export type Textures = {
|
|||
leaf: Texture
|
||||
needle: Texture
|
||||
rock: Texture
|
||||
flower: Texture
|
||||
wall: Texture
|
||||
crate: Texture
|
||||
npc: Texture
|
||||
|
|
@ -23,18 +25,19 @@ 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, wall, crate, npc] = await Promise.all([
|
||||
const [floor, grass, bark, leaf, needle, rock, flower, wall, crate, npc] = await Promise.all([
|
||||
loadTexture(floorUrl),
|
||||
loadTexture(grassUrl),
|
||||
loadTexture(barkUrl),
|
||||
loadTexture(leafUrl),
|
||||
loadTexture(needleUrl),
|
||||
loadTexture(rockUrl),
|
||||
loadTexture(flowerUrl),
|
||||
loadTexture(wallUrl),
|
||||
loadTexture(crateUrl),
|
||||
loadTexture(npcUrl),
|
||||
])
|
||||
return { floor, grass, bark, leaf, needle, rock, wall, crate, npc }
|
||||
return { floor, grass, bark, leaf, needle, rock, flower, wall, crate, npc }
|
||||
}
|
||||
|
||||
function loadTexture(url: string): Promise<Texture> {
|
||||
|
|
|
|||
75
app/level.ts
75
app/level.ts
|
|
@ -2,6 +2,8 @@ import { Color } from "../engine/render/Color"
|
|||
import type { CloudLayer, SkyConfig } from "../engine/render/Sky"
|
||||
import { STRIDE, type Mesh } from "../engine/scene/Mesh"
|
||||
import { Boulder } from "../engine/scene/Boulder"
|
||||
import { Bush } from "../engine/scene/Bush"
|
||||
import { Flower, type FlowerColor } from "../engine/scene/Flower"
|
||||
import { Terrain } from "../engine/scene/Terrain"
|
||||
import { Tree } from "../engine/scene/Tree"
|
||||
|
||||
|
|
@ -35,6 +37,8 @@ export type Chunk = {
|
|||
leaf: Mesh
|
||||
needle: Mesh
|
||||
rock: Mesh
|
||||
/** Flowers (color-atlas texture); drawn double-sided, so kept separate. */
|
||||
flowers: Mesh
|
||||
}
|
||||
|
||||
/** The playground: a flat-floored room dropped into the center of a big open
|
||||
|
|
@ -91,6 +95,16 @@ const BOULDER_COUNT = 70
|
|||
const BOULDER_SEED = 0xB0142
|
||||
const BOULDER_REACH = 0.7
|
||||
|
||||
/** Bushes + flowers: ground detail, kept to the nearer band since they're small
|
||||
* and fog/size hides them far out. Flowers roll white/red/yellow. */
|
||||
const BUSH_COUNT = 140
|
||||
const BUSH_SEED = 0xB554
|
||||
const BUSH_REACH = 0.35
|
||||
const FLOWER_COUNT = 340
|
||||
const FLOWER_SEED = 0xF10E
|
||||
const FLOWER_REACH = 0.3
|
||||
const FLOWER_COLORS: FlowerColor[] = ["white", "red", "yellow"]
|
||||
|
||||
/** Spatial partition of the world for frustum culling: `CHUNK_GRID` x
|
||||
* `CHUNK_GRID` square cells over [-outer, outer]. Smaller cells cull tighter
|
||||
* (less drawn off-screen) but cost more per-cell tests + bounds; this is the
|
||||
|
|
@ -175,15 +189,18 @@ export function buildLevel(): Level {
|
|||
// frustum-cullable spatial chunks.
|
||||
const trees = placeTrees(colliders)
|
||||
const boulders = placeBoulders(colliders)
|
||||
const chunks = buildChunks(trees, boulders)
|
||||
const bushes = placeBushes()
|
||||
const flowers = placeFlowers()
|
||||
const chunks = buildChunks(trees, boulders, bushes, flowers)
|
||||
|
||||
return { floor, walls, crate, chunks, colliders, npcPosition, terrain: TERRAIN, sky }
|
||||
}
|
||||
|
||||
/** Bake the terrain + props into a `CHUNK_GRID` x `CHUNK_GRID` set of spatial
|
||||
* chunks. Each prop lands in the cell holding its base; the cell's bounds are
|
||||
* grown to the real geometry so overhanging canopies never get culled early. */
|
||||
function buildChunks(trees: Tree[], boulders: Boulder[]): Chunk[] {
|
||||
* grown to the real geometry so overhanging canopies never get culled early.
|
||||
* Bushes share the oak leaf mesh; flowers get their own (double-sided) mesh. */
|
||||
function buildChunks(trees: Tree[], boulders: Boulder[], bushes: Bush[], flowers: Flower[]): Chunk[] {
|
||||
const cell = (TERRAIN.outer * 2) / CHUNK_GRID
|
||||
const chunks: Chunk[] = []
|
||||
for (let ci = 0; ci < CHUNK_GRID; ci++) {
|
||||
|
|
@ -197,6 +214,7 @@ function buildChunks(trees: Tree[], boulders: Boulder[]): Chunk[] {
|
|||
const leaf = mesh()
|
||||
const needle = mesh()
|
||||
const rock = mesh()
|
||||
const flowerMesh = 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)) {
|
||||
|
|
@ -208,11 +226,21 @@ function buildChunks(trees: Tree[], boulders: Boulder[]): Chunk[] {
|
|||
Boulder.build(boulder, rock)
|
||||
}
|
||||
}
|
||||
const b = bounds([grass, bark, leaf, needle, rock])
|
||||
for (const bush of bushes) {
|
||||
if (inCell(bush.position, x0, z0, x1, z1)) {
|
||||
Bush.build(bush, leaf)
|
||||
}
|
||||
}
|
||||
for (const flower of flowers) {
|
||||
if (inCell(flower.position, x0, z0, x1, z1)) {
|
||||
Flower.build(flower, flowerMesh)
|
||||
}
|
||||
}
|
||||
const b = bounds([grass, bark, leaf, needle, rock, flowerMesh])
|
||||
if (b === null) {
|
||||
continue
|
||||
}
|
||||
chunks.push({ ...b, grass, bark, leaf, needle, rock })
|
||||
chunks.push({ ...b, grass, bark, leaf, needle, rock, flowers: flowerMesh })
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
|
|
@ -302,6 +330,43 @@ function placeBoulders(colliders: Aabb[]): Boulder[] {
|
|||
return boulders
|
||||
}
|
||||
|
||||
/** Scatter bushes on the grass near the play area (no colliders -- walk through). */
|
||||
function placeBushes(): Bush[] {
|
||||
const rand = mulberry(BUSH_SEED)
|
||||
const maxDist = TERRAIN.outer * BUSH_REACH
|
||||
const bushes: Bush[] = []
|
||||
for (let guard = 0; bushes.length < BUSH_COUNT && guard < BUSH_COUNT * 20; guard++) {
|
||||
const angle = rand() * Math.PI * 2
|
||||
const dist = ARENA + 3 + rand() * (maxDist - ARENA - 3)
|
||||
const x = Math.cos(angle) * dist
|
||||
const z = Math.sin(angle) * dist
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) < TERRAIN.inner + 2) {
|
||||
continue
|
||||
}
|
||||
bushes.push({ position: { x, y: Terrain.height(TERRAIN, x, z), z }, size: 0.8 + rand() * 1, seed: (rand() * 0xFFFFFFFF) | 0 })
|
||||
}
|
||||
return bushes
|
||||
}
|
||||
|
||||
/** Scatter small flowers on the grass near the play area, colors rolled. */
|
||||
function placeFlowers(): Flower[] {
|
||||
const rand = mulberry(FLOWER_SEED)
|
||||
const maxDist = TERRAIN.outer * FLOWER_REACH
|
||||
const flowers: Flower[] = []
|
||||
for (let guard = 0; flowers.length < FLOWER_COUNT && guard < FLOWER_COUNT * 20; guard++) {
|
||||
const angle = rand() * Math.PI * 2
|
||||
const dist = ARENA + 2 + rand() * (maxDist - ARENA - 2)
|
||||
const x = Math.cos(angle) * dist
|
||||
const z = Math.sin(angle) * dist
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) < TERRAIN.inner + 1) {
|
||||
continue
|
||||
}
|
||||
const color = FLOWER_COLORS[(rand() * FLOWER_COLORS.length) | 0]
|
||||
flowers.push({ position: { x, y: Terrain.height(TERRAIN, x, z), z }, color, size: 0.28 + rand() * 0.22, seed: (rand() * 0xFFFFFFFF) | 0 })
|
||||
}
|
||||
return flowers
|
||||
}
|
||||
|
||||
/** Deterministic 0..1 generator (mulberry32) for tree placement. */
|
||||
function mulberry(seed: number): () => number {
|
||||
let a = seed >>> 0
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ async function main(): Promise<void> {
|
|||
Rasterizer.draw(fb, c.bark, textures.bark, viewProj, config, true)
|
||||
Rasterizer.draw(fb, c.leaf, textures.leaf, viewProj, config, true)
|
||||
Rasterizer.draw(fb, c.needle, textures.needle, viewProj, config, true)
|
||||
Rasterizer.draw(fb, c.flowers, textures.flower, viewProj, config)
|
||||
}
|
||||
Rasterizer.draw(fb, Sprite.billboard(npc, camera), npc.texture, viewProj, config)
|
||||
Framebuffer.quantize(fb, config)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue