Getting a direction constant from normalized vector

Started by
15 comments, last by aggieblue92 10 years, 1 month ago

I'd have to make a pull for using the dot product too: if you take the dot product of all the direction vectors, the one that has the largest value is the direction you want to take. This also has an advantage of being scaleable, you could modify it easily to only support N/E/S/W, and also could easily support N/NNE/ENE/NE/...


	public static float DotProduct(Point2D.Float p1, Point2D.Float p2)
        {
               return (p1.x * p2.x + p1.y * p2.y);
        }

public static Direction getDirection(Point2D.Float normalizedPoint)
	{
		int x = Math.round(normalizedPoint.x);
		int y = Math.round(normalizedPoint.y);

                // Order of directions: N, NE, E, SE, S, SW, W, NW
                int maxDir = 0;
                float maxScalarProduct = DotProduct(normalizedPoint, Point2D(0.0, 1.0)); // Check NORTH

                final int NUM_DIRECTIONS = 8; // Number of directions to look for
                final float DELTA = 2.0 * 3.141592654 / NUM_DIRECTIONS; // Change in radians from one direction to another

                // Go through other directions, look for largest
                float angle = DELTA;
                for(int i = 1; i < NUM_DIRECTIONS; i++)
                {
                    if(DotProduct(normalizedPoint, Point2D(Math.sin(angle), Math.cos(angle)) > maxScalarProduct)
                    {
                       maxDir = i;
                       maxScalarProduct = DotProduct(normalizedPoint, Point2D(Math.sin(angle), Math.cos(angle))
                    }
                }
                // NOTE: It would be a LOT faster if this code is a critical point (which it probably is)
                //  to instead or using Point2D(Math.sin(angle), Math.cos(angle)), make an array like
                //  xAngles = {Math.sin(0), Math.sin(45 degrees), Math.sin(90 degrees)...} from values you find yourself
                //  and then load them instead. Computing the sine and cosine is expensive and unnecessary.
		
                switch(maxDir)
                {
                    case 0: return Direction.N; break;
                    case 1: return Direction.NE; break;
                    // ...
                }	
		return null;
	} 
Advertisement

I'm very interested in the OP's meaning of simplicity … hopefully s/he will give a feedback in this thread. In the meanwhile, I wanted to hint at quirks in the answers above, because some of the answers are misleading. Please don't feel offended; I simply think the OP should get reasoned answers.

@Waterlimon: Comparing the effort of a single dot-product with a trigonometric function is unfair because an entire max search for N directions using the dot-product in 2D requires N*2 MULs, N ADDs, and N CMPs. This means there are 16 MULs and 8 ADDs to be compared with 1 trig function (when using the max search in both cases). Your wording lets one assume that there are 2 MULs and 1 ADD to be compared to N trig. functions. Nonetheless, your method may still be faster for the given use case though ...

@papalazaru: Is it really simply if one needs to stare 2 minutes onto 5 lines of code until understanding it? Is it really simple if you use the word "quadrant" although you compute 6 and 2 sections, resp.? Is it really simple if you need 12 elements in the mapping, although 8 are expected? And why do you use 30° and 60° as limits, which causes obviously not an equal quantization w.r.t. the angle, without any textual hint about that? And what is the check (x >= 0.0f) good for?

@aggieblue92: Please tell me why using the dot-product has the advantage of being scaleable; compared to what other method? Each additional resolution step doubles the amount of MULs and ADDs, while using the atan2 function always costs the same (because exactly 1 call is needed). Moreover, your code snippet runs a loop, and inside the loop you are computing the sine and the cosine in dependence on the loop argument (okay, you actually do not, but that is an error in your code; intentionally you use an altering angle argument). This is hardly an optimization, isn't it?

@papalazaru: Is it really simply if one needs to stare 2 minutes onto 5 lines of code until understanding it? Is it really simple if you use the word "quadrant" although you compute 6 and 2 sections, resp.? Is it really simple if you need 12 elements in the mapping, although 8 are expected? And why do you use 30° and 60° as limits, which causes obviously not an equal quantization w.r.t. the angle, without any textual hint about that? And what is the check (x >= 0.0f) good for?

If you can't read code, then I can't help you.

Everything is better with Metal.


If you can't read code, then I can't help you.

Is this seriously your answer? There is needless code in your solution, there is a misleading variable name, and there is for no obvious reason a non equidistant quantization. (<sarcasm>Don't know how I've recognized this from your code without being able to read it …</sarcasm>) The OP seems to have problems to understand, but you save any explanation. Well, I personally don't need your help, it's the OP and not me who has asked for help. I simply want to know your reasoning where your solution is better (be it more correct, more performant, or simpler) compared to what the OP has already. That's all.

@papalazaru: Is it really simply if one needs to stare 2 minutes onto 5 lines of code until understanding it? Is it really simple if you use the word "quadrant" although you compute 6 and 2 sections, resp.? Is it really simple if you need 12 elements in the mapping, although 8 are expected? And why do you use 30° and 60° as limits, which causes obviously not an equal quantization w.r.t. the angle, without any textual hint about that? And what is the check (x >= 0.0f) good for?

If you can't read code, then I can't help you.

<off-topic>

You just reminded me of the joke:

- Hey! How was that course you were taking about constructive criticism?

- I didn't need it. It was crap.

</off-topic>

Anyway, your version is pretty much C0lumbo's with a different coding style and without splitting the circle evenly. Your code considers E, N, W and S to have a range of 60 degrees range each, and NE, NW, SE and NW to have a range of 30 degrees each. For example, you consider 30º to be East, when the closer direction is actually North-East (which is only 15º away instead of 30º).

It might be fine to the OP, though.

EDIT: By the way, just curious, do you really read your code better than this:?


const double rad_22_5  = Math.PI/8;
const double rad_67_5  = 3 * degrees_22_5;
const double rad_112_5 = 5 * degrees_22_5;
const double rad_157_5 = 7 * degrees_22_5;

double angle = Math.Atan2(y, x);

if (angle > -rad_157_5 && angle <= -rad_112_5) return Direction.SW;
if (angle > -rad_112_5 && angle <= -rad_67_5 ) return Direction.S;
if (angle > -rad_67_5  && angle <= -rad_22_5 ) return Direction.SE;
if (angle > -rad_22_5  && angle <= +rad_22_5 ) return Direction.E;
if (angle > +rad_22_5  && angle <= +rad_67_5 ) return Direction.NE;
if (angle > +rad_67_5  && angle <= +rad_112_5) return Direction.N;
if (angle > +rad_112_5 && angle <= +rad_157_5) return Direction.NW;
/* otherwise */                                return Direction.W;

Of course yours doesn't require the Atan2, but come on, we are programming in C#, inside a game, unless you call this function an absurdly amount of times the performance loss due to the Atan2 is going to be neglible.

“We should forget about small efficiencies, say about 97% of the time; premature optimization is the root of all evil” - Donald E. Knuth, Structured Programming with go to Statements

"First you learn the value of abstraction, then you learn the cost of abstraction, then you're ready to engineer" - Ken Beck, Twitter

Thanks all. C0lumbos code works fine and is most likely the fastest and have highest readability.

My math skills are very limited so I do not understand many terms and such used here, but I trying :)

@haegarr

I'm very interested in the OP's meaning of simplicity … hopefully s/he will give a feedback in this thread. In the meanwhile, I wanted to hint at quirks in the answers above, because some of the answers are misleading. Please don't feel offended; I simply think the OP should get reasoned answers.

@aggieblue92: Please tell me why using the dot-product has the advantage of being scaleable; compared to what other method? Each additional resolution step doubles the amount of MULs and ADDs, while using the atan2 function always costs the same (because exactly 1 call is needed). Moreover, your code snippet runs a loop, and inside the loop you are computing the sine and the cosine in dependence on the loop argument (okay, you actually do not, but that is an error in your code; intentionally you use an altering angle argument). This is hardly an optimization, isn't it?

TL;DR version - dot product definitely isn't the best method, but I like them conceptually. It's just another way of doing things, and the entire point of forums is for people to throw out their way of doing something, for the OP to then consider among the others.

Anyways.

Using the dot product is scaleable: As opposed to using inverse tangents (for sufficiently cases at least), really it's not any better. The whole looping thing does cause a worse complexity than the atan2 function probably is. The atan2 check is O(1), and my check is O(n), perhaps O(log(n)) if you added some crazy optimizations that would ruin the scaleability of the code anyways. The dot product method, should it have any advantage, would only be advantageous for a small set, like perhaps the likely 2-8 directions that the whole idea of discrete directional units like this calls for. I don't know the actual runtime of atan2, and I'm aware that it doesn't run a full Taylor series, but it still has to be more than a few multiplications and additions.

You also asked about scaleable - should the programmer decide they wanted to use more directions, say 9 or 12 or (more likely) 16, it would simply be a matter of finding more sines and cosines (pre-run time of course) and adding a few more cases to the select statement, and updating the constant NUM_DIRECTIONS. This is usually my practice, as I tend to like avoiding highly specific solutions unless I know I'm deep into development of an end product, usually not a condition I'm under when I'm on the forums. If you were doing an atan2, sure, you could write a bunch of if/else statements, maybe even do some clever division tricks, and get the same result. The other fun thing I was thinking about was the conversion from 2D to 3D mentality - this same type of algorithm applies quite nicely to doing the same thing in three (or more) dimensions. Which is totally against what I said in the first paragraph, I know.

Also yes, about the sines and cosines in the loop. That was for a display of what numbers the computer would use, and again, upon completion of the algorithm, would be replaced with an array found run-time. I mentioned that very explicitly in the comments below. Probably should have been above, I'll own up to that. This function, again, will work fantastically in the debug phase, and then could be easily modified and optimized in preparation for any releases.

Really, using the dot product is probably not the best way. I was just throwing out my suggestion, which the OP read and decided they preferred somebody else's solution anyways. I really like the dot product when doing anything with vectors, 2D or 3D, especially because of how well they translate between the two. Conceptually, I also find them a really powerful tool and taking advantage of vector mathematics.

Anywho. Don't mean to start any flame wars, just out here to contribute where I can. Constructively. Possibly, but not necessarily, helpfully.

This topic is closed to new replies.

Advertisement