표면 셰이더 조명 예제
버텍스 및 프래그먼트 셰이더 작성(Writing vertex and fragment shaders)

DX11/OpenGL Core 테셀레이션 지원 표면 셰이더

표면 셰이더는 DirectX 11/OpenGL Core GPU 테셀레이션을 다음과 같이 일부 지원합니다.

  • 테셀레이션은 tessellate:FunctionName 모디파이어(modifier)로 나타냅니다. 이 함수는 삼각형 에지와 내부 테셀레이션 팩터를 계산합니다.
  • 테셀레이션이 사용되는 경우 “버텍스 모디파이어”(vertex:FunctionName)가 테셀레이션 후에 도메인 셰이더에 생성된 각 버텍스에 대해 호출됩니다. 여기서 일반적으로 변위 매핑을 합니다.
  • 표면 셰이더는 옵션으로 퐁 테셀레이션을 계산하여 변위 매핑 없이도 모델 표면을 매끄럽게 만듭니다.

현재 테셀레이션 지원의 한계:

  • 삼각 도메인만 지원 - 사각형, 등치선 테셀레이션은 지원하지 않습니다.
  • 테셀레이션을 사용하는 경우 셰이더는 자동으로 Shader Model 4.6 타겟으로 컴파일됩니다. 즉 DX11/12, OpenGL Core 및 PS4/XB1에서만 작동합니다.

버텍스 모디파이어에서 GPU 테셀레이션을 사용하지 않을 때 예제

우선 테셀레이션 사용 없이 변위 매핑을 하는 표면 셰이더에서 시작해 봅시다. 이 셰이더는 단순히 변위 맵에서 오는 양에 기반하여 버텍스의 노멀을 따라 버텍스를 움직입니다.

    Shader "Tessellation Sample" {
            Properties {
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _DispTex ("Disp Texture", 2D) = "gray" {}
                _NormalMap ("Normalmap", 2D) = "bump" {}
                _Displacement ("Displacement", Range(0, 1.0)) = 0.3
                _Color ("Color", color) = (1,1,1,0)
                _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 300
            
                CGPROGRAM
                #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp nolightmap
                #pragma target 4.6

                struct appdata {
                    float4 vertex : POSITION;
                    float4 tangent : TANGENT;
                    float3 normal : NORMAL;
                    float2 texcoord : TEXCOORD0;
                };

                sampler2D _DispTex;
                float _Displacement;

                void disp (inout appdata v)
                {
                    float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                    v.vertex.xyz += v.normal * d;
                }

                struct Input {
                    float2 uv_MainTex;
                };

                sampler2D _MainTex;
                sampler2D _NormalMap;
                fixed4 _Color;

                void surf (Input IN, inout SurfaceOutput o) {
                    half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    o.Specular = 0.2;
                    o.Gloss = 1.0;
                    o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
                }
                ENDCG
            }
            FallBack "Diffuse"
        }

위 셰이더는 상당히 표준에 가깝습니다. 여기서 주목할 점은 다음과 같습니다.

  • disp 버텍스 모디파이어는 변위 맵을 샘플링하고 해당 노멀을 따라 버텍스를 움직입니다.
  • 이 셰이더는 디폴트 appdata_full 대신 커스텀 “버텍스 데이터 입력” 구조(appdata)를 사용합니다. 아직은 필요하지 않지만 테셀레이션에서는 가능한 한 작은 구조를 사용하는 편이 훨씬 효율적입니다.
  • 여기서는 버텍스 데이터에 두 번째 UV 좌표가 없기 때문에 라이트맵을 제외하기 위해 nolightmap 지시자를 추가합니다.

다음은 간단한 오브젝트가 이 셰이더를 사용하면 어떻게 보이는지를 나타냅니다.

고정 크기 테셀레이션

고정 크기 테셀레이션, 즉 전체 메시에 대해 동일 테셀레이션 레벨을 추가해 봅시다. 모델의 면이 스크린 크기와 대략 같을 경우 적합한 접근방식입니다. 카메라까지의 거리에 기반하여 일부 스크립트에서는 이후 코드에서 테셀레이션 레벨을 변경할 수 있습니다.

    Shader "Tessellation Sample" {
            Properties {
                _Tess ("Tessellation", Range(1,32)) = 4
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _DispTex ("Disp Texture", 2D) = "gray" {}
                _NormalMap ("Normalmap", 2D) = "bump" {}
                _Displacement ("Displacement", Range(0, 1.0)) = 0.3
                _Color ("Color", color) = (1,1,1,0)
                _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 300
            
                CGPROGRAM
                #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap
                #pragma target 4.6

                struct appdata {
                    float4 vertex : POSITION;
                    float4 tangent : TANGENT;
                    float3 normal : NORMAL;
                    float2 texcoord : TEXCOORD0;
                };

                float _Tess;

                float4 tessFixed()
                {
                    return _Tess;
                }

                sampler2D _DispTex;
                float _Displacement;

                void disp (inout appdata v)
                {
                    float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                    v.vertex.xyz += v.normal * d;
                }

                struct Input {
                    float2 uv_MainTex;
                };

                sampler2D _MainTex;
                sampler2D _NormalMap;
                fixed4 _Color;

                void surf (Input IN, inout SurfaceOutput o) {
                    half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    o.Specular = 0.2;
                    o.Gloss = 1.0;
                    o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
                }
                ENDCG
            }
            FallBack "Diffuse"
        }

이 셰이더에서 tessFixed라는 이름의 테셀레이션 함수는 테셀레이션 팩터를 단일 float4 값으로 반환합니다. 이 값은 삼각형의 각 모서리에 대한 트리 팩터, 그리고 삼각형 내부 용의 팩터 하나로 이루어집니다. 여기서는 단지 머티리얼 프로퍼티에 설정된 상수 값을 반환합니다.

거리 기반 테셀레이션

카메라로부터의 거리에 기반하여 테셀레이션 레벨을 변경할 수도 있습니다. 예를 들어, 다음과 같이 두 거리 값을 정의할 수 있습니다. 하나는 테셀레이션이 최대일 때의 거리(예: 10미터), 다른 하나는 테셀레이션 레벨이 점점 감소해 가는 거리(예: 20미터)입니다.

    Shader "Tessellation Sample" {
            Properties {
                _Tess ("Tessellation", Range(1,32)) = 4
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _DispTex ("Disp Texture", 2D) = "gray" {}
                _NormalMap ("Normalmap", 2D) = "bump" {}
                _Displacement ("Displacement", Range(0, 1.0)) = 0.3
                _Color ("Color", color) = (1,1,1,0)
                _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 300
            
                CGPROGRAM
                #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap
                #pragma target 4.6
                #include "Tessellation.cginc"

                struct appdata {
                    float4 vertex : POSITION;
                    float4 tangent : TANGENT;
                    float3 normal : NORMAL;
                    float2 texcoord : TEXCOORD0;
                };

                float _Tess;

                float4 tessDistance (appdata v0, appdata v1, appdata v2) {
                    float minDist = 10.0;
                    float maxDist = 25.0;
                    return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);
                }

                sampler2D _DispTex;
                float _Displacement;

                void disp (inout appdata v)
                {
                    float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                    v.vertex.xyz += v.normal * d;
                }

                struct Input {
                    float2 uv_MainTex;
                };

                sampler2D _MainTex;
                sampler2D _NormalMap;
                fixed4 _Color;

                void surf (Input IN, inout SurfaceOutput o) {
                    half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    o.Specular = 0.2;
                    o.Gloss = 1.0;
                    o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
                }
                ENDCG
            }
            FallBack "Diffuse"
        }

여기서 테셀레이션 함수는 세 개의 파라미터, 즉 테셀레이션 전의 세 삼각형 코너 버텍스 데이터를 받습니다. 이 파라미터를 가지고 이제 버텍스 포지션에 따라 달라지는 테셀레이션 레벨을 계산합니다. 내장형 헬퍼 파일 Tessellation.cginc을 포함하고 이 파일로부터 UnityDistanceBasedTess 함수를 호출하여 모든 동작이 이루어지도록 합니다. 이 함수는 각 버텍스에서 카메라까지의 거리를 계산하고 최종 테셀레이션 팩터를 유도합니다.

모서리 길이 기반 테셀레이션

순수 거리 기반 테셀레이션은 삼각형 크기가 상당히 유사한 경우에만 유용합니다. 위 이미지의 경우 작은 삼각형을 가지는 오브젝트는 테셀레이션이 지나치게 된 것을 볼 수 있으며 큰 삼각형을 가지는 오브젝트는 충분히 테셀레이션되지 않은 것을 확인할 수 있습니다.

대신 화면의 삼각형 모서리 길이를 기반으로 테셀레이션 레벨을 계산할 수도 있습니다. 모서리가 길수록 더 큰 테셀레이션 팩터가 적용됩니다.

    Shader "Tessellation Sample" {
            Properties {
                _EdgeLength ("Edge length", Range(2,50)) = 15
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _DispTex ("Disp Texture", 2D) = "gray" {}
                _NormalMap ("Normalmap", 2D) = "bump" {}
                _Displacement ("Displacement", Range(0, 1.0)) = 0.3
                _Color ("Color", color) = (1,1,1,0)
                _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 300
            
                CGPROGRAM
                #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessEdge nolightmap
                #pragma target 4.6
                #include "Tessellation.cginc"

                struct appdata {
                    float4 vertex : POSITION;
                    float4 tangent : TANGENT;
                    float3 normal : NORMAL;
                    float2 texcoord : TEXCOORD0;
                };

                float _EdgeLength;

                float4 tessEdge (appdata v0, appdata v1, appdata v2)
                {
                    return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
                }

                sampler2D _DispTex;
                float _Displacement;

                void disp (inout appdata v)
                {
                    float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                    v.vertex.xyz += v.normal * d;
                }

                struct Input {
                    float2 uv_MainTex;
                };

                sampler2D _MainTex;
                sampler2D _NormalMap;
                fixed4 _Color;

                void surf (Input IN, inout SurfaceOutput o) {
                    half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    o.Specular = 0.2;
                    o.Gloss = 1.0;
                    o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
                }
                ENDCG
            }
            FallBack "Diffuse"
        }

여기서도 마찬가지로 모든 실제 작업을 처리하기 위해 Tessellation.cginc에서 UnityEdgeLengthBasedTess을 호출합니다.

성능 향상을 위해 권장되는 방법은 패치 절두체 컬링을 수행하는 UnityEdgeLengthBasedTessCull 함수를 대신 호출합니다. 이렇게 하면 셰이더 비용이 조금 더 들지만 카메라 뷰 바깥에 있는 메시 부분에 대한 GPU 작업을 상당히 절약할 수 있습니다.

퐁 테셀레이션

퐁 테셀레이션은 세부 분할된 면의 포지션을 수정하여 그 결과로 얻어진 표면이 메시 노멀을 다소 따르도록 해 줍니다. 이 방식은 폴리곤이 적은 메시를 더 매끄럽게 만드는 데 상당히 효과적인 방법입니다.

Unity의 표면 셰이더는 tessphong:VariableName 컴파일 지시자를 사용하여 퐁 테셀레이션을 자동으로 계산할 수 있습니다. 아래는 예제 셰이더입니다.

    Shader "Phong Tessellation" {
            Properties {
                _EdgeLength ("Edge length", Range(2,50)) = 5
                _Phong ("Phong Strengh", Range(0,1)) = 0.5
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _Color ("Color", color) = (1,1,1,0)
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 300
            
                CGPROGRAM
                #pragma surface surf Lambert vertex:dispNone tessellate:tessEdge tessphong:_Phong nolightmap
                #include "Tessellation.cginc"

                struct appdata {
                    float4 vertex : POSITION;
                    float3 normal : NORMAL;
                    float2 texcoord : TEXCOORD0;
                };

                void dispNone (inout appdata v) { }

                float _Phong;
                float _EdgeLength;

                float4 tessEdge (appdata v0, appdata v1, appdata v2)
                {
                    return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
                }

                struct Input {
                    float2 uv_MainTex;
                };

                fixed4 _Color;
                sampler2D _MainTex;

                void surf (Input IN, inout SurfaceOutput o) {
                    half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    o.Alpha = c.a;
                }

                ENDCG
            }
            FallBack "Diffuse"
        }

다음은 일반 셰이더(상단 행)와 퐁 테셀레이션을 사용한 셰이더(하단 행)의 차이를 비교합니다. 변위 매핑이 전혀 없이도 표면이 더 둥글게 변한 것을 볼 수 있습니다.

표면 셰이더 조명 예제
버텍스 및 프래그먼트 셰이더 작성(Writing vertex and fragment shaders)