Why does my fly though camera flip over on the first call of Input.GetAxis("Mouse X") (or Y)?

I have a script attached to the main camera that I use to do a fly through of my game area. It works fine except that on the first move of the mouse, (used for pitch and yaw), the camera flips over. Scrubbing the mouse around a bit brings it back into proper orientation, and then the code works as expected, including using the mouse, after that. The camera is set to proper orientation at start and the view is correct until the first mouse move. I’ve done a whole lot of searching here and elsewhere, but, nothing quite seems to explain this. Any pointers would be appreciated. The code follows:

// FlyThough.cs  
// Alan Swithenbank  
// Version 0.0.1  
// September 2013  
  
// Attach to Main Camera to provide keyboard/mouse navigation (NOTE 01)  
// through the 3D data space generated by {turtle,shark}TrackRend.cs,   
// with terrain avoidance (NOTE 02). Controls are as follows:  
//  
//             W/S or Up-Arrow/Down-Arrow: Z motion in/out (depth)  
//  
//          A/D or Left-Arrow/Right-Arrow: X motion left/right (width)  
//  
//                                    Q/E: Y motion down/up (altitude)  
//  
//                                    Tab: Increase speed  
//  
//                             Left-Shift: Decrease speed  
//  
//                       Mouse Left/Right: Yaw (horizontal rotation)  
//  
//                  Mouse forward/Reverse: Pitch (vertical rotation)  
//  
//                                   Home: Reset camera to start position  
//    
//                                    End: Toggle cursor lock on screen  
//  
//                                 Escape: Application quit  
//  
//                               <ctrl>-P: Toggle play mode  


// NOTES:  
// ------  
  
// NOTE 01: This code is loosely based on ExtendedFlyCam.cs by Desi Quintans,  
//          CowfaceGames.com, 17 August 2012, released with Free as in speech,  
//          and Free as in beer license. Added start position set on Awake().  
//          Added terrain avoidance (NOTE 02). Updated speed control to work  
//          on all axes. Added escape key game exit.  
  
// NOTE 02: Terrain avoidance is via Physics.Raycast(). The basic method is  
//          allow motion until the camera approaches within 10 meters of the  
//          bathymetry terrain, either forward or down. On a 10 meter approach  
//          stop motion, but allow rotations to reorient so forward and down  
//          directions are clear by more than 10 meters. Once the directions  
//          are clear allow motion again. Only the bathymetry terrain is  
//          involved in collisions (NOTE 03).  
  
// NOTE 03: Along with the bathymetry terrain, the water surface planes are  
//          collider objects and could interfer in collision detection ray  
//          casts. The most general way to ignore collider objects is assign  
//          them to a layer and use a layer mask in the Physics.Raycast() call  
//          to ignore those layers. Here just assigning the water surfaces to  
//          the "Ignore Raycast" layer in their Inspector is sufficient to  
//          ensure only the bathymetry terrain is the only active collider.  
//          None of the displayed data objects have colliders so they are  
//          invisible to ray casts by default.  
  

using UnityEngine;  
using System.Collections;

public class FlyThrough : MonoBehaviour {

	public float cameraSensitivity = 120;
	public float climbSpeed = 10;
	public float normalMoveSpeed = 15;
	public float slowMoveFactor = 0.25f;
	public float fastMoveFactor = 8;
 
	private float rotationX;
	private float rotationY;
    
	private Camera mainCamera;
	private Vector3 homePos;
    private Vector3 homeRot;
	
    private RaycastHit camhit = new RaycastHit();
    private Vector3 rayStart;
	private Vector3 rayDirFwd;
	private Vector3 rayDirDwn;

	public float cast_distance = 10f; // public for runtime adjustment test

	void Awake() {

		// Get the Main Camera GameObject (as a Camera:
		
		mainCamera = GameObject.Find("Main Camera").camera;

		// Set start position:
		
		homePos = new Vector3(903.8f,294.6f,707.0f);
		
		// Set start rotation:
	
		homeRot = new Vector3(36.5f,211.0f,4.6f);

		// Move to start position/rotation:

		MoveCameraPosRot (mainCamera,homePos,homeRot);

	}
	
	
	void Start () {		
		
		Screen.lockCursor = true;
		
	}


