Helper class for camera operations

Started by Wojtek, February 09, 2010, 01:15:14 AM

Previous topic - Next topic

paulscode

My initial tests on your demo using pivots as I described above fixed the behavior only half-way (x-axis works perfectly regardless of any prior rotations, but I'm still experiencing a shifting z-axis for some reason).  I'll see if I can figure out a better solution.

paulscode

OMG, I am really stupid sometimes..  I just realized that I answered my own question when I said "rotates the ship around its relative axis'".  ACTUALLY, the following are both equivalent:

ship.rotateX( angle );
// SAME AS:
ship.rotateAxis( new Simplevector( 1, 0, 0 ), angle );


What you WANT to do is this:
ship.rotateAxis( ship.getXAxis(), angle );

Just tried it, and it immediately fixed the problem.  I'll repost the demo applet in a little bit (I can get rid of that pivot stuff from the DummyObjectCameraManager now, too).  Thanks for asking your question - helped me solve a problem that's been giving me a headache for months now!

paulscode

#17
Ok, version 2 of the DummyObjectCameraManager class (ironic I used the word "dummy" in the title..):

package CameraManagers;

import com.threed.jpct.Camera;
import com.threed.jpct.Matrix;
import com.threed.jpct.Object3D;
import com.threed.jpct.SimpleVector;

/**
* The DummyObjectCameraManager demonstrates using Object3D parent/child
* relationships to simplify camera control.
*
* @author Paul Lamb
*/
public class DummyObjectCameraManager extends AbstractCameraManager
{
    // Position for the camera to start out at (relative to cameraTarget):
    private final SimpleVector INITIAL_CAMERA_POSITION =
                                                  new SimpleVector( 0, 0, -10 );

    // Object for the camera to follow (spaceship, for example):
    private Object3D followObject = null;
    // Point that the camera is looking towards (not necessarily followObject):
    private Object3D cameraTarget = null;
    // Simplifies setting the camera's orientation in the update() method:
    private Object3D cameraUp = null;
    // Actual position of the camera:
    private Object3D cameraSatellite = null;

    // Handle to the actual camera:
    private Camera camera = null;

/**
* Constructor: Sets up the camera assembly.
* @param followObject Object for the camera to follow (spaceship for example)
*/
    public DummyObjectCameraManager( Object3D followObject )
    {
        this.followObject = followObject;
       
        // Create the dummy objects needed for the camera assembly:
        cameraTarget = Object3D.createDummyObj();
        cameraUp = Object3D.createDummyObj();
        cameraSatellite = Object3D.createDummyObj();

        // Place the up object in the correct position:
        // IMPORTANT: take cameraTarget direction into account.
        cameraUp.translate( new SimpleVector( 0, -1, 0 ) );

        // Set the initial relative position of the camera satellite:
        cameraSatellite.translate( INITIAL_CAMERA_POSITION );
       
        // Set up the parent/child relationships:
        followObject.addChild( cameraTarget );
        cameraTarget.addChild( cameraSatellite );
        cameraSatellite.addChild( cameraUp );


        // TODO: temporary fix for ship facing "UP" instead of "FORWARD"
        /*************************/
        // Get the cameraTarget's x-axis:
        SimpleVector xAxis = cameraTarget.getXAxis();
        // Get the transformation matrix of its parent (the followObject):
        Matrix m = new Matrix( followObject.getWorldTransformation() );
        // Turn that into a rotation matrix:
        float[] dm = m.getDump();
        for( int i = 12; i < 15; i++ )
        {
            dm[i] = 0;
        }
        dm[15] = 1;
        m.setDump( dm );
        // Invert it:
        Matrix mI = m.invert3x3();
        // Apply parent's rotations to the cameraTarget's x-axis:
        xAxis.matMul( mI );
        // Rotate the cameraTarget:
        cameraTarget.rotateAxis( xAxis, (float) Math.PI / 2.0f );
        // Get the cameraTarget's new z-axis:
        SimpleVector zAxis = cameraTarget.getZAxis();
        // Apply the parent's rotations:
        zAxis.matMul( mI );
        // Rotate the cameraTarget:
        cameraTarget.rotateAxis( zAxis, (float) Math.PI );
        /**************************/
    }

/**
* Sets the camera to be managed.
* @param camera Handle to the camera to control.
*/
    @Override
    public void setCamera( Camera camera )
    {
    this.camera = camera;
    }

/**
* Orbits the camera around its target.
* @param deltaX Mouse's linear change in x-position.
* @param deltaY Mouse's linear change in y-position.
*/
    @Override
    public void rotate( final double deltaX, final double deltaY )
    {
        // Convert the delta's into some angles:
        float thetaX = -(float)Math.PI * ( (float)(deltaY) / 480f );
        float thetaY = (float)Math.PI * ( (float)(deltaX) / 640f );

        // NOTE:  If using followObject as the cameraTarget, then it isn't
        // necessary to do this matrix stuff - just rotate around the axis'.

        // Get the cameraTarget's x-axis:
        SimpleVector xAxis = cameraTarget.getXAxis();
        // Get the transformation matrix of its parent (the followObject):
        Matrix m = new Matrix( followObject.getWorldTransformation() );
        // Turn that into a rotation matrix:
        float[] dm = m.getDump();
        for( int i = 12; i < 15; i++ )
        {
            dm[i] = 0;
        }
        dm[15] = 1;
        m.setDump( dm );
        // Invert it:
        Matrix mI = m.invert3x3();
        // Apply parent's rotations to the cameraTarget's x-Axis:
        xAxis.matMul( mI );
        // Rotate the cameraTarget:
        cameraTarget.rotateAxis( xAxis, thetaX );
        // Get the cameraTarget's new y-axis:
        SimpleVector yAxis = cameraTarget.getYAxis();
        // Apply the parent's rotations:
        yAxis.matMul( mI );
        // Rotate the cameraTarget:
        cameraTarget.rotateAxis( yAxis, thetaY );
    }

/**
* Zooms the camera toward or away from its target.
* @param delta Distance to translate the camera.
*/
    @Override
    public void zoom( double delta )
    {
        // translate along the satellite's z-axis:
    cameraSatellite.translate( new SimpleVector( 0, 0, (float) delta ) );
    }

/**
* Moves the camera to the correct position and orientation.
* @param followObject Object for the camera to follow (spaceship for example).
*/
    @Override
    public void update( Object3D followObject )
    {
        // check if the followObject has changed:
        if( this.followObject != followObject )
        {
            // New object to follow with the camera:
            this.followObject.removeChild( cameraTarget );
            followObject.addChild( cameraTarget );
            this.followObject = followObject;
        }

        // Position the camera at the cameraSatellite's position:
        camera.setPosition( cameraSatellite.getTransformedCenter() );

        // Determine the camera's look and up directions:
        SimpleVector look = cameraTarget.getTransformedCenter().calcSub(
                           cameraSatellite.getTransformedCenter() ).normalize();
        SimpleVector up = cameraUp.getTransformedCenter().calcSub(
                           cameraSatellite.getTransformedCenter() ).normalize();

        // Set the camera's orientation based on target and up directions:
        camera.setOrientation( look, up );
    }
}


--EDIT-- Notice I put in a temporary fix for the "ship" facing up instead of forward.  Just delete that section of code if you're game doesn't have the same orientation oddity.  Also, in the "rotate" method, I put in some matrix stuff to take into account the rotations of the cameraTarget's parent (the ship).  If you don't need a seperate cameraTarget, you could use the ship itself as the cameraTarget, and then that matrix stuff would no longer be required.  Having a seperate cameraTarget just makes the class more flexable in case you decide you want the capability of pointing the camera in different directions while still having it follow the ship, and it also allows you to look at the ship from different angles.  Whether or not you need those capabilities just depends on what you have in mind for your game.

Working demo applet:

Demo Applet v.2  (Source code)

Let me know if there are other issues (hopefully I got it right this time   ;))

