🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Finding from a Given Point the Nearest Point on a Squashed Circle

Started by
26 comments, last by Thaumaturge 2 years, 2 months ago

Thaumaturge said:
And yet, what I have does seem to work. :P

Yes, but only as you decide it's good enough for your needs. In general such approximations show increasing error with increasing squashing.
So the narrower the ellipse, the larger the error:

If we would use this within a physics simulation to resolve collisions, the error would add energy to the system making it unstable.
I saw this for example in Bullet Physics, which had an elliptical joint limit, but if the ellipse is narrow the joint just explodes in practice, so the feature was not really useful.
To improve this, they would need to do an iterative search. But if you want to use a small count of iterations, the solution can jump by non continuous amounts, which causes micro jitter.
And with many such joints in a ragdoll, micro jitter sums up and turns into a serious problem in special cases.

So it really depends on your application. If there is no error feedback, you are probably fine with some error.

You probably describe an improved method over my trivial example in my picture, to minimize the error further? Though, that's usually black art which helps in many cases, but may even increase the error in rare cases.
If it's the former, maybe you can make it the inner loop of an iterative search to solve the problem until you hit the limit of machine precision, which would be as good as an analytical solution if it existed. (But probably Eberlys solution would be still faster)

But - if you truly think your method gives an accurate solution without iterations, you should insist and publicate the algorithm. It would change the world a little bit! :D

Advertisement

JoeJ said:
So the narrower the ellipse, the larger the error:

Yes, but as I described above, what you show there isn't what I'm doing (again, save for points above and below the circle).

What I am doing seems to give good results up to at least a scaling factor of 0.4--that is, up to a circle squashed such that it has a width 0.4 that of its unsquashed form.

(I forget whether I've tried it at values below 0.4, although I think that I have.)

JoeJ said:
If we would use this within a physics simulation to resolve collisions, the error would add energy to the system making it unstable.

But I'm not using this for a physics simulation? o_0

Why use a more-complex solution when it doesn't significantly improve matters in the purpose at hand?

JoeJ said:
You probably describe an improved method over my trivial example in my picture, to minimize the error further?

It seems thus far to be so, at least!

JoeJ said:
Though, that's usually black art which helps in many cases, but may even increase the error in rare cases.

Perhaps, but I've not found such issues thus far--save, again, for my falling back to the inaccurate method of simply scaling the player's position.

JoeJ said:
But - if you truly think your method gives an accurate solution without iterations, you should insist and publicate the algorithm. It would change the world a little bit! :D

Oh, I doubt that it's perfectly accurate! But it does seem pretty accurate--accurate enough for my purposes thus far, at least!

(If anything, I'd like to eliminate that matter of falling back to the “scaling” approach--I just haven't yet figured out how to determine the relevant offset distance when below or above the circle…)

MWAHAHAHAHAHAHA!!!

My Twitter Account: @EbornIan

Thaumaturge said:
But I'm not using this for a physics simulation? o_0 Why use a more-complex solution when it doesn't significantly improve matters in the purpose at hand?

You did not mention the usecase? (I may have missed it)
My comments thus are all about the problem in general, and saying something like

JoeJ said:
So it really depends on your application. If there is no error feedback, you are probably fine with some error.

is all i can do to agree with your choice.
So i do agree, but when you say ‘And yet, what I have does seem to work. :P’, then i still need to inform others reading this thread, that there is no such quick solution which is accurate in general.

Thaumaturge said:
(If anything, I'd like to eliminate that matter of falling back to the “scaling” approach--I just haven't yet figured out how to determine the relevant offset distance when below or above the circle…)

This is what i did, as implementig Eberlys paper lacking math background probably becomes a bit hard:

  1. Find the closest point on the circle, project it to the ellipse.
  2. Calculate the ellipse tangent at the intersection point (which is the circle tangent and applying the inverse nonuniform scale). Calculate the error by finding the closest point on the tangent.
    I've added this in green to the drawing. As you see, we're already close. And the method works equally well no matter if our point is in or outside the circle:

3. Project the current closest point to the circle along scaling direction, so we can get a new point on the ellipse, then we can proceed with step 2 to come closer to the true solution.
I've added this in blue:

The error is now so small i could no longer improve given the resolution if the image, so i'm done after two iterations.

I guess this works, and maybe better than your current solution. But not sure without trying…

JoeJ said:
You did not mention the usecase? (I may have missed it)

I didn't, as far as I recall. But I would argue then that there's hence no reason to assume it, or that there are other considerations associated with it (such as avoiding jitter).

(The full specifics would extend the post pointlessly, I feel, but in short I'm aiming to place some colliders dynamically, according to the player's position. There's more between this stage and implementing that, admittedly, but little that's relevant to this discussion, I daresay!)

JoeJ said:
is all i can do to agree with your choice.

That's fair.

JoeJ said:
So i do agree, but when you say ‘And yet, what I have does seem to work. :P’, then i still need to inform others reading this thread, that there is no such quick solution which is accurate in general.

And that's also fair. I suppose that, to me, what you previously said didn't come across that way--it seemed to me less like you were saying “for others who might be reading, note that this isn't a general solution for all use-cases” and more like you were saying “your solution for your use-case doesn't work; here is a solution that does”.

JoeJ said:
This is what i did, as implementig Eberlys paper lacking math background probably becomes a bit hard:

Hmm… That's an interesting solution--thank you for it! ^_^

I might give that one a little more thought tomorrow, then. ^_^

MWAHAHAHAHAHAHA!!!

My Twitter Account: @EbornIan

Thaumaturge said:
and more like you were saying “your solution for your use-case doesn't work; here is a solution that does”.

I didn't mean this, so it's just a misunderstanding.
The jitter example was made to provide just one example to point out why approximations, even if seemingly good enough initially, often give unexpected headaches later. From my experience this happened so often that i assume it affects the majority of applications. But my assumptions were not meant to doubt your specific solution.
Beside that, i always try to generalize problems people bring up, because general solutions is what we truly desire. So even if you had mentioned your application, i would have behaved like this regardless.
Usually i try to get away with cheap approximations and some creativity myself. I think we should try this first and see what we get. :D

JoeJ said:
I didn't mean this, so it's just a misunderstanding.

Yup! And I'm glad that it's cleared up, then. ^_^

JoeJ said:
The jitter example was made to provide just one example to point out why approximations, even if seemingly good enough initially, often give unexpected headaches later. From my experience this happened so often that i assume it affects the majority of applications. But my assumptions were not meant to doubt your specific solution.

Ah, that's fair. For my purpose I think that I can be confident that jitter is unlikely to be an issue, at least.

(I only really need a reasonably-accurate region, when it comes down to it, with sufficient stability close to the player's position.)

JoeJ said:
Beside that, i always try to generalize problems people bring up, because general solutions is what we truly desire.

Hmm… On this I disagree, I think.

General solutions have their advantages, but sometimes a specific solution is better. It may be more performant, or provide better results in its specific use-case, or be easier to maintain, or something else besides.

JoeJ said:
Usually i try to get away with cheap approximations and some creativity myself.

This is perhaps not a bad idea, indeed! An adjunct to the usual advice of avoiding premature optimisation, perhaps. ^_^

MWAHAHAHAHAHAHA!!!

My Twitter Account: @EbornIan

Well, i have covid currently, and feel unable to work. :/ But i felt motivated to work on a interesting problems like this… : )

However, as often it turned out i'm too much of an amateur in math. My idea works well for the very most cases, even on the first guess.
But in some cases, the solver starts to oscillating on a constant amount of positive / negative error.
It would have been much wiser to bite through Eberlys paper. ;D

Still, here's what i got:

It generates many points, which all have the same distance to the solution. It does not converge.
To fix this, i need to set a very small damping constant. Which than needs much more iterations, so it sucks.
I think we could d better by identifying the reason of the oscillation, and treating this differently. But i doubt the outcome could compete Eberlys solution.

However, it's not that bad. Maybe you can use it.

EDIT: improved code
Another improvement, should be final : )

		static bool visClosestPointOnEllipse = 1; ImGui::Checkbox("visClosestPointOnEllipse", &visClosestPointOnEllipse);
		if (visClosestPointOnEllipse)
		{
			// Solution works in local space of ellipse, with the scaling of y to be one.
			// X-scaling must be less than one, otherwise flip your local axis, so this assumption holds

			static float ellipseXScale = 0.2f; ImGui::SliderFloat("ellipseXScale", &ellipseXScale, 0.00001f, 1.0);
			

			{
				// render unit circle
				RenderCircle(1.f, vec2(0), vec(0,0,1), 0.7f,0.7f,0.7f,1, 360);
				// render ellipse
				matrix scaleMatrix = matrix::identity();
				scaleMatrix[0][0] = ellipseXScale;
				simpleVis.LoadModelviewMatrix((float*)&scaleMatrix);
				RenderCircle(1.f, vec2(0), vec(0,0,1), 1,1,1,1, 360);
				simpleVis.SetModelviewMatrixToIdentity();
			}

			static vec2 queryPoint (0.5f, 2.8f); ImGui::DragFloat2("queryPoint", (float*)&queryPoint, 0.01f); // those numbers are a problem case causing oscillation, which i try to fix with damping
			RenderPoint (queryPoint, 1,1,1);
			RenderLabel (queryPoint, 1,1,1, "queryPoint");


			auto IntersectLineWithYAxis = [&](const vec2 &lineOrigin, const vec2 lineDirection)
			{
				if (lineDirection[0] == 0)
					return lineOrigin;
				
				float t = lineOrigin[0] / lineDirection[0];
				return vec2(lineOrigin - t * lineDirection);
			};

			auto ClosestPointOnCircle = [&](const vec2 &point, const vec2 circleOrigin, const float circleRadius)
			{
				vec2 diff = point - circleOrigin;
				return circleOrigin + diff * circleRadius / diff.Length();
			};

			auto ProjectToEllipse = [&](vec2 &point, vec2 &tangent, const bool doTrick)
			{
				if (doTrick) point[1] *= ellipseXScale;
				point.Normalize();
				tangent = vec2 (-point[1], point[0]);
				point[0] *= ellipseXScale;
				tangent[0] *= ellipseXScale;
			};

			// initial guess

			static int iterations = 4; ImGui::SliderInt("iterations", &iterations,1,100);
			//static float damping = 1.0f; ImGui::DragFloat("damping", &damping, 0.01f);
			//static float testCompensation = 0.5f; ImGui::DragFloat("testCompensation", &testCompensation, 0.01f);
			//static bool doTrickInit = 0; ImGui::Checkbox("doTrickInit", &doTrickInit);
			//static bool doTrickIter = 1; ImGui::Checkbox("doTrickIter", &doTrickIter);

			vec2 ellipseTangent;
			vec2 ellipsePoint = queryPoint.Unit();
			ProjectToEllipse(ellipsePoint, ellipseTangent, false);

			RenderPoint (ellipsePoint, 1,1,0);
			RenderLabel (ellipsePoint, 1,1,0, "naive initial guess");
			if (iterations==1)
			{
				RenderVector (ellipsePoint, ellipseTangent, 1,1,0);
				RenderVector (ellipsePoint, -ellipseTangent, 1,1,0);
			}

			//float clipXmin, clipXmax;
			//if (queryPoint[0] >= 0) 
			//	{ clipXmin = 0; clipXmax = 1; }
			//else
			//	{ clipXmin = -1; clipXmax = 0; }

			for (int i=0; i<iterations; i++)
			{
#if 1
				// new and good method - construct ellipse inner circle and find closest point on that
				{
					vec2 ellipseNormal (-ellipseTangent[1], ellipseTangent[0]);
					//ellipseNormal.Normalize(); // not needed
					vec2 circleOrigin = IntersectLineWithYAxis (ellipsePoint, ellipseNormal);
					float circleRadius = vec2(ellipsePoint - circleOrigin).Length();

					RenderVector(ellipsePoint, ellipseNormal, 1,0,1);
					RenderCircle(circleRadius, circleOrigin, vec(0,0,1), 1,0,1);

					ellipsePoint = ClosestPointOnCircle (queryPoint, circleOrigin, circleRadius);
				}
#else				
				// old and bad method - get error from projection to tangent
				{
					//float compensationFactor = fabs(ellipsePoint[0]); // still wobbles
					//float compensationFactor = ellipsePoint[0] * ellipsePoint[0]; // needs too many iterations
					float compensationFactor = (fabs(ellipsePoint[0]) + ellipsePoint[0] * ellipsePoint[0]) / 2; // average seems fine
					float error = ellipseTangent.Dot(queryPoint - ellipsePoint) / ellipseTangent.SqL(); // SqL gives squaerd magnitude
					ellipsePoint += ellipseTangent * error * compensationFactor;// * damping;
				}
#endif

				ProjectToEllipse(ellipsePoint, ellipseTangent, true);
				
				// vis
				{
					float t = float(i) / float(iterations)*0.66f;
					float R = Vis::RainbowR(t);
					float G = Vis::RainbowG(t);
					float B = Vis::RainbowB(t);
					//ImGui::TextColored(ImVec4(R,G,B,1), "err %f", error * ellipseTangent.Length());
					RenderPoint (ellipsePoint, R,G,B);
					//RenderLabel (ellipsePoint, R,G,B, "%i", i);
					//RenderVector (ellipsePoint, ellipseTangent, R,G,B);
					//RenderVector (ellipsePoint, -ellipseTangent, R,G,B);
				}
			}

			RenderLine (ellipsePoint, queryPoint, 1,1,1);
	
		}

