|
to the MojoWorld SDK
Ruth "Calyxa" Fry
Who this Document is For and By
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 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:
|
|
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:
|
|
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
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
Primitive plugins
Material plugins
Backdrop, Atmosphere and Cloud Layer plugins
|
|
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
Simple Plugins - A Set of Nodes for Doc Mojo
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 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
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 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:
|