Avoid trigonometry

Introduction



It seems to me that we need to use less trigonometry in computer graphics. A good understanding of projections, reflections, and vector operations (as in the true meaning of scalar (dot) and vector (cross) products of vectors) usually comes with a growing sense of anxiety when using trigonometry. More precisely, I believe that trigonometry is good for entering data into the algorithm (for the concept of angles, this is an intuitive way to measure orientation), I feel that something is wrong when I see trigonometry located in the depths of some 3D rendering algorithm. In fact, I think that somewhere a kitten dies when trigonometry creeps in there. And I'm not so worried about speed or accuracy, but with conceptual elegance, I think ... Now I will explain.



In other places, I have already discussed that scalar and vector products of vectors contain all the necessary information for rotations, for those two “rectangular” operations - sines and cosines of angles. This information is equivalent to sines and cosines in such a large number of places that it seems that you can simply use the product of vectors and get rid of trigonometry and angles. In practice, you can do this by staying in ordinary Euclidean vectors, without trigonometry at all. This makes us wonder: “Aren't we doing something extra?” It seems we are doing it. However, unfortunately, even experienced professionals are prone to abuse trigonometry and make things very complex, cumbersome and not the most concise. And even possibly "wrong."



Let's stop making the article even more abstract. Let us imagine one of the cases of replacing trigonometric formulas with vector products and see what I just talked about.



Wrong option to rotate a space or object



Let us have a function that calculates the rotation matrix of a vector around a normalized vector  vecv on the corner a . In any 3D engine or real-time math library, there will be one such function that is most likely blindly copied from another engine, Wikipedia, or OpenGL tutorial ... (yes, by this moment you have to admit, and depending on your mood, it’s possible to worry from because of this).



The function will look something like this:



mat3x3 rotationAxisAngle( const vec3 & v, float a ) { const float si = sinf( a ); const float co = cosf( a ); const float ic = 1.0f - co; return mat3x3( vx*vx*ic + co, vy*vx*ic - si*vz, vz*vx*ic + si*vy, vx*vy*ic + si*vz, vy*vy*ic + co, vz*vy*ic - si*vx, vx*vz*ic - si*vy, vy*vz*ic + si*vx, vz*vz*ic + co ); }
      
      





Imagine that you are digging through the insides of a demo or a game, possibly finishing some kind of animation module, and you need to rotate the object in a given direction. You want to rotate it so that one of its axes, say, an axis  vecz coincided with a specific vector  vecd , say, tangent to the animation path. Of course, you decide to create a matrix that will contain transformations using rotationAxisAngle()



. So, you will first need to measure the angle between the axis z your object and the desired orientation vector. Since you are a graphic programmer, you know that this can be done with a scalar product and then extracting the angle with acos()



.







 veca cdot vecb=axbx+ayby=ab cos angle veca vecb





Also, you know that sometimes acosf()



can return strange values ​​if the scalar product is outside the range [-1; 1], and you decide to change its value so that it falls into this range ( approx. Per. To clamp) (at this point you can even dare to blame the accuracy of your computer, because the length of the normalized vector is not exactly 1). At this point, one kitten died. But until you know about it, you continue to write your code. Next, you calculate the rotation axis, and you know that this is the vector product of the vector  vecz your object and the chosen direction  vecd , all points in your object will rotate in planes parallel to the one defined by these two vectors, just in case ... (the kitten was revived and killed again). As a result, the code looks something like this:



 const vec3 axi = normalize( cross( z, d ) ); const float ang = acosf( clamp( dot( z, d ), -1.0f, 1.0f ) ); const mat3x3 rot = rotationAxisAngle( axi, ang );
      
      





To understand why this works, but still mistakenly, we will open all the rotationAxisAngle()



