NDS WiFi Programming with devkitARM — Part 2

Posted in C++, OpenGL, rendering on June 27th, 2009 by Chaos Engineer

So the idea I had to display the potential for socket programming on the NDS was to simply pipe the input data from the NDS over the network to a remote host. To make this appealing from beyond just a technical standpoint, I figured the remote host should be running some sort of visualization that the user could interact with remotely by using the NDS touchscreen. Making an OpenGL application with a 3D cube the user could spin using the touchscreen would work nicely. Lets figure out how to do that from an OpenGL perspective.

Candidate for CrossproductWe want to rotate the cube along an axis perpendicular to the mouse motion vector. This will give the impression that the cube is spinning as a direct result of the motion, as if you had quickly ran your hand along it. So we first need to construct a vector that is perpendicular to the motion, and lies in the plane of the screen. Lets assume we have a default OpenGL view setup, where we are looking along the negative z-axis, and the screen lies in the xy-plane. Thus, we want a vector that is perpendicular to the mouse motion and lies in the xy-plane.

Anyone with some math background may immediately recognize the cross-product as a candidate to construct the perpendicular vector. Indeed, if we create a vector from the relative mouse motion (conveniently in our environment the mouse motion is on the xy-plane, so the screen coordinates align nicely), then take the cross-product of this vector with the z-axis, the result is a vector that is perpendicular to the mouse motion and lies in the xy-plane.

 
 
//iRelX and iRelY is the relative mouse motion
 
float fTheta;
float vCross[3];
float fCrossMag;
 
// Assign theta to the magnitude of the mouse motion vector
// This is rather arbitrary
fTheta = (iRelX*iRelX + iRelY*iRelY)/16.0;
 
/*
vCrossX = v1y*v2z - v1z*v2y
vCrossY = v1z*v2x - v1x*v2z
vCrossZ = v1x*v2y - v1y*v2x
*/
 
// Perform cross-product.
// Here v1 is the mouse motion vector and v2 is the z-axis.
// This gives us a vector that is perpendicular to
//the mouse motion and lies in the xy-plane.
vCross[0] = iRelY*1.0 - 0.0*0.0;
vCross[1] = 0.0*0.0 - -iRelX*1.0;
vCross[2] = -iRelX*0.0 - iRelY*0.0;
 
// Normalize this vector for use as axis in rotation matrix
fCrossMag = sqrt(vCross[0]*vCross[0] + vCross[1]*vCross[1] + vCross[2]*vCross[2]);
vCross[0] = vCross[0] / fCrossMag;
vCross[1] = vCross[1] / fCrossMag;
vCross[2] = vCross[2] / fCrossMag;
 

Great. So now we can just take this vector and use it when calling glRotatef(...) in order to transform the modelview matrix, right? Well, not quite. We CAN do this, but if you've played around with glRotatef(...) to manipulate objects in your scene via the modelview matrix, you may have noticed that when you rotate an object on one axis, it actually transforms the other two axes. This basically means that using glRotatef(...) or any other matrix transform in this manner is actually operating in the model's local coordinate system. If we tried to rotate our cube around our constructed axis using glRotatef(...), it would work as expected initially but then subsequent rotations would rotate around the already transformed axis. We don't want this, we want to operate on a fixed coordinate system, what is often referred to as the world coordinate system.

This issue arises because matrix operations post-multiply on the matrix stack in OpenGL. If we could pre-multiply the operations, we would be good, but unfortunately there is no way to tell OpenGL to pre-multiply instead, so we really have to do this operation manually. In order to accomplish this, we need to maintain our own modelview matrix accumulation, and each frame multiply our rotation matrix by this accumulation matrix to get the new modelview transform. Note that if we used glRotatef(...), it would multiply the accumulation matrix by the rotation matrix. Matrix multiplication isn't commutative, and in our situation it makes all the difference in the world.

So we need to get our hands dirty with a little linear algebra. First we need to define a few 4x4 matrices that we can hand to OpenGL:

 
// OpenGL likes this format for a 4x4 matrix
//as declared in the Redbook
float matWorld[16];
float matWorldAccum[16];
float matBuff[16];
 

