Utilities Provided by Rings

-- The Rings engine provides some basic types of objects that are required by computer graphics algorithms. The most important of these are Vectors and RGBs.


Vector

-- A Rings Vector is a set of three scalar values.

-- The Vector class is a Java class that provides a data structure for storing and operating on these three scalar values. The values are stored as Java doubles and are accessible by conventional getter/setter methods. The class provides addition and subtraction between vectors and multiplication and division by scalars and all the methods provide a way to avoid creating new Vector objects, which is a powerful way of increasing the performance of an algorithm. In addition to these basic operation, crossProduct and dotProduct methods that accept and return other Vector objects are provided.


RGB

-- Color values are stored in Rings using RGB. An RGB value is a set of three scalar values, just like a Vector. The three scalar values are the red, green, and blue components of the color where 0.0 corresponds to no color and 1.0 corresponds to full intensity. White, for example, is RGB[1.0, 1.0, 1.0] and black is RGB[0.0, 0.0, 0.0].

-- The RGB class is a Java class that provides a data structure for storing and operation on these three scalar values. The values are stored as Java doubles and are accessible by conventional getter/setter methods. The class provides addition, subtraction, multiplication, and division between RGBs and scalars and all the methods provide a way to avoid creating new RGB objects. The scalar values that are stored by this class are not required to be between 0.0 and 1.0, but the Rings rendering engine will treat all values above 1.0 as 1.0 and all values below 0.0 as 0.0.


Basic Components of Rings

-- The Rings engine is composed of three main types of objects: Surfaces, Lights, and a Camera.

Surface

-- A Rings Surface is the most important object involved in rendering a scene. All physical objects that are to be rendered must be described to the Rings engine as a Surface object.

-- A Surface is defined by the equations that determine its intersection with a geometric ray.

-- For example, a sphere is defined by the solution to a quadratic equation that is derived by solving for intersection between a ray and the cartesian equation for a sphere (x2 + y2 + z2 = 1).

-- The Surface interface is a Java interface that defines the type for a Surface in Rings. This interface requires that the user implement a few methods to describe the surface including intersection methods (one that returns true or false, and one that returns an Intersection object). The Surface interface also specifies the getColorAt method, which accepts a Vector and returns an RGB for the color of the surface at the point specified. A similar method exists, getNormalAt, which accepts a Vector and returns a Vector for the surface normal at the point specified. A Surface implementation must also provide getShadeBack and getShadeFront methods which returns booleans. If getShadeFront returns true then shaders will produce a color based on the value of the surface normal and if getShadeBack returns true shaders will do the same calculation with the surface normal reversed. If both methods return true, shaders will add the two values when calculating a color for the surface. A Surface implementation must also provide a shade method that accepts a ShaderParameters object. This method returns an RGB that is the value for the color of the surface based on the parameters provided. Many of these methods, however, should not be implemented by one who wishes to add a new type of surface to Rings. This is the reason for the AbstractSurface class.

-- The AbstractSurface class is an abstract Java class that implements the Surface interface and is extended by classes that wish to provide a new type of surface to Rings. The AbstractSurface class provides many useful tools for surfaces and also provide implementation of all the methods of the Surface interface except for the getNormalAt method and the intersection methods. This means that when one extends the AbstractSurface class only the geometry of the surface (calculation of intersection with a line and surface normal at a point) must be considered.

-- Many Surface classes are provided by Rings. One example is the Sphere primitive which is defined exactly as described above. The source code is surprisingly compact. The only unexplained complication is that the intersection methods begin with:

ray.transform(this.getTransform(true).getInverse());

This is because the AbstractSurface class provides a method for specifying transformations to a surface and any class that extends it and provides intersection methods must first transform the ray parameter based on the transformations specified by the superclass. This is optional, of course, and simply allows automation of transforms (translation, scaling, etc.) that are often performed on a surface.


Light

-- A Rings Light is the second most important object involved in rendering a scene. All physical objects that are to be rendered must be illuminated by some type of light. The color and intensity of direct light at a given point in a scene is calculated based on information described to the Rings engine by a series of Light objects.

-- A Light is defined by specifying a function for the color of the light for a given point in space.

-- For example, a point light source is defined by taking a base color, multiplying by an intensity factor, and then adjusting for distance attenuation based on the distance from the point light to the point that is to be illuminated.

-- The Light interface is a Java interface that defines the type for a Light in Rings. This interface requires that the user implement two pairs of getter/setter methods for color and intensity of light (which can be used however the user sees fit) and also the getColorAt method, which accepts a Vector and returns an RGB for the color of light at the point specified. Rings also provides a SurfaceLight interface, which allows a light to be defined by a set of samples (Light implementations of some sort). A SurfaceLight implementation must provide all the methods described by the Light interface, but also a method getSamples that returns an array of Light objects. This can be used with super-sampling to produce softer shadows and better light distribution for most scenes.