JoeJ said:
Well, i have covid currently, and feel unable to work.

Ah, I'm sorry to read it! I hope that you recover swiftly!

JoeJ said:
But i felt motivated to work on a interesting problems like this… : )

Well, then I'm glad to have so provided a problem! ^_^

JoeJ said:
But in some cases, the solver starts to oscillating on a constant amount of positive / negative error.

Ah, that's a pity!

And honestly, I think that I'd prefer against iterative approaches, myself, unless no better solution comes to me.

I think that, if I want to improve on what I have, I might try to focus on coming up with a better way to handle my “above and below” cases in my own approach--if I can just find a way of determining a usable distance in those, then I think that I should be able to just use my approach in all cases!

Nevertheless, thank you for the code! If I do end up deciding to go with your approach then it might prove handy! ^_^

MWAHAHAHAHAHAHA!!!

My Twitter Account: @EbornIan

Thaumaturge said:
And honestly, I think that I'd prefer against iterative approaches, myself, unless no better solution comes to me.

Yes. But that's because we are too stupid to implement proper iterative solutions :D

Mine is still good with setting iterations to one. So it's a nice approximation, but can't extend to a robust iterative solutions.
That's what mostly happens if i try something like this. Mostly i can fix it by combining two different methods with differing strengths and weaknesses.
But i still hate it, and try to work around… ; )

Thaumaturge said:
Well, then I'm glad to have so provided a problem! ^_^

Yeah, i see i'm pretty fit already again. Yesterday it was really bad, and the test did not yet confirm it at all. Today the test was positive, and i feel much better already.
It was the same for my wife, but then some other issues like hard to breath are still in front of me. And i'm not vaxed, unlike her.
Wife is still positive and feels bad, but she has to work regardless, servicing breakfast to 60 people. That's our law. First they force us to vax, so i'm actually breaking the law, then they force us to spread.

I tell you - my trust in govs expertise is as big as in tech mega corps. :/

I had an idea to improve it with a better initial guess. Edited the code. Much better, but the problem remains in cases, but it's smaller.

This topic is closed to new replies.

Advertisement