Difference between revisions of "Glome tutorial"

From HaskellWiki
Jump to navigation Jump to search
Line 116: Line 116:
 
===Spheres, Triangles, Etc..===
 
===Spheres, Triangles, Etc..===
   
  +
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:
  +
  +
<haskell>
  +
data Solid = Sphere {center :: Vec,
  +
radius, invradius :: Flt}
  +
| Triangle {v1, v2, v3 :: Vec}
  +
| TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}
  +
| Disc Vec Vec Flt -- position, normal, r*r
  +
| Cylinder Flt Flt Flt -- radius height1 height2
  +
| Cone Flt Flt Flt Flt -- r clip1 clip2 height
  +
| Plane Vec Flt -- normal, offset from origin
  +
| Box Bbox
  +
| Group [Solid]
  +
| Intersection [Solid]
  +
| Bound Solid Solid
  +
| Difference Solid Solid
  +
| Bih {bihbb :: Bbox, bihroot :: BihNode}
  +
| Instance Solid Xfm
  +
| Tex Solid Texture
  +
| Nothing deriving Show
  +
</haskell>
  +
(this list has been simplified a bit: the actual code may be different)
  +
  +
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.)
  +
  +
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):
  +
  +
<haskell>
  +
sphere :: Vec -> Flt -> Solid
  +
sphere c r =
  +
Sphere c r (1.0/r)
  +
</haskell>
  +
  +
We can, for instance, construct a Sphere at the origin with radius 3 like this:
  +
  +
<haskell>sphere (Vec 0 0 0) 3</haskell>
  +
  +
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.
  +
  +
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).
  +
  +
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.
  +
  +
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.
  +
  +
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:
  +
  +
<haskell>
  +
plane :: Vec -> Vec -> Solid
  +
plane orig norm_ = Plane norm d
  +
where norm = vnorm norm_
  +
d = vdot orig norm
  +
</haskell>
  +
  +
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").
  +
  +
<haskell>plane (Vec 0 0 0) (Vec 0 1 0)</haskell>
  +
  +
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.
  +
  +
As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:
  +
  +
<haskell>
  +
cylinder_z :: Flt -> Flt -> Flt -> Solid
  +
cylinder_z r h1 h2 = Cylinder r h1 h2
  +
  +
cone_z :: Flt -> Flt -> Flt -> Flt -> Solid
  +
cone_z r h1 h2 height = Cone r h1 h2 height
  +
  +
-- construct a general cylinder from p1 to p2 with radius r
  +
cylinder :: Vec -> Vec -> Flt -> Solid
  +
cylinder p1 p2 r =
  +
let axis = vsub p2 p1
  +
len = vlen axis
  +
ax1 = vscale axis (1/len)
  +
(ax2,ax3) = orth ax1
  +
in Instance (cylinder_z r 0 len)
  +
(compose [ (xyz_to_uvw ax2 ax3 ax1),
  +
(translate p1) ])
  +
  +
-- similar for cone
  +
cone :: Vec -> Flt -> Vec -> Flt -> Solid
  +
cone p1 r1 p2 r2 =
  +
if r1 < r2
  +
then cone p2 r2 p1 r1
  +
else if r1-r2 < delta
  +
then cylinder p1 p2 r2
  +
else
  +
let axis = vsub p2 p1
  +
len = vlen axis
  +
ax1 = vscale axis (1/len)
  +
(ax2,ax3) = orth ax1
  +
height = (r1*len)/(r1-r2) -- distance to end point
  +
in
  +
Instance (cone_z r1 0 len height)
  +
(compose [ (xyz_to_uvw ax2 ax3 ax1),
  +
(translate p1) ])
  +
</haskell>
  +
  +
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.
  +
  +
Boxes are axis-aligned, and can be created with a constructor that takes two corner points:
  +
  +
<haskell>
  +
box :: Vec -> Vec -> Solid
  +
box p1 p2 =
  +
Box (Bbox p1 p2)
  +
</haskell>
   
 
===Groups===
 
===Groups===

Revision as of 03:16, 26 April 2008

Installing Glome

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 How to install a Cabal package. Familiarity with Haskell is not required, but it will help.

The source code is available from hackage. You must untar the file ("tar xvfz [filename]", "cd glome-hs[version]") , and then build the binary with:

runhaskell Setup.lhs configure --prefix=$HOME --user
runhaskell Setup.lhs build
runhaskell Setup.lhs install

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".

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 me know.

Command line options

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 SPD level-3 sphereflake.

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.

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.

Describing Scenes in Haskell

Ideally, using Glome would be a matter of firing up Blender and editing 3-d geometry in a graphical, interactive way and then exporting the scene to Glome, which would do the final render.

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).

So, with only limited import functionality, how do we model complex scenes?

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.

This isn't quite as difficult as it sounds. POV-Ray, for instance, has a very user-friendly scene description language (SDL), and many artists type their scenes in directly as text.

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.

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.

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.

