Rotating local Z-axis (roll) on a MouseLook (or similarly) controlled camera?

Desired Result: Similar to a space craft, capable of maneuvering in any direction within a 3d environment: keyboard movement (X/Y/Z translation), mouse Yaw/Pitch (X/Y rotation), and keyboard Roll (Z rotation).

Problem: Can't manipulate the Z-axis rotation for roll.

Basic example in a (v2.6) new scene-

  1. Select the default Main Camera
  2. Component -> Camera-Control -> MouseLook
  3. Component -> Physics -> Rigid Body
  4. In the Inspector, turn off 'Use Gravity'
  5. Create a new JavaScript asset and apply it to the Main Camera

JavaScript-

var speed : int = 1;
function FixedUpdate() {
  if (Input.GetKey("w")) {
    rigidbody.AddRelativeForce(Vector3.forward * speed);
  }
}

This behaves exactly the way I want, except for roll. It works great for forward momentum with the mouse controlling your direction of travel (yaw/pitch), and I can easily add additional controls using other Vector3's (x/y/z translation). However, I just can't seem to find any way to rotate the Z-axis for that desired roll effect.

I've tried using `transform.Rotate()` and `rigidBody.AddRelativeTorque()`, but both only work if the MouseLook node is detached/disabled.

I've also tried mimicking MouseLook behavior directly in the above script by applying `Input.GetAxis("Mouse X/Y")` to `transform.Rotate()`, which does seem to work for Pitch/Yaw, and roll does rotate the Z axis - but it would only rotate within 0-8 degrees, and moving the mouse would cause it to "jitter" within 0-15 degrees.

I found that attempting to use `rigidBody.AddRelativeTorque()` with `Input.GetAxis("Mouse X/Y")`, causes it to spin uncontrollably. Which I think does make sense, since it's constantly applying torque based on the mouse's position - not the desirable effect.

From what I can tell so far, setting the X/Y rotation effects the Z axis as well, which is where my problem is. Perhaps this is part of Quaternion nature to avoid gimbal lock? That's just a curious guess, as I don't understand the Quaternion concept well enough to grasp why this is happening.

For reference, http://answers.unity3d.com/questions/2544/first-person-flight-controls helped me get this far - but unless I'm missing something, it doesn't quite fulfill this scenario.

So, the basic question is: How do I let the user 'roll' the camera on the Z axis with a keystroke, while still controlling the yaw/pitch (X/Y) axis with the mouse?

Thank you for reading, and even more thanks for any answers or suggestions!

~Badger

I don't have an exact answer for you, but the reason your MouseLook script breaks anything you try to do with roll is because the MouseLook script sets the transform.rotation of your object. So, depending on the order of operations of update, the MouseLook will hammer the rotation, back to be facing upwards, probably causing the "jittering" effect you're talking about.

If you open the default MouseLook script it does something like this:

        // Read the mouse input axis
        rotationX += Input.GetAxis("Mouse X") * sensitivityX;
        rotationY += Input.GetAxis("Mouse Y") * sensitivityY;

        rotationX = ClampAngle (rotationX, minimumX, maximumX);
        rotationY = ClampAngle (rotationY, minimumY, maximumY);

        Quaternion xQuaternion = Quaternion.AngleAxis (rotationX, Vector3.up);
        Quaternion yQuaternion = Quaternion.AngleAxis (rotationY, Vector3.left);

        transform.localRotation = originalRotation * xQuaternion * yQuaternion;

You might be able to add a Z rotation in there and just multiply them all together. I'd suggest trying that.

The following is the full control script that I wrote after finding the solution. It's a strong basis for the 3d-space controller I was looking for - and fun to fly around with! For best testing results, I suggest setting your rigid body `Mass`, `Drag` and `Angular Drag` all to `1`; also, make sure `Gravity`, `Is Kinematic` and `Freeze Rotation` are all disabled.

var speed : int = 10; // Relative force applied for standard ws,ad,zx,qe movement.
var mouseSensativity : int = 15; // Speed of mouse movements for X/Y rotation, looking around.
var invertPitch : boolean = false; // Invert the X rotation axis. False: MouseDown=LookDown, True:MouseDown=LookUp
// Whether to use transform.Rotate() or rigidbody.AddRelativeTorque();
enum rotationMethods { Torque = 0, Rotate = 1 };
var rotationMethod = rotationMethods.Rotate;

