@call-me-sensei/toonlab 0.2.0 → 0.3.0
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/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { clamp, finite, isPlainObject, vector } from './utils.js';
|
|
2
|
+
|
|
3
|
+
export const LIGHT_INTENSITY_UNITS = Object.freeze([
|
|
4
|
+
'unitless',
|
|
5
|
+
'lux',
|
|
6
|
+
'candela',
|
|
7
|
+
'lumens',
|
|
8
|
+
'nits',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const DEFAULT_UNIT_BY_TYPE = Object.freeze({
|
|
12
|
+
ambient: 'unitless',
|
|
13
|
+
discArea: 'nits',
|
|
14
|
+
directional: 'lux',
|
|
15
|
+
hemisphere: 'unitless',
|
|
16
|
+
point: 'candela',
|
|
17
|
+
rectArea: 'nits',
|
|
18
|
+
spot: 'candela',
|
|
19
|
+
tubeArea: 'nits',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function channelToHex(value) {
|
|
23
|
+
return Number.parseInt(value, 16) / 255;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseHexColor(value) {
|
|
27
|
+
const hex = String(value).trim().replace(/^#/, '');
|
|
28
|
+
if (/^[0-9a-f]{3}$/i.test(hex)) {
|
|
29
|
+
return [...hex].map((channel) => channelToHex(`${channel}${channel}`));
|
|
30
|
+
}
|
|
31
|
+
if (/^[0-9a-f]{6}$/i.test(hex)) {
|
|
32
|
+
return [channelToHex(hex.slice(0, 2)), channelToHex(hex.slice(2, 4)), channelToHex(hex.slice(4, 6))];
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Approximates a black-body color in authoring-space sRGB.
|
|
39
|
+
* The useful range is deliberately clamped to 1,000-40,000 kelvin.
|
|
40
|
+
*/
|
|
41
|
+
export function colorTemperatureToRgb(temperatureKelvin, { clampOutput = true } = {}) {
|
|
42
|
+
const temperature = clamp(finite(temperatureKelvin, 6500), 1000, 40000) / 100;
|
|
43
|
+
let red;
|
|
44
|
+
let green;
|
|
45
|
+
let blue;
|
|
46
|
+
|
|
47
|
+
if (temperature <= 66) {
|
|
48
|
+
red = 255;
|
|
49
|
+
green = 99.4708025861 * Math.log(temperature) - 161.1195681661;
|
|
50
|
+
blue = temperature <= 19
|
|
51
|
+
? 0
|
|
52
|
+
: 138.5177312231 * Math.log(temperature - 10) - 305.0447927307;
|
|
53
|
+
} else {
|
|
54
|
+
red = 329.698727446 * ((temperature - 60) ** -0.1332047592);
|
|
55
|
+
green = 288.1221695283 * ((temperature - 60) ** -0.0755148492);
|
|
56
|
+
blue = 255;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rgb = [red / 255, green / 255, blue / 255];
|
|
60
|
+
return clampOutput ? rgb.map((channel) => clamp(channel, 0, 1)) : rgb;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Converts an RGB array, hex color, or temperature/tint object to sRGB. */
|
|
64
|
+
export function resolveLightColor(value = [1, 1, 1]) {
|
|
65
|
+
if (typeof value === 'string') return parseHexColor(value) ?? [1, 1, 1];
|
|
66
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
67
|
+
return [((value >> 16) & 255) / 255, ((value >> 8) & 255) / 255, (value & 255) / 255];
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(value)) return vector(value, [1, 1, 1], 3).map((channel) => clamp(channel, 0, 1));
|
|
70
|
+
if (!isPlainObject(value)) return [1, 1, 1];
|
|
71
|
+
|
|
72
|
+
const base = value.temperatureKelvin !== null && value.temperatureKelvin !== undefined
|
|
73
|
+
? colorTemperatureToRgb(value.temperatureKelvin)
|
|
74
|
+
: resolveLightColor(value.rgb ?? value.color ?? [1, 1, 1]);
|
|
75
|
+
const tint = vector(value.tint, [1, 1, 1], 3);
|
|
76
|
+
return base.map((channel, index) => clamp(channel * tint[index], 0, 1));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Normalizes author-facing light color metadata into a serializable object. */
|
|
80
|
+
export function createLightColor(value = null) {
|
|
81
|
+
if (isPlainObject(value)) {
|
|
82
|
+
const temperature = value.temperatureKelvin === null || value.temperatureKelvin === undefined
|
|
83
|
+
? null
|
|
84
|
+
: clamp(finite(value.temperatureKelvin, 6500), 1000, 40000);
|
|
85
|
+
return {
|
|
86
|
+
rgb: temperature === null ? resolveLightColor(value.rgb ?? value.color ?? [1, 1, 1]) : null,
|
|
87
|
+
temperatureKelvin: temperature,
|
|
88
|
+
tint: vector(value.tint, [1, 1, 1], 3).map((channel) => Math.max(channel, 0)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
rgb: resolveLightColor(value ?? [1, 1, 1]),
|
|
93
|
+
temperatureKelvin: null,
|
|
94
|
+
tint: [1, 1, 1],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Converts luminous flux to intensity for an isotropic point source. */
|
|
99
|
+
export function lumensToCandela(lumens, solidAngleSteradians = Math.PI * 4) {
|
|
100
|
+
return Math.max(finite(lumens, 0), 0) / Math.max(finite(solidAngleSteradians, Math.PI * 4), 1e-6);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Converts candela to luminous flux for a supplied solid angle. */
|
|
104
|
+
export function candelaToLumens(candela, solidAngleSteradians = Math.PI * 4) {
|
|
105
|
+
return Math.max(finite(candela, 0), 0) * Math.max(finite(solidAngleSteradians, Math.PI * 4), 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Solid angle of a cone whose half-angle is `angleRadians`. */
|
|
109
|
+
export function coneSolidAngle(angleRadians) {
|
|
110
|
+
return Math.PI * 2 * (1 - Math.cos(clamp(finite(angleRadians, Math.PI / 4), 0.001, Math.PI / 2)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Illuminance in lux from a candela value at a distance in meters. */
|
|
114
|
+
export function luxAtDistance(candela, distanceMeters) {
|
|
115
|
+
const distance = Math.max(finite(distanceMeters, 1), 0.001);
|
|
116
|
+
return Math.max(finite(candela, 0), 0) / (distance * distance);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Approximate luminance for a diffuse rectangular emitter. */
|
|
120
|
+
export function lumensToNits(lumens, widthMeters = 1, heightMeters = 1) {
|
|
121
|
+
const area = Math.max(finite(widthMeters, 1) * finite(heightMeters, 1), 1e-6);
|
|
122
|
+
return Math.max(finite(lumens, 0), 0) / (Math.PI * area);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Normalizes physical intensity metadata. `artisticMultiplier` remains
|
|
127
|
+
* separate so a look can be tuned without destroying authored units.
|
|
128
|
+
*/
|
|
129
|
+
export function createLightIntensity(type, value = null) {
|
|
130
|
+
const defaultUnit = DEFAULT_UNIT_BY_TYPE[type] ?? 'unitless';
|
|
131
|
+
if (typeof value === 'number') {
|
|
132
|
+
return { artisticMultiplier: 1, referenceDistance: 1, unit: defaultUnit, value: Math.max(value, 0) };
|
|
133
|
+
}
|
|
134
|
+
const source = isPlainObject(value) ? value : {};
|
|
135
|
+
const unit = LIGHT_INTENSITY_UNITS.includes(source.unit) ? source.unit : defaultUnit;
|
|
136
|
+
return {
|
|
137
|
+
artisticMultiplier: Math.max(finite(source.artisticMultiplier, source.multiplier ?? 1), 0),
|
|
138
|
+
referenceDistance: Math.max(finite(source.referenceDistance, 1), 0.001),
|
|
139
|
+
unit,
|
|
140
|
+
value: Math.max(finite(source.value, 1), 0),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolves portable intensity metadata to the value expected by Three.js.
|
|
146
|
+
* Directional lux, local-light candela, and rect-area nits pass through.
|
|
147
|
+
* Conversions from lumens are geometric approximations, not photometry.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveThreeLightIntensity(type, intensity, geometry = {}) {
|
|
150
|
+
const resolved = createLightIntensity(type, intensity);
|
|
151
|
+
const { artisticMultiplier, referenceDistance, unit, value } = resolved;
|
|
152
|
+
let output = value;
|
|
153
|
+
|
|
154
|
+
if (type === 'point') {
|
|
155
|
+
if (unit === 'lumens') output = lumensToCandela(value);
|
|
156
|
+
if (unit === 'lux') output = value * referenceDistance * referenceDistance;
|
|
157
|
+
if (unit === 'nits') output = value;
|
|
158
|
+
} else if (type === 'spot') {
|
|
159
|
+
if (unit === 'lumens') output = lumensToCandela(value, coneSolidAngle(geometry.angle));
|
|
160
|
+
if (unit === 'lux') output = value * referenceDistance * referenceDistance;
|
|
161
|
+
if (unit === 'nits') output = value;
|
|
162
|
+
} else if (type === 'rectArea' || type === 'discArea' || type === 'tubeArea') {
|
|
163
|
+
if (unit === 'lumens') output = lumensToNits(value, geometry.width, geometry.height);
|
|
164
|
+
if (unit === 'candela') {
|
|
165
|
+
const area = Math.max(finite(geometry.width, 1) * finite(geometry.height, 1), 1e-6);
|
|
166
|
+
output = value / area;
|
|
167
|
+
}
|
|
168
|
+
if (unit === 'lux') output = value / Math.PI;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return Math.max(output * artisticMultiplier, 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Returns the preferred authoring unit for a descriptor type. */
|
|
175
|
+
export function getDefaultIntensityUnit(type) {
|
|
176
|
+
return DEFAULT_UNIT_BY_TYPE[type] ?? 'unitless';
|
|
177
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Public lighting-authoring surface. Kept as a subpath-ready cluster so hosts
|
|
2
|
+
// can use JSON documents and Unreal export without constructing a renderer.
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
LIGHT_INTENSITY_UNITS,
|
|
6
|
+
candelaToLumens,
|
|
7
|
+
colorTemperatureToRgb,
|
|
8
|
+
coneSolidAngle,
|
|
9
|
+
createLightColor,
|
|
10
|
+
createLightIntensity,
|
|
11
|
+
getDefaultIntensityUnit,
|
|
12
|
+
lumensToCandela,
|
|
13
|
+
lumensToNits,
|
|
14
|
+
luxAtDistance,
|
|
15
|
+
resolveLightColor,
|
|
16
|
+
resolveThreeLightIntensity,
|
|
17
|
+
} from './colorIntensity.js';
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
COOKIE_CAPABLE_LIGHT_TYPES,
|
|
21
|
+
LIGHT_TYPES,
|
|
22
|
+
SHADOW_CAPABLE_LIGHT_TYPES,
|
|
23
|
+
createAmbientLightDescriptor,
|
|
24
|
+
createDirectionalLightDescriptor,
|
|
25
|
+
createDiscAreaLightDescriptor,
|
|
26
|
+
createHemisphereLightDescriptor,
|
|
27
|
+
createLightArtisticSettings,
|
|
28
|
+
createLightCookie,
|
|
29
|
+
createLightDescriptor,
|
|
30
|
+
createLightIesProfile,
|
|
31
|
+
createLightLinking,
|
|
32
|
+
createLightShadow,
|
|
33
|
+
createPointLightDescriptor,
|
|
34
|
+
createRectAreaLightDescriptor,
|
|
35
|
+
createSpotLightDescriptor,
|
|
36
|
+
createTubeAreaLightDescriptor,
|
|
37
|
+
mergeLightDescriptor,
|
|
38
|
+
validateLightDescriptor,
|
|
39
|
+
} from './lightDescriptors.js';
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
LIGHTING_LOOK_DOCUMENT_TYPE,
|
|
43
|
+
LIGHTING_LOOK_SCHEMA_VERSION,
|
|
44
|
+
LIGHTING_RECIPE_DOCUMENT_TYPE,
|
|
45
|
+
LIGHTING_RECIPE_SCHEMA_VERSION,
|
|
46
|
+
SHADOW_POLICY_MODES,
|
|
47
|
+
SHADOW_UPDATE_MODES,
|
|
48
|
+
assertLightingLook,
|
|
49
|
+
assertLightingRecipe,
|
|
50
|
+
createLightingLook,
|
|
51
|
+
createLightingLookPreset,
|
|
52
|
+
createLightingRecipe,
|
|
53
|
+
createShadowPolicy,
|
|
54
|
+
deserializeLightingLook,
|
|
55
|
+
deserializeLightingLookPreset,
|
|
56
|
+
deserializeLightingRecipe,
|
|
57
|
+
serializeLightingLook,
|
|
58
|
+
serializeLightingLookPreset,
|
|
59
|
+
serializeLightingRecipe,
|
|
60
|
+
validateLightingLook,
|
|
61
|
+
validateLightingLookPreset,
|
|
62
|
+
validateLightingRecipe,
|
|
63
|
+
} from './lightingDocuments.js';
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
LIGHTING_LOOK_PRESETS,
|
|
67
|
+
LIGHTING_LUMINAIRE_PRESETS,
|
|
68
|
+
LIGHTING_QUALITY_PRESETS,
|
|
69
|
+
LIGHTING_RIG_PRESETS,
|
|
70
|
+
createLightingQualityProfile,
|
|
71
|
+
getLightingPresetOptions,
|
|
72
|
+
resolveLightingLookPreset,
|
|
73
|
+
resolveLightingPreset,
|
|
74
|
+
resolveLightingQualityPreset,
|
|
75
|
+
resolveLightingRigPreset,
|
|
76
|
+
resolveLuminairePreset,
|
|
77
|
+
} from './lightingPresets.js';
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
createLightingCapabilityReport,
|
|
81
|
+
getLightingTypeCapability,
|
|
82
|
+
snapshotLightingCapabilities,
|
|
83
|
+
} from './lightingCapabilities.js';
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
createLightingManager,
|
|
87
|
+
createLightingRig,
|
|
88
|
+
ensureAreaLightSupport,
|
|
89
|
+
getAreaLightSupportState,
|
|
90
|
+
realizeLightingRecipe,
|
|
91
|
+
} from './lightingRuntime.js';
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
LIGHTING_STYLE_APPLY_METADATA,
|
|
95
|
+
LIGHTING_STYLE_DOCUMENT_TYPE,
|
|
96
|
+
LIGHTING_STYLE_SCHEMA_VERSION,
|
|
97
|
+
createLightingStylePresetDocument,
|
|
98
|
+
createLightingStyleSettings,
|
|
99
|
+
getLightingStylePresetOptions,
|
|
100
|
+
parseLightingStylePresetDocument,
|
|
101
|
+
registerLightingStylePreset,
|
|
102
|
+
registerLightingStylePresetDocument,
|
|
103
|
+
resolveLightingStylePreset,
|
|
104
|
+
sampleLightingStyle,
|
|
105
|
+
sanitizeLightingStyleSettings,
|
|
106
|
+
serializeLightingStylePresetDocument,
|
|
107
|
+
validateLightingStylePresetDocument,
|
|
108
|
+
} from './lightingStyle.js';
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
FIXTURE_SCHEDULE_MODES,
|
|
112
|
+
LIGHT_FIXTURE_APPLY_METADATA,
|
|
113
|
+
LIGHT_FIXTURE_DOCUMENT_TYPE,
|
|
114
|
+
LIGHT_FIXTURE_SCHEMA_VERSION,
|
|
115
|
+
createLightFixtureDocument,
|
|
116
|
+
createLightFixtureSettings,
|
|
117
|
+
getLightFixtureOptions,
|
|
118
|
+
parseLightFixtureDocument,
|
|
119
|
+
registerLightFixture,
|
|
120
|
+
registerLightFixtureDocument,
|
|
121
|
+
resolveFixturePlacement,
|
|
122
|
+
resolveLightFixture,
|
|
123
|
+
sanitizeLightFixtureSettings,
|
|
124
|
+
serializeLightFixtureDocument,
|
|
125
|
+
validateLightFixtureDocument,
|
|
126
|
+
} from './lightingFixtures.js';
|
|
127
|
+
|
|
128
|
+
export { createLightingSystem } from './lightingSystem.js';
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
DEFAULT_LIGHTING_STYLE_DOMAINS,
|
|
132
|
+
DEFAULT_LIGHT_FIXTURE_DOMAINS,
|
|
133
|
+
LIGHTING_STYLE_GENERATOR_DOMAIN,
|
|
134
|
+
LIGHT_FIXTURE_GENERATOR_DOMAIN,
|
|
135
|
+
buildLightFixtureFromSample,
|
|
136
|
+
buildLightingStyleFromSample,
|
|
137
|
+
createGeneratedLightFixtureDocument,
|
|
138
|
+
createGeneratedLightingStyleDocument,
|
|
139
|
+
createLightFixtureGeneratorRecipe,
|
|
140
|
+
createLightingStyleGeneratorRecipe,
|
|
141
|
+
getLightFixtureGeneratorFamilyOptions,
|
|
142
|
+
getLightingStyleGeneratorFamilyOptions,
|
|
143
|
+
parseLightFixtureGeneratorRecipe,
|
|
144
|
+
parseLightingStyleGeneratorRecipe,
|
|
145
|
+
registerLightFixtureGeneratorFamily,
|
|
146
|
+
registerLightingStyleGeneratorFamily,
|
|
147
|
+
resolveLightFixtureGeneratorRecipe,
|
|
148
|
+
resolveLightingStyleGeneratorRecipe,
|
|
149
|
+
serializeLightFixtureGeneratorRecipe,
|
|
150
|
+
serializeLightingStyleGeneratorRecipe,
|
|
151
|
+
validateLightFixtureGeneratorRecipe,
|
|
152
|
+
validateLightingStyleGeneratorRecipe,
|
|
153
|
+
} from './lightingGenerator.js';
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
UNREAL_LIGHTING_MANIFEST_SCHEMA_VERSION,
|
|
157
|
+
UNREAL_LIGHTING_MANIFEST_TYPE,
|
|
158
|
+
exportLightingRecipeToUnreal58,
|
|
159
|
+
serializeUnrealLightingManifest,
|
|
160
|
+
threePositionToUnreal,
|
|
161
|
+
} from './unrealExport.js';
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLightColor,
|
|
3
|
+
createLightIntensity,
|
|
4
|
+
getDefaultIntensityUnit,
|
|
5
|
+
} from './colorIntensity.js';
|
|
6
|
+
import {
|
|
7
|
+
clamp,
|
|
8
|
+
createValidationResult,
|
|
9
|
+
finite,
|
|
10
|
+
isPlainObject,
|
|
11
|
+
mergePlain,
|
|
12
|
+
slug,
|
|
13
|
+
vector,
|
|
14
|
+
} from './utils.js';
|
|
15
|
+
|
|
16
|
+
/** Portable light families supported by the v1 recipe schema. */
|
|
17
|
+
export const LIGHT_TYPES = Object.freeze([
|
|
18
|
+
'ambient',
|
|
19
|
+
'hemisphere',
|
|
20
|
+
'directional',
|
|
21
|
+
'point',
|
|
22
|
+
'spot',
|
|
23
|
+
'rectArea',
|
|
24
|
+
'discArea',
|
|
25
|
+
'tubeArea',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export const SHADOW_CAPABLE_LIGHT_TYPES = Object.freeze(['directional', 'point', 'spot']);
|
|
29
|
+
export const COOKIE_CAPABLE_LIGHT_TYPES = Object.freeze(['spot']);
|
|
30
|
+
|
|
31
|
+
const DEFAULT_POSITIONS = Object.freeze({
|
|
32
|
+
ambient: Object.freeze([0, 0, 0]),
|
|
33
|
+
directional: Object.freeze([8, 12, 6]),
|
|
34
|
+
discArea: Object.freeze([0, 2, 0]),
|
|
35
|
+
hemisphere: Object.freeze([0, 1, 0]),
|
|
36
|
+
point: Object.freeze([0, 2, 0]),
|
|
37
|
+
rectArea: Object.freeze([0, 2, 0]),
|
|
38
|
+
spot: Object.freeze([0, 3, 0]),
|
|
39
|
+
tubeArea: Object.freeze([0, 2, 0]),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const DEFAULT_TARGETS = Object.freeze({
|
|
43
|
+
ambient: Object.freeze([0, 0, 0]),
|
|
44
|
+
directional: Object.freeze([0, 0, 0]),
|
|
45
|
+
discArea: Object.freeze([0, 0, 0]),
|
|
46
|
+
hemisphere: Object.freeze([0, 0, 0]),
|
|
47
|
+
point: Object.freeze([0, 0, 0]),
|
|
48
|
+
rectArea: Object.freeze([0, 0, 0]),
|
|
49
|
+
spot: Object.freeze([0, 0, 0]),
|
|
50
|
+
tubeArea: Object.freeze([0, 0, 0]),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function normalizeLayers(value) {
|
|
54
|
+
const source = Array.isArray(value) ? value : [0];
|
|
55
|
+
return [...new Set(source.map(Number)
|
|
56
|
+
.filter((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 31))]
|
|
57
|
+
.sort((a, b) => a - b);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeTags(value) {
|
|
61
|
+
if (!Array.isArray(value)) return [];
|
|
62
|
+
return [...new Set(value.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Creates a portable cookie reference. Runtime textures are intentionally not serialized. */
|
|
66
|
+
export function createLightCookie(value = null) {
|
|
67
|
+
if (!isPlainObject(value)) return null;
|
|
68
|
+
const uri = String(value.uri ?? value.url ?? '').trim() || null;
|
|
69
|
+
const key = String(value.key ?? '').trim() || null;
|
|
70
|
+
if (!uri && !key) return null;
|
|
71
|
+
return {
|
|
72
|
+
channel: ['r', 'g', 'b', 'a', 'rgb'].includes(value.channel) ? value.channel : 'rgb',
|
|
73
|
+
intensity: Math.max(finite(value.intensity, 1), 0),
|
|
74
|
+
key,
|
|
75
|
+
uri,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Creates a portable IES photometric-profile reference for host adapters. */
|
|
80
|
+
export function createLightIesProfile(value = null) {
|
|
81
|
+
if (!isPlainObject(value)) return null;
|
|
82
|
+
const uri = String(value.uri ?? value.url ?? '').trim() || null;
|
|
83
|
+
const key = String(value.key ?? '').trim() || null;
|
|
84
|
+
if (!uri && !key) return null;
|
|
85
|
+
return {
|
|
86
|
+
intensity: Math.max(finite(value.intensity, 1), 0),
|
|
87
|
+
key,
|
|
88
|
+
uri,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Normalizes portable include/exclude tags used by host light-linking adapters. */
|
|
93
|
+
export function createLightLinking(value = null) {
|
|
94
|
+
const source = isPlainObject(value) ? value : {};
|
|
95
|
+
return {
|
|
96
|
+
excludeTags: normalizeTags(source.excludeTags ?? source.exclude),
|
|
97
|
+
includeTags: normalizeTags(source.includeTags ?? source.include),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Normalizes per-light shadow authoring without allocating a shadow map. */
|
|
102
|
+
export function createLightShadow(value = null, type = 'point') {
|
|
103
|
+
const source = isPlainObject(value) ? value : {};
|
|
104
|
+
const supported = SHADOW_CAPABLE_LIGHT_TYPES.includes(type);
|
|
105
|
+
return {
|
|
106
|
+
bias: finite(source.bias, -0.0001),
|
|
107
|
+
enabled: supported && Boolean(source.enabled),
|
|
108
|
+
extent: Math.max(finite(source.extent, 40), 0.01),
|
|
109
|
+
far: Math.max(finite(source.far, type === 'directional' ? 250 : 50), 0.01),
|
|
110
|
+
mapSize: 2 ** Math.round(Math.log2(clamp(finite(source.mapSize, 1024), 128, 8192))),
|
|
111
|
+
near: Math.max(finite(source.near, 0.1), 0.001),
|
|
112
|
+
normalBias: Math.max(finite(source.normalBias, 0.01), 0),
|
|
113
|
+
priority: finite(source.priority, 0),
|
|
114
|
+
radius: Math.max(finite(source.radius, 1), 0),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Normalizes toon-specific metadata preserved for material adapters. */
|
|
119
|
+
export function createLightArtisticSettings(value = null) {
|
|
120
|
+
const source = isPlainObject(value) ? value : {};
|
|
121
|
+
return {
|
|
122
|
+
bandSoftness: clamp(finite(source.bandSoftness, 0.08), 0, 1),
|
|
123
|
+
diffuseMultiplier: Math.max(finite(source.diffuseMultiplier, 1), 0),
|
|
124
|
+
rimInfluence: clamp(finite(source.rimInfluence, 0), 0, 1),
|
|
125
|
+
role: ['key', 'fill', 'rim', 'ambient', 'practical', 'effect'].includes(source.role)
|
|
126
|
+
? source.role
|
|
127
|
+
: 'practical',
|
|
128
|
+
shadowTint: source.shadowTint === null || source.shadowTint === undefined
|
|
129
|
+
? null
|
|
130
|
+
: createLightColor(source.shadowTint),
|
|
131
|
+
specularMultiplier: Math.max(finite(source.specularMultiplier, 1), 0),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates a complete JSON-compatible light descriptor.
|
|
137
|
+
*
|
|
138
|
+
* @param {string|object} typeOrOptions Light type or an object containing `type`.
|
|
139
|
+
* @param {object} [options] Partial descriptor when the first argument is a type.
|
|
140
|
+
*/
|
|
141
|
+
export function createLightDescriptor(typeOrOptions = 'point', options = {}) {
|
|
142
|
+
const source = isPlainObject(typeOrOptions)
|
|
143
|
+
? typeOrOptions
|
|
144
|
+
: isPlainObject(options) ? { ...options, type: typeOrOptions } : { type: typeOrOptions };
|
|
145
|
+
const type = LIGHT_TYPES.includes(source.type) ? source.type : 'point';
|
|
146
|
+
const name = String(source.name ?? `${type[0].toUpperCase()}${type.slice(1)} Light`);
|
|
147
|
+
const id = slug(source.id ?? name, type);
|
|
148
|
+
const angle = clamp(finite(source.angle, Math.PI / 4), 0.001, Math.PI / 2);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
angle,
|
|
152
|
+
artistic: createLightArtisticSettings(source.artistic),
|
|
153
|
+
castShadow: SHADOW_CAPABLE_LIGHT_TYPES.includes(type) && Boolean(source.castShadow ?? source.shadow?.enabled),
|
|
154
|
+
color: createLightColor(source.color),
|
|
155
|
+
cookie: COOKIE_CAPABLE_LIGHT_TYPES.includes(type) ? createLightCookie(source.cookie) : null,
|
|
156
|
+
decay: Math.max(finite(source.decay, 2), 0),
|
|
157
|
+
distance: Math.max(finite(source.distance, 0), 0),
|
|
158
|
+
enabled: source.enabled === undefined ? true : Boolean(source.enabled),
|
|
159
|
+
groundColor: createLightColor(source.groundColor ?? [0.18, 0.22, 0.34]),
|
|
160
|
+
height: Math.max(finite(source.height, 1), 0.001),
|
|
161
|
+
id,
|
|
162
|
+
intensity: createLightIntensity(type, source.intensity),
|
|
163
|
+
ies: ['point', 'spot'].includes(type) ? createLightIesProfile(source.ies) : null,
|
|
164
|
+
layers: normalizeLayers(source.layers),
|
|
165
|
+
linking: createLightLinking(source.linking),
|
|
166
|
+
maxDistance: Math.max(finite(source.maxDistance, source.distance || 0), 0),
|
|
167
|
+
name,
|
|
168
|
+
penumbra: clamp(finite(source.penumbra, 0.25), 0, 1),
|
|
169
|
+
position: vector(source.position, DEFAULT_POSITIONS[type], 3),
|
|
170
|
+
priority: finite(source.priority, 0),
|
|
171
|
+
shadow: createLightShadow({ ...source.shadow, enabled: source.castShadow ?? source.shadow?.enabled }, type),
|
|
172
|
+
tags: normalizeTags(source.tags),
|
|
173
|
+
target: vector(source.target, DEFAULT_TARGETS[type], 3),
|
|
174
|
+
type,
|
|
175
|
+
userData: isPlainObject(source.userData) ? mergePlain({}, source.userData) : {},
|
|
176
|
+
width: Math.max(finite(source.width, 1), 0.001),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Re-normalizes a descriptor after applying a deep partial update. */
|
|
181
|
+
export function mergeLightDescriptor(descriptor, overrides = {}) {
|
|
182
|
+
return createLightDescriptor(mergePlain(descriptor, overrides));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const createAmbientLightDescriptor = (options = {}) => createLightDescriptor('ambient', options);
|
|
186
|
+
export const createHemisphereLightDescriptor = (options = {}) => createLightDescriptor('hemisphere', options);
|
|
187
|
+
export const createDirectionalLightDescriptor = (options = {}) => createLightDescriptor('directional', options);
|
|
188
|
+
export const createPointLightDescriptor = (options = {}) => createLightDescriptor('point', options);
|
|
189
|
+
export const createSpotLightDescriptor = (options = {}) => createLightDescriptor('spot', options);
|
|
190
|
+
export const createRectAreaLightDescriptor = (options = {}) => createLightDescriptor('rectArea', options);
|
|
191
|
+
export const createDiscAreaLightDescriptor = (options = {}) => createLightDescriptor('discArea', options);
|
|
192
|
+
export const createTubeAreaLightDescriptor = (options = {}) => createLightDescriptor('tubeArea', options);
|
|
193
|
+
|
|
194
|
+
/** Performs structural validation without mutating or coercing the descriptor. */
|
|
195
|
+
export function validateLightDescriptor(value, { path = 'light' } = {}) {
|
|
196
|
+
const errors = [];
|
|
197
|
+
const warnings = [];
|
|
198
|
+
if (!isPlainObject(value)) {
|
|
199
|
+
errors.push({ message: 'must be an object', path });
|
|
200
|
+
return createValidationResult(errors, warnings);
|
|
201
|
+
}
|
|
202
|
+
if (!LIGHT_TYPES.includes(value.type)) {
|
|
203
|
+
errors.push({ message: `type must be one of ${LIGHT_TYPES.join(', ')}`, path: `${path}.type` });
|
|
204
|
+
}
|
|
205
|
+
if (typeof value.id !== 'string' || !value.id.trim()) {
|
|
206
|
+
errors.push({ message: 'must be a non-empty string', path: `${path}.id` });
|
|
207
|
+
}
|
|
208
|
+
if (value.position !== undefined && (!Array.isArray(value.position)
|
|
209
|
+
|| value.position.length < 3
|
|
210
|
+
|| !value.position.slice(0, 3).every((entry) => Number.isFinite(Number(entry))))) {
|
|
211
|
+
errors.push({ message: 'must be a finite [x, y, z] array', path: `${path}.position` });
|
|
212
|
+
}
|
|
213
|
+
if (value.target !== undefined && (!Array.isArray(value.target)
|
|
214
|
+
|| value.target.length < 3
|
|
215
|
+
|| !value.target.slice(0, 3).every((entry) => Number.isFinite(Number(entry))))) {
|
|
216
|
+
errors.push({ message: 'must be a finite [x, y, z] array', path: `${path}.target` });
|
|
217
|
+
}
|
|
218
|
+
if (value.intensity !== undefined && !isPlainObject(value.intensity) && !Number.isFinite(value.intensity)) {
|
|
219
|
+
errors.push({ message: 'must be a number or intensity object', path: `${path}.intensity` });
|
|
220
|
+
}
|
|
221
|
+
if (isPlainObject(value.intensity)
|
|
222
|
+
&& value.intensity.unit !== undefined
|
|
223
|
+
&& !['unitless', 'lux', 'candela', 'lumens', 'nits'].includes(value.intensity.unit)) {
|
|
224
|
+
errors.push({ message: 'has an unsupported physical unit', path: `${path}.intensity.unit` });
|
|
225
|
+
}
|
|
226
|
+
if (value.cookie && !COOKIE_CAPABLE_LIGHT_TYPES.includes(value.type)) {
|
|
227
|
+
warnings.push({ message: 'cookies are currently realized only for spot lights', path: `${path}.cookie` });
|
|
228
|
+
}
|
|
229
|
+
if (value.ies && !['point', 'spot'].includes(value.type)) {
|
|
230
|
+
warnings.push({ message: 'IES profiles are portable metadata only for point and spot lights', path: `${path}.ies` });
|
|
231
|
+
}
|
|
232
|
+
if (value.linking !== undefined && !isPlainObject(value.linking)) {
|
|
233
|
+
errors.push({ message: 'must be an include/exclude tag object', path: `${path}.linking` });
|
|
234
|
+
}
|
|
235
|
+
if ((value.castShadow || value.shadow?.enabled) && !SHADOW_CAPABLE_LIGHT_TYPES.includes(value.type)) {
|
|
236
|
+
warnings.push({ message: 'this light type cannot cast Three.js shadows', path: `${path}.shadow` });
|
|
237
|
+
}
|
|
238
|
+
const expectedUnit = getDefaultIntensityUnit(value.type);
|
|
239
|
+
if (isPlainObject(value.intensity)
|
|
240
|
+
&& value.intensity.unit
|
|
241
|
+
&& value.intensity.unit !== expectedUnit
|
|
242
|
+
&& value.intensity.unit !== 'lumens') {
|
|
243
|
+
warnings.push({
|
|
244
|
+
message: `${value.intensity.unit} is converted approximately; ${expectedUnit} is native for ${value.type}`,
|
|
245
|
+
path: `${path}.intensity.unit`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return createValidationResult(errors, warnings);
|
|
249
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { LIGHT_TYPES } from './lightDescriptors.js';
|
|
2
|
+
import { cloneJson, finite, isPlainObject } from './utils.js';
|
|
3
|
+
|
|
4
|
+
function detectBackend(renderer) {
|
|
5
|
+
if (renderer?.isWebGPURenderer) return 'webgpu';
|
|
6
|
+
if (renderer?.isWebGLRenderer && renderer.capabilities?.isWebGL2) return 'webgl2';
|
|
7
|
+
if (renderer?.isWebGLRenderer) return 'webgl1';
|
|
8
|
+
return 'unknown';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reports the portable Three.js lighting features available to this module.
|
|
13
|
+
* This is a capability description, not a GPU benchmark or shader-limit test.
|
|
14
|
+
*/
|
|
15
|
+
export function createLightingCapabilityReport(options = {}) {
|
|
16
|
+
const source = isPlainObject(options) ? options : {};
|
|
17
|
+
const renderer = source.renderer ?? null;
|
|
18
|
+
const backend = source.backend ?? detectBackend(renderer);
|
|
19
|
+
const maxTextureUnits = Math.max(finite(renderer?.capabilities?.maxTextures, 0), 0);
|
|
20
|
+
const maxTextureSize = Math.max(finite(renderer?.capabilities?.maxTextureSize, 0), 0);
|
|
21
|
+
const warnings = [];
|
|
22
|
+
|
|
23
|
+
if (backend === 'unknown') warnings.push('No renderer was supplied; runtime capabilities are conservative estimates.');
|
|
24
|
+
if (backend === 'webgl1') warnings.push('WebGL1 is outside ToonLab lighting v1 support targets.');
|
|
25
|
+
if (backend === 'webgl2') warnings.push('Large light counts remain shader- and material-dependent on WebGL2; use a measured quality profile.');
|
|
26
|
+
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
backend,
|
|
29
|
+
features: Object.freeze({
|
|
30
|
+
areaLights: Object.freeze({
|
|
31
|
+
discArea: 'rect-area-approximation',
|
|
32
|
+
rectArea: true,
|
|
33
|
+
tubeArea: 'rect-area-approximation',
|
|
34
|
+
}),
|
|
35
|
+
cookies: Object.freeze({ spot: true }),
|
|
36
|
+
iesProfiles: 'metadata-only',
|
|
37
|
+
lightLinking: 'three-layers-plus-metadata',
|
|
38
|
+
lumen: false,
|
|
39
|
+
megaLights: false,
|
|
40
|
+
manyLightRenderer: false,
|
|
41
|
+
shadows: Object.freeze({
|
|
42
|
+
ambient: false,
|
|
43
|
+
directional: true,
|
|
44
|
+
discArea: false,
|
|
45
|
+
hemisphere: false,
|
|
46
|
+
point: true,
|
|
47
|
+
rectArea: false,
|
|
48
|
+
spot: true,
|
|
49
|
+
tubeArea: false,
|
|
50
|
+
}),
|
|
51
|
+
}),
|
|
52
|
+
limits: Object.freeze({
|
|
53
|
+
maxTextureSize,
|
|
54
|
+
maxTextureUnits,
|
|
55
|
+
recipeLightCount: 1024,
|
|
56
|
+
}),
|
|
57
|
+
renderer: renderer ? renderer.constructor?.name ?? 'Renderer' : null,
|
|
58
|
+
supportedLightTypes: LIGHT_TYPES,
|
|
59
|
+
warnings: Object.freeze(warnings),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Returns one type-specific view of a capability report. */
|
|
64
|
+
export function getLightingTypeCapability(type, report = createLightingCapabilityReport()) {
|
|
65
|
+
if (!LIGHT_TYPES.includes(type)) return null;
|
|
66
|
+
const area = report.features.areaLights[type];
|
|
67
|
+
return {
|
|
68
|
+
areaRealization: area ?? null,
|
|
69
|
+
cookies: Boolean(report.features.cookies[type]),
|
|
70
|
+
iesProfiles: ['point', 'spot'].includes(type) ? report.features.iesProfiles : false,
|
|
71
|
+
shadows: Boolean(report.features.shadows[type]),
|
|
72
|
+
type,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Creates a mutable JSON-safe snapshot suitable for logs and diagnostics UIs. */
|
|
77
|
+
export function snapshotLightingCapabilities(report) {
|
|
78
|
+
return cloneJson(report ?? createLightingCapabilityReport());
|
|
79
|
+
}
|