Last active
April 21, 2023 19:19
-
-
Save marcheiligers/1655e7ca45c4a575a4465c0725677506 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Stolen from Xenobrain and remixed (https://gist.github.com/xenobrain/5fa64f8a3e8f8f6f1b31eee4f870dd75) | |
TIME_STEP = 1 / 60 # delta time isn't required in DragonRuby but it really handy for tuning and debugging physics | |
COLLISION_BIAS = 0.05 # adds some energy into the collision to get objects to separate. tune this in steps of 0.01 | |
COLLISION_SLOP = 0.1 # amount shapes are allowed to overlap without triggering correction. helps avoid position jitter | |
COLLISION_ITERATIONS = 10 # how many times to run the solver. a good range is between 5 and 15 | |
CIRCLE_RADIUS = 10 | |
CIRCLE_RADIUS_2 = CIRCLE_RADIUS * 2 | |
CIRCLE_RADIUS_SQ = CIRCLE_RADIUS_2**2 | |
CIRCLE_MASS = 10_000 # Math::PI * CIRCLE_RADIUS * CIRCLE_RADIUS | |
CIRCLE_ROTATIONAL_INERTIA = 0.5 * CIRCLE_RADIUS * CIRCLE_RADIUS * CIRCLE_MASS | |
MAX_V_COMPONENT = 3 | |
MAX_ANGULAR_V = 10 | |
MAX_V_SQ = 10_000 | |
CIRCLE_COUNT = 0 | |
ITERS = 10 # how many iterations to run on each frame | |
GRAVITY = 100 * TIME_STEP | |
LINE_BOUNCE = 0.6 | |
def tick(args) | |
init(args) if args.state.tick_count.zero? | |
inputs(args) | |
i = -1 | |
while (i += 1) < ITERS | |
move(args) | |
collide(args) | |
end | |
draw(args) | |
end | |
def make_circle(args, x = nil, y = nil) | |
{ | |
x: x || rand(args.grid.w - CIRCLE_RADIUS_2) + CIRCLE_RADIUS, | |
y: y || rand(args.grid.h - CIRCLE_RADIUS_2) + CIRCLE_RADIUS, | |
w: CIRCLE_RADIUS_2, | |
h: CIRCLE_RADIUS_2, | |
angle: 0, | |
path: 'sprites/circle/violet.png', | |
radius: CIRCLE_RADIUS, | |
vx: rand(MAX_V_COMPONENT * 2) - MAX_V_COMPONENT, # velocity x | |
vy: rand(MAX_V_COMPONENT * 2) - MAX_V_COMPONENT, # velocity y | |
av: rand(MAX_ANGULAR_V), # angular velocity | |
bounce: 0.95, # 0..1 | |
friction: 0.2, | |
mass: CIRCLE_MASS, | |
rotational_inertia: CIRCLE_ROTATIONAL_INERTIA | |
}.sprite! | |
end | |
def init(args) | |
args.state.gravity = true | |
# Create a pile of circles | |
args.state.circles = Array.new(CIRCLE_COUNT) { make_circle(args) } | |
# Create some borders, and a couple of extra lines for funsies | |
args.state.lines ||= [ | |
{ x: 0, y: 20, x2: 1280, y2: 20, vx: 0, vy: 0, av: 0, bounce: 1, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY }.line!, | |
{ x: 0, y: 0, x2: 0, y2: 720, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY }.line!, | |
{ x: 1280, y: 0, x2: 1280, y2: 720, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY }.line!, | |
{ x: 0, y: 720, x2: 1280, y2: 720, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY}.line!, | |
# { x: 640, y: 360, x2: 700, y2: 250, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY}.line!, | |
# { x: 200, y: 400, x2: 360, y2: 250, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY}.line! | |
] | |
end | |
def inputs(args) | |
args.state.gravity = !args.state.gravity if args.inputs.keyboard.key_down.g | |
args.state.circles << make_circle(args, args.inputs.mouse.x, args.inputs.mouse.y) if args.inputs.keyboard.key_down.c | |
args.state.circles.shift if args.inputs.keyboard.key_down.d | |
if args.inputs.keyboard.key_down.l | |
closest_line = find_closest_line(args.inputs.mouse, args.state.lines) | |
putz closest_line | |
args.state.lines.delete(closest_line.second) if !closest_line.nil? && closest_line.first < 15 && !args.state.lines.first(4).include?(closest_line.second) | |
end | |
$gtk.reset if args.inputs.keyboard.key_down.r | |
if args.state.new_line | |
args.state.new_line.x2 = args.inputs.mouse.x | |
args.state.new_line.y2 = args.inputs.mouse.y | |
args.state.new_line = nil if args.inputs.mouse.up | |
elsif args.state.new_line.nil? && args.inputs.mouse.down | |
args.state.new_line = { | |
x: args.inputs.mouse.x, | |
y: args.inputs.mouse.y, | |
x2: args.inputs.mouse.x, | |
y2: args.inputs.mouse.y, | |
vx: 0, | |
vy: 0, | |
av: 0, | |
bounce: LINE_BOUNCE, | |
friction: 0.4, | |
mass: Float::INFINITY, | |
rotational_inertia: Float::INFINITY | |
}.line! | |
args.state.lines << args.state.new_line | |
end | |
end | |
def draw(args) | |
args.outputs.primitives << args.state.circles << args.state.lines | |
end | |
def collide(args) | |
ls = args.state.lines | |
kl = ls.length | |
cs = args.state.circles | |
cl = cs.length | |
i = -1 | |
while (i += 1) < cl | |
c1 = cs[i] | |
j = i | |
while (j += 1) < cl | |
c2 = cs[j] | |
next if c1.mass == Float::INFINITY && c2.mass == Float::INFINITY | |
collision = find_circle_circle c1, c2 | |
calc_collision collision | |
end | |
k = -1 | |
while (k += 1) < kl | |
l1 = ls[k] | |
collision = find_circle_line c1, l1 | |
calc_collision collision | |
end | |
end | |
end | |
def move(args) | |
cs = args.state.circles | |
i = -1 | |
l = cs.length | |
while (i += 1) < l | |
c = cs[i] | |
if c.mass != Float::INFINITY | |
c.vy -= GRAVITY / ITERS if args.state.gravity | |
c.x += c.vx / ITERS | |
c.y += c.vy / ITERS | |
c.angle += (c.av * TIME_STEP).to_degrees / ITERS | |
end | |
end | |
line = args.state.lines.first | |
line.y += line.vy / ITERS | |
line.y2 += line.vy / ITERS | |
line.vy = (args.state.tick_count % 360 * 40).sin | |
line.bounce = 1.2 + line.vy / 2 | |
# putz line.bounce | |
end | |
def find_closest_line(point, lines) | |
lines.map { |line| [distance_from_point_to_line(point, line), line] }.sort_by(&:first).first | |
end | |
def distance_from_point_to_line(point, line) | |
a = line.y2 - line.y | |
b = line.x - line.x2 | |
c = line.y * line.x2 - line.x * line.y2 | |
(a * point.x + b * point.y + c).abs / Math.sqrt(a**2 + b**2) | |
end | |
def find_circle_circle a, b | |
circle_ar = a.radius || [a.w, a.h].max * 0.5 | |
circle_br = b.radius || [b.w, b.h].max * 0.5 | |
circle_ax = a.x + circle_ar | |
circle_ay = a.y + circle_ar | |
circle_bx = b.x + circle_br | |
circle_by = b.y + circle_br | |
dx = circle_bx - circle_ax | |
dy = circle_by - circle_ay | |
distance = dx * dx + dy * dy | |
min_distance = circle_ar + circle_br | |
# distance should be less than the sum of radii | |
# zero distance means the circles have the same centers, do nothing | |
return if (distance > min_distance * min_distance) || distance.zero? | |
distance = Math.sqrt distance | |
dx /= distance | |
dy /= distance | |
contact = { r1x: circle_ax + circle_ar * dx, | |
r1y: circle_ay + circle_ar * dy, | |
r2x: circle_bx - circle_br * dx, | |
r2y: circle_by - circle_br * dy, | |
depth: distance - min_distance, | |
jn: 0, jt: 0 } | |
{ a: a, ax: circle_ax, ay: circle_ay, | |
b: b, bx: circle_bx, by: circle_by, | |
normal_x: dx, normal_y: dy, | |
contacts: [contact] } | |
end | |
def find_circle_line c, l | |
circle_r = c.radius || [c.w, c.h].max * 0.5 | |
line_r = l.radius || 0 | |
circle_x = c.x + circle_r | |
circle_y = c.y + circle_r | |
line_x = l.x2 - l.x | |
line_y = l.y2 - l.y | |
line_len_sq = [line_x * line_x + line_y * line_y, 1].max | |
t = ((line_x * (circle_x - l.x) + line_y * (circle_y - l.y)) / line_len_sq).clamp(0, 1) | |
closest_x = l.x + line_x * t | |
closest_y = l.y + line_y * t | |
dx = closest_x - circle_x | |
dy = closest_y - circle_y | |
distance = dx * dx + dy * dy | |
min_distance = circle_r + line_r | |
return if distance > min_distance * min_distance | |
distance = Math.sqrt distance | |
dx /= distance | |
dy /= distance | |
contact = { r1x: circle_x + circle_r * dx, | |
r1y: circle_y + circle_r * dy, | |
r2x: closest_x - line_r * dx, | |
r2y: closest_y - line_r * dy, | |
depth: distance - min_distance, | |
jn: 0, jt: 0 } | |
{ a: c, ax: circle_x, ay: circle_y, | |
b: l, bx: line_x, by: line_y, | |
normal_x: dx, normal_y: dy, | |
contacts: [contact] } | |
end | |
def calc_collision collision | |
return unless collision | |
a = collision[:a] | |
b = collision[:b] | |
nx = collision[:normal_x] | |
ny = collision[:normal_y] | |
average_bounce = a.bounce * b.bounce | |
average_friction = a.friction * b.friction | |
inv_m_a = 1.0 / a.mass | |
inv_m_b = 1.0 / b.mass | |
inv_i_a = 1.0 / a.rotational_inertia | |
inv_i_b = 1.0 / b.rotational_inertia | |
inv_mass_sum = inv_m_a + inv_m_b | |
fn.each collision.contacts do |contact| | |
# contact point in local space | |
r1x = contact[:r1x] - collision[:ax] | |
r1y = contact[:r1y] - collision[:ay] | |
r2x = contact[:r2x] - collision[:bx] | |
r2y = contact[:r2y] - collision[:by] | |
# contact point cross normal, tangent | |
r1cn = r1x * ny - r1y * nx | |
r2cn = r2x * ny - r2y * nx | |
r1ct = r1x * nx + r1y * ny | |
r2ct = r2x * nx + r2y * ny | |
# sum of masses in normal and tangent directions | |
mass_normal = 1.0 / (inv_mass_sum + inv_i_a * r1cn * r1cn + inv_i_b * r2cn * r2cn) | |
mass_tangent = 1.0 / (inv_mass_sum + inv_i_a * r1ct * r1ct + inv_i_b * r2ct * r2ct) | |
# penetration correction -- feed positional error into separation impulse (Baumgarte stabilization) | |
bias = COLLISION_BIAS * [0.0, contact[:depth] + COLLISION_SLOP].min / TIME_STEP | |
# relative velocity | |
rvx = b.vx - r2y * b.av - (a.vx - r1y * a.av) | |
rvy = b.vy + r2x * b.av - (a.vy + r1x * a.av) | |
# relative velocity along normal * average bounce | |
bounce = (rvx * nx + rvy * ny) * average_bounce | |
COLLISION_ITERATIONS.times do | |
# update the relative velocity | |
vrx = b.vx - r2y * b.av - (a.vx - r1y * a.av) | |
vry = b.vy + r2x * b.av - (a.vy + r1x * a.av) | |
# relative velocity along normal and tangent | |
rvn = vrx * nx + vry * ny | |
rvt = vrx * -ny + vry * nx | |
# impulse scalar (aka lambda, lagrange multiplier) | |
jn = -(bounce + rvn + bias) * mass_normal | |
previous_jn = contact[:jn] | |
contact[:jn] = [previous_jn + jn, 0.0].max | |
# tangent scalar, cannot exceed force along normal (Coulomb's law) | |
max_jt = average_friction * contact[:jn] | |
jt = -rvt * mass_tangent | |
previous_jt = contact[:jt] | |
contact[:jt] = (previous_jt + jt).clamp(-max_jt, max_jt) | |
jn = contact[:jn] - previous_jn | |
jt = contact[:jt] - previous_jt | |
impulse_x = nx * jn - ny * jt | |
impulse_y = nx * jt + ny * jn | |
a[:vx] -= impulse_x * inv_m_a | |
a[:vy] -= impulse_y * inv_m_a | |
a[:av] -= inv_i_a * (r1x * impulse_y - r1y * impulse_x) | |
clamp_v(a) | |
b[:vx] += impulse_x * inv_m_b | |
b[:vy] += impulse_y * inv_m_b | |
b[:av] += inv_i_b * (r2x * impulse_y - r2y * impulse_x) | |
clamp_v(b) | |
end | |
end | |
end | |
def clamp_v(a) | |
v = a.vx**2 + a.vy**2 | |
return if v < MAX_V_SQ | |
f = MAX_V_SQ / v | |
a.vx *= f | |
a.vy *= f | |
end | |
$gtk.reset |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment