diff --git a/src/assets/960px-Moon_texture.jpg b/src/assets/960px-Moon_texture.jpg new file mode 100644 index 0000000..23a824e Binary files /dev/null and b/src/assets/960px-Moon_texture.jpg differ diff --git a/src/data/planets.ts b/src/data/planets.ts index 0cad35f..af49028 100644 --- a/src/data/planets.ts +++ b/src/data/planets.ts @@ -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"; @@ -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 = { diff --git a/src/pages/Simulation/components/PlanetMesh.tsx b/src/pages/Simulation/components/PlanetMesh.tsx index 7c310d4..d031476 100644 --- a/src/pages/Simulation/components/PlanetMesh.tsx +++ b/src/pages/Simulation/components/PlanetMesh.tsx @@ -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 } + { + mesh: THREE.Mesh; + position: React.MutableRefObject; + velocity: React.MutableRefObject; + } > >; + mergingPlanets: React.MutableRefObject>; 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( () => ({ @@ -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], @@ -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; @@ -75,10 +172,12 @@ 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 () => { @@ -86,7 +185,14 @@ export function PlanetMesh({ 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(), []); diff --git a/src/pages/Simulation/index.tsx b/src/pages/Simulation/index.tsx index dabf4aa..0c4920f 100644 --- a/src/pages/Simulation/index.tsx +++ b/src/pages/Simulation/index.tsx @@ -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"; @@ -18,6 +19,7 @@ import { const planetTexturePaths = [ earth.texturePath, + moon.texturePath, sun.texturePath, mars.texturePath, jupiter.texturePath, @@ -25,16 +27,21 @@ const planetTexturePaths = [ ]; 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(null); const planetRegistry = useRef< Map< string, - { mesh: THREE.Mesh; position: React.MutableRefObject } + { + mesh: THREE.Mesh; + position: React.MutableRefObject; + velocity: React.MutableRefObject; + } > >(new Map()); + const mergingPlanets = useRef>(new Set()); const [planets, setPlanets] = useState([earth]); const [explosions, setExplosions] = useState([]); @@ -50,6 +57,7 @@ export default function Page() { value: "earth", options: { Earth: "earth", + Moon: "moon", Sun: "sun", Mars: "mars", Jupiter: "jupiter", @@ -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 (
setFollowedPlanetId(id)} + onMerge={( + obsoletePlanetIdA, + obsoletePlanetIdB, + newPlanetData, + ) => + handleMerge( + obsoletePlanetIdA, + obsoletePlanetIdB, + newPlanetData, + ) + } /> ))} diff --git a/src/pages/Simulation/utils/decideCollisionOutcome.ts b/src/pages/Simulation/utils/decideCollisionOutcome.ts new file mode 100644 index 0000000..9d64947 --- /dev/null +++ b/src/pages/Simulation/utils/decideCollisionOutcome.ts @@ -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; + } +} diff --git a/src/pages/Simulation/utils/gravityUtils.ts b/src/pages/Simulation/utils/gravityUtils.ts index f54a829..6a39c57 100644 --- a/src/pages/Simulation/utils/gravityUtils.ts +++ b/src/pages/Simulation/utils/gravityUtils.ts @@ -1,6 +1,6 @@ import * as THREE from "three"; -const G = 1; +export const G = 1; const softeningFactor = 0.005; export function calcGravityForce( diff --git a/src/pages/Simulation/utils/mergePlanets.ts b/src/pages/Simulation/utils/mergePlanets.ts new file mode 100644 index 0000000..8bbdf0d --- /dev/null +++ b/src/pages/Simulation/utils/mergePlanets.ts @@ -0,0 +1,56 @@ +import * as THREE from "three"; +import moonTexture from "@/assets/960px-Moon_texture.jpg"; +import type { Planet } from "@/types/planet"; + +export function mergePlanets( + massA: number, + radA: number, + posA: THREE.Vector3, + velA: THREE.Vector3, + yrotA: number, + massB: number, + radB: number, + posB: THREE.Vector3, + velB: THREE.Vector3, + yrotB: number, +): Planet { + const newId = crypto.randomUUID(); + const newName = "mergedPlanet"; + const newTexturePath = moonTexture; + + const newRotationSpeedY = + (massA * radA ** 2 * yrotA + massB * radB ** 2 * yrotB) / + (massA * radA ** 2 + massB * radB ** 2); + + const newRadius = (radA ** 3 + radB ** 3) ** (1 / 3); + const newWidth = 64; + const newHeight = 64; + const newMass = massA + massB; + + const newPosition = new THREE.Vector3() + .addVectors( + posA.clone().multiplyScalar(massA), + posB.clone().multiplyScalar(massB), + ) + .divideScalar(newMass); + + const newVelocity = new THREE.Vector3() + .addVectors( + velA.clone().multiplyScalar(massA), + velB.clone().multiplyScalar(massB), + ) + .divideScalar(newMass); + + return { + id: newId, + name: newName, + texturePath: newTexturePath, + rotationSpeedY: newRotationSpeedY, + radius: newRadius, + width: newWidth, + height: newHeight, + position: newPosition, + velocity: newVelocity, + mass: newMass, + }; +}