@call-me-sensei/toonlab 0.1.1

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.
Files changed (146) hide show
  1. package/ATTRIBUTION.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/package.json +97 -0
  5. package/src/character/characterRig.js +353 -0
  6. package/src/character/freestyleSwimClip.js +541 -0
  7. package/src/character/index.js +4 -0
  8. package/src/character/modelLoader.js +433 -0
  9. package/src/core/materialRoles.js +438 -0
  10. package/src/core/presetDocuments.js +110 -0
  11. package/src/core/shaderBackend.js +24 -0
  12. package/src/debrisgen/debrisFields.js +544 -0
  13. package/src/debrisgen/debrisGenerator.js +1929 -0
  14. package/src/debrisgen/debrisPalettes.js +71 -0
  15. package/src/debrisgen/debrisPhysics.js +198 -0
  16. package/src/debrisgen/debrisPresets.js +155 -0
  17. package/src/debrisgen/debrisSettings.js +333 -0
  18. package/src/debrisgen/debrisTextures.js +380 -0
  19. package/src/debrisgen/index.js +5 -0
  20. package/src/debug/fieldValues.js +95 -0
  21. package/src/debug/index.js +7 -0
  22. package/src/debug/settingsPanel.js +170 -0
  23. package/src/environment/environmentAmbientProbe.js +160 -0
  24. package/src/environment/environmentMaterialAdapter.js +336 -0
  25. package/src/environment/environmentMaterialClassifier.js +178 -0
  26. package/src/environment/environmentPlanarReflection.js +201 -0
  27. package/src/environment/environmentPresets.js +443 -0
  28. package/src/environment/environmentRigs.js +684 -0
  29. package/src/environment/environmentSettings.js +504 -0
  30. package/src/environment/environmentShaderMaterials.js +318 -0
  31. package/src/environment/environmentSunShadowPass.js +268 -0
  32. package/src/environment/environmentTextureResolver.js +161 -0
  33. package/src/environment/environmentTimeOfDay.js +167 -0
  34. package/src/environment/environmentVertexAo.js +188 -0
  35. package/src/environment/index.js +8 -0
  36. package/src/environment/scanAssetStylize.js +103 -0
  37. package/src/index.js +21 -0
  38. package/src/loaders/index.js +1 -0
  39. package/src/post/index.js +2 -0
  40. package/src/post/postProcessing.js +1087 -0
  41. package/src/rockgen/export/glbExport.js +177 -0
  42. package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
  43. package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
  44. package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
  45. package/src/rockgen/index.js +14 -0
  46. package/src/rockgen/mesh/meshAttributes.js +374 -0
  47. package/src/rockgen/mesh/meshDocument.js +158 -0
  48. package/src/rockgen/mesh/surfaceNets.js +329 -0
  49. package/src/rockgen/noise/cellularNoise3.js +92 -0
  50. package/src/rockgen/noise/prng.js +62 -0
  51. package/src/rockgen/noise/simplexNoise3.js +89 -0
  52. package/src/rockgen/noise/valueNoise3.js +87 -0
  53. package/src/rockgen/rockDocument.js +255 -0
  54. package/src/rockgen/rockHelpers.js +8 -0
  55. package/src/rockgen/rockgenPresets.js +559 -0
  56. package/src/rockgen/rockgenSettings.js +756 -0
  57. package/src/rockgen/sdf/fieldCompiler.js +544 -0
  58. package/src/rockgen/sdf/sculptEdits.js +66 -0
  59. package/src/rockgen/sdf/sdfModifiers.js +45 -0
  60. package/src/rockgen/sdf/sdfOps.js +37 -0
  61. package/src/rockgen/sdf/sdfPrimitives.js +88 -0
  62. package/src/shaders-tsl/anime.js +963 -0
  63. package/src/shaders-tsl/chunks/character-color.js +64 -0
  64. package/src/shaders-tsl/chunks/character-highlights.js +174 -0
  65. package/src/shaders-tsl/chunks/character-lighting.js +309 -0
  66. package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
  67. package/src/shaders-tsl/chunks/character-roles.js +52 -0
  68. package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
  69. package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
  70. package/src/shaders-tsl/chunks/character-skinning.js +140 -0
  71. package/src/shaders-tsl/chunks/environment-color.js +47 -0
  72. package/src/shaders-tsl/chunks/environment-debug.js +79 -0
  73. package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
  74. package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
  75. package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
  76. package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
  77. package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
  78. package/src/shaders-tsl/chunks/water-color.js +190 -0
  79. package/src/shaders-tsl/chunks/water-common.js +124 -0
  80. package/src/shaders-tsl/chunks/water-foam.js +93 -0
  81. package/src/shaders-tsl/chunks/water-lighting.js +116 -0
  82. package/src/shaders-tsl/chunks/water-ripple.js +64 -0
  83. package/src/shaders-tsl/chunks/water-waves.js +100 -0
  84. package/src/shaders-tsl/environment-ao-overlay.js +60 -0
  85. package/src/shaders-tsl/environment.js +680 -0
  86. package/src/shaders-tsl/flower.js +267 -0
  87. package/src/shaders-tsl/grass.js +193 -0
  88. package/src/shaders-tsl/post-composite.js +461 -0
  89. package/src/shaders-tsl/sky.js +175 -0
  90. package/src/shaders-tsl/tree-leaf.js +264 -0
  91. package/src/shaders-tsl/water-breaker.js +423 -0
  92. package/src/shaders-tsl/water-kelp.js +171 -0
  93. package/src/shaders-tsl/water-rain.js +107 -0
  94. package/src/shaders-tsl/water-simulation.js +140 -0
  95. package/src/shaders-tsl/water-splash.js +298 -0
  96. package/src/shaders-tsl/water.js +509 -0
  97. package/src/sky/index.js +2 -0
  98. package/src/sky/stylizedSky.js +341 -0
  99. package/src/toon/characterRenderPasses.js +646 -0
  100. package/src/toon/index.js +4 -0
  101. package/src/toon/settings/alphaSettings.js +170 -0
  102. package/src/toon/settings/averageShadowSettings.js +181 -0
  103. package/src/toon/settings/baseTextureSettings.js +133 -0
  104. package/src/toon/settings/celShadeSettings.js +96 -0
  105. package/src/toon/settings/contactShadowSettings.js +114 -0
  106. package/src/toon/settings/eyeHighlightSettings.js +187 -0
  107. package/src/toon/settings/faceLightingSettings.js +135 -0
  108. package/src/toon/settings/furSettings.js +112 -0
  109. package/src/toon/settings/glitterSettings.js +102 -0
  110. package/src/toon/settings/hairHighlightSettings.js +325 -0
  111. package/src/toon/settings/indirectLightSettings.js +244 -0
  112. package/src/toon/settings/localLightSettings.js +176 -0
  113. package/src/toon/settings/materialMapSettings.js +299 -0
  114. package/src/toon/settings/outlineSettings.js +263 -0
  115. package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
  116. package/src/toon/settings/rimLightSettings.js +268 -0
  117. package/src/toon/settings/sceneShadowSettings.js +147 -0
  118. package/src/toon/settings/selfShadowSettings.js +221 -0
  119. package/src/toon/settings/shadowColorSettings.js +238 -0
  120. package/src/toon/settings/skinToneSettings.js +150 -0
  121. package/src/toon/settings/specularSettings.js +314 -0
  122. package/src/toon/settings/stickerSettings.js +102 -0
  123. package/src/toon/toonMaterialAdapter.js +2235 -0
  124. package/src/toon/toonSettings.js +860 -0
  125. package/src/vegetation/flowerSpecies.js +260 -0
  126. package/src/vegetation/index.js +10 -0
  127. package/src/vegetation/stylizedBush.js +136 -0
  128. package/src/vegetation/stylizedFlower.js +213 -0
  129. package/src/vegetation/stylizedFlowers.js +297 -0
  130. package/src/vegetation/stylizedGrass.js +491 -0
  131. package/src/vegetation/stylizedTree.js +2449 -0
  132. package/src/vegetation/stylizedTreeFoliage.js +691 -0
  133. package/src/vegetation/treeExport.js +300 -0
  134. package/src/vegetation/treeRecipe.js +647 -0
  135. package/src/water/index.js +2 -0
  136. package/src/water/water.js +20 -0
  137. package/src/water/waterBreakerSystem.js +591 -0
  138. package/src/water/waterInteraction.js +193 -0
  139. package/src/water/waterMaterial.js +263 -0
  140. package/src/water/waterRain.js +136 -0
  141. package/src/water/waterRippleSimulation.js +227 -0
  142. package/src/water/waterScenePasses.js +415 -0
  143. package/src/water/waterSettings.js +1138 -0
  144. package/src/water/waterSplashSystem.js +251 -0
  145. package/src/water/waterSurface.js +475 -0
  146. package/src/water/waterVegetation.js +83 -0
