본문으로 바로가기

Normal Blending

category Technical Report/Graphics Tech Reports 2016. 4. 1. 11:22
반응형

 

출처 : http://blog.selfshadow.com/publications/blending-in-detail/

 

Blending in Detail

By Colin Barré-Brisebois and Stephen Hill.

The x, why, z

보기엔 간단한 문제입니다. : 주어진 두개의 노멀맵, 이걸 어떻게 조합하죠? 특히, 당신이 일관된 방식으로 기본 노멀맵에 디테일을 추가하는 방법은 무엇입니까? 우리는 잘 알려진 일반적인 방법 몇가지와 기존의 노멀맵핑과는 조금 다른 새로운 접근을 설명하려 합니다.

Does it Blend?

텍스처 블렌딩은 비디오 게임 렌더링에서 자주 사용됩니다. 일반적인 용도로는 재질 간 전환, 타일링 패턴 분리, 주름 맵을 통한 국소 변형 시뮬레이션, 표면에 미세한 디테일 추가 등이 있습니다. 여기서는 마지막 시나리오에 집중하겠습니다.

블렌딩의 정확한 방법은 상황에 따라 다릅니다.
알베도 맵의 경우 일반적으로 선형 보간이 적합하지만, 노멀맵은 다릅니다. 데이터는 방향을 나타내므로 색상처럼 채널을 독립적으로 처리할 수 없습니다. 속도나 편의성을 위해 채널을 무시하는 경우도 있지만, 그렇게 하면 결과가 좋지 않을 수 있습니다.

Linear Blending

이것이 실제로 어떻게 되는지 보려면 순진한 방식으로 기본 노멀맵(원뿔)에 고주파 세부 정보를 추가하는 간단한 사례를 살펴보겠습니다.

그림 1: (왼쪽에서 오른쪽으로) 기본맵, 세부맵 및 선형 혼합 결과

여기서는 일반 맵을 언패킹(unpacking)하고, 추가(add)하고, 재정규화(normalize)한 다음 마지막으로 시각화 목적으로 다시 패키징합니다.


float3 n1 = tex2D(texBase,   uv).xyz*2 - 1;

float3 n2 = tex2D(texDetail, uv).xyz*2 - 1;

float3 r  = normalize(n1 + n2);

return r * 0.5 + 0.5;
 

아웃풋은 평균화와 유사하며, 텍스처가 상당히 다르기 때문에 기본 방향과 디테일을 모두 '평평하게' 만듭니다. 이로 인해 입력 중 하나가 평평한 경우와 같은 간단한 상황에서도 직관적이지 않은 동작이 발생합니다. 아무런 영향도 없을 것으로 예상했지만, 대신 [0,0,1] 방향으로 이동합니다.

Overlay Blending

아트 측면에서 일반적인 대안은 오버레이 블렌딩 모드입니다.

그림 2: Overlay blending


float3 n1 = tex2D(texBase,   uv).xyz;

float3 n2 = tex2D(texDetail, uv).xyz;

float3 r  = n1 < 0.5 ? 2 * n1 * n2 : 1 - 2 * (1 - n1) * (1 - n2);

r = normalize(r * 2 - 1);

return r * 0.5 + 0.5;

전반적으로 개선된 것처럼 보이지만, 결합된 노멀은 여전히 ​​부정확해 보입니다. 하지만 채널을 독립적으로 처리하고 있기 때문에 이는 그리 놀라운 일이 아닙니다! 실제로 오버레이를 사용할 이유는 없지만, 다른 포토샵 블렌드 모드보다 성능이 약간 더 뛰어나다는 점, 그리고 이것이 일부 아티스트들이 오버레이를 선호하는 이유입니다.

Partial Derivative Blending

