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!