Need help with dynamic textures and materials

Hey everyone!

I am currently working on an Open-Source Real-Time Strategy Plugin for Unreal Engine 4 and am struggling with dynamic textures and materials.

The goal is to render classical fog of war in the 3D world. Both the computation of the logical visibility mask, and the rendering of fog of war on the minimap, are done.

Our setup is as follows:

There’s an actor with an attached DecalComponent in the level. The decal uses the following material:

The actor is based on a class ARTSFogOfWarDecalActor which looks as follows:

RTSFogOfWarDecalActor.h:



#pragma once

#include "RTSPluginPrivatePCH.h"

#include "GameFramework/Actor.h"

#include "RTSFogOfWarDecalActor.generated.h"


class UTexture2D;
class UDecalComponent;
struct FUpdateTextureRegion2D;
class UDynamicMaterialInstance;

class ARTSVisionVolume;

/** Renders fog of war in 3D space. */
UCLASS()
class ARTSFogOfWarDecalActor : public AActor
{
 GENERATED_BODY()

public:
 ARTSFogOfWarDecalActor(const FObjectInitializer& ObjectInitializer);

 void BeginPlay() override;
 void Tick(float DeltaTime) override;

 void UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData);

private:
 /** Renders the fog of war. */
 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
 UDecalComponent* DecalComponent;

 /** Provides visibility information for how to render the fog of war. */
 ARTSVisionVolume* VisionVolume;

 UTexture2D* DecalTexture;
 uint8* DecalTextureBuffer;
 FUpdateTextureRegion2D* DecalUpdateTextureRegion;
 UMaterialInstanceDynamic* DecalMaterial;
};


RTSFogOfWarDecalActor.cpp:



#include "RTSPluginPrivatePCH.h"
#include "RTSFogOfWarDecalActor.h"

#include "EngineUtils.h"
#include "Engine/Texture2D.h"
#include "Materials/MaterialInstanceDynamic.h"

#include "RTSVisionInfo.h"
#include "RTSVisionVolume.h"


ARTSFogOfWarDecalActor::ARTSFogOfWarDecalActor(const FObjectInitializer& ObjectInitializer)
 : AActor(ObjectInitializer)
{
 PrimaryActorTick.bCanEverTick = true;

 DecalComponent = CreateDefaultSubobject<UDecalComponent>(TEXT("FogOfWarDecal"));
}

void ARTSFogOfWarDecalActor::BeginPlay()
{
 AActor::BeginPlay();

 // Get vision size.
 for (TActorIterator<ARTSVisionVolume> It(GetWorld()); It; ++It)
 {
  VisionVolume = *It;
  break;
 }

 if (!VisionVolume)
 {
  UE_LOG(RTSLog, Warning, TEXT("No vision volume found, won't update vision."));
 }

 // Setup fog of war buffer.
 FIntVector TileSize = VisionVolume->GetTileSize();

 DecalTextureBuffer = new uint8[TileSize.X * TileSize.Y * 4];

 DecalTexture = UTexture2D::CreateTransient(TileSize.X, TileSize.Y);
 DecalTexture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
 DecalTexture->AddToRoot();

 DecalTexture->UpdateResource();

 DecalUpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, TileSize.X, TileSize.Y);

 DecalMaterial = DecalComponent->CreateDynamicMaterialInstance();
 DecalMaterial->SetTextureParameterValue(FName("DynamicTextureParam"), DecalTexture);
}

