Can't see my custom Niagara Data Interface in "Add User Parameter → Data Interface" menu despite proper setup and registration

Hey everyone, I’m trying to create a custom Niagara Data Interface in UE5 for sampling wind data from a custom UWindVectorField class. I’ve followed all the usual steps, but the data interface is not showing up in the “Add User Parameter → Data Interface” dropdown in Niagara.

Basically, what I am trying to do is show my wind simulation field using Niagara particles like Peter Sikachev is doing in this Youtube video: https://youtu.be/HTlALlfz_T0?t=841

Here’s what I’ve done so far:

Created two files:

  • NiagaraWindFieldDataInterface.h:
  • NiagaraWindFieldDataInterface.cpp:

What these files include:

  • NiagaraWindFieldDataInterface.h:
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "NiagaraDataInterface.h"
#include "WindVectorField.h"
#include "NiagaraWindFieldDataInterface.generated.h"

UCLASS(EditInlineNew, BlueprintType, Category = "Wind", meta = (DisplayName = "Wind Field"))
class EMBERFLIGHT_API UNiagaraWindFieldDataInterface : public UNiagaraDataInterface
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, Category="Wind")
    FVector GetZeroWind() const { return FVector::ZeroVector; }

    UNiagaraWindFieldDataInterface();

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Wind")
    TObjectPtr<UWindVectorField> WindField;
    virtual void GetFunctions(TArray<FNiagaraFunctionSignature>& OutFunctions) override;
    virtual void GetVMExternalFunction(const FVMExternalFunctionBindingInfo& BindingInfo, void* InstanceData, FVMExternalFunction& OutFunc) override;
    virtual bool Equals(const UNiagaraDataInterface* Other) const override;
    virtual bool CanExecuteOnTarget(ENiagaraSimTarget Target) const override;
};
  • NiagaraWindFieldDataInterface.cpp:
// Fill out your copyright notice in the Description page of Project Settings.


#include "NiagaraWindFieldDataInterface.h"
#include "NiagaraTypes.h"
#include "NiagaraModule.h"
#include "NiagarafunctionLibrary.h"
#include "Modules/ModuleManager.h"

#define LOCTEXT_NAMESPACE "NiagaraWindFieldDI"

UNiagaraWindFieldDataInterface::UNiagaraWindFieldDataInterface() {}

struct FSampleWindAtLocation
{
    static void Exec(FVectorVMExternalFunctionContext& Context, const UWindVectorField* WindField)
    {
        VectorVM::FUserPtrHandler<const UWindVectorField> FieldHandler(Context);
        VectorVM::FExternalFuncInputHandler<float> X(Context);
        VectorVM::FExternalFuncInputHandler<float> Y(Context);
        VectorVM::FExternalFuncInputHandler<float> Z(Context);

        VectorVM::FExternalFuncRegisterHandler<float> OutX(Context);
        VectorVM::FExternalFuncRegisterHandler<float> OutY(Context);
        VectorVM::FExternalFuncRegisterHandler<float> OutZ(Context);

        for (int32 i = 0; i < Context.GetNumInstances(); ++i)
        {
            FVector WorldPos(X.GetAndAdvance(), Y.GetAndAdvance(), Z.GetAndAdvance());
            FVector Velocity = WindField ? WindField->SampleWindAtPosition(WorldPos) : FVector::ZeroVector;

            *OutX.GetDestAndAdvance() = Velocity.X;
            *OutY.GetDestAndAdvance() = Velocity.Y;
            *OutZ.GetDestAndAdvance() = Velocity.Z;
        }
    }
};

void UNiagaraWindFieldDataInterface::GetFunctions(TArray<FNiagaraFunctionSignature>& OutFunctions)
{
    FNiagaraFunctionSignature Sig;
    Sig.Name = FName(TEXT("SampleWindAtLocation"));
    Sig.Inputs.Add(FNiagaraVariable(FNiagaraTypeDefinition(GetClass()), TEXT("Wind Field")));
    Sig.Inputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("X")));
    Sig.Inputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("Y")));
    Sig.Inputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("Z")));

    Sig.Outputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("OutX")));
    Sig.Outputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("OutY")));
    Sig.Outputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("OutZ")));

    Sig.SetDescription(LOCTEXT("SampleWindDesc", "Sample wind velocity at a given world position"));
    Sig.bMemberFunction = true;

    OutFunctions.Add(Sig);
}

