A Look at .ray Files

What follows is a simple introduction to the file format used by the raytracer. This should help you write new scenes. Also, it should give you some sense of how to extend the file format with new declarations to support any novel features you add to the raytracer.

The general approach taken is to break down the process of reading the input file into two steps. The first step turns the file into a parse tree, with no regard for the correctness of the content (parse.cpp). This step encapsulates all the annoying details of writing a parser, such as breaking the input into tokens and detecting comments. The second step walks the parse tree and outputs a scene (read.cpp). Because this is a transformation of a data structure and not of a stream of characters, it's much easier to work with. If you're thinking of extending the file format in some way, this is the step you should be modifying in almost all cases.

The Grammar

Because of the two step process, the syntax for raytracer input files is extremely simple. The parser will accept any file containing a tag line followed by a sequence of objects.

For this version of the raytracer, the tag line must be

SBT-raytracer 1.0
And must appear at the very start of the file.

An object can be one of several different things:

Type Description Examples
Scalars: Any integer or floating point number is an object 4
-1
0.00005
-1E6
String Literals: String literals, beginning and ending with a double quote and possibly containing C-style escaped characters, are objects. "Hello"
"blah blah\nblah"
IDs: Any C-style identifier (a string of alphanumeric characters) is an object. foo
bar
baz
dogs_everywhere
Booleans: The special reserved IDs true and false are boolean objects. true
false
Tuples: A tuple object corresponds to a vector of subobjects. It consists of an open parenthesis, a comma-delimited sequence of objects, and a close parenthesis. ()
(1.0,1.0,1.0)
(1,"hello",foo,(7,6))
Dictionaries: A dictionary is like a struct in C, but the field names are unknown in advance. It's written as an open brace, a semicolon-delimited list of assignments of objects to IDs, and a close brace. {}
{ silly = true }
{ position = (1.0,1.0,2.0); radius = 3 }
Named Objects: Any object can be named. Naming an object simply means putting an ID before it. The ID becomes an additional tag for the object. orange "crush"
sphere { radius = 2 }
material { ks = (1,0,0); ka = (0,0,0) }

One more note: the parser supports both C and C++ style comments. But make sure that the SBT-raytracer line is always first in the file!

Describing a scene

The format given above is more general than the set of files that describe scenes. Now that we have the file format, here are the kinds of objects that can actually appear in the input file. Note that both "colour" and "color" are recognized by the system.

Name Description Example
camera The camera declaration describes the position and orientation of the virtual camera and the geometry of the frustum. It has the following parameters:
  • position: the 3D position of the eye.
  • viewdir: the direction in which the camera is looking.
  • updir: the orientation of the camera with respect to viewdir (which way is up?).
  • aspectratio: the aspect ratio of the projection plane (width/height). Should probably correspond to the aspect ratio of your final image.
  • fov: the angle of the vertex of the frustum, measured in degrees.
camera {
  position = (0,0,-4);
  viewdir = (0,0,1);
  updir = (0,1,0);
  aspectratio = 1;
}
point_light A point_light is a light source where energy radiates equally in all directions from a single point. It has two parameters:
  • position: the 3D position of the point source.
  • colour: the colour of the emitted light.
point_light {
  position = (1,3,-2);
  // yellow light.
  colour = (1,1,0);
}
directional_light A directional_light is a light source where energy radiates equally in a direction from infinitely far away. It has two parameters:
  • direction: the direction vector of the directional source.
  • colour: the colour of the emitted light.
directional_light {
  direction = (0,0,1); 
  // cyan light.
  colour = (0,1,1);
}
sphere A sphere is a unit (radius 1) sphere centered at the origin. It has no intrinsic parameters, but like other geometry types, it can be transformed and assigned a material. These are both discussed below.
sphere
box A box is a unit (side length 1) cube centered at the origin (it goes from (-0.5,-0.5,-0.5) to (0.5,0.5,0.5)). Like the sphere, it has no intrinsic parameters.
box
square A square is a unit (side length 1) square centered at the origin and lying in the XY plane (its opposite corners are (-0.5,-0.5,0) and (0.5,0.5,0)). No intrinsic parameters.
square {
  material = {
    diffuse = (1,0,0);
  }
}
cylinder A cylinder is a radius 1 cylinder. Its central axis lies on the Z axis and its ends are at Z = 0 and Z = 1. It has one intrinsic parameter:
  • capped: a boolean indicating whether the cylinder has caps or not.
cylinder {
  capped = false;
}
cone A cone is a generalized cylinder with central axis on the Z axis. It takes a height parameter, and runs from Z = 0 to Z = height. The radius of each endpoint can be specified, and caps can be turned on and off.
  • capped: a boolean indicating whether the cone has caps or not.
  • height: a scalar giving the height of the cone.
  • bottom_radius: a scalar giving the radius at Z = 0.
  • top_radius: a scalar giving the radius at Z = height.
cone {
  capped = true;
  height = 2;
  bottom_radius = 1;
  top_radius = 0;
}
polymesh A polymesh is a big container for polygonal data. It allows you to give a set of vertices and a set of faces based on those vertices. It has the following parameters:
  • points: A tuple of the points in the mesh.
  • normals: A tuple with one normal for each point. (optional)
  • materials: A tuple of materials, one for each point. (optional)
  • faces: A tuple of faces. A face is a tuple of indices. Each index specifies one of the points (counting from zero), and each tuple of indices specifies the points in a face in counter-clockwise order.
  • gennormals: If this is set to be true then per-vertex normals will be automatically generated for the mesh.

