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))
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)
Copyright 1997-2024, Director Online. Article content copyright by respective authors.