-- A few Light implementations are provided by Rings. These include AmbientLight, DirectionalAmbientLight, and PointLight. These classes all provide essentially the same thing (return the RGB color of the light multiplied by the intensity factor for all values of the Vector parameter), but they are treated differently by the Rings engine. When the ray tracing engine encounters an AmbientLight it does not use any shaders applied to the surface, but simply multiplies the value returned by getColorAt of AmbientLight and the value returned by getColorAt of Surface to give a color value for the surface. Reflection, refraction and other light interactions that involve a direction are not considered when using AmbientLight. If the engine discovers a DirectionalAmbientLight, it behaves differently. The DirectionalAmbientLight class specified a direction, and this direction is passed to a ShaderParameters object that will end up being passed to a Shader that will calculate a color for the surface. If the engine encounters a PointLight, it behaves in another way. The PointLight class specifies a location and the line from this location to the location in space that is being rendered is used as the direction for the light. After a direction pointing from the light to the object to be shaded is calculated, the engine handles a PointLight in the exact same way as a DirectionalAmbientLight. Rings also provides two implementations of the SurfaceLight interface, called RectangularLight and SphericalLight, which provide random samples from the surfaces of a plane and a sphere, respectively. This allows the user to light a scene in a dynamic way that adds a more realistic sense of illumination to the image.


Camera

-- The Rings engine allows the user to specify details of the way in which a scene should be viewed in a very general way through the concept of a camera. A Rings Camera is an object that provides a description of the possible lines of sight from a viewer to a place in the scene.

-- A Camera is defined by specifying a function for a viewing direction ray (origin + direction) based on a coordinate on an imaginary viewing plane (which could be called film, assuming the implementation is physically plausible and realistic).

-- For example, an orthographic camera is defined by a function that returns a ray that points perpendicular to the viewing plane and has its origin at the point on the viewing plane that was specified to it by the Rings engine.

-- The Camera interface is a Java interface that defines the type for a Camera in Rings. The only method that is required by a Camera implementation is rayAt. The rayAt method accepts two decimal values and two integer values. The two integer values are values for width and height of the viewing plane in pixels. The two decimal values are the coordinates on the viewing plane (same units as the integer values) of the pixel that is being rendered. These are decimal values because the Rings engine may take multiple samples for a single pixel with coordinates that are within the same pixel.

-- Three implementations of Camera are provided by Rings. These include OrthographicCamera, PinholeCamera, and ThinLensCamera. These Camera types can be extended to provide new or modified viewing properties (e.g. fisheye, mirror, magnify, etc.) and they can be configured in a wide range of ways to fit most rendering needs.



Customizing Surfaces

-- Rings provides a variety of methods for customizing the appearance of a surface before it is rendered. These include Textures, Shaders, and Transformations.


Texture

-- Surfaces in the Rings engine have complete control over the color that is used for each point on the surface. This is done by using a set of Textures.

-- A Texture is defined by a function that gives an RGB when provided a Vector. A Texture allows the user to describe a the color of a surface in any way that can be expressed as a function that accepts and returns three scalar values.

-- For example, a stripe texture is implemented by using a sine function to produce a values between -1.0 and 1.0 based on the position of the point on the surface. This value is then multiplied by an RGB and returned.

-- The Texture interface is a Java interface that defines the type for a Texture in Rings. The only two methods required by a Texture implementation are the two getColorAt methods. Both methods accept a Vector object, representing the point that the Texture is to be evaluated for. The second method, however, accepts an array of Objects as arguments to the Texture. These arguments might be provided by another Texture, or any class that uses the Texture implementation and knows what form these arguments should take.

-- Two implementations of Texture are provided by Rings. These are StripeTexture and ImageTexture and their functions are self explanatory.


Shader

-- Surfaces in the Rings engine may provide a method for shading. Shading is the process of calculating the color that should be displayed for a given point on a surface based on the actual color of the surface and other factors that effect the way it looks (position of viewer, position and color of light, surface normal, etc.) This can be done by using a set of Shaders.

-- A Shader is defined by a function that gives an RGB when provided a set of shader parameters. The Rings engine provides the following parameters:

- Point on surface to shade (Vector)

- Direction towards viewer (Vector)

- Direction towards light (Vector)

- Reference to the light object (Light)

- Reference to the other light objects in the scene (Light[])

- Reference to the surface object (Surface)

- Reference to the other surface objects in the scene (Surface[])

-- For example, the DiffuseShader provided by Rings uses a lambertian shading model that returns color based on the angle between the surface normal and the direction toward the light source.

