This is the fog of war implementation for my programming game Jump, Step, Step which is available on Steam and Xbox One now. To use this fog of war in your game, follow these step:

Copy this fog of war material FowMat.uasset to your project asset folder “Content/Fow/”. Add the below line to your Project.Build.cs ( inside your C++ solution explorer ) PrivateDependencyModuleNames.AddRange(new string[] { "RHI", "RenderCore" }); Add the AProFow actor ( ProFow.h and ProFow.cpp ) to your C++ project. In your GamePlayerController.h declare your fow actor: #include "ProFow.h" ... UPROPERTY() AProFow *m_fow; In your GamePlayerController’s BeginPlay function declare and create the Fow actor like this: m_fow = GetWorld()->SpawnActor<AProFow>(AProFow::StaticClass()); and to reveal an circle area with radius 30 at location x=100, y=100 of the fog of war somewhere in your Player Controller by calling this m_fow->revealSmoothCircle(FVector2D(100,100), 30);

That’s it ! Below are just additional stuff that help in case you run in to trouble.

Appendix:

How it works:

AProFow actor create a square plane static mesh with the mesh is a 1×1 unit mesh coming from Engine’s default asset ( You can use any other 1×1 plane mesh if required) . The size of the square fow area can be set by calling AProFow::SetSize function, which will scale the 1×1 plane to required size. Inside AProFow constructor and postComponentInitialization, we create a dynamic grayscale texture, representing the fog of war, and then create dynamic material instance from the base FowMat asset to replace the fow texture into its Param2D node. This material is just black with the opacity controlled by the fow texture. The fow update process is done by keeping AProFow::m_pixelArray which is the exact mirror of the fow texture. Whenever we need to update the fow texture ( e.g inside revealSmoothCircle ) we first change an the m_pixelArray values first and then copy it over to the texture. Note that we can’t change the texture directly inside RevealSmoothCircle because that can only be done on Graphical Thread and on the whole texture each call.

FowMat.uasset Material

The base material used is a very simple Translucent material with color is constant black. Opacity connected to a TextureSampleRuntime2D with name “FowTexture” which is our fog of war texture. The material have “Disable Depth Test” ticked so togather with the staticMesh’s translucency priority 100 will make sure our fow actor is drawn on top of everything.

ProFow.h:

// Thang Phung Dinh, 2016, Fog of War implementation for Jump Step Step game // Feel free to use this in your game #pragma once #include "GameFramework/Actor.h" #include "ProFow.generated.h" UCLASS() class PRO_API AProFow : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties AProFow(); virtual void PostInitializeComponents() override; // Set the fog size void setSize(float); // Reveal a portion of the fow void revealSmoothCircle(const FVector2D &pos, float radius); private: void UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData); // Fow texture size static const int m_textureSize = 512; UPROPERTY() UStaticMeshComponent* m_squarePlane; UPROPERTY() UTexture2D* m_dynamicTexture; UPROPERTY() UMaterialInterface* m_dynamicMaterial; UPROPERTY() UMaterialInstanceDynamic *m_dynamicMaterialInstance; uint8 m_pixelArray[m_textureSize * m_textureSize]; FUpdateTextureRegion2D m_wholeTextureReagion; float m_coverSize; };

ProFow.cpp