function FixedUpdate() {
    // This probably could be turned into a constant, instead of checking every update, but doing so lets the user change the setting on the fly without repercussions.
    var invertPitchInt;
    if (invertPitch) invertPitchInt = -1;
    else invertPitchInt = 1;

   // Standard translate/position controls.
    if (Input.GetKey("w")) { rigidbody.AddRelativeForce (Vector3.forward * speed);  }
    if (Input.GetKey("s")) { rigidbody.AddRelativeForce (Vector3.forward * -1 * speed); }
    if (Input.GetKey("a")) { rigidbody.AddRelativeForce (Vector3.left * speed); }
    if (Input.GetKey("d")) { rigidbody.AddRelativeForce (Vector3.right * speed); }
    if (Input.GetKey("z")) { rigidbody.AddRelativeForce (Vector3.down* speed); }
    if (Input.GetKey("x")) { rigidbody.AddRelativeForce (Vector3.up * speed); }

    // Keyboard controls to mimic mouse movements.
    if (Input.GetKey("i")) { // Simulate increasing the X axis, mouse movement up.
        if (rotationMethod == rotationMethods.Torque) { rigidbody.AddRelativeTorque (invertPitchInt * -mouseSensativity * Time.deltaTime, 0, 0); }
        else { transform.Rotate(invertPitchInt *  -mouseSensativity * Time.deltaTime, 0, 0); }
    }
    if (Input.GetKey("k")) { // Simulate decreasing the X axis, mouse movement down.
        if (rotationMethod == rotationMethods.Torque) { rigidbody.AddRelativeTorque (invertPitchInt * mouseSensativity * Time.deltaTime, 0, 0); }
        else { transform.Rotate(invertPitchInt * mouseSensativity * Time.deltaTime,0,0); }
    }
    if (Input.GetKey("j")) { // Simulate increasing the Y axis, mouse movement left.
        if (rotationMethod == rotationMethods.Torque) { rigidbody.AddRelativeTorque (0, -mouseSensativity * Time.deltaTime, 0); }
        else { transform.Rotate(0, -mouseSensativity * Time.deltaTime, 0); }
    }
    if (Input.GetKey("l")) { // Simulate decreasing the Y axis, mouse movement right.
        if (rotationMethod == rotationMethods.Torque) { rigidbody.AddRelativeTorque (0, mouseSensativity * Time.deltaTime, 0); }
        else { transform.Rotate(0, mouseSensativity * Time.deltaTime, 0); }
    }

    // Actual mouse movement controls
    if (Input.GetAxis("Mouse X")) {
        if (rotationMethod == rotationMethods.Torque) { rigidbody.AddRelativeTorque( 0, Input.GetAxis("Mouse X") * mouseSensativity, 0); }
        else { transform.Rotate(0, Input.GetAxis("Mouse X") * mouseSensativity, 0); }
    }
    if (Input.GetAxis("Mouse Y")) {
        if (rotationMethod == rotationMethods.Torque) { rigidbody.AddRelativeTorque( invertPitchInt * Input.GetAxis("Mouse Y") * -mouseSensativity, 0, 0); }
        else { transform.Rotate(invertPitchInt * Input.GetAxis("Mouse Y") * -mouseSensativity, 0, 0); }
    }

    // Roll controls
    if (Input.GetKey("q")) {
        if (rotationMethod == rotationMethods.Torque) { rigidbody.AddRelativeTorque(0, 0, speed * Time.deltaTime); }
        else { transform.Rotate( 0, 0, -speed * Time.deltaTime); }
    }
    if (Input.GetKey("e")) {
        if (rotationMethod == rotationMethods.Torque) { rigidbody.AddRelativeTorque(0, 0, -speed * Time.deltaTime); }
        else { transform.Rotate( 0, 0, speed * Time.deltaTime); }
    }
}

I don't know if it's "proper" to post this, too, but I think it can be removed if overkill. This is the test script I was working with, so others that might have similar issues can see what I tested, and why it didn't work. It's not very pretty, but hopefully the comments explain my thought process. I'm still getting used to this, though, so my apologies if it's not very clear.

var speed : int = 10; // Relative force applied for standard ws,ad,zx movement.
var mouseSensativity : int = 15;
// Rotation method used for rolling. {q/e}
enum rollMethods { Torque = 0, Rotate = 1 };
var rollMethod = rollMethods.Rotate;
// Rotation method used for mouse, or mouse-simulated key strokes  {ik,jl}
enum rotationTypes { Torque = 0, Rotate = 1 };
var rotationType = rotationTypes.Rotate;
// Option to roll before or after the X/Y mouse rotations.
enum whenToRollOptions {BeforeMouseLook = 0, DuringMouseLook = 1, AfterMouseLook = 2};
var whenToRoll = whenToRollOptions.DuringMouseLook;
// Switch between the mouse-simulated keyboard, or actual mouse movement.
var useKeyboardMouseSimulator : boolean = false;
// Select which test result to use.
enum mouseLookTests { TestA = 0, TestB = 1, TestC = 3, TestD = 4, TestE = 5, TestF = 6, TestG = 7, TestH = 8 }
var mouseLookTest = mouseLookTests.TestA;

