How to pass a Texture to a .usf file through Niagara ? (UE5)

Hello,

I’m struggling on a specific thing I would like to do with Niagara.

Currently, I have a pretty long HLSL code in CustomHLSL node in Niagara. It works well. However, it’s a bit tricky to maintain and to work with, so I would like to have it in a .usf file, wrap my code in a function in that file and call that function in a Niagara CustomHLSL.

I’ve created and linked a usf to my Niagara system. However, my code samples textures, and it also computes their UVs, so I need to pass it the textures and do the sampling inside the function body. Indeed, I can’t use the Niagara node SampleTexture2D from a TextureSample for example, because I don’t know the UVs before executing the function.

Anyway, please see a simple example of what I try to do, with arbitrary uvs.
My question is: how can I access “TestTexture” from Niagara input inside my HLSL function ?
For example, if I want to pass it as a parameter of my function Test(), what should be the type of that texture ? Do I need to explicitly declare a sampler ?
Alternatively, is there a way to assign this “TestTexture” to a global shader parameter Texture2D declared in my hlsl ?

Thanks a lot!

Hello ! I’m writing back because I found a solution for my issue, and I believe it’s the “correct” way to do it. So if anyone, at some point, has a similar question, here is the beginning of an answer.

The solution is to extend the C++ class UNiagaraDataInterface, by creating your own DataInterface inheriting from this class. Indeed, the Niagara Data Interfaces are a powerful and modular tool that Epic uses to extend Niagara features.

For example, by “default” (ie Niagara without any Data Interfaces), Niagara doesn’t seem to be able to sample a texture (in the HLSL way), because it requires a Texture2D and a SamplerState. Indeed, it seems there is no way inside Niagara to extract the texture RHI from a UTexture2D (so a UObject, a CPU-side representation) to pass it to a shader.
That is why Epic built, for that case, a UNiagaraDataInterfaceTexture class, that you use through “TextureSample” inside Niagara. This Data Interface allows you to specify a UTexture2D (CPU representation) and will dispatch a compute shader with the corresponding Texture2D and SamplerState (HLSL types). The shader parameters looks like this :

	BEGIN_SHADER_PARAMETER_STRUCT(FShaderParameters, )
		SHADER_PARAMETER(FIntPoint,				TextureSize)
		SHADER_PARAMETER(int32,					MipLevels)
		SHADER_PARAMETER_RDG_TEXTURE(Texture2D,	Texture)
		SHADER_PARAMETER_SAMPLER(SamplerState,	TextureSampler)
	END_SHADER_PARAMETER_STRUCT()

(implementation can be found in Engine/Plugins/FX/Niagara/Source/Niagara/Classes)

The bounded compute shader is NiagaraDataInterfaceTexture.ush, and is a bit particular at first (at least for me). Here is how it looks like:

Texture2D		{ParameterName}_Texture;
SamplerState	{ParameterName}_TextureSampler;

//...

void SampleTexture2D_{ParameterName}(in float2 UV, in float MipLevel, out float4 OutValue)
{
	OutValue = {ParameterName}_Texture.SampleLevel({ParameterName}_TextureSampler, UV, MipLevel);
}

SampleTexture2D here is the node you can find inside the Niagara editor.

This " {ParameterName} " was a bit confusing to me, but what it means is that this .ush defines a template that would execute some code. And when you create a new Data Interface inside your Niagara system, you also create an instance of this template (you could imagine that {ParameterName} becomes the name of your instance DI, and -it’s a guess- it generates a new shader file with a more standard HLSL code by replacing this {ParameterName} ).

In the end, it means that you can declare any amount of UTexture (or something else) in your custom Data Interface, bind them to the corresponding shader parameters in your custom shader file (ush) and you could execute your custom code on these textures in a more traditional HLSL style, without relying on the CustomHlsl node from Niagara (which is not always the most convinient tool if you have a lot of code), and harvest the result directly inside your Niagara system. (A traditional compute shader would require to pass the output to the Niagara system through the CPU I believe, which is a lot less efficient)

Another example called MousePosition data interface, with some comments, is provided by Epic in the engine, as a plugin called “Niagara Example Custom DataInterface” or at this location : Engine/Plugins/FX/ExampleCustomDataInterface.

So yes, it was the way to go for me, and I hope this post could help if you try to implement your own custom DataInterface !

3 Likes

Thank you for that follow up!
Super interesting, will bookmark this and dive in at some point because that unlocks even more possibilities!