7
8

衝突の美学。異なる質量を持つボールが3D空間内で衝突し、反発する様子をシミュレートするゲーム。

Last updated at Posted at 2024-08-27

スクリーンショット 2024-08-28 054019.png

image.png

異なる質量を持つボールが3D空間内で衝突し、反発する様子をシミュレートするゲーム。

衝突の際に、異なる質量のボールが様々な方向から衝突する場合の精密な反発シミュレーションを行うために、運動量保存の法則とエネルギー保存の法則に基づく計算を行っています。

コードをメモ帳などのテキストエディタに貼り付け、ファイルを「index.html」などの拡張子が.htmlのファイルとして保存します。その後、保存したファイルをブラウザで開けば、コードが実行されます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3次元ボールシミュレーション(精密な衝突計算)</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #000; /* 背景色を黒に設定 */
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <canvas id="gameCanvas"></canvas>

    <!-- Three.js ライブラリを読み込む -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>

    <script>
        // キャンバス要素を取得
        const canvas = document.getElementById('gameCanvas');
        const width = window.innerWidth;
        const height = window.innerHeight;

        // シーン、カメラ、レンダラーの作成
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({ canvas });

        renderer.setSize(width, height); // キャンバスのサイズをウィンドウサイズに合わせる
        document.body.appendChild(renderer.domElement);

        camera.position.z = 500; // カメラをZ軸上に500だけ離して配置

        // ベクトル計算を行うためのヘルパー関数群

        // ベクトルの減算
        function vectorSubtract(v1, v2) {
            return new THREE.Vector3(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
        }

        // ベクトルの加算
        function vectorAdd(v1, v2) {
            return new THREE.Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
        }

        // ベクトルのスカラー倍
        function vectorMultiplyScalar(v, scalar) {
            return new THREE.Vector3(v.x * scalar, v.y * scalar, v.z * scalar);
        }

        // ベクトルの内積
        function dotProduct(v1, v2) {
            return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
        }

        // ボールクラスの定義
        class Ball {
            constructor(x, y, z, radius, mass, dx, dy, dz, color) {
                // ボールの形状を球体で作成
                const geometry = new THREE.SphereGeometry(radius, 32, 32);
                // ボールの材質を設定し、色をランダムに指定
                const material = new THREE.MeshBasicMaterial({ color: color });
                this.mesh = new THREE.Mesh(geometry, material);
                this.mesh.position.set(x, y, z); // ボールの初期位置を設定
                this.radius = radius; // ボールの半径
                this.mass = mass; // ボールの質量
                this.velocity = new THREE.Vector3(dx, dy, dz); // ボールの初速度
                scene.add(this.mesh); // ボールをシーンに追加
            }

            // ボールの動きを更新するメソッド
            update(balls) {
                // ボールの位置を速度ベクトルに基づいて更新
                this.mesh.position.add(this.velocity);

                // 画面の端でボールが反射する処理
                if (this.mesh.position.x + this.radius > width / 2 || this.mesh.position.x - this.radius < -width / 2) {
                    this.velocity.x = -this.velocity.x; // X軸方向の速度を反転
                }
                if (this.mesh.position.y + this.radius > height / 2 || this.mesh.position.y - this.radius < -height / 2) {
                    this.velocity.y = -this.velocity.y; // Y軸方向の速度を反転
                }
                if (this.mesh.position.z + this.radius > 500 || this.mesh.position.z - this.radius < -500) {
                    this.velocity.z = -this.velocity.z; // Z軸方向の速度を反転
                }

                // 他のボールとの衝突判定と処理
                for (let i = 0; i < balls.length; i++) {
                    const other = balls[i];
                    if (this === other) continue; // 自分自身との衝突は無視

                    // 2つのボール間の位置ベクトルの差を計算
                    const deltaPos = vectorSubtract(this.mesh.position, other.mesh.position);
                    const distance = deltaPos.length(); // 2つのボール間の距離を計算

                    // 衝突が発生しているか判定
                    if (distance < this.radius + other.radius) {
                        // 衝突の解決 (運動量保存則に基づく)
                        const normal = deltaPos.normalize(); // 衝突の法線ベクトルを計算
                        const relativeVelocity = vectorSubtract(this.velocity, other.velocity); // 相対速度を計算
                        const velocityAlongNormal = dotProduct(relativeVelocity, normal); // 法線方向の速度成分

                        if (velocityAlongNormal > 0) continue; // 既に離れている場合は無視

                        const e = 1.0; // 反発係数 (1.0は完全弾性衝突)
                        const j = -(1 + e) * velocityAlongNormal / (1 / this.mass + 1 / other.mass);

                        const impulse = vectorMultiplyScalar(normal, j); // 衝突時のインパルスを計算
                        this.velocity.add(vectorMultiplyScalar(impulse, 1 / this.mass)); // 自分の速度を更新
                        other.velocity.sub(vectorMultiplyScalar(impulse, 1 / other.mass)); // 相手の速度を更新

                        // ボールが重ならないように位置を調整
                        const overlap = 0.5 * (this.radius + other.radius - distance);
                        this.mesh.position.add(vectorMultiplyScalar(normal, overlap)); // 自分を動かす
                        other.mesh.position.sub(vectorMultiplyScalar(normal, overlap)); // 相手を動かす
                    }
                }
            }
        }

        const balls = []; // ボールの配列
        const ballCount = 500; // ボールの数

        // ランダムな位置、速度、質量でボールを生成
        for (let i = 0; i < ballCount; i++) {
            const radius = Math.random() * 20 + 10; // ボールの半径をランダムに設定
            const mass = radius; // 質量は半径に比例
            const x = (Math.random() - 0.5) * width; // X座標の初期位置をランダムに設定
            const y = (Math.random() - 0.5) * height; // Y座標の初期位置をランダムに設定
            const z = (Math.random() - 0.5) * 1000; // Z座標の初期位置をランダムに設定
            const dx = (Math.random() - 0.5) * 4; // X方向の初速度をランダムに設定
            const dy = (Math.random() - 0.5) * 4; // Y方向の初速度をランダムに設定
            const dz = (Math.random() - 0.5) * 4; // Z方向の初速度をランダムに設定
            const color = Math.random() * 0xffffff; // 色をランダムに設定

            balls.push(new Ball(x, y, z, radius, mass, dx, dy, dz, color)); // ボールを作成して配列に追加
        }

        // アニメーションループ
        function animate() {
            requestAnimationFrame(animate); // 次のフレームで再度animateを呼び出す
            balls.forEach(ball => ball.update(balls)); // 各ボールの位置と速度を更新
            renderer.render(scene, camera); // シーンをレンダリング
        }

        animate(); // アニメーションを開始
    </script>
</body>
</html>


7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8