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
Binary file added src/assets/960px-Moon_texture.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 8 additions & 7 deletions src/data/planets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import jupiterTexture from "@/assets/2k_jupiter.jpg";
import marsTexture from "@/assets/2k_mars.jpg";
import sunTexture from "@/assets/2k_sun.jpg";
import venusTexture from "@/assets/2k_venus_atmosphere.jpg";
import moonTexture from "@/assets/960px-Moon_texture.jpg";
import earthTexture from "@/assets/earth_atmos_2048.jpg";
import type { Planet } from "@/types/planet";

Expand All @@ -19,17 +20,17 @@ export const earth: Planet = {
mass: 1,
};

export const testPlanet: Planet = {
id: "test-planet",
name: "TestPlanet",
texturePath: earthTexture,
rotationSpeedY: 2,
radius: 2,
export const moon: Planet = {
id: "moon",
name: "Moon",
texturePath: moonTexture,
rotationSpeedY: 0.07,
radius: 0.546,
width: 64,
height: 64,
position: new THREE.Vector3(100, 0, 0),
velocity: new THREE.Vector3(-10, 0, 0),
mass: 1,
mass: 0.0123,
};

export const sun: Planet = {
Expand Down
112 changes: 109 additions & 3 deletions src/pages/Simulation/components/PlanetMesh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,42 @@ import type React from "react";
import { useEffect, useMemo, useRef } from "react";
import * as THREE from "three";
import type { Planet } from "@/types/planet";
import {
CollisionType,
decideCollisionOutcome,
} from "../utils/decideCollisionOutcome";
import { calcGravityForce } from "../utils/gravityUtils";
import { mergePlanets } from "../utils/mergePlanets";

type PlanetMeshProps = {
planet: Planet;
planetRegistry: React.MutableRefObject<
Map<
string,
{ mesh: THREE.Mesh; position: React.MutableRefObject<number[]> }
{
mesh: THREE.Mesh;
position: React.MutableRefObject<number[]>;
velocity: React.MutableRefObject<number[]>;
}
>
>;
mergingPlanets: React.MutableRefObject<Set<string>>;
onExplosion: (position: THREE.Vector3, radius: number) => void;
onSelect: (planetId: string) => void;
onMerge: (
obsoletePlanetIdA: string,
obsoletePlanetIdB: string,
newPlanetData: Planet,
) => void;
};

export function PlanetMesh({
planet,
planetRegistry,
mergingPlanets,
onExplosion,
onSelect,
onMerge,
}: PlanetMeshProps) {
const [ref, api] = useSphere<THREE.Mesh>(
() => ({
Expand All @@ -35,7 +52,75 @@ export function PlanetMesh({
linearDamping: 0, // 宇宙空間なので抵抗なし
angularDamping: 0, // 宇宙空間なので回転の減衰もない
onCollide: (e) => {
// 衝突時の衝撃が一定以上なら爆発とみなす
const myId = e.target.userData.id;
const otherId = e.body.userData.id;

// どちらかが合体処理中なら即リターン
if (
mergingPlanets.current.has(myId) ||
mergingPlanets.current.has(otherId)
) {
return;
}

// 相手のIDが取得できない、または自分のIDの方が大きい場合は処理をスキップして重複を防ぐ
if (!otherId || myId > otherId) {
return;
}

if (
!planetRegistry.current.has(myId) ||
!planetRegistry.current.has(otherId)
) {
return;
}

const myPlanet = planetRegistry.current.get(myId);
const otherPlanet = planetRegistry.current.get(otherId);

if (!myPlanet || !otherPlanet) return;
if (!myPlanet.position || !otherPlanet.position) return;
if (!myPlanet.velocity || !otherPlanet.velocity) return;

const myPos = new THREE.Vector3().fromArray(myPlanet.position.current);
const myVel = new THREE.Vector3().fromArray(myPlanet.velocity.current);
const otherPos = new THREE.Vector3().fromArray(
otherPlanet.position.current,
);
const otherVel = new THREE.Vector3().fromArray(
otherPlanet.velocity.current,
);

const result: string = decideCollisionOutcome(
myPlanet.mesh.userData.mass,
myPlanet.mesh.userData.radius,
myPos,
myVel,
otherPlanet.mesh.userData.mass,
otherPlanet.mesh.userData.radius,
otherPos,
otherVel,
);

if (result === CollisionType.Merge) {
console.log(CollisionType.Merge);
const newPlanetData = mergePlanets(
myPlanet.mesh.userData.mass,
myPlanet.mesh.userData.radius,
myPos,
myVel,
myPlanet.mesh.userData.rotationSpeedY,
otherPlanet.mesh.userData.mass,
otherPlanet.mesh.userData.radius,
otherPos,
otherVel,
otherPlanet.mesh.userData.rotationSpeedY,
);
onMerge(myId, otherId, newPlanetData);
} else {
console.log(CollisionType.Explode);
}

if (e.contact.impactVelocity > 0.5) {
const contactPoint = new THREE.Vector3(
e.contact.contactPoint[0],
Expand Down Expand Up @@ -65,6 +150,18 @@ export function PlanetMesh({
return () => unsubscribe(); // アンマウント時に購読解除
}, [api.position]);

const velocity = useRef([
planet.velocity.x,
planet.velocity.y,
planet.velocity.z,
]);
useEffect(() => {
const unsubscribe = api.velocity.subscribe((v) => {
velocity.current = v;
});
return () => unsubscribe(); // アンマウント時に購読解除
}, [api.velocity]);

// マウント時に自分のMeshをレジストリに登録し、他の惑星から参照できるようにする
useEffect(() => {
if (!planetRegistry.current) return;
Expand All @@ -75,18 +172,27 @@ export function PlanetMesh({
mass: planet.mass,
id: planet.id,
radius: planet.radius,
rotationSpeedY: planet.rotationSpeedY,
};
planetRegistry.current.set(planet.id, {
mesh: ref.current,
position,
velocity,
});
}
return () => {
if (planetRegistry.current) {
planetRegistry.current.delete(planet.id);
}
};
}, [planet.id, planetRegistry, planet.mass, planet.radius, ref]);
}, [
planet.id,
planetRegistry,
planet.mass,
planet.radius,
planet.rotationSpeedY,
ref,
]);

// 計算用ベクトルをメモリに保持しておく(毎フレームnewしないため)
const forceAccumulator = useMemo(() => new THREE.Vector3(), []);
Expand Down
51 changes: 48 additions & 3 deletions src/pages/Simulation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Physics } from "@react-three/cannon";
import { OrbitControls, Stars, useTexture } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { button, useControls } from "leva";
import type React from "react";
import { Suspense, useMemo, useRef, useState } from "react";
import * as THREE from "three";
import type { OrbitControls as Controls } from "three-stdlib";
import { earth, jupiter, mars, sun, venus } from "@/data/planets";
import { earth, jupiter, mars, moon, sun, venus } from "@/data/planets";
import type { ExplosionData } from "@/types/Explosion";
import type { Planet } from "@/types/planet";
import { CameraController } from "./components/CameraController";
Expand All @@ -18,23 +19,29 @@ import {

const planetTexturePaths = [
earth.texturePath,
moon.texturePath,
sun.texturePath,
mars.texturePath,
jupiter.texturePath,
venus.texturePath,
];
useTexture.preload(planetTexturePaths);

const planetTemplates = { earth, sun, mars, jupiter, venus } as const;
const planetTemplates = { earth, moon, sun, mars, jupiter, venus } as const;

export default function Page() {
const orbitControlsRef = useRef<Controls | null>(null);
const planetRegistry = useRef<
Map<
string,
{ mesh: THREE.Mesh; position: React.MutableRefObject<number[]> }
{
mesh: THREE.Mesh;
position: React.MutableRefObject<number[]>;
velocity: React.MutableRefObject<number[]>;
}
>
>(new Map());
const mergingPlanets = useRef<Set<string>>(new Set());

const [planets, setPlanets] = useState<Planet[]>([earth]);
const [explosions, setExplosions] = useState<ExplosionData[]>([]);
Expand All @@ -50,6 +57,7 @@ export default function Page() {
value: "earth",
options: {
Earth: "earth",
Moon: "moon",
Sun: "sun",
Mars: "mars",
Jupiter: "jupiter",
Expand Down Expand Up @@ -160,6 +168,31 @@ export default function Page() {
});
};

const handleMerge = (
obsoletePlanetIdA: string,
obsoletePlanetIdB: string,
newPlanetData: Planet,
) => {
mergingPlanets.current.add(obsoletePlanetIdA);
mergingPlanets.current.add(obsoletePlanetIdB);
planetRegistry.current.delete(obsoletePlanetIdA);
planetRegistry.current.delete(obsoletePlanetIdB);
setPlanets((prev) => {
// 削除して追加
return prev
.filter((p) => p.id !== obsoletePlanetIdA && p.id !== obsoletePlanetIdB)
.concat(newPlanetData);
});

// フォロー中の惑星が削除対象なら解除
if (
followedPlanetId === obsoletePlanetIdA ||
followedPlanetId === obsoletePlanetIdB
) {
setFollowedPlanetId(null);
}
};

return (
<div className="relative h-screen w-screen">
<Canvas
Expand All @@ -185,8 +218,20 @@ export default function Page() {
<PlanetMesh
planet={planet}
planetRegistry={planetRegistry}
mergingPlanets={mergingPlanets}
onExplosion={handleExplosion}
onSelect={(id) => setFollowedPlanetId(id)}
onMerge={(
obsoletePlanetIdA,
obsoletePlanetIdB,
newPlanetData,
) =>
handleMerge(
obsoletePlanetIdA,
obsoletePlanetIdB,
newPlanetData,
)
}
/>
</Suspense>
))}
Expand Down
44 changes: 44 additions & 0 deletions src/pages/Simulation/utils/decideCollisionOutcome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as THREE from "three";
import { G } from "./gravityUtils";

export const CollisionType = {
Explode: "explode",
Merge: "merge",
} as const;
//経験的係数
const kFactor = 0.5;

export function decideCollisionOutcome(
massA: number,
radA: number,
posA: THREE.Vector3,
velA: THREE.Vector3,
massB: number,
radB: number,
posB: THREE.Vector3,
velB: THREE.Vector3,
): string {
//脱出速度vEsc
const vEsc = Math.sqrt((2 * G * (massA + massB)) / (radA + radB));

//相対速度及び相対位置
const vRel = new THREE.Vector3().subVectors(velB, velA);
const pRel = new THREE.Vector3().subVectors(posB, posA);

//衝突角補正
const distance = pRel.length();
const angleFactor =
distance > 0 ? Math.abs(vRel.dot(pRel) / (vRel.length() * distance)) : 1;
//質量比補正
const massFactor = massA > massB ? massB / massA : massA / massB;

//臨界速度vCrit
const vCrit = vEsc * (1 + kFactor * (1 - massFactor)) * angleFactor;

const vRelLen = vRel.length();
if (vRelLen < vCrit) {
return CollisionType.Merge;
} else {
return CollisionType.Explode;
}
}
2 changes: 1 addition & 1 deletion src/pages/Simulation/utils/gravityUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as THREE from "three";

const G = 1;
export const G = 1;
const softeningFactor = 0.005;

export function calcGravityForce(
Expand Down
Loading