Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/components/Explosion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useFrame } from "@react-three/fiber";
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import type { Planet } from "@/types/planet";

type Fragment = {
mesh: THREE.Mesh;
velocity: THREE.Vector3;
rotationAxis: THREE.Vector3;
lifetime: number;
};

type ExplosionProps = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コード全体を通して、interface と type が混在していると思うのですが、どちらかに統一するのがいいと思います。type に統一するべきっていう意見もあれば interface に統一するべきっていう意見もあるのですが、個人的には type の方が直感的でいいかな、と思います。

planet: Planet;
fragmentCount?: number;
onComplete?: () => void;
};

export const Explosion: React.FC<ExplosionProps> = ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コンポーネント関数は無名関数ではなく function で定義するよう統一した方がいいと思います。これも type と interface と同じように双方意見があるのですが、ut.code(); Learn に合わせるのが ut.code(); のプロジェクトとして進める上ではいいかと思います。

planet,
fragmentCount = 50,
onComplete,
}: ExplosionProps) => {
const groupRef = useRef<THREE.Group>(null);
const [fragments, setFragments] = useState<Fragment[]>([]);

// 爆発初期化
useEffect(() => {
const newFragments: Fragment[] = [];
for (let i = 0; i < fragmentCount; i++) {
const size = Math.random() * (planet.radius * 0.2) + 0.05;
const geometry = new THREE.SphereGeometry(size, 6, 6);
const material = new THREE.MeshStandardMaterial({
color: 0xffaa33,
emissive: 0xff5500,
});
const mesh = new THREE.Mesh(geometry, material);

// 初期位置は惑星中心
mesh.position.copy(planet.position);

// ランダム方向に飛ぶ速度
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * 4,
(Math.random() - 0.5) * 4,
(Math.random() - 0.5) * 4,
);

// 回転軸
const rotationAxis = new THREE.Vector3(
Math.random(),
Math.random(),
Math.random(),
).normalize();

newFragments.push({
mesh,
velocity,
rotationAxis,
lifetime: Math.random() * 2 + 1, // 1~3秒で消える
});
}
setFragments(newFragments);
}, [planet, fragmentCount]);

// フレームごとの更新
useFrame((_, delta) => {
if (fragments.length === 0) return;

setFragments((prev) => {
const alive: Fragment[] = [];
prev.forEach((f) => {
// 位置更新
f.mesh.position.add(f.velocity.clone().multiplyScalar(delta));

// 回転
f.mesh.rotateOnAxis(f.rotationAxis, delta * 5);

// 減速(摩擦的)
f.velocity.multiplyScalar(0.98);

// 減衰
f.lifetime -= delta;
if (f.lifetime > 0) alive.push(f);
else f.mesh.parent?.remove(f.mesh); // Group から削除
});

// 爆発完了通知
if (alive.length === 0 && onComplete) onComplete();

return alive;
});
});

return (
<group ref={groupRef}>
{fragments.map((f, i) => (
<primitive key={i} object={f.mesh} />
))}
</group>
);
};
14 changes: 14 additions & 0 deletions src/data/planets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@ import * as THREE from "three";
import type { Planet } from "@/types/planet";

export const earth: Planet = {
name: "Earth",
texturePath:
"https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg",
rotationSpeedY: 2,
radius: 2,
width: 64,
height: 64,
position: new THREE.Vector3(0, 0, 0),
velocity: new THREE.Vector3(0, 0, 0),
};

export const testPlanet: Planet = {
name: "TestPlanet",
texturePath:
"https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg",
rotationSpeedY: 2,
radius: 2,
width: 64,
height: 64,
position: new THREE.Vector3(100, 0, 0),
velocity: new THREE.Vector3(-10, 0, 0),
};

// Easy to add more planets later:
Expand Down
67 changes: 64 additions & 3 deletions src/pages/Simulation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { OrbitControls, Stars, useTexture } from "@react-three/drei";
import { Canvas, useFrame } from "@react-three/fiber";
import { useRef } from "react";
import { useRef, useState } from "react";
import type * as THREE from "three";
import { earth } from "@/data/planets";
import { Explosion } from "@/components/Explosion";
import { earth, testPlanet } from "@/data/planets";
import type { Planet } from "@/types/planet";
import { isColliding } from "@/utils/isColliding";

const testPlanets: Planet[] = [earth, testPlanet];

interface PlanetMeshProps {
planet: Planet;
Expand All @@ -20,6 +24,12 @@ function PlanetMesh({ planet }: PlanetMeshProps) {
if (meshRef.current) {
// Rotate the planet on its Y-axis
meshRef.current.rotation.y += delta * planet.rotationSpeedY;
// 位置を planet.position に同期
meshRef.current.position.set(
planet.position.x,
planet.position.y,
planet.position.z,
);
}
});

Expand All @@ -36,8 +46,53 @@ function PlanetMesh({ planet }: PlanetMeshProps) {
</mesh>
);
}
interface SimulationProps {
planets: Planet[];
setExplosions: React.Dispatch<React.SetStateAction<Planet[]>>;
}

export function Simulation({ planets, setExplosions }: SimulationProps) {
// 前フレームの衝突ペアを記録して、連続爆発を防ぐ
const collidedPairsRef = useRef<Set<string>>(new Set());

useFrame((_state, delta) => {
// 並進運動
for (let i = 0; i < planets.length; i++) {
if (planets[i].velocity) {
planets[i].position.addScaledVector(planets[i].velocity, delta);
}
}

// 衝突判定
for (let i = 0; i < planets.length; i++) {
for (let j = i + 1; j < planets.length; j++) {
const a = planets[i];
const b = planets[j];
const key = `${i}-${j}`;

if (isColliding(a, b)) {
if (!collidedPairsRef.current.has(key)) {
collidedPairsRef.current.add(key);

// 衝突したら爆発を追加
setExplosions((prev) => [...prev, a, b]);

console.log(`Collision detected between planet ${i} and ${j}`);
}
} else {
// 衝突していない場合は記録を削除
collidedPairsRef.current.delete(key);
}
}
}
});

return null;
}

export default function Page() {
const [explosions, setExplosions] = useState<Planet[]>([]);

return (
<Canvas
camera={{ position: [0, 0, 6] }}
Expand All @@ -50,7 +105,13 @@ export default function Page() {
<ambientLight intensity={1.2} />
<pointLight position={[10, 10, 10]} intensity={3} />

<PlanetMesh planet={earth} />
{testPlanets.map((planet) => (
<PlanetMesh key={planet.name} planet={planet} />
))}
<Simulation planets={testPlanets} setExplosions={setExplosions} />
{explosions.map((exp, idx) => (
<Explosion key={idx} planet={exp} />
))}

{/* Optional background and controls */}
<Stars
Expand Down
2 changes: 2 additions & 0 deletions src/types/planet.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type * as THREE from "three";
export type Planet = {
name: string;
texturePath: string;
rotationSpeedY: number;
radius: number;
width: number;
height: number;
position: THREE.Vector3;
velocity: THREE.Vector3;
};
9 changes: 9 additions & 0 deletions src/utils/isColliding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Planet } from "@/types/planet";

export function isColliding(planet1: Planet, planet2: Planet): boolean {
const distanceSquared = planet1.position.distanceToSquared(planet2.position);

const radiusSum = planet1.radius + planet2.radius;

return distanceSquared <= radiusSum ** 2;
}