SuperQuadric Ellipsoids and Toroids, OpenGL Lighting, and Timing

Published August 08, 2000 by Jonathan Metzgar, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
[size="5"]SuperQuadric Ellipsoids and Toroids

There has been some coverage of OpenGL's support for Quadrics via the GLU library. In this article, a superior model using Super Quadric shapes will be discussed. To start with, Super Quadrics are very similar to Quadrics except that we can control the shape with only two variables. Two, we can find if a given point is located inside, on the surface, or outside of the Super Quadric. Another useful operation that can be derived is the Inertia Tensor for rigid body equations. However, that will not be discussed in this tutorial. This code will use the code from NeHe's Lesson 1 as its basis.

The two Super Quadrics we will discuss will be the Ellipsoid and the Toroid. The Hyperboloid is another shape but I don't have enough information yet about it to include it here. An ellipsoid is based on a sphere and can be manipulated to generate a variety of shapes including pillows, rounded boxes, "stars," spheres, and others. Toroids are a torus shapes and can be manipulated in the same manor to pinch or squarify its shape.

As you may have already suspected, we will need a whole lot of math to generate such equations. There will be no derivations of equations, only the equations that produce these shapes. I couldn't find (or spend much time finding) the derivations for these equations at the time this tutorial was written. Anyway, I think I should explain some of the parameters first.

We only need to know about 5 variables for ellipsoids and 6 variables for toroids. The first three are a[sub]1[/sub], a[sub]2[/sub], and a[sub]3[/sub] and are used to define the radius for the X, Y and Z axes. The next two are the most important, n and e. These two are used to define the North-South roundness/squareness/pinched and East-West roundness/squareness/pinched shapes, respectively. Plugging a value of 1 for n and e will yield a sphere. Increasing these values will make the shape more pinched, while decreasing them makes the shape more square. Finally, alpha, which is specific to the toroid only, is used for the torus's inner radius.

Anyway, let's get going. First, we will define the mathematical functions for all these shapes.

[size="3"]SuperQuadric Ellipsoid:

Position Coordinates:


image1.gif

Normal Vector:

image2.gif

Inside-Outside Function:

image3.gif

[size="3"]SuperQuadric Toroid:

Position Coordinates:


image4.gif

Normal Vector:

image5.gif

Inside-Outside Function:

image6.gif

where

image7.gif

If you carefully examine the above equations, you'll notice that the trig functions are raised to a power. Since the domain of the functions include possibly negative cosine and sine results we need to define a proper behavior since raising a negative number to a power is undefined. The correct way to do such an operation is to multiply the sign of that value with the absolute value raised to the desired value. Defined below are functions which do exactly that.

image8.gif

Now that we have those defined, we can start coding! The first thing we have to do is write some utility functions based on the equations above.

/* Returns the sign of x */
float sgnf ( float x ) {
if ( x < 0 )
return -1;
if ( x > 0 )
return 1;
return 0;
}

/* Returns the absolute value of x */
float absf ( float x ) {
if ( x < 0 )
return -x;
return x;
}

/* sqC (v, n)
* This function implements the c(v,n) utility function
*
* c(v,n) = sgnf(cos(v)) * |cos(v)|^n
*/
float sqC ( float v, float n ) {
return sgnf((float)cos(v)) * (float)powf(absf((float)cos(v)),n);
}

/* sqCT (v, n, alpha)
* This function implements the CT(v,n,alpha) utility function
*
* CT(v,n,alpha) = alpha + c(v,n)
*/
float sqCT ( float v, float n, float alpha ) {
return alpha + sqC(v,n);
}

/* sqS (v, n)
* This function implements the s(v,n) utility function
*
* s(v,n) = sgnf(sin(v)) * |sin(v)|^n
*/
float sqS ( float v, float n ) {
return sgnf((float)sin(v)) * (float)powf(absf((float)sin(v)),n);
}
Now that those have been defined, we can implement the actual formula that will determine the surface coordinate and normal. To simplify matters, we will use pointers to the data we want to return.

