------------------------------------
**Update 09-02-2015
Updated material graph (SceneTexture:Color->SceneTexture:PostProcessInput0) and LevelInfo’s construction script blueprint to work with 4.9. Happy fogging!
------------------------------------
**Update 01-23-2015
Raise your chainswords towards Terra and bless the God-Emperor, there are updates!
[ol]
[li] The solution now calculates the visibility-data on a separate thread. The frame times turned terrible for large FOW-textures with the synchronized solution.[/li][li] Unveiling of visible texels now uses line-of-sight calculations, thus only the texels visible to the pawns will be unveiled.[/li][li] Increased visibility resolution to 50UU/texel for more detailed fogging.[/li][li] Improved performance greatly by only blurring the texels visible at the moment. For sparsely visible levels the calculation time went from ~400ms to ~3ms[/li][li] Tweaked the visibility-updating to make the thread run whenever the blending of the last frame is finished. Blending-times between the data for the last and the current frame is lowered accordingly.[/li][li] Added “shroud of darkness”, meaning that areas that are previously visited, but not currently in sight, are darkened.[/li][li] Added a little bit of extra blurring in the material by sampling 4 locations (though I’m not sure if it works properly, at least the result isn’t very apparent).[/li][/ol]
The code and pictures of the tutorial below has been altered to reflect the changes.
Notable issues/considerations/topics of discussion/future work with the current solution:
[LIST=2]
[li] Since I’m now using a resolution of 50UU/texel, levels are restricted to 512x512m (assuming 100UU/m)when using a 1024x1024 texture. I tried using 2k textures, though this creates a major hiccup when the UpdateTextureRegions()-method is called.[/li]With the 1k-texture there are no noticeable hiccups on my rig, though it is outfitted with a 980GTX. Lower-end hardware might therefore also experience hiccups with 1k-textures. It is of course possible decrease the visibility resolution (by changing the value of the field SamplesPerMeter in FogOfWarManager.h) for bigger levels, but at the cost of reduced resolution.
[li] Shutting down the worker-thread properly turned out to be non-trivial. I have occasionally experienced a crash when exiting the game because the actors whose positions we are calculating visibility data from are already deconstructed. Being a C++ rookie I’ve tried to fix this several times though I’m not completely sure the thread deconstruction is done properly.[/li][li] The solution is not tailored for multiplayer games.[/li][li] Additional performance improvements could definitely be implemented. I haven’t tested the solution with hundreds of actors/dense level-geometry and long-sight ranges, though I expect it to run fairly poor because of the way line of sight-visibility is done.[/li]Because I’m lazy, I just perform a trace against every ws-point within the actor’s sight radius sampled at every half-meter. This is ridiculously inefficient because all of the inner points of the sight circle are potentially unnecessary, and it gets worse as the sight radius increases. What I should have done is create a “rasterized” circle
centred around the actor using something like Bresenham’s midpoint circle algorithm to determine the boundary texels of the circle. We could then trace from origo to each boundary point, and unveil just the texels from the hit-position and back to origo using Bresenham’s line-drawing algorithm.
[li] There is still a little blockiness when projecting the texture onto the level. I guess it’s possible to increase the resolution for small levels or do more blurring in the material, however the graph gets terribly big and I haven’t figured out if it’s possible to do separable gaussian blur using the current UE pp-material solution.[/li]A better solution (overall?) would probably be to make the FOW-texture follow the camera and just display a section of the underlying visibility data. However, that would call for a large rewrite of the current solution and has to deal with issues like camera teleportation.
[/LIST]
I guess that’s it for now, please feel free to comment, give feedback, and ask for help if you’re unable to get the tutorial working.
Cheers,
------------------------------------
**
**Original file
**CASE FILE 112:67:A:AA6:Xad
**Subject: Verbal transcription of holo-diary MN74-X92
**Author identity: Transmechanic Gamecrafter , Adeptus Mechanicus
**Title: A challenge arises
Day one, 7 049 243.M41
My level was running but like the God-Emperor himself, my pawns could see it all, which is indeed heresy.
To rectify this vile insult to the Emperor, a system must be created. A system to shield the areas of the unveiled to the pawns, or they would forever be corrupted by the taint of heresy.
In the dark of the night, only aided by the light of my servo-skull, I researched. Browsing the libraries of Forum Unrealis and the Hub of Answers to see if any great Tech-Priests or scribes had
knowledge to share on the matter. There were hints and whispers of ancient techniques, blueprints and code ciphered in the Language of the Machine God, but no solution appeared beyond the realm of hypothesises.
My mission became clear to me. Alone I had to face this challenge. I undertook the Rite of Caffeinemaking, then closed my mind from distractions, and started working.
****Title: A plan is outlined
Day seven, 7 049 243.M41
The Engine Gods of Epic are indeed to be praised. All the tools were there, I just didn’t know where to look. Alas! Had I the bionics of a Magos my work would have been swifter.
No matter, a plan has formed:
[LIST=3]
[li] A dynamic texture must be formed. This will be fashioned as described by the great Artisan Rama here: A new, community-hosted Unreal Engine Wiki - Announcements - Epic Developer Community Forums . [/li]The texture will be black as the taint of Chaos for regions within the Fog of War, and white as the God-Emperor’s glory for regions discovered by the pawns.
[li] The dynamic texture will be used in a post-process material projected onto the level. One texel will correspond to vision data of a hundred Units Unrealis square. The material will make use of[/li]the node named “AbsoluteWorldPosition” as UV-coordinates for the dynamic texture to project it onto the game world in the xy-plane. It will the be multiplied with the magnificent colors from the rendered scene, using the node of SceneTexture:SceneColor.
It is of paramount importance to do so before the Mapping of Tones, or results will look undesirable.
[li] While the generation of the texture must be ciphered in the Language of the Machine God, commonly known as C++, I must utilise the power of Blueprint to push the texture from code [/li]into the Editorium to have the power to use it in the post-process material. The grand blueprint-node of “SetTextureParameterValue” will be utilised.
[/LIST]
One should note that the solution is limited to doing Fog of War on the xy-plane. An area unveiled for any z-value will unveil it for all z-values.
****Title: Struggeling with the Language of the Machine God
**Day twelve, 7 049 243.M41 **
Entering into the realm of the Language of the Machine God is indeed a challenge for the novice. Trained in the arts of Java, the ancient glyphs of C++ appear grotesque, almost heretical to the untrained mind.
One day it might unhinge my sanity for good. While the great Artisan Rama possesses unfathomable powers, he appears to sometimes forget the limitations of his inferiors. A crucial detail should be mentioned if one is to follow
his design for Dynamic Textures. Having tried to compile these ciphers the Machine Spirit became wrath with me and spat out errors of the linker.
For many nights I struggeled. The wrath of the Machine Spirit’s linker thwarted my every attempt to please it, until I discovered the following rite:
[LIST=4]
[li] Open the file named MyProject.Build.cs[/li][li] Locate the cipher-line PublicDependencyModuleNames.AddRange(new string[] { “Core”, “Engine”…[/li][li] Add cipher-strings “RHI”, “RenderCore” to the array.[/li][li] The Machine Spirit is willing.[/li][/LIST]
The following conclusion has been reached: The Engine Gods of Epic has structured their divine creation in a modular fashion. Each shard of functionality is
compiled into a .dll-file. To make the mighty Tool of Building aware of which modules we depend upon, explicit includes must be provided in the Build.cs-file.
****Title: Alterations
Day twenty, 12 050 243.M41
Code-ciphers, post-process material and blueprints are completed. Additional changes alters original plan:
[LIST=5]
[li] A resolution of one texel per 100 Units Unrealis reveals fairly blocky results when texture is black/white. Separable blur of the Grand Logus Gauss is done in code-cipher to[/li]make result more pleasing to the unaugemented eye.
[li] FOW-calculation is not done on every tick, but scheduled every 0.25 seconds. This keeps the Machine Spirit willing to solve other problems such as planetary bombardment trajectories or warp-travel coordinates.[/li][li] To avoid popping when the Fog Of War-texture is updated, we save the texture from the last update and blend between the former result and the new one in the post-process material.[/li][/LIST]
Detailed inner workings:
Observe resulting header code-ciphers named FogOfWarManager.h:
#pragma once
#include "GameFramework/Actor.h"
#include "FogOfWarWorker.h"
#include "FogOfWarManager.generated.h"
/**
*
*/
UCLASS()
class RPGTEST_API AFogOfWarManager : public AActor
{
GENERATED_BODY()
AFogOfWarManager(const FObjectInitializer & FOI);
virtual ~AFogOfWarManager();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
public:
//Triggers a update in the blueprint
UFUNCTION(BlueprintNativeEvent)
void OnFowTextureUpdated(UTexture2D* currentTexture, UTexture2D* lastTexture);
//Register an actor to influence the FOW-texture
void RegisterFowActor(AActor* Actor);
//Stolen from https://wiki.unrealengine.com/Dynamic_Textures
void UpdateTextureRegions(
UTexture2D* Texture,
int32 MipIndex,
uint32 NumRegions,
FUpdateTextureRegion2D* Regions,
uint32 SrcPitch,
uint32 SrcBpp,
uint8* SrcData,
bool bFreeData);
//How far will an actor be able to see
//CONSIDER: Place it on the actors to allow for individual sight-radius
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FogOfWar)
float SightRange = 9.0f;
//The number of samples per 100 unreal units
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FogOfWar)
float SamplesPerMeter = 2.0f;
//If the last texture blending is done
UPROPERTY(BlueprintReadWrite)
bool bIsDoneBlending;
//Should we blur? It takes up quite a lot of CPU time...
UPROPERTY(EditAnywhere)
bool bIsBlurEnabled = true;
//The size of our textures
uint32 TextureSize = 1024;
//Array containing what parts of the map we've unveiled.
UPROPERTY()
TArray<bool> UnfoggedData;
//Temp array for horizontal blur pass
UPROPERTY()
TArray<uint8> HorizontalBlurData;
//Our texture data (result of vertical blur pass)
UPROPERTY()
TArray<FColor> TextureData;
//Our texture data from the last frame
UPROPERTY()
TArray<FColor> LastFrameTextureData;
//Check to see if we have a new FOW-texture.
bool bHasFOWTextureUpdate = false;
//Blur size
uint8 blurKernelSize = 15;
//Blur kernel
UPROPERTY()
TArray<float> blurKernel;
//Store the actors that will be unveiling the FOW-texture.
UPROPERTY()
TArray<AActor*> FowActors;
//DEBUG: Time it took to update the fow texture
float fowUpdateTime = 0;
//Getter for the working thread
bool GetIsBlurEnabled();
private:
void UpdateFowTexture();
//Triggers the start of a new FOW-texture-update
void StartFOWTextureUpdate();
//Our dynamically updated texture
UPROPERTY()
UTexture2D* FOWTexture;
//Texture from last update. We blend between the two to do a smooth unveiling of newly discovered areas.
UPROPERTY()
UTexture2D* LastFOWTexture;
//Texture regions
FUpdateTextureRegion2D* textureRegions;
//Our fowupdatethread
AFogOfWarWorker* FowThread;
};
Observe resulting body code-ciphers FogOfWarManager.cpp:
// Fill out your copyright notice in the Description page of Project Settings.
#include "RpgTest.h"
#include "FogOfWarManager.h"
AFogOfWarManager::AFogOfWarManager(const FObjectInitializer &FOI) : Super(FOI) {
PrimaryActorTick.bCanEverTick = true;
textureRegions = new FUpdateTextureRegion2D(0, 0, 0, 0, TextureSize, TextureSize);
//15 Gaussian samples. Sigma is 2.0.
//CONSIDER: Calculate the kernel instead, more flexibility...
blurKernel.Init(0.0f, blurKernelSize);
blurKernel[0] = 0.000489f;
blurKernel[1] = 0.002403f;
blurKernel[2] = 0.009246f;
blurKernel[3] = 0.02784f;
blurKernel[4] = 0.065602f;
blurKernel[5] = 0.120999f;
blurKernel[6] = 0.174697f;
blurKernel[7] = 0.197448f;
blurKernel[8] = 0.174697f;
blurKernel[9] = 0.120999f;
blurKernel[10] = 0.065602f;
blurKernel[11] = 0.02784f;
blurKernel[12] = 0.009246f;
blurKernel[13] = 0.002403f;
blurKernel[14] = 0.000489f;
}
AFogOfWarManager::~AFogOfWarManager() {
if (FowThread) {
FowThread->ShutDown();
}
}
void AFogOfWarManager::BeginPlay() {
Super::BeginPlay();
bIsDoneBlending = true;
AFogOfWarManager::StartFOWTextureUpdate();
}
void AFogOfWarManager::Tick(float DeltaSeconds) {
Super::Tick(DeltaSeconds);
if (FOWTexture && LastFOWTexture && bHasFOWTextureUpdate && bIsDoneBlending) {
LastFOWTexture->UpdateResource();
UpdateTextureRegions(LastFOWTexture, (int32)0, (uint32)1, textureRegions, (uint32)(4 * TextureSize), (uint32)4, (uint8*)LastFrameTextureData.GetData(), false);
FOWTexture->UpdateResource();
UpdateTextureRegions(FOWTexture, (int32)0, (uint32)1, textureRegions, (uint32)(4 * TextureSize), (uint32)4, (uint8*)TextureData.GetData(), false);
bHasFOWTextureUpdate = false;
bIsDoneBlending = false;
//Trigger the blueprint update
OnFowTextureUpdated(FOWTexture, LastFOWTexture);
}
}
void AFogOfWarManager::StartFOWTextureUpdate() {
if (!FOWTexture) {
FOWTexture = UTexture2D::CreateTransient(TextureSize, TextureSize);
LastFOWTexture = UTexture2D::CreateTransient(TextureSize, TextureSize);
int arraySize = TextureSize * TextureSize;
TextureData.Init(FColor(0, 0, 0, 255), arraySize);
LastFrameTextureData.Init(FColor(0, 0, 0, 255), arraySize);
HorizontalBlurData.Init(0, arraySize);
UnfoggedData.Init(false, arraySize);
FowThread = new AFogOfWarWorker(this);
}
}
void AFogOfWarManager::OnFowTextureUpdated_Implementation(UTexture2D* currentTexture, UTexture2D* lastTexture) {
//Handle in blueprint
}
void AFogOfWarManager::RegisterFowActor(AActor* Actor) {
FowActors.Add(Actor);
}
bool AFogOfWarManager::GetIsBlurEnabled() {
return bIsBlurEnabled;
}
void AFogOfWarManager::UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData)
{
if (Texture && 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;
});
}
}
Observe resulting header code-ciphers named FogOfWarWorker.h:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
/**
* Worker thread for updating the fog of war data.
*/
class AFogOfWarManager;
class AFogOfWarWorker : public FRunnable
{
//Thread to run the FRunnable on
FRunnableThread* Thread;
//Pointer to our manager
AFogOfWarManager* Manager;
//Thread safe counter
FThreadSafeCounter StopTaskCounter;
public:
AFogOfWarWorker();
AFogOfWarWorker(AFogOfWarManager* manager);
virtual ~AFogOfWarWorker();
//FRunnable interface
virtual bool Init();
virtual uint32 Run();
virtual void Stop();
//Method to perform work
void UpdateFowTexture();
bool bShouldUpdate = false;
void ShutDown();
};
Observe resulting body code-ciphers FogOfWarWorker.cpp:
// Fill out your copyright notice in the Description page of Project Settings.
#include "RpgTest.h"
AFogOfWarWorker::AFogOfWarWorker() {}
AFogOfWarWorker::AFogOfWarWorker(AFogOfWarManager* manager){
Manager = manager;
Thread = FRunnableThread::Create(this, TEXT("AFogOfWarWorker"), 0U, TPri_BelowNormal);
}
AFogOfWarWorker::~AFogOfWarWorker() {
delete Thread;
Thread = NULL;
}
void AFogOfWarWorker::ShutDown() {
Stop();
Thread->WaitForCompletion();
}
bool AFogOfWarWorker::Init() {
if (Manager) {
Manager->GetWorld()->GetFirstPlayerController()->ClientMessage("Fog of War worker thread started");
return true;
}
return false;
}
uint32 AFogOfWarWorker::Run() {
FPlatformProcess::Sleep(0.03f);
while (StopTaskCounter.GetValue() == 0) {
float time;
if (Manager && Manager->GetWorld()) {
time = Manager->GetWorld()->TimeSeconds;
}
if (!Manager->bHasFOWTextureUpdate) {
UpdateFowTexture();
if (Manager && Manager->GetWorld()) {
Manager->fowUpdateTime = Manager->GetWorld()->TimeSince(time);
}
}
FPlatformProcess::Sleep(0.1f);
}
return 0;
}
void AFogOfWarWorker::UpdateFowTexture() {
Manager->LastFrameTextureData = TArray<FColor>(Manager->TextureData);
uint32 halfTextureSize = Manager->TextureSize / 2;
int signedSize = (int)Manager->TextureSize; //For convenience....
TSet<FVector2D> currentlyInSight;
TSet<FVector2D> texelsToBlur;
int sightTexels = Manager->SightRange * Manager->SamplesPerMeter;
float dividend = 100.0f / Manager->SamplesPerMeter;
for (auto Itr(Manager->FowActors.CreateIterator()); Itr; Itr++) {
//Find actor position
if(!*Itr) return;
FVector position = (*Itr)->GetActorLocation();
//We divide by 100.0 because 1 texel equals 1 meter of visibility-data.
int posX = (int)(position.X / dividend) + halfTextureSize;
int posY = (int)(position.Y / dividend) + halfTextureSize;
float integerX, integerY;
FVector2D fractions = FVector2D(modf(position.X / 50.0f, &integerX), modf(position.Y / 50.0f, &integerY));
FVector2D textureSpacePos = FVector2D(posX, posY);
int size = (int)Manager->TextureSize;
FCollisionQueryParams queryParams(FName(TEXT("FOW trace")), false, (*Itr));
int halfKernelSize = (Manager->blurKernelSize - 1) / 2;
//Store the positions we want to blur
for (int y = posY - sightTexels - halfKernelSize; y <= posY + sightTexels + halfKernelSize; y++) {
for (int x = posX - sightTexels - halfKernelSize; x <= posX + sightTexels + halfKernelSize; x++) {
if (x > 0 && x < size && y > 0 && y < size) {
texelsToBlur.Add(FIntPoint(x, y));
}
}
}
//Unveil the positions our actors are currently looking at
for (int y = posY - sightTexels; y <= posY + sightTexels; y++) {
for (int x = posX - sightTexels; x <= posX + sightTexels; x++) {
//Kernel for radial sight
if (x > 0 && x < size && y > 0 && y < size) {
FVector2D currentTextureSpacePos = FVector2D(x, y);
int length = (int)(textureSpacePos - currentTextureSpacePos).Size();
if (length <= sightTexels) {
FVector currentWorldSpacePos = FVector(
((x - (int)halfTextureSize)) * dividend,
((y - (int)halfTextureSize)) * dividend,
position.Z);
//CONSIDER: This is NOT the most efficient way to do conditional unfogging. With long view distances and/or a lot of actors affecting the FOW-data
//it would be preferrable to not trace against all the boundary points and internal texels/positions of the circle, but create and cache "rasterizations" of
//viewing circles (using Bresenham's midpoint circle algorithm) for the needed sightranges, shift the circles to the actor's location
//and just trace against the boundaries.
//We would then use Manager->GetWorld()->LineTraceSingle() and find the first collision texel. Having found the nearest collision
//for every ray we would unveil all the points between the collision and origo using Bresenham's Line-drawing algorithm.
//However, the tracing doesn't seem like it takes much time at all (~0.02ms with four actors tracing circles of 18 texels each),
//it's the blurring that chews CPU..
if (!Manager->GetWorld()->LineTraceTest(position, currentWorldSpacePos, ECC_WorldStatic, queryParams)) {
//Unveil the positions we are currently seeing
Manager->UnfoggedData[x + y * Manager->TextureSize] = true;
//Store the positions we are currently seeing.
currentlyInSight.Add(FVector2D(x, y));
}
}
}
}
}
}
if (Manager->GetIsBlurEnabled()) {
//Horizontal blur pass
int offset = floorf(Manager->blurKernelSize / 2.0f);
for (auto Itr(texelsToBlur.CreateIterator()); Itr; ++Itr) {
int x = (Itr)->IntPoint().X;
int y = (Itr)->IntPoint().Y;
float sum = 0;
for (int i = 0; i < Manager->blurKernelSize; i++) {
int shiftedIndex = i - offset;
if (x + shiftedIndex >= 0 && x + shiftedIndex <= signedSize - 1) {
if (Manager->UnfoggedData[x + shiftedIndex + (y * signedSize)]) {
//If we are currently looking at a position, unveil it completely
if (currentlyInSight.Contains(FVector2D(x + shiftedIndex, y))) {
sum += (Manager->blurKernel* * 255);
}
//If this is a previously discovered position that we're not currently looking at, put it into a "shroud of darkness".
else {
sum += (Manager->blurKernel* * 100);
}
}
}
}
Manager->HorizontalBlurData[x + y * signedSize] = (uint8)sum;
}
//Vertical blur pass
for (auto Itr(texelsToBlur.CreateIterator()); Itr; ++Itr) {
int x = (Itr)->IntPoint().X;
int y = (Itr)->IntPoint().Y;
float sum = 0;
for (int i = 0; i < Manager->blurKernelSize; i++) {
int shiftedIndex = i - offset;
if (y + shiftedIndex >= 0 && y + shiftedIndex <= signedSize - 1) {
sum += (Manager->blurKernel* * Manager->HorizontalBlurData[x + (y + shiftedIndex) * signedSize]);
}
}
Manager->TextureData[x + y * signedSize] = FColor((uint8)sum, (uint8)sum, (uint8)sum, 255);
}
}
else {
for (int y = 0; y < signedSize; y++) {
for (int x = 0; x < signedSize; x++) {
if (Manager->UnfoggedData[x + (y * signedSize)]) {
if (currentlyInSight.Contains(FVector2D(x, y))) {
Manager->TextureData[x + y * signedSize] = FColor((uint8)255, (uint8)255, (uint8)255, 255);
}
else {
Manager->TextureData[x + y * signedSize] = FColor((uint8)100, (uint8)100, (uint8)100, 255);
}
}
}
}
}
Manager->bHasFOWTextureUpdate = true;
}
void AFogOfWarWorker::Stop() {
StopTaskCounter.Increment();
}
Observe image of blueprint named LevelInfo construction-script. Note that the blueprint is based upon FogOfWarManager and contains an unbounded post-process component.
Observer image of blueprint event graph
Observe image of material