[SOLVED] [LibGDX] Curved Path Constant Speed Problem with End Points

Hi all, I’m trying to move an image at a constant speed over a curved path in LibGDX. I calculated the average speed needed to travel the curve by taking the derivative at various points along the curve and averaging them. Then I multiply the path’s position (t) by a ratio of the average speed and the speed at the current location of the curve. This method for setting constant speed works great.

The problem I’m having occurs when multiple control points (3 or more) are put in the same location. Then the speed at this point is 0 (or close to 0) and dividing the average speed by a speed of 0 causes problems in the calculations.

BSpline requires three control points to be placed at the ends in order to have the curve actually reach the start and end at the end points. If I only put 1 or 2 control points at the ends the path starts after the first control point and ends before the last control point. For my application it is important that the motion reaches the end points because I will be linking together multiple BSplines and it’s important for them to line up correctly and to not have any time gaps between them either.

I’ve tried several different attempts at moving over the zero derivative area, but none of them were successful. I also asked on stackoverflow and there’s been some discussion in the comments but it hasn’t been solved yet.

Here is a short sample program and I’ve included comments to indicate where the problem is. Any help getting this figured out would be greatly appreciated. Thanks.

NOTE: I used CatmullRomSpline in my example instead of BSpline only because I found a bug in the BSpline’s derivative method, which has been fixed but is not yet in the stable version of LibGDX.

Test.java

public class Test extends Game {
	private Stage stage;
	private MyPath path;
	private Array<Texture> textures = new Array<Texture>(Texture.class);
	private Array<Image> images = new Array<Image>(Image.class);
	
	@Override
	public void create () {
		Gdx.graphics.setDisplayMode(1000, 1000, false);
		stage = new Stage();
		stage.setViewport(new ScreenViewport(stage.getViewport().getCamera()));
		Gdx.input.setInputProcessor(stage);
		createImages();
		path = new MyPath(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), images);
		stage.addActor(path);
	}	
	@Override
	public void render () {
		Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		stage.act(Gdx.graphics.getDeltaTime());
		stage.draw();
	}
	
	// IMAGES
	private void createImages(){
		images.add(getImage(Color.GREEN, true));
		for (int i=0; i<MyPath.pointsData.length; i++){
			images.add(getImage(Color.WHITE, false));
		}
	}
	private Image getImage(Color color, boolean fillCircle){
		Pixmap pixmap = new Pixmap(50, 50, Pixmap.Format.RGBA8888);
		pixmap.setColor(color);
		if (fillCircle){
			pixmap.fillCircle(pixmap.getWidth()/2, pixmap.getHeight()/2, pixmap.getWidth()/2-1);
		} else {
			pixmap.drawCircle(pixmap.getWidth()/2, pixmap.getHeight()/2, pixmap.getWidth()/2-1);
		}
		textures.add(new Texture(pixmap));
		pixmap.dispose();
		return new Image(textures.peek());
	}
	@Override
	public void dispose(){
		while (textures.size > 0){
			textures.pop().dispose();
		}
		stage.dispose();
		super.dispose();
	}
}

MyPath.java

public class MyPath extends WidgetGroup {
	private Path<Vector2> path;
	private Vector2 result=new Vector2(), derivative=new Vector2();
	private float t, tPrev, dt, tConst, tConstPrev, dist, pathLength, speedAverage;
	private Image dot;
	
	private float speed = 1500 * 1000;
	
	public static final Vector2[] pointsData = {
		new Vector2(100, 100),
		new Vector2(100, 100),
//		new Vector2(100, 100),	// << UN-COMMENT TO PRODUCE BUG
		
		new Vector2(350, 800),
		new Vector2(550, 200),
		new Vector2(650, 400),
		new Vector2(900, 100),
		new Vector2(900, 100)
	};
	
	public MyPath(int width, int height, Array<Image> images){
		this.setSize(width, height);
		path = new CatmullRomSpline<Vector2>(pointsData, false);
		pathLength_SpeedAverage();
		addImages(images);
	}
	
	@Override
	public void act(float delta){
		result = getValue(delta);
		dot.setPosition(result.x - dot.getWidth()/2, result.y - dot.getHeight()/2);		
	}
	private Vector2 getValue(float delta){		
		// set t in the range [0,1] for path
		dist += speed * delta;
		if (dist > pathLength){
			dist = tPrev = tConst = tConstPrev = 0;
		}
		t = dist / pathLength;
				
		// CONSTANT SPEED
		dt = t - tPrev;
		path.derivativeAt(derivative, tConstPrev);
		tConst += dt * (speedAverage / derivative.len());  // << ERROR when derivative.len() is 0
		path.valueAt(result, tConst);
		
		tPrev = t;
		tConstPrev = tConst;
		return result;
	}
	
    private void pathLength_SpeedAverage(){
		float segmentCount = 20000;
		pathLength = 0;
		for (float i=0; i<=1; i+=1.0/segmentCount) {	
			path.derivativeAt(result, i);
			pathLength += result.len();
		}
		speedAverage = pathLength / segmentCount;
	}
	
    private void addImages(Array<Image> images){
		dot = images.items[0];
    	for (int i=1; i<images.size-1; i++){
    		images.items[i].setPosition(pointsData[i].x - images.items[i].getWidth()/2, pointsData[i].y - images.items[i].getHeight()/2);
			addActor(images.items[i]);
		}
		addActor(dot);
    }
}

When the curve is degenerate, you can’t get a derivative. Turns out that existence stuff in calculus is important. In graphics i constantly find myself simply working around all the possible degenerate cases. For almost exactly this problem, i would replace curves with a non degenerate curve that has the same path but a different form in t. This worked fairly well if it is not a common problem. Also i could split a single spline into more than one to keep derivatives stable/accurate.

Thanks for the response delt0r.

I’m not sure what you mean by “i would replace curves with a non degenerate curve that has the same path but a different form in t”, can you elaborate on that?

BSplines will work really well in my situation because I’m going to be creating complex paths which would be hard to piece together myself from individual curves. BSplines handle all the math and makes sure the individual Bezier curves join together smoothly. So if I can figure out a way to smoothly step over the zero (or near zero) derivatives at the end points then everything else will work great.

I think i miss understood your problem a little.

By degenerate i mean in fact having a derivative of zero length. You either need to catch this special case, or you need to never let it arise (make == control points invalid). Or play with the equations and interpret the new curve properly (its probably now got a a zero on some of the coefficients).

Another option, the one i used, is that i would integrate for s not the derivative with respect to t. Since i don’t care about its “velocity” as t is just a parameterization. ie it is not real velocity.

Yeah, I’m trying to figure out how to handle the special case and this has proven to be rather difficult.

The BSpline requires 3 control points at the end points in order to pull the curve all the way to the first control point. With only one control point at the ends the path actually starts after the first control point and ends before the last control point. Having multiple control points in the same location is also helpful for shaping the curve (making it a sharper curve at a particular point). So if I could allow multiple control points and then somehow step over that point smoothly while moving along the path, that would be perfect.

I have tried a lot of different methods to move beyond that point and this one theoretically seems like it has potential.

To summarize, I store the previous location value (in the vector2 “lastPoint”) and then check to see if it the position has moved at all from the last frame. The position should change every frame so if the position hasn’t changed then it’s encountered a zero derivative. When it encounters a zero derivative, I keep increasing the curve parameter t and going through all the position calculations again in a loop until the position has changed, then it escapes the loop once the distance is greater than an allowed minimum speed (minSpeed = 1) and the derivative is greater than 1.

private Vector2 getValue(float delta){
	do {
		// set t in the range [0,1] for path
		dist += speed * delta;
		if (dist > pathLength){
			dist = tPrev = tConst = tConstPrev = 0;
			lastPoint.set(0,0);
		}
		t = dist / pathLength;
		
		// CONSTANT SPEED
		dt = t - tPrev;
		path.derivativeAt(derivative, tConstPrev);
		tConst += dt * (speedAverage / derivative.len());  // << ERROR when derivative.len() is 0
		path.valueAt(result, tConst);
	}
	while (result.dst(lastPoint)<minSpeed || derivative.len()<1);
	
	lastPoint.set(result);	
	tPrev = t;
	tConstPrev = tConst;
	
	return result;
}

I did the same thing where I calculate the average speed to keep it from including zero derivatives in the average speed.

private void pathLength_SpeedAverage(){
	float segmentCount = 20000;
	pathLength = 0;
	
	path.valueAt(lastPoint, 0);
	for (float i=0; i<=1; i+=1.0/segmentCount){
		path.valueAt(result, i);
		if (result.dst(lastPoint) >= minSpeed){
			path.derivativeAt(result, i);
			if (result.len()>1){
				pathLength += result.len();
				
				lastPoint.set(result);
			}
		}
	}
	speedAverage = pathLength / segmentCount;
	lastPoint.set(0,0);
}

That’s where I’m at so far with this method. If the zero derivative occurs at the first control point then the program just locks up. If the zero derivative is moved towards the center of the curve then it occasionally works (moving past the zero derivative) on some passes through the curve and on others it glitches.

I have looked into the arc-length parameterization methods (or parametarizing by “s”) and most of what I’ve found involves an insane amount of math (it’s been a while since calculus lol) and I don’t really understand the pseudo code provided. This pdf seems to pop up in pretty much every thread talking about this topic.


The simplest one I’ve seen uses a lookup table to store arc length values along the curve and then searches the table to find the closest arc length and then grabs the next one and guesses at a value in between them. But I’m not really a fan of this method.

It seems like I’m really close with my current method because the constant speed part is working perfectly. It’s just this one special case that I need to get figured out. So if there is a way to manipulate t in order to step over any zero (or near zero) derivatives, then that should solve my problem. :slight_smile:

The math is only insane if you care. If you don’t a simple Gaussian quadrature method can integrate for s given t quite accurately. I promised moons ago to post my spline code. But i never did. I will see where it is and if its practical to cut it down to something useful for others.

However your degenerate case could still exist here. Parametric curves generally require that x=f(t),y=g(t) be monotonic in t. that is dx/dt !=0, dy/dt !=0 anywhere.

Basically having a control point on top of each other turns your curve into a quadratic or something. This is why i would break up some of my curves.

I think that’s the problem I’m having with researching this stuff because even your “simple solution” of Gaussian quadrature method…I had to google that. lol So it might not be insane for those who have kept up with their higher level math skills, but if you haven’t kept up with it then it is kind of insane when you don’t grasp the simple explanations.

As long as I don’t try to force the speed to be constant, then the path motion works without glitching or locking up…but obviously the speed varies over the curve. So it seems like there should be a way to unitize the fact that it’s possible to move over the zero derivative without glitching and when it does start moving make that speed constant.

This method removes all constant speed code.

private Vector2 getValue(float delta){
	// set t in the range [0,1] for path
	dist += speed * delta;
	if (dist > pathLength){
		dist = tPrev = 0;
	}
	t = dist / pathLength;
	
	path.valueAt(result, t);
	
	tPrev = t;
	return result;
}

So I guess what I’m trying to do is:
IF dot is moving THEN make speed constant
IF dot is not moving (zero or close to zero derivative) THEN skip to a frame when it is moving and make that speed constant

I know it’s kind of a odd problem to explain, but hopefully that makes sense.

I’m also not sure how to go about breaking the curves up. What kind of curves would you use if not BSpline/Bezier curves and how would you line them back up so they flow smoothly one into the other? It seems like that would be even more difficult to pull off.

UPDATE

I tried using the lookup table method and got it working. :slight_smile: I was worried I’d have to store thousands and thousands of values in order for it to be precise, but turns out that 100 looks pretty perfect. That really is a great method. I currently have a slight freeze for a fraction of a second at the very beginning, but hopefully I’ll be able to figure that out. I’ve been stuck on this for a while, so I’m glad to be done with this bug! :smiley:

Oh sorry i didn’t’ even post the code in the end. Also for some reason i missed your reply.

I really feel far to few people in programming take math seriously enough. I had to learn 3d rotation matrices just to write demos when i was a young fella… Remember demos. Dammit i am old.

Also don’t get too caught up in details. Gaussian Quadrature integration is a GREAT example of lots of details you can just totally ignore.

To integrate a function f(x) between 0 and 1 (for example) for fourth order i have

af(x1)+bf(x2)+cf(x3)+df(x4)

I evaluate the function at points x1…x4 and use weights a,b,c,d. I just look those numbers up in the internet and i am done. It is very accurate and you don’t need to care why. You don’t need to care how to find the points or the weights. Just look em up.

Did you still want my example code?

That’s alright, no worries! :slight_smile:

I did have high level math in college because I minored in calculus-based physics, but it’s just been a while since I’ve heard the terms and had to apply this stuff. If you don’t keep on top of it, it slips away. lol

I am really happy with the look-up table method, but if you’d like to share your example code that would be great too. Thanks! :slight_smile:

if you’re looking for constant-speed on a curve with segments of random length, a centripetal to chordal catmul rom curve works really good : https://en.wikipedia.org/wiki/Centripetal_Catmull–Rom_spline