Rebuild the foliage with leaf cards: leafier silhouette at two thirds the triangles
No files matched your search
@@ -8,6 +8,11 @@ Semantic Versioning.
|
||||
|
||||
### Changed
|
||||
|
||||
- Street greenery now reads as leaves instead of green rocks: the planter shrubs and tree crowns
|
||||
are built from leaf cards, which also cut their triangle count by about a third.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed every hat and hood from the NPC roster: the firefighter helmet is the only head gear
|
||||
left, so no character hides its face behind a cap. The street performer no longer wears the
|
||||
cone hat and carries the baton and board in the hands instead.
|
||||
|
||||
@@ -130,6 +130,12 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
gear library. The procedural library + MakeHuman head stay in the tree as the tested fallback
|
||||
(`build --source procedural`), not in the shipped roster. Silhouette variety is the known
|
||||
regression — three meshes for twelve roles — and the modular part library is the `0.9.10` fix.
|
||||
- **Foliage is leaf cards, not sphere stacks (2026-11-06):** `_shrub()` is one low-poly inner mass
|
||||
plus 18–53 two-triangle cards scattered on the canopy shell by a jittered Fibonacci sequence, each
|
||||
rotated onto the canopy normal and tilted off it. No alpha texture, so the flat-colour library and
|
||||
the Compatibility renderer are unaffected — and it is ~35 % cheaper than the sphere stack it
|
||||
replaced. If a shrub ever reads as a rock again, the cards were probably rotated to the normal
|
||||
without the random tilt (`spread`).
|
||||
- **Roster is eleven outfits over twelve roles (2026-11-06):** hoodie, Casual, Worker, Suit,
|
||||
Adventurer, Beach, Casual_2, Farmer, King, Punk (Swat is intaken but unused — it faces Godot −Z and
|
||||
every yaw attempt broke the skin: node rotation turns the skin with it, object rotation collapses
|
||||
|
||||
@@ -945,8 +945,11 @@ scorecard source, this milestone is the actionable list.
|
||||
- [ ] Rebuild the ranked prop candidates from §D: the foliage-language P1
|
||||
(`planter_box`/`round_planter` foliage, `street_tree`/`palm_tree`), plus the
|
||||
`newspaper_box` slot and `bollard` collar if the budget allows.
|
||||
- [ ] Replace the flat `_shrub()`/`_tree()` clumps with carded clusters and keep the 60k
|
||||
library triangle budget; measure before/after.
|
||||
- [x] Replace the flat `_shrub()` clumps with **leaf cards** (`_leaf_card()`), the foliage-language
|
||||
P1. Each shrub is now one low-poly inner mass wrapped in 18–53 two-triangle cards scattered on
|
||||
the canopy shell and tilted off its normal, so the silhouette is ragged instead of
|
||||
rock-like — and it is *cheaper*: `planter_box` 1,324 → 818 tris, `round_planter`
|
||||
1,240 → 808, `street_tree` 2,592 → 1,506. Props total 13,808 → 11,784 tris (budget 60k).
|
||||
- [ ] Review the 18 gear pieces for silhouette and strap/buckle readability and rebuild only what
|
||||
fails; re-run `fit` after every change.
|
||||
- [ ] Verify prop colliders and navigation obstacles against the final silhouettes.
|
||||
|
||||
@@ -975,7 +975,7 @@
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.5,
|
||||
1.08,
|
||||
1.118,
|
||||
0.5
|
||||
],
|
||||
"min": [
|
||||
@@ -993,8 +993,8 @@
|
||||
],
|
||||
"path": "assets/models/blender/props/planter_box.glb",
|
||||
"replaces": "generated/planter_box.glb",
|
||||
"triangles": 1324,
|
||||
"vertices": 2780
|
||||
"triangles": 818,
|
||||
"vertices": 1701
|
||||
},
|
||||
"protest_sign": {
|
||||
"attach": "hand",
|
||||
@@ -1049,14 +1049,14 @@
|
||||
"round_planter": {
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.432,
|
||||
1.12,
|
||||
0.399,
|
||||
1.175,
|
||||
0.36
|
||||
],
|
||||
"min": [
|
||||
-0.36,
|
||||
0,
|
||||
-0.363
|
||||
-0.376
|
||||
]
|
||||
},
|
||||
"category": "props",
|
||||
@@ -1066,8 +1066,8 @@
|
||||
"foliage_dark"
|
||||
],
|
||||
"path": "assets/models/blender/props/round_planter.glb",
|
||||
"triangles": 1240,
|
||||
"vertices": 2418
|
||||
"triangles": 808,
|
||||
"vertices": 1522
|
||||
},
|
||||
"scba_tank": {
|
||||
"attach": "back",
|
||||
@@ -1221,14 +1221,14 @@
|
||||
"street_tree": {
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
1.179,
|
||||
3.62,
|
||||
1.102
|
||||
1.15,
|
||||
3.634,
|
||||
1.053
|
||||
],
|
||||
"min": [
|
||||
-0.941,
|
||||
-1.041,
|
||||
0,
|
||||
-1.082
|
||||
-1.08
|
||||
]
|
||||
},
|
||||
"category": "props",
|
||||
@@ -1240,8 +1240,8 @@
|
||||
],
|
||||
"path": "assets/models/blender/props/street_tree.glb",
|
||||
"replaces": "WorldBuilder cone trees",
|
||||
"triangles": 2592,
|
||||
"vertices": 5620
|
||||
"triangles": 1506,
|
||||
"vertices": 3330
|
||||
},
|
||||
"sunglasses": {
|
||||
"attach": "head",
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 52 KiB |
@@ -285,6 +285,23 @@ The chosen s48 is the `head_proto` default; the rejected s24/s64 stay local unde
|
||||
front/three-quarter/profile) is
|
||||
[`blender_previews/sheet_head_proto_before_after.jpg`](blender_previews/sheet_head_proto_before_after.jpg).
|
||||
|
||||
## Foliage language P1 — resolved 2026-11-06
|
||||
|
||||
The ranked P1 was that shrubs read as rocks: `_shrub()` stacked spheres, so the silhouette was
|
||||
smooth and the light response was a single facet per clump.
|
||||
|
||||
`_leaf_card()` replaces the stack: one low-poly inner mass for opacity, then 18–53 two-triangle
|
||||
quads scattered over the canopy shell by a jittered Fibonacci distribution, each rotated onto the
|
||||
canopy normal and tilted off it. No alpha texture, so the flat-colour library and the Compatibility
|
||||
renderer are untouched.
|
||||
|
||||
| Prop | Before | After |
|
||||
| --- | --- | --- |
|
||||
| `planter_box` | 1,324 tris | 818 |
|
||||
| `round_planter` | 1,240 tris | 808 |
|
||||
| `street_tree` | 2,592 tris | 1,506 |
|
||||
| Props total (20) | 13,808 | 11,784 |
|
||||
|
||||
## Scorecard skeleton
|
||||
|
||||
Verdicts use `qa-review` semantics: **P0** (blocker: wrong scale, broken normals, budget fail),
|
||||
|
||||
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 132 KiB |
@@ -73,7 +73,7 @@ PROP_MAX_TRIS = 4000
|
||||
try:
|
||||
import bpy # type: ignore
|
||||
import bmesh # type: ignore
|
||||
from mathutils import Matrix, Vector # type: ignore
|
||||
from mathutils import Euler, Matrix, Vector # type: ignore
|
||||
except ImportError: # pragma: no cover - audit/sheet/list run outside Blender
|
||||
bpy = None
|
||||
|
||||
@@ -2621,20 +2621,73 @@ def build_gear(name):
|
||||
# Props (grounded, front = Blender -Y)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _shrub(name, center, radii, mat, lumps=7, seed=1, lump=0.42):
|
||||
"""Dense foliage cluster: many small flat-shaded clumps in two greens.
|
||||
def _leaf_card(name, size, centre, normal, mat, rng, spread=0.5):
|
||||
"""One flat leaf card: a 2-triangle quad, angled off the canopy normal.
|
||||
|
||||
Alternating light/dark clumps break the single-colour read that made the
|
||||
old shrub look like a rock; the facets still catch the light at game
|
||||
distance (the realism-pass foliage note).
|
||||
Cards are what makes foliage read as leaves instead of a rock: the silhouette
|
||||
becomes ragged and the light breaks across many small faces. They carry no
|
||||
alpha texture, so the material stays in the flat-colour library and the
|
||||
Compatibility renderer needs no transparency pass.
|
||||
"""
|
||||
bm = bmesh.new()
|
||||
corners = [(-size, -size * 0.7), (size, -size * 0.7),
|
||||
(size, size * 0.7), (-size, size * 0.7)]
|
||||
verts = [bm.verts.new((x, y, 0.0)) for x, y in corners]
|
||||
bm.faces.new(verts)
|
||||
mesh = bpy.data.meshes.new(name)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
# Face the card along the canopy normal, then tilt it so the cluster is not a
|
||||
# shell of coplanar quads.
|
||||
direction = Vector(normal)
|
||||
if direction.length < 1e-6:
|
||||
direction = Vector((0.0, 0.0, 1.0))
|
||||
direction.normalize()
|
||||
quat = Vector((0.0, 0.0, 1.0)).rotation_difference(direction)
|
||||
tilt = Euler((rng.uniform(-spread, spread), rng.uniform(-spread, spread),
|
||||
rng.uniform(0.0, math.tau)), "XYZ").to_quaternion()
|
||||
obj.rotation_mode = "QUATERNION"
|
||||
obj.rotation_quaternion = quat @ tilt
|
||||
obj.location = centre
|
||||
return _finish(obj, mat, smooth=False)
|
||||
|
||||
|
||||
def _shrub(name, center, radii, mat, lumps=7, seed=1, lump=0.42):
|
||||
"""Foliage cluster: a dark inner mass wrapped in leaf cards.
|
||||
|
||||
The old version was a stack of spheres, which read as a rock at distance. The
|
||||
inner mass keeps the canopy opaque and the cards give it a leaf silhouette and
|
||||
a broken light response; it also costs fewer triangles than the sphere stack.
|
||||
"""
|
||||
parts = []
|
||||
import random
|
||||
rng = random.Random(seed)
|
||||
mat_light = material("foliage")
|
||||
mat_dark = material("foliage_dark")
|
||||
parts.append(sphere(name, 1.0, center, mat, seg=10, rings=7, scale=radii,
|
||||
# Inner mass: one low-poly sphere at 72 % so the cards always have something
|
||||
# opaque behind them.
|
||||
parts.append(sphere(name, 1.0, center, mat, seg=8, rings=6,
|
||||
scale=(radii[0] * 0.72, radii[1] * 0.72, radii[2] * 0.72),
|
||||
smooth=False))
|
||||
card_count = 18 + lumps * 7
|
||||
card_size = max(radii) * 0.17
|
||||
for i in range(card_count):
|
||||
# Fibonacci-ish distribution over the ellipsoid, jittered.
|
||||
t = (i + 0.5) / card_count
|
||||
z = 1.0 - 2.0 * t
|
||||
r = math.sqrt(max(0.0, 1.0 - z * z))
|
||||
a = math.tau * i * 0.61803398875 + rng.uniform(-0.25, 0.25)
|
||||
off = Vector((center[0] + math.cos(a) * radii[0] * r * rng.uniform(0.86, 1.02),
|
||||
center[1] + math.sin(a) * radii[1] * r * rng.uniform(0.86, 1.02),
|
||||
center[2] + z * radii[2] * rng.uniform(0.86, 1.02)))
|
||||
normal = (off - Vector(center))
|
||||
normal.x /= max(radii[0], 1e-4)
|
||||
normal.y /= max(radii[1], 1e-4)
|
||||
normal.z /= max(radii[2], 1e-4)
|
||||
parts.append(_leaf_card(f"{name}Leaf", card_size * rng.uniform(0.75, 1.3), off,
|
||||
normal, mat_light if i % 3 else mat_dark, rng))
|
||||
for i in range(lumps * 2): # inner clumps fill the volume
|
||||
a = math.tau * i / (lumps * 2) + rng.uniform(-0.25, 0.25)
|
||||
ring = rng.uniform(0.25, 0.75)
|
||||
@@ -2643,8 +2696,9 @@ def _shrub(name, center, radii, mat, lumps=7, seed=1, lump=0.42):
|
||||
center[1] + math.sin(a) * radii[1] * ring,
|
||||
center[2] + rng.uniform(-0.2, 0.55) * radii[2])
|
||||
parts.append(sphere(name, 1.0, off, mat_light if i % 2 == 0 else mat_dark,
|
||||
seg=8, rings=6,
|
||||
scale=(radii[0] * r, radii[1] * r, radii[2] * r * 0.9),
|
||||
seg=6, rings=4,
|
||||
scale=(radii[0] * r * 0.8, radii[1] * r * 0.8,
|
||||
radii[2] * r * 0.75),
|
||||
smooth=False))
|
||||
for i in range(max(3, lumps // 2)): # outer clumps break the silhouette
|
||||
a = math.tau * i / max(3, lumps // 2) + rng.uniform(-0.3, 0.3)
|
||||
@@ -2653,8 +2707,9 @@ def _shrub(name, center, radii, mat, lumps=7, seed=1, lump=0.42):
|
||||
center[1] + math.sin(a) * radii[1] * rng.uniform(0.85, 1.0),
|
||||
center[2] + rng.uniform(-0.05, 0.4) * radii[2])
|
||||
parts.append(sphere(name, 1.0, off, mat_dark if i % 2 == 0 else mat_light,
|
||||
seg=8, rings=6,
|
||||
scale=(radii[0] * r, radii[1] * r, radii[2] * r * 0.8),
|
||||
seg=6, rings=4,
|
||||
scale=(radii[0] * r * 0.8, radii[1] * r * 0.8,
|
||||
radii[2] * r * 0.7),
|
||||
rot=(rng.uniform(0, 1), rng.uniform(0, 1), rng.uniform(0, 3)),
|
||||
smooth=False))
|
||||
return parts
|
||||
|
||||