Search Unity

Question Help Needed - Camera Retrieve & Align To Surface Normals From Character Controller

Discussion in 'Scripting' started by DiamondEater, Mar 15, 2023.

  1. DiamondEater

    DiamondEater

    Joined:
    Feb 6, 2022
    Posts:
    8
    I have been pestering ChatGPT, and ironically, I don’t think I have enough of a hang of scripting to know what to ask it to get the most effective help.

    I have been following CatLikeCoding’s tutorial series on movement and have stopped to play around a bit on his Climbing Tutorial.

    My goal is to get the camera’s normal vector to match the averaged normals contact points that the character controller is touching while climbing.

    I actually reached out to him and got an answer back (and got quite a bit farther as a result) but it's not his job to be my personal tutor so I figure I should put out my question to the wider community.

    CatLikeCoding’s scripts get a bit complex for me to be able to keep track, so for the sake of my learning, I have gone back to an earlier tutorial (specifically Chapter 2: Physics) that is a little more simple and tried to isolate the parts I need in a second script attached to the character controller and am trying to get just that smaller script to talk to the camera.

    I understand I can use the part of the script that helps the character controller know what direction to push out in when you jump on a slope, and I think I’ve picked that part out. I think I have gotten the camera script to retrieve that information but where I run into issues is getting the camera to align itself with the normal that the EvaluateCollision method is returning.

    If I were try to guess at what is going on, I think the camera's alignment is being overridden by something else in its script. But I am not seeing what...

    Below are the two scripts I am trying to get to talk to one another.
    BTW Here are the links to the original scripts he wrote if this helps catch a mistake I may have made:
    CHARACTER CONTROLLER (This is the script for CH 2 and not CH 8 on Climbing)
    CAMERA

    Here is my isolated script:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class FindingNormal : MonoBehaviour
    6. {
    7.     // vvv used in void EvaluateCollision() vvv
    8.     float minGroundDotProduct = 0.9f;
    9.     // vvv used in void EvaluateCollision() vvv
    10.     int groundContactCount = 0;
    11.     // vvv used in void EvaluateCollision() vvv
    12.     public Vector3 contactNormal = Vector3.zero;
    13.  
    14.     //Are we touching anthing?
    15.     void OnCollisionEnter (Collision collision)
    16.     {    EvaluateCollision(collision);
    17.     }
    18.     //Are we still touching anything?
    19.     void OnCollisionStay (Collision collision)
    20.     {    EvaluateCollision(collision);
    21.     }
    22.  
    23.     // I aim to find the normal(s) of what the sphere is touching and tell it to another script
    24.     public void EvaluateCollision (Collision collision)
    25.     {    if (collision != null)
    26.         {    int contactCount = collision.contactCount;
    27.         }
    28.         for (int i = 0; i < collision.contactCount; i++)
    29.         {    Vector3 normal = collision.GetContact(i).normal;
    30.             if (normal.y >= minGroundDotProduct)
    31.             {    groundContactCount += 1;
    32.                 contactNormal += normal;
    33.             }
    34.         }
    35.     }
    36. }
    And here is the camera script with the little bits I’ve added in:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [RequireComponent(typeof(Camera))]
    4. public class OrbitCamera : MonoBehaviour
    5. {
    6.     [SerializeField]
    7.     Transform focus = default;
    8.  
    9.     [SerializeField, Range(1f, 20f)]
    10.     float distance = 5f;
    11.  
    12.     [SerializeField, Min(0f)]
    13.     float focusRadius = 5f;
    14.  
    15.     [SerializeField, Range(0f, 1f)]
    16.     float focusCentering = 0.5f;
    17.  
    18.     [SerializeField, Range(1f, 360f)]
    19.     float rotationSpeed = 90f;
    20.  
    21.     [SerializeField, Range(-89f, 89f)]
    22.     float minVerticalAngle = -45f, maxVerticalAngle = 45f;
    23.  
    24.     [SerializeField, Min(0f)]
    25.     float alignDelay = 5f;
    26.  
    27.     [SerializeField, Range(0f, 90f)]
    28.     float alignSmoothRange = 45f;
    29.  
    30.     [SerializeField, Min(0f)]
    31.     float upAlignmentSpeed = 360f;
    32.  
    33.     [SerializeField]
    34.     LayerMask obstructionMask = -1;
    35.  
    36.     Camera regularCamera;
    37.  
    38.     Vector3 focusPoint, previousFocusPoint;
    39.  
    40.     Vector3 normal;
    41.  
    42.     Vector2 orbitAngles = new Vector2(45f, 0f);
    43.  
    44.     float lastManualRotationTime;
    45.  
    46.     Quaternion gravityAlignment = Quaternion.identity;
    47.  
    48.     Quaternion orbitRotation;
    49.  
    50.     Vector3 CameraHalfExtends
    51.     {    get
    52.         {    Vector3 halfExtends;
    53.             halfExtends.y =
    54.             regularCamera.nearClipPlane *
    55.             Mathf.Tan(0.5f * Mathf.Deg2Rad * regularCamera.fieldOfView);
    56.             halfExtends.x = halfExtends.y * regularCamera.aspect;
    57.             halfExtends.z = 0f;
    58.             return halfExtends;
    59.         }
    60.     }
    61.  
    62.     public FindingNormal findingNormalScript;
    63.     private Collision collision;
    64.  
    65.     void Start()
    66.     {    findingNormalScript = FindObjectOfType<FindingNormal>();
    67.     }
    68.  
    69.     void Update()
    70.     {    //figure out what way is up - GPT
    71.         if (findingNormalScript != null && collision != null)
    72.         {    findingNormalScript.EvaluateCollision(collision);
    73.         }
    74.         // calculate the new camera rotation based on the collision normal GPT
    75.         Vector3 normal = findingNormalScript.contactNormal;
    76.         Quaternion gravityAlignment = Quaternion.LookRotation(-normal, transform.up);
    77.         transform.rotation = Quaternion.Slerp(transform.rotation, gravityAlignment, Time.deltaTime * rotationSpeed);
    78.     }
    79.  
    80.     void OnValidate ()
    81.     {    if (maxVerticalAngle < minVerticalAngle)
    82.         {    maxVerticalAngle = minVerticalAngle;
    83.         }
    84.     }
    85.  
    86.     void Awake ()
    87.     {    regularCamera = GetComponent<Camera>();
    88.         focusPoint = focus.position;
    89.         transform.localRotation = orbitRotation = Quaternion.Euler(orbitAngles);
    90.     }
    91.  
    92.     void LateUpdate ()
    93.     {    //UpdateGravityAlignment();
    94.         UpdateFocusPoint();
    95.         if (ManualRotation() || AutomaticRotation())
    96.         {    ConstrainAngles();
    97.             orbitRotation = Quaternion.Euler(orbitAngles);
    98.         }
    99.         Quaternion lookRotation = gravityAlignment * orbitRotation;
    100.  
    101.         Vector3 lookDirection = lookRotation * Vector3.forward;
    102.         Vector3 lookPosition = focusPoint - lookDirection * distance;
    103.  
    104.         Vector3 rectOffset = lookDirection * regularCamera.nearClipPlane;
    105.         Vector3 rectPosition = lookPosition + rectOffset;
    106.         Vector3 castFrom = focus.position;
    107.         Vector3 castLine = rectPosition - castFrom;
    108.         float castDistance = castLine.magnitude;
    109.         Vector3 castDirection = castLine / castDistance;
    110.  
    111.         if (Physics.BoxCast(castFrom, CameraHalfExtends, castDirection, out RaycastHit hit, lookRotation, castDistance, obstructionMask))
    112.         {    rectPosition = castFrom + castDirection * hit.distance;
    113.             lookPosition = rectPosition - rectOffset;
    114.         }
    115.         transform.SetPositionAndRotation(lookPosition, lookRotation);
    116.     }
    117.  
    118.     //once I get my personal 'up' up and running work it into this method
    119.     void UpdateGravityAlignment ()
    120.     {    Vector3 fromUp = gravityAlignment * Vector3.up;
    121.         //the below line used to call the GetUpAxis from the CustomGravity script
    122.         //Vector3 toUp = CustomGravity.GetUpAxis(focusPoint);
    123.         //But now we are going to get the normal from the FindingNormal script
    124.         Vector3 toUp = FindingNormal.contactNormal;
    125.  
    126.         float dot = Mathf.Clamp(Vector3.Dot(fromUp, toUp), -1f, 1f);
    127.         float angle = Mathf.Acos(dot) * Mathf.Rad2Deg;
    128.         float maxAngle = upAlignmentSpeed * Time.deltaTime;
    129.  
    130.         Quaternion newAlignment =
    131.         Quaternion.FromToRotation(fromUp, toUp) * gravityAlignment;
    132.         if (angle <= maxAngle)
    133.         {    gravityAlignment = newAlignment;
    134.         }
    135.         else
    136.         {    gravityAlignment = Quaternion.SlerpUnclamped(gravityAlignment, newAlignment, maxAngle / angle);
    137.         }
    138.     }
    139.  
    140.     void UpdateFocusPoint ()
    141.     {    previousFocusPoint = focusPoint;
    142.         Vector3 targetPoint = focus.position;
    143.         if (focusRadius > 0f)
    144.         {    float distance = Vector3.Distance(targetPoint, focusPoint);
    145.             float t = 1f;
    146.             if (distance > 0.01f && focusCentering > 0f)
    147.             {    t = Mathf.Pow(1f - focusCentering, Time.unscaledDeltaTime);
    148.             }
    149.             if (distance > focusRadius)
    150.             {    t = Mathf.Min(t, focusRadius / distance);
    151.             }
    152.             focusPoint = Vector3.Lerp(targetPoint, focusPoint, t);
    153.         }
    154.         else
    155.         {    focusPoint = targetPoint;
    156.         }
    157.     }
    158.  
    159.     bool ManualRotation ()
    160.     {    Vector2 input = new Vector2(Input.GetAxis("Vertical Camera"),Input.GetAxis("Horizontal Camera"));
    161.         const float e = 0.001f;
    162.         if (input.x < -e || input.x > e || input.y < -e || input.y > e)
    163.         {    orbitAngles += rotationSpeed * Time.unscaledDeltaTime * input;
    164.             lastManualRotationTime = Time.unscaledTime;
    165.             return true;
    166.         }
    167.         return false;
    168.     }
    169.  
    170.     bool AutomaticRotation ()
    171.     {    if (Time.unscaledTime - lastManualRotationTime < alignDelay)
    172.         {    return false;
    173.         }
    174.  
    175.         Vector3 alignedDelta =
    176.         Quaternion.Inverse(gravityAlignment) *
    177.         (focusPoint - previousFocusPoint);
    178.         Vector2 movement = new Vector2(alignedDelta.x, alignedDelta.z);
    179.         float movementDeltaSqr = movement.sqrMagnitude;
    180.         if (movementDeltaSqr < 0.0001f)
    181.         {    return false;
    182.         }
    183.  
    184.         float headingAngle = GetAngle(movement / Mathf.Sqrt(movementDeltaSqr));
    185.         float deltaAbs = Mathf.Abs(Mathf.DeltaAngle(orbitAngles.y, headingAngle));
    186.         float rotationChange =
    187.         rotationSpeed * Mathf.Min(Time.unscaledDeltaTime, movementDeltaSqr);
    188.         if (deltaAbs < alignSmoothRange)
    189.         {    rotationChange *= deltaAbs / alignSmoothRange;
    190.         }
    191.         else if (180f - deltaAbs < alignSmoothRange)
    192.         {    rotationChange *= (180f - deltaAbs) / alignSmoothRange;
    193.         }
    194.         orbitAngles.y =
    195.         Mathf.MoveTowardsAngle(orbitAngles.y, headingAngle, rotationChange);
    196.         return true;
    197.     }
    198.  
    199.     void ConstrainAngles ()
    200.     {    orbitAngles.x =
    201.         Mathf.Clamp(orbitAngles.x, minVerticalAngle, maxVerticalAngle);
    202.         if (orbitAngles.y < 0f)
    203.         {    orbitAngles.y += 360f;
    204.         }
    205.         else if (orbitAngles.y >= 360f)
    206.         {    orbitAngles.y -= 360f;
    207.         }
    208.     }
    209.  
    210.     static float GetAngle (Vector2 direction)
    211.     {    float angle = Mathf.Acos(direction.y) * Mathf.Rad2Deg;
    212.         return direction.x < 0f ? 360f - angle : angle;
    213.     }
    214. }
    I feel like others had to have tried to do this before and maybe I just don't know what words to use when searching for it. I didn't find anything on the forums or wider web for this...

    I posted this during my break at work and should be able to respond throughout the day but won't get back to my dev computer till after my shift.

    Thank you to anyone who takes time to read all this and offer advice. I did my best to concisely communicate what is going on but I will take no insult at all if it still came out incoherent anyway and you tell me so. I don't post here often but I am trying to abide by the etiquette.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,749
    I don't think you want the average of the normals... I think you want the normal of the plane defined by three or more contact points, like a tripod passing over an uneven surface.

    Otherwise, Camera stuff is pretty tricky... you may wish to consider using Cinemachine from the Unity Package Manager.

    There's even a dedicated forum: https://forum.unity.com/forums/cinemachine.136/
     
    DiamondEater likes this.
  3. DiamondEater

    DiamondEater

    Joined:
    Feb 6, 2022
    Posts:
    8
    I have seen people mention Cinemachine in other threads. I'll check into it a little closer!