본문으로 바로가기

Unity Noraml Map 제작/적용 (3)

category Technical Report/Tutorials 2018. 4. 14. 02:48
반응형

Unity Normal Map 제작/적용 (1) : http://illu.tistory.com/1319
Unity Normal Map 제작/적용 (2) : http://illu.tistory.com/1324

자 지금까지 노멀텍스쳐를 뽑아보았다. 이제 노멀맵이 무엇인가에 대해 개략적인 개념을 정리해보자. 개념적인게 어렵다면 그냥 이런게 있구나 정도의 개략적인 개념만 알고 있으면 충분하다. 여기서부터는 수학에 대한 이해를 많이 필요로 하기 때문에 인터넷에 있는 자료를 많이 찾아보던가 아니면 대략적인 개념만 가지고 있다면, 아티스트가 작업하는데에서는 크게 문제가 되지 않으니 너무 어렵게 접근하지 않도록 한다.

 

3D공간에서의 노멀(Normal)

주로 이와 관련한 번역문서를 보면 일본어서적의 번역 표현인 '법선'이라는 표현을 많이 사용하고 있다. 여기서는 normal-노멀 이라는 용어로 쓰겠지만 가끔 퍼온글에 보면 법선이라고 쓰인경우도 있을것이다. 둘다 그냥 그러려니 하고 넘어간다.[각주:1]

法線 - ほうせん[ 법선 ] : 평면(平面) 위의 곡선(曲線) 위의 한 점(點)에서 이 점의 접선(接線)에 직교(直交)하는 직선(直線) - 출처 : 네이버 어학사전

그래픽스에서 노멀은 버텍스당 계산하게 된다.

3D 그래픽스에서는 버텍스당 노멀값을 가지게 된다.[각주:2] 위 이미지에서 삼각형 가운데 있는것이 Normal이다. 그런데... 가끔 DCC 툴에서 face normal이라는 걸로 확인이 되는데 버텍스당 가진다는 노멀이 면에서도 가지는 것 처럼 보인다. 이는 각 버텍스의 평균값을 통해 화면에 보일뿐 실제 계산은 버텍스당 이루어진다는걸 기억하자. 버텍스당으로 계산하는건 이러한 방식이 연산에 유리하기 때문이다.

3DS max에서는 modifier에서 Edit normal로 노멀을 편집할 수 있다.

노멀 텍스쳐는 하이폴 폴리곤 모델의 노멀을 텍스쳐의 텍셀(pixel + texture), 즉 UV상의 텍스쳐 위치에 기록한 텍스쳐를 말한다. 그래서 앞의 포스팅에서 smoothing group을 나누었지만 UV Seam을 붙였을때 줄이 가는건

이렇게 되기 때문에 하이폴리곤이 로폴에 projection되어 그 노멀값이 UV 좌표에 기록될때 버텍스의 노멀값이 위 mesh에서는 두개이기 때문이다.

라이트맵/라이팅을 계산할때에는 보통 vertex normal을 기준으로 계산하기 때문에

나무같은 프랍의 경우에는 버텍스 노멀을 수정해서 라이팅이나 라이트맵을 자연스럽게 보이도록 하는 트릭을 사용하고는 한다. 아래 이미지를 가지고 비교해보자.

출처 : polycount.com(max script로 target normal을 통해 자동으로 normal을 변형하는 툴을 보통 제작해 쓴다)

자 그럼 탄젠트는 무엇인가? 일전에도 한번 공유했지만 다음 링크에 잘 설명되어 있다.
Normal, Tangent, BiTagent Vector 그리고 Unity Normal Map

출처 : 법선매핑 노멀 맵 파헤치기
아래 내용은 위 링크의 내용을 참고해 서술한다. 다만, UV좌표계는 엔진과 DCC 툴이 또 다르다..;;; 이건 그냥 참고용...

