Accessibility
 
Home / Developer Center / Director Developer Center /

Director Article

3D maze magic - Part two: Texturing a Lingo box maze dynamically

In this part of the series, we'll look at how to do some decorating within the box maze. We'll discuss some tricks to optimize the number of polygons used to display the maze and learn how you can control the appearance with shaders and textures of each wall within the maze.

 
Optimizing the maze
There's not much error checking or optimizing in part one, because I didn't want to stray too far from the basics. Now that we're getting further into it, I'll add some of the more mundane (and yet absolutely necessary) elements. Let's start with the optimization of the polygons. Switch your mind back to the map of the maze in a bird's eye view.
 

Now imagine that each cell is a box with 5 sides, bottom, front, back, left and right. Smoosh some boxes together in your mind and notice that where each wall touches the next, there are actually two (2), that's right TWO walls at every position where walls touch another cell.

 
The outer extremes of the maze don't suffer from this redundancy—and we've already culled both walls when we took away a wall in order to create a pathway in the maze. This leaves several walls doubling up, creating dozens of extra polygons in the scene. Eventually they would create rendering problems. (The maze would display with streaky interpenetrating images, like those that appear when the system can't decide on an appropriate z depth for the planes.)
 
To avoid the doubled walls and resolve the z depth issues, we need to adjust our list of box walls. This time, we'll start with an understanding that all boxes except the bottom row and the far right side will only use either the front or the left wall. Fronts of the second row become back walls for the first, and left walls of the second column become right walls for the first. This pattern continues to the far right and bottom (or back) of the maze map. The amended list looks like this:
 
put value(member("plan").text)
-- ["BR", "BLR", "BLR", "BLR", "BLR", "BLR", "BLR", "BLR", "BL", 
"FBR", "BR", "BLR", "BLR", "BLR", "BLR", "BLR", "BLR", "FB", 
"FBR", "FBR", "BR", "BLR", "BLR", "FBLR", "BLR", "BLR", "FBL", 
"FBR", "FBR", "FBR", "BR", "BLR", "BLR", "BLR", "BLR", "FB", 
"FBR", "FBR", "FBR", "FBR", "BR", "FBR", "FBR", "FBR", "FB", 
"FBR", "FBR", "FBR", "FBR", "FBR", "FBR", "FBR", "FBR", "FBL", 
"FBR", "BLR", "FBLR", "FBR", "FBR", "FBLR", "FBR", "FBR", "FB", 
"FBR", "FBR", "FBR", "FBR", "BLR", "FBR", "FBR", "FBR", "FB", 
"FBR", "FBR", "FBR", "FBR", "BLR", "FBLR", "BLR", "FBLR", "FB", 
"FR", "FLR", "FR", "FR", "LR", "LR", "LR", "LR", "FL"]
 
Note that this drops your polygon count from 1052 to 780 polygons. That's a 26% reduction in polygons and there will be a reasonable increase in performance when you do trimming like this. (Not to mention the improved appearance.) Also notice that there are no 'B's' in the bottom row and no 'R's' in the right row. Other than that, simply add B's and R's to every other slot that didn't already have them.
 
Painting the walls
The next logical step in creating this maze is to add some color to the walls. It's important, before we go much further, to make certain that you have a reasonable grasp of the way that things become colored within Director's 3D members. Just as in most 3D creation and display utilities, Director breaks the display of materials into several different parts in an attempt to approximate the sorts of things that influence the appearance of objects, just as they might be revealed in the natural world.
 
We only see things because there is light. Therefore in the 3D world, nothing is visible without the presence of light. Take that principle a bit further and you'll rapidly realize that it doesn't limit itself to visibility, but the amount and quality of the light determine the color, clarity, and other qualities of the models. Light interacts with the colors of the materials to determine the end display.
 
The material itself is also broken into two major portions, the shader and the texture. Think of the shader as the base and the texture as a sort of optional decorative skin. (But don't forget that the shader has tremendous potential to determine the look and feel of the finished model.)
 
In Director 3D, virtually everything is an object. The root object is the member. In order to do just about anything, you'll need to reference the member. It holds the special commands and properties that you need to use to get the job done. You cannot simply ask model("chair") to move for example. You must ask member("world").model("chair").translate(x,y,z).
 
The model object is a property of the member. The translate command is referenced in the same way as subordinate to the model. Shaders and textures live within this hierarchy as well. When you create a shader or a texture, it will live as a child object of the world as in: (member(n).shader(n)). These shader and texture objects are available to any model, so you may use them over and over, but they are individual resources—like a single cast member. That means that making an alteration to a shader or texture will cause that alteration to happen globally. Got a forest full of trees sharing a single shader or texture? Make one purple, and they'll all turn purple. Like a single cast member, changes to the original resource will be displayed on all models (sprites carry the cast metaphor even further).
 
Now, one of the cool things about models within the Director 3D paradigm is that a model may divided into multiple meshes (groups of polygons). When you generate a box primitive, it is automatically divided into six meshes. So, while the obvious way to adjust the shader of a model is to change the member(n)Model(n).shader, this will really only affect the first of six shaders assigned to that box. You could change all of the shaders by referencing the shaderlist, as shown below:
 
member(n)Model(n).shaderlist = member(n).shader(n)
 
However, in our case, we want to adjust the individual shaders of each mesh within the model. The floors will have one texture and the walls will have another. Additionally, we may want to add some special walls along the way.
 
For us then, the syntax to deal with any individual shader will be:
 
member(n)Model(n).shader(n) = member(n).shader(n)
 
Note that while the shader of the member refers to a shader object, the shader of the model is a property that stores a reference or pointer to the actual shader object.
 
Next, let's shade those walls. We'll make the bulk of the walls foliage, like a garden maze. And we'll make the floor surfaces grass and path.
 
Note the ridiculously small size of my bush texture: (Yes this is the actual size.)
 
This image is a JPEG which uses 482 bytes. Needless to say, it will leave a very small footprint. Every bit counts, in terms of RAM needed for texture display, so it's important to try to keep the images as small as humanly possible. One trick I'm fond of is layering the textures to create an illusion of depth. In fact, in this exercise, we'll layer this texture on top of itself. The trick is, we'll tile one version of the texture and leave the other stretched to fit the entire surface of each mesh.
 
I'll use two texture maps for the ground cover, but I'll use them in a similar way. Here they are:  
 
The gray image will create noise texture as it tiles beneath and the green texture will cover the entire surface area. The ground is 634 bytes and the noise is 398 bytes. Combined, all three texture maps will be 1.4K in size. In addition to requiring only 1.4 K of RAM to display, our frugal use of bitmaps will go a long way in ensuring a speedy display of the maze.
 
The code to create the textures is provided below:
 

global w

on newTextures()
   -- build the shaders



   -- ground
   g = w.newShader("ground", #standard)
   g.flat = TRUE
   --no gouraud shading
   g.shininess = 0
   --no highlight
   g.emissive = rgb(255,255,255)
   --emit pure white...this allows textures
   --to show up even with no light in the scene
   -- bush
   bsh = w.newShader("bush", #standard)
   bsh.flat = TRUE
   --no gouraud shading
   bsh.shininess = 0
   --no highlight
   bsh.emissive = rgb(255,255,255)
   --emit pure white...this allows textures
   --to show up even with no light in the scene

   -- build the textures

   gTS = w.newTexture("noise", #fromCastMember, member("noise"))
   gTM = w.newTexture("ground", #fromCastMember, member("ground"))
   nT = w.newTexture("bushSmall", #fromCastMember, member("bush"))
   bT = w.newTexture("bushLarge", #fromCastMember, member("bush"))

   -- Tweak the texture parameters to
   -- display repeating textures within layers --
   -- ground
   g.textureList[1] = gTS
   g.textureRepeatList[1] = TRUE
   --this allows the texture to repeat when
   --texture scale is less than 1
   g.textureTransformList[1].scale = vector(.25,.25,1)
   --scale texture to look good
   g.textureList[2] = gTM
   --this allows the texture to repeat when
   --texture scale is less than 1
   --bush
   bsh.textureList[2] = bT
   bsh.textureList[1] = NT
   bsh.textureRepeatList[1] = TRUE
   --this allows the texture to repeat
   --when texture scale is less than 1
   bsh.textureTransformList[1]Scale = vector(.25,.25,1)
   --add noise to the beach, better
   --look...not much added size
end

 
The global reference to w is a reference to the 3D member. I find it easier to simply create a quick reference to the member rather than retyping it thousands of times. The first step in this handler is to create a new shader. Textures are assigned as properties of shaders (though they exist as independent objects as well) so it is sensible to create your shaders first.
 
Set the flat property of the shader to true to switch off the gouraud shading feature. It's normally necessary, but we're all about speed today and so we'll sacrifice some quality for performance.
 
Next, turn off the shininess. If there are specialty lights in the scene I don't want them wasting their time creating highlights on these bushes. Finally, set the emissive property to a nice bold white and move on to the next shader.
 
The textures in this case are built from cast members. Each is assigned a variable reference so they may be easily called later. As you can see, the names within the class of textures must be unique, but they may share names with models or cast members, etc.
 
Next, assign the newly created textures to the texture list of each respective shader. This way you can layer the noise and primary textures in order to get a breakup pattern even though the primary texture is extremely small. Note that the textureRepeatList property of the correlating texture item must be set to true in order to tile the texture. (Some video cards don't support this feature.)
 
Finally the scale of the repeating texture is set and we're ready to rock. You may have noticed that the texture and shader is not applied to any models here, they are simply created.
 
The other half of the formula requires a bit more work than it probably should. If I hadn't tampered with the resources of the boxes, they would all have shaderList values that matched and the bottom of each box would be in the same position. I'm not so lucky in this case. As a result, it takes an odd series of checks to find the right side of the box to apply the ground texture. The following handler places shaders and textures on the box faces and its subroutines validate which boxes get which surfaces on which face.
 

global w

on paint()
   repeat with x = 1 to 90
      if checkCaseX(x) then
         w.model[x].shaderlist = w.shader[3]
         w.model[x].shaderlist[4] = w.shader[2]
      else
         -- find the right bottom
         b = findBottom(x)
         w.model[x].shaderlist = w.shader[3]
         w.model[x].shaderlist[b] = w.shader[2]
      end if
   end repeat
end


on findBottom(x)
  sixList = [1,9,11,18,21,31,36,41,45,63,72,81,82,84,85,86,87,88,89,90]
  .twoList = [24,57,60,78,80]
case
TRUE of
   (getOne(sixList, x)):
      return 6
   (getOne(twoList, x)):


on findBottom(x)
   .sixList = [1,9,11,18,21,31,36,41,45,63,72,81,82,84,85,86,87,88,89,90]
   twoList = [24,57,60,78,80]
case
TRUE of
   (getOne(sixList, x)):
      return 6
   (getOne(twoList, x)):
      return 2
   end case
end

on checkCaseX(x)
   if x > 1 AND x < 80 then
     if (x <> 9) AND (x <> 11) AND\
        (x <> 18) AND\
        (x <> 21) AND\
        (x <> 24) AND\
        (x <> 31) AND\
        (x <> 36) AND\
        (x <> 41) AND\
        (x <> 45) AND\
        (x <> 57) AND\
        (x <> 60) AND\
        (x <> 63) AND\
        (x <> 72) AND\
        (x <> 78) then
        return TRUE
      else
         return FALSE
      end if
   else
      if
x <> 83 then
         return FALSE
      else
         return TRUE
      end if
   end if
end

 
The paint handler is called immediately after the newTexture handler. It uses the checkCaseX subroutine to determine whether or not the box is an exception to the bottom, meaning it is a side 4 system. If it is not a side 4 system, then it simply builds that box with the ground in shaderlist 4. If it is an exception, then there are two varieties. Some have the bottom in slot 6 and others have the bottom in slot 2. The findBottom subroutine is used to sort the two varieties out and it returns the integer that represents the correct slot for any given model.
 
How did I figure out which was which? Good old fashioned trial and error. ;(
 
I've included the source for our maze movie (as it appears at this point) on the first page of this tutorial. It is named maze_magic_1.dir. If you haven't already, you can download the source files and tinker away in Director. I added a bit of camera interaction to this sample, so you can look around a bit more. We'll discuss working with a camera in the next section. For now, use the arrow keys to move around and use Arrow+Shift to rotate the camera vertically.
 
In part three, we'll walk through some basic collision options. We'll also discuss how to control and manipulate the camera.
 
 
  Previous Contents Next