Articles Archive
Articles Search
Director Wiki
 

Making 3D Simpler

May 20, 2005
by Gary Rosenzweig

I love playing around in Director's 3D environment. I'm not an artist, so I don't start in a 3D graphics tool creating models. Instead I just start with Lingo. I create primitive shapes using Lingo to add elements, rather than importing models.

But this can be a pain. To create a simple sphere, you need to first initialize the world, then create a sphere resource, then set the resource properties, then create a model from the resource, then set the model's properties, then create a shader and texture, then set those properties, and so on.

Wouldn't it be nice if there was a way to create a primitive shape, complete with properties, position, rotation, and a texture or color, all in one line of Lingo code?

Well you can! To do it, I've created a simple library of 3D handlers that make doing this rather simple. The handlers themselves are pretty simple as well.

For instance, say you wanted to create a cone. Here is how you might do it:

myModelResource = sprite(me.spriteNum).member.newModelResource("mycone",#cylinder,#both)
myModelResource.topRadius = 2
myModelResource.bottomRadius = 10
myModel = sprite(me.spriteNum).member.newModel("mycone",myModelResource)
myModel.transform.position = vector(30,-30,0)
myModel.transform.rotation = vector(0,0,-45)
myShader = sprite(me.spriteNum).newShader("myshader",#standard)
myTexture = sprite(me.spriteNum).newTexture("mytexture",#fromCastMember,"My Texture")
myShader.texture = myTexture
myModel.shaderList = myShader

Wow. That's a lot of lines of code. Now what if you need to create 10 or 20 such shapes? Wouldn't something like this be better:

glAddShape([#name: "My Cone", #type: #cylinder, #resourceProps: [#topRadius: 2, #bottomRadius: 10], #position: vector(30,-30,0), #texture: "My Texture", #rotation: vector(0,0,-45)])

So, now all you need is the glAddShape handler. That is the primary handler in my little gl (Graphics Library) set of handlers.

First, I start off with some assumptions. The first assumption is that I will only be using one 3D sprite. This way, I can set a global to the sprite's member and easily reference it in any handler.

Using that assumption, I can write a glInitWorld handler that will use the critical resetWorld function and also set the gWorld global that the rest of the handlers will use.

global gWorld
on glInitWorld spriteObject
  gWorld = sprite(spriteObject.spriteNum).member
  gWorld.resetWorld()
end

In the code below, you'll notice that I take a different approach to passing parameters to handlers than you may be used to. Instead of a list of parameters separated by commas, I pass a single property list into the handlers. This property list can contain any number of properties, each visibly defined right there. Compare these two alternatives:

glAddShape("My Cone" #cylinder, 2, 10, vector(30,-30,0), "My Texture", vector(0,0,-45))

and

glAddShape([#name: "My Cone", #type: #cylinder, #resourceProps: [#topRadius: 2, #bottomRadius: 10], #position: vector(30,-30,0), #texture: "My Texture", #rotation: vector(0,0,-45)])

Sure, the first one is shorter, but what does it mean? Which vector is for the position and which is for the rotation? It is impossible to tell without finding the handler itself and comparing each parameter. In addition, it is hard to use fewer or more parameters as each spot in the parameter list must be occupied. Plus, once we are in the handler, we can refer to a nice property list to get vlaues, rather than a whole set of different variables.

The glAddShape handler takes two primary parameters. The first is the name we wish to give the model. the second is the type of shape: #box, #sphere, #cylinder or #plane.

We can also give several optional parameters. #faces can be #front, #back or #both. This, like just about all parameters, are passed right to Director's 3D Lingo functions. We're not creating anything new here.

The #resourceProps parameter is a property list of resource properties. Again, these will just be passed on to the proper core Lingo functions. So for a sphere, we can use a property of #radius, while a box would need #width, #height and #length. Or, you could leave any of these out to get the default values.

We also take a bunch of optional model properties, like #position, #rotation, and #scale. Then we take the optional #color and #texture properties as well. We'll get to what they do later.

-- CREATE A SHAPE RESOURCE AND ADD ONE COPY
-- props: #name, #type
-- props optional: #faces, #resourceProps, #position, #rotation, #scale, #texture, #color
on glAddShape props
  resource = glAddShapeResource(props)
  return glAddShapeFromResource(resource, props)
end

So the glAddShape handler doesn't do much on it's own. It passes off the resource creation to glAddShapeResource and then the model creation to glAddShapeFromResource.

The glAddShapeResource handler creates a model resource using the properties passed to it via the glAddShape handler. Mostly, it uses the subproperties of the #resourceProps property. these can be things like #radius or #width. It then returns the new resource.

-- CREATE A SHAPE RESOURCE
-- props: #name, #type, #faces, #resourceProps
on glAddShapeResource props
  -- create resource
  if props[#faces] <> VOID then
    r = gWorld.newModelResource(props.name, props.type, props.faces)
  else
    r = gWorld.newModelResource(props.name, props.type)
  end if
  
  -- set resource properties according to props.resourceProps
  if not voidP(props[#resourceProps]) then
    repeat with i = 1 to props.resourceProps.count
      setProp(r,getPropAt(props.resourceProps,i),props.resourceProps[i])
    end repeat
  end if
  
  return r
end

The glAddShapeFromResource handler now takes the resource and creates a model. It will take as the first parameter either the resource itself, or a string with the name of the resource. When glAddShape calls it, it will get the resource itself. But we may want to call this handler directly, and doing so would be easier if we could just use the name of the resource that was already created. The handler looks for values for the properties #position, #rotation and #scale and sets the model's transform to those values if they are available. Otherwise, defaults are used.

If the property #color is set, then the shaderList is set to the returned value of glGetColorShader. If the property #texture is set, then the shaderList is set to the returned value of glGetTextureShader. Both of these handlers will create simple shaders. We'll look at those next.

-- GIVEN A RESOURCE, MAKE A SHAPE
on glAddShapeFromResource resource, props
  if stringP(resource) then resource = gWorld.modelResource(resource)
  m = gWorld.newModel(props.name, resource)
  if props[#position] <> VOID then m.transform.position = props.position
  if props[#rotation] <> VOID then m.transform.rotation = props.rotation
  if props[#texture] <> VOID then m.shaderList = glGetTextureShader(props.texture)
  if props[#color] <> VOID then m.shaderList = glGetColorShader(props.color)
  if props[#scale] <> VOID then m.transform.scale = vector(props.scale, props.scale, props.scale)
  return m
end

The glGetColorShader handler takes a string like "FF0000" and creates a simple shader using that color. First, it checks to see if that shader already exists. If so, it just passes back that shader. if not, a new shader is created and teh ambient and diffuse light for that shader is set to the color value.

-- GET EXISTING COLORED SHADER OR MAKE A NEW ONE
on glGetColorShader shaderColor
  if gWorld.shader(shaderColor) = VOID then
    s = gWorld.newShader(shaderColor, #standard)
    s.texture = VOID
    s.ambient = rgb(shaderColor)
    s.diffuse = rgb(shaderColor)
    return s
  end if
  return gWorld.shader(shaderColor)
end

The glGetTextureShader takes the name of a bitmap cast member and creates a simple shader and texture of the same name, using that image. It will also check to make sure the shader doesn't already exist. If it does, it simply uses the current shader.

-- GET EXISTING TEXTURED SHADER OR MAKE A NEW ONE
on glGetTextureShader shaderName
  if gWorld.shader(shaderName) = VOID then
    t = gWorld.newTexture(shaderName, #fromCastMember, member(shaderName))
    t.renderFormat = #rgba8888
    s = gWorld.newShader(shaderName, #standard)
    s.texture = t
    return s
  end if
  return gWorld.shader(shaderName)
end

So now we have handlers to create a model resource, add a model from that resource, add a color shader and add a textured shader. So what can we do with it?

Here is a simple handler that can be attached to a 3D sprite that uses these movie handlers. This handler simply creates a default sphere in a default location at a default size. No shader is used.

on beginSprite me
  glInitWorld(me)
  glAddShape([#name: "My sphere", #type: #sphere])
end

The next handler would create a sphere that has a radius and a position.

on beginSprite me
  glInitWorld(me)
  glAddShape([#name: "Another sphere", #type: #sphere, #resourceProps: [#radius: 10], #position: vector(30,30,0)])
end

Here is one that creates a sphere as well as a red shader.

on beginSprite me
  glInitWorld(me)
  glAddShape([#name: "Color sphere", #type: #sphere, #resourceProps: [#radius: 10], #position: vector(-30,30,0), #color: "FF0000"])
end

This next one creates a sphere that uses a textured shader taken from a bitmap member named "My Texture".

on beginSprite me
  glInitWorld(me)
  glAddShape([#name: "Textured sphere", #type: #sphere, #resourceProps: [#radius: 10], #position: vector(-30,-30,0), #texture: "My Texture"])
end

This more complex example creates a cone using a cylinder with different radius values for the top and bottom. It also uses a texture and a rotation value.

on beginSprite me
  glInitWorld(me)
  glAddShape([#name: "My Cone", #type: #cylinder, #resourceProps: [#topRadius: 2, #bottomRadius: 10], #position: vector(30,-30,0), #texture: "My Texture", #rotation: vector(0,0,-45)])
end

This sample movie contains all of the examples above.

The idea here is that you can just copy and paste the "gl" handlers into any movie and start creating 3D primitives very easily. I know this has made it easier for me to start 3D projects and I hope it will do the same for you. The library can be easily expanded to include particle systems and other 3D features as well.

Source files: 3dsimple.zip (Mac & Windows)

Gary Rosenzweig is the Chief Engineer, founder, and owner of CleverMedia, a game and multimedia development company in Denver, Colorado. He is the author of ten books on Macromedia Director and Flash, including his latest, Special Edition Using Director MX.

Copyright 1997-2024, Director Online. Article content copyright by respective authors.