entangled-worlds-orbiters-shared 1.0.3 → 1.0.5
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 +10 -0
- package/package.json +1 -1
- package/src/config/textureConfig.js +21 -24
- package/src/world/worldTexture.js +80 -2
package/README.md
CHANGED
|
@@ -16,6 +16,16 @@ A shared library containing core components for 3D world rendering, moons system
|
|
|
16
16
|
```bash
|
|
17
17
|
npm install entangled-worlds-orbiters-shared
|
|
18
18
|
```
|
|
19
|
+
## Test locally
|
|
20
|
+
|
|
21
|
+
In /entangled-worlds-orbiters-shared:
|
|
22
|
+
npm install
|
|
23
|
+
npm link
|
|
24
|
+
|
|
25
|
+
In /entangled-worlds-preview or /orbiters:
|
|
26
|
+
|
|
27
|
+
npm link entangled-worlds-orbiters-shared
|
|
28
|
+
|
|
19
29
|
|
|
20
30
|
## Usage
|
|
21
31
|
|
package/package.json
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Texture Configuration Loader
|
|
3
|
-
* Loads texture definitions from
|
|
3
|
+
* Loads texture definitions from remote endpoint or uses passed config
|
|
4
4
|
*/
|
|
5
5
|
import { resolvePublicUrl } from './env.js';
|
|
6
6
|
|
|
7
|
-
const DEFAULT_CONFIG_URL = '/config/textures.json';
|
|
8
|
-
const FALLBACK_CONFIG_URL = 'https://api.entangled-worlds.com/config/textures.json'; // Future endpoint
|
|
9
|
-
|
|
10
7
|
let cachedConfig = null;
|
|
11
8
|
let configPromise = null;
|
|
9
|
+
let lastConfigUrl = null;
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
|
-
* Load texture configuration from
|
|
15
|
-
* @param {string} configUrl - Override
|
|
12
|
+
* Load texture configuration from remote endpoint or use passed config
|
|
13
|
+
* @param {string} configUrl - Override config URL
|
|
16
14
|
* @param {boolean} useCache - Whether to use cached config if available
|
|
17
15
|
* @returns {Promise<Object>} Texture configuration object
|
|
18
16
|
*/
|
|
19
|
-
export async function loadTextureConfig(configUrl =
|
|
17
|
+
export async function loadTextureConfig(configUrl = null, useCache = true) {
|
|
20
18
|
if (useCache && cachedConfig) {
|
|
21
19
|
return cachedConfig;
|
|
22
20
|
}
|
|
@@ -25,11 +23,19 @@ export async function loadTextureConfig(configUrl = DEFAULT_CONFIG_URL, useCache
|
|
|
25
23
|
return configPromise;
|
|
26
24
|
}
|
|
27
25
|
|
|
26
|
+
if (configUrl) {
|
|
27
|
+
lastConfigUrl = configUrl;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const targetUrl = configUrl || lastConfigUrl;
|
|
31
|
+
|
|
28
32
|
configPromise = (async () => {
|
|
29
33
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
if (!targetUrl) {
|
|
35
|
+
throw new Error('No texture config URL provided');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const response = await fetch(targetUrl);
|
|
33
39
|
|
|
34
40
|
if (!response.ok) {
|
|
35
41
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
@@ -46,19 +52,10 @@ export async function loadTextureConfig(configUrl = DEFAULT_CONFIG_URL, useCache
|
|
|
46
52
|
return config;
|
|
47
53
|
} catch (error) {
|
|
48
54
|
console.error('[TextureConfig] Failed to load config:', error);
|
|
49
|
-
|
|
50
|
-
// If primary URL fails and it's not already the fallback, try fallback
|
|
51
|
-
if (configUrl !== FALLBACK_CONFIG_URL) {
|
|
52
|
-
try {
|
|
53
|
-
return await loadTextureConfig(FALLBACK_CONFIG_URL, false);
|
|
54
|
-
} catch (fallbackError) {
|
|
55
|
-
console.error('[TextureConfig] Fallback also failed:', fallbackError);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Return minimal fallback configuration
|
|
55
|
+
// Return minimal fallback configuration when loading fails
|
|
60
56
|
console.warn('[TextureConfig] Using hardcoded fallback config');
|
|
61
|
-
|
|
57
|
+
cachedConfig = getHardcodedFallback();
|
|
58
|
+
return cachedConfig;
|
|
62
59
|
} finally {
|
|
63
60
|
configPromise = null;
|
|
64
61
|
}
|
|
@@ -267,10 +264,10 @@ function getHardcodedFallback() {
|
|
|
267
264
|
* @param {string} configUrl - Override config URL
|
|
268
265
|
* @returns {Promise<Object>} Fresh configuration object
|
|
269
266
|
*/
|
|
270
|
-
export async function reloadTextureConfig(configUrl =
|
|
267
|
+
export async function reloadTextureConfig(configUrl = null) {
|
|
271
268
|
cachedConfig = null;
|
|
272
269
|
configPromise = null;
|
|
273
|
-
return loadTextureConfig(configUrl, false);
|
|
270
|
+
return loadTextureConfig(configUrl || lastConfigUrl, false);
|
|
274
271
|
}
|
|
275
272
|
|
|
276
273
|
/**
|
|
@@ -5,6 +5,25 @@ import { getLightFresnel } from '../core/lightFresnel.js';
|
|
|
5
5
|
import { createCloudShell } from './cloudShell.js';
|
|
6
6
|
import { resolvePublicUrl } from '../config/env.js';
|
|
7
7
|
|
|
8
|
+
// Debug tag so you can verify the loaded file version in console
|
|
9
|
+
export const WORLD_TEXTURE_DEBUG_TAG = 'WT:vFlipGeom-r3@2025-10-28T17:22:00Z';
|
|
10
|
+
|
|
11
|
+
// Enable debug if query/localStorage says so; expose tag
|
|
12
|
+
if (typeof window !== 'undefined') {
|
|
13
|
+
const q = (window.location && window.location.search) || '';
|
|
14
|
+
const qsDebug = /(?:[?&])debugTextures=1(?:&|$)/.test(q);
|
|
15
|
+
const lsDebug = (window.localStorage && window.localStorage.DEBUG_TEXTURE_FLIP === '1') || false;
|
|
16
|
+
if (!window.DEBUG_TEXTURE_FLIP && (qsDebug || lsDebug)) {
|
|
17
|
+
window.DEBUG_TEXTURE_FLIP = true;
|
|
18
|
+
}
|
|
19
|
+
window.EW_WT_DEBUG_TAG = WORLD_TEXTURE_DEBUG_TAG;
|
|
20
|
+
if (window.DEBUG_TEXTURE_FLIP) {
|
|
21
|
+
console.info('[worldTexture] loaded', WORLD_TEXTURE_DEBUG_TAG);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let WT_LOGGED_FIRST_CALL = false;
|
|
26
|
+
|
|
8
27
|
export const EARTH_RADIUS_UNITS = 0.5; // scene units
|
|
9
28
|
|
|
10
29
|
/**
|
|
@@ -75,6 +94,7 @@ export async function makeSphere({
|
|
|
75
94
|
const widthSegs = Number.isFinite(sphereWidthSegments) ? Math.max(3, Math.floor(sphereWidthSegments)) : 64;
|
|
76
95
|
const heightSegs = Number.isFinite(sphereHeightSegments) ? Math.max(2, Math.floor(sphereHeightSegments)) : 64;
|
|
77
96
|
const geometry = new THREE.SphereGeometry(radius, widthSegs, heightSegs);
|
|
97
|
+
let meshGeometry = geometry; // may clone/flip UVs for compressed textures
|
|
78
98
|
|
|
79
99
|
const rough = (typeof materialRoughness === 'number') ? THREE.MathUtils.clamp(materialRoughness, 0, 1) : 0.7;
|
|
80
100
|
const metal = (typeof materialMetalness === 'number') ? THREE.MathUtils.clamp(materialMetalness, 0, 1) : 0.0;
|
|
@@ -86,6 +106,48 @@ export async function makeSphere({
|
|
|
86
106
|
mirror: THREE.MirroredRepeatWrapping,
|
|
87
107
|
};
|
|
88
108
|
|
|
109
|
+
// For compressed textures (e.g., KTX2) WebGL cannot apply UNPACK_FLIP_Y at upload.
|
|
110
|
+
// To normalize orientation across loaders, emulate vertical flip in UV sampling:
|
|
111
|
+
// v' = 1 - v => repeat.y = -repeat.y, offset.y = 1 - offset.y
|
|
112
|
+
const normalizeCompressedFlip = (t) => {
|
|
113
|
+
if (!t || !t.isCompressedTexture) return;
|
|
114
|
+
const wantFlip = (typeof flipY === 'boolean') ? flipY : true; // match uncompressed default
|
|
115
|
+
if (!wantFlip) return;
|
|
116
|
+
const rY = (t.repeat && typeof t.repeat.y === 'number') ? t.repeat.y : 1;
|
|
117
|
+
const oY = (t.offset && typeof t.offset.y === 'number') ? t.offset.y : 0;
|
|
118
|
+
t.repeat.y = -rY;
|
|
119
|
+
t.offset.y = 1 - oY;
|
|
120
|
+
t.needsUpdate = true;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Emulate vertical flip for compressed textures (e.g., KTX2) using UV transform
|
|
124
|
+
const normalizeCompressedFlipY = (t, desired) => {
|
|
125
|
+
if (!t || !t.isCompressedTexture) return;
|
|
126
|
+
const wantFlip = (typeof desired === 'boolean') ? desired : true; // default to match uncompressed loader
|
|
127
|
+
const rY = t.repeat?.y ?? 1;
|
|
128
|
+
const oY = t.offset?.y ?? 0;
|
|
129
|
+
if (wantFlip) {
|
|
130
|
+
t.repeat.y = -Math.abs(rY);
|
|
131
|
+
t.offset.y = 1 - oY;
|
|
132
|
+
} else {
|
|
133
|
+
t.repeat.y = Math.abs(rY);
|
|
134
|
+
t.offset.y = 0;
|
|
135
|
+
}
|
|
136
|
+
t.needsUpdate = true;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Flip UV.y on a cloned geometry (keeps other layers safe)
|
|
140
|
+
const flipGeometryUVY = (geom) => {
|
|
141
|
+
if (!geom || !geom.attributes || !geom.attributes.uv) return 0;
|
|
142
|
+
const uv = geom.attributes.uv;
|
|
143
|
+
const arr = uv.array;
|
|
144
|
+
for (let i = 1; i < arr.length; i += 2) {
|
|
145
|
+
arr[i] = 1 - arr[i]; // v = 1 - v
|
|
146
|
+
}
|
|
147
|
+
uv.needsUpdate = true;
|
|
148
|
+
return uv.count;
|
|
149
|
+
};
|
|
150
|
+
|
|
89
151
|
// Attach a Fresnel glow mesh as a child of the sphere mesh
|
|
90
152
|
function createGlow(rimHex, facingHex) {
|
|
91
153
|
const fresnelMat = getLightFresnel({ rimHex, facingHex });
|
|
@@ -153,6 +215,11 @@ export async function makeSphere({
|
|
|
153
215
|
const applyWrapOptions = (t) => {
|
|
154
216
|
t.wrapS = WRAP[wrapS] ?? THREE.RepeatWrapping;
|
|
155
217
|
t.wrapT = WRAP[wrapT] ?? THREE.ClampToEdgeWrapping;
|
|
218
|
+
// Compressed textures + NPOT prefer clamp to avoid artifacts
|
|
219
|
+
if (isCompressed) {
|
|
220
|
+
t.wrapS = THREE.ClampToEdgeWrapping;
|
|
221
|
+
t.wrapT = THREE.ClampToEdgeWrapping;
|
|
222
|
+
}
|
|
156
223
|
if (Array.isArray(center) && center.length === 2) t.center.set(center[0], center[1]);
|
|
157
224
|
if (typeof rotationDeg === 'number') t.rotation = (rotationDeg * Math.PI) / 180;
|
|
158
225
|
if (typeof repeatX === 'number' || typeof repeatY === 'number') {
|
|
@@ -201,12 +268,23 @@ export async function makeSphere({
|
|
|
201
268
|
tex.wrapT = WRAP[wrapT] ?? THREE.ClampToEdgeWrapping;
|
|
202
269
|
tex.repeat.x = Math.max(2, typeof repeatX === 'number' ? repeatX : 2);
|
|
203
270
|
applyWrapOptions(tex);
|
|
271
|
+
// If compressed (e.g., KTX2), normalize vertical orientation using sampling transform
|
|
272
|
+
normalizeCompressedFlip(tex);
|
|
204
273
|
material.map = tex;
|
|
205
274
|
material.needsUpdate = true;
|
|
206
275
|
}
|
|
207
276
|
} else {
|
|
208
|
-
// Equirectangular or no canvas: just use original texture
|
|
277
|
+
// Equirectangular or no canvas: just use original texture
|
|
209
278
|
applyWrapOptions(tex);
|
|
279
|
+
const wantFlip = (typeof flipY === 'boolean') ? flipY : true;
|
|
280
|
+
if (isCompressed && wantFlip) {
|
|
281
|
+
meshGeometry = geometry.clone();
|
|
282
|
+
flipGeometryUVY(meshGeometry);
|
|
283
|
+
} else if (isCompressed) {
|
|
284
|
+
tex.repeat.y = Math.abs(tex.repeat?.y ?? 1);
|
|
285
|
+
tex.offset.y = 0;
|
|
286
|
+
tex.needsUpdate = true;
|
|
287
|
+
}
|
|
210
288
|
material.map = tex;
|
|
211
289
|
material.needsUpdate = true;
|
|
212
290
|
}
|
|
@@ -216,7 +294,7 @@ export async function makeSphere({
|
|
|
216
294
|
}
|
|
217
295
|
|
|
218
296
|
if (flags) applyFlagsToMaterial(material, flags);
|
|
219
|
-
const mesh = new THREE.Mesh(
|
|
297
|
+
const mesh = new THREE.Mesh(meshGeometry, material);
|
|
220
298
|
mesh.position.set(0, 0, 0);
|
|
221
299
|
|
|
222
300
|
// NEW: add cloud shell before glow
|