@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.
Files changed (68) hide show
  1. package/dist/autoGenerate.d.ts.map +1 -1
  2. package/dist/autoGenerate.js +7 -1
  3. package/dist/camera.d.ts +1 -0
  4. package/dist/camera.d.ts.map +1 -1
  5. package/dist/camera.js +8 -1
  6. package/dist/engine/ViewerEngine.d.ts +20 -0
  7. package/dist/engine/ViewerEngine.d.ts.map +1 -1
  8. package/dist/engine/ViewerEngine.js +254 -63
  9. package/dist/engine/types.d.ts +69 -3
  10. package/dist/engine/types.d.ts.map +1 -1
  11. package/dist/exports/download.d.ts +6 -0
  12. package/dist/exports/download.d.ts.map +1 -1
  13. package/dist/exports/download.js +142 -0
  14. package/dist/exports/three-export.d.ts +4 -1
  15. package/dist/exports/three-export.d.ts.map +1 -1
  16. package/dist/exports/three-export.js +138 -4
  17. package/dist/index.d.ts +13 -9
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -4
  20. package/dist/materials/architectural.d.ts +3 -3
  21. package/dist/materials/architectural.d.ts.map +1 -1
  22. package/dist/materials/architectural.js +295 -37
  23. package/dist/materials/catalogue-data.d.ts +15 -0
  24. package/dist/materials/catalogue-data.d.ts.map +1 -1
  25. package/dist/materials/catalogue-data.js +16 -0
  26. package/dist/materials/presets.d.ts +15 -8
  27. package/dist/materials/presets.d.ts.map +1 -1
  28. package/dist/materials/presets.js +140 -39
  29. package/dist/materials/resolve.d.ts +42 -0
  30. package/dist/materials/resolve.d.ts.map +1 -1
  31. package/dist/materials/resolve.js +228 -13
  32. package/dist/materials/types.d.ts +32 -3
  33. package/dist/materials/types.d.ts.map +1 -1
  34. package/dist/presets/defaults.d.ts.map +1 -1
  35. package/dist/presets/defaults.js +17 -1
  36. package/dist/presets/sciencePresets.d.ts.map +1 -1
  37. package/dist/presets/sciencePresets.js +167 -50
  38. package/dist/scene.d.ts +28 -4
  39. package/dist/scene.d.ts.map +1 -1
  40. package/dist/scene.js +196 -31
  41. package/dist/sceneSemanticRegistry.d.ts +64 -0
  42. package/dist/sceneSemanticRegistry.d.ts.map +1 -0
  43. package/dist/sceneSemanticRegistry.js +199 -0
  44. package/dist/sky/scienceSky.d.ts.map +1 -1
  45. package/dist/sky/scienceSky.js +16 -0
  46. package/dist/systems/debugSystem.d.ts +24 -1
  47. package/dist/systems/debugSystem.d.ts.map +1 -1
  48. package/dist/systems/debugSystem.js +324 -77
  49. package/dist/systems/environmentSystem.d.ts +5 -4
  50. package/dist/systems/environmentSystem.d.ts.map +1 -1
  51. package/dist/systems/environmentSystem.js +138 -62
  52. package/dist/systems/lightingSystem.d.ts +10 -1
  53. package/dist/systems/lightingSystem.d.ts.map +1 -1
  54. package/dist/systems/lightingSystem.js +118 -17
  55. package/dist/systems/postfxSystem.d.ts +5 -1
  56. package/dist/systems/postfxSystem.d.ts.map +1 -1
  57. package/dist/systems/postfxSystem.js +84 -1
  58. package/dist/systems/rendererSystem.d.ts.map +1 -1
  59. package/dist/systems/rendererSystem.js +1 -0
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/dist/uploads/geometry3dm.d.ts.map +1 -1
  62. package/dist/uploads/geometry3dm.js +1 -0
  63. package/dist/uploads/grasshopper.d.ts.map +1 -1
  64. package/dist/uploads/grasshopper.js +63 -5
  65. package/dist/uploads/mesh.js +5 -5
  66. package/dist/uploads/types.d.ts +4 -0
  67. package/dist/uploads/types.d.ts.map +1 -1
  68. 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(camera) {
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 skyGeometry = new THREE.SphereGeometry(this.gradientBaseRadius, 64, 64);
127
- const skyMaterial = new THREE.ShaderMaterial({
128
- uniforms: {
129
- topColor: { value: new THREE.Color(gradient.topColor) },
130
- horizonColor: { value: new THREE.Color(gradient.horizonColor) },
131
- sunPosition: { value: new THREE.Vector3(...gradient.sunPosition) },
132
- sunIntensity: { value: gradient.sunIntensity },
133
- sunSize: { value: gradient.sunSize },
134
- },
135
- vertexShader: `
136
- varying vec3 vPosition;
137
- void main() {
138
- vPosition = position;
139
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
140
- }
141
- `,
142
- fragmentShader: `
143
- uniform vec3 topColor;
144
- uniform vec3 horizonColor;
145
- uniform vec3 sunPosition;
146
- uniform float sunIntensity;
147
- uniform float sunSize;
148
- varying vec3 vPosition;
149
- void main() {
150
- vec3 direction = normalize(vPosition);
151
- float h = direction.y;
152
- float gradientFactor = smoothstep(-0.1, 0.4, h);
153
- vec3 color = mix(horizonColor, topColor, gradientFactor);
154
- vec3 sunDir = normalize(sunPosition);
155
- float sunDist = distance(direction, sunDir);
156
- float sun = smoothstep(sunSize, 0.0, sunDist) * sunIntensity;
157
- color = mix(color, topColor, sun);
158
- gl_FragColor = vec4(color, 1.0);
159
- }
160
- `,
161
- side: THREE.BackSide,
162
- depthWrite: false,
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
- const usePmrem = hdr.usePmrem ?? false;
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
- const usePmrem = cube.usePmrem ?? false;
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;gBAE3B,KAAK,EAAE,KAAK,CAAC,KAAK;IAI9B,KAAK,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI;IAuD3C,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,GAAG;IAKX,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,KAAK;IAWb,OAAO,CAAC,eAAe;CAcxB"}
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 lightTarget = preset.sceneExtras?.lightTarget ?? [0, 1.5, 0];
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.set(...key.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.set(...fill.position);
48
- light.target.position.set(lightTarget[0], lightTarget[1], lightTarget[2]);
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
- configureShadow(light, config) {
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 cameraSize = config?.cameraSize ?? 25;
89
- light.shadow.camera.left = -cameraSize;
90
- light.shadow.camera.right = cameraSize;
91
- light.shadow.camera.top = cameraSize;
92
- light.shadow.camera.bottom = -cameraSize;
93
- light.shadow.camera.near = config?.near ?? 0.1;
94
- light.shadow.camera.far = config?.far ?? 60;
95
- const mapSize = config?.mapSize ?? 2048;
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
- constructor(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera);
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;AAEpD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAA0B;IACxC,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;gBAEtC,QAAQ,EAAE,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,iBAAiB;IAM9F,KAAK,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,IAAI;IAiHlC,MAAM,IAAI,IAAI;IAoBd,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAwB3C,OAAO,CAAC,oBAAoB;IAS5B,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
+ {"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.composer.render();
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;AAsBvE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,aAAa,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAuBhG"}
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"}
@@ -4,6 +4,7 @@ const DEFAULT_CONFIG = {
4
4
  toneMappingExposure: 1.0,
5
5
  outputColorSpace: "srgb",
6
6
  maxPixelRatio: 1.75,
7
+ powerPreference: "high-performance",
7
8
  antialias: true,
8
9
  alpha: false,
9
10
  preserveDrawingBuffer: false,