/* sqEllipsoid(a1, a2, a3, u, v, n, e, *x, *y, *z, *nx, *ny, *nz)
*
* a1, a2, and a3 are the x, y, and z scaling factors, respecfully.
* For proper generation of the solid, u should be >= -PI / 2 and <= PI / 2.
* Similarly, v should be >= -PI and <= PI.
*/
void sqEllipsoid ( float a1, float a2, float a3, float u, float v, float n, float e,
float *x, float *y, float *z, float *nx, float *ny, float *nz ) {
*x = a1 * sqC (u, n) * sqC (v, e);
*y = a2 * sqC (u, n) * sqS (v, e);
*z = a3 * sqS (u, n);
*nx= sqC (u, 2 - n) * sqC (v, 2 - e) / a1;
*ny= sqC (u, 2 - n) * sqS (v, 2 - e) / a2;
*nz= sqS (u, 2 - n) / a3;
}

/* sqToroid(a1, a2, a3, u, v, n, e, alpha, *x, *y, *z, *nx, *ny, *nz)
*
* a1, a2, and a3 are the x, y, and z scaling factors, respecfully.
* For proper generation of the solid, u should be >= -PI and <= PI.
* Similarly, v should be >= -PI and <= PI.
* Also, alpha should be > 1.
*/
void sqToroid ( float a1, float a2, float a3, float u, float v, float n, float e, float alpha,
float *x, float *y, float *z, float *nx, float *ny, float *nz ) {
float A1, A2, A3;
A1 = 1 / (a1 + alpha);
A2 = 1 / (a2 + alpha);
A3 = 1 / (a3 + alpha);
*x = A1 * sqCT (u, e, alpha) * sqC (v, n);
*y = A2 * sqCT (u, e, alpha) * sqS (v, n);
*z = A3 * sqS (u, e);
*nx= sqC (u, 2 - e) * sqC (v, 2 - n) / A1;
*ny= sqC (u, 2 - e) * sqS (v, 2 - n) / A2;
*nz= sqS (u, 2 - e) / A3;
}
Finally, we can now write the generation functions that will actually draw those shapes using OpenGL. To further optimize this example, we'll implement the ability to generate a OpenGL display list for these objects. To send all our information about the superquadric to the function we'll also define a structure to hold more advanced information.

struct SuperQuadric {
float a1, a2, a3; /* Scaling factors for x, y, and z */
float alpha; /* For generating toroids. This is the inner radius */
float n, e; /* North-South/East-West Roundness/Squareness Factors */
float u1, u2; /* Initial and Final U values */
float v1, v2; /* Initial and Final V values */
int u_segs; /* Number of segments for U */
int v_segs; /* Number of segments for V */
float s1, t1; /* Initial s and t texture coordinates */
float s2, t2; /* Final S and T texture coordinates */
int texture_flag; /* Flag determining texture coordinate specification */
int gl_list_id; /* OpenGL Display List ID */
};

