@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,397 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
access,
|
|
4
|
+
mkdir,
|
|
5
|
+
readFile,
|
|
6
|
+
readdir,
|
|
7
|
+
rename,
|
|
8
|
+
stat,
|
|
9
|
+
unlink,
|
|
10
|
+
writeFile,
|
|
11
|
+
} from 'node:fs/promises';
|
|
12
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export const TOONLAB_WORKSPACE_VERSION = 1;
|
|
15
|
+
export const DEFAULT_WORKSPACE_DIRECTORY = '.toonlab';
|
|
16
|
+
|
|
17
|
+
const WORKSPACE_DIRECTORIES = Object.freeze([
|
|
18
|
+
'assets',
|
|
19
|
+
'creations',
|
|
20
|
+
'exports',
|
|
21
|
+
'imports',
|
|
22
|
+
'library/entries',
|
|
23
|
+
'presets',
|
|
24
|
+
'storage',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const WRITABLE_ROOTS = new Set(['assets', 'creations', 'exports', 'imports', 'presets']);
|
|
28
|
+
const SENSITIVE_STORAGE_KEYS = new Set([
|
|
29
|
+
'toonlab.asset-lab.polypizza-key.v1',
|
|
30
|
+
'toonlab.texture-lab.ai.v1',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const MIME_BY_EXTENSION = Object.freeze({
|
|
34
|
+
'.glb': 'model/gltf-binary',
|
|
35
|
+
'.gltf': 'model/gltf+json',
|
|
36
|
+
'.jpeg': 'image/jpeg',
|
|
37
|
+
'.jpg': 'image/jpeg',
|
|
38
|
+
'.json': 'application/json',
|
|
39
|
+
'.png': 'image/png',
|
|
40
|
+
'.webp': 'image/webp',
|
|
41
|
+
'.zip': 'application/zip',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function now() {
|
|
45
|
+
return new Date().toISOString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function exists(path) {
|
|
49
|
+
try {
|
|
50
|
+
await access(path);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function writeJsonAtomic(path, value) {
|
|
58
|
+
await mkdir(dirname(path), { recursive: true });
|
|
59
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
60
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
61
|
+
await rename(temporary, path);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readJson(path, fallback) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
67
|
+
} catch {
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertWorkspaceChild(workspacePath, path) {
|
|
73
|
+
const rel = relative(workspacePath, path);
|
|
74
|
+
if (!rel || rel === '.') return path;
|
|
75
|
+
if (rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel)) {
|
|
76
|
+
throw new Error('Path escapes the ToonLab workspace.');
|
|
77
|
+
}
|
|
78
|
+
return path;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeRelativePath(value) {
|
|
82
|
+
const parts = String(value ?? '')
|
|
83
|
+
.replaceAll('\\', '/')
|
|
84
|
+
.replace(/^\/+/, '')
|
|
85
|
+
.split('/')
|
|
86
|
+
.filter((part) => part && part !== '.');
|
|
87
|
+
if (parts.includes('..')) throw new Error('Workspace paths cannot contain "..".');
|
|
88
|
+
return parts.join('/');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function safeSlug(value, fallback = 'untitled') {
|
|
92
|
+
return String(value ?? '')
|
|
93
|
+
.trim()
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
96
|
+
.replace(/^-+|-+$/g, '') || fallback;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function libraryFilename(id) {
|
|
100
|
+
return `${Buffer.from(String(id)).toString('base64url')}.json`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function storagePath(workspacePath) {
|
|
104
|
+
return join(workspacePath, 'storage', 'local-storage.json');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function manifestPath(workspacePath) {
|
|
108
|
+
return join(workspacePath, 'toonlab.json');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isPersistableStorageKey(key) {
|
|
112
|
+
const normalized = String(key ?? '');
|
|
113
|
+
if (SENSITIVE_STORAGE_KEYS.has(normalized)) return false;
|
|
114
|
+
if (normalized.includes('.thumb') || normalized.endsWith('.__probe__')) return false;
|
|
115
|
+
return normalized.startsWith('toonlab.') || normalized.startsWith('threejs-toon-shader.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function resolveWorkspacePath(input = DEFAULT_WORKSPACE_DIRECTORY, cwd = process.cwd()) {
|
|
119
|
+
return resolve(cwd, input || DEFAULT_WORKSPACE_DIRECTORY);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function ensureWorkspace(workspacePath) {
|
|
123
|
+
const root = resolve(workspacePath);
|
|
124
|
+
await mkdir(root, { recursive: true });
|
|
125
|
+
await Promise.all(WORKSPACE_DIRECTORIES.map((directory) => mkdir(join(root, directory), { recursive: true })));
|
|
126
|
+
const path = manifestPath(root);
|
|
127
|
+
if (!(await exists(path))) {
|
|
128
|
+
await writeJsonAtomic(path, {
|
|
129
|
+
createdAt: now(),
|
|
130
|
+
migrations: {},
|
|
131
|
+
updatedAt: now(),
|
|
132
|
+
version: TOONLAB_WORKSPACE_VERSION,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return root;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function readWorkspaceManifest(workspacePath) {
|
|
139
|
+
const root = await ensureWorkspace(workspacePath);
|
|
140
|
+
return readJson(manifestPath(root), {
|
|
141
|
+
createdAt: now(),
|
|
142
|
+
migrations: {},
|
|
143
|
+
updatedAt: now(),
|
|
144
|
+
version: TOONLAB_WORKSPACE_VERSION,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function updateWorkspaceManifest(workspacePath, update) {
|
|
149
|
+
const root = await ensureWorkspace(workspacePath);
|
|
150
|
+
const current = await readWorkspaceManifest(root);
|
|
151
|
+
const next = typeof update === 'function' ? update(current) : { ...current, ...update };
|
|
152
|
+
next.updatedAt = now();
|
|
153
|
+
next.version = TOONLAB_WORKSPACE_VERSION;
|
|
154
|
+
await writeJsonAtomic(manifestPath(root), next);
|
|
155
|
+
return next;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function getWorkspaceInfo(workspacePath) {
|
|
159
|
+
const root = await ensureWorkspace(workspacePath);
|
|
160
|
+
const manifest = await readWorkspaceManifest(root);
|
|
161
|
+
const storageInitialized = await exists(storagePath(root));
|
|
162
|
+
const library = await getLibraryState(root);
|
|
163
|
+
return {
|
|
164
|
+
libraryCount: library.entries.length,
|
|
165
|
+
manifest,
|
|
166
|
+
path: root,
|
|
167
|
+
storageInitialized,
|
|
168
|
+
version: TOONLAB_WORKSPACE_VERSION,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function readBrowserStorage(workspacePath) {
|
|
173
|
+
const root = await ensureWorkspace(workspacePath);
|
|
174
|
+
const path = storagePath(root);
|
|
175
|
+
if (!(await exists(path))) return { entries: {}, initialized: false };
|
|
176
|
+
const parsed = await readJson(path, {});
|
|
177
|
+
const entries = {};
|
|
178
|
+
for (const [key, value] of Object.entries(parsed ?? {})) {
|
|
179
|
+
if (isPersistableStorageKey(key) && typeof value === 'string') entries[key] = value;
|
|
180
|
+
}
|
|
181
|
+
return { entries, initialized: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function importBrowserStorage(workspacePath, input) {
|
|
185
|
+
const root = await ensureWorkspace(workspacePath);
|
|
186
|
+
const current = await readBrowserStorage(root);
|
|
187
|
+
if (current.initialized) return current;
|
|
188
|
+
const entries = {};
|
|
189
|
+
for (const [key, value] of Object.entries(input ?? {})) {
|
|
190
|
+
if (isPersistableStorageKey(key) && typeof value === 'string') entries[key] = value;
|
|
191
|
+
}
|
|
192
|
+
await writeJsonAtomic(storagePath(root), entries);
|
|
193
|
+
await updateWorkspaceManifest(root, (manifest) => ({
|
|
194
|
+
...manifest,
|
|
195
|
+
migrations: { ...manifest.migrations, browserLocalStorage: now() },
|
|
196
|
+
}));
|
|
197
|
+
return { entries, initialized: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function setBrowserStorageValue(workspacePath, key, value) {
|
|
201
|
+
if (!isPersistableStorageKey(key)) throw new Error('This browser-storage key is not persisted.');
|
|
202
|
+
if (typeof value !== 'string') throw new Error('Browser-storage values must be strings.');
|
|
203
|
+
const root = await ensureWorkspace(workspacePath);
|
|
204
|
+
const current = await readBrowserStorage(root);
|
|
205
|
+
const entries = { ...current.entries, [key]: value };
|
|
206
|
+
await writeJsonAtomic(storagePath(root), entries);
|
|
207
|
+
return entries;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function deleteBrowserStorageValue(workspacePath, key) {
|
|
211
|
+
const root = await ensureWorkspace(workspacePath);
|
|
212
|
+
const current = await readBrowserStorage(root);
|
|
213
|
+
if (!current.initialized) return {};
|
|
214
|
+
const entries = { ...current.entries };
|
|
215
|
+
delete entries[key];
|
|
216
|
+
await writeJsonAtomic(storagePath(root), entries);
|
|
217
|
+
return entries;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function clearBrowserStorage(workspacePath) {
|
|
221
|
+
const root = await ensureWorkspace(workspacePath);
|
|
222
|
+
await writeJsonAtomic(storagePath(root), {});
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function listLibraryEntries(workspacePath) {
|
|
227
|
+
const root = await ensureWorkspace(workspacePath);
|
|
228
|
+
const directory = join(root, 'library', 'entries');
|
|
229
|
+
const files = (await readdir(directory, { withFileTypes: true }))
|
|
230
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'));
|
|
231
|
+
const entries = [];
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
const entry = await readJson(join(directory, file.name), null);
|
|
234
|
+
if (entry?.id && typeof entry.id === 'string') entries.push(entry);
|
|
235
|
+
}
|
|
236
|
+
return entries.sort((a, b) => String(a.label ?? a.id).localeCompare(String(b.label ?? b.id)));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function getLibraryState(workspacePath) {
|
|
240
|
+
const root = await ensureWorkspace(workspacePath);
|
|
241
|
+
const manifest = await readWorkspaceManifest(root);
|
|
242
|
+
return {
|
|
243
|
+
entries: await listLibraryEntries(root),
|
|
244
|
+
migrated: Boolean(manifest.migrations?.indexedDbCatalog),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function saveLibraryEntry(workspacePath, entry) {
|
|
249
|
+
if (!entry || typeof entry !== 'object' || typeof entry.id !== 'string' || !entry.id.trim()) {
|
|
250
|
+
throw new Error('A library entry with an id is required.');
|
|
251
|
+
}
|
|
252
|
+
const root = await ensureWorkspace(workspacePath);
|
|
253
|
+
const path = join(root, 'library', 'entries', libraryFilename(entry.id));
|
|
254
|
+
await writeJsonAtomic(path, entry);
|
|
255
|
+
return entry;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function deleteLibraryEntry(workspacePath, id) {
|
|
259
|
+
const root = await ensureWorkspace(workspacePath);
|
|
260
|
+
const path = join(root, 'library', 'entries', libraryFilename(id));
|
|
261
|
+
try {
|
|
262
|
+
await unlink(path);
|
|
263
|
+
return true;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error?.code === 'ENOENT') return false;
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function migrateLibraryEntries(workspacePath, entries) {
|
|
271
|
+
const root = await ensureWorkspace(workspacePath);
|
|
272
|
+
const state = await getLibraryState(root);
|
|
273
|
+
if (!state.migrated) {
|
|
274
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
275
|
+
if (entry?.id) await saveLibraryEntry(root, entry);
|
|
276
|
+
}
|
|
277
|
+
await updateWorkspaceManifest(root, (manifest) => ({
|
|
278
|
+
...manifest,
|
|
279
|
+
migrations: { ...manifest.migrations, indexedDbCatalog: now() },
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
return getLibraryState(root);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function resolveWritableWorkspaceFile(workspacePath, relativePath) {
|
|
286
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
287
|
+
const [topLevel] = normalized.split('/');
|
|
288
|
+
if (!normalized || !WRITABLE_ROOTS.has(topLevel)) {
|
|
289
|
+
throw new Error(`Files must be saved below ${[...WRITABLE_ROOTS].join(', ')}.`);
|
|
290
|
+
}
|
|
291
|
+
const path = assertWorkspaceChild(resolve(workspacePath), resolve(workspacePath, normalized));
|
|
292
|
+
return { path, relativePath: normalized };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function writeWorkspaceFile(workspacePath, relativePath, data) {
|
|
296
|
+
const root = await ensureWorkspace(workspacePath);
|
|
297
|
+
const resolved = resolveWritableWorkspaceFile(root, relativePath);
|
|
298
|
+
await mkdir(dirname(resolved.path), { recursive: true });
|
|
299
|
+
await writeFile(resolved.path, data);
|
|
300
|
+
return describeWorkspaceFile(root, resolved.path);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function describeWorkspaceFile(workspacePath, path) {
|
|
304
|
+
const info = await stat(path);
|
|
305
|
+
const relativePath = relative(workspacePath, path).split(sep).join('/');
|
|
306
|
+
return {
|
|
307
|
+
absolutePath: path,
|
|
308
|
+
id: `file:${relativePath}`,
|
|
309
|
+
kind: extname(path).slice(1).toLowerCase() || 'file',
|
|
310
|
+
mimeType: MIME_BY_EXTENSION[extname(path).toLowerCase()] ?? 'application/octet-stream',
|
|
311
|
+
modifiedAt: info.mtime.toISOString(),
|
|
312
|
+
name: basename(path),
|
|
313
|
+
relativePath,
|
|
314
|
+
sizeBytes: info.size,
|
|
315
|
+
source: 'workspace',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function walkFiles(workspacePath, directory, output) {
|
|
320
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
321
|
+
const path = join(directory, entry.name);
|
|
322
|
+
if (entry.isDirectory()) await walkFiles(workspacePath, path, output);
|
|
323
|
+
else if (entry.isFile()) output.push(await describeWorkspaceFile(workspacePath, path));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function listWorkspaceFiles(workspacePath, { roots = [...WRITABLE_ROOTS] } = {}) {
|
|
328
|
+
const root = await ensureWorkspace(workspacePath);
|
|
329
|
+
const files = [];
|
|
330
|
+
for (const directory of roots) {
|
|
331
|
+
if (!WRITABLE_ROOTS.has(directory)) continue;
|
|
332
|
+
await walkFiles(root, join(root, directory), files);
|
|
333
|
+
}
|
|
334
|
+
return files.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function readWorkspaceFile(workspacePath, relativePath) {
|
|
338
|
+
const root = await ensureWorkspace(workspacePath);
|
|
339
|
+
const normalized = normalizeRelativePath(relativePath.replace(/^file:/, ''));
|
|
340
|
+
const [topLevel] = normalized.split('/');
|
|
341
|
+
if (!WRITABLE_ROOTS.has(topLevel)) throw new Error('File is outside a readable workspace collection.');
|
|
342
|
+
const path = assertWorkspaceChild(root, resolve(root, normalized));
|
|
343
|
+
const description = await describeWorkspaceFile(root, path);
|
|
344
|
+
return { ...description, data: await readFile(path) };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function saveCreation(workspacePath, {
|
|
348
|
+
document,
|
|
349
|
+
filename = null,
|
|
350
|
+
kind = 'creation',
|
|
351
|
+
name = 'Untitled',
|
|
352
|
+
} = {}) {
|
|
353
|
+
if (document === undefined) throw new Error('document is required.');
|
|
354
|
+
const collection = safeSlug(kind, 'creation');
|
|
355
|
+
const requested = filename ? safeSlug(basename(filename), `${safeSlug(name)}.json`) : `${safeSlug(name)}.json`;
|
|
356
|
+
const finalName = extname(requested) ? requested : `${requested}.json`;
|
|
357
|
+
const payload = typeof document === 'string' ? document : `${JSON.stringify(document, null, 2)}\n`;
|
|
358
|
+
return writeWorkspaceFile(workspacePath, `creations/${collection}/${finalName}`, payload);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function inferStorageCluster(key) {
|
|
362
|
+
const match = String(key).match(/^toonlab\.([a-z0-9-]+?)(?:\.document|\.presets|\.projects|$)/i);
|
|
363
|
+
return match?.[1] ?? 'lab';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function listStorageDocuments(workspacePath) {
|
|
367
|
+
const storage = await readBrowserStorage(workspacePath);
|
|
368
|
+
const documents = [];
|
|
369
|
+
for (const [key, raw] of Object.entries(storage.entries)) {
|
|
370
|
+
let parsed;
|
|
371
|
+
try {
|
|
372
|
+
parsed = JSON.parse(raw);
|
|
373
|
+
} catch {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const values = Array.isArray(parsed) ? parsed : [parsed];
|
|
377
|
+
values.forEach((document, index) => {
|
|
378
|
+
if (!document || typeof document !== 'object') return;
|
|
379
|
+
const documentId = String(document.id ?? document.presetId ?? index);
|
|
380
|
+
documents.push({
|
|
381
|
+
cluster: inferStorageCluster(key),
|
|
382
|
+
document,
|
|
383
|
+
id: `storage:${Buffer.from(key).toString('base64url')}:${Buffer.from(documentId).toString('base64url')}`,
|
|
384
|
+
key,
|
|
385
|
+
kind: Array.isArray(parsed) ? 'preset' : 'document',
|
|
386
|
+
label: String(document.label ?? document.name ?? document.id ?? `${inferStorageCluster(key)} ${index + 1}`),
|
|
387
|
+
source: 'workspace-storage',
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return documents;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function matchesText(value, query) {
|
|
395
|
+
if (!query) return true;
|
|
396
|
+
return JSON.stringify(value).toLowerCase().includes(String(query).toLowerCase());
|
|
397
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@call-me-sensei/toonlab",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Stylized (anime/toon) game starter kit for Three.js: character
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Stylized (anime/toon) game starter kit for Three.js: character and environment shading, lighting, weather, water, vegetation, sky, and post-processing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hyperbond Studio PTE. LTD. (Call Me Sensei)",
|
|
7
7
|
"repository": {
|
|
@@ -19,14 +19,24 @@
|
|
|
19
19
|
"cel-shading",
|
|
20
20
|
"anime",
|
|
21
21
|
"shader",
|
|
22
|
+
"lighting",
|
|
22
23
|
"water",
|
|
24
|
+
"weather",
|
|
23
25
|
"game-starter"
|
|
24
26
|
],
|
|
25
27
|
"type": "module",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"toonlab-mcp": "./mcp/server.mjs"
|
|
33
|
+
},
|
|
26
34
|
"main": "./src/index.js",
|
|
27
35
|
"module": "./src/index.js",
|
|
28
36
|
"files": [
|
|
29
37
|
"src",
|
|
38
|
+
"mcp",
|
|
39
|
+
"docs",
|
|
30
40
|
"README.md",
|
|
31
41
|
"LICENSE",
|
|
32
42
|
"ATTRIBUTION.md",
|
|
@@ -36,12 +46,29 @@
|
|
|
36
46
|
".": "./src/index.js",
|
|
37
47
|
"./toon": "./src/toon/index.js",
|
|
38
48
|
"./environment": "./src/environment/index.js",
|
|
49
|
+
"./lighting": "./src/lighting/index.js",
|
|
39
50
|
"./water": "./src/water/index.js",
|
|
40
51
|
"./vegetation": "./src/vegetation/index.js",
|
|
52
|
+
"./vegetation-shaders": "./src/vegetation/vegetationShaders.js",
|
|
53
|
+
"./grass-palettes": "./src/vegetation/grassPalettes.js",
|
|
54
|
+
"./styles": "./src/styles/index.js",
|
|
41
55
|
"./sky": "./src/sky/index.js",
|
|
56
|
+
"./weather": "./src/weather/index.js",
|
|
42
57
|
"./post": "./src/post/index.js",
|
|
58
|
+
"./camera": "./src/camera/index.js",
|
|
59
|
+
"./game-feel": "./src/game-feel/index.js",
|
|
43
60
|
"./rockgen": "./src/rockgen/index.js",
|
|
44
61
|
"./debrisgen": "./src/debrisgen/index.js",
|
|
62
|
+
"./pathgen": "./src/pathgen/index.js",
|
|
63
|
+
"./propgen": "./src/propgen/index.js",
|
|
64
|
+
"./buildinggen": "./src/buildinggen/index.js",
|
|
65
|
+
"./villagegen": "./src/villagegen/index.js",
|
|
66
|
+
"./ambientfx": "./src/ambientfx/index.js",
|
|
67
|
+
"./vfxgen": "./src/vfxgen/index.js",
|
|
68
|
+
"./fauna": "./src/fauna/index.js",
|
|
69
|
+
"./catalog": "./src/catalog/index.js",
|
|
70
|
+
"./texgen": "./src/texgen/index.js",
|
|
71
|
+
"./assetlib": "./src/assetlib/index.js",
|
|
45
72
|
"./character": "./src/character/index.js",
|
|
46
73
|
"./loaders": "./src/loaders/index.js",
|
|
47
74
|
"./debug": "./src/debug/index.js",
|
|
@@ -55,18 +82,49 @@
|
|
|
55
82
|
"assets:environments": "node scripts/generate-local-environment-assets.mjs",
|
|
56
83
|
"assets:local": "npm run assets:characters && npm run assets:environments",
|
|
57
84
|
"dev": "vite",
|
|
85
|
+
"mcp": "node mcp/server.mjs",
|
|
58
86
|
"build": "vite build",
|
|
59
87
|
"baseline:compare": "node scripts/compare-baselines.mjs",
|
|
60
88
|
"preview": "vite preview",
|
|
61
89
|
"baseline:capture": "node scripts/baseline-capture.mjs",
|
|
62
90
|
"verify:rockgen": "node scripts/verify-rockgen.mjs",
|
|
63
|
-
"
|
|
91
|
+
"verify:pathgen": "node scripts/verify-pathgen.mjs",
|
|
92
|
+
"visual:check": "node scripts/visual-check.mjs",
|
|
93
|
+
"verify:propgen": "node scripts/verify-propgen.mjs",
|
|
94
|
+
"verify:buildinggen": "node scripts/verify-buildinggen.mjs",
|
|
95
|
+
"verify:villagegen": "node scripts/verify-villagegen.mjs",
|
|
96
|
+
"verify:fauna": "node scripts/verify-fauna.mjs",
|
|
97
|
+
"verify:ambientfx": "node scripts/verify-ambientfx.mjs",
|
|
98
|
+
"verify:vfxgen": "node scripts/verify-vfxgen.mjs",
|
|
99
|
+
"verify:water": "node scripts/verify-watergen.mjs",
|
|
100
|
+
"verify:lighting": "node scripts/verify-lighting.mjs",
|
|
101
|
+
"verify:lighting-lab": "node scripts/verify-lighting-lab.mjs",
|
|
102
|
+
"verify:weather": "node scripts/verify-weather.mjs",
|
|
103
|
+
"verify:sky": "node scripts/verify-sky.mjs",
|
|
104
|
+
"verify:grass": "node scripts/verify-grass-palettes.mjs",
|
|
105
|
+
"verify:vegetation-shader": "node scripts/verify-vegetation-shader.mjs",
|
|
106
|
+
"verify:api": "node scripts/verify-public-api.mjs",
|
|
107
|
+
"verify:generation": "node scripts/verify-generation.mjs",
|
|
108
|
+
"verify:post": "node scripts/verify-post.mjs",
|
|
109
|
+
"verify:camera": "node scripts/verify-camera.mjs",
|
|
110
|
+
"verify:motion": "node scripts/verify-motion.mjs",
|
|
111
|
+
"verify:biome": "node scripts/verify-biome.mjs",
|
|
112
|
+
"verify:soundscape": "node scripts/verify-soundscape.mjs",
|
|
113
|
+
"verify:game-feel": "node scripts/verify-game-feel.mjs",
|
|
114
|
+
"verify:all": "npm run verify:generation && npm run verify:post && npm run verify:camera && npm run verify:motion && npm run verify:biome && npm run verify:soundscape && npm run verify:game-feel && npm run verify:rockgen && npm run verify:pathgen && npm run verify:propgen && npm run verify:buildinggen && npm run verify:villagegen && npm run verify:fauna && npm run verify:ambientfx && npm run verify:vfxgen && npm run verify:water && npm run verify:lighting && npm run verify:lighting-lab && npm run verify:weather && npm run verify:sky && npm run verify:grass && npm run verify:vegetation-shader && npm run verify:api && npm run verify:catalog && npm run verify:assetlib && npm run verify:mcp",
|
|
115
|
+
"verify:catalog": "node scripts/verify-catalog.mjs",
|
|
116
|
+
"verify:assetlib": "node scripts/verify-assetlib.mjs",
|
|
117
|
+
"verify:mcp": "node scripts/verify-mcp.mjs",
|
|
118
|
+
"thumbs:prop": "node scripts/generate-prop-thumbs.mjs",
|
|
119
|
+
"thumbs:building": "node scripts/generate-building-thumbs.mjs",
|
|
120
|
+
"thumbs:catalog": "node scripts/generate-catalog-thumbs.mjs",
|
|
121
|
+
"thumbs:home": "node scripts/generate-home-shots.mjs"
|
|
64
122
|
},
|
|
65
123
|
"peerDependencies": {
|
|
66
124
|
"@pixiv/three-vrm": "^3.5.4",
|
|
125
|
+
"three": "^0.185.1",
|
|
67
126
|
"three-mesh-bvh": "^0.8.3",
|
|
68
|
-
"three-stdlib": "^2.36.1"
|
|
69
|
-
"three": "^0.185.1"
|
|
127
|
+
"three-stdlib": "^2.36.1"
|
|
70
128
|
},
|
|
71
129
|
"peerDependenciesMeta": {
|
|
72
130
|
"@pixiv/three-vrm": {
|
|
@@ -87,6 +145,7 @@
|
|
|
87
145
|
"@vitejs/plugin-react": "^4.7.0",
|
|
88
146
|
"ecctrl": "^1.0.97",
|
|
89
147
|
"lucide-react": "^1.23.0",
|
|
148
|
+
"marked": "^18.0.6",
|
|
90
149
|
"playwright": "^1.60.0",
|
|
91
150
|
"react": "^19.2.7",
|
|
92
151
|
"react-dom": "^19.2.7",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Ambient VFX — integration snippets
|
|
2
|
+
|
|
3
|
+
The `src/ambientfx/` cluster is self-contained; these are the EXACT edits to
|
|
4
|
+
shared files that wire it into the library. Apply verbatim.
|
|
5
|
+
|
|
6
|
+
## 1. `src/index.js` — root export
|
|
7
|
+
|
|
8
|
+
Add after the `stylizedTerrain.js` line:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
export * from './ambientfx/index.js';
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 2. `package.json` — subpath export
|
|
15
|
+
|
|
16
|
+
Add to `"exports"` after `"./pathgen"`:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
"./ambientfx": "./src/ambientfx/index.js",
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 3. `vite.config.js` — package alias
|
|
23
|
+
|
|
24
|
+
Add `'ambientfx'` to the subpath array (order inside the array doesn't
|
|
25
|
+
matter; subpath entries must stay before the bare root entry, which the array
|
|
26
|
+
already guarantees):
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
...['toon', 'environment', 'water', 'vegetation', 'sky', 'post', 'rockgen',
|
|
30
|
+
'debrisgen', 'pathgen', 'texgen', 'character', 'loaders', 'debug', 'ambientfx'].map((subpath) => ({
|
|
31
|
+
find: `@call-me-sensei/toonlab/${subpath}`,
|
|
32
|
+
replacement: resolve(__dirname, `src/${subpath}/index.js`),
|
|
33
|
+
})),
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Once aliased, `examples/ambientfx-demo/main.js` line
|
|
37
|
+
`import { createAmbientFx } from '../../src/ambientfx/index.js';` can become
|
|
38
|
+
`import { createAmbientFx } from '@call-me-sensei/toonlab/ambientfx';`.
|
|
39
|
+
|
|
40
|
+
## 4. `src/stylizedWorld.js` — `createStylizedWorld({ ambientfx })` wiring
|
|
41
|
+
|
|
42
|
+
Import at the top:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { createAmbientFx } from './ambientfx/index.js';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Add an `ambientfx = false` option to the destructured signature. After the
|
|
49
|
+
grass/flowers blocks (so `heightFogParams`, `environmentBox`, `waterSurface`,
|
|
50
|
+
and `followTarget` are in scope), create the system — the important part is
|
|
51
|
+
that it RECEIVES the world's wind, height-fog, sun, and time-of-day instead
|
|
52
|
+
of inventing its own:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// Ambient atmosphere: petals/leaves/fireflies/pollen/mist over one GPU
|
|
56
|
+
// particle backbone, in a follow window like the grass.
|
|
57
|
+
let ambientFx = null;
|
|
58
|
+
if (ambientfx) {
|
|
59
|
+
const fxOptions = cleanObject(ambientfx);
|
|
60
|
+
ambientFx = createAmbientFx({
|
|
61
|
+
followTarget,
|
|
62
|
+
heightAt,
|
|
63
|
+
seed: Number(fxOptions.seed) || 1,
|
|
64
|
+
waterLevel: waterSurface ? waterLevel : null,
|
|
65
|
+
...fxOptions,
|
|
66
|
+
});
|
|
67
|
+
// One wind for the whole world: reuse the exact grass wind settings.
|
|
68
|
+
const grassWind = grassField?.settings ?? {};
|
|
69
|
+
ambientFx.setWind({
|
|
70
|
+
windDirection: grassWind.windDirection,
|
|
71
|
+
windSpeed: grassWind.windSpeed,
|
|
72
|
+
windStrength: grassWind.windStrength,
|
|
73
|
+
});
|
|
74
|
+
// Same height-fog layer terrain/water/forest already share.
|
|
75
|
+
ambientFx.setDistanceFog({
|
|
76
|
+
color: heightFogParams.heightFogColor ?? [0.66, 0.8, 0.94],
|
|
77
|
+
density: Number(heightFogParams.heightFogDensity) || 0.0016,
|
|
78
|
+
falloff: Number(heightFogParams.heightFogFalloff) || 400,
|
|
79
|
+
floorY: environmentBox.min.y,
|
|
80
|
+
});
|
|
81
|
+
if (sunDirection) ambientFx.setSun({ direction: sunDirection });
|
|
82
|
+
scene.add(ambientFx.root);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In the returned object add `ambientFx,` and in `world.update(delta)` add:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
ambientFx?.update(delta, camera);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
In `dispose()` add `ambientFx?.dispose();` and include `ambientFx?.root` in
|
|
93
|
+
the parent-removal list.
|
|
94
|
+
|
|
95
|
+
Time of day: hosts that drive `applyEnvironmentTimeOfDay(state, ...)` should
|
|
96
|
+
also call `world.ambientFx?.setTimeOfDay(state.hour)` in the same place (or
|
|
97
|
+
pass `ambientfx: { timeOfDay: () => clock.hour }` and never think about it) —
|
|
98
|
+
fireflies/pollen/mist gates follow that one canonical hour.
|
|
99
|
+
|
|
100
|
+
Bloom sources (petals from flowering canopies, leaves from autumn ones) are
|
|
101
|
+
host-registered because only the host knows which recipes flower:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
world.ambientFx?.addBloomSources(
|
|
105
|
+
floweringTreePlacements.map((p) => ({
|
|
106
|
+
color: [1.0, 0.66, 0.8], // the recipe's bloom color
|
|
107
|
+
effect: 'petals', // or 'leaves' for autumn canopies
|
|
108
|
+
radius: treeSize * 0.85,
|
|
109
|
+
x: p.x, y: p.y + treeSize * 1.6, z: p.z,
|
|
110
|
+
})),
|
|
111
|
+
);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Water note: the mist mesh carries `userData.waterExclude` itself; fireflies
|
|
115
|
+
and petals deliberately stay in the water passes so they reflect. Verify the
|
|
116
|
+
firefly reflection reads once wired into a world with water (the standalone
|
|
117
|
+
demo has no water surface).
|
|
118
|
+
|
|
119
|
+
## 5. `scripts/generate-settings-reference.mjs` — MODULES entry
|
|
120
|
+
|
|
121
|
+
Add to the `MODULES` array (after the Paths entry):
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
{
|
|
125
|
+
title: 'Ambient VFX',
|
|
126
|
+
subpath: 'toonlab/ambientfx',
|
|
127
|
+
module: '/src/ambientfx/ambientFxSettings.js',
|
|
128
|
+
groups: 'AMBIENTFX_SETTING_GROUPS',
|
|
129
|
+
schema: 'AMBIENTFX_SETTING_FIELD_SCHEMA',
|
|
130
|
+
note: 'Settings are nested per group: `createAmbientFx({ settings: { fireflies: { blinkSpeed: 0.8 } } })`. Effect entries in `effects` override their group; `density` there is a multiplier.',
|
|
131
|
+
},
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 6. `AGENTS.md` — subpath list line
|
|
135
|
+
|
|
136
|
+
In the "Subpath imports" paragraph, add `/ambientfx` to the list, e.g.:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
`/toon` `/environment` `/water` `/vegetation` (incl. scatter helpers +
|
|
140
|
+
`StylizedForest`) `/sky` `/post` `/character` `/loaders` `/rockgen`
|
|
141
|
+
`/debrisgen` `/pathgen` `/ambientfx` (petals/leaves/fireflies/pollen/mist —
|
|
142
|
+
one particle backbone, 3 draw calls, follow-window like grass) `/debug`; ...
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 7. `examples/outdoor-world/main.js` — `?nofx` perf toggle sketch
|
|
146
|
+
|
|
147
|
+
With the world wiring from §4 the example only needs the toggle plus the
|
|
148
|
+
per-effect URL knobs (matching the existing `?nograss`/`?notrees` idiom):
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
// Ambient atmosphere (?nofx=1 disables; ?fireflies=2 &petals=0 etc.
|
|
152
|
+
// scale/kill individual effects for perf triage).
|
|
153
|
+
ambientfx: params.has('nofx') ? false : {
|
|
154
|
+
effects: Object.fromEntries(['petals', 'leaves', 'fireflies', 'pollen', 'mist']
|
|
155
|
+
.map((id) => [id, params.has(id)
|
|
156
|
+
? (Number(params.get(id)) > 0 ? { density: Number(params.get(id)) } : false)
|
|
157
|
+
: true])),
|
|
158
|
+
seed: WORLD.seed,
|
|
159
|
+
timeOfDay: () => 14, // swap for the game clock when one exists
|
|
160
|
+
},
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
…passed inside the existing `createStylizedWorld({ ... })` options object.
|
|
164
|
+
Update the `#hint` copy to mention `?nofx`.
|