物理模拟

学习Canvas物理模拟技术,掌握物理引擎基础、运动学和动力学模拟方法。物理模拟基于物理规律计算运动,创造真实的动画效果,是游戏开发的核心技术。

物理模拟基础

运动学

运动学描述物体的运动,不考虑力的作用。

class Kinematics {
  constructor() {
    this.position = { x: 0, y: 0 }
    this.velocity = { x: 0, y: 0 }
    this.acceleration = { x: 0, y: 0 }
  }
  
  update(dt) {
    this.velocity.x += this.acceleration.x * dt
    this.velocity.y += this.acceleration.y * dt
    this.position.x += this.velocity.x * dt
    this.position.y += this.velocity.y * dt
  }
}

动力学

动力学考虑力的作用,使用牛顿运动定律。

class Dynamics {
  constructor(mass = 1) {
    this.position = { x: 0, y: 0 }
    this.velocity = { x: 0, y: 0 }
    this.acceleration = { x: 0, y: 0 }
    this.force = { x: 0, y: 0 }
    this.mass = mass
  }
  
  applyForce(fx, fy) {
    this.force.x += fx
    this.force.y += fy
  }
  
  update(dt) {
    this.acceleration.x = this.force.x / this.mass
    this.acceleration.y = this.force.y / this.mass
    
    this.velocity.x += this.acceleration.x * dt
    this.velocity.y += this.acceleration.y * dt
    
    this.position.x += this.velocity.x * dt
    this.position.y += this.velocity.y * dt
    
    this.force.x = 0
    this.force.y = 0
  }
}

物理实体类

class PhysicsBody {
  constructor(x, y, options = {}) {
    this.x = x
    this.y = y
    this.vx = options.vx || 0
    this.vy = options.vy || 0
    this.ax = 0
    this.ay = 0
    this.mass = options.mass || 1
    this.restitution = options.restitution || 0.8    // 弹性系数
    this.friction = options.friction || 0.1          // 摩擦系数
    this.width = options.width || 20
    this.height = options.height || 20
    this.type = options.type || 'dynamic'            // dynamic, static, kinematic
  }
  
  applyForce(fx, fy) {
    if (this.type === 'dynamic') {
      this.ax += fx / this.mass
      this.ay += fy / this.mass
    }
  }
  
  applyImpulse(ix, iy) {
    if (this.type === 'dynamic') {
      this.vx += ix / this.mass
      this.vy += iy / this.mass
    }
  }
  
  update(dt) {
    if (this.type !== 'dynamic') return
    
    this.vx += this.ax * dt
    this.vy += this.ay * dt
    
    this.x += this.vx * dt
    this.y += this.vy * dt
    
    this.ax = 0
    this.ay = 0
  }
}

重力模拟

class GravityWorld {
  constructor(gravity = 0.5) {
    this.gravity = gravity
    this.bodies = []
  }
  
  addBody(body) {
    this.bodies.push(body)
  }
  
  removeBody(body) {
    const index = this.bodies.indexOf(body)
    if (index !== -1) {
      this.bodies.splice(index, 1)
    }
  }
  
  update(dt) {
    this.bodies.forEach(body => {
      if (body.type === 'dynamic') {
        body.applyForce(0, this.gravity * body.mass)
      }
      body.update(dt)
    })
  }
}

const world = new GravityWorld(0.5)
const ball = new PhysicsBody(200, 50, { mass: 1, type: 'dynamic' })
world.addBody(ball)

重力模拟演示

重力模拟(点击添加球体)

抛物运动

class Projectile {
  constructor(x, y, angle, speed, gravity = 0.5) {
    this.x = x
    this.y = y
    this.vx = Math.cos(angle) * speed
    this.vy = Math.sin(angle) * speed
    this.gravity = gravity
    this.trail = []
  }
  
  update() {
    this.trail.push({ x: this.x, y: this.y })
    if (this.trail.length > 20) {
      this.trail.shift()
    }
    
    this.vy += this.gravity
    this.x += this.vx
    this.y += this.vy
  }
  
