VERC: Dynamically Resizing Models Last edited 1 year ago2022-09-29 07:54:34 UTC

In the client.dll source code, there's a file called StudoModelRenderer.cpp. It handles the drawing of all the .mdl models in the game.

Each time the engine wants to draw a model, it calls the function StudioDrawModel. This function then works out where the model is, where all the bones of its skeleton should go, whether there are any special effects that need applying, and finally draws it.

Needless to say, having access to all this code gives us a great opportunity to do special effects!

For this article, I'll be looking at something simple - scaling. I added the following code at the very end of StudioSetupTransform (a function in StudioModelRenderer.cpp which decides where the model will be drawn on the screen):
if (m_pCurrentEntity->curstate.scale != 0)
{
     int j;
     for (i = 0; i < 3; i++)
     {
          for (j = 0; j < 3; j++)
          {
               (*m_protationmatrix)[i][j] *= m_pCurrentEntity->curstate.scale;
          }
     }
}
That's it. Add this code, compile, and you've finished! To test it, you just need to make a monster in Worldcraft, turn off Smartedit and give it a "scale" setting of 2.0 (double size) or 0.5 (half size), or whatever.

So, how does this actually work?

m_pCurrentEntity->curstate is the current state of the entity we're supposed to be drawing. The "scale" variable is one of the properties that make up that state. (I didn't create it - it's been there all along, although previously it was only used by sprites. I'm using it here because having a pre-existing variable makes the example nice and simple. And it seems sensible to reuse it for such a similar purpose.)

So, that's the simple stuff out of the way. What are these "for" loops doing? Multiplying up the numbers in m_protationmatrix.

To understand that, we need to talk a little bit about matrices. Yeah, matrices - the seemingly pointless things you hated (or, for the younger audience, will hate) in maths lessons. Well, this is the moment where you get to learn what matrices are actually for!

A matrix is a rectangular grid of numbers. m_protationmatrix is a matrix with 4 columns and 3 rows, and its 12 numbers are used to define exactly where a model will appear on the screen. Don't let the name fool you: it can express all sorts of transformations, not just a rotation. It comes in two parts:
a b c x
d e f y
g h i z
Numbers in the first three columns are used to scale, rotate and/or skew the model. (I'll explain how in a minute.) Numbers in the right-hand column simply define where the model is in 3d space.

In the StudioSetupTransform function, there's a good example of how the last column can be used - the very first and very last things it does.
void CStudioModelRenderer::StudioSetUpTransform (int trivial_accept)
{
//...some variables...
     vec3_t modelpos;

     VectorCopy( m_pCurrentEntity->origin, modelpos );
//...the rest of the function...
     (*m_protationmatrix)[0][3] = modelpos[0];
     (*m_protationmatrix)[1][3] = modelpos[1];
     (*m_protationmatrix)[2][3] = modelpos[2];
}
As I hope you can see, this code first saves the entity's origin into the modelpos vector, and at the end, copies that vector directly into the last column of the matrix. Yes, it really is that simple to use.

Sadly, the same isn't true of the other three columns. To understand how those work, let's talk about matrices.

Here's how you multiply a 3-by-3 matrix by a 3d vector, to produce a new 3d vector:
[a b c]   [x]   [?]
[d e f] * [y] = [?]
[g h i]   [z]   [?]
1) Take the top line of the first matrix:
[a b c]
2) Multiply the first number in that line by the first number in the vector, multiply the second by the second, and so on.
[ax by cz]
3) Add up the results, and write the answer as the first entry in the result vector.
[ax+by+cz]
[    ?   ]
[    ?   ]
Then, repeat this process for the second row of the matrix, and put the result in as the second entry in the result vector. And then do the same for the third.
[a b c]   [x]   [ax+by+cz]
[d e f] * [y] = [dx+ey+fz]
[g h i]   [z]   [gx+hy+iz]
With me still? Once you get your head around it, you'll hopefully realise that the idea of a matrix is actually quite simple: it's like a recipe. Our ingredients are x, y and z from the vector; the matrix is telling us how much of each ingredient we need to stir into a given part of the result.

So for example, with a matrix like this...
[1 0 0]
[1 0 0]
[1 0 0]
...each line says "stir in 100% of the vector's x value, and 0% of the other two values". Let's see how that works on a vector containing a bunch of random numbers:
[1 0 0]   [453]   [1*453 + 0*7 + 0*99]   [453]
[1 0 0] * [  7] = [1*453 + 0*7 + 0*99] = [453]
[1 0 0]   [ 99]   [1*453 + 0*7 + 0*99]   [453]
Since the three lines in the matrix were the same, we got the same answer for each entry in the result.
Now, here's a related one:
[1 0 0]
[0 1 0]
[0 0 1]
Here, the first line says "use 100% of x, and nothing else". The second line says "use 100% of y, and nothing else". The third line says "use 100% of z, and nothing else".

Guess what the result is -
[1 0 0]   [453]   [1*453 + 0*7 + 0*99]   [453]
[0 1 0] * [  7] = [0*453 + 1*7 + 0*99] = [  7]
[0 0 1]   [ 99]   [0*453 + 0*7 + 1*99]   [ 99]
No change at all! This "no change" matrix is known as the "identity" matrix.

Ok. How about if we wanted to rearrange the components of a vector? (don't ask me why...)
[0 1 0]   [453]   [0*453 + 1*7 + 0*99]   [  7]
[0 0 1] * [  7] = [0*453 + 0*7 + 1*99] = [ 99]
[1 0 0]   [ 99]   [1*453 + 0*7 + 0*99]   [453]
Or making a vector longer, without changing its direction?
[2 0 0]   [453]   [2*453 + 0*7 + 0*99]   [906]
[0 2 0] * [  7] = [0*453 + 2*7 + 0*99] = [ 14]
[0 0 2]   [ 99]   [0*453 + 0*7 + 2*99]   [198]
If you've been paying attention, that last one should have made you remember the beginning. By scaling up each of the components in a matrix, we scale up each value in the result.

So, two pages later, we finally get to see why the scaling code works. Half-Life supplies me with a matrix, I multiply each entry by the scale factor, and as a result, when the model gets drawn, each bit is scaled up! (note that the scaling will be relative to its origin - which for most monsters is at its feet, but not for all).

I hope this was helpful. I'll go into more interesting effects in a future article - assuming people are interested...
This article was originally published on Valve Editing Resource Collective (VERC).
The archived page is available here.
TWHL only publishes archived articles from defunct websites, or with permission. For more information on TWHL's archiving efforts, please visit the TWHL Archiving Project page.

Comments

You must log in to post a comment. You can login or register a new account.