void ARTSFogOfWarDecalActor::Tick(float DeltaTime)
{
 AActor::Tick(DeltaTime);

 // Update texture.
 ARTSVisionInfo* VisionInfo = ARTSVisionInfo::GetLocalVisionInfo(GetWorld());
 FIntVector TileSize = VisionVolume->GetTileSize();

 for (int32 Y = 0; Y < TileSize.Y; ++Y)
 {
  for (int32 X = 0; X < TileSize.X; ++X)
  {
   int i = Y * TileSize.X + X;

   int iBlue = i * 4 + 0;
   int iGreen = i * 4 + 1;
   int iRed = i * 4 + 2;
   int iAlpha = i * 4 + 3;

   switch (VisionInfo->GetVision(X, Y))
   {
   case ERTSVisionState::VISION_Visible:
   case ERTSVisionState::VISION_Known:
    DecalTextureBuffer[iBlue] = 255;
    DecalTextureBuffer[iGreen] = 255;
    DecalTextureBuffer[iRed] = 255;
    DecalTextureBuffer[iAlpha] = 255;
    break;

   case ERTSVisionState::VISION_Unknown:
    DecalTextureBuffer[iBlue] = 0;
    DecalTextureBuffer[iGreen] = 0;
    DecalTextureBuffer[iRed] = 0;
    DecalTextureBuffer[iAlpha] = 255;
    break;
   }
  }
 }

 UpdateTextureRegions(DecalTexture, 0, 1, DecalUpdateTextureRegion, 256 * 4, (uint32)4, DecalTextureBuffer, false);
 DecalMaterial->SetTextureParameterValue(FName("DynamicTextureParam"), DecalTexture);
}

void ARTSFogOfWarDecalActor::UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData)
{
 if (Texture->Resource)
 {
  struct FUpdateTextureRegionsData
  {
   FTexture2DResource* Texture2DResource;
   int32 MipIndex;
   uint32 NumRegions;
   FUpdateTextureRegion2D* Regions;
   uint32 SrcPitch;
   uint32 SrcBpp;
   uint8* SrcData;
  };

  FUpdateTextureRegionsData* RegionData = new FUpdateTextureRegionsData;

  RegionData->Texture2DResource = (FTexture2DResource*)Texture->Resource;
  RegionData->MipIndex = MipIndex;
  RegionData->NumRegions = NumRegions;
  RegionData->Regions = Regions;
  RegionData->SrcPitch = SrcPitch;
  RegionData->SrcBpp = SrcBpp;
  RegionData->SrcData = SrcData;

  ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(
   UpdateTextureRegionsData,
   FUpdateTextureRegionsData*, RegionData, RegionData,
   bool, bFreeData, bFreeData,
   {
    for (uint32 RegionIndex = 0; RegionIndex < RegionData->NumRegions; ++RegionIndex)
    {
     int32 CurrentFirstMip = RegionData->Texture2DResource->GetCurrentFirstMip();
     if (RegionData->MipIndex >= CurrentFirstMip)
     {
      RHIUpdateTexture2D(
       RegionData->Texture2DResource->GetTexture2DRHI(),
       RegionData->MipIndex - CurrentFirstMip,
       RegionData->Regions[RegionIndex],
       RegionData->SrcPitch,
       RegionData->SrcData
       + RegionData->Regions[RegionIndex].SrcY * RegionData->SrcPitch
       + RegionData->Regions[RegionIndex].SrcX * RegionData->SrcBpp
      );
     }
    }
  if (bFreeData)
  {
   FMemory::Free(RegionData->Regions);
   FMemory::Free(RegionData->SrcData);
  }
  delete RegionData;
   });
 }
}



The result looks as follows:

As you can see, the visibility is correctly computed and rendered on the minimap. However, taking a look at the fog of war in 3D space, we can make the following observations:

  • The fog of war is updated one-dimensional, only, somehow. Whenever a part of the fog is lifted, it is lifted across the entire decal in one direction.
  • The fog of war is lifted only if units are moving at a specific corridor within the map, which suggests that the other direction seems to matter still.

I think it’s safe to assume that the visibility computation itself is correct. Also, the method UpdateTextureRegions should be correct, as it has been used in a variety of sources across the internet, such as the Unreal Wiki and these forums.

This narrows it down to the setup of the material itself, and about 70 lines of code. I guess I might be misunderstanding the way the texture is updated here, especially with respect to the texture coordinates.

Do you have any idea? Any help or pointers here are highly appreciated!

if the texture on your decal is the same as in the minimap, indeed the texture looks correct
maybe you could post your material setup :slight_smile:

