본문 바로가기

게임 개발/유니티

[유니티/셰이더] 커스텀 셰이더 기초

728x90

# 준비

기본적인 머테리얼을 생성합니다.

Unlit Shader를 생성합니다.

이것은 조명없이 텍스처만 표시하는 간단한 셰이더입니다.

유니티의 에셋스토어에서 무료로 제공하는 Space Robot Kyle 에셋을 임포트합니다.

 

 

 

# 간단한 Unlit Shader

기본적인 언릿 셰이더를 생성하고 더 단순화한 뒤 코멘트를 붙였습니다.

Shader "Unlit/NewUnlitShader"
{
    Properties
    {
        // 인스펙터에 타일링/오프셋 설정이 보이지 않습니다.
        [NoScaleOffset]
        _MainTex("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert     // 버텍스 셰이더는 vert
            #pragma fragment frag   // 픽셀(프래그먼트) 셰이더는 frag

            struct appdata  // 버텍스 셰이더의 input
            {
                float4 vertex : POSITION;   // 버텍스 위치
                float2 uv : TEXCOORD0;      // 텍스처 위치
            };

            struct v2f   // 버텍스 셰이더의 output ( 버텍스 >> 픽셀 )
            {
                float2 uv : TEXCOORD0;  	// 텍스처 위치
                float4 vertex : SV_POSITION;    // 잘라낸 공간 위치
            };

            sampler2D _MainTex; // 이 텍스처를 샘플링합니다.

            // 버텍스 셰이더
            // 각 버텍스마다 실행됩니다.
            v2f vert (appdata v)
            {
                v2f o;
                // 클립 공간으로 변환
                // 투영행렬으로 곱하기
                o.vertex = UnityObjectToClipPos(v.vertex);

                // 텍스처 좌표를 전달합니다.
                o.uv = v.uv;
                return o;
            }

            // 픽셀 셰이더 (fixed4 타입으로 리턴)
            // 각 픽셀마다 실행됩니다.
            fixed4 frag (v2f i) : SV_Target // SV_Target은 셰이더 시멘틱이라 합니다.
            {
                // 텍스처를 샘플링하고 리턴합니다.
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

 

버텍스 셰이더가 오브젝트의 위치 및 텍스처 좌표를 클립 공간으로 보내고, 

픽셀 셰이더가 클립 공간의 데이터를 픽셀로 바꾸어 보내는 흐름이 보입니다.

 

 

 

# 더 간단한 컬러 셰이더

더 간단하게 단순화해봤습니다.

실제로 사용할 만한 셰이더는 아니지만, 셰이더의 흐름을 이해하는데 도움이 될 것입니다.

Shader "Unlit/SingleColor"
{
    Properties
    {
        // 인스펙터에서 색상 설정. 기본 흰색.
        _Color("Main Color", Color) = (1,1,1,1)
    }
        SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert     
            #pragma fragment frag   

            // 버텍스 셰이더
            // 이번에는 appdata 구조체를 사용하지 않았습니다.
            // 리턴 타입도 v2f 구조체가 아닌, 단일 출력인 float4 입니다.
            float4 vert(float4 vertex : POSITION) : SV_POSITION
            {
                return UnityObjectToClipPos(vertex);
            };

            // 머테리얼의 색상
            float4 _Color;

            // 픽셀 셰이더
            // 별다른 입력이 없고, 그대로 리턴합니다.
            fixed4 frag() : SV_Target
            {
                return _Color;
            }
            ENDCG
        }
    }
}

셰이더의 입출력으로 구조체를 사용할 수도 있고, 수동으로 사용할 수도 있습니다.

 

 

 

# Normal 셰이더

월드 공간에 메시 노멀을 표시하는 셰이더입니다.

Shader "Unlit/WorldSpaceNormals"
{
	// 이번에는 프로퍼티가 없습니다.

	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert     
			#pragma fragment frag   

			// 월드 노멀 헬퍼를 추가합니다.
			#include "UnityCG.cginc"

			struct v2f {
				// 텍스처 좌표를 사용한 월드 노멀을 출력하기 위함
				half3 worldNormal : TEXCOORD0;
				float4 pos : SV_POSITION;
			};

			// 버텍스 셰이더 : 객체 공간의 노멀도 입력으로 사용할 수 있습니다.
			v2f vert(float4 vertex : POSITION, float4 normal : NORMAL)
			{
				v2f o;
				// "UnityCG.cginc"가 가지고 있는 함수들을 사용합니다.
				o.pos = UnityObjectToClipPos(vertex);
				o.worldNormal = UnityObjectToWorldNormal(normal);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed4 c = 0;
				
				// 노말은 -1 ~ 1사이의 xyz 값을 갖는 3D 벡터값입니다.
				// 색상으로 표시하려면 0 ~ 1 사이에 값을
				// rgb 구성요소에 대입해줍니다.
				c.rgb = i.worldNormal * 0.5 + 0.5;
				return c;
			}

			ENDCG
		}
	}
}

정규화된 벡터를 컬러로 시각화하는 간단한 방법입니다.

-1 ~ 1 사이값이기 때문에

0.5를 곱하면 -0.5 ~ 0.5 사이 값이 되고

0.5를 더하면 0 ~ 1 사이 값이 됩니다.

 

 

# 월드-공간 노멀을 사용한 환경 반사

Shader "Unlit/SkyReflection"
{
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert     
			#pragma fragment frag   
			#include "UnityCG.cginc"

			struct v2f {
				half3 worldRefl : TEXCOORD0;
				float4 pos : SV_POSITION;
			};

			v2f vert(float4 vertex : POSITION, float3 normal : NORMAL)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(vertex);

				// 버텍스의 월드 공간 위치를 계산합니다.
				float3 worldPos = mul(unity_ObjectToWorld, vertex).xyz;

				// 카메라 방향을 계산합니다.
				float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

				// 월드 공간 노멀을 계산합니다.
				float3 worldNormal = UnityObjectToWorldNormal(normal);

				// 월드 공간 반사 벡터입니다.
				o.worldRefl = reflect(-worldViewDir, worldNormal);

				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				// 반사 벡터를 사용해 기본 큐브맵을 샘플링합니다.
				half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);

				// DecodeHDR 로 반사 프로브 데이터에서 실제 컬러값을 얻을 수 있습니다.
				half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);

				fixed4 c = 0;
				c.rgb = skyColor;
				return c;
			}

			ENDCG
		}
	}
}

씬에서 반사 소스로 스카이박스가 사용될 때,
스카이박스 데이터를 가지고 있는 '기본' 반사프로브가 생성됩니다.
반사 프로브는 내부적으로 큐브맵 텍스쳐입니다.

 

 

 

# 노멀맵이 있는 환경 반사

노멀맵을 사용한 반사이미지

 

위에 있는 셰이더를 살짝 수정합니다.

Shader "Unlit/SkyReflection Per Pixel"
{
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert     
			#pragma fragment frag   
			#include "UnityCG.cginc"

			struct v2f {
				half3 worldPos : TEXCOORD0;
				half3 worldNormal : TEXCOORD1;
				float4 pos : SV_POSITION;
			};

			v2f vert(float4 vertex : POSITION, float3 normal : NORMAL)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(vertex);
				o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
				o.worldNormal = UnityObjectToWorldNormal(normal);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				// 카메라 방향과 반사 벡터 계산
				half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				half3 worldRefl = reflect(-worldViewDir, i.worldNormal);

				// 큐브맵 샘플링 및 컬러값 얻기
				half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
				half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);

				fixed4 c = 0;
				c.rgb = skyColor;
				return c;
			}

			ENDCG
		}
	}
}

화면을 보기에는 크게 달라진 점이 없습니다.

하지만 이전 셰이더에서 버텍스에서 계산한 것을

화면의 모든 픽셀에서 계산하는 것으로 바뀌었습니다.

 

 

그리고 "탄젠트 공간"에 대해 알아야하는데,

노멀 맵 텍스처는 대부분 모델의 표면을 따르는 것이라 볼 수 있습니다.

이번 셰이더는 탄젠트 공간 기반 벡터를 알아야 하고

텍스처로부터 노멀 벡터를 읽어야하고,

이 벡터를 월드 공간으로 변환한 후

셰이더에서 계산을 해주어야 합니다.

Shader "Unlit/SkyReflection Per Pixel"
{
	Properties{
		// 머테리얼의 노멀맵 텍스처입니다.
		// 기본값은 평평한 표면의 더미값입니다.
		_BumpMap("Normal Map", 2D) = "bump" {}
	}
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert     
			#pragma fragment frag   
			#include "UnityCG.cginc"

			struct v2f {
				half3 worldPos : TEXCOORD0;

				// 탄젠트 공간에서 월드 공간으로 변환하는
				// 3*3 회전 행렬을 갖는 세 벡터입니다.
				half3 tspace0 : TEXCOORD1;	// tangent.x, bitangent.x, normal.x
				half3 tspace1 : TEXCOORD2;  // tangent.y, bitangent.y, normal.y
				half3 tspace2 : TEXCOORD3;  // tangent.z, bitangent.z, normal.z
				
				// 노멀맵을 위한 텍스처 좌표
				float2 uv : TEXCOORD4;
				float4 pos : SV_POSITION;
			};
			

			
			v2f vert(float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(vertex);
				o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
				half3 wNormal = UnityObjectToWorldNormal(normal);
				half3 wTangent = UnityObjectToWorldDir(tangent.xyz);

				half tangentSign = tangent.w * unity_WorldTransformParams.w;
				half3 wBitangent = cross(wNormal, wTangent) * tangentSign;

				o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
				o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
				o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
				o.uv = uv;
				return o;
			}

			sampler2D _BumpMap;

			fixed4 frag(v2f i) : SV_Target
			{
				half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
				half3 worldNormal;
				worldNormal.x = dot(i.tspace0, tnormal);
				worldNormal.y = dot(i.tspace1, tnormal);
				worldNormal.z = dot(i.tspace2, tnormal);
				
				
				half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				half3 worldRefl = reflect(-worldViewDir, worldNormal);
				half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
				half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
				fixed4 c = 0;
				c.rgb = skyColor;
				return c;
			}

			ENDCG
		}
	}
}

 

 

 

# 다양한 텍스처를 추가하기

여기에 처음 셰이더에서 사용했었던 베이스 컬러 텍스처를 추가하면 로봇이 사실적으로 표현됩니다.

위 이미지는 오클루전 텍스처를 실제로 사용하지 않았습니다.

Shader "Unlit/More Textures"
{
    Properties{
        // 베이스 컬러 텍스처, 오클루전 텍스처를 추가합니다.
        _MainTex("Base texture", 2D) = "white" {}
        _OcclusionMap("Occlusion", 2D) = "white" {}
        _BumpMap("Normal Map", 2D) = "bump" {}
    }
        SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            
            struct v2f {
                float3 worldPos : TEXCOORD0;
                half3 tspace0 : TEXCOORD1;
                half3 tspace1 : TEXCOORD2;
                half3 tspace2 : TEXCOORD3;
                float2 uv : TEXCOORD4;
                float4 pos : SV_POSITION;
            };
            v2f vert(float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(vertex);
                o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
                half3 wNormal = UnityObjectToWorldNormal(normal);
                half3 wTangent = UnityObjectToWorldDir(tangent.xyz);
                half tangentSign = tangent.w * unity_WorldTransformParams.w;
                half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
                o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
                o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
                o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
                o.uv = uv;
                return o;
            }

            // 위 프로퍼티를 위한 텍스처 변수입니다.
            sampler2D _MainTex;
            sampler2D _OcclusionMap;
            sampler2D _BumpMap;

            fixed4 frag(v2f i) : SV_Target
            {
                half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
                half3 worldNormal;
                worldNormal.x = dot(i.tspace0, tnormal);
                worldNormal.y = dot(i.tspace1, tnormal);
                worldNormal.z = dot(i.tspace2, tnormal);
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 worldRefl = reflect(-worldViewDir, worldNormal);
                half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
                half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
                fixed4 c = 0;
                c.rgb = skyColor;

                // 베이스컬러 텍스처와 오클루전 텍스처를 곱해줍니다.
                fixed3 baseColor = tex2D(_MainTex, i.uv).rgb;
                fixed occlusion = tex2D(_OcclusionMap, i.uv).r;
                c.rgb *= baseColor;
                c.rgb *= occlusion;

                return c;
            }
            ENDCG
        }
    }
}
728x90