I had the same problem: assigning transform.up seems to be equivalent to transform.rotation = Quaternion.FromToRotation(Vector3.up, newNormal). The solution for me was to keep a “compass” variable which showed the current rotation angle from forward: thus I “rotated” this angle and applied to the object using Euler:
float curDir = 0f; // compass indicating direction
float vertSpeed = 0f; // vertical speed (see note)
...
void Update(){
float turn = Input.GetAxis("Horizontal") * turnSpeed * 100 * Time.deltaTime,
curDir = (curDir + turn) % 360; // rotate angle modulo 360 according to input
if (Physics.Raycast(transform.position, -transform.up, out hit))
{
Quaternion grndTilt = Quaternion.FromToRotation(transform.up, hit.normal);
transform.rotation = grndTilt * Quaternion.Euler(0, curDir, 0);
}
Vector3 movDir;
movDir = transform.forward*Input.GetAxis("Vertical")*speed;
// moves the character in horizontal direction (gravity changed!)
if (controller.isGrounded) vertSpeed = 0; // zero v speed when grounded
vertSpeed -= 9.8f * Time.deltaTime; // apply gravity
movDir.y = vertSpeed; // keep the current vert speed
controller.Move(movDir*Time.deltaTime);
...
An alternative way to do that is using the OnControllerColliderHit event to get the normal - but only if its Y coordinate is > 0.3 (or some other suitable value) or else the car can stick to walls or to other vehicles during lateral collisions!
NOTE: I changed the way gravity is applied - it gives a better behaviour when “flying” after a hill. Discard these changes if you don’t want them - the rotation thing is at the beginning, and has nothing to do with gravity.
EDITED: There was an error in the script above: the rotation was being calculated from transform.up to the normal (it should be Vector3.up to normal) what was causing the crazy instability.
In my tests, I also noticed that the vehicle was following the terrain normal immediately, what was producing a strange behaviour. I added a new variable - curNormal - which smoothly followed the terrain normal using Lerp, and also was used in the Raycast to avoid other instabilities. The result is very convincent - hope you like it too:
float curDir = 0f; // compass indicating direction
float vertSpeed = 0f; // vertical speed (see note)
Vector3 curNormal = Vector3.up; // smoothed terrain normal
void Update(){
float turn = Input.GetAxis("Horizontal") * turnSpeed * 100 * Time.deltaTime;
curDir = (curDir + turn) % 360; // rotate angle modulo 360 according to input
RaycastHit hit;
if (Physics.Raycast(transform.position, -curNormal, out hit)){
curNormal = Vector3.Lerp(curNormal, hit.normal, 4*Time.deltaTime);
Quaternion grndTilt = Quaternion.FromToRotation(Vector3.up, curNormal);
transform.rotation = grndTilt * Quaternion.Euler(0, curDir, 0);
}
Vector3 movDir;
movDir = transform.forward*Input.GetAxis("Vertical")*speed;
// moves the character in horizontal direction (gravity changed!)
if (controller.isGrounded) vertSpeed = 0; // zero v speed when grounded
vertSpeed -= 9.8f * Time.deltaTime; // apply gravity
movDir.y = vertSpeed; // keep the current vert speed
controller.Move(movDir*Time.deltaTime);
}
JAVASCRIPT VERSION:
private var curDir: float = 0f; // compass indicating direction
private var vertSpeed: float = 0f; // vertical speed (see note)
private var curNormal = Vector3.up; // smoothed terrain normal
function Update(){
var turn = Input.GetAxis("Horizontal") * turnSpeed * 100 * Time.deltaTime;
curDir = (curDir + turn) % 360; // rotate angle modulo 360 according to input
var hit: RaycastHit;
if (Physics.Raycast(transform.position, -curNormal, hit)){
curNormal = Vector3.Lerp(curNormal, hit.normal, 4*Time.deltaTime);
var grndTilt = Quaternion.FromToRotation(Vector3.up, curNormal);
transform.rotation = grndTilt * Quaternion.Euler(0, curDir, 0);
}
var movDir = transform.forward*Input.GetAxis("Vertical")*speed;
// moves the character in horizontal direction (gravity changed!)
if (controller.isGrounded) vertSpeed = 0; // zero v speed when grounded
vertSpeed -= 9.8f * Time.deltaTime; // apply gravity
movDir.y = vertSpeed; // keep the current vert speed
controller.Move(movDir*Time.deltaTime);
}