@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.
Files changed (123) hide show
  1. package/NPM-LIBRARY.md +230 -0
  2. package/README.md +78 -36
  3. package/agents/PROMPTS.md +91 -0
  4. package/agents/README.md +9 -14
  5. package/agents/claude/CLAUDE.md +8 -0
  6. package/agents/codex/AGENTS.md +161 -0
  7. package/agents/cursor/toonlab.mdc +37 -0
  8. package/agents/references/mcp-asset-discovery.md +12 -9
  9. package/agents/references/runtime-entry-points.md +5 -4
  10. package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
  11. package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
  12. package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
  13. package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
  14. package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
  15. package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
  16. package/cli/toonlab.mjs +3 -2
  17. package/database/fal-patina.mjs +131 -0
  18. package/database/generation-service.mjs +108 -7
  19. package/database/migrations/0007_creation_revisions.sql +80 -0
  20. package/database/migrations/0008_creation_revision_hardening.sql +98 -0
  21. package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
  22. package/database/migrations/0010_external_catalog_assets.sql +28 -0
  23. package/database/providers.mjs +195 -16
  24. package/database/repository.mjs +518 -70
  25. package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
  26. package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
  27. package/mcp/public-catalog.mjs +4 -52
  28. package/mcp/server.mjs +175 -12
  29. package/mcp/vite-plugin.mjs +78 -2
  30. package/package.json +32 -12
  31. package/scripts/generate-catalog-seed.mjs +34 -7
  32. package/src/asset-policy/catalogLicenses.js +64 -6
  33. package/src/assetlib/loadImported.js +15 -1
  34. package/src/catalog/officialCatalogAssetRuntime.js +13 -2
  35. package/src/catalog/officialCatalogLod.js +34 -1
  36. package/src/catalog/officialCatalogPlacement.js +29 -14
  37. package/src/character/animationRetarget.js +2 -0
  38. package/src/character/characterRig.js +6 -1
  39. package/src/character/characterRuntime.js +175 -7
  40. package/src/environment/environmentMaterialAdapter.js +30 -0
  41. package/src/environment/environmentPresets.js +90 -0
  42. package/src/environment/environmentSunShadowPass.js +42 -0
  43. package/src/environment/toonLabSurfaceLighting.js +108 -10
  44. package/src/environment/urbanPropMaterial.js +104 -2
  45. package/src/lighting/lightingSystem.js +65 -10
  46. package/src/renderer/index.js +7 -0
  47. package/src/renderer/styleComparison.js +985 -0
  48. package/src/rock-shader/index.js +5 -0
  49. package/src/rock-shader/rockGeometryDetail.js +701 -0
  50. package/src/rock-shader/rockMaterial.js +782 -39
  51. package/src/rock-shader/rockRegionRuntime.js +185 -0
  52. package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
  53. package/src/rock-shader/rockShaderRuntime.js +354 -67
  54. package/src/rock-shader/rockShaderSettings.js +246 -16
  55. package/src/rock-shader/rockTangentIntegrity.js +136 -0
  56. package/src/rock-shader/rockTextureIntegrity.js +115 -0
  57. package/src/rockgen/index.js +1 -0
  58. package/src/rockgen/rockDocument.js +27 -3
  59. package/src/rockgen/surface/c7GeologySurface.js +372 -0
  60. package/src/shaders-tsl/water.js +8 -0
  61. package/src/sky/cloudShadow.js +25 -1
  62. package/src/sky/skySystem.js +9 -0
  63. package/src/styles/index.js +23 -0
  64. package/src/styles/neutralStylePresets.js +712 -0
  65. package/src/styles/sceneStyleRuntime.js +51 -4
  66. package/src/styles/styleAdapters.js +13 -2
  67. package/src/styles/styleBundle.js +1 -1
  68. package/src/texgen/evaluateTexture.js +30 -3
  69. package/src/texgen/textureGenerators.js +63 -31
  70. package/src/texgen/textureSettings.js +25 -8
  71. package/src/toon/toonSettings.js +96 -2
  72. package/src/vegetation/branchTree.js +327 -42
  73. package/src/vegetation/index.js +24 -2
  74. package/src/vegetation/scatter.js +372 -1
  75. package/src/vegetation/stylizedTree.js +312 -126
  76. package/src/vegetation/stylizedTreeFoliage.js +229 -7
  77. package/src/vegetation/treeSurfaceTextures.js +328 -0
  78. package/src/version.js +1 -1
  79. package/src/water/waterSettings.js +15 -7
  80. package/types/asset-policy/catalogLicenses.d.ts +2 -0
  81. package/types/asset-policy/index.d.ts +2 -0
  82. package/types/assetlib/loadImported.d.ts +3 -1
  83. package/types/catalog/officialCatalogLod.d.ts +63 -5
  84. package/types/character/characterRig.d.ts +59 -0
  85. package/types/character/characterRuntime.d.ts +1 -0
  86. package/types/environment/environmentMaterialAdapter.d.ts +3 -1
  87. package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
  88. package/types/index.d.ts +89 -7
  89. package/types/lighting/lightingSystem.d.ts +7 -0
  90. package/types/react/index.d.ts +20 -0
  91. package/types/renderer/index.d.ts +1 -0
  92. package/types/renderer/styleComparison.d.ts +409 -0
  93. package/types/rock-shader/index.d.ts +5 -0
  94. package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
  95. package/types/rock-shader/rockMaterial.d.ts +3 -0
  96. package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
  97. package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
  98. package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
  99. package/types/rock-shader/rockShaderSettings.d.ts +19 -5
  100. package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
  101. package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
  102. package/types/rockgen/index.d.ts +1 -0
  103. package/types/rockgen/rockDocument.d.ts +29 -1
  104. package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
  105. package/types/sky/cloudShadow.d.ts +1 -0
  106. package/types/styles/index.d.ts +1 -0
  107. package/types/styles/neutralStylePresets.d.ts +944 -0
  108. package/types/styles/sceneStyleRuntime.d.ts +20 -0
  109. package/types/vegetation/branchTree.d.ts +4 -0
  110. package/types/vegetation/index.d.ts +9 -7
  111. package/types/vegetation/scatter.d.ts +1 -0
  112. package/types/vegetation/stylizedTree.d.ts +1 -2
  113. package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
  114. package/types/version.d.ts +1 -1
  115. package/agents/references/geology-playbook.md +0 -118
  116. package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
  117. package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
  118. package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
  119. package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
  120. package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
  121. package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
  122. package/src/vegetation/legacyTreePresets.js +0 -120
  123. 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
+ }
@@ -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));
@@ -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
- const sunDirection = sun.direction.value;
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(
@@ -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;
@@ -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';