-- The Shader interface is a Java interface that defines the type for a Shader in rings. A Shader must provide only one method, called shade, which accepts a ShaderParameters object that contains the data mentioned above.

-- A variety of shaders are provided with Rings including DiffuseShader (lambertian), HighlightShader (phong), ReflectionShader, SilhouetteShader, and some other (less reliable) ones.


Transformation

-- 3D transformations (described by matrices) may be applied to Surfaces in the rings engine. Many utilities are provided for creating and applying these transformations.

-- A TransformMatrix is defined by a set of 16 decimal values (a 4 x 4 matrix)

-- The TransformMatrix class is a Java class that stores these 16 decimal values and provides static methods for constructing matrices.

-- The AbstractSurface class provides a method for applying a TransformMatrix to the surface. This depends on the intersection methods of the surface applying the inverse transformation before calculation as shown below:

ray.transform(this.getTransform(true).getInverse());


Using the API to Generate Images

-- The Rings API allows a simple Java program to create and render a scene. This sample will provide a line by line explanation of a somewhat complicated program designed to demonstrate a variety of possible ways to extend the basic functionality of the Rings engine.


The RenderDemo class has all of its functionality in the main method.

A Scene object is created to store the scene information for the demo and scene data is loaded from a file called CronellBox.xml. This file contains the walls for a cornell box, which will be the basis for the demo scene:


Scene scene = null;

try {

scene = FileDecoder.decodeSceneFile(new File("CornellBox.xml"),

FileDecoder.XMLEncoding,

false, null);

} catch (IOException e) {

e.printStackTrace();

System.exit(1);

}


Next we create a Sphere primitive and set its location and color and create a transform matrix that will scale the sphere to make it a little taller:


Sphere s = new Sphere();

s.setLocation(new Vector(0.0, -0.5, 0.0));

s.setColor(new RGB(0.8, 0.2, 0.2));

TransformMatrix scale = TransformMatrix.createScaleMatrix(1.0, 1.4, 1.0);

s.addTransform(scale);


Create a new implementation of the Texture interface to provide an interesting texture for the sphere:


Texture randomTex = new Texture() {

public RGB getColorAt(Vector point) {

point.setZ(0.0);

double d = (point.length() * 4.0) % 3;

if (d < 1) {

return new RGB (0.5 + Math.random() / 2.0, 0.0, 0.0);

} else if (d < 2) {

return new RGB (0.0, 0.5 + Math.random() / 2.0, 0.0);

} else {

return new RGB (0.0, 0.0, 0.5 + Math.random() / 2.0);

}

}

public RGB getColorAt(Vector point, Object args[]) { return this.getColorAt(point); }

public RGB evaluate(Object args[]) { return this.getColorAt((Vector) args[0]); }

};

s.addTexture(randomTex);


This inner class provides the three methods required by the Texture interface. The important method here is the getColorAt method that accepts a Vector. The method calculates a color for the surface at the point specified by using its distance from the origin when projected onto the XY plane. The class also provides a getColorAt method that accepts an array of arguments, but this texture does not use any parameters. Because a Texture implementation is also a ColorProducer implementation, the evaluate method must be defined by this texture implementation. The method assumes the argument list contains a Vector.


Create a new type of surface by extending the AbstractSurface class:


AbstractSurface thing = new AbstractSurface() {

private Plane p = new Plane(Plane.XY);

public Vector getNormalAt(Vector point) { return new Vector(0.0, 0.0, 1.0); }

public boolean intersect(Ray ray) {

ray.transform(this.getTransform(true).getInverse());

return this.p.intersect(ray);

}

public Intersection intersectAt(Ray ray) {

ray.transform(this.getTransform(true).getInverse());

if (Math.random() > 0.5) {

return this.p.intersectAt(ray);

} else {

return new Intersection(ray, this, new double[0]);

}

}

};

thing.setColor(new RGB(1.0, 1.0, 1.0));


This inner class provides the three methods required by the Surface interface that are not provided by the AbstractSurface class. The class is a simple wrapper for a Plane primitive that makes "holes" in the plane by randomly preventing intersection calculations from returning a value. This is not an incredibly useful type of surface, but it demonstrates the way that a surface can be specified using intersection calculations. Notice that the intersection methods first transform the ray using the transformations stored by the parent class, AbstractSurface. This allows for translating, scaling, and other types of transformations without modification to the original surface code. The getNormalAt method returns a vector pointing in the positive Z direction no matter what input it is given, since the plane is totally flat and has the same surface normal everywhere. The color of the new surface has to be set to white, because the default color for an AbstractSurface is black.


Create a new StripeTexture instance to provide a texture for the new surface:


