javascript - 在 JS Canvas 动画中为台球物理添加重力

标签 javascript html5-canvas game-physics particle-system

我正在尝试使用 Javascript 编写一个小型物理演示。我有多个球,它们彼此弹跳得很好,但当我尝试增加重力时,事情就出了问题。

我试图在它们撞击后保存动量,但是当我向每个物体添加恒定的重力时,物理原理开始崩溃。

这是我的代码:

class Ball {
  constructor ({
    x,
    y,
    vx,
    vy,
    radius,
    color = 'red',
  }) {
    this.x = x
    this.y = y
    this.vx = vx
    this.vy = vy
    this.radius = radius
    this.color = color

    this.mass = 1
  }
  render (ctx) {
    ctx.save()
    ctx.fillStyle = this.color
    ctx.strokeStyle = this.color
    ctx.translate(this.x, this.y)
    ctx.strokeRect(-this.radius, -this.radius, this.radius * 2, this.radius * 2)
    ctx.beginPath()
    ctx.arc(0, 0, this.radius, Math.PI * 2, false)
    ctx.closePath()
    ctx.fill()
    ctx.restore()
    return this
  }
  getBounds () {
    return {
      x: this.x - this.radius,
      y: this.y - this.radius,
      width: this.radius * 2,
      height: this.radius * 2
    }
  }
}

const intersects = (rectA, rectB) => {
  return !(rectA.x + rectA.width < rectB.x ||
           rectB.x + rectB.width < rectA.x ||
           rectA.y + rectA.height < rectB.y ||
           rectB.y + rectB.height < rectA.y)
}

const checkWall = (ball) => {
  const bounceFactor = 0.5
  if (ball.x + ball.radius > canvas.width) {
    ball.x = canvas.width - ball.radius
    ball.vx *= -bounceFactor
  }
  if (ball.x - ball.radius < 0) {
    ball.x = ball.radius
    ball.vx *= -bounceFactor
  }
  if (ball.y + ball.radius > canvas.height) {
    ball.y = canvas.height - ball.radius
    ball.vy *= -1
  }
  if (ball.y - ball.radius < 0) {
    ball.y = ball.radius
    ball.vy *= -bounceFactor
  }
}

const rotate = (x, y, sin, cos, reverse) => {
  return {
     x: reverse ? x * cos + y * sin : x * cos - y * sin,
     y: reverse ? y * cos - x * sin : y * cos + x * sin
   }
}

const checkCollision = (ball0, ball1, dt) => {
  const dx = ball1.x - ball0.x
  const dy = ball1.y - ball0.y
  const dist = Math.sqrt(dx * dx + dy * dy)
  const minDist = ball0.radius + ball1.radius
  if (dist < minDist) {
    //calculate angle, sine, and cosine
    const angle = Math.atan2(dy, dx)
    const sin = Math.sin(angle)
    const cos = Math.cos(angle)
    
    //rotate ball0's position
    const pos0 = {x: 0, y: 0}
    
    //rotate ball1's position
    const pos1 = rotate(dx, dy, sin, cos, true)
    
    //rotate ball0's velocity
    const vel0 = rotate(ball0.vx, ball0.vy, sin, cos, true)
    
    //rotate ball1's velocity
    const vel1 = rotate(ball1.vx, ball1.vy, sin, cos, true)
    
    //collision reaction
    const vxTotal = (vel0.x - vel1.x)
    vel0.x = ((ball0.mass - ball1.mass) * vel0.x + 2 * ball1.mass * vel1.x) /
      (ball0.mass + ball1.mass)
    vel1.x = vxTotal + vel0.x
    
    const absV = Math.abs(vel0.x) + Math.abs(vel1.x)
    const overlap = (ball0.radius + ball1.radius) - Math.abs(pos0.x - pos1.x)
    pos0.x += vel0.x / absV * overlap
    pos1.x += vel1.x / absV * overlap
    
    //rotate positions back
    const pos0F = rotate(pos0.x, pos0.y, sin, cos, false)
    const pos1F = rotate(pos1.x, pos1.y, sin, cos, false)
    
    //adjust positions to actual screen positions
    ball1.x = ball0.x + pos1F.x
    ball1.y = ball0.y + pos1F.y
    ball0.x = ball0.x + pos0F.x
    ball0.y = ball0.y + pos0F.y
    
    //rotate velocities back
    const vel0F = rotate(vel0.x, vel0.y, sin, cos, false)
    const vel1F = rotate(vel1.x, vel1.y, sin, cos, false)
    
    ball0.vx = vel0F.x
    ball0.vy = vel0F.y
    ball1.vx = vel1F.x
    ball1.vy = vel1F.y
  }
}