  draw(ctx) {
    ctx.strokeStyle = 'rgba(52, 152, 219, 0.5)'
    ctx.lineWidth = 2
    ctx.beginPath()
    this.trail.forEach((p, i) => {
      if (i === 0) ctx.moveTo(p.x, p.y)
      else ctx.lineTo(p.x, p.y)
    })
    ctx.stroke()
    
    ctx.fillStyle = '#e74c3c'
    ctx.beginPath()
    ctx.arc(this.x, this.y, 8, 0, Math.PI * 2)
    ctx.fill()
  }
}

摩擦力

function applyFriction(body, friction) {
  const speed = Math.sqrt(body.vx * body.vx + body.vy * body.vy)
  
  if (speed > 0) {
    const frictionForce = friction * body.mass
    const fx = -(body.vx / speed) * frictionForce
    const fy = -(body.vy / speed) * frictionForce
    
    body.applyForce(fx, fy)
  }
}

空气阻力

function applyAirResistance(body, drag = 0.01) {
  const speed = Math.sqrt(body.vx * body.vx + body.vy * body.vy)
  const dragForce = drag * speed * speed
  
  if (speed > 0) {
    body.applyForce(
      -(body.vx / speed) * dragForce,
      -(body.vy / speed) * dragForce
    )
  }
}

弹簧力

class Spring {
  constructor(bodyA, bodyB, restLength, stiffness, damping) {
    this.bodyA = bodyA
    this.bodyB = bodyB
    this.restLength = restLength
    this.stiffness = stiffness
    this.damping = damping
  }
  
  apply() {
    const dx = this.bodyB.x - this.bodyA.x
    const dy = this.bodyB.y - this.bodyA.y
    const distance = Math.sqrt(dx * dx + dy * dy)
    
    if (distance === 0) return
    
    const stretch = distance - this.restLength
    const force = this.stiffness * stretch
    
    const nx = dx / distance
    const ny = dy / distance
    
    const relVx = this.bodyB.vx - this.bodyA.vx
    const relVy = this.bodyB.vy - this.bodyA.vy
    const relVn = relVx * nx + relVy * ny
    
    const dampingForce = this.damping * relVn
    
    const totalForce = force + dampingForce
    
    this.bodyA.applyForce(nx * totalForce, ny * totalForce)
    this.bodyB.applyForce(-nx * totalForce, -ny * totalForce)
  }
}

弹簧系统演示

弹簧物理模拟

绳索模拟

class Rope {
  constructor(x, y, segments, segmentLength) {
    this.points = []
    
    for (let i = 0; i <= segments; i++) {
      this.points.push({
        x: x,
        y: y + i * segmentLength,
        oldX: x,
        oldY: y + i * segmentLength,
        pinned: i === 0
      })
    }
    
    this.segmentLength = segmentLength
  }
  
  update(gravity) {
    this.points.forEach(point => {
      if (point.pinned) return
      
      const vx = (point.x - point.oldX) * 0.99
      const vy = (point.y - point.oldY) * 0.99
      
      point.oldX = point.x
      point.oldY = point.y
      
      point.x += vx
      point.y += vy + gravity
    })
    
    for (let i = 0; i < 5; i++) {
      this.constrain()
    }
  }
  
  constrain() {
    for (let i = 0; i < this.points.length - 1; i++) {
      const p1 = this.points[i]
      const p2 = this.points[i + 1]
      
      const dx = p2.x - p1.x
      const dy = p2.y - p1.y
      const distance = Math.sqrt(dx * dx + dy * dy)
      
      const diff = (this.segmentLength - distance) / distance
      
      if (!p1.pinned && !p2.pinned) {
        p1.x -= dx * diff * 0.5
        p1.y -= dy * diff * 0.5
        p2.x += dx * diff * 0.5
        p2.y += dy * diff * 0.5
      } else if (!p1.pinned) {
        p1.x -= dx * diff
        p1.y -= dy * diff
      } else if (!p2.pinned) {
        p2.x += dx * diff
        p2.y += dy * diff
      }
    }
  }
  
  draw(ctx) {
    ctx.strokeStyle = '#8b4513'
    ctx.lineWidth = 3
    ctx.beginPath()
    ctx.moveTo(this.points[0].x, this.points[0].y)
    
    this.points.forEach(p => {
      ctx.lineTo(p.x, p.y)
    })
    
    ctx.stroke()
    
    ctx.fillStyle = '#333'
    this.points.forEach(p => {
      ctx.beginPath()
      ctx.arc(p.x, p.y, p.pinned ? 6 : 4, 0, Math.PI * 2)
      ctx.fill()
    })
  }
}

