Files
blight/blight-vegetation-generator/src/main/java/de/blight/eztree/Tree.java
2026-06-16 07:03:08 +02:00

416 lines
17 KiB
Java

package de.blight.eztree;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.util.BufferUtils;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
/**
* JME3 port of the ez-tree procedural tree generator.
*
* After construction call {@link #generate()} (or re-call it to regenerate).
* The resulting scene graph has two child {@link Geometry} nodes:
* <ul>
* <li>{@code "bark"} — cylinder mesh with VertexBuffer.Color.R = wind factor</li>
* <li>{@code "leaves"} — quad mesh with VertexBuffer.Color.R = wind factor</li>
* </ul>
* Materials must be assigned by the caller.
*/
public class Tree extends Node {
private final TreeOptions opts;
// Accumulated geometry data, reset on each generate() call
private final List<Float> barkPos = new ArrayList<>();
private final List<Float> barkNorm = new ArrayList<>();
private final List<Float> barkUV = new ArrayList<>();
private final List<Integer> barkIdx = new ArrayList<>();
private final List<Float> barkWind = new ArrayList<>();
private final List<Float> leafPos = new ArrayList<>();
private final List<Float> leafNorm = new ArrayList<>();
private final List<Float> leafUV = new ArrayList<>();
private final List<Integer> leafIdx = new ArrayList<>();
private final List<Float> leafWind = new ArrayList<>();
/** No-arg constructor required by JME3 BinaryImporter. */
public Tree() {
super("EzTree");
this.opts = new TreeOptions();
}
public Tree(TreeOptions opts) {
super("EzTree");
this.opts = opts;
}
public TreeOptions getOptions() { return opts; }
// ── Public API ───────────────────────────────────────────────────────────
/** Clears previous geometry and rebuilds the tree mesh. */
public void generate() {
detachAllChildren();
barkPos.clear(); barkNorm.clear(); barkUV.clear(); barkIdx.clear(); barkWind.clear();
leafPos.clear(); leafNorm.clear(); leafUV.clear(); leafIdx.clear(); leafWind.clear();
Rng rng = new Rng(opts.seed);
Branch trunk = new Branch();
trunk.level = 0;
trunk.length = opts.branch.getLength(0);
trunk.radius = opts.branch.getRadius(0);
Queue<Branch> queue = new ArrayDeque<>();
queue.add(trunk);
while (!queue.isEmpty()) {
processBranch(queue.poll(), rng, queue);
}
if (!barkPos.isEmpty()) {
attachChild(buildGeometry("bark", barkPos, barkNorm, barkUV, barkIdx, barkWind));
}
if (!leafPos.isEmpty()) {
attachChild(buildGeometry("leaves", leafPos, leafNorm, leafUV, leafIdx, leafWind));
}
if (opts.trellis.enabled) {
Node trellisNode = Trellis.build(opts.trellis);
attachChild(trellisNode);
}
}
// ── Branch processing ────────────────────────────────────────────────────
private void processBranch(Branch branch, Rng rng, Queue<Branch> queue) {
BranchOptions bo = opts.branch;
int level = branch.level;
int sections = bo.getSections(level);
int segments = bo.getSegments(level);
float gnarliness = bo.getGnarliness(level);
float taper = bo.getTaper(level);
float twistStep = bo.getTwist(level) * FastMath.DEG_TO_RAD;
float sectionLen = branch.length / sections;
float startRad = branch.radius;
float endRad = startRad * taper;
// Initial direction = orientation applied to Y+
Vector3f dir = branch.orientation.mult(Vector3f.UNIT_Y).normalizeLocal();
Vector3f pos = branch.origin.clone();
// Parallel-transport frame for smooth cylinder rings
Vector3f frameRight = findPerp(dir);
Vector3f frameUp = dir.cross(frameRight).normalizeLocal();
List<BranchSection> pts = new ArrayList<>(sections + 1);
pts.add(new BranchSection(pos.clone(), startRad, dir.clone(), 0f));
for (int i = 0; i < sections; i++) {
float t = (i + 1f) / sections;
// Gnarliness: random XZ perturbation (abs used so negative JSON values work too)
float absG = Math.abs(gnarliness);
if (absG > 0f) {
dir.x += rng.range(-absG, absG);
dir.z += rng.range(-absG, absG);
dir.normalizeLocal();
}
// External force (gravity / wind bias)
Vector3f fd = bo.force.direction;
float fs = bo.force.strength;
if (fs != 0f) {
dir.x += fd.x * fs;
dir.y += fd.y * fs;
dir.z += fd.z * fs;
dir.normalizeLocal();
}
// Twist: spin the transport frame around the branch axis
if (twistStep != 0f) {
Quaternion twistQ = new Quaternion().fromAngleAxis(twistStep, dir);
frameRight = twistQ.mult(frameRight).normalizeLocal();
frameUp = dir.cross(frameRight).normalizeLocal();
}
pos.addLocal(dir.mult(sectionLen));
float radius = startRad + (endRad - startRad) * t;
pts.add(new BranchSection(pos.clone(), radius, dir.clone(), t));
}
addCylinderGeometry(pts, segments, taper);
if (level < bo.levels) {
generateChildren(branch, pts, rng, queue);
}
// Leaves on the last (finest) level branches
if (level == bo.levels) {
generateLeaves(pts, rng);
}
}
// ── Cylinder mesh contribution ───────────────────────────────────────────
private void addCylinderGeometry(List<BranchSection> pts, int segments, float taper) {
int vertsPerRing = segments + 1;
int baseVertex = barkPos.size() / 3;
// Parallel-transport frame
Vector3f frameRight = findPerp(pts.get(0).dir());
Vector3f frameUp = pts.get(0).dir().cross(frameRight).normalizeLocal();
for (BranchSection sp : pts) {
Vector3f d = sp.dir().normalize();
// Transport the frame to the new direction
float dot = frameRight.dot(d);
frameRight = frameRight.subtract(d.mult(dot));
if (frameRight.lengthSquared() < 1e-6f) {
frameRight = findPerp(d);
} else {
frameRight.normalizeLocal();
}
frameUp = d.cross(frameRight).normalizeLocal();
float uv_v = (1f - sp.t()) * opts.bark.textureScaleY;
for (int seg = 0; seg <= segments; seg++) {
float angle = (float) seg / segments * FastMath.TWO_PI;
float cosA = FastMath.cos(angle);
float sinA = FastMath.sin(angle);
// Vertex = center + radial offset along (right, up)
float ox = frameRight.x * cosA + frameUp.x * sinA;
float oy = frameRight.y * cosA + frameUp.y * sinA;
float oz = frameRight.z * cosA + frameUp.z * sinA;
float r = sp.radius();
barkPos.add(sp.pos().x + ox * r);
barkPos.add(sp.pos().y + oy * r);
barkPos.add(sp.pos().z + oz * r);
// Normal = outward radial direction
float nlen = FastMath.sqrt(ox * ox + oy * oy + oz * oz);
barkNorm.add(ox / nlen);
barkNorm.add(oy / nlen);
barkNorm.add(oz / nlen);
barkUV.add((float) seg / segments * opts.bark.textureScaleX);
barkUV.add(uv_v);
barkWind.add(sp.t());
}
}
// Connect adjacent rings with quads
int numRings = pts.size();
for (int si = 0; si < numRings - 1; si++) {
int r0 = baseVertex + si * vertsPerRing;
int r1 = baseVertex + (si + 1) * vertsPerRing;
for (int seg = 0; seg < segments; seg++) {
int a = r0 + seg, b = r0 + seg + 1;
int c = r1 + seg, d = r1 + seg + 1;
barkIdx.add(a); barkIdx.add(b); barkIdx.add(c);
barkIdx.add(b); barkIdx.add(d); barkIdx.add(c);
}
}
}
// ── Child branch placement ───────────────────────────────────────────────
private void generateChildren(Branch parent, List<BranchSection> pts,
Rng rng, Queue<Branch> queue) {
BranchOptions bo = opts.branch;
int level = parent.level;
int childLevel = level + 1;
int childCount = bo.getChildren(level);
float childStart = bo.getStart(childLevel);
float childLenFactor = bo.getLength(childLevel);
float childRadFactor = bo.getRadius(childLevel);
float childAngle = bo.getAngle(childLevel) * FastMath.DEG_TO_RAD;
float childTwistBase = bo.getTwist(childLevel) * FastMath.DEG_TO_RAD;
int sections = pts.size() - 1;
for (int i = 0; i < childCount; i++) {
// Stratified placement along parent with small jitter
float t = childStart + ((float) i / childCount) * (1f - childStart);
t = FastMath.clamp(t + rng.range(-0.02f, 0.02f), childStart, 1f);
int sectionIdx = Math.min(Math.max(0, (int) (t * sections)), sections);
BranchSection sp = pts.get(sectionIdx);
// Azimuth: evenly distribute around parent axis, with optional twist accumulation
float phi = ((float) i / childCount) * FastMath.TWO_PI
+ childTwistBase * i
+ rng.range(-0.15f, 0.15f);
Vector3f parentDir = sp.dir().normalize();
Vector3f perp = findPerp(parentDir);
// Rotate perp around parentDir by phi to obtain the swing axis
Quaternion phiQ = new Quaternion().fromAngleAxis(phi, parentDir);
Vector3f swingAxis = phiQ.mult(perp).normalizeLocal();
// Rotate parentDir by childAngle around swingAxis
Quaternion thetaQ = new Quaternion().fromAngleAxis(childAngle, swingAxis);
Vector3f childDir = thetaQ.mult(parentDir).normalizeLocal();
Branch child = new Branch();
child.origin = sp.pos().clone();
child.orientation = dirToQuat(childDir);
child.level = childLevel;
child.parent = parent;
child.length = parent.length * childLenFactor;
child.radius = sp.radius() * childRadFactor;
queue.add(child);
}
}
// ── Leaf placement ───────────────────────────────────────────────────────
private void generateLeaves(List<BranchSection> pts, Rng rng) {
LeavesOptions lo = opts.leaves;
int count = lo.count;
int nSections = pts.size() - 1;
for (int i = 0; i < count; i++) {
float t = lo.start + rng.next() * (1f - lo.start);
int si = Math.min(Math.max(0, (int) (t * nSections)), nSections);
BranchSection sp = pts.get(si);
float size = lo.size * (1f + rng.range(-lo.sizeVariance, lo.sizeVariance));
float yaw = rng.range(0f, FastMath.TWO_PI);
// Use preset angle as base pitch; add slight random variance
float pitch = lo.angle * FastMath.DEG_TO_RAD + rng.range(-0.25f, 0.25f);
float wind = sp.t();
switch (lo.billboard) {
case CROSS -> {
addLeafQuad(sp.pos(), yaw, pitch, size, wind);
addLeafQuad(sp.pos(), yaw + FastMath.HALF_PI, pitch, size, wind);
}
case ROTATE_X -> addLeafQuad(sp.pos(), yaw, FastMath.HALF_PI, size, wind);
default -> addLeafQuad(sp.pos(), yaw, pitch, size, wind);
}
}
}
private void addLeafQuad(Vector3f center, float yaw, float pitch, float size, float wind) {
float h = size * 0.5f;
float cy = FastMath.cos(yaw), sy = FastMath.sin(yaw);
float cp = FastMath.cos(pitch), sp = FastMath.sin(pitch);
// Local frame: right = rotate X by yaw, up = rotated Y by yaw+pitch
Vector3f right = new Vector3f(cy, 0f, -sy);
Vector3f up = new Vector3f(sy * sp, cp, cy * sp);
Vector3f norm = right.cross(up).normalizeLocal();
// 4 corners: BL, BR, TR, TL
float[] cornerX = { center.x - right.x*h - up.x*h,
center.x + right.x*h - up.x*h,
center.x + right.x*h + up.x*h,
center.x - right.x*h + up.x*h };
float[] cornerY = { center.y - right.y*h - up.y*h,
center.y + right.y*h - up.y*h,
center.y + right.y*h + up.y*h,
center.y - right.y*h + up.y*h };
float[] cornerZ = { center.z - right.z*h - up.z*h,
center.z + right.z*h - up.z*h,
center.z + right.z*h + up.z*h,
center.z - right.z*h + up.z*h };
int base = leafPos.size() / 3;
for (int i = 0; i < 4; i++) {
leafPos.add(cornerX[i]); leafPos.add(cornerY[i]); leafPos.add(cornerZ[i]);
if (opts.leaves.roundedNormals) {
float len = FastMath.sqrt(cornerX[i]*cornerX[i] + cornerY[i]*cornerY[i] + cornerZ[i]*cornerZ[i]);
if (len > 1e-6f) {
leafNorm.add(cornerX[i]/len); leafNorm.add(cornerY[i]/len); leafNorm.add(cornerZ[i]/len);
} else {
leafNorm.add(0f); leafNorm.add(1f); leafNorm.add(0f);
}
} else {
leafNorm.add(norm.x); leafNorm.add(norm.y); leafNorm.add(norm.z);
}
leafWind.add(wind);
}
leafUV.add(0f); leafUV.add(0f);
leafUV.add(1f); leafUV.add(0f);
leafUV.add(1f); leafUV.add(1f);
leafUV.add(0f); leafUV.add(1f);
leafIdx.add(base); leafIdx.add(base+1); leafIdx.add(base+2);
leafIdx.add(base); leafIdx.add(base+2); leafIdx.add(base+3);
}
// ── Mesh assembly ────────────────────────────────────────────────────────
private static Geometry buildGeometry(String name,
List<Float> pos, List<Float> norm,
List<Float> uv, List<Integer> idx,
List<Float> wind) {
int nVerts = pos.size() / 3;
FloatBuffer pb = BufferUtils.createFloatBuffer(nVerts * 3);
FloatBuffer nb = BufferUtils.createFloatBuffer(nVerts * 3);
FloatBuffer ub = BufferUtils.createFloatBuffer(nVerts * 2);
FloatBuffer cb = BufferUtils.createFloatBuffer(nVerts * 4); // RGBA — R = wind
IntBuffer ib = BufferUtils.createIntBuffer(idx.size());
for (float f : pos) pb.put(f);
for (float f : norm) nb.put(f);
for (float f : uv) ub.put(f);
for (float f : wind) { cb.put(f); cb.put(1f); cb.put(1f); cb.put(1f); }
for (int i : idx) ib.put(i);
pb.flip(); nb.flip(); ub.flip(); cb.flip(); ib.flip();
Mesh mesh = new Mesh();
mesh.setBuffer(Type.Position, 3, pb);
mesh.setBuffer(Type.Normal, 3, nb);
mesh.setBuffer(Type.TexCoord, 2, ub);
mesh.setBuffer(Type.Color, 4, cb);
mesh.setBuffer(Type.Index, 3, ib);
mesh.updateBound();
return new Geometry(name, mesh);
}
// ── Math helpers ─────────────────────────────────────────────────────────
/** Returns a unit vector perpendicular to v. */
private static Vector3f findPerp(Vector3f v) {
Vector3f p = v.cross(Vector3f.UNIT_X);
if (p.lengthSquared() < 0.01f) p = v.cross(Vector3f.UNIT_Z);
return p.normalizeLocal();
}
/** Creates a quaternion that rotates UNIT_Y onto dir. */
private static Quaternion dirToQuat(Vector3f dir) {
Vector3f d = dir.normalize();
Vector3f axis = Vector3f.UNIT_Y.cross(d);
float len = axis.length();
if (len < 1e-6f) {
if (Vector3f.UNIT_Y.dot(d) > 0f) return new Quaternion();
return new Quaternion().fromAngleAxis(FastMath.PI, Vector3f.UNIT_X);
}
axis.divideLocal(len);
float angle = FastMath.acos(FastMath.clamp(Vector3f.UNIT_Y.dot(d), -1f, 1f));
return new Quaternion().fromAngleAxis(angle, axis);
}
}