Unity 검색

셰이더 그래프의 커스텀 조명: 2019 버전 그래프 확장

2019년 7월 31일 테크놀로지 | 13 분 소요
공유

Unity 에디터 2019.1이 출시되면서 셰이더 그래프(Shader Graph) 패키지의 프리뷰가 공식적으로 종료되었습니다! 2019.2 버전에도 셰이더 그래프에 더 많은 기능이 추가될 예정입니다.

2019 버전에서 향상된 기능

커스텀 함수 및 서브그래프 업그레이드

이제 새로운 커스텀 함수(Custom Function) 노드를 사용하여 셰이더 그래프 내에서 커스텀 코드를 그대로 사용할 수 있습니다. 이 노드를 사용하면 커스텀 입력 및 출력을 정의하고 순서를 변경하며 노드 자체에 직접 또는 외부 파일을 참조하여 커스텀 함수를 삽입할 수 있습니다.

서브그래프(Sub Graph)도 업그레이드되었습니다. 이제 포트의 유형과 이름, 순서를 변경할 수 있고 이를 통해 서브그래프에 대한 출력을 정의할 수 있습니다. 또한 서브그래프용 블랙보드(Blackboard)는 이제 메인 그래프에서 지원하는 모든 데이터 유형을 지원합니다.

컬러 모드 및 정밀 모드

셰이더 그래프를 사용하여 강력하고 최적화된 셰이더를 더 쉽게 제작할 수 있습니다. 2019.2에서는 그래프 전체 또는 노드별로 그래프 계산의 정밀도를 수동으로 설정할 수 있습니다. 새로운 컬러 모드(Color Mode)를 사용하면 노드 카테고리 중 정밀도를 더 쉽고 빠르게 시각화하고 사용자가 원하는 커스텀 컬러를 표현할 수 있습니다.

새로운 기능에 관한 자세한 내용은 셰이더 그래프 기술 자료를 확인하시기 바랍니다.

샘플 프로젝트

새로운 커스텀 함수 워크플로를 사용하는 데 참고할 수 있는 프로젝트 예제를 만들었습니다. 저장소에서 프로젝트를 다운로드하고 따라해 보세요! 이 프로젝트는 Custom Function 노드를 사용하여 경량 렌더 파이프라인(LWRP)용 커스텀 조명 셰이더를 작성하는 방법을 보여줍니다. 새로운 프로젝트를 생성하여 따라하려면 2019.2 에디터 및 LWRP 패키지 버전 6.10.0 이상을 사용해야 합니다.

주요 광원에서 데이터 가져오기

먼저 씬의 주요 광원에서 정보를 가져옵니다. Create > Shader > Unlit Graph를 선택하여 새로운 언릿(Unlit) 셰이더 그래프를 생성합니다. Create Node 메뉴에서 새 Custom Function 노드를 찾은 다음 오른쪽 상단의 톱니바퀴 아이콘을 클릭하여 노드 메뉴를 엽니다.

이 메뉴에서 입력 및 출력을 추가할 수 있습니다. Direction 및 Color 출력 포트를 추가하고 둘 다 Vector 3을 선택합니다. “선언되지 않은 식별자” 오류 플래그는 코드를 추가하면 사라집니다. Type 드롭다운 메뉴에서 String을 선택합니다. 함수 이름을 업데이트합니다. 이 예제에서는 “MainLight”를 사용했습니다. 이제 텍스트 박스에 커스텀 코드를 추가할 수 있습니다.

먼저, `#ifdef SHADERGRAPH_PREVIEW`라는 플래그를 사용하겠습니다. 노드의 프리뷰 박스는 광원 데이터에 액세스할 수 없기 때문에 그래프 프리뷰 박스에 표시할 내용을 노드에 알려줘야 합니다. `#ifdef`는 컴파일러가 상황에 따라 다른 코드를 사용하도록 지시합니다. 출력 포트의 폴백(fallback) 값을 정의하여 시작합니다.

#if SHADERGRAPH_PREVIEW
	Direction = half3(0.5, 0.5, 0);
	Color = 1;

다음으로 `#else`를 사용하여 프리뷰가 아닐 때 수행할 작업을 컴파일러에 지시합니다. 이 단계가 실제 광원 데이터를 가져오는 단계입니다. LWRP 패키지의 빌트인 함수인 `GetMainLight()`를 사용합니다. 이 정보를 사용하여 Direction 및 Color 출력을 할당할 수 있습니다. 이제 커스텀 함수는 아래와 같아집니다.

#if SHADERGRAPH_PREVIEW
	Direction = half3(0.5, 0.5, 0);
	Color = 1;
#else
	Light light = GetMainLight();
	Direction = light.direction;
	Color = light.color;
#endif

이때 노드를 그룹에 추가하여 노드가 수행 중인 작업을 표시하도록 합니다. 노드를 마우스 오른쪽 버튼으로 클릭하고 Create Group from Selection을 선택한 후, 그룹 이름을 변경하여 노드가 수행하는 작업을 표시합니다. 예제에서는 “Get Main Light”라고 입력했습니다.

