Nanite Foliage
UE 5.7의 고밀도 식물 렌더링 시스템
Nanite Foliage 개요
LOD 없이 밀집된 고밀도 폴리지 렌더링
Nanite Foliage는 UE 5.7에서 실험적 기능으로 도입된 고밀도 식물 렌더링 시스템입니다. 기존 폴리지 시스템의 LOD 제작 필요성을 제거하고, 크로스페이드/팝핑 없이 매끄러운 전환을 제공합니다.
핵심 시스템 3가지
1. Nanite Assemblies
폴리지 파트 인스턴스를 하나의 유닛으로 관리. 나무의 줄기, 가지, 잎을 하나의 어셈블리로 묶어 효율적으로 처리합니다.
2. Nanite Skinning
바람 등 동적 동작을 결정. 버텍스 애니메이션을 통해 자연스러운 바람 효과를 구현합니다.
3. Nanite Voxels
카메라 거리에 따른 디테일, 애니메이션, 머티리얼 특성을 유지하면서 복셀 기반 최적화를 수행합니다.
현재 제한사항 (실험적 기능)
- 물리 시뮬레이션 지원 불완전
- 충돌 처리 제한적
- 바람 애니메이션 완전하지 않음
- 프로덕션 사용 시 충분한 테스트 필요
Nanite Foliage 설정
C++에서 Nanite Foliage 구성하기
// NaniteFoliageManager.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "NaniteFoliageManager.generated.h"
/**
* Nanite Foliage 관리 서브시스템
* 대규모 오픈월드의 고밀도 식물 렌더링 관리
*/
USTRUCT(BlueprintType)
struct FNaniteFoliageSettings
{
GENERATED_BODY()
// Nanite Foliage 활성화
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bEnableNaniteFoliage = true;
// 최대 렌더링 거리
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "1000.0"))
float MaxDrawDistance = 100000.0f;
// 바람 강도
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0.0", ClampMax = "1.0"))
float WindStrength = 0.5f;
// 바람 방향
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector WindDirection = FVector(1.0f, 0.0f, 0.0f);
// 폴리지 밀도 스케일
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0.1", ClampMax = "2.0"))
float DensityScale = 1.0f;
// Voxel LOD 바이어스
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "-2.0", ClampMax = "2.0"))
float VoxelLODBias = 0.0f;
};
UCLASS()
class MYGAME_API UNaniteFoliageManager : public UWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// 전역 설정 적용
UFUNCTION(BlueprintCallable, Category = "NaniteFoliage")
void ApplyGlobalSettings(const FNaniteFoliageSettings& Settings);
// 바람 설정 업데이트
UFUNCTION(BlueprintCallable, Category = "NaniteFoliage")
void UpdateWindSettings(float Strength, FVector Direction);
// 특정 영역 폴리지 밀도 조절
UFUNCTION(BlueprintCallable, Category = "NaniteFoliage")
void SetAreaDensityScale(const FBox& Area, float DensityScale);
// 현재 렌더링 통계
UFUNCTION(BlueprintPure, Category = "NaniteFoliage")
FNaniteFoliageStats GetRenderingStats() const;
protected:
UPROPERTY()
FNaniteFoliageSettings CurrentSettings;
};
// NaniteFoliageManager.cpp
#include "NaniteFoliageManager.h"
#include "FoliageInstancedStaticMeshComponent.h"
void UNaniteFoliageManager::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// 기본 설정 로드
CurrentSettings = FNaniteFoliageSettings();
// INI 파일에서 설정 로드
GConfig->GetBool(
TEXT("NaniteFoliage"),
TEXT("bEnableNaniteFoliage"),
CurrentSettings.bEnableNaniteFoliage,
GEngineIni
);
UE_LOG(LogNaniteFoliage, Log,
TEXT("Nanite Foliage Manager 초기화: Enabled=%s"),
CurrentSettings.bEnableNaniteFoliage ? TEXT("true") : TEXT("false"));
}
void UNaniteFoliageManager::ApplyGlobalSettings(
const FNaniteFoliageSettings& Settings)
{
CurrentSettings = Settings;
// 콘솔 변수 설정
static IConsoleVariable* CVarNaniteFoliage =
IConsoleManager::Get().FindConsoleVariable(TEXT("r.Nanite.Foliage"));
if (CVarNaniteFoliage)
{
CVarNaniteFoliage->Set(Settings.bEnableNaniteFoliage ? 1 : 0);
}
// 바람 설정 적용
UpdateWindSettings(Settings.WindStrength, Settings.WindDirection);
// 모든 폴리지 컴포넌트에 설정 전파
for (TActorIterator<AInstancedFoliageActor> It(GetWorld()); It; ++It)
{
AInstancedFoliageActor* FoliageActor = *It;
for (UActorComponent* Component : FoliageActor->GetComponents())
{
if (UFoliageInstancedStaticMeshComponent* FoliageComp =
Cast<UFoliageInstancedStaticMeshComponent>(Component))
{
// Nanite 설정 적용
FoliageComp->SetCullDistance(0, Settings.MaxDrawDistance);
}
}
}
}
void UNaniteFoliageManager::UpdateWindSettings(
float Strength,
FVector Direction)
{
// Wind Directional Source 찾기
for (TActorIterator<AWindDirectionalSource> It(GetWorld()); It; ++It)
{
AWindDirectionalSource* WindSource = *It;
if (UWindDirectionalSourceComponent* WindComp = WindSource->GetComponent())
{
WindComp->SetStrength(Strength);
WindSource->SetActorRotation(Direction.Rotation());
}
}
// 머티리얼 파라미터 컬렉션 업데이트
if (UMaterialParameterCollection* WindMPC = WindParameterCollection.LoadSynchronous())
{
UMaterialParameterCollectionInstance* MPCI =
GetWorld()->GetParameterCollectionInstance(WindMPC);
if (MPCI)
{
MPCI->SetScalarParameterValue(FName("WindStrength"), Strength);
MPCI->SetVectorParameterValue(FName("WindDirection"),
FLinearColor(Direction.X, Direction.Y, Direction.Z));
}
}
}
void UNaniteFoliageManager::SetAreaDensityScale(
const FBox& Area,
float DensityScale)
{
// 영역 내 폴리지 인스턴스 밀도 조절
for (TActorIterator<AInstancedFoliageActor> It(GetWorld()); It; ++It)
{
AInstancedFoliageActor* FoliageActor = *It;
for (UActorComponent* Component : FoliageActor->GetComponents())
{
if (UFoliageInstancedStaticMeshComponent* FoliageComp =
Cast<UFoliageInstancedStaticMeshComponent>(Component))
{
// 영역 내 인스턴스 필터링
TArray<int32> InstancesInArea;
for (int32 i = 0; i < FoliageComp->GetInstanceCount(); ++i)
{
FTransform InstanceTransform;
if (FoliageComp->GetInstanceTransform(i, InstanceTransform, true))
{
if (Area.IsInside(InstanceTransform.GetLocation()))
{
InstancesInArea.Add(i);
}
}
}
// 밀도 스케일에 따라 인스턴스 가시성 조절
int32 TargetVisible = FMath::RoundToInt(
InstancesInArea.Num() * DensityScale);
// 간단한 구현: 일부 인스턴스 숨김
for (int32 i = TargetVisible; i < InstancesInArea.Num(); ++i)
{
// 실제 구현에서는 더 세련된 LOD 시스템 사용
}
}
}
}
}
Nanite Assembly 생성
복합 폴리지 에셋 구성
// NaniteTreeAssembly.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "NaniteTreeAssembly.generated.h"
/**
* Nanite Tree Assembly
* 나무의 여러 파트를 하나의 어셈블리로 관리
*/
USTRUCT(BlueprintType)
struct FNaniteTreePart
{
GENERATED_BODY()
// 파트 메시
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSoftObjectPtr<UStaticMesh> Mesh;
// 상대 위치
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FTransform RelativeTransform;
// 바람 영향도
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0.0", ClampMax = "1.0"))
float WindInfluence = 0.5f;
// 충돌 타입
UPROPERTY(EditAnywhere, BlueprintReadWrite)
ECollisionEnabled::Type CollisionType = ECollisionEnabled::QueryAndPhysics;
};
UCLASS(BlueprintType)
class MYGAME_API UNaniteTreeAssembly : public UDataAsset
{
GENERATED_BODY()
public:
// 트렁크 (줄기)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parts")
FNaniteTreePart Trunk;
// 가지들
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parts")
TArray<FNaniteTreePart> Branches;
// 잎 클러스터
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parts")
TArray<FNaniteTreePart> LeafClusters;
// 전체 스케일 범위
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Variation")
FVector2D ScaleRange = FVector2D(0.8f, 1.2f);
// 가지 수 범위
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Variation")
FIntPoint BranchCountRange = FIntPoint(3, 7);
// 잎 클러스터 수 범위
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Variation")
FIntPoint LeafClusterCountRange = FIntPoint(5, 15);
};
// NaniteTreeActor.h - 런타임 트리 액터
UCLASS()
class MYGAME_API ANaniteTreeActor : public AActor
{
GENERATED_BODY()
public:
ANaniteTreeActor();
// 어셈블리에서 트리 생성
UFUNCTION(BlueprintCallable, Category = "NaniteTree")
void GenerateFromAssembly(UNaniteTreeAssembly* Assembly, int32 Seed);
// 바람 반응 업데이트
UFUNCTION(BlueprintCallable, Category = "NaniteTree")
void UpdateWindResponse(float DeltaTime);
protected:
UPROPERTY(VisibleAnywhere)
TObjectPtr<USceneComponent> RootSceneComponent;
UPROPERTY()
TArray<TObjectPtr<UStaticMeshComponent>> PartComponents;
UPROPERTY()
TObjectPtr<UNaniteTreeAssembly> SourceAssembly;
virtual void Tick(float DeltaTime) override;
};
// NaniteTreeActor.cpp
#include "NaniteTreeActor.h"
ANaniteTreeActor::ANaniteTreeActor()
{
PrimaryActorTick.bCanEverTick = true;
RootSceneComponent = CreateDefaultSubobject<USceneComponent>(
TEXT("RootSceneComponent"));
SetRootComponent(RootSceneComponent);
}
void ANaniteTreeActor::GenerateFromAssembly(
UNaniteTreeAssembly* Assembly,
int32 Seed)
{
if (!Assembly) return;
SourceAssembly = Assembly;
FRandomStream RandomStream(Seed);
// 기존 파트 정리
for (UStaticMeshComponent* Comp : PartComponents)
{
if (Comp)
{
Comp->DestroyComponent();
}
}
PartComponents.Empty();
// 전체 스케일
float TotalScale = RandomStream.FRandRange(
Assembly->ScaleRange.X,
Assembly->ScaleRange.Y);
// 줄기 생성
if (UStaticMesh* TrunkMesh = Assembly->Trunk.Mesh.LoadSynchronous())
{
UStaticMeshComponent* TrunkComp = NewObject<UStaticMeshComponent>(this);
TrunkComp->SetupAttachment(RootSceneComponent);
TrunkComp->SetStaticMesh(TrunkMesh);
TrunkComp->SetRelativeTransform(Assembly->Trunk.RelativeTransform);
TrunkComp->SetRelativeScale3D(FVector(TotalScale));
TrunkComp->SetCollisionEnabled(Assembly->Trunk.CollisionType);
TrunkComp->RegisterComponent();
PartComponents.Add(TrunkComp);
}
// 가지 생성
int32 BranchCount = RandomStream.RandRange(
Assembly->BranchCountRange.X,
Assembly->BranchCountRange.Y);
BranchCount = FMath::Min(BranchCount, Assembly->Branches.Num());
for (int32 i = 0; i < BranchCount; ++i)
{
int32 BranchIndex = RandomStream.RandRange(0, Assembly->Branches.Num() - 1);
const FNaniteTreePart& BranchPart = Assembly->Branches[BranchIndex];
if (UStaticMesh* BranchMesh = BranchPart.Mesh.LoadSynchronous())
{
UStaticMeshComponent* BranchComp = NewObject<UStaticMeshComponent>(this);
BranchComp->SetupAttachment(PartComponents[0]); // 줄기에 부착
BranchComp->SetStaticMesh(BranchMesh);
// 랜덤 회전 적용
FTransform BranchTransform = BranchPart.RelativeTransform;
FRotator RandomRotation(
0,
RandomStream.FRandRange(0.0f, 360.0f),
0
);
BranchTransform.SetRotation(
(BranchTransform.Rotator() + RandomRotation).Quaternion());
BranchComp->SetRelativeTransform(BranchTransform);
BranchComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
BranchComp->RegisterComponent();
PartComponents.Add(BranchComp);
}
}
// 잎 클러스터 생성 (유사한 패턴)
// ...
}
void ANaniteTreeActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (SourceAssembly)
{
UpdateWindResponse(DeltaTime);
}
}
void ANaniteTreeActor::UpdateWindResponse(float DeltaTime)
{
// 머티리얼 파라미터 컬렉션에서 바람 데이터 가져오기
// 실제 구현에서는 WPO(World Position Offset)나
// Skeletal 애니메이션 사용
float Time = GetWorld()->GetTimeSeconds();
float WindStrength = 0.5f; // 실제로는 바람 시스템에서 가져옴
// 가지와 잎에 미세한 흔들림 적용
for (int32 i = 1; i < PartComponents.Num(); ++i) // 줄기 제외
{
if (UStaticMeshComponent* Comp = PartComponents[i])
{
FRotator CurrentRotation = Comp->GetRelativeRotation();
// 사인파 기반 흔들림
float Sway = FMath::Sin(Time * 2.0f + i) * WindStrength * 5.0f;
CurrentRotation.Pitch += Sway * DeltaTime * 10.0f;
CurrentRotation.Yaw += Sway * 0.5f * DeltaTime * 10.0f;
Comp->SetRelativeRotation(CurrentRotation);
}
}
}
Nanite Foliage 최적화 전략
LOD 설정, Cull Distance, Overdraw 감소 기법
Nanite Foliage는 자동 LOD를 제공하지만, 대규모 오픈월드에서 수십만 인스턴스를 효율적으로 렌더링하려면 추가적인 최적화 전략이 필수적입니다. GPU 메모리, 렌더링 시간, 오버드로우를 균형 있게 관리해야 합니다.
LOD 바이어스와 Cull Distance 설정
// NaniteFoliageOptimizer.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "NaniteFoliageOptimizer.generated.h"
/**
* Nanite Foliage 최적화 컴포넌트
* 폴리지 타입별 LOD/컬링 전략 관리
*/
USTRUCT(BlueprintType)
struct FFoliageCullSettings
{
GENERATED_BODY()
// 시작 페이드 아웃 거리
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float StartCullDistance = 50000.0f;
// 완전히 컬링되는 거리
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float EndCullDistance = 80000.0f;
// Nanite LOD 바이어스 (-2 ~ 2, 음수=고품질)
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "-2.0", ClampMax = "2.0"))
float NaniteLODBias = 0.0f;
// 그림자 캐스팅 거리
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float ShadowCullDistance = 30000.0f;
};
UCLASS(ClassGroup=(Rendering), meta=(BlueprintSpawnableComponent))
class MYGAME_API UNaniteFoliageOptimizer : public UActorComponent
{
GENERATED_BODY()
public:
// 폴리지 타입별 컬 설정
UPROPERTY(EditAnywhere, Category = "Optimization")
TMap<FName, FFoliageCullSettings> FoliageTypeSettings;
// 전체 폴리지에 최적화 설정 적용
UFUNCTION(BlueprintCallable, Category = "Optimization")
void ApplyOptimizationSettings();
// 오버드로우 분석 결과 기반 자동 최적화
UFUNCTION(BlueprintCallable, Category = "Optimization")
void AutoOptimizeByOverdraw(float TargetOverdrawRatio = 2.5f);
};
Overdraw 감소 전략
// NaniteFoliageOptimizer.cpp (발췌)
void UNaniteFoliageOptimizer::ApplyOptimizationSettings()
{
for (TActorIterator<AInstancedFoliageActor> It(GetWorld()); It; ++It)
{
for (UActorComponent* Comp : (*It)->GetComponents())
{
UFoliageInstancedStaticMeshComponent* FoliageComp =
Cast<UFoliageInstancedStaticMeshComponent>(Comp);
if (!FoliageComp) continue;
FName FoliageTypeName = FoliageComp->GetStaticMesh()->GetFName();
FFoliageCullSettings* Settings = FoliageTypeSettings.Find(FoliageTypeName);
if (!Settings) continue;
// Cull Distance 적용
FoliageComp->SetCullDistances(
Settings->StartCullDistance,
Settings->EndCullDistance);
// 그림자 설정 - 먼 거리에서는 그림자 비활성화
FoliageComp->SetCastShadow(true);
FoliageComp->LDMaxDrawDistance = Settings->ShadowCullDistance;
// Nanite LOD 바이어스 - 큰 나무는 고품질, 풀은 저품질
if (UStaticMesh* Mesh = FoliageComp->GetStaticMesh())
{
Mesh->NaniteFallbackRelativeError = Settings->NaniteLODBias;
}
}
}
UE_LOG(LogNaniteFoliage, Log,
TEXT("Foliage 최적화 설정 적용 완료: %d 타입"),
FoliageTypeSettings.Num());
}
void UNaniteFoliageOptimizer::AutoOptimizeByOverdraw(
float TargetOverdrawRatio)
{
// Nanite 통계에서 현재 오버드로우 비율 확인
float CurrentOverdraw = 0.0f;
// r.Nanite.Stats 콘솔 변수로 통계 가져오기
static IConsoleVariable* CVarNaniteStats =
IConsoleManager::Get().FindConsoleVariable(
TEXT("r.Nanite.MaxPixelsPerEdge"));
if (CurrentOverdraw > TargetOverdrawRatio)
{
// 오버드로우가 높으면 LOD 바이어스를 올려 폴리곤 수 감소
for (auto& Pair : FoliageTypeSettings)
{
Pair.Value.NaniteLODBias = FMath::Min(
Pair.Value.NaniteLODBias + 0.5f, 2.0f);
// 컬 거리도 약간 줄임
Pair.Value.EndCullDistance *= 0.9f;
}
// 변경사항 재적용
ApplyOptimizationSettings();
}
}
- 대형 나무: LOD Bias -1.0, Cull Distance 100,000, Shadow Distance 50,000
- 중형 관목: LOD Bias 0.0, Cull Distance 50,000, Shadow Distance 20,000
- 풀/지피 식물: LOD Bias 1.0, Cull Distance 20,000, Shadow 비활성화
- 꽃/장식물: LOD Bias 0.5, Cull Distance 30,000, Shadow Distance 10,000
에디터에서 ViewMode Overdraw (단축키: Alt+5) 또는 콘솔 명령어 r.Nanite.Visualize.Overdraw 1을 사용하여 오버드로우를 시각화하세요. 빨간색 영역이 많으면 밀도를 줄이거나 Cull Distance를 조정해야 합니다.
핵심 요약
- Nanite Foliage는 UE 5.7의 실험적 기능으로 LOD 없이 고밀도 식물 렌더링
- 3가지 핵심 시스템: Nanite Assemblies, Nanite Skinning, Nanite Voxels
- 크로스페이드/팝핑 없음으로 매끄러운 LOD 전환
- 60 FPS 목표로 밀집된 폴리지 렌더링 가능
- 현재 물리, 충돌, 바람 애니메이션 제한 - 프로덕션 사용 시 주의
도전 과제
배운 내용을 직접 실습해보세요
Foliage 메시에 Nanite를 활성화하고, 수만 그루의 나무/풀을 배치하세요. 기존 LOD 기반 Foliage와 Nanite Foliage의 비주얼 품질과 성능을 비교하세요.
Nanite Foliage에서 바람에 흔들리는 WPO 애니메이션을 설정하세요. r.Nanite.AllowWorldPositionOffset을 활성화하고, WPO의 GPU 비용 증가를 프로파일링하세요.
Foliage 인스턴스 수, Nanite 클러스터 크기, Culling 거리를 조합하여 수십만 인스턴스의 Foliage를 최적화하세요. GPU 메모리와 렌더링 시간의 균형점을 찾으세요.