📐

反射・壁ずり・鏡像ベクトルの考え方

2023/09/21に公開

目的

直線 ab に衝突した速度ベクトル cd を反射させたい。

ここまでにわかっていること

a = V[600.0, 340.0]
b = V[200.0,  60.0]
c = V[360.0,  52.0]
d = V[400.0, 200.0]
ab = b - a  # => (-400.0, -280.0)
cd = d - c  # => (40.0, 148.0)
dc = c - d  # => (-40.0, -148.0)

最初にやること

ベクトル de を求める。

ここが求まれば後は足したりずらしたりすればあっちこっちが求まる。

内積で正射影 de を求める

  1. ベクトル cd を反転し斜辺と考える
  2. 直線 ab から法線を出す

「直線 ab の法線の正規化」を n とし、dc から n に対して内積を取るとベクトル de の長さが求まる。単位ベクトルの n にその長さを掛けると膨らんで de になる。

n = ab.perp.normalize  # => (0.5734623443633283, -0.8192319205190405)
length = dc.dot(n)     # => 98.30783046228487
de = n * length        # => (56.3758389261745, -80.53691275167786)

de の長さは直線 ab を地面とすれば外積でも求まる。が、そこはあんまり重要じゃない。

cd.cross(ab.normalize)  # => 98.30783046228487

高速化

de = dc.project_onto(ab.perp)  # => (56.37583892617449, -80.53691275167785)

とすれば一発で de を求められる。project_onto の中身は、

de = ab.perp * dc.dot(ab.perp) / ab.perp.length_squared  # => (56.375838926174495, -80.53691275167785)

となっていて平方根を使わないので速い。その上、小数の誤差が生じにくい。なので、長さからベクトルを求めるのではなく、直接ベクトルを求めたい場合は平方根を省ける。

反射ベクトルまでの道のり (考え方1)

e は d から de 進めたところなので

e = d + de  # => (456.3758389261745, 119.46308724832215)

ce は e - c で

ce = e - c  # => (96.37583892617448, 67.46308724832215)

h は c から ce を2つ分進めたところにある。

h = c + ce * 2  # => (552.751677852349, 186.9261744966443)

としたけどもう e はわかっているんだから e から ce だけ進めても同じ。

h = e + ce  # => (552.751677852349, 186.9261744966443)

反射ベクトルと壁ずりベクトル

h まで求まれば dh が反射ベクトルになる。

dh = h - d  # => (152.75167785234896, -13.073825503355692)

ついでに h から直線の方に de だけずらせば g がわかる。

g = h - de  # => (496.3758389261745, 267.46308724832215)

g は d 経由で ce だけ進めたものとしてイメージしてもいい。

g = d + ce  # => (496.3758389261745, 267.46308724832215)

g が求まれば dg が壁ずりベクトルになる。

dg = g - d  # => (96.37583892617448, 67.46308724832215)

計算方法を整理する

de は重要なので置いといて

de = dc.project_onto(ab.perp)  # => (56.37583892617449, -80.53691275167785)

次の計算を整理する。

e = d + de  # => (456.3758389261745, 119.46308724832215)
ce = e - c  # => (96.37583892617448, 67.46308724832215)
h = e + ce  # => (552.751677852349, 186.9261744966443)
g = h - de  # => (496.3758389261745, 267.46308724832215)
dg = g - d  # => (96.37583892617448, 67.46308724832215)
dh = h - d  # => (152.75167785234896, -13.073825503355692)

まず e を消す。

ce = (d + de) - c          # => (96.37583892617448, 67.46308724832215)
h = d + de + (d + de) - c  # => (552.751677852349, 186.9261744966443)
g = h - de                 # => (496.3758389261745, 267.46308724832215)
dg = g - d                 # => (96.37583892617448, 67.46308724832215)
dh = h - d                 # => (152.75167785234896, -13.073825503355692)

ce と h と g も消す。

dg = d + d + de + de - c - de - d  # => (96.3758389261746, 67.46308724832215)
dh = d + d + de + de - c - d       # => (152.75167785234908, -13.073825503355692)

さらに打ち消し当っているのを消す。

dg = d - c + de       # => (96.37583892617448, 67.46308724832215)
dh = d - c + de + de  # => (152.75167785234896, -13.073825503355692)

さらに d - c はベクトル cd のことなので最後は

dg = cd + de      # => (96.37583892617448, 67.46308724832215)
dh = cd + de * 2  # => (152.75167785234896, -13.073825503355692)

となる。

反射ベクトルまでの道のり (考え方2)

壁を突き抜けたところから戻る考え方で、速度ベクトルをもうひとつ足したところを f とする。そこから de を足せば g が求まる。ついでにもう1つ足せば h が求まる。

f = d + cd       # => (440.0, 348.0)
g = f + de       # => (496.3758389261745, 267.46308724832215)
h = f + de + de  # => (552.751677852349, 186.9261744966443)
dg = g - d       # => (96.37583892617448, 67.46308724832215)
dh = h - d       # => (152.75167785234896, -13.073825503355692)

ここで、また計算を整理する。f g h を消すと、

