@call-me-sensei/toonlab 0.4.19 → 0.4.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM-LIBRARY.md +230 -0
- package/README.md +78 -36
- package/agents/PROMPTS.md +91 -0
- package/agents/README.md +9 -14
- package/agents/claude/CLAUDE.md +8 -0
- package/agents/codex/AGENTS.md +161 -0
- package/agents/cursor/toonlab.mdc +37 -0
- package/agents/references/mcp-asset-discovery.md +12 -9
- package/agents/references/runtime-entry-points.md +5 -4
- package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
- package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
- package/cli/toonlab.mjs +3 -2
- package/database/fal-patina.mjs +131 -0
- package/database/generation-service.mjs +108 -7
- package/database/migrations/0007_creation_revisions.sql +80 -0
- package/database/migrations/0008_creation_revision_hardening.sql +98 -0
- package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
- package/database/migrations/0010_external_catalog_assets.sql +28 -0
- package/database/providers.mjs +195 -16
- package/database/repository.mjs +518 -70
- package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
- package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
- package/mcp/public-catalog.mjs +4 -52
- package/mcp/server.mjs +175 -12
- package/mcp/vite-plugin.mjs +78 -2
- package/package.json +32 -12
- package/scripts/generate-catalog-seed.mjs +34 -7
- package/src/asset-policy/catalogLicenses.js +64 -6
- package/src/assetlib/loadImported.js +15 -1
- package/src/catalog/officialCatalogAssetRuntime.js +13 -2
- package/src/catalog/officialCatalogLod.js +34 -1
- package/src/catalog/officialCatalogPlacement.js +29 -14
- package/src/character/animationRetarget.js +2 -0
- package/src/character/characterRig.js +6 -1
- package/src/character/characterRuntime.js +175 -7
- package/src/environment/environmentMaterialAdapter.js +30 -0
- package/src/environment/environmentPresets.js +90 -0
- package/src/environment/environmentSunShadowPass.js +42 -0
- package/src/environment/toonLabSurfaceLighting.js +108 -10
- package/src/environment/urbanPropMaterial.js +104 -2
- package/src/lighting/lightingSystem.js +65 -10
- package/src/renderer/index.js +7 -0
- package/src/renderer/styleComparison.js +985 -0
- package/src/rock-shader/index.js +5 -0
- package/src/rock-shader/rockGeometryDetail.js +701 -0
- package/src/rock-shader/rockMaterial.js +782 -39
- package/src/rock-shader/rockRegionRuntime.js +185 -0
- package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
- package/src/rock-shader/rockShaderRuntime.js +354 -67
- package/src/rock-shader/rockShaderSettings.js +246 -16
- package/src/rock-shader/rockTangentIntegrity.js +136 -0
- package/src/rock-shader/rockTextureIntegrity.js +115 -0
- package/src/rockgen/index.js +1 -0
- package/src/rockgen/rockDocument.js +27 -3
- package/src/rockgen/surface/c7GeologySurface.js +372 -0
- package/src/shaders-tsl/water.js +8 -0
- package/src/sky/cloudShadow.js +25 -1
- package/src/sky/skySystem.js +9 -0
- package/src/styles/index.js +23 -0
- package/src/styles/neutralStylePresets.js +712 -0
- package/src/styles/sceneStyleRuntime.js +51 -4
- package/src/styles/styleAdapters.js +13 -2
- package/src/styles/styleBundle.js +1 -1
- package/src/texgen/evaluateTexture.js +30 -3
- package/src/texgen/textureGenerators.js +63 -31
- package/src/texgen/textureSettings.js +25 -8
- package/src/toon/toonSettings.js +96 -2
- package/src/vegetation/branchTree.js +327 -42
- package/src/vegetation/index.js +24 -2
- package/src/vegetation/scatter.js +372 -1
- package/src/vegetation/stylizedTree.js +312 -126
- package/src/vegetation/stylizedTreeFoliage.js +229 -7
- package/src/vegetation/treeSurfaceTextures.js +328 -0
- package/src/version.js +1 -1
- package/src/water/waterSettings.js +15 -7
- package/types/asset-policy/catalogLicenses.d.ts +2 -0
- package/types/asset-policy/index.d.ts +2 -0
- package/types/assetlib/loadImported.d.ts +3 -1
- package/types/catalog/officialCatalogLod.d.ts +63 -5
- package/types/character/characterRig.d.ts +59 -0
- package/types/character/characterRuntime.d.ts +1 -0
- package/types/environment/environmentMaterialAdapter.d.ts +3 -1
- package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
- package/types/index.d.ts +89 -7
- package/types/lighting/lightingSystem.d.ts +7 -0
- package/types/react/index.d.ts +20 -0
- package/types/renderer/index.d.ts +1 -0
- package/types/renderer/styleComparison.d.ts +409 -0
- package/types/rock-shader/index.d.ts +5 -0
- package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
- package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
- package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
- package/types/rock-shader/rockShaderSettings.d.ts +19 -5
- package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
- package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/rockDocument.d.ts +29 -1
- package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
- package/types/sky/cloudShadow.d.ts +1 -0
- package/types/styles/index.d.ts +1 -0
- package/types/styles/neutralStylePresets.d.ts +944 -0
- package/types/styles/sceneStyleRuntime.d.ts +20 -0
- package/types/vegetation/branchTree.d.ts +4 -0
- package/types/vegetation/index.d.ts +9 -7
- package/types/vegetation/scatter.d.ts +1 -0
- package/types/vegetation/stylizedTree.d.ts +1 -2
- package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
- package/types/version.d.ts +1 -1
- package/agents/references/geology-playbook.md +0 -118
- package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
- package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
- package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/src/vegetation/legacyTreePresets.js +0 -120
- package/types/vegetation/legacyTreePresets.d.ts +0 -22
|
@@ -98,6 +98,187 @@ function seededRandom(seed) {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Canopy pruning — decide which branch tips carry foliage and which are bared.
|
|
103
|
+
*
|
|
104
|
+
* A recursive skeleton puts a tuft on every terminal twig, and terminal twigs
|
|
105
|
+
* fill the crown volume evenly. The result is always a dome, however the
|
|
106
|
+
* individual tufts are shaped: a hundred small masses packed shoulder to
|
|
107
|
+
* shoulder is a dome, and forty larger ones is a bigger dome. That is correct
|
|
108
|
+
* for a broadleaf and it is wrong for anything whose identity is DISCRETE
|
|
109
|
+
* foliage masses with sky and bare limb between them.
|
|
110
|
+
*
|
|
111
|
+
* Three independent policies, applied in this order. Each is off by default,
|
|
112
|
+
* so a caller that passes nothing — or a bare `padCount`, the original
|
|
113
|
+
* signature — gets exactly the geometry it always did.
|
|
114
|
+
*
|
|
115
|
+
* crownBase Bare every tip below this fraction of the crown's own
|
|
116
|
+
* vertical extent. A skeleton that branches low enough to read
|
|
117
|
+
* multi-stemmed also hangs foliage all the way down that low
|
|
118
|
+
* wood and buries the trunk it just built; on a garden tree the
|
|
119
|
+
* trunk is half the subject. This lifts the skirt WITHOUT
|
|
120
|
+
* raising `branches.start`, so the limb structure keeps its low
|
|
121
|
+
* fork and only the foliage moves.
|
|
122
|
+
*
|
|
123
|
+
* layers Horizontal PLATES: keep tips inside `count` bands across the
|
|
124
|
+
* crown height and bare the tips between them. Pad pruning
|
|
125
|
+
* (below) spaces masses isotropically, which is right for a
|
|
126
|
+
* cloud-pruned pine — a pine's pads are a 3D scatter. It is
|
|
127
|
+
* wrong for the layered broadleaves, whose signature is flat
|
|
128
|
+
* tiers with sky and limb showing BETWEEN them: an Acer
|
|
129
|
+
* palmatum, a dogwood, a Japanese hornbeam. Isotropic spacing
|
|
130
|
+
* can never produce that structure at any count, because the
|
|
131
|
+
* structure is anisotropic. Bands are placed edge to edge
|
|
132
|
+
* across the crown (the lowest sits at the crown base, the
|
|
133
|
+
* highest at the apex) so pruning never decapitates the tree.
|
|
134
|
+
*
|
|
135
|
+
* padCount Keep this many well-separated survivors by farthest-point
|
|
136
|
+
* sampling, seeded from the topmost tip, so kept pads spread
|
|
137
|
+
* over the whole crown instead of clumping. This is what makes
|
|
138
|
+
* a cloud-pruned pine possible at all.
|
|
139
|
+
*
|
|
140
|
+
* Every policy is a pure function of the skeleton: same tree, same pruning,
|
|
141
|
+
* every build. Bared attachments use the existing `densityScale: 0` override,
|
|
142
|
+
* which is already the documented way to strip a branch, so nothing new is
|
|
143
|
+
* needed downstream. `densityBoost` multiplies the survivors' card counts, so
|
|
144
|
+
* a crown can be restructured into plates at an unchanged card total instead
|
|
145
|
+
* of simply losing the foliage the pruning removed.
|
|
146
|
+
*
|
|
147
|
+
* A caller's own override for an attachment always wins: pruning is a layout
|
|
148
|
+
* policy, not an authority over explicit per-branch authoring.
|
|
149
|
+
*/
|
|
150
|
+
export function resolvePadPruning(attachments, options = null, existingOverrides = null) {
|
|
151
|
+
const total = Array.isArray(attachments) ? attachments.length : 0;
|
|
152
|
+
if (!total) return existingOverrides;
|
|
153
|
+
|
|
154
|
+
// The original signature took the pad count positionally.
|
|
155
|
+
const config = (options && typeof options === 'object') ? options : { padCount: options };
|
|
156
|
+
const padCount = Number.isFinite(config.padCount) ? Math.round(config.padCount) : 0;
|
|
157
|
+
const crownBase = Number.isFinite(config.crownBase)
|
|
158
|
+
? THREE.MathUtils.clamp(config.crownBase, 0, 0.95)
|
|
159
|
+
: 0;
|
|
160
|
+
const layers = config.layers && typeof config.layers === 'object' ? config.layers : null;
|
|
161
|
+
const layerCount = layers && Number.isFinite(layers.count) ? Math.round(layers.count) : 0;
|
|
162
|
+
const layerThickness = layers && Number.isFinite(layers.thickness)
|
|
163
|
+
? THREE.MathUtils.clamp(layers.thickness, 0.02, 1)
|
|
164
|
+
: 0.5;
|
|
165
|
+
const layerJitter = layers && Number.isFinite(layers.jitter)
|
|
166
|
+
? THREE.MathUtils.clamp(layers.jitter, 0, 1)
|
|
167
|
+
: 0;
|
|
168
|
+
const densityBoost = Number.isFinite(config.densityBoost)
|
|
169
|
+
? THREE.MathUtils.clamp(config.densityBoost, 0.1, 12)
|
|
170
|
+
: 1;
|
|
171
|
+
const hasHeightPolicy = crownBase > 0 || layerCount >= 2;
|
|
172
|
+
const hasPadPolicy = padCount > 0 && padCount < total;
|
|
173
|
+
if (!hasHeightPolicy && !hasPadPolicy && densityBoost === 1) return existingOverrides;
|
|
174
|
+
|
|
175
|
+
let alive = new Array(total);
|
|
176
|
+
for (let i = 0; i < total; i += 1) alive[i] = true;
|
|
177
|
+
|
|
178
|
+
if (hasHeightPolicy) {
|
|
179
|
+
let minY = Infinity;
|
|
180
|
+
let maxY = -Infinity;
|
|
181
|
+
for (let i = 0; i < total; i += 1) {
|
|
182
|
+
const { y } = attachments[i].position;
|
|
183
|
+
if (y < minY) minY = y;
|
|
184
|
+
if (y > maxY) maxY = y;
|
|
185
|
+
}
|
|
186
|
+
const span = Math.max(maxY - minY, 1e-4);
|
|
187
|
+
// Crown height is measured on the attachments themselves, not on the
|
|
188
|
+
// trunk anchor: it is the foliage envelope that is being restructured,
|
|
189
|
+
// and a leaning or gnarled trunk makes anchor-relative height a
|
|
190
|
+
// different quantity per variant.
|
|
191
|
+
const heights = new Float64Array(total);
|
|
192
|
+
for (let i = 0; i < total; i += 1) heights[i] = (attachments[i].position.y - minY) / span;
|
|
193
|
+
|
|
194
|
+
if (crownBase > 0) {
|
|
195
|
+
for (let i = 0; i < total; i += 1) if (heights[i] < crownBase) alive[i] = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (layerCount >= 2) {
|
|
199
|
+
// Re-normalize above the lifted skirt so the plates divide the foliage
|
|
200
|
+
// that actually survives rather than the empty band below it.
|
|
201
|
+
const base = crownBase > 0 ? crownBase : 0;
|
|
202
|
+
const usable = Math.max(1 - base, 1e-4);
|
|
203
|
+
const pitch = 1 / (layerCount - 1);
|
|
204
|
+
const halfWidth = (layerThickness * pitch) / 2;
|
|
205
|
+
const centres = new Float64Array(layerCount);
|
|
206
|
+
for (let k = 0; k < layerCount; k += 1) {
|
|
207
|
+
// Deterministic per-plate wobble: real tiers are not a comb.
|
|
208
|
+
const wobble = layerJitter
|
|
209
|
+
? (Math.sin((k + 1) * 12.9898) * 43758.5453 % 1 + 1) % 1 - 0.5
|
|
210
|
+
: 0;
|
|
211
|
+
centres[k] = k * pitch + wobble * layerJitter * pitch;
|
|
212
|
+
}
|
|
213
|
+
for (let i = 0; i < total; i += 1) {
|
|
214
|
+
if (!alive[i]) continue;
|
|
215
|
+
const t = (heights[i] - base) / usable;
|
|
216
|
+
let inside = false;
|
|
217
|
+
for (let k = 0; k < layerCount; k += 1) {
|
|
218
|
+
if (Math.abs(t - centres[k]) <= halfWidth) { inside = true; break; }
|
|
219
|
+
}
|
|
220
|
+
if (!inside) alive[i] = false;
|
|
221
|
+
}
|
|
222
|
+
// A band-pass that keeps nothing is a bug, not a style. Fall back to
|
|
223
|
+
// the un-plated crown rather than shipping an invisible tree.
|
|
224
|
+
if (!alive.some(Boolean)) alive = new Array(total).fill(true);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (hasPadPolicy) {
|
|
229
|
+
const candidates = [];
|
|
230
|
+
for (let i = 0; i < total; i += 1) if (alive[i]) candidates.push(i);
|
|
231
|
+
if (candidates.length > padCount) {
|
|
232
|
+
// Seed from the highest surviving tip: a garden tree's uppermost mass
|
|
233
|
+
// is the one a viewer registers first, so it is never the one pruned.
|
|
234
|
+
let firstSlot = 0;
|
|
235
|
+
for (let s = 1; s < candidates.length; s += 1) {
|
|
236
|
+
if (attachments[candidates[s]].position.y > attachments[candidates[firstSlot]].position.y) {
|
|
237
|
+
firstSlot = s;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const chosen = [firstSlot];
|
|
241
|
+
const nearest = new Float64Array(candidates.length);
|
|
242
|
+
for (let s = 0; s < candidates.length; s += 1) {
|
|
243
|
+
nearest[s] = attachments[candidates[s]].position
|
|
244
|
+
.distanceToSquared(attachments[candidates[firstSlot]].position);
|
|
245
|
+
}
|
|
246
|
+
nearest[firstSlot] = -1;
|
|
247
|
+
while (chosen.length < padCount) {
|
|
248
|
+
let best = -1;
|
|
249
|
+
let bestDistance = -1;
|
|
250
|
+
for (let s = 0; s < candidates.length; s += 1) {
|
|
251
|
+
if (nearest[s] > bestDistance) { bestDistance = nearest[s]; best = s; }
|
|
252
|
+
}
|
|
253
|
+
if (best < 0 || bestDistance <= 0) break;
|
|
254
|
+
chosen.push(best);
|
|
255
|
+
nearest[best] = -1;
|
|
256
|
+
for (let s = 0; s < candidates.length; s += 1) {
|
|
257
|
+
if (nearest[s] < 0) continue;
|
|
258
|
+
const distance = attachments[candidates[s]].position
|
|
259
|
+
.distanceToSquared(attachments[candidates[best]].position);
|
|
260
|
+
if (distance < nearest[s]) nearest[s] = distance;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const keptSlots = new Set(chosen);
|
|
264
|
+
for (let s = 0; s < candidates.length; s += 1) {
|
|
265
|
+
if (!keptSlots.has(s)) alive[candidates[s]] = false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const overrides = { ...(existingOverrides ?? {}) };
|
|
271
|
+
for (let i = 0; i < total; i += 1) {
|
|
272
|
+
if (overrides[i]) continue;
|
|
273
|
+
if (!alive[i]) {
|
|
274
|
+
overrides[i] = { densityScale: 0, cardsPerCluster: 0 };
|
|
275
|
+
} else if (densityBoost !== 1) {
|
|
276
|
+
overrides[i] = { densityScale: densityBoost };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return overrides;
|
|
280
|
+
}
|
|
281
|
+
|
|
101
282
|
// Curved trunk along a seeded 3D spine. Returns { geometry, canopyAnchor }:
|
|
102
283
|
// the anchor is the spine's top, so the crown follows the trunk's lean.
|
|
103
284
|
//
|
|
@@ -758,6 +939,12 @@ export function createBranchingTreeSkeleton({
|
|
|
758
939
|
twist = 0,
|
|
759
940
|
gnarl = 0,
|
|
760
941
|
lean = 0,
|
|
942
|
+
// World heading of the bow, and the lean heading relative to it — the same
|
|
943
|
+
// meaning they carry on the space-colonization generator. Without them a
|
|
944
|
+
// stand of wind-shaped trees leans a different way per seed, which reads
|
|
945
|
+
// as damage rather than as wind. null keeps the historical seeded pick.
|
|
946
|
+
bendDirection = null,
|
|
947
|
+
leanOffset = null,
|
|
761
948
|
} = trunk;
|
|
762
949
|
const rand = seededRandom(seed * 4.87 + 2.3);
|
|
763
950
|
let randKey = 0;
|
|
@@ -807,11 +994,19 @@ export function createBranchingTreeSkeleton({
|
|
|
807
994
|
const workVector = new THREE.Vector3();
|
|
808
995
|
const workAxis = new THREE.Vector3();
|
|
809
996
|
const workQuaternion = new THREE.Quaternion();
|
|
810
|
-
|
|
997
|
+
// The bow's world heading. Tilting about a horizontal axis displaces the
|
|
998
|
+
// trunk 90° away from that axis, so an authored heading is stored rotated
|
|
999
|
+
// back by a quarter turn — that way `bendDirection` means "the direction the
|
|
1000
|
+
// trunk bows toward", identical to the space-colonization generator.
|
|
1001
|
+
const seededBendHeading = seededRandom(seed * 9.17 + 4.3)(17) * Math.PI * 2;
|
|
1002
|
+
const trunkBowHeading = bendDirection == null
|
|
1003
|
+
? seededBendHeading + Math.PI / 2
|
|
1004
|
+
: bendDirection;
|
|
1005
|
+
const trunkBendAxisHeading = trunkBowHeading - Math.PI / 2;
|
|
811
1006
|
const trunkBendAxis = new THREE.Vector3(
|
|
812
|
-
Math.cos(
|
|
1007
|
+
Math.cos(trunkBendAxisHeading),
|
|
813
1008
|
0,
|
|
814
|
-
Math.sin(
|
|
1009
|
+
Math.sin(trunkBendAxisHeading),
|
|
815
1010
|
);
|
|
816
1011
|
|
|
817
1012
|
// { origin, quaternion, length, radius, level, segments } — BFS like EZ.
|
|
@@ -1069,9 +1264,18 @@ export function createBranchingTreeSkeleton({
|
|
|
1069
1264
|
// Trunk: optional initial lean carries the classic trunk styles over.
|
|
1070
1265
|
const rootQuaternion = new THREE.Quaternion();
|
|
1071
1266
|
if (!spinePoints && lean) {
|
|
1072
|
-
|
|
1267
|
+
// The seeded draw is consumed either way, so pinning the lean direction
|
|
1268
|
+
// never shifts the rest of this tree's random stream and every existing
|
|
1269
|
+
// seed keeps its exact geometry.
|
|
1270
|
+
const seededLeanHeading = next() * Math.PI * 2;
|
|
1271
|
+
const leanHeading = leanOffset == null
|
|
1272
|
+
? seededLeanHeading + Math.PI / 2
|
|
1273
|
+
: trunkBowHeading + leanOffset;
|
|
1274
|
+
const leanAxisHeading = leanHeading - Math.PI / 2;
|
|
1073
1275
|
rootQuaternion.setFromAxisAngle(
|
|
1074
|
-
new THREE.Vector3(Math.cos(
|
|
1276
|
+
new THREE.Vector3(Math.cos(leanAxisHeading), 0, Math.sin(leanAxisHeading)),
|
|
1277
|
+
lean * 0.45,
|
|
1278
|
+
);
|
|
1075
1279
|
}
|
|
1076
1280
|
queue.push({
|
|
1077
1281
|
origin: new THREE.Vector3(0, 0, 0),
|
|
@@ -1130,88 +1334,6 @@ export const TREE_TRUNK_STYLES = Object.freeze({
|
|
|
1130
1334
|
height: 1.8, radiusBottom: 0.24 },
|
|
1131
1335
|
});
|
|
1132
1336
|
|
|
1133
|
-
// Ready-made example recipes, ordered least → most complex configuration.
|
|
1134
|
-
// Used by Tree Lab and the playground scene's showcase row; each is a
|
|
1135
|
-
// complete options object for `new StylizedTree(...)`.
|
|
1136
|
-
export const STYLIZED_TREE_EXAMPLES = Object.freeze([
|
|
1137
|
-
// 1. Baseline: straight trunk, default crown, one flat color.
|
|
1138
|
-
{ seed: 3, size: 1.7, canopyColor: 0x4da258, leafDensity: 1,
|
|
1139
|
-
trunk: TREE_TRUNK_STYLES.straight },
|
|
1140
|
-
// 2. Leaning trunk, same simple crown.
|
|
1141
|
-
{ seed: 8, size: 1.8, canopyColor: 0x54a85e, leafDensity: 1,
|
|
1142
|
-
trunk: TREE_TRUNK_STYLES.leaning },
|
|
1143
|
-
// 3. Slight see-through: gap pockets open, branches peek through.
|
|
1144
|
-
{ seed: 5, size: 1.9, canopyColor: 0x5eb063, leafDensity: 0.85,
|
|
1145
|
-
trunk: TREE_TRUNK_STYLES.leaning },
|
|
1146
|
-
// 4. Curved trunk + its own irregular crown layout.
|
|
1147
|
-
{ seed: 11, size: 2.0, canopyColor: 0x58ab5c, leafDensity: 0.95,
|
|
1148
|
-
trunk: TREE_TRUNK_STYLES.curved },
|
|
1149
|
-
// 5. Color picked from a list, per-seed (forest variation from one spec).
|
|
1150
|
-
{ seed: 17, size: 2.0, canopyColor: [0x4da258, 0x7fb84e, 0x9cbf46], leafDensity: 0.95,
|
|
1151
|
-
trunk: TREE_TRUNK_STYLES.curved },
|
|
1152
|
-
// 6. Wide-and-shallow crown (X reach 1.6, Z reach 0.7).
|
|
1153
|
-
{ seed: 9, size: 2.0, canopyColor: 0x6db54f, leafDensity: 0.95,
|
|
1154
|
-
canopyWidth: 1.6, canopyDepth: 0.7, trunk: TREE_TRUNK_STYLES.leaning },
|
|
1155
|
-
// 7. Autumn blend: seeded mix between two colors.
|
|
1156
|
-
{ seed: 21, size: 2.1, canopyColor: { from: 0xe8a33c, to: 0xd96f29 }, leafDensity: 0.9,
|
|
1157
|
-
trunk: TREE_TRUNK_STYLES.curved },
|
|
1158
|
-
// 8. Gnarled old tree: knotted growth, sparser crown shows the wood.
|
|
1159
|
-
{ seed: 14, size: 2.0, canopyColor: 0x8f9e44, leafDensity: 0.72,
|
|
1160
|
-
trunk: TREE_TRUNK_STYLES.gnarled },
|
|
1161
|
-
// 9. Bonsai: corkscrew twist, flat wide pads, HSL-range blossom color.
|
|
1162
|
-
{ seed: 26, size: 1.7, leafDensity: 0.8, canopyWidth: 1.4, canopyDepth: 1.2,
|
|
1163
|
-
canopyColor: { hue: [0.9, 1.0], saturation: [0.45, 0.6], lightness: [0.62, 0.72] },
|
|
1164
|
-
trunk: TREE_TRUNK_STYLES.bonsai },
|
|
1165
|
-
// 10. The Liyue golden gingko: fat-based serpentine trunk (bow right,
|
|
1166
|
-
// top swept hard left, S locked by leanOffset), extra-wide crown,
|
|
1167
|
-
// pinned pale-gold highlight tone. bendDirection 0 keeps the S in the
|
|
1168
|
-
// X-Y plane so the silhouette reads head-on in the showcase row.
|
|
1169
|
-
{ seed: 12, size: 2.4, canopyColor: 0xf5c531, canopyPalette: { crown: 0xffe98a },
|
|
1170
|
-
leafDensity: 0.95, canopyWidth: 1.5,
|
|
1171
|
-
skeleton: { radialSegments: 10 },
|
|
1172
|
-
trunk: { ...TREE_TRUNK_STYLES.swooping, bend: 0.5, lean: 0.95,
|
|
1173
|
-
bendDirection: 0, height: 2.0, radiusBottom: 0.28 } },
|
|
1174
|
-
// 11. Sumeru-style: long bare pale limbs reaching out of the crown with
|
|
1175
|
-
// violet leaf bushes only at the branch ends (leafPlacement 'tips').
|
|
1176
|
-
{ seed: 31, size: 2.3, pale: true, canopyColor: 0x8578e6,
|
|
1177
|
-
canopyPalette: { crown: 0xbdb2ff },
|
|
1178
|
-
leafDensity: 0.9, canopyWidth: 1.45, leafPlacement: 'tips',
|
|
1179
|
-
trunkReceiveShadow: false,
|
|
1180
|
-
skeleton: { attractionCount: 70, influenceRadius: 1.35 },
|
|
1181
|
-
trunk: { ...TREE_TRUNK_STYLES.curved, bend: 0.3, lean: 0.35, gnarl: 0.45,
|
|
1182
|
-
height: 1.9, radiusBottom: 0.26 } },
|
|
1183
|
-
// 12. MASSIVE climbable Sumeru tree: thick bare limbs long and low enough
|
|
1184
|
-
// to stand or sit on (scenes read `climbable: true` and collide the
|
|
1185
|
-
// wood as a trimesh instead of a trunk capsule), sparse skeleton so
|
|
1186
|
-
// the pale limbs stay on show, foliage clouds only at the limb ends.
|
|
1187
|
-
{ seed: 46, size: 4.0, pale: true, climbable: true,
|
|
1188
|
-
canopyColor: 0x8578e6, canopyPalette: { crown: 0xbdb2ff },
|
|
1189
|
-
leafDensity: 0.92, canopyWidth: 1.75, canopyDepth: 1.2,
|
|
1190
|
-
leafPlacement: 'tips', trunkReceiveShadow: false,
|
|
1191
|
-
skeleton: { attractionCount: 55, influenceRadius: 1.7, killRadius: 0.55,
|
|
1192
|
-
segmentLength: 0.36, attractionReach: 0.95, radialSegments: 14,
|
|
1193
|
-
tipRadius: 0.05, minLimbRadius: 0.04, maxNodes: 130 },
|
|
1194
|
-
canopy: { cardsPerCluster: 12, clusterRadius: 0.62 },
|
|
1195
|
-
trunk: { ...TREE_TRUNK_STYLES.leaning, bend: 0.24, lean: 0.42,
|
|
1196
|
-
height: 1.4, radiusBottom: 0.48 } },
|
|
1197
|
-
]);
|
|
1198
|
-
|
|
1199
|
-
// Centered X offsets for a showcase row: cumulative spacing from each tree's
|
|
1200
|
-
// approximate crown footprint, so a massive example doesn't swallow its
|
|
1201
|
-
// neighbors the way fixed spacing would.
|
|
1202
|
-
export function layoutTreeRow(configs, { margin = 1.6 } = {}) {
|
|
1203
|
-
const footprints = configs.map((config) =>
|
|
1204
|
-
(config.size ?? 1) * (config.canopyWidth ?? 1) * 2.3 + 1.4);
|
|
1205
|
-
const offsets = [];
|
|
1206
|
-
let cursor = 0;
|
|
1207
|
-
footprints.forEach((footprint, index) => {
|
|
1208
|
-
if (index > 0) cursor += (footprints[index - 1] + footprint) / 2 + margin;
|
|
1209
|
-
offsets.push(cursor);
|
|
1210
|
-
});
|
|
1211
|
-
const center = cursor / 2;
|
|
1212
|
-
return offsets.map((offset) => offset - center);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
1337
|
// Recipe documents: a plant serialized as { schema, version, type, options }.
|
|
1216
1338
|
// The options are exactly what the constructor takes, so a recipe rebuilds
|
|
1217
1339
|
// the identical plant (generation is deterministic per seed). Defined here —
|
|
@@ -1289,6 +1411,24 @@ function booleanOption(value, fallback) {
|
|
|
1289
1411
|
return value === undefined ? fallback : Boolean(value);
|
|
1290
1412
|
}
|
|
1291
1413
|
|
|
1414
|
+
// `canopy.padLayers` — the horizontal-plate band-pass. A count below 2 is not
|
|
1415
|
+
// a plate structure (one band is the whole crown), so it resolves to null
|
|
1416
|
+
// rather than silently pruning to a single slab.
|
|
1417
|
+
function padLayerOption(value, fallback) {
|
|
1418
|
+
if (value === undefined) return fallback;
|
|
1419
|
+
if (value === null || typeof value !== 'object') return null;
|
|
1420
|
+
const count = integerNumber(value.count, 0, { min: 0, max: 24 });
|
|
1421
|
+
if (count < 2) return null;
|
|
1422
|
+
return Object.freeze({
|
|
1423
|
+
count,
|
|
1424
|
+
// Fraction of each band's pitch that carries foliage; the rest is the
|
|
1425
|
+
// gap a viewer sees sky and limb through.
|
|
1426
|
+
thickness: finiteNumber(value.thickness, 0.5, { min: 0.02, max: 1 }),
|
|
1427
|
+
// Deterministic per-band height wobble, as a fraction of the pitch.
|
|
1428
|
+
jitter: finiteNumber(value.jitter, 0, { min: 0, max: 1 }),
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1292
1432
|
function colorArray(value, fallback) {
|
|
1293
1433
|
if (value?.isColor) return value.clone().convertLinearToSRGB().toArray();
|
|
1294
1434
|
if (Array.isArray(value) && value.length >= 3) {
|
|
@@ -1398,6 +1538,29 @@ export const DEFAULT_STYLIZED_TREE_SETTINGS = Object.freeze({
|
|
|
1398
1538
|
clusterRadius: 0.48,
|
|
1399
1539
|
frondCount: 7,
|
|
1400
1540
|
frondLength: 1.25,
|
|
1541
|
+
// Bare every branch tip below this fraction of the crown's own vertical
|
|
1542
|
+
// extent, lifting the foliage skirt so the trunk and the lower limb
|
|
1543
|
+
// structure read as form. 0 = no lift, which is every historical tree.
|
|
1544
|
+
crownBase: 0,
|
|
1545
|
+
// Keep only this many well-separated foliage attachments and bare the
|
|
1546
|
+
// rest, so the crown reads as discrete pads with visible limb between
|
|
1547
|
+
// them. null = no pruning, which is every historical tree.
|
|
1548
|
+
padCount: null,
|
|
1549
|
+
// Multiplies the card count of every tip that SURVIVES pruning, so a
|
|
1550
|
+
// crown can be restructured into plates or pads at an unchanged card
|
|
1551
|
+
// total instead of simply losing the foliage the pruning removed.
|
|
1552
|
+
padDensityBoost: 1,
|
|
1553
|
+
// Horizontal plates: { count, thickness, jitter }. Keeps tips inside
|
|
1554
|
+
// `count` bands across the crown height and bares the tips between them,
|
|
1555
|
+
// which is the structure of a layered broadleaf and the one thing
|
|
1556
|
+
// isotropic pad pruning cannot produce. null = no plating.
|
|
1557
|
+
padLayers: null,
|
|
1558
|
+
// 0..1, layered-sprays only. Rotates each tuft's spray disc from
|
|
1559
|
+
// across-the-twig (0) to world-horizontal (1). See stylizedTreeFoliage.
|
|
1560
|
+
plateAlignment: 0,
|
|
1561
|
+
// Drops the outer edge of each spray disc as a fraction of its radius, so
|
|
1562
|
+
// a plate sags rather than reading as a machined shelf. 0 = flat.
|
|
1563
|
+
plateDroop: 0,
|
|
1401
1564
|
shellFill: true,
|
|
1402
1565
|
sprayLayers: 3,
|
|
1403
1566
|
spraySpread: 0.8,
|
|
@@ -1422,37 +1585,15 @@ export const DEFAULT_STYLIZED_TREE_SETTINGS = Object.freeze({
|
|
|
1422
1585
|
}),
|
|
1423
1586
|
});
|
|
1424
1587
|
|
|
1425
|
-
// Named
|
|
1426
|
-
//
|
|
1427
|
-
//
|
|
1428
|
-
// registerStylizedTreePreset().
|
|
1588
|
+
// Named runtime settings: npm ships only the neutral baseline. Authored tree
|
|
1589
|
+
// recipes live in the public Gallery and may be registered explicitly after
|
|
1590
|
+
// MCP discovery when an application wants name-based selection.
|
|
1429
1591
|
const stylizedTreePresetRegistry = new Map([
|
|
1430
1592
|
['default', Object.freeze({
|
|
1431
1593
|
description: 'Baseline stylized tree.',
|
|
1432
1594
|
label: 'Default',
|
|
1433
1595
|
settings: Object.freeze({}),
|
|
1434
1596
|
})],
|
|
1435
|
-
['call_me_sensei', Object.freeze({
|
|
1436
|
-
description: 'Studio-managed signature tree, curated by Call Me Sensei and updated over releases. Open broadleaf with visible branching: leaf clusters sit at the branch tips instead of filling a solid shell, so the limb structure reads through the crown.',
|
|
1437
|
-
label: 'Call Me Sensei',
|
|
1438
|
-
settings: Object.freeze({
|
|
1439
|
-
skeleton: Object.freeze({ attractionCount: 55, influenceRadius: 1.35 }),
|
|
1440
|
-
tree: Object.freeze({
|
|
1441
|
-
canopyWidth: 1.35,
|
|
1442
|
-
leafDensity: 1.15,
|
|
1443
|
-
leafPlacement: 'tips',
|
|
1444
|
-
trunkColor: Object.freeze([0.58, 0.36, 0.2]),
|
|
1445
|
-
}),
|
|
1446
|
-
trunk: Object.freeze({
|
|
1447
|
-
...TREE_TRUNK_STYLES.curved,
|
|
1448
|
-
bend: 0.24,
|
|
1449
|
-
gnarl: 0.22,
|
|
1450
|
-
height: 1.9,
|
|
1451
|
-
lean: 0.24,
|
|
1452
|
-
radiusBottom: 0.2,
|
|
1453
|
-
}),
|
|
1454
|
-
}),
|
|
1455
|
-
})],
|
|
1456
1597
|
]);
|
|
1457
1598
|
|
|
1458
1599
|
/**
|
|
@@ -1592,6 +1733,12 @@ export function createStylizedTreeSettings(options = {}) {
|
|
|
1592
1733
|
clusterRadius: finiteNumber(canopySource.clusterRadius, base.canopy.clusterRadius, { min: 0.01 }),
|
|
1593
1734
|
frondCount: integerNumber(canopySource.frondCount, base.canopy.frondCount, { min: 3, max: 24 }),
|
|
1594
1735
|
frondLength: finiteNumber(canopySource.frondLength, base.canopy.frondLength, { min: 0.1, max: 4 }),
|
|
1736
|
+
crownBase: finiteNumber(canopySource.crownBase, base.canopy.crownBase, { min: 0, max: 0.95 }),
|
|
1737
|
+
padCount: nullableNumber(canopySource.padCount, base.canopy.padCount, { min: 1, max: 2000 }),
|
|
1738
|
+
padDensityBoost: finiteNumber(canopySource.padDensityBoost, base.canopy.padDensityBoost, { min: 0.1, max: 12 }),
|
|
1739
|
+
padLayers: padLayerOption(canopySource.padLayers, base.canopy.padLayers),
|
|
1740
|
+
plateAlignment: finiteNumber(canopySource.plateAlignment, base.canopy.plateAlignment, { min: 0, max: 1 }),
|
|
1741
|
+
plateDroop: finiteNumber(canopySource.plateDroop, base.canopy.plateDroop, { min: 0, max: 2 }),
|
|
1595
1742
|
shellFill: booleanOption(canopySource.shellFill, base.canopy.shellFill),
|
|
1596
1743
|
sprayLayers: integerNumber(canopySource.sprayLayers, base.canopy.sprayLayers, { min: 1, max: 12 }),
|
|
1597
1744
|
spraySpread: finiteNumber(canopySource.spraySpread, base.canopy.spraySpread, { min: 0.05, max: 4 }),
|
|
@@ -2469,16 +2616,25 @@ export class StylizedTree extends THREE.Group {
|
|
|
2469
2616
|
baseZ + Math.sin(bend) * (baseRadius * 0.2 + length * t),
|
|
2470
2617
|
];
|
|
2471
2618
|
};
|
|
2619
|
+
// The tip has to end UNDER the surface. The historical profile dived
|
|
2620
|
+
// to a fixed -0.18 however far the root ran, so every preset beyond a
|
|
2621
|
+
// stub laid a long tapered spike flat on the ground in full view —
|
|
2622
|
+
// this is why `medium` and `large` read as black spider legs around
|
|
2623
|
+
// the collar in any frame low enough to see the base, and it lands
|
|
2624
|
+
// squarely on the Gate 1 ground-contact shot. Diving in proportion to
|
|
2625
|
+
// the root's own reach keeps the collar flare on show and puts the
|
|
2626
|
+
// rest of the root where a root belongs.
|
|
2627
|
+
const dive = Math.max(0.18, length * 0.44);
|
|
2472
2628
|
const tube = createBranchTubeGeometry({
|
|
2473
2629
|
flareBase: false,
|
|
2474
2630
|
irregularity: 0.2,
|
|
2475
2631
|
points: [
|
|
2476
2632
|
pointAt(0, collarY),
|
|
2477
2633
|
pointAt(0.2, collarY * 0.45),
|
|
2478
|
-
pointAt(0.4, 0.
|
|
2479
|
-
pointAt(0.6, -0.
|
|
2480
|
-
pointAt(0.8, -0.
|
|
2481
|
-
pointAt(1, -
|
|
2634
|
+
pointAt(0.4, -dive * 0.1),
|
|
2635
|
+
pointAt(0.6, -dive * 0.3),
|
|
2636
|
+
pointAt(0.8, -dive * 0.6),
|
|
2637
|
+
pointAt(1, -dive),
|
|
2482
2638
|
],
|
|
2483
2639
|
radialSegments: 7,
|
|
2484
2640
|
radiusEnd: baseRadius * rootSpec.radius * 0.4,
|
|
@@ -2550,9 +2706,22 @@ export class StylizedTree extends THREE.Group {
|
|
|
2550
2706
|
// Only forward canopy values that differ from the defaults, so the
|
|
2551
2707
|
// tips-placement geometry presets below keep winning unless the caller
|
|
2552
2708
|
// explicitly overrides them (exactly the legacy sparse-options behavior).
|
|
2709
|
+
//
|
|
2710
|
+
// That difference test cannot distinguish "not supplied" from "supplied,
|
|
2711
|
+
// and equal to the module default", so a caller that authors the
|
|
2712
|
+
// documented default gets the preset instead — cardsPerCluster: 5 lands
|
|
2713
|
+
// as the branching preset's 2, silently halving the canopy. Callers that
|
|
2714
|
+
// do know which fields they were handed may declare them in
|
|
2715
|
+
// `canopy.explicit`, and those are forwarded whatever their value.
|
|
2716
|
+
// Nothing that omits `explicit` changes behaviour.
|
|
2717
|
+
const explicitCanopyKeys = new Set(
|
|
2718
|
+
Array.isArray(settings.canopy.explicit) ? settings.canopy.explicit : [],
|
|
2719
|
+
);
|
|
2553
2720
|
const canopyOverrides = {};
|
|
2554
2721
|
for (const [key, value] of Object.entries(settings.canopy)) {
|
|
2555
|
-
if (
|
|
2722
|
+
if (key === 'explicit') continue;
|
|
2723
|
+
if (explicitCanopyKeys.has(key) ||
|
|
2724
|
+
!(key in DEFAULT_STYLIZED_TREE_SETTINGS.canopy) ||
|
|
2556
2725
|
!sameSettingValue(value, DEFAULT_STYLIZED_TREE_SETTINGS.canopy[key])) {
|
|
2557
2726
|
canopyOverrides[key] = value;
|
|
2558
2727
|
}
|
|
@@ -2589,7 +2758,16 @@ export class StylizedTree extends THREE.Group {
|
|
|
2589
2758
|
? { shellFill: true, shellBudget: extraBlobs.length * 8 }
|
|
2590
2759
|
: { shellFill: false, ...(attachments.length ? {} : { cardCount: 0 }) })
|
|
2591
2760
|
: {}),
|
|
2592
|
-
attachmentOverrides:
|
|
2761
|
+
attachmentOverrides: resolvePadPruning(
|
|
2762
|
+
attachments,
|
|
2763
|
+
{
|
|
2764
|
+
crownBase: settings.canopy.crownBase,
|
|
2765
|
+
densityBoost: settings.canopy.padDensityBoost,
|
|
2766
|
+
layers: settings.canopy.padLayers,
|
|
2767
|
+
padCount: settings.canopy.padCount,
|
|
2768
|
+
},
|
|
2769
|
+
branchOverrides,
|
|
2770
|
+
),
|
|
2593
2771
|
blobs: (generator === 'drawn' || generator === 'branching') && extraBlobs.length
|
|
2594
2772
|
? extraBlobs : blobs,
|
|
2595
2773
|
});
|
|
@@ -2604,6 +2782,19 @@ export class StylizedTree extends THREE.Group {
|
|
|
2604
2782
|
vegetationShader,
|
|
2605
2783
|
});
|
|
2606
2784
|
|
|
2785
|
+
// A caller can select the style either as `preset` or by handing over the
|
|
2786
|
+
// vegetation shader profile itself (a string, or { style } / { preset } —
|
|
2787
|
+
// the same shape setVegetationShader accepts). Testing only `options.preset`
|
|
2788
|
+
// meant every generator that forwards `vegetationShader` but not `preset` —
|
|
2789
|
+
// BranchTree among them — silently built a bare, untextured trunk.
|
|
2790
|
+
const requestedStyleId = String(
|
|
2791
|
+
options.preset
|
|
2792
|
+
?? (typeof vegetationShader === 'string'
|
|
2793
|
+
? vegetationShader
|
|
2794
|
+
: vegetationShader?.style ?? vegetationShader?.preset ?? ''),
|
|
2795
|
+
).trim();
|
|
2796
|
+
const styleDefaultSurfaceProfile = TREE_SURFACE_PROFILE_DEFAULTS[requestedStyleId] ?? null;
|
|
2797
|
+
|
|
2607
2798
|
this.trunkMesh = new THREE.Mesh(
|
|
2608
2799
|
trunkGeometry,
|
|
2609
2800
|
trunkMaterial ?? createWoodySurfaceNodeMaterial({
|
|
@@ -2612,11 +2803,8 @@ export class StylizedTree extends THREE.Group {
|
|
|
2612
2803
|
map: trunkMap ?? (
|
|
2613
2804
|
trunkSurfaceProfile && trunkSurfaceProfile !== 'none'
|
|
2614
2805
|
? createTreeSurfaceTexture({ profileId: trunkSurfaceProfile, seed })
|
|
2615
|
-
:
|
|
2616
|
-
? createTreeSurfaceTexture({
|
|
2617
|
-
profileId: TREE_SURFACE_PROFILE_DEFAULTS.call_me_sensei,
|
|
2618
|
-
seed,
|
|
2619
|
-
})
|
|
2806
|
+
: styleDefaultSurfaceProfile
|
|
2807
|
+
? createTreeSurfaceTexture({ profileId: styleDefaultSurfaceProfile, seed })
|
|
2620
2808
|
: null
|
|
2621
2809
|
),
|
|
2622
2810
|
sceneShadowStrength: trunkReceiveShadow ? 1 : 0,
|
|
@@ -2630,9 +2818,7 @@ export class StylizedTree extends THREE.Group {
|
|
|
2630
2818
|
? null
|
|
2631
2819
|
: trunkSurfaceProfile && trunkSurfaceProfile !== 'none'
|
|
2632
2820
|
? trunkSurfaceProfile
|
|
2633
|
-
:
|
|
2634
|
-
? TREE_SURFACE_PROFILE_DEFAULTS.call_me_sensei
|
|
2635
|
-
: null;
|
|
2821
|
+
: styleDefaultSurfaceProfile;
|
|
2636
2822
|
this.trunkMesh.material.userData.toonlabBarkSurface = {
|
|
2637
2823
|
profileId: selectedTrunkSurfaceProfile,
|
|
2638
2824
|
source: authoredTrunkMap
|