implicit-0.4.1.0: Graphics/Implicit/Export/Render.hs
{- ORMOLU_DISABLE -}
-- Copyright 2016, Julia Longtin (julial@turinglace.com)
-- Implicit CAD. Copyright (C) 2011, Christopher Olah (chris@colah.ca)
-- Released under the GNU AGPLV3+, see LICENSE
-- Allow us to use the tearser parallel list comprehension syntax, to avoid having to call zip in the complicated comprehensions below.
{-# LANGUAGE ParallelListComp #-}
-- export getContour and getMesh, which returns the edge of a 2D object, or the surface of a 3D object, respectively.
module Graphics.Implicit.Export.Render (getMesh, getContour) where
import Prelude(error, (-), ceiling, ($), (+), (*), max, div, tail, fmap, reverse, (.), foldMap, min, Int, (<>), (<$>), traverse)
import Graphics.Implicit.Definitions (ℝ, ℕ, Fastℕ, ℝ2, ℝ3, TriangleMesh, Obj2, SymbolicObj2, Obj3, SymbolicObj3, Polyline(getSegments), (⋯/), fromℕtoℝ, fromℕ, ℝ3' (ℝ3'))
import Graphics.Implicit.Export.Symbolic.Rebound2 (rebound2)
import Graphics.Implicit.Export.Symbolic.Rebound3 (rebound3)
import Graphics.Implicit.ObjectUtil (getBox2, getBox3)
import Data.Foldable(fold)
import Linear ( V3(V3), V2(V2) )
-- Here's the plan for rendering a cube (the 2D case is trivial):
-- (1) We calculate midpoints using interpolate.
-- This guarentees that our mesh will line up everywhere.
-- (Contrast with calculating them in getSegs)
import Graphics.Implicit.Export.Render.Interpolate (interpolate)
-- (2) We calculate the segments separating the inside and outside of our
-- object on the sides of the cube.
-- getSegs internally uses refine from RefineSegs to subdivide the segs
-- to better match the boundary.
import Graphics.Implicit.Export.Render.GetSegs (getSegs)
-- (3) We put the segments from all sides of the cube together
-- and extract closed loops.
import Graphics.Implicit.Export.Render.GetLoops (getLoops)
-- (4) We tesselate the loops, using a mixture of triangles and squares
import Graphics.Implicit.Export.Render.TesselateLoops (tesselateLoop)
-- (5) We try to merge squares, then turn everything into triangles.
import Graphics.Implicit.Export.Render.HandleSquares (mergedSquareTris)
-- Success: This is our mesh.
-- Each step on the Z axis is done in parallel using Control.Parallel.Strategies
import Control.Parallel.Strategies (using, rdeepseq, parBuffer)
-- The actual code is just a bunch of ugly argument passing.
-- Utility functions can be found at the end.
-- For efficiency, we need to avoid looking things up in other lists
-- (since they're 3D, it's an O(n³) operation...). So we need to make
-- our algorithms "flow" along the data structure instead of accessing
-- within it. To do this we use the ParallelListComp GHC extention.
-- We also compute lots of things in advance and pass them in as arguments,
-- to reduce redundant computations.
-- All in all, this is kind of ugly. But it is necessary.
-- Note: As far as the actual results of the rendering algorithm, nothing in
-- this file really matters. All the actual decisions about how to build
-- the mesh are abstracted into the imported files.
-- For the 2D case, we need one last thing, cleanLoopsFromSegs:
import Graphics.Implicit.Export.Render.HandlePolylines (cleanLoopsFromSegs)
import Data.Maybe (fromMaybe)
import Graphics.Implicit.Primitives (getImplicit)
import Control.Lens (_Wrapped, view, over, _Just)
-- Set the default types for the numbers in this file.
default (ℕ, Fastℕ, ℝ)
getMesh :: ℝ3 -> SymbolicObj3 -> TriangleMesh
getMesh res@(V3 xres yres zres) symObj =
let
-- Grow bounds a little to avoid sampling at exact bounds
(obj, (p1@(V3 x1 y1 z1), p2)) = rebound3 (getImplicit symObj, getBox3 symObj)
-- How much space are we rendering?
d = p2 - p1
-- How many steps will we take on each axis?
nx, ny, nz :: ℕ
steps@(V3 nx ny nz) = ceiling <$> ( d ⋯/ res)
-- How big are the steps?
(V3 rx ry rz) = d ⋯/ (fromℕtoℝ <$> steps)
-- The planes we're rendering along.
pYZ = [ x1 + rx*fromℕtoℝ n | n <- [0.. nx] ]
pXZ = [ y1 + ry*fromℕtoℝ n | n <- [0.. ny] ]
pXY = [ z1 + rz*fromℕtoℝ n | n <- [0.. nz] ]
-- performance tuning.
-- FIXME: magic number.
forcesteps :: Int
forcesteps = 32
-- Evaluate obj to avoid waste in mids, segs, later.
objV = par3DList nx ny nz
-- Sample our object(s) at every point in the 3D space given.
par3DList :: ℕ -> ℕ -> ℕ -> [[[ℝ]]]
par3DList lenx leny lenz =
[[[ sample mx my mz
| mx <- [0..lenx] ] | my <- [0..leny] ] | mz <- [0..lenz] ]
`using` parBuffer (max 1 $ div (fromℕ (lenx+leny+lenz)) forcesteps) rdeepseq
-- sample our object(s) at the given point.
sample :: ℕ -> ℕ -> ℕ -> ℝ
sample mx my mz = obj $
V3
(x1 + rx*fromℕtoℝ mx)
(y1 + ry*fromℕtoℝ my)
(z1 + rz*fromℕtoℝ mz)
-- (1) Calculate mid points on X, Y, and Z axis in 3D space.
midsZ = [[[
interpolate (V2 z0 objX0Y0Z0) (V2 z1' objX0Y0Z1) (appABC obj x0 y0) zres
| x0 <- pYZ | objX0Y0Z0 <- objY0Z0 | objX0Y0Z1 <- objY0Z1
]| y0 <- pXZ | objY0Z0 <- objZ0 | objY0Z1 <- objZ1
]| z0 <- pXY | z1' <- tail pXY | objZ0 <- objV | objZ1 <- tail objV
] `using` parBuffer (max 1 $ div (fromℕ nz) forcesteps) rdeepseq
midsY = [[[
interpolate (V2 y0 objX0Y0Z0) (V2 y1' objX0Y1Z0) (appACB obj x0 z0) yres
| x0 <- pYZ | objX0Y0Z0 <- objY0Z0 | objX0Y1Z0 <- objY1Z0
]| y0 <- pXZ | y1' <- tail pXZ | objY0Z0 <- objZ0 | objY1Z0 <- tail objZ0
]| z0 <- pXY | objZ0 <- objV
] `using` parBuffer (max 1 $ div (fromℕ ny) forcesteps) rdeepseq
midsX = [[[
interpolate (V2 x0 objX0Y0Z0) (V2 x1' objX1Y0Z0) (appBCA obj y0 z0) xres
| x0 <- pYZ | x1' <- tail pYZ | objX0Y0Z0 <- objY0Z0 | objX1Y0Z0 <- tail objY0Z0
]| y0 <- pXZ | objY0Z0 <- objZ0
]| z0 <- pXY | objZ0 <- objV
] `using` parBuffer (max 1 $ div (fromℕ nx) forcesteps) rdeepseq
-- (2) Calculate segments for each side
segsZ = [[[
injZ z0 <$> getSegs (V2 x0 y0) (V2 x1' y1') (obj **$ z0) (objX0Y0Z0, objX1Y0Z0, objX0Y1Z0, objX1Y1Z0) (midA0, midA1, midB0, midB1)
| x0<-pYZ | x1'<-tail pYZ |midB0<-mX'' | midB1<-mX'T | midA0<-mY'' | midA1<-tail mY'' | objX0Y0Z0<-objY0Z0 | objX1Y0Z0<- tail objY0Z0 | objX0Y1Z0<-objY1Z0 | objX1Y1Z0<-tail objY1Z0
]| y0<-pXZ | y1'<-tail pXZ |mX'' <-mX' | mX'T <-tail mX' | mY'' <-mY' | objY0Z0 <-objZ0 | objY1Z0 <-tail objZ0
]| z0<-pXY |mX' <-midsX | mY' <-midsY | objZ0 <-objV
] `using` parBuffer (max 1 $ div (fromℕ nz) forcesteps) rdeepseq
segsY = [[[
injY y0 <$> getSegs (V2 x0 z0) (V2 x1' z1') (obj *$* y0) (objX0Y0Z0, objX1Y0Z0, objX0Y0Z1, objX1Y0Z1) (midA0, midA1, midB0, midB1)
| x0<-pYZ | x1'<-tail pYZ | midB0<-mB'' | midB1<-mBT' | midA0<-mA'' | midA1<-tail mA'' | objX0Y0Z0<-objY0Z0 | objX1Y0Z0<-tail objY0Z0 | objX0Y0Z1<-objY0Z1 | objX1Y0Z1<-tail objY0Z1
]| y0<-pXZ | mB'' <-mB' | mBT' <-mBT | mA'' <-mA' | objY0Z0 <-objZ0 | objY0Z1 <-objZ1
]| z0<-pXY | z1'<-tail pXY | mB' <-midsX | mBT <-tail midsX | mA' <-midsZ | objZ0 <-objV | objZ1 <-tail objV
] `using` parBuffer (max 1 $ div (fromℕ ny) forcesteps) rdeepseq
segsX = [[[
injX x0 <$> getSegs (V2 y0 z0) (V2 y1' z1') (obj $** x0) (objX0Y0Z0, objX0Y1Z0, objX0Y0Z1, objX0Y1Z1) (midA0, midA1, midB0, midB1)
| x0<-pYZ | midB0<-mB'' | midB1<-mBT' | midA0<-mA'' | midA1<-mA'T | objX0Y0Z0<-objY0Z0 | objX0Y1Z0<-objY1Z0 | objX0Y0Z1<-objY0Z1 | objX0Y1Z1<- objY1Z1
]| y0<-pXZ | y1'<-tail pXZ | mB'' <-mB' | mBT' <-mBT | mA'' <-mA' | mA'T <-tail mA' | objY0Z0 <-objZ0 | objY1Z0 <-tail objZ0 | objY0Z1 <-objZ1 | objY1Z1 <-tail objZ1
]| z0<-pXY | z1'<-tail pXY | mB' <-midsY | mBT <-tail midsY | mA' <-midsZ | objZ0 <- objV | objZ1 <- tail objV
] `using` parBuffer (max 1 $ div (fromℕ nx) forcesteps) rdeepseq
-- (3) & (4) : get and tesselate loops
-- FIXME: hack.
minres = xres `min` yres `min` zres
sqTris = [[[
foldMap (tesselateLoop minres obj) $
fromMaybe (error "unclosed loop in paths given") $
-- Shove the ℝ3s into ℝ3's to get the NaN checks, then
-- unwrap everything. This should mostly compile away
-- given that it is lensy and passing a newtype instance
-- around. `getLoops` is the function actually doing the
-- work we care about
over (_Just . traverse . traverse . traverse) (view _Wrapped) . getLoops . over (traverse . traverse) ℝ3' $
segX''' <>
mapR segX''T <>
mapR segY''' <>
segY'T' <>
segZ''' <>
mapR segZT''
| segZ'''<- segZ''| segZT''<- segZT'
| segY'''<- segY''| segY'T'<- segY'T
| segX'''<- segX''| segX''T<- tail segX''
]| segZ'' <- segZ' | segZT' <- segZT
| segY'' <- segY' | segY'T <- tail segY'
| segX'' <- segX'
]| segZ' <- segsZ | segZT <- tail segsZ
| segY' <- segsY
| segX' <- segsX
] `using` parBuffer (max 1 $ div (fromℕ $ nx+ny+nz) forcesteps) rdeepseq
in
-- (5) merge squares, etc
mergedSquareTris . fold . fold $ fold sqTris
-- | getContour gets a polyline describing the edge of a 2D object.
getContour :: ℝ2 -> SymbolicObj2 -> [Polyline]
getContour res@(V2 xres yres) symObj =
let
-- Grow bounds a little to avoid sampling at exact bounds
(obj, (p1@(V2 x1 y1), p2)) = rebound2 (getImplicit symObj, getBox2 symObj)
-- The size of the region we're being asked to search.
d = p2 - p1
-- How many steps will we take on each axis?
nx, ny :: ℕ
steps@(V2 nx ny) = ceiling <$> (d ⋯/ res)
-- How big are the steps?
(V2 rx ry) = d ⋯/ (fromℕtoℝ <$> steps)
-- The lines we are rendering along.
pX = [ x1 + rx*fromℕtoℝ p | p <- [0.. nx] ]
pY = [ y1 + ry*fromℕtoℝ p | p <- [0.. ny] ]
-- Performance tuning.
-- FIXME: magic number.
forcesteps :: Int
forcesteps = 32
-- Evaluate obj to avoid waste in mids, segs, later.
objV = par2DList nx ny
-- Sample our object(s) at every point in the 2D plane given.
par2DList :: ℕ -> ℕ -> [[ℝ]]
par2DList lenx leny =
[[ sample mx my
| mx <- [0..lenx]
] | my <- [0..leny]
] `using` parBuffer (max 1 $ div (fromℕ $ lenx+leny) forcesteps) rdeepseq
-- sample our object(s) at the given point.
sample :: ℕ -> ℕ -> ℝ
sample mx my = obj $
V2
(x1 + rx*fromℕtoℝ mx)
(y1 + ry*fromℕtoℝ my)
-- Calculate mid points on X axis in 2D space.
midsX = [[
interpolate (V2 x0 objX0Y0) (V2 x1' objX1Y0) (obj *$ y0) xres
| x0 <- pX | x1' <- tail pX | objX0Y0 <- objY0 | objX1Y0 <- tail objY0
]| y0 <- pY | objY0 <- objV
] `using` parBuffer (max 1 $ div (fromℕ nx) forcesteps) rdeepseq
-- Calculate mid points on Y axis in 2D space.
midsY = [[
interpolate (V2 y0 objX0Y0) (V2 y1' objX0Y1) (obj $* x0) yres
| x0 <- pX | objX0Y0 <- objY0 | objX0Y1 <- objY1
]| y0 <- pY | y1' <- tail pY | objY0 <- objV | objY1 <- tail objV
] `using` parBuffer (max 1 $ div (fromℕ ny) forcesteps) rdeepseq
-- Calculate segments for each side
segs = [[
getSegs (V2 x0 y0) (V2 x1' y1') obj (objX0Y0, objX1Y0, objX0Y1, objX1Y1) (midA0, midA1, midB0, midB1)
| x0<-pX | x1'<-tail pX |midB0<-mX'' | midB1<-mX'T | midA0<-mY'' | midA1<-tail mY'' | objX0Y0<-objY0 | objX1Y0<-tail objY0 | objX0Y1<-objY1 | objX1Y1<-tail objY1
]| y0<-pY | y1'<-tail pY |mX'' <-midsX | mX'T <-tail midsX | mY'' <-midsY | objY0 <- objV | objY1 <- tail objV
] `using` parBuffer (max 1 $ div (fromℕ $ nx+ny) forcesteps) rdeepseq
in
-- Merge squares
cleanLoopsFromSegs . fold $ fold segs
-- utility functions
injX :: ℝ -> Polyline -> [ℝ3]
injX val polyline = prepend val <$> getSegments polyline
where
prepend :: ℝ -> ℝ2 -> ℝ3
prepend a (V2 b c) = V3 a b c
injY :: ℝ -> Polyline -> [ℝ3]
injY val polyline = insert val <$> getSegments polyline
where
insert :: ℝ -> ℝ2 -> ℝ3
insert b (V2 a c) = V3 a b c
injZ :: ℝ -> Polyline -> [ℝ3]
injZ val polyline = postfix val <$> getSegments polyline
where
postfix :: ℝ -> ℝ2 -> ℝ3
postfix c (V2 a b) = V3 a b c
($**) :: Obj3 -> ℝ -> ℝ2 -> ℝ
f $** a = \(V2 b c) -> f (V3 a b c)
infixr 0 $**
(*$*) :: Obj3 -> ℝ -> ℝ2 -> ℝ
f *$* b = \(V2 a c) -> f (V3 a b c)
infixr 0 *$*
(**$) :: Obj3 -> ℝ -> ℝ2 -> ℝ
f **$ c = \(V2 a b) -> f (V3 a b c)
infixr 0 **$
($*) :: Obj2 -> ℝ -> ℝ -> ℝ
f $* a = f . V2 a
infixr 0 $*
(*$) :: Obj2 -> ℝ -> ℝ -> ℝ
f *$ b = \a -> f (V2 a b)
infixr 0 *$
appABC :: Obj3 -> ℝ -> ℝ -> ℝ -> ℝ
appABC f a b c = f (V3 a b c)
appBCA :: Obj3 -> ℝ -> ℝ -> ℝ -> ℝ
appBCA f b c a = f (V3 a b c)
appACB :: Obj3 -> ℝ -> ℝ -> ℝ -> ℝ
appACB f a c b = f (V3 a b c)
mapR :: [[ℝ3]] -> [[ℝ3]]
mapR = fmap reverse