preventing objects from clipping camera view port

Hi all,

I’ve a 3rd person camera similar to Mario64. Currently, I’m working on preventing the camera from getting stuck inside a mesh. The goal is to move the camera overhead & this much trickier than I’d expected.

Here’s the code:

using UnityEngine;
using System.Collections;

/// <summary>
/// This is a C# and VCS friendly conversion of ThirdPersonCamera.js found in
/// Unity's Character Controllers package.  For any questions, contact
/// support@bitbybitstudios.com.
/// Appendeded camera clipping function
/// Camera culling set up:
/// 1. create 2 layers name them clip & clipNone
/// 2. use bitswitch to switch between layer masks
/// 3. ensure mesh in game has colliders
/// 4. create a tag & label environment - checked by RayCastHit
/// 5. if camera is occluded, move it overhead
/// 6. if an object will clip move it overhead
/// </summary>
public class VCThirdPersonCamera : MonoBehaviour
{
    public  Transform cameraTransform; //use this as raycast emitterPoint
    private Transform _target;
 
    // The distance in the x-z plane to the target
    public float distance= 7.0f;
 
    // the height we want the camera to be above the target
    public float height= 3.0f;
 
    public float angularSmoothLag = 0.3f;
    public float angularMaxSpeed  = 15.0f;
 
    public float heightSmoothLag = 0.3f;
 
    public float snapSmoothLag = 0.2f;
    public float snapMaxSpeed  = 720.0f;
 
    public float clampHeadPositionScreenSpace = 0.75f;
 
    public float lockCameraTimeout = 0.2f;
 
    private Vector3 headOffset   = Vector3.zero;
    private Vector3 centerOffset = Vector3.zero;
 
    private float heightVelocity = 0.0f;
    private float angleVelocity  = 0.0f;
    private bool  snap           = false;
    private VCThirdPersonController controller;
    private float targetHeight= 100000.0f;
 
    /// clipping variables
    /// use the camera's slider to determine defaultFOV
    public Vector3    cameraOffset = new Vector3(0,0.125f,-0.125f); //moves camera so player doesn't get clipped

    public float      fovDefault   = 60f;                           //the desired default field of view set by user
    public float      camTurnTime  = 0f;                            //use to compensate for the camera turning
    public float      headCamTimer = 1.5f;                          //how long should the camera stay overHead
    public float      fieldOfView  = 95.0f;                         //set when camera is overHead
    public bool       camOccluded  = false;                         //is the camera occluded
    public bool       clipped      = true;                          //used for clipping
    public Camera     _camera;
    public GameObject theCamera;

    private LayerMask   _mask;
    private RaycastHit  _hit;
    /// end occlusionHandlers

    private void Awake ()
    {
        if (cameraTransform == null && Camera.main != null)
            cameraTransform = Camera.main.transform;
        if (cameraTransform == null)
        {
            Debug.Log("Please assign a camera to the ThirdPersonCamera script.");
            enabled = false; 
        }
     
        _target = transform;
        if (_target != null)
        {
            controller = _target.GetComponent<VCThirdPersonController>();
        }
     
        if (controller != null)
        {
            CharacterController characterController = _target.GetComponent<CharacterController>();
            centerOffset = characterController.bounds.center - _target.position;
            headOffset = centerOffset;
            headOffset.y = characterController.bounds.max.y - _target.position.y;
        }
        else
            Debug.Log("Please assign a target to the camera that has a ThirdPersonController script attached.");
     
        Cut(_target, centerOffset);

        /// instantiate occlusion culling handler through bitSwitch
        _mask = 1 << LayerMask.NameToLayer("clip") | 0 << LayerMask.NameToLayer("clipNone");
    }

    public float AngleDistance ( float a ,   float b  )
    {
        a = Mathf.Repeat(a, 360);
        b = Mathf.Repeat(b, 360);
     
        return Mathf.Abs(b - a);
    }
 
