-- Put your name and matriculation number here
-- Baldur Karlsson s0561810
--
-- ## Comments written by me, not in the original file, prefixed with
-- ## and separated from original comments
{-
'########::'##:::::::'########::::'###:::::'######::'########:
##.... ##: ##::::::: ##.....::::'## ##:::'##... ##: ##.....::
##:::: ##: ##::::::: ##::::::::'##:. ##:: ##:::..:: ##:::::::
########:: ##::::::: ######:::'##:::. ##:. ######:: ######:::
##.....::: ##::::::: ##...:::: #########::..... ##: ##...::::
##:::::::: ##::::::: ##::::::: ##.... ##:'##::: ##: ##:::::::
##:::::::: ########: ########: ##:::: ##:. ######:: ########:
..:::::::::........::........::..:::::..:::......:::........::
'##::: ##::'#######::'########:'########:
###:: ##:'##.... ##:... ##..:: ##.....::
####: ##: ##:::: ##:::: ##:::: ##:::::::
## ## ##: ##:::: ##:::: ##:::: ######:::
##. ####: ##:::: ##:::: ##:::: ##...::::
##:. ###: ##:::: ##:::: ##:::: ##:::::::
##::. ##:. #######::::: ##:::: ########:
..::::..:::.......::::::..:::::........::
The inf1a 2005 competition entry for myself is only by myself, and is at the end of this file.
PLEASE READ THE FINAL COMMENTS as they explain usage and some minor gotchas. Specifically,
the entry may take a while to run and generate at the default settings - the DICE machine maesquoy
ran it in 30 seconds on average, please wait at least that long for it to finish.
Thank you :)
Baldur
-}
import LSystem
import List
-- 1.
join :: [Command] -> Command
join [] = Sit
join (x:xs) = foldr1 (:#) (x:xs)
-- 2.
--
-- ## if we have two commands separated by an :# then
-- ## split each command and concatenate the split strings
-- ## together. Then if it is only a single command, drop it
-- ## if it is Sit, or make it into a list otherwise
split :: Command -> [Command]
split (x:#xs) = (split x) ++ (split xs)
split Sit = []
split x = [x]
-- 3.
copy :: Int -> Command -> Command
copy n c = join (replicate n c)
-- 4.
hexagon :: Distance -> Command
hexagon len = copy 6 (Go len :# Turn 60.0)
-- 5.
--
-- ## 360 / n gives the size of each angle
polygon :: Distance -> Int -> Command
polygon len n = copy n (Go len :# Turn (360.0 / fromIntegral(n)))
-- 6.
--
-- ## Go the distance, turn the angle, recurse again until you hit level 0
spiral :: Distance -> Int -> Distance -> Angle -> Command
spiral _ 0 _ _ = Sit
spiral maxlen n step angle = Go maxlen :# Turn angle :# (spiral (maxlen-step) (n-1) step angle)
-- 7.
--
-- ## Assumes that Sits have been stripped out.
-- ##
-- ## a single optimise pass will remove Go 0 or Turn 0 commands, then add Go
-- ## commands then add Turn commands. When adding go or turn commands you have
-- ## to apply the pass over the added command, in case it is go or turn 0 now.
-- ## If you have neither go or turn 0 and no adjacant same commands, you must have
-- ## Turn Go, or Go Turn. There's nothing you can do to optimise this, so don't
-- ## do anything. However, only drop off the first command. If you had Go 10 :# Turn 20 :# Turn -20
-- ## you need to keep teh turn 20 in for later optimising, but you can drop off the go 10.
-- ##
-- ## ** NB ** - If you only run this once, you might get something like this:
-- ## Go 10 :# Turn 20 :# Turn -20 :# Go 20
-- ## because the first go 10 would be dropped, you would end up with Go 10 :# Go 20, as the Go 10
-- ## would not be revisited after the Turns disappear. For this reason you should call this function
-- ## twice to be sure you have the optimum command.
optimisePass :: [Command] -> [Command]
optimisePass [] = []
optimisePass (Go 0:xs) = optimisePass xs
optimisePass (Turn 0:xs) = optimisePass xs
optimisePass (Go x:Go y:xs) = optimisePass (Go (x+y) : xs)
optimisePass (Turn x:Turn y:xs) = optimisePass (Turn (x+y) : xs)
optimisePass (x:xs) = x:optimisePass xs
-- ## split the command to a string, take out sits, then call optimisepass twice, then join it all together again
optimise :: Command -> Command
optimise c = join (optimisePass (optimisePass ((filter (/= Sit) (split c)))))
-- 8.
arrowhead :: Int -> Command
arrowhead x = f x
where
f 0 = Go 10
f (x+1) = g x :# p :# f x :# p :# g x
g 0 = Go 10
g (x+1) = f x :# n :# g x :# n :# f x
n = Turn 60
p = Turn (-60)
-- 9.
branch :: Int -> Command
branch x = g x
where
g 0 = Go 10
g (x+1) = f x :# n :# Branch ( Branch (g x) :# p :# g x) :# p :# f x :# Branch (p :# f x :# g x) :# n :# g x
f 0 = Go 10
f (x+1) = f x :# f x
n = Turn 22.5
p = Turn (-22.5)
-- 10.
hilbert :: Int -> Command
hilbert x = l x
where
l 0 = Go 10
l (x+1) = p :# r x :# f x :# n :# l x :# f x :# l x :# n :# f x :# r x :# p
r 0 = Go 10
r (x+1) = n :# l x :# f x :# p :# r x :# f x :# r x :# p :# f x :# l x :# n
f 0 = Go 10
f (x+1) = f x
n = Turn 90
p = Turn (-90)
-----------------------
-- competition entry --
-----------------------
-- *******************--
-----------------------
--
-- Comments here might not be great. They won't be prefixed with ## either
--
-- *******************--
-----------------------
-- Type declarations for 2D, 3D, 3D homogenised vertices and 2D lines
type Vertex2f = (Float, Float)
type Vertex3f = (Float, Float, Float)
type Vertex4f = (Float, Float, Float, Float)
type Line2f = (Vertex2f, Vertex2f)
-- draw a line at point (x, y) to point (a, b) with the colour specified
--
-- the method here is to move inklessly to the start of the line, then work out the direction and length of the line and draw it.
-- Generally, you'd want to branch before doing this because it will not return to its starting position.
-- It assumes the turtle is at the origin
drawline :: Pen -> Line2f -> Command
drawline col ((x,y),(a,b)) = GrabPen Inkless :# Turn yang :# Go (abs y) :# Turn xang :# Go (abs x) :# Turn (-xang-yang) :# Turn (180.0/pi * lineang) :# GrabPen col :# Go (sqrt ((b-y)^2 + (a-x)^2))
where yang | y >= 0.0 = 0.0
| y < 0.0 = 180.0
xang | x >= 0.0 = yang-90.0
| x < 0.0 = yang+90.0
-- use trig to work out the signs and which angles to turn to get to the start point
lineang | a-x == 0.0 && b-y < 0.0 = pi
| a-x == 0.0 && b-y >= 0.0 = 0.0
| b-y == 0.0 && a-x < 0.0 = pi/2
| b-y == 0.0 && a-x >= 0.0 = -pi/2
| b-y > 0.0 && a-x > 0.0 = -(pi/2) + atan(abs((b-y)/(a-x)))
| b-y > 0.0 && a-x < 0.0 = (pi/2) - atan(abs((b-y)/(a-x)))
| b-y < 0.0 && a-x > 0.0 = -pi + atan(abs((a-x)/(b-y)))
| b-y < 0.0 && a-x < 0.0 = pi - atan(abs((a-x)/(b-y)))
-- draw a series of vertices. This just branches and passes the colours and vertices to drawline
verts :: [Pen] -> [Vertex2f] -> Command
verts _ [] = Sit
verts [] (x:xs) = error ("not enough pens, need " ++ show ((length xs)+1) ++ " more")
verts (p:ps) (x:y:xs) = Branch (drawline p (x,y)) :# verts ps xs
verts _ _ = error "uneven number of verts"
-- Projection constants and a lame hack to scale things up
zNear, zFar, aspect, scale, fovy :: Float
zNear = 1.0
zFar = 1500.0
scale = 100.0
aspect = 3.0/4.0
fovy = pi/4.0
-- give vertices a w component
homogenise :: Vertex3f -> Vertex4f
homogenise (x,y,z) = (x,y,z,1.0)
-- take away a vertex's w component by dividing through (so it is 1 and can be ommitted)
-- w = 0 is the infinity point
dehomogenise :: Vertex4f -> Vertex3f
dehomogenise (x,y,z,0.0) = error "w = 0"
dehomogenise (x,y,z,w) = (x/w,y/w,z/w)
-- render a series of points by homogenising, projecting and dehomogenising them. Scale them up with a cheap hack
render :: [Vertex3f] -> [Vertex2f]
render xs = [(x*scale,y*scale) | (x,y,z) <- (map dehomogenise (map project (map homogenise xs)))]
-- project a single point from world space to screen space. See derivation of a projection
-- matrix for more details here
project :: Vertex4f -> Vertex4f
project (x,y,z,w) = ((zNear*x)/(aspect*(tan fovy)),
(zNear*y)/(tan fovy),
-(((zFar+zNear)*z)/(zFar-zNear))-((2*zFar*zNear*w)/(zFar-zNear)),
-z)
-- the following three functions rotate xs by theta around an axis
rotz :: Float -> [Vertex3f] -> [Vertex3f]
rotz theta xs = [(
(x * (cos theta)) - (y * (sin theta)),
(y * (cos theta)) + (x * (sin theta)),
z
)
| (x,y,z) <- xs]
roty :: Float -> [Vertex3f] -> [Vertex3f]
roty theta xs = [(
(x * (cos theta)) + (z * (sin theta)),
y,
(z * (cos theta)) - (x * (sin theta))
)
| (x,y,z) <- xs]
rotx :: Float -> [Vertex3f] -> [Vertex3f]
rotx theta xs = [(
x,
(y * (cos theta)) + (z * (sin theta)),
(z * (cos theta)) - (y * (sin theta))
)
| (x,y,z) <- xs]
-- translate in 2 or 3 dimensions. Translate is a synonym for translate3f as it is most common
translate3f, translate :: Vertex3f -> [Vertex3f] -> [Vertex3f]
translate3f (dx,dy,dz) xs = [(x+dx, y+dy, z+dz) | (x,y,z) <- xs]
translate = translate3f
translate2f :: Vertex2f -> [Vertex2f] -> [Vertex2f]
translate2f (dx,dy) xs = [(x+dx, y+dy) | (x,y) <- xs]
-- generate a pseudorandom number based on a seed
-- RAND_MAX = 32768
rand :: Int -> Int
rand prev = (next `div` 65536) `mod` 32768
where next = prev * 1103515245 + 12345
-- rands is an array of pseudorandom numbers
numrands :: Int
numrands = 100000
rands :: [Float]
rands = [((fromIntegral . rand) x)/32768.0 | x <- [1..numrands]]
-- random returns a number between 0 and 1 based on a seed (aka index)
random :: Int -> Float
random x = rands!!(mod x numrands)
-- recursive tree generation function. Good luck understanding this one
treegenrec :: Vertex3f -> Float -> Int -> Int -> Int -> [Vertex3f]
treegenrec _ _ 0 _ _ = []
treegenrec (x, y, z) len (n+1) num seed =
[(x, y, z), (x, y+len, z)] ++
(translate (x, y+len, z) (roty phi0 (rotz theta0 (translate (-x, -y-len, -z) (treegenrec (x,y+len,z) (len*0.7) n num (seed*17)))))) ++
(translate (x, y+len, z) (roty phi1 (rotz theta1 (translate (-x, -y-len, -z) (treegenrec (x,y+len,z) (len*0.7) n num (seed*18)))))) ++
(translate (x, y+len, z) (roty phi2 (rotz theta2 (translate (-x, -y-len, -z) (treegenrec (x,y+len,z) (len*0.7) n num (seed*19))))))
where
theta0 = (-pi/6)+(randrange (-0.4) 0.4 (seed*4))
theta1 = (randrange (-0.4) 0.4 (seed*5))
theta2 = (pi/6)+(randrange (-0.4) 0.4 (seed*6))
phi0 = (pi/3)+(randrange (-0.4) 0.4 (seed*7))
phi1 = (4*pi/3)+(randrange (-0.4) 0.4 (seed*8))
phi2 = (8*pi/3)+(randrange (-0.4) 0.4 (seed*9))
randrange lower upper seed = ((upper-lower)*(random seed))+lower
-- alias to call the recursive function with a parameter doubled up
treegen :: Vertex3f -> Float -> Int -> Int -> [Vertex3f]
treegen a b c d = treegenrec a b c c d
-- generate colours for the tree. Each line gets a value between 0 and 1
-- used to interpolate between two colours (bark and leaf). It is randomised
-- a little to look better
colourgenrec :: Int -> Int -> Int -> [Float]
colourgenrec 0 _ _ = []
{-
colourgenrec (n+1) num seed = [1.0 - ((2/pi)*(acos w))] ++
(colourgenrec n num seed) ++
(colourgenrec n num seed) ++
(colourgenrec n num seed)
where
w = (fromIntegral (num-n))/(fromIntegral num)
-}
colourgenrec (n+1) num seed = [w] ++
(colourgenrec n num ((round . random) seed)) ++
(colourgenrec n num ((round . random) (seed+1))) ++
(colourgenrec n num ((round . random) (seed+2)))
where
w | n == 0 = 1.0-((random seed)*0.4)
| otherwise = 0.0+((random seed)*0.2)
-- another alias
colourgen :: Int -> [Float]
colourgen a = colourgenrec a a ((round .random) a)
-- interpolate linearly between two colours
interp :: Pen -> Pen -> Float -> Pen
interp (Colour r1 g1 b1) (Colour r2 g2 b2) f = (Colour ((r1*f)+(r2*(1.0-f))) ((g1*f)+(g2*(1.0-f))) ((b1*f)+(b2*(1.0-f))))
interp _ _ _ = Inkless
-- bark and leaf colours
bark, leaf :: Pen
bark = (Colour (151.0/255.0)
(107.0/255.0)
(14.0/255.0))
leaf = (Colour 0 1.0 0)
-- do a little section of grass
grassbit :: Int -> Vertex2f -> [Vertex3f]
grassbit 0 _ = []
grassbit (n+1) (x, y) = [(x+dx,0,y+dy),(x+dx,height,y+dy)] ++ grassbit n (x,y)
where dx = random (n*672)
dy = random (n*263)
height = (random (n*342))*0.5 + 0.5
-- put together sections of grass in a square of dimensions x and y
grass :: Vertex2f -> [Vertex3f]
grass (x, y) = foldr1 (++) (map (grassbit 5) ([(a,b) | (a, b) <- (zip (foldr1 (++) (replicate (round (y+1)) [-x/2..(x/2)-1])) (foldr1 (++) (map (replicate (round (x+1))) [-y/2..(y/2)-1])))]))
-- get grass colours for a x by y lawn of grass
grasscolours :: Vertex2f -> [Pen]
grasscolours (x, y) = colours ((lengrass (x,y)) `div` 2)
where lengrass dim = length (grass dim)
scales n = random (n*2873)
colours n = [interp (Colour 0 0.6 0) (Colour 0.6 1.0 0.6) (scales x) | x <- [1..n]]
-- recursive function to generate a grid for the ground
groundrec :: Float -> Float -> Float -> Float -> [Vertex3f]
groundrec offx offy x y | offx > x || offy > y = []
| otherwise = [(-x/2, 0, y/2-offy), (x/2, 0, y/2-offy),
(-x/2+offx, 0, y/2), (-x/2+offx, 0, -y/2)] ++
groundrec (offx+1.0) (offy+1.0) x y
ground :: Float -> Float -> [Vertex3f]
ground = groundrec 0 0
-- get ground colours (not actually recursive)
groundcoloursrec :: Float -> Float -> Float -> Float -> [Pen]
groundcoloursrec a b c d = replicate ((lenground a b c d) `div` 2) (Colour 0 1 0)
where lenground a b c d = length (groundrec a b c d)
groundcolours :: Float -> Float -> [Pen]
groundcolours a b = replicate ((lenground a b) `div` 2) (Colour 0 1 0)
where lenground a b = length (ground a b)
-- parameters:
-- tree seed, x rotation angle, y rotation angle ground dimensions (square), tree depth, postscript scale
treedisplay :: Int -> Float -> Float -> Float -> Int -> Float -> IO ()
treedisplay treeseed xrot yrot gdim depth scale = (display scale) (verts (groundcolours gdim gdim ++ grasscolours (gdim, gdim) ++ (map (interp leaf bark) (colourgen depth))) (translate2f (0,-60) (render (translate (0, 0, -30) (rotx xrot (roty yrot (ground gdim gdim++grass (gdim, gdim)++(treegen (0, 0, 0) 8 depth treeseed))))))))
maindisplay :: Float -> IO ()
maindisplay = treedisplay 20 (-pi/6) (pi/3) 35 8
-- FINAL COMMENTS:
-- This takes 30 seconds to display on the DICE machine maesquoy. Please allow up to 1 minute on a slow computer, this is a computationally expensive
-- rendering, so please be patient. For the parameter to maindisplay, this is the scale passed to "display". 2.5 works well, but please adjust so the
-- whole image is viewable. You can call treedisplay directly and change some of the values. For example play with the two rotation values (not too much
-- or it might break) to check that the tree really is 3D. The last parameter given in maindisplay (8) is the number of subbranches. Turn that down and
-- reduce the computation time. Same with the second last (35) which is the dimensions of the grass. To generate different trees, change the seed (20).
-- This is limited by the pseudorandom number generator though.
--
-- Hope you like the picture,
--
-- Baldur Karlsson (16/11/05)