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 sequence of tokens. The second takes that sequence of tokens and turns it into the internal scene representation.
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
-1E6String 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_everywhereBooleans: The special reserved IDs true
andfalse
are boolean objects.true
falseTuples: 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) }Groups: You can group a collection of geometries together under a single transform by enclosing them in braces scale(2, { sphere{} cylinder{} } )
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.
- (NEW!) look_at: the direction that you want to look.
camera { position = (0,0,-4); viewdir = (0,0,1); updir = (0,1,0); aspectratio = 1; }New
camera { position = (0,0,-4); look_at = (0,0,0); }ambient_light An ambient_light corresponds to an ambient light source. When computing the ambient light for the scene, the sum of all the ambient lights is taken. ambient_light { color = (1.0, 1.0, 1.0); }point_light A point_light is a light source where energy radiates equally in all directions from a single point. It has the following parameters:
- position: the 3D position of the point source.
- colour: the colour of the emitted light.
- constant_attenuation_coeff, linear_attenuation_coeff, quadratic_attenuation_coeff: Coefficients controlling the distance attenuation of the 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 it must be assigned a material. These are both discussed below. sphere { material = { diffuse=(1,0,0) }; }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 { material = { diffuse=(1,0,0) }; }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:
cylinder { material = { diffuse=(1,0,0) }; }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 { material = { diffuse=(1,0,0) }; capped = true; height = 2; bottom_radius = 1; top_radius = 0; }trimesh A trimesh 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.
- 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.
- normals: A tuple with one normal for each point. (optional)
- texture_uv: A tuple of texture coordinates, one for each point. (optional)
- materials: A tuple of materials, one for each point. (optional)
- gennormals: If this is set then per-vertex normals will be automatically generated for the mesh. Just writing this keyword in the model file will set it to true. For example, see scenes/trimeshes/dragon.ray
Note:
- If either normals or materials is specified they must have the same number of elements as points.
- If normals aren't specified or generated, the mesh will be rendered without Phong interpolation.
- A single material may be specified for the whole mesh using the material (sans s) just like other objects. If the 'materials' parameter is specified, then this parameter is optional, else it is required.
- Per-vertex normals can be specified in two ways - (1) explicitly specifying the normals as in scenes/trimeshes/chess.ray or (2) setting gennormals as in scenes/trimeshes/dragon.ray
trimesh { 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 { material = { diffuse=(1,0,0) }; });scale Scales the lower-level object. Both proportional and nonproportional scale are supported. // gives an ellipsoid scale( 1,5,5, sphere { material = {diffuse=(1,0,0)}; } ); // give a small sphere scale( 0.2, sphere { material = { diffuse=(1,0,0) }; });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 { material = { diffuse=(1,0,0) };}));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 = { diffuse=(1,0,0) };});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 color.
- ambient: ka, the ambient color.
- specular: ks, the specular color.
- reflective: kr, the reflective color. Defaults to ks if not specified.
- diffuse: kd, the diffuse color.
- transmissive: kt, the ability for this material to transmit light in each channel (as a 3-tuple).
- shininess: ns, the shininess (any float value).
- 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"; }Using Texture Mapping
The trace parser accpets texture files. Again, texture mapping is commanly applied, but not limited, to diffuse color of a surface. Many other parameters can be mapped. Reflected color can be mapped to create the sense of a surrounding environment. Transparency can be mapped to create holes in objects.
To let the parser read texture file names, use the keyword "map". For example, in texture_reflection.ray:
diffuse = map( "texture-checkerboard.png" );Hacking the Format
The trace parser is designed to be fairly extensible, and it should be possible to add support for whatever you need. It consists of two main modules, the tokenizer and the parser. The tokenizer does nothing more than split the input up into distinct tokens, while the parser does the actual work of converting these into a scene in memory.
The operation of the tokenizer is quite simple, and it is unlikely that you would ever have a need to modify this. You are, however, likely to need to add keywords in token.{h,cpp}, because unregistered keywords are rejected as syntax errors by the parser. See below for instruction on how to do this.
The parser is a more complex beast, but its basic operation is as follows: as it reads the stream of tokens, it looks at the next one and uses this token to make a guess at what the user is intending; that is, if it sees the "sphere" keyword, it calls the "parseSphere" method. Thus, it is important that any extensions that you make to the language be easily recognizable by a single token. There are a number of helper functions in the parser, like "parseVec3f" and "parseScalar", that can take care of many of the dirty details for you, so that you can focus on the higher-level language.
Now I'll include a couple of sample modifications to the raytracer.
Suppose for of all that we want to add a new primitive, the "pseudosphere". The first thing we need to do is open up Token.h and find the enum listing all the token types. We can add the symbol "PSEUDOSPHERE" to the end of this list as follows:
enum SYMBOL { ... MAP, PSEUDOSPHERE // <-- new token goes here };Now we need to open up Token.cpp and add the pseudosphere to the two lookup tables:
tokenNames[ PSEUDOSPHERE ] = "pseudosphere"; ... reservedWords[ "pseudosphere" ] = PSEUDOSPHERE;Now, finally, we can add the new primitive to Parser.{h,cpp}. The best way to go about this is to look at how existing primitives are handled (sphere, box, etc.) and copy that code. It should also be noted that in order to parse a new kind of primitive, you should add dispatching for that primitive to the parseScene, parseTransformableElement, and parseGeometry functions, which is fairly self-explanatory.
Now, suppose you want to add a new attribute to a material. You would start by making the same modifications to the Token class as above. In Parser.cpp, however, you would find that the only function you actually need to modify is parseMaterial(); depending on the type of parameter, a call to parseScalarMaterialParameter() or parseVec3fMaterialParameter() should do the trick.