@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,374 @@
|
|
|
1
|
+
// Derived vertex attributes for meshed documents. Everything here is a
|
|
2
|
+
// function of the compiled field (gradient normals, SDF ambient occlusion)
|
|
3
|
+
// or of deterministic seeded noise (color variation), so attributes can be
|
|
4
|
+
// re-derived after any re-mesh or decimation and stay bit-identical for a
|
|
5
|
+
// given document.
|
|
6
|
+
|
|
7
|
+
import { cellular3 } from '../noise/cellularNoise3.js';
|
|
8
|
+
import { fbm3, valueNoise3 } from '../noise/valueNoise3.js';
|
|
9
|
+
|
|
10
|
+
function clamp01(value) {
|
|
11
|
+
return Math.min(Math.max(value, 0), 1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function smoothstep(edge0, edge1, value) {
|
|
15
|
+
const t = clamp01((value - edge0) / (edge1 - edge0));
|
|
16
|
+
return t * t * (3 - 2 * t);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fract(value) {
|
|
20
|
+
return value - Math.floor(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Surface colors are authored as display (sRGB) values in the settings UI
|
|
24
|
+
// but vertex colors feed the shader linearly, so convert once per bake
|
|
25
|
+
// (three colors, not per vertex — Math.pow stays out of the hot loop).
|
|
26
|
+
function srgbToLinear(color) {
|
|
27
|
+
return color.map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Central-difference field gradient per vertex, normalized. */
|
|
31
|
+
export function computeGradientNormals(evaluate, positions, epsilon) {
|
|
32
|
+
const normals = new Float32Array(positions.length);
|
|
33
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
34
|
+
const x = positions[i];
|
|
35
|
+
const y = positions[i + 1];
|
|
36
|
+
const z = positions[i + 2];
|
|
37
|
+
let nxv = evaluate(x + epsilon, y, z) - evaluate(x - epsilon, y, z);
|
|
38
|
+
let nyv = evaluate(x, y + epsilon, z) - evaluate(x, y - epsilon, z);
|
|
39
|
+
let nzv = evaluate(x, y, z + epsilon) - evaluate(x, y, z - epsilon);
|
|
40
|
+
const length = Math.sqrt(nxv * nxv + nyv * nyv + nzv * nzv);
|
|
41
|
+
if (length > 0) {
|
|
42
|
+
nxv /= length;
|
|
43
|
+
nyv /= length;
|
|
44
|
+
nzv /= length;
|
|
45
|
+
} else {
|
|
46
|
+
nyv = 1;
|
|
47
|
+
}
|
|
48
|
+
normals[i] = nxv;
|
|
49
|
+
normals[i + 1] = nyv;
|
|
50
|
+
normals[i + 2] = nzv;
|
|
51
|
+
}
|
|
52
|
+
return normals;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Five-tap SDF ambient occlusion along the vertex normal. Near-free since
|
|
57
|
+
* the field is already compiled, deterministic, and baked into the
|
|
58
|
+
* `envVertexAo` attribute (1 = open, matching environmentVertexAo.js).
|
|
59
|
+
*/
|
|
60
|
+
export function computeSdfAo(evaluate, positions, normals, { radius, strength }) {
|
|
61
|
+
const ao = new Float32Array(positions.length / 3);
|
|
62
|
+
const tapCount = 5;
|
|
63
|
+
for (let v = 0; v < ao.length; v += 1) {
|
|
64
|
+
const i = v * 3;
|
|
65
|
+
let occlusion = 0;
|
|
66
|
+
let weight = 0.5;
|
|
67
|
+
let weightSum = 0;
|
|
68
|
+
for (let tap = 1; tap <= tapCount; tap += 1) {
|
|
69
|
+
const t = (radius * tap) / tapCount;
|
|
70
|
+
const d = evaluate(
|
|
71
|
+
positions[i] + normals[i] * t,
|
|
72
|
+
positions[i + 1] + normals[i + 1] * t,
|
|
73
|
+
positions[i + 2] + normals[i + 2] * t,
|
|
74
|
+
);
|
|
75
|
+
occlusion += weight * Math.max(t - d, 0) / t;
|
|
76
|
+
weightSum += weight;
|
|
77
|
+
weight *= 0.5;
|
|
78
|
+
}
|
|
79
|
+
ao[v] = clamp01(1 - strength * (occlusion / weightSum));
|
|
80
|
+
}
|
|
81
|
+
return ao;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Baked stylized albedo: base color, cavity color mixed in by occlusion,
|
|
86
|
+
* top (snow/moss/highlight) color by up-slope and height band, plus a low
|
|
87
|
+
* amplitude seeded color variation.
|
|
88
|
+
*/
|
|
89
|
+
export function computeVertexColors(positions, normals, ao, surface, seed, bounds, tintAt = null) {
|
|
90
|
+
const colors = new Float32Array(positions.length);
|
|
91
|
+
const {
|
|
92
|
+
aoRadius, colorNoise, textureStyle, topHeightStart, topSlopeStart,
|
|
93
|
+
} = surface;
|
|
94
|
+
const baseColor = srgbToLinear(surface.baseColor);
|
|
95
|
+
const cavityColor = srgbToLinear(surface.cavityColor);
|
|
96
|
+
const topColor = srgbToLinear(surface.topColor);
|
|
97
|
+
const veinColor = srgbToLinear(surface.veinColor);
|
|
98
|
+
const stainColor = srgbToLinear(surface.stainColor);
|
|
99
|
+
const mossColor = srgbToLinear(surface.mossColor);
|
|
100
|
+
const lichenColor = srgbToLinear(surface.lichenColor);
|
|
101
|
+
const topCoatStrength = clamp01(surface.topCoatStrength ?? 1);
|
|
102
|
+
const textureScale = Math.max(Number(surface.textureScale) || 1, 0.1);
|
|
103
|
+
const textureStrength = clamp01(surface.textureStrength ?? 0);
|
|
104
|
+
const veinStrength = clamp01(surface.veinStrength ?? 0);
|
|
105
|
+
const stainStrength = clamp01(surface.stainStrength ?? 0);
|
|
106
|
+
const mossCoverage = clamp01(surface.mossCoverage ?? 0);
|
|
107
|
+
const lichenCoverage = clamp01(surface.lichenCoverage ?? 0);
|
|
108
|
+
const minY = bounds.min[1];
|
|
109
|
+
const invHeight = 1 / Math.max(bounds.max[1] - minY, 1e-6);
|
|
110
|
+
// Noise frequency scales with AO radius so the mottling tracks rock size.
|
|
111
|
+
const noiseFrequency = 2.4 / Math.max(aoRadius, 0.05);
|
|
112
|
+
const textureFrequency = textureScale / Math.max(aoRadius, 0.05);
|
|
113
|
+
const hasTexture = textureStyle !== 'none' && textureStrength > 0;
|
|
114
|
+
const hasVeins = veinStrength > 0;
|
|
115
|
+
const hasStain = stainStrength > 0;
|
|
116
|
+
const hasMoss = mossCoverage > 0;
|
|
117
|
+
const hasLichen = lichenCoverage > 0;
|
|
118
|
+
|
|
119
|
+
const maskFrequency = 1.2 / Math.max(aoRadius, 0.05);
|
|
120
|
+
for (let v = 0; v < ao.length; v += 1) {
|
|
121
|
+
const i = v * 3;
|
|
122
|
+
const cavity = clamp01((1 - ao[v]) * 1.4);
|
|
123
|
+
const slope = smoothstep(topSlopeStart, Math.min(topSlopeStart + 0.2, 1), normals[i + 1]);
|
|
124
|
+
const height = smoothstep(
|
|
125
|
+
topHeightStart,
|
|
126
|
+
Math.min(topHeightStart + 0.15, 1),
|
|
127
|
+
(positions[i + 1] - minY) * invHeight,
|
|
128
|
+
);
|
|
129
|
+
// Break the top-coat edge up with noise so moss/snow reads as patches
|
|
130
|
+
// instead of a painted-on waterline.
|
|
131
|
+
const mask = valueNoise3(
|
|
132
|
+
seed + 1,
|
|
133
|
+
positions[i] * maskFrequency,
|
|
134
|
+
positions[i + 1] * maskFrequency,
|
|
135
|
+
positions[i + 2] * maskFrequency,
|
|
136
|
+
);
|
|
137
|
+
const top = smoothstep(0.25, 0.75, slope * height + mask * 0.3);
|
|
138
|
+
const variation = 1 + colorNoise * valueNoise3(
|
|
139
|
+
seed,
|
|
140
|
+
positions[i] * noiseFrequency,
|
|
141
|
+
positions[i + 1] * noiseFrequency,
|
|
142
|
+
positions[i + 2] * noiseFrequency,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Erosion story (heightfield pieces): sediment deposits read as light
|
|
146
|
+
// dust settling in the gullies, water flow darkens its channels — the
|
|
147
|
+
// hand-painted weathering look, straight from the sim's masks.
|
|
148
|
+
let sedimentBlend = 0;
|
|
149
|
+
let flowBlend = 0;
|
|
150
|
+
if (tintAt) {
|
|
151
|
+
const tint = tintAt(positions[i], positions[i + 1], positions[i + 2]);
|
|
152
|
+
if (tint) {
|
|
153
|
+
sedimentBlend = clamp01(tint.sediment) * 0.4;
|
|
154
|
+
flowBlend = clamp01(tint.flow) * 0.3;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let red = baseColor[0] + (cavityColor[0] - baseColor[0]) * cavity;
|
|
159
|
+
let green = baseColor[1] + (cavityColor[1] - baseColor[1]) * cavity;
|
|
160
|
+
let blue = baseColor[2] + (cavityColor[2] - baseColor[2]) * cavity;
|
|
161
|
+
const topBlend = top * topCoatStrength;
|
|
162
|
+
red += (topColor[0] - red) * topBlend;
|
|
163
|
+
green += (topColor[1] - green) * topBlend;
|
|
164
|
+
blue += (topColor[2] - blue) * topBlend;
|
|
165
|
+
red += (topColor[0] - red) * sedimentBlend;
|
|
166
|
+
green += (topColor[1] - green) * sedimentBlend;
|
|
167
|
+
blue += (topColor[2] - blue) * sedimentBlend;
|
|
168
|
+
red += (cavityColor[0] - red) * flowBlend;
|
|
169
|
+
green += (cavityColor[1] - green) * flowBlend;
|
|
170
|
+
blue += (cavityColor[2] - blue) * flowBlend;
|
|
171
|
+
|
|
172
|
+
if (hasTexture) {
|
|
173
|
+
const tx = positions[i] * textureFrequency;
|
|
174
|
+
const ty = positions[i + 1] * textureFrequency;
|
|
175
|
+
const tz = positions[i + 2] * textureFrequency;
|
|
176
|
+
const grain = fbm3(seed + 17, tx, ty, tz, 4, 2, 0.52);
|
|
177
|
+
if (textureStyle === 'granite') {
|
|
178
|
+
const fine = fbm3(seed + 23, tx * 3.4, ty * 3.4, tz * 3.4, 3, 2.1, 0.45);
|
|
179
|
+
const flecks = cellular3(seed + 29, tx * 4.2, ty * 4.2, tz * 4.2, 0.85);
|
|
180
|
+
const paleFleck = (1 - smoothstep(0.06, 0.2, flecks.f1)) * textureStrength * 0.42;
|
|
181
|
+
const darkFleck = smoothstep(0.45, 0.95, -fine) * textureStrength * 0.22;
|
|
182
|
+
const tone = 1 + fine * textureStrength * 0.12;
|
|
183
|
+
red *= tone;
|
|
184
|
+
green *= tone;
|
|
185
|
+
blue *= tone;
|
|
186
|
+
red += (0.78 - red) * paleFleck;
|
|
187
|
+
green += (0.77 - green) * paleFleck;
|
|
188
|
+
blue += (0.72 - blue) * paleFleck;
|
|
189
|
+
red += (0.24 - red) * darkFleck;
|
|
190
|
+
green += (0.24 - green) * darkFleck;
|
|
191
|
+
blue += (0.25 - blue) * darkFleck;
|
|
192
|
+
} else if (textureStyle === 'sandstone') {
|
|
193
|
+
const bandCoord = (positions[i + 1] - minY) * invHeight * (5 + textureScale * 3)
|
|
194
|
+
+ grain * 0.35;
|
|
195
|
+
const band = 1 - Math.abs(fract(bandCoord) * 2 - 1);
|
|
196
|
+
const layer = smoothstep(0.58, 0.92, band) * textureStrength;
|
|
197
|
+
const dust = smoothstep(-0.2, 0.75, grain) * textureStrength * 0.22;
|
|
198
|
+
red += (0.83 - red) * dust;
|
|
199
|
+
green += (0.63 - green) * dust;
|
|
200
|
+
blue += (0.42 - blue) * dust;
|
|
201
|
+
red += (0.46 - red) * layer * 0.22;
|
|
202
|
+
green += (0.27 - green) * layer * 0.22;
|
|
203
|
+
blue += (0.17 - blue) * layer * 0.22;
|
|
204
|
+
} else if (textureStyle === 'basalt') {
|
|
205
|
+
const cells = cellular3(seed + 31, tx * 0.9, ty * 0.9, tz * 0.9, 0.65);
|
|
206
|
+
const joint = (1 - smoothstep(0.08, 0.26, cells.f2 - cells.f1)) * textureStrength;
|
|
207
|
+
const crystal = smoothstep(0.3, 0.82, cells.f1) * textureStrength * 0.16;
|
|
208
|
+
red += (0.11 - red) * joint * 0.4;
|
|
209
|
+
green += (0.12 - green) * joint * 0.4;
|
|
210
|
+
blue += (0.14 - blue) * joint * 0.4;
|
|
211
|
+
red += (0.45 - red) * crystal;
|
|
212
|
+
green += (0.47 - green) * crystal;
|
|
213
|
+
blue += (0.5 - blue) * crystal;
|
|
214
|
+
} else if (textureStyle === 'limestone') {
|
|
215
|
+
const cloud = (grain + 1) * 0.5;
|
|
216
|
+
const chalk = smoothstep(0.34, 0.82, cloud) * textureStrength * 0.36;
|
|
217
|
+
const pit = smoothstep(0.18, 0.64, -grain) * textureStrength * 0.18;
|
|
218
|
+
red += (0.79 - red) * chalk;
|
|
219
|
+
green += (0.77 - green) * chalk;
|
|
220
|
+
blue += (0.66 - blue) * chalk;
|
|
221
|
+
red += (0.38 - red) * pit;
|
|
222
|
+
green += (0.35 - green) * pit;
|
|
223
|
+
blue += (0.28 - blue) * pit;
|
|
224
|
+
} else if (textureStyle === 'veined') {
|
|
225
|
+
const coolCloud = smoothstep(-0.25, 0.85, grain) * textureStrength * 0.18;
|
|
226
|
+
red += (0.58 - red) * coolCloud;
|
|
227
|
+
green += (0.6 - green) * coolCloud;
|
|
228
|
+
blue += (0.64 - blue) * coolCloud;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (hasVeins) {
|
|
233
|
+
const warp = fbm3(
|
|
234
|
+
seed + 47,
|
|
235
|
+
positions[i] * textureFrequency * 0.65,
|
|
236
|
+
positions[i + 1] * textureFrequency * 0.65,
|
|
237
|
+
positions[i + 2] * textureFrequency * 0.65,
|
|
238
|
+
3,
|
|
239
|
+
2,
|
|
240
|
+
0.5,
|
|
241
|
+
) * 0.45;
|
|
242
|
+
const coord = (
|
|
243
|
+
positions[i] * 0.61 + positions[i + 1] * 0.24 - positions[i + 2] * 0.72
|
|
244
|
+
) * (0.9 + textureScale * 0.6) + warp;
|
|
245
|
+
const centerLine = 1 - Math.abs(fract(coord) * 2 - 1);
|
|
246
|
+
const vein = smoothstep(0.86, 0.985, centerLine) * veinStrength;
|
|
247
|
+
red += (veinColor[0] - red) * vein;
|
|
248
|
+
green += (veinColor[1] - green) * vein;
|
|
249
|
+
blue += (veinColor[2] - blue) * vein;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (hasStain) {
|
|
253
|
+
const streakNoise = (valueNoise3(
|
|
254
|
+
seed + 61,
|
|
255
|
+
positions[i] * textureFrequency * 0.7,
|
|
256
|
+
positions[i + 1] * textureFrequency * 0.18,
|
|
257
|
+
positions[i + 2] * textureFrequency * 0.7,
|
|
258
|
+
) + 1) * 0.5;
|
|
259
|
+
const exposedWall = (1 - slope) * 0.45 + cavity * 0.65 + streakNoise * 0.35;
|
|
260
|
+
const stain = smoothstep(0.42, 1.05, exposedWall)
|
|
261
|
+
* (0.6 + (1 - height) * 0.4)
|
|
262
|
+
* stainStrength;
|
|
263
|
+
red += (stainColor[0] - red) * stain;
|
|
264
|
+
green += (stainColor[1] - green) * stain;
|
|
265
|
+
blue += (stainColor[2] - blue) * stain;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (hasLichen) {
|
|
269
|
+
const lichenCells = cellular3(
|
|
270
|
+
seed + 83,
|
|
271
|
+
positions[i] * textureFrequency * 2.5,
|
|
272
|
+
positions[i + 1] * textureFrequency * 2.5,
|
|
273
|
+
positions[i + 2] * textureFrequency * 2.5,
|
|
274
|
+
1,
|
|
275
|
+
);
|
|
276
|
+
const spots = (1 - smoothstep(0.07, 0.24, lichenCells.f1));
|
|
277
|
+
const patchNoise = fbm3(
|
|
278
|
+
seed + 89,
|
|
279
|
+
positions[i] * textureFrequency,
|
|
280
|
+
positions[i + 1] * textureFrequency,
|
|
281
|
+
positions[i + 2] * textureFrequency,
|
|
282
|
+
3,
|
|
283
|
+
2,
|
|
284
|
+
0.5,
|
|
285
|
+
);
|
|
286
|
+
const exposed = (1 - cavity * 0.55) * (0.72 + slope * 0.28);
|
|
287
|
+
const lichen = spots
|
|
288
|
+
* smoothstep(-0.3, 0.55, patchNoise)
|
|
289
|
+
* exposed
|
|
290
|
+
* lichenCoverage;
|
|
291
|
+
red += (lichenColor[0] - red) * lichen;
|
|
292
|
+
green += (lichenColor[1] - green) * lichen;
|
|
293
|
+
blue += (lichenColor[2] - blue) * lichen;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (hasMoss) {
|
|
297
|
+
const mossNoise = fbm3(
|
|
298
|
+
seed + 97,
|
|
299
|
+
positions[i] * textureFrequency * 0.8,
|
|
300
|
+
positions[i + 1] * textureFrequency * 0.3,
|
|
301
|
+
positions[i + 2] * textureFrequency * 0.8,
|
|
302
|
+
4,
|
|
303
|
+
2,
|
|
304
|
+
0.5,
|
|
305
|
+
);
|
|
306
|
+
const ledges = slope * (0.55 + height * 0.45);
|
|
307
|
+
const support = ledges + cavity * 0.45 + (1 - height) * 0.1;
|
|
308
|
+
const moss = smoothstep(0.32, 0.9, support)
|
|
309
|
+
* smoothstep(-0.25, 0.65, mossNoise)
|
|
310
|
+
* mossCoverage;
|
|
311
|
+
red += (mossColor[0] - red) * moss;
|
|
312
|
+
green += (mossColor[1] - green) * moss;
|
|
313
|
+
blue += (mossColor[2] - blue) * moss;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
colors[i] = clamp01(red * variation);
|
|
317
|
+
colors[i + 1] = clamp01(green * variation);
|
|
318
|
+
colors[i + 2] = clamp01(blue * variation);
|
|
319
|
+
}
|
|
320
|
+
return colors;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* De-indexes positions and emits per-face normals for the 'flat' normals
|
|
325
|
+
* mode. Colors/AO are expanded alongside so the attribute contract holds.
|
|
326
|
+
*/
|
|
327
|
+
export function deindexWithFlatNormals({ ao, colors, indices, positions }) {
|
|
328
|
+
const triangleCount = indices.length / 3;
|
|
329
|
+
const flatPositions = new Float32Array(indices.length * 3);
|
|
330
|
+
const flatNormals = new Float32Array(indices.length * 3);
|
|
331
|
+
const flatColors = new Float32Array(indices.length * 3);
|
|
332
|
+
const flatAo = new Float32Array(indices.length);
|
|
333
|
+
|
|
334
|
+
for (let t = 0; t < triangleCount; t += 1) {
|
|
335
|
+
const i0 = indices[t * 3];
|
|
336
|
+
const i1 = indices[t * 3 + 1];
|
|
337
|
+
const i2 = indices[t * 3 + 2];
|
|
338
|
+
const ax = positions[i0 * 3];
|
|
339
|
+
const ay = positions[i0 * 3 + 1];
|
|
340
|
+
const az = positions[i0 * 3 + 2];
|
|
341
|
+
const ux = positions[i1 * 3] - ax;
|
|
342
|
+
const uy = positions[i1 * 3 + 1] - ay;
|
|
343
|
+
const uz = positions[i1 * 3 + 2] - az;
|
|
344
|
+
const vx = positions[i2 * 3] - ax;
|
|
345
|
+
const vy = positions[i2 * 3 + 1] - ay;
|
|
346
|
+
const vz = positions[i2 * 3 + 2] - az;
|
|
347
|
+
let nx = uy * vz - uz * vy;
|
|
348
|
+
let ny = uz * vx - ux * vz;
|
|
349
|
+
let nz = ux * vy - uy * vx;
|
|
350
|
+
const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
351
|
+
if (length > 0) {
|
|
352
|
+
nx /= length;
|
|
353
|
+
ny /= length;
|
|
354
|
+
nz /= length;
|
|
355
|
+
}
|
|
356
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
357
|
+
const source = indices[t * 3 + corner];
|
|
358
|
+
const write = t * 3 + corner;
|
|
359
|
+
flatPositions[write * 3] = positions[source * 3];
|
|
360
|
+
flatPositions[write * 3 + 1] = positions[source * 3 + 1];
|
|
361
|
+
flatPositions[write * 3 + 2] = positions[source * 3 + 2];
|
|
362
|
+
flatNormals[write * 3] = nx;
|
|
363
|
+
flatNormals[write * 3 + 1] = ny;
|
|
364
|
+
flatNormals[write * 3 + 2] = nz;
|
|
365
|
+
flatColors[write * 3] = colors[source * 3];
|
|
366
|
+
flatColors[write * 3 + 1] = colors[source * 3 + 1];
|
|
367
|
+
flatColors[write * 3 + 2] = colors[source * 3 + 2];
|
|
368
|
+
flatAo[write] = ao[source];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ao: flatAo, colors: flatColors, normals: flatNormals, positions: flatPositions,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Meshing orchestrator: document -> compiled field -> padded bounds ->
|
|
2
|
+
// sampled grid -> surface nets -> derived attributes -> BufferGeometry.
|
|
3
|
+
//
|
|
4
|
+
// Typical budgets (single default piece): preview 80 cells on the longest
|
|
5
|
+
// axis samples ~0.5M points (40-120 ms), export 224 samples ~11M (1-4 s,
|
|
6
|
+
// grid ~45 MB freed after extraction). Resolution is hard-capped at 320.
|
|
7
|
+
|
|
8
|
+
import * as THREE from 'three';
|
|
9
|
+
|
|
10
|
+
import { hashCombine } from '../noise/prng.js';
|
|
11
|
+
import { compileDocument } from '../sdf/fieldCompiler.js';
|
|
12
|
+
import {
|
|
13
|
+
computeGradientNormals,
|
|
14
|
+
computeSdfAo,
|
|
15
|
+
computeVertexColors,
|
|
16
|
+
deindexWithFlatNormals,
|
|
17
|
+
} from './meshAttributes.js';
|
|
18
|
+
import { filterSmallIslands, sampleGrid, surfaceNets } from './surfaceNets.js';
|
|
19
|
+
|
|
20
|
+
const MAX_RESOLUTION = 320;
|
|
21
|
+
const SEED_COLOR = 131;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Meshes a rock document into a THREE.BufferGeometry with the rockgen
|
|
25
|
+
* attribute contract: `position`, `normal`, `color` (baked stylized
|
|
26
|
+
* albedo), `envVertexAo` (1 = open), and `index` (dropped in 'flat'
|
|
27
|
+
* normals mode, which de-indexes).
|
|
28
|
+
*
|
|
29
|
+
* @param {object} document Rock document.
|
|
30
|
+
* @param {object} [options]
|
|
31
|
+
* @param {number} [options.resolution] Cells along the longest bounds axis
|
|
32
|
+
* (defaults to `document.meshing.previewResolution`).
|
|
33
|
+
* @param {{min: number[], max: number[]}} [options.bounds] Bounds override.
|
|
34
|
+
* @param {'gradient'|'flat'} [options.normals] Normal mode override.
|
|
35
|
+
* @param {{color?: boolean, ao?: boolean}} [options.attributes] Skip baked
|
|
36
|
+
* attributes (both default true).
|
|
37
|
+
* @param {boolean} [options.includeHelpers] Include construction-only lab
|
|
38
|
+
* helpers such as hidden ground supports. Defaults false so preview/export
|
|
39
|
+
* match the final visible rock.
|
|
40
|
+
* @param {string} [options.pieceId] Mesh one piece in its local space.
|
|
41
|
+
*/
|
|
42
|
+
export function meshDocument(document, {
|
|
43
|
+
attributes = {},
|
|
44
|
+
bounds = null,
|
|
45
|
+
includeHelpers = false,
|
|
46
|
+
normals = null,
|
|
47
|
+
pieceId = null,
|
|
48
|
+
resolution = null,
|
|
49
|
+
} = {}) {
|
|
50
|
+
const program = compileDocument(document, { includeHelpers, pieceId });
|
|
51
|
+
const gridResolution = Math.min(
|
|
52
|
+
Math.max(Math.round(resolution ?? document.meshing.previewResolution), 8),
|
|
53
|
+
MAX_RESOLUTION,
|
|
54
|
+
);
|
|
55
|
+
const normalsMode = normals ?? document.meshing.normalsMode;
|
|
56
|
+
const wantColor = attributes.color !== false;
|
|
57
|
+
const wantAo = attributes.ao !== false;
|
|
58
|
+
|
|
59
|
+
// Final safety pad: two voxels beyond the compiler's displacement pad.
|
|
60
|
+
const sourceBounds = bounds ?? program.bounds;
|
|
61
|
+
const longest = Math.max(
|
|
62
|
+
sourceBounds.max[0] - sourceBounds.min[0],
|
|
63
|
+
sourceBounds.max[1] - sourceBounds.min[1],
|
|
64
|
+
sourceBounds.max[2] - sourceBounds.min[2],
|
|
65
|
+
);
|
|
66
|
+
const voxelPad = (longest / gridResolution) * 2;
|
|
67
|
+
const paddedBounds = {
|
|
68
|
+
max: sourceBounds.max.map((value) => value + voxelPad),
|
|
69
|
+
min: sourceBounds.min.map((value) => value - voxelPad),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const grid = sampleGrid(program.evaluate, paddedBounds, gridResolution);
|
|
73
|
+
let surface = surfaceNets(grid, {
|
|
74
|
+
evaluate: document.meshing.sharpFeatures !== false ? program.evaluate : null,
|
|
75
|
+
});
|
|
76
|
+
if (document.meshing.removeIslands !== false) {
|
|
77
|
+
surface = filterSmallIslands(surface);
|
|
78
|
+
}
|
|
79
|
+
const { indices, positions } = surface;
|
|
80
|
+
if (positions.length === 0) {
|
|
81
|
+
throw new Error('Rock document produced an empty surface (check sizes and amplitudes).');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const epsilon = grid.cellSize * 0.5;
|
|
85
|
+
let vertexNormals = computeGradientNormals(program.evaluate, positions, epsilon);
|
|
86
|
+
const ao = wantAo || wantColor
|
|
87
|
+
? computeSdfAo(program.evaluate, positions, vertexNormals, {
|
|
88
|
+
radius: document.surface.aoRadius,
|
|
89
|
+
strength: document.surface.aoStrength,
|
|
90
|
+
})
|
|
91
|
+
: null;
|
|
92
|
+
const colors = wantColor
|
|
93
|
+
? computeVertexColors(
|
|
94
|
+
positions,
|
|
95
|
+
vertexNormals,
|
|
96
|
+
ao,
|
|
97
|
+
document.surface,
|
|
98
|
+
hashCombine(document.seed >>> 0, SEED_COLOR),
|
|
99
|
+
paddedBounds,
|
|
100
|
+
program.tintAt,
|
|
101
|
+
)
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
const geometry = new THREE.BufferGeometry();
|
|
105
|
+
if (normalsMode === 'flat') {
|
|
106
|
+
const flat = deindexWithFlatNormals({
|
|
107
|
+
ao: ao ?? new Float32Array(positions.length / 3).fill(1),
|
|
108
|
+
colors: colors ?? new Float32Array(positions.length),
|
|
109
|
+
indices,
|
|
110
|
+
positions,
|
|
111
|
+
});
|
|
112
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(flat.positions, 3));
|
|
113
|
+
geometry.setAttribute('normal', new THREE.BufferAttribute(flat.normals, 3));
|
|
114
|
+
if (wantColor) geometry.setAttribute('color', new THREE.BufferAttribute(flat.colors, 3));
|
|
115
|
+
if (wantAo) geometry.setAttribute('envVertexAo', new THREE.BufferAttribute(flat.ao, 1));
|
|
116
|
+
} else {
|
|
117
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
118
|
+
geometry.setAttribute('normal', new THREE.BufferAttribute(vertexNormals, 3));
|
|
119
|
+
if (wantColor) geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
120
|
+
if (wantAo) geometry.setAttribute('envVertexAo', new THREE.BufferAttribute(ao, 1));
|
|
121
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
122
|
+
}
|
|
123
|
+
geometry.computeBoundingBox();
|
|
124
|
+
geometry.computeBoundingSphere();
|
|
125
|
+
return geometry;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* FNV-1a hash over every attribute's byte view (name-salted) plus the
|
|
130
|
+
* index. Used by scripts/verify-rockgen.mjs to assert determinism and by
|
|
131
|
+
* callers as a cheap content key.
|
|
132
|
+
*/
|
|
133
|
+
export function hashGeometry(geometry) {
|
|
134
|
+
let hash = 0x811c9dc5;
|
|
135
|
+
const mixBytes = (bytes) => {
|
|
136
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
137
|
+
hash ^= bytes[i];
|
|
138
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const mixString = (text) => {
|
|
142
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
143
|
+
hash ^= text.charCodeAt(i) & 0xff;
|
|
144
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
for (const name of Object.keys(geometry.attributes).sort()) {
|
|
148
|
+
mixString(name);
|
|
149
|
+
const { array } = geometry.attributes[name];
|
|
150
|
+
mixBytes(new Uint8Array(array.buffer, array.byteOffset, array.byteLength));
|
|
151
|
+
}
|
|
152
|
+
if (geometry.index) {
|
|
153
|
+
mixString('index');
|
|
154
|
+
const { array } = geometry.index;
|
|
155
|
+
mixBytes(new Uint8Array(array.buffer, array.byteOffset, array.byteLength));
|
|
156
|
+
}
|
|
157
|
+
return hash.toString(16).padStart(8, '0');
|
|
158
|
+
}
|