노멀 맵 대신 높이를 사용할 수 있다면 훨씬 더 합리적일 것입니다. 표준 연산이 예측 가능하게 작동하기 때문입니다. 하지만 아쉽게도 높이 맵은 생성 과정에서 항상 사용할 수 있는 것은 아니며, 셰이딩에 직접 사용하기에는 비현실적일 수 있습니다.

다행히도 노멀 맵 자체에서 쉽게 계산되는 편미분(PD)을 사용하면 동일한 결과를 얻을 수 있습니다. Jörn Loviscach가 이미 이 주제에 대해 심도 있게 다루었으므로 여기서는 이론적인 내용은 다루지 않겠습니다. 대신, 이 접근 방식을 당면한 문제에 바로 적용해 보겠습니다.

그림 3: Partial derivative blending.


float3 n1 = tex2D(texBase,   uv).xyz * 2 - 1;

float3 n2 = tex2D(texDetail, uv).xyz * 2 - 1;

float2 pd = n1.xy/n1.z + n2.xy/n2.z; // Add the PDs

float3 r  = normalize(float3(pd, 1));

return r * 0.5 + 0.5;

실제로, 견고성을 위해 3번째와 4번째 줄은 다음과 같이 대체되어야 합니다.

 

float3 r = normalize(float3(n1.xy * n2.z + n2.xy * n1.z, n1.z * n2.z));

"그림 3을 보면 출력 결과가 이전보다 훨씬 개선된 것을 확인할 수 있습니다. 결합된 맵은 이제 기대했던 것처럼 기본 맵이 약간 변형된 형태를 띠고 있으며, 또한, 편미분을 단순히 합산하는 것만으로 평평한 노멀 케이스도 정확히 처리할 수 있습니다.

아쉽게도, 이 과정은 완벽하지 않습니다. 원뿔 표면의 디테일이 여전히 은은하게 남아 있기 때문입니다. 하지만 재질 간 페이드 효과를 주는 데 사용하면 효과적입니다(예시는 [1] 또는 [2] 참조).


float2 pd = lerp(n1.xy / n1.z, n2.xy / n2.z, blend);

float3 r = normalize(float3(pd, 1));

 

Whiteout Blending

SIGGRAPH'07에서 Christopher Oat은 주름을 추가하는 목적으로 AMD Ruby: Whiteout 데모[3]에서 사용된 접근 방식을 설명했습니다.

그림 4: Whiteout blending.

이 코드는 두 번째 형태의 PD 코드와 매우 유사하지만 xy 구성 요소에 대한 z 크기 조정이 없다는 점이 다릅니다.


float3 r = normalize(float3(n1.xy + n2.xy, n1.z * n2.z));

이러한 수정을 통해 원뿔 위의 세부 사항이 더 명확하게 드러나는 반면, 평평한 노멀은 여전히 ​​직관적으로 작동합니다.

UDN Blending

마지막으로, 훨씬 더 간단한 형태가 Unreal Developer Network[4]에 등장했습니다.

그림 5: UDN blending.

이전 기술과 달라진 점은 n2.z로 곱하는 부분을 삭제했다는 것입니다.


float3 r = normalize(float3(n1.xy + n2.xy, n1.z));

이를 다른 관점에서 보면, 디테일 맵에서 x와 y만 더한다는 점을 제외하면 선형 블렌딩이라고 볼 수 있습니다.

나중에 살펴보겠지만, 이 방식은 화이트아웃(Whiteout) 방식에 비해 셰이더 인스트런션을 줄일 수 있으며, 이는 저사양 플랫폼에서 항상 유용합니다. 하지만 더 평평한 기본 노멀(base normal)에 대한 디테일 감소로 이어집니다. 하지만 더 평평한 기본 노멀(base normal)에 대한 디테일 감소도 발생합니다. 최악의 경우 출력의 모서리 부분을 참조하세요. 하지만 이는 눈에 띄지 않을 수 있습니다. 사실, 전반적으로 화이트아웃 방식과의 시각적 차이는 여기서는 감지하기 어렵습니다. 더 자세한 시각적 비교는 다음 섹션의 그림 5를 참조하세요.

