유니티 VR 카메라가 벽에 충돌할 때 어둡게 만들기 #Screen Fade

Meta XR Core SDK를 다운받으면 Util Script에 OVRScreenFade.cs가 존재합니다.

OVRScreenFade를 이용해서 카메라가 벽에 충돌할 때 어두워지는 효과를 연출하는 코드를 가져왔습니다.

OVRScreenFade / Center Eye에 달아준다


CharacterCameraConstraint

CharacterCameraConstraint 스크립트는 카메라와 오브젝트간의 충돌을 감지하고, 화면 페이드를 적용하거나 캐릭터의 캡슐 콜라이더 높이를 조정하여 자연스러운 움직임을 구현하는 스크립트입니다.

using UnityEngine;

public class CharacterCameraConstraint : MonoBehaviour
{
    private const float FADE_RAY_LENGTH = 0.25f;
    private const float FADE_OVERLAP_MAXIMUM = 0.1f;
    private const float FADE_AMOUNT_MAXIMUM = 1.0f;
    private const float SPHERECAST_RADIUS_SCALE = 0.2f;
    private const float Y_OFFSET_ADJUSTMENT = 0.01f;

    private CapsuleCollider characterCapsuleCollider;
    private SimpleOVRMovement simpleOVRController;
    
    public OVRCameraRig CameraRig;
    public Camera Cam;
    public LayerMask CollideLayers;

    public float HeightOffset;
    public float MinimumHeight;
    public float MaximumHeight;
    

    #region MonoBehaviour
    private void Awake()
    {
        CameraRig ??= GetComponentInChildren<OVRCameraRig>();
        characterCapsuleCollider = GetComponent<CapsuleCollider>();
        simpleOVRController = GetComponent<SimpleOVRMovement>();
        Cam = CameraRig.centerEyeAnchor.GetComponent<Camera>();
    }

    private void OnEnable()
    {
        simpleOVRController.CameraUpdated += CameraUpdate;
    }

    private void OnDisable()
    {
        simpleOVRController.CameraUpdated -= CameraUpdate;
    }
    #endregion
    
    private void CameraUpdate()
    {
        if (IsCameraOverlapped())
        {
            OVRScreenFade.instance.SetExplicitFade(FADE_AMOUNT_MAXIMUM);
        }
        else if (IsCameraNearClipping(out float maxOverlapDistance))
        {
            float fadeParameter = Mathf.InverseLerp(0.0f, FADE_OVERLAP_MAXIMUM, maxOverlapDistance);
            float fadeAlpha = Mathf.Lerp(0.0f, FADE_AMOUNT_MAXIMUM, fadeParameter);
            OVRScreenFade.instance.SetExplicitFade(fadeAlpha);
        }
        else
        {
            OVRScreenFade.instance.SetExplicitFade(0.0f);
        }

        AdjustCapsuleHeight();
    }

    private bool IsCameraOverlapped()
    {
        Camera camera = Cam;
        Vector3 origin = CalculateRaycastOrigin(camera);
        Vector3 direction = (CameraRig.centerEyeAnchor.position - origin).normalized;
        float distance = Vector3.Distance(CameraRig.centerEyeAnchor.position, origin);

        return Physics.SphereCast(origin, camera.nearClipPlane, direction, out RaycastHit hitInfo, distance, CollideLayers, QueryTriggerInteraction.Ignore);
    }

    private Vector3 CalculateRaycastOrigin(Camera camera)
    {
        Vector3 origin = characterCapsuleCollider.transform.position;
        float yOffset = Mathf.Max(0.0f, (characterCapsuleCollider.height * 0.5f) - camera.nearClipPlane - Y_OFFSET_ADJUSTMENT);
        origin.y = Mathf.Clamp(CameraRig.centerEyeAnchor.position.y, characterCapsuleCollider.transform.position.y - yOffset, characterCapsuleCollider.transform.position.y + yOffset);
        return origin;
    }

    private bool IsCameraNearClipping(out float maxOverlapDistance)
    {
        Camera camera = Cam;
        Vector3[] frustumCorners = CalculateFrustumCorners(camera);

        maxOverlapDistance = 0.0f;
        bool isClippingDetected = false;

        foreach (Vector3 frustumPoint in frustumCorners)
        {
            if (Physics.Linecast(CameraRig.centerEyeAnchor.position, frustumPoint, out RaycastHit hitInfo, CollideLayers, QueryTriggerInteraction.Ignore))
            {
                isClippingDetected = true;
                maxOverlapDistance = Mathf.Max(maxOverlapDistance, Vector3.Distance(hitInfo.point, frustumPoint));
            }
        }

        return isClippingDetected;
    }