btw I’d suggest you to reconsider the usage of a decal in the first place. a decal is supposedly cheap but a decal that covers the entire screen all the time is more questionable. a good alternative (that we used in a shipped title) would be as a worldposition postprocess very much like this

Ah, interesting idea! I heard about another approach using lighting, but it never occurred to me that using a post-processing effect could be an even better idea.

Still, I’m a little lost with your article - can you hint me anywhere to the official Unreal 4 documentation to get started? Where do I build and apply your material graphs? Should I start here?

that’s not “my” material graph (that’s not my blog) but it’s basically the same techinque I used

yes that page is the way to start. as that doc describes you’ll need to create a material that uses the PostProcess domain and then put it in the postprocess volume blendables list.
the material itself will need to map your fog of war texture to the world-space area that it’s supposed to cover, and that’s where the material nodes would look similar to that from Oliver’s blog
once you get the postprocess material applied and working with the scene I could help you further

btw your decal material looks ok so could it be that your texture isn’t 100% correct?

Hey Chokser! Thanks for the pointers - I’m a good step ahead now. I understood Post Process Volumes, materials, and the graph in the tutorial. I’m now as far as the left of the two images in the blog post.

Next, I’d love to have a similar effect as in the right picture, e.g. what he’s calling a “Lerp between the original scene color and …] a simple scene desaturation.”

I guess it has something to do with the output of the right-most node in the example not being plugged directly into the emissive color of the material? I’ve also found a Desaturation node I could add, but I’m unaware of how to access the original scene color and how to lerp between both. Or is that not done in the material?

Sorry for being such a post-process noob :slight_smile:

what you have is a black and white mask then, so you can just put this into a Linear Interpolate (lerp) node:

  • “A” would be your original scene color. you can get this by using a SceneColor node and changing the source to PostProcessInput0
  • “B” would be your desaturated color, so just take the original scene color and put a Desaturate node next to it.
    -or-
    if you want true classic fog of war, just connect a constant of 0
  • “Alpha” is your black and white mask, which you already have

And yes, plug it then into Emissive and you’re done.

don’t worry about it (but do check my nick spelling ;))

Hey Chosker (triple-checked - and really hope I got it right this time :o)

Thanks again for your help - this works like a charm. I had to use the node “SceneTexture” and change the “Scene Texture ID” to “PostProcessInput0”, just as you said. Confusingly, the default “SceneColor” node causes the following error

[SM5] (Node SceneColor) SceneColor lookups are only available when MaterialDomain = Surface.

I also flipped the inputs, making A the desaturated color and B the original scene color, as a lerp parameter of 0 (= black = no vision) should lead to the desaturated color, and 1 (= white = full vision) should show the original scene color.

I’m gonna post the full graph here and plug everything back into the original game as soon as I return from Greece.

Until then, thanks again for all your help! :slight_smile:

I really like the look up this. I do have a question how hard would it be to make this into a space RTS?

Basically, it doesn’t make any difference at all :slight_smile: You can make any kind of RTS with the plugin. Feel free to head over to the thread of the plugin itself:

https://forums.unrealengine.com/community/community-content-tools-and-tutorials/121455-open-rts-plugin-for-unreal-engine-4

That allows us to keep the discussion focused on the plugin there, and focused on rendering here :slight_smile:

Hey everyone,

I just wanted to say a big thank you for all of your help. I promised to post the results here, so here we go.

To summarize in a few words, there’s an actor you can place in your level called FogOfWarActor. That actor will create an instance of a special material and set that instance on a post process volume also placed in that level. That material instance is updated regularly by passing a texture with vision info for the local player.

The code is as follows:

RTSFogOfWarActor.h:




#pragma once

#include "RTSPluginPrivatePCH.h"

#include "GameFramework/Actor.h"

#include "RTSFogOfWarActor.generated.h"


class UTexture2D;
struct FUpdateTextureRegion2D;
class UDynamicMaterialInstance;
class UMaterialInterface;
class APostProcessVolume;

class ARTSVisionInfo;
class ARTSVisionVolume;


/** Renders fog of war in 3D space. */
UCLASS()
class ARTSFogOfWarActor : public AActor
{
 GENERATED_BODY()

public:
 ARTSFogOfWarActor(const FObjectInitializer& ObjectInitializer);

