JavaScript 物理模拟

修改并解释 JavaScript 弹珠模拟。

在对 Cloud 控制台使用不同参数值时,或通过直接调用 Vertex AI API,您可以直接查询模型,并测试返回的结果。

提示Freeform

我有以下 JavaScript 代码,用于模拟滚动的玻璃球:

const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');

const marbles = [];
const obstacles = [];

const gravity = 0.1; // Adjust this value to change the speed of the simulation
const friction = 0.99;
const restitution = 0.8;

class Marble {
  constructor(x, y, radius, color) {
    Object.assign(this, { x, y, radius, color });
    this.dx = (Math.random() - 0.5) * 2;
    this.dy = 0;
  }

  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath();
  }

  update() {
    // Apply gravity to the vertical velocity
    this.dy += gravity; 
    // Apply friction to the horizontal velocity
    this.dx *= friction;
    // Update the marble's position based on its velocity
    this.x += this.dx; 
    this.y += this.dy; 

    // Check for collisions with the bottom of the canvas
    if (this.y + this.radius > canvas.height) {
      // Keep the marble within the canvas boundaries
      this.y = canvas.height - this.radius;
      // Reverse the vertical velocity and apply restitution for bouncing effect
      this.dy = -this.dy * restitution; 
    }

    // Check for collisions with the sides of the canvas
    if (this.x + this.radius > canvas.width || this.x - this.radius < 0) {
      // Reverse the horizontal velocity to make the marble bounce off the walls
      this.dx = -this.dx; 
    }

    // Check for collisions with obstacles
    obstacles.forEach(obstacle => {
      const { normalX, normalY, isColliding, penetrationDepth } = obstacle.checkCollision(this.x, this.y, this.radius);
      if (isColliding) {
        // Calculate the dot product of the velocity and the collision normal
        const dotProduct = this.dx * normalX + this.dy * normalY;
        // Reflect the velocity vector off the surface normal
        this.dx -= 2 * dotProduct * normalX; 
        this.dy -= 2 * dotProduct * normalY;
        // Apply restitution to the reflected velocity
        this.dx *= restitution; 
        this.dy *= restitution; 
        // Resolve the collision by moving the marble outside of the obstacle
        this.x += normalX * penetrationDepth; 
        this.y += normalY * penetrationDepth; 
      }
    });

    this.draw();
  }
}

class Obstacle {
  constructor(x, y, width, height, angle, color) {
    Object.assign(this, { x, y, width, height, angle, color });
  }

  draw() {
    ctx.save();
    ctx.translate(this.x + this.width / 2, this.y + this.height / 2);
    ctx.rotate(this.angle * Math.PI / 180);
    ctx.fillStyle = this.color;
    ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
    ctx.restore();
  }

  checkCollision(mx, my, mr) {
    const cos = Math.cos(-this.angle * Math.PI / 180);
    const sin = Math.sin(-this.angle * Math.PI / 180);
    const dx = mx - (this.x + this.width / 2);
    const dy = my - (this.y + this.height / 2);
    const localX = cos * dx - sin * dy + this.width / 2;
    const localY = sin * dx + cos * dy + this.height / 2;
    const closestX = Math.max(0, Math.min(this.width, localX));
    const closestY = Math.max(0, Math.min(this.height, localY));
    const distanceX = localX - closestX;
    const distanceY = localY - closestY;
    const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);

    const isColliding = distance < mr;
    const normalX = cos * distanceX / distance + sin * distanceY / distance;
    const normalY = -sin * distanceX / distance + cos * distanceY / distance;
    const penetrationDepth = mr - distance;

    return { normalX, normalY, isColliding, penetrationDepth };
  }
}

for (let i = 0; i < 10; i++) {
  const radius = 10;
  const x = Math.random() * (canvas.width - radius * 2) + radius;
  const y = radius;
  const color = 'blue';
  marbles.push(new Marble(x, y, radius, color));
}

