셰이더에서 FOV 값 계산하기.
책만 출판사를 통해 내년 4월 출간을 예정으로 둔 "유니티 6.0 셰이더프로그래밍의 모든것" 책의 8장 일부를 선공유 해 봅니다.여전히 탈고를 위해 많은 내용을 더 써야하지만... 책 제목이 유니티 6.0 셰이더프로그래밍의 모든것 이기 때문에 최대한 읽기 쉬우면서도 실무적인 내용을 빼놓지 않으려고 몇 번을 고치고 수정하는 중입니다. 물론 선공유 한 내용도 탈고 후에는 어떻게 바뀌어 출판 될지는 아직 알수 없지만요.
8.4.5 카메라 거리 및 FOV값과 아웃라인 두께 연동하기
게임 유투버들이 사용하는 버튜버 캐릭터처럼 캐릭터과 화면거리사이의 관계가 거의 고정일 때는 큰 의미가 없겠지만 인터렉티브 게임을 개발하다 보면 카메라 안에 여러 캐릭터가 동시에 등장하게된다. 이때 특정 거리를 기준으로 앞에 있는 캐릭터의 아웃라인과 특정 거리 뒤에 있는 아웃라인이 동일한 값을 취하고 있다면 매우 어색한 결과로 보여지게 된다.
캐릭터 셰이딩을 개발할 때 게임 카메라의 기본거리를 기준으로 아웃라인 머티리얼의 파라메터가 결정되게 되지만 카메라에서 멀리 떨어져 있는 캐릭터의 아웃라인 두께는 고정값이고 화면공간에서 계산했음으로 의도하지 않은 결과를 만들어 낼 수 있다. 미리 정확히 언급하자면 아트디렉터의 의도에 의해 이렇게 해야 할 수도, 아닐 수도 있다.
가장 먼저 MainCamera 와 SceneView 카메라 설정을 일치 시켜야 한다. 아무래도 GameView 와 SceneView 에서 보여지는 것이 다르면 적합한 중간값을 유도하는 부분에 문제가 생길 여지가 있다.
저자는 MainCamera 의 Field of View 값을 45 그리고 Clipping Planes 의 Near 값을 0.1, Far 값을 50 으로 설정 했다. 그렇다면 SceneView 도 동일하게 설정한다.
[그림 31. 저자의 MainCamera 설정값]
SceneView 우측 상단의 카메라 아이콘을 클릭 하고 설정을 변경 하자.
[그림 32. SceneView 우측 상단의 카메라 아이콘]
Scene Camera 설정창이 팝업 됐다면 값을 Field of View 역시 45로 수정하고 Dynamic Clipping 을 끈 후 Clipping Planes 의 Near 와 Far 를 각각 0.1 과 50으로 수정 해 주면 된다.
[그림 33. SceneView 카메라 설정값]
FOV를 GameView 와 SceneView 모두 일치 시켰다는 것은 FOV 값이 뭔가 의미가 있다는 뜻이겠다. 게임 카메라에는 거리감을 위해 두 가지를 변경하게 되는데 하나는 실제 피사체 로부터 카메라의 물리적인 거리(Distance) 이며 하나는 Field of View 이다. 여자친구와 여행을 가서 사진을 멋지게 찍어주고 칭찬을 듣기위해서는 카메라를 들고 앞뒤로 움직이면서 카메라 렌즈의 FOV 를 함께 수정해야 했을것이다. 게임마다 Field Of View 는 약간씩 다를테고 버튜버 캐릭터가 주로 상반신의 정면 앵글로 고정 된다면 Field Of View 는 50 보다 훨씬 작을 것이다. 캐릭터 선택창 등에서의 FOV 도 꽤 작은 편인데 커스터마이징 창에서 얼굴을 클로즈 업 했을 때의 FOV 도 동적으로 수정 되었을 것이다. 아무튼 거리감을 변경하는 요소에 이 두가지가 있다고 알아두자.
유니티 엔진에서 MainCamera 의 FOV 값을 셰이더에서 직접 엑서스 하는 방법은 일반적으로 C# 을 사용하여 Camera 의 FOV 값을 취득 한 후 SetGlobalFloat 등의 API 를 사용하여 Outline 셰이더에 미리 준비 해 둔 Uniform 변수에 값을 전달하는 방법을 사용한다.
하지만 이 절에서는 C# 스크립트를 사용한 프로그래밍에 대해서 다루지 않을 것이기 때문에 부득이 unity_CameraProjection 의 행렬을 이용하여 셰이더 내부에서 직접 FOV 를 계산 하도록 하겠다.
unity_CameraProjection 의 4 by 4 matrix 의 특성을 이용하여 FOV 값을 계산 할 수 있는데 이럴 때 동 출판사에서 출간 했던 “이득우의 게임수학”을 독자분들이 미리 읽었더라면 더 쉽게 이해 할 수 있을 것이라는 생각이 불현듯 저자의 머릿속을 스쳤다.
유니티의 카메라 투영행렬은 아래와 같은 형태를 사용한다. Mxx는 Matrix Identifier list 이며 행과열 번호를 나타낸다. 책 제목에 셰이더 프로그래밍이라는 단어가 들어가 있으니 셰이더에서 계산 해 보는 것도 나쁘지 않은것 같다.
_m11 은 Unity 엔진과 셰이더에서 사용하는 투영행렬의 특정 요소를 의미하며 카메라 프로젝션 표준 행렬에서의 _m00 는 FOV, _m11 은 종횡비, _m22와 _m23은 클리핑 평면에 의해 결정된다.
유니티 엔진 내부에서 GetFrustumPlaneSizeAtFixedAspect()의 함수에서도 표준 투영 행렬을 사용하는 경우 이와 유사한 형식을 사용하고 있다.
float cotangent = m_State.m_ProjectionMatrix.m_Data[5]; //_m11
float calculatedAspect = cotangent / m_State.m_ProjectionMatrix.m_Data[0]; // _m00
float fov = atan(1.0f / cotangent) * 2.0 * kRad2Deg;
이 설명을 완전히 이해 할 필요는 없으며 셰이더에서 이렇게 계산 할 수 있는 방안이 있다는 것을 아는 것 정도면 충분하다. 행렬이 나왔다고 해서 너무 부담스럽게 생각할 필요 없다. 자신만의 저장소에 잘 기억해 두면 된다.
아래와 같은 코드블럭을 float DotShadingCalculate( Light light, float3 vertexNormal)함수 블록 아래에 작성 한다.
// 8.4.5 Outline Thickness 를 구현하기 위한 코드
// 원주율 PI 를 정의한다.
#ifndef kPI
#define kPI 3.141592f
#endif
// Degree 와 Radian 변환 상수를 정의한다.
#define Deg2Rad (2.0F * kPI / 360.0F)
#define Rad2Deg (1.F / Deg2Rad)
// 유니티 카메라의 FOV 를 셰이더에서 직접 계산하기 위한 함수.
float CacluateCameraFOV()
{
float fov = atan(1.0f / unity_CameraProjection._m11) * 2.0 * Rad2Deg;
return fov; // 수직 FOV 반환
}
_m11은 투영 행렬에서 Y축 방향 스케일링 값이기 때문에 카메라의 수직 시야각인 FOV에 따라 계산 된다. 자 우리는 지금 FOV 값을 모르고 있다고 가정해야한다. _m11 이 작아지거나 커지면 FOV 가 넓어지거나 좁아지는 특징을 참조 할 수 있음으로 _m11과 FOV 의 관계를 사용하여 역으로 계산할 수 있을 것이다. 자 이제 셰이더에서 카메라의 수직 FOV 값을 계산 해 보자. 역탄젠트 atan 함수를 활용하여 FOV의 절반값을 취하고 거기에 다시 2를 곱하여 전체 FOV 값을 계산 할 수 있다. atan 함수는 라디안 단위로 반환하는 특성이 있음으로 다시 degree(도) 단위 변환을 하기 위해 미리 정의 해 둔 Rad2Deg 를 곱하여 최종 FOV 값으로 반환했다.
위에서 작성한 함수를 Outline.shader 에 추가 했다면 셰이더에서 구한 FOV 값을 사용하여 아웃라인 두께등을 제어하는 최종 함수를 구현해 보자.
이미 유니티 카메라의 표준행렬을 통해 FOV 값을 얻어 내었으니 이 결과를 두께에 반영해 보자.
Varyings Vert(Attributes input)
{
// 위 생략...
// DotShadingCalculate 결과를 반전 하기 위하 oneMinus 처리한다.
t = 1.0 - t;
// t 값을 0 ~ 1 로부터 _ThicknessRatio ~ 1 로 변환한다.
t = lerp(_ThicknessRatio, 1, t);
// 8.4.5 Outline Thickness 를 구현하기 위한 코드
// Outline Thickness 를 FOV 값을 가중치로 둔 positionVS 의 z 값을 곱.
float3 positionVS = vertexInput.positionVS;
// positionVS 의 z 값을 절대값을 취하고, 0 ~ 1 사이의 값을 가지도록 saturate 함수를 적용.
positionVS.z = saturate(abs(positionVS)).z;
// CacluateCameraFOV 함수를 이용하여 FOV 값을 취한다.
positionVS.z *= CacluateCameraFOV();
// positionVS 의 z 값을 0.00005 로 곱하여 _Thickness 에 대입.
_Thickness *= positionVS.z * 0.00005;
// 아래 생략…
positionVS 는 “뷰 공간 좌표 기준 정점 위치”이며 z 축 값을 사용한다. abs 함수를 사용하여 z 축 값의 절대값을 가져와 거리를 계산해야 객체가 카메라 뒤에 있어도 결과를 모두 양수로 반환한다. 그리고 saturate 를 사용해 반환 된 값이 0에서 1 사이값이 되도록 해야 아웃라인 두께의 비정상적 출력을 방지할 수 있다.
수직 FOV를 반환하는 함수 CalculateCameraFOV를 positionVS.z 에 곱한다. FOV 값이 크거나 작아질 때 아웃라인 두께를 화면상에서 일관된 두께로 렌더링 할 수 있게 한다.
미리 추가 해 둔 _Thickness 변수에 positionVS.z 를 곱한다. 이 상태에서는 두께가 매우 크게 렌더링 될 것이다. 상수 0.00005 인 magic value 를 곱해주자.
FOV 에 의한 거리에 따라 _Thickness 값을 변경하는 처리가 완료 되었다면 다음으로는 실제 카메라와 객체의 거리에 따라 두께를 유지하는 코드블럭을 추가 해 보자.
이 부분은 정점의 월드 공간을 사용한다. 유니티에서 기본적으로 제공하는 _WorldSpaceCameraPos 를 활용한다. _WorldSpaceCameraPos 는 유니티 셰이더에서 제공하는 내장 변수이며 카메라의 월드 공간 위치를 의미한다.
Varyings Vert(Attributes input)
{
// 위 생략...
// positionVS 의 z 값을 0.00005 로 곱하여 _Thickness 에 대입.
_Thickness *= positionVS.z * 0.00005;
// 월드 공간 거리 보정을 위한 코드
float distancePreserveOutlineThickness = distance(_WorldSpaceCameraPos, vertexInput.positionWS.xyz);
// 카메라와 정점 간의 유클리드 거리를 계산 했으며 당 값을 곱하여 객체가 멀리있을수록 두께가 유지되도록 한다.
_Thickness *= distancePreserveOutlineThickness;
// 아래 생략…
카메라와 정점간의 거리를 계산 한 후 이 값을 곱해 객체가 멀리 있을 경우라도 아웃라인 두께가 자연스럽게 줄어들어보이도록 보정하는 역할을 수행하게 된다. distance함수를 사용하면 간단히 유클리디안 거리(Euclidean Distance)를 구할 수 있다.
float 형 변수 distancePreserveOutlineThickness 를 추가하고 _WorldSpaceCameraPos 와 vertexInput.PositionWS 사이의 유클리디안 거리를 구한 값을 대입하자.