416 lines
17 KiB
Java
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);
|
|
}
|
|
}
|