 void BeginPlay() override;
 void Tick(float DeltaTime) override;

 /** Sets the vision info to render in 3D space. */
 void SetupVisionInfo(ARTSVisionInfo* VisionInfo);

 void UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData);

private:
 /** Material to instance for rendering the fog of war effect. */
 UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
 UMaterialInterface* FogOfWarMaterial;

 /** Renders the fog of war. */
 UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
 APostProcessVolume* FogOfWarVolume;

 /** Provides visibility information for how to render the fog of war. */
 ARTSVisionInfo* VisionInfo;

 /** Provides world size information for how to render the fog of war. */
 ARTSVisionVolume* VisionVolume;

 /** Texture containing visibility information to be rendered in 3D space. */
 UTexture2D* FogOfWarTexture;

 /** Buffer for updating the contents of the fog of war texture. */
 uint8* FogOfWarTextureBuffer;

 /** Update region for updating the contents of the fog of war texture. */
 FUpdateTextureRegion2D* FogOfWarUpdateTextureRegion;

 /** Post-process material instance for rendering fog of war in 3D space. */
 UMaterialInstanceDynamic* FogOfWarMaterialInstance;
};


RTSFogOfWarActor.cpp:




#include "RTSPluginPrivatePCH.h"
#include "RTSFogOfWarActor.h"

#include "EngineUtils.h"
#include "Engine/PostProcessVolume.h"
#include "Engine/Texture2D.h"
#include "Materials/MaterialInstanceDynamic.h"

#include "RTSVisionInfo.h"
#include "RTSVisionVolume.h"


ARTSFogOfWarActor::ARTSFogOfWarActor(const FObjectInitializer& ObjectInitializer)
 : AActor(ObjectInitializer)
{
 PrimaryActorTick.bCanEverTick = true;
}

void ARTSFogOfWarActor::BeginPlay()
{
 AActor::BeginPlay();

 // Get vision size.
 for (TActorIterator<ARTSVisionVolume> It(GetWorld()); It; ++It)
 {
  VisionVolume = *It;
  break;
 }

 if (!VisionVolume)
 {
  UE_LOG(RTSLog, Warning, TEXT("No vision volume found, won't update vision."));
  return;
 }

 if (!FogOfWarVolume)
 {
  UE_LOG(RTSLog, Warning, TEXT("No fog of war volume found, won't render vision."));
  return;
 }

 // Setup fog of war buffer.
 FIntVector TileSize = VisionVolume->GetTileSize();
 FVector WorldSize = VisionVolume->GetWorldSize();

 FogOfWarTextureBuffer = new uint8[TileSize.X * TileSize.Y * 4];

 // Setup fog of war texture.
 FogOfWarTexture = UTexture2D::CreateTransient(TileSize.X, TileSize.Y);
 FogOfWarTexture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
 FogOfWarTexture->AddToRoot();

 FogOfWarTexture->UpdateResource();

 FogOfWarUpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, TileSize.X, TileSize.Y);

 // Setup fog of war material.
 FogOfWarMaterialInstance = UMaterialInstanceDynamic::Create(FogOfWarMaterial, nullptr);
 FogOfWarMaterialInstance->SetTextureParameterValue(FName("VisibilityMask"), FogOfWarTexture);
 FogOfWarMaterialInstance->SetScalarParameterValue(FName("OneOverWorldSize"), 1.0f / WorldSize.X);
 FogOfWarMaterialInstance->SetScalarParameterValue(FName("OneOverTileSize"), 1.0f / TileSize.X);

 // Setup fog of war post-process volume.
 FogOfWarVolume->AddOrUpdateBlendable(FogOfWarMaterialInstance);
}