code and see what really happens:



 const vec3 axi = normalize( cross( z, d ) ); const float ang = acosf( clamp( dot( z, d ), -1.0f, 1.0f ) ); const float co = cosf( ang ); const float si = sinf( ang ); const float ic = 1.0f - co; const mat3x3 rot = mat3x3( axi.x*axi.x*ic + co, axi.y*axi.x*ic - si*axi.z, axi.z*axi.x*ic + si*axi.y, axi.x*axi.y*ic + si*axi.z, axi.y*axi.y*ic + co, axi.z*axi.y*ic - si*axi.x, axi.x*axi.z*ic - si*axi.y, axi.y*axi.z*ic + si*axi.x, axi.z*axi.z*ic + co);
      
      





As you may have noticed, we are making a rather inaccurate and expensive acos call to cancel it right away by computing the cosine of the return value. And the first question appears: “why not skip the acos()



---> cos()



call chain and save processor time?” Moreover, does this not tell us that we are doing something wrong and very confusing , and that some simple mathematical principle comes to us that manifests itself through simplification of this expression?



You can argue that simplification cannot be done, since you will need an angle to calculate the sine. However, it is not. If you are familiar with the vector product of vectors, then you know that just like the scalar product contains cosine, the vector contains sine. Most graphic programmers understand why a scalar product of vectors is needed, but not everyone understands why a vector product is needed (and use it only to read normals and rotation axes). Basically, the mathematical principle that helps us get rid of the cos / acos pair also tells us that where there is a scalar product, there is possibly a vector product that reports the missing piece of information (perpendicular part, sine).





|| veca times vecb||=ab sin angle veca vecb





The right way to rotate a space or object



Now we can extract the sine of the angle between  vecz and  vecd just by looking at the length of their vector product ... - remember that  vecz and  vecd normalized! And this means that we can (we must !!) rewrite the function in this way:



 const vec3 axi = cross( z, d ); const float si = length( axi ); const float co = dot( z, d ); const mat3x3 rot = rotationAxisCosSin( axi/si, co, si );
      
      





and make sure that our new function for constructing the rotation matrix, rotationAxisCosSin()



, does not calculate sines and cosines anywhere, but takes them as arguments:



 mat3x3 rotationAxisCosSin( const vec3 & v, const float co, const float si ) { const float ic = 1.0f - co; return mat3x3( vx*vx*ic + co, vy*vx*ic - si*vz, vz*vx*ic + si*vy, vx*vy*ic + si*vz, vy*vy*ic + co, vz*vy*ic - si*vx, vx*vz*ic - si*vy, vy*vz*ic + si*vx, vz*vz*ic + co ); }
      
      





There is one more thing that can be done to get rid of normalizations and square roots - encapsulating the entire logic in one new function and passing 1/si



to the matrix:



 mat3x3 rotationAlign( const vec3 & d, const vec3 & z ) { const vec3 v = cross( z, d ); const float c = dot( z, d ); const float k = (1.0fc)/(1.0fc*c); return mat3x3( vx*vx*k + c, vy*vx*k - vz, vz*vx*k + vy, vx*vy*k + vz, vy*vy*k + c, vz*vy*k - vx, vx*vz*K - vy, vy*vz*k + vx, vz*vz*k + c ); }
      
      





Later, Zoltan Vrana noticed that k



can be simplified to k = 1/(1+c)



, which not only looks mathematically more elegant, but also moves two features to k and, thus, the whole function (  vecd and  vecz parallel) goes into one (when  vecd and  vecz coincide in this case there is no clear rotation). The final code looks something like this:



 mat3x3 rotationAlign( const vec3 & d, const vec3 & z ) { const vec3 v = cross( z, d ); const float c = dot( z, d ); const float k = 1.0f/(1.0f+c); return mat3x3( vx*vx*k + c, vy*vx*k - vz, vz*vx*k + vy, vx*vy*k + vz, vy*vy*k + c, vz*vy*k - vx, vx*vz*K - vy, vy*vz*k + vx, vz*vz*k + c ); }
      
      





We not only got rid of three trigonometric functions and got rid of the ugly clamp (and normalization!), But also conceptually simplified our 3D mathematics. No transcendental functions, only vectors are used here. Vectors create matrices that modify other vectors. And this is important, because the less trigonometry in your 3D engine, the not only faster and clearer it becomes, but, first of all, mathematically more elegant (more correct!).



All Articles