	void Update () {
		
		// Bail out on escape key (build game):
		
		if (Input.GetKeyDown(KeyCode.Escape)) {
			
			Application.Quit();
			
		}
		
		// Terrain avoidance: Always allow rotations:

		if (    Input.GetAxis("Mouse X") > 0 || Input.GetAxis("Mouse X") < 0
			||  Input.GetAxis("Mouse Y") > 0 || Input.GetAxis("Mouse Y") < 0) {
				
			// This rotation method almost works. Don't allow rotation updates unless
		    // there is a change in the mouse input. The camera stays in the correct
			// orientation on start, but, first motion of mouse takes a large jump. 
			// Can rotate back into place with mouse and then all is well.

			rotationX += Input.GetAxis("Mouse X") * cameraSensitivity * Time.deltaTime;
			rotationY += Input.GetAxis("Mouse Y") * cameraSensitivity * Time.deltaTime;
			rotationY = Mathf.Clamp (rotationY, -90, 90);
 
			transform.localRotation = Quaternion.AngleAxis(rotationX, Vector3.up);
			transform.localRotation *= Quaternion.AngleAxis(rotationY, Vector3.left);
			
		}

		// Terrain avoidance: Stop translations if forward direction or down direction within
		// 10 m of terrain:

		// assign the ray start point to the current camera position:

		// (Yes, for a script attached to Main Camera the mainCamera is
		// superflous, but, I like to do it anyway.)

		rayStart = mainCamera.transform.position;

		// assign ray directions from the camera transform:

		rayDirFwd = mainCamera.transform.forward;

		rayDirDwn = -mainCamera.transform.up;

		// make the cast to out to cast_distance meters:  

//		if (!(     Physics.Raycast(rayStart, rayDirFwd, out camhit, cast_distance)  
//			  ||  Physics.Raycast(rayStart, rayDirDwn, out camhit, cast_distance))) { // not using camhit now  

		if (!(     Physics.Raycast(rayStart, rayDirFwd, cast_distance)
			  ||  Physics.Raycast(rayStart, rayDirDwn, cast_distance))) {

	 		if (Input.GetKey (KeyCode.Tab)) {                // fast speed moves
						
				transform.position += transform.forward * (normalMoveSpeed * fastMoveFactor) * Input.GetAxis("Vertical") * Time.deltaTime;
				transform.position += transform.right * (normalMoveSpeed * fastMoveFactor) * Input.GetAxis("Horizontal") * Time.deltaTime;

				if (Input.GetKey (KeyCode.E)) {transform.position += transform.up * climbSpeed * fastMoveFactor * Time.deltaTime;}
				if (Input.GetKey (KeyCode.Q)) {transform.position -= transform.up * climbSpeed * fastMoveFactor *  Time.deltaTime;}

			} else if (Input.GetKey (KeyCode.LeftShift)) {   // slow speed moves

				transform.position += transform.forward * (normalMoveSpeed * slowMoveFactor) * Input.GetAxis("Vertical") * Time.deltaTime;
				transform.position += transform.right * (normalMoveSpeed * slowMoveFactor) * Input.GetAxis("Horizontal") * Time.deltaTime;
	 	
				if (Input.GetKey (KeyCode.E)) {transform.position += transform.up * climbSpeed * slowMoveFactor *  Time.deltaTime;}
				if (Input.GetKey (KeyCode.Q)) {transform.position -= transform.up * climbSpeed * slowMoveFactor *  Time.deltaTime;}
			
			} else {                                         // normal speed moves
			
	 			transform.position += transform.forward * normalMoveSpeed * Input.GetAxis("Vertical") * Time.deltaTime;
				transform.position += transform.right * normalMoveSpeed * Input.GetAxis("Horizontal") * Time.deltaTime;

				if (Input.GetKey (KeyCode.E)) {transform.position += transform.up * climbSpeed * Time.deltaTime;}
				if (Input.GetKey (KeyCode.Q)) {transform.position -= transform.up * climbSpeed * Time.deltaTime;}

			}
		}

		// This moves the camera to homePos, but flips it to reverse start rotation, not the given rotation...
				
		if (Input.GetKeyDown (KeyCode.Home)) {
	
	       MoveCameraPosRot (mainCamera,homePos,homeRot);
			
		}

		if (Input.GetKeyDown (KeyCode.End)) {
			Screen.lockCursor = (Screen.lockCursor == false) ? true : false;
		}

	}
	
	
	void MoveCameraPosRot (Camera cam, Vector3 pos, Vector3 rot) {

		// Set position:
		
		cam.transform.position = pos;
		
		// Set rotation:
	
		cam.transform.eulerAngles = rot;

	}
	

}

OK…get to answer my own question again…at least a few people do seem to look at them, so I guess that’s something…;^)