Also define a function that can construct a rotation matrix when rotating around an arbitrary axis:

 
// Theta is in radians
void MatrixRotation(float *matRot, float fTheta, float fAxisX, float fAxisY, float fAxisZ)
{
	float fCosTheta, fSinTheta;
 
	// m0   m4   m8   m12
	// m1   m5   m9   m13
	// m2   m6   m10  m14
	// m3   m7   m11  m15
	fCosTheta = cos(fTheta);
	fSinTheta = sin(fTheta);
	matRot[0] = fAxisX*fAxisX+(1.0-fAxisX*fAxisX)*fCosTheta;
	matRot[4] = fAxisX*fAxisY*(1.0-fCosTheta)-fAxisZ*fSinTheta;
	matRot[8] = fAxisX*fAxisZ*(1.0-fCosTheta)+fAxisY*fSinTheta;
	matRot[12] = 0.0; // m12,m13,m14 can be used for translation
	matRot[1] = fAxisX*fAxisY*(1.0-fCosTheta)+fAxisZ*fSinTheta;
	matRot[5] = fAxisY*fAxisY+(1.0-fAxisY*fAxisY)*fCosTheta;
	matRot[9] = fAxisY*fAxisZ*(1.0-fCosTheta)-fAxisX*fSinTheta;
	matRot[13] = 0.0;
	matRot[2] = fAxisX*fAxisZ*(1.0-fCosTheta)-fAxisY*fSinTheta;
	matRot[6] = fAxisY*fAxisZ*(1.0-fCosTheta)+fAxisX*fSinTheta;
	matRot[10] = fAxisZ*fAxisZ+(1.0-fAxisZ*fAxisZ)*fCosTheta;
	matRot[14] = 0.0;
	matRot[3] = 0.0;
	matRot[7] = 0.0;
	matRot[11] = 0.0;
	matRot[15] = 1.0;
 
}
 

Now we need to be able to multiply our rotation matrix by the accumulated modelview transformation matrix:

 
void MatrixMultiply(float *matDest, const float *matA, const float *matB)
{
	// m0   m4   m8   m12
	// m1   m5   m9   m13
	// m2   m6   m10  m14
	// m3   m7   m11  m15
 
	// Inner product of each row with each column
	matDest[0]=matA[0]*matB[0] + matA[4]*matB[1] + matA[8]*matB[2] + matA[12]*matB[3];
	matDest[4]=matA[0]*matB[4] + matA[4]*matB[5] + matA[8]*matB[6] + matA[12]*matB[7];
	matDest[8]=matA[0]*matB[8] + matA[4]*matB[9] + matA[8]*matB[10] + matA[12]*matB[11];
	matDest[12]=matA[0]*matB[12] + matA[4]*matB[13] + matA[8]*matB[14] + matA[12]*matB[15];
 
	matDest[1]=matA[1]*matB[0] + matA[5]*matB[1] + matA[9]*matB[2] + matA[13]*matB[3];
	matDest[5]=matA[1]*matB[4] + matA[5]*matB[5] + matA[9]*matB[6] + matA[13]*matB[7];
	matDest[9]=matA[1]*matB[8] + matA[5]*matB[9] + matA[9]*matB[10] + matA[13]*matB[11];
	matDest[13]=matA[1]*matB[12] + matA[5]*matB[13] + matA[9]*matB[14] + matA[13]*matB[15];
 
	matDest[2]=matA[2]*matB[0] + matA[6]*matB[1] + matA[10]*matB[2] + matA[14]*matB[3];
	matDest[6]=matA[2]*matB[4] + matA[6]*matB[5] + matA[10]*matB[6] + matA[14]*matB[7];
	matDest[10]=matA[2]*matB[8] + matA[6]*matB[9] + matA[10]*matB[10] + matA[14]*matB[11];
	matDest[14]=matA[2]*matB[12] + matA[6]*matB[13] + matA[10]*matB[14] + matA[14]*matB[15];
 
	matDest[3]=matA[3]*matB[0] + matA[7]*matB[1] + matA[11]*matB[2] + matA[15]*matB[3];
	matDest[7]=matA[3]*matB[4] + matA[7]*matB[5] + matA[11]*matB[6] + matA[15]*matB[7];
	matDest[11]=matA[3]*matB[8] + matA[7]*matB[9] + matA[11]*matB[10] + matA[15]*matB[11];
	matDest[15]=matA[3]*matB[12] + matA[7]*matB[13] + matA[11]*matB[14] + matA[15]*matB[15];
 
}
 

Great, so we are now equipped to construct and maintain our own modelview matrix! This means we no longer have to ask OpenGL for transformations. We can do them ourselves, and just hand the transformation matrix to OpenGL for rendering the current scene.

 
// Now we contruct a matrix to rotate around this axis
MatrixRotation(matWorld, fTheta*PI/180.0, vCross[0], vCross[1], vCross[2]);
 
// Notice this is matWorld * matWorldAccum, instead of
//matWorldAccum * matWorld, as it would be if we just used
//glMultMatrix with the constructed matWorld rotation matrix
MatrixMultiply(matBuff, matWorld, matWorldAccum);
 
// Assign the world matrix accumulation to the modelview matrix
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(matBuff);
 
// Update accumulated matrix
for(i=0; i<16; i++)
	matWorldAccum[i] = matBuff[i];
 

Fun stuff, eh? I always feel empowered by doing more things in my client code instead of processing data in a black box. Next part of the article we will get into the NDS and socket programming. I know you are holding your breath. =)

Tags: , , , ,

Newline neutrality with ifstream

Posted in C++, rendering on June 25th, 2009 by Chaos Engineer

