VERC: How to Align a Model Against the Floor Last edited 1 year ago2022-09-29 07:54:56 UTC

It is simple enough you could find it yourself. This article's purpose is to save you some time.

I will assume you have at least some knowledge in liner algebra for the purpose of this article. I won't explain what a dot product [1] is, what trigonometric functions are, etc.

Base Knowledge

Half-Life models are oriented by 3 float values called angles. They are measured in degrees. Though they are a very different quantity from a vector, they are stored in a vector object internally. Mappers would be familiar to the names "pitch," "yaw," and "roll," [2] which are the values stored in the vector object as x, y, and z, respectively.

Here is a figure that shows how models are transformed.
User posted image
Half-Life uses a right-handed coordinate system, and treats x, y, and z components as forward, left, and up directions, respectively.
Note that pitch will increase as we look down.

A normal vector is a vector which is perpendicular to a surface and has a length of 1 [3]. A normal vector can completely express a plane's orientation.

How It Works

We can obtain a vector normal to a surface using UTIL_TraceLine(). We can use this vector to figure out how to adjust the model's angles to align it to the surface.

First, we must convert the model's angles to two vectors representing orientation, because the liner algebra doesn't use angles directly. As you may know, the engine provides a function to convert a vector to angles, but not the reverse (there is a function called pfnAngleVectors() used by the engine, but no documentation on how it works, and I'm the kind of person who doesn't like to use undocumented features). So we must do this ourselves. Be aware that there isn't enough information in one vector to determine angles.

We chose (1,0,0) and (0,1,0) when the model doesn't rotate as the vectors. Then, we project the vectors to the surface.
User posted image
We can project a vector to a plane using this equation:
v' = v - (n • v)n
where v' is projected vector, v is the vector to be projected, and n is the normal vector.

Finally, we convert the vectors back to angles.

How to Code It

Suppose we are going to make weapons align to the ground when they are dropped. We want to add code to the CBasePlayerItem::FallThink() function. This function is located in the upper half of weapons.cpp.
void CBasePlayerItem::FallThink ( void )
{
     pev->nextthink = gpGlobals->time + 0.1;

     if ( pev->flags & FL_ONGROUND )
     {
          // clatter if we have an owner (i.e., don't clatter if the gun is waiting to ..
          if ( !FNullEnt( pev->owner ) )
          {
               int pitch = 95 + RANDOM_LONG(0,29);
               EMIT_SOUND_DYN(ENT(pev), ..
          }

          // lie flat
          pev->angles.x = 0;
          pev->angles.z = 0;

          Materialize();
     }
}
You see, this code assumes a surface is always flat. So we begin to comment out the code.

First, we trace a line down to get the normal vector, so we need a TraceResult object to store the result of tracing.

Then, name the orientation vectors "angdir" and "angdiry". They can be easily obtained using the angles (look at the first figure). One notable point is that they are in degrees, so we must convert degrees to radians prior to calling the trigonometric functions. This is simplified by defining a macro like this:
#define deg2rad (2 * M_PI / 360)
Project the vectors using tr.vecPlaneNormal and the equation shown above. Change the angles with the projected vectors. UTIL_VecToAngles() sets resulting angle's roll to 0, so we must find it with angdiry.

The final code:
    // lie flat
    // pev->angles.x = 0;
    // pev->angles.z = 0;

    TraceResult tr;

    // look down directly to know the surface we're lying.
    UTIL_TraceLine( pev->origin, pev->origin - Vector(0,0,10), ignore_monsters, edict(), &tr );

#ifndef M_PI
#define M_PI 3.14159265358979
#endif
#define ang2rad (2 * M_PI / 360)

    if ( tr.flFraction < 1.0 )
    {
        Vector angdir = Vector(
            cos(pev->angles.y * ang2rad) * cos(pev->angles.x * ang2rad),
            sin(pev->angles.y * ang2rad) * cos(pev->angles.x * ang2rad),
            -sin(pev->angles.x * ang2rad));

        Vector angdiry = Vector(
            sin(pev->angles.y * ang2rad) * cos(pev->angles.x * ang2rad),
            cos(pev->angles.y * ang2rad) * cos(pev->angles.x * ang2rad),
            -sin(pev->angles.x * ang2rad));

        pev->angles = UTIL_VecToAngles(angdir - DotProduct(angdir, tr.vecPlaneNormal) * tr.vecPlaneNormal);
        pev->angles.z = -UTIL_VecToAngles(angdiry - DotProduct(angdiry, tr.vecPlaneNormal) * tr.vecPlaneNormal).x;
    }

#undef ang2rad
If you want to make weapons act more realistically (a.l.a. the Svencoop mod), you must do some more tweaking. But it is beyond the scope of this article.

Footnotes

Following are totally useless essays for the purpose of this article, so you can ignore it.
Thanks to Zipster for inspiring me to write this.

[1] It is one of the wonders of English to me that the terms "dot product" and "cross product" are not less popular than their synonyms, "scalar product" and "vector product", respectively. I honestly never thought "dot product" is a good term, because it has no particular reason except the form, and the form may vary by writer (not necesarilly a dot). Actually, a person I know writes a scalar(dot) product of vectors A and B as "<A,B>", in order to distinguish it from normal "products" in terms of tensors. "Scalar product", on the other hand, is more explicit that it yields scalar quantity and is very different from a vector product. But if it causes less confusion, I settle for it. I don't think I can or I should change the trend.

[2] Three values, pitch, yaw, and roll, form rotational matrices around y, z, and x axes, respectively. And then the matrices are multiplied in that order into one matrix. Model vertices are transformed by the matrix from left. So vertex coordinates are rotated in roll, yaw, pitch in that order. It explains why models are transformed in that (like seen in the figure) way.
If the model is to be scaled, a diagonal matrix will be multiplied as well.

[3] Mathematically, a normal vector needs not to have a length of 1. But practical computing almost always treats normal vectors to have, for the sake of performance and convenience.
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.