📐

固定の線分に衝突した円の補正と反射

2023/10/03に公開

手順

  1. 線分と判定を行う
  2. 始点と判定を行う
  3. 終点と判定を行う

必ず線分との判定を最初に行う。

固定値

線分 ab と法線の正規化:

a = V[520.0, 320.0]
b = V[280.0, 320.0]
ab = b - a          # => (-240.0, 0.0)
abn = ab.normalize  # => (-1.0, 0.0)

円の半径:

radius = 120

線分との判定

c = V[400.0, 240.0]
d = V[400.0, 280.0]
speed = d - c  # => (0.0, 40.0)

円心の垂線が線分に含まれるかを判定する。

ac = c - a
bc = c - b
inside = (ac.dot(abn) * bc.dot(abn)).negative?  # => true

true なら次へ。

円が線にめり込んでいるか?

distance = abn.cross(ac)  # => 80.0
gap = distance - radius   # => -40.0
gap.round.negative?       # => true

true なら次へ。

押し戻す。

c2 = c + abn.perp * -gap  # => (400.0, 200.0)

速度ベクトルを反射させる。

speed2 = speed.bounce(abn.perp)  # => (0.0, -40.0)

始点との判定

線分に衝突していなかった場合はこちらの判定を行う。

c = V[568.0, 240.0]
d = V[400.0, 200.0]
speed = d - c       # => (-168.0, -40.0)
ac = c - a          # => (48.0, -80.0)
acn = ac.normalize  # => (0.5144957554275265, -0.8574929257125441)

始点が円にめり込んでいるか?

distance = c.distance_to(a)
gap = distance - radius
gap.round.negative?  # => true

true なら次へ。

めり込んでいる分だけ押し返す。

c2 = c + acn * -gap  # => (581.7394906513032, 217.1008489144947)

速度ベクトルを反射させる。

speed2 = speed.bounce(acn)  # => (-114.3529411764706, -129.41176470588235)

終点との判定

始点と同様に行う。

c = V[232.0, 240.0]
d = V[400.0, 200.0]
speed = d - c       # => (168.0, -40.0)
bc = c - b          # => (-48.0, -80.0)
bcn = bc.normalize  # => (-0.5144957554275265, -0.8574929257125441)
distance = c.distance_to(b)  # => 93.29523031752481
gap = distance - radius      # => -26.704769682475188
gap.round.negative?          # => true
c2 = c + bcn * -gap         # => (218.26050934869681, 217.1008489144947)
speed2 = speed.bounce(bcn)  # => (114.3529411764706, -129.41176470588235)

判定の順序に注意する

繰り返しになるが線分との判定を優先すること。そうしないと線分に円がめり込む。

裏面で反発させたいときは?

これまでのコードは一方通行タイプだったので、線分の裏(左側)から円が侵入した場合、表から来たと判定され、表に押し出されてしまう。それが困る場合にのみ裏面にも対応する。表から来たか裏から来たかは速度ベクトルの向きでわかりそうだが、他の物体によって押されて衝突した場合を考えると、必ずしも線分を向いているとは限らない。

この場合は「円心が線分のどちら側にあるか」で判定し「線分にめり込んだ長さを半径の長さまで」とすればよい。

この裏面に対応した場合、半径より移動量の方が多いと突き抜けるので、単に壁でいいのなら一方通行にした方がよい。

関連

https://zenn.dev/megeton/articles/3eaef220cdadbc
https://zenn.dev/megeton/articles/c227b46cc437c8
https://zenn.dev/megeton/articles/1930f4e5c286b2
https://zenn.dev/megeton/articles/b965f38aa7a839

コード
class App < Base
  def initialize
    super

    layout0

    @mode = 0
    @perp = 0
  end

  def layout0
    a = window_wh * V[0.65, 0.80]  # => (520.0, 320.0)
    b = window_wh * V[0.35, 0.80]  # => (280.0, 320.0)
    c = window_wh * V[0.50, 0.60]  # => (400.0, 240.0)
    d = window_wh * V[0.50, 0.70]  # => (400.0, 280.0)
    @points = [a, b, c, d]
  end

  def layout1
    a = window_wh * V[0.65, 0.80]  # => (520.0, 320.0)
    b = window_wh * V[0.35, 0.80]  # => (280.0, 320.0)
    c = window_wh * V[0.72, 0.60]  # => (576.0, 240.0)
    d = window_wh * V[0.50, 0.50]  # => (400.0, 200.0)
    @points = [a, b, c, d]
  end

  def layout2
    a = window_wh * V[0.65, 0.80]  # => (520.0, 320.0)
    b = window_wh * V[0.35, 0.80]  # => (280.0, 320.0)
    c = window_wh * V[0.28, 0.60]  # => (224.00000000000003, 240.0)
    d = window_wh * V[0.50, 0.50]  # => (400.0, 200.0)
    @points = [a, b, c, d]
  end

  def button_down(id)
    super

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

  def draw
    super

    a, b, c, d = @points
    radius = 120
    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
        distance = abn.cross(ac)
        gap = distance - radius
        hit = gap.round.negative?
        if hit
          c2 = c + abn.perp * -gap
          speed2 = cd.bounce(abn.perp) * e
        end
      end
    end

    unless hit
      distance = c.distance_to(a)
      gap = distance - radius
      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
      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")
    vector_draw(c, d, "c", "d")

    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