@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,544 @@
1
+ // SDF-composed debris forms (bones, skulls, jaws, masonry chunks) meshed
2
+ // with rockgen's surface-nets extractor. Assembling these from separate
3
+ // THREE primitives reads as glued-together programmer art; smooth-blended
4
+ // implicit surfaces produce the single continuous form that scanned debris
5
+ // references (and Rock Lab) have. All noise is rockgen's coherent
6
+ // hash-based simplex/fbm — never uncorrelated per-vertex jitter.
7
+
8
+ import * as THREE from 'three';
9
+
10
+ import { computeGradientNormals, computeSdfAo } from '../rockgen/mesh/meshAttributes.js';
11
+ import { filterSmallIslands, sampleGrid, surfaceNets } from '../rockgen/mesh/surfaceNets.js';
12
+ import { cellular3, cellularCrease3 } from '../rockgen/noise/cellularNoise3.js';
13
+ import { hash3f, hashCombine } from '../rockgen/noise/prng.js';
14
+ import { simplexNoise3 } from '../rockgen/noise/simplexNoise3.js';
15
+ import { fbm3 } from '../rockgen/noise/valueNoise3.js';
16
+ import { opSmoothIntersect, opSmoothSubtract, opSmoothUnion } from '../rockgen/sdf/sdfOps.js';
17
+ import { sdEllipsoid, sdRoundBox } from '../rockgen/sdf/sdfPrimitives.js';
18
+
19
+ const SEED_WEATHER = 17;
20
+ const SEED_BITES = 29;
21
+ const SEED_DRIFT = 43;
22
+ const SEED_CUTS = 59;
23
+ const SEED_FACET = 73;
24
+
25
+ const mix2 = (a, b, amount) => a + (b - a) * amount;
26
+
27
+ function sphere(x, y, z, cx, cy, cz, r) {
28
+ const dx = x - cx;
29
+ const dy = y - cy;
30
+ const dz = z - cz;
31
+ return Math.sqrt(dx * dx + dy * dy + dz * dz) - r;
32
+ }
33
+
34
+ function ellipsoid(x, y, z, cx, cy, cz, rx, ry, rz) {
35
+ return sdEllipsoid(x - cx, y - cy, z - cz, rx, ry, rz);
36
+ }
37
+
38
+ /**
39
+ * Distance to a polyline of `points` ([x,y,z][]) minus a per-point radius
40
+ * (lerped along each segment): a tapered capsule chain, the workhorse for
41
+ * bone shafts and jaw bodies.
42
+ */
43
+ function chainDistance(points, radii, x, y, z) {
44
+ let best = Infinity;
45
+ for (let i = 0; i < points.length - 1; i += 1) {
46
+ const [ax, ay, az] = points[i];
47
+ const [bx, by, bz] = points[i + 1];
48
+ const ex = bx - ax;
49
+ const ey = by - ay;
50
+ const ez = bz - az;
51
+ const wx = x - ax;
52
+ const wy = y - ay;
53
+ const wz = z - az;
54
+ const lengthSq = ex * ex + ey * ey + ez * ez;
55
+ const t = lengthSq > 0 ? Math.min(Math.max((wx * ex + wy * ey + wz * ez) / lengthSq, 0), 1) : 0;
56
+ const dx = wx - ex * t;
57
+ const dy = wy - ey * t;
58
+ const dz = wz - ez * t;
59
+ const radius = radii[i] + (radii[i + 1] - radii[i]) * t;
60
+ best = Math.min(best, Math.sqrt(dx * dx + dy * dy + dz * dz) - radius);
61
+ }
62
+ return best;
63
+ }
64
+
65
+ /** Weathering pass shared by every bone form: coherent erosion + bite chips. */
66
+ function applyBoneWeathering(d, x, y, z, weather) {
67
+ const { amplitude, bites, frequency, seed } = weather;
68
+ if (amplitude > 0) {
69
+ d -= amplitude * fbm3(hashCombine(seed, SEED_WEATHER), x * frequency, y * frequency, z * frequency, 3, 2.1, 0.52);
70
+ }
71
+ for (const bite of bites) {
72
+ d = opSmoothSubtract(d, sphere(x, y, z, bite[0], bite[1], bite[2], bite[3]), bite[4]);
73
+ }
74
+ return d;
75
+ }
76
+
77
+ function makeBites(seed, anchors, damage, scale) {
78
+ const bites = [];
79
+ const count = Math.floor(damage * 3.2);
80
+ const biteSeed = hashCombine(seed, SEED_BITES);
81
+ for (let i = 0; i < count; i += 1) {
82
+ const anchor = anchors[i % anchors.length];
83
+ const theta = hash3f(biteSeed, i, 0, 0) * Math.PI * 2;
84
+ const up = hash3f(biteSeed, i, 1, 0) * 2 - 1;
85
+ const ring = Math.sqrt(Math.max(1 - up * up, 0));
86
+ const reach = scale * (0.65 + hash3f(biteSeed, i, 2, 0) * 0.5);
87
+ bites.push([
88
+ anchor[0] + Math.cos(theta) * ring * reach,
89
+ anchor[1] + up * reach,
90
+ anchor[2] + Math.sin(theta) * ring * reach,
91
+ scale * (0.3 + hash3f(biteSeed, i, 3, 0) * 0.4) * (0.4 + damage * 0.6),
92
+ scale * 0.22,
93
+ ]);
94
+ }
95
+ return bites;
96
+ }
97
+
98
+ /**
99
+ * Long bone: waisted diaphysis (capsule chain), a ball head + trochanter
100
+ * bump on the proximal end, twin condyle lobes with an intercondylar
101
+ * groove on the distal end — the asymmetric ends are what make it read as
102
+ * a femur instead of a dog-toy dumbbell.
103
+ */
104
+ export function createLongBoneField(shape, seed) {
105
+ const { curvature, damage, jointSize, length, thickness } = shape;
106
+ const half = length * 0.44;
107
+ const joint = thickness * jointSize;
108
+ const bow = curvature * length * 0.055;
109
+ const phase = hash3f(seed, 5, 0, 0) * Math.PI * 2;
110
+
111
+ const points = [];
112
+ const radii = [];
113
+ for (let i = 0; i <= 4; i += 1) {
114
+ const t = i / 4;
115
+ points.push([
116
+ (t - 0.5) * 2 * half,
117
+ bow * Math.sin(t * Math.PI),
118
+ curvature * length * 0.02 * Math.sin(t * Math.PI * 2 + phase),
119
+ ]);
120
+ radii.push(thickness * (1.06 - 0.4 * Math.sin(t * Math.PI)));
121
+ }
122
+ const end0 = points[0];
123
+ const end1 = points[4];
124
+
125
+ const head = [end0[0] - joint * 0.3, end0[1] + joint * 0.62, end0[2], joint * 0.78];
126
+ const troch = [end0[0] + joint * 0.1, end0[1] - joint * 0.28, end0[2] - joint * 0.3];
127
+ const condyles = [-1, 1].map((side) => [
128
+ end1[0] + joint * 0.22, end1[1] - joint * 0.08, end1[2] + side * joint * 0.48,
129
+ ]);
130
+ const groove = [end1[0] + joint * 0.72, end1[1] + joint * 0.3, end1[2]];
131
+
132
+ const weather = {
133
+ amplitude: damage * thickness * 0.14,
134
+ bites: makeBites(seed, [end0, end1], damage, joint),
135
+ frequency: 1 / (thickness * 2.6),
136
+ seed,
137
+ };
138
+
139
+ const reach = joint * 1.7;
140
+ return {
141
+ bounds: {
142
+ max: [half + reach, bow + reach, reach],
143
+ min: [-half - reach, -reach, -reach],
144
+ },
145
+ evaluate: (x, y, z) => {
146
+ let d = chainDistance(points, radii, x, y, z);
147
+ d = opSmoothUnion(d, sphere(x, y, z, head[0], head[1], head[2], head[3]), thickness * 0.85);
148
+ d = opSmoothUnion(
149
+ d,
150
+ ellipsoid(x, y, z, troch[0], troch[1], troch[2], joint * 0.55, joint * 0.45, joint * 0.5),
151
+ thickness * 0.6,
152
+ );
153
+ for (const c of condyles) {
154
+ d = opSmoothUnion(
155
+ d,
156
+ ellipsoid(x, y, z, c[0], c[1], c[2], joint * 0.68, joint * 0.58, joint * 0.46),
157
+ thickness * 0.55,
158
+ );
159
+ }
160
+ // Y-axis capsule via swapped coords: groove runs along X between the
161
+ // condyles.
162
+ const gx = x - groove[0];
163
+ const gz = z - groove[2];
164
+ const gy = y - groove[1];
165
+ const clampedX = gx - Math.min(Math.max(gx, -joint * 0.8), joint * 0.8);
166
+ const grooveD = Math.sqrt(clampedX * clampedX + gy * gy + gz * gz) - thickness * 0.34;
167
+ d = opSmoothSubtract(d, grooveD, thickness * 0.3);
168
+ return applyBoneWeathering(d, x, y, z, weather);
169
+ },
170
+ resolution: 60,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Ungulate skull (deer/goat family, matching scanned reference packs):
176
+ * braincase + frontal plate + tapering snout smooth-blended into one form,
177
+ * orbits and nasal aperture subtracted so sockets read as true cavities,
178
+ * zygomatic arches and pedicle stubs breaking the silhouette.
179
+ */
180
+ export function createSkullField(shape, seed) {
181
+ const { damage, jointSize, length } = shape;
182
+ const s = length * 0.62;
183
+
184
+ const orbitR = s * 0.145 * jointSize;
185
+ const weather = {
186
+ amplitude: damage * s * 0.035,
187
+ bites: makeBites(seed, [[-s * 0.3, s * 0.58, 0], [s * 0.15, s * 0.6, 0]], damage * 0.8, s * 0.3),
188
+ frequency: 2.6 / s,
189
+ seed,
190
+ };
191
+
192
+ return {
193
+ bounds: {
194
+ max: [s * 1.05, s * 0.95, s * 0.5],
195
+ min: [-s * 0.65, 0, -s * 0.5],
196
+ },
197
+ evaluate: (x, y, z) => {
198
+ // Union half: cranium, frontal plate, wedge snout, nasal ridge,
199
+ // maxilla plate. Blends stay tight — over-blending is what turns a
200
+ // skull into a peanut.
201
+ let d = ellipsoid(x, y, z, -s * 0.26, s * 0.48, 0, s * 0.32, s * 0.3, s * 0.23);
202
+ d = opSmoothUnion(d, ellipsoid(x, y, z, 0.02 * s, s * 0.54, 0, s * 0.26, s * 0.18, s * 0.26), s * 0.07);
203
+ // Snout tapers in plan AND profile: a slimmer main wedge plus a
204
+ // narrow premaxilla tip carrying the taper to the nose.
205
+ d = opSmoothUnion(d, ellipsoid(x, y, z, s * 0.36, s * 0.36, 0, s * 0.38, s * 0.18, s * 0.13), s * 0.08);
206
+ d = opSmoothUnion(d, ellipsoid(x, y, z, s * 0.72, s * 0.32, 0, s * 0.26, s * 0.12, s * 0.085), s * 0.07);
207
+ d = opSmoothUnion(d, ellipsoid(x, y, z, s * 0.4, s * 0.48, 0, s * 0.3, s * 0.09, s * 0.07), s * 0.06);
208
+ d = opSmoothUnion(d, ellipsoid(x, y, z, s * 0.42, s * 0.24, 0, s * 0.34, s * 0.1, s * 0.115), s * 0.07);
209
+ for (const side of [-1, 1]) {
210
+ // Zygomatic arch: thin bar standing proud of the skull side so it
211
+ // catches its own shading band.
212
+ const arch = chainDistance(
213
+ [[s * 0.24, s * 0.33, side * s * 0.21], [s * 0.02, s * 0.36, side * s * 0.27], [-s * 0.14, s * 0.44, side * s * 0.25]],
214
+ [s * 0.04, s * 0.045, s * 0.05],
215
+ x, y, z,
216
+ );
217
+ d = opSmoothUnion(d, arch, s * 0.035);
218
+ // Pedicle stubs (shed-antler mounts) on the rear crown.
219
+ const pedicle = chainDistance(
220
+ [[-s * 0.18, s * 0.62, side * s * 0.13], [-s * 0.24, s * 0.8, side * s * 0.21]],
221
+ [s * 0.065, s * 0.05],
222
+ x, y, z,
223
+ );
224
+ d = opSmoothUnion(d, pedicle, s * 0.03);
225
+ // Orbit cavities carved AFTER the arch so a socket rim survives.
226
+ d = opSmoothSubtract(
227
+ d,
228
+ sphere(x, y, z, s * 0.14, s * 0.5, side * s * 0.22, orbitR),
229
+ s * 0.03,
230
+ );
231
+ }
232
+ // Nasal aperture: a shallow notch nicking the top of the nose tip.
233
+ d = opSmoothSubtract(d, ellipsoid(x, y, z, s * 0.95, s * 0.42, 0, s * 0.14, s * 0.07, s * 0.05), s * 0.02);
234
+ // Foramen magnum hollow at the rear.
235
+ d = opSmoothSubtract(d, sphere(x, y, z, -s * 0.56, s * 0.42, 0, s * 0.1), s * 0.02);
236
+ // Flatten the underside just enough to rest on the maxilla — cutting
237
+ // higher pancakes the whole skull under toon banding.
238
+ d = opSmoothIntersect(d, s * 0.05 - y, s * 0.035);
239
+ return applyBoneWeathering(d, x, y, z, weather);
240
+ },
241
+ resolution: 72,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Hemimandible (half jaw, the classic found bone): curved tooth-bearing
247
+ * body sweeping up into a flat ramus with condyle + coronoid processes,
248
+ * molar row as rounded boxes with damage-dependent missing teeth, and the
249
+ * long toothless diastema gap that makes herbivore jaws instantly legible.
250
+ */
251
+ export function createJawField(shape, seed) {
252
+ const { curvature, damage, jointSize, length } = shape;
253
+ const s = length * 0.5;
254
+ const lift = 0.1 + curvature * 0.1;
255
+
256
+ const body = [
257
+ [s * 0.92, s * 0.2, 0],
258
+ [s * 0.5, s * (lift + 0.03), s * 0.03],
259
+ [s * 0.05, s * lift, s * 0.05],
260
+ [-s * 0.3, s * (lift + 0.06), s * 0.04],
261
+ ];
262
+ const bodyRadii = [s * 0.045, s * 0.06, s * 0.075, s * 0.09];
263
+
264
+ const teeth = [];
265
+ const teethSeed = hashCombine(seed, SEED_BITES);
266
+ // Molar row over the rear body; the front stays bare (diastema).
267
+ for (let i = 0; i < 6; i += 1) {
268
+ if (hash3f(teethSeed, i, 7, 0) < damage * 0.45) continue;
269
+ const t = i / 5;
270
+ const cx = s * (0.42 - t * 0.62);
271
+ const cy = s * (lift + 0.1 + t * 0.025);
272
+ teeth.push([cx, cy, s * 0.045, s * (0.05 + jointSize * 0.012)]);
273
+ }
274
+ // Two incisor nubs at the chin tip.
275
+ for (let i = 0; i < 2; i += 1) {
276
+ teeth.push([s * (0.88 - i * 0.09), s * 0.26, s * 0.01, s * 0.032]);
277
+ }
278
+
279
+ const weather = {
280
+ amplitude: damage * s * 0.03,
281
+ bites: makeBites(seed, [[-s * 0.5, s * 0.5, 0], [s * 0.9, s * 0.2, 0]], damage * 0.7, s * 0.18),
282
+ frequency: 3 / s,
283
+ seed,
284
+ };
285
+
286
+ return {
287
+ bounds: {
288
+ max: [s * 1.1, s * 0.95, s * 0.35],
289
+ min: [-s * 0.8, 0, -s * 0.25],
290
+ },
291
+ evaluate: (x, y, z) => {
292
+ let d = chainDistance(body, bodyRadii, x, y, z);
293
+ // Ramus: thin vertical plate rising behind the molars.
294
+ d = opSmoothUnion(
295
+ d,
296
+ ellipsoid(x, y, z, -s * 0.42, s * 0.42, s * 0.03, s * 0.24, s * 0.3, s * 0.045),
297
+ s * 0.08,
298
+ );
299
+ // Condyle knob (articular) and coronoid spike on the ramus top.
300
+ d = opSmoothUnion(d, ellipsoid(x, y, z, -s * 0.56, s * 0.68, s * 0.03, s * 0.08, s * 0.055, s * 0.09), s * 0.04);
301
+ d = opSmoothUnion(d, ellipsoid(x, y, z, -s * 0.26, s * 0.7, s * 0.02, s * 0.055, s * 0.14, s * 0.03), s * 0.04);
302
+ for (const tooth of teeth) {
303
+ d = opSmoothUnion(
304
+ d,
305
+ sdRoundBox(x - tooth[0], y - tooth[1], z - tooth[2], tooth[3] * 0.75, tooth[3], tooth[3] * 0.6, tooth[3] * 0.3),
306
+ s * 0.02,
307
+ );
308
+ }
309
+ return applyBoneWeathering(d, x, y, z, weather);
310
+ },
311
+ resolution: 64,
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Masonry chunk / brick / shard as a mini-rock: base solid + planar cuts
317
+ * for the fractured silhouette + cellular crease facets + low fbm — the
318
+ * exact recipe rockgen's cliffs use, at debris scale.
319
+ */
320
+ export function createMasonryChunkField(shape, variant, seed) {
321
+ const { angularity, chunkSize, fracture } = shape;
322
+ const brick = variant === 'brick';
323
+ const shard = variant === 'shard';
324
+
325
+ const rx = chunkSize * (brick ? 0.9 : 0.55 + hash3f(seed, 1, 0, 0) * 0.4);
326
+ const ry = chunkSize * (brick ? 0.34 : shard ? 0.2 : 0.38 + hash3f(seed, 2, 0, 0) * 0.25);
327
+ const rz = chunkSize * (brick ? 0.44 : 0.45 + hash3f(seed, 3, 0, 0) * 0.35);
328
+
329
+ // Planar cuts: random directions, offset so each bites 15-45% of the
330
+ // support radius — straight fracture faces instead of blobby noise.
331
+ const cutSeed = hashCombine(seed, SEED_CUTS);
332
+ const cutCount = brick ? 1 + Math.round(fracture * 2) : 3 + Math.round(angularity * 3);
333
+ const planes = [];
334
+ for (let i = 0; i < cutCount; i += 1) {
335
+ const theta = hash3f(cutSeed, i, 0, 0) * Math.PI * 2;
336
+ const up = (hash3f(cutSeed, i, 1, 0) * 2 - 1) * (shard ? 0.35 : 1);
337
+ const ring = Math.sqrt(Math.max(1 - up * up, 0));
338
+ const nx = Math.cos(theta) * ring;
339
+ const ny = up;
340
+ const nz = Math.sin(theta) * ring;
341
+ const support = Math.sqrt((nx * rx) ** 2 + (ny * ry) ** 2 + (nz * rz) ** 2);
342
+ const bite = (brick ? 0.2 : 0.18 + angularity * 0.3) * (0.5 + hash3f(cutSeed, i, 2, 0));
343
+ planes.push([nx, ny, nz, support * (1 - bite)]);
344
+ }
345
+
346
+ // DM2 calibration: gravel's cellular displacement is the MAIN shaper
347
+ // (strength 30-45% of radius, feature size ~= radius), planar facets
348
+ // secondary. Slate/sandstone plates keep noise in-plane only so faces
349
+ // stay flat while outlines wobble.
350
+ const facetSeed = hashCombine(seed, SEED_FACET);
351
+ const facetStrength = chunkSize * (brick ? 0.05 : mix2(0.08, 0.22, fracture));
352
+ const facetScale = 1.3 / chunkSize;
353
+ const noiseAmp = chunkSize * (brick ? 0.02 : shard ? 0.07 : 0.06) * Math.max(fracture, shard ? 0.6 : 0);
354
+ const noiseFreq = (shard ? 1.3 : 1.7) / chunkSize;
355
+ const noiseFlatten = shard ? 0.15 : 1;
356
+ const bevel = chunkSize * (shard ? 0.015 : 0.03);
357
+
358
+ const pad = chunkSize * 0.18;
359
+ return {
360
+ bounds: {
361
+ max: [rx + pad, ry + pad, rz + pad],
362
+ min: [-rx - pad, -ry - pad, -rz - pad],
363
+ },
364
+ evaluate: (x, y, z) => {
365
+ let d = brick
366
+ ? sdRoundBox(x, y, z, rx, ry, rz, chunkSize * 0.05)
367
+ : sdEllipsoid(x, y, z, rx, ry, rz);
368
+ if (noiseAmp > 0) {
369
+ d -= noiseAmp * fbm3(hashCombine(seed, SEED_WEATHER), x * noiseFreq, y * noiseFreq * noiseFlatten, z * noiseFreq, 3, 2, 0.5);
370
+ }
371
+ if (facetStrength > 0) {
372
+ d += facetStrength * cellularCrease3(facetSeed, x * facetScale, y * facetScale, z * facetScale, 1);
373
+ }
374
+ for (const plane of planes) {
375
+ d = opSmoothIntersect(d, plane[0] * x + plane[1] * y + plane[2] * z - plane[3], bevel);
376
+ }
377
+ return d;
378
+ },
379
+ resolution: shard ? 40 : 36,
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Stone & mineral fields, each following its DebrisMaker2 recipe:
385
+ * - riverstones: flat ellipsoid + subtle large cellular + heavy smoothing
386
+ * (low amplitudes stand in for DM2's ~150 relax iterations)
387
+ * - obsidian: tapered wedge + large cellular lobes + conchoidal banding
388
+ * ridges + hard cuts, NO smoothing noise
389
+ * - meteor: sphere + inward cellular craters layered large->medium->small
390
+ * - gems: many planar slices around a sphere, beveled (chamfer) edges
391
+ */
392
+ export function createStoneField(shape, variant, seed) {
393
+ const { banding, chunkSize, detail, flatness, sharpness } = shape;
394
+ const gem = variant === 'gems';
395
+ const meteor = variant === 'meteor';
396
+ const obsidian = variant === 'obsidian';
397
+
398
+ const rx = chunkSize * (obsidian ? 0.85 + hash3f(seed, 1, 0, 0) * 0.5 : 0.6 + hash3f(seed, 1, 0, 0) * 0.4);
399
+ const ry = chunkSize * mix2(0.62, 0.2, gem || meteor ? 0 : flatness) * (0.8 + hash3f(seed, 2, 0, 0) * 0.4);
400
+ const rz = chunkSize * (obsidian ? 0.32 + hash3f(seed, 3, 0, 0) * 0.25 : 0.5 + hash3f(seed, 3, 0, 0) * 0.4);
401
+
402
+ // Obsidian taper: shrink the cross-section toward +X (DM2 taper -0.4..-0.7).
403
+ const taper = obsidian ? mix2(0.35, 0.7, sharpness) : 0;
404
+
405
+ // Banding direction for conchoidal ridges / strata.
406
+ const bandTheta = hash3f(seed, 4, 0, 0) * Math.PI;
407
+ const bandDir = [Math.cos(bandTheta), mix2(0.1, 0.5, hash3f(seed, 5, 0, 0)), Math.sin(bandTheta)];
408
+ const bandFreq = (obsidian ? 9 : 5) / chunkSize;
409
+ const bandAmp = chunkSize * (obsidian ? 0.035 : 0.012) * banding;
410
+
411
+ // Gem facets / obsidian fracture faces as planar intersections.
412
+ const cutSeed = hashCombine(seed, SEED_CUTS);
413
+ const cutCount = gem ? 12 + Math.round(sharpness * 8) : obsidian ? 3 + Math.round(sharpness * 3) : 0;
414
+ const planes = [];
415
+ for (let i = 0; i < cutCount; i += 1) {
416
+ const theta = hash3f(cutSeed, i, 0, 0) * Math.PI * 2;
417
+ const up = hash3f(cutSeed, i, 1, 0) * 2 - 1;
418
+ const ring = Math.sqrt(Math.max(1 - up * up, 0));
419
+ const nx = Math.cos(theta) * ring;
420
+ const ny = up;
421
+ const nz = Math.sin(theta) * ring;
422
+ const support = Math.sqrt((nx * rx) ** 2 + (ny * ry) ** 2 + (nz * rz) ** 2);
423
+ // Gems slice close to the surface everywhere (convex faceted solid);
424
+ // obsidian takes a couple of deep fracture faces.
425
+ const bite = gem ? mix2(0.14, 0.34, hash3f(cutSeed, i, 2, 0)) : mix2(0.15, 0.4, hash3f(cutSeed, i, 2, 0));
426
+ planes.push([nx, ny, nz, support * (1 - bite)]);
427
+ }
428
+ // Gem chamfer: banding doubles as the facet-edge bevel radius.
429
+ const bevel = chunkSize * (gem ? mix2(0.005, 0.028, banding) : obsidian ? 0.008 : 0.02);
430
+
431
+ const cellSeed = hashCombine(seed, SEED_FACET);
432
+ const cellScale = (meteor ? 1.1 : 0.9) / chunkSize;
433
+ const cellAmp = chunkSize * detail * (obsidian ? 0.1 : meteor ? 0.2 : 0.08);
434
+ const microSeed = hashCombine(seed, SEED_WEATHER);
435
+ const microAmp = chunkSize * detail * (gem ? 0 : obsidian ? 0.015 : 0.035);
436
+
437
+ const pad = chunkSize * 0.25;
438
+ return {
439
+ bounds: {
440
+ max: [rx + pad, ry + pad, rz + pad],
441
+ min: [-rx - pad, -ry - pad, -rz - pad],
442
+ },
443
+ evaluate: (x, y, z) => {
444
+ if (taper > 0) {
445
+ const squeeze = 1 - taper * Math.min(Math.max((x / rx) * 0.5 + 0.5, 0), 1);
446
+ return evaluateStone(x, y / squeeze, z / squeeze) * squeeze;
447
+ }
448
+ return evaluateStone(x, y, z);
449
+
450
+ function evaluateStone(px, py, pz) {
451
+ let d = sdEllipsoid(px, py, pz, rx, ry, rz);
452
+ if (cellAmp > 0) {
453
+ if (meteor) {
454
+ // Craters: inward dents at cell centers, three octave layers.
455
+ for (const [scaleMul, ampMul] of [[1, 1], [2.3, 0.4], [5.1, 0.15]]) {
456
+ const cell = cellular3(hashCombine(cellSeed, scaleMul * 7), px * cellScale * scaleMul, py * cellScale * scaleMul, pz * cellScale * scaleMul, 1);
457
+ const crater = Math.max(0, 1 - cell.f1 * 1.6);
458
+ d += cellAmp * ampMul * crater * crater;
459
+ }
460
+ } else {
461
+ // Lobes: cellular crease borders carve the silhouette.
462
+ d += cellAmp * cellularCrease3(cellSeed, px * cellScale, py * cellScale, pz * cellScale, 1);
463
+ }
464
+ }
465
+ if (microAmp > 0) {
466
+ d -= microAmp * fbm3(microSeed, px * 2.2 / chunkSize, py * 2.2 / chunkSize, pz * 2.2 / chunkSize, 3, 2, 0.5);
467
+ }
468
+ if (bandAmp > 0) {
469
+ d += bandAmp * Math.abs(Math.sin((bandDir[0] * px + bandDir[1] * py + bandDir[2] * pz) * bandFreq * Math.PI));
470
+ }
471
+ for (const plane of planes) {
472
+ d = opSmoothIntersect(d, plane[0] * px + plane[1] * py + plane[2] * pz - plane[3], bevel);
473
+ }
474
+ return d;
475
+ }
476
+ },
477
+ resolution: gem ? 52 : obsidian ? 48 : 40,
478
+ };
479
+ }
480
+
481
+ /**
482
+ * Meshes a debris field with surface nets (dual-contouring vertex
483
+ * placement so cut planes and socket rims stay sharp) and bakes coherent
484
+ * vertex colors: palette drift from low-frequency simplex, SDF ambient
485
+ * occlusion pulling cavities toward the accent (dirt) color, and the
486
+ * debris look's upward edge light.
487
+ */
488
+ export function meshDebrisField(field, surface, seed, { cavityTint = 0.75 } = {}) {
489
+ const { bounds, evaluate } = field;
490
+ const longest = Math.max(
491
+ bounds.max[0] - bounds.min[0],
492
+ bounds.max[1] - bounds.min[1],
493
+ bounds.max[2] - bounds.min[2],
494
+ );
495
+ const cell = longest / field.resolution;
496
+ const padded = {
497
+ max: bounds.max.map((v) => v + cell * 2),
498
+ min: bounds.min.map((v) => v - cell * 2),
499
+ };
500
+ const grid = sampleGrid(evaluate, padded, field.resolution);
501
+ const extracted = filterSmallIslands(surfaceNets(grid, { evaluate }));
502
+ const { indices, positions } = extracted;
503
+ if (positions.length === 0) return null;
504
+
505
+ const normals = computeGradientNormals(evaluate, positions, grid.cellSize * 0.5);
506
+ const ao = computeSdfAo(evaluate, positions, normals, {
507
+ radius: longest * 0.16,
508
+ strength: 0.9,
509
+ });
510
+
511
+ const primary = new THREE.Color(...surface.primaryColor);
512
+ const secondary = new THREE.Color(...surface.secondaryColor);
513
+ const accent = new THREE.Color(...surface.accentColor);
514
+ const driftSeed = hashCombine(seed, SEED_DRIFT);
515
+ const driftFreq = 2.4 / longest;
516
+ const colors = new Float32Array(positions.length);
517
+ const swatch = new THREE.Color();
518
+ for (let i = 0; i < positions.length / 3; i += 1) {
519
+ const px = positions[i * 3];
520
+ const py = positions[i * 3 + 1];
521
+ const pz = positions[i * 3 + 2];
522
+ const drift = simplexNoise3(driftSeed, px * driftFreq, py * driftFreq, pz * driftFreq);
523
+ const blend = Math.min(Math.max(0.5 + drift * (0.5 + surface.variation * 1.4), 0), 1);
524
+ swatch.copy(primary).lerp(secondary, blend);
525
+ // Cavities (sockets, grooves, bite chips) collect the accent color and
526
+ // shade — this is what makes carved detail read at toon contrast.
527
+ const cavity = Math.min(Math.max((1 - ao[i]) * cavityTint * 2, 0), 1);
528
+ swatch.lerp(accent, cavity);
529
+ const upward = Math.max(0, normals[i * 3 + 1]);
530
+ const light = (0.72 + ao[i] * 0.28) * (1 + upward * surface.edgeLight * 0.28);
531
+ colors[i * 3] = Math.min(swatch.r * light, 1);
532
+ colors[i * 3 + 1] = Math.min(swatch.g * light, 1);
533
+ colors[i * 3 + 2] = Math.min(swatch.b * light, 1);
534
+ }
535
+
536
+ const geometry = new THREE.BufferGeometry();
537
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
538
+ geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
539
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
540
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1));
541
+ geometry.computeBoundingBox();
542
+ geometry.computeBoundingSphere();
543
+ return geometry;
544
+ }