A Novice's Guide
to the
MojoWorld SDK

Ruth "Calyxa" Fry
Copyright 2002
Pandromeda Inc.


 

Who this Document is For and By
This document is for novices, by a novice. Anyone who has interest in writing plugins for MojoWorld will hopefully find something useful in it. If you have a strong background in C++ and / or in graphics programming, feel free to skip to the sections on Plugin Types and Where to Access Them and Choosing Plugin IDs. After that, you'll want to just dive on in to the sample code and Reference document. Macintosh users will want to make note of the Macintosh OpenGL issue.

If, on the other hand, you are like I am, someone who has dabbled in programming and has little knowledge of 3D geometry, you'll want to work your way through this whole document. There will be places where some sections of the Reference document will be needed as background and in those instances there'll be links directly to those sections.

I'll cover some of my experiences writing some simple and not-so-simple plugins. I'll talk about some basics of 3D geometry along the way and my still very incomplete understanding of what's going on behind the scenes in MojoWorld. To absorb all of this will require some bouncing back and forth between these documents, the sample code and MojoWorld itself. There is a brief section on troubleshooting at the very end of this document which covers some of the more obvious "gotchas".

 

What's Included in the SDK
The SDK is a collection of headers, libraries, documetation and sample code. It is divided up into the following folders specific to each platform:

  • doc/
      contains this document and the Reference document.
  • include/
      contains the header files. While most of these will be the same across both Mac and Win plaforms, the Mac version of the SDK requires some extra files.
  • lib/
      contains the library files. These are definitely NOT cross-platform.
  • samples/
      contains a folder structure of sample source code and project files.

The samples folders contain platform-specific projects tailored to the recommended development environments: Microsoft Visual C++ Version 6 for the Windows platform and MetroWerks CodeWarrior Version 7.1 for the Macintosh platform. The actual sample code is the same for both. If you are using MS VC++, use the File->Open Workspace... menu item to open the sample projects. If you are using CodeWarrior, use the File->Open... menu item to open the sample projects.

The sample code is divided into the following folders:

  • Functions
      contains the FAltitude node and the FNoise node.
  • Materials
      contains the MAddMat and MDispMat materials.
  • Noises
      contains the NChecker and NSine basis function noises.
  • Primitives
      contains the PSphere and PTerrainPatch primitives.
  • Stub
      contains a project stub. It is strongly recommended that you make a copy of this project to use as a basis for any new projects you create.

 

Macintosh OpenGL

Specifically regarding the sample Primitives code - the CodeWarrior project file assumes that the OpenGL SDK 1.2 Core files have been copied into a folder named "OpenGL SDK 1.2 Core" in the "Metrowerks CodeWarrior 7.0" folder. If you already have the OpenGL SDK 1.2 Core installed on your machine in a different location, you will need to edit the CodeWarrior project file for the Primitives sample code as follows:

  • in the Libraries section of the project, remove the references to "OpenGLUtilityStub" and "OpenGLLibraryStub", then add new references for those files as they are situated on your machine.
  • in the Access Paths panel of the project Settings for each target, change the system paths for:
    • {Compiler}:OpenGL SDK 1.2 Core:Headers:
      and
    • {Compiler}:OpenGL SDK 1.2 Core:Libraries:
      to point to the location where those folders are situated on your machine.
 

Plugin Types and Where to Access Them
There are several different types of plugins which can be written for MojoWorld. How those plugins will be accessed in the MojoWorld user interface will depend on what type of plugin you're writing. All of the plugins created by the sample code are listed below as specific examples. Note that each plugin created by the sample code will have the word 'Sample' prefacing its name to distinguish the sample plugin from the default type of the same name.

Function plugins
Functions may show up in more than one place in the MojoWorld UI, depending on the type of function it is. All functions will show up in the Pro UIs "Create New Node" dialog. The Sample Altitude will be in the leaf nodes category and the Sample Noise will be in the fractals category.

Some functions may show up in the coordinates DDLB for texture leaves in the Generator UIs texture editors (they won't show up in the lists for the blends between nodes, unless it's a blend of type "blend", which is just another texture leaf). The Sample Altitude node does this. Note that an altitude cannot be used in a height texture, so the Sample Altitude will not be found in the list for any texture component that is used as part of the Mountain Height texture. See the comments in the FAltitude.cpp sample code for further details on this behavior.