    private Vector3[] CalculateFrustumCorners(Camera camera)
    {
        Vector3[] frustumCorners = new Vector3[4];
        Vector3[] transformedCorners = new Vector3[5];
    
        camera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), camera.nearClipPlane, Camera.MonoOrStereoscopicEye.Mono, frustumCorners);

        for (int i = 0; i < 4; i++)
        {
            transformedCorners[i] = CalculateTransformedCorner(frustumCorners[i]);
        }
    
        transformedCorners[4] = (transformedCorners[1] + transformedCorners[3]) / 2.0f; 

        return transformedCorners;
    }

    private Vector3 CalculateTransformedCorner(Vector3 corner)
    {
        return CameraRig.centerEyeAnchor.position + (Vector3.Normalize(CameraRig.centerEyeAnchor.TransformVector(corner)) * FADE_RAY_LENGTH);
    }
    
    private void AdjustCapsuleHeight()
    {
        float capsuleOffset = FADE_RAY_LENGTH;
        float calculatedHeight = CameraRig.centerEyeAnchor.localPosition.y + HeightOffset + capsuleOffset;

        float calculatedMinimumHeight = Mathf.Min(characterCapsuleCollider.height, MinimumHeight);
        float calculatedMaximumHeight = GetCalculatedMaximumHeight();

        characterCapsuleCollider.height = Mathf.Clamp(calculatedHeight, calculatedMinimumHeight, calculatedMaximumHeight);

        float cameraRigHeightOffset = HeightOffset - (characterCapsuleCollider.height * 0.5f) - capsuleOffset;
        CameraRig.transform.localPosition = new Vector3(0.0f, cameraRigHeightOffset, 0.0f);
    }

    private float GetCalculatedMaximumHeight()
    {
        float calculatedMaximumHeight = MaximumHeight;
        if (Physics.SphereCast(characterCapsuleCollider.transform.position, characterCapsuleCollider.radius * SPHERECAST_RADIUS_SCALE, Vector3.up, out RaycastHit heightHitInfo, MaximumHeight - characterCapsuleCollider.transform.position.y, CollideLayers, QueryTriggerInteraction.Ignore))
        {
            calculatedMaximumHeight = heightHitInfo.point.y;
        }

        return Mathf.Max(characterCapsuleCollider.height, calculatedMaximumHeight);
    }
}

 

필드 정의

필드  이름
설명
FADE_RAY_LENGTH 카메라의 시작 위치에서부터 페이드가 시작되는 거리
FADE_OVERLAP_MAXIMUM 카메라가 물체(지오메트리)와 겹쳤을 때, 화면이 완전히 페이드 아웃되기까지의 최대 거리
FADE_AMOUNT_MAXIMUM 최대 페이드(아웃) 양
SPHERECAST_RADIUS_SCALE SphereCast에서 사용할 스케일 값

 

 

FADE_RAY_LENGTH는 페이드 아웃이 시작되는 거리

FADE_OVERLAP_MAXIMUM는 페이드 아웃이 완전히 완료되는 거리

 

 

함수 정의 

함수 이름 타입설명
CalculateRaycastOrigin() 캡슐 콜라이더의 위치를 기반으로 레이캐스트의 시작 위치를 계산
IsCameraNearClipping() 카메라가 지오메트리에 매우 가까워 클리핑이 발생하는지 확인하고, 발생할 경우 최대 겹침 거리를 반환
CalculateFrustumCorners() 카메라의 프러스텀 코너를 계산하고 변환된 코너를 반환
CalculateTransformedCorner() 주어진 코너 벡터를 카메라 리그의 중심을 기준으로 변환

 

최대 겹침 거리라는 것은 벽의 표면과 카메라의 근접 거리 또는 겹침 정도를 의미합니다. 예를 들면 카메라가 벽에 거의 붙어 있으면, "겹침 거리"는 매우 작습니다(거의 0에 가까움).

 

출처 - https://docs.unity3d.com/kr/2020.3/Manual/UnderstandingFrustum.html

프러스텀 코너라는 것은 프러스텀의 네 개의 모서리를 의미합니다. 프러스텀(Frustum)은 카메라의 시야 범위를 나타내는 4면체 형태의 3D 기하학적 도형입니다. 카메라를 통해 볼 수 있는 장면의 영역을 의미하는데, 이 영역은 가까운 클리핑 평면(near plane)과 먼 클리핑 평면(far plane) 사이에서 카메라의 시야가 이루는 피라미드 형태의 잘린 모양을 하고 있습니다.

 

프러스텀 코너(Frustum Corners)는 근접 평면(near plane)의 네 모서리, 원거리 평면(far plane)의 네 모서리를 의미합니다.