Compare commits

...

2 Commits

Author SHA1 Message Date
mula.liu 638561d86d 修复土星星环 2026-01-02 02:44:13 +08:00
mula.liu 50de9835ef 修复土星星环 2026-01-02 02:41:51 +08:00
6 changed files with 204 additions and 106 deletions

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
# OS files
.DS_Store
Thumbs.db
.gemini-clipboard/
# Environments
.env*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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<Mesh>(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 (
<mesh
ref={meshRef}
rotation={[-Math.PI / 2, 0, 0]}
geometry={geometry}
receiveShadow
castShadow
>
<meshStandardMaterial
map={texture}
transparent
opacity={0.9}
side={DoubleSide}
color="#ffffff"
roughness={0.8}
/>
</mesh>
);
}
return null;
}
export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) {
const meshRef = useRef<Mesh>(null);
const [texturePath, setTexturePath] = useState<string | null | undefined>(undefined);
const [ringTexturePath, setRingTexturePath] = useState<string | null | undefined>(undefined);
const [modelPath, setModelPath] = useState<string | null | undefined>(undefined);
const [modelScale, setModelScale] = useState<number>(1.0);
const [loadError, setLoadError] = useState<boolean>(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<Mesh>;
disableGlow?: boolean;
}) {
@ -331,39 +429,6 @@ function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePat
);
}
// Saturn Rings component - multiple rings for band effect
function SaturnRings() {
return (
<group rotation={[Math.PI / 2, 0, 0]}>
{/* Inner bright ring */}
<mesh>
<ringGeometry args={[1.4, 1.6, 32]} />
<meshBasicMaterial color="#D4B896" transparent opacity={0.7} side={THREE.DoubleSide} />
</mesh>
{/* Middle darker band */}
<mesh>
<ringGeometry args={[1.6, 1.75, 32]} />
<meshBasicMaterial color="#8B7355" transparent opacity={0.5} side={THREE.DoubleSide} />
</mesh>
{/* Outer bright ring */}
<mesh>
<ringGeometry args={[1.75, 2.0, 32]} />
<meshBasicMaterial color="#C4A582" transparent opacity={0.6} side={THREE.DoubleSide} />
</mesh>
{/* Cassini Division (gap) */}
<mesh>
<ringGeometry args={[2.0, 2.05, 32]} />
<meshBasicMaterial color="#000000" transparent opacity={0.2} side={THREE.DoubleSide} />
</mesh>
{/* A Ring (outer) */}
<mesh>
<ringGeometry args={[2.05, 2.2, 32]} />
<meshBasicMaterial color="#B89968" transparent opacity={0.5} side={THREE.DoubleSide} />
</mesh>
</group>
);
}
return (
<group>
{/* Use irregular nucleus for comets, regular sphere for others */}
@ -395,8 +460,10 @@ function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePat
</mesh>
)}
{/* Saturn Rings */}
{body.id === '699' && <SaturnRings />}
{/* Planetary Rings: Render ONLY if texture exists */}
{ringTexturePath && (
<PlanetaryRings texturePath={ringTexturePath} planetRadius={size} />
)}
{/* Sun glow effect - multi-layer scattered light (仅在非禁用光晕模式下显示) */}
{body.type === 'star' && !disableGlow && (

View File

@ -19,62 +19,70 @@ interface CelestialBodyProps {
onBodySelect?: (body: CelestialBodyType) => void;
}
// Saturn Rings component - multiple rings for band effect
function SaturnRings() {
return (
<group rotation={[Math.PI / 2, 0, 0]}>
{/* Inner bright ring */}
<mesh>
<ringGeometry args={[1.4, 1.6, 32]} />
<meshBasicMaterial
color="#D4B896"
// 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<Mesh>(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 (
<mesh
ref={meshRef}
rotation={[-Math.PI / 2, 0, 0]}
geometry={geometry}
receiveShadow
castShadow
>
<meshStandardMaterial
map={texture}
transparent
opacity={0.7}
opacity={0.9}
side={DoubleSide}
color="#ffffff"
roughness={0.8}
/>
</mesh>
{/* Middle darker band */}
<mesh>
<ringGeometry args={[1.6, 1.75, 32]} />
<meshBasicMaterial
color="#8B7355"
transparent
opacity={0.5}
side={DoubleSide}
/>
</mesh>
{/* Outer bright ring */}
<mesh>
<ringGeometry args={[1.75, 2.0, 32]} />
<meshBasicMaterial
color="#C4A582"
transparent
opacity={0.6}
side={DoubleSide}
/>
</mesh>
{/* Cassini Division (gap) */}
<mesh>
<ringGeometry args={[2.0, 2.05, 32]} />
<meshBasicMaterial
color="#000000"
transparent
opacity={0.2}
side={DoubleSide}
/>
</mesh>
{/* A Ring (outer) */}
<mesh>
<ringGeometry args={[2.05, 2.2, 32]} />
<meshBasicMaterial
color="#B89968"
transparent
opacity={0.5}
side={DoubleSide}
/>
</mesh>
</group>
);
);
}
return null;
}
// Planet component with texture
@ -90,6 +98,7 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected
const meshRef = useRef<Mesh>(null);
const position = body.positions[0];
const [texturePath, setTexturePath] = useState<string | null | undefined>(undefined);
const [ringTexturePath, setRingTexturePath] = useState<string | null | undefined>(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<Mesh>;
hasOffset: boolean;
@ -376,8 +404,10 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur
</mesh>
)}
{/* Saturn Rings */}
{body.id === '699' && <SaturnRings />}
{/* Planetary Rings: Render ONLY if texture exists (Standardized) */}
{ringTexturePath && (
<PlanetaryRings texturePath={ringTexturePath} planetRadius={size} />
)}
{/* Sun glow effect - multi-layer scattered light */}
{body.type === 'star' && (