Showing posts with label ugly. Show all posts
Showing posts with label ugly. Show all posts

Tuesday, 11 February 2014

Parallel Delaunay Triangulation

Resource handling post will happen in a bit, as I got distracted after reading a fascinating paper entitled Localizing the Delaunay Triangulation and its Parallel Implementation (Renjie Chen/Craig Gotsman). The paper itself is quite googleable, although in some cases it hides behind a paywall.
After a few false starts, it translated to Haskell relatively cleanly. As ever, this is not terribly idiomatic but I thought it might be interesting to some people.

First, some elided code: I have some very, very simple linear algebra types kicking around of the form FloatN. They're designed to mimic the ones found in shading languages or in SIMD intrinsics rather than behave correctly, so you can multiply them together and add a scalar to them if you like.

Further, I cobbled together a quick k-d tree implementation for doing spatial queries. In this case, the only important one is fetching all the objects within a circle. For my purposes I'm only interested in periodic triangulations and corresponding Voronoi cells, and will be restricting the point distribution to be dense with a known minimum distance between points. Anticipating a move to a more efficient spatial structure at a later date, here's the interface I'm broadly coding against:

class SpatialStructure2D a where
  type Query a
  positionOf :: a -> Query a -> Float2
  queryCircle :: a -> Float2 -> Float -> [Query a]

In particular, the Query a type and positionOf function exist so some future structure can use indices into a flat array or vector of positional data, which will be useful when constructing vertex buffers later. Might be thinking too far ahead there.

On to the implementation! A quick record type holds the local Delaunay one-ring (vertices directly connected to the point under consideration in the triangulaiton) and the circumcircles described by this fan of triangles. The circumcenters obviously form our nice little Voronoi cell at the end.

data Delaunay = Delaunay {
  dlyPoint :: !Float2,
  dlyRing :: [Float2],
  vorCell :: [(Float2,Float)] } deriving (Eq, Show)

initDly :: Float2 -> Delaunay
initDly c = Delaunay {
  dlyPoint = c,
  dlyRing = p0,
  vorCell = zipWith (circumcircle c) p0 (rotateList p0)
  } where
    p0 = [float2 1.5 0.5, float2 0.5 1.5, float2 (-0.5) 0.5, float2 0.5 (-0.5)]

The utility function initDly just makes a massive quad around the test point so we can start from a known (but uselessly huge) triangulation. Some more tangential stuff:

circumcircle :: Float2 -> Float2 -> Float2 -> (Float2,Float)
circumcircle a b c = (cen,rad)
  where
    cen = circumcenter a b c
    rad = circumradius a b c

rotateList :: [a] -> [a]
rotateList [] = []
rotateList (x:xs) = xs ++ [x]

rotateUntil :: (a -> Bool) -> [a] -> [a]
rotateUntil _ [] = []
rotateUntil _ [a] = [a]
rotateUntil p xs = go (take (length xs) (iterate rotateList xs))
  where
    go [] = xs
    go (ys@(y:_) : yss)
      | p y = ys
      | otherwise = go yss

The functions to calculate the circumcenter and circumradius of a triangle are omitted because I shamelessly translated the implementations from here, and they're quite long and uninteresting. The list rotations are ugly but made things easy.

data Halfplane = Halfplane Float2 Float2 Float
halfplane :: Float2 -> Float2 -> Halfplane
halfplane v o = Halfplane o (normalise d) (0.5 * vecLen d) where d = v - o

halfplaneTest :: Halfplane -> Float2 -> Float
halfplaneTest (Halfplane o n d) x = d - (n `dot` (x - o))

inHalfplane :: Halfplane -> Float2 -> Bool
inHalfplane h x = halfplaneTest h x >= 0

Testing whether or not points are inside the halfplane defined by the bisector of two points is key to the incremental bit of the algorithm, where we test a point against the current local triangulation:

