Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question Make Object follow any 3D Mesh

Discussion in 'Scripting' started by virusexgaming, Jun 1, 2023.

  1. virusexgaming

    virusexgaming

    Joined:
    Apr 3, 2019
    Posts:
    4
    Hey all, this is my first post and I am still very much a beginner when it comes to 3D programming.
    What I want to achieve is some code, that will semi randomly move an object across any surface mesh. So far I can move my Object (cube) and it does follow the mesh of a sphere (for testing) but either my method of achieving this is flawed or I am making some beginner mistakes in my code ..

    upload_2023-6-1_17-9-35.png

    This is how it looks when all is working. The cube follows the mesh, it does not turn yet but I'm sure that has to do with the normal of the Raycast hit I'm using .. but eventually the Raycast used to determine where on the mesh my object is and how I need to rotate it fails.
    I am countering that by raising the object along it's up-vector but that seems like a really bad solution. Or rather a fix for a problem that must ley somewhere else.

    upload_2023-6-1_17-17-45.gif

    As you can hopefully see in the GIF, the red Debug line flashes when the Raycast fails. I would assume the object (cube) is just too far inside the sphere? What would be the best way to prevent this for any given object, not just this particular cube?

    Also, if there is an easy fix to my cube not rotating to face "forward" I would love to hear about that too :)

    Here is my code:

    Code (CSharp - Script attatched to the object (cube)):
    1. public class Test : MonoBehaviour
    2. {
    3.     private float _nextRandomSteerTime;
    4.     private Vector3 _lastRandomSteerForce;
    5.     private Vector3 _currentVelocity;
    6.  
    7.     void Start()
    8.     {
    9.         _currentVelocity = transform.forward * 2;
    10.     }
    11.  
    12. void Update()
    13. {
    14.     var myAcceleration = 2f;
    15.     var maxSpeed = 2f;
    16.     var maxRandom = 6f;
    17.  
    18.     if (_lastRandomSteerForce == null || Time.time > _nextRandomSteerTime)
    19.     {
    20.         // I copy pasted this, I am not sure why we devide by 3 here?
    21.         _nextRandomSteerTime = Time.time + Random.Range(maxRandom / 3, maxRandom);
    22.         _lastRandomSteerForce = RandomSteerForce.GetRandomDir(transform.forward) * maxSpeed;
    23.     }
    24.  
    25.     Vector3 desiredVelocity = _lastRandomSteerForce.normalized * 2;
    26.     Vector3 steeringForce = desiredVelocity - _currentVelocity;
    27.     Vector3 acceleration = Vector3.ClampMagnitude(steeringForce * myAcceleration, myAcceleration);
    28.     _currentVelocity = Vector3.ClampMagnitude(_currentVelocity + acceleration * Time.deltaTime, maxSpeed);
    29.  
    30.     float moveDst = _currentVelocity.magnitude * Time.deltaTime;
    31.     Vector3 desiredPos = transform.position + _currentVelocity * Time.deltaTime;
    32.  
    33.     RaycastHit hit;
    34.     if (Physics.Raycast(desiredPos, -transform.up, out hit))
    35.     {
    36.         // I think i need to change this to achieve my object rotating properly in the forward direction
    37.         // I tried Vector3.Cross but it did absolutely not what I expected
    38.         transform.SetLocalPositionAndRotation(hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
    39.     }
    40.     else
    41.     {
    42.         transform.position = Vector3.Lerp(transform.position, transform.position+transform.up, Time.deltaTime);
    43.         Debug.DrawLine(desiredPos, -transform.up * 500, Color.red);
    44.     }
    45. }
    46. }
    Code (CSharp - some helper class function):
    1.  
    2. public static Vector3 GetRandomDir(Vector3 referenceDir)
    3.     {
    4.         Vector3 smallestRandomDir = Vector3.zero;
    5.         float change = -1;
    6.         const int iterations = 4;
    7.         for (int i = 0; i < iterations; i++)
    8.         {
    9.             Vector3 randomDir = Random.onUnitSphere.normalized;
    10.             float dot = Vector3.Dot(referenceDir, randomDir);
    11.             if (dot > change)
    12.             {
    13.                 change = dot;
    14.                 smallestRandomDir = randomDir;
    15.             }
    16.         }
    17.         Debug.DrawLine(smallestRandomDir, smallestRandomDir.normalized * 5000, Color.yellow, 10);
    18.         // don't change y angle, raycast hit normal should do that, right?
    19.         return new Vector3(smallestRandomDir.x, 0, smallestRandomDir.z);
    20.     }
    21.  
    Thanks a lot in advance!
     
    Last edited: Jun 1, 2023
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,520
  3. virusexgaming

    virusexgaming

    Joined:
    Apr 3, 2019
    Posts:
    4
    Hey Kurt,

    thanks a lot for the reply. I checked your code and if I am not mistaken you are doing something very similar to what I am trying, the difference being that you're using 3 points to Raycast from, then creating a plane between those points to determine the average "up" of that plane. Also you calculate the new forward Vector from your 3 Raycast hits.

    I belive I am doing exactly that (or I'm trying at least ..) just with a single Raycast .. shouldn't that work also? It might be less appropriate for larger objects like the tank in your example but shouldn't it in theory work anyways?

    Here is what I tried after studying your code:
    Code (CSharp):
    1.  
    2. // Step1: Calculate the new position
    3. Vector3 desiredPos = transform.position + _currentVelocity * Time.deltaTime;
    4.  
    5. // Step2: Raycast at the new position in the objects current down direction
    6. RaycastHit hit;
    7. if (Physics.Raycast(desiredPos, -transform.up, out hit))
    8. {
    9.     // Rotate in the new forward direction (_currentVelocity) along the new up axis (hit.normal)
    10.     transform.LookAt(transform.position + _currentVelocity, hit.normal);
    11.     // add hit.normal*.1f to prevent clipping through the floor
    12.     transform.position = hit.point + hit.normal * .01f;
    13. }
    14.  
    upload_2023-6-2_22-46-51.gif

    As you can hopefully see in my crude little GIF, rotating in and by its self is working, it's just the matching up of the hit.normal (red line) with the objects transform.up (green arrow) that I can't figure out.

    Can you spot any obvious flaw in my code or my attempt in solving this in general?

    Thanks a lot! :)
     
  4. virusexgaming

    virusexgaming

    Joined:
    Apr 3, 2019
    Posts:
    4
    I hope it is ok for me to reply to myself. I figured out the solution and thought others might find it useful in the future as well. Here is the rather simple code to make an object follow a mesh surface while applying random steering as well:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class RandomWalkMovement : MonoBehaviour
    4. {
    5.     public float MoveSpeed = 5f;
    6.     public float RotationInterval = 2f;
    7.     public float MaxRotationAngle = 45f;
    8.     public float RaycastDistance = 1f;
    9.     public LayerMask GroundLayerMask;
    10.  
    11.     private bool _isMoving = false;
    12.     private float _nextRandomSteerTime;
    13.     private float _rotationSpeed;
    14.     private Vector3 _targetSteerForce;
    15.  
    16.     private void Update()
    17.     {
    18.         _rotationSpeed = MoveSpeed / 3;
    19.         MoveForward();
    20.     }
    21.  
    22.     private void MoveForward()
    23.     {
    24.         if (Physics.Raycast(transform.position, -transform.up, out RaycastHit hit, Mathf.Infinity, GroundLayerMask))
    25.         {
    26.             var surfaceNormal = hit.normal;
    27.             ApplyRandomRotation(surfaceNormal);
    28.  
    29.             transform.SetPositionAndRotation(
    30.                 transform.position + (_targetSteerForce * MoveSpeed * Time.deltaTime),
    31.                 Quaternion.Slerp(
    32.                     transform.rotation, Quaternion.LookRotation(_targetSteerForce, surfaceNormal),
    33.                     _rotationSpeed * Time.deltaTime
    34.                 )
    35.             );
    36.  
    37.             StickToGround(hit);
    38.         }
    39.         else
    40.             Debug.DrawLine(transform.position, -transform.up * 10, Color.red);
    41.     }
    42.  
    43.     private void ApplyRandomRotation(Vector3 surfaceNormal)
    44.     {
    45.         if (!_isMoving || Time.time > _nextRandomSteerTime)
    46.         {
    47.             _isMoving = true;
    48.             _nextRandomSteerTime = Time.time + Random.Range(1f, 2f);
    49.  
    50.             float randomAngle = Random.Range(-MaxRotationAngle, MaxRotationAngle);
    51.             Quaternion randomRotation = Quaternion.AngleAxis(randomAngle, surfaceNormal);
    52.  
    53.             var localForward = transform.TransformDirection(Vector3.forward);
    54.             _targetSteerForce = Vector3.ProjectOnPlane(randomRotation * localForward, surfaceNormal).normalized;
    55.         }
    56.     }
    57.  
    58.     private void StickToGround(RaycastHit hit)
    59.     {
    60.         float distanceToGround = hit.distance;
    61.         Vector3 targetPosition = transform.position + (transform.up * (RaycastDistance - distanceToGround));
    62.         transform.position = targetPosition;
    63.     }
    64. }
    65.