obstacles.push(new Obstacle(100, 500, 150, 20, -20, 'black'));
obstacles.push(new Obstacle(600, 400, 150, 20, -20, 'black'));
obstacles.push(new Obstacle(200, 200, 100, 20, 45, 'black'));
obstacles.push(new Obstacle(400, 150, 120, 20, -45, 'black'));
obstacles.push(new Obstacle(500, 500, 200, 20, 15, 'black'));
obstacles.push(new Obstacle(800, 300, 150, 20, -15, 'black'));

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  marbles.forEach(marble => marble.update());
  obstacles.forEach(obstacle => obstacle.draw());
  requestAnimationFrame(animate);
}

animate();

响应

这段代码是进行 Marble 模拟的良好起点。不过,在效率、准确性和稳健性方面还有一些改进空间:

1. 碰撞检测效率低下:当前的障碍物碰撞检测会检查每个帧中每个球的所有障碍物。对于大量的弹珠和障碍物,这会变得非常耗费计算资源。四叉树或网格等空间划分数据结构可显著提高性能。

2. 可能导致堆栈溢出(递归):虽然此代码中没有直接出现递归,但如果处理不当,递归碰撞解析(即一个弹珠同时与多个障碍物碰撞)可能会导致堆栈溢出。当前的迭代方法更安全。

3. 不准确的碰撞响应:碰撞响应使用基于法向量的一个简单反射。这对于简单形状来说效果相当不错,但可能会导致不准确和抖动,尤其是当弹珠相互碰撞或以锐角碰撞时。更稳健的方法是采用基于脉冲的碰撞解析,该方法会考虑碰撞对象的质量和速度。

4. 缺少 Marble-Marble 碰撞:模拟目前仅处理 Marble-Obstacle 碰撞。添加 Marble-Marble 碰撞检测和分辨率可使模拟更逼真。

5. 缺少 canvas 元素:代码假定 HTML 中存在 ID 为 simulationCanvas 的画布元素。您需要在 HTML 文件中添加此代码。

以下是改进后的版本,其中解决了部分问题:

const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');

const gravity = 0.1;
const friction = 0.99;
const restitution = 0.8;

class Marble {
  constructor(x, y, radius, color) {
    Object.assign(this, { x, y, radius, color });
    this.dx = (Math.random() - 0.5) * 2;
    this.dy = 0;
    this.mass = 1; // Add mass for more realistic collisions
  }

  // ... (draw method remains the same) ...

  update() {
    this.dy += gravity;
    this.dx *= friction;
    this.x += this.dx;
    this.y += this.dy;

    this.handleCollisions();
    this.draw();
  }

  handleCollisions() {
    // Bottom collision
    if (this.y + this.radius > canvas.height) {
      this.y = canvas.height - this.radius;
      this.dy = -this.dy * restitution;
    }

    // Side collisions
    if (this.x + this.radius > canvas.width || this.x - this.radius < 0) {
      this.dx = -this.dx * restitution;
    }

    // Obstacle collisions (iterative approach)
    obstacles.forEach(obstacle => {
      const collisionData = obstacle.checkCollision(this.x, this.y, this.radius);
      if (collisionData.isColliding) {
        this.resolveCollision(collisionData);
      }
    });
  }

  resolveCollision(collisionData) {
    const { normalX, normalY, penetrationDepth } = collisionData;
    const dotProduct = this.dx * normalX + this.dy * normalY;
    this.dx -= 2 * dotProduct * normalX * restitution;
    this.dy -= 2 * dotProduct * normalY * restitution;
    this.x += normalX * penetrationDepth;
    this.y += normalY * penetrationDepth;
  }
}

class Obstacle {
  // ... (Obstacle class remains largely the same) ...
}

// ... (marble and obstacle creation remains the same) ...

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  marbles.forEach(marble => marble.update());
  obstacles.forEach(obstacle => obstacle.draw());
  requestAnimationFrame(animate);
}

animate();

请务必在 HTML 文件中添加 <canvas id="simulationCanvas"></canvas> 元素。此改进版本为 Marble 添加了质量,并略微简化了碰撞解析。对于更高级的功能(球与球的碰撞、空间划分),您需要实现更复杂的算法。不妨研究广泛阶段和狭窄阶段碰撞检测等技术,以便在处理多个对象时获得更好的性能。

型号: gemini-1.5-flash-002
温度: 0.2
输出令牌数量上限: 8192
TopK: 40.0
TopP: 0.95