프로그래밍(C#) /유니티공부

[유니티C# 스크립팅 마스터하기] - 5챕터/카메라, 렌더링, 씬

정체불명의 모모 2022. 6. 28. 14:18
카메라 기즈모

: Gizmo 표시가 활성화된 상태에서 Scene 탭 안의 카메라를 선택하면, 아래 스샷 처럼 씬안에서의 카메라 위치, 시야 등의

  속성을 통해 카메라에 보이는 모습을 명확히 보여주는 프러스텀(frustum) 기즈모 (gizmo)가 표시됩니다.

 

프러스텀 기즈모를 보려면 화면 처럼 카메라를 선택해야 확인을 할 수 있습니다.

이럴 경우 다른 오브젝트 선택후 이동시 제대로 프러스텀안에 있는지 확인이 어렵습니다.

그렇기에 코드로 프러스텀 기즈모를 계속 보여주게 하면 훨 씬 편하게 씬을 세팅 할 수 있습니다.

 

▿  프러스텀 기즈모 그려주기

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode] // 스크립트를 편집 모드에서 실행 하게 한다. 
[RequireComponent(typeof(Camera))] // RequireComponent 속성은 요구되는 컴포넌트를 종속성으로 자동으로 추가 해줍니다.(여기선 카메라에)
public class DrawFrustuRefined : MonoBehaviour
{

    private Camera Cam = null;
    public bool ShowCamGizmo = true;

    private void Awake()
    {
        Cam = GetComponent<Camera>();
    }

    private void OnDrawGizmos()
    {
        if (!ShowCamGizmo) return;

        Matrix4x4 temp = Gizmos.matrix;
        // Matrix4x4.TRS : 변환, 회전 및 스케일링 매트릭스를 만듭니다.
        // 반환된 행렬은 물건을 위치 Pos에 배치하는 행렬 입니다. 
        Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
        if (Cam.orthographic)
        {
            float spread = Cam.farClipPlane - Cam.nearClipPlane;
            float center = (Cam.farClipPlane + Cam.nearClipPlane) * 0.5f;
            Gizmos.DrawWireCube(new Vector3(0, 0, center),
                new Vector3(Cam.orthographicSize * 2 * Cam.aspect, Cam.orthographicSize * 2, spread));
        }
        else
        {
            Gizmos.DrawFrustum(new Vector3(0, 0, (Cam.nearClipPlane)), Cam.fieldOfView, Cam.farClipPlane,
                Cam.nearClipPlane, Cam.aspect);
        }

        Gizmos.matrix = temp;

        // 기존 코드 (책에 기재된)
        // // Game 탭의 크기(면적)을 구한다.
        // Vector2 v = DrawFrustuRefined.GetGameViewSize();
        //
        // float GameAspect = v.x / v.y;   // 탭의 종횡비를 계산한다.
        // float FinalAspect = GameAspect / Cam.aspect; // 카메라의 종횡비로 나눈다.
        //
        // // localToWorldMatrix : 로컬 공간에서 월드 공간으로 점을 변환하는 행령 입니다.
        // Matrix4x4 LocalToWorld = transform.localToWorldMatrix;
        // Matrix4x4 ScaleMatrix =
        //     Matrix4x4.Scale(new Vector3(Cam.aspect * (Cam.rect.width / Cam.rect.height), FinalAspect, 1));
        //
        // Gizmos.matrix = LocalToWorld * ScaleMatrix;
        //
        // // 카메라 프러스텀을 그리는 함수
        // // 로컬 공간이 아닌 월드 공간의 위치와 회전 값 같은 파라미터를 받습니다.
        // // 모든 위치 파라미터는 변환 행렬을 이용해 로컬 공간에서 월드 공간의 위치로 변경 되어야 한다는 것 입니다.
        //
        // // ✢ 추가로 ✢
        // // 실제 뷰포트의 높이와 너비, 게임창의 너비와 높이간의 비율을 계산함으로써 종횡비 파라미터가 만들어 집니다.
        // Gizmos.DrawFrustum(transform.position,Cam.fieldOfView, Cam.nearClipPlane, Cam.farClipPlane, FinalAspect);
        // // 기즈모 행렬을 초기화 해줍니다.
        // Gizmos.matrix = Matrix4x4.identity;
    }

}

❖ 참고

: 현재 책에 작성된 코드로 하면 기대한 결과가 나오지 않습니다.

  다른 방법을 찾아 정상적으로 기즈모를 그려 주었습니다.


오브젝트 가시성

: 가시성에 대해서는 두가지 주요 개념이 있는데, 바로 프러스텀 과 오클루전(occlusion) 입니다.

 

- 프러스텀 : 카메라가 현재 볼 수 있는 가능성을 가진 영역 입니다. (수학적으로 정의된 카메라의 지평선)

- 오클루전 컬링 : 오클루전 컬링은 다른 오브젝트에 가려 카메라에 보이지 않는 오브젝트의 렌더링을 비활성화 하는 기능 입니다.

   (https://docs.unity3d.com/kr/2018.4/Manual/OcclusionCulling.html)  

 

 프러스텀에 오브젝트들이 존재 한다고 해도, 오브젝트를 감추거나 사용자 정의 쉐이더 또는 다른 후 처리 효과를 통해 보이지 않게

 렌더링 하는 겅우가 아니라면 '보이는 오브젝트'라고 할 수 있습니다.

 

오브젝트 가시성 감지

: 유니티에서 가장 간단하고 직접적인 가시성 테스트 방법은 어느 카메라에든지 오브젝트가 보이거나 보이지 않게

  될 때를 감지하는 방법 입니다.

 

❉ 사용하기 좋은 곳 : NPC 간의 상호작용과 같이 가시성에 의존하는 AI 동작을 켜고 끄는 등의 동작을 넣기에 좋습니다.

 

   - OnBecameVisible : 이전에 보이지 않던 오브젝트가 최소한 하나 이상의 카메라 프러스텀에 들어왔을 때 한번 호출 된다.

   - OnBecameInvisible : 이전엔 보이던 오브젝트가 모든 카메라의 프러스텀 밖으로 벗어났을 때 한번 호출 된다.

 

위 짝궁 이벤트들은 MeshRender 이나 SkinneMeshRenderer와 같은 렌더러 컴포넌트를 가지는 모든 오브젝트에서

자동으로 호출 됩니다.

 

 

위 이벤트가 쓸모 없어지는 경우

- 첫 번째 : 오브젝트가 카메라 프러스텀 내에 들어왔다는 의미 이므로 다른 더 가까운
  오브젝트 등에 의해 차단되어 실제로는 보이지 않을 수 있습니다.

- 두 번째 : 이벤트는 특정 카메라에 속하는 것이 아니라 전체 카메라에 대해 발생하는것 입니다. 
   그래서 원하는식으로 오브젝트의 가시성을 확인 못 할 수 있습니다.


다른 이벤트 

- OnWillRenderObject : 위에 있던 이벤트과 달리 이전 상태에 대한 정보 없이 오브젝트의 현재 상태를 검사합니다.
   이 이벤트는 오브젝트가 카메라에 보이는 동안 각각의 카메라에서 매 프레임 마다 호출 된다.
    ( 이 이벤트에서는 다른 오브젝트에 차단 되는지 검사 하지 않습니다.)


프리스텀 검사 : 렌더러 

:  유니티에 내장된 카메라 이벤트들은 필요한 가시성 및 프리스턴 검사에 필요한 요건들을 충분히 갖추지 못하였습니다.

 

- 보이지 않는 오브젝트가 보이는 상태였다면 정말 보이는지 

- 공간상의 특정 지점이 카메라에 보이는지

- 특정한 오브젝트가 새로운 위치로 옮겨지면 카메라에 보이게 될지

 

이러한 카메라 가시성 검사를 추가적으로 작성해 줘야 합니다.

 

▿ 아래의 예제 코드에서는 특정 카메라 오브젝트의 프러스텀 내에 특정 렌더러 컴포넌트가 있는지 검사 하는 함수


프리스텀 검사 : 점  

: 가시성을 확인하기 위해 항상 렌더러를 검사하는 것을 원하지 않을 수 있습니다.

  대신, 간단히 점 검사를 할 수 있습니다.

 

 사용하기 좋은 곳

- 파티클이나 권총 표적 위치와 같은 오브젝트가 실제로 보이는지 알고 싶을 때

- 화면 공간(카메라에 랜더)에 보이는지 알길 원할 때

 

 아래의 예제 코드는 점이 카메라 프러스텀 안에 있는지 검사하고, 프러스텀 안에 있는 경우 더 나아가

     정규화된 뷰포트 공간 안에 존재하는 화면상의 어느 곳에 점이 렌더링 될지 위치를 반환 합니다.


프러스텀 검사 : 오클루전

: 지금까지 위에 본 가시성 검사 방법은 카메라 프러스텀 내에 오브젝트가 존재하는지만을 검사하는 방식으로 이뤄져 있습니다.

  오브젝트의 가시성을 검사하는데 있어 주된 관심사는 사실 카메라가 성능 집약적인 동작들(예를 들어 AI 동작들)에 얼마나 가까이

  있는지를 알기 원하는 것 입니다. 
  이 경우에 오브젝트가 가려졌는지는 중요하지 않고, 프러스텀 안에 있는지 여부가 중요 합니다.

  하지만, 때때로 GUI 구성 요소나 팝업 알림과 같이 플레이어가 특정 오브젝트를 바라볼 때 가려지는 것이 문제입니다.

   이럴 때 오클루전 검사를 이용하여 판단할 수 있습니다.

 

 하지만, 오클루전 검사를 하는것이 상당한 성능 부하를 일으키는 구현을 통해 가능하기 때문에, 

 Physics.LineCast 메소드를 호출해 카메라와 도착 오브젝트 사이에 가상의 선을 이용해 다른 충돌체와 교차하는지 검사하는

 방법이 최선의 방법 중 하나가 될 수 있습니다.

 

  LineCast에  대한 제약사항

 

- 모든 오브젝트에 충돌체가 있다는것을 가정한다.

- 충돌체는 메시의 대력적인 경계만을 나타내므로 메시의 버텍스 수준의 검사는 불가능 하며, 

   메시 내부에 구멍이 있는 경우 감싸는 충돌체가 LineCast로 하여금 그 곳을 통과하는것을 막기 때문에 실패 할 수 있습니다.

- 투명한 재질을 가진 오브젝트가 노출하는 뒤쪽의 오브젝트에 대해서는 LineCast가 항상 실패 합니다.


카메라 시야 : 앞뒤 판별

: 카메라가 대상 오브젝트를 볼 수 있는지 없는지, 즉 타깃 오브젝트가 카메라 앞쪽 제한된 시야에 들어오는지 감지 하기위해서

  벡터의 내적으로 이용할 수 있습니다.

  두 벡터를 입력 값으로 하여 내적 연산을 하면 출력으로 하나의 숫자 값을 얻을 수 있습니다. 

  이 값은 두 벡터 사이의 각도를 나타냅니다.

 

▿  벡터 내적을 이용하여 앞, 뒤 판별 예제

using UnityEngine;

public class CamFieldView : MonoBehaviour
{
    // 전방 시야(각도)
    // 전방을 향하는 벡터로부터의 각도(좌우로)를 정한다.
    public float AngleView = 30.0f;
    
    // 보려는 대상 오브젝트
    public Transform Target = null; 
    
    // 로컬 트랜스폼
    private Transform thisTransform = null;

    private void Awake()
    {   // 로컬 트랜스폼을 얻는다.
        thisTransform = transform;
    }

    private void Update()
    {
        // 카메라와 대상 오브젝트 사이의 시야 계산을 업데이트 한다.
        // normalized : 크기가 1인 벡터를 반환 합니다.(정규화)
        Vector3 Forward = thisTransform.forward.normalized; 
        Vector3 ToObject = (Target.position - thisTransform.position).normalized;
        
        // 내적 연산
        float DotProduct = Vector3.Dot(Forward, ToObject);
        float Angle = DotProduct * 180f;
        
        // 시야를 검사한다.
        if(Angle >= 180f - AngleView)
            Debug.Log("Object can be seen");

    }
}

직교 카메라 ( Orthographic )

: 직교 카메라는 2D 평면상에 변환하는 기능을 가지고 있습니다.

  2D및 등각(isometric) 투영법 게임을 포함한 여러 가지 게임을 만들 때 유용한 카메라 입니다.

  직교 카메라는 평면의 렌즈를 이용해 화상 축소로 인한 손실이 없습니다.

  즉, 평행선이 평행하게 보이고 오브젝트가 멀리있는 것처럼 줄어들지 않으며, 시야의 중앙에서 벗어나더라도 

  2D는 2D 로 보이게 됩니다.

 

   원근 카메라에서 직교 카메라로 변경하고 나면 카메라 프러스텀 또한 사다리꼴에서 박스 형태로 바뀐다.

그리고 깊이의 개념이 사라진다.

 


직교 카메라를 이용할 때 주된 고민은 월드 크기(씬 안에서의 크기) 단위와 픽셀크기(화면상의 크기)를 어떻게 1:1로 

맞출 것인지에 대한 것 입니다.

화면상의 텍스처 파일에 정의된 대로 정확한 기본 크기를 보여줄 필요가 있기 때문입니다.

스프라이트를 이용하는 2D의 경우에는 그래픽이 정면으로 보이게 됩니다.

따라서 픽셀 단위로 일치하는 기본 크기대로 표시되는것이 바람직 합니다.

이렇게 표시하는 것을 픽셀 무결성이라 부르는데, 텍스처 안의 각각의 픽셀이 게임과 화면상에 원래 상태대로 표시되는 것을 말합니다.

 

1월드 단위를 1픽셀에 대응하게 하려면 Camera탭의 Size 필드를 게임의 수직 해상도의 절반으로 설정해야 합니다.

 

예 ) 1024 x 768 해상도의 경우

 

Size 필드 : 768 / 2 = 364

 

▿  해상도에 따른 Size 값 구하기


카메라 렌더링과 후 처리

: 카메라로 렌더링한 출력 결과에 일반적인 렌더링에는 포함되지 않는 모든 추가적인 편집이나 수정을 가하는 것을 

  후처리라고 부릅니다.

  이러한 후처리에는 흐림 효과, 컬러 보정, 어안 효과 등이 포함 됩니다.

   

   책에서 설명해주는 한대의 카메라가 크로스페이드되어 다른 카메라로 부드럽게 넘어가도록 하는 카메라

   전환 시스템을 만드는 방법에 대해 설명하지만, 유니티가 프로 버전에서만 나오기 때문에 패스 하도록 하겠습니다.

 

   personal 에서 위 기능을 쉽게 구현하는 방법을 유튜브에서 발견해 링크 달도록 하겠습니다.(Animation을 이용한)

   링크 : https://www.youtube.com/watch?v=xeXW4RxlWmw 

 

다양한 효과를 주는 유니티의 포스트 프로세싱은 아래의 링크에서 확인하시면 될 것 같습니다.

https://docs.unity3d.com/kr/2019.4/Manual/BestPracticeMakingBelievableVisuals8.html

 

포스트 프로세싱 이해 - Unity 매뉴얼

포스트 프로세싱은 기존에 렌더링된 씬에 렌더링 효과를 더하는 작업입니다. 포스트 프로세싱의 효과는 일반적으로 Scene 뷰에 따라 달라지거나, 최종 렌더링 결과물을 생성하기 전에 렌더링되

docs.unity3d.com


 카메라 진동

: 이번엔 유니티 무료 버전으로도 가능한 카메라 진동 효과를 구현해 보도록 하겠습니다.

  전투나 무기 발사 등 액션 게임에서 카메라 진동 효과는 요긴하게 사용 됩니다.

 

  카메라 진동을 만들어내기 위한 방법에는 여러 가지가 있지만, 최솟값에서 최대값 사이의 범위에서 일종의 '무작위'

  함수를 이용해 카메라 위치 값의 변동을 만들어낸다는 공통점이 있습니다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraShake : MonoBehaviour
{
    private Transform ThisTransform = null;
    
    // 진동을 줄 총시간(초)
    public float ShakeTime = 2.0f;
    
    // 진동량 - 진동할 거리
    public float ShakeAmount = 3.0f;
    
    // 진동할 카메라의 속도
    public float ShakeSpeed = 2.0f;

    void Start()
    {
        ThisTransform = GetComponent<Transform>();
        
        // 진동 시작
        StartCoroutine(Shake());
    }

    public IEnumerator Shake()
    {
        // 카메라의 원래 위치를 저장해 둔다.
        Vector3 OrigPosition = ThisTransform.localPosition;
        
        // 소요 시간을 잰다.
        float ElapsedTime = 0.0f;
        
        // 총 진동 시간 동안 반복한다.
        while (ElapsedTime < ShakeTime)
        {
            // 단위 구상의 한 점을 무작위로 선택 합니다.
            Vector3 RandomPoint = OrigPosition + Random.insideUnitSphere * ShakeAmount;
            
            // 위치를 업데이트 한다.
            ThisTransform.localPosition =
                Vector3.Lerp(ThisTransform.localPosition, RandomPoint, Time.deltaTime * ShakeSpeed);
            
            // 다음 프레임 까지 멈춘다.
            yield return null;
            
            // 시간을 업데이트 한다.
            ElapsedTime += Time.deltaTime;
        }
        
        // 카메라 위치를 원래대로 되돌린다.
        ThisTransform.localPosition = OrigPosition;
    }
}

작업 후 테스트 하면 꽤나 그럴싸하게 흔들리는걸 확인 할 수 있습니다.


카메라와 애니메이션

: 카메라 비행이란 영화에서와 같은 연출을 위해 카메라가 특정 위치를 시간에 따라 이동 및 회전하는 애니메이션 입니다.

  이런 애니메이션이 단독으로 사용되는 경우가 아니더라도 컷씬을 만들 때 카메라 비행은 중요하게 이용됩니다.

  또한 3인칭 형식의 카메라나 일정하게 의도된 길을 따라 움직이는 카메라를 이용하는 '하향식' 화면을 만들 때

  유용하게 이용 할 수 있습니다.

 

► 추적 카메라

: 추적 카메라란 씬에서 특정 오브젝트를 추적하고 따라가는 카메라다.

  이 카메라는 오브젝트와 카메라 사이에 일정 거리를 유지 하게 된다.

  어깨 너머로 보는 것과 같은 3인칭 화면이나 RTS 게임의 하향식 화면을 만들 때 유용합니다.

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class CamFollow : MonoBehaviour
{
    public Transform Target = null;

    private Transform thisTransform = null;

    public float DistanceFromTarget = 10.0f;

    public float CamHeight = 1f;
    public float RotationDamp = 4f;
    public float PosDamp = 4f;

    private void Awake()
    {
        thisTransform = GetComponent<Transform>();
    }

    private void LateUpdate()
    {
        Vector3 Velocity = Vector3.zero;
        
        // 회전 보간 계산
        thisTransform.rotation = Quaternion.Slerp(
            thisTransform.rotation,
            Target.rotation,
            RotationDamp * Time.deltaTime);
        
        // 새로운 좌표를 구한다.
        Vector3 Dest = thisTransform.position = Vector3.SmoothDamp(
            thisTransform.position,
            Target.position,
            ref Velocity,
            PosDamp * Time.deltaTime);
        
        // 대상으로부터 떨어뜨린다.
        thisTransform.position = new Vector3(
            thisTransform.position.x,
            CamHeight,
            thisTransform.position.z);
        
        // 대상을 바라보도록 한다.
        thisTransform.LookAt(Dest);
    }
}

카메라와 곡선

: 컷씬이나 메뉴 배경, 단순한 카메라 비행과 같은 것을 만들려면 일단 대략 직선을 따라 카메라를 이동시킬 수 있어야 합니다.

  카메라가 부드럽게 점차 커지거나 작아지는 움직임으로 이동할 수 있도록 곡률이나 속도에 변화를 줄 수도 있어야 합니다.

  그럴려면 유니티의 애니메이션 에디터에서 미리 정의된 애니메이션을 이용하거나 애니메이션 커브를 사용하면 됩니다.

 

  시간에 따라 오브젝트의 속도와 곡선 움직임 및 감속을 포함한 카메라의 움직임을 제어 하는 예제를 봅시다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraMover : MonoBehaviour
{
    // 애니메이션이 진행되는 총 시간
    public float TotalTime = 5.0f;
    
    // 각 축마다 이동 가능한 총거리
    public float TotalDistance = 30.0f;
    
    // 움직임을 만들기 위한 애니메이션 커브들
    public AnimationCurve XCurve;
    public AnimationCurve YCurve;
    public AnimationCurve ZCurve;
    
    // 현재 오브젝트의 트랜스폼
    private Transform thisTransform = null;

    private void Start()
    {
        // 트랜스폼 컴포넌트를 얻는다.
        thisTransform = GetComponent<Transform>();
        
        // 애니메이션을 시작한다.
        StartCoroutine(PlayAnim());
    }

    public IEnumerator PlayAnim()
    {
        // 애니메이션 시작 시점으로부터 흐른 시간
        float TimeElapsed = 0.0f;

        while (TimeElapsed < TotalTime)
        {
            // 정규화된 형태의 시간을 구한다.
            float normalTime = TimeElapsed / TotalTime;
            
            // X, Y , Z 의 그래프상 위치를 구한다.
            Vector3 NewPos = thisTransform.right.normalized * XCurve.Evaluate(normalTime) * TotalDistance;

            NewPos += thisTransform.up.normalized * YCurve.Evaluate(normalTime) * TotalDistance;

            NewPos += thisTransform.forward.normalized * ZCurve.Evaluate(normalTime) * TotalDistance;
            
            // 위치를 업데이트 한다.
            thisTransform.position = NewPos;
            
            // 다음 프레임까지 멈춘다.
            yield return null;
            
            // 시간을 업데이트한다.
            TimeElapsed += Time.deltaTime;
        }
    }
}