const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')

let oldTime = 0

canvas.width = innerWidth
canvas.height = innerHeight
document.body.appendChild(canvas)

const log = document.getElementById('log')

const balls = new Array(36).fill(null).map(_ => new Ball({
  x: Math.random() * innerWidth,
  y: Math.random() * innerHeight,
  vx: (Math.random() * 2 - 1) * 5,
  vy: (Math.random() * 2 - 1) * 5,
  radius: 20,
}))

requestAnimationFrame(updateFrame)

function updateFrame (ts) {
  const dt = ts - oldTime
  oldTime = ts

  ctx.clearRect(0, 0, innerWidth, innerHeight)
  
  for (let i = 0; i < balls.length; i++) {
    const ball = balls[i]
    // ADD GRAVITY HERE
    ball.vy += 2
    ball.x += ball.vx * (dt * 0.005)
    ball.y += ball.vy * (dt * 0.005)
    checkWall(ball)
  }
  
  for (let i = 0; i < balls.length; i++) {
    const ball0 = balls[i]
    for (let j = i + 1; j < balls.length; j++) {
      const ball1 = balls[j]
      // CHECK FOR COLLISIONS HERE
      checkCollision(ball0, ball1, dt)
    }
  }
  
  for (let i = 0; i < balls.length; i++) {
    const ball = balls[i]
    ball.render(ctx)
  }
  
  // const dist = ball2.x - ball1.x
//   if (Math.abs(dist) < ball1.radius + ball2.radius) {
//     const vxTotal = ball1.vx - ball2.vx
//     ball1.vx = ((ball1.mass - ball2.mass) * ball1.vx + 2 * ball2.mass * ball2.vx) / (ball1.mass + ball2.mass)
//     ball2.vx = vxTotal + ball1.vx

//     ball1.x += ball1.vx
//     ball2.x += ball2.vx
//   }
  

//     ball.vy += 0.5
//     ball.x += ball.vx
//     ball.y += ball.vy

//     

//     ball.render(ctx)
  
  requestAnimationFrame(updateFrame)
}
* { margin: 0; padding: 0; }

正如您所看到的,我有 checkCollision 辅助方法,它可以计算一个球与另一个球碰撞后的动能和新速度。我的更新循环如下所示:

  // add velocities to balls position
  // check if its hitting any wall and bounce it back
  for (let i = 0; i < balls.length; i++) {
    const ball = balls[i]
    // Add constant gravity to the vertical velocity
    // When balls stack up on each other at the bottom, the gravity is still applied and my
    // "checkCollision" method freaks out and the physics start to explode
    ball.vy += 0.8
    ball.x += ball.vx * (dt * 0.005)
    ball.y += ball.vy * (dt * 0.005)
    checkWall(ball)
  }
  
  for (let i = 0; i < balls.length; i++) {
    const ball0 = balls[i]
    for (let j = i + 1; j < balls.length; j++) {
      const ball1 = balls[j]
      // Check collisions between two balls
      checkCollision(ball0, ball1, dt)
    }
  }
  
  // Finally render the ball on-screen
  for (let i = 0; i < balls.length; i++) {
    const ball = balls[i]
    ball.render(ctx)
  }

如何计算重力,同时防止球开始相互堆叠时物理爆炸?

看来重力正在与“checkCollision”方法发生碰撞。 checkCollision 方法尝试将它们移回原位,但恒定的重力会覆盖它并继续将它们向下拉。

编辑:经过一番阅读后,我明白一些 Verlet 集成是有序的,但我很难理解它。

for (let i = 0; i < balls.length; i++) {
        const ball = balls[i]
        // This line needs to be converted to verlet motion?
        ball.vy += 2
        ball.x += ball.vx * (dt * 0.005)
        ball.y += ball.vy * (dt * 0.005)
        checkWall(ball)
      }

最佳答案

