https://wiki.haskell.org/api.php?action=feedcontributions&user=Jsnow&feedformat=atomHaskellWiki - User contributions [en]2019-06-17T13:46:59ZUser contributionsMediaWiki 1.27.4https://wiki.haskell.org/index.php?title=Glome&diff=57860Glome2014-04-11T19:46:04Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome consists of the vector library GlomeVec, the ray tracing library GlomeTrace and an application called GlomeView.<br />
<br />
GlomeView renders into an SDL window, and thus requires SDL. The ray tracing engine itself, though, has few dependencies.<br />
<br />
(There is a deprecated package available on Hackage called glome-hs, which renders into an OpenGL window. It probably won't work with recent versions of GlomeTrace.)<br />
<br />
If everything works, you should see an image something like this when you run Glome the first time:<br />
<br />
(Testscene for early versions of Glome)<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(Testscene for 0.3.1)<br />
[[Image:GlomeView-0.3.png]]<br />
<br />
(Use +RTS -N4 or whatever if you want to use more than one core.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs included in GlomeView, then re-compile.<br />
<br />
==News==<br />
* Oct 25, 2009: GlomeVec library released<br />
* Jan 20, 2010: GlomeTrace library released<br />
* Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile * failure<br />
* Jan 27, 2014: New version of GlomeVec, GlomeTrace, and GlomeView fixing several bugs that caused CSG to render incorrectly.<br />
* Apr 7, 2014: New version of GloveTrace and GlomeView. Major reorganization of textures, and now objects can be given arbitrary tags that are returned by the ray intersection test.<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
(GlomeVec is also used by a separate project, [[IcoGrid]])<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]] (updated April 2014)<br />
<br />
==Features Lacking in Glome==<br />
<br />
This is a sort of to-do list<br />
<br />
*Antialiasing (update: it's available in version 0.3.1 on github)<br />
*Refraction<br />
*Photon mapping<br />
*PLY file parser<br />
*Rendering to a file<br />
*Support for portals (update: done)<br />
*Ability to tag objects and have ray intersection return the tag to tell you what you hit (update: done)<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
These are some successful open-source ray tracers. They're worth looking at for anyone interested in writing their own.<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
==Links==<br />
<br />
* [https://github.com/jimsnow/glome Glome repositories on github]<br />
* [http://hackage.haskell.org/package/GlomeView Glome Viewer on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]<br />
* [http://tog.acm.org/resources/RTNews/html/ Ray Tracing News]<br />
* [http://tog.acm.org/resources/SPD/ The Standard Procedural Database]<br />
* [http://www-graphics.stanford.edu/data/3Dscanrep/ Stanford Scanning Repository]<br />
* [http://ompf2.com resurrected OMPF forum]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=57859Glome2014-04-11T19:27:09Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome consists of the vector library GlomeVec, the ray tracing library GlomeTrace and an application called GlomeView.<br />
<br />
GlomeView renders into an SDL window, and thus requires SDL. The ray tracing engine itself, though, has few dependencies.<br />
<br />
(There is a deprecated package available on Hackage called glome-hs, which renders into an OpenGL window. It probably won't work with recent versions of GlomeTrace.)<br />
<br />
If everything works, you should see an image something like this when you run Glome the first time:<br />
<br />
(Testscene for early versions of Glome)<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(Testscene for 0.3.1)<br />
[[Image:GlomeView-0.3.png]]<br />
<br />
(Use +RTS -N4 or whatever if you want to use more than one core.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs included in GlomeView, then re-compile.<br />
<br />
==News==<br />
* Oct 25, 2009: GlomeVec library released<br />
* Jan 20, 2010: GlomeTrace library released<br />
* Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile * failure<br />
* Jan 27, 2014: New version of GlomeVec, GlomeTrace, and GlomeView fixing several bugs that caused CSG to render incorrectly.<br />
* Apr 7, 2014: New version of GloveTrace and GlomeView. Major reorganization of textures, and now objects can be given arbitrary tags that are returned by the ray intersection test.<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
(GlomeVec is also used by a separate project, [[IcoGrid]])<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]] (updated April 2014)<br />
<br />
==Features Lacking in Glome==<br />
<br />
This is a sort of to-do list<br />
<br />
*Antialiasing<br />
*Refraction<br />
*Photon mapping<br />
*PLY file parser<br />
*Rendering to a file<br />
*Support for portals (update: done)<br />
*Ability to tag objects and have ray intersection return the tag to tell you what you hit (update: done)<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
These are some successful open-source ray tracers. They're worth looking at for anyone interested in writing their own.<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
==Links==<br />
<br />
* [https://github.com/jimsnow/glome Glome repositories on github]<br />
* [http://hackage.haskell.org/package/GlomeView Glome Viewer on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]<br />
* [http://tog.acm.org/resources/RTNews/html/ Ray Tracing News]<br />
* [http://tog.acm.org/resources/SPD/ The Standard Procedural Database]<br />
* [http://www-graphics.stanford.edu/data/3Dscanrep/ Stanford Scanning Repository]<br />
* [http://ompf2.com resurrected OMPF forum]</div>Jsnowhttps://wiki.haskell.org/index.php?title=File:GlomeView-0.3.png&diff=57858File:GlomeView-0.3.png2014-04-11T19:23:33Z<p>Jsnow: Jsnow uploaded a new version of &quot;File:GlomeView-0.3.png&quot;</p>
<hr />
<div>Screenshot of Glome raytracer test scene.</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=57838Glome2014-04-09T19:46:38Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome consists of the vector library GlomeVec, the ray tracing library GlomeTrace and an application called GlomeView.<br />
<br />
GlomeView renders into an SDL window, and thus requires SDL. The ray tracing engine itself, though, has few dependencies.<br />
<br />
(There is a deprecated package available on Hackage called glome-hs, which renders into an OpenGL window. It probably won't work with recent versions of GlomeTrace.)<br />
<br />
If everything works, you should see an image something like this when you run Glome the first time:<br />
<br />
(Testscene for early versions of Glome)<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(Testscene for 0.3)<br />
[[Image:GlomeView-0.3.png]]<br />
<br />
(Use +RTS -N4 or whatever if you want to use more than one core.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs included in GlomeView, then re-compile.<br />
<br />
==News==<br />
* Oct 25, 2009: GlomeVec library released<br />
* Jan 20, 2010: GlomeTrace library released<br />
* Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile * failure<br />
* Jan 27, 2014: New version of GlomeVec, GlomeTrace, and GlomeView fixing several bugs that caused CSG to render incorrectly.<br />
* Apr 7, 2014: New version of GloveTrace and GlomeView. Major reorganization of textures, and now objects can be given arbitrary tags that are returned by the ray intersection test.<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
(GlomeVec is also used by a separate project, [[IcoGrid]])<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]] (updated April 2014)<br />
<br />
==Features Lacking in Glome==<br />
<br />
This is a sort of to-do list<br />
<br />
*Antialiasing<br />
*Refraction<br />
*Photon mapping<br />
*PLY file parser<br />
*Rendering to a file<br />
*Support for portals (update: done)<br />
*Ability to tag objects and have ray intersection return the tag to tell you what you hit (update: done)<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
These are some successful open-source ray tracers. They're worth looking at for anyone interested in writing their own.<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
==Links==<br />
<br />
* [https://github.com/jimsnow/glome Glome repositories on github]<br />
* [http://hackage.haskell.org/package/GlomeView Glome Viewer on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]<br />
* [http://tog.acm.org/resources/RTNews/html/ Ray Tracing News]<br />
* [http://tog.acm.org/resources/SPD/ The Standard Procedural Database]<br />
* [http://www-graphics.stanford.edu/data/3Dscanrep/ Stanford Scanning Repository]<br />
* [http://ompf2.com resurrected OMPF forum]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=57811Glome tutorial2014-04-08T01:19:05Z<p>Jsnow: </p>
<hr />
<div>==Notes==<br />
This tutorial is written against a very old version of Glome. Some things have changed, including a transition to a type-class system for defining solids. Most of this content is still relevant, though.<br />
<br />
==Installing Glome==<br />
<br />
First, you need to install the software. The GlomeTrace library has few dependencies, but GlomeView requires SDL. (Earlier versions required OpenGL.)<br />
<br />
You should be able to install GlomeTrace with "cabal install GlomeTrace". (It may be a good idea to run "cabal update" first, if you haven't done that recently, to make sure you get the latest package.)<br />
<br />
You should be able to "cabal install GlomeView" as well, but to use Glome for anything other than viewing the default test scene, you may want to download the source tar file manually.<br />
<br />
To build GlomeView manually, you'll need to run the commands:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/Glome/Glome". (To use more than one core, you can pass in "+RTS -N4" for instance if you want to run on 4 cores.)<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://jsnow.bootlegether.net me] know or file a bug on the github page.<br />
<br />
==Command line options, key commands==<br />
<br />
(Note: None of these options are currently implemented in GlomeView. We may re-enable this at some point.)<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
Once an image is rendered, typing "q" with the rendering window in focus will close Glome. Typing "s" will print a dump of the internal representation of the scene. (Not very useful at this stage, perhaps, but it's a useful debugging tool that might come in handy later.)<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] or some other modeler and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of a rendering library than a standalone rendering application.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A simple scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
type Tag = String<br />
type T = Texture Tag M<br />
type M = Material Tag<br />
type SI = SolidItem Tag M<br />
type Scene = (SI, [Light], Camera, Shader Tag M [Light] [(Color, Vec)])<br />
<br />
lights = [ light (Vec (-100) 70 (140)) (cscale (Color 1 0.8 0.8) 7000)<br />
, light (Vec (-3) 5 8) (cscale (Color 1.5 2 2) 10)<br />
]<br />
<br />
cam = camera (vec (-2) (4.3) (15)) (vec 0 2 0) (vec 0 1 0) 45<br />
<br />
geometry = sphere (vec 0 0 0) 1<br />
<br />
scn :: IO Scene<br />
scn = return (geometry, lights, cam, materialShader)<br />
</haskell><br />
<br />
GlomeView executes the function "scn", which returns the scene to be rendered. (It runs in the IO monad so that you can open files or whatnot if that's necessary to gather the data you need to render the scene. Most of the time we don't need that functionality.)<br />
<br />
In order to construct a valid scene we need to put something into all the required fields of the scene type.<br />
<br />
The first field takes a SolidItem of some kind. (A SolidItem is a type that represents any concrete type that is a member of the Solid typeclass defined in Solid.hs.) A SolidItem could be any of the basic primitive types that Glome natively supports, or they can be user-defined. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single triangle or box or sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a location and a color (in RGB). Light intensity can be controlled by scaling up the color's rgb values (they don't need to lie between 0 and 1).<br />
<br />
Location is specified by a vector, which can be constructed by calling "vec" with x, y, and z arguments.<br />
<br />
<br />
(See Vec.hs from GlomeVec and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
One note on how Haskell treats numbers: unlike C, a decimal point isn't necessary to distinguish Float literals from Ints. However, it is usually necessary to enclose negative numbers in parentheses.<br />
<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
The shader field defines how surfaces are lit. As of GlomeTrace 0.3, it is now possible to define your own shader, but in most cases the standard shader (materialShader defined in Shader.hs) is probably what you want.<br />
<br />
The shader also defines what happens when a ray misses all objects in the scene. The standard shader just returns black in that case with the alpha channel set to zero (full transparency). GlomeView currently ignores the alpha channel, but at some point we may add support for png output.<br />
<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is a member of the typeclass "Solid". There are quite a few instances: Triangle, TriangleNorm, Disc, Cylinder, Cone, Plane, Box, Group, Intersection, Bound, Difference, Intersection, Bih, Instance, Tex, Tex, and Void.<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard Sphere constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> SolidItem t m<br />
sphere c r =<br />
SolidItem (Sphere c r (1.0/r))<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a flat triangle that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> SolidItem t m<br />
plane orig norm_ = SolidItem $ Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> SolidItem t m<br />
cylinder_z r h1 h2 = SolidItem (Cylinder r h1 h2)<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> SolidItem t m<br />
cone_z r h1 h2 height = SolidItem (Cone r h1 h2 height)<br />
<br />
-- | Construct a general cylinder from p1 to p2 with radius r.<br />
cylinder :: Vec -> Vec -> Flt -> SolidItem t m<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in transform (cylinder_z r 0 len)<br />
[ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]<br />
<br />
-- | Construct a cone from p1 to p2. R1 and r2 are the radii at each<br />
-- end. A cone need not come to a point at either end.<br />
cone :: Vec -> Flt -> Vec -> Flt -> SolidItem t m<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
transform (cone_z r1 0 len height)<br />
[ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]<br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
These constructors make use of transformations to orient them properly in space, which is something we'll get to later on.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> SolidItem t m<br />
box (Vec x1 y1 z1) (Vec x2 y2 z2) =<br />
SolidItem (Box (Bbox (Vec (fmin x1 x2) (fmin y1 y2) (fmin z1 z2))<br />
(Vec (fmax x1 x2) (fmax y1 y2) (fmax z1 z2))))<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group is just an alias for [SolidItem]. It has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [SolidItem t m] -> SolidItem t m<br />
group [] = SolidItem Void<br />
group (sld:[]) = sld<br />
group slds = SolidItem (flatten_group slds)<br />
<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Void, which is a degenerate object that has no appearance.<br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better if your group contains more than a couple items. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. (We'll get into why later on.)<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections (they're more properly described as half-spaces; everything on one side is outside, and everything on the other side is inside); we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
This effectively creates an instance of a in a new location. This an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want. (This is consistent with how most graphics APIs work -- if you're familiar with, say, OpenGL or POV-Ray, this should be pretty familiar.)<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which GlomeView currently does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it may also render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex (SolidItem t m) (Texture tag mat)</haskell><br />
<br />
The SolidItem is the thing that we want to apply the texture to, and the Texture is the texture itself.<br />
<br />
A texture, in turn, is a function that takes a ray and it's corresponding ray intersection and returns a material. The shader's job is to take the material and ray intersection data and figure out what color to render it.<br />
<br />
It's possible to define your own material types, but most of the time you'll want to use the one provided by Shader.hs:<br />
<br />
<haskell><br />
data Material t =<br />
Surface Color Flt Flt Flt Flt Flt Bool | -- color, alpha, ambient, diffuse, specular, shine, dielectric<br />
Reflect Flt | -- amount<br />
Refract Flt Flt | -- amount, ior<br />
Warp (SolidItem t (Material t))<br />
(SolidItem t (Material t))<br />
[Light]<br />
(Ray -> Rayint t (Material t) -> Ray) | -- frame, scene, ctx, xfm<br />
AdditiveLayers [Material t] |<br />
Blend (Material t) (Material t) Flt<br />
</haskell><br />
<br />
A "Surface" defines basic surface properties like ambient, diffuse, and specular illumination. "Reflect" is used for reflective surfaces (like mirrors), and Refract is used for refraction (not currently implemented). AdditiveLayers is used to layer multiple Materials and add the colors together, while Blend allows you to create a weighted mixture of two layers.<br />
<br />
Often, we might want to texture a single object by itself:<br />
<br />
<haskell><br />
m_matte :: Color -> M<br />
m_matte c = (Surface c 1 0.03 1 0 0 False)<br />
<br />
matte :: Color -> T<br />
matte c = <br />
(\_ _ -> m_matte c)<br />
<br />
Tex (sphere (Vec 0 0 0) 1) (matte (Color 1 0 0))<br />
<br />
</haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once by wrapping a "tex" around a whole group (or bih) of objects.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (matte (Color 1 0 0))) (matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture. An exception is if the inner texture returns an alpha value less than one (i.e. it's at least partially transparent), in which case the outer texture is applied underneath the inner texture.<br />
<br />
Textures can have very complicated behavior, since they can act conditionally on all the data the ray tracer returns.<br />
<br />
<haskell><br />
type Texture tag mat = Ray -> Rayint tag mat -> mat<br />
<br />
data Rayint tag mat = RayHit {<br />
ridepth' :: !Flt,<br />
ripos :: !Vec,<br />
rinorm :: !Vec,<br />
riray :: !Ray,<br />
riuvw :: !Vec,<br />
ritex :: [Texture tag mat],<br />
ritag :: [tag]<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "ripos" which is the XYZ coordinates of the location of the ray intersection. Most textures won't need to access the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object.<br />
<br />
When creating textures, it may be necessary to add some random variation to make it more interesting to look at. For this, "perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
One caveat about using Tex is that the Bih constructor treats a Tex (and all the objects it contain) as a monolithic object. So, if a Tex contains many objects (or even just a few), you might want to use "bih" instead of "group" on the children, even if you're running "bih" at the root of the scene.<br />
<br />
Along with Tex, Glome provides a Tag type which behaves similarly to Tex except that the tag type is left up to the application. Tags aren't directly useful for rendering, but they can be very handy for other computational geometry tasks if you need to have some way to know exactly what a ray hit.<br />
<br />
==A guide to the Glome source code==<br />
<br />
Editing scenes directly in Haskell makes it possible to use the pre-existing ray tracing infrastructure to help us create our scene. For instance, the "trace" function can be used to help us place one object on another; a plant growing on a non-flat surface, for instance. Also, we may want our scene to include geometric primitives of a type that Glome does not yet support, and so we might want to add our own.<br />
<br />
This section of the tutorial is for those who are interested in understanding not just how to use Glome, but how it works and how it can be extended.<br />
<br />
===File Reference===<br />
<br />
First, an overview of the source code files. Glome is currently split amongst many source files and between GlomeTrace and GlomeVec is about 3500 lines of code. Some of the interesting files are:<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting definitions are: definitions of the Solid typeclass and SolidItem existential type, definitions of the Rayint type, and a few of the base primitives (like Group, Void, and Instance).<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen.<br />
<br />
;Shader.hs<br />
:This provides an illumination model. (You can implement your own, but most of the time you'll want to stick with materialShader.)<br />
<br />
;Bih.hs<br />
:The BIH acceleration structure.<br />
<br />
;Sphere.hs, Triangle.hs, Box.hs, Plane,hs, Cone.hs, etc..<br />
:Basic geometric primitives.<br />
<br />
;Glome.hs<br />
:The main loop of the program, part of the GlomeView package. All SDL-related code resides in this module. Also included is a get_color function that accepts screen coordinates and a scene, and computes the ray for that screen coordinate, traces the ray, and returns the color.<br />
<br />
;TestScene.hs<br />
:This is what gets rendered if no input file is specified. This is meant to be edited by users. Provided in GlomeView package.<br />
<br />
;SolidTexture.hs<br />
:Perlin noise and other related texture functions. Part of GlomeVec package.<br />
<br />
;Spd.hs<br />
:NFF file parser for SPD scenes.<br />
<br />
===Tracing Rays===<br />
<br />
<br />
Glome's Solid typeclass defines a ray-intersection function "rayint" that pattern matches against a primitive and returns an appropriate Rayint.<br />
<br />
For instance, let's look at the "disc" case, as it is short and simple:<br />
<br />
<haskell><br />
instance Solid (Disc t m) t m where<br />
rayint = rayint_disc<br />
shadow = shadow_disc<br />
inside (Disc _ _ _) _ = False<br />
bound = bound_disc<br />
<br />
rayint_disc :: Disc tag mat -> Ray -> Flt -> [Texture tag mat] -> [tag] -> Rayint tag mat<br />
rayint_disc (Disc point norm radius_sqr) r@(Ray orig dir) d t tags =<br />
let dist = plane_int_dist r point norm <br />
in if dist < 0 || dist > d <br />
then RayMiss<br />
else let pos = vscaleadd orig dir dist<br />
offset = vsub pos point<br />
in <br />
if (vdot offset offset) > radius_sqr<br />
then RayMiss<br />
else RayHit dist pos norm r vzero t tags<br />
</haskell><br />
<br />
"rayint" takes five arguments: the Solid to be intersected, the Ray to intersect with it, a maximum trace depth, a Texture stack, and a tag stack. "rayint" is expected to return a RayHit value if a ray intersection exists that's closer than the maximum depth. If there is more than one hit, rayint should return the closest one, but never an intersection that is behind the Ray's origin.<br />
<br />
Rayint_disc extracts the Ray's origin and direction from "r", and then uses a function called "plane_int_dist" defined in Vec.hs. This returns the distance to the plane defined by a point on the plane and it's normal, and intersected by ray r.<br />
<br />
Then Glome checks if the distance is less than zero or more than the maximum allowed, and if so returns RayMiss.<br />
<br />
Otherwise, Glome computes the hit location from the Ray and distance to the plane. "vscaleadd" is another function from Vec.hs that takes one vector, and then adds a second vector after scaling the second vector by some scalar. By taking the Ray's (normalized) direction vector scaled by the distance to the plane and adding it to the Ray's origin, we get the hit location. (This technique is used in many of the ray-intersection tests.)<br />
<br />
Once we know the location on the disc's plane where our ray hit, we need to know if it is within the radius of the disc. For that, we compute an offset vector from the center of the disc ("point") to the hit location ("pos"). Then we want to check if this offset vector is less than the radius of the disc. Or, in other words:<br />
<br />
sqrt (offset.x^2 + offset.y^2 + offset.z^2) < r<br />
<br />
We can square both sides to get rid of the square root, and then observe that squaring the components of a vector is the same as taking the dot product of that vector with itself:<br />
<br />
vdot offset offset < r^2<br />
<br />
Also, Glome doesn't store the radius with a disc but rather it's radius squared (to avoid a multiply, since we don't often need to know the disc's actual radius), and that explains the last if statement.<br />
<br />
The RayHit constructor needs a little explanation, though. What are all those fields for?<br />
<br />
First, there's the distance to the nearest hit and the position. (You might notice some redundancy here, since the calling function could infer the distance to the nearest hit from the ray and the hit position, or the the hit position from the distance and the ray. We return both to save the trouble of recomputing values we've already determined.) "norm" is the vector perpendicular to the surface of the disc, used in lighting calculations. For discs, this is easy: the normal is stored as part of the disc's definition. For other objects (like Spheres or Cones), we might have to compute a normal. <br />
<br />
The texture passed in as an argument to "rayint" is simply returned in the returned Rayint record. All of the ray intersection cases behave this way except Tex, which pushes a new texture onto the stack.<br />
<br />
As for the other basic primitives like Triangle, Sphere, Cylinder, Cone, Plane, and Box, Glome uses fairly typical intersection tests that can be found in graphics textbooks (such as [http://pbrt.org/ Physically Based Rendering] and Graphics Gems volume one). <br />
<br />
A "Void" object is a special case: its ray intersector simply returns "RayMiss" regardless of input. The existence of "Void" is somewhat redundant, since it is equivalent to "Group []".<br />
<br />
The composite primitives (Group, Difference, Intersection, Tex, Bih, Instance, and Bound) are more interesting, as their ray-intersection tests are defined recursively. This recursion is what allows us to treat a complex object made up of many sub-objects the same as we would treat a simple base primitive like a Sphere, and in fact Glome makes no distinction whatsoever between base primitives and composite primitives.<br />
<br />
We'll look at Group as our composite ray-intersection test example:<br />
<br />
<haskell><br />
instance Solid [SolidItem t m] t m where<br />
rayint xs r d t tags = foldl' nearest RayMiss (map (\s -> rayint s r d t tags) xs)<br />
...<br />
</haskell><br />
<br />
Here, we traverse the list calling "rayint" for each primitive and returning the nearest hit. ("nearest" is defined in Solid.hs, and returns the nearest of two ray intersections, or RayMiss if they both miss.)<br />
<br />
One important thing to keep in mind about Group is that intersecting with a large group is very inefficient. That's why we have Bih. However, even Bih uses Groups as leaf nodes, so Groups are still important in some cases.<br />
<br />
There is a second ray intersection function called "shadow" that only takes the primitive, a ray, and a maximum distance, and returns True if the ray hits the object and False otherwise.<br />
<br />
"shadow" is used for shadow-ray occlusion tests. In order to test whether a particular point is lit by a particular light, a shadow ray is traced from the ray intersection point to the light. If there is something in the way, that light is in shadow and it does not contribute to the illumination at that point.<br />
<br />
For most scenes with more than one light, more shadow rays are traced than regular rays. Therefore, we want the shadow ray intersection tests to be as fast as possible.<br />
<br />
<haskell><br />
instance Solid [SolidItem t m] t m where<br />
shadow xs r d = foldl' (||) False (map (\s -> shadow s r d) xs)<br />
</haskell><br />
<br />
This is faster in some cases than the full ray intersection, because we can stop as soon as one of the results comes back as True.<br />
<br />
Primitives are not required to implement a shadow test. Glome defines a reasonable default case:<br />
<br />
<haskell><br />
shadow s !r !d =<br />
case (rayint s r d undefined []) of<br />
RayHit _ _ _ _ _ _ _ -> True<br />
RayMiss -> False<br />
</haskell><br />
<br />
For base primitives, the performance penalty of using the full ray intersection test instead of a shadow test may be insignificant. However, composite primitives should always define a shadow test. Consider, for instance, if Group did not implement a shadow test: all it's children would be tested with "rayint" rather than "shadow", and if any of those objects have sub-objects, they will be tested with "rayint" as well! A single primitive type high in the tree that doesn't support "shadow" will force its entire subtree to be evaluated with "rayint".<br />
<br />
There is one case where "rayint" actually calls "shadow", rather than the other way around: Bound uses a shadow test to determine if the ray hits the bounding object or not.<br />
<br />
==Advanced Topics, and things that don't quite work yet==<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=57809Glome tutorial2014-04-07T20:54:23Z<p>Jsnow: </p>
<hr />
<div>==Notes==<br />
This tutorial is written against a very old version of Glome. Some things have changed, including a transition to a type-class system for defining solids. Most of this content is still relevant, though.<br />
<br />
==Installing Glome==<br />
<br />
First, you need to install the software. The GlomeTrace library has few dependencies, but GlomeView requires SDL. (Earlier versions required OpenGL.)<br />
<br />
You should be able to install GlomeTrace with "cabal install GlomeTrace". (It may be a good idea to run "cabal update" first, if you haven't done that recently, to make sure you get the latest package.)<br />
<br />
You should be able to "cabal install GlomeView" as well, but to use Glome for anything other than viewing the default test scene, you may want to download the source tar file manually.<br />
<br />
To build GlomeView manually, you'll need to run the commands:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/Glome/Glome". (To use more than one core, you can pass in "+RTS -N4" for instance if you want to run on 4 cores.)<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://jsnow.bootlegether.net me] know or file a bug on the github page.<br />
<br />
==Command line options, key commands==<br />
<br />
(Note: None of these options are currently implemented in GlomeView. We may re-enable this at some point.)<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
Once an image is rendered, typing "q" with the rendering window in focus will close Glome. Typing "s" will print a dump of the internal representation of the scene. (Not very useful at this stage, perhaps, but it's a useful debugging tool that might come in handy later.)<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] or some other modeler and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of a rendering library than a standalone rendering application.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A simple scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
type Tag = String<br />
type T = Texture Tag M<br />
type M = Material Tag<br />
type SI = SolidItem Tag M<br />
type Scene = (SI, [Light], Camera, Shader Tag M [Light] [(Color, Vec)])<br />
<br />
lights = [ light (Vec (-100) 70 (140)) (cscale (Color 1 0.8 0.8) 7000)<br />
, light (Vec (-3) 5 8) (cscale (Color 1.5 2 2) 10)<br />
]<br />
<br />
cam = camera (vec (-2) (4.3) (15)) (vec 0 2 0) (vec 0 1 0) 45<br />
<br />
geometry = sphere (vec 0 0 0) 1<br />
<br />
scn :: IO Scene<br />
scn = return (geometry, lights, cam, materialShader)<br />
</haskell><br />
<br />
GlomeView executes the function "scn", which returns the scene to be rendered. (It runs in the IO monad so that you can open files or whatnot if that's necessary to gather the data you need to render the scene. Most of the time we don't need that functionality.)<br />
<br />
In order to construct a valid scene we need to put something into all the required fields of the scene type.<br />
<br />
The first field takes a SolidItem of some kind. (A SolidItem is a type that represents any concrete type that is a member of the Solid typeclass defined in Solid.hs.) A SolidItem could be any of the basic primitive types that Glome natively supports, or they can be user-defined. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single triangle or box or sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a location and a color (in RGB). Light intensity can be controlled by scaling up the color's rgb values (they don't need to lie between 0 and 1).<br />
<br />
Location is specified by a vector, which can be constructed by calling "vec" with x, y, and z arguments.<br />
<br />
<br />
(See Vec.hs from GlomeVec and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
One note on how Haskell treats numbers: unlike C, a decimal point isn't necessary to distinguish Float literals from Ints. However, it is usually necessary to enclose negative numbers in parentheses.<br />
<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
The shader argument defines how surfaces are lit. As of GlomeTrace 0.3, it is now possible to define your own shader, but in most cases the standard shader (materialShader defined in Shader.hs) is probably what you want.<br />
<br />
The shader also defines what happens when a ray misses all objects in the scene. The standard shader just returns black in that case with the alpha channel set to zero (full transparency). GlomeView currently ignores the alpha channel, but at some point we may add support for png output.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is a member of the typeclass "Solid". There are quite a few instances: Triangle, TriangleNorm, Disc, Cylinder, Cone, Plane, Box, Group, Intersection, Bound, Difference, Intersection, Bih, Instance, Tex, Tex, and Void.<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard Sphere constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> SolidItem t m<br />
sphere c r =<br />
SolidItem (Sphere c r (1.0/r))<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a flat triangle that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> SolidItem t m<br />
plane orig norm_ = SolidItem $ Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> SolidItem t m<br />
cylinder_z r h1 h2 = SolidItem (Cylinder r h1 h2)<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> SolidItem t m<br />
cone_z r h1 h2 height = SolidItem (Cone r h1 h2 height)<br />
<br />
-- | Construct a general cylinder from p1 to p2 with radius r.<br />
cylinder :: Vec -> Vec -> Flt -> SolidItem t m<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in transform (cylinder_z r 0 len)<br />
[ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]<br />
<br />
-- | Construct a cone from p1 to p2. R1 and r2 are the radii at each<br />
-- end. A cone need not come to a point at either end.<br />
cone :: Vec -> Flt -> Vec -> Flt -> SolidItem t m<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
transform (cone_z r1 0 len height)<br />
[ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]<br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
These constructors make use of transformations to orient them properly in space, which is something we'll get to later on.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> SolidItem t m<br />
box (Vec x1 y1 z1) (Vec x2 y2 z2) =<br />
SolidItem (Box (Bbox (Vec (fmin x1 x2) (fmin y1 y2) (fmin z1 z2))<br />
(Vec (fmax x1 x2) (fmax y1 y2) (fmax z1 z2))))<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group is just an alias for [SolidItem]. It has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [SolidItem t m] -> SolidItem t m<br />
group [] = SolidItem Void<br />
group (sld:[]) = sld<br />
group slds = SolidItem (flatten_group slds)<br />
<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Void, which is a degenerate object that has no appearance.<br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better if your group contains more than a couple items. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. (We'll get into why later on.)<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections (they're more properly described as half-spaces; everything on one side is outside, and everything on the other side is inside); we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
(Todo: textures are much more general is GlomeTrace 0.3, so most of this out of date.)<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
One caveat about using Tex is that the Bih constructor treats a Tex (and all the objects it contain) as a monolithic object. So, if a Tex contains many objects (or even just a few), you might want to use "bih" instead of "group" on the children, even if you're running "bih" at the root of the scene.<br />
<br />
==A guide to the Glome source code==<br />
<br />
Editing scenes directly in Haskell makes it possible to use the pre-existing ray tracing infrastructure to help us create our scene. For instance, the "trace" function can be used to help us place one object on another; a plant growing on a non-flat surface, for instance. Also, we may want our scene to include geometric primitives of a type that Glome does not yet support, and so we might want to add our own.<br />
<br />
This section of the tutorial is for those who are interested in understanding not just how to use Glome, but how it works and how it can be extended.<br />
<br />
===File Reference===<br />
<br />
First, an overview of the source code files. Glome is currently split amongst eight source files and is about 2500 lines of code. These are the files:<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting code is: definitions of basic data types, ray-intersection, shadow, and inside/outside tests for all the supported primitives, and constructors for the various primitives.<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen. It also includes the shading algorithms.<br />
<br />
;Glome.hs<br />
:The main loop of the program. All OpenGL-related code resides in this module. Also included is a get_color function that accepts screen coordinates and a scene, and computes the ray for that screen coordinate, traces the ray, and returns the color.<br />
<br />
;TestScene.hs<br />
:This is what gets rendered if no input file is specified. This is meant to be edited by users.<br />
<br />
;SolidTexture.hs<br />
:Perlin noise and other related texture functions.<br />
<br />
;Spd.hs<br />
:NFF file parser for SPD scenes.<br />
<br />
===Tracing Rays===<br />
<br />
Solid.hs defines a ray intersection thus:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
And these are our basic primitives:<br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec,<br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
<br />
Glome defines a ray-intersection function "rayint" that pattern matches against all of these primitives and returns an appropriate Rayint.<br />
<br />
For instance, let's look at the "disc" case, as it is quite simple:<br />
<br />
<haskell><br />
rayint :: Solid -> Ray -> Flt -> Texture -> Rayint<br />
rayint (Disc point norm radius_sqr) r d t =<br />
let (Ray orig dir) = r<br />
dist = plane_int_dist r point norm <br />
in if dist < 0 || dist > d <br />
then RayMiss<br />
else let pos = vscaleadd orig dir dist<br />
offset = vsub pos point<br />
in <br />
if (vdot offset offset) > radius_sqr<br />
then RayMiss<br />
else RayHit dist pos norm t<br />
</haskell><br />
<br />
"rayint" takes four arguments: the Solid to be intersected, the Ray to intersect with it, a Flt (shorthand for Float or Double, depending on how Vec.hs is configured), and a Texture. "rayint" is expected to return a RayHit value if a ray intersection exists within the distance given by "d". If there is more than one hit, rayint should return the closest one, but never an intersection that is behind the Ray's origin. Discs are flat, though, so we can never intersect one twice with a single ray.<br />
<br />
Glome extracts the Ray's origin and direction from "r", and then uses a function called "plane_int_dist" defined in Vec.hs. This returns the distance to the plane defined by a point on the plane and it's normal, and intersected by ray r.<br />
<br />
Then Glome checks if the distance is less than zero or more than the maximum allowed, and if so returns RayMiss.<br />
<br />
Otherwise, Glome computes the hit location from the Ray and distance to the plane. "vscaleadd" is another function from Vec.hs that takes one vector, and then adds a second vector after scaling the second vector by some scalar. By taking the Ray's (normalized) direction vector scaled by the distance to the plane and adding it to the Ray's origin, we get the hit location. (This technique is used in many of the ray-intersection tests.)<br />
<br />
Once we know the location on the disc's plane where our ray hit, we need to know if it is within the radius of the disc. For that, we compute an offset vector from the center of the disc ("point") to the hit location ("pos"). Then we want to check if this offset vector is less than the radius of the disc. Or, in other words:<br />
<br />
sqrt (offset.x^2 + offset.y^2 + offset.z^2) < r<br />
<br />
We can square both sides to get rid of the square route, and then observe that squaring the components of a vector is the same as taking the dot product of that vector with itself:<br />
<br />
vdot offset offset < r^2<br />
<br />
Also, Glome doesn't store the radius with a disc but rather it's radius squared (to avoid a multiply, since we don't often need to know the disc's actual radius), and that explains the last if statement.<br />
<br />
The RayHit constructor needs a little explanation, though. What are all those fields for?<br />
<br />
First, there's the distance to the nearest hit and the position. (You might notice some redundancy here, since the calling function could infer the distance to the nearest hit from the ray and the hit position, or the the hit position from the distance and the ray. We return both to save the trouble of recomputing values we've already determined.) "norm" is the vector perpendicular to the surface of the disc, used in lighting calculations. For discs, this is easy: the normal is stored as part of the disc's definition. For other objects (like Spheres or Cones), we might have to compute a normal. <br />
<br />
The texture passed in as an argument to "rayint" is simply returned in the returned Rayint record. All of the ray intersection cases behave this way except Tex, which overrides the texture with its own.<br />
<br />
As for the other basic primitives like Triangle, Sphere, Cylinder, Cone, Plane, and Box, Glome uses standard intersection tests that can be found in graphics textbooks (such as [http://pbrt.org/ Physically Based Rendering] and Graphics Gems volume one). <br />
<br />
A "Nothing" object is a special case: its ray intersector simply returns "RayMiss" regardless of input. The existence of "Nothing" is somewhat redundant, since it is equivalent to "Group []".<br />
<br />
The composite primitives (Group, Difference, Intersection, Tex, Bih, Instance, and Bound) are more interesting, as their ray-intersection tests are defined recursively. This recursion is what allows us to treat a complex object made up of many sub-objects the same as we would treat a simple base primitive like a Sphere, and in fact Glome makes no distinction whatsoever between base primitives and composite primitives.<br />
<br />
We'll look at Group as our composite ray-intersection test example:<br />
<br />
<haskell><br />
rayint (Group xs) r d t =<br />
let rig [] = RayMiss<br />
rig (x:xs) = nearest (rayint x r d t) (rig xs)<br />
in rig xs<br />
</haskell><br />
<br />
Here, "rayint" defines a simple function to traverse the list calling "rayint" for each primitive and returning the nearest hit. ("nearest" is defined in Solid.hs, and returns the nearest of two ray intersections, or RayMiss if they both miss.)<br />
<br />
This could be defined more succinctly with a fold:<br />
<br />
<haskell><br />
rayint (Group lst) r d t = foldl nearest RayMiss (map (\x -> rayint x r d t) lst)<br />
</haskell><br />
<br />
...and we only use the more verbose style for efficiency. (I'm not sure if I actually benchmarked if it was faster, so this might be worth checking.)<br />
<br />
One important thing to note about Group is that intersecting with a large group is very inefficient. That's why we have Bih. However, even Bih uses Groups as leaf nodes, so Groups are still important.<br />
<br />
There is a second ray intersection function called "shadow" that has a simpler type signature than "rayint":<br />
<br />
<haskell><br />
shadow :: Solid -> Ray -> Flt -> Bool<br />
</haskell><br />
<br />
"shadow" is used for shadow-ray occlusion tests. In order to test whether a particular point is lit by a particular light, a shadow ray is traced from the ray intersection point to the light. If there is something in the way, that light is in shadow and it does not contribute to the illumination at that point.<br />
<br />
For most scenes with more than one light, more shadow rays are traced than regular rays. Therefore, we want the shadow ray intersection tests to be as fast as possible. "shadow" does not require a texture, and it returns True if the ray hits the object or False if it misses.<br />
<br />
Let's look at the shadow test for Group:<br />
<br />
<haskell><br />
shadow (Group xs) r d =<br />
let sg [] = False<br />
sg (x:xs) = (shadow x r d) || (sg xs)<br />
in sg xs<br />
</haskell><br />
<br />
Note that "shadow" stops as soon as one of the shadow tests returns True.<br />
<br />
Primitives are not required to implement a shadow test. Glome defines a reasonable default case:<br />
<br />
<haskell><br />
shadow s r d =<br />
case (rayint s r d t_white) of<br />
RayHit _ _ _ _ -> True<br />
RayMiss -> False<br />
</haskell><br />
<br />
For base primitives, the performance penalty of using the full ray intersection test instead of a shadow test may be insignificant. However, composite primitives should always define a shadow test. Consider, for instance, if Group did not implement a shadow test: all it's children would be tested with "rayint" rather than "shadow", and if any of those objects have sub-objects, they will be tested with "rayint" as well! A single primitive type high in the tree that doesn't support "shadow" will force its entire subtree to be evaluated with "rayint".<br />
<br />
There is one case where "rayint" actually calls "shadow", rather than the other way around: Bound uses a shadow test to determine if the ray hits the bounding object or not.<br />
<br />
==Advanced Topics, and things that don't quite work yet==<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=57808Glome2014-04-07T19:50:32Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome consists of the vector library GlomeVec, the ray tracing library GlomeTrace and an application called GlomeView.<br />
<br />
GlomeView renders into an SDL window, and thus requires SDL. The ray tracing engine itself, though, has few dependencies.<br />
<br />
(There is a deprecated package available on Hackage called glome-hs, which renders into an OpenGL window. It probably won't work with recent versions of GlomeTrace.)<br />
<br />
If everything works, you should see an image something like this when you run Glome the first time:<br />
<br />
(Testscene for early versions of Glome)<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(Testscene for 0.3)<br />
[[Image:GlomeView-0.3.png]]<br />
<br />
(Use +RTS -N4 or whatever if you want to use more than one core.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs included in GlomeView, then re-compile.<br />
<br />
==News==<br />
* Oct 25, 2009: GlomeVec library released<br />
* Jan 20, 2010: GlomeTrace library released<br />
* Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile * failure<br />
* Jan 27, 2014: New version of GlomeVec, GlomeTrace, and GlomeView fixing several bugs that caused CSG to render incorrectly.<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Features Lacking in Glome==<br />
<br />
This is a sort of to-do list<br />
<br />
*Antialiasing<br />
*Refraction<br />
*Photon mapping<br />
*PLY file parser<br />
*Rendering to a file<br />
*Support for portals<br />
*Ability to tag objects and have ray intersection return the tag to tell you what you hit.<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
These are some successful open-source ray tracers. They're worth looking at for anyone interested in writing their own.<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
==Links==<br />
<br />
* [https://github.com/jimsnow/glome Glome repositories on github]<br />
* [http://hackage.haskell.org/package/GlomeView Glome Viewer on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]<br />
* [http://tog.acm.org/resources/RTNews/html/ Ray Tracing News]<br />
* [http://tog.acm.org/resources/SPD/ The Standard Procedural Database]<br />
* [http://www-graphics.stanford.edu/data/3Dscanrep/ Stanford Scanning Repository]<br />
* [http://ompf2.com resurrected OMPF forum]</div>Jsnowhttps://wiki.haskell.org/index.php?title=File:GlomeView-0.3.png&diff=57807File:GlomeView-0.3.png2014-04-07T19:48:49Z<p>Jsnow: Screenshot of Glome raytracer test scene.</p>
<hr />
<div>Screenshot of Glome raytracer test scene.</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=57511Glome tutorial2014-01-27T23:09:05Z<p>Jsnow: </p>
<hr />
<div>==Notes==<br />
This tutorial is written against a very old version of Glome. Some things have changed, including a transition to a type-class system for defining solids. Most of this content is still relevant, though.<br />
<br />
==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help. Familiarity with ray tracing is not required either, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options, key commands==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
Once an image is rendered, typing "q" with the rendering window in focus will close Glome. Typing "s" will print a dump of the internal representation of the scene. (Not very useful at this stage, perhaps, but it's a useful debugging tool that might come in handy later.)<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
One caveat about using Tex is that the Bih constructor treats a Tex (and all the objects it contain) as a monolithic object. So, if a Tex contains many objects (or even just a few), you might want to use "bih" instead of "group" on the children, even if you're running "bih" at the root of the scene.<br />
<br />
==A guide to the Glome source code==<br />
<br />
Editing scenes directly in Haskell makes it possible to use the pre-existing ray tracing infrastructure to help us create our scene. For instance, the "trace" function can be used to help us place one object on another; a plant growing on a non-flat surface, for instance. Also, we may want our scene to include geometric primitives of a type that Glome does not yet support, and so we might want to add our own.<br />
<br />
This section of the tutorial is for those who are interested in understanding not just how to use Glome, but how it works and how it can be extended.<br />
<br />
===File Reference===<br />
<br />
First, an overview of the source code files. Glome is currently split amongst eight source files and is about 2500 lines of code. These are the files:<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting code is: definitions of basic data types, ray-intersection, shadow, and inside/outside tests for all the supported primitives, and constructors for the various primitives.<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen. It also includes the shading algorithms.<br />
<br />
;Glome.hs<br />
:The main loop of the program. All OpenGL-related code resides in this module. Also included is a get_color function that accepts screen coordinates and a scene, and computes the ray for that screen coordinate, traces the ray, and returns the color.<br />
<br />
;TestScene.hs<br />
:This is what gets rendered if no input file is specified. This is meant to be edited by users.<br />
<br />
;SolidTexture.hs<br />
:Perlin noise and other related texture functions.<br />
<br />
;Spd.hs<br />
:NFF file parser for SPD scenes.<br />
<br />
===Tracing Rays===<br />
<br />
Solid.hs defines a ray intersection thus:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
And these are our basic primitives:<br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec,<br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
<br />
Glome defines a ray-intersection function "rayint" that pattern matches against all of these primitives and returns an appropriate Rayint.<br />
<br />
For instance, let's look at the "disc" case, as it is quite simple:<br />
<br />
<haskell><br />
rayint :: Solid -> Ray -> Flt -> Texture -> Rayint<br />
rayint (Disc point norm radius_sqr) r d t =<br />
let (Ray orig dir) = r<br />
dist = plane_int_dist r point norm <br />
in if dist < 0 || dist > d <br />
then RayMiss<br />
else let pos = vscaleadd orig dir dist<br />
offset = vsub pos point<br />
in <br />
if (vdot offset offset) > radius_sqr<br />
then RayMiss<br />
else RayHit dist pos norm t<br />
</haskell><br />
<br />
"rayint" takes four arguments: the Solid to be intersected, the Ray to intersect with it, a Flt (shorthand for Float or Double, depending on how Vec.hs is configured), and a Texture. "rayint" is expected to return a RayHit value if a ray intersection exists within the distance given by "d". If there is more than one hit, rayint should return the closest one, but never an intersection that is behind the Ray's origin. Discs are flat, though, so we can never intersect one twice with a single ray.<br />
<br />
Glome extracts the Ray's origin and direction from "r", and then uses a function called "plane_int_dist" defined in Vec.hs. This returns the distance to the plane defined by a point on the plane and it's normal, and intersected by ray r.<br />
<br />
Then Glome checks if the distance is less than zero or more than the maximum allowed, and if so returns RayMiss.<br />
<br />
Otherwise, Glome computes the hit location from the Ray and distance to the plane. "vscaleadd" is another function from Vec.hs that takes one vector, and then adds a second vector after scaling the second vector by some scalar. By taking the Ray's (normalized) direction vector scaled by the distance to the plane and adding it to the Ray's origin, we get the hit location. (This technique is used in many of the ray-intersection tests.)<br />
<br />
Once we know the location on the disc's plane where our ray hit, we need to know if it is within the radius of the disc. For that, we compute an offset vector from the center of the disc ("point") to the hit location ("pos"). Then we want to check if this offset vector is less than the radius of the disc. Or, in other words:<br />
<br />
sqrt (offset.x^2 + offset.y^2 + offset.z^2) < r<br />
<br />
We can square both sides to get rid of the square route, and then observe that squaring the components of a vector is the same as taking the dot product of that vector with itself:<br />
<br />
vdot offset offset < r^2<br />
<br />
Also, Glome doesn't store the radius with a disc but rather it's radius squared (to avoid a multiply, since we don't often need to know the disc's actual radius), and that explains the last if statement.<br />
<br />
The RayHit constructor needs a little explanation, though. What are all those fields for?<br />
<br />
First, there's the distance to the nearest hit and the position. (You might notice some redundancy here, since the calling function could infer the distance to the nearest hit from the ray and the hit position, or the the hit position from the distance and the ray. We return both to save the trouble of recomputing values we've already determined.) "norm" is the vector perpendicular to the surface of the disc, used in lighting calculations. For discs, this is easy: the normal is stored as part of the disc's definition. For other objects (like Spheres or Cones), we might have to compute a normal. <br />
<br />
The texture passed in as an argument to "rayint" is simply returned in the returned Rayint record. All of the ray intersection cases behave this way except Tex, which overrides the texture with its own.<br />
<br />
As for the other basic primitives like Triangle, Sphere, Cylinder, Cone, Plane, and Box, Glome uses standard intersection tests that can be found in graphics textbooks (such as [http://pbrt.org/ Physically Based Rendering] and Graphics Gems volume one). <br />
<br />
A "Nothing" object is a special case: its ray intersector simply returns "RayMiss" regardless of input. The existence of "Nothing" is somewhat redundant, since it is equivalent to "Group []".<br />
<br />
The composite primitives (Group, Difference, Intersection, Tex, Bih, Instance, and Bound) are more interesting, as their ray-intersection tests are defined recursively. This recursion is what allows us to treat a complex object made up of many sub-objects the same as we would treat a simple base primitive like a Sphere, and in fact Glome makes no distinction whatsoever between base primitives and composite primitives.<br />
<br />
We'll look at Group as our composite ray-intersection test example:<br />
<br />
<haskell><br />
rayint (Group xs) r d t =<br />
let rig [] = RayMiss<br />
rig (x:xs) = nearest (rayint x r d t) (rig xs)<br />
in rig xs<br />
</haskell><br />
<br />
Here, "rayint" defines a simple function to traverse the list calling "rayint" for each primitive and returning the nearest hit. ("nearest" is defined in Solid.hs, and returns the nearest of two ray intersections, or RayMiss if they both miss.)<br />
<br />
This could be defined more succinctly with a fold:<br />
<br />
<haskell><br />
rayint (Group lst) r d t = foldl nearest RayMiss (map (\x -> rayint x r d t) lst)<br />
</haskell><br />
<br />
...and we only use the more verbose style for efficiency. (I'm not sure if I actually benchmarked if it was faster, so this might be worth checking.)<br />
<br />
One important thing to note about Group is that intersecting with a large group is very inefficient. That's why we have Bih. However, even Bih uses Groups as leaf nodes, so Groups are still important.<br />
<br />
There is a second ray intersection function called "shadow" that has a simpler type signature than "rayint":<br />
<br />
<haskell><br />
shadow :: Solid -> Ray -> Flt -> Bool<br />
</haskell><br />
<br />
"shadow" is used for shadow-ray occlusion tests. In order to test whether a particular point is lit by a particular light, a shadow ray is traced from the ray intersection point to the light. If there is something in the way, that light is in shadow and it does not contribute to the illumination at that point.<br />
<br />
For most scenes with more than one light, more shadow rays are traced than regular rays. Therefore, we want the shadow ray intersection tests to be as fast as possible. "shadow" does not require a texture, and it returns True if the ray hits the object or False if it misses.<br />
<br />
Let's look at the shadow test for Group:<br />
<br />
<haskell><br />
shadow (Group xs) r d =<br />
let sg [] = False<br />
sg (x:xs) = (shadow x r d) || (sg xs)<br />
in sg xs<br />
</haskell><br />
<br />
Note that "shadow" stops as soon as one of the shadow tests returns True.<br />
<br />
Primitives are not required to implement a shadow test. Glome defines a reasonable default case:<br />
<br />
<haskell><br />
shadow s r d =<br />
case (rayint s r d t_white) of<br />
RayHit _ _ _ _ -> True<br />
RayMiss -> False<br />
</haskell><br />
<br />
For base primitives, the performance penalty of using the full ray intersection test instead of a shadow test may be insignificant. However, composite primitives should always define a shadow test. Consider, for instance, if Group did not implement a shadow test: all it's children would be tested with "rayint" rather than "shadow", and if any of those objects have sub-objects, they will be tested with "rayint" as well! A single primitive type high in the tree that doesn't support "shadow" will force its entire subtree to be evaluated with "rayint".<br />
<br />
There is one case where "rayint" actually calls "shadow", rather than the other way around: Bound uses a shadow test to determine if the ray hits the bounding object or not.<br />
<br />
==Advanced Topics, and things that don't quite work yet==<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=57510Glome2014-01-27T21:10:35Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome consists of the vector library GlomeVec, the ray tracing library GlomeTrace and an application called GlomeView.<br />
<br />
GlomeView renders into an SDL window, and thus requires SDL. The ray tracing engine itself, though, has few dependencies.<br />
<br />
(There is a deprecated package available on Hackage called glome-hs, which renders into an OpenGL window. It probably won't work with recent versions of GlomeTrace.)<br />
<br />
If everything works, you should see an image something like this when you run Glome the first time:<br />
<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(Use +RTS -N4 or whatever if you want to use more than one core.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs included in GlomeView, then re-compile.<br />
<br />
==News==<br />
* Oct 25, 2009: GlomeVec library released<br />
* Jan 20, 2010: GlomeTrace library released<br />
* Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile * failure<br />
* Jan 27, 2014: New version of GlomeVec, GlomeTrace, and GlomeView fixing several bugs that caused CSG to render incorrectly.<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Features Lacking in Glome==<br />
<br />
This is a sort of to-do list<br />
<br />
*Antialiasing<br />
*Refraction<br />
*Photon mapping<br />
*PLY file parser<br />
*Rendering to a file<br />
*Support for portals<br />
*Ability to tag objects and have ray intersection return the tag to tell you what you hit.<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
These are some successful open-source ray tracers. They're worth looking at for anyone interested in writing their own.<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
==Links==<br />
<br />
* [https://github.com/jimsnow/glome Glome repositories on github]<br />
* [http://hackage.haskell.org/package/GlomeView Glome Viewer on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]<br />
* [http://tog.acm.org/resources/RTNews/html/ Ray Tracing News]<br />
* [http://tog.acm.org/resources/SPD/ The Standard Procedural Database]<br />
* [http://www-graphics.stanford.edu/data/3Dscanrep/ Stanford Scanning Repository]<br />
* [http://ompf2.com resurrected OMPF forum]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=57507Glome2014-01-27T10:21:17Z<p>Jsnow: Update to cover changes in new release</p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome consists of the vector library GlomeVec, the ray tracing library GlomeTrace and an application called GlomeView.<br />
<br />
GlomeView renders into an SDL window, and thus requires SDL. The ray tracing engine itself, though, has few dependencies.<br />
<br />
(There is a deprecated package available on Hackage called glome-hs, which renders into an OpenGL window. It probably won't work with recent versions of GlomeTrace.)<br />
<br />
If everything works, you should see an image something like this when you run Glome the first time:<br />
<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(Use +RTS -N4 or whatever if you want to use more than one core.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs included in GlomeView, then re-compile.<br />
<br />
==News==<br />
* Oct 25, 2009: GlomeVec library released<br />
* Jan 20, 2010: GlomeTrace library released<br />
* Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile * failure<br />
* Jan 27, 2014: New version of GlomeVec, GlomeTrace, and GlomeView fixing several bugs that caused CSG to render incorrectly.<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Features Lacking in Glome==<br />
<br />
This is a sort of to-do list<br />
<br />
*Antialiasing<br />
*Refraction<br />
*Photon mapping<br />
*PLY file parser<br />
*Rendering to a file<br />
*Support for portals<br />
*Ability to tag objects and have ray intersection return the tag to tell you what you hit.<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
These are some successful open-source ray tracers. They're worth looking at for anyone interested in writing their own.<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/GlomeView Glome Viewer on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]<br />
* [http://tog.acm.org/resources/RTNews/html/ Ray Tracing News]<br />
* [http://tog.acm.org/resources/SPD/ The Standard Procedural Database]<br />
* [http://www-graphics.stanford.edu/data/3Dscanrep/ Stanford Scanning Repository]<br />
* [http://ompf2.com resurrected OMPF forum]</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=33247IcoGrid2010-01-24T03:35:58Z<p>Jsnow: </p>
<hr />
<div>IcoGrid (Icosohedron Grid) is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.<br />
<br />
It's easier to show than to describe, so here is a screenshot of an OpenGL application I wrote to render a grid generated by IcoGrid:<br />
<br />
[[Image:IcoGrid-small.png]]<br />
<br />
==Grid Overview==<br />
<br />
The grid is layed out as twenty triangular hexagon grids fitted together to make an icosahedron. The points where file of these triangular grids meet are pentagons. The size of the grid is length of one of the sides of the triangular grids. Thus a grid of size S will have approximately (S^2)*10 cells.<br />
<br />
Most of the library functions require the grid size as the first parameter. Cells are identified by integers from [0..N-1] for a grid with N cells.<br />
<br />
==Library Features==<br />
<br />
Functions are provided to generate a list of all cells, a list of neighbors to a particular cell, a list of "triads" to which a cell belongs, and a list of all triads.<br />
<br />
A "triad" (my own terminology) is a group of 3 cells that meet at a point.<br />
<br />
Another useful function takes a cell ID and returns a vector, which is the center of that cell, as if the whole grid were mapped to a sphere centered at the origin with radius 1.<br />
<br />
(I use GlomeVec for my vector library. One could substitute a different library with some minor modifications to the code.)<br />
<br />
A function which I have not implemented is the reverse function, which takes any arbitrary point and returns the closest cell. I expect the easiest way to implement such a function efficiently is to sort the cells into a bounding volume hierarchy of some sort, then look up the point (after normalizing it to lie on the surface of the grid) in the BVH to get a small list of candidate cells, and then search the candidate cells for the closest one.<br />
<br />
==Limitations==<br />
<br />
The cells aren't quite a uniform size.<br />
<br />
The library saves its data in a lazy array indexed by grid size. Sizes larger than 1024 aren't supported. (A maximum-sized grid would then have about 10 million cells.)<br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/IcoGrid IcoGrid on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec vector library on Hackage]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=33246Glome2010-01-24T03:30:41Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome renders into an OpenGL window, and thus requires HOpenGL, though the libraries GlomeVec and GlomeTrace do not.<br />
<br />
If everything works, you should see this image when you run Glome the first time:<br />
<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(It renders in about 20 seconds on my AMD-64. It does not currently use multiple cores.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs, then re-compile.<br />
<br />
==News==<br />
* Oct 25, 2009: GlomeVec library released<br />
* Jan 20, 2010: GlomeTrace library released<br />
* Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile * failure<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library.<br />
<br />
GlomeVec is yet another vector library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Features Lacking in Glome==<br />
<br />
This is a sort of to-do list<br />
<br />
*Antialiasing<br />
*Refraction<br />
*Photon mapping<br />
*PLY file parser<br />
*Rendering to a file<br />
*Multithreading<br />
*Support for portals<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
These are some successful open-source ray tracers. They're worth looking at for anyone interested in writing their own.<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]<br />
* [http://tog.acm.org/resources/RTNews/html/ Ray Tracing News]<br />
* [http://tog.acm.org/resources/SPD/ The Standard Procedural Database]<br />
* [http://www-graphics.stanford.edu/data/3Dscanrep/ Stanford Scanning Repository]<br />
* [http://ompf.org/forum/ OMPF forum]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=33245Glome2010-01-24T03:27:38Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome renders into an OpenGL window, and thus requires HOpenGL, though the libraries GlomeVec and GlomeTrace do not.<br />
<br />
If everything works, you should see this image when you run Glome the first time:<br />
<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(It renders in about 20 seconds on my AMD-64. It does not currently use multiple cores.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs, then re-compile.<br />
<br />
==News==<br />
* Oct 25, 2009: GlomeVec library released<br />
* Jan 20, 2010: GlomeTrace library released<br />
* Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile * failure<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library.<br />
<br />
GlomeVec is yet another vector library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Features Lacking in Glome==<br />
<br />
This is a sort of to-do list<br />
<br />
*Antialiasing<br />
*Refraction<br />
*Photon mapping<br />
*PLY file parser<br />
*Rendering to a file<br />
*Multithreading<br />
*Support for portals<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
These are some successful open-source ray tracers.<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]<br />
<br />
* [http://tog.acm.org/resources/RTNews/html/ Ray Tracing News]<br />
* [http://tog.acm.org/resources/SPD/ The Standard Procedural Database]<br />
* [http://www-graphics.stanford.edu/data/3Dscanrep/ Stanford Scanning Repository]<br />
* [http://ompf.org/forum/ OMPF forum]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=33242Glome2010-01-23T07:42:56Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome renders into an OpenGL window, and thus requires HOpenGL, though the libraries GlomeVec and GlomeTrace do not.<br />
<br />
If everything works, you should see this image when you run Glome the first time:<br />
<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(It renders in about 20 seconds on my AMD-64. It does not currently use multiple cores.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs, then re-compile.<br />
<br />
==News==<br />
* Oct 25, 2009: GlomeVec library released<br />
* Jan 20, 2010: GlomeTrace library released<br />
* Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile * failure<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library.<br />
<br />
GlomeVec is yet another vector library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Features Lacking in Glome==<br />
<br />
This is a sort of to-do list<br />
<br />
*Antialiasing<br />
*Refraction<br />
*Photon mapping<br />
*PLY file parser<br />
*Rendering to a file<br />
*Multithreading<br />
*Support for portals<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=33241Glome2010-01-23T07:38:37Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome renders into an OpenGL window, and thus requires HOpenGL, though the libraries GlomeVec and GlomeTrace do not.<br />
<br />
If everything works, you should see this image when you run Glome the first time:<br />
<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(It renders in about 20 seconds on my AMD-64. It does not currently use multiple cores.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs, then re-compile.<br />
<br />
==News==<br />
Oct 25, 2009: GlomeVec library released<br />
Jan 20, 2010: GlomeTrace library released<br />
Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile failure<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library.<br />
<br />
GlomeVec is yet another vector library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
* [http://www.povray.org/ POV-Ray] - fairly slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
* [http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
* [http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=33240Glome2010-01-23T07:33:37Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome renders into an OpenGL window, and thus requires HOpenGL, though the libraries GlomeVec and GlomeTrace do not.<br />
<br />
If everything works, you should see this image when you run Glome the first time:<br />
<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(It renders in about 20 seconds on my AMD-64. It does not currently use multiple cores.)<br />
<br />
Glome does not have a scene description language of its own (save for a rather rudimentary NFF parser), so the most convenient way to describe a scene is directly in Haskell. One can edit the file TestScene.hs, then re-compile.<br />
<br />
==News==<br />
Oct 25, 2009: GlomeVec library released<br />
Jan 20, 2010: GlomeTrace library released<br />
Jan 22, 2010: New version of GlomeVec to (hopefully) fix GlomeTrace compile failure<br />
<br />
==GlomeVec==<br />
GlomeVec is the vector library used by Glome. Originally, it was part of Glome, but now it is a separate library.<br />
<br />
GlomeVec is yet another vector library. It isn't implemented in any particularly clever way, but it has many useful routines for graphics built-in, like transformation matrices. <br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
GlomeVec also includes a basic solid texture library. Right now, it has an implementation of perlin noise and a couple of simple textures like stripes.<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. Originally, it was part of Glome, but now it is a stand-alone library.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Other Notable Renderers==<br />
<br />
[http://www.povray.org/ POV-Ray] - slow, high-quality renderer with many features. This is the renderer that has had the most influence on the design of Glome.<br />
[http://igad.nhtv.nl/~bikker/ Arauna] - high performance renderer used for realtime games, faster than Glome by two or three orders of magnitude.<br />
[http://www.pbrt.org/ PBRT] - topic of an excellent book on ray tracing. <br />
<br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=33195Glome2010-01-21T02:39:14Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome renders into an OpenGL window, and thus requires HOpenGL, though the libraries GlomeVec and GlomeTrace do not.<br />
<br />
If everything works, you should see this image when you run Glome the first time:<br />
<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
(It renders in about 20 seconds on my AMD-64. It does not currently use multiple cores.)<br />
<br />
===GlomeVec===<br />
GlomeVec is the vector library used by Glome. On Oct 25, 2009, it was released as a separate library.<br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. It was released as a separate library on Jan 20, 2010.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=33194Glome2010-01-21T02:37:20Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome renders into an OpenGL window, and thus requires HOpenGL, though the libraries GlomeVec and GlomeTrace do not.<br />
<br />
[[Image:Glome-testscene-small.png]]<br />
<br />
===GlomeVec===<br />
GlomeVec is the vector library used by Glome. On Oct 25, 2009, it was released as a separate library.<br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. It was released as a separate library on Jan 20, 2010.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]</div>Jsnowhttps://wiki.haskell.org/index.php?title=File:Glome-testscene-small.png&diff=33193File:Glome-testscene-small.png2010-01-21T02:36:19Z<p>Jsnow: Test render image for glome-hs ray tracer.</p>
<hr />
<div>Test render image for glome-hs ray tracer.</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=33192Glome2010-01-21T02:35:09Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome renders into an OpenGL window, and thus requires HOpenGL, though the libraries GlomeVec and GlomeTrace do not.<br />
<br />
===GlomeVec===<br />
GlomeVec is the vector library used by Glome. On Oct 25, 2009, it was released as a separate library.<br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. It was released as a separate library on Jan 20, 2010.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any other unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=33191Glome2010-01-21T02:33:26Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
Glome renders into an OpenGL window, and thus requires HOpenGL, though the libraries GlomeVec and GlomeTrace do not.<br />
<br />
===GlomeVec===<br />
GlomeVec is the vector library used by Glome. On Oct 25 2009, it was released as a separate library.<br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
==GlomeTrace==<br />
GlomeTrace is a library that provides ray-tracing algorithms and a variety of geometry primitives. It was released as a separate library on Jan 20 2010.<br />
<br />
Supported base primitives include: sphere, triangle, cone, cylinder, disc, plane, and box.<br />
<br />
Composite primitives (those which are containers for other primitives) include groups, csg differenc and intersection, transformed instances, textured objects, and bounding objects.<br />
<br />
In addition, there is an implementation of the BIH acceleration structure, which automatically builds a bounding volume hierarchy around unsorted lists of primitives. This greatly improves the performance for large scenes.<br />
<br />
GlomeTrace depends on GlomeVec, but it doesn't have any unusual dependencies.<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Questions==<br />
<br />
Send any questions to the author: jsnow@cs.pdx.edu<br />
In particular, I'd like to know if the test image is failing to render on you platform, or if it is failing to build.<br />
<br />
==Links==<br />
<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeTrace GlomeTrace on Hackage]<br />
* [http://en.wikipedia.org/wiki/Bounding_interval_hierarchy BIH on Wikipedia]</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=31170IcoGrid2009-10-26T06:48:53Z<p>Jsnow: </p>
<hr />
<div>IcoGrid (Icosohedron Grid) is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.<br />
<br />
It's easier to show than to describe, so here is a screenshot of an OpenGL application I wrote to render a grid generated by IcoGrid:<br />
<br />
[[Image:IcoGrid-small.png]]<br />
<br />
==Grid Overview==<br />
<br />
The grid is layed out as twenty triangular hexagon grids fitted together to make an icosahedron. The points where file of these triangular grids meet are pentagons. The size of the grid is length of one of the sides of the triangular grids. Thus a grid of size S will have approximately (S^2)*10 cells.<br />
<br />
Most of the library functions require the grid size as the first parameter. Cells are identified by integers from [0..N-1] for a grid with N cells.<br />
<br />
==Library Features==<br />
<br />
Functions are provided to generate a list of all cells, a list of neighbors to a particular cell, a list of "triads" to which a cell belongs, and a list of all triads.<br />
<br />
A "triad" (my own terminology) is a group of 3 cells that meet at a point.<br />
<br />
Another useful function takes a cell ID and returns a vector, which is the center of that cell, as if the whole grid were mapped to a sphere centered at the origin with radius 1.<br />
<br />
A function which I have not implemented is the reverse function, which takes any arbitrary point and returns the closest cell. I expect the easiest way to implement such a function efficiently is to sort the cells into a bounding volume hierarchy of some sort, then look up the point (after normalizing it to lie on the surface of the grid) in the BVH to get a small list of candidate cells, and then search the candidate cells for the closest one.<br />
<br />
==Limitations==<br />
<br />
The cells aren't quite a uniform size.<br />
<br />
The library saves its data in a lazy array indexed by grid size. Sizes larger than 1024 aren't supported. (A maximum-sized grid would then have about 10 million cells.)<br />
<br />
==Links==<br />
<br />
[http://hackage.haskell.org/package/IcoGrid IcoGrid on Hackage]</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=31169IcoGrid2009-10-26T06:37:46Z<p>Jsnow: </p>
<hr />
<div>IcoGrid (Icosohedron Grid) is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.<br />
<br />
It's easier to show than to describe, so here is a screenshot of an OpenGL application I wrote to render a grid generated by IcoGrid:<br />
<br />
[[Image:IcoGrid-small.png]]<br />
<br />
==Grid Overview==<br />
<br />
The grid is layed out as twenty triangular hexagon grids fitted together to make an icosahedron. The points where file of these triangular grids meet are pentagons. The size of the grid is length of one of the sides of the triangular grids. Thus a grid of size S will have approximately (S^2)*10 cells.<br />
<br />
Most of the library functions require the grid size as the first parameter. Cells are identified by integers from [0..N-1] for a grid with N cells.<br />
<br />
==Library Features==<br />
<br />
Functions are provided to generate a list of all cells, a list of neighbors to a particular cell, a list of "triads" to which a cell belongs, and a list of all triads.<br />
<br />
A "triad" (my own terminology) is a group of 3 cells that meet at a point.<br />
<br />
Another useful function takes a cell ID and returns a vector, which is the center of that cell, as if the whole grid were mapped to a sphere centered at the origin with radius 1.<br />
<br />
A function which I have not implemented is the reverse function, which takes any arbitrary point and returns the closest cell. I expect the easiest way to implement such a function efficiently is to sort the cells into a bounding volume hierarchy of some sort, then look up the point (after normalizing it to lie on the surface of the grid) in the BVH to get a small list of candidate cells, and then search the candidate cells for the closest one.<br />
<br />
==Links==<br />
<br />
[http://hackage.haskell.org/package/IcoGrid IcoGrid on Hackage]</div>Jsnowhttps://wiki.haskell.org/index.php?title=File:IcoGrid-small.png&diff=31158File:IcoGrid-small.png2009-10-26T05:46:15Z<p>Jsnow: Screenshot of an OpenGL app demonstrating the IcoGrid library.</p>
<hr />
<div>Screenshot of an OpenGL app demonstrating the IcoGrid library.</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=31156IcoGrid2009-10-26T05:45:41Z<p>Jsnow: </p>
<hr />
<div>IcoGrid (Icosohedron Grid) is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.<br />
<br />
It's easier to show than to describe, so here is a screenshot of an OpenGL application I wrote to render a grid generated by IcoGrid:<br />
<br />
[[Image:IcoGrid-small.png]]<br />
<br />
The grid is layed out as twenty triangular hexagon grids fitted together to make an icosahedron. The points where file of these triangular grids meet are pentagons. The size of the grid is length of one of the sides of the triangular grids. Thus a grid of size S will have approximately (S^2)*10 cells.<br />
<br />
Most of the library functions require the grid size as the first parameter. Cells are identified by integers from [0..N-1] for a grid with N cells.<br />
<br />
Functions are provided to generate a list of all cells, a list of neighbors to a particular cell, a list of "triads" to which a cell belongs, and a list of all triads.<br />
<br />
A "triad" (my own terminology) is a group of 3 cells that meet at a point.<br />
<br />
Another useful function takes a cell ID and returns a vector, which is the center of that cell, as if the whole grid were mapped to a sphere centered at the origin with radius 1.<br />
<br />
A function which I have not implemented is the reverse function, which takes any arbitrary point and returns the closest cell. I expect the easiest way to implement such a function efficiently is to sort the cells into a bounding volume hierarchy of some sort, then look up the point (after normalizing it to lie on the surface of the grid) in the BVH to get a small list of candidate cells, and then search the candidate cells for the closest one.<br />
<br />
==Links==<br />
<br />
[http://hackage.haskell.org/package/IcoGrid IcoGrid on Hackage]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=31153Glome2009-10-26T05:39:51Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
===GlomeVec===<br />
GlomeVec is the vector library used by Glome, and is now (as of Oct 25 2009) available as a separate library. My intention is to make the core of Glome into a library as well.<br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
==Documentation==<br />
<br />
*[[Glome tutorial]]<br />
<br />
==Links==<br />
<br />
* [http://syn.cs.pdx.edu/~jsnow/glome Glome homepage] (This is down and needs to be moved elsewhere.)<br />
* [http://hackage.haskell.org/package/glome-hs Glome on Hackage]<br />
* [http://hackage.haskell.org/package/GlomeVec GlomeVec on Hackage]</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=31152IcoGrid2009-10-26T05:36:51Z<p>Jsnow: </p>
<hr />
<div>IcoGrid (Icosohedron Grid) is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.<br />
<br />
It's easier to show than to describe, so here is a screenshot of an OpenGL application I wrote to render a grid generated by IcoGrid:<br />
<br />
[[Image:IcoGrid.png]]<br />
<br />
The grid is layed out as twenty triangular hexagon grids fitted together to make an icosahedron. The points where file of these triangular grids meet are pentagons. The size of the grid is length of one of the sides of the triangular grids. Thus a grid of size S will have approximately (S^2)*10 cells.<br />
<br />
Most of the library functions require the grid size as the first parameter. Cells are identified by integers from [0..N-1] for a grid with N cells.<br />
<br />
Functions are provided to generate a list of all cells, a list of neighbors to a particular cell, a list of "triads" to which a cell belongs, and a list of all triads.<br />
<br />
A "triad" (my own terminology) is a group of 3 cells that meet at a point.<br />
<br />
Another useful function takes a cell ID and returns a vector, which is the center of that cell, as if the whole grid were mapped to a sphere centered at the origin with radius 1.<br />
<br />
A function which I have not implemented is the reverse function, which takes any arbitrary point and returns the closest cell. I expect the easiest way to implement such a function efficiently is to sort the cells into a bounding volume hierarchy of some sort, then look up the point (after normalizing it to lie on the surface of the grid) in the BVH to get a small list of candidate cells, and then search the candidate cells for the closest one.<br />
<br />
==Links==<br />
<br />
[http://hackage.haskell.org/package/IcoGrid IcoGrid on Hackage]</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=31150IcoGrid2009-10-26T05:36:29Z<p>Jsnow: </p>
<hr />
<div>IcoGrid (Icosohedron Grid) is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.<br />
<br />
It's easier to show than to describe, so here is a screenshot of an OpenGL application I wrote to render a grid generated by IcoGrid:<br />
<br />
[[Image:IcoGrid.png]]<br />
<br />
The grid is layed out as twenty triangular hexagon grids fitted together to make an icosahedron. The points where file of these triangular grids meet are pentagons. The size of the grid is length of one of the sides of the triangular grids. Thus a grid of size S will have approximately (S^2)*10 cells.<br />
<br />
Most of the library functions require the grid size as the first parameter. Cells are identified by integers from [0..N-1] for a grid with N cells.<br />
<br />
Functions are provided to generate a list of all cells, a list of neighbors to a particular cell, a list of "triads" to which a cell belongs, and a list of all triads.<br />
<br />
A "triad" (my own terminology) is a group of 3 cells that meet at a point.<br />
<br />
Another useful function takes a cell ID and returns a vector, which is the center of that cell, as if the whole grid were mapped to a sphere centered at the origin with radius 1.<br />
<br />
A function which I have not implemented is the reverse function, which takes any arbitrary point and returns the closest cell. I expect the easiest way to implement such a function efficiently is to sort the cells into a bounding volume hierarchy of some sort, then look up the point (after normalizing it to lie on the surface of the grid) in the BVH to get a small list of candidate cells, and then search the candidate cells for the closest one.<br />
<br />
==Links==<br />
<br />
[http://hackage.haskell.org/package/IcoGrid IcoGrid]</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=31148IcoGrid2009-10-26T05:36:09Z<p>Jsnow: </p>
<hr />
<div>IcoGrid (Icosohedron Grid) is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.<br />
<br />
It's easier to show than to describe, so here is a screenshot of an OpenGL application I wrote to render a grid generated by IcoGrid:<br />
<br />
[[Image:IcoGrid.png]]<br />
<br />
The grid is layed out as twenty triangular hexagon grids fitted together to make an icosahedron. The points where file of these triangular grids meet are pentagons. The size of the grid is length of one of the sides of the triangular grids. Thus a grid of size S will have approximately (S^2)*10 cells.<br />
<br />
Most of the library functions require the grid size as the first parameter. Cells are identified by integers from [0..N-1] for a grid with N cells.<br />
<br />
Functions are provided to generate a list of all cells, a list of neighbors to a particular cell, a list of "triads" to which a cell belongs, and a list of all triads.<br />
<br />
A "triad" (my own terminology) is a group of 3 cells that meet at a point.<br />
<br />
Another useful function takes a cell ID and returns a vector, which is the center of that cell, as if the whole grid were mapped to a sphere centered at the origin with radius 1.<br />
<br />
A function which I have not implemented is the reverse function, which takes any arbitrary point and returns the closest cell. I expect the easiest way to implement such a function efficiently is to sort the cells into a bounding volume hierarchy of some sort, then look up the point (after normalizing it to lie on the surface of the grid) in the BVH to get a small list of candidate cells, and then search the candidate cells for the closest one.<br />
<br />
==Links==<br />
<br />
[http://hackage.haskell.org/package/IcoGrid]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=31145Glome2009-10-26T04:11:10Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
===GlomeVec===<br />
GlomeVec is the vector library used by Glome, and is now (as of Oct 25 2009) available as a separate library. My intention is to make the core of Glome into a library as well.<br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
==Documentation==<br />
todo:<br />
*[[Glome tutorial]]<br />
<br />
==Links==<br />
<br />
* [http://syn.cs.pdx.edu/~jsnow/glome Glome homepage] (This is down and needs to be moved elsewhere.)<br />
* [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=31144IcoGrid2009-10-26T04:09:04Z<p>Jsnow: </p>
<hr />
<div>IcoGrid (Icosohedron Grid) is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.<br />
<br />
It's easier to show than to describe, so here is a screenshot of an OpenGL application I wrote to render a grid generated by IcoGrid:<br />
<br />
[[Image:IcoGrid.png]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=File:IcoGrid.png&diff=31143File:IcoGrid.png2009-10-26T04:06:52Z<p>Jsnow: Screenshot showing an OpenGL application making use of IcoGrid library.</p>
<hr />
<div>Screenshot showing an OpenGL application making use of IcoGrid library.</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=31142IcoGrid2009-10-26T04:04:40Z<p>Jsnow: </p>
<hr />
<div>IcoGrid is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.<br />
<br />
[[Image:IcoGrid.png]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=IcoGrid&diff=31141IcoGrid2009-10-26T03:44:16Z<p>Jsnow: </p>
<hr />
<div>IcoGrid is a haskell library for generating grids of hexagons and pentagons wrapped around a sphere.</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome&diff=31140Glome2009-10-26T03:43:11Z<p>Jsnow: </p>
<hr />
<div>==About==<br />
Glome is a ray tracer written by Jim Snow. Originally it was written in Ocaml, but the more recent versions are written in Haskell.<br />
<br />
===GlomeVec===<br />
GlomeVec is the vector library used by Glome, and is now (as of Oct 25 2009) available as a separate library. My intention is to make the core of Glome into a library as well.<br />
<br />
GlomeVec is also used by a separate project, [[IcoGrid]]<br />
<br />
==Documentation==<br />
todo:<br />
*[[Glome tutorial]]<br />
<br />
==Links==<br />
<br />
* [http://syn.cs.pdx.edu/~jsnow/glome Glome homepage] (This is down.)<br />
* [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=21050Glome tutorial2008-05-20T02:19:18Z<p>Jsnow: </p>
<hr />
<div>==Notes==<br />
This tutorial currently assumes version 0.41 of Glome. Many things have changed in 0.5.<br />
<br />
==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help. Familiarity with ray tracing is not required either, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options, key commands==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
Once an image is rendered, typing "q" with the rendering window in focus will close Glome. Typing "s" will print a dump of the internal representation of the scene. (Not very useful at this stage, perhaps, but it's a useful debugging tool that might come in handy later.)<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
One caveat about using Tex is that the Bih constructor treats a Tex (and all the objects it contain) as a monolithic object. So, if a Tex contains many objects (or even just a few), you might want to use "bih" instead of "group" on the children, even if you're running "bih" at the root of the scene.<br />
<br />
==A guide to the Glome source code==<br />
<br />
Editing scenes directly in Haskell makes it possible to use the pre-existing ray tracing infrastructure to help us create our scene. For instance, the "trace" function can be used to help us place one object on another; a plant growing on a non-flat surface, for instance. Also, we may want our scene to include geometric primitives of a type that Glome does not yet support, and so we might want to add our own.<br />
<br />
This section of the tutorial is for those who are interested in understanding not just how to use Glome, but how it works and how it can be extended.<br />
<br />
===File Reference===<br />
<br />
First, an overview of the source code files. Glome is currently split amongst eight source files and is about 2500 lines of code. These are the files:<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting code is: definitions of basic data types, ray-intersection, shadow, and inside/outside tests for all the supported primitives, and constructors for the various primitives.<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen. It also includes the shading algorithms.<br />
<br />
;Glome.hs<br />
:The main loop of the program. All OpenGL-related code resides in this module. Also included is a get_color function that accepts screen coordinates and a scene, and computes the ray for that screen coordinate, traces the ray, and returns the color.<br />
<br />
;TestScene.hs<br />
:This is what gets rendered if no input file is specified. This is meant to be edited by users.<br />
<br />
;SolidTexture.hs<br />
:Perlin noise and other related texture functions.<br />
<br />
;Spd.hs<br />
:NFF file parser for SPD scenes.<br />
<br />
===Tracing Rays===<br />
<br />
Solid.hs defines a ray intersection thus:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
And these are our basic primitives:<br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec,<br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
<br />
Glome defines a ray-intersection function "rayint" that pattern matches against all of these primitives and returns an appropriate Rayint.<br />
<br />
For instance, let's look at the "disc" case, as it is quite simple:<br />
<br />
<haskell><br />
rayint :: Solid -> Ray -> Flt -> Texture -> Rayint<br />
rayint (Disc point norm radius_sqr) r d t =<br />
let (Ray orig dir) = r<br />
dist = plane_int_dist r point norm <br />
in if dist < 0 || dist > d <br />
then RayMiss<br />
else let pos = vscaleadd orig dir dist<br />
offset = vsub pos point<br />
in <br />
if (vdot offset offset) > radius_sqr<br />
then RayMiss<br />
else RayHit dist pos norm t<br />
</haskell><br />
<br />
"rayint" takes four arguments: the Solid to be intersected, the Ray to intersect with it, a Flt (shorthand for Float or Double, depending on how Vec.hs is configured), and a Texture. "rayint" is expected to return a RayHit value if a ray intersection exists within the distance given by "d". If there is more than one hit, rayint should return the closest one, but never an intersection that is behind the Ray's origin. Discs are flat, though, so we can never intersect one twice with a single ray.<br />
<br />
Glome extracts the Ray's origin and direction from "r", and then uses a function called "plane_int_dist" defined in Vec.hs. This returns the distance to the plane defined by a point on the plane and it's normal, and intersected by ray r.<br />
<br />
Then Glome checks if the distance is less than zero or more than the maximum allowed, and if so returns RayMiss.<br />
<br />
Otherwise, Glome computes the hit location from the Ray and distance to the plane. "vscaleadd" is another function from Vec.hs that takes one vector, and then adds a second vector after scaling the second vector by some scalar. By taking the Ray's (normalized) direction vector scaled by the distance to the plane and adding it to the Ray's origin, we get the hit location. (This technique is used in many of the ray-intersection tests.)<br />
<br />
Once we know the location on the disc's plane where our ray hit, we need to know if it is within the radius of the disc. For that, we compute an offset vector from the center of the disc ("point") to the hit location ("pos"). Then we want to check if this offset vector is less than the radius of the disc. Or, in other words:<br />
<br />
sqrt (offset.x^2 + offset.y^2 + offset.z^2) < r<br />
<br />
We can square both sides to get rid of the square route, and then observe that squaring the components of a vector is the same as taking the dot product of that vector with itself:<br />
<br />
vdot offset offset < r^2<br />
<br />
Also, Glome doesn't store the radius with a disc but rather it's radius squared (to avoid a multiply, since we don't often need to know the disc's actual radius), and that explains the last if statement.<br />
<br />
The RayHit constructor needs a little explanation, though. What are all those fields for?<br />
<br />
First, there's the distance to the nearest hit and the position. (You might notice some redundancy here, since the calling function could infer the distance to the nearest hit from the ray and the hit position, or the the hit position from the distance and the ray. We return both to save the trouble of recomputing values we've already determined.) "norm" is the vector perpendicular to the surface of the disc, used in lighting calculations. For discs, this is easy: the normal is stored as part of the disc's definition. For other objects (like Spheres or Cones), we might have to compute a normal. <br />
<br />
The texture passed in as an argument to "rayint" is simply returned in the returned Rayint record. All of the ray intersection cases behave this way except Tex, which overrides the texture with its own.<br />
<br />
As for the other basic primitives like Triangle, Sphere, Cylinder, Cone, Plane, and Box, Glome uses standard intersection tests that can be found in graphics textbooks (such as [http://pbrt.org/ Physically Based Rendering] and Graphics Gems volume one). <br />
<br />
A "Nothing" object is a special case: its ray intersector simply returns "RayMiss" regardless of input. The existence of "Nothing" is somewhat redundant, since it is equivalent to "Group []".<br />
<br />
The composite primitives (Group, Difference, Intersection, Tex, Bih, Instance, and Bound) are more interesting, as their ray-intersection tests are defined recursively. This recursion is what allows us to treat a complex object made up of many sub-objects the same as we would treat a simple base primitive like a Sphere, and in fact Glome makes no distinction whatsoever between base primitives and composite primitives.<br />
<br />
We'll look at Group as our composite ray-intersection test example:<br />
<br />
<haskell><br />
rayint (Group xs) r d t =<br />
let rig [] = RayMiss<br />
rig (x:xs) = nearest (rayint x r d t) (rig xs)<br />
in rig xs<br />
</haskell><br />
<br />
Here, "rayint" defines a simple function to traverse the list calling "rayint" for each primitive and returning the nearest hit. ("nearest" is defined in Solid.hs, and returns the nearest of two ray intersections, or RayMiss if they both miss.)<br />
<br />
This could be defined more succinctly with a fold:<br />
<br />
<haskell><br />
rayint (Group lst) r d t = foldl nearest RayMiss (map (\x -> rayint x r d t) lst)<br />
</haskell><br />
<br />
...and we only use the more verbose style for efficiency. (I'm not sure if I actually benchmarked if it was faster, so this might be worth checking.)<br />
<br />
One important thing to note about Group is that intersecting with a large group is very inefficient. That's why we have Bih. However, even Bih uses Groups as leaf nodes, so Groups are still important.<br />
<br />
There is a second ray intersection function called "shadow" that has a simpler type signature than "rayint":<br />
<br />
<haskell><br />
shadow :: Solid -> Ray -> Flt -> Bool<br />
</haskell><br />
<br />
"shadow" is used for shadow-ray occlusion tests. In order to test whether a particular point is lit by a particular light, a shadow ray is traced from the ray intersection point to the light. If there is something in the way, that light is in shadow and it does not contribute to the illumination at that point.<br />
<br />
For most scenes with more than one light, more shadow rays are traced than regular rays. Therefore, we want the shadow ray intersection tests to be as fast as possible. "shadow" does not require a texture, and it returns True if the ray hits the object or False if it misses.<br />
<br />
Let's look at the shadow test for Group:<br />
<br />
<haskell><br />
shadow (Group xs) r d =<br />
let sg [] = False<br />
sg (x:xs) = (shadow x r d) || (sg xs)<br />
in sg xs<br />
</haskell><br />
<br />
Note that "shadow" stops as soon as one of the shadow tests returns True.<br />
<br />
Primitives are not required to implement a shadow test. Glome defines a reasonable default case:<br />
<br />
<haskell><br />
shadow s r d =<br />
case (rayint s r d t_white) of<br />
RayHit _ _ _ _ -> True<br />
RayMiss -> False<br />
</haskell><br />
<br />
For base primitives, the performance penalty of using the full ray intersection test instead of a shadow test may be insignificant. However, composite primitives should always define a shadow test. Consider, for instance, if Group did not implement a shadow test: all it's children would be tested with "rayint" rather than "shadow", and if any of those objects have sub-objects, they will be tested with "rayint" as well! A single primitive type high in the tree that doesn't support "shadow" will force its entire subtree to be evaluated with "rayint".<br />
<br />
There is one case where "rayint" actually calls "shadow", rather than the other way around: Bound uses a shadow test to determine if the ray hits the bounding object or not.<br />
<br />
==Advanced Topics, and things that don't quite work yet==<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20709Glome tutorial2008-04-27T06:43:49Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help. Familiarity with ray tracing is not required either, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options, key commands==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
Once an image is rendered, typing "q" with the rendering window in focus will close Glome. Typing "s" will print a dump of the internal representation of the scene. (Not very useful at this stage, perhaps, but it's a useful debugging tool that might come in handy later.)<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
One caveat about using Tex is that the Bih constructor treats a Tex (and all the objects it contain) as a monolithic object. So, if a Tex contains many objects (or even just a few), you might want to use "bih" instead of "group" on the children, even if you're running "bih" at the root of the scene.<br />
<br />
==A guide to the Glome source code==<br />
<br />
Editing scenes directly in Haskell makes it possible to use the pre-existing ray tracing infrastructure to help us create our scene. For instance, the "trace" function can be used to help us place one object on another; a plant growing on a non-flat surface, for instance. Also, we may want our scene to include geometric primitives of a type that Glome does not yet support, and so we might want to add our own.<br />
<br />
This section of the tutorial is for those who are interested in understanding not just how to use Glome, but how it works and how it can be extended.<br />
<br />
===File Reference===<br />
<br />
First, an overview of the source code files. Glome is currently split amongst eight source files and is about 2500 lines of code. These are the files:<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting code is: definitions of basic data types, ray-intersection, shadow, and inside/outside tests for all the supported primitives, and constructors for the various primitives.<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen. It also includes the shading algorithms.<br />
<br />
;Glome.hs<br />
:The main loop of the program. All OpenGL-related code resides in this module. Also included is a get_color function that accepts screen coordinates and a scene, and computes the ray for that screen coordinate, traces the ray, and returns the color.<br />
<br />
;TestScene.hs<br />
:This is what gets rendered if no input file is specified. This is meant to be edited by users.<br />
<br />
;SolidTexture.hs<br />
:Perlin noise and other related texture functions.<br />
<br />
;Spd.hs<br />
:NFF file parser for SPD scenes.<br />
<br />
===Tracing Rays===<br />
<br />
Solid.hs defines a ray intersection thus:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
And these are our basic primitives:<br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec,<br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
<br />
Glome defines a ray-intersection function "rayint" that pattern matches against all of these primitives and returns an appropriate Rayint.<br />
<br />
For instance, let's look at the "disc" case, as it is quite simple:<br />
<br />
<haskell><br />
rayint :: Solid -> Ray -> Flt -> Texture -> Rayint<br />
rayint (Disc point norm radius_sqr) r d t =<br />
let (Ray orig dir) = r<br />
dist = plane_int_dist r point norm <br />
in if dist < 0 || dist > d <br />
then RayMiss<br />
else let pos = vscaleadd orig dir dist<br />
offset = vsub pos point<br />
in <br />
if (vdot offset offset) > radius_sqr<br />
then RayMiss<br />
else RayHit dist pos norm t<br />
</haskell><br />
<br />
"rayint" takes four arguments: the Solid to be intersected, the Ray to intersect with it, a Flt (shorthand for Float or Double, depending on how Vec.hs is configured), and a Texture. "rayint" is expected to return a RayHit value if a ray intersection exists within the distance given by "d". If there is more than one hit, rayint should return the closest one, but never an intersection that is behind the Ray's origin. Discs are flat, though, so we can never intersect one twice with a single ray.<br />
<br />
Glome extracts the Ray's origin and direction from "r", and then uses a function called "plane_int_dist" defined in Vec.hs. This returns the distance to the plane defined by a point on the plane and it's normal, and intersected by ray r.<br />
<br />
Then Glome checks if the distance is less than zero or more than the maximum allowed, and if so returns RayMiss.<br />
<br />
Otherwise, Glome computes the hit location from the Ray and distance to the plane. "vscaleadd" is another function from Vec.hs that takes one vector, and then adds a second vector after scaling the second vector by some scalar. By taking the Ray's (normalized) direction vector scaled by the distance to the plane and adding it to the Ray's origin, we get the hit location. (This technique is used in many of the ray-intersection tests.)<br />
<br />
Once we know the location on the disc's plane where our ray hit, we need to know if it is within the radius of the disc. For that, we compute an offset vector from the center of the disc ("point") to the hit location ("pos"). Then we want to check if this offset vector is less than the radius of the disc. Or, in other words:<br />
<br />
sqrt (offset.x^2 + offset.y^2 + offset.z^2) < r<br />
<br />
We can square both sides to get rid of the square route, and then observe that squaring the components of a vector is the same as taking the dot product of that vector with itself:<br />
<br />
vdot offset offset < r^2<br />
<br />
Also, Glome doesn't store the radius with a disc but rather it's radius squared (to avoid a multiply, since we don't often need to know the disc's actual radius), and that explains the last if statement.<br />
<br />
The RayHit constructor needs a little explanation, though. What are all those fields for?<br />
<br />
First, there's the distance to the nearest hit and the position. (You might notice some redundancy here, since the calling function could infer the distance to the nearest hit from the ray and the hit position, or the the hit position from the distance and the ray. We return both to save the trouble of recomputing values we've already determined.) "norm" is the vector perpendicular to the surface of the disc, used in lighting calculations. For discs, this is easy: the normal is stored as part of the disc's definition. For other objects (like Spheres or Cones), we might have to compute a normal. <br />
<br />
The texture passed in as an argument to "rayint" is simply returned in the returned Rayint record. All of the ray intersection cases behave this way except Tex, which overrides the texture with its own.<br />
<br />
As for the other basic primitives like Triangle, Sphere, Cylinder, Cone, Plane, and Box, Glome uses standard intersection tests that can be found in graphics textbooks (such as [http://pbrt.org/ Physically Based Rendering] and Graphics Gems volume one). <br />
<br />
A "Nothing" object is a special case: its ray intersector simply returns "RayMiss" regardless of input. The existence of "Nothing" is somewhat redundant, since it is equivalent to "Group []".<br />
<br />
The composite primitives (Group, Difference, Intersection, Tex, Bih, Instance, and Bound) are more interesting, as their ray-intersection tests are defined recursively. This recursion is what allows us to treat a complex object made up of many sub-objects the same as we would treat a simple base primitive like a Sphere, and in fact Glome makes no distinction whatsoever between base primitives and composite primitives.<br />
<br />
We'll look at Group as our composite ray-intersection test example:<br />
<br />
<haskell><br />
rayint (Group xs) r d t =<br />
let rig [] = RayMiss<br />
rig (x:xs) = nearest (rayint x r d t) (rig xs)<br />
in rig xs<br />
</haskell><br />
<br />
Here, "rayint" defines a simple function to traverse the list calling "rayint" for each primitive and returning the nearest hit. ("nearest" is defined in Solid.hs, and returns the nearest of two ray intersections, or RayMiss if they both miss.)<br />
<br />
This could be defined more succinctly with a fold:<br />
<br />
<haskell><br />
rayint (Group lst) r d t = foldl nearest RayMiss (map (\x -> rayint x r d t) lst)<br />
</haskell><br />
<br />
...and we only use the more verbose style for efficiency. (I'm not sure if I actually benchmarked if it was faster, so this might be worth checking.)<br />
<br />
One important thing to note about Group is that intersecting with a large group is very inefficient. That's why we have Bih. However, even Bih uses Groups as leaf nodes, so Groups are still important.<br />
<br />
There is a second ray intersection function called "shadow" that has a simpler type signature than "rayint":<br />
<br />
<haskell><br />
shadow :: Solid -> Ray -> Flt -> Bool<br />
</haskell><br />
<br />
"shadow" is used for shadow-ray occlusion tests. In order to test whether a particular point is lit by a particular light, a shadow ray is traced from the ray intersection point to the light. If there is something in the way, that light is in shadow and it does not contribute to the illumination at that point.<br />
<br />
For most scenes with more than one light, more shadow rays are traced than regular rays. Therefore, we want the shadow ray intersection tests to be as fast as possible. "shadow" does not require a texture, and it returns True if the ray hits the object or False if it misses.<br />
<br />
Let's look at the shadow test for Group:<br />
<br />
<haskell><br />
shadow (Group xs) r d =<br />
let sg [] = False<br />
sg (x:xs) = (shadow x r d) || (sg xs)<br />
in sg xs<br />
</haskell><br />
<br />
Note that "shadow" stops as soon as one of the shadow tests returns True.<br />
<br />
Primitives are not required to implement a shadow test. Glome defines a reasonable default case:<br />
<br />
<haskell><br />
shadow s r d =<br />
case (rayint s r d t_white) of<br />
RayHit _ _ _ _ -> True<br />
RayMiss -> False<br />
</haskell><br />
<br />
For base primitives, the performance penalty of using the full ray intersection test instead of a shadow test may be insignificant. However, composite primitives should always define a shadow test. Consider, for instance, if Group did not implement a shadow test: all it's children would be tested with "rayint" rather than "shadow", and if any of those objects have sub-objects, they will be tested with "rayint" as well! A single primitive type high in the tree that doesn't support "shadow" will force its entire subtree to be evaluated with "rayint".<br />
<br />
There is one case where "rayint" actually calls "shadow", rather than the other way around: Bound uses a shadow test to determine if the ray hits the bounding object or not.<br />
<br />
==Advanced Topics, and things that don't quite work yet==<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20708Glome tutorial2008-04-27T06:25:54Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help. Familiarity with ray tracing is not required either, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
==A guide to the Glome source code==<br />
<br />
Editing scenes directly in Haskell makes it possible to use the pre-existing ray tracing infrastructure to help us create our scene. For instance, the "trace" function can be used to help us place one object on another; a plant growing on a non-flat surface, for instance. Also, we may want our scene to include geometric primitives of a type that Glome does not yet support, and so we might want to add our own.<br />
<br />
This section of the tutorial is for those who are interested in understanding not just how to use Glome, but how it works and how it can be extended.<br />
<br />
===File Reference===<br />
<br />
First, an overview of the source code files. Glome is currently split amongst eight source files and is about 2500 lines of code. These are the files:<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting code is: definitions of basic data types, ray-intersection, shadow, and inside/outside tests for all the supported primitives, and constructors for the various primitives.<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen. It also includes the shading algorithms.<br />
<br />
;Glome.hs<br />
:The main loop of the program. All OpenGL-related code resides in this module. Also included is a get_color function that accepts screen coordinates and a scene, and computes the ray for that screen coordinate, traces the ray, and returns the color.<br />
<br />
;TestScene.hs<br />
:This is what gets rendered if no input file is specified. This is meant to be edited by users.<br />
<br />
;SolidTexture.hs<br />
:Perlin noise and other related texture functions.<br />
<br />
;Spd.hs<br />
:NFF file parser for SPD scenes.<br />
<br />
===Tracing Rays===<br />
<br />
Solid.hs defines a ray intersection thus:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
And these are our basic primitives:<br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec,<br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
<br />
Glome defines a ray-intersection function "rayint" that pattern matches against all of these primitives and returns an appropriate Rayint.<br />
<br />
For instance, let's look at the "disc" case, as it is quite simple:<br />
<br />
<haskell><br />
rayint :: Solid -> Ray -> Flt -> Texture -> Rayint<br />
rayint (Disc point norm radius_sqr) r d t =<br />
let (Ray orig dir) = r<br />
dist = plane_int_dist r point norm <br />
in if dist < 0 || dist > d <br />
then RayMiss<br />
else let pos = vscaleadd orig dir dist<br />
offset = vsub pos point<br />
in <br />
if (vdot offset offset) > radius_sqr<br />
then RayMiss<br />
else RayHit dist pos norm t<br />
</haskell><br />
<br />
"rayint" takes four arguments: the Solid to be intersected, the Ray to intersect with it, a Flt (shorthand for Float or Double, depending on how Vec.hs is configured), and a Texture. "rayint" is expected to return a RayHit value if a ray intersection exists within the distance given by "d". If there is more than one hit, rayint should return the closest one, but never an intersection that is behind the Ray's origin. Discs are flat, though, so we can never intersect one twice with a single ray.<br />
<br />
Glome extracts the Ray's origin and direction from "r", and then uses a function called "plane_int_dist" defined in Vec.hs. This returns the distance to the plane defined by a point on the plane and it's normal, and intersected by ray r.<br />
<br />
Then Glome checks if the distance is less than zero or more than the maximum allowed, and if so returns RayMiss.<br />
<br />
Otherwise, Glome computes the hit location from the Ray and distance to the plane. "vscaleadd" is another function from Vec.hs that takes one vector, and then adds a second vector after scaling the second vector by some scalar. By taking the Ray's (normalized) direction vector scaled by the distance to the plane and adding it to the Ray's origin, we get the hit location. (This technique is used in many of the ray-intersection tests.)<br />
<br />
Once we know the location on the disc's plane where our ray hit, we need to know if it is within the radius of the disc. For that, we compute an offset vector from the center of the disc ("point") to the hit location ("pos"). Then we want to check if this offset vector is less than the radius of the disc. Or, in other words:<br />
<br />
sqrt (offset.x^2 + offset.y^2 + offset.z^2) < r<br />
<br />
We can square both sides to get rid of the square route, and then observe that squaring the components of a vector is the same as taking the dot product of that vector with itself:<br />
<br />
vdot offset offset < r^2<br />
<br />
Also, Glome doesn't store the radius with a disc but rather it's radius squared (to avoid a multiply, since we don't often need to know the disc's actual radius), and that explains the last if statement.<br />
<br />
The RayHit constructor needs a little explanation, though. What are all those fields for?<br />
<br />
First, there's the distance to the nearest hit and the position. (You might notice some redundancy here, since the calling function could infer the distance to the nearest hit from the ray and the hit position, or the the hit position from the distance and the ray. We return both to save the trouble of recomputing values we've already determined.) "norm" is the vector perpendicular to the surface of the disc, used in lighting calculations. For discs, this is easy: the normal is stored as part of the disc's definition. For other objects (like Spheres or Cones), we might have to compute a normal. <br />
<br />
The texture passed in as an argument to "rayint" is simply returned in the returned Rayint record. All of the ray intersection cases behave this way except Tex, which overrides the texture with its own.<br />
<br />
As for the other basic primitives like Triangle, Sphere, Cylinder, Cone, Plane, and Box, Glome uses standard intersection tests that can be found in graphics textbooks (such as [http://pbrt.org/ Physically Based Rendering] and Graphics Gems volume one). <br />
<br />
A "Nothing" object is a special case: its ray intersector simply returns "RayMiss" regardless of input. The existence of "Nothing" is somewhat redundant, since it is equivalent to "Group []".<br />
<br />
The composite primitives (Group, Difference, Intersection, Tex, Bih, Instance, and Bound) are more interesting, as their ray-intersection tests are defined recursively. This recursion is what allows us to treat a complex object made up of many sub-objects the same as we would treat a simple base primitive like a Sphere, and in fact Glome makes no distinction whatsoever between base primitives and composite primitives.<br />
<br />
We'll look at Group as our composite ray-intersection test example:<br />
<br />
<haskell><br />
rayint (Group xs) r d t =<br />
let rig [] = RayMiss<br />
rig (x:xs) = nearest (rayint x r d t) (rig xs)<br />
in rig xs<br />
</haskell><br />
<br />
Here, "rayint" defines a simple function to traverse the list calling "rayint" for each primitive and returning the nearest hit. ("nearest" is defined in Solid.hs, and returns the nearest of two ray intersections, or RayMiss if they both miss.)<br />
<br />
This could be defined more succinctly with a fold:<br />
<br />
<haskell><br />
rayint (Group lst) r d t = foldl nearest RayMiss (map (\x -> rayint x r d t) lst)<br />
</haskell><br />
<br />
...and we only use the more verbose style for efficiency. (I'm not sure if I actually benchmarked if it was faster, so this might be worth checking.)<br />
<br />
One important thing to note about Group is that intersecting with a large group is very inefficient. That's why we have Bih. However, even Bih uses Groups as leaf nodes, so Groups are still important.<br />
<br />
There is a second ray intersection function called "shadow" that has a simpler type signature than "rayint":<br />
<br />
<haskell><br />
shadow :: Solid -> Ray -> Flt -> Bool<br />
</haskell><br />
<br />
"shadow" is used for shadow-ray occlusion tests. In order to test whether a particular point is lit by a particular light, a shadow ray is traced from the ray intersection point to the light. If there is something in the way, that light is in shadow and it does not contribute to the illumination at that point.<br />
<br />
For most scenes with more than one light, more shadow rays are traced than regular rays. Therefore, we want the shadow ray intersection tests to be as fast as possible. "shadow" does not require a texture, and it returns True if the ray hits the object or False if it misses.<br />
<br />
Let's look at the shadow test for Group:<br />
<br />
<haskell><br />
shadow (Group xs) r d =<br />
let sg [] = False<br />
sg (x:xs) = (shadow x r d) || (sg xs)<br />
in sg xs<br />
</haskell><br />
<br />
Note that "shadow" stops as soon as one of the shadow tests returns True.<br />
<br />
Primitives are not required to implement a shadow test. Glome defines a reasonable default case:<br />
<br />
<haskell><br />
shadow s r d =<br />
case (rayint s r d t_white) of<br />
RayHit _ _ _ _ -> True<br />
RayMiss -> False<br />
</haskell><br />
<br />
For base primitives, the performance penalty of using the full ray intersection test instead of a shadow test may be insignificant. However, composite primitives should always define a shadow test. Consider, for instance, if Group did not implement a shadow test: all it's children would be tested with "rayint" rather than "shadow", and if any of those objects have sub-objects, they will be tested with "rayint" as well! A single primitive type high in the tree that doesn't support "shadow" will force its entire subtree to be evaluated with "rayint".<br />
<br />
There is one case where "rayint" actually calls "shadow", rather than the other way around: Bound uses a shadow test to determine if the ray hits the bounding object or not.<br />
<br />
==Advanced Topics, and things that don't quite work yet==<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20707Glome tutorial2008-04-27T05:52:20Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help. Familiarity with ray tracing is not required either, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
===A guide to understanding the source code===<br />
<br />
Editing scenes directly in Haskell makes it possible to use the pre-existing ray tracing infrastructure to help us create our scene. For instance, the "trace" function can be used to help us place one object on another; a plant growing on a non-flat surface, for instance. Also, we may want our scene to include geometric primitives of a type that Glome does not yet support, and so we might want to add our own.<br />
<br />
This section of the tutorial is for those who are interested in understanding not just how to use Glome, but how it works and how it can be extended.<br />
<br />
====File Reference====<br />
<br />
First, an overview of the source code files. Glome is currently split amongst eight source files and is about 2500 lines of code. These are the files:<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting code is: definitions of basic data types, ray-intersection, shadow, and inside/outside tests for all the supported primitives, and constructors for the various primitives.<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen. It also includes the shading algorithms.<br />
<br />
;Glome.hs<br />
:The main loop of the program. All OpenGL-related code resides in this module. Also included is a get_color function that accepts screen coordinates and a scene, and computes the ray for that screen coordinate, traces the ray, and returns the color.<br />
<br />
;TestScene.hs<br />
:This is what gets rendered if no input file is specified. This is meant to be edited by users.<br />
<br />
;SolidTexture.hs<br />
:Perlin noise and other related texture functions.<br />
<br />
;Spd.hs<br />
:NFF file parser for SPD scenes.<br />
<br />
====Tracing Rays====<br />
<br />
Solid.hs defines a ray intersection thus:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
And these are our basic primitives:<br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec,<br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
<br />
Glome defines a ray-intersection function "rayint" that pattern matches against all of these primitives and returns an appropriate Rayint.<br />
<br />
For instance, let's look at the "disc" case, as it is quite simple:<br />
<br />
<haskell><br />
rayint :: Solid -> Ray -> Flt -> Texture -> Rayint<br />
rayint (Disc point norm radius_sqr) r d t =<br />
let (Ray orig dir) = r<br />
dist = plane_int_dist r point norm <br />
in if dist < 0 || dist > d <br />
then RayMiss<br />
else let pos = vscaleadd orig dir dist<br />
offset = vsub pos point<br />
in <br />
if (vdot offset offset) > radius_sqr<br />
then RayMiss<br />
else RayHit dist pos norm t<br />
</haskell><br />
<br />
"rayint" takes four arguments: the Solid to be intersected, the Ray to intersect with it, a Flt (shorthand for Float or Double, depending on how Vec.hs is configured), and a Texture. "rayint" is expected to return a RayHit value if a ray intersection exists within the distance given by "d". If there is more than one hit, rayint should return the closest one, but never an intersection that is behind the Ray's origin. Discs are flat, though, so we can never intersect one twice with a single ray.<br />
<br />
Glome extracts the Ray's origin and direction from "r", and then uses a function called "plane_int_dist" defined in Vec.hs. This returns the distance to the plane defined by a point on the plane and it's normal, and intersected by ray r.<br />
<br />
Then Glome checks if the distance is less than zero or more than the maximum allowed, and if so returns RayMiss.<br />
<br />
Otherwise, Glome computes the hit location from the Ray and distance to the plane. "vscaleadd" is another function from Vec.hs that takes one vector, and then adds a second vector after scaling the second vector by some scalar. By taking the Ray's (normalized) direction vector scaled by the distance to the plane and adding it to the Ray's origin, we get the hit location. (This technique is used in many of the ray-intersection tests.)<br />
<br />
Once we know the location on the disc's plane where our ray hit, we need to know if it is within the radius of the disc. For that, we compute an offset vector from the center of the disc ("point") to the hit location ("pos"). Then we want to check if this offset vector is less than the radius of the disc. Or, in other words:<br />
<br />
sqrt (offset.x^2 + offset.y^2 + offset.z^2) < r<br />
<br />
We can square both sides to get rid of the square route, and then observe that squaring the components of a vector is the same as taking the dot product of that vector with itself:<br />
<br />
vdot offset offset < r^2<br />
<br />
Also, Glome doesn't store the radius with a disc but rather it's radius squared (to avoid a multiply, since we don't often need to know the disc's actual radius), and that explains the last if statement.<br />
<br />
The RayHit constructor needs a little explanation, though. What are all those fields for?<br />
<br />
First, there's the distance to the nearest hit and the position. (You might notice some redundancy here, since the calling function could infer the distance to the nearest hit from the ray and the hit position, or the the hit position from the distance and the ray. We return both to save the trouble of recomputing values we've already determined.) "norm" is the vector perpendicular to the surface of the disc, used in lighting calculations. For discs, this is easy: the normal is stored as part of the disc's definition. For other objects (like Spheres or Cones), we might have to compute a normal. <br />
<br />
The texture passed in as an argument to "rayint" is simply returned in the returned Rayint record. All of the ray intersection cases behave this way except Tex, which overrides the texture with its own.<br />
<br />
As for the other basic primitives like Triangle, Sphere, Cylinder, Cone, Plane, and Box, Glome uses standard intersection tests that can be found in graphics textbooks (such as [http://pbrt.org/ Physically Based Rendering] and Graphics Gems volume one). <br />
<br />
A "Nothing" object is a special case: its ray intersector simply returns "RayMiss" regardless of input. The existence of "Nothing" is somewhat redundant, since it is equivalent to "Group []".<br />
<br />
The composite primitives (Group, Difference, Intersection, Tex, Bih, Instance, and Bound) are more interesting, as their ray-intersection tests are defined recursively. This recursion is what allows us to treat a complex object made up of many sub-objects the same as we would treat a simple base primitive like a Sphere, and in fact Glome makes no distinction whatsoever between base primitives and composite primitives.<br />
<br />
We'll look at Group as our composite ray-intersection test example:<br />
<br />
<haskell><br />
rayint (Group xs) r d t =<br />
let rig [] = RayMiss<br />
rig (x:xs) = nearest (rayint x r d t) (rig xs)<br />
in rig xs<br />
</haskell><br />
<br />
Here, "rayint" defines a simple function to traverse the list calling "rayint" for each primitive and returning the nearest hit. ("nearest" is defined in Solid.hs, and returns the nearest of two ray intersections, or RayMiss if they both miss.)<br />
<br />
This could be defined more succinctly with a fold:<br />
<br />
<haskell><br />
rayint (Group lst) r d t = foldl nearest RayMiss (map (\x -> rayint x r d t) lst)<br />
</haskell><br />
<br />
...and we only use the more verbose style for efficiency. (I'm not sure if I actually benchmarked if it was faster, so this might be worth checking.)<br />
<br />
One important thing to note about Group is that intersecting with a large group is very inefficient. That's why we have Bih. However, even Bih uses Groups as leaf nodes, so Groups are still important.<br />
<br />
There is a second ray intersection function called "shadow" that has a simpler type signature than "rayint":<br />
<br />
<haskell><br />
shadow :: Solid -> Ray -> Flt -> Bool<br />
</haskell><br />
<br />
"shadow" is used for shadow-ray occlusion tests. In order to test whether a particular point is lit by a particular light, a shadow ray is traced from the ray intersection point to the light. If there is something in the way, that light is in shadow and it does not contribute to the illumination at that point.<br />
<br />
For most scenes with more than one light, more shadow rays are traced than regular rays. Therefore, we want the shadow ray intersection tests to be as fast as possible. "shadow" does not require a texture, and it returns True if the ray hits the object or False if it misses.<br />
<br />
Let's look at the shadow test for Group:<br />
<br />
<haskell><br />
shadow (Group xs) r d =<br />
let sg [] = False<br />
sg (x:xs) = (shadow x r d) || (sg xs)<br />
in sg xs<br />
</haskell><br />
<br />
Note that "shadow" stops as soon as one of the shadow tests returns True.<br />
<br />
Primitives are not required to implement a shadow test. Glome defines a reasonable default case:<br />
<br />
<haskell><br />
shadow s r d =<br />
case (rayint s r d t_white) of<br />
RayHit _ _ _ _ -> True<br />
RayMiss -> False<br />
</haskell><br />
<br />
For base primitives, the performance penalty of using the full ray intersection test instead of a shadow test may be insignificant. However, composite primitives should always define a shadow test. Consider, for instance, if Group did not implement a shadow test: all it's children would be tested with "rayint" rather than "shadow", and if any of those objects have sub-objects, they will be tested with "rayint" as well! A single primitive type high in the tree that doesn't support "shadow" will force its entire subtree to be evaluated with "rayint".<br />
<br />
There is one case where "rayint" actually calls "shadow", rather than the other way around: Bound uses a shadow test to determine if the ray hits the bounding object or not.<br />
<br />
===Advanced Topics, and things that don't quite work yet===<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20706Glome tutorial2008-04-27T05:40:28Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
===A guide to understanding the source code===<br />
<br />
Editing scenes directly in Haskell makes it possible to use the pre-existing ray tracing infrastructure to help us create our scene. For instance, the "trace" function can be used to help us place one object on another; a plant growing on a non-flat surface, for instance. Also, we may want our scene to include geometric primitives of a type that Glome does not yet support, and so we might want to add our own.<br />
<br />
This section of the tutorial is for those who are interested in understanding not just how to use Glome, but how it works and how it can be extended.<br />
<br />
====File Reference====<br />
<br />
First, an overview of the source code files. Glome is currently split amongst eight source files and is about 2500 lines of code. These are the files:<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting code is: definitions of basic data types, ray-intersection, shadow, and inside/outside tests for all the supported primitives, and constructors for the various primitives.<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen. It also includes the shading algorithms.<br />
<br />
;Glome.hs<br />
:The main loop of the program. All OpenGL-related code resides in this module. Also included is a get_color function that accepts screen coordinates and a scene, and computes the ray for that screen coordinate, traces the ray, and returns the color.<br />
<br />
;TestScene.hs<br />
:This is what gets rendered if no input file is specified. This is meant to be edited by users.<br />
<br />
;SolidTexture.hs<br />
:Perlin noise and other related texture functions.<br />
<br />
;Spd.hs<br />
:NFF file parser for SPD scenes.<br />
<br />
====Tracing Rays====<br />
<br />
Solid.hs defines a ray intersection thus:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
And these are our basic primitives:<br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec,<br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
<br />
Glome defines a ray-intersection function "rayint" that pattern matches against all of these primitives and returns an appropriate Rayint.<br />
<br />
For instance, let's look at the "disc" case, as it is quite simple:<br />
<br />
<haskell><br />
rayint :: Solid -> Ray -> Flt -> Texture -> Rayint<br />
rayint (Disc point norm radius_sqr) r d t =<br />
let (Ray orig dir) = r<br />
dist = plane_int_dist r point norm <br />
in if dist < 0 || dist > d <br />
then RayMiss<br />
else let pos = vscaleadd orig dir dist<br />
offset = vsub pos point<br />
in <br />
if (vdot offset offset) > radius_sqr<br />
then RayMiss<br />
else RayHit dist pos norm t<br />
</haskell><br />
<br />
"rayint" takes four arguments: the Solid to be intersected, the Ray to intersect with it, a Flt (shorthand for Float or Double, depending on how Vec.hs is configured), and a Texture. "rayint" is expected to return a RayHit value if a ray intersection exists within the distance given by "d". If there is more than one hit, rayint should return the closest one, but never an intersection that is behind the Ray's origin. Discs are flat, though, so we can never intersect one twice with a single ray.<br />
<br />
Glome extracts the Ray's origin and direction from "r", and then uses a function called "plane_int_dist" defined in Vec.hs. This returns the distance to the plane defined by a point on the plane and it's normal, and intersected by ray r.<br />
<br />
Then Glome checks if the distance is less than zero or more than the maximum allowed, and if so returns RayMiss.<br />
<br />
Otherwise, Glome computes the hit location from the Ray and distance to the plane. "vscaleadd" is another function from Vec.hs that takes one vector, and then adds a second vector after scaling the second vector by some scalar. By taking the Ray's (normalized) direction vector scaled by the distance to the plane and adding it to the Ray's origin, we get the hit location. (This technique is used in many of the ray-intersection tests.)<br />
<br />
Once we know the location on the disc's plane where our ray hit, we need to know if it is within the radius of the disc. For that, we compute an offset vector from the center of the disc ("point") to the hit location ("pos"). Then we want to check if this offset vector is less than the radius of the disc. Or, in other words:<br />
<br />
sqrt (offset.x^2 + offset.y^2 + offset.z^2) < r<br />
<br />
We can square both sides to get rid of the square route, and then observe that squaring the components of a vector is the same as taking the dot product of that vector with itself:<br />
<br />
vdot offset offset < r^2<br />
<br />
Also, Glome doesn't store the radius with a disc but rather it's radius squared (to avoid a multiply, since we don't often need to know the disc's actual radius), and that explains the last if statement.<br />
<br />
The RayHit constructor needs a little explanation, though. What are all those fields for?<br />
<br />
First, there's the distance to the nearest hit and the position. (You might notice some redundancy here, since the calling function could infer the distance to the nearest hit from the ray and the hit position, or the the hit position from the distance and the ray. We return both to save the trouble of recomputing values we've already determined.) "norm" is the vector perpendicular to the surface of the disc, used in lighting calculations. For discs, this is easy: the normal is stored as part of the disc's definition. For other objects (like Spheres or Cones), we might have to compute a normal. <br />
<br />
The texture passed in as an argument to "rayint" is simply returned in the returned Rayint record. All of the ray intersection cases behave this way except Tex, which overrides the texture with its own.<br />
<br />
As for the other basic primitives like Triangle, Sphere, Cylinder, Cone, Plane, and Box, Glome uses standard intersection tests that can be found in graphics textbooks (such as [http://pbrt.org/ Physically Based Rendering] and Graphics Gems volume one). <br />
<br />
A "Nothing" object is a special case: its ray intersector simply returns "RayMiss" regardless of input. The existence of "Nothing" is somewhat redundant, since it is equivalent to "Group []".<br />
<br />
The composite primitives (Group, Difference, Intersection, Tex, Bih, Instance, and Bound) are more interesting, as their ray-intersection tests are defined recursively. This recursion is what allows us to treat a complex object made up of many sub-objects the same as we would treat a simple base primitive like a Sphere, and in fact Glome makes no distinction whatsoever between base primitives and composite primitives.<br />
<br />
We'll look at Group as our composite ray-intersection test example:<br />
<br />
<haskell><br />
rayint (Group xs) r d t =<br />
let rig [] = RayMiss<br />
rig (x:xs) = nearest (rayint x r d t) (rig xs)<br />
in rig xs<br />
</haskell><br />
<br />
Here, "rayint" defines a simple function to traverse the list calling "rayint" for each primitive and returning the nearest hit. ("nearest" is defined in Solid.hs, and returns the nearest of two ray intersections, or RayMiss if they both miss.)<br />
<br />
This could be defined more succinctly with a fold:<br />
<br />
<haskell><br />
rayint (Group lst) r d t = foldl nearest RayMiss (map (\x -> rayint x r d t) lst)<br />
</haskell><br />
<br />
...and we only use the more verbose style for efficiency. (I'm not sure if I actually benchmarked if it was faster, so this might be worth checking.)<br />
<br />
One important thing to note about Group is that intersecting with a large group is very inefficient. That's why we have Bih. However, even Bih uses Groups as leaf nodes, so Groups are still important.<br />
<br />
There is a second ray intersection function called "shadow" that has a simpler type signature than "rayint":<br />
<br />
<haskell><br />
shadow :: Solid -> Ray -> Flt -> Bool<br />
</haskell><br />
<br />
"shadow" is used for shadow-ray occlusion tests. In order to test whether a particular point is lit by a particular light, a shadow ray is traced from the ray intersection point to the light. If there is something in the way, that light is in shadow and it does not contribute to the illumination at that point.<br />
<br />
For most scenes with more than one light, more shadow rays are traced than regular rays. Therefore, we want the shadow ray intersection tests to be as fast as possible. "shadow" does not require a texture, and it returns True if the ray hits the object or False if it misses.<br />
<br />
Let's look at the shadow test for Group:<br />
<br />
<haskell><br />
shadow (Group xs) r d =<br />
let sg [] = False<br />
sg (x:xs) = (shadow x r d) || (sg xs)<br />
in sg xs<br />
</haskell><br />
<br />
Note that "shadow" stops as soon as one of the shadow tests returns True.<br />
<br />
Primitives are not required to implement a shadow test. Glome defines a reasonable default case:<br />
<br />
<haskell><br />
shadow s r d =<br />
case (rayint s r d t_white) of<br />
RayHit _ _ _ _ -> True<br />
RayMiss -> False<br />
</haskell><br />
<br />
For base primitives, the performance penalty of using the full ray intersection test instead of a shadow test may be insignificant. However, composite primitives should always define a shadow test. Consider, for instance, if Group did not implement a shadow test: all it's children would be tested with "rayint" rather than "shadow", and if any of those objects have sub-objects, they will be tested with "rayint" as well! A single primitive type high in the tree that doesn't support "shadow" will force its entire subtree to be evaluated with "rayint".<br />
<br />
There is one case where "rayint" actually calls "shadow", rather than the other way around: Bound uses a shadow test to determine if the ray hits the bounding object or not.<br />
<br />
===Advanced Topics, and things that don't quite work yet===<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20705Glome tutorial2008-04-27T03:24:57Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
===A guide to understanding the source code===<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting code is: definitions of basic data types, ray-intersection, shadow, and inside/outside tests for all the supported primitives, and constructors for the various primitives.<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen. It also includes the shading algorithms.<br />
<br />
;Glome.hs<br />
:The main loop of the program. All OpenGL-related code resides in this module. Also included is a get_color function that accepts screen coordinates and a scene, and computes the ray for that screen coordinate, traces the ray, and returns the color.<br />
<br />
;TestScene.hs<br />
:This is what gets rendered if no input file is specified. This is meant to be edited by users.<br />
<br />
;SolidTexture.hs<br />
:Perlin noise and other related texture functions.<br />
<br />
;Spd.hs<br />
:NFF file parser for SPD scenes.<br />
<br />
===Advanced Topics, and things that don't quite work yet===<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20704Glome tutorial2008-04-27T03:14:44Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
===A guide to understanding the source code===<br />
<br />
;Vec.hs<br />
:This is the vector library. It contains all the things you might want to do with vectors: add, subtract, normalize, take dot products and cross products, reverse a vector, etc... It also includes the transformation matrix code, and routines for transforming vectors, points, and normals. A data type "Flt" is defined for floating point numbers. Switching from Float to Double or vice versa is a matter of changing the definition of Flt in Vec.hs and CFlt in Clr.hs.<br />
<br />
;Clr.hs<br />
:Color library. Colors are records of three floats in RGB format. There's nothing surprising or particularly clever here.<br />
<br />
;Solid.hs<br />
:This is where most of the interesting code is: definitions of basic data types, ray-intersection, shadow, and inside/outside tests for all the supported primitives, and constructors for the various primitives.<br />
<br />
;Trace.hs<br />
:This contains the "trace" function, which converts a ray and a scene into a color to be drawn to the screen. It also includes the shading algorithms.<br />
<br />
<br />
===Advanced Topics, and things that don't quite work yet===<br />
<br />
==Navigation==<br />
<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20703Glome tutorial2008-04-27T02:56:00Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects it finds, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory; however, it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
In Glome, textures are not associated with individual geometric primitives. Instead, it uses a container object called "Tex":<br />
<br />
<haskell>Tex Solid Texture</haskell><br />
<br />
The Solid is the thing that we want to apply the texture to, and the Texture is the texture itself. Often, we might want to texture a single object by itself:<br />
<br />
<haskell>Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0)) </haskell><br />
<br />
This produces a red ball. Or we could texture a whole group of objects at once.<br />
<br />
A textured object is just a regular object, so what happens if we apply another Texture?<br />
<br />
<haskell>Tex (Tex (sphere (Vec 0 0 0) 1) (t_matte (Color 1 0 0))) (t_matte (Color 0 1 0))</haskell><br />
<br />
You might think this will produce a green ball, but in fact it produces a red one. The rule here is that the innermost texture has highest priority. Applying a texture to a large group of objects applies that texture only to the objects that don't already have a texture.<br />
<br />
As you might already have guessed, "t_matte" accepts a color, and produces a Texture. A Texture is a rather complicated data type with a simple definition:<br />
<br />
<haskell>type Texture = Rayint -> Material</haskell> <br />
<br />
It is a function that accepts a Rayint and returns a Material. A Rayint is a datatype used internally by Glome to represent the intersection of a ray and an object:<br />
<br />
<haskell><br />
data Rayint = RayHit {<br />
depth :: Flt,<br />
pos :: Vec,<br />
norm :: Vec,<br />
texture :: Texture<br />
} | RayMiss deriving Show<br />
</haskell><br />
<br />
The most useful field here is "pos" which is the XYZ coordinates of the location of the ray intersection, and few textures will need to access any of the other fields.<br />
<br />
Note that a ray can miss it's target object, in which case the value of the Rayint is "RayMiss". In general, a texture shouldn't need to worry about that case, since Glome wouldn't be evaluating the texture if the ray missed the object, but we'll do the pattern match anyways.<br />
<br />
A "Material" describes an objects material properties at a point. A texture that is a function of hit location might return a different Material depending on where the ray hit the object.<br />
<br />
<blockquote><br />
data Material = Material {clr :: Color, <br />
reflect, refract, ior, <br />
kd, shine :: !Flt} deriving Show<br />
</blockquote><br />
<br />
Materials are a relatively unwieldy datatype that is likely to change in future versions of Glome, but currently, it consists of a color and a handful of numbers.<br />
<br />
The "color" is equivalent to POV-Ray's "pigment", and is simply the color of the object. Reflect is a value (preferably between 0 and 1) that describes the reflectiveness of the object. For any value but 0, Glome will spawn another reflected ray off the surface for any ray that hits the object. Reflection should be used with care, since too many reflections will cause the scene to render very slow.<br />
<br />
"refract" and "ior" aren't used yet. "kd" is a diffuse illumination term. For most regular non-shiny objects, this should be set to one. For things like mirrors, it should be set very low.<br />
<br />
"shine" is an exponent used to compute specular highlights. These are the bright highlights seen on shiny objects. Glome uses Blinn highlighting.<br />
<br />
Unfortunately, there isn't currently any way to control the magnitude of Blinn highlighting (save by editing Trace.hs), but the size of the highlights can be controlled with "shine".<br />
<br />
We can now look at the definition of a few Textures and see how they work:<br />
<br />
<haskell><br />
m_matte c = (Material c 0 0 0 1 2)<br />
<br />
t_matte c = <br />
(\ri -> (Material c 0 0 0 1 2)) <br />
</haskell><br />
<br />
"t_matte" accepts a color and returns a function that takes a ray intersection as argument, and completely ignores it's contents, simply returning a diffuse material with the requested color.<br />
<br />
We can define some more materials:<br />
<br />
<haskell><br />
m_shiny_white :: Material<br />
m_shiny_white = (Material c_white 0.3 0 0 0.7 10)<br />
<br />
m_dull_gray :: Material<br />
m_dull_gray = (Material (Color 0.4 0.3 0.35) 0 0 0 0.2 1)<br />
<br />
m_mirror :: Material<br />
m_mirror = (Material (Color 0.8 0.8 1) 1 0 0 0.2 1000)<br />
</haskell><br />
<br />
And then use them in more interesting textures:<br />
<br />
<haskell><br />
t_mottled (RayHit _ pos norm _) =<br />
let val = perlin (vscale pos 3)<br />
m_interp m_mirror (m_matte (Color 0 0 1)) val<br />
<br />
--shouldn't happen<br />
t_mottled RayMiss = m_shiny_white<br />
</haskell><br />
<br />
"m_interp" is a function that interpolates between two materials according to some number passed in ("val", in this case). If "val" is zero, you get the first texture, if it's one, you get the second, and any number in between is a weighted combination of the two.<br />
<br />
"perlin" is a Perlin noise function, defined in "SolidTexture.hs". Perlin noise is a well-known and efficient algorithm for generating a three dimensional splotchy pattern that is a very useful building block for defining more complex textures.<br />
<br />
===A guide to understanding the source code===<br />
<br />
===Advanced Topics, and things that don't quite work yet===<br />
<br />
==Navigation==<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20698Glome tutorial2008-04-26T06:08:54Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory, however it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
===A guide to understanding the source code===<br />
<br />
===Advanced Topics, and things that don't quite work yet===<br />
<br />
==Navigation==<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20697Glome tutorial2008-04-26T06:07:18Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings. Also, it is not always obvious whether the overhead introduced by testing against the bounding object is outweighed by the reduced number of intersections against the bounded object.<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
BIH is one of many acceleration structures used in ray tracing. Other choices are: regular grids, BSP trees, octrees, bounding volume hierarchies (BVH), and kd-trees. Currently, Glome only supports BIH (though there is an earlier written in Ocaml that supports kd-trees as well).<br />
<br />
In general, BIH is well-behaved but there are a few cases to avoid when possible. For instance: try not to use very long skinny things, especially if they're overlapping a lot of other long skinny things. If you want to render a thousand toothpicks spilled on the floor, then you might want to consider representing each toothpick as a series of short cylinders instead of one long cylinder.<br />
<br />
Another problem with the bih constructor is that it doesn't know how to interpret complex hierarchies. For instance, if you pass a list containing a single transformation of a group of objects, then bih will treat it as a list of a single object.<br />
<br />
There is a useful helper function to flatten out complex hierarchies of bound objects, transformations, and groups. The function is "flatten_transform":<br />
<br />
<haskell><br />
flatten_transform :: Solid -> [Solid]<br />
flatten_transform (Group slds) =<br />
flatten_group $ concat (map flatten_transform slds)<br />
<br />
flatten_transform (Instance s xfm) =<br />
case s of <br />
Group slds -> flatten_transform $ group (map (\x -> transform x [xfm]) slds)<br />
Bound sa sb -> flatten_transform (transform sb [xfm])<br />
Instance sa xfm2 -> flatten_transform (transform s [xfm])<br />
_ -> [transform s [xfm]]<br />
<br />
flatten_transform (Bound sa sb) = flatten_transform sb<br />
</haskell><br />
<br />
flatten_transform throws away manually created bounding objects, and pushes all transformations out to the leaves of the tree. In many cases, this will mean that the scene will consume more memory, however it will probably render much faster.<br />
<br />
===Textures, Lighting===<br />
<br />
<br />
<br />
===Advanced Topics, and things that don't quite work yet===<br />
<br />
==Navigation==<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20696Glome tutorial2008-04-26T05:42:22Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
One caveat is that you should use transformations sparingly. For instance, rather than creating a unit sphere and then scaling it and moving it into position, it's better to use the sphere's constructor the way it was intended. A transformed sphere consumes more memory than just a sphere by itself, and rendering will be a little slower.<br />
<br />
Cones and cylinders, on the other hand, are already stored as transformations (except for the rare case when you really do want a Z-axis aligned cone or cylinder), so transforming them won't take up any extra space.<br />
<br />
===Bounding Objects===<br />
The preceding sections have (hopefully) shown everything you need to know to model the kinds of shapes you want to render. However, there are many equivalent ways to represent the same scene, and some of them render much faster than others. To understand why, you will need to know a little bit about how a ray tracer works.<br />
<br />
A ray tracer sends rays out from the camera into the scene, one ray per pixel. (Or more than one ray, if the ray tracer supports anti-aliasing, which Glome does not.) Each ray has to be tested for intersection with each object in the scene. (Conceptually, scenes in Glome are only comprised of a single object, but in practice intersecting with that object usually means doing a lot of ray-intersections with sub-objects.) For scenes with many objects, this can be very slow. Supposing we have a thousand spheres in our scene, and at the default resolution of 720x480, we have 345,600 rays. This means that for each image, we have to do 345.6 million ray-sphere intersection tests! Hardware may be getting faster, but we need to do better than that if we want anything near reasonable rendering speed.<br />
<br />
Fortunately, we don't really need to do that many ray-intersection tests. What we can do instead is place bounding objects around the complicated bits of our scene. If a ray doesn't hit the bounding object, then we know it won't hit anything inside the bounding object, and we don't have to do any of those intersection tests.<br />
<br />
Glome has a "Bound" primitive just for this purpose: If you have some complicated solid "a" and a simple bounding solid (such as a sphere or a box) "b", you can declare the bounded object as <haskell>Bound b a</haskell> (the simple object comes first).<br />
<br />
You could do a similar thing with Intersection, but Bound is a bit more efficient.<br />
<br />
Bounding objects are great for improving performance, but they're unwieldy. Finding the smallest bounding object that will enclose another object is a non-trivial task, and it's easy to make mistakes, leading to incorrect renderings.<br />
<br />
Fortunately, there's an easier way, and we've mentioned it before: use "bih".<br />
<br />
The "bih" constructor takes a list of primitives and sorts them into a hierarchy of bounding planes called a Bounding Interval Hierarchy.<br />
<br />
(more information on this method can be found here: Carsten Wächter and Alexander Keller,[http://ainc.de/Research/BIH.pdf Instant Ray Tracing: The Bounding Interval Hierarchy])<br />
<br />
<br />
<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
===Textures, Lighting===<br />
<br />
===Advanced Topics, and things that don't quite work yet===<br />
<br />
==Navigation==<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20695Glome tutorial2008-04-26T04:44:58Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
One caveat on difference is that you shouldn't ever create objects with infinitely thin walls. They might render correctly, or they might not depending on floating point approximations.<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a [http://en.wikipedia.org/wiki/Dodecahedron dodecahedron] succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron]. (Similarly, we can create an Icosahedron by using the coordinates of the verticies of a dodecahedron.)<br />
<br />
I put a sphere in the list of objects as an optimization. Any ray that misses the first sphere can be assumed to miss the whole object. This also allows Glome to compute an efficient bounding box for the intersection: all the planes are infinite objects, so they have infinite bounding boxes.<br />
<br />
===Transformations===<br />
Transformations are a convenient way of moving, rotating, and stretching objects.<br />
<br />
To move an object a by some vector v, we can say:<br />
<br />
<haskell><br />
transform a [translate v]<br />
</haskell><br />
<br />
In pure Haskell there are no side effects, so this doesn't actually move "a" but rather creates a copy that has been moved. This is, in fact, an efficient way of creating many copies of a complex object without using much extra space. (Each transform uses 24 floating point numbers to store a matrix and its inverse that describe the transformation.)<br />
<br />
"transform" takes a list, and we can combine several transformation and they behave as you might expect:<br />
<br />
<haskell><br />
transform a [translate (Vec 0 3 0),<br />
rotate (Vec 0 1 0) (deg 90),<br />
scale (Vec 2 2 2)]<br />
</haskell><br />
<br />
This moves object "a" up three units, then rotates 90 degrees about the Y axis, and then stretches the object equally on all three axes by a factor of 2.<br />
<br />
Stretching and rotations happen about the origin, so you might need to translate an object to the origin, rotate it, then translate it back in order for the translations to do what you want.<br />
<br />
Interestingly, performing multiple transformations at a time doesn't produce any more overhead than just performing one transformation; internally, any combination of transformations can be represented as a single matrix.<br />
<br />
(Arcane trivia: internally, "transform" reverses the list of transformations before applying them.)<br />
<br />
===Bounding Objects===<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
===Textures, Lighting===<br />
<br />
==Navigation==<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20694Glome tutorial2008-04-26T04:21:14Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
Sometimes it's convenient to treat a whole group of object as if they were a single object, and for that purpose we have Group. One reason we might want to do this has already been mentioned: the scene needs to be described in terms of a single object. There are several others. We might want to apply a texture or move or rotate a whole group of objects at once, instead of treating them individually. Using groups is quite simple. For instance:<br />
<br />
<haskell><br />
Group [ (sphere (Vec (-1) 0 0) 2), <br />
(sphere (Vec 1 0 0) 2),<br />
(sphere (Vec 0 (-1) 0) 2),<br />
(sphere (Vec 0 1 0) 2) ]<br />
</haskell><br />
<br />
This gives us four spheres we can treat as a single object.<br />
<br />
Group has a special constructor, "group", that does a little bit of extra optimization for us:<br />
<br />
<haskell><br />
group :: [Solid] -> Solid<br />
group [] = Solid.Nothing<br />
group (sld:[]) = sld<br />
group slds =<br />
Group (flatten_group slds)<br />
</haskell><br />
<br />
If you try to create an empty group, Glome will replace it with a primitive of type Nothing, which is a degenerate object that has no appearance. ("Nothing" conflicts with the Maybe type in the Haskell prelude, so we say Solid.Nothing to disambiguate.) <br />
<br />
If you try to create a group that contains only one object, Glome throws the group away and just uses that object directly.<br />
<br />
If you try to create a group that contains other groups, Glome will consolidate those into one big group. (This is what "flatten_group" does.)<br />
<br />
In general, it's better to use "group" than "Group". Even better than "group", though, is "bih", which behaves similarly to group but performs much better. I'll explain why later.<br />
<br />
Haskell has some convenient syntax for creating lists. For instance, if you want to create a lot of spheres, you can use:<br />
<br />
<haskell><br />
let lattice = <br />
let n = 20<br />
in [sphere (Vec x y z) 0.1 | x <- [(-n)..n],<br />
y <- [(-n)..n], <br />
z <- [(-n)..n]]<br />
</haskell><br />
<br />
This gives you a list of spheres arranged in a 41x41x41 3-D grid. You can then create a group out of it:<br />
<br />
<haskell><br />
group lattice<br />
</haskell><br />
<br />
But it will render very slowly. If you use "bih" instead of "group", it will render much faster. [http://syn.cs.pdx.edu/~jsnow/glome/Glome.hs-lattice-1e6-720p.png Here] is a rendering of over a million reflective spheres. It took about ten minutes or so to render, largely because of the reflections.<br />
<br />
We can use cones and spheres together in a useful way by stringing a series of cones together with a sphere to hide each joint.<br />
<br />
<haskell><br />
spiral = [ ((Vec ((sin (rot n))*n) <br />
((cos (rot n))*n) <br />
(n-3)), (n/15)) | n <- [0, 0.05..6]]<br />
<br />
coil = bih (zipWith (\ (p1,r1) (p2,r2) -> (Solid.group [(cone p1 r1 p2 r2), <br />
(sphere p1 r1)] )) <br />
spiral <br />
(tail spiral))<br />
</haskell><br />
<br />
Here, "spiral" is a list of (location,radius) tuples, and we use [http://haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html#v%3AzipWith zipWith] to create cylinders and cones from pairs of (location,radius) tuples. We save the result into the variable "coil".<br />
<br />
===CSG===<br />
<br />
The basic primitives give us something to work with, and we might make reasonably complex scenes from them, but many simple objects are still difficult or impossible to create, unless we approximate them with triangles.<br />
<br />
Constructive Solid Geometry (CSG) gives us the ability to combine objects in more interesting ways than we can with "group".<br />
<br />
With a Difference object, we can subtract one object from another. For instance, to make a pipe, we could start with a cylinder, and then subtract a cylinder with a smaller radius. Or, to make a house, we might start with a box, and then subtract a smaller box to make the interior space, and then subtract more boxes to make space for windows and doors.<br />
<br />
Creating a difference is simple: it's just <haskell>Difference a b</haskell>, where b is the solid subtracted from a.<br />
<br />
In order for CSG to be meaningful, it needs to be performed with objects that have a well-defined inside and outside (like planes, spheres, cones, and cylinders, but not triangles or discs). It won't break anything if you use objects that don't have volume, but you might not get the results you want.<br />
<br />
CSG can be performed on composite objects, like group or bih, and they can be nested. (Note, however, that not all combinations have been tested, so things might not work the way you expect. If this happens, send me an email about it.)<br />
<br />
Intersection is like Difference, but it takes a list of objects, and the resulting object is the overlap of all those objects. Planes are very useful in intersections; we could easily define a box as the intersection of six axis-aligned planes, with their normals all facing outward.<br />
<br />
We can define a dodecahedron succinctly:<br />
<br />
<haskell><br />
dodecahedron pos r =<br />
let gr = (1+(sqrt 5))/2 -- golden ratio, 1.618033988749895<br />
n11 = [(-r),r]<br />
ngrgr = [(-gr)*r,gr*r]<br />
points = [Vec 0 y z | y <- n11, z <- ngrgr] ++<br />
[Vec x 0 z | z <- n11, x <- ngrgr] ++<br />
[Vec x y 0 | x <- n11, y <- ngrgr]<br />
pln x = (Plane (vnorm x) (r+(vdot (vnorm x) pos)))<br />
in<br />
Intersection ((sphere pos (1.26*r)):(map pln points))<br />
</haskell><br />
<br />
This is a function that takes a position and a radius, and generates a dodecahedron circumscribed about the sphere with that center and radius.<br />
<br />
The plane normal vectors are taken from the coordinates for the verticies of an [http://en.wikipedia.org/wiki/Icosahedron icosahedron].<br />
===Transformations===<br />
<br />
===Bounding Objects===<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
===Textures, Lighting===<br />
<br />
==Navigation==<br />
* [[Glome]]</div>Jsnowhttps://wiki.haskell.org/index.php?title=Glome_tutorial&diff=20693Glome tutorial2008-04-26T03:16:17Z<p>Jsnow: </p>
<hr />
<div>==Installing Glome==<br />
<br />
First, you need to install the software. I will assume that you already have ghc and the Haskell OpenGL libraries installed, and are comfortable with "tar" and the like. See [http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package How to install a Cabal package]. Familiarity with Haskell is not required, but it will help.<br />
<br />
The source code is available from [http://hackage.haskell.org/cgi-bin/hackage-scripts/package/glome-hs hackage]. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:<br />
<br />
runhaskell Setup.lhs configure --prefix=$HOME --user<br />
runhaskell Setup.lhs build<br />
runhaskell Setup.lhs install<br />
<br />
Glome doesn't really need to be installed in order to run it. If you'd prefer, you can invoke it directly from the build directory as "./dist/build/glome/glome".<br />
<br />
If everything works, a window should open and you should (after a pause) see a test scene with a variety of geometric shapes. If it doesn't work, then let [http://syn.cs.pdx.edu/~jsnow me] know.<br />
<br />
==Command line options==<br />
<br />
These are pretty sparse at the moment. You can specify an input scene file in NFF format with the "-n [filename]" option, and there is one such scene included with Glome, a standard [http://tog.acm.org/resources/SPD/ SPD] level-3 sphereflake.<br />
<br />
NFF isn't very expressive (and it was never intended to be), so I won't say much about it here. Glome supports most of the basic features of NFF except for refraction. My approach to polygon tesselation is also questionable: the SPD "gears" scene, for instance, doesn't render correctly.<br />
<br />
You may have to adjust the lighting to get satisfactory results (i.e. by adjusting the value of "intensity" in "shade" function in the Trace.hs file and recompiling). NFF doesn't define a specific intensity, and I'm not sure what sort of falloff (if any) Eric Haines used when he rendered the reference images.<br />
<br />
==Describing Scenes in Haskell==<br />
<br />
Ideally, using Glome would be a matter of firing up [http://www.blender.org/ Blender] and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.<br />
<br />
Unfortunately, Glome isn't able to import files from any standard 3-d format except NFF (which isn't typically used for anything but benchmark scenes).<br />
<br />
So, with only limited import functionality, how do we model complex scenes?<br />
<br />
One option we have left is to describe our scene directly in Haskell, and then compile the description and link it with the Glome binary. This is the approach we will be following for the remainder of this tutorial.<br />
<br />
This isn't quite as difficult as it sounds. [http://povray.org/ POV-Ray], for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.<br />
<br />
Glome was, in fact, quite heavily influenced by POV-Ray, so anyone familiar with POV's SDL and Haskell should be able to write scenes for Glome without much trouble.<br />
<br />
Unlike POV-Ray, in which the SDL is separate from the implementation language (C, or more recently, C++), in Glome there is no distinction. In that sense, Glome is more of an API than a standalone rendering system.<br />
<br />
The default scene, which is loaded if the user does not specify an input file on the command line, is defined in TestScene.hs. To define a new scene, you must edit this file and then recompile the source.<br />
<br />
===TestScene.hs: Camera, Lights, and the minimal scene===<br />
TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.<br />
<br />
<haskell><br />
scn :: IO Scene<br />
scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)<br />
</haskell><br />
<br />
"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument. <br />
<br />
"scn" uses the IO monad, in case we want to load a file from disk. We won't be doing that in any of our examples, so you can safely ignore the "IO" part. It returns an object of type "Scene".<br />
<br />
A Scene is described like this in Solid.hs:<br />
<br />
<haskell><br />
data Scene = Scene {sld :: Solid, <br />
lights :: [Light], <br />
cam :: Camera, <br />
dtex :: Texture, <br />
bground :: Color} deriving Show<br />
</haskell><br />
<br />
So, in order to construct a valid scene we need to put something into all the fields.<br />
<br />
The first field takes a Solid. A Solid is any of the basic primitive types that Glome supports. These might be triangles, spheres, cones, etc... <br />
<br />
You might wonder why we only need one primitive to define a scene. Certainly, we'd want to have a scene that contains more than a single sphere!<br />
<br />
The solution is that Glome includes several primitive types that let us create more complex Solids out of simple ones. For instance, a Group is a list of Solids, and Glome treats that list as if it was a single Solid. This will be discussed in more detail later on.<br />
<br />
A light is defined by a Color and a Vec:<br />
<br />
<haskell><br />
data Light = Light {litpos :: !Vec,<br />
litcol :: !Color} deriving Show<br />
</haskell><br />
<br />
A "Vec" is a vector of three floating point numbers, while a "Color" is also three floats as red, green, and blue values. (This may change in the future: RGB isn't necessarily the best representation for colors.)<br />
<br />
(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)<br />
<br />
We can define a light like so: <br />
<haskell><br />
Light (Vec (-3) 5 8) (Color 1.5 2 2)<br />
</haskell><br />
<br />
Note that the rgb values don't have to be between 0 and 1. In fact, we may wish to make them quite a bit larger if they're far away.<br />
<br />
Also note that a decimal point isn't mandatory. Haskell is smart enough to infer that the Color constructor expects a float. The parentheses around the "-3", on the other hand, are required.<br />
<br />
The square brackets is the definition of Scene tells us that we need a list of lights rather than a single light, and we can turn our single light into a list simply by enclosing it in square brackets. We could also use the empty list [], but then our scene would be completely black except the background.<br />
<br />
A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:<br />
<br />
<haskell><br />
camera :: Vec -> Vec -> Vec -> Flt -> Camera<br />
camera pos at up angle =<br />
let fwd = vnorm $ vsub at pos<br />
right = vnorm $ vcross up fwd<br />
up_ = vnorm $ vcross fwd right<br />
cam_scale = tan ((pi/180)*(angle/2))<br />
in<br />
Camera pos fwd<br />
(vscale up_ cam_scale) <br />
(vscale right cam_scale)<br />
</haskell><br />
<br />
It's arguments are: a point defining it's position, another point defining where it's looking, an "up" vector, and an angle. At this point, we need to decide which direction is up. I usually pick the "Y" axis as pointing up, So, to set up a camera at position <20,3,0> looking at the origin <0,0,0>, and a 45 degree field of view (measured from the top of the image to the bottom, not right to left or diagonal) we might write:<br />
<br />
<haskell><br />
camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45<br />
</haskell><br />
<br />
"dtex" and "background" define a default texture and a background color. The background color is the color if we miss everything in the scene. The defaults should work okay for the present.<br />
<br />
===Spheres, Triangles, Etc..===<br />
<br />
Now with all that out of the way, we can describe some geometry. As we mentioned earlier, every primitive is of type "Solid". The definition of Solid is quite long: <br />
<br />
<haskell><br />
data Solid = Sphere {center :: Vec, <br />
radius, invradius :: Flt}<br />
| Triangle {v1, v2, v3 :: Vec}<br />
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}<br />
| Disc Vec Vec Flt -- position, normal, r*r<br />
| Cylinder Flt Flt Flt -- radius height1 height2<br />
| Cone Flt Flt Flt Flt -- r clip1 clip2 height<br />
| Plane Vec Flt -- normal, offset from origin<br />
| Box Bbox<br />
| Group [Solid]<br />
| Intersection [Solid]<br />
| Bound Solid Solid<br />
| Difference Solid Solid<br />
| Bih {bihbb :: Bbox, bihroot :: BihNode}<br />
| Instance Solid Xfm<br />
| Tex Solid Texture<br />
| Nothing deriving Show <br />
</haskell><br />
(this list has been simplified a bit: the actual code may be different)<br />
<br />
The simplest primitive is the sphere and that's where we'll start. (Its ubiquity in ray tracing might lead some to believe that it's the only primitive ray tracers are any good at rendering.)<br />
<br />
The standard constructor takes a radius and the reciprocal of the radius: this is for efficiency, to avoid a division (which is typically much slower than multiplication). We don't really want to specify the inverse radius every time, so there's a simpler constructor we'll use instead (note the lower case vs upper case):<br />
<br />
<haskell><br />
sphere :: Vec -> Flt -> Solid<br />
sphere c r =<br />
Sphere c r (1.0/r)<br />
</haskell><br />
<br />
We can, for instance, construct a Sphere at the origin with radius 3 like this:<br />
<br />
<haskell>sphere (Vec 0 0 0) 3</haskell><br />
<br />
Triangles are described by the coordinates of their vertices. There is a second kind of triangle, "TriangleNorm" that deserves a little bit of explanation. This second kind of triangle allows the user to specify a normal vector at each vertex. The normal vector is a unit vector perpendicular to the surface. Usually, this is computed automatically. However, sometimes the resulting image looks too angular, and by interpolating the normal vectors, Glome can render a curve that appears curved even if it really isn't.<br />
<br />
This is, perhaps, an inelegant trick, but it works well. Sometimes, we'll be able to define surfaces exactly without approximation, but many models are only available as triangle meshes. Also, triangles may be useful to approximate shapes that Glome doesn't support natively (like toruses).<br />
<br />
Discs are another simple primitive that just happens to have a very simple, fast ray-intersection test. They are defined by a center point, a normal vector, and a radius. The surface of the Disc is oriented perpendicular to the normal. The radius is actually specified as radius squared, so you need to be aware of that if you're going to use them.<br />
<br />
Planes are even simpler than the Disc, they're defined as a normal vector and a perpendicular offset from the origin. Essentially, a plane is a half-space; everything on one side is inside, and everything on the other side (the direction pointed at by the normal) is on the outside.<br />
<br />
A (usually) more convenient way to specify a Plane is with a point on the surface, and a normal, and a function that does just that has been provided:<br />
<br />
<haskell><br />
plane :: Vec -> Vec -> Solid<br />
plane orig norm_ = Plane norm d<br />
where norm = vnorm norm_<br />
d = vdot orig norm<br />
</haskell><br />
<br />
If we want a horizon stretching to infinity, we simply add a plane oriented to the Y axis (assuming that's our current definition of "up").<br />
<br />
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell><br />
<br />
Glome supports Z-axis aligned cones and cylinders. The cones are perhaps more accurately described as tapered cylinders; they need not come to a point.<br />
<br />
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:<br />
<br />
<haskell><br />
cylinder_z :: Flt -> Flt -> Flt -> Solid<br />
cylinder_z r h1 h2 = Cylinder r h1 h2<br />
<br />
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid<br />
cone_z r h1 h2 height = Cone r h1 h2 height<br />
<br />
-- construct a general cylinder from p1 to p2 with radius r<br />
cylinder :: Vec -> Vec -> Flt -> Solid<br />
cylinder p1 p2 r =<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
in Instance (cylinder_z r 0 len)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ])<br />
<br />
-- similar for cone<br />
cone :: Vec -> Flt -> Vec -> Flt -> Solid<br />
cone p1 r1 p2 r2 =<br />
if r1 < r2 <br />
then cone p2 r2 p1 r1<br />
else if r1-r2 < delta<br />
then cylinder p1 p2 r2<br />
else<br />
let axis = vsub p2 p1<br />
len = vlen axis<br />
ax1 = vscale axis (1/len)<br />
(ax2,ax3) = orth ax1 <br />
height = (r1*len)/(r1-r2) -- distance to end point<br />
in<br />
Instance (cone_z r1 0 len height)<br />
(compose [ (xyz_to_uvw ax2 ax3 ax1),<br />
(translate p1) ]) <br />
</haskell><br />
<br />
cone_z and cylinder_z don't do anything the regular constructors don't do, but "cylinder" and "cone" are much more interesting. "cylinder" takes a start point and an end point and a radius, and creates a cone whose axis stretches from one point to the other. The "cone" constructor is similar, but it takes a radius for each end. Note that if you call "cone" with an identical radius at both ends, it automatically simplifies it to a cylinder. We'll see how to use cones effectively in the next section.<br />
<br />
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:<br />
<br />
<haskell><br />
box :: Vec -> Vec -> Solid<br />
box p1 p2 =<br />
Box (Bbox p1 p2)<br />
</haskell><br />
<br />
===Groups===<br />
<br />
===CSG===<br />
<br />
===Transformations===<br />
<br />
===Bounding Objects===<br />
<br />
===The Bounding Interval Hierarchy===<br />
<br />
===Textures, Lighting===<br />
<br />
==Navigation==<br />
* [[Glome]]</div>Jsnow