I would use “Map” instead of “ReMap” to be consistent with other tools and use the argument names inputFrom, inputTo, outputFrom, outputTo, to be more clear.
The algebra is the same in the two methods, they just use different names and Raza shows his work step by step. Basically, start by normalizing your position in the first vector and then multiply the normalized position by the second vector and finally add the second vector’s starting position.
test code
using System;
public class Program
{
public static void Main()
{
var r = new Random();
for (var i = 0; i < 100; i ++)
{
var x1 = r.Next(int.MinValue, int.MaxValue);
var x2 = r.Next(int.MinValue, int.MaxValue);
var y1 = r.Next(int.MinValue, int.MaxValue);
var y2 = r.Next(int.MinValue, int.MaxValue);
var p = r.Next(int.MinValue, int.MaxValue);
var jessy = JessyMap(p, x1, x2, y1, y2);
var razza = RazzaMap(p, x1, x2, y1, y2);
if(jessy == razza)
Console.WriteLine($"Same result for arguments {p}, {x1}, {x2}, {y1}, {y2}");
else
Console.WriteLine($"Bad result for arguments {p}, {x1}, {x2}, {y1}, {y2}");
}
}
public static float JessyMap (float value, float from1, float to1, float from2, float to2) {
return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
}
public static float RazzaMap (float from, float fromMin, float fromMax, float toMin, float toMax)
{
var fromAbs = from - fromMin;
var fromMaxAbs = fromMax - fromMin;
var normal = fromAbs / fromMaxAbs;
var toMaxAbs = toMax - toMin;
var toAbs = toMaxAbs * normal;
var to = toAbs + toMin;
return to;
}
}
Here’s another way of looking at the same problem. Imagine drawing both ranges on a number line but then rotating the second range to be perpendicular, so you have two number lines: an x an y dimension.
You might be able to see that in order to translate from one range to the other, we can use a linear equation:
y = mx + c
where:
m = the slope of the line (the ratio of range2 / range1)
c = the y intercept of the line (the difference between where range2 and range1 start, multiplied by the slope)
y = dependent variable
x = independent variable
public static float Map (this float x, float x1, float x2, float y1, float y2)
{
var m = (y2 - y1) / (x2 - x1);
var c = y1 - m * x1; // point of interest: c is also equal to y2 - m * x2, though float math might lead to slightly different results.
return m * x + c;
}
Jessy’s code totally confused me until i realised that what he meant by ‘from’ and ‘to’ were totally non-intuitive to me. I thought it meant ‘range we’re mapping from’ and ‘range we’re mapping to’.
but actually we’re using ‘from1…to1’ to ‘from2…to2’, instead of ‘from1…from2’ to ‘to1…to2’.
Just in case someone else might have come here to save thinking time and made a mistake from it, since this result comes up first on google.
Overloaded version with clamping on the input and output values. The original one has no clamping on anything, so you can input and output values outside the ranges (may not be what you want).
// Full version, clamping settable on all 4 range elements (in1, in2, out1, out2)
public static float remap(float val, float in1, float in2, float out1, float out2,
bool in1Clamped, bool in2Clamped, bool out1Clamped, bool out2Clamped)
{
if (in1Clamped == true && val < in1) val = in1;
if (in2Clamped == true && val > in2) val = in2;
float result = out1 + (val - in1) * (out2 - out1) / (in2 - in1);
if (out1Clamped == true && result < out1) result = out1;
if (out2Clamped == true && result > out2) result = out2;
return result;
}
I originally used names like “low” and “high” as well, but I found it unintuitive, because the ranges can be reversed. (Though my “clamped” version disallows reversed ranges.)
+1000 for adding at least a basic version to Mathf…
Right, generally when you want a clamped version, it’s usually best to just clamp the normalized value. The normalized value is always in the range 0 to +1, so clamping there is trivial. For example imagine you want to map the range
“20 to 10” and your input value is “12”. It means we subtract the “first” bounds and divide by the difference. So we get
(12-20) / (10-20) ---> (-8) / (-10) ---> 0.8
So clamping the normalized value between 0 and 1 would give you the right result. That means if you want to write a single method that does a remap that clamps, you can simply do
It doesn’t really make much sense to clamp the inputs / outputs seperately as clamping one would automatically clamp the other as well since we make a mapping between those ranges. So if you clamp the input to stay in the input range, the output will also be in the output range since that’s the definition of our remap function.
In my second version we could set “t” to 1 when it’s larger than 1. However since we know that the final equation will evaluate to, we can directly return the correct result. This is also better for numerical stability. At dodgy range values, clamping t to 1 may not yield “aOut2” because even aOut1 and (-aOut1) mathematically cancel each other, due to floating point rounding issues the result may be slightly off. So directly returning the proper upper / lower bounds is not only faster, but also more stable. So such an expression would 100% of the time evaluate to true if we run into the upper bounds of the output range
float val = 7;
if (val.RemapClamped(20,10, 5, 200) == 200)
likewise this would also work reliably
float val = 21;
if (val.RemapClamped(20,10, 5, 200) == 5)
Agreed, and that’s what I do, including some list-based methods like randomize. But at some point it would just be nice to have them in the library…several other languages (Processing, Max/MSP) do, so they’re very straightforward to use. Also just having them readily available means you’re aware of them, maybe before realizing you even need them