본문 바로가기
computer graphics

[UE5] HLSL로 커스텀 셰이더 만들기

by objet 2024. 10. 13.

 

이 포스트는 언리얼 기능 구현 공유 커뮤니티인 SEEN 활동의 일환으로 쓰여진 글입니다.

 

구현 주제

특정 메쉬에 비주얼적으로 특별한 효과를 주고 싶을 때, 이를 수행하는 커스텀 셰이더를 만드는 방법을 다룹니다.

 

이런 사람이 읽으면 좋아요

  • 동적인 게임 환경을 만들고 싶으신 분
  • 사실적인 그래픽을 구현하고 싶으신 분
  • 그래픽스에 흥미가 있는 분
  • 머티리얼 그래프(블루프린트) 말고도 HLSL과 수식으로 셰이더를 만들고 싶으신 분

 

이 구현 경험을 통해 얻은 인사이트

  • 언리얼 엔진에서의 셰이더 사용 방법과 처리 과정
  • 언리얼 엔진이 커스텀 셰이더의 종류를 구분하는 방법

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/unreal-engine-materials

 

 

 

 

 


 

 

 

 

 

구현 과정

HLSL 커스텀 셰이더를 작성하기에 앞서 관련된 개념들을 먼저 정리하겠습니다.

 

 

셰이더란?

셰이더는 금속, 천의 질감 등 시각적 특성을 세세히 정의하고, 빛과 물리의 상호작용을 처리하여 화면에 렌더링하는 작은 프로그램입니다. 셰이더의 종류는 여러가지가 있는데, 그래픽스 파이프라인의 처리 과정 속에서 다양하게 등장합니다. 많이 쓰는 셰이더는 버텍스 셰이더(Vertex Shader), 프래그먼트 셰이더(Fragment Shader), 지오메트리 셰이더(Geometry Shader) 등이 있습니다.

언리얼 엔진에서의 셰이더는 HLSL(High-Level Shading Language)로 작성되고, 해당 코드는 GPU가 읽을 수 있는 어셈블리 언어로 변환되어 실행됩니다.

언리얼 에디터에서 셰이더를 만들 때에는 주로 머티리얼 에디터를 통해 사용합니다. HLSL 코드를 작성하지 않아도 머티리얼 에디터(Material Editor)라는 비주얼 스크립팅 인터페이스에서 생성할 수 있습니다.

 

HLSL이란?

HLSL란 그래픽스 프로그래밍에서 사용되는 고급 셰이딩 언어로, DirectX와 함께 사용됩니다. 문법은 C언어와 비슷합니다. HLSL은 주로 GPU에서 실행되는 셰이더를 작성하는 데 사용되며, 다양한 효과와 처리를 구현할 수 있습니다.

  • 기본 타입으로는 float, int, bool 등이 있으며, 벡터 타입(float2, float3, float4)과 행렬 타입(float4x4) 등이 있습니다.
  • 함수는 다음과 같은 형식으로 정의합니다:
  • float4 MyFunction(float3 position) { return float4(position, 1.0); // 3D 위치를 4D 벡터로 변환 }
  • 주의할 점: 픽셀 셰이더에서 사용하는 각각의 텍스처 맵에서 정보를 가져오기 위해서는 맵마다 sampler를 반드시 선언해주어야 합니다.
  • 내장 함수: abs(), exp(), ddy(), clamp() 등 다양한 수학 내장 함수들이 포함되어 있고, 텍스처 데이터를 샘플링하기 위한 내장 함수도 다양하게 포함되어 있습니다.
  • 이외 uniform 타입, varing 데이터 등 다양한 데이터 형식을 지원합니다.

더 자세한 정보를 얻고 싶다면 http://www.infopub.co.kr/ebook/pdf/5674-212.pdf

 

머티리얼이란?

머티리얼은 좁은 의미로 오브젝트의 표면을 정의하는 역할을 하고, 넓은 의미로는 Mesh의 시각적인 형태를 제어하는 Paint 역할을 합니다. 따라서 머티리얼은 텍스처, 색상, 반사도, 거칠기 등의 속성을 가지고 있고, 이 값들은 셰이더를 통해 최종 렌더링 결과를 도출합니다.

머티리얼 에디터는 위의 속성값들을 변경할 수 있는 인터페이스입니다. 이 안에서 우리가 직접 커스텀 셰이더 시퀀스를 만들어서 언리얼 엔진의 셰이더를 변경할 수 있습니다.

머티리얼 에디터 UI

머티리얼에 다양한 효과를 주는 셰이더를 만들고 싶다면, 머티리얼 표현식(Material Expression), 즉 노드들을 효과에 맞게 연결시켜 **셰이더 그래프(Shader Graph)**를 구성합니다. 즉 셰이더 그래프란 특정 효과를 내기 위한 머티리얼 표현식으로 이루어진 일련의 시퀀스를 말합니다.

 

언리얼에서의 커스텀 쉐이더 처리 과정

  1. 머티리얼 에디터에서 노드(표현식)들을 연결하여 셰이더 그래프를 만듭니다. 이 때 HLSL로 짠 코드를 커스텀 노드에 추가시킬 수도 있습니다.
  2. 머티리얼을 Save할 때, 언리얼 엔진이 셰이더 그래프를 포함한 다른 노드들의 정보를 통합하여 컴파일하여 최종 HLSL 셰이더 코드를 생성합니다.
  3. 위에서 컴파일된 셰이더 코드는 언리얼 엔진의 렌더링 파이프라인에 포함되어, 해당 머티리얼의 메쉬에 우리가 적용한 셰이더 코드가 적용되어 화면에 렌더링됩니다.

 

본격적으로 커스텀 셰이더를 구현하는 방법에 대해 살펴보겠습니다.

먼저 셰이더를 구현하기 위해서는 머티리얼 그래프의 구성요소에 대한 이해가 필요합니다.

위 그림은 머티리얼 UI의 구성요소들을 구분해놓은 것입니다.

커스텀 셰이더를 구현하기 위해 집중적으로 살펴봐야 할 부분은 바로 4번과 5번, 노드의 Details 창과 머티리얼 그래프입니다.

디테일 패널로 할 수 있는 것들은 다음과 같습니다:

  • 이 창에는 해당 머티리얼의 베이스 프로퍼티가 나오거나, 특정 노드를 선택했을 때 노드의 프로퍼티가 출력됩니다.
  • 머티리얼의 프로퍼티에는 피지컬 머티리얼, 포워드 셰이딩, 포스트 프로세스 머티리얼에 대한 속성들을 변경하고 관리할 수 있습니다.
  • 특정 노드(머티리얼 표현식)를 선택했을 때, 해당 노드가 가지고 있는 프로퍼티를 적절히 이용하면 우리가 원하는 셰이더의 효과를 구현할 수 있습니다.

 

 

관련 용어들에 대해 추가로 설명하겠습니다.

머티리얼 그래프

머티리얼 그래프에는 이 머티리얼에 속한 모든 머티리얼 표현식의 그래프가 포함되어 있습니다.

  • 머티리얼 그래프 패널
    기본적으로 모든 머티리얼에는 다음과 같은 하나의 베이스 머티리얼 노드(=메인 머티리얼 노드)가 있습니다. 이 노드에는 다양한 입력 핀들이 있는데, 모든 머티리얼에 입력이 전부 필요하지는 않습니다.
  • 메인 머티리얼 노드
    우리가 셰이더 그래프를 만들었을 때, 해당 그래프의 출력 핀을 메인 노드의 어떤 입력 핀에 연결하느냐에 따라 버텍스 셰이더인지 픽셀 셰이더인지를 언리얼 엔진이 구분합니다.

또한, 지금 그림을 보시면 흰색으로 활성화된 입력 핀과 회색으로 비활성화된 입력 핀이 보이는데 이는 Details 패널의 설정과 연관되어 있습니다.

  • Material Domain: 머티리얼의 용도를 결정합니다. 예를 들어 해당 머티리얼이 표면의 일부인지, 라이트 함수인지, 포스트 프로세스 머티리얼인지 결정하게 됩니다.
  • Blend Mode: 머티리얼이 배경의 픽셀과 블렌드되는 방법을 정의합니다.
  • Shading Model: 머티리얼의 표면에서 빛이 계산되는 방법을 정의합니다.

 

메인 머터리얼 노드에서 자주 사용되는 입력 핀들은 다음과 같습니다.

  • Base Color: 해당 머티리얼의 전반적인 색을 정의합니다. 즉, Specular나 highlight가 아니라 diffuse를 말합니다.
  • Metallic: 금속 느낌을 얼마나 낼 것인지를 정의합니다. 0부터 1까지로 나타낼 수 있습니다.
  • Specular: 표면에서 반사되는 빛의 정도를 정의합니다.
  • Roughness: 머티리얼 표면의 거칠거나 부드러운 정도를 제어합니다.
  • Emissive Color: 머티리얼이 빛나는 위치와 밝기를 제어합니다. 네온 효과 등을 줄 때 주로 사용됩니다.
  • Normal: 머티리얼의 개별 픽셀들이 향하는 방향을 수정하여 표면에 유의미한 물리적 디테일을 더하는 데 사용되는 노멀 맵을 받습니다. 고해상도 모델링을 수행해야 할 때 사용됩니다.
  • World Position Offset: 메시의 버텍스를 머티리얼로 월드 스페이스에서 조작할 수 있습니다. 이는 오브젝트를 움직이며, 모양을 변경하고, 회전시키고, 그 외 다양한 효과를 주는 데 유용합니다.

 


 

커스텀 셰이더 만들기

먼저, 커스텀 셰이더를 만드는 방법은 크게 두 가지로 나뉩니다.

  1. 비주얼 스크립팅, 즉 언리얼 엔진에서 제공하는 표현식들을 이어붙여서 블루프린트 방식으로 만들기 → 구성요소 전부 노드
    • 장점: 엔진이 내부적으로 처리를 끝내 놓은 표현식을 사용하므로 해당 표현식에 한해 최적화가 잘 되어 있습니다.
    • 단점: 구현할 수 있는 효과의 다양성에 한계가 있습니다.
  2. 커스텀 표현식을 사용하여 HLSL 코드로 효과 만들기
    • 장점: 기존 노드로는 할 수 없는 기능을 구현할 수 있습니다.
    • 단점: 상수 폴딩(최적화)이 되어 있지 않아 연산 수가 더 많아질 수도 있습니다.

머터리얼 그래프를 다룰 때에는 사실상 두 방법을 전부 다뤄야 하는 경우가 많습니다. 이 글의 목표는 HLSL을 다루어 셰이더를 만드는 것이므로, 2번 방법을 집중적으로 살펴보도록 하겠습니다.

 

구현하고 싶은 효과

셰이더를 구현하기 전에, 당연하게도 셰이더를 통해 렌더링하고 싶은 효과에 대해 자세히 정의하는 과정이 꼭 필요합니다.

앞서 말씀드렸다시피 셰이더에는 다양한 카테고리가 있고, 그 중 본인이 원하는 효과가 정점들의 위치나 특성을 컨트롤하는 버텍스 셰이더인지, 픽셀에 관여하는 프래그먼트 셰이더인지 확인할 필요가 있습니다. 그에 따라 메인 머티리얼 노드에 연결시킬 입력 핀이 달라지기 때문입니다.

예를 들면, 버추얼 휴먼의 피부 메쉬에는 어떤 효과를 적용할 수 있을까요?

  • 모공, 주름 효과: 피부의 특정 위치의 z좌표를 내려가게 하는 효과 → 버텍스 셰이더
  • 주근깨, 잡티 효과: 특정 픽셀의 색상을 변화시키는 효과 → 프래그먼트 셰이더

그리고 해당 효과를 구현하기 위해서 필요한 Input에는 어떤 것이 있는지 알아보고 이를 커스텀 노드의 Input에 연결시켜야 합니다. 대표적으로 다음과 같은 것이 자주 쓰입니다.

  • Absolute World Position: 해당 메쉬의 월드 포지션 좌표
  • Material Collection Parameter: 다른 메테리얼의 특정 좌표를 갖고 올 수 있는 매개체
  • Time: 시간의 흐름에 따라 달라지는 효과를 구현할 때 필요한 시간 정보

이외 UV, Tangent, Binormal 등 다양하게 사용할 수 있는 입력들이 있습니다.

 

 

 

그럼 이제 머터리얼 에디터에서 HLSL 코드를 넣은 커스텀 셰이더를 구현하는 방법에 대해 순차적으로 알아보겠습니다.

1.  커스텀 표현식 생성하기

머티리얼 그래프 위에서 마우스 왼쪽 클릭 > Custom > Custom으로 노드를 생성할 수 있습니다.

생성된 노드의 디테일 창은 다음과 같이 구성되어 있습니다.

  • Code: 표현식이 실행할 HLSL 셰이더 코드가 들어갑니다.
    • 여기에 들어가는 코드는 결함이 없는 코드여야만 합니다.
  • Output Type: 표현식의 출력 값 유형을 나타냅니다.
    • 해당 표현식의 결과값을 float1~float4로 나타낼 것인지, 머티리얼 프로퍼티로 나타낼 것인지 출력 형식을 지정할 수 있습니다.
  • Description: 머티리얼 에디터에서 노드 이름에 해당하는 텍스트를 나타냅니다.
  • Inputs 배열: 표현식에 사용되는 입력 배열입니다.

 

2. 필요한 입력 연결하기

구현하려는 효과에 필요한 입력들을 선별하여 커스텀 노드의 입력 핀에 연결합니다.

핀을 연결하면 다음과 같이 Inputs 탭에 요소로 들어가게 되고, 각각의 데이터에 대해 커스텀 셰이더 내에서 편하게 사용할 수 있도록 Input Name을 지정해줄 수 있습니다.

아래는 블러 효과를 만들기 위해 필요한 입력 핀들입니다.

  • Tex: 메쉬에 입힐 텍스처
  • UV: 텍스처를 입힐 좌표
  • r: 블러 효과를 적용할 픽셀의 수
  • dist: 블러를 적용할 픽셀 간의 거리

언리얼 4.27 버전의 예시이나 구성은 동일.

 

3. HLSL 코드 구현

구현하려는 효과에 대한 HLSL 코드를 생성하여, Details 패널의 Code에 넣어줍니다.

아래 코드는 머티리얼 노드 내에 쓸 수 있는 블러 효과 예시 코드입니다.

앞서 커스텀 표현식의 입력 핀으로 넣어주었던 Tex, UV, r, dist가 쓰이고 있는 것을 확인할 수 있습니다.

float3 blur = Texture2DSample(Tex, TexSampler, UV);

for (int i = 0; i < r; i++)
{

  blur += Texture2DSample(Tex, TexSampler, UV + float2(i * dist, 0));
  blur += Texture2DSample(Tex, TexSampler, UV - float2(i * dist, 0));

}

for (int j = 0; j < r; j++)
{

  blur += Texture2DSample(Tex, TexSampler, UV + float2(0, j * dist));
  blur += Texture2DSample(Tex, TexSampler, UV - float2(0, j * dist));

}

blur /= 2*(2*r)+1;
return blur;

HLSL 코드를 짤 때의 팁은 아래에 간단히 적어두겠습니다.

 

 

4. 메인 머티리얼 노드와 연결

위에서 설명한 메인 머티리얼 노드 내 핀들의 특징을 바탕으로, 구현한 효과를 제대로 적용시키기 위해서는 어느 핀에 연결해야 하는지 결정합니다.

예시에서 구현한 블러 효과를 주는 셰이더는 픽셀의 색상 등에 영향을 주는 프래그먼트 셰이더의 일종이고, float3 타입을 리턴하므로 Base Color 핀에 연결해줍니다.

위의 절차들을 통해 생각보다 간단하게 셰이더를 만들어 보았습니다.

이후에는 셰이더를 구현할 때 알고 있으면 도움이 되는 팁에 대해 간단하게 소개하겠습니다.

 

머티리얼의 전체 셰이더 코드 보기

머티리얼 에디터를 킨 다음 Window 탭 > Shader Code > HLSL Code 에서 확인할 수 있습니다.

이 기능을 눌렀을 때 나오는 머터리얼 셰이더 코드는 에디터에서 조작할 수 있는 간단한 코드가 아니므로 읽기 전용으로만 볼 수 있습니다.

이 기능으로 우리는 다음과 같은 효과를 얻을 수 있습니다:

  • 이 코드를 통해 우리가 짠 HLSL 코드가 어떻게 전체 셰이더 코드에 통합되어 돌아가는지를 확인할 수 있습니다.
  • HLSL 코드를 짤 때, 입력 핀으로 따로 받을 필요 없이 머티리얼 셰이더에서 관리하고 있는 변수들을 확인할 수 있습니다.

Copy 버튼을 눌러 가독성 있게 조절하신 뒤, 살펴보시는 걸 추천드립니다!

 

 

 

 

 


 

 

느낀 점

UNSEEN 기간동안 개발한 기능 중 제 개발 추구점 및 흥미와 가장 맞아떨어져서 즐겁게 구현했던 것 같습니다. 원래 이 주제에 대하여 컨퍼런스 때 발표할까 망설였는데 지금 후회되는 걸 보니 그때 할 걸 그랬나 싶습니다🤣

개발 당시에는 HLSL 코드가 주는 프로페셔널한 느낌(ㅋㅋ)이 마음에 들어 무작정 HLSL 코드로 셰이더를 구성했었는데, 이 글을 작성하기 위해 여러모로 조사를 해보면서 기존 노드를 사용해서 충분히 구현할 수 있는 기능이었다고 생각이 드네요. 글을 쓰면서도 조금 더 아는 게 많아지고 성장한 것 같습니다.

분량 상의 이유로 뺀 내용이 있어서 조금 아쉽게 느껴지기도 합니다. 예시로 언리얼 엔진 docs에 쓰여 있는 blur 효과 셰이더 코드를 가져왔는데, 나중에 기회가 된다면 제가 개발한 셰이더에 대해서도 포스팅을 하고 싶다는 생각이 들었습니다.

모쪼록 끝까지 글 읽어주신 분들께 감사합니다🙇‍♀️