.\" .\" Brief tutorial on adding new primitives to rayshade. .\" Craig Kolb 10/89 .\" .\" $Id: primitive.ms,v 3.0.1.1 90/04/10 17:22:36 craig Exp $ .\" .\" $Log: primitive.ms,v $ .\" Revision 3.0.1.1 90/04/10 17:22:36 craig .\" patch5: addscaledvec() no longer returns a structure... .\" .\" Revision 3.0 89/10/24 18:07:30 craig .\" Baseline for first official release. .\" .de D( .DS .nr PS 8 .ps 8 .nr VS 12p .vs 12p .cs 1 20 .. .de D) .DE .nr PS 10 .ps 10 .nr VS 18p .vs 18p .cs 1 .. .DS .ND October, 1989 .RP .de ]H .nr PS 10 .ps 10 'sp |0.5i-1 .tl 'Rayshade Primitive Tutorial'-%-'Draft' .ps .ft 'sp |1.0i .ns .. .wh0 ]H .pn 2 .LP .TL Adding Primitives to Rayshade .TL \fR\fIDraft\fR .AU Craig Kolb .AI Department of Mathematics Yale University New Haven, CT 06520 .sp .5i .nr VS 18pts .PP This tutorial describes the process of adding new primitives to rayshade. Although the hooks for adding primitives are relatively straight-forward, it is difficult to see the "big picture" by studying the source code. While this tutorial is primarily meant for those interested getting their hands dirty, it provides a overview of the design of at least one portion of rayshade and thus may be of interest even if you are not planning on making modifications. .LP Adding a new primitive involves modifying at least six source files and creating at least one new file: .NH primobj.h .LP A datatype for the new primitive is added, and a pointer for the new type is added to the Primitive type. .NH constants.h .LP A numerical type is reserved for the primitive. .NH intersect.c .LP Various arrays of pointers to functions are modified to include new functions which will be written for the new primitive. The name of the new primitive is added to a list of names, and an array of flags is modified to reflect the addition of the new primitive. .NH funcdefs.h .LP The ray-primitive intersection function, primitive creation function, normal calculation function, and bounding-box calculation function are declared. .NH .c .LP This file contains all of the code needed to perform ray-primitive intersection tests, find normals, and compute bounding boxes for the new primitive type. .NH input_lex.l .LP The keyword used to identify the new primitive is added to the list of recognized keywords. .NH input_yacc.y .LP The new primitive is added to the list of primitives recognized by the yacc grammar. The added code will call the routine which creates and returns a reference to the primitive which was defined in .c .LP In addition, the Makefile must be updated to reflect the existence of the new source file. .sp 1 .LP In this tutorial, a new primitive .I disc, is added to rayshade. A disc is specified by its center, radius, and normal. .br .NR PS 12 .ps 12 .sp .5 \fBThe Primitive type\fR .nr PS 10 .ps 10 .sp .5 .LP All Primitives in rayshade are referenced using a single Primitive structure. This structure is defined as: .D( typedef struct Primitive { char type; /* object type */ struct Surface *surf; /* default surface */ union { Sphere *p_sphere; Box *p_box; Triangle *p_triangle; Superq *p_superq; Plane *p_plane; Cylinder *p_cylinder; Polygon *p_poly; Cone *p_cone; Hf *p_hf; } objpnt; /* Pointer to primitive */ } Primitive; .D) .LP The .I type field is used by various routines in intersect.c to determine which elements in a number of arrays should be used when dealing with a given Primitive structure. The .I surf field points to the surface associated with the primitive. The .I objpnt field is a union of pointers to different primitive types. The .I type of the Primitive is used to determine which pointer to dereference. .NH 0 Modifying primobj.h .LP Primobj contains structures describing each primitive type. For example, a sphere is defined as: .D( typedef struct { double r; /* radius */ double x, y, z; /* position */ } Sphere; .D) We must define a similar structure for the disc primitive. After the definition of the Hf type, we add: .D( /* * Disc */ typedef struct { Vector center, /* Center of the disc */ normal; /* Normal to disc */ double d, /* Plane constant */ radius; /* Radius of disc */ } Disc; .D) .LP We must also add a pointer for the Disc type to the Primitive type. So, we add a line to the \fIobjpnt\fR union in the Primitive definition, giving us: .D( typedef struct Primitive { char type; /* object type */ struct Surface *surf; /* default surface */ union { Sphere *p_sphere; Box *p_box; Triangle *p_triangle; Superq *p_superq; Plane *p_plane; Cylinder *p_cylinder; Polygon *p_poly; Cone *p_cone; Hf *p_hf; Disc *p_disc; } objpnt; /* Pointer to primitive */ } Primitive; .D) .NH Modifying constants.h .LP In constants.h, there are a series of lines resemblin: .D( #define SPHERE 0 #define BOX 1 #define TRIANGLE 2 #define SUPERQ 3 #define PLANE 4 #define CYL 5 #define POLY 6 #define PHONGTRI 7 #define CONE 8 #define HF 9 #define LIST 10 #define GRID 11 .D) .LP These lines define the values assigned to the 'type' field in each Primitive and Object. (Actually, a Primitive can never be of type LIST or GRID, but an Object may be.) We must add a similar line for the new primitive. .LP When adding new values, we .I must add the primitive .I before the lines defining the types for LIST and GRID. This is due to the way arrays are indexed in intersect.c So, below the HF type, we add a line for the Disc type and increment the LIST and GRID types: .D( #define CONE 8 #define HF 9 #define DISC 10 #define LIST 11 #define GRID 12 .D) We must also increment PRIMTYPES, the total number of primitives types: .D( #define PRIMTYPES 11 .D) .NH Modifying intersect.c .LP In intersect.c, several arrays are declared and initialized which are indexed by Primitive or Object type. We must modify these array declarations to reflect the addition of the new primitive type. The first array to be modified is : .D( double (*objint[])() = {intsph, intbox, inttri, intsup, intplane, intcyl, intpoly, inttri, intcone, inthf}; .D) This array of pointers to functions contains the names of the functions which perform ray/primitive intersection tests. Here, .I intsph, the 0th element in the array, is the name of the intersection routine for the Sphere primitive, which is declared as type 0 in constants.h. Similarly, .I intplane is the 4th element in the array, and the Plane primitive is defined to be type 4. This is due to the fact that a Primitive's type field is used as an index into this array. .LP So, we must add the name of the new ray/disc intersection test, which we will name "intdisc", to the objint[] array: .D( double (*objint[])() = {intsph, intbox, inttri, intsup, intplane, intcyl, intpoly, inttri, intcone, inthf, intdisc}; .D) Similarly, we must modify the objnrm[] and objextent[] arrays to contain the names of the functions that compute a specific disc's normal and bounding box: .D( int (*objnrm[])() = {nrmsph, nrmbox, nrmtri, nrmsup, nrmplane, nrmcyl, nrmpoly, nrmtri, nrmcone, nrmhf, nrmdisc}; int (*objextent[])() = {sphextent, boxextent, triextent, supextent, planeextent, cylextent, polyextent, triextent, coneextent, hfextent, discextent}; .D) .LP In addition, there is an array of Primitive names which is used when printing statistics. Again, we need to modify the array to include the name of the Disc primitive: .D( char *primnames[PRIMTYPES] = { "Sphere", "Box", "Triangle", "Superq", "Plane", "Cylinder", "Polygon", "Phongtri", "Cone", "Heightfield", "Disc"}; .D) .LP Lastly, there is an array of flags named CheckBounds[] which is indexed by Object type. This array is used by intersect() to determine if a ray/bounding-box intersection test should be performed before a ray/Object intersection test. If the element in CheckBounds[] corresponding to an Object's type is TRUE, a ray/Object intersection test will only occur if a ray/bounding-box intersection test succeeds. (Even if ray/bounding-box tests are not performed for a given primitive, the bounding box is still used to determine the extent of the object when it is included in compound Objects.) .LP We will set the flag corresponding to the Disc primitive to be TRUE. Note that the CheckBounds array is indexed by \fIObject\fR type, so we add the entry for the Disc type before the final two entries in the array: .D( char CheckBounds[] = {TRUE, FALSE, TRUE, FALSE, FALSE, TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE}; .D) .NH Modifying funcdefs.h .LP The file funcdefs.h contains a number of function declarations. We must add declarations for the four new functions to be written. Firstly, we add the declaration of "nrmdisc" to the normal-finding functions: .D( int nrmsph(), nrmbox(), nrmtri(), nrmsup(),nrmplane(), nrmcyl(), nrmpoly(), nrmcone(), nrmhf(), nrmdisc(); .D) .LP Next, we declare "intdisc" with the other ray/primitive intersection test functions: .D( /* * Intersection routines */ double intsph(), intbox(), inttri(), intsup(),intplane(), crossp(), intcyl(), intpoly(), intcone(), inthf(), intdisc(); .D) And the bounding-box routine: .D( /* * Extent-box finding routines */ int sphextent(),boxextent(),triextent(),supextent(),planeextent(), cylextent(), polyextent(), coneextent(), hfextent(), discextent(); .D) .LP And lastly, we add "makdisc", the routine which will create a reference to a particular disc, to the list of object creation functions: .D( /* * Object creation routines */ Object *maksph(), *makbox(), *maktri(), *maksup(), *makplane(), *makcyl(), *makpoly(), *makcone(), *makhf(), *makdisc(), *new_object() .D) .NH Writing disc.c .LP And now we must write the four functions makdisc(), intdisc(), nrmdisc() and discextent(). The first thing you should do is copy the Copyright template to the top of disc.c. .LP Next, we need to #include the necessary header files. In addition to math.h and stdio.h, you will need "constants.h" (which includes the definition of DISC), "typedefs.h" (which includes all sorts of useful structure definitions), and "funcdefs.h" (which includes all sorts of useful function declarations). .NH 2 makdisc() .LP Every primitive-creation function must take a least one argument -- the name of the surface to be associated with the primitive. Besides this argument, these functions will be passed any parameters needed to define a particular primitive. For us, this means two vectors (the center of the disc and its normal) and one double (the radius of the disc). .LP The makdisc() routine will do several things. In addition to creating a new Disc, it must allocate a Primitive structure and set its fields appropriately -- it must point to the new Disc, have its "type" field set correctly, and it must point to the surface associated with the primitive. In addition, an Object which points to this new Primitive must be created and initialized properly. It is this Object structure which is returned by makdisc(). .LP So, we write: .D( Object * makdisc(surf, center, radius, norm) char *surf; Vector center, norm; double radius; { Disc *disc; /* Pointer to new disc. */ Primitive *prim; /* Pointer to new Primitive */ Object *newobj; /* Pointer to new Object */ Vector tmpnorm; /* normalized normal */ extern int Quiet; /* True if we shouldn't complain */ extern int yylineno; /* Current line # in input file. */ if (radius < EPSILON) { if (!Quiet) fprintf(stderr,"Degenerate disc (line %d).\\n", yylineno); /* * Don't create this primitive. */ return (Object *)0; } tmpnorm = norm; if (normalize(&tmpnorm) == 0.) { if (!Quiet) fprintf(stderr,"Degenerate disc normal (line %d).\\n", yylineno); return (Object *)0; } /* * Allocate new Disc. */ disc = (Disc *)Malloc(sizeof(Disc)); /* * Initialize new disc. * We store the square of the radius to save us a sqrt(). */ disc->radius = radius*radius; disc->center = center; disc->normal = tmpnorm; /* * Compute plane constant. */ disc->d = dotp(¢er, &tmpnorm); /* * Allocate new Primitive */ prim = mallocprim(); /* * Set Primitive type and pointer to new Disc. */ prim->type = DISC; prim->objpnt.p_disc = disc; /* * Search for named surface in list of defined surfaces. * find_surface() will exit if the surface is not found. */ prim->surf = find_surface(surf); /* * Create and return new object with NULL name, of type DISC, * which points to the new Primitive and has no transformation * associated with it. */ return new_object(NULL, DISC, (char *)prim, (Trans *)NULL); } .D) .LP In this case, our primitive creation function is straight-forward. In some cases, in order to facilitate ray-primitive intersection tests, a more general version of the primitive is created, and a transformation matrix is computed to transform the generic primitive to the specific primitive requested by the user. Then, one need only perform intersection tests against the generic version of the primitive. Examples of these types of primitives are the cone and cylinder. The generic versions of both of these primitives have their main axes coincident with the Z axis and their base at the origin. Transformations are computed in makcyl() and makcone() to transform the generic case to the specific case. See "cone.c", "cylinder.c" and "input_yacc.y" for details. .NH 2 intdisc() .LP Each primitive/ray intersection routine is passed three values: a pointer to a Primitive, the origin of the ray, and the direction of the ray. Each intersection function must do two things. Firstly, it must increment an element in the primtests[] array to reflect the fact that a ray/primitive intersection test has occurred. Most importantly, each function must return the distance from the ray origin along the ray direction to the closest point of ray/primitive intersection. This distance must be greater than EPSILON. If it is less than EPSILON, it is assumed that no intersection occurs between the ray and the given primitive. If not valid intersection exists, 0 is returned. .LP So, we write: .D( double intdisc(pos, ray, obj) Vector *pos, *ray; Primitive *obj; { Disc *disc; Vector hit; double denom, dist; extern unsigned long primtests[]; primtests[DISC]++; disc = obj->objpnt.p_disc; denom = dotp(&disc->normal, ray); if (denom == 0.) return 0.; dist = (disc->d - dotp(&disc->normal, pos)) / denom; if (dist > FAR_AWAY || dist < EPSILON) return 0.; /* * Find difference between point of intersection and center of disc. */ addscaledvec(*pos, dist, *ray, &hit); vecsub(hit, disc->center, &hit); /* * If hit point is <= disc->radius from center, we've hit the disc. */ if (dotp(&hit, &hit) <= disc->radius) return dist; return 0.; } .D) .NH 2 nrmdisc() .LP Each primitive normal routine is passed a location to a primitive, a pointer to a point on the surface of the primitive, and a pointer to a vector which must be set to the normal to the primitive at the point of intersection. For the disc, this is very simple, as the disc is planar: .D( Vector nrmdisc(pos, prim, nrm) Vector *pos, *nrm; Primitive *prim; { *nrm = prim->objpnt.p_disc->normal; } .D) .NH 2 discextent() .LP The discextent() routine is passed a pointer to a disc Primitive as well as a bounding box stored as a 2 by 3 array of doubles. The routine computes the extent of the disc along each axis and fills the bounding box array appropriately. .LP Note that the bounding box of all primitives should be at least 2*EPSILON along each axis to avoid problems with roundoff error. Fortunately, primextent(), routine which calls the primitive bounding box functions, will check for and "widen" degenerate bounding boxes. Thus, the bounding box volumes are allowed to compute degenerate boxes. Also, if a primitive is unbounded (e.g., a plane), the maximum X extent should be set to be less than the minimum X extent. .LP So, discextent is written as: .D( discextent(prim, bounds) Primitive *prim; double bounds[2][3]; { Disc *disc; double extent, rad; disc = prim->objpnt.p_disc; rad = sqrt(disc->radius); /* * Project disc along each of X, Y and Z axes. */ extent = 1. - disc->normal.x * disc->normal.x; extent *= rad; bounds[LOW][X] = disc->center.x - extent; bounds[HIGH][X] = disc->center.x + extent; extent = 1. - disc->normal.y * disc->normal.y; extent *= rad; bounds[LOW][Y] = disc->center.y - extent; bounds[HIGH][Y] = disc->center.y + extent; extent = 1. - disc->normal.z * disc->normal.z; extent *= rad; bounds[LOW][Z] = disc->center.z - extent; bounds[HIGH][Z] = disc->center.z + extent; } .D) .NH input_lex.l .LP Among other things, input_lex.l contains a list of all the keywords recognized by rayshade. To this list we must add the keyword for the new primitive type. Following the example of other keywords, we add: .D( disc {return(tDISC);} .D) .NH input_yacc.y .LP Near the top of input_yacc.y are the declarations for the tokens returned by lex. We must add the new token, tDISC, to this list: .D( %token tBACKGROUND tBLOTCH tBOX tBUMP tCONE tCYL tDIRECTIONAL tDISC .D) Finally, we need to add the production to the yacc grammar which will call makdisc(). We first modify the Primtype production to include a new production named Disc: .D( Primtype : Plane | Sphere | Box | Triangle | Cylinder | Cone | Superq | Poly | HeightField | Disc ; .D) .LP Next, near the productions for the other primitives, we add: .D( Disc : tDISC tSTRING Vector Fnumber Vector { /* * disc surfname center.x center.y center.z * radius norm.x norm.y norm.z */ LastObj = maksph($2, $3, $4, $5); } ; .D) .NH Testing .LP Once you add "disc.o" to the OBJS list in the Makefile, you should be able to re-compile rayshade. A good input file for testing the disc primitive might be: .D( /* * Demo picture of a textured ground plane with a sphere, cone * and disc. The disc is situated on the top of the cone and faces * up and towards the viewer. */ eyep 0. 25. 7. screen 256 256 light 1.4 point -15. 20. 15. surface red .02 0 0 .5 0 0 .2 .2 .2 32. 0.0 0 0 surface blacktile 0.01 0.015 0.01 0.02 0.03 0.02 0.3 0.35 0.3 30 0. 0 0 surface white .02 .02 .008 .5 .5 .25 0.8 0.8 0.8 18 0. 0 0 surface glass 0.02 0.02 0.02 0. 0. 0. 0.8 0.8 0.8 25 0. 0. 1.15 disc white -5. 3. 4. 3. 0. 1. 1. sphere red 4. 3 0 0 /* * Cone actually sticks through ground plane. This solves problems * that arise when the bottom of the cone and the plane are coincident. */ cone glass -5. 3 -4.1 -5. 3. 4. 4. 0. plane white 0. 0. 1. 0. 0. -4. texture marble scale 4. 4. 4. texture checker blacktile translate 0. 0. 0.3 scale 4. 4. 4. .D)