This is an algorithm I came up with for determining the surface normal on a 2d collision mask array for one of my projects. It can flexibly determine the slope inside of a sprite- without any predefined vectors, polygons, etc.


The "standard" way of doing this- comparing the image on a per-pixel basis in an array- is slower, less accurate, and a more difficult implementation.


Finding the surface normal is important for many things. For example, if you want to have a bouncing object reflect off a surface in 2d, you need to know the angle of the surface. If you want to have an object that moves along the perimeter of a surface- you need to know the angle at any given point.


This algorithm works by finding the points of intersection of a circle with the collision mask in an optimized fashion, then finding the angle between those points- approximating the normal angle at your given point. For example, given an image that looks like this, and the point highlighted in red:

Image


You would find the green points where the circle intersects:


Image

Taking the angle between those two points, and adding 90 degrees, you have the normal angle to the surface at that point! The way you find these green points is by rotating and checking collision mask points along the circle. The more degrees you move between points, the faster your algorithm will operate in the average case, but the higher your error will be.

Start by checking points at:
Previous Angle - 90
&
Previous Angle + 90
With a "left" and "right" detector. For one, if it is overlapping the backdrop, increment it until it is no longer overlapping the backdrop. If it was not overlapping the backdrop, decrement it until it is- and then take the last angle used (add one to the return value.)


An example of this in lua:
function correctAngle(x,y,angle)

angleless = angle;
anglemore = angle + 180;
dist = distchecker;
testX = x + math.cos(math.rad(angleless)) * dist;
testY = y - math.sin(math.rad(angleless)) * dist;
truth = mmf.Collisions.BackdropTestPoint(testX,testY,3);
if truth then
angleless = underAngle(x,y,angleless,dist,4);
else
angleless = overAngle(x,y,angleless,dist,-4);
end
testX = x + math.cos(math.rad(anglemore)) * dist;
testY = y - math.sin(math.rad(anglemore)) * dist;
truth = mmf.Collisions.BackdropTestPoint(testX,testY,3);
if truth then
anglemore = underAngle(x,y,anglemore,dist,-4);
else
anglemore = overAngle(x,y,anglemore,dist,4);
end
xl = x + math.cos(math.rad(angleless)) * dist;
yl = y - math.sin(math.rad(angleless)) * dist;
xr = x + math.cos(math.rad(anglemore)) * dist;
yr = y - math.sin(math.rad(anglemore)) * dist;
dx = xr - xl;
dy = yr - yl;
res = 180 - math.deg(math.atan2(dy,dx));
return res;
end

function overAngle(x,y,angle,dist,diff)
for i=0,45 do
testX = x + math.cos(math.rad(angle)) * dist;
testY = y - math.sin(math.rad(angle)) * dist;
truth = mmf.Collisions.BackdropTestPoint(testX,testY,3);
if truth then
return angle - diff;
else
angle = angle + diff;
end
end
return angle;
end

function underAngle(x,y,angle,dist,diff)
for i=0,45 do
testX = x + math.cos(math.rad(angle)) * dist;
testY = y - math.sin(math.rad(angle)) * dist;
truth = mmf.Collisions.BackdropTestPoint(testX,testY,3);
if truth then
angle = angle + diff;
else
return angle;
end
end
return angle;
end



I wish there was an easier way to say the algorithm than the code above! Once you get an understanding of what its doing, its not hard at all to do! And it has a very good average running time, a low enough mean case, etc. You can personalize the code to run better by adding exponentially increasing increments- this allows your objects to easily detect small adjustments to the slope, while not taking long on the worst case scenarios (going over a 90 degree cliff)