참치김밥은 최고의 한식이다

쉐이더 첫 공부할 때 쓴 문서.. 본문

Unity

쉐이더 첫 공부할 때 쓴 문서..

l__j__h 2024. 2. 20. 16:53

버텍스 셰이더 (Vertex Shader)

v2f vert(appdata v){
	v2f o;
	UNITY_SETUP_INSTANCE_ID(v);
	UNITY_TRANSFER_INSTANCE_ID(v, o);
	//위 두 줄은 GPU 인스터싱 할거면 추가

	o.vertex = TransformObjectToHClip(v.vertex.xyz);
	o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
	o.normal = TransformObjectToWorldNormal(v.normal);
	o.fogCoord = ComputeFogFactor(o.vertex.z);
	o.worldPos = TransformObjectToWorld(v.vertex.xyz);
	o.shadowCoord = TransformWorldToShadoeCoord(o.worldPos);

	return o;
}

① o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); → texcoord는 일반적으로 uv라 부르는 텍스처 샘플링 좌표입니다. TRANSFORM_TEX 함수로 _MainTex_ST와 연결시켜줍니다. ST는 Scale Translate의 약자입니다.

② o.normal = TransformObjectToWorldNormal(v.normal); → 오브젝트 노멀을 월드 노멀로 변환해줍니다. 라이팅연산을 안 할 거면 해당 코드는 없어도 됩니다. (즉, 라이팅 연산을 할 거라면 오브젝트 노멀을 월드 노멀로 변환해서 갖고 있어야 함)

③ o.fogCoord = ComputeFogFactor(o.vertex.z); → 클립포지션의 z값을 기반으로 어느정도 포그가 적용되어야 할지에 대한 값을 구합니다.

④ o.worldPos = TransformObjectToWorld(v.vertex.xyz); → 월드에서 계산할 일이 있으면(마우스 Input을 C#코드로부터 받아야한다든가) 월드 좌표를 이렇게 받아올 수 있습니다.

⑤ o.shadowCoord = TransformWorldToShadoeCoord(o.worldPos); → 그림자맵의 좌표를 받아옵니다. 

 

 

프래그먼트 셰이더 (Fragment Shader)

half4 frag(v2f i) : SV_Target
{
	UNITY_SETUP_INSTANCE_ID(i);

	Light mainLight = GetMainLight(i.shadowCoord);
	i.normal = normalize(i.normal);

	float3 lambert = LightingLambert(mainLight.color, mainLight.direction, i.normal);
	float4 c = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.texcoord) * _Color;

	float3 specular = LightingSpecular(mainLight.color, mainLight.direction, i.normal, i.viewDir, 1, _Gloss);

	half3 ambient = SampleSH(i.normal);

	c.rgb *= lambert * mainLight.distanceAttenuation * mainLight.shadowAttenuation;
	c.rgb += ambient + specular;

	#ifdef _ADDITIONAL_LIGHTS
		uint pixelLightCount = GetAdditionalLightsCount();
		for (uint lightIndex = 0; lightIndex < pixelLightCount; ++lightIndex)
			{
				Light addLight = GetAdditionalLight(lightIndex, i.worldPos);
				float3 addLightResult = LightingLambert(addLight.color, addLight.direction, i.normal) * addLight.distanceAttenuation;
				float3 addLightSpecular = LightingSpecular(addLight.color, addLight.direction, i.normal, i.viewDir, 1, _Gloss);
				c.rgb += addLightResult + addLightSpecular;
			}
	#endif

	c.rgb = MixFog(c.rgb, i.fogCoord);

	return c;
}

순서 :

  1. 섀도우를 고려한 mainLight를 얻어온다.
  2. 라이팅 연산을 위해, i.normal을 normalize 해준다.
  3. 램버트값, 스페큘러값, 앰비언트값, 컬러값을 구한다.
    • LightingLambert 함수로 float3 lambert 값을 계산한다.
    • SAMPLE_TEXTURE2D 함수로 텍스처와 샘플러, UV, 그리고 _Color를 통해 해당 픽셀의 색상값 c를 계산한다.
    • LightingSpecular 함수로 float3 specular 값을 계산한다.
    • SampleSH 함수로 half3 ambient 값을 계산한다.

MyLit.shader

1 : 쉐이더의 프로퍼티. 즉 머터리얼에 드러날 프로퍼티. SRP Instancing을 위해서 머터리얼 프로퍼티들을 모두 아래와 같이 CBUFFER_START(UnityPerMaterial) ~ CBUFFER_END 블록으로 감싸주어야 한다. (UnityPerDraw도 있는데, 자세한 건 후술)

2 : 서브쉐이더(Tags + Pass들). 하나의 서브쉐이더는 여러 개의 Pass를 가질 수 있으며, 각 Pass마다 담당하는 역할이 다르다. HLSL 쉐이더의 경우, 작성할 코드를 HLSLPROGRAM ~ ENDHLSL로 감싸주어야 한다. #pragma vertex Vertex와 #pragma fragment Fragment는 내가 사용할 vertex 함수와 fragment 함수의 이름이 각각 “Vertex”와 “Fragment”다 라고 정의해준다. #include “MyLitForwadPass.hlsl”는 말 그대로, MyLitForwardPass.hlsl 파일을 임포트해오라는 말이다. 나는 vertex함수와 fragment함수를 포함한 실제 기능을 담당하는 코드들을 MyLitForwardPass.hlsl 파일에 분리해두었다. 쉐이더 파일에서는 이렇게 hlsl파일을 불러오기만 해도 해당 hlsl파일의 기능이 적용된다(?).


MyLitForwardPass.hlsl (일부)

3 : Vertex Function. 래스터라이징 단계(클립 공간 계산) 전 이루어지는, 오브젝트 공간 속에서 각 정점에 대해 처리되는 함수이다. 각 정점에 대해 독립적으로 실행되므로, 각 정점은 다른 정점의 데이터를 얻어올 수 없다.

4 : Fragment Function. 래스터라이징 단계 후 이루어지는, 각 픽셀에 대해 처리되는 함수이다. 컬러값(float4)을 리턴하여 각 픽셀의 색상을 결정짓는 단계이다. 만일, 정점의 월드 좌표가 필요하다면, Vertex Function으로부터 Object 좌표를 World 좌표로 변환한 후, Input으로 받아와야 한다(여기선 Interpolators 라는 이름의 구조체를 정의해두었다.)


MyLitForwardPass.hlsl (전체)

1 : 구조체. Attributes는 Vertex함수의 Input으로, Interpolators는 Vertex함수의 결과물이자 Fragment함수의 Input으로 들어갈 구조체이다.

1-1 ) Attributes - position 해당 정점의 오브젝트 공간 상의 위치 1-2 ) Attributes - normalOS 해당 정점의 오브젝트 공간 상의 노말 벡터

2-1 ) Interpolators - positionCS Vertex함수로부터 받아온, 각 정점의 클립 공간 상의 위치 2-2 ) Interpolators - positionWS Vertex함수로부터 받아온, 각 정점의 월드 공간 상의 위치 2-3 ) Interpolators - normalWS Vertex함수로부터 받아온, 각 정점의 월드 공간 상의 노말 벡터

2 : MyLit.shader에서 사용할 프로퍼티를 SRP Batching 고려 X / 고려 O 일 때로 정의해본 것.

2-1 ) SRP Batching 고려 X 그냥 MyLit.shader에서 받아올 프로퍼티들을 쭉 나열해주었따. 2-2 ) SRP Batching 고려 O 사용할 머터리얼 프로퍼티들을 CBUFFER_START(UnityPerMaterial) ~ CBUFFER_END로 감싸주었다. 이렇게 하면 (유니티 에디터 상에서) 쉐이더의 Inspector창을 보면, 아래와 같이 SRP batching이 Compatible(잘 되고 있음)으로 바뀐다.

이때, SRP batching은 해당 쉐이더에서 사용할 프로퍼티들을 모두 GPU에 올려놓기 때문에, 해당 쉐이더를 많은 곳에서 사용하더라도 Batching수가 늘어나지 않는다고 하는데 자세히는 공부가 필요하다.

+) UnityPerMaterial외에, UnityPerDraw라는 이름의 CBUFFER도 있다. 이 둘의 역할은 각각 뭘까?? 는 아래 캡처에 있다~!

 

즉 쉐이더에서 선언한 프로퍼티는 일단 무조건 UnityPerMaterial에 다 몰아넣어야 SRP Batching이 작동한다고 보면 될 것 같다.

 

3 : Vertex 함수 (= 버텍스 셰이더) 버텍스 셰이더는 버텍스의 마지막 클립 공간의 위치를 출력해 GPU에게 화면의 어느 부분을, 어느 뎁스로 래스터화할지 알려준다. 이 Output은 SV_POSITION 시맨틱을 가진 float4 타입이어야 한다. (여기에서는 Interpolators 구조체의 float4 positionCS : SV_POSITION 이 된다.)

4 : 내가 정의한 함수들

5 : Fragment 함수 (옆에 쓴 SV_Target은, “시맨틱”이라고 불리며, 반환 타입을 명시해 준 것이다. SV_Target은 fixed4 타입을 반환한다는 뜻인 것 같다..??) Fragment가 받는 Input(여기선 Interpolators 구조체)의 변수 개수에는 제한이 있다. 이 제한은 플랫폼 및 GPU에 따라 달라지며, 일반적인 가이드라인은 아래와 같다.

728x90