만화cartoon처럼 명암을 칼같이 딱딱 끊어서 처리하는 툰셰이더에 대해 알아보도록 하겠다.

    완만한 곡선의 기존 난반사광 그래프와 달리 툰 셰이더는 ceil()함수를 사용하여 특정 값을 무조건 올림하여 사용한다.

    일반 난반사광 그래프와 다른 계단형태의 툰셰이딩 그래프

     

    코드를 작성하기에 앞서 RenderMonkey Workspace에 난반사광 계산을 위한 float4 빛의 위치값 변수를 생성한다.

    값은 임의로 (500, 500, -500, 1)로 맞춘다.

     

    정점의 법선정보를 위해 Stream Mapping에 float3 Normal 필드를 추가하고

     

    임의의 색을 지정하기 위해 float3 변수 또한 추가해준다.

    녹색으로 설정하기 위해 값을 (0,1,0)로 변경한다.

     

    그리고 기존 작업에서는 float4x4 World행렬, View행렬, Projection행렬을 각각 변수를 따로 만들어 곱했으나

    이번에는 행렬들을 미리 합쳐 불필요한 연산을 줄일 수 있도록 하겠다.

    float4x4 WorldViewProjection 값을 넣어준다.

     

    이때 한가지 고려할 점은 바로 난반사광을 계산하려면 월드행렬이 필요했다

    하지만 현재 전역변수로 미리 합쳐버린 하나의 행렬만 존재하는데 어떻게 해야할까?

    답은 정점의 위치와 법선벡터를 월드공간으로 변환하는 대신에 빛의 위치를 지역공간으로 변환하는 것이다.

    월드공간을 물체공간으로 변환하려면 월드행렬의 역행렬inverse matrix을 곱하면 된다.

    float4x4 WorldInverse

     

    정점셰이더 Vertex Shader

    float4x4 gWorldViewProjectionMatrix;
    float4x4 gInvWorldMatrix;
    
    float4 gWorldLightPosition;
    
    struct VS_INPUT 
    {
       float4 Position : POSITION0;
       
       // 난반사광 계산을 위한 법선을 추가한다.
       float3 Normal : NORMAL;
    };
    
    struct VS_OUTPUT 
    {
       float4 Position : POSITION0;
       float3 Diffuse : TEXCOORD1;
    };
    
    VS_OUTPUT vs_main( VS_INPUT Input )
    {
       VS_OUTPUT Output;
       
       // 미리 합쳐놓은 행렬로 한번에 정점의 위치를 투영공간으로 가져왔다.
       Output.Position = mul( Input.Position, gWorldViewProjectionMatrix );
       
       // 빛의 위치를 지역공간으로 변환한 뒤
       float3 objectLightPosition = mul(gWorldLightPosition, gInvWorldMatrix);
       
       // 광원의 위치에서 현재 위치를 가리키는 방향벡터를 만든다.
       float3 lightDir = normalize(Input.Position.xyz - objectLightPosition);
       
       // 그리고 N ㆍ L 계산을 통해 난반사광의 양을 구한다.
       Output.Diffuse = dot(-lightDir, normalize(Input.Normal));
       
       return( Output );
    }

     

    픽셀셰이더 Pixel Shader

    float3 gSurfaceColor;
    
    struct PS_INPUT
    {
       float3 Diffuse : TEXCOORD1;
    };
    
    float4 ps_main(PS_INPUT Input) : COLOR0
    {  
       // 0 이하의 값을 잘라내고
       float3 diffuse = saturate(Input.Diffuse);
       
       // 이 값을 0.2 단위로 자른다. 0.2 단위로 무조건 올림을 하면 된다.
       // ceil() 함수는 언제나 바로 위의 정수로만 올림을 하기 때문에 다음과 같이 한다.
       // diffuse 0 ~ 1 사이의 값에 5를 곱하면 범위가 0 ~ 5가 되고
       // 여기에 ceil()을 적용하면 그 결과 값이 0, 1, 2, 3, 4, 5 중에 하나가 되고
       // 이 값을 5로 나누면 최종 결과 값이 0, 0.2, 0.4, 0.5, 0.8, 1이 된다.
       diffuse = ceil(diffuse * 5) / 5.0f;
       
       // 표면의 색을 곱하면 끝
       return( float4( gSurfaceColor * diffuse.xyz, 1.0f ) );  
    }

     

    모델을 주전자로 바꾸어 명암차이가 더 잘보이도록 하였다.
    diffuse = ceil(diffuse*3) / 3.0f; 값을 수정하여 명암의 단계를 조절할 수 있다.
    임의의 텍스처를 입혀보았다. 뭔가 이상한데?

    툰셰이더를 사용할 때는 툰Toon의 특성에 맞게 DiffuseMap을 만들어야 할 것 같다.

     

    출처 : 셰이더 프로그래밍 입문

    댓글