updateDT :: Delaunay -> Float2 -> Delaunay
updateDT locDT v = if inHvQ == q0 then locDT else locDT { dlyRing = p1, vorCell = q1 }
  where
    c = dlyPoint locDT
    p0 = dlyRing locDT
    q0 = vorCell locDT
    hv = halfplane v (dlyPoint locDT)
    passHv = inHalfplane hv . fst . fst
    failHv = not . passHv
    --ensure the prefix of the one-ring/voronoi polygon is in halfplane
    qp0 = rotateUntil passHv (rotateUntil failHv (zip q0 p0))
    (inHv, outHv) = span passHv qp0
    (inHvQ, inHvP) = unzip inHv
    --avoid strictness, profit
    dp0 = snd (head outHv)
    dp1 = head inHvP
    newQ0 = circumcircle c dp0 v
    newQ1 = circumcircle c v dp1
    q1 = newQ0 : newQ1 : inHvQ
    p1 = dp0 : v : inHvP

Again, ugly. Apart from the noise of packing and unpacking tuples, this code is essentially rotating both the one-ring and Voronoi cell until the circumcenters outside the halfplane under consideration comprise the suffix of the list. If no points fall outside the halfplane, the triangulation remains unchanged and we just return it. Otherwise we introduce the new point in place of those we removed and stitch everything together. Note that although we furiously and pointlessly rotate both the one-ring and Voronoi polygon, they never change winding order.

Finally, to actually calculate the local triangulation from scratch:

localDT :: (SpatialStructure2D a, Query a ~ Float2) => a -> Float -> Float2 -> Delaunay
localDT tree r c = updateFrom dly0
  where
    dly0 = foldl' updateDT (initDly c) (filter (/= c) v0)
    v0 = queryCircle tree c r
    updateFrom dly = if null vs then dly else updateFrom (updateDT dly (head $ head vs))
      where
        fl = filter (`notElem` (c: (dlyRing dly)))
        vss :: [[Float2]]
        vss = map (fl . uncurry (queryCircle tree)) (vorCell dly)
        vs = dropWhile null vss

Technically I should rewrite this to use the positionOf and relax the restriction on Query a. Oops. Anyway!
Because we know the minimum distance between points and that the point set is relatively dense, passing in an initial query radius to quickly cut the initial working set down to size is a huge win. Then all that is left is to try each circumcircle in the current triangulation - if it might add a new point, update the local triangulation and recurse. When every circumcircle is empty (excepting those points already in the triangulation), we're done.

Now all that we need to do is something like map (localDT tree (r*2.0)) points `using` parListChunk 32 rseq, where tree is a spatial structure containing our random points, r is the minimum distance between those random points... and we get parallel computation of all the interesting things.

Now, there are lots and lots of bits of code that are ugly and somewhat hacky in here, and it could certainly be optimised hugely (profiling suggests most of the time is spent traversing my k-d tree), this still manages to spit the periodic Voronoi diagram for ~64k points to a .obj file on disc in around ten seconds (on an 8-core i7). Should absolutely be sufficient for my purpose as it stands.

And courtesy of Blender and the subdivision modifier, here are those little cells with a bit of smoothing applied:

Saturday, 18 February 2012

Holes



Not an awful lot to discuss this weekend. The dungeon is now a multi-level affair; you can just see the hole in the ceiling in the ugly screenshot.

At the moment, you can fall down a hole by walking into it, but climbing up requires a few conditions to be met. You must be directly under the hole, facing a wall (against which I shall eventually rest a ladder, I suspect), and the hex on the upper floor directly above the bit of wall you're facing must also be clear of obstacles. These conditions being met, you scramble on to the upper floor.

I'm considering shrinking the vertical distance somewhat. To keep things simple, the hexes have a diameter of 1 unit, the player has a notional eye-height of 1 unit above the floor, and ceilings are 2 units off the floor. Whilst this mostly works, it means that even with a relatively wide vertical FoV (I'm using ~100 degrees in recent screenshots), it's hard to clearly see a hole you're standing underneath. I'm considering just halving everything in the vertical direction and seeing what it looks like.

