最近正好有實(shí)現(xiàn)戰(zhàn)爭迷霧的需求,在forum里找到了一篇教程
Fog of War tutorial by Isvulfe
Isvulfe的思路是這樣的:
1、需要一張動(dòng)態(tài)生成的貼圖。這張貼圖將會(huì)投影到游戲場景里,因此,未被探索的部分對(duì)應(yīng)的像素黑色的,探索部分是白色的。
2、利用Post-process材質(zhì)將動(dòng)態(tài)貼圖投影到場景中。一個(gè)紋素將會(huì)對(duì)應(yīng)到游戲世界里的一個(gè)面積。材質(zhì)會(huì)用到“AbsoluteWorldPosition”節(jié)點(diǎn)作為動(dòng)態(tài)貼圖的uv坐標(biāo),將貼圖投影到游戲世界的xy平面上。貼圖的顏色與場景中的顏色相乘得到輸入的顏色。場景的顏色需通過SceneTexture:SceneColor獲得。
Post-Process的順序是很重要的,作者提及這個(gè)效果的應(yīng)用要在Mapping of Tones前,否則會(huì)產(chǎn)生奇怪的效果。
3、動(dòng)態(tài)貼圖生成用C++實(shí)現(xiàn),在藍(lán)圖中完成對(duì)Post-process材質(zhì)的使用,需要用到藍(lán)圖節(jié)點(diǎn)“SetTextureParameterValue”。
4、動(dòng)態(tài)紋理生成時(shí),利用高斯blur使得黑白區(qū)域的過渡不會(huì)生硬。
5、FOW 不是每幀都計(jì)算,而是每0.25s,這樣可以減少計(jì)算開銷。
6、由于不是逐幀計(jì)算FOW,所以為了不會(huì)有明顯的跳變感,將上一次計(jì)算的貼圖和當(dāng)前計(jì)算的貼圖混合。
7、FOW的計(jì)算放在單獨(dú)的線程中。
相關(guān)聯(lián)的概念有:
FOV(filed of vision),Blur。不同的算法可以實(shí)現(xiàn)不同的效果,效率也各不相同。
可以參考:
http://www.roguebasin.com/index.php?title=FOV
接下來是具體實(shí)現(xiàn):
首先提一句,因?yàn)榭创a的時(shí)候,你可能會(huì)想問為什么要給一堆TArray變量加UPROPERTY(),明明這些變量有的都沒暴露到Editor中,這是因?yàn)閁E的內(nèi)存管理,TArray必須加UPROPERTY(),否則會(huì)導(dǎo)致內(nèi)存管理出錯(cuò)。
FogOfWarManager.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
// 在藍(lán)圖中實(shí)現(xiàn)和調(diào)用,觸發(fā)一次紋理的更新
//感覺用BlueprintImplementableEvent就行,因?yàn)閏++版并沒有寫實(shí)現(xiàn)
UFUNCTION(BlueprintNativeEvent)
void OnFowTextureUpdated(UTexture2D* currentTexture, UTexture2D* lastTexture);
//Register an actor to influence the FOW-texture
// 注冊(cè)一個(gè)會(huì)影響FOW紋理的角色
void RegisterFowActor(AActor* Actor);
//Stolen from https://wiki.unrealengine.com/Dynamic_Textures
//從unreal wiki上偷來的動(dòng)態(tài)生成紋理的算法,好像4.17版本后,引擎自帶了UTexture2D::UpdateTextureRegions可以不用在自己寫
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
// 一個(gè)Actor的可視范圍,可以考慮將這個(gè)屬性放到Actor中,這樣就可以配置不同的可視范圍
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FogOfWar)
float SightRange = 9.0f;
//The number of samples per 100 unreal units
//每100個(gè)單位,即1米需要幾個(gè)采樣
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FogOfWar)
float SamplesPerMeter = 2.0f;
//If the last texture blending is done
//是否完成了與上一個(gè)紋理的混合,因?yàn)榛旌喜皇窃谥骶€程中完成的
UPROPERTY(BlueprintReadWrite)
bool bIsDoneBlending;
//Should we blur? It takes up quite a lot of CPU time...
//是否要用Blur,使得紋理邊緣不那么生硬,需要較大的CPU開銷
UPROPERTY(EditAnywhere)
bool bIsBlurEnabled = true;
//The size of our textures
//動(dòng)態(tài)紋理的大小,1024其實(shí)蠻大了,需要大量計(jì)算,可以根據(jù)需要減小為512、256、128
uint32 TextureSize = 1024;
//Array containing what parts of the map we've unveiled.
//數(shù)組存儲(chǔ)地圖中哪些部分被探索了
UPROPERTY()
TArray<bool> UnfoggedData;
//Temp array for horizontal blur pass
//水平方向blur后的暫時(shí)紋理副本
UPROPERTY()
TArray<uint8> HorizontalBlurData;
//Our texture data (result of vertical blur pass)
//blur后的最終產(chǎn)出紋理
UPROPERTY()
TArray<FColor> TextureData;
//Our texture data from the last frame
// 上一次計(jì)算的紋理
UPROPERTY()
TArray<FColor> LastFrameTextureData;
//Check to see if we have a new FOW-texture.
//是否生成了新的FOW紋理,同樣是因?yàn)榧y理的產(chǎn)生是在另一個(gè)線程里
bool bHasFOWTextureUpdate = false;
//Blur size
//blur算法的kernel大小
uint8 blurKernelSize = 15;
//Blur kernel
//blur算法的kernel
UPROPERTY()
TArray<float> blurKernel;
//Store the actors that will be unveiling the FOW-texture.
//保存會(huì)影響 FOW紋理的Actor
UPROPERTY()
TArray<AActor*> FowActors;
//DEBUG: Time it took to update the fow texture
//用來記錄Update FOW紋理需要的時(shí)間
float fowUpdateTime = 0;
//Getter for the working thread
// 供紋理生成線程調(diào)用的Getter函數(shù)
bool GetIsBlurEnabled();
private:
//好像都沒在cpp中實(shí)現(xiàn),這里作者也沒寫備注
void UpdateFowTexture();
//Triggers the start of a new FOW-texture-update
//觸發(fā)開始新的FOW紋理的更新
void StartFOWTextureUpdate();
//Our dynamically updated texture
//我們的動(dòng)態(tài)更新紋理
UPROPERTY()
UTexture2D* FOWTexture;
//Texture from last update. We blend between the two to do a smooth unveiling of newly discovered areas.
//上一次更新的FOW紋理,用來blend
UPROPERTY()
UTexture2D* LastFOWTexture;
//Texture regions
FUpdateTextureRegion2D* textureRegions;
//Our fowupdatethread
// 我們辛勤工作的fow計(jì)算線程
AFogOfWarWorker* FowThread;
};
FogOfWarManager.cpp
AFogOfWarManager::AFogOfWarManager(const FObjectInitializer &FOI) : Super(FOI) {
PrimaryActorTick.bCanEverTick = true;
// 我們用來動(dòng)態(tài)生成紋理的結(jié)構(gòu)體,這里用了new,我看析構(gòu)函數(shù)里也沒delete。。。。推薦還是用TSharePtr之類來進(jìn)行內(nèi)存管理,雖然我還沒怎么用過
textureRegions = new FUpdateTextureRegion2D(0, 0, 0, 0, TextureSize, TextureSize);
//15 Gaussian samples. Sigma is 2.0.
//CONSIDER: Calculate the kernel instead, more flexibility...
//高斯采樣的kernel,還是用計(jì)算來的方便,這里是直接寫了
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() {
//關(guān)閉線程
if (FowThread) {
FowThread->ShutDown();
}
}
void AFogOfWarManager::BeginPlay() {
Super::BeginPlay();
bIsDoneBlending = true;
//為FOW紋理計(jì)算做初始化
AFogOfWarManager::StartFOWTextureUpdate();
}
void AFogOfWarManager::Tick(float DeltaSeconds) {
Super::Tick(DeltaSeconds);
//判斷工作線程是否計(jì)算完了FOW紋理并完成了Blend
//如果完成了,則更新data到紋理上,并觸發(fā)藍(lán)圖中的OnFowTextureUpdated
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);
}
}
//初始化紋理,數(shù)組,工作線程
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);
//應(yīng)該也要delete吧
FowThread = new AFogOfWarWorker(this);
}
}
void AFogOfWarManager::OnFowTextureUpdated_Implementation(UTexture2D* currentTexture, UTexture2D* lastTexture) {
//Handle in blueprint
}
//添加actor進(jìn)數(shù)組
void AFogOfWarManager::RegisterFowActor(AActor* Actor) {
FowActors.Add(Actor);
}
//Getter函數(shù)供工作線程使用
bool AFogOfWarManager::GetIsBlurEnabled() {
return bIsBlurEnabled;
}
//4.17以后的版本提供了,不用自己寫啦!!雖然也沒看懂,知道是將data數(shù)組中的數(shù)據(jù)寫進(jìn)Texture中就好了。
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;
});
}
}
來看看我們的工作線程:
FogOfWarWorker.h
/**
* 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();
};
AFogOfWarWorker::AFogOfWarWorker() {}
AFogOfWarWorker::AFogOfWarWorker(AFogOfWarManager* manager){
Manager = manager;
//創(chuàng)建線程
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() {
//盲猜這個(gè)時(shí)間是用來等manager的初始化
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;
//逐個(gè)Actor進(jìn)行循環(huán)
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.
// 將actor的世界坐標(biāo)轉(zhuǎn)移到紋理坐標(biāo),世界的xy平面(0,0)-->紋理中心(halfTextureSize ,halfTextureSize)
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;
// Collision Query
FCollisionQueryParams queryParams(FName(TEXT("FOW trace")), false, (*Itr));
int halfKernelSize = (Manager->blurKernelSize - 1) / 2;
//Store the positions we want to blur
//需要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));
}
}
}
// FOV & Blur
//FOV
//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));
}
}
}
}
}
}
//Blur
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[i] * 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[i] * 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[i] * 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();
}
還有部分藍(lán)圖實(shí)現(xiàn),請(qǐng)點(diǎn)擊鏈接查看原文,有大圖。
Fog of War tutorial by Isvulfe
第10頁Juancahf對(duì)代碼做了修改并post出了他的工作;第11頁有他的ue工程(UE 4.21版本)供下載。
正如原作者所說的一樣,這不是一種最好的實(shí)現(xiàn)戰(zhàn)爭迷霧的方式,僅僅是提供了一種實(shí)現(xiàn)的思路。