@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.
- package/NPM-LIBRARY.md +87 -27
- package/README.md +11 -10
- package/agents/PROMPTS.md +17 -3
- package/agents/README.md +13 -3
- package/agents/claude/CLAUDE.md +3 -0
- package/agents/codex/AGENTS.md +112 -159
- package/agents/cursor/toonlab.mdc +7 -3
- package/agents/examples/game-foundation.mjs +104 -0
- package/agents/references/game-lifecycle.md +57 -0
- package/agents/references/runtime-entry-points.md +26 -0
- package/agents/references/style-bundles.md +4 -1
- package/agents/skills/claude/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/claude/environment/SKILL.md +4 -0
- package/agents/skills/claude/game-dev/SKILL.md +63 -87
- package/agents/skills/claude/lighting/SKILL.md +36 -0
- package/agents/skills/claude/post-processing/SKILL.md +8 -3
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/claude/rockgen/SKILL.md +52 -21
- package/agents/skills/claude/scene-style-application/SKILL.md +4 -0
- package/agents/skills/claude/style-presets/SKILL.md +11 -8
- package/agents/skills/claude/toon-shading/SKILL.md +4 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/claude/visual-verification/SKILL.md +45 -119
- package/agents/skills/claude/water/SKILL.md +6 -2
- package/agents/skills/codex/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/codex/environment/SKILL.md +4 -0
- package/agents/skills/codex/game-dev/SKILL.md +63 -87
- package/agents/skills/codex/lighting/SKILL.md +36 -0
- package/agents/skills/codex/post-processing/SKILL.md +8 -3
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/codex/rockgen/SKILL.md +52 -21
- package/agents/skills/codex/scene-style-application/SKILL.md +4 -0
- package/agents/skills/codex/style-presets/SKILL.md +11 -8
- package/agents/skills/codex/toon-shading/SKILL.md +4 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/codex/visual-verification/SKILL.md +45 -119
- package/agents/skills/codex/water/SKILL.md +6 -2
- package/database/apply-one-catalog-seed.mjs +63 -0
- package/database/providers.mjs +1 -0
- package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
- package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
- package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
- package/mcp/lab-management.mjs +11 -8
- package/mcp/public-catalog.mjs +0 -16
- package/mcp/tree-lab-contract.mjs +2381 -0
- package/mcp/vite-plugin.mjs +5 -1
- package/package.json +29 -18
- package/src/catalog/officialCatalog.js +1 -0
- package/src/catalog/officialCatalogAssetRuntime.js +98 -1
- package/src/catalog/officialCatalogLod.js +160 -16
- package/src/catalog/officialCatalogPlacement.js +52 -12
- package/src/catalog/officialCatalogProvider.js +79 -8
- package/src/catalog/officialCatalogRockPackage.js +172 -0
- package/src/cloud/cloudReprojection.js +40 -7
- package/src/cloud/cloudVolume.js +44 -7
- package/src/cloud/noise/baseShapeVolume.js +8 -6
- package/src/cloud/noise/cirrusMap.js +2 -1
- package/src/cloud/noise/curlNoise.js +2 -1
- package/src/cloud/noise/erosionVolume.js +2 -1
- package/src/cloud/noise/textureCache.js +77 -0
- package/src/cloud/noise/weatherMap.js +2 -1
- package/src/core/sha256.js +83 -0
- package/src/ground-shader/groundShaderMaterial.js +12 -3
- package/src/rock-shader/rockMaterial.js +205 -28
- package/src/rock-shader/rockShaderRuntime.js +17 -1
- package/src/rock-shader/rockShaderSettings.js +16 -0
- package/src/rockgen/index.js +9 -0
- package/src/rockgen/lod/index.js +1 -0
- package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
- package/src/rockgen/rockDocument.js +899 -35
- package/src/rockgen/surface/c7GeologySurface.js +39 -9
- package/src/rockgen/surface/naturalRockSurface.js +1948 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +14 -2
- package/src/shaders-tsl/chunks/water-foam.js +6 -6
- package/src/shaders-tsl/chunks/water-lighting.js +28 -5
- package/src/shaders-tsl/chunks/water-shore-state.js +5 -2
- package/src/shaders-tsl/chunks/water-waves.js +10 -5
- package/src/shaders-tsl/flower.js +7 -4
- package/src/shaders-tsl/grass.js +4 -3
- package/src/shaders-tsl/water-shore-state-simulation.js +3 -2
- package/src/shaders-tsl/water.js +209 -41
- package/src/sky/skySystem.js +30 -7
- package/src/toon/settings/perspectiveRemovalSettings.js +1 -1
- package/src/toon/toonSettings.js +74 -0
- package/src/vegetation/stylizedFlower.js +6 -0
- package/src/version.js +1 -1
- package/src/water/waterDetailSpectrum.js +91 -0
- package/src/water/waterDynamics.js +422 -0
- package/src/water/waterFoamParticles.js +196 -0
- package/src/water/waterFoamTexture.js +57 -0
- package/src/water/waterHydrodynamics.js +209 -0
- package/src/water/waterMaterial.js +1 -0
- package/src/water/waterRenderExtension.js +107 -0
- package/src/water/waterScenePasses.js +14 -6
- package/src/water/waterSettings.js +53 -26
- package/src/water/waterShoreMaterial.js +38 -14
- package/src/water/waterSpectralOcean.js +137 -0
- package/src/water/waterStageSettings.js +19 -1
- package/src/water/waterSurface.js +62 -18
- package/src/water/waterUnderwaterAtmosphere.js +30 -3
- package/types/catalog/officialCatalog.d.ts +1 -0
- package/types/catalog/officialCatalogPlacement.d.ts +4 -0
- package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
- package/types/cloud/noise/baseShapeVolume.d.ts +1 -1
- package/types/cloud/noise/textureCache.d.ts +12 -0
- package/types/core/sha256.d.ts +2 -0
- package/types/index.d.ts +30 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockShaderSettings.d.ts +2 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/lod/index.d.ts +1 -0
- package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
- package/types/rockgen/rockDocument.d.ts +654 -71
- package/types/rockgen/surface/naturalRockSurface.d.ts +100 -0
- package/types/shaders-tsl/chunks/projected-water-caustics.d.ts +7 -1
- package/types/shaders-tsl/chunks/water-lighting.d.ts +4 -3
- package/types/shaders-tsl/water.d.ts +2 -1
- package/types/version.d.ts +1 -1
- package/types/water/waterDetailSpectrum.d.ts +13 -0
- package/types/water/waterDynamics.d.ts +89 -0
- package/types/water/waterFoamParticles.d.ts +32 -0
- package/types/water/waterFoamTexture.d.ts +1 -0
- package/types/water/waterHydrodynamics.d.ts +49 -0
- package/types/water/waterRenderExtension.d.ts +16 -0
- package/types/water/waterScenePasses.d.ts +1 -1
- package/types/water/waterSettings.d.ts +1 -0
- package/types/water/waterSpectralOcean.d.ts +17 -0
- package/types/water/waterStageSettings.d.ts +1 -1
- 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
|
-
|
|
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
|
|
1107
|
+
return safeNormalizeDirection(transpose(TBNViewMatrix).mul(viewNormal));
|
|
999
1108
|
}
|
|
1000
1109
|
|
|
1001
1110
|
export function tangentNormalToView(tangentNormal) {
|
|
1002
|
-
return
|
|
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
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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 =
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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 =
|
|
1815
|
-
|
|
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.
|
|
1821
|
-
|
|
1822
|
-
|
|
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: '
|
|
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
|
package/src/rockgen/index.js
CHANGED
|
@@ -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';
|
package/src/rockgen/lod/index.js
CHANGED
|
@@ -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
|
+
}
|