강의 필기/3-1

<11> 엔진 활용

YujinK 2020. 6. 23. 19:34

Diffuse Wrap


Diffuse Wrap이라는 기법을 이용해봅니다. 이 기법을 이용하면 텍스쳐를 통해 라이팅을 제어할 수 있게 되는데요, 차근히 구현해 보겠습니다.

(*Diffuse Wrap은 밸브에서 개발한 기술로, 팀 포트리스2에서 이용되었습니다.)


1)  커스텀 라이트 구현 + 하프램버트 적용 + 텍스쳐를 받을 변수, RampTex 생성


여기서 RampTex의 uv는 따로 받아올 필요 없습니다.

하프 램버트를 적용하여 라이트 값을 0~1까지로 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _RampTex ("Ramp", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
 
        CGPROGRAM
        #pragma surface surf cus fullforwardshadows noambient
        #pragma target 3.0
 
        sampler2D _MainTex;
        sampler2D _RampTex;
 
        struct Input
        {
            float2 uv_MainTex;
        };
 
        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
        }
        
        float4 Lightingcus(SurfaceOutput s, float3 lightDir, float atten)
        {
            float ndotl = dot(s.Normal, lightDir) * 0.5+0.5;
 
            return ndotl;
        }
 
        ENDCG
    }
cs

 



2) 커스텀 라이트에서 RampTex 연산


커스텀 라이트에서 텍스쳐 연산을 진행합니다.


float4 ramp = tex2D(_RampTex,          );


위에서 RampTex의 uv를 따로 받아오지 않았는데요, uv가 들어갈 자리에는 무얼 넣으면 좋을까요?

UV는 float2의 값을 받습니다. 즉, UV의 자리엔 float2의 숫자가 들어가도 괜찮습니다.


그럼 저는 uv의 자리에 float2(0,0)값을 넣은 후 ramp 변수를 리턴해보겠습니다.


1
2
3
4
5
6
7
8
  float4 Lightingcus(SurfaceOutput s, float3 lightDir, float atten)
        {
            float ndotl = dot(s.Normal, lightDir)*0.5+0.5;
            
            float4 ramp = tex2D(_RampTex,float2(0,0));
 
            return ramp;
        }
cs


또 다음의 텍스쳐를 _RampTex에 넣어봅니다.





붉은색을 출력합니다!


UV 좌표상 (0,0)의 위치에 있는 컬러인 붉은색 값을 리턴한 것인데요,


텍스쳐를 좌표로, 숫자로 생각해봅시다.



float4 ramp = tex2D(_RampTex,float2(0,0));


위 코드에서 uv자리에 어떤 좌표를 넣느냐에 따라, 출력할 컬러 값을 조절할 수 있습니다!




2) uv의 자리에 ndotl 연산


현재 ndotl은 하프 램버트를 적용하여 값이 0에서 1까지로 제한된 상태입니다. 

결국 ndotl도 하나의 숫자일 뿐이니, float2를 이용하여 uv의 자리에 넣을 수 있겠죠.



_RampTex에 위의 텍스쳐를 넣어주고, ramp는 다음과 같이 연산해 보겠습니다.


1
float4 ramp = tex2D(_RampTex,float2(ndotl,0.5));
cs


여기서 y의 자리엔 어떤 값을 넣어도 상관 없습니다. x축 만으로 제어할 수 있도록 짜여진 텍스쳐를 사용했기 때문입니다.




이처럼 uv좌표를 ndotl 연산에서 받아옵니다. ramp 텍스쳐만 바꿔주면 원하는 형태의 라이팅을 구현할 수 있습니다.



활용하면 이처럼 텍스쳐로 보라색 역광을 표현하거나, SSS를 구현하는 것이 가능합니다!



3) 추가: viewDir로 외곽선 구현


1
2
3
4
5
6
7
8
9
10
   float4 Lightingcus(SurfaceOutput s, float3 lightDir,float3 viewDir, float atten)
        {
            float ndotl = dot(s.Normal, lightDir)*0.5+0.5;
            float ndotv = saturate(dot(s.Normal, viewDir));
 
            float4 ramp = tex2D(_RampTex,float2(ndotv,0.5));
            
 
            return ramp;
        }
cs


 우선 위와 같이 코드를 입력하고 확인해봅시다. 

*viewDir는 카메라로 보이는 각도만을 표현합니다. 하프 램버트를 사용할 것 없이 saturate로 값을 제한합시다.



카메라의 방향에 따라, 텍스쳐에 맞춘 음영을 표현합니다.


이제 다음과 같이 코드를 바꾸어봅시다.


1
 float4 ramp = tex2D(_RampTex,float2(ndotl,ndotv));
cs


uv좌표의 x의 자리에 ndotl을, y자리에 ndotv를 넣어주었습니다.


텍스쳐를 수정해봅니다.



아래쪽에 가로지르는 검정색 면을 그려주었습니다. 


위 텍스쳐로, uv 좌표의 x축은 ndotl, y축은 ndotv를 넣어 연산합니다. 결과물을 살펴봅시다.

이전엔 y축에 어떤 값을 주어도 결과물과 상관 없었지만, 텍스쳐 소스를 수정한 지금은 다를 것입니다.



외곽선이 성공적으로 표현되었습니다. 


Reflection


이번엔 셰이더를 통해 '반사'를 구현해 볼 것입니다. 

'반사'는 빛에 따라 어두워지지 않으므로(그림자가 지지 않습니다) Emission으로 표현해주어야 합니다.

 

반사를 구현하기 위해선 일반적인 텍스쳐가 아닌, 큐브맵이 필요한데요, 오브젝트에 큐브맵을 적용해 봅시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Shader "Custom/Reflection"
{
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Cube("큐브맵",Cube) =""{}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
 
        CGPROGRAM
        #pragma surface surf Standard
        #pragma target 3.0
 
        sampler2D _MainTex;
        samplerCUBE _Cube;
 
        struct Input
        {
            float2 uv_MainTex;
            float3 worldRefl;
        };
 
 
        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            float4 r = texCUBE(_Cube, IN.worldRefl);
 
            o.Albedo = 0;
            o.Emission = r.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}
 
cs


위와 같이 코드를 입력해주었습니다. (큐브맵을 작성하는 양식은 외워줍시다)


큐브맵의 uv는 z방향까지 포함하여 float3입니다. Input에서 입력할 uv는 'worldRefl'이라는 내장값을 이용합니다.



오브젝트에 큐브맵을 넣어주고, 배경에도 같은 것을 적용합니다. 그럼 주변을 반사하는 것 처럼 표현할 수 있습니다.


리플렉션을 본격적으로 구현하기 위해선 Normal이 필요합니다. 다만 지금 상태에서 지금까지 해온 것과 같이 Normal을 구현하면...오류가 납니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Cube("큐브맵",Cube) =""{}
        _BumpMap ("노말맵",2D) = "bump"{}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
 
        CGPROGRAM
        #pragma surface surf Lambert
        #pragma target 3.0
 
        sampler2D _MainTex;
        sampler2D _BumpMap;
        samplerCUBE _Cube;
 
        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpMap;
            float3 worldRefl;
        };
 
 
        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            fixed4 n = tex2D (_BumpMap, IN.uv_BumpMap);
            float4 r = texCUBE(_Cube, IN.worldRefl);
 
            o.Albedo = 0;
            o.Normal = UnpackNormal(n);
            o.Emission = r.rgb;
            o.Alpha = c.a;
        }
 
        ENDCG
    }
    FallBack "Diffuse"
cs




어째서인가 알아보려면, 좌표계에 대한 이해가 필요한데요.


'탄젠트' 좌표계라는 것이 습니다. 이는 버텍스 하나하나가 가진 'Local' 좌표값 정도로 이해하고 있습니다.

Normal의 경우, 이 '탄젠트' 좌표값을 가지고 있고요. 

(사실...완벽히 이해하지 못했습니다ㅠㅠ)


중요한 것은, 리플렉션/라이트의 좌표계 (월드)와 노말이 가진 좌표계(탄젠트)가 다르다는 것, 같은 좌표계를 가져야만 함께 연산할 수 있다는 것입니다.


(*지금까지 light와 normal을 연산할 때 => dot(lightDir,s.Normal) 의 경우에는 잘 연산이 되었습니다. 이 경우, s.Normal을 가져오면서 일반 Normal이 아닌 WorldNormal로 변환되어 가져왔기 때문에 연산이 된 것입니다)


결론: 좌표계가 같은 것 끼리만 연산이 된다. World좌표를 가진 'worldRefl'과 탄젠트 좌표를 가진 'Normal'을 함께 연산하면 오류가 난다.


따라서 다음과 같이 코드를 바꾸어 적어주어야 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Cube("큐브맵",Cube) =""{}
        _BumpMap ("노말맵",2D) = "bump"{}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
 
        CGPROGRAM
        #pragma surface surf Lambert
        #pragma target 3.0
 
        sampler2D _MainTex;
        sampler2D _BumpMap;
        samplerCUBE _Cube;
 
        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpMap;
            float3 worldRefl;
 
            INTERNAL_DATA
        };
 
 
        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            fixed4 n = tex2D (_BumpMap, IN.uv_BumpMap);
            
            o.Normal = UnpackNormal(n);//o.Normal를 사용하는 함수보다 위에 둘 것
            
            float4 r = texCUBE(_Cube, WorldReflectionVector(IN,o.Normal));
 
            o.Albedo = 0;
            o.Emission = r.rgb;
            o.Alpha = c.a;
        }
cs


(https://docs.unity3d.com/Manual/SL-SurfaceShaders.html)


Input 안에  "INTERNAL_DATA" 을 입력하고 

float4 r = texCUBE(_Cube, WorldReflectionVector(IN,o.Normal));


와 같이 함수를 입력해야만 worldRefl과 o.Normal이 함께 연산되어 원하는 결과물을 얻을 수 있습니다.



드디어 노말이 적용되었습니다..




다음으로 헬맷오브젝트를 활용해, 주변을 반사하는 금속 재질을 표현해봅시다.




지금은 이상합니다...


알베도 위에 에미션으로 큐브맵을 통한 리플렉션이 더해져, 알베도 텍스쳐는 하나도 보이지 않게 되었어요.


마스킹맵을 이용해 리플렉션의 강도를 조절해봅시다.



해당 메탈릭 맵을 마스킹 맵으로 활용해보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Cube("큐브맵",Cube) =""{}
        _BumpMap ("노말맵",2D) = "bump"{}
        _MaskTex("Mask",2D) = "White"{}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
 
        CGPROGRAM
        #pragma surface surf Lambert
        #pragma target 3.0
 
        sampler2D _MainTex;
        sampler2D _BumpMap;
        sampler2D _MaskTex;
        samplerCUBE _Cube;
 
        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpMap;
            float2 uv_MaskTex;
            float3 worldRefl;
 
            INTERNAL_DATA
        };
 
 
        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            fixed4 m = tex2D (_MaskTex, IN.uv_MaskTex);
            fixed4 n = tex2D (_BumpMap, IN.uv_BumpMap);
            
            o.Normal = UnpackNormal(n);
            
            float4 r = texCUBE(_Cube, WorldReflectionVector(IN,o.Normal));
 
            o.Albedo = c.rgb;
            o.Emission = r.rgb * m.a;
            o.Alpha = c.a;
        }
 
        ENDCG
    }
cs


리플렉션이 들어간 에미션에, 마스킹 맵을 곱해 연산해줍니다.



금속으로 표현한 부분, 텍스쳐에서 검정색(0)인 부분은 알베도 값을 출력하고

흰색(1)인 부분은 강한 반사가 일어나도록, 금속과 같은 재질이 표현되었습니다. 회색~(중간값)의 경우 비교적 강하지 않은 반사가 일어나고 있습니다.