@call-me-sensei/toonlab 0.4.19 → 0.4.21
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/NPM-LIBRARY.md +230 -0
- package/README.md +78 -36
- package/agents/PROMPTS.md +91 -0
- package/agents/README.md +9 -14
- package/agents/claude/CLAUDE.md +8 -0
- package/agents/codex/AGENTS.md +161 -0
- package/agents/cursor/toonlab.mdc +37 -0
- package/agents/references/mcp-asset-discovery.md +12 -9
- package/agents/references/runtime-entry-points.md +5 -4
- package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
- package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
- package/cli/toonlab.mjs +3 -2
- package/database/fal-patina.mjs +131 -0
- package/database/generation-service.mjs +108 -7
- package/database/migrations/0007_creation_revisions.sql +80 -0
- package/database/migrations/0008_creation_revision_hardening.sql +98 -0
- package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
- package/database/migrations/0010_external_catalog_assets.sql +28 -0
- package/database/providers.mjs +195 -16
- package/database/repository.mjs +518 -70
- package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
- package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
- package/mcp/public-catalog.mjs +4 -52
- package/mcp/server.mjs +175 -12
- package/mcp/vite-plugin.mjs +78 -2
- package/package.json +32 -12
- package/scripts/generate-catalog-seed.mjs +34 -7
- package/src/asset-policy/catalogLicenses.js +64 -6
- package/src/assetlib/loadImported.js +15 -1
- package/src/catalog/officialCatalogAssetRuntime.js +13 -2
- package/src/catalog/officialCatalogLod.js +34 -1
- package/src/catalog/officialCatalogPlacement.js +29 -14
- package/src/character/animationRetarget.js +2 -0
- package/src/character/characterRig.js +6 -1
- package/src/character/characterRuntime.js +175 -7
- package/src/environment/environmentMaterialAdapter.js +30 -0
- package/src/environment/environmentPresets.js +90 -0
- package/src/environment/environmentSunShadowPass.js +42 -0
- package/src/environment/toonLabSurfaceLighting.js +108 -10
- package/src/environment/urbanPropMaterial.js +104 -2
- package/src/lighting/lightingSystem.js +65 -10
- package/src/renderer/index.js +7 -0
- package/src/renderer/styleComparison.js +985 -0
- package/src/rock-shader/index.js +5 -0
- package/src/rock-shader/rockGeometryDetail.js +701 -0
- package/src/rock-shader/rockMaterial.js +782 -39
- package/src/rock-shader/rockRegionRuntime.js +185 -0
- package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
- package/src/rock-shader/rockShaderRuntime.js +354 -67
- package/src/rock-shader/rockShaderSettings.js +246 -16
- package/src/rock-shader/rockTangentIntegrity.js +136 -0
- package/src/rock-shader/rockTextureIntegrity.js +115 -0
- package/src/rockgen/index.js +1 -0
- package/src/rockgen/rockDocument.js +27 -3
- package/src/rockgen/surface/c7GeologySurface.js +372 -0
- package/src/shaders-tsl/water.js +8 -0
- package/src/sky/cloudShadow.js +25 -1
- package/src/sky/skySystem.js +9 -0
- package/src/styles/index.js +23 -0
- package/src/styles/neutralStylePresets.js +712 -0
- package/src/styles/sceneStyleRuntime.js +51 -4
- package/src/styles/styleAdapters.js +13 -2
- package/src/styles/styleBundle.js +1 -1
- package/src/texgen/evaluateTexture.js +30 -3
- package/src/texgen/textureGenerators.js +63 -31
- package/src/texgen/textureSettings.js +25 -8
- package/src/toon/toonSettings.js +96 -2
- package/src/vegetation/branchTree.js +327 -42
- package/src/vegetation/index.js +24 -2
- package/src/vegetation/scatter.js +372 -1
- package/src/vegetation/stylizedTree.js +312 -126
- package/src/vegetation/stylizedTreeFoliage.js +229 -7
- package/src/vegetation/treeSurfaceTextures.js +328 -0
- package/src/version.js +1 -1
- package/src/water/waterSettings.js +15 -7
- package/types/asset-policy/catalogLicenses.d.ts +2 -0
- package/types/asset-policy/index.d.ts +2 -0
- package/types/assetlib/loadImported.d.ts +3 -1
- package/types/catalog/officialCatalogLod.d.ts +63 -5
- package/types/character/characterRig.d.ts +59 -0
- package/types/character/characterRuntime.d.ts +1 -0
- package/types/environment/environmentMaterialAdapter.d.ts +3 -1
- package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
- package/types/index.d.ts +89 -7
- package/types/lighting/lightingSystem.d.ts +7 -0
- package/types/react/index.d.ts +20 -0
- package/types/renderer/index.d.ts +1 -0
- package/types/renderer/styleComparison.d.ts +409 -0
- package/types/rock-shader/index.d.ts +5 -0
- package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
- package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
- package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
- package/types/rock-shader/rockShaderSettings.d.ts +19 -5
- package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
- package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/rockDocument.d.ts +29 -1
- package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
- package/types/sky/cloudShadow.d.ts +1 -0
- package/types/styles/index.d.ts +1 -0
- package/types/styles/neutralStylePresets.d.ts +944 -0
- package/types/styles/sceneStyleRuntime.d.ts +20 -0
- package/types/vegetation/branchTree.d.ts +4 -0
- package/types/vegetation/index.d.ts +9 -7
- package/types/vegetation/scatter.d.ts +1 -0
- package/types/vegetation/stylizedTree.d.ts +1 -2
- package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
- package/types/version.d.ts +1 -1
- package/agents/references/geology-playbook.md +0 -118
- package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
- package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
- package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/src/vegetation/legacyTreePresets.js +0 -120
- package/types/vegetation/legacyTreePresets.d.ts +0 -22
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// Deterministic C7 geology-map source for the released 480-rock catalog.
|
|
2
|
+
// This module is deliberately renderer-free: Node batch tools and the browser
|
|
3
|
+
// editor consume the same byte-identical map fields. Geometry remains the
|
|
4
|
+
// editable authority; these maps are replaceable derivatives.
|
|
5
|
+
|
|
6
|
+
export const C7_GEOLOGY_SURFACE_SCHEMA = 'toonlab/c7-geology-surface';
|
|
7
|
+
export const C7_GEOLOGY_SURFACE_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
export const C7_GEOLOGY_PROFILES = Object.freeze({
|
|
10
|
+
'weathered-limestone': Object.freeze({ low: [0.24, 0.23, 0.18], high: [0.72, 0.67, 0.54], scale: 2.8, microScale: 0.075, fabric: 1, roughness: [0.68, 0.94], normal: 1.5 }),
|
|
11
|
+
'blocky-granite': Object.freeze({ low: [0.18, 0.20, 0.23], high: [0.63, 0.59, 0.53], scale: 2.5, microScale: 0.095, fabric: 2, roughness: [0.58, 0.88], normal: 1.2 }),
|
|
12
|
+
'layered-sandstone': Object.freeze({
|
|
13
|
+
low: [0.26, 0.035, 0.018],
|
|
14
|
+
high: [0.86, 0.24, 0.09],
|
|
15
|
+
strataDark: [0.10, 0.008, 0.006],
|
|
16
|
+
strataLight: [0.90, 0.46, 0.22],
|
|
17
|
+
scale: 3.2,
|
|
18
|
+
microScale: 0.08,
|
|
19
|
+
fabric: 3,
|
|
20
|
+
roughness: [0.68, 0.96],
|
|
21
|
+
normal: 0.95,
|
|
22
|
+
}),
|
|
23
|
+
'alpine-granite': Object.freeze({ low: [0.17, 0.20, 0.24], high: [0.67, 0.65, 0.59], scale: 2.7, microScale: 0.10, fabric: 4, roughness: [0.56, 0.88], normal: 1.25 }),
|
|
24
|
+
'columnar-basalt': Object.freeze({ low: [0.025, 0.03, 0.034], high: [0.24, 0.27, 0.26], scale: 3.0, microScale: 0.09, fabric: 5, roughness: [0.60, 0.90], normal: 1.3 }),
|
|
25
|
+
'sharp-karst': Object.freeze({ low: [0.21, 0.24, 0.20], high: [0.68, 0.71, 0.61], scale: 2.4, microScale: 0.075, fabric: 6, roughness: [0.70, 0.96], normal: 1.55 }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function clamp(value, minimum, maximum) {
|
|
29
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clamp01(value) {
|
|
33
|
+
return clamp(value, 0, 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function lerp(left, right, amount) {
|
|
37
|
+
return left + ((right - left) * amount);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function smoothstep(minimum, maximum, value) {
|
|
41
|
+
const amount = clamp01((value - minimum) / Math.max(1e-6, maximum - minimum));
|
|
42
|
+
return amount * amount * (3 - (2 * amount));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function linearToSrgb(value) {
|
|
46
|
+
const channel = clamp01(value);
|
|
47
|
+
// Match Three.js Color.convertLinearToSRGB byte-for-byte. Its deliberately
|
|
48
|
+
// rounded exponent is part of the accepted C7 pilot hashes.
|
|
49
|
+
return channel < 0.0031308
|
|
50
|
+
? channel * 12.92
|
|
51
|
+
: (1.055 * (channel ** 0.41666)) - 0.055;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fnv(value, hash) {
|
|
55
|
+
return Math.imul((hash ^ value) >>> 0, 16777619) >>> 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function c7SurfaceSeed(value) {
|
|
59
|
+
let hash = 2166136261 >>> 0;
|
|
60
|
+
for (const character of String(value ?? '')) {
|
|
61
|
+
hash = fnv(character.charCodeAt(0), hash);
|
|
62
|
+
}
|
|
63
|
+
return hash >>> 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function dimensionsArray(dimensions) {
|
|
67
|
+
if (Array.isArray(dimensions)) return dimensions.slice(0, 3).map(Number);
|
|
68
|
+
return [dimensions?.width, dimensions?.height, dimensions?.depth].map(Number);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve one isotropic, metre-based triplanar projection. The same scale is
|
|
73
|
+
* used on all three axes, so even highly elongated assets never inherit UV
|
|
74
|
+
* stretching. Scale grows sublinearly for large formations and is capped to
|
|
75
|
+
* keep mountain surfaces detailed instead of smearing one tile over 100 m.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveC7Projection({ dimensionsMetres, geology } = {}) {
|
|
78
|
+
const profile = C7_GEOLOGY_PROFILES[geology];
|
|
79
|
+
if (!profile) throw new RangeError(`Unknown C7 geology profile “${String(geology)}”.`);
|
|
80
|
+
const dimensions = dimensionsArray(dimensionsMetres);
|
|
81
|
+
if (dimensions.length !== 3 || dimensions.some((value) => !Number.isFinite(value) || value <= 0)) {
|
|
82
|
+
throw new RangeError('C7 projection requires positive width, height, and depth in metres.');
|
|
83
|
+
}
|
|
84
|
+
const sorted = [...dimensions].sort((left, right) => left - right);
|
|
85
|
+
const geometricMean = (dimensions[0] * dimensions[1] * dimensions[2]) ** (1 / 3);
|
|
86
|
+
const characteristicMetres = Math.sqrt(Math.max(geometricMean * sorted[1], 1e-6));
|
|
87
|
+
const scaleFactor = clamp(Math.sqrt(characteristicMetres / 1.5), 0.78, 4);
|
|
88
|
+
const scaleMetres = profile.scale * scaleFactor;
|
|
89
|
+
const nearDetailScaleMetres = profile.microScale * clamp(Math.sqrt(characteristicMetres), 0.8, 2.5);
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
characteristicMetres,
|
|
92
|
+
dimensionsMetres: Object.freeze([...dimensions]),
|
|
93
|
+
mode: 'world-metre-triplanar-isotropic',
|
|
94
|
+
nearDetailScaleMetres,
|
|
95
|
+
scaleMetres,
|
|
96
|
+
axisScale: Object.freeze([scaleMetres, scaleMetres, scaleMetres]),
|
|
97
|
+
tileSpan: Object.freeze(dimensions.map((value) => value / scaleMetres)),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function latticeHash(x, y, period, seed) {
|
|
102
|
+
const wrappedX = ((x % period) + period) % period;
|
|
103
|
+
const wrappedY = ((y % period) + period) % period;
|
|
104
|
+
const value = Math.sin((wrappedX * 127.1) + (wrappedY * 311.7) + (seed * 74.7)) * 43758.5453;
|
|
105
|
+
return value - Math.floor(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function tileNoise(u, v, frequency, seed) {
|
|
109
|
+
const x = u * frequency;
|
|
110
|
+
const y = v * frequency;
|
|
111
|
+
const x0 = Math.floor(x);
|
|
112
|
+
const y0 = Math.floor(y);
|
|
113
|
+
const fx0 = x - x0;
|
|
114
|
+
const fy0 = y - y0;
|
|
115
|
+
const fx = fx0 * fx0 * (3 - (2 * fx0));
|
|
116
|
+
const fy = fy0 * fy0 * (3 - (2 * fy0));
|
|
117
|
+
const a = latticeHash(x0, y0, frequency, seed);
|
|
118
|
+
const b = latticeHash(x0 + 1, y0, frequency, seed);
|
|
119
|
+
const c = latticeHash(x0, y0 + 1, frequency, seed);
|
|
120
|
+
const d = latticeHash(x0 + 1, y0 + 1, frequency, seed);
|
|
121
|
+
return lerp(lerp(a, b, fx), lerp(c, d, fx), fy);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function tileFbm(u, v, baseFrequency, seed, octaves = 4) {
|
|
125
|
+
let amplitude = 0.56;
|
|
126
|
+
let total = 0;
|
|
127
|
+
let weight = 0;
|
|
128
|
+
for (let octave = 0; octave < octaves; octave += 1) {
|
|
129
|
+
total += tileNoise(u, v, baseFrequency * (2 ** octave), seed + (octave * 19)) * amplitude;
|
|
130
|
+
weight += amplitude;
|
|
131
|
+
amplitude *= 0.48;
|
|
132
|
+
}
|
|
133
|
+
return total / weight;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function c7Field(u, v, profile, seed) {
|
|
137
|
+
const macro = tileFbm(u, v, 3, seed + 3, 4);
|
|
138
|
+
const micro = tileFbm(u, v, 17, seed + 11, 3);
|
|
139
|
+
const fine = tileNoise(u, v, 61, seed + 29);
|
|
140
|
+
let height = (macro * 0.70) + (micro * 0.24) + (fine * 0.06);
|
|
141
|
+
if (profile.fabric === 1 || profile.fabric === 6) {
|
|
142
|
+
const pitMask = 1 - smoothstep(0.10, 0.30, micro);
|
|
143
|
+
const pitBreakup = smoothstep(0.34, 0.68, tileFbm(u, v, 7, seed + 53, 2));
|
|
144
|
+
height -= pitMask * pitBreakup * (profile.fabric === 6 ? 0.10 : 0.075);
|
|
145
|
+
} else if (profile.fabric === 2 || profile.fabric === 4) {
|
|
146
|
+
const mineralPatch = smoothstep(0.48, 0.70, tileFbm(u, v, 5, seed + 67, 2));
|
|
147
|
+
height += smoothstep(0.88, 0.975, fine) * mineralPatch * 0.014;
|
|
148
|
+
} else if (profile.fabric === 3) {
|
|
149
|
+
height += Math.sin((v * 8.5 + u * 0.62) * Math.PI * 2) * lerp(0.009, 0.024, macro);
|
|
150
|
+
} else if (profile.fabric === 5) {
|
|
151
|
+
height -= (1 - smoothstep(0.08, 0.26, micro)) * 0.085;
|
|
152
|
+
}
|
|
153
|
+
let value = clamp01(0.5 + ((macro - 0.5) * 0.44) + ((micro - 0.5) * 0.34) + ((fine - 0.5) * 0.18));
|
|
154
|
+
if (profile.fabric === 3) value = clamp01(value + Math.sin((v * 9 + u * 0.5) * Math.PI * 2) * 0.08);
|
|
155
|
+
return { fine, height, macro, micro, value };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function pointToSegmentDistance(px, py, segment) {
|
|
159
|
+
const dx = segment.bx - segment.ax;
|
|
160
|
+
const dy = segment.by - segment.ay;
|
|
161
|
+
const amount = clamp01((((px - segment.ax) * dx) + ((py - segment.ay) * dy)) / Math.max(1e-6, (dx * dx) + (dy * dy)));
|
|
162
|
+
return Math.hypot(px - (segment.ax + (dx * amount)), py - (segment.ay + (dy * amount)));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function createFractureSegments(profile, seed) {
|
|
166
|
+
const segments = [];
|
|
167
|
+
const primaryCount = profile.fabric === 3 ? 4 : 3;
|
|
168
|
+
for (let index = 0; index < primaryCount; index += 1) {
|
|
169
|
+
const cx = 0.14 + (latticeHash(index, 17, 97, seed + 151) * 0.72);
|
|
170
|
+
const cy = 0.14 + (latticeHash(index, 31, 101, seed + 163) * 0.72);
|
|
171
|
+
const randomAngle = latticeHash(index, 47, 103, seed + 179);
|
|
172
|
+
let angle;
|
|
173
|
+
if (profile.fabric === 3) angle = (randomAngle - 0.5) * 0.34;
|
|
174
|
+
else if (profile.fabric === 5) angle = (Math.PI * 0.5) + ((randomAngle - 0.5) * 0.48);
|
|
175
|
+
else if (profile.fabric === 2 || profile.fabric === 4) {
|
|
176
|
+
angle = index % 2 === 0
|
|
177
|
+
? (Math.PI * 0.5) + ((randomAngle - 0.5) * 0.72)
|
|
178
|
+
: 0.55 + (randomAngle * 0.58);
|
|
179
|
+
} else angle = randomAngle * Math.PI;
|
|
180
|
+
const length = profile.fabric === 2 || profile.fabric === 4
|
|
181
|
+
? 0.10 + (latticeHash(index, 59, 107, seed + 191) * 0.19)
|
|
182
|
+
: 0.20 + (latticeHash(index, 59, 107, seed + 191) * 0.30);
|
|
183
|
+
const width = profile.fabric === 2 || profile.fabric === 4
|
|
184
|
+
? 0.0011 + (latticeHash(index, 71, 109, seed + 211) * 0.0018)
|
|
185
|
+
: 0.0018 + (latticeHash(index, 71, 109, seed + 211) * 0.0028);
|
|
186
|
+
const half = length * 0.5;
|
|
187
|
+
const start = { x: cx - (Math.cos(angle) * half), y: cy - (Math.sin(angle) * half) };
|
|
188
|
+
const end = { x: cx + (Math.cos(angle) * half), y: cy + (Math.sin(angle) * half) };
|
|
189
|
+
const perpendicularX = -Math.sin(angle);
|
|
190
|
+
const perpendicularY = Math.cos(angle);
|
|
191
|
+
const points = [start];
|
|
192
|
+
for (let step = 1; step < 4; step += 1) {
|
|
193
|
+
const amount = step / 4;
|
|
194
|
+
const jitter = (latticeHash(index * 7 + step, 79, 131, seed + 217) - 0.5)
|
|
195
|
+
* (profile.fabric === 3 ? 0.022 : 0.055);
|
|
196
|
+
points.push({
|
|
197
|
+
x: lerp(start.x, end.x, amount) + (perpendicularX * jitter),
|
|
198
|
+
y: lerp(start.y, end.y, amount) + (perpendicularY * jitter),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
points.push(end);
|
|
202
|
+
for (let step = 0; step < points.length - 1; step += 1) {
|
|
203
|
+
segments.push({
|
|
204
|
+
ax: points[step].x,
|
|
205
|
+
ay: points[step].y,
|
|
206
|
+
bx: points[step + 1].x,
|
|
207
|
+
by: points[step + 1].y,
|
|
208
|
+
width: width * lerp(1, 0.72, step / (points.length - 2)),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (index < 2 && profile.fabric !== 3) {
|
|
212
|
+
const branchStart = 0.40 + (latticeHash(index, 83, 113, seed + 223) * 0.28);
|
|
213
|
+
const branchX = lerp(start.x, end.x, branchStart);
|
|
214
|
+
const branchY = lerp(start.y, end.y, branchStart);
|
|
215
|
+
const branchAngle = angle + (index % 2 === 0 ? 0.78 : -0.92);
|
|
216
|
+
const branchLength = 0.10 + (latticeHash(index, 97, 127, seed + 227) * 0.13);
|
|
217
|
+
segments.push({
|
|
218
|
+
ax: branchX,
|
|
219
|
+
ay: branchY,
|
|
220
|
+
bx: branchX + (Math.cos(branchAngle) * branchLength),
|
|
221
|
+
by: branchY + (Math.sin(branchAngle) * branchLength),
|
|
222
|
+
width: width * 0.58,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return segments;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function fractureField(u, v, segments, seed) {
|
|
230
|
+
let fracture = 0;
|
|
231
|
+
for (const segment of segments) {
|
|
232
|
+
fracture = Math.max(fracture, 1 - smoothstep(
|
|
233
|
+
segment.width,
|
|
234
|
+
segment.width * 2.8,
|
|
235
|
+
pointToSegmentDistance(u, v, segment),
|
|
236
|
+
));
|
|
237
|
+
}
|
|
238
|
+
return fracture * lerp(0.22, 1, smoothstep(0.28, 0.70, tileFbm(u, v, 11, seed + 241, 2)));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Generate the accepted C7 BaseColor/NormalGL/Height/AO/Roughness map set. */
|
|
242
|
+
export function createC7GeologyMapData({ geology, seed, size = 512 } = {}) {
|
|
243
|
+
const profile = C7_GEOLOGY_PROFILES[geology];
|
|
244
|
+
if (!profile) throw new RangeError(`Unknown C7 geology profile “${String(geology)}”.`);
|
|
245
|
+
if (!Number.isInteger(size) || size < 32 || size > 2048) throw new RangeError('C7 map size must be an integer from 32 to 2048.');
|
|
246
|
+
const resolvedSeed = Math.round(Number(seed) || 0) >>> 0;
|
|
247
|
+
const fields = new Array(size * size);
|
|
248
|
+
const maps = Object.fromEntries([
|
|
249
|
+
'baseColor', 'normalGL', 'height', 'ao', 'roughness', 'smoothness', 'ormHeight',
|
|
250
|
+
].map((role) => [role, new Uint8Array(size * size * 4)]));
|
|
251
|
+
const fractureSegments = createFractureSegments(profile, resolvedSeed);
|
|
252
|
+
let minimumHeight = Infinity;
|
|
253
|
+
let maximumHeight = -Infinity;
|
|
254
|
+
for (let y = 0; y < size; y += 1) {
|
|
255
|
+
for (let x = 0; x < size; x += 1) {
|
|
256
|
+
const field = c7Field(x / size, y / size, profile, resolvedSeed);
|
|
257
|
+
field.fracture = fractureField(x / size, y / size, fractureSegments, resolvedSeed);
|
|
258
|
+
const fractureDepth = profile.fabric === 3 ? 0.018
|
|
259
|
+
: profile.fabric === 2 || profile.fabric === 4 ? 0.024 : 0.034;
|
|
260
|
+
field.height -= field.fracture * fractureDepth;
|
|
261
|
+
fields[(y * size) + x] = field;
|
|
262
|
+
minimumHeight = Math.min(minimumHeight, field.height);
|
|
263
|
+
maximumHeight = Math.max(maximumHeight, field.height);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const sampleHeight = (x, y) => fields[
|
|
267
|
+
((((y % size) + size) % size) * size) + (((x % size) + size) % size)
|
|
268
|
+
].height;
|
|
269
|
+
let hash = 2166136261 >>> 0;
|
|
270
|
+
for (let y = 0; y < size; y += 1) {
|
|
271
|
+
for (let x = 0; x < size; x += 1) {
|
|
272
|
+
const pixel = (y * size) + x;
|
|
273
|
+
const offset = pixel * 4;
|
|
274
|
+
const field = fields[pixel];
|
|
275
|
+
let color = profile.low.map((channel, index) => lerp(channel, profile.high[index], smoothstep(0.06, 0.94, field.value)));
|
|
276
|
+
if (profile.fabric === 2 || profile.fabric === 4) {
|
|
277
|
+
const u = x / size;
|
|
278
|
+
const v = y / size;
|
|
279
|
+
const darkPatch = smoothstep(0.50, 0.72, tileFbm(u, v, 5, resolvedSeed + 67, 2));
|
|
280
|
+
const quartzPatch = smoothstep(0.46, 0.68, tileFbm(u, v, 4, resolvedSeed + 73, 2));
|
|
281
|
+
const darkGrain = smoothstep(0.91, 0.985, field.fine) * darkPatch;
|
|
282
|
+
const quartz = smoothstep(0.92, 0.985, tileNoise(u, v, 83, resolvedSeed + 47)) * quartzPatch;
|
|
283
|
+
color = color.map((channel) => lerp(channel, 0.055, darkGrain * 0.46));
|
|
284
|
+
color = color.map((channel) => lerp(channel, 0.72, quartz * 0.34));
|
|
285
|
+
}
|
|
286
|
+
if (profile.fabric === 3) {
|
|
287
|
+
const u = x / size;
|
|
288
|
+
const v = y / size;
|
|
289
|
+
const warpedBed = (v * 6.4) + (u * 0.26) + ((field.macro - 0.5) * 1.35)
|
|
290
|
+
+ (Math.sin(u * Math.PI * 4) * 0.12);
|
|
291
|
+
const bed = 0.5 + (0.5 * Math.sin(warpedBed * Math.PI * 2));
|
|
292
|
+
const seamVariation = lerp(0.12, 1, tileNoise(u, v, 5, resolvedSeed + 91));
|
|
293
|
+
const fineBed = 0.5 + (0.5 * Math.sin(((v * 16) + (u * 0.42)
|
|
294
|
+
+ ((field.micro - 0.5) * 0.72)) * Math.PI * 2));
|
|
295
|
+
const creamSeam = Math.max(
|
|
296
|
+
smoothstep(0.90, 0.992, bed) * seamVariation,
|
|
297
|
+
smoothstep(0.955, 0.998, fineBed) * 0.34,
|
|
298
|
+
);
|
|
299
|
+
const burgundySeam = 1 - smoothstep(0.025, 0.18, bed);
|
|
300
|
+
color = color.map((channel, index) => lerp(channel, profile.strataLight[index], creamSeam * 0.72));
|
|
301
|
+
color = color.map((channel, index) => lerp(channel, profile.strataDark[index], burgundySeam * 0.58));
|
|
302
|
+
}
|
|
303
|
+
const topLeft = sampleHeight(x - 1, y + 1);
|
|
304
|
+
const top = sampleHeight(x, y + 1);
|
|
305
|
+
const topRight = sampleHeight(x + 1, y + 1);
|
|
306
|
+
const left = sampleHeight(x - 1, y);
|
|
307
|
+
const right = sampleHeight(x + 1, y);
|
|
308
|
+
const bottomLeft = sampleHeight(x - 1, y - 1);
|
|
309
|
+
const bottom = sampleHeight(x, y - 1);
|
|
310
|
+
const bottomRight = sampleHeight(x + 1, y - 1);
|
|
311
|
+
const gradientX = (topLeft + (2 * left) + bottomLeft) - (topRight + (2 * right) + bottomRight);
|
|
312
|
+
const gradientY = (bottomLeft + (2 * bottom) + bottomRight) - (topLeft + (2 * top) + topRight);
|
|
313
|
+
let normalX = gradientX * profile.normal * 0.55;
|
|
314
|
+
let normalY = gradientY * profile.normal * 0.55;
|
|
315
|
+
let normalZ = 1;
|
|
316
|
+
const normalLength = Math.hypot(normalX, normalY, normalZ);
|
|
317
|
+
normalX /= normalLength;
|
|
318
|
+
normalY /= normalLength;
|
|
319
|
+
normalZ /= normalLength;
|
|
320
|
+
const neighbourAverage = (left + right + top + bottom) * 0.25;
|
|
321
|
+
const concavity = clamp01(((neighbourAverage - field.height) * 18) + ((0.5 - field.micro) * 0.18));
|
|
322
|
+
const ao = clamp01(1 - (concavity * 0.68) - ((1 - field.macro) * 0.08) - (field.fracture * 0.16));
|
|
323
|
+
const roughness = clamp01(lerp(profile.roughness[0], profile.roughness[1], field.micro)
|
|
324
|
+
+ (concavity * 0.06) + (field.fracture * 0.025));
|
|
325
|
+
const height = clamp01((field.height - minimumHeight) / Math.max(1e-6, maximumHeight - minimumHeight));
|
|
326
|
+
const colorBytes = color.map((channel) => Math.round(linearToSrgb(channel) * 255));
|
|
327
|
+
const normalBytes = [normalX, normalY, normalZ].map((channel) => Math.round((channel * 0.5 + 0.5) * 255));
|
|
328
|
+
const heightByte = Math.round(height * 255);
|
|
329
|
+
const aoByte = Math.round(ao * 255);
|
|
330
|
+
const roughnessByte = Math.round(roughness * 255);
|
|
331
|
+
const smoothnessByte = 255 - roughnessByte;
|
|
332
|
+
const values = {
|
|
333
|
+
baseColor: [...colorBytes, 255],
|
|
334
|
+
normalGL: [...normalBytes, 255],
|
|
335
|
+
height: [heightByte, heightByte, heightByte, 255],
|
|
336
|
+
ao: [aoByte, aoByte, aoByte, 255],
|
|
337
|
+
roughness: [roughnessByte, roughnessByte, roughnessByte, 255],
|
|
338
|
+
smoothness: [smoothnessByte, smoothnessByte, smoothnessByte, 255],
|
|
339
|
+
ormHeight: [aoByte, roughnessByte, 0, heightByte],
|
|
340
|
+
};
|
|
341
|
+
for (const [role, channels] of Object.entries(values)) maps[role].set(channels, offset);
|
|
342
|
+
for (const value of [...colorBytes, normalBytes[0], normalBytes[1], heightByte, aoByte, roughnessByte, smoothnessByte]) {
|
|
343
|
+
hash = fnv(value, hash);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
audit: Object.freeze({
|
|
349
|
+
fractureSegments: fractureSegments.length,
|
|
350
|
+
hash: hash.toString(16).padStart(8, '0'),
|
|
351
|
+
heightRange: Object.freeze([minimumHeight, maximumHeight]),
|
|
352
|
+
profile: geology,
|
|
353
|
+
roles: Object.freeze(Object.keys(maps)),
|
|
354
|
+
seed: resolvedSeed,
|
|
355
|
+
size,
|
|
356
|
+
}),
|
|
357
|
+
maps,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function createC7SurfaceSpecification({ assetId, dimensionsMetres, geology, seed = null } = {}) {
|
|
362
|
+
const resolvedSeed = seed === null ? c7SurfaceSeed(`${assetId}:c7-v1`) : Math.round(Number(seed) || 0) >>> 0;
|
|
363
|
+
return Object.freeze({
|
|
364
|
+
assetId: String(assetId ?? ''),
|
|
365
|
+
geology,
|
|
366
|
+
mapResolution: 512,
|
|
367
|
+
projection: resolveC7Projection({ dimensionsMetres, geology }),
|
|
368
|
+
schema: C7_GEOLOGY_SURFACE_SCHEMA,
|
|
369
|
+
seed: resolvedSeed,
|
|
370
|
+
version: C7_GEOLOGY_SURFACE_VERSION,
|
|
371
|
+
});
|
|
372
|
+
}
|
package/src/shaders-tsl/water.js
CHANGED
|
@@ -560,12 +560,20 @@ export function createWaterNodeMaterial({
|
|
|
560
560
|
const gerstnerNormalY = gerstnerNormal.y.toVar();
|
|
561
561
|
|
|
562
562
|
const detailFade = smoothstep(16.0, 60.0, viewDistance).oneMinus();
|
|
563
|
+
// Beyond ~100 m the ripple field is far below one texel per period, so the
|
|
564
|
+
// 20% residual left by `detailFade` stopped reading as water and started
|
|
565
|
+
// reading as a fixed screen-space weave that crawls with the camera — the
|
|
566
|
+
// "uniformly tiled ripple pattern" §13 rejects outright. Fading it to a
|
|
567
|
+
// 10% floor across 100-360 m removes the aliasing without flattening any
|
|
568
|
+
// distance a hero shot can actually resolve (D19-061).
|
|
569
|
+
const horizonFade = smoothstep(100.0, 360.0, viewDistance).oneMinus();
|
|
563
570
|
const swashOptics = clamp(vSwashZone, 0.0, 1.0).toVar();
|
|
564
571
|
// A centimetres-thin film is optically smooth at this camera scale. Full
|
|
565
572
|
// open-water normal detail turned every texel into a bright grazing-angle
|
|
566
573
|
// mirror and made the swash look like an opaque sheet of snow.
|
|
567
574
|
const detailStrength = u.uDetailNormalStrength
|
|
568
575
|
.mul(mix(0.2, 1.0, detailFade))
|
|
576
|
+
.mul(mix(0.1, 1.0, horizonFade))
|
|
569
577
|
.mul(mix(1.0, 0.18, swashOptics)).toVar();
|
|
570
578
|
const flowOffset = u.uFlowDirection.mul(u.uTime).mul(u.uFlowSpeed).toVar();
|
|
571
579
|
const detailUv1 = vRestWorldXZ.mul(u.uDetailScale).add(flowOffset.mul(0.55));
|
package/src/sky/cloudShadow.js
CHANGED
|
@@ -264,6 +264,7 @@ export function createCloudShadowPass({
|
|
|
264
264
|
let frame = 0;
|
|
265
265
|
let cadence = Math.max(1, Math.round(Number(bakeInterval) || 1));
|
|
266
266
|
let groundY = Number.isFinite(Number(groundReferenceY)) ? Number(groundReferenceY) : 0;
|
|
267
|
+
let lightDirectionOverride = null;
|
|
267
268
|
|
|
268
269
|
const api = {
|
|
269
270
|
axisU,
|
|
@@ -305,6 +306,24 @@ export function createCloudShadowPass({
|
|
|
305
306
|
if (Number.isFinite(next)) groundY = next;
|
|
306
307
|
},
|
|
307
308
|
|
|
309
|
+
setLightDirection(value = null) {
|
|
310
|
+
if (value === null) {
|
|
311
|
+
lightDirectionOverride = null;
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const next = value?.isVector3
|
|
315
|
+
? value.clone()
|
|
316
|
+
: Array.isArray(value) || ArrayBuffer.isView(value)
|
|
317
|
+
? new THREE.Vector3().fromArray(Array.from(value))
|
|
318
|
+
: null;
|
|
319
|
+
if (!next || next.lengthSq() === 0) {
|
|
320
|
+
throw new TypeError('Cloud-shadow light direction must be a non-zero Vector3 or numeric array.');
|
|
321
|
+
}
|
|
322
|
+
lightDirectionOverride = next.normalize();
|
|
323
|
+
lightDirection.value.copy(lightDirectionOverride);
|
|
324
|
+
return lightDirectionOverride.toArray();
|
|
325
|
+
},
|
|
326
|
+
|
|
308
327
|
setResolution(value) {
|
|
309
328
|
const next = Math.max(64, Math.round(Number(value) || target.width));
|
|
310
329
|
if (next !== target.width) target.setSize(next, next);
|
|
@@ -321,7 +340,12 @@ export function createCloudShadowPass({
|
|
|
321
340
|
cameraPositionScratch.z,
|
|
322
341
|
);
|
|
323
342
|
|
|
324
|
-
|
|
343
|
+
// Stylized time-of-day rigs can deliberately keep their gameplay key
|
|
344
|
+
// above the horizon after the physical sky sun reaches it. The cloud
|
|
345
|
+
// transmittance field must follow the light it attenuates; otherwise the
|
|
346
|
+
// near-horizontal physical ray becomes opaque and removes that elevated
|
|
347
|
+
// key from every surface.
|
|
348
|
+
const sunDirection = lightDirectionOverride ?? sun.direction.value;
|
|
325
349
|
const elevation = Math.asin(Math.max(-1, Math.min(1, sunDirection.y)));
|
|
326
350
|
const sunIsActive = !timeOfDay || elevation >= CLOUD_SHADOW_NIGHT_THRESHOLD;
|
|
327
351
|
lightDirection.value.copy(
|
package/src/sky/skySystem.js
CHANGED
|
@@ -702,6 +702,15 @@ export class SkySystem {
|
|
|
702
702
|
return this.#sunDriver.state;
|
|
703
703
|
}
|
|
704
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Align the cloud-transmittance bake with a scene directional key without
|
|
707
|
+
* moving the visible celestial sun. Pass null to resume physical-sun input.
|
|
708
|
+
*/
|
|
709
|
+
setCloudShadowDirection(direction = null) {
|
|
710
|
+
this.#assertLive();
|
|
711
|
+
return this.#cloudShadow.setLightDirection(direction);
|
|
712
|
+
}
|
|
713
|
+
|
|
705
714
|
/** The sky dome. */
|
|
706
715
|
get dome() {
|
|
707
716
|
return this.#dome;
|
package/src/styles/index.js
CHANGED
|
@@ -145,3 +145,26 @@ export {
|
|
|
145
145
|
serializeSceneQualityProfile,
|
|
146
146
|
validateSceneQualityProfile,
|
|
147
147
|
} from './sceneQualityProfiles.js';
|
|
148
|
+
export {
|
|
149
|
+
NEUTRAL_ENVIRONMENT_PRESET,
|
|
150
|
+
NEUTRAL_ENVIRONMENT_RESOLVED_SETTINGS,
|
|
151
|
+
NEUTRAL_GRASS_SETTINGS,
|
|
152
|
+
NEUTRAL_GROUND_SHADER_SETTINGS,
|
|
153
|
+
NEUTRAL_MANUFACTURED_SURFACE_DOCUMENT,
|
|
154
|
+
NEUTRAL_MANUFACTURED_SURFACE_RESOLVED_SETTINGS,
|
|
155
|
+
NEUTRAL_MANUFACTURED_SURFACE_SETTINGS,
|
|
156
|
+
NEUTRAL_POST_SETTINGS,
|
|
157
|
+
NEUTRAL_SKY_SETTINGS,
|
|
158
|
+
NEUTRAL_STYLE_BUNDLE,
|
|
159
|
+
NEUTRAL_STYLE_NON_SHADING_KEYS,
|
|
160
|
+
NEUTRAL_STYLE_PRESET_ID,
|
|
161
|
+
NEUTRAL_STYLE_SLOT_COVERAGE,
|
|
162
|
+
NEUTRAL_STYLE_SLOT_IDS,
|
|
163
|
+
NEUTRAL_TOON_RESOLVED_SETTINGS,
|
|
164
|
+
NEUTRAL_TOON_SETTINGS,
|
|
165
|
+
NEUTRAL_VEGETATION_SHADER_SETTINGS,
|
|
166
|
+
NEUTRAL_WATER_STYLE,
|
|
167
|
+
auditNeutralStyleShadingOnly,
|
|
168
|
+
describeNeutralStyleCoverage,
|
|
169
|
+
registerNeutralStylePresets,
|
|
170
|
+
} from './neutralStylePresets.js';
|