@call-me-sensei/toonlab 0.2.0 → 0.3.0
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/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// Ribbon geometry for path routes: a skirted strip that follows the route's
|
|
2
|
+
// flattened height profile, painted per style (dirt / stone / planks) with
|
|
3
|
+
// vertex colors + a procedural detail map. The terrain mesh is never
|
|
4
|
+
// modified — the ribbon is a true overlay (lift + edge skirts tucked under
|
|
5
|
+
// the terrain), and the flattened profile is what `paths.heightAt` reports.
|
|
6
|
+
|
|
7
|
+
import * as THREE from 'three';
|
|
8
|
+
|
|
9
|
+
import { hashCombine } from '../rockgen/noise/prng.js';
|
|
10
|
+
import { valueNoise3 } from '../rockgen/noise/valueNoise3.js';
|
|
11
|
+
|
|
12
|
+
// Values are linear-space (THREE vertex colors), same convention as the
|
|
13
|
+
// debris palettes — earthy and saturated enough to survive the environment
|
|
14
|
+
// shader's ambient lift without washing to pink.
|
|
15
|
+
export const PATH_STYLE_PALETTES = Object.freeze({
|
|
16
|
+
dirt: Object.freeze({
|
|
17
|
+
base: Object.freeze([0.3, 0.21, 0.12]),
|
|
18
|
+
edge: Object.freeze([0.24, 0.18, 0.11]),
|
|
19
|
+
wear: Object.freeze([0.42, 0.31, 0.18]),
|
|
20
|
+
}),
|
|
21
|
+
stone: Object.freeze({
|
|
22
|
+
base: Object.freeze([0.4, 0.41, 0.39]),
|
|
23
|
+
edge: Object.freeze([0.3, 0.31, 0.3]),
|
|
24
|
+
wear: Object.freeze([0.52, 0.51, 0.47]),
|
|
25
|
+
}),
|
|
26
|
+
planks: Object.freeze({
|
|
27
|
+
base: Object.freeze([0.38, 0.26, 0.15]),
|
|
28
|
+
edge: Object.freeze([0.3, 0.2, 0.12]),
|
|
29
|
+
wear: Object.freeze([0.5, 0.36, 0.21]),
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Even resampling of a 2D polyline at `stepLength` meters (arclength). */
|
|
34
|
+
export function resamplePolyline(points, stepLength = 2) {
|
|
35
|
+
if (!Array.isArray(points) || points.length < 2) return [];
|
|
36
|
+
const step = Math.max(Number(stepLength) || 2, 0.5);
|
|
37
|
+
const samples = [{ s: 0, x: points[0].x, z: points[0].z }];
|
|
38
|
+
let total = 0; // arclength at the start of the current segment
|
|
39
|
+
let nextS = step; // arclength of the next sample to emit
|
|
40
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
41
|
+
const a = points[index];
|
|
42
|
+
const b = points[index + 1];
|
|
43
|
+
const span = Math.hypot(b.x - a.x, b.z - a.z);
|
|
44
|
+
if (span < 1e-6) continue;
|
|
45
|
+
while (nextS <= total + span) {
|
|
46
|
+
const t = (nextS - total) / span;
|
|
47
|
+
samples.push({ s: nextS, x: a.x + (b.x - a.x) * t, z: a.z + (b.z - a.z) * t });
|
|
48
|
+
nextS += step;
|
|
49
|
+
}
|
|
50
|
+
total += span;
|
|
51
|
+
}
|
|
52
|
+
const last = points[points.length - 1];
|
|
53
|
+
const tail = samples[samples.length - 1];
|
|
54
|
+
const closing = Math.hypot(last.x - tail.x, last.z - tail.z);
|
|
55
|
+
if (closing > step * 0.35) {
|
|
56
|
+
samples.push({ s: total, x: last.x, z: last.z });
|
|
57
|
+
} else if (samples.length > 1) {
|
|
58
|
+
tail.x = last.x;
|
|
59
|
+
tail.z = last.z;
|
|
60
|
+
tail.s = total;
|
|
61
|
+
}
|
|
62
|
+
return samples;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Builds per-sample route data: raw terrain height, flattened profile,
|
|
67
|
+
* bridge spans (open-water runs longer than `minSpan`), ford flags, and
|
|
68
|
+
* stair runs (steep dry stretches). This is the single source of truth the
|
|
69
|
+
* ribbon mesh, the bridges, `paths.heightAt`, and `paths.maskAt` all share.
|
|
70
|
+
*/
|
|
71
|
+
export function buildRouteProfile({
|
|
72
|
+
points,
|
|
73
|
+
heightAt,
|
|
74
|
+
waterLevel = 0,
|
|
75
|
+
settings,
|
|
76
|
+
seed = 1,
|
|
77
|
+
}) {
|
|
78
|
+
const sample = typeof heightAt === 'function' ? heightAt : () => 0;
|
|
79
|
+
const { ribbon, bridge, stairs } = settings;
|
|
80
|
+
const samples = resamplePolyline(points, ribbon.stepLength);
|
|
81
|
+
if (samples.length < 2) return null;
|
|
82
|
+
|
|
83
|
+
for (const point of samples) {
|
|
84
|
+
point.raw = Number(sample(point.x, point.z)) || 0;
|
|
85
|
+
point.wet = point.raw <= waterLevel + 0.05;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Flattened profile: moving average of raw height, deviation-clamped so
|
|
89
|
+
// the path can fill dips (embankment) but never cuts deep into a rise.
|
|
90
|
+
const window = Math.max(Math.round((ribbon.smoothing || 0) / Math.max(ribbon.stepLength, 0.5)), 0);
|
|
91
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
92
|
+
if (window === 0) { samples[index].profile = samples[index].raw; continue; }
|
|
93
|
+
let sum = 0;
|
|
94
|
+
let count = 0;
|
|
95
|
+
for (let k = -window; k <= window; k += 1) {
|
|
96
|
+
const at = Math.min(samples.length - 1, Math.max(0, index + k));
|
|
97
|
+
sum += samples[at].raw;
|
|
98
|
+
count += 1;
|
|
99
|
+
}
|
|
100
|
+
const smoothed = sum / count;
|
|
101
|
+
const deviation = Math.min(Math.max(smoothed - samples[index].raw, -0.45), 1.4);
|
|
102
|
+
samples[index].profile = samples[index].raw + deviation;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Open-water runs → bridge spans (long) or fords (short). Bridges get an
|
|
106
|
+
// arched deck profile clamped above the waterline; fords wade the profile
|
|
107
|
+
// just above it.
|
|
108
|
+
const bridges = [];
|
|
109
|
+
let runStart = -1;
|
|
110
|
+
const flushRun = (endIndex) => {
|
|
111
|
+
if (runStart < 0) return;
|
|
112
|
+
const startIndex = Math.max(runStart - 1, 0);
|
|
113
|
+
const stopIndex = Math.min(endIndex, samples.length - 1);
|
|
114
|
+
const span = samples[stopIndex].s - samples[startIndex].s;
|
|
115
|
+
if (span >= bridge.minSpan) {
|
|
116
|
+
bridges.push({ endIndex: stopIndex, span, startIndex });
|
|
117
|
+
} else {
|
|
118
|
+
for (let index = startIndex; index <= stopIndex; index += 1) {
|
|
119
|
+
samples[index].profile = Math.max(samples[index].profile, waterLevel + 0.14);
|
|
120
|
+
samples[index].ford = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
runStart = -1;
|
|
124
|
+
};
|
|
125
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
126
|
+
if (samples[index].wet) {
|
|
127
|
+
if (runStart < 0) runStart = index;
|
|
128
|
+
} else {
|
|
129
|
+
flushRun(index);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
flushRun(samples.length - 1);
|
|
133
|
+
|
|
134
|
+
for (const crossing of bridges) {
|
|
135
|
+
const { startIndex, endIndex } = crossing;
|
|
136
|
+
const startY = samples[startIndex].profile;
|
|
137
|
+
const endY = samples[endIndex].profile;
|
|
138
|
+
const span = crossing.span;
|
|
139
|
+
const rise = Math.min(span * bridge.arc, 2.6);
|
|
140
|
+
const clearance = waterLevel + bridge.deckClearance;
|
|
141
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
142
|
+
const t = (samples[index].s - samples[startIndex].s) / Math.max(span, 1e-6);
|
|
143
|
+
const base = startY + (endY - startY) * t;
|
|
144
|
+
const arch = base + Math.sin(t * Math.PI) * rise;
|
|
145
|
+
samples[index].profile = index === startIndex || index === endIndex
|
|
146
|
+
? base
|
|
147
|
+
: Math.max(arch, clearance);
|
|
148
|
+
// Interior samples are deck-only; the bank samples stay in the ribbon
|
|
149
|
+
// so the dirt strip runs under the deck ends (no seam gap).
|
|
150
|
+
if (index > startIndex && index < endIndex) samples[index].bridge = true;
|
|
151
|
+
}
|
|
152
|
+
crossing.deckHeights = samples
|
|
153
|
+
.slice(startIndex, endIndex + 1)
|
|
154
|
+
.map((point) => point.profile);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Stair runs: steep, dry, non-bridge stretches. Visual only — the profile
|
|
158
|
+
// stays a smooth ramp so character controllers need no step logic.
|
|
159
|
+
const stairRuns = [];
|
|
160
|
+
let stairStart = -1;
|
|
161
|
+
for (let index = 0; index < samples.length - 1; index += 1) {
|
|
162
|
+
const a = samples[index];
|
|
163
|
+
const b = samples[index + 1];
|
|
164
|
+
const slope = Math.abs(b.profile - a.profile) / Math.max(b.s - a.s, 1e-6);
|
|
165
|
+
const steep = slope >= stairs.slopeThreshold && !a.bridge && !b.bridge && !a.ford;
|
|
166
|
+
if (steep && stairStart < 0) stairStart = index;
|
|
167
|
+
if (!steep && stairStart >= 0) {
|
|
168
|
+
if (index - stairStart >= 1) stairRuns.push({ endIndex: index, startIndex: stairStart });
|
|
169
|
+
stairStart = -1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (stairStart >= 0 && samples.length - 1 - stairStart >= 1) {
|
|
173
|
+
stairRuns.push({ endIndex: samples.length - 1, startIndex: stairStart });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Per-sample half-width with low-frequency wobble (hand-drawn look) and
|
|
177
|
+
// the side direction (perpendicular of the local tangent).
|
|
178
|
+
const halfBase = ribbon.width / 2;
|
|
179
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
180
|
+
const prev = samples[Math.max(index - 1, 0)];
|
|
181
|
+
const next = samples[Math.min(index + 1, samples.length - 1)];
|
|
182
|
+
const tx = next.x - prev.x;
|
|
183
|
+
const tz = next.z - prev.z;
|
|
184
|
+
const invLen = 1 / Math.max(Math.hypot(tx, tz), 1e-6);
|
|
185
|
+
samples[index].sideX = -tz * invLen;
|
|
186
|
+
samples[index].sideZ = tx * invLen;
|
|
187
|
+
const wobble = valueNoise3(hashCombine(seed, 811), samples[index].s * 0.045, 0.5, 0.5);
|
|
188
|
+
samples[index].half = samples[index].bridge
|
|
189
|
+
? halfBase
|
|
190
|
+
: halfBase * (1 + ribbon.widthWobble * wobble);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { bridges, samples, stairRuns };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const scratchColor = new THREE.Color();
|
|
197
|
+
|
|
198
|
+
function pushColor(colors, rgb, shade) {
|
|
199
|
+
scratchColor.setRGB(rgb[0], rgb[1], rgb[2]);
|
|
200
|
+
colors.push(
|
|
201
|
+
Math.min(scratchColor.r * shade, 1),
|
|
202
|
+
Math.min(scratchColor.g * shade, 1),
|
|
203
|
+
Math.min(scratchColor.b * shade, 1),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Ribbon mesh for one route (bridge spans skipped — those become bridge
|
|
209
|
+
* groups). Cross-section: skirt / edge / crown / edge / skirt; skirts drop
|
|
210
|
+
* below the neighbouring terrain so the visible edge is the organic
|
|
211
|
+
* intersection line, never a floating hem.
|
|
212
|
+
*/
|
|
213
|
+
export function buildRibbonGeometry(routeData, {
|
|
214
|
+
heightAt,
|
|
215
|
+
settings,
|
|
216
|
+
style = 'dirt',
|
|
217
|
+
seed = 1,
|
|
218
|
+
}) {
|
|
219
|
+
const sample = typeof heightAt === 'function' ? heightAt : () => 0;
|
|
220
|
+
const { ribbon } = settings;
|
|
221
|
+
const palette = PATH_STYLE_PALETTES[style] ?? PATH_STYLE_PALETTES.dirt;
|
|
222
|
+
const positions = [];
|
|
223
|
+
const uvs = [];
|
|
224
|
+
const colors = [];
|
|
225
|
+
const indices = [];
|
|
226
|
+
const skirtDrop = 0.4;
|
|
227
|
+
const crown = Math.min(ribbon.width * 0.02, 0.05);
|
|
228
|
+
const vRepeat = 1 / Math.max(ribbon.width, 1); // ~square texture tiles
|
|
229
|
+
|
|
230
|
+
const { samples } = routeData;
|
|
231
|
+
let rowIndex = -1;
|
|
232
|
+
let previousRow = null;
|
|
233
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
234
|
+
const point = samples[index];
|
|
235
|
+
if (point.bridge) { previousRow = null; continue; }
|
|
236
|
+
const lift = ribbon.lift;
|
|
237
|
+
const y = point.profile + lift;
|
|
238
|
+
const half = point.half;
|
|
239
|
+
const skirt = half + ribbon.edgeSkirt;
|
|
240
|
+
const wear = valueNoise3(hashCombine(seed, 977), point.s * 0.11, 0.25, 0.75);
|
|
241
|
+
|
|
242
|
+
const across = [
|
|
243
|
+
{ off: -skirt, u: 0, kind: 'skirt' },
|
|
244
|
+
{ off: -half, u: 0.04, kind: 'edge' },
|
|
245
|
+
{ off: 0, u: 0.5, kind: 'center' },
|
|
246
|
+
{ off: half, u: 0.96, kind: 'edge' },
|
|
247
|
+
{ off: skirt, u: 1, kind: 'skirt' },
|
|
248
|
+
];
|
|
249
|
+
for (const slot of across) {
|
|
250
|
+
const x = point.x + point.sideX * slot.off;
|
|
251
|
+
const z = point.z + point.sideZ * slot.off;
|
|
252
|
+
let vy;
|
|
253
|
+
if (slot.kind === 'skirt') {
|
|
254
|
+
vy = Math.min((Number(sample(x, z)) || 0) - skirtDrop, y - 0.05);
|
|
255
|
+
} else if (slot.kind === 'center') {
|
|
256
|
+
vy = y + crown;
|
|
257
|
+
} else {
|
|
258
|
+
vy = y;
|
|
259
|
+
}
|
|
260
|
+
positions.push(x, vy, z);
|
|
261
|
+
uvs.push(slot.u, point.s * vRepeat);
|
|
262
|
+
if (slot.kind === 'skirt') {
|
|
263
|
+
pushColor(colors, palette.edge, 0.72);
|
|
264
|
+
} else if (slot.kind === 'edge') {
|
|
265
|
+
pushColor(colors, palette.edge, 0.94 + wear * 0.05);
|
|
266
|
+
} else {
|
|
267
|
+
const worn = wear * 0.5 + 0.5;
|
|
268
|
+
const rgb = [
|
|
269
|
+
palette.base[0] + (palette.wear[0] - palette.base[0]) * worn,
|
|
270
|
+
palette.base[1] + (palette.wear[1] - palette.base[1]) * worn,
|
|
271
|
+
palette.base[2] + (palette.wear[2] - palette.base[2]) * worn,
|
|
272
|
+
];
|
|
273
|
+
pushColor(colors, rgb, 1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
rowIndex += 1;
|
|
277
|
+
if (previousRow !== null) {
|
|
278
|
+
const a = previousRow * 5;
|
|
279
|
+
const b = rowIndex * 5;
|
|
280
|
+
for (let k = 0; k < 4; k += 1) {
|
|
281
|
+
// Wound so face normals point up (+y): row b is ahead of row a
|
|
282
|
+
// along the route, slots run left→right across it.
|
|
283
|
+
indices.push(a + k, b + k + 1, b + k, a + k, a + k + 1, b + k + 1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
previousRow = rowIndex;
|
|
287
|
+
}
|
|
288
|
+
if (positions.length === 0) return null;
|
|
289
|
+
|
|
290
|
+
const geometry = new THREE.BufferGeometry();
|
|
291
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
292
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
293
|
+
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
|
294
|
+
geometry.setIndex(indices);
|
|
295
|
+
geometry.computeVertexNormals();
|
|
296
|
+
return geometry;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Stepped stone segments over steep runs. Boxes sunk into the ramp — the
|
|
301
|
+
* read is stairs, the walk is the smooth profile underneath.
|
|
302
|
+
*/
|
|
303
|
+
export function buildStairsGeometry(routeData, { settings, seed = 1, maxSteps = 400 }) {
|
|
304
|
+
const { stairRuns, samples } = routeData;
|
|
305
|
+
if (!stairRuns.length) return null;
|
|
306
|
+
const { stairs } = settings;
|
|
307
|
+
let stepsLeft = Math.max(Number(maxSteps) || 400, 0); // triangle-budget guard
|
|
308
|
+
const palette = PATH_STYLE_PALETTES.stone;
|
|
309
|
+
const geometries = [];
|
|
310
|
+
const box = new THREE.BoxGeometry(1, 1, 1);
|
|
311
|
+
const matrix = new THREE.Matrix4();
|
|
312
|
+
const quaternion = new THREE.Quaternion();
|
|
313
|
+
const up = new THREE.Vector3(0, 1, 0);
|
|
314
|
+
const random = (value) => valueNoise3(hashCombine(seed, 601), value * 3.7, 0.2, 0.8);
|
|
315
|
+
|
|
316
|
+
for (const run of stairRuns) {
|
|
317
|
+
if (stepsLeft <= 0) break;
|
|
318
|
+
const start = samples[run.startIndex];
|
|
319
|
+
const end = samples[run.endIndex];
|
|
320
|
+
const rise = end.profile - start.profile;
|
|
321
|
+
const stepCount = Math.min(
|
|
322
|
+
Math.max(Math.ceil(Math.abs(rise) / stairs.stepHeight), 2),
|
|
323
|
+
stepsLeft,
|
|
324
|
+
);
|
|
325
|
+
stepsLeft -= stepCount;
|
|
326
|
+
for (let stepIndex = 0; stepIndex < stepCount; stepIndex += 1) {
|
|
327
|
+
const t = (stepIndex + 0.5) / stepCount;
|
|
328
|
+
const s = start.s + (end.s - start.s) * t;
|
|
329
|
+
// find bracketing samples for position/heading
|
|
330
|
+
let at = run.startIndex;
|
|
331
|
+
while (at < run.endIndex && samples[at + 1].s < s) at += 1;
|
|
332
|
+
const a = samples[at];
|
|
333
|
+
const b = samples[Math.min(at + 1, run.endIndex)];
|
|
334
|
+
const spanT = (s - a.s) / Math.max(b.s - a.s, 1e-6);
|
|
335
|
+
const x = a.x + (b.x - a.x) * spanT;
|
|
336
|
+
const z = a.z + (b.z - a.z) * spanT;
|
|
337
|
+
const profile = a.profile + (b.profile - a.profile) * spanT;
|
|
338
|
+
const heading = Math.atan2(b.x - a.x, b.z - a.z);
|
|
339
|
+
const width = (a.half + b.half) * 0.92;
|
|
340
|
+
const depth = (end.s - start.s) / stepCount + 0.14;
|
|
341
|
+
const height = stairs.stepHeight + 0.1;
|
|
342
|
+
const jitter = random(stepIndex + run.startIndex);
|
|
343
|
+
|
|
344
|
+
const geometry = box.clone();
|
|
345
|
+
quaternion.setFromAxisAngle(up, heading + jitter * 0.06);
|
|
346
|
+
matrix.compose(
|
|
347
|
+
new THREE.Vector3(x, profile + stairs.stepHeight * 0.28, z),
|
|
348
|
+
quaternion,
|
|
349
|
+
new THREE.Vector3(width, height, depth),
|
|
350
|
+
);
|
|
351
|
+
geometry.applyMatrix4(matrix);
|
|
352
|
+
const count = geometry.attributes.position.count;
|
|
353
|
+
const stepColors = new Float32Array(count * 3);
|
|
354
|
+
const shade = 0.9 + jitter * 0.12;
|
|
355
|
+
for (let v = 0; v < count; v += 1) {
|
|
356
|
+
stepColors[v * 3] = Math.min(palette.base[0] * shade, 1);
|
|
357
|
+
stepColors[v * 3 + 1] = Math.min(palette.base[1] * shade, 1);
|
|
358
|
+
stepColors[v * 3 + 2] = Math.min(palette.base[2] * shade, 1);
|
|
359
|
+
}
|
|
360
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(stepColors, 3));
|
|
361
|
+
geometries.push(geometry);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
box.dispose();
|
|
365
|
+
if (!geometries.length) return null;
|
|
366
|
+
const merged = mergePathGeometries(geometries);
|
|
367
|
+
for (const geometry of geometries) geometry.dispose();
|
|
368
|
+
return merged;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Minimal local merge (positions/normals/uvs/colors + index) — avoids the
|
|
372
|
+
// three/examples import here so the module stays lean for tests.
|
|
373
|
+
export function mergePathGeometries(geometries) {
|
|
374
|
+
const hasUv = geometries.every((geometry) => geometry.attributes.uv);
|
|
375
|
+
const positions = [];
|
|
376
|
+
const normals = [];
|
|
377
|
+
const uvs = [];
|
|
378
|
+
const colors = [];
|
|
379
|
+
const indices = [];
|
|
380
|
+
let offset = 0;
|
|
381
|
+
for (const geometry of geometries) {
|
|
382
|
+
const position = geometry.attributes.position;
|
|
383
|
+
const normal = geometry.attributes.normal;
|
|
384
|
+
const uv = geometry.attributes.uv;
|
|
385
|
+
const color = geometry.attributes.color;
|
|
386
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
387
|
+
positions.push(position.getX(index), position.getY(index), position.getZ(index));
|
|
388
|
+
if (normal) normals.push(normal.getX(index), normal.getY(index), normal.getZ(index));
|
|
389
|
+
if (hasUv && uv) uvs.push(uv.getX(index), uv.getY(index));
|
|
390
|
+
if (color) colors.push(color.getX(index), color.getY(index), color.getZ(index));
|
|
391
|
+
else colors.push(1, 1, 1);
|
|
392
|
+
}
|
|
393
|
+
if (geometry.index) {
|
|
394
|
+
for (let index = 0; index < geometry.index.count; index += 1) {
|
|
395
|
+
indices.push(geometry.index.getX(index) + offset);
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
for (let index = 0; index < position.count; index += 1) indices.push(index + offset);
|
|
399
|
+
}
|
|
400
|
+
offset += position.count;
|
|
401
|
+
}
|
|
402
|
+
const merged = new THREE.BufferGeometry();
|
|
403
|
+
merged.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
404
|
+
if (normals.length) merged.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
|
|
405
|
+
if (uvs.length) merged.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
406
|
+
merged.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
|
407
|
+
merged.setIndex(indices);
|
|
408
|
+
if (!normals.length) merged.computeVertexNormals();
|
|
409
|
+
return merged;
|
|
410
|
+
}
|