@call-me-sensei/toonlab 0.4.21 → 0.4.23

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 (129) hide show
  1. package/NPM-LIBRARY.md +87 -27
  2. package/README.md +11 -10
  3. package/agents/PROMPTS.md +17 -3
  4. package/agents/README.md +13 -3
  5. package/agents/claude/CLAUDE.md +3 -0
  6. package/agents/codex/AGENTS.md +112 -159
  7. package/agents/cursor/toonlab.mdc +7 -3
  8. package/agents/examples/game-foundation.mjs +104 -0
  9. package/agents/references/game-lifecycle.md +57 -0
  10. package/agents/references/runtime-entry-points.md +26 -0
  11. package/agents/references/style-bundles.md +4 -1
  12. package/agents/skills/claude/asset-sourcing/SKILL.md +7 -4
  13. package/agents/skills/claude/environment/SKILL.md +4 -0
  14. package/agents/skills/claude/game-dev/SKILL.md +63 -87
  15. package/agents/skills/claude/lighting/SKILL.md +36 -0
  16. package/agents/skills/claude/post-processing/SKILL.md +8 -3
  17. package/agents/skills/claude/rock-ground-shaders/SKILL.md +10 -15
  18. package/agents/skills/claude/rockgen/SKILL.md +52 -21
  19. package/agents/skills/claude/scene-style-application/SKILL.md +4 -0
  20. package/agents/skills/claude/style-presets/SKILL.md +11 -8
  21. package/agents/skills/claude/toon-shading/SKILL.md +4 -0
  22. package/agents/skills/claude/vegetation-sky/SKILL.md +13 -7
  23. package/agents/skills/claude/visual-verification/SKILL.md +45 -119
  24. package/agents/skills/claude/water/SKILL.md +6 -2
  25. package/agents/skills/codex/asset-sourcing/SKILL.md +7 -4
  26. package/agents/skills/codex/environment/SKILL.md +4 -0
  27. package/agents/skills/codex/game-dev/SKILL.md +63 -87
  28. package/agents/skills/codex/lighting/SKILL.md +36 -0
  29. package/agents/skills/codex/post-processing/SKILL.md +8 -3
  30. package/agents/skills/codex/rock-ground-shaders/SKILL.md +10 -15
  31. package/agents/skills/codex/rockgen/SKILL.md +52 -21
  32. package/agents/skills/codex/scene-style-application/SKILL.md +4 -0
  33. package/agents/skills/codex/style-presets/SKILL.md +11 -8
  34. package/agents/skills/codex/toon-shading/SKILL.md +4 -0
  35. package/agents/skills/codex/vegetation-sky/SKILL.md +13 -7
  36. package/agents/skills/codex/visual-verification/SKILL.md +45 -119
  37. package/agents/skills/codex/water/SKILL.md +6 -2
  38. package/database/apply-one-catalog-seed.mjs +63 -0
  39. package/database/providers.mjs +1 -0
  40. package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
  41. package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
  42. package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
  43. package/mcp/lab-management.mjs +11 -8
  44. package/mcp/public-catalog.mjs +0 -16
  45. package/mcp/tree-lab-contract.mjs +2381 -0
  46. package/mcp/vite-plugin.mjs +5 -1
  47. package/package.json +29 -18
  48. package/src/catalog/officialCatalog.js +1 -0
  49. package/src/catalog/officialCatalogAssetRuntime.js +98 -1
  50. package/src/catalog/officialCatalogLod.js +160 -16
  51. package/src/catalog/officialCatalogPlacement.js +52 -12
  52. package/src/catalog/officialCatalogProvider.js +79 -8
  53. package/src/catalog/officialCatalogRockPackage.js +172 -0
  54. package/src/cloud/cloudReprojection.js +40 -7
  55. package/src/cloud/cloudVolume.js +44 -7
  56. package/src/cloud/noise/baseShapeVolume.js +8 -6
  57. package/src/cloud/noise/cirrusMap.js +2 -1
  58. package/src/cloud/noise/curlNoise.js +2 -1
  59. package/src/cloud/noise/erosionVolume.js +2 -1
  60. package/src/cloud/noise/textureCache.js +77 -0
  61. package/src/cloud/noise/weatherMap.js +2 -1
  62. package/src/core/sha256.js +83 -0
  63. package/src/ground-shader/groundShaderMaterial.js +12 -3
  64. package/src/rock-shader/rockMaterial.js +205 -28
  65. package/src/rock-shader/rockShaderRuntime.js +17 -1
  66. package/src/rock-shader/rockShaderSettings.js +16 -0
  67. package/src/rockgen/index.js +9 -0
  68. package/src/rockgen/lod/index.js +1 -0
  69. package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
  70. package/src/rockgen/rockDocument.js +899 -35
  71. package/src/rockgen/surface/c7GeologySurface.js +39 -9
  72. package/src/rockgen/surface/naturalRockSurface.js +1948 -0
  73. package/src/shaders-tsl/chunks/projected-water-caustics.js +14 -2
  74. package/src/shaders-tsl/chunks/water-foam.js +6 -6
  75. package/src/shaders-tsl/chunks/water-lighting.js +28 -5
  76. package/src/shaders-tsl/chunks/water-shore-state.js +5 -2
  77. package/src/shaders-tsl/chunks/water-waves.js +10 -5
  78. package/src/shaders-tsl/flower.js +7 -4
  79. package/src/shaders-tsl/grass.js +4 -3
  80. package/src/shaders-tsl/water-shore-state-simulation.js +3 -2
  81. package/src/shaders-tsl/water.js +209 -41
  82. package/src/sky/skySystem.js +30 -7
  83. package/src/toon/settings/perspectiveRemovalSettings.js +1 -1
  84. package/src/toon/toonSettings.js +74 -0
  85. package/src/vegetation/stylizedFlower.js +6 -0
  86. package/src/version.js +1 -1
  87. package/src/water/waterDetailSpectrum.js +91 -0
  88. package/src/water/waterDynamics.js +422 -0
  89. package/src/water/waterFoamParticles.js +196 -0
  90. package/src/water/waterFoamTexture.js +57 -0
  91. package/src/water/waterHydrodynamics.js +209 -0
  92. package/src/water/waterMaterial.js +1 -0
  93. package/src/water/waterRenderExtension.js +107 -0
  94. package/src/water/waterScenePasses.js +14 -6
  95. package/src/water/waterSettings.js +53 -26
  96. package/src/water/waterShoreMaterial.js +38 -14
  97. package/src/water/waterSpectralOcean.js +137 -0
  98. package/src/water/waterStageSettings.js +19 -1
  99. package/src/water/waterSurface.js +62 -18
  100. package/src/water/waterUnderwaterAtmosphere.js +30 -3
  101. package/types/catalog/officialCatalog.d.ts +1 -0
  102. package/types/catalog/officialCatalogPlacement.d.ts +4 -0
  103. package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
  104. package/types/cloud/noise/baseShapeVolume.d.ts +1 -1
  105. package/types/cloud/noise/textureCache.d.ts +12 -0
  106. package/types/core/sha256.d.ts +2 -0
  107. package/types/index.d.ts +30 -0
  108. package/types/rock-shader/rockMaterial.d.ts +3 -0
  109. package/types/rock-shader/rockShaderSettings.d.ts +2 -0
  110. package/types/rockgen/index.d.ts +1 -0
  111. package/types/rockgen/lod/index.d.ts +1 -0
  112. package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
  113. package/types/rockgen/rockDocument.d.ts +654 -71
  114. package/types/rockgen/surface/naturalRockSurface.d.ts +100 -0
  115. package/types/shaders-tsl/chunks/projected-water-caustics.d.ts +7 -1
  116. package/types/shaders-tsl/chunks/water-lighting.d.ts +4 -3
  117. package/types/shaders-tsl/water.d.ts +2 -1
  118. package/types/version.d.ts +1 -1
  119. package/types/water/waterDetailSpectrum.d.ts +13 -0
  120. package/types/water/waterDynamics.d.ts +89 -0
  121. package/types/water/waterFoamParticles.d.ts +32 -0
  122. package/types/water/waterFoamTexture.d.ts +1 -0
  123. package/types/water/waterHydrodynamics.d.ts +49 -0
  124. package/types/water/waterRenderExtension.d.ts +16 -0
  125. package/types/water/waterScenePasses.d.ts +1 -1
  126. package/types/water/waterSettings.d.ts +1 -0
  127. package/types/water/waterSpectralOcean.d.ts +17 -0
  128. package/types/water/waterStageSettings.d.ts +1 -1
  129. package/types/water/waterUnderwaterAtmosphere.d.ts +19 -4
