How to create polygonal objects using the Nt_POLYGON primitive
How to create polygonal objects using the Nt_INDEXEDPOLYGON
primitive
How to create polygonal objects using the Nt_USERDEFINED
primitive
How to create and cast shadows from point and spot light sources
How to use the fog atmospheric shader
How to set instance attributes with the command
This tutorial will introduce the Nt_POLYGON, Nt_INDEXEDPOLYGON
and Nt_USERDEFINED geometric primitives. These are the three main primitives that
can be used to send polygonal data to the renderer.
We will illustrate the usage of these three primitives by way of the
above scene which contains two distinct texture mapped polygons (the two spheres were
thrown in for a nice visual effect).
Please note that the following C code examples have been greatly
simplified for use in this tutorial; in particular, each object only contains a single
polygon, and only vertex and texture coordinate information is sent to the renderer. In
typical real-world usage you would probably create objects with thousands of polygons, and
each polygon would be sent to the renderer with numerous attributes such as color, vertex
normals, vertex texture coordinates, vertex U/V tangent vectors and opacity values.
For typical examples of these primitives please refer to the NuGraf
C script files located in the'examples/scripts' sub-directory contained on the
diskettes distributed with NuGraf; this directory contains numerous scene description
files written in the NuGraf C Interface language.
Each of these three primitives has its own merits, as well as its
own shortcomings, as explained here:
The Nt_POLYGON primitive is very simple to use and is useful
when you are only sending a few polygons to the renderer. However, it should not be used
to create polygonal models with more than a few hundred polygons in it since this
primitive stores the polygonal data very inefficiently in memory (as compared with the
other two primitives).
The Nt_INDEXEDPOLYGON primitive is the main workhorse
primitive that should be used to send large quantities of polygonal data to the renderer.
The syntax description of this primitive looks quite complex, and it is, but only due to
its generality, power and versatility. The data is specified in a
coordinate-list/index-list format which is very memory efficient since shared coordinates
do not have to be specified multiple times. The main disadvantage to using this primitive
is memory usage: all data sent to the renderer with this primitive is copied and stored in
internal data arrays; this can potentially double the amount of memory used by your
application if you do not free up the original data passed to the renderer. Also, if the
original data set is continually being modified between renderings then this primitive is
not ideal since the data must be recopied into the renderer's local memory space before
the data can be rendered once again. Notwithstanding, this is still the preferred
primitive to use to send polygonal data to the renderer.
The Nt_USERDEFINED primitive is another workhorse primitive
that allows unlimited numbers of polygons to be sent to the renderer. Rather than send the
polygon data in large arrays to the renderer, as with the Nt_INDEXEDPOLYGON
primitive, this primitive requests the polygonal data one polygon at a time from the host
application via a user-defined callback routine. The data is only requested when the
polygonal data is to be rendered, thus it does not have to be copied and stored internally
within the renderer's data space; the main consequence of this is that only one copy of
the polygonal data has to be stored in memory at any one time (the original copy in the
host application's memory space), thus memory usage is reduced. The disadvantages to using
this primitive is that the host application has to maintain the polygonal database rather
than let the renderer do it, and a fairly complex callback routine must be integrated into
the host application. Also, this primitive does not have the same processing commands
available to it that the Nt_INDEXEDPOLYGON primitive does (such as splitting
polygonal databases into separate objects, deleting sub-sets of the data, etc).
Nonetheless, this is the preferred primitive to use if you are writing a modeler in which
the database is changing frequently between renderings, or if the host application is
maintaining the polygonal database itself (it loads and saves the database from a disk
file).
First, let's look at the Nt_POLYGON primitive. One or more
polygons can be sent to the renderer between a and command pair using the Nt_POLYGON
option of the command. In the following example a single polygon is defined using this
primitive. The Nt_NUMVERTICES option specifies how many vertices are in this
polygon, while the arguments to the Nt_VERTEX and Nt_TEXTURE options are
pointers to the list of vertices and texture coordinates. This primitive is not
recommended for large numbers of polygons since the command must be called for each and
every polygon; also, the polygons are not stored very efficiently within the renderer as
compared to the other two primitives.
The next example utilizes the Nt_INDEXEDPOLYGON primitive.
This primitive is very speed and memory efficient since it sends all of the polygonal data
to the renderer using one function call. The data is stored in a
coordinate-list/index-list format which allows coordinates to be shared between different
polygons rather than having them duplicated as with the previous polygon primitive. This
sharing principle is illustrated in Figure 1.2 in which two polygons share vertices V2 and
V3.
Figure 1.2: Two
Polygons Sharing Two Common Vertices (IndexedPolygon)
Rather than send 8 vertices to the renderer, the Nt_INDEXEDPOLYGON
primitive only requires that the 6 unique vertices be sent. Then, for each polygon, a list
of indices is given which specify which of these 6 vertices belong to the
polygon. The corresponding vertex-list/index-list for Figure 1.2 is listed below. The 'vertex_coords'
array contains the list of 6 unique vertices and the 'vertex_indice' contains the
list of indices into the 'vertex_coords' array for the two polygons. Note that
the indices start at 1, not 0 (this is by convention).
A separate coordinate-list/index-list is required by the Nt_INDEXEDPOLYGON
primitive for the vertices, vertex normals, vertex texture coordinates, vertex colors,
vertex U/V tangent vectors and the vertex opacity lists. It can become quite messy and
cumbersome when coding this routine, but the end results are usually worth it if your
application is moving a large number of polygons into the renderer.
The following C code shows how to send an indexed list of vertices
and texture coordinates to the renderer using the Nt_INDEXEDPOLYGON primitive. It
uses the same vertex and texture coordinates listed previously in the Nt_POLYGON
primitive example to define the wall object seen in the image at the start of this
tutorial.
{
/* Total number of polygons in the object */
Nd_Int num_polygons = 1;
/* Array of length 'num_polygons' describing the # of vertices in */
/* each polygon (including holes and islands). */
Nd_Int num_vertices_per_polygon[] = { 4 };
/* Vertex data variables */
Nd_Int num_vertex_coords = 4;
Nd_Int vertex_indices[] = { 1, 2, 3, 4 };
/* Texture coordinates variables */
Nd_Int num_texture_coords = 4;
Nd_Int texture_indices[] = { 1, 2, 3, 4 };
Ni_Object_Define("wall object", Nt_CMDEND);
Ni_Primitive(Nt_INDEXEDPOLYGONS,
Nt_NUMPOLYGONS, (Nd_Int *) &num_polygons, Nt_CMDSEP,
Nt_VERTICESPERPOLY, (Nd_Int *) num_vertices_per_polygon, Nt_CMDSEP,
Nt_VERTEX,
(Nd_Int *) &num_vertex_coords,
(Nd_Vector *) wall_vertices,
(Nd_Int *) vertex_indices,
Nt_CMDSEP,
Nt_TEXTURE,
(Nd_Int *) &num_texture_coords,
(Nd_Vector *) texture_coordinates,
(Nd_Int *) texture_indices,
Nt_CMDSEP,
Nt_CMDEND);
Ni_Object_End(Nt_CMDEND);
}
The Nt_NUMPOLYGONS option specifies how many polygons are
contained in the arrays. The Nt_VERTICESPERPOLY option points to an array which
describes how many vertices are in each polygon. The three arguments to the Nt_VERTEX
option specify how many vertex coordinates are contained in the vertex coordinate list, a
pointer to the vertex coordinates array and a pointer to the list of vertex indices.
Likewise, the three arguments to the Nt_TEXTURE option specify how many texture
coordinates are contained in the texture coordinate list, a pointer to the texture
coordinates array and a pointer to the list of texture indices.
The above example is rather simple since only 1 polygon is being
sent to the renderer. The real power of this primitive is best shown when numerous
polygons are being sent to the renderer, each of which shares vertices with each other.
Lastly, let's examine the Nt_USERDEFINED primitive. This
primitive is quite different from the previous two primitives described because the data
is requested from the host application only at the moment it is to be rendered. The main
mechanism by which this is implemented is via a user-defined callback routine that is
added to the host application program. The renderer makes calls to this callback routine
to request specific polygon attributes and in response the host application returns
pointers to the requested data items to the renderer.
The callback function is associated with an object definition by
specifying a pointer to the callback function and two arbitrary user-defined 32-bits
values along with the Nt_USERDEFINED option of the function. No data is sent to
the renderer at this time; the polygonal data for this object definition will be sent
to the renderer via the callback at the moment when an instance of this object definition
is to be rendered (after the command is issued).
{
/* The 32-bit user-defined values passed to the primitive callback */
Nd_Ptr Nv_User_Data_Ptr1;
Nd_Ptr Nv_User_Data_Ptr2;
/* Create a wall object which uses the user-defined callback */
/* function. Pass the vertices and texture coordinates in the */
/* 32-bit user data pointers. */
Nv_User_Data_Ptr1 = (void *) wall_vertices;
Nv_User_Data_Ptr2 = (void *) texture_coordinates;
Ni_Object_Define("wall object", Nt_CMDEND);
Ni_Primitive(Nt_USERDEFINED,
(Nd_UserDefPrim_CB_Func) UserDefined_Primitive_Callback,
(void **) &Nv_User_Data_Ptr1,
(void **) &Nv_User_Data_Ptr2,
Nt_CMDEND);
Ni_Object_End(Nt_CMDEND);
}
Note that we pass the pointers to the wall polygon's vertex and
texture coordinates to the callback function via the two 32-bit user-defined values. These
pointers will be accessible to the callback function when it is called. The reason that we
pass the pointers in these two variables and not in global variables is due to the fact
the wall and floor object definitions share the same callback routine but use different
polygonal geometry; the callback routine will use these 32-bit pointers to access the
proper polygon vertex and texture coordinates when it is called.
The following C code lists the user-defined callback routine used to
send the wall and floor polygons to the renderer. Basically the renderer calls this
routine whenever it needs information about the polygonal data comprising an instance. The
Nc_UDP_ACTION_GET_NEXT_POLYGON action is called when the renderer is requesting
another polygon and the Nc_UDP_ACTION_GET_NEXT_VERTEX action is called when the
renderer is requesting attributes for another vertex of this polygon. We will not go into
any further detail here since the user-defined primitive is documented in detail
elsewhere.
static Nd_Void
UserDefined_Primitive_Callback(Nd_UserDefPrim_Callback_Info *cbi)
{
static short vertex_count, polygon_count;
Nd_Vector *vertex_data_ptr, *texture_coords_ptr;
/* Pick up the two user pointers which were passed to the */
/* Ni_Primitive() command. They point to the vertices and texture */
/* coordinates of this polygon. */
vertex_data_ptr = (Nd_Vector *) cbi->Nv_User_Data_Ptr1;
texture_coords_ptr = (Nd_Vector *) cbi->Nv_User_Data_Ptr2;
/* Determine what data the renderer wants us to return. */
switch(cbi->Nv_Action) {
case Nc_UDP_ACTION_CB_INITIALIZE:
/* This is called to initialize this callback */
/* function before it is called to inquire about */
/* the polygon data. */
polygon_count = 0;
break;
case Nc_UDP_ACTION_CB_SHUTDOWN:
/* This is called to shutdown the callback function */
break;
case Nc_UDP_ACTION_DELETE:
/* This is called whenever the parent object definition */
/* holding this primitive is about to be deleted by the */
/* renderer. All locally allocated data should be */
/* freed up here. */
break;
case Nc_UDP_ACTION_GET_BOUNDINGBOX:
/* Return the primitive-space extents of the */
/* geometric data */
break;
case Nc_UDP_ACTION_GET_NEXT_POLYGON:
vertex_count = 0;
/* There is only 1 polygon in each object for this */
/* tutorial example */
if (++polygon_count > 1) {
/* We are done once all of the polygons have */
/* been processed */
cbi->Nv_Return_Status = \
Nc_UDP_STATUS_NO_DATA_AVAILABLE;
return;
}
break;
case Nc_UDP_ACTION_GET_NEXT_VERTEX:
/* Return the next vertex attributes of the current polygon */
/* Return a pointer to the vertex position (mandatory) */
cbi->Nv_Vertex_Position = \
&vertex_data_ptr[vertex_count];
/* If the renderer is requesting texture */
/* information then return a pointer to the */
/* vertex's u/v texture coordinate. */
if (cbi->Nv_Request_Mask & Nc_UDP_REQUEST_TEXTURE) {
cbi->Nv_Vertex_Texture = \
&texture_coords_ptr[vertex_count];
cbi->Nv_Returned_Data_Mask |= Nc_UDP_HAS_TEXTURE;
}
/* Determine the 'return status' */
++vertex_count;
if (vertex_count == 4)
/* This is the last vertex of this polygon */
cbi->Nv_Return_Status = Nc_UDP_STATUS_LAST_VERTEX;
else
/* Else, this is just another vertex of this polygon */
cbi->Nv_Return_Status = Nc_UDP_STATUS_SUCCESS;
break;
}
}
This completes our short discussion of the 3 main polygon primitives
provided with NuGraf.
Next we will look at the fog atmospheric shader. This shader
simulates a fog effect by blending the foreground pixel color into either the fog color or
the background pixel color. The shader can also be used to create a depth-cueing effect
where objects further away from the camera become darker or, alternatively, blend into the
background color.
The following C code shows how to use the fog shader. The fog starts
at a distance of 10 units away from the camera with a density of 0% and reaches a maximum
saturation of 100% at a distance of 50 units from the camera. The fog type is linear and
the fog color is red.
The last part of this tutorial will briefly discuss how to cast
shadows from point and spot light sources.
Before adding the shadow casting commands you should first specify
for each instance whether it can cast shadows and whether it can receive shadows. As an
example, a floor usually doesn't cast shadows but it can receive shadows. The shadow
generation algorithm will run faster if you specify which instances do not or can not cast
shadows. Likewise, specifying which instances are not to receive shadows will make the
shadow casting algorithm run faster since no shadow checking has to performed for these
instances.
These shadow options can be changed for an instance with the
command. This command also allows numerous attributes to be set or changed for an
instance, such as its shadow casting options, a localized culling-override switch, a hidden
switch and a local light illumination override switch.
{
/* The floor does not cast shadows */
Ni_Instance("floor instance",
Nt_CASTSSHADOWS, Nt_ENABLED, Nt_OFF, Nt_CMDSEP,
Nt_SHADOWED, Nt_ENABLED, Nt_ON, Nt_CMDSEP,
Nt_CMDEND);
}
NuGraf's shading algorithm casts shadows using a depth map
shadow algorithm technique. This is much faster than ray tracing shadows and produces
nice soft edges on the shadow boundaries. This algorithm and the shadow casting technique
is described in .
The easiest way to cast shadows in a scene is with a spot light
source. Basically all you have to do is make sure the following options are set:
Set the Nt_ENABLED option in the "default'' shadowmap
definition of the spot light source to Nt_ON,
Set the 'Nt_SHADOWS,Nt_ENABLED' option of the command
to Nt_ON,
Set the Nt_SHADING option of the command to Nt_SMOOTH,
Set the Nt_RENDERLEVEL option of the command to Nt_ZSCANLINE,
A shadow map will then be generated automatically for "light
3'' before the final scene with shadows in it is rendered. Nothing else has to be
done by the host application program.
The appearance of the cast shadows can be changed globally with the Nt_SHADOWMAP
sub-options of the command; these same parameters can be specified on a per-light basis if
the Nt_USEGLOBALVALUES sub-option is disabled within a light source definition.
In the following C code the Nt_SHADOWMAPSIZE sub-option sets
the resolution of the shadow map generated (a resolution of 256 or 512 is sufficient), Nt_ATTENUATION
sets the darkness of the shadows, Nt_BIAS0 and Nt_BIAS1 are kludge factors
used to hide aliasing artifacts, Nt_NUMSAMPLES determines how noisy the shadow
edges are, Nt_RESFACTOR determines the width of the shadow edges and Nt_CULLINGOVERRIDE
sets the culling mode during shadow generation. All of these sub-options are optional and
do not normally have to be specified to have shadows cast in a scene.
{
Nd_Float SM_Attenuation = 0.8;
Nd_Int SM_Numsamples = 32;
Nd_Float SM_Resfactor = 2;
Nd_Float SM_Bias0 = 0.1;
Nd_Float SM_Bias1 = 0.2;
Nd_Int SM_Resolution = 512;
/* Set the global shadowmap parameters */
Ni_Option(Nt_SHADOWMAP,
/* The following parameters are used during shadowmap creation */
Nt_CULLINGOVERRIDE, Nt_BACK,
Nt_SHADOWMAPSIZE, (Nd_Int *) &SM_Resolution,
/* The following parameters are used during shadow rendering */
Nt_ATTENUATION, &SM_Attenuation,
Nt_BIAS0, &SM_Bias0,
Nt_BIAS1, &SM_Bias1,
Nt_NUMSAMPLES, &SM_Numsamples,
Nt_RESFACTOR, &SM_Resfactor,
Nt_CMDEND);
}
Lastly, we will describe how to cast high-quality shadows from point
light sources. Normally the renderer will cast shadows from a point light source by
creating 6 different shadow maps, each looking in a different direction from the center of
the point light source. However, this is inefficient in speed and memory usage since the
renderer has to deal with 6 shadow maps. An alternative method exists which requires some
intervention by the host application program. Basically what you do is create a new
perspective camera with its look-from location centered on or near the point light source
location at the look-at location situated in the direction where you want shadows to be
cast. The camera's projection plane should be sized so that all objects that are to cast
shadows can be seen by this camera.
Once this camera has been defined you link it to the point light
source that is to cast shadows with the Nt_USECAMERA sub-option of the command. By
specifying an explicit camera to use, the renderer will generate a single shadow map using
this camera viewpoint rather than 6 separate shadow maps.
{
Nd_Vector light1_shinefrom = { 9.0, 16.0, 0.0 };
/* Shadow map camera parameters */
Nd_Vector sm_camera_lookfrom = { 20.0, 24.0, 20.0 };
Nd_Vector sm_camera_lookat = { 0.0, 4.0, 0.0 };
Nd_Float sm_camera_distance = 10.0;
Nd_Float sm_camera_window[2] = { 4.0, 4.0 };
/* Add the light sources */
Ni_Light("light1",
Nt_MODEL, Nt_POINT, Nt_CMDSEP,
Nt_SHINEFROM, light1_shinefrom, Nt_CMDSEP,
Nt_CMDEND);
/* Define a camera viewpoint which will be used to create the */
/* shadowmap for point light source # 1. */
Ni_Camera("shadowmap light1 camera",
Nt_FOCALLENGTH, (Nd_Float *) &sm_camera_distance, Nt_CMDSEP,
Nt_WINDOW, (Nd_Float *) sm_camera_window, Nt_CMDSEP,
Nt_LOOKFROM, sm_camera_lookfrom, Nt_CMDSEP,
Nt_LOOKAT, sm_camera_lookat, Nt_CMDSEP,
Nt_PROJECTION, Nt_PERSPECTIVE, Nt_CMDSEP,
Nt_CMDEND);
/* Now associate a shadow map camera with light # 1 which will be used */
/* to create the point light source's shadow map. */
Ni_Light("light1",
Nt_SHADOWMAP, "default",
Nt_ENABLED, Nt_ON,
Nt_USECAMERA, "shadowmap light1 camera",
Nt_CMDEND);
}