I recently got distracted and started porting my old game engine to use a new platform independent rendering interface I devised. I'm pretty psyched about the rendering system, it fully encapsulates a graphics sdk and all it's associated resources (vertex and index buffers, textures, etc). Deriving from the pure virtual renderer and resource base classes, I can write a new renderer class that targets a specific platform/SDK, plug it in, and just have it work without modifying a single line of code.

Part of this old game engine was a resource management class that could load 3D Studio MAX ASCII exported scenes/objects. These files are .ASE files, which is a versatile and simple format that has a surprisingly wide usage by various game engines. ASE files can be used when building Doom3 and Quake3 / Quake4 levels, since the format boasts native support in ID and Loki software's GtkRadiant design tool, so it is a generally accepted format for storing game content. The ASE format specification itself is quite versatile, and can contain material specifications, verticies, faces (vertex indexes), vertex colors, texture coordinates, and even animations with full interpolation parameters just to name a few. Being ASCII, it is rather easy to parse and modify.

In the ASE format, each line of the file serves a single purpose. It is either an opening or closing tag for a field, or an element in a field. It made sense then when I first wrote the import code to parse the file on a line-by-line basis using ifstream::getline(...). This worked great in Windows, and I had no issues with my implementation. Flash forward to now when I am rewriting this code to work in Linux, but using files created in Windows. I never thought the disparities of a 'newline' between platforms would ever cause such a problem.

The ifstream::getline function was specifically designed to get a single line from a file without including the newline or delimiting character(s). The newline character ('\n') is the delimiting character by default, as it serves its purpose most of the time. ifstream::getline(...) reads in all characters up to the delimiter, then extracts and discards the delimiter. The problem arises when considering the definition of a newline on different platforms.

In Windows, the newline for iostream operations is a carriage-return,line-feed combo. It is actually two characters with ASCII decimal values 13, 10 (0x0D, 0xOA in hex), often referred to as a CRLF combo. In Linux, a newline is simply a line-feed (dec 10, hex 0x0A). So when performing ifstream::getline(...) in Linux, what you are doing is reading a line up to the delimiter--newline by default--and if your file was created in Windows only the last half of the CRLF combo is discarded, while the first half is read in as the last character in your string variable. This makes any literal comparisons made against this string variable fail in Linux while working fine in Windows.

I thought for sure there was some elegant way to overcome this and attain platform independent file parsing without forcing a complete rewrite of the import code. This turned out to not be the case. The ifstream::getline function is handy, but by doing you the favor of automatically extracting and discarding the delimiter, it somewhat cripples it's versatility. My impetus was two fold. Obviously first I wanted to be able to read these ASE files with 0x0D,0x0A line delimiters into Linux without having the 0x0D on the end of my read lines. This would be simple enough, but I wanted also code that would work regardless of platform combinations. This meant reading Windows files in Linux, Linux files in Windows, Windows files in Windows and Linux files in Linux.

So this added a bit of complexity to the requirements. First off, it would be impossible to use ifstream::getline(...) because it would extract and discard different characters from the file stream depending on platform. When reading a Linux file in Windows, it would keep reading from the stream, looking for a CRLF combo, and would likely read the entire file without finding one. When reading a Windows file in Linux as I found out, it would read the line including 0x0D, then stop at and discard the 0x0A. If it didn't consider reading Linux files in Windows, the following implementation was elegant, and worked in all other situations:

 
char szLineBuff[256];
ifstream fsIn("test.ase", ios::in);
 
// Note that failbit would be set if this
//failed to extract any chracters before
//the delimiter, and the loop would end.
while(fsIn.get(szLineBuff, 128, '\n').good())
{
	// This extracts 2 characters in
	//windows, just 1 in Linux.
	fsIn.ignore(2,'\x0A');
	// Cleans Windows line in Linux
	if(szLineBuff[strlen(szLineBuff)-1] == '\x0D')
		szLineBuff[strlen(szLineBuff)-1] = '\0';
 
	// Process szLineBuff...
 
}
 

Now I recognize that opening the file in binary mode (ios::binary) would make the '\n' be interpreted as simply '\x0A' on both Windows and Linux. My problem with this is that the file isn't binary. I'm stubborn and think this ASCII text file should be read in text mode, and that there should be a simple way to interpret newlines from any platform on any other platform. There must be a simple and elegant solution out there somewhere...

I'll be honest and say I stopped there, as my code worked for 3/4 of the scenarios I laid out. I have yet to find a Linux native source of ASE files, so the last scenario (Linux files on Windows) hasn't become important for me yet. If anyone has a better solution for this, or knows one that would work in all scenarios while still being somewhat elegant, I'd love to hear about it.

Also, I know I promised a follow-up on the NDS WiFi programming. I only got a little distracted, the example applications are nearly finished. You'll definitely like it when it comes.

Tags: , , , ,