Detail Oriented

이제 우리만의 방법을 살펴보겠습니다. 아티스트에게 직관적인 동작을 제공하기 위해 다음과 같은 속성을 찾고 있었습니다.

  • 논리적: 연산에 명확한 수학적 기반(예: 기하학적 해석)이 있습니다.
  • 동일성 처리 : 노멀맵 중 하나가 평평하면 출력은 다른 노멀맵과 일치합니다.
  • 평탄화 없음 : 두 노멀맵의 강도가 유지됩니다.

화이트아웃 솔루션이 잘 작동하는 것처럼 보이지만, 첫 번째와 마지막 지점에서 약간 모호합니다.

이러한 목표를 달성하기 위해, 저희 전략은 디테일 맵을 회전(또는 재배향)하여 기본 노멀맵의 '표면'을 따르도록 합니다. 이는 오브젝트 공간이나 월드 공간에서 조명을 비출 때 탄젠트 스페이스 노멀이 기본 지오메트리에 의해 변환되는 것과 같습니다. 이를 재배향 노멀 매핑(RNM)이라고 합니다. 마지막 두 가지 기법과 비교한 결과는 다음과 같습니다.

디테일의 차이가 눈에 띄며, 이는 최종 셰이딩에서도 드러납니다(마지막 데모 참조).

분명히 말씀드리자면, 이런 아이디어를 생각해 낸 사람이 저희뿐만은 아닙니다.
Unity 기술 데모의 일부로 피부에 모공을 추가하는 방식으로 개발된 동일한 아이디어가 최근 GDC에서 Renaldas Zioma[5]에 의해 발표되었습니다. 아마도 이전 예시도 있을 것이지만, 아직은 찾기가 어렵습니다. 그럼에도 불구하고, 저희의 접근 방식이 Unity 방식보다 몇 가지 장점이 있는데, 구현 과정을 자세히 살펴보면서 설명드리겠습니다.

The Nitty Gritty

좋아요, 수학 문제를 풀어보겠습니다. 지오메트리 노멀 s, 기본 노멀 t, 그리고 보조(또는 detail) u가 있다고 가정해 보겠습니다. s를 t로 회전시키는 변환을 생성한 다음, 이를 u에 적용하여 재배치된 노멀 r을 계산할 수 있습니다.

그림 7: 디테일 노멀 u(왼쪽)를 기본 노멀맵(오른쪽)을 따르도록 다시 조정합니다.

우리는 가장 짧은 아크 쿼터니언[6]을 통해 이 변환을 달성할 수 있습니다.

그런 다음 표준 방식[7]으로 수행할 수 있습니다.

[8]에서 보여지는 것처럼 이는 다음과 같이 축소됩니다.

탄젠트 공간에서 작업하므로 관례상 s=[0,0,1]입니다. 따라서 (1)을 (4)에 대입하여 단순화하면 다음과 같습니다.

이는 다음과 같이 더욱 간략하게 요약됩니다.

다음은 (7)의 HLSL 구현으로, 법선의 언패킹에 추가 사항과 부호 변경 사항을 적용한 것입니다. 편의상, u와 t는 위 t′와 u′입니다.


float3 t = tex2D(texBase,   uv).xyz * float3( 2,  2, 2) + float3(-1, -1,  0);

float3 u = tex2D(texDetail, uv).xyz * float3(-2, -2, 2) + float3( 1,  1, -1);

float3 r = t * dot(t, u)/t.z - u;

return r * 0.5 + 0.5;

이 방법의 잠재적으로 멋진 특성은 t가 단위 길이일 때 u의 길이가 유지된다는 것입니다. 따라서 u도 단위 길이이면 정규화가 필요하지 않습니다! 하지만 양자화, 압축, 밉맵핑, 필터링 때문에 실제로는 이 특성이 적용되지 않을 가능성이 높습니다. 디퓨즈 셰이딩에는 큰 영향을 미치지 않을 수 있지만, 에너지 보존형 스페큘러에는 실질적인 영향을 미칠 수 있습니다. 따라서 결과를 정규화하는 것이 좋습니다.


