기본 머티리얼과 셰이더 이해
언리얼 엔진의 머티리얼 시스템은 게임 내 객체의 시각적 표현을 정의하는 강력한 도구입니다.
이 절에서는 C++ 관점에서 머티리얼 시스템과 셰이더 프로그래밍의 기초를 살펴보겠습니다.
UMaterial 및 UMaterialInstance 클래스
UMaterial은 기본 머티리얼 클래스이며 UMaterialInstance는 이를 상속받아 인스턴스화된 머티리얼을 나타냅니다.
UCLASS()
class MYGAME_API UMyMaterialManager : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Materials")
UMaterialInterface* CreateSimpleMaterial(FLinearColor Color);
UFUNCTION(BlueprintCallable, Category = "Materials")
UMaterialInstanceDynamic* CreateDynamicMaterialInstance(UMaterialInterface* BaseMaterial);
};
UMaterialInterface* UMyMaterialManager::CreateSimpleMaterial(FLinearColor Color)
{
UMaterial* NewMaterial = NewObject<UMaterial>();
NewMaterial->BaseColor.Connect(TBaseColor<FLinearColor>(Color));
NewMaterial->PostEditChange();
return NewMaterial;
}
UMaterialInstanceDynamic* UMyMaterialManager::CreateDynamicMaterialInstance(UMaterialInterface* BaseMaterial)
{
return UMaterialInstanceDynamic::Create(BaseMaterial, this);
}
동적 머티리얼 인스턴스 생성 및 조작
동적 머티리얼 인스턴스를 사용하면 런타임에 머티리얼 속성을 변경할 수 있습니다.
UCLASS()
class MYGAME_API AMyActor : public AActor
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Materials")
void ChangeMaterialColor(FLinearColor NewColor);
private:
UPROPERTY()
UMaterialInstanceDynamic* DynamicMaterial;
void InitializeDynamicMaterial();
};
void AMyActor::InitializeDynamicMaterial()
{
UStaticMeshComponent* MeshComponent = GetComponentByClass<UStaticMeshComponent>();
if (MeshComponent)
{
UMaterialInterface* BaseMaterial = MeshComponent->GetMaterial(0);
DynamicMaterial = UMaterialInstanceDynamic::Create(BaseMaterial, this);
MeshComponent->SetMaterial(0, DynamicMaterial);
}
}
void AMyActor::ChangeMaterialColor(FLinearColor NewColor)
{
if (DynamicMaterial)
{
DynamicMaterial->SetVectorParameterValue("BaseColor", NewColor);
}
}
C++에서 머티리얼 파라미터 제어
머티리얼 파라미터를 C++에서 직접 제어할 수 있습니다.
void AMyActor::UpdateMaterialParameters(float Roughness, float Metallic)
{
if (DynamicMaterial)
{
DynamicMaterial->SetScalarParameterValue("Roughness", Roughness);
DynamicMaterial->SetScalarParameterValue("Metallic", Metallic);
}
}
커스텀 셰이더 작성
언리얼 엔진에서는 HLSL을 사용하여 커스텀 셰이더를 작성할 수 있습니다.
// MyCustomShader.usf
##include "/Engine/Private/Common.ush"
void MainVS(
in float4 InPosition : ATTRIBUTE0,
out float4 OutPosition : SV_POSITION
)
{
OutPosition = mul(InPosition, View.ViewProjectionMatrix);
}
void MainPS(
out float4 OutColor : SV_Target0
)
{
OutColor = float4(1, 0, 0, 1); // 빨간색
}
이 셰이더를 C++에서 사용하려면 FGlobalShader 클래스를 상속받아 구현합니다.
class FMyCustomShader : public FGlobalShader
{
DECLARE_SHADER_TYPE(FMyCustomShader, Global);
public:
FMyCustomShader() {}
FMyCustomShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
}
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return true;
}
};
IMPLEMENT_SHADER_TYPE(, FMyCustomShader, TEXT("/Game/Shaders/MyCustomShader.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_SHADER_TYPE(, FMyCustomShader, TEXT("/Game/Shaders/MyCustomShader.usf"), TEXT("MainPS"), SF_Pixel);
HLSL을 사용한 머티리얼 함수 구현
HLSL을 사용하여 복잡한 머티리얼 함수를 구현할 수 있습니다.
// MyMaterialFunction.usf
##include "/Engine/Private/Common.ush"
float3 CalculateNormal(float3 WorldPosition)
{
float3 dPdx = ddx(WorldPosition);
float3 dPdy = ddy(WorldPosition);
return normalize(cross(dPdx, dPdy));
}
void MainMaterialFunction(
in float3 WorldPosition,
out float3 OutNormal
)
{
OutNormal = CalculateNormal(WorldPosition);
}
이 함수를 머티리얼 에디터에서 사용하려면 Custom 노드를 추가하고 HLSL 코드를 직접 입력하면 됩니다.
성능을 고려한 셰이더 최적화 기법
- 분기문 최소화 : 조건문 대신 수학적 표현식 사용
- 텍스처 샘플링 최적화 : 밉맵 및 LOD 활용
- 복잡한 연산 사전 계산 : 가능한 경우 CPU에서 계산 후 셰이더에 전달
// 최적화된 셰이더 예시
float3 OptimizedCalculation(float3 Input)
{
// 분기문 대신 수학적 표현식 사용
return lerp(Input, float3(1,1,1), step(0.5, length(Input)));
}
머티리얼 에디터와 C++ 코드의 연동
머티리얼 에디터에서 생성한 파라미터를 C++에서 제어할 수 있습니다.
UCLASS()
class MYGAME_API UMyMaterialParameterCollection : public UMaterialParameterCollection
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Parameters")
FName ColorParameterName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Parameters")
FName RoughnessParameterName;
};
void AMyActor::UpdateMaterialCollectionParameter()
{
UMaterialParameterCollectionInstance* CollectionInstance = GetWorld()->GetParameterCollectionInstance(MyMaterialParameterCollection);
if (CollectionInstance)
{
CollectionInstance->SetVectorParameterValue(ColorParameterName, FLinearColor::Red);
CollectionInstance->SetScalarParameterValue(RoughnessParameterName, 0.5f);
}
}
프로시저럴 머티리얼 생성
C++에서 프로시저럴하게 머티리얼을 생성할 수 있습니다.
UMaterial* UMyMaterialGenerator::CreateProceduralMaterial()
{
UMaterial* NewMaterial = NewObject<UMaterial>();
// 노이즈 텍스처 생성
UMaterialExpressionPerlinNoise* NoiseExpression = NewObject<UMaterialExpressionPerlinNoise>(NewMaterial);
NoiseExpression->Scale = 10.0f;
// 베이스 컬러에 노이즈 연결
NewMaterial->BaseColor.Expression = NoiseExpression;
NewMaterial->PostEditChange();
return NewMaterial;
}
동적 텍스처 생성 및 업데이트
런타임에 동적으로 텍스처를 생성하고 업데이트할 수 있습니다.
UCLASS()
class MYGAME_API UDynamicTextureManager : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Dynamic Texture")
UTexture2D* CreateDynamicTexture(int32 Width, int32 Height);
UFUNCTION(BlueprintCallable, Category = "Dynamic Texture")
void UpdateDynamicTexture(UTexture2D* Texture, const TArray<FColor>& PixelData);
};
UTexture2D* UDynamicTextureManager::CreateDynamicTexture(int32 Width, int32 Height)
{
UTexture2D* NewTexture = UTexture2D::CreateTransient(Width, Height, PF_B8G8R8A8);
NewTexture->UpdateResource();
return NewTexture;
}
void UDynamicTextureManager::UpdateDynamicTexture(UTexture2D* Texture, const TArray<FColor>& PixelData)
{
if (Texture && PixelData.Num() == Texture->GetSizeX() * Texture->GetSizeY())
{
FUpdateTextureRegion2D Region(0, 0, 0, 0, Texture->GetSizeX(), Texture->GetSizeY());
Texture->UpdateTextureRegions(0, 1, &Region, Texture->GetSizeX() * 4, 4, (uint8*)PixelData.GetData());
}
}
고급 렌더링 기법 구현 및 최적화
물리 기반 렌더링 (PBR)
PBR 구현을 위해 머티리얼에서 Metallic, Roughness, Base Color 파라미터를 사용합니다.
void AMyActor::SetupPBRMaterial()
{
UMaterialInstanceDynamic* PBRMaterial = UMaterialInstanceDynamic::Create(BasePBRMaterial, this);
PBRMaterial->SetScalarParameterValue("Metallic", 0.5f);
PBRMaterial->SetScalarParameterValue("Roughness", 0.3f);
PBRMaterial->SetVectorParameterValue("BaseColor", FLinearColor(0.75f, 0.75f, 0.75f));
UStaticMeshComponent* MeshComponent = GetComponentByClass<UStaticMeshComponent>();
if (MeshComponent)
{
MeshComponent->SetMaterial(0, PBRMaterial);
}
}
SubsurfaceScattering
SubsurfaceScattering을 구현하려면 머티리얼에서 Subsurface Color와 Subsurface Profile 파라미터를 사용합니다.
void AMyActor::SetupSubsurfaceScatteringMaterial()
{
UMaterialInstanceDynamic* SSSMaterial = UMaterialInstanceDynamic::Create(BaseSSMaterial, this);
SSSMaterial->SetVectorParameterValue("SubsurfaceColor", FLinearColor(1.0f, 0.1f, 0.1f));
SSSMaterial->SetScalarParameterValue("SubsurfaceOpacity", 0.5f);
UStaticMeshComponent* MeshComponent = GetComponentByClass<UStaticMeshComponent>();
if (MeshComponent)
{
MeshComponent->SetMaterial(0, SSSMaterial);
}
}
이러한 고급 렌더링 기법을 최적화하려면,
- 적절한 LOD 설정 : 거리에 따라 셰이더 복잡도 조절
- 셰이더 복잡도 관리 : 필요한 기능만 활성화
- 텍스처 압축 및 스트리밍 : 메모리 사용량 최적화
머티리얼 시스템과 셰이더 프로그래밍을 효과적으로 활용하면 시각적으로 뛰어나고 성능이 최적화된 게임을 개발할 수 있습니다.
C++를 통한 직접적인 제어와 HLSL을 통한 커스텀 셰이더 작성을 조합하여 사용하면, 언리얼 엔진의 렌더링 기능을 최대한 활용할 수 있습니다.