유니티3D/셰이더

유니티로 게임 제작 기법 연습01 - 유니티 라이팅 셰이더 바닥부터 구현하기

codehunter 2024. 11. 11. 23:04

유니티 6가 나온것을 기점으로 다시 리마인드를 위해 다른사람들의 강좌를 따라하면서 기초 기법등을 다시 연습해볼 생각이다.

 

필요 프로그램 유니티 6 (60000.0.30f1)

참조 강좌 (레트로, 유니티 공식 블로그) - 블로그들은 URP에 대하여, 레트로님의 강좌는 SRP에 대하여 설명하고 있음

 

아래 비교글은 ChatGPT로 작성

1. SRP (Scriptable Render Pipeline)

  • 정의: SRP는 Unity에서 사용자 정의 가능한 렌더링 파이프라인의 기본 프레임워크입니다. Unity 개발자가 특정 렌더링 요구 사항에 맞는 파이프라인을 스크립트로 직접 설계하고 구현할 수 있도록 설계되었습니다.
  • 특징:
    • 완전한 커스터마이징 가능: 렌더링 순서, 셰이더, 후처리 효과 등을 자유롭게 구성 가능.
    • Unity에서 제공하는 HDRPURP도 SRP를 기반으로 만들어졌음.
    • 고급 사용자와 그래픽 엔지니어를 대상으로 설계됨.
    • 직접 SRP를 활용하려면 C# 스크립팅과 그래픽 API(OpenGL, Vulkan, DirectX 등)에 대한 지식이 필요.

2. URP (Universal Render Pipeline)

  • 정의: URP는 SRP를 기반으로 Unity에서 사전 구성된 렌더링 파이프라인입니다. 다양한 플랫폼에서 효율적으로 작동하도록 설계되었으며, 성능과 품질 간의 균형을 제공합니다.
  • 특징:
    • 범용성: 모바일, PC, 콘솔 등 여러 플랫폼에서 사용할 수 있도록 최적화.
    • 사용 용이성: SRP의 복잡한 구현 없이, Unity 사용자들이 쉽게 사용할 수 있도록 사전 정의된 기능 제공.
    • 성능 최적화: 저사양 기기에서도 효율적으로 작동하도록 경량화된 렌더링.
    • 커스터마이징 가능하지만, SRP만큼 세밀하지는 않음.

SRP와 URP의 차이

항목SRP (Scriptable Render Pipeline)URP (Universal Render Pipeline)

정의 사용자 정의 가능한 렌더링 파이프라인 프레임워크 SRP 기반의 사전 정의된 렌더링 파이프라인
대상 그래픽 엔지니어, 고급 사용자 일반 Unity 사용자
유연성 렌더링 단계부터 효과까지 완전 커스터마이징 가능 제한적이지만 적절한 커스터마이징 제공
난이도 고급 기술 요구 사용이 쉬움
플랫폼 지원 구현 방식에 따라 다름 다양한 플랫폼 지원 (모바일, PC, 콘솔 등)
성능 최적화 사용자의 구현에 따라 달라짐 저사양 기기에서도 안정적인 성능 제공

 

 

https://unity.com/kr/srp/universal-render-pipeline

 

유니버설 렌더 파이프라인(URP) | 단일성

Unity의 URP(유니버설 렌더 파이프라인)는 아름다운 그래픽 렌더링 성능을 제공하며 대상으로 하는 모든 Unity 플랫폼에서 작동합니다. 지금 URP에 대해 자세히 알아보세요.

unity.com

https://unity.com/kr/resources/introduction-to-urp-advanced-creators-unity-6

 

고급 크리에이터를 위한 URP 소개(Unity 6 에디션) | Unity

이 가이드는 숙련된 Unity 개발자와 테크니컬 아티스트가 Unity 6의 유니버설 렌더 파이프라인(URP)을 최대한 효율적으로 개발하는 데 도움이 될 수 있습니다.

unity.com

 

 

아래는 바로 실습용이고 

전체 강좌는 여기이다.

 

https://www.youtube.com/watch?v=Q448UVXT-8M

 

Create Shader > Unlit Shader > HelloShader 이 파일내용을 일단 모두 삭제후 빈 파일 상태에서 새로 작성

 

기본개념 몇개

- 제일 상단에는 Shader의 이름을 작성한다

- 외부에서 셰이더 내부로 값을 전달하려면 머티리얼 프로퍼티(Properties)라는게 필요 

머티리얼 프로퍼티에 쓰여지는 변수의 형식은 아래와 같다.

_변수명 ("인스펙트에 보여질 이름", 타입) = 기본값 이런식으로 구성된다.

Shader "retr0/HelloShader"
{
	// 머티리얼의 프로퍼티 목록이 온다
	// 머티리얼 프로퍼티의 값은 셰이더 내부의 변수로 전달된다.
	Properties
	{
		_BaseColor ("Color", Color) = (1,1,1,1)
	}
}

 

- SubShader가 실제 작동할 몸체가 되겠다. 하나 이상을 배치할수가 있는데 유니티는 위에서 아래로 진행하는데 최초로 성공하는 서브셰이더를 작동시키는 방식으로 동작한다. 이 서브셰이더를 사용하는 대표적인 예로써는 하드웨어 사양별로 동작하는 코드를 배치해두고 작동시키는 것이 있다.

Shader "retr0/HelloShader"
{
	// 머티리얼의 프로퍼티 목록이 온다
	// 머티리얼 프로퍼티의 값은 셰이더 내부의 변수로 전달된다.
	Properties
	{
		_BaseColor ("Color", Color) = (1,1,1,1)
		_Scale ("Scale", Float) = 1
	}

	// 풍부한 그래픽 효과를 사용 --> 고사양 하드웨어
	SubShader
	{

	}

	// 중급 그래픽 효과를 사용 --> 중급 하드웨어
	SubShader
	{

	}
}

 

- Tags라는 블럭은 셰이더에 추가적인 정보를 제공하는 블럭이다. 아래 코드는 유니버설 렌더 파이프라이에서 동작하는 셰이더라는 의미이다. 

Tag에 대한 자세한 설명은 여기 참조

 

 

- PreviewType이라는 태그는 머티리얼을 인스펙터에 어떻게 표시할지를 알린다.

 

Shader "retr0/HelloShader"
{
	// 머티리얼의 프로퍼티 목록이 온다
	// 머티리얼 프로퍼티의 값은 셰이더 내부의 변수로 전달된다.
	Properties
	{
		_BaseColor ("Color", Color) = (1,1,1,1)
		_Scale ("Scale", Float) = 1
	}

	SubShader
	{
		Tags { "RenderPipeline" = "UniversalRenderPipeline" "PreviewType" = "Sphere"}
	}
}

 

- Pass 블럭은 게임오브젝트 하나를 완전히 한번에 그리는것에 대응 하는것임. 그래서 여러개의 Pass 블럭이 있다면 하나의 오브젝트를 여러번 그리는 효과를 내게 됨.

 

- 셰이더 프로그램 추가는 HLSLPROGRAM - ENDHLSL이라는 블럭안에서 작성하게 된다.

자세한건 여기 참조

Shader "retr0/HelloShader"
{
	// 머티리얼의 프로퍼티 목록이 온다
	// 머티리얼 프로퍼티의 값은 셰이더 내부의 변수로 전달된다.
	Properties
	{
		_BaseColor ("Color", Color) = (1,1,1,1)
		_Scale ("Scale", Float) = 1
	}

	SubShader
	{
		Tags { "RenderPipeline" = "UniversalRenderPipeline" "PreviewType" = "Sphere"}

		// 게임 오브젝트 그리기 한번에 대응
		Pass 
		{
			HLSLPROGRAM

			ENDHLSL
		}
	}
}

 

- 유니티는 작업의 편의성을 위해 작업자에게 제공하는 라이브러리 함수가 있는데 그걸 사용하려면 #include "" 구문으로 사용할수 있다. 

- 기본 코어 라이브러리는 Core.hlsl 인데 아래처럼 참조가 가능하다.

	... 
    
    SubShader
	{
		Tags { "RenderPipeline" = "UniversalRenderPipeline" "PreviewType" = "Sphere"}

		// 게임 오브젝트 그리기 한번에 대응
		Pass 
		{
			HLSLPROGRAM
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
			ENDHLSL
		}
	}
}

 

- #Pragma 명령어는 컴파일러에게 옵션을 알려줄때 사용하는 것이다.

자세한 옵션이나 사용법은 여기를 참조하면 된다.

- vertex 이름뒤에 나오는 함수로 vertex함수를 정하는 것이다.

- fragment 이름뒤에 나오는 함수 fragment함수를 정하는 것이다.

	...
    
    SubShader
	{
		Tags { "RenderPipeline" = "UniversalRenderPipeline" "PreviewType" = "Sphere"}

		// 게임 오브젝트 그리기 한번에 대응
		Pass 
		{
			HLSLPROGRAM
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

			// 우리가 작성할 버텍스 셰이더와 프래그먼트 셰이더 함수의 이름을 결정
			#pragma vertex vert
			#pragma fragment frag


			ENDHLSL
		}
	}
}

 

- 변수 선언은 맨 위에 선언한 머터리얼 프로퍼티 이름으로 선언할수 있다.

- RGBA 컬러값을 저장할수 있는 변수 타입인 half4 형식을 지정후에 _BaseColor라는 변수명으로 변수를 선언했다.

- pragma 밑에 선언되는 변수들은 유니폼변수라고 해서 버텍스, 프라그먼트 모두에 사용할수 있는 변수들이다.

- 참고로 half는 float의 메모리사용량의 절반만 쓰는 변수형이다.

 

- 그리고 각 함수의 입력으로 쓰이는 구조체를 선언해주어야 한다.

- 이 구조체의 자세한 설명은 여기를 참조한다.

	
    ...
    
    SubShader
	{
		Tags { "RenderPipeline" = "UniversalRenderPipeline" "PreviewType" = "Sphere"}

		// 게임 오브젝트 그리기 한번에 대응
		Pass 
		{
			HLSLPROGRAM
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

			// 우리가 작성할 버텍스 셰이더와 프래그먼트 셰이더 함수의 이름을 결정
			#pragma vertex vert
			#pragma fragment frag

			half4 _BaseColor;
			half _Scale;

			// 버텍스 함수에게 전달할 입력
			struct VertexInput
			{
				// 3D 모델의 각 정점의 위치를 가진다.
				// 시멘틱은 변수에 대한 추가 정보를 제공하는 문법/키워드
				// POSITION - 버텍스 함수의 입력으로 사용할 오브젝트 정점을 표시
				float3 objectSpacePosition : POSITION;
			};

			// 프래그먼트 함수에게 전달할 입력
			// 버텍스 함수의 출력이기도 하다
			struct FragmentInput
			{
				// 화면상의 위치
				// x, y - 화면 위치, z - 깊이, w - 동차 좌표계
				// SV_POSITION - 프래그먼트 함수의 입력으로 사용할 화면상의 정점을 표시하는데 사용
				float4 screenPosition : SV_POSITION;
			};

			ENDHLSL
		}
	}
}

 

입력에 사용할 구조체들을 선언했으면 실제 함수들을 작성하면 된다.

			// 우리가 작성할 버텍스 셰이더와 프래그먼트 셰이더 함수의 이름을 결정
			#pragma vertex vert
			#pragma fragment frag

			...
            
            half4 _BaseColor;
			half _Scale;

			// 버텍스 함수에게 전달할 입력
			struct VertexInput
			{
				// 3D 모델의 각 정점의 위치를 가진다.
				// 시멘틱은 변수에 대한 추가 정보를 제공하는 문법/키워드
				// POSITION - 버텍스 함수의 입력으로 사용할 오브젝트 정점을 표시
				float3 objectSpacePosition : POSITION;
			};

			// 프래그먼트 함수에게 전달할 입력
			// 버텍스 함수의 출력이기도 하다
			struct FragmentInput
			{
				// 화면상의 위치
				// x, y - 화면 위치, z - 깊이, w - 동차 좌표계
				// SV_POSITION - 프래그먼트 함수의 입력으로 사용할 화면상의 정점을 표시하는데 사용
				float4 screenPosition : SV_POSITION;
			};

			FragmentInput vert(VertexInput input)
			{
				// 오브젝트 버텍스들의 좌표값을 얻어와서 스케일값을 곱해서 원점에서의 거리가 변경되서 스케일값이 변경되게 됨
				// input.objectSpacePosition 이 값은 그래픽스 드라이버에 의해서 자동으로 채워져서 들어오게 된다.
				half3 objectSpacePosition = input.objectSpacePosition;
				objectSpacePosition *= _Scale;

				// 오브젝트 공간의 정점을 월드 공간으로 변환 (Core.hlsl에 포함되어 있는함수)
				half3 worldPosition = TransformObjectToWorld(objectSpacePosition);

				// 월드 공간에 있는 점점을 뷰 공간으로 변환 
				half3 viewPosition = TransformWorldToView(worldPosition);

				// 뷰 공간에 있는 점점을 클립 공간으로 변환(동차 클립 좌표계 Homobeneous Clip Space)
				half4 clipPosition = TransformWViewToHClip(viewPosition);

                FragmentInput output;
                // vert -> screenPosition - HClip 위치 클립 공간위치
                // --> 래스터라이저 --> frag : screenPosition 화면상의 좌표 위치로 변환 된 것을 받게된다.
                output.screenPosition = clipPosition;
                return output;
            }

            half4 frag(FragmentInput input) : SV_Target
            {
                return _BaseColor;
            }

			ENDHLSL
		}
	}
}

 

이렇게 셰이더를 다 작성했으면 유니티에서 셰이더 이름위에서 오른쪽 클릭으로 메뉴를 호출해 바로 매터리얼을 생성한다.

 

그리고 3D 객체를 생성해서 적용하면 바로 어떤식으로 동작하는지 알수 있다.