이제 광원 데이터를 가져왔으므로 셰이딩을 계산할 수 있습니다. 표준 램버시안(Lambertian) 조명부터 계산하겠습니다. 월드 노멀 벡터와 광원 방향의 내적을 구한 후 Saturate 노드에 전달하고 광원 컬러 값을 곱합니다. 이를 Unlit Master 노드의 Color 포트에 연결하면 프리뷰가 커스텀 셰이딩으로 업데이트됩니다!

커스텀 함수 파일 모드 사용

Custom Function 노드를 사용하여 광원 데이터를 가져온 후에 함수를 확장할 수 있습니다. 다음 함수는 주요 광원에서 방향과 컬러 외에도 감쇠 값을 가져옵니다.

이는 더 복잡한 함수이므로 파일 모드로 전환하고 HLSL include 파일을 사용합니다. 이를 통해 그래프에 삽입하기 전에 적절한 코드 에디터에서 더 복잡한 함수를 작성하고, 하나의 통일된 위치에서 코드를 디버깅할 수 있습니다.

프로젝트의 Assets > Include 폴더에서 `CustomLighting` include 파일을 열어 시작합니다. 이 단계에서는 `MainLight_half` 함수만 살펴보도록 하겠습니다. 함수는 아래와 같습니다.

void MainLight_half(float3 WorldPos, out half3 Direction, out half3 Color, out half DistanceAtten, out half ShadowAtten)
{
#if SHADERGRAPH_PREVIEW
   Direction = half3(0.5, 0.5, 0);
   Color = 1;
   DistanceAtten = 1;
   ShadowAtten = 1;
#else
#if SHADOWS_SCREEN
   half4 clipPos = TransformWorldToHClip(WorldPos);
   half4 shadowCoord = ComputeScreenPos(clipPos);
#else
   half4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
#endif
   Light mainLight = GetMainLight(shadowCoord);
   Direction = mainLight.direction;
   Color = mainLight.color;
   DistanceAtten = mainLight.distanceAttenuation;
   ShadowAtten = mainLight.shadowAttenuation;
#endif
}

위 함수에는 새로운 입력 및 출력 데이터가 포함되어 있으므로 Custom Function 노드로 돌아가 입력 및 출력을 추가합니다. 새로운 DistanceAtten(거리 감쇠) 및 ShadowAtten(그림자 감쇠) 출력을 추가합니다. 그런 다음, 새로운 WorldPos(월드 포지션) 입력을 추가합니다. 이제 입력과 출력이 추가되었으므로 include 파일을 참조할 수 있습니다. Type 드롭다운을 File로 변경합니다. Source 입력에서 include 파일을 찾고 참조할 Asset을 선택합니다. 이제 어떤 함수를 사용할지 노드에 지시해야 합니다. Name 박스에 “MainLight”를 입력하여 해당 함수를 사용하도록 설정합니다.

사용자가 설정한 함수와 달리 include 파일은 함수 이름 끝에 `_half`가 추가되어 있습니다. 이는 셰이더 그래프 컴파일러가 정밀도 형식을 각 함수 이름에 추가하기 때문입니다. 커스텀 함수를 정의할 때는 소스 코드를 컴파일러에 전달하여 함수가 어떤 정밀도 형식을 사용하는지 알려줘야 합니다. 그러나 노드에서는 메인 함수 이름만 참조하면 됩니다. 플로트 정밀도 모드에서 컴파일하려면 ‘float’ 값을 사용하는 함수의 복사본을 만들면 됩니다. ‘Precision’ 컬러 모드를 사용하면 그래프의 각 노드에 설정된 정밀도가 색상으로 나타나며 파란색은 float을, 빨간색은 half를 나타냅니다.

Custom Function을 서브그래프에 래핑해두면 필요에 따라 이 함수를 재사용할 수 있습니다. 노드와 해당 그룹을 선택하고 마우스 오른쪽 버튼을 클릭하여 Convert to Sub-graph를 찾습니다. 예시에서는 그룹 이름을 “Get Main Light”로 지정했습니다. 서브그래프에서 필요한 출력 포트를 서브그래프 출력 노드에 추가하고 노드의 출력을 서브그래프 출력과 연결합니다. 다음으로 입력에 연결할 월드 포지션 노드를 추가합니다.

서브그래프를 저장하고 언릿 그래프로 돌아갑니다. 기존 로직에 새로운 multiply 노드를 두 개 추가하겠습니다. 먼저, 두 감쇠 출력을 서로 곱합니다. 그런 다음 해당 출력에 광원 컬러 값을 곱합니다. 기본 셰이딩에서 감쇠를 올바르게 계산하려면 이전 단계의 NdotL을 곱하면 됩니다.

다이렉트 스페큘러 셰이더 만들기

