How to add foliage (in the editor) through C++ and have it recognized by Foliage Mode?

Hey, guys. I’m using Unreal Engine 5.3.2 and need some help writing C++ code that runs in the editor.

Specifically, I’ve been trying to write some C++ code to help me populate my level with tree foliage. The current workflow I have is this: I use the Foliage Mode tools to paint ground foliage, rocks, boulders and tree trunks; the tree foliage (which is connected to tree trunks) is added using C++ code I wrote. The gross functionality is working, I can add a new UFoliageInstancedStaticMeshComponent to the AInstancedFoliageActor and I manage to create the instances with no issues.

The problem is, however, that the Foliage ISM Component is not picked up by the Foliage Mode.

When I switch back to Foliage Mode after running my code and select all foliage, I get something like this:

Notice the Foliage branches are not highlighted, even though I just hit ALL from the Foliage Mode panel. The Static Mesh Foliage (UFoliageType_InstancedStaticMesh) appears in the level list when Foliage Mode is active, but no instances are counted.

For the purposes of gameplay, this doesn’t really matter. But when I use the world partition commandlet to change foliage cell size, for example, the fact the tree foliage is not recognized by the foliage mode matters a lot: it disappears. This is a problem because I’m using a Foliage Cell size of 256m for working in the editor, for performance, but when actually running the level I want to use a Foliage Cell size of 32m, also for performance.

Working with a foliage cell size of 32m in the editor is not feasible: for a 16 square km landscape, so many AInstancedFoliageActors are created that the editor can take ages to load and unload them all. Conversely (when actually running the level) having the Foliage Cell size of 256m nets me ~40FPS, but at 32m I get 60+ FPS.

The code I’m running is this:

UFoliageInstancedStaticMeshComponent* NewFoliageComponent = NewObject<UFoliageInstancedStaticMeshComponent>(Actor, UFoliageInstancedStaticMeshComponent::StaticClass());
NewFoliageComponent->OnComponentCreated();
NewFoliageComponent->AttachToComponent(Actor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
NewFoliageComponent->RegisterComponent();
Actor->AddInstanceComponent(NewFoliageComponent);

NewFoliageComponent->SetStaticMesh(Mesh);
this->ProcessNewTreeFoliageComponent(NewFoliageComponent);

this->RegisterFoliageTypeToActor(Cast<AInstancedFoliageActor>(Actor), /*FoliageType*/ Entry.Value.Get<1>());
TreeFoliageComponentMap.Add(Mesh->GetName(), NewFoliageComponent);

ProcessNewTreeFoliageComponent is a function for setting a bunch of parameters on the Foliage Component; so that nothing is left unsaid, it does this:

void AFoliageHandler::ProcessNewTreeFoliageComponent(UFoliageInstancedStaticMeshComponent* NewFoliageComponent) {
    NewFoliageComponent->SetMobility(EComponentMobility::Type::Movable);
    NewFoliageComponent->SetGenerateOverlapEvents(true);
    NewFoliageComponent->SetCollisionProfileName(FName("Custom..."), true);
    NewFoliageComponent->SetCollisionEnabled(ECollisionEnabled::Type::QueryOnly);
    NewFoliageComponent->SetCollisionObjectType(this->FoliageCollisionChannel);
    NewFoliageComponent->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Overlap);
    NewFoliageComponent->bMultiBodyOverlap = true;
    NewFoliageComponent->SetNumCustomDataFloats(1);
    NewFoliageComponent->bEnableAutoLODGeneration = true;
    NewFoliageComponent->HLODBatchingPolicy = EHLODBatchingPolicy::MeshSection;
    NewFoliageComponent->CastShadow = true;
    NewFoliageComponent->bAffectDynamicIndirectLighting = true;
    NewFoliageComponent->bAffectDistanceFieldLighting = true;
    NewFoliageComponent->bCastDynamicShadow = true;
    NewFoliageComponent->bCastStaticShadow = true;
    NewFoliageComponent->bCastContactShadow = true;
    NewFoliageComponent->bHasCustomNavigableGeometry = EHasCustomNavigableGeometry::No;
    NewFoliageComponent->bUseAsOccluder = true;
    NewFoliageComponent->bVisibleInRayTracing = true;
    NewFoliageComponent->SetEvaluateWorldPositionOffset(true);
    NewFoliageComponent->SetEvaluateWorldPositionOffsetInRayTracing(true);
    NewFoliageComponent->WorldPositionOffsetDisableDistance = 2500;
    if (!NewFoliageComponent->ComponentHasTag(FName("TreeFoliage"))) NewFoliageComponent->ComponentTags.Add(FName("TreeFoliage"));
}

