ChatGPT o3-mini が実現！回転する五角形内で物理シミュレーションを行う p5.js スケッチ

![gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82835/15b168cd-6a80-1803-7458-7fd65898f18e.gif)

今回は、ChatGPT o3-mini を利用して、一発ノーミスで作成された p5.js のスケッチをご紹介します。

このスケッチは、[p5.js Web エディタ](https://editor.p5js.org/) にそのまま貼り付けるだけで実行可能です。物理シミュレーションの実装において、重力、摩擦、衝突判定、そして回転する五角形の壁との相互作用など、複雑な要素を一瞬で正確に処理できるクオリティの高さを実感できます！

## スケッチの概要

- **回転する五角形の容器**  
  画面中央に配置された五角形が、一定の角速度でゆっくり回転します。  
  この回転する五角形が、ボールを閉じ込める「容器」として動作します。

- **物理シミュレーション**  
  - **重力**: ボールに下向きの一定加速度を与え、自然な落下運動を再現  
  - **摩擦**: 移動中のボールの速度を徐々に減衰させる効果  
  - **衝突・跳ね返り**:  
    - ボール同士の衝突は、弾性衝突（衝突係数を考慮）で処理  
    - ボールと五角形の壁との衝突では、速度ベクトルが壁面に対して反射され、ボールが容器の外に出ないようになっています

- **初期設定と GUI**  
  - ボールは五角形内部にランダムに配置され、サイズ（半径）は約 10〜20 ピクセル  
  - 初期速度もランダムに設定し、動きにバリエーションを持たせています  
  - 重力、摩擦、反発係数、そして五角形の回転速度をリアルタイムで調整できるように、GUI スライダーを実装しています  
  - 各ボールの色もランダムに生成され、視覚的な変化を楽しめます

## コードの特徴

今回のコードは、物理法則を瞬時にミスなく実装できる高いクオリティが特徴です。  
ChatGPT o3-mini は、衝突判定の処理や法線ベクトルの計算など、細部に至るまで正確に実装できました。  
複雑な物理シミュレーションを一発ノーミスで実現できる点は、非常に驚異的です！

また、このスケッチは [p5.js Web エディタ](https://editor.p5js.org/) でそのまま動作させることが可能です。  
エディタにコードを貼り付けて実行すれば、回転する五角形の中でボールが重力や衝突の影響を受けながら動く様子をすぐに確認できます。

## コード全文
以下に、今回のスケッチのソースコードを掲載します。

```js
/**
 * 回転する五角形の中で、5つのボールが
 * 重力・摩擦・衝突を考慮しながら跳ね回るサンプル
 * （GUIスライダーで物理パラメータを調整できます）
 */

// ----------------------
// グローバル変数
// ----------------------
const NUM_BALLS = 5; // ボールの数

// 物理パラメータ（初期値）
let gravity = 0.2;       // 重力加速度
let friction = 0.99;     // 摩擦係数
let restitution = 0.9;   // 衝突時の反発係数（ボール同士、壁とも）
let rotationSpeed = 0.01; // 五角形の回転速度

// GUIスライダー
let gravitySlider, frictionSlider, restitutionSlider, rotationSpeedSlider;

let balls = [];          // ボール配列
let angle = 0;           // 五角形の回転角度

// 五角形の頂点（中心原点で定義）
let pentagon = [];

function setup() {
  createCanvas(600, 600);

  // GUIスライダーの作成
  gravitySlider = createSlider(0, 1, gravity, 0.01);
  gravitySlider.position(10, height + 10);
  gravitySlider.style('width', '150px');

  frictionSlider = createSlider(0.8, 1, friction, 0.001);
  frictionSlider.position(170, height + 10);
  frictionSlider.style('width', '150px');

  restitutionSlider = createSlider(0.5, 1, restitution, 0.01);
  restitutionSlider.position(330, height + 10);
  restitutionSlider.style('width', '150px');

  rotationSpeedSlider = createSlider(0, 0.05, rotationSpeed, 0.001);
  rotationSpeedSlider.position(490, height + 10);
  rotationSpeedSlider.style('width', '150px');

  // 五角形の頂点を定義（中心(0,0)基準、半径 200）
  const r = 200;
  for (let i = 0; i < 5; i++) {
    let theta = TWO_PI * (i / 5) - HALF_PI; // 真上からスタート
    let x = r * cos(theta);
    let y = r * sin(theta);
    pentagon.push(createVector(x, y));
  }

  // ボールを初期化（五角形内にランダム配置）
  for (let i = 0; i < NUM_BALLS; i++) {
    let radius = random(10, 20);
    // とりあえずキャンバス中央付近に配置
    let x = width / 2 + random(-100, 100);
    let y = height / 2 + random(-100, 100);
    // 初期速度もランダムに
    let vx = random(-2, 2);
    let vy = random(-2, 2);

    balls.push(new Ball(x, y, vx, vy, radius));
  }
}

function draw() {
  background(220);

  // GUIスライダーの値を物理パラメータに反映
  gravity = gravitySlider.value();
  friction = frictionSlider.value();
  restitution = restitutionSlider.value();
  rotationSpeed = rotationSpeedSlider.value();

  // 五角形を回転させる
  angle += rotationSpeed;

  // 各ボールの物理更新
  for (let b of balls) {
    b.update();
  }
  // ボール同士の衝突判定
  checkBallCollisions();

  // ボールと五角形の壁の衝突判定
  for (let b of balls) {
    collideWithPentagon(b);
  }

  // 五角形の描画
  push();
  translate(width / 2, height / 2);
  rotate(angle);
  stroke(0);
  fill(200, 100, 100);
  beginShape();
  for (let v of pentagon) {
    vertex(v.x, v.y);
  }
  endShape(CLOSE);
  pop();

  // ボールを前面に描画（五角形より上に表示）
  for (let b of balls) {
    b.show();
  }

  // オプション：各パラメータの値を画面下部に表示
  displayParameters();
}

// ----------------------
// ボールクラス
// ----------------------
class Ball {
  constructor(x, y, vx, vy, r) {
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.r = r;
    // ランダムな色
    this.col = color(random(255), random(255), random(255));
  }

  update() {
    // 重力の適用
    this.vy += gravity;

    // 摩擦の適用
    this.vx *= friction;
    this.vy *= friction;

    // 位置更新
    this.x += this.vx;
    this.y += this.vy;
  }

  show() {
    push();
    noStroke();
    fill(this.col);
    ellipse(this.x, this.y, this.r * 2, this.r * 2);
    pop();
  }
}

// ----------------------
// ボール同士の衝突処理
// ----------------------
function checkBallCollisions() {
  for (let i = 0; i < balls.length; i++) {
    for (let j = i + 1; j < balls.length; j++) {
      let b1 = balls[i];
      let b2 = balls[j];

      let dx = b2.x - b1.x;
      let dy = b2.y - b1.y;
      let distSq = dx * dx + dy * dy;
      let minDist = b1.r + b2.r;

      if (distSq < minDist * minDist) {
        let dist = sqrt(distSq);
        // 重なり解消のための押し出し
        let overlap = (minDist - dist) * 0.5;
        let nx = dx / dist;
        let ny = dy / dist;

        // 位置調整
        b1.x -= nx * overlap;
        b1.y -= ny * overlap;
        b2.x += nx * overlap;
        b2.y += ny * overlap;

        // 簡易的な等質量の弾性衝突
        let dot1 = b1.vx * nx + b1.vy * ny;
        let dot2 = b2.vx * nx + b2.vy * ny;

        // 衝突後の法線方向速度成分を交換（反発係数考慮）
        let m1 = dot2 * restitution;
        let m2 = dot1 * restitution;

        b1.vx += (m1 - dot1) * nx;
        b1.vy += (m1 - dot1) * ny;
        b2.vx += (m2 - dot2) * nx;
        b2.vy += (m2 - dot2) * ny;
      }
    }
  }
}

// ----------------------
// ボールと回転五角形の壁の衝突処理
// ----------------------
function collideWithPentagon(ball) {
  // 回転後の五角形の頂点座標を計算
  let rotatedPoints = [];
  for (let i = 0; i < pentagon.length; i++) {
    let px = pentagon[i].x;
    let py = pentagon[i].y;
    let rx = px * cos(angle) - py * sin(angle) + width / 2;
    let ry = px * sin(angle) + py * cos(angle) + height / 2;
    rotatedPoints.push(createVector(rx, ry));
  }

  // 各辺との衝突判定
  for (let i = 0; i < rotatedPoints.length; i++) {
    let p1 = rotatedPoints[i];
    let p2 = rotatedPoints[(i + 1) % rotatedPoints.length];

    // 線分とボール中心の最短距離を計算
    let collisionInfo = lineSegmentDistance(ball.x, ball.y, p1.x, p1.y, p2.x, p2.y);
    if (collisionInfo.distance < ball.r) {
      // 衝突時：法線方向へボールを押し出し、速度を反射させる
      let nx = collisionInfo.nx;
      let ny = collisionInfo.ny;
      let diff = ball.r - collisionInfo.distance;

      ball.x += nx * diff;
      ball.y += ny * diff;

      let dot = ball.vx * nx + ball.vy * ny;
      ball.vx -= (1 + restitution) * dot * nx;
      ball.vy -= (1 + restitution) * dot * ny;
    }
  }
}

// ----------------------
// 線分と点の最短距離および法線ベクトルを計算する関数
// ----------------------
function lineSegmentDistance(px, py, x1, y1, x2, y2) {
  let vx = x2 - x1;
  let vy = y2 - y1;
  let wx = px - x1;
  let wy = py - y1;

  let lenSq = vx * vx + vy * vy;
  let t = (wx * vx + wy * vy) / lenSq;
  t = constrain(t, 0, 1);

  let cx = x1 + vx * t;
  let cy = y1 + vy * t;

  let dx = px - cx;
  let dy = py - cy;
  let dist = sqrt(dx * dx + dy * dy);

  // 法線ベクトル（ゼロ除算回避）
  let nx = dx / (dist || 1);
  let ny = dy / (dist || 1);

  return {
    distance: dist,
    nx: nx,
    ny: ny
  };
}

// ----------------------
// 画面下部に各パラメータの現在値を表示する関数
// ----------------------
function displayParameters() {
  fill(0);
  noStroke();
  textSize(14);
  textAlign(LEFT, CENTER);
  let info = 
    "Gravity: " + nf(gravity, 1, 2) +
    "    Friction: " + nf(friction, 1, 3) +
    "    Restitution: " + nf(restitution, 1, 2) +
    "    Rotation Speed: " + nf(rotationSpeed, 1, 3);
  text(info, 10, height - 20);
}
```

## まとめ

今回ご紹介したスケッチは、ChatGPT o3-mini が物理法則の実装を一発ノーミスで完遂できる実力を証明するものです。  
複雑な衝突判定や反発処理、回転するコンテナ内でのボールの動きを正確にシミュレーションできるこのコードは、インタラクティブなアート作品や物理シミュレーションのプロジェクトにも大いに役立つでしょう。

ぜひ、[p5.js Web エディタ](https://editor.p5js.org/) にコードを貼り付け、動作を確認してみてください！  
これを機に、物理シミュレーションやインタラクティブコンテンツの作成に挑戦してみてはいかがでしょうか？

Happy Coding!