Prevent camera inversion on Y

I currently have the following code for the rotate camera handler:

[java]
protected void rotateCamera(float value, float tpf, Vector3f axis)
{
float rotationSpeed = 1000;

	Matrix3f mat = new Matrix3f();
	mat.fromAngleNormalAxis(rotationSpeed * value * tpf, axis);

	Vector3f upDir = cam.getUp();
	Vector3f leftDir = cam.getLeft();
	Vector3f dir = cam.getDirection();

	mat.mult(upDir, upDir);
	mat.mult(leftDir, leftDir);
	mat.mult(dir, dir);

	Quaternion q = new Quaternion();
	q.fromAxes(leftDir, upDir, dir);
	q.normalizeLocal();
	sa.getCamera().setAxes(q);

	dir = cam.getDirection();
	dir = dir.mult(dist);
	Vector3f invDir = new Vector3f(-dir.x, -dir.y, -dir.z);
	Vector3f position = invDir.add(ctl.getWorldTranslation());

	position.y = FastMath.clamp(position.y, ctl.getWorldTranslation().y - 1, ctl.getWorldTranslation().y + dist + 1);
			
	cam.setLocation(position);
	cam.lookAt(ctl.getWorldTranslation(), Vector3f.UNIT_Y);
}

[/java]

My problem is that the camera appears to loop at a certain point of scrolling up, and I have been unable to find anything on the forum that has indeed helped, therefore, any help would be gladly appreciated

Which camera is this? Why is this related to “Docmentation-JME3”?

Firstly; I apologise that this is blatantly in the wrong section, however I think i was just half asleep and must’ve posted in the wrong browser tab, secondly; it’s an implementation of the camera similar to chasecam, however we needed to get rid of a few features for our project, so this is a more custom camera handler than the ones they are using.

I know a lot of the cameras do all of this funky vector math and so on but to me it’s always been simpler just to track yaw and pitch and compose the Quaternion from those. (new Quaternion().fromAngles(pitch, yaw, 0))

One issue right off the bat with what you have is that if your lookAt() point is directly up or down from the location (ie: parallel to the Y Axis) then you will get strange results because your up axis is Y. And there really isn’t a good way around that.

For cameras like the FlyCam and the chase cam, where roll is basically fixed and anyway subsequent angles are never relative to roll… then yaw + pitch seems like a superior approach.

Even aside from that, I’m not sure the lookAt() is even necessary in this case. You already have the camera location and it seems like you could just back position up from there… but I don’t know if that goes against other things you might be trying to achieve.

Perhaps the rest of the TopView.java could give some insight as to what we’re trying to achieve with this camera:

