@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,71 @@
1
+ // Curated three-tone palettes (primary base / secondary mid / accent
2
+ // highlight-or-detail) per debris family — one-click combinations that
3
+ // read well under the toon shader, mirroring Tree Lab's leaf palettes.
4
+ // Values are linear 0-1 RGB triplets matching surface.*Color.
5
+
6
+ const palette = (id, label, primaryColor, secondaryColor, accentColor) => Object.freeze({
7
+ accentColor: Object.freeze(accentColor),
8
+ id,
9
+ label,
10
+ primaryColor: Object.freeze(primaryColor),
11
+ secondaryColor: Object.freeze(secondaryColor),
12
+ });
13
+
14
+ export const DEBRIS_PALETTES = Object.freeze({
15
+ ash: Object.freeze([
16
+ palette('cold-ash', 'Cold ash', [0.035, 0.03, 0.025], [0.16, 0.15, 0.14], [0.55, 0.06, 0.01]),
17
+ palette('warm-embers', 'Warm embers', [0.05, 0.03, 0.02], [0.2, 0.16, 0.13], [0.85, 0.28, 0.04]),
18
+ palette('white-ash', 'White ash', [0.22, 0.21, 0.2], [0.42, 0.41, 0.39], [0.65, 0.6, 0.52]),
19
+ palette('sawdust', 'Fresh sawdust', [0.58, 0.47, 0.32], [0.78, 0.66, 0.47], [0.55, 0.4, 0.24]),
20
+ palette('volcanic', 'Volcanic', [0.06, 0.05, 0.06], [0.18, 0.16, 0.18], [0.9, 0.35, 0.12]),
21
+ ]),
22
+ bone: Object.freeze([
23
+ palette('bleached', 'Bleached ivory', [0.78, 0.71, 0.56], [0.93, 0.87, 0.7], [0.42, 0.31, 0.19]),
24
+ palette('aged-tan', 'Aged tan', [0.62, 0.52, 0.38], [0.8, 0.7, 0.52], [0.34, 0.24, 0.15]),
25
+ palette('fossil', 'Fossil brown', [0.42, 0.33, 0.24], [0.6, 0.5, 0.38], [0.24, 0.18, 0.12]),
26
+ palette('museum', 'Museum white', [0.85, 0.83, 0.78], [0.96, 0.95, 0.9], [0.55, 0.5, 0.42]),
27
+ palette('swamp-bone', 'Swamp stained', [0.45, 0.44, 0.3], [0.66, 0.63, 0.44], [0.25, 0.26, 0.14]),
28
+ ]),
29
+ metal: Object.freeze([
30
+ palette('steel-rust', 'Steel & rust', [0.25, 0.31, 0.32], [0.49, 0.54, 0.52], [0.72, 0.26, 0.08]),
31
+ palette('galvanized', 'Galvanized', [0.36, 0.42, 0.45], [0.62, 0.68, 0.7], [0.3, 0.34, 0.38]),
32
+ palette('painted-red', 'Painted red', [0.5, 0.12, 0.08], [0.72, 0.24, 0.16], [0.32, 0.36, 0.38]),
33
+ palette('copper', 'Copper verdigris', [0.45, 0.26, 0.15], [0.68, 0.42, 0.25], [0.2, 0.55, 0.45]),
34
+ palette('gunmetal', 'Gunmetal', [0.12, 0.13, 0.16], [0.3, 0.32, 0.36], [0.55, 0.45, 0.2]),
35
+ ]),
36
+ organic: Object.freeze([
37
+ palette('autumn', 'Autumn litter', [0.34, 0.18, 0.05], [0.64, 0.43, 0.12], [0.66, 0.23, 0.06]),
38
+ palette('fresh-fall', 'Fresh fall', [0.25, 0.3, 0.08], [0.55, 0.52, 0.14], [0.72, 0.4, 0.08]),
39
+ palette('dried', 'Sun dried', [0.42, 0.32, 0.18], [0.66, 0.55, 0.34], [0.5, 0.3, 0.12]),
40
+ palette('shoreline', 'Shoreline', [0.68, 0.58, 0.48], [0.92, 0.81, 0.67], [0.79, 0.46, 0.39]),
41
+ palette('deep-forest', 'Deep forest', [0.12, 0.16, 0.07], [0.3, 0.34, 0.14], [0.5, 0.36, 0.1]),
42
+ ]),
43
+ stone: Object.freeze([
44
+ palette('concrete', 'Concrete', [0.39, 0.4, 0.38], [0.58, 0.56, 0.51], [0.53, 0.3, 0.19]),
45
+ palette('red-brick', 'Red brick', [0.45, 0.18, 0.11], [0.68, 0.31, 0.18], [0.32, 0.19, 0.14]),
46
+ palette('slate', 'Slate blue', [0.12, 0.16, 0.18], [0.3, 0.36, 0.38], [0.2, 0.25, 0.26]),
47
+ palette('sandstone', 'Sandstone', [0.55, 0.42, 0.27], [0.76, 0.62, 0.42], [0.42, 0.28, 0.16]),
48
+ palette('basalt', 'Basalt', [0.08, 0.08, 0.09], [0.22, 0.22, 0.24], [0.4, 0.38, 0.35]),
49
+ palette('mossy', 'Mossy stone', [0.3, 0.33, 0.28], [0.5, 0.52, 0.44], [0.28, 0.42, 0.2]),
50
+ ]),
51
+ wood: Object.freeze([
52
+ palette('bark-brown', 'Bark brown', [0.34, 0.2, 0.11], [0.55, 0.36, 0.2], [0.7, 0.46, 0.24]),
53
+ palette('bleached-drift', 'Bleached driftwood', [0.52, 0.43, 0.31], [0.78, 0.68, 0.5], [0.52, 0.39, 0.25]),
54
+ palette('silver-grey', 'Silvered grey', [0.38, 0.37, 0.34], [0.6, 0.58, 0.53], [0.45, 0.4, 0.33]),
55
+ palette('redwood', 'Redwood', [0.32, 0.12, 0.07], [0.55, 0.25, 0.13], [0.72, 0.4, 0.2]),
56
+ palette('charred', 'Charred', [0.06, 0.05, 0.045], [0.2, 0.17, 0.15], [0.5, 0.2, 0.06]),
57
+ palette('mossy-log', 'Mossy log', [0.22, 0.17, 0.1], [0.4, 0.34, 0.2], [0.3, 0.45, 0.18]),
58
+ ]),
59
+ });
60
+
61
+ function nearly(a, b) {
62
+ return Math.abs(a - b) < 0.012;
63
+ }
64
+
65
+ /** Finds the palette matching the current surface colors, if any. */
66
+ export function matchDebrisPalette(type, surface) {
67
+ const palettes = DEBRIS_PALETTES[type] ?? [];
68
+ return palettes.find((entry) => ['primaryColor', 'secondaryColor', 'accentColor'].every(
69
+ (key) => entry[key].every((channel, index) => nearly(channel, surface[key][index])),
70
+ )) ?? null;
71
+ }
@@ -0,0 +1,198 @@
1
+ // Real-gravity settling for debris compositions. The generator's
2
+ // deterministic placement gives each piece a near-rest starting pose;
3
+ // this pass hands the pieces to Rapier (convex-hull colliders, gravity,
4
+ // friction) and lets them fall, tumble, and slide until every body
5
+ // sleeps — so piles, bundles, and scatters rest the way they would in
6
+ // real life, with no interpenetration and no floaters. Runs in browser
7
+ // and Node (rapier3d-compat inlines its WASM).
8
+
9
+ import * as THREE from 'three';
10
+
11
+ const MAX_STEPS = 300;
12
+ const SLEEP_CHECK_EVERY = 20;
13
+ // Every vertex goes into the hull: a sampled subset leaves mesh vertices
14
+ // outside the collider, and once the body rotates during the sim those
15
+ // vertices swing below the contact surface — pieces visually sink into
16
+ // the ground. Rapier's quickhull chews through this budget in ~ms.
17
+ const HULL_POINT_BUDGET = 30000;
18
+
19
+ let rapierPromise = null;
20
+
21
+ function getRapier() {
22
+ if (!rapierPromise) {
23
+ rapierPromise = import('@dimforge/rapier3d-compat').then(async (module) => {
24
+ const RAPIER = module.default ?? module;
25
+ await RAPIER.init();
26
+ return RAPIER;
27
+ });
28
+ }
29
+ return rapierPromise;
30
+ }
31
+
32
+ // One point cloud PER SUB-MESH, in the piece's body frame. A single hull
33
+ // around a whole piece bridges concavities (a forked branch rides half a
34
+ // unit up on the invisible envelope between its prongs); a compound body
35
+ // with one hull per tube/cap/knot hugs the real shape.
36
+ function collectMeshClouds(piece) {
37
+ const clouds = [];
38
+ const vertex = new THREE.Vector3();
39
+ const inverseRotation = piece.quaternion.clone().invert();
40
+ piece.traverse((mesh) => {
41
+ if (!mesh.isMesh) return;
42
+ const position = mesh.geometry.attributes.position;
43
+ if (!position || position.count < 4) return;
44
+ const stride = Math.max(1, Math.ceil(position.count / HULL_POINT_BUDGET));
45
+ const points = [];
46
+ for (let index = 0; index < position.count; index += stride) {
47
+ vertex
48
+ .fromBufferAttribute(position, index)
49
+ .applyMatrix4(mesh.matrixWorld)
50
+ .sub(piece.position)
51
+ .applyQuaternion(inverseRotation);
52
+ points.push(vertex.x, vertex.y, vertex.z);
53
+ }
54
+ if (points.length < 12) return;
55
+ // Merged meshes (Tree Lab branches, SDF skulls) are one concave
56
+ // geometry — slab-decompose along the longest axis so the hulls
57
+ // follow trunk and forks instead of bridging them.
58
+ for (const slab of splitCloudIntoSlabs(points)) clouds.push(slab);
59
+ });
60
+ return clouds;
61
+ }
62
+
63
+ function splitCloudIntoSlabs(points) {
64
+ const min = [Infinity, Infinity, Infinity];
65
+ const max = [-Infinity, -Infinity, -Infinity];
66
+ for (let index = 0; index < points.length; index += 3) {
67
+ for (let axis = 0; axis < 3; axis += 1) {
68
+ min[axis] = Math.min(min[axis], points[index + axis]);
69
+ max[axis] = Math.max(max[axis], points[index + axis]);
70
+ }
71
+ }
72
+ const extents = [max[0] - min[0], max[1] - min[1], max[2] - min[2]];
73
+ const longAxis = extents.indexOf(Math.max(...extents));
74
+ const sorted = [...extents].sort((a, b) => b - a);
75
+ const slabCount = Math.min(Math.max(Math.round(sorted[0] / Math.max(sorted[1], 1e-4)), 1), 8);
76
+ if (slabCount <= 1) return [new Float32Array(points)];
77
+ const buckets = Array.from({ length: slabCount }, () => []);
78
+ const span = extents[longAxis] || 1e-4;
79
+ for (let index = 0; index < points.length; index += 3) {
80
+ const t = (points[index + longAxis] - min[longAxis]) / span;
81
+ const bucket = Math.min(Math.floor(t * slabCount), slabCount - 1);
82
+ buckets[bucket].push(points[index], points[index + 1], points[index + 2]);
83
+ }
84
+ const slabs = buckets.filter((bucket) => bucket.length >= 12).map((bucket) => new Float32Array(bucket));
85
+ return slabs.length > 0 ? slabs : [new Float32Array(points)];
86
+ }
87
+
88
+ /**
89
+ * Simulates the asset's pieces under gravity and bakes the resting
90
+ * transforms. Deterministic for a given asset (fixed body order, fixed
91
+ * timestep). Skips ash/pile assets (mound geometry is its own ground).
92
+ */
93
+ export async function settleDebrisPhysics(root, { maxSteps = MAX_STEPS } = {}) {
94
+ const recipe = root.userData.debrisRecipe;
95
+ // Single pieces settle too — a lone branch balances on real contact
96
+ // points only under real gravity.
97
+ if (!recipe || recipe.asset.type === 'ash' || root.children.length === 0) return root;
98
+
99
+ const RAPIER = await getRapier();
100
+ // Physics runs in unscaled space; rest poses are scale-invariant, so
101
+ // the asset scale is restored afterwards.
102
+ const savedScale = root.scale.x;
103
+ root.scale.setScalar(1);
104
+ root.updateMatrixWorld(true);
105
+
106
+ const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
107
+ world.createCollider(
108
+ RAPIER.ColliderDesc.cuboid(200, 1, 200).setTranslation(0, -1, 0).setFriction(0.9),
109
+ );
110
+
111
+ const bodies = [];
112
+ for (const piece of root.children) {
113
+ piece.updateMatrixWorld(true);
114
+ const clouds = collectMeshClouds(piece);
115
+ if (clouds.length === 0) continue;
116
+ const body = world.createRigidBody(
117
+ RAPIER.RigidBodyDesc.dynamic()
118
+ // Tiny lift relieves any initial interpenetration without a pop.
119
+ .setTranslation(piece.position.x, piece.position.y + 0.02, piece.position.z)
120
+ .setRotation({
121
+ w: piece.quaternion.w, x: piece.quaternion.x, y: piece.quaternion.y, z: piece.quaternion.z,
122
+ })
123
+ // Low angular damping: pieces caught mid-tip fall over to a truly
124
+ // stable face instead of freezing upright on an end cap.
125
+ .setLinearDamping(0.4)
126
+ .setAngularDamping(0.25),
127
+ );
128
+ let attached = 0;
129
+ for (const cloud of clouds) {
130
+ const colliderDesc = RAPIER.ColliderDesc.convexHull(cloud);
131
+ if (!colliderDesc) continue;
132
+ world.createCollider(colliderDesc.setFriction(0.85).setRestitution(0.02), body);
133
+ attached += 1;
134
+ }
135
+ if (attached === 0) {
136
+ const bounds = new THREE.Box3().setFromObject(piece, true);
137
+ const size = bounds.getSize(new THREE.Vector3());
138
+ world.createCollider(
139
+ RAPIER.ColliderDesc.cuboid(
140
+ Math.max(size.x / 2, 0.01), Math.max(size.y / 2, 0.01), Math.max(size.z / 2, 0.01),
141
+ ).setFriction(0.85).setRestitution(0.02),
142
+ body,
143
+ );
144
+ }
145
+ bodies.push({ body, piece });
146
+ }
147
+
148
+ world.timestep = 1 / 60;
149
+ const run = (steps) => {
150
+ for (let step = 0; step < steps; step += 1) {
151
+ world.step();
152
+ if (step % SLEEP_CHECK_EVERY === SLEEP_CHECK_EVERY - 1
153
+ && bodies.every(({ body }) => body.isSleeping())) break;
154
+ }
155
+ };
156
+ const bake = () => {
157
+ for (const { body, piece } of bodies) {
158
+ const translation = body.translation();
159
+ const rotation = body.rotation();
160
+ piece.position.set(translation.x, translation.y, translation.z);
161
+ piece.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w);
162
+ piece.updateMatrixWorld(true);
163
+ }
164
+ };
165
+
166
+ run(maxSteps);
167
+ bake();
168
+
169
+ // Balanced-on-end poses (a log standing on its sawn face) are stable
170
+ // under pure gravity but almost never occur with real thrown debris —
171
+ // pieces arrive with momentum. Detect anything resting taller than
172
+ // wide, give it a deterministic tip-over impulse, and settle again.
173
+ const bounds = new THREE.Box3();
174
+ for (let round = 0; round < 3; round += 1) {
175
+ const standing = bodies.filter(({ piece }) => {
176
+ bounds.setFromObject(piece, true);
177
+ const height = bounds.max.y - bounds.min.y;
178
+ const footprint = Math.max(bounds.max.x - bounds.min.x, bounds.max.z - bounds.min.z);
179
+ return height > footprint * 1.25;
180
+ });
181
+ if (standing.length === 0) break;
182
+ for (const [index, { body }] of standing.entries()) {
183
+ body.wakeUp();
184
+ // Strong deterministic shove, direction varied per round so a piece
185
+ // wedged against neighbours eventually finds a way down.
186
+ const magnitude = body.mass() * (0.45 + round * 0.3);
187
+ const flip = (index + round) % 2 === 0 ? 1 : -1;
188
+ body.applyTorqueImpulse({ x: magnitude * flip, y: 0, z: magnitude * 0.5 * -flip }, true);
189
+ }
190
+ run(220);
191
+ bake();
192
+ }
193
+ world.free();
194
+
195
+ root.scale.setScalar(savedScale);
196
+ root.updateMatrixWorld(true);
197
+ return root;
198
+ }
@@ -0,0 +1,155 @@
1
+ import { createDebrisSettings } from './debrisSettings.js';
2
+
3
+ function preset(id, label, type, variant, description, overrides = {}) {
4
+ return Object.freeze({
5
+ description,
6
+ id,
7
+ label,
8
+ settings: createDebrisSettings({
9
+ ...overrides,
10
+ asset: { ...(overrides.asset ?? {}), type, variant },
11
+ }),
12
+ type,
13
+ variant,
14
+ });
15
+ }
16
+
17
+ export const BUILT_IN_DEBRIS_PRESETS = Object.freeze([
18
+ preset('bleached-driftwood', 'Bleached driftwood', 'wood', 'driftwood', 'Salt-worn pale wood with broken fibers.', {
19
+ asset: { count: 1, seed: 7301, spread: 0 },
20
+ shape: { branchiness: 0.38, crookedness: 0.86, length: 3.05, splinters: 0.8, thickness: 0.23 },
21
+ surface: { accentColor: [0.52, 0.39, 0.25], primaryColor: [0.52, 0.43, 0.31], secondaryColor: [0.78, 0.68, 0.5], variation: 0.24 },
22
+ }),
23
+ preset('forest-branch', 'Forest branch', 'wood', 'branch', 'Bark-dark fallen limb with secondary twigs.', {
24
+ asset: { count: 1, seed: 4407, spread: 0 },
25
+ shape: { branchiness: 0.74, crookedness: 0.5, length: 3.2, splinters: 0.38, thickness: 0.25 },
26
+ surface: { accentColor: [0.38, 0.16, 0.045], primaryColor: [0.12, 0.055, 0.018], secondaryColor: [0.28, 0.13, 0.045], variation: 0.16 },
27
+ }),
28
+ preset('dry-twig-pile', 'Dry twig pile', 'wood', 'twigPile', 'A light scatter of snapped dry sticks.', {
29
+ asset: { count: 12, seed: 991, spread: 1.25 },
30
+ shape: { branchiness: 0.18, crookedness: 0.58, length: 0.92, splinters: 0.62, thickness: 0.055 },
31
+ surface: { accentColor: [0.42, 0.2, 0.07], primaryColor: [0.16, 0.07, 0.02], secondaryColor: [0.35, 0.17, 0.055] },
32
+ }),
33
+ preset('weathered-leg-bones', 'Weathered long bones', 'bone', 'longBone', 'Sun-bleached animal bones with enlarged joints.', {
34
+ asset: { count: 5, seed: 2204, spread: 1.35 },
35
+ shape: { curvature: 0.24, damage: 0.48, jointSize: 1.32, length: 1.18, thickness: 0.1 },
36
+ }),
37
+ preset('small-animal-skull', 'Small animal skull', 'bone', 'skull', 'Stylized cranium, sockets, snout, and broken jaw.', {
38
+ asset: { count: 1, seed: 8764, spread: 0 },
39
+ shape: { curvature: 0.2, damage: 0.62, jointSize: 1.08, length: 1.4, thickness: 0.16 },
40
+ }),
41
+ preset('herbivore-jaw', 'Herbivore jaw bones', 'bone', 'jawBone', 'Half jaws with molar rows and a bare diastema.', {
42
+ asset: { count: 3, seed: 4152, spread: 1.05 },
43
+ shape: { curvature: 0.4, damage: 0.35, jointSize: 1.1, length: 1.5, thickness: 0.11 },
44
+ }),
45
+ preset('shed-antlers', 'Shed antlers', 'bone', 'antler', 'Curving antler beams with forked tines.', {
46
+ asset: { count: 2, seed: 6108, spread: 1.1 },
47
+ shape: { curvature: 0.76, damage: 0.22, jointSize: 0.92, length: 1.85, thickness: 0.095 },
48
+ }),
49
+ preset('concrete-rubble', 'Concrete rubble', 'stone', 'rubble', 'Angular construction chunks with dusty variation.', {
50
+ asset: { count: 13, seed: 3041, spread: 1.55 },
51
+ shape: { brickRatio: 0.12, chunkSize: 0.48, detail: 0.72, sharpness: 0.8, stacking: 0.45 },
52
+ }),
53
+ preset('broken-red-bricks', 'Broken red bricks', 'stone', 'bricks', 'Recognizable brick fragments mixed with chips.', {
54
+ asset: { count: 11, seed: 8211, spread: 1.45 },
55
+ shape: { brickRatio: 0.92, chunkSize: 0.52, detail: 0.7, sharpness: 0.92, stacking: 0.42 },
56
+ surface: { accentColor: [0.32, 0.19, 0.14], primaryColor: [0.45, 0.18, 0.11], secondaryColor: [0.68, 0.31, 0.18] },
57
+ }),
58
+ preset('slate-shards', 'Slate shards', 'stone', 'shards', 'Thin sharp fragments with cool stone tones.', {
59
+ asset: { count: 17, seed: 1517, spread: 1.6 },
60
+ shape: { brickRatio: 0, chunkSize: 0.38, detail: 0.86, sharpness: 0.95, stacking: 0.28 },
61
+ surface: { accentColor: [0.2, 0.25, 0.26], primaryColor: [0.12, 0.16, 0.18], secondaryColor: [0.3, 0.36, 0.38] },
62
+ }),
63
+ preset('shattered-planks', 'Shattered planks', 'wood', 'planks', 'Snapped weathered boards with knotholes and warp.', {
64
+ asset: { count: 7, seed: 5217, spread: 1.4 },
65
+ shape: { branchiness: 0.6, crookedness: 0.55, length: 2.4, splinters: 0.7, thickness: 0.14 },
66
+ surface: { accentColor: [0.3, 0.24, 0.16], primaryColor: [0.42, 0.33, 0.22], secondaryColor: [0.6, 0.5, 0.35] },
67
+ }),
68
+ preset('firewood-logs', 'Firewood logs', 'wood', 'logs', 'Short sawn logs with pale cut ends and branch stubs.', {
69
+ asset: { count: 6, seed: 883, spread: 1.1 },
70
+ shape: { branchiness: 0.5, crookedness: 0.35, length: 1.6, splinters: 0.2, thickness: 0.18 },
71
+ }),
72
+ preset('gathered-sticks', 'Gathered stick bundle', 'wood', 'twigPile', 'A tied-together bunch of dry sticks.', {
73
+ asset: { arrangement: 'bundle', count: 14, messiness: 0.35, seed: 4471, spread: 0.5 },
74
+ shape: { branchiness: 0.12, crookedness: 0.4, kinks: 0.35, length: 1.3, splinters: 0.5, thickness: 0.05 },
75
+ }),
76
+ preset('dead-root-stump', 'Dead root stump', 'wood', 'rootStump', 'Weathered root ball with twisting dead roots.', {
77
+ asset: { count: 1, seed: 9925, spread: 0 },
78
+ shape: { barkStripped: 0.75, branchiness: 0.7, crookedness: 0.8, kinks: 0.55, length: 2.2, splinters: 0.3, thickness: 0.14 },
79
+ surface: { accentColor: [0.72, 0.65, 0.52], primaryColor: [0.3, 0.24, 0.17], secondaryColor: [0.52, 0.45, 0.34] },
80
+ }),
81
+ preset('bark-chip-litter', 'Bark chip litter', 'wood', 'barkChips', 'Curled bark flakes, dark outside and pale inside.', {
82
+ asset: { arrangement: 'patch', count: 20, seed: 2210, spread: 1.5 },
83
+ shape: { length: 1.4, thickness: 0.12 },
84
+ }),
85
+ preset('log-pile', 'Log pile', 'wood', 'logs', 'Firewood heaped into a loose stack.', {
86
+ asset: { arrangement: 'heap', count: 9, messiness: 0.4, seed: 5150, spread: 1.3 },
87
+ shape: { branchiness: 0.4, crookedness: 0.3, length: 1.5, splinters: 0.2, thickness: 0.17 },
88
+ }),
89
+ preset('sawdust-heap', 'Sawdust heap', 'ash', 'sawdust', 'Fresh pale sawdust with embedded slivers.', {
90
+ asset: { count: 8, seed: 7302, spread: 0.55 },
91
+ shape: { charcoal: 0.3, embers: 0, footprint: 1.2, moundHeight: 0.34, rim: 0.15 },
92
+ surface: { accentColor: [0.55, 0.4, 0.24], primaryColor: [0.58, 0.47, 0.32], secondaryColor: [0.78, 0.66, 0.47] },
93
+ }),
94
+ preset('river-pebbles', 'River pebbles', 'stone', 'riverstones', 'Smooth flat skipping stones in cool greys.', {
95
+ asset: { count: 15, seed: 3320, spread: 1.35 },
96
+ shape: { banding: 0.3, chunkSize: 0.26, detail: 0.35, flatness: 0.7, sharpness: 0.1 },
97
+ }),
98
+ preset('obsidian-shards', 'Obsidian shards', 'stone', 'obsidian', 'Glassy tapered fragments with conchoidal ridges.', {
99
+ asset: { count: 9, seed: 6644, spread: 1.2 },
100
+ shape: { banding: 0.7, chunkSize: 0.3, detail: 0.55, flatness: 0.25, sharpness: 0.85 },
101
+ surface: { accentColor: [0.16, 0.13, 0.22], primaryColor: [0.05, 0.05, 0.07], secondaryColor: [0.18, 0.19, 0.24] },
102
+ }),
103
+ preset('meteorite-fall', 'Meteorite fall', 'stone', 'meteor', 'Cratered pitted stones with a scorched crust.', {
104
+ asset: { count: 5, seed: 7811, spread: 1.3 },
105
+ shape: { banding: 0.1, chunkSize: 0.42, detail: 0.8, flatness: 0.15, sharpness: 0.2 },
106
+ surface: { accentColor: [0.45, 0.2, 0.08], primaryColor: [0.1, 0.09, 0.09], secondaryColor: [0.24, 0.21, 0.19] },
107
+ }),
108
+ preset('rough-gems', 'Rough gems', 'stone', 'gems', 'Faceted uncut crystals with beveled edges.', {
109
+ asset: { count: 8, seed: 1092, spread: 1.05 },
110
+ shape: { banding: 0.5, chunkSize: 0.2, detail: 0.15, flatness: 0.1, sharpness: 0.75 },
111
+ surface: { accentColor: [0.16, 0.5, 0.55], primaryColor: [0.1, 0.26, 0.36], secondaryColor: [0.36, 0.68, 0.72] },
112
+ }),
113
+ preset('rusted-sheet-metal', 'Rusted sheet metal', 'metal', 'sheets', 'Bent, corrugated sheets with oxidized edges.', {
114
+ asset: { count: 5, seed: 5604, spread: 1.35 },
115
+ shape: { bend: 0.82, corrugation: 0.62, rust: 0.82, sheetSize: 1.05, wireChance: 0.08 },
116
+ }),
117
+ preset('crushed-cans', 'Crushed cans', 'metal', 'cans', 'Low-sided dented cans scattered on the ground.', {
118
+ asset: { count: 9, seed: 773, spread: 1.45 },
119
+ shape: { bend: 0.74, corrugation: 0.16, rust: 0.34, sheetSize: 0.48, wireChance: 0 },
120
+ surface: { accentColor: [0.8, 0.25, 0.07], primaryColor: [0.3, 0.42, 0.45], secondaryColor: [0.7, 0.78, 0.74] },
121
+ }),
122
+ preset('workshop-scrap', 'Workshop scrap', 'metal', 'scrapPile', 'Mixed plates, pipes, cans, and curled wire.', {
123
+ asset: { count: 12, seed: 4690, spread: 1.5 },
124
+ shape: { bend: 0.66, corrugation: 0.28, rust: 0.7, sheetSize: 0.62, wireChance: 0.62 },
125
+ }),
126
+ preset('autumn-leaf-litter', 'Autumn leaf litter', 'organic', 'leafLitter', 'A broad scatter of curled dry leaves.', {
127
+ asset: { count: 28, seed: 9318, spread: 1.8 },
128
+ shape: { coneRatio: 0.12, coverage: 1.45, curl: 0.66, dryness: 0.9, leafSize: 0.22 },
129
+ }),
130
+ preset('forest-pinecones', 'Forest pinecones', 'organic', 'pinecones', 'Layered cones with a few needles and husks.', {
131
+ asset: { count: 10, seed: 3008, spread: 1.4 },
132
+ shape: { coneRatio: 0.9, coverage: 0.85, curl: 0.24, dryness: 0.68, leafSize: 0.38 },
133
+ }),
134
+ preset('shoreline-shells', 'Shoreline shells', 'organic', 'shells', 'Fan shells and worn spiral fragments.', {
135
+ asset: { count: 13, seed: 1182, spread: 1.55 },
136
+ shape: { coneRatio: 0.45, coverage: 0.92, curl: 0.4, dryness: 0.5, leafSize: 0.3 },
137
+ surface: { accentColor: [0.79, 0.46, 0.39], primaryColor: [0.68, 0.58, 0.48], secondaryColor: [0.92, 0.81, 0.67] },
138
+ }),
139
+ preset('cold-ash-pile', 'Cold ash pile', 'ash', 'ashPile', 'A soft grey mound with scattered black remains.', {
140
+ asset: { count: 14, seed: 6721, spread: 0.65 },
141
+ shape: { charcoal: 0.38, embers: 0, footprint: 1.5, moundHeight: 0.32, rim: 0.36 },
142
+ }),
143
+ preset('spent-campfire', 'Spent campfire', 'ash', 'campfire', 'Charred crossed wood, ash, and a faint ember accent.', {
144
+ asset: { count: 10, seed: 2026, spread: 0.5 },
145
+ shape: { charcoal: 0.76, embers: 0.18, footprint: 1.65, moundHeight: 0.24, rim: 0.68 },
146
+ }),
147
+ preset('charcoal-scatter', 'Charcoal scatter', 'ash', 'charcoal', 'Cracked black fuel pieces and powder.', {
148
+ asset: { count: 18, seed: 8146, spread: 1.15 },
149
+ shape: { charcoal: 0.94, embers: 0.04, footprint: 1.25, moundHeight: 0.12, rim: 0.2 },
150
+ }),
151
+ ]);
152
+
153
+ export function findDebrisPreset(id) {
154
+ return BUILT_IN_DEBRIS_PRESETS.find((entry) => entry.id === id) ?? null;
155
+ }