@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.
- package/ATTRIBUTION.md +40 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/package.json +97 -0
- package/src/character/characterRig.js +353 -0
- package/src/character/freestyleSwimClip.js +541 -0
- package/src/character/index.js +4 -0
- package/src/character/modelLoader.js +433 -0
- package/src/core/materialRoles.js +438 -0
- package/src/core/presetDocuments.js +110 -0
- package/src/core/shaderBackend.js +24 -0
- package/src/debrisgen/debrisFields.js +544 -0
- package/src/debrisgen/debrisGenerator.js +1929 -0
- package/src/debrisgen/debrisPalettes.js +71 -0
- package/src/debrisgen/debrisPhysics.js +198 -0
- package/src/debrisgen/debrisPresets.js +155 -0
- package/src/debrisgen/debrisSettings.js +333 -0
- package/src/debrisgen/debrisTextures.js +380 -0
- package/src/debrisgen/index.js +5 -0
- package/src/debug/fieldValues.js +95 -0
- package/src/debug/index.js +7 -0
- package/src/debug/settingsPanel.js +170 -0
- package/src/environment/environmentAmbientProbe.js +160 -0
- package/src/environment/environmentMaterialAdapter.js +336 -0
- package/src/environment/environmentMaterialClassifier.js +178 -0
- package/src/environment/environmentPlanarReflection.js +201 -0
- package/src/environment/environmentPresets.js +443 -0
- package/src/environment/environmentRigs.js +684 -0
- package/src/environment/environmentSettings.js +504 -0
- package/src/environment/environmentShaderMaterials.js +318 -0
- package/src/environment/environmentSunShadowPass.js +268 -0
- package/src/environment/environmentTextureResolver.js +161 -0
- package/src/environment/environmentTimeOfDay.js +167 -0
- package/src/environment/environmentVertexAo.js +188 -0
- package/src/environment/index.js +8 -0
- package/src/environment/scanAssetStylize.js +103 -0
- package/src/index.js +21 -0
- package/src/loaders/index.js +1 -0
- package/src/post/index.js +2 -0
- package/src/post/postProcessing.js +1087 -0
- package/src/rockgen/export/glbExport.js +177 -0
- package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
- package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
- package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
- package/src/rockgen/index.js +14 -0
- package/src/rockgen/mesh/meshAttributes.js +374 -0
- package/src/rockgen/mesh/meshDocument.js +158 -0
- package/src/rockgen/mesh/surfaceNets.js +329 -0
- package/src/rockgen/noise/cellularNoise3.js +92 -0
- package/src/rockgen/noise/prng.js +62 -0
- package/src/rockgen/noise/simplexNoise3.js +89 -0
- package/src/rockgen/noise/valueNoise3.js +87 -0
- package/src/rockgen/rockDocument.js +255 -0
- package/src/rockgen/rockHelpers.js +8 -0
- package/src/rockgen/rockgenPresets.js +559 -0
- package/src/rockgen/rockgenSettings.js +756 -0
- package/src/rockgen/sdf/fieldCompiler.js +544 -0
- package/src/rockgen/sdf/sculptEdits.js +66 -0
- package/src/rockgen/sdf/sdfModifiers.js +45 -0
- package/src/rockgen/sdf/sdfOps.js +37 -0
- package/src/rockgen/sdf/sdfPrimitives.js +88 -0
- package/src/shaders-tsl/anime.js +963 -0
- package/src/shaders-tsl/chunks/character-color.js +64 -0
- package/src/shaders-tsl/chunks/character-highlights.js +174 -0
- package/src/shaders-tsl/chunks/character-lighting.js +309 -0
- package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
- package/src/shaders-tsl/chunks/character-roles.js +52 -0
- package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
- package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
- package/src/shaders-tsl/chunks/character-skinning.js +140 -0
- package/src/shaders-tsl/chunks/environment-color.js +47 -0
- package/src/shaders-tsl/chunks/environment-debug.js +79 -0
- package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
- package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
- package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
- package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
- package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
- package/src/shaders-tsl/chunks/water-color.js +190 -0
- package/src/shaders-tsl/chunks/water-common.js +124 -0
- package/src/shaders-tsl/chunks/water-foam.js +93 -0
- package/src/shaders-tsl/chunks/water-lighting.js +116 -0
- package/src/shaders-tsl/chunks/water-ripple.js +64 -0
- package/src/shaders-tsl/chunks/water-waves.js +100 -0
- package/src/shaders-tsl/environment-ao-overlay.js +60 -0
- package/src/shaders-tsl/environment.js +680 -0
- package/src/shaders-tsl/flower.js +267 -0
- package/src/shaders-tsl/grass.js +193 -0
- package/src/shaders-tsl/post-composite.js +461 -0
- package/src/shaders-tsl/sky.js +175 -0
- package/src/shaders-tsl/tree-leaf.js +264 -0
- package/src/shaders-tsl/water-breaker.js +423 -0
- package/src/shaders-tsl/water-kelp.js +171 -0
- package/src/shaders-tsl/water-rain.js +107 -0
- package/src/shaders-tsl/water-simulation.js +140 -0
- package/src/shaders-tsl/water-splash.js +298 -0
- package/src/shaders-tsl/water.js +509 -0
- package/src/sky/index.js +2 -0
- package/src/sky/stylizedSky.js +341 -0
- package/src/toon/characterRenderPasses.js +646 -0
- package/src/toon/index.js +4 -0
- package/src/toon/settings/alphaSettings.js +170 -0
- package/src/toon/settings/averageShadowSettings.js +181 -0
- package/src/toon/settings/baseTextureSettings.js +133 -0
- package/src/toon/settings/celShadeSettings.js +96 -0
- package/src/toon/settings/contactShadowSettings.js +114 -0
- package/src/toon/settings/eyeHighlightSettings.js +187 -0
- package/src/toon/settings/faceLightingSettings.js +135 -0
- package/src/toon/settings/furSettings.js +112 -0
- package/src/toon/settings/glitterSettings.js +102 -0
- package/src/toon/settings/hairHighlightSettings.js +325 -0
- package/src/toon/settings/indirectLightSettings.js +244 -0
- package/src/toon/settings/localLightSettings.js +176 -0
- package/src/toon/settings/materialMapSettings.js +299 -0
- package/src/toon/settings/outlineSettings.js +263 -0
- package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
- package/src/toon/settings/rimLightSettings.js +268 -0
- package/src/toon/settings/sceneShadowSettings.js +147 -0
- package/src/toon/settings/selfShadowSettings.js +221 -0
- package/src/toon/settings/shadowColorSettings.js +238 -0
- package/src/toon/settings/skinToneSettings.js +150 -0
- package/src/toon/settings/specularSettings.js +314 -0
- package/src/toon/settings/stickerSettings.js +102 -0
- package/src/toon/toonMaterialAdapter.js +2235 -0
- package/src/toon/toonSettings.js +860 -0
- package/src/vegetation/flowerSpecies.js +260 -0
- package/src/vegetation/index.js +10 -0
- package/src/vegetation/stylizedBush.js +136 -0
- package/src/vegetation/stylizedFlower.js +213 -0
- package/src/vegetation/stylizedFlowers.js +297 -0
- package/src/vegetation/stylizedGrass.js +491 -0
- package/src/vegetation/stylizedTree.js +2449 -0
- package/src/vegetation/stylizedTreeFoliage.js +691 -0
- package/src/vegetation/treeExport.js +300 -0
- package/src/vegetation/treeRecipe.js +647 -0
- package/src/water/index.js +2 -0
- package/src/water/water.js +20 -0
- package/src/water/waterBreakerSystem.js +591 -0
- package/src/water/waterInteraction.js +193 -0
- package/src/water/waterMaterial.js +263 -0
- package/src/water/waterRain.js +136 -0
- package/src/water/waterRippleSimulation.js +227 -0
- package/src/water/waterScenePasses.js +415 -0
- package/src/water/waterSettings.js +1138 -0
- package/src/water/waterSplashSystem.js +251 -0
- package/src/water/waterSurface.js +475 -0
- 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
|
+
}
|