TestScene.hs: Camera, Lights, and the minimal scene

TestScene.hs import a number of modules at the beginning, and it contains a handful of objects and then defines a single function.

 scn :: IO Scene
 scn = return (Scene geom lits cust_cam (t_matte (Color 0.8 0.5 0.4)) c_sky)

"scn" is called from Glome.hs to specify a scene if there wasn't one passed in as a command line argument.

"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".

A Scene is described like this in Solid.hs:

data Scene = Scene {sld     :: Solid, 
                    lights  :: [Light], 
                    cam     :: Camera, 
                    dtex    :: Texture, 
                    bground :: Color} deriving Show

So, in order to construct a valid scene we need to put something into all the fields.

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...

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!

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.

A light is defined by a Color and a Vec:

data Light = Light {litpos :: !Vec,
                    litcol :: !Color} deriving Show

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.)

(See Vec.hs and Clr.hs for the definitions and useful functions for dealing with vectors and colors, respectively.)

We can define a light like so:

Light (Vec (-3) 5 8) (Color 1.5 2 2)

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.

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.

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.

A camera describes the location and orientation of the viewer. There is a function for creating a camera defined in Solid.hs:

camera :: Vec -> Vec -> Vec -> Flt -> Camera
camera pos at up angle =
 let fwd   = vnorm $ vsub at pos
     right = vnorm $ vcross up fwd
     up_   = vnorm $ vcross fwd right
     cam_scale = tan ((pi/180)*(angle/2))
 in
  Camera pos fwd
         (vscale up_ cam_scale) 
         (vscale right cam_scale)

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:

 camera (vec 20 3 0) (vec 0 0 0) (vec 0 1 0) 45

"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.

Spheres, Triangles, Etc..

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:

data Solid =  Sphere {center :: Vec, 
                      radius, invradius :: Flt}
            | Triangle {v1, v2, v3 :: Vec}
            | TriangleNorm {v1, v2, v3, n1, n2, n3 :: Vec}
            | Disc Vec Vec Flt  -- position, normal, r*r
            | Cylinder Flt Flt Flt -- radius height1 height2
            | Cone Flt Flt Flt Flt -- r clip1 clip2 height
            | Plane Vec Flt -- normal, offset from origin
            | Box Bbox
            | Group [Solid]
            | Intersection [Solid]
            | Bound Solid Solid
            | Difference Solid Solid
            | Bih {bihbb :: Bbox, bihroot :: BihNode}
            | Instance Solid Xfm
            | Tex Solid Texture
            | Nothing deriving Show

(this list has been simplified a bit: the actual code may be different)

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.)

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):

sphere :: Vec -> Flt -> Solid
sphere c r =
 Sphere c r (1.0/r)

We can, for instance, construct a Sphere at the origin with radius 3 like this:

sphere (Vec 0 0 0) 3

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.

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).

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.

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.

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:

plane :: Vec -> Vec -> Solid
plane orig norm_ = Plane norm d
 where norm = vnorm norm_
       d = vdot orig norm

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").

plane (Vec 0 0 0) (Vec 0 1 0)

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.

As is, the primitives are fairly useless unless we really want a Z-aligned cone or cylinder. Fortunately, Glome provides more convenient constructors:

cylinder_z :: Flt -> Flt -> Flt -> Solid
cylinder_z r h1 h2 = Cylinder r h1 h2

cone_z :: Flt -> Flt -> Flt -> Flt -> Solid
cone_z r h1 h2 height = Cone r h1 h2 height

-- construct a general cylinder from p1 to p2 with radius r
cylinder :: Vec -> Vec -> Flt -> Solid
cylinder p1 p2 r =
 let axis = vsub p2 p1
     len  = vlen axis
     ax1  = vscale axis (1/len)
     (ax2,ax3) = orth ax1 
 in Instance (cylinder_z r 0 len)
             (compose [ (xyz_to_uvw ax2 ax3 ax1),
                        (translate p1) ])
                        
-- similar for cone
cone :: Vec -> Flt -> Vec -> Flt -> Solid
cone p1 r1 p2 r2 =
 if r1 < r2 
 then cone p2 r2 p1 r1
 else if r1-r2 < delta
      then cylinder p1 p2 r2
      else
        let axis = vsub p2 p1
            len  = vlen axis
            ax1  = vscale axis (1/len)
            (ax2,ax3) = orth ax1 
            height = (r1*len)/(r1-r2) -- distance to end point
        in
         Instance (cone_z r1 0 len height)
                  (compose [ (xyz_to_uvw ax2 ax3 ax1),
                             (translate p1) ])

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.

Boxes are axis-aligned, and can be created with a constructor that takes two corner points:

box :: Vec -> Vec -> Solid
box p1 p2 =
 Box (Bbox p1 p2)

Groups

CSG

Transformations

Bounding Objects

The Bounding Interval Hierarchy

Textures, Lighting

Navigation