The stylized tree's foliage was a stack of ellipsoid lobes (canopy) or loose
per-tip cards (cards): the first reads as a bunch of grapes, the second clumps
and leaves holes because every card is drawn independently.
Add a cluster foliage mode built the way stylized foliage is normally authored:
* leaf centres on a plane are blue noise (Poisson disk), so the distribution
has no clumps and no bare patches;
* only the leaves emit geometry, which is how a CPU mesh expresses
'leaves opaque, everything else transparent' without alpha sorting or
transparent overdraw;
* 8-10 such planes are placed around the cluster's Y axis with a small random
tilt and offset, plus a few near-horizontal planes that close the poles;
* vertex normals are transferred from the cluster sphere instead of the leaf's
own plane, so a bundle of flat cards still shades as a rounded mass;
* both windings are emitted, because the engine's mesh3d pipelines cull back
faces unless a Material opts into two-sided rasterization.
Cluster centres are picked from the existing foliage anchors in a shuffled order
under a Poisson separation test, so they cover the crown evenly, stay inside a
hard geometry budget, and the twigs visibly enter the foliage.
FoliageCluster is a standalone implementation unit with its own
FoliageClusterDesc so other recipes can reuse it, and its disk sampling goes
through the module's existing Bridson sampler (poissonDiskPoints) instead of a
second blue-noise implementation.
Also fix the mesh.tree schema, which advertised clusters and colonization as
leafMode/branchAlgorithm choices while the generator rejected both, and was
missing canopy.
Tests live in their own translation unit (the shared procgen.cpp is already
~4500 lines): property tests for the sampler, the sphere normal transfer and
two-sided emission, a case asserting every cluster knob is exposed in the
schema and actually changes the mesh, and an opt-in step-by-step render dump
(EVENGINE_TREE_STEPS_DIR) that draws each construction stage.
Verified with ctest -R 'mesh\.|poisson' (136/136).
90 lines
3.4 KiB
Plaintext
90 lines
3.4 KiB
Plaintext
// Arbor Lab — interactive procedural tree recipe showcase.
|
|
// R regenerate, 1/2 Low Poly/Realistic, A algorithm, L cycle leaf mode, [/] density.
|
|
|
|
persist treeSeed = 31415
|
|
persist treeStyle = "lowpoly"
|
|
persist treeAlgorithm = "weberPenn"
|
|
persist leafMode = "clusters"
|
|
persist leafDensity = 0.65
|
|
persist treeMesh = null
|
|
persist treeObject = null
|
|
persist treeCamera = null
|
|
persist treeYaw = 0.0
|
|
function pressed(k) {
|
|
return key_just_pressed(k);
|
|
}
|
|
|
|
function rebuildTree() {
|
|
local paramsResult = procgen.newParams();
|
|
if (!paramsResult.ok) return;
|
|
local p = paramsResult.value;
|
|
p.setSeed(treeSeed);
|
|
p.setString("style", treeStyle);
|
|
p.setString("branchAlgorithm", treeAlgorithm);
|
|
p.setString("leafMode", leafMode);
|
|
p.setFloat("leafDensity", leafDensity);
|
|
p.setFloat("height", 6.2);
|
|
p.setFloat("crownRadius", 2.15);
|
|
p.setInt("branchLevels", treeStyle == "realistic" ? 3 : 2);
|
|
p.setInt("branchCount", treeStyle == "realistic" ? 10 : 6);
|
|
// Blue-noise leaf clusters: a sphere of leaf planes per foliage anchor.
|
|
// Denser, more separated clusters give a continuous crown instead of a few
|
|
// big lobes; smaller leaves suit the realistic style.
|
|
p.setFloat("clusterSize", treeStyle == "realistic" ? 0.26 : 0.30);
|
|
p.setFloat("clusterSeparation", treeStyle == "realistic" ? 0.45 : 0.55);
|
|
p.setFloat("clusterLeafScale", treeStyle == "realistic" ? 0.70 : 0.85);
|
|
p.setInt("clusterPlanes", treeStyle == "realistic" ? 12 : 10);
|
|
p.setInt("clusterLeaves", treeStyle == "realistic" ? 34 : 28);
|
|
p.setInt("clusterLimit", treeStyle == "realistic" ? 160 : 120);
|
|
local meshResult = procgen.generateMesh("mesh.tree", p, gfx);
|
|
if (!meshResult.ok) return;
|
|
treeMesh = meshResult.value;
|
|
if (treeObject == null) {
|
|
treeObject = eve.Renderable3D();
|
|
treeObject.setPosition(1.35, -3.0, 0.0);
|
|
treeObject.setTint(0.34, 0.48, 0.22, 1.0);
|
|
treeObject.setRoughness(0.86);
|
|
treeObject.setCastShadow(true);
|
|
}
|
|
treeObject.setMesh(treeMesh);
|
|
}
|
|
|
|
if (treeCamera == null) {
|
|
treeCamera = eve.Camera3D();
|
|
treeCamera.setEye(10.0, 4.6, 12.0);
|
|
treeCamera.setTarget(1.1, 0.2, 0.0);
|
|
treeCamera.setUp(0.0, 1.0, 0.0);
|
|
treeCamera.setFov(38.0);
|
|
treeCamera.setAmbient(0.32, 0.38, 0.30);
|
|
treeCamera.setActive(true);
|
|
gfx.setDirectionalLight(-0.45, -1.0, -0.35, 1.35, 1.24, 1.02);
|
|
}
|
|
if (treeMesh == null) rebuildTree();
|
|
gfx.setBackgroundColor(0.055, 0.075, 0.07, 1.0);
|
|
|
|
function eve_update(dt) {
|
|
treeYaw += dt * 7.0;
|
|
treeObject.setYaw(treeYaw);
|
|
if (pressed("r") || pressed("R")) { treeSeed += 1; rebuildTree(); }
|
|
if (pressed("1")) { treeStyle = "lowpoly"; rebuildTree(); }
|
|
if (pressed("2")) { treeStyle = "realistic"; rebuildTree(); }
|
|
if (pressed("a") || pressed("A")) {
|
|
treeAlgorithm = treeAlgorithm == "weberPenn" ? "spaceColonization" : "weberPenn";
|
|
rebuildTree();
|
|
}
|
|
if (pressed("l") || pressed("L")) {
|
|
leafMode = leafMode == "clusters" ? "cards" :
|
|
leafMode == "cards" ? "canopy" :
|
|
leafMode == "canopy" ? "none" : "clusters";
|
|
rebuildTree();
|
|
}
|
|
if (pressed("leftbracket")) { leafDensity = max(0.0, leafDensity - 0.1); rebuildTree(); }
|
|
if (pressed("rightbracket")) { leafDensity = min(1.0, leafDensity + 0.1); rebuildTree(); }
|
|
}
|
|
|
|
function eve_render() {
|
|
gfx.clear();
|
|
gfx.render3D();
|
|
gfx.drawSolidRect(28.0, 28.0, 310.0, 212.0, 0.92, 0.94, 0.86, 0.96);
|
|
}
|