Adding Textures to Rayshade Draft Craig Kolb Department of Mathematics Yale University New Haven, CT 06520 This tutorial describes the process of adding new textures to rayshade. Although the texturing interface is relatively straight-forward, it is dif- ficult to see the "big picture" by studying the source code, as changes must be made in a number of different files. While this tutorial is primarily meant for those interested getting their hands dirty, and assumes familiarity with solid texturing, 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. Adding new textures to rayshade involves modifying at least four source files: 1. texture.h A numerical type is given to the texture. The rou- tines to create a reference to the new texture and the texturing function itself are declared. 2. texture.c At least two new routines are added. The first creates and returns a reference to a texture of the given type, and the second performs the actual tex- turing. In addition, an array of pointers to func- tions is modified to include the new texturing func- tion. 3. input_lex.l The keyword used to identify the new texture is added to the list of recognized keywords. 4. input_yacc.y The new texture is added to the list of textures parsed by the yacc grammar. The added code will call the routine which creates and returns a refer- ence to the texture type which was defined in Rayshade Texture Tutorial -2- Draft texture.c In this tutorial, a new texture, mountain, is added to rayshade. This texture will modify the diffuse component of a surface as a function of the Z component of the point of intersection. If the name of a colormap is given, an index into the colormap is computed and the corresponding color is used to scale the ambient and diffuse components of the surface. Otherwise, the ambient and diffuse components of the surface are simply scaled. To avoid strictly horizontal boundaries between colors when using a colormap, we add a bit of "noise" to the Z component of the point of intersection. The magnitude and nature of the noise is defined by the user. The Texture type All textures in rayshade are referenced using a sin- gle Texture structure. This structure is defined as: typedef struct Texture { char type; /* Texture type */ Surface *surf1; /* Alternate surface */ double size; /* Scale/size factor */ double *args; /* Random arguments. */ Color *colormap; /* Colormap */ Trans *trans; /* Transformation matrices. */ struct Texture *next; /* Pointer to next texture. */ } Texture; The type field is used by apply_textures() to deter- mine which texturing function to call. The trans field and next field are used internally by rayshade. The rest of the fields are for use by the texturing functions. The args field provides a pointer to space for stor- ing an arbitrary number of floating-point parame- ters. The size field is a handy general-purpose floating-point value (the idea being that you get one parameter "free" with every Texture structure). The colormap field is generally used to store an array of Colors read from an ascii colormap file. The surf1 field is often set to point to a secondary surface. This is useful if the texture performs some sort of interpolation between two surfaces -- the first being the surface assigned to the object being textured and the second specified as an argu- ment to the texture (e.g., the blotch texture). for Rayshade Texture Tutorial -3- Draft an example). 1. Modifying texture.h The file texture.h contains a list of #defines which look something like: #define CHECKER 0 /* Checkerboard */ #define BLOTCH 1 /* Color blotches */ #define BUMP 2 /* Bump mapping */ #define MARBLE 3 /* marble texture */ #define FBM 4 /* fBm texture */ #define FBMBUMP 5 /* fBm bump map */ #define WOOD 6 /* wood-like texture */ These numerical types are used to identify the type of a given texture structure (via the type field in the Texture structure). We need to add a similar definition for our new texture. After the WOOD definition, we add: #define MOUNTAIN 7 /* bad mountain texture */ In addition, we must declare the two new functions which we will add to texture.c. The first function, NewMountainText(), will return a pointer to a Tex- ture structure: Texture *NewMountainText(); The second routine, MountainText, returns nothing, but needs to be declared: int MountainText(); 2. Modifying texture.c Firstly, we must include the new texturing function in the array of texturing functions used by rayshade. This array, indexed by texture type, is used by apply_textures() to call the correct textur- ing function. So, we modify the textures[] definition to read: int (*textures[])() = {CheckerText, BlotchText, BumpText, MarbleText, fBmText, fBmBumpText, WoodText, MountainText}; Note that MOUNTAIN was defined to be 7, and that Rayshade Texture Tutorial -4- Draft MountainText is texture number 7 (starting from 0) in the array. Next, we need to write NewMountainText(), which will create and return a reference to the texture. Our new texture will be a function of 5 parameters: scale amount to scale Noise() by omega, lambda fBm parameters octaves number of octaves of Noise() in fBm colormap name of colormap file, if any Thus, we add to the end of texture.c: Texture * NewMountainText(scale, omega, lambda, octaves, mapname) double scale, omega, lambda; int octaves; char *mapname; { /* * Pointer to new texture. */ Texture *text; /* * Allocate new texture of type MOUNTAIN */ text = new_texture(MOUNTAIN); /* * Allocate space to store omega, lambda and octaves. */ text->args = (double *)Malloc(3 * sizeof(double)); text->args[0] = omega; text->args[1] = lambda; text->args[2] = (double)octaves; /* * scale is stored in 'size'. */ text->size = scale; /* * If a colormap name was specified, read it into 'colormap'. */ if (mapname != (char *)0) text->colormap = read_colormap(mapname); /* * All done -- return new texture. */ return text; } Thus, NewMountainText is called with the desired parameters and a new Texture is returned. Rayshade Texture Tutorial -5- Draft Finally, we must write the routine which actually performs the texturing. Each texturing function is called by apply_textures() with the following argu- ments: text a pointer to the Texture being applied pos a pointer to the coordinates of the point of intersection norm a pointer to the surface normal at the point of intersection surf the pointer to a copy of the surface of the object being textured (a copy is used so that the surface can be modified for a given shading calculation without affecting subsequent calculations). Thus, we write: MountainText(text, pos, norm, surf) Texture *text; Vector *pos, *norm; Surface *surf; { double val; int index; /* * Compute value of fBm (fractional Brownian motion) for * the given point. */ val = fBm(pos, text->args[0], text->args[1], (int)text->args[2]); /* * Scale the result as requested and add in the Z component of * the point of intersection. Note that in a better texture * we'd probably have additional parameters to afford * greater control of val. */ val = pos->z + text->size * val; if (text->colormap) { /* * If we're using a colormap, compute an index into * the colormap and use the appropriate color as the * diffuse component of the surface. */ index = 255. * val; if (index > 255) index = 255; if (index < 0) index = 0; surf->diff.r *= text->colormap[index].r; surf->diff.g *= text->colormap[index].g; Rayshade Texture Tutorial -6- Draft surf->diff.b *= text->colormap[index].b; surf->amb.r *= text->colormap[index].r; surf->amb.g *= text->colormap[index].g; surf->amb.b *= text->colormap[index].b; } else { /* * If there's no colormap, simply scale the diffuse * component. */ ScaleColor(val, surf->diff, &surf->diff); ScaleColor(val, surf->amb, &surf->amb); } } 3. input_lex.l Now that we have the hard parts written, all that is left is making the parser recognize the new texture. To do this, we first need to add a keyword for our texture to the list of keywords recognized by rayshade. This is done by editing input_lex.l. The file input_lex.l contains, among other things, an alphabetical list of rayshade's keywords. To add a new keyword, one simply follows the example of the other keywords. Thus, we add the line: mountain {return(tMOUNTAIN);} between the lines defining 'mist' and 'object'. This line directs lex to return the token tMOUNTAIN whenever the string "mountain" is encountered in an input file. 4. input_yacc.y Finally, we need to write the code which will actu- ally create an instance of the new texture by cal- ling NewMountainText(). This is done in input_yacc.y. In input_yacc.y, there are a series of lines which look something like: %token tBACKGROUND tBLOTCH tBOX tBUMP tCONE tCYL tDIRECTIONAL %token tENDDEF tEXTENDED tEYEP tFBM tFBMBUMP tFOG tFOV tGRID %token tHEIGHTFIELD tLIGHT tLIST tLOOKP tMARBLE tMAXDEPTH tMIST %token tOBJECT tOUTFILE %token tPLANE tPOINT tPOLY tROTATE %token tSCALE tSCREEN tSPHERE tSTARTDEF tSUPERQ tSURFACE tRESOLUTION %token tTHRESH tTRANSLATE tTRANSFORM tTRIANGLE tUP tENDFILE %token tTEXTURE tCHECKER tWOOD Rayshade Texture Tutorial -7- Draft These lines declare the tokens returned by lex. We need to declare tMOUNTAIN in a similar manner. So, we change the last line to read: %token tTEXTURE tCHECKER tWOOD tMOUNTAIN Next, we need to call NewMountainText() in the proper place. In input_yacc.y, there is a produc- tion which reads something like: Texturetype : tCHECKER String { $$ = NewCheckText($2); } | ... ... | tWOOD { $$ = NewWoodText(); } ; These productions invoke the proper texture creation routine when appropriate. For example, when the keyword corresponding to tCHECKER is followed by a String, yacc will invoke NewCheckText() with the string (the name of a surface, in this case) as an argument. The Yacc grammar understands the follow- ing datatypes, among others: String a series of alphanumerics surrounded by white space (i.e., the string need not be quoted) Fnumber a floating-point number tINT an integer Vector a vector (x, y, z) Color a color (r, g, b) To add a texture to the list of recognized textures, we change: ... | tWOOD { $$ = NewWoodText(); } ; to: | tWOOD { $$ = NewWoodText(); } Rayshade Texture Tutorial -8- Draft | tMOUNTAIN Fnumber Fnumber Fnumber tINT { $$ = NewMountainText($2, $3, $4, $5, (char *)0); } | tMOUNTAIN Fnumber Fnumber Fnumber tINT String { $$ = NewMountainText($2, $3, $4, $5, $6); } ; The first new production invokes NewMountainText() when the keyword associated with tMOUNTAIN ("moun- tain") appears in an appropriate place in the input file followed by four parameters (scale, omega, lambda, and octaves). In this case, NewMountainText is passed a NULL pointer as the name of the colormap to use. This code creates a reference to a mountain texture that does not make use of a colormap. So, this production is invoked whenever a line such as the following is encountered in the input file: texture mountain 0.2 0.5 2.0 6 The second production works in a similar manner, except that it passes a colormap name to NewMoun- tainText(). It handles lines such as: texture mountain 0.2 0.5 2.0 6 mountain.map 5. Compiling That, in theory, is all there is to it. Run 'make' to recompile input_lex.o, input_yacc.o, and texture.o. 6. Testing A good test input file for the new texture might be something like: screen 512 512 eyep 0 -10 0 lookp 0 0 0 fov 20. light 1.0 directional 1. -1. 1. surface boring .1 .1 .1 .8 .8 .8 0 0 0 0 0 0 0 sphere boring 1. 0. 0. 0. texture mountain 0.2 0.5 2.0 6 planet.map Rayshade Texture Tutorial -9- Draft