    public void Apply (Transform dummyTarget, Vector3 dummyCenter)
    {
        // Early out if we don't have a target
        if (controller == null)
            return;
     
        Vector3 targetCenter = _target.position + centerOffset;
        Vector3 targetHead = _target.position + headOffset;
             
        // Calculate the current & target rotation angles
        float originalTargetAngle= _target.eulerAngles.y;
        float currentAngle= cameraTransform.eulerAngles.y;
     
        // Adjust real target angle when camera is locked
        float targetAngle= originalTargetAngle;
     
        // When pressing Fire2 (alt) the camera will snap to the target direction real quick.
        // It will stop snapping when it reaches the target
        if (Input.GetButton("Fire2")) // VCS Note: For touch controls, you may want to change this to use a VCAnalogJoystick's TapCount or a VCButtonBase's Pressed properties.
            snap = true;
     
        if (snap)
        {
            // We are close to the target, so we can stop snapping now!
            if (AngleDistance (currentAngle, originalTargetAngle) < 3.0f)
                snap = false;
         
            currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, snapSmoothLag, snapMaxSpeed);
        }
        // Normal camera motion
        else
        {
            if (controller.GetLockCameraTimer() < lockCameraTimeout)
            {
                targetAngle = currentAngle;
            }         
            // Lock the camera when moving backwards!
            // * It is really confusing to do 180 degree spins when turning around.
            if (AngleDistance (currentAngle, targetAngle) > 160 && controller.IsMovingBackwards ())
                targetAngle += 180;
         
            currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, angularSmoothLag, angularMaxSpeed);
        }     
     
        // When jumping don't move camera upwards but only down!
        if (controller.IsJumping ())
        {
            // We'd be moving the camera upwards, do that only if it's really high
            float newTargetHeight= targetCenter.y + height;
            if (newTargetHeight < targetHeight || newTargetHeight - targetHeight > 5)
                targetHeight = targetCenter.y + height;
        }
        // When walking always update the target height
        else
        {
            targetHeight = targetCenter.y + height;
        }
     
        // Damp the height
        float currentHeight= cameraTransform.position.y;
        currentHeight = Mathf.SmoothDamp (currentHeight, targetHeight, ref heightVelocity, heightSmoothLag);
     
        // Convert the angle into a rotation, by which we then reposition the camera
        Quaternion currentRotation= Quaternion.Euler (0, currentAngle, 0);
     
        // Set the position of the camera on the x-z plane to:
        // distance meters behind the target
        cameraTransform.position = targetCenter;
        cameraTransform.position += currentRotation * Vector3.back * distance;
     
        Vector3 tempCameraTransformPos=cameraTransform.position;
     
        tempCameraTransformPos.y = currentHeight;
        cameraTransform.position = tempCameraTransformPos;
     
        // Always look at the target 
        SetUpRotation(targetCenter, targetHead);
    }
    private void LateUpdate ()
    {
        Vector3 targetCenter = _target.position + centerOffset;
        Vector3 targetHead = _target.position + headOffset;

        Vector3 theTarget = transform.TransformDirection(Vector3.forward)* 3.5f;// <--original line
        //Vector3 theTarget = transform.LookAt(targetHead);
        Ray centerRay = _camera.ViewportPointToRay(new Vector3(.5f, 0.5f, 1f));
        //cameraTransform.transform.LookAt(targetHead);//alwasy lookAt the player's head
        // Raycast(Vector3 origin, Vector3 direction, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);

        if(Physics.Raycast(_camera.transform.position, theTarget, out _hit, 2.77f) && _hit.collider.CompareTag("Player"))
        {
            camOccluded = false;
        }
        if(Physics.Raycast(_camera.transform.position, theTarget, out _hit, 2.77f) && _hit.collider.CompareTag("environment"))
        {
            camOccluded = true;
        }

        /// creates ray from cameraViewPort to nearClipPlane
        float camClipPlane = _camera.nearClipPlane;
        /// sets up clipPoints from camera to viewPort
        Vector3 pos1 = _camera.ViewportToWorldPoint(new Vector3(0,0,camClipPlane));   //btmL
        Vector3 pos2 = _camera.ViewportToWorldPoint(new Vector3(.5f,0,camClipPlane)); //btmCtr
        Vector3 pos3 = _camera.ViewportToWorldPoint(new Vector3(1,0,camClipPlane));   //btmR
        //--------------------------
        Vector3 pos4 = _camera.ViewportToWorldPoint(new Vector3(0,.5f,camClipPlane)); //ctrL
        Vector3 pos5 = _camera.ViewportToWorldPoint(new Vector3(1,.5f,camClipPlane)); //ctrR
        //--------------------------
        Vector3 pos6 = _camera.ViewportToWorldPoint(new Vector3(0,1,camClipPlane));   //topL
        Vector3 pos7 = _camera.ViewportToWorldPoint(new Vector3(.5f,1,camClipPlane)); //topCtr
        Vector3 pos8 = _camera.ViewportToWorldPoint(new Vector3(1,1,camClipPlane));   //topR
        /// visualizes clipPoints- can be deleted
        //Debug.DrawLine(cameraTransform.position, pos1, Color.yellow);
        //Debug.DrawLine(cameraTransform.position, pos2, Color.yellow);
        //Debug.DrawLine(cameraTransform.position, pos3, Color.yellow);
        //Debug.DrawLine(cameraTransform.position, pos4, Color.yellow);
        //Debug.DrawLine(cameraTransform.position, pos5, Color.yellow);
        //Debug.DrawLine(cameraTransform.position, pos6, Color.yellow);
        //Debug.DrawLine(cameraTransform.position, pos7, Color.yellow);
        //Debug.DrawLine(cameraTransform.position, pos8, Color.yellow);

        bool clipLine01 = Physics.Linecast(cameraTransform.position, pos1, out _hit, _mask.value);
        bool clipLine02 = Physics.Linecast(cameraTransform.position, pos2, out _hit, _mask.value);
        bool clipLine03 = Physics.Linecast(cameraTransform.position, pos3, out _hit, _mask.value);
        bool clipLine04 = Physics.Linecast(cameraTransform.position, pos4, out _hit, _mask.value);
        bool clipLine05 = Physics.Linecast(cameraTransform.position, pos5, out _hit, _mask.value);
        bool clipLine06 = Physics.Linecast(cameraTransform.position, pos6, out _hit, _mask.value);
        bool clipLine07 = Physics.Linecast(cameraTransform.position, pos7, out _hit, _mask.value);
        bool clipLine08 = Physics.Linecast(cameraTransform.position, pos8, out _hit, _mask.value);

        ///clip conditions are met & camera is occluded
        if((clipLine01)||(clipLine02)||(clipLine03)||(clipLine04)||(clipLine05)||(clipLine06)||(clipLine07)||(clipLine08))
        {
            camOccluded = true;
            //StartCoroutine(ResetCamera());
        }
        else
        {
            camOccluded = false;
        }

        if(!camOccluded)
        {
            Apply (transform, Vector3.zero);//default VCScamera method
                     
        }
        else
        {
            StartCoroutine(ResetCamera());
        }
        print(camOccluded);
        print(headCamTimer);

    }
    public void OverheadCamera()
    {
        Vector3 targetCenter = _target.position + centerOffset;
        Vector3 targetHead = _target.position + headOffset;
     
        _camera.fieldOfView = fieldOfView;
     
        cameraTransform.position = (targetHead + cameraOffset) ;
        cameraTransform.LookAt(targetHead);
    }

    IEnumerator ResetCamera()
    {
        //camOccluded = true;
        OverheadCamera();
        yield return new WaitForSeconds(headCamTimer);
        _camera.fieldOfView = fovDefault;
        Apply (transform, Vector3.zero);//default VCScamera position
        camOccluded = false;
    }

    public void Cut ( Transform dummyTarget ,   Vector3 dummyCenter  ){
        float oldHeightSmooth= heightSmoothLag;
        float oldSnapMaxSpeed= snapMaxSpeed;
        float oldSnapSmooth= snapSmoothLag;
     
        snapMaxSpeed = 10000;
        snapSmoothLag = 0.001f;
        heightSmoothLag = 0.001f;
     
        snap = true;
        Apply (transform, Vector3.zero);
     
        heightSmoothLag = oldHeightSmooth;
        snapMaxSpeed = oldSnapMaxSpeed;
        snapSmoothLag = oldSnapSmooth;
    }

    public void SetUpRotation (Vector3 centerPos,  Vector3 headPos)
    {
        // Now it's getting hairy. The devil is in the details here, the big issue is jumping of course.
        // * When jumping up and down we don't want to center the guy in screen space.
        //  This is important to give a feel for how high you jump and avoiding large camera movements.
        //
        // * At the same time we dont want him to ever go out of screen and we want all rotations to be totally smooth.
        //
        // So here is what we will do:
        //
        // 1. We first find the rotation around the y axis. Thus he is always centered on the y-axis
        // 2. When grounded we make him be centered
        // 3. When jumping we keep the camera rotation but rotate the camera to get him back into view if his head is above some threshold
        // 4. When landing we smoothly interpolate towards centering him on screen

        //--------------------------------------------------------------------------//

        _camera.fieldOfView = fovDefault; // defaults camera to desired FOV

        Vector3 cameraPos = cameraTransform.position;
        Vector3 offsetToCenter= centerPos - cameraPos;
     
        // Generate base rotation only around y-axis
        Quaternion yRotation= Quaternion.LookRotation(new Vector3(offsetToCenter.x, 0, offsetToCenter.z));
     
        Vector3 relativeOffset= Vector3.forward * distance + Vector3.down * height;
        cameraTransform.rotation = yRotation * Quaternion.LookRotation(relativeOffset);
     
        // Calculate the projected center position and top position in world space
        Ray centerRay = _camera.ViewportPointToRay(new Vector3(.5f, 0.5f, 1f));
        Ray topRay = _camera.ViewportPointToRay(new Vector3(.5f, clampHeadPositionScreenSpace, 1f));
     
        Vector3 centerRayPos= centerRay.GetPoint(distance);
        Vector3 topRayPos= topRay.GetPoint(distance);
     
        float centerToTopAngle= Vector3.Angle(centerRay.direction, topRay.direction);
     
        float heightToAngle= centerToTopAngle / (centerRayPos.y - topRayPos.y);
     
        float extraLookAngle= heightToAngle * (centerRayPos.y - centerPos.y);
        if (extraLookAngle < centerToTopAngle)
        {
            extraLookAngle = 0;
        }
        else
        {
            extraLookAngle = extraLookAngle - centerToTopAngle;
            cameraTransform.rotation *= Quaternion.Euler(-extraLookAngle, 0, 0);
        }
    }
 
    public Vector3 GetCenterOffset ()
    {
        return centerOffset;
    }
}

Using a while loops crash the game…

Any advise is greatly appreciated.

One approach I have used is to put a big invisible sphere collider on the camera, give it a rigidbody, attach it to your character by a joint, and then let the physics “push” the camera upwards when the camera’s sphere bumps into geometry.

It’s not perfect but it might get you a long way there. The next step after the above is to come up with good damping solutions to prevent jitter and bounce and jerking of the camera, which means then you put your camera on yet another gameobject, and have some kind of “tween towards” script to dampen out the sharp motions of impact.

Thanks for that @Kurt-Dekker , believe me I’ve tried that approach already. That method isn’t very compatible with Virtual Control Suite’s 3rd person camera.

Then looking specifically at the problem you are seeing in your video, you might need to implement some kind of dwell timer that doesn’t let the camera choose a new location sooner than (say) a full second after the last time I chose.

Another way to do it is to select possible altitudes and/or offsets for your camera, and then score them based on a rough estimate of how much geometry is between the camera and the player (say a raycast to head, middle and foot).

Then using that score value, switch the camera only when there is a clearly superior score at another location than the one you are at.

You just gave me an idea, that simplifies everything…set up volume colliders along the borders that will set colClip true, thus moving the camera overhead…

Yup, using volume triggers- is the simplest solution & it works…