The function that actually tries to do what I’m struggling with is RegisterFoliageTypeToActor, it is this:

void AFoliageHandler::RegisterFoliageTypeToActor(AInstancedFoliageActor* FoliageActor, UFoliageType_InstancedStaticMesh* FoliageType)
{
    if (!FoliageActor || !FoliageType)
    {
        if (this->debug) UE_LOG(LogTemp, Warning, TEXT("FoliageActor or FoliageType is null!"));
        return;
    }

    // Register the FoliageType with the actor
    bool shouldRegister = FoliageActor->ForEachFoliageInfo(
        [&](UFoliageType* ExistingType, FFoliageInfo& Info)
        {
			UFoliageType_InstancedStaticMesh* ExistingFoliageType = Cast<UFoliageType_InstancedStaticMesh>(ExistingType);
            if (ExistingFoliageType == FoliageType)
            {
                if (this->debug) UE_LOG(LogTemp, Display, TEXT("FoliageType already registered."));
                return false; // Stop if already registered
            }

            return true;
        }
    );

    if (shouldRegister) {
        // Create a FoliageInfo for the new FoliageType
        FFoliageInfo* NewFoliageInfoPtr = new FFoliageInfo();
        NewFoliageInfoPtr->CreateImplementation(FoliageType);
        NewFoliageInfoPtr->IFA = FoliageActor;
        NewFoliageInfoPtr->Initialize(FoliageType);

        // Add the new FoliageType if not present
        FoliageActor->AddFoliageType(FoliageType, &NewFoliageInfoPtr);
        if (this->debug) UE_LOG(LogTemp, Display, TEXT("Registered new FoliageType: %s"), *FoliageType->GetName());
    }
}

The code that adds instances is called later, after all Foliage ISM Components are created/processed. The snippet responsible for that is this:

UFoliageInstancedStaticMeshComponent* FoliageComponent = TreeFoliageComponentMap[Entry.Key];
FTransform TrunkTransform;
Trunk->GetInstanceTransform(i, TrunkTransform, false);
FTransform FinalTransform = Entry.Value * TrunkTransform;
const int32 instanceIndex = FoliageComponent->AddInstance(FinalTransform);
FoliageComponent->SetCustomDataValue(instanceIndex, 0, FMath::FRand());

Has anyone tried this before? Am I missing something or is it not possible to do what I’m trying to do in version 5.3.2?

Any help is appreciated, thanks in advance!

In case anyone needs the answer, the issue was extra code is necessary to register the instance to the Foliage Mode. I created two new functions:

// Helper function to find UFoliageType & FFoliageInfo for a given UFoliageInstancedStaticMeshComponent in a specific AInstancedFoliageActor
TTuple<UFoliageType*, FFoliageInfo*> AFoliageHandler::FindFoliageInfoForComponent(AInstancedFoliageActor* FoliageActor, UFoliageInstancedStaticMeshComponent* FoliageComponent)
{
    TTuple<UFoliageType*, FFoliageInfo*> FoundInfo = TTuple<UFoliageType*, FFoliageInfo*>(nullptr, nullptr);

    if (!FoliageActor || !FoliageComponent)
    {}
    else
    {
        FoliageActor->ForEachFoliageInfo(
            [&](UFoliageType* FoliageType, FFoliageInfo& FoliageInfo)
            {
                if (Cast<UObject>(FoliageComponent->GetStaticMesh()) == FoliageType->GetSource())
                {
                    FoundInfo = TTuple<UFoliageType*, FFoliageInfo*>(FoliageType, &FoliageInfo);
                    return false; // Stop searching
                }

                return true;
            }
        );
    }

    return FoundInfo;
}
// Usage example: Register a new instance to the correct UFoliageType & FFoliageInfo
void AFoliageHandler::RegisterNewFoliageInstance(AInstancedFoliageActor* FoliageActor, UFoliageInstancedStaticMeshComponent* FoliageComponent, const FFoliageInstance& NewInstance)
{
    TTuple<UFoliageType*, FFoliageInfo*> FoliageData = FindFoliageInfoForComponent(FoliageActor, FoliageComponent);
	UFoliageType* FoliageType = FoliageData.Get<0>();
	FFoliageInfo* FoliageInfo = FoliageData.Get<1>();

    if (FoliageType && FoliageInfo)
    {
        FoliageInfo->AddInstance(FoliageType, NewInstance, FoliageComponent);
    }
}