float3 r = normalize(t * dot(t, u) - u * t.z);

Devil in the Details

이 논문을 준비하는 동안, Jeppe Revall Frisvad [9]가 로컬 벡터 회전에 동일한 전략을 사용하는 논문을 발표할 예정이라는 사실을 알게 되었습니다. HLSL에 맞춰 수정한 관련 코드는 다음과 같습니다.


float3 n1 = tex2D(texBase,   uv).xyz * 2 - 1;
float3 n2 = tex2D(texDetail, uv).xyz * 2 - 1;

float a = 1/(1 + n1.z);
float b = -n1.x * n1.y * a;

// Form a basis
float3 b1 = float3(1 - n1.x * n1.x * a, b, -n1.x);
float3 b2 = float3(b, 1 - n1.y * n1.y * a, -n1.y);
float3 b3 = n1;


if (n1.z < -0.9999999) // Handle the singularity
{
    b1 = float3( 0, -1, 0);
    b2 = float3(-1,  0, 0);
}

// Rotate n2 via the basis
float3 r = n2.x*b1 + n2.y*b2 + n2.z*b3;

return r*0.5 + 0.5;

저희 버전은 GPU를 염두에 두고 작성되었으며, 이 맥락에서 ALU 연산이 덜 필요합니다. 반면 Jeppe의 구현은 몬테카를로 샘플링처럼 기저를 재사용할 수 있는 상황에 적합합니다. 또 다른 주목할 점은 기저 노멀의 z 성분이 -1일 때 특이점이 발생한다는 것입니다.

Jeppe는 이를 확인하지만, 저희의 경우 아트 파이프라인 내에서 이를 방지할 수 있습니다.


더 중요한 것은 z가 ≥0이어야 한다고 주장할 수 있지만, 저희 방법의 출력에 항상 해당되는 것은 아니라는 점입니다! 잠재적인 문제 중 하나는 제작 과정에서 재배열을 사용한 후 2성분 노멀 맵 형식으로 압축하는 경우입니다. 재구성은 일반적으로 z가 ≥0이라고 가정하기 때문입니다. 가장 간단한 해결책은 z를 0으로 고정하고 압축 전에 재정규화하는 것입니다.

셰이딩과 관련하여, 음의 z 값(즉, 재배열된 노멀이 표면을 가리키는 경우)으로 인한 부정적인 영향은 확인되지 않았지만, 이는 분명히 염두에 두어야 할 사항입니다. 여러분의 경험을 듣고 싶습니다.

Unity Blending

이제 Unity 기술 데모에서 사용된 접근 방식으로 돌아가 보겠습니다. Jeppe와 마찬가지로 Renaldas도 2차 법선을 변환하기 위해 기저를 사용합니다. 이는 y축과 x축을 중심으로 기저 법선을 회전하여 행렬의 다른 두 행을 생성함으로써 생성됩니다.


float3 n1 = tex2D(texBase,   uv).xyz*2 - 1;
float3 n2 = tex2D(texDetail, uv).xyz*2 - 1;


float3x3 nBasis = float3x3(
    float3(n1.z, n1.y, -n1.x), // +90 degree rotation around y axis
    float3(n1.x, n1.z, -n1.y), // -90 degree rotation around x axis
    float3(n1.x, n1.y,  n1.z));

float3 r = normalize(n2.x*nBasis[0] + n2.y*nBasis[1] + n2.z*nBasis[2]);

return r*0.5 + 0.5;

참고: 이 코드는 "Unity로 DirectX 11 마스터하기" 슬라이드에 있는 코드와 약간 다릅니다. 기저의 첫 번째 행이 수정되었습니다.