I would think that the problem I was having was some kind of gimbal lock issue and using quaternions, which are supposed to be immune to it, would have taken care of it. Apparently not. I switched to Transform.Rotate type operations, and got rid of the first mouse move camera flip. With the switch to rotate operations the flying camera can accumulate some roll when sharp turns with simultaneous pitch and yaw are made, particularly at high speed. I included some roll compensating torque by adding a RigidBody to the camera and using the physics system function rigidbody.AddRelativeTorque. It isn’t perfect, but, keeps things under control enough that it is easy to get back into proper alignment with a few mouse moves, even while in flight at high speed. My work is scientific visualization, not alien chasing, so, the way it works now is not a problem for me.

I’ve pasted my flight control script below. Hopefully someone may find it useful. It’s fairly complete, including terrain avoidance in flight mode, and a GUI control mode where mouse movements do not affect the camera, allowing mouse cursor left clicks to control buttons in a GUI HUD. You just hold the right mouse button down to use the flight controls. There are lots of comments in the code explaining how it works.

// FlyThough.cs
// Alan Swithenbank
// Version 0.0.3
// October 2013

// Attach to Main Camera to provide keyboard/mouse navigation (NOTE 01)
// through the 3D data space generated by {turtle,shark}TrackRend.cs, 
// with terrain avoidance (NOTE 02). Controls are as follows:
//
//             W/S or Up-Arrow/Down-Arrow: Z motion in/out (depth)
//
//          A/D or Left-Arrow/Right-Arrow: X motion left/right (width)
//
//                                    Q/E: Y motion down/up (altitude)
//
//                                    Tab: Increase speed
//
//                             Left-Shift: Decrease speed
//
//                       Mouse Left/Right: Yaw (horizontal rotation)
//
//                  Mouse forward/Reverse: Pitch (vertical rotation)
//
//                                   Home: Reset camera to start position
//
//                                 Escape: Application (built game) quit 
//
//                               <ctrl>-P: Toggle play mode


// NOTES:
// ------

// NOTE 01: This code is loosely based on ExtendedFlyCam.cs by Desi Quintans,
//          CowfaceGames.com, 17 August 2012, released with Free as in speech,
//          and Free as in beer license. Added start position set on Awake().
//          Added home key position reset. Added terrain avoidance (NOTE 02).
//          Updated speed control to work on all axes. Added escape key build
//          game exit. Fixed an issue with camera flip on first mouse move
//          (NOTE 03). Added right mouse button activation of flight controls
//          and automatic turn on of screen lock when in flight mode (NOTE 04).
//          Added torque compensation to control roll induced by combined 
//          pitch and yaw moves (NOTE 03, NOTE 05).

// NOTE 02: Terrain avoidance is via Physics.Raycast(). The basic method is
//          allow motion until the camera approaches within 10 meters of the
//          bathymetry terrain, either forward or down. On a 10 meter approach
//          stop motion, but allow rotations to reorient so forward and down
//          directions are clear by more than 10 meters. Once the directions
//          are clear allow motion again. Only the bathymetry terrain is
//          involved in collisions (NOTE 06).

// NOTE 03: Changing the transform.localRotation = (*=) Quaternion.AngleAxis()
//          calls, (see V0.0.1 this code), to the transform.Rotate() function
//          solved the initial mouse move camera flip problem. (Note: first used
//          transform.RotateAroundLocal(), which works, but that has been
//          depricated in favor of transform.Rotate().) This also changed the 
//          rotation value computation from an incrementing type (+=) to a 
//          simple value (=) form, thus eliminating the need for a test looking
//          for mouse position changes in the Update() function. Why a change 
//          to the transform.Rotate() function worked isn't entirely clear
//          since the original problem looked possibly like gimbal lock and 
//          quaternions are supposed to be immune to that. But, it does. The 
//          method with Rotate() will show some undesirable roll if you get too
//          wild scrubbing the mouse around in flight, but, it can be backed
//          out with some opposite mouse action. This is mainly only a problem 
//          if you do pitch and yaw simultaneously in a tight turn and can be
//          greatly eliminated by adding corrective torque (NOTE 05).

// NOTE 04: In anticipation of adding left mouse cursor click controls to the 
//          HUD GUI, add right mouse button activation to the flight controls
//          Pitch and yaw control via mouse are the critical ones, but all
//          flight controls only work when the right mouse button is held down.
//          In previous versions of the code the cursor screen lock could be 
//          toggled off and on with the 'end' key, allowing and the mouse
//          cursor to be used in the menus. Starting with version 0.0.3 the
//          lock is turned on automatically when in flight mode (right mouse
//          button down) and turned off automatically when in HUD control mode
//          (right mouse button up). This allows the mouse cursor and left
//          mouse clicks to control program features while removing the mouse
//          cursor from the scene when in fight mode.

