본문 바로가기

Graphics/Ronja's Unity Shader tutorials

트라이플래너 매핑 (Triplanar Mapping)

 

 

Triplanar Mapping

Summary I made a tutorial about planar mapping previously. The biggest disadvantage of the technique is that it only works from one direction and breaks when the surface we’re drawing isn’t oriented towards the direction we’re mapping from (up in the

www.ronja-tutorials.com

Ronja 님의 허락을 받고 번역한 튜토리얼입니다. 원문은 위 링크에서 확인하실 수 있습니다.
몇몇 부분은 생략·추가하였습니다.
의역과 오역이 넘쳐날 수 있으니 편하게 봐주시고 잘못된 부분은 알려주시면 감사하겠습니다!

 

목차

  • 개요
  • 평면 투영 계산
  • 노말

 


 

개요

이전엔 평면 매핑에 대한 튜토리얼을 작성했습니다.
평면 매핑의 가장 큰 단점은 한 방향에서만 동작하고, 화면에 그려지는 표면이 매핑하려는 방향을 향하지 않을 때 (이전 예에서는 위쪽) 제대로 동작하지 않는다는 점입니다. 이런 자동 UV 생성을 개선하는 방법은 매핑을 다른 세 방향에서 매핑을 수행하고, 이 세 가지 결과값을 블렌딩하는 것입니다.

이번 튜토리얼은 언릿 셰이더인 평면 매핑 셰이더 위에 작성되지만, 서피스 셰이더를 포함한 많은 셰이더들에도 이 테크닉을 활용할 수 있습니다.

 

 


 

평면 투영 계산

서로 다른 세 세트의 UV 좌표를 생성하기 위해, UV좌표를 얻는 방식부터 바꾸겠습니다. 버텍스 셰이더에서 변환된 UV 좌표를 반환하는 대신, 월드 좌표를 반환해 프래그먼트 셰이더에서 UV 좌표를 생성합니다.

 

struct v2f{
    float4 position : SV_POSITION;
    float3 worldPos : TEXCOORD0;
};

v2f vert(appdata v){
    v2f o;
    // 오브젝트를 렌더하기 위해 클립 공간의 위치좌표를 계산
    o.position = UnityObjectToClipPos(v.vertex);
    // 버텍스의 월드 포지션 좌표를 계산
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.worldPos = worldPos.xyz;
    return o;
}

 

평소와 같이, TRANSFORM_TEX() 함수를 사용해 텍스처의 타일링과 오프셋을 적용해줍니다. 셰이더 코드에서 worldPos.xy 와 zy 를 사용해 월드의 높이 축이 텍스처의 y축으로 매핑되도록 합니다. 두 텍스처를 서로 회전시키지는 않지만, 이 값들을 사용하는 방식은 다양하게 시도해볼 수 있습니다. (상단 UV를 매핑하는 방식은 여러가지입니다.)

 

fixed4 frag(v2f i) : SV_TARGET{
    // 세 방향 투영에 대한 UV 좌표 계산
    float2 uv_front = TRANSFORM_TEX(i.worldPos.xy, _MainTex);
    float2 uv_side = TRANSFORM_TEX(i.worldPos.zy, _MainTex);
    float2 uv_top = TRANSFORM_TEX(i.worldPos.xz, _MainTex);
    
    ···

 

올바른 좌표를 얻은 뒤, 해당 좌표의 텍스처를 읽고, 세 가지 결과 색상값들을 모두 더한 뒤 3으로 나눕니다. (세 가지 색상을 더한 값을 색상의 가짓수로 나누지 않으면 최종 색상은 너무 밝아지게 됩니다.)

 

// 세 방향 투영한 UV 좌표의 텍스처를 읽습니다.
fixed4 col_front = tex2D(_MainTex, uv_front);
fixed4 col_side = tex2D(_MainTex, uv_side);
fixed4 col_top = tex2D(_MainTex, uv_top);

// 투영된 색상들 합치기
fixed4 col = (col_front + col_side + col_top) / 3;

// 틴트 색상과 텍스처 색상 곱하기
col *= _Color;
return col;

 

 


 

노말

지금까지의 작업을 완료한 메테리얼은 굉장히 이상해 보이는데, 이는 세 가지 투영의 평균값을 표시했기 때문입니다. 이를 고치기 위해서는 표면이 바라보고 있는 방향에 따라 다른 투영값을 보여줘야 합니다. 표면이 바라보는 방향을 '노말' 이라고도 부르는데, 이 노말 값은 버텍스의 좌표값과 동일하게 오브젝트 파일에 저장됩니다.

따라서 이제 해야 할 일은 입력 구조체에서 노말 값을 가져와 버텍스 셰이더에서 월드 공간의 노말 값으로 변환해주는 것입니다. (투영이 월드 공간에서 이뤄지기 때문입니다. 만약 오브젝트 공간 투영을 사용한다면, 변환하지 않고 오브젝트 노말 값으로 사용하면 됩니다.)

오브젝트 공간에서 월드 공간으로의 노말 변환을 위해선, 역전치행렬을 곱해줘야 합니다. 정확히 이게 어떻게 연산되는지는 이해하는 데에 중요하지 않지만 (행렬 곱셈은 복잡하기 때문에), 위치값을 변환할 때처럼 오브젝트-월드 변환 행렬과 곱해버리면 안되는 이유에 대해 설명하고 싶습니다.

노말은 표면에 수직입니다. 그래서 X축으로만 크기를 조절했을 경우 표면은 가파르게 되는데, 같은 방식으로 노말을 변환하면 노말 값은 이전보다 더 위를 향하게 되어 더 이상 표면으로부터 수직이 아니게 됩니다. 대신, 표면이 가팔라지는 만큼 노말을 더 플랫하게 만들어줘야 하는데, 역전치행렬이 이를 수행해줍니다. 그런 다음 행렬을 3x3 행렬로 변환해주고, 노말을 움직이는 부분들을 제거해줍니다. (노말값이 위치값 대신 방향을 나타내기 때문에, 노말을 움직이길 원하지 않습니다.)

역전치 오브젝트-월드 변환 행렬을 사용하는 방법은, 노말을 월드-오브젝트 변환 행렬과 곱하는 것입니다. (이전에 월드 좌표를 계산할 때 행렬과 벡터 곱해주었는데, 행렬 곱셈에서는 서순이 중요합니다.)

 

 

struct appdata{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
};

struct v2f{
    float4 position : SV_POSITION;
    float3 worldPos : TEXCOORD0;
    float3 normal : NORMAL;
};

v2f vert(appdata v){
    v2f o;
    // 오브젝트를 렌더하기 위해 클립 공간의 위치좌표를 계산
    o.position = UnityObjectToClipPos(v.vertex);
    // 버텍스의 월드 포지션 좌표를 계산
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.worldPos = worldPos.xyz;
    // 월드 노말을 계산
    float3 worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
    o.normal = normalize(worldNormal);
    return o;
}

 

노말값을 확인하기 위해, 프래그먼트 셰이더에서 노말값을 반환해 축들을 색상으로 나타낼 수 있습니다.

 

fixed4 frag(v2f i) : SV_TARGET{
    return fixed4(i.normal.xyz, 1);
}

 

노말값을 서로 다른 투영에 대한 가중치 값으로 변환하기 위해선 먼저 노말의 절대값을 구해야 합니다. 이는 노말이 양수와 음수 방향으로 갈 수 있기 때문입니다. 이는 디버그 뷰에서 노말이 음수인 오브젝트 뒷면이 검게 보이는 이유이기도 합니다.

 

float3 weights = i.normal;
weights = abs(weights);

 

그 후, 다른 투영들을 가중치와 곱해서 텍스처가 늘어나는 다른 면이 아닌 투영할 면에만 나타나게 만들어줍니다. xy 평면에서의 투영을 z 가중치와 곱하면 해당 축을 따라 텍스처가 늘어나지 않게 됩니다. 다른 축들에도 동일한 작업을 해줍니다.

또한, 더 이상 모든 투영 값들을 더하지 않기 때문에 3으로 나누는 부분도 제거합니다.

 

 

// 월드 노말을 가중치로 사용
float3 weights = i.normal;
// 오브젝트의 앞뒷면 모두에 텍스처 표시 (양수와 음수 방향)
weights = abs(weights);

// 투영된 색상값과 가중치를 합치기
col_front *= weights.z;
col_side *= weights.x;
col_top *= weights.y;

// 투영된 색상들을 합치기
fixed4 col = col_front + col_side + col_top;

// 틴트 색상과 텍스처 색상 곱하기
col *= _Color;
return col;

 

 

훨씬 나아졌지만, 투영값들의 합을 3으로 나눴을 때와 동일한 문제가 생겼습니다. 노말값의 성분들이 합쳐져 3 이상이 되는 경우, 텍스처를 원래보다 더 밝게 보이게 만듭니다. 이 문제를 해결하기 위해 가중치를 노멀의 성분 값들의 합으로 나누어서 최대값이 1이 되도록 만들어줍니다.

 

// 모든 성분의 합이 1이 되도록 만들어줍니다.
weights = weights / (weights.x + weights.y + weights.z);

 

 

적용한 뒤엔 텍스처가 다시 원래의 밝기로 돌아오게 됩니다.

이 셰이더에 마지막으로 추가할 것은 다른 방향들이 더 뚜렷하게 보이도록 만들어줄 기능입니다. 현재는 서로 섞이는 영역이 여전히 커서 색상들이 지저분하게 보이기 때문입니다. 이를 위해 블렌딩의 선명도를 조절할 새로운 프로퍼티를 추가해줍니다. 그런 다음, 가중치가 하나로 합쳐지기 전에 가중치를 선명도 값으로 제곱해 계산합니다. 0에서 1 사이의 범위에서 값이 움직이기 때문에, 선명도가 높다면 낮은 값들은 낮아지지만, 높은 값들엔 크게 변하지 않습니다.
선명도 프로퍼티의 속성을 Range 로 설정해 셰이더 UI 에 멋진 슬라이더로 표시해줍니다.

 

···

_Sharpness("Blend Sharpness", Range(1, 64)) = 1

···

float _Sharpness;

···

// 트랜지션을 더 선명하게 만들어줍니다.
weights = pow(weights, _sharpness)

···

 

 

트라이플래너 매핑은 여전히 완벽하진 않습니다. 제대로 동작하기 위해선 타일링 텍스처가 필요하고, 정확히 45°의 표면에선 문제가 발생하며, 단일 텍스처 샘플보단 비용이 더 많이 듭니다. (많이 차이나지는 않습니다.)

이 기술을 서피스 셰이더에서 알베도나 스페큘러 등등의 맵들에 사용할 수 있지만, 노멀맵에선 여기선 다루지 않을 몇몇 변경점 없이는 완벽히 동작하지 않습니다.

 

Shader "Tutorial/010_Triplanar_Mapping"{
    // 인스펙터에서 조작할 수 있게 값 표시
    Properties{
        _Color ("Tint", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
        _Sharpness ("Blend sharpness", Range(1, 64)) = 1
    }

    SubShader{
        // 메테리얼은 불투명하고 다른 불투명 모델들과 같은 시점에 렌더됩니다.
        Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

        Pass{
            CGPROGRAM

            #include "UnityCG.cginc"

            #pragma vertex vert
            #pragma fragment frag

            // 텍스처와 텍스처의 트랜스폼
            sampler2D _MainTex;
            float4 _MainTex_ST;

            fixed4 _Color;
            float _Sharpness;

            struct appdata{
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f{
                float4 position : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 normal : NORMAL;
            };

            v2f vert(appdata v){
                v2f o;
                // 오브젝트를 렌더하기 위해 클립 공간의 위치좌표를 계산
                o.position = UnityObjectToClipPos(v.vertex);
                // 버텍스의 월드 포지션 좌표를 계산
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldPos = worldPos.xyz;
                // 월드 노말을 계산
                float3 worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                o.normal = normalize(worldNormal);
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET{
                // 세 방향 투영에 대한 UV 좌표 계산
                float2 uv_front = TRANSFORM_TEX(i.worldPos.xy, _MainTex);
                float2 uv_side = TRANSFORM_TEX(i.worldPos.zy, _MainTex);
                float2 uv_top = TRANSFORM_TEX(i.worldPos.xz, _MainTex);

                // 세 방향 투영한 UV 좌표의 텍스처를 읽습니다.
                fixed4 col_front = tex2D(_MainTex, uv_front);
                fixed4 col_side = tex2D(_MainTex, uv_side);
                fixed4 col_top = tex2D(_MainTex, uv_top);

                // 월드 노말을 가중치로 사용
                float3 weights = i.normal;
                // 오브젝트의 앞뒷면 모두에 텍스처 표시 (양수와 음수 방향)
                weights = abs(weights);
                // 트랜지션을 더 선명하게 만들어줍니다.
                weights = pow(weights, _Sharpness);
                // 모든 성분의 합이 1이 되도록 만들어줍니다.
                weights = weights / (weights.x + weights.y + weights.z);

                // 투영된 색상값과 가중치를 합치기
                col_front *= weights.z;
                col_side *= weights.x;
                col_top *= weights.y;

                // 투영된 색상들을 합치기
                fixed4 col = col_front + col_side + col_top;

                // 틴트 색상과 텍스처 색상 곱하기
                col *= _Color;

                return col;
            }

            ENDCG
        }
    }
    FallBack "Standard" // FallBack 으로 다른 오브젝트에 그림자가 지도록 그림자 패스를 추가합니다.
}

 

 

 

'Graphics > Ronja's Unity Shader tutorials' 카테고리의 다른 글

체크 무늬 만들기  (0) 2024.08.19
색상 보간  (0) 2024.06.15
평면 매핑 (Planar Mapping)  (0) 2024.05.05
스프라이트 셰이더  (0) 2024.03.03
기본 투명 셰이더  (0) 2024.01.21