@call-me-sensei/toonlab 0.1.1
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/ATTRIBUTION.md +40 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/package.json +97 -0
- package/src/character/characterRig.js +353 -0
- package/src/character/freestyleSwimClip.js +541 -0
- package/src/character/index.js +4 -0
- package/src/character/modelLoader.js +433 -0
- package/src/core/materialRoles.js +438 -0
- package/src/core/presetDocuments.js +110 -0
- package/src/core/shaderBackend.js +24 -0
- package/src/debrisgen/debrisFields.js +544 -0
- package/src/debrisgen/debrisGenerator.js +1929 -0
- package/src/debrisgen/debrisPalettes.js +71 -0
- package/src/debrisgen/debrisPhysics.js +198 -0
- package/src/debrisgen/debrisPresets.js +155 -0
- package/src/debrisgen/debrisSettings.js +333 -0
- package/src/debrisgen/debrisTextures.js +380 -0
- package/src/debrisgen/index.js +5 -0
- package/src/debug/fieldValues.js +95 -0
- package/src/debug/index.js +7 -0
- package/src/debug/settingsPanel.js +170 -0
- package/src/environment/environmentAmbientProbe.js +160 -0
- package/src/environment/environmentMaterialAdapter.js +336 -0
- package/src/environment/environmentMaterialClassifier.js +178 -0
- package/src/environment/environmentPlanarReflection.js +201 -0
- package/src/environment/environmentPresets.js +443 -0
- package/src/environment/environmentRigs.js +684 -0
- package/src/environment/environmentSettings.js +504 -0
- package/src/environment/environmentShaderMaterials.js +318 -0
- package/src/environment/environmentSunShadowPass.js +268 -0
- package/src/environment/environmentTextureResolver.js +161 -0
- package/src/environment/environmentTimeOfDay.js +167 -0
- package/src/environment/environmentVertexAo.js +188 -0
- package/src/environment/index.js +8 -0
- package/src/environment/scanAssetStylize.js +103 -0
- package/src/index.js +21 -0
- package/src/loaders/index.js +1 -0
- package/src/post/index.js +2 -0
- package/src/post/postProcessing.js +1087 -0
- package/src/rockgen/export/glbExport.js +177 -0
- package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
- package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
- package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
- package/src/rockgen/index.js +14 -0
- package/src/rockgen/mesh/meshAttributes.js +374 -0
- package/src/rockgen/mesh/meshDocument.js +158 -0
- package/src/rockgen/mesh/surfaceNets.js +329 -0
- package/src/rockgen/noise/cellularNoise3.js +92 -0
- package/src/rockgen/noise/prng.js +62 -0
- package/src/rockgen/noise/simplexNoise3.js +89 -0
- package/src/rockgen/noise/valueNoise3.js +87 -0
- package/src/rockgen/rockDocument.js +255 -0
- package/src/rockgen/rockHelpers.js +8 -0
- package/src/rockgen/rockgenPresets.js +559 -0
- package/src/rockgen/rockgenSettings.js +756 -0
- package/src/rockgen/sdf/fieldCompiler.js +544 -0
- package/src/rockgen/sdf/sculptEdits.js +66 -0
- package/src/rockgen/sdf/sdfModifiers.js +45 -0
- package/src/rockgen/sdf/sdfOps.js +37 -0
- package/src/rockgen/sdf/sdfPrimitives.js +88 -0
- package/src/shaders-tsl/anime.js +963 -0
- package/src/shaders-tsl/chunks/character-color.js +64 -0
- package/src/shaders-tsl/chunks/character-highlights.js +174 -0
- package/src/shaders-tsl/chunks/character-lighting.js +309 -0
- package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
- package/src/shaders-tsl/chunks/character-roles.js +52 -0
- package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
- package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
- package/src/shaders-tsl/chunks/character-skinning.js +140 -0
- package/src/shaders-tsl/chunks/environment-color.js +47 -0
- package/src/shaders-tsl/chunks/environment-debug.js +79 -0
- package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
- package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
- package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
- package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
- package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
- package/src/shaders-tsl/chunks/water-color.js +190 -0
- package/src/shaders-tsl/chunks/water-common.js +124 -0
- package/src/shaders-tsl/chunks/water-foam.js +93 -0
- package/src/shaders-tsl/chunks/water-lighting.js +116 -0
- package/src/shaders-tsl/chunks/water-ripple.js +64 -0
- package/src/shaders-tsl/chunks/water-waves.js +100 -0
- package/src/shaders-tsl/environment-ao-overlay.js +60 -0
- package/src/shaders-tsl/environment.js +680 -0
- package/src/shaders-tsl/flower.js +267 -0
- package/src/shaders-tsl/grass.js +193 -0
- package/src/shaders-tsl/post-composite.js +461 -0
- package/src/shaders-tsl/sky.js +175 -0
- package/src/shaders-tsl/tree-leaf.js +264 -0
- package/src/shaders-tsl/water-breaker.js +423 -0
- package/src/shaders-tsl/water-kelp.js +171 -0
- package/src/shaders-tsl/water-rain.js +107 -0
- package/src/shaders-tsl/water-simulation.js +140 -0
- package/src/shaders-tsl/water-splash.js +298 -0
- package/src/shaders-tsl/water.js +509 -0
- package/src/sky/index.js +2 -0
- package/src/sky/stylizedSky.js +341 -0
- package/src/toon/characterRenderPasses.js +646 -0
- package/src/toon/index.js +4 -0
- package/src/toon/settings/alphaSettings.js +170 -0
- package/src/toon/settings/averageShadowSettings.js +181 -0
- package/src/toon/settings/baseTextureSettings.js +133 -0
- package/src/toon/settings/celShadeSettings.js +96 -0
- package/src/toon/settings/contactShadowSettings.js +114 -0
- package/src/toon/settings/eyeHighlightSettings.js +187 -0
- package/src/toon/settings/faceLightingSettings.js +135 -0
- package/src/toon/settings/furSettings.js +112 -0
- package/src/toon/settings/glitterSettings.js +102 -0
- package/src/toon/settings/hairHighlightSettings.js +325 -0
- package/src/toon/settings/indirectLightSettings.js +244 -0
- package/src/toon/settings/localLightSettings.js +176 -0
- package/src/toon/settings/materialMapSettings.js +299 -0
- package/src/toon/settings/outlineSettings.js +263 -0
- package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
- package/src/toon/settings/rimLightSettings.js +268 -0
- package/src/toon/settings/sceneShadowSettings.js +147 -0
- package/src/toon/settings/selfShadowSettings.js +221 -0
- package/src/toon/settings/shadowColorSettings.js +238 -0
- package/src/toon/settings/skinToneSettings.js +150 -0
- package/src/toon/settings/specularSettings.js +314 -0
- package/src/toon/settings/stickerSettings.js +102 -0
- package/src/toon/toonMaterialAdapter.js +2235 -0
- package/src/toon/toonSettings.js +860 -0
- package/src/vegetation/flowerSpecies.js +260 -0
- package/src/vegetation/index.js +10 -0
- package/src/vegetation/stylizedBush.js +136 -0
- package/src/vegetation/stylizedFlower.js +213 -0
- package/src/vegetation/stylizedFlowers.js +297 -0
- package/src/vegetation/stylizedGrass.js +491 -0
- package/src/vegetation/stylizedTree.js +2449 -0
- package/src/vegetation/stylizedTreeFoliage.js +691 -0
- package/src/vegetation/treeExport.js +300 -0
- package/src/vegetation/treeRecipe.js +647 -0
- package/src/water/index.js +2 -0
- package/src/water/water.js +20 -0
- package/src/water/waterBreakerSystem.js +591 -0
- package/src/water/waterInteraction.js +193 -0
- package/src/water/waterMaterial.js +263 -0
- package/src/water/waterRain.js +136 -0
- package/src/water/waterRippleSimulation.js +227 -0
- package/src/water/waterScenePasses.js +415 -0
- package/src/water/waterSettings.js +1138 -0
- package/src/water/waterSplashSystem.js +251 -0
- package/src/water/waterSurface.js +475 -0
- package/src/water/waterVegetation.js +83 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import { setEnvironmentPlanarReflection } from './environmentShaderMaterials.js';
|
|
4
|
+
|
|
5
|
+
// One planar reflection pass for the dominant floor plane — the glossy-lobby
|
|
6
|
+
// look. Renders the scene mirrored across the floor into a half-res target
|
|
7
|
+
// each frame and publishes it to every glossFloor-role environment material
|
|
8
|
+
// through the shared planarReflectionMap/planarReflectionMatrix uniforms.
|
|
9
|
+
//
|
|
10
|
+
// Costs a second scene render while enabled; keep it preset/opt-in driven.
|
|
11
|
+
|
|
12
|
+
// Finds the world Y of the largest up-facing surface near the bottom of the
|
|
13
|
+
// environment — good enough to auto-place the reflection plane when the
|
|
14
|
+
// integrator does not pass floorY explicitly.
|
|
15
|
+
export function detectEnvironmentFloorY(root, environmentBox = null) {
|
|
16
|
+
const box = environmentBox ?? new THREE.Box3().setFromObject(root);
|
|
17
|
+
return box.min.y;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createEnvironmentPlanarReflection({
|
|
21
|
+
renderer,
|
|
22
|
+
scene,
|
|
23
|
+
camera,
|
|
24
|
+
floorY = 0,
|
|
25
|
+
normal = new THREE.Vector3(0, 1, 0),
|
|
26
|
+
resolutionScale = 0.5,
|
|
27
|
+
clipBias = 0.003,
|
|
28
|
+
} = {}) {
|
|
29
|
+
const planeNormal = normal.clone().normalize();
|
|
30
|
+
const planePoint = new THREE.Vector3(0, floorY, 0);
|
|
31
|
+
const reflectorPlane = new THREE.Plane();
|
|
32
|
+
const virtualCamera = new THREE.PerspectiveCamera();
|
|
33
|
+
const textureMatrix = new THREE.Matrix4();
|
|
34
|
+
|
|
35
|
+
let renderTarget = new THREE.WebGLRenderTarget(2, 2, {
|
|
36
|
+
minFilter: THREE.LinearFilter,
|
|
37
|
+
magFilter: THREE.LinearFilter,
|
|
38
|
+
});
|
|
39
|
+
let enabled = true;
|
|
40
|
+
|
|
41
|
+
const reflectorWorldPosition = new THREE.Vector3();
|
|
42
|
+
const cameraWorldPosition = new THREE.Vector3();
|
|
43
|
+
const lookAtPosition = new THREE.Vector3();
|
|
44
|
+
const view = new THREE.Vector3();
|
|
45
|
+
const target = new THREE.Vector3();
|
|
46
|
+
const clipPlane = new THREE.Vector4();
|
|
47
|
+
const q = new THREE.Vector4();
|
|
48
|
+
|
|
49
|
+
function resizeToRenderer() {
|
|
50
|
+
const size = renderer.getSize(new THREE.Vector2());
|
|
51
|
+
const width = Math.max(2, Math.floor(size.x * renderer.getPixelRatio() * resolutionScale));
|
|
52
|
+
const height = Math.max(2, Math.floor(size.y * renderer.getPixelRatio() * resolutionScale));
|
|
53
|
+
if (renderTarget.width !== width || renderTarget.height !== height) {
|
|
54
|
+
renderTarget.setSize(width, height);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Meshes that consume the reflection must not draw while it renders (WebGL
|
|
59
|
+
// feedback loop); the oblique clip plane would discard their fragments but
|
|
60
|
+
// the texture would still be bound.
|
|
61
|
+
function collectConsumerMeshes() {
|
|
62
|
+
const consumers = [];
|
|
63
|
+
scene.traverse((obj) => {
|
|
64
|
+
if (!obj.isMesh || !obj.material) return;
|
|
65
|
+
const materials = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
66
|
+
if (materials.some((mat) => mat?.defines?.USE_ENV_PLANAR_REFLECTION
|
|
67
|
+
|| mat?.userData?.environmentRole === 'glossFloor')) {
|
|
68
|
+
consumers.push(obj);
|
|
69
|
+
}
|
|
70
|
+
if (obj.userData?.environmentReflectionExclude) consumers.push(obj);
|
|
71
|
+
});
|
|
72
|
+
return consumers;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function update() {
|
|
76
|
+
if (!enabled) return;
|
|
77
|
+
|
|
78
|
+
camera.getWorldPosition(cameraWorldPosition);
|
|
79
|
+
reflectorWorldPosition.copy(planePoint);
|
|
80
|
+
view.subVectors(reflectorWorldPosition, cameraWorldPosition);
|
|
81
|
+
// Camera below the floor plane: nothing sensible to mirror.
|
|
82
|
+
if (view.dot(planeNormal) > 0) return;
|
|
83
|
+
|
|
84
|
+
resizeToRenderer();
|
|
85
|
+
|
|
86
|
+
view.reflect(planeNormal).negate();
|
|
87
|
+
view.add(reflectorWorldPosition);
|
|
88
|
+
|
|
89
|
+
lookAtPosition.set(0, 0, -1).applyQuaternion(camera.quaternion).add(cameraWorldPosition);
|
|
90
|
+
target.subVectors(reflectorWorldPosition, lookAtPosition);
|
|
91
|
+
target.reflect(planeNormal).negate();
|
|
92
|
+
target.add(reflectorWorldPosition);
|
|
93
|
+
|
|
94
|
+
virtualCamera.coordinateSystem = camera.coordinateSystem;
|
|
95
|
+
virtualCamera.position.copy(view);
|
|
96
|
+
virtualCamera.up.set(0, 1, 0).applyQuaternion(camera.quaternion).reflect(planeNormal);
|
|
97
|
+
virtualCamera.lookAt(target);
|
|
98
|
+
virtualCamera.near = camera.near;
|
|
99
|
+
virtualCamera.far = camera.far;
|
|
100
|
+
virtualCamera.updateMatrixWorld();
|
|
101
|
+
virtualCamera.projectionMatrix.copy(camera.projectionMatrix);
|
|
102
|
+
|
|
103
|
+
// The classic pipeline samples render targets in GL orientation; the node
|
|
104
|
+
// backends sample them with the origin flipped (same convention gap the
|
|
105
|
+
// shared sun-shadow matrix adjusts for), so fold uv.y -> 1-uv.y into the
|
|
106
|
+
// bias matrix there. Projective form: y' = -y + w.
|
|
107
|
+
if (renderer.isWebGPURenderer) {
|
|
108
|
+
textureMatrix.set(
|
|
109
|
+
0.5, 0.0, 0.0, 0.5,
|
|
110
|
+
0.0, -0.5, 0.0, 0.5,
|
|
111
|
+
0.0, 0.0, 0.5, 0.5,
|
|
112
|
+
0.0, 0.0, 0.0, 1.0,
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
textureMatrix.set(
|
|
116
|
+
0.5, 0.0, 0.0, 0.5,
|
|
117
|
+
0.0, 0.5, 0.0, 0.5,
|
|
118
|
+
0.0, 0.0, 0.5, 0.5,
|
|
119
|
+
0.0, 0.0, 0.0, 1.0,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
textureMatrix.multiply(virtualCamera.projectionMatrix);
|
|
123
|
+
textureMatrix.multiply(virtualCamera.matrixWorldInverse);
|
|
124
|
+
|
|
125
|
+
// Oblique near-plane clip against the reflection plane so geometry below
|
|
126
|
+
// the floor never leaks into the mirror image. The node renderer needs
|
|
127
|
+
// three's ReflectorNode variant of the Lengyel formula: 1/dot scaling and
|
|
128
|
+
// a coordinate-system-dependent third row (WebGPU clip z is 0..1, so the
|
|
129
|
+
// GL +1.0 row-4 fold-in must not be applied there).
|
|
130
|
+
reflectorPlane.setFromNormalAndCoplanarPoint(planeNormal, reflectorWorldPosition);
|
|
131
|
+
reflectorPlane.applyMatrix4(virtualCamera.matrixWorldInverse);
|
|
132
|
+
clipPlane.set(reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant);
|
|
133
|
+
const projectionMatrix = virtualCamera.projectionMatrix;
|
|
134
|
+
q.x = (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0];
|
|
135
|
+
q.y = (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5];
|
|
136
|
+
q.z = -1.0;
|
|
137
|
+
q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];
|
|
138
|
+
if (renderer.isWebGPURenderer) {
|
|
139
|
+
clipPlane.multiplyScalar(1.0 / clipPlane.dot(q));
|
|
140
|
+
projectionMatrix.elements[10] = renderer.coordinateSystem === THREE.WebGPUCoordinateSystem
|
|
141
|
+
? clipPlane.z - clipBias
|
|
142
|
+
: clipPlane.z + 1.0 - clipBias;
|
|
143
|
+
} else {
|
|
144
|
+
clipPlane.multiplyScalar(2.0 / clipPlane.dot(q));
|
|
145
|
+
projectionMatrix.elements[10] = clipPlane.z + 1.0 - clipBias;
|
|
146
|
+
}
|
|
147
|
+
projectionMatrix.elements[2] = clipPlane.x;
|
|
148
|
+
projectionMatrix.elements[6] = clipPlane.y;
|
|
149
|
+
projectionMatrix.elements[14] = clipPlane.w;
|
|
150
|
+
|
|
151
|
+
const consumers = collectConsumerMeshes();
|
|
152
|
+
const previousVisibility = consumers.map((mesh) => mesh.visible);
|
|
153
|
+
consumers.forEach((mesh) => { mesh.visible = false; });
|
|
154
|
+
|
|
155
|
+
const currentRenderTarget = renderer.getRenderTarget();
|
|
156
|
+
const currentXrEnabled = renderer.xr.enabled;
|
|
157
|
+
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
|
|
158
|
+
renderer.xr.enabled = false;
|
|
159
|
+
renderer.shadowMap.autoUpdate = false;
|
|
160
|
+
renderer.setRenderTarget(renderTarget);
|
|
161
|
+
// renderer.state is a classic-WebGL-only surface; the node renderer
|
|
162
|
+
// clears through autoClear inside render() instead.
|
|
163
|
+
if (renderer.state) {
|
|
164
|
+
renderer.state.buffers.depth.setMask(true);
|
|
165
|
+
renderer.clear();
|
|
166
|
+
}
|
|
167
|
+
renderer.render(scene, virtualCamera);
|
|
168
|
+
renderer.xr.enabled = currentXrEnabled;
|
|
169
|
+
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
|
|
170
|
+
renderer.setRenderTarget(currentRenderTarget);
|
|
171
|
+
|
|
172
|
+
consumers.forEach((mesh, index) => { mesh.visible = previousVisibility[index]; });
|
|
173
|
+
|
|
174
|
+
setEnvironmentPlanarReflection({ texture: renderTarget.texture, matrix: textureMatrix });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function setEnabled(value) {
|
|
178
|
+
enabled = Boolean(value);
|
|
179
|
+
if (!enabled) setEnvironmentPlanarReflection({ texture: null });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function setFloorY(value) {
|
|
183
|
+
if (Number.isFinite(value)) planePoint.y = value;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function dispose() {
|
|
187
|
+
setEnvironmentPlanarReflection({ texture: null });
|
|
188
|
+
renderTarget.dispose();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
dispose,
|
|
193
|
+
get enabled() { return enabled; },
|
|
194
|
+
renderTarget: () => renderTarget,
|
|
195
|
+
setEnabled,
|
|
196
|
+
setFloorY,
|
|
197
|
+
texture: () => renderTarget.texture,
|
|
198
|
+
textureMatrix,
|
|
199
|
+
update,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// Named environment presets: one string selects a coherent, reference-checked
|
|
2
|
+
// look — shader features/parameters plus rig hints (sun, lamps, reflection,
|
|
3
|
+
// probe, motes) that scene-level integrations consume. Registry mirrors the
|
|
4
|
+
// character preset helpers in toonSettings.js.
|
|
5
|
+
//
|
|
6
|
+
// 'default' stays an empty override layer so the shipped baseline (the
|
|
7
|
+
// approved Liyue room look) never shifts underneath integrators.
|
|
8
|
+
|
|
9
|
+
import { ENVIRONMENT_SETTING_FIELD_SCHEMA } from './environmentSettings.js';
|
|
10
|
+
|
|
11
|
+
/** Document type tag stamped on shareable environment preset JSON documents. */
|
|
12
|
+
export const ENVIRONMENT_PRESET_DOCUMENT_TYPE = 'toonlab/environment-preset';
|
|
13
|
+
|
|
14
|
+
/** Current schema version for environment preset documents. */
|
|
15
|
+
export const ENVIRONMENT_PRESET_SCHEMA_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
// Rig keys consumed by the shipped scene integrations. Unknown rig keys are
|
|
18
|
+
// kept (integrations may carry custom hints) but flagged with a warning.
|
|
19
|
+
const KNOWN_ENVIRONMENT_RIG_KEYS = Object.freeze(new Set([
|
|
20
|
+
'bakeVertexAo',
|
|
21
|
+
'dustMotes',
|
|
22
|
+
'lampIntensity',
|
|
23
|
+
'planarReflection',
|
|
24
|
+
'probe',
|
|
25
|
+
'spotShadows',
|
|
26
|
+
'sun',
|
|
27
|
+
'timeOfDayHour',
|
|
28
|
+
]));
|
|
29
|
+
|
|
30
|
+
const ENVIRONMENT_PRESETS = new Map();
|
|
31
|
+
|
|
32
|
+
export function registerEnvironmentPreset(name, preset, { overwrite = false } = {}) {
|
|
33
|
+
const key = String(name ?? '').trim();
|
|
34
|
+
if (!key) throw new Error('Environment preset name is required.');
|
|
35
|
+
if (!overwrite && ENVIRONMENT_PRESETS.has(key)) {
|
|
36
|
+
throw new Error(`Environment preset "${key}" is already registered.`);
|
|
37
|
+
}
|
|
38
|
+
ENVIRONMENT_PRESETS.set(key, {
|
|
39
|
+
features: {},
|
|
40
|
+
label: key,
|
|
41
|
+
parameters: {},
|
|
42
|
+
rig: {},
|
|
43
|
+
...preset,
|
|
44
|
+
});
|
|
45
|
+
return key;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeEnvironmentPresetName(name) {
|
|
49
|
+
const key = String(name ?? 'default').trim();
|
|
50
|
+
return ENVIRONMENT_PRESETS.has(key) ? key : 'default';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getEnvironmentPresetOptions() {
|
|
54
|
+
return Array.from(ENVIRONMENT_PRESETS.entries())
|
|
55
|
+
.map(([value, preset]) => ({ label: preset.label, value }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Returns { features, parameters, rig } ready to spread into
|
|
59
|
+
// applyEnvironmentShader options and the rig constructors.
|
|
60
|
+
export function resolveEnvironmentPreset(name) {
|
|
61
|
+
const preset = ENVIRONMENT_PRESETS.get(normalizeEnvironmentPresetName(name));
|
|
62
|
+
return {
|
|
63
|
+
features: { ...preset.features },
|
|
64
|
+
parameters: { ...preset.parameters },
|
|
65
|
+
rig: { ...preset.rig },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isPlainObject(value) {
|
|
70
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cleanObject(value) {
|
|
74
|
+
return isPlainObject(value) ? value : {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function booleanFromValue(value) {
|
|
78
|
+
if (typeof value === 'boolean') return value;
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
const normalized = value.trim().toLowerCase();
|
|
81
|
+
if (['1', 'true', 'on', 'yes'].includes(normalized)) return true;
|
|
82
|
+
if (['0', 'false', 'off', 'no'].includes(normalized)) return false;
|
|
83
|
+
}
|
|
84
|
+
if (value === 0 || value === 1) return value === 1;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function colorArrayFromValue(value) {
|
|
89
|
+
const source = Array.isArray(value)
|
|
90
|
+
? value
|
|
91
|
+
: isPlainObject(value)
|
|
92
|
+
? [value.r, value.g, value.b]
|
|
93
|
+
: null;
|
|
94
|
+
if (!source || source.length < 3) return null;
|
|
95
|
+
const channels = source.slice(0, 3).map(Number);
|
|
96
|
+
return channels.every(Number.isFinite) ? channels : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validates and coerces an environment preset definition against the
|
|
101
|
+
* environment settings schema. Feature values are coerced to booleans and
|
|
102
|
+
* must name known feature toggles; parameter values must be finite numbers
|
|
103
|
+
* (or `[r, g, b]` arrays for color parameters, or `null` for auto) and must
|
|
104
|
+
* name known shader parameters; rig hints must be JSON scalars (boolean,
|
|
105
|
+
* finite number, or string) — unknown rig keys are kept but produce a warning.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} preset Preset definition ({ label, description, features, parameters, rig }).
|
|
108
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[], value: object | null }}
|
|
109
|
+
* `value` is the sanitized `{ label, description, features, parameters, rig }` when `ok`.
|
|
110
|
+
*/
|
|
111
|
+
export function sanitizeEnvironmentPreset(preset) {
|
|
112
|
+
const errors = [];
|
|
113
|
+
const warnings = [];
|
|
114
|
+
const source = cleanObject(preset);
|
|
115
|
+
|
|
116
|
+
const features = {};
|
|
117
|
+
for (const [key, entry] of Object.entries(cleanObject(source.features))) {
|
|
118
|
+
if (!ENVIRONMENT_SETTING_FIELD_SCHEMA.features[key]) {
|
|
119
|
+
errors.push(`Unknown environment feature "${key}".`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const coerced = booleanFromValue(entry);
|
|
123
|
+
if (coerced === null) {
|
|
124
|
+
errors.push(`Environment feature "${key}" must be a boolean.`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
features[key] = coerced;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parameters = {};
|
|
131
|
+
for (const [key, entry] of Object.entries(cleanObject(source.parameters))) {
|
|
132
|
+
const field = ENVIRONMENT_SETTING_FIELD_SCHEMA.parameters[key];
|
|
133
|
+
if (!field) {
|
|
134
|
+
errors.push(`Unknown environment parameter "${key}".`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (entry === null) {
|
|
138
|
+
parameters[key] = null;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (field.type === 'color') {
|
|
142
|
+
const color = colorArrayFromValue(entry);
|
|
143
|
+
if (!color) {
|
|
144
|
+
errors.push(`Environment parameter "${key}" must be an [r, g, b] color array.`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
parameters[key] = color;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const number = typeof entry === 'boolean' ? Number.NaN : Number(entry);
|
|
151
|
+
if (!Number.isFinite(number)) {
|
|
152
|
+
errors.push(`Environment parameter "${key}" must be a finite number.`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
parameters[key] = number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rig = {};
|
|
159
|
+
for (const [key, entry] of Object.entries(cleanObject(source.rig))) {
|
|
160
|
+
const type = typeof entry;
|
|
161
|
+
const isScalar = type === 'boolean'
|
|
162
|
+
|| type === 'string'
|
|
163
|
+
|| (type === 'number' && Number.isFinite(entry));
|
|
164
|
+
if (!isScalar) {
|
|
165
|
+
errors.push(`Environment rig value "${key}" must be a boolean, finite number, or string.`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!KNOWN_ENVIRONMENT_RIG_KEYS.has(key)) {
|
|
169
|
+
warnings.push(`Unknown environment rig key "${key}" was kept as-is.`);
|
|
170
|
+
}
|
|
171
|
+
rig[key] = entry;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const ok = errors.length === 0;
|
|
175
|
+
return {
|
|
176
|
+
errors,
|
|
177
|
+
ok,
|
|
178
|
+
value: ok
|
|
179
|
+
? {
|
|
180
|
+
description: String(source.description ?? ''),
|
|
181
|
+
features,
|
|
182
|
+
label: String(source.label ?? ''),
|
|
183
|
+
parameters,
|
|
184
|
+
rig,
|
|
185
|
+
}
|
|
186
|
+
: null,
|
|
187
|
+
warnings,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Serializes a registered environment preset into a shareable JSON document
|
|
193
|
+
* (`{ type, schemaVersion, id, label, description, preset }`). The registered
|
|
194
|
+
* preset itself is untouched; `label`/`description` overrides only affect the
|
|
195
|
+
* emitted document.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} name Registered preset name (e.g. 'interiorNight').
|
|
198
|
+
* @param {{ label?: string, description?: string }} [overrides] Optional label/description overrides.
|
|
199
|
+
* @returns {object} Environment preset document ready for `JSON.stringify`.
|
|
200
|
+
* @throws {Error} If the preset is not registered or fails sanitization.
|
|
201
|
+
*/
|
|
202
|
+
export function createEnvironmentPresetDocument(name, { description, label } = {}) {
|
|
203
|
+
const key = String(name ?? '').trim();
|
|
204
|
+
const preset = ENVIRONMENT_PRESETS.get(key);
|
|
205
|
+
if (!preset) throw new Error(`Environment preset "${key}" is not registered.`);
|
|
206
|
+
|
|
207
|
+
const sanitized = sanitizeEnvironmentPreset({
|
|
208
|
+
...preset,
|
|
209
|
+
description: description ?? preset.description ?? '',
|
|
210
|
+
label: label ?? preset.label ?? key,
|
|
211
|
+
});
|
|
212
|
+
if (!sanitized.ok) throw new Error(sanitized.errors.join(' '));
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
description: sanitized.value.description,
|
|
216
|
+
id: key,
|
|
217
|
+
label: sanitized.value.label,
|
|
218
|
+
preset: {
|
|
219
|
+
features: sanitized.value.features,
|
|
220
|
+
parameters: sanitized.value.parameters,
|
|
221
|
+
rig: sanitized.value.rig,
|
|
222
|
+
},
|
|
223
|
+
schemaVersion: ENVIRONMENT_PRESET_SCHEMA_VERSION,
|
|
224
|
+
type: ENVIRONMENT_PRESET_DOCUMENT_TYPE,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validates an environment preset document (a parsed object or a JSON string)
|
|
230
|
+
* without registering it. Mirrors `validateToonPresetDocument` in
|
|
231
|
+
* toonSettings.js.
|
|
232
|
+
*
|
|
233
|
+
* @param {object | string} input Document object or JSON string.
|
|
234
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[], value: object | null }}
|
|
235
|
+
* On `ok`, `value` is `{ id, label, description, features, parameters, rig }`,
|
|
236
|
+
* ready to pass to `registerEnvironmentPreset(value.id, value)`.
|
|
237
|
+
*/
|
|
238
|
+
export function validateEnvironmentPresetDocument(input) {
|
|
239
|
+
const errors = [];
|
|
240
|
+
const warnings = [];
|
|
241
|
+
|
|
242
|
+
let source = input;
|
|
243
|
+
if (typeof source === 'string') {
|
|
244
|
+
try {
|
|
245
|
+
source = JSON.parse(source);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return {
|
|
248
|
+
errors: [`Invalid environment preset JSON: ${error.message}`],
|
|
249
|
+
ok: false,
|
|
250
|
+
value: null,
|
|
251
|
+
warnings,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!isPlainObject(source)) {
|
|
257
|
+
return {
|
|
258
|
+
errors: ['Environment preset document must be a JSON object.'],
|
|
259
|
+
ok: false,
|
|
260
|
+
value: null,
|
|
261
|
+
warnings,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (source.type !== ENVIRONMENT_PRESET_DOCUMENT_TYPE) {
|
|
266
|
+
errors.push(`Environment preset document type must be "${ENVIRONMENT_PRESET_DOCUMENT_TYPE}".`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (source.schemaVersion === undefined) {
|
|
270
|
+
warnings.push(`Environment preset schemaVersion was missing and defaulted to ${ENVIRONMENT_PRESET_SCHEMA_VERSION}.`);
|
|
271
|
+
} else if (!Number.isFinite(source.schemaVersion)) {
|
|
272
|
+
errors.push('Environment preset schemaVersion must be a number.');
|
|
273
|
+
} else if (source.schemaVersion > ENVIRONMENT_PRESET_SCHEMA_VERSION) {
|
|
274
|
+
errors.push(`Environment preset schema version ${source.schemaVersion} is newer than supported version ${ENVIRONMENT_PRESET_SCHEMA_VERSION}.`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const id = String(source.id ?? '').trim();
|
|
278
|
+
if (!id) errors.push('Environment preset id is required.');
|
|
279
|
+
|
|
280
|
+
const presetSource = cleanObject(source.preset);
|
|
281
|
+
const sanitized = sanitizeEnvironmentPreset({
|
|
282
|
+
...presetSource,
|
|
283
|
+
description: source.description ?? presetSource.description ?? '',
|
|
284
|
+
label: source.label ?? presetSource.label ?? id,
|
|
285
|
+
});
|
|
286
|
+
errors.push(...sanitized.errors);
|
|
287
|
+
warnings.push(...sanitized.warnings);
|
|
288
|
+
|
|
289
|
+
const ok = errors.length === 0;
|
|
290
|
+
return {
|
|
291
|
+
errors,
|
|
292
|
+
ok,
|
|
293
|
+
value: ok
|
|
294
|
+
? {
|
|
295
|
+
description: sanitized.value.description,
|
|
296
|
+
features: sanitized.value.features,
|
|
297
|
+
id,
|
|
298
|
+
label: sanitized.value.label,
|
|
299
|
+
parameters: sanitized.value.parameters,
|
|
300
|
+
rig: sanitized.value.rig,
|
|
301
|
+
}
|
|
302
|
+
: null,
|
|
303
|
+
warnings,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Validates an environment preset document and registers it in one call.
|
|
309
|
+
*
|
|
310
|
+
* @param {object | string} document Document object or JSON string.
|
|
311
|
+
* @param {{ overwrite?: boolean }} [options] Pass `overwrite: true` to replace an existing preset.
|
|
312
|
+
* @returns {string} The registered preset name.
|
|
313
|
+
* @throws {Error} If the document fails validation or the name is taken and `overwrite` is false.
|
|
314
|
+
*/
|
|
315
|
+
export function registerEnvironmentPresetDocument(document, { overwrite = false } = {}) {
|
|
316
|
+
const result = validateEnvironmentPresetDocument(document);
|
|
317
|
+
if (!result.ok) throw new Error(result.errors.join(' '));
|
|
318
|
+
return registerEnvironmentPreset(result.value.id, result.value, { overwrite });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
registerEnvironmentPreset('default', {
|
|
322
|
+
label: 'Default (Baseline)',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
registerEnvironmentPreset('interiorDay', {
|
|
326
|
+
label: 'Interior Day',
|
|
327
|
+
parameters: {
|
|
328
|
+
ambientProbeBlend: 0.35,
|
|
329
|
+
ambientStrength: 0.22,
|
|
330
|
+
aoWarmth: 0.6,
|
|
331
|
+
interiorOcclusionStrength: 0.5,
|
|
332
|
+
},
|
|
333
|
+
rig: {
|
|
334
|
+
dustMotes: true,
|
|
335
|
+
probe: true,
|
|
336
|
+
sun: true,
|
|
337
|
+
timeOfDayHour: 12,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
registerEnvironmentPreset('interiorEvening', {
|
|
342
|
+
label: 'Interior Evening',
|
|
343
|
+
parameters: {
|
|
344
|
+
ambientProbeBlend: 0.4,
|
|
345
|
+
ambientStrength: 0.42,
|
|
346
|
+
directLightStrength: 0.85,
|
|
347
|
+
interiorOcclusionStrength: 0.42,
|
|
348
|
+
shadowLift: 0.3,
|
|
349
|
+
spotLightStrength: 0.55,
|
|
350
|
+
},
|
|
351
|
+
rig: {
|
|
352
|
+
dustMotes: true,
|
|
353
|
+
lampIntensity: 0.8,
|
|
354
|
+
probe: true,
|
|
355
|
+
spotShadows: true,
|
|
356
|
+
sun: true,
|
|
357
|
+
timeOfDayHour: 18,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
registerEnvironmentPreset('interiorNight', {
|
|
362
|
+
label: 'Interior Night (Lamp Lit)',
|
|
363
|
+
parameters: {
|
|
364
|
+
ambientProbeBlend: 0.4,
|
|
365
|
+
emissiveStrength: 0.7,
|
|
366
|
+
pointLightStrength: 0.36,
|
|
367
|
+
shadowLift: 0.56,
|
|
368
|
+
spotLightStrength: 0.85,
|
|
369
|
+
},
|
|
370
|
+
rig: {
|
|
371
|
+
lampIntensity: 1.3,
|
|
372
|
+
probe: true,
|
|
373
|
+
spotShadows: true,
|
|
374
|
+
sun: false,
|
|
375
|
+
timeOfDayHour: 22,
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Flat-color / untextured scenes: strong grounding, gentle gradient interest,
|
|
380
|
+
// no room-specific darkening.
|
|
381
|
+
registerEnvironmentPreset('interiorStudio', {
|
|
382
|
+
label: 'Interior Studio (Untextured)',
|
|
383
|
+
features: {
|
|
384
|
+
leftSideShadow: false,
|
|
385
|
+
},
|
|
386
|
+
parameters: {
|
|
387
|
+
ambientProbeBlend: 0.3,
|
|
388
|
+
ambientStrength: 0.9,
|
|
389
|
+
aoWarmth: 0.45,
|
|
390
|
+
lightingInfluence: 0.6,
|
|
391
|
+
shadowLift: 0.5,
|
|
392
|
+
specularStrength: 0.1,
|
|
393
|
+
untexturedGradientStrength: 0.5,
|
|
394
|
+
vertexAoStrength: 1.0,
|
|
395
|
+
},
|
|
396
|
+
rig: {
|
|
397
|
+
bakeVertexAo: true,
|
|
398
|
+
probe: true,
|
|
399
|
+
spotShadows: true,
|
|
400
|
+
sun: true,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
registerEnvironmentPreset('exteriorDay', {
|
|
405
|
+
label: 'Exterior Day',
|
|
406
|
+
features: {
|
|
407
|
+
leftSideShadow: false,
|
|
408
|
+
windowCutout: false,
|
|
409
|
+
},
|
|
410
|
+
parameters: {
|
|
411
|
+
cloudShadowStrength: 0.35,
|
|
412
|
+
heightFogDensity: 0.012,
|
|
413
|
+
heightFogFalloff: 9,
|
|
414
|
+
skyTintStrength: 0.4,
|
|
415
|
+
},
|
|
416
|
+
rig: {
|
|
417
|
+
sun: true,
|
|
418
|
+
timeOfDayHour: 12,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Everything on with demo values: a feature tour, not a production look
|
|
423
|
+
// (pairs with the character 'showcase' preset).
|
|
424
|
+
registerEnvironmentPreset('showcase', {
|
|
425
|
+
label: 'Showcase (All Features)',
|
|
426
|
+
parameters: {
|
|
427
|
+
ambientProbeBlend: 0.45,
|
|
428
|
+
aoWarmth: 0.6,
|
|
429
|
+
heightFogDensity: 0.006,
|
|
430
|
+
interiorOcclusionStrength: 0.35,
|
|
431
|
+
planarReflectionStrength: 0.32,
|
|
432
|
+
specularStrength: 0.2,
|
|
433
|
+
spotLightStrength: 0.6,
|
|
434
|
+
},
|
|
435
|
+
rig: {
|
|
436
|
+
dustMotes: true,
|
|
437
|
+
planarReflection: true,
|
|
438
|
+
probe: true,
|
|
439
|
+
spotShadows: true,
|
|
440
|
+
sun: true,
|
|
441
|
+
timeOfDayHour: 15,
|
|
442
|
+
},
|
|
443
|
+
});
|