diff --git a/.DS_Store b/.DS_Store index a0fca42..36cef48 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gemini-clipboard/clipboard-1767292132587.png b/.gemini-clipboard/clipboard-1767292132587.png new file mode 100644 index 0000000..fca186f Binary files /dev/null and b/.gemini-clipboard/clipboard-1767292132587.png differ diff --git a/.gemini-clipboard/clipboard-1767292246532.png b/.gemini-clipboard/clipboard-1767292246532.png new file mode 100644 index 0000000..718f5b8 Binary files /dev/null and b/.gemini-clipboard/clipboard-1767292246532.png differ diff --git a/.gemini-clipboard/clipboard-1767292311983.png b/.gemini-clipboard/clipboard-1767292311983.png new file mode 100644 index 0000000..3f5d7ee Binary files /dev/null and b/.gemini-clipboard/clipboard-1767292311983.png differ diff --git a/.gemini-clipboard/clipboard-1767292763950.png b/.gemini-clipboard/clipboard-1767292763950.png new file mode 100644 index 0000000..5939e8e Binary files /dev/null and b/.gemini-clipboard/clipboard-1767292763950.png differ diff --git a/backend/upload/texture/2k_saturn_ring.jpg b/backend/upload/texture/2k_saturn_ring.jpg index cc9de41..ce82586 100644 Binary files a/backend/upload/texture/2k_saturn_ring.jpg and b/backend/upload/texture/2k_saturn_ring.jpg differ diff --git a/backend/upload/texture/2k_saturn_ring_alpha.png b/backend/upload/texture/2k_saturn_ring_alpha.png deleted file mode 100644 index ce82586..0000000 Binary files a/backend/upload/texture/2k_saturn_ring_alpha.png and /dev/null differ diff --git a/frontend/src/components/BodyViewer.tsx b/frontend/src/components/BodyViewer.tsx index 4afb147..1c1d417 100644 --- a/frontend/src/components/BodyViewer.tsx +++ b/frontend/src/components/BodyViewer.tsx @@ -1,5 +1,5 @@ import { useRef, useMemo, useState, useEffect, Suspense } from 'react'; -import { Mesh } from 'three'; +import { Mesh, Group, DoubleSide } from 'three'; import * as THREE from 'three'; import { useGLTF, useTexture } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; @@ -12,10 +12,75 @@ interface BodyViewerProps { disableGlow?: boolean; // 禁用光晕效果(用于详情视图) } -// Reusable component to render just the 3D model/mesh of a celestial body +// Planetary Rings component - handles texture-based rings +function PlanetaryRings({ texturePath, planetRadius }: { texturePath?: string | null, planetRadius: number }) { + const texture = texturePath ? useTexture(texturePath) : null; + const meshRef = useRef(null); + + // Dynamic ring dimensions based on planet size + // Standard Saturn proportions: Inner ~1.1 R, Outer ~2.3 R + const innerRadius = planetRadius * 1.05; + const outerRadius = planetRadius * 2.2; + + // Custom geometry with Polar UV mapping for strip textures + const geometry = useMemo(() => { + if (!texturePath) return null; + + const geo = new THREE.RingGeometry(innerRadius, outerRadius, 64); + const pos = geo.attributes.position; + const uv = geo.attributes.uv; + + // Manually remap UVs to be polar: + // We map normalized radius to BOTH U and V coordinates. + // This allows the texture to work whether it's a horizontal strip OR a vertical strip. + // - If horizontal strip: U varies with radius (correct), V varies (doesn't matter as strip is uniform vertically) + // - If vertical strip: V varies with radius (correct), U varies (doesn't matter as strip is uniform horizontally) + for (let i = 0; i < pos.count; i++) { + const x = pos.getX(i); + const y = pos.getY(i); + + // Calculate radius from center + const r = Math.sqrt(x * x + y * y); + + // Normalize radius: 0 at inner, 1 at outer + // Clamp to ensure floating point errors don't break texture wrapping + const normalizedR = Math.max(0, Math.min(1, (r - innerRadius) / (outerRadius - innerRadius))); + + // Map to UV (same value for both axes to support H/V strips) + uv.setXY(i, normalizedR, normalizedR); + } + + geo.attributes.uv.needsUpdate = true; + return geo; + }, [texturePath, innerRadius, outerRadius]); + + if (texture && geometry) { + return ( + + + + ); + } + + return null; +} export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) { const meshRef = useRef(null); const [texturePath, setTexturePath] = useState(undefined); + const [ringTexturePath, setRingTexturePath] = useState(undefined); const [modelPath, setModelPath] = useState(undefined); const [modelScale, setModelScale] = useState(1.0); const [loadError, setLoadError] = useState(false); @@ -62,31 +127,62 @@ export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) { setLoadError(false); setModelPath(undefined); setTexturePath(undefined); + setRingTexturePath(undefined); const loadResources = async () => { try { const response = await fetchBodyResources(body.id, body.type === 'probe' ? 'model' : 'texture'); if (response.resources.length > 0) { - const mainResource = response.resources[0]; - const fullPath = `/upload/${mainResource.file_path}`; - + if (body.type === 'probe') { - useGLTF.preload(fullPath); // Preload GLTF - setModelPath(fullPath); - setModelScale(mainResource.extra_data?.scale || 1.0); + const mainResource = response.resources[0]; + const fullPath = `/upload/${mainResource.file_path}`; + useGLTF.preload(fullPath); // Preload GLTF + setModelPath(fullPath); + setModelScale(mainResource.extra_data?.scale || 1.0); } else { - setTexturePath(fullPath); + // Find main texture + const mainTexture = response.resources.find( + (r) => !r.file_path.includes('atmosphere') && + !r.file_path.includes('night') && + !r.file_path.includes('_ring') + ); + + // Find ring texture + const bodyNameLower = body.name.toLowerCase(); + const ringTexture = response.resources.find( + (r) => r.file_path.toLowerCase().includes(`${bodyNameLower}_ring`) || + (r.file_path.toLowerCase().includes('ring') && r.file_path.toLowerCase().includes(bodyNameLower)) + ); + + if (mainTexture) { + setTexturePath(`/upload/${mainTexture.file_path}`); + } else { + setTexturePath(null); + } + + if (ringTexture) { + setRingTexturePath(`/upload/${ringTexture.file_path}`); + } else { + setRingTexturePath(null); + } } } else { // No resources found if (body.type === 'probe') setModelPath(null); - else setTexturePath(null); + else { + setTexturePath(null); + setRingTexturePath(null); + } } } catch (err) { console.error(`Failed to load resource for ${body.name}:`, err); setLoadError(true); if (body.type === 'probe') setModelPath(null); - else setTexturePath(null); + else { + setTexturePath(null); + setRingTexturePath(null); + } } }; loadResources(); @@ -115,6 +211,7 @@ export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) { emissive={appearance.emissive} emissiveIntensity={appearance.emissiveIntensity} texturePath={texturePath} + ringTexturePath={ringTexturePath} meshRef={meshRef} disableGlow={disableGlow} /> @@ -179,12 +276,13 @@ function ProbeModelViewer({ modelPath, modelScale }: { modelPath: string; modelS } // Sub-component for Planet models -function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePath, meshRef, disableGlow = false }: { +function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePath, ringTexturePath, meshRef, disableGlow = false }: { body: CelestialBodyType; size: number; emissive: string; emissiveIntensity: number; texturePath: string | null; + ringTexturePath?: string | null; meshRef: React.RefObject; disableGlow?: boolean; }) { @@ -331,39 +429,6 @@ function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePat ); } - // Saturn Rings component - multiple rings for band effect - function SaturnRings() { - return ( - - {/* Inner bright ring */} - - - - - {/* Middle darker band */} - - - - - {/* Outer bright ring */} - - - - - {/* Cassini Division (gap) */} - - - - - {/* A Ring (outer) */} - - - - - - ); - } - return ( {/* Use irregular nucleus for comets, regular sphere for others */} @@ -395,8 +460,10 @@ function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePat )} - {/* Saturn Rings */} - {body.id === '699' && } + {/* Planetary Rings: Render ONLY if texture exists */} + {ringTexturePath && ( + + )} {/* Sun glow effect - multi-layer scattered light (仅在非禁用光晕模式下显示) */} {body.type === 'star' && !disableGlow && ( diff --git a/frontend/src/components/CelestialBody.tsx b/frontend/src/components/CelestialBody.tsx index 455f06b..8400021 100644 --- a/frontend/src/components/CelestialBody.tsx +++ b/frontend/src/components/CelestialBody.tsx @@ -19,62 +19,70 @@ interface CelestialBodyProps { onBodySelect?: (body: CelestialBodyType) => void; } -// Saturn Rings component - multiple rings for band effect -function SaturnRings() { - return ( - - {/* Inner bright ring */} - - - (null); + + // Dynamic ring dimensions based on planet size + // Standard Saturn proportions: Inner ~1.1 R, Outer ~2.3 R + const innerRadius = planetRadius * 1.05; + const outerRadius = planetRadius * 2.2; + + // Custom geometry with Polar UV mapping for strip textures + const geometry = useMemo(() => { + if (!texturePath) return null; + + const geo = new THREE.RingGeometry(innerRadius, outerRadius, 64); + const pos = geo.attributes.position; + const uv = geo.attributes.uv; + + // Manually remap UVs to be polar: + // We map normalized radius to BOTH U and V coordinates. + // This allows the texture to work whether it's a horizontal strip OR a vertical strip. + // - If horizontal strip: U varies with radius (correct), V varies (doesn't matter as strip is uniform vertically) + // - If vertical strip: V varies with radius (correct), U varies (doesn't matter as strip is uniform horizontally) + for (let i = 0; i < pos.count; i++) { + const x = pos.getX(i); + const y = pos.getY(i); + + // Calculate radius from center + const r = Math.sqrt(x * x + y * y); + + // Normalize radius: 0 at inner, 1 at outer + // Clamp to ensure floating point errors don't break texture wrapping + const normalizedR = Math.max(0, Math.min(1, (r - innerRadius) / (outerRadius - innerRadius))); + + // Map to UV (same value for both axes to support H/V strips) + uv.setXY(i, normalizedR, normalizedR); + } + + geo.attributes.uv.needsUpdate = true; + return geo; + }, [texturePath, innerRadius, outerRadius]); + + if (texture && geometry) { + return ( + + - {/* Middle darker band */} - - - - - {/* Outer bright ring */} - - - - - {/* Cassini Division (gap) */} - - - - - {/* A Ring (outer) */} - - - - - - ); + ); + } + + return null; } // Planet component with texture @@ -90,6 +98,7 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected const meshRef = useRef(null); const position = body.positions[0]; const [texturePath, setTexturePath] = useState(undefined); + const [ringTexturePath, setRingTexturePath] = useState(undefined); // Use smart render position calculation const renderPosition = useMemo(() => { @@ -102,23 +111,40 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected useEffect(() => { fetchBodyResources(body.id, 'texture') .then((response) => { - // Find the main texture (not atmosphere or night layers) + // 1. Find the main texture (body surface) + // Exclude atmosphere, night, and ring textures from main body texture const mainTexture = response.resources.find( - (r) => !r.file_path.includes('atmosphere') && !r.file_path.includes('night') + (r) => !r.file_path.includes('atmosphere') && + !r.file_path.includes('night') && + !r.file_path.includes('_ring') ); + + // 2. Find the ring texture + // Convention: filename contains body name + "_ring" (e.g. "saturn_ring") + const bodyNameLower = body.name.toLowerCase(); + const ringTexture = response.resources.find( + (r) => r.file_path.toLowerCase().includes(`${bodyNameLower}_ring`) || + (r.file_path.toLowerCase().includes('ring') && r.file_path.toLowerCase().includes(bodyNameLower)) + ); + if (mainTexture) { - // Construct path for Nginx proxy - // file_path is like "texture/2k_sun.jpg", need to add "upload/" prefix setTexturePath(`/upload/${mainTexture.file_path}`); } else { setTexturePath(null); } + + if (ringTexture) { + setRingTexturePath(`/upload/${ringTexture.file_path}`); + } else { + setRingTexturePath(null); + } }) .catch((err) => { if (import.meta.env.DEV) { console.error(`Failed to load texture for ${body.name}:`, err); } setTexturePath(null); + setRingTexturePath(null); }); }, [body.id, body.name]); @@ -134,6 +160,7 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected emissiveIntensity={emissiveIntensity} scaledPos={scaledPos} texturePath={texturePath} + ringTexturePath={ringTexturePath} position={position} meshRef={meshRef} hasOffset={renderPosition.hasOffset} @@ -284,13 +311,14 @@ function CometComa({ radius }: { radius: number }) { } // Separate component to handle texture loading -function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, texturePath, position, meshRef, hasOffset, allBodies, isSelected = false, onBodySelect }: { +function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, texturePath, ringTexturePath, position, meshRef, hasOffset, allBodies, isSelected = false, onBodySelect }: { body: CelestialBodyType; size: number; emissive: string; emissiveIntensity: number; scaledPos: { x: number; y: number; z: number }; texturePath: string | null; + ringTexturePath?: string | null; position: { x: number; y: number; z: number }; meshRef: React.RefObject; hasOffset: boolean; @@ -376,8 +404,10 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur )} - {/* Saturn Rings */} - {body.id === '699' && } + {/* Planetary Rings: Render ONLY if texture exists (Standardized) */} + {ringTexturePath && ( + + )} {/* Sun glow effect - multi-layer scattered light */} {body.type === 'star' && (