布料模拟

class Cloth {
  constructor(x, y, width, height, resolution) {
    this.points = []
    this.resolution = resolution
    this.spacing = width / resolution
    
    for (let j = 0; j <= resolution; j++) {
      for (let i = 0; i <= resolution; i++) {
        this.points.push({
          x: x + i * this.spacing,
          y: y + j * this.spacing,
          oldX: x + i * this.spacing,
          oldY: y + j * this.spacing,
          pinned: j === 0 && (i === 0 || i === resolution)
        })
      }
    }
  }
  
  update(gravity) {
    this.points.forEach(point => {
      if (point.pinned) return
      
      const vx = (point.x - point.oldX) * 0.99
      const vy = (point.y - point.oldY) * 0.99
      
      point.oldX = point.x
      point.oldY = point.y
      
      point.x += vx
      point.y += vy + gravity
    })
    
    for (let i = 0; i < 3; i++) {
      this.constrain()
    }
  }
  
  constrain() {
    const res = this.resolution
    
    for (let j = 0; j <= res; j++) {
      for (let i = 0; i <= res; i++) {
        const idx = j * (res + 1) + i
        const point = this.points[idx]
        
        if (i < res) {
          this.constrainPoints(point, this.points[idx + 1])
        }
        
        if (j < res) {
          this.constrainPoints(point, this.points[idx + res + 1])
        }
      }
    }
  }
  
  constrainPoints(p1, p2) {
    const dx = p2.x - p1.x
    const dy = p2.y - p1.y
    const distance = Math.sqrt(dx * dx + dy * dy)
    
    const diff = (this.spacing - distance) / distance
    
    if (!p1.pinned && !p2.pinned) {
      p1.x -= dx * diff * 0.5
      p1.y -= dy * diff * 0.5
      p2.x += dx * diff * 0.5
      p2.y += dy * diff * 0.5
    } else if (!p1.pinned) {
      p1.x -= dx * diff
      p1.y -= dy * diff
    } else if (!p2.pinned) {
      p2.x += dx * diff
      p2.y += dy * diff
    }
  }
  
  draw(ctx) {
    const res = this.resolution
    
    for (let j = 0; j < res; j++) {
      for (let i = 0; i < res; i++) {
        const idx = j * (res + 1) + i
        const p1 = this.points[idx]
        const p2 = this.points[idx + 1]
        const p3 = this.points[idx + res + 1]
        const p4 = this.points[idx + res + 2]
        
        ctx.fillStyle = `hsl(${200 + i * 5}, 70%, 60%)`
        ctx.beginPath()
        ctx.moveTo(p1.x, p1.y)
        ctx.lineTo(p2.x, p2.y)
        ctx.lineTo(p4.x, p4.y)
        ctx.lineTo(p3.x, p3.y)
        ctx.closePath()
        ctx.fill()
      }
    }
  }
}

数值积分方法

欧拉法

function eulerIntegrate(body, dt) {
  body.vx += body.ax * dt
  body.vy += body.ay * dt
  body.x += body.vx * dt
  body.y += body.vy * dt
}

Verlet积分

function verletIntegrate(point, dt) {
  const vx = (point.x - point.oldX) * 0.99
  const vy = (point.y - point.oldY) * 0.99 + 0.5 * dt
  
  point.oldX = point.x
  point.oldY = point.y
  
  point.x += vx
  point.y += vy
}

RK4积分

function rk4Integrate(state, derivative, dt) {
  const k1 = derivative(state, 0)
  const k2 = derivative(state, dt * 0.5, k1)
  const k3 = derivative(state, dt * 0.5, k2)
  const k4 = derivative(state, dt, k3)
  
  return {
    x: state.x + (k1.x + 2 * k2.x + 2 * k3.x + k4.x) / 6 * dt,
    y: state.y + (k1.y + 2 * k2.y + 2 * k3.y + k4.y) / 6 * dt
  }
}