Some functions may show up in the primary 'fractal' and/or distortion 'fractal' DDLBs of texture leaves. The Sample Noise node will be found here.

Noise plugins
Somewhat related to Functions are Noises. Noise plugins will only show up in the Basis Function menu in any fractal function that uses a Basis Function. The sample Noise plugins Sample Sine and Sample Checkerboard will be available to use as the basis for any fractal with a Basis Function parameter.

Primitive plugins
Primitives, such as the Sample Sphere and Sample Terrain Patch, will show up on the plug-shaped menu (where 'Load Mesh' and 'New River Terrain' items are). Plugins derived from the Light class will appear on this menu. This is also where non-rendering object plugins will be accessed. There aren't any code samples that create lights or non-rendering objects. A non-rendering object would be similar to a Marker Flag object - something that appears in the Real Time Renderer but does not appear in a final render.

Material plugins
The Sample Add Material and Sample Disp Material will be found as material types in the DDLBs underneath material leaves in the material editor.

Backdrop, Atmosphere and Cloud Layer plugins
These will show up in the appropriate DDLBs in the Sky Editor (Backdrop in the Sky Background DDLB, Atmosphere in the Atmosphere DDLB and Cloud Layer in the Cloud Layer DDLBs).

 

Choosing Plugin IDs
Each plugin needs to have a unique ID number. Pandromeda reserves the range 0x00000000 through 0x7FFFFFFF. If you are just starting out and are unsure if writing MojoWorld plugins is for you, the range 0x80000000 through 0x800000FF has been set aside just for experimentation purposes. Plugins with IDs in this range should not be released publically.

To request a block of IDs from Pandromeda for your plugins which will be released to the public, send email to support@pandromeda.com. You will be assigned a block of 256 IDs. If you should require another block, you will need to submit another request.

 

Some Personal Experiences
Before you read these, I recommend that you review the section in the Reference document that covers the Steps in Creating a Plugin. Don't worry if it doesn't all make sense now, I found that I had to write three or four different plugins before things started sinking in for me. Not all of those early plugins were successful for one reason or another; I'll talk about that, too.

Simple Plugins - A Set of Nodes for Doc Mojo
These are really the first plugins I wrote that worked and proved to be useful. Doc Mojo had some very specific requests for plugins he wanted. I'd written the less-than-successful Pinhead node, a couple other failed nodes and my lathe project was still a work in progress. That experience and the specific nature of Mo's requests gave me the confidence to write these up for him.

All of the nodes Mo wanted would simply return a piece of information about the planet at the given point. They all fit the same model as the FAltitude function sample node. One was Surface Normal, another was Ray Direction and the last was Sun Direction.

Geometry digression - What the heck is a "Surface Normal"? A surface normal is a way to specify the orientation of a plane using just a line. Imagine a piece of cardboard with a straw stuck through it straight up and down right through the middle. The cardboard can then be moved around in space by orienting the straw. The straw is the surface normal. The strict mathematical definition is: a vector perpendicular to a surface.
I also had some idea of what Mo wanted to do with these nodes, so I was able to forsee his need for a Dot Product node. (Note that it is possible with the default set of Pro UI nodes to use 'Vector Element' nodes to strip out each component of a vector, then use 'Multiply' nodes and 'Add' nodes to come up with the Dot Product of two vectors, but when you're talking about getting the Dot Product of two 3D vectors, you're talking about 11 different nodes in the Pro UI graph to make this calculation. It'd be so much nicer to do it all in one node, not to mention the fact that there's a DotProd() function already defined and ready to use.)

All of these plugins are set up exactly the same way. The only difference between them, aside from their IDs and name strings, is in the Eval() method which fills in a Vec3d value.

Surface Normal was the most obvious. The third data member described in the FunContext class is a Vec3d called "normal". The workhorse Eval method for the Surface Normal plugin looks like this:

    void CalySrfNrm::Eval(Vec3d &v, FunContext *fc)
    {
        v = fc->normal;
        v.Normalize();
    }
All I'm doing here is grabbing the normal data member of the function context. The call to Normalize() is quite possibly redundant. Normalizing a vector means to make it one unit long (set its magnitude to exactly 1.0). It still points in the same direction, but because it's exactly one unit long, that makes some other mathematical tricks with vectors easier.

Ray Direction needed to return the vector specifying the direction of the current ray - the line between the camera eye point and the point being rendered. Again, the FunContext class already contains this information in the viewDir data member. The workhorse Eval method for the Ray Direction plugin looks like this:

    void CalyRayDir::Eval(Vec3d &v, FunContext *fc)
    {
        v = fc->viewDir;
        v.Normalize();
    }
