@call-me-sensei/toonlab 0.1.1 → 0.2.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 +131 -0
- package/README.md +73 -0
- package/package.json +3 -2
- package/src/debrisgen/debrisPresets.js +7 -0
- package/src/environment/environmentPresets.js +19 -0
- package/src/environment/environmentSettings.js +10 -0
- package/src/environment/environmentTextureResolver.js +3 -0
- package/src/environment/index.js +1 -0
- package/src/index.js +7 -0
- package/src/post/postProcessing.js +28 -0
- package/src/rockgen/rockgenPresets.js +8 -0
- package/src/rockgen/rockgenSettings.js +47 -0
- package/src/shaders-tsl/environment.js +60 -0
- package/src/shaders-tsl/water.js +54 -1
- package/src/sky/stylizedSky.js +49 -1
- package/src/stylizedTerrain.js +601 -0
- package/src/stylizedWorld.js +500 -0
- package/src/vegetation/index.js +2 -0
- package/src/vegetation/scatter.js +297 -0
- package/src/vegetation/stylizedFlowers.js +48 -1
- package/src/vegetation/stylizedForest.js +448 -0
- package/src/vegetation/stylizedGrass.js +50 -2
- package/src/vegetation/stylizedTree.js +71 -2
- package/src/vegetation/stylizedTreeFoliage.js +11 -2
- package/src/water/waterScenePasses.js +5 -1
- package/src/water/waterSettings.js +10 -0
- package/src/water/waterSurface.js +27 -0
- package/src/worldCollision.js +94 -0
- package/src/worldMinimap.js +152 -0
- package/src/worldPresets.js +122 -0
package/src/shaders-tsl/water.js
CHANGED
|
@@ -33,7 +33,9 @@ import {
|
|
|
33
33
|
cameraProjectionMatrix,
|
|
34
34
|
cameraViewMatrix,
|
|
35
35
|
clamp,
|
|
36
|
+
Discard,
|
|
36
37
|
dot,
|
|
38
|
+
exp,
|
|
37
39
|
float,
|
|
38
40
|
Fn,
|
|
39
41
|
If,
|
|
@@ -139,6 +141,21 @@ export function createWaterNodeMaterial({
|
|
|
139
141
|
// `.value` like ShaderMaterial uniform entries; array uniforms are
|
|
140
142
|
// wrapped — see waterArrayUniformEntry) ----
|
|
141
143
|
const u = {
|
|
144
|
+
// Manual scene-fog (linear, matches THREE.Fog). material.fog stays false
|
|
145
|
+
// (see the fog-ordering TODO above); WaterSurface.update() mirrors
|
|
146
|
+
// scene.fog into these each frame so distant water fades into the same
|
|
147
|
+
// haze as everything else instead of cutting sharp against fogged
|
|
148
|
+
// terrain. far <= near disables.
|
|
149
|
+
uSceneFogColor: uniform(new THREE.Color(0.72, 0.83, 0.94)),
|
|
150
|
+
uSceneFogFar: uniform(0),
|
|
151
|
+
uSceneFogNear: uniform(0),
|
|
152
|
+
// Exponential distance fog matching the environment shader's height fog
|
|
153
|
+
// (water sits at the bottom of the height falloff, so the depth term is
|
|
154
|
+
// the whole story). Without this, water at the far shore stays bright
|
|
155
|
+
// while terrain behind it hazes out — reading like water slicing into
|
|
156
|
+
// the mountains. density 0 disables.
|
|
157
|
+
uDistanceFogColor: uniform(new THREE.Color(0.66, 0.8, 0.94)),
|
|
158
|
+
uDistanceFogDensity: uniform(0),
|
|
142
159
|
uTime: uniform(0),
|
|
143
160
|
uResolution: uniform(new THREE.Vector2(1, 1)),
|
|
144
161
|
uCameraNear: uniform(0.05),
|
|
@@ -244,6 +261,10 @@ export function createWaterNodeMaterial({
|
|
|
244
261
|
const vBreaker = varying(float(), 'vWaterBreaker');
|
|
245
262
|
const vSwash = varying(float(), 'vWaterSwash');
|
|
246
263
|
const vChop = varying(float(), 'vWaterChop');
|
|
264
|
+
// Rest-pose column depth (waterline − bed). Deeply negative = the surface
|
|
265
|
+
// is under dry land; the fragment shader discards those fragments so
|
|
266
|
+
// triangles spanning a cliff face can't poke through the terrain.
|
|
267
|
+
const vRestDepth = varying(float(), 'vWaterRestDepth');
|
|
247
268
|
|
|
248
269
|
material.vertexNode = Fn(() => {
|
|
249
270
|
const worldPosition = modelWorldMatrix.mul(vec4(positionGeometry, 1.0)).toVar();
|
|
@@ -263,6 +284,7 @@ export function createWaterNodeMaterial({
|
|
|
263
284
|
if (flags.shoaling) {
|
|
264
285
|
const aBedHeight = attribute('aBedHeight', 'float');
|
|
265
286
|
const restDepth = worldPosition.y.sub(aBedHeight).toVar();
|
|
287
|
+
vRestDepth.assign(restDepth);
|
|
266
288
|
// Big swells feel the bottom much deeper than small ones.
|
|
267
289
|
const shoalRange = max(max(u.uShoalingDepth, u.uWaveEnergy.mul(2.2)), 1e-3).toVar();
|
|
268
290
|
chopWeight.assign(mix(0.15, 1.0, smoothstep(shoalRange.mul(0.3), shoalRange.mul(1.4), restDepth)));
|
|
@@ -289,7 +311,14 @@ export function createWaterNodeMaterial({
|
|
|
289
311
|
const runup = u.uShorelineRunup.mul(u.uWaveEnergy).mul(max(wavePhase, 0.0));
|
|
290
312
|
// Dry land tucks the film 20cm under the terrain.
|
|
291
313
|
const film = clamp(restDepth.add(runup), -0.2, 0.05).toVar();
|
|
292
|
-
|
|
314
|
+
// Cap the drape: the film follows the bed up the beach, but a steep
|
|
315
|
+
// bank (terraced shore, cliff base) puts a whole cliff step inside one
|
|
316
|
+
// mesh cell — the dry vertex would ride 10m+ up the slope and drag
|
|
317
|
+
// surviving near-shore fragments with it (giant white "iceberg"
|
|
318
|
+
// wedges at distant shorelines). Real swash never climbs above ~½m
|
|
319
|
+
// over the rest waterline, so clamp the geometry there and let the
|
|
320
|
+
// vRestDepth fragment discard own the rest.
|
|
321
|
+
const filmY = min(aBedHeight.sub(worldPosition.y).add(film), 0.5);
|
|
293
322
|
const beach = smoothstep(-0.02, 0.06, restDepth).oneMinus().toVar();
|
|
294
323
|
|
|
295
324
|
rippleShoal.assign(smoothstep(0.0, 0.12, restDepth));
|
|
@@ -301,6 +330,7 @@ export function createWaterNodeMaterial({
|
|
|
301
330
|
rawWave.z.mul(shoal),
|
|
302
331
|
));
|
|
303
332
|
} else {
|
|
333
|
+
vRestDepth.assign(float(1000.0));
|
|
304
334
|
rawWave = waves.gerstnerDisplacementFiltered(restXZ, u.uTime, chopWeight).toVar();
|
|
305
335
|
waveDisplacement.assign(rawWave);
|
|
306
336
|
}
|
|
@@ -324,6 +354,12 @@ export function createWaterNodeMaterial({
|
|
|
324
354
|
|
|
325
355
|
// ---- Fragment stage (water.frag.glsl) ----
|
|
326
356
|
material.fragmentNode = Fn(() => {
|
|
357
|
+
// Surface sections tucked under dry land (see vRestDepth) never shade:
|
|
358
|
+
// without this, water triangles spanning steep banks slice through the
|
|
359
|
+
// terrain as visible shards.
|
|
360
|
+
if (flags.shoaling) {
|
|
361
|
+
Discard(vRestDepth.lessThan(-0.35));
|
|
362
|
+
}
|
|
327
363
|
const screenUv = screenUV.toVar();
|
|
328
364
|
const toCamera = cameraPosition.sub(vWorldPosition).toVar();
|
|
329
365
|
const viewDir = normalize(toCamera).toVar();
|
|
@@ -460,6 +496,23 @@ export function createWaterNodeMaterial({
|
|
|
460
496
|
alpha.assign(max(alpha, vSwash.mul(0.55)));
|
|
461
497
|
alpha.assign(max(alpha, clamp(reflectionMix.add(fresnel.mul(0.4)), 0.0, 1.0).mul(0.85)));
|
|
462
498
|
|
|
499
|
+
// Fog last: foam, sparkles, and shorelines haze out with the rest of
|
|
500
|
+
// the world. Exponential term mirrors the environment height fog;
|
|
501
|
+
// linear term mirrors scene.fog.
|
|
502
|
+
If(u.uDistanceFogDensity.greaterThan(0.0), () => {
|
|
503
|
+
const depthTerm = exp(viewDistance.mul(u.uDistanceFogDensity).negate()).oneMinus();
|
|
504
|
+
litColor.assign(mix(litColor, u.uDistanceFogColor, clamp(depthTerm, 0.0, 1.0)));
|
|
505
|
+
});
|
|
506
|
+
If(u.uSceneFogFar.greaterThan(u.uSceneFogNear), () => {
|
|
507
|
+
const fogAmount = clamp(
|
|
508
|
+
viewDistance.sub(u.uSceneFogNear)
|
|
509
|
+
.div(max(u.uSceneFogFar.sub(u.uSceneFogNear), 1e-3)),
|
|
510
|
+
0.0,
|
|
511
|
+
1.0,
|
|
512
|
+
);
|
|
513
|
+
litColor.assign(mix(litColor, u.uSceneFogColor, fogAmount));
|
|
514
|
+
});
|
|
515
|
+
|
|
463
516
|
// --- debug visualizations (?waterDebug=...) ---
|
|
464
517
|
// Masked sum instead of an if/else chain (docs/tsl-conventions.md #3);
|
|
465
518
|
// always compiled — every input already lives in the graph.
|
package/src/sky/stylizedSky.js
CHANGED
|
@@ -35,6 +35,53 @@ export const DEFAULT_SKY_SETTINGS = Object.freeze({
|
|
|
35
35
|
starsStrength: 0.0,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
// Named sky presets: 'default' is the baseline; 'call_me_sensei' is the
|
|
39
|
+
// studio-managed signature look, curated and updated over releases.
|
|
40
|
+
// Community presets register alongside them via registerSkyPreset().
|
|
41
|
+
const skyPresetRegistry = new Map([
|
|
42
|
+
['default', Object.freeze({
|
|
43
|
+
description: 'Baseline daytime sky.',
|
|
44
|
+
label: 'Default',
|
|
45
|
+
settings: Object.freeze({}),
|
|
46
|
+
})],
|
|
47
|
+
['call_me_sensei', Object.freeze({
|
|
48
|
+
description: 'Studio-managed signature sky, curated by Call Me Sensei and updated over releases. Currently the tuned library defaults.',
|
|
49
|
+
label: 'Call Me Sensei',
|
|
50
|
+
settings: Object.freeze({}),
|
|
51
|
+
})],
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Registers a named sky preset so it resolves in `createSkySettings({
|
|
56
|
+
* preset })` exactly like the built-ins. Accepts `{ label?, description?,
|
|
57
|
+
* settings? }` or flat settings.
|
|
58
|
+
*/
|
|
59
|
+
export function registerSkyPreset(name, preset = {}, { overwrite = false } = {}) {
|
|
60
|
+
const id = String(name ?? '').trim();
|
|
61
|
+
if (!id) throw new Error('Sky preset name is required.');
|
|
62
|
+
if (!overwrite && skyPresetRegistry.has(id)) {
|
|
63
|
+
throw new Error(`Sky preset "${id}" already exists.`);
|
|
64
|
+
}
|
|
65
|
+
const source = preset && typeof preset === 'object' ? preset : {};
|
|
66
|
+
const { label, description, settings, ...flat } = source;
|
|
67
|
+
const entry = Object.freeze({
|
|
68
|
+
description: typeof description === 'string' ? description : '',
|
|
69
|
+
label: typeof label === 'string' && label ? label : id,
|
|
70
|
+
settings: Object.freeze({ ...(settings && typeof settings === 'object' ? settings : flat) }),
|
|
71
|
+
});
|
|
72
|
+
skyPresetRegistry.set(id, entry);
|
|
73
|
+
return { description: entry.description, id, label: entry.label };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Lists registered sky presets as `{ id, label, description }` (for HUDs). */
|
|
77
|
+
export function getSkyPresetOptions() {
|
|
78
|
+
return Array.from(skyPresetRegistry.entries()).map(([id, preset]) => ({
|
|
79
|
+
description: preset.description,
|
|
80
|
+
id,
|
|
81
|
+
label: preset.label,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
38
85
|
function finiteNumber(value, fallback) {
|
|
39
86
|
const number = Number(value);
|
|
40
87
|
return Number.isFinite(number) ? number : fallback;
|
|
@@ -77,7 +124,8 @@ function vector3Array(value, fallback) {
|
|
|
77
124
|
*/
|
|
78
125
|
export function createSkySettings(options = {}) {
|
|
79
126
|
const source = options && typeof options === 'object' ? options : {};
|
|
80
|
-
const
|
|
127
|
+
const presetSettings = skyPresetRegistry.get(source.preset)?.settings;
|
|
128
|
+
const base = presetSettings ? { ...DEFAULT_SKY_SETTINGS, ...presetSettings } : DEFAULT_SKY_SETTINGS;
|
|
81
129
|
return {
|
|
82
130
|
radius: Math.max(finiteNumber(source.radius, base.radius), 0.1),
|
|
83
131
|
zenithColor: colorArray(source.zenithColor, base.zenithColor),
|