Decal atlas with one material - is it possible?

I need a lot of decals in the scene and I can use a single texture for all of them, using atlas, inside a single material. But there is no way to select atlas row/column in the Decal Component. The only way is to create a new material instance for every different decal, which, if I’m not mistaken, will add 1 extra drawcall for every unique decal.
I’ve though about setting custom primitive data for each decal component to control its UVs (to select atlas row & column), but it’s not a Primitive Component. Any ideas?

Same problem/scenario for me and very curious about that too! I also want to use a single texture atlas (1 material) for decals and change the sprite by feeding an offset value to the material via custom primitive data. Is it somehow possible to extend the functionality of the DecalActor to allow for custom primitive data?

It’s quite easy guys.
You should script your Decal Material with some variable you’ll then edit via blueprints in your decal class.

Material:

DecalBP (BeginPlay):

Header

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/DecalActor.h"
#include "MutliDecalActor.generated.h"

/**
 * 
 */
UCLASS()
class YOUR_API AMutliDecalActor : public ADecalActor
{
	GENERATED_BODY()

public:

	AMutliDecalActor();

		UPROPERTY(BlueprintReadWrite, EditAnywhere)
		int32 TilesX = 1;

		UPROPERTY(BlueprintReadWrite, EditAnywhere)
		int32 TilesY = 1;
		
		UPROPERTY(BlueprintReadWrite, EditAnywhere)
		int32 OffsetX = 0;

		UPROPERTY(BlueprintReadWrite, EditAnywhere)
		int32 OffsetY = 0;

		UPROPERTY(BlueprintReadWrite, EditAnywhere)
			class UTexture2D* DecalTexture;

		UFUNCTION()
			void CalculateAtlas();

		UPROPERTY()
		class UMaterialInstanceDynamic* dmi;

		
#if WITH_EDITOR
		//~ Begin UObject Interface
		virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
		//~ End UObject Interface
#endif // WITH_EDITOR

};

CPP

#include "MutliDecalActor.h"

AMutliDecalActor::AMutliDecalActor() {
	
	dmi = CreateDynamicMaterialInstance();
	SetDecalMaterial(dmi);
	CalculateAtlas();

}

void AMutliDecalActor::CalculateAtlas() {
	if (dmi == nullptr) {
		dmi = CreateDynamicMaterialInstance();
		SetDecalMaterial(dmi);
	}
	if (dmi != nullptr) {
		dmi->SetScalarParameterValue("TilesX", TilesX);
		dmi->SetScalarParameterValue("TilesY", TilesY);
		dmi->SetScalarParameterValue("OffsetX", OffsetX);
		dmi->SetScalarParameterValue("OffsetY", OffsetY);
		dmi->SetTextureParameterValue("Texture", DecalTexture);				
	}
}

void AMutliDecalActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) {
	Super::PostEditChangeProperty(PropertyChangedEvent);
	CalculateAtlas();	
}

Material

In scene

1 Like

Thank you, but if I’m not mistaken, both these solutions require creating a new dynamic material instance per atlas row/column. The larger the atlas, the more material instances we need to create == more drawcalls.
Custom primitive data can be used to avoid this in many cases (so we operate only on 1 material instance), but decals can’t use it, since they are not primitives.

1 Like

Since custom primitive data is unavailable, perhaps other data that is could be used to a similar effect. For meshes we often use pseudo procedural techniques, such as using the actors transforms to control the material. I imagine these could also be used to control the atlas index.

Indeed, that’s what I’ve also tried, but it looks like decal transform is not accessible via materials, here’s a little more info: How to pass data into decals without a material instance for each one?

Wouldn’t a dynamic material instance created from blueprint actually work?
I think you still get the drawcall since its not part of an instance system. But maybe it doesn’t since you act on it programmatically instead of creating an actual instance and assigning it. Worth checking into…

1 Like

Some of them definitely are available. Actor/Object/Pivot Position and Orientation seem to be. But based on your other post, if the decal is on a moving actor those may not be suitable. Bounds and Radius did not work at all, nor did any scale based method I tried.

But it can sample some scene textures, notably custom stencil, which could possibly be exploited for a small cost. This would only allow one value per decal receiver at a time, but could easily allow an array to select a different index depending upon what object it was put onto.

It also can sample world normals, which is useful for changing the behavior based upon the surface the decal is applied to (ie different atlas index for ground vs wall) but also not particularly useful for most moving objects.

You could also use a static mesh with the node “static mesh decal function” instead of a decal. This allows a mesh to project a texture to a surface in a manner similar to a decal. Then you would have access to Prim Data or instanced custom data for instanced meshes. Probably more expensive and less convenient than a decal, so I’m not sure if it’s worth it unless the draw calls are a big problem for you.

1 Like

My bad, I overlooked part of your question. :sweat_smile:
Unfortunately I don’t have an answer right away but I’m joining your effort in finding a solution. Let’s see if we can find a good solution.

1 Like

Thank you all for the info!
In my case, it was quite a premature optimization case (it turns out that many drawcalls created this way don’t hurt the performance that much + I’ve limited the max decal amount). However, still, it would be nice to have a way to conveniently pass data to decals without creating a material instance for each one.

As a workaround for now (as an addition to your other ideas mentioned above) you can also reuse material instances if it’s possible for your project (more info)

1 Like

You may be missing a more basic approach.
Shove a texture in the decal, use world position to place it and stretch it across your world.
You can then sample that texture in the decal, and do whatever based on the value it reports.

An 8k texture can provide 4pixels stretched to 1/2m on a 4km^2 area.

Dimminishing returns come in as you add more textures or set up a tile&swap system.
Also consider the fact running a single 8k texture may cost more ms than 10 instances…

If you need more control than this, you can hard code a location array based on world positions.
At that point though, .usf the thing instead of messing with the engine instances…

2 Likes

I know it’s been ages - But I’ve finally found a solution to this while working on some other stuff that should work for most use cases.
There is a “Decal Color” node that is per instance. It allows you to pass an RGBA value set in the component and basically works just like a vertex color. This should allow you to pass an index and/or even a vector and can be set via blueprints or the editor.

2 Likes

Indeed, this is now a good solution for decal colors with 1 material, now we can just access Decal Color in the material. It was added quite recently (I believe 5.2+?).