球不重叠

碰撞测试存在一个根本缺陷,因为只有当 2 个球重叠时才计算碰撞。在现实世界中,这种情况永远不会发生。

当许多球相互作用时,“重叠碰撞”的结果将导致系统总能量不守恒的行为。

按碰撞顺序解决

您可以解决碰撞,使球永远不会重叠,但每帧的处理量是不确定的,随着球密度的增加呈指数增长。

该方法是在帧之间的时间内定位球之间的第一次碰撞。解决该碰撞,然后使用该碰撞的新位置查找距离上一次最近的时间上最接近的下一个碰撞。执行此操作,直到该帧没有待处理的冲突为止。 (还有更多)结果是模拟永远不会处于球重叠的不可能状态。

查看我的Pool simulator CodePen 上使用此方法来模拟台球。球可以有任何速度并且始终正确解析。

Verlet 集成。

但是,您可以通过使用 Verlet 积分来减少重叠碰撞的噪音,这将使球的总能量保持在更稳定的水平。

为此,我们引入了球的 2 个新属性,pxpy 来保存球的先前位置。

每一帧我们都会将球速度计算为当前位置和新位置之间的差值。该速度用于计算框架。

当球改变方向(撞到墙壁或另一个球)时,我们还需要更改球之前的位置以匹配它在新轨迹上的位置。

使用恒定时间步长。

使用基于自上一帧以来的时间的时间步也会引入噪声,不应在重叠碰撞方法中使用。

减少时间,增加迭代

为了进一步消除噪音,您需要降低球的整体速度,以减少它们重叠的数量,从而更接近地表现出它们在 ballA.radius + ballB.radius 点发生碰撞的情况。分开。另外,您应该将每个球与其他球进行测试,而不仅仅是将球与 balls 数组中位于其上方的球进行测试。

为了保持动画速度,您需要每帧解决几次球 V 球 V 墙碰撞问题。该示例执行 5。最佳值取决于球的总能量、可接受的噪音水平以及运行该球的设备的 CPU 功率。

准确性很重要

你的碰撞功能也很出色。我快速浏览了一下,看起来不太对劲。我在示例中添加了一个替代方案。

当球撞到墙壁时,它会在帧之间的某个时间发生。您必须将球移离墙壁正确的距离。不这样做就像模拟一个球,每次撞击时都会稍微粘在墙上,与实际发生的情况进一步偏离。

示例

这是对原始代码的重写。单击 Canvas 以添加一些能量。

const ctx = canvas.getContext("2d");
const BOUNCE = 0.75;
const resolveSteps = 5;
var oldTime = 0;
const $setOf = (count, fn = (i) => i) => {var a = [], i = 0; while (i < count) { a.push(fn(i++)) } return a };
const $rand  = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const $randP  = (min = 1, max = min + (min = 0), p = 2) => Math.random() ** p * (max - min) + min;
var W = canvas.width, H = canvas.height;
const BALL_COUNT = 80;
const BALL_RADIUS = 15, BALL_MIN_RADIUS = 6;
const GRAV = 0.5 / resolveSteps;
requestAnimationFrame(updateFrame);

