@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,380 @@
1
+ // Procedural detail textures for debris materials. Each debris type gets
2
+ // a tileable 256x256 DataTexture (no canvas/DOM — works headless and in
3
+ // workers) built from the same hash-based value noise rockgen uses, so
4
+ // results are deterministic per seed. Textures are near-white detail maps
5
+ // that MULTIPLY the baked vertex colors: the palette still comes from the
6
+ // generator's color slots; the map adds material read (wood grain, bone
7
+ // porosity, mineral speckle, brushed metal, leaf veins, ash powder) that
8
+ // survives the environment toon conversion (the adapter resolves
9
+ // `material.map` into its stylized albedo).
10
+
11
+ import * as THREE from 'three';
12
+
13
+ import { hashCombine } from '../rockgen/noise/prng.js';
14
+ import { fbm3, valueNoise3 } from '../rockgen/noise/valueNoise3.js';
15
+
16
+ const SIZE = 256;
17
+ const CACHE_LIMIT = 24;
18
+ const cache = new Map();
19
+
20
+ const clamp01 = (value) => Math.min(Math.max(value, 0), 1);
21
+
22
+ /**
23
+ * Fills a texture from `shade(u, v) -> [r, g, b]` with u/v in [0, 1).
24
+ * Sampling wraps because every noise call below uses u/v scaled by whole
25
+ * numbers, keeping the tile seamless.
26
+ */
27
+ function bakeTexture(shade) {
28
+ const data = new Uint8Array(SIZE * SIZE * 4);
29
+ for (let y = 0; y < SIZE; y += 1) {
30
+ for (let x = 0; x < SIZE; x += 1) {
31
+ const [r, g, b] = shade(x / SIZE, y / SIZE);
32
+ const offset = (y * SIZE + x) * 4;
33
+ data[offset] = Math.round(clamp01(r) * 255);
34
+ data[offset + 1] = Math.round(clamp01(g) * 255);
35
+ data[offset + 2] = Math.round(clamp01(b) * 255);
36
+ data[offset + 3] = 255;
37
+ }
38
+ }
39
+ const texture = new THREE.DataTexture(data, SIZE, SIZE, THREE.RGBAFormat);
40
+ texture.colorSpace = THREE.SRGBColorSpace;
41
+ texture.wrapS = THREE.RepeatWrapping;
42
+ texture.wrapT = THREE.RepeatWrapping;
43
+ texture.generateMipmaps = true;
44
+ texture.minFilter = THREE.LinearMipmapLinearFilter;
45
+ texture.magFilter = THREE.LinearFilter;
46
+ texture.needsUpdate = true;
47
+ return texture;
48
+ }
49
+
50
+ // Tileable fbm: sample on a torus via two phase-shifted reads (cheap and
51
+ // good enough for detail maps — exact periodic noise is overkill here).
52
+ function tileNoise(seed, u, v, scaleU, scaleV, octaves = 3) {
53
+ const a = fbm3(seed, u * scaleU, v * scaleV, 0.37, octaves, 2, 0.5);
54
+ const b = fbm3(seed, (u - 1) * scaleU, v * scaleV, 0.37, octaves, 2, 0.5);
55
+ const c = fbm3(seed, u * scaleU, (v - 1) * scaleV, 0.37, octaves, 2, 0.5);
56
+ const d = fbm3(seed, (u - 1) * scaleU, (v - 1) * scaleV, 0.37, octaves, 2, 0.5);
57
+ return a * (1 - u) * (1 - v) + b * u * (1 - v) + c * (1 - u) * v + d * u * v;
58
+ }
59
+
60
+ const gray = (value) => [value, value, value];
61
+
62
+ // Wood: grain streaks running along V — noise compressed 12x across so
63
+ // bands read as fibers, plus sparse darker growth lines.
64
+ function shadeWood(seed) {
65
+ return (u, v) => {
66
+ const wobble = 0.06 * tileNoise(hashCombine(seed, 2), u, v, 3, 3, 2);
67
+ const grain = tileNoise(seed, u + wobble, v, 24, 2, 3);
68
+ const line = valueNoise3(hashCombine(seed, 3), (u + wobble) * 48, v * 3, 0.7);
69
+ let value = 0.88 + grain * 0.1;
70
+ if (line > 0.55) value -= (line - 0.55) * 0.5;
71
+ return [value, value * 0.97, value * 0.92];
72
+ };
73
+ }
74
+
75
+ // Bone: fine porous speckle with sparse darker pits and a faint mottle.
76
+ function shadeBone(seed) {
77
+ return (u, v) => {
78
+ const mottle = tileNoise(seed, u, v, 4, 4, 2);
79
+ const pore = valueNoise3(hashCombine(seed, 5), u * 60, v * 60, 0.3);
80
+ let value = 0.92 + mottle * 0.07;
81
+ if (pore > 0.62) value -= (pore - 0.62) * 0.9;
82
+ return [value, value, value * 0.97];
83
+ };
84
+ }
85
+
86
+ // Stone / masonry: mineral speckle over a broad mottle.
87
+ function shadeStone(seed) {
88
+ return (u, v) => {
89
+ const mottle = tileNoise(seed, u, v, 5, 5, 3);
90
+ const speck = valueNoise3(hashCombine(seed, 7), u * 90, v * 90, 0.2);
91
+ let value = 0.9 + mottle * 0.08;
92
+ if (speck > 0.7) value += (speck - 0.7) * 0.5;
93
+ if (speck < -0.7) value += (speck + 0.7) * 0.4;
94
+ return gray(value);
95
+ };
96
+ }
97
+
98
+ // Metal: brushed streaks along U plus broad patchy tarnish.
99
+ function shadeMetal(seed) {
100
+ return (u, v) => {
101
+ const brush = tileNoise(seed, u, v, 2, 40, 2);
102
+ const patch = tileNoise(hashCombine(seed, 11), u, v, 3, 3, 2);
103
+ const value = 0.9 + brush * 0.06 + patch * 0.08;
104
+ return [value, value, value * 1.02];
105
+ };
106
+ }
107
+
108
+ // Leaf: mid vein at u=0.5 with angled side veins, over papery fibers.
109
+ // Leaf geometry UVs put u across the blade and v stem->tip.
110
+ function shadeLeaf(seed) {
111
+ return (u, v) => {
112
+ const paper = tileNoise(seed, u, v, 8, 8, 2);
113
+ let value = 0.92 + paper * 0.06;
114
+ const acrossMid = Math.abs(u - 0.5);
115
+ if (acrossMid < 0.02) value -= (1 - acrossMid / 0.02) * 0.18;
116
+ // Side veins: diagonal lines every ~0.14 of blade length.
117
+ const veinPhase = (v * 7 + acrossMid * 4.2) % 1;
118
+ if (veinPhase < 0.07 && acrossMid > 0.02 && acrossMid < 0.42) {
119
+ value -= (1 - veinPhase / 0.07) * 0.1;
120
+ }
121
+ return [value * 0.99, value, value * 0.95];
122
+ };
123
+ }
124
+
125
+ // Ash: fine powder with soft clumps.
126
+ function shadeAsh(seed) {
127
+ return (u, v) => {
128
+ const clump = tileNoise(seed, u, v, 6, 6, 3);
129
+ const dust = valueNoise3(hashCombine(seed, 13), u * 110, v * 110, 0.2);
130
+ return gray(0.92 + clump * 0.06 + dust * 0.04);
131
+ };
132
+ }
133
+
134
+ // Fibrous radial detail for cones, shells, and other organic solids.
135
+ function shadeFiber(seed) {
136
+ return (u, v) => {
137
+ const fiber = tileNoise(seed, u, v, 24, 4, 2);
138
+ const mottle = tileNoise(hashCombine(seed, 17), u, v, 3, 3, 2);
139
+ return gray(0.9 + fiber * 0.07 + mottle * 0.05);
140
+ };
141
+ }
142
+
143
+ // Bark: deep lengthwise ridge streaks with dark furrows.
144
+ function shadeBark(seed) {
145
+ return (u, v) => {
146
+ const wobble = 0.08 * tileNoise(hashCombine(seed, 2), u, v, 3, 3, 2);
147
+ const ridge = tileNoise(seed, u + wobble, v, 16, 2, 3);
148
+ const furrow = valueNoise3(hashCombine(seed, 4), (u + wobble) * 26, v * 2.2, 0.5);
149
+ let value = 0.82 + ridge * 0.14;
150
+ if (furrow > 0.35) value -= (furrow - 0.35) * 0.55;
151
+ return [value, value * 0.94, value * 0.88];
152
+ };
153
+ }
154
+
155
+ // Cracked wood: grain plus crossing check-crack lines.
156
+ function shadeCrackedWood(seed) {
157
+ const grain = shadeWood(seed);
158
+ return (u, v) => {
159
+ const color = grain(u, v);
160
+ const crack = valueNoise3(hashCombine(seed, 9), u * 9, v * 34, 0.4);
161
+ if (crack > 0.66) {
162
+ const depth = Math.min((crack - 0.66) * 2.4, 0.5);
163
+ return [color[0] - depth, color[1] - depth, color[2] - depth];
164
+ }
165
+ return color;
166
+ };
167
+ }
168
+
169
+ // Bleached bone: smoother and brighter than porous, faint sutures.
170
+ function shadeBleachedBone(seed) {
171
+ return (u, v) => {
172
+ const mottle = tileNoise(seed, u, v, 3, 3, 2);
173
+ const suture = valueNoise3(hashCombine(seed, 6), u * 14, v * 14, 0.6);
174
+ let value = 0.96 + mottle * 0.04;
175
+ if (suture > 0.74) value -= (suture - 0.74) * 0.35;
176
+ return [value, value, value * 0.98];
177
+ };
178
+ }
179
+
180
+ // Veined stone: thin bright mineral veins over mottle.
181
+ function shadeVeinedStone(seed) {
182
+ return (u, v) => {
183
+ const mottle = tileNoise(seed, u, v, 4, 4, 3);
184
+ const vein = Math.abs(tileNoise(hashCombine(seed, 8), u, v, 6, 6, 3));
185
+ let value = 0.88 + mottle * 0.07;
186
+ if (vein < 0.05) value += (1 - vein / 0.05) * 0.18;
187
+ return gray(value);
188
+ };
189
+ }
190
+
191
+ // Porous stone (lava/limestone): dark pit holes.
192
+ function shadePorousStone(seed) {
193
+ return (u, v) => {
194
+ const mottle = tileNoise(seed, u, v, 5, 5, 2);
195
+ const pore = valueNoise3(hashCombine(seed, 10), u * 70, v * 70, 0.25);
196
+ let value = 0.9 + mottle * 0.06;
197
+ if (pore > 0.55) value -= (pore - 0.55) * 1;
198
+ return gray(value);
199
+ };
200
+ }
201
+
202
+ // Galvanized metal: crystalline spangle patches.
203
+ function shadeGalvanized(seed) {
204
+ return (u, v) => {
205
+ const patch = valueNoise3(hashCombine(seed, 12), u * 9, v * 9, 0.9);
206
+ const grainy = tileNoise(seed, u, v, 30, 30, 1);
207
+ const facet = (Math.round(patch * 5) / 5 - 0.5) * 0.14;
208
+ const value = 0.92 + facet + grainy * 0.03;
209
+ return [value, value, value * 1.03];
210
+ };
211
+ }
212
+
213
+ // Painted metal: flat color with chipped scratches to bare metal.
214
+ function shadePaintedMetal(seed) {
215
+ return (u, v) => {
216
+ const chip = valueNoise3(hashCombine(seed, 14), u * 22, v * 22, 0.35);
217
+ const scratch = tileNoise(seed, u, v, 2, 46, 1);
218
+ let value = 0.95 + scratch * 0.02;
219
+ if (chip > 0.62) value -= (chip - 0.62) * 0.85;
220
+ return [value, value * 0.99, value * 0.97];
221
+ };
222
+ }
223
+
224
+ // Style registry per material kind; the first entry is the 'auto' pick
225
+ // unless debrisTextureAuto overrides by variant.
226
+ export const DEBRIS_TEXTURE_STYLES = Object.freeze({
227
+ ash: Object.freeze([Object.freeze({ id: 'powder', label: 'Powder' })]),
228
+ bone: Object.freeze([
229
+ Object.freeze({ id: 'porous', label: 'Porous' }),
230
+ Object.freeze({ id: 'bleached', label: 'Bleached' }),
231
+ ]),
232
+ leaf: Object.freeze([Object.freeze({ id: 'veins', label: 'Leaf veins' })]),
233
+ metal: Object.freeze([
234
+ Object.freeze({ id: 'brushed', label: 'Brushed' }),
235
+ Object.freeze({ id: 'galvanized', label: 'Galvanized' }),
236
+ Object.freeze({ id: 'painted', label: 'Painted & chipped' }),
237
+ ]),
238
+ organicFiber: Object.freeze([Object.freeze({ id: 'fiber', label: 'Fiber' })]),
239
+ stone: Object.freeze([
240
+ Object.freeze({ id: 'speckle', label: 'Speckled' }),
241
+ Object.freeze({ id: 'veined', label: 'Veined' }),
242
+ Object.freeze({ id: 'porous', label: 'Porous' }),
243
+ ]),
244
+ wood: Object.freeze([
245
+ Object.freeze({ id: 'grain', label: 'Plain grain' }),
246
+ Object.freeze({ id: 'bark', label: 'Bark' }),
247
+ Object.freeze({ id: 'cracked', label: 'Cracked' }),
248
+ ]),
249
+ });
250
+
251
+ const STYLE_SHADERS = {
252
+ 'ash:powder': shadeAsh,
253
+ 'bone:bleached': shadeBleachedBone,
254
+ 'bone:porous': shadeBone,
255
+ 'leaf:veins': shadeLeaf,
256
+ 'metal:brushed': shadeMetal,
257
+ 'metal:galvanized': shadeGalvanized,
258
+ 'metal:painted': shadePaintedMetal,
259
+ 'organicFiber:fiber': shadeFiber,
260
+ 'stone:porous': shadePorousStone,
261
+ 'stone:speckle': shadeStone,
262
+ 'stone:veined': shadeVeinedStone,
263
+ 'wood:bark': shadeBark,
264
+ 'wood:cracked': shadeCrackedWood,
265
+ 'wood:grain': shadeWood,
266
+ };
267
+
268
+ /**
269
+ * Returns the cached detail texture for a material kind + style at a
270
+ * tiling scale. Textures are owned by this cache (evicted LRU, disposed
271
+ * on eviction) — asset disposal must NOT dispose `material.map`.
272
+ */
273
+ export function getDebrisDetailTexture(kind, seed, style = 'auto', scale = 1) {
274
+ const styles = DEBRIS_TEXTURE_STYLES[kind] ?? DEBRIS_TEXTURE_STYLES.stone;
275
+ const resolved = styles.some((entry) => entry.id === style) ? style : styles[0].id;
276
+ const shader = STYLE_SHADERS[`${kind}:${resolved}`] ?? shadeStone;
277
+ const scaleKey = Math.round(scale * 10) / 10;
278
+ const key = `${kind}:${resolved}:${seed >>> 0}:${scaleKey}`;
279
+ const hit = cache.get(key);
280
+ if (hit) {
281
+ cache.delete(key);
282
+ cache.set(key, hit);
283
+ return hit;
284
+ }
285
+ const texture = bakeTexture(shader(hashCombine(seed >>> 0, kind.length * 37 + resolved.length)));
286
+ texture.repeat.set(scaleKey, scaleKey);
287
+ texture.name = `Debris detail ${key}`;
288
+ cache.set(key, texture);
289
+ if (cache.size > CACHE_LIMIT) {
290
+ const [oldestKey, oldest] = cache.entries().next().value;
291
+ cache.delete(oldestKey);
292
+ oldest.dispose();
293
+ }
294
+ return texture;
295
+ }
296
+
297
+ // User-uploaded textures, cached by data URL. Browser only (Image
298
+ // decode); headless generation skips custom maps.
299
+ const customCache = new Map();
300
+
301
+ export function getCustomDebrisTexture(customTexture, scale = 1) {
302
+ if (!customTexture?.dataUrl || typeof document === 'undefined') return null;
303
+ const scaleKey = Math.round(scale * 10) / 10;
304
+ const key = `${customTexture.dataUrl.length}:${customTexture.dataUrl.slice(-40)}:${scaleKey}`;
305
+ const hit = customCache.get(key);
306
+ if (hit) return hit;
307
+ const texture = new THREE.TextureLoader().load(customTexture.dataUrl);
308
+ texture.colorSpace = THREE.SRGBColorSpace;
309
+ texture.wrapS = THREE.RepeatWrapping;
310
+ texture.wrapT = THREE.RepeatWrapping;
311
+ texture.repeat.set(scaleKey, scaleKey);
312
+ texture.name = `Debris custom ${customTexture.name ?? 'texture'}`;
313
+ customCache.set(key, texture);
314
+ if (customCache.size > 8) {
315
+ const [oldestKey, oldest] = customCache.entries().next().value;
316
+ customCache.delete(oldestKey);
317
+ oldest.dispose();
318
+ }
319
+ return texture;
320
+ }
321
+
322
+ /**
323
+ * Material kind + auto style per debris type/variant. Logs, root stumps,
324
+ * and bark chips default to bark; everything else takes its kind's first
325
+ * style.
326
+ */
327
+ export function debrisTextureAuto(type, variant) {
328
+ if (type === 'wood') {
329
+ const bark = variant === 'logs' || variant === 'rootStump' || variant === 'barkChips';
330
+ return { kind: 'wood', style: bark ? 'bark' : 'grain' };
331
+ }
332
+ if (type === 'bone') return { kind: 'bone', style: 'porous' };
333
+ if (type === 'stone') return { kind: 'stone', style: 'speckle' };
334
+ if (type === 'metal') return { kind: 'metal', style: 'brushed' };
335
+ if (type === 'ash') return { kind: 'ash', style: 'powder' };
336
+ return variant === 'leafLitter'
337
+ ? { kind: 'leaf', style: 'veins' }
338
+ : { kind: 'organicFiber', style: 'fiber' };
339
+ }
340
+
341
+ /** @deprecated kept for callers that only need the kind. */
342
+ export function debrisTextureKind(type, variant) {
343
+ return debrisTextureAuto(type, variant).kind;
344
+ }
345
+
346
+ /**
347
+ * Ensures UVs exist for texture sampling. Geometry without UVs (surface
348
+ * nets output, custom slabs) gets a dominant-axis box projection scaled so
349
+ * the tile repeats ~repeat times across the mesh — plenty for detail maps,
350
+ * and seam-free enough at toon contrast.
351
+ */
352
+ export function ensureDebrisUvs(geometry, repeat = 2) {
353
+ if (geometry.attributes.uv) return;
354
+ const position = geometry.attributes.position;
355
+ const normal = geometry.attributes.normal;
356
+ if (!position) return;
357
+ geometry.computeBoundingSphere();
358
+ const scale = repeat / Math.max(geometry.boundingSphere?.radius ?? 1, 1e-4) / 2;
359
+ const uvs = new Float32Array(position.count * 2);
360
+ for (let i = 0; i < position.count; i += 1) {
361
+ const nx = normal ? Math.abs(normal.getX(i)) : 0;
362
+ const ny = normal ? Math.abs(normal.getY(i)) : 1;
363
+ const nz = normal ? Math.abs(normal.getZ(i)) : 0;
364
+ let u;
365
+ let v;
366
+ if (nx >= ny && nx >= nz) {
367
+ u = position.getZ(i);
368
+ v = position.getY(i);
369
+ } else if (ny >= nz) {
370
+ u = position.getX(i);
371
+ v = position.getZ(i);
372
+ } else {
373
+ u = position.getX(i);
374
+ v = position.getY(i);
375
+ }
376
+ uvs[i * 2] = u * scale + 0.5;
377
+ uvs[i * 2 + 1] = v * scale + 0.5;
378
+ }
379
+ geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
380
+ }
@@ -0,0 +1,5 @@
1
+ export * from './debrisGenerator.js';
2
+ export * from './debrisPhysics.js';
3
+ export * from './debrisPresets.js';
4
+ export * from './debrisSettings.js';
5
+ export * from './debrisTextures.js';
@@ -0,0 +1,95 @@
1
+ // Generic value coercion between settings-schema fields and DOM controls.
2
+ // Field metadata comes from the settings modules' *_SETTING_FIELD_SCHEMA
3
+ // exports (type, range, options, optionLabels, defaultValue).
4
+
5
+ function clamp01(value) {
6
+ return Math.min(1, Math.max(0, Number(value) || 0));
7
+ }
8
+
9
+ function toHexChannel(value) {
10
+ return Math.round(clamp01(value) * 255)
11
+ .toString(16)
12
+ .padStart(2, '0');
13
+ }
14
+
15
+ export function colorToHex(value) {
16
+ if (value?.isColor) return `#${toHexChannel(value.r)}${toHexChannel(value.g)}${toHexChannel(value.b)}`;
17
+ if (Array.isArray(value) && value.length >= 3) {
18
+ return `#${toHexChannel(value[0])}${toHexChannel(value[1])}${toHexChannel(value[2])}`;
19
+ }
20
+ return '#ffffff';
21
+ }
22
+
23
+ export function hexToColorArray(value, fallback = [1, 1, 1]) {
24
+ const normalized = String(value ?? '').trim().replace(/^#/, '');
25
+ if (!/^[0-9a-f]{6}$/i.test(normalized)) return fallback;
26
+ return [
27
+ parseInt(normalized.slice(0, 2), 16) / 255,
28
+ parseInt(normalized.slice(2, 4), 16) / 255,
29
+ parseInt(normalized.slice(4, 6), 16) / 255,
30
+ ];
31
+ }
32
+
33
+ export function vectorToArray(value, fallback = []) {
34
+ if (value?.isVector2) return [value.x, value.y];
35
+ if (value?.isVector3) return [value.x, value.y, value.z];
36
+ if (value?.isVector4) return [value.x, value.y, value.z, value.w];
37
+ if (Array.isArray(value)) return [...value];
38
+ if (value && typeof value === 'object') {
39
+ return [value.x, value.y, value.z, value.w].filter((entry) => entry !== undefined);
40
+ }
41
+ return [...fallback];
42
+ }
43
+
44
+ export function formatFieldValue(value, field) {
45
+ if (field.type === 'boolean') return value ? 'On' : 'Off';
46
+ if (field.type === 'select') return field.optionLabels?.[String(value)] ?? String(value ?? '');
47
+ if (field.type === 'number') {
48
+ const number = Number(value);
49
+ return Number.isFinite(number) ? number.toFixed(field.range?.step < 0.01 ? 3 : 2) : String(value);
50
+ }
51
+ if (field.type === 'color') return colorToHex(value);
52
+ if (field.type.startsWith('vector')) {
53
+ return vectorToArray(value, field.defaultValue)
54
+ .map((entry) => Number(entry).toFixed(2))
55
+ .join(', ');
56
+ }
57
+ return String(value ?? '');
58
+ }
59
+
60
+ export function readFieldValueFromSettings(settings, field) {
61
+ const group = settings?.[field.group] ?? {};
62
+ return group[field.key] ?? field.defaultValue;
63
+ }
64
+
65
+ export function readFieldValueFromControl(control, field) {
66
+ if (field.type === 'boolean') return Boolean(control.checked);
67
+ if (field.type === 'number') return Number(control.value);
68
+ if (field.type === 'color') return hexToColorArray(control.value, field.defaultValue);
69
+ if (field.type.startsWith('vector')) {
70
+ const size = Number(field.type.replace('vector', '')) || field.defaultValue?.length || 2;
71
+ const parts = String(control.value)
72
+ .split(',')
73
+ .map((entry) => Number(entry.trim()));
74
+ return Array.from({ length: size }, (_, index) => (
75
+ Number.isFinite(parts[index]) ? parts[index] : field.defaultValue[index] ?? 0
76
+ ));
77
+ }
78
+ if (field.type === 'select' && typeof field.defaultValue === 'number') return Number(control.value);
79
+ return control.value;
80
+ }
81
+
82
+ export function writeFieldControlValue(control, output, field, value, formatValue = formatFieldValue) {
83
+ if (field.type === 'boolean') {
84
+ control.checked = Boolean(value);
85
+ } else if (field.type === 'color') {
86
+ control.value = colorToHex(value);
87
+ } else if (field.type.startsWith('vector')) {
88
+ control.value = vectorToArray(value, field.defaultValue)
89
+ .map((entry) => Number(entry).toFixed(3))
90
+ .join(', ');
91
+ } else {
92
+ control.value = String(value ?? field.defaultValue ?? '');
93
+ }
94
+ if (output) output.textContent = formatValue(value, field);
95
+ }
@@ -0,0 +1,7 @@
1
+ // Debug/tuning UI barrel. Import from '@call-me-sensei/toonlab/debug'.
2
+ //
3
+ // Optional, DOM-dependent tooling: a schema-driven settings panel that turns
4
+ // any settings module's group/field schema into live controls. The labs use
5
+ // it for their HUD; games can ship it behind a debug flag or not at all.
6
+ export * from './fieldValues.js';
7
+ export * from './settingsPanel.js';
@@ -0,0 +1,170 @@
1
+ // Schema-driven settings panel: renders the setting groups exported by the
2
+ // settings modules (TOON_SETTING_GROUPS / TOON_SETTING_FIELD_SCHEMA and
3
+ // friends) as collapsible groups of typed controls, without knowing anything
4
+ // about what the settings do.
5
+ //
6
+ // const panel = createSettingsPanel({
7
+ // container: document.getElementById('toonSettingGroups'),
8
+ // groups: TOON_SETTING_GROUPS,
9
+ // fieldSchema: TOON_SETTING_FIELD_SCHEMA,
10
+ // getValue: (field) => readFieldValueFromSettings(settings, field),
11
+ // onChange: (field, value) => applyOverride(field, value),
12
+ // });
13
+ // panel.refresh(); // re-pull every control value via getValue
14
+ //
15
+ // The host owns the settings state; the panel only reads through getValue and
16
+ // reports edits through onChange (after echoing the value back into the
17
+ // control and its <output>).
18
+
19
+ import { formatFieldValue, readFieldValueFromControl, writeFieldControlValue } from './fieldValues.js';
20
+
21
+ function createFieldControl(field, options) {
22
+ const {
23
+ dataAttribute, idPrefix, rowClassName, getValue, onChange, isDisabled, formatValue,
24
+ } = options;
25
+
26
+ const row = document.createElement('div');
27
+ row.className = rowClassName;
28
+ row.dataset[dataAttribute] = field.id;
29
+
30
+ const controlId = `${idPrefix}_${field.group}_${field.key}`.replace(/[^a-z0-9_]/gi, '_');
31
+ const label = document.createElement('label');
32
+ label.htmlFor = controlId;
33
+ label.textContent = field.label;
34
+ label.title = field.description;
35
+
36
+ let control;
37
+ if (field.type === 'boolean') {
38
+ control = document.createElement('input');
39
+ control.type = 'checkbox';
40
+ } else if (field.type === 'select') {
41
+ control = document.createElement('select');
42
+ for (const option of field.options ?? []) {
43
+ const element = document.createElement('option');
44
+ element.value = String(option);
45
+ element.textContent = field.optionLabels?.[String(option)] ?? String(option);
46
+ control.append(element);
47
+ }
48
+ } else if (field.type === 'color') {
49
+ control = document.createElement('input');
50
+ control.type = 'color';
51
+ } else if (field.type === 'number') {
52
+ control = document.createElement('input');
53
+ control.type = 'range';
54
+ control.min = String(field.range?.min ?? 0);
55
+ control.max = String(field.range?.max ?? 1);
56
+ control.step = String(field.range?.step ?? 0.01);
57
+ } else {
58
+ control = document.createElement('input');
59
+ control.type = 'text';
60
+ }
61
+ control.id = controlId;
62
+ control.title = field.description;
63
+ control.disabled = Boolean(isDisabled(field));
64
+
65
+ const output = document.createElement('output');
66
+ output.id = `${controlId}Value`;
67
+ output.htmlFor = controlId;
68
+
69
+ const eventName = field.type === 'number' || field.type === 'color' ? 'input' : 'change';
70
+ control.addEventListener(eventName, () => {
71
+ const value = readFieldValueFromControl(control, field);
72
+ writeFieldControlValue(control, output, field, value, formatValue);
73
+ onChange(field, value);
74
+ });
75
+
76
+ row.append(label, control, output);
77
+ return row;
78
+ }
79
+
80
+ export function createSettingsPanel({
81
+ container,
82
+ groups,
83
+ fieldSchema,
84
+ getValue,
85
+ onChange,
86
+ fieldFilter = () => true,
87
+ isDisabled = () => false,
88
+ formatValue = formatFieldValue,
89
+ dataAttribute = 'settingsField',
90
+ idPrefix = 'setting',
91
+ rowClassName = 'hud-control settings-field-control',
92
+ groupClassName = 'toon-setting-group',
93
+ fieldsClassName = 'toon-setting-fields',
94
+ isGroupOpen = () => false,
95
+ prepend = [],
96
+ }) {
97
+ const fieldRows = [];
98
+ const controlOptions = { dataAttribute, idPrefix, rowClassName, getValue, onChange, isDisabled, formatValue };
99
+
100
+ function refresh() {
101
+ for (const { field, control, output } of fieldRows) {
102
+ control.disabled = Boolean(isDisabled(field));
103
+ writeFieldControlValue(control, output, field, getValue(field), formatValue);
104
+ }
105
+ }
106
+
107
+ if (container && container.childElementCount === 0) {
108
+ for (const node of prepend) container.append(node);
109
+
110
+ for (const group of groups) {
111
+ const fields = Object.values(fieldSchema[group.id] ?? {}).filter(fieldFilter);
112
+ if (fields.length === 0) continue;
113
+
114
+ const details = document.createElement('details');
115
+ details.className = groupClassName;
116
+ details.dataset.settingsGroup = group.id;
117
+ details.open = Boolean(isGroupOpen(group));
118
+
119
+ const summary = document.createElement('summary');
120
+ summary.textContent = group.label;
121
+ summary.title = group.description;
122
+
123
+ const fieldList = document.createElement('div');
124
+ fieldList.className = fieldsClassName;
125
+ for (const field of fields) {
126
+ const row = createFieldControl(field, controlOptions);
127
+ fieldList.append(row);
128
+ fieldRows.push({
129
+ control: row.querySelector('input, select'),
130
+ field,
131
+ output: row.querySelector('output'),
132
+ });
133
+ }
134
+
135
+ details.append(summary, fieldList);
136
+ container.append(details);
137
+ }
138
+
139
+ refresh();
140
+ }
141
+
142
+ return { container, refresh };
143
+ }
144
+
145
+ // A plain labelled <select> row for panel headers (preset pickers, debug view
146
+ // selectors) that live alongside the schema-generated groups.
147
+ export function createSelectRow({ id, label, title, options, value, onChange, rowClassName = 'hud-control' }) {
148
+ const row = document.createElement('div');
149
+ row.className = rowClassName;
150
+
151
+ const labelEl = document.createElement('label');
152
+ labelEl.htmlFor = id;
153
+ labelEl.textContent = label;
154
+ labelEl.title = title;
155
+
156
+ const select = document.createElement('select');
157
+ select.id = id;
158
+ select.title = title;
159
+ for (const option of options) {
160
+ const el = document.createElement('option');
161
+ el.value = option.value;
162
+ el.textContent = option.label;
163
+ if (option.value === value) el.selected = true;
164
+ select.append(el);
165
+ }
166
+ select.addEventListener('change', () => onChange(select.value));
167
+
168
+ row.append(labelEl, select);
169
+ return row;
170
+ }