Fog of War(FOW) Tutorial --- UE4

最近正好有實現戰爭迷霧的需求,在forum里找到了一篇教程
Fog of War tutorial by Isvulfe

Isvulfe的思路是這樣的:
1、需要一張動態生成的貼圖。這張貼圖將會投影到游戲場景里,因此,未被探索的部分對應的像素黑色的,探索部分是白色的。
2、利用Post-process材質將動態貼圖投影到場景中。一個紋素將會對應到游戲世界里的一個面積。材質會用到“AbsoluteWorldPosition”節點作為動態貼圖的uv坐標,將貼圖投影到游戲世界的xy平面上。貼圖的顏色與場景中的顏色相乘得到輸入的顏色。場景的顏色需通過SceneTexture:SceneColor獲得。
Post-Process的順序是很重要的,作者提及這個效果的應用要在Mapping of Tones前,否則會產生奇怪的效果。
3、動態貼圖生成用C++實現,在藍圖中完成對Post-process材質的使用,需要用到藍圖節點“SetTextureParameterValue”。
4、動態紋理生成時,利用高斯blur使得黑白區域的過渡不會生硬。
5、FOW 不是每幀都計算,而是每0.25s,這樣可以減少計算開銷。
6、由于不是逐幀計算FOW,所以為了不會有明顯的跳變感,將上一次計算的貼圖和當前計算的貼圖混合。
7、FOW的計算放在單獨的線程中。

相關聯的概念有:
FOV(filed of vision),Blur。不同的算法可以實現不同的效果,效率也各不相同。
可以參考:
http://www.roguebasin.com/index.php?title=FOV


接下來是具體實現:
首先提一句,因為看代碼的時候,你可能會想問為什么要給一堆TArray變量加UPROPERTY(),明明這些變量有的都沒暴露到Editor中,這是因為UE的內存管理,TArray必須加UPROPERTY(),否則會導致內存管理出錯。

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
    // 在藍圖中實現和調用,觸發一次紋理的更新
//感覺用BlueprintImplementableEvent就行,因為c++版并沒有寫實現
    UFUNCTION(BlueprintNativeEvent)
    void OnFowTextureUpdated(UTexture2D* currentTexture, UTexture2D* lastTexture);
    
    //Register an actor to influence the FOW-texture
    // 注冊一個會影響FOW紋理的角色
    void RegisterFowActor(AActor* Actor);

    //Stolen from https://wiki.unrealengine.com/Dynamic_Textures
      //從unreal wiki上偷來的動態生成紋理的算法,好像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
// 一個Actor的可視范圍,可以考慮將這個屬性放到Actor中,這樣就可以配置不同的可視范圍
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FogOfWar)
    float SightRange = 9.0f;

    //The number of samples per 100 unreal units
  //每100個單位,即1米需要幾個采樣
    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...
//是否要用Blur,使得紋理邊緣不那么生硬,需要較大的CPU開銷
    UPROPERTY(EditAnywhere)
    bool bIsBlurEnabled = true;

    //The size of our textures
//動態紋理的大小,1024其實蠻大了,需要大量計算,可以根據需要減小為512、256、128
    uint32 TextureSize = 1024;

    //Array containing what parts of the map we've unveiled.
//數組存儲地圖中哪些部分被探索了
    UPROPERTY()
    TArray<bool> UnfoggedData;

    //Temp array for horizontal blur pass
//水平方向blur后的暫時紋理副本
    UPROPERTY()
    TArray<uint8> HorizontalBlurData;

    //Our texture data (result of vertical blur pass)
//blur后的最終產出紋理
    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.
//是否生成了新的FOW紋理,同樣是因為紋理的產生是在另一個線程里
    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.
//保存會影響 FOW紋理的Actor 
    UPROPERTY()
    TArray<AActor*> FowActors;

    //DEBUG: Time it took to update the fow texture
//用來記錄Update FOW紋理需要的時間
    float fowUpdateTime = 0;

    //Getter for the working thread
// 供紋理生成線程調用的Getter函數
    bool GetIsBlurEnabled();
        
private:    
//好像都沒在cpp中實現,這里作者也沒寫備注
    void UpdateFowTexture();
    
    //Triggers the start of a new FOW-texture-update
//觸發開始新的FOW紋理的更新
    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.
//上一次更新的FOW紋理,用來blend
    UPROPERTY()
    UTexture2D* LastFOWTexture; 
    
    //Texture regions   
    FUpdateTextureRegion2D* textureRegions; 

    //Our fowupdatethread   
// 我們辛勤工作的fow計算線程   
    AFogOfWarWorker* FowThread;
};

FogOfWarManager.cpp

AFogOfWarManager::AFogOfWarManager(const FObjectInitializer &FOI) : Super(FOI) {    
    PrimaryActorTick.bCanEverTick = true;

// 我們用來動態生成紋理的結構體,這里用了new,我看析構函數里也沒delete。。。。推薦還是用TSharePtr之類來進行內存管理,雖然我還沒怎么用過
    textureRegions = new FUpdateTextureRegion2D(0, 0, 0, 0, TextureSize, TextureSize);      
    
    //15 Gaussian samples. Sigma is 2.0.
    //CONSIDER: Calculate the kernel instead, more flexibility...
//高斯采樣的kernel,還是用計算來的方便,這里是直接寫了
    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;
//為FOW紋理計算做初始化
    AFogOfWarManager::StartFOWTextureUpdate();
}

void AFogOfWarManager::Tick(float DeltaSeconds) {
    Super::Tick(DeltaSeconds);  
//判斷工作線程是否計算完了FOW紋理并完成了Blend
//如果完成了,則更新data到紋理上,并觸發藍圖中的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);        
    }
}

//初始化紋理,數組,工作線程
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);    
//應該也要delete吧   
        FowThread = new AFogOfWarWorker(this);
    }   
}

void AFogOfWarManager::OnFowTextureUpdated_Implementation(UTexture2D* currentTexture, UTexture2D* lastTexture) {
    //Handle in blueprint
}

//添加actor進數組
void AFogOfWarManager::RegisterFowActor(AActor* Actor) {
    FowActors.Add(Actor);
}
//Getter函數供工作線程使用
bool AFogOfWarManager::GetIsBlurEnabled() {
    return bIsBlurEnabled;
}

//4.17以后的版本提供了,不用自己寫啦!!雖然也沒看懂,知道是將data數組中的數據寫進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;
//創建線程
    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() {
//盲猜這個時間是用來等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; 

//逐個Actor進行循環
    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的世界坐標轉移到紋理坐標,世界的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();
}

還有部分藍圖實現,請點擊鏈接查看原文,有大圖。
Fog of War tutorial by Isvulfe
第10頁Juancahf對代碼做了修改并post出了他的工作;第11頁有他的ue工程(UE 4.21版本)供下載。

正如原作者所說的一樣,這不是一種最好的實現戰爭迷霧的方式,僅僅是提供了一種實現的思路。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容