@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
@@ -131,6 +131,8 @@ export const TOONLAB_ROCK_PROFILE_DEFAULTS = Object.freeze({
131
131
  distanceScale: 1,
132
132
  }),
133
133
  base: Object.freeze({
134
+ mode: 'triplanar',
135
+ upAxis: 'y',
134
136
  scale: 1,
135
137
  tint: Object.freeze([1, 1, 1]),
136
138
  saturation: 1,
@@ -335,6 +337,14 @@ function bool(value, fallback) {
335
337
  return fallback;
336
338
  }
337
339
 
340
+ function projectionMode(value, fallback = 'triplanar') {
341
+ return value === 'directional-bedding' || value === 'triplanar' ? value : fallback;
342
+ }
343
+
344
+ function projectionUpAxis(value, fallback = 'y') {
345
+ return value === 'x' || value === 'y' || value === 'z' ? value : fallback;
346
+ }
347
+
338
348
  function color3(value, fallback) {
339
349
  if (Array.isArray(value)) {
340
350
  return [
@@ -401,6 +411,8 @@ export function normalizeToonLabRockProfile(profile = {}) {
401
411
  ),
402
412
  },
403
413
  base: {
414
+ mode: projectionMode(base.mode, defaults.base.mode),
415
+ upAxis: projectionUpAxis(base.upAxis, defaults.base.upAxis),
404
416
  scale: finite(base.scale, defaults.base.scale),
405
417
  tint: color3(base.tint, defaults.base.tint),
406
418
  saturation: finite(base.saturation, defaults.base.saturation),
@@ -944,6 +956,87 @@ export function toonLabTriplanarColor(map, scale, blend = 1, coordinates = { zSi
944
956
  .add(mapNode.sample(projected.xy).rgb.mul(weights.z));
945
957
  }
946
958
 
959
+ const DIRECTIONAL_BEDDING_AXES = Object.freeze({
960
+ x: Object.freeze({
961
+ lateral: Object.freeze([
962
+ Object.freeze({ faceAxis: 'y', uAxis: 'z', uv: 'zx' }),
963
+ Object.freeze({ faceAxis: 'z', uAxis: 'y', uv: 'yx' }),
964
+ ]),
965
+ suppressedProjectionAxis: 'x',
966
+ upAxis: 'x',
967
+ }),
968
+ y: Object.freeze({
969
+ lateral: Object.freeze([
970
+ Object.freeze({ faceAxis: 'x', uAxis: 'z', uv: 'zy' }),
971
+ Object.freeze({ faceAxis: 'z', uAxis: 'x', uv: 'xy' }),
972
+ ]),
973
+ suppressedProjectionAxis: 'y',
974
+ upAxis: 'y',
975
+ }),
976
+ z: Object.freeze({
977
+ lateral: Object.freeze([
978
+ Object.freeze({ faceAxis: 'x', uAxis: 'y', uv: 'yz' }),
979
+ Object.freeze({ faceAxis: 'y', uAxis: 'x', uv: 'xz' }),
980
+ ]),
981
+ suppressedProjectionAxis: 'z',
982
+ upAxis: 'z',
983
+ }),
984
+ });
985
+
986
+ /**
987
+ * Describes the compile-time directional-bedding graph. The returned data is
988
+ * deliberately plain CPU data so authoring tools and focused verifiers can
989
+ * prove which axis is suppressed without compiling a WebGPU pipeline.
990
+ */
991
+ export function describeToonLabDirectionalBeddingProjection(upAxis = 'y') {
992
+ const resolved = projectionUpAxis(upAxis, 'y');
993
+ const descriptor = DIRECTIONAL_BEDDING_AXES[resolved];
994
+ return {
995
+ lateral: descriptor.lateral.map((entry) => ({ ...entry })),
996
+ suppressedProjectionAxis: descriptor.suppressedProjectionAxis,
997
+ upAxis: descriptor.upAxis,
998
+ };
999
+ }
1000
+
1001
+ function directionalBeddingState(scale, blend, coordinates, upAxis) {
1002
+ const descriptor = DIRECTIONAL_BEDDING_AXES[projectionUpAxis(upAxis, 'y')];
1003
+ const projected = toonLabSourcePosition(coordinates).div(safeScale(scale));
1004
+ const geometryNormal = toonLabSourceGeometryNormal(coordinates);
1005
+ const exponent = float(Math.max(finite(blend, 1), 0.000001));
1006
+ const upWeight = pow(
1007
+ max(abs(geometryNormal[descriptor.upAxis]), TOONLAB_FLOAT_EPSILON),
1008
+ exponent,
1009
+ );
1010
+ const lateralWeights = descriptor.lateral.map(({ faceAxis }) => pow(
1011
+ max(abs(geometryNormal[faceAxis]), TOONLAB_FLOAT_EPSILON),
1012
+ exponent,
1013
+ ).add(upWeight.mul(0.5)));
1014
+ const denominator = max(lateralWeights[0].add(lateralWeights[1]), 0.000001);
1015
+ return {
1016
+ descriptor,
1017
+ geometryNormal,
1018
+ projected,
1019
+ weights: lateralWeights.map((weight) => weight.div(denominator)),
1020
+ };
1021
+ }
1022
+
1023
+ export function toonLabDirectionalBeddingColor(
1024
+ map,
1025
+ scale,
1026
+ blend = 1,
1027
+ coordinates = { zSign: 1 },
1028
+ upAxis = 'y',
1029
+ ) {
1030
+ const mapNode = texture(map);
1031
+ const state = directionalBeddingState(scale, blend, coordinates, upAxis);
1032
+ const sample = ({ uv: swizzle }) => mapNode.sample(vec2(
1033
+ state.projected[swizzle[0]],
1034
+ state.projected[swizzle[1]],
1035
+ )).rgb;
1036
+ return sample(state.descriptor.lateral[0]).mul(state.weights[0])
1037
+ .add(sample(state.descriptor.lateral[1]).mul(state.weights[1]));
1038
+ }
1039
+
947
1040
  function toonLabSideProjection(map, scale, projectionContrast, {
948
1041
  negativeScale = true,
949
1042
  clampResult = true,
@@ -966,7 +1059,18 @@ function toonLabSideProjection(map, scale, projectionContrast, {
966
1059
  }
967
1060
 
968
1061
  function toonLabRockProjection(map, profile) {
969
- return profile.base.sideOnly
1062
+ // Directional bedding has explicit precedence. `sideOnly` remains the exact
1063
+ // legacy graph for old presets, but cannot re-enable the suppressed top-axis
1064
+ // sample on a bedded C8 surface.
1065
+ return profile.base.mode === 'directional-bedding'
1066
+ ? toonLabDirectionalBeddingColor(
1067
+ map,
1068
+ profile.base.scale,
1069
+ profile.base.projectionContrast,
1070
+ profile.coordinates,
1071
+ profile.base.upAxis,
1072
+ )
1073
+ : profile.base.sideOnly
970
1074
  ? toonLabSideProjection(map, profile.base.scale, profile.base.projectionContrast, {
971
1075
  coordinates: profile.coordinates,
972
1076
  })
@@ -993,13 +1097,18 @@ function normalGreenSignForTexture(map, profileSign) {
993
1097
  return finite(profileSign, 1) * importerSign;
994
1098
  }
995
1099
 
1100
+ function safeNormalizeDirection(direction, fallback = vec3(0, 0, 1)) {
1101
+ const valid = step(float(1e-12), dot(direction, direction));
1102
+ return normalize(mix(fallback, direction, valid));
1103
+ }
1104
+
996
1105
  function worldNormalToTangent(worldNormal) {
997
1106
  const viewNormal = transformNormalByViewMatrix(worldNormal, cameraViewMatrix);
998
- return normalize(transpose(TBNViewMatrix).mul(viewNormal));
1107
+ return safeNormalizeDirection(transpose(TBNViewMatrix).mul(viewNormal));
999
1108
  }
1000
1109
 
1001
1110
  export function tangentNormalToView(tangentNormal) {
1002
- return normalize(TBNViewMatrix.mul(tangentNormal));
1111
+ return safeNormalizeDirection(TBNViewMatrix.mul(tangentNormal));
1003
1112
  }
1004
1113
 
1005
1114
  export function toonLabTriplanarNormal(map, scale, blend, greenSign, coordinates) {
@@ -1027,6 +1136,52 @@ export function toonLabTriplanarNormal(map, scale, blend, greenSign, coordinates
1027
1136
  return worldNormalToTangent(worldNormal);
1028
1137
  }
1029
1138
 
1139
+ function directionalBeddingNormalCandidate(decoded, geometryNormal, entry, upAxis) {
1140
+ const components = {
1141
+ x: geometryNormal.x,
1142
+ y: geometryNormal.y,
1143
+ z: geometryNormal.z,
1144
+ };
1145
+ components[entry.faceAxis] = abs(decoded.z).mul(geometryNormal[entry.faceAxis]);
1146
+ components[entry.uAxis] = decoded.x.add(geometryNormal[entry.uAxis]);
1147
+ components[upAxis] = decoded.y.add(geometryNormal[upAxis]);
1148
+ return vec3(components.x, components.y, components.z);
1149
+ }
1150
+
1151
+ export function toonLabDirectionalBeddingNormal(
1152
+ map,
1153
+ scale,
1154
+ blend,
1155
+ greenSign,
1156
+ coordinates,
1157
+ upAxis = 'y',
1158
+ ) {
1159
+ const state = directionalBeddingState(scale, blend, coordinates, upAxis);
1160
+ const mapNode = texture(map);
1161
+ const candidates = state.descriptor.lateral.map((entry) => {
1162
+ const decoded = decodeToonLabNormal(mapNode.sample(vec2(
1163
+ state.projected[entry.uv[0]],
1164
+ state.projected[entry.uv[1]],
1165
+ )).rgb, greenSign);
1166
+ return directionalBeddingNormalCandidate(
1167
+ decoded,
1168
+ state.geometryNormal,
1169
+ entry,
1170
+ state.descriptor.upAxis,
1171
+ );
1172
+ });
1173
+ const sourceWorldNormal = safeNormalizeDirection(
1174
+ candidates[0].mul(state.weights[0]).add(candidates[1].mul(state.weights[1])),
1175
+ state.geometryNormal,
1176
+ );
1177
+ const worldNormal = safeNormalizeDirection(vec3(
1178
+ sourceWorldNormal.x,
1179
+ sourceWorldNormal.y,
1180
+ sourceWorldNormal.z.mul(coordinates.zSign),
1181
+ ), state.geometryNormal);
1182
+ return worldNormalToTangent(worldNormal);
1183
+ }
1184
+
1030
1185
  function toonLabNormalBlend(a, b) {
1031
1186
  // S_Rock's Normal Blend node uses BlendMode.Default, not RNM.
1032
1187
  return normalize(vec3(a.xy.add(b.xy), a.z.mul(b.z)));
@@ -1286,12 +1441,14 @@ export function createToonRockMaterial({
1286
1441
  // The macro projection carries the authored silhouette-scale color breakup;
1287
1442
  // a second metre-scale octave preserves readable rock surface detail close
1288
1443
  // to the camera without changing that macro identity.
1289
- const nearDetailSample = toonLabTriplanarColor(
1290
- textures.rock,
1291
- resolvedProfile.base.nearDetailScale,
1292
- resolvedProfile.base.projectionContrast,
1293
- resolvedProfile.coordinates,
1294
- );
1444
+ const nearDetailProfile = {
1445
+ ...resolvedProfile,
1446
+ base: {
1447
+ ...resolvedProfile.base,
1448
+ scale: resolvedProfile.base.nearDetailScale,
1449
+ },
1450
+ };
1451
+ const nearDetailSample = toonLabRockProjection(textures.rock, nearDetailProfile);
1295
1452
  const nearDetailValue = dot(nearDetailSample, vec3(0.2126, 0.7152, 0.0722));
1296
1453
  const nearDetailFade = clamp(
1297
1454
  radialDistance.div(resolvedProfile.base.nearDetailDistance),
@@ -1320,16 +1477,24 @@ export function createToonRockMaterial({
1320
1477
  );
1321
1478
 
1322
1479
  if (resolvedProfile.base.striping.enabled) {
1323
- const stripeProjected = toonLabSideProjection(
1324
- textures.stripe,
1325
- resolvedProfile.base.striping.scale,
1326
- resolvedProfile.base.projectionContrast,
1327
- {
1328
- negativeScale: false,
1329
- clampResult: false,
1330
- coordinates: resolvedProfile.coordinates,
1331
- },
1332
- );
1480
+ const stripeProjected = resolvedProfile.base.mode === 'directional-bedding'
1481
+ ? toonLabDirectionalBeddingColor(
1482
+ textures.stripe,
1483
+ resolvedProfile.base.striping.scale,
1484
+ resolvedProfile.base.projectionContrast,
1485
+ resolvedProfile.coordinates,
1486
+ resolvedProfile.base.upAxis,
1487
+ )
1488
+ : toonLabSideProjection(
1489
+ textures.stripe,
1490
+ resolvedProfile.base.striping.scale,
1491
+ resolvedProfile.base.projectionContrast,
1492
+ {
1493
+ negativeScale: false,
1494
+ clampResult: false,
1495
+ coordinates: resolvedProfile.coordinates,
1496
+ },
1497
+ );
1333
1498
  const stripeOpacity = clamp(
1334
1499
  toonLabContrast(stripeProjected.r, resolvedProfile.base.striping.contrast),
1335
1500
  0,
@@ -1811,16 +1976,28 @@ export function createToonRockMaterial({
1811
1976
  : vec3(0, 0, 1);
1812
1977
  let combinedNormal = stylizedNormal;
1813
1978
  if (textures.rockNormal) {
1814
- const crackNormal = toonLabTriplanarNormal(
1815
- textures.rockNormal,
1816
- resolvedProfile.base.scale,
1817
- resolvedProfile.base.projectionContrast,
1818
- normalGreenSignForTexture(
1979
+ const crackNormal = resolvedProfile.base.mode === 'directional-bedding'
1980
+ ? toonLabDirectionalBeddingNormal(
1819
1981
  textures.rockNormal,
1820
- resolvedProfile.normals.normalGreenSign,
1821
- ),
1822
- resolvedProfile.coordinates,
1823
- );
1982
+ resolvedProfile.base.scale,
1983
+ resolvedProfile.base.projectionContrast,
1984
+ normalGreenSignForTexture(
1985
+ textures.rockNormal,
1986
+ resolvedProfile.normals.normalGreenSign,
1987
+ ),
1988
+ resolvedProfile.coordinates,
1989
+ resolvedProfile.base.upAxis,
1990
+ )
1991
+ : toonLabTriplanarNormal(
1992
+ textures.rockNormal,
1993
+ resolvedProfile.base.scale,
1994
+ resolvedProfile.base.projectionContrast,
1995
+ normalGreenSignForTexture(
1996
+ textures.rockNormal,
1997
+ resolvedProfile.normals.normalGreenSign,
1998
+ ),
1999
+ resolvedProfile.coordinates,
2000
+ );
1824
2001
  const normalFade = clamp(
1825
2002
  radialDistance.div(Math.max(
1826
2003
  resolvedProfile.normals.distance * authoredDistanceScale,
@@ -226,6 +226,8 @@ export function rockShaderSettingsToProfile(input = {}) {
226
226
 
227
227
  return normalizeToonLabRockProfile({
228
228
  base: {
229
+ mode: projection.mode,
230
+ upAxis: projection.upAxis,
229
231
  scale: projection.scale,
230
232
  projectionContrast: projection.projectionContrast,
231
233
  sideOnly: projection.sideOnly,
@@ -373,6 +375,11 @@ function createMaterialForSource({
373
375
  materialProfile.shoreline = structuredClone(profile.shoreline);
374
376
  materialProfile.base = {
375
377
  ...materialProfile.base,
378
+ // Projection choice belongs to the edited C8 surface package/runtime
379
+ // binding, not to a shared semantic material set. These strings select
380
+ // a CPU-authored TSL graph; they are deliberately not shader uniforms.
381
+ mode: profile.base.mode,
382
+ upAxis: profile.base.upAxis,
376
383
  closeTintDistance: profile.base.closeTintDistance,
377
384
  distantTint: profile.base.distantTint,
378
385
  distantTintMix: profile.base.distantTintMix,
@@ -529,6 +536,12 @@ function createMaterialForSource({
529
536
  },
530
537
  sourceTextureCount: rockMaterial.userData.toonlabSourceTextureIds.length,
531
538
  };
539
+ rockMaterial.userData.toonlabRockProjectionContract = {
540
+ mapRoles: ['BaseColor', 'NearDetail', 'Smoothness', 'NormalGL', 'AO'],
541
+ mode: materialProfile.base.mode,
542
+ sideOnly: materialProfile.base.sideOnly,
543
+ upAxis: materialProfile.base.upAxis,
544
+ };
532
545
  if (semantic) {
533
546
  rockMaterial.userData.toonLabRockTextureComposition.materialSetId = semantic.materialSetId;
534
547
  rockMaterial.userData.toonLabRockTextureComposition.surfaceState = surfaceState;
@@ -560,8 +573,11 @@ function createMaterialForSource({
560
573
  : materialProfile.layers?.[activeLayer]?.roughness ?? null,
561
574
  };
562
575
  rockMaterial.userData.toonlabRockProjectionContract = {
576
+ ...rockMaterial.userData.toonlabRockProjectionContract,
563
577
  normalBlend: 'whiteout-world-to-tangent',
564
- primary: 'world-space-triplanar',
578
+ primary: materialProfile.base.mode === 'directional-bedding'
579
+ ? 'world-space-directional-bedding'
580
+ : 'world-space-triplanar',
565
581
  scaleCompensation: 'absolute-world-position',
566
582
  texelDensityInvariantUnderNonUniformScale: true,
567
583
  };
@@ -34,6 +34,20 @@ const GROUP_DEFINITIONS = Object.freeze({
34
34
  description: 'World-space base projection and graphic texture treatment.',
35
35
  label: 'Base Projection',
36
36
  fields: Object.freeze({
37
+ mode: field({
38
+ defaultValue: 'triplanar',
39
+ description: 'Compile-time texture graph: ordinary three-axis projection or coherent bedding that excludes the top-axis structural sample.',
40
+ label: 'Projection Mode',
41
+ options: ['triplanar', 'directional-bedding'],
42
+ type: 'select',
43
+ }),
44
+ upAxis: field({
45
+ defaultValue: 'y',
46
+ description: 'World-space geological up axis used by directional bedding.',
47
+ label: 'Bedding Up Axis',
48
+ options: ['x', 'y', 'z'],
49
+ type: 'select',
50
+ }),
37
51
  scale: field({
38
52
  defaultValue: 1.6,
39
53
  description: 'World-space size of the projected rock texture in meters.',
@@ -792,6 +806,8 @@ export const DEFAULT_ROCK_SHADER_SETTINGS = Object.freeze(Object.fromEntries(
792
806
  // visible, serializable, and independent from any preview fixture.
793
807
  export const CALL_ME_SENSEI_ROCK_SHADER_SETTINGS = Object.freeze({
794
808
  projection: Object.freeze({
809
+ mode: 'triplanar',
810
+ upAxis: 'y',
795
811
  scale: 48,
796
812
  // Geological maps already own lithology color and value structure. The
797
813
  // previous 0.72 saturation/contrast pair washed every family toward the
@@ -14,3 +14,12 @@ export * from './mesh/meshDocument.js';
14
14
  export * from './lod/index.js';
15
15
  export * from './export/glbExport.js';
16
16
  export * from './surface/c7GeologySurface.js';
17
+
18
+ // Public names for the realistic material library; stored schema IDs stay compatible.
19
+ export {
20
+ C8_FIRST12_LITHOLOGY_PROFILES as NATURAL_ROCK_SURFACE_PROFILES,
21
+ C8_FIRST12_MAP_ROLES as NATURAL_ROCK_MAP_ROLES,
22
+ createC8First12GeologyMapData as createNaturalRockMapData,
23
+ createC8First12SurfaceSpecification as createNaturalRockSurfaceSpecification,
24
+ resolveC8First12Projection as resolveNaturalRockProjection,
25
+ } from './surface/naturalRockSurface.js';
@@ -1,4 +1,5 @@
1
1
  export * from './rockLodMetrics.js';
2
+ export * from './rockDenseFieldPolicy.js';
2
3
  export * from './rockLodPlanner.js';
3
4
  export * from './rockLodPolicy.js';
4
5
  export * from './rockLodRuntime.js';
@@ -0,0 +1,143 @@
1
+ import {
2
+ collectCatalogLodBindings,
3
+ createCatalogLodRuntime,
4
+ } from '../../catalog/officialCatalogLod.js';
5
+
6
+ export const DENSE_FIELD_ROCK_LOD_LEVELS = Object.freeze([
7
+ Object.freeze({ id: 'LOD0', level: 0, purpose: 'hero-close' }),
8
+ Object.freeze({ id: 'LOD1', level: 1, purpose: 'near-gameplay' }),
9
+ Object.freeze({ id: 'LOD2', level: 2, purpose: 'mid-gameplay' }),
10
+ Object.freeze({ id: 'LOD3', level: 3, purpose: 'far-simplified' }),
11
+ Object.freeze({ id: 'LOD4', level: 4, purpose: 'very-far-silhouette' }),
12
+ ]);
13
+
14
+ export const DENSE_FIELD_ROCK_PIXEL_THRESHOLDS = Object.freeze([240, 110, 48, 16, 6]);
15
+ export const DENSE_FIELD_ROCK_CULL_BELOW_PIXELS = 5;
16
+
17
+ export const DENSE_FIELD_ROCK_TRIANGLE_CEILINGS = Object.freeze({
18
+ ordinary: Object.freeze({ LOD3: 1200, LOD4: 350 }),
19
+ formation: Object.freeze({ LOD3: 1800, LOD4: 600 }),
20
+ landmark: Object.freeze({ LOD3: 2500, LOD4: 800 }),
21
+ });
22
+
23
+ export const DENSE_FIELD_ROCK_PRODUCTION_RULE = Object.freeze({
24
+ cullBelowPixels: DENSE_FIELD_ROCK_CULL_BELOW_PIXELS,
25
+ farShadowLastLevel: 2,
26
+ levelCount: 5,
27
+ levels: DENSE_FIELD_ROCK_LOD_LEVELS,
28
+ pixelThresholds: DENSE_FIELD_ROCK_PIXEL_THRESHOLDS,
29
+ selectionBasis: 'projected-object-diameter-pixels',
30
+ triangleCeilings: DENSE_FIELD_ROCK_TRIANGLE_CEILINGS,
31
+ veryFar: Object.freeze({
32
+ allowedSurfaceChannels: Object.freeze(['baseColor', 'broadLighting']),
33
+ castShadow: false,
34
+ preserve: Object.freeze(['outerSilhouette', 'primaryMass', 'groundContact', 'broadBaseColor']),
35
+ remove: Object.freeze([
36
+ 'cracks',
37
+ 'strataRelief',
38
+ 'cavities',
39
+ 'secondaryLedges',
40
+ 'normalMap',
41
+ 'heightMap',
42
+ 'aoMap',
43
+ 'uniqueRoughness',
44
+ ]),
45
+ silhouetteOnly: true,
46
+ }),
47
+ });
48
+
49
+ export function inferDenseFieldRockClass({ scaleClass = null, targetHeightMetres = null } = {}) {
50
+ const named = String(scaleClass ?? '').trim().toLowerCase();
51
+ if (named === 'landmark' || named === 'hero' || named === 'backdrop') return 'landmark';
52
+ if (named === 'formation' || named === 'outcrop' || named === 'cliff') return 'formation';
53
+ if (named === 'ordinary' || named === 'boulder' || named === 'debris') return 'ordinary';
54
+ const height = Number(targetHeightMetres);
55
+ if (Number.isFinite(height) && height > 6) return 'landmark';
56
+ if (Number.isFinite(height) && height > 2) return 'formation';
57
+ return 'ordinary';
58
+ }
59
+
60
+ export function validateDenseFieldRockLodTargets(targets, {
61
+ assetClass = 'ordinary',
62
+ } = {}) {
63
+ const classification = Object.hasOwn(DENSE_FIELD_ROCK_TRIANGLE_CEILINGS, assetClass)
64
+ ? assetClass
65
+ : 'ordinary';
66
+ const ceilings = DENSE_FIELD_ROCK_TRIANGLE_CEILINGS[classification];
67
+ const values = DENSE_FIELD_ROCK_LOD_LEVELS.map(({ id }) => Number(targets?.[id]));
68
+ const errors = [];
69
+ values.forEach((value, level) => {
70
+ if (!Number.isInteger(value) || value <= 0) {
71
+ errors.push(`${DENSE_FIELD_ROCK_LOD_LEVELS[level].id} must be a positive integer`);
72
+ }
73
+ });
74
+ for (let level = 1; level < values.length; level += 1) {
75
+ if (Number.isFinite(values[level]) && Number.isFinite(values[level - 1])
76
+ && values[level] >= values[level - 1]) {
77
+ errors.push(`LOD${level} must contain fewer triangles than LOD${level - 1}`);
78
+ }
79
+ }
80
+ if (Number.isFinite(values[3]) && values[3] > ceilings.LOD3) {
81
+ errors.push(`LOD3 exceeds the ${classification} ${ceilings.LOD3}-triangle ceiling`);
82
+ }
83
+ if (Number.isFinite(values[4]) && values[4] > ceilings.LOD4) {
84
+ errors.push(`LOD4 exceeds the ${classification} ${ceilings.LOD4}-triangle ceiling`);
85
+ }
86
+ return Object.freeze({
87
+ assetClass: classification,
88
+ ceilings,
89
+ errors: Object.freeze(errors),
90
+ valid: errors.length === 0,
91
+ values: Object.freeze(values),
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Install the mandatory dense-field rock behavior on an authored LOD0-LOD4
97
+ * root. The final tier is geometry-only silhouette support; it never casts a
98
+ * shadow and the root is culled once its projected diameter drops below the
99
+ * production pixel threshold.
100
+ */
101
+ export function createDenseFieldRockLodRuntime(root, {
102
+ cullBelowPixels = DENSE_FIELD_ROCK_CULL_BELOW_PIXELS,
103
+ hysteresis = 0.12,
104
+ maxLevel = Number.POSITIVE_INFINITY,
105
+ pixelThresholds = DENSE_FIELD_ROCK_PIXEL_THRESHOLDS,
106
+ referenceDiameter = null,
107
+ requireComplete = true,
108
+ } = {}) {
109
+ const bindings = collectCatalogLodBindings(root);
110
+ const levels = [...new Set(bindings.map(({ level }) => level))].sort((a, b) => a - b);
111
+ const missing = DENSE_FIELD_ROCK_LOD_LEVELS
112
+ .map(({ level }) => level)
113
+ .filter((level) => !levels.includes(level));
114
+ if (requireComplete && missing.length > 0) {
115
+ throw new Error(`Dense-field rock is missing required visual tiers: ${missing.map((level) => `LOD${level}`).join(', ')}`);
116
+ }
117
+
118
+ const originalShadows = new Map(bindings.map(({ mesh }) => [mesh, mesh.castShadow]));
119
+ for (const { level, mesh } of bindings) {
120
+ if (level > DENSE_FIELD_ROCK_PRODUCTION_RULE.farShadowLastLevel) mesh.castShadow = false;
121
+ mesh.userData.toonlabDenseFieldRock = Object.freeze({
122
+ castShadow: mesh.castShadow,
123
+ silhouetteOnly: level === 4,
124
+ visualLevel: level,
125
+ });
126
+ }
127
+
128
+ const runtime = createCatalogLodRuntime(root, {
129
+ cullBelowPixels,
130
+ hysteresis,
131
+ maxLevel,
132
+ pixelThresholds,
133
+ referenceDiameter,
134
+ });
135
+ return Object.freeze({
136
+ ...runtime,
137
+ policy: DENSE_FIELD_ROCK_PRODUCTION_RULE,
138
+ dispose() {
139
+ runtime.dispose();
140
+ for (const [mesh, castShadow] of originalShadows) mesh.castShadow = castShadow;
141
+ },
142
+ });
143
+ }