canvas.addEventListener("click", () => {
  balls.forEach(b => {
    b.px = b.x + (Math.random() * 18 - 9);
    b.py = b.y + (Math.random() * -18);
  })
});
class Ball {
  constructor({x, y, vx, vy, radius}) {
    this.x = x;
    this.y = y;
    this.px = x - vx;
    this.py = y - vy;
    this.vx = vx;
    this.vy = vy;
    this.radius = radius;
    this.mass = radius * radius * Math.PI * (4 / 3); // use sphere volume as mass
  }
  render(ctx) {
    ctx.moveTo(this.x + this.radius, this.y);
    ctx.arc(this.x, this.y, this.radius, Math.PI * 2, false);
  }
  move() {
    this.vx = this.x - this.px;
    this.vy = this.y - this.py;
    this.vy += GRAV;
    this.px = this.x;
    this.py = this.y;
    this.x += this.vx;
    this.y += this.vy;
    this.checkWall();
  }
  checkWall() {
    const ball = this;
    const top = ball.radius;
    const left = ball.radius;
    const bottom = H - ball.radius;
    const right = W - ball.radius;
    if (ball.x > right) {
      const away = (ball.x - right) * BOUNCE;
      ball.x = right - away;
      ball.vx = -Math.abs(ball.vx) * BOUNCE;
      ball.px = ball.x - ball.vx;
    } else if (ball.x < left) {
      const away = (ball.x - left) * BOUNCE;
      ball.x = left + away;
      ball.vx = Math.abs(ball.vx) * BOUNCE;
      ball.px = ball.x - ball.vx;
    }
    if (ball.y > bottom) {
      const away = (ball.y - bottom) * BOUNCE;
      ball.y = bottom - away;
      ball.vy = -Math.abs(ball.vy) * BOUNCE;
      ball.py = ball.y - ball.vy;
    } else if (ball.y < top) {
      const away = (ball.y - top) * BOUNCE;
      ball.y = top + away;
      ball.vy = Math.abs(ball.vy) * BOUNCE;
      ball.py = ball.y - ball.vy;
    }
  }
  collisions() {
    var b, dx, dy, nx, ny, cpx, cpy, p, d, i = 0;
    var {x, y, vx, vy, px, py, radius: r, mass: m} = this;
    while (i < balls.length) {
      b = balls[i++];
      if (this !== b) {
        const rr = r + b.radius;
        if (x + rr > b.x && x < b.x + rr && y + rr > b.y && y < b.y + rr) {
          dx = x - b.x;
          dy = y - b.y;
          d = (dx * dx + dy * dy) ** 0.5;
          if (d < rr) {
            nx = (b.x - x) / d;
            ny = (b.y - y) / d;
            p = 2 * (vx * nx + vy * ny - b.vx * nx - b.vy * ny) / (m + b.mass);
            cpx = (x * b.radius + b.x * r) / rr;
            cpy = (y * b.radius + b.y * r) / rr;
            x = cpx + r * (x - b.x) / d;
            y = cpy + r * (y - b.y) / d;
            b.x = cpx + b.radius * (b.x - x) / d;
            b.y = cpy + b.radius * (b.y - y) / d;
            px = x - (vx -= p * b.mass * nx);
            py = y - (vy -= p * b.mass * ny);
            b.px = b.x - (b.vx += p * m * nx);
            b.py = b.y - (b.vy += p * m * ny);
          }
        }
      }
    }
    this.x = x;
    this.y = y;
    this.px = px;
    this.py = py;
    this.vx = vx;
    this.vy = vy;
    this.checkWall();
  }
}
const balls = (() => {
  return $setOf(BALL_COUNT, () => new Ball({
    x: $rand(BALL_RADIUS, W - BALL_RADIUS),
    y: $rand(BALL_RADIUS, H - BALL_RADIUS),
    vx: $rand(-2, 2),
    vy: $rand(-2, 2),
    radius: $randP(BALL_MIN_RADIUS, BALL_RADIUS, 4),
  }));
})();

function updateFrame(ts) {
  var i = 0, j = resolveSteps;
  ctx.clearRect(0, 0, W, H);

  while (i < balls.length) { balls[i++].move() }
  while (j--) {
    i = 0;
    while (i < balls.length) { balls[i++].collisions(balls) }
  }
  ctx.fillStyle = "#0F0";
  ctx.beginPath();
  i = 0;
  while (i < balls.length) { balls[i++].render(ctx) }
  ctx.fill();
  requestAnimationFrame(updateFrame)
}
<canvas id="canvas" width="400" height="180" style="border:1px solid black;"></canvas>
<div style="position: absolute; top: 10px; left: 10px;">Click to stir</div>

关于javascript - 在 JS Canvas 动画中为台球物理添加重力,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65455231/

相关文章:

javascript - 如何从javascript函数中获取值?

javascript - HTML5 Canvas : Get Event when drawing is finished

javascript - 将球从一个点弹到另一个点 - 矢量和加速度

lua - corona sdk中拖动物理对象

Javascript:子弹动画遵循直接路径

javascript - 如何在页面加载时发出请求, react Hook

javascript - encodeForHTMLAttribute 与 encodeForJavaScript

javascript - 无论如何要等待打印窗口在 print.js 中关闭

javascript - 有没有办法在 React 中每次状态更改时不重新加载 Canvas 的一部分?

javascript - 在div中隐藏滚动条