修复土星星环
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 606 KiB |
|
After Width: | Height: | Size: 809 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
|
@ -1,5 +1,5 @@
|
||||||
import { useRef, useMemo, useState, useEffect, Suspense } from 'react';
|
import { useRef, useMemo, useState, useEffect, Suspense } from 'react';
|
||||||
import { Mesh } from 'three';
|
import { Mesh, Group, DoubleSide } from 'three';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useGLTF, useTexture } from '@react-three/drei';
|
import { useGLTF, useTexture } from '@react-three/drei';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
|
@ -12,10 +12,75 @@ interface BodyViewerProps {
|
||||||
disableGlow?: boolean; // 禁用光晕效果(用于详情视图)
|
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) {
|
export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) {
|
||||||
const meshRef = useRef<Mesh>(null);
|
const meshRef = useRef<Mesh>(null);
|
||||||
const [texturePath, setTexturePath] = useState<string | null | undefined>(undefined);
|
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 [modelPath, setModelPath] = useState<string | null | undefined>(undefined);
|
||||||
const [modelScale, setModelScale] = useState<number>(1.0);
|
const [modelScale, setModelScale] = useState<number>(1.0);
|
||||||
const [loadError, setLoadError] = useState<boolean>(false);
|
const [loadError, setLoadError] = useState<boolean>(false);
|
||||||
|
|
@ -62,31 +127,62 @@ export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) {
|
||||||
setLoadError(false);
|
setLoadError(false);
|
||||||
setModelPath(undefined);
|
setModelPath(undefined);
|
||||||
setTexturePath(undefined);
|
setTexturePath(undefined);
|
||||||
|
setRingTexturePath(undefined);
|
||||||
|
|
||||||
const loadResources = async () => {
|
const loadResources = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetchBodyResources(body.id, body.type === 'probe' ? 'model' : 'texture');
|
const response = await fetchBodyResources(body.id, body.type === 'probe' ? 'model' : 'texture');
|
||||||
if (response.resources.length > 0) {
|
if (response.resources.length > 0) {
|
||||||
const mainResource = response.resources[0];
|
|
||||||
const fullPath = `/upload/${mainResource.file_path}`;
|
|
||||||
|
|
||||||
if (body.type === 'probe') {
|
if (body.type === 'probe') {
|
||||||
useGLTF.preload(fullPath); // Preload GLTF
|
const mainResource = response.resources[0];
|
||||||
setModelPath(fullPath);
|
const fullPath = `/upload/${mainResource.file_path}`;
|
||||||
setModelScale(mainResource.extra_data?.scale || 1.0);
|
useGLTF.preload(fullPath); // Preload GLTF
|
||||||
|
setModelPath(fullPath);
|
||||||
|
setModelScale(mainResource.extra_data?.scale || 1.0);
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
// No resources found
|
// No resources found
|
||||||
if (body.type === 'probe') setModelPath(null);
|
if (body.type === 'probe') setModelPath(null);
|
||||||
else setTexturePath(null);
|
else {
|
||||||
|
setTexturePath(null);
|
||||||
|
setRingTexturePath(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to load resource for ${body.name}:`, err);
|
console.error(`Failed to load resource for ${body.name}:`, err);
|
||||||
setLoadError(true);
|
setLoadError(true);
|
||||||
if (body.type === 'probe') setModelPath(null);
|
if (body.type === 'probe') setModelPath(null);
|
||||||
else setTexturePath(null);
|
else {
|
||||||
|
setTexturePath(null);
|
||||||
|
setRingTexturePath(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadResources();
|
loadResources();
|
||||||
|
|
@ -115,6 +211,7 @@ export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) {
|
||||||
emissive={appearance.emissive}
|
emissive={appearance.emissive}
|
||||||
emissiveIntensity={appearance.emissiveIntensity}
|
emissiveIntensity={appearance.emissiveIntensity}
|
||||||
texturePath={texturePath}
|
texturePath={texturePath}
|
||||||
|
ringTexturePath={ringTexturePath}
|
||||||
meshRef={meshRef}
|
meshRef={meshRef}
|
||||||
disableGlow={disableGlow}
|
disableGlow={disableGlow}
|
||||||
/>
|
/>
|
||||||
|
|
@ -179,12 +276,13 @@ function ProbeModelViewer({ modelPath, modelScale }: { modelPath: string; modelS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub-component for Planet 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;
|
body: CelestialBodyType;
|
||||||
size: number;
|
size: number;
|
||||||
emissive: string;
|
emissive: string;
|
||||||
emissiveIntensity: number;
|
emissiveIntensity: number;
|
||||||
texturePath: string | null;
|
texturePath: string | null;
|
||||||
|
ringTexturePath?: string | null;
|
||||||
meshRef: React.RefObject<Mesh>;
|
meshRef: React.RefObject<Mesh>;
|
||||||
disableGlow?: boolean;
|
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 (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Use irregular nucleus for comets, regular sphere for others */}
|
{/* Use irregular nucleus for comets, regular sphere for others */}
|
||||||
|
|
@ -395,8 +460,10 @@ function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePat
|
||||||
</mesh>
|
</mesh>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Saturn Rings */}
|
{/* Planetary Rings: Render ONLY if texture exists */}
|
||||||
{body.id === '699' && <SaturnRings />}
|
{ringTexturePath && (
|
||||||
|
<PlanetaryRings texturePath={ringTexturePath} planetRadius={size} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Sun glow effect - multi-layer scattered light (仅在非禁用光晕模式下显示) */}
|
{/* Sun glow effect - multi-layer scattered light (仅在非禁用光晕模式下显示) */}
|
||||||
{body.type === 'star' && !disableGlow && (
|
{body.type === 'star' && !disableGlow && (
|
||||||
|
|
|
||||||
|
|
@ -19,62 +19,70 @@ interface CelestialBodyProps {
|
||||||
onBodySelect?: (body: CelestialBodyType) => void;
|
onBodySelect?: (body: CelestialBodyType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saturn Rings component - multiple rings for band effect
|
// Planetary Rings component - handles texture-based rings
|
||||||
function SaturnRings() {
|
function PlanetaryRings({ texturePath, planetRadius }: { texturePath?: string | null, planetRadius: number }) {
|
||||||
return (
|
const texture = texturePath ? useTexture(texturePath) : null;
|
||||||
<group rotation={[Math.PI / 2, 0, 0]}>
|
const meshRef = useRef<Mesh>(null);
|
||||||
{/* Inner bright ring */}
|
|
||||||
<mesh>
|
// Dynamic ring dimensions based on planet size
|
||||||
<ringGeometry args={[1.4, 1.6, 32]} />
|
// Standard Saturn proportions: Inner ~1.1 R, Outer ~2.3 R
|
||||||
<meshBasicMaterial
|
const innerRadius = planetRadius * 1.05;
|
||||||
color="#D4B896"
|
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
|
transparent
|
||||||
opacity={0.7}
|
opacity={0.9}
|
||||||
side={DoubleSide}
|
side={DoubleSide}
|
||||||
|
color="#ffffff"
|
||||||
|
roughness={0.8}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
{/* Middle darker band */}
|
);
|
||||||
<mesh>
|
}
|
||||||
<ringGeometry args={[1.6, 1.75, 32]} />
|
|
||||||
<meshBasicMaterial
|
return null;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Planet component with texture
|
// Planet component with texture
|
||||||
|
|
@ -90,6 +98,7 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected
|
||||||
const meshRef = useRef<Mesh>(null);
|
const meshRef = useRef<Mesh>(null);
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
const [texturePath, setTexturePath] = useState<string | null | undefined>(undefined);
|
const [texturePath, setTexturePath] = useState<string | null | undefined>(undefined);
|
||||||
|
const [ringTexturePath, setRingTexturePath] = useState<string | null | undefined>(undefined);
|
||||||
|
|
||||||
// Use smart render position calculation
|
// Use smart render position calculation
|
||||||
const renderPosition = useMemo(() => {
|
const renderPosition = useMemo(() => {
|
||||||
|
|
@ -102,23 +111,40 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchBodyResources(body.id, 'texture')
|
fetchBodyResources(body.id, 'texture')
|
||||||
.then((response) => {
|
.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(
|
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) {
|
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}`);
|
setTexturePath(`/upload/${mainTexture.file_path}`);
|
||||||
} else {
|
} else {
|
||||||
setTexturePath(null);
|
setTexturePath(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ringTexture) {
|
||||||
|
setRingTexturePath(`/upload/${ringTexture.file_path}`);
|
||||||
|
} else {
|
||||||
|
setRingTexturePath(null);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
console.error(`Failed to load texture for ${body.name}:`, err);
|
console.error(`Failed to load texture for ${body.name}:`, err);
|
||||||
}
|
}
|
||||||
setTexturePath(null);
|
setTexturePath(null);
|
||||||
|
setRingTexturePath(null);
|
||||||
});
|
});
|
||||||
}, [body.id, body.name]);
|
}, [body.id, body.name]);
|
||||||
|
|
||||||
|
|
@ -134,6 +160,7 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected
|
||||||
emissiveIntensity={emissiveIntensity}
|
emissiveIntensity={emissiveIntensity}
|
||||||
scaledPos={scaledPos}
|
scaledPos={scaledPos}
|
||||||
texturePath={texturePath}
|
texturePath={texturePath}
|
||||||
|
ringTexturePath={ringTexturePath}
|
||||||
position={position}
|
position={position}
|
||||||
meshRef={meshRef}
|
meshRef={meshRef}
|
||||||
hasOffset={renderPosition.hasOffset}
|
hasOffset={renderPosition.hasOffset}
|
||||||
|
|
@ -284,13 +311,14 @@ function CometComa({ radius }: { radius: number }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate component to handle texture loading
|
// 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;
|
body: CelestialBodyType;
|
||||||
size: number;
|
size: number;
|
||||||
emissive: string;
|
emissive: string;
|
||||||
emissiveIntensity: number;
|
emissiveIntensity: number;
|
||||||
scaledPos: { x: number; y: number; z: number };
|
scaledPos: { x: number; y: number; z: number };
|
||||||
texturePath: string | null;
|
texturePath: string | null;
|
||||||
|
ringTexturePath?: string | null;
|
||||||
position: { x: number; y: number; z: number };
|
position: { x: number; y: number; z: number };
|
||||||
meshRef: React.RefObject<Mesh>;
|
meshRef: React.RefObject<Mesh>;
|
||||||
hasOffset: boolean;
|
hasOffset: boolean;
|
||||||
|
|
@ -376,8 +404,10 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur
|
||||||
</mesh>
|
</mesh>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Saturn Rings */}
|
{/* Planetary Rings: Render ONLY if texture exists (Standardized) */}
|
||||||
{body.id === '699' && <SaturnRings />}
|
{ringTexturePath && (
|
||||||
|
<PlanetaryRings texturePath={ringTexturePath} planetRadius={size} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Sun glow effect - multi-layer scattered light */}
|
{/* Sun glow effect - multi-layer scattered light */}
|
||||||
{body.type === 'star' && (
|
{body.type === 'star' && (
|
||||||
|
|
|
||||||