{-# OPTIONS_GHC -XBangPatterns -XNoMonomorphismRestriction -XScopedTypeVariables -XFunctionalDependencies -XFlexibleInstances -XMultiParamTypeClasses #-} ------------------------------------------------------------------------------- import GHC.Prim import System.Random import System.Environment import Data.IORef import Data.Maybe import Data.List (minimumBy, deleteBy) import Data.Function (on) import FRP.Yampa.VectorSpace import FRP.Yampa.Vector2 import FRP.Yampa.Point2 import FRP.Yampa.Forceable import qualified Graphics.UI.GLFW as GLFW import qualified Graphics.Rendering.OpenGL as GL import qualified Graphics.Rendering.OpenGL.GLU as GLU import Graphics.Rendering.OpenGL (($=), get, Vertex2(..)) import Control.Applicative import Control.Monad import Debug.Trace import System.IO.Unsafe import Control.Concurrent ------------------------------------------------------------------------------- -- Old school stream based FRP type Time = Double type DTime = Double type SF a b = [a] -> [b] constant a = repeat a integral :: (Show v, VectorSpace v s) => v -> [v] -> [s] -> [v] integral x (v:vs) (dt:dts) = x:(integral (x ^+^ dt *^ v) vs dts) population :: [SF a b] -> SF a [b] population sfs as = transpose (map ($ as) sfs) where transpose :: [[a]] -> [[a]] transpose ss = (map strictHead ss) : transpose (map tail ss) where -- Make sure we force evaluation of all heads of our streams strictHead !((!x):xs) = x ------------------------------------------------------------------------------- windowWidth = 800 -- physical pixels windowHeight = 600 -- physical pixels windowDepth = 16 -- bits ------------------------------------------------------------------------------- worldMinX = -400 -- logical units worldMaxX = 400 -- logical units worldMinY = -300 -- logical units worldMaxY = 300 -- logical units worldMinXY = vector2 worldMinX worldMinY worldMaxXY = vector2 worldMaxX worldMaxY worldWidth = worldMaxX - worldMinX -- logical units worldHeight = worldMaxY - worldMinY -- logical units worldFarZ = 1 worldNearZ = 0 ------------------------------------------------------------------------------- type Scalar = Double type Vector = Vector2 Scalar type UnitVector = Vector type Velocity = Vector type Position = Vector type Speed = Scalar type Radius = Scalar type Length = Scalar type Degrees = Scalar type Radians = Scalar type RGBA = GL.Color4 Scalar orthogonalOn v = let (x,y) = vector2XY v in vector2 (-y) x lerp s lo hi = lo ^+^ s*^(hi^-^lo) prop v lo hi = (v-lo) / (hi-lo) scale sx sy v = vector2 (vx*sx) (vy*sy) where (vx,vy) = vector2XY v translate tx ty v = vector2 tx ty ^+^ v ------------------------------------------------------------------------------- -- IL = list of items that have a unique identifier type IK = Int data IL a = IL { ilNextKey :: !IK , ilItems :: [(IK,a)] } deriving Show deleteFirst :: (a -> Bool) -> [a] -> [a] deleteFirst p (x:xs) = if p x then xs else x:deleteFirst p xs deleteFirst _ [] = [] ilEmpty = IL 0 [] ilFromList = ilAppend ilEmpty ilInsert (IL key items) value = IL (key+1) ((key,value):items) ilRemove (IL key items) id = IL key $ deleteFirst ( (/=id) . fst) items ilAppend il (v:vs) = (il `ilInsert` v) `ilAppend` vs ilAppend il [] = il ilMap f (IL key items) = let (keys, values) = unzip items in IL key $ zip keys (map f values) ilValues (IL key items) = let (keys, values) = unzip items in values instance Functor IL where fmap = ilMap ------------------------------------------------------------------------------- -- The elements in our tiny simulation data RigidObject = Particle { objPosition :: !Position , objVelocity :: !Velocity , objRadius :: !Scalar , objColor :: !RGBA } | Wall -- The wall is actually a (hyper)plane, which in the 2D case is just a line, given as point and normal... { objPosition :: !Position , objNormal :: !UnitVector , objColor :: !RGBA } deriving (Show, Eq) wall p d c = Wall p (d ^/ norm d) c ------------------------------------------------------------------------------- -- Convert OpenGL screen position to a world position screenToWorldPosition :: GL.Position -> Vector screenToWorldPosition (GL.Position x y) = let (nx,ny) = (fromIntegral x, fromIntegral (windowHeight - y - 1)) (wx,wy) = (lerp (prop nx 0 $ fromIntegral windowWidth) worldMinX worldMaxX, lerp (prop ny 0 $ fromIntegral windowHeight) worldMinY worldMaxY) in vector2 wx wy {- screenToWorldPosition :: GL.Position -> IO Vector screenToWorldPosition pos@(GL.Position x y) = do viewport@(_,GL.Size _ height) <- get GL.viewport putStrLn $ show height modelviewMatrix <- getMatrix $ GL.Modelview 0 projectionMatrix <- getMatrix $ GL.Projection let pos3 = GL.Vertex3 (fromIntegral x) (fromIntegral (height-y)) 0 GL.Vertex3 vx vy _ <- GLU.unProject pos3 modelviewMatrix projectionMatrix viewport return $ vector2 vx vy where getMatrix :: GL.MatrixMode -> IO(GL.GLmatrix Scalar) getMatrix mode = get $ GL.matrix $ Just mode -} ------------------------------------------------------------------------------- data Sensors = Sensors { deltaTime :: !DTime, mouseWorldPosition :: !Vector } deriving Show sense :: IORef Time -> IO Sensors sense timeRef = do GLFW.pollEvents lastTime <- readIORef timeRef thisTime <- get GLFW.time writeIORef timeRef thisTime smp <- get GLFW.mousePos let wmp = screenToWorldPosition smp -- wmp <- screenToWorldPosition smp return $ Sensors { deltaTime = min (thisTime-lastTime) 0.05 , mouseWorldPosition = wmp } sinterleave action = unsafeInterleaveIO $ do a <- action as <- sinterleave action return (a:as) makeSensors = do time <- get GLFW.time timeRef <- newIORef time sinterleave (sense timeRef) ------------------------------------------------------------------------------- -- Quick and dirty frames per second counter type FPS = IORef(Scalar,Int) initFPS :: IO FPS initFPS = do t0 <- get GLFW.time newIORef (t0,0) updateFPS :: FPS -> IO() updateFPS fps = do (t1,c1) <- readIORef fps t2 <- get GLFW.time let c2 = c1+1 dt = t2-t1 if dt >= 1 then do let text = show (round $ 1000000*dt / fromIntegral c2) ++ " microsec per frame = " ++ show (round $ fromIntegral c2 / dt) ++ " frames per sec" GLFW.windowTitle $= text --print text writeIORef fps (t2,0) else writeIORef fps (t1,c2) ------------------------------------------------------------------------------- initGfx = do GLFW.initialize GLFW.disableSpecial GLFW.AutoPollEvent GLFW.openWindow (GL.Size windowWidth windowHeight) [GLFW.DisplayDepthBits windowDepth, GLFW.DisplayAlphaBits 8] GLFW.Window -- Set to 0 to render and swap as fast as possible -- Set to 1 to synchronize rendering with screen's vertical refresh rate GLFW.swapInterval $= 1 GLFW.windowTitle $= "Peter Verswyvelen's Elastic Collision Nightmare - move mouse left/right to slowdown/speedup" GLFW.windowSizeCallback $= onWindowResized GL.hint GL.PolygonSmooth $= GL.Nicest GL.hint GL.LineSmooth $= GL.Nicest GL.blend $= GL.Enabled GL.blendFunc $= (GL.SrcAlpha, GL.OneMinusSrcAlpha) GL.shadeModel $= GL.Smooth GL.lineSmooth $= GL.Enabled GL.polygonSmooth $= GL.Enabled GL.pointSmooth $= GL.Enabled GL.pointSize $= 5.0 GL.lineWidth $= 1.5 GL.clearColor $= GL.Color4 0 0 0 1 GL.clearDepth $= worldFarZ GL.depthRange $= (worldNearZ, worldFarZ) GL.depthFunc $= Just GL.Lequal GL.matrixMode $= GL.Projection GL.loadIdentity GL.ortho worldMinX worldMaxX worldMinY worldMaxY worldNearZ worldFarZ GL.matrixMode $= GL.Modelview 0 GL.loadIdentity where onWindowResized windowSize@(GL.Size windowWidth windowHeight) = do let ww = fromIntegral windowWidth wh = fromIntegral windowHeight cw = worldWidth ch = worldHeight sw = wh * cw / ch sh = ww * ch / cw if sw < ww then GL.viewport $= (GL.Position (round $ (ww-sw)/2) 0, GL.Size (round sw) (round wh)) else GL.viewport $= (GL.Position 0 (round $ (wh-sh)/2), GL.Size (round ww) (round sh)) termGfx = do GLFW.terminate ------------------------------------------------------------------------------- exited :: IO Bool exited = do windowOpenStatus <- get $ GLFW.windowParam GLFW.Opened return $ windowOpenStatus == 0 ------------------------------------------------------------------------------- run actions = do loop actions where loop (action:actions) = do viewport@(_,GL.Size _ height) <- get GL.viewport action done <- exited unless done (loop actions) ------------------------------------------------------------------------------- unitCircle :: Int -> [Vector] unitCircle numPoints = [vector2 (cos rads) (sin rads) | i <- [0..numPoints], let rads = fromIntegral i * 2 * pi / fromIntegral numPoints ] vector2vertex v = GL.Vertex2 (vector2X v) (vector2Y v) foreach = flip mapM_ renderObject (Particle p v r c) = do GL.color c GL.preservingMatrix $ do GL.translate $ GL.Vector3 (vector2X p) (vector2Y p) 0 GL.scale r r r GL.renderPrimitive GL.LineLoop $ mapM_ (GL.vertex . vector2vertex) (unitCircle 36) renderObject (Wall p d c) = let (dx,dy) = vector2XY d in do renderRay p (vector2 (-dy) dx) c renderRay p (vector2 dy (-dx)) c renderLine p (p ^+^ 10 *^ d) c renderOverlay (Particle p v r c) = do renderLine p (p ^+^ r *^ normalize v) c renderOverlay _ = return () renderLine :: Vector -> Vector -> RGBA -> IO() renderLine p1 p2 color = do GL.lineWidth $= 3 GL.color color GL.renderPrimitive GL.Lines $ do GL.vertex $ GL.Vertex2 x1 y1 GL.vertex $ GL.Vertex2 x2 y2 where x1 = vector2X p1 y1 = vector2Y p1 x2 = vector2X p2 y2 = vector2Y p2 renderRay :: Position -> Vector -> RGBA -> IO() renderRay p d color = do GL.lineWidth $= 2 GL.color color GL.renderPrimitive GL.Lines $ do GL.vertex $ GL.Vertex4 x1 y1 0 1 GL.vertex $ GL.Vertex4 x2 y2 0 0 where x1 = vector2X p y1 = vector2Y p x2 = vector2X d y2 = vector2Y d renderObjects :: [RigidObject] -> IO () renderObjects obs = do GL.clearColor $= GL.Color4 0 0 0 0 GL.clear [GL.ColorBuffer] -- REALLY SLOW foreach obs renderObject --foreach obs renderOverlay GLFW.swapBuffers ------------------------------------------------------------------------------- unorderedPairs [] = [] unorderedPairs (p:ps) = pairs p ps ++ unorderedPairs ps where pairs p (q:qs) = (p, q):pairs p qs pairs p [] = [] solveQuadratic a b c = let d = b*b - 4*a*c in if d<0 then [] else [((-b)+sqrt(d))/(2*a), ((-b)-sqrt(d))/(2*a)] maybeCollisionAtTime (Particle p1 v1 r1 c1) (Particle p2 v2 r2 c2) dt = let r = r1+r2 rr = r*r dp = p1^-^p2 dv = v1^-^v2 dvdv = dv `dot` dv dvdp = dv `dot` dp dpdp = dp `dot` dp ss = solveQuadratic dvdv (2*dvdp) (dpdp-rr) in if dvdp>=0 then -- Particles are not moving towards eachother Nothing else -- Compute times of future impacts, if any case filter (>=0) $ filter (