I also finally knuckled down and properly categorised wall-tiles. Given a tile that contains a wall and that isn't completely surrounded by walls, you either have:
  • Zero neighbouring walls - a pillar.
  • One neighbouring wall, a length of wall comes to a stop here.
  • Two neighbouring walls - with the walls in ortho, meta, or para position (adjacent, with a one-hex gap, or opposite each other).
  • Three neighbouring walls - four possibilities here, either clumped together, in a Y shape, or two different chiralities of "two-blocks-together plus one block on its own".
  • Four neighbouring walls - equivalent to swapping the two-wall case for two-spaces.
  • And lastly five neighoubring walls, giving a little nook.

Someone smart would doubtless have done something clever involving symmetries; I just assigned each wall tile a binary number by starting at the hex due north and proceeding in counterclockwise fashion around neighbours, setting a 1 bit for walls and 0 for empty spaces. I created rough meshes for the thirteen interesting cases in Blender and named them according to this scheme (wall_00, wall_09 etc). Then it's just a case of extracting this categorisation number from the map data at runtime, rotating the bits to get the minimum number out of the given arrangement of zeroes and ones, and rotating the numbered mesh the same number of times before rendering.

So that worked quite well, apart from the tiny fact that it's looking like a real pain in the arse to author interesting meshes without leaving sparkly gaps. I'm considering welding the mesh as a post-process, either the whole dungeon or in chunks depending on poly budget, but by this point it almost feels more sensible to hand-craft a dungeon level as a chunk of unique geometry and then go around setting up the logical grid to match.

Alternatively, it should be easy to do something like calculate a large, concave polygon for all dungeon geometry and then extrude it, but this doesn't help make the walls interesting to look at.

None of these approaches save hand-crafting or something involving marching cubes/tets suggest anything for a more natural environment such as caves, either.

Think I'm going to try to get the 2D debug viewer working and maintain that alongside the project, so I have somewhere to test out things whilst wrestling with the demons of content creation.

Monday, 6 February 2012

Sunday Progress

A few really minor features added over the weekend:

  • Mouselook. Trying out an implementation where you can only look around in a limited arc, and having the view direction automatically rotate back to the default when you start moving or turning your character. Not sure on this, might be more useful to have your character's direction change once the view rotates sufficiently. Danger here is that I'd like to have facing be an important tactical concern within the game, so character rotation is a distinct action that you don't want to trigger by accident.
  • Textures! OK, this was a ten minute bodge-job hooking up javax.image.ImageIO to grab JPG and PNG files, and they don't even have mipmaps yet. But it works, and as I have a working DDS export implementation... getting DDS import in place shouldn't be too hard. Then they can even be properly compressed textures with pregenerated mipmaps and all kinds of... ancient, unremarkable technology.
  • Tangents! And Bitangents! I'm just generating these at load-time from the data in the .obj files. I'm fully aware that at some point I'm just going to have to face the Python and create a Blender export script to a more sensible format, but... bleck, Python.


As a consequence behold the power of really, really ugly normal and diffuse maps:

More importantly than all this minor tech progress, and thanks in no small part to the aid of He who is called Fhtagn, the game actually has a design document! And the design is, at least for now, inspiring. It's good to have a direction. We're currently lumping everything in a few shared google docs, which is a nicely asynchronous way of working.

In between writing the code and reducing the inspiration quotient of the document, I've been playing a bunch of CRPGs (I blame finding The CRPG Addict's blog archive). In particular: Morrowind, Wizardry 8, Temple of Elemental Evil, Neverwinter Nights, and Neverwinter Nights 2. Flitting about between these is playing merry hell with my ability to keep track of quests, but the contrast is fascinating.