내일배움캠프 Unity 86일차 TIL - 팀 9와 4분의 3 - 개발일지

Wooooo·2024년 2월 28일
0

내일배움캠프Unity

목록 보기
88/94

[오늘의 키워드]

  • 트러블 슈팅 - 아웃라인 셰이더
    • 메시를 통째로 키우기
    • 정점을 법선 방향으로 늘리기
    • 스무스 노말
  • 아웃라인 최적화 방향 결정

[트러블 슈팅 - 아웃라인 셰이더]

자료조사를 해보니, 아웃라인 셰이더를 구현하는 방법은 크게 3가지 정도가 있었다.

  • 2Pass
    • 메시를 통째로 키운 다음, 앞면 대신 뒷면을 그리기
    • 각 정점을 법선 방향으로 잡아 당긴 다음, 앞면 대신 뒷면을 그리기
  • 포스트 프로세싱

구현이 간단하고 자료도 많은 2Pass 방법을 사용해보기로 했다!
2Pass라고 불리는 이유는, 셰이더에 Pass를 하나 더 줘서 2개의 오브젝트를 그리기 떄문이다. (원본 메시, 외곽선 메시)

메시를 통째로 키우기

개요

원본 정점들의 스케일을 키우고, 뒷면만 그리도록 설정했다.
(Graph Settings에서 Render FaceBack으로 설정)

문제점

이 방법은 구현이 너무너무너무 간단하지만, 그 만큼 큰 문제가 있었다.

왼쪽의 큐브처럼 로컬좌표의 원점이 메시의 중심점과 같은 경우에는 문제가 없지만, 다른 경우에는 원점에서 먼 정점일 수록 스케일 값을 크게 받아서 원본 메시와의 정점 위치가 어긋나버린다.

현재 프로젝트에 사용중인 대부분의 오브젝트는 로컬좌표의 원점이 메시의 중심점과 다르기 때문에 대신에 다른 방법을 찾아야했다.

정점을 법선 방향으로 늘리기

개요

현재 정점을 법선 방향으로 이동시키도록 수정했다.

문제점

이렇게 하니, 로컬좌표의 중심이 어디던 상관없어졌지만, 아웃라인 메시가 이어지지 않아서 아웃라인처럼 안보인다.

이 현상은 큐브를 예로 들면, 한 쪽 모서리 정점 당 3개의 법선 벡터를 갖고있기 때문에 발생한 현상이다.
버텍스 셰이더에서 정점을 하나는 위(y)로만, 하나는 오른쪽(x)으로만, 하나는 앞(z)으로만 이동시켰기 때문에, 이렇게 이동한 정점들은 서로 연결이 되지 않는다.
따라서, 같은 정점끼리는 하나의 노말을 사용하도록 바꿔줘야한다.

스무스 노멀

개요

위의 현상을 해결하기 위한 방법이 Smooth Normal이다.
하나의 정점에 있는 여러 법선들을 평균을 내서 하나의 법선으로 합친다.

단, 이 기능을 원본 메시에도 적용하면, 라이팅 쪽에서 이상하게 보일 수 있기 때문에 외곽선 메시에만 적용해야할 것 같다.

자료 출처 : https://blog.naver.com/mnpshino/221495979665

코드 작성

Extensions.cs

public static Vector3[] CalcaultateSmoothNormal(this Mesh mesh)
{
    Dictionary<Vector3, List<int>> indicesDict = new();

    for (int i = 0; i < mesh.vertexCount; i++)
    {
        if (!indicesDict.ContainsKey(mesh.vertices[i]))
            indicesDict.Add(mesh.vertices[i], new());

        indicesDict[mesh.vertices[i]].Add(i);
    }

    Vector3[] normals = mesh.normals;

    foreach (var indices in indicesDict.Values)
    {
        Vector3 normal = Vector3.zero;

        foreach (var index in indices)
            normal += mesh.normals[index];

        normal /= indices.Count;

        foreach (var index in indices)
            normals[index] = normal;
    }

    return normals;
}

하나로 합쳐진 법선벡터의 배열을 반환해주는 확장 메서드를 작성했다.

CustomOutlineDrawer.cs

private void Initialize()
{
    _outlineObject = new($"[Outline] {gameObject.name}");
    _outlineObject.transform.SetParent(transform);
    _outlineObject.transform.localPosition = Vector3.zero;
    _outlineObject.transform.localRotation = Quaternion.identity;
    _outlineObject.transform.localScale = Vector3.one;

    var outlineMesh = Instantiate(GetComponent<MeshFilter>().sharedMesh);
    outlineMesh.normals = outlineMesh.CalcaultateSmoothNormal();
    _outlineObject.AddComponent<MeshFilter>().sharedMesh = outlineMesh;

    var outlineRenderer = _outlineObject.AddComponent<MeshRenderer>();
    outlineRenderer.material = Managers.Resource.GetCache<Material>("OutlineMaterial.mat");
}

MeshFilterMeshRenderer를 가지고 있는 게임오브젝트에 추가하면 스무스 노말을 계산한 뒤 아웃라인을 그려주는 오브젝트를 자식 객체로 생성해주는 컴포넌트를 작성했다.

원본 객체의 Mesh를 Instantiate해서, SmoothNormal을 적용한 다음 아웃라인 객체의 Mesh로 넣어줬다.

아웃라인 머티리얼은 어드레서블에 등록해놓고, 리소스 매니저를 이용해 할당받았다.

동작 모습

꽤나 그럴싸한 아웃라인 셰이더가 됐다 !


[TODO - 최적화]

구현을 하다보니 스무스 노말을 계산해서 적용한 새로운 메시를 할당해야하는 구조가 됐다.

따라서 최적화를 생각하지 않을 수가 없다.

지금 생각나는 최적화 방법은 세 가지 정도 있다.

  1. 셰이더에서 스무스 노말 직접 계산?
    -> 버퍼에 저장해서 셰이더에 매개변수로 넘겨줄 수 있을 것 같긴한데 ,,
    -> 일단은 후순위,, 다른 방법을 먼저 생각해보자
  2. 런타임에서 JobSystem으로 스무스 노말을 계산한 뒤, 사용하지 않는 아웃라인 메시 파괴
    -> 아웃라인을 on/off할 때마다 CPU 부하가 생길지도? (생성, 계산, 파괴)
    -> 대신 다양한 메시에 적용 가능 (메시 데이터만 넘겨주면 런타임에서 적용할 수 있다.)
  3. 미리 스무스 노말이 계산된 메시를 적용해둔 다음, 필요할 때 on/off
    -> CPU 부하를 줄이는 대신 메모리 사용량 증가
    -> 사전에 적용해둔 메시에만 적용 가능
    -> 에디터 상에서 만든 메시를 Asset으로 뽑아서 프리팹에 추가해줘야할 듯 (번거로움)

2번과 3번 방법을 짬뽕해서 스무스 노말의 계산은 JobSystem을 이용하고, 한 번 계산된 아웃라인 메시를 캐싱해뒀다가 쓰는 방법으로 최적화를 할 것 같다.
ex) Dictionary<originMesh, outlineMesh>


[참고 자료]

https://blog.naver.com/mnpshino/221495979665
https://lightbakery.tistory.com/34
https://ssan.tistory.com/17
https://roomdev-diary.tistory.com/9

profile
game developer

0개의 댓글