Microsoft MVP성태의 닷넷 이야기
Graphics: 21. shader - _Time 내장 변수를 이용한 UV 변동 효과 [링크 복사], [링크+제목 복사]
조회: 14798
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

shader - _Time 내장 변수를 이용한 UV 변동 효과

다음의 책을 보면,

유니티 쉐이더 스타트업
; http://www.yes24.com/24/goods/58495827

Part 6에서 불 이펙트를 만들기 위해 다음과 같은 shader 코드를 작성합니다.

Shader "Custom/fireShader" {

    Properties {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _MainTex2("Albedo (RGB)", 2D) = "white" {}
    }

    SubShader {
        Tags { "RenderType"="Transparent" "Queue" = "Transparent" }

        CGPROGRAM
        #pragma surface surf Lambert alpha:fade noambient

        sampler2D _MainTex;
        sampler2D _MainTex2;

        struct Input {
            float2 uv_MainTex;
            float2 uv_MainTex2;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 d = tex2D(_MainTex2, float2(IN.uv_MainTex2.x, IN.uv_MainTex2.y - _Time.y));
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex + d.r);

            o.Emission = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

직관적으로는 저 코드가 사실 어떤 것인지 잘 이해가 안 되었는데요, 한번 차근히 분석해봤습니다. ^^ 우선, 중요한 것은 다음의 값이 의미하는 게 뭐냐는 것입니다.

IN.uv_MainTex2.y - _Time.y

(Unity Editor에서 변경하지 않은 경우) Plane 객체에서 UV 좌표계는 0 ~ 1 범위의 값을 갖기 때문에 위의 코드에서 IN.uv_MainTex2.y의 값은 0 ~ 1 사이입니다. 그런데, Unity에서는 UV 매핑이 다음과 같기 때문에,

[그림: Unity - UV 좌표계 - 출처 http://lovelyseekerclaire.tistory.com/27]
unity_uv_1.png

Plane 객체의 상단에서 y는 1로 시작해 하단으로 오면서 0이 됩니다.

그다음, _Time.y의 값은 Scene이 시작된 이후 흐른 시간을 초 단위로 소수점 이하의 값을 유지하면서 반환합니다. 즉, 1.531초, 1.612초와 같은 식입니다. 따라서 "IN.uv_MainTex2.y - _Time.y" 코드의 의미는 1초마다 IN.uv_MainTex2의 texture를 위에서 아래로 훑게 됩니다. 만약 이상적으로 1 fps를 유지하며 화면이 업데이트 된다면 IN.uv_MainTex2.y의 값은 언제나 같은 UV 좌표 값을 반환하는 것이나 같습니다. 가령, IN.uv_MainTex2.y의 값이 0.4인 위치에 있었다면 각각의 시간마다 다음과 같은 값을 반환하게 됩니다.

IN.uv_MainTex2.y = 0.4의 위치에서,

0.1초 후: 0.3
0.3초 후: 0.1
0.5초 후: -0.1
0.8초 후: -0.4
1.0초 후: -0.6
1.5초 후: -1.1
2.0초 후: -1.6
3.0초 후: -2.6
4.0초 후: -3.6
....

테스트를 위해 MainTex2의 이미지를 다음과 같은 코드로 생성해 보겠습니다.

using System.Collections.Generic;
using System.Drawing;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            int w = 32;
            int h = 32;
            Bitmap img = new Bitmap(w, h);

            int r = 0;
            Sequence seq = new Sequence(0, 32);
            var rSeq = seq.Get();

            for (int y = 0; y < h; y++)
            {
                r = rSeq.Current;
                rSeq.MoveNext();
                for (int x = 0; x < w; x++)
                {
                    Color rInc = Color.FromArgb(r, 0, 0);
                    img.SetPixel(x, y, rInc);
                }
            }

            img.Save("test_" + w + "_" + h + ".png");
        }
    }

    class Sequence
    {
        int _start;
        int _end;
        int _current;
        bool _increment;

        public Sequence(int start, int end)
        {
            _start = start;
            _end = end;

            _current = _start;
            _increment = true;
        }

        public IEnumerator<int> Get()
        {
            while (true)
            {
                yield return _current;

                if (_increment == true)
                {
                    _current++;
                }
                else
                {
                    _current--;
                }

                if (_current == _end)
                {
                    _increment = false;
                }
                else if (_current == _start)
                {
                    _increment = true;
                }
            }
        }
    }
}

위의 코드로 생성하면, y 좌표의 증가에 따라 R 값이 0, 1, 2, 3, ..., 30, 31까지 올라가는 이미지가 생성됩니다. 따라서 다음의 코드는,

fixed4 d = tex2D(_MainTex2, float2(IN.uv_MainTex2.x, IN.uv_MainTex2.y - _Time.y));

UV 좌표로 (0, 0) 위치에 있던 texture의 경우라면 1초 동안 R 성분이 0 ~ 31까지 변하는 "fixed4 d" 값을 반환하게 됩니다. 물론, 0 ~ 31은 바이트 범위의 R 컬러 값을 가정한 것이고, shader에서는 1바이트 값이 0 ~ 1로 정규화되기 때문에 만약 R == 5라면 "5:255 = x:1"의 비례식에 x = 5 / 255 = 0.01960... 값이 나옵니다. 일단 여기까지 이해했으면 다음의 코드는 쉽게 이해가 됩니다.

fixed4 c = tex2D(_MainTex, IN.uv_MainTex + d.r); // R 값이 5인 경우, d.r = 0.01960
                                                 //      255인 경우, d.r = 1.0
                                                 //        0인 경우, d.r = 0.0

위의 코드를 좀 더 쉽게 이해하려면 다음과 같이 y 좌표에 대해서만 영향을 주는 걸로 바꿔볼 수 있습니다.

fixed4 c = tex2D(_MainTex, float2(IN.uv_MainTex.x, IN.uv_MainTex.y + d.r));

이렇게 하면 texture 이미지에 담긴 R 값의 범위에 따라 _MainTex 이미지 크기에 비례해 다른 y 위치의 점에 해당하는 컬러를 c에 반환하게 됩니다. 예를 들어 예제로 생성한 0 ~ 31까지의 범위를 갖는 MainTex2 이미지인 경우, Height = 1024 pixel인 MainTex 이미지에 대해 저 shader 코드를 실행한다면, 원래의 점을 반환하는 좌표로부터 다음의 비례식에 따라,

0:255 = 0:1023
31:255 = y:1023

y = 124.3647058823529

최대 124 pixel까지의 이웃하는 점을 반환하는 것이 가능해집니다.




일단, 예제에 따라 0 ~ 31의 순차 값으로 채워진 32x32 이미지를 MainTex2로 지정하면 어떻게 될까요? 31까지 이동한 다음 곧바로 다시 0부터 시작하기 때문에 다음과 같이 순간적으로 제자리로 돌아가는 움직임이 발생하게 됩니다. (y 값만 변경하도록 바꾼 것입니다.)



따라서 부드럽게 진동을 하고 싶다면 0 ~ 16까지 증가한 다음 다시 16 ~ 0으로 내려오도록 R 값을 구성하면 됩니다.

static void Main(string[] args)
{
    int w = 32;
    int h = 32;
    Bitmap img = new Bitmap(w, h);

    int r = 0;
    Sequence seq = new Sequence(0, 16);
    ...[생략]...
    img.Save("test_" + w + "_" + h + ".png");
}

위와 같이 구성한 png 파일을 다시 _MainTex2 texture로 지정하고 실행하면 이번엔 다음과 같이 부드러운 진동을 볼 수 있습니다.





(첨부 파일은 이 글의 C# 프로젝트를 포함합니다.)




자, 이제 그럼 대충 _Time과 함께 어떤 식으로 _MainTex에 반영되는지 알았다면 "유니티 쉐이더 스타트업" 책에서 낸 실습 과제를 풀어낼 수 있을 것입니다.

첫 번째 문제는 "구겨지는 정도"인데, 그것은 _MainTex2에 지정한 texture에 담긴 R 값의 범위가 255까지 사용하면 구겨짐이 심하고 0에 가깝게만 구성하면 구겨짐이 약해질 것입니다. 물론 texture 이미지를 그렇게 바꾸는 것도 답일 수 있겠지만 shader 측에서 다음과 같이 r 값을 조절해 주는 것도 가능합니다.

void surf (Input IN, inout SurfaceOutput o) {
    fixed4 d = tex2D(_MainTex2, float2(IN.uv_MainTex2.x, IN.uv_MainTex2.y - _Time.y));
    fixed4 c = tex2D(_MainTex, IN.uv_MainTex + d.r / 2);

    o.Emission = c.rgb;
    o.Alpha = c.a;
}

위에서는 2로 나눴지만 저 값을 Properties에 지정해서 아티스트가 지정하도록 바꾸면 됩니다. 나누는 값이 커질수록 변위가 작아지기 때문에 구겨짐이 약해집니다.

두 번째 문제는 전체적으로 약간 왼쪽 아래로 내려간 이미지를 다시 오른쪽 위로 올릴 수 있느냐는 것입니다. 위치 0에 있던 점이 0 ~ 31까지의 + 위치에 대해서만 진동을 하기 때문에 Unity의 UV 좌표계에 따라 왼쪽 아래로 진동을 하게 되는 것입니다. 게다가 아래로 이동하는 문제로 인해 상단의 일정 영역이 타일링되어 불꽃이 다음과 같이 뜨는 현상이 발생합니다.

unity_uv_4.png

이 글에서 살펴본 바에 따르면 R 값이 반환하는 범위가 0 ~ 31이라면 15를 빼서 -15 ~ +15 사이로 진동하도록 바꾸는 것이 가능합니다. 그럼 어느 쪽에도 기울지 않고 원래의 texture 이미지를 기준으로 좌/우로 진동할 것입니다. 그런데 shader에서 그 값을 구하는 코드를 넣을 수는 없습니다.

일반화를 제외하고, 수동으로 조절하는 것이라면 약간의 offset 값을 주는 것으로 가능합니다. 책을 쓴 저자가 어떤 답을 기대하고 있었는지는 알 수 없지만, 다음과 같이 일정 수의 값을 임의로 Properties를 통해 지정할 수 있다면 불을 중앙으로 다시 옮길 수 있습니다. (예제의 경우 0.1을 빼면 됩니다.)

void surf (Input IN, inout SurfaceOutput o) {
    fixed4 d = tex2D(_MainTex2, float2(IN.uv_MainTex2.x, IN.uv_MainTex2.y - _Time.y));
    fixed4 c = tex2D(_MainTex, IN.uv_MainTex + (d.r - 0.1) / 2);

    o.Emission = c.rgb;
    o.Alpha = c.a;
}




그런데, noise2.png 파일로 불 효과를 구현하는 해당 예제에는 또 다른 문제가 있습니다.

unity_uv_5.png

보는 바와 같이 끊김 선이 보인다는 것입니다. 저 선이 있는 이유는 예제에서 사용하고 있는 noise2.png 파일의 가장 하단과 가장 상단의 값이 부드럽게 이어지지 않기 때문에 발생하는 것입니다. 일례로, 제가 만든 C# 프로그램에서 생성한 0, 1, 2, ..., 16까지 증가하고 다시 감소하는 32x32 이미지를 예제로 사용하면 저런 현상이 없습니다.

따라서 이런 줄무늬가 생성되는 현상을 방지하려면 noise2.png를 만드는 디자이너에게 하단의 값을 상단의 값으로 부드럽게 이어지는 노이즈를 생성해 달라고 해야 합니다. 아니면 직접 ^^ GIMP 같은 도구에서 "Filters" / "Map" / "Tile Seamless..." 메뉴를 실행하면 그런 작업을 자동으로 해줍니다.)

해당 이미지를 사용하면 위에서 보였던 줄 무늬가 더 이상 보이지 않게 됩니다.




애셋이 로컬에 설치된 경우 다음의 경로에서 찾을 수 있습니다.

%APPDATA%\Unity\Asset Store-5.x

간혹, 애셋을 Unity에서 import하려는 경우 다음과 같은 오류가 발생하면서 안 될 때가 있습니다.

Nothing to import!
All assets from this package are already in your project.

이런 경우 Unity를 다시 시작하면 정상적으로 애셋을 가져올 수 있습니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 9/3/2018]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13421정성태10/4/20233237닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
13420정성태9/26/20235261스크립트: 57. 파이썬 - UnboundLocalError: cannot access local variable '...' where it is not associated with a value
13419정성태9/25/20233088스크립트: 56. 파이썬 - RuntimeError: dictionary changed size during iteration
13418정성태9/25/20233749닷넷: 2146. C# - ConcurrentDictionary 자료 구조의 동기화 방식
13417정성태9/19/20233332닷넷: 2145. C# - 제네릭의 형식 매개변수에 속한 (매개변수를 가진) 생성자를 호출하는 방법
13416정성태9/19/20233150오류 유형: 877. redis-py - MISCONF Redis is configured to save RDB snapshots, ...
13415정성태9/18/20233637닷넷: 2144. C# 12 - 컬렉션 식(Collection Expressions)
13414정성태9/16/20233387디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법
13413정성태9/14/20233578닷넷: 2143. C# - 시스템 Time Zone 변경 시 이벤트 알림을 받는 방법
13412정성태9/14/20236846닷넷: 2142. C# 12 - 인라인 배열(Inline Arrays) [1]
13411정성태9/12/20233360Windows: 252. 권한 상승 전/후 따로 관리되는 공유 네트워크 드라이브 정보
13410정성태9/11/20234858닷넷: 2141. C# 12 - Interceptor (컴파일 시에 메서드 호출 재작성) [1]
13409정성태9/8/20233704닷넷: 2140. C# - Win32 API를 이용한 모니터 전원 끄기
13408정성태9/5/20233714Windows: 251. 임의로 만든 EXE 파일을 포함한 ZIP 파일의 압축을 해제할 때 Windows Defender에 의해 삭제되는 경우
13407정성태9/4/20233471닷넷: 2139. C# - ParallelEnumerable을 이용한 IEnumerable에 대한 병렬 처리
13406정성태9/4/20233408VS.NET IDE: 186. Visual Studio Community 버전의 라이선스
13405정성태9/3/20233835닷넷: 2138. C# - async 메서드 호출 원칙
13404정성태8/29/20233356오류 유형: 876. Windows - 키보드의 등호(=, Equals sign) 키가 눌리지 않는 경우
13403정성태8/21/20233186오류 유형: 875. The following signatures couldn't be verified because the public key is not available: NO_PUBKEY EB3E94ADBE1229CF
13402정성태8/20/20233243닷넷: 2137. ILSpy의 nuget 라이브러리 버전 - ICSharpCode.Decompiler
13401정성태8/19/20233505닷넷: 2136. .NET 5+ 환경에서 P/Invoke의 성능을 높이기 위한 SuppressGCTransition 특성 [1]
13400정성태8/10/20233341오류 유형: 874. 파이썬 - pymssql을 윈도우 환경에서 설치 불가
13399정성태8/9/20233369닷넷: 2135. C# - 지역 변수로 이해하는 메서드 매개변수의 값/참조 전달
13398정성태8/3/20234122스크립트: 55. 파이썬 - pyodbc를 이용한 SQL Server 연결 사용법
13397정성태7/23/20233634닷넷: 2134. C# - 문자열 연결 시 string.Create를 이용한 GC 할당 최소화
13396정성태7/22/20233332스크립트: 54. 파이썬 pystack 소개 - 메모리 덤프로부터 콜 스택 열거
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...