var clampedRotationX : float = 0.0;
var clampedRotationY : float = 0.0;
var clampedRotationZ : float = 0.0;

function FixedUpdate() {
   // Standard translate/position controls.
    if (Input.GetKey("w")) { rigidbody.AddRelativeForce (Vector3.forward * speed);  }
    /*else*/ if (Input.GetKey("s")) { rigidbody.AddRelativeForce (Vector3.forward * -1 * speed); }
    if (Input.GetKey("a")) { rigidbody.AddRelativeForce (Vector3.left * speed); }
    /*else*/ if (Input.GetKey("d")) { rigidbody.AddRelativeForce (Vector3.right * speed); }
    if (Input.GetKey("z")) { rigidbody.AddRelativeForce (Vector3.down* speed); }
    /*else*/ if (Input.GetKey("x")) { rigidbody.AddRelativeForce (Vector3.up * speed); }

    if (useKeyboardMouseSimulator) { // Manual key controls simulating mouse movement.
        if (Input.GetKey("q")) {
            if (rollMethod == rollMethods.Torque) { rigidbody.AddRelativeTorque(0,0,speed * Time.deltaTime); }
            else { transform.Rotate(0,0,speed * Time.deltaTime); }
        } else if (Input.GetKey("e")) {
            if (rollMethod == rollMethods.Torque) { rigidbody.AddRelativeTorque(0,0,-speed * Time.deltaTime); }
            else { transform.Rotate(0,0,-speed * Time.deltaTime); }
        }

        // ** -- Converting this block to accept Mouse input should be the solution I'm looking for -- **
        // The result these keys produce is the exact same result the mouse movement should produce: i=MouseDown, k=MouseUp, j=MouseLeft, k=MouseRight
        // Using Rotate is much more responsive, and likely the best result for user controlled mouse-like movement, and probobly is the result I'm most likely to desire to use.
        // However, I'd also like to have the option to use the Torque method, if it would be possible to impliment - just to see how it functions.
        if (Input.GetKey("i")) { // Simulate increasing the X axis, mouse movement up.
            if (rotationType == rotationTypes.Torque) { rigidbody.AddRelativeTorque (speed * Time.deltaTime, 0, 0); }
            else { transform.Rotate(speed * Time.deltaTime, 0, 0); }
        } else if (Input.GetKey("k")) { // Simulate decreasing the X axis, mouse movement down.
            if (rotationType == rotationTypes.Torque) { rigidbody.AddRelativeTorque (-speed * Time.deltaTime, 0, 0); }
            else { transform.Rotate(-speed * Time.deltaTime,0,0); }
        }
        if (Input.GetKey("j")) { // Simulate increasing the Y axis, mouse movement left.
            if (rotationType == rotationTypes.Torque) { rigidbody.AddRelativeTorque (0, -speed * Time.deltaTime, 0); }
            else { transform.Rotate(0, -speed * Time.deltaTime, 0); }
        } else if (Input.GetKey("l")) { // Simulate decreasing the Y axis, mouse movement right.
            if (rotationType == rotationTypes.Torque) { rigidbody.AddRelativeTorque (0, speed * Time.deltaTime, 0); }
            else { transform.Rotate(0, speed * Time.deltaTime, 0); }
        }
    } else { // This block contains my various attempts to achieve the goal I'm after.
        clampedRotationX = ClampAngle(clampedRotationX + (Input.GetAxis("Mouse X") * mouseSensativity), -360, 360);
        clampedRotationY = ClampAngle(clampedRotationY + (Input.GetAxis("Mouse Y") * mouseSensativity), -360, 360);
        if (Input.GetKey("q")) { clampedRotationZ = ClampAngle(clampedRotationZ + (speed * Time.deltaTime), -360, 360); }
        else if (Input.GetKey("e")) { clampedRotationZ = ClampAngle(clampedRotationZ + (-speed * Time.deltaTime), -360, 360); }

        if (mouseLookTest == mouseLookTests.TestA) {
            // This test works almost perfectly, except that the mouse movements do not follow the Z rotation propperly.
            // Moving the mouse up/down looks up/down as expected when Z=0, but as Z changes the up/down movement does not follow.
            // Basically if Z=90, moving the mouse up/down looks left/right. This is not the desired effect. Moving the mouse up/down, should always look up/down.
            transform.localRotation = Quaternion.Euler(-clampedRotationY, clampedRotationX, clampedRotationZ);
        } else if (mouseLookTest == mouseLookTests.TestB) {
            // This seems to have the same result as TestA.
            transform.localRotation = Quaternion.AngleAxis(clampedRotationX, Vector3.up) * Quaternion.AngleAxis(clampedRotationY, Vector3.left) * Quaternion.AngleAxis(clampedRotationZ, Vector3.forward);
        } else if (mouseLookTest == mouseLookTests.TestC) {
            // This freaks out beyond my understanding.
            transform.localRotation.x = clampedRotationX; // clampedRotationY;
            transform.localRotation.y = clampedRotationY; // clampedRotationX;
            transform.localRotation.z = clampedRotationZ;
        } else if (mouseLookTest == mouseLookTests.TestD) {
            // This applies a constant rotation to the object, meaning it will always be rotating as long as clampedRotationX/Y/Z have non-zero values.
            // Effectively this makes it impossible to control, since the camera will just start spinning.
            transform.Rotate(clampedRotationY, clampedRotationX, clampedRotationZ);
        } else if (mouseLookTest == mouseLookTests.TestE) {
            // Same as TestD, just with Torque/force instead of direct rotation.
            rigidbody.AddRelativeTorque(clampedRotationY, clampedRotationX, clampedRotationZ);
        } else if (mouseLookTest == mouseLookTest.TestF) {
            // This works perfectly! Used without torque, best used with 'Freeze Rotation' enabled to avoid Torque forces interfearing with your rotational axis.
            if (Input.GetKey("q")) { transform.Rotate(Input.GetAxis("Mouse Y") * -mouseSensativity, Input.GetAxis("Mouse X") * mouseSensativity, speed * Time.deltaTime); }
            else if (Input.GetKey("e")) { transform.Rotate(Input.GetAxis("Mouse Y") * -mouseSensativity, Input.GetAxis("Mouse X") * mouseSensativity, -speed * Time.deltaTime); }
            else { transform.Rotate(Input.GetAxis("Mouse Y") * -mouseSensativity, Input.GetAxis("Mouse X") * mouseSensativity, 0); }
        } else if (mouseLookTest == mouseLookTest.TestG) {
            // This works perfectly! This is truely what I was after, with 'Freeze Rotation' disabled, Torque forces apply beautifully.
            if (Input.GetKey("q")) { rigidbody.AddRelativeTorque(Input.GetAxis("Mouse Y") * -mouseSensativity, Input.GetAxis("Mouse X") * mouseSensativity, speed * Time.deltaTime); }
            else if (Input.GetKey("e")) { rigidbody.AddRelativeTorque(Input.GetAxis("Mouse Y") * -mouseSensativity, Input.GetAxis("Mouse X") * mouseSensativity, -speed * Time.deltaTime); }
            else { rigidbody.AddRelativeTorque(Input.GetAxis("Mouse Y") * -mouseSensativity, Input.GetAxis("Mouse X") * mouseSensativity, 0); }
        } else if (mouseLookTest == mouseLookTests.TestH) {
            // This also works as expected, and accepts the roll/rotate options accurately.
            // This was used to test/prove that X/Y rotation for looking can be 'quick' using transform.Rotate(), while leaving Z roll effected by physics/torque/drag, producing an interesting control result.
            // Set rollMethod=Torque and rotationMethod=Rotate to test. -- Though I've read that using Rotate() on a Torque controlled objct, can have unpredictable results.
            if (rotationType == rotationTypes.Torque) { rigidbody.AddRelativeTorque(Input.GetAxis("Mouse Y") * -mouseSensativity, Input.GetAxis("Mouse X") * mouseSensativity, 0); }
            else { transform.Rotate(Input.GetAxis("Mouse Y") * -mouseSensativity, Input.GetAxis("Mouse X") * mouseSensativity, 0); }
            if (Input.GetKey("q")) {
                if (rollMethod == rollMethods.Torque) { rigidbody.AddRelativeTorque(0, 0, -speed * Time.deltaTime); }
                else { transform.Rotate(0, 0, -speed * Time.deltaTime); }
            }
            if (Input.GetKey("e")) {
                if (rollMethod == rollMethods.Torque) { rigidbody.AddRelativeTorque(0, 0, speed * Time.deltaTime); }
                else { transform.Rotate(0, 0, speed * Time.deltaTime); }
            }
        }
    }
}

function ClampAngle (angle : float, min : float, max : float) {
    if (angle < -360) angle += 360;
    if (angle > 360) angle -= 360;
    return Mathf.Clamp (angle, min, max);
}

Hopefully this helps someone else sometime!

~Badger