// Thang Phung Dinh, 2016, Fog of War implementation for Jump Step Step game // Feel free to use this in your game #include "Pro.h" #include "ProFow.h" // Sets default values AProFow::AProFow() :m_wholeTextureReagion(0, 0, 0, 0, m_textureSize, m_textureSize) { m_coverSize = 5000; // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = false; // Create a planar mesh from engine's planar static mesh m_squarePlane = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Fow Plane Static Mesh")); RootComponent = m_squarePlane; m_squarePlane->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName); { static ConstructorHelpers::FObjectFinder<UStaticMesh> asset(TEXT("/Engine/ArtTools/RenderToTexture/Meshes/S_1_Unit_Plane.S_1_Unit_Plane")); m_squarePlane->StaticMesh = asset.Object; } m_squarePlane->TranslucencySortPriority = 100; m_squarePlane->SetRelativeScale3D(FVector(m_coverSize, m_coverSize, 1)); // Load the base material from your created material { static ConstructorHelpers::FObjectFinder<UMaterial> asset(TEXT("Material'/Game/Fow/FowMat.FowMat'")); m_dynamicMaterial = asset.Object; } // Create the runtime FOW texture if (!m_dynamicTexture) { m_dynamicTexture = UTexture2D::CreateTransient(m_textureSize, m_textureSize, PF_G8); m_dynamicTexture->CompressionSettings = TextureCompressionSettings::TC_Grayscale; m_dynamicTexture->SRGB = 0; m_dynamicTexture->UpdateResource(); m_dynamicTexture->MipGenSettings = TMGS_NoMipmaps; } // Initialise array to all black for (int x = 0; x < m_textureSize; ++x) for (int y = 0; y < m_textureSize; ++y) m_pixelArray[y * m_textureSize + x] = 255; // Propagate memory's array to texture if (m_dynamicTexture) UpdateTextureRegions(m_dynamicTexture, 0, 1, &m_wholeTextureReagion, m_textureSize, 1, m_pixelArray, false); } void AProFow::PostInitializeComponents() { Super::PostInitializeComponents(); // Create a dynamic material instance to swap in the FOW texture if (m_dynamicMaterial) { m_dynamicMaterialInstance = UMaterialInstanceDynamic::Create(m_dynamicMaterial, this); m_dynamicMaterialInstance->SetTextureParameterValue("FowTexture", m_dynamicTexture); } // Set the dynamic material to the mesh if (m_dynamicMaterialInstance) m_squarePlane->SetMaterial(0, m_dynamicMaterialInstance); } void AProFow::setSize(float s) { m_coverSize = s; m_squarePlane->SetRelativeScale3D(FVector(m_coverSize, m_coverSize, 1)); } void AProFow::revealSmoothCircle(const FVector2D &pos, float radius) { // Calculate the where circle center is inside texture space FVector2D texel = pos - FVector2D(GetActorLocation().X, GetActorLocation().Y); texel = texel * m_textureSize / m_coverSize; texel += FVector2D(m_textureSize / 2, m_textureSize / 2); // Calculate radius in texel unit ( 1 is 1 pixel ) float texelRadius = radius * m_textureSize / m_coverSize; // The square area to update int minX = FMath::Clamp<int>(texel.X - texelRadius, 0, m_textureSize - 1); int minY = FMath::Clamp<int>(texel.Y - texelRadius, 0, m_textureSize - 1); int maxX = FMath::Clamp<int>(texel.X + texelRadius, 0, m_textureSize - 1); int maxY = FMath::Clamp<int>(texel.Y + texelRadius, 0, m_textureSize - 1); uint8 theVal = 0; // Update our array of fow value in memory bool dirty = false; for (int x = minX; x < maxX; ++x) { for (int y = minY; y < maxY; ++y) { float distance = FVector2D::Distance(texel, FVector2D(x, y)); if (distance < texelRadius) { static float smoothPct = 0.7f; uint8 oldVal = m_pixelArray[y * m_textureSize + x]; float lerp = FMath::GetMappedRangeValueClamped(FVector2D(smoothPct, 1.0f), FVector2D(0, 1), distance / texelRadius); uint8 newVal = lerp * 255; newVal = FMath::Min(newVal, oldVal); m_pixelArray[y * m_textureSize + x] = newVal; dirty = dirty || oldVal != newVal; } } } // Propagate the values in memory's array to texture if (dirty) UpdateTextureRegions(m_dynamicTexture, 0, 1, &m_wholeTextureReagion, m_textureSize, 1, m_pixelArray, false); } void AProFow::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; }); } }