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's derivedTransform
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.
See Question&Answers more detail:
os