Normal(법선)이란?
Normal이란 메쉬의 버텍스 방향 정보를 뜻합니다, 일반적으로 물체의 표면 방향을 의미합니다.
위와 같이 단순한 구체 모델은 Normal 방향이 정직하게 표면 방향을 향합니다.
하지만 위와 같이 울퉁불퉁한 모델을 렌더링하기 위해서는 수 많은 폴리곤이 존재해야 표현이 가능합니다.
그런데 Normal 방향이 필요한 이유는 빛을 어디로 반사하여 음영처리 될것인가가 핵심입니다.
즉 폴리곤이 필요한게 아니라 Normal 방향 정보가 필요한 것입니다.
그 Normal 방향 정보를 담는 데이터를 Normal Texture라고 합니다.
Unity Vertex & Fragment Shader Source
Shader "Unlit/Sh_NormalMap2"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_BumpTex("Normal Texture", 2D) = "bump" {}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 T : TEXCOORD1;
float3 B : TEXCOORD2;
float3 N : TEXCOORD3;
float3 lightDir : TEXCOORD4;
half3 viewDir : TEXCOORD5;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
void Fuc_LocalNormal2TBN(half3 localnormal, float4 tangent, inout half3 T, inout half3 B, inout half3 N)
{
half fTangentSign = tangent.w * unity_WorldTransformParams.w;
N = normalize(UnityObjectToWorldNormal(localnormal));
T = normalize(UnityObjectToWorldDir(tangent.xyz));
B = normalize(cross(N, T) * fTangentSign);
}
half3 Fuc_TangentNormal2WorldNormal(half3 fTangnetNormal, half3 T, half3 B, half3 N)
{
float3x3 TBN = float3x3(T, B, N);
TBN = transpose(TBN);
return mul(TBN, fTangnetNormal);
}
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.lightDir = WorldSpaceLightDir(v.vertex);
o.viewDir = normalize(WorldSpaceViewDir(v.vertex));
Fuc_LocalNormal2TBN(v.normal, v.tangent, o.T, o.B, o.N);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
half3 fTangnetNormal = UnpackNormal(tex2D(_BumpTex, i.uv * _BumpTex_ST.rg));
fTangnetNormal.xy *= 1.5f; // 노말강도 조절
float3 worldNormal = Fuc_TangentNormal2WorldNormal(fTangnetNormal, i.T, i.B, i.N);
fixed fNDotL = dot(i.lightDir, worldNormal);
float3 fReflection = reflect(i.lightDir, worldNormal);
fixed fSpec_Phong = saturate(dot(fReflection, -normalize(i.viewDir)));
fSpec_Phong = pow(fSpec_Phong, 20.0f);
return float4(col.rgb * fNDotL + fSpec_Phong, 1);
}
ENDCG
}
}
}
코드 분석
1. appdata 구조체
appdata구조체는 메쉬의 버텍스에서 얻어오는 정보입니다, 여기서 중요한 정보는 NORMAL(법선)과 TANGENT(접선)입니다.
Normal(법선) : 버텍스의 수직을 향하는 방향
Tangnet(접선) : 버텍스의 수평을 향하는 방향
2. Vertex Shader - Tangent Space(접선 공간)
Tangent Space란 흉악한 이름과 달리 단순하게 메쉬의 폴리곤 기준의 좌표계를 뜻합니다.
다시 말해서 메쉬의 폴리곤 기준의 좌표계를 뜻합니다.
Normal(법선) | 버텍스의 수직을 향하는 방향 |
Tangent(접선) | 버텍스의 수평을 향하는 방향 |
BiNormal(종법선) | Normal과 Tangent를 외적(Cross)하여 나온 방향 |
일반 적인 2D 평면에서 좌표 축이 있으면 정확한 위치를 지명할수 있듯이 Tangent Space 내부에서 정확한 위치를 지명하기 위해 위 3가지 벡터 정보가 필요합니다.
각각 벡터는 버텍스 쉐이더에서 T, B, N으로 연산해서 v2f구조체를 통해 픽셀쉐이더로 보냅니다.
3.1 Pixel Shader - Normal map
-노말맵이 푸른색인 이유-
노말맵은 보통 위 예시와 같이 푸른색입니다, 저 색을 포토샵에서 제현해 보자면
RG를 각각 절반값, B를 최대값으로 했을때 나오는 색과 비슷합니다, 벡터로 표현하면 (0.5, 0.5, 1.0)이죠.
그 이유는 노말맵은 앞장에서 설명한 Tangent Space 기준으로 노말방향 데이터가 들어있는 텍스처입니다.
정확히 말해서 Tangent Space에서의 Normal벡터(Z)가 항상 위(Z)방향을 향하고 있기 때문에 이 데이터를 RGB로 바꿨을때 B값이 높아 푸른색을 띄게 됩니다.
또한 Z값은 항상 표면 밖을 향하기 때문에 B값이 높아 푸른색의 띄게 됩니다.
3.2 Pixel Shader - UnpackNormal
노멀맵 텍스처를 읽는 부분은 tex2D에 UnpackNormal이라는 함수를 사용하고 있습니다.
이 함수는 푸른 노말 텍스처를 이전에 계산한 TBN과 연산하기 위해 데이터 변환하는 기능을 합니다.
- GLES, MOBILE : 모바일환경 처리
UnityCG.cginc에 들어있는 UnpackNormal 코드를 분석해보면
모바일 혹은 그외 디바이스로 전처리 조건문이 나눠져있습니다, 우선 모바일쪽 코드부터 분석해보겠습니다.
기본 적으로 방향벡터의 값은 위 사진처럼 -1 ~ 1 값으로 되어 있어야합니다, 하지만 텍스처의 값은 0~1로 되어 있기에
읽은 노멀맵정보 * 2 - 1 연산을 하여 0~1 값을 -1 ~ 1 값으로 변환하는 코드입니다.
(계산기 키고 0, 0.5, 1 값을 대입해보면 바로 이해됩니다.)
-1~1 값으로 변환된 노말맵 데이터는 다음 장에서 픽셀노말 연산에 사용되게 됩니다.
- 그외, PC 환경 처리
다음 모바일이 아닌 구문의 코드입니다, 텍스처 포맷을 DXT5를 사용하는 PC 디바이스를 의미합니다.
위 코드를 보면 읽은 노말맵의 xyz를 사용하는 것이 아닌 wy(G채널, A채널)을 사용해 계산합니다, 이 이유는
유니티에서 텍스처 포맷 정보를 보면 DXTnm으로 표시되어 있는데 이는 DXT5 포맷으로 이 포맷은 채널압축이 R/5bit, G/6bit, B/5bit, A/8bit 으로 변환됩니다, 그래서 노말맵을 더 섬세하게 표현하기 위해 포맷 압축단계에서 R=>G, G=>A로 텍스처 데이터를 채널 이동시켜서 포맷 저장합니다.(B채널은 사용하지 않습니다.)
다시 코드로 돌아와서 앞선 포맷 설정떄문에 wy(G채널, A채널)을 이용해 노말의 XY값을 계산하고 Z값은 XY값을 X^2 + Y^2 + Z^2 = 1 공식으로 추출하여 데이터를 계산합니다.
3.3 Pixel Shader - Tangent Space -> World Space 변환
이번 단계에서는 노말맵 데이터에서 얻은 픽셀 노말 벡터 데이터로 월드 노말 벡터를 계산하는 단계입니다.
이전 버텍스 쉐이더에서 계산한 T(Tangent), B(BiNormal), N(WorldNormal)을 float3x3 행렬 만들었습니다.
위 행렬은 현재 아래와 같은 상황입니다.
{T, T, T}
{B, B, B}
{N, N, N}
위 3개의 벡터는 위 사진과 같이 되어 있으며 TBN행렬에 정규화(Normalize)해서 표현하면
{1, 0, 0}
{0, 1, 0}
{0, 0, 1}
위와 같이 표현됩니다, 이 행렬의 형태는 MVP 좌표변환 할때 Model과정의 이동행렬(Translation)과 동일합니다, 즉 해당 행열과 노말맵에서 추출한 탄젠트 벡터와 곱하면 월드 좌표로 변환이 가능합니다.
최종 TBN행렬과 노말맵에서 얻은 탄젠트 벡터를 곱해서 월드좌표계 노말 벡터를 얻었습니다.
추가 설명으로 TBN에 transpose(전치행렬)을 한 이유는 마지막에 mul함수에서 행렬 x 벡터 계산을 시행했는데 행렬의 곱은 행기준으로 이루어 지기 때문에 전치행렬로 행과 열을 바꿨습니다.
그러므로 전치행렬을 시행하지 않고 곱셈 순서만 바꿔도 결과는 동일합니다.
(인터넷에 오픈소스들이 전부 행렬x벡터 순서로 곱셈하고 있는데 이유는 모르겠습니다.)
4. 마무리
위 과정에서 픽셀 월드 노말 벡터를 얻었으니 해당 데이터를 이용해 NDotL, Phong등 조명 쉐이더 연산하여 완성했습니다.
'Unity > Shader & VFX' 카테고리의 다른 글
Vertex Fragment - Shadow Receive & Cast (0) | 2020.02.12 |
---|---|
[Stencil] 3D 영정사진 (0) | 2020.02.10 |
Hexagon Barrier (1) | 2020.01.31 |
[Unity Shader Effect] Cosmic Shader - ScreenPos UV (1) | 2020.01.08 |
[Unity PostProcess] Depth Buffer & Depth of Field (0) | 2020.01.03 |
WRITTEN BY
- CatDarkGame
Technical Artist dhwlgn12@gmail.com