void ARTSFogOfWarActor::Tick(float DeltaTime)
{
 AActor::Tick(DeltaTime);

 // Update texture.
 if (!VisionInfo)
 {
  return;
 }

 FIntVector TileSize = VisionVolume->GetTileSize();

 for (int32 Y = 0; Y < TileSize.Y; ++Y)
 {
  for (int32 X = 0; X < TileSize.X; ++X)
  {
   const int i = Y * TileSize.X + X;

   const int iBlue = i * 4 + 0;
   const int iGreen = i * 4 + 1;
   const int iRed = i * 4 + 2;
   const int iAlpha = i * 4 + 3;

   switch (VisionInfo->GetVision(X, Y))
   {
   case ERTSVisionState::VISION_Visible:
    FogOfWarTextureBuffer[iBlue] = 0;
    FogOfWarTextureBuffer[iGreen] = 0;
    FogOfWarTextureBuffer[iRed] = 255;
    FogOfWarTextureBuffer[iAlpha] = 0;
    break;

   case ERTSVisionState::VISION_Known:
    FogOfWarTextureBuffer[iBlue] = 0;
    FogOfWarTextureBuffer[iGreen] = 255;
    FogOfWarTextureBuffer[iRed] = 0;
    FogOfWarTextureBuffer[iAlpha] = 0;
    break;

   case ERTSVisionState::VISION_Unknown:
    FogOfWarTextureBuffer[iBlue] = 0;
    FogOfWarTextureBuffer[iGreen] = 0;
    FogOfWarTextureBuffer[iRed] = 0;
    FogOfWarTextureBuffer[iAlpha] = 0;
    break;
   }
  }
 }

 UpdateTextureRegions(FogOfWarTexture, 0, 1, FogOfWarUpdateTextureRegion, 256 * 4, (uint32)4, FogOfWarTextureBuffer, false);
 FogOfWarMaterialInstance->SetTextureParameterValue(FName("VisibilityMask"), FogOfWarTexture);
}

void ARTSFogOfWarActor::SetupVisionInfo(ARTSVisionInfo* InVisionInfo)
{
 VisionInfo = InVisionInfo;
}

void ARTSFogOfWarActor::UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData)
{
 if (Texture->Resource)
 {
  struct FUpdateTextureRegionsData
  {
   FTexture2DResource* Texture2DResource;
   int32 MipIndex;
   uint32 NumRegions;
   FUpdateTextureRegion2D* Regions;
   uint32 SrcPitch;
   uint32 SrcBpp;
   uint8* SrcData;
  };

  FUpdateTextureRegionsData* RegionData = new FUpdateTextureRegionsData;

  RegionData->Texture2DResource = (FTexture2DResource*)Texture->Resource;
  RegionData->MipIndex = MipIndex;
  RegionData->NumRegions = NumRegions;
  RegionData->Regions = Regions;
  RegionData->SrcPitch = SrcPitch;
  RegionData->SrcBpp = SrcBpp;
  RegionData->SrcData = SrcData;

  ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(
   UpdateTextureRegionsData,
   FUpdateTextureRegionsData*, RegionData, RegionData,
   bool, bFreeData, bFreeData,
   {
    for (uint32 RegionIndex = 0; RegionIndex < RegionData->NumRegions; ++RegionIndex)
    {
     int32 CurrentFirstMip = RegionData->Texture2DResource->GetCurrentFirstMip();
     if (RegionData->MipIndex >= CurrentFirstMip)
     {
      RHIUpdateTexture2D(
       RegionData->Texture2DResource->GetTexture2DRHI(),
       RegionData->MipIndex - CurrentFirstMip,
       RegionData->Regions[RegionIndex],
       RegionData->SrcPitch,
       RegionData->SrcData
       + RegionData->Regions[RegionIndex].SrcY * RegionData->SrcPitch
       + RegionData->Regions[RegionIndex].SrcX * RegionData->SrcBpp
      );
     }
    }
  if (bFreeData)
  {
   FMemory::Free(RegionData->Regions);
   FMemory::Free(RegionData->SrcData);
  }
  delete RegionData;
   });
 }
}


And here’s the material graph, once in full (to get an overview), and in details after that:

If you want to take a closer look, you can checkout the project from GitHub - npruehs/ue4-rts: Real-time strategy plugin and showcase for Unreal Engine 4. at any time.

Thanks again!