@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,177 @@
|
|
|
1
|
+
// GLB export for generated rocks. Exports carry the field-derived vertex
|
|
2
|
+
// colors and SDF ambient occlusion (asset-intrinsic self-occlusion only —
|
|
3
|
+
// no scene/ground contact baked in), so the asset is portable and re-imports
|
|
4
|
+
// into this repo's environment pipeline cleanly.
|
|
5
|
+
//
|
|
6
|
+
// Browser-only: GLTFExporter assembles the GLB container through FileReader,
|
|
7
|
+
// which node lacks. Everything else in the cluster (meshing, hashing,
|
|
8
|
+
// serialization) runs in node — see scripts/verify-rockgen.mjs.
|
|
9
|
+
|
|
10
|
+
import * as THREE from 'three';
|
|
11
|
+
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
|
12
|
+
|
|
13
|
+
import { meshDocument } from '../mesh/meshDocument.js';
|
|
14
|
+
|
|
15
|
+
// glTF requires custom attribute names to start with an underscore.
|
|
16
|
+
const AO_EXPORT_ATTRIBUTE = '_ROCK_AO';
|
|
17
|
+
|
|
18
|
+
function packAoIntoColorAlpha(geometry) {
|
|
19
|
+
const color = geometry.getAttribute('color');
|
|
20
|
+
const ao = geometry.getAttribute('envVertexAo');
|
|
21
|
+
const rgba = new Float32Array((color.count) * 4);
|
|
22
|
+
for (let v = 0; v < color.count; v += 1) {
|
|
23
|
+
rgba[v * 4] = color.getX(v);
|
|
24
|
+
rgba[v * 4 + 1] = color.getY(v);
|
|
25
|
+
rgba[v * 4 + 2] = color.getZ(v);
|
|
26
|
+
rgba[v * 4 + 3] = ao ? ao.getX(v) : 1;
|
|
27
|
+
}
|
|
28
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(rgba, 4));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prepares an export copy of a rockgen geometry: AO is packed per `ao` —
|
|
33
|
+
* 'colorAlpha' (default; COLOR_0 alpha, portable and round-trippable),
|
|
34
|
+
* 'attribute' (`_ROCK_AO` custom attribute for in-house pipelines), or
|
|
35
|
+
* 'omit'. The nonstandard `envVertexAo` attribute never ships raw.
|
|
36
|
+
*/
|
|
37
|
+
// Box-projection UVs from the bounding box: each vertex projects along
|
|
38
|
+
// its normal's dominant axis. Seams land on the (already sharp) axis
|
|
39
|
+
// creases — fine for detail/tiling textures; engines wanting seamless
|
|
40
|
+
// texturing should triplanar in-shader instead.
|
|
41
|
+
function addBoxProjectionUvs(geometry) {
|
|
42
|
+
geometry.computeBoundingBox();
|
|
43
|
+
const box = geometry.boundingBox;
|
|
44
|
+
const size = [
|
|
45
|
+
Math.max(box.max.x - box.min.x, 1e-6),
|
|
46
|
+
Math.max(box.max.y - box.min.y, 1e-6),
|
|
47
|
+
Math.max(box.max.z - box.min.z, 1e-6),
|
|
48
|
+
];
|
|
49
|
+
const position = geometry.getAttribute('position');
|
|
50
|
+
const normal = geometry.getAttribute('normal');
|
|
51
|
+
const uv = new Float32Array(position.count * 2);
|
|
52
|
+
for (let v = 0; v < position.count; v += 1) {
|
|
53
|
+
const nx = Math.abs(normal.getX(v));
|
|
54
|
+
const ny = Math.abs(normal.getY(v));
|
|
55
|
+
const nz = Math.abs(normal.getZ(v));
|
|
56
|
+
let u;
|
|
57
|
+
let w;
|
|
58
|
+
if (nx >= ny && nx >= nz) {
|
|
59
|
+
u = (position.getZ(v) - box.min.z) / size[2];
|
|
60
|
+
w = (position.getY(v) - box.min.y) / size[1];
|
|
61
|
+
} else if (ny >= nx && ny >= nz) {
|
|
62
|
+
u = (position.getX(v) - box.min.x) / size[0];
|
|
63
|
+
w = (position.getZ(v) - box.min.z) / size[2];
|
|
64
|
+
} else {
|
|
65
|
+
u = (position.getX(v) - box.min.x) / size[0];
|
|
66
|
+
w = (position.getY(v) - box.min.y) / size[1];
|
|
67
|
+
}
|
|
68
|
+
uv[v * 2] = u;
|
|
69
|
+
uv[v * 2 + 1] = w;
|
|
70
|
+
}
|
|
71
|
+
geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function prepareGeometryForExport(geometry, { ao = 'colorAlpha', uv = 'none' } = {}) {
|
|
75
|
+
const exportGeometry = geometry.clone();
|
|
76
|
+
if (ao === 'colorAlpha' && exportGeometry.getAttribute('color')) {
|
|
77
|
+
packAoIntoColorAlpha(exportGeometry);
|
|
78
|
+
} else if (ao === 'attribute' && exportGeometry.getAttribute('envVertexAo')) {
|
|
79
|
+
exportGeometry.setAttribute(AO_EXPORT_ATTRIBUTE, exportGeometry.getAttribute('envVertexAo'));
|
|
80
|
+
}
|
|
81
|
+
exportGeometry.deleteAttribute('envVertexAo');
|
|
82
|
+
if (uv === 'box') addBoxProjectionUvs(exportGeometry);
|
|
83
|
+
return exportGeometry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Exports a geometry (or a ready-made mesh/scene) as a binary GLB.
|
|
88
|
+
* Geometries are wrapped in a vertex-colored standard material so viewers
|
|
89
|
+
* outside this repo show the baked look.
|
|
90
|
+
*
|
|
91
|
+
* @returns {Promise<ArrayBuffer>}
|
|
92
|
+
*/
|
|
93
|
+
export async function exportGeometryToGLB(geometryOrObject, {
|
|
94
|
+
ao = 'colorAlpha', name = 'rock', uv = 'none',
|
|
95
|
+
} = {}) {
|
|
96
|
+
let target = geometryOrObject;
|
|
97
|
+
if (target?.isBufferGeometry) {
|
|
98
|
+
const material = new THREE.MeshStandardMaterial({
|
|
99
|
+
metalness: 0,
|
|
100
|
+
roughness: 0.95,
|
|
101
|
+
vertexColors: true,
|
|
102
|
+
});
|
|
103
|
+
target = new THREE.Mesh(prepareGeometryForExport(target, { ao, uv }), material);
|
|
104
|
+
target.name = name;
|
|
105
|
+
}
|
|
106
|
+
const exporter = new GLTFExporter();
|
|
107
|
+
return exporter.parseAsync(target, { binary: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Meshes the document at export resolution and exports it as GLB.
|
|
112
|
+
*
|
|
113
|
+
* With `lods` (default: the document's meshing.exportLods, on unless
|
|
114
|
+
* disabled) the GLB carries three meshes named <name>_LOD0/1/2 at full,
|
|
115
|
+
* half and quarter grid resolution. Because LODs are independent SDF
|
|
116
|
+
* re-meshes — not decimations of LOD0 — every level keeps clean toon
|
|
117
|
+
* silhouettes and its own correctly baked colors/AO.
|
|
118
|
+
*
|
|
119
|
+
* `uv: 'box'` adds box-projection UVs for detail textures (see
|
|
120
|
+
* addBoxProjectionUvs for the trade-off).
|
|
121
|
+
*
|
|
122
|
+
* @returns {Promise<ArrayBuffer>}
|
|
123
|
+
*/
|
|
124
|
+
export async function exportDocumentToGLB(document, {
|
|
125
|
+
ao = 'colorAlpha',
|
|
126
|
+
lods = null,
|
|
127
|
+
name = null,
|
|
128
|
+
normals = null,
|
|
129
|
+
resolution = null,
|
|
130
|
+
uv = 'none',
|
|
131
|
+
} = {}) {
|
|
132
|
+
const exportResolution = resolution ?? document.meshing.exportResolution;
|
|
133
|
+
const exportName = name ?? document.name;
|
|
134
|
+
const wantLods = lods ?? document.meshing.exportLods !== false;
|
|
135
|
+
|
|
136
|
+
const makeMesh = (res, meshName) => {
|
|
137
|
+
const geometry = meshDocument(document, { includeHelpers: false, normals, resolution: res });
|
|
138
|
+
const material = new THREE.MeshStandardMaterial({
|
|
139
|
+
metalness: 0,
|
|
140
|
+
roughness: 0.95,
|
|
141
|
+
vertexColors: true,
|
|
142
|
+
});
|
|
143
|
+
const mesh = new THREE.Mesh(prepareGeometryForExport(geometry, { ao, uv }), material);
|
|
144
|
+
mesh.name = meshName;
|
|
145
|
+
return mesh;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (!wantLods) {
|
|
149
|
+
const geometry = meshDocument(document, { includeHelpers: false, normals, resolution: exportResolution });
|
|
150
|
+
return exportGeometryToGLB(geometry, { ao, name: exportName, uv });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const root = new THREE.Group();
|
|
154
|
+
root.name = exportName;
|
|
155
|
+
const levels = [
|
|
156
|
+
exportResolution,
|
|
157
|
+
Math.max(Math.round(exportResolution / 2), 32),
|
|
158
|
+
Math.max(Math.round(exportResolution / 4), 24),
|
|
159
|
+
];
|
|
160
|
+
levels.forEach((res, level) => {
|
|
161
|
+
root.add(makeMesh(res, `${exportName}_LOD${level}`));
|
|
162
|
+
});
|
|
163
|
+
const exporter = new GLTFExporter();
|
|
164
|
+
return exporter.parseAsync(root, { binary: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Browser download helper for exported buffers (no-op outside browsers). */
|
|
168
|
+
export function downloadArrayBuffer(buffer, filename, mimeType = 'application/octet-stream') {
|
|
169
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
170
|
+
const blob = new Blob([buffer], { type: mimeType });
|
|
171
|
+
const url = URL.createObjectURL(blob);
|
|
172
|
+
const anchor = document.createElement('a');
|
|
173
|
+
anchor.href = url;
|
|
174
|
+
anchor.download = filename;
|
|
175
|
+
anchor.click();
|
|
176
|
+
URL.revokeObjectURL(url);
|
|
177
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Eroded heightfield patches for the 'heightfield' rock piece: the recipe
|
|
2
|
+
// the dedicated terrain tools (Gaea / World Machine / Terrain Mixer) all
|
|
3
|
+
// converge on, at rock-piece scale —
|
|
4
|
+
//
|
|
5
|
+
// ridged+fbm relief → silhouette profile → terracing → EROSION
|
|
6
|
+
//
|
|
7
|
+
// The output is a normalized [0, 1] height grid the field compiler turns
|
|
8
|
+
// into an SDF (mesas, canyon walls, mountain flanks with real drainage),
|
|
9
|
+
// plus the erosion masks for stylization. Everything derives from hashed
|
|
10
|
+
// noise + the seeded erosion sim, so patches are bit-deterministic and a
|
|
11
|
+
// document's golden hash covers them.
|
|
12
|
+
//
|
|
13
|
+
// Patches are memoized on their full parameter key: meshing re-samples the
|
|
14
|
+
// field millions of times, and slider drags must not re-erode per sample.
|
|
15
|
+
|
|
16
|
+
import { fbm3, ridgedFbm3 } from '../noise/valueNoise3.js';
|
|
17
|
+
import { hashCombine } from '../noise/prng.js';
|
|
18
|
+
import { erodeHeightfield } from './heightfieldErosion.js';
|
|
19
|
+
|
|
20
|
+
export const HEIGHTFIELD_PROFILES = Object.freeze(['mesa', 'ridge', 'slope', 'open']);
|
|
21
|
+
|
|
22
|
+
const PATCH_RESOLUTION = 128;
|
|
23
|
+
const CACHE_LIMIT = 8;
|
|
24
|
+
const cache = new Map(); // key -> patch (insertion-ordered; oldest evicted)
|
|
25
|
+
|
|
26
|
+
const SEED_RELIEF = 17;
|
|
27
|
+
const SEED_DETAIL = 41;
|
|
28
|
+
|
|
29
|
+
function clamp01(v) {
|
|
30
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Silhouette profiles shape the patch before erosion so droplets carve
|
|
34
|
+
// the landform rather than a noise plane:
|
|
35
|
+
// mesa — flat elevated plateau falling off toward every rim
|
|
36
|
+
// ridge — a crest along Z, falling off toward ±X (canyon/cliff walls)
|
|
37
|
+
// slope — one-directional ramp, high at -X (a hillside band)
|
|
38
|
+
// open — raw relief edge to edge
|
|
39
|
+
function profileWeight(profile, u, v) {
|
|
40
|
+
if (profile === 'mesa') {
|
|
41
|
+
const rim = Math.min(u, 1 - u, v, 1 - v) * 2; // 0 at rim, 1 at center
|
|
42
|
+
const t = clamp01(rim / 0.55);
|
|
43
|
+
return 0.15 + 0.85 * (t * t * (3 - 2 * t));
|
|
44
|
+
}
|
|
45
|
+
if (profile === 'ridge') {
|
|
46
|
+
const crest = 1 - Math.abs(u - 0.5) * 2;
|
|
47
|
+
const t = clamp01(crest / 0.75);
|
|
48
|
+
return 0.1 + 0.9 * (t * t * (3 - 2 * t));
|
|
49
|
+
}
|
|
50
|
+
if (profile === 'slope') {
|
|
51
|
+
return 0.15 + 0.85 * clamp01(1.15 - u * 1.3);
|
|
52
|
+
}
|
|
53
|
+
return 1; // open
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Soft terracing (the cliff-banding move): quantize height into steps but
|
|
57
|
+
// keep a smoothed remainder so bands read as strata, not stairsteps.
|
|
58
|
+
function terraceHeight(h, amount, steps) {
|
|
59
|
+
if (amount <= 0) return h;
|
|
60
|
+
const scaled = h * steps;
|
|
61
|
+
const base = Math.floor(scaled);
|
|
62
|
+
const frac = scaled - base;
|
|
63
|
+
const sharp = clamp01((frac - 0.5) / 0.28 + 0.5);
|
|
64
|
+
const stepped = (base + sharp * sharp * (3 - 2 * sharp)) / steps;
|
|
65
|
+
return h + (stepped - h) * amount;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Builds (or returns cached) an eroded patch for one heightfield piece.
|
|
70
|
+
*
|
|
71
|
+
* @param {number} seed Combined document+piece seed (uint32).
|
|
72
|
+
* @param {object} hf The piece's `heightfield` settings group.
|
|
73
|
+
* @returns {{ heights: Float32Array, masks: object, resolution: number,
|
|
74
|
+
* sample(u: number, v: number): number }} heights normalized to [0, 1].
|
|
75
|
+
*/
|
|
76
|
+
export function getHeightfieldPatch(seed, hf) {
|
|
77
|
+
const key = [
|
|
78
|
+
seed >>> 0, hf.profile, hf.relief, hf.roughness, hf.terrace, hf.terraceSteps,
|
|
79
|
+
hf.erosion, hf.droplets, hf.thermal, hf.seedOffset,
|
|
80
|
+
].join('|');
|
|
81
|
+
const cached = cache.get(key);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
|
|
84
|
+
const res = PATCH_RESOLUTION;
|
|
85
|
+
const patchSeed = hashCombine(seed >>> 0, (hf.seedOffset >>> 0) || 0);
|
|
86
|
+
const reliefSeed = hashCombine(patchSeed, SEED_RELIEF);
|
|
87
|
+
const detailSeed = hashCombine(patchSeed, SEED_DETAIL);
|
|
88
|
+
|
|
89
|
+
// --- base relief: ridged backbone blended with rolling fbm ---------------
|
|
90
|
+
const heights = new Float32Array(res * res);
|
|
91
|
+
const frequency = 2.2 + hf.roughness * 2.4;
|
|
92
|
+
for (let iz = 0; iz < res; iz += 1) {
|
|
93
|
+
for (let ix = 0; ix < res; ix += 1) {
|
|
94
|
+
const u = ix / (res - 1);
|
|
95
|
+
const v = iz / (res - 1);
|
|
96
|
+
const ridged = ridgedFbm3(reliefSeed, u * frequency, 0, v * frequency, 5, 2.05, 0.5);
|
|
97
|
+
const rolling = fbm3(detailSeed, u * frequency * 1.7, 0, v * frequency * 1.7, 4, 2.0, 0.5)
|
|
98
|
+
* 0.5 + 0.5;
|
|
99
|
+
let h = ridged * hf.relief + rolling * (1 - hf.relief);
|
|
100
|
+
h = terraceHeight(clamp01(h), hf.terrace, Math.max(2, Math.round(hf.terraceSteps)));
|
|
101
|
+
h *= profileWeight(hf.profile, u, v);
|
|
102
|
+
// Mesas are PLATEAUS: soft-clamp the caprock so peaks flatten into a
|
|
103
|
+
// tabletop instead of a mountain (relief above the cap compresses).
|
|
104
|
+
if (hf.profile === 'mesa' && h > 0.62) h = 0.62 + (h - 0.62) * 0.18;
|
|
105
|
+
heights[iz * res + ix] = h;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- erosion --------------------------------------------------------------
|
|
110
|
+
let masks = null;
|
|
111
|
+
let final = heights;
|
|
112
|
+
if (hf.erosion > 0) {
|
|
113
|
+
// Heights are normalized: talus/minSlope stay in per-cell units scaled
|
|
114
|
+
// to the patch's implied relief (1.0 over ~res cells).
|
|
115
|
+
const result = erodeHeightfield({
|
|
116
|
+
height: res,
|
|
117
|
+
heightmap: heights,
|
|
118
|
+
params: {
|
|
119
|
+
droplets: Math.round(10_000 + hf.droplets * 70_000),
|
|
120
|
+
seed: patchSeed,
|
|
121
|
+
strength: hf.erosion,
|
|
122
|
+
talus: (0.35 + (1 - hf.thermal) * 0.9) * (1.4 / res) * 20,
|
|
123
|
+
thermalIterations: Math.round(hf.thermal * 60),
|
|
124
|
+
thermalStrength: 0.25 + hf.thermal * 0.45,
|
|
125
|
+
},
|
|
126
|
+
width: res,
|
|
127
|
+
});
|
|
128
|
+
final = result.eroded;
|
|
129
|
+
masks = result;
|
|
130
|
+
// Erosion deposits can push past 1: renormalize into [0, 1] so sizeY
|
|
131
|
+
// stays the honest height bound the SDF metrics rely on.
|
|
132
|
+
let max = 1e-6;
|
|
133
|
+
for (let i = 0; i < final.length; i += 1) if (final[i] > max) max = final[i];
|
|
134
|
+
if (max > 1) for (let i = 0; i < final.length; i += 1) final[i] /= max;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const bilinear = (array, u, v) => {
|
|
138
|
+
const fx = clamp01(u) * (res - 1);
|
|
139
|
+
const fz = clamp01(v) * (res - 1);
|
|
140
|
+
const ix = Math.min(Math.floor(fx), res - 2);
|
|
141
|
+
const iz = Math.min(Math.floor(fz), res - 2);
|
|
142
|
+
const tx = fx - ix;
|
|
143
|
+
const tz = fz - iz;
|
|
144
|
+
const row = iz * res + ix;
|
|
145
|
+
const a = array[row];
|
|
146
|
+
const b = array[row + 1];
|
|
147
|
+
const c = array[row + res];
|
|
148
|
+
const d = array[row + res + 1];
|
|
149
|
+
return a + (b - a) * tx + (c - a) * tz + (a - b - c + d) * tx * tz;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const patch = {
|
|
153
|
+
heights: final,
|
|
154
|
+
masks,
|
|
155
|
+
resolution: res,
|
|
156
|
+
/** Erosion story for stylization: deposition + water flow at (u, v). */
|
|
157
|
+
sampleMasks(u, v) {
|
|
158
|
+
if (!masks) return null;
|
|
159
|
+
return {
|
|
160
|
+
flow: bilinear(masks.flow, u, v),
|
|
161
|
+
sediment: bilinear(masks.depositionMask, u, v),
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
sample(u, v) {
|
|
165
|
+
const fx = clamp01(u) * (res - 1);
|
|
166
|
+
const fz = clamp01(v) * (res - 1);
|
|
167
|
+
const ix = Math.min(Math.floor(fx), res - 2);
|
|
168
|
+
const iz = Math.min(Math.floor(fz), res - 2);
|
|
169
|
+
const tx = fx - ix;
|
|
170
|
+
const tz = fz - iz;
|
|
171
|
+
const row = iz * res + ix;
|
|
172
|
+
const a = final[row];
|
|
173
|
+
const b = final[row + 1];
|
|
174
|
+
const c = final[row + res];
|
|
175
|
+
const d = final[row + res + 1];
|
|
176
|
+
return a + (b - a) * tx + (c - a) * tz + (a - b - c + d) * tx * tz;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
cache.set(key, patch);
|
|
181
|
+
if (cache.size > CACHE_LIMIT) cache.delete(cache.keys().next().value);
|
|
182
|
+
return patch;
|
|
183
|
+
}
|