큐브를 UV좌표에 펼쳤을대 각 버텍스의 노멀은 펼쳐진 모습이므로 Z값은 위 이미지의 수직이 되므로 무조건 1이 된다.(이 이미지의 버텍스 노멀만을 노멀맵으로 표현하면 (0,0,1)이 된다. 이를 -1~1 값을 가지는 값을 0~1로 매핑하게 되면(음수를 텍스처에 기록할 순 없으니) 기준값은 (0.5, 0.5, 1) 이 된다. 즉 우리가 보통 알고 있는 푸르스름한 노멀맵의 기본 색상이 나오는 것이다.(이것이 탄젠트 노멀 텍스처가 된다)

위 세가지 이미지는 Tangent space, object space, world space normal의 차이점을 설명하고 있다. 탄젠트 공간의 노멀맵은 UV공간에서 각 버텍스의 방향을 기록하고 있기 때문에 모서리 부분을 제외하고는 모두 같은 색상으로 방향을 나타내고 있으며(+Z공간은 무조건 1을 가진다), object space는 오브젝트의 중심에서 방향을 보여주고 있기때문에 면의 방향에 따라 색상이 다르다. world space는 월드 공간 기준으로 그리기 때문에 object space와 같이 면의 방향에 따라 색상이 다르다. 둘 사이의 차이점은 월드공간 기준은 오브젝트가 회전하게 되면 면이 월드공간상에서 바라보는 방향이 달라지기 때문에 색상이 바뀐다는 것이다.

필요에 따라 staic(고정된) object의 경우 object space normal을 사용하는 경우도 있긴 하다. 여기서는 이 부분에 대해서는 다루지 않는다. 아래 이미지는 unity HDRP material에서 normal 매핑 연산을 TangentSpace에서 할건지 ObjectSpace에서 할건지 결정하는 메뉴와 GDC에서 스타워즈 배틀 프론트에서 objectSpace normal을 활용한 내용이다.

Unity Editor에서 Normal 계산을 TangentSpace로 할건지 ObjectSpace로 할건지 선택할수 있는 옵션이 있다.
GDC2016 Photogrammetry and StarWars battlefront

https://www.ea.com/frostbite/news/photogrammetry-and-star-wars-battlefront

이러한 방법에 따라 하이폴리곤에서 로우 폴리곤을 투영해서 각 픽셀의 색상을 결정하게 된다

위를 정리해보면 결국 오브젝트의 표면의 한점(UV를 기준으로)이 어느 방향으로 기울어져 있느지를 폴리곤 평면에서 수직인 노멀로 폴리곤의 기울기 값을 만들고 탄젠트의 나머지 두축으로 어느방향으로 기울었는지를 알아내는데 이 세개의 축을 접선좌표계라고 하고 하고 이 좌표계가 존재하는 공간을 접선공간(Tangent Space)라고 한다. 탄젠트 스페이스로 불리는 이유는 다음 링크를 참조한다. : https://mgun.tistory.com/1289


링크에 나와있는 내용으로 탄젠트 스페이스 노멀 텍스쳐를 주로 사용하는 이유는

  • 뽑아내기 쉽다
  • 한번 뽑아내서 여러 매쉬에 사용 가능하다.(오브젝트 좌표에 상관없이 표면-surface space-좌표만 같다면. 또한, 미러링, 회전, 크기조절 또는 변형에 상관없이 자동으로 표면에 조정된다.
  • Bone이나 바이패드를 사용하는 에니메이션이 되는 메쉬에서는 항상 Normal이 변하기 때문에 Tangent-Space Normal을 사용해야 한다.(Model Space Normal에서 Forward Kinematics 연산을 한 후의 Vertex Normal을 구하기 위해서는 에니메이션 객체의 회전과 FK 연산을 모두해줘야 하는 Overhead가 발생한다)
  • object space normal map에 비해 비싸다.(object space normal은 staic mesh에는 효율이 좋으나 미러링에는 사용할 수 없다)
  • 노멀의 단위길이 속성과 Z값을 항상 양의 값으로 제한해 하나의 구성요소를 재구성 할 수 있으며, 두개의 구성요소만 저장하면 된다

링크 내용을 간략하게 설명하면... (여기서부터는 Shader 영역이므로 어려우면 패스한다.)  노멀텍스쳐에 라이트를 반영해서 계산하려면 픽셀당으로 계산해야 하는데, 픽셀셰이더에서 계산하면 연산 부하가 어마어마 해진다. 이 부하를 줄이기 위해 해당 텍셀의 밝기를 버텍스 셰이더에서 계산해 이를 픽셀 셰이더에 전달해 계산하게 된다. 이를 위해 버텍스 셰이더에서 탄젠트 스페이스로 계산하게 된다.  vertex buffer에서 아래와 같이 읽어온다.

struct VertexInput
{
  float4 positionOS   : POSITION;        // 버텍스의 오리지널 포지션
  float3 normalOS    : NORMAL;         // 버텍스의 노멀
  float2 uv              : TEXCOORD0;     // UV정보
  float4 tangentOS   : TANGENT;         // 탄젠트. 이 값은 editor에서 계산된 값을 쓴다
};

Tangents 계산은 Calculate Mikktspace로 한다

* 보통 mesh를 export할때 맥스나 마야에서 특별한 목적이 있는 이상 tangent 값은 엔진에서 계산하게 된다(Calculate legacy). NPR 프로젝트처럼 특별한 값을 탄젠트 공간을 이용해 사용하는 경우를 제외하고. 아래 이야기하는 MiKKT방식의 경우 엔진내부에서 보정해주므로 요즘엔 unity의 경우 Mikkt가 기본으로 설정되어 있다. 아래는 Mikkt에 대한 내용이다.

http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf 
https://catlikecoding.com/unity/tutorials/rendering/part-6/

-------------------------
In modern computer graphics, material layering is critical to achieve rich and complex environments. However, conventional normal mapping does not extend to multiple sets of texture coordinates since game engines do not store more than one tangent space per vertex.
2000년 이후 실시간 3D 그래픽에서 범프 / 노멀 매핑에 대한 접근 방식은 거의 동일하게 유지되었습니다. 이는 3x3 TBN (탄젠트, 바이탄젠트 및 노멀) 매트릭스에 의한 변형으로, 탄젠트와 바이탄젠트는 버텍스 레벨에서 오프라인으로 사전 계산됩니다. 오늘날 이 전통적인 접근 방식은 그 시대의 한계를 보여주기 시작했습니다.
현대 컴퓨터 그래픽스에서 매터리얼 레이어는 풍부하고 복잡한 환경으로 구성되어 있습니다. 그러나, 게임 엔진은 버텍스당 하나 이상의 탄젠트 공간을 저장하지 않기 때문에 종래의 노멀 매핑은 여러 텍스처 좌표 세트로 확장되지 않습니다.

2. Traditional normal mapping does not allow for proper order-independent blending between different forms of bump influences such as separate sets of texture coordinates, object space normal maps and volume bump maps (such as triplanar projection, decal projectors and volumetric noise).
기존의 노멀 매핑은 별도의 텍스처 좌표, 오브젝트 공간 노멀 맵 및 볼륨 범프 맵 (삼각형 투영, 데칼 프로젝터 및 볼륨 노이즈)과 같은 다양한 형태의 범프 영향 사이에서 순서에 독립적 인 블렌딩을 허용하지 않습니다.

3. Geometry of a more procedural nature does not work well with traditional normal mapping either since generating per vertex tangent space in realtime, in every frame, is impractical. Some examples are blendshapes, tessellation, cloth, water and potentially trees.
더 많은 절차적 특성의 지오메트리는 모든 프레임에서 실시간으로 정점 탄젠트 공간을 생성하는 것이 비현실적이기 때문에 전통적인 노멀 매핑에서 잘 작동하지 않습니다. 몇가지 예를 들면 블렌드쉐입, 테셀레이션, cloth, water 및 potentially trees등 입니다.
----------------------------------------
normal과 tangent는 mesh에서 읽어오게 되고 binormal(bitangent)은 두 벡터에 외적 (cross)으로 계산된다. 아래 내용은 ShaderVariablesFunctions.hlsl에서 찾아볼 수 있다.

VertexOutput vert(VertexInput v)
   {
VertexOutput o = (VertexOutput)0;

//local space의 vertex 좌료를 clip공간으로 변환
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);

//local space의 normal 좌표를 world 공간으로 변환
o.normalWS = TransformObjectToWorldNormal(v.normalOS.xyz);

//tangents spape를 WorldSpace로 변환
o.tangentWS = TransformObjectToWorldDir(v.tangentOS.xyz);
o.bitangentWS = cross(o.normalWS, o.tangentWS) * v.tangentOS.w * unity_WorldTransformParams.w;
o.positionWS = TransformObjectToWorld(v.positionOS.xyz);

o.viewDirWS = _WorldSpaceCameraPos.xyz - TransformObjectToWorld(v.positionOS.xyz);
o.uv = v.uv.xy * _BaseMap_ST.xy + _BaseMap_ST.zw;

//VertexPositionInputs vertexInput = GetVertexPositionInputs(v.positionOS.xyz);
 half3 vertexLight = VertexLighting(o.positionWS, o.normalWS);
 half fogFactor = ComputeFogFactor(o.positionCS.z);

  #ifdef _MAIN_LIGHT_SHADOWS   
 //o.shadowCoord = GetShadowCoord(vertexInput);
   o.shadowCoord = float4(0,0,0,0);
#endif
o.fogFactorAndVertexLight = half4(fogFactor, vertexLight);
   return o;
  }

 

SpaceTransforms.hlsl

float3 TransformObjectToWorldDir(float3 dirOS, bool doNormalize = true)
{
    #ifndef SHADER_STAGE_RAY_TRACING
    float3 dirWS = mul((float3x3)GetObjectToWorldMatrix(), dirOS);
    #else
    float3 dirWS = mul((float3x3)ObjectToWorld3x4(), dirOS);
    #endif
    if (doNormalize)
     return SafeNormalize(dirWS);

    return dirWS;
}
struct VertexOutput
{

  float4 positionCS  : SV_POSITION;
  float3 normalWS    : NORMAL;

  float2 uv               : TEXCOORD0;
  float3 tangentWS    : TEXCOORD1;
  float3 bitangentWS  : TEXCOORD2;
  float3 viewDirWS    : TEXCOORD3;
  half4  fogFactorAndVertexLight     : TEXCOORD4; // x: fogFactor, yzw: vertex light
  float4 shadowCoord  : TEXCOORD5;
  float3 positionWS   : TEXCOORD6;

 };


이렇게 구해진 값을 보간기를 통해 픽셀 셰이더로 옮긴다.
float3x3 TBN = float3x3(tangentWS, binormalWS, normalWS);
TBN = transpose(TBN);
return mul(TBN, tangnetNormal);
https://bbtarzan12.github.io/Noraml-Mapping/

이를 pixel shader에서 옮겨 계산하게 된다.

https://darkcatgame.tistory.com/84

TANGENT 계산식

float3 tangent;

float3 c1 = cross (Normal, float3 (0.0, 0.0, 1.0));
float3 c2 = cross (Normal, float3 (0.0, 1.0, 0.0));

if (length (c1)> length (c2))
{
     tangent = c1;
}
else
{
     tangent = c2;
}

tangent = normalize (tangent);


BINORMAL 계산식

float3 binormal;

binormal = cross (Normal, tangent);
binormal = normalize (binormal);

 

 

 

위 이미지를 보자. 오른쪽 메쉬는 Model option에서 tangent 옵션을 끈 상태이다. 빛은 받지만 감쇠가 없는 느낌이다.
모델 옵션에 대한 내용은 : FBX export 옵션 설명에 자세히 나와있다.

이제 노멀 텍스쳐의 Flip에 대해 살펴본다.

우선 가운데 엣지를 추가해준다.

UV를 중심을 기점으로 flip되도록 펼쳐주고

Projection을 걸어 노멀 텍스쳐를 뽑아준다. 이때 주의해야 할점은

padding 값을 충분히 줘야 Seam라인에서 틀어지는걸 최대한 막을수 있다.

자 이제 유니티로 가져와보면

원본과 비교해부면 그럴싸 하게 나오는것 같다.

그렇지만 Seam라인에 줄이 가는게 보인다. bake된 노멀 텍스쳐를 보더라도 이미 줄이 가 있는게 보인다. 중간에 복잡한 문양이 들어가 있는경우에 이럴 때는 포토샵으로 편집을 하던가...

밀도가 있는 영역은 flip을 하지말고 하나로 펴주고 나머지 영역만 flip을 하는 방식이던가 Seam라인에는 복잡한 메쉬 변화가 없도록 조절하는 것이 좋다.

 

Unity에서 specular 값을 설정해 빛이 닿는 부분을 확인한 이미지

 

Unity에서 Normal은 UnpackNormal로 계산하는데 이는 다음 포스팅에서 자세한 내용을 확인할 수 있다

 

Unity Normal defined function

Unity Shader에서 unpack normal을 해주면 flip된 반대면에 대한 처리도 자동으로 해준다. 그냥 texture를 샘플링해서 normal texture를 사용했을때와 unpack 처리를 했었을때의 비교 스샷은 아래와 같다.

이렇게 노멀이 나오게 되나 Flat하게 나오며 뒤집히는 면에 대해서는 뒤집힌 그대로 계산이 되므로 음영이 틀어지게 된다.(UV 좌표가 뒤집혀 있으므로 탄젠트와 바이노멀이 반대값을 가지게 된다)

 아래는 이를 반영해 수정한 스크린샷이다.

 

Unity Normal Map 제작/적용 (1) : http://illu.tistory.com/1319

Unity Normal Map 제작/적용 (2) : http://illu.tistory.com/1324

 
  1. normal은 평범하다라는 의미로 많이 쓰이지만 수직이라는 뜻도 가지고 있다. - 예전에 왜 노멀이란 용어를 쓰는지에 대한 토론(이라 쓰고 노가리라 읽는다)이 있어서 찾아봤었다...ㅎㅎ;; [본문으로]
  2. 이를 vertex normal이라고 한다 [본문으로]
반응형