Wojtek

Wow, it works :) Thanks a lot for help - I would struggle with it for a long time....
The other thing is that for sure I will have to read more about 3d gemetry to be able to understand it well.

In one of previous posts you have written:
Quote from: paulscode
I think the reason for this behavior is probably that you are using ship.rotateZ and ship.rotateX, which rotates the ship around its relative axis'.  When using one one of them, that axis is constant, so rotating only vertically or only "horizontally" behaves the way you would expect.  However, when you do rotations around both axis', they are no longer constant.

I really can't explain the problem any better than that, because honestly every time I try to figure it out logically, it seems like what you are doing should work, but it doesn't.
I am not sure if my observation would be usefull, especially that you have already solved the problem, but I have noticed that if you apply X and Z rotation to object and try to get angles by deriveAngles method, you will get 3 non-zero angles instead of 2.
I have found an article about rotation matrixes (link) which shows 2 interesting things:
* depending on order you apply rotations, the rotation matrix will look different
* you can apply rotations in only 2 dimensions, but 3rd dimension is also affected (see example where object is rotated by 90* about X, 90* about Y and -90* about X and it gives rotation of 90* about Z)
I also remember that EgonOlsen was writing about rotation order some time ago, so that could be a cause of strange behavior.
Perhaps if I reset ship rotations every time and apply full angles instead of doing incrementary rotations in different directions it will work good (I have not checked that yet, but just for pure curiosity I will)...