In this case, the call to Normalize is more important. I'm not sure I could explain why, though. It's just one of those things I've been taking for granted so far.
Geometry Digression - Here's a bit of discussion that helped clarify a lot of things for me on points, vectors and the workings of MojoWorld. I had read in the SDK reference document that slope is the dot product of the local "up" vector and the surface normal vector. That lead me to ask, "what is the difference between a 'world position' point and 'the local "up" vector?" The answer to that is, "local up is the vector you get by normalizing the world position (by definition as long as the planet is centred on the origin)."

Sun Direction needed to return the vector specifying the direction between the camera eye point and the source of the sun light. This was the trickiest, because while I could see how to get access to "all the lights in the scene", I couldn't figure out how to determine which one was the sun. Even if I could figure out which was the sun, I wasn't too clear on how to turn that information into the right 3D vector. A few words of explanation from Craig, though, and I was on my way.

The test I used to figure out which of the many lights in a scene is the sun light is based on the light's position. The position is a 4-dimensional vector where the first three components give either the light source position, or in the case of a infinite light source, the direction from which the light is coming. The fourth component will be 1.0 for a local light source and 0.0 for an infinite one. So, if the fourth component is 0.0 and the length of the vector is greater than "a very small number", then it's the sun light. This test will break if/when new plugin light types get added that are infinite and have direction vectors that are longer than EPSILON. (EPSILON is "a very small number".)

The actual Eval method for the Sun Direction node looks like this:

    void CalySunDir::Eval(Vec3d &v, FunContext *fc)
    {
        TList<Light *> *lgtlist;
        Light *lgt;
        real64 w, l;
        Vec4d pos;
        Vec3d sun = Vec3d(0.0, 0.0, 0.0);

        lgtlist = fc->lights;
        lgt = lgtlist->Head();
        while (lgt)
        {
            pos = lgt->GlLightPos();
            if (pos.w == 0 && pos.Length2() > EPSILON)
                sun = Vec3d(pos.x, pos.y, pos.z);
            lgt = lgtlist->Next();
        }
        v = -sun;
    }
One other thing to note in this is that instead of getting the actual length of the vector, I use the Length2() method. That returns the length squared, which is faster than calculating the actual length. To get the actual length of a vector requires taking a square root and in this case, those would be compute cycles wasted.

Dot Product takes two parameters that are vectors and returns their dot product. The dot product is a mathematical trick that can be done with vectors and is a lot easier when their lengths are one unit long. It calculates the cosine of the angle between the two vectors and that turns out to be useful for all sorts of things (such as determining the slope).

The Eval method for Dot Product is pretty simple:

    void CalyDotPro::Eval(real64 &v, FunContext *fc)
    {
        Vec3d v1, v2;

        v1 = params[0]->GetVec3d(fc);
        v2 = params[1]->GetVec3d(fc);

        v = DotProd(v1, v2);
    }
But that's not the end of it. MojoWorld is being built from the ground up to be able to eventually handle multiprocessing. In order to do that, functions need to be able to evaluate not just a single value but also an array of values. This is one job of the SIMDEval methods. Another benefit of the SIMDEval methods is that it allows the renderer to antialias more efficiently.

One of my first questions about all of this SDK stuff was, "what does SIMD mean?" It stands for "Single Instruction, Multiple Data" Any function that is just a simple leaf can get by without defining its own SIMDEval methods and will fall back on defaults. But for functions which take parameters where those parameters can be functions, those will benefit from having SIMDEval methods. The Dot Product's SIMDEval method is the first SIMDEval I've written, so I hope it's right! Here's what it looks like:
 

    void CalyDotPro::SIMDEval(real64 *v, FunContext *fc, int32 xs, int32 ys)
    {
        Vec3d *v1 = (Vec3d *)GlobalCore()->simdStack.Push(xs*ys*sizeof(Vec3d));
        params[0]->SIMDGetVec3d(v1, fc, xs, ys);

        Vec3d *v2 = (Vec3d *)GlobalCore()->simdStack.Push(xs*ys*sizeof(Vec3d));
        params[1]->SIMDGetVec3d(v2, fc, xs, ys);

        for (int32 i=0; i<xs*ys; i++)
        {
            if (!fc[i].worthMyWhile)
                v[i] = 0.0;
            else
                v[i] = DotProd(v1[i], v2[i]);
        }
        GlobalCore()->simdStack.Pop();
        GlobalCore()->simdStack.Pop();
    }
 
