I recently had an issue where my Niagara systems seemingly wouldn’t spawn occasionally in Unreal Engine 5. I noticed that the particles were visible only when I was close to or looking towards the origin. It turned out that the systems were fine but the particles were being culled.

My systems were being spawned by a Niagara Data Channel using the “Niagara Data Channel Islands” type.

The data channel configuration

The solution lay with the FNiagaraDataChannelSearchParameters object that you pass to UNiagaraDataChannelLibrary::WriteToNiagaraDataChannel when creating a channel writing. You must either set the Location or OwningComponent member variables so the emitter is spawned in the correct island.

To learn more about Niagara Data Channels, I recommend viewing the following:

Full Explanation

Niagara Data Channels let you efficiently spawn systems throughout your world. They’re especially useful for effects such as spark from bullet collisions.

Epic recommends using the “Islands” data channel type as opposed to the “Global” type. The islands type breaks the world up into adjacent cubes. The idea is that systems spawned in islands far from the player won’t be rendered and thus their cost is significantly reduced.

As in the figure above, you specify the data the channel accepts. There is also a field for the emitter systems that the channel supports.

To write data to a channel you must create a writer using UNiagaraDataChannelLibrary::WriteToNiagaraDataChannel from NiagaraDataChannel.h. I’ve included the signature below with the important parameters annotated.

static UNiagaraDataChannelWriter * CreateDataChannelWriter ( 
    // The world to spawn the systems in
    const UObject* WorldContextObject,
    // The data channel asset to write to
    const UNiagaraDataChannelAsset* Channel,
    // To be explained
    FNiagaraDataChannelSearchParameters SearchParams,
    // The number of systems to spawn
    int32 Count,
    bool bVisibleToGame,
    bool bVisibleToCPU,
    bool bVisibleToGPU,
    const FString& DebugSource
);

The default constructed search parameters uses FVector::ZeroVector for Location, meaning your Niagara system will be spawned in the island there. Once leave this island, your particles will be culled unless you have line of sight to this island.

The FNiagaraDataChannelSearchParameters struct has three member variables:

  • bool bOverrideLocation
  • FVector Location
  • TObjectPtr<USceneComponent> OwningComponent

By setting OwningComponent or Location with bOverrideLocation, we can tell the writer what island to spawn the system in. In most cases I recommend setting the owning component to the player as it’s what will be viewing the particles.

Creating and using a Niagara Data Channel writer

The following is a short example of using the writer with my UNiagaraNdcWriterSubsystem class. The full source code can be found here.

The subsystem stores a FNiagaraDataChannelSearchParameters member variable called search_parameters_. When the world begins (OnWorldBeginPlay()), I set the search_parameters_ owning component to the player’s root component.

void UNiagaraNdcWriterSubsystem::OnWorldBeginPlay(UWorld& world) {
    Super::OnWorldBeginPlay(world);
    FCoreDelegates::OnEndFrame.AddUObject(this, &UNiagaraNdcWriterSubsystem::flush_ndc_writes);
    update_owning_component(world);
}

You can get the active player with UGameplayStatics::GetPlayerCharacter from Kismet/GameplayStatics.h.

void UNiagaraNdcWriterSubsystem::update_owning_component(UWorld& world) {
    if (auto* character{UGameplayStatics::GetPlayerCharacter(&world, 0)}) {
        search_parameters_.OwningComponent = character->GetRootComponent();
    }
}

I wrote a small helper function to create the writer.

auto UNiagaraNdcWriterSubsystem::create_data_channel_writer(UWorld& world, NdcAsset& asset, int32 n)
    -> NdcWriter* {
    constexpr bool visible_to_game{false};
    constexpr bool visible_to_cpu{true};
    constexpr bool visible_to_gpu{true};

    auto* writer{UNiagaraDataChannelLibrary::WriteToNiagaraDataChannel(
        &world,
        &asset,
        search_parameters_,
        n,
        visible_to_game,
        visible_to_cpu,
        visible_to_gpu,
        writer_debug_source
    )};
    return writer;
}

The code below is a summarised version of how the subsystem writes to the data channel. Simply open the writer and use the WriteX member functions to write your data to the channel

static auto const position_label{FName("position")};
static auto const rotation_label{FName("rotation")};

auto const n_systems{ /* get n emitters... */ };
auto* writer{create_data_channel_writer(*world, asset, n_systems)};

for (int32 i{0}; i < n_systems; ++i) {
    writer->WritePosition(position_label, i, locations[i]);
    writer->WriteVector(rotation_label, i, rotations[i]);
}

Conclusion

This post details an issue where Niagara particles were being culled in Unreal Engine 5 when using Niagara Data Channel assets. The issue was due to the channel’s search parameters being left blank when the channel writer was opened. Setting the search parameters to spawn the Niagara system in the data channel island near the player will solve the culling issue.