@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
|
@@ -156,6 +156,78 @@ export function traceLeafShapePath(ctx, shape, length, width, outline = null) {
|
|
|
156
156
|
if (i === 0) ctx.moveTo(x, y);
|
|
157
157
|
else ctx.lineTo(x, y);
|
|
158
158
|
}
|
|
159
|
+
} else if (shape === 'palmate') {
|
|
160
|
+
// Acer palmatum. Seven deeply divided lobes, sinuses cutting almost to
|
|
161
|
+
// the petiole, each lobe lanceolate with a serrated margin.
|
|
162
|
+
//
|
|
163
|
+
// This is NOT a variant of 'maple' above: that outline is a five-point
|
|
164
|
+
// sugar-maple star with blunt lobes and shallow sinuses, which is a
|
|
165
|
+
// correct Acer saccharum and a wrong Acer palmatum. The Japanese maple's
|
|
166
|
+
// whole identity is that the crown reads as lace rather than as a mass of
|
|
167
|
+
// leaf blades, and at 85 mm the difference between a five-point star and
|
|
168
|
+
// a seven-lobe dissected leaf is the difference between the species.
|
|
169
|
+
// The fan is nearly a full circle: on a real A. palmatum the outer pair of
|
|
170
|
+
// lobes swings back past horizontal, almost touching the petiole, which is
|
|
171
|
+
// what gives the leaf its star read rather than a hand read. A narrow fan
|
|
172
|
+
// is the single most common way a stylized "maple" ends up looking like a
|
|
173
|
+
// palm frond instead.
|
|
174
|
+
const lobes = 7;
|
|
175
|
+
const spread = 4.9; // full fan, radians (~281 deg)
|
|
176
|
+
const sinus = 0.11; // sinus depth, as a radial fraction
|
|
177
|
+
const half = (spread / (lobes - 1)) * 0.34;
|
|
178
|
+
const steps = 10; // even → symmetric serration
|
|
179
|
+
let started = false;
|
|
180
|
+
const emit = (angle, radial) => {
|
|
181
|
+
const x = Math.sin(angle) * width * radial;
|
|
182
|
+
const y = -Math.cos(angle) * length * radial;
|
|
183
|
+
if (started) ctx.lineTo(x, y);
|
|
184
|
+
else { ctx.moveTo(x, y); started = true; }
|
|
185
|
+
};
|
|
186
|
+
for (let i = 0; i < lobes; i += 1) {
|
|
187
|
+
const axis = ((i / (lobes - 1)) * 2 - 1) * spread * 0.5;
|
|
188
|
+
for (let side = -1; side <= 1; side += 2) {
|
|
189
|
+
for (let k = 0; k <= steps; k += 1) {
|
|
190
|
+
// Left margin runs outward, right margin runs back in, so the
|
|
191
|
+
// outline stays a single continuous closed loop.
|
|
192
|
+
const step = side < 0 ? k : steps - k;
|
|
193
|
+
const t = step / steps;
|
|
194
|
+
// Lanceolate profile: pinched at the sinus, widest around 40% out,
|
|
195
|
+
// drawn to a point at the tip.
|
|
196
|
+
const body = Math.sin(Math.PI * Math.min(1, 0.08 + t * 0.9)) ** 0.62;
|
|
197
|
+
// Alternating teeth along the margin.
|
|
198
|
+
const tooth = step % 2 === 0 ? 1 : 0.74;
|
|
199
|
+
emit(axis + side * half * body * tooth, sinus + (0.5 - sinus) * t);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else if (shape === 'needle-fascicle') {
|
|
204
|
+
// Pinus thunbergii. A black pine's foliage unit is a FASCICLE: two stiff
|
|
205
|
+
// needles bound at a common sheath, diverging into a narrow V. Drawing a
|
|
206
|
+
// pine from single 'needle' blades gives a scatter of loose hairs; the
|
|
207
|
+
// paired V is what makes a card read as pine rather than as fur, and it
|
|
208
|
+
// is also what carries the species' characteristic stiffness.
|
|
209
|
+
// Black pine needles are 6-12 cm long and about 1.5 mm across, held stiff
|
|
210
|
+
// and nearly parallel — the pair diverges by well under 20 deg. A wide V
|
|
211
|
+
// reads as a grass tussock, not as a conifer.
|
|
212
|
+
//
|
|
213
|
+
// The thickness is deliberately far above botanical scale. A card that is
|
|
214
|
+
// 0.5 m across renders about 40 px tall at the distance a garden pine is
|
|
215
|
+
// actually seen from, and a true-scale needle is then a quarter of a pixel:
|
|
216
|
+
// the mip chain averages it below the alpha cutoff and the whole card
|
|
217
|
+
// collapses into a soft lobed blob — which is exactly what the first
|
|
218
|
+
// version did, and it made the pine read as a broadleaf. Stylized conifers
|
|
219
|
+
// exaggerate needle width for the same reason hand-painted ones do.
|
|
220
|
+
const t = width * 0.23;
|
|
221
|
+
const reach = width * 0.17;
|
|
222
|
+
const sheathTop = length * 0.3;
|
|
223
|
+
ctx.moveTo(-t * 1.7, length * 0.5);
|
|
224
|
+
ctx.lineTo(-reach - t * 0.3, -length * 0.5);
|
|
225
|
+
ctx.lineTo(-reach + t * 0.3, -length * 0.48);
|
|
226
|
+
ctx.lineTo(-t * 0.3, sheathTop);
|
|
227
|
+
ctx.lineTo(t * 0.3, sheathTop);
|
|
228
|
+
ctx.lineTo(reach - t * 0.3, -length * 0.48);
|
|
229
|
+
ctx.lineTo(reach + t * 0.3, -length * 0.5);
|
|
230
|
+
ctx.lineTo(t * 1.7, length * 0.5);
|
|
159
231
|
} else if (shape === 'gingko' || shape === 'fan') {
|
|
160
232
|
// Fan: narrow stem base opening to a wide notched top edge.
|
|
161
233
|
ctx.moveTo(0, length * 0.5);
|
|
@@ -173,9 +245,22 @@ export function traceLeafShapePath(ctx, shape, length, width, outline = null) {
|
|
|
173
245
|
}
|
|
174
246
|
|
|
175
247
|
export const LEAF_SHAPE_PRESETS = Object.freeze([
|
|
176
|
-
'teardrop', 'round', 'oak', 'maple', 'gingko', 'needle',
|
|
248
|
+
'teardrop', 'round', 'oak', 'maple', 'palmate', 'gingko', 'needle',
|
|
249
|
+
'needle-fascicle',
|
|
177
250
|
]);
|
|
178
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Shapes whose card is a needle spray rather than a broadleaf cluster.
|
|
254
|
+
*
|
|
255
|
+
* Needles are an order of magnitude longer than they are wide, and on a real
|
|
256
|
+
* shoot they radiate from the twig rather than facing every direction at
|
|
257
|
+
* once. Painting them with the broadleaf arrangement below gives a fuzzy
|
|
258
|
+
* felted disc; keying the arrangement off the shape keeps every existing
|
|
259
|
+
* broadleaf sprite byte-identical while letting a conifer card look like a
|
|
260
|
+
* conifer.
|
|
261
|
+
*/
|
|
262
|
+
export const NEEDLE_LEAF_SHAPES = Object.freeze(['needle', 'needle-fascicle']);
|
|
263
|
+
|
|
179
264
|
export function createLeafSpriteTexture({
|
|
180
265
|
size = 512,
|
|
181
266
|
leafCount = 170,
|
|
@@ -202,6 +287,76 @@ export function createLeafSpriteTexture({
|
|
|
202
287
|
ctx.restore();
|
|
203
288
|
};
|
|
204
289
|
|
|
290
|
+
const needleSpray = NEEDLE_LEAF_SHAPES.includes(shape);
|
|
291
|
+
|
|
292
|
+
if (needleSpray) {
|
|
293
|
+
// A conifer card is not a scatter of organs over a disc. Needles are borne
|
|
294
|
+
// on SHOOTS: each year's growth is a short candle, and the fascicles stand
|
|
295
|
+
// out from that candle along its own axis. Distributing them over a disc
|
|
296
|
+
// and pointing each one at the card centre — the broadleaf arrangement —
|
|
297
|
+
// gives a sea-urchin starburst, which is what the first version of this
|
|
298
|
+
// card looked like. Building a handful of shoots and hanging needles off
|
|
299
|
+
// each one is what turns the card back into a bough.
|
|
300
|
+
// Enough shoots, started at enough different radii, that their needle
|
|
301
|
+
// masses OVERLAP. Six evenly spaced candles left visible sky between them
|
|
302
|
+
// and the card read as an asterisk of brushes; a real bough is a
|
|
303
|
+
// continuous mass with structure inside it, not a ring of separate
|
|
304
|
+
// brooms.
|
|
305
|
+
const shootCount = 9;
|
|
306
|
+
const shoots = Array.from({ length: shootCount }, (_, index) => ({
|
|
307
|
+
axis: (index / shootCount) * Math.PI * 2 + (rng() - 0.5) * 0.9,
|
|
308
|
+
base: size * (-0.02 + rng() * 0.14),
|
|
309
|
+
reach: size * (0.2 + rng() * 0.2),
|
|
310
|
+
// Shoots nearer the rim carry the crisp, bright silhouette needles.
|
|
311
|
+
lift: rng(),
|
|
312
|
+
}));
|
|
313
|
+
// Fewer, bolder fascicles. Packing the card with hundreds of hairline
|
|
314
|
+
// needles reads as felt once mipped; a stylized bough wants a countable
|
|
315
|
+
// number of confident strokes.
|
|
316
|
+
const perShoot = Math.max(5, Math.round((leafCount * 0.5) / shootCount));
|
|
317
|
+
const needles = [];
|
|
318
|
+
for (const shoot of shoots) {
|
|
319
|
+
for (let i = 0; i < perShoot; i += 1) {
|
|
320
|
+
// Along the candle, packed toward its outer half the way a real
|
|
321
|
+
// year's growth is.
|
|
322
|
+
const along = Math.pow(rng(), 0.72);
|
|
323
|
+
const radius = shoot.base + along * shoot.reach;
|
|
324
|
+
// Fascicles stand out from the shoot at a shallow angle, alternating
|
|
325
|
+
// to either side, so the candle keeps a visible axis.
|
|
326
|
+
const side = i % 2 === 0 ? 1 : -1;
|
|
327
|
+
const flare = (0.24 + rng() * 0.42) * side;
|
|
328
|
+
const lateral = (rng() - 0.5) * size * 0.07;
|
|
329
|
+
needles.push({
|
|
330
|
+
x: center + Math.cos(shoot.axis) * radius - Math.sin(shoot.axis) * lateral,
|
|
331
|
+
y: center + Math.sin(shoot.axis) * radius + Math.cos(shoot.axis) * lateral,
|
|
332
|
+
orientation: shoot.axis - Math.PI / 2 + flare,
|
|
333
|
+
depth: radius,
|
|
334
|
+
edgeT: Math.min(radius / (size * 0.4), 1),
|
|
335
|
+
pick: rng(),
|
|
336
|
+
spin: rng(),
|
|
337
|
+
shade: rng(),
|
|
338
|
+
lift: shoot.lift,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
needles.sort((a, b) => a.depth - b.depth);
|
|
343
|
+
for (const needle of needles) {
|
|
344
|
+
const length = size * (0.24 + needle.pick * 0.16);
|
|
345
|
+
const width = length * (0.4 + needle.spin * 0.16);
|
|
346
|
+
const luminance = THREE.MathUtils.lerp(
|
|
347
|
+
0.46 + needle.shade * 0.2,
|
|
348
|
+
0.74 + needle.shade * 0.26,
|
|
349
|
+
Math.min(1, needle.edgeT * 0.7 + needle.lift * 0.4),
|
|
350
|
+
);
|
|
351
|
+
drawLeaf(needle.x, needle.y, length, width, needle.orientation, luminance);
|
|
352
|
+
}
|
|
353
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
354
|
+
texture.name = `ToonLabLeafSprite.${shape}`;
|
|
355
|
+
texture.colorSpace = THREE.NoColorSpace;
|
|
356
|
+
texture.anisotropy = 4;
|
|
357
|
+
return texture;
|
|
358
|
+
}
|
|
359
|
+
|
|
205
360
|
const leaves = [];
|
|
206
361
|
for (let i = 0; i < leafCount; i += 1) {
|
|
207
362
|
const angle = rng() * Math.PI * 2;
|
|
@@ -231,6 +386,7 @@ export function createLeafSpriteTexture({
|
|
|
231
386
|
});
|
|
232
387
|
|
|
233
388
|
const texture = new THREE.CanvasTexture(canvas);
|
|
389
|
+
texture.name = `ToonLabLeafSprite.${shape}`;
|
|
234
390
|
texture.colorSpace = THREE.NoColorSpace;
|
|
235
391
|
texture.anisotropy = 4;
|
|
236
392
|
return texture;
|
|
@@ -464,6 +620,31 @@ export function createTreeFoliageGeometry({
|
|
|
464
620
|
sprayLayers = 3,
|
|
465
621
|
spraySpread = 0.8,
|
|
466
622
|
sprayThickness = 0.18,
|
|
623
|
+
// layered-sprays only. 0 (the default, and every historical tree) builds
|
|
624
|
+
// each spray disc ACROSS THE TWIG: the disc's thickness axis is the branch
|
|
625
|
+
// growth direction, so a twig rising at 50 deg carries a disc tilted at
|
|
626
|
+
// 50 deg. Summed over a few hundred tips that is a dome of randomly tilted
|
|
627
|
+
// discs — which is the correct read for most broadleaves.
|
|
628
|
+
//
|
|
629
|
+
// It is the wrong read for a tree whose identity IS horizontality. An Acer
|
|
630
|
+
// palmatum, a dogwood, a layered Japanese hornbeam: the leaf layer is flat
|
|
631
|
+
// WITH THE GROUND, not with the twig that carries it. 1 rotates each spray
|
|
632
|
+
// basis all the way to world horizontal, so every tuft becomes a flat plate
|
|
633
|
+
// however its twig is oriented, and the crown reads as stacked layers you
|
|
634
|
+
// can see between instead of a shell you cannot.
|
|
635
|
+
//
|
|
636
|
+
// Only the DISC is realigned. Placement along the twig (the tuft's offset
|
|
637
|
+
// from its attachment) still follows the branch, so foliage stays where the
|
|
638
|
+
// wood puts it and the skeleton keeps its authority over the silhouette.
|
|
639
|
+
plateAlignment = 0,
|
|
640
|
+
// Drops the OUTER edge of each spray disc, as a fraction of the disc's own
|
|
641
|
+
// radius, on a quadratic falloff from its centre. A perfectly flat plate is
|
|
642
|
+
// a machined shelf: it is what a Cornus controversa or an acacia reads as,
|
|
643
|
+
// and the reason a plated crown can look manufactured rather than grown.
|
|
644
|
+
// Real tiers sag under their own weight and their outer twigs hang, so the
|
|
645
|
+
// tier has a soft drooping edge instead of a cut one. 0 keeps the plate
|
|
646
|
+
// flat, which is every historical tree.
|
|
647
|
+
plateDroop = 0,
|
|
467
648
|
whorlArms = 6,
|
|
468
649
|
whorlRadius = 0.48,
|
|
469
650
|
frondCount = 7,
|
|
@@ -741,6 +922,41 @@ export function createTreeFoliageGeometry({
|
|
|
741
922
|
: new THREE.Vector3(0, 1, 0);
|
|
742
923
|
const branchRight = new THREE.Vector3().crossVectors(growth, basisReference).normalize();
|
|
743
924
|
const branchForward = new THREE.Vector3().crossVectors(branchRight, growth).normalize();
|
|
925
|
+
// The spray disc's own basis. At plateAlignment 0 these are exactly the
|
|
926
|
+
// branch vectors above and nothing moves; at 1 the disc normal is world
|
|
927
|
+
// up and the tuft is a horizontal plate. The blend is on the NORMAL, so
|
|
928
|
+
// partial values tilt the plate rather than shearing it.
|
|
929
|
+
const tuftPlateAlignment = THREE.MathUtils.clamp(
|
|
930
|
+
override?.plateAlignment ?? attachment.plateAlignment ?? plateAlignment,
|
|
931
|
+
0,
|
|
932
|
+
1,
|
|
933
|
+
);
|
|
934
|
+
const tuftPlateDroop = Math.max(
|
|
935
|
+
override?.plateDroop ?? attachment.plateDroop ?? plateDroop,
|
|
936
|
+
0,
|
|
937
|
+
);
|
|
938
|
+
let sprayNormal = growth;
|
|
939
|
+
let sprayRight = branchRight;
|
|
940
|
+
let sprayForward = branchForward;
|
|
941
|
+
if (tuftPlateAlignment > 0) {
|
|
942
|
+
const worldUp = new THREE.Vector3(0, growth.y < 0 ? -1 : 1, 0);
|
|
943
|
+
sprayNormal = growth.clone().lerp(worldUp, tuftPlateAlignment);
|
|
944
|
+
// growth and worldUp are antipodal only when the twig points exactly
|
|
945
|
+
// down, which the sign flip above already rules out; a degenerate
|
|
946
|
+
// lerp result can still appear at float precision.
|
|
947
|
+
if (sprayNormal.lengthSq() < 1e-8) sprayNormal.copy(worldUp);
|
|
948
|
+
sprayNormal.normalize();
|
|
949
|
+
// The plate's long axis follows its own limb's heading, so a plate is
|
|
950
|
+
// elongated ALONG the branch that carries it (which is what a real
|
|
951
|
+
// layered crown does) instead of every plate sharing one world axis —
|
|
952
|
+
// a squash direction repeated over 300 tufts is visible as a grain.
|
|
953
|
+
sprayRight = new THREE.Vector3(growth.x, 0, growth.z);
|
|
954
|
+
if (sprayRight.lengthSq() < 1e-6) sprayRight.copy(branchRight);
|
|
955
|
+
sprayRight.projectOnPlane(sprayNormal);
|
|
956
|
+
if (sprayRight.lengthSq() < 1e-6) sprayRight.copy(branchRight).projectOnPlane(sprayNormal);
|
|
957
|
+
sprayRight.normalize();
|
|
958
|
+
sprayForward = new THREE.Vector3().crossVectors(sprayRight, sprayNormal).normalize();
|
|
959
|
+
}
|
|
744
960
|
for (let i = 0; i < tuftCards; i += 1) {
|
|
745
961
|
let cardSizeScale = 1;
|
|
746
962
|
let cardShape = [1, 1];
|
|
@@ -754,17 +970,23 @@ export function createTreeFoliageGeometry({
|
|
|
754
970
|
const angle = tuftRng() * Math.PI * 2;
|
|
755
971
|
cardCenter.copy(attachment.position)
|
|
756
972
|
.addScaledVector(
|
|
757
|
-
|
|
973
|
+
sprayRight,
|
|
758
974
|
Math.cos(angle) * radialT * spraySpread * foliageSprayScale,
|
|
759
975
|
)
|
|
760
976
|
.addScaledVector(
|
|
761
|
-
|
|
977
|
+
sprayForward,
|
|
762
978
|
Math.sin(angle) * radialT * spraySpread * foliageSprayScale * 0.62,
|
|
763
979
|
)
|
|
764
|
-
.addScaledVector(
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
);
|
|
980
|
+
.addScaledVector(sprayNormal, layerT * sprayThickness * foliageSprayScale)
|
|
981
|
+
// Placement along the twig stays on the BRANCH axis whatever the
|
|
982
|
+
// disc is aligned to: the tuft sits at the end of its own shoot.
|
|
983
|
+
.addScaledVector(growth, tuftRadius * 0.16);
|
|
984
|
+
if (tuftPlateDroop > 0) {
|
|
985
|
+
// Quadratic in the radial coordinate, so the plate is level over
|
|
986
|
+
// its inner half and only the rim hangs.
|
|
987
|
+
cardCenter.y -= tuftPlateDroop * radialT * radialT
|
|
988
|
+
* spraySpread * foliageSprayScale;
|
|
989
|
+
}
|
|
768
990
|
cardSizeScale = (0.88 + tuftRng() * 0.2)
|
|
769
991
|
* (0.88 + foliageSprayScale * 0.12);
|
|
770
992
|
if (individualBroadleafCards && organType === 'broad-leaf') {
|
|
@@ -25,6 +25,74 @@ export const TREE_SURFACE_PROFILES = Object.freeze({
|
|
|
25
25
|
textureVersion: TREE_SURFACE_TEXTURE_VERSION,
|
|
26
26
|
uvRepeat: Object.freeze([1.5, 1]),
|
|
27
27
|
}),
|
|
28
|
+
// Smooth-barked broadleaf. Beech has no fissures at all: the read is soft
|
|
29
|
+
// mottled patches and faint vertical shading on a pale grey-green ground,
|
|
30
|
+
// which is why it needs its own generator rather than a re-tinted oak.
|
|
31
|
+
// Softer light bands than the fissured profiles, because there is no deep
|
|
32
|
+
// relief for a hard terminator to sit in.
|
|
33
|
+
'beech-smooth-v1': Object.freeze({
|
|
34
|
+
id: 'beech-smooth-v1',
|
|
35
|
+
label: 'Stylized smooth beech',
|
|
36
|
+
shader: Object.freeze({
|
|
37
|
+
bandSoftness: 0.115,
|
|
38
|
+
shadowFloor: 0.54,
|
|
39
|
+
skyFillStrength: 0.07,
|
|
40
|
+
}),
|
|
41
|
+
textureVersion: TREE_SURFACE_TEXTURE_VERSION,
|
|
42
|
+
uvRepeat: Object.freeze([1.25, 1]),
|
|
43
|
+
}),
|
|
44
|
+
// Papery pale bark with dark horizontal lenticel dashes and the occasional
|
|
45
|
+
// shed-limb patch. The lenticels are the entire silhouette read, so they
|
|
46
|
+
// stay high-contrast and the ground stays near-white.
|
|
47
|
+
'birch-papery-v1': Object.freeze({
|
|
48
|
+
id: 'birch-papery-v1',
|
|
49
|
+
label: 'Stylized papery birch',
|
|
50
|
+
shader: Object.freeze({
|
|
51
|
+
bandSoftness: 0.13,
|
|
52
|
+
shadowFloor: 0.6,
|
|
53
|
+
skyFillStrength: 0.085,
|
|
54
|
+
}),
|
|
55
|
+
textureVersion: TREE_SURFACE_TEXTURE_VERSION,
|
|
56
|
+
uvRepeat: Object.freeze([1.25, 1]),
|
|
57
|
+
}),
|
|
58
|
+
// Acer palmatum. The read is a fine-grained, cool grey-brown cylinder
|
|
59
|
+
// carrying close vertical striae and scattered pale lenticels, with only a
|
|
60
|
+
// shallow fissure where an old multi-stem trunk has started to split. It is
|
|
61
|
+
// neither smooth like beech nor plated like oak, and a re-tinted oak reads
|
|
62
|
+
// far too coarse on a 4 m tree standing 3 m from the lens.
|
|
63
|
+
'maple-striated-v1': Object.freeze({
|
|
64
|
+
id: 'maple-striated-v1',
|
|
65
|
+
label: 'Stylized striated Japanese maple',
|
|
66
|
+
shader: Object.freeze({
|
|
67
|
+
bandSoftness: 0.1,
|
|
68
|
+
shadowFloor: 0.5,
|
|
69
|
+
skyFillStrength: 0.065,
|
|
70
|
+
}),
|
|
71
|
+
textureVersion: TREE_SURFACE_TEXTURE_VERSION,
|
|
72
|
+
uvRepeat: Object.freeze([1.35, 1]),
|
|
73
|
+
}),
|
|
74
|
+
// Pinus thunbergii. Black pine bark is the opposite of a ridge-and-furrow
|
|
75
|
+
// profile: thick irregular PLATES, blocky in both axes, separated by deep
|
|
76
|
+
// near-black fissures, with warm ochre showing in the splits. The plate
|
|
77
|
+
// boundaries are cellular, not sinusoidal, which is why this needs its own
|
|
78
|
+
// generator rather than a darker oak — a warped sine gives long vertical
|
|
79
|
+
// ridges and reads as elm at any distance the garden camera works at.
|
|
80
|
+
'pine-plated-v1': Object.freeze({
|
|
81
|
+
id: 'pine-plated-v1',
|
|
82
|
+
label: 'Stylized plated Japanese black pine',
|
|
83
|
+
shader: Object.freeze({
|
|
84
|
+
bandSoftness: 0.065,
|
|
85
|
+
// Higher than a dark bark would suggest, on purpose. A cloud-pruned
|
|
86
|
+
// pine's bare limbs cross its own crown in full view, and they are
|
|
87
|
+
// thin: at the default floor a dark plated bark on a 3 cm twig turned
|
|
88
|
+
// shadow-side goes to near-black and reads as a crack scribbled across
|
|
89
|
+
// the foliage rather than as wood.
|
|
90
|
+
shadowFloor: 0.6,
|
|
91
|
+
skyFillStrength: 0.1,
|
|
92
|
+
}),
|
|
93
|
+
textureVersion: TREE_SURFACE_TEXTURE_VERSION,
|
|
94
|
+
uvRepeat: Object.freeze([1.6, 1.15]),
|
|
95
|
+
}),
|
|
28
96
|
'bamboo-waxy-v1': Object.freeze({
|
|
29
97
|
id: 'bamboo-waxy-v1',
|
|
30
98
|
label: 'Stylized waxy bamboo culm',
|
|
@@ -64,6 +132,45 @@ export const TREE_SURFACE_PROFILE_DEFAULTS = Object.freeze({
|
|
|
64
132
|
call_me_sensei: 'call-me-sensei-bark-v1',
|
|
65
133
|
});
|
|
66
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Short bark names an author actually reaches for, mapped onto profile ids.
|
|
137
|
+
*
|
|
138
|
+
* Tree Lab has always spoken in species words ('beech', 'birch', 'oak'), and
|
|
139
|
+
* recipes get authored in the same vocabulary. Without this table those names
|
|
140
|
+
* resolve to nothing and the trunk silently ships bare, so the alias set is
|
|
141
|
+
* part of the contract rather than a convenience.
|
|
142
|
+
*/
|
|
143
|
+
export const TREE_SURFACE_PROFILE_ALIASES = Object.freeze({
|
|
144
|
+
beech: 'beech-smooth-v1',
|
|
145
|
+
birch: 'birch-papery-v1',
|
|
146
|
+
oak: 'oak-fissured-v1',
|
|
147
|
+
maple: 'maple-striated-v1',
|
|
148
|
+
acer: 'maple-striated-v1',
|
|
149
|
+
'japanese-maple': 'maple-striated-v1',
|
|
150
|
+
pine: 'pine-plated-v1',
|
|
151
|
+
'black-pine': 'pine-plated-v1',
|
|
152
|
+
'japanese-black-pine': 'pine-plated-v1',
|
|
153
|
+
bamboo: 'bamboo-waxy-v1',
|
|
154
|
+
yucca: 'yucca-fibrous-v1',
|
|
155
|
+
saguaro: 'saguaro-waxy-v1',
|
|
156
|
+
classic: 'call-me-sensei-bark-v1',
|
|
157
|
+
call_me_sensei: 'call-me-sensei-bark-v1',
|
|
158
|
+
'call-me-sensei': 'call-me-sensei-bark-v1',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a bark reference — a profile id or a short alias — to a profile id.
|
|
163
|
+
* Returns null for anything unregistered so callers can fall back to their own
|
|
164
|
+
* default instead of throwing on a caller-owned asset string.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveTreeSurfaceProfileId(reference) {
|
|
167
|
+
if (typeof reference !== 'string') return null;
|
|
168
|
+
const key = reference.trim();
|
|
169
|
+
if (!key) return null;
|
|
170
|
+
if (TREE_SURFACE_PROFILES[key]) return key;
|
|
171
|
+
return TREE_SURFACE_PROFILE_ALIASES[key.toLowerCase()] ?? null;
|
|
172
|
+
}
|
|
173
|
+
|
|
67
174
|
export function getTreeSurfaceProfileOptions() {
|
|
68
175
|
return Object.values(TREE_SURFACE_PROFILES).map(({ id, label }) => ({
|
|
69
176
|
id,
|
|
@@ -122,6 +229,45 @@ function quantize(value, steps) {
|
|
|
122
229
|
/ Math.max(steps - 1, 1);
|
|
123
230
|
}
|
|
124
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Tiling cellular (Worley) noise on a jittered lattice.
|
|
234
|
+
*
|
|
235
|
+
* Returns the nearest and second-nearest feature distances plus a stable
|
|
236
|
+
* per-cell id. Plated bark — pine, and the plated end of the oak family — is
|
|
237
|
+
* a *cellular* pattern: irregular blocks bounded on all sides, each block
|
|
238
|
+
* weathered to its own tone. Warped sine bands cannot express that; they only
|
|
239
|
+
* ever produce long ridges. Periodic in both axes so the trunk tile still
|
|
240
|
+
* wraps, which the sine-based generators above already rely on.
|
|
241
|
+
*/
|
|
242
|
+
function periodicCellular(x, y, periodX, periodY, seed) {
|
|
243
|
+
const xi = Math.floor(x);
|
|
244
|
+
const yi = Math.floor(y);
|
|
245
|
+
let best = Infinity;
|
|
246
|
+
let second = Infinity;
|
|
247
|
+
let bestId = 0;
|
|
248
|
+
for (let dy = -1; dy <= 1; dy += 1) {
|
|
249
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
250
|
+
const cx = xi + dx;
|
|
251
|
+
const cy = yi + dy;
|
|
252
|
+
const wx = ((cx % periodX) + periodX) % periodX;
|
|
253
|
+
const wy = ((cy % periodY) + periodY) % periodY;
|
|
254
|
+
const jx = hash2d(wx, wy, seed);
|
|
255
|
+
const jy = hash2d(wx, wy, seed + 911);
|
|
256
|
+
const fx = cx + 0.15 + jx * 0.7;
|
|
257
|
+
const fy = cy + 0.15 + jy * 0.7;
|
|
258
|
+
const distance = Math.hypot(x - fx, y - fy);
|
|
259
|
+
if (distance < best) {
|
|
260
|
+
second = best;
|
|
261
|
+
best = distance;
|
|
262
|
+
bestId = hash2d(wx, wy, seed + 1733);
|
|
263
|
+
} else if (distance < second) {
|
|
264
|
+
second = distance;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { edge: second - best, first: best, id: bestId };
|
|
269
|
+
}
|
|
270
|
+
|
|
125
271
|
function oakFissuredPixel(u, v, seed) {
|
|
126
272
|
// Large vertical plates and a small number of cross-breaks carry the
|
|
127
273
|
// silhouette-scale bark read. Fine photographic pores are intentionally
|
|
@@ -155,6 +301,184 @@ function oakFissuredPixel(u, v, seed) {
|
|
|
155
301
|
];
|
|
156
302
|
}
|
|
157
303
|
|
|
304
|
+
function beechSmoothPixel(u, v, seed) {
|
|
305
|
+
// Fagus bark is smooth: no fissures, no plates, no cross-breaks. What reads
|
|
306
|
+
// at silhouette scale is soft overlapping mottle patches on a pale grey
|
|
307
|
+
// ground, a faint vertical grain from the trunk's own taper, and sparse
|
|
308
|
+
// darker healed scars. Quantized like the other profiles so it stays inside
|
|
309
|
+
// ToonLab's broad stylized light bands.
|
|
310
|
+
const broad = periodicValueNoise(u * 3, v * 4, 3, 4, seed + 229);
|
|
311
|
+
const mottle = periodicValueNoise(u * 6, v * 5, 6, 5, seed + 233);
|
|
312
|
+
const fine = periodicValueNoise(u * 13, v * 9, 13, 9, seed + 239);
|
|
313
|
+
|
|
314
|
+
// Faint vertical shading bands — a smooth cylinder, not a carved one.
|
|
315
|
+
const grainWarp = (broad - 0.5) * 0.22;
|
|
316
|
+
const grain = (Math.sin((u * 4 + grainWarp) * Math.PI * 2) * 0.5 + 0.5) * 0.06;
|
|
317
|
+
|
|
318
|
+
// Sparse healed scars: small, soft, and far apart. They are the only dark
|
|
319
|
+
// marks on a smooth trunk, so they are deliberately rare.
|
|
320
|
+
const scarNoise = periodicValueNoise(u * 9, v * 11, 9, 11, seed + 241);
|
|
321
|
+
const scar = THREE.MathUtils.smoothstep(scarNoise, 0.87, 0.98) * 0.16;
|
|
322
|
+
|
|
323
|
+
const tone = quantize(0.46 + broad * 0.28 + mottle * 0.18 + fine * 0.06, 5);
|
|
324
|
+
const shade = THREE.MathUtils.clamp(0.74 + tone * 0.34 + grain - scar, 0.62, 1.04);
|
|
325
|
+
// Pale grey-green ground: green and blue close together, red held under both.
|
|
326
|
+
const coolLift = quantize(periodicValueNoise(u * 2, v * 3, 2, 3, seed + 251), 4);
|
|
327
|
+
return [
|
|
328
|
+
(0.62 + coolLift * 0.035) * shade,
|
|
329
|
+
(0.63 + coolLift * 0.03) * shade,
|
|
330
|
+
(0.58 + coolLift * 0.04) * shade,
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function birchPaperyPixel(u, v, seed) {
|
|
335
|
+
// Betula reads as a near-white papery ground carrying dark horizontal
|
|
336
|
+
// lenticel dashes. The dashes are the whole identity, so they keep hard
|
|
337
|
+
// edges and high contrast while the ground stays almost flat.
|
|
338
|
+
const broad = periodicValueNoise(u * 4, v * 3, 4, 3, seed + 257);
|
|
339
|
+
const paper = periodicValueNoise(u * 11, v * 7, 11, 7, seed + 263);
|
|
340
|
+
|
|
341
|
+
// Lenticels: short horizontal strokes, banded in v, broken up in u so they
|
|
342
|
+
// do not run all the way around the trunk.
|
|
343
|
+
const bandNoise = periodicValueNoise(u * 5, v * 13, 5, 13, seed + 269);
|
|
344
|
+
const bandPhase = Math.abs(
|
|
345
|
+
((v * 14 + (bandNoise - 0.5) * 0.5) % 1 + 1) % 1 - 0.5,
|
|
346
|
+
) * 2;
|
|
347
|
+
const bandMask = 1 - THREE.MathUtils.smoothstep(bandPhase, 0.05, 0.16);
|
|
348
|
+
const breakNoise = periodicValueNoise(u * 17, v * 13, 17, 13, seed + 271);
|
|
349
|
+
const lenticel = bandMask * THREE.MathUtils.smoothstep(breakNoise, 0.36, 0.6);
|
|
350
|
+
|
|
351
|
+
// Occasional large dark patch where a limb was shed.
|
|
352
|
+
const shedNoise = periodicValueNoise(u * 3, v * 5, 3, 5, seed + 277);
|
|
353
|
+
const shed = THREE.MathUtils.smoothstep(shedNoise, 0.84, 0.99);
|
|
354
|
+
|
|
355
|
+
const tone = quantize(0.78 + broad * 0.12 + paper * 0.1, 4);
|
|
356
|
+
const shade = THREE.MathUtils.clamp(
|
|
357
|
+
0.86 + tone * 0.22 - lenticel * 0.52 - shed * 0.3,
|
|
358
|
+
0.3,
|
|
359
|
+
1.05,
|
|
360
|
+
);
|
|
361
|
+
// Warm-white paper; the dashes darken all three channels together so they
|
|
362
|
+
// read as value, not as a hue shift.
|
|
363
|
+
const warmLift = quantize(periodicValueNoise(u * 2, v * 4, 2, 4, seed + 281), 3);
|
|
364
|
+
return [
|
|
365
|
+
(0.89 + warmLift * 0.03) * shade,
|
|
366
|
+
(0.87 + warmLift * 0.025) * shade,
|
|
367
|
+
(0.81 + warmLift * 0.02) * shade,
|
|
368
|
+
];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function mapleStriatedPixel(u, v, seed) {
|
|
372
|
+
// Acer palmatum. Three things carry the read and nothing else should
|
|
373
|
+
// compete with them: close vertical striae (fine pale lines, ~2 mm apart on
|
|
374
|
+
// the real thing), a soft grey-brown mottle underneath, and scattered pale
|
|
375
|
+
// lenticels. A single shallow fissure family is allowed for the old
|
|
376
|
+
// multi-stem trunks the garden uses, but at a fraction of oak's depth —
|
|
377
|
+
// maple is a fine-grained bark and reading it as fissured is the classic
|
|
378
|
+
// way a stylized maple ends up looking like a small oak.
|
|
379
|
+
const broad = periodicValueNoise(u * 3, v * 4, 3, 4, seed + 307);
|
|
380
|
+
const mottle = periodicValueNoise(u * 7, v * 6, 7, 6, seed + 311);
|
|
381
|
+
|
|
382
|
+
// Close vertical striae. Two incommensurate frequencies keep them from
|
|
383
|
+
// reading as a comb; the warp keeps them from reading as a ruled grid.
|
|
384
|
+
const striaWarp = (broad - 0.5) * 0.28 + Math.sin(v * Math.PI * 6) * 0.02;
|
|
385
|
+
const striaA = Math.sin((u * 26 + striaWarp) * Math.PI * 2) * 0.5 + 0.5;
|
|
386
|
+
const striaB = Math.sin((u * 41 - striaWarp * 0.7) * Math.PI * 2) * 0.5 + 0.5;
|
|
387
|
+
const stria = striaA * 0.68 + striaB * 0.32;
|
|
388
|
+
|
|
389
|
+
// Shallow age fissures: sparse, narrow, and much weaker than oak's.
|
|
390
|
+
const fissureWarp = (mottle - 0.5) * 0.4;
|
|
391
|
+
const fissureCoordinate = u * 7 + fissureWarp;
|
|
392
|
+
const fissurePhase = Math.abs(
|
|
393
|
+
(fissureCoordinate - Math.floor(fissureCoordinate)) - 0.5,
|
|
394
|
+
) * 2;
|
|
395
|
+
const fissure = (1 - THREE.MathUtils.smoothstep(fissurePhase, 0.03, 0.1))
|
|
396
|
+
* THREE.MathUtils.smoothstep(broad, 0.52, 0.86);
|
|
397
|
+
|
|
398
|
+
// Lenticels: small pale flecks, brighter than the ground, sparse.
|
|
399
|
+
const lenticelNoise = periodicValueNoise(u * 15, v * 19, 15, 19, seed + 313);
|
|
400
|
+
const lenticel = THREE.MathUtils.smoothstep(lenticelNoise, 0.9, 0.99) * 0.2;
|
|
401
|
+
|
|
402
|
+
const tone = quantize(0.3 + broad * 0.24 + mottle * 0.18 + stria * 0.2, 5);
|
|
403
|
+
const shade = THREE.MathUtils.clamp(
|
|
404
|
+
0.6 + tone * 0.42 + stria * 0.14 + lenticel - fissure * 0.26,
|
|
405
|
+
0.42,
|
|
406
|
+
1.02,
|
|
407
|
+
);
|
|
408
|
+
// Cool grey-brown, and genuinely dark: an Acer palmatum trunk is a middle
|
|
409
|
+
// value, not a pale one. A washed-out ground was the first version's real
|
|
410
|
+
// failure — against the deep-green field of §2 a pale trunk pulls the eye
|
|
411
|
+
// to the wood instead of to the autumn canopy it is supposed to support.
|
|
412
|
+
// Red only a little above green, blue held back but not starved, which is
|
|
413
|
+
// what separates maple from both the warm oak family and the near-neutral
|
|
414
|
+
// beech.
|
|
415
|
+
const warmLift = quantize(periodicValueNoise(u * 2, v * 3, 2, 3, seed + 317), 4);
|
|
416
|
+
return [
|
|
417
|
+
(0.52 + warmLift * 0.05) * shade,
|
|
418
|
+
(0.46 + warmLift * 0.035) * shade,
|
|
419
|
+
(0.42 + warmLift * 0.035) * shade,
|
|
420
|
+
];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function pinePlatedPixel(u, v, seed) {
|
|
424
|
+
// Pinus thunbergii. Thick irregular plates, blocky in BOTH axes, separated
|
|
425
|
+
// by deep near-black fissures with warm ochre showing in the splits. The
|
|
426
|
+
// plates are cellular, so they come from periodicCellular rather than from
|
|
427
|
+
// a warped band; each cell is weathered to its own tone, which is the whole
|
|
428
|
+
// close-camera read. A second, finer cellular pass breaks the large plates
|
|
429
|
+
// into the scaly sub-plates the species is named for.
|
|
430
|
+
const broad = periodicValueNoise(u * 4, v * 5, 4, 5, seed + 331);
|
|
431
|
+
const warpX = (broad - 0.5) * 0.55;
|
|
432
|
+
const warpY = (periodicValueNoise(u * 5, v * 4, 5, 4, seed + 337) - 0.5) * 0.45;
|
|
433
|
+
|
|
434
|
+
// Large plates. The lattice is sampled at 7 x 6 over a tile that is itself
|
|
435
|
+
// 1:2, so a cell spans roughly twice the trunk height it spans in
|
|
436
|
+
// circumference and the plates come out clearly ELONGATED along the trunk.
|
|
437
|
+
// Equal periods gave near-round cells, which read as dried mud rather than
|
|
438
|
+
// as pine, and that was the first version's real failure.
|
|
439
|
+
const plate = periodicCellular(u * 7 + warpX, v * 6 + warpY, 7, 6, seed + 347);
|
|
440
|
+
// Scales inside each plate.
|
|
441
|
+
const scale = periodicCellular(u * 15 + warpX * 0.5, v * 13 + warpY * 0.5, 15, 13, seed + 353);
|
|
442
|
+
|
|
443
|
+
// Fissure = the cell boundary. Deep and hard-edged for the plate network,
|
|
444
|
+
// shallow and soft for the scale network. Narrower than the first version:
|
|
445
|
+
// a black pine's fissures are deep, not wide, and widening them turns the
|
|
446
|
+
// plate network into a grout grid.
|
|
447
|
+
const plateFissure = 1 - THREE.MathUtils.smoothstep(plate.edge, 0.02, 0.1);
|
|
448
|
+
const scaleFissure = (1 - THREE.MathUtils.smoothstep(scale.edge, 0.015, 0.075)) * 0.36;
|
|
449
|
+
const fissure = Math.min(1, plateFissure + scaleFissure * (1 - plateFissure));
|
|
450
|
+
|
|
451
|
+
// Per-plate weathering. Quantized hard so plates group into ToonLab's broad
|
|
452
|
+
// value bands instead of dissolving into per-pixel grain.
|
|
453
|
+
const plateTone = quantize(plate.id, 4);
|
|
454
|
+
const scaleTone = quantize(scale.id, 3);
|
|
455
|
+
const grain = periodicValueNoise(u * 21, v * 27, 21, 27, seed + 359);
|
|
456
|
+
|
|
457
|
+
// The tile is a MULTIPLIER, not a final colour: the woody material takes
|
|
458
|
+
// `map x trunk.color` and then tone-maps it. Authoring black pine at the
|
|
459
|
+
// absolute value real black pine bark reads at put the tile around byte 76
|
|
460
|
+
// — half the value of every other profile in this file — and the trunk came
|
|
461
|
+
// out a featureless black cylinder in which none of the plate work was
|
|
462
|
+
// visible at 85 mm. The plate-to-fissure CONTRAST is what carries the
|
|
463
|
+
// species; the overall level has to sit with its neighbours.
|
|
464
|
+
const shade = THREE.MathUtils.clamp(
|
|
465
|
+
0.72 + plateTone * 0.34 + scaleTone * 0.1 + grain * 0.07 - fissure * 0.46,
|
|
466
|
+
0.32,
|
|
467
|
+
1.12,
|
|
468
|
+
);
|
|
469
|
+
// Dark grey-charcoal plate faces; the fissures let a little warm brown
|
|
470
|
+
// through, so red is lifted exactly where the value drops. Without any
|
|
471
|
+
// warmth the trunk reads as wet slate — but the first version overdid it
|
|
472
|
+
// and the fissure network glowed orange like fired brick, so the lift is
|
|
473
|
+
// now a fraction of what it was and stays under the value drop.
|
|
474
|
+
const ochre = fissure * (0.35 + plateTone * 0.4);
|
|
475
|
+
return [
|
|
476
|
+
(0.45 + ochre * 0.17) * shade,
|
|
477
|
+
(0.44 + ochre * 0.08) * shade,
|
|
478
|
+
(0.43 + ochre * 0.02) * shade,
|
|
479
|
+
];
|
|
480
|
+
}
|
|
481
|
+
|
|
158
482
|
function bambooWaxyPixel(u, v, seed) {
|
|
159
483
|
// Moso culms read as smooth gray-green cylinders with restrained vertical
|
|
160
484
|
// striation and irregular wax bloom. Nodes remain semantic ring geometry;
|
|
@@ -244,6 +568,10 @@ function surfacePixel(profileId, u, v, seed) {
|
|
|
244
568
|
if (profileId === 'call-me-sensei-bark-v1' || profileId === 'oak-fissured-v1') {
|
|
245
569
|
return oakFissuredPixel(u, v, seed);
|
|
246
570
|
}
|
|
571
|
+
if (profileId === 'beech-smooth-v1') return beechSmoothPixel(u, v, seed);
|
|
572
|
+
if (profileId === 'birch-papery-v1') return birchPaperyPixel(u, v, seed);
|
|
573
|
+
if (profileId === 'maple-striated-v1') return mapleStriatedPixel(u, v, seed);
|
|
574
|
+
if (profileId === 'pine-plated-v1') return pinePlatedPixel(u, v, seed);
|
|
247
575
|
if (profileId === 'bamboo-waxy-v1') return bambooWaxyPixel(u, v, seed);
|
|
248
576
|
if (profileId === 'yucca-fibrous-v1') return yuccaFibrousPixel(u, v, seed);
|
|
249
577
|
if (profileId === 'saguaro-waxy-v1') return saguaroWaxyPixel(u, v, seed);
|
package/src/version.js
CHANGED