Be sure to take a look at the discussion of the SIMDEval methods in the reference document. That's near the end of the Steps In Creating A Plugin section. My Dot Product's SIMDEval method comes right out of that discussion. Another place to get more information on SIMD-type methods is in the sample code for the Sample Disp Material and Sample Add Material, in particular, check out the SIMDShade and SIMDDisplacement methods.

The First Plugin Attempt - Pinhead
Although this was the first plugin I tried writing, I put those other ones first because they're a little more basic, conceptually. They all pretty much just got information from the world and reported it. This next one actually makes changes in the world.

The Pinhead node was something Craig originally wrote for the Alpha version of MojoWorld. The theory behind it is that a height field gets cut up into square columns and all points within those squares get set to the same height. My grandiose idea was that I'd write a plugin to create eroded hexagonal basalt-type columns based on this Pinhead concept. Then I read through the Reference document and thought that I should simplify things to flat hexagonal columns. Before too long, I decided that I'd be lucky to figure out square ones.

The first thing I figured out was that Pinhead is a Function and not only that, but it'd be a function which would only be available in the Pro UI. I was doing a lot of this figuring before I had any sample code save for the FDistance example in the Reference document.

I had a pretty good idea of how to define and set up the parameters and could see where all the action was supposed to take place. The form that action would take was a total mystery, though. The inputs of the original Pinhead node were a world position, the height function to "pinhead-ise" and a value for the size of the pinheads. I could see how to access the data. Changing the world was another matter.

As it turns out, the world is changed by the Eval methods. I'd forgotten enough of C++ to realize that the first parameter of all these Eval methods was being passed as the address, which means the methods could change its actual value. So, here's the snippet of code that Craig described as a simplified version of his original Pinhead node:
 

   void FPinHead::Eval(real64 &v, FunContext *fc)
   {
       // Get the point...
       Vec3d pos = params[0]->GetVec3d(fc);
       real64 size = params[2]->GetDouble(fc);
   
       // Now munge the point to the centre of a pinhead
       pos.x = (floor(pos.x / size) + 0.5) * size;
       pos.y = (floor(pos.y / size) + 0.5) * size;
       pos.z = (floor(pos.z / size) + 0.5) * size;
   
       Vec3d oldp = fc->point;
       fc->point = pos;  // change the context to be the centre
                         // rather than the original point
   
       real64 hgt = params[1]->GetDouble(fc);
       // get the height at the centre from the original function
   
       fc->point = oldp;  // and set the context back again.
   
       v = hgt;  // and set the return value to the height
                 // at the centre (this will give flat tops)
   }
 
While that sample code worked for me, it also raised a few new questions which I was able to answer for myself by thinking carefully about it. One of those related to the bit where the point is munged to the center of a pinhead. "Why is this 'the center of a column' as opposed to being 'a point that is some function of 'size' away from the point I have right now? In other words, why is it not a moving target?"

The answer I came up with was, "because it is a point in 'the world' and not 'the point I have right now'."

I was also confused about the function context. What's going on here is that the context is told it is at a different point (the center of a pinhead column) and then the function to determine the height is evaluated. The context needs to be put back to the original point afterwards, or everything else from then on will get evaluated in the changed context. The context is created by the renderer.

Later on, when I tried to make a node that would return a slope of an area, not just the slope at a point, I tried to get a sampling of slopes over an area by changing the context around - but because I had no function to evaluate in that changed context, changing the context made no difference. In order to get more than one slope sample, a new point is created and then a call is made to the planet itself, not just to a function hanging off a parameter. Making calls out to the planet like that is very expensive in terms of computation time. Here's a little snippet of what that would look like:

      slope = fc->slope;
      newPoint = basePoint + northVector * radius;
      slope += GlobalCore()->PlanetSlope(newPoint);
I played around with variants on Pinhead for a while and eventually gave up on it. The steep edges give the renderer fits. The grid the planet gets diced up into isn't a series of square columns, either. It's a stack of cubes. That means you get squares at the poles and at some places along the equator, but in other parts of the world, the pinheads are oddly distorted - cut by arcing lines. I learned a lot from trying to recreate the Pinhead node, though, so it was well worth the time I spent on it, even if it isn't worth using.

