void PostList()

안개처럼 사라지는 텍스쳐 이미지 만들기



작년 유니티 스터디를 하다가 연기처럼 사라지는 글자에 대한 질문을 받았습니다.
당시엔 저도 아무것도 모르던 때라 제대로 된 답변을 하지 못했습니다.

이와 관련해서 한번 실제로 작성해보고 제대로 답변드릴 필요가 있었습니다.
이번 셰이더에서는 UV Distortion 과 상위 MipMap 샘플링을 응용해보았습니다.

새벽 중학생 감성으로 작성한 문장 텍스쳐를 샘플로 사용해
셰이더를 작성보았습니다.

< 흐트리기 영역을 계산해 보기 >

흐트러지는 영역에 대한 정의는 여러가지가 있겠지만 이번 셰이더의 비주얼을 보면
시간에 따라 특정한 UV 좌표를 기준으로 점점 연기처럼 흐트러지는 범위지고, 또한
원점에 가까울 수록 흐트러지고, 흐려지는 강도가 높아지는 형태를 취합니다.

마치 잔잔한 물결에 돌을 던졌을때 일어나는 물 표면의 파동 느낌을 생각하시면 좋을 것 같습니다.

따라서 이 단계에서 필요한 것은 다음과 같습니다.
1. 흐트러지는 영역의 중점
2. 현재 프래그먼트 셰이더에서 그리고 있는 픽셀 UV 좌표와 중점과의 거리

1 -- 흐트러지는 영역의 중점

먼저 텍스쳐의 어디를 흐트려야하는지 알기 위해선 특정 점을 표현하는 좌표가 하나 필요합니다.
그러니 우리는 외부에서 좌표를 받아올 필요가 있죠

Properties
    {
        _StPt("Dissolve Start Point",Vector) = (0,0,0,0)
    }

유니티에선 프로퍼티를 통해 벡터 자료형을 가져오는 방법이 있습니다.
UV 좌표는 2차원 좌표계를 사용하니, 우리는 프로퍼티로 가져온 Vector 자료형의 x,y 요소만 사용하면 되겠습니다.

2 -- 현재 중점과의 거리

이제 흐트려버릴 영역의 중점을 알아냈으니 여기에 거리만 적용하면 우리가 원하는 부분을 마음대로 흐트릴 수 있습니다.

여기선

if (_원하는 거리 > distance(_흐트러지는 영역의 중점, 현재 프래그먼트의 UV))
                    흐트리기;

이런 느낌으로 현재 그리고 있는 픽셀의 UV 좌표와 중점과의 거리를 비교해서 안쪽은 흐트리고 바깥은 그대로 두는 느낌을 연출할 수 있겠습니다.

다만 이번 포스트에서는 거리에 비례해서 흐트러지는 강도가 점차적으로 달라지도록 할 것입니다.

그 전에 프래그먼트 셰이더에 코드를 작성하여 실제로 흐트리기 전에 어디를 얼마나 흐트리면 되는지 확인을 해봅시다

float2 DissolvePoint = _StPt.xy;
half4 col = tex2D(_MainTex, i.uv);
half DissolveFactor = distance(i.uv, _StPt.xy);
col.+= DissolveFactor;
return col;


DissolveFactor 실수를 정의하고 이 실수에 프래그먼트의 UV 좌표값과 흐트림 영역의 중점과의 거리를 대입한 후, DissolveFactor를 최종 반환 색상의 R 값에 대입하여 픽셀별로 흐트릴 강도가 어느정도인지 붉은 색을 통해 확인하고자 했습니다.

붉어질수록 강하게 흐트러지는것이고 검정색일수록 흐트러짐이 줄어드는것이죠.
문제는 우리가 원하는것은 중점에서 제일 강도가 높고 외각일수록 덜 흐트러지는 모습을 원하는데 결과가 반대입니다.

이를 해결하기 위해 값을 뒤집어줄 필요가 있습니다.
float2 DissolvePoint = _StPt.xy;
half4 col = tex2D(_MainTex, i.uv);
half DissolveFactor = distance(i.uv, _StPt.xy);
DissolveFactor = 1 - (DissolveFactor);
col.+= DissolveFactor;
return col;


값을 뒤집어 주었지만 문제가 발생했습니다.
어느정도 거리까지는 우리가 의도한 대로 점점 어두워지는 모습을 볼 수 있었지만,
어느 정도의 거리 이상 떨어진 부분부터 오히려 점점 밝아지는 모습을 확인할 수 있습니다.
이는 DissolveFactor 값이 1을 넘어가서 DissolveFactor 값이 음수로 넘어가버리기 때문입니다.