Given the instance creation snippet shown above, change it to the following:

UFoliageInstancedStaticMeshComponent* FoliageComponent = TreeFoliageComponentMap[Entry.Key];
FTransform TrunkTransform;
Trunk->GetInstanceTransform(i, TrunkTransform, true);
FTransform FinalTransform = Entry.Value * TrunkTransform;
FFoliageInstance Instance = FFoliageInstance();
Instance.Location = FinalTransform.GetLocation();
Instance.Rotation = FinalTransform.GetRotation().Rotator();
Instance.DrawScale3D = FVector3f(FinalTransform.GetScale3D());
RegisterNewFoliageInstance(Cast<AInstancedFoliageActor>(Trunk->GetOwner()), FoliageComponent, Instance);

I couldn’t get around the fact that FFoliageInfo::AddInstance creates the component by itself if the component doesn’t have the UFoliageType and FFoliageInfo correctly set up. This made me pivot my approach: I stopped processing tree foliage components (calling this->ProcessNewTreeFoliageComponent(NewFoliageComponent); ) at component creation and/or iteration and now do it after the instance creation snippet runs. Lastly, if I had components that were previously created without registering the UFoliageType and FFoliageInfo correctly, I remove them from the Actor and destroy the component, so duplicates with the same Mesh aren’t around with no instances.

Custom Data floats are now generated during processing. ProcessNewTreeFoliageComponent becomes ProcessExistingTreeFoliageComponent:

void AFoliageHandler::ProcessExistingTreeFoliageComponent(UFoliageInstancedStaticMeshComponent* FoliageComponent) {
    FoliageComponent->SetMobility(EComponentMobility::Type::Movable);
    FoliageComponent->SetGenerateOverlapEvents(true);
    FoliageComponent->SetCollisionProfileName(FName("Custom..."), true);
    FoliageComponent->SetCollisionEnabled(ECollisionEnabled::Type::QueryOnly);
    FoliageComponent->SetCollisionObjectType(this->FoliageCollisionChannel);
    FoliageComponent->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Overlap);
    FoliageComponent->bMultiBodyOverlap = true;
    if (FoliageComponent->NumCustomDataFloats != 1) FoliageComponent->SetNumCustomDataFloats(1);
    FoliageComponent->bEnableAutoLODGeneration = true;
    FoliageComponent->HLODBatchingPolicy = EHLODBatchingPolicy::MeshSection;
    FoliageComponent->CastShadow = true;
    FoliageComponent->bAffectDynamicIndirectLighting = true;
    FoliageComponent->bAffectDistanceFieldLighting = true;
    FoliageComponent->bCastDynamicShadow = true;
    FoliageComponent->bCastStaticShadow = true;
    FoliageComponent->bCastContactShadow = true;
    FoliageComponent->bHasCustomNavigableGeometry = EHasCustomNavigableGeometry::No;
    FoliageComponent->bUseAsOccluder = true;
    FoliageComponent->bVisibleInRayTracing = true;
    FoliageComponent->SetEvaluateWorldPositionOffset(true);
    FoliageComponent->SetEvaluateWorldPositionOffsetInRayTracing(true);
    FoliageComponent->WorldPositionOffsetDisableDistance = 2500;
    if (!FoliageComponent->ComponentHasTag(FName("TreeFoliage"))) FoliageComponent->ComponentTags.Add(FName("TreeFoliage"));
    for (int i = 0; i < FoliageComponent->GetInstanceCount(); i++) {
        FoliageComponent->SetCustomDataValue(i, 0, FMath::FRand());
    }
}