Thanks a lot one more time,
Wojtek

paulscode

You're right about order of rotations influences your final orientation.  In fact, you can achieve any orientation with rotations around only two axis'.

However, in this case, that was not the problem.  In fact, for this particular application, you actually WANT the two remaining axis' to change when you rotate around one axis.  Regardless of which order you rotate the ship, you want +x to always be the ship's right, -y to always be the ship's up, etc.  You used the rotateX and rotateZ methods because you assumed, just as I did, that these methods rotate an object around its own x and z axis'.  But in fact, that is NOT what these methods do.  What they really do is rotate an object around the WORLD x and z axis' (which never change - they are always 1,0,0 and 0,0,1).  They do NOT around the x and z axis of the object.  So in other words, we were making an assumption about how jPCT works, but unfortunately we were wrong about that assumption.

Wojtek

Ok, I understand it now.

Thanks for all the help,
Wojtek

Wojtek

Hello,

I have spent some additional time on the camera managers and came with new version of FollowingCameraManager. It behaves a little bit different than DummyObjectCameraManager. It is visible during object rotations, especially when camera perspective is changed (left mouse button) and object is rotated in 2 dimensions (arrow keys).

There is a link with sources: sources and app

And there are changed files:

public class FollowingCameraManager extends AbstractCameraManager
{
    private static final double MAX_POS_VERTICAL_ANGLE = Math.PI / 4;
    private static final double MIN_NEG_VERTICAL_ANGLE = 2 * Math.PI
    - MAX_POS_VERTICAL_ANGLE;
    private static final double RAD_BY_PIXEL = 1 / 200.0;

    private double camHAngle, camVAngle;

    @Override
    public void rotate(final double deltaX, final double deltaY)
    {
camVAngle = limitVerticalAngle(MathUtil.normalizeAngle(camVAngle
+ deltaY * RAD_BY_PIXEL));
camHAngle = MathUtil.normalizeAngle(camHAngle - deltaX * RAD_BY_PIXEL);
    }

    private double limitVerticalAngle(double angle)
    {
if (angle > MAX_POS_VERTICAL_ANGLE && angle < Math.PI)
    return MAX_POS_VERTICAL_ANGLE;
else if (angle < MIN_NEG_VERTICAL_ANGLE && angle > Math.PI)
    return MIN_NEG_VERTICAL_ANGLE;
return angle;
    }

    @Override
    public void update(Object3D target)
    {
resetCamera();
updateTargetRotation(target);
updateCameraPosition(target);
    }

    private void resetCamera()
    {
getCamera().setPosition(SimpleVector.ORIGIN);
getCamera().setOrientation(new SimpleVector(0, 1, 0),
    new SimpleVector(0, 0, -1));
    }

    private void updateTargetRotation(Object3D target)
    {
SimpleVector rotationVector = MathUtil.deriveAngles(target.getRotationMatrix());
getCamera().rotateAxis(getCamera().getYAxis(), -rotationVector.y);
getCamera().rotateAxis(getCamera().getZAxis(), -rotationVector.z);
getCamera().rotateAxis(getCamera().getXAxis(), -rotationVector.x);
getCamera().rotateCameraX((float) camVAngle);
getCamera().rotateCameraY((float) camHAngle);
    }

    private void updateCameraPosition(Object3D target)
    {
getCamera().setPosition(target.getTransformedCenter());
getCamera().moveCamera(Camera.CAMERA_MOVEOUT, getDistance());
    }

    @Override
    public void setCamera(Camera cam)
    {
super.setCamera(cam);
camHAngle = 0;
camVAngle = 0;
    }
}



public final class MathUtil
{

    private static final double FULL_ANGLE = 2 * Math.PI;

    public static double normalizeAngle(double angle)
    {
while (angle > FULL_ANGLE)
    angle -= FULL_ANGLE;
while (angle < 0)
    angle += FULL_ANGLE;
return angle;
    }
    public static SimpleVector deriveAngles(Matrix mat)
    {
float[] m = mat.getDump();
double heading;
double attitude;
double bank;
if (m[4] > 0.998) { // singularity at north pole
heading = Math.atan2(m[2],m[10]);
attitude = Math.PI/2;
bank = 0;

}
else if (m[4] < -0.998) { // singularity at south pole
heading = Math.atan2(m[2],m[10]);
attitude = -Math.PI/2;
bank = 0;
}
else{
heading = Math.atan2(-m[8],m[0]);
bank = Math.atan2(-m[6],m[5]);
attitude = Math.asin(m[4]);}
return new SimpleVector(bank,heading,attitude);
    }
}


Thanks,
Wojtek