We have now prepared the movie for the generation of 3D content on-the-fly. This section explains how to build an API of custom handlers for the creation of primitives. These custom handlers are designed to approach several of the primitive types discussed in Chapter 2: the plane, sphere, box, and cylinder. The particle primitive is explored in depth in Chapter 17, "Special Effects."
In general, the custom handlers for creating primitives utilize some of the properties of each Modelresource type. For instance, it is not useful to build an extremely specific API with features for every aspect of the cylinder unless you are going to need these features. We will revisit the box and cylinder primitive custom handlers in Chapter 10, "Histograms," and Chapter 11, "Pie Charts," respectively. The custom handlers that we are going to build in this section are quite general, allowing you to modify them as required for your individual projects.
The plane primitive is versatile, simple, and concise, and it can be used in a variety of situations. The plane primitive does not have an excessive amount of properties that define it. The two properties that are most critical are Length and Width. For this reason, we will focus on the Length and Width properties as the controllable parameters for the createplane() custom handler, shown here:
1: Global scene 2: On createplane(planeName, L, W, planeColor) 3: If check3Dready(scene) then 4: Res = Scene.newmodelresource(planeName & "res", #plane) 5: Res.length = L 6: Res.width = W 7: Obj = scene.newmodel(planeName, res) 8: Shd = scene.newshader(planeName & "shd", #standard) 9: Shd.diffuse = planeColor 10: Shd.texture = void 11: Obj.shaderlist = shd 12: Return scene.model(planename) 13: Else 14: Return false 15: End if 16: End
The createplane() custom handler is called with the syntax createplane("modelname", float, float, rgb(R,G,B)). This custom handler is built to conserve your typing. Although calling it requires typing a long line of code, this is easier than typing the contents of the handler each time you want a plane. Also, you are probably aware that creating planes, boxes, and spheres is a good first step, but there are many other concerns to contend with. Because of this, it is best to simplify the parts of code that are crucial but not demanding. What's more, after you have established an API of custom handlers to deal with frequent tasks, it will become easier to perform these repetitive tasks with ease.
If we were creating an environment that called for two planes, it might be easier to create them in other ways. When you realize that you actually need an environment with 30 planes, this method may seem more approachable. This process is easily encapsulated inside of a repeat loop to accomplish the mass-creation of objects. Here's an example:
1: Repeat with x = 1 to 30 2: Createplane("plane" & x, 10, 20, rgb(200,30,30)) 3: End repeat
This example will quickly create 30 planes of a bright red color. If you decide later that you need another plane, you have a very concise way of changing the repeat loop to repeat until 31. Therefore, the time saved in typing accumulates as you develop a project.
In this code example, you might expect that a wrong type error might occur, due to the fact that I am concatenating a string and an integer. Although you might disagree, Lingo is a very forgiving programming language when compared to others. Data types are not strongly enforced in Lingo, as they are in other languages. This logic is therefore a good programming trick for Lingo, but it's not a trick to take to other languages.
Finally, there are many occasions when you might be testing certain functions of the 3D environment on an isolated object. If you were to build a test Model to check one or two functions of a project, it would be tiring to have to type up all the commands to create a plane or a sphere. This method allows you to focus your attention on the needs of your project, rather than spending your energy reinventing the wheel (or in this case, the plane).
The sphere is perhaps one of the more aesthetically pleasing types of primitives in the 3D environment because it exhibits visually interesting qualities. The smooth surface and the ability to simulate complex interactions with light make the sphere an attractive primitive. Even when you're working with primitives, it is obvious that some are more visually engaging.
One reason for this complexity is the curvature of the surface. When you are designing your 3D worlds and modeling many of the objects, you can use the visual interest that draws the eye to curved surfaces to create focal points in your worlds. Although you may not necessarily use a sphere to accomplish this, you might create complex objects with curved surfaces.
Curved surfaces tend to utilize more geometry and therefore more memory than flat-surfaced objects. For this reason, depending on your intended user, you will most likely need to limit the usage of highly descriptive geometry in your scenes. This can aid in emphasizing your choices for using curvilinear surfaces in areas of visual focus.
Finally, as a compositional element, spheres tend to have well-defined areas of highlight, midtone, and shadow. These three visual demarcations are used in a painting technique known as chiaroscuro. This technique is used to create the illusion of three-dimensional objects on a two-dimensional surface. By this description, the approach toward realistic painting techniques are similar to that of a 3D environment. For this reason, you may want to examine the vocabulary of formal composition as a method of learning how to control what you would like to see in your 3D world.
The custom handler for the creation of a sphere is similar to the createplane() handler. Note that the key difference is that the defining property of the sphere is truly its radius. For this reason, our handler will only deal with this one property. Here's the code:
1: Global scene 2: On createsphere(sphereName, R, sphereColor) 3: If check3Dready(scene) then 4: Res = Scene.newmodelresource(sphereName & "res", #sphere) 5: Res.radius = R 6: Obj = scene.newmodel(sphereName, res) 7: Shd = scene.newshader(sphereName & "shd", #standard) 8: Shd.diffuse = sphereColor 9: Shd.texture = void 10: Obj.shaderlist = shd 11: Return scene.model(spherename) 12: Else 13: Return false 14: End if 15: End
The critical lines of code that create Models in the createplane() and createsphere() custom handlers are encapsulated in an if/then statement. This if/then statement utilizes the check3Dready() custom handler we explored earlier. Note that after the successful creation of the Model, createsphere() returns a reference to the code object for the Model. This is true in createplane() as well. Also, upon failure, these custom functions will return false. The reason for this is twofold. Recall in prior examples, as well as in the createsphere() custom handler, that when we create code objects in the 3D world, we often save a reference to those objects in a variable to reduce our typing requirements. If we were to call the createsphere() function from the initialize() custom handler, the returning information allows us to create a reference to the new sphere's code object as follows:
Mynewsphere = createsphere("ball", 27, rgb(50,100,10))
This syntax allows us to refer to this sphere as mynewsphere throughout the initialize() handler. If we were to declare the mynewsphere variable as global prior to this line of code, we could easily reference the sphere throughout the rest of the Movie through this variable. Even as a local variable, this information affords us much latitude in our coding. One addition that we could make would be to encapsulate our creation of the sphere as follows:
-- create the sphere 1: Mynewsphere = createsphere("ball", 27, rgb(10,40,50)) then --check to make sure that mynewsphere is not false 2: If not mynewsphere -- take some action if mynewsphere is false 3: Alert "problem!" 4: End if
Line 3 of this example is criticalwe have decided to alert that there is a problem. We can do a few useful things here: We can either call an alert or halt the program, or we can redirect the Playback Head to a section of the movie where we can make the process of error handling more transparent to the end users. For example, you might send them back to the "hold" marker to make sure that the 3D Castmember is truly ready. Alternatively, you might have a marker called "error" where you could gracefully let the users know that something is wrong and give them a general idea of the problem. Error checking in this case is quite simple because the only error a user might encounter is that the Castmember somehow does not have a State setting of 4 when you are trying to create the sphere. The createsphere() custom handler could be expanded to include error checking to ensure that when the movie is loaded, the name you intend to use for the sphere is unique. Here's an example:
1: Global scene 2: On createsphere sphereName, R, sphereColor 3: If check3Dready(scene) then 4: If scene.model(spherename) = void then 5: Res = Scene.newmodelresource(sphereName & "res", #sphere) 6: Res.radius = R 7: Obj = scene.newmodel(sphereName, res) 8: Shd = scene.newshader(sphereName & "shd", #standard) 9: Shd.diffuse = sphereColor 10: Shd.texture = void 11: Obj.shaderlist = shd 12: Return scene.model(spherename) 13: Else 14: Return 2 15: End if 16: Else 17: Return -1 18: End if 19: End
Now this custom handler begins to develop some character, and more importantly a system for error checking. In this example, -1 informs us that the Castmember was not ready, and -2 tells us that we are using a duplicate name. It would not be difficult to modify these values to return -2001 and -2002. We could then easily say that error -2002 was generated when we tried to create a sphere, because we used a duplicate name. Perhaps 1002 could be reserved for when we try to create a plane with a duplicate name.
Only the plane primitive rivals the versatility of the box primitive in the creation of basic architectural spaces. The box primitive will often be your first choice for walls, ceilings, and floors when describing interior spaces. Although a special case of the box primitive is the cube, rectangular boxes are viable building blocks. For this reason, our createbox() custom handler will be concerned with the Length, Width, and Height parameters of boxes. If we were more interested in only creating cubes, our custom handler would only need to accept one value for Length, Width, and Height. As it is, the createbox() custom handler is similar in form to createsphere() and createplane(), but it requires several arguments, as shown here:
1: Global scene 2: On createbox boxName, L,W,H, boxColor 3: If check3Dready(scene) then 4: Res = Scene.newmodelresource(boxName & "res", #box) 5: Res.length = L 6: Res.width = W 7: Res.height = H 8: Obj = scene.newmodel(boxName, res) 9: Shd = scene.newshader(boxName & "shd", #standard) 10: Shd.diffuse = boxColor 11: Shd.texture = void 12: Obj.shaderlist = shd 13: Return scene.model(boxname) 14: Else 15: Return false 16: End if 17: End
Lines 9 through 12 occupy themselves with the creation of a Shader dedicated to this Model. Note that the name of the Shader is the name of the box with the letters "shd" appended. Using a concise naming convention like this will aid you in controlling your worlds. Also, understand that the Shader object and Shader property are two separate entities.
This handler is quick and should suffice for the creation of basic box shapes. If we were interested in creating a maze-generation game, we might customize the box handler to build several boxes and position them correctly. We could then rename it the "createroom()" handler.
The cylinder primitive is intriguing because it can be reshaped into so many variations. Because of this, I have created two custom handlers: one to deal with common cylinder creation and another to deal with the creation of cones.
Basic Cylinder Handler
For a basic cylinder, the pertinent parameters we are interested in are its height and radius. Therefore, the custom handler looks like this:
1: Global scene 2: On createcylinder cylName, R,H, cylColor 3: If check3Dready(scene) then 4: Res = Scene.newmodelresource(cylName & "res", #cylinder) 5: Res.height = H 6: res.topradius = R 7: res.bottomradius = R 8: Obj = scene.newmodel(cylName, res) 9: Shd = scene.newshader(cylName & "shd", #standard) 10: Shd.diffuse = cylColor 11: Shd.texture = void 12: Obj.shaderlist = shd 13: Return scene.model(cylname) 14: Else 15: Return false 16: End if 17: End
Notice in this example that in order to set the radius of the cylinder, I must set the Topradius and Bottomradius. Keep in mind that radius is not a property of the cylinder primitive. I will elaborate on the basic cylinder-creation handler in Chapter 11, where we will specialize this function to deal with the creation of pie charts.
Basic Cone Handler
You can also use the cylinder primitive to create cones by modifying the Topradius and Bottomradius properties. If our goal is to create a simple cone, we only need to modify one of these properties. In addition, we still need to modify the height. The revised cylinder handler becomes the simple cone handler and looks like this:
1: Global scene 2: On createcone coneName, R,H, coneColor 3: If check3Dready(scene) then 4: Res = Scene.newmodelresource(coneName & "res", #cylinder) 5: Res.height = H 6: Res.bottomradius = R 7: Res.topradius = 0 8: Obj = scene.newmodel(coneName, res) 9: Shd = scene.newshader(coneName & "shd", #standard) 10: Shd.diffuse = coneColor 11: Shd.texture = void 12: Obj.shaderlist = shd 13: Return scene.model(conename) 14: Else 15: Return false 16: End if 17: End
Line 6 now sets Bottomradius as defined when you call the createcone() custom handler, and line 7 is "hard-wired" to set Topradius to 0. Although this will always create cones that point up, it is possible to rotate them into any position we want.
It would not be overly difficult to add a control for the resolution property to this handler in order to create a pyramid-creation custom handler. We could easily have a createpyramid() custom handler if we were to change all references of cone to pyramid and then insert the following line of code at line 7:
Res.resolution = 1
You can see that it is possible to develop an entire suite of frequently used primitive-creation handlers that are meant to ease your workload. Also, depending on the needs of your application, you may want to build error checking into your custom handlers. You will probably need more error checking if you are going to be building projects that generate Models on their own or via user manipulation. If all the Models you are going to create are known, you will most likely not need to have robust error checking for these operations. Aside from error checking, the strategy is clear: encapsulate and reuse. With this strategy, we can begin to focus our attention on how to work with the Models we have created.