2025-12-04 12:37:10 +00:00
|
|
|
import { useRef, useMemo, useState, useEffect, Suspense } from 'react';
|
|
|
|
|
import { Mesh, Group } from 'three';
|
|
|
|
|
import * as THREE from 'three';
|
|
|
|
|
import { useGLTF, useTexture } from '@react-three/drei';
|
|
|
|
|
import { useFrame } from '@react-three/fiber';
|
|
|
|
|
import type { CelestialBody as CelestialBodyType } from '../types';
|
|
|
|
|
import { fetchBodyResources } from '../utils/api';
|
|
|
|
|
import { getCelestialSize } from '../config/celestialSizes';
|
|
|
|
|
|
|
|
|
|
interface BodyViewerProps {
|
|
|
|
|
body: CelestialBodyType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reusable component to render just the 3D model/mesh of a celestial body
|
|
|
|
|
export function BodyViewer({ body }: BodyViewerProps) {
|
|
|
|
|
const meshRef = useRef<Mesh>(null);
|
|
|
|
|
const groupRef = useRef<Group>(null);
|
|
|
|
|
const [texturePath, setTexturePath] = 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);
|
|
|
|
|
|
2025-12-04 13:43:43 +00:00
|
|
|
// Determine size and appearance - use larger sizes for detail view with a cap
|
2025-12-04 12:37:10 +00:00
|
|
|
const appearance = useMemo(() => {
|
2025-12-04 13:43:43 +00:00
|
|
|
const baseSize = getCelestialSize(body.name, body.type);
|
|
|
|
|
|
|
|
|
|
// Detail view scaling strategy:
|
|
|
|
|
// - Small bodies (< 0.15): scale up 3x for visibility
|
|
|
|
|
// - Medium bodies (0.15-0.3): scale up 2x
|
|
|
|
|
// - Large bodies (> 0.3): use base size with max cap of 1.2
|
|
|
|
|
let finalSize: number;
|
|
|
|
|
|
|
|
|
|
if (body.type === 'star') {
|
|
|
|
|
finalSize = 1.0; // Fixed size for stars
|
|
|
|
|
} else if (baseSize < 0.15) {
|
|
|
|
|
// Small bodies: scale up for visibility
|
|
|
|
|
finalSize = baseSize * 3.0;
|
|
|
|
|
} else if (baseSize < 0.3) {
|
|
|
|
|
// Medium bodies: moderate scaling
|
|
|
|
|
finalSize = baseSize * 2.0;
|
|
|
|
|
} else {
|
|
|
|
|
// Large bodies: minimal or no scaling with a cap
|
|
|
|
|
finalSize = Math.min(baseSize * 1.2, 1.2);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-04 12:37:10 +00:00
|
|
|
if (body.type === 'star') {
|
2025-12-04 13:43:43 +00:00
|
|
|
return { size: finalSize, emissive: '#FDB813', emissiveIntensity: 1.5 };
|
2025-12-04 12:37:10 +00:00
|
|
|
}
|
|
|
|
|
if (body.type === 'comet') {
|
2025-12-04 13:43:43 +00:00
|
|
|
return { size: finalSize, emissive: '#000000', emissiveIntensity: 0 };
|
2025-12-04 12:37:10 +00:00
|
|
|
}
|
|
|
|
|
if (body.type === 'satellite') {
|
2025-12-04 13:43:43 +00:00
|
|
|
return { size: finalSize, emissive: '#888888', emissiveIntensity: 0.4 };
|
2025-12-04 12:37:10 +00:00
|
|
|
}
|
2025-12-04 13:43:43 +00:00
|
|
|
return { size: finalSize, emissive: '#000000', emissiveIntensity: 0 };
|
2025-12-04 12:37:10 +00:00
|
|
|
}, [body.name, body.type]);
|
|
|
|
|
|
|
|
|
|
// Fetch resources (texture or model)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setLoadError(false);
|
|
|
|
|
setModelPath(undefined);
|
|
|
|
|
setTexturePath(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);
|
|
|
|
|
} else {
|
|
|
|
|
setTexturePath(fullPath);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No resources found
|
|
|
|
|
if (body.type === 'probe') setModelPath(null);
|
|
|
|
|
else setTexturePath(null);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(`Failed to load resource for ${body.name}:`, err);
|
|
|
|
|
setLoadError(true);
|
|
|
|
|
if (body.type === 'probe') setModelPath(null);
|
|
|
|
|
else setTexturePath(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
loadResources();
|
|
|
|
|
}, [body.id, body.name, body.type]);
|
|
|
|
|
|
|
|
|
|
// Handle Probe Model rendering
|
|
|
|
|
if (body.type === 'probe' && modelPath !== undefined) {
|
|
|
|
|
if (modelPath === null || loadError) {
|
|
|
|
|
// Fallback sphere for probes if model fails
|
|
|
|
|
return (
|
|
|
|
|
<mesh ref={meshRef}>
|
|
|
|
|
<sphereGeometry args={[0.15, 32, 32]} />
|
|
|
|
|
<meshStandardMaterial color="#ff0000" emissive="#ff0000" emissiveIntensity={0.8} />
|
|
|
|
|
</mesh>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return <ProbeModelViewer modelPath={modelPath} modelScale={modelScale} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle Celestial Body (Planet, Star, etc.) rendering
|
|
|
|
|
if (texturePath !== undefined) {
|
|
|
|
|
return (
|
|
|
|
|
<PlanetModelViewer
|
|
|
|
|
body={body}
|
|
|
|
|
size={appearance.size}
|
|
|
|
|
emissive={appearance.emissive}
|
|
|
|
|
emissiveIntensity={appearance.emissiveIntensity}
|
|
|
|
|
texturePath={texturePath}
|
|
|
|
|
meshRef={meshRef}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show nothing while loading resources
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sub-component for Probe models
|
|
|
|
|
function ProbeModelViewer({ modelPath, modelScale }: { modelPath: string; modelScale: number }) {
|
|
|
|
|
const groupRef = useRef<Group>(null);
|
|
|
|
|
const gltf = useGLTF(modelPath);
|
|
|
|
|
const scene = gltf.scene;
|
|
|
|
|
|
|
|
|
|
const optimalScale = useMemo(() => {
|
|
|
|
|
if (!scene) return 1;
|
|
|
|
|
const box = new THREE.Box3().setFromObject(scene);
|
|
|
|
|
const size = new THREE.Vector3();
|
|
|
|
|
box.getSize(size);
|
|
|
|
|
const maxDimension = Math.max(size.x, size.y, size.z);
|
2025-12-04 13:43:43 +00:00
|
|
|
const targetSize = 1.2; // Increased from 0.35 to 1.2 for detail view - larger probes
|
|
|
|
|
const calculatedScale = maxDimension > 0 ? targetSize / maxDimension : 0.8;
|
|
|
|
|
const finalScale = Math.max(0.5, Math.min(3.0, calculatedScale)); // Increased range for larger display
|
2025-12-04 12:37:10 +00:00
|
|
|
return finalScale * modelScale;
|
|
|
|
|
}, [scene, modelScale]);
|
|
|
|
|
|
|
|
|
|
const configuredScene = useMemo(() => {
|
|
|
|
|
if (!scene) return null;
|
|
|
|
|
const clonedScene = scene.clone();
|
|
|
|
|
clonedScene.traverse((child: any) => {
|
|
|
|
|
if (child.isMesh) {
|
|
|
|
|
if (child.material) {
|
|
|
|
|
if (Array.isArray(child.material)) {
|
|
|
|
|
child.material = child.material.map((mat: any) => {
|
|
|
|
|
const clonedMat = mat.clone();
|
|
|
|
|
clonedMat.depthTest = true;
|
|
|
|
|
clonedMat.depthWrite = true;
|
|
|
|
|
return clonedMat;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
child.material = child.material.clone();
|
|
|
|
|
child.material.depthTest = true;
|
|
|
|
|
child.material.depthWrite = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return clonedScene;
|
|
|
|
|
}, [scene]);
|
|
|
|
|
|
|
|
|
|
if (!configuredScene) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Suspense fallback={null}>
|
|
|
|
|
<group ref={groupRef}>
|
|
|
|
|
<primitive object={configuredScene} scale={optimalScale} />
|
|
|
|
|
</group>
|
|
|
|
|
</Suspense>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sub-component for Planet models
|
|
|
|
|
function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePath, meshRef }: {
|
|
|
|
|
body: CelestialBodyType;
|
|
|
|
|
size: number;
|
|
|
|
|
emissive: string;
|
|
|
|
|
emissiveIntensity: number;
|
|
|
|
|
texturePath: string | null;
|
|
|
|
|
meshRef: React.RefObject<Mesh>;
|
|
|
|
|
}) {
|
|
|
|
|
const texture = texturePath ? useTexture(texturePath) : null;
|
|
|
|
|
|
|
|
|
|
// Rotation animation
|
|
|
|
|
useFrame((_, delta) => {
|
|
|
|
|
if (meshRef.current) {
|
|
|
|
|
meshRef.current.rotation.y += delta * 0.1;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Irregular Comet Nucleus - potato-like shape
|
|
|
|
|
function IrregularNucleus({ size, texture }: { size: number; texture: THREE.Texture | null }) {
|
|
|
|
|
const nucleusRef = useRef<Mesh>(null);
|
|
|
|
|
|
|
|
|
|
// Create irregular geometry by deforming a sphere
|
|
|
|
|
const geometry = useMemo(() => {
|
|
|
|
|
const geo = new THREE.SphereGeometry(size, 64, 64); // Higher resolution for detail view
|
|
|
|
|
const positions = geo.attributes.position;
|
|
|
|
|
|
|
|
|
|
// Deform vertices to create irregular shape
|
|
|
|
|
for (let i = 0; i < positions.count; i++) {
|
|
|
|
|
const x = positions.getX(i);
|
|
|
|
|
const y = positions.getY(i);
|
|
|
|
|
const z = positions.getZ(i);
|
|
|
|
|
|
|
|
|
|
// Create multiple noise frequencies for complex shape
|
|
|
|
|
const noise1 = Math.sin(x * 3.5) * Math.cos(y * 2.7) * Math.sin(z * 4.2);
|
|
|
|
|
const noise2 = Math.cos(x * 5.1) * Math.sin(y * 4.3) * Math.cos(z * 3.8);
|
|
|
|
|
const noise3 = Math.sin(x * 7.2) * Math.sin(y * 6.1) * Math.cos(z * 5.5);
|
|
|
|
|
|
|
|
|
|
const deformation = 1 + (noise1 * 0.25 + noise2 * 0.15 + noise3 * 0.1);
|
|
|
|
|
|
|
|
|
|
positions.setXYZ(i, x * deformation, y * deformation, z * deformation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
geo.computeVertexNormals();
|
|
|
|
|
return geo;
|
|
|
|
|
}, [size]);
|
|
|
|
|
|
|
|
|
|
// Slow rotation
|
|
|
|
|
useFrame((_, delta) => {
|
|
|
|
|
if (nucleusRef.current) {
|
|
|
|
|
nucleusRef.current.rotation.y += delta * 0.05;
|
|
|
|
|
nucleusRef.current.rotation.z += delta * 0.02;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<mesh ref={nucleusRef} geometry={geometry}>
|
|
|
|
|
{texture ? (
|
|
|
|
|
<meshStandardMaterial
|
|
|
|
|
map={texture}
|
|
|
|
|
roughness={0.9}
|
|
|
|
|
metalness={0.1}
|
|
|
|
|
color="#b8b8b8"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<meshStandardMaterial
|
|
|
|
|
color="#6b6b6b"
|
|
|
|
|
roughness={0.95}
|
|
|
|
|
metalness={0.05}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</mesh>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enhanced Coma (gas/dust cloud) around comet nucleus
|
|
|
|
|
function CometComa({ radius }: { radius: number }) {
|
|
|
|
|
const positions = useMemo(() => {
|
|
|
|
|
const count = 300; // More particles for detail view
|
|
|
|
|
const p = new Float32Array(count * 3);
|
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
|
|
|
// Denser near center, spreading outward
|
|
|
|
|
const r = radius * (0.8 + Math.pow(Math.random(), 1.5) * 4); // 0.8x to 4.8x radius
|
|
|
|
|
const theta = Math.random() * Math.PI * 2;
|
|
|
|
|
const phi = Math.acos(2 * Math.random() - 1);
|
|
|
|
|
|
|
|
|
|
p[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
|
|
|
|
p[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
|
|
|
|
p[i * 3 + 2] = r * Math.cos(phi);
|
|
|
|
|
}
|
|
|
|
|
return p;
|
|
|
|
|
}, [radius]);
|
|
|
|
|
|
|
|
|
|
const pointsRef = useRef<THREE.Points>(null);
|
|
|
|
|
|
|
|
|
|
// Animate coma
|
|
|
|
|
useFrame((state, delta) => {
|
|
|
|
|
if (pointsRef.current) {
|
|
|
|
|
pointsRef.current.rotation.y += delta * 0.05;
|
|
|
|
|
// Pulsing effect
|
|
|
|
|
const scale = 1 + Math.sin(state.clock.elapsedTime * 0.5) * 0.1;
|
|
|
|
|
pointsRef.current.scale.setScalar(scale);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{/* Inner bright coma */}
|
|
|
|
|
<points ref={pointsRef}>
|
|
|
|
|
<bufferGeometry>
|
|
|
|
|
<bufferAttribute attach="position" args={[positions, 3]} />
|
|
|
|
|
</bufferGeometry>
|
|
|
|
|
<pointsMaterial
|
|
|
|
|
size={radius * 0.8}
|
|
|
|
|
color="#88ccff"
|
|
|
|
|
transparent
|
|
|
|
|
opacity={0.7}
|
|
|
|
|
sizeAttenuation={true}
|
|
|
|
|
blending={THREE.AdditiveBlending}
|
|
|
|
|
depthWrite={false}
|
|
|
|
|
/>
|
|
|
|
|
</points>
|
|
|
|
|
|
|
|
|
|
{/* Middle glow layer */}
|
|
|
|
|
<mesh>
|
|
|
|
|
<sphereGeometry args={[radius * 2.5, 32, 32]} />
|
|
|
|
|
<meshBasicMaterial
|
|
|
|
|
color="#66aadd"
|
|
|
|
|
transparent
|
|
|
|
|
opacity={0.15}
|
|
|
|
|
side={THREE.BackSide}
|
|
|
|
|
blending={THREE.AdditiveBlending}
|
|
|
|
|
depthWrite={false}
|
|
|
|
|
/>
|
|
|
|
|
</mesh>
|
|
|
|
|
|
|
|
|
|
{/* Outer diffuse glow */}
|
|
|
|
|
<mesh>
|
|
|
|
|
<sphereGeometry args={[radius * 4.5, 32, 32]} />
|
|
|
|
|
<meshBasicMaterial
|
|
|
|
|
color="#4488aa"
|
|
|
|
|
transparent
|
|
|
|
|
opacity={0.12}
|
|
|
|
|
side={THREE.BackSide}
|
|
|
|
|
blending={THREE.AdditiveBlending}
|
|
|
|
|
depthWrite={false}
|
|
|
|
|
/>
|
|
|
|
|
</mesh>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 */}
|
|
|
|
|
{body.type === 'comet' ? (
|
|
|
|
|
<>
|
|
|
|
|
<IrregularNucleus size={size} texture={texture} />
|
|
|
|
|
<CometComa radius={size} />
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<mesh ref={meshRef} position={[0,0,0]}>
|
|
|
|
|
<sphereGeometry args={[size, 64, 64]} /> {/* Higher resolution for detail view */}
|
|
|
|
|
{texture ? (
|
|
|
|
|
<meshStandardMaterial
|
|
|
|
|
map={texture}
|
|
|
|
|
emissive={emissive}
|
|
|
|
|
emissiveIntensity={emissiveIntensity}
|
|
|
|
|
roughness={body.type === 'star' ? 0 : 0.7}
|
|
|
|
|
metalness={0.1}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<meshStandardMaterial
|
|
|
|
|
color="#888888"
|
|
|
|
|
emissive={emissive}
|
|
|
|
|
emissiveIntensity={emissiveIntensity}
|
|
|
|
|
roughness={0.7}
|
|
|
|
|
metalness={0.1}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</mesh>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Saturn Rings */}
|
|
|
|
|
{body.id === '699' && <SaturnRings />}
|
|
|
|
|
|
2025-12-04 13:43:43 +00:00
|
|
|
{/* Sun glow effect - multi-layer scattered light */}
|
2025-12-04 12:37:10 +00:00
|
|
|
{body.type === 'star' && (
|
|
|
|
|
<>
|
|
|
|
|
<pointLight intensity={10} distance={400} color="#fff8e7" />
|
2025-12-04 13:43:43 +00:00
|
|
|
|
|
|
|
|
{/* Inner bright corona */}
|
|
|
|
|
<mesh>
|
|
|
|
|
<sphereGeometry args={[size * 1.3, 32, 32]} />
|
|
|
|
|
<meshBasicMaterial
|
|
|
|
|
color="#FDB813"
|
|
|
|
|
transparent
|
|
|
|
|
opacity={0.6}
|
|
|
|
|
blending={THREE.AdditiveBlending}
|
|
|
|
|
depthWrite={false}
|
|
|
|
|
/>
|
|
|
|
|
</mesh>
|
|
|
|
|
|
|
|
|
|
{/* Middle glow layer */}
|
2025-12-04 12:37:10 +00:00
|
|
|
<mesh>
|
|
|
|
|
<sphereGeometry args={[size * 1.8, 32, 32]} />
|
2025-12-04 13:43:43 +00:00
|
|
|
<meshBasicMaterial
|
|
|
|
|
color="#FDB813"
|
|
|
|
|
transparent
|
|
|
|
|
opacity={0.3}
|
|
|
|
|
blending={THREE.AdditiveBlending}
|
|
|
|
|
depthWrite={false}
|
|
|
|
|
/>
|
|
|
|
|
</mesh>
|
|
|
|
|
|
|
|
|
|
{/* Outer diffuse halo */}
|
|
|
|
|
<mesh>
|
|
|
|
|
<sphereGeometry args={[size * 2.5, 32, 32]} />
|
|
|
|
|
<meshBasicMaterial
|
|
|
|
|
color="#FFD700"
|
|
|
|
|
transparent
|
|
|
|
|
opacity={0.15}
|
|
|
|
|
blending={THREE.AdditiveBlending}
|
|
|
|
|
depthWrite={false}
|
|
|
|
|
/>
|
|
|
|
|
</mesh>
|
|
|
|
|
|
|
|
|
|
{/* Far scattered light */}
|
|
|
|
|
<mesh>
|
|
|
|
|
<sphereGeometry args={[size * 3.5, 32, 32]} />
|
|
|
|
|
<meshBasicMaterial
|
|
|
|
|
color="#FFA500"
|
|
|
|
|
transparent
|
|
|
|
|
opacity={0.08}
|
|
|
|
|
blending={THREE.AdditiveBlending}
|
|
|
|
|
depthWrite={false}
|
|
|
|
|
/>
|
2025-12-04 12:37:10 +00:00
|
|
|
</mesh>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</group>
|
|
|
|
|
);
|
|
|
|
|
}
|