// NOTE 05: A roll compensating torque can be applied to the main camera via 
//          the physics system through an attached RigidBody. This helps keep
//          the camera level after coming out of a turn with simultaneous 
//          pitch and yaw control applied (NOTE 03). Add a RigidBody to the
//          main camera by selecting the main camera in the hierarchy panel,
//          then from the menu select: Component -> Physics -> RigidBody.
//          The RigidBody is set up by setting 'Drag' and 'Angular Drag' to 0,
//          deselecting the 'Use Gravity' button, (so the camera doesn't sink 
//          when not in flight), and, in Constraints, select 'Freeze Rotation'
//          for X, Y, and Z, (so the added torque doesn't continue spinning the
//          camera when motion is stopped). The 'Is Kinematic' button should be
//          left unselected. A more complicated system than a single camera 
//          with a RigidBody might be subject to jitter if torque is applied in
//          the Update() function, so, in general should be applied in the
//          FixedUpdata() function. That is done here, though it would work
//          here in Update() as well. The value of torque applied was obtained
//          heuristically.

// NOTE 06: Along with the bathymetry terrain, the water surface planes are
//          collider objects and could interfere in collision detection ray
//          casts. The most general way to ignore collider objects is assign
//          them to a layer and use a layer mask in the Physics.Raycast() call
//          to ignore those layers. Here just assigning the water surfaces to
//          the "Ignore Raycast" layer in their Inspector is sufficient to
//          ensure only the bathymetry terrain is the only active collider.
//          None of the displayed data objects have colliders so they are
//          invisible to ray casts by default.


// VERSION HISTORY:
// ----------------

// V0.0.1, September 2013: Project castAR_sharks_00_421: First working version. 

// V0.0.2, October 2013: Project castAR_sharks_01_421: Recognizing the project
//         update with a code version update. Fixed the first mouse move 
//         causing the camera to flip problem (NOTE 03). Change camera speed 
//         control parameters and terrain avoidance parameter from public to
//         private since the current choices seem to be good so there is no 
//         longer a need to allow changing them.

// V0.0.3, October 2013: Project castAR_sharks_01_421: Add right mouse button
//         activation of flight control and update screen lock to activate
//         with right mouse button hold (NOTE 04). Continue work on mouse based
//         pitch and yaw control update (NOTE 03); adding torque compensation
//         to prevent unwanted roll (NOTE 05). 


// ToDo:
// -----

// Try camera zoom someday...


using UnityEngine;
using System.Collections;

public class FlyThrough : MonoBehaviour {

	private float cameraSensitivity = 120;
	private float climbSpeed = 10;
	private float normalMoveSpeed = 15;
	private float slowMoveFactor = 0.25f;
	private float fastMoveFactor = 8;
 
	private float rotationX;
	private float rotationY;
    
	private Camera mainCamera;
	private Vector3 homePos;
    private Vector3 homeRot;
	
    // private RaycastHit camhit = new RaycastHit();
    private Vector3 rayStart;
	private Vector3 rayDirFwd;
	private Vector3 rayDirDwn;

	private float cast_distance = 10f; // terrain aviodance distance 
	
	private int lftmouse = 0;
	private int rgtmouse = 1;
	
	private bool btndown;
	
	void Awake() {

		// Get the Main Camera GameObject (as a Camera:
		
		mainCamera = GameObject.Find("Main Camera").camera;

		// Set start position:
		
		homePos = new Vector3(903.8f,294.6f,707.0f);
		
		// Set start rotation:
	
		homeRot = new Vector3(36.5f,211.0f,4.6f);

		// Move to start position/rotation:

		MoveCameraPosRot (mainCamera,homePos,homeRot);

	}
	
	
	
	void Start () {		
	
		// unlock cursor when start since assume won't be pushing the 
		// right mouse button then (NOTE 04):
		
		Screen.lockCursor = false;
		
	}

