Monday, July 21, 2014

Making the Correct Rotations Work

Continued from the previous entry.

In the previous posts, I've managed to get the relative position and rotation values to match the ones from the retail version of EMI. In this post, I'm going to discuss how to present these rotations on the screen, fixing the messed up image we left off with in the last post:
Where we left off...
Let's start by taking a look at the apitrace output of this scene again. I started by running the retail version of the game with the options "-w -gl" to start in a windowed mode using OpenGL. After capturing the apitrace of jumping ahead in the script and moving to the set pph using the jump target "160",  I then loaded the trace into qapitrace. First, I had the trace build thumbnails of all of the frames so that I could identify the first frame that drew the scene pictured above. With the scene identified, I then looked through the data captured for this frame to inspect the rotation matrices that were responsible for rotating and placing the rendered objects.

I decided to focus on the trap first because it is a constant object in the scene and isn't moving. To find the part of the trace responsible for drawing the trap, I browsed through the API calls, checking the Surfaces tab to see if the trap had been rendered yet:
APITrace output, examining the drawing of the trap
With this section of trace found, I could then inspect the OpenGL state (in the Parameters tab) when this was rendered, providing us with the transforms used to place the object in the scene. With these transforms in hand, I then proceeded back to the ResidualVM code and checked the matrices against the ones I found in the trace. From this analysis, it appears that the Projection Matrix is correct, but the ModelView Matrix is incorrect. Let's take a look at what ResidualVM is doing to arrive at the ModelView Matrix for the model of the trap.

First, I inspected the value of the ModelView matrix at the start of drawing the trap and found that it was set to an identity matrix. This was then multiplied by another matrix using glMultMatrixf, then translated using glTranslatef, then multiplied once more, again with glMultMatrixf. In the retail version, the only command run is to load the matrix using glLoadMatrix, indicating that the matrix was probably prepared ahead of time or done in software. That said, with the correct result for the ModelView matrix, we can go back through the steps taken in ResidualVM and correct the problem.

So, where in the code do we find this sequence for setting the ModelView? In the OpenGL path (engines/grim/gfx_opengl.cpp), there's a function called startActorDraw which prepares the ModelView matrix for drawing the object. In the non-overworld case, it multiplies the current camera rotation, then translates with the camera position. Then, it takes the Actor's final matrix, transposes it and multiplies the ModelView by the result. There are a few likely possibilities for where this implementation is wrong:
  1. The camera rotation is wrong because we changed the Euler Order for the actor as part of this fix
  2. The actor's final rotation matrix is incorrect
Since I suspect that the final rotation matrix is correct because of the work we've done up to now, I'm going to first start with checking into the camera rotation. To begin, where does the camera rotation come from? In the same file, there's two functions that are responsible for setting up the camera, positionCamera and setupCamera. When the set is loaded, there are a number of parameters that define the camera for each setup in the set:
  • position - The point position of the camera
  • interest - The point that the camera is pointed at
  • roll - The roll of the camera
  • fov - The field of view of the camera
  • nclip - The near clipping plane of the viewing frustum
  • fclip - The far clipping plane of the viewing frustum
However, after some investigation, I found that the interest and roll parameters are really just quaternion components, unlike in Grim. The existing ResidualVM code was using them as Quaternion  components, but they were using the original Grim Fandango names for them.

Also, when creating a set with these parameters, the retail version of EMI creates an actor object that's kept as part of the setup to represent the camera. This allows it to keep track of the position and rotation information in the same way as the actors in the set.

Following the usage of these parameters in ResidualVM to the function setupCamera, I compared the calculated frustum to the apitrace results and found that ResidualVM was computing this correctly. So the issue was most likely related to the positionCamera function. So, how can we fix it?

