(* Source of this demo: * http://www.ffconsultancy.com/ocaml/index.html * Non-free, not for redistribution in Fedora. *) open Printf let pi = 4. *. atan 1. module Array = struct include Array let for_all p a = fold_left (fun b x -> p x && b) true a end type vec2 = { x: float; y: float } let vec2 x y = {x=x; y=y} let zero = vec2 0. 0. let of_list = function [x; y] -> vec2 x y | _ -> invalid_arg "Vec2.of_list" let ( +| ) a b = {x = a.x +. b.x; y = a.y +. b.y} let ( -| ) a b = {x = a.x -. b.x; y = a.y -. b.y} let ( ~| ) a = {x = -. a.x; y = -. a.y} let ( *| ) s r = {x = s *. r.x; y = s *. r.y} let normal a = { x = a.y; y = -. a.x } let dot a b = a.x *. b.x +. a.y *. b.y let length2 r = dot r r let length r = sqrt(length2 r) let unitise r = 1. /. length r *| r (* Get the time since the program started. *) let time = let t = Unix.gettimeofday() in fun () -> Unix.gettimeofday() -. t let width = ref 1 and height = ref 1 (* Reshape the viewport and store the width and height. *) let reshape ~w ~h = GlDraw.viewport ~x:0 ~y:0 ~w ~h; width := max 1 w; height := max 1 h (* Pass a single vertex to the OpenGL. *) let vertex {x=x; y=y} = GlDraw.vertex ~x ~y () let translate r = GlMat.translate ~x:r.x ~y:r.y () let rotate angle = let angle = angle *. 180. /. pi in GlMat.rotate ~angle ~z:1. () let scale s = GlMat.scale ~x:s ~y:s () let protect f g = try f(); g() with e -> g(); raise e let mat f = GlMat.push(); protect f GlMat.pop let render prim f = GlDraw.begins prim; protect f GlDraw.ends let circle x = vec2 (sin x) (cos x) (* Memoize OpenGL calls in a display list. *) let gl_memoize f = let dl = ref None in fun () -> match !dl with | Some dl -> GlList.call dl | None -> let dl' = GlList.create `compile in f (); GlList.ends (); dl := Some dl' let rec iter f l u = if l let n = 36 in let aux i = circle (2. *. pi *. float i /. float n) in render `triangle_fan (fun () -> vertex zero; iter (fun i -> vertex (aux i)) 0 (n/4+1)); render `triangle_fan (fun () -> vertex zero; iter (fun i -> vertex (aux i)) (n/2) (3*n/4+1)); render `triangle_strip (fun () -> iter (fun i -> vertex (0.9 *| aux i); vertex (aux i)) 0 (n+1))) type circle = { center: vec2; radius: float } type ball = { circle: circle; velocity: vec2; angle: float; angular_velocity: float } let make_ball ?(v=vec2 0.1 0.) ?(r=0.05) x y = { circle = { center = vec2 x y; radius = r }; velocity = v; angle = 0.; angular_velocity = 0. } type surface = Line of vec2 * vec2 | Circle of vec2 * float let surfaces = ref [Line (vec2 0. 0., vec2 0. 1.); Line (vec2 0. 1., vec2 1. 1.); Line (vec2 1. 1., vec2 1. 0.); Line (vec2 1. 0.2, vec2 0. 0.)] (* Split balls. *) let balls = ref [|{(make_ball 0.5 0.9) with velocity = vec2 0. (-1.)}; {(make_ball 0.45 0.05) with velocity = vec2 0. 0.}; {(make_ball 0.55 0.05) with velocity = vec2 0. 0.}|] let surfaces = ref [Line (vec2 0. 0., vec2 0. 1.); Line (vec2 0. 1., vec2 1. 1.); Line (vec2 1. 1., vec2 1. 0.); Line (vec2 1. 0., vec2 0. 0.)] let g = vec2 0. 0.1 let n_balls = try min 100 (max 0 (int_of_string(Sys.argv.(1)))) with _ -> 3 (* Funnel *) let balls = let n = 10 in ref (Array.init n_balls (fun x -> make_ball ~v:(vec2 0. 0.) ~r:0.02 (0.2 +. 0.03 *. float ((x/n) mod 2) +. 0.06 *. float (x mod n)) (0.9 -. 0.04 *. float (x/n)))) let surfaces = let n = 128 in let aux i = let r = circle (2. *. pi *. float (i + n/4) /. float n) in vec2 (0.5 +. 0.5 *. r.x) (1. +. r.y) in ref (Array.to_list (Array.init n (fun i -> Line (aux (i-1), aux i)))) let surfaces = ref (Circle (vec2 0.37 0.3, 0.11) :: Circle (vec2 0.63 0.3, 0.11) :: !surfaces) let g = vec2 0. 0.5 let display_ball ball = mat (fun () -> translate ball.circle.center; rotate ball.angle; scale ball.circle.radius; render_ball()) let display_balls () = Array.iter display_ball !balls let display_surface = function Line (v1, v2) -> GlDraw.begins `lines; List.iter vertex [v1; v2]; GlDraw.ends () | Circle (c, r) -> GlMat.push(); mat (fun () -> translate c; scale r; let n = 360 in GlDraw.begins `line_loop; for i=0 to n-1 do vertex (circle (2. *. pi *. float i /. float n)) done; GlDraw.ends ()) let display_surfaces = gl_memoize (fun () -> List.iter display_surface !surfaces) let display () = GlClear.clear [ `color; `depth ]; Gl.enable `depth_test; GlFunc.depth_func `lequal; (* Initialise a orthogonal projection of the 2D scene. *) GlMat.mode `projection; GlMat.load_identity (); GlMat.ortho ~x:(0., 1.) ~y:(0., 1.) ~z:(0., 1.); GlMat.mode `modelview; GlMat.load_identity (); GlDraw.color (1., 1., 1.) ~alpha:1.; display_balls(); display_surfaces(); Gl.finish (); Unix.sleep 0; Glut.swapBuffers () let clamp l u (x : float) = if xu then u else x let circle_circle c1 r1 c2 r2 = let s = c2 -| c1 in c1 +| 0.5 *. (length s +. r1 -. r2) *| unitise s let circle_circle c1 r1 c2 r2 = let s = c2 -| c1 in let l = length s in let s = (1. /. l) *| s in c1 +| 0.5 *. (l +. r1 -. r2) *| s (* Find the point of impact between a circle and a surface. *) let point_of_impact circle = function Line (p1, p2) -> (* Find the closest approach of the finite line v1 -> v2 to the point r. *) let p21 = p2 -| p1 in let x = clamp 0. 1. (dot (circle.center -| p1) p21 /. length2 p21) in p1 +| x *| p21 | Circle (c2, r2) -> circle_circle circle.center circle.radius c2 r2 let circle_circle_intersects c1 c2 = let p = circle_circle c1.center c1.radius c2.center c2.radius in length(c1.center -| p) < c1.radius let rec update_ball dt (ball, impacts as accu) surface = let p = point_of_impact ball.circle surface in let d = ball.circle.center -| p in if length d > ball.circle.radius then accu else begin (* Local basis through center and perpendicular. *) let a = unitise d in let b = normal a in (* Resolve the velocity at the point of impact into components through the center and perpendicular. *) let dva = dot ball.velocity a in let dvb = dot ball.velocity b in if dva >= 0. then accu else { ball with velocity = ball.velocity +| 2. *. abs_float dva *| a -| 0. *. ball.angular_velocity *. ball.circle.radius *| b; angular_velocity = -. dvb /. ball.circle.radius }, p::impacts end (* Bisect the time slice until there is only one impact. *) let rec update t t' balls = let dt = t' -. t in (* Ball-ball impacts. *) let balls', impacts = let balls = Array.copy balls in let impacts = Array.map (fun _ -> []) balls in let n = Array.length balls in for i=0 to n-1 do (* Ball-surface impacts *) (fun (b, is) -> balls.(i) <- b; impacts.(i) <- is) (List.fold_left (update_ball dt) (balls.(i), impacts.(i)) !surfaces); (* Ball-ball impacts. *) if i>0 then let b1 = balls.(i) in for j=0 to i-1 do let b2 = balls.(j) in let c1 = b1.circle and c2 = b2.circle in let p = circle_circle c1.center c1.radius c2.center c2.radius in if length(c1.center -| p) < c1.radius then begin let u1 = b1.velocity and u2 = b2.velocity in let da1 = b1.angular_velocity and da2 = b2.angular_velocity in let r1 = c1.radius and r2 = c2.radius in (* Find the velocity difference to the center-of-momentum frame. *) let com = 0.5 *| (u1 +| u2) in (* Move to COM frame. *) let u1 = u1 -| com and u2 = u2 -| com in let u = unitise (c2.center -| c1.center) in let v = normal u in let impulse = u2 -| u1 +| (da1 *. r1 -. da2 *. r2) *| v in let impulse_u = dot u impulse *| u in let impulse_v = dot v impulse in let v1 = u1 +| impulse_u and v2 = u2 -| impulse_u in let da1' = da1 +. impulse_v *. r1 in let da2' = da2 -. impulse_v *. r2 in (* Move from COM frame. *) let v1 = v1 +| com and v2 = v2 +| com in balls.(i) <- { balls.(i) with velocity = v1; angular_velocity = da1' }; balls.(j) <- { balls.(j) with velocity = v2; angular_velocity = da2' }; impacts.(i) <- p :: impacts.(i); impacts.(j) <- p :: impacts.(j); end done; done; for i=0 to n-1 do let b = balls.(i) in let r = b.circle.center and dr = b.velocity in let a = b.angle and da = b.angular_velocity in let rec aux dr = function [] -> { balls.(i) with circle = { balls.(i).circle with center = r +| dt *| dr }; angle = mod_float (a +. dt *. da) (2. *. pi); velocity = dr -| (t' -. t) *| g } | p::t -> (* Make sure the velocity is not pointing towards the impact point. *) let d = unitise(r -| p) in aux (dr -| min 0. (dot dr d) *| d) t in balls.(i) <- aux dr impacts.(i); match impacts.(i) with [] | [_] -> () | _ -> balls.(i) <- { balls.(i) with angular_velocity = 0. } done; balls, impacts in (* Bisect if there was at least one impact and the time slice was large enough. *) let aux = function [] | [_] -> true | _ -> false in if dt<0.01 && (Array.for_all aux impacts || dt < 1e-3) then balls' else let t2 = (t +. t') *. 0.5 in update t2 t' (update t t2 balls) let old_time = ref 0. let idle () = let time' = time() in balls := update !old_time time' !balls; old_time := time'; Glut.postRedisplay () let () = ignore (Glut.init Sys.argv); Glut.initDisplayMode ~alpha:true ~double_buffer:true ~depth:true (); Glut.initWindowSize ~w:1024 ~h:1024; ignore (Glut.createWindow ~title:"Bouncing balls"); GlClear.color (0., 0., 0.) ~alpha:0.; Gl.enable `blend; List.iter Gl.enable [`line_smooth; `polygon_smooth]; List.iter (fun x -> GlMisc.hint x `nicest) [`line_smooth; `polygon_smooth]; GlDraw.line_width 4.; GlFunc.blend_func ~src:`src_alpha ~dst:`one; Glut.reshapeFunc ~cb:reshape; Glut.displayFunc ~cb:display; Glut.idleFunc ~cb:(Some idle); Glut.keyboardFunc ~cb:(fun ~key ~x ~y -> if key=27 then exit 0); Glut.mainLoop ()