entangled-worlds-orbiters-shared 1.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 +75 -0
- package/package.json +49 -0
- package/public/config/textures.json +332 -0
- package/src/arrow-ui/BooleanController.js +31 -0
- package/src/arrow-ui/ColorController.js +108 -0
- package/src/arrow-ui/Controller.js +447 -0
- package/src/arrow-ui/FunctionController.js +27 -0
- package/src/arrow-ui/GUI.js +595 -0
- package/src/arrow-ui/NumberController.js +567 -0
- package/src/arrow-ui/OptionController.js +64 -0
- package/src/arrow-ui/StringController.js +41 -0
- package/src/arrow-ui/index.js +13 -0
- package/src/arrow-ui/lilGuiLoader.js +10 -0
- package/src/arrow-ui/utils/getColorFormat.js +75 -0
- package/src/arrow-ui/utils/injectStyles.js +10 -0
- package/src/arrow-ui/utils/normalizeColorString.js +29 -0
- package/src/config/index.js +8 -0
- package/src/config/textureConfig.js +299 -0
- package/src/core/index.js +3 -0
- package/src/core/lightFresnel.js +64 -0
- package/src/core/lights.js +43 -0
- package/src/index.js +8 -0
- package/src/loaders/bodyTextureLoader.js +39 -0
- package/src/loaders/cubeMapLoader.js +264 -0
- package/src/loaders/glbLoader.js +71 -0
- package/src/loaders/index.js +4 -0
- package/src/moons/index.js +3 -0
- package/src/moons/moonsGeneration.js +228 -0
- package/src/moons/moonsManager.js +64 -0
- package/src/utils/dispose.js +20 -0
- package/src/utils/index.js +3 -0
- package/src/utils/materials.js +31 -0
- package/src/world/cloudShell.js +89 -0
- package/src/world/index.js +6 -0
- package/src/world/worldGLB.js +7 -0
- package/src/world/worldManager.js +93 -0
- package/src/world/worldNormal.js +25 -0
- package/src/world/worldTexture.js +239 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { loadTexture } from '../loaders/bodyTextureLoader.js';
|
|
3
|
+
|
|
4
|
+
// Texture-only clouds material (alpha mask + tint), UV-scrolled
|
|
5
|
+
async function makeTextureMaterial({
|
|
6
|
+
color = 0xffffff,
|
|
7
|
+
opacity = 0.6,
|
|
8
|
+
textureUrl,
|
|
9
|
+
speed = 0.015,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
12
|
+
color,
|
|
13
|
+
transparent: true,
|
|
14
|
+
opacity: 1.0, // drive alpha via alphaMap
|
|
15
|
+
roughness: 1.0,
|
|
16
|
+
metalness: 0.0,
|
|
17
|
+
depthTest: true,
|
|
18
|
+
depthWrite: false,
|
|
19
|
+
side: THREE.FrontSide,
|
|
20
|
+
toneMapped: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (textureUrl) {
|
|
24
|
+
const tex = await loadTexture(textureUrl);
|
|
25
|
+
tex.wrapS = THREE.RepeatWrapping;
|
|
26
|
+
tex.wrapT = THREE.RepeatWrapping;
|
|
27
|
+
// Use linear space for alpha masks if available
|
|
28
|
+
tex.colorSpace = THREE.LinearSRGBColorSpace || THREE.SRGBColorSpace;
|
|
29
|
+
tex.repeat.set(1, 1); // fixed tiling
|
|
30
|
+
tex.anisotropy = 2;
|
|
31
|
+
mat.alphaMap = tex;
|
|
32
|
+
mat.opacity = opacity; // material opacity multiplier
|
|
33
|
+
mat.needsUpdate = true;
|
|
34
|
+
} else {
|
|
35
|
+
// No texture -> invisible shell
|
|
36
|
+
mat.opacity = 0.0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Randomize wind direction per instance (0..360)
|
|
40
|
+
const dirRad = (Math.random() * 360 * Math.PI) / 180;
|
|
41
|
+
const wind = new THREE.Vector2(Math.cos(dirRad), Math.sin(dirRad)).multiplyScalar(speed);
|
|
42
|
+
mat.userData.update = (timeSec) => {
|
|
43
|
+
const t = mat.alphaMap;
|
|
44
|
+
if (t) {
|
|
45
|
+
t.offset.x = wind.x * timeSec;
|
|
46
|
+
t.offset.y = wind.y * timeSec;
|
|
47
|
+
t.needsUpdate = true;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return mat;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function createCloudShell({
|
|
55
|
+
baseGeometry,
|
|
56
|
+
baseRadius = 0.5,
|
|
57
|
+
widthSegments = 64,
|
|
58
|
+
heightSegments = 64,
|
|
59
|
+
textureUrl = '/assets/texture/2k_earth_clouds.jpg',
|
|
60
|
+
color = 0xffffff,
|
|
61
|
+
opacity = 0.6,
|
|
62
|
+
speed = 0.015,
|
|
63
|
+
} = {}) {
|
|
64
|
+
const geom = baseGeometry
|
|
65
|
+
? baseGeometry.clone()
|
|
66
|
+
: new THREE.SphereGeometry(baseRadius, widthSegments, heightSegments);
|
|
67
|
+
|
|
68
|
+
const material = await makeTextureMaterial({
|
|
69
|
+
color,
|
|
70
|
+
opacity,
|
|
71
|
+
textureUrl,
|
|
72
|
+
speed,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const mesh = new THREE.Mesh(geom, material);
|
|
76
|
+
const s = 1.001; // fixed scale
|
|
77
|
+
mesh.scale.set(s, s, s);
|
|
78
|
+
mesh.name = 'worldCloudShell';
|
|
79
|
+
mesh.renderOrder = 998; // after planet, before glow
|
|
80
|
+
|
|
81
|
+
mesh.onBeforeRender = () => {
|
|
82
|
+
const t = performance.now() * 0.001;
|
|
83
|
+
if (material.userData && typeof material.userData.update === 'function') {
|
|
84
|
+
material.userData.update(t);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return mesh;
|
|
89
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// World system exports
|
|
2
|
+
export { WorldManager } from './worldManager.js';
|
|
3
|
+
export { makeSphere, EARTH_RADIUS_UNITS } from './worldTexture.js';
|
|
4
|
+
export { makeNormalSphere } from './worldNormal.js';
|
|
5
|
+
export { makeGLB } from './worldGLB.js';
|
|
6
|
+
export { createCloudShell } from './cloudShell.js';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { disposeObject3D } from '../utils/dispose.js';
|
|
2
|
+
import { makeGLB } from './worldGLB.js';
|
|
3
|
+
import { makeSphere } from './worldTexture.js';
|
|
4
|
+
import { makeNormalSphere } from './worldNormal.js';
|
|
5
|
+
|
|
6
|
+
export class WorldManager {
|
|
7
|
+
constructor(scene, { onLoadStart, onLoadEnd } = {}) {
|
|
8
|
+
this.scene = scene;
|
|
9
|
+
this.current = null;
|
|
10
|
+
this.flags = {};
|
|
11
|
+
this.onLoadStart = onLoadStart;
|
|
12
|
+
this.onLoadEnd = onLoadEnd;
|
|
13
|
+
this.token = 0;
|
|
14
|
+
// Registry for custom world creators
|
|
15
|
+
this.customCreators = new Map();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setFlags(flags) {
|
|
19
|
+
console.log('[WorldManager] setFlags:', flags);
|
|
20
|
+
this.flags = { ...this.flags, ...flags };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
disposeCurrent() {
|
|
24
|
+
if (this.current) {
|
|
25
|
+
console.log('[WorldManager] disposeCurrent:', this.current?.name || this.current);
|
|
26
|
+
disposeObject3D(this.current);
|
|
27
|
+
this.scene.remove(this.current);
|
|
28
|
+
this.current = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Register custom world creators for app-specific world types
|
|
33
|
+
registerWorldCreator(mode, creatorFunction) {
|
|
34
|
+
this.customCreators.set(mode, creatorFunction);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async setMode(mode, args = {}) {
|
|
38
|
+
const myToken = ++this.token;
|
|
39
|
+
console.log('[WorldManager] setMode request:', { mode, args });
|
|
40
|
+
this.onLoadStart?.();
|
|
41
|
+
try {
|
|
42
|
+
let obj;
|
|
43
|
+
|
|
44
|
+
// Check for custom creators first
|
|
45
|
+
if (this.customCreators.has(mode)) {
|
|
46
|
+
console.log(`[WorldManager] -> custom creator for mode ${mode}`);
|
|
47
|
+
obj = await this.customCreators.get(mode)({ flags: this.flags, ...args });
|
|
48
|
+
} else if (mode === 2) {
|
|
49
|
+
console.log('[WorldManager] -> makeGLB');
|
|
50
|
+
obj = await makeGLB({ modelUrl: args.modelUrl, flags: this.flags });
|
|
51
|
+
} else if (mode === 3) {
|
|
52
|
+
console.log('[WorldManager] -> makeNormalSphere');
|
|
53
|
+
obj = await makeNormalSphere({ flags: this.flags, ...args });
|
|
54
|
+
} else {
|
|
55
|
+
console.log('[WorldManager] -> makeSphere (textured)');
|
|
56
|
+
obj = await makeSphere({ textureUrl: args.textureUrl, flags: this.flags, ...args });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.token !== myToken) {
|
|
60
|
+
console.log('[WorldManager] setMode superseded, abandoning result');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.disposeCurrent();
|
|
64
|
+
this.scene.add(obj);
|
|
65
|
+
this.current = obj;
|
|
66
|
+
console.log('[WorldManager] setMode applied. current:', obj?.name || obj);
|
|
67
|
+
|
|
68
|
+
// If the object exposes a frame updater, run a render-time loop
|
|
69
|
+
if (obj && obj.userData && typeof obj.userData.update === 'function' && window.__EW_RENDERER) {
|
|
70
|
+
const updater = obj.userData.update;
|
|
71
|
+
console.log('[WorldManager] update loop started for', obj.name || obj);
|
|
72
|
+
const tick = () => {
|
|
73
|
+
if (this.current === obj) {
|
|
74
|
+
try {
|
|
75
|
+
updater(window.__EW_RENDERER, performance.now() * 0.001);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.warn('[WorldManager] update loop error:', e);
|
|
78
|
+
}
|
|
79
|
+
requestAnimationFrame(tick);
|
|
80
|
+
} else {
|
|
81
|
+
console.log('[WorldManager] update loop stopped (object replaced)');
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
requestAnimationFrame(tick);
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn('[WorldManager] setMode error:', e);
|
|
88
|
+
throw e;
|
|
89
|
+
} finally {
|
|
90
|
+
if (this.token === myToken) this.onLoadEnd?.();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Normal world (MeshNormalMaterial) implementation
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import { EARTH_RADIUS_UNITS } from './worldTexture.js';
|
|
4
|
+
import { applyFlagsToMaterial } from '../utils/materials.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a simple sphere using THREE.MeshNormalMaterial.
|
|
8
|
+
* Accepts the same basic geometry params as textured sphere so the manager API is consistent.
|
|
9
|
+
*/
|
|
10
|
+
export async function makeNormalSphere({
|
|
11
|
+
sphereRadius,
|
|
12
|
+
sphereWidthSegments,
|
|
13
|
+
sphereHeightSegments,
|
|
14
|
+
flags,
|
|
15
|
+
} = {}) {
|
|
16
|
+
const radius = typeof sphereRadius === 'number' ? Math.max(0.0001, sphereRadius) : EARTH_RADIUS_UNITS;
|
|
17
|
+
const widthSegs = Number.isFinite(sphereWidthSegments) ? Math.max(3, Math.floor(sphereWidthSegments)) : 64;
|
|
18
|
+
const heightSegs = Number.isFinite(sphereHeightSegments) ? Math.max(2, Math.floor(sphereHeightSegments)) : 64;
|
|
19
|
+
const geometry = new THREE.SphereGeometry(radius, widthSegs, heightSegs);
|
|
20
|
+
const material = new THREE.MeshNormalMaterial();
|
|
21
|
+
if (flags) applyFlagsToMaterial(material, flags);
|
|
22
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
23
|
+
mesh.name = 'worldNormalSphere';
|
|
24
|
+
return mesh;
|
|
25
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { loadTexture } from '../loaders/bodyTextureLoader.js';
|
|
3
|
+
import { applyFlagsToMaterial } from '../utils/materials.js';
|
|
4
|
+
import { getLightFresnel } from '../core/lightFresnel.js';
|
|
5
|
+
import { createCloudShell } from './cloudShell.js';
|
|
6
|
+
|
|
7
|
+
export const EARTH_RADIUS_UNITS = 0.5; // scene units
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Simplified detection: width = height -> rectangular, width = 2×height -> equirectangular
|
|
11
|
+
*/
|
|
12
|
+
function isLikelyEquirectangular(img) {
|
|
13
|
+
const aspectRatio = img.width / img.height;
|
|
14
|
+
|
|
15
|
+
// Check for 2:1 aspect ratio (equirectangular)
|
|
16
|
+
const is2to1 = Math.abs(aspectRatio - 2.0) < 0.1;
|
|
17
|
+
|
|
18
|
+
// Check for 1:1 aspect ratio (square -> treat as rectangular)
|
|
19
|
+
const is1to1 = Math.abs(aspectRatio - 1.0) < 0.1;
|
|
20
|
+
|
|
21
|
+
if (is1to1) {
|
|
22
|
+
return false; // Square images are rectangular
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (is2to1) {
|
|
26
|
+
return true; // 2:1 images are equirectangular
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// For other aspect ratios, default to rectangular
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function makeSphere({
|
|
34
|
+
textureUrl,
|
|
35
|
+
flags,
|
|
36
|
+
uOffset,
|
|
37
|
+
textureOffsetDeg,
|
|
38
|
+
mirrorX,
|
|
39
|
+
mirrorDuplicateX, // optional, still supported for explicit override
|
|
40
|
+
autoDetectEquirectangular = true,
|
|
41
|
+
forceEquirectangular = false,
|
|
42
|
+
forceRectangular = false,
|
|
43
|
+
// Wrapping params
|
|
44
|
+
wrapS = 'repeat',
|
|
45
|
+
wrapT = 'clamp',
|
|
46
|
+
repeatX,
|
|
47
|
+
repeatY,
|
|
48
|
+
rotationDeg,
|
|
49
|
+
center = [0.5, 0.5],
|
|
50
|
+
flipY,
|
|
51
|
+
// NEW: sphere geometry controls (optional)
|
|
52
|
+
sphereRadius,
|
|
53
|
+
sphereWidthSegments,
|
|
54
|
+
sphereHeightSegments,
|
|
55
|
+
// PBR material tuning (optional)
|
|
56
|
+
materialRoughness,
|
|
57
|
+
materialMetalness,
|
|
58
|
+
// NEW: glow controls
|
|
59
|
+
glow = true,
|
|
60
|
+
glowRimHex = 0x66ccff,
|
|
61
|
+
glowFacingHex = 0x000000,
|
|
62
|
+
glowScale = 1.18,
|
|
63
|
+
glowBias = 0.02,
|
|
64
|
+
glowFresnelScale = 1.0,
|
|
65
|
+
glowPower = 3.5,
|
|
66
|
+
// texture clouds controls (simplified)
|
|
67
|
+
cloudsEnabled = false,
|
|
68
|
+
cloudsTextureUrl = '/assets/texture/2k_earth_clouds.jpg',
|
|
69
|
+
cloudsColor = 0xffffff,
|
|
70
|
+
cloudsOpacity = 0.6,
|
|
71
|
+
cloudsSpeed = 0.015,
|
|
72
|
+
} = {}) {
|
|
73
|
+
const radius = typeof sphereRadius === 'number' ? Math.max(0.0001, sphereRadius) : EARTH_RADIUS_UNITS;
|
|
74
|
+
const widthSegs = Number.isFinite(sphereWidthSegments) ? Math.max(3, Math.floor(sphereWidthSegments)) : 64;
|
|
75
|
+
const heightSegs = Number.isFinite(sphereHeightSegments) ? Math.max(2, Math.floor(sphereHeightSegments)) : 64;
|
|
76
|
+
const geometry = new THREE.SphereGeometry(radius, widthSegs, heightSegs);
|
|
77
|
+
|
|
78
|
+
const rough = (typeof materialRoughness === 'number') ? THREE.MathUtils.clamp(materialRoughness, 0, 1) : 0.7;
|
|
79
|
+
const metal = (typeof materialMetalness === 'number') ? THREE.MathUtils.clamp(materialMetalness, 0, 1) : 0.0;
|
|
80
|
+
const material = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: rough, metalness: metal });
|
|
81
|
+
|
|
82
|
+
const WRAP = {
|
|
83
|
+
clamp: THREE.ClampToEdgeWrapping,
|
|
84
|
+
repeat: THREE.RepeatWrapping,
|
|
85
|
+
mirror: THREE.MirroredRepeatWrapping,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Attach a Fresnel glow mesh as a child of the sphere mesh
|
|
89
|
+
function createGlow(rimHex, facingHex) {
|
|
90
|
+
const fresnelMat = getLightFresnel({ rimHex, facingHex });
|
|
91
|
+
// tweak uniforms for stronger rim
|
|
92
|
+
fresnelMat.uniforms.fresnelBias.value = glowBias;
|
|
93
|
+
fresnelMat.uniforms.fresnelScale.value = glowFresnelScale;
|
|
94
|
+
fresnelMat.uniforms.fresnelPower.value = glowPower;
|
|
95
|
+
|
|
96
|
+
const glowGeom = geometry.clone();
|
|
97
|
+
const fresnelMesh = new THREE.Mesh(glowGeom, fresnelMat);
|
|
98
|
+
// Slightly larger to avoid z-fighting and create a halo
|
|
99
|
+
const s = Math.max(glowScale, 1.0);
|
|
100
|
+
fresnelMesh.scale.set(s, s, s);
|
|
101
|
+
fresnelMesh.renderOrder = 999; // render after base mesh
|
|
102
|
+
fresnelMesh.name = 'worldTextureGlow';
|
|
103
|
+
return fresnelMesh;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const chosenTexture = textureUrl; // require explicit textureUrl — no implicit default
|
|
107
|
+
if (chosenTexture) {
|
|
108
|
+
try {
|
|
109
|
+
const tex = await loadTexture(chosenTexture);
|
|
110
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
111
|
+
tex.generateMipmaps = true;
|
|
112
|
+
tex.anisotropy = 8;
|
|
113
|
+
if (typeof flipY === 'boolean') tex.flipY = flipY;
|
|
114
|
+
|
|
115
|
+
// Decide mapping approach
|
|
116
|
+
let shouldMirrorDuplicate = mirrorDuplicateX;
|
|
117
|
+
if (tex.image) {
|
|
118
|
+
const img = tex.image;
|
|
119
|
+
let isEquirectangular = false;
|
|
120
|
+
|
|
121
|
+
if (forceEquirectangular) {
|
|
122
|
+
isEquirectangular = true;
|
|
123
|
+
} else if (forceRectangular) {
|
|
124
|
+
isEquirectangular = false;
|
|
125
|
+
} else if (autoDetectEquirectangular && typeof mirrorDuplicateX === 'undefined') {
|
|
126
|
+
const aspectDetection = isLikelyEquirectangular(img);
|
|
127
|
+
isEquirectangular = aspectDetection && Math.abs(img.width / img.height - 2.0) < 0.05;
|
|
128
|
+
} else {
|
|
129
|
+
isEquirectangular = !shouldMirrorDuplicate;
|
|
130
|
+
}
|
|
131
|
+
shouldMirrorDuplicate = !isEquirectangular;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Default to rectangular duplicate if still undefined
|
|
135
|
+
if (typeof shouldMirrorDuplicate === 'undefined') {
|
|
136
|
+
shouldMirrorDuplicate = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Seam alignment (offset only)
|
|
140
|
+
let offsetX = 0;
|
|
141
|
+
if (typeof uOffset === 'number') {
|
|
142
|
+
offsetX = ((uOffset % 1) + 1) % 1;
|
|
143
|
+
} else if (typeof textureOffsetDeg === 'number') {
|
|
144
|
+
offsetX = (((textureOffsetDeg / 360) % 1) + 1) % 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const applyWrapOptions = (t) => {
|
|
148
|
+
t.wrapS = WRAP[wrapS] ?? THREE.RepeatWrapping;
|
|
149
|
+
t.wrapT = WRAP[wrapT] ?? THREE.ClampToEdgeWrapping;
|
|
150
|
+
if (Array.isArray(center) && center.length === 2) t.center.set(center[0], center[1]);
|
|
151
|
+
if (typeof rotationDeg === 'number') t.rotation = (rotationDeg * Math.PI) / 180;
|
|
152
|
+
if (typeof repeatX === 'number' || typeof repeatY === 'number') {
|
|
153
|
+
t.repeat.set(
|
|
154
|
+
typeof repeatX === 'number' ? repeatX : t.repeat.x,
|
|
155
|
+
typeof repeatY === 'number' ? repeatY : t.repeat.y
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
t.offset.x = offsetX;
|
|
159
|
+
if (mirrorX) {
|
|
160
|
+
t.repeat.x = -Math.abs(t.repeat.x || 1);
|
|
161
|
+
t.offset.x = (t.offset.x + 1) % 1;
|
|
162
|
+
}
|
|
163
|
+
t.needsUpdate = true;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (shouldMirrorDuplicate && tex.image && typeof document !== 'undefined') {
|
|
167
|
+
// Create a mirrored duplicate without any blending
|
|
168
|
+
const img = tex.image;
|
|
169
|
+
try {
|
|
170
|
+
const canvas = document.createElement('canvas');
|
|
171
|
+
canvas.width = img.width * 2;
|
|
172
|
+
canvas.height = img.height;
|
|
173
|
+
const ctx = canvas.getContext('2d');
|
|
174
|
+
|
|
175
|
+
// Left half: original
|
|
176
|
+
ctx.drawImage(img, 0, 0, img.width, img.height);
|
|
177
|
+
// Right half: horizontally flipped copy
|
|
178
|
+
ctx.save();
|
|
179
|
+
ctx.translate(img.width, 0);
|
|
180
|
+
ctx.scale(-1, 1);
|
|
181
|
+
ctx.drawImage(img, -img.width, 0, img.width, img.height);
|
|
182
|
+
ctx.restore();
|
|
183
|
+
|
|
184
|
+
const processedTex = new THREE.CanvasTexture(canvas);
|
|
185
|
+
processedTex.colorSpace = THREE.SRGBColorSpace;
|
|
186
|
+
processedTex.generateMipmaps = true;
|
|
187
|
+
processedTex.anisotropy = 8;
|
|
188
|
+
|
|
189
|
+
applyWrapOptions(processedTex);
|
|
190
|
+
material.map = processedTex;
|
|
191
|
+
material.needsUpdate = true;
|
|
192
|
+
} catch (e) {
|
|
193
|
+
// Fallback: use mirrored repeat directly
|
|
194
|
+
tex.wrapS = THREE.MirroredRepeatWrapping;
|
|
195
|
+
tex.wrapT = WRAP[wrapT] ?? THREE.ClampToEdgeWrapping;
|
|
196
|
+
tex.repeat.x = Math.max(2, typeof repeatX === 'number' ? repeatX : 2);
|
|
197
|
+
applyWrapOptions(tex);
|
|
198
|
+
material.map = tex;
|
|
199
|
+
material.needsUpdate = true;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// Equirectangular or no canvas: just use original texture without any edge processing
|
|
203
|
+
applyWrapOptions(tex);
|
|
204
|
+
material.map = tex;
|
|
205
|
+
material.needsUpdate = true;
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.warn('[worldTexture] Failed to load texture:', e);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (flags) applyFlagsToMaterial(material, flags);
|
|
213
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
214
|
+
mesh.position.set(0, 0, 0);
|
|
215
|
+
|
|
216
|
+
// NEW: add cloud shell before glow
|
|
217
|
+
if (cloudsEnabled) {
|
|
218
|
+
const cloudMesh = await createCloudShell({
|
|
219
|
+
baseGeometry: geometry,
|
|
220
|
+
baseRadius: radius,
|
|
221
|
+
widthSegments: widthSegs,
|
|
222
|
+
heightSegments: heightSegs,
|
|
223
|
+
textureUrl: cloudsTextureUrl,
|
|
224
|
+
color: cloudsColor,
|
|
225
|
+
opacity: cloudsOpacity,
|
|
226
|
+
speed: cloudsSpeed,
|
|
227
|
+
});
|
|
228
|
+
mesh.add(cloudMesh);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Optionally add glow as a child of the mesh so it follows transforms
|
|
232
|
+
if (glow) {
|
|
233
|
+
const glowMesh = createGlow(glowRimHex, glowFacingHex);
|
|
234
|
+
mesh.add(glowMesh);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return mesh;
|
|
238
|
+
}
|
|
239
|
+
|