Sin and Cos: The Programmer's Pals
helixsoft.nl
helixsoft.nl
It's because atan2, along with all the other inverse trigonometric functions, is incredibly slow. On a GPU, sin and cos may be a couple of cycles, but asin/acos/atan/atan2 may be upwards of 30 or 40.
Often times, angles are best avoided when possible for this reason. Normalized vectors end up faster and simpler in many cases.
Relevant anecdote: I recently wrote some code to compute normals in 2D and thought I would be clever by encoding the normal as an angle to save space. It turned out that the atan2 call at the end was so slow that it outweighed the cost of everything else I was doing several times over. Lesson learned…
But also - most of the time gamedev happens in 3D space not in 2D space (even if it gets rendered down to 2D). So it's more natural to do a dot product of two 3d vectors, than try to mess around with trig functions that work on scalar angles. Same with rotations: it's easier and better to use quaternions in 3D space than try to rotate along each axis separately.
[1] GPU intrinsic estimated costs: http://www.fractalforums.com/programming/shader-function-or-...
* Compact storage compared to matrices
* No gimbal-lock
* Multiplication is cheap (mult + add, very similar to vector operations)
* Interpolation is cheap
You really only pay the sin/cos costs when translating out to a Mat3x3 at the end.
You don’t need sin/cos for that either.
Here’s a code that does that:
https://github.com/Microsoft/DirectXMath/blob/master/Inc/Dir...
Also, you don't pay the sin/cos costs when converting to a rotation matrix. For angle axis
R = I + (sin x) * S + (1 - cos(x)) * S^2
Where S is the 3x3 skew symmetric matrix.A unit quaternion representing a 3D rotation is sort of like angle axis
q = cos(x/2), sin(x/2) <axis>
But sin(x) = 2*cos(x/2)*sin(x/2)
(1-cos(x) = 2*sin(x/2)*sin(x/2)
Where you pay the cost is in the construction of the quaternion.I've more often encountered the need to integrate a quaternion, e.g. from inertial measurements or a rigid body physics model, but this is also a textbook lookup away.
This is what surprised me to learn recently, that some transcendentals are free, as long as you don't use too many of them.
atan2 requires this even in 2d, we just assume the natural basis and assume the other vector is x hat.
Dot product avoids all this in any dimension.
I agree dot product is often better, for other reasons.
sin(x) / x
(1 - cos(x)) / x^2
directly without incurring the numerical problems arising from the division.One thing to understand is that the stdlib math functions are accurate to machine precision, i.e. the closest representable value to the actual answer.
For 32-bit floats, this is achievable using a 5 or 6 order polynomial approximation. However, there was one surprise I was previously unaware of.
Obtaining the correct precision becomes difficult when the result is close to zero, since any absolute error is divided by the magnitude of the result. Silly example, if
sin(0.000001) ~= 0.1
Then the absolute error is small (~0.1), but the result is still off by four orders of magnitude.Sometimes this is not a problem. When x is small, sin(x) can be computed by simply returning x. This rule is valid until x^3/6 > precision ~= 1e-38, or x = 4e-16. So exact precision is obtainable by simply returning x if x < 1e-16.
But now consider:
x = pi/2; cos(x) = 0
The logical way to compute this is as -sin(x - pi/2)
But this creates another problem, since x - pi/2 ~= 0, a catastrophic cancellation occurs. Now recall that glibc is required to be accurate to machine precision, which goes down to 1e-38. But x is order 1 and only represents 7 digits, down to 1e-6. So glibc is forced to scour lookup tables to recover the missing digits of pi.This makes computing cos(x > pi/4) about twice as expensive than cos(x < pi/4), but there is a certain irony since it is very unlikely x is known down to 7+ digits, so those CPU cycles are wasted. How many times have you seen x = sqrt(aa + bb + c*c); ?
On my 4 year old laptop:
time ./a.out 0.5 4.12s
time ./a.out 1.5 7.31s
On my raspberry pi: time ./a.out 0.5 1m15.829s
time ./a.out 1.5 1m47.522sIs this actually the case? This patch seems to suggest it's not:
https://www.sourceware.org/ml/libc-alpha/2018-02/msg00336.ht...
However, in glibc, there are definitely code paths for looking up digits of pi.
"This rule" seems a lot like the "skinny triangle" rule. I recently ran across it on Wikipedia after going through astrophysics articles. Specifically, to my understanding, the parsec could be calculated without using a trig function thanks to the skinny triangle rule. (To a certain level of error, of course. And, the parsec is now a defined value so the original trig-derived definition is invalid.)
This rule is exactly the Skinny Triangle Rule, or rather the part of the Skinny Triangle Rule which overlaps with the Small Angle Approximation[0].
[0]: https://en.wikipedia.org/wiki/Small-angle_approximation
In many use-cases we would pre-multiply the value we throw into sin and cos with two times Pi anyway, so couldn't one skip the circle constant altogether and a write function like sin_one(t) and cos_one(t), where each call "implicitly multiplies" t by two times Pi? (perhaps, if constructed well, the function could even be more precise, since it avoids the rounding errors, however small, of multiplying by the pi constant)
If you're working in a case where you can afford slightly lower accuracy, the 3.5ULP method can be a very significant speedup, especially since you're vectorizing at the same time. I got dozens of times in speedup when testing just the 1.0 ULP bound when performing sin/cos on a large vector. (I don't remember the precise number and should just re-run the tests.)
To generically dispatch this library, in case there's any interest, I wrapped it in some template metaprogramming in [2], so that the widest vectorization available is selected at compile-time when selecting a given operation.
[0]: http://sleef.org/
Scaling the polynomial, as you suggest, would simply require reevaluating the coefficients. For example, a5 (2pix)^5 would become 32 a5 pi^5 x^5.
In the cos case where regions of interest approach zero, divide by another polynomial with the same zeros to create limits. cos(x)/(pi^2/4 - x^2) -> 1/pi as x -> pi/2. Then approximate that function, and evaluate approx(x) \* (pi^2/4 - x^2), so you are multiplying the approximated value by something close to zero. This way you get precision in a way similar to sin(x) = x when x small.
I learned games programming as a teenager in that era, and I present my fixed-point 3D code from 1996, which does sin and cos through the medium of 512-entry 32-bit integer lookup tables:
The follow-on article [0] references this article as appearing in "issue 5" of Pixelate, so it seems you found the original source. Issue 4 [1] covered June 2001, probably published in July, and so it seems that issue 5 was probably published in August 2001.
0: http://www.helixsoft.nl/articles/sphere/sphere.html
1: https://wiki.allegro.cc/index.php?title=Pixelate:Issue_4/Bit...
Original reply: And from the presence (even in that 2002 version) of the 'allegro.cc' domain, we can narrow the range of dates from the other end, too: The .cc domain was introduced in October 1997, and Allegro.cc claims visitors since January 1999.
In the early days of Macromedia Flash, I wanted to build a GTA I clone. I didn't know about trigonometric functions yet, so I had a line of height 1, width 0 that I rotated around one of its ends at a fixed angular velocity while the user pressed the left and right arrow keys.
Then I read the height and width of the rotated object and added it to the x and y coordinates of the car, respectively.
For some reason, trigonometry is treated pretty late in Swiss schools (often only during the Matura, where students are 16-20y/o). I never understood this, since understanding trig can be really useful in a lot of everyday situations.
Maybe it's possible to learn about sines and cosines from Euler's identity up and forget about triangles altogether. This would enable you to do calculus, statistics, etc. But there's some value to teaching the theory of triangles as a beginner's introduction to pure mathematics (and hence the beauty in mathematics).
--- What follows has to do with triangles, but is mostly offtopic.
One of the clearest memories of my childhood, somewhere between ages 8 and 10, is a German-made (but dubbed into my language) TV educational show that had men in togas walking around Classical Greek ruins and dramatizing some scenes -- Diogenes and Alexander and so on. But what has sticked in my mind and still burns red hot was Pythagoras and his mates, crouched and drawing lines in the sand. After Pythagoras explains his hypothenuse theorem, someone interjects:
"But this holds for all triangles?"
"All triangles. All triangles that have ever been drawn -- and all triangles that will never be drawn."
A sufficiently powerful brain scanning device probably can find this line somewhere inside my skull. It got me in big trouble: I'm not particularly intelligent, let alone disciplined, but I'm slogging through a Masters in mathematics, maybe dreaming about a PhD in physics because... I don't know. It's like when ghetto kids see some violent scene right in front of them and 20 years later their criminals. It sticks with you.
It's like asking "what everyday situations is addition useful in".
To that I’d say: finding the height of something only knowing the length of the base and an angle. Or triangulating your position, surveying your property to make a garden, etc.
This seems like an exaggeration, at least from the point of view of calculus in the classroom, where the prevalence of trigonometric functions reflects only their convenience as examples. Did I misunderstand? Perhaps more apposite is differential equations, where trigonometric functions (or exponentials more generally) are ubiquitous.
For instance calculating the deflection of a structural beam as you load it with weight (a discipline known as statics), depends heavily on trig. Anything involving rotation and angular momentum (such as gears and axles) require knowledge about trig.
Other than that triangulation- computing the distance between points is a useful practical application.
What i hadn't realised is that before logarithms were developed, people used to do it with sines and cosines, exploiting some identities from spherical geometry, and the tables they already had for navigation:
I'd like to do an update or write a follow up article. What would you be interested in seeing? More examples? Different concepts? Should I port the examples to a different framework?
And for cases when you need to multiply angles by a scalar (e.g. to interpolate between two angles), quaternions is the way to go: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotati...
My what?
Oh, you mean Oscar Had A Headache Over Algebra.
I'd love to find a math cheat sheet poster with comparative mnemonics for the office wall. There are a ton of them. I imagine they vary depending on the pedigree of the teacher.
- Soh: sin θ = o/h
- Cah: cos θ = a/h
- Toa: tan θ = o/a
Where "θ" is the angle, "o" is the side opposite of the angle, "a" is the side adjacent to the angle, and "h" is the hypotenuse.
Or for the visually inclined:
+
/|
h / |
/ | o
/θ |
+----+
aWhenever I pass through central station in Sydney I hear my high school math teacher's voice:
Less than PI/2 "All are positive PI/2 through to PI "Sin is positive" PI through to 3/2 PI "Tan is positive" 3/2 PI through to 2PI "Cos is positive"
I had come from a QBasic background and was trying to learn C++. QBasic was wonderful because it put everything a budding game developer could want at their fingertips—full screen graphics and text modes, drawing primitives, sprites, rudimentary sound and music using the PC speaker, etc.
In 1999 I got my first taste of C++ with MSVC++ 6.0, but soon lost interest. I had to limit myself to text-based command-line programs, or attempt to learn the Windows APIs, which seemed way over my head at that point. What I really wanted was C++ but with QBasic-like APIs.
Probably around 2002-2003 I discovered Dev-C++ and the Allegro library. It was exactly what I had been looking for and more! It reinvigorated my interest in programming and helped me learn tricky concepts like pointers and memory management. I probably wouldn't be an engineer today if it weren't for Allegro.
(Source: I volunteer with the Toronto Public Library to help high school students with homework, mainly math.)
Sin and Cos correspond to the rejection and projection of two unit vectors. When they rotate past each other, the projection still has the same direction, but the rejection changes sign, so it must correspond to the anti-symmetric sin, not the symmetric cos.
Take a circle of radius 1 around the origin. Draw a line with angle x through the origin. The point where this line intersects the circle has vertical position sin x and horizontal position cos x. Moreover, if you measure the angle in radians, x is how much of the unit circle your angle covers.
This also immediately makes the identity (sin x)^2 + (cos x)^2 = 1 obvious.
I guess your method works if your angle starts at north and increases clockwise.
Hmm, I wouldn't be so sure... Because it is by the way of geometry that people normally find explanations of mathematical concepts that are the most intuitive to them. (To be fair, some prefer dealing with symbolic algebra instead.)
For instance, a cropped sine ( [-pi/2,3pi/2] ), remapped to [0,1] in both axes is very handy as a smooth-step function.
Another range ( [0, pi] ) is very convincing as a screen flash.
I just couldn't wrap my head around the idea that you could simply calculate this using basic arithmetic operations.
One thing I learned from this experience was that these functions are expensive to calculate and it's actually pretty easy to write your own, faster but less precise implementation.
Here’s a C++ implementation of minimax approximations for some popular functions, with good license (boost): https://www.geometrictools.com/Source/Functions.html
This is now even more true than when this article was written, enough that I've been surprised lately and feel like I need to unlearn things I thought I knew about floats and transcendentals. They only cost a handful of clocks now in the worst case, with dependent instructions before and after. Depending on what else is going on, I think you can sometimes get sin & cos for the same cost as 1 add instruction; as long as you have a lot of other independent math nearby and/or memory access. Sin & cos & sqrt are crazy fast on GPUs, when using ShaderToy, for example.