@treasuryspatial/viewer-kit 0.2.45 → 0.2.50
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/dist/autoGenerate.d.ts.map +1 -1
- package/dist/autoGenerate.js +7 -1
- package/dist/camera.d.ts +1 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +8 -1
- package/dist/engine/ViewerEngine.d.ts +20 -0
- package/dist/engine/ViewerEngine.d.ts.map +1 -1
- package/dist/engine/ViewerEngine.js +254 -63
- package/dist/engine/types.d.ts +69 -3
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/exports/download.d.ts +6 -0
- package/dist/exports/download.d.ts.map +1 -1
- package/dist/exports/download.js +142 -0
- package/dist/exports/three-export.d.ts +4 -1
- package/dist/exports/three-export.d.ts.map +1 -1
- package/dist/exports/three-export.js +138 -4
- package/dist/index.d.ts +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/materials/architectural.d.ts +3 -3
- package/dist/materials/architectural.d.ts.map +1 -1
- package/dist/materials/architectural.js +295 -37
- package/dist/materials/catalogue-data.d.ts +15 -0
- package/dist/materials/catalogue-data.d.ts.map +1 -1
- package/dist/materials/catalogue-data.js +16 -0
- package/dist/materials/presets.d.ts +15 -8
- package/dist/materials/presets.d.ts.map +1 -1
- package/dist/materials/presets.js +140 -39
- package/dist/materials/resolve.d.ts +42 -0
- package/dist/materials/resolve.d.ts.map +1 -1
- package/dist/materials/resolve.js +228 -13
- package/dist/materials/types.d.ts +32 -3
- package/dist/materials/types.d.ts.map +1 -1
- package/dist/presets/defaults.d.ts.map +1 -1
- package/dist/presets/defaults.js +17 -1
- package/dist/presets/sciencePresets.d.ts.map +1 -1
- package/dist/presets/sciencePresets.js +167 -50
- package/dist/scene.d.ts +28 -4
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +196 -31
- package/dist/sceneSemanticRegistry.d.ts +64 -0
- package/dist/sceneSemanticRegistry.d.ts.map +1 -0
- package/dist/sceneSemanticRegistry.js +199 -0
- package/dist/sky/scienceSky.d.ts.map +1 -1
- package/dist/sky/scienceSky.js +16 -0
- package/dist/systems/debugSystem.d.ts +24 -1
- package/dist/systems/debugSystem.d.ts.map +1 -1
- package/dist/systems/debugSystem.js +324 -77
- package/dist/systems/environmentSystem.d.ts +5 -4
- package/dist/systems/environmentSystem.d.ts.map +1 -1
- package/dist/systems/environmentSystem.js +138 -62
- package/dist/systems/lightingSystem.d.ts +10 -1
- package/dist/systems/lightingSystem.d.ts.map +1 -1
- package/dist/systems/lightingSystem.js +118 -17
- package/dist/systems/postfxSystem.d.ts +5 -1
- package/dist/systems/postfxSystem.d.ts.map +1 -1
- package/dist/systems/postfxSystem.js +84 -1
- package/dist/systems/rendererSystem.d.ts.map +1 -1
- package/dist/systems/rendererSystem.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/uploads/geometry3dm.d.ts.map +1 -1
- package/dist/uploads/geometry3dm.js +1 -0
- package/dist/uploads/grasshopper.d.ts.map +1 -1
- package/dist/uploads/grasshopper.js +63 -5
- package/dist/uploads/mesh.js +5 -5
- package/dist/uploads/types.d.ts +4 -0
- package/dist/uploads/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -8,10 +8,9 @@ export class EnvironmentSystem {
|
|
|
8
8
|
currentObjects = [];
|
|
9
9
|
currentBackground = null;
|
|
10
10
|
currentEnvironment = null;
|
|
11
|
-
gradientSky = null;
|
|
12
|
-
gradientBaseRadius = 300;
|
|
13
11
|
objectUrls = [];
|
|
14
12
|
loadToken = 0;
|
|
13
|
+
appliedSkySignature = null;
|
|
15
14
|
state = { mode: "none", backgroundEnabled: false, lightingEnabled: false };
|
|
16
15
|
fallbackGradient = {
|
|
17
16
|
topColor: "#f8fafc",
|
|
@@ -28,21 +27,16 @@ export class EnvironmentSystem {
|
|
|
28
27
|
getState() {
|
|
29
28
|
return { ...this.state };
|
|
30
29
|
}
|
|
31
|
-
update(
|
|
32
|
-
if (!this.gradientSky || !camera)
|
|
33
|
-
return;
|
|
34
|
-
const cam = camera;
|
|
35
|
-
if (!cam || typeof cam.near !== "number")
|
|
36
|
-
return;
|
|
37
|
-
const targetRadius = Math.max(this.gradientBaseRadius, cam.near * 4);
|
|
38
|
-
const scale = targetRadius / this.gradientBaseRadius;
|
|
39
|
-
if (Number.isFinite(scale) && scale > 0) {
|
|
40
|
-
this.gradientSky.scale.setScalar(scale);
|
|
41
|
-
}
|
|
42
|
-
this.gradientSky.position.copy(cam.position);
|
|
43
|
-
}
|
|
30
|
+
update(_camera) { }
|
|
44
31
|
async apply(preset) {
|
|
45
32
|
const sky = preset.sky;
|
|
33
|
+
// The runtime preset is rebuilt on every lighting/debug/postfx tweak, but the env
|
|
34
|
+
// texture only depends on `sky`. Skip the dispose+reload cycle when sky is unchanged
|
|
35
|
+
// so HDR/cube envs survive unrelated preset edits instead of flickering on each one.
|
|
36
|
+
const nextSignature = EnvironmentSystem.skySignature(sky);
|
|
37
|
+
if (nextSignature === this.appliedSkySignature)
|
|
38
|
+
return;
|
|
39
|
+
this.appliedSkySignature = nextSignature;
|
|
46
40
|
this.disposeCurrent();
|
|
47
41
|
if (!sky || sky.mode === "none") {
|
|
48
42
|
this.scene.background = null;
|
|
@@ -61,6 +55,19 @@ export class EnvironmentSystem {
|
|
|
61
55
|
}
|
|
62
56
|
catch (error) {
|
|
63
57
|
console.warn("[viewer-kit] HDR sky failed, falling back to gradient.", error);
|
|
58
|
+
this.appliedSkySignature = null;
|
|
59
|
+
this.applyGradientSky(this.fallbackGradient);
|
|
60
|
+
this.state = { mode: "gradient", backgroundEnabled: true, lightingEnabled: false };
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (sky.mode === "image") {
|
|
65
|
+
try {
|
|
66
|
+
await this.applyImageSky(sky.image);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.warn("[viewer-kit] LDR image sky failed, falling back to gradient.", error);
|
|
70
|
+
this.appliedSkySignature = null;
|
|
64
71
|
this.applyGradientSky(this.fallbackGradient);
|
|
65
72
|
this.state = { mode: "gradient", backgroundEnabled: true, lightingEnabled: false };
|
|
66
73
|
}
|
|
@@ -72,18 +79,30 @@ export class EnvironmentSystem {
|
|
|
72
79
|
}
|
|
73
80
|
catch (error) {
|
|
74
81
|
console.warn("[viewer-kit] Cube sky failed, falling back to gradient.", error);
|
|
82
|
+
this.appliedSkySignature = null;
|
|
75
83
|
this.applyGradientSky(this.fallbackGradient);
|
|
76
84
|
this.state = { mode: "gradient", backgroundEnabled: true, lightingEnabled: false };
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
dispose() {
|
|
89
|
+
this.appliedSkySignature = null;
|
|
81
90
|
this.disposeCurrent();
|
|
82
91
|
if (this.pmremGenerator) {
|
|
83
92
|
this.pmremGenerator.dispose();
|
|
84
93
|
this.pmremGenerator = null;
|
|
85
94
|
}
|
|
86
95
|
}
|
|
96
|
+
static skySignature(sky) {
|
|
97
|
+
if (!sky)
|
|
98
|
+
return "none";
|
|
99
|
+
try {
|
|
100
|
+
return JSON.stringify(sky);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return `${sky.mode}:unstable`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
87
106
|
disposeCurrent() {
|
|
88
107
|
this.currentObjects.forEach((obj) => {
|
|
89
108
|
if (obj.parent)
|
|
@@ -118,55 +137,48 @@ export class EnvironmentSystem {
|
|
|
118
137
|
}
|
|
119
138
|
this.currentBackground = null;
|
|
120
139
|
this.currentEnvironment = null;
|
|
121
|
-
this.gradientSky = null;
|
|
122
140
|
this.objectUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
123
141
|
this.objectUrls = [];
|
|
124
142
|
}
|
|
125
143
|
applyGradientSky(gradient) {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
|
|
165
|
-
sky.renderOrder = -1;
|
|
166
|
-
sky.frustumCulled = false;
|
|
167
|
-
this.scene.add(sky);
|
|
168
|
-
this.currentObjects.push(sky);
|
|
169
|
-
this.gradientSky = sky;
|
|
144
|
+
const canvas = typeof document !== "undefined"
|
|
145
|
+
? document.createElement("canvas")
|
|
146
|
+
: typeof OffscreenCanvas !== "undefined"
|
|
147
|
+
? new OffscreenCanvas(1024, 512)
|
|
148
|
+
: null;
|
|
149
|
+
if (!canvas) {
|
|
150
|
+
this.scene.background = new THREE.Color(gradient.horizonColor);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
canvas.width = 1024;
|
|
154
|
+
canvas.height = 512;
|
|
155
|
+
const context = canvas.getContext("2d");
|
|
156
|
+
if (!context) {
|
|
157
|
+
this.scene.background = new THREE.Color(gradient.horizonColor);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const baseGradient = context.createLinearGradient(0, 0, 0, canvas.height);
|
|
161
|
+
baseGradient.addColorStop(0, gradient.topColor);
|
|
162
|
+
baseGradient.addColorStop(0.55, gradient.topColor);
|
|
163
|
+
baseGradient.addColorStop(1, gradient.horizonColor);
|
|
164
|
+
context.fillStyle = baseGradient;
|
|
165
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
166
|
+
const normalizedSunIntensity = THREE.MathUtils.clamp(gradient.sunIntensity, 0, 0.24);
|
|
167
|
+
const normalizedSunSize = THREE.MathUtils.clamp(gradient.sunSize, 0.005, 0.08);
|
|
168
|
+
const sunX = canvas.width * (0.5 + THREE.MathUtils.clamp(gradient.sunPosition[0], -1, 1) * 0.32);
|
|
169
|
+
const sunY = canvas.height * (0.5 - THREE.MathUtils.clamp(gradient.sunPosition[1], -1, 1) * 0.28);
|
|
170
|
+
const sunRadius = canvas.height * (0.18 + normalizedSunSize * 2.8);
|
|
171
|
+
const sunGradient = context.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunRadius);
|
|
172
|
+
sunGradient.addColorStop(0, `rgba(255,255,255,${0.08 + normalizedSunIntensity * 2.8})`);
|
|
173
|
+
sunGradient.addColorStop(0.32, `rgba(255,255,255,${0.05 + normalizedSunIntensity * 1.9})`);
|
|
174
|
+
sunGradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
175
|
+
context.fillStyle = sunGradient;
|
|
176
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
177
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
178
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
179
|
+
texture.needsUpdate = true;
|
|
180
|
+
this.scene.background = texture;
|
|
181
|
+
this.currentBackground = texture;
|
|
170
182
|
}
|
|
171
183
|
async resolveAsset(src, kind) {
|
|
172
184
|
if (!this.assetResolver)
|
|
@@ -186,7 +198,9 @@ export class EnvironmentSystem {
|
|
|
186
198
|
const token = ++this.loadToken;
|
|
187
199
|
const backgroundEnabled = hdr.showBackground !== false;
|
|
188
200
|
const lightingEnabled = hdr.showLighting !== false;
|
|
189
|
-
|
|
201
|
+
// When the env feeds PBR sampling (lightingEnabled), it must be PMREM-filtered.
|
|
202
|
+
// Without prefiltering Three.js samples a flat low-mip env and the scene loses the IBL look.
|
|
203
|
+
const usePmrem = hdr.usePmrem ?? lightingEnabled;
|
|
190
204
|
const resolved = await this.resolveAsset(hdr.src, "hdr");
|
|
191
205
|
if (token !== this.loadToken)
|
|
192
206
|
return;
|
|
@@ -232,11 +246,73 @@ export class EnvironmentSystem {
|
|
|
232
246
|
}
|
|
233
247
|
this.state = { mode: "hdr", backgroundEnabled, lightingEnabled };
|
|
234
248
|
}
|
|
249
|
+
async applyImageSky(image) {
|
|
250
|
+
const token = ++this.loadToken;
|
|
251
|
+
const backgroundEnabled = image.showBackground !== false;
|
|
252
|
+
const lightingEnabled = image.showLighting === true;
|
|
253
|
+
const usePmrem = image.usePmrem ?? false;
|
|
254
|
+
const resolved = await this.resolveAsset(image.src, "texture");
|
|
255
|
+
if (token !== this.loadToken)
|
|
256
|
+
return;
|
|
257
|
+
const loader = new THREE.TextureLoader();
|
|
258
|
+
const texture = await loader.loadAsync(resolved);
|
|
259
|
+
if (token !== this.loadToken) {
|
|
260
|
+
texture.dispose();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const projection = image.projection ?? "equirectangular";
|
|
264
|
+
if (projection !== "equirectangular" && projection !== "backplate") {
|
|
265
|
+
texture.dispose();
|
|
266
|
+
throw new Error(`Unsupported LDR image sky projection: ${projection}`);
|
|
267
|
+
}
|
|
268
|
+
if (projection === "backplate" && lightingEnabled) {
|
|
269
|
+
texture.dispose();
|
|
270
|
+
throw new Error("LDR image backplates cannot drive scene.environment lighting. Use a real HDR/equirectangular sky.");
|
|
271
|
+
}
|
|
272
|
+
texture.mapping = projection === "backplate" ? THREE.UVMapping : THREE.EquirectangularReflectionMapping;
|
|
273
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
274
|
+
texture.magFilter = THREE.LinearFilter;
|
|
275
|
+
texture.minFilter = THREE.LinearFilter;
|
|
276
|
+
texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
277
|
+
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
278
|
+
texture.needsUpdate = true;
|
|
279
|
+
texture.center = new THREE.Vector2(0.5, 0.5);
|
|
280
|
+
if (projection === "equirectangular" && typeof image.rotationY === "number") {
|
|
281
|
+
texture.rotation = image.rotationY;
|
|
282
|
+
}
|
|
283
|
+
if (projection === "equirectangular") {
|
|
284
|
+
this.applyRotationToScene(image.rotation, image.rotationY);
|
|
285
|
+
}
|
|
286
|
+
let environmentTexture = null;
|
|
287
|
+
if (lightingEnabled) {
|
|
288
|
+
if (usePmrem) {
|
|
289
|
+
if (!this.pmremGenerator) {
|
|
290
|
+
this.pmremGenerator = new THREE.PMREMGenerator(this.renderer);
|
|
291
|
+
this.pmremGenerator.compileEquirectangularShader();
|
|
292
|
+
}
|
|
293
|
+
environmentTexture = this.pmremGenerator.fromEquirectangular(texture).texture;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
environmentTexture = texture;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (backgroundEnabled) {
|
|
300
|
+
this.scene.background = texture;
|
|
301
|
+
this.currentBackground = texture;
|
|
302
|
+
}
|
|
303
|
+
if (environmentTexture) {
|
|
304
|
+
this.scene.environment = environmentTexture;
|
|
305
|
+
this.currentEnvironment = environmentTexture;
|
|
306
|
+
}
|
|
307
|
+
this.state = { mode: "image", backgroundEnabled, lightingEnabled };
|
|
308
|
+
}
|
|
235
309
|
async applyCubeSky(cube) {
|
|
236
310
|
const token = ++this.loadToken;
|
|
237
311
|
const backgroundEnabled = cube.showBackground !== false;
|
|
238
312
|
const lightingEnabled = cube.showLighting !== false;
|
|
239
|
-
|
|
313
|
+
// When the env feeds PBR sampling (lightingEnabled), it must be PMREM-filtered.
|
|
314
|
+
// The debug cubemap previously rendered as a flat sRGB env because this defaulted to false.
|
|
315
|
+
const usePmrem = cube.usePmrem ?? lightingEnabled;
|
|
240
316
|
const faces = await Promise.all([
|
|
241
317
|
this.resolveAsset(cube.px, "cubeface"),
|
|
242
318
|
this.resolveAsset(cube.nx, "cubeface"),
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import type { RenderPresetDefinition } from "../engine/types";
|
|
3
|
+
export type LightingSystemState = {
|
|
4
|
+
keyPosition: [number, number, number] | null;
|
|
5
|
+
keyTarget: [number, number, number] | null;
|
|
6
|
+
fillPosition: [number, number, number] | null;
|
|
7
|
+
fillTarget: [number, number, number] | null;
|
|
8
|
+
};
|
|
3
9
|
export declare class LightingSystem {
|
|
4
10
|
private scene;
|
|
5
11
|
private lights;
|
|
6
12
|
private targets;
|
|
13
|
+
private state;
|
|
7
14
|
constructor(scene: THREE.Scene);
|
|
8
|
-
apply(preset: RenderPresetDefinition): void;
|
|
15
|
+
apply(preset: RenderPresetDefinition, bounds?: THREE.Box3 | null): void;
|
|
9
16
|
dispose(): void;
|
|
17
|
+
getState(): LightingSystemState;
|
|
10
18
|
private add;
|
|
11
19
|
private addTarget;
|
|
12
20
|
private clear;
|
|
21
|
+
private resolveDirectionalPosition;
|
|
13
22
|
private configureShadow;
|
|
14
23
|
}
|
|
15
24
|
//# sourceMappingURL=lightingSystem.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lightingSystem.d.ts","sourceRoot":"","sources":["../../src/systems/lightingSystem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAqB,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAUjF,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAwB;
|
|
1
|
+
{"version":3,"file":"lightingSystem.d.ts","sourceRoot":"","sources":["../../src/systems/lightingSystem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAqB,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAUjF,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC7C,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC3C,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9C,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC7C,CAAC;AAoBF,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,KAAK,CAKX;gBAEU,KAAK,EAAE,KAAK,CAAC,KAAK;IAI9B,KAAK,CAAC,MAAM,EAAE,sBAAsB,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI;IAiEvE,OAAO,IAAI,IAAI;IAIf,QAAQ,IAAI,mBAAmB;IAS/B,OAAO,CAAC,GAAG;IAKX,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,KAAK;IAiBb,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,eAAe;CAwExB"}
|
|
@@ -6,17 +6,43 @@ const DEFAULT_LIGHTING = {
|
|
|
6
6
|
key: { color: "#ffffff", intensity: 1.4, position: [10, 15, 5], castShadow: true },
|
|
7
7
|
fill: { color: "#ffffff", intensity: 0.5, position: [-10, 10, -5] },
|
|
8
8
|
};
|
|
9
|
+
const vectorToTuple = (value) => [value.x, value.y, value.z];
|
|
10
|
+
const cloneTuple = (value) => value ? [value[0], value[1], value[2]] : null;
|
|
11
|
+
const DEFAULT_SHADOW_MARGIN = 0.18;
|
|
12
|
+
const DEFAULT_SHADOW_DEPTH_MARGIN = 0.35;
|
|
13
|
+
const getBoxCorners = (box) => [
|
|
14
|
+
new THREE.Vector3(box.min.x, box.min.y, box.min.z),
|
|
15
|
+
new THREE.Vector3(box.min.x, box.min.y, box.max.z),
|
|
16
|
+
new THREE.Vector3(box.min.x, box.max.y, box.min.z),
|
|
17
|
+
new THREE.Vector3(box.min.x, box.max.y, box.max.z),
|
|
18
|
+
new THREE.Vector3(box.max.x, box.min.y, box.min.z),
|
|
19
|
+
new THREE.Vector3(box.max.x, box.min.y, box.max.z),
|
|
20
|
+
new THREE.Vector3(box.max.x, box.max.y, box.min.z),
|
|
21
|
+
new THREE.Vector3(box.max.x, box.max.y, box.max.z),
|
|
22
|
+
];
|
|
9
23
|
export class LightingSystem {
|
|
10
24
|
scene;
|
|
11
25
|
lights = [];
|
|
12
26
|
targets = [];
|
|
27
|
+
state = {
|
|
28
|
+
keyPosition: null,
|
|
29
|
+
keyTarget: null,
|
|
30
|
+
fillPosition: null,
|
|
31
|
+
fillTarget: null,
|
|
32
|
+
};
|
|
13
33
|
constructor(scene) {
|
|
14
34
|
this.scene = scene;
|
|
15
35
|
}
|
|
16
|
-
apply(preset) {
|
|
36
|
+
apply(preset, bounds) {
|
|
17
37
|
this.clear();
|
|
18
38
|
const config = preset.lighting ?? DEFAULT_LIGHTING;
|
|
19
|
-
const
|
|
39
|
+
const boundsCenter = bounds && !bounds.isEmpty() ? bounds.getCenter(new THREE.Vector3()) : null;
|
|
40
|
+
const lightTargetArray = preset.sceneExtras?.lightTarget ?? [
|
|
41
|
+
boundsCenter?.x ?? 0,
|
|
42
|
+
boundsCenter?.y ?? 1.5,
|
|
43
|
+
boundsCenter?.z ?? 0,
|
|
44
|
+
];
|
|
45
|
+
const lightTarget = new THREE.Vector3(...lightTargetArray);
|
|
20
46
|
if (config.rig === "none")
|
|
21
47
|
return;
|
|
22
48
|
const ambient = config.ambient ?? DEFAULT_LIGHTING.ambient;
|
|
@@ -32,22 +58,26 @@ export class LightingSystem {
|
|
|
32
58
|
const key = config.key ?? DEFAULT_LIGHTING.key;
|
|
33
59
|
if (key && key.intensity > 0) {
|
|
34
60
|
const light = new THREE.DirectionalLight(new THREE.Color(key.color), key.intensity);
|
|
35
|
-
light.position.
|
|
61
|
+
light.position.copy(this.resolveDirectionalPosition(key.position, lightTarget, key.positionMode));
|
|
36
62
|
light.castShadow = key.castShadow ?? false;
|
|
63
|
+
light.target.position.copy(lightTarget);
|
|
64
|
+
this.addTarget(light.target);
|
|
37
65
|
if (light.castShadow) {
|
|
38
|
-
this.configureShadow(light, config.shadow);
|
|
66
|
+
this.configureShadow(light, config.shadow, bounds, lightTarget);
|
|
39
67
|
}
|
|
40
|
-
light.target.position.set(lightTarget[0], lightTarget[1], lightTarget[2]);
|
|
41
|
-
this.addTarget(light.target);
|
|
42
68
|
this.add(light);
|
|
69
|
+
this.state.keyPosition = vectorToTuple(light.position);
|
|
70
|
+
this.state.keyTarget = vectorToTuple(light.target.position);
|
|
43
71
|
}
|
|
44
72
|
const fill = config.fill ?? DEFAULT_LIGHTING.fill;
|
|
45
73
|
if (fill && fill.intensity > 0) {
|
|
46
74
|
const light = new THREE.DirectionalLight(new THREE.Color(fill.color), fill.intensity);
|
|
47
|
-
light.position.
|
|
48
|
-
light.target.position.
|
|
75
|
+
light.position.copy(this.resolveDirectionalPosition(fill.position, lightTarget, fill.positionMode));
|
|
76
|
+
light.target.position.copy(lightTarget);
|
|
49
77
|
this.addTarget(light.target);
|
|
50
78
|
this.add(light);
|
|
79
|
+
this.state.fillPosition = vectorToTuple(light.position);
|
|
80
|
+
this.state.fillTarget = vectorToTuple(light.target.position);
|
|
51
81
|
}
|
|
52
82
|
if (preset.interiorLights?.length) {
|
|
53
83
|
preset.interiorLights.forEach((interior) => {
|
|
@@ -62,6 +92,14 @@ export class LightingSystem {
|
|
|
62
92
|
dispose() {
|
|
63
93
|
this.clear();
|
|
64
94
|
}
|
|
95
|
+
getState() {
|
|
96
|
+
return {
|
|
97
|
+
keyPosition: cloneTuple(this.state.keyPosition),
|
|
98
|
+
keyTarget: cloneTuple(this.state.keyTarget),
|
|
99
|
+
fillPosition: cloneTuple(this.state.fillPosition),
|
|
100
|
+
fillTarget: cloneTuple(this.state.fillTarget),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
65
103
|
add(light) {
|
|
66
104
|
this.scene.add(light);
|
|
67
105
|
this.lights.push(light);
|
|
@@ -81,20 +119,83 @@ export class LightingSystem {
|
|
|
81
119
|
target.parent.remove(target);
|
|
82
120
|
});
|
|
83
121
|
this.targets = [];
|
|
122
|
+
this.state = {
|
|
123
|
+
keyPosition: null,
|
|
124
|
+
keyTarget: null,
|
|
125
|
+
fillPosition: null,
|
|
126
|
+
fillTarget: null,
|
|
127
|
+
};
|
|
84
128
|
}
|
|
85
|
-
|
|
129
|
+
resolveDirectionalPosition(position, target, mode = "target-offset") {
|
|
130
|
+
const configured = new THREE.Vector3(...position);
|
|
131
|
+
return mode === "world" ? configured : target.clone().add(configured);
|
|
132
|
+
}
|
|
133
|
+
configureShadow(light, config, bounds, target) {
|
|
86
134
|
if (!light.shadow)
|
|
87
135
|
return;
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
136
|
+
const boundsSize = bounds && !bounds.isEmpty() ? bounds.getSize(new THREE.Vector3()) : null;
|
|
137
|
+
const boundsSphere = bounds && !bounds.isEmpty() ? bounds.getBoundingSphere(new THREE.Sphere()) : null;
|
|
138
|
+
const boundsSpan = boundsSize ? Math.max(boundsSize.x, boundsSize.y, boundsSize.z) : 0;
|
|
139
|
+
const radius = boundsSphere?.radius ?? (boundsSpan > 0 ? boundsSpan * 0.5 : 25);
|
|
140
|
+
const autoFit = config?.autoFit !== false;
|
|
141
|
+
const configuredSize = config?.cameraSize;
|
|
142
|
+
const targetPoint = target ?? new THREE.Vector3();
|
|
143
|
+
const distance = light.position.distanceTo(targetPoint);
|
|
144
|
+
if (autoFit && bounds && !bounds.isEmpty()) {
|
|
145
|
+
const up = new THREE.Vector3(0, 1, 0);
|
|
146
|
+
const lightDir = targetPoint.clone().sub(light.position).normalize();
|
|
147
|
+
if (Math.abs(lightDir.dot(up)) > 0.98) {
|
|
148
|
+
up.set(0, 0, 1);
|
|
149
|
+
}
|
|
150
|
+
const shadowCamera = light.shadow.camera;
|
|
151
|
+
shadowCamera.position.copy(light.position);
|
|
152
|
+
shadowCamera.up.copy(up);
|
|
153
|
+
shadowCamera.lookAt(targetPoint);
|
|
154
|
+
shadowCamera.updateMatrixWorld(true);
|
|
155
|
+
const viewMatrix = shadowCamera.matrixWorldInverse.clone();
|
|
156
|
+
const corners = getBoxCorners(bounds).map((corner) => corner.applyMatrix4(viewMatrix));
|
|
157
|
+
const min = new THREE.Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
|
|
158
|
+
const max = new THREE.Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
|
|
159
|
+
corners.forEach((corner) => {
|
|
160
|
+
min.min(corner);
|
|
161
|
+
max.max(corner);
|
|
162
|
+
});
|
|
163
|
+
const fitWidth = Math.max(max.x - min.x, 1);
|
|
164
|
+
const fitHeight = Math.max(max.y - min.y, 1);
|
|
165
|
+
const baseMargin = Math.max(config?.margin ?? 0, Math.max(fitWidth, fitHeight, radius) * DEFAULT_SHADOW_MARGIN);
|
|
166
|
+
const configuredHalfSize = configuredSize && configuredSize > 0 ? configuredSize : 0;
|
|
167
|
+
const halfWidth = Math.max(fitWidth * 0.5 + baseMargin, configuredHalfSize);
|
|
168
|
+
const halfHeight = Math.max(fitHeight * 0.5 + baseMargin, configuredHalfSize);
|
|
169
|
+
light.shadow.camera.left = -halfWidth;
|
|
170
|
+
light.shadow.camera.right = halfWidth;
|
|
171
|
+
light.shadow.camera.top = halfHeight;
|
|
172
|
+
light.shadow.camera.bottom = -halfHeight;
|
|
173
|
+
const depthPadding = Math.max(config?.depthMargin ?? 0, Math.max(0.5, radius * DEFAULT_SHADOW_DEPTH_MARGIN));
|
|
174
|
+
const fittedNear = Math.max(0.1, -max.z - depthPadding);
|
|
175
|
+
const fittedFar = Math.max(fittedNear + 1, -min.z + depthPadding);
|
|
176
|
+
light.shadow.camera.near = Math.max(config?.near ?? 0.1, Math.min(fittedNear, fittedFar - 0.5));
|
|
177
|
+
light.shadow.camera.far = Math.max(config?.far ?? 0, fittedFar, light.shadow.camera.near + 1);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const cameraSize = configuredSize ?? (boundsSpan > 0 ? Math.max(8, radius * 1.35, boundsSpan * 0.5) : 25);
|
|
181
|
+
light.shadow.camera.left = -cameraSize;
|
|
182
|
+
light.shadow.camera.right = cameraSize;
|
|
183
|
+
light.shadow.camera.top = cameraSize;
|
|
184
|
+
light.shadow.camera.bottom = -cameraSize;
|
|
185
|
+
const depthPadding = Math.max(0.5, radius * 0.35);
|
|
186
|
+
const fittedNear = Math.max(0.1, distance - radius - depthPadding);
|
|
187
|
+
const fittedFar = Math.max(fittedNear + 1, distance + radius + depthPadding);
|
|
188
|
+
light.shadow.camera.near = config?.near ?? fittedNear;
|
|
189
|
+
light.shadow.camera.far = config?.far ?? Math.max(fittedFar, light.shadow.camera.near + 1);
|
|
190
|
+
}
|
|
191
|
+
const mapSize = THREE.MathUtils.clamp(config?.mapSize ?? 2048, 512, 4096);
|
|
96
192
|
light.shadow.mapSize.set(mapSize, mapSize);
|
|
97
193
|
light.shadow.bias = config?.bias ?? -0.0001;
|
|
194
|
+
light.shadow.normalBias = config?.normalBias ?? 0.035;
|
|
98
195
|
light.shadow.radius = config?.radius ?? 3;
|
|
196
|
+
if (typeof light.shadow.camera.updateProjectionMatrix === "function") {
|
|
197
|
+
light.shadow.camera.updateProjectionMatrix();
|
|
198
|
+
}
|
|
199
|
+
light.shadow.camera.updateMatrixWorld(true);
|
|
99
200
|
}
|
|
100
201
|
}
|
|
@@ -15,11 +15,15 @@ export declare class PostFxSystem {
|
|
|
15
15
|
private lastHeight;
|
|
16
16
|
private pendingConfig;
|
|
17
17
|
private currentConfig;
|
|
18
|
-
|
|
18
|
+
private contextAttributesWarningIssued;
|
|
19
|
+
constructor(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera);
|
|
20
|
+
setCamera(camera: THREE.Camera): void;
|
|
19
21
|
apply(config?: PostFxConfig): void;
|
|
20
22
|
render(): void;
|
|
21
23
|
resize(width: number, height: number): void;
|
|
22
24
|
private getDrawingBufferSize;
|
|
25
|
+
private ensureRendererContextAttributes;
|
|
26
|
+
private withAoDepthProxiesVisible;
|
|
23
27
|
dispose(): void;
|
|
24
28
|
invalidate(): void;
|
|
25
29
|
getState(): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postfxSystem.d.ts","sourceRoot":"","sources":["../../src/systems/postfxSystem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"postfxSystem.d.ts","sourceRoot":"","sources":["../../src/systems/postfxSystem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,8BAA8B,CAAS;gBAEnC,QAAQ,EAAE,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM;IAMnF,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI;IAiBrC,KAAK,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,IAAI;IAsHlC,MAAM,IAAI,IAAI;IAsBd,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAwB3C,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,+BAA+B;IA8CvC,OAAO,CAAC,yBAAyB;IAiBjC,OAAO,IAAI,IAAI;IAaf,UAAU,IAAI,IAAI;IAqBlB,QAAQ,IAAI;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,OAAO,CAAC;QAAC,eAAe,EAAE,OAAO,CAAA;KAAE;CAOpF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { BloomEffect, EffectComposer, EffectPass, RenderPass, VignetteEffect } from "postprocessing";
|
|
3
3
|
import { N8AOPostPass } from "n8ao";
|
|
4
|
+
import { AO_DEPTH_PROXY_USER_DATA_KEY } from "./debugSystem";
|
|
4
5
|
export class PostFxSystem {
|
|
5
6
|
renderer;
|
|
6
7
|
scene;
|
|
@@ -16,11 +17,28 @@ export class PostFxSystem {
|
|
|
16
17
|
lastHeight = 0;
|
|
17
18
|
pendingConfig = null;
|
|
18
19
|
currentConfig = null;
|
|
20
|
+
contextAttributesWarningIssued = false;
|
|
19
21
|
constructor(renderer, scene, camera) {
|
|
20
22
|
this.renderer = renderer;
|
|
21
23
|
this.scene = scene;
|
|
22
24
|
this.camera = camera;
|
|
23
25
|
}
|
|
26
|
+
setCamera(camera) {
|
|
27
|
+
this.camera = camera;
|
|
28
|
+
if (this.composer) {
|
|
29
|
+
this.composer.dispose();
|
|
30
|
+
this.composer = null;
|
|
31
|
+
this.renderPass = null;
|
|
32
|
+
this.aoPass = null;
|
|
33
|
+
this.effectPass = null;
|
|
34
|
+
this.enabled = false;
|
|
35
|
+
}
|
|
36
|
+
if (this.currentConfig) {
|
|
37
|
+
const config = this.currentConfig;
|
|
38
|
+
this.currentConfig = null;
|
|
39
|
+
this.apply(config);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
24
42
|
apply(config) {
|
|
25
43
|
const ao = config?.ao;
|
|
26
44
|
const bloom = config?.bloom;
|
|
@@ -41,6 +59,10 @@ export class PostFxSystem {
|
|
|
41
59
|
this.enabled = false;
|
|
42
60
|
return;
|
|
43
61
|
}
|
|
62
|
+
if (!this.ensureRendererContextAttributes()) {
|
|
63
|
+
this.enabled = false;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
44
66
|
if (!this.composer) {
|
|
45
67
|
this.composer = new EffectComposer(this.renderer);
|
|
46
68
|
this.renderPass = new RenderPass(this.scene, this.camera);
|
|
@@ -142,7 +164,9 @@ export class PostFxSystem {
|
|
|
142
164
|
this.renderer.setRenderTarget(prevTarget);
|
|
143
165
|
}
|
|
144
166
|
}
|
|
145
|
-
this.
|
|
167
|
+
this.withAoDepthProxiesVisible(Boolean(this.aoPass?.enabled), () => {
|
|
168
|
+
this.composer?.render();
|
|
169
|
+
});
|
|
146
170
|
return;
|
|
147
171
|
}
|
|
148
172
|
this.renderer.render(this.scene, this.camera);
|
|
@@ -178,6 +202,65 @@ export class PostFxSystem {
|
|
|
178
202
|
height: Math.max(1, Math.round(size.y)),
|
|
179
203
|
};
|
|
180
204
|
}
|
|
205
|
+
ensureRendererContextAttributes() {
|
|
206
|
+
const context = this.renderer.getContext();
|
|
207
|
+
if (context.isContextLost?.()) {
|
|
208
|
+
if (!this.contextAttributesWarningIssued) {
|
|
209
|
+
console.warn("[PostFxSystem] post-processing disabled while WebGL context is lost.");
|
|
210
|
+
this.contextAttributesWarningIssued = true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const getContextAttributes = context.getContextAttributes?.bind(context);
|
|
215
|
+
const attributes = getContextAttributes?.() ?? null;
|
|
216
|
+
const fallbackAttributes = {
|
|
217
|
+
alpha: attributes?.alpha ?? this.renderer.getClearAlpha() < 1,
|
|
218
|
+
antialias: attributes?.antialias ?? false,
|
|
219
|
+
depth: attributes?.depth ?? true,
|
|
220
|
+
failIfMajorPerformanceCaveat: attributes?.failIfMajorPerformanceCaveat ?? false,
|
|
221
|
+
powerPreference: attributes?.powerPreference ?? "default",
|
|
222
|
+
premultipliedAlpha: attributes?.premultipliedAlpha ?? true,
|
|
223
|
+
preserveDrawingBuffer: attributes?.preserveDrawingBuffer ?? false,
|
|
224
|
+
stencil: attributes?.stencil ?? false,
|
|
225
|
+
};
|
|
226
|
+
try {
|
|
227
|
+
Object.defineProperty(context, "getContextAttributes", {
|
|
228
|
+
configurable: true,
|
|
229
|
+
value: () => getContextAttributes?.() ?? fallbackAttributes,
|
|
230
|
+
});
|
|
231
|
+
if (!attributes && !this.contextAttributesWarningIssued) {
|
|
232
|
+
console.warn("[PostFxSystem] using fallback WebGL context attributes for post-processing.");
|
|
233
|
+
this.contextAttributesWarningIssued = true;
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
if (!this.contextAttributesWarningIssued) {
|
|
239
|
+
console.warn("[PostFxSystem] post-processing disabled because WebGL context attributes are unavailable.");
|
|
240
|
+
this.contextAttributesWarningIssued = true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
withAoDepthProxiesVisible(enabled, renderFn) {
|
|
246
|
+
if (!enabled)
|
|
247
|
+
return renderFn();
|
|
248
|
+
const visibility = new Map();
|
|
249
|
+
this.scene.traverse((object) => {
|
|
250
|
+
if (object.userData?.[AO_DEPTH_PROXY_USER_DATA_KEY] !== true)
|
|
251
|
+
return;
|
|
252
|
+
visibility.set(object, object.visible);
|
|
253
|
+
object.visible = true;
|
|
254
|
+
});
|
|
255
|
+
try {
|
|
256
|
+
return renderFn();
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
visibility.forEach((visible, object) => {
|
|
260
|
+
object.visible = visible;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
181
264
|
dispose() {
|
|
182
265
|
if (this.composer) {
|
|
183
266
|
this.composer.dispose();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rendererSystem.d.ts","sourceRoot":"","sources":["../../src/systems/rendererSystem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"rendererSystem.d.ts","sourceRoot":"","sources":["../../src/systems/rendererSystem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,iBAAiB,CAAC;AAuBvE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,aAAa,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAuBhG"}
|