壁に衝突した物体の本当の反発ベクトル
縦方向の力は弱まっていくが横方向の力は減らない
ただの反発ベクトルとは異なる
反発係数 0.5 で黒の方向で衝突した物体の速度ベクトルは緑の方に反発する。なぜなら、反射は壁に対して垂直方向のみが影響を受けるから。
続いてこちらの記事の固定壁に質点を当てるを実証していく。
パラメータ
e = 0.5
a = V[400.0, 200.0] # 中心
v = V[640.0, 87.0] # 中心 + 速度ベクトル
av = v - a # => (240.0, -113.0)
円か点かは今回関係がないので点と考えてもいい。
c = V[548.0, 370.0]
b = V[548.0, 30.0]
bc = c - b # => (0.0, 340.0)
bcn = bc.normalize # => (0.0, 1.0)
n = bcn.perp # => (-1.0, 0.0)
計算方法
壁に垂直な成分を抽出する。
avx = n * av.dot(n) # => (240.0, -0.0)
avx = av.project_onto_normalized(n) # => (240.0, -0.0)
壁に水平な成分を抽出する。
avy = av - avx # => (0.0, -113.0)
avy = av.reject_from_normalized(n) # => (0.0, -113.0)
壁に垂直な成分のみにマイナスの反発係数を掛ける。
avx2 = -e * avx # => (-120.0, 0.0)
壁に水平な成分と合体して元に戻す。
av2 = avx2 + avy # => (-120.0, -113.0)
整理する
avx = n * av.dot(n) # => (240.0, -0.0)
avy = av - avx # => (0.0, -113.0)
avx2 = -e * avx # => (-120.0, 0.0)
av2 = avx2 + avy # => (-120.0, -113.0)
を、av2 = の右辺に展開すると、
av2 = -e * n * av.dot(n) + av - n * av.dot(n) # => (-120.0, -113.0)
av2 = av - e * n * av.dot(n) - n * av.dot(n) # => (-120.0, -113.0)
av2 = av - (e * n + n) * av.dot(n) # => (-120.0, -113.0)
となり、最後は
av2 = av - n * av.dot(n) * (1 + e) # => (-120.0, -113.0)
となる。
単なる反射ベクトルとの違いを再度確認する
av # => (240.0, -113.0)
av3 = (av - n * av.dot(n) * 2) * e # => (-120.0, -56.5)
av3 = (av - av.project_onto_normalized(n) * 2) * e # => (-120.0, -56.5)
av3 = av.bounce(n) * e # => (-120.0, -56.5)
av2 = av - n * av.dot(n) * (1 + e) # => (-120.0, -113.0)
av2 = av - av.project_onto_normalized(n) * (1 + e) # => (-120.0, -113.0)
単なる反発ベクトルは元の速度ベクトルの半分になっているだけだが、垂直方向のみに反発係数の影響を受けたベクトルは水平方向の強さを維持しているのが計算結果からもわかる。
計算式まとめ
velocity # => (240.0, -113.0)
e # => 0.5
n # => (-1.0, 0.0)
velocity - n * velocity.dot(n) * (1 + e) # => (-120.0, -113.0)
velocity - velocity.project_onto_normalized(n) * (1 + e) # => (-120.0, -113.0)
反発係数 1.0 のとき単なる反射ベクトルと同じか?
同じ。今回の計算では運動量が壁に移動したりしないため反発係数 1.0 のときは同じになる。だからといって普通の反発ベクトルを求める式に書き換えるメリットは少ない。どちらも内積を1回やってる程度で計算量はほとんど変わらない。正確には単なる反射ベクトルの計算量より 1.09 倍遅い。
反発係数は摩擦係数と同じか?
おそらく同じ。Nature of Code では垂直抗力の大きさを 1.0 と仮定しているものの行っていることはだいたい同じ。
c = 0.01 # 摩擦係数
normal = 1.0 # 壁への当たりの強さ
friction_mag = c * normal
friction = -velocity.normalize * friction_mag
acceleration += friction / mass # 質量を考慮して加速度に足す
ただ acceleration に足していいのだろうか? 上にも書いたけど acceleration に足すのはまずいような気がする。
壁が動いている場合
続いて迫り来る壁を実証する。
変数 | 意味 | 値 |
---|---|---|
e | 反発係数 | 1.0 |
v | 物体の速度ベクトル | 2.0 |
w | 壁の速度ベクトル | -8.0 |
e = 1.0
v = V[2.0, 0.0]
w = V[-8.0, 0.0]
このとき相対速度は v - w
になる。
相対速度 = v - w # => (10.0, 0.0)
確かに自分が 2 で進んでいところに相手が 8 で向かってきたら 10 のスピードに感じる。ここまでは問題ない。そこで相手は止まっていて自分は 10 のスピードで進んでいると考える。したがって v を v - w
に置き換える。
上の方で求めた式
v - (1 + e) * n * v.dot(n) # => (-2.0, 0.0)
の v を v - w
に置き換えると
(v - w) - (1 + e) * n * (v - w).dot(n) # => (-10.0, 0.0)
となり、物体は相手の速度も足して反転したのがわかる。勘違いしていなければここで終わってよさそうだが元記事には続きがある。
実際には壁も動いていますので、それを元に戻す必要があります。これはv'にvWを足し算するだけです。
どういうことだ……?
実際に w を足すと -18 になってしまう。
(v - w) - (1 + e) * n * (v - w).dot(n) + w # => (-18.0, 0.0)
元記事の最下段の導出式を計算結果も、
v - (1 + e) * n * (v - w).dot(n) # => (-18.0, 0.0)
となってしまう。
2 の速度で来た卓球の球を 8 の力で打ったとき 18 にもなるだろうか?
……わからん。
シミュレーション
class Wall
attr_accessor :p0
attr_accessor :p1
def initialize
@p0 = $app.window_wh * V[0.9, 0.85]
@p1 = $app.window_wh * V[0.1, 0.90]
end
def draw
$app.line_draw(p0, p1, infinity: true)
$app.vector_draw(p0, p1, "b", "c")
end
end
class Ball
attr_accessor :location
attr_accessor :velocity
attr_accessor :restitution
def initialize
@location = $app.window_wh * 0.5
@velocity = V.zero
@acceleration = V.zero
@radius = 120
@restitution = 1.0
end
def draw
b = $app.wall.p0
c = $app.wall.p1
bc = c - b
n = bc.normalize.perp
ba = @location - b
distance = ba.dot(n)
t = distance / @radius
if t < 1.0
@location.replace(@location + n * (1 - t) * @radius) # 補正方法1
# @location.replace((b + ba.project_onto(bc)) + (n * @radius)) # 補正方法2
@velocity = @velocity - @velocity.project_onto_normalized(n) * (1 + @restitution) # 反発
end
if !$app.pause
@acceleration += $app.gravity
@velocity += @acceleration
@location.replace(@location + @velocity)
@location.replace(@location.modulo($app.window_wh))
@acceleration = V.zero
end
$app.vputs "velocity: #{@velocity.length.round(2)} #{@velocity.round(2)}"
$app.point_draw(@location)
$app.circle_draw(@location, @radius, color: :grey)
$app.vector_draw(@location, @location + @velocity.clamp_length_min(@radius), "a")
end
end
class App < Base
attr_accessor :ball
attr_accessor :wall
attr_accessor :gravity
attr_accessor :pause
def initialize
super
@pause = true
preset1
end
def preset1
@gravity = V[0.0, 0.03]
@ball = Ball.new
@ball.location = window_wh * V[0.8, 0.4]
@ball.velocity = V[-0.5, -2.0]
@ball.restitution = 0.7
@wall = Wall.new
@wall.p0 = window_wh * V[0.9, 0.7]
@wall.p1 = window_wh * V[0.1, 0.7]
@points = [@ball.location, @wall.p0, @wall.p1]
end
def button_down(id)
super
case id
when Gosu::KB_R
preset1
when Gosu::KB_P
@pause ^= true
end
end
def draw
super
@wall.draw
@ball.draw
vputs "[p] pause: #{@pause}"
end
show
end
ベクトル確認
class App < Base
def initialize
super
preset1
end
def preset1
a = V[400.0, 200.0]
v = V[640.0, 87.0]
b = V[548.0, 30.0]
c = V[548.0, 370.0]
@points = [a, v, b, c]
end
def preset2
a = V[400.0, 200.0]
v = V[400.0, 200.0]
b = V[677.0, 327.0]
c = V[169.0, 365.0]
d = V[558.0, 300.0]
@points = [a, v, b, c, d]
end
def button_down(id)
super
case id
when Gosu::KB_1
preset1
when Gosu::KB_2
preset2
end
end
def draw
super
a, v, b, c, d = @points
bc = c - b
bcn = bc.normalize
n = bcn.perp
vputs "a: #{a.round}"
vputs "v: #{v.round}"
vputs "b: #{b.round}"
vputs "c: #{c.round}"
vputs "d: #{d.round}" if d
vputs
ar = 150
am = PI * ar**2
av = v - a
as = av.dot(n)
ab = b - a
ba = a - b
(ba.cross(bcn) - ab.dot(n)).abs == 0 or raise
# 補正する場合
if true
distance = ba.dot(n)
t = distance / ar
tt = a - n * ar * t
point_draw tt, color: :blue
if t < 1.0
a2 = a + n * (1 - t) * ar
if button_down?(Gosu::KB_Z) || true
circle_draw(a2, ar, color: :blue_light)
end
end
end
avx = av.project_onto_normalized(n)
avy = av.reject_from_normalized(n)
e = 0.5
as2 = -as * e
avx2 = n * as2
av2 = avy + avx2
av2.abs_diff_eq(av - (1 + e) * n * av.dot(n), 0.001) or raise
av3 = av.bounce(n) * e
circle_draw(a, ar, color: :grey)
vector_draw(a, v, "", "v", name: "av")
vector_draw(a, a + avx, "", "", color: :blue, name: "avx")
vector_draw(a, a + avy, "a", "", color: :red, name: "avy")
line_draw(a, a + avx, color: :grey, infinity: true)
line_draw(b, c, infinity: true)
vector_draw(b, c, "b", "c")
vector_draw(b, d, "", "d") if d
vputs "av: #{av.round}"
vputs "avx: #{avx.round}"
vputs "ar: #{ar}"
vputs "as: #{as.round}"
vputs "e: #{e}"
vputs "as2: #{as2.round}"
vputs "avx2: #{avx2.round}"
vputs "av2: #{av2.round}"
vputs "av3: #{av3.round}"
vputs
vector_draw(a, a + av3, "", "", name: "av3", color: :grey_lighter)
vector_draw(a, a + avx2, "", "", name: "avx2", color: :magenta)
vector_draw(a, a + av2, "", "", name: "av2", color: :green)
if d
line_draw(b, b + n, infinity: true)
bd = d - b
bdx = bd.project_onto_normalized(n)
vector_draw(b, b + bdx, "", "", name: "bdx", color: :blue)
end
end
show
end
Discussion