[java]public class TopView extends AbstractAppState
{
private SimpleApplication sa;
private Node rootNode;
private Spatial ctl;
private InputManager im;
private Camera cam;
private float dist;
private Vector3f oldPos;

@Override public void initialize(AppStateManager stateManager, Application app)
{
	super.initialize(stateManager, app);

	// get stuff we need from the world
	this.sa = (SimpleApplication) app;
	rootNode = sa.getRootNode();
	cam = sa.getCamera();
	ctl = rootNode.getChild("cameraControl");
	im = sa.getInputManager();

	// set up the camera
	sa.getFlyByCamera().setEnabled(false);

	dist = 20f;
	oldPos = ctl.getWorldTranslation();
	rotateCamera(0.01f, 1, Vector3f.UNIT_X);
	cam.lookAt(ctl.getWorldTranslation(), Vector3f.UNIT_Y);

	// assign inputs for movement
	im.addMapping("Fast", new KeyTrigger(KeyInput.KEY_LSHIFT), new KeyTrigger(KeyInput.KEY_RSHIFT));
	im.addMapping("Forward", new KeyTrigger(KeyInput.KEY_W));
	im.addMapping("Backward", new KeyTrigger(KeyInput.KEY_S));
	im.addMapping("Left", new KeyTrigger(KeyInput.KEY_A));
	im.addMapping("Right", new KeyTrigger(KeyInput.KEY_D));

	im.addMapping("North", new KeyTrigger(KeyInput.KEY_UP));
	im.addMapping("South", new KeyTrigger(KeyInput.KEY_DOWN));
	im.addMapping("East", new KeyTrigger(KeyInput.KEY_RIGHT));
	im.addMapping("West", new KeyTrigger(KeyInput.KEY_LEFT));

	im.addListener(evt.digital, "Fast");
	im.addListener(evt.analog, "Forward", "Backward", "Left", "Right", "North", "South", "East", "West");

	// assign inputs for camera rotation
	im.addMapping("Rotator",new KeyTrigger(KeyInput.KEY_LCONTROL), new MouseButtonTrigger(MouseInput.BUTTON_MIDDLE));
	im.addMapping("Up", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
	im.addMapping("Down", new MouseAxisTrigger(MouseInput.AXIS_Y, false));
	im.addMapping("RLeft", new MouseAxisTrigger(MouseInput.AXIS_X, false));
	im.addMapping("RRight", new MouseAxisTrigger(MouseInput.AXIS_X, true));
	im.addMapping("In", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
	im.addMapping("Out", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));

	im.addListener(evt.digital, "Rotator");
	im.addListener(evt.analog, "Up", "Down", "RLeft", "RRight", "In", "Out");
}

@Override public void update(float tpf)
{
	Vector3f pos = ctl.getWorldTranslation();
	if ( oldPos.equals(pos) ) // only do this if we have moved
	{
		cam.lookAt(pos, Vector3f.UNIT_Y);
		oldPos = pos;
	}
}

@Override public void cleanup()
{
	super.cleanup();
	//TODO: clean up what you initialized in the initialize method,
	//e.g. remove all spatials from rootNode
	//this is called on the OpenGL thread after the AppState has been detached
}

private EventListener evt = new EventListener()
{
	// simple state-tracking for the input system
	boolean speedBtn = false;
	boolean rotator = false;

	@Override public void onInput(String name, float value, float tpf, float spd)
	{
		Vector3f pos = new Vector3f(0, 0, 0);

		// simple state-tracking for the input system
		if ( name.equals("Fast") ) { if ( value == 1 ) { speedBtn = true; } else { speedBtn = false; } }
		if ( name.equals("Rotator") ) { if ( value == 1 ) { rotator = true; } else { rotator = false; } }

		// go faster!
		if ( speedBtn ) { spd = spd * 2; }

		switch ( name )
		{
			// relative movement
			case "Forward": pos = cam.getDirection(); pos.y = 0; break;
			case "Backward": pos = cam.getDirection(); pos.y = 0; pos = pos.mult(-1); break;
			case "Right": pos = cam.getLeft(); pos.y = 0; pos = pos.mult(-1); break;
			case "Left": pos = cam.getLeft(); pos.y = 0; break;

			// absolute movement
			case "North": pos.z = + 1; break;
			case "South": pos.z = - 1; break;
			case "East": pos.x = - 1; break;
			case "West": pos.x = + 1; break;

			// camera distance from object
			case "In": dist -= spd / 10; fixDist(); break;
			case "Out": dist += spd / 10; fixDist(); break;
		}

		// update the controller and camera
		pos = pos.mult(spd);
		ctl.move(pos);
		rotateCamera(0, tpf, Vector3f.ZERO);

		// you spin me round
		if ( !rotator ) { return; }

		// camera rotators
		switch ( name )
		{
			case "Up": rotateCamera(value, tpf, new Vector3f(1, 0, 1)); break;
			case "Down": rotateCamera(-value, tpf, new Vector3f(1, 0, 1)); break;
			case "RLeft": rotateCamera(-value, tpf, Vector3f.UNIT_Y); break;
			case "RRight": rotateCamera(value, tpf, Vector3f.UNIT_Y); break;
		}
	}
};

protected void fixDist()
{
	FastMath.clamp(dist, 5f, 50f);
}

protected void rotateCamera(float value, float tpf, Vector3f axis)
{
	float rotationSpeed = 1000;

	Matrix3f mat = new Matrix3f();
	mat.fromAngleNormalAxis(rotationSpeed * value * tpf, axis);

	Vector3f upDir = cam.getUp();
	Vector3f leftDir = cam.getLeft();
	Vector3f dir = cam.getDirection();

	mat.mult(upDir, upDir);
	mat.mult(leftDir, leftDir);
	mat.mult(dir, dir);

	Quaternion q = new Quaternion();
	q.fromAxes(leftDir, upDir, dir);
	q.normalizeLocal();
	sa.getCamera().setAxes(q);

	dir = cam.getDirection();
	dir = dir.mult(dist);
	Vector3f invDir = new Vector3f(-dir.x, -dir.y, -dir.z);
	Vector3f position = invDir.add(ctl.getWorldTranslation());

	position.y = FastMath.clamp(position.y, ctl.getWorldTranslation().y - 1, ctl.getWorldTranslation().y + dist + 1);
			
	cam.setLocation(position);
	cam.lookAt(ctl.getWorldTranslation(), Vector3f.UNIT_Y);
}

}[/java]

I do apologise for any trouble this may be causing.

This looks very strange to me:
rotateCamera(value, tpf, new Vector3f(1, 0, 1));

Again, I think your whole life gets easier if you track yaw and pitch and just compose your Quaternion from them when needed.

The way you are rotating around an invalid axis like that makes me wonder, though.

Does this camera operate like one would expect? Rotate around the point being viewed, left/right (yaw) or up/down (pitch)?

Code doesn’t really help me understand what it’s supposed to do in this case because it’s strange at best and incorrect at worst. Hard to reverse-engineer intent sometimes.

Yeah, I think just tracking the Yaw and Pitch, composing the quaternion from then would be the only real way to do this tbh; i think programming without coffee at like, 3AM isn’t exactly the best sometimes; The camera is intended to operate as expected, around the point being used, yaw/pitch. I think I just kinda lost myself with Quaternion forming a while ago.

I’m working with Darren on this project. I don’t know if you’ve played location management games (The Sims, Tropico, Dungeon Keeper, SimCity), but that’s the kind of camera control we’re going for.

Basically, it’s a top-down game where the camera tracks some invisible object in-world. You can move around and the camera angle remains consistent. The camera can be rotated around the invisible object, and there’s a zoom control which is basically distance from the invisible object. We also need the camera to be clamped on vertical rotation so the players can’t see skyward where we don’t have anything.

The components are: tracking location (object), angles from location, and distance from location.

Did you look at the “math for dummies” tutorial? It shows you how to rotate direction vectors etc., as paul indicated, its much easier to track direction/up vectors than trying to deal with rotations.

@ladyserenakitty said: I'm working with Darren on this project. I don't know if you've played location management games (The Sims, Tropico, Dungeon Keeper, SimCity), but that's the kind of camera control we're going for.

Basically, it’s a top-down game where the camera tracks some invisible object in-world. You can move around and the camera angle remains consistent. The camera can be rotated around the invisible object, and there’s a zoom control which is basically distance from the invisible object. We also need the camera to be clamped on vertical rotation so the players can’t see skyward where we don’t have anything.

The components are: tracking location (object), angles from location, and distance from location.

So, yaw, pitch, and distance/zoom are all you need to track and then you can replace 15 lines of code with like 3 lines. Clamp pitch to sensible values, etc…

I fixed it. The fix was ridiculously simple. I got to thinking our horizontal axis around the object is really just a circle, so I drew one (kinda). Here’s the current iteration of rotateCamera():
[java]protected void rotateCamera(float p, float y)
{
float vSpeed = 10f;
float hSpeed = 10f;
pitch = FastMath.clamp(pitch + (p * vSpeed), 15, 85);
yaw += (y * hSpeed);
yaw = yaw % 360;

float tp = pitch	* FastMath.DEG_TO_RAD;
float ty = yaw		* FastMath.DEG_TO_RAD;

Quaternion q = new Quaternion().fromAngles(0, 0, tp); //Yaw, Roll, Pitch
Vector3f pos = q.mult(Vector3f.UNIT_Y);
pos.x = (float)Math.sin(ty);
pos.z = (float)Math.cos(ty);

Vector3f point = ctl.getWorldTranslation();
cam.setLocation(pos.mult(dist).add(ctl.getWorldTranslation()));

}
[/java]

If you want to see the rest of the file, feel free to ask.

Quaternion q = new Quaternion().fromAngles(0, ty, tp);
Vector3f pos = q.mult(Vector3f.UNIT_Z);
pos.multLocal(-dist);

…add it to camera. Done.

@pspeed said: Quaternion q = new Quaternion().fromAngles(0, ty, tp); Vector3f pos = q.mult(Vector3f.UNIT_Z); pos.multLocal(-dist);

…add it to camera. Done.


Not quite. I’ve done a few modifications since the last post, nothing too terribly fancy. Just modified the sin/cos lines to use the X value the quaternion provided in pos.

The main thing is now the camera behaves exactly as needed.

@ladyserenakitty said: Not quite. I've done a few modifications since the last post, nothing too terribly fancy. Just modified the sin/cos lines to use the X value the quaternion provided in pos.

The main thing is now the camera behaves exactly as needed.

What I posted should be right. If you are grabbing values directly from a Quaternion then you are doing something very very wrong (always).

Anyway, the quaternion has all of the rotation you need. Unless your pitch is somehow strange, the code I posted is all you need.

@pspeed said: What I posted should be right. If you are grabbing values directly from a Quaternion then you are doing something very very wrong (always).

Anyway, the quaternion has all of the rotation you need. Unless your pitch is somehow strange, the code I posted is all you need.


I have simplified the code. We have all the camera features we need, and the camera angle is computed to coordinates in a simple one-liner. Then we adjust for distance and object position and we’ve got the result.

Seriously, once I figured out this is really a problem involving circles, it became super-simple. No Quaternions, matrices, or other high-level functions. Just draw a circle around it twice, literally.

[java] // circle magic
float stp = (float)Math.sin(tp);
Vector3f pos = new Vector3f(stp * (float)Math.sin(ty), (float)Math.cos(tp), stp * (float)Math.cos(ty));

	Vector3f point = ctl.getWorldTranslation();
	// adjust for camera distance and control object location
	cam.setLocation(pos.mult(dist).add(point));
	cam.lookAt(point, Vector3f.UNIT_Y);

[/java]

@ladyserenakitty said: [java] // circle magic float stp = (float)Math.sin(tp); Vector3f pos = new Vector3f(stp * (float)Math.sin(ty), (float)Math.cos(tp), stp * (float)Math.cos(ty)); [/java]

Ah, I see, you are manually doing the same math the Quaternion would have done… you just have your pitch rotated 90 degrees from what the quat would have expected.

Your lookAt issue will bite you later, though. You wouldn’t have needed that at all with the quaternion.

To be crystal clear:
[java]
Quaternion rot = new Quaternion().fromAngles(tp - FastMath.HALF_PI, ty, 0);
Vector3f pos = rot.mult(Vector3f.UNIT_Z);
cam.setLocation(pos.mult(-dist).addLocal(point));
cam.setRotation(rot);
[/java]

The -HALF_PI may be a bit off (for quaternions.fromAngles(), pitch=0 means looking at the horizon) but this approach prevents you from having issues if you look straight down the y axis. With your code, the camera rotation becomes strange and invalid in that case.

Actually, you can probably keep pitch the same and just rot.mult(Vector3f.UNIT_Y) if pitch=0 means ‘straight overhead’ like I think it does in your case.

[java]
Quaternion rot = new Quaternion().fromAngles(tp, ty, 0);
Vector3f pos = rot.mult(Vector3f.UNIT_Y);
cam.setLocation(pos.mult(-dist).addLocal(point));
cam.setRotation(rot);
[/java]

No lookAt() required… which was kind of the original point.

@pspeed said:

Actually, you can probably keep pitch the same and just rot.mult(Vector3f.UNIT_Y) if pitch=0 means ‘straight overhead’ like I think it does in your case.

[java]
Quaternion rot = new Quaternion().fromAngles(tp, ty, 0);
Vector3f pos = rot.mult(Vector3f.UNIT_Y);
cam.setLocation(pos.mult(-dist).addLocal(point));
cam.setRotation(rot);
[/java]

No lookAt() required… which was kind of the original point.


No, when pitch = 0, the camera is staring horizontally. We have pitch clamped between 15 and 85, so at its highest value, the camera is above the object facing downward. I’ve rotated and panned the camera through all the minimums and maximums, and it has no issues.

Although you’re probably right - I might be able to set rotation directly and bypass using lookAt(). I shall investigate this. Right now I’m using lookAt to keep the camera staring at the control object.