📐

円同士を重ならない位置まで補正する

2023/09/22に公開


円aは重いのであまりずれていない

衝突判定

a = V[300.0, 150.0]             # 円aの位置
ar = 125                        # 円aの半径
am = 4.0                        # 円aの質量
b = V[500.0, 150.0]             # 円bの位置
br = 125                        # 円bの半径
bm = 1.0                        # 円bの質量

ab ベクトルを求める。ここを間違えて a - b として ba ベクトルとしてしまうと重なりを解消するのではなく余計に近づくことになるので注意。

ab = b - a  # => (200.0, 0.0)

その長さから両方の半径を引いた残りが円と円の隙間になる。

gap = ab.length - ar - br  # => -50.0
gap.negative?              # => true

これが負なら円はめり込んでいる。ただこの判定では浮動小数点の誤差が問題になる(後述)

円を離す

めり込んでいる長さを分配して反発する方向にずらす。ずらす量は相手の質量に比例する。係数は「相手の質量 / 両方足した質量」になる。

ae = bm / (am + bm)  # => 0.2
be = am / (am + bm)  # => 0.8

ただ a から b の視点で見ているので a は引いて b は押す。つまり a 側だけマイナスにする。

ae = -bm / (am + bm)  # => -0.2
be =  am / (am + bm)  # => 0.8

そこに長さを掛けると実際にずらす量が求まる。

len = -gap  # => 50.0
len * ae    # => -10.0
len * be    # => 40.0

ずらす方向は ab の単位ベクトル

ab.normalize  # => (1.0, 0.0)

なので、補正後の青と赤の位置は、

a + ab.normalize * len * ae  # => (290.0, 150.0)
b + ab.normalize * len * be  # => (540.0, 150.0)

となる。

離しても重なっていると判定される? (重要)

上の計算では意図的に整数になるようにしていたので問題が見えなかった。実際は浮動小数点の誤差の影響でぴったり隣接するように離すのは難しい。離したつもりが 0.00000000000003 ほど重なってしまう。具体的な値で言うと、二つの円の半径が 50 だったとき 100 の距離を取るように離しても実際の距離は 99.99999999999997 となる。こうなると永遠に重なりを解消できない。

(99.99999999999997 - 50 - 50).negative?  # => true

その場合、重なっているかの判定時に距離を四捨五入するのが望ましい。

(99.99999999999997 - 50 - 50).round.negative?  # => false

計算手順まとめ

a = V[300.0, 150.0]
ar = 125
am = 4.0
b = V[500.0, 150.0]
br = 125
bm = 1.0
ab = b - a                          # => (200.0, 0.0)
gap = ab.length - ar - br           # => -50.0
if gap.round.negative?
  len = -gap                        # => 50.0
  abn = ab.normalize                # => (1.0, 0.0)
  a += abn * len * -bm / (am + bm)  # => (290.0, 150.0)
  b += abn * len * +am / (am + bm)  # => (540.0, 150.0)
end
コード
class App < Base
  def initialize
    super

    a = window_wh * V[3.0/8, 0.5]    # => (300.0, 150.0)
    b = window_wh * V[5.0/8, 0.5]    # => (500.0, 150.0)

    # a = window_wh * V[4.0/8, 0.5]  # => (300.0, 150.0)
    # b = window_wh * V[4.0/8, 0.5]  # => (500.0, 150.0)
    # a += V.one

    @points = [a, b]

    @mode = 0
  end

  def button_down(id)
    super

    if id == Gosu::KB_Z
      @mode = @mode.next.modulo(4)
    end
  end

  def draw
    super

    a, b = @points

    ar = 125
    am = 4.0

    br = 125
    bm = 1.0

    ab = b - a
    abn = ab.normalize_or_zero

    gap = ab.length - ar - br
    len = -gap

    ae = -bm / (am + bm)
    be = +am / (am + bm)

    a2 = a + abn * len * ae
    b2 = b + abn * len * be

    if false
      a.replace(a2)
      b.replace(b2)
    end

    if true
      arrow_head(a + V.from_angle(270.deg_to_rad), a, "a(#{am.round})")
      circle_draw(a, ar)
      point_draw(a)
      if a2
        point_draw(a2, color: :blue_light)
        circle_draw(a2, ar, color: :blue_light, line_width: 3)
      end
    end

    if true
      arrow_head(b + V.from_angle(270.deg_to_rad), b, "b(#{bm.round})")
      circle_draw(b, br)
      point_draw(b)
      if b2
        point_draw(b2, color: :red_light)
        circle_draw(b2, br, color: :red_light, line_width: 3)
      end
    end

    vputs "a: #{a.round}"
    vputs "ar: #{ar}"
    vputs "am: #{am.round}"
    vputs
    vputs "b: #{b.round}"
    vputs "br: #{br}"
    vputs "bm: #{bm.round}"
    vputs
    vputs "ab: #{ab.round(2)}"
    vputs "ab間: #{ab.length.round}"
    vputs "円の隙間: #{gap.round}"
    vputs "ae: #{ae.round(2)}"
    vputs "be: #{be.round(2)}"
    vputs "aの補正長: %+d" % [len * ae]
    vputs "bの補正長: %+d" % [len * be]
    vputs "青: #{a2.round}"
    vputs "赤: #{b2.round}"
    vputs "補正後の隙間: #{((b2 - a2).length - ar - br).round(8)}"

    line_draw(a, b, color: :grey)
  end

  def window_size_default
    V[800, 300]
  end

  def font_size_default
    16
  end

  show
end

参照

https://hakuhin.jp/as/collide.html

Discussion