https://booth.pm/ko/items/3521047
オリジナル3Dモデル「生駒ミル」Original 3D Character "Miru Ikoma" Cloth ver. - RiBLA Laboratory売店 - BOOTH
オリジナルキャラクター3Dモデル「生駒ミル」 Original Character 3D Model "Miru Ikoma" キャラクターデザイン:konomi(きのこのみ) (https://twitter.com/konominoco) オリジナルキャラクター「生駒ミル」の
booth.pm
비상업적, 개인 용도로만 이용 가능하다고 되어 있어 다운받고 그냥 스크린샷만 찍었다...
이 포스팅은 산으로 가다못해 우주로 가버린 그림자 시스템 제작기에 대한 내용을 다룬다.
주요 기능
- 캐릭터의 Self Shadow Caster / Receive
- 캐릭터의 Self Shadow를 배경에도 그릴 수 있도록 Screen Space Resolve Shadow Pass
- 원신 등의 호요버스 게임에서 보이는 그림자 경계면에 생기는 Colored Penumbra 표현 (Per Object Shadow / Cascade Shadow 모두 포함)
Per Object Shadow의 필요성
unity의 Cascade Shadow Map을 사용해보면 보이는 배경에 그림자가 모두 생길 수 있도록 거리를 늘리게 될 경우 배경의 스케일에 비해 매우 작은 비율을 차지하고 있는 캐릭터의 그림자가 선명하지 않게 출력되는 것을 볼 수 있다.
특히 캐릭터 팔이나 다리처럼 상대적으로 더 얇은 부분에 대해서 제대로 표현을 못하는 것을 확인할 수 있다. 쪼그라드는 걸 막기 위해 Shadow Caster pass에서 Normal 방향으로 Vertex를 확장해서 표현할 수도 있겠지만 글쎄... 그걸 한다고 해서 만족할만한 결과를 얻을 수 있을까?
배경 그림자 뿐만이 아니라 캐릭터 자신에게 드리우는 그림자인 Self Shadow의 품질은 도저히 써먹을 수 없을 정도이다.
다음은 캐스케이드 그림자를 그대로 출력했을 때의 결과이다.
해상도를 4096으로 올려도 마찬가지이다.
이 특유의 빗살 무늬 비슷한게 나타나기 때문에 캐릭터에는 그림자가 안드리우도록 셰이더를 짜거나 캐릭터가 그림자 영역에 있는지 여부를 판정해서 그림자 안에 있을 경우 캐릭터 전체를 다 어둡게 처리하는 등의 처리를 하는 경우를 본 기억이 있다.
하지만 세계는 대 씹덕 겜의 시대이다. 캐릭터에 영혼을 갈아야 한다! 캐릭터에 양감, 깊이 등을 나타내기 위해서는 미세한 어둠을 최대한 표현해주어야 하는 것이다.
높은 품질의 그림자를 얻는 법
왜 캐릭터 그림자가 저렇게 형편없이 나오는 건가? 그림자 품질을 올리려면 어떻게 해야하는가? 간단하다. Cascade Shadow Map의 해상도를 2048에서 4096으로 올렸을 때 그림자 품질이 올라간 것을 확인할 수 있었다.
즉 그림자 품질이 올라갔으면 하는 대상이 그림자 맵에 최대한 많은 픽셀을 점유할 수 있도록 하면 된다.
캐릭터를 이렇게 배경보다 엄청나게 크게 만들어버리면 그림자 맵에서 캐릭터가 차지하는 비중이 올라가고, 이는 곧 높은 품질의 그림자 결과로 이어진다. 뭐 그렇다고 진격의 거인 게임하는 것도 아니고 캐릭터만 저렇게 크게 할 수는 없다.
Shadow Map 기술의 핵심
Per Object Shadow나 Cascade Shadow나 사실 그 나물에 그 밥이다.
Shadow Mapping은 단순히 특정 위치를 기준으로 찍은 Depth Texture일 뿐이다.
본질은 따로 있다. Shadow Caster Pass 셰이더를 작성하거나 URP의 Lit 셰이더를 보면 Shadow Caster Pass가 매우 심플하게 작성된 것을 본 적이 있을 것이다.
셰이더는 단순하나, 무심코 넘겼던 코드에는 머리가 깨지는 선형대수학이 숨겨져 있다. 바로 view, projection Matrix이다.
결국 Shadow Map은 view, projection matrix를 어떻게 관리하냐에 달려있다.
View
카메라 위치와 바라보는 방향을 담고 있는 매트릭스. 진짜 별 거 없다. view matrix는 굳이 카메라가 아니어도 Decal과 같은 기술에서도 많이 써먹는다. World 좌표를 Local 좌표로 변환시킨다던지, tangent space normal을 world로 보냈다가 world space 노멀을 tangent space로 보내거나 등등등 일종의 Transformation Matrix이다.
Projection
View Frustum이라는 녀석이다. 가상의 직육면체를 만들어서 메쉬를 그 안에 우겨넣다가 삐져나오면 잘라버리는 무시무시한 매트릭스.
View, Projection 중에서도 가장 중요한 건 바로 Projection Matrix이다. 메쉬를 어떻게 구겨서 정육면체에 집어넣느냐에 따라 메쉬가 텍스처에서 얼마나 크게 그려질지가 결정된다.
Cascade Shadow Map이 캐릭터 그림자 용으로 부적합한 이유
그러면 이러한 배경 지식 하에서 Cascade Shadow Map이 캐릭터의 그림자를 그리기에 부적합한 이유를 찾아보자.
어렵게 생각안해도 그냥 다음 그림만 봐도 된다.
Cascade Shadow Map은 카메라의 Frustum Cube를 분할해서 위 이미지처럼 cascade index 0번은 연두색, cascade index 1은 청록색, 2번은 마젠타, 3번은 빨강에 저장하고 마지막 흰색 부분은 부드럽게 그림자가 사라지도록 처리한다.
즉, 카메라의 Frustum Cube를 분할한 각각의 Sub Cube를 포함하는 Bounding Box를 기준으로 그림자 맵을 그리는 식이다.
그러면 카메라의 fov 값과 aspect ratio에 따라서 BoundingBox가 더 커지게 되고, 제한된 해상도 하에서 작은 오브젝트가 그림자맵에서 그다지 많은 면적을 차지하지 못하게 되어 그림자 품질 저하로 이어진다.
정리하면 다음과 같다.
- 카메라 종속적인 Frustum Cube를 사용함으로써 발생하는 문제로, 카메라가 움직일 때마다 Bounds가 달라지고 그에 따라 값이 모두 갱신되어야 한다. view projection matrix를 다시 계산해야 한다. 또 Frustum 공간 전체를 감싸는 Bounds를 계산해야 하기 때문에 작은 오브젝트에 대한 충분한 해상도를 보장하기 어렵다.
- 제작 목적 자체가 공간에 대한 그림자 매핑을 목표로 하고 있기 때문에 캐릭터와 같이 예외 특별 케이스에 대한 고려가 되어 있지 않다.
Shadow Mapping 기술을 사용하면서 생기는 문제점과 해결 방법
위에서 잠시 설명했지만 결국 Shadow Map은 View, Projection Matrix만 잘 만들면 기술적으로는 별게 없다. 하지만 그 잘 만든다라는 모호한 얘기가 대체 어떤 것을 고려해서 만들어야 한다는 얘기인가?
수학 계산보다도 고민해야 할 부분이 바로 이것이다. 그러므로 Shadow Mapping 기술적으로 어떤 문제가 발생할 수 있고 어떻게 대응해야 할 지 정리할 필요가 있다.
일단 마이크로소프트에서 좋은 글을 정리해둔 것이 있다.
고퀄리티 그림자를 구현하기 위해서 반드시 대응해야하는 요소이기 때문에 정리하고 들어가도록 한다.
오브젝트의 텍셀 비율 확보와 관계된 문제
Perspective Matrix를 사용할 시 조명 기준 원경 오브젝트에 생기는 그림자 앨리어싱 현상
Perspective Matrix를 이용해 Depth를 썼을 경우, 카메라로부터 멀리있는 오브젝트일 수록 작게 나오는 것을 알 수 있다.
당연한 얘기지만, Perspective Matrix가 하는 일 자체가 카메라의 fov 각도 내에 있는 오브젝트를 2D 이미지로 구겨넣어야 하기 때문에 멀리있는 오브젝트일 수록 더 넓은 영역이 포함될 수 있도록 작아져야 한다.
그러면 원경 오브젝트가 근경 오브젝트에 비해 항상 더 적은 비율의 텍셀을 가지게 되어 조명 카메라 기준으로 멀리있는 오브젝트일 수록 저 퀄리티의 그림자가 나타나게 된다.
이 문제를 해결하는 핵심 포인트는 원경 그림자를 디테일하게 확보하기 위한 충분한 텍셀을 그림자 텍스처에 출력하는 것이다. 링크에도 쓰여있듯 이 공간변환 matrix를 log식으로 바꿔 사용하거나 Cascade Shadow Map을 사용하는 등의 아이디어를 낼 수 있으나, 구조적으로 해결할 방법은 없다. 구조적으로 해결하는 건 Raytracing 같은 다른 파이프라인을 쓰거나, 베이크된 그림자(Light Map)을 이용할 수 밖에 없다.
아니면 Perspective Matrix를 쓰지 않고 Orthgraphic으로 Depth를 찍는게 원경과 근경의 차이가 없어서 깔끔하긴 하다.
해결 방법
즉 이러한 원경 오브젝트에 대해 낮은 비율의 텍셀이 뎁스에 쓰여서 발생하는 문제는 다음과 같은 아이디어를 생각해볼 수 있다.
- log, exponential 스케일링을 통해 가장 가까울 때와 멀리있을 때의 차이가 크지 않도록 projection matrix를 변형한다. x,y 좌표가 far plane에 가까워지더라도 near plane에 가까울 때와 비교해 큰 차이가 없도록 만들 수 있어야 한다.
- perspective matrix 대신 orthographic matrix를 이용한다.
깊이 정밀도로 인한 이슈도 있지만 일단 앨리어싱 문제는 전적으로 뎁스 맵에 충분한 텍셀이 확보되지 않아서 발생하는 것이므로 최대한 물체가 뎁스 텍스처에 크게 찍히도록 만드는 게 중요하다.
그리고 이건 화면 해상도와도 관계가 있다. 그래서 Shadow Map의 해상도는 실제 렌더 해상도에 비례하는 관계를 갖고 있기 때문에 모바일에서 shadow map의 해상도를 크게 사용할 수 없다면 일정 수준 이상의 퀄리티를 달성하기 위해서 화면 해상도를 얼마나 가져갈지에 대해서도 고민해야 한다.
급경사, 또는 구체와 같이 경사가 계속 변하는 오브젝트에서 확인되는 그림자가 찢어지는 것처럼 보이는 앨리어싱 현상
사실 이게 아주 치명적이다. 유니티의 그림자 시스템에서도 해결을 못한 부분인데, 급격사, 구체 등의 평평하지 않은 물체에 생기는 그림자가 지그재그로 찢어지는 것처럼 출력되는 현상이다.
라이트 기준으로는 인접 픽셀 간 차이가 별로 안나는데 카메라로 볼 때 서로 각도 차이가 많이나 그 사이 부분을 Shadow Map에서 처리를 못해주는 경우 발생한다.
해결 방법
최대한 텍셀을 많이 확보하는 것과 조명과 카메라 사이의 각도 차이가 많이 안나도록 하는 것이 중요하다.
그래서 Light Direction이 아니라 Half Direction을 이용해 그림자를 캡처하는 방법 등을 사용할 수 있다.
래스터라이징으로 인한 양자화 및 깊이 정밀도에 따라 발생하는 문제
그림자 여드름(Shadow Acne) / Self-Shadowing 에러
그림자 맵의 깊이 정밀도는 보통 최적화를 위해 16bit를 사용한다. 하지만 카메라 뎁스 버퍼는 24비트 + 스텐실 8비트 이런 식으로 쓰는게 일반적이다. 즉 셰이더 내에서 해당 오브젝트의 깊이의 실제 값이 그대로 그림자 맵에 정확하게 저장될 것이라고 기대할 수 없다. 또 깊이 값은 선형적이지 않고 가까울 수록 더 정밀해지는 특징이 있다.
다음은 양자화로 인해 발생하는 문제이다. 그래픽스에서 래스터라이즈 파이프라인을 쓰면 메쉬가 얼마나 vertex를 많이 사용하건 간에 저장되는 깊이는 텍스처 형태로 저장된다. 즉, 아주 큐브를 많이 쓴 마인크래프트 아트 작품과 비슷한 형태이다.
그러면 당연히 이미 마인크래프트화 된 채로 그림자맵에 저장된 메쉬와 현재 셰이더 내에서 계산하고 있는 메쉬의 깊이 값은 아무리 해상도를 크게 찍는다고 해도 미세하게 이격이 발생할 수 밖에 없다.
그래서 깊이가 애매하게 비슷한 부분에 대해서 계산 오류로 인해 거의 비슷한 픽셀에 대해서 어느 픽셀은 비교했을 때 그림자고 어느 픽셀은 아니고 이런 게 반복되면서 특유의 물결 모양같은 게 나타나는 현상이다.
해결 방법
뎁스 맵을 쓰고 메쉬를 래스터라이징하는 기술적 한계로 근본적으로 해결은 불가능하다.
같은 위치에 대해서 깊이 계산 결과가 달라지는 현상이므로 계산 결과 오류가 발생하지 않도록 위치 값에 오프셋을 살짝 추가해서 그림자 맵을 샘플하면 정밀도가 낮더라도 차이가 크게 날 것이므로 이러한 현상을 완화할 수 있다. 즉 Depth Bias와 Normal Bias, Slope Bias와 같은 바이어스 값이 등장한 배경이 바로 이것이다.
하지만 이런 식으로 오프셋으로 밀어버리면 다음과 같이 Peter Panning 현상, 즉 그림자가 물체에 붙어있는 느낌이 아니라 살짝 어긋나게 그려지는 시각적 문제가 발생한다.
Depth Bias 값을 과도하게 추가할 경우 발생하는 문제이다. 그래서 Bias 값을 적절하게 넣는 것도 중요하지만 Projection Matrix의 near, far plane 값을 최대한 타이트하게 잡는다, 즉 최대한 그리고자하는 공간에 맞춰 들어갈 수 있도록 값을 정하는 것이 중요하다. 막 far plane 값이 5000 미터 이런식으로 해버리고 그림자는 20미터 내에 있는 오브젝트만 그린다고 하면 실제로는 near plane ~ 20 까지의 데이터는 0.001 0.0012 이런 식으로 깊이 값이 별로 큰 차이가 안나는 값으로 저장이 될 것이고, 이로 인해 발생하는 Shadow Acne(그림자여드름) 현상을 완화하려고 Bias를 덕지덕지 추가하게 되는 수가 있다.
Bias 값 적용에 대한 아이디어
위에서 Bias 값은 깊이 정밀도로 인해 발생하는 이슈를 해결하기 위해 등장한 개념임을 설명했다. 그러면 어떤 방식으로 Bias를 적용해야 Peter Panning 현상을 최소화하면서 시각적으로 만족스런 그림자를 그려낼 수 있을까?
Depth Bias와 Slope Bias
Depth라고 해서 화면 깊이와 연괏짓기 쉽지만 여기에서 쓰인 Depth는 조명 방향, 즉 조명이 바라보는 방향으로 월드 포지션을 살짝 더 이동시킨 위치 값을 Shadow Matrix를 통해 그림자 맵의 좌표로 변환하여 쓰는 개념이다.
이때 모든 위치 값에 대해 동일한 량의 Bias를 적용하는 것이 아니라, 물체의 표면과 조명 사이의 각도 차이, 즉 Slope에 따라서 변화하는 값을 넣는 것을 고려해볼 수 있다.
각도라 함은 dot product로 단순하게 얻을 수 있다. 현재 노멀과 라이트 사이의 각도인 NdotL 값에 따라 Light Direction 방향으로 더 많은 오프셋이 추가되도록 만든다면 이를 Slope Bias라고 부를 수 있을 것이다.
Near, Far Plane 값 지정에 대한 아이디어
모든 Shadow Caster와 Receiver를 포함하는 최소 Bounds를 계산한다는 아이디어야 흔하다. 하지만 조금 흥미로운 아이디어가 링크에 추가되어 있어서 이 부분을 언급하는 게 좋겠다고 생각했다.
그림자 Bounds를 얻을 때 카메라 View Frustum 까지 모두 포함하는 영역을 이용해 Light 공간 상에서의 AABB의 수평, 수직 정보를 구성하며, 깊이 정보는 Scene에 실제 배치된 오브젝트를 기준으로 near, far plane 값을 지정한다. 즉, 그림자가 그려지는 대상과 카메라 뷰 프러스텀을 모두 고려하여 적합한 Bounds를 구성할 수 있어야 한다는 얘기다. 근데 이건 장면을 그린다는 전제 하에 동작 기능하고 있으므로 앞으로 제작할 시스템에서 필수 고려 사항까진 아닌 듯 하다.
Near 값을 가볍게 생각하면 안되는 이유 (매우 중요)
Far 값이야 오브젝트가 다 포함될 수 있게 늘려야 한다는 건 그렇다 쳐도, 문제는 Near 값이다. 나이브하게 생각하면 near는 0이어도 상관없지 않나하는 생각이 들 수 있다. 하지만 Near 값을 어떻게 잡느냐는 Depth 정밀도에 큰 영향을 끼치게 된다.
깊이를 저장하는 방식 자체가 y = 1/x 형태의 그래프로 나타나고 Near 값이 작아지면 작아질 수록 이 그래프는 점점 더 y축에 가깝게 다가간다. 즉, 깊이 데이터 정밀도가 대부분 Near 값에 몰리는 현상이 발생한다.
Far 값이 100이라고 고정되어 있을 때 EyeDepth, 즉 View Position Z 값을 위 공식에 대입한다고 해보자.
- Near 값이 0.001인 경우
- Near 값이 0.1인 경우
그러므로 Near 값을 적당히 큰 값을 잡아주어 Depth 정밀도가 상대적으로 멀리 있는 오브젝트에도 할당될 수 있도록 하는 것이 중요하다.
물론 일부러 Z 정밀도를 올리기 위해 Near 값을 아주 작게 쓰면서 메쉬가 완전 코앞에 그려지도록 하는 방법도 얼마든지 쓸 수 있다.
결국 사용하기 나름인 셈이다. 그 사용하기 나름을 위해서 Projection Matrix의 각각의 요소의 변경이 어떤 결과를 초래하는지 파악할 필요가 있다고 느껴 언급한 것이다.
Shimmering Edge Effect / Shadow Flickering
그림자의 경계면이 자꾸 애니메이션되는 것처럼 꿀렁꿀렁하는 현상이다. 마이크로소프트 문서에는 카메라의 이동으로 인한 view projection matrix의 변경을 원인으로 꼽고 있는데, 이후로 제작할 Per Object Shadow에서도 동일한 문제가 발생한다.
캐릭터의 애니메이션에 따라 Bounds가 이리저리 크기와 위치가 이동하게 되어 그림자의 경계 부분이 자꾸 꿀렁꿀렁하는 느낌이 난다. 당연하게도 Bounds의 변형에 따라 Depth 정보가 시각적으로 느껴질 정도로 크게 차이가 나기 때문이다. 즉 카메라가 아니더라도 Projection Matrix가 크게 변경되는 경우 발생하는 시각적 아티팩트이다.
이를 보완하기 위해서 어떤 아이디어를 내볼 수 있는가 하면, 핵심은 Projection Matrix의 적은 프레임 사이에 큰 변화가 일어나지 않게 이른 바 '느슨한' 변화가 반영되도록 하는 것이다.
이건 나중에 이미지 첨부해야겠다.
'TA' 카테고리의 다른 글
[Unity] Custom Shadow System 제작하기 (2) - Directional Light Shadow Map 그리기 (0) | 2025.02.15 |
---|---|
(작성중) [Unity] URP환경에서 GPU Instancing Tool 만들기 (0) | 2025.01.20 |
(작성중) [Unity] Irradiance Volume 만들기 (짭 Adaptive Probe Volume) (0) | 2024.12.29 |
[Unity] Blend State 프로퍼티로 관리할 경우 Blend 옵션 비활성화 (0) | 2024.06.18 |
그래픽 리소스 / 렌더링 최적화에 대한 생각 (0) | 2024.06.09 |