Note:

  • If either normals or materials is specified they must have the same number of elements as points.
  • If normals aren't specifed or generated, the mesh will be rendered without Phong interpolation.
  • A single material can be specifed for the whole mesh using the material (sans s) just like other objects.
  • The polymesh only works well for triangles. Other faces are simply triangulated using a fan. This is not guaranteed to work in general.

polymesh {
  material = { diffuse=(1,0,0) };
  points = ( 
    (0,0,0), 
    (0,1,0), 
    (0,1,1), 
    (0,0,1),
    (1,0,0), 
    (1,1,0), 
    (1,1,1), 
    (1,0,1) );
  faces = ( 
    (2,3,7,6), 
    (1,5,4,0), 
    (2,1,0,3),
    (6,7,4,5), 
    (2,6,5,1), 
    (3,0,4,7) );
  materials = ( 
    { diffuse=(1,0,0) },
    { diffuse=(0,1,0) },
    { diffuse=(0.3,0.2,0) }, 
    { diffuse=(0,1,0) },
    { diffuse=(1,0,0) }, 
    { diffuse=(1,1,0) },
    { diffuse=(0.4,0.3,0.3) }, 
    { diffuse=(1,0,1) } );
}

translate Each of the primitives described above (sphere, cylinder, etc) can have a nested set of transforms above it. A translate declaration wraps the lower-level object inside a translation matrix. The example shows how to get a sphere centered at (1,1,2).
translate( 1,1,2, sphere );
scale Scales the lower-level object. Both proportional and nonproportional scale are supported.
// gives an ellipsoid
scale( 1,5,5, sphere );
// give a small sphere
scale( 0.2, sphere );
rotate A generalized rotation about a given axis. A vector is given as the axis of rotation, followed by the angle to rotate by (in radians!).
// rotate a scaled cylinder 
// about the X axis by 90 
// degrees
rotate(1,0,0,1.57, 
  scale( 0,0,3, cylinder ));
transform The mother of all transformations. Apply an arbitrary 4x4 matrix to the underlying geometry.
// I don't even want to know 
// what this does.
transform( 
    (1,2,3,4),
    (5,6,7,8),
    (9,10,11,12),
    (0,0,0,1), cylinder );
material Give the properties that describe what a surface looks like. This corresponds mostly to the parameters of the Phong lighting model.
  • emissive:
  • ke, the emissive colour.
  • ambient:
  • ka, the ambient colour.
  • specular:
  • ks, the specular colour.
  • diffuse:
  • kd, the diffuse colour.
  • transmissive:
  • kt, the ability for this material to transmit light in each channel (as a 3-tuple).
  • shininess:
  • ns, the shininess (between 0 and 1, multiplied by 128 to match n definied in [Foley & VanDam]).
  • index:
  • the material's index of refraction.
  • name:
  • the material's name, which can be used to create a top-level declaration by that name which can be reused later.
Materials can be used in two ways: inline or declared. Inline materials are defined as they are attached to primitives:
sphere {
  material = {
    diffuse = (1,0,0.4);
    specular = (1,1,1);
  }
}
A declared material is given a name at the top level and referred to later:
material {
  name = "gold";
  diffuse = (0.9,0.9,0);
  specular = (1,1,0);
  emissive = (0.1,0.1,0);
  shininess = 0.92;
}

box {
  material = "gold";
}

Hacking the Format

Note that you should never have to modify parse.{h,cpp}. You need only emulate the style of read.cpp to extract additional fields from objects. For example, let's say I add a "hairiness" parameter to materials, with a scalar value from 0 to 1. Looking in read.cpp, I see a function called processMaterial with a sequence of constructs like this:
if( hasField( child, "specular" ) ) {
  mat->ks = tupleToVec( getField( child, "specular" ) );
}
child refers to the node being searched for fields, in this case the dictionary containing all the parameters. The condition is asking whether the child indeed has a field named "specular". If so, the field is assumed to be a 3-tuple and the value are extracted and converted (if something goes wrong, it's a parse error). To add hairiness, I add another such if statement:
if( hasField( child, "hairiness" ) ) {
  mat->khair = getField( child, "hairiness" )->getScalar();
}
Here I use getField to extract the value of the field in the dictionary as a parsed object, on which I call getScalar to treat it as a scalar value.

As another example, let's say I wanted to add a new kind of primitive to the system: the velociraptor primitive. processObject is the top level entry point for parsing declarations in the .ray file, and in there I see something like

if( name == "sphere" || name == "box" || ... ) {
  SceneObject *geo = processGeometry( name, child, scene, materials );
  ...
}
All I need to do is add the test for velociraptor here:
if( name == "sphere" || name == "box" || ... || name == "velociraptor" ) {
  SceneObject *geo = processGeometry( name, child, scene, materials );
  ...
}
Then, in processGeometry, I do something similar to pull out the velociraptor case and extract its intrinsic parameters.