dg = d + cd + de - d       # => (96.37583892617448, 67.46308724832215)
dh = d + cd + de + de - d  # => (152.75167785234896, -13.073825503355692)

となり、打ち消し合う d を取る。

dg = cd + de      # => (96.37583892617448, 67.46308724832215)
dh = cd + de * 2  # => (152.75167785234896, -13.073825503355692)

するとおもしろいことに壁を意識して辿ったときの式と同じになるのだった。

考え方の最適化

途中の位置ベクトルを求めようとしていたから無駄に複雑になっていた。上の図をよく見れば青と赤は同じ形になっている。つまり壁ずりベクトルは青が入射ベクトルの先端から生えているだけ。反射ベクトルは壁ずりベクトルの最後を伸ばしただけだとわかる。

鏡像ベクトル

直線を鏡として見たときに鏡の中にいる自分を表わす。ベクトル de を二倍したところから速度ベクトルを引くと求まる。反転すると反射ベクトルになる。

計算方法まとめ

a = V[600.0, 340.0]
b = V[200.0,  60.0]
c = V[360.0,  52.0]
d = V[400.0, 200.0]
壁の法線の正規化
n = (b - a).perp.normalize  # => (0.5734623443633283, -0.8192319205190405)
速度ベクトル
speed = d - c  # => (40.0, 148.0)
壁ずりベクトル
speed - speed.project_onto_normalized(n)  # => (96.37583892617451, 67.46308724832214)
speed.slide(n)                            # => (96.37583892617451, 67.46308724832214)
反射ベクトル
speed - speed.project_onto_normalized(n) * 2  # => (152.75167785234902, -13.073825503355721)
speed.bounce(n)                               # => (152.75167785234902, -13.073825503355721)
鏡像ベクトル
speed.project_onto_normalized(n) * 2 - speed  # => (-152.75167785234902, 13.073825503355721)
speed.reflect(n)                              # => (-152.75167785234902, 13.073825503355721)
コード
class App < Base
  def initialize
    super

    a = window_wh * V[0.75, 0.85]  # => (600.0, 340.0)
    b = window_wh * V[0.25, 0.15]  # => (200.0, 60.0)
    c = window_wh * V[0.45, 0.13]  # => (360.0, 52.0)
    d = window_wh * V[0.50, 0.50]  # => (400.0, 200.0)

    @points.concat([a, b, c, d])

    @mode = 0
  end

  def button_down(id)
    super

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

  def draw
    super

    a, b, c, d = @points
    ab = b - a
    cd = d - c
    dc = c - d

    n = ab.perp

    if @mode == 0
      vputs "a: #{a.round}"
      vputs "b: #{b.round}"
      vputs "c: #{c.round}"
      vputs "d: #{d.round}"

      vector_draw(c, d, "c", "d")
    end

    n = ab.perp.normalize
    de = dc.project_onto(ab.perp)
    e = d + de
    ce = e - c
    h = c + ce
    f = d + cd
    g = f + de
    h = f + de + de
    dh = h - d
    dg = g - d

    point_draw(d + cd.slide(n), color: :yellow_light)
    point_draw(d + cd.reflect(n), color: :green_light)
    point_draw(d + cd.bounce(n), color: :orange_light)

    if @mode == 1 || @mode == 3 || @mode == 4
      vector_draw(d, c, "d", "c")
      vector_draw(d, e, "", "e")
      line_draw(c, e)
      vputs "de: #{de.round}"
    end

    if @mode == 2
      vputs "n: #{n.round(2)}"
      vputs "de: #{de.round}"
      vector_draw(d, c, "d", "c")
      line_draw(d, d + ab.perp.clamp_length_max(1000), color: :blue_light, line_width: 2)
    end

    if @mode == 3 || @mode == 4
      vector_draw(c, h, "", "h")
      vputs "ce: #{ce.round}"
    end

    if @mode == 4
      vector_draw(d, h, "", "")
      vector_draw(d, g, "", "g")
      vputs "dh: #{dh.round}"
      vputs "dg: #{dg.round}"
      line_draw(h, g)
    end

    if @mode == 5 || @mode == 6
      vector_draw(d, e, "", "e")
      vector_draw(c, d, "c", "d")
      vector_draw(d, f, "", "f")
      vector_draw(d, g, "", "g")
      vector_draw(d, h, "", "h")
      line_draw(f, g)
      line_draw(g, h)
    end

    if @mode == 6
      line_draw(c, d, color: :blue, line_width: 2)
      line_draw(d, e, color: :blue, line_width: 2)
      line_draw(d, f, color: :red,  line_width: 2)
      line_draw(f, g, color: :red,  line_width: 2)
    end

    if @mode == 7
      v = cd.project_onto_normalized(n)
      vector_draw(c, d, "c", "d")
      vector_draw(d, d + v, "", "", color: :green_light, line_width: 1)
      vector_draw(d + v, d + v + v, "", "", color: :green_light, line_width: 1)
      vector_draw(d + v + v, d + v + v - cd, "", "", color: :green_light, line_width: 1)
      vector_draw(d, d + cd.reflect(n), "", "")
    end

    vector_draw(a, b, "a", "b")
  end

  def window_size_default
    V[800, 400]
  end

  show
end

参照

Discussion