--- title: "Drawing a torus with Haskell" author: "Stéphane Laurent" date: '2018-05-26' tags: haskell, graphics, opengl, geometry output: md_document: variant: markdown preserve_yaml: true html_document: highlight: zenburn keep_md: no prettify: yes linenums: yes prettifycss: minimal highlighter: pandoc-solarized --- ***Problem:*** *With the Haskell `OpenGL` library, how to draw a torus passing by three given points?* ___ Firstly, we write a function to get the circle passing by three given points, with the help of the `linear` library. The imported `toList` function will be used later. ```haskell module TransformationMatrix where import Data.Foldable (toList) import Linear -- | the plane passing by three points plane3pts :: Num a => V3 a -> V3 a -> V3 a -> (V3 a, a) plane3pts p1 p2 p3 = (V3 xcoef ycoef zcoef, offset) where V3 p1x p1y p1z = p1 V3 p2x p2y p2z = p2 V3 p3x p3y p3z = p3 xcoef = (p1y-p2y)*(p2z-p3z)-(p1z-p2z)*(p2y-p3y) ycoef = (p1z-p2z)*(p2x-p3x)-(p1x-p2x)*(p2z-p3z) zcoef = (p1x-p2x)*(p2y-p3y)-(p1y-p2y)*(p2x-p3x) offset = p1x*xcoef + p1y*ycoef + p1z*zcoef -- | plane given a point and a normal plane1ptnormal :: Num a => V3 a -> V3 a -> (V3 a, a) plane1ptnormal p normal = (normal, p `dot` normal) -- | circumcenter and circumradius given three points circleCenterRadius :: (Num a, Fractional a, Floating a) => V3 a -> V3 a -> V3 a -> ((V3 a, a), V3 a) circleCenterRadius p1 p2 p3 = ((center, radius), coefs1) where p12 = (p1 ^+^ p2) ^/ 2 p23 = (p2 ^+^ p3) ^/ 2 v12 = p2 ^-^ p1 v23 = p3 ^-^ p2 (coefs1, offset1) = plane3pts p1 p2 p3 (coefs2, offset2) = plane1ptnormal p12 v12 (coefs3, offset3) = plane1ptnormal p23 v23 a = V3 coefs1 coefs2 coefs3 b = V3 offset1 offset2 offset3 center = inv33 a !* b op1 = p1 ^-^ center radius = norm op1 ``` The `circleCenterRadius` function returns the center of the circle passing by the points `p1`, `p2`, `p3`, its radius $R$, as well as a vector perpendicular to the plane passing by these points. Now we write a function which returns an appropriate *transformation matrix*. The torus drawn by `Torus` in the `GLUT` library is always centered at $(0,0,0)$ and it is in the $xy$-plane (the plane $z=0$). Setting its outer radius to $R$, we are looking for the transformation matrix which maps this torus to the desired torus. ```haskell -- | the transformation matrix for a torus passing by three points transformationMatrix :: (Real a, Floating a) => V3 a -> V3 a -> V3 a -> ([a], a) transformationMatrix p1 p2 p3 = (concatMap toList (toList (mkTransformationMat m center)), radius) where ((center, radius), plane) = circleCenterRadius p1 p2 p3 V3 a b c = plane measure = sqrt (a*a + b*b + c*c) a' = a / measure b' = b / measure c' = c / measure n = V3 a' b' c' s = sqrt (a'*a' + b'*b') -- TODO: case a=b=0 a'' = a' / s b'' = b' / s u = V3 b'' (-a'') 0 v = cross n u m = transpose $ V3 u v n ``` The function `transformationMatrix` also returns the radius $R$. The matrix it returns is given by a list, thanks to the `toList` function. We return this list instead of a matrix of the `linear` library in order to use it in `OpenGL`. The transformation matrix is applied with the `multMatrix` function of `OpenGL`: ```haskell module TestTransformationMatrix where import Data.Tuple.Extra (fst3, snd3, thd3, second) import Graphics.Rendering.OpenGL.GL import Graphics.UI.GLUT import Linear (V3 (..)) import TransformationMatrix type Point = (GLfloat,GLfloat,GLfloat) pointToV3 :: Point -> V3 GLfloat pointToV3 (x,y,z) = V3 x y z myTriplet :: (Point, Point, Point) -- the three points for our test myTriplet = ((-1,5,1),(2,1,8),(-5,0,3)) white,black,blue :: Color4 GLfloat white = Color4 1 1 1 1 black = Color4 0 0 0 1 blue = Color4 0 0 1 1 tmatAndRadius :: ([GLfloat], GLdouble) tmatAndRadius = second realToFrac (transformationMatrix (pointToV3 $ fst3 myTriplet) (pointToV3 $ snd3 myTriplet) (pointToV3 $ thd3 myTriplet)) display :: DisplayCallback display = do clear [ColorBuffer, DepthBuffer] loadIdentity preservingMatrix $ do m <- newMatrix RowMajor (fst tmatAndRadius) :: IO (GLmatrix GLfloat) multMatrix m materialDiffuse Front $= blue renderObject Solid $ Torus 0.2 (snd tmatAndRadius) 30 30 renderPrimitive Triangles $ mapM_ (\(x, y, z) -> vertex $ Vertex3 x y z) [fst3 myTriplet, snd3 myTriplet, thd3 myTriplet] swapBuffers resize :: Size -> IO () resize s@(Size w h) = do viewport $= (Position 0 0, s) matrixMode $= Projection loadIdentity perspective 45.0 (w'/h') 1.0 100.0 lookAt (Vertex3 0 0 (-20)) (Vertex3 0 0 0) (Vector3 0 1 0) matrixMode $= Modelview 0 where w' = realToFrac w h' = realToFrac h main :: IO () main = do _ <- getArgsAndInitialize _ <- createWindow "Torus passing by three points" windowSize $= Size 500 500 initialDisplayMode $= [RGBAMode, DoubleBuffered, WithDepthBuffer] clearColor $= white materialAmbient Front $= black lighting $= Enabled light (Light 0) $= Enabled position (Light 0) $= Vertex4 0 0 (-100) 1 ambient (Light 0) $= black diffuse (Light 0) $= white specular (Light 0) $= black depthFunc $= Just Less shadeModel $= Smooth displayCallback $= display reshapeCallback $= Just resize idleCallback $= Nothing mainLoop ``` And here is the result: ![](figures/torusPassingByThreePoints.png) Thanks to this technique, I made a Haskell library to draw *Hopf tori* with `OpenGL`. This library is [available here](https://github.com/stla/villarceau).