It eschews angles entirely, sticking to ratios. It avoids square roots by sticking to "quadrances" (squared distance; i.e. pythagoras/euclidean-distance without taking square roots).
I highly recommend Wildberger's extensive Youtube channels too https://www.youtube.com/@njwildberger and https://www.youtube.com/@WildEggmathematicscourses
He's quite contrarian, so I'd take his informal statements with a pinch of salt (e.g. that there's no such thing as Real numbers; the underlying argument is reasonable, but the grand statements lose all that nuance); but he ends up approaching many subjects from an interesting perspective, and presents lots of nice connections e.g. between projective geometry, linear algebra, etc.
I wholeheartedly agree with the point being made in the post. I had commented about this in the recent asin() post but deleted thinking it might not be of general interest.
If you care about angles and rotations in the plane, it is often profitable to represent an angle not by a scalar such as a degree or a radian but as a tuple
(cos \theta, sin \theta)
or as a complex number.This way one can often avoid calls to expensive trigonometric functions. One may need calls to square roots and general polynomial root finding.
In Python you can represent an angle as a unit complex numbers and the runtime will do the computations for you.
For example, if you needed the angular bisector of an angle subtended at the origin (you can translate the vertex there and later undo the translation), the bisector is just the geometric mean of the arms of the angle
sqrt(z1 * z2)
Along with stereographic transform and its inverse you can do a lot.This is directly related to the field of algebraic numbers.
With complex numbers you get translations, scaled rotations and reflections. Sufficient for Euclidean geometry.
I think this is missing the reason why these APIs are designed like this: because they're convenient and intuitive
Its rare that this kind of performance matters, or that the minor imprecisions of this kind of code matter at all. While its certainly true that we can write a better composite function, it also means that.. we have to write a completely new function for it
Breaking things up into simple, easy to understand, reusable representations is good. The complex part about this kinds of maths is not the code, its breaking up what you're trying to do into a set of abstracted concepts so that it doesn't turn into a maintenance nightmare
Where this really shows up more obviously is in more real-world library: axis angle rotations are probably a strong type with a lot of useful functions attached to it, to make your life easier. For maths there is always an abstraction penalty, but its usually worth the time saved, because 99.9999% of the time it simply doesn't matter
Add on top of this that this code would be optimised away with -ffast-math, and its not really relevant most of the time. I think everyone goes through this period when they think "lots of this trig is redundant, oh no!", but the software engineering takes priority generally
You can then use householder matrix to avoid trigonometry.
These geometric math tricks are sometimes useful for efficient computations.
For example you can improve Vector-Quantization Variational AutoEncoder (VQ-VAE) using a rotation trick, and compute it efficiently without trigonometry using Householder matrix to find the optimal rotation which map one vector to the other. See section 4.2 of [1]
The question why would someone avoid trigonometry instead of looking toward it is another one. Trigonometry [2] is related to the study of the triangles and connect it naturally to the notion of rotation.
Rotations [3] are a very rich concept related to exponentiation (Multiplication is repeated addition, Exponentiation is repeated multiplication).
As doing things repeatedly tend to diverge, rotations are self stabilizing, which makes them good candidates as building blocks for the universe [4].
Because those operations are non commutative, tremendous complexity emerge just from the order in which the simple operations are repeated, yet it's stable by construction [5][6]
[0]https://en.wikipedia.org/wiki/Householder_transformation
[1]https://arxiv.org/abs/2410.06424
[2]https://en.wikipedia.org/wiki/Trigonometry
[3]https://en.wikipedia.org/wiki/Matrix_exponential
[4]https://en.wikipedia.org/wiki/Exponential_map_(Lie_theory)
I think over the years I subconsciously learned to avoid trig because of the issues mentioned, but I do still fall back to angles, especially for things like camera rotation. I am curious how far the OP goes with this crusade in their production code.
Agreed. In my view, the method the author figured out is far from intuitive for the general population, including me.
User moves cursor or stick a number of pixels/units. User holds key for a number of ms. This is a scalar: An integer or floating point. I pose this to the trig-avoiders: How do I introduce a scalar value into a system of vectors and matrices or quaternions?
I find this flow works well because it's like building arbitrarily complex transformation by composing a few operations, so easy to keep in my head. Or maybe I just got used to it, and the key is find a stick with a pattern you're effective with.
So:
> For example, you are aligning a spaceship to an animation path, by making sure the spaceship's z axis aligns with the path's tangent or direction vector d.
Might be:
let ship_z = ship.orientation.rotate_vec(Z_AXIS);
let rotator = Quaternion::from_unit_vecs(ship_z.to_normalized(), path.to_normalized());
ship.orientation *= rotator;
I should break this down into individual interoperations to compare this to the two examples in the article. To start, `from_unit_vecs` is based on the cross product, and `rotate_vec` is based on quaternion-vector multiplication. So no trig there. But `quaternion::from_axis_angle()` uses sin and cos.I need to review for the sort of redundant operations it warns about, but from a skim, I'm only using acos for SLERP, and computing dihedral angles, which aren't really the basic building blocks. Not using atan. So maybe OK?
edit: Insight: It appears the use of trig in my code is exclusively for when an angle is part of the concept. If something is only vectors and quaternions, it stays that way. If an angle is introduced, trig occurs. And to the article: For that spaceship alignment example, it doesn't introduce an angle, so no trig. But there are many cases IMO where you want an explicit angle (Think user interactions)
I mean I'm perfectly aware that language is a descriptive cultural process etc etc but man this bugs the crap out of me for some reason
We ran into an analogous situation building audn.ai (https://audn.ai), an AI red-teaming platform. The naive approach to finding behavioral vulnerabilities in AI agents is to enumerate attack prompts β essentially computing "angles" by hand. But the real geometric primitive is adversarial pressure: you want a system that directly produces the failure modes without an acos/cos detour through human-crafted prompt lists.
So we built an autonomous Red Team AI (powered by our Pingu Unchained LLM) that generates and runs adversarial simulations in a closed RL loop with a Blue Team that patches defenses in real time. The result β millions of attack vectors without the manual enumeration step β feels a bit like replacing rotationAxisAngle(acos(dot(z,d))) with a direct cross/dot formulation. The "angle" abstraction just falls away.
Anyway, great article. The principle that elegance usually signals you've found the right representation applies pretty broadly.
There are certain drawbacks. If the solution involves non-algebraic numbers there is no getting away from the transcendental numbers (that ultimately get approximated by algebraic numbers).
If I orient myself I have not taken the subway but the orient express, Iβm afraid..
Have you ended up with a set of self-implemented tools that you reuse?
I'm a trig-avoider too, but see it more as about not wiggling back and forth. You don't want to be computing angle -> linear algebra -> angle -> linear algebra... (I.e., once you've computed derived values from angles, you can usually stay in the derived values realm.)
Pro-tip I once learned from Eric Haines (https://erich.realtimerendering.com/) at a conference: angles should be represented in degrees until you have to convert them to radians to do the trig. That way, user-friendly angles like 90, 45, 30, 60, 180 are all exact and you can add and subtract and multiply them without floating-point drift. I.e., 90.0f is exactly representable in FP32, pi/2 is not. 1000 full revolutions of 360.0f degrees is exact, 1000 full revolutions of float(2*pi) is not.
> Now, don't get me wrong. Trigonometry is convenient and necessary for data input and for feeding the larger algorithm. What's wrong is when angles and trigonometry suddenly emerge deep in the internals of a 3D engine or algorithm out of nowhere.
In most cases it is perfectly fine to store and clamp your first person view camera angles as angles (unless you are working on 6dof game). That's surface level input data not deep internals of 3d engine. You process your input, convert it to relevant vectors/matrices and only then you forget about angles. You will have at most few dozen such interactive inputs from user with well defined ranges and behavior. It's neither a problem from edge case handling perspective nor performance.
The point isn't to avoid trig for the sake of avoiding it at all cost. It's about not introducing it in situations where it's unnecessary and redundant.
Better use spin groups: they work in every dimension.
I can build a coherent algebra system in which 2 + 2 = 5 and people have every right to be annoyed by that. The origins of mathematics and all its conceivable applications are connected to reality, so the axioms are not immune to criticism on these grounds, no matter how many mathematicians think otherwise.
If you can derive a contradiction using his methods of computation I would study that with interest.
By "sound" I do not mean provably sound. I mean I have not seen a proof of unsoundness yet.
Can you elaborate on this? I think many understand that the "existence of some object" implies there is some semantic difference even if there isn't a practical one.
I really enjoyed Wildberger's take back in high school and college. It can be far more intuitive to avoid unnecessary invocation of calculation and abstraction when possible.
I think the main thrust of his argument was that if we're going to give in to notions of infinity, irrationals, etc. it should be when they're truly needed. Most students are being given the opposite (as early as possible and with bad examples) to suit the limited time given in school. He then asks if/where we really need them at all, and has yet to be answered convincingly enough (probably only because nobody cares).
Very good tip about the degrees mapping neatly to fp... I had not considered that in my reasoning.
You can then calculate a quaternion from the pitch/yaw and do whatever additional transforms you wish (e.g. temporary rotation for recoil, or roll when peeking around a corner).
The most impressive math I've seen done during a real-time technical conversation was by someone leveraging comprehensive command of trig identities.
Fair point, but I think you misspelled Projective Geometric Algebra
I think we should use less trigonometry in computer graphics. As my understanding of projections, reflections, and vector operations improved over time, I experienced a growing unease every time I saw trigonometry at the core of 3D algorithms inside rendering engines. These days I'm at a point where if I see some asin, acos, atan, sin, cos or tan in the middle of a 3D algorithm, I'll assume the code was written by an inexperienced programmer and that it needs review.
Now, don't get me wrong. Trigonometry is convenient and necessary for data input and for feeding the larger algorithm. What's wrong is when angles and trigonometry suddenly emerge deep in the internals of a 3D engine or algorithm out of nowhere. In other words, where the inputs and the outputs to an algorithm of function are vectors and geometry, and suddenly in the middle of the implementation trigonometry appears. Because, most of the time (if not all of the time) such a thing is unnecessarily complicated, error prone and overall unnecessary. It also gets in the way of elegance and truth, if that matters to you. Let me explain.
I've already discussed in other places before how the dot and cross products encode all the information you need to deal with orientation related operations. These two products respectively capture all that you've learnt about cosine and sine of angles. Actually, cosine and sine are really dot and cross products in disguise, which has been taken out of a geometric context and ripped of physicality. For example, they operate on "angles" which are a rather abstract thing compared to the actual "vectors" or "lines" that the two products operate on. Anyways, my claim for now is that when working with objects and vectors, going to angular and trigonometry land is an error. But let me make this claim more concrete with one typical example:
Say you have this function, which computes a matrix that rotates vectors around a given axis v by an amount of a radians. Many 3D engines, renderers or math libraries will have one such routine. 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( v.x*v.x*ic + co, v.y*v.x*ic - si*v.z, v.z*v.x*ic + si*v.y, v.x*v.y*ic + si*v.z, v.y*v.y*ic + co, v.z*v.y*ic - si*v.x, v.x*v.z*ic - si*v.y, v.y*v.z*ic + si*v.x, v.z*v.z*ic + co ); }
So now imagine you are somewhere in the internals of your project, writing some code that needs to set the orientation of an object in a given direction. For example, you are aligning a spaceship to an animation path, by making sure the spaceship's z axis aligns with the path's tangent or direction vector d. So, you decide you want to use the rotationAxisAngle() function for that. Cool.
Then, you measure the angle between the spaceship's z axis and the desired orientation vector d so you know by how much you'll need to rotate it. Since you are a graphics programmer, you know you can do this by taking the dot product of the two vectors and then extracting the angle from it with an acos() call. Great. Also, because you know that acos() can return nonsense if its argument lays outside of the -1..1 range, you decide to clamp the dot product to -1..1 before calling acos(), just in case. "Because floating point". Fine. Although at this stage one kitten has already been murdered. But since you don't know that yet, you proceed to writing the rest of the code.
So, next you compute the rotation axis, which you know is the cross product of your z and d vectors, for all points in the spaceship need to rotate in planes parallel to that spanned by these two vectors. Then you decide to normalize the rotation axis vector, just because you often do so just to make sure things will work, regardless of whether you actually needed to normalize the vector or not. And so, in the end, your code looks 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 );
This does work. But I claim it's wrong. To see why, let's expand the code of rotationAxisAngle() in place and see the whole picture of what's mathematically going on:
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*axiz, 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 have already probably noticed, we are performing an rather imprecise and expensive acos() call just to undo it immediately after by computing a cos() on its return value. cos(acos(x)) is just x after all. This begs the question: why not skip the acos()/cos() chain altogether?
The key insight here is that this is not just a mere optimization (skipping cos, acos and clamp calls), but that maybe what we are revealing here is a deeper mathematical structure or relationship. Indeed, while sometimes simplifications and symmetries in mathematical expressions are certainly just accidental and meaningless, more often than not they actually are the consequence of deep truths. So let's keep exploring what's going on in our code, see if we can learn anything here.
So, a first roadblock seems to be that despite we could in principle simplify the cos(acos(x)) chain, it seems we need to compute the angle or rotation anyways for our sin() function that comes right after the cosine. So we might not be able to simplify things much after all. However this is where we start using the dot and cross products.
If you are familiar with the cross product, you might know that it somehow encodes sines, just like dot products encode cosines. If you never realized this, and that's fine, that's why we are here today. A hint is that the length of a cross product is the length of the two vectors involved times the sine of the angle they form (while the dot product is the lengths of the vectors times the cosine of the angle they form).
So this is the point - wherever there's a dot product, chances are there's a cross product lying around, sometimes a bit hidden perhaps, that completes the picture of what's geometrically happening (an alignment and rotation in our case). And so, generally, for any parallel thing you can measure (dot products, cosines) there's a perpendicular component that you can measure too (cross product, sines). So, this is all a bit abstract perhaps, so let's go ahead and check this out:
We can extract the sine of the angle between z and d by just looking at the length of their cross product. Indeed, z and d are normalized so the length of their cross product is the sine of the angle they form times one and times one again! Which means we can and should rewrite our operation this way:
mat3x3 rotationAlign( const vec3 & d, const vec3 & z ) { const vec3 ax = cross( z, d ); const float co = dot( z, d ); const float si = length( ax ); const vec3 v = ax/si; const float ic = 1.0f - co; return mat3x3( v.x*v.x*ic + co, v.y*v.x*ic - si*v.z, v.z*v.x*ic + si*v.y, v.x*v.y*ic + si*v.z, v.y*v.y*ic + co, v.z*v.y*ic - si*v.x, v.x*v.z*ic - si*v.y, v.y*v.z*ic + si*v.x, v.z*v.z*ic + co ); }
Good, no normalize(), no acos(), no clamp() anymore. We are definitely getting somewhere now. Let's keep going and also get rid of the length/square root operation by noticing that si2=1-co2 (since sine and cosine lay on the unit circle) and by propagating the 1/si term into the matrix and getting some cancellations. What we get is:
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-c)/(1.0f-c*c); return mat3x3( v.x*v.x*k + c, v.y*v.x*k - v.z, v.z*v.x*k + v.y, v.x*v.y*k + v.z, v.y*v.y*k + c, v.z*v.y*k - v.x, v.x*v.z*K - v.y, v.y*v.z*k + v.x, v.z*v.z*k + c ); }
Lastly, we can simplify k to k = 1/(1+c) which is more elegant and also moves the two singularities in k and whole function (d and z are parallel) into a single singularity (when d and z are the same vector, in which case there's no clear rotation that is best anyways (thanks Zoltan Vrana for this tip). So, the final function looks 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( v.x*v.x*k + c, v.y*v.x*k - v.z, v.z*v.x*k + v.y, v.x*v.y*k + v.z, v.y*v.y*k + c, v.z*v.y*k - v.x, v.x*v.z*k - v.y, v.y*v.z*k + v.x, v.z*v.z*k + c ); }
So look at that beauty - there aren't any trigonometric or inverse trigonometric functions, nor clamp or normalizations or even square roots. This is great from a stability and performance point of view. And also, we have solved a problem relating vectors using only vectors, which just feels right.
Now, I'm not going to claim that in this particular example of today we got an intuitive interpretation of the final matrix. Neither we had it in the beginning, so nothing was lost there. Indeed, all I can think of doing to the matrix above is decompose it as
v*vT ( c, -v.z, v.y) M = ------- + ( v.z, c, -v.x) 1+c (-v.y, v.x, c)
The point is, in almost all situations you can perform similar trigonometric simplifications and slowly untangle the math to uncover a simpler vector expression that describes the same problem. Also, oftentimes you can actually rethink the whole problem in terms of vectors from the beginning, which is more intuitive anyways. For that you'll need to develop some intuition about dot products, cross products and projections. But once you have it you'll find that many of the trigonometric identities you find in literature are just a statement about some particular vector configurations, so you'll never have to memorize or look up the identities again. But I digress.
I'll just conclude saying that part of the problem with overusing trigonometry in 3D rendering codebases comes from poorly designed third party APIs, like that of rotationAxisAngle(v,a) or WebXR stereo projection information, which work on angles and force their users to use angles too, infecting our codebases. Sometimes the cost is a performance or stability hit; some other times there's no performance cost but either way I think we should strive to doing more vectors operations and less trigonometry!