Meta XR Core SDK를 다운받으면 Util Script에 OVRScreenFade.cs가 존재합니다.
OVRScreenFade를 이용해서 카메라가 벽에 충돌할 때 어두워지는 효과를 연출하는 코드를 가져왔습니다.
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에 가까움).
프러스텀 코너라는 것은 프러스텀의 네 개의 모서리를 의미합니다. 프러스텀(Frustum)은 카메라의 시야 범위를 나타내는 4면체 형태의 3D 기하학적 도형입니다. 카메라를 통해 볼 수 있는 장면의 영역을 의미하는데, 이 영역은 가까운 클리핑 평면(near plane)과 먼 클리핑 평면(far plane) 사이에서 카메라의 시야가 이루는 피라미드 형태의 잘린 모양을 하고 있습니다.
프러스텀 코너(Frustum Corners)는 근접 평면(near plane)의 네 모서리, 원거리 평면(far plane)의 네 모서리를 의미합니다.