 |
 |
| 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. |
| |
| |
| |
| |
|
|
|