@call-me-sensei/toonlab 0.4.19 → 0.4.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM-LIBRARY.md +230 -0
- package/README.md +78 -36
- package/agents/PROMPTS.md +91 -0
- package/agents/README.md +9 -14
- package/agents/claude/CLAUDE.md +8 -0
- package/agents/codex/AGENTS.md +161 -0
- package/agents/cursor/toonlab.mdc +37 -0
- package/agents/references/mcp-asset-discovery.md +12 -9
- package/agents/references/runtime-entry-points.md +5 -4
- package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
- package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
- package/cli/toonlab.mjs +3 -2
- package/database/fal-patina.mjs +131 -0
- package/database/generation-service.mjs +108 -7
- package/database/migrations/0007_creation_revisions.sql +80 -0
- package/database/migrations/0008_creation_revision_hardening.sql +98 -0
- package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
- package/database/migrations/0010_external_catalog_assets.sql +28 -0
- package/database/providers.mjs +195 -16
- package/database/repository.mjs +518 -70
- package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
- package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
- package/mcp/public-catalog.mjs +4 -52
- package/mcp/server.mjs +175 -12
- package/mcp/vite-plugin.mjs +78 -2
- package/package.json +32 -12
- package/scripts/generate-catalog-seed.mjs +34 -7
- package/src/asset-policy/catalogLicenses.js +64 -6
- package/src/assetlib/loadImported.js +15 -1
- package/src/catalog/officialCatalogAssetRuntime.js +13 -2
- package/src/catalog/officialCatalogLod.js +34 -1
- package/src/catalog/officialCatalogPlacement.js +29 -14
- package/src/character/animationRetarget.js +2 -0
- package/src/character/characterRig.js +6 -1
- package/src/character/characterRuntime.js +175 -7
- package/src/environment/environmentMaterialAdapter.js +30 -0
- package/src/environment/environmentPresets.js +90 -0
- package/src/environment/environmentSunShadowPass.js +42 -0
- package/src/environment/toonLabSurfaceLighting.js +108 -10
- package/src/environment/urbanPropMaterial.js +104 -2
- package/src/lighting/lightingSystem.js +65 -10
- package/src/renderer/index.js +7 -0
- package/src/renderer/styleComparison.js +985 -0
- package/src/rock-shader/index.js +5 -0
- package/src/rock-shader/rockGeometryDetail.js +701 -0
- package/src/rock-shader/rockMaterial.js +782 -39
- package/src/rock-shader/rockRegionRuntime.js +185 -0
- package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
- package/src/rock-shader/rockShaderRuntime.js +354 -67
- package/src/rock-shader/rockShaderSettings.js +246 -16
- package/src/rock-shader/rockTangentIntegrity.js +136 -0
- package/src/rock-shader/rockTextureIntegrity.js +115 -0
- package/src/rockgen/index.js +1 -0
- package/src/rockgen/rockDocument.js +27 -3
- package/src/rockgen/surface/c7GeologySurface.js +372 -0
- package/src/shaders-tsl/water.js +8 -0
- package/src/sky/cloudShadow.js +25 -1
- package/src/sky/skySystem.js +9 -0
- package/src/styles/index.js +23 -0
- package/src/styles/neutralStylePresets.js +712 -0
- package/src/styles/sceneStyleRuntime.js +51 -4
- package/src/styles/styleAdapters.js +13 -2
- package/src/styles/styleBundle.js +1 -1
- package/src/texgen/evaluateTexture.js +30 -3
- package/src/texgen/textureGenerators.js +63 -31
- package/src/texgen/textureSettings.js +25 -8
- package/src/toon/toonSettings.js +96 -2
- package/src/vegetation/branchTree.js +327 -42
- package/src/vegetation/index.js +24 -2
- package/src/vegetation/scatter.js +372 -1
- package/src/vegetation/stylizedTree.js +312 -126
- package/src/vegetation/stylizedTreeFoliage.js +229 -7
- package/src/vegetation/treeSurfaceTextures.js +328 -0
- package/src/version.js +1 -1
- package/src/water/waterSettings.js +15 -7
- package/types/asset-policy/catalogLicenses.d.ts +2 -0
- package/types/asset-policy/index.d.ts +2 -0
- package/types/assetlib/loadImported.d.ts +3 -1
- package/types/catalog/officialCatalogLod.d.ts +63 -5
- package/types/character/characterRig.d.ts +59 -0
- package/types/character/characterRuntime.d.ts +1 -0
- package/types/environment/environmentMaterialAdapter.d.ts +3 -1
- package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
- package/types/index.d.ts +89 -7
- package/types/lighting/lightingSystem.d.ts +7 -0
- package/types/react/index.d.ts +20 -0
- package/types/renderer/index.d.ts +1 -0
- package/types/renderer/styleComparison.d.ts +409 -0
- package/types/rock-shader/index.d.ts +5 -0
- package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
- package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
- package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
- package/types/rock-shader/rockShaderSettings.d.ts +19 -5
- package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
- package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/rockDocument.d.ts +29 -1
- package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
- package/types/sky/cloudShadow.d.ts +1 -0
- package/types/styles/index.d.ts +1 -0
- package/types/styles/neutralStylePresets.d.ts +944 -0
- package/types/styles/sceneStyleRuntime.d.ts +20 -0
- package/types/vegetation/branchTree.d.ts +4 -0
- package/types/vegetation/index.d.ts +9 -7
- package/types/vegetation/scatter.d.ts +1 -0
- package/types/vegetation/stylizedTree.d.ts +1 -2
- package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
- package/types/version.d.ts +1 -1
- package/agents/references/geology-playbook.md +0 -118
- package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
- package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
- package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/src/vegetation/legacyTreePresets.js +0 -120
- package/types/vegetation/legacyTreePresets.d.ts +0 -22
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
// Neutral style presets — the un-stylized counterpart of every style slot.
|
|
2
|
+
//
|
|
3
|
+
// A before/after comparison is only honest if the "before" is a real,
|
|
4
|
+
// defensible baseline. Before this module, `rock` was the only domain that
|
|
5
|
+
// shipped a `neutral` preset, so a whole-scene "here is what ToonLab does to
|
|
6
|
+
// your scene" claim had nothing to compare against for toon, environment,
|
|
7
|
+
// manufactured surface, tree, grass, ground, water, sky or cloud.
|
|
8
|
+
//
|
|
9
|
+
// WHAT NEUTRAL MEANS HERE
|
|
10
|
+
//
|
|
11
|
+
// Neutral is un-stylized STANDARD PBR, not "off". Source albedo, normal,
|
|
12
|
+
// roughness, metalness and AO maps stay bound at full strength; real shadows,
|
|
13
|
+
// real specular and real transmission stay on. What is removed is the
|
|
14
|
+
// stylization layered over them: cel banding, outlines, rim and edge ink,
|
|
15
|
+
// colour lifts, hue shifts, palette tints, aerial-perspective tinting,
|
|
16
|
+
// graphic projection and painted wash.
|
|
17
|
+
//
|
|
18
|
+
// A neutral preset therefore changes SHADING ONLY. It never changes geometry,
|
|
19
|
+
// density, placement, tiling, motion, wind, or the masks that decide which
|
|
20
|
+
// material goes where — because a comparison in which the two halves have
|
|
21
|
+
// different geometry is exactly the thing this pass exists to eliminate.
|
|
22
|
+
// `auditNeutralStyleShadingOnly()` asserts that property mechanically.
|
|
23
|
+
//
|
|
24
|
+
// SYMMETRY
|
|
25
|
+
//
|
|
26
|
+
// Every entry is generated from the same table against the same slot ids as
|
|
27
|
+
// `CALL_ME_SENSEI_STYLE_SLOT_IDS`, so `NEUTRAL_STYLE_BUNDLE` and
|
|
28
|
+
// `CALL_ME_SENSEI_STYLE_BUNDLE` are guaranteed to name the same slots. Two
|
|
29
|
+
// slots are deliberately NOT paired, and `describeNeutralStyleCoverage()`
|
|
30
|
+
// reports why rather than hiding it — see `abSafe: false` below.
|
|
31
|
+
|
|
32
|
+
import { createEnvironmentSettings } from '../environment/environmentMaterialAdapter.js';
|
|
33
|
+
import { registerEnvironmentPreset } from '../environment/environmentPresets.js';
|
|
34
|
+
import {
|
|
35
|
+
createUrbanPropShaderProfileDocument,
|
|
36
|
+
createUrbanPropShaderProfileSettings,
|
|
37
|
+
} from '../environment/urbanPropMaterial.js';
|
|
38
|
+
import {
|
|
39
|
+
createGroundShaderSettings,
|
|
40
|
+
registerGroundShaderPreset,
|
|
41
|
+
} from '../ground-shader/groundShaderSettings.js';
|
|
42
|
+
import { registerPostProcessingPreset } from '../post/postProcessing.js';
|
|
43
|
+
import { createSkySettings, registerSkyPreset } from '../sky/stylizedSky.js';
|
|
44
|
+
import { createToonSettings, registerToonPreset } from '../toon/toonSettings.js';
|
|
45
|
+
import { createGrassSettings, registerGrassPreset } from '../vegetation/stylizedGrass.js';
|
|
46
|
+
import { registerVegetationShaderPreset } from '../vegetation/vegetationShaders.js';
|
|
47
|
+
import { registerWaterStyle } from '../water/waterSettings.js';
|
|
48
|
+
import { createStyleBundleDocument, CALL_ME_SENSEI_STYLE_SLOT_IDS } from './styleBundle.js';
|
|
49
|
+
|
|
50
|
+
/** The reserved id every domain uses for its un-stylized baseline. */
|
|
51
|
+
export const NEUTRAL_STYLE_PRESET_ID = 'neutral';
|
|
52
|
+
|
|
53
|
+
const NEUTRAL_DESCRIPTION = 'Un-stylized standard PBR baseline: source maps at full strength, real shadows and specular, and no stylization layered on top. The "before" half of a ToonLab comparison.';
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Per-domain neutral settings. Each block strips stylization and leaves the
|
|
57
|
+
// physical response — and, where a key controls geometry, density, tiling,
|
|
58
|
+
// masking or motion, it is deliberately absent so both halves stay identical.
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Character toon shading. Disables every graphic feature (cel bands, outlines,
|
|
63
|
+
* rim, hair/eye highlights, face proxy normals, shadow tinting, skin-tone
|
|
64
|
+
* grading) and lets shadows fall to their physical depth by dropping the anime
|
|
65
|
+
* "minimum light" floors to zero. Material maps, alpha, specular, scene/self
|
|
66
|
+
* shadow and indirect light stay ON — that is the PBR half of the shader.
|
|
67
|
+
*/
|
|
68
|
+
export const NEUTRAL_TOON_SETTINGS = Object.freeze({
|
|
69
|
+
averageShadow: { enabled: false },
|
|
70
|
+
celShade: { enabled: false },
|
|
71
|
+
contactShadow: { enabled: false },
|
|
72
|
+
eyeHighlight: { enabled: false },
|
|
73
|
+
faceLighting: { enabled: false },
|
|
74
|
+
fur: { enabled: false },
|
|
75
|
+
glitter: { enabled: false },
|
|
76
|
+
hairHighlight: { enabled: false },
|
|
77
|
+
outline: { enabled: false },
|
|
78
|
+
perspectiveRemoval: { enabled: false },
|
|
79
|
+
rimLight: { enabled: false },
|
|
80
|
+
sceneShadow: {
|
|
81
|
+
defaultMinLight: 0,
|
|
82
|
+
defaultStrength: 1,
|
|
83
|
+
enabled: true,
|
|
84
|
+
eyeMinLight: 0,
|
|
85
|
+
eyeStrength: 1,
|
|
86
|
+
faceMinLight: 0,
|
|
87
|
+
faceStrength: 1,
|
|
88
|
+
shadowAreaStrength: 1,
|
|
89
|
+
skinMinLight: 0,
|
|
90
|
+
skinStrength: 1,
|
|
91
|
+
},
|
|
92
|
+
selfShadow: {
|
|
93
|
+
defaultMinLight: 0,
|
|
94
|
+
defaultStrength: 1,
|
|
95
|
+
enabled: true,
|
|
96
|
+
eyeMinLight: 0,
|
|
97
|
+
eyeStrength: 1,
|
|
98
|
+
faceMinLight: 0,
|
|
99
|
+
faceStrength: 1,
|
|
100
|
+
hairMinLight: 0,
|
|
101
|
+
hairStrength: 1,
|
|
102
|
+
shadowAreaStrength: 1,
|
|
103
|
+
skinMinLight: 0,
|
|
104
|
+
skinStrength: 1,
|
|
105
|
+
},
|
|
106
|
+
shadowColor: { enabled: false },
|
|
107
|
+
skinTone: { enabled: false },
|
|
108
|
+
sticker: { enabled: false },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Vegetation shader — one registration serves the `treeShader`, `grassShader`
|
|
113
|
+
* and `flowerShader` scopes, which share a registry. Bands go to their
|
|
114
|
+
* smoothest admissible form (`bandCount` is clamped to `[2, 6]`, so 6 with full
|
|
115
|
+
* softness is the closest the schema gets to continuous), tints and rim fills
|
|
116
|
+
* go to zero, and physical transmission (`thinSurface`) and weather response
|
|
117
|
+
* are left exactly as authored because they are PBR, not stylization.
|
|
118
|
+
*/
|
|
119
|
+
export const NEUTRAL_VEGETATION_SHADER_SETTINGS = Object.freeze({
|
|
120
|
+
bark: {
|
|
121
|
+
bandCount: 6,
|
|
122
|
+
bandSoftness: 1,
|
|
123
|
+
emissiveStrength: 0,
|
|
124
|
+
normalFlatness: 0,
|
|
125
|
+
rimStrength: 0,
|
|
126
|
+
roughness: 1,
|
|
127
|
+
shadowFloor: 0,
|
|
128
|
+
skyFillStrength: 0,
|
|
129
|
+
specularStrength: 0,
|
|
130
|
+
sunTintStrength: 0,
|
|
131
|
+
tintStrength: 0,
|
|
132
|
+
verticalShadeStrength: 0,
|
|
133
|
+
},
|
|
134
|
+
flower: {
|
|
135
|
+
backlitStrength: 0,
|
|
136
|
+
bandSoftness: 1,
|
|
137
|
+
emissiveStrength: 0,
|
|
138
|
+
tintStrength: 0,
|
|
139
|
+
unlitPetalLift: 0,
|
|
140
|
+
},
|
|
141
|
+
foliage: {
|
|
142
|
+
backlitStrength: 0,
|
|
143
|
+
bandSoftness: 1,
|
|
144
|
+
cardVariationStrength: 0,
|
|
145
|
+
crestSoftness: 1,
|
|
146
|
+
crownOcclusionStrength: 0,
|
|
147
|
+
emissiveStrength: 0,
|
|
148
|
+
gradientContrast: 1,
|
|
149
|
+
gradientOffset: 0,
|
|
150
|
+
hueShift: 0,
|
|
151
|
+
hueVariation: 0,
|
|
152
|
+
spriteLuminanceStrength: 0,
|
|
153
|
+
styleColorStrength: 0,
|
|
154
|
+
},
|
|
155
|
+
grass: {
|
|
156
|
+
backlitStrength: 0,
|
|
157
|
+
bandSoftness: 1,
|
|
158
|
+
colorVariationStrength: 0,
|
|
159
|
+
emissiveStrength: 0,
|
|
160
|
+
gustSheenStrength: 0,
|
|
161
|
+
rootOcclusionStrength: 0,
|
|
162
|
+
shadowFloor: 0,
|
|
163
|
+
styleColorStrength: 0,
|
|
164
|
+
tipDesaturation: 0,
|
|
165
|
+
tipHueShift: 0,
|
|
166
|
+
},
|
|
167
|
+
lighting: {
|
|
168
|
+
rimStrength: 0,
|
|
169
|
+
shadowTintStrength: 0,
|
|
170
|
+
skyFillStrength: 0,
|
|
171
|
+
sunTintStrength: 0,
|
|
172
|
+
},
|
|
173
|
+
stem: {
|
|
174
|
+
bandCount: 6,
|
|
175
|
+
bandSoftness: 1,
|
|
176
|
+
colorStrength: 0,
|
|
177
|
+
emissiveStrength: 0,
|
|
178
|
+
rimStrength: 0,
|
|
179
|
+
shadowFloor: 0,
|
|
180
|
+
skyFillStrength: 0,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Grass field. SHADING KEYS ONLY — `bladesPerClump`, `clumpRadius`,
|
|
186
|
+
* `bladeHeightRange`, `bladeWidthRange`, `leanStrength`, every wind/gust key
|
|
187
|
+
* and `pushRadius` are intentionally absent so the neutral and styled halves
|
|
188
|
+
* grow the identical blades in the identical places with the identical motion.
|
|
189
|
+
*/
|
|
190
|
+
export const NEUTRAL_GRASS_SETTINGS = Object.freeze({
|
|
191
|
+
backlitStrength: 0,
|
|
192
|
+
baseColor: Object.freeze([0.118, 0.196, 0.075]),
|
|
193
|
+
cloudShadowStrength: 0,
|
|
194
|
+
groundAdoptStrength: 0,
|
|
195
|
+
groundAdoptTint: Object.freeze([1, 1, 1]),
|
|
196
|
+
shadowStrength: 0.5,
|
|
197
|
+
shadowTint: Object.freeze([1, 1, 1]),
|
|
198
|
+
tipColor: Object.freeze([0.196, 0.298, 0.11]),
|
|
199
|
+
washLift: 0,
|
|
200
|
+
washOpacity: 1,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Ground shader. Projection scales, slope thresholds and shoreline widths are
|
|
205
|
+
* copied from the `call_me_sensei` preset ON PURPOSE: those decide WHICH layer
|
|
206
|
+
* is painted WHERE and at what tiling. Keeping them equal means the neutral
|
|
207
|
+
* half paints the identical mask at the identical scale, and only the treatment
|
|
208
|
+
* differs — layer tints go neutral, macro variation and edge highlight go to
|
|
209
|
+
* zero, shadows lose their tint and lift, and the aerial distance tint is off.
|
|
210
|
+
*/
|
|
211
|
+
export const NEUTRAL_GROUND_SHADER_SETTINGS = Object.freeze({
|
|
212
|
+
distance: { detailFade: 1, strength: 0 },
|
|
213
|
+
layers: {
|
|
214
|
+
brightness: 0,
|
|
215
|
+
contrast: 1,
|
|
216
|
+
dirtTint: [1, 1, 1],
|
|
217
|
+
grassTint: [1, 1, 1],
|
|
218
|
+
rockTint: [1, 1, 1],
|
|
219
|
+
sandTint: [1, 1, 1],
|
|
220
|
+
saturation: 1,
|
|
221
|
+
textureStrength: 1,
|
|
222
|
+
},
|
|
223
|
+
lighting: {
|
|
224
|
+
backShadowStrength: 0,
|
|
225
|
+
shadowLift: 0,
|
|
226
|
+
shadowTint: [1, 1, 1],
|
|
227
|
+
shadowTintStrength: 0,
|
|
228
|
+
skyFillStrength: 0,
|
|
229
|
+
sunIntensity: 1,
|
|
230
|
+
},
|
|
231
|
+
macro: { amount: 0, secondaryAmount: 0, tintStrength: 0 },
|
|
232
|
+
material: { emissiveStrength: 0, metalness: 0, roughness: 1 },
|
|
233
|
+
// Identical to call_me_sensei — mask geometry, not look.
|
|
234
|
+
projection: {
|
|
235
|
+
dirtScale: 13,
|
|
236
|
+
grassScale: 16,
|
|
237
|
+
rockScale: 25,
|
|
238
|
+
sandScale: 10,
|
|
239
|
+
triplanarSharpness: 2,
|
|
240
|
+
triplanarStrength: 1,
|
|
241
|
+
},
|
|
242
|
+
printResponse: { rimLightening: 0 },
|
|
243
|
+
shoreline: { autoSandStrength: 0, bandWidth: 0.75, softness: 0.25, wetBandWidth: 0.75 },
|
|
244
|
+
slope: {
|
|
245
|
+
autoRockStrength: 1,
|
|
246
|
+
edgeHighlight: 0,
|
|
247
|
+
fade: 0.05,
|
|
248
|
+
noiseScale: 1 / 80,
|
|
249
|
+
noiseStrength: 0.08,
|
|
250
|
+
start: 0.15,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Environment. Keeps every map-consuming and light-consuming feature on and
|
|
256
|
+
* turns off the five graphic ones (AO overlay, the authored left-side shade,
|
|
257
|
+
* sky tinting, sun boost and the untextured gradient), then flattens the
|
|
258
|
+
* grading parameters to unity.
|
|
259
|
+
*/
|
|
260
|
+
export const NEUTRAL_ENVIRONMENT_PRESET = Object.freeze({
|
|
261
|
+
features: Object.freeze({
|
|
262
|
+
aoOverlay: false,
|
|
263
|
+
leftSideShadow: false,
|
|
264
|
+
skyTint: false,
|
|
265
|
+
sunBoost: false,
|
|
266
|
+
untexturedGradient: false,
|
|
267
|
+
}),
|
|
268
|
+
parameters: Object.freeze({
|
|
269
|
+
aoWarmth: 0,
|
|
270
|
+
cloudShadowStrength: 0,
|
|
271
|
+
directLightStrength: 1,
|
|
272
|
+
exposure: 1,
|
|
273
|
+
lightingInfluence: 1,
|
|
274
|
+
saturation: 1,
|
|
275
|
+
shadowLift: 0,
|
|
276
|
+
shadowTintColor: [1, 1, 1],
|
|
277
|
+
skyTintStrength: 0,
|
|
278
|
+
triplanarDetail: 0,
|
|
279
|
+
triplanarEdgeHighlight: 0,
|
|
280
|
+
untexturedGradientStrength: 0,
|
|
281
|
+
}),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Manufactured surface. Every `*Enabled` control that adds a graphic layer goes
|
|
286
|
+
* to 0; source authority, normal detail, reflection probes, decals and graphics
|
|
287
|
+
* stay at 1 so the surface still reads as the material it actually is.
|
|
288
|
+
*/
|
|
289
|
+
export const NEUTRAL_MANUFACTURED_SURFACE_SETTINGS = Object.freeze({
|
|
290
|
+
celLightingEnabled: 0,
|
|
291
|
+
colorLiftEnabled: 0,
|
|
292
|
+
colorLiftStrength: 0,
|
|
293
|
+
coolShadowsEnabled: 0,
|
|
294
|
+
coolShadowStrength: 0,
|
|
295
|
+
decalStrength: 1,
|
|
296
|
+
edgeInkEnabled: 0,
|
|
297
|
+
fresnelEnabled: 0,
|
|
298
|
+
fresnelStrength: 0,
|
|
299
|
+
graphicsEnabled: 1,
|
|
300
|
+
highlightBandEnabled: 0,
|
|
301
|
+
highlightBandStrength: 0,
|
|
302
|
+
materialResponseEnabled: 0,
|
|
303
|
+
materialResponseStrength: 0,
|
|
304
|
+
normalDetailEnabled: 1,
|
|
305
|
+
paintBandsEnabled: 0,
|
|
306
|
+
paintExtractionEnabled: 1,
|
|
307
|
+
paintExtractionStrength: 1,
|
|
308
|
+
pastelPaletteEnabled: 0,
|
|
309
|
+
pastelStrength: 0,
|
|
310
|
+
planarSheenEnabled: 0,
|
|
311
|
+
planarSheenStrength: 0,
|
|
312
|
+
reflectionNormalEnabled: 1,
|
|
313
|
+
reflectionProbeLayerEnabled: 1,
|
|
314
|
+
reflectionSelectivityEnabled: 0,
|
|
315
|
+
rimEnabled: 0,
|
|
316
|
+
roughnessBreakupEnabled: 0,
|
|
317
|
+
shadowPastelEnabled: 0,
|
|
318
|
+
shadowPastelStrength: 0,
|
|
319
|
+
silhouetteInkEnabled: 0,
|
|
320
|
+
sourceAuthorityEnabled: 1,
|
|
321
|
+
sourceAuthorityStrength: 1,
|
|
322
|
+
viewReflectionEnabled: 1,
|
|
323
|
+
wearEnabled: 0,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Water. `colorTone: 'classic'` is the schema's own "palette untouched" tone —
|
|
328
|
+
* it hands colour back to the preset instead of force-applying the `anime`
|
|
329
|
+
* palette over it (see D19-005). Motion identity stays owned by the preset, so
|
|
330
|
+
* both halves run the same waves.
|
|
331
|
+
*/
|
|
332
|
+
export const NEUTRAL_WATER_STYLE = Object.freeze({
|
|
333
|
+
settings: Object.freeze({
|
|
334
|
+
colorTone: 'classic',
|
|
335
|
+
sceneShadowStrength: 1,
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Sky. A restrained physical-looking gradient with the signature glow and
|
|
341
|
+
* saturation pulled back. Cloud coverage, scale, speed, direction and seed are
|
|
342
|
+
* NOT set — cloud shape is geometry, and both halves must see the same clouds.
|
|
343
|
+
*/
|
|
344
|
+
export const NEUTRAL_SKY_SETTINGS = Object.freeze({
|
|
345
|
+
// Cloud SEED is scene identity, not style: two halves drawing differently
|
|
346
|
+
// shaped clouds is a scene comparison, not a shader comparison. Pinned to the
|
|
347
|
+
// same seed the styled preset uses so the shapes match and only the treatment
|
|
348
|
+
// differs. Coverage/scale/speed stay unpinned — they are driven by the shared
|
|
349
|
+
// scenario axis, which both halves see identically.
|
|
350
|
+
cloudSeed: 7,
|
|
351
|
+
horizonColor: [0.71, 0.79, 0.86],
|
|
352
|
+
horizonScattering: 0.5,
|
|
353
|
+
sunGlowStrength: 0.6,
|
|
354
|
+
zenithColor: [0.22, 0.42, 0.72],
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
/** Post. Pipeline stays enabled at zero strength — same cost, no grade. */
|
|
358
|
+
export const NEUTRAL_POST_SETTINGS = Object.freeze({
|
|
359
|
+
features: Object.freeze({
|
|
360
|
+
bloom: false,
|
|
361
|
+
colorGrade: false,
|
|
362
|
+
enabled: true,
|
|
363
|
+
verticalGrade: false,
|
|
364
|
+
vignette: false,
|
|
365
|
+
}),
|
|
366
|
+
parameters: Object.freeze({
|
|
367
|
+
bloomStrength: 0,
|
|
368
|
+
bottomDark: 0,
|
|
369
|
+
contrast: 1,
|
|
370
|
+
exposure: 1,
|
|
371
|
+
saturation: 1,
|
|
372
|
+
strength: 0,
|
|
373
|
+
topLight: 0,
|
|
374
|
+
vignetteStrength: 0,
|
|
375
|
+
warmth: 0,
|
|
376
|
+
}),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
/** Portable manufactured-surface document, for the bundle's `{ document }` payload. */
|
|
380
|
+
export const NEUTRAL_MANUFACTURED_SURFACE_DOCUMENT = Object.freeze(
|
|
381
|
+
createUrbanPropShaderProfileDocument(NEUTRAL_STYLE_PRESET_ID, {
|
|
382
|
+
description: NEUTRAL_DESCRIPTION,
|
|
383
|
+
label: 'Neutral',
|
|
384
|
+
settings: NEUTRAL_MANUFACTURED_SURFACE_SETTINGS,
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Registration — one table, one loop, so every slot is treated identically.
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
const REGISTRARS = Object.freeze({
|
|
393
|
+
environment: () => registerEnvironmentPreset(NEUTRAL_STYLE_PRESET_ID, {
|
|
394
|
+
features: { ...NEUTRAL_ENVIRONMENT_PRESET.features },
|
|
395
|
+
label: 'Neutral',
|
|
396
|
+
parameters: { ...NEUTRAL_ENVIRONMENT_PRESET.parameters },
|
|
397
|
+
}, { overwrite: true }),
|
|
398
|
+
grass: () => registerGrassPreset(NEUTRAL_STYLE_PRESET_ID, {
|
|
399
|
+
description: NEUTRAL_DESCRIPTION,
|
|
400
|
+
label: 'Neutral',
|
|
401
|
+
settings: { ...NEUTRAL_GRASS_SETTINGS },
|
|
402
|
+
}, { overwrite: true }),
|
|
403
|
+
groundShader: () => registerGroundShaderPreset(NEUTRAL_STYLE_PRESET_ID, {
|
|
404
|
+
description: NEUTRAL_DESCRIPTION,
|
|
405
|
+
label: 'Neutral',
|
|
406
|
+
settings: NEUTRAL_GROUND_SHADER_SETTINGS,
|
|
407
|
+
}, { overwrite: true }),
|
|
408
|
+
post: () => registerPostProcessingPreset(NEUTRAL_STYLE_PRESET_ID, {
|
|
409
|
+
description: NEUTRAL_DESCRIPTION,
|
|
410
|
+
label: 'Neutral',
|
|
411
|
+
settings: NEUTRAL_POST_SETTINGS,
|
|
412
|
+
}, { overwrite: true }),
|
|
413
|
+
sky: () => registerSkyPreset(NEUTRAL_STYLE_PRESET_ID, {
|
|
414
|
+
description: NEUTRAL_DESCRIPTION,
|
|
415
|
+
label: 'Neutral',
|
|
416
|
+
settings: { ...NEUTRAL_SKY_SETTINGS },
|
|
417
|
+
}, { overwrite: true }),
|
|
418
|
+
toon: () => registerToonPreset(NEUTRAL_STYLE_PRESET_ID, {
|
|
419
|
+
description: NEUTRAL_DESCRIPTION,
|
|
420
|
+
label: 'Neutral',
|
|
421
|
+
settings: NEUTRAL_TOON_SETTINGS,
|
|
422
|
+
}, { overwrite: true }),
|
|
423
|
+
vegetationShader: () => registerVegetationShaderPreset(NEUTRAL_STYLE_PRESET_ID, {
|
|
424
|
+
description: NEUTRAL_DESCRIPTION,
|
|
425
|
+
label: 'Neutral',
|
|
426
|
+
settings: NEUTRAL_VEGETATION_SHADER_SETTINGS,
|
|
427
|
+
}, { overwrite: true }),
|
|
428
|
+
water: () => registerWaterStyle(NEUTRAL_STYLE_PRESET_ID, {
|
|
429
|
+
description: NEUTRAL_DESCRIPTION,
|
|
430
|
+
label: 'Neutral',
|
|
431
|
+
...NEUTRAL_WATER_STYLE,
|
|
432
|
+
}, { overwrite: true }),
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Per-slot coverage. `authored` slots gained a neutral counterpart here;
|
|
437
|
+
* `shipped` already had one; `inherited` resolves to schema defaults because
|
|
438
|
+
* the domain has no style registry; `excluded` has no usable neutral and says
|
|
439
|
+
* why. Nothing is silently missing.
|
|
440
|
+
*/
|
|
441
|
+
export const NEUTRAL_STYLE_SLOT_COVERAGE = Object.freeze({
|
|
442
|
+
cloud: Object.freeze({
|
|
443
|
+
abSafe: true,
|
|
444
|
+
coverage: 'inherited',
|
|
445
|
+
note: 'There is no cloud style registry — the slot resolves to SkyParams schema defaults for every style, neutral included (see D19-006). Schema defaults ARE the un-stylized cloud.',
|
|
446
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
447
|
+
}),
|
|
448
|
+
environment: Object.freeze({
|
|
449
|
+
abSafe: true, coverage: 'authored', registrar: 'environment',
|
|
450
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
451
|
+
}),
|
|
452
|
+
flowerShader: Object.freeze({
|
|
453
|
+
abSafe: true, coverage: 'authored', registrar: 'vegetationShader',
|
|
454
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
455
|
+
}),
|
|
456
|
+
grass: Object.freeze({
|
|
457
|
+
abSafe: true, coverage: 'authored', registrar: 'grass',
|
|
458
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
459
|
+
}),
|
|
460
|
+
grassShader: Object.freeze({
|
|
461
|
+
abSafe: true, coverage: 'authored', registrar: 'vegetationShader',
|
|
462
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
463
|
+
}),
|
|
464
|
+
groundShader: Object.freeze({
|
|
465
|
+
abSafe: true, coverage: 'authored', registrar: 'groundShader',
|
|
466
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
467
|
+
}),
|
|
468
|
+
lighting: Object.freeze({
|
|
469
|
+
abSafe: false,
|
|
470
|
+
coverage: 'excluded',
|
|
471
|
+
note: 'Deliberately unpaired. A lighting style owns sun intensity, sun path and the day cycle, so a neutral lighting style changes LIGHT TRANSFORMS — which §11 requires both halves of a comparison to share. Neutral lighting would be unusable in the only construction it exists for.',
|
|
472
|
+
}),
|
|
473
|
+
manufacturedSurface: Object.freeze({
|
|
474
|
+
abSafe: true,
|
|
475
|
+
coverage: 'authored',
|
|
476
|
+
note: 'Carried as an inline document: the manufactured-surface slot has no style registry, so a { style } payload resolves to defaults regardless of the id.',
|
|
477
|
+
payload: Object.freeze({ document: NEUTRAL_MANUFACTURED_SURFACE_DOCUMENT }),
|
|
478
|
+
registrar: null,
|
|
479
|
+
}),
|
|
480
|
+
post: Object.freeze({
|
|
481
|
+
abSafe: false,
|
|
482
|
+
coverage: 'authored',
|
|
483
|
+
note: 'Registered and usable, but not part of an A/B: post runs over the composited frame, after both scissored renders, so it cannot differ per half. Both halves must share it — which §11 also requires ("stable exposure").',
|
|
484
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
485
|
+
registrar: 'post',
|
|
486
|
+
}),
|
|
487
|
+
rock: Object.freeze({
|
|
488
|
+
abSafe: true,
|
|
489
|
+
coverage: 'shipped',
|
|
490
|
+
note: 'Pre-existing — registered by src/rock-shader/rockShaderSettings.js. The only domain that already had a neutral preset.',
|
|
491
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
492
|
+
}),
|
|
493
|
+
sky: Object.freeze({
|
|
494
|
+
abSafe: true, coverage: 'authored', registrar: 'sky',
|
|
495
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
496
|
+
}),
|
|
497
|
+
toon: Object.freeze({
|
|
498
|
+
abSafe: true, coverage: 'authored', registrar: 'toon',
|
|
499
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
500
|
+
}),
|
|
501
|
+
treeShader: Object.freeze({
|
|
502
|
+
abSafe: true, coverage: 'authored', registrar: 'vegetationShader',
|
|
503
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
504
|
+
}),
|
|
505
|
+
water: Object.freeze({
|
|
506
|
+
abSafe: true, coverage: 'authored', registrar: 'water',
|
|
507
|
+
payload: Object.freeze({ style: NEUTRAL_STYLE_PRESET_ID }),
|
|
508
|
+
}),
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
let registered = false;
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Registers `neutral` into every domain registry that has one. Idempotent and
|
|
515
|
+
* safe to call from any entry point; returns the per-registrar result.
|
|
516
|
+
*/
|
|
517
|
+
export function registerNeutralStylePresets({ force = false } = {}) {
|
|
518
|
+
if (registered && !force) return { alreadyRegistered: true, registrars: [] };
|
|
519
|
+
const registrars = [];
|
|
520
|
+
for (const [id, register] of Object.entries(REGISTRARS)) {
|
|
521
|
+
try {
|
|
522
|
+
registrars.push({ id, ok: true, result: register() });
|
|
523
|
+
} catch (error) {
|
|
524
|
+
registrars.push({ error: String(error?.message ?? error), id, ok: false });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
registered = true;
|
|
528
|
+
return { alreadyRegistered: false, registrars };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
registerNeutralStylePresets();
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* The neutral counterpart of `CALL_ME_SENSEI_STYLE_BUNDLE`. Same slot ids,
|
|
535
|
+
* minus the two that cannot honestly carry a neutral half — so applying this
|
|
536
|
+
* bundle and then the Call Me Sensei bundle to the same scene produces a
|
|
537
|
+
* symmetric A/B in which only material treatment changed.
|
|
538
|
+
*/
|
|
539
|
+
export const NEUTRAL_STYLE_BUNDLE = createStyleBundleDocument('neutral', {
|
|
540
|
+
description: 'Un-stylized standard-PBR baseline for every slot that can carry one. The measured "before" half of a ToonLab style comparison.',
|
|
541
|
+
label: 'Neutral',
|
|
542
|
+
slots: Object.fromEntries(
|
|
543
|
+
CALL_ME_SENSEI_STYLE_SLOT_IDS
|
|
544
|
+
.map((slotId) => [slotId, NEUTRAL_STYLE_SLOT_COVERAGE[slotId]])
|
|
545
|
+
.filter(([, entry]) => entry?.abSafe && entry.payload)
|
|
546
|
+
.map(([slotId, entry]) => [slotId, entry.payload]),
|
|
547
|
+
),
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
/** The slot ids `NEUTRAL_STYLE_BUNDLE` actually carries. */
|
|
551
|
+
export const NEUTRAL_STYLE_SLOT_IDS = Object.freeze(Object.keys(NEUTRAL_STYLE_BUNDLE.slots));
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Reports neutral coverage per slot against the Call Me Sensei slot list, so a
|
|
555
|
+
* gap is visible instead of implied.
|
|
556
|
+
*/
|
|
557
|
+
export function describeNeutralStyleCoverage() {
|
|
558
|
+
return CALL_ME_SENSEI_STYLE_SLOT_IDS.map((slotId) => {
|
|
559
|
+
const entry = NEUTRAL_STYLE_SLOT_COVERAGE[slotId] ?? { coverage: 'missing' };
|
|
560
|
+
return {
|
|
561
|
+
abSafe: entry.abSafe ?? false,
|
|
562
|
+
coverage: entry.coverage,
|
|
563
|
+
inBundle: Object.hasOwn(NEUTRAL_STYLE_BUNDLE.slots, slotId),
|
|
564
|
+
note: entry.note ?? '',
|
|
565
|
+
slot: slotId,
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Keys per domain that control geometry, density, placement, tiling, masking or
|
|
572
|
+
* motion. A neutral preset that touched one of these would make the two halves
|
|
573
|
+
* structurally different, which is the failure this whole pass exists to
|
|
574
|
+
* prevent. Used by {@link auditNeutralStyleShadingOnly}.
|
|
575
|
+
*/
|
|
576
|
+
export const NEUTRAL_STYLE_NON_SHADING_KEYS = Object.freeze({
|
|
577
|
+
grass: Object.freeze([
|
|
578
|
+
'bladeHeightRange', 'bladeWidthRange', 'bladesPerClump', 'clumpRadius',
|
|
579
|
+
'gustFrequency', 'gustResponse', 'gustSpeed', 'leanStrength', 'pushRadius',
|
|
580
|
+
'windDirection', 'windResponse', 'windSpeed', 'windStrength',
|
|
581
|
+
]),
|
|
582
|
+
groundShader: Object.freeze([
|
|
583
|
+
'projection.dirtScale', 'projection.grassScale', 'projection.rockScale',
|
|
584
|
+
'projection.sandScale', 'projection.triplanarSharpness', 'projection.triplanarStrength',
|
|
585
|
+
'slope.autoRockStrength', 'slope.fade', 'slope.noiseScale', 'slope.noiseStrength',
|
|
586
|
+
'slope.start', 'shoreline.autoSandStrength', 'shoreline.bandWidth',
|
|
587
|
+
'shoreline.softness', 'shoreline.wetBandWidth',
|
|
588
|
+
]),
|
|
589
|
+
sky: Object.freeze([
|
|
590
|
+
'cloudCoverage', 'cloudDirection', 'cloudProjection', 'cloudScale',
|
|
591
|
+
'cloudSeed', 'cloudSpeed', 'radius',
|
|
592
|
+
]),
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
function readPath(source, path) {
|
|
596
|
+
return path.split('.').reduce((value, key) => (value == null ? value : value[key]), source);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function writePath(target, path, value) {
|
|
600
|
+
const keys = path.split('.');
|
|
601
|
+
const last = keys.pop();
|
|
602
|
+
const parent = keys.reduce((node, key) => {
|
|
603
|
+
node[key] ??= {};
|
|
604
|
+
return node[key];
|
|
605
|
+
}, target);
|
|
606
|
+
parent[last] = value;
|
|
607
|
+
return target;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function sameValue(a, b) {
|
|
611
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
612
|
+
return a.length === b.length && a.every((value, index) => sameValue(value, b[index]));
|
|
613
|
+
}
|
|
614
|
+
if (typeof a === 'number' && typeof b === 'number') return Math.abs(a - b) < 1e-9;
|
|
615
|
+
return a === b;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Proves that `neutral` differs from `call_me_sensei` in SHADING ONLY.
|
|
620
|
+
*
|
|
621
|
+
* Two questions, deliberately separated, because they have different answers
|
|
622
|
+
* and only one of them is a defect:
|
|
623
|
+
*
|
|
624
|
+
* - **issues (must be empty).** When a host supplies the same explicit
|
|
625
|
+
* recipe to both presets — which is what a scene always does — do the
|
|
626
|
+
* geometry / mask / tiling / motion keys resolve identically? If not, the
|
|
627
|
+
* neutral preset is overriding something it has no business owning, and the
|
|
628
|
+
* two halves of a wipe would not share geometry.
|
|
629
|
+
* - **warnings (informational).** With NO host recipe, do the bare
|
|
630
|
+
* resolutions differ? Some do, and that is correct: `bladesPerClump` is a
|
|
631
|
+
* property of the field the host authored, not of the style, so `neutral`
|
|
632
|
+
* does not declare it and it falls through to the schema default rather
|
|
633
|
+
* than to the styled preset's value. Each key listed here is one the host
|
|
634
|
+
* must pass identically to both halves. The ground shader takes the other
|
|
635
|
+
* policy — it PINS its mask keys to the styled values so an un-parameterised
|
|
636
|
+
* neutral still paints the same masks — and reports no warnings as a result.
|
|
637
|
+
*/
|
|
638
|
+
export function auditNeutralStyleShadingOnly() {
|
|
639
|
+
const issues = [];
|
|
640
|
+
const warnings = [];
|
|
641
|
+
const domains = [];
|
|
642
|
+
|
|
643
|
+
const compare = (domain, keys, resolve, hostRecipe) => {
|
|
644
|
+
const bareNeutral = resolve({ preset: NEUTRAL_STYLE_PRESET_ID });
|
|
645
|
+
const bareStyled = resolve({ preset: 'call_me_sensei' });
|
|
646
|
+
const hostedNeutral = resolve({ ...structuredClone(hostRecipe), preset: NEUTRAL_STYLE_PRESET_ID });
|
|
647
|
+
const hostedStyled = resolve({ ...structuredClone(hostRecipe), preset: 'call_me_sensei' });
|
|
648
|
+
|
|
649
|
+
const describe = (path, a, b) => `${domain}.${path}: ${JSON.stringify(readPath(a, path))} vs ${JSON.stringify(readPath(b, path))}`;
|
|
650
|
+
const driftWithHostRecipe = keys
|
|
651
|
+
.filter((path) => !sameValue(readPath(hostedNeutral, path), readPath(hostedStyled, path)))
|
|
652
|
+
.map((path) => describe(path, hostedNeutral, hostedStyled));
|
|
653
|
+
const driftBare = keys
|
|
654
|
+
.filter((path) => !sameValue(readPath(bareNeutral, path), readPath(bareStyled, path)))
|
|
655
|
+
.map((path) => describe(path, bareNeutral, bareStyled));
|
|
656
|
+
|
|
657
|
+
issues.push(...driftWithHostRecipe);
|
|
658
|
+
warnings.push(...driftBare);
|
|
659
|
+
domains.push({
|
|
660
|
+
checkedKeys: keys.length,
|
|
661
|
+
domain,
|
|
662
|
+
driftBare,
|
|
663
|
+
driftWithHostRecipe,
|
|
664
|
+
hostMustSupply: driftBare.length,
|
|
665
|
+
ok: driftWithHostRecipe.length === 0,
|
|
666
|
+
});
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// A synthetic host recipe: every non-shading key set to a distinctive value,
|
|
670
|
+
// so a preset that silently overrides one is caught rather than masked by a
|
|
671
|
+
// coincidental match with the default.
|
|
672
|
+
const recipeFrom = (keys, seed) => keys.reduce((recipe, path, index) => {
|
|
673
|
+
const current = readPath(seed, path);
|
|
674
|
+
if (Array.isArray(current)) {
|
|
675
|
+
return writePath(recipe, path, current.map((value, position) => value + 0.011 * (index + position + 1)));
|
|
676
|
+
}
|
|
677
|
+
if (typeof current === 'number') {
|
|
678
|
+
return writePath(recipe, path, Number.isInteger(current) ? current + 3 : current + 0.017 * (index + 1));
|
|
679
|
+
}
|
|
680
|
+
return recipe;
|
|
681
|
+
}, {});
|
|
682
|
+
|
|
683
|
+
compare('grass', NEUTRAL_STYLE_NON_SHADING_KEYS.grass, createGrassSettings,
|
|
684
|
+
recipeFrom(NEUTRAL_STYLE_NON_SHADING_KEYS.grass, createGrassSettings({})));
|
|
685
|
+
compare('groundShader', NEUTRAL_STYLE_NON_SHADING_KEYS.groundShader, createGroundShaderSettings,
|
|
686
|
+
recipeFrom(NEUTRAL_STYLE_NON_SHADING_KEYS.groundShader, createGroundShaderSettings({})));
|
|
687
|
+
compare('sky', NEUTRAL_STYLE_NON_SHADING_KEYS.sky, createSkySettings,
|
|
688
|
+
recipeFrom(NEUTRAL_STYLE_NON_SHADING_KEYS.sky, createSkySettings({})));
|
|
689
|
+
|
|
690
|
+
return { domains, issues, ok: issues.length === 0, warnings };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Resolved neutral toon settings — handy for `applyToonShader(root, { settings })`
|
|
695
|
+
* when a host wants the neutral half without going through a bundle.
|
|
696
|
+
*/
|
|
697
|
+
export const NEUTRAL_TOON_RESOLVED_SETTINGS = Object.freeze(
|
|
698
|
+
createToonSettings({ preset: NEUTRAL_STYLE_PRESET_ID }),
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
/** Resolved neutral environment settings, for direct `applyEnvironmentShader` use. */
|
|
702
|
+
export const NEUTRAL_ENVIRONMENT_RESOLVED_SETTINGS = Object.freeze(
|
|
703
|
+
createEnvironmentSettings({
|
|
704
|
+
features: NEUTRAL_ENVIRONMENT_PRESET.features,
|
|
705
|
+
parameters: NEUTRAL_ENVIRONMENT_PRESET.parameters,
|
|
706
|
+
}),
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
/** Resolved neutral manufactured-surface settings. */
|
|
710
|
+
export const NEUTRAL_MANUFACTURED_SURFACE_RESOLVED_SETTINGS = Object.freeze(
|
|
711
|
+
createUrbanPropShaderProfileSettings(NEUTRAL_MANUFACTURED_SURFACE_SETTINGS),
|
|
712
|
+
);
|