@@ -1003,6 +1003,7 @@ export function marchCloud({
1003
1003
  moonRadiance,
1004
1004
  jitter,
1005
1005
  stepConeAngle = float(0.003),
1006
+ surfaceLighting = float(1),
1006
1007
  steps = CLOUD_PRIMARY_MARCH_STEPS,
1007
1008
  }) {
1008
1009
  const scattered = vec3(0).toVar();
@@ -1027,7 +1028,8 @@ export function marchCloud({
1027
1028
  weight,
1028
1029
  }) => {
1029
1030
  If(
1030
- surfaceLightSet.lessThan(0.5).and(weight.greaterThan(CLOUD_SURFACE_LIGHT_MIN_WEIGHT)),
1031
+ float(surfaceLighting).greaterThan(0).and(surfaceLightSet.lessThan(0.5))
1032
+ .and(weight.greaterThan(CLOUD_SURFACE_LIGHT_MIN_WEIGHT)),
1031
1033
  () => {
1032
1034
  const radius = CLOUD_SURFACE_NORMAL_RADIUS;
1033
1035
  // Use the broad envelope for the lighting normal. Including the full
@@ -1136,11 +1138,11 @@ export function marchCloud({
1136
1138
  CLOUD_INTERIOR_COARSEN_FULL,
1137
1139
  opticalDepthAccum,
1138
1140
  ).toVar();
1139
- const coreStep = mix(
1141
+ const coreStep = min(shell.tEnd.sub(t), mix(
1140
1142
  surfaceCoreStep,
1141
1143
  effectiveBaseStep.mul(CLOUD_INTERIOR_STEP_SCALE),
1142
1144
  interiorOpenness,
1143
- ).toVar();
1145
+ )).toVar();
1144
1146
  stepSize.assign(coreStep);
1145
1147
  const cheapLight = step(0.3, transmittance).oneMinus().toVar();
1146
1148
  const lightOpticalDepth = lightDepthNode(
@@ -1231,11 +1233,11 @@ export function marchCloud({
1231
1233
  CLOUD_INTERIOR_COARSEN_FULL,
1232
1234
  opticalDepthAccum,
1233
1235
  ).toVar();
1234
- const coreStep = mix(
1236
+ const coreStep = min(shell.tEnd.sub(t), mix(
1235
1237
  surfaceCoreStep,
1236
1238
  effectiveBaseStep.mul(CLOUD_INTERIOR_STEP_SCALE),
1237
1239
  interiorOpenness,
1238
- ).toVar();
1240
+ )).toVar();
1239
1241
  stepSize.assign(coreStep);
1240
1242
  const cheapLight = step(0.3, transmittance).oneMinus().toVar();
1241
1243
  const lightOpticalDepth = lightDepthNode(
@@ -1403,6 +1405,11 @@ export function createCloudVolumeMaterial({
1403
1405
  const stepConeAngle = uniform(0.003);
1404
1406
  const baseShapeResolution = uniform(resolved.baseShape?.image?.width ?? 64);
1405
1407
  const rayHitDistance = property('float', 'toonlabCloudRayHitDistance');
1408
+ const opaqueViewDistance = property('float', 'toonlabCloudOpaqueViewDistance');
1409
+ const sceneDepthFallback = new THREE.DataTexture(new Float32Array([65000]), 1, 1, THREE.RedFormat, THREE.FloatType);
1410
+ sceneDepthFallback.needsUpdate = true;
1411
+ const sceneDepthNode = texture(sceneDepthFallback);
1412
+ const sceneDepthEnabled = uniform(0);
1406
1413
  // A generated cirrus mask ships with the field; the 1x1 stand-in keeps the
1407
1414
  // graph stable when a host explicitly clears it. `cirrusEnabled` is the gate,
1408
1415
  // so setCirrusTexture(null) still removes the deck exactly.
@@ -1482,6 +1489,7 @@ export function createCloudVolumeMaterial({
1482
1489
  pixelJitter,
1483
1490
  rayBasis,
1484
1491
  stepConeAngle,
1492
+ sceneDepthEnabled,
1485
1493
  };
1486
1494
 
1487
1495
  const material = new NodeMaterial();
@@ -1502,6 +1510,16 @@ export function createCloudVolumeMaterial({
1502
1510
  screenUV.y.mul(-2).add(1),
1503
1511
  ).add(pixelJitter).toVar();
1504
1512
  const viewDir = normalize(rayBasis.mul(vec3(ndc, 1))).toVar();
1513
+ opaqueViewDistance.assign(65000);
1514
+ If(sceneDepthEnabled.greaterThan(0.5), () => {
1515
+ const depthUv = vec2(ndc.x.mul(0.5).add(0.5), ndc.y.mul(-0.5).add(0.5));
1516
+ opaqueViewDistance.assign(min(max(sceneDepthNode.sample(depthUv).r, 0), 65000));
1517
+ });
1518
+ const opaqueRayDistance = select(
1519
+ sceneDepthEnabled.greaterThan(0.5).and(opaqueViewDistance.lessThan(64000)),
1520
+ opaqueViewDistance.mul(length(rayBasis.mul(vec3(ndc, 1)))),
1521
+ float(1e20),
1522
+ ).toVar();
1505
1523
  const cameraAltitude = max(cameraPositionUniform.y.sub(groundLevelUniform), 0.5).toVar();
1506
1524
 
1507
1525
  const baseAltitude = max(shape.altitude, 0).toVar();
@@ -1509,7 +1527,7 @@ export function createCloudVolumeMaterial({
1509
1527
  const shell = intersectCloudShell({
1510
1528
  baseAltitude,
1511
1529
  cameraAltitude,
1512
- maxDistance: fade.maxMarchDist,
1530
+ maxDistance: min(fade.maxMarchDist, opaqueRayDistance),
1513
1531
  topAltitude,
1514
1532
  viewDir,
1515
1533
  });
@@ -1530,6 +1548,9 @@ export function createCloudVolumeMaterial({
1530
1548
 
1531
1549
  // --- the volumetric march ---------------------------------------------
1532
1550
  const march = marchCloud({
1551
+ surfaceLighting: style?.surfaceLight
1552
+ ? style.enabled.mul(style.amount).mul(style.surfaceLight.enabled).mul(style.surfaceLight.amount)
1553
+ : float(0),
1533
1554
  cameraPosition: cameraPositionUniform,
1534
1555
  coarseDensityNode: field.sampleCoarseDensityNode,
1535
1556
  cosMoon,
@@ -1739,6 +1760,7 @@ export function createCloudVolumeMaterial({
1739
1760
  ).rgb.toVar();
1740
1761
 
1741
1762
  // Composite the cirrus/haze deck behind the volumetric layer.
1763
+ deckOpacity.mulAssign(step(deckDistance, opaqueRayDistance));
1742
1764
  const deckTerm = deckRadiance.mul(deckOpacity).toVar();
1743
1765
  scattered.addAssign(deckTerm.mul(transmittance));
1744
1766
  transmittance.mulAssign(deckOpacity.oneMinus());
@@ -1754,7 +1776,7 @@ export function createCloudVolumeMaterial({
1754
1776
  // are matched by cloudReprojection's MRT render target.
1755
1777
  material.fragmentNode = mrt({
1756
1778
  output: cloudColorNode,
1757
- rayHitDist: vec4(rayHitDistance, 0, 0, 1),
1779
+ rayHitDist: vec4(rayHitDistance, opaqueViewDistance, 0, 1),
1758
1780
  });
1759
1781
 
1760
1782
  const rightScratch = new THREE.Vector3();
@@ -1762,6 +1784,20 @@ export function createCloudVolumeMaterial({
1762
1784
  const forwardScratch = new THREE.Vector3();
1763
1785
 
1764
1786
  const api = {
1787
+ sceneDepthNode,
1788
+ /** Positive view-space depth (metres) in R; clear to >=65000. Host-owned. */
1789
+ setSceneDepthTexture(map = null) {
1790
+ if (map !== null && (!map?.isTexture || map.isDepthTexture)) {
1791
+ throw new TypeError('Cloud scene depth must be a linear view-distance color texture, not hardware depth.');
1792
+ }
1793
+ if (map && (map.colorSpace !== THREE.NoColorSpace
1794
+ || ![THREE.FloatType, THREE.HalfFloatType].includes(map.type)
1795
+ || map.minFilter !== THREE.NearestFilter || map.magFilter !== THREE.NearestFilter)) {
1796
+ throw new TypeError('Cloud scene depth requires a nearest-filtered float texture with NoColorSpace.');
1797
+ }
1798
+ sceneDepthNode.value = map ?? sceneDepthFallback;
1799
+ sceneDepthEnabled.value = map ? 1 : 0;
1800
+ },
1765
1801
  material,
1766
1802
  uniforms,
1767
1803
  marchBudget: budget,
@@ -1893,6 +1929,7 @@ export function createCloudVolumeMaterial({
1893
1929
  },
1894
1930
 
1895
1931
  dispose() {
1932
+ sceneDepthFallback.dispose();
1896
1933
  cirrusFallback.dispose();
1897
1934
  material.dispose();
1898
1935
  },
@@ -1,3 +1,4 @@
1
+ import { createCloudTextureCache } from './textureCache.js';
1
2
  // Cloud base-shape volume — packed inverted-Worley FBMs.
2
3
  //
3
4
  // R inverted Worley FBM at 4 / 8 / 16 cells
@@ -10,8 +11,9 @@
10
11
  // the Perlin-Worley + separate erosion-volume path used by the discarded lab.
11
12
  //
12
13
  // The hash has no author seed, so it produces one canonical field. The public
13
- // seed remains in metadata/cache keys for document compatibility, but does not
14
- // perturb these bytes.
14
+ // seed remains in uncached bake metadata for document compatibility, but does
15
+ // not perturb these bytes. Cached textures use canonical seed 0 and share one
16
+ // texture and mip chain per resolution, independent of the authored seed.
15
17
  //
16
18
  // Every band is periodic over the unit tile and the texture wraps on all three
17
19
  // axes, so the field continues exactly across a repeat. Sampling is at texel
@@ -77,7 +79,7 @@ export const CLOUD_BASE_SHAPE_PLAN = Object.freeze({
77
79
  worleyTexelsPerCell: 1,
78
80
  });
79
81
 
80
- const volumeCache = new Map();
82
+ const volumeCache = createCloudTextureCache('base-shape');
81
83
 
82
84
  // Fixed u32 hash and cellular sampler. These stay local rather than using the
83
85
  // seeded noise utilities because the canonical base-shape bake must remain
@@ -307,7 +309,7 @@ export function createCloudBaseShapeVolume({
307
309
  }
308
310
 
309
311
  /**
310
- * Cached base-shape volume, keyed by resolution and seed. A tier switch that
312
+ * Cached canonical base-shape volume, keyed by resolution. A tier switch that
311
313
  * returns to a resolution already baked pays nothing, and the shadow/env-map
312
314
  * bakes share the runtime's volumes.
313
315
  */
@@ -316,10 +318,10 @@ export function getCloudBaseShapeVolume({
316
318
  seed = 1,
317
319
  } = {}) {
318
320
  const size = resolveNoiseDims(dims, CLOUD_BASE_SHAPE_MASTER_DIM);
319
- const key = noiseVolumeKey(size, hashSeed(seed));
321
+ const key = noiseVolumeKey(size, 0);
320
322
  const cached = volumeCache.get(key);
321
323
  if (cached) return cached;
322
- const texture = createCloudBaseShapeVolume({ dims: size, seed });
324
+ const texture = createCloudBaseShapeVolume({ dims: size, seed: 0 });
323
325
  volumeCache.set(key, texture);
324
326
  return texture;
325
327
  }
@@ -1,3 +1,4 @@
1
+ import { createCloudTextureCache } from './textureCache.js';
1
2
  // Procedural high-cloud mask for the thin cirrus deck.
2
3
  //
3
4
  // Cirrus is not a shallower version of the lower volumetric layer. Ice-crystal
@@ -17,7 +18,7 @@ export const CLOUD_CIRRUS_MAP_WIDTH = 1024;
17
18
  export const CLOUD_CIRRUS_MAP_HEIGHT = 512;
18
19
  export const CLOUD_CIRRUS_SEED_NAMESPACE = 'cloud-cirrus';
19
20
 
20
- const mapCache = new Map();
21
+ const mapCache = createCloudTextureCache('cirrus');
21
22
 
22
23
  function smoothstep01(value) {
23
24
  const x = value < 0 ? 0 : value > 1 ? 1 : value;
@@ -1,3 +1,4 @@
1
+ import { createCloudTextureCache } from './textureCache.js';
1
2
  // Curl noise — the divergence-free vector field that advects cloud edges into
2
3
  // wisps. Displacing a sample position by an arbitrary noise vector pumps
3
4
  // density in and out of the cloud (the field has sources and sinks), so edges
@@ -68,7 +69,7 @@ const NORMALIZATION_PROBE_DIMS = 32;
68
69
  // 15%, resolveNoiseDims' 256³ ceiling included.
69
70
  const NORMALIZATION_HEADROOM = 1.15;
70
71
 
71
- const volumeCache = new Map();
72
+ const volumeCache = createCloudTextureCache('curl');
72
73
 
73
74
  /**
74
75
  * Builds the divergence-free field.
@@ -1,3 +1,4 @@
1
+ import { createCloudTextureCache } from './textureCache.js';
1
2
  // Cloud erosion volume — the high-frequency Worley detail field that carves
2
3
  // the cloud silhouette after the base shape has decided where cloud exists.
3
4
  //
@@ -66,7 +67,7 @@ export const CLOUD_EROSION_PLAN = Object.freeze({
66
67
  texelsPerCell: 2,
67
68
  });
68
69
 
69
- const volumeCache = new Map();
70
+ const volumeCache = createCloudTextureCache('erosion');
70
71
 
71
72
  /**
72
73
  * Bakes the RGBA8 erosion field. Returned separately from the texture so
@@ -0,0 +1,77 @@
1
+ // Bound cache-owned retention without disposing textures still used by a
2
+ // renderer or host. Weak entries preserve identity for live consumers; the
3
+ // browser can reclaim unreferenced textures after they leave the recent set.
4
+ const recent = new Map();
5
+ const entries = new Map();
6
+ const MAX_BYTES = 64 * 1024 * 1024;
7
+ const MAX_ENTRIES = 32;
8
+ let retainedBytes = 0;
9
+ const finalized = new FinalizationRegistry(({ key, ref }) => {
10
+ if (entries.get(key)?.ref === ref) entries.delete(key);
11
+ });
12
+
13
+ function forget(key) {
14
+ const item = recent.get(key);
15
+ if (item) retainedBytes -= item.bytes;
16
+ recent.delete(key);
17
+ const entry = entries.get(key);
18
+ if (entry) finalized.unregister(entry.ref);
19
+ entries.delete(key);
20
+ }
21
+
22
+ function retain(key, texture, bytes) {
23
+ const previous = recent.get(key);
24
+ if (previous) retainedBytes -= previous.bytes;
25
+ recent.delete(key);
26
+ recent.set(key, { texture, bytes });
27
+ retainedBytes += bytes;
28
+ while (retainedBytes > MAX_BYTES || recent.size > MAX_ENTRIES) {
29
+ const oldest = recent.keys().next().value;
30
+ retainedBytes -= recent.get(oldest).bytes;
31
+ recent.delete(oldest);
32
+ }
33
+ }
34
+
35
+ export function getCloudTextureCacheStats() {
36
+ return { retainedBytes, retainedEntries: recent.size, maxBytes: MAX_BYTES, maxEntries: MAX_ENTRIES };
37
+ }
38
+
39
+ export function createCloudTextureCache(namespace) {
40
+ const prefix = `${namespace}:`;
41
+ return {
42
+ get(key) {
43
+ const id = prefix + key;
44
+ const entry = entries.get(id);
45
+ const texture = entry?.ref.deref();
46
+ if (!texture) { forget(id); return undefined; }
47
+ retain(id, texture, entry.bytes);
48
+ return texture;
49
+ },
50
+ set(key, texture) {
51
+ const id = prefix + key;
52
+ forget(id);
53
+ const levels = texture.userData?.toonlabVolumeMipChain?.levels;
54
+ const bytes = levels
55
+ ? levels.reduce((sum, data) => sum + data.byteLength, 0)
56
+ : (texture.image?.data?.byteLength ?? 0);
57
+ const ref = new WeakRef(texture);
58
+ entries.set(id, { ref, bytes });
59
+ finalized.register(texture, { key: id, ref }, ref);
60
+ texture.addEventListener('dispose', () => {
61
+ if (entries.get(id)?.ref === ref) forget(id);
62
+ });
63
+ retain(id, texture, bytes);
64
+ },
65
+ *values() {
66
+ for (const [key, entry] of entries) {
67
+ if (key.startsWith(prefix)) {
68
+ const texture = entry.ref.deref();
69
+ if (texture) yield texture;
70
+ }
71
+ }
72
+ },
73
+ clear() {
74
+ for (const key of entries.keys()) if (key.startsWith(prefix)) forget(key);
75
+ },
76
+ };
77
+ }
@@ -1,3 +1,4 @@
1
+ import { createCloudTextureCache } from './textureCache.js';
1
2
  // Weather map — the 2D field that decides *where* cloud exists, before any of
2
3
  // the 3D noise decides what shape it takes. CPU FBM, deterministic from a
3
4
  // seed, tiling in both axes, regenerated whenever the sky-cloud lab changes the
@@ -151,7 +152,7 @@ export const WEATHER_MAP_PROFILE_DEFAULTS = Object.freeze(
151
152
  ),
152
153
  );
153
154
 
154
- const mapCache = new Map();
155
+ const mapCache = createCloudTextureCache('weather');
155
156
 
156
157
  /**
157
158
  * Normalizes any partial input into a complete WeatherMapProfile: every field
@@ -0,0 +1,83 @@
1
+ const SHA256_INITIAL = Object.freeze([
2
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
3
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
4
+ ]);
5
+
6
+ const SHA256_ROUND = Object.freeze([
7
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
8
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
9
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
10
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
11
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
12
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
13
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
14
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
15
+ ]);
16
+
17
+ function rotateRight(value, amount) {
18
+ return (value >>> amount) | (value << (32 - amount));
19
+ }
20
+
21
+ function inputBytes(value) {
22
+ if (typeof value === 'string') return new TextEncoder().encode(value);
23
+ if (value instanceof Uint8Array) return value;
24
+ if (ArrayBuffer.isView(value)) {
25
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
26
+ }
27
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
28
+ throw new TypeError('sha256Hex requires a string, ArrayBuffer, or typed-array input.');
29
+ }
30
+
31
+ /** Synchronous browser-safe SHA-256 used for portable document identities. */
32
+ export function sha256Hex(value) {
33
+ const source = inputBytes(value);
34
+ const bitLength = BigInt(source.byteLength) * 8n;
35
+ const paddedLength = Math.ceil((source.byteLength + 9) / 64) * 64;
36
+ const bytes = new Uint8Array(paddedLength);
37
+ bytes.set(source);
38
+ bytes[source.byteLength] = 0x80;
39
+ const view = new DataView(bytes.buffer);
40
+ view.setUint32(paddedLength - 8, Number((bitLength >> 32n) & 0xffffffffn), false);
41
+ view.setUint32(paddedLength - 4, Number(bitLength & 0xffffffffn), false);
42
+
43
+ const state = [...SHA256_INITIAL];
44
+ const words = new Uint32Array(64);
45
+ for (let offset = 0; offset < paddedLength; offset += 64) {
46
+ for (let index = 0; index < 16; index += 1) {
47
+ words[index] = view.getUint32(offset + (index * 4), false);
48
+ }
49
+ for (let index = 16; index < 64; index += 1) {
50
+ const left = words[index - 15];
51
+ const right = words[index - 2];
52
+ const sigma0 = rotateRight(left, 7) ^ rotateRight(left, 18) ^ (left >>> 3);
53
+ const sigma1 = rotateRight(right, 17) ^ rotateRight(right, 19) ^ (right >>> 10);
54
+ words[index] = (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
55
+ }
56
+ let [a, b, c, d, e, f, g, h] = state;
57
+ for (let index = 0; index < 64; index += 1) {
58
+ const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
59
+ const choose = (e & f) ^ (~e & g);
60
+ const temporary1 = (h + sum1 + choose + SHA256_ROUND[index] + words[index]) >>> 0;
61
+ const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
62
+ const majority = (a & b) ^ (a & c) ^ (b & c);
63
+ const temporary2 = (sum0 + majority) >>> 0;
64
+ h = g;
65
+ g = f;
66
+ f = e;
67
+ e = (d + temporary1) >>> 0;
68
+ d = c;
69
+ c = b;
70
+ b = a;
71
+ a = (temporary1 + temporary2) >>> 0;
72
+ }
73
+ state[0] = (state[0] + a) >>> 0;
74
+ state[1] = (state[1] + b) >>> 0;
75
+ state[2] = (state[2] + c) >>> 0;
76
+ state[3] = (state[3] + d) >>> 0;
77
+ state[4] = (state[4] + e) >>> 0;
78
+ state[5] = (state[5] + f) >>> 0;
79
+ state[6] = (state[6] + g) >>> 0;
80
+ state[7] = (state[7] + h) >>> 0;
81
+ }
82
+ return state.map((word) => word.toString(16).padStart(8, '0')).join('');
83
+ }
@@ -239,6 +239,8 @@ export function createGroundShaderMaterial({
239
239
  uSceneGroundSnowDepth: uniform(0),
240
240
  uSceneGroundShadowDebug: uniform(0),
241
241
  uSceneGroundSunColor: uniform(new THREE.Color(0xfff0d2)),
242
+ uSceneGroundSunIntensity: uniform(1),
243
+ uSceneGroundSkyIntensity: uniform(1),
242
244
  uSceneGroundSunDirection: uniform(new THREE.Vector3(0.45, 0.75, 0.5).normalize()),
243
245
  uSceneGroundSkyColor: uniform(new THREE.Color(0xc7dcff)),
244
246
  uSceneGroundWaterLevel: uniform(-10000),
@@ -572,9 +574,12 @@ export function createGroundShaderMaterial({
572
574
  u('material', 'metalness'),
573
575
  );
574
576
  groundColor.addAssign(specularColor.mul(specularAmount));
575
- groundColor.addAssign(
576
- groundColor.mul(u('material', 'emissiveStrength')),
577
- );
577
+ const emission = groundColor.mul(u('material', 'emissiveStrength')).toVar();
578
+ // Defaults of one preserve the authored look. Scene intensity remains
579
+ // transient, and can reach zero without erasing intentional emission.
580
+ groundColor.mulAssign(mix(sceneUniforms.uSceneGroundSkyIntensity,
581
+ sceneUniforms.uSceneGroundSunIntensity, smoothstep(0.06, 0.72, direct)));
582
+ groundColor.addAssign(emission);
578
583
 
579
584
  groundColor.assign(mix(
580
585
  groundColor,
@@ -617,7 +622,9 @@ export function createGroundShaderMaterial({
617
622
  snowDepth,
618
623
  sunColor,
619
624
  sunDirection,
625
+ sunIntensity,
620
626
  skyColor,
627
+ skyIntensity,
621
628
  shadowDebug,
622
629
  waterLevel,
623
630
  wetness,
@@ -639,6 +646,8 @@ export function createGroundShaderMaterial({
639
646
  sceneUniforms.uSceneGroundShadowDebug.value = shadowDebug ? 1 : 0;
640
647
  }
641
648
  if (sunColor !== undefined) sceneUniforms.uSceneGroundSunColor.value.set(sunColor);
649
+ if (Number.isFinite(sunIntensity)) sceneUniforms.uSceneGroundSunIntensity.value = Math.max(0, sunIntensity);
650
+ if (Number.isFinite(skyIntensity)) sceneUniforms.uSceneGroundSkyIntensity.value = Math.max(0, skyIntensity);
642
651
  if (Array.isArray(sunDirection) && sunDirection.length >= 3) {
643
652
  sceneUniforms.uSceneGroundSunDirection.value.set(...sunDirection).normalize();
644
653
  } else if (sunDirection?.isVector3) {