TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역][기초]커스텀 익스프레션 프로젝트 경로 사용하기.

jplee 2023. 9. 24. 17:55

역자의 말.
경험상 노드 기반의 머트리얼이 노드 자체가 가독성도 너무 떨어지고 데이터 플로우 트레이스 하기에도 딱히 좋지도 않고(개인적으로...) 말이죠.
너무 복잡한 스파게티 노드는 함수화 하고 그 안에서도 익스프레션을 혼용해서 조금이라도 유지관리를 도모 할 수도 있고 간단히 셰이더 평션 유닛 테스트 할 때는 유용하게 쓰일 수 있고 특정 효과를 코드 빌드 없이 추가할 때는 이 방법을 주로 선호하게 될 수도 있겠죠.
머트리얼 에디터는 일반사용자에게 블랙박스와 같은 것이기도 하고요.
가능하다면 효과 테스트 후 릴리스 빌드 안에 추가 하고 머트리얼 인터페이스 코드를 수정 해서 좀 더 사용자 친화적으로 개진 하는 것이 올바르다고 봅니다. 
커스텀 익스프레션은 노드 자체에 코드를 작성 하는 방법과 .usf 파일로 저장 후 읽어 들여서 파싱하는 방식 이렇게 두 가지가 있습니다. 후자의 코드 관리 방법은 프로그래머의 도움을 받거나 테크 아티스트 도움을 받길 추천 합니다. 개인적으로 리마인드 할 겸 하여 커스터 익스프레션 사용할 때 읽어볼만한 블로그 포스팅이 있기에 간단히 번역하여 올려봅니다.


원문. ( 빅터의 테크아트 블로그 )

Unreal Engine Custom Node

victor’s tech art blog.

viclw17.github.io

 

Unreal Engine Custom Node

최근 저는 레이마칭 기법을 가지고 놀면서 언리얼 엔진 머티리얼 에디터에서 이를 구현해 보려고 합니다. 이 알고리즘에는 for 루프가 필요하기 때문에 커스텀 노드가 있는 HLSL 코드만 사용할 수 있습니다.

특히 코드가 복잡해질 때 커스텀 노드를 사용하면 많은 함정이 있습니다. 디테일 패널 코드 에디터는 매우 원시적이어서 VSCode처럼 코드를 편집하고 싶었고, 패널에 더 많은 옵션이 추가되었지만 전에는 거의 사용하지 않았기 때문에 나중에 참조할 수 있도록 제가 연구하고 테스트한 내용을 문서화했습니다.

새로운 사용자 지정 노드 옵션

예:

생성된 코드는 아래와 같습니다. (머티리얼 에디터의 창 -> HLSL 코드에서 생성된 코드를 확인하세요.) FMaterialPixelParameters 파라미터는 컴파일러에서 추가 비밀 입력으로 전달된 것을 참고하세요.

#ifndef additional_defines
#define additional_defines 42
#endif//additional_defines
MaterialFloat3 CustomExpression0(
  FMaterialPixelParameters Parameters,
  MaterialFloat input, 
  inout MaterialFloat a, 
  inout MaterialFloat b)
{
a = input;
b = a+1;
return b;
}

최종 셰이더에 포함되는 모든 자동 생성 상용구 코드를 볼 수 있으므로 문제 해결에 매우 유용합니다. C:\Program Files\Epic Games\UE_4.27\Engine\Shaders에서도 코드를 확인할 수 있으며, 여기에는 많은 유용한 내용이 있습니다.

다음 포스트에서 더 자세히 알아보세요! :)

글로벌 함수 만들기(핵!)

컴파일러는 말 그대로 Custom 노드의 텍스트를 CustomExpressionX라는 함수에 다음과 같이 복사하여 붙여넣습니다:

MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters)
{
  return 1; // return 1 is essential!
}

이를 기반으로 다음과 같은 코드를 입력하면 해킹을 수행할 수 있습니다:

//MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters)
//{
    return 1;
  }

  float MyGlobalVariable;

  float MyGlobalFunction(float a)
  {
    return a;
//}

중괄호로 함수를 세분화하고 글로벌 네임스페이스에 MyGlobalVariable 및 MyGlobalFunction()을 정의하는 트릭을 볼 수 있습니다. (엉성하지만 멋지죠!)
 
 

#include shader code ( 셰이더 코드 포함 )

이슈

당연히 디테일 패널에서 파일 경로 포함 옵션을 사용하고 싶습니다. 하지만 이렇게 하면 셰이더를 더미 경로 /Project/a.usf에 넣는 경우와 같은 오류가 발생하기 쉽습니다:

[SM5] Can't map virtual shader source path "/Project/a.usf".
Directory mappings are:
  /Engine -> D:/Program Files/Epic Games/UE_4.27/Engine/Shaders
[SM5] /Engine/Generated/Material.ush(2080): error: Can't open include file "/Project/a.usf"
  #include "/Project/a.usf"
  from /Engine/Private/BasePassVertexCommon.ush: 15:    #include "/Engine/Generated/Material.ush"
  from /Engine/Private/BasePassVertexShader.usf: 7:    #include "BasePassVertexCommon.ush"
  from /Engine/Private/BasePassPixelShader.usf: 38:    #include "/Engine/Generated/Material.ush"
[SM5] 1 error in preprocessor.

UE는 커스텀 노드 hlsl 코드(.usf 또는 .ush)를 /Engine -> D:/Program Files/Epic Games/UE_4.27/Engine/Shaders에 넣으라고 하는데, 셰이더를 프로젝트와 함께 제공받고 싶으면 어떻게 해야 하나요?

C++ 코드 수정

새 더미 C++ 클래스를 생성하여 현재 프로젝트를 C++ 프로젝트로 변환하고 컴파일합니다(저는 Actor 베이스 클래스에서 상속된 MyActor라는 클래스를 생성했습니다.).
프로젝트가 Visual Studio에 올바르게 로드되고 컴파일되려면 .uproject 파일 아이콘을 마우스 오른쪽 버튼으로 클릭하고 Visual Studio 프로젝트 파일 생성 을 선택해야 할 수 있습니다.
프로젝트 디렉터리의 Content 폴더와 같은 레벨에 Shaders라는 폴더를 생성합니다.
<project>.build.cs 파일의 공용 종속성 모듈 배열에 RenderCore를 추가합니다:

using UnrealBuildTool;
public class ShaderBits : ModuleRules
{
	public ShaderBits(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "RenderCore" }); // add "RenderCore"!

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
	}