float2 DissolvePoint = _StPt.xy;
half4 col = tex2D(_MainTex, i.uv);
half DissolveFactor = distance(i.uv, _StPt.xy);
DissolveFactor = 1 - saturate(DissolveFactor);
col.+= DissolveFactor;
return col;

따라서 saturate() 함수를 사용하여 DissolveFactor값이 0부터 1사이의 값으로 Clamp 시켜줄 필요가 있습니다.


자 이제 제대로 되었습니다.
그런데 문제는 거리를 사용해서 영역의 표현을 하긴 했는데
영역의 위치는 조절할 수 있지만 그 영역의 크기가 고정되어 있습니다.

_DissolveRange ("Dissolve Range", Range(0,1)) = 0.0
프로퍼티로 실수를 하나 받아오고

half DissolveFactor = distance(i.uv, _StPt.xy) + (_DissolveRange);
DissolveFactor 값 코드를 다음과 같이 수정합니다.


이렇게 프로퍼티로 범위값을 가져와 영역의 크기를 제어할 수 있게 되었습니다.
그러나 지금 프로퍼티의 Range 값과 실제 영역의 크기가 반비례하는걸 볼 수 있습니다.

half DissolveFactor = distance(i.uv, _StPt.xy) + (1-_DissolveRange);

이를 직관적으로 수정할 필요가 있습니다. 따라서 DissolveRange 또한 뒤집어줍니다.



이제 우리는 영역의 크기도 제어할 수 있게 되었습니다.
문제는 선형적이라 경계가 잘 보이지 않고, 흐트러지는 강도가 한번에 전체적으로 변해버리는 느낌을 확인할 수 있죠


따라서 흐트러지는 강도의 영역 대한 경계선을 좀더 극단적인 형태를 취하도록 조정할 필요가 있습니다. 따라서 지수함수의 형태로 만들기 위해 pow() 함수를 사용합니다.

_DissolveExp("Dissolve Exponent", Range(0,5)) = 0.0

float2 DissolvePoint = _StPt.xy;
half4 col = tex2D(_MainTex, i.uv);
half DissolveFactor = distance(i.uv, _StPt.xy) + (1-_DissolveRange);
DissolveFactor = pow(DissolveFactor, _DissolveExp);
DissolveFactor = 1 - saturate(DissolveFactor);

지수함수의 승으로 사용하기 위한 실수 하나를 프로퍼티를 받아오고 위와 같이 코드를 수정합니다.


코드를 수정하고 바로 테스트 해보면 마치 일식마냥 영역 중앙에 검은 원이 생성되는걸 확인할 수 있습니다.

이를 해결하기 위해 위에서 작성한 saturate의 위치를 옳겨줄 필요가 있습니다.

float2 DissolvePoint = _StPt.xy;
half4 col = tex2D(_MainTex, i.uv);
half DissolveFactor = saturate ( distance(i.uv, _StPt.xy) + (1-_DissolveRange) );
DissolveFactor = pow(DissolveFactor, _DissolveExp);
DissolveFactor = 1 - (DissolveFactor);
col.+= DissolveFactor;
return col;


자 이제 우리는 범위도 조절할 수 있고 범위의 경계 또한 조절할 수 있게 되었습니다.
이제 어디를 어느정도 뒤틀면 되는지 알았으니 본격적으로 UV 좌표를 뒤틀 준비가 되었습니다.

<UV Distortion>

우리가 텍스쳐 이미지로부터 색상을 읽어오기 위해서 샘플러와 UV 좌표를 필요로 하는데
이 UV 좌표는 보통 모델 데이터를 제작하는 과정에서 Unwarp 등의 방법으로 각각 정점에 대한 UV 좌표값을 저장합니다.

우리는 이 모델링 데이터의 각 정점에 저장된 UV 좌표를 그대로 가져와 텍스쳐 매핑을 위한 좌표값으로 사용합니다.

이때 이 UV 좌표값을 그대로 사용하지 않고 증감시킨다면, 다른 위치에 있는 텍셀의 색상을 읽어올 수 있다는것을 의미합니다.

예를들어 버텍스 셰이더에서 넘어온 UV 값(0.5, 0.5)에 0.25씩 더한다면 (0.75, 0.75)에 위치한 텍셀의 색상을 읽어오게 되겠죠.


이번 포스트에선 다음과 같은 노이즈 텍스쳐의 색상을 기존 UV 좌표로 읽어낸 후
기존 UV 좌표에 노이즈 텍스쳐로부터 읽어온 색상값을 감산하여 뒤틀린 느낌을 주고자 합니다.

노이즈 텍스쳐를 어떤 것을 사용하느냐에 따라 느낌이 다르니, 여러가지를 사용해보시고 제일 적절한 것을 사용하시면 되겠습니다.

_NoiseTex("Noise Texture", 2D) = "grey" {}

float2 DissolvePoint = _StPt.xy;

half NoiseValue = tex2D(_NoiseTex, i.uv);

half4 col = tex2D(_MainTex, i.uv);
half DissolveFactor = saturate ( distance(i.uv, _StPt.xy) + (1-_DissolveRange) );
DissolveFactor = pow(DissolveFactor, _DissolveExp);
DissolveFactor = 1 - (DissolveFactor);
col.+= DissolveFactor* NoiseValue;
return col;

우선 노이즈 텍스쳐가 어떻게 반영되는지 확인해보기 위해 코드를 다음과 같이 수정하였습니다.


이제 붉은색 영역이 단순한 원이 아니라 구름 모양으로 나타나기 시작했습니다.
이 붉은색을 기존 UV 좌표와 감산, 또는 가산하면 기존 좌표와 다른 곳에 있는 색상을 읽게 될 것입니다.

이때 NoiseValue에 DissolveFactor 를 곱해주어 영역에 밝기에 비례해서 뒤틀리는 정도가 반영되도록 해줍니다.

float2 DissolvePoint = _StPt.xy;
half NoiseValue = tex2D(_NoiseTex, i.uv);
half DissolveFactor = saturate ( distance(i.uv, _StPt.xy) + (1-_DissolveRange) );
DissolveFactor = pow(DissolveFactor, _DissolveExp);
DissolveFactor = 1 - (DissolveFactor);
half4 col = tex2D(_MainTex, i.uv - NoiseValue * DissolveFactor);

col.+= DissolveFactor* NoiseValue;
return col;


실제로 UV 좌표값에 노이즈 색상값을 감산한 후 테스트해보면 다음과 같이 글자의 형태가 일그러지는것을 확인할 수 있습니다.

_DistortionStrength("Distortion Strength", Range(-1,1)) = 0.0
i.uv - NoiseValue * DissolveFactor * _DistortionStrength

프로퍼티로 실수를 하나 더 받아 Distortion Strength 를 만든 후 추가로 곱하면 어느정도 일그러지는지 전체적인 강도를 설정할 수 있을 것입니다.

우리는 글자가 연기처럼 사라지는 것을 원하기에 뒤틀리면 뒤틀릴수록 흐리게 보이도록 처리할 필요성이 있습니다.

이미지 프로세싱에서 블러링을 하기 위해서 가우시안 필터 등을 씌우는 방법이 있겠지만 거리에 비례해서 흐리기의 강도를 전부 다르게 하고, 또 매 프레임마다 세세한 블러링을 처리하기엔 너무 무겁다는 문제가 있습니다.

다행히도 컴퓨터 그래픽스 상에서 렌더링 속도를 향상시키기 위해 낮은 해상도의 이미지들을 미리 생성해두는 밉맵 (MipMap) 이라는 것이 존재합니다.

<밉맵 예시>

미리 만들어진 이미지인 밉맵을 사용한다면 우리가 매 프레임마다 직접 힘들게 블러링할 것이 아니라 거리에 비례해서 다른 밉맵의 텍셀 색상을 샘플링하기만 해도 점차 흐려지는 이미지를 만들 수 있습니다.

CG 코드에서 원하는 밉맵의 텍셀 색상을 샘플링하는 함수의 원형은 다음과 같습니다.
tex2Dlod(샘플러2D, float4(U좌표 ,V좌표 ,0 ,원하는 밉맵 레벨));

tex2Dlod 의 2번째 인자에 float4() 자료형이 들어가는데,
이때 float4 의 xy 인자에 기존의 tex2D와 마찬가지로 UV 좌표가 들어가고
마지막 w 인자에 원하는 밉맵 레벨을 정수로 입력하면 됩니다.

만약 (0.5,0.5) 위치의 1번째 밉맵 레벨의 텍셀을 읽고자 한다면
float4(0.5 ,0.5 ,0 ,1)
를 tex2Dlod 의 두번째 인자로 입력해주면 됩니다.

여기서는 거리에 비례해서 흐려지는 정도를 다르게 하고자 하니 DissolveFactor를 밉맵 레벨로 주면 됩니다. 추가로 프로퍼티상에서 흐려짐의 강도 또한 조절하기 위해
_BlurStrength("Blur Strength", Range(0,30)) = 0.0
를 추가로 넣어 인자로 넣은 DissolveFactor에 곱해주도록 합니다.

float4 NewUV = float4(
    float2(i.uv - NoiseValue * DissolveFactor * _DistortionStrength),
    0, 
    DissolveFactor*_BlurStrength
    );
half4 col = tex2Dlod(_MainTex, NewUV)*(1-DissolveFactor);

이제 본격적인 비주얼을 보기위해 r채널에 색상을 대입했던 코드를 지우고 코드를 위와 같이 정리하면 다음과 같은 화면이 됩니다.


분명 거리에 따라 흐려지는 정도가 달라지긴 하는데 계단현상이 발생합니다.


이는 밉맵과 밉맵 사이 앨리어싱 현상이 발생하기 때문입니다.
이를 해결하기 위해 하나의 밉맵 안에서 겹선형 보간을 하는 것 뿐만 아니라 밉맵과 밉맵 사이에서도 보간이 일어나야 합니다.

따라서 앨리어싱 현상을 제거하기 위해서 Filter Mode 를 삼선형 보간 ( Trilinear ) 으로 변경할 필요가 있습니다.


이상으로.

지금까지 연기처럼 사라지는 텍스쳐 이미지를 만들기였습니다.
여기서 Distortion Strength 가 UV 둘 다에 같은 양만큼 영향을 주고 있기 때문에 프로퍼티를 둘로 나누어 각각 적용한다면 수직, 수평 뒤틀림 양을 따로 조절할 수 있을 것이고,
또한 블랜딩을 응용하여 새로운 효과를 만들 수도 있을 것입니다.

스크립트를 통해 재질의 프로퍼티에 접근하여 게임의 컷신 또는 특수효과에 사용할 수 있으리란 기대가 됩니다.

이와 관련된 의견이 있으시다면 하단에 코맨트를 남겨주시면 감사하겠습니다!

전체코드
◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆


Shader "Unlit/SmokeDissolveShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _NoiseTex("Noise Texture", 2D) = "grey" {}
        _StPt("Dissolve Start Point",Vector) = (0,0,0,0)
        _DissolveRange ("Dissolve Range", Range(0,5)) = 0.0
        _DissolveExp("Dissolve Exponent", Range(0,5)) = 0.0
        _DistortionStrength("Distortion Strength", Range(-1,1)) = 0.0
        _BlurStrength("Blur Strength", Range(0,30)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog


            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float2 Smokeuv : TEXCOORD1;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _NoiseTex;
            float4 _MainTex_ST;
            float4 _NoiseTex_ST;
            half4 _StPt;
            half _DissolveRange;
            half _DissolveExp;
            half _DistortionStrength;
            half _BlurStrength;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.Smokeuv = TRANSFORM_TEX(v.uv, _NoiseTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float2 DissolvePoint = _StPt.xy;

                half NoiseValue = tex2D(_NoiseTex, i.uv);
                
                half DissolveFactor = saturate ( distance(i.uv, _StPt.xy) + (1-_DissolveRange) );
                DissolveFactor = pow(DissolveFactor, _DissolveExp);
                DissolveFactor = 1 - (DissolveFactor);
                
                float4 NewUV = float4(float2(i.uv - NoiseValue * DissolveFactor * _DistortionStrength), 0, DissolveFactor*_BlurStrength);
                half4 col = tex2Dlod(_MainTex, NewUV)*(1-DissolveFactor);
                
                return col;
            }
            ENDCG
        }
    }
}


p.s.










글자가 그려진 이미지 뿐만 아니라 다른 이미지에도 적용해볼 수 있습니다.

댓글

이 블로그의 인기 게시물

Array Modifier 로 오브젝트를 원형으로 나열하기

회전 루프 애니메이션 만들기 ( Graph Editor F-Curve Modifiers )

원형으로 뚫려있는 구멍을 토폴로지 흐름에 맞게 채우기.