StripeTexture stripes = new StripeTexture();

stripes.setAxis(StripeTexture.XAxis);

stripes.setStripeWidth(0.25);

stripes.setFirstColor(new RGB(1.0, 0.0, 0.0));

stripes.setSecondColor(new RGB(0.0, 0.0, 1.0));

thing.addTexture(stripes);


This code creates a StripeTexture object, sets parameters for the texture, and adds the texture to the surface define earlier.


Add the surfaces to the scene:

scene.addSurface(s);

scene.addSurface(thing);


This code adds the surfaces created earlier to the scene that will be rendered.


Create a RectangularLight object for the ceiling of the room.


RectangularLight rl = new RectangularLight(2.0, 2.0);

rl.setColor(new RGB(1.0, 1.0, 1.0));

rl.getLocation().setY(6.0);

rl.setType(Plane.XZ);

rl.setIntensity(0.7);

rl.setSampleCount(6);


This code constructs a new RectangularLight object and sets parameters. First, the color is set to white (the default is black). Next the location is moved up along the Y axis so that the light will illuminate the scene. The setType method sets the up the RectangularLight so that it is in the plane of X and Z axes. Also, the setSampleCount method sets the number of samples that will be used for the surface of the light. More samples means the light will take longer to render, but shadows will be smoother and antialiasing will look better.


Create some PointLight objects to add illumination:


PointLight pl1 = new PointLight(new Vector(4.0, 4.0, 3.0), 0.6, new RGB(0.4, 1.0, 0.4));

PointLight pl2 = new PointLight(new Vector(-4.0, 4.0, 3.0), 0.6, new RGB(1.0, 0.4, 0.4));


This code creates two PointLight objects by specifying position, intensity, and color. One light is made to be greenish RGB[0.4, 1.0, 0.4], and the other to by redish RGB[1.0, 0.4, 0.4].


Create an extension of PointLight to customize the lighting:


PointLight pl3 = new PointLight(new Vector(0.0, 5.0, 4.0), 0.7, new RGB(0.0, 0.0, 1.0)) {

public RGB getColorAt(Vector p) {

RGB c = super.getColorAt(p);

c.multiplyBy(Math.sin(p.subtract(super.getLocation()).length()));

return c;

}

};


This inner class provides the getColorAt method required by implementations of the Light interface. The code uses the sin function to oscillate the intensity of the light based on the distance from the light source.


Add the lights to the scene:


scene.addLight(rl);

scene.addLight(pl1);

scene.addLight(pl2);

scene.addLight(pl3);


This code adds each light created earlier to the scene.


Create a ThinLensCamera object and set the parameters:


ThinLensCamera c = new ThinLensCamera();

c.setLocation(new Vector(0.0, 0.0, 10.0));

c.setViewDirection(new Vector(0.0, 0.0, -1.0));

c.setProjectionDimensions(c.getProjectionWidth(), c.getProjectionWidth() * 1.6);

c.setFocalLength(0.05);

c.setFocus(10.0);

c.setLensRadius(0.2);

scene.setCamera(c);


This code creates a new ThinLensCamera object. The projections dimensions are set up so that they are close to the golden ratio. The camera is focus and focal length of the camera so that the objects in the scene are in focus. Increasing the lens radius (aperture) creates more blur in areas that are not in focus. The last line of code sets the scene camera to the camera just created.


Write out an XML file containing the scene information just created:


try {

FileEncoder.encodeSceneFile(scene, new File("RenderDemo.xml"), FileEncoder.XMLEncoding);

} catch (IOException e) {

e.printStackTrace();

}


This code uses the static method of the FileEncoder class to save the scene just created. This is not necessary to produce an image, as the scene is stored in memory while the program is running, but it is probably a good idea to output the scene in case you want to go back to it.


Render the scene and display the image in a new window:


RenderTestFrame f = new RenderTestFrame(scene, 200, 2);

f.render();


This code creates a RenderTestFrame which can be used to render a scene and display the image. The window will display the progress of the render while the image is being produced. The constructor of RenderTestFrame accepts a Scene and two width values. The first value is the image width (the height will be selected such that the pixels are square, based on the projection dimensions of the camera being used, not to say the pixels must be square...). The next value is the super sample width and height (the same value is used for both, here 2, resulting in 4 samples per pixel). The greater the super sample width, the better antialiasing will appear, and the image will look smoother. A high number of samples per pixel (4 or more) is recommended when trying to produce soft shadows and smooth boundaries between intricate surfaces.


Note

If you need more information about how to use the Rings rendering engine or if you have a suggestion for improvement or specific feature request, please don't hesitate to send email to ashesfall@users.sf.net. I have plenty of time for all the comments I can get, so don't think it will take forever to get my attention.