/* sqSolidEllipsoid ( sq, make_display_list, gen_texture_coordinates )
*
* Generates a solid ellipsoid using the parameters from sq and optionally
* generates texture coordinates and a display list using the ID from sq.
*/
void sqSolidEllipsoid ( SuperQuadric *sq, int make_display_list, int gen_texture_coordinates ) {
float U, dU, V, dV;
float S, dS, T, dT;
int X, Y; /* for looping */
float x, y, z;
float nx, ny, nz;

/* Calculate delta variables */
dU = (float)(sq->u2 - sq->u1) / (float)sq->u_segs;
dV = (float)(sq->v2 - sq->v1) / (float)sq->v_segs;
dS = (float)(sq->s2 - sq->s1) / (float)sq->u_segs;
dT = (float)(sq->t2 - sq->t1) / (float)sq->v_segs;

/* If we're going to make a display list then start it */
if ( make_display_list ) {
glNewList ( sq->gl_list_id, GL_COMPILE );
}

/* Initialize variables for loop */
U = sq->u1;
S = sq->s1;
glBegin ( GL_QUADS );
for ( Y = 0; Y < sq->u_segs; Y++ ) {
/* Initialize variables for loop */
V = sq->v1;
T = sq->t1;
for ( X = 0; X < sq->v_segs; X++ ) {
/* VERTEX #1 */
sqEllipsoid ( 1, 1, 1, U, V, sq->n, sq->e, &x, &y, &z, &nx, &ny, &nz );
glNormal3f ( nx, ny, nz );
glTexCoord2f ( S, T );
glVertex3f ( x, y, z );

/* VERTEX #2 */
sqEllipsoid ( 1, 1, 1, U + dU, V, sq->n, sq->e, &x, &y, &z, &nx, &ny, &nz );
glNormal3f ( nx, ny, nz );
glTexCoord2f ( S + dS, T );
glVertex3f ( x, y, z );

/* VERTEX #3 */
sqEllipsoid ( 1, 1, 1, U + dU, V + dV, sq->n, sq->e, &x, &y, &z, &nx, &ny, &nz );
glNormal3f ( nx, ny, nz );
glTexCoord2f ( S + dS, T + dT );
glVertex3f ( x, y, z );

/* VERTEX #4 */
sqEllipsoid ( 1, 1, 1, U, V + dV, sq->n, sq->e, &x, &y, &z, &nx, &ny, &nz );
glNormal3f ( nx, ny, nz );
glTexCoord2f ( S, T + dT );
glVertex3f ( x, y, z );

/* Update variables for next loop */
V += dV;
T += dT;
}
/* Update variables for next loop */
S += dS;
U += dU;
}
glEnd ( );

/* If we're making a display list then stop */
if ( make_display_list ) {
glEndList ( );
}
}

/* sqSolidToroid ( sq, make_display_list, gen_texture_coordinates )
*
* Generates a solid toroid using the parameters from sq and optionally
* generates texture coordinates and a display list using the ID from sq.
*/
void sqSolidToroid ( SuperQuadric *sq, int make_display_list, int gen_texture_coordinates ) {
float U, dU, V, dV;
float S, dS, T, dT;
int X, Y; /* for looping */
float x, y, z;
float nx, ny, nz;

/* Calculate delta variables */
dU = (float)(sq->u2 - sq->u1) / sq->u_segs;
dV = (float)(sq->v2 - sq->v1) / sq->v_segs;
dS = (float)(sq->s2 - sq->s1) / sq->u_segs;
dT = (float)(sq->t2 - sq->t1) / sq->v_segs;

/* If we're going to make a display list then start it */
if ( make_display_list ) {
glNewList ( sq->gl_list_id, GL_COMPILE );
}

/* Initialize variables for loop */
V = sq->v1;
S = sq->s1;
glBegin ( GL_QUADS );
for ( Y = 0; Y < sq->u_segs; Y++ ) {
/* Initialize variables for loop */
U = sq->u1;
T = sq->t1;
for ( X = 0; X < sq->v_segs; X++ ) {
/* VERTEX #1 */
sqToroid ( sq->a1, sq->a2, sq->a3, U, V, sq->n, sq->e, sq->alpha,
&x, &y, &z, &nx, &ny, &nz );
if ( gen_texture_coordinates )
glTexCoord2f ( S, T );
glNormal3f ( nx, ny, nz );
glVertex3f ( x, y, z );

/* VERTEX #2 */
sqToroid ( sq->a1, sq->a2, sq->a3, U + dU, V, sq->n, sq->e, sq->alpha,
&x, &y, &z, &nx, &ny, &nz );
if ( gen_texture_coordinates )
glTexCoord2f ( S + dS, T );
glNormal3f ( nx, ny, nz );
glVertex3f ( x, y, z );

/* VERTEX #3 */
sqToroid ( sq->a1, sq->a2, sq->a3, U + dU, V + dV, sq->n, sq->e, sq->alpha,
&x, &y, &z, &nx, &ny, &nz );
if ( gen_texture_coordinates )
glTexCoord2f ( S + dS, T + dT );
glNormal3f ( nx, ny, nz );
glVertex3f ( x, y, z );

/* VERTEX #4 */
sqToroid ( sq->a1, sq->a2, sq->a3, U, V + dV, sq->n, sq->e, sq->alpha,
&x, &y, &z, &nx, &ny, &nz );
if ( gen_texture_coordinates )
glTexCoord2f ( S, T + dT);
glNormal3f ( nx, ny, nz );
glVertex3f ( x, y, z );

/* Update variables for next loop */
U += dU;
T += dT;
}
/* Update variables for next loop */
S += dS;
V += dV;
}
glEnd ( );

/* If we're making a display list then stop */
if ( make_display_list ) {
glEndList ( );
}
}
That's pretty much all you need to generate them. Now for the other functions that can help with colision detection.