I started by examining the retail version again in IDA Pro and found that there are a number of camera functions to retrieve the camera rotation and position. Unfortunately, these weren't yet functional in ResidualVM, so I added them as a part of this work. These functions are:
  • GetCameraYaw
  • GetCameraRoll
  • GetCameraPitch
  • PitchCamera
  • RollCamera
  • YawCamera
To implement this functionality, I decided to keep the rotation of the camera internally as a matrix, and calculate the Euler Angles as needed. When writing out the value for saved games, I use the Quaternion representation of this rotation so that it's compatible with the existing version. Why did I choose to keep the rotation as a matrix? In all of the graphical code that uses the camera rotation, it previously had to convert between a quaternion and a matrix each time the rotation was used. By keeping the rotation as a matrix, we avoid that overhead for the common case and only convert when reading or writing the setup rotation values for saved games. After checking these functions against the retail version, we see that the camera rotation values are placed and rotated properly.

Still, at this point, after all of this work, the graphics themselves haven't been fixed. We haven't change any of the output since the beginning of this post, just the representation in the code. So, how are the rotation matrices computed for display? First, here's the desired ModelView matrix for the trap, extracted with apitrace:

Crawdad Trap: Retail ModelView Matrix
-0.516999 -0.234287 0.823299 0.0
-0.0168343 0.964411 0.263873 0.0
0.85582 -0.122562 0.502544 0.0
-2.60927 -1.20781 -19.2888 1.0

What can we see just from this inspection? Well, we can see that there's a 3x3 rotation matrix in the upper right corner, and a 1x3 position vector in the last row. Let's compare this with the matrix from ResidualVM:

Crawdad Trap: ResidualVM ModelView Matrix
-0.542569 0.212863 -0.812594 0.0
-0.016834 0.964411 0.263873 0.0
-0.839843 -0.156848 0.519676 0.0
-2.60927 -1.20781 -19.2888 1.0

First, we can see that the ResidualVM position agrees with the position from the Retail version of EMI. We can also see that the middle row of the rotation matrix matches, but the others are slightly off. What might be causing this problem? Well, if we inspect the rotation for the trap, we can see that only the Yaw parameter is rotated, which corresponds to a matrix where the middle row is an identity. This explains why the middle row matches! Now, how about the other rows?

Let's start by going back to startActorDraw in gfx_opengl.cpp. I first checked to see what the current value of the ModelView matrix was before this function acted on it and found that the matrix was an identity, but the 3rd row was multiplied by -1. This was caused by the previous call to glScalef(1.0, 1.0, -1.0). I also checked the stored rotation for the trap's actor and compared that with ResidualVM and found that both ResidualVM and the retail version had the same values. With this information in hand, I then tried changing the order of the operations, as a rotation followed by a translation might not have the same result as the opposite operation. In the end, performing the rotation first, then computing the translation resulted in the correct rotation result. Here's the output now:
Looks pretty good?
Awesome! It looks like it's working properly again! How about the pole after detaching? What are the differences remaining in this scene?
I knew something was still missing! :(

At first glance, it looks fine, but a glaring difference remains: the pole! It's completely missing in ResidualVM. It appears that there's still some users of getFinalMatrix which seems to be producing an incorrect result. In the next post, we'll dig into those and see if we can fix those rotations as well.

As an aside, one interesting observation that can be made from the apitrace results is that ResidualVM makes a lot more OpenGL calls to produce the same screen. It appears that in the original version, instead of always drawing each vertex as ResidualVM does, the game instead sometimes passes a pointer to a list of vertices and uses the following sequence to draw the object:

     glEnable(GL_CULL_FACE)
     glFrontFace(GL_CW)
     glBindTexture
     glTexCoordPointer
     glColorPointer
     glVertexPointer
     glDrawElements 

This indicates that the original engine passes these coordinates in a list rather than individually. It would probably be a good idea to make the same optimization in ResidualVM as well because as of right now, the original takes ~5k calls to draw the set pph, while ResidualVM takes ~58k. Maybe a later project? We'll see!

No comments:

Post a Comment