entangled-worlds-orbiters-shared 3.1.0 → 4.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 +4 -3
- package/dist/config/env.d.ts +8 -0
- package/dist/config/env.js +36 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +4 -0
- package/dist/config/textureConfig.d.ts +140 -0
- package/dist/config/textureConfig.js +318 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/lightFresnel.d.ts +6 -0
- package/dist/core/lightFresnel.js +63 -0
- package/dist/core/lights.d.ts +48 -0
- package/dist/core/lights.js +79 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/loaders/bodyTextureLoader.d.ts +10 -0
- package/dist/loaders/bodyTextureLoader.js +28 -0
- package/dist/loaders/cubeMapLoader.d.ts +58 -0
- package/dist/loaders/cubeMapLoader.js +291 -0
- package/dist/loaders/glbLoader.d.ts +24 -0
- package/dist/loaders/glbLoader.js +72 -0
- package/dist/loaders/index.d.ts +5 -0
- package/{src → dist}/loaders/index.js +1 -1
- package/dist/loaders/ktx2.d.ts +14 -0
- package/dist/loaders/ktx2.js +48 -0
- package/dist/moons/index.d.ts +3 -0
- package/dist/moons/index.js +3 -0
- package/dist/moons/moonsGeneration.d.ts +64 -0
- package/dist/moons/moonsGeneration.js +238 -0
- package/dist/moons/moonsManager.d.ts +43 -0
- package/dist/moons/moonsManager.js +93 -0
- package/dist/render/frameBudget.d.ts +30 -0
- package/dist/render/frameBudget.js +42 -0
- package/dist/render/index.d.ts +4 -0
- package/dist/render/viewportCompositor.d.ts +124 -0
- package/dist/render/viewportCompositor.js +643 -0
- package/dist/star/framing.d.ts +17 -6
- package/dist/star/framing.js +16 -52
- package/dist/star/index.d.ts +1 -1
- package/dist/star/index.js +1 -1
- package/dist/star/layout.js +1 -2
- package/dist/star/params.js +3 -2
- package/dist/studio/headerBroadcast.d.ts +10 -1
- package/dist/studio/hostTransport.d.ts +2 -2
- package/dist/studio/index.d.ts +1 -1
- package/dist/utils/dispose.d.ts +5 -0
- package/dist/utils/dispose.js +50 -0
- package/dist/utils/index.d.ts +3 -0
- package/{src → dist}/utils/index.js +1 -1
- package/dist/utils/materials.d.ts +10 -0
- package/dist/utils/materials.js +36 -0
- package/dist/world/cloudShell.d.ts +16 -0
- package/dist/world/cloudShell.js +73 -0
- package/dist/world/index.d.ts +6 -0
- package/{src → dist}/world/index.js +1 -1
- package/dist/world/worldGLB.d.ts +17 -0
- package/dist/world/worldGLB.js +22 -0
- package/dist/world/worldManager.d.ts +57 -0
- package/dist/world/worldManager.js +125 -0
- package/dist/world/worldNormal.d.ts +30 -0
- package/dist/world/worldNormal.js +58 -0
- package/dist/world/worldTexture.d.ts +56 -0
- package/dist/world/worldTexture.js +299 -0
- package/package.json +45 -30
- package/src/config/env.ts +48 -0
- package/src/config/{index.js → index.ts} +7 -0
- package/src/config/{textureConfig.js → textureConfig.ts} +126 -60
- package/src/core/{lightFresnel.js → lightFresnel.ts} +1 -1
- package/src/core/{lights.js → lights.ts} +22 -10
- package/src/loaders/{bodyTextureLoader.js → bodyTextureLoader.ts} +12 -2
- package/src/loaders/{cubeMapLoader.js → cubeMapLoader.ts} +95 -34
- package/src/loaders/{glbLoader.js → glbLoader.ts} +41 -12
- package/src/loaders/index.ts +6 -0
- package/src/loaders/{ktx2.js → ktx2.ts} +5 -7
- package/src/moons/{index.js → index.ts} +1 -0
- package/src/moons/{moonsGeneration.js → moonsGeneration.ts} +49 -6
- package/src/moons/{moonsManager.js → moonsManager.ts} +37 -11
- package/src/render/{frameBudget.js → frameBudget.ts} +15 -8
- package/src/render/index.ts +14 -0
- package/src/render/{viewportCompositor.js → viewportCompositor.ts} +121 -59
- package/src/star/framing.ts +17 -55
- package/src/star/index.ts +1 -1
- package/src/star/layout.ts +1 -2
- package/src/star/params.ts +3 -2
- package/src/studio/headerBroadcast.ts +14 -1
- package/src/studio/hostTransport.ts +2 -1
- package/src/studio/index.ts +1 -1
- package/src/utils/dispose.ts +47 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/materials.ts +44 -0
- package/src/world/{cloudShell.js → cloudShell.ts} +21 -1
- package/src/world/index.ts +7 -0
- package/src/world/worldGLB.ts +41 -0
- package/src/world/{worldManager.js → worldManager.ts} +76 -10
- package/src/world/{worldNormal.js → worldNormal.ts} +26 -2
- package/src/world/{worldTexture.js → worldTexture.ts} +76 -16
- package/src/config/env.js +0 -30
- package/src/utils/dispose.js +0 -39
- package/src/utils/materials.js +0 -31
- package/src/world/worldGLB.js +0 -8
- /package/{src → dist}/render/index.js +0 -0
- /package/src/core/{index.js → index.ts} +0 -0
- /package/src/{index.js → index.ts} +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Ensure a scene has the default hemisphere + ambient lights. Idempotent PER SCENE.
|
|
4
|
+
*
|
|
5
|
+
* A multi-orbiter realm renders N independent scenes through one renderer. A module-level
|
|
6
|
+
* singleton light is wrong twice over — it's only added to the FIRST scene, and a THREE light can
|
|
7
|
+
* live in just one scene (adding it to a second removes it from the first), so scenes 2..N rendered
|
|
8
|
+
* DARK ("only one entangled world"). So create + add the lights PER scene, keyed by name so a
|
|
9
|
+
* re-call on the same scene is a no-op. Single-orbiter (one scene) is unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export function ensureDefaultLights(scene) {
|
|
12
|
+
// Found by name: the objects under these names are the lights this function added.
|
|
13
|
+
let hemi = scene.getObjectByName('ew_HemisphereLight');
|
|
14
|
+
if (!hemi) {
|
|
15
|
+
hemi = new THREE.HemisphereLight(0xffffff, 0xffffff, 1.0);
|
|
16
|
+
hemi.position.set(1, 1, 1);
|
|
17
|
+
hemi.name = 'ew_HemisphereLight';
|
|
18
|
+
scene.add(hemi);
|
|
19
|
+
}
|
|
20
|
+
let ambient = scene.getObjectByName('ew_AmbientLight');
|
|
21
|
+
if (!ambient) {
|
|
22
|
+
ambient = new THREE.AmbientLight(0xffffff, 1.0);
|
|
23
|
+
ambient.name = 'ew_AmbientLight';
|
|
24
|
+
scene.add(ambient);
|
|
25
|
+
}
|
|
26
|
+
return { hemi, ambient };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Apply a lighting patch to ONE scene's default lights.
|
|
30
|
+
*
|
|
31
|
+
* The scene is required. This used to address module-global light refs pointing at whichever scene
|
|
32
|
+
* was ensured last, so on a page rendering several scenes every lighting update landed on one of
|
|
33
|
+
* them — a shared-renderer page (N feed worlds, N orbiters) lit the wrong world.
|
|
34
|
+
*
|
|
35
|
+
* Every field is optional; an omitted one leaves that part of the lighting as it is.
|
|
36
|
+
*/
|
|
37
|
+
export function updateDefaultLights(scene, { ambientIntensity, diffuseIntensity, lightColor, lightDirection, hemisphere } = {}) {
|
|
38
|
+
if (!scene?.isObject3D) {
|
|
39
|
+
console.error('[lights] updateDefaultLights(scene, patch) requires the scene to update; ignoring patch.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const { hemi, ambient } = ensureDefaultLights(scene);
|
|
43
|
+
// `hemisphere: false` switches the white hemisphere off (e.g. when a star lights the scene);
|
|
44
|
+
// omitted = unchanged, so old callers keep today's look.
|
|
45
|
+
if (typeof hemisphere === 'boolean')
|
|
46
|
+
hemi.visible = hemisphere;
|
|
47
|
+
if (typeof ambientIntensity === 'number')
|
|
48
|
+
ambient.intensity = Math.max(0, ambientIntensity);
|
|
49
|
+
if (typeof diffuseIntensity === 'number')
|
|
50
|
+
hemi.intensity = Math.max(0, diffuseIntensity);
|
|
51
|
+
if (lightColor != null) {
|
|
52
|
+
if (lightColor instanceof THREE.Color) {
|
|
53
|
+
hemi.color.copy(lightColor);
|
|
54
|
+
}
|
|
55
|
+
else if (typeof lightColor === 'number') {
|
|
56
|
+
hemi.color.setHex(lightColor);
|
|
57
|
+
}
|
|
58
|
+
else if (typeof lightColor === 'string') {
|
|
59
|
+
hemi.color.set(lightColor);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (lightDirection && typeof lightDirection.x === 'number') {
|
|
63
|
+
hemi.position.set(lightDirection.x, lightDirection.y ?? 0, lightDirection.z ?? 0).normalize();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Switch a scene's default hemisphere light on or off (the ambient light is untouched). Hosts turn it
|
|
68
|
+
* off when a directional light — the star — lights the scene, so night sides can read.
|
|
69
|
+
*/
|
|
70
|
+
export function setHemisphereEnabled(scene, on) {
|
|
71
|
+
if (!scene?.isObject3D)
|
|
72
|
+
return;
|
|
73
|
+
const { hemi } = ensureDefaultLights(scene);
|
|
74
|
+
hemi.visible = Boolean(on);
|
|
75
|
+
}
|
|
76
|
+
// Back-compat helper
|
|
77
|
+
export function addDefaultLights(scene) {
|
|
78
|
+
return ensureDefaultLights(scene);
|
|
79
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Main exports for entangled-worlds-orbiters-shared library
|
|
2
|
+
export * from './world/index.js';
|
|
3
|
+
export * from './moons/index.js';
|
|
4
|
+
export * from './loaders/index.js';
|
|
5
|
+
export * from './core/index.js';
|
|
6
|
+
export * from './config/index.js';
|
|
7
|
+
export * from './utils/index.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* `url`: `.ktx2` goes through the renderer's KTX2 transcoder; anything else through
|
|
4
|
+
* THREE.TextureLoader. `signal`: an abort during the load rejects the promise. `renderer`: REQUIRED
|
|
5
|
+
* for a `.ktx2` url.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadTexture(url: string, { signal, renderer }?: {
|
|
8
|
+
signal?: AbortSignal | undefined;
|
|
9
|
+
renderer?: THREE.WebGLRenderer | null | undefined;
|
|
10
|
+
}): Promise<THREE.Texture>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { getKTX2LoaderForRenderer } from './ktx2.js';
|
|
3
|
+
const textureLoader = new THREE.TextureLoader();
|
|
4
|
+
/**
|
|
5
|
+
* `url`: `.ktx2` goes through the renderer's KTX2 transcoder; anything else through
|
|
6
|
+
* THREE.TextureLoader. `signal`: an abort during the load rejects the promise. `renderer`: REQUIRED
|
|
7
|
+
* for a `.ktx2` url.
|
|
8
|
+
*/
|
|
9
|
+
export function loadTexture(url, { signal, renderer } = {}) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
if (url && url.endsWith('.ktx2')) {
|
|
12
|
+
// A missing renderer throws inside (as before): the type allows `null` only because the
|
|
13
|
+
// non-KTX2 path never needs one.
|
|
14
|
+
const ktx2 = getKTX2LoaderForRenderer(renderer);
|
|
15
|
+
const onAbort = () => reject(new Error('aborted'));
|
|
16
|
+
if (signal)
|
|
17
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
18
|
+
ktx2.load(url, (tex) => { if (signal)
|
|
19
|
+
signal.removeEventListener('abort', onAbort); resolve(tex); }, undefined, (err) => { if (signal)
|
|
20
|
+
signal.removeEventListener('abort', onAbort); reject(err); });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const tex = textureLoader.load(url, () => resolve(tex), undefined, (err) => reject(err));
|
|
24
|
+
if (signal) {
|
|
25
|
+
signal.addEventListener('abort', () => reject(new Error('aborted')));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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>;
|
|
@@ -0,0 +1,291 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { type MaterialFlags } from '../utils/materials.js';
|
|
3
|
+
/**
|
|
4
|
+
* `renderer` is REQUIRED at runtime (throws without one).
|
|
5
|
+
* - `retries` (3): load attempts before giving up.
|
|
6
|
+
* - `signal`: rejects the load on abort.
|
|
7
|
+
* - `applyFlags`: material flags (`forceOpaque`, `wireframe`) applied to every mesh.
|
|
8
|
+
* - `withCredentials` (false).
|
|
9
|
+
* - `emissiveIntensity` (1): self-glow of textured materials (their map doubles as the emissive
|
|
10
|
+
* map). 1 keeps the old flat-lit look; with a directional star light pass ≈0.1–0.15 so the night
|
|
11
|
+
* side reads.
|
|
12
|
+
* - `renderer`: REQUIRED (3.0): the renderer this GLB's textures bind to. A page with several
|
|
13
|
+
* renderers has no single correct global, so the old `window.__EW_RENDERER` fallback is gone —
|
|
14
|
+
* thread the owning view's renderer.
|
|
15
|
+
*/
|
|
16
|
+
export declare function loadGLB(url: string, { retries, signal, onProgress, applyFlags, withCredentials, renderer, emissiveIntensity, }?: {
|
|
17
|
+
retries?: number | undefined;
|
|
18
|
+
signal?: AbortSignal | undefined;
|
|
19
|
+
onProgress?: ((event: ProgressEvent) => void) | undefined;
|
|
20
|
+
applyFlags?: MaterialFlags | undefined;
|
|
21
|
+
withCredentials?: boolean | undefined;
|
|
22
|
+
emissiveIntensity?: number | undefined;
|
|
23
|
+
renderer: THREE.WebGLRenderer;
|
|
24
|
+
}): Promise<THREE.Group | undefined>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
3
|
+
import { ensureStandardMaterial, applyFlagsToMaterial } from '../utils/materials.js';
|
|
4
|
+
import { getKTX2LoaderForRenderer } from './ktx2.js';
|
|
5
|
+
/**
|
|
6
|
+
* `renderer` is REQUIRED at runtime (throws without one).
|
|
7
|
+
* - `retries` (3): load attempts before giving up.
|
|
8
|
+
* - `signal`: rejects the load on abort.
|
|
9
|
+
* - `applyFlags`: material flags (`forceOpaque`, `wireframe`) applied to every mesh.
|
|
10
|
+
* - `withCredentials` (false).
|
|
11
|
+
* - `emissiveIntensity` (1): self-glow of textured materials (their map doubles as the emissive
|
|
12
|
+
* map). 1 keeps the old flat-lit look; with a directional star light pass ≈0.1–0.15 so the night
|
|
13
|
+
* side reads.
|
|
14
|
+
* - `renderer`: REQUIRED (3.0): the renderer this GLB's textures bind to. A page with several
|
|
15
|
+
* renderers has no single correct global, so the old `window.__EW_RENDERER` fallback is gone —
|
|
16
|
+
* thread the owning view's renderer.
|
|
17
|
+
*/
|
|
18
|
+
export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags, withCredentials = false, renderer, emissiveIntensity = 1.0, } = {}) {
|
|
19
|
+
if (!renderer)
|
|
20
|
+
throw new Error('[glb] loadGLB requires the owning renderer (KTX2 textures bind per WebGL context).');
|
|
21
|
+
let attempt = 0;
|
|
22
|
+
while (attempt < retries) {
|
|
23
|
+
try {
|
|
24
|
+
const loader = new GLTFLoader()
|
|
25
|
+
.setCrossOrigin(withCredentials ? 'use-credentials' : 'anonymous')
|
|
26
|
+
.setWithCredentials(Boolean(withCredentials))
|
|
27
|
+
.setKTX2Loader(getKTX2LoaderForRenderer(renderer));
|
|
28
|
+
const root = await new Promise((resolve, reject) => {
|
|
29
|
+
loader.load(url, (gltf) => resolve(gltf.scene), onProgress, reject);
|
|
30
|
+
if (signal) {
|
|
31
|
+
signal.addEventListener('abort', () => reject(new Error('aborted')));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
// Post-process materials
|
|
35
|
+
root.traverse((object) => {
|
|
36
|
+
// The meshes a GLB loads carry one material each, and `ensureStandardMaterial` just made it a
|
|
37
|
+
// MeshStandardMaterial, which is what the lines below write to.
|
|
38
|
+
const child = object;
|
|
39
|
+
if (child.isMesh && child.material) {
|
|
40
|
+
ensureStandardMaterial(child);
|
|
41
|
+
if (applyFlags)
|
|
42
|
+
applyFlagsToMaterial(child.material, applyFlags);
|
|
43
|
+
// Force texture updates for KTX2
|
|
44
|
+
if (child.material.map) {
|
|
45
|
+
child.material.map.anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
|
|
46
|
+
child.material.map.needsUpdate = true;
|
|
47
|
+
// Ensure proper color space for compressed textures
|
|
48
|
+
child.material.map.colorSpace = THREE.SRGBColorSpace;
|
|
49
|
+
// Ensure material color doesn't block the texture
|
|
50
|
+
child.material.color.setHex(0xffffff);
|
|
51
|
+
// Add strong emissive to ensure visibility
|
|
52
|
+
child.material.emissive.setHex(0x888888);
|
|
53
|
+
child.material.emissiveMap = child.material.map;
|
|
54
|
+
child.material.emissiveIntensity = Number.isFinite(emissiveIntensity) ? Math.max(0, emissiveIntensity) : 1.0;
|
|
55
|
+
}
|
|
56
|
+
child.material.needsUpdate = true;
|
|
57
|
+
child.castShadow = false;
|
|
58
|
+
child.receiveShadow = false;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
root.position.set(0, 0, 0);
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
console.warn(`GLB load attempt ${attempt + 1} failed:`, e.message);
|
|
66
|
+
if (signal?.aborted || attempt >= retries - 1)
|
|
67
|
+
throw e;
|
|
68
|
+
attempt++;
|
|
69
|
+
await new Promise(resolve => setTimeout(resolve, 500)); // Brief delay between retries
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
|
|
2
|
+
export { loadGLB } from './glbLoader.js';
|
|
3
|
+
export { loadTexture } from './bodyTextureLoader.js';
|
|
4
|
+
export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
|
|
5
|
+
export type { SkyboxResource } from './cubeMapLoader.js';
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
|
|
3
3
|
export { loadGLB } from './glbLoader.js';
|
|
4
4
|
export { loadTexture } from './bodyTextureLoader.js';
|
|
5
|
-
export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
|
|
5
|
+
export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
3
|
+
/**
|
|
4
|
+
* The KTX2Loader bound to `renderer` — REQUIRED (3.0). The old `window.__EW_RENDERER` fallback let
|
|
5
|
+
* call sites compile while silently binding the transcoder to whichever renderer a page published
|
|
6
|
+
* last; on any multi-view page that meant untextured worlds. Every caller now threads its own
|
|
7
|
+
* renderer, so a missing one is a caller bug and fails loudly here rather than as a dark planet.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getKTX2LoaderForRenderer(renderer: THREE.WebGLRenderer): KTX2Loader;
|
|
10
|
+
/**
|
|
11
|
+
* Dispose and forget the KTX2Loader for a renderer — call on that renderer's / scene's teardown so the
|
|
12
|
+
* transcoder workers are terminated (avoids leaking worker threads + GPU memory across view churn).
|
|
13
|
+
*/
|
|
14
|
+
export declare function disposeKTX2LoaderForRenderer(renderer: THREE.WebGLRenderer): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
2
|
+
/**
|
|
3
|
+
* One KTX2Loader PER RENDERER.
|
|
4
|
+
*
|
|
5
|
+
* `KTX2Loader.detectSupport(renderer)` binds the loader's Basis transcoder worker pool to ONE WebGL
|
|
6
|
+
* context. A single shared loader therefore only decodes textures for the FIRST renderer it saw —
|
|
7
|
+
* every other view's planets/skyboxes silently fail and render untextured (dark). That breaks any
|
|
8
|
+
* multi-view page: N orbiters in one realm, or N Entangled World planets shown together.
|
|
9
|
+
*
|
|
10
|
+
* Keying the loader by renderer gives each view its own correctly-bound loader. Single-renderer apps
|
|
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/cubeMap loaders.)
|
|
13
|
+
*/
|
|
14
|
+
const loadersByRenderer = new WeakMap();
|
|
15
|
+
function makeLoader(renderer) {
|
|
16
|
+
return new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The KTX2Loader bound to `renderer` — REQUIRED (3.0). The old `window.__EW_RENDERER` fallback let
|
|
20
|
+
* call sites compile while silently binding the transcoder to whichever renderer a page published
|
|
21
|
+
* last; on any multi-view page that meant untextured worlds. Every caller now threads its own
|
|
22
|
+
* renderer, so a missing one is a caller bug and fails loudly here rather than as a dark planet.
|
|
23
|
+
*/
|
|
24
|
+
export function getKTX2LoaderForRenderer(renderer) {
|
|
25
|
+
if (!renderer) {
|
|
26
|
+
throw new Error('[ktx2] getKTX2LoaderForRenderer requires the owning renderer — thread it from the view that renders this texture.');
|
|
27
|
+
}
|
|
28
|
+
let loader = loadersByRenderer.get(renderer);
|
|
29
|
+
if (!loader) {
|
|
30
|
+
loader = makeLoader(renderer);
|
|
31
|
+
loadersByRenderer.set(renderer, loader);
|
|
32
|
+
}
|
|
33
|
+
return loader;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Dispose and forget the KTX2Loader for a renderer — call on that renderer's / scene's teardown so the
|
|
37
|
+
* transcoder workers are terminated (avoids leaking worker threads + GPU memory across view churn).
|
|
38
|
+
*/
|
|
39
|
+
export function disposeKTX2LoaderForRenderer(renderer) {
|
|
40
|
+
const loader = renderer && loadersByRenderer.get(renderer);
|
|
41
|
+
if (loader) {
|
|
42
|
+
try {
|
|
43
|
+
loader.dispose?.();
|
|
44
|
+
}
|
|
45
|
+
catch { /* already gone */ }
|
|
46
|
+
loadersByRenderer.delete(renderer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { MoonsManager } from './moonsManager.js';
|
|
2
|
+
export type { MoonAmountOptions } from './moonsManager.js';
|
|
3
|
+
export { generateMoons, DEFAULT_MOON_TEX_KTX2, DEFAULT_MOON_TEX_PNG, MOON_BASE_RADIUS, MOON_SCALE_MIN, MOON_SCALE_SPAN, MOON_MAX_RADIUS, MOON_ORBIT_SCATTER, MOON_HEIGHT_SCATTER, } from './moonsGeneration.js';
|