/* sqEllipsoidInsideOut ( sq, x, y, z )
*
* Tests to see if point P is inside the SuperQuadric sq.
* Returns 1 if on the surface, > 1 if outside the surface, or
* < 1 if inside the surface
*/
float sqEllipsoidInsideOut ( SuperQuadric *sq , float x, float y, float z ) {
float result;
result = powf ( powf ( x / sq->a1, 2 / sq->e ) +
powf ( y / sq->a2, 2 / sq->e ), sq->e / sq->n ) +
powf ( z / sq->a3, 2 / sq->n );
return result;
}

/* sqToroidInsideOut ( sq, x, y, z )
*
* Tests to see if point P is inside the SuperQuadric sq.
* Returns 1 if on the surface, > 1 if outside the surface, or
* < 1 if inside the surface
*/
float sqToroidInsideOut ( SuperQuadric *sq , float x, float y, float z ) {
float result;
result = powf ( powf ( powf ( x / sq->a1, 2 / sq->e ) + powf ( y / sq->a2, 2 / sq->e ),
sq->e / 2 ) - sq->alpha, 2 / sq->n ) + powf ( z / sq->a3, 2 / sq->n );
return result;
}
Finally, we can add some functions that generate common shapes. You can find the source code in [font="Courier New"][color="#000080"]squadric.cpp[/color][/font].

void sqSolidSphere ( float radius, int slices, int segments );
void sqSolidCylinder ( float radius, int slices, int segments );
void sqSolidStar ( float radius, int slices, int segments );
void sqSolidDoublePyramid ( float radius, int slices, int segments );
void sqSolidTorus ( float radius1, float radius2, int slices, int segments );
void sqSolidPineappleSlice ( float radius1, float radius2, int slices, int segments );
void sqSolidPillow ( float radius, int slices, int segments );
void sqSolidSquareTorus ( float radius1, float radius2, int slices, int segments );
void sqSolidPinchedTorus ( float radius1, float radius2, int slices, int segments );
void sqSolidRoundCube ( float radius, int slices, int segments );
That's all the functions we need.


[size="5"]Improved C++ Light Class

Next, let's develop a C++ class to encapsulate OpenGL's lighting features. In our implementation, we will encapsulate some of OpenGL's lighting functionality. For now we will only talk about the individual lights.

Before we get to any coding, I want to explain some of OpenGL's lighting algorithms so that you know how to use the functions. When I first learned about OpenGL's lighting functions, I had a hard time learning how to use them.

First, OpenGL requires that the light have either a position or direction vector. If you want the light to be located at a certain position, be sure that the w coordinate is set to one. If w is zero then the light will be located at infinity pointing in the direction you gave. Most people want the light located at infinity if they want a directional light. If you want a point light then specify 1 for w.

Next, OpenGL has a few color parameters that can be set to adjust how the light affects primitives. A primitive can be a triangle, quad, or other basic shape that is defined for the creation of objects. There are three colors that can be set. The first of those is the ambient color which is used to define the lowest possible color that will be visible even if there is no light. For example, if you have no lights on whatsoever, you'll notice it's pitch dark and you can't see anything. When there is ambient light, everything will be lit at constant intensity. That is what this is used for.

The next color is diffuse color which is used for determining what color will light the primitive. If you've ever had a colored light, like a flashlight with a tinted cover, say red, you'll notice that when you shine it one something it will look red. This is the same in OpenGL.

