extends RefCounted ## Procedural low-poly mesh builders: trees, rocks, bushes, logs, grass, flowers. ## ## Every builder takes a seed, so the same seed gives the same mesh on every ## machine (the determinism rule in docs/ARCHITECTURE.md ยง4). Meshes are ## flat-shaded and coloured per face through vertex colours (authored as sRGB). ## Callers assign materials per surface; tree dictionaries list which surface is ## which in "surfaces". ## ## Winding: Godot treats triangle (a, b, c) as front-facing when ## (b - a).cross(c - a) points AWAY from the viewer. `_tri()` orders the ## vertices from an outward hint, so builders never have to think about it. const BARK := Color(0.31, 0.22, 0.15) const BARK_GREY := Color(0.42, 0.38, 0.33) const BIRCH_WHITE := Color(0.86, 0.85, 0.80) const BIRCH_DARK := Color(0.16, 0.15, 0.14) const WOOD := Color(0.70, 0.55, 0.36) const SPRUCE := Color(0.13, 0.27, 0.17) const PINE := Color(0.18, 0.31, 0.16) const LEAF_GREEN := Color(0.27, 0.43, 0.16) const LEAF_GOLD := Color(0.62, 0.50, 0.15) const BIRCH_LEAF := Color(0.42, 0.54, 0.20) const BUSH := Color(0.19, 0.34, 0.14) const ROCK := Color(0.50, 0.49, 0.47) const STEM := Color(0.25, 0.42, 0.15) static var _ico_cache := {} # --- primitives ------------------------------------------------------------- static func _begin() -> SurfaceTool: var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) st.set_smooth_group(-1) # flat shading return st static func _commit(st: SurfaceTool, mesh: ArrayMesh = null) -> ArrayMesh: st.generate_normals() return st.commit(mesh) static func _mul(c: Color, k: float) -> Color: return Color(c.r * k, c.g * k, c.b * k, 1.0) static func _tri(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, outward: Vector3, col: Color) -> void: if (b - a).cross(c - a).dot(outward) > 0.0: var tmp := b b = c c = tmp st.set_color(col) st.add_vertex(a) st.set_color(col) st.add_vertex(b) st.set_color(col) st.add_vertex(c) ## Tapered cylinder from `base` to `top`. `off` fixes the ring rotation so ## stacked segments line up; cap_col.a > 0 adds end caps in that colour. static func _cylinder(st: SurfaceTool, base: Vector3, top: Vector3, r0: float, r1: float, segs: int, col: Color, rng: RandomNumberGenerator, off: float = -1.0, cap_col: Color = Color(0, 0, 0, 0)) -> void: var ax := (top - base).normalized() var ref := Vector3.UP if absf(ax.y) < 0.95 else Vector3.RIGHT var u := ax.cross(ref).normalized() var v := ax.cross(u).normalized() if off < 0.0: off = rng.randf() * TAU var ring0: Array[Vector3] = [] var ring1: Array[Vector3] = [] for i in range(segs): var a := off + TAU * i / segs var dir := u * cos(a) + v * sin(a) ring0.append(base + dir * r0) ring1.append(top + dir * r1) for i in range(segs): var j := (i + 1) % segs var mid := (ring0[i] + ring0[j] + ring1[i] + ring1[j]) * 0.25 var out := mid - (base + ax * (mid - base).dot(ax)) var c := _mul(col, rng.randf_range(0.85, 1.1)) _tri(st, ring0[i], ring0[j], ring1[j], out, c) _tri(st, ring0[i], ring1[j], ring1[i], out, c) if cap_col.a > 0.0: for i in range(segs): var j := (i + 1) % segs _tri(st, top, ring1[i], ring1[j], ax, cap_col) _tri(st, base, ring0[i], ring0[j], -ax, cap_col) static func icosphere(subdiv: int) -> Array: if _ico_cache.has(subdiv): return _ico_cache[subdiv] var t := (1.0 + sqrt(5.0)) / 2.0 var v := [ Vector3(-1, t, 0), Vector3(1, t, 0), Vector3(-1, -t, 0), Vector3(1, -t, 0), Vector3(0, -1, t), Vector3(0, 1, t), Vector3(0, -1, -t), Vector3(0, 1, -t), Vector3(t, 0, -1), Vector3(t, 0, 1), Vector3(-t, 0, -1), Vector3(-t, 0, 1), ] var faces := [ [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], ] var tris := [] for f in faces: tris.append([(v[f[0]] as Vector3).normalized(), (v[f[1]] as Vector3).normalized(), (v[f[2]] as Vector3).normalized()]) for _s in range(subdiv): var nt := [] for tr in tris: var a: Vector3 = tr[0] var b: Vector3 = tr[1] var c: Vector3 = tr[2] var ab := ((a + b) * 0.5).normalized() var bc := ((b + c) * 0.5).normalized() var ca := ((c + a) * 0.5).normalized() nt.append([a, ab, ca]) nt.append([b, bc, ab]) nt.append([c, ca, bc]) nt.append([ab, bc, ca]) tris = nt _ico_cache[subdiv] = tris return tris ## Lumpy flat-shaded sphere. Displacement is a function of position, so shared ## corners move together and the surface stays watertight. static func _blob(st: SurfaceTool, center: Vector3, radii: Vector3, noise: FastNoiseLite, amount: float, col: Color, rng: RandomNumberGenerator, subdiv: int = 1) -> void: for tr in icosphere(subdiv): var p: Array[Vector3] = [] for k in range(3): var q: Vector3 = tr[k] var d := 1.0 + noise.get_noise_3dv(q * 1.7 + center) * amount p.append(center + q * radii * d) _tri(st, p[0], p[1], p[2], (p[0] + p[1] + p[2]) / 3.0 - center, _mul(col, rng.randf_range(0.88, 1.1))) ## One drooping tier of a conifer: a star-shaped ring (alternating branch tips ## and notches) rising to an apex, with a darker underside. static func _conifer_tier(st: SurfaceTool, center: Vector3, r: float, h: float, segs: int, col: Color, rng: RandomNumberGenerator) -> void: var n := segs * 2 var ring: Array[Vector3] = [] var off := rng.randf() * TAU for i in range(n): var a := off + TAU * i / n + rng.randf_range(-0.12, 0.12) var tip := i % 2 == 0 var rr := r * (rng.randf_range(0.9, 1.12) if tip else rng.randf_range(0.58, 0.7)) var yy := -(r * 0.32 if tip else r * 0.12) + rng.randf_range(-0.08, 0.08) ring.append(center + Vector3(cos(a) * rr, yy, sin(a) * rr)) var apex := center + Vector3(rng.randf_range(-0.08, 0.08), h, rng.randf_range(-0.08, 0.08)) var under := center + Vector3(0.0, h * 0.18, 0.0) for i in range(n): var a := ring[i] var b := ring[(i + 1) % n] var mid := (a + b) * 0.5 - center var out := Vector3(mid.x, 0.0, mid.z).normalized() + Vector3(0.0, 0.8, 0.0) _tri(st, apex, a, b, out, _mul(col, rng.randf_range(0.85, 1.12))) _tri(st, under, a, b, Vector3(mid.x * 0.3, -1.0, mid.z * 0.3), _mul(col, 0.55)) # --- trees ------------------------------------------------------------------ ## Spruce (bushy, tiers from near the ground) or pine (tall, tiers only up top). static func conifer(seed: int, tall: bool) -> Dictionary: var rng := RandomNumberGenerator.new() rng.seed = seed var bark := _begin() var leaf := _begin() var trunk_h := rng.randf_range(8.5, 10.5) if tall else rng.randf_range(5.5, 7.0) var trunk_r := 0.26 if tall else 0.3 var lean := Vector3(rng.randf_range(-0.15, 0.15), 0.0, rng.randf_range(-0.15, 0.15)) _cylinder(bark, Vector3(0, -0.5, 0), Vector3(0, trunk_h, 0) + lean, trunk_r, 0.05, 7, BARK, rng) var tiers := rng.randi_range(4, 5) if tall else rng.randi_range(5, 7) var y0 := trunk_h * 0.55 if tall else 1.3 var y1 := trunk_h - 0.6 var r_bottom := 1.7 if tall else 2.4 var col := _mul(PINE if tall else SPRUCE, rng.randf_range(0.9, 1.1)) for i in range(tiers): var f := float(i) / float(tiers - 1) var y := lerpf(y0, y1, f) var r := lerpf(r_bottom, 0.55, pow(f, 0.9)) * rng.randf_range(0.9, 1.1) _conifer_tier(leaf, Vector3(0, y, 0) + lean * (y / trunk_h), r, lerpf(1.9, 1.3, f), 9, col, rng) _conifer_tier(leaf, Vector3(0, y1 + 0.9, 0) + lean, 0.35, 1.1, 5, col, rng) var mesh := _commit(bark) _commit(leaf, mesh) return {"mesh": mesh, "surfaces": ["bark", "leaf"], "trunk_r": trunk_r, "trunk_h": trunk_h} ## Broadleaf (oak-ish): short thick trunk, a few branches, a crown of blobs. static func broadleaf(seed: int, leaf_col: Color) -> Dictionary: var rng := RandomNumberGenerator.new() rng.seed = seed var noise := FastNoiseLite.new() noise.seed = seed noise.frequency = 1.3 var bark := _begin() var leaf := _begin() var trunk_h := rng.randf_range(2.6, 3.6) var top := Vector3(rng.randf_range(-0.3, 0.3), trunk_h, rng.randf_range(-0.3, 0.3)) _cylinder(bark, Vector3(0, -0.4, 0), top, 0.36, 0.2, 7, BARK, rng) var k := rng.randi_range(3, 4) for i in range(k): var a := TAU * i / k + rng.randf_range(-0.4, 0.4) var start := top * rng.randf_range(0.75, 1.0) var end := top + Vector3(cos(a) * rng.randf_range(1.1, 1.8), rng.randf_range(0.6, 1.4), sin(a) * rng.randf_range(1.1, 1.8)) _cylinder(bark, start, end, 0.15, 0.06, 5, BARK, rng) var crown := top + Vector3(0, 1.4, 0) var col := _mul(leaf_col, rng.randf_range(0.92, 1.08)) var blobs := rng.randi_range(6, 9) for i in range(blobs): var c := crown + Vector3(rng.randf_range(-1.7, 1.7), rng.randf_range(-0.6, 1.2), rng.randf_range(-1.7, 1.7)) if i == 0: c = crown + Vector3(0, 0.8, 0) var r := rng.randf_range(1.0, 1.6) _blob(leaf, c, Vector3(r, r * 0.82, r), noise, 0.28, _mul(col, rng.randf_range(0.85, 1.15)), rng) var mesh := _commit(bark) _commit(leaf, mesh) return {"mesh": mesh, "surfaces": ["bark", "leaf"], "trunk_r": 0.36, "trunk_h": trunk_h} ## Birch: slim white trunk with dark bands, light airy crown. static func birch(seed: int) -> Dictionary: var rng := RandomNumberGenerator.new() rng.seed = seed var noise := FastNoiseLite.new() noise.seed = seed noise.frequency = 1.5 var bark := _begin() var leaf := _begin() var trunk_h := rng.randf_range(6.0, 8.0) var lean := Vector3(rng.randf_range(-0.4, 0.4), 0.0, rng.randf_range(-0.4, 0.4)) var off := rng.randf() * TAU var y := -0.4 while y < trunk_h: var dark := rng.randf() < 0.3 var seg_h := rng.randf_range(0.12, 0.22) if dark else rng.randf_range(0.5, 1.0) var y2 := minf(y + seg_h, trunk_h) var r0 := lerpf(0.18, 0.05, maxf(y, 0.0) / trunk_h) var r1 := lerpf(0.18, 0.05, y2 / trunk_h) var col := BIRCH_DARK if dark else _mul(BIRCH_WHITE, rng.randf_range(0.92, 1.03)) _cylinder(bark, Vector3(0, y, 0) + lean * (maxf(y, 0.0) / trunk_h), Vector3(0, y2, 0) + lean * (y2 / trunk_h), r0, r1, 6, col, rng, off) y = y2 var col_leaf := _mul(BIRCH_LEAF, rng.randf_range(0.92, 1.08)) for i in range(rng.randi_range(5, 7)): var cy := rng.randf_range(trunk_h * 0.5, trunk_h + 0.6) var c := Vector3(rng.randf_range(-0.9, 0.9), cy, rng.randf_range(-0.9, 0.9)) + lean * (cy / trunk_h) var r := rng.randf_range(0.7, 1.1) _blob(leaf, c, Vector3(r, r * 1.35, r), noise, 0.3, _mul(col_leaf, rng.randf_range(0.85, 1.15)), rng) var mesh := _commit(bark) _commit(leaf, mesh) return {"mesh": mesh, "surfaces": ["bark", "leaf"], "trunk_r": 0.2, "trunk_h": trunk_h} ## Dead tree: bare grey trunk with forked branches. static func dead_tree(seed: int) -> Dictionary: var rng := RandomNumberGenerator.new() rng.seed = seed var bark := _begin() var trunk_h := rng.randf_range(4.0, 6.0) var top := Vector3(rng.randf_range(-0.3, 0.3), trunk_h, rng.randf_range(-0.3, 0.3)) _cylinder(bark, Vector3(0, -0.4, 0), top, 0.28, 0.07, 6, BARK_GREY, rng) for i in range(rng.randi_range(4, 6)): var a := rng.randf() * TAU var start := top * rng.randf_range(0.45, 0.95) var end := start + Vector3(cos(a) * rng.randf_range(0.8, 1.6), rng.randf_range(0.5, 1.5), sin(a) * rng.randf_range(0.8, 1.6)) _cylinder(bark, start, end, 0.09, 0.03, 4, BARK_GREY, rng) var a2 := a + rng.randf_range(-0.8, 0.8) _cylinder(bark, end, end + Vector3(cos(a2) * 0.6, rng.randf_range(0.3, 0.8), sin(a2) * 0.6), 0.04, 0.015, 4, BARK_GREY, rng) return {"mesh": _commit(bark), "surfaces": ["bark"], "trunk_r": 0.28, "trunk_h": trunk_h} # --- small props ------------------------------------------------------------ static func bush(seed: int) -> Dictionary: var rng := RandomNumberGenerator.new() rng.seed = seed var noise := FastNoiseLite.new() noise.seed = seed noise.frequency = 2.0 var leaf := _begin() var col := _mul(BUSH, rng.randf_range(0.9, 1.15)) for i in range(rng.randi_range(3, 5)): var r := rng.randf_range(0.45, 0.8) var c := Vector3(rng.randf_range(-0.55, 0.55), rng.randf_range(0.25, 0.6), rng.randf_range(-0.55, 0.55)) _blob(leaf, c, Vector3(r, r * 0.8, r), noise, 0.3, _mul(col, rng.randf_range(0.85, 1.15)), rng) return {"mesh": _commit(leaf), "surfaces": ["leaf"]} ## Faceted rock. `tall` gives slab/pillar shapes for cliff outcrops. static func rock(seed: int, tall: bool) -> ArrayMesh: var rng := RandomNumberGenerator.new() rng.seed = seed var noise := FastNoiseLite.new() noise.seed = seed noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH noise.frequency = 0.9 var squash := Vector3(rng.randf_range(0.9, 1.4), rng.randf_range(0.55, 0.85), rng.randf_range(0.8, 1.2)) if tall: squash = Vector3(rng.randf_range(0.7, 1.0), rng.randf_range(1.2, 1.6), rng.randf_range(0.6, 0.9)) var st := _begin() var base_col := _mul(ROCK, rng.randf_range(0.9, 1.1)) for tr in icosphere(1): var p: Array[Vector3] = [] for k in range(3): var q: Vector3 = tr[k] q = q * (1.0 + noise.get_noise_3dv(q) * 0.38) * squash q.y = maxf(q.y, -0.35 * squash.y) # flat-ish base so it sits on the ground p.append(q) _tri(st, p[0], p[1], p[2], p[0] + p[1] + p[2], _mul(base_col, rng.randf_range(0.82, 1.12))) return _commit(st) static func log_mesh(seed: int, length: float, radius: float) -> ArrayMesh: var rng := RandomNumberGenerator.new() rng.seed = seed var st := _begin() _cylinder(st, Vector3(-length * 0.5, 0, 0), Vector3(length * 0.5, 0, 0), radius, radius * 0.85, 8, BARK, rng, -1.0, WOOD) return _commit(st) static func stump_mesh(seed: int, height: float, radius: float) -> ArrayMesh: var rng := RandomNumberGenerator.new() rng.seed = seed var st := _begin() _cylinder(st, Vector3(0, -0.3, 0), Vector3(0, height, 0), radius * 1.15, radius, 8, BARK, rng, -1.0, WOOD) return _commit(st) ## A clump of tapered blades. UV.y = 0 at the root, 1 at the tip (drives wind). static func grass_clump(seed: int, blades: int = 10) -> ArrayMesh: var rng := RandomNumberGenerator.new() rng.seed = seed var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) for i in range(blades): var a := rng.randf() * TAU var rr := sqrt(rng.randf()) * 0.3 var pos := Vector3(cos(a) * rr, -0.05, sin(a) * rr) var face := rng.randf() * TAU var side := Vector3(cos(face), 0.0, sin(face)) var fwd := Vector3(-side.z, 0.0, side.x) var h := rng.randf_range(0.32, 0.72) var w := rng.randf_range(0.035, 0.06) var bend := rng.randf_range(0.08, 0.28) var b0 := pos - side * w var b1 := pos + side * w var mc := pos + fwd * bend * 0.35 + Vector3(0, h * 0.55, 0) var m0 := mc - side * w * 0.7 var m1 := mc + side * w * 0.7 var tip := pos + fwd * bend + Vector3(0, h, 0) var u := rng.randf() var data := [[b0, 0.0], [b1, 0.0], [m1, 0.55], [b0, 0.0], [m1, 0.55], [m0, 0.55], [m0, 0.55], [m1, 0.55], [tip, 1.0]] for d in data: st.set_normal(Vector3.UP) st.set_uv(Vector2(u, d[1])) st.add_vertex(d[0]) return st.commit() ## Small flower: a stem (surface 0) and a five-petal head (surface 1, white so ## the MultiMesh instance colour tints it). static func flower(seed: int) -> ArrayMesh: var rng := RandomNumberGenerator.new() rng.seed = seed var stem := _begin() var head := _begin() var h := 0.32 var c := Vector3(0, h, 0) _tri(stem, Vector3(-0.012, -0.05, 0), Vector3(0.012, -0.05, 0), c, Vector3.BACK, STEM) _tri(stem, Vector3(0, -0.05, -0.012), Vector3(0, -0.05, 0.012), c, Vector3.RIGHT, STEM) for k in range(5): var a := TAU * k / 5.0 var tip := c + Vector3(cos(a) * 0.085, 0.025, sin(a) * 0.085) var l := c + Vector3(cos(a - 0.45) * 0.045, 0.012, sin(a - 0.45) * 0.045) var r := c + Vector3(cos(a + 0.45) * 0.045, 0.012, sin(a + 0.45) * 0.045) _tri(head, c, l, tip, Vector3.UP, Color.WHITE) _tri(head, c, tip, r, Vector3.UP, Color.WHITE) var mesh := _commit(stem) _commit(head, mesh) return mesh