My Third Plugin - The Lathe
The lathe project took me a long time. There are details of it that still don't work right. It's the first plugin I tried to write that is something other than a Function. The lathe is a primitive which uses a curve rotated around the z axis. One thing I didn't realize about primitives when I started is that you have to provide the code needed to tell the renderer how to render the object. I'm not going to include all the code for it, the sample code for the Sample Sphere and Sample Terrain Patch covers all of this. The bits I've included I hope will augment that sample code with my personal bias of what I found to be "easy" and what I found to be "hard".

The types of methods needed for the lathe object (which are overridden methods of the RenderObject class) include those which create the user interface (parameters), the preview drawing routines (one to draw the wireframe, which was easy, and another to draw the fully shaded preview, which was hard - there's also a textured preview, but I've elected not to use that one for the time being), the method which allows an object to be selected by clicking on it in the RTR window and lastly the routines needed for rendering.

Setting up the user interface for this was a snap. This is done by defining the parameters in the object's constructor. The parameters of my lathe are a radius, a length, the 'preview detail' (more on that in a bit) and a curve to draw the lathe profile. At some point, I should probably add options for end caps, but since it's possible to do those with the curve profile, I haven't bothered yet.

Here's a closer look at my lathe object's constructor:
 

   CalyLathe::CalyLathe() : RenderObject()
   {
       radius = 1.0;
       length = 1.0;
       previewSize = 10;
   
       // maintain an array of normals
       norms = NULL;
   
       // initialize the params
       numParams = 3;
       params = new ParamPtr[numParams];
       params[0] = new SParam<real64>(this, radius);
       params[0]->canFunction = FALSE;
       params[0]->name = MojoString("Lathe Radius");
       params[0]->live = TRUE;
       params[0]->SetRange(0.0, INFINITY);
   
       params[1] = new SParam<real64>(this, length);
       params[1]->canFunction = FALSE;
       params[1]->name = MojoString("Lathe Length");
       params[1]->live = TRUE;
       params[1]->SetRange(0.0, INFINITY);
   
       params[2] = new SParam<int32>(this, previewSize);
       params[2]->canFunction = FALSE;
       params[2]->name = MojoString("Preview Detail");
       params[2]->SetRange(2, 100);
   
       numCurves = 1;
       curves = new CurveHolderPtr[numCurves];
       curves[0] = new CurveHolder(1,"Lathe Profile");
   
       //name = ClassName();
       name = ObjectBaseName(ClassName());
   }
 
The first thing to note here are that none of the parameters can be driven by textures. The "live" member of params 0 and 1 relate to the behavior of drawing the object during its creation. Setting this "live" member to TRUE allows the RTR to draw the object changing size as the mouse is dragged during object creation. The other thing to note is the range on the previewSize parameter. It cannot be smaller than 2 nor larger than 100. What the heck is this parameter all about, anyway? Its primary purpose is for the preview rendering routines. It also plays a hand in the routines which handle the final render.

The previewSize parameter divides the lathe object up into sections radially and lengthwise. For the shaded preview, these will appear as facets. For the wireframe preview, they appear as the actual wireframe. The final render routines are based on dividing the object up into sections called subprimitives. Those subprimitives then split themselves up into smaller subprimitives until finally when the renderer sees that the subprimitives are smaller than a certain size, it tells each subprimitive to dice itself into a grid of micro-polygons that are shaded to create the final render. That was a long way off in my project, though.

The first successful drawing of my lathe object was a wireframe version. The wireframe preview turned out to be an easy bit to write. Both the wireframe preview and the shaded preview make use of OpenGL calls. There are many copies of OpenGL documentation on the web, find one you like and bookmark it.

This next code snippet is not the whole WireDraw() method, it's only the parts that make the GL calls to create the lines which make up the wireframe. This code can be directly compared to the WireDraw() method in the Sample Terrain Patch code.
 

      int i, j;
      real64 ang;
   
      // loops circling z axis
      for (i=0; i<previewSize; i++) // traverse the z axis
      {
         real64 ti = (real64)i/(real64)previewSize;
         real64 rr = curves[0]->EvalGrey(ti);
         glBegin(GL_LINE_LOOP);
         for (j=0; j<previewSize; j++) // rotate around z axis
         {
            ang = ((2 * PI) / (real64)previewSize) * j;
            glVertex3d(radius*cos(ang)*rr, radius*sin(ang)*rr, ti*length);
         }
         glEnd();
      }
   
      // lines paralleling the z axis
      for (j=0; j<previewSize; j++) // rotate around z axis
      {
         ang = ((2 * PI) / (real64)previewSize) * j;
         glBegin(GL_LINE_STRIP);
         for (i=0; i<previewSize; i++) // traverse z axis
         {
            real64 ti = (real64)i/(real64)previewSize;
            real64 rr = curves[0]->EvalGrey(ti);
            glVertex3d(radius*cos(ang)*rr, radius*sin(ang)*rr, ti*length);
         }
         glEnd();
      }
 
