창 올라타기

This commit is contained in:
2026-08-26 11:01:56 +09:00
parent 65024110df
commit cab0d772b1
18 changed files with 1335 additions and 31 deletions

View File

@@ -0,0 +1,111 @@
using UnityEngine;
/// <summary>
/// 카메라를 직교 투영으로 바꾸고, 불러온 캐릭터 크기에 맞춰 자동으로 구도를 잡는다.
///
/// 왜 직교 투영인가:
/// 원근 투영에서는 화면 중앙을 벗어날수록 캐릭터를 비스듬히 옆에서 보게 되어
/// 형태가 왜곡된다. 데스크톱 캐릭터는 화면 어디로든 이동하므로 이 왜곡이 그대로
/// 드러난다. 직교 투영은 위치와 무관하게 항상 같은 각도로 보인다.
///
/// 왜 자동인가:
/// 사용자가 임의의 VRM 을 넣는 구조라 캐릭터 키를 미리 알 수 없다. 모델마다
/// 직교 크기를 손으로 맞추면 모델을 바꿀 때마다 다시 맞춰야 한다.
/// </summary>
public class CharacterCameraFit : MonoBehaviour
{
[Header("참조")]
[Tooltip("비우면 같은 오브젝트의 Camera 또는 Camera.main")]
[SerializeField] Camera targetCamera;
[Tooltip("비우면 씬에서 탐색")]
[SerializeField] VrmCharacterLoader loader;
[Header("구도")]
[Tooltip("직교 투영으로 강제한다. 끄면 현재 투영 방식을 유지한 채 크기만 맞춘다")]
[SerializeField] bool forceOrthographic = true;
[Tooltip("캐릭터가 화면 높이에서 차지할 비율. 0.30면 화면의 30%")]
[Range(0.1f, 1f)]
[SerializeField] float screenHeightFraction = 0.30f;
[Tooltip("캐릭터가 처음에 화면 중앙에 오도록 카메라 위치도 맞춘다")]
[SerializeField] bool centerOnCharacter = true;
void Awake()
{
if (targetCamera == null) targetCamera = GetComponent<Camera>();
if (targetCamera == null) targetCamera = Camera.main;
if (loader == null) loader = FindFirstObjectByType<VrmCharacterLoader>();
if (loader != null) loader.Loaded += OnCharacterLoaded;
}
void OnDestroy()
{
if (loader != null) loader.Loaded -= OnCharacterLoaded;
}
void OnCharacterLoaded(ICharacterAvatar avatar)
{
if (avatar != null) Fit(avatar.Root);
}
/// <summary>캐릭터를 감싸도록 카메라 크기와 위치를 맞춘다.</summary>
public void Fit(Transform character)
{
if (targetCamera == null || character == null) return;
var renderers = character.GetComponentsInChildren<Renderer>(true);
if (renderers.Length == 0)
{
Debug.LogWarning("[CharacterCameraFit] 렌더러가 없어 구도를 잡지 못했습니다.");
return;
}
Bounds bounds = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++) bounds.Encapsulate(renderers[i].bounds);
float characterHeight = Mathf.Max(0.01f, bounds.size.y);
if (forceOrthographic) targetCamera.orthographic = true;
if (targetCamera.orthographic)
{
// orthographicSize 는 화면에 보이는 세계 높이의 절반이다.
// 보이는 높이 = 캐릭터 키 / 비율 이므로, 그 절반이 필요한 값이다.
targetCamera.orthographicSize = characterHeight / (2f * screenHeightFraction);
}
else
{
// 원근이면 거리로 맞춘다. 왜곡은 남으므로 권장하지 않는다.
float visibleHeight = characterHeight / screenHeightFraction;
float halfFovRad = targetCamera.fieldOfView * 0.5f * Mathf.Deg2Rad;
float distance = visibleHeight * 0.5f / Mathf.Tan(halfFovRad);
Vector3 dir = targetCamera.transform.forward;
targetCamera.transform.position = bounds.center - dir * distance;
}
if (centerOnCharacter)
{
// 카메라의 시선 방향(깊이)은 유지하고, 화면상 중앙만 캐릭터에 맞춘다.
Vector3 camPos = targetCamera.transform.position;
Vector3 toCharacter = bounds.center - camPos;
Vector3 forward = targetCamera.transform.forward;
// 깊이 성분만 남기고 나머지를 상쇄해 캐릭터가 화면 중앙에 오게 한다.
Vector3 depthOnly = Vector3.Project(toCharacter, forward);
targetCamera.transform.position = bounds.center - depthOnly;
}
// 직교 투영에서 near 가 캐릭터보다 앞에 있으면 잘려 보인다.
if (targetCamera.orthographic && targetCamera.nearClipPlane > 0.05f)
{
targetCamera.nearClipPlane = 0.05f;
}
Debug.Log($"[CharacterCameraFit] 구도 적용. 캐릭터 키 {characterHeight:F2}m, " +
$"직교={targetCamera.orthographic}, size={targetCamera.orthographicSize:F2}");
}
}