The Short Version (TL;DR)
I have a Camera
attached to a SceneNode
and movement works fine as long as the SceneNode
's rotation/axes are aligned with the world's. However, when an object rotates to "look" in a different direction and is told to move "forward" it does not move along the new "forward" direction. Instead, it continues to move in the same direction it was facing before the rotation was applied.
Details and Example
I have a scene graph to manage a 3D scene. The graph is a tree of SceneNode
objects, which know about their transformations relative to their parent and the world.
As per the TL;DR; snippet, imagine you have a cameraNode
with zero rotation (e.g. facing north) and then rotate the cameraNode
90 degrees to the left around the +Y "up" axis, i.e. make it look to the west. Things are OK so far. If you now try to move the cameraNode
"forward", which is now to the west, the cameraNode
instead moves as if "forward" were still facing north.
In short, it moves as if it had never been rotated in the first place.
The code below shows what I've attempted most recently and my (current) best guess at narrowing down the areas most likely to be related to the problem.
Relevant SceneNode
Members
The SceneNode
implementation has the following fields (only those relevant to this question are shown):
class GenericSceneNode implements SceneNode {
// this node's parent; always null for the root scene node in the graph
private SceneNode parentNode;
// transforms are relative to a parent scene node, if any
private Vector3 relativePosition = Vector3f.createZeroVector();
private Matrix3 relativeRotation = Matrix3f.createIdentityMatrix();
private Vector3 relativeScale = Vector3f.createFrom(1f, 1f, 1f);
// transforms are derived by combining transforms from all parents;
// these are relative to the world --in world space
private Vector3 derivedPosition = Vector3f.createZeroVector();
private Matrix3 derivedRotation = Matrix3f.createIdentityMatrix();
private Vector3 derivedScale = Vector3f.createFrom(1f, 1f, 1f);
// ...
}
Adding a Camera
to a scene simply means that it gets attached to a SceneNode
in the graph. Since the Camera
has no positional/rotational information of its own, the client simply handles the SceneNode
to which the Camera
is attached and that's it.
Except for the issue mentioned in this question, everything else appears to be working as expected.
SceneNode
Translation
The math to translate the node in a specific direction is straightforward and basically boils down to:
currentPosition = currentPosition + normalizedDirectionVector * offset;
The SceneNode
implementation follows:
@Override
public void moveForward(float offset) {
translate(getDerivedForwardAxis().mult(-offset));
}
@Override
public void moveBackward(float offset) {
translate(getDerivedForwardAxis().mult(offset));
}
@Override
public void moveLeft(float offset) {
translate(getDerivedRightAxis().mult(-offset));
}
@Override
public void moveRight(float offset) {
translate(getDerivedRightAxis().mult(offset));
}
@Override
public void moveUp(float offset) {
translate(getDerivedUpAxis().mult(offset));
}
@Override
public void moveDown(float offset) {
translate(getDerivedUpAxis().mult(-offset));
}
@Override
public void translate(Vector3 tv) {
relativePosition = relativePosition.add(tv);
isOutOfDate = true;
}
Other than the issue mentioned in this question, things around as expected.
SceneNode
Rotation
The client application rotates the cameraNode
as follows:
final Angle rotationAngle = new Degreef(-90f);
// ...
cameraNode.yaw(rotationAngle);
And the SceneNode
implementation is also fairly straightforward:
@Override
public void yaw(Angle angle) {
// FIXME?: rotate(angle, getDerivedUpAxis()) accumulates other rotations
rotate(angle, Vector3f.createUnitVectorY());
}
@Override
public void rotate(Angle angle, Vector3 axis) {
relativeRotation = relativeRotation.rotate(angle, axis);
isOutOfDate = true;
}
The math/code for the rotation is encapsulated in a 3x3 matrix object. Note that, during tests, you can see the scene being rotated around the camera, so rotations are indeed being applied, which makes this issue even more puzzling to me.
Direction Vectors
The directional vectors are simply columns from taken from the derived 3x3 rotation matrix, relative to the world:
@Override
public Vector3 getDerivedRightAxis() {
return derivedRotation.column(0);
}
@Override
public Vector3 getDerivedUpAxis() {
return derivedRotation.column(1);
}
@Override
public Vector3 getDerivedForwardAxis() {
return derivedRotation.column(2);
}
Computing Derived Transforms
If it's relevant, this is how the parentNode
transforms are combined to compute the derived transforms of this
instance:
private void updateDerivedTransforms() {
if (parentNode != null) {
/**
* derivedRotation = parent.derivedRotation * relativeRotation
* derivedScale = parent.derivedScale * relativeScale
* derivedPosition = parent.derivedPosition + parent.derivedRotation * (parent.derivedScale * relativePosition)
*/
derivedRotation = parentNode.getDerivedRotation().mult(relativeRotation);
derivedScale = parentNode.getDerivedScale().mult(relativeScale);
Vector3 scaledPosition = parentNode.getDerivedScale().mult(relativePosition);
derivedPosition = parentNode.getDerivedPosition().add(parentNode.getDerivedRotation().mult(scaledPosition));
} else {
derivedPosition = relativePosition;
derivedRotation = relativeRotation;
derivedScale = relativeScale;
}
Matrix4 t, r, s;
t = Matrix4f.createTranslationFrom(relativePosition);
r = Matrix4f.createFrom(relativeRotation);
s = Matrix4f.createScalingFrom(relativeScale);
relativeTransform = t.mult(r).mult(s);
t = Matrix4f.createTranslationFrom(derivedPosition);
r = Matrix4f.createFrom(derivedRotation);
s = Matrix4f.createScalingFrom(derivedScale);
derivedTransform = t.mult(r).mult(s);
}
This is used to propagate transforms through the scene graph, so that child SceneNode
s can take their parent's transforms into account.
Other/Related Questions
I've gone through several answers inside and outside of SO during the last ~3 weeks prior to posting this question (e.g. here, here, here, and here, among several others). Obviously, though related, they really weren't helpful in my case.
Answers to Questions in the Comments
Are you sure that when computing
derivedTransform
your parent'sderivedTransform
is already computed?
Yes, the parent SceneNode
is always updated before updating children. The update
logic is:
@Override
public void update(boolean updateChildren, boolean parentHasChanged) {
boolean updateRequired = parentHasChanged || isOutOfDate;
// update this node's transforms before updating children
if (updateRequired)
updateFromParent();
if (updateChildren)
for (Node n : childNodesMap.values())
n.update(updateChildren, updateRequired);
emitNodeUpdated(this);
}
@Override
public void updateFromParent() {
updateDerivedTransforms(); // implementation above
isOutOfDate = false;
}
This piece invokes the private method in the previous section.
This is not meant to be a direct answer but as reference upon the request of the OP.
OpenGL v1.0 using old API calls: Implementation of a Camera Class Object while using it in a Scene Class outside of the Scene Class's Scene Graph. This is written in C++
Camera.h
Camera.cpp
In the
Camera
'sRender
function using the old OpenGL API calls we first load in the Modelview matrix, then we load the identity matrix; then we finally use glu's gluLookAt(...) method to set the positions of the needed vectors.Scene.h - Has many members and functions; but as in regards with the
Camera
Object it has a Camera as a member and not a pointer to a Camera.Scene.cpp - Render()
Here the
Camera
is independent of thePlayer
class as well as the Scene's Scene Graph Hierarchy and we use theCamera
in the Scene'sRender
Call. Here we set theCamera
by getting thePlayer
's current Position, and thePlayer's
LookCenter
direction.EDIT - Adding Player Class And Related Code For Movement Calculations
Player.h
Player.cpp
Scene.h - Same here as for the Camera; there is a Player Object and not a pointer to a player object. However there is a pointer to a playerTransform which is a NodeTransform. There are too many functions to list here because of the interaction of the Player with the Scene since this is a working 3D Game. I can provide a few functions that may be of interest.
Scene.cpp
Scene::Update()
EDIT - Adding NodeTransform::Render() - Show the order of operations for MVP