entangled-worlds-orbiters-shared 5.1.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/textureConfig.d.ts +2 -15
- package/dist/config/textureConfig.js +1 -43
- package/dist/loaders/index.d.ts +0 -2
- package/dist/loaders/index.js +0 -1
- package/dist/loaders/ktx2.js +2 -2
- package/dist/sky/index.js +1 -1
- package/dist/sky/sky.js +1 -1
- package/dist/star/bounds.d.ts +1 -1
- package/dist/star/bounds.js +1 -1
- package/dist/star/params.d.ts +1 -1
- package/dist/star/params.js +1 -1
- package/dist/star/starBody.d.ts +1 -1
- package/dist/star/starManager.d.ts +1 -1
- package/package.json +1 -1
- package/public/config/textures.json +0 -73
- package/src/config/index.ts +0 -2
- package/src/config/textureConfig.ts +2 -53
- package/src/loaders/index.ts +0 -2
- package/src/loaders/ktx2.ts +2 -2
- package/src/sky/index.ts +1 -1
- package/src/sky/sky.ts +1 -1
- package/src/star/bounds.ts +1 -1
- package/src/star/params.ts +1 -1
- package/src/star/starBody.ts +1 -1
- package/src/star/starManager.ts +1 -1
- package/dist/loaders/cubeMapLoader.d.ts +0 -58
- package/dist/loaders/cubeMapLoader.js +0 -291
- package/src/loaders/cubeMapLoader.ts +0 -342
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A shared library containing core components for 3D world rendering, moons system
|
|
|
6
6
|
|
|
7
7
|
- **World System**: Core world rendering with textured spheres, normal materials, GLB models, and cloud shells
|
|
8
8
|
- **Moons System**: Orbital moon generation and management
|
|
9
|
-
- **Loaders**: Texture
|
|
9
|
+
- **Loaders**: Texture and GLB loading utilities
|
|
10
10
|
- **Core Rendering**: Lighting, Fresnel effects, and material utilities
|
|
11
11
|
- **Configuration**: Texture and configuration management system
|
|
12
12
|
|
|
@@ -57,7 +57,6 @@ import * as EntangledWorlds from 'entangled-worlds-orbiters-shared';
|
|
|
57
57
|
- `moonsGeneration` - Moon generation logic
|
|
58
58
|
|
|
59
59
|
### Loaders
|
|
60
|
-
- `cubeMapLoader` - Skybox loading
|
|
61
60
|
- `glbLoader` - GLB loading utilities
|
|
62
61
|
- `bodyTextureLoader` - Texture loading
|
|
63
62
|
|
|
@@ -150,7 +149,7 @@ Defaults:
|
|
|
150
149
|
|
|
151
150
|
Effects:
|
|
152
151
|
- All URLs returned by `config/textureConfig` helpers are absolute.
|
|
153
|
-
-
|
|
152
|
+
- Moons generation resolves relative paths against the configured base.
|
|
154
153
|
- Cloud shell textures passed to `world.makeSphere` are resolved to absolute URLs.
|
|
155
154
|
|
|
156
155
|
## Peer Dependencies
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture,
|
|
1
|
+
export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getTextureFallbackUrl } from './textureConfig.js';
|
|
2
2
|
export type { TextureConfig, TextureCategory, TextureItem, TextureChoice, TextureConfigMetadata } from './textureConfig.js';
|
|
3
3
|
export { setPublicBase, getPublicBase, resolvePublicUrl } from './env.js';
|
package/dist/config/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Configuration exports
|
|
2
|
-
export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture,
|
|
2
|
+
export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getTextureFallbackUrl } from './textureConfig.js';
|
|
3
3
|
// runtime base helpers
|
|
4
4
|
export { setPublicBase, getPublicBase, resolvePublicUrl } from './env.js';
|
|
@@ -9,12 +9,11 @@ export interface TextureItem {
|
|
|
9
9
|
fallback?: string;
|
|
10
10
|
tags?: string[];
|
|
11
11
|
}
|
|
12
|
-
/** One catalogue category (clouds, moons,
|
|
12
|
+
/** One catalogue category (clouds, moons, …). */
|
|
13
13
|
export interface TextureCategory {
|
|
14
14
|
category?: string;
|
|
15
15
|
basePath?: string;
|
|
16
16
|
defaultTexture?: string;
|
|
17
|
-
defaultSkybox?: string;
|
|
18
17
|
items?: TextureItem[];
|
|
19
18
|
}
|
|
20
19
|
/** The texture catalogue as loaded from the config URL. */
|
|
@@ -70,7 +69,7 @@ export type TextureChoice = {
|
|
|
70
69
|
export declare function getTextureChoices(category: string, config?: TextureConfig | null): Promise<TextureChoice[]>;
|
|
71
70
|
/**
|
|
72
71
|
* Get texture options for a specific category (clouds, moons, etc.)
|
|
73
|
-
* @param {string} category - Texture category (clouds, moons
|
|
72
|
+
* @param {string} category - Texture category (clouds, moons)
|
|
74
73
|
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
75
74
|
* @returns {Promise<Record<string, string>>} `{ label: url }`, suitable for dropdown option lists
|
|
76
75
|
*/
|
|
@@ -114,18 +113,6 @@ export declare function getTextureUrl(category: string, textureId: string, confi
|
|
|
114
113
|
* @returns {Promise<TextureItem[]>} Array of matching texture definitions
|
|
115
114
|
*/
|
|
116
115
|
export declare function searchTexturesByTags(category: string, tags: string[], config?: TextureConfig | null): Promise<TextureItem[]>;
|
|
117
|
-
/**
|
|
118
|
-
* Get skybox options formatted for GUI dropdowns
|
|
119
|
-
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
120
|
-
* @returns {Promise<Record<string, string>>} `{ label: id }`
|
|
121
|
-
*/
|
|
122
|
-
export declare function getSkyboxOptions(config?: TextureConfig | null): Promise<Record<string, string>>;
|
|
123
|
-
/**
|
|
124
|
-
* Get default skybox ID
|
|
125
|
-
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
126
|
-
* @returns {Promise<string>} Default skybox ID
|
|
127
|
-
*/
|
|
128
|
-
export declare function getDefaultSkybox(config?: TextureConfig | null): Promise<string>;
|
|
129
116
|
/**
|
|
130
117
|
* Reload configuration (force refresh)
|
|
131
118
|
* @param {string} configUrl - Override config URL
|
|
@@ -117,7 +117,7 @@ export async function getTextureChoices(category, config) {
|
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
119
|
* Get texture options for a specific category (clouds, moons, etc.)
|
|
120
|
-
* @param {string} category - Texture category (clouds, moons
|
|
120
|
+
* @param {string} category - Texture category (clouds, moons)
|
|
121
121
|
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
122
122
|
* @returns {Promise<Record<string, string>>} `{ label: url }`, suitable for dropdown option lists
|
|
123
123
|
*/
|
|
@@ -215,40 +215,6 @@ export async function searchTexturesByTags(category, tags, config) {
|
|
|
215
215
|
}
|
|
216
216
|
return categoryConfig.items.filter(item => item.tags && tags.some(tag => item.tags.includes(tag)));
|
|
217
217
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Get skybox options formatted for GUI dropdowns
|
|
220
|
-
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
221
|
-
* @returns {Promise<Record<string, string>>} `{ label: id }`
|
|
222
|
-
*/
|
|
223
|
-
export async function getSkyboxOptions(config) {
|
|
224
|
-
const textureConfig = await resolveConfig(config);
|
|
225
|
-
const skyboxConfig = textureConfig.textures.skyboxes;
|
|
226
|
-
if (!skyboxConfig) {
|
|
227
|
-
console.warn('[TextureConfig] Skyboxes category not found');
|
|
228
|
-
return {};
|
|
229
|
-
}
|
|
230
|
-
const options = {};
|
|
231
|
-
// Reads `items` unguarded, unlike the other readers: a skyboxes category without items throws here.
|
|
232
|
-
skyboxConfig.items.forEach(item => {
|
|
233
|
-
const label = item.label || item.id;
|
|
234
|
-
const id = item.id;
|
|
235
|
-
options[label] = id;
|
|
236
|
-
});
|
|
237
|
-
return options;
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Get default skybox ID
|
|
241
|
-
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
242
|
-
* @returns {Promise<string>} Default skybox ID
|
|
243
|
-
*/
|
|
244
|
-
export async function getDefaultSkybox(config) {
|
|
245
|
-
const textureConfig = await resolveConfig(config);
|
|
246
|
-
const skyboxConfig = textureConfig.textures.skyboxes;
|
|
247
|
-
if (!skyboxConfig) {
|
|
248
|
-
return '001'; // fallback
|
|
249
|
-
}
|
|
250
|
-
return skyboxConfig.defaultSkybox || '001';
|
|
251
|
-
}
|
|
252
218
|
/**
|
|
253
219
|
* Hardcoded fallback configuration (minimal)
|
|
254
220
|
* Used when remote config loading fails
|
|
@@ -275,14 +241,6 @@ function getHardcodedFallback() {
|
|
|
275
241
|
items: [
|
|
276
242
|
{ id: "default-moon", filename: "moon-equirectangular.ktx2", label: "Default Moon", tags: ["lunar"] }
|
|
277
243
|
]
|
|
278
|
-
},
|
|
279
|
-
skyboxes: {
|
|
280
|
-
category: "environment",
|
|
281
|
-
basePath: "/skybox",
|
|
282
|
-
defaultSkybox: "001",
|
|
283
|
-
items: [
|
|
284
|
-
{ id: "001", label: "Deep Space", tags: ["space"] }
|
|
285
|
-
]
|
|
286
244
|
}
|
|
287
245
|
}
|
|
288
246
|
};
|
package/dist/loaders/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
|
|
2
1
|
export { loadGLB } from './glbLoader.js';
|
|
3
2
|
export { loadTexture } from './bodyTextureLoader.js';
|
|
4
3
|
export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
|
|
5
|
-
export type { SkyboxResource } from './cubeMapLoader.js';
|
package/dist/loaders/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Loaders exports
|
|
2
|
-
export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
|
|
3
2
|
export { loadGLB } from './glbLoader.js';
|
|
4
3
|
export { loadTexture } from './bodyTextureLoader.js';
|
|
5
4
|
export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
|
package/dist/loaders/ktx2.js
CHANGED
|
@@ -4,12 +4,12 @@ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
|
4
4
|
*
|
|
5
5
|
* `KTX2Loader.detectSupport(renderer)` binds the loader's Basis transcoder worker pool to ONE WebGL
|
|
6
6
|
* context. A single shared loader therefore only decodes textures for the FIRST renderer it saw —
|
|
7
|
-
* every other view's planets
|
|
7
|
+
* every other view's planets silently fail and render untextured (dark). That breaks any
|
|
8
8
|
* multi-view page: N orbiters in one realm, or N Entangled World planets shown together.
|
|
9
9
|
*
|
|
10
10
|
* Keying the loader by renderer gives each view its own correctly-bound loader. Single-renderer apps
|
|
11
11
|
* (a single orbiter, today's Entangled World) get exactly ONE entry, so behaviour is byte-identical.
|
|
12
|
-
* (Was: a single `window.__EW_KTX2_LOADER` global shared by bodyTexture/glb
|
|
12
|
+
* (Was: a single `window.__EW_KTX2_LOADER` global shared by the bodyTexture/glb loaders.)
|
|
13
13
|
*/
|
|
14
14
|
const loadersByRenderer = new WeakMap();
|
|
15
15
|
function makeLoader(renderer) {
|
package/dist/sky/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ./sky — the sky behind a world: a parametric star field (seeded, or the world's own sky as seen
|
|
2
2
|
// from its host star, read from the backend's precomputed file), an optional nebula + Milky Way
|
|
3
|
-
// baked once per change, and constellation figures.
|
|
3
|
+
// baked once per change, and constellation figures.
|
|
4
4
|
export { createSky } from './sky.js';
|
|
5
5
|
export { DEFAULT_SKY_PARAMS, SKY_PARAM_RANGES, SKY_LOOK_KEYS, SKY_SHAPE_KEYS, normalizeSkyParams, } from './params.js';
|
|
6
6
|
export { parseWorldSky, WORLD_SKY_MAGIC, WORLD_SKY_FORMAT } from './worldSkyFile.js';
|
package/dist/sky/sky.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// LineSegments). All three draw after the opaque bodies with depth test
|
|
5
5
|
// on, so sky behind a planet costs nothing.
|
|
6
6
|
//
|
|
7
|
-
// Cost, measured against the six-face painted
|
|
7
|
+
// Cost, measured against the six-face painted sky it replaced (bench/sky/results): stars alone about
|
|
8
8
|
// 40% of its frame time; with the nebula about the same frame time, at 1/5–1/15 of its GPU memory
|
|
9
9
|
// and with nothing to download.
|
|
10
10
|
import * as THREE from 'three';
|
package/dist/star/bounds.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface MinStarDistanceInput extends Partial<SceneSizes> {
|
|
|
30
30
|
* that the primary spans at most `MAX_STAR_ANGULAR_RADIUS_DEG`. Pure.
|
|
31
31
|
*/
|
|
32
32
|
export declare function minStarDistance(input: MinStarDistanceInput): number;
|
|
33
|
-
/** Sky margin: the system keeps this fraction of the sky radius clear of the
|
|
33
|
+
/** Sky margin: the system keeps this fraction of the sky radius clear of the sky. */
|
|
34
34
|
export declare const SKY_MARGIN_FRACTION = 0.02;
|
|
35
35
|
/**
|
|
36
36
|
* Farthest barycentre distance that keeps a system of `systemExtent` inside a sky of `skyRadius`
|
package/dist/star/bounds.js
CHANGED
|
@@ -34,7 +34,7 @@ export function minStarDistance(input) {
|
|
|
34
34
|
const angular = pos(input.primaryRadius, 0) / Math.tan((MAX_STAR_ANGULAR_RADIUS_DEG * Math.PI) / 180);
|
|
35
35
|
return Math.max(clear, angular);
|
|
36
36
|
}
|
|
37
|
-
/** Sky margin: the system keeps this fraction of the sky radius clear of the
|
|
37
|
+
/** Sky margin: the system keeps this fraction of the sky radius clear of the sky. */
|
|
38
38
|
export const SKY_MARGIN_FRACTION = 0.02;
|
|
39
39
|
/**
|
|
40
40
|
* Farthest barycentre distance that keeps a system of `systemExtent` inside a sky of `skyRadius`
|
package/dist/star/params.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export declare const DEFAULT_STAR_PARAMS: Readonly<StarParams>;
|
|
|
45
45
|
/**
|
|
46
46
|
* Accepted ranges; values outside are clamped (azimuth wraps). Sized so the star reads as a
|
|
47
47
|
* background host star: at the minimum (and default) distance the largest radius spans ~10° from the
|
|
48
|
-
* planet, 220 stays inside a 500-unit
|
|
48
|
+
* planet, 220 stays inside a 500-unit sky, and no distance puts it among the planet's moons (hosts pass their scene size to StarManager for the
|
|
49
49
|
* exact, non-colliding minimum — see `minStarDistance`).
|
|
50
50
|
*/
|
|
51
51
|
export declare const STAR_PARAM_RANGES: Readonly<{
|
package/dist/star/params.js
CHANGED
|
@@ -26,7 +26,7 @@ export const DEFAULT_STAR_PARAMS = Object.freeze({
|
|
|
26
26
|
/**
|
|
27
27
|
* Accepted ranges; values outside are clamped (azimuth wraps). Sized so the star reads as a
|
|
28
28
|
* background host star: at the minimum (and default) distance the largest radius spans ~10° from the
|
|
29
|
-
* planet, 220 stays inside a 500-unit
|
|
29
|
+
* planet, 220 stays inside a 500-unit sky, and no distance puts it among the planet's moons (hosts pass their scene size to StarManager for the
|
|
30
30
|
* exact, non-colliding minimum — see `minStarDistance`).
|
|
31
31
|
*/
|
|
32
32
|
export const STAR_PARAM_RANGES = Object.freeze({
|
package/dist/star/starBody.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface StarBody {
|
|
|
30
30
|
setMinDistance(d: number): void;
|
|
31
31
|
/**
|
|
32
32
|
* Upper bound for the barycentre distance (0 / Infinity = none), e.g. to stay inside the host's
|
|
33
|
-
*
|
|
33
|
+
* sky. The minimum wins when the two conflict.
|
|
34
34
|
*/
|
|
35
35
|
setMaxDistance(d: number): void;
|
|
36
36
|
/** The distance actually used: max(minDistance, min(params.distance, maxDistance)). */
|
|
@@ -6,7 +6,7 @@ import { type StarQuality } from './quality.js';
|
|
|
6
6
|
import { type MinStarDistanceInput } from './bounds.js';
|
|
7
7
|
/** The host's scene sizes: `outerRadius` directly, or the planet/glow/moon sizes to derive it. */
|
|
8
8
|
export type StarSceneBounds = Pick<MinStarDistanceInput, 'planetRadius' | 'moonOrbitRadius' | 'moonRadius' | 'cloudOrGlowScale' | 'outerRadius' | 'margin'> & {
|
|
9
|
-
/** Radius of the host's sky (e.g. 250 for a 500-unit
|
|
9
|
+
/** Radius of the host's sky (e.g. 250 for a 500-unit sky): the whole system stays inside it. */
|
|
10
10
|
skyRadius?: number;
|
|
11
11
|
};
|
|
12
12
|
export interface StarManagerOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "entangled-worlds-orbiters-shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Shared library for entangled worlds orbiters - contains core world rendering, moons system, and UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -286,79 +286,6 @@
|
|
|
286
286
|
"fallback": "silver-glow.jpg"
|
|
287
287
|
}
|
|
288
288
|
]
|
|
289
|
-
},
|
|
290
|
-
"skyboxes": {
|
|
291
|
-
"category": "environment",
|
|
292
|
-
"basePath": "/skybox",
|
|
293
|
-
"defaultSkybox": "001",
|
|
294
|
-
"items": [
|
|
295
|
-
{
|
|
296
|
-
"id": "001",
|
|
297
|
-
"label": "Deep Space",
|
|
298
|
-
"description": "Deep space with distant stars",
|
|
299
|
-
"tags": ["space", "dark", "minimal"]
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
"id": "002",
|
|
303
|
-
"label": "Nebula Field",
|
|
304
|
-
"description": "Colorful nebula formations",
|
|
305
|
-
"tags": ["nebula", "colorful", "cosmic"]
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
"id": "003",
|
|
309
|
-
"label": "Galaxy Center",
|
|
310
|
-
"description": "Dense galactic core region",
|
|
311
|
-
"tags": ["galaxy", "dense", "bright"]
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
"id": "004",
|
|
315
|
-
"label": "Binary System",
|
|
316
|
-
"description": "Binary star system environment",
|
|
317
|
-
"tags": ["binary", "stars", "orange"]
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
"id": "005",
|
|
321
|
-
"label": "Cosmic Web",
|
|
322
|
-
"description": "Large-scale cosmic structure",
|
|
323
|
-
"tags": ["cosmic", "web", "structure"]
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
"id": "006",
|
|
327
|
-
"label": "Stellar Nursery",
|
|
328
|
-
"description": "Star formation region",
|
|
329
|
-
"tags": ["formation", "young", "bright"]
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
"id": "007",
|
|
333
|
-
"label": "Dark Matter",
|
|
334
|
-
"description": "Dark matter visualization",
|
|
335
|
-
"tags": ["dark", "matter", "theoretical"]
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
"id": "008",
|
|
339
|
-
"label": "Quantum Foam",
|
|
340
|
-
"description": "Quantum scale space-time",
|
|
341
|
-
"tags": ["quantum", "foam", "microscopic"]
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
"id": "009",
|
|
345
|
-
"label": "Event Horizon",
|
|
346
|
-
"description": "Black hole event horizon",
|
|
347
|
-
"tags": ["black hole", "horizon", "gravity"]
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
"id": "010",
|
|
351
|
-
"label": "Multiverse",
|
|
352
|
-
"description": "Multiple universe visualization",
|
|
353
|
-
"tags": ["multiverse", "infinite", "parallel"]
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
"id": "011",
|
|
357
|
-
"label": "Void Space",
|
|
358
|
-
"description": "Empty cosmic void region",
|
|
359
|
-
"tags": ["void", "empty", "minimal"]
|
|
360
|
-
}
|
|
361
|
-
]
|
|
362
289
|
}
|
|
363
290
|
}
|
|
364
291
|
}
|
package/src/config/index.ts
CHANGED
|
@@ -16,12 +16,11 @@ export interface TextureItem {
|
|
|
16
16
|
tags?: string[];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/** One catalogue category (clouds, moons,
|
|
19
|
+
/** One catalogue category (clouds, moons, …). */
|
|
20
20
|
export interface TextureCategory {
|
|
21
21
|
category?: string;
|
|
22
22
|
basePath?: string;
|
|
23
23
|
defaultTexture?: string;
|
|
24
|
-
defaultSkybox?: string;
|
|
25
24
|
items?: TextureItem[];
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -196,7 +195,7 @@ export async function getTextureChoices(category: string, config?: TextureConfig
|
|
|
196
195
|
|
|
197
196
|
/**
|
|
198
197
|
* Get texture options for a specific category (clouds, moons, etc.)
|
|
199
|
-
* @param {string} category - Texture category (clouds, moons
|
|
198
|
+
* @param {string} category - Texture category (clouds, moons)
|
|
200
199
|
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
201
200
|
* @returns {Promise<Record<string, string>>} `{ label: url }`, suitable for dropdown option lists
|
|
202
201
|
*/
|
|
@@ -309,48 +308,6 @@ export async function searchTexturesByTags(category: string, tags: string[], con
|
|
|
309
308
|
);
|
|
310
309
|
}
|
|
311
310
|
|
|
312
|
-
/**
|
|
313
|
-
* Get skybox options formatted for GUI dropdowns
|
|
314
|
-
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
315
|
-
* @returns {Promise<Record<string, string>>} `{ label: id }`
|
|
316
|
-
*/
|
|
317
|
-
export async function getSkyboxOptions(config?: TextureConfig | null): Promise<Record<string, string>> {
|
|
318
|
-
const textureConfig = await resolveConfig(config);
|
|
319
|
-
const skyboxConfig = textureConfig.textures.skyboxes;
|
|
320
|
-
|
|
321
|
-
if (!skyboxConfig) {
|
|
322
|
-
console.warn('[TextureConfig] Skyboxes category not found');
|
|
323
|
-
return {};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const options: Record<string, string> = {};
|
|
327
|
-
|
|
328
|
-
// Reads `items` unguarded, unlike the other readers: a skyboxes category without items throws here.
|
|
329
|
-
skyboxConfig.items!.forEach(item => {
|
|
330
|
-
const label = item.label || item.id;
|
|
331
|
-
const id = item.id;
|
|
332
|
-
options[label] = id;
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
return options;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Get default skybox ID
|
|
340
|
-
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
341
|
-
* @returns {Promise<string>} Default skybox ID
|
|
342
|
-
*/
|
|
343
|
-
export async function getDefaultSkybox(config?: TextureConfig | null): Promise<string> {
|
|
344
|
-
const textureConfig = await resolveConfig(config);
|
|
345
|
-
const skyboxConfig = textureConfig.textures.skyboxes;
|
|
346
|
-
|
|
347
|
-
if (!skyboxConfig) {
|
|
348
|
-
return '001'; // fallback
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return skyboxConfig.defaultSkybox || '001';
|
|
352
|
-
}
|
|
353
|
-
|
|
354
311
|
/**
|
|
355
312
|
* Hardcoded fallback configuration (minimal)
|
|
356
313
|
* Used when remote config loading fails
|
|
@@ -377,14 +334,6 @@ function getHardcodedFallback(): TextureConfig {
|
|
|
377
334
|
items: [
|
|
378
335
|
{ id: "default-moon", filename: "moon-equirectangular.ktx2", label: "Default Moon", tags: ["lunar"] }
|
|
379
336
|
]
|
|
380
|
-
},
|
|
381
|
-
skyboxes: {
|
|
382
|
-
category: "environment",
|
|
383
|
-
basePath: "/skybox",
|
|
384
|
-
defaultSkybox: "001",
|
|
385
|
-
items: [
|
|
386
|
-
{ id: "001", label: "Deep Space", tags: ["space"] }
|
|
387
|
-
]
|
|
388
337
|
}
|
|
389
338
|
}
|
|
390
339
|
};
|
package/src/loaders/index.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// Loaders exports
|
|
2
|
-
export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
|
|
3
2
|
export { loadGLB } from './glbLoader.js';
|
|
4
3
|
export { loadTexture } from './bodyTextureLoader.js';
|
|
5
4
|
export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
|
|
6
|
-
export type { SkyboxResource } from './cubeMapLoader.js';
|
package/src/loaders/ktx2.ts
CHANGED
|
@@ -6,12 +6,12 @@ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
|
6
6
|
*
|
|
7
7
|
* `KTX2Loader.detectSupport(renderer)` binds the loader's Basis transcoder worker pool to ONE WebGL
|
|
8
8
|
* context. A single shared loader therefore only decodes textures for the FIRST renderer it saw —
|
|
9
|
-
* every other view's planets
|
|
9
|
+
* every other view's planets silently fail and render untextured (dark). That breaks any
|
|
10
10
|
* multi-view page: N orbiters in one realm, or N Entangled World planets shown together.
|
|
11
11
|
*
|
|
12
12
|
* Keying the loader by renderer gives each view its own correctly-bound loader. Single-renderer apps
|
|
13
13
|
* (a single orbiter, today's Entangled World) get exactly ONE entry, so behaviour is byte-identical.
|
|
14
|
-
* (Was: a single `window.__EW_KTX2_LOADER` global shared by bodyTexture/glb
|
|
14
|
+
* (Was: a single `window.__EW_KTX2_LOADER` global shared by the bodyTexture/glb loaders.)
|
|
15
15
|
*/
|
|
16
16
|
const loadersByRenderer = new WeakMap<THREE.WebGLRenderer, KTX2Loader>();
|
|
17
17
|
|
package/src/sky/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ./sky — the sky behind a world: a parametric star field (seeded, or the world's own sky as seen
|
|
2
2
|
// from its host star, read from the backend's precomputed file), an optional nebula + Milky Way
|
|
3
|
-
// baked once per change, and constellation figures.
|
|
3
|
+
// baked once per change, and constellation figures.
|
|
4
4
|
|
|
5
5
|
export { createSky } from './sky.js';
|
|
6
6
|
export type { Sky, CreateSkyOptions } from './sky.js';
|
package/src/sky/sky.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// LineSegments). All three draw after the opaque bodies with depth test
|
|
5
5
|
// on, so sky behind a planet costs nothing.
|
|
6
6
|
//
|
|
7
|
-
// Cost, measured against the six-face painted
|
|
7
|
+
// Cost, measured against the six-face painted sky it replaced (bench/sky/results): stars alone about
|
|
8
8
|
// 40% of its frame time; with the nebula about the same frame time, at 1/5–1/15 of its GPU memory
|
|
9
9
|
// and with nothing to download.
|
|
10
10
|
|
package/src/star/bounds.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function minStarDistance(input: MinStarDistanceInput): number {
|
|
|
59
59
|
return Math.max(clear, angular);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/** Sky margin: the system keeps this fraction of the sky radius clear of the
|
|
62
|
+
/** Sky margin: the system keeps this fraction of the sky radius clear of the sky. */
|
|
63
63
|
export const SKY_MARGIN_FRACTION = 0.02;
|
|
64
64
|
|
|
65
65
|
/**
|
package/src/star/params.ts
CHANGED
|
@@ -72,7 +72,7 @@ export const DEFAULT_STAR_PARAMS: Readonly<StarParams> = Object.freeze({
|
|
|
72
72
|
/**
|
|
73
73
|
* Accepted ranges; values outside are clamped (azimuth wraps). Sized so the star reads as a
|
|
74
74
|
* background host star: at the minimum (and default) distance the largest radius spans ~10° from the
|
|
75
|
-
* planet, 220 stays inside a 500-unit
|
|
75
|
+
* planet, 220 stays inside a 500-unit sky, and no distance puts it among the planet's moons (hosts pass their scene size to StarManager for the
|
|
76
76
|
* exact, non-colliding minimum — see `minStarDistance`).
|
|
77
77
|
*/
|
|
78
78
|
export const STAR_PARAM_RANGES = Object.freeze({
|
package/src/star/starBody.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface StarBody {
|
|
|
49
49
|
setMinDistance(d: number): void;
|
|
50
50
|
/**
|
|
51
51
|
* Upper bound for the barycentre distance (0 / Infinity = none), e.g. to stay inside the host's
|
|
52
|
-
*
|
|
52
|
+
* sky. The minimum wins when the two conflict.
|
|
53
53
|
*/
|
|
54
54
|
setMaxDistance(d: number): void;
|
|
55
55
|
/** The distance actually used: max(minDistance, min(params.distance, maxDistance)). */
|
package/src/star/starManager.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { maxStarDistance, minStarDistance, type MinStarDistanceInput } from './b
|
|
|
30
30
|
|
|
31
31
|
/** The host's scene sizes: `outerRadius` directly, or the planet/glow/moon sizes to derive it. */
|
|
32
32
|
export type StarSceneBounds = Pick<MinStarDistanceInput, 'planetRadius' | 'moonOrbitRadius' | 'moonRadius' | 'cloudOrGlowScale' | 'outerRadius' | 'margin'> & {
|
|
33
|
-
/** Radius of the host's sky (e.g. 250 for a 500-unit
|
|
33
|
+
/** Radius of the host's sky (e.g. 250 for a 500-unit sky): the whole system stays inside it. */
|
|
34
34
|
skyRadius?: number;
|
|
35
35
|
};
|
|
36
36
|
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
2
|
-
/** A skycube id (`'7'`, `'07_skycube'`, `'skybox-007'`, …) as its three-digit folder name; `'001'` when none. */
|
|
3
|
-
export declare function normalizeSkycubeName(raw: string | null | undefined): string;
|
|
4
|
-
export declare function buildSkycubefaceKeys(skycubeId: string | null | undefined, extension?: string): {
|
|
5
|
-
right: string;
|
|
6
|
-
left: string;
|
|
7
|
-
top: string;
|
|
8
|
-
bottom: string;
|
|
9
|
-
front: string;
|
|
10
|
-
back: string;
|
|
11
|
-
};
|
|
12
|
-
/**
|
|
13
|
-
* Main loader function - always uses explicit face URLs.
|
|
14
|
-
* `renderer` binds the KTX2 transcoder to the right WebGL context. REQUIRED (3.0) when the KTX2
|
|
15
|
-
* path is taken — the old `window.__EW_RENDERER` fallback is gone; thread the owning view's
|
|
16
|
-
* renderer. (The PNG/JPG path is renderer-agnostic, so its absence only bites on KTX2.)
|
|
17
|
-
*
|
|
18
|
-
* - `faceKeys`: explicit face URLs; when absent they are built from `skycubeName`.
|
|
19
|
-
* - `skycubeName`: skybox id (e.g. `'001'`), used when `faceKeys` is absent.
|
|
20
|
-
* - `preferKTX2` (true).
|
|
21
|
-
* - `forceLow`: force (or forbid) the low-resolution faces; omitted = detect the device.
|
|
22
|
-
*
|
|
23
|
-
* Resolves to what `setBackground` takes.
|
|
24
|
-
*/
|
|
25
|
-
export declare function loadCubeMap({ faceKeys, skycubeName, preferKTX2, forceLow, renderer, }?: {
|
|
26
|
-
faceKeys?: {
|
|
27
|
-
right: string;
|
|
28
|
-
left: string;
|
|
29
|
-
top: string;
|
|
30
|
-
bottom: string;
|
|
31
|
-
front: string;
|
|
32
|
-
back: string;
|
|
33
|
-
} | undefined;
|
|
34
|
-
skycubeName?: string | undefined;
|
|
35
|
-
preferKTX2?: boolean | undefined;
|
|
36
|
-
forceLow?: boolean | undefined;
|
|
37
|
-
renderer?: THREE.WebGLRenderer | null | undefined;
|
|
38
|
-
}): Promise<SkyboxResource>;
|
|
39
|
-
/**
|
|
40
|
-
* What `loadCubeMap` resolves to: a skybox mesh wrapper (the KTX2 path) or a cube texture (the
|
|
41
|
-
* PNG/JPG path). Read `mesh` to tell them apart.
|
|
42
|
-
*/
|
|
43
|
-
export type SkyboxResource = {
|
|
44
|
-
__ewType: 'skybox-mesh';
|
|
45
|
-
mesh: THREE.Mesh;
|
|
46
|
-
} | (THREE.CubeTexture & {
|
|
47
|
-
mesh?: undefined;
|
|
48
|
-
});
|
|
49
|
-
/**
|
|
50
|
-
* Show a loaded skybox as the scene's background. A previous skybox mesh or cube-texture background is
|
|
51
|
-
* disposed (a plain-texture background is not). Takes what `loadCubeMap` resolved to (or any texture);
|
|
52
|
-
* anything else, `null` included, clears it.
|
|
53
|
-
*
|
|
54
|
-
*/
|
|
55
|
-
export declare function setBackground(scene: THREE.Scene, resource: unknown): void;
|
|
56
|
-
export declare function initSpaceScene(scene: THREE.Scene, { renderer }?: {
|
|
57
|
-
renderer?: THREE.WebGLRenderer | null;
|
|
58
|
-
}): Promise<void>;
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
2
|
-
import { getPublicBase } from '../config/env.js';
|
|
3
|
-
import { getKTX2LoaderForRenderer } from './ktx2.js';
|
|
4
|
-
/** A skycube id (`'7'`, `'07_skycube'`, `'skybox-007'`, …) as its three-digit folder name; `'001'` when none. */
|
|
5
|
-
export function normalizeSkycubeName(raw) {
|
|
6
|
-
if (!raw)
|
|
7
|
-
return '001';
|
|
8
|
-
const legacy = raw.match(/^(\d{1,2})_skycube(?:_low)?$/);
|
|
9
|
-
if (legacy)
|
|
10
|
-
return String(parseInt(legacy[1], 10)).padStart(3, '0');
|
|
11
|
-
const num = raw.match(/(\d{1,3})/);
|
|
12
|
-
if (num)
|
|
13
|
-
return String(parseInt(num[1], 10)).padStart(3, '0');
|
|
14
|
-
return '001';
|
|
15
|
-
}
|
|
16
|
-
// Standard face order for cube maps
|
|
17
|
-
const FACE_ORDER = ['right', 'left', 'top', 'bottom', 'front', 'back'];
|
|
18
|
-
// Build complete face URL sets for a given skycube ID (now returns relative paths)
|
|
19
|
-
export function buildSkycubefaceKeys(skycubeId, extension = 'ktx2') {
|
|
20
|
-
const id = normalizeSkycubeName(skycubeId);
|
|
21
|
-
const base = `/default/skybox/${id}`;
|
|
22
|
-
return {
|
|
23
|
-
right: `${base}/right.${extension}`,
|
|
24
|
-
left: `${base}/left.${extension}`,
|
|
25
|
-
top: `${base}/top.${extension}`,
|
|
26
|
-
bottom: `${base}/bottom.${extension}`,
|
|
27
|
-
front: `${base}/front.${extension}`,
|
|
28
|
-
back: `${base}/back.${extension}`
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
// Heuristic low-performance detection
|
|
32
|
-
function isLowPerformanceDevice() {
|
|
33
|
-
try {
|
|
34
|
-
const ua = navigator.userAgent.toLowerCase();
|
|
35
|
-
const isMobileUA = /mobi|android|iphone|ipad|ipod/.test(ua);
|
|
36
|
-
// `deviceMemory` is not in every browser, nor in the DOM types.
|
|
37
|
-
const mem = navigator.deviceMemory || 0; // GB
|
|
38
|
-
const cores = navigator.hardwareConcurrency || 0;
|
|
39
|
-
const dpr = window.devicePixelRatio || 1;
|
|
40
|
-
return isMobileUA || mem && mem <= 4 || cores && cores <= 4 || (dpr > 2 && cores <= 6);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// Validate face URLs object
|
|
47
|
-
function validatefaceKeys(faceKeys) {
|
|
48
|
-
if (!faceKeys || typeof faceKeys !== 'object')
|
|
49
|
-
return false;
|
|
50
|
-
const keys = faceKeys;
|
|
51
|
-
return FACE_ORDER.every(face => keys[face] && typeof keys[face] === 'string');
|
|
52
|
-
}
|
|
53
|
-
// Detect extension type from face URLs
|
|
54
|
-
function detectExtension(faceKeys) {
|
|
55
|
-
const url = faceKeys.right || '';
|
|
56
|
-
if (url.toLowerCase().includes('.ktx2'))
|
|
57
|
-
return 'ktx2';
|
|
58
|
-
if (url.toLowerCase().match(/\.(jpg|jpeg|png)$/))
|
|
59
|
-
return 'jpg';
|
|
60
|
-
return 'ktx2'; // default assumption
|
|
61
|
-
}
|
|
62
|
-
// How many faces may be in flight at once. Not six: the KTX2 worker pool three.js gives us holds
|
|
63
|
-
// four transcoder workers, each carrying its own copy of the basis wasm, and that same pool also
|
|
64
|
-
// serves body textures and GLBs. Asking for all six together spawned every worker and queued the
|
|
65
|
-
// other loaders behind the whole sky; measured on cold fetches it was no faster than three.
|
|
66
|
-
const FACES_IN_FLIGHT = 3;
|
|
67
|
-
// Load KTX2 faces and create skybox mesh
|
|
68
|
-
async function loadFacesKTX2(faceKeys, renderer) {
|
|
69
|
-
const loader = getKTX2LoaderForRenderer(renderer);
|
|
70
|
-
const anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
|
|
71
|
-
const loadFace = async (face) => {
|
|
72
|
-
const tex = await loader.loadAsync(faceKeys[face]);
|
|
73
|
-
tex.flipY = false;
|
|
74
|
-
tex.wrapS = THREE.ClampToEdgeWrapping;
|
|
75
|
-
tex.wrapT = THREE.ClampToEdgeWrapping;
|
|
76
|
-
tex.anisotropy = anisotropy;
|
|
77
|
-
return { face, tex };
|
|
78
|
-
};
|
|
79
|
-
// Faces overlap instead of chaining. Awaiting each one in turn made every face wait on the
|
|
80
|
-
// previous face's download *and* its transcode, so the sky arrived six round trips deep.
|
|
81
|
-
const settled = [];
|
|
82
|
-
for (let i = 0; i < FACE_ORDER.length; i += FACES_IN_FLIGHT) {
|
|
83
|
-
const wave = FACE_ORDER.slice(i, i + FACES_IN_FLIGHT).map(face => loadFace(face));
|
|
84
|
-
settled.push(...await Promise.allSettled(wave));
|
|
85
|
-
}
|
|
86
|
-
// allSettled, not all: a bare Promise.all rejects on the first failure and leaks the faces that
|
|
87
|
-
// land after it. Settle every face first, then dispose everything that arrived before rethrowing.
|
|
88
|
-
const loaded = [];
|
|
89
|
-
let failure = null;
|
|
90
|
-
for (const result of settled) {
|
|
91
|
-
if (result.status === 'fulfilled')
|
|
92
|
-
loaded.push(result.value);
|
|
93
|
-
else if (!failure)
|
|
94
|
-
failure = result.reason;
|
|
95
|
-
}
|
|
96
|
-
if (failure) {
|
|
97
|
-
// Dispose already loaded textures on failure
|
|
98
|
-
loaded.forEach(f => f.tex.dispose?.());
|
|
99
|
-
throw failure;
|
|
100
|
-
}
|
|
101
|
-
const size = 500;
|
|
102
|
-
const box = new THREE.BoxGeometry(size, size, size);
|
|
103
|
-
// Map faces to geometry indices (corrected for orientation)
|
|
104
|
-
const orderMap = { right: 0, left: 1, top: 3, bottom: 2, front: 4, back: 5 };
|
|
105
|
-
const mats = new Array(6).fill(null);
|
|
106
|
-
loaded.forEach(({ face, tex }) => {
|
|
107
|
-
mats[orderMap[face]] = new THREE.MeshBasicMaterial({
|
|
108
|
-
map: tex,
|
|
109
|
-
side: THREE.BackSide,
|
|
110
|
-
depthWrite: false
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
// Fill any missing materials with black
|
|
114
|
-
for (let i = 0; i < 6; i++) {
|
|
115
|
-
if (!mats[i]) {
|
|
116
|
-
mats[i] = new THREE.MeshBasicMaterial({
|
|
117
|
-
color: 0x000000,
|
|
118
|
-
side: THREE.BackSide,
|
|
119
|
-
depthWrite: false
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
// Every slot is filled by now (a face's material or black).
|
|
124
|
-
const mesh = new THREE.Mesh(box, mats);
|
|
125
|
-
mesh.name = 'SkyboxKTX2Mesh';
|
|
126
|
-
return { __ewType: 'skybox-mesh', mesh };
|
|
127
|
-
}
|
|
128
|
-
// Load JPG/PNG faces and create CubeTexture
|
|
129
|
-
function loadFacesJPG(faceKeys) {
|
|
130
|
-
const urls = FACE_ORDER.map(face => faceKeys[face]);
|
|
131
|
-
return new Promise((resolve, reject) => {
|
|
132
|
-
new THREE.CubeTextureLoader().load(urls, tex => {
|
|
133
|
-
tex.magFilter = THREE.LinearFilter;
|
|
134
|
-
tex.minFilter = THREE.LinearMipMapLinearFilter;
|
|
135
|
-
tex.anisotropy = 8;
|
|
136
|
-
resolve(tex);
|
|
137
|
-
}, undefined, err => reject(err));
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
// Resolve relative paths to full URLs
|
|
141
|
-
function resolveUrls(faceKeys) {
|
|
142
|
-
const PUBLIC_BASE = getPublicBase();
|
|
143
|
-
// Every face is set below.
|
|
144
|
-
const resolved = {};
|
|
145
|
-
FACE_ORDER.forEach(face => {
|
|
146
|
-
const url = faceKeys[face];
|
|
147
|
-
if (url && url.startsWith('/')) {
|
|
148
|
-
resolved[face] = `${PUBLIC_BASE}${url}`;
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
resolved[face] = url; // already absolute or invalid
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
return resolved;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Main loader function - always uses explicit face URLs.
|
|
158
|
-
* `renderer` binds the KTX2 transcoder to the right WebGL context. REQUIRED (3.0) when the KTX2
|
|
159
|
-
* path is taken — the old `window.__EW_RENDERER` fallback is gone; thread the owning view's
|
|
160
|
-
* renderer. (The PNG/JPG path is renderer-agnostic, so its absence only bites on KTX2.)
|
|
161
|
-
*
|
|
162
|
-
* - `faceKeys`: explicit face URLs; when absent they are built from `skycubeName`.
|
|
163
|
-
* - `skycubeName`: skybox id (e.g. `'001'`), used when `faceKeys` is absent.
|
|
164
|
-
* - `preferKTX2` (true).
|
|
165
|
-
* - `forceLow`: force (or forbid) the low-resolution faces; omitted = detect the device.
|
|
166
|
-
*
|
|
167
|
-
* Resolves to what `setBackground` takes.
|
|
168
|
-
*/
|
|
169
|
-
export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, forceLow, renderer, } = {}) {
|
|
170
|
-
// If no face URLs provided, build them from skycube name
|
|
171
|
-
if (!faceKeys && skycubeName) {
|
|
172
|
-
const low = typeof forceLow === 'boolean' ? forceLow : isLowPerformanceDevice();
|
|
173
|
-
const extension = preferKTX2 ? 'ktx2' : 'jpg';
|
|
174
|
-
const id = normalizeSkycubeName(skycubeName);
|
|
175
|
-
const suffix = low ? '_low' : '';
|
|
176
|
-
const relativePath = `/default/skybox/${id}${suffix}`;
|
|
177
|
-
faceKeys = {
|
|
178
|
-
right: `${relativePath}/right.${extension}`,
|
|
179
|
-
left: `${relativePath}/left.${extension}`,
|
|
180
|
-
top: `${relativePath}/top.${extension}`,
|
|
181
|
-
bottom: `${relativePath}/bottom.${extension}`,
|
|
182
|
-
front: `${relativePath}/front.${extension}`,
|
|
183
|
-
back: `${relativePath}/back.${extension}`
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
if (!validatefaceKeys(faceKeys)) {
|
|
187
|
-
throw new Error('Invalid face URLs provided');
|
|
188
|
-
}
|
|
189
|
-
// Resolve relative paths to full URLs
|
|
190
|
-
const resolvedUrls = resolveUrls(faceKeys);
|
|
191
|
-
const extension = detectExtension(resolvedUrls);
|
|
192
|
-
// Try KTX2 first if preferred and supported
|
|
193
|
-
if (preferKTX2 && extension === 'ktx2' && renderer) {
|
|
194
|
-
try {
|
|
195
|
-
return await loadFacesKTX2(resolvedUrls, renderer);
|
|
196
|
-
}
|
|
197
|
-
catch (e) {
|
|
198
|
-
console.warn('[CubeMapLoader] KTX2 load failed, trying PNG fallback:', e);
|
|
199
|
-
// Fall through to PNG attempt
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// Try PNG fallback (convert URLs if needed)
|
|
203
|
-
// Every face is set below.
|
|
204
|
-
const pngUrls = {};
|
|
205
|
-
FACE_ORDER.forEach(face => {
|
|
206
|
-
pngUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.png');
|
|
207
|
-
});
|
|
208
|
-
try {
|
|
209
|
-
return await loadFacesJPG(pngUrls);
|
|
210
|
-
}
|
|
211
|
-
catch (e) {
|
|
212
|
-
console.error('[CubeMapLoader] All loading attempts failed:', e);
|
|
213
|
-
throw new Error('Failed to load cube map with any format');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
// Dispose helpers to ensure clean swaps
|
|
217
|
-
function disposeSkyboxMesh(mesh) {
|
|
218
|
-
try {
|
|
219
|
-
// The skybox mesh's materials are the MeshBasicMaterials `loadFacesKTX2` made.
|
|
220
|
-
const material = mesh.material;
|
|
221
|
-
const mats = Array.isArray(material) ? material : [material];
|
|
222
|
-
mats.forEach(mat => {
|
|
223
|
-
if (!mat)
|
|
224
|
-
return;
|
|
225
|
-
if (mat.map && mat.map.dispose)
|
|
226
|
-
mat.map.dispose();
|
|
227
|
-
if (mat.dispose)
|
|
228
|
-
mat.dispose();
|
|
229
|
-
});
|
|
230
|
-
if (mesh.geometry && mesh.geometry.dispose)
|
|
231
|
-
mesh.geometry.dispose();
|
|
232
|
-
}
|
|
233
|
-
catch (e) {
|
|
234
|
-
console.warn('[CubeMapLoader] Disposal error:', e);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Show a loaded skybox as the scene's background. A previous skybox mesh or cube-texture background is
|
|
239
|
-
* disposed (a plain-texture background is not). Takes what `loadCubeMap` resolved to (or any texture);
|
|
240
|
-
* anything else, `null` included, clears it.
|
|
241
|
-
*
|
|
242
|
-
*/
|
|
243
|
-
export function setBackground(scene, resource) {
|
|
244
|
-
const sky = scene;
|
|
245
|
-
// Read as whatever it may be: a skybox wrapper, a texture, or anything else (which clears).
|
|
246
|
-
const res = resource;
|
|
247
|
-
// Clean up previous skybox mesh
|
|
248
|
-
if (sky.__EW_SKYBOX_MESH && sky.__EW_SKYBOX_MESH.parent === scene) {
|
|
249
|
-
const oldMesh = sky.__EW_SKYBOX_MESH;
|
|
250
|
-
scene.remove(oldMesh);
|
|
251
|
-
disposeSkyboxMesh(oldMesh);
|
|
252
|
-
sky.__EW_SKYBOX_MESH = null;
|
|
253
|
-
}
|
|
254
|
-
// Dispose previous CubeTexture background
|
|
255
|
-
// A Color or a Texture; `dispose` is only called once it is a CubeTexture.
|
|
256
|
-
const background = scene.background;
|
|
257
|
-
if (background && background.isCubeTexture) {
|
|
258
|
-
try {
|
|
259
|
-
background.dispose();
|
|
260
|
-
}
|
|
261
|
-
catch (e) {
|
|
262
|
-
console.warn('[CubeMapLoader] Background disposal error:', e);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// Set new background
|
|
266
|
-
if (res?.__ewType === 'skybox-mesh') {
|
|
267
|
-
scene.background = null;
|
|
268
|
-
sky.__EW_SKYBOX_MESH = res.mesh;
|
|
269
|
-
scene.add(res.mesh);
|
|
270
|
-
}
|
|
271
|
-
else if (res?.isCubeTexture || res?.isTexture) {
|
|
272
|
-
scene.background = resource;
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
scene.background = null;
|
|
276
|
-
}
|
|
277
|
-
// Dispatch update event for reactive systems
|
|
278
|
-
try {
|
|
279
|
-
window.dispatchEvent(new CustomEvent('ew:skycube-updated', {
|
|
280
|
-
detail: { type: res?.__ewType || 'texture' }
|
|
281
|
-
}));
|
|
282
|
-
}
|
|
283
|
-
catch (e) {
|
|
284
|
-
console.warn('[CubeMapLoader] Event dispatch error:', e);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
export async function initSpaceScene(scene, { renderer } = {}) {
|
|
288
|
-
const faceKeys = buildSkycubefaceKeys('001');
|
|
289
|
-
const cube = await loadCubeMap({ faceKeys, preferKTX2: true, renderer });
|
|
290
|
-
setBackground(scene, cube);
|
|
291
|
-
}
|
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
2
|
-
import { getPublicBase } from '../config/env.js';
|
|
3
|
-
import { getKTX2LoaderForRenderer } from './ktx2.js';
|
|
4
|
-
|
|
5
|
-
// Base URL is resolved at runtime via helper
|
|
6
|
-
|
|
7
|
-
/** One skybox face, in the order cube maps list them. */
|
|
8
|
-
type FaceName = 'right' | 'left' | 'top' | 'bottom' | 'front' | 'back';
|
|
9
|
-
/** A URL for each of the six faces. */
|
|
10
|
-
type FaceKeys = Record<FaceName, string>;
|
|
11
|
-
|
|
12
|
-
/** The scene field `setBackground` keeps the current skybox mesh in. It stays a field on the scene,
|
|
13
|
-
* not a private map, because the apps read it to tell whether a sky is already showing. */
|
|
14
|
-
type SkyScene = THREE.Scene & { __EW_SKYBOX_MESH?: THREE.Mesh | null };
|
|
15
|
-
|
|
16
|
-
/** A skycube id (`'7'`, `'07_skycube'`, `'skybox-007'`, …) as its three-digit folder name; `'001'` when none. */
|
|
17
|
-
export function normalizeSkycubeName(raw: string | null | undefined): string {
|
|
18
|
-
if (!raw) return '001';
|
|
19
|
-
const legacy = raw.match(/^(\d{1,2})_skycube(?:_low)?$/);
|
|
20
|
-
if (legacy) return String(parseInt(legacy[1], 10)).padStart(3, '0');
|
|
21
|
-
const num = raw.match(/(\d{1,3})/);
|
|
22
|
-
if (num) return String(parseInt(num[1], 10)).padStart(3, '0');
|
|
23
|
-
return '001';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Standard face order for cube maps
|
|
27
|
-
const FACE_ORDER: FaceName[] = ['right', 'left', 'top', 'bottom', 'front', 'back'];
|
|
28
|
-
|
|
29
|
-
// Build complete face URL sets for a given skycube ID (now returns relative paths)
|
|
30
|
-
export function buildSkycubefaceKeys(skycubeId: string | null | undefined, extension = 'ktx2') {
|
|
31
|
-
const id = normalizeSkycubeName(skycubeId);
|
|
32
|
-
const base = `/default/skybox/${id}`;
|
|
33
|
-
return {
|
|
34
|
-
right: `${base}/right.${extension}`,
|
|
35
|
-
left: `${base}/left.${extension}`,
|
|
36
|
-
top: `${base}/top.${extension}`,
|
|
37
|
-
bottom: `${base}/bottom.${extension}`,
|
|
38
|
-
front: `${base}/front.${extension}`,
|
|
39
|
-
back: `${base}/back.${extension}`
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Heuristic low-performance detection
|
|
44
|
-
function isLowPerformanceDevice() {
|
|
45
|
-
try {
|
|
46
|
-
const ua = navigator.userAgent.toLowerCase();
|
|
47
|
-
const isMobileUA = /mobi|android|iphone|ipad|ipod/.test(ua);
|
|
48
|
-
// `deviceMemory` is not in every browser, nor in the DOM types.
|
|
49
|
-
const mem = (navigator as Navigator & { deviceMemory?: number }).deviceMemory || 0; // GB
|
|
50
|
-
const cores = navigator.hardwareConcurrency || 0;
|
|
51
|
-
const dpr = window.devicePixelRatio || 1;
|
|
52
|
-
return isMobileUA || mem && mem <= 4 || cores && cores <= 4 || (dpr > 2 && cores <= 6);
|
|
53
|
-
} catch { return false; }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Validate face URLs object
|
|
57
|
-
function validatefaceKeys(faceKeys: unknown): faceKeys is FaceKeys {
|
|
58
|
-
if (!faceKeys || typeof faceKeys !== 'object') return false;
|
|
59
|
-
const keys = faceKeys as Record<string, unknown>;
|
|
60
|
-
return FACE_ORDER.every(face => keys[face] && typeof keys[face] === 'string');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Detect extension type from face URLs
|
|
64
|
-
function detectExtension(faceKeys: FaceKeys): 'ktx2' | 'jpg' {
|
|
65
|
-
const url = faceKeys.right || '';
|
|
66
|
-
if (url.toLowerCase().includes('.ktx2')) return 'ktx2';
|
|
67
|
-
if (url.toLowerCase().match(/\.(jpg|jpeg|png)$/)) return 'jpg';
|
|
68
|
-
return 'ktx2'; // default assumption
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// How many faces may be in flight at once. Not six: the KTX2 worker pool three.js gives us holds
|
|
72
|
-
// four transcoder workers, each carrying its own copy of the basis wasm, and that same pool also
|
|
73
|
-
// serves body textures and GLBs. Asking for all six together spawned every worker and queued the
|
|
74
|
-
// other loaders behind the whole sky; measured on cold fetches it was no faster than three.
|
|
75
|
-
const FACES_IN_FLIGHT = 3;
|
|
76
|
-
|
|
77
|
-
// Load KTX2 faces and create skybox mesh
|
|
78
|
-
async function loadFacesKTX2(faceKeys: FaceKeys, renderer: THREE.WebGLRenderer): Promise<SkyboxResource> {
|
|
79
|
-
const loader = getKTX2LoaderForRenderer(renderer);
|
|
80
|
-
const anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
|
|
81
|
-
|
|
82
|
-
const loadFace = async (face: FaceName) => {
|
|
83
|
-
const tex = await loader.loadAsync(faceKeys[face]);
|
|
84
|
-
tex.flipY = false;
|
|
85
|
-
tex.wrapS = THREE.ClampToEdgeWrapping;
|
|
86
|
-
tex.wrapT = THREE.ClampToEdgeWrapping;
|
|
87
|
-
tex.anisotropy = anisotropy;
|
|
88
|
-
return { face, tex };
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Faces overlap instead of chaining. Awaiting each one in turn made every face wait on the
|
|
92
|
-
// previous face's download *and* its transcode, so the sky arrived six round trips deep.
|
|
93
|
-
const settled = [];
|
|
94
|
-
for (let i = 0; i < FACE_ORDER.length; i += FACES_IN_FLIGHT) {
|
|
95
|
-
const wave = FACE_ORDER.slice(i, i + FACES_IN_FLIGHT).map(face => loadFace(face));
|
|
96
|
-
settled.push(...await Promise.allSettled(wave));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// allSettled, not all: a bare Promise.all rejects on the first failure and leaks the faces that
|
|
100
|
-
// land after it. Settle every face first, then dispose everything that arrived before rethrowing.
|
|
101
|
-
const loaded: Awaited<ReturnType<typeof loadFace>>[] = [];
|
|
102
|
-
let failure: unknown = null;
|
|
103
|
-
for (const result of settled) {
|
|
104
|
-
if (result.status === 'fulfilled') loaded.push(result.value);
|
|
105
|
-
else if (!failure) failure = result.reason;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (failure) {
|
|
109
|
-
// Dispose already loaded textures on failure
|
|
110
|
-
loaded.forEach(f => f.tex.dispose?.());
|
|
111
|
-
throw failure;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const size = 500;
|
|
115
|
-
const box = new THREE.BoxGeometry(size, size, size);
|
|
116
|
-
// Map faces to geometry indices (corrected for orientation)
|
|
117
|
-
const orderMap: Record<FaceName, number> = { right:0, left:1, top:3, bottom:2, front:4, back:5 };
|
|
118
|
-
const mats: (THREE.MeshBasicMaterial | null)[] = new Array(6).fill(null);
|
|
119
|
-
|
|
120
|
-
loaded.forEach(({ face, tex }) => {
|
|
121
|
-
mats[orderMap[face]] = new THREE.MeshBasicMaterial({
|
|
122
|
-
map: tex,
|
|
123
|
-
side: THREE.BackSide,
|
|
124
|
-
depthWrite: false
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Fill any missing materials with black
|
|
129
|
-
for (let i = 0; i < 6; i++) {
|
|
130
|
-
if (!mats[i]) {
|
|
131
|
-
mats[i] = new THREE.MeshBasicMaterial({
|
|
132
|
-
color: 0x000000,
|
|
133
|
-
side: THREE.BackSide,
|
|
134
|
-
depthWrite: false
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Every slot is filled by now (a face's material or black).
|
|
140
|
-
const mesh = new THREE.Mesh(box, mats as THREE.MeshBasicMaterial[]);
|
|
141
|
-
mesh.name = 'SkyboxKTX2Mesh';
|
|
142
|
-
return { __ewType: 'skybox-mesh', mesh };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Load JPG/PNG faces and create CubeTexture
|
|
146
|
-
function loadFacesJPG(faceKeys: FaceKeys): Promise<THREE.CubeTexture> {
|
|
147
|
-
const urls = FACE_ORDER.map(face => faceKeys[face]);
|
|
148
|
-
return new Promise((resolve, reject) => {
|
|
149
|
-
new THREE.CubeTextureLoader().load(
|
|
150
|
-
urls,
|
|
151
|
-
tex => {
|
|
152
|
-
tex.magFilter = THREE.LinearFilter;
|
|
153
|
-
tex.minFilter = THREE.LinearMipMapLinearFilter;
|
|
154
|
-
tex.anisotropy = 8;
|
|
155
|
-
resolve(tex);
|
|
156
|
-
},
|
|
157
|
-
undefined,
|
|
158
|
-
err => reject(err)
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Resolve relative paths to full URLs
|
|
164
|
-
function resolveUrls(faceKeys: FaceKeys): FaceKeys {
|
|
165
|
-
const PUBLIC_BASE = getPublicBase();
|
|
166
|
-
// Every face is set below.
|
|
167
|
-
const resolved = {} as FaceKeys;
|
|
168
|
-
FACE_ORDER.forEach(face => {
|
|
169
|
-
const url = faceKeys[face];
|
|
170
|
-
if (url && url.startsWith('/')) {
|
|
171
|
-
resolved[face] = `${PUBLIC_BASE}${url}`;
|
|
172
|
-
} else {
|
|
173
|
-
resolved[face] = url; // already absolute or invalid
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
return resolved;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Main loader function - always uses explicit face URLs.
|
|
181
|
-
* `renderer` binds the KTX2 transcoder to the right WebGL context. REQUIRED (3.0) when the KTX2
|
|
182
|
-
* path is taken — the old `window.__EW_RENDERER` fallback is gone; thread the owning view's
|
|
183
|
-
* renderer. (The PNG/JPG path is renderer-agnostic, so its absence only bites on KTX2.)
|
|
184
|
-
*
|
|
185
|
-
* - `faceKeys`: explicit face URLs; when absent they are built from `skycubeName`.
|
|
186
|
-
* - `skycubeName`: skybox id (e.g. `'001'`), used when `faceKeys` is absent.
|
|
187
|
-
* - `preferKTX2` (true).
|
|
188
|
-
* - `forceLow`: force (or forbid) the low-resolution faces; omitted = detect the device.
|
|
189
|
-
*
|
|
190
|
-
* Resolves to what `setBackground` takes.
|
|
191
|
-
*/
|
|
192
|
-
export async function loadCubeMap({
|
|
193
|
-
faceKeys,
|
|
194
|
-
skycubeName,
|
|
195
|
-
preferKTX2 = true,
|
|
196
|
-
forceLow,
|
|
197
|
-
renderer,
|
|
198
|
-
}: {
|
|
199
|
-
faceKeys?: { right: string; left: string; top: string; bottom: string; front: string; back: string } | undefined;
|
|
200
|
-
skycubeName?: string | undefined;
|
|
201
|
-
preferKTX2?: boolean | undefined;
|
|
202
|
-
forceLow?: boolean | undefined;
|
|
203
|
-
renderer?: THREE.WebGLRenderer | null | undefined;
|
|
204
|
-
} = {}): Promise<SkyboxResource> {
|
|
205
|
-
|
|
206
|
-
// If no face URLs provided, build them from skycube name
|
|
207
|
-
if (!faceKeys && skycubeName) {
|
|
208
|
-
const low = typeof forceLow === 'boolean' ? forceLow : isLowPerformanceDevice();
|
|
209
|
-
const extension = preferKTX2 ? 'ktx2' : 'jpg';
|
|
210
|
-
const id = normalizeSkycubeName(skycubeName);
|
|
211
|
-
const suffix = low ? '_low' : '';
|
|
212
|
-
const relativePath = `/default/skybox/${id}${suffix}`;
|
|
213
|
-
|
|
214
|
-
faceKeys = {
|
|
215
|
-
right: `${relativePath}/right.${extension}`,
|
|
216
|
-
left: `${relativePath}/left.${extension}`,
|
|
217
|
-
top: `${relativePath}/top.${extension}`,
|
|
218
|
-
bottom: `${relativePath}/bottom.${extension}`,
|
|
219
|
-
front: `${relativePath}/front.${extension}`,
|
|
220
|
-
back: `${relativePath}/back.${extension}`
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (!validatefaceKeys(faceKeys)) {
|
|
225
|
-
throw new Error('Invalid face URLs provided');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Resolve relative paths to full URLs
|
|
229
|
-
const resolvedUrls = resolveUrls(faceKeys);
|
|
230
|
-
const extension = detectExtension(resolvedUrls);
|
|
231
|
-
|
|
232
|
-
// Try KTX2 first if preferred and supported
|
|
233
|
-
if (preferKTX2 && extension === 'ktx2' && renderer) {
|
|
234
|
-
try {
|
|
235
|
-
return await loadFacesKTX2(resolvedUrls, renderer);
|
|
236
|
-
} catch (e) {
|
|
237
|
-
console.warn('[CubeMapLoader] KTX2 load failed, trying PNG fallback:', e);
|
|
238
|
-
// Fall through to PNG attempt
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Try PNG fallback (convert URLs if needed)
|
|
243
|
-
// Every face is set below.
|
|
244
|
-
const pngUrls = {} as FaceKeys;
|
|
245
|
-
FACE_ORDER.forEach(face => {
|
|
246
|
-
pngUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.png');
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
return await loadFacesJPG(pngUrls);
|
|
251
|
-
} catch (e) {
|
|
252
|
-
console.error('[CubeMapLoader] All loading attempts failed:', e);
|
|
253
|
-
throw new Error('Failed to load cube map with any format');
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Dispose helpers to ensure clean swaps
|
|
258
|
-
function disposeSkyboxMesh(mesh: THREE.Mesh) {
|
|
259
|
-
try {
|
|
260
|
-
// The skybox mesh's materials are the MeshBasicMaterials `loadFacesKTX2` made.
|
|
261
|
-
const material = mesh.material as THREE.MeshBasicMaterial | THREE.MeshBasicMaterial[];
|
|
262
|
-
const mats = Array.isArray(material) ? material : [material];
|
|
263
|
-
mats.forEach(mat => {
|
|
264
|
-
if (!mat) return;
|
|
265
|
-
if (mat.map && mat.map.dispose) mat.map.dispose();
|
|
266
|
-
if (mat.dispose) mat.dispose();
|
|
267
|
-
});
|
|
268
|
-
if (mesh.geometry && mesh.geometry.dispose) mesh.geometry.dispose();
|
|
269
|
-
} catch(e) {
|
|
270
|
-
console.warn('[CubeMapLoader] Disposal error:', e);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* What `loadCubeMap` resolves to: a skybox mesh wrapper (the KTX2 path) or a cube texture (the
|
|
276
|
-
* PNG/JPG path). Read `mesh` to tell them apart.
|
|
277
|
-
*/
|
|
278
|
-
export type SkyboxResource =
|
|
279
|
-
| { __ewType: 'skybox-mesh'; mesh: THREE.Mesh }
|
|
280
|
-
| (THREE.CubeTexture & { mesh?: undefined });
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Show a loaded skybox as the scene's background. A previous skybox mesh or cube-texture background is
|
|
284
|
-
* disposed (a plain-texture background is not). Takes what `loadCubeMap` resolved to (or any texture);
|
|
285
|
-
* anything else, `null` included, clears it.
|
|
286
|
-
*
|
|
287
|
-
*/
|
|
288
|
-
export function setBackground(scene: THREE.Scene, resource: unknown): void {
|
|
289
|
-
const sky = scene as SkyScene;
|
|
290
|
-
// Read as whatever it may be: a skybox wrapper, a texture, or anything else (which clears).
|
|
291
|
-
const res = resource as
|
|
292
|
-
| { __ewType?: unknown; mesh: THREE.Mesh; isCubeTexture?: boolean; isTexture?: boolean }
|
|
293
|
-
| null
|
|
294
|
-
| undefined;
|
|
295
|
-
// Clean up previous skybox mesh
|
|
296
|
-
if (sky.__EW_SKYBOX_MESH && sky.__EW_SKYBOX_MESH.parent === scene) {
|
|
297
|
-
const oldMesh = sky.__EW_SKYBOX_MESH;
|
|
298
|
-
scene.remove(oldMesh);
|
|
299
|
-
disposeSkyboxMesh(oldMesh);
|
|
300
|
-
sky.__EW_SKYBOX_MESH = null;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Dispose previous CubeTexture background
|
|
304
|
-
// A Color or a Texture; `dispose` is only called once it is a CubeTexture.
|
|
305
|
-
const background = scene.background as { isCubeTexture?: boolean; dispose(): void } | null;
|
|
306
|
-
if (background && background.isCubeTexture) {
|
|
307
|
-
try {
|
|
308
|
-
background.dispose();
|
|
309
|
-
} catch(e) {
|
|
310
|
-
console.warn('[CubeMapLoader] Background disposal error:', e);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Set new background
|
|
315
|
-
if (res?.__ewType === 'skybox-mesh') {
|
|
316
|
-
scene.background = null;
|
|
317
|
-
sky.__EW_SKYBOX_MESH = res.mesh;
|
|
318
|
-
scene.add(res.mesh);
|
|
319
|
-
} else if (res?.isCubeTexture || res?.isTexture) {
|
|
320
|
-
scene.background = resource as THREE.Texture;
|
|
321
|
-
} else {
|
|
322
|
-
scene.background = null;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Dispatch update event for reactive systems
|
|
326
|
-
try {
|
|
327
|
-
window.dispatchEvent(new CustomEvent('ew:skycube-updated', {
|
|
328
|
-
detail: { type: res?.__ewType || 'texture' }
|
|
329
|
-
}));
|
|
330
|
-
} catch(e) {
|
|
331
|
-
console.warn('[CubeMapLoader] Event dispatch error:', e);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export async function initSpaceScene(
|
|
336
|
-
scene: THREE.Scene,
|
|
337
|
-
{ renderer }: { renderer?: THREE.WebGLRenderer | null } = {},
|
|
338
|
-
): Promise<void> {
|
|
339
|
-
const faceKeys = buildSkycubefaceKeys('001');
|
|
340
|
-
const cube = await loadCubeMap({ faceKeys, preferKTX2: true, renderer });
|
|
341
|
-
setBackground(scene, cube);
|
|
342
|
-
}
|