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,264 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
3
|
+
|
|
4
|
+
// Base URL
|
|
5
|
+
const PUBLIC_BASE = (import.meta?.env?.VITE_PUBLIC_HERBARIUM_BASE) || (window?.VITE_PUBLIC_HERBARIUM_BASE) || 'https://dev-herbarium.plantasia.space';
|
|
6
|
+
|
|
7
|
+
// Shared loader + caches
|
|
8
|
+
const ktx2SupportCache = new Map(); // base -> true|false
|
|
9
|
+
|
|
10
|
+
function getSharedKTX2Loader(renderer) {
|
|
11
|
+
if (window.__EW_KTX2_LOADER) return window.__EW_KTX2_LOADER;
|
|
12
|
+
const loader = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
|
|
13
|
+
window.__EW_KTX2_LOADER = loader;
|
|
14
|
+
return loader;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeSkycubeName(raw) {
|
|
18
|
+
if (!raw) return '001';
|
|
19
|
+
const legacy = raw.match(/^(\d{1,2})_skycube(?:_low)?$/);
|
|
20
|
+
if (legacy) return String(parseInt(legacy[1], 10)).padStart(3, '0');
|
|
21
|
+
const num = raw.match(/(\d{1,3})/);
|
|
22
|
+
if (num) return String(parseInt(num[1], 10)).padStart(3, '0');
|
|
23
|
+
return '001';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Standard face order for cube maps
|
|
27
|
+
const FACE_ORDER = ['right', 'left', 'top', 'bottom', 'front', 'back'];
|
|
28
|
+
|
|
29
|
+
// Build complete face URL sets for a given skycube ID (now returns relative paths)
|
|
30
|
+
export function buildSkycubefaceKeys(skycubeId, extension = 'ktx2') {
|
|
31
|
+
const id = normalizeSkycubeName(skycubeId);
|
|
32
|
+
const base = `/default/skybox/${id}`;
|
|
33
|
+
return {
|
|
34
|
+
right: `${base}/right.${extension}`,
|
|
35
|
+
left: `${base}/left.${extension}`,
|
|
36
|
+
top: `${base}/top.${extension}`,
|
|
37
|
+
bottom: `${base}/bottom.${extension}`,
|
|
38
|
+
front: `${base}/front.${extension}`,
|
|
39
|
+
back: `${base}/back.${extension}`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Heuristic low-performance detection
|
|
44
|
+
function isLowPerformanceDevice() {
|
|
45
|
+
try {
|
|
46
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
47
|
+
const isMobileUA = /mobi|android|iphone|ipad|ipod/.test(ua);
|
|
48
|
+
const mem = navigator.deviceMemory || 0; // GB
|
|
49
|
+
const cores = navigator.hardwareConcurrency || 0;
|
|
50
|
+
const dpr = window.devicePixelRatio || 1;
|
|
51
|
+
return isMobileUA || mem && mem <= 4 || cores && cores <= 4 || (dpr > 2 && cores <= 6);
|
|
52
|
+
} catch { return false; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate face URLs object
|
|
56
|
+
function validatefaceKeys(faceKeys) {
|
|
57
|
+
if (!faceKeys || typeof faceKeys !== 'object') return false;
|
|
58
|
+
return FACE_ORDER.every(face => faceKeys[face] && typeof faceKeys[face] === 'string');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Detect extension type from face URLs
|
|
62
|
+
function detectExtension(faceKeys) {
|
|
63
|
+
const url = faceKeys.right || '';
|
|
64
|
+
if (url.toLowerCase().includes('.ktx2')) return 'ktx2';
|
|
65
|
+
if (url.toLowerCase().match(/\.(jpg|jpeg|png)$/)) return 'jpg';
|
|
66
|
+
return 'ktx2'; // default assumption
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Load KTX2 faces and create skybox mesh
|
|
70
|
+
async function loadFacesKTX2(faceKeys, renderer) {
|
|
71
|
+
const loader = getSharedKTX2Loader(renderer);
|
|
72
|
+
const loaded = [];
|
|
73
|
+
|
|
74
|
+
for (const face of FACE_ORDER) {
|
|
75
|
+
const url = faceKeys[face];
|
|
76
|
+
try {
|
|
77
|
+
const tex = await loader.loadAsync(url);
|
|
78
|
+
tex.flipY = false;
|
|
79
|
+
tex.wrapS = THREE.ClampToEdgeWrapping;
|
|
80
|
+
tex.wrapT = THREE.ClampToEdgeWrapping;
|
|
81
|
+
tex.anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
|
|
82
|
+
loaded.push({ face, tex });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// Dispose already loaded textures on failure
|
|
85
|
+
loaded.forEach(f => f.tex.dispose?.());
|
|
86
|
+
throw e;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const size = 500;
|
|
91
|
+
const box = new THREE.BoxGeometry(size, size, size);
|
|
92
|
+
// Map faces to geometry indices (corrected for orientation)
|
|
93
|
+
const orderMap = { right:0, left:1, top:3, bottom:2, front:4, back:5 };
|
|
94
|
+
const mats = new Array(6).fill(null);
|
|
95
|
+
|
|
96
|
+
loaded.forEach(({ face, tex }) => {
|
|
97
|
+
mats[orderMap[face]] = new THREE.MeshBasicMaterial({
|
|
98
|
+
map: tex,
|
|
99
|
+
side: THREE.BackSide,
|
|
100
|
+
depthWrite: false
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Fill any missing materials with black
|
|
105
|
+
for (let i = 0; i < 6; i++) {
|
|
106
|
+
if (!mats[i]) {
|
|
107
|
+
mats[i] = new THREE.MeshBasicMaterial({
|
|
108
|
+
color: 0x000000,
|
|
109
|
+
side: THREE.BackSide,
|
|
110
|
+
depthWrite: false
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const mesh = new THREE.Mesh(box, mats);
|
|
116
|
+
mesh.name = 'SkyboxKTX2Mesh';
|
|
117
|
+
return { __ewType: 'skybox-mesh', mesh };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Load JPG/PNG faces and create CubeTexture
|
|
121
|
+
function loadFacesJPG(faceKeys) {
|
|
122
|
+
const urls = FACE_ORDER.map(face => faceKeys[face]);
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
new THREE.CubeTextureLoader().load(
|
|
125
|
+
urls,
|
|
126
|
+
tex => {
|
|
127
|
+
tex.magFilter = THREE.LinearFilter;
|
|
128
|
+
tex.minFilter = THREE.LinearMipMapLinearFilter;
|
|
129
|
+
tex.anisotropy = 8;
|
|
130
|
+
resolve(tex);
|
|
131
|
+
},
|
|
132
|
+
undefined,
|
|
133
|
+
err => reject(err)
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Resolve relative paths to full URLs
|
|
139
|
+
function resolveUrls(faceKeys) {
|
|
140
|
+
const resolved = {};
|
|
141
|
+
FACE_ORDER.forEach(face => {
|
|
142
|
+
const url = faceKeys[face];
|
|
143
|
+
if (url && url.startsWith('/')) {
|
|
144
|
+
resolved[face] = `${PUBLIC_BASE}${url}`;
|
|
145
|
+
} else {
|
|
146
|
+
resolved[face] = url; // already absolute or invalid
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return resolved;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Main loader function - always uses explicit face URLs
|
|
153
|
+
export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, forceLow } = {}) {
|
|
154
|
+
const renderer = window.__EW_RENDERER;
|
|
155
|
+
|
|
156
|
+
// If no face URLs provided, build them from skycube name
|
|
157
|
+
if (!faceKeys && skycubeName) {
|
|
158
|
+
const low = typeof forceLow === 'boolean' ? forceLow : isLowPerformanceDevice();
|
|
159
|
+
const extension = preferKTX2 ? 'ktx2' : 'jpg';
|
|
160
|
+
const id = normalizeSkycubeName(skycubeName);
|
|
161
|
+
const suffix = low ? '_low' : '';
|
|
162
|
+
const relativePath = `/default/skybox/${id}${suffix}`;
|
|
163
|
+
|
|
164
|
+
faceKeys = {
|
|
165
|
+
right: `${relativePath}/right.${extension}`,
|
|
166
|
+
left: `${relativePath}/left.${extension}`,
|
|
167
|
+
top: `${relativePath}/top.${extension}`,
|
|
168
|
+
bottom: `${relativePath}/bottom.${extension}`,
|
|
169
|
+
front: `${relativePath}/front.${extension}`,
|
|
170
|
+
back: `${relativePath}/back.${extension}`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!validatefaceKeys(faceKeys)) {
|
|
175
|
+
throw new Error('Invalid face URLs provided');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Resolve relative paths to full URLs
|
|
179
|
+
const resolvedUrls = resolveUrls(faceKeys);
|
|
180
|
+
const extension = detectExtension(resolvedUrls);
|
|
181
|
+
|
|
182
|
+
// Try KTX2 first if preferred and supported
|
|
183
|
+
if (preferKTX2 && extension === 'ktx2' && renderer) {
|
|
184
|
+
try {
|
|
185
|
+
return await loadFacesKTX2(resolvedUrls, renderer);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.warn('[CubeMapLoader] KTX2 load failed, trying JPG fallback:', e);
|
|
188
|
+
// Fall through to JPG attempt
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Try JPG fallback (convert URLs if needed)
|
|
193
|
+
const jpgUrls = {};
|
|
194
|
+
FACE_ORDER.forEach(face => {
|
|
195
|
+
jpgUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.jpg');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
return await loadFacesJPG(jpgUrls);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.error('[CubeMapLoader] All loading attempts failed:', e);
|
|
202
|
+
throw new Error('Failed to load cube map with any format');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Dispose helpers to ensure clean swaps
|
|
207
|
+
function disposeSkyboxMesh(mesh) {
|
|
208
|
+
try {
|
|
209
|
+
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
210
|
+
mats.forEach(mat => {
|
|
211
|
+
if (!mat) return;
|
|
212
|
+
if (mat.map && mat.map.dispose) mat.map.dispose();
|
|
213
|
+
if (mat.dispose) mat.dispose();
|
|
214
|
+
});
|
|
215
|
+
if (mesh.geometry && mesh.geometry.dispose) mesh.geometry.dispose();
|
|
216
|
+
} catch(e) {
|
|
217
|
+
console.warn('[CubeMapLoader] Disposal error:', e);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function setBackground(scene, resource) {
|
|
222
|
+
// Clean up previous skybox mesh
|
|
223
|
+
if (scene.__EW_SKYBOX_MESH && scene.__EW_SKYBOX_MESH.parent === scene) {
|
|
224
|
+
const oldMesh = scene.__EW_SKYBOX_MESH;
|
|
225
|
+
scene.remove(oldMesh);
|
|
226
|
+
disposeSkyboxMesh(oldMesh);
|
|
227
|
+
scene.__EW_SKYBOX_MESH = null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Dispose previous CubeTexture background
|
|
231
|
+
if (scene.background && scene.background.isCubeTexture) {
|
|
232
|
+
try {
|
|
233
|
+
scene.background.dispose();
|
|
234
|
+
} catch(e) {
|
|
235
|
+
console.warn('[CubeMapLoader] Background disposal error:', e);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Set new background
|
|
240
|
+
if (resource?.__ewType === 'skybox-mesh') {
|
|
241
|
+
scene.background = null;
|
|
242
|
+
scene.__EW_SKYBOX_MESH = resource.mesh;
|
|
243
|
+
scene.add(resource.mesh);
|
|
244
|
+
} else if (resource?.isCubeTexture || resource?.isTexture) {
|
|
245
|
+
scene.background = resource;
|
|
246
|
+
} else {
|
|
247
|
+
scene.background = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Dispatch update event for reactive systems
|
|
251
|
+
try {
|
|
252
|
+
window.dispatchEvent(new CustomEvent('ew:skycube-updated', {
|
|
253
|
+
detail: { type: resource?.__ewType || 'texture' }
|
|
254
|
+
}));
|
|
255
|
+
} catch(e) {
|
|
256
|
+
console.warn('[CubeMapLoader] Event dispatch error:', e);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function initSpaceScene(scene) {
|
|
261
|
+
const faceKeys = buildSkycubefaceKeys('001');
|
|
262
|
+
const cube = await loadCubeMap({ faceKeys, preferKTX2: true });
|
|
263
|
+
setBackground(scene, cube);
|
|
264
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
3
|
+
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
4
|
+
import { ensureStandardMaterial, applyFlagsToMaterial } from '../utils/materials.js';
|
|
5
|
+
|
|
6
|
+
function getSharedKTX2Loader(renderer) {
|
|
7
|
+
if (window.__EW_KTX2_LOADER) return window.__EW_KTX2_LOADER;
|
|
8
|
+
const loader = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
|
|
9
|
+
window.__EW_KTX2_LOADER = loader;
|
|
10
|
+
return loader;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags } = {}) {
|
|
14
|
+
const renderer = window.__EW_RENDERER;
|
|
15
|
+
if (!renderer) throw new Error('Renderer not ready for KTX2');
|
|
16
|
+
|
|
17
|
+
let attempt = 0;
|
|
18
|
+
while (attempt < retries) {
|
|
19
|
+
try {
|
|
20
|
+
const loader = new GLTFLoader()
|
|
21
|
+
.setCrossOrigin('anonymous')
|
|
22
|
+
.setKTX2Loader(getSharedKTX2Loader(renderer));
|
|
23
|
+
|
|
24
|
+
const root = await new Promise((resolve, reject) => {
|
|
25
|
+
loader.load(
|
|
26
|
+
url,
|
|
27
|
+
(gltf) => resolve(gltf.scene),
|
|
28
|
+
onProgress,
|
|
29
|
+
reject
|
|
30
|
+
);
|
|
31
|
+
if (signal) {
|
|
32
|
+
signal.addEventListener('abort', () => reject(new Error('aborted')));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Post-process materials
|
|
37
|
+
root.traverse((child) => {
|
|
38
|
+
if (child.isMesh && child.material) {
|
|
39
|
+
ensureStandardMaterial(child);
|
|
40
|
+
if (applyFlags) applyFlagsToMaterial(child.material, applyFlags);
|
|
41
|
+
|
|
42
|
+
// Force texture updates for KTX2
|
|
43
|
+
if (child.material.map) {
|
|
44
|
+
child.material.map.anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
|
|
45
|
+
child.material.map.needsUpdate = true;
|
|
46
|
+
// Ensure proper color space for compressed textures
|
|
47
|
+
child.material.map.colorSpace = THREE.SRGBColorSpace;
|
|
48
|
+
// Ensure material color doesn't block the texture
|
|
49
|
+
child.material.color.setHex(0xffffff);
|
|
50
|
+
// Add strong emissive to ensure visibility
|
|
51
|
+
child.material.emissive.setHex(0x888888);
|
|
52
|
+
child.material.emissiveMap = child.material.map;
|
|
53
|
+
child.material.emissiveIntensity = 1.0;
|
|
54
|
+
console.log('✅ KTX2 texture loaded:', child.material.map.constructor.name, `${child.material.map.image.width}x${child.material.map.image.height}`);
|
|
55
|
+
}
|
|
56
|
+
child.material.needsUpdate = true;
|
|
57
|
+
child.castShadow = false;
|
|
58
|
+
child.receiveShadow = false;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
root.position.set(0, 0, 0);
|
|
63
|
+
return root;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.warn(`GLB load attempt ${attempt + 1} failed:`, e.message);
|
|
66
|
+
if (signal?.aborted || attempt >= retries - 1) throw e;
|
|
67
|
+
attempt++;
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, 500)); // Brief delay between retries
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { loadTexture } from '../loaders/bodyTextureLoader.js';
|
|
3
|
+
|
|
4
|
+
// Resolve public herbarium base (strip trailing slash)
|
|
5
|
+
const HERBARIUM_BASE = (import.meta?.env?.VITE_PUBLIC_HERBARIUM_BASE || 'https://dev-herbarium.plantasia.space').replace(/\/$/, '');
|
|
6
|
+
function moonTexPath(file) { return `${HERBARIUM_BASE}/default/textures/moons/${file}`; }
|
|
7
|
+
export const DEFAULT_MOON_TEX_KTX2 = moonTexPath('moon-equirectangular.ktx2');
|
|
8
|
+
export const DEFAULT_MOON_TEX_PNG = moonTexPath('moon-equirectangular.png');
|
|
9
|
+
|
|
10
|
+
export async function generateMoons({
|
|
11
|
+
count = 0,
|
|
12
|
+
orbitRadius = 1.5,
|
|
13
|
+
textureKTX2 = DEFAULT_MOON_TEX_KTX2,
|
|
14
|
+
texturePNG = DEFAULT_MOON_TEX_PNG,
|
|
15
|
+
useTexture = true,
|
|
16
|
+
// NEW: seed for deterministic positioning
|
|
17
|
+
seed = null, // if provided, ensures consistent moon positions
|
|
18
|
+
// NEW: specific texture key/path
|
|
19
|
+
textureKey = null, // if provided, overrides textureKTX2/texturePNG
|
|
20
|
+
moonColor = '#ffffff', // NEW: moon color
|
|
21
|
+
// lighting tweaks
|
|
22
|
+
brightness = 1.15,
|
|
23
|
+
emissiveColor = 0x333333,
|
|
24
|
+
emissiveIntensity = 0.18,
|
|
25
|
+
roughness = 0.85,
|
|
26
|
+
metalness = 0.0,
|
|
27
|
+
// debug & appearance controls
|
|
28
|
+
debug = true,
|
|
29
|
+
smooth = true, // set false to keep flatShading
|
|
30
|
+
// debug / appearance params
|
|
31
|
+
geometryWidthSegments = 16, // Reduced from 24 for better performance
|
|
32
|
+
geometryHeightSegments = 12, // Reduced from 24 for better performance
|
|
33
|
+
useBumpFromColor = true,
|
|
34
|
+
bumpScale = 0.15,
|
|
35
|
+
debugShowUvGrid = false,
|
|
36
|
+
debugShowReferenceMoon = false, // changed from true to prevent extra moon
|
|
37
|
+
referenceMoonScale = 0.25,
|
|
38
|
+
// behavior flags
|
|
39
|
+
preserveInstanceTint = false, // keep per-instance color even when textured
|
|
40
|
+
useEmissiveMapFromTexture = true, // reuse color texture as emissiveMap to lift dark sides
|
|
41
|
+
} = {}) {
|
|
42
|
+
if (count <= 0) return null;
|
|
43
|
+
|
|
44
|
+
// Seeded random number generator for deterministic positioning
|
|
45
|
+
let seedValue = seed;
|
|
46
|
+
if (typeof seed === 'string') {
|
|
47
|
+
// Convert string seed to number using simple hash
|
|
48
|
+
seedValue = 0;
|
|
49
|
+
for (let i = 0; i < seed.length; i++) {
|
|
50
|
+
const char = seed.charCodeAt(i);
|
|
51
|
+
seedValue = ((seedValue << 5) - seedValue) + char;
|
|
52
|
+
seedValue = seedValue & seedValue; // Convert to 32-bit integer
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Seeded random function (Linear Congruential Generator)
|
|
57
|
+
let currentSeed = seedValue !== null ? Math.abs(seedValue) : Date.now();
|
|
58
|
+
const seededRandom = () => {
|
|
59
|
+
if (seedValue !== null) {
|
|
60
|
+
currentSeed = (currentSeed * 1664525 + 1013904223) % 4294967296;
|
|
61
|
+
return currentSeed / 4294967296;
|
|
62
|
+
}
|
|
63
|
+
return Math.random(); // Use regular random if no seed
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Log the seed being used for debugging
|
|
67
|
+
if (debug) {
|
|
68
|
+
console.log('[Moons] Using seed for deterministic positioning:', {
|
|
69
|
+
originalSeed: seed,
|
|
70
|
+
seedValue: seedValue,
|
|
71
|
+
seedType: typeof seed,
|
|
72
|
+
isSeeded: seedValue !== null
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const moonGeometry = new THREE.SphereGeometry(0.1, geometryWidthSegments, geometryHeightSegments);
|
|
77
|
+
|
|
78
|
+
// Optional UV grid override
|
|
79
|
+
let sharedTexture = null;
|
|
80
|
+
let usedUvGrid = false;
|
|
81
|
+
async function buildUvGridTexture() {
|
|
82
|
+
const w = 512, h = 256; // 2:1 aspect
|
|
83
|
+
const canvas = document.createElement('canvas');
|
|
84
|
+
canvas.width = w; canvas.height = h;
|
|
85
|
+
const ctx = canvas.getContext('2d');
|
|
86
|
+
ctx.fillStyle = '#101018'; ctx.fillRect(0,0,w,h);
|
|
87
|
+
for (let lon=0; lon<=12; lon++) {
|
|
88
|
+
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();
|
|
89
|
+
}
|
|
90
|
+
for (let lat=0; lat<=6; lat++) {
|
|
91
|
+
const y = (lat/6)*h; ctx.strokeStyle = lat % 3 ===0 ? '#4dff7a':'#444'; ctx.lineWidth = lat%3===0?2:1; ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(w,y); ctx.stroke();
|
|
92
|
+
}
|
|
93
|
+
ctx.fillStyle = 'white'; ctx.font = '20px monospace'; ctx.fillText('UV GRID', 10, 24);
|
|
94
|
+
const tex = new THREE.CanvasTexture(canvas);
|
|
95
|
+
tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 4; return tex;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (debugShowUvGrid) {
|
|
99
|
+
try { sharedTexture = await buildUvGridTexture(); usedUvGrid = true; } catch(_) {}
|
|
100
|
+
} else if (useTexture) {
|
|
101
|
+
// Use textureKey if provided, otherwise fall back to default textures
|
|
102
|
+
let primaryTexture = textureKey || textureKTX2;
|
|
103
|
+
const fallbackTexture = texturePNG;
|
|
104
|
+
|
|
105
|
+
// Resolve textureKey to absolute URL if it's relative
|
|
106
|
+
if (textureKey && textureKey.startsWith('/')) {
|
|
107
|
+
primaryTexture = `${HERBARIUM_BASE}${textureKey}`;
|
|
108
|
+
if (debug) console.log('[Moons] Using custom texture key:', textureKey, '-> resolved:', primaryTexture);
|
|
109
|
+
} else if (textureKey) {
|
|
110
|
+
primaryTexture = textureKey; // already absolute
|
|
111
|
+
if (debug) console.log('[Moons] Using absolute texture URL:', primaryTexture);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
sharedTexture = await loadTexture(primaryTexture);
|
|
116
|
+
if (debug) console.log('[Moons] Loaded texture from:', primaryTexture);
|
|
117
|
+
} catch (eKTX) {
|
|
118
|
+
if (debug) console.warn('[Moons] Primary texture failed, trying fallback:', eKTX);
|
|
119
|
+
try {
|
|
120
|
+
sharedTexture = await loadTexture(fallbackTexture);
|
|
121
|
+
if (debug) console.log('[Moons] Fallback texture loaded from:', fallbackTexture);
|
|
122
|
+
} catch (ePNG) {
|
|
123
|
+
if (debug) console.warn('[Moons] Both primary and fallback texture loading failed');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (sharedTexture) {
|
|
129
|
+
try {
|
|
130
|
+
sharedTexture.colorSpace = THREE.SRGBColorSpace;
|
|
131
|
+
sharedTexture.anisotropy = 2; // Reduced from 4 for better performance
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (debug) console.warn('[Moons] Texture processing failed:', e);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const enableVertexColors = !sharedTexture || preserveInstanceTint;
|
|
138
|
+
const moonMaterial = new THREE.MeshStandardMaterial({
|
|
139
|
+
map: sharedTexture || null,
|
|
140
|
+
vertexColors: enableVertexColors,
|
|
141
|
+
roughness,
|
|
142
|
+
metalness,
|
|
143
|
+
emissive: new THREE.Color(emissiveColor),
|
|
144
|
+
emissiveIntensity,
|
|
145
|
+
flatShading: !smooth,
|
|
146
|
+
transparent: false,
|
|
147
|
+
depthWrite: true,
|
|
148
|
+
depthTest: true,
|
|
149
|
+
});
|
|
150
|
+
if (sharedTexture && useEmissiveMapFromTexture) {
|
|
151
|
+
moonMaterial.emissiveMap = sharedTexture; // boosts subtle texture detail
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!sharedTexture && debug && !debugShowUvGrid) {
|
|
155
|
+
console.warn('[Moons] No texture assigned (using pure color instancing).');
|
|
156
|
+
}
|
|
157
|
+
if (sharedTexture) {
|
|
158
|
+
moonMaterial.map = sharedTexture;
|
|
159
|
+
if (useBumpFromColor) {
|
|
160
|
+
moonMaterial.bumpMap = sharedTexture;
|
|
161
|
+
moonMaterial.bumpScale = bumpScale;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Pre-computed color palette for variation (white to dark gray)
|
|
166
|
+
const colorPalette = [
|
|
167
|
+
0xffffff, 0xf5f5f5, 0xeeeeee, 0xe8e8e8, 0xdddddd, 0xd3d3d3,
|
|
168
|
+
0xcccccc, 0xc0c0c0, 0xb8b8b8, 0xaaaaaa, 0x999999, 0x888888
|
|
169
|
+
].map(hex => new THREE.Color(hex));
|
|
170
|
+
|
|
171
|
+
const instanced = new THREE.InstancedMesh(moonGeometry, moonMaterial, count + (debugShowReferenceMoon ? 1 : 0));
|
|
172
|
+
|
|
173
|
+
// Set render order to ensure proper rendering above glow effects
|
|
174
|
+
instanced.renderOrder = 2000;
|
|
175
|
+
|
|
176
|
+
const dummy = new THREE.Object3D();
|
|
177
|
+
|
|
178
|
+
let instanceIndex = 0;
|
|
179
|
+
for (let i = 0; i < count; i++) {
|
|
180
|
+
const angle = (i / Math.max(1, count)) * Math.PI * 2;
|
|
181
|
+
const radiusVar = orbitRadius + (seededRandom() - 0.5) * 0.22;
|
|
182
|
+
const yOffset = (seededRandom() - 0.5) * 0.12;
|
|
183
|
+
dummy.position.set(radiusVar * Math.cos(angle), yOffset, radiusVar * Math.sin(angle));
|
|
184
|
+
const s = 0.07 + seededRandom() * 0.15;
|
|
185
|
+
dummy.scale.set(s, s, s);
|
|
186
|
+
dummy.rotation.y = seededRandom() * Math.PI * 2;
|
|
187
|
+
dummy.rotation.x = seededRandom() * 0.3 - 0.15;
|
|
188
|
+
dummy.updateMatrix();
|
|
189
|
+
instanced.setMatrixAt(instanceIndex, dummy.matrix);
|
|
190
|
+
|
|
191
|
+
// Always set instance color as tint, even if texture is present
|
|
192
|
+
const base = new THREE.Color(moonColor || '#ffffff');
|
|
193
|
+
instanced.setColorAt(instanceIndex, base);
|
|
194
|
+
|
|
195
|
+
instanceIndex++;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (debugShowReferenceMoon) {
|
|
199
|
+
dummy.position.set(orbitRadius * 0.6, 0.05, orbitRadius * 0.2);
|
|
200
|
+
dummy.scale.set(referenceMoonScale, referenceMoonScale, referenceMoonScale);
|
|
201
|
+
dummy.rotation.y = seededRandom() * Math.PI * 2;
|
|
202
|
+
dummy.rotation.x = seededRandom() * 0.3 - 0.15;
|
|
203
|
+
dummy.updateMatrix();
|
|
204
|
+
instanced.setMatrixAt(instanceIndex, dummy.matrix);
|
|
205
|
+
const refColor = new THREE.Color('#ffffff');
|
|
206
|
+
instanced.setColorAt(instanceIndex, refColor);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
instanced.instanceMatrix.needsUpdate = true;
|
|
210
|
+
if (instanced.instanceColor) instanced.instanceColor.needsUpdate = true;
|
|
211
|
+
|
|
212
|
+
const group = new THREE.Group();
|
|
213
|
+
group.rotation.x = THREE.MathUtils.degToRad(15);
|
|
214
|
+
group.add(instanced);
|
|
215
|
+
group.name = 'moonsGroup';
|
|
216
|
+
|
|
217
|
+
// Add disposal method for cleanup
|
|
218
|
+
group.dispose = () => {
|
|
219
|
+
if (moonGeometry) moonGeometry.dispose();
|
|
220
|
+
if (moonMaterial.map && moonMaterial.map !== sharedTexture) moonMaterial.map.dispose();
|
|
221
|
+
if (moonMaterial.emissiveMap && moonMaterial.emissiveMap !== sharedTexture) moonMaterial.emissiveMap.dispose();
|
|
222
|
+
if (moonMaterial.bumpMap && moonMaterial.bumpMap !== sharedTexture) moonMaterial.bumpMap.dispose();
|
|
223
|
+
moonMaterial.dispose();
|
|
224
|
+
instanced.dispose();
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return group;
|
|
228
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { disposeObject3D } from '../utils/dispose.js';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import { generateMoons } from './moonsGeneration.js';
|
|
4
|
+
|
|
5
|
+
export class MoonsManager {
|
|
6
|
+
constructor(scene) {
|
|
7
|
+
this.scene = scene;
|
|
8
|
+
this.group = null;
|
|
9
|
+
this._opId = 0; // concurrency guard
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async setAmount(count, opts = {}) {
|
|
13
|
+
// Increment operation id to invalidate older async calls
|
|
14
|
+
const opId = ++this._opId;
|
|
15
|
+
|
|
16
|
+
// remove existing
|
|
17
|
+
if (this.group) {
|
|
18
|
+
disposeObject3D(this.group);
|
|
19
|
+
this.group = null;
|
|
20
|
+
}
|
|
21
|
+
if (!count || count <= 0) return;
|
|
22
|
+
|
|
23
|
+
// Gather options for texture sharing / orbit params
|
|
24
|
+
const {
|
|
25
|
+
orbitRadius = 1.5,
|
|
26
|
+
textureKTX2,
|
|
27
|
+
texturePNG,
|
|
28
|
+
useTexture = true,
|
|
29
|
+
seed = null, // seed for deterministic positioning
|
|
30
|
+
textureKey = null, // NEW: specific texture key/path to use
|
|
31
|
+
moonColor = '#ffffff', // NEW: moon color
|
|
32
|
+
} = opts;
|
|
33
|
+
|
|
34
|
+
let newGroup = null;
|
|
35
|
+
try {
|
|
36
|
+
newGroup = await generateMoons({
|
|
37
|
+
count,
|
|
38
|
+
orbitRadius,
|
|
39
|
+
textureKTX2,
|
|
40
|
+
texturePNG,
|
|
41
|
+
useTexture,
|
|
42
|
+
seed,
|
|
43
|
+
textureKey, // Pass texture key for custom moon textures
|
|
44
|
+
moonColor, // Pass moon color to generator
|
|
45
|
+
});
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.warn('[MoonsManager] Failed to generate moons:', e);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If another call started meanwhile, abandon this result
|
|
52
|
+
if (opId !== this._opId) {
|
|
53
|
+
if (newGroup) disposeObject3D(newGroup);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (newGroup instanceof THREE.Object3D) {
|
|
58
|
+
this.group = newGroup;
|
|
59
|
+
this.scene.add(this.group);
|
|
60
|
+
} else if (newGroup) {
|
|
61
|
+
console.warn('[MoonsManager] generateMoons did not return an Object3D');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function disposeMaterial(material) {
|
|
2
|
+
if (!material) return;
|
|
3
|
+
for (const key in material) {
|
|
4
|
+
const val = material[key];
|
|
5
|
+
if (val && typeof val.dispose === 'function') {
|
|
6
|
+
try { val.dispose(); } catch {}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function disposeObject3D(obj) {
|
|
12
|
+
if (!obj) return;
|
|
13
|
+
obj.traverse((child) => {
|
|
14
|
+
if (child.isMesh) {
|
|
15
|
+
if (child.geometry) child.geometry.dispose();
|
|
16
|
+
disposeMaterial(child.material);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
if (obj.parent) obj.parent.remove(obj);
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
export function ensureStandardMaterial(mesh) {
|
|
4
|
+
if (!mesh || !mesh.isMesh) return;
|
|
5
|
+
const mat = mesh.material;
|
|
6
|
+
if (mat && !(mat instanceof THREE.MeshStandardMaterial)) {
|
|
7
|
+
mesh.material = new THREE.MeshStandardMaterial({
|
|
8
|
+
map: mat.map || null,
|
|
9
|
+
metalness: 0.3,
|
|
10
|
+
roughness: 0.7,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function applyFlagsToMaterial(material, flags = {}) {
|
|
16
|
+
if (!material) return;
|
|
17
|
+
const { forceOpaque, wireframe } = flags;
|
|
18
|
+
if (forceOpaque) {
|
|
19
|
+
if (material.transparent || (material.transmission && material.transmission > 0)) {
|
|
20
|
+
material.transparent = false;
|
|
21
|
+
material.opacity = 1;
|
|
22
|
+
if ('transmission' in material) material.transmission = 0;
|
|
23
|
+
material.alphaTest = 0;
|
|
24
|
+
material.needsUpdate = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (wireframe) {
|
|
28
|
+
material.wireframe = true;
|
|
29
|
+
material.needsUpdate = true;
|
|
30
|
+
}
|
|
31
|
+
}
|