void UNiagaraWindFieldDataInterface::GetVMExternalFunction(const FVMExternalFunctionBindingInfo& BindingInfo, void* InstanceData, FVMExternalFunction& OutFunc)
{
    if (BindingInfo.Name == TEXT("SampleWindAtLocation"))
    {
        OutFunc = FVMExternalFunction::CreateLambda([this](FVectorVMExternalFunctionContext& Context)
            {
                FSampleWindAtLocation::Exec(Context, WindField);
            });
    }
}

bool UNiagaraWindFieldDataInterface::Equals(const UNiagaraDataInterface* Other) const
{
    const UNiagaraWindFieldDataInterface* OtherTyped = CastChecked<UNiagaraWindFieldDataInterface>(Other);
    return OtherTyped && OtherTyped->WindField == WindField;
}

bool UNiagaraWindFieldDataInterface::CanExecuteOnTarget(ENiagaraSimTarget Target) const
{
    return true;
}
#undef LOCTEXT_NAMESPACE

Some explanation for the code:

  • Added a dummy UFUNCTION (FVector GetZeroWind()) marked as BlueprintCallable, just to ensure it’s exposed for reflection.

  • The WindField is exposed via:

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = “Wind”)

    TObjectPtr WindField;

Just to be sure that the data interface is registered successfully, I have also tried to list it in the console upon begin play:

My_Game.cpp (this is the game module):

include "EmberFlight.h"
#include "NiagaraDataInterface.h"
#include "NiagaraWindFieldDataInterface.h"
#include "NiagaraDataInterface.h"
#include "UObject/UObjectIterator.h"
#include "Modules/ModuleManager.h"

IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, EmberFlight, "EmberFlight" );

void FEmberFlightModule::StartupModule()
{
    UE_LOG(LogTemp, Warning, TEXT("Wind DI class is: %s"), *UNiagaraWindFieldDataInterface::StaticClass()->GetName());
}

void FEmberFlightModule::ShutdownModule()
{
    
}

void ListAllNiagaraInterfaces()
{
    static const FName TempClassName = UNiagaraWindFieldDataInterface::StaticClass()->GetFName();

    for (TObjectIterator<UClass> It; It; ++It)
    {
        if (It->IsChildOf(UNiagaraDataInterface::StaticClass()) && !It->HasAnyClassFlags(CLASS_Abstract))
        {
            UE_LOG(LogTemp, Warning, TEXT("Found DI: %s"), *It->GetName());
        }
    }
}

My_Game_GameMode.cpp:

// Copyright Epic Games, Inc. All Rights Reserved.

#include "EmberFlightGameMode.h"
#include "EmberFlight.h"
#include "UObject/ConstructorHelpers.h"

AEmberFlightGameMode::AEmberFlightGameMode()
{
	// set default pawn class to our Blueprinted character
	static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("Blueprint'/Game/Blueprints/BP_PhoenixCharacter.BP_PhoenixCharacter'"));
	if (PlayerPawnBPClass.Class != NULL)
	{
		DefaultPawnClass = PlayerPawnBPClass.Class;
	}
}

void AEmberFlightGameMode::BeginPlay()
{
    Super::BeginPlay();
    ListAllNiagaraInterfaces();

    // Initialize Wind Field
    WindFieldInstance = NewObject<UWindVectorField>(this);
    if (WindFieldInstance)
    {
        WindFieldInstance->Initialize(30, 30, 30, 100.0f);
    }
}

Console Log Image with registered wind field interface: https://imgur.com/a/16aRI24
Add User parameters → Data Interface → (custom wind field date interface not appearing): https://imgur.com/a/XJSe4K7