	void Update () {
		
		// Bail out on escape key (build game control):
		
		if (Input.GetKeyDown(KeyCode.Escape)) {	
			Application.Quit();
		}
		
		// Check for right mouse button pressed (NOTE 04):
		
		// NOTE: can not try for efficency presetting btndown false and only
		//       set true on GetMouseButtonDown, as then btndown gets reset
		//       with each Update() and flight control don't work. The formal
		//       check for GetMouseButtonUp and setting btndown false only then
		//       is necessary. Also, it provides a convenient automatic way of 
		//       toggling the screen cursor lock when switching between flight
		//       controls and HUD controls.
		
        if (Input.GetMouseButtonDown(rgtmouse)) {
          btndown = true;
          Screen.lockCursor = true;			
		} else if (Input.GetMouseButtonUp(rgtmouse)) {
		  btndown = false;
		  Screen.lockCursor = false;
		}
		
		// Only allow flight control when right mouse button is pressed (NOTE 04):
		
		if (btndown) {
		
  		  // Terrain avoidance: Always allow rotations:
		
          rotationX = Input.GetAxis("Mouse X") * cameraSensitivity * Time.deltaTime;
		  rotationX = Mathf.Clamp (rotationX, -90, 90);
          rotationY = Input.GetAxis("Mouse Y") * cameraSensitivity * Time.deltaTime;
		  rotationY = Mathf.Clamp (rotationY, -90, 90);
		
		  transform.Rotate(mainCamera.transform.up, Mathf.Deg2Rad * rotationX);  
		
		  // the -rotationY below gives inverted (real airplane) pitch control
			
          transform.Rotate(mainCamera.transform.right, Mathf.Deg2Rad * -rotationY );
				
		  // Terrain avoidance: Stop translations if forward direction or down direction 
		  // comes within cast_distance (as of V0.0.3 10 m) of the terrain:

		  // assign the ray start point to the current camera position:

		  // (Yes, for a script attached to Main Camera the mainCamera is
		  // superflous, but, I like to do it anyway.)

		  rayStart = mainCamera.transform.position;

		  // assign ray directions from the camera transform:
		
		  rayDirFwd = mainCamera.transform.forward;

		  rayDirDwn = -mainCamera.transform.up;

		  // make the cast to out to cast_distance meters:

//		    if (!(     Physics.Raycast(rayStart, rayDirFwd, out camhit, cast_distance)
//			      ||  Physics.Raycast(rayStart, rayDirDwn, out camhit, cast_distance))) { // not using camhit now

		  if (!(     Physics.Raycast(rayStart, rayDirFwd, cast_distance)
   	             ||  Physics.Raycast(rayStart, rayDirDwn, cast_distance))) {

	 	    if (Input.GetKey (KeyCode.Tab)) {                // fast speed moves

			  transform.position += transform.forward * (normalMoveSpeed * fastMoveFactor) * Input.GetAxis("Vertical") * Time.deltaTime;
		  	  transform.position += transform.right * (normalMoveSpeed * fastMoveFactor) * Input.GetAxis("Horizontal") * Time.deltaTime;

			  if (Input.GetKey (KeyCode.E)) {transform.position += transform.up * climbSpeed * fastMoveFactor * Time.deltaTime;}
			  if (Input.GetKey (KeyCode.Q)) {transform.position -= transform.up * climbSpeed * fastMoveFactor *  Time.deltaTime;}

		    } else if (Input.GetKey (KeyCode.LeftShift)) {   // slow speed moves

			  transform.position += transform.forward * (normalMoveSpeed * slowMoveFactor) * Input.GetAxis("Vertical") * Time.deltaTime;
			  transform.position += transform.right * (normalMoveSpeed * slowMoveFactor) * Input.GetAxis("Horizontal") * Time.deltaTime;
	 	
			  if (Input.GetKey (KeyCode.E)) {transform.position += transform.up * climbSpeed * slowMoveFactor *  Time.deltaTime;}
			  if (Input.GetKey (KeyCode.Q)) {transform.position -= transform.up * climbSpeed * slowMoveFactor *  Time.deltaTime;}
			
		    } else {                                         // normal speed moves
					
	 		  transform.position += transform.forward * normalMoveSpeed * Input.GetAxis("Vertical") * Time.deltaTime;
			  transform.position += transform.right * normalMoveSpeed * Input.GetAxis("Horizontal") * Time.deltaTime;

			  if (Input.GetKey (KeyCode.E)) {transform.position += transform.up * climbSpeed * Time.deltaTime;}
			  if (Input.GetKey (KeyCode.Q)) {transform.position -= transform.up * climbSpeed * Time.deltaTime;}

		    }

          }
			
		}  // end if (btndown)	
	

		// Return camera to start position on 'home' key:
				
		if (Input.GetKeyDown (KeyCode.Home)) {
	       MoveCameraPosRot (mainCamera,homePos,homeRot);
		}
				
	}
	
	// Add a heruistically derived torque to compenstate for roll (NOTE 05):
	
	void FixedUpdate () {
	  rigidbody.AddRelativeTorque(Camera.main.transform.right * rotationX * 0.5f);
	}
	
	// Set camera postion and rotation:
	
	void MoveCameraPosRot (Camera cam, Vector3 pos, Vector3 rot) {

		// Set position:
		
		cam.transform.position = pos;
		
		// Set rotation:
	
		cam.transform.eulerAngles = rot;

	}
	

}