Collision impulses

This commit is contained in:
yaw-man 2023-01-14 14:47:46 -04:00
parent a2d94def4d
commit c8fe7b7d36
5 changed files with 112 additions and 26 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
build/ build/
cover.png

View File

@ -36,10 +36,13 @@ local function DetectCollision( curMarble, newMarble, curWave, newWave )
local fRadius, fTheta = tPolar( newMarble ) local fRadius, fTheta = tPolar( newMarble )
local iWaveRadius = curWave:Interpolate( iTheta ) local iWaveRadius = curWave:Interpolate( iTheta )
local fWaveRadius = curWave:Interpolate( fTheta ) local fWaveRadius = curWave:Interpolate( fTheta )
local speed = newMarble.dx * newMarble.dx + newMarble.dy * newMarble.dy
--Case 1: marble is already outside the curve. --Case 1: marble is already outside the curve.
if iRadius > iWaveRadius then if iRadius > iWaveRadius then
return { return {
speed = speed,
dt = newMarble.t - curMarble.t, dt = newMarble.t - curMarble.t,
t = curMarble.t, t = curMarble.t,
r = iWaveRadius, r = iWaveRadius,
@ -51,6 +54,7 @@ local function DetectCollision( curMarble, newMarble, curWave, newWave )
if fRadius > fWaveRadius then if fRadius > fWaveRadius then
--This isn't right, fix later. --This isn't right, fix later.
return { return {
speed = speed,
dt = 0, dt = 0,
t = newMarble.t, t = newMarble.t,
r = fWaveRadius, r = fWaveRadius,

View File

@ -1,5 +1,5 @@
local love = love local love = love
local step = 1.0 / 120.0 local step = 1.0 / 120
local sitelenpona local sitelenpona
local text local text
local marble local marble
@ -107,7 +107,8 @@ local function BeatScore( t )
--Safety: avoid a dbz. --Safety: avoid a dbz.
if mu < 0.001 or dt < 0.001 then if mu < 0.001 or dt < 0.001 then
error( "DBZ! Beat length too small." ) return 0.0
--error( "DBZ! Beat length too small." )
end end
--Calculate beat score. --Calculate beat score.
@ -171,6 +172,31 @@ function love.draw()
love.graphics.push( "transform" ) love.graphics.push( "transform" )
love.graphics.applyTransform( transform ) love.graphics.applyTransform( transform )
wave.Draw() wave.Draw()
if debugRenderImpact then
love.graphics.setLineWidth( 0.01 )
love.graphics.setColor( 1, 0, 0, 0.5 ) --Red: Incoming
love.graphics.line(
debugRenderImpact.xi,
debugRenderImpact.yi,
debugRenderImpact.xf,
debugRenderImpact.yf)
love.graphics.setColor( 0, 1, 0, 0.5 ) --Green: Normal
love.graphics.line(
debugRenderImpact.xi,
debugRenderImpact.yi,
debugRenderImpact.xn,
debugRenderImpact.yn)
love.graphics.setColor( 0, 0, 1, 0.5 ) -- Blue: Outgoing
love.graphics.line(
debugRenderImpact.xi,
debugRenderImpact.yi,
debugRenderImpact.vxout,
debugRenderImpact.vyout)
end
love.graphics.pop() love.graphics.pop()
sitelenpona.Draw( text.tok[state.currentBeat] ) sitelenpona.Draw( text.tok[state.currentBeat] )

View File

@ -26,7 +26,47 @@ end
local function OnImpact( impact ) local function OnImpact( impact )
--Adjust current trajectory according to collision. --Adjust current trajectory according to collision.
if not impact.dt then return end if not impact.dt then return end
return Integrate( impact.dt ) --Hmm! Maybe this should be a fixed step instead for stability's sake.
local x, y = impact.r * math.cos( impact.th ), impact.r * math.sin( impact.th )
local vx, vy = newState.dx, newState.dy --Velocity of particle going into collision.
local unx, uny = math.cos( impact.normal ), math.sin( impact.normal ) --Outward-facing normal of wave.
local uvx, uvy --Unit vector velocity of particle.
local speed = math.sqrt( vx * vx + vy * vy )
if speed < 0.01 then
--If the marble is motionless, there is no angular velocity wrt 0,
--so the wave is headed directly inward.
--We handle the collision as if the marble is headed directly outward.
uvx, uvy = unx, uny
vx, vy = uvx, uvy
else
uvx, uvy = vx / speed , vy / speed
end
--Get signed angle between normal and incoming velocity (both unit vectors)
local dot = unx * uvy - uny * uvx
--Fudge factor: apply an impulse inward so that you don't stick or slide on the wave.
local inward = ( dot > 0 ) and dot or -dot
inward = inward * inward * inward
--Calculate the rotation matrix:
--counterclockwise rotation by 2 * pi - 2 * arccos( n dot v )
local c, s = 1 - 2 * dot * dot, - 2 * dot * math.sqrt( 1 - dot * dot )
--Apply:
local vxout, vyout =
inward * (- math.cos(impact.th) ) - (1.0 - inward) * ( vx * c - vy * s ),
inward * (- math.sin(impact.th) ) - (1.0 - inward) * ( x * s + vy * c )
curState.x, curState.y = x, y
curState.dx, curState.dy = 0.5 * vxout, 0.5 * vyout
debugRenderImpact = { xi = x, yi = y, xf = x - vx, yf = y - vy,
xn = 0.2 * unx + x, yn = 0.2 * uny + y,
vxout = x + vxout, vyout = y + vyout}
return Integrate( math.max( impact.dt , 1 / 60 ) ) --Hmm! Maybe this should be a fixed step instead for stability's sake.
end end
local function Update() local function Update()

View File

@ -1,15 +1,23 @@
--Render and simulate 1D wave equation. --Render and simulate 1D wave equation.
local love = love local love = love
local old, cur, new --States add beginning of last tick, current tick, and next tick respectively. local N = 15
local SOUNDSPEED = 0.5
local IMPULSESIZE = 20
local DAMPING = 0.01
local N = 17
local SOUNDSPEED = 2.0 local old, cur, new --States add beginning of last tick, current tick, and next tick respectively.
local function Current() return cur end local function Current() return cur end
local function Next() return new end local function Next() return new end
local Integrate
local Interpolate
local Derivative
local SecondDerivative
local DFT
--Calculate discrete fourier transform of radius function. --Calculate discrete fourier transform of radius function.
local DFT
do do
local twiddlec = {} local twiddlec = {}
local twiddles = {} local twiddles = {}
@ -43,7 +51,7 @@ do
end end
--Minimal-oscillation interpolant of a real function from its discrete Fourier coefficients. --Minimal-oscillation interpolant of a real function from its discrete Fourier coefficients.
local Interpolate = function( wave, x ) Interpolate = function( wave, x )
local re, im = wave.dftre, wave.dftim local re, im = wave.dftre, wave.dftim
local y = re[1] local y = re[1]
@ -61,7 +69,7 @@ local Interpolate = function( wave, x )
end end
--Derivative of the interpolation. --Derivative of the interpolation.
local Derivative = function( wave, x ) Derivative = function( wave, x )
local re, im = wave.dftre, wave.dftim local re, im = wave.dftre, wave.dftim
local y = 0 local y = 0
@ -79,7 +87,7 @@ local Derivative = function( wave, x )
end end
--Second derivative of the interpolation. --Second derivative of the interpolation.
local SecondDerivative = function( wave, x ) SecondDerivative = function( wave, x )
local re, im = wave.dftre, wave.dftim local re, im = wave.dftre, wave.dftim
local y = 0 local y = 0
@ -103,13 +111,20 @@ local function AliasedSinc( theta, x )
end end
--Apply bandlimited impulse to wave. --Apply bandlimited impulse to wave.
local Impulse = function( wave, location, magnitude ) local function OnImpact( impact )
local speed = wave.vrad
local r = cur.radii
local theta = impact.th
local magnitude = IMPULSESIZE * impact.speed
local dt = math.max( impact.dt, 1 / 120.0 )
for i = 0, N - 1 do for i = 0, N - 1 do
speed[ i + 1 ] = speed[ i + 1 ] + magnitude * AliasedSinc( 2.0 * math.pi * i, location ) r[ i + 1 ] = r[ i + 1 ] + dt * magnitude * AliasedSinc( theta, 2.0 * math.pi * i / N )
end end
--We've updated the positions, now we need to take a DFT
--in order to get the bandlimited second spatial derivative.
cur:DFT()
return Integrate( dt )
end end
local mt = { __index = { local mt = { __index = {
@ -134,7 +149,7 @@ local function Wave( )
} }
for i = 1, N do for i = 1, N do
t.radii[i] = 1.0 + 0.05 * math.sin( i * 2.0 * math.pi / N ) t.radii[i] = 1.0 + 0.05 * ( i/N + math.sin( i * 2.0 * math.pi / N ))
t.vrad[i] = 0.0 t.vrad[i] = 0.0
end end
DFT( t ) DFT( t )
@ -166,11 +181,11 @@ local function Draw()
th = ( i - 1 + k ) * 2.0 * math.pi / N th = ( i - 1 + k ) * 2.0 * math.pi / N
local r = cur:Interpolate( th ) local r = cur:Interpolate( th )
local x, y = r * math.cos( th ), r * math.sin( th ) local x, y = r * math.cos( th ), r * math.sin( th )
--love.graphics.circle( "fill", x, y, 0.01 ) love.graphics.circle( "fill", x, y, 0.01 )
love.graphics.circle( "fill", th / math.pi - 1.0 , r, 0.01) --love.graphics.circle( "fill", th / math.pi - 1.0 , r, 0.01)
--First derivative. --First derivative.
love.graphics.setColor( 0, 1.0, 0, 0.7 ) --[[love.graphics.setColor( 0, 1.0, 0, 0.7 )
r = cur:Derivative( th ) r = cur:Derivative( th )
x, y = r * math.cos( th ), r * math.sin( th ) x, y = r * math.cos( th ), r * math.sin( th )
--love.graphics.circle( "fill", x, y, 0.01 ) --love.graphics.circle( "fill", x, y, 0.01 )
@ -184,7 +199,7 @@ local function Draw()
love.graphics.circle( "fill", th / math.pi - 1.0, r, 0.01) love.graphics.circle( "fill", th / math.pi - 1.0, r, 0.01)
love.graphics.setColor( 1.0, 1.0, 1.0, 0.2 ) love.graphics.setColor( 1.0, 1.0, 1.0, 0.2 )
love.graphics.circle( "fill", 2.0 * ( i + k ) / N - 1.2, 0, 0.02 ) love.graphics.circle( "fill", 2.0 * ( i + k ) / N - 1.2, 0, 0.02 )]]
end end
end end
@ -195,9 +210,6 @@ local function Draw()
love.graphics.circle( "fill", x, y, 0.02 ) love.graphics.circle( "fill", x, y, 0.02 )
end end
local function OnImpact( impact )
end
local function Update() local function Update()
@ -216,12 +228,15 @@ local function Update()
end end
end end
local function Integrate( step ) Integrate = function( step )
for i = 1, N do for i = 1, N do
--new.vrad[i] =
local rxx = cur:SecondDerivative( math.pi * 2.0 * ( i - 1 ) / N ) local rxx = cur:SecondDerivative( math.pi * 2.0 * ( i - 1 ) / N )
new.radii[i] = 2.0 * cur.radii[i] - old.radii[i] + step * step * SOUNDSPEED * rxx local r = ( 1.0 - DAMPING ) * ( 2.0 * cur.radii[i] - old.radii[i] + step * step * SOUNDSPEED * rxx ) --Verlet
+ DAMPING --Damping: oscillate toward 1.
if r > 1.5 then r = 1.5 end
if r < 0.5 then r = 0.5 end
new.radii[i] = r
end end
new:DFT( ) new:DFT( )