feat: foilage
This commit is contained in:
parent
67cd54fe33
commit
6fb46573da
9 changed files with 270 additions and 19 deletions
|
|
@ -94,6 +94,7 @@ export namespace RenderConfig {
|
|||
colorDepth: 8,
|
||||
dither: 0,
|
||||
vertexSnap: 0,
|
||||
perspectiveCorrect: 1,
|
||||
textureFilter: "linear",
|
||||
lighting: "flat",
|
||||
fog: null,
|
||||
|
|
|
|||
76
engine/scene/Bush.ts
Normal file
76
engine/scene/Bush.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import type { Vec3 } from "../math/Vec3"
|
||||
import { STRIDE, type Mesh } from "./Mesh"
|
||||
|
||||
const TAU = Math.PI * 2
|
||||
|
||||
/** A low shrub: a tight cluster of small leafy blobs sitting on the ground.
|
||||
* Textured with the same leaf sheet as oak canopies, so it batches into the
|
||||
* chunk's foliage mesh. `size` is the overall spread; `seed` the per-bush wobble. */
|
||||
export type Bush = {
|
||||
position: Vec3
|
||||
size: number
|
||||
seed: number
|
||||
}
|
||||
|
||||
/** Low-poly bush geometry, same faceted flat-shaded style as the trees. A few
|
||||
* overlapping jittered spheres read as a rounded shrub; blobs are closed and
|
||||
* wound outward, so backface culling is safe. `build` appends into a shared
|
||||
* (leaf-textured) mesh. */
|
||||
export namespace Bush {
|
||||
export function build(bush: Bush, mesh: Mesh): void {
|
||||
const rand = rng(bush.seed)
|
||||
// A handful of smaller overlapping lumps reads as a soft shrub; one big
|
||||
// sphere reads as a boulder.
|
||||
const blobs = 3 + Math.floor(rand() * 3)
|
||||
const r = bush.size * 0.42
|
||||
for (let i = 0; i < blobs; i++) {
|
||||
const angle = rand() * TAU
|
||||
const dist = i === 0 ? 0 : bush.size * 0.5 * rand()
|
||||
const cx = bush.position.x + Math.cos(angle) * dist
|
||||
const cz = bush.position.z + Math.sin(angle) * dist
|
||||
const cy = bush.position.y + r * (0.5 + rand() * 0.4)
|
||||
blob(mesh, cx, cy, cz, r * (0.55 + rand() * 0.3), rand)
|
||||
}
|
||||
}
|
||||
|
||||
/** A small lumpy low-poly sphere, wound outward (matches the oak canopy blob). */
|
||||
function blob(mesh: Mesh, cx: number, cy: number, cz: number, radius: number, rand: () => number): void {
|
||||
const seg = 6
|
||||
const rings = 4
|
||||
const start = mesh.verts.length / STRIDE
|
||||
for (let r = 0; r <= rings; r++) {
|
||||
const phi = (r / rings) * Math.PI
|
||||
const cyv = Math.cos(phi)
|
||||
const crv = Math.sin(phi)
|
||||
const scale = radius * (0.9 + rand() * 0.18)
|
||||
for (let s = 0; s <= seg; s++) {
|
||||
const theta = (s / seg) * TAU
|
||||
mesh.verts.push(
|
||||
cx + crv * Math.cos(theta) * scale,
|
||||
cy + cyv * scale,
|
||||
cz + crv * Math.sin(theta) * scale,
|
||||
(s / seg) * 2,
|
||||
(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Deterministic 0..1 generator (mulberry32) seeded per bush. */
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
79
engine/scene/Flower.ts
Normal file
79
engine/scene/Flower.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import type { Vec3 } from "../math/Vec3"
|
||||
import { Mesh } from "./Mesh"
|
||||
|
||||
const TAU = Math.PI * 2
|
||||
|
||||
/** Flower bloom color, indexing a region of the `flower` texture atlas. */
|
||||
export type FlowerColor = "white" | "red" | "yellow"
|
||||
|
||||
/** A single small flower: a thin crossed-quad stem plus a shallow fan of petals.
|
||||
* Tiny, so it is drawn double-sided (no backface cull) and carries no collider.
|
||||
* `size` is roughly its height; `seed` jitters the petals. */
|
||||
export type Flower = {
|
||||
position: Vec3
|
||||
color: FlowerColor
|
||||
size: number
|
||||
seed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-poly flower geometry. The `flower` texture is a 2x2 color atlas -- green
|
||||
* (stem) plus white / red / yellow blooms -- and every vertex samples the flat
|
||||
* center of one region, so a flower is solid-colored with no per-flower texture
|
||||
* or draw call. `build` appends into one shared flower mesh.
|
||||
*/
|
||||
export namespace Flower {
|
||||
/** uv center of each bloom color's atlas region (tile units). */
|
||||
const BLOOM_UV: Record<FlowerColor, [number, number]> = {
|
||||
white: [0.75, 0.25],
|
||||
red: [0.25, 0.75],
|
||||
yellow: [0.75, 0.75],
|
||||
}
|
||||
/** uv center of the green stem region. */
|
||||
const STEM_U = 0.25
|
||||
const STEM_V = 0.25
|
||||
|
||||
export function build(flower: Flower, mesh: Mesh): void {
|
||||
const rand = rng(flower.seed)
|
||||
const p = flower.position
|
||||
const height = flower.size * (0.8 + rand() * 0.4)
|
||||
const bloomY = p.y + height
|
||||
const w = flower.size * 0.04
|
||||
// Stem: two thin crossed quads so it reads from any angle.
|
||||
stem(mesh, p.x, p.y, p.z, bloomY, w, 0)
|
||||
stem(mesh, p.x, p.y, p.z, bloomY, 0, w)
|
||||
// Bloom: a shallow fan of petals, center raised a touch so it domes.
|
||||
const [bu, bv] = BLOOM_UV[flower.color]
|
||||
const rad = flower.size * 0.38
|
||||
const center = Mesh.push(mesh, p.x, bloomY + rad * 0.3, p.z, bu, bv)
|
||||
const ring = center + 1
|
||||
const petals = 5
|
||||
for (let i = 0; i <= petals; i++) {
|
||||
const angle = (i / petals) * TAU + rand() * 0.4
|
||||
Mesh.push(mesh, p.x + Math.cos(angle) * rad, bloomY, p.z + Math.sin(angle) * rad, bu, bv)
|
||||
}
|
||||
for (let i = 0; i < petals; i++) {
|
||||
mesh.indices.push(center, ring + i, ring + i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** A thin vertical quad from the ground to `y1`, width along (dx, dz). */
|
||||
function stem(mesh: Mesh, x: number, y0: number, z: number, y1: number, dx: number, dz: number): void {
|
||||
const a = Mesh.push(mesh, x - dx, y0, z - dz, STEM_U, STEM_V)
|
||||
const b = Mesh.push(mesh, x + dx, y0, z + dz, STEM_U, STEM_V)
|
||||
const c = Mesh.push(mesh, x + dx, y1, z + dz, STEM_U, STEM_V)
|
||||
const d = Mesh.push(mesh, x - dx, y1, z - dz, STEM_U, STEM_V)
|
||||
mesh.indices.push(a, b, c, a, c, d)
|
||||
}
|
||||
|
||||
/** Deterministic 0..1 generator (mulberry32) seeded per flower. */
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue