본문 바로가기

유니티/Catlikecoding

[렌더링] 셰이더 기초 (Catlikecoding)

반응형

해당 글은 CatlikeCoding의 튜토리얼을 실습하며 번역해놓은 글입니다. 번역 과정에서 제가 내용을 덧붙이거나 삭제한 부분이 존재합니다. 원본의 링크는 글 하단에 있습니다.

 

 원본 글에 적혀있지 않은 내용을 추가할 때에는 (작성자 추가: ..내용)과 같은 형태로 작성하겠습니다.

 

요약

- 정점 변형

- 컬러 픽셀 

- 셰이더 프로퍼티 활용하기

- 버텍스에서 프래그먼트로 데이터 전달하기

- 컴파일된 셰이더 코드 검사

- 타일링 및 오프셋을 사용하여 텍스쳐 샘플링

 

 

1. 기본 씬

 Unity에서 새 씬을 생성할 떄 기본 카메라와 디렉셔널 라이트로 시작합니다. 씬에 구를 추가하고 원점에 배치한 다음 카메라를 바로 앞에 배치합니다. 이 씬은 매우 단순한 장면이지만 이미 많은 복잡한 렌더링이 진행 중입니다. 렌더링 프로세스를 잘 이해하려면 모든 화려한 요소를 제거하고 기본에만 집중하는 것이 도움이 됩니다.

 

1.1. 벗겨내기

라이팅 세팅에서 씬의 조명 설정을 살펴보세요. 그러면 세 개의 탭이 있는 조명 창이 나타납니다. 여기서는 기본적으로 활성화되어 있는 씬 탭에만 살펴보겠습니다.

 

 스카이박스를 현재 선택할 수 있는 환경 조명에 대한 섹션이 있습니다. 이 스카이박스는 현재 씬 배경, 주변 조명 및 리플렉션에 사용됩니다. 없음으로 설정하면 꺼집니다. Skybox Material을 None으로 설정합니다. 이 과정에서 미리 계산된 전역 조명 패널과 실시간 전역 조명 패널도 끌 수 있습니다. 이는 당분간 사용하지 않을 예정입니다.

 

 

배경색은 카메라별로 정의됩니다. 기본적으로 스카이박스를 렌더링하지만 스카이박스가 없는 지금 단색으로 설정됩니다.

 

 렌더링을 더욱 단순화하려면 디렉셔널 라이트 오브젝트를 비호라성화하거나 삭제합니다. 이렇게 하면 씬의 직접 조명과 그로 인해 드리워지는 그림자가 제거됩니다. 그러면 구의 실루엣이 ambient color로 처리된 단색 배경만 남게 됩니다.

 

2. 오브젝트에서 이미지로

 아주 간단한 씬은 두 단계로 그려집니다. 먼저 카메라의 배경색으로 이미지를 채웁니다. 그런 다음 그 위에 구의 실루엣을 그립니다. Unity는 구를 그려야 한다는 것을 어떻게 알 수 있을까요? 구체 오브젝트가 있고 이 오브젝트에는 메시 렌더러 컴포넌트가 있습니다. 이 오브젝트가 카메라 뷰 안에 있으면 렌더링되어야 합니다. Unity는 오브젝트의 바운딩 박스가 카메라의 뷰 프러스텀과 교차하는지 확인하여 이를 확인합니다.

 

 Transform컴포넌트는 메쉬와 바운딩 박스의 위치, 방향, 크기를 변경하는 데 사용됩니다. 실제로는 1부의 행렬 파트에서 설명된 대로 전체 트랜스폼 계층 구조가 사용됩니다. 오브젝트가 카메라의 시야에 들어오면 렌더링이 예약됩니다. 마지막으로 GPU는 오브젝트의 메시를 렌더링하는 작업을 수행합니다. 구체적인 렌더링 지침은 오브젝트의 메터리얼에 의해 정의됩니다. 메터리얼은 GPU 프로그램인 셰이더와 셰이더에 있는 모든 설정을 참조합니다.

 

 

 현재 오브젝트에는 Unity의 스탠다드 셰이더를 사용하는 기본 메터리얼이 있습니다. 이 셰이더를 처음부터 새로 빌드하는 자체 셰이더로 대체할 것입니다.

 

2.1. 당신의 첫 번째 셰이더

Assets / Create / Shader / Unlit Shader 메뉴를 통해 'My First Shader'이라는 이름의 셰이더를 생성합니다. 셰이더 파일을 열고 파일의 모든 내용을 제거합니다. 우리는 처음부터 시작하겠습니다. 셰이더는 셰이더 키워드로 정의됩니다. 그 뒤에는 이 셰이더를 선택하는 데 사용할 수 있는 셰이더 메뉴 항목을 설명하는 문자열이 이어집니다. 파일 이름과 일치할 필요는 없습니다. 그 다음에는 셰이더의 콘텐츠가 포함된 블록이 나옵니다.

 

Shader "Custom/My First Shader" 
{

}

 

 파일을 저장합니다. 하위 셰이더나 대체 셰이더가 없기 때문에 셰이더가 지원되지 않는다는 경고가 표시됩니다. 셰이더가 작동하지 않지만 메터리얼에 셰이더를 할당할 수 있씁니다. 따라서 Assets / Create / Material를 통해 새 메터리얼을 생성하고, Shader 메뉴에서 셰이더를 선택합니다.

 

 

 구 객체가 기본 메터리얼 대신 방금 제작한 자체 메터리얼을 사용하도록 변경합니다. 구가 마젠타색이 됩니다. 이 색상을 사용하여 문제에 대한 주의를 환기시키는 오류 셰이더로 전환되기 때문에 이런 일이 발생합니다. 셰이더 오류에 서브 셰이더가 언급되었습니다. 이를 사용하여 여러 셰이더 배리언트를 함께 그룹화할 수 있습니다. 이를 통해 빌드 플랫폼이나 디테일 수준에 따라 서로 다른 서브 셰이더를 제공할 수 있습니다. 예를 들어 데스크톱용 서브 셰이더와 모바일용 서브 셰이더가 있을 수 있습니다. 우리에게 이번 챕터에서 서브 셰이더 블록은 하나만 있으면 됩니다.

 

Shader "Custom/My First Shader" {
	SubShader {
		
	}
}

 

 서브 셰이더에는 하나 이상의 패스가 포함되어야 합니다. 셰이더 패스는 오브젝트가 실제로 렌더링되는 곳입니다. 여기서는 하나의 패스를 사용하지만 더 많은 패스를 사용할 수도 있씁니다.패스가 두 개 이상이라는 것은 오브젝트가 여러번 렌더링된다는 것을 의미하며, 이는 많은 효과에 이용됩니다.

 

Shader "Custom/My First Shader" {
	SubShader {
		Pass {

		}
	}
}

 

 이제 빈 패스의 기본 동작을 사용하므로 구체가 흰색이 됩니다. 이 경우 셰이더 오류가 더 이상 발생하지 않는다는 뜻입니다. 하지만 콘솔에는 여전히 이전 오류가 표시될 수 있습니다. 셰이더가 오류 없이 리컴파일될 때 이전 에러가 지워지지 않고 계속 남아있는 경향이 있습니다.

 

 

2.2. 셰이더 프로그램

 이제 셰이더 프로그램을 직접 작성할 차례입니다. HLSL 및 CG 셰이딩 언어의 변형인 Unity의 셰이딩 언어를 사용합니다. 코드의 시작을 'CGPROGRAM" 키워드로 표시해야 합니다. 그리고 'ENDCG' 키워드로 종료되어야 합니다.

 

Shader "Custom/My First Shader" 
{
	SubShader {
		Pass {
			CGPROGRAM

			ENDCG
		}
	}
}

 

 이제 셰이더 컴파일러에서 셰이더에 버텍스 및 프래그먼트 프로그램이 없다고 불평합니다. 셰이더는 각각 두 개의 프로그램으로 구성됩니다. 버텍스 프로그램은 메시의 버텍스 데이터 처리를 담당합니다. 여기에는 1부의 행렬에서 했던 것처럼 오브젝트 공간에서 디스플레이 공간으로 변환하는 작업이 포함됩니다. 프래그먼트 프로그램은 메시의 삼각형 내부에 있는 개별 픽셀에 색을 입히는 작업을 담당합니다. 'pragma' 지시어를 통해 컴파일러에 어떤 프로그램을 사용할지 알려줘야 합니다.

 

Shader "Custom/My First Shader" 
{
	SubShader {
		Pass {
			CGPROGRAM

			#pragma vertex MyVertexProgram
			#pragma fragment MyFragmentProgram
			
			ENDCG
		}
	}
}

 

 컴파일러는 이번에는 우리가 지정한 프로그램을 찾을 수 없다는 이유로 다시 불평합니다. 아직 정의하지 않았기 떄문입니다. 버텍스와 프래그먼트 프로그램은 C#에서와 마찬가지로 메서드로 작성되지만 일반적으로 함수로 불립니다. 적절한 일므을 가진 빈 메서드 두 개를 간단히 만들어보겠습니다.

 

			CGPROGRAM

			#pragma vertex MyVertexProgram
			#pragma fragment MyFragmentProgram
			
			void MyVertexProgram () {

			}

			void MyFragmentProgram () {

			}
			ENDCG

 

 이 시점에 셰이더가 컴파일되고 구체가 사라집니다. 또는 여전히 에러가 발생할 수 있습니다. 이러한 결과는 에디터에서 사용하고 있는 렌더링 플랫폼에 따라 달라집니다. Direct3D 9를 사용하는 경우 오류가 발생할 수 있습니다.

 

2.3. 셰이더 편집

 유니티의 셰이더 컴파일러는 대상 플랫폼에 따라 코드를 다른 프로그램으로 변환합니다. 플랫폼마다 다른 솔루션이 필요합니다. 예를 들어 윈도우용 DIrect3D, Mac용 OpenGL, 모바일용 OpenGL ES 등이 있습니다. 여기서는 단일 컴파일러가 아니라 여러 컴파일러를 다루고 있습니다.

 

 어떤 컴파일러를 사용할지는 타겟팅 대상에 따라 달라집니다. 그리고 이러한 컴파일러는 동일하지 않기 때문에 플랫폼마다 다른 결과를 얻을 수 있습니다. 예를 들어, 빈 프로그램은 OpenGL및 Direct3D11 에서는 정상적으로 작동하지만, Direct3D 9을 대상으로 할 때는 에러가 발생합니다.

 

 에디터에서 셰이더를 선택하고 인스펙터 창을 살펴보세요. 현재 컴파일러 오류를 포함하여 셰이더에 대한 몇 가지 정보가 표시됩니다. 컴파일 및 코드 표시 버튼과 드롭다운 메뉴가 있는 컴파일된 코드 항목도 있습니다. 버튼을 클릭하면 유니티가 셰이더를 컴파일하고 에디터에서 그 출력을 열어서  생성된 코드를 검사할 수 있습니다.

 

 

 드롭다운 메뉴를 통해 셰이더를 수동으로 컴파일할 플랫폼을 선택할 수 있습니다. 기본값은 에디터에서 사용하는 그래픽 디바이스에 맞게 컴파일하는 것입니다. 현재 빌드 플랫폼, 라이센스가 있는 모든 플랫폼 또는 커스텀 선택 등 다른 플랫폼에 대해서도 수동으로 컴파일할 수 있습니다. 이렇게 하면 전체 빌드를 만들지 않고도 셰이더가 여러 플랫폼에서 컴파일되는지 빠르게 확인할 수 있습니다.

 

 

 선택한 프로그램을 컴파일하려면 팝업을 닫고 'Complie and show code' 버튼을 클릭합니다. 팝업 안의 작은 표시 버튼을 클릭하면 사용된 셰이더 배리언트가 표시되지만 지금은 유용하지 않습니다. 예를 들어 셰이더가 OpenGlCore 용으로 컴파일되었을 때의 결과 코드는 다음과 같습니다.

 

// Compiled shader for custom platforms, uncompressed size: 0.5KB

// Skipping shader variants that would not be included into build of current scene.

Shader "Custom/My First Shader" {
SubShader { 
 Pass {
  GpuProgramID 16807
Program "vp" {
SubProgram "glcore " {
"#ifdef VERTEX
#version 150
#extension GL_ARB_explicit_attrib_location : require
#extension GL_ARB_shader_bit_encoding : enable
void main()
{
    return;
}
#endif
#ifdef FRAGMENT
#version 150
#extension GL_ARB_explicit_attrib_location : require
#extension GL_ARB_shader_bit_encoding : enable
void main()
{
    return;
}
#endif
"
}
}
Program "fp" {
SubProgram "glcore " {
"// shader disassembly not supported on glcore"
}
}
 }
}
}

 

 생성된 코드는 버텍스 프로그램과 프래그먼트 프로그램에 대해 두 개의 블록, 즉 vp와 fp로 나뉩니다. 그러나 OpenGL의 경우 두 프로그램 모두 vp블록으로 끝납니다. 두 개의 주요 함수는 우리의 빈 메서드 두 개에 해당합니다. 따라서 이 두 가지에 집중하고 다른 코드는 무시해 봅시다.

 

#ifdef VERTEX
void main()
{
    return;
}
#endif
#ifdef FRAGMENT
void main()
{
    return;
}
#endif

 

다음은 흥미로운 부분만 가져온 Direct3D 11용으로 생성된 코드입니다. 상당히 달라 보이지만, 코드가 별 기능을 하지 않는다는 것은 분명합니다.

 

Program "vp" {
SubProgram "d3d11 " {
      vs_4_0
   0: ret 
}
}
Program "fp" {
SubProgram "d3d11 " {
      ps_4_0
   0: ret 
}
}

 

 프로그램 작업을 진행하면서 OpenGLCore 및 D3D11의 컴파일된 코드를 자주 보여줄 것으로, 내부에서 어떤 일이 일어나는지 파악할 수 있을겁니다.

 

2.4. 다른 파일 포함하기

 함수형 셰이더를 생성하려면 많은 상용구(boilerplate) 코드가 필요합니다. 이 코드는 일반적인 변수, 함수 등을 정의하는 코드입니다. C# 프로그램이라면 이 코드를 다른 클래스에 넣었을 것입니다. 하지만 셰이더에는 클래스가 없습니다. 셰이더는 클래스나 네임스페이스가 제공하는 그룹화 없이 모든 코드가 하나의 큰 파일에 포함될 뿐입니다.

 

 다행히도 코드를 여러 파일로 분할할 수 있습니다. include 지시문을 사용하여 다른 파일의 내용을 현재 파일에 로드할 수 있습니다. 포함할 수 있는 일반적인 파일은 'UnityCG.cginc'이므로 이를 예로 들어보겠습니다.

 

			CGPROGRAM

			#pragma vertex MyVertexProgram
			#pragma fragment MyFragmentProgram

			#include "UnityCG.cginc"

			void MyVertexProgram () {

			}

			void MyFragmentProgram () {

			}

			ENDCG

 

 UnityCG.cginc는 Unity와 함께 번들로 제공되는 셰이더 인클루드 파일 중 하나입니다. 여기에는 몇 가지 다른 필수 파일이  포함되어 있으며 몇 가지 일반적인 기능이 포함되어 있습니다.

UnityCG부터 시작되는 포함된 파일의 계층구조.

 

 UnityShaderVariables.cginc에는 트랜스폼, 카메라, 라이트 데이터 등 렌더링에 필요한 다양한 셰이더 변수가 정의되어 있습니다. 이러한 변수는 모두 필요할 때 Unity에서 설정합니다.

 HLSLSupport.cginc는 타겟팅하는 플랫폼에 관계없이 동일한 코드를 사용할 수 있도록 설정합니다. 따라서 플랫폼별 데이터 유형 등을 사용하는 것에 대해 걱정할 필요가 없습니다.

 UnityInstancing.cginc는 드로 콜을 줄이기 위한 특정 렌더링 기법인 인스턴싱 지원을 위한 파일입니다. 이 파일에 직접 포함되지는 않지만 UnityShaderVariables에 의존합니다.

 

 이 파일의 내용은 include 지시어를 대체하여 자신의 파일에 효과적으로 복사합니다. 이는 모든 전처리 지시어를 수행하는 전처리 단계에서 수행됩니다. 이러한 지시문은 #include 및 #pragma와 같이 해시로 시작하는 모든 문입니다. 이 단계가 완료되면 코드가 다시 처리되고, 컴파일됩니다.

 

2.5. 출력 생성

 무언가를 렌더링하려면 셰이더 프로그램이 결과를 생성해야 합니다. 버텍스 프로그램은 버텍스의 최종 좌표를 변환해야 합니다. 함수의 유형을 void에서 float4로 변경합니다. float4는 단순히 4개의 부동 소수점 숫자의 모음입니다. 지금은 0을 반환합니다.

 

			float4 MyVertexProgram () {
				return 0;
			}

 

 

 

 

더보기

0이 반환할 수 있는 유효한 값인가요?

이와 같은 단일 값을 사용하면 컴파일된 모든 실수 컴포넌트에 대해 반복됩니다. 원한다면 명시적으로 float4(0, 0, 0, 0)을 반환할 수도 있습니다.

 

 

 이제 버텍스 함수 리턴 값에 대한 시맨틱이 누락되었다는 에러가 발생합니다. 컴파일러는 4개의 플로트 컬렉션을 반환한다는 것을 알지만 해당 데이터가 무엇을 나타내는지 알지 못합니다. 따라서 GPU가 이 데이터로 무엇을 해야 하는지 알 수 없습니다. 우리는 프로그램의 출력에 대해 구체적으로 명시해줘야 합니다.

 

 우리의 경우 정점의 위치를 출력하려고 합니다. 메서드의 SV_POSITION 시맨틱을 첨부하여 이를 표시해야 합니다. SV는 System Value를 의미하고, POSITION은 최종 버텍스의 위치를 나타냅니다.

 

			float4 MyVertexProgram () : SV_POSITION {
				return 0;
			}

 

 프래그먼트 프로그램은 한 픽셀에 대해 RGBA 색상 값을 출력해야 합니다. 이를 위해 float4를 사용할 수 있습니다. 0을 반환하면 표면이 검은색이 됩니다. 프래그먼트 셰이더에도 시맨틱이 필요합니다. 이 경우 최종 색상이 어디에 쓰여질지 표시해야 합니다. 기본 셰이더 타깃인 SV_TARGET을 사용합니다. 이것은 우리가 생성하는 이미지를 포함하는 프레임 버퍼입니다.

 

			float4 MyFragmentProgram () : SV_TARGET {
				return 0;
			}

 

 하지만 빠진게 있습니다. 버텍스 프로그램의 출력은 프래그먼트 프로그램의 입력으로 사용됩니다. 이는 프래그먼트 프로그램이 버텍스 프로그램의 출력과 일치하는 타입의 매개변수를 가져와야 한다는 것을 의미합니다. 매개변수에 어떤 이름을 지정하든 상관없지만 올바른 시맨틱을 사용해야 합니다.

 

			float4 MyFragmentProgram (
				float4 position : SV_POSITION
			) : SV_TARGET {
				return 0;
			}

 

 셰이더가 다시 한 번 오류 없이 컴파일되었지만 씬의 구는 사라졌습니다. 모든 버텍스를 단일 지점으로 축소했기 때문에 발생한 일입니다. 컴파일된 OpenGLCore프로그램을 보면 이제 출력 값에 우리가 지정한 값을 쓰고 있는 것을 볼 수 있습니다. 단일 값은 4개의 벡터로 대체되었습니다.

 

#ifdef VERTEX
void main()
{
    gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
    return;
}
#endif
#ifdef FRAGMENT
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0 = vec4(0.0, 0.0, 0.0, 0.0);
    return;
}
#endif

 

구문은 다르지만 D3D11프로그램도 마찬가지입니다.

 

Program "vp" {
SubProgram "d3d11 " {
      vs_4_0
      dcl_output_siv o0.xyzw, position
   0: mov o0.xyzw, l(0,0,0,0)
   1: ret 
}
}
Program "fp" {
SubProgram "d3d11 " {
      ps_4_0
      dcl_output o0.xyzw
   0: mov o0.xyzw, l(0,0,0,0)
   1: ret 
}
}

 

2.6. 정점 변환하기

 구를 복구하려면 버텍스 프로그램이 올바른 버텍스 위치를 생성해야 합니다. 그러기 위해서는 버텍스의 오브젝트 공간 위치를 알아야 합니다. 함수에 POSITION 시맨틱을 가진 변수를 추가하면 이 위치에 엑세스 할 수 있습니다. 그러면 위치는 다음과 같은 동차 좌표로 제공됩니다. [x, y, z, 1] 이것의 타입은 float4 입니다. 이것을 직접 리턴하도록 하겠습니다.

 

			float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
				return position;
			}

 

 이제 컴파일된 버텍스 프로그램을 버텍스 입력을 갖고, 이를 출력에 복사합니다.

 

해상도 1024x768기준, 원시 버텍스 위치

 검은색 구가 표시되지만 왜곡되어 보입니다. 이는 객체 공간 위치를 마치 디스플레이 위치처럼 사용하고 있기 때문입니다. 따라서 구를 움직여도 렌더링되는 이미지에는 아무런 차이가 없습니다. 원시 버텍스 위치에서 모델-뷰-투영 행렬을 곱해줘야 합니다. 이 행렬은 1부 행렬에서 했던 것처럼 오브젝트의 트랜스폼 계층 구조와 카메라 트랜스폼 및 투영을 결합합니다.

 

  4x4 MVP 행렬은 UnityShaderVariables에서 UNITY_MATRIX_MVP로 정의되어있습니다. 곱하기 함수를 사용하여 버텍스 위치를 곱할 수 있습니다. 이렇게 하면 구가 디스플레이에 올바르게 투영됩니다. 또한 구체를 이동, 회전, 스케일이 조정되어 이미지가 예상대로 변경됩니다.

			float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
				return UnityObjectToClipPos(position);
			}

 

 OpenGLCore 버텍스 프로그램을 확인해보면 갑자기 uniform 변수가 많이 나타난 것을 알 수 있습니다. 이러한 변수는 사용되지 않고 무시되지만 행렬에 액세스하면 컴파일러가 이 변수들 포함하도록 트리거됩니다.

 

더보기

uniform 변수란 무엇인가요?

uniform이란 변수가 메시의 모든 정점과 프래그먼트에 대해 동일한 값을 갖는다는 의미입니다. 따라서 모든 정점과 프래그먼트에 대해 균일한 값을 가집니다. 자체 셰이더 프로그램에서 변수를 명시적으로 균일하게 표시할 수 있지만, 반드시 그럴 필요는 없습니다.

 

또한 곱셈과 덧셈의 묶음으로 인코딩된 행렬 곱셈도 볼 수 있습니다.

 

uniform 	vec4 _Time;
uniform 	vec4 _SinTime;
uniform 	vec4 _CosTime;
uniform 	vec4 unity_DeltaTime;
uniform 	vec3 _WorldSpaceCameraPos;
…
in  vec4 in_POSITION0;
vec4 t0;
void main()
{
    t0 = in_POSITION0.yyyy * glstate_matrix_mvp[1];
    t0 = glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;
    t0 = glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;
    gl_Position = glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;
    return;
}

 

 D3D11 컴파일러는 사용하지 않는 변수를 포함시키지 않습니다. 행렬 곱셈을 mul과 3개의 mad 명령어로 인코딩합니다. mad 명령어는 곱셈에 이어 덧셈을 수행합니다.

 

Bind "vertex" Vertex
ConstBuffer "UnityPerDraw" 352
Matrix 0 [glstate_matrix_mvp]
BindCB  "UnityPerDraw" 0
      vs_4_0
      dcl_constantbuffer cb0[4], immediateIndexed
      dcl_input v0.xyzw
      dcl_output_siv o0.xyzw, position
      dcl_temps 1
   0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw
   1: mad r0.xyzw, cb0[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb0[2].xyzw, v0.zzzz, r0.xyzw
   3: mad o0.xyzw, cb0[3].xyzw, v0.wwww, r0.xyzw
   4: ret

 

3. 컬러 픽셀 

 이제 모양을 제대로 만들었으니 색상을 추가해 보겠습니다. 가장 간단한 방법은 노란색과 같은 일정한 색상을 사용하는 것입니다.

 

			float4 MyFragmentProgram (
				float4 position : SV_POSITION
			) : SV_TARGET {
				return float4(1, 1, 0, 1);
			}

 

 물론 항상 노란색 오브젝트만 렌더링하길 원하는 것은 아닙니다. 셰이더가 모든 색상을 지원하는 것이 이상적입니다. 셰이더 프로퍼티를 통해 적용할 색상을 선택할 수 있습니다.

 

3.1. 셰이더 프로퍼티

셰이더 프로퍼티는 별도의 블록에 선언됩니다. 셰이더 상단에 추가합니다.

 

 새 블록 안에 _Tint라는 프로퍼티를 넣습니다. 어떤 이름을 지정해도 좋지만 밑줄로 시작하여 이후 첫 글자는 대문자로 시작하고 그 뒤에 소문자를 사용하는 것이 규칙입니다.

 

 프로퍼티 이름 뒤에는 메서드를 호출할 때처럼 괄호 안에 문자열과 유형이 와야 합니다. 문자열은 머티리얼 인스펙터에서 프로퍼티에 레이블을 지정하는 데 사용됩니다. 이 경우 타입은 Color입니다.

 

Shader "Custom/My First Shader" 
{	
	Properties {
		_Tint ("Tint", Color) = (1, 1, 1, 1)
	}
	SubShader {
    ...

 

 이제 셰이더 인스펙터의 프로퍼티 섹션에 틴트 프로퍼티가 표시될 것입니다. 메터리얼를 선택하면 흰색으로 설정된 새로운 색조 속성이 표시됩니다. 녹색 등 원하는 색상으로 변경할 수 있습니다.

 

3.2. 프로퍼티 접근하기

 프로퍼티를 실제로 사용하려면 셰이더 코드에 변수를 추가해야 합니다. 그 이름은 프로퍼티 이름과 정확히 일치해야 하므로 _Tint가 될 것입니다. 그런 다음 프래그먼트 프로그램에서 해당 변수를 반환하기만 하면 됩니다.

 

			#include "UnityCG.cginc"

			float4 _Tint;

			float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
				return mul(UNITY_MATRIX_MVP, position);
			}

			float4 MyFragmentProgram (
				float4 position : SV_POSITION
			) : SV_TARGET {
				return _Tint;
			}

 

 변수를 사용하려면 먼저 변수를 정의해야 합니다. C# 클래스에서는 필드와 메서드의 순서를 문제 없이 변경할 수 있지만 셰이더에서는 그렇지 않습니다. 컴파일러는 위에서 아래로 작동합니다.

 

이제 컴파일된 프래그먼트 프로그램에 색조 변수가 포함됩니다.

 

uniform 	vec4 _Time;
uniform 	vec4 _SinTime;
uniform 	vec4 _CosTime;
uniform 	vec4 unity_DeltaTime;
uniform 	vec3 _WorldSpaceCameraPos;
…
uniform 	vec4 _Tint;
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0 = _Tint;
    return;
}
ConstBuffer "$Globals" 112
Vector 96 [_Tint]
BindCB  "$Globals" 0
      ps_4_0
      dcl_constantbuffer cb0[7], immediateIndexed
      dcl_output o0.xyzw
   0: mov o0.xyzw, cb0[6].xyzw
   1: ret

 

3.3. 버텍스에서 프래그먼트로

 지금까지는 모든 픽셀에 동일한 색상을 부여했지만 이는 상당히 제한적인 기능입니다. 일반적으로 버텍스 데이터가 큰 역할을 합니다. 예를 들어 위치를 색상으로 해석할 수 있습니다. 하지만 변환된 위치는 그다지 유용하지 않습니다. 따라서 메시의 로컬 위치를 색상으로 사용하겠습니다. 버텍스 프로그램에서 프래그먼트 프로그램으로 이 추가 데이터를 어떻게 전달할까요?

 

 GPU는 삼각형을 래스터화하여 이미지를 생성합니다. 처리된 세 개의 정점을 취하고 그 사이를 보간합니다. 삼각형이 커버하는 모든 픽셀에 대해 프래그먼트 프로그램을 호출하여 보간된 데이터를 전달합니다.

 

버텍스 데이터 보간

 

 버텍스 프로그램의 출력은 프래그먼트 프로그램의 입력으로 직접 사용되지 않습니다. 보간 프로세스가 그 사이에 위치합니다. 여기서는 SV_POSITION 데이터가 보간되지만 다른 것들도 보간될 수 있습니다. 보간된 로컬 위치에 액세스하려면 프래그먼트 프로그램에 매개변수를 추가합니다. 여기서는 X, Y, Z 컴포넌트만 필요하므로 float3이면 충분합니다. 그런 다음 위치를 마치 색상처럼 출력할 수 있습니다. float4의 출력 형식을 맞추기 위해 네 번째 색상 컴포넌트를 제공해야 하므로 1을 추가해줍니다. 컴파일러에 추가한 'localPosition' 매개변수를 해석하는 방법을 알려주기 위해 시맨틱을 사용해야 합니다. 여기서는 'TEXCOORD0'을 사용하겠습니다.

 

			float4 MyFragmentProgram (
				float4 position : SV_POSITION,
				float3 localPosition : TEXCOORD0
			) : SV_TARGET {
				return float4(localPosition, 1);
			}

 

 이제 컴파일된 프래그먼트 셰이더는 균일한 색조 대신 보간된 데이터를 사용합니다.

 

in  vec3 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0.xyz = vs_TEXCOORD0.xyz;
    SV_TARGET0.w = 1.0;
    return;
}
     ps_4_0
      dcl_input_ps linear v0.xyz
      dcl_output o0.xyzw
   0: mov o0.xyz, v0.xyzx
   1: mov o0.w, l(1.000000)
   2: ret

 

 물론 버텍스 프로그램이 제대로 작동하려면 로컬 위치를 프래그먼트로 출력해줘야 합니다. 출력 매개변수(out parameter)를 추가하면 동일한 TEXCOORD0 시맨틱으로 출력할 수 있습니다. 버텍스와 프래그먼트 함수의 매개변수 이름이 일치할 필요는 없습니다. 시맨틱이 중요합니다. 버텍스 프로그램을 통해 데이터를 전달하려면 position의 X,Y,Z 컴포넌트를 이용해서 localPosition으로 값을 복사해줍니다.

 

			float4 MyVertexProgram (
				float4 position : POSITION,
				out float3 localPosition : TEXCOORD0
			) : SV_POSITION {
				localPosition = position.xyz;
				return UnityObjectToClipPos(position);
			}

 

추가된 버텍스 프로그램 출력이 컴파일러 셰이더에 포함되고 구에 색이 입혀지는 것을 볼 수 있습니다.

 

in  vec4 in_POSITION0;
out vec3 vs_TEXCOORD0;
vec4 t0;
void main()
{
    t0 = in_POSITION0.yyyy * glstate_matrix_mvp[1];
    t0 = glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;
    t0 = glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;
    gl_Position = glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;
    vs_TEXCOORD0.xyz = in_POSITION0.xyz;
    return;
}
Bind "vertex" Vertex
ConstBuffer "UnityPerDraw" 352
Matrix 0 [glstate_matrix_mvp]
BindCB  "UnityPerDraw" 0
      vs_4_0
      dcl_constantbuffer cb0[4], immediateIndexed
      dcl_input v0.xyzw
      dcl_output_siv o0.xyzw, position
      dcl_output o1.xyz
      dcl_temps 1
   0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw
   1: mad r0.xyzw, cb0[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb0[2].xyzw, v0.zzzz, r0.xyzw
   3: mad o0.xyzw, cb0[3].xyzw, v0.wwww, r0.xyzw
   4: mov o1.xyz, v0.xyzx
   5: ret

로컬 위치를 색상으로 해석합니다.

 

3.4. 구조체 이용하기

 우리 프로그램의 매개변수 목록이 지저분해 보인다고 생각하시나요? 더 많은 데이터를 전달할수록 더 지저분해질 것입니다. 버텍스 출력은 프래그먼트 입력과 일치해야 하므로 매개변수 목록을 한 곳에 정의할 수 있다면 편리할 것입니다. 다행히 그렇게 할 수 있는 방법이 있습니다.

 

데이터 구조체는 단순히 변수의 집합으로 정의할 수 있습니다. 구문이 약간 다르다는 점을 제외하면 C#의 구조체와 비슷합니다. 다음은 보간할 데이터를 정의하는 구조체입니다. 정의 뒤에 세미콜론이 사용된다는 점에 유의하세요.

 

			struct Interpolators {
				float4 position : SV_POSITION;
				float3 localPosition : TEXCOORD0;
			};

 

이 구조체를 사용하면 코드가 훨씬 더 깔끔해집니다.

 

			float4 _Tint;
			
			struct Interpolators {
				float4 position : SV_POSITION;
				float3 localPosition : TEXCOORD0;
			};

			Interpolators MyVertexProgram (float4 position : POSITION) {
				Interpolators i;
				i.localPosition = position.xyz;
				i.position = mul(UNITY_MATRIX_MVP, position);
				return i;
			}

			float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
				return float4(i.localPosition, 1);
			}

 

3.5. 색상 조정

색상에서 음수 값은 0으로 고정됩니다. 기본 구의 객체의 오브젝트 공간에서의 반경은 -0.5에서 0.5 사이이기 때문에 구가 다소 어둡게 렌더링 되고 있습니다. 모든 채널에 0.5를 더하면 이 색상 0-1범위로 이동시킬 수 있습니다.

				return float4(i.localPosition + 0.5, 1);

 

_Tint를 결과에 반영하여 적용할 수도 있습니다.

 

				return float4(i.localPosition + 0.5, 1) * _Tint;
uniform 	vec4 _Tint;
in  vec3 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_TARGET0;
vec4 t0;
void main()
{
    t0.xyz = vs_TEXCOORD0.xyz + vec3(0.5, 0.5, 0.5);
    t0.w = 1.0;
    SV_TARGET0 = t0 * _Tint;
    return;
}
ConstBuffer "$Globals" 128
Vector 96 [_Tint]
BindCB  "$Globals" 0
      ps_4_0
      dcl_constantbuffer cb0[7], immediateIndexed
      dcl_input_ps linear v0.xyz
      dcl_output o0.xyzw
      dcl_temps 1
   0: add r0.xyz, v0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000)
   1: mov r0.w, l(1.000000)
   2: mul o0.xyzw, r0.xyzw, cb0[6].xyzw
   3: ret

 

4. 텍스쳐링

 삼각형을 더 추가하지 않고 메쉬에 더 뚜렷한 디테일과 다양성을 추가하려면 텍스쳐를 사용하면 됩니다. 메시의 삼각형에 이미지를 투영해서 이와 같은 작업을 수행합니다. 텍스쳐 좌표는 텍스쳐 투영을 제어하는 데 사용됩니다. 이는 텍스쳐의 실제 종횡비에 관계없이 이미지 전체를 하나의 단위 정사각형 영역으로 덮는 2D좌표 쌍입니다. 수평좌표는 U, 수직좌표는 V로 부르며 일반적으로 UV좌표라고 합니다.

 

 

 유니티에서 U좌표는 왼쪽에서 오른쪽으로 증가합니다. V좌표는 아래에서 수직으로 같은 방향으로 증가하며, UV 모두 0에서 1사이의 값을 가집니다. Direct3D에서는 V좌표가 위에서 아래로 증가합니다만, 이 차이에 대해 걱정할 필요는 거의 없습니다.

 

4.1. UV 좌표계 이용하기

 Unity의 기본 메시에는 텍스처 매핑에 적합한 UV 좌표가 있습니다. 버텍스 프로그램은 TEXCOORD0 시맨틱을 가진 파라미터를 통해 이 좌표에 액세스할 수 있습니다. 버텍스 프로그램의 입력으로 uv를 추가해야 하는데, 이를 구조체를 이용해 그룹화해서 정리하겠습니다.

 

			struct Interpolators {
				float4 position : SV_POSITION;
				float3 localPosition : TEXCOORD0;
			};

			struct VertexData {
				float4 position : POSITION;
				float2 uv : TEXCOORD0;
			};

			Interpolators MyVertexProgram (VertexData v) {
				Interpolators i;
				i.localPosition = v.position.xyz;
				i.position = UnityObjectToClipPos(v.position);
				return i;
			}

 

 

 uv좌표를 프래그먼트 프로그램에 바로 전달하여 로컬 위치를 대체해 보겠습니다. r값에는 u를, g값에는 v를 넣고, b와 a값은 항상 1로 유지하도록 하겠습니다.

 

			struct Interpolators {
				float4 position : SV_POSITION;
				float2 uv : TEXCOORD0;
			};

			struct VertexData {
				float4 position : POSITION;
				float2 uv : TEXCOORD0;
			};

			Interpolators MyVertexProgram (VertexData v) {
				Interpolators i;
				i.uv = v.uv;
				i.position = UnityObjectToClipPos(v.position);
				return i;
			}

			float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
				return float4(i.uv, 1, 1) * _Tint;
			}

 

이제 컴파일된 버텍스 프로그램이 버텍스 데이터에서 보간기 출력으로 UV를 복사하는 것을 볼 수 있습니다.

 

in  vec4 in_POSITION0;
in  vec2 in_TEXCOORD0;
out vec2 vs_TEXCOORD0;
vec4 t0;
void main()
{
    t0 = in_POSITION0.yyyy * glstate_matrix_mvp[1];
    t0 = glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;
    t0 = glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;
    gl_Position = glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;
    vs_TEXCOORD0.xy = in_TEXCOORD0.xy;
    return;
}
Bind "vertex" Vertex
Bind "texcoord" TexCoord0
ConstBuffer "UnityPerDraw" 352
Matrix 0 [glstate_matrix_mvp]
BindCB  "UnityPerDraw" 0
      vs_4_0
      dcl_constantbuffer cb0[4], immediateIndexed
      dcl_input v0.xyzw
      dcl_input v1.xy
      dcl_output_siv o0.xyzw, position
      dcl_output o1.xy
      dcl_temps 1
   0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw
   1: mad r0.xyzw, cb0[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb0[2].xyzw, v0.zzzz, r0.xyzw
   3: mad o0.xyzw, cb0[3].xyzw, v0.wwww, r0.xyzw
   4: mov o1.xy, v1.xyxx
   5: ret

 

유니티는 UV좌표를 구 주위로 감쌉니다. 이미지의 왼쪽과 오른쪽이 결합되는 북극에서 남쪽 극으로 이어지는 이음새가 보이는 것을 확인할 수 있습니다. 이 이음새를 따라 0과 1의 U좌표 값을 갖게 됩니다. 

 

 

4.2. 텍스쳐 추가하기

텍스쳐를 추가하려면 이미지 파일을 가져와야 합니다. 다음은 테스트 목적으로 사용할 파일입니다. 아래 이미지를 프로젝트에 추가해서 사용하겠습니다.

 

 

 텍스처를 사용하려면 셰이더 프로퍼티를 추가해야 합니다. 일반 텍스처 프로퍼티의 유형은 2D이며, 다른 유형의 텍스처도 있습니다. 기본값은  "white", "black", "gray" 중 Unity의 기본 텍스처 중 하나를 참조하는 문자열입니다.

 

 메인 텍스처의 이름은 _MainTex로 지정하는 것이 관례이므로 이를 사용하겠습니다. 이렇게 하면 필요한 경우 스크립트를 통해 편리한 Material.mainTexture 프로퍼티를 사용하여 액세스할 수도 있습니다.

 

	Properties {
		_Tint ("Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Texture", 2D) = "white" {}
	}

 

더보기

중괄호는 어디에 사용하나요?
이전에는 고정 함수 셰이더에 대한 텍스처 설정이 있었지만 더 이상 사용되지 않습니다. 이러한 설정은 괄호 안에 넣었습니다.

지금은 쓸모가 없어졌지만 셰이더 컴파일러는 여전히 이 설정을 필요로 하므로 이를 생략하면 오류가 발생할 수 있습니다. 특히 {}가 없는 텍스처 파라미터 뒤에 텍스처가 아닌 파라미터를 넣으면 오류가 발생합니다. 향후 Unity 버전에서는 생략하는 것이 안전할 수 있습니다.

 

이제 드래그하거나 'Select' 버튼을 통해 머티리얼에 텍스처를 할당할 수 있습니다. sampler2D 타입의 변수를 사용하여 셰이더의 텍스처에 액세스할 수 있습니다.

 

			float4 _Tint;
			sampler2D _MainTex;

 

UV 좌표로 텍스처를 샘플링하는 작업은 프래그먼트 프로그램에서 tex2D 함수를 사용하여 수행합니다.

 

uniform  sampler2D _MainTex;
in  vec2 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0 = texture(_MainTex, vs_TEXCOORD0.xy);
    return;
}
SetTexture 0 [_MainTex] 2D 0
      ps_4_0
      dcl_sampler s0, mode_default
      dcl_resource_texture2d (float,float,float,float) t0
      dcl_input_ps linear v0.xy
      dcl_output o0.xyzw
   0: sample o0.xyzw, v0.xyxx, t0.xyzw, s0
   1: ret

 

 이제 각 프래그먼트에 대해 텍스처가 샘플링되었으므로 구체에 투영되어 나타납니다. 예상대로 구를 감싸고 있지만 극 근처에서는 상당히 불안정한 것처럼 보입니다. 왜 그럴까요?

 

 Unity의 구체에는 UV 좌표가 가장 많이 왜곡되는 극 근처에 적은 수의 삼각형이 있습니다. UV 좌표는 정점에서 정점으로는 비선형적으로 변경되지만, 정점 사이에서는 선형적으로 변경됩니다. 그 결과 텍스처의 직선이 트라이앵글 경계에서 갑자기 방향이 바뀝니다.

 

  메시마다 UV 좌표가 다르기 때문에 매핑이 달라집니다. Unity의 기본 구체는 경도-위도 텍스처 매핑을 사용하는 반면, 메시에는 저해상도의 큐브 구체가 사용됩니다. 테스트용으로는 충분하지만 더 나은 결과를 얻으려면 커스텀 구체 메시를 사용하는 것이 좋습니다.

 

(작성자 추가: 위에서 설명한 큐브 구체는 아래 방식으로 생성된 구체를 말하는듯 합니다.)

 

[메시 기초] 큐브 구체 (Catlikecoding)

해당 글은 CatlikeCoding의 튜토리얼을 실습하며 번역해놓은 글입니다. 번역 과정에서 제가 내용을 덧붙이거나 삭제한 부분이 존재합니다. 원본의 링크는 글 하단에 있습니다. 원본 글에 적혀있지

doobudubu.tistory.com

 

마지막으로 색조(_Tint)를 적용하 구의 텍스처 모양을 조정할 수 있습니다.

 

				return tex2D(_MainTex, i.uv) * _Tint;

 

4.3. 타일링과 오프셋

 셰이더에 텍스처 프로퍼티를 추가한 후 메티리얼 인스펙터는 텍스처 필드만 추가한 것이 아닙니다. 타일링 및 오프셋 컨트롤도 추가되었습니다. 그러나 현재 새로 추가된 타일링과 오프셋 2D 벡터를 변경해도 아무런 변화가 없습니다. 이 추가 텍스처 데이터는 메티리얼에 저장되며 셰이더에서도 액세스할 수 있습니다. 연결된 머티리얼과 이름이 같고 _ST 접미사가 붙은 변수를 통해 액세스합니다. 이 변수의 유형은 float4여야 합니다.

 

 타일링 벡터는 텍스처의 크기를 조정하는 데 사용되므로 기본적으로 (1, 1)입니다. 변수의 XY 부분에 저장됩니다. 이를 사용하려면 UV 좌표에 곱하기만 하면 됩니다. 이 작업은 버텍스 셰이더 또는 프래그먼트 셰이더에서 수행할 수 있습니다. 이것은 버텍스 셰이더에서 수행하는 것이 효율적입니다. 모든 픽셀이 아닌 각 버텍스에 대해서만 곱셈을 수행하는 것이 연산량이 더 적기 때문입니다.

 

 

			sampler2D _MainTex;
			float4 _MainTex_ST;
			
	...

			Interpolators MyVertexProgram (VertexData v) {
				Interpolators i;
				i.uv = v.uv * _MainTex_ST.xy;
				i.position = UnityObjectToClipPos(v.position);
				return i;
			}

 

오프셋 부분은 텍스처를 이동시키고 변수의 ZW 부분에 저장됩니다. 스케일링 후 UV에 추가됩니다.

 

				i.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;

 

 UnityCG.cginc에는 이 상용구를 간소화하는 편리한 매크로가 포함되어 있습니다.

 

				i.uv = TRANSFORM_TEX(v.uv, _MainTex);

 

정의되어 있는 매크로는 다음과 같습니다.

#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

 

5. 텍스처 셋팅

지금까지 기본 텍스처 임포 설정을 사용했습니다. 몇 가지 옵션을 살펴보고 각 옵션이 어떤 기능을 하는지 알아보겠습니다.

 

WrapMode는 0-1 범위를 벗어난 UV좌표로 샘플링할 때 어떻게 처리할지 지정합니다. WrapMode를 clamped으로 설정하면 UV가 0-1범위 안에 머물도록 제한됩니다. 즉, 엣지 너머의 픽셀은 엣지에 있는 픽셀과 동일합니다. WrapMode를 repeat으로 설정하면 UV가 반복됩니다. 즉 엣지 너머의 픽셀은, 텍스처의 반대편에 있는 픽셀과 동일합니다. 기본 모드는 repeat로 텍스처를 타일링하는 것입니다.

 

타일링 텍스처가 없는 경우 대신 UV 좌표를 클램프하는 것이 좋습니다. 이렇게 하면 텍스처가 반복되는 것을 방지하는 대신 텍스처 경계가 복제되어 텍스처가 늘어져 보이게 됩니다.

 

clamped모드에서 (2,2)범위의 uv를 갖는 텍스처 렌더링

 

 텍스처가 0-1범위 안에 있더라도 WrapMode가 중요할 수 있습니다. bilinear 또는 trilinear 필터링을 사용하는 경우 텍스처를 샘플링하는 동안 인접 픽셀이 보간됩니다. 이는 텍스처 안쪽에 있는 픽셀에는 괜찮습니다. 하지만 엣지에 있는 픽셀의 인접 픽셀은 어떻게 될까요? 답은 WrapMode에 따라 다릅니다.

 

 클램핑되면 엣지에 있는 픽셀이 자체적으로 블랜딩됩니다. 이렇게 하면 픽셀이 블랜딩되지 않는 작은 영역이 생성되어 눈에 띄지 않습니다. 반복되면 엣지의 픽셀이 텍스처의 반대쪽과 블랜딩됩니다. 양쪽이 비슷하지 않으면 엣지를 통해 반대쪽이 약간 번지는 것을 알 수 있습니다.

 

5.1. 밉맵과 필터링

 텍스처의 픽셀이 투사되는 픽셀과 정확히 일치하지 않으면 어떻게 될까요? 불일치가 발생하며, 이 불일치를 어떻게든 해결해야 합니다. 이를 해결하는 방법은 Filter Mode를 통해 제어됩니다.

 가장 간단한 필터링 모드는 Point(No Filter)입니다. 즉, 텍스처가 특정 UV 좌표에서 샘플링될 때 가장 가까운 텍셀이 사용됩니다. 이렇게 하면 픽셀이 디스플레이 픽셀에 정확히 매핑되지 않는 한 텍스처가 블록처럼 보입니다. 따라서 일반적으로 픽셀을 완벽하게 렌더링하거나 블록 같은 스타일을 원할 때 사용됩니다.

 기본값은 bilinear 필터링을 사용합니다. 텍스처가 두 픽셀 사이의 어딘가에서 샘플링되면 해당 두 픽셀이 보간됩니다. 텍스처는 2D이므로 이 작업은 U축과 V축 모두에서 발생합니다. 따라서 단순한 선형 필터링이 아닌 이중 필터링이 필요합니다.

 이 접근 방식은 텍스처 밀도가 디스플레이 픽셀 밀도보다 낮을 때, 즉 텍스처를 확대할 때 작동합니다. 결과는 흐릿하게 보입니다. 그 반대의 경우, 즉 텍스처를 축소할 때는 작동하지 않습니다. 인접한 디스플레이 픽셀은 결국 한 개 이상의 텍셀 간격이 있는 샘플이 됩니다. 즉, 텍스처의 일부를 건너뛰게 되어 마치 이미지가 선명하게 처리된 것처럼 거칠게 전환이 발생합니다.

 이 문제에 대한 해결책은 텍셀 밀도가 너무 높아질 때마다 더 작은 텍스처를 사용하는 것입니다. 디스플레이에 텍스처가 작게 표시될수록 더 작은 버전을 사용해야 합니다. 이러한 작은 버전의 텍스처를 밉맵이라고 하며, 이는 자동으로 생성될 수 있습니다. 연속되는 각 밉맵은 이전 레벨의 너비와 높이의 절반을 갖습니다. 따라서 원본 텍스처 크기가 512x512인 경우 밉맵은 256x256, 128x128, 64x64, 32x32, 16x16, 8x8, 4x4 및 2x2입니다.

 

 원하는 경우 밉맵을 비활성화할 수 있습니다. 먼저 텍스처 설정 인스펙터에서 Advanced 드랍다운을 활성화합니다. 그런 다음 'Generate Mip Map' 을 비활성화하고 변경 사항을 적용할 수 있습니다. 차이를 확인하는 좋은 방법은 쿼드와 같은 평평한 물체를 사용하여 비스듬한 각도에서 보는 것입니다.
 
밉맵의 유무 차이

 

 밉맵 레벨이 적용되는것을 시각적으로 확인하기 좋은 방법 중 하나는 Fadeout Mip Maps를 사용하는 것입니다. 밉맵 레벨이 어떻게 사용되고, 적용시 얼마나 다르게 보일까요? Advanced 텍스처 설정에서 Fadeout Mip Map을 활성화하여 전환을 확인할 수 있습니다. 활성화하면 인스펙터에 페이드 범위 슬라이더가 표시됩니다. 이 슬라이더는 밉맵이 단색 회색으로 전환되는 밉맵 범위를 정의합니다. 슬라이더의 크기를 한 스탭으로 설정하면 회색으로 변환시 블랜딩 없이 선명하게 전환됩니다. 스탭의 크기를 키울수록 전환이 부드럽게 변경됩니다.

 

 
(작성자 추가: 위 설명에서 '스탭'은 Fade Range 슬라이더의 크기를 의미합니다. 슬라이더의 화살표 부분을 클릭하여 슬라이더의 값이 아닌 크기를 변경할 수 있습니다.)

이 효과를 잘 보려면 일단 텍스처의 Aniso Level을 0으로 설정하세요.

 

 

 다양한 밉맵 레벨의 위치를 파악했다면 그 사이의 텍스처 품질이 급격하게 변화하는 것을 볼 수 있을 것입니다. 투영된 텍스처가 작아질수록 텍셀 밀도가 증가하여 더 샤프하게 보입니다. 그러다가 갑자기 다음 밉맵 레벨이 시작되면 다시 흐릿해집니다.

 따라서 밉맵이 없으면 흐릿했다가 선명해지고, 그러다 너무 선명하게 변합니다. 밉맵을 사용하면 흐릿했다가 선명해졌다가 갑자기 다시 흐릿해지고, 선명해졌다가 갑자기 다시 흐릿해지는 식으로 반복됩니다.

 이러한 흐릿하고 선명한 밴드는 bilinear 필터링의 특징입니다. 필터 모드를 Trilinear 필터로 전환하면 이러한 현상을 없앨 수 있습니다. 이 방법은 이선형(bilinear) 필터링과 동일하게 작동하지만 인접한 밉맵 레벨 사이를 보간합니다. 따라서 삼선형(Trilinear)입니다. 이렇게 하면 샘플링 비용이 더 많이 들지만 밉맵 레벨 간의 전환이 부드러워집니다.

 또 다른 유용한 기법은 비등방성필터링(anisotropic filtering)입니다. Aniso Level을 0으로 설정했을 때 텍스처가 더 흐릿해진 것을 보셨을 것입니다. 이는 밉맵 레벨의 선택과 관련이 있습니다.

 텍스처를 비스듬히 투영할 때 원근감으로 인해 텍스처 한 축의 치수 중 하나가 다른 축의 치수보다 훨씬 더 왜곡되는 경우가 종종 있습니다. 텍스처가 있는 plane 평을 좋은 예로 들 수 있습니다. 멀리서 보면 텍스처의 앞뒤 치수가 왼쪽에서 오른쪽 치수보다 훨씬 작게 나타납니다.

 어떤 밉맵 레벨이 선택되는지는 최악의 축을 기준으로 합니다. 두 축 간의 차이가 크면 한 차원이 매우 흐릿한 결과를 얻을 수 있습니다. 비등방성 필터링은 차원을 분리하여 이 문제를 완화합니다. 텍스처를 균일하게 축소하는 것 외에도 두 차원 모두에서 서로 다른 비율로 축소된 버전도 제공합니다. 따라서 256x256용 밉맵뿐만 아니라 256x128, 256x64용 밉맵도 만들 수 있습니다.

 

비등성 필터링 유무 차이


 이러한 추가 밉맵은 일반 밉맵처럼 미리 생성되지 않습니다. 대신 추가 텍스처 샘플을 수행하여 시뮬레이션합니다. 따라서 더 많은 공간이 필요하지 않지만 샘플링 비용이 더 많이 듭니다.

 


비등방성 필터링이 얼마나 적용될 지는 Aniso Level로 제어됩니다. 1이면 활성화되어 최소한의 효과를 제공합니다. 16에서는 최대 효과를 제공합니다. 그리고 이러한 설정은 프로젝트의 quality setting의 영향을 받습니다.

 

 퀄리티 세팅에 접근하는 방법은 윈도우에서 Edit / Project Settings / Quality 메뉴를 통해서 입니다. 그러면 Anisotropic Textures 옵션을 찾을 수 있습니다.

 

 

비등방성 텍스처가 비활성화되면 텍스처의 설정에 관계없이 비등방성 필터링이 수행되지 않습니다. 만약 Per Texture로 설정되면 각 개별 텍스처에 의해 완전히 제어됩니다. 또한 Forced On으로 설정하면 각 텍스처의 Aniso Level이 최소 9이상의 값으로 설정된 것처럼 작동합니다. 그러나 Aniso Level이 0으로 설정된 텍스처는 여전히 비등방성 필터링을 사용하지 않습니다.

 

 


 

https://catlikecoding.com/unity/tutorials/rendering/part-2/

 

Rendering 2

A Unity Rendering tutorial about shader fundamentals. Part 2 of 20.

catlikecoding.com

 

 

반응형