I'm probably doing more casting than I need to be, but I figure better safe than sorry. The key points here are the GL calls. Within each loop, there's a glBegin() call, then a sub-loop, then a glEnd() call. Inside the sub-loop is where all the vertex information is created. In both of the sub-loops, the vertices are the same. The difference between the loops is that the first creates the circular lines of the preview and the second creates the lines running along its length.

At this point, I built my lathe.dll and dropped it into place. I selected my lathe object from the plugin menu and dragged in the RTR to create it. It drew a sphere. I suppose I shouldn't have been surprised, as the code for the shaded preview was still the sphere code. When I switched the MojoWorld UI preview from Textured to Wireframe, then I saw a conical wireframe. More exciting yet was when I edited the curve for my lathe object and the wireframe preview of the lathe responded! Then I tried twiddling my previewSize parameter and re-learned a bunch I'd forgotten about arrays and pointers and what happens when you run out of bounds. Back to the drawing board.

Array bounds got figured out and it was time to move on to the shaded preview. I found this to be one of the harder parts, but that's due to my lack of 3D geometry more than anything else. Again, GL calls are used to define the object. This routine also relies on a PreparePreview() method to be called ahead of time which sets up the array of surface normals (vectors perpendicular to the lathe object's surface at each vertex). One trick involved in getting the surface normals for the lathe is getting the first derivative of the curve defining the lathe profile. Fortunately, there's already a routine built into the Curve class to get this value. Here's the whole PreparePreview() method:
 

   void CalyLathe::PreparePreview(Camera *cam, FunContext *fc) 
   {        
   
      Curve* c = curves[0]->GetCurve(1);
      
      // set up the array of normals
      if (norms)
         delete[] norms;
      
      norms = new fVec3d[previewSize*previewSize];
      real64 ang = (2 * PI) / (real64)previewSize;
      real64 anginc = ang;
      int i, j;   
    
      for (j=0; j<previewSize; j++) // rotate around the z axis
      {
         ang = anginc * j;
         for (i=0; i<previewSize; i++) // traverse the z axis
         {
            Vec3d p1;
   
            real64 x0 = c->UnitEval((real64)i/(real64)previewSize, true) * radius;
            real64 z0 = length;
   
            real64 x = cos(ang) * z0;
            real64 y = sin(ang) * z0;
            real64 z = -x0;
   
            p1 = Vec3d(x, y, z);
            p1.Normalize();
   
            norms[i*previewSize+j] = p1;
         }
      }
   }
 
The sole purpose of that method is to set up the array of surface normals. They are only used by the ShadeDraw() method. When the final renderer routines need surface normals, they need to calculate them in a much finer resolution than this method does.

The first half of the ShadeDraw() method gathers information about the material applied to the object in order to know what color to use when drawing the preview. That was code I lifted directly from either the Sample Sphere or Sample Terrain Patch, so I won't bother showing it. Here's the second half of the ShadeDraw() method where the GL calls to create the actual shape are made:
 

      glPushMatrix();
      glEnable(GL_NORMALIZE);
      
      SetOffsetCameraMatrix();
      
      glShadeModel(GL_FLAT);
      glDisable(GL_CULL_FACE);
   
      real64 anginc = ((2 * PI) / previewSize);
      real64 ang;
      int i, j;
      for (j=0; j<previewSize; j++) // rotate around z axis
      {
         ang = anginc * j;
         glBegin(GL_QUAD_STRIP);
         for (i=0; i<previewSize; i++) // traverse along z axis
         {
            real64 ti = (real64)i/(real64)previewSize;
            real64 rr = curves[0]->EvalGrey(ti);
            real64 tl = length * ti;
            glNormal3fv(&norms[i*previewSize+j].x);
            glVertex3d(radius*cos(ang)*rr, radius*sin(ang)*rr, tl);
            glVertex3d(radius*cos(ang+anginc)*rr, radius*sin(ang+anginc)*rr, tl);
         }
         glEnd();
      } 
      glShadeModel(GL_SMOOTH);
      glEnable(GL_CULL_FACE);
         
      glPopMatrix(); 
      glDisable(GL_NORMALIZE);
 