The final color is specular color. Specular color is the "shiny" color that highlights will have. Take metal, for example, when you shine light at it, it will have a very bright highlight. OpenGL supports this and can even specify a different color for specular highlights.

As for the rest of OpenGL's lighting features, I know little about how to use them. I am investigating them however. For now, knowing about colors and directional lights will get you going in the right direction.

Now, we'll implement the individual light class.

class CGL_Light3D {
public:
CGL_Light3D ( );
~CGL_Light3D ( );

void Init ( int light ); // Resets all light information and sets current
// light to a OpenGL light GL_LIGHT0, GL_LIGHT1, etc.
// specified by the user
void SetLight ( int light ); // Similar to Init (light) except that current
// light settings in the class are set to defaults.
void SetValues ( ); // Completely Sets Light Up. Must be called if any changes
// are made to the light.

void TurnOn ( ); // Turns on light
void TurnOff ( ); // Turns off light
void Toggle ( ); // Toggles light on or off
void GetOnOffState ( ); // Returns current on/off state

void SetAmbientColor ( float r, float g, float b, float a );
void GetAmbientColor ( float *r, float *g, float *b, float *a );
void SetAmbientColor ( float c[4] );
void GetAmbientColor ( float *c[4] );

void SetDiffuseColor ( float r, float g, float b, float a );
void GetDiffuseColor ( float *r, float *g, float *b, float *a );
void SetDiffuseColor ( float c[4] );
void GetDiffuseColor ( float *c[4] );

void SetPosition ( float x, float y, float z, float w );
void GetPosition ( float *x, float *y, float *z, float *w );
void SetPosition ( float p[4] );
void GetPosition ( float *p[4] );

void SetSpecular ( float r, float g, float b, float a );
void GetSpecular ( float *r, float *g, float *b, float *a );
void SetSpecular ( float s[4] );
void GetSpecular ( float *s[4] );

/* Spot Light Functions */
void SetSpotDirection ( float x, float y, float z );
void GetSpotDirection ( float *x, float *y, float *z );
void SetSpotDirection ( float s[3] );
void GetSpotDirection ( float *s[3] );

void SetSpotExponent ( float exponent );
float GetSpotExponent ( );

void SetSpotCutoff ( float cutoff );
float GetSpotCutoff ( );

/* Attenuation factors */
void SetConstantAtt ( float constant );
float GetConstantAtt ( );
void SetLinearAtt ( float linear );
float GetLinearAtt ( );
void SetQuadraticAtt ( float quadratic );
float GetQuadraticAtt ( );
private:
int LIGHT;

float ambient[4];
float diffuse[4];
float specular[4];
float position[4];
float spot_direction[3];
float spot_exponent;
float spot_cutoff;
float constant_attenuation;
float linear_attenuation;
float quadratic_attenuation;

int on_off_state;
};
Since most of the function only set values in the class, we'll leave those alone. However, we'll take a look at the [font="Courier New"][color="#000080"]Init()[/color][/font] and [font="Courier New"][color="#000080"]SetValues()[/color][/font] functions. [font="Courier New"][color="#000080"]Init()[/color][/font] simply sets the default values and is compliant with the OpenGL 1.2.1 specification. That means that the default light values are the same as OpenGL's. [font="Courier New"][color="#000080"]Init()[/color][/font] takes one parameter and that is the same as the corresponding OpenGL ID for the light. So to use the first OpenGL light, [font="Courier New"][color="#000080"]GL_LIGHT0[/color][/font], you pass [font="Courier New"][color="#000080"]GL_LIGHT0[/color][/font].

