@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
|
@@ -1003,6 +1003,25 @@ export function createPostProcessingPipeline({
|
|
|
1003
1003
|
const currentViewProjection = new THREE.Matrix4();
|
|
1004
1004
|
const previousViewProjection = new THREE.Matrix4();
|
|
1005
1005
|
let hasPreviousViewProjection = false;
|
|
1006
|
+
let disposed = false;
|
|
1007
|
+
|
|
1008
|
+
function stats() {
|
|
1009
|
+
const fullWidth = Math.max(1, Math.round(currentWidth * currentPixelRatio));
|
|
1010
|
+
const fullHeight = Math.max(1, Math.round(currentHeight * currentPixelRatio));
|
|
1011
|
+
const bloomTargets = bloomDownChain.length + bloomUpChain.length;
|
|
1012
|
+
return {
|
|
1013
|
+
bloomTargets,
|
|
1014
|
+
disposed,
|
|
1015
|
+
enabled,
|
|
1016
|
+
fullResolutionTargets: 2,
|
|
1017
|
+
height: fullHeight,
|
|
1018
|
+
// Approximation for authoring diagnostics: two RGBA16F full-res targets
|
|
1019
|
+
// plus allocated bloom-chain targets. Driver-specific overhead excluded.
|
|
1020
|
+
renderTargetBytesApprox: fullWidth * fullHeight * 8 * (2 + bloomTargets),
|
|
1021
|
+
scenePasses: enabled ? 1 + (needsScenePostDepth(settings) ? 1 : 0) : 1,
|
|
1022
|
+
width: fullWidth,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1006
1025
|
|
|
1007
1026
|
return {
|
|
1008
1027
|
compositeMaterial,
|
|
@@ -1013,8 +1032,26 @@ export function createPostProcessingPipeline({
|
|
|
1013
1032
|
get settings() {
|
|
1014
1033
|
return settings;
|
|
1015
1034
|
},
|
|
1035
|
+
dispose() {
|
|
1036
|
+
if (disposed) return;
|
|
1037
|
+
disposed = true;
|
|
1038
|
+
target.dispose();
|
|
1039
|
+
sceneDepthColorTarget.dispose();
|
|
1040
|
+
disposeBloomChain();
|
|
1041
|
+
for (const material of [
|
|
1042
|
+
compositeMaterial,
|
|
1043
|
+
sceneDepthColorMaterial,
|
|
1044
|
+
bloomPrefilterMaterial,
|
|
1045
|
+
bloomDownsampleMaterial,
|
|
1046
|
+
bloomUpsampleMaterial,
|
|
1047
|
+
]) material?.dispose?.();
|
|
1048
|
+
},
|
|
1049
|
+
resetHistory() {
|
|
1050
|
+
hasPreviousViewProjection = false;
|
|
1051
|
+
},
|
|
1016
1052
|
render(delta) {
|
|
1017
1053
|
void delta;
|
|
1054
|
+
if (disposed) return;
|
|
1018
1055
|
if (!enabled) {
|
|
1019
1056
|
renderer.render(scene, camera);
|
|
1020
1057
|
return;
|
|
@@ -1070,6 +1107,7 @@ export function createPostProcessingPipeline({
|
|
|
1070
1107
|
renderer.setRenderTarget(previousRenderTarget);
|
|
1071
1108
|
},
|
|
1072
1109
|
setSize(nextWidth, nextHeight, nextPixelRatio = pixelRatio) {
|
|
1110
|
+
if (disposed) return;
|
|
1073
1111
|
currentWidth = nextWidth;
|
|
1074
1112
|
currentHeight = nextHeight;
|
|
1075
1113
|
currentPixelRatio = nextPixelRatio;
|
|
@@ -1091,6 +1129,7 @@ export function createPostProcessingPipeline({
|
|
|
1091
1129
|
);
|
|
1092
1130
|
},
|
|
1093
1131
|
setCharacterMask(texture) {
|
|
1132
|
+
if (disposed) return;
|
|
1094
1133
|
// See applyCompositeSettings' tLut comment: the node backends need a
|
|
1095
1134
|
// real bound texture at all times, never `null`; harmless on classic
|
|
1096
1135
|
// since tCharacterMask is only read behind the useCharacterMask guard.
|
|
@@ -1098,6 +1137,7 @@ export function createPostProcessingPipeline({
|
|
|
1098
1137
|
compositeMaterial.uniforms.useCharacterMask.value = texture ? 1.0 : 0.0;
|
|
1099
1138
|
},
|
|
1100
1139
|
setSettings(nextSettingsInput = {}) {
|
|
1140
|
+
if (disposed) return;
|
|
1101
1141
|
settings = createPostProcessingSettings(nextSettingsInput);
|
|
1102
1142
|
enabled = isPostProcessingEnabled(settings);
|
|
1103
1143
|
applyCompositeSettings(
|
|
@@ -1110,6 +1150,7 @@ export function createPostProcessingPipeline({
|
|
|
1110
1150
|
currentPixelRatio,
|
|
1111
1151
|
);
|
|
1112
1152
|
},
|
|
1153
|
+
stats,
|
|
1113
1154
|
target,
|
|
1114
1155
|
};
|
|
1115
1156
|
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// Wave-1 prop generators: path dressing. Each generator returns
|
|
2
|
+
// { main, glow, footprint } — merged painted geometry per role plus the
|
|
3
|
+
// collision footprint ({ radius } or { circles }) in the prop's local
|
|
4
|
+
// frame (origin = ground contact, +y up).
|
|
5
|
+
//
|
|
6
|
+
// `detail` is 'hi' or 'lo': lo drops segment counts and garnish parts but
|
|
7
|
+
// keeps the silhouette, so the distance swap is invisible in motion.
|
|
8
|
+
|
|
9
|
+
import { PartsBuilder } from './propParts.js';
|
|
10
|
+
|
|
11
|
+
const TAU = Math.PI * 2;
|
|
12
|
+
|
|
13
|
+
// --- lantern -----------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export function generateLantern(settings, random, detail = 'hi') {
|
|
16
|
+
const { shape } = settings;
|
|
17
|
+
const hi = detail === 'hi';
|
|
18
|
+
const builder = new PartsBuilder(settings.surface, random);
|
|
19
|
+
// seeded proportion jitter: every lantern is a cousin, not a clone
|
|
20
|
+
const height = shape.height * (0.95 + random() * 0.1);
|
|
21
|
+
const lamp = shape.lampSize * (0.94 + random() * 0.12);
|
|
22
|
+
|
|
23
|
+
if (settings.asset.variant === 'chochin') {
|
|
24
|
+
// Post + arm + hanging paper barrel. The paper is the glow role — warm
|
|
25
|
+
// unlit lamp bodies read at dusk without any real light.
|
|
26
|
+
const postHeight = height;
|
|
27
|
+
builder.cylinder(0.05, 0.065, postHeight, {
|
|
28
|
+
position: [0, postHeight / 2, 0], segments: hi ? 8 : 5, slot: 0,
|
|
29
|
+
});
|
|
30
|
+
builder.box(0.05, 0.05, 0.42, {
|
|
31
|
+
position: [0, postHeight - 0.05, 0.18], slot: 0, shade: 0.92,
|
|
32
|
+
});
|
|
33
|
+
const bodyR = 0.16 * lamp;
|
|
34
|
+
const bodyH = 0.42 * lamp;
|
|
35
|
+
const bodyY = postHeight - 0.14 - bodyH / 2;
|
|
36
|
+
builder.lathe([
|
|
37
|
+
[0.02, 0], [bodyR * 0.8, bodyH * 0.12], [bodyR, bodyH * 0.5],
|
|
38
|
+
[bodyR * 0.8, bodyH * 0.88], [0.02, bodyH],
|
|
39
|
+
], {
|
|
40
|
+
position: [0, bodyY, 0.34], role: 'glow', segments: hi ? 12 : 7,
|
|
41
|
+
shade: 0.75 + shape.glow * 0.5, slot: 2,
|
|
42
|
+
});
|
|
43
|
+
builder.cylinder(0.05, 0.05, 0.03, {
|
|
44
|
+
position: [0, bodyY + bodyH + 0.015, 0.34], segments: hi ? 8 : 5, shade: 0.5, slot: 0,
|
|
45
|
+
});
|
|
46
|
+
builder.cylinder(0.05, 0.05, 0.03, {
|
|
47
|
+
position: [0, bodyY - 0.015, 0.34], segments: hi ? 8 : 5, shade: 0.5, slot: 0,
|
|
48
|
+
});
|
|
49
|
+
return { footprint: { radius: 0.14 }, ...builder.build() };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (settings.asset.variant === 'woodPost') {
|
|
53
|
+
const postHeight = height;
|
|
54
|
+
builder.box(0.09, postHeight, 0.09, { position: [0, postHeight / 2, 0], slot: 0 });
|
|
55
|
+
const boxSize = 0.24 * lamp;
|
|
56
|
+
const boxY = postHeight - boxSize / 2 - 0.02;
|
|
57
|
+
// housing frame: four corner sticks + glow core
|
|
58
|
+
for (const [dx, dz] of [[-1, -1], [-1, 1], [1, -1], [1, 1]]) {
|
|
59
|
+
builder.box(0.03, boxSize, 0.03, {
|
|
60
|
+
position: [dx * boxSize * 0.45, boxY, dz * boxSize * 0.45], shade: 0.85, slot: 0,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
builder.box(boxSize * 0.8, boxSize * 0.8, boxSize * 0.8, {
|
|
64
|
+
position: [0, boxY, 0], role: 'glow', shade: 0.7 + shape.glow * 0.5, slot: 2,
|
|
65
|
+
});
|
|
66
|
+
builder.box(boxSize * 1.5, 0.04, boxSize * 1.5, {
|
|
67
|
+
position: [0, boxY + boxSize * 0.55, 0], rotation: [0, Math.PI / 4, 0], shade: 0.8, slot: 1,
|
|
68
|
+
});
|
|
69
|
+
return { footprint: { radius: 0.12 }, ...builder.build() };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// stoneToro: plinth → pillar → firebox (glow core behind stone posts) →
|
|
73
|
+
// pyramidal cap → jewel. The anime shrine-path staple.
|
|
74
|
+
const plinthH = height * 0.14;
|
|
75
|
+
const pillarH = height * 0.34;
|
|
76
|
+
const shelfH = height * 0.06;
|
|
77
|
+
const boxH = height * 0.26 * lamp;
|
|
78
|
+
const roofH = height * 0.16;
|
|
79
|
+
let y = 0;
|
|
80
|
+
builder.lathe([
|
|
81
|
+
[height * 0.22, 0], [height * 0.2, plinthH * 0.5], [height * 0.13, plinthH],
|
|
82
|
+
], { position: [0, y, 0], segments: hi ? 10 : 6, slot: 0 });
|
|
83
|
+
y += plinthH;
|
|
84
|
+
builder.cylinder(height * 0.06, height * 0.08, pillarH, {
|
|
85
|
+
position: [0, y + pillarH / 2, 0], segments: hi ? 9 : 6, shade: 0.96, slot: 0,
|
|
86
|
+
});
|
|
87
|
+
y += pillarH;
|
|
88
|
+
builder.box(height * 0.3, shelfH, height * 0.3, {
|
|
89
|
+
position: [0, y + shelfH / 2, 0], shade: 0.9, slot: 0,
|
|
90
|
+
});
|
|
91
|
+
y += shelfH;
|
|
92
|
+
const boxW = height * 0.22;
|
|
93
|
+
builder.box(boxW * 0.8, boxH * 0.9, boxW * 0.8, {
|
|
94
|
+
position: [0, y + boxH / 2, 0], role: 'glow', shade: 0.7 + shape.glow * 0.5, slot: 2,
|
|
95
|
+
});
|
|
96
|
+
if (hi) {
|
|
97
|
+
for (const [dx, dz] of [[-1, -1], [-1, 1], [1, -1], [1, 1]]) {
|
|
98
|
+
builder.box(boxW * 0.16, boxH, boxW * 0.16, {
|
|
99
|
+
position: [dx * boxW * 0.42, y + boxH / 2, dz * boxW * 0.42], shade: 0.85, slot: 0,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
y += boxH;
|
|
104
|
+
builder.cone(height * 0.24 * shape.roofOverhang, roofH, {
|
|
105
|
+
position: [0, y + roofH / 2, 0], rotation: [0, Math.PI / 4, 0], segments: 4, shade: 0.88, slot: 0,
|
|
106
|
+
});
|
|
107
|
+
y += roofH;
|
|
108
|
+
builder.sphere(height * 0.045, {
|
|
109
|
+
position: [0, y + height * 0.03, 0], segments: hi ? 8 : 6, shade: 1.05, slot: 0,
|
|
110
|
+
});
|
|
111
|
+
return { footprint: { radius: Math.max(height * 0.22, 0.2) }, ...builder.build() };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- signpost ----------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
export function generateSignpost(settings, random, detail = 'hi') {
|
|
117
|
+
const { shape } = settings;
|
|
118
|
+
const hi = detail === 'hi';
|
|
119
|
+
const builder = new PartsBuilder(settings.surface, random);
|
|
120
|
+
const tilt = (random() * 2 - 1) * shape.tilt * 0.09;
|
|
121
|
+
const height = shape.height;
|
|
122
|
+
builder.cylinder(0.045, 0.06, height, {
|
|
123
|
+
position: [Math.sin(tilt) * height * 0.4, height / 2, 0],
|
|
124
|
+
rotation: [0, 0, tilt],
|
|
125
|
+
segments: hi ? 8 : 5,
|
|
126
|
+
slot: 0,
|
|
127
|
+
});
|
|
128
|
+
const boards = settings.asset.variant === 'single' ? 1
|
|
129
|
+
: settings.asset.variant === 'double' ? 2 : 4;
|
|
130
|
+
for (let index = 0; index < boards; index += 1) {
|
|
131
|
+
const y = height - 0.22 - index * 0.24;
|
|
132
|
+
const yaw = settings.asset.variant === 'crossroads'
|
|
133
|
+
? (index * Math.PI) / 2 + (random() - 0.5) * 0.2
|
|
134
|
+
: (random() - 0.5) * 0.6 + (index % 2 ? Math.PI : 0);
|
|
135
|
+
const length = shape.boardLength * (0.85 + random() * 0.3);
|
|
136
|
+
builder.box(length, 0.13, 0.03, {
|
|
137
|
+
position: [
|
|
138
|
+
Math.cos(yaw) * length * 0.42 + Math.sin(tilt) * y * 0.8,
|
|
139
|
+
y,
|
|
140
|
+
-Math.sin(yaw) * length * 0.42,
|
|
141
|
+
],
|
|
142
|
+
rotation: [0, yaw, (random() - 0.5) * shape.tilt * 0.12],
|
|
143
|
+
shade: 0.95 + random() * 0.1,
|
|
144
|
+
slot: 1,
|
|
145
|
+
});
|
|
146
|
+
if (hi) {
|
|
147
|
+
// pointed tip: a small wedge at the far end sells "direction"
|
|
148
|
+
builder.cone(0.075, 0.1, {
|
|
149
|
+
position: [
|
|
150
|
+
Math.cos(yaw) * length * 0.95 + Math.sin(tilt) * y * 0.8,
|
|
151
|
+
y,
|
|
152
|
+
-Math.sin(yaw) * length * 0.95,
|
|
153
|
+
],
|
|
154
|
+
// cone +y → horizontal board direction (cos yaw, 0, -sin yaw)
|
|
155
|
+
rotation: [Math.PI / 2, Math.atan2(Math.cos(yaw), -Math.sin(yaw)), 0],
|
|
156
|
+
segments: 4,
|
|
157
|
+
shade: 0.9,
|
|
158
|
+
slot: 1,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { footprint: { radius: 0.14 }, ...builder.build() };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- stone stairs -------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
export function generateStoneStairs(settings, random, detail = 'hi') {
|
|
168
|
+
const { shape } = settings;
|
|
169
|
+
const builder = new PartsBuilder(settings.surface, random);
|
|
170
|
+
const depth = 0.34;
|
|
171
|
+
const worn = settings.asset.variant === 'worn' ? 1 : shape.wear;
|
|
172
|
+
for (let index = 0; index < shape.stepCount; index += 1) {
|
|
173
|
+
const wearJitter = (random() * 2 - 1) * worn;
|
|
174
|
+
const mossRoll = random(); // drawn unconditionally: hi and lo must stay in step
|
|
175
|
+
builder.box(
|
|
176
|
+
shape.width * (1 - Math.abs(wearJitter) * 0.06),
|
|
177
|
+
shape.stepHeight + 0.05,
|
|
178
|
+
depth + 0.1,
|
|
179
|
+
{
|
|
180
|
+
position: [
|
|
181
|
+
wearJitter * 0.05,
|
|
182
|
+
(index + 0.5) * shape.stepHeight - 0.04,
|
|
183
|
+
index * depth,
|
|
184
|
+
],
|
|
185
|
+
rotation: [wearJitter * 0.03, wearJitter * 0.05, wearJitter * 0.025],
|
|
186
|
+
shade: 0.92 + random() * 0.14,
|
|
187
|
+
// lower steps go mossy: accent slot carries the green
|
|
188
|
+
slot: detail === 'hi' && index < shape.stepCount * shape.wear * 0.6 && mossRoll < 0.5 ? 2 : 0,
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const runDepth = shape.stepCount * depth;
|
|
193
|
+
return {
|
|
194
|
+
footprint: {
|
|
195
|
+
circles: [
|
|
196
|
+
{ radius: shape.width * 0.55, x: 0, z: runDepth * 0.25 },
|
|
197
|
+
{ radius: shape.width * 0.55, x: 0, z: runDepth * 0.75 },
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
...builder.build(),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --- milestone -----------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
export function generateMilestone(settings, random, detail = 'hi') {
|
|
207
|
+
const { shape } = settings;
|
|
208
|
+
const hi = detail === 'hi';
|
|
209
|
+
const builder = new PartsBuilder(settings.surface, random);
|
|
210
|
+
const height = shape.height;
|
|
211
|
+
const girth = shape.girth * height * 0.22;
|
|
212
|
+
|
|
213
|
+
if (settings.asset.variant === 'obelisk') {
|
|
214
|
+
builder.box(girth * 1.6, height * 0.12, girth * 1.6, {
|
|
215
|
+
position: [0, height * 0.06, 0], shade: 0.9, slot: 0,
|
|
216
|
+
});
|
|
217
|
+
builder.box(girth, height * 0.76, girth, {
|
|
218
|
+
position: [0, height * 0.5, 0], scale: [1, 1, 1], slot: 0,
|
|
219
|
+
});
|
|
220
|
+
builder.cone(girth * 0.72, height * 0.14, {
|
|
221
|
+
position: [0, height * 0.93, 0], rotation: [0, Math.PI / 4, 0], segments: 4, shade: 1.04, slot: 0,
|
|
222
|
+
});
|
|
223
|
+
} else if (settings.asset.variant === 'jizo') {
|
|
224
|
+
builder.lathe([
|
|
225
|
+
[girth * 1.3, 0], [girth * 1.1, height * 0.1], [girth * 0.9, height * 0.16],
|
|
226
|
+
], { segments: hi ? 10 : 6, shade: 0.9, slot: 0 });
|
|
227
|
+
builder.lathe([
|
|
228
|
+
[girth * 0.85, 0], [girth, height * 0.3], [girth * 0.8, height * 0.62], [girth * 0.35, height * 0.72],
|
|
229
|
+
], { position: [0, height * 0.14, 0], segments: hi ? 10 : 6, slot: 0 });
|
|
230
|
+
builder.sphere(girth * 0.55, {
|
|
231
|
+
position: [0, height * 0.9, 0], segments: hi ? 9 : 6, shade: 1.02, slot: 0,
|
|
232
|
+
});
|
|
233
|
+
// the red bib — tiny, and instantly reads "jizō"
|
|
234
|
+
builder.box(girth * 0.9, height * 0.2, 0.02, {
|
|
235
|
+
position: [0, height * 0.68, girth * 0.72], rotation: [0.25, 0, 0], shade: 1, slot: 2,
|
|
236
|
+
});
|
|
237
|
+
} else {
|
|
238
|
+
// roadStone: weathered rounded slab
|
|
239
|
+
builder.lathe([
|
|
240
|
+
[girth * 1.15, 0], [girth * 1.05, height * 0.32], [girth * 0.92, height * 0.7],
|
|
241
|
+
[girth * 0.6, height * 0.94], [0.01, height],
|
|
242
|
+
], { rotation: [0, random() * TAU, 0], scale: [1, 1, 0.7], segments: hi ? 11 : 7, slot: 0 });
|
|
243
|
+
if (hi && shape.inscription > 0.05) {
|
|
244
|
+
builder.box(girth * 1.1, height * 0.26 * shape.inscription, girth * 0.16, {
|
|
245
|
+
position: [0, height * 0.46, girth * 0.62], shade: 0.78, slot: 0,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (hi && shape.moss > 0.05) {
|
|
250
|
+
builder.lathe([
|
|
251
|
+
[girth * 1.22, 0], [girth * 1.08, height * 0.1 + shape.moss * height * 0.12],
|
|
252
|
+
], { segments: 8, shade: 0.95, slot: 2 });
|
|
253
|
+
}
|
|
254
|
+
return { footprint: { radius: Math.max(girth * 1.3, 0.18) }, ...builder.build() };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// --- fence (linear) -------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Builds a fence run along resolved 3D points (one per post). Returns
|
|
261
|
+
* { main, glow: null, footprints } where footprints are world-frame circles
|
|
262
|
+
* along the run (the run is baked in world space by the caller's points).
|
|
263
|
+
*/
|
|
264
|
+
export function generateFenceAlong(settings, random, points, detail = 'hi') {
|
|
265
|
+
const { shape } = settings;
|
|
266
|
+
const hi = detail === 'hi';
|
|
267
|
+
const builder = new PartsBuilder(settings.surface, random);
|
|
268
|
+
const variant = settings.asset.variant;
|
|
269
|
+
const thickness = shape.thickness;
|
|
270
|
+
|
|
271
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
272
|
+
const point = points[index];
|
|
273
|
+
const leanX = (random() * 2 - 1) * shape.lean * 0.1;
|
|
274
|
+
const leanZ = (random() * 2 - 1) * shape.lean * 0.1;
|
|
275
|
+
const postHeight = shape.postHeight * (1 + (random() - 0.5) * 0.08);
|
|
276
|
+
if (variant === 'picket' || variant === 'ranch') {
|
|
277
|
+
builder.box(thickness * 1.2, postHeight, thickness * 1.2, {
|
|
278
|
+
position: [point.x, point.y + postHeight / 2, point.z],
|
|
279
|
+
rotation: [leanX, 0, leanZ],
|
|
280
|
+
shade: 0.9 + random() * 0.16,
|
|
281
|
+
slot: 0,
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
builder.cylinder(thickness * 0.8, thickness, postHeight, {
|
|
285
|
+
position: [point.x, point.y + postHeight / 2, point.z],
|
|
286
|
+
rotation: [leanX, 0, leanZ],
|
|
287
|
+
segments: hi ? 7 : 5,
|
|
288
|
+
shade: 0.9 + random() * 0.16,
|
|
289
|
+
slot: 0,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
295
|
+
const a = points[index];
|
|
296
|
+
const b = points[index + 1];
|
|
297
|
+
if (random() < shape.gapChance) continue;
|
|
298
|
+
const span = Math.hypot(b.x - a.x, b.z - a.z);
|
|
299
|
+
const heading = Math.atan2(b.x - a.x, b.z - a.z);
|
|
300
|
+
const rails = variant === 'rope' ? 1 : Math.round(shape.railCount);
|
|
301
|
+
for (let rail = 0; rail < rails; rail += 1) {
|
|
302
|
+
const t = (rail + 1) / (rails + 1);
|
|
303
|
+
const yA = a.y + shape.postHeight * (0.35 + t * 0.55);
|
|
304
|
+
const yB = b.y + shape.postHeight * (0.35 + t * 0.55);
|
|
305
|
+
if (variant === 'rope') {
|
|
306
|
+
// sagging rope: three chained segments approximating a catenary
|
|
307
|
+
const sagDrop = shape.sag * 0.22 * span * 0.25;
|
|
308
|
+
const midX = (a.x + b.x) / 2;
|
|
309
|
+
const midZ = (a.z + b.z) / 2;
|
|
310
|
+
const midY = (yA + yB) / 2 - sagDrop;
|
|
311
|
+
for (const [fromX, fromY, fromZ, toX, toY, toZ] of [
|
|
312
|
+
[a.x, yA, a.z, midX, midY, midZ],
|
|
313
|
+
[midX, midY, midZ, b.x, yB, b.z],
|
|
314
|
+
]) {
|
|
315
|
+
const length = Math.hypot(toX - fromX, toY - fromY, toZ - fromZ);
|
|
316
|
+
// cylinder +y → segment direction: pitch = acos(dy), yaw = atan2(dx, dz)
|
|
317
|
+
const dy = (toY - fromY) / Math.max(length, 1e-6);
|
|
318
|
+
builder.cylinder(0.02, 0.02, length, {
|
|
319
|
+
position: [(fromX + toX) / 2, (fromY + toY) / 2, (fromZ + toZ) / 2],
|
|
320
|
+
rotation: [
|
|
321
|
+
Math.acos(Math.min(Math.max(dy, -1), 1)),
|
|
322
|
+
Math.atan2(toX - fromX, toZ - fromZ),
|
|
323
|
+
0,
|
|
324
|
+
],
|
|
325
|
+
segments: 5,
|
|
326
|
+
shade: 0.85,
|
|
327
|
+
slot: 1,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const pitch = Math.atan2(yB - yA, span);
|
|
333
|
+
const bow = variant === 'log' ? 0 : (random() - 0.5) * shape.sag * 0.06;
|
|
334
|
+
builder.box(thickness * 0.8, thickness * (variant === 'log' ? 1.4 : 0.8), span + thickness, {
|
|
335
|
+
position: [(a.x + b.x) / 2, (yA + yB) / 2 - bow, (a.z + b.z) / 2],
|
|
336
|
+
rotation: [-pitch, heading, 0],
|
|
337
|
+
shade: 0.92 + random() * 0.14,
|
|
338
|
+
slot: 1,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (variant === 'picket') {
|
|
342
|
+
const slats = Math.max(Math.floor(span / 0.22), 2);
|
|
343
|
+
for (let slat = 0; slat < slats; slat += 1) {
|
|
344
|
+
const t = (slat + 0.5) / slats;
|
|
345
|
+
const roll = (random() - 0.5) * 0.05;
|
|
346
|
+
const shade = 0.94 + random() * 0.12;
|
|
347
|
+
if (!hi) continue; // values drawn either way — streams stay aligned
|
|
348
|
+
const x = a.x + (b.x - a.x) * t;
|
|
349
|
+
const z = a.z + (b.z - a.z) * t;
|
|
350
|
+
const y = a.y + (b.y - a.y) * t;
|
|
351
|
+
builder.box(0.1, shape.postHeight * 0.82, 0.02, {
|
|
352
|
+
position: [x, y + shape.postHeight * 0.5, z],
|
|
353
|
+
rotation: [0, heading, roll],
|
|
354
|
+
shade,
|
|
355
|
+
slot: 1,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Collision: a chain of circles along the run so characters can't slip
|
|
362
|
+
// between posts.
|
|
363
|
+
const footprints = [];
|
|
364
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
365
|
+
const a = points[index];
|
|
366
|
+
const b = points[index + 1];
|
|
367
|
+
const span = Math.hypot(b.x - a.x, b.z - a.z);
|
|
368
|
+
const steps = Math.max(Math.round(span / 0.55), 1);
|
|
369
|
+
for (let step = 0; step <= steps; step += 1) {
|
|
370
|
+
const t = step / steps;
|
|
371
|
+
footprints.push({
|
|
372
|
+
radius: 0.2,
|
|
373
|
+
x: a.x + (b.x - a.x) * t,
|
|
374
|
+
z: a.z + (b.z - a.z) * t,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return { footprints, ...builder.build() };
|
|
379
|
+
}
|