Unity - Surface Shader

3. Vertex Shader를 사용하여 Toon Shading 해보기

SimonLee 2023. 11. 14. 21:47

만화케릭터를 강조하기 위하여 외곽 효과를 주는 것을 toon shading, cell shding 이라고 합니다.

이번에 해볼 예제는 외곽선 그리기(toon shading) 입니다.

 

외곽선을 그리는 방법에는 림라이트 방법을 사용해도 되지만, 2 pass 렌더링 사용하는 방법도 가능합니다.

2 pass란 동일한 픽셀에 2번 렌더링 한다는 의미 입니다.

렌더링 한다고 해서 반드시 컬러값을 업데이트 하는 것은 아닙니다.

프래그먼트 쉐이더에서 아무일도 하지 않으면 변화가 없어 보입니다.

 

그러면 왜 하느냐? 

백버퍼에 특정 값을 기록하여 진짜 렌더링하는 패스에서 해당 값을 사용하기도 하며,

이번 예제처럼 덧그리기도 할수도 있습니다.

 

그럼 2 pass 렌더링을 해보러 가봅시다.

Vertex shader를 사용한 Toon shading

 

 

원리를 먼저 생각해보면요

사각형이 그려져 있는데, 사각형 보다 약간 큰 사각형이 바로 뒤에 있으면

앞에 있는 사각형이 뒤에 있는 사각형의 가장자리는 가리게 되고, 끝부분만 남게 될 것입니다. 

빨간색 체크된 부분이 외곽선 처럼 보임

 

아래 처럼 2 번 렌더링 합니다.

1) 모델을 약간만 크게 만들어서 뒤에 그립니다.

2) 모델을 원래 크기로 앞에 그립니다.

 

약간만 크게 만드는 방법은 어떻게 할까요 ?

버텍스 셰이더에서 정점을 노말방향 * coeff 값을 더해 주면 됩니다.

void vert(inout appdata_full v) 
{
    v.vertex.xyz = v.vertex.xyz + v.normal.xyz * 0.01;
}

 

뒤에 그리는 방법은? 

컬링 방법을 사용합니다.

프론트 페이스 컬링 (cull front) 을 하게 되면 앞면이 없어지고, 백페이스 컬링(cull back) 을 하면 뒷면이 없어집니다.

보통 렌더링 할때 불투명한 오브젝트의 뒷면은 그리지 않기 때문에 디폴트로 백페이스 컬링이 설정되어 있습니다.

 

예를 들어 A 조금 큰 모델 (전부 검정), B 원래 크기 모델 (원래 색상) 이라고 가정해보면,

A - cull back, B - cull front : 

--> A 렌더링 vertex의 영역이 넓기 때문에 검은색으로 B를 다 가려버립니다.

A - cull front, B - cull back : 

--> A 검은색은 뒷면만 렌더링 되고, B  원래 크기 모델 색상은 앞면에 그려지면서 외곽 효과가 잘 그려집니다.

Shader "Custom/Toon"
{
    Properties
    {
       ......
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }        
        
       	// 1nd pass
        cull back
        CGPROGRAM
        ..........
        ENDCG
		
        // 2nd pass
        cull front
        CGPROGRAM
        .........
        ENDCG
    }
    FallBack "Diffuse"
}

 

첫번째 렌더링은 fragment shader에서 검은색만 리턴하면 되고,

두번째 렌더링은 custom lighting을 적용시켜 봅니다.

 

디퓨즈 텀 음영을 자연스럽게 보간 하지 않고 영역별로 끊어지게 만들고 싶으면

n dot l 조명 값에 따라 범위를 지정하고 대표 음영 값을 넣어주면 됩니다.

float ndotl = dot(s.Normal, lightDir) * 0.5 + 0.5;

if (ndotl > 0.4) 
{
    ndotl = 1;
}
else if (ndotl > 0.2 && ndotl <= 0.4) 
{
    ndotl = 0.7;
}
else 
{
    ndotl = 0.5;
}

 

물론 if문을 쓰지 않고 다른 방법을 써도 됩니다.

3개의 대표 음영을 값을 얻어 내기도 합니다. ( 1 / 3,  2 / 3, 3 / 3 )

ndotl *= 3;
ndotl = ceil(ndotl) / 3;

 

 

전체 코드 입니다.

Shader "Custom/Toon"
{
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}         
        _BumpMap ("BumpMap", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }        
        cull back

        CGPROGRAM
        #pragma surface surf Nolight Lambert vertex:vert noshadow noambient

        #pragma target 3.0

        void vert(inout appdata_full v) 
        {
            v.vertex.xyz = v.vertex.xyz + v.normal.xyz * 0.01;
        }

        struct Input
        {
            float4 color:Color;
        };

        void surf (Input IN, inout SurfaceOutput o)
        {

        }

        float4 LightingNolight(SurfaceOutput s, float3 lightDir, float atten)
        {
            return float4(0, 0, 0, 1);
        }
        ENDCG

        cull front
        // 2nd pass
        CGPROGRAM
        #pragma surface surf Toon

        #pragma target 3.0
        
        sampler2D _MainTex;
        sampler2D _BumpMap;
        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpMap;
        };

        void surf(Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
            o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        float4 LightingToon (SurfaceOutput s, float3 lightDir, float atten) {
            float ndotl = dot(s.Normal, lightDir) * 0.5 + 0.5;
            if (ndotl > 0.4) 
            {
                ndotl = 1;
            }
            else 
            {
                ndotl = 0.5;
            }
            float4 final;
            final.rgb = s.Albedo * ndotl * _LightColor0.rgb;
            final.a = s.Alpha;
            return final;
        }

        ENDCG
    }
    FallBack "Diffuse"
}

 

728x90