icon안동민 개발노트

기본 머티리얼과 셰이더 이해


 언리얼 엔진의 머티리얼 시스템은 게임 내 객체의 시각적 표현을 정의하는 강력한 도구입니다.

 이 절에서는 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 코드를 직접 입력하면 됩니다.

성능을 고려한 셰이더 최적화 기법

  1. 분기문 최소화 : 조건문 대신 수학적 표현식 사용
  2. 텍스처 샘플링 최적화 : 밉맵 및 LOD 활용
  3. 복잡한 연산 사전 계산 : 가능한 경우 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);
    }
}

 이러한 고급 렌더링 기법을 최적화하려면,

  1. 적절한 LOD 설정 : 거리에 따라 셰이더 복잡도 조절
  2. 셰이더 복잡도 관리 : 필요한 기능만 활성화
  3. 텍스처 압축 및 스트리밍 : 메모리 사용량 최적화

 머티리얼 시스템과 셰이더 프로그래밍을 효과적으로 활용하면 시각적으로 뛰어나고 성능이 최적화된 게임을 개발할 수 있습니다.

 C++를 통한 직접적인 제어와 HLSL을 통한 커스텀 셰이더 작성을 조합하여 사용하면, 언리얼 엔진의 렌더링 기능을 최대한 활용할 수 있습니다.