Partial Scaling – How to do Half a Multiplication
by Malte Skarupke
Programmers have a habit of over-generalizing things, and so it happened that I found myself writing more generalized versions of rotation, translation and scaling, the three most common operations that you’d want to do to 3D objects. These were more generalized in that they had a parameter for how much you want to do a certain operation. Like “translate by (10, 0, 0), but only apply the operation 20%”. This is easy to do for translation: Just multiply by the percentage and only translate by (2, 0, 0). Rotations are also easy in many representations: If the angles are explicit, like in Euler angles, you can just multiply those by the percentage; if you’re using quaternions, you can slerp.
But scaling is more complicated. Internally scaling is just multiplication, but how do you do half a multiplication? What does it mean to say “multiply by 4, but only apply the operation 50%”? My first approach was to multiply by the power, so you’d get “multiply by ” (or if you only want to apply 10% of the operation) and that seems to work when you’re close to 1, but the further away you are from 1, the more wrong it gets. The answer ends up being to take the median of multiplication, division and exponentiation, but let me further explain the problem first:
It turns out that multiplying by answers the question “which operation do I have to apply twice to multiply by ?” Which is almost right, but not what I needed. I needed to answer the question “which operation has half the effect of multiplying by ?” To illustrate how this goes more wrong the further you are removed from 1, lets say you want to scale by 100, but only apply the operation 50%. Then you would expect the object to roughly be half as big as if you had scaled by 100. So you want to scale by 50. But is 10, so the object ends up five times too small.
On the other end of the scale you get the same problem when you’re close to 0. If you’re shrinking the object by factor 100, multiplying by would shrink it by factor 10. But if you apply half the operation, you expect it to be roughly twice as big as the full operation, so instead you want to scale by 0.02, and the exponentiation ends up making the object five times too big.
If this doesn’t convince you yet, try bigger values or different percentages. Like scaling by factor 1000, but only applying the operation 90% results in a scale of . Now put an object that was scaled by 90% next to an object that was scaled 100% and it will be very unexpected that one of them is twice as big as the other.
After writing down a bunch of cases and writing down all the expected behavior, I realized that there are three possible behaviors. If the original multiplication is , and the parameter that controls the strength of the operation is named you get these three cases:
- Close to 0 you want to do
- Close to 1 you want to do
- For large numbers you want to do
This was easy to figure out for extreme values far from 1, but it leaves open the question of where to transition from one behavior to the other. This can get really complicated to figure out for all combinations of , and . But luckily it turns out that if you do the median of these three operations, you always get exactly the expected behavior. One of these is always too big, one is always too small, and the one in the middle is always exactly right. That results in surprisingly elegant code where you never have to figure out the transition point. Just calculate all three and then write a small function that returns the median of three.
There is one detail that you have to look out for: In order to support a value of 0 (which just returns unchanged) you have to handle the case where the division returns NaN. But if you’re aware of that case, you can add a unit test and then your median-of-three function can handle that without overhead, just by doing the comparisons in the right order. (any comparison to NaN returns false, so write the comparisons in an order that ensures the division result won’t be picked when it only returns false)
This isn’t really about the code though. This is just about the realization that
- Doing partial scaling is surprisingly tricky. I thought this would be easy because translation and rotation are easy. Scaling seems easy at first because is so close to the right answer, and you only realize that it’s wrong when you’re using it for a bunch of cases.
- The answer comes out to this weirdly elegant operation: Take the median of division, multiplication and exponentiation. It’s rare that you end up doing something like this. It’s especially unexpected to find this for such a seemingly simple question as “how to do a partial multiplication.”
One elegant thing that I haven’t mentioned yet is that this also seems to handle cases where the value is bigger than 1. The exponentiation explodes quickly for large values, but since we use the median, the exponentiation doesn’t get used. And the multiplication and division cases behave pretty well for larger values.
And a bonus fun-fact: if you’re doing this in log-space (which I was doing at first) this ends up being the median of addition, subtraction and multiplication. Because and and . This is cheaper since you don’t need to calculate the exponent, but it only makes sense if you’re already in log-space. (also watch out for the pattern-break on the last three of those formulas: That one just uses instead of .
Also: I tried googling to find if somebody else had already discovered this, but it’s nearly impossible to google for the question of “how to do a half a multiplication.” So I have no idea if I discovered a new mathematical operation or if this is already known. If anyone knows, I would be curious to know how I could have found this without having to reinvent it.