본문 바로가기
유니티/Shader Study

URP Shader Study #2 (pragma, Rendering Pipeline, hlsl, 코드리뷰)

by witn331ss 2023. 11. 29.

구글에 unity shader outline glow 라는 식으로 쳐보면 R&D하거나 공부하는 자료들을 많이 찾아볼 수 있다.

레거시 코드에서는 CG를 많이 사용했는데 요즘은 hlsl을 많이 사용하게 된다.

 

 

pragma

 

컴파일러 입력을 처리하는 방법을 지정하는 문법 체계이다.

C언어에서 pragma는 컴파일러가 입력을 처리하는 방법을 지정하는 언어 문법이며

이는 셰이더가 어떻게 컴파일 될지를 지시하는 전처리 문법이다.

 

우리가 작성하는 쉐이더로 PC나 Mobile등 여러가지 플랫폼으로 사용하기 위해

컴파일을 해주는 지지자임

 

분기를 만들거나 다른 선언자가 들어가는 둥 여러가지 사용을 할 때

프라그마 지지자를 통해 사용하게 된다.

 

 

##

 

지금 URP에서는 필요한 것들을 사용자가 직접 인클루트 시켜야한다.

//cg shader는 .cginc를 hlsl shader는 .hlsl을 include하게 됩니다.
       	#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

 

나중에 수준이 높아지면 인클루트 파일들을 뜯어보면서 공부하면 도움이 많이된다.

 

파일은 여기에 있다!

 

유니티 쉐이더가 어떻게 짜여있는지 볼 수 있다.

 

 

Q 실무에서 String 으로 다 적나요? 아니면 상수로 다 적나요?

A 엥 String 타입으로 왜적음?패스같은 경우는 내부 URP에 선언된걸 쓰게되고 커스텀을 사용하게 되면 그냥 거기에 맞춰 사용하면 됨

 

 

Rendering Pipeline

다이렉트X 9 렌더 파이프라인

 

우리가 보여지는 쉐이더라고 하는 것은 다이렉트X 9 의 렌더 파이프라인을 기본으로 하여 그려지게 된다.

 

먼저 메모리에서 버텍스/인덱스 버퍼를 읽어온다.

그 다음 버텍스 쉐이더 단계에서 3D 공간에 그리게 된다.

레스터라이제이션으로 모니터 화면, 그러니까 픽셀으로 배열을 한다.

그 다음 픽셀에 무슨색이 들어갈지 연산하는 픽셀 쉐이딩 작업에 들어간다.

 

일단 다이렉트X 9의 렌더 파이프라인을 살펴보자.

 

Input Assembly GPU는 메모리에서 버텍스 및 인덱스 버퍼를 읽고, 버텍스가 삼각형을 형성하는 방식을 결정한다. 나머지는 파이프라인에 전달
Vertex Shading 한번에 하나의 버텍스에서 실행되는 Mesh의 모든 버텍스에 대해 한번 실행한다. 
여기서 버텍스를 변환하고 위치를 받은 다음
카메라 및 뷰포트 설정을 사용해 화면에서 최종 위치를 계산한다
Rasterization 버텍스 쉐이더가 삼각형의 각 버텍스에 실행되고 GPU가 화면에 표시될 위치를 알고 있으면
삼각형이 레스터화 되어 개별 픽셀의 모음으로 변환된다.

버텍스별 정보(UV좌표, 버텍스 컬러, 노말 등)은 삼각형 픽셀을 통해서 보간된다
(레스터화 된 픽셀은 각 버텍스의 보간된 값을 가지게 된다.)
Pixel Shading 레스터화된 각 픽셀은 픽셀쉐이더를 통해
(기술적으로는 아직 픽셀이 아닌 프래그먼트 쉐이더라고 한다)

Material 속성, Texture, 광원 등 기타 매개변수를 프로그래밍 된 방식으로 결합하여
특정한 색을 얻음으로써 픽셀에 색상을 부여한다.
Render Target Output 마지막으로 픽셀은 렌더링 대상에 쓰여지지만 일부 테스트를 거쳐야만 유효하게 된다.
예를 들자면 깊이 테스트는 이미 렌더링 대상보다 멀리 떨어진 픽셀을 생략할 수 있다.

이러한 모든 테스트(Depth, alpha, stencil 등)를 통화하면 메모리의 렌더링 타켓에 쓰여진다.

Z sort 방식 : 렌더링 하는 물체를 카메라 시점으로부터 거리로 소팅(정렬)해 두고

시점으로부터 먼 것 부터 순서대로 렌더링 하는 방식이다.

 

뒤에 있는 것을 앞에 있는 물체가 덮어 씌워간다. (Mesh가 겹치면 소팅이슈가 발생한다)

 

Z Buffer 방식 : 물체를 렌더링하면서 그 물제까지의 거리를 픽셀(텍셀) 단위로 Depth Buffer에 저장해두고 다음에 렌더링 할 때

이미 렌더링 되어 있는 물체의 거리와 렌더링 하려는 물제의 거리를 비교하면서 앞에 있다면 렌더링한다.

(반투명인 경우는 렌더링이 어렵다)

 

//vertex buffer에서 읽어올 정보를 선언합니다. 	
         	struct VertexInput
         	{
            	float4 vertex : POSITION;
          	};

//보간기를 통해 버텍스 셰이더에서 픽셀 셰이더로 전달할 정보를 선언합니다.
        	struct VertexOutput
          	{
           	float4 vertex  	: SV_POSITION;
      	};

//버텍스 셰이더
      	VertexOutput vert(VertexInput v)
        	{

          	VertexOutput o;      
          	o.vertex = TransformObjectToHClip(v.vertex.xyz);

         	return o;
        	}

//픽셀 셰이더
        	half4 frag(VertexOutput i) : SV_Target
        	{ 
                 	
          	return half4(0.5 , 0.5, 0.5, 1);  
       	
        	}

        	ENDHLSL  
    	}
     }
}

 

hlsl은 이 네가지 구조를 이해하면 되는데

 

먼저 3D매쉬에서 읽어올 정보를 선언한다

픽셀쉐이더로 보내기 이전에 보간기를 통해서 넘어가게 된다

그 다음 버텍스쉐이더 계산 픽셀쉐이더 계산하는 식이다.

 

보간기란? : 계산된 정보가 데이터를 넘기는데 그녀석을 태워보낼 배를 보낸다고 생각하면 된다.

 

 

Application to Vertex Shader Structure(Attributes) 1 :: 읽어오기

struct VertexInput
         	{
            	float4 vertex : POSITION;
          	};

 

Type(s) Name(변경가능) Semantic Notes
float4 vertex POSITION 로컬공간의 정점위치
float3 normal NORMAL 버텍스의 노말
float4 texcoord[n] TEXCOORD[N] 버텍스의 UV좌표
float4 tangent TANGENT Mesh에서 계산된, 또는 import 된 탄젠트값
float4 color COLOR 버텍스의 컬러값

 

버텍스 버퍼에서 읽어오는 것은 정해져있다.

시맨틱은 문법이라 지켜줘야한다

 

저 메모리들은 그래픽카드에 붙어있는 메모리라고 생각해도 될까?

비슷하게 생각해도 될듯하다 GPU상에 올라가있는 메모리일 수도 있고 공용메모리일 수도 있고..

 

Input Assembly 단계에서 버텍스 버퍼에서 필요한 정보를 가져오는 역할을 한다.

 

DCC에서 매쉬가 4개라고? 이건 유저들 사용하기 편하라고 그런거고

삼각형은 어딜봐도 면이니까 이걸로 변환해서 사용하게 된다.

 

 

 Vertex Shader to Fragment Shader Structure (Varying) :: 2 보간기

 

 

보간기 또한 문법에 따라 사용해줘야 한다.

 

다만 차이가 있다면 내가 원하는 것을 임의로만들어 버텍스쉐이더에서 계산한 다음에

보간기를 통해 픽셀쉐이더로 넘기는 것이 가능은 하다.

 

그래픽스에서 말하는 폴리곤은 보통 삼각형이다.

dcc랑 엔진에서 폴리카운트가 다른 이유임

 

*최적화이야기*

보간기의 숫자는 쉐이더모델의 영향을 받는다.

#pragma target 설정을 참고.

 

모바일에서 낮은 사양을 다루게 될 때 보간기의 갯수는 중요하다.

넘겨저야할 정보가 많을 때 쉐이더가 안나온다 그림자가 안나온다 등의 문제가 생긴다

이건 갯수문제임

 

Vertex Function(Vertex shader stage) :: 3 버텍스 쉐이더

앞에 보여준 버텍스 버퍼에서 읽어온 정보를 내부에 선언하게 되고보간기에서 어떻게 움직일지 선언하게 된다.

 

공간 변환(transform)

유니티는 개쩌는게 행렬변환을 안해도 쉐이더를 다룰 수 있게 만들어준다

근디 수준 올라가면 결국 행변환을 공부해야한다...

 

로컬 > 월드 > 카메라 > 클립 공간

float4x4 GetObjectToWorldMatrix() //오브젝트(로컬)을 월드공간으로
{
    return UNITY_MATRIX_M;
}

...

float3 TransformObjectToWorld(float3 positionOS) // 오리지날 포지션이랑 백터3 가져오면 월드로 바꿔준다는 말
{
    return mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)).xyz;   
}

##

float4x4 GetWorldToObjectMatrix()
{
    return UNITY_MATRIX_I_M;
}

 Core/ShaderLibrary/SpaceTransforms.hlsl 을 살펴보면 확인할 수 있다.

 

// Transforms position from object space to homogenous space
float4 TransformObjectToHClip(float3 positionOS)
{

// More efficient than computing M*VP matrix product
return mul(GetWorldToHClipMatrix(), mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)));

}

이것들을 간단하게 치는 방법은 이걸 쓰면된다.

 

 

설명하자면 보간기에서 버텍스 포지션을 쓰고

계산된 포지션값을 버텍스라는 이름으로 넘길거고 그 이름은 SV_POSITION 이다.

 

그다음 버텍스쉐이더에서 보간기에 넘길 이름은 선언해주고 (o vertex)

로컬로 가지고 있는 점의 위치를 'TransformObjectToHCilp' 이라는 함수를 통해서

 

버텍스 버퍼에서 읽어온 버텍스의 위치값을 .xyz으로 읽어서 계산을 한다

그러면 o.vertex에 담긴 것 이다.

그 포지션 값을 픽셀 쉐이더로 넘기는데 계산을 안한다. 왜?

포지션 값은 픽셀쉐이더에서 계산할게 없다!

 

보간기에서 계산한걸 i를 입력해서 읽는다.

픽셀 쉐이더 있는 타겟설정도 몇가지 있는데 여기서는 그것까지 다루진 않겠다.

쉐이더 기본 과정에서는 이걸 건들 일이 없음

 

*여기까지 짧막한 쉐이더 코드의 리뷰이다.*