@@ -0,0 +1,177 @@
1
+ // GLB export for generated rocks. Exports carry the field-derived vertex
2
+ // colors and SDF ambient occlusion (asset-intrinsic self-occlusion only —
3
+ // no scene/ground contact baked in), so the asset is portable and re-imports
4
+ // into this repo's environment pipeline cleanly.
5
+ //
6
+ // Browser-only: GLTFExporter assembles the GLB container through FileReader,
7
+ // which node lacks. Everything else in the cluster (meshing, hashing,
8
+ // serialization) runs in node — see scripts/verify-rockgen.mjs.
9
+
10
+ import * as THREE from 'three';
11
+ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
12
+
13
+ import { meshDocument } from '../mesh/meshDocument.js';
14
+
15
+ // glTF requires custom attribute names to start with an underscore.
16
+ const AO_EXPORT_ATTRIBUTE = '_ROCK_AO';
17
+
18
+ function packAoIntoColorAlpha(geometry) {
19
+ const color = geometry.getAttribute('color');
20
+ const ao = geometry.getAttribute('envVertexAo');
21
+ const rgba = new Float32Array((color.count) * 4);
22
+ for (let v = 0; v < color.count; v += 1) {
23
+ rgba[v * 4] = color.getX(v);
24
+ rgba[v * 4 + 1] = color.getY(v);
25
+ rgba[v * 4 + 2] = color.getZ(v);
26
+ rgba[v * 4 + 3] = ao ? ao.getX(v) : 1;
27
+ }
28
+ geometry.setAttribute('color', new THREE.BufferAttribute(rgba, 4));
29
+ }
30
+
31
+ /**
32
+ * Prepares an export copy of a rockgen geometry: AO is packed per `ao` —
33
+ * 'colorAlpha' (default; COLOR_0 alpha, portable and round-trippable),
34
+ * 'attribute' (`_ROCK_AO` custom attribute for in-house pipelines), or
35
+ * 'omit'. The nonstandard `envVertexAo` attribute never ships raw.
36
+ */
37
+ // Box-projection UVs from the bounding box: each vertex projects along
38
+ // its normal's dominant axis. Seams land on the (already sharp) axis
39
+ // creases — fine for detail/tiling textures; engines wanting seamless
40
+ // texturing should triplanar in-shader instead.
41
+ function addBoxProjectionUvs(geometry) {
42
+ geometry.computeBoundingBox();
43
+ const box = geometry.boundingBox;
44
+ const size = [
45
+ Math.max(box.max.x - box.min.x, 1e-6),
46
+ Math.max(box.max.y - box.min.y, 1e-6),
47
+ Math.max(box.max.z - box.min.z, 1e-6),
48
+ ];
49
+ const position = geometry.getAttribute('position');
50
+ const normal = geometry.getAttribute('normal');
51
+ const uv = new Float32Array(position.count * 2);
52
+ for (let v = 0; v < position.count; v += 1) {
53
+ const nx = Math.abs(normal.getX(v));
54
+ const ny = Math.abs(normal.getY(v));
55
+ const nz = Math.abs(normal.getZ(v));
56
+ let u;
57
+ let w;
58
+ if (nx >= ny && nx >= nz) {
59
+ u = (position.getZ(v) - box.min.z) / size[2];
60
+ w = (position.getY(v) - box.min.y) / size[1];
61
+ } else if (ny >= nx && ny >= nz) {
62
+ u = (position.getX(v) - box.min.x) / size[0];
63
+ w = (position.getZ(v) - box.min.z) / size[2];
64
+ } else {
65
+ u = (position.getX(v) - box.min.x) / size[0];
66
+ w = (position.getY(v) - box.min.y) / size[1];
67
+ }
68
+ uv[v * 2] = u;
69
+ uv[v * 2 + 1] = w;
70
+ }
71
+ geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
72
+ }
73
+
74
+ export function prepareGeometryForExport(geometry, { ao = 'colorAlpha', uv = 'none' } = {}) {
75
+ const exportGeometry = geometry.clone();
76
+ if (ao === 'colorAlpha' && exportGeometry.getAttribute('color')) {
77
+ packAoIntoColorAlpha(exportGeometry);
78
+ } else if (ao === 'attribute' && exportGeometry.getAttribute('envVertexAo')) {
79
+ exportGeometry.setAttribute(AO_EXPORT_ATTRIBUTE, exportGeometry.getAttribute('envVertexAo'));
80
+ }
81
+ exportGeometry.deleteAttribute('envVertexAo');
82
+ if (uv === 'box') addBoxProjectionUvs(exportGeometry);
83
+ return exportGeometry;
84
+ }
85
+
86
+ /**
87
+ * Exports a geometry (or a ready-made mesh/scene) as a binary GLB.
88
+ * Geometries are wrapped in a vertex-colored standard material so viewers
89
+ * outside this repo show the baked look.
90
+ *
91
+ * @returns {Promise<ArrayBuffer>}
92
+ */
93
+ export async function exportGeometryToGLB(geometryOrObject, {
94
+ ao = 'colorAlpha', name = 'rock', uv = 'none',
95
+ } = {}) {
96
+ let target = geometryOrObject;
97
+ if (target?.isBufferGeometry) {
98
+ const material = new THREE.MeshStandardMaterial({
99
+ metalness: 0,
100
+ roughness: 0.95,
101
+ vertexColors: true,
102
+ });
103
+ target = new THREE.Mesh(prepareGeometryForExport(target, { ao, uv }), material);
104
+ target.name = name;
105
+ }
106
+ const exporter = new GLTFExporter();
107
+ return exporter.parseAsync(target, { binary: true });
108
+ }
109
+
110
+ /**
111
+ * Meshes the document at export resolution and exports it as GLB.
112
+ *
113
+ * With `lods` (default: the document's meshing.exportLods, on unless
114
+ * disabled) the GLB carries three meshes named <name>_LOD0/1/2 at full,
115
+ * half and quarter grid resolution. Because LODs are independent SDF
116
+ * re-meshes — not decimations of LOD0 — every level keeps clean toon
117
+ * silhouettes and its own correctly baked colors/AO.
118
+ *
119
+ * `uv: 'box'` adds box-projection UVs for detail textures (see
120
+ * addBoxProjectionUvs for the trade-off).
121
+ *
122
+ * @returns {Promise<ArrayBuffer>}
123
+ */
124
+ export async function exportDocumentToGLB(document, {
125
+ ao = 'colorAlpha',
126
+ lods = null,
127
+ name = null,
128
+ normals = null,
129
+ resolution = null,
130
+ uv = 'none',
131
+ } = {}) {
132
+ const exportResolution = resolution ?? document.meshing.exportResolution;
133
+ const exportName = name ?? document.name;
134
+ const wantLods = lods ?? document.meshing.exportLods !== false;
135
+
136
+ const makeMesh = (res, meshName) => {
137
+ const geometry = meshDocument(document, { includeHelpers: false, normals, resolution: res });
138
+ const material = new THREE.MeshStandardMaterial({
139
+ metalness: 0,
140
+ roughness: 0.95,
141
+ vertexColors: true,
142
+ });
143
+ const mesh = new THREE.Mesh(prepareGeometryForExport(geometry, { ao, uv }), material);
144
+ mesh.name = meshName;
145
+ return mesh;
146
+ };
147
+
148
+ if (!wantLods) {
149
+ const geometry = meshDocument(document, { includeHelpers: false, normals, resolution: exportResolution });
150
+ return exportGeometryToGLB(geometry, { ao, name: exportName, uv });
151
+ }
152
+
153
+ const root = new THREE.Group();
154
+ root.name = exportName;
155
+ const levels = [
156
+ exportResolution,
157
+ Math.max(Math.round(exportResolution / 2), 32),
158
+ Math.max(Math.round(exportResolution / 4), 24),
159
+ ];
160
+ levels.forEach((res, level) => {
161
+ root.add(makeMesh(res, `${exportName}_LOD${level}`));
162
+ });
163
+ const exporter = new GLTFExporter();
164
+ return exporter.parseAsync(root, { binary: true });
165
+ }
166
+
167
+ /** Browser download helper for exported buffers (no-op outside browsers). */
168
+ export function downloadArrayBuffer(buffer, filename, mimeType = 'application/octet-stream') {
169
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
170
+ const blob = new Blob([buffer], { type: mimeType });
171
+ const url = URL.createObjectURL(blob);
172
+ const anchor = document.createElement('a');
173
+ anchor.href = url;
174
+ anchor.download = filename;
175
+ anchor.click();
176
+ URL.revokeObjectURL(url);
177
+ }
@@ -0,0 +1,7 @@
1
+ import { erodeHeightfieldStylized } from './stylizedErosionSim.js';
2
+
3
+ export { erodeHeightfieldStylized };
4
+
5
+ export function erodeHeightfield(options) {
6
+ return erodeHeightfieldStylized(options);
7
+ }
@@ -0,0 +1,183 @@
1
+ // Eroded heightfield patches for the 'heightfield' rock piece: the recipe
2
+ // the dedicated terrain tools (Gaea / World Machine / Terrain Mixer) all
3
+ // converge on, at rock-piece scale —
4
+ //
5
+ // ridged+fbm relief → silhouette profile → terracing → EROSION
6
+ //
7
+ // The output is a normalized [0, 1] height grid the field compiler turns
8
+ // into an SDF (mesas, canyon walls, mountain flanks with real drainage),
9
+ // plus the erosion masks for stylization. Everything derives from hashed
10
+ // noise + the seeded erosion sim, so patches are bit-deterministic and a
11
+ // document's golden hash covers them.
12
+ //
13
+ // Patches are memoized on their full parameter key: meshing re-samples the
14
+ // field millions of times, and slider drags must not re-erode per sample.
15
+
16
+ import { fbm3, ridgedFbm3 } from '../noise/valueNoise3.js';
17
+ import { hashCombine } from '../noise/prng.js';
18
+ import { erodeHeightfield } from './heightfieldErosion.js';
19
+
20
+ export const HEIGHTFIELD_PROFILES = Object.freeze(['mesa', 'ridge', 'slope', 'open']);
21
+
22
+ const PATCH_RESOLUTION = 128;
23
+ const CACHE_LIMIT = 8;
24
+ const cache = new Map(); // key -> patch (insertion-ordered; oldest evicted)
25
+
26
+ const SEED_RELIEF = 17;
27
+ const SEED_DETAIL = 41;
28
+
29
+ function clamp01(v) {
30
+ return v < 0 ? 0 : v > 1 ? 1 : v;
31
+ }
32
+
33
+ // Silhouette profiles shape the patch before erosion so droplets carve
34
+ // the landform rather than a noise plane:
35
+ // mesa — flat elevated plateau falling off toward every rim
36
+ // ridge — a crest along Z, falling off toward ±X (canyon/cliff walls)
37
+ // slope — one-directional ramp, high at -X (a hillside band)
38
+ // open — raw relief edge to edge
39
+ function profileWeight(profile, u, v) {
40
+ if (profile === 'mesa') {
41
+ const rim = Math.min(u, 1 - u, v, 1 - v) * 2; // 0 at rim, 1 at center
42
+ const t = clamp01(rim / 0.55);
43
+ return 0.15 + 0.85 * (t * t * (3 - 2 * t));
44
+ }
45
+ if (profile === 'ridge') {
46
+ const crest = 1 - Math.abs(u - 0.5) * 2;
47
+ const t = clamp01(crest / 0.75);
48
+ return 0.1 + 0.9 * (t * t * (3 - 2 * t));
49
+ }
50
+ if (profile === 'slope') {
51
+ return 0.15 + 0.85 * clamp01(1.15 - u * 1.3);
52
+ }
53
+ return 1; // open
54
+ }
55
+
56
+ // Soft terracing (the cliff-banding move): quantize height into steps but
57
+ // keep a smoothed remainder so bands read as strata, not stairsteps.
58
+ function terraceHeight(h, amount, steps) {
59
+ if (amount <= 0) return h;
60
+ const scaled = h * steps;
61
+ const base = Math.floor(scaled);
62
+ const frac = scaled - base;
63
+ const sharp = clamp01((frac - 0.5) / 0.28 + 0.5);
64
+ const stepped = (base + sharp * sharp * (3 - 2 * sharp)) / steps;
65
+ return h + (stepped - h) * amount;
66
+ }
67
+
68
+ /**
69
+ * Builds (or returns cached) an eroded patch for one heightfield piece.
70
+ *
71
+ * @param {number} seed Combined document+piece seed (uint32).
72
+ * @param {object} hf The piece's `heightfield` settings group.
73
+ * @returns {{ heights: Float32Array, masks: object, resolution: number,
74
+ * sample(u: number, v: number): number }} heights normalized to [0, 1].
75
+ */
76
+ export function getHeightfieldPatch(seed, hf) {
77
+ const key = [
78
+ seed >>> 0, hf.profile, hf.relief, hf.roughness, hf.terrace, hf.terraceSteps,
79
+ hf.erosion, hf.droplets, hf.thermal, hf.seedOffset,
80
+ ].join('|');
81
+ const cached = cache.get(key);
82
+ if (cached) return cached;
83
+
84
+ const res = PATCH_RESOLUTION;
85
+ const patchSeed = hashCombine(seed >>> 0, (hf.seedOffset >>> 0) || 0);
86
+ const reliefSeed = hashCombine(patchSeed, SEED_RELIEF);
87
+ const detailSeed = hashCombine(patchSeed, SEED_DETAIL);
88
+
89
+ // --- base relief: ridged backbone blended with rolling fbm ---------------
90
+ const heights = new Float32Array(res * res);
91
+ const frequency = 2.2 + hf.roughness * 2.4;
92
+ for (let iz = 0; iz < res; iz += 1) {
93
+ for (let ix = 0; ix < res; ix += 1) {
94
+ const u = ix / (res - 1);
95
+ const v = iz / (res - 1);
96
+ const ridged = ridgedFbm3(reliefSeed, u * frequency, 0, v * frequency, 5, 2.05, 0.5);
97
+ const rolling = fbm3(detailSeed, u * frequency * 1.7, 0, v * frequency * 1.7, 4, 2.0, 0.5)
98
+ * 0.5 + 0.5;
99
+ let h = ridged * hf.relief + rolling * (1 - hf.relief);
100
+ h = terraceHeight(clamp01(h), hf.terrace, Math.max(2, Math.round(hf.terraceSteps)));
101
+ h *= profileWeight(hf.profile, u, v);
102
+ // Mesas are PLATEAUS: soft-clamp the caprock so peaks flatten into a
103
+ // tabletop instead of a mountain (relief above the cap compresses).
104
+ if (hf.profile === 'mesa' && h > 0.62) h = 0.62 + (h - 0.62) * 0.18;
105
+ heights[iz * res + ix] = h;
106
+ }
107
+ }
108
+
109
+ // --- erosion --------------------------------------------------------------
110
+ let masks = null;
111
+ let final = heights;
112
+ if (hf.erosion > 0) {
113
+ // Heights are normalized: talus/minSlope stay in per-cell units scaled
114
+ // to the patch's implied relief (1.0 over ~res cells).
115
+ const result = erodeHeightfield({
116
+ height: res,
117
+ heightmap: heights,
118
+ params: {
119
+ droplets: Math.round(10_000 + hf.droplets * 70_000),
120
+ seed: patchSeed,
121
+ strength: hf.erosion,
122
+ talus: (0.35 + (1 - hf.thermal) * 0.9) * (1.4 / res) * 20,
123
+ thermalIterations: Math.round(hf.thermal * 60),
124
+ thermalStrength: 0.25 + hf.thermal * 0.45,
125
+ },
126
+ width: res,
127
+ });
128
+ final = result.eroded;
129
+ masks = result;
130
+ // Erosion deposits can push past 1: renormalize into [0, 1] so sizeY
131
+ // stays the honest height bound the SDF metrics rely on.
132
+ let max = 1e-6;
133
+ for (let i = 0; i < final.length; i += 1) if (final[i] > max) max = final[i];
134
+ if (max > 1) for (let i = 0; i < final.length; i += 1) final[i] /= max;
135
+ }
136
+
137
+ const bilinear = (array, u, v) => {
138
+ const fx = clamp01(u) * (res - 1);
139
+ const fz = clamp01(v) * (res - 1);
140
+ const ix = Math.min(Math.floor(fx), res - 2);
141
+ const iz = Math.min(Math.floor(fz), res - 2);
142
+ const tx = fx - ix;
143
+ const tz = fz - iz;
144
+ const row = iz * res + ix;
145
+ const a = array[row];
146
+ const b = array[row + 1];
147
+ const c = array[row + res];
148
+ const d = array[row + res + 1];
149
+ return a + (b - a) * tx + (c - a) * tz + (a - b - c + d) * tx * tz;
150
+ };
151
+
152
+ const patch = {
153
+ heights: final,
154
+ masks,
155
+ resolution: res,
156
+ /** Erosion story for stylization: deposition + water flow at (u, v). */
157
+ sampleMasks(u, v) {
158
+ if (!masks) return null;
159
+ return {
160
+ flow: bilinear(masks.flow, u, v),
161
+ sediment: bilinear(masks.depositionMask, u, v),
162
+ };
163
+ },
164
+ sample(u, v) {
165
+ const fx = clamp01(u) * (res - 1);
166
+ const fz = clamp01(v) * (res - 1);
167
+ const ix = Math.min(Math.floor(fx), res - 2);
168
+ const iz = Math.min(Math.floor(fz), res - 2);
169
+ const tx = fx - ix;
170
+ const tz = fz - iz;
171
+ const row = iz * res + ix;
172
+ const a = final[row];
173
+ const b = final[row + 1];
174
+ const c = final[row + res];
175
+ const d = final[row + res + 1];
176
+ return a + (b - a) * tx + (c - a) * tz + (a - b - c + d) * tx * tz;
177
+ },
178
+ };
179
+
180
+ cache.set(key, patch);
181
+ if (cache.size > CACHE_LIMIT) cache.delete(cache.keys().next().value);
182
+ return patch;
183
+ }