The important parts of this sample are the calls to glEnable(GL_NORMALIZE) and glNormal3fv with the surface normal for that vertex as its parameter. My early versions of the shaded preview that were not totally wrong were shaded like a cylinder - the surface normals did not take the curve profile into account. This resulted in strips of even shading rather than facets. Another early version which was close but not quite right had the calls with the GL_NORMALIZE argument commented out (thinking that I'd already normalized things). The result of that was a preview that looked great until it was resized. During a resize operation, the specular highlights on the shaded preview either brightened as the object was made smaller, or darkened as the object was made larger. The versions I had that were just plain wrong had adjacent strips shaded exactly the same, or strips that were all black, that is, not shaded at all.

Before I get to the routines that handle the actual rendering, I'm taking a brief detour through the HitTest() method. This is how the object can be selected in the RTR with a mouse click. I've actually punted on this and my current version of the lathe object uses a modified copy of the sphere's HitTest. At some point I should see about approximating with conic sections, but for now the sphere test works pretty well.

The HitTest() method needs to figure out if a ray from the camera to where the mouse was clicked in the scene intersects with the object. For shapes like spheres and cylinders and cones, this is fairly straight-forward math. One takes the equation for the sphere and substitutes in for x, y and z the equations for the ray in terms of x, y and z. After things get all multiplied out and simplified, you're left with the quadratic equation which will have 0, 1 or 2 solutions. Where it's 0, the ray misses the sphere. Where it's 1, the ray just grazes a tangent point of the sphere. Where it's 2, the ray intersects the sphere. See the Sample Sphere code for more detail.

For my lathe object, however, a true HitTest() multiplies out and "simplifies" to a sixth-order polynomial, which can't be solved analytically. So my current cheap kludge is to use the sphere test with the z coordinate of the lathe's origin moved to the lathe's length parameter divided by two. It was suggested that I could not have a HitTest() at all and just tell people that to select a lathe object in the RTR they'd have to use the Object List. I figured the sphere test kludge was a better solution. A conic section approximation will be even better when I get that far.

OK, now for the fun part, subprimitives and final rendering. This was another hard part for me, but I did eventually figure most of it out without too much outside help. There are still problems I'm having with it, though. I'm pretty sure those relate to the PrimBounds() method and that I'm still a bit confused about how to handle that all when some of the data is in real world data (the length) and some of the data is abstracted (the angles). The points bounding a subprimitive are defined by two points along the lathe length at two different angles. It seems logical to me to try to take the curve into account when turning those into actual world points, but if I include the curve in the calculation, things go horribly wrong. If I just multiply by the radius, things are mostly right, but not completely right.

Note that if you get the subprimitive stuff wrong, renders will take a very long time, if they finish at all, and will probably consume all your machine's memory and crash first. I had that happen a lot!

There's a separate SubPrimitive class. The lathe object has a couple methods related to subprimitives. One reports back how many subprimitives the object divides up into initially and the other returns the ith subprimitive object when passed the integer i. Since the lathe object already has a natural division occuring with the previewSize parameter, the GetSubPrim() method simply returns a subprimitive which corresponds to one of the facets on the shaded preview. Here's the full GetSubPrim() method:
 

   SubPrimitive *CalyLathe::GetSubPrim(int32 i, FunContext *fc)
   {
      int t, l;
      t = i%previewSize;
      l = i/previewSize;

      real64 td = (2*PI)/(real64)previewSize;
      real64 theta = (real64)t*td;
      real64 ld = length/(real64)previewSize;
      real64 len = (real64)l*ld;

      CalyLatheSubPrimitive *psp =
         new CalyLatheSubPrimitive(this, theta, theta+td, len, len+ld);
      return psp;
   }
 
The thing that has me confused and will keep me from including much more sample code here is that the information defining the subprimitive doesn't correspond to real world points. The start and end lengths of the subprimitive are only slightly less abstract than the angles defining its span. In order for the renderer to have some idea about where these subprimitives are, the function PrimBounds() turns out to be critical. When I had PrimBounds() wrong, the entire left half of the lathe wouldn't render until the redoing blocks pass and even then, several holes were left behind. When I had it very wrong, I ran into another case of where the pre-processing scene phase of the render took a long time and consumed all my memory and usually would crash before any blocks were drawn. Getting the PrimBounds() close to right still leaves some holes behind, but most of those are being filled in by the redoing block pass. The pre-processing portion of the render goes quickly when this method is doing the right thing. I'm not sure exactly what's wrong with my PrimBounds() method. When I ignore the curve and set up my real points as if it was a straight cylinder, I get the best results. If I try to take my curve into account, I run into the "takes forever, eats all the memory and crashes" problem.

Subprimitives need to know how to split themselves into smaller subprimitives. That's handled with the Split() method. This one is pretty easy because each subprimitive is essentially a quadrilateral and all Split() does is to cut the subprimitive in half both ways into four new subprimtives, each of which is a quadrant of the original.

At some point, the renderer decides that subprimitives are small enough and instead of calling Split() on them, it calls their Dice() method. Dice() is where the rendering of the object really happens. The subprimitive is cut up into a grid of micropolygons and each face is shaded. I was pretty happy to find out that the only part of Dice() I needed to handle was the bit where it makes the grid of micropolygons. I was able to leave all the shading stuff (which also handles displacement) as it was from the Sample Sphere code. Here's the portion of Dice() that I changed:
 

      Curve* c = clp->curves[0]->GetCurve(1);
      real64 ls = 1.0 / clp->length;

      // Find the actual points and some normals
      // for the shading and displacement
      for (y=0; y<=ys; y++)
      {
         real64 dely = lenLo + (real64)y / (real64)ys*(lenHi-lenLo);
         for (x = 0; x<=xs; x++)
         {
            int32 dex = y*(xs+1)+x;   // index for points
            real64 ar = clp->radius * clp->curves[0]->EvalGrey(dely*ls);
            real64 ang = thetaLo + (real64)x / (real64)xs*(thetaHi-thetaLo);
   
            lfc[dex] = *fc;         // copy out the base context info
            lfc[dex].UV = Vec2d(ang/(2.0*PI), dely*ls);
   
            // points
            lfc[dex].objectPoint.x = cos(ang)*ar;
            lfc[dex].objectPoint.y = sin(ang)*ar;
            lfc[dex].objectPoint.z = dely;
            lfc[dex].point = lfc[dex].objectPoint * clp->toWorld;
            lfc[dex].preDisplacePoint = lfc[dex].point;
   
            // normals
            real64 x0 = c->UnitEval(dely*ls, true) * ar;
            real64 z0 = clp->length;
            real64 tvx = cos(ang) * z0;
            real64 tvy = sin(ang) * z0;
            real64 tvz = -x0;
            lfc[dex].normal = Vec3d(tvx, tvy, tvz);
            lfc[dex].normal.Normalize();
   
            grid->normals[dex] = lfc[dex].normal;
            grid->points[dex] = lfc[dex].point;
            Vec2d sss;
            flag valid = fc->cam->WorldToScreen(grid->points[dex], sss);
            lfc[dex].screenPos =
               sss * Vec2d((real64)fc->cam->ixs, (real64)fc->cam->iys);
         }
      }
 
Well, that's it for my introduction to the SDK. I hope that it helps you make sense of it all!

 

Some Common Problems
Here are a couple of problems I ran into when I first started attempting to write MojoWorld plugins:
  • I can't find my plugin anywhere in the UI!

    Be sure you're looking in the right place. One of the times this happened to me, I was expecting to see my plugin show up as a blend type in the texture and material editors. There is no plugin type that will show up in this level of the UI. See the section above on plugin types for a comprehensive list of where to look in the UI for your plugins.

    If you're sure you're looking in the right place but the plugin still isn't there, check the NumPlugs() function and make sure it's returning the right value. Also, if your file contains a lot of plugins, make sure that there's an appropriate case in the Plugins() function.

    Even more basic than that - make sure your .dll file has been placed into the correct plugins folder! I've put my .dll file into a plugins folder only to launch a MojoWorld binary that's off in some entirely different folder. Needless to say, it didn't see my plugins.

  • MojoWorld crashes when it tries to load my plugin!

    When this happened to me, it was because I had added a new parameter without updating the numParams index of the params[] array.

    This will also happen if RTTI is not enabled and / or the Multithreaded DLL option is not specified when the dll file is built.