📐

固定カプセルに衝突した円の補正と反射

2023/10/05に公開

https://zenn.dev/megeton/articles/1180fa4d3c8c18

の続きとして

  • 線分がカプセル状
  • 裏面にも当たり判定がある

の条件で考える。

固定値

線分
a = V[480.0, 200.0]
b = V[320.0, 200.0]
c = V[400.0, 108.0]             # 円心
d = V[416.0, 148.0]             # 円心 + 速度ベクトル
radius = 80
カプセル半径(筒の幅の半分)
radius2 = 30

事前に計算しておくもの

円の速度ベクトル
cd = d - c  # => (16.0, 40.0)
線分の法線の正規化
ab = b - a          # => (-160.0, 0.0)
abn = ab.normalize  # => (-1.0, 0.0)
線分の両端から円心に向けたベクトル
ac = c - a  # => (-80.0, -92.0)
bc = c - b  # => (80.0, -92.0)

筒との判定


hit = false
unless hit
  inside = (ac.dot(abn) * bc.dot(abn)).negative?                     # => true
  if inside
    unless hit
      distance = abn.cross(ac)
      sign = distance.positive? ? 1 : -1                             # => 1
      t = distance / (radius + radius2)                              # => 0.8363636363636363
      if t.abs < 1.0
        hit = true
        c2 = c + abn.perp * (1 - t.abs) * (radius + radius2) * sign  # => (400.0, 90.0)
        speed2 = cd.bounce(abn.perp)                                 # => (16.0, -40.0)
      end
    end
  end
end

線分との違いは radius2 を条件に追加するところ。また裏面にも対応できるように「線分にめり込んだ長さを半径の長さまで」とする。それには t.abs < 1.0 の条件を入れる。

始点の半円との判定

unless hit
  distance = c.distance_to(a)
  gap = distance - radius - radius2
  hit = gap.round.negative?
  if hit
    c2 = c + ac.normalize * -gap
    speed2 = cd.bounce(ac.normalize)
  end
end

こちらも radius2 を追加するのみ。

終点の半円との判定

unless hit
  distance = c.distance_to(b)
  gap = distance - radius - radius2
  hit = gap.round.negative?
  if hit
    c2 = c + bc.normalize * -gap
    speed2 = cd.bounce(bc.normalize)
  end
end

始点の半円と同様に行う。

コード
class App < Base
  def initialize
    super

    layout0

    @mode = 0
    @perp = 0
  end

  def layout0
    a = V[480.0, 200.0]
    b = V[320.0, 200.0]
    c = V[400.0, 108.0]
    d = V[416.0, 148.0]
    @points = [a, b, c, d]
  end

  def layout1
    a = V[480.0, 200.0]
    b = V[320.0, 200.0]
    c = V[400.0, 292.0]
    d = V[384.0, 238.0]
    @points = [a, b, c, d]
  end

  def layout2
    a = V[480.0, 200.0]
    b = V[320.0, 200.0]
    c = V[559.0, 142.0]
    d = V[609.0, 309.0]
    @points = [a, b, c, d]
  end

  def layout3
    a = V[480.0, 200.0]
    b = V[320.0, 200.0]
    c = V[237.0, 230.0]
    d = V[323.0, 310.0]
    @points = [a, b, c, d]
  end

  def button_down(id)
    super

    if id == Gosu::KB_Z
      @mode = @mode.next.modulo(4)
      if @mode == 0
        layout0
      end
      if @mode == 1
        layout1
      end
      if @mode == 2
        layout2
      end
      if @mode == 3
        layout3
      end
    end
  end

  def draw
    super

    a, b, c, d = @points
    radius = 80
    radius2 = 30
    cd = d - c

    ab = b - a
    abn = ab.normalize

    ac = c - a
    bc = c - b

    hit = false
    e = 1.0

    unless hit
      inside = (ac.dot(abn) * bc.dot(abn)).negative?
      if inside
        if false
          unless hit
            distance = abn.cross(ac)
            gap = distance - radius - radius2
            hit = gap.round.negative?
            if hit
              c2 = c + abn.perp * -gap
              speed2 = cd.bounce(abn.perp) * e
            end
          end
        end

        if false
          unless hit
            distance = abn.cross(ac)
            sign = distance.positive? ? 1 : -1
            gap = distance * sign - radius - radius2
            if -gap < radius + radius2
              if gap.round.negative?
                hit = true
                c2 = c + abn.perp * -gap * sign
                speed2 = cd.bounce(abn.perp) * e
              end
            end
          end
        end

        if true
          unless hit
            distance = abn.cross(ac)
            sign = distance.positive? ? 1 : -1
            t = distance / (radius + radius2)
            if t.abs < 1.0
              hit = true
              c2 = c + abn.perp * (1 - t.abs) * (radius + radius2) * sign
              speed2 = cd.bounce(abn.perp) * e
            end
          end
        end
      end
    end

    unless hit
      distance = c.distance_to(a)
      gap = distance - radius - radius2
      hit = gap.round.negative?
      if hit
        c2 = c + ac.normalize * -gap
        speed2 = cd.bounce(ac.normalize) * e
        line_draw(a, a + ac.perp, color: :blue_light, infinity: true)
      end
    end

    unless hit
      distance = c.distance_to(b)
      gap = distance - radius - radius2
      hit = gap.round.negative?
      if hit
        c2 = c + bc.normalize * -gap
        speed2 = cd.bounce(bc.normalize) * e
        line_draw(b, b + bc.perp, color: :grey, infinity: true)
      end
    end

    vputs "a: #{a.round}"
    vputs "b: #{b.round}"
    vputs "c: #{c.round}"
    vputs "d: #{d.round}"
    vputs "ab: #{ab.round}"
    vputs "abn: #{abn.round(2)}"
    vputs "ac: #{ac.round}"
    vputs "bc: #{bc.round}"
    vputs "hit: #{hit}"
    vputs "inside: #{inside}"
    vputs "c2: #{c2.round}" if c2

    vector_draw(a, b, "a", "b", color: :grey_lighter)
    vector_draw(c, d, "c", "d")

    if true
      v = abn.perp * radius2
      line_draw(a + v, b + v, line_width: 1)
      line_draw(a - v, b - v, line_width: 1)
      line_draw(a + v, b + v, line_width: 1)
      line_draw(a - v, b - v, line_width: 1)
      circle_draw(a, radius2, start: abn.angle.rad_to_deg + 180, half: true, line_width: 2, sides: 32)
      circle_draw(b, radius2, start: abn.angle.rad_to_deg +   0, half: true, line_width: 2, sides: 32)
    end

    vector_draw(a, c, "", "", color: :orange_light)
    vector_draw(b, c, "", "", color: :orange_light)

    circle_draw(c, radius)
    if hit
      circle_draw(c2, radius, color: :blue_light, line_width: 4)
      vector_draw(c2, c2 + speed2, "", "", color: :blue_light)
      arrow_head(c2 + V.from_angle(270.deg_to_rad), c2, "c2", color: :blue_light)
    end

  end

  def window_size_default
    V[800, 400]
  end

  show
end

Discussion