entangled-worlds-orbiters-shared 3.3.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 +3 -2
- 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 +0 -2
- package/dist/{types/loaders → loaders}/ktx2.d.ts +4 -6
- 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/{types/render → render}/frameBudget.d.ts +18 -17
- 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/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 +0 -2
- 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 +29 -39
- 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} +21 -21
- package/src/loaders/{bodyTextureLoader.js → bodyTextureLoader.ts} +10 -7
- package/src/loaders/{cubeMapLoader.js → cubeMapLoader.ts} +83 -49
- package/src/loaders/{glbLoader.js → glbLoader.ts} +41 -18
- 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} +45 -32
- 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/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} +19 -12
- package/src/world/index.ts +7 -0
- package/src/world/worldGLB.ts +41 -0
- package/src/world/{worldManager.js → worldManager.ts} +67 -32
- package/src/world/{worldNormal.js → worldNormal.ts} +25 -20
- package/src/world/{worldTexture.js → worldTexture.ts} +70 -51
- package/dist/types/config/env.d.ts +0 -12
- package/dist/types/config/index.d.ts +0 -2
- package/dist/types/config/textureConfig.d.ts +0 -121
- package/dist/types/core/index.d.ts +0 -2
- package/dist/types/core/lightFresnel.d.ts +0 -5
- package/dist/types/core/lights.d.ts +0 -58
- package/dist/types/index.d.ts +0 -6
- package/dist/types/loaders/bodyTextureLoader.d.ts +0 -12
- package/dist/types/loaders/cubeMapLoader.d.ts +0 -70
- package/dist/types/loaders/glbLoader.d.ts +0 -24
- package/dist/types/loaders/index.d.ts +0 -5
- package/dist/types/moons/index.d.ts +0 -2
- package/dist/types/moons/moonsGeneration.d.ts +0 -72
- package/dist/types/moons/moonsManager.d.ts +0 -27
- package/dist/types/render/index.d.ts +0 -2
- package/dist/types/render/viewportCompositor.d.ts +0 -58
- package/dist/types/utils/dispose.d.ts +0 -2
- package/dist/types/utils/index.d.ts +0 -2
- package/dist/types/utils/materials.d.ts +0 -2
- package/dist/types/world/cloudShell.d.ts +0 -25
- package/dist/types/world/index.d.ts +0 -6
- package/dist/types/world/worldGLB.d.ts +0 -20
- package/dist/types/world/worldManager.d.ts +0 -63
- package/dist/types/world/worldNormal.d.ts +0 -43
- package/dist/types/world/worldTexture.d.ts +0 -80
- package/src/config/env.js +0 -38
- package/src/utils/dispose.js +0 -39
- package/src/utils/materials.js +0 -31
- package/src/world/worldGLB.js +0 -19
- /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
|
@@ -4,7 +4,17 @@ import { getKTX2LoaderForRenderer } from './ktx2.js';
|
|
|
4
4
|
|
|
5
5
|
// Base URL is resolved at runtime via helper
|
|
6
6
|
|
|
7
|
-
|
|
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 {
|
|
8
18
|
if (!raw) return '001';
|
|
9
19
|
const legacy = raw.match(/^(\d{1,2})_skycube(?:_low)?$/);
|
|
10
20
|
if (legacy) return String(parseInt(legacy[1], 10)).padStart(3, '0');
|
|
@@ -14,10 +24,10 @@ export function normalizeSkycubeName(raw) {
|
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
// Standard face order for cube maps
|
|
17
|
-
const FACE_ORDER = ['right', 'left', 'top', 'bottom', 'front', 'back'];
|
|
27
|
+
const FACE_ORDER: FaceName[] = ['right', 'left', 'top', 'bottom', 'front', 'back'];
|
|
18
28
|
|
|
19
29
|
// Build complete face URL sets for a given skycube ID (now returns relative paths)
|
|
20
|
-
export function buildSkycubefaceKeys(skycubeId, extension = 'ktx2') {
|
|
30
|
+
export function buildSkycubefaceKeys(skycubeId: string | null | undefined, extension = 'ktx2') {
|
|
21
31
|
const id = normalizeSkycubeName(skycubeId);
|
|
22
32
|
const base = `/default/skybox/${id}`;
|
|
23
33
|
return {
|
|
@@ -35,7 +45,8 @@ function isLowPerformanceDevice() {
|
|
|
35
45
|
try {
|
|
36
46
|
const ua = navigator.userAgent.toLowerCase();
|
|
37
47
|
const isMobileUA = /mobi|android|iphone|ipad|ipod/.test(ua);
|
|
38
|
-
|
|
48
|
+
// `deviceMemory` is not in every browser, nor in the DOM types.
|
|
49
|
+
const mem = (navigator as Navigator & { deviceMemory?: number }).deviceMemory || 0; // GB
|
|
39
50
|
const cores = navigator.hardwareConcurrency || 0;
|
|
40
51
|
const dpr = window.devicePixelRatio || 1;
|
|
41
52
|
return isMobileUA || mem && mem <= 4 || cores && cores <= 4 || (dpr > 2 && cores <= 6);
|
|
@@ -43,13 +54,14 @@ function isLowPerformanceDevice() {
|
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
// Validate face URLs object
|
|
46
|
-
function validatefaceKeys(faceKeys) {
|
|
57
|
+
function validatefaceKeys(faceKeys: unknown): faceKeys is FaceKeys {
|
|
47
58
|
if (!faceKeys || typeof faceKeys !== 'object') return false;
|
|
48
|
-
|
|
59
|
+
const keys = faceKeys as Record<string, unknown>;
|
|
60
|
+
return FACE_ORDER.every(face => keys[face] && typeof keys[face] === 'string');
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
// Detect extension type from face URLs
|
|
52
|
-
function detectExtension(faceKeys) {
|
|
64
|
+
function detectExtension(faceKeys: FaceKeys): 'ktx2' | 'jpg' {
|
|
53
65
|
const url = faceKeys.right || '';
|
|
54
66
|
if (url.toLowerCase().includes('.ktx2')) return 'ktx2';
|
|
55
67
|
if (url.toLowerCase().match(/\.(jpg|jpeg|png)$/)) return 'jpg';
|
|
@@ -63,11 +75,11 @@ function detectExtension(faceKeys) {
|
|
|
63
75
|
const FACES_IN_FLIGHT = 3;
|
|
64
76
|
|
|
65
77
|
// Load KTX2 faces and create skybox mesh
|
|
66
|
-
async function loadFacesKTX2(faceKeys, renderer) {
|
|
78
|
+
async function loadFacesKTX2(faceKeys: FaceKeys, renderer: THREE.WebGLRenderer): Promise<SkyboxResource> {
|
|
67
79
|
const loader = getKTX2LoaderForRenderer(renderer);
|
|
68
80
|
const anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
|
|
69
81
|
|
|
70
|
-
const loadFace = async face => {
|
|
82
|
+
const loadFace = async (face: FaceName) => {
|
|
71
83
|
const tex = await loader.loadAsync(faceKeys[face]);
|
|
72
84
|
tex.flipY = false;
|
|
73
85
|
tex.wrapS = THREE.ClampToEdgeWrapping;
|
|
@@ -86,8 +98,8 @@ async function loadFacesKTX2(faceKeys, renderer) {
|
|
|
86
98
|
|
|
87
99
|
// allSettled, not all: a bare Promise.all rejects on the first failure and leaks the faces that
|
|
88
100
|
// land after it. Settle every face first, then dispose everything that arrived before rethrowing.
|
|
89
|
-
const loaded = [];
|
|
90
|
-
let failure = null;
|
|
101
|
+
const loaded: Awaited<ReturnType<typeof loadFace>>[] = [];
|
|
102
|
+
let failure: unknown = null;
|
|
91
103
|
for (const result of settled) {
|
|
92
104
|
if (result.status === 'fulfilled') loaded.push(result.value);
|
|
93
105
|
else if (!failure) failure = result.reason;
|
|
@@ -102,8 +114,8 @@ async function loadFacesKTX2(faceKeys, renderer) {
|
|
|
102
114
|
const size = 500;
|
|
103
115
|
const box = new THREE.BoxGeometry(size, size, size);
|
|
104
116
|
// Map faces to geometry indices (corrected for orientation)
|
|
105
|
-
const orderMap = { right:0, left:1, top:3, bottom:2, front:4, back:5 };
|
|
106
|
-
const mats = new Array(6).fill(null);
|
|
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);
|
|
107
119
|
|
|
108
120
|
loaded.forEach(({ face, tex }) => {
|
|
109
121
|
mats[orderMap[face]] = new THREE.MeshBasicMaterial({
|
|
@@ -124,13 +136,14 @@ async function loadFacesKTX2(faceKeys, renderer) {
|
|
|
124
136
|
}
|
|
125
137
|
}
|
|
126
138
|
|
|
127
|
-
|
|
139
|
+
// Every slot is filled by now (a face's material or black).
|
|
140
|
+
const mesh = new THREE.Mesh(box, mats as THREE.MeshBasicMaterial[]);
|
|
128
141
|
mesh.name = 'SkyboxKTX2Mesh';
|
|
129
142
|
return { __ewType: 'skybox-mesh', mesh };
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
// Load JPG/PNG faces and create CubeTexture
|
|
133
|
-
function loadFacesJPG(faceKeys) {
|
|
146
|
+
function loadFacesJPG(faceKeys: FaceKeys): Promise<THREE.CubeTexture> {
|
|
134
147
|
const urls = FACE_ORDER.map(face => faceKeys[face]);
|
|
135
148
|
return new Promise((resolve, reject) => {
|
|
136
149
|
new THREE.CubeTextureLoader().load(
|
|
@@ -148,9 +161,10 @@ function loadFacesJPG(faceKeys) {
|
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
// Resolve relative paths to full URLs
|
|
151
|
-
function resolveUrls(faceKeys) {
|
|
164
|
+
function resolveUrls(faceKeys: FaceKeys): FaceKeys {
|
|
152
165
|
const PUBLIC_BASE = getPublicBase();
|
|
153
|
-
|
|
166
|
+
// Every face is set below.
|
|
167
|
+
const resolved = {} as FaceKeys;
|
|
154
168
|
FACE_ORDER.forEach(face => {
|
|
155
169
|
const url = faceKeys[face];
|
|
156
170
|
if (url && url.startsWith('/')) {
|
|
@@ -168,16 +182,26 @@ function resolveUrls(faceKeys) {
|
|
|
168
182
|
* path is taken — the old `window.__EW_RENDERER` fallback is gone; thread the owning view's
|
|
169
183
|
* renderer. (The PNG/JPG path is renderer-agnostic, so its absence only bites on KTX2.)
|
|
170
184
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* @param {import('three').WebGLRenderer | null} [opts.renderer]
|
|
178
|
-
* @returns {Promise<SkyboxResource>} pass it to `setBackground`.
|
|
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.
|
|
179
191
|
*/
|
|
180
|
-
export async function loadCubeMap({
|
|
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> {
|
|
181
205
|
|
|
182
206
|
// If no face URLs provided, build them from skycube name
|
|
183
207
|
if (!faceKeys && skycubeName) {
|
|
@@ -216,7 +240,8 @@ export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, fo
|
|
|
216
240
|
}
|
|
217
241
|
|
|
218
242
|
// Try PNG fallback (convert URLs if needed)
|
|
219
|
-
|
|
243
|
+
// Every face is set below.
|
|
244
|
+
const pngUrls = {} as FaceKeys;
|
|
220
245
|
FACE_ORDER.forEach(face => {
|
|
221
246
|
pngUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.png');
|
|
222
247
|
});
|
|
@@ -230,9 +255,11 @@ export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, fo
|
|
|
230
255
|
}
|
|
231
256
|
|
|
232
257
|
// Dispose helpers to ensure clean swaps
|
|
233
|
-
function disposeSkyboxMesh(mesh) {
|
|
258
|
+
function disposeSkyboxMesh(mesh: THREE.Mesh) {
|
|
234
259
|
try {
|
|
235
|
-
|
|
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];
|
|
236
263
|
mats.forEach(mat => {
|
|
237
264
|
if (!mat) return;
|
|
238
265
|
if (mat.map && mat.map.dispose) mat.map.dispose();
|
|
@@ -247,42 +274,50 @@ function disposeSkyboxMesh(mesh) {
|
|
|
247
274
|
/**
|
|
248
275
|
* What `loadCubeMap` resolves to: a skybox mesh wrapper (the KTX2 path) or a cube texture (the
|
|
249
276
|
* PNG/JPG path). Read `mesh` to tell them apart.
|
|
250
|
-
* @typedef {{ __ewType: 'skybox-mesh', mesh: import('three').Mesh } | (import('three').CubeTexture & { mesh?: undefined })} SkyboxResource
|
|
251
277
|
*/
|
|
278
|
+
export type SkyboxResource =
|
|
279
|
+
| { __ewType: 'skybox-mesh'; mesh: THREE.Mesh }
|
|
280
|
+
| (THREE.CubeTexture & { mesh?: undefined });
|
|
252
281
|
|
|
253
282
|
/**
|
|
254
283
|
* Show a loaded skybox as the scene's background. A previous skybox mesh or cube-texture background is
|
|
255
284
|
* disposed (a plain-texture background is not). Takes what `loadCubeMap` resolved to (or any texture);
|
|
256
285
|
* anything else, `null` included, clears it.
|
|
257
286
|
*
|
|
258
|
-
* @param {import('three').Scene} scene
|
|
259
|
-
* @param {unknown} resource
|
|
260
287
|
*/
|
|
261
|
-
export function setBackground(scene, resource) {
|
|
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;
|
|
262
295
|
// Clean up previous skybox mesh
|
|
263
|
-
if (
|
|
264
|
-
const oldMesh =
|
|
296
|
+
if (sky.__EW_SKYBOX_MESH && sky.__EW_SKYBOX_MESH.parent === scene) {
|
|
297
|
+
const oldMesh = sky.__EW_SKYBOX_MESH;
|
|
265
298
|
scene.remove(oldMesh);
|
|
266
299
|
disposeSkyboxMesh(oldMesh);
|
|
267
|
-
|
|
300
|
+
sky.__EW_SKYBOX_MESH = null;
|
|
268
301
|
}
|
|
269
302
|
|
|
270
303
|
// Dispose previous CubeTexture background
|
|
271
|
-
|
|
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) {
|
|
272
307
|
try {
|
|
273
|
-
|
|
308
|
+
background.dispose();
|
|
274
309
|
} catch(e) {
|
|
275
310
|
console.warn('[CubeMapLoader] Background disposal error:', e);
|
|
276
311
|
}
|
|
277
312
|
}
|
|
278
313
|
|
|
279
314
|
// Set new background
|
|
280
|
-
if (
|
|
315
|
+
if (res?.__ewType === 'skybox-mesh') {
|
|
281
316
|
scene.background = null;
|
|
282
|
-
|
|
283
|
-
scene.add(
|
|
284
|
-
} else if (
|
|
285
|
-
scene.background = resource;
|
|
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;
|
|
286
321
|
} else {
|
|
287
322
|
scene.background = null;
|
|
288
323
|
}
|
|
@@ -290,18 +325,17 @@ export function setBackground(scene, resource) {
|
|
|
290
325
|
// Dispatch update event for reactive systems
|
|
291
326
|
try {
|
|
292
327
|
window.dispatchEvent(new CustomEvent('ew:skycube-updated', {
|
|
293
|
-
detail: { type:
|
|
328
|
+
detail: { type: res?.__ewType || 'texture' }
|
|
294
329
|
}));
|
|
295
330
|
} catch(e) {
|
|
296
331
|
console.warn('[CubeMapLoader] Event dispatch error:', e);
|
|
297
332
|
}
|
|
298
333
|
}
|
|
299
334
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
export async function initSpaceScene(scene, { renderer } = {}) {
|
|
335
|
+
export async function initSpaceScene(
|
|
336
|
+
scene: THREE.Scene,
|
|
337
|
+
{ renderer }: { renderer?: THREE.WebGLRenderer | null } = {},
|
|
338
|
+
): Promise<void> {
|
|
305
339
|
const faceKeys = buildSkycubefaceKeys('001');
|
|
306
340
|
const cube = await loadCubeMap({ faceKeys, preferKTX2: true, renderer });
|
|
307
341
|
setBackground(scene, cube);
|
|
@@ -1,24 +1,44 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
3
|
-
import { ensureStandardMaterial, applyFlagsToMaterial } from '../utils/materials.js';
|
|
3
|
+
import { ensureStandardMaterial, applyFlagsToMaterial, type MaterialFlags } from '../utils/materials.js';
|
|
4
4
|
import { getKTX2LoaderForRenderer } from './ktx2.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* textures bind to. A page with several renderers has no single correct global, so the old
|
|
19
|
-
* `window.__EW_RENDERER` fallback is gone — thread the owning view's renderer.
|
|
7
|
+
* `renderer` is REQUIRED at runtime (throws without one).
|
|
8
|
+
* - `retries` (3): load attempts before giving up.
|
|
9
|
+
* - `signal`: rejects the load on abort.
|
|
10
|
+
* - `applyFlags`: material flags (`forceOpaque`, `wireframe`) applied to every mesh.
|
|
11
|
+
* - `withCredentials` (false).
|
|
12
|
+
* - `emissiveIntensity` (1): self-glow of textured materials (their map doubles as the emissive
|
|
13
|
+
* map). 1 keeps the old flat-lit look; with a directional star light pass ≈0.1–0.15 so the night
|
|
14
|
+
* side reads.
|
|
15
|
+
* - `renderer`: REQUIRED (3.0): the renderer this GLB's textures bind to. A page with several
|
|
16
|
+
* renderers has no single correct global, so the old `window.__EW_RENDERER` fallback is gone —
|
|
17
|
+
* thread the owning view's renderer.
|
|
20
18
|
*/
|
|
21
|
-
export async function loadGLB(
|
|
19
|
+
export async function loadGLB(
|
|
20
|
+
url: string,
|
|
21
|
+
{
|
|
22
|
+
retries = 3,
|
|
23
|
+
signal,
|
|
24
|
+
onProgress,
|
|
25
|
+
applyFlags,
|
|
26
|
+
withCredentials = false,
|
|
27
|
+
renderer,
|
|
28
|
+
emissiveIntensity = 1.0,
|
|
29
|
+
}: {
|
|
30
|
+
retries?: number | undefined;
|
|
31
|
+
signal?: AbortSignal | undefined;
|
|
32
|
+
onProgress?: ((event: ProgressEvent) => void) | undefined;
|
|
33
|
+
applyFlags?: MaterialFlags | undefined;
|
|
34
|
+
withCredentials?: boolean | undefined;
|
|
35
|
+
emissiveIntensity?: number | undefined;
|
|
36
|
+
renderer: THREE.WebGLRenderer;
|
|
37
|
+
// The options stay optional as a whole, as the JSDoc had them: without them `renderer` is
|
|
38
|
+
// missing and the guard below throws, as it always did.
|
|
39
|
+
} = {} as { renderer: THREE.WebGLRenderer },
|
|
40
|
+
// The loaded scene. `undefined` only when `retries` is 0 or less: no attempt is made.
|
|
41
|
+
): Promise<THREE.Group | undefined> {
|
|
22
42
|
if (!renderer) throw new Error('[glb] loadGLB requires the owning renderer (KTX2 textures bind per WebGL context).');
|
|
23
43
|
|
|
24
44
|
let attempt = 0;
|
|
@@ -29,7 +49,7 @@ export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags
|
|
|
29
49
|
.setWithCredentials(Boolean(withCredentials))
|
|
30
50
|
.setKTX2Loader(getKTX2LoaderForRenderer(renderer));
|
|
31
51
|
|
|
32
|
-
const root = await new Promise((resolve, reject) => {
|
|
52
|
+
const root = await new Promise<THREE.Group>((resolve, reject) => {
|
|
33
53
|
loader.load(
|
|
34
54
|
url,
|
|
35
55
|
(gltf) => resolve(gltf.scene),
|
|
@@ -42,7 +62,10 @@ export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags
|
|
|
42
62
|
});
|
|
43
63
|
|
|
44
64
|
// Post-process materials
|
|
45
|
-
root.traverse((
|
|
65
|
+
root.traverse((object) => {
|
|
66
|
+
// The meshes a GLB loads carry one material each, and `ensureStandardMaterial` just made it a
|
|
67
|
+
// MeshStandardMaterial, which is what the lines below write to.
|
|
68
|
+
const child = object as THREE.Mesh<THREE.BufferGeometry, THREE.MeshStandardMaterial>;
|
|
46
69
|
if (child.isMesh && child.material) {
|
|
47
70
|
ensureStandardMaterial(child);
|
|
48
71
|
if (applyFlags) applyFlagsToMaterial(child.material, applyFlags);
|
|
@@ -69,7 +92,7 @@ export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags
|
|
|
69
92
|
root.position.set(0, 0, 0);
|
|
70
93
|
return root;
|
|
71
94
|
} catch (e) {
|
|
72
|
-
console.warn(`GLB load attempt ${attempt + 1} failed:`, e.message);
|
|
95
|
+
console.warn(`GLB load attempt ${attempt + 1} failed:`, (e as Error).message);
|
|
73
96
|
if (signal?.aborted || attempt >= retries - 1) throw e;
|
|
74
97
|
attempt++;
|
|
75
98
|
await new Promise(resolve => setTimeout(resolve, 500)); // Brief delay between retries
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Loaders exports
|
|
2
|
+
export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
|
|
3
|
+
export { loadGLB } from './glbLoader.js';
|
|
4
|
+
export { loadTexture } from './bodyTextureLoader.js';
|
|
5
|
+
export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
|
|
6
|
+
export type { SkyboxResource } from './cubeMapLoader.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
1
2
|
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -12,9 +13,9 @@ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
|
12
13
|
* (a single orbiter, today's Entangled World) get exactly ONE entry, so behaviour is byte-identical.
|
|
13
14
|
* (Was: a single `window.__EW_KTX2_LOADER` global shared by bodyTexture/glb/cubeMap loaders.)
|
|
14
15
|
*/
|
|
15
|
-
const loadersByRenderer = new WeakMap();
|
|
16
|
+
const loadersByRenderer = new WeakMap<THREE.WebGLRenderer, KTX2Loader>();
|
|
16
17
|
|
|
17
|
-
function makeLoader(renderer) {
|
|
18
|
+
function makeLoader(renderer: THREE.WebGLRenderer): KTX2Loader {
|
|
18
19
|
return new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -23,10 +24,8 @@ function makeLoader(renderer) {
|
|
|
23
24
|
* call sites compile while silently binding the transcoder to whichever renderer a page published
|
|
24
25
|
* last; on any multi-view page that meant untextured worlds. Every caller now threads its own
|
|
25
26
|
* renderer, so a missing one is a caller bug and fails loudly here rather than as a dark planet.
|
|
26
|
-
* @param {import('three').WebGLRenderer} renderer
|
|
27
|
-
* @returns {KTX2Loader}
|
|
28
27
|
*/
|
|
29
|
-
export function getKTX2LoaderForRenderer(renderer) {
|
|
28
|
+
export function getKTX2LoaderForRenderer(renderer: THREE.WebGLRenderer): KTX2Loader {
|
|
30
29
|
if (!renderer) {
|
|
31
30
|
throw new Error('[ktx2] getKTX2LoaderForRenderer requires the owning renderer — thread it from the view that renders this texture.');
|
|
32
31
|
}
|
|
@@ -41,9 +40,8 @@ export function getKTX2LoaderForRenderer(renderer) {
|
|
|
41
40
|
/**
|
|
42
41
|
* Dispose and forget the KTX2Loader for a renderer — call on that renderer's / scene's teardown so the
|
|
43
42
|
* transcoder workers are terminated (avoids leaking worker threads + GPU memory across view churn).
|
|
44
|
-
* @param {import('three').WebGLRenderer} renderer
|
|
45
43
|
*/
|
|
46
|
-
export function disposeKTX2LoaderForRenderer(renderer) {
|
|
44
|
+
export function disposeKTX2LoaderForRenderer(renderer: THREE.WebGLRenderer): void {
|
|
47
45
|
const loader = renderer && loadersByRenderer.get(renderer);
|
|
48
46
|
if (loader) {
|
|
49
47
|
try { loader.dispose?.(); } catch { /* already gone */ }
|
|
@@ -2,7 +2,7 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { loadTexture } from '../loaders/bodyTextureLoader.js';
|
|
3
3
|
import { resolvePublicUrl } from '../config/env.js';
|
|
4
4
|
|
|
5
|
-
function moonTexPath(file) {
|
|
5
|
+
function moonTexPath(file: string): string {
|
|
6
6
|
return resolvePublicUrl(`default/textures/moons/${file}`);
|
|
7
7
|
}
|
|
8
8
|
export const DEFAULT_MOON_TEX_KTX2 = moonTexPath('moon-equirectangular.ktx2');
|
|
@@ -22,34 +22,18 @@ export const MOON_ORBIT_SCATTER = 0.22;
|
|
|
22
22
|
export const MOON_HEIGHT_SCATTER = 0.12;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* `
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @param {number} [opts.emissiveColor=0x333333]
|
|
38
|
-
* @param {number} [opts.emissiveIntensity=0.18]
|
|
39
|
-
* @param {number} [opts.emissiveLift] night-side self-glow 0..1; omitted = `emissiveIntensity`.
|
|
40
|
-
* @param {number} [opts.roughness=0.85]
|
|
41
|
-
* @param {number} [opts.metalness=0]
|
|
42
|
-
* @param {boolean} [opts.debug=true]
|
|
43
|
-
* @param {boolean} [opts.smooth=true]
|
|
44
|
-
* @param {number} [opts.geometryWidthSegments=16]
|
|
45
|
-
* @param {number} [opts.geometryHeightSegments=12]
|
|
46
|
-
* @param {boolean} [opts.useBumpFromColor=true]
|
|
47
|
-
* @param {number} [opts.bumpScale=0.15]
|
|
48
|
-
* @param {boolean} [opts.debugShowUvGrid=false]
|
|
49
|
-
* @param {boolean} [opts.debugShowReferenceMoon=false]
|
|
50
|
-
* @param {number} [opts.referenceMoonScale=0.25]
|
|
51
|
-
* @param {boolean} [opts.useEmissiveMapFromTexture=true]
|
|
52
|
-
* @param {import('three').WebGLRenderer | null} [opts.renderer] binds the KTX2 moon texture.
|
|
25
|
+
* - `count` (0): 0 or less resolves to null. `orbitRadius` 1.5.
|
|
26
|
+
* - `textureFallback`: used instead of `texturePNG` when set.
|
|
27
|
+
* - `seed`: deterministic positions when set.
|
|
28
|
+
* - `textureKey`: replaces `textureKTX2` when set (the fallback stays `textureFallback` or
|
|
29
|
+
* `texturePNG`).
|
|
30
|
+
* - `emissiveLift`: night-side self-glow 0..1; omitted = `emissiveIntensity`.
|
|
31
|
+
* - `renderer`: binds the KTX2 moon texture.
|
|
32
|
+
* - Defaults: `moonColor` '#ffffff', `brightness` 1.15, `emissiveColor` 0x333333,
|
|
33
|
+
* `emissiveIntensity` 0.18, `roughness` 0.85, `metalness` 0, `debug` true, `smooth` true,
|
|
34
|
+
* `geometryWidthSegments` 16, `geometryHeightSegments` 12, `useBumpFromColor` true, `bumpScale`
|
|
35
|
+
* 0.15, `debugShowUvGrid` false, `debugShowReferenceMoon` false, `referenceMoonScale` 0.25,
|
|
36
|
+
* `useEmissiveMapFromTexture` true.
|
|
53
37
|
*/
|
|
54
38
|
export async function generateMoons({
|
|
55
39
|
count = 0,
|
|
@@ -88,6 +72,33 @@ export async function generateMoons({
|
|
|
88
72
|
// The renderer whose KTX2 transcoder decodes the moon texture. REQUIRED for the KTX2 texture (3.0:
|
|
89
73
|
// the loader has no global fallback).
|
|
90
74
|
renderer,
|
|
75
|
+
}: {
|
|
76
|
+
count?: number | undefined;
|
|
77
|
+
orbitRadius?: number | undefined;
|
|
78
|
+
textureKTX2?: string | undefined;
|
|
79
|
+
texturePNG?: string | undefined;
|
|
80
|
+
textureFallback?: string | null | undefined;
|
|
81
|
+
useTexture?: boolean | undefined;
|
|
82
|
+
seed?: string | number | null | undefined;
|
|
83
|
+
textureKey?: string | null | undefined;
|
|
84
|
+
moonColor?: string | undefined;
|
|
85
|
+
brightness?: number | undefined;
|
|
86
|
+
emissiveColor?: number | undefined;
|
|
87
|
+
emissiveIntensity?: number | undefined;
|
|
88
|
+
emissiveLift?: number | undefined;
|
|
89
|
+
roughness?: number | undefined;
|
|
90
|
+
metalness?: number | undefined;
|
|
91
|
+
debug?: boolean | undefined;
|
|
92
|
+
smooth?: boolean | undefined;
|
|
93
|
+
geometryWidthSegments?: number | undefined;
|
|
94
|
+
geometryHeightSegments?: number | undefined;
|
|
95
|
+
useBumpFromColor?: boolean | undefined;
|
|
96
|
+
bumpScale?: number | undefined;
|
|
97
|
+
debugShowUvGrid?: boolean | undefined;
|
|
98
|
+
debugShowReferenceMoon?: boolean | undefined;
|
|
99
|
+
referenceMoonScale?: number | undefined;
|
|
100
|
+
useEmissiveMapFromTexture?: boolean | undefined;
|
|
101
|
+
renderer?: THREE.WebGLRenderer | null | undefined;
|
|
91
102
|
} = {}) {
|
|
92
103
|
if (count <= 0) return null;
|
|
93
104
|
|
|
@@ -104,7 +115,8 @@ export async function generateMoons({
|
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
// Seeded random function (Linear Congruential Generator)
|
|
107
|
-
|
|
118
|
+
// A string seed was hashed to a number above.
|
|
119
|
+
let currentSeed = seedValue !== null ? Math.abs(seedValue as number) : Date.now();
|
|
108
120
|
const seededRandom = () => {
|
|
109
121
|
if (seedValue !== null) {
|
|
110
122
|
currentSeed = (currentSeed * 1664525 + 1013904223) % 4294967296;
|
|
@@ -116,13 +128,14 @@ export async function generateMoons({
|
|
|
116
128
|
const moonGeometry = new THREE.SphereGeometry(MOON_BASE_RADIUS, geometryWidthSegments, geometryHeightSegments);
|
|
117
129
|
|
|
118
130
|
// Optional UV grid override
|
|
119
|
-
let sharedTexture = null;
|
|
131
|
+
let sharedTexture: THREE.Texture | null = null;
|
|
120
132
|
let usedUvGrid = false;
|
|
121
133
|
async function buildUvGridTexture() {
|
|
122
134
|
const w = 512, h = 256; // 2:1 aspect
|
|
123
135
|
const canvas = document.createElement('canvas');
|
|
124
136
|
canvas.width = w; canvas.height = h;
|
|
125
|
-
|
|
137
|
+
// A missing 2D context throws on first use; the caller catches it, as before.
|
|
138
|
+
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
|
126
139
|
ctx.fillStyle = '#101018'; ctx.fillRect(0,0,w,h);
|
|
127
140
|
for (let lon=0; lon<=12; lon++) {
|
|
128
141
|
const x = (lon/12)*w; ctx.strokeStyle = lon % 3 ===0 ? '#ff4d4d':'#444'; ctx.lineWidth = lon%3===0?2:1; ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,h); ctx.stroke();
|
|
@@ -2,13 +2,36 @@ import { disposeObject3D } from '../utils/dispose.js';
|
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
import { generateMoons } from './moonsGeneration.js';
|
|
4
4
|
|
|
5
|
+
/** The `setAmount` options: what the moons look like and where they orbit. */
|
|
6
|
+
export interface MoonAmountOptions {
|
|
7
|
+
/** 1.5 by default. */
|
|
8
|
+
orbitRadius?: number;
|
|
9
|
+
textureKTX2?: string;
|
|
10
|
+
texturePNG?: string;
|
|
11
|
+
/** true by default. */
|
|
12
|
+
useTexture?: boolean;
|
|
13
|
+
/** Deterministic positions when set. */
|
|
14
|
+
seed?: string | number | null;
|
|
15
|
+
/** Replaces `textureKTX2` when set. */
|
|
16
|
+
textureKey?: string | null;
|
|
17
|
+
/** '#ffffff' by default. */
|
|
18
|
+
moonColor?: string;
|
|
19
|
+
/** Used instead of `texturePNG` when set. */
|
|
20
|
+
textureFallback?: string | null;
|
|
21
|
+
/** Night-side self-glow 0..1, kept for later rebuilds; anything but a number is ignored. */
|
|
22
|
+
emissiveLift?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
export class MoonsManager {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
26
|
+
// `declare`d so the emitted class assigns them in the constructor only, as the JS did.
|
|
27
|
+
declare scene: THREE.Scene;
|
|
28
|
+
declare renderer: THREE.WebGLRenderer | null | undefined;
|
|
29
|
+
declare group: THREE.Group | null;
|
|
30
|
+
declare _opId: number;
|
|
31
|
+
declare _emissiveLift: number | undefined;
|
|
32
|
+
|
|
33
|
+
/** `renderer`: the renderer the moon texture binds to (KTX2). */
|
|
34
|
+
constructor(scene: THREE.Scene, { renderer }: { renderer?: THREE.WebGLRenderer | null | undefined } = {}) {
|
|
12
35
|
this.scene = scene;
|
|
13
36
|
this.renderer = renderer;
|
|
14
37
|
this.group = null;
|
|
@@ -16,7 +39,8 @@ export class MoonsManager {
|
|
|
16
39
|
this._emissiveLift = undefined; // night-side self-glow; undefined = generator default
|
|
17
40
|
}
|
|
18
41
|
|
|
19
|
-
|
|
42
|
+
/** Replace the moons with `count` new ones; 0 or less (or none) just removes them. */
|
|
43
|
+
async setAmount(count: number | null | undefined, opts: MoonAmountOptions = {}): Promise<void> {
|
|
20
44
|
// Increment operation id to invalidate older async calls
|
|
21
45
|
const opId = ++this._opId;
|
|
22
46
|
|
|
@@ -41,7 +65,7 @@ export class MoonsManager {
|
|
|
41
65
|
} = opts;
|
|
42
66
|
if (typeof emissiveLift === 'number') this._emissiveLift = emissiveLift;
|
|
43
67
|
|
|
44
|
-
let newGroup = null;
|
|
68
|
+
let newGroup: THREE.Group | null = null;
|
|
45
69
|
try {
|
|
46
70
|
newGroup = await generateMoons({
|
|
47
71
|
count,
|
|
@@ -79,12 +103,14 @@ export class MoonsManager {
|
|
|
79
103
|
* Night-side self-glow 0..1, applied in place to the live moons and kept for later rebuilds.
|
|
80
104
|
* Low values (≈0.05) let a star-lit scene show the moons' dark sides.
|
|
81
105
|
*/
|
|
82
|
-
setEmissiveLift(v) {
|
|
106
|
+
setEmissiveLift(v: number): void {
|
|
83
107
|
if (typeof v !== 'number' || !Number.isFinite(v)) return;
|
|
84
108
|
this._emissiveLift = Math.min(1, Math.max(0, v));
|
|
85
|
-
this.group?.traverse((
|
|
109
|
+
this.group?.traverse((object) => {
|
|
110
|
+
// Only a mesh whose material has `emissiveIntensity` is changed; the value was just set above.
|
|
111
|
+
const child = object as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>;
|
|
86
112
|
if (child.isMesh && child.material && 'emissiveIntensity' in child.material) {
|
|
87
|
-
child.material.emissiveIntensity = this._emissiveLift;
|
|
113
|
+
child.material.emissiveIntensity = this._emissiveLift as number;
|
|
88
114
|
}
|
|
89
115
|
});
|
|
90
116
|
}
|