- Home /
Raycast to align character on MeshCollider normals
Hello, I'm trying to move an object (let's say a character) on the surface of another object. I know that this subject has been dealt with in a lot of other posts, but I still couldn't find a fully working solution.
I've seen different approaches and tried them all with some good results, but I can't really be satisfied because there are still some problems and glitches.
So I've decided to break the whole thing into smaller tasks, hoping to find the final solution to this.
Here's what I have so far:
void Update ()
{
// Input translation
float inputTranslation = Input.GetAxis("Vertical") * moveSpeed * Time.deltaTime;
transform.position += transform.forward * inputTranslation;
RaycastHit hit;
if (Physics.Raycast (transform.position, -transform.up, out hit, Mathf.Infinity))
{
// Stick on surface
transform.position = hit.point;
// Align to surface normal
Quaternion fro = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation;
transform.rotation = Quaternion.Lerp(transform.rotation, fro, Time.deltaTime * 5.0f);
}
// Inpput heading rotation
float headingDeltaAngle = Input.GetAxis("Horizontal") * Time.deltaTime * headingRotSpeed;
Quaternion headingDelta = Quaternion.AngleAxis(headingDeltaAngle, transform.up);
transform.rotation = headingDelta * transform.rotation;
}
First task: placing the object (character) onto another object's surface.
Here it looks like pretty much easy stuff: cast a ray downwards from the character, get the hit point and update the character's position.
Also, I need the caracter to move through user input.
This kinda works, but it depends on the surface of the object: sometimes the characters get stuck and won't move anymore; other times it will just go through the object's mesh and pass on the other side (because the raycast gets another hit.point) or even better, it will keep travelling in the outer space...Off course this is not good: I need to find a more generic solution that will work (hopefully) with any arbitrary mesh.
So the first question is: is this a right approach to make the character stick on the object's surface, while also allowing user input to move it? Is it right to set the position of the character to hit.point or does it conflict with the input translation?
Also, how can I add a little offset between the character's transform and the object's surface? I already put the pivot below the character, but I guess I also need to offset the Y position a bit.
Second task: orient the character to the surface normal
This is kinda tricky for me, my vector and quaternion math is lame (to say the least). Anyway, from what I can see and read on Unity docs, Quaternion.FromToRotation rotates the transfom from “fromDirection” to “toDirection”. In this case, it rotates the transform from it's up direction to the direction of the hit.normal. Seems legit to me.
Then all this stuff is being interpolated with Quaternion.Lerp, going from the current object's rotation to the desired one (according to the surface normal) in a given amount of time.
I've seen different examples to do this (at least 4 different ways), but this one seems to me the one which is working better. Again, it's not perfect. Is there a way to improve it, or maybe is better to accomplish this task in a different way?
Third task: changing character's direction with right/left arrow keys
I don't know if this is the right approach; it seems to work fine but I'm not sure if it is causing any of the problems stated above.
Answer by robertbu · Jul 17, 2013 at 08:51 AM
I don't have a lot of experience moving characters over terrain, but in visualizing what is going on here, I have a few thoughts.
You are moving the character along the forward vector of the character (transform.forward), and you are Raycasting the character's down (-transform.up). I can visualize a number of situations where this combo will cause you trouble. For example at a sharp transition, the raycast will not hit the ground and therefore walk through the air, or it will drive the character into the ground and the Raycast() will fail because it starts below the surface of the terrain. I think you want to project the forward onto the XZ plane and use that for forward movement, and for Raycasting() you want to use Vector3.down from somewhere above the character.
float inputTranslation = Input.GetAxis("Vertical") * moveSpeed * Time.deltaTime;
Vector3 v3 = transform.forward;
v3.y = 0.0f;
transform.position += v3.normalized * inputTranslation;
And for the Raycast():
if (Physics.Raycast (transform.position + Vector3.up * some_dist, Vector3.down, out hit, Mathf.Infinity))
Note that unless you need to raycast against all the objects in the scene, you can use Collider.Raycast() instead of Physics.Raycast().
If you want to position the character above the terrain a bit more, then use the normal:
transform.position = hit.point + hit.normal * some_distance;
As for aligning with the normal, you might want to try Quaternion.RotateTowards() in place of the Lerp() for a different kind of movement.
Thank you for your help. I'm not trying to move the character over a terrain or XZ plane; I need to move him all around arbitrary meshes (they can be different shapes). Think about Super $$anonymous$$ario Galaxy or something like that.
If you want to handle arbitrary meshes, then you are going to to have to figure out how to handle the transitions. Note the scale of these 'zigs' don't have to be big nor does it have to be super sharp to cause your code issues:
At the top of the zig, your Raycast will miss the mesh and your character will head off into space. In the bottom, your Raycast() point can be inside the mesh.
You might be able to solve this issue by using a central attraction point either all the time or when your current Raycast fails. Your movement might need to use a vector tangent to the attraction vector rather than the forward vector.
this helped a lot:
transform.position = hit.point + hit.normal * some_distance;
and it's exactly what I was looking for. It's kinda working, though not perfect. Here's a video of how it's working:
As you can see, it's not flying around anymore, nor it's falling into the mesh, but it's jittering when it's near the edge of two faces with different inclination, and it gets quite "jumpy" when it's near a peak. Look like the situaion you described in your post.
Also, I've noticed that in this line:
transform.rotation = Quaternion.Lerp(transform.rotation, fro, Time.deltaTime * 2.0f);
the 2.0f value (the speed at which the character rotates from its previous rotation to the desired one) highly influences the overall behaviour: if it's too small or too high, the character keeps flying in the space or getting inside the mesh.
Any suggestions to fix the jittering and how to handle the Lerp thing? I tried with Quaterion.RotateTowards(), but it didn't work, the characters was flying or entering into the mesh.
You can see why the jitter is occurring. As you make the angle change from one surface to another, you rotate the cube. As you rotate the cube, the direction -transform.up gets angled back so that it is again on the previous face. The next thing I'd try is to change the Raycasting so that it was towards pivot point of the terrain object. Something like:
Vector3 v3 = transform.position - planet.position;
if (Physics.Raycast (transform.position + v3 * someDist, -v3, out hit, $$anonymous$$athf.Infinity)) {
...where 'planet' is the transform of the terrain you are driving around on, and 'someDist' is a float value to bring the raycast up a bit. The '+v3*someDist' might not be necessary.
You may also have to change the vector you use for forward movement.
hmm...not working; the character starts moving up (outside the mesh) as soon as I hit play. Here is the full Update() method:
void Update ()
{
// Input translation
float inputTranslation = Input.GetAxis("Vertical") * moveSpeed * Time.deltaTime;
transform.position += transform.forward * inputTranslation;
RaycastHit hit;
Vector3 v3 = transform.position - planet.transform.position;
if (Physics.Raycast (transform.position + v3 * 0.2f, -v3, out hit, $$anonymous$$athf.Infinity))
{
// Stick to surface
transform.position = hit.point + (hit.normal * 0.2f);
// Align to surface normal
Quaternion fro = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation;
transform.rotation = Quaternion.Slerp(transform.rotation, fro, Time.deltaTime * 2.0f);
}
else
{
Debug.Log("Lost contact...");
}
// Inpput heading rotation
float headingDeltaAngle = Input.GetAxis("Horizontal") * Time.deltaTime * headingRotSpeed;
Quaternion headingDelta = Quaternion.AngleAxis(headingDeltaAngle, transform.up);
transform.rotation = headingDelta * transform.rotation;
}
I also tried without "+v3 * 0.2f", but with no luck, the character is sliding on the surface (without user input) towards some point.