앞서 만든 셰이더는 무광택 오브젝트에 적합하며 광택이 있는 오브젝트에 작업할 때는 셰이더에 스페큘러 계산 결과를 추가하면 됩니다. 이 단계에서는 서브그래프에 래핑된 또 다른 Custom Function 노드인 Direct Specular를 사용하겠습니다. `CustomLighting` include 파일을 확인해 보면 이번에는 같은 파일의 다른 함수를 참조하고 있습니다.

void DirectSpecular_half(half3 Specular, half Smoothness, half3 Direction, half3 Color, half3 WorldNormal, half3 WorldView, out half3 Out)
{
#if SHADERGRAPH_PREVIEW
   Out = 0;
#else
   Smoothness = exp2(10 * Smoothness + 1);
   WorldNormal = normalize(WorldNormal);
   WorldView = SafeNormalize(WorldView);
   Out = LightingSpecular(Color, Direction, WorldNormal, WorldView, half4(Specular, 0), Smoothness);
#endif
}

이 함수는 간단한 스페큘러 계산을 수행하며, 이에 대한 자세한 내용은 여기에서 확인할 수 있습니다. 이 함수의 서브그래프는 블랙보드에 다음과 같은 입력을 포함합니다.

새 노드에 함수와 맞는 입력 및 출력 포트가 모두 있는지 확인해야 합니다. 블랙보드에 프로퍼티를 추가하는 방법은 간단합니다. 먼저 오른쪽 상단의 Add (+) 아이콘을 클릭하고 데이터 유형을 선택합니다. 타원형 모양의 항목을 더블 클릭하여 입력의 이름을 변경하고 이를 드래그 앤 드롭하여 그래프에 추가합니다. 마지막으로 서브그래프의 출력 포트를 업데이트하고 저장합니다. 

이제 스페큘러 계산이 설정되었으므로 언릿 그래프로 돌아가 Create Node 메뉴를 사용하여 이를 추가할 수 있습니다. Attenuation 출력을 Direct Specular 서브그래프의 Color 입력에 연결합니다. 다음으로 Get Main Light 함수의 Direction 출력을 스페큘러 서브그래프의 Direction 입력에 연결합니다. NdotL*Attenuation의 결과값을 Direct Specular 서브그래프의 출력에 추가하고 이를 Color 출력에 연결합니다.

이렇게 하면 약간의 광택이 추가됩니다!

다중 광원으로 작업하기

LWRP의 주요 광원은 오브젝트에 대해 상대적으로 가장 밝은 방향 광원(일반적으로 태양)을 말합니다. 로우엔드 하드웨어에서 성능을 향상시키기 위해 LWRP는 주요 광원과 추가 광원을 따로 계산합니다. 셰이더가 씬 내의 가장 밝은 방향 광원뿐만 아니라 모든 광원을 정확하게 계산하려면 함수에 루프를 생성해야 합니다.

예제에서는 추가 광원 데이터를 가져오기 위해 새 Custom Function 노드를 래핑할 새 서브그래프를 사용했습니다. `CustomLighting` include 파일의 `AdditionalLight_float` 함수를 살펴보겠습니다.

void AdditionalLights_half(half3 SpecColor, half Smoothness, half3 WorldPosition, half3 WorldNormal, half3 WorldView, out half3 Diffuse, out half3 Specular)
{
   half3 diffuseColor = 0;
   half3 specularColor = 0;

#ifndef SHADERGRAPH_PREVIEW
   Smoothness = exp2(10 * Smoothness + 1);
   WorldNormal = normalize(WorldNormal);
   WorldView = SafeNormalize(WorldView);
   int pixelLightCount = GetAdditionalLightsCount();
   for (int i = 0; i < pixelLightCount; ++i)
   {
       Light light = GetAdditionalLight(i, WorldPosition);
       half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
       diffuseColor += LightingLambert(attenuatedLightColor, light.direction, WorldNormal);
       specularColor += LightingSpecular(attenuatedLightColor, light.direction, WorldNormal, WorldView, half4(SpecColor, 0), Smoothness);
   }
#endif

   Diffuse = diffuseColor;
   Specular = specularColor;
}

이전과 마찬가지로 Custom Function 노드의 파일 참조에 `AdditionalLights` 함수를 사용하고 올바른 입력 및 출력을 모두 생성했는지 확인합니다. 노드가 래핑된 서브그래프의 블랙보드에 Specular Color와 Specular Smoothness가 표시되는지 확인합니다. Position, Normal Vector, 및 View Direction 노드를 사용하여 서브그래프에 World PositionWorld Normal, 및 World Space View Direction을 연결합니다.

함수를 설정한 후 사용방법은 다음과 같습니다. 먼저, 이전 단계에서 메인 언릿 그래프를 가져와 서브그래프로 합칩니다. 노드를 선택하고 Convert to Sub-graph를 마우스 오른쪽 버튼으로 클릭합니다. 마지막 Add 노드를 제거하고 출력을 서브그래프의 출력 포트에 연결합니다. Specular 및 Smoothness에 대한 입력 프로퍼티도 생성할 것을 권장합니다.

2019년 7월 31일 테크놀로지 | 13 분 소요