最近正好有实现战争迷雾的需求,在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版本)供下载。
正如原作者所说的一样,这不是一种最好的实现战争迷雾的方式,仅仅是提供了一种实现的思路。