void CGL_Light3D::Init ( int light ) {
LIGHT = light;

// set all members to OpenGL defaults
// all values comply with the OpenGL 1.2.1 Specification
ambient[0] = 0.0f;
ambient[1] = 0.0f;
ambient[2] = 0.0f;
ambient[3] = 1.0f;

if ( light == GL_LIGHT0 ) {
diffuse[0] = 1.0f;
diffuse[1] = 1.0f;
diffuse[2] = 1.0f;
diffuse[3] = 1.0f;
} else {
diffuse[0] = 0.0f;
diffuse[1] = 0.0f;
diffuse[2] = 0.0f;
diffuse[3] = 1.0f;
}

if ( light == GL_LIGHT0 ) {
specular[0] = 1.0f;
specular[1] = 1.0f;
specular[2] = 1.0f;
specular[3] = 1.0f;
} else {
specular[0] = 0.0f;
specular[1] = 0.0f;
specular[2] = 0.0f;
specular[3] = 1.0f;
}

position[0] = 0.0f;
position[1] = 1.0f;
position[2] = 0.0f;
position[3] = 0.0f;

spot_direction[0] = 0.0f;
spot_direction[1] = 0.0f;
spot_direction[2] = -1.0f;

spot_exponent = 0.0f;
spot_cutoff = 180.0f;

constant_attenuation = 1.0f;
linear_attenuation = 0.0f;
quadratic_attenuation = 0.0f;
}
[font="Courier New"][color="#000080"]SetValues()[/color][/font] calls OpenGL and sets all the light parameters that the class is storing. This function must be called after changing any values in the class because it needs to notify OpenGL of the change.

void CGL_Light3D::SetValues ( ) {
glLightfv ( LIGHT, GL_AMBIENT, ambient );
glLightfv ( LIGHT, GL_DIFFUSE, diffuse );
glLightfv ( LIGHT, GL_SPECULAR, specular );
glLightfv ( LIGHT, GL_POSITION, position );
glLightfv ( LIGHT, GL_SPOT_DIRECTION, spot_direction );
glLightfv ( LIGHT, GL_SPOT_EXPONENT, &spot_exponent );
glLightfv ( LIGHT, GL_CONSTANT_ATTENUATION, &constant_attenuation );
glLightfv ( LIGHT, GL_LINEAR_ATTENUATION, &linear_attenuation );
glLightfv ( LIGHT, GL_QUADRATIC_ATTENUATION, &quadratic_attenuation );
}
As you can see, these are pretty straightforward functions. Included in the source is a 2D image library that I hacked so I could make some noise bitmaps for the SuperQuadrics.


[size="5"]Timing

Timing is a critical part of developing simulations. Many simulations suffer from this because they don't have support for controlling the speed of their program. Here is a sample implementation of a timing loop in Win32.

In this tutorial's program, we have defined a few variables, [font="Courier New"][color="#000080"]DTick[/color][/font], [font="Courier New"][color="#000080"]Tick1[/color][/font], and [font="Courier New"][color="#000080"]Tick2[/color][/font]. These are used to store the delta time, initial time, and final time, respectively. These are defined globally so they can be used by the entire program.

int Tick1, Tick2; // used to help keep speed constant
float DTick;
Next we have to "prime" the variables so we do this at the very beginning of our program in [font="Courier New"][color="#000080"]WinMain[/color][/font]. We use the [font="Courier New"][color="#000080"]GetTickCount()[/color][/font] function to return the number of milliseconds since the computer was started.

Tick1 = GetTickCount ( );
Tick2 = GetTickCount ( );
DTick = DTick = (float)((Tick2 - Tick1) / 30.0);
Now we go to our loop and do the same calculation except that we replace [font="Courier New"][color="#000080"]Tick1[/color][/font] with the value from [font="Courier New"][color="#000080"]Tick2[/color][/font] so we can calculate the difference in time since the last frame. We get the delta time by subtracting the two and finally divide by 30.0 to get a factor for updating our variables.

// update time
Tick1 = Tick2;
Tick2 = GetTickCount ( );
DTick = (float)((Tick2 - Tick1) / 30.0);
That's all you need and it should keep your simulation running smoother when the computer is faster. On slow computers, it will keep the frame up to date to ensure accuracy.

If you have any questions, please free feel to e-mail me at [email="microwerx@yahoo.com"]microwerx@yahoo.com[/email]. You can also visit my website at http://www.geocities.com/microwerx/ for more information, games, articles, and more!
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement