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.
Files changed (38) hide show
  1. package/README.md +75 -0
  2. package/package.json +49 -0
  3. package/public/config/textures.json +332 -0
  4. package/src/arrow-ui/BooleanController.js +31 -0
  5. package/src/arrow-ui/ColorController.js +108 -0
  6. package/src/arrow-ui/Controller.js +447 -0
  7. package/src/arrow-ui/FunctionController.js +27 -0
  8. package/src/arrow-ui/GUI.js +595 -0
  9. package/src/arrow-ui/NumberController.js +567 -0
  10. package/src/arrow-ui/OptionController.js +64 -0
  11. package/src/arrow-ui/StringController.js +41 -0
  12. package/src/arrow-ui/index.js +13 -0
  13. package/src/arrow-ui/lilGuiLoader.js +10 -0
  14. package/src/arrow-ui/utils/getColorFormat.js +75 -0
  15. package/src/arrow-ui/utils/injectStyles.js +10 -0
  16. package/src/arrow-ui/utils/normalizeColorString.js +29 -0
  17. package/src/config/index.js +8 -0
  18. package/src/config/textureConfig.js +299 -0
  19. package/src/core/index.js +3 -0
  20. package/src/core/lightFresnel.js +64 -0
  21. package/src/core/lights.js +43 -0
  22. package/src/index.js +8 -0
  23. package/src/loaders/bodyTextureLoader.js +39 -0
  24. package/src/loaders/cubeMapLoader.js +264 -0
  25. package/src/loaders/glbLoader.js +71 -0
  26. package/src/loaders/index.js +4 -0
  27. package/src/moons/index.js +3 -0
  28. package/src/moons/moonsGeneration.js +228 -0
  29. package/src/moons/moonsManager.js +64 -0
  30. package/src/utils/dispose.js +20 -0
  31. package/src/utils/index.js +3 -0
  32. package/src/utils/materials.js +31 -0
  33. package/src/world/cloudShell.js +89 -0
  34. package/src/world/index.js +6 -0
  35. package/src/world/worldGLB.js +7 -0
  36. package/src/world/worldManager.js +93 -0
  37. package/src/world/worldNormal.js +25 -0
  38. 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,4 @@
1
+ // Loaders exports
2
+ export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
3
+ export { loadGLB } from './glbLoader.js';
4
+ export { loadTexture } from './bodyTextureLoader.js';
@@ -0,0 +1,3 @@
1
+ // Moons system exports
2
+ export { MoonsManager } from './moonsManager.js';
3
+ export { generateMoons, DEFAULT_MOON_TEX_KTX2, DEFAULT_MOON_TEX_PNG } from './moonsGeneration.js';
@@ -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,3 @@
1
+ // Utilities exports
2
+ export { ensureStandardMaterial, applyFlagsToMaterial } from './materials.js';
3
+ export { disposeObject3D } from './dispose.js';
@@ -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
+ }