I've been following the Haskell OpenGL tutorial.
Rotations in a 3D space intrigued me so I started learning about Euler angles and finally, quaternions.
I wanted to implement my own function using quaternions to perform a rotation (on a cube), I've based myself on those two papers: mostly this one and this one.
My function works fine when I'm performing a rotation on only one axis, but when I do it on X and Y for example, the cube start to randomly go forward and being "blocked" when it rotates.
Video of the cube performing rotation on XY.
When I set the three axis (X, Y, Z), it zooms even more (but doesn't have that weird blocking thing): video.
Here is the code of my program:
Here is the main file that creates a window, set idle function and outputs result of rotation by angle A on the screen where A is increment by 0.05 at each frames.
module Main (main) where
import Core
import Utils
import Data.IORef
import Graphics.UI.GLUT
import Graphics.Rendering.OpenGL
main :: IO ()
main = do
createAWindow "177013"
mainLoop
createAWindow :: [Char] -> IO ()
createAWindow windowName = do
(procName, _args) <- getArgsAndInitialize
createWindow windowName
initialDisplayMode $= [DoubleBuffered]
angle <- newIORef 0.0
delta <- newIORef 0.05
displayCallback $= (start angle)
reshapeCallback $= Just reshape
keyboardMouseCallback $= Just keyboardMouse
idleCallback $= Just (idle angle delta)
reshape :: ReshapeCallback
reshape size = do
viewport $= (Position 0 0, size)
postRedisplay Nothing
keyboardMouse :: KeyboardMouseCallback
keyboardMouse _ _ _ _ = return ()
idle :: IORef GLfloat -> IORef GLfloat -> IdleCallback
idle angle delta = do
d <- get delta
a <- get angle
angle $~! (+d)
postRedisplay Nothing
start :: IORef GLfloat -> DisplayCallback
start angle = do
clear [ColorBuffer]
loadIdentity
a <- get angle
let c = rotate3f (0, 0, 0) [X,Y,Z] a $ cube3f 0.2 -- here I'm rotating on X, Y and Z axis
draw3f Quads c CCyan
flush
swapBuffers
where
Here is the core file where the rotation function is defined (with a few other ones). I added some comments as it's probably some low quality haskell code.
module Core (draw3f, vertex3f, rotate3f, translate3f, rotate3d, Colors(..), Axes(..)) where
import Control.Lens
import Graphics.Rendering.OpenGL
data Axes = X | Y | Z
deriving Eq
data Colors = CRed | CGreen | CBlue | CYellow | CWhite | CMagenta | CCyan | CBlack | CNone | CPreset
deriving Eq
rotate3f :: (GLfloat, GLfloat, GLfloat) -> [Axes] -> GLfloat -> [(GLfloat, GLfloat, GLfloat)] -> [(GLfloat, GLfloat, GLfloat)]
rotate3f _ _ _ [] = []
rotate3f _ [] _ _ = []
rotate3f o axes a p = let p' = translate3f p u -- translation if I don't want to rotate it by the origin
q = cos a' : ((\x -> if x `elem` axes then sin a' else 0) <$> [X,Y,Z]) -- if the axe is set then its related component is equal to sin theta/2, otherwise it will be 0
q' = q !! 0 : (negate <$> (tail q)) -- quaternion inversion
in translate3f ((rotate q q') <$> p') [(0,0,0),o] -- rotate and translate again to put the object where it belongs
where
a' = (a * (pi / 180)) / 2 -- convert to radians and divide by 2 as all q components takes theta/2
u :: [(GLfloat, GLfloat, GLfloat)]
u = [o,(0,0,0)]
rotate :: [GLfloat] -> [GLfloat] -> (GLfloat, GLfloat, GLfloat) -> (GLfloat, GLfloat, GLfloat)
rotate q q' (x,y,z) = let p = [0,x,y,z]
qmul q1 q2 = [(q1 !! 0) * (q2 !! 0) - (q1 !! 1) * (q2 !! 1) - (q1 !! 2) * (q2 !! 2) - (q1 !! 3) * (q2 !! 3),
(q1 !! 0) * (q2 !! 1) + (q1 !! 1) * (q2 !! 0) + (q1 !! 2) * (q2 !! 3) - (q1 !! 3) * (q2 !! 2),
(q1 !! 0) * (q2 !! 2) - (q1 !! 1) * (q2 !! 3) + (q1 !! 2) * (q2 !! 0) + (q1 !! 3) * (q2 !! 1),
(q1 !! 0) * (q2 !! 3) + (q1 !! 1) * (q2 !! 2) - (q1 !! 2) * (q2 !! 1) + (q1 !! 3) * (q2 !! 0)]
p' = qmul (qmul q p) q'
in (p' !! 1, p' !! 2, p' !! 3)
translate3f :: [(GLfloat, GLfloat, GLfloat)] -> [(GLfloat, GLfloat, GLfloat)] -> [(GLfloat, GLfloat, GLfloat)]
translate3f p [(ax,ay,az),(bx,by,bz)] = map (\(x,y,z) -> (x + (bx - ax), y + (by - ay), z + (bz - az))) p
draw3f :: PrimitiveMode -> [(GLfloat, GLfloat, GLfloat)] -> Colors -> IO()
draw3f shape points color = renderPrimitive shape $ mapM_ (\(x,y,z) -> vertex3f x y z color) points
vertex3f :: GLfloat -> GLfloat -> GLfloat -> Colors -> IO()
vertex3f x y z c = do
if c /= CPreset
then color $ Color3 (c' ^. _1) (c' ^. _2) ((c' ^. _3) :: GLfloat)
else return ()
vertex $ Vertex3 x y z
where
c' :: (GLfloat, GLfloat, GLfloat)
c' = case c of CRed -> (1,0,0)
CGreen -> (0,1,0)
CBlue -> (0,0,1)
CYellow -> (1,1,0)
CMagenta -> (1,0,1)
CCyan -> (0,1,1)
CBlack -> (0,0,0)
_ -> (1,1,1)
And here is the utils file where there's just the definition of the cube, from the Haskell OpenGL tutorial
module Utils (cube3f) where
import Core
import Graphics.UI.GLUT
import Graphics.Rendering.OpenGL
cube3f :: GLfloat -> [(GLfloat, GLfloat, GLfloat)]
cube3f w = [( w, w, w), ( w, w,-w), ( w,-w,-w), ( w,-w, w),
( w, w, w), ( w, w,-w), (-w, w,-w), (-w, w, w),
( w, w, w), ( w,-w, w), (-w,-w, w), (-w, w, w),
(-w, w, w), (-w, w,-w), (-w,-w,-w), (-w,-w, w),
( w,-w, w), ( w,-w,-w), (-w,-w,-w), (-w,-w, w),
( w, w,-w), ( w,-w,-w), (-w,-w,-w), (-w, w,-w)]
Finally, if it can helps people to see if there's a problem in my algorithms, here are some rotation samples using my function:
Rotation at 90°, of point (1, 2, 3) on X axis around point (0, 0, 0) (origin) gives: (0.99999994,-3.0,2.0)
Same rotation but on X & Y axis gives: (5.4999995,-0.99999994,-0.49999988)
Same rotation again but on X, Y and Z axis gives: (5.9999995,1.9999999,3.9999995)
The second paper about rotations by quaternions that you point to has this sentence:
“(x̂, ŷ, ẑ) is a unit vector that defines the axis of rotation.”.
So the quaternion has to be normalized, the sum of components squared being equal to 1.
So for example if you have all 3 axis involved, it has to be (cos θ/2, r3sin θ/2, r3sin θ/2, r3*sin θ/2) where r3 is the reciprocal of the square root of 3. This is how I would explain that the rotation results you mention at the end of your post fail to conserve the length of the vector when several axis are involved.
The critical piece is thus this line in function rotate3f:
q = cos a' : ((\x -> if x `elem` axes then sin a' else 0) <$> [X,Y,Z])
where a normalization factor is missing.
Your code offers a number of opportunities for readability improvement. You might consider using CodeReview for further details.
A major concern is the fact that the source code lines are too wide. If the reader has to use an horizontal slider, it is much more difficult to understand the code and find the bugs. Below, I will try to avoid going beyond 80 characters width.
First, we need some quaternion infrastructure:
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ExplicitForAll #-}
type GLfloat = Float
type GLfloatV3 = (GLfloat, GLfloat, GLfloat)
type QuatFloat = [GLfloat]
data Axes = X | Y | Z deriving Eq
qmul :: QuatFloat -> QuatFloat -> QuatFloat
qmul [qa0, qa1, qa2, qa3] [qb0, qb1, qb2, qb3] =
[
qa0*qb0 - qa1*qb1 - qa2*qb2 - qa3*qb3 ,
qa0*qb1 + qa1*qb0 + qa2*qb3 - qa3*qb2 ,
qa0*qb2 - qa1*qb3 + qa2*qb0 + qa3*qb1 ,
qa0*qb3 + qa1*qb2 - qa2*qb1 + qa3*qb0
]
qmul _ _ = error "Quaternion length differs from 4"
qconj :: QuatFloat -> QuatFloat
qconj q = (head q) : (map negate (tail q)) -- q-conjugation
rotate :: [GLfloat] -> [GLfloat] -> GLfloatV3 -> GLfloatV3
rotate q q' (x,y,z) = let p = [0, x,y,z]
[q0,q1,q2,q3] = qmul (qmul q p) q'
in (q1, q2, q3)
Note that the idea of defining ad hoc types not only allows for reduced code width, but that also gives extra flexibility. If some day you decide to represent quaternions by some other data structure which is more efficient than a plain list, it can be done while leaving the client code unchanged.
Next, the rotation code proper. Function rotQuat0 is your initial algorithm, which reproduces the numerical results mentioned at the end of your question. Function rotQuat1 is the modified version giving a 1-normalized quaternion.
-- original code:
rotQuat0 :: [Axes] -> GLfloat -> QuatFloat
rotQuat0 axes angle = let fn x = if (x `elem` axes) then (sin angle) else 0
in (cos angle) : (map fn [X,Y,Z])
-- modified code:
rotQuat1 :: [Axes] -> GLfloat -> QuatFloat
rotQuat1 axes angle = let corr = 1.0 / sqrt (fromIntegral (length axes))
fn x = if (x `elem` axes) then corr*(sin angle) else 0
in (cos angle) : (map fn [X,Y,Z])
Code using rotQuat1:
rotate3f :: GLfloatV3 -> [Axes] -> GLfloat -> [GLfloatV3] -> [GLfloatV3]
rotate3f _ _ _ [] = []
rotate3f _ [] _ _ = []
rotate3f org axes degθ pts =
let -- convert to radians and divide by 2, as all q components take θ/2
a' = (degθ * (pi / 180)) / 2
u :: [GLfloatV3]
u = [org, (0,0,0)]
-- translation if I don't want to rotate it by the origin
p' = translate3f pts u
-- if the axis is set, then its related component is
-- equal to sin θ/2, otherwise it will be zero
---- q = cos a' : ((\x -> if x `elem` axes then sin a' else 0) <$> [X,Y,Z])
q = rotQuat1 axes a' -- modified version
q' = qconj q
-- rotate and translate again to put the object where it belongs
in translate3f ((rotate q q') <$> p') [(0,0,0), org]
translate3f :: [GLfloatV3] -> [GLfloatV3] -> [GLfloatV3]
translate3f pts [(ax,ay,az), (bx,by,bz)] =
let dx = bx - ax
dy = by - ay
dz = bz - az
in map (\(x,y,z) -> (x + dx, y + dy, z + dz)) pts
Testing code:
sqNorm3 :: GLfloatV3 -> GLfloat
sqNorm3 (x,y,z) = x*x + y*y +z*z
printAsLines :: Show α => [α] -> IO ()
printAsLines xs = mapM_ (putStrLn . show) xs
main = do
let pt = (1,2,3) :: GLfloatV3
pt1 = rotate3f (0,0,0) [X] 90 [pt]
pt2 = rotate3f (0,0,0) [X,Y] 90 [pt]
pt3 = rotate3f (0,0,0) [X,Y,Z] 90 [pt]
pts = map head [pt1, pt2, pt3]
ptN = map sqNorm3 pts
printAsLines pts
putStrLn " "
printAsLines ptN
Let's check that with function rotQuat1, the squared norm of your initial (1,2,3) input vector (that is 1+4+9=13) remains unchanged, as befits a proper rotation:
$ ghc opengl00.hs -o ./opengl00.x && ./opengl00.x
[1 of 1] Compiling Main ( opengl00.hs, opengl00.o )
Linking ./opengl00.x ...
(0.99999994,-3.0,2.0)
(3.6213198,-0.62132025,0.70710695)
(2.5773501,0.84529924,2.5773501)
14.0
13.999995
13.999998
$
Unfortunately I don't have enough time to install the OpenGL infrastructure and reproduce the animation. Please let us know whether this fixes the whole thing.
Related
In an attempt to dip my toes in functional programming, I'm attempting to pick up Haskell and running into some mental issues with the type system.
Running the following code gives proper output (e.g. generates coordinates for a circle wrapped around a cylinder of radius R at angle theta):
coilGeneration_AngleTest housingRadius coilWidth coilDepth numEle zoffset centralAngle
= [ (x',y',z)
| theta <- [0,2*pi/(numEle-1)..2*pi]
, let x = housingRadius * cos(coilWidth*cos(theta)/housingRadius)
, let y = housingRadius * sin(coilWidth*cos(theta)/housingRadius)
, let z = coilDepth * sin(theta)+zoffset
, let x' = x * cos(centralAngle) - y * sin(centralAngle)
, let y' = x * sin(centralAngle) + y * cos(centralAngle)
]
Sample coilGeneration_AngleTest function output
However, attempting to generalize this into a function that generates an arbitrary NxM array of circles with varying overlaps in the polar and z-directions by running:
coilArrayGeneration_Test r nE width depth n m mu gam
= [ (x',y',z',i,j)
| theta <- [0,2*pi/(nE-1)..2*pi]
, i <- [1..n]
, j <- [1..m]
, let a = width/2
, let b = depth/2
, let x = r * cos(a*cos(theta)/r)
, let y = r * sin(a*cos(theta)/r)
, let z = b * sin(theta)
, let phi = (2*i-1-n)((a-mu)/r)
, let zo = (2*j-1-m)(b-gam)
, let x' = x * cos(phi) - y * sin(phi)
, let y' = x * sin(phi) + y * cos(phi)
, let z' = z + zo
]
gives the following error:
Build profile: -w ghc-9.2.5 -O1
In order, the following will be built (use -v for more details):
- Haskell-0.1.0.0 (exe:Haskell) (file app/Main.hs changed)
Preprocessing executable 'Haskell' for Haskell-0.1.0.0..
Building executable 'Haskell' for Haskell-0.1.0.0..
[1 of 1] Compiling Main ( app/Main.hs, /Users/zack/Desktop/Udemy/Haskell/dist-newstyle/build/aarch64-osx/ghc-9.2.5/Haskell-0.1.0.0/x/Haskell/build/Haskell/Haskell-tmp/Main.o )
app/Main.hs:66:1: error:
• Non type-variable argument in the constraint: Num (c -> c)
(Use FlexibleContexts to permit this)
• When checking the inferred type
coilArrayGeneration_Test :: forall {c}.
(Floating c, Num (c -> c), Enum c, Enum (c -> c)) =>
c
-> c
-> c
-> c
-> (c -> c)
-> (c -> c)
-> c
-> c
-> [(c, c, c, c -> c, c -> c)]
|
66 | coilArrayGeneration_Test r nE width depth n m mu gam = [(x',y',z',i,j)|theta <- [0,2*pi/(nE-1)..2*pi],....
Failure Output
After googling for a while, it seemed that my function had an improper type implied by the compiler but I unfortunately don't understand the idea Haskell Type Definition well enough to fix it. I attempted to define the types the way I see them, namely:
r -> Double
nE -> Int
width -> Double
depth -> Double
n -> Int
m -> Int
mu -> Double
gam -> Double
x' -> Double
y' -> Double
z' -> Double
I -> Int
j -> Int
Getting:
coilArrayGeneration_Test :: (Floating a, Integral b) => a -> b -> a -> a -> b -> b -> a -> a -> [(a,a,a,b,b)]
coilArrayGeneration_Test r nE width depth n m mu gam
= [ (x',y',z',i,j)
| theta <- [0,2*pi/(nE-1)..2*pi]
, i <- [1..n]
, j <- [1..m]
, let a = width/2
, let b = depth/2
, let x = r * cos(a*cos(theta)/r)
, let y = r * sin(a*cos(theta)/r)
, let z = b * sin(theta)
, let phi = (2*i-1-n)((a-mu)/r)
, let zo = (2*j-1-m)(b-gam)
, let x' = x * cos(phi) - y * sin(phi)
, let y' = x * sin(phi) + y * cos(phi)
, let z' = z + zo
]
But this threw a whole host of errors:
Errors after Type Declaration
Which clearly means I don't know what I'm doing and mucked up the type declarations somehow.
Can anyone steer me the right way?
When you see a compiler error involving something like Num (c -> c), it never has anything to do with -XFlexibleContexts or with incorrect inferred types. It simply means you attempted to use something as a function which is not a function.
“Use as a function” entails simply that you have some expression of the form f x, where f and x can be arbitrary subexpressions. This includes in particular also expressions like (1+2)(3+4), which is the same as
let f = 1 + 2
x = 3 + 4
in f x
Presumably you meant to express multiplication by the juxtaposition. Well, use the multiplication operator then! I.e. (1+2)*(3+4).
Your code has also another problem: you trying to use the index variables in real-valued expression. Unlike the missing multiplication operators, this is fairly sensible, but Haskell doesn't allow this either. You need to explicitly wrap the integrals in fromIntegral.
coilArrayGeneration_Test r nE width depth n m μ γ
= [ (x',y',z',i,j)
| ϑ <- [0, 2*pi/fromIntegral(nE-1) .. 2*pi]
, i <- [1..n]
, j <- [1..m]
, let a = width/2
b = depth/2
x = r * cos(a*cos ϑ/r)
y = r * sin(a*cos ϑ/r)
z = b * sin ϑ
φ = fromIntegral(2*i-1-n) * ((a-μ)/r)
z₀ = fromIntegral(2*j-1-m) * (b-γ)
x' = x * cos φ - y * sin φ
y' = x * sin φ + y * cos φ
z' = z + z₀
]
I would strongly recommend you refactor this a bit, both code and types. 5-tuples are very obscure, you should at least wrap x,y,z in a suitable vector type.
I want to take two different images (taken from image files, like .png) and draw one over the other several times in different positions. The resulting image should be presented on screen or generate a new image file, whichever is easier. I´ll be taking that new image and drawing on it more with further operations
Is there any Haskell library that allows me to do this?
You can use JuicyPixels to do that sort of thing:
module Triangles where
import Codec.Picture
import LineGraphics
{-| Parameterize color smoothly as a function of angle -}
colorWheel :: Float -> Colour
colorWheel x = (r, g, b, a)
where
r = floor $ (cos x + 1) * (255 / 2)
g = floor $ (sin x + 1) * (255 / 2)
b = floor $ (cos (x+(pi/2)) + 1) * (255 / 2)
a = 255
{-| Draw a triangle centered about the point (x, y) -}
triangle :: Point -> Path
triangle (x, y) =
[ (x - k, y - k)
, (x + k, y - k)
, (x, y + k)
, (x - k, y - k)
]
where
size = 30
k = size / 2
{-|
Draw 'n' equally-spaced triangles at a radius of 'r' about a center
point, '(x, y)'.
-}
triangles :: Float -> Radius -> Vector -> Picture
triangles n r (x, y) =
[ (colorWheel theta, tri theta) | theta <- steps n ]
where
tri theta = triangle ((r * cos theta) + x, (r * sin theta) + y)
{-| Interpolate the range [0, 2pi] by 'n' steps -}
steps :: Float -> [Float]
steps n = map (\i -> i * (2*pi/n)) [0 .. n]
And we'll use this module of supporting code:
module LineGraphics (
Point, Vector, Line, Path, Picture, Colour, Radius,
black,
drawPicture,
) where
import Graphics.Rasterific hiding (Point, Vector, Line, Path, polygon)
import Graphics.Rasterific.Texture
import Codec.Picture
type Radius = Float
type Point = (Float, Float)
type Vector = (Float, Float)
type Line = (Point, Point)
type Path = [Point]
type Picture = [(Colour, Path)]
type Colour = (Int, Int, Int, Int) -- red, green, blue, opacity
black = (0, 0, 0, 255)
drawPicture :: Float -> Picture -> Image PixelRGBA8
drawPicture linewidth picture =
renderDrawing 800 800 (toColour black) $
mapM_ renderFn picture
where
renderFn (col, path) = withTexture (uniformTexture $ toColour col) (drawPath path)
drawPath points = stroke linewidth JoinRound (CapRound, CapStraight 0) $
polyline (map (\(x, y) -> V2 x y) points)
toColour (a,b,c,d) = PixelRGBA8
(fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d)
And here's what we get:
How do I optimize numerical integration routine (comparing to C)?
What has been done to the moment:
I replaced lists with unboxed vectors (obvious).
I applied profiling techniques described in the book "Read World Haskell" http://book.realworldhaskell.org/read/profiling-and-optimization.html.
I have inlined some trivial functions and inserted a lot of bangs everywhere.
That gave about 10x speedup.
I refactored the code (i.e. extracted iterator function). That gave 3x speedup.
I tried to replace polymorphic signatures with Floats
as in the answer to this question
Optimizing numerical array performance in Haskell.
That gave almost 2x speedup.
I compile like this
cabal exec ghc -- Simul.hs -O2 -fforce-recomp -fllvm -Wall
UPDATE As suggested by cchalmers, type Sample = (F, F) was replaced with
data Sample = Sample {-# UNPACK #-} !F {-# UNPACK #-} !F
The performance now is almost as good as C code. Can we do better?
{-# LANGUAGE BangPatterns #-}
module Main
where
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as UM
import qualified Control.Monad.Primitive as PrimitiveM
import Dynamics.Nonlin ( birefrP )
type F = Float
type Delay = U.Vector F
type Input = U.Vector F
-- Sample can be a vector of any length (x, y, z, ...)
data Sample = Sample {-# UNPACK #-} !F {-# UNPACK #-} !F
-- Pair is used to define exactly a pair of values
data Pair = Pair {-# UNPACK #-} !F {-# UNPACK #-} !F
type ParametrizedDelayFunction = (Sample, F) -> Sample
getX :: Sample -> F
getX (Sample a _) = a
{-# INLINE getX #-}
toDelay :: [F] -> Delay
toDelay = U.fromList
stepsPerNode :: Int
stepsPerNode = 40 -- Number of integration steps per node
infixl 6 ..+..
(..+..) :: Sample -> Sample -> Sample
(..+..) (Sample x1 y1) (Sample x2 y2) = Sample (x1 + x2) (y1 + y2)
{-# INLINE (..+..) #-}
infixl 7 .*..
(.*..) :: F -> Sample -> Sample
(.*..) c (Sample x2 y2) = Sample (c * x2) (c * y2)
{-# INLINE (.*..) #-}
-- | Ikeda model (dynamical system, DDE)
ikeda_model2
:: (F -> F) -> (Sample, F) -> Sample
ikeda_model2 f (!(Sample x y), !x_h) = Sample x' y'
where
! x' = recip_epsilon * (-x + (f x_h))
y' = 0
recip_epsilon = 2^(6 :: Int)
-- | Integrate using improved Euler's method (fixed step).
--
-- hOver2 is already half of step size h
-- f is the function to integrate
-- x_i is current argument (x and y)
-- x_h is historical (delayed) value
-- x_h2 it the value after x_h
heun2 :: F -> ParametrizedDelayFunction
-> Sample -> Pair -> Sample
heun2 hOver2 f !x !(Pair x_h x_h2) = x_1
where
! f1 = f (x, x_h)
! x_1' = x ..+.. 2 * hOver2 .*.. f1
! f2 = f (x_1', x_h2)
! x_1 = x ..+.. hOver2 .*.. (f1 ..+.. f2)
initialCond :: Int -> (Sample, Delay, Int)
initialCond nodesN = (initialSampleXY, initialInterval, samplesPerDelay)
where cdi = 1.1247695e-4 :: F -- A fixed point for birefrP
initialInterval = U.replicate samplesPerDelay cdi
samplesPerDelay = nodesN * stepsPerNode
initialSampleXY = Sample 0.0 0.0
integrator
:: PrimitiveM.PrimMonad m =>
(Sample -> Pair -> Sample)
-> Int
-> Int
-> (Sample, (Delay, Input))
-> m (Sample, U.Vector F)
integrator iterate1 len total (xy0, (history0, input)) = do
! v <- UM.new total
go v 0 xy0
history <- U.unsafeFreeze v
-- Zero y value, currently not used
let xy = Sample (history `U.unsafeIndex` (total - 1)) 0.0
return (xy, history)
where
h i = history0 `U.unsafeIndex` i
go !v !i !xy
-- The first iteration
| i == 0 = do
let !r = iterate1 xy (Pair (h 0) (h 1))
UM.unsafeWrite v i (getX r)
go v 1 r
| i < len - 1 = do
let !r = iterate1 xy (Pair (h i) (h $ i + 1))
UM.unsafeWrite v i (getX r)
go v (i + 1) r
| i == total = do
return ()
-- Iterations after the initial history has been exhausted
| otherwise = do
! newX0 <- if i == len - 1
then return (getX xy0)
else UM.unsafeRead v (i - len - 1)
! newX <- UM.unsafeRead v (i - len)
let !r = iterate1 xy (Pair newX0 newX)
UM.unsafeWrite v i (getX r)
go v (i + 1) r
-- Not used in this version
zero :: Input
zero = U.fromList []
nodes :: Int
nodes = 306
main :: IO ()
main = do
let delays = 4000
(sample0, hist0, delayLength) = initialCond nodes
-- Iterator implements Heun's schema
iterator = heun2 (recip 2^(7::Int) :: F) (ikeda_model2 birefrP)
totalComputedIterations = delayLength * delays
-- Calculates all the time trace
(xy1, history1) <- integrator iterator delayLength totalComputedIterations (sample0, (hist0, zero))
putStrLn $ show $ getX xy1
return ()
The nonlinear function (imported) can look like this:
data Parameters = Parameters { beta :: Float
, alpha :: Float
, phi :: Float } deriving Show
paramA :: Parameters
paramA = Parameters { beta = 1.1
, alpha = 1.0
, phi = 0.01 }
birefr :: Parameters -> Float -> Float
birefr par !x = 0.5 * beta' * (1 - alpha' * (cos $ 2.0 * (x + phi')))
where
! beta' = beta par
! alpha' = alpha par
! phi' = phi par
birefrP :: Float -> Float
birefrP = birefr paramA
I'm new to both Haskell and OpenGL and I'm trying to render a texture on the screen.
Here's what I've got so far:
makeTexture :: FilePath -> IO GL.TextureObject
makeTexture f = do
t <- either error id <$> GLU.readTexture f
GL.textureFilter GL.Texture2D GL.$= ((GL.Linear', Nothing), GL.Linear')
GLU.texture2DWrap GL.$= (GL.Mirrored, GL.ClampToEdge)
return t
renderEntity :: Entity -> IO ()
renderEntity e = do
GL.activeTexture GL.$= GL.TextureUnit 0
GL.textureBinding GL.Texture2D GL.$= Just (texture $ model e)
-- I see a white triangle on the screen.
renderTriangle $ fmap (vadd $ position e) (points $ model e :: [Vector2])
-- I do not see this. Nor is the triangle textured either.
GL.renderPrimitive GL.Quads $ do
n 0 1 0
t 0 1 >> v 10 (-10) 10
t 1 1 >> v 10 (-10) (-10)
t 1 0 >> v (-10) (-10) (-10)
t 0 0 >> v (-10) (-10) 10
where v x y z = GL.vertex (GL.Vertex3 x y z :: GL.Vertex3 GL.GLfloat)
n x y z = GL.normal (GL.Normal3 x y z :: GL.Normal3 GL.GLfloat)
t u v = GL.texCoord (GL.TexCoord2 u v :: GL.TexCoord2 GL.GLfloat)
Where Entity looks like:
texMetal <- makeTexture "texture/metal.jpg"
let
entity = Entity
{ angle = 0
, position = (0, 0) :: Vector2
, velocity = (5, 5) :: Vector2
, model = Model
{ points = [(-60, -40), (60, -40), (0, 60)] :: [Vector2]
, texture = texMetal
}
}
And when I'm initializing I have these:
GL.viewport GL.$= (pos, size)
GL.matrixMode GL.$= GL.Projection
GL.texture GL.Texture2D GL.$= GL.Enabled
GL.normalize GL.$= GL.Enabled
GL.loadIdentity
GL.ortho2D (-fromIntegral width / 2)
(fromIntegral width / 2)
(-fromIntegral height / 2)
(fromIntegral height / 2)
GL.matrixMode GL.$= GL.Modelview 0
GL.loadIdentity
And the resulting picture:
It has two triangles because I have two 'entities' set in my code. But I don't see my quad nor do I see any sign of texturing.
It seems I had incorrect coordinates, because this works well:
renderEntity :: Entity -> IO ()
renderEntity e = do
GL.activeTexture GL.$= GL.TextureUnit 0
GL.textureBinding GL.Texture2D GL.$= Just (texture $ model e)
GL.renderPrimitive GL.Quads $ do
v 100 100
t 0 1
v 100 (-100)
t 1 1
v (-100) (-100)
t 1 0
v (-100) 100
t 0 0
where v x y = GL.vertex (GL.Vertex2 x y :: GL.Vertex2 GL.GLfloat)
t u v = GL.texCoord (GL.TexCoord2 u v :: GL.TexCoord2 GL.GLfloat)
The output looks like this:
You should just see a flat, continuous red wall on one side, blue wall on another, green on another, yellow on another (see the definition of the map, testMapTiles, it's just a map with four walls). Yet there are these phantom wall faces of varying height, which are perpendicular to the real walls. Why?
Note that the white "gaps" aren't actually gaps: it's trying to draw a wall of height Infinity (distance 0). If you specifically account for it (this version of the code doesn't) and just cap it at screen height, then you just see a very high wall there.
The source code is below. It's plain Haskell, using Haste to compile to JavaScript and render to canvas. It is based on the C++ code from this tutorial, though note that I replaced mapX and mapY with tileX and tileY, and I don't have the ray prefix for pos and dir within the main loop. Any discrepancies from the C++ code are probably what's breaking everything, but I can't seem to find any after having pored over this code many times.
Any help?
import Data.Array.IArray
import Control.Arrow (first, second)
import Control.Monad (forM_)
import Haste
import Haste.Graphics.Canvas
data MapTile = Empty | RedWall | BlueWall | GreenWall | YellowWall deriving (Eq)
type TilemapArray = Array (Int, Int) MapTile
emptyTilemapArray :: (Int, Int) -> TilemapArray
emptyTilemapArray dim#(w, h) = listArray ((1, 1), dim) $ replicate (w * h) Empty
testMapTiles :: TilemapArray
testMapTiles =
let arr = emptyTilemapArray (16, 16)
myBounds#((xB, yB), (w, h)) = bounds arr
in listArray myBounds $ flip map (indices arr) (\(x, y) ->
if x == xB then RedWall
else if y == yB then BlueWall
else if x == w then GreenWall
else if y == h then YellowWall
else Empty)
type Vec2 a = (a, a)
type DblVec2 = Vec2 Double
type IntVec2 = Vec2 Int
add :: (Num a) => Vec2 a -> Vec2 a -> Vec2 a
add (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
mul :: (Num a) => Vec2 a -> a -> Vec2 a
mul (x, y) factor = (x * factor, y * factor)
rot :: (Floating a) => Vec2 a -> a -> Vec2 a
rot (x, y) angle =
(x * (cos angle) - y * (sin angle), x * (sin angle) + y * (cos angle))
dbl :: Int -> Double
dbl = fromIntegral
-- fractional part of a float
-- `truncate` matches behaviour of C++'s int()
frac :: Double -> Double
frac d = d - dbl (truncate d)
-- get whole and fractional parts of a float
split :: Double -> (Int, Double)
split d = (truncate d, frac d)
-- stops 'Warning: Defaulting the following constraint(s) to type ‘Integer’'
square :: Double -> Double
square = (^ (2 :: Int))
-- raycasting algorithm based on code here:
-- http://lodev.org/cgtutor/raycasting.html#Untextured_Raycaster_
data HitSide = NorthSouth | EastWest deriving (Show)
-- direction, tile, distance
type HitInfo = (HitSide, IntVec2, Double)
-- pos: start position
-- dir: initial direction
-- plane: camera "plane" (a line, really, perpendicular to the direction)
traceRays :: TilemapArray -> Int -> DblVec2 -> DblVec2 -> DblVec2 -> [HitInfo]
traceRays arr numRays pos dir plane =
flip map [0..numRays] $ \x ->
let cameraX = 2 * ((dbl x) / (dbl numRays)) - 1
in traceRay arr pos $ dir `add` (plane `mul` cameraX)
traceRay :: TilemapArray -> DblVec2 -> DblVec2 -> HitInfo
traceRay arr pos#(posX, posY) dir#(dirX, dirY) =
-- map tile we're in (whole part of position)
-- position within map tile (fractional part of position)
let ((tileX, fracX), (tileY, fracY)) = (split posX, split posY)
tile = (tileX, tileY)
-- length of ray from one x or y-side to next x or y-side
deltaDistX = sqrt $ 1 + (square dirY / square dirX)
deltaDistY = sqrt $ 1 + (square dirX / square dirY)
deltaDist = (deltaDistX, deltaDistY)
-- direction of step
stepX = if dirX < 0 then -1 else 1
stepY = if dirY < 0 then -1 else 1
step = (stepX, stepY)
-- length of ray from current position to next x or y-side
sideDistX = deltaDistX * if dirX < 0 then fracX else 1 - fracX
sideDistY = deltaDistY * if dirY < 0 then fracY else 1 - fracY
sideDist = (sideDistX, sideDistY)
(hitSide, wallTile) = traceRayInner arr step deltaDist tile sideDist
in (hitSide, wallTile, calculateDistance hitSide pos dir wallTile step)
traceRayInner :: TilemapArray -> IntVec2 -> DblVec2 -> IntVec2 -> DblVec2 -> (HitSide, IntVec2)
traceRayInner arr step#(stepX, stepY) deltaDist#(deltaDistX, deltaDistY) tile sideDist#(sideDistX, sideDistY)
-- a wall has been hit, report hit direction and coördinates
| arr ! tile /= Empty = (hitSide, tile)
-- advance until a wall is hit
| otherwise = case hitSide of
EastWest ->
let newSideDist = first (deltaDistX+) sideDist
newTile = first (stepX+) tile
in
traceRayInner arr step deltaDist newTile newSideDist
NorthSouth ->
let newSideDist = second (deltaDistY+) sideDist
newTile = second (stepY+) tile
in
traceRayInner arr step deltaDist newTile newSideDist
where
hitSide = if sideDistX < sideDistY then EastWest else NorthSouth
-- calculate distance projected on camera direction
-- (an oblique distance would give a fisheye effect)
calculateDistance :: HitSide -> DblVec2 -> DblVec2 -> IntVec2 -> IntVec2 -> Double
calculateDistance EastWest (startX, _) (dirX, _) (tileX, _) (stepX, _) =
((dbl tileX) - startX + (1 - dbl stepX) / 2) / dirX
calculateDistance NorthSouth (_, startY) (_, dirY) (_, tileY) (_, stepY) =
((dbl tileY) - startY + (1 - dbl stepY) / 2) / dirY
-- calculate the height of the vertical line on-screen based on the distance
calculateHeight :: Double -> Double -> Double
calculateHeight screenHeight 0 = screenHeight
calculateHeight screenHeight perpWallDist = screenHeight / perpWallDist
width :: Double
height :: Double
(width, height) = (640, 480)
main :: IO ()
main = do
cvElem <- newElem "canvas" `with` [
attr "width" =: show width,
attr "height" =: show height
]
addChild cvElem documentBody
Just canvas <- getCanvas cvElem
let pos = (8, 8)
dir = (-1, 0)
plane = (0, 0.66)
renderGame canvas pos dir plane
renderGame :: Canvas -> DblVec2 -> DblVec2 -> DblVec2 -> IO ()
renderGame canvas pos dir plane = do
let rays = traceRays testMapTiles (floor width) pos dir plane
render canvas $ forM_ (zip [0..width - 1] rays) (\(x, (side, tile, dist)) ->
let lineHeight = calculateHeight height dist
wallColor = case testMapTiles ! tile of
RedWall -> RGB 255 0 0
BlueWall -> RGB 0 255 0
GreenWall -> RGB 0 0 255
YellowWall -> RGB 255 255 0
_ -> RGB 255 255 255
shadedWallColor = case side of
EastWest ->
let (RGB r g b) = wallColor
in RGB (r `div` 2) (g `div` 2) (b `div` 2)
NorthSouth -> wallColor
in color shadedWallColor $ do
translate (x, height / 2) $ stroke $ do
line (0, -lineHeight / 2) (0, lineHeight / 2))
-- 25fps
let fps = 25
timeout = (1000 `div` fps) :: Int
rots_per_min = 1
rots_per_sec = dbl rots_per_min / 60
rots_per_frame = rots_per_sec / dbl fps
tau = 2 * pi
increment = tau * rots_per_frame
setTimeout timeout $ do
renderGame canvas pos (rot dir $ -increment) (rot plane $ -increment)
HTML page:
<!doctype html>
<meta charset=utf-8>
<title>Raycaster</title>
<noscript>If you're seeing this message, either your browser doesn't support JavaScript, or it is disabled for some reason. This game requires JavaScript to play, so you'll need to make sure you're using a browser which supports it, and enable it, to play.</noscript>
<script src=raycast.js></script>
The "phantom faces" are occurring because an incorrect HitSide is being reported: you're saying the face was hit on a horizontal move (EastWest), but was actually hit on a vertical move (NorthSouth), or vice-versa.
Why is it reporting an incorrect value, then? if sideDistX < sideDistY then EastWest else NorthSouth seems pretty foolproof, right? And it is.
The problem isn't how we calculated that value. It's when we calculated that value. The distance calculation function needs to know the direction we moved in to get to the wall. However, what we've actually given is the direction we would move in if we were to keep going (that is, if that tile wasn't a wall, or we were to ignore it for some reason).
Look at the Haskell code:
traceRayInner arr step#(stepX, stepY) deltaDist#(deltaDistX, deltaDistY) tile sideDist#(sideDistX, sideDistY)
-- a wall has been hit, report hit direction and coördinates
| arr ! tile /= Empty = (hitSide, tile)
-- advance until a wall is hit
| otherwise = case hitSide of
EastWest ->
let newSideDist = first (deltaDistX+) sideDist
newTile = first (stepX+) tile
in
traceRayInner arr step deltaDist newTile newSideDist
NorthSouth ->
let newSideDist = second (deltaDistY+) sideDist
newTile = second (stepY+) tile
in
traceRayInner arr step deltaDist newTile newSideDist
where
hitSide = if sideDistX < sideDistY then EastWest else NorthSouth
Notice that we do things in this order:
calculate hitSide
check if a wall has been hit, and if so, report hitSide
move
Compare this to the original C++ code:
//perform DDA
while (hit == 0)
{
//jump to next map square, OR in x-direction, OR in y-direction
if (sideDistX < sideDistY)
{
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
}
else
{
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
//Check if ray has hit a wall
if (worldMap[mapX][mapY] > 0) hit = 1;
}
It does things in a different order:
check if a wall has been hit, and if so, report side (equivalent to hitSide)
move and calculate side
The C++ code only calculates side when it moves, and then it reports that value if it hits a wall. So, it reports the way it moved in order to hit the wall.
The Haskell code calculates side whether or not it moves: so it's correct for each move, but when it hits a wall, it reports the way it would have moved were it to keep going.
So, the Haskell code can be fixed by re-ordering it so that it checks for a hit after moving, and if so, reports the hitSide value from that move. This isn't pretty code, but it works:
traceRayInner arr step#(stepX, stepY) deltaDist#(deltaDistX, deltaDistY) tile sideDist#(sideDistX, sideDistY) =
let hitSide = if sideDistX < sideDistY then EastWest else NorthSouth
in case hitSide of
EastWest ->
let newSideDist = first (deltaDistX+) sideDist
newTile = first (stepX+) tile
in case arr ! newTile of
-- advance until a wall is hit
Empty -> traceRayInner arr step deltaDist newTile newSideDist
-- a wall has been hit, report hit direction and coördinates
_ -> (hitSide, newTile)
NorthSouth ->
let newSideDist = second (deltaDistY+) sideDist
newTile = second (stepY+) tile
in case arr ! newTile of
-- advance until a wall is hit
Empty -> traceRayInner arr step deltaDist newTile newSideDist
-- a wall has been hit, report hit direction and coördinates
_ -> (hitSide, newTile)
Problem solved!
Side note: I figured out what was wrong after carrying out the algorithm on paper. While in that particular case it just so happened the last two HitSide values matched, it became obvious that that they might not in every case. So, a big thanks to Madsy on Freenode's #algorithms for suggesting trying it out on paper. :)