그러나 기저는 n1이 [0, 0, ± 1]일 때만 정규분포를 벗어납니다. 그리고 너멀이 이 두 방향 중 하나에서 더 멀어질수록 상황은 점진적으로 악화됩니다. 시각적 예로, 그림 8은 n1을 x축으로 회전하고 n2 대신 상반구(+z)의 점 집합을 변환할 때 발생하는 상황을 보여줍니다.

그림 8 : Unity Bias(위쪽 행) vs 쿼터니언 변환(아래쪽 행).

Unity에서는 n1이 x축에 도달하면 기저가 다음과 같이 되어 점이 원으로 축소됩니다.

반면, 쿼터니언 변환에서는 이러한 문제가 발생하지 않습니다. 이는 블렌딩된 출력에도 반영됩니다.

그림 9 : 재정향된(Reoriented) 노멀매핑(왼쪽) vs Unity 방식(오른쪽).

Start Your Engines

대표적인 성능 수치를 제시하는 것은 불가능한 일입니다. 플랫폼, 주변 코드, 노멀 맵 인코딩 선택(게임에 따라 다를 수 있음), 심지어 셰이더 컴파일러까지 여러 요인에 따라 크게 달라지기 때문입니다.

참고로, 텍스처 읽기와 리패키징을 제외한 다양한 기법의 핵심 부분을 셰이더 모델 3.0 가상 명령어 집합(부록 참조)에 맞춰 최적화했습니다. 명령어 수 측면에서 각 기법의 성능은 다음과 같습니다.

Method SM3.0 ALU Inst.
Linear 5
Overlay 9
PD 7
Whiteout 7
UDN 5
RNM * 8
Unity 8

표 1 : 다양한 방법에 대한 instruction 비용.

* 여기에는 정규화가 포함됩니다. 정규화가 필요하지 않은 경우 RNM은 6개의 ALU 명령어로 구성됩니다.

실제로 GPU는 일부 명령어를 쌍으로 처리할 수 있으며, 특정 연산은 다른 연산보다 비용이 더 많이 들 수 있습니다. 특히 normalize는 여기서 dot rcp mul로 확장되지만, 특정 콘솔은 반정밀도에서 단일 명령어 nrm을 제공합니다.

공간(과 시간!) 측면에서, 2성분 노멀맵 인코딩에 대한 코드와 통계는 포함하지 않았지만, z 재구성은 대부분의 방법에서 유사할 것입니다. 한 가지 예외는 UDN인데, 디테일 노멀의 z 성분이 사용되지 않기 때문에 이 경우 이 기법이 특히 매력적입니다.

A Light Demo

이제 조명 아래에서 이러한 방법들이 어떻게 비교되는지 궁금하실 텐데요, 움직이는 광원을 사용한 간단한 WebGL 데모를 소개합니다. RenderMonkey project, 도 준비했으니, 직접 만든 텍스처로 쉽게 테스트해 보세요.

Conclusions

분석과 결과를 바탕으로, 디테일 노멀 매핑 측면에서 선형 및 오버레이 블렌딩은 아무런 장점이 없다는 것이 분명해졌습니다. GPU 사이클이 매우 중요한 경우에도 UDN이 더 나은 선택이며, 포토샵에서도 쉽게 복제할 수 있을 것입니다.

화이트아웃이 UDN보다 유리한지 여부는 텍스처와 셰이딩 모델에 따라 달라질 수 있습니다. 이 예시에서는 두 가지를 구분하는 것이 거의 없습니다. 이러한 점 외에도 RNM은 더 많은 디테일을 유지하면서도 유사한 명령어 비용으로 효과를 발휘할 수 있으므로, RNM이 매력적인 대안이 되기를 바랍니다.

두 가지 구성 요소 형식 외에도 페이딩 전략, 패럴랙스 매핑과의 통합, 반사 방지 안티앨리어싱에 대해서는 다루지 않았습니다. 이러한 주제는 향후 다루어 보고자 합니다.

Acknowledgements

첫째, 블렌딩을 위해 사원수를 사용하여 법선을 회전하는 아이디어를 처음 제안한 Gabriel Lassonde에게 감사드립니다. 둘째, 도움이 되는 의견을 주신 Pierric Gimmig, Steve McAuley, Morgan McGuire에게 감사드리며, 예시 노멀맵을 만들어 주신 David Massicotte에게도 감사드립니다.

References

[1] Loviscach, J., “Care and Feeding of Normal Vectors”, ShaderX^6, Charles River Media, 2008.
[2] Mikkelsen, M., “How to do more generic mixing of derivative maps?”, 2012.
[3] Oat, C., “Real-Time Wrinkles”, Advanced Real-Time Rendering in 3D Graphics and Games, SIGGRAPH Course, 2007.
[4] “Material Basics: Detail Normal Map”, Unreal Developer Network.
[5] Zioma, R., Green, S., “Mastering DirectX 11 with Unity”, GDC 2012.
[6] Melax, S., “The Shortest Arc Quaternion”, Game Programming Gems, Charles River Media, 2000.
[7] Akenine-Möller, T., Haines, E., Hoffman, N., Real-Time Rendering 3rd Edition, A. K. Peters, Ltd., 2008.
[8] Watt, A., Watt, M., Advanced Animation and Rendering Techniques, Addison-Wesley, 1992.
[9] Frisvad, R. J., “Building an Orthonormal Basis from a 3D Unit Vector Without Normalization”, Journal of Graphics Tools 16(3), 2012.

Appendix

Optimised blending methods


float3 blend_linear(float4 n1, float4 n2)
{
    float3 r = (n1 + n2)*2 - 2;

    return normalize(r);
}


float3 blend_overlay(float4 n1, float4 n2)
{
    n1 = n1*4 - 2;
    float4 a = n1 >= 0 ? -1 : 1;
    float4 b = n1 >= 0 ?  1 : 0;

    n1 =  2*a + n1;
    n2 = n2*a + b;

    float3 r = n1*n2 - a;

    return normalize(r);
}


float3 blend_pd(float4 n1, float4 n2)
{
    n1 = n1*2 - 1;
    n2 = n2.xyzz*float4(2, 2, 2, 0) + float4(-1, -1, -1, 0);
    float3 r = n1.xyz*n2.z + n2.xyw*n1.z;

    return normalize(r);
}


float3 blend_whiteout(float4 n1, float4 n2)
{
    n1 = n1*2 - 1;
    n2 = n2*2 - 1;

    float3 r = float3(n1.xy + n2.xy, n1.z*n2.z);

    return normalize(r);
}


float3 blend_udn(float4 n1, float4 n2)
{
    float3 c = float3(2, 1, 0);
    float3 r;

    r = n2*c.yyz + n1.xyz;
    r =  r*c.xxx -  c.xxy;

    return normalize(r);
}


float3 blend_rnm(float4 n1, float4 n2)
{

    float3 t = n1.xyz*float3( 2,  2, 2) + float3(-1, -1,  0);
    float3 u = n2.xyz*float3(-2, -2, 2) + float3( 1,  1, -1);
    float3 r = t*dot(t, u) - u*t.z;

    return normalize(r);
}


float3 blend_unity(float4 n1, float4 n2)
{
    n1 = n1.xyzz*float4(2, 2, 2, -2) + float4(-1, -1, -1, 1);
    n2 = n2*2 - 1;

    float3 r;

    r.x = dot(n1.zxx,  n2.xyz);
    r.y = dot(n1.yzy,  n2.xyz);
    r.z = dot(n1.xyw, -n2.xyz);

    return normalize(r);
}

(4)에서 (5)까지의 단순화 단계

s=[0,0,1]이므로 :

반응형