|
|
|
|
@@ -33,9 +33,11 @@ const C: Record<string, string> = {
|
|
|
|
|
|
|
|
|
|
// Temperate Forest (Balanced greens)
|
|
|
|
|
leafDeep: '#3E6B42', leafMid: '#4E7A45', leafLight: '#6A9655',
|
|
|
|
|
leafBirch: '#85C265', // New: Brighter, fresh green for Birch
|
|
|
|
|
pineD: '#385E4B', pineL: '#567D65',
|
|
|
|
|
willow: '#7E9E64', poplar: '#68994D',
|
|
|
|
|
woodOak: '#6B503B', woodBirch: '#E8E8E0', spot: '#3B3B3B',
|
|
|
|
|
pineCone: '#C29B61', acorn: '#D4B483', // Bright Tan/Light Brown for visibility
|
|
|
|
|
dirt: '#755E49', grass: '#6FA648', stone: '#888C8D',
|
|
|
|
|
|
|
|
|
|
// Ice/Tundra (Balanced greens)
|
|
|
|
|
@@ -89,19 +91,69 @@ type AddBlockFn = (dx: number, dy: number, dz: number, color: string) => void;
|
|
|
|
|
|
|
|
|
|
class VoxelBuilder {
|
|
|
|
|
private addBlock: AddBlockFn;
|
|
|
|
|
private offsetX: number = 0;
|
|
|
|
|
private offsetY: number = 0;
|
|
|
|
|
private offsetZ: number = 0;
|
|
|
|
|
|
|
|
|
|
// Temporary storage for face culling
|
|
|
|
|
private tempVoxels: Map<string, string> = new Map();
|
|
|
|
|
|
|
|
|
|
constructor(addBlock: AddBlockFn) {
|
|
|
|
|
this.addBlock = addBlock;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setOffset(x: number, y: number, z: number) {
|
|
|
|
|
this.offsetX = x;
|
|
|
|
|
this.offsetY = y;
|
|
|
|
|
this.offsetZ = z;
|
|
|
|
|
this.tempVoxels.clear(); // Clear previous tree data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
add(x: number, y: number, z: number, hexColor: string) {
|
|
|
|
|
const finalX = Math.round(x + this.offsetX);
|
|
|
|
|
const finalY = Math.round(y + this.offsetY);
|
|
|
|
|
const finalZ = Math.round(z + this.offsetZ);
|
|
|
|
|
|
|
|
|
|
let darkFactor = 1.0;
|
|
|
|
|
// Simple AO / shading
|
|
|
|
|
if (y < 1) darkFactor = 0.85;
|
|
|
|
|
if (y < 0.5) darkFactor = 0.7;
|
|
|
|
|
|
|
|
|
|
const finalColor = darkFactor < 1.0 ? darkenHex(hexColor, darkFactor) : hexColor;
|
|
|
|
|
this.addBlock(Math.round(x), Math.round(y), Math.round(z), finalColor);
|
|
|
|
|
|
|
|
|
|
// Store in temp map instead of adding directly
|
|
|
|
|
this.tempVoxels.set(`${finalX},${finalY},${finalZ}`, finalColor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
commit() {
|
|
|
|
|
// Process temp voxels and perform internal face culling
|
|
|
|
|
// Only add voxels that have at least one exposed face
|
|
|
|
|
for (const [key, color] of this.tempVoxels) {
|
|
|
|
|
const [x, y, z] = key.split(',').map(Number);
|
|
|
|
|
|
|
|
|
|
// Check neighbors
|
|
|
|
|
let exposed = false;
|
|
|
|
|
const neighbors = [
|
|
|
|
|
`${x+1},${y},${z}`,
|
|
|
|
|
`${x-1},${y},${z}`,
|
|
|
|
|
`${x},${y+1},${z}`,
|
|
|
|
|
`${x},${y-1},${z}`,
|
|
|
|
|
`${x},${y},${z+1}`,
|
|
|
|
|
`${x},${y},${z-1}`
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const neighborKey of neighbors) {
|
|
|
|
|
if (!this.tempVoxels.has(neighborKey)) {
|
|
|
|
|
exposed = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (exposed) {
|
|
|
|
|
this.addBlock(x, y, z, color);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.tempVoxels.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -235,77 +287,727 @@ const createSaguaro = (b: VoxelBuilder, ox: number, oz: number) => {
|
|
|
|
|
};
|
|
|
|
|
// (Omitting some minor desert variations for brevity, keeping the impressive ones)
|
|
|
|
|
|
|
|
|
|
// --- TEMPERATE FOREST (Qiulin) ---
|
|
|
|
|
// --- TEMPERATE FOREST (Qiulin) - Enhanced for Mountainous Majesty ---
|
|
|
|
|
const createLushOak = (builder: VoxelBuilder, ox: number, oz: number) => {
|
|
|
|
|
drawCylinder(builder, ox, 0, oz, 7, 2, C.woodOak);
|
|
|
|
|
drawSphere(builder, ox, 0, oz, 4, C.dirt, C.grass, 0.4);
|
|
|
|
|
drawCloudCluster(builder, ox, 8, oz, 4.5, 12, 2.5, 4.0, C.leafDeep, C.leafLight);
|
|
|
|
|
};
|
|
|
|
|
const createFullBirch = (builder: VoxelBuilder, ox: number, oz: number) => {
|
|
|
|
|
let h = 14;
|
|
|
|
|
let lean = (Math.random() - 0.5) * 2;
|
|
|
|
|
for (let y = 0; y < h; y++) {
|
|
|
|
|
let dx = Math.round(lean * (y / h));
|
|
|
|
|
builder.add(ox + dx, y, oz, Math.random() > 0.85 ? C.spot : C.woodBirch);
|
|
|
|
|
if (y > 1 && y < 4) {
|
|
|
|
|
builder.add(ox + dx + 1, y, oz, C.woodBirch);
|
|
|
|
|
builder.add(ox + dx, y, oz + 1, C.woodBirch);
|
|
|
|
|
// 1. Height & Scale
|
|
|
|
|
// Height: 24 - 36 (Wide and majestic, shorter than Poplar/Pine but massive)
|
|
|
|
|
const h = 24 + Math.random() * 12;
|
|
|
|
|
const sizeFactor = 0.9 + (h - 24) / 24.0;
|
|
|
|
|
|
|
|
|
|
// 2. Colors (Oak Palette - Classic Deep Green)
|
|
|
|
|
const oakColors = [C.leafDeep, C.leafDeep, C.leafMid, C.leafLight];
|
|
|
|
|
|
|
|
|
|
// 3. Trunk (Thick and sturdy)
|
|
|
|
|
const trunkR = 2.2 * sizeFactor;
|
|
|
|
|
const trunkH = h * 0.45; // Trunk goes up about halfway
|
|
|
|
|
|
|
|
|
|
// Roots (Spreading wide)
|
|
|
|
|
drawCylinder(builder, ox, -4, oz, 4, trunkR * 1.6, C.woodOak);
|
|
|
|
|
|
|
|
|
|
// Main Trunk
|
|
|
|
|
for (let y = 0; y < trunkH; y++) {
|
|
|
|
|
const t = y / trunkH;
|
|
|
|
|
// Slight taper but mostly thick
|
|
|
|
|
const r = trunkR * (1 - t * 0.2);
|
|
|
|
|
const rCeil = Math.ceil(r);
|
|
|
|
|
for(let dx = -rCeil; dx <= rCeil; dx++) {
|
|
|
|
|
for(let dz = -rCeil; dz <= rCeil; dz++) {
|
|
|
|
|
if(dx*dx + dz*dz <= r*r) builder.add(ox+dx, y, oz+dz, C.woodOak);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
drawSphere(builder, ox + Math.round(lean), h - 1, oz, 4.5, C.poplar, C.leafLight, 1.2, 0.2, 0.9);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Large Branches (3-4 branches reaching out)
|
|
|
|
|
const numBranches = 3 + Math.floor(Math.random() * 2);
|
|
|
|
|
const branchStartH = trunkH * 0.6; // Branches start slightly below top of trunk
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < numBranches; i++) {
|
|
|
|
|
const angle = (i / numBranches) * Math.PI * 2 + Math.random() * 0.5;
|
|
|
|
|
const length = (6 + Math.random() * 4) * sizeFactor;
|
|
|
|
|
const branchY = branchStartH + Math.random() * 3;
|
|
|
|
|
|
|
|
|
|
// Draw Branch
|
|
|
|
|
const endX = ox + Math.cos(angle) * length;
|
|
|
|
|
const endZ = oz + Math.sin(angle) * length;
|
|
|
|
|
const endY = branchY + length * 0.5; // Branches go up
|
|
|
|
|
|
|
|
|
|
// Simple line for branch
|
|
|
|
|
const steps = Math.ceil(length * 1.5);
|
|
|
|
|
for (let s = 0; s <= steps; s++) {
|
|
|
|
|
const t = s / steps;
|
|
|
|
|
const bx = Math.round(ox + (endX - ox) * t);
|
|
|
|
|
const by = Math.round(branchY + (endY - branchY) * t);
|
|
|
|
|
const bz = Math.round(oz + (endZ - oz) * t);
|
|
|
|
|
// Thicker near trunk
|
|
|
|
|
if (t < 0.4) builder.add(bx, by, bz, C.woodOak);
|
|
|
|
|
if (t < 0.2) { // Extra thick base
|
|
|
|
|
builder.add(bx+1, by, bz, C.woodOak);
|
|
|
|
|
builder.add(bx-1, by, bz, C.woodOak);
|
|
|
|
|
builder.add(bx, by, bz+1, C.woodOak);
|
|
|
|
|
builder.add(bx, by, bz-1, C.woodOak);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Branch Sub-Crown (Flattened ellipsoids)
|
|
|
|
|
drawNoisyEllipsoid(
|
|
|
|
|
builder,
|
|
|
|
|
Math.round(endX), Math.round(endY), Math.round(endZ),
|
|
|
|
|
6.0 * sizeFactor, 4.5 * sizeFactor, 6.0 * sizeFactor,
|
|
|
|
|
oakColors,
|
|
|
|
|
0, 0.3, true, 0.2,
|
|
|
|
|
{ chance: 0.01, color: C.acorn } // Acorns on branches
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. Main Crown (Top center)
|
|
|
|
|
// Huge covering canopy
|
|
|
|
|
const topY = trunkH + 2;
|
|
|
|
|
drawNoisyEllipsoid(
|
|
|
|
|
builder,
|
|
|
|
|
ox, topY, oz,
|
|
|
|
|
8.0 * sizeFactor, 7.0 * sizeFactor, 8.0 * sizeFactor,
|
|
|
|
|
oakColors,
|
|
|
|
|
0, 0.35, true, 0.15,
|
|
|
|
|
{ chance: 0.015, color: C.acorn } // Acorns on top
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Procedural Shapes (Noise-based)
|
|
|
|
|
const drawNoisyEllipsoid = (
|
|
|
|
|
builder: VoxelBuilder,
|
|
|
|
|
ox: number,
|
|
|
|
|
oy: number,
|
|
|
|
|
oz: number,
|
|
|
|
|
rx: number,
|
|
|
|
|
ry: number,
|
|
|
|
|
rz: number,
|
|
|
|
|
colors: string[], // Changed to array: [Shadow, Base, Highlight, Noise]
|
|
|
|
|
noiseScale: number = 0.2,
|
|
|
|
|
noiseStrength: number = 0.3,
|
|
|
|
|
isHollow: boolean = true,
|
|
|
|
|
weepingFactor: number = 0.0, // New: Weeping effect (0.0 - 1.0)
|
|
|
|
|
fruitSettings?: { chance: number, color: string } // New: Fruit generation
|
|
|
|
|
) => {
|
|
|
|
|
// Bounding box
|
|
|
|
|
const minX = -Math.ceil(rx);
|
|
|
|
|
const maxX = Math.ceil(rx);
|
|
|
|
|
const minY = -Math.ceil(ry * (1.0 + weepingFactor * 0.5)); // Extend downward
|
|
|
|
|
const maxY = Math.ceil(ry);
|
|
|
|
|
const minZ = -Math.ceil(rz);
|
|
|
|
|
const maxZ = Math.ceil(rz);
|
|
|
|
|
|
|
|
|
|
const [cShadow, cBase, cHighlight, cNoise] = colors;
|
|
|
|
|
|
|
|
|
|
for (let x = minX; x <= maxX; x++) {
|
|
|
|
|
for (let y = minY; y <= maxY; y++) {
|
|
|
|
|
for (let z = minZ; z <= maxZ; z++) {
|
|
|
|
|
// Normalized distance from center (0 to 1 at surface)
|
|
|
|
|
const dx = x / rx;
|
|
|
|
|
const dy = y / ry;
|
|
|
|
|
const dz = z / rz;
|
|
|
|
|
|
|
|
|
|
// Basic Ellipsoid distance
|
|
|
|
|
let dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
|
|
|
|
|
|
|
|
|
|
// Weeping Deformation:
|
|
|
|
|
// If y is negative (bottom half), stretch it downwards based on x/z distance
|
|
|
|
|
// The further from center horizontally, the more it droops
|
|
|
|
|
if (y < 0 && weepingFactor > 0) {
|
|
|
|
|
const horizDist = Math.sqrt(dx*dx + dz*dz); // 0..1
|
|
|
|
|
// Warp 'y' coordinate to simulate droop
|
|
|
|
|
// Effectively, we make 'dy' smaller (so dist is smaller) for lower voxels
|
|
|
|
|
const droop = horizDist * weepingFactor * 1.5;
|
|
|
|
|
const warpedDy = (y + Math.abs(y) * droop) / ry;
|
|
|
|
|
dist = Math.sqrt(dx*dx + warpedDy*warpedDy + dz*dz);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dist > 1.2) continue;
|
|
|
|
|
|
|
|
|
|
// Soft fuzzy edge probability
|
|
|
|
|
let probability = 1.0;
|
|
|
|
|
if (dist > 0.8) {
|
|
|
|
|
probability = 1.0 - (dist - 0.8) / 0.4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Math.random() < probability) {
|
|
|
|
|
// Hollow check
|
|
|
|
|
if (isHollow && dist < 0.6) continue;
|
|
|
|
|
|
|
|
|
|
// Fruit Generation (Check first, overwrite leaf)
|
|
|
|
|
if (fruitSettings && Math.random() < fruitSettings.chance) {
|
|
|
|
|
// Fruits usually near edges or bottom
|
|
|
|
|
if (dist > 0.5 || y < 0) {
|
|
|
|
|
// Make Acorns larger (Cluster of 2)
|
|
|
|
|
builder.add(ox + x, oy + y, oz + z, fruitSettings.color);
|
|
|
|
|
// Add a neighbor for volume
|
|
|
|
|
if (Math.random() > 0.5) {
|
|
|
|
|
builder.add(ox + x + (Math.random()>0.5?1:-1), oy + y, oz + z, fruitSettings.color);
|
|
|
|
|
} else {
|
|
|
|
|
builder.add(ox + x, oy + y - 1, oz + z, fruitSettings.color);
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Improved Color Logic ---
|
|
|
|
|
// Goal: Rich 4-color mix everywhere, with slight vertical bias
|
|
|
|
|
const normalizedY = y / ry; // -1 (bottom) to 1 (top)
|
|
|
|
|
|
|
|
|
|
// Base Weights (Global Richness)
|
|
|
|
|
// Ensure Deep Green (Shadow) appears everywhere, even at top
|
|
|
|
|
let pShadow = 0.20;
|
|
|
|
|
let pHighlight = 0.20;
|
|
|
|
|
let pNoise = 0.25;
|
|
|
|
|
// Base takes remaining ~35%
|
|
|
|
|
|
|
|
|
|
// Apply Gradient Bias
|
|
|
|
|
// Top: More highlight, but keep some shadow
|
|
|
|
|
if (normalizedY > 0.3) {
|
|
|
|
|
pHighlight += 0.25 * normalizedY; // Up to 0.45
|
|
|
|
|
pShadow -= 0.05 * normalizedY; // Down to 0.15 (Still present!)
|
|
|
|
|
}
|
|
|
|
|
// Bottom: More shadow
|
|
|
|
|
else if (normalizedY < -0.3) {
|
|
|
|
|
pShadow += 0.3 * Math.abs(normalizedY); // Up to 0.50
|
|
|
|
|
pHighlight -= 0.1 * Math.abs(normalizedY); // Down to 0.10
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const r = Math.random();
|
|
|
|
|
let col = cBase;
|
|
|
|
|
|
|
|
|
|
if (r < pShadow) col = cShadow;
|
|
|
|
|
else if (r < pShadow + pHighlight) col = cHighlight;
|
|
|
|
|
else if (r < pShadow + pHighlight + pNoise) col = cNoise;
|
|
|
|
|
else col = cBase;
|
|
|
|
|
|
|
|
|
|
builder.add(ox + x, oy + y, oz + z, col);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createProceduralBirch = (builder: VoxelBuilder, ox: number, oz: number) => {
|
|
|
|
|
// 1. Height-Width Correlation
|
|
|
|
|
// Adjusted: Nerfed to be the "middle layer" of the forest
|
|
|
|
|
const h = 20 + Math.random() * 10; // Height: 20 - 30
|
|
|
|
|
|
|
|
|
|
// Size Factor: Adjusted baseline for shorter trees
|
|
|
|
|
const sizeFactor = 0.8 + (h - 20) / 20.0;
|
|
|
|
|
|
|
|
|
|
// Define Crown Dimensions (Fix ReferenceError)
|
|
|
|
|
const baseRy = 9.0 + Math.random() * 3.0;
|
|
|
|
|
const crownRy = baseRy * sizeFactor;
|
|
|
|
|
|
|
|
|
|
const leanX = (Math.random() - 0.5) * 8;
|
|
|
|
|
const leanZ = (Math.random() - 0.5) * 8;
|
|
|
|
|
|
|
|
|
|
// Root extension
|
|
|
|
|
for (let y = -5; y < 0; y++) {
|
|
|
|
|
const t = y / h;
|
|
|
|
|
const cx = Math.round(leanX * t * 0.5);
|
|
|
|
|
const cz = Math.round(leanZ * t * 0.5);
|
|
|
|
|
drawCylinder(builder, ox + cx, y, oz + cz, 1, 1.8 * sizeFactor, C.woodBirch);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Main trunk with Horizontal Striping
|
|
|
|
|
for (let y = 0; y < h - 4; y++) {
|
|
|
|
|
const t = y / h;
|
|
|
|
|
const cx = Math.round(leanX * t);
|
|
|
|
|
const cz = Math.round(leanZ * t);
|
|
|
|
|
// Tapering trunk - Thinned out
|
|
|
|
|
let r = (t < 0.3 ? 1.3 : (t < 0.7 ? 1.0 : 0.7)) * sizeFactor;
|
|
|
|
|
if (r < 0.6) r = 0.6;
|
|
|
|
|
|
|
|
|
|
for(let dx = -Math.ceil(r); dx <= Math.ceil(r); dx++) {
|
|
|
|
|
for(let dz = -Math.ceil(r); dz <= Math.ceil(r); dz++) {
|
|
|
|
|
if(dx*dx + dz*dz <= r*r) {
|
|
|
|
|
// Birch bark texture: Horizontal Stripes (Lenticels)
|
|
|
|
|
let c = C.woodBirch;
|
|
|
|
|
// Use y-coordinate and noise to create horizontal bands
|
|
|
|
|
// Modulo with some noise
|
|
|
|
|
const bandNoise = Math.sin(y * 0.8 + dx * 0.5) + Math.cos(y * 0.9 + dz * 0.5);
|
|
|
|
|
// Occasional dark bands
|
|
|
|
|
if (Math.abs(bandNoise) > 1.5 && Math.random() > 0.6) {
|
|
|
|
|
c = C.spot;
|
|
|
|
|
}
|
|
|
|
|
// Base patches
|
|
|
|
|
if (y < 3 && Math.random() > 0.7) c = C.spot;
|
|
|
|
|
|
|
|
|
|
builder.add(ox + cx + dx, y, oz + cz + dz, c);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- MULTI-CLUSTER CROWN SYSTEM ---
|
|
|
|
|
|
|
|
|
|
// Birch Colors: [Base, Highlight, Light, Poplar] -> Brighter mix
|
|
|
|
|
const birchColors = [C.leafMid, C.leafBirch, C.leafLight, C.poplar];
|
|
|
|
|
const topX = Math.round(ox + leanX);
|
|
|
|
|
const topZ = Math.round(oz + leanZ);
|
|
|
|
|
|
|
|
|
|
// 1. Top Main Cluster
|
|
|
|
|
// Lowered the main crown slightly (was h - 2) to feel more grounded
|
|
|
|
|
const crownY = h - crownRy * 0.3;
|
|
|
|
|
|
|
|
|
|
drawNoisyEllipsoid(
|
|
|
|
|
builder,
|
|
|
|
|
topX,
|
|
|
|
|
crownY,
|
|
|
|
|
topZ,
|
|
|
|
|
7.0 * sizeFactor,
|
|
|
|
|
8.0 * sizeFactor,
|
|
|
|
|
7.0 * sizeFactor,
|
|
|
|
|
birchColors,
|
|
|
|
|
0, 0.4, true,
|
|
|
|
|
0.3 // Slight weeping
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 2. Side Clusters (2-3)
|
|
|
|
|
const numSide = 2 + Math.floor(Math.random() * 2); // 2 or 3
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < numSide; i++) {
|
|
|
|
|
const angle = (i / numSide) * Math.PI * 2 + Math.random() * 0.5;
|
|
|
|
|
const dist = (4 + Math.random() * 3) * sizeFactor;
|
|
|
|
|
// Lowered side branches significantly: was 0.6-0.8, now 0.5-0.7
|
|
|
|
|
// This makes the crown start lower and look fuller/less top-heavy
|
|
|
|
|
const sideH = h * (0.5 + Math.random() * 0.2);
|
|
|
|
|
|
|
|
|
|
// Calculate side position
|
|
|
|
|
// Interpolate trunk position at this height
|
|
|
|
|
const t = sideH / h;
|
|
|
|
|
const trunkX = ox + leanX * t;
|
|
|
|
|
const trunkZ = oz + leanZ * t;
|
|
|
|
|
|
|
|
|
|
const cx = Math.round(trunkX + Math.cos(angle) * dist);
|
|
|
|
|
const cz = Math.round(trunkZ + Math.sin(angle) * dist);
|
|
|
|
|
const cy = Math.round(sideH);
|
|
|
|
|
|
|
|
|
|
// Draw Cluster
|
|
|
|
|
drawNoisyEllipsoid(
|
|
|
|
|
builder,
|
|
|
|
|
cx, cy, cz,
|
|
|
|
|
5.5 * sizeFactor,
|
|
|
|
|
6.5 * sizeFactor,
|
|
|
|
|
5.5 * sizeFactor,
|
|
|
|
|
birchColors,
|
|
|
|
|
0, 0.4, true,
|
|
|
|
|
0.4 // More weeping for lower branches
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// New Helper: Draw Noisy Cone
|
|
|
|
|
const drawNoisyCone = (
|
|
|
|
|
builder: VoxelBuilder,
|
|
|
|
|
ox: number,
|
|
|
|
|
yStart: number,
|
|
|
|
|
oz: number,
|
|
|
|
|
height: number,
|
|
|
|
|
bottomR: number,
|
|
|
|
|
colors: string[],
|
|
|
|
|
noiseStrength: number = 0.3,
|
|
|
|
|
fruitSettings?: { chance: number, color: string }
|
|
|
|
|
) => {
|
|
|
|
|
const [cShadow, cBase, cHighlight, cNoise] = colors;
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < height; y++) {
|
|
|
|
|
// Normalized Height (0 at bottom, 1 at top)
|
|
|
|
|
const t = y / height;
|
|
|
|
|
|
|
|
|
|
// Radius tapers linearly from bottomR to 0.5
|
|
|
|
|
let currentR = bottomR * (1 - t * 0.8); // Don't go to 0, keep top slightly thick
|
|
|
|
|
if (currentR < 0.5) currentR = 0.5;
|
|
|
|
|
|
|
|
|
|
// Draw circle at this height
|
|
|
|
|
const rCeil = Math.ceil(currentR + 1);
|
|
|
|
|
for (let x = -rCeil; x <= rCeil; x++) {
|
|
|
|
|
for (let z = -rCeil; z <= rCeil; z++) {
|
|
|
|
|
const distSq = x*x + z*z;
|
|
|
|
|
|
|
|
|
|
// Add noise to the edge radius
|
|
|
|
|
// Noise based on angle and height
|
|
|
|
|
const angle = Math.atan2(z, x);
|
|
|
|
|
const noise = Math.sin(angle * 5 + y * 0.5) * noiseStrength * currentR;
|
|
|
|
|
|
|
|
|
|
if (distSq <= (currentR + noise) ** 2) {
|
|
|
|
|
// Fruit Generation
|
|
|
|
|
if (fruitSettings && Math.random() < fruitSettings.chance) {
|
|
|
|
|
// Pine cones hang near edges or bottom of layer
|
|
|
|
|
if (distSq > (currentR * 0.6)**2 || y < height * 0.3) {
|
|
|
|
|
// Make Pine Cones longer (Hang down 2 blocks)
|
|
|
|
|
builder.add(ox + x, Math.round(yStart + y), oz + z, fruitSettings.color);
|
|
|
|
|
builder.add(ox + x, Math.round(yStart + y) - 1, oz + z, fruitSettings.color);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Color Logic
|
|
|
|
|
let col = cBase;
|
|
|
|
|
// Bottom/Inner = Darker
|
|
|
|
|
if (y < height * 0.2 || distSq < (currentR * 0.5)**2) {
|
|
|
|
|
col = Math.random() > 0.6 ? cShadow : cBase;
|
|
|
|
|
}
|
|
|
|
|
// Top/Outer = Lighter
|
|
|
|
|
else if (y > height * 0.8) {
|
|
|
|
|
col = Math.random() > 0.7 ? cHighlight : cBase;
|
|
|
|
|
}
|
|
|
|
|
// Random noise/snow
|
|
|
|
|
if (Math.random() > 0.95) col = cNoise; // Can act as snow or texture
|
|
|
|
|
|
|
|
|
|
builder.add(ox + x, Math.round(yStart + y), oz + z, col);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createDensePine = (builder: VoxelBuilder, ox: number, oz: number) => {
|
|
|
|
|
drawCylinder(builder, ox, 0, oz, 4, 1.5, C.woodOak);
|
|
|
|
|
let startY = 3;
|
|
|
|
|
let layers = 4;
|
|
|
|
|
for (let i = 0; i < layers; i++) {
|
|
|
|
|
let y = startY + i * 3.5;
|
|
|
|
|
let bottomR = 5.5 - i * 1.2;
|
|
|
|
|
let topR = 3.5 - i * 0.8;
|
|
|
|
|
let height = 4;
|
|
|
|
|
for (let ly = 0; ly < height; ly++) {
|
|
|
|
|
let r = bottomR + (topR - bottomR) * (ly / height);
|
|
|
|
|
for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) {
|
|
|
|
|
for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) {
|
|
|
|
|
if (x * x + z * z <= r * r) {
|
|
|
|
|
if (x * x + z * z < r * r * 0.8 || Math.random() > 0.3)
|
|
|
|
|
builder.add(ox + x, Math.round(y + ly), oz + z, C.pineD);
|
|
|
|
|
// 1. Height & Scale
|
|
|
|
|
// Buffed: Pines are now the giants (36 - 54)
|
|
|
|
|
const h = 36 + Math.random() * 18;
|
|
|
|
|
|
|
|
|
|
// 2. Colors (Deep Coniferous Palette)
|
|
|
|
|
// Shadow: PineD, Base: PineD, Highlight: PineL, Noise: PineL (Snow tips?)
|
|
|
|
|
const pineColors = [C.gDarkest, C.pineD, C.pineL, C.pineL];
|
|
|
|
|
|
|
|
|
|
// 3. Root & Trunk
|
|
|
|
|
// Roots
|
|
|
|
|
drawCylinder(builder, ox, -4, oz, 4, 2.2, C.woodOak);
|
|
|
|
|
|
|
|
|
|
// Main Trunk (Visible at bottom)
|
|
|
|
|
// The trunk goes all the way up but gets thinner
|
|
|
|
|
const trunkH = h - 2;
|
|
|
|
|
|
|
|
|
|
// Draw Trunk - Thicker base for taller trees
|
|
|
|
|
// Reduced thickness: 2.5 -> 1.6 base
|
|
|
|
|
const baseR = 1.6 + (h - 36) / 25.0;
|
|
|
|
|
|
|
|
|
|
for(let y = 0; y < trunkH; y++) {
|
|
|
|
|
const t = y / trunkH;
|
|
|
|
|
const r = baseR * (1 - t * 0.5); // Tapering (gentler taper since base is thinner)
|
|
|
|
|
for(let dx = -Math.ceil(r); dx <= Math.ceil(r); dx++) {
|
|
|
|
|
for(let dz = -Math.ceil(r); dz <= Math.ceil(r); dz++) {
|
|
|
|
|
if(dx*dx + dz*dz <= r*r) builder.add(ox+dx, y, oz+dz, C.woodOak);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Tiered Conical Foliage
|
|
|
|
|
// Number of layers depends on height
|
|
|
|
|
let numLayers = 3;
|
|
|
|
|
if (h > 46) numLayers = 4; // Giants get 4 layers
|
|
|
|
|
|
|
|
|
|
// Foliage covers top 70% of the tree
|
|
|
|
|
const foliageStartH = h * 0.3;
|
|
|
|
|
const foliageTopH = h;
|
|
|
|
|
const totalFoliageH = foliageTopH - foliageStartH;
|
|
|
|
|
|
|
|
|
|
const layerHeight = (totalFoliageH / numLayers) * 1.2; // Overlap layers
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < numLayers; i++) {
|
|
|
|
|
// i=0 is bottom layer
|
|
|
|
|
const t = i / (numLayers - 1 || 1); // 0 to 1
|
|
|
|
|
|
|
|
|
|
// Position
|
|
|
|
|
// Bottom layer starts at foliageStartH
|
|
|
|
|
// Top layer ends near top
|
|
|
|
|
const startY = foliageStartH + i * (totalFoliageH / numLayers * 0.85);
|
|
|
|
|
|
|
|
|
|
// Radius
|
|
|
|
|
// Bottom layer is widest (Buffed radius for giants: 8.0 -> 10.0+)
|
|
|
|
|
const maxR = 8.0 + (h - 36) / 5.0;
|
|
|
|
|
const bottomR = maxR * (1 - t * 0.6);
|
|
|
|
|
|
|
|
|
|
drawNoisyCone(
|
|
|
|
|
builder,
|
|
|
|
|
ox,
|
|
|
|
|
startY,
|
|
|
|
|
oz,
|
|
|
|
|
layerHeight,
|
|
|
|
|
bottomR,
|
|
|
|
|
pineColors,
|
|
|
|
|
0.4, // Noise strength
|
|
|
|
|
{ chance: 0.02, color: C.pineCone } // Pine Cones
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
drawCylinder(builder, ox, startY + layers * 3.5, oz, 3, 1, C.pineD);
|
|
|
|
|
builder.add(ox, startY + layers * 3.5 + 3, oz, C.pineL);
|
|
|
|
|
|
|
|
|
|
// Top Spire
|
|
|
|
|
builder.add(ox, h, oz, C.pineL);
|
|
|
|
|
builder.add(ox, h+1, oz, C.pineL);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper: Draw a twisted limb (Trunk or Branch)
|
|
|
|
|
// Returns the end position for attaching foliage or sub-branches
|
|
|
|
|
const drawTwistedLimb = (
|
|
|
|
|
builder: VoxelBuilder,
|
|
|
|
|
sx: number, sy: number, sz: number,
|
|
|
|
|
length: number,
|
|
|
|
|
startR: number,
|
|
|
|
|
endR: number,
|
|
|
|
|
woodCol: string,
|
|
|
|
|
direction: {x: number, y: number, z: number}, // General direction bias
|
|
|
|
|
waviness: number = 1.0
|
|
|
|
|
): {x: number, y: number, z: number} => {
|
|
|
|
|
let cx = sx;
|
|
|
|
|
let cy = sy;
|
|
|
|
|
let cz = sz;
|
|
|
|
|
let cr = startR;
|
|
|
|
|
const rStep = (startR - endR) / length;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
|
|
|
// Draw slice
|
|
|
|
|
const rCeil = Math.ceil(cr);
|
|
|
|
|
for(let dx = -rCeil; dx <= rCeil; dx++) {
|
|
|
|
|
for(let dy = -rCeil; dy <= rCeil; dy++) {
|
|
|
|
|
for(let dz = -rCeil; dz <= rCeil; dz++) {
|
|
|
|
|
if(dx*dx + dy*dy + dz*dz <= cr*cr) {
|
|
|
|
|
builder.add(Math.round(cx+dx), Math.round(cy+dy), Math.round(cz+dz), woodCol);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Move walker
|
|
|
|
|
// 1. Follow general direction
|
|
|
|
|
cx += direction.x;
|
|
|
|
|
cy += direction.y;
|
|
|
|
|
cz += direction.z;
|
|
|
|
|
|
|
|
|
|
// 2. Add Waviness (Sine/Cos noise based on step i)
|
|
|
|
|
cx += Math.sin(i * 0.3) * waviness;
|
|
|
|
|
cz += Math.cos(i * 0.3) * waviness;
|
|
|
|
|
|
|
|
|
|
// 3. Taper
|
|
|
|
|
cr -= rStep;
|
|
|
|
|
if (cr < 0.5) cr = 0.5;
|
|
|
|
|
}
|
|
|
|
|
return {x: cx, y: cy, z: cz};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createScotsPine = (builder: VoxelBuilder, ox: number, oz: number) => {
|
|
|
|
|
// 1. Height & Scale
|
|
|
|
|
const h = 20 + Math.random() * 10; // Not too tall, but wide
|
|
|
|
|
const sizeFactor = 0.9 + (h - 20) / 20.0;
|
|
|
|
|
|
|
|
|
|
// 2. Colors
|
|
|
|
|
const pineColors = [C.gDarkest, C.pineD, C.pineL, C.pineL];
|
|
|
|
|
const woodColor = Math.random() > 0.5 ? C.woodOak : C.woodD;
|
|
|
|
|
|
|
|
|
|
// 3. Root Base
|
|
|
|
|
drawCylinder(builder, ox, -3, oz, 3, 2.8 * sizeFactor, woodColor);
|
|
|
|
|
|
|
|
|
|
// 4. Main Trunk (S-Shape / Crooked)
|
|
|
|
|
// Split trunk into 2-3 segments to change direction drastically
|
|
|
|
|
let cursor = {x: ox, y: 0, z: oz};
|
|
|
|
|
let currentR = 2.2 * sizeFactor;
|
|
|
|
|
|
|
|
|
|
// Segment 1: Lean one way
|
|
|
|
|
const leanAng = Math.random() * Math.PI * 2;
|
|
|
|
|
cursor = drawTwistedLimb(
|
|
|
|
|
builder, cursor.x, cursor.y, cursor.z,
|
|
|
|
|
h * 0.4, currentR, currentR * 0.8, woodColor,
|
|
|
|
|
{x: Math.cos(leanAng)*0.4, y: 0.9, z: Math.sin(leanAng)*0.4},
|
|
|
|
|
0.5
|
|
|
|
|
);
|
|
|
|
|
currentR *= 0.8;
|
|
|
|
|
|
|
|
|
|
// Segment 2: Curve back or sideways (The "Elbow")
|
|
|
|
|
const elbowAng = leanAng + Math.PI * (0.5 + Math.random()); // Turn 90-270 deg
|
|
|
|
|
const seg2Len = h * 0.4;
|
|
|
|
|
|
|
|
|
|
// --- Generate Lower Branches at Elbow ---
|
|
|
|
|
const numLowBranches = 1 + Math.floor(Math.random() * 2);
|
|
|
|
|
for(let i=0; i<numLowBranches; i++) {
|
|
|
|
|
const bAng = Math.random() * Math.PI * 2;
|
|
|
|
|
const bLen = (6 + Math.random() * 5) * sizeFactor;
|
|
|
|
|
// Horizontal or slightly down
|
|
|
|
|
const bEnd = drawTwistedLimb(
|
|
|
|
|
builder, cursor.x, cursor.y, cursor.z,
|
|
|
|
|
bLen, currentR * 0.8, currentR * 0.4, woodColor,
|
|
|
|
|
{x: Math.cos(bAng), y: 0.1, z: Math.sin(bAng)}, // Flat
|
|
|
|
|
0.2
|
|
|
|
|
);
|
|
|
|
|
// Foliage pads on branches
|
|
|
|
|
createScotsCloud(builder, Math.round(bEnd.x), Math.round(bEnd.y), Math.round(bEnd.z), 5.0 * sizeFactor, pineColors);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Continue Trunk
|
|
|
|
|
cursor = drawTwistedLimb(
|
|
|
|
|
builder, cursor.x, cursor.y, cursor.z,
|
|
|
|
|
seg2Len, currentR, currentR * 0.7, woodColor,
|
|
|
|
|
{x: Math.cos(elbowAng)*0.3, y: 0.9, z: Math.sin(elbowAng)*0.3},
|
|
|
|
|
0.5
|
|
|
|
|
);
|
|
|
|
|
currentR *= 0.7;
|
|
|
|
|
|
|
|
|
|
// Segment 3: Top crown support
|
|
|
|
|
const seg3Len = h * 0.3;
|
|
|
|
|
// --- Generate Upper Branches ---
|
|
|
|
|
const numHighBranches = 2 + Math.floor(Math.random() * 2);
|
|
|
|
|
for(let i=0; i<numHighBranches; i++) {
|
|
|
|
|
const bAng = Math.random() * Math.PI * 2;
|
|
|
|
|
const bLen = (4 + Math.random() * 4) * sizeFactor;
|
|
|
|
|
// Upward reaching
|
|
|
|
|
const bEnd = drawTwistedLimb(
|
|
|
|
|
builder, cursor.x, cursor.y, cursor.z,
|
|
|
|
|
bLen, currentR * 0.8, currentR * 0.4, woodColor,
|
|
|
|
|
{x: Math.cos(bAng)*0.8, y: 0.6, z: Math.sin(bAng)*0.8},
|
|
|
|
|
0.2
|
|
|
|
|
);
|
|
|
|
|
createScotsCloud(builder, Math.round(bEnd.x), Math.round(bEnd.y), Math.round(bEnd.z), 4.5 * sizeFactor, pineColors);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Top Crown
|
|
|
|
|
createScotsCloud(builder, Math.round(cursor.x), Math.round(cursor.y + seg3Len), Math.round(cursor.z), 6.0 * sizeFactor, pineColors);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper for Cloud-like Foliage (Flattened Noisy Ellipsoid)
|
|
|
|
|
const createScotsCloud = (builder: VoxelBuilder, ox: number, oy: number, oz: number, radius: number, colors: string[]) => {
|
|
|
|
|
// Very Flat pads (Plate-like)
|
|
|
|
|
const ry = radius * 0.4;
|
|
|
|
|
const rx = radius;
|
|
|
|
|
const rz = radius;
|
|
|
|
|
|
|
|
|
|
drawNoisyEllipsoid(
|
|
|
|
|
builder,
|
|
|
|
|
ox, oy, oz,
|
|
|
|
|
rx, ry, rz,
|
|
|
|
|
colors,
|
|
|
|
|
0, 0.5, // High noise
|
|
|
|
|
true,
|
|
|
|
|
0.0, // No weeping, holds shape
|
|
|
|
|
{ chance: 0.04, color: C.pineCone }
|
|
|
|
|
);
|
|
|
|
|
// Add a second smaller pad slightly above for volume
|
|
|
|
|
if (Math.random() > 0.5) {
|
|
|
|
|
drawNoisyEllipsoid(
|
|
|
|
|
builder,
|
|
|
|
|
ox, oy + ry, oz,
|
|
|
|
|
rx * 0.7, ry * 0.8, rz * 0.7,
|
|
|
|
|
colors,
|
|
|
|
|
0, 0.5, true, 0.0
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createCurtainWillow = (builder: VoxelBuilder, ox: number, oz: number) => {
|
|
|
|
|
drawCylinder(builder, ox, 0, oz, 5, 1.5, C.woodOak);
|
|
|
|
|
drawSphere(builder, ox, 6, oz, 5, C.willow, C.leafLight, 0.7, 0.1, 0.95);
|
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
|
|
|
let ang = (i / 20) * Math.PI * 2;
|
|
|
|
|
let dist = 3.5 + Math.random();
|
|
|
|
|
// Giant Willow
|
|
|
|
|
// Root extension
|
|
|
|
|
drawCylinder(builder, ox, -5, oz, 5, 4, C.woodOak);
|
|
|
|
|
|
|
|
|
|
drawCylinder(builder, ox, 0, oz, 8, 3.5, C.woodOak); // Thicker trunk
|
|
|
|
|
|
|
|
|
|
// Huge crown
|
|
|
|
|
drawSphere(builder, ox, 10, oz, 10, C.willow, C.leafLight, 0.6, 0.2, 0.9);
|
|
|
|
|
|
|
|
|
|
// Many more vines, longer
|
|
|
|
|
for (let i = 0; i < 40; i++) {
|
|
|
|
|
let ang = (i / 40) * Math.PI * 2 + Math.random()*0.5;
|
|
|
|
|
let dist = 5 + Math.random() * 4;
|
|
|
|
|
let sx = ox + Math.cos(ang) * dist;
|
|
|
|
|
let sz = oz + Math.sin(ang) * dist;
|
|
|
|
|
let len = 4 + Math.random() * 5;
|
|
|
|
|
let len = 8 + Math.random() * 10; // Longer vines
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < len; y++) {
|
|
|
|
|
let px = Math.round(sx + (Math.random() - 0.5) * 0.5);
|
|
|
|
|
let pz = Math.round(sz + (Math.random() - 0.5) * 0.5);
|
|
|
|
|
let py = 6 - y;
|
|
|
|
|
if (py < 0) continue;
|
|
|
|
|
let px = Math.round(sx + (Math.random() - 0.5) * 0.8);
|
|
|
|
|
let pz = Math.round(sz + (Math.random() - 0.5) * 0.8);
|
|
|
|
|
let py = 10 - y;
|
|
|
|
|
if (py < 1) continue;
|
|
|
|
|
builder.add(px, py, pz, C.willow);
|
|
|
|
|
if (Math.random() > 0.6) builder.add(px, py, pz + 1, C.willow);
|
|
|
|
|
if (Math.random() > 0.6) builder.add(px, py, pz + 1, Math.random() > 0.5 ? C.willow : C.leafMid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createFatPoplar = (builder: VoxelBuilder, ox: number, oz: number) => {
|
|
|
|
|
drawCylinder(builder, ox, 0, oz, 3, 1, C.woodOak);
|
|
|
|
|
let h = 16;
|
|
|
|
|
for (let y = 0; y < h; y++) {
|
|
|
|
|
let r = 2.2 * Math.sin((y / h) * Math.PI);
|
|
|
|
|
for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) {
|
|
|
|
|
for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) {
|
|
|
|
|
if (x * x + z * z <= r * r) {
|
|
|
|
|
builder.add(ox + x, y + 2, oz + z, C.poplar);
|
|
|
|
|
// 1. Height: 30 - 46 (Middle-Upper layer, taller than Birch, overlapping lower Pines)
|
|
|
|
|
const h = 30 + Math.random() * 16;
|
|
|
|
|
const sizeFactor = 0.8 + (h - 30) / 20.0;
|
|
|
|
|
|
|
|
|
|
// 2. Colors (Poplar Palette)
|
|
|
|
|
const poplarColors = [C.leafDeep, C.poplar, C.leafLight, C.leafMid];
|
|
|
|
|
const [cShadow, cBase, cHighlight, cNoise] = poplarColors;
|
|
|
|
|
|
|
|
|
|
// 3. Trunk
|
|
|
|
|
// Exposed height: 3-5 blocks (Clear ground clearance)
|
|
|
|
|
const trunkH = 3 + Math.random() * 2;
|
|
|
|
|
const crownH = h - trunkH;
|
|
|
|
|
|
|
|
|
|
// Thickness: 1.5 base, tapering slightly (Sturdy but slender)
|
|
|
|
|
const trunkBaseR = 1.5 * sizeFactor;
|
|
|
|
|
|
|
|
|
|
// Roots
|
|
|
|
|
drawCylinder(builder, ox, -4, oz, 4, trunkBaseR * 1.4, C.woodOak);
|
|
|
|
|
|
|
|
|
|
// Exposed Trunk
|
|
|
|
|
for (let y = 0; y < trunkH + 2; y++) { // +2 extends into crown
|
|
|
|
|
const t = y / (trunkH + 2);
|
|
|
|
|
const r = trunkBaseR * (1 - t * 0.3); // Gentle taper
|
|
|
|
|
const rCeil = Math.ceil(r);
|
|
|
|
|
for(let dx = -rCeil; dx <= rCeil; dx++) {
|
|
|
|
|
for(let dz = -rCeil; dz <= rCeil; dz++) {
|
|
|
|
|
if(dx*dx + dz*dz <= r*r) builder.add(ox+dx, y, oz+dz, C.woodOak);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Spindle Crown (Custom construction for Fusiform shape)
|
|
|
|
|
// Max Radius: ~4.5 - 6.5 (Ratio ~ 1:7 of height, slender)
|
|
|
|
|
const maxR = (h / 7.0) * sizeFactor;
|
|
|
|
|
const startY = trunkH;
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < crownH; y++) {
|
|
|
|
|
const t = y / crownH; // 0 to 1
|
|
|
|
|
|
|
|
|
|
// Spindle Shape Formula:
|
|
|
|
|
// 1. Math.pow(t, 0.85): Shifts the peak slightly downwards (approx 40% height) for stability
|
|
|
|
|
// 2. Math.sin(... * PI): Creates the curve
|
|
|
|
|
// 3. Math.pow(..., 0.9): Slightly sharpens the curve, avoiding the "balloon" look
|
|
|
|
|
let rBase = maxR * Math.pow(Math.sin(Math.pow(t, 0.85) * Math.PI), 0.9);
|
|
|
|
|
|
|
|
|
|
// Taper tip aggressively to form a point
|
|
|
|
|
if (t > 0.9) rBase *= (1 - t) * 10.0;
|
|
|
|
|
if (rBase < 0.1) continue;
|
|
|
|
|
|
|
|
|
|
const rCeil = Math.ceil(rBase + 1.5); // Search radius
|
|
|
|
|
|
|
|
|
|
for (let x = -rCeil; x <= rCeil; x++) {
|
|
|
|
|
for (let z = -rCeil; z <= rCeil; z++) {
|
|
|
|
|
const distSq = x*x + z*z;
|
|
|
|
|
|
|
|
|
|
// Noise Logic
|
|
|
|
|
// Vertical streaks for Poplar characteristic
|
|
|
|
|
const angle = Math.atan2(z, x);
|
|
|
|
|
// High frequency angle noise makes it look like vertical branches
|
|
|
|
|
const noise = Math.sin(y * 0.6) * 0.2 + Math.cos(angle * 5) * 0.3;
|
|
|
|
|
|
|
|
|
|
const effectiveR = rBase * (1 + noise * 0.4);
|
|
|
|
|
|
|
|
|
|
if (distSq <= effectiveR * effectiveR) {
|
|
|
|
|
// Color Logic (Vertical Gradient + Volume)
|
|
|
|
|
const normalizedY = y / crownH; // 0 to 1
|
|
|
|
|
|
|
|
|
|
let pShadow = 0.25;
|
|
|
|
|
let pHighlight = 0.15;
|
|
|
|
|
let pNoise = 0.2;
|
|
|
|
|
|
|
|
|
|
// Vertical Gradient
|
|
|
|
|
if (normalizedY > 0.7) {
|
|
|
|
|
pHighlight += 0.25 * (normalizedY - 0.7); // Top is lighter
|
|
|
|
|
pShadow -= 0.1;
|
|
|
|
|
} else if (normalizedY < 0.2) {
|
|
|
|
|
pShadow += 0.3 * (0.2 - normalizedY); // Bottom undercut is darker
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rnd = Math.random();
|
|
|
|
|
let col = cBase;
|
|
|
|
|
if (rnd < pShadow) col = cShadow;
|
|
|
|
|
else if (rnd < pShadow + pHighlight) col = cHighlight;
|
|
|
|
|
else if (rnd < pShadow + pHighlight + pNoise) col = cNoise;
|
|
|
|
|
|
|
|
|
|
builder.add(ox + x, Math.round(startY + y), oz + z, col);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -540,7 +1242,7 @@ const createBanana = (builder: VoxelBuilder, ox: number, oz: number) => {
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
const DESERT_GENERATORS = [createJointedClassic, createJointedCandelabra, createSaguaro];
|
|
|
|
|
const FOREST_GENERATORS = [createLushOak, createFullBirch, createDensePine, createCurtainWillow, createFatPoplar];
|
|
|
|
|
const FOREST_GENERATORS = [createLushOak, createProceduralBirch, createDensePine, createFatPoplar, createScotsPine];
|
|
|
|
|
const SWAMP_GENERATORS = [createCypress, createSwampWillow, createGiantMushroom];
|
|
|
|
|
const TUNDRA_GENERATORS = [createSpruceTallSnowy, createSpruceMediumClear, createLarch];
|
|
|
|
|
const ICE_GENERATORS = [createCoralTree, createIceSpike];
|
|
|
|
|
@@ -661,15 +1363,15 @@ export const generateVegetation = async (
|
|
|
|
|
if (Math.random() < density) {
|
|
|
|
|
const gen = generators[Math.floor(Math.random() * generators.length)];
|
|
|
|
|
|
|
|
|
|
// Inject offset into builder
|
|
|
|
|
const originalAdd = builder['addBlock'];
|
|
|
|
|
builder['addBlock'] = (dx, dy, dz, color) => {
|
|
|
|
|
addVoxel(microX + dx, surfaceY + dy, microZ + dz, color);
|
|
|
|
|
};
|
|
|
|
|
// 使用内置的 offset 功能,避免修改方法带来的潜在栈溢出风险
|
|
|
|
|
builder.setOffset(microX, surfaceY, microZ);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
gen(builder, 0, 0);
|
|
|
|
|
|
|
|
|
|
builder['addBlock'] = originalAdd;
|
|
|
|
|
builder.commit(); // Commit and cull hidden voxels
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Tree generation failed at', lx, lz, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|