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);
}
}