In .h file add a new module with a StartupModule() function overrides:

#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h" // include this!

class FShaderBitsModule : public IModuleInterface
{
public:
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
};

<project_name>.cpp에서 추가된 셰이더 경로의 정의와 함께 StartupModule() 오버라이드를 추가하고 이 새 경로를 /Project로 매핑합니다.
마지막으로 매크로 IMPLEMENT_PRIMARY_GAME_MODULE에서 FDefaultGameModuleImpl을 FShaderBitsModule로 대체합니다.

#include "ShaderBits.h"
#include "Modules/ModuleManager.h"
#include "Interfaces/IPluginManager.h"
#include "Logging/LogMacros.h"
#include "Misc/Paths.h" // include this!

void FShaderBitsModule::StartupModule()
{
	//#if (ENGINE_MINOR_VERSION >= 21)
	FString ShaderDirectory = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders")); // add this!
	AddShaderSourceDirectoryMapping("/Project", ShaderDirectory);
	//#endif

}

void FShaderBitsModule::ShutdownModule(){}

//IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ShaderBits, "ShaderBits" );
IMPLEMENT_PRIMARY_GAME_MODULE(FShaderBitsModule, Project, "Project"); // edit this!

셰이더 코드 포함

그런 다음 /<프로젝트>/셰이더에서 셰이더를 만들 수 있습니다:

를 클릭하고 셰이더 파일을 포함합니다:

함수를 명시적으로 호출해야 할 수도 있습니다.
또는 코드 블록에 직접 포함할 수도 있습니다:

#include "/Project/ShaderPathTest/MyShaderStatement.usf"
return 1;

이 경우 컴파일러에 유효한 함수임을 알리기 위해 반환값 1이 필요합니다.
생성된 코드를 확인하면 뉘앙스의 차이를 확인할 수 있습니다:

MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters,MaterialFloat4 SceneTexture)
{
#include "/Project/ShaderPathTest/MyShaderStatement.usf"
return 1;
}
#include "/Project/ShaderPathTest/MyShaderFunction.usf"
MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters,MaterialFloat4 SceneTexture)
{
return func(SceneTexture,Parameters);;
}

그런 다음 외부 편집기에서 코드를 편집하고 저장한 후 머티리얼 편집기에서 코드에 줄 바꿈을 추가(Shift-enter)하여 다시 컴플라이언스를 트리거하고 적용 버튼을 클릭합니다.

UE 커스텀 노드 안에서 여러 함수 정의하기

커스텀 노드는 CustomExpression#() 함수 안에 코드를 래핑하므로 일반적으로 자체 함수를 정의하는 것이 금지됩니다.
하지만 HLSL에는 구조체 정의 안에 함수(메서드)를 정의할 수 있는 거의 문서화되지 않은 기능이 있는 것 같습니다. 
구조체 정의는 함수 안에 중첩될 수 있습니다.

struct Functions
{
  float3 OrangeBright(float3 c)
  {
      return c * float3(1, .7, 0);
  }
  float3 Out()
  {
    return OrangeBright(InColor);
  }
};
Functions f;
return f.Out();

멋진 점은 이 모든 작업이 언리얼의 USF 파일을 방해하지 않고 자신만의 유효 네임스페이스 내에서 이루어지며, 컴파일 속도가 빨라 반복작업이 가능하다는 점입니다.
이제 커스텀 함수 라이브러리와 더 복잡한 셰이더를 빌드할 수 있습니다.
HLSL은 구조체 안에 중첩된 구조체를 정의하는 것을 금지하는 것 같으니, 커스텀 구조체는 함수 구조체 위와 외부에 정의해야 합니다.


Reference

  • Creating a Gaussian Blur
  • https://forums.unrealengine.com/t/extending-custom-hlsl-custom-expressions/88820
  • https://forums.unrealengine.com/t/custom-hlsl-tips/104760
  • https://odederell3d.blog/2021/03/22/ue4-loading-shaders-from-within-the-project-folder/
  • https://forums.unrealengine.com/t/virtual-shader-source-path-link-custom-shaders-shadertoy-demo-download/119996

 
언리얼 엔진 5.0과 5.1의 경우 플러그인이 있습니다.
Shader Reader PRO in Code Plugins - UE Marketplace (unrealengine.com)

Shader Reader PRO in Code Plugins - UE Marketplace

The shader reader will allow you to write custom shading code for your materials, also you can have full control over the folder structure

www.unrealengine.com