From d5c8b6e8c8e27e30a8dbdd2a6e8f64422620b2b2 Mon Sep 17 00:00:00 2001 From: Cameron Reikes Date: Mon, 6 Feb 2023 08:43:36 -0800 Subject: [PATCH] Partial broken refactor into testkit --- physics.jai | 371 ++++++++++++++++++++++------------------------------ testkit.jai | 223 +++++++++++++++++++++++++++++++ 2 files changed, 381 insertions(+), 213 deletions(-) create mode 100644 testkit.jai diff --git a/physics.jai b/physics.jai index 87164b0..4d6bb00 100644 --- a/physics.jai +++ b/physics.jai @@ -1,16 +1,9 @@ #import "Basic"; #import "Math"; -#import "Window_Creation"; -Debug :: #import "Debug"; -Input :: #import "Input"; -Simp :: #import "Simp"; +#import "testkit.jai"; TIMESTEP: float = 1.0 / 60.0; DEBUGGING :: true; -window_width : s32 = 1280; -window_height : s32 = 720; - -dbgprint :: print; // so can be xy :: (x: int, y: int) -> Vector2 { return xy(cast(float)x, cast(float)y); @@ -33,75 +26,25 @@ normalize_or_zero :: inline (using _v: Vector2) -> Vector2 #must { normalize_or_zero(*v); return v; } +length_squared :: inline (a: Vector2) -> float { + return a.x*a.x + a.y*a.y; +} negative :: (v: Vector2) -> Vector2 #must { return xy(-v.x, -v.y); } -WHITE :: Vector4.{1.0, 1.0, 1.0, 1.0}; -RED :: Vector4.{1.0, 0.0, 0.0, 1.0}; -GREEN :: Vector4.{0.0, 1.0, 0.0, 1.0}; -DrawingSettings :: struct { - world_space: bool = true; - color: Vector4 = WHITE; -}; -line :: (using d: DrawingSettings, _from: Vector2, _to: Vector2, thickness: float = 1.0) { - width := thickness; - from := _from; - to := _to; - if world_space { - from = world_to_screen(from); - to = world_to_screen(to); - } - Simp.set_shader_for_color(true); - normal := rotate(unit_vector(to - from), PI/2.0); - Simp.immediate_quad(from + normal*width, from - normal*width, to + normal*width, to - normal*width, color = color); +cross :: (a: Vector2, b: Vector2) -> float { + return a.x * b.y - a.y * b.x; } LastingPip :: struct { alive_for: float; pos: Vector2; d: DrawingSettings; } -pips : [10] LastingPip; -push_pip :: (d: DrawingSettings, at: Vector2) -{ - for * pips - { - if it.alive_for <= 0.0 - { - it.alive_for = 1.0; - it.pos = at; - it.d = d; - break; - } - } -} -font: *Simp.Dynamic_Font; -text :: (using d: DrawingSettings, t: string, p: Vector2) { - drawing_at := p; - if world_space { - drawing_at = world_to_screen(drawing_at); - } - Simp.draw_text(font, xx drawing_at.x, xx drawing_at.y, t, color = color); -} -draw_pips :: (dt: float) -{ - for * pips if it.alive_for > 0.0 - { - d := it.d; - d.color.w *= it.alive_for; - pip(d, it.pos); - it.alive_for -= dt; - } -} -pip :: (using d: DrawingSettings, _at: Vector2, size: float = 0.05) { - at := _at; - Simp.set_shader_for_color(true); - if world_space { - at = world_to_screen(at); - size /= camera.zoom; - } - Simp.immediate_quad(at + xy(-size, size), at + xy(size, size), at + xy(size, -size), at + xy(-size, -size), color = color); -} +hit_impulse :: (body_pos: Vector2, mouse_pos: Vector2) -> Vector2 { + return .{0, -15.0}; + //return normalize(body_pos - mouse_pos) * 15.0; +} ShapeType :: enum { Circle; Rectangle; @@ -112,138 +55,143 @@ Shape :: struct { offset: Vector2; // in local space of body radius: float; // only valid on circle halfsize: Vector2; // only valid on rectangle - mass: float = 1.0; - moment_of_inertia: float; + invmass: float = 1.0; + inv_moment_of_inertia: float; } Body :: struct { pos , vel , force : Vector2; // pos is in world space angle, angle_vel, torque: float; - static: bool; + elasticity: float; // 1 is bouncy, 0 is absorbant shape: Shape; } -local_to_world :: (using b: Body, local_point: Vector2) -> Vector2 { - return rotate(local_point, angle) + pos; +point_query :: (bodies: [] Body, world_point: Vector2) -> *Body { + for *bodies { + if length_squared(world_to_local(it, world_point)) < it.shape.radius*it.shape.radius return it; + } + return null; } -draw_body :: (using b: Body) { - if shape.type == { - case .Circle; - d: DrawingSettings = .{true, GREEN}; - POINTS :: 31; - drawing_circle_at: Vector2 = local_to_world(b, shape.offset); - for 0..POINTS { - theta: float = (it/cast(float)POINTS) * PI*2.0; - theta_next: float = (it+1)/cast(float)POINTS * PI*2.0; - from := drawing_circle_at + .{cos(theta),sin(theta)} * shape.radius; - to := drawing_circle_at + .{cos(theta_next),sin(theta_next)} * shape.radius; - line(d, from, to); - } +Contact :: struct { + point_a: Vector2; + point_b: Vector2; - case .Rectangle; - assert(false); + normal: Vector2; + separation: float; // positive when non-penetrating, negative when penetrating + time_of_impact: float; + + body_a: *Body; + body_b: *Body; +}; + +// returns world point +center_of_mass :: (using b: Body) -> Vector2 { + return b.pos; +} +apply_impulse_linear :: (using b: *Body, impulse: Vector2) { + if shape.invmass != 0.0 { + vel += impulse * shape.invmass; } } -/* -draw_rect :: (using r: Shape) { - assert(type == .Rectangle); - facing_to_right := rotate(xy(halfsize.x,0.0), angle); - facing_to_up := rotate(xy(0.0,halfsize.y), angle); - - upper_right := pos + facing_to_right + facing_to_up; - upper_left := pos - facing_to_right + facing_to_up; - lower_left := pos - facing_to_right - facing_to_up; - lower_right := pos + facing_to_right - facing_to_up; - - line(upper_left, upper_right); - line(upper_right, lower_right); - line(lower_right, lower_left); - line(lower_left, upper_left); +apply_impulse_angular :: (using b: *Body, angle_impulse: float) { + if shape.inv_moment_of_inertia != 0.0 { + angle_vel += angle_impulse * shape.inv_moment_of_inertia; + } } -*/ +apply_impulse :: (using b: *Body, world_point: Vector2, impulse: Vector2) { + apply_impulse_linear(b, impulse); + r: Vector2 = center_of_mass(b) - world_point; + dL: float = cross(r, impulse); + apply_impulse_angular(b, dL); +} -mouse :: () -> Vector2 { - x, y := get_mouse_pointer_position(); - pos: Vector2 = xy(cast(float)x, cast(float)y); +intersect :: (a: *Body, b: *Body, c: *Contact) -> bool { + c.body_a = a; + c.body_b = b; - // simp is lower left is (0, 0) and y+ is up - pos.y = window_height - pos.y; + ab := b.pos - a.pos; + c.normal = normalize_or_zero(ab); - return pos; -} + assert(a.shape.type == .Circle); + assert(b.shape.type == .Circle); -Camera :: struct { - pos: Vector2; - zoom: float = 1.0; -}; + c.point_a = a.pos + c.normal * a.shape.radius; + c.point_b = b.pos - c.normal * b.shape.radius; -camera : Camera; -screen_to_world :: (screen: Vector2) -> Vector2 { - using camera; - return (screen + pos)*zoom; + + radius_ab: float = a.shape.radius + b.shape.radius; + lengthsqr: float = length_squared(ab); + + if lengthsqr <= (radius_ab * radius_ab) { + return true; + } else { + return false; + } } -world_to_screen :: (world: Vector2) -> Vector2 { - using camera; - // world = (screen + pos)*zoom; - // world/zoom = screen + pos; - // world/zoom - pos = screen; - return (world/zoom) - pos; + +resolve :: (using c: Contact) { + // collision impulse + elasticity: float = body_a.elasticity * body_b.elasticity; + vab: Vector2 = body_a.vel - body_b.vel; + dot_product: float = dot(vab, normal); + impulse_j_magnitude: float = -(1.0 + elasticity) * dot_product / (body_a.shape.invmass + body_b.shape.invmass); + impulse_j: Vector2 = normal * impulse_j_magnitude; + apply_impulse_linear(body_a, impulse_j); + apply_impulse_linear(body_b, -impulse_j); + + // move outside eachother, center of mass of both bodies stays the same, but separate them. + ta: float = body_a.shape.invmass / (body_a.shape.invmass + body_b.shape.invmass); + tb: float = body_b.shape.invmass / (body_a.shape.invmass + body_b.shape.invmass); + + ds: Vector2 = point_b - point_a; + body_a.pos += ds * ta; + body_b.pos -= ds * tb; } -main :: () { - window := create_window(window_width, window_height, "A Window"); - // Actual render size in pixels can be different from the window dimensions we specified above (for example on high-resolution displays on macOS/iOS). - window_width, window_height = Simp.get_render_dimensions(window); +draw_body :: (d: DrawingSettings, using b: Body) { + if shape.type == { + case .Circle; + points: int = 32; + if shape.radius > 100.0 points = 128; + drawing_circle_at: Vector2 = local_to_world(b, shape.offset); + for 0..points { + theta: float = (it/cast(float)points) * PI*2.0; + theta_next: float = (it+1)/cast(float)points * PI*2.0; + from := drawing_circle_at + .{cos(theta),sin(theta)} * shape.radius; + to := drawing_circle_at + .{cos(theta_next),sin(theta_next)} * shape.radius; + line(d, from, to); + } + pip(d, pos); - camera.pos = -xy(window_width, window_height)/2.0; - camera.zoom = 0.01; + case .Rectangle; + using b.shape; + facing_to_right := rotate(xy(halfsize.x,0.0), angle); + facing_to_up := rotate(xy(0.0,halfsize.y), angle); + + upper_right := pos + facing_to_right + facing_to_up; + upper_left := pos - facing_to_right + facing_to_up; + lower_left := pos - facing_to_right - facing_to_up; + lower_right := pos + facing_to_right - facing_to_up; + + line(d, upper_left, upper_right); + line(d, upper_right, lower_right); + line(d, lower_right, lower_left); + line(d, lower_left, upper_left); + } +} - Simp.set_render_target(window); - font = Simp.get_font_at_size(".", "Roboto-Regular.ttf", 18); - assert(font != null); +main :: () { bodies: [..]Body; - array_add(*bodies, .{pos = .{0.0, 0.0}, shape = .{type = .Circle, radius = 1.0}}); - array_add(*bodies, .{pos = .{3.0, 0.0}, shape = .{type = .Circle, radius = 1.0}}); - - quit := false; - last_time := get_time(); - panning: bool = false; - last_mouse_pos := mouse(); - unprocessed_time: float = 0.0; - while !quit { - dt := cast(float)(get_time() - last_time); - last_time = get_time(); - Input.update_window_events(); - mouse_delta := mouse() - last_mouse_pos; // using Input.mouse_delta_x seems to incorrectly accumulate - last_mouse_pos = mouse(); - - for Input.get_window_resizes() { - Simp.update_window(it.window); // Simp will do nothing if it doesn't care about this window. - - if it.window == window { - should_reinit := (it.width != window_width) || (it.height != window_height); - - window_width = it.width; - window_height = it.height; - - //if should_reinit my_init_fonts(); // Resize the font for the new window size. - } - } - - camera.zoom *= 1.0 - 0.1*Input.mouse_delta_z/120.0; - if panning { - camera.pos -= mouse_delta; - - // this doesn't fix it - //Input.mouse_delta_x = 0; - //Input.mouse_delta_y = 0; - } + array_add(*bodies, .{pos = .{0.0, 0.0}, shape = .{type = .Circle, radius = 1.0}, elasticity = 0.5}); + array_add(*bodies, .{pos = .{0.0, -1003.0}, shape = .{type = .Circle, radius = 1000.0, invmass = 0.0}, elasticity = 1.0}); // ground sphere + array_add(*bodies, .{pos = .{3.0, 0.0}, shape = .{type = .Circle, radius = 1.0}, elasticity = 0.3}); + while !quit { // process physics unprocessed_time += dt; { @@ -256,68 +204,65 @@ main :: () { { //apply_force_at_point(*rects[0], xy(3, 0), rects[0].pos + xy(-0.1, 0.03)); } - for * bodies { - defer it.force = .{}; - defer it.torque = 0.0; - + // iterating by index so can do a sort of iteration that avoids double checking + // collisions between bodies. This is important because if intersections are double + // tested then they may have operated on shapes which were just separated but not + // moved by velocity + for 0..bodies.count-1 { + a := *bodies[it]; // calculate moment of inertia - if it.shape.type == { + if a.shape.type == { case .Circle; - it.shape.moment_of_inertia = PI * pow(it.shape.radius, 4.0) / 4.0; + a.shape.inv_moment_of_inertia = 1.0/(PI * pow(a.shape.radius, 4.0) / 4.0); + + case; + assert(false); } - // gravity - it.force.y += -9.81 * it.shape.mass; + // gravity must be impulse for some reason + apply_impulse_linear(a, .{0.0, -9.81 * (1.0 / a.shape.invmass) * TIMESTEP}); - if !it.static - { - it.vel += (it.force/it.shape.mass) * TIMESTEP; - it.pos += it.vel * TIMESTEP; - it.angle_vel += (it.torque / it.shape.moment_of_inertia) * TIMESTEP; - it.angle += it.angle_vel * TIMESTEP; + // collisions + for it+1..bodies.count-1 { + b := *bodies[it]; + contact: Contact; + if intersect(a, b, *contact) { + resolve(contact); + } } } - } - } + for * bodies { + defer { it.force = .{}; it.torque = 0.0; } - - Simp.immediate_begin(); - Simp.clear_render_target(0.0, 0.0, 0.0, 1.0); - - // draw grid - grid_d: DrawingSettings = .{true,.{0.2, 0.2, 0.2, 0.5}}; - text_d := grid_d; - text_d.color = .{0.6, 0.6, 0.6, 1.0}; - help_text_max :: 8; - help_zoom_max :: 0.04; - for x: -30..30 { - line(grid_d, xy(x, 30), xy(x, -30)); - if camera.zoom <= help_zoom_max && abs(x) < help_text_max then text(text_d, tprint("%m", x), .{xx x, 0.0}); - } - for y: -30..30 { - line(grid_d, xy(30, y), xy(-30, y)); - if camera.zoom <= help_zoom_max && abs(y) < help_text_max then text(text_d, tprint("%m", y), .{0.0, xx y}); + it.vel += (it.force * it.shape.invmass) * TIMESTEP; + it.pos += it.vel * TIMESTEP; + it.angle_vel += (it.torque * it.shape.inv_moment_of_inertia) * TIMESTEP; + it.angle += it.angle_vel * TIMESTEP; + } + } } - draw_pips(dt); - for bodies draw_body(it); + b := bodies[0]; + pip(.{}, local_to_world(b, world_to_local(b, mouse()))); - Simp.immediate_flush(); - - Simp.swap_buffers(window); + + hovering: *Body = point_query(bodies, screen_to_world(mouse())); + for * bodies { + d: DrawingSettings = .{true, GREEN}; + if hovering == it { + d.color = RED; + vector(d, screen_to_world(mouse()), hit_impulse(it.pos, screen_to_world(mouse())) * TIMESTEP); + } + draw_body(d, it); + } for Input.events_this_frame { - if it.type == .QUIT then quit = true; - if it.type == { - case .KEYBOARD; - if it.key_pressed && it.key_code == .ESCAPE { - quit = true; - } if it.key_code == .MOUSE_BUTTON_LEFT { panning = cast(bool)it.key_pressed; + to_tap := point_query(bodies, screen_to_world(mouse())); } } } diff --git a/testkit.jai b/testkit.jai new file mode 100644 index 0000000..25aa591 --- /dev/null +++ b/testkit.jai @@ -0,0 +1,223 @@ + +#import "Window_Creation"; +Debug :: #import "Debug"; +Simp :: #import "Simp"; +Input :: #import "Input"; + +Camera :: struct { + pos: Vector2; + zoom: float = 1.0; +}; + +window: Window; +quit: bool; +panning: bool = false; +camera : Camera; +mouse_frozen: bool; +mouse_frozen_at: Vector2; + + +local_to_world :: (using b: Body, local_point: Vector2) -> Vector2 { + return rotate(local_point, angle) + pos; +} +world_to_local :: (using b: Body, world_point: Vector2) -> Vector2 { + // world = rotate(local, angle) + pos + // world - pos = rotate(local, angle) + // rotate(world - pos, -angle) = local + return rotate(world_point - pos, -angle); +} +window_width : s32 = 1280; +window_height : s32 = 720; + +dbgprint :: print; // so can be detected and removed + +WHITE :: Vector4.{1.0, 1.0, 1.0, 1.0}; +RED :: Vector4.{1.0, 0.0, 0.0, 1.0}; +GREEN :: Vector4.{0.0, 1.0, 0.0, 1.0}; +ORANGE :: Vector4.{1.0, 0xA5/255, 0.0, 1.0}; +DrawingSettings :: struct { + world_space: bool = true; + color: Vector4 = WHITE; +}; +line :: (using d: DrawingSettings, _from: Vector2, _to: Vector2, thickness: float = 1.0) { + width := thickness; + from := _from; + to := _to; + if world_space { + from = world_to_screen(from); + to = world_to_screen(to); + } + Simp.set_shader_for_color(true); + normal := rotate(unit_vector(to - from), PI/2.0); + Simp.immediate_quad(from + normal*width, from - normal*width, to + normal*width, to - normal*width, color = color); +} +vector :: (d: DrawingSettings, from: Vector2, vector: Vector2) { + line(d, from, from+vector); + + arrow_head_length := length(vector) * 0.5; + + // arrow head + line(d, from + vector, from + vector + rotate(normalize(vector)*arrow_head_length, PI/2.0 + PI/4.0)); + line(d, from + vector, from + vector + rotate(normalize(vector)*arrow_head_length, -PI/2.0 + -PI/4.0)); +} + +pips : [10] LastingPip; +push_pip :: (d: DrawingSettings, at: Vector2) { + for * pips + { + if it.alive_for <= 0.0 + { + it.alive_for = 1.0; + it.pos = at; + it.d = d; + break; + } + } +} +draw_pips :: (dt: float) { + for * pips if it.alive_for > 0.0 + { + d := it.d; + d.color.w *= it.alive_for; + pip(d, it.pos); + it.alive_for -= dt; + } +} +pip :: (using d: DrawingSettings, _at: Vector2, size: float = 0.05) { + at := _at; + Simp.set_shader_for_color(true); + if world_space { + at = world_to_screen(at); + size /= camera.zoom; + } + Simp.immediate_quad(at + xy(-size, size), at + xy(size, size), at + xy(size, -size), at + xy(-size, -size), color = color); +} + +font: *Simp.Dynamic_Font; +text :: (using d: DrawingSettings, t: string, p: Vector2) { + drawing_at := p; + if world_space { + drawing_at = world_to_screen(drawing_at); + } + Simp.draw_text(font, xx drawing_at.x, xx drawing_at.y, t, color = color); +} + +init :: () { + window = create_window(window_width, window_height, "Test Kit"); + // Actual render size in pixels can be different from the window dimensions we specified above (for example on high-resolution displays on macOS/iOS). + window_width, window_height = Simp.get_render_dimensions(window); + + camera.pos = -xy(window_width, window_height)/2.0; + camera.zoom = 0.01; + + Simp.set_render_target(window); + + font = Simp.get_font_at_size(".", "Roboto-Regular.ttf", 18); + assert(font != null); +} +// screen coordinates +mouse_screen :: () -> Vector2 { + if mouse_frozen return mouse_frozen_at; + x, y := get_mouse_pointer_position(); + pos: Vector2 = xy(cast(float)x, cast(float)y); + + // simp is lower left is (0, 0) and y+ is up + pos.y = window_height - pos.y; + + return pos; +} +mouse_world :: () -> Vector2 { + return screen_to_world(mouse()); +} + +// query quit to see if should quit. Call this in a loop +frame_start :: () { + last_time := get_time(); + last_mouse_pos := mouse(); + unprocessed_time: float = 0.0; + + dt := cast(float)(get_time() - last_time); + last_time = get_time(); + Input.update_window_events(); + mouse_delta := mouse() - last_mouse_pos; // using Input.mouse_delta_x seems to incorrectly accumulate + last_mouse_pos = mouse(); + + for Input.get_window_resizes() { + Simp.update_window(it.window); // Simp will do nothing if it doesn't care about this window. + + if it.window == window { + should_reinit := (it.width != window_width) || (it.height != window_height); + + window_width = it.width; + window_height = it.height; + + //if should_reinit my_init_fonts(); // Resize the font for the new window size. + } + } + + camera.zoom *= 1.0 - 0.1*Input.mouse_delta_z/120.0; + if panning { + camera.pos -= mouse_delta; + } + Simp.immediate_begin(); + Simp.clear_render_target(0.0, 0.0, 0.0, 1.0); + + // draw grid + grid_d: DrawingSettings = .{true,.{0.2, 0.2, 0.2, 0.5}}; + text_d := grid_d; + text_d.color = .{0.6, 0.6, 0.6, 1.0}; + help_text_max :: 8; + help_zoom_max :: 0.04; + for x: -30..30 { + line(grid_d, xy(x, 30), xy(x, -30)); + if camera.zoom <= help_zoom_max && abs(x) < help_text_max then text(text_d, tprint("%m", x), .{xx x, 0.0}); + } + for y: -30..30 { + line(grid_d, xy(30, y), xy(-30, y)); + if camera.zoom <= help_zoom_max && abs(y) < help_text_max then text(text_d, tprint("%m", y), .{0.0, xx y}); + } + + draw_pips(dt); + + if mouse_frozen pip(.{false, ORANGE}, mouse_frozen_at, size = 20.0); + + + // handle inputs + for Input.events_this_frame { + if it.type == .QUIT then quit = true; + + if it.type == { + case .KEYBOARD; + if it.key_pressed { + if it.key_code == { + case .ESCAPE; + quit = true; + + case #char "T"; + if !mouse_frozen mouse_frozen_at = mouse(); + mouse_frozen = !mouse_frozen; + } + } + if it.key_code == .MOUSE_BUTTON_LEFT { + panning = cast(bool)it.key_pressed; + } + } + } +} + +frame_end :: () { + Simp.immediate_flush(); + Simp.swap_buffers(window); +} + +screen_to_world :: (screen: Vector2) -> Vector2 { + using camera; + return (screen + pos)*zoom; +} +world_to_screen :: (world: Vector2) -> Vector2 { + using camera; + // world = (screen + pos)*zoom; + // world/zoom = screen + pos; + // world/zoom - pos = screen; + return (world/zoom) - pos; +}