Weiter gearbeitet

This commit is contained in:
2026-06-16 07:03:08 +02:00
parent a80269e681
commit a369647e9c
531 changed files with 8327 additions and 1381 deletions

View File

@@ -0,0 +1,8 @@
ext {
jmeVersion = '3.9.0-stable'
}
dependencies {
implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
}

View File

@@ -0,0 +1,23 @@
package de.blight.eztree;
public final class BarkOptions {
/** Optional asset path for a bark diffuse texture. */
public String textureFile = null;
public boolean flatShading = false;
public float textureScaleX = 1f;
public float textureScaleY = 1f;
/** Base bark color (used when no texture is assigned). */
public float r = 0.45f, g = 0.30f, b = 0.18f;
public BarkOptions copy() {
BarkOptions c = new BarkOptions();
c.textureFile = textureFile;
c.flatShading = flatShading;
c.textureScaleX = textureScaleX;
c.textureScaleY = textureScaleY;
c.r = r; c.g = g; c.b = b;
return c;
}
}

View File

@@ -0,0 +1,5 @@
package de.blight.eztree;
public enum Billboard {
NONE, CROSS, ROTATE_X
}

View File

@@ -0,0 +1,18 @@
package de.blight.eztree;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
public final class Branch {
/** World-space start position. */
public Vector3f origin = new Vector3f();
/** Rotation that maps Vector3f.UNIT_Y to the initial branch direction. */
public Quaternion orientation = new Quaternion();
public float length;
public float radius;
public int level;
public Branch parent;
}

View File

@@ -0,0 +1,81 @@
package de.blight.eztree;
import java.util.HashMap;
import java.util.Map;
/**
* All branch parameters, stored as per-level maps (key = branch level).
* When a level is not present the highest defined level at or below is used.
*/
public final class BranchOptions {
/** Maximum branch depth (0 = trunk only). */
public int levels = 3;
/** Spread angle in degrees from parent direction for children at each level. */
public Map<Integer, Float> angle = new HashMap<>();
/** Number of child branches produced by a branch at each level. */
public Map<Integer, Integer> children = new HashMap<>();
/** Random direction perturbation magnitude (radians) per section step. */
public Map<Integer, Float> gnarliness = new HashMap<>();
/** Gravitational / wind force applied to branch growth direction. */
public ForceOptions force = new ForceOptions();
/** Length multiplier for children (relative to their parent). Level 0 = absolute trunk length. */
public Map<Integer, Float> length = new HashMap<>();
/** Radius multiplier for children (relative to parent section radius). Level 0 = absolute trunk radius. */
public Map<Integer, Float> radius = new HashMap<>();
/** Number of cylinder sections along a branch at each level. */
public Map<Integer, Integer> sections = new HashMap<>();
/** Number of polygon sides per cylinder cross-section at each level. */
public Map<Integer, Integer> segments = new HashMap<>();
/** Fraction along parent at which children of this level begin to appear. */
public Map<Integer, Float> start = new HashMap<>();
/** Tip-radius / base-radius ratio for each level (0=needle, 1=no taper). */
public Map<Integer, Float> taper = new HashMap<>();
/** Degrees per section by which the ring frame is rotated around the branch axis. */
public Map<Integer, Float> twist = new HashMap<>();
// ── Accessors with fallback ──────────────────────────────────────────────
public float getAngle(int lv) { return floatAt(angle, lv, 45f); }
public int getChildren(int lv) { return intAt (children, lv, 0); }
public float getGnarliness(int lv){ return floatAt(gnarliness,lv, 0f); }
public float getLength(int lv) { return floatAt(length, lv, 1f); }
public float getRadius(int lv) { return floatAt(radius, lv, 0.5f);}
public int getSections(int lv) { return intAt (sections, lv, 4); }
public int getSegments(int lv) { return intAt (segments, lv, 6); }
public float getStart(int lv) { return floatAt(start, lv, 0f); }
public float getTaper(int lv) { return floatAt(taper, lv, 0.6f);}
public float getTwist(int lv) { return floatAt(twist, lv, 0f); }
private float floatAt(Map<Integer, Float> m, int lv, float def) {
if (m.containsKey(lv)) return m.get(lv);
int best = -1;
for (int k : m.keySet()) if (k <= lv && k > best) best = k;
return best >= 0 ? m.get(best) : def;
}
private int intAt(Map<Integer, Integer> m, int lv, int def) {
if (m.containsKey(lv)) return m.get(lv);
int best = -1;
for (int k : m.keySet()) if (k <= lv && k > best) best = k;
return best >= 0 ? m.get(best) : def;
}
public BranchOptions copy() {
BranchOptions c = new BranchOptions();
c.levels = levels;
c.angle = new HashMap<>(angle);
c.children = new HashMap<>(children);
c.gnarliness = new HashMap<>(gnarliness);
c.force = force.copy();
c.length = new HashMap<>(length);
c.radius = new HashMap<>(radius);
c.sections = new HashMap<>(sections);
c.segments = new HashMap<>(segments);
c.start = new HashMap<>(start);
c.taper = new HashMap<>(taper);
c.twist = new HashMap<>(twist);
return c;
}
}

View File

@@ -0,0 +1,6 @@
package de.blight.eztree;
import com.jme3.math.Vector3f;
/** Immutable snapshot of one section along a branch. */
record BranchSection(Vector3f pos, float radius, Vector3f dir, float t) {}

View File

@@ -0,0 +1,20 @@
package de.blight.eztree;
import com.jme3.math.Vector3f;
public final class ForceOptions {
public Vector3f direction = new Vector3f(0f, 0.5f, 0f);
public float strength = 0.02f;
public ForceOptions() {}
public ForceOptions(float dx, float dy, float dz, float strength) {
this.direction.set(dx, dy, dz);
this.strength = strength;
}
public ForceOptions copy() {
return new ForceOptions(direction.x, direction.y, direction.z, strength);
}
}

View File

@@ -0,0 +1,36 @@
package de.blight.eztree;
public final class LeavesOptions {
/** Optional asset path for a leaf alpha texture. */
public String textureFile = null;
public Billboard billboard = Billboard.CROSS;
public int count = 10;
/** Fraction along the last-level branch at which leaf placement begins. */
public float start = 0.4f;
public float size = 0.5f;
public float sizeVariance = 0.25f;
public float alphaTest = 0.5f;
/** Pitch angle of leaves relative to the branch axis (degrees). Matches JSON "angle". */
public float angle = 10f;
/** Per-vertex normals point outward from tree centre, giving a rounded canopy look. */
public boolean roundedNormals = true;
/** Base leaf color. */
public float r = 0.13f, g = 0.53f, b = 0.17f;
public LeavesOptions copy() {
LeavesOptions c = new LeavesOptions();
c.textureFile = textureFile;
c.billboard = billboard;
c.count = count;
c.start = start;
c.size = size;
c.sizeVariance = sizeVariance;
c.alphaTest = alphaTest;
c.angle = angle;
c.roundedNormals = roundedNormals;
c.r = r; c.g = g; c.b = b;
return c;
}
}

View File

@@ -0,0 +1,30 @@
package de.blight.eztree;
/** Marsaglia's Multiply-With-Carry RNG — matches the ez-tree JS implementation. */
public final class Rng {
private int mz = 987654321;
private int mw;
public Rng(int seed) {
this.mw = (seed == 0) ? 12345 : seed;
}
/** Returns a value in [0, 1). */
public float next() {
mz = 36969 * (mz & 0xFFFF) + (mz >>> 16);
mw = 18000 * (mw & 0xFFFF) + (mw >>> 16);
long result = (((long) mz << 16) + (mw & 0xFFFFL)) & 0xFFFFFFFFL;
return (float) (result / 4294967296.0);
}
/** Returns a value in [min, max). */
public float range(float min, float max) {
return min + next() * (max - min);
}
/** Returns an integer in [min, max). */
public int integer(int min, int max) {
return min + (int) (next() * (max - min));
}
}

View File

@@ -0,0 +1,415 @@
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);
}
}

View File

@@ -0,0 +1,22 @@
package de.blight.eztree;
public final class TreeOptions {
public int seed = 0;
public TreeType type = TreeType.DECIDUOUS;
public BarkOptions bark = new BarkOptions();
public BranchOptions branch = new BranchOptions();
public LeavesOptions leaves = new LeavesOptions();
public TrellisOptions trellis = new TrellisOptions();
public TreeOptions copy() {
TreeOptions c = new TreeOptions();
c.seed = seed;
c.type = type;
c.bark = bark.copy();
c.branch = branch.copy();
c.leaves = leaves.copy();
c.trellis = trellis.copy();
return c;
}
}

View File

@@ -0,0 +1,539 @@
package de.blight.eztree;
/**
* Presets from https://github.com/dgreenheck/ez-tree/tree/main/src/lib/presets
*
* Scaling applied vs. raw JSON values to match JME3 meter units:
* trunk length[0] ÷ 4
* trunk radius[0] ÷ 4
* leaf size ÷ 5
* Relative factors (length/radius level 1+) are JSON[n]/JSON[n-1] — unchanged.
* Twist: JSON radians × 57.2958 → degrees.
* Pine children capped at 20-22 (JS uses 91-100 for needle density, our generator
* creates full branch geometry per child).
*/
public final class TreePresets {
private TreePresets() {}
private static final String BARK1 = "Textures/internal/bark/Bark001_Color.jpg";
private static final String BARK2 = "Textures/internal/bark/Bark002_Color.jpg";
private static final String BARK3 = "Textures/internal/bark/Bark003_Color.jpg";
private static final String LEAF_OAK = "Textures/internal/leaves/oak.png";
private static final String LEAF_ASH = "Textures/internal/leaves/ash.png";
private static final String LEAF_ASPEN = "Textures/internal/leaves/aspen.png";
private static final String LEAF_PINE = "Textures/internal/leaves/pine.png";
// ════════════════════════════════════════════════════════════════════════
// OAK
// ════════════════════════════════════════════════════════════════════════
public static TreeOptions oakSmall() {
TreeOptions o = new TreeOptions();
o.seed = 30895;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK1;
o.bark.r = 1.000f; o.bark.g = 0.953f; o.bark.b = 0.820f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 10f;
BranchOptions b = o.branch;
b.levels = 3;
b.angle.put(1, 54f); b.angle.put(2, 58f); b.angle.put(3, 32f);
b.children.put(0, 4); b.children.put(1, 2); b.children.put(2, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f;
b.gnarliness.put(0, 0.07f); b.gnarliness.put(1, -0.08f);
b.gnarliness.put(2, 0.11f); b.gnarliness.put(3, 0.09f);
b.length.put(0, 7.02f); b.length.put(1, 0.162f); b.length.put(2, 2.149f); b.length.put(3, 0.732f);
b.radius.put(0, 0.25f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 16); b.sections.put(1, 9); b.sections.put(2, 8);
b.segments.put(0, 7); b.segments.put(1, 5); b.segments.put(2, 3); b.segments.put(3, 3);
b.start.put(1, 0.49f); b.start.put(2, 0.06f); b.start.put(3, 0.12f);
b.taper.put(0, 0.73f); b.taper.put(1, 0.42f); b.taper.put(2, 0.69f); b.taper.put(3, 0.75f);
b.twist.put(0, -13.18f); b.twist.put(1, 24.06f);
o.leaves = leaves(LEAF_OAK, 0.835f, 0.835f, 0.804f, Billboard.CROSS, 14, 0.16f, 0.28f, 0.70f, 0.5f, 42f);
return o;
}
public static TreeOptions oakMedium() {
TreeOptions o = new TreeOptions();
o.seed = 35729;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK1;
o.bark.r = 1.000f; o.bark.g = 0.953f; o.bark.b = 0.820f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 10f;
BranchOptions b = o.branch;
b.levels = 3;
b.angle.put(1, 54f); b.angle.put(2, 58f); b.angle.put(3, 32f);
b.children.put(0, 6); b.children.put(1, 4); b.children.put(2, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.02f;
b.gnarliness.put(0, 0.00f); b.gnarliness.put(1, -0.10f);
b.gnarliness.put(2, -0.15f); b.gnarliness.put(3, 0.09f);
b.length.put(0, 9.31f); b.length.put(1, 0.298f); b.length.put(2, 1.118f); b.length.put(3, 0.578f);
b.radius.put(0, 0.35f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 8); b.sections.put(1, 6); b.sections.put(2, 4);
b.segments.put(0, 7); b.segments.put(1, 5); b.segments.put(2, 3); b.segments.put(3, 3);
b.start.put(1, 0.49f); b.start.put(2, 0.06f); b.start.put(3, 0.12f);
b.taper.put(0, 0.73f); b.taper.put(1, 0.42f); b.taper.put(2, 0.69f); b.taper.put(3, 0.75f);
b.twist.put(0, -13.18f); b.twist.put(1, 24.06f);
o.leaves = leaves(LEAF_OAK, 0.835f, 0.835f, 0.804f, Billboard.CROSS, 18, 0.16f, 0.50f, 0.70f, 0.5f, 42f);
return o;
}
public static TreeOptions oakLarge() {
TreeOptions o = new TreeOptions();
o.seed = 23399;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK1;
o.bark.r = 1.000f; o.bark.g = 0.953f; o.bark.b = 0.820f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 10f;
BranchOptions b = o.branch;
b.levels = 3;
b.angle.put(1, 54f); b.angle.put(2, 43f); b.angle.put(3, 32f);
b.children.put(0, 9); b.children.put(1, 5); b.children.put(2, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.02f;
b.gnarliness.put(0, -0.04f); b.gnarliness.put(1, 0.16f);
b.gnarliness.put(2, -0.06f); b.gnarliness.put(3, 0.09f);
b.length.put(0, 11.93f); b.length.put(1, 0.616f); b.length.put(2, 0.600f); b.length.put(3, 0.406f);
b.radius.put(0, 0.75f); b.radius.put(1, 0.58f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 16); b.sections.put(1, 9); b.sections.put(2, 8); b.sections.put(3, 3);
b.segments.put(0, 12); b.segments.put(1, 5); b.segments.put(2, 3); b.segments.put(3, 3);
b.start.put(1, 0.35f); b.start.put(2, 0.10f); b.start.put(3, 0.00f);
b.taper.put(0, 0.73f); b.taper.put(1, 0.42f); b.taper.put(2, 0.69f); b.taper.put(3, 0.75f);
b.twist.put(0, -13.18f); b.twist.put(1, 24.06f);
o.leaves = leaves(LEAF_OAK, 0.835f, 0.835f, 0.804f, Billboard.CROSS, 10, 0.16f, 0.90f, 0.70f, 0.5f, 36f);
return o;
}
// ════════════════════════════════════════════════════════════════════════
// ASH
// ════════════════════════════════════════════════════════════════════════
public static TreeOptions ashSmall() {
TreeOptions o = new TreeOptions();
o.seed = 26867;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK1;
o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
BranchOptions b = o.branch;
b.levels = 2;
b.angle.put(1, 48f); b.angle.put(2, 75f); b.angle.put(3, 60f);
b.children.put(0, 10); b.children.put(1, 3); b.children.put(2, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f;
b.gnarliness.put(0, 0.11f); b.gnarliness.put(1, 0.09f);
b.gnarliness.put(2, 0.05f); b.gnarliness.put(3, 0.09f);
b.length.put(0, 5.97f); b.length.put(1, 0.754f); b.length.put(2, 0.311f); b.length.put(3, 0.823f);
b.radius.put(0, 0.20f); b.radius.put(1, 0.62f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 10); b.sections.put(3, 10);
b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
b.start.put(1, 0.53f); b.start.put(2, 0.33f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
b.twist.put(0, 17.19f); b.twist.put(1, -4.01f);
o.leaves = leaves(LEAF_ASH, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 30, 0.00f, 0.41f, 0.717f, 0.5f, 55f);
return o;
}
public static TreeOptions ashMedium() {
TreeOptions o = new TreeOptions();
o.seed = 36330;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK1;
o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
BranchOptions b = o.branch;
b.levels = 3;
b.angle.put(1, 48f); b.angle.put(2, 75f); b.angle.put(3, 60f);
b.children.put(0, 7); b.children.put(1, 4); b.children.put(2, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f;
b.gnarliness.put(0, 0.03f); b.gnarliness.put(1, 0.25f);
b.gnarliness.put(2, 0.20f); b.gnarliness.put(3, 0.09f);
b.length.put(0, 10.87f); b.length.put(1, 0.624f); b.length.put(2, 0.350f); b.length.put(3, 0.484f);
b.radius.put(0, 0.50f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 12); b.sections.put(1, 8); b.sections.put(2, 6); b.sections.put(3, 4);
b.segments.put(0, 12); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
b.start.put(1, 0.23f); b.start.put(2, 0.33f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
b.twist.put(0, 5.16f); b.twist.put(1, -4.01f);
o.leaves = leaves(LEAF_ASH, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 16, 0.00f, 0.53f, 0.720f, 0.5f, 55f);
return o;
}
public static TreeOptions ashLarge() {
TreeOptions o = new TreeOptions();
o.seed = 29919;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK1;
o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
BranchOptions b = o.branch;
b.levels = 3;
b.angle.put(1, 39f); b.angle.put(2, 39f); b.angle.put(3, 51f);
b.children.put(0, 10); b.children.put(1, 4); b.children.put(2, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f;
b.gnarliness.put(0, -0.05f); b.gnarliness.put(1, 0.20f);
b.gnarliness.put(2, 0.16f); b.gnarliness.put(3, 0.05f);
b.length.put(0, 11.25f); b.length.put(1, 0.654f); b.length.put(2, 0.520f); b.length.put(3, 0.301f);
b.radius.put(0, 0.76f); b.radius.put(1, 0.58f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 12); b.sections.put(1, 8); b.sections.put(2, 6); b.sections.put(3, 4);
b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
b.start.put(1, 0.32f); b.start.put(2, 0.34f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.62f); b.taper.put(2, 0.76f); b.taper.put(3, 0.00f);
b.twist.put(0, 5.16f); b.twist.put(1, -4.01f);
o.leaves = leaves(LEAF_ASH, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 10, 0.01f, 0.92f, 0.720f, 0.5f, 30f);
return o;
}
// ════════════════════════════════════════════════════════════════════════
// ASPEN
// ════════════════════════════════════════════════════════════════════════
public static TreeOptions aspenSmall() {
TreeOptions o = new TreeOptions();
o.seed = 36330;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK2;
o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
BranchOptions b = o.branch;
b.levels = 2;
b.angle.put(1, 70f); b.angle.put(2, 35f); b.angle.put(3, 7f);
b.children.put(0, 4); b.children.put(1, 3); b.children.put(2, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.011f;
b.gnarliness.put(0, 0.04f); b.gnarliness.put(1, -0.01f);
b.gnarliness.put(2, 0.12f); b.gnarliness.put(3, 0.02f);
b.length.put(0, 6.00f); b.length.put(1, 0.140f); b.length.put(2, 2.292f); b.length.put(3, 0.130f);
b.radius.put(0, 0.093f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); b.sections.put(3, 6);
b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
b.start.put(1, 0.45f); b.start.put(2, 0.33f);
b.taper.put(0, 0.37f); b.taper.put(1, 0.13f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
o.leaves = leaves(LEAF_ASPEN, 1.000f, 0.980f, 0.384f, Billboard.CROSS, 13, 0.20f, 0.50f, 0.70f, 0.5f, 30f);
return o;
}
public static TreeOptions aspenMedium() {
TreeOptions o = new TreeOptions();
o.seed = 18020;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK2;
o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
BranchOptions b = o.branch;
b.levels = 2;
b.angle.put(1, 75f); b.angle.put(2, 32f); b.angle.put(3, 7f);
b.children.put(0, 10); b.children.put(1, 3); b.children.put(2, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.015f;
b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.12f);
b.gnarliness.put(2, 0.12f); b.gnarliness.put(3, 0.02f);
b.length.put(0, 12.50f); b.length.put(1, 0.121f); b.length.put(2, 1.843f); b.length.put(3, 0.089f);
b.radius.put(0, 0.18f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); b.sections.put(3, 6);
b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
b.start.put(1, 0.59f); b.start.put(2, 0.35f);
b.taper.put(0, 0.37f); b.taper.put(1, 0.13f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
o.leaves = leaves(LEAF_ASPEN, 1.000f, 0.980f, 0.384f, Billboard.CROSS, 11, 0.124f, 0.50f, 0.70f, 0.5f, 30f);
return o;
}
public static TreeOptions aspenLarge() {
TreeOptions o = new TreeOptions();
o.seed = 30631;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK2;
o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
BranchOptions b = o.branch;
b.levels = 2;
b.angle.put(1, 47f); b.angle.put(2, 63f); b.angle.put(3, 7f);
b.children.put(0, 10); b.children.put(1, 6); b.children.put(2, 0);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.022f;
b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, -0.03f);
b.gnarliness.put(2, 0.12f); b.gnarliness.put(3, 0.02f);
b.length.put(0, 17.40f); b.length.put(1, 0.267f); b.length.put(2, 0.603f); b.length.put(3, 0.089f);
b.radius.put(0, 0.28f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); b.sections.put(3, 6);
b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
b.start.put(1, 0.62f); b.start.put(2, 0.05f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.13f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
o.leaves = leaves(LEAF_ASPEN, 0.988f, 1.000f, 0.149f, Billboard.CROSS, 20, 0.152f, 0.70f, 0.70f, 0.5f, 36f);
return o;
}
// ════════════════════════════════════════════════════════════════════════
// PINE
// ════════════════════════════════════════════════════════════════════════
public static TreeOptions pineSmall() {
TreeOptions o = new TreeOptions();
o.seed = 11744;
o.type = TreeType.EVERGREEN;
o.bark.textureFile = BARK3;
o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
BranchOptions b = o.branch;
b.levels = 1;
b.angle.put(1, 117f); b.angle.put(2, 60f);
b.children.put(0, 20); b.children.put(1, 7);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f;
b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.08f);
b.length.put(0, 9.89f); b.length.put(1, 0.307f); b.length.put(2, 0.825f);
b.radius.put(0, 0.14f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8);
b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4);
b.start.put(1, 0.16f); b.start.put(2, 0.30f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f);
o.leaves = leaves(LEAF_PINE, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 21, 0.00f, 0.19f, 0.70f, 0.3f, 10f);
return o;
}
public static TreeOptions pineMedium() {
TreeOptions o = new TreeOptions();
o.seed = 13977;
o.type = TreeType.EVERGREEN;
o.bark.textureFile = BARK3;
o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
BranchOptions b = o.branch;
b.levels = 1;
b.angle.put(1, 110f); b.angle.put(2, 16f);
b.children.put(0, 18); b.children.put(1, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = -0.003f;
b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.08f);
b.length.put(0, 12.50f); b.length.put(1, 0.477f); b.length.put(2, 0.590f);
b.radius.put(0, 0.26f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8);
b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4);
b.start.put(1, 0.27f); b.start.put(2, 0.14f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f);
o.leaves = leaves(LEAF_PINE, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 30, 0.09f, 0.29f, 0.201f, 0.3f, 39f);
return o;
}
public static TreeOptions pineLarge() {
TreeOptions o = new TreeOptions();
o.seed = 44166;
o.type = TreeType.EVERGREEN;
o.bark.textureFile = BARK3;
o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
BranchOptions b = o.branch;
b.levels = 1;
b.angle.put(1, 129f); b.angle.put(2, 16f);
b.children.put(0, 22); b.children.put(1, 3);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.009f;
b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.08f);
b.length.put(0, 16.31f); b.length.put(1, 0.534f); b.length.put(2, 0.782f);
b.radius.put(0, 0.32f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8);
b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4);
b.start.put(1, 0.294f); b.start.put(2, 0.14f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f);
o.leaves = leaves(LEAF_PINE, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 18, 0.076f, 0.52f, 0.201f, 0.3f, 17f);
return o;
}
// ════════════════════════════════════════════════════════════════════════
// BUSH
// ════════════════════════════════════════════════════════════════════════
public static TreeOptions bush1() {
TreeOptions o = new TreeOptions();
o.seed = 45590;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK1;
o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
BranchOptions b = o.branch;
b.levels = 3;
b.angle.put(1, 21.52f); b.angle.put(2, 62.61f); b.angle.put(3, 60f);
b.children.put(0, 7); b.children.put(1, 3); b.children.put(2, 2);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f;
b.gnarliness.put(0, 0.11f); b.gnarliness.put(1, 0.09f);
b.gnarliness.put(2, 0.05f); b.gnarliness.put(3, 0.09f);
// Near-zero trunk: 0.25f stump, factor adjusted so actual branch1 ≈ 3.8 units
b.length.put(0, 0.25f); b.length.put(1, 15.30f); b.length.put(2, 0.365f); b.length.put(3, 0.823f);
b.radius.put(0, 0.14f); b.radius.put(1, 0.65f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 4); b.sections.put(1, 6); b.sections.put(2, 10); b.sections.put(3, 10);
b.segments.put(0, 4); b.segments.put(1, 4); b.segments.put(2, 4); b.segments.put(3, 3);
b.start.put(1, 0.53f); b.start.put(2, 0.33f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
b.twist.put(0, 17.19f); b.twist.put(1, -4.01f);
o.leaves = leaves(LEAF_ASH, 0.878f, 1.000f, 0.835f, Billboard.CROSS, 12, 0.00f, 0.49f, 0.717f, 0.5f, 55f);
return o;
}
public static TreeOptions bush2() {
TreeOptions o = new TreeOptions();
o.seed = 45590;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK1;
o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
BranchOptions b = o.branch;
b.levels = 2;
b.angle.put(1, 19.57f); b.angle.put(2, 27.39f); b.angle.put(3, 60f);
b.children.put(0, 10); b.children.put(1, 3); b.children.put(2, 2);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f;
b.gnarliness.put(0, 0.022f); b.gnarliness.put(1, 0.109f);
b.gnarliness.put(2, 0.050f); b.gnarliness.put(3, 0.090f);
// Actual branch1 ≈ 4.9 units (19.646/4 ÷ 0.25)
b.length.put(0, 0.25f); b.length.put(1, 19.65f); b.length.put(2, 0.392f); b.length.put(3, 0.597f);
b.radius.put(0, 0.14f); b.radius.put(1, 0.65f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 3); b.sections.put(1, 4); b.sections.put(2, 10); b.sections.put(3, 10);
b.segments.put(0, 4); b.segments.put(1, 4); b.segments.put(2, 4); b.segments.put(3, 3);
b.start.put(1, 0.641f); b.start.put(2, 0.707f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
b.twist.put(0, 20.55f); b.twist.put(1, -2.49f);
o.leaves = leaves(LEAF_ASPEN, 0.878f, 1.000f, 0.835f, Billboard.CROSS, 7, 0.00f, 0.49f, 0.717f, 0.5f, 55f);
return o;
}
public static TreeOptions bush3() {
TreeOptions o = new TreeOptions();
o.seed = 31343;
o.type = TreeType.EVERGREEN;
o.bark.textureFile = BARK1;
o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
BranchOptions b = o.branch;
b.levels = 3;
b.angle.put(1, 66.52f); b.angle.put(2, 52.83f); b.angle.put(3, 0f);
b.children.put(0, 13); b.children.put(1, 4); b.children.put(2, 4);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f;
b.gnarliness.put(0, 0.054f); b.gnarliness.put(1, 0.065f);
b.gnarliness.put(2, 0.050f); b.gnarliness.put(3, 0.090f);
b.length.put(0, 2.74f); b.length.put(1, 1.991f); b.length.put(2, 0.602f); b.length.put(3, 0.421f);
b.radius.put(0, 0.14f); b.radius.put(1, 0.65f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 4); b.sections.put(1, 3); b.sections.put(2, 3); b.sections.put(3, 10);
b.segments.put(0, 3); b.segments.put(1, 3); b.segments.put(2, 3); b.segments.put(3, 3);
b.start.put(1, 0.141f); b.start.put(2, 0.294f);
b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
b.twist.put(0, 17.19f); b.twist.put(1, -1.87f);
o.leaves = leaves(LEAF_PINE, 0.616f, 0.765f, 1.000f, Billboard.CROSS, 3, 0.152f, 0.61f, 0.457f, 0.5f, 54f);
return o;
}
// ════════════════════════════════════════════════════════════════════════
// TRELLIS
// ════════════════════════════════════════════════════════════════════════
public static TreeOptions trellis() {
TreeOptions o = new TreeOptions();
o.seed = 41563;
o.type = TreeType.DECIDUOUS;
o.bark.textureFile = BARK1;
o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
o.bark.textureScaleX = 1f; o.bark.textureScaleY = 8f;
BranchOptions b = o.branch;
b.levels = 3;
b.angle.put(1, 26f); b.angle.put(2, 79f); b.angle.put(3, 0f);
b.children.put(0, 7); b.children.put(1, 5); b.children.put(2, 1);
b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.026f;
b.gnarliness.put(0, 0.00f); b.gnarliness.put(1, 0.02f);
b.gnarliness.put(2, -0.41f); b.gnarliness.put(3, 0.09f);
b.length.put(0, 1.20f); b.length.put(1, 3.521f); b.length.put(2, 0.669f); b.length.put(3, 0.982f);
b.radius.put(0, 0.068f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
b.sections.put(0, 6); b.sections.put(1, 12); b.sections.put(2, 10); b.sections.put(3, 4);
b.segments.put(0, 3); b.segments.put(1, 3); b.segments.put(2, 3); b.segments.put(3, 3);
b.start.put(1, 0.19f); b.start.put(2, 0.10f); b.start.put(3, 0.06f);
b.taper.put(0, 0.60f); b.taper.put(1, 0.50f); b.taper.put(2, 0.50f); b.taper.put(3, 0.50f);
b.twist.put(0, -1.15f); b.twist.put(1, -0.57f); b.twist.put(2, 5.16f);
// tint 15204310 = 0xE7D8C6: (231,216,198)/255
o.leaves = leaves(LEAF_ASH, 0.906f, 0.847f, 0.776f, Billboard.NONE, 13, 0.00f, 0.34f, 0.50f, 0.5f, 30f);
o.trellis.enabled = true;
o.trellis.sections = 8;
o.trellis.radius = 0.5f;
o.trellis.length = 4f;
o.trellis.memberRadius = 0.08f;
o.trellis.crossMembers = 4;
return o;
}
// ════════════════════════════════════════════════════════════════════════
// Lookup
// ════════════════════════════════════════════════════════════════════════
public static TreeOptions byName(String name) {
return switch (name.toLowerCase()) {
case "oak small" -> oakSmall();
case "oak medium" -> oakMedium();
case "oak large" -> oakLarge();
case "ash small" -> ashSmall();
case "ash medium" -> ashMedium();
case "ash large" -> ashLarge();
case "aspen small" -> aspenSmall();
case "aspen medium" -> aspenMedium();
case "aspen large" -> aspenLarge();
case "pine small" -> pineSmall();
case "pine medium" -> pineMedium();
case "pine large" -> pineLarge();
case "bush 1" -> bush1();
case "bush 2" -> bush2();
case "bush 3" -> bush3();
case "trellis" -> trellis();
default -> oakMedium();
};
}
public static String[] presetNames() {
return new String[]{
"Oak Small", "Oak Medium", "Oak Large",
"Ash Small", "Ash Medium", "Ash Large",
"Aspen Small", "Aspen Medium", "Aspen Large",
"Pine Small", "Pine Medium", "Pine Large",
"Bush 1", "Bush 2", "Bush 3",
"Trellis"
};
}
private static LeavesOptions leaves(String tex, float r, float g, float b,
Billboard bb, int count,
float start, float size, float sizeVar, float alpha,
float angle) {
LeavesOptions l = new LeavesOptions();
l.textureFile = tex;
l.r = r; l.g = g; l.b = b;
l.billboard = bb;
l.count = count;
l.start = start;
l.size = size;
l.sizeVariance = sizeVar;
l.alphaTest = alpha;
l.angle = angle;
return l;
}
}

View File

@@ -0,0 +1,5 @@
package de.blight.eztree;
public enum TreeType {
DECIDUOUS, EVERGREEN
}

View File

@@ -0,0 +1,141 @@
package de.blight.eztree;
import com.jme3.math.FastMath;
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.ArrayList;
import java.util.List;
/**
* Builds a cylindrical lattice (trellis) for climbing plants.
*
* The trellis consists of {@code sections} vertical poles arranged in a circle
* of {@code radius}, connected by {@code crossMembers} horizontal rings.
* All members are thin cylinders with {@code memberRadius}.
*/
public final class Trellis {
private Trellis() {}
public static Node build(TrellisOptions o) {
Node node = new Node("trellis");
int poles = Math.max(3, o.sections);
int crossMembers = Math.max(1, o.crossMembers);
float totalHeight = o.length;
float poleRadius = o.radius;
float memberR = o.memberRadius;
int cylSegs = 5;
List<Float> pos = new ArrayList<>();
List<Float> norm = new ArrayList<>();
List<Float> uv = new ArrayList<>();
List<Integer> idx = new ArrayList<>();
// Vertical poles
for (int p = 0; p < poles; p++) {
float phi = (float) p / poles * FastMath.TWO_PI;
float px = FastMath.cos(phi) * poleRadius;
float pz = FastMath.sin(phi) * poleRadius;
addCylinder(pos, norm, uv, idx,
new Vector3f(px, 0f, pz),
new Vector3f(px, totalHeight, pz),
memberR, cylSegs);
}
// Horizontal cross-member rings
for (int cm = 0; cm < crossMembers; cm++) {
float y = totalHeight * (cm + 1f) / (crossMembers + 1f);
for (int p = 0; p < poles; p++) {
float phi0 = (float) p / poles * FastMath.TWO_PI;
float phi1 = (float) (p + 1) / poles * FastMath.TWO_PI;
Vector3f from = new Vector3f(
FastMath.cos(phi0) * poleRadius, y, FastMath.sin(phi0) * poleRadius);
Vector3f to = new Vector3f(
FastMath.cos(phi1) * poleRadius, y, FastMath.sin(phi1) * poleRadius);
addCylinder(pos, norm, uv, idx, from, to, memberR, cylSegs);
}
}
Geometry geom = toGeometry("trellis-mesh", pos, norm, uv, idx);
node.attachChild(geom);
return node;
}
// ── Cylinder helper ──────────────────────────────────────────────────────
private static void addCylinder(List<Float> pos, List<Float> norm, List<Float> uv,
List<Integer> idx,
Vector3f from, Vector3f to, float radius, int segs) {
Vector3f dir = to.subtract(from).normalizeLocal();
Vector3f right = findPerp(dir);
Vector3f up = dir.cross(right).normalizeLocal();
int base = pos.size() / 3;
for (int ring = 0; ring <= 1; ring++) {
Vector3f center = (ring == 0) ? from : to;
float v = ring;
for (int s = 0; s <= segs; s++) {
float a = (float) s / segs * FastMath.TWO_PI;
float ca = FastMath.cos(a), sa = FastMath.sin(a);
float ox = right.x * ca + up.x * sa;
float oy = right.y * ca + up.y * sa;
float oz = right.z * ca + up.z * sa;
pos.add(center.x + ox * radius);
pos.add(center.y + oy * radius);
pos.add(center.z + oz * radius);
float nl = FastMath.sqrt(ox*ox + oy*oy + oz*oz);
norm.add(ox / nl); norm.add(oy / nl); norm.add(oz / nl);
uv.add((float) s / segs); uv.add(v);
}
}
int ringSz = segs + 1;
for (int s = 0; s < segs; s++) {
int a = base + s, b = base + s + 1;
int c = base + ringSz + s, d = base + ringSz + s + 1;
idx.add(a); idx.add(b); idx.add(c);
idx.add(b); idx.add(d); idx.add(c);
}
}
private static Geometry toGeometry(String name,
List<Float> pos, List<Float> norm,
List<Float> uv, List<Integer> idx) {
int nv = pos.size() / 3;
FloatBuffer pb = BufferUtils.createFloatBuffer(nv * 3);
FloatBuffer nb = BufferUtils.createFloatBuffer(nv * 3);
FloatBuffer ub = BufferUtils.createFloatBuffer(nv * 2);
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 (int i : idx) ib.put(i);
pb.flip(); nb.flip(); ub.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.Index, 3, ib);
mesh.updateBound();
return new Geometry(name, mesh);
}
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();
}
}

View File

@@ -0,0 +1,24 @@
package de.blight.eztree;
public final class TrellisOptions {
public boolean enabled = false;
public int sections = 4;
public float radius = 0.5f;
public float length = 2f;
/** Radius of the cylindrical trellis members. */
public float memberRadius = 0.04f;
/** Number of cross-members per section. */
public int crossMembers = 3;
public TrellisOptions copy() {
TrellisOptions c = new TrellisOptions();
c.enabled = enabled;
c.sections = sections;
c.radius = radius;
c.length = length;
c.memberRadius = memberRadius;
c.crossMembers = crossMembers;
return c;
}
}