You're breathtaking!
This commit is contained in:
71
Source/Engine/ShadowsOfMordor/AtlasChartsPacker.h
Normal file
71
Source/Engine/ShadowsOfMordor/AtlasChartsPacker.h
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Builder.h"
|
||||
#include "Engine/Utilities/RectPack.h"
|
||||
|
||||
#if COMPILE_WITH_GI_BAKING
|
||||
|
||||
namespace ShadowsOfMordor
|
||||
{
|
||||
class AtlasChartsPacker
|
||||
{
|
||||
public:
|
||||
|
||||
struct Node : RectPack<Node>
|
||||
{
|
||||
Builder::LightmapUVsChart* Chart = nullptr;
|
||||
|
||||
Node(uint32 x, uint32 y, uint32 width, uint32 height)
|
||||
: RectPack<Node>(x, y, width, height)
|
||||
{
|
||||
}
|
||||
|
||||
void OnInsert(Builder::LightmapUVsChart* chart, const LightmapSettings* settings)
|
||||
{
|
||||
Chart = chart;
|
||||
const float invSize = 1.0f / (int32)settings->AtlasSize;
|
||||
chart->Result.UVsArea = Rectangle(X * invSize, Y * invSize, chart->Width * invSize, chart->Height * invSize);
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
Node _root;
|
||||
const LightmapSettings* _settings;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtlasChartsPacker"/> class.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
AtlasChartsPacker(const LightmapSettings* settings)
|
||||
: _root(settings->ChartsPadding, settings->ChartsPadding, (int32)settings->AtlasSize - settings->ChartsPadding, (int32)settings->AtlasSize - settings->ChartsPadding)
|
||||
, _settings(settings)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="AtlasChartsPacker"/> class.
|
||||
/// </summary>
|
||||
~AtlasChartsPacker()
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the specified chart into atlas .
|
||||
/// </summary>
|
||||
/// <param name="chart">The chart.</param>
|
||||
/// <returns></returns>
|
||||
Node* Insert(Builder::LightmapUVsChart* chart)
|
||||
{
|
||||
return _root.Insert(chart->Width, chart->Height, _settings->ChartsPadding, chart, _settings);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
||||
242
Source/Engine/ShadowsOfMordor/Builder.BuildCache.cpp
Normal file
242
Source/Engine/ShadowsOfMordor/Builder.BuildCache.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Builder.h"
|
||||
#include "Engine/Level/Scene/Lightmap.h"
|
||||
#include "Engine/Level/Scene/Scene.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
#include "Engine/ContentImporters/AssetsImportingManager.h"
|
||||
#include "Engine/ContentImporters/ImportTexture.h"
|
||||
#include "Engine/Graphics/PixelFormatExtensions.h"
|
||||
#include <ThirdParty/DirectXTex/DirectXTex.h>
|
||||
|
||||
ShadowsOfMordor::Builder::LightmapBuildCache::~LightmapBuildCache()
|
||||
{
|
||||
SAFE_DELETE_GPU_RESOURCE(LightmapData);
|
||||
}
|
||||
|
||||
bool ShadowsOfMordor::Builder::LightmapBuildCache::Init(const LightmapSettings* settings)
|
||||
{
|
||||
if (LightmapData)
|
||||
return false;
|
||||
LightmapData = GPUDevice::Instance->CreateBuffer(TEXT("LightmapBuildCache"));
|
||||
const auto elementsCount = (int32)settings->AtlasSize * (int32)settings->AtlasSize * NUM_SH_TARGETS;
|
||||
if (LightmapData->Init(GPUBufferDescription::Typed(elementsCount, HemispheresFormatToPixelFormat[HEMISPHERES_IRRADIANCE_FORMAT], true)))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
ShadowsOfMordor::Builder::SceneBuildCache::SceneBuildCache()
|
||||
: Scene(nullptr)
|
||||
, TempLightmapData(nullptr)
|
||||
, LightmapsCount(0)
|
||||
, HemispheresCount(0)
|
||||
, MergedHemispheresCount(0)
|
||||
, ImportLightmapIndex(0)
|
||||
, ImportLightmapTextureIndex(0)
|
||||
{
|
||||
}
|
||||
|
||||
const LightmapSettings& ShadowsOfMordor::Builder::SceneBuildCache::GetSettings() const
|
||||
{
|
||||
ASSERT(Scene);
|
||||
return Scene->Info.LightmapSettings;
|
||||
}
|
||||
|
||||
bool ShadowsOfMordor::Builder::SceneBuildCache::WaitForLightmaps()
|
||||
{
|
||||
Texture* lightmaps[3];
|
||||
for (int32 lightmapIndex = 0; lightmapIndex < Lightmaps.Count(); lightmapIndex++)
|
||||
{
|
||||
const auto lightmap = Scene->LightmapsData.GetLightmap(lightmapIndex);
|
||||
lightmap->GetTextures(lightmaps);
|
||||
|
||||
for (int32 textureIndex = 0; textureIndex < NUM_SH_TARGETS; textureIndex++)
|
||||
{
|
||||
AssetReference<Texture> lightmapTexture = lightmaps[textureIndex];
|
||||
if (lightmapTexture == nullptr)
|
||||
{
|
||||
LOG(Error, "Missing lightmap {0} texture{1}", lightmapIndex, textureIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wait for loading end and check result
|
||||
if (lightmapTexture->WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Failed to load lightmap {0} texture {1}", lightmapIndex, textureIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: disable streaming for lightmap texture here (enable it later after baking)
|
||||
|
||||
// Wait texture to be streamed to the target quality
|
||||
const auto t = lightmapTexture->GetTexture();
|
||||
const int32 stepSize = 30;
|
||||
const int32 maxWaitTime = 60000;
|
||||
int32 stepsCount = static_cast<int32>(maxWaitTime / stepSize);
|
||||
while ((t->ResidentMipLevels() < t->MipLevels() || t->ResidentMipLevels() == 0) && stepsCount-- > 0)
|
||||
Platform::Sleep(stepSize);
|
||||
if (t->ResidentMipLevels() < t->MipLevels() || t->ResidentMipLevels() == 0)
|
||||
{
|
||||
LOG(Error, "Waiting for lightmap no. {0} texture {1} to be fully resided timed out (loaded mips: {2}, mips count: {3})", lightmapIndex, textureIndex, t->ResidentMipLevels(), t->MipLevels());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::SceneBuildCache::UpdateLightmaps()
|
||||
{
|
||||
Texture* lightmaps[3];
|
||||
for (int32 lightmapIndex = 0; lightmapIndex < Lightmaps.Count(); lightmapIndex++)
|
||||
{
|
||||
// Cache data
|
||||
auto& lightmapEntry = Lightmaps[lightmapIndex];
|
||||
auto lightmap = Scene->LightmapsData.GetLightmap(lightmapIndex);
|
||||
ASSERT(lightmap);
|
||||
lightmap->GetTextures(lightmaps);
|
||||
|
||||
// Download buffer data
|
||||
if (lightmapEntry.LightmapData->DownloadData(ImportLightmapTextureData))
|
||||
{
|
||||
LOG(Warning, "Cannot download LightmapData.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Import all textures but don't use file proxy to improve performance
|
||||
for (int32 textureIndex = 0; textureIndex < NUM_SH_TARGETS; textureIndex++)
|
||||
{
|
||||
// Get asset name
|
||||
String assetPath;
|
||||
if (lightmaps[textureIndex])
|
||||
assetPath = lightmaps[textureIndex]->GetPath();
|
||||
else
|
||||
Scene->LightmapsData.GetCachedLightmapPath(&assetPath, lightmapIndex, textureIndex);
|
||||
|
||||
// Import texture with custom options
|
||||
#if COMPILE_WITH_ASSETS_IMPORTER
|
||||
Guid id = Guid::Empty;
|
||||
ImportTexture::Options options;
|
||||
options.Type = TextureFormatType::HdrRGBA;
|
||||
options.IndependentChannels = true;
|
||||
options.Compress = Scene->GetLightmapSettings().CompressLightmaps;
|
||||
options.GenerateMipMaps = true;
|
||||
options.IsAtlas = false;
|
||||
options.sRGB = false;
|
||||
options.NeverStream = false;
|
||||
ImportLightmapIndex = lightmapIndex;
|
||||
ImportLightmapTextureIndex = textureIndex;
|
||||
options.InternalLoad.Bind<SceneBuildCache, &SceneBuildCache::onImportLightmap>(this);
|
||||
if (AssetsImportingManager::Create(AssetsImportingManager::CreateTextureTag, assetPath, id, &options))
|
||||
{
|
||||
LOG(Error, "Cannot create new lightmap {0}:{1}", lightmapIndex, textureIndex);
|
||||
return;
|
||||
}
|
||||
const auto result = Content::LoadAsync<Texture>(id);
|
||||
if (result == nullptr)
|
||||
#else
|
||||
#error "Cannot import lightmaps. Assets importer module iss missing."
|
||||
auto result = nullptr;
|
||||
#endif
|
||||
{
|
||||
LOG(Error, "Cannot load new lightmap {0}:{1}", lightmapIndex, textureIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update lightmap
|
||||
lightmap->UpdateTexture(result, textureIndex);
|
||||
}
|
||||
|
||||
#if DEBUG_EXPORT_LIGHTMAPS_PREVIEW
|
||||
// Temporary save lightmaps (after last bounce)
|
||||
if (Builder->_giBounceRunningIndex == Builder->_bounceCount - 1)
|
||||
{
|
||||
exportLightmapPreview(this, lightmapIndex);
|
||||
}
|
||||
#endif
|
||||
|
||||
ImportLightmapTextureData.Release();
|
||||
}
|
||||
}
|
||||
|
||||
bool ShadowsOfMordor::Builder::SceneBuildCache::Init(ShadowsOfMordor::Builder* builder, int32 index, ::Scene* scene)
|
||||
{
|
||||
Builder = builder;
|
||||
SceneIndex = index;
|
||||
Scene = scene;
|
||||
const int32 atlasSize = (int32)GetSettings().AtlasSize;
|
||||
TempLightmapData = GPUDevice::Instance->CreateBuffer(TEXT("LightmapBuildCache"));
|
||||
const auto elementsCount = atlasSize * atlasSize * NUM_SH_TARGETS;
|
||||
if (TempLightmapData->Init(GPUBufferDescription::Typed(elementsCount, HemispheresFormatToPixelFormat[HEMISPHERES_IRRADIANCE_FORMAT], true)))
|
||||
return true;
|
||||
|
||||
LOG(Info, "Scene \'{0}\' quality: {1}", scene->GetName(), scene->Info.LightmapSettings.Quality);
|
||||
return false;
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::SceneBuildCache::Release()
|
||||
{
|
||||
EntriesLocker.Lock();
|
||||
|
||||
// Cleanup
|
||||
Entries.Resize(0);
|
||||
Lightmaps.Resize(0);
|
||||
Charts.Resize(0);
|
||||
Scene = nullptr;
|
||||
|
||||
EntriesLocker.Unlock();
|
||||
|
||||
SAFE_DELETE_GPU_RESOURCE(TempLightmapData);
|
||||
}
|
||||
|
||||
#if COMPILE_WITH_ASSETS_IMPORTER
|
||||
|
||||
bool ShadowsOfMordor::Builder::SceneBuildCache::onImportLightmap(TextureData& image)
|
||||
{
|
||||
// Cache data
|
||||
const int32 lightmapIndex = ImportLightmapIndex;
|
||||
const int32 textureIndex = ImportLightmapTextureIndex;
|
||||
|
||||
// Setup image
|
||||
image.Width = image.Height = (int32)GetSettings().AtlasSize;
|
||||
image.Depth = 1;
|
||||
image.Format = HemispheresFormatToPixelFormat[HEMISPHERES_IRRADIANCE_FORMAT];
|
||||
image.Items.Resize(1);
|
||||
image.Items[0].Mips.Resize(1);
|
||||
auto& mip = image.Items[0].Mips[0];
|
||||
mip.RowPitch = PixelFormatExtensions::SizeInBytes(image.Format) * image.Width;
|
||||
mip.DepthPitch = mip.RowPitch * image.Height;
|
||||
mip.Lines = image.Height;
|
||||
mip.Data.Allocate(mip.DepthPitch);
|
||||
|
||||
#if HEMISPHERES_IRRADIANCE_FORMAT == HEMISPHERES_FORMAT_R32G32B32A32
|
||||
auto pos = (Vector4*)mip.Data.Get();
|
||||
const auto textureData = ImportLightmapTextureData.Get<Vector4>();
|
||||
for (int32 y = 0; y < image.Height; y++)
|
||||
{
|
||||
for (int32 x = 0; x < image.Width; x++)
|
||||
{
|
||||
const int32 texelAddress = (y * image.Width + x) * NUM_SH_TARGETS;
|
||||
*pos = textureData[texelAddress + textureIndex];
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
#elif HEMISPHERES_IRRADIANCE_FORMAT == HEMISPHERES_FORMAT_R16G16B16A16
|
||||
auto pos = (Half4*)mip.Data.Get();
|
||||
const auto textureData = ImportLightmapTextureData.Get<Half4>();
|
||||
for (int32 y = 0; y < image.Height; y++)
|
||||
{
|
||||
for (int32 x = 0; x < image.Width; x++)
|
||||
{
|
||||
const int32 texelAddress = (y * image.Width + x) * NUM_SH_TARGETS;
|
||||
*pos = textureData[texelAddress + textureIndex];
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
167
Source/Engine/ShadowsOfMordor/Builder.Charts.cpp
Normal file
167
Source/Engine/ShadowsOfMordor/Builder.Charts.cpp
Normal file
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Builder.h"
|
||||
#include "Engine/Engine/Engine.h"
|
||||
#include "AtlasChartsPacker.h"
|
||||
#include "Engine/Level/Scene/SceneLightmapsData.h"
|
||||
#include "Engine/Core/Math/Math.h"
|
||||
#include "Engine/Core/Collections/Sorting.h"
|
||||
#include "Engine/ContentImporters/ImportTexture.h"
|
||||
#include "Engine/Level/SceneQuery.h"
|
||||
#include "Engine/Level/Scene/Lightmap.h"
|
||||
|
||||
bool ShadowsOfMordor::Builder::sortCharts(const LightmapUVsChart& a, const LightmapUVsChart& b)
|
||||
{
|
||||
// Sort by area
|
||||
return (b.Width * b.Height) < (a.Width * a.Height);
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::generateCharts()
|
||||
{
|
||||
reportProgress(BuildProgressStep::GenerateLightmapCharts, 0.0f);
|
||||
|
||||
auto scene = _scenes[_workerActiveSceneIndex];
|
||||
auto& settings = scene->GetSettings();
|
||||
ScopeLock lock(scene->EntriesLocker);
|
||||
|
||||
// Generate lightmap UVs charts
|
||||
const int32 entriesCount = scene->Entries.Count();
|
||||
scene->Charts.EnsureCapacity(entriesCount);
|
||||
const int32 MaximumChartSize = (int32)settings.AtlasSize - settings.ChartsPadding * 2;
|
||||
for (int32 i = 0; i < entriesCount; i++)
|
||||
{
|
||||
LightmapUVsChart chart;
|
||||
chart.Result.TextureIndex = INVALID_INDEX;
|
||||
|
||||
GeometryEntry& entry = scene->Entries[i];
|
||||
entry.ChartIndex = INVALID_INDEX;
|
||||
|
||||
// Calculate desired area for the entry's chart (based on object dimensions and settings)
|
||||
// Reject missing models or too small objects
|
||||
Vector3 size = entry.Box.GetSize();
|
||||
float dimensionsCoeff = size.AverageArithmetic();
|
||||
if (size.X <= 1.0f)
|
||||
dimensionsCoeff = Vector2(size.Y, size.Z).AverageArithmetic();
|
||||
else if (size.Y <= 1.0f)
|
||||
dimensionsCoeff = Vector2(size.X, size.Z).AverageArithmetic();
|
||||
else if (size.Z <= 1.0f)
|
||||
dimensionsCoeff = Vector2(size.Y, size.X).AverageArithmetic();
|
||||
const float scale = settings.GlobalObjectsScale * entry.Scale * LightmapTexelsPerWorldUnit * dimensionsCoeff;
|
||||
if (scale <= ZeroTolerance)
|
||||
continue;
|
||||
|
||||
// Apply lightmap uvs bounding box (in uv space) to reduce waste of lightmap atlas space
|
||||
chart.Width = Math::Clamp(Math::CeilToInt(scale * entry.UVsBox.GetWidth()), LightmapMinChartSize, MaximumChartSize);
|
||||
chart.Height = Math::Clamp(Math::CeilToInt(scale * entry.UVsBox.GetHeight()), LightmapMinChartSize, MaximumChartSize);
|
||||
|
||||
// Register lightmap atlas chart entry
|
||||
chart.EntryIndex = i;
|
||||
scene->Charts.Add(chart);
|
||||
|
||||
// Progress Point
|
||||
reportProgress(BuildProgressStep::GenerateLightmapCharts, static_cast<float>(i) / entriesCount);
|
||||
}
|
||||
|
||||
reportProgress(BuildProgressStep::GenerateLightmapCharts, 1.0f);
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::packCharts()
|
||||
{
|
||||
reportProgress(BuildProgressStep::PackLightmapCharts, 0.0f);
|
||||
auto scene = _scenes[_workerActiveSceneIndex];
|
||||
|
||||
// Pack UV charts into atlases
|
||||
Array<AtlasChartsPacker*> packers;
|
||||
if (scene->Charts.HasItems())
|
||||
{
|
||||
// Sort charts from the biggest to the smallest
|
||||
Sorting::QuickSort(scene->Charts.Get(), scene->Charts.Count(), &sortCharts);
|
||||
|
||||
reportProgress(BuildProgressStep::PackLightmapCharts, 0.1f);
|
||||
|
||||
// Cache charts indices after sorting operation
|
||||
scene->EntriesLocker.Lock();
|
||||
for (int32 chartIndex = 0; chartIndex < scene->Charts.Count(); chartIndex++)
|
||||
{
|
||||
auto& chart = scene->Charts[chartIndex];
|
||||
scene->Entries[chart.EntryIndex].ChartIndex = chartIndex;
|
||||
}
|
||||
scene->EntriesLocker.Unlock();
|
||||
|
||||
reportProgress(BuildProgressStep::PackLightmapCharts, 0.5f);
|
||||
|
||||
// Pack all the charts
|
||||
for (int32 i = 0; i < scene->Charts.Count(); i++)
|
||||
{
|
||||
auto chart = &scene->Charts[i];
|
||||
|
||||
bool cannotPack = true;
|
||||
for (int32 j = 0; j < packers.Count(); j++)
|
||||
{
|
||||
if (packers[j]->Insert(chart))
|
||||
{
|
||||
chart->Result.TextureIndex = j;
|
||||
cannotPack = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cannotPack)
|
||||
{
|
||||
auto packer = New<AtlasChartsPacker>(&scene->GetSettings());
|
||||
auto result = packer->Insert(chart);
|
||||
ASSERT(result);
|
||||
chart->Result.TextureIndex = packers.Count();
|
||||
packers.Add(packer);
|
||||
}
|
||||
}
|
||||
}
|
||||
const int32 lightmapsCount = scene->LightmapsCount = packers.Count();
|
||||
LOG(Info, "Scene \'{0}\': building {1} lightmap(s) ({2} chart(s) to bake)...", scene->Scene->GetName(), lightmapsCount, scene->Charts.Count());
|
||||
packers.ClearDelete();
|
||||
|
||||
// Progress Point
|
||||
reportProgress(BuildProgressStep::PackLightmapCharts, 1.0f);
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::updateLightmaps()
|
||||
{
|
||||
reportProgress(BuildProgressStep::UpdateLightmapsCollection, 0.0f);
|
||||
|
||||
auto scene = _scenes[_workerActiveSceneIndex];
|
||||
auto& settings = scene->GetSettings();
|
||||
const int32 lightmapsCount = scene->LightmapsCount;
|
||||
|
||||
// Update lightmaps collection
|
||||
scene->Scene->LightmapsData.UpdateLightmapsCollection(lightmapsCount, (int32)settings.AtlasSize);
|
||||
scene->Lightmaps.Resize(lightmapsCount, false);
|
||||
for (int32 lightmapIndex = 0; lightmapIndex < lightmapsCount; lightmapIndex++)
|
||||
{
|
||||
if (scene->Lightmaps[lightmapIndex].Init(&settings))
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for all lightmaps to be ready (after creating new lightmaps assets we need to wait for resources to be prepared)
|
||||
GPUDevice::Instance->Locker.Lock();
|
||||
for (int32 lightmapIndex = 0; lightmapIndex < lightmapsCount; lightmapIndex++)
|
||||
{
|
||||
Texture* textures[NUM_SH_TARGETS];
|
||||
scene->Scene->LightmapsData.GetLightmap(lightmapIndex)->GetTextures(textures);
|
||||
for (int32 textureIndex = 0; textureIndex < NUM_SH_TARGETS; textureIndex++)
|
||||
{
|
||||
auto texture = textures[textureIndex];
|
||||
GPUDevice::Instance->Locker.Unlock();
|
||||
if (texture->WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Lightmap load failed.");
|
||||
return;
|
||||
}
|
||||
GPUDevice::Instance->Locker.Lock();
|
||||
}
|
||||
|
||||
reportProgress(BuildProgressStep::UpdateLightmapsCollection, (float)lightmapIndex / lightmapsCount);
|
||||
}
|
||||
GPUDevice::Instance->Locker.Unlock();
|
||||
|
||||
reportProgress(BuildProgressStep::UpdateLightmapsCollection, 1.0f);
|
||||
}
|
||||
61
Source/Engine/ShadowsOfMordor/Builder.Config.h
Normal file
61
Source/Engine/ShadowsOfMordor/Builder.Config.h
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Core/Enums.h"
|
||||
#include "Engine/Graphics/PixelFormat.h"
|
||||
#include "Engine/Graphics/Textures/TextureData.h"
|
||||
#include "Types.h"
|
||||
|
||||
namespace ShadowsOfMordor
|
||||
{
|
||||
DECLARE_ENUM_9(BuildProgressStep,
|
||||
Initialize,
|
||||
CacheEntries,
|
||||
GenerateLightmapCharts,
|
||||
PackLightmapCharts,
|
||||
UpdateLightmapsCollection,
|
||||
UpdateEntries,
|
||||
GenerateHemispheresCache,
|
||||
RenderHemispheres,
|
||||
Cleanup);
|
||||
extern float BuildProgressStepProgress[BuildProgressStep_Count];
|
||||
extern PixelFormat HemispheresFormatToPixelFormat[2];
|
||||
const float LightmapTexelsPerWorldUnit = 1.0f / 4.0f;
|
||||
const int32 LightmapMinChartSize = 1;
|
||||
|
||||
struct GenerateHemispheresData
|
||||
{
|
||||
TextureData PositionsData;
|
||||
TextureData NormalsData;
|
||||
};
|
||||
};
|
||||
|
||||
#define HEMISPHERES_FORMAT_R32G32B32A32 0
|
||||
#define HEMISPHERES_FORMAT_R16G16B16A16 1
|
||||
|
||||
// Adjustable configuration
|
||||
#define LIGHTMAP_SCALE_MAX 1000000.0f
|
||||
#define HEMISPHERES_RENDERING_TARGET_FPS 24
|
||||
#define HEMISPHERES_PER_JOB_MIN 10
|
||||
#define HEMISPHERES_PER_JOB_MAX 1000
|
||||
#define HEMISPHERES_PER_GPU_FLUSH 15
|
||||
#define HEMISPHERES_FOV 120.0f
|
||||
#define HEMISPHERES_NEAR_PLANE 0.1f
|
||||
#define HEMISPHERES_FAR_PLANE 10000.0f
|
||||
#define HEMISPHERES_IRRADIANCE_FORMAT HEMISPHERES_FORMAT_R16G16B16A16
|
||||
#define HEMISPHERES_BAKE_STATE_SAVE 1
|
||||
#define HEMISPHERES_BAKE_STATE_SAVE_DELAY 300
|
||||
#define CACHE_ENTRIES_PER_JOB 10
|
||||
#define CACHE_POSITIONS_FORMAT HEMISPHERES_FORMAT_R32G32B32A32
|
||||
#define CACHE_NORMALS_FORMAT HEMISPHERES_FORMAT_R16G16B16A16
|
||||
|
||||
// Debugging tools settings
|
||||
// Note: debug images will be exported to the temporary folder ('<project-root>\Cache\ShadowsOfMordor_Debug')
|
||||
#define DEBUG_EXPORT_LIGHTMAPS_PREVIEW 0
|
||||
#define DEBUG_EXPORT_CACHE_PREVIEW 0
|
||||
#define DEBUG_EXPORT_HEMISPHERES_PREVIEW 0
|
||||
|
||||
// Constants
|
||||
#define HEMISPHERES_RESOLUTION 64
|
||||
#define NUM_SH_TARGETS 3
|
||||
216
Source/Engine/ShadowsOfMordor/Builder.Debug.cpp
Normal file
216
Source/Engine/ShadowsOfMordor/Builder.Debug.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Builder.h"
|
||||
#include "Engine/Platform/FileSystem.h"
|
||||
#include "Engine/Graphics/RenderTargetPool.h"
|
||||
#if DEBUG_EXPORT_HEMISPHERES_PREVIEW
|
||||
#include "Engine/Graphics/GPUContext.h"
|
||||
#endif
|
||||
|
||||
#if DEBUG_EXPORT_LIGHTMAPS_PREVIEW || DEBUG_EXPORT_CACHE_PREVIEW || DEBUG_EXPORT_HEMISPHERES_PREVIEW
|
||||
|
||||
String GetDebugDataPath()
|
||||
{
|
||||
auto result = Globals::ProjectCacheFolder / TEXT("ShadowsOfMordor_Debug");
|
||||
if (!FileSystem::DirectoryExists(result))
|
||||
FileSystem::CreateDirectory(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if DEBUG_EXPORT_LIGHTMAPS_PREVIEW
|
||||
|
||||
void ShadowsOfMordor::Builder::exportLightmapPreview(SceneBuildCache* scene, int32 lightmapIndex)
|
||||
{
|
||||
auto settings = &scene->GetSettings();
|
||||
auto atlasSize = (int32)settings->AtlasSize;
|
||||
|
||||
int32 bytesPerPixel = 3;
|
||||
int32 dataBmpSize = atlasSize * atlasSize * bytesPerPixel;
|
||||
byte* dataBmp = new byte[dataBmpSize];
|
||||
|
||||
for (int sh = 0; sh < NUM_SH_TARGETS; sh++)
|
||||
{
|
||||
auto tmpPath = GetDebugDataPath() / String::Format(TEXT("Scene{2}_lighmap_{0}_{1}.bmp"), lightmapIndex, sh, scene->SceneIndex);
|
||||
|
||||
for (int32 y = 0; y < atlasSize; y++)
|
||||
{
|
||||
for (int32 x = 0; x < atlasSize; x++)
|
||||
{
|
||||
const int32 pos = (y * atlasSize + x) * 3;
|
||||
const int32 texelAdress = ((atlasSize - y - 1) * atlasSize + x) * NUM_SH_TARGETS;
|
||||
|
||||
#if HEMISPHERES_IRRADIANCE_FORMAT == HEMISPHERES_FORMAT_R32G32B32A32
|
||||
auto textureData = scene->ImportLightmapTextureData.Get<Vector4>();
|
||||
Color color = Color(Vector4::Clamp(textureData[texelAdress + sh], Vector4::Zero, Vector4::One));
|
||||
#elif HEMISPHERES_IRRADIANCE_FORMAT == HEMISPHERES_FORMAT_R16G16B16A16
|
||||
auto textureData = scene->ImportLightmapTextureData.Get<Half4>();
|
||||
Color color = Color(Vector4::Clamp(textureData[texelAdress + sh].ToVector4(), Vector4::Zero, Vector4::One));
|
||||
#endif
|
||||
|
||||
dataBmp[pos + 0] = static_cast<byte>(color.B * 255);
|
||||
dataBmp[pos + 1] = static_cast<byte>(color.G * 255);
|
||||
dataBmp[pos + 2] = static_cast<byte>(color.R * 255);
|
||||
}
|
||||
}
|
||||
FileSystem::SaveBitmapToFile(dataBmp, atlasSize, atlasSize, bytesPerPixel * 8, 0, tmpPath);
|
||||
}
|
||||
|
||||
delete[] dataBmp;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if DEBUG_EXPORT_CACHE_PREVIEW
|
||||
|
||||
void ShadowsOfMordor::Builder::exportCachePreview(SceneBuildCache* scene, GenerateHemispheresData& cacheData, LightmapBuildCache& lightmapEntry) const
|
||||
{
|
||||
auto settings = &scene->GetSettings();
|
||||
auto atlasSize = (int32)settings->AtlasSize;
|
||||
|
||||
int32 bytesPerPixel = 3;
|
||||
int32 dataSize = atlasSize * atlasSize * bytesPerPixel;
|
||||
byte* data = new byte[dataSize];
|
||||
|
||||
{
|
||||
auto tmpPath = GetDebugDataPath() / String::Format(TEXT("Scene{1}_lightmapCache_{0}_Posiion.bmp"), _workerStagePosition0, scene->SceneIndex);
|
||||
auto mipData = cacheData.PositionsData.GetData(0, 0);
|
||||
|
||||
for (int32 x = 0; x < cacheData.PositionsData.Width; x++)
|
||||
{
|
||||
for (int32 y = 0; y < cacheData.PositionsData.Height; y++)
|
||||
{
|
||||
#if CACHE_POSITIONS_FORMAT == HEMISPHERES_FORMAT_R32G32B32A32
|
||||
Vector3 color(mipData->Get<Vector4>(x, y));
|
||||
#elif CACHE_POSITIONS_FORMAT == HEMISPHERES_FORMAT_R16G16B16A16
|
||||
Vector3 color = mipData->Get<Half4>(x, y).ToVector3();
|
||||
#endif
|
||||
color /= 100.0f;
|
||||
|
||||
const int32 pos = ((cacheData.PositionsData.Height - y - 1) * cacheData.PositionsData.Width + x) * 3;
|
||||
data[pos + 0] = (byte)(color.Z * 255);
|
||||
data[pos + 1] = (byte)(color.Y * 255);
|
||||
data[pos + 2] = (byte)(color.X * 255);
|
||||
}
|
||||
}
|
||||
|
||||
FileSystem::SaveBitmapToFile(data, atlasSize, atlasSize, bytesPerPixel * 8, 0, tmpPath);
|
||||
}
|
||||
|
||||
{
|
||||
auto tmpPath = GetDebugDataPath() / String::Format(TEXT("Scene{1}_lightmapCache_{0}_Normal.bmp"), _workerStagePosition0, scene->SceneIndex);
|
||||
Platform::MemoryClear(data, dataSize);
|
||||
auto mipData = cacheData.NormalsData.GetData(0, 0);
|
||||
|
||||
for (int32 x = 0; x < cacheData.NormalsData.Width; x++)
|
||||
{
|
||||
for (int32 y = 0; y < cacheData.NormalsData.Height; y++)
|
||||
{
|
||||
#if CACHE_NORMALS_FORMAT == HEMISPHERES_FORMAT_R32G32B32A32
|
||||
Vector3 color(mipData->Get<Vector4>(x, y));
|
||||
#elif CACHE_NORMALS_FORMAT == HEMISPHERES_FORMAT_R16G16B16A16
|
||||
Vector3 color = mipData->Get<Half4>(x, y).ToVector3();
|
||||
#endif
|
||||
color.Normalize();
|
||||
|
||||
const int32 pos = ((cacheData.NormalsData.Height - y - 1) * cacheData.NormalsData.Width + x) * 3;
|
||||
data[pos + 0] = (byte)(color.Z * 255);
|
||||
data[pos + 1] = (byte)(color.Y * 255);
|
||||
data[pos + 2] = (byte)(color.X * 255);
|
||||
}
|
||||
}
|
||||
|
||||
FileSystem::SaveBitmapToFile(data, atlasSize, atlasSize, bytesPerPixel * 8, 0, tmpPath);
|
||||
}
|
||||
|
||||
delete[] data;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if DEBUG_EXPORT_HEMISPHERES_PREVIEW
|
||||
|
||||
int32 DebugExportHemispheresPerAtlasRow = 32;
|
||||
int32 DebugExportHemispheresPerAtlas = DebugExportHemispheresPerAtlasRow * DebugExportHemispheresPerAtlasRow;
|
||||
int32 DebugExportHemispheresAtlasSize = DebugExportHemispheresPerAtlasRow * HEMISPHERES_RESOLUTION;
|
||||
int32 DebugExportHemispheresPosition = 0;
|
||||
Array<GPUTexture*> DebugExportHemispheresAtlases;
|
||||
|
||||
void ShadowsOfMordor::Builder::addDebugHemisphere(GPUContext* context, GPUTextureView* hemisphere)
|
||||
{
|
||||
// Get atlas
|
||||
if (DebugExportHemispheresAtlases.IsEmpty() || DebugExportHemispheresPosition >= DebugExportHemispheresPerAtlas)
|
||||
{
|
||||
DebugExportHemispheresPosition = 0;
|
||||
DebugExportHemispheresAtlases.Insert(0, RenderTargetPool::Get(GPUTextureDescription::New2D(DebugExportHemispheresAtlasSize, DebugExportHemispheresAtlasSize, PixelFormat::R32G32B32A32_Float)));
|
||||
}
|
||||
GPUTexture* atlas = DebugExportHemispheresAtlases[0];
|
||||
|
||||
// Copy frame to atlas
|
||||
context->SetRenderTarget(atlas->View());
|
||||
const int32 x = (DebugExportHemispheresPosition % DebugExportHemispheresPerAtlasRow) * HEMISPHERES_RESOLUTION;
|
||||
const int32 y = (DebugExportHemispheresPosition / DebugExportHemispheresPerAtlasRow) * HEMISPHERES_RESOLUTION;
|
||||
context->SetViewportAndScissors(Viewport(static_cast<float>(x), static_cast<float>(y), HEMISPHERES_RESOLUTION, HEMISPHERES_RESOLUTION));
|
||||
context->Draw(hemisphere);
|
||||
|
||||
// Move
|
||||
DebugExportHemispheresPosition++;
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::downloadDebugHemisphereAtlases(SceneBuildCache* scene)
|
||||
{
|
||||
for (int32 atlasIndex = 0; atlasIndex < DebugExportHemispheresAtlases.Count(); atlasIndex++)
|
||||
{
|
||||
GPUTexture* atlas = DebugExportHemispheresAtlases[atlasIndex];
|
||||
|
||||
TextureData textureData;
|
||||
if (atlas->DownloadData(&textureData))
|
||||
{
|
||||
LOG(Error, "Cannot download hemispheres atlas data.");
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
auto tmpPath = GetDebugDataPath() / String::Format(TEXT("Scene{1}_hemispheresAtlas_{0}.bmp"), atlasIndex, scene->SceneIndex);
|
||||
|
||||
int32 bytesPerPixel = 3;
|
||||
int32 dataSize = DebugExportHemispheresAtlasSize * DebugExportHemispheresAtlasSize * bytesPerPixel;
|
||||
byte* data = new byte[dataSize];
|
||||
Platform::MemoryClear(data, dataSize);
|
||||
|
||||
auto mipData = textureData.GetData(0, 0);
|
||||
auto dddd = (Vector4*)mipData->Data.Get();
|
||||
|
||||
for (int x = 0; x < textureData.Width; x++)
|
||||
{
|
||||
for (int y = 0; y < textureData.Height; y++)
|
||||
{
|
||||
int pos = ((textureData.Height - y - 1) * textureData.Width + x) * 3;
|
||||
int srcPos = (y * textureData.Width + x);
|
||||
|
||||
Vector4 color = Vector4::Clamp(dddd[srcPos], Vector4::Zero, Vector4::One);
|
||||
|
||||
data[pos + 0] = (byte)(color.Z * 255);
|
||||
data[pos + 1] = (byte)(color.Y * 255);
|
||||
data[pos + 2] = (byte)(color.X * 255);
|
||||
}
|
||||
}
|
||||
|
||||
FileSystem::SaveBitmapToFile(data, DebugExportHemispheresAtlasSize, DebugExportHemispheresAtlasSize, bytesPerPixel * 8, 0, tmpPath);
|
||||
delete[] data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::releaseDebugHemisphereAtlases()
|
||||
{
|
||||
DebugExportHemispheresPosition = 0;
|
||||
for (int32 i = 0; i < DebugExportHemispheresAtlases.Count(); i++)
|
||||
{
|
||||
RenderTargetPool::Release(DebugExportHemispheresAtlases[i]);
|
||||
}
|
||||
DebugExportHemispheresAtlases.Clear();
|
||||
}
|
||||
|
||||
#endif
|
||||
420
Source/Engine/ShadowsOfMordor/Builder.DoWork.cpp
Normal file
420
Source/Engine/ShadowsOfMordor/Builder.DoWork.cpp
Normal file
@@ -0,0 +1,420 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Builder.h"
|
||||
#include "Engine/Core/Math/Math.h"
|
||||
#include "Engine/Level/Actors/BoxBrush.h"
|
||||
#include "Engine/Level/SceneQuery.h"
|
||||
#include "Engine/Renderer/Renderer.h"
|
||||
#include "Engine/Graphics/RenderTargetPool.h"
|
||||
|
||||
#define STEPS_SLEEP_TIME 20
|
||||
#define RUN_STEP(handler) handler(); if (checkBuildCancelled()) goto BUILDING_END; Platform::Sleep(STEPS_SLEEP_TIME)
|
||||
|
||||
int32 ShadowsOfMordor::Builder::doWork()
|
||||
{
|
||||
// Start
|
||||
bool buildFailed = true;
|
||||
DateTime buildEnd, buildStart = DateTime::NowUTC();
|
||||
_lastStep = BuildProgressStep::CacheEntries;
|
||||
_lastStepStart = buildStart;
|
||||
_hemispheresPerJob = HEMISPHERES_PER_JOB_MIN;
|
||||
_hemispheresPerJobUpdateTime = DateTime::Now();
|
||||
LOG(Info, "Start building lightmaps...");
|
||||
_isActive = true;
|
||||
OnBuildStarted();
|
||||
reportProgress(BuildProgressStep::Initialize, 0.1f);
|
||||
|
||||
// Check resources and state
|
||||
if (checkBuildCancelled() || initResources())
|
||||
{
|
||||
_wasBuildCalled = false;
|
||||
|
||||
// Fire event
|
||||
_isActive = false;
|
||||
OnBuildFinished(buildFailed);
|
||||
|
||||
// Back
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Wait for the scene rendering service to be ready
|
||||
reportProgress(BuildProgressStep::Initialize, 0.5f);
|
||||
if (!Renderer::IsReady())
|
||||
{
|
||||
const int32 stepSize = 5;
|
||||
const int32 maxWaitTime = 30000;
|
||||
int32 stepsCount = static_cast<int32>(maxWaitTime / stepSize);
|
||||
while (!Renderer::IsReady() && stepsCount-- > 0)
|
||||
Platform::Sleep(stepSize);
|
||||
if (!Renderer::IsReady())
|
||||
{
|
||||
LOG(Error, "Failed to initialize Renderer service.");
|
||||
_wasBuildCalled = false;
|
||||
_isActive = false;
|
||||
OnBuildFinished(buildFailed);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Init scenes cache
|
||||
reportProgress(BuildProgressStep::Initialize, 0.7f);
|
||||
{
|
||||
Array<Scene*> scenes;
|
||||
Level::GetScenes(scenes);
|
||||
if (scenes.Count() == 0)
|
||||
{
|
||||
LOG(Warning, "No scenes to bake lightmaps.");
|
||||
_wasBuildCalled = false;
|
||||
_isActive = false;
|
||||
OnBuildFinished(false);
|
||||
return 0;
|
||||
}
|
||||
_scenes.Resize(scenes.Count());
|
||||
for (int32 sceneIndex = 0; sceneIndex < scenes.Count(); sceneIndex++)
|
||||
{
|
||||
_scenes[sceneIndex] = New<SceneBuildCache>();
|
||||
if (_scenes[sceneIndex]->Init(this, sceneIndex, scenes[sceneIndex]))
|
||||
{
|
||||
LOG(Error, "Failed to initialize Scene Build Cache data.");
|
||||
_wasBuildCalled = false;
|
||||
_isActive = false;
|
||||
OnBuildFinished(buildFailed);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IsBakingLightmaps = true;
|
||||
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
_lastStateSaveTime = DateTime::Now();
|
||||
_firstStateSave = true;
|
||||
|
||||
// Try to load the state that was cached during hemispheres rendering (restore rendering in case of GPU driver crash)
|
||||
if (loadState())
|
||||
{
|
||||
reportProgress(BuildProgressStep::RenderHemispheres, 0.0f);
|
||||
const int32 firstScene = _workerActiveSceneIndex;
|
||||
{
|
||||
// Wait for lightmaps to be fully loaded
|
||||
for (_workerActiveSceneIndex = 0; _workerActiveSceneIndex < _scenes.Count(); _workerActiveSceneIndex++)
|
||||
{
|
||||
if (_scenes[_workerActiveSceneIndex]->WaitForLightmaps())
|
||||
{
|
||||
LOG(Error, "Failed to load lightmap textures.");
|
||||
_wasBuildCalled = false;
|
||||
_isActive = false;
|
||||
OnBuildFinished(buildFailed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (checkBuildCancelled())
|
||||
goto BUILDING_END;
|
||||
}
|
||||
|
||||
// Continue the hemispheres rendering for the last scene from the cached position
|
||||
{
|
||||
_workerActiveSceneIndex = firstScene;
|
||||
if (runStage(RenderHemispheres, false))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Fill black holes with blurred data to prevent artifacts on the edges
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(PostprocessLightmaps))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Wait for GPU commands to sync
|
||||
if (waitForJobDataSync())
|
||||
goto BUILDING_END;
|
||||
|
||||
// Update lightmaps textures
|
||||
_scenes[_workerActiveSceneIndex]->UpdateLightmaps();
|
||||
}
|
||||
for (_workerActiveSceneIndex = firstScene + 1; _workerActiveSceneIndex < _scenes.Count(); _workerActiveSceneIndex++)
|
||||
{
|
||||
// Skip scenes without any lightmaps
|
||||
if (_scenes[_workerActiveSceneIndex]->Lightmaps.IsEmpty())
|
||||
continue;
|
||||
|
||||
// Clear hemispheres target
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(ClearLightmapData))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Render all registered Hemispheres rendering
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(RenderHemispheres))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Fill black holes with blurred data to prevent artifacts on the edges
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(PostprocessLightmaps))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Wait for GPU commands to sync
|
||||
if (waitForJobDataSync())
|
||||
goto BUILDING_END;
|
||||
|
||||
// Update lightmaps textures
|
||||
_scenes[_workerActiveSceneIndex]->UpdateLightmaps();
|
||||
}
|
||||
}
|
||||
for (int32 bounce = _giBounceRunningIndex + 1; bounce < _bounceCount; bounce++)
|
||||
{
|
||||
_giBounceRunningIndex = bounce;
|
||||
|
||||
// Wait for lightmaps to be fully loaded
|
||||
for (_workerActiveSceneIndex = 0; _workerActiveSceneIndex < _scenes.Count(); _workerActiveSceneIndex++)
|
||||
{
|
||||
if (_scenes[_workerActiveSceneIndex]->WaitForLightmaps())
|
||||
{
|
||||
LOG(Error, "Failed to load lightmap textures.");
|
||||
_wasBuildCalled = false;
|
||||
_isActive = false;
|
||||
OnBuildFinished(buildFailed);
|
||||
return 0;
|
||||
}
|
||||
if (checkBuildCancelled())
|
||||
goto BUILDING_END;
|
||||
}
|
||||
|
||||
// Render bounce for every scene separately
|
||||
for (_workerActiveSceneIndex = firstScene; _workerActiveSceneIndex < _scenes.Count(); _workerActiveSceneIndex++)
|
||||
{
|
||||
// Skip scenes without any lightmaps
|
||||
if (_scenes[_workerActiveSceneIndex]->Lightmaps.IsEmpty())
|
||||
continue;
|
||||
|
||||
// Clear hemispheres target
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(ClearLightmapData))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Render all registered Hemispheres rendering
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(RenderHemispheres))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Fill black holes with blurred data to prevent artifacts on the edges
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(PostprocessLightmaps))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Wait for GPU commands to sync
|
||||
if (waitForJobDataSync())
|
||||
goto BUILDING_END;
|
||||
|
||||
// Update lightmaps textures
|
||||
_scenes[_workerActiveSceneIndex]->UpdateLightmaps();
|
||||
}
|
||||
}
|
||||
reportProgress(BuildProgressStep::RenderHemispheres, 1.0f);
|
||||
goto BUILDING_END;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Compute the final weight for integration
|
||||
{
|
||||
float weightSum = 0.0f;
|
||||
for (uint32 y = 0; y < HEMISPHERES_RESOLUTION; y++)
|
||||
{
|
||||
const float v = (float(y) / float(HEMISPHERES_RESOLUTION)) * 2.0f - 1.0f;
|
||||
for (uint32 x = 0; x < HEMISPHERES_RESOLUTION; x++)
|
||||
{
|
||||
const float u = (float(x) / float(HEMISPHERES_RESOLUTION)) * 2.0f - 1.0f;
|
||||
const float t = 1.0f + u * u + v * v;
|
||||
const float weight = 4.0f / (Math::Sqrt(t) * t);
|
||||
weightSum += weight;
|
||||
}
|
||||
}
|
||||
weightSum *= 6;
|
||||
_hemisphereTexelsTotalWeight = (4.0f * PI) / weightSum;
|
||||
}
|
||||
|
||||
// Initialize the lightmaps and pack entries to the charts
|
||||
for (_workerActiveSceneIndex = 0; _workerActiveSceneIndex < _scenes.Count(); _workerActiveSceneIndex++)
|
||||
{
|
||||
RUN_STEP(cacheEntries);
|
||||
RUN_STEP(generateCharts);
|
||||
RUN_STEP(packCharts);
|
||||
RUN_STEP(updateLightmaps);
|
||||
RUN_STEP(updateEntries);
|
||||
}
|
||||
|
||||
// TODO: if settings require wait for asset dependencies to all materials and models be loaded (maybe only for higher quality profiles)
|
||||
|
||||
// Generate hemispheres cache and prepare for baking
|
||||
for (_workerActiveSceneIndex = 0; _workerActiveSceneIndex < _scenes.Count(); _workerActiveSceneIndex++)
|
||||
{
|
||||
// Wait for lightmaps to be fully loaded
|
||||
if (_scenes[_workerActiveSceneIndex]->WaitForLightmaps())
|
||||
{
|
||||
LOG(Error, "Failed to load lightmap textures.");
|
||||
_wasBuildCalled = false;
|
||||
_isActive = false;
|
||||
OnBuildFinished(buildFailed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ASSERT(_cachePositions == nullptr && _cacheNormals == nullptr);
|
||||
const int32 atlasSize = (int32)_scenes[_workerActiveSceneIndex]->GetSettings().AtlasSize;
|
||||
auto tempDesc = GPUTextureDescription::New2D(atlasSize, atlasSize, HemispheresFormatToPixelFormat[CACHE_POSITIONS_FORMAT]);
|
||||
_cachePositions = RenderTargetPool::Get(tempDesc);
|
||||
tempDesc.Format = HemispheresFormatToPixelFormat[CACHE_NORMALS_FORMAT];
|
||||
_cacheNormals = RenderTargetPool::Get(tempDesc);
|
||||
if (_cachePositions == nullptr || _cacheNormals == nullptr)
|
||||
goto BUILDING_END;
|
||||
|
||||
generateHemispheres();
|
||||
|
||||
RenderTargetPool::Release(_cachePositions);
|
||||
_cachePositions = nullptr;
|
||||
RenderTargetPool::Release(_cacheNormals);
|
||||
_cacheNormals = nullptr;
|
||||
|
||||
if (checkBuildCancelled())
|
||||
goto BUILDING_END;
|
||||
Platform::Sleep(STEPS_SLEEP_TIME);
|
||||
}
|
||||
|
||||
// Prepare before actual baking
|
||||
int32 hemispheresCount = 0;
|
||||
int32 mergedHemispheresCount = 0;
|
||||
int32 bounceCount = 0;
|
||||
int32 lightmapsCount = 0;
|
||||
int32 entriesCount = 0;
|
||||
for (int32 sceneIndex = 0; sceneIndex < _scenes.Count(); sceneIndex++)
|
||||
{
|
||||
auto& scene = *_scenes[sceneIndex];
|
||||
hemispheresCount += scene.HemispheresCount;
|
||||
mergedHemispheresCount += scene.MergedHemispheresCount;
|
||||
lightmapsCount += scene.Lightmaps.Count();
|
||||
entriesCount += scene.Entries.Count();
|
||||
bounceCount = Math::Max(bounceCount, scene.GetSettings().BounceCount);
|
||||
|
||||
// Cleanup unused data to reduce memory usage
|
||||
scene.Entries.Resize(0);
|
||||
scene.Charts.Resize(0);
|
||||
for (auto& lightmap : scene.Lightmaps)
|
||||
lightmap.Entries.Resize(0);
|
||||
}
|
||||
_bounceCount = bounceCount;
|
||||
LOG(Info, "Rendering {0} hemispheres in {1} bounce(s) (merged: {2})", hemispheresCount, bounceCount, mergedHemispheresCount);
|
||||
if (bounceCount <= 0 || hemispheresCount <= 0)
|
||||
{
|
||||
LOG(Warning, "No data to render");
|
||||
goto BUILDING_END;
|
||||
}
|
||||
|
||||
// For each bounce
|
||||
for (int32 bounce = 0; bounce < _bounceCount; bounce++)
|
||||
{
|
||||
_giBounceRunningIndex = bounce;
|
||||
|
||||
// Wait for lightmaps to be fully loaded
|
||||
for (_workerActiveSceneIndex = 0; _workerActiveSceneIndex < _scenes.Count(); _workerActiveSceneIndex++)
|
||||
{
|
||||
if (_scenes[_workerActiveSceneIndex]->WaitForLightmaps())
|
||||
{
|
||||
LOG(Error, "Failed to load lightmap textures.");
|
||||
_wasBuildCalled = false;
|
||||
_isActive = false;
|
||||
OnBuildFinished(buildFailed);
|
||||
return 0;
|
||||
}
|
||||
if (checkBuildCancelled())
|
||||
goto BUILDING_END;
|
||||
}
|
||||
|
||||
// Render bounce for every scene separately
|
||||
for (_workerActiveSceneIndex = 0; _workerActiveSceneIndex < _scenes.Count(); _workerActiveSceneIndex++)
|
||||
{
|
||||
// Skip scenes without any lightmaps
|
||||
if (_scenes[_workerActiveSceneIndex]->Lightmaps.IsEmpty())
|
||||
continue;
|
||||
|
||||
// Clear hemispheres target
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(ClearLightmapData))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Render all registered Hemispheres rendering
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(RenderHemispheres))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Fill black holes with blurred data to prevent artifacts on the edges
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(PostprocessLightmaps))
|
||||
goto BUILDING_END;
|
||||
|
||||
// Wait for GPU commands to sync
|
||||
if (waitForJobDataSync())
|
||||
goto BUILDING_END;
|
||||
|
||||
// Update lightmaps textures
|
||||
_scenes[_workerActiveSceneIndex]->UpdateLightmaps();
|
||||
}
|
||||
}
|
||||
|
||||
reportProgress(BuildProgressStep::RenderHemispheres, 1.0f);
|
||||
|
||||
#if DEBUG_EXPORT_HEMISPHERES_PREVIEW
|
||||
for (int32 sceneIndex = 0; sceneIndex < _scenes.Count(); sceneIndex++)
|
||||
downloadDebugHemisphereAtlases(_scenes[sceneIndex]);
|
||||
#endif
|
||||
|
||||
// References:
|
||||
// "Optimization of numerical calculations execution time in multiprocessor systems" - Wojciech Figat
|
||||
// https://knarkowicz.wordpress.com/2014/07/20/lightmapping-in-anomaly-2-mobile/
|
||||
// http://the-witness.net/news/2010/09/hemicube-rendering-and-integration/
|
||||
// http://the-witness.net/news/2010/03/graphics-tech-texture-parameterization/
|
||||
// http://the-witness.net/news/2010/03/graphics-tech-lighting-comparison/
|
||||
|
||||
// Some ideas:
|
||||
// - render hemispheres to atlas or sth and batch integration and downscalling for multiply texels
|
||||
// - use conservative rasterization for dx12 instead of blur or MSAA for all platforms
|
||||
// - use hemisphere depth buffer to compute AO
|
||||
|
||||
// End
|
||||
const int32 hemispheresRenderedCount = hemispheresCount * bounceCount;
|
||||
buildEnd = DateTime::NowUTC();
|
||||
LOG(Info, "Building lightmap finished! Time: {0}s, Lightmaps: {1}, Entries: {2}, Hemicubes rendered: {3}",
|
||||
static_cast<int32>((buildEnd - buildStart).GetTotalSeconds()),
|
||||
lightmapsCount,
|
||||
entriesCount,
|
||||
hemispheresRenderedCount);
|
||||
buildFailed = false;
|
||||
|
||||
BUILDING_END:
|
||||
|
||||
// Cleanup cached data
|
||||
reportProgress(BuildProgressStep::Cleanup, 0.0f);
|
||||
|
||||
_locker.Lock();
|
||||
|
||||
// Clear
|
||||
_wasBuildCalled = false;
|
||||
IsBakingLightmaps = false;
|
||||
if (!Globals::FatalErrorOccurred)
|
||||
deleteState();
|
||||
|
||||
// Release scenes data
|
||||
reportProgress(BuildProgressStep::Cleanup, 0.5f);
|
||||
for (int32 sceneIndex = 0; sceneIndex < _scenes.Count(); sceneIndex++)
|
||||
_scenes[sceneIndex]->Release();
|
||||
_scenes.ClearDelete();
|
||||
_scenes.Resize(0);
|
||||
|
||||
_locker.Unlock();
|
||||
|
||||
// Cleanup
|
||||
releaseResources();
|
||||
|
||||
// Fire events
|
||||
reportProgress(BuildProgressStep::Cleanup, 1.0f);
|
||||
_isActive = false;
|
||||
OnBuildFinished(buildFailed);
|
||||
|
||||
return 0;
|
||||
}
|
||||
271
Source/Engine/ShadowsOfMordor/Builder.Entries.cpp
Normal file
271
Source/Engine/ShadowsOfMordor/Builder.Entries.cpp
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Builder.h"
|
||||
#include "Engine/Core/Math/Math.h"
|
||||
#include "Engine/Level/Actors/BoxBrush.h"
|
||||
#include "Engine/Level/Actors/StaticModel.h"
|
||||
#include "Engine/ContentImporters/ImportTexture.h"
|
||||
#include "Engine/Level/Scene/Scene.h"
|
||||
#include "Engine/Level/Level.h"
|
||||
#include "Engine/Terrain/Terrain.h"
|
||||
#include "Engine/Terrain/TerrainPatch.h"
|
||||
#include "Engine/Foliage/Foliage.h"
|
||||
|
||||
bool canUseMaterialWithLightmap(MaterialBase* material, ShadowsOfMordor::Builder::SceneBuildCache* scene)
|
||||
{
|
||||
// Check objects with missing materials can be used
|
||||
if (material == nullptr)
|
||||
return scene->GetSettings().UseGeometryWithNoMaterials;
|
||||
|
||||
return material->CanUseLightmap();
|
||||
}
|
||||
|
||||
bool cacheStaticGeometryTree(Actor* actor, ShadowsOfMordor::Builder::SceneBuildCache* scene)
|
||||
{
|
||||
ShadowsOfMordor::Builder::GeometryEntry entry;
|
||||
const bool useLightmap = actor->GetIsActive() && (actor->GetStaticFlags() & StaticFlags::Lightmap);
|
||||
auto& results = scene->Entries;
|
||||
|
||||
// Switch actor type
|
||||
if (auto staticModel = dynamic_cast<StaticModel*>(actor))
|
||||
{
|
||||
// Check if model has been linked and is loaded
|
||||
auto model = staticModel->Model.Get();
|
||||
if (model && !model->WaitForLoaded())
|
||||
{
|
||||
entry.Type = ShadowsOfMordor::Builder::GeometryType::StaticModel;
|
||||
entry.UVsBox = Rectangle(Vector2::Zero, Vector2::One);
|
||||
entry.AsStaticModel.Actor = staticModel;
|
||||
entry.Scale = Math::Clamp(staticModel->GetScaleInLightmap(), 0.0f, LIGHTMAP_SCALE_MAX);
|
||||
|
||||
// Spawn entry for each mesh
|
||||
Matrix world;
|
||||
staticModel->GetWorld(&world);
|
||||
|
||||
// Use the first LOD
|
||||
const int32 lodIndex = 0;
|
||||
auto& lod = model->LODs[lodIndex];
|
||||
|
||||
bool anyValid = false;
|
||||
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
|
||||
{
|
||||
const auto& mesh = lod.Meshes[meshIndex];
|
||||
auto& bufferEntry = staticModel->Entries[mesh.GetMaterialSlotIndex()];
|
||||
if (bufferEntry.Visible && canUseMaterialWithLightmap(staticModel->GetMaterial(meshIndex), scene))
|
||||
{
|
||||
if (mesh.HasLightmapUVs())
|
||||
{
|
||||
anyValid = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(Warning, "Model \'{0}\' mesh index {1} (lod: {2}) has missing lightmap UVs (at actor: {3})",
|
||||
model->GetPath(),
|
||||
meshIndex,
|
||||
lodIndex,
|
||||
staticModel->GetName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (useLightmap && anyValid && entry.Scale > ZeroTolerance)
|
||||
{
|
||||
entry.Box = model->GetBox(world);
|
||||
results.Add(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
staticModel->RemoveLightmap();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (auto terrain = dynamic_cast<Terrain*>(actor))
|
||||
{
|
||||
entry.AsTerrain.Actor = terrain;
|
||||
entry.Type = ShadowsOfMordor::Builder::GeometryType::Terrain;
|
||||
entry.UVsBox = Rectangle(Vector2::Zero, Vector2::One);
|
||||
entry.Scale = Math::Clamp(terrain->GetScaleInLightmap(), 0.0f, LIGHTMAP_SCALE_MAX);
|
||||
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchIndex);
|
||||
entry.AsTerrain.PatchIndex = patchIndex;
|
||||
for (int32 chunkIndex = 0; chunkIndex < TerrainPatch::CHUNKS_COUNT; chunkIndex++)
|
||||
{
|
||||
auto chunk = patch->Chunks[chunkIndex];
|
||||
entry.AsTerrain.ChunkIndex = chunkIndex;
|
||||
auto material = chunk.OverrideMaterial ? chunk.OverrideMaterial.Get() : terrain->Material.Get();
|
||||
const bool canUseLightmap = useLightmap
|
||||
&& canUseMaterialWithLightmap(material, scene)
|
||||
&& entry.Scale > ZeroTolerance;
|
||||
if (canUseLightmap)
|
||||
{
|
||||
entry.Box = chunk.GetBounds();
|
||||
results.Add(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
chunk.RemoveLightmap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (auto foliage = dynamic_cast<Foliage*>(actor))
|
||||
{
|
||||
entry.AsFoliage.Actor = foliage;
|
||||
entry.Type = ShadowsOfMordor::Builder::GeometryType::Foliage;
|
||||
entry.UVsBox = Rectangle(Vector2::Zero, Vector2::One);
|
||||
for (auto i = foliage->Instances.Begin(); i.IsNotEnd(); ++i)
|
||||
{
|
||||
auto& instance = *i;
|
||||
auto& type = foliage->FoliageTypes[instance.Type];
|
||||
entry.AsFoliage.InstanceIndex = i.Index();
|
||||
entry.AsFoliage.TypeIndex = instance.Type;
|
||||
entry.Scale = Math::Clamp(type.ScaleInLightmap, 0.0f, LIGHTMAP_SCALE_MAX);
|
||||
const bool canUseLightmap = useLightmap
|
||||
&& canUseMaterialWithLightmap(type.Entries[0].Material, scene)
|
||||
&& entry.Scale > ZeroTolerance;
|
||||
auto model = type.Model.Get();
|
||||
if (canUseLightmap && model && !model->WaitForLoaded())
|
||||
{
|
||||
BoundingBox::FromSphere(instance.Bounds, entry.Box);
|
||||
const int32 lodIndex = 0;
|
||||
auto& lod = model->LODs[lodIndex];
|
||||
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
|
||||
{
|
||||
entry.AsFoliage.MeshIndex = meshIndex;
|
||||
results.Add(entry);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
instance.RemoveLightmap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actor->GetIsActive();
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::cacheEntries()
|
||||
{
|
||||
auto scene = _scenes[_workerActiveSceneIndex];
|
||||
|
||||
reportProgress(BuildProgressStep::CacheEntries, 0);
|
||||
scene->EntriesLocker.Lock();
|
||||
|
||||
// Gather static scene geometry entries
|
||||
Function<bool(Actor*, SceneBuildCache*)> cacheGeometry = &cacheStaticGeometryTree;
|
||||
scene->Scene->TreeExecute(cacheGeometry, scene);
|
||||
|
||||
scene->EntriesLocker.Unlock();
|
||||
reportProgress(BuildProgressStep::CacheEntries, 1.0f);
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::updateEntries()
|
||||
{
|
||||
auto scene = _scenes[_workerActiveSceneIndex];
|
||||
|
||||
reportProgress(BuildProgressStep::UpdateEntries, 0.0f);
|
||||
scene->EntriesLocker.Lock();
|
||||
|
||||
// Update entries (and cache entries list per lightmap as linear array instead of tree structure)
|
||||
ScopeLock lock(Level::ScenesLock); // TODO: maybe don;t lock scene?
|
||||
const int32 entriesCount = scene->Entries.Count();
|
||||
for (int32 i = 0; i < entriesCount; i++)
|
||||
{
|
||||
auto& e = scene->Entries[i];
|
||||
if (e.ChartIndex == INVALID_INDEX)
|
||||
{
|
||||
// Removed previously baked lightmap info
|
||||
LightmapEntry emptyEntry;
|
||||
switch (e.Type)
|
||||
{
|
||||
case GeometryType::StaticModel:
|
||||
{
|
||||
auto staticModel = e.AsStaticModel.Actor;
|
||||
if (staticModel)
|
||||
staticModel->Lightmap = emptyEntry;
|
||||
}
|
||||
break;
|
||||
case GeometryType::Terrain:
|
||||
{
|
||||
auto terrain = e.AsTerrain.Actor;
|
||||
if (terrain)
|
||||
terrain->GetPatch(e.AsTerrain.PatchIndex)->Chunks[e.AsTerrain.ChunkIndex].Lightmap = emptyEntry;
|
||||
}
|
||||
break;
|
||||
case GeometryType::Foliage:
|
||||
{
|
||||
auto foliage = e.AsFoliage.Actor;
|
||||
if (foliage)
|
||||
foliage->Instances[e.AsFoliage.InstanceIndex].Lightmap = emptyEntry;
|
||||
}
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
auto& chart = scene->Charts[e.ChartIndex];
|
||||
|
||||
// Update result uvs by taking into account lightmap uvs box
|
||||
chart.Result.UVsArea.Size /= e.UVsBox.Size;
|
||||
chart.Result.UVsArea.Location += e.UVsBox.Location * chart.Result.UVsArea.Size;
|
||||
|
||||
switch (e.Type)
|
||||
{
|
||||
case GeometryType::StaticModel:
|
||||
{
|
||||
auto staticModel = e.AsStaticModel.Actor;
|
||||
if (staticModel)
|
||||
{
|
||||
// Update data
|
||||
staticModel->Lightmap = chart.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Discard chart due to data leaks
|
||||
chart.Result.TextureIndex = INVALID_INDEX;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GeometryType::Terrain:
|
||||
{
|
||||
auto terrain = e.AsTerrain.Actor;
|
||||
if (terrain)
|
||||
{
|
||||
// Update data
|
||||
terrain->GetPatch(e.AsTerrain.PatchIndex)->Chunks[e.AsTerrain.ChunkIndex].Lightmap = chart.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Discard chart due to data leaks
|
||||
chart.Result.TextureIndex = INVALID_INDEX;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GeometryType::Foliage:
|
||||
{
|
||||
auto foliage = e.AsFoliage.Actor;
|
||||
if (foliage)
|
||||
{
|
||||
// Update data
|
||||
foliage->Instances[e.AsFoliage.InstanceIndex].Lightmap = chart.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Discard chart due to data leaks
|
||||
chart.Result.TextureIndex = INVALID_INDEX;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Cache entry link
|
||||
if (chart.Result.TextureIndex != INVALID_INDEX)
|
||||
scene->Lightmaps[chart.Result.TextureIndex].Entries.Add(i);
|
||||
|
||||
reportProgress(BuildProgressStep::UpdateEntries, static_cast<float>(i) / entriesCount);
|
||||
}
|
||||
|
||||
scene->EntriesLocker.Unlock();
|
||||
reportProgress(BuildProgressStep::UpdateEntries, 1.0f);
|
||||
}
|
||||
182
Source/Engine/ShadowsOfMordor/Builder.Hemispheres.cpp
Normal file
182
Source/Engine/ShadowsOfMordor/Builder.Hemispheres.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Builder.h"
|
||||
#include "Engine/Core/Math/Math.h"
|
||||
#include "Engine/Level/SceneQuery.h"
|
||||
#include "Engine/Level/Actors/BoxBrush.h"
|
||||
#include "Engine/ContentImporters/ImportTexture.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
void SampleCache(ShadowsOfMordor::GenerateHemispheresData& data, int32 texelX, int32 texelY, Vector3& outPosition, Vector3& outNormal)
|
||||
{
|
||||
const auto mipDataPositions = data.PositionsData.GetData(0, 0);
|
||||
#if CACHE_POSITIONS_FORMAT == HEMISPHERES_FORMAT_R32G32B32A32
|
||||
outPosition = Vector3(mipDataPositions->Get<Vector4>(texelX, texelY));
|
||||
#elif CACHE_POSITIONS_FORMAT == HEMISPHERES_FORMAT_R16G16B16A16
|
||||
outPosition = mipDataPositions->Get<Half4>(texelX, texelY).ToVector3();
|
||||
#else
|
||||
#error "Unknown format."
|
||||
#endif
|
||||
|
||||
const auto mipDataNormals = data.NormalsData.GetData(0, 0);
|
||||
#if CACHE_NORMALS_FORMAT == HEMISPHERES_FORMAT_R32G32B32A32
|
||||
outNormal = Vector3(mipDataNormals->Get<Vector4>(texelX, texelY));
|
||||
#elif CACHE_NORMALS_FORMAT == HEMISPHERES_FORMAT_R16G16B16A16
|
||||
outNormal = mipDataNormals->Get<Half4>(texelX, texelY).ToVector3();
|
||||
#else
|
||||
#error "Unknown format."
|
||||
#endif
|
||||
}
|
||||
|
||||
void RejectTexel(ShadowsOfMordor::GenerateHemispheresData& data, int32 texelX, int32 texelY)
|
||||
{
|
||||
const auto mipDataNormals = data.NormalsData.GetData(0, 0);
|
||||
#if CACHE_NORMALS_FORMAT == HEMISPHERES_FORMAT_R32G32B32A32
|
||||
mipDataNormals->Get<Vector4>(texelX, texelY) = Vector4::Zero;
|
||||
#elif CACHE_NORMALS_FORMAT == HEMISPHERES_FORMAT_R16G16B16A16
|
||||
mipDataNormals->Get<Half4>(texelX, texelY) = Half4::Zero;
|
||||
#else
|
||||
#error "Unknown format."
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::generateHemispheres()
|
||||
{
|
||||
reportProgress(BuildProgressStep::GenerateHemispheresCache, 0.0f);
|
||||
|
||||
// Clear all lightmaps
|
||||
_workerStagePosition0 = 0;
|
||||
if (runStage(CleanLightmaps))
|
||||
return;
|
||||
|
||||
auto scene = _scenes[_workerActiveSceneIndex];
|
||||
auto lightmapsCount = scene->Lightmaps.Count();
|
||||
auto& settings = scene->GetSettings();
|
||||
|
||||
// Collect Hemispheres render tasks
|
||||
int32 hemispheresCount = 0, mergedHemispheresCount = 0;
|
||||
GenerateHemispheresData cacheData;
|
||||
|
||||
// Config
|
||||
float normalizedQuality = Math::Saturate((float)settings.Quality / 100.0f);
|
||||
float maxMergeRadius = Math::Lerp(5.0f, 1.0f, normalizedQuality) / LightmapTexelsPerWorldUnit;
|
||||
float normalSimilarityMin = Math::Lerp(0.8f, 0.95f, normalizedQuality);
|
||||
int32 maxTexelsDistance = static_cast<int32>(Math::Lerp(2.0f, 1.0f, normalizedQuality));
|
||||
int32 atlasSize = static_cast<int32>(settings.AtlasSize);
|
||||
|
||||
// Process every lightmap
|
||||
for (_workerStagePosition0 = 0; _workerStagePosition0 < lightmapsCount; _workerStagePosition0++)
|
||||
{
|
||||
// Prepare
|
||||
auto& lightmapEntry = scene->Lightmaps[_workerStagePosition0];
|
||||
lightmapEntry.Hemispheres.Clear();
|
||||
lightmapEntry.Hemispheres.EnsureCapacity(Math::Square(atlasSize / 2));
|
||||
Vector3 position, normal;
|
||||
|
||||
// Fill cache
|
||||
if (runStage(RenderCache))
|
||||
return;
|
||||
|
||||
// Post-process cache
|
||||
if (runStage(PostprocessCache))
|
||||
return;
|
||||
|
||||
// Wait for GPU commands to sync
|
||||
if (waitForJobDataSync())
|
||||
return;
|
||||
if (checkBuildCancelled())
|
||||
return;
|
||||
|
||||
// Download cache to CPU memory from GPU memory
|
||||
if (_cachePositions->DownloadData(cacheData.PositionsData)
|
||||
|| _cacheNormals->DownloadData(cacheData.NormalsData))
|
||||
{
|
||||
LOG(Fatal, "Cannot download data from the GPU. Target: ShadowsOfMordor::Builder::RenderPositionsAndNormals");
|
||||
return;
|
||||
}
|
||||
if (checkBuildCancelled())
|
||||
return;
|
||||
|
||||
#if DEBUG_EXPORT_CACHE_PREVIEW
|
||||
// Here we can export cache to drive
|
||||
exportCachePreview(scene, cacheData, lightmapEntry);
|
||||
#endif
|
||||
|
||||
// For each texel
|
||||
for (int32 texelX = 0; texelX < atlasSize; texelX++)
|
||||
{
|
||||
for (int32 texelY = 0; texelY < atlasSize; texelY++)
|
||||
{
|
||||
// Sample cache for current texel
|
||||
SampleCache(cacheData, texelX, texelY, position, normal);
|
||||
|
||||
// Reject 'empty' texels
|
||||
if (normal.IsZero())
|
||||
continue;
|
||||
normal.Normalize();
|
||||
|
||||
// Try to merge similar hemispheres (threshold values are controlled by the quality slider)
|
||||
int32 mergedCount = 1;
|
||||
Vector3 mergedSumPos = position, mergedSumNorm = normal;
|
||||
for (int32 x = -maxTexelsDistance; x <= maxTexelsDistance; x++)
|
||||
{
|
||||
for (int32 y = -maxTexelsDistance; y <= maxTexelsDistance; y++)
|
||||
{
|
||||
int32 xx = Math::Clamp(x + texelX, 0, atlasSize - 1);
|
||||
int32 yy = Math::Clamp(y + texelY, 0, atlasSize - 1);
|
||||
|
||||
// Skip current texel
|
||||
if (xx == texelX && yy == texelY)
|
||||
continue;
|
||||
|
||||
// Sample cache for possible to use texel
|
||||
Vector3 pp, nn;
|
||||
SampleCache(cacheData, xx, yy, pp, nn);
|
||||
nn.Normalize();
|
||||
|
||||
if (Vector3::Distance(position, pp) <= maxMergeRadius
|
||||
&& Vector3::Dot(normal, nn) >= normalSimilarityMin)
|
||||
{
|
||||
// Merge them!
|
||||
mergedCount++;
|
||||
mergedSumPos += pp;
|
||||
mergedSumNorm += nn;
|
||||
|
||||
// Remove this hemisphere
|
||||
RejectTexel(cacheData, xx, yy);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mergedCount > 1)
|
||||
mergedHemispheresCount += mergedCount;
|
||||
|
||||
// TODO: check if we need to use avg pos and normal?
|
||||
//position = mergedSumPos / mergedCount;
|
||||
//normal = mergedSumNorm / mergedCount;
|
||||
//normal.Normalize();
|
||||
|
||||
// Enqueue hemisphere data to perform batched rendering
|
||||
HemisphereData data;
|
||||
data.Position = position;
|
||||
data.Normal = normal;
|
||||
data.TexelX = texelX;
|
||||
data.TexelY = texelY;
|
||||
lightmapEntry.Hemispheres.Add(data);
|
||||
hemispheresCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Progress Point
|
||||
reportProgress(BuildProgressStep::GenerateHemispheresCache, (float)_workerStagePosition0 / lightmapsCount);
|
||||
if (checkBuildCancelled())
|
||||
return;
|
||||
}
|
||||
|
||||
// Update stats
|
||||
scene->HemispheresCount = hemispheresCount;
|
||||
scene->MergedHemispheresCount = mergedHemispheresCount;
|
||||
|
||||
reportProgress(BuildProgressStep::GenerateHemispheresCache, 1.0f);
|
||||
}
|
||||
561
Source/Engine/ShadowsOfMordor/Builder.Jobs.cpp
Normal file
561
Source/Engine/ShadowsOfMordor/Builder.Jobs.cpp
Normal file
@@ -0,0 +1,561 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Builder.h"
|
||||
#include "Engine/Engine/Engine.h"
|
||||
#include "Engine/Renderer/Renderer.h"
|
||||
#include "Engine/Level/Scene/Lightmap.h"
|
||||
#include "Engine/Level/Actors/StaticModel.h"
|
||||
#include "Engine/Level/Actors/BoxBrush.h"
|
||||
#include "Engine/Level/Scene/Scene.h"
|
||||
#include "Engine/Graphics/GPUContext.h"
|
||||
#include "Engine/Graphics/Shaders/GPUConstantBuffer.h"
|
||||
#include "Engine/Graphics/RenderTargetPool.h"
|
||||
#include "Engine/Graphics/PixelFormatExtensions.h"
|
||||
#include "Engine/Terrain/Terrain.h"
|
||||
#include "Engine/Terrain/TerrainPatch.h"
|
||||
#include "Engine/Terrain/TerrainManager.h"
|
||||
#include "Engine/Foliage/Foliage.h"
|
||||
#include "Engine/Profiler/Profiler.h"
|
||||
|
||||
namespace ShadowsOfMordor
|
||||
{
|
||||
PACK_STRUCT(struct ShaderData {
|
||||
Rectangle LightmapArea;
|
||||
Matrix WorldMatrix;
|
||||
Matrix ToTangentSpace;
|
||||
float FinalWeight;
|
||||
uint32 TexelAddress;
|
||||
uint32 AtlasSize;
|
||||
float TerrainChunkSizeLOD0;
|
||||
Vector4 HeightmapUVScaleBias;
|
||||
Vector3 WorldInvScale;
|
||||
float Dummy1;
|
||||
});
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::onJobRender(GPUContext* context)
|
||||
{
|
||||
auto scene = _scenes[_workerActiveSceneIndex];
|
||||
int32 atlasSize = (int32)scene->GetSettings().AtlasSize;
|
||||
|
||||
switch (_stage)
|
||||
{
|
||||
case CleanLightmaps:
|
||||
{
|
||||
PROFILE_GPU_CPU_NAMED("CleanLightmaps");
|
||||
|
||||
uint32 cleanerSize = 0;
|
||||
for (int32 i = 0; i < scene->Lightmaps.Count(); i++)
|
||||
{
|
||||
auto lightmap = scene->Scene->LightmapsData.GetLightmap(_workerStagePosition0);
|
||||
GPUTexture* textures[NUM_SH_TARGETS];
|
||||
lightmap->GetTextures(textures);
|
||||
for (int32 textureIndex = 0; textureIndex < NUM_SH_TARGETS; textureIndex++)
|
||||
cleanerSize = Math::Max(textures[textureIndex]->SlicePitch(), cleanerSize);
|
||||
}
|
||||
auto cleaner = Allocator::Allocate(cleanerSize);
|
||||
Platform::MemoryClear(cleaner, cleanerSize);
|
||||
|
||||
for (; _workerStagePosition0 < scene->Lightmaps.Count(); _workerStagePosition0++)
|
||||
{
|
||||
auto lightmap = scene->Scene->LightmapsData.GetLightmap(_workerStagePosition0);
|
||||
GPUTexture* textures[NUM_SH_TARGETS];
|
||||
lightmap->GetTextures(textures);
|
||||
for (int32 textureIndex = 0; textureIndex < NUM_SH_TARGETS; textureIndex++)
|
||||
{
|
||||
auto texture = textures[textureIndex];
|
||||
for (int32 mipIndex = 0; mipIndex < texture->MipLevels(); mipIndex++)
|
||||
{
|
||||
uint32 rowPitch, slicePitch;
|
||||
texture->ComputePitch(mipIndex, rowPitch, slicePitch);
|
||||
context->UpdateTexture(textures[textureIndex], 0, 0, cleaner, rowPitch, slicePitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Allocator::Free(cleaner);
|
||||
_wasStageDone = true;
|
||||
break;
|
||||
}
|
||||
case RenderCache:
|
||||
{
|
||||
PROFILE_GPU_CPU_NAMED("RenderCache");
|
||||
|
||||
scene->EntriesLocker.Lock();
|
||||
|
||||
int32 entriesToRenderLeft = CACHE_ENTRIES_PER_JOB;
|
||||
auto& lightmapEntry = scene->Lightmaps[_workerStagePosition0];
|
||||
ShaderData shaderData;
|
||||
GPUTextureView* rts[2] =
|
||||
{
|
||||
_cachePositions->View(),
|
||||
_cacheNormals->View(),
|
||||
};
|
||||
context->SetRenderTarget(nullptr, ToSpan(rts, ARRAY_COUNT(rts)));
|
||||
float atlasSizeFloat = (float)atlasSize;
|
||||
context->SetViewportAndScissors(atlasSizeFloat, atlasSizeFloat);
|
||||
|
||||
// Clear targets if there is no progress for that lightmap (no entries rendered at all)
|
||||
if (_workerStagePosition1 == 0)
|
||||
{
|
||||
context->Clear(_cachePositions->View(), Color::Black);
|
||||
context->Clear(_cacheNormals->View(), Color::Black);
|
||||
}
|
||||
|
||||
for (; _workerStagePosition1 < lightmapEntry.Entries.Count(); _workerStagePosition1++)
|
||||
{
|
||||
if (entriesToRenderLeft == 0)
|
||||
break;
|
||||
entriesToRenderLeft--;
|
||||
|
||||
// Render entry
|
||||
auto& entry = scene->Entries[lightmapEntry.Entries[_workerStagePosition1]];
|
||||
auto cb = _shader->GetShader()->GetCB(0);
|
||||
switch (entry.Type)
|
||||
{
|
||||
case GeometryType::StaticModel:
|
||||
{
|
||||
auto staticModel = entry.AsStaticModel.Actor;
|
||||
auto& lod = staticModel->Model->LODs[0];
|
||||
|
||||
Matrix worldMatrix;
|
||||
staticModel->GetWorld(&worldMatrix);
|
||||
Matrix::Transpose(worldMatrix, shaderData.WorldMatrix);
|
||||
shaderData.LightmapArea = staticModel->Lightmap.UVsArea;
|
||||
|
||||
context->UpdateCB(cb, &shaderData);
|
||||
context->BindCB(0, cb);
|
||||
context->SetState(_psRenderCacheModel);
|
||||
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
|
||||
{
|
||||
auto& mesh = lod.Meshes[meshIndex];
|
||||
auto& materialSlot = staticModel->Entries[mesh.GetMaterialSlotIndex()];
|
||||
|
||||
if (materialSlot.Visible && mesh.HasLightmapUVs())
|
||||
{
|
||||
mesh.Render(context);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case GeometryType::Terrain:
|
||||
{
|
||||
auto terrain = entry.AsTerrain.Actor;
|
||||
auto patch = terrain->GetPatch(entry.AsTerrain.PatchIndex);
|
||||
auto chunk = &patch->Chunks[entry.AsTerrain.ChunkIndex];
|
||||
auto chunkSize = terrain->GetChunkSize();
|
||||
const auto heightmap = patch->Heightmap.Get()->GetTexture();
|
||||
|
||||
Matrix world;
|
||||
chunk->GetWorld(&world);
|
||||
Matrix::Transpose(world, shaderData.WorldMatrix);
|
||||
shaderData.LightmapArea = chunk->Lightmap.UVsArea;
|
||||
shaderData.TerrainChunkSizeLOD0 = TERRAIN_UNITS_PER_VERTEX * chunkSize;
|
||||
chunk->GetHeightmapUVScaleBias(&shaderData.HeightmapUVScaleBias);
|
||||
|
||||
// Extract per axis scales from LocalToWorld transform
|
||||
const float scaleX = Vector3(world.M11, world.M12, world.M13).Length();
|
||||
const float scaleY = Vector3(world.M21, world.M22, world.M23).Length();
|
||||
const float scaleZ = Vector3(world.M31, world.M32, world.M33).Length();
|
||||
shaderData.WorldInvScale = Vector3(
|
||||
scaleX > 0.00001f ? 1.0f / scaleX : 0.0f,
|
||||
scaleY > 0.00001f ? 1.0f / scaleY : 0.0f,
|
||||
scaleZ > 0.00001f ? 1.0f / scaleZ : 0.0f);
|
||||
|
||||
DrawCall drawCall;
|
||||
if (TerrainManager::GetChunkGeometry(drawCall, chunkSize, 0))
|
||||
return;
|
||||
|
||||
context->UpdateCB(cb, &shaderData);
|
||||
context->BindCB(0, cb);
|
||||
context->BindSR(0, heightmap);
|
||||
context->SetState(_psRenderCacheTerrain);
|
||||
context->BindIB(drawCall.Geometry.IndexBuffer);
|
||||
context->BindVB(ToSpan(drawCall.Geometry.VertexBuffers, 1));
|
||||
context->DrawIndexed(drawCall.Geometry.IndicesCount, 0, drawCall.Geometry.StartIndex);
|
||||
|
||||
break;
|
||||
}
|
||||
case GeometryType::Foliage:
|
||||
{
|
||||
auto foliage = entry.AsFoliage.Actor;
|
||||
auto& instance = foliage->Instances[entry.AsFoliage.InstanceIndex];
|
||||
auto& type = foliage->FoliageTypes[entry.AsFoliage.TypeIndex];
|
||||
|
||||
Matrix::Transpose(instance.World, shaderData.WorldMatrix);
|
||||
shaderData.LightmapArea = instance.Lightmap.UVsArea;
|
||||
|
||||
context->UpdateCB(cb, &shaderData);
|
||||
context->BindCB(0, cb);
|
||||
context->SetState(_psRenderCacheModel);
|
||||
type.Model->LODs[0].Meshes[entry.AsFoliage.MeshIndex].Render(context);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
// TODO: on directx 12 use conservative rasterization
|
||||
// TODO: we could also MSAA -> even better results
|
||||
}
|
||||
|
||||
// Check if stage has been done
|
||||
if (_workerStagePosition1 >= lightmapEntry.Entries.Count())
|
||||
_wasStageDone = true;
|
||||
|
||||
scene->EntriesLocker.Unlock();
|
||||
break;
|
||||
}
|
||||
case PostprocessCache:
|
||||
{
|
||||
PROFILE_GPU_CPU_NAMED("PostprocessCache");
|
||||
|
||||
// In ideal case we should use analytical anti-aliasing and conservative rasterization
|
||||
// But for now let's use simple trick to blur positions and normals cache to reduce amount of black artifacts on uv edges
|
||||
|
||||
auto tempDesc = GPUTextureDescription::New2D(atlasSize, atlasSize, HemispheresFormatToPixelFormat[CACHE_POSITIONS_FORMAT]);
|
||||
auto resultPositions = RenderTargetPool::Get(tempDesc);
|
||||
tempDesc.Format = HemispheresFormatToPixelFormat[CACHE_NORMALS_FORMAT];
|
||||
auto resultNormals = RenderTargetPool::Get(tempDesc);
|
||||
if (resultPositions == nullptr || resultNormals == nullptr)
|
||||
{
|
||||
RenderTargetPool::Release(resultPositions);
|
||||
RenderTargetPool::Release(resultNormals);
|
||||
LOG(Error, "Cannot get temporary targets for ShadowsOfMordor::Builder::PostprocessCache");
|
||||
_wasStageDone = true;
|
||||
break;
|
||||
}
|
||||
|
||||
auto srcPositions = _cachePositions;
|
||||
auto srcNormals = _cacheNormals;
|
||||
|
||||
GPUTextureView* rts[2] =
|
||||
{
|
||||
resultPositions->View(),
|
||||
resultNormals->View(),
|
||||
};
|
||||
context->SetRenderTarget(nullptr, ToSpan(rts, ARRAY_COUNT(rts)));
|
||||
float atlasSizeFloat = (float)atlasSize;
|
||||
context->SetViewportAndScissors(atlasSizeFloat, atlasSizeFloat);
|
||||
|
||||
context->BindSR(0, srcNormals);
|
||||
context->BindSR(1, srcPositions);
|
||||
|
||||
ShaderData shaderData;
|
||||
shaderData.AtlasSize = atlasSize;
|
||||
auto cb = _shader->GetShader()->GetCB(0);
|
||||
context->UpdateCB(cb, &shaderData);
|
||||
context->BindCB(0, cb);
|
||||
|
||||
context->SetState(_psBlurCache);
|
||||
context->DrawFullscreenTriangle();
|
||||
|
||||
_cachePositions = resultPositions;
|
||||
_cacheNormals = resultNormals;
|
||||
|
||||
RenderTargetPool::Release(srcPositions);
|
||||
RenderTargetPool::Release(srcNormals);
|
||||
|
||||
_wasStageDone = true;
|
||||
break;
|
||||
}
|
||||
case ClearLightmapData:
|
||||
{
|
||||
PROFILE_GPU_CPU_NAMED("ClearLightmapData");
|
||||
|
||||
// Before hemispheres rendering we have to clear target lightmap data
|
||||
// Later we use blur shader to interpolate empty texels (so empty texels should be pure black)
|
||||
|
||||
ASSERT(scene->Lightmaps.Count() > _workerStagePosition0);
|
||||
auto& lightmapEntry = scene->Lightmaps[_workerStagePosition0];
|
||||
|
||||
// All black everything!
|
||||
context->ClearUA(lightmapEntry.LightmapData, Vector4::Zero);
|
||||
|
||||
_wasStageDone = true;
|
||||
break;
|
||||
}
|
||||
case RenderHemispheres:
|
||||
{
|
||||
auto now = DateTime::Now();
|
||||
auto& lightmapEntry = scene->Lightmaps[_workerStagePosition0];
|
||||
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
if (lightmapEntry.LightmapDataInit.HasItems())
|
||||
{
|
||||
context->UpdateBuffer(lightmapEntry.LightmapData, lightmapEntry.LightmapDataInit.Get(), lightmapEntry.LightmapDataInit.Count());
|
||||
lightmapEntry.LightmapDataInit.Resize(0);
|
||||
}
|
||||
|
||||
// Every few minutes save the baking state to restore it in case of GPU driver crash
|
||||
if (now - _lastStateSaveTime >= TimeSpan::FromSeconds(HEMISPHERES_BAKE_STATE_SAVE_DELAY))
|
||||
{
|
||||
saveState();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
PROFILE_GPU_CPU_NAMED("RenderHemispheres");
|
||||
|
||||
// Dynamically adjust hemispheres to render per-job to minimize the bake speed but without GPU hangs
|
||||
if (now - _hemispheresPerJobUpdateTime >= TimeSpan::FromSeconds(1.0))
|
||||
{
|
||||
_hemispheresPerJobUpdateTime = now;
|
||||
const int32 fps = Engine::GetFramesPerSecond();
|
||||
int32 hemispheresPerJob = _hemispheresPerJob;
|
||||
if (fps > HEMISPHERES_RENDERING_TARGET_FPS * 5)
|
||||
hemispheresPerJob *= 4;
|
||||
else if (fps > HEMISPHERES_RENDERING_TARGET_FPS * 3)
|
||||
hemispheresPerJob *= 2;
|
||||
else if (fps > (int32)(HEMISPHERES_RENDERING_TARGET_FPS * 1.5f))
|
||||
hemispheresPerJob = Math::RoundToInt((float)hemispheresPerJob * 1.1f);
|
||||
else if (fps < (int32)(HEMISPHERES_RENDERING_TARGET_FPS * 0.8f))
|
||||
hemispheresPerJob = Math::RoundToInt((float)hemispheresPerJob * 0.9f);
|
||||
hemispheresPerJob = Math::Clamp(hemispheresPerJob, HEMISPHERES_PER_JOB_MIN, HEMISPHERES_PER_JOB_MAX);
|
||||
if (hemispheresPerJob != _hemispheresPerJob)
|
||||
{
|
||||
LOG(Info, "Changing GI baking hemispheres count per job from {0} to {1}", _hemispheresPerJob, hemispheresPerJob);
|
||||
_hemispheresPerJob = hemispheresPerJob;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare
|
||||
int32 hemispheresToRenderLeft = _hemispheresPerJob;
|
||||
int32 hemispheresToRenderBeforeSyncLeft = hemispheresToRenderLeft > 10 ? HEMISPHERES_PER_GPU_FLUSH : HEMISPHERES_PER_JOB_MAX;
|
||||
Matrix view, projection;
|
||||
Matrix::PerspectiveFov(HEMISPHERES_FOV * DegreesToRadians, 1.0f, HEMISPHERES_NEAR_PLANE, HEMISPHERES_FAR_PLANE, projection);
|
||||
ShaderData shaderData;
|
||||
#if COMPILE_WITH_PROFILER
|
||||
auto gpuProfilerEnabled = ProfilerGPU::Enabled;
|
||||
ProfilerGPU::Enabled = false;
|
||||
#endif
|
||||
|
||||
// Render hemispheres
|
||||
for (; _workerStagePosition1 < lightmapEntry.Hemispheres.Count(); _workerStagePosition1++)
|
||||
{
|
||||
if (hemispheresToRenderLeft == 0)
|
||||
break;
|
||||
hemispheresToRenderLeft--;
|
||||
auto& hemisphere = lightmapEntry.Hemispheres[_workerStagePosition1];
|
||||
|
||||
// Create tangent frame
|
||||
Vector3 tangent;
|
||||
Vector3 c1 = Vector3::Cross(hemisphere.Normal, Vector3(0.0, 0.0, 1.0));
|
||||
Vector3 c2 = Vector3::Cross(hemisphere.Normal, Vector3(0.0, 1.0, 0.0));
|
||||
tangent = c1.Length() > c2.Length() ? c1 : c2;
|
||||
tangent = Vector3::Normalize(tangent);
|
||||
const Vector3 binormal = Vector3::Cross(tangent, hemisphere.Normal);
|
||||
|
||||
// Setup view
|
||||
const Vector3 pos = hemisphere.Position + hemisphere.Normal * 0.001f;
|
||||
Matrix::LookAt(pos, pos + hemisphere.Normal, tangent, view);
|
||||
_task->View.SetUp(view, projection);
|
||||
_task->View.Position = pos;
|
||||
_task->View.Direction = hemisphere.Normal;
|
||||
|
||||
// Render hemisphere
|
||||
// TODO: maybe render geometry backfaces in postLightPass to set the pure black? - to remove light leaking
|
||||
IsRunningRadiancePass = true;
|
||||
EnableLightmapsUsage = _giBounceRunningIndex != 0;
|
||||
//
|
||||
Renderer::Render(_task);
|
||||
context->ClearState();
|
||||
//
|
||||
IsRunningRadiancePass = false;
|
||||
EnableLightmapsUsage = true;
|
||||
auto radianceMap = _output->View();
|
||||
|
||||
#if DEBUG_EXPORT_HEMISPHERES_PREVIEW
|
||||
addDebugHemisphere(context, radianceMap);
|
||||
#endif
|
||||
|
||||
// Setup shader data
|
||||
Matrix worldToTangent;
|
||||
worldToTangent.SetRow1(Vector4(tangent, 0.0f));
|
||||
worldToTangent.SetRow2(Vector4(binormal, 0.0f));
|
||||
worldToTangent.SetRow3(Vector4(hemisphere.Normal, 0.0f));
|
||||
worldToTangent.SetRow4(Vector4(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
worldToTangent.Invert();
|
||||
//
|
||||
Matrix viewToWorld; // viewToWorld is inverted view, since view is worldToView
|
||||
Matrix::Invert(view, viewToWorld);
|
||||
viewToWorld.SetRow4(Vector4(0.0f, 0.0f, 0.0f, 1.0f)); // reset translation row
|
||||
Matrix viewToTangent;
|
||||
Matrix::Multiply(viewToWorld, worldToTangent, viewToTangent);
|
||||
Matrix::Transpose(viewToTangent, shaderData.ToTangentSpace);
|
||||
shaderData.FinalWeight = _hemisphereTexelsTotalWeight;
|
||||
shaderData.AtlasSize = atlasSize;
|
||||
shaderData.TexelAddress = (hemisphere.TexelY * atlasSize + hemisphere.TexelX) * NUM_SH_TARGETS;
|
||||
|
||||
// Calculate per pixel irradiance using compute shaders
|
||||
auto cb = _shader->GetShader()->GetCB(0);
|
||||
context->UpdateCB(cb, &shaderData);
|
||||
context->BindCB(0, cb);
|
||||
context->BindUA(0, _irradianceReduction->View());
|
||||
context->BindSR(0, radianceMap);
|
||||
context->Dispatch(_shader->GetShader()->GetCS("CS_Integrate"), 1, HEMISPHERES_RESOLUTION, 1);
|
||||
|
||||
// Downscale H-basis to 1x1 and copy results to lightmap data buffer
|
||||
context->BindUA(0, lightmapEntry.LightmapData->View());
|
||||
context->FlushState();
|
||||
context->BindSR(0, _irradianceReduction->View());
|
||||
// TODO: cache shader handle
|
||||
context->Dispatch(_shader->GetShader()->GetCS("CS_Reduction"), 1, NUM_SH_TARGETS, 1);
|
||||
|
||||
// Unbind slots now to make rendering backend live easier
|
||||
context->UnBindSR(0);
|
||||
context->UnBindUA(0);
|
||||
context->FlushState();
|
||||
|
||||
// Keep GPU busy
|
||||
if (hemispheresToRenderBeforeSyncLeft-- < 0)
|
||||
{
|
||||
hemispheresToRenderBeforeSyncLeft = HEMISPHERES_PER_GPU_FLUSH;
|
||||
context->Flush();
|
||||
}
|
||||
}
|
||||
#if COMPILE_WITH_PROFILER
|
||||
ProfilerGPU::Enabled = gpuProfilerEnabled;
|
||||
#endif
|
||||
|
||||
// Report progress
|
||||
float hemispheresProgress = static_cast<float>(_workerStagePosition1) / lightmapEntry.Hemispheres.Count();
|
||||
float lightmapsProgress = static_cast<float>(_workerStagePosition0 + hemispheresProgress) / scene->Lightmaps.Count();
|
||||
float bouncesProgress = static_cast<float>(_giBounceRunningIndex) / _bounceCount;
|
||||
reportProgress(BuildProgressStep::RenderHemispheres, lightmapsProgress / _bounceCount + bouncesProgress);
|
||||
|
||||
// Check if work has been finished
|
||||
if (hemispheresProgress >= 1.0f)
|
||||
{
|
||||
// Move to another lightmap
|
||||
_workerStagePosition0++;
|
||||
_workerStagePosition1 = 0;
|
||||
|
||||
// Check if it's stage end
|
||||
if (_workerStagePosition0 == scene->Lightmaps.Count())
|
||||
{
|
||||
_wasStageDone = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PostprocessLightmaps:
|
||||
{
|
||||
PROFILE_GPU_CPU_NAMED("PostprocessLightmaps");
|
||||
|
||||
// Let's blur generated lightmaps to reduce amount of black artifacts and holes
|
||||
|
||||
// Prepare
|
||||
auto& lightmapEntry = scene->Lightmaps[_workerStagePosition0];
|
||||
ShaderData shaderData;
|
||||
shaderData.AtlasSize = atlasSize;
|
||||
auto cb = _shader->GetShader()->GetCB(0);
|
||||
context->UpdateCB(cb, &shaderData);
|
||||
context->BindCB(0, cb);
|
||||
|
||||
// Blur empty lightmap texel to reduce black artifacts during sampling lightmap on objects
|
||||
context->ResetRenderTarget();
|
||||
context->BindSR(0, lightmapEntry.LightmapData->View());
|
||||
context->BindUA(0, scene->TempLightmapData->View());
|
||||
context->Dispatch(_shader->GetShader()->GetCS("CS_BlurEmpty"), atlasSize, atlasSize, 1);
|
||||
|
||||
// Swap temporary buffer used as output with lightmap entry data (these buffers are the same)
|
||||
// So we can rewrite data from one buffer to another with custom sampling
|
||||
Swap(scene->TempLightmapData, lightmapEntry.LightmapData);
|
||||
|
||||
// Keep blurring the empty lightmap texels (from background)
|
||||
const int32 blurPasses = 24;
|
||||
for (int32 blurPassIndex = 0; blurPassIndex < blurPasses; blurPassIndex++)
|
||||
{
|
||||
context->UnBindSR(0);
|
||||
context->UnBindUA(0);
|
||||
context->FlushState();
|
||||
|
||||
context->BindSR(0, lightmapEntry.LightmapData->View());
|
||||
context->BindUA(0, scene->TempLightmapData->View());
|
||||
context->Dispatch(_shader->GetShader()->GetCS("CS_Dilate"), atlasSize, atlasSize, 1);
|
||||
|
||||
Swap(scene->TempLightmapData, lightmapEntry.LightmapData);
|
||||
}
|
||||
context->UnBindSR(0);
|
||||
context->BindUA(0, lightmapEntry.LightmapData->View());
|
||||
|
||||
// Remove the BACKGROUND_TEXELS_MARK from the unused texels (see shader for more info)
|
||||
context->Dispatch(_shader->GetShader()->GetCS("CS_Finalize"), atlasSize, atlasSize, 1);
|
||||
|
||||
// Move to another lightmap
|
||||
_workerStagePosition0++;
|
||||
|
||||
// Check if it's stage end
|
||||
if (_workerStagePosition0 >= scene->Lightmaps.Count())
|
||||
{
|
||||
_wasStageDone = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup after rendering
|
||||
context->ClearState();
|
||||
|
||||
// Mark job as done
|
||||
Platform::AtomicStore(&_wasJobDone, 1);
|
||||
_lastJobFrame = Engine::FrameCount;
|
||||
|
||||
// Check if stage has been done
|
||||
if (_wasStageDone)
|
||||
{
|
||||
// Disable task
|
||||
_task->Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ShadowsOfMordor::Builder::checkBuildCancelled()
|
||||
{
|
||||
const bool wasCancelled = Platform::AtomicRead(&_wasBuildCancelled) != 0;
|
||||
if (wasCancelled)
|
||||
{
|
||||
LOG(Warning, "Lightmap building was cancelled");
|
||||
}
|
||||
return wasCancelled;
|
||||
}
|
||||
|
||||
bool ShadowsOfMordor::Builder::runStage(BuildingStage stage, bool resetPosition)
|
||||
{
|
||||
bool wasCancelled;
|
||||
_wasStageDone = false;
|
||||
if (resetPosition)
|
||||
_workerStagePosition1 = 0;
|
||||
_stage = stage;
|
||||
_lastJobFrame = 0;
|
||||
|
||||
// Start the job
|
||||
RenderTask::TasksLocker.Lock();
|
||||
_task->Enabled = true;
|
||||
RenderTask::TasksLocker.Unlock();
|
||||
|
||||
// Split work into more jobs to reduce overhead
|
||||
while (true)
|
||||
{
|
||||
// Wait for the end or cancellation event
|
||||
while (true)
|
||||
{
|
||||
Platform::Sleep(1);
|
||||
|
||||
wasCancelled = checkBuildCancelled();
|
||||
const bool wasJobDone = Platform::AtomicRead(&_wasJobDone) != 0;
|
||||
if (wasJobDone)
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for stage end
|
||||
if (_wasStageDone || wasCancelled)
|
||||
break;
|
||||
}
|
||||
|
||||
// Ensure to disable task
|
||||
RenderTask::TasksLocker.Lock();
|
||||
_task->Enabled = false;
|
||||
RenderTask::TasksLocker.Unlock();
|
||||
|
||||
return wasCancelled;
|
||||
}
|
||||
538
Source/Engine/ShadowsOfMordor/Builder.cpp
Normal file
538
Source/Engine/ShadowsOfMordor/Builder.cpp
Normal file
@@ -0,0 +1,538 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Builder.h"
|
||||
#include "Engine/Engine/Engine.h"
|
||||
#include "Engine/Level/Scene/Scene.h"
|
||||
#include "Engine/Level/Level.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
#include "Engine/Engine/EngineService.h"
|
||||
#include "Engine/Threading/ThreadSpawner.h"
|
||||
#include "Engine/Graphics/GPUDevice.h"
|
||||
#include "Engine/Graphics/GPUPipelineState.h"
|
||||
#include "Engine/Graphics/RenderTargetPool.h"
|
||||
|
||||
namespace ShadowsOfMordor
|
||||
{
|
||||
bool IsRunningRadiancePass = false;
|
||||
bool EnableLightmapsUsage = true;
|
||||
|
||||
float BuildProgressStepProgress[9] =
|
||||
{
|
||||
0.01f,
|
||||
// Initialize
|
||||
0.017f,
|
||||
// CacheEntries,
|
||||
0.002f,
|
||||
// GenerateLightmapCharts,
|
||||
0.002f,
|
||||
// PackLightmapCharts,
|
||||
0.028f,
|
||||
// UpdateLightmapsCollection,
|
||||
0.004f,
|
||||
// UpdateEntries,
|
||||
0.018f,
|
||||
// GenerateHemispheresCache,
|
||||
0.90f,
|
||||
// RenderHemispheres,
|
||||
0.01f,
|
||||
// Cleanup,
|
||||
}; // Sum == 1
|
||||
|
||||
PixelFormat HemispheresFormatToPixelFormat[2] =
|
||||
{
|
||||
PixelFormat::R32G32B32A32_Float,
|
||||
PixelFormat::R16G16B16A16_Float,
|
||||
};
|
||||
|
||||
float GetProgressBeforeStep(BuildProgressStep step)
|
||||
{
|
||||
float sum = 0;
|
||||
for (int32 i = 0; i < static_cast<int32>(step); i++)
|
||||
sum += BuildProgressStepProgress[i];
|
||||
return sum;
|
||||
}
|
||||
|
||||
float GetProgressWithStep(BuildProgressStep step)
|
||||
{
|
||||
float sum = 0;
|
||||
for (int32 i = 0; i <= static_cast<int32>(step); i++)
|
||||
sum += BuildProgressStepProgress[i];
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
class ShadowsOfMordorBuilderService : public EngineService
|
||||
{
|
||||
public:
|
||||
|
||||
ShadowsOfMordorBuilderService()
|
||||
: EngineService(TEXT("ShadowsOfMordor Builder"), 80)
|
||||
{
|
||||
}
|
||||
|
||||
void Dispose() override;
|
||||
};
|
||||
|
||||
ShadowsOfMordorBuilderService ShadowsOfMordorBuilderServiceInstance;
|
||||
|
||||
ShadowsOfMordor::Builder::Builder()
|
||||
: _wasBuildCalled(false)
|
||||
, _isActive(false)
|
||||
, _wasBuildCancelled(false)
|
||||
{
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::Build()
|
||||
{
|
||||
// To bake static lighting we have to support compute shaders
|
||||
ASSERT_LOW_LAYER(GPUDevice::Instance && GPUDevice::Instance->Limits.HasCompute);
|
||||
|
||||
_locker.Lock();
|
||||
|
||||
if (!_wasBuildCalled)
|
||||
{
|
||||
_wasBuildCalled = true;
|
||||
_wasBuildCancelled = 0;
|
||||
|
||||
// Ensure any scene has been loaded
|
||||
ASSERT(Level::IsAnySceneLoaded());
|
||||
|
||||
// Register background work
|
||||
Function<int32()> f;
|
||||
f.Bind<Builder, &Builder::doWork>(this);
|
||||
ThreadSpawner::Start(f, TEXT("GI Baking"));
|
||||
}
|
||||
|
||||
_locker.Unlock();
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::CancelBuild()
|
||||
{
|
||||
_locker.Lock();
|
||||
|
||||
if (_wasBuildCalled)
|
||||
{
|
||||
Platform::AtomicStore(&_wasBuildCancelled, 1);
|
||||
}
|
||||
|
||||
_locker.Unlock();
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::Dispose()
|
||||
{
|
||||
_locker.Lock();
|
||||
const bool waitForEnd = _wasBuildCalled;
|
||||
CancelBuild();
|
||||
_locker.Unlock();
|
||||
|
||||
if (waitForEnd)
|
||||
{
|
||||
// Lightmaps builder must respond always withing 100ms after cancel work signal!
|
||||
Platform::Sleep(100);
|
||||
}
|
||||
|
||||
releaseResources();
|
||||
}
|
||||
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
|
||||
#include "Engine/Platform/FileSystem.h"
|
||||
#include "Engine/Platform/MessageBox.h"
|
||||
#include "Engine/Serialization/FileReadStream.h"
|
||||
#include "Engine/Serialization/FileWriteStream.h"
|
||||
#include "Engine/Engine/CommandLine.h"
|
||||
#include "Engine/Scripting/Scripting.h"
|
||||
#include "FlaxEngine.Gen.h"
|
||||
|
||||
namespace ShadowsOfMordor
|
||||
{
|
||||
const Char* StateCacheFileName = TEXT("ShadowsOfMordor_Cache.bin");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void ShadowsOfMordor::Builder::CheckIfRestoreState()
|
||||
{
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
// Check if there is a state to restore
|
||||
const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
|
||||
if (!FileSystem::FileExists(path))
|
||||
return;
|
||||
|
||||
// Ask user if restore state
|
||||
if (!CommandLine::Options.Headless.IsTrue() && MessageBox::Show(TEXT("The last Lightmaps Baking job had crashed. Do you want to restore the state and continue baking?"), TEXT("Restore lightmaps baking?"), MessageBoxButtons::YesNo, MessageBoxIcon::Question) != DialogResult::Yes)
|
||||
{
|
||||
deleteState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip compilation on startup so editor will just load binaries
|
||||
CommandLine::Options.SkipCompile = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ShadowsOfMordor::Builder::RestoreState()
|
||||
{
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
// Check if there is a state to restore
|
||||
const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
|
||||
if (!FileSystem::FileExists(path))
|
||||
return false;
|
||||
|
||||
// Open file
|
||||
LOG(Info, "Restoring the lightmaps baking state...");
|
||||
auto stream = FileReadStream::Open(path);
|
||||
int32 version;
|
||||
stream->ReadInt32(&version);
|
||||
if (version != 1)
|
||||
{
|
||||
LOG(Error, "Invalid version.");
|
||||
Delete(stream);
|
||||
deleteState();
|
||||
return false;
|
||||
}
|
||||
int32 scenesCount;
|
||||
stream->ReadInt32(&scenesCount);
|
||||
|
||||
// Open scenes used during baking
|
||||
for (int32 i = 0; i < scenesCount; i++)
|
||||
{
|
||||
Guid id;
|
||||
stream->Read(&id);
|
||||
Level::LoadScene(id);
|
||||
}
|
||||
|
||||
Delete(stream);
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::saveState()
|
||||
{
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
|
||||
const auto pathTmp = path + TEXT(".tmp");
|
||||
auto stream = FileWriteStream::Open(pathTmp);
|
||||
|
||||
LOG(Info, "Saving the lightmaps baking state (scene: {0}, lightmap: {1}, hemisphere: {2})", _workerActiveSceneIndex, _workerStagePosition0, _workerStagePosition1);
|
||||
|
||||
// Save all scenes on first state saving (actors have modified lightmap entries mapping to the textures and scene lightmaps list has been edited)
|
||||
if (_firstStateSave)
|
||||
{
|
||||
_firstStateSave = false;
|
||||
Level::SaveAllScenes();
|
||||
}
|
||||
|
||||
// Format version
|
||||
stream->WriteInt32(1);
|
||||
|
||||
// Scenes ids
|
||||
stream->WriteInt32(_scenes.Count());
|
||||
for (int32 i = 0; i < _scenes.Count(); i++)
|
||||
stream->Write(&_scenes[i]->Scene->GetID());
|
||||
|
||||
// State
|
||||
stream->WriteInt32(_giBounceRunningIndex);
|
||||
stream->WriteInt32(_bounceCount);
|
||||
stream->WriteInt32(_workerActiveSceneIndex);
|
||||
stream->WriteInt32(_workerStagePosition0);
|
||||
stream->WriteInt32(_workerStagePosition1);
|
||||
stream->WriteFloat(_hemisphereTexelsTotalWeight);
|
||||
|
||||
// Scenes data
|
||||
for (int32 sceneIndex = 0; sceneIndex < _scenes.Count(); sceneIndex++)
|
||||
{
|
||||
auto& scene = _scenes[sceneIndex];
|
||||
stream->WriteInt32(scene->LightmapsCount);
|
||||
stream->WriteInt32(scene->HemispheresCount);
|
||||
stream->WriteInt32(scene->MergedHemispheresCount);
|
||||
|
||||
if (scene->LightmapsCount == 0)
|
||||
continue;
|
||||
auto lightmapDataStaging = scene->Lightmaps[0].LightmapData->ToStagingReadback();
|
||||
for (int32 lightmapIndex = 0; lightmapIndex < scene->LightmapsCount; lightmapIndex++)
|
||||
{
|
||||
auto& lightmap = scene->Lightmaps[lightmapIndex];
|
||||
|
||||
// Hemispheres
|
||||
stream->WriteInt32(lightmap.Hemispheres.Count());
|
||||
stream->WriteBytes(lightmap.Hemispheres.Get(), lightmap.Hemispheres.Count() * sizeof(HemisphereData));
|
||||
|
||||
// Lightmap Data
|
||||
// TODO: instead of doing hackish flush/sleep just copy data to some temporary buffer one frame before saving the state
|
||||
ASSERT(GPUDevice::Instance->IsRendering());
|
||||
auto context = GPUDevice::Instance->GetMainContext();
|
||||
const int32 lightmapDataSize = lightmapDataStaging->GetSize();
|
||||
context->CopyBuffer(lightmapDataStaging, lightmap.LightmapData, lightmapDataSize);
|
||||
context->Flush();
|
||||
Platform::Sleep(10);
|
||||
void* mapped = lightmapDataStaging->Map(GPUResourceMapMode::Read);
|
||||
stream->WriteInt32(lightmapDataSize);
|
||||
stream->WriteBytes(mapped, lightmapDataSize);
|
||||
lightmapDataStaging->Unmap();
|
||||
}
|
||||
SAFE_DELETE_GPU_RESOURCE(lightmapDataStaging);
|
||||
}
|
||||
|
||||
Delete(stream);
|
||||
|
||||
// Update the cache file
|
||||
if (FileSystem::FileExists(path))
|
||||
FileSystem::DeleteFile(path);
|
||||
FileSystem::MoveFile(path, pathTmp);
|
||||
FileSystem::DeleteFile(pathTmp);
|
||||
|
||||
_lastStateSaveTime = DateTime::Now();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ShadowsOfMordor::Builder::loadState()
|
||||
{
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
|
||||
if (!FileSystem::FileExists(path))
|
||||
return false;
|
||||
|
||||
// Open file
|
||||
LOG(Info, "Loading the lightmaps baking state...");
|
||||
auto stream = FileReadStream::Open(path);
|
||||
int32 version;
|
||||
stream->ReadInt32(&version);
|
||||
if (version != 1)
|
||||
{
|
||||
LOG(Error, "Invalid version.");
|
||||
Delete(stream);
|
||||
deleteState();
|
||||
return false;
|
||||
}
|
||||
int32 scenesCount;
|
||||
stream->ReadInt32(&scenesCount);
|
||||
|
||||
// Verify if scenes used during baking are loaded
|
||||
if (Level::Scenes.Count() != scenesCount || scenesCount != _scenes.Count())
|
||||
{
|
||||
LOG(Error, "Invalid scenes.");
|
||||
Delete(stream);
|
||||
deleteState();
|
||||
return false;
|
||||
}
|
||||
for (int32 i = 0; i < scenesCount; i++)
|
||||
{
|
||||
Guid id;
|
||||
stream->Read(&id);
|
||||
if (Level::Scenes[i]->GetID() != id || _scenes[i]->SceneIndex != i)
|
||||
{
|
||||
LOG(Error, "Invalid scenes.");
|
||||
Delete(stream);
|
||||
deleteState();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// State
|
||||
stream->ReadInt32(&_giBounceRunningIndex);
|
||||
stream->ReadInt32(&_bounceCount);
|
||||
stream->ReadInt32(&_workerActiveSceneIndex);
|
||||
stream->ReadInt32(&_workerStagePosition0);
|
||||
stream->ReadInt32(&_workerStagePosition1);
|
||||
stream->ReadFloat(&_hemisphereTexelsTotalWeight);
|
||||
|
||||
// Scenes data
|
||||
for (int32 sceneIndex = 0; sceneIndex < _scenes.Count(); sceneIndex++)
|
||||
{
|
||||
auto& scene = _scenes[sceneIndex];
|
||||
stream->ReadInt32(&scene->LightmapsCount);
|
||||
stream->ReadInt32(&scene->HemispheresCount);
|
||||
stream->ReadInt32(&scene->MergedHemispheresCount);
|
||||
|
||||
scene->Lightmaps.Resize(scene->LightmapsCount);
|
||||
if (scene->LightmapsCount == 0)
|
||||
continue;
|
||||
for (int32 lightmapIndex = 0; lightmapIndex < scene->LightmapsCount; lightmapIndex++)
|
||||
{
|
||||
auto& lightmap = scene->Lightmaps[lightmapIndex];
|
||||
lightmap.Init(&scene->GetSettings());
|
||||
|
||||
// Hemispheres
|
||||
int32 hemispheresCount;
|
||||
stream->ReadInt32(&hemispheresCount);
|
||||
lightmap.Hemispheres.Resize(hemispheresCount);
|
||||
stream->ReadBytes(lightmap.Hemispheres.Get(), lightmap.Hemispheres.Count() * sizeof(HemisphereData));
|
||||
|
||||
// Lightmap Data
|
||||
int32 lightmapDataSize;
|
||||
stream->ReadInt32(&lightmapDataSize);
|
||||
const auto lightmapData = lightmap.LightmapData;
|
||||
if (lightmapDataSize != lightmapData->GetSize())
|
||||
{
|
||||
LOG(Error, "Invalid lightmap data size.");
|
||||
Delete(stream);
|
||||
deleteState();
|
||||
return false;
|
||||
}
|
||||
lightmap.LightmapDataInit.Resize(lightmapDataSize);
|
||||
stream->ReadBytes(lightmap.LightmapDataInit.Get(), lightmapDataSize);
|
||||
}
|
||||
}
|
||||
|
||||
Delete(stream);
|
||||
|
||||
_firstStateSave = false;
|
||||
_lastStateSaveTime = DateTime::Now();
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::deleteState()
|
||||
{
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
|
||||
if (FileSystem::FileExists(path))
|
||||
FileSystem::DeleteFile(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::reportProgress(BuildProgressStep step, float stepProgress)
|
||||
{
|
||||
const auto currentStepTotalProgress = BuildProgressStepProgress[(int32)step];
|
||||
|
||||
// Apply scenes progress
|
||||
//stepProgress = (_workerActiveSceneIndex + stepProgress) / _scenes.Count();
|
||||
|
||||
// Get progress in 'before' steps
|
||||
reportProgress(step, stepProgress, GetProgressBeforeStep(step) + stepProgress * currentStepTotalProgress);
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::reportProgress(BuildProgressStep step, float stepProgress, float totalProgress)
|
||||
{
|
||||
// Send step changes info
|
||||
if (_lastStep != step)
|
||||
{
|
||||
const auto now = DateTime::Now();
|
||||
LOG(Info, "Lightmaps baking step {0} time: {1}s", ShadowsOfMordor::ToString(_lastStep), Math::RoundToInt((float)(now - _lastStepStart).GetTotalSeconds()));
|
||||
|
||||
_lastStep = step;
|
||||
_lastStepStart = now;
|
||||
}
|
||||
|
||||
// Send event
|
||||
OnBuildProgress(step, stepProgress, totalProgress);
|
||||
}
|
||||
|
||||
bool ShadowsOfMordor::Builder::initResources()
|
||||
{
|
||||
// TODO: remove this release and just create missing resources
|
||||
releaseResources();
|
||||
|
||||
_output = GPUTexture::New();
|
||||
if (_output->Init(GPUTextureDescription::New2D(HEMISPHERES_RESOLUTION, HEMISPHERES_RESOLUTION, PixelFormat::R11G11B10_Float)))
|
||||
return true;
|
||||
_task = New<BuilderRenderTask>();
|
||||
_task->Enabled = false;
|
||||
_task->Output = _output;
|
||||
auto& view = _task->View;
|
||||
view.Mode = ViewMode::NoPostFx;
|
||||
view.Flags =
|
||||
ViewFlags::GI |
|
||||
ViewFlags::DirectionalLights |
|
||||
ViewFlags::PointLights |
|
||||
ViewFlags::SpotLights |
|
||||
ViewFlags::Shadows |
|
||||
ViewFlags::Decals |
|
||||
ViewFlags::SkyLights |
|
||||
ViewFlags::Reflections;
|
||||
view.IsOfflinePass = true;
|
||||
view.Near = HEMISPHERES_NEAR_PLANE;
|
||||
view.Far = HEMISPHERES_FAR_PLANE;
|
||||
view.StaticFlagsMask = StaticFlags::Lightmap;
|
||||
view.MaxShadowsQuality = Quality::Low;
|
||||
_task->Resize(HEMISPHERES_RESOLUTION, HEMISPHERES_RESOLUTION);
|
||||
|
||||
// Load shader
|
||||
_shader = Content::LoadAsyncInternal<Shader>(TEXT("Shaders/BakeLightmap"));
|
||||
if (_shader == nullptr)
|
||||
return true;
|
||||
if (_shader->WaitForLoaded())
|
||||
return true;
|
||||
|
||||
_psRenderCacheModel = GPUDevice::Instance->CreatePipelineState();
|
||||
GPUPipelineState::Description desc = GPUPipelineState::Description::DefaultNoDepth;
|
||||
desc.CullMode = CullMode::TwoSided;
|
||||
desc.VS = _shader->GetShader()->GetVS("VS_RenderCacheModel");
|
||||
desc.PS = _shader->GetShader()->GetPS("PS_RenderCache");
|
||||
if (_psRenderCacheModel->Init(desc))
|
||||
return true;
|
||||
|
||||
_psRenderCacheTerrain = GPUDevice::Instance->CreatePipelineState();
|
||||
desc.VS = _shader->GetShader()->GetVS("VS_RenderCacheTerrain");
|
||||
if (_psRenderCacheTerrain->Init(desc))
|
||||
return true;
|
||||
|
||||
_psBlurCache = GPUDevice::Instance->CreatePipelineState();
|
||||
desc = GPUPipelineState::Description::DefaultFullscreenTriangle;
|
||||
desc.PS = _shader->GetShader()->GetPS("PS_BlurCache");
|
||||
if (_psBlurCache->Init(desc))
|
||||
return true;
|
||||
|
||||
_irradianceReduction = GPUDevice::Instance->CreateBuffer(TEXT("IrradianceReduction"));
|
||||
if (_irradianceReduction->Init(GPUBufferDescription::Typed(HEMISPHERES_RESOLUTION * NUM_SH_TARGETS, HemispheresFormatToPixelFormat[HEMISPHERES_IRRADIANCE_FORMAT], true)))
|
||||
return true;
|
||||
|
||||
#if DEBUG_EXPORT_HEMISPHERES_PREVIEW
|
||||
releaseDebugHemisphereAtlases();
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ShadowsOfMordor::Builder::releaseResources()
|
||||
{
|
||||
#if DEBUG_EXPORT_HEMISPHERES_PREVIEW
|
||||
releaseDebugHemisphereAtlases();
|
||||
#endif
|
||||
|
||||
SAFE_DELETE_GPU_RESOURCE(_psRenderCacheModel);
|
||||
SAFE_DELETE_GPU_RESOURCE(_psRenderCacheTerrain);
|
||||
SAFE_DELETE_GPU_RESOURCE(_psBlurCache);
|
||||
_shader.Unlink();
|
||||
|
||||
SAFE_DELETE_GPU_RESOURCE(_irradianceReduction);
|
||||
|
||||
RenderTargetPool::Release(_cachePositions);
|
||||
_cachePositions = nullptr;
|
||||
RenderTargetPool::Release(_cacheNormals);
|
||||
_cacheNormals = nullptr;
|
||||
|
||||
if (_output)
|
||||
_output->ReleaseGPU();
|
||||
|
||||
SAFE_DELETE(_task);
|
||||
SAFE_DELETE_GPU_RESOURCE(_output);
|
||||
}
|
||||
|
||||
bool ShadowsOfMordor::Builder::waitForJobDataSync()
|
||||
{
|
||||
bool wasCancelled = false;
|
||||
const int32 framesToSyncCount = 3;
|
||||
|
||||
while (!wasCancelled)
|
||||
{
|
||||
Platform::Sleep(1);
|
||||
|
||||
wasCancelled = checkBuildCancelled();
|
||||
if (_lastJobFrame + framesToSyncCount <= Engine::FrameCount)
|
||||
break;
|
||||
}
|
||||
|
||||
return wasCancelled;
|
||||
}
|
||||
|
||||
void ShadowsOfMordorBuilderService::Dispose()
|
||||
{
|
||||
ShadowsOfMordor::Builder::Instance()->Dispose();
|
||||
}
|
||||
339
Source/Engine/ShadowsOfMordor/Builder.h
Normal file
339
Source/Engine/ShadowsOfMordor/Builder.h
Normal file
@@ -0,0 +1,339 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Content/Assets/Model.h"
|
||||
#include "Engine/Content/Assets/Shader.h"
|
||||
#include "Engine/CSG/CSGMesh.h"
|
||||
#include "Builder.Config.h"
|
||||
|
||||
#if COMPILE_WITH_GI_BAKING
|
||||
|
||||
// Forward declarations
|
||||
#if COMPILE_WITH_ASSETS_IMPORTER
|
||||
namespace DirectX
|
||||
{
|
||||
class ScratchImage;
|
||||
};
|
||||
#endif
|
||||
class Actor;
|
||||
class Terrain;
|
||||
class Foliage;
|
||||
class StaticModel;
|
||||
|
||||
namespace ShadowsOfMordor
|
||||
{
|
||||
/// <summary>
|
||||
/// Shadows Of Mordor lightmaps builder utility.
|
||||
/// </summary>
|
||||
class Builder : public Singleton<Builder>
|
||||
{
|
||||
public:
|
||||
|
||||
enum class GeometryType
|
||||
{
|
||||
StaticModel,
|
||||
Terrain,
|
||||
Foliage,
|
||||
};
|
||||
|
||||
struct LightmapUVsChart
|
||||
{
|
||||
int32 Width;
|
||||
int32 Height;
|
||||
|
||||
LightmapEntry Result;
|
||||
|
||||
int32 EntryIndex;
|
||||
};
|
||||
|
||||
struct GeometryEntry
|
||||
{
|
||||
GeometryType Type;
|
||||
float Scale;
|
||||
BoundingBox Box;
|
||||
Rectangle UVsBox;
|
||||
|
||||
union
|
||||
{
|
||||
struct
|
||||
{
|
||||
StaticModel* Actor;
|
||||
} AsStaticModel;
|
||||
|
||||
struct
|
||||
{
|
||||
Terrain* Actor;
|
||||
int32 PatchIndex;
|
||||
int32 ChunkIndex;
|
||||
} AsTerrain;
|
||||
|
||||
struct
|
||||
{
|
||||
Foliage* Actor;
|
||||
int32 InstanceIndex;
|
||||
int32 TypeIndex;
|
||||
int32 MeshIndex;
|
||||
} AsFoliage;
|
||||
};
|
||||
|
||||
int32 ChartIndex;
|
||||
};
|
||||
|
||||
typedef Array<GeometryEntry> GeometryEntriesCollection;
|
||||
typedef Array<LightmapUVsChart> LightmapUVsChartsCollection;
|
||||
|
||||
enum BuildingStage
|
||||
{
|
||||
CleanLightmaps = 0,
|
||||
RenderCache,
|
||||
PostprocessCache,
|
||||
ClearLightmapData,
|
||||
RenderHemispheres,
|
||||
PostprocessLightmaps,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Single hemisphere data
|
||||
/// </summary>
|
||||
struct HemisphereData
|
||||
{
|
||||
Vector3 Position;
|
||||
Vector3 Normal;
|
||||
|
||||
int16 TexelX;
|
||||
int16 TexelY;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Per lightmap cache data
|
||||
/// </summary>
|
||||
class LightmapBuildCache
|
||||
{
|
||||
public:
|
||||
|
||||
Array<int32> Entries;
|
||||
Array<HemisphereData> Hemispheres;
|
||||
GPUBuffer* LightmapData = nullptr;
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
// Restored data for the lightmap from the loaded state (copied to the LightmapData on first hemispheres render job)
|
||||
Array<byte> LightmapDataInit;
|
||||
#endif
|
||||
|
||||
~LightmapBuildCache();
|
||||
|
||||
bool Init(const LightmapSettings* settings);
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Per scene cache data
|
||||
/// </summary>
|
||||
class SceneBuildCache
|
||||
{
|
||||
public:
|
||||
|
||||
// Meta
|
||||
Builder* Builder;
|
||||
int32 SceneIndex;
|
||||
|
||||
// Data
|
||||
Scene* Scene;
|
||||
CriticalSection EntriesLocker;
|
||||
GeometryEntriesCollection Entries;
|
||||
LightmapUVsChartsCollection Charts;
|
||||
Array<LightmapBuildCache> Lightmaps;
|
||||
GPUBuffer* TempLightmapData;
|
||||
|
||||
// Stats
|
||||
int32 LightmapsCount;
|
||||
int32 HemispheresCount;
|
||||
int32 MergedHemispheresCount;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneBuildCache"/> class.
|
||||
/// </summary>
|
||||
SceneBuildCache();
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lightmaps baking settings.
|
||||
/// </summary>
|
||||
/// <returns>Settings</returns>
|
||||
const LightmapSettings& GetSettings() const;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Waits for lightmap textures being fully loaded before baking process.
|
||||
/// </summary>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
bool WaitForLightmaps();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the lightmaps textures data.
|
||||
/// </summary>
|
||||
void UpdateLightmaps();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this instance.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
/// <param name="index">The scene index.</param>
|
||||
/// <param name="scene">The scene.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
bool Init(ShadowsOfMordor::Builder* builder, int32 index, ::Scene* scene);
|
||||
|
||||
/// <summary>
|
||||
/// Releases this scene data cache.
|
||||
/// </summary>
|
||||
void Release();
|
||||
|
||||
public:
|
||||
|
||||
// Importing lightmaps data
|
||||
BytesContainer ImportLightmapTextureData;
|
||||
int32 ImportLightmapIndex;
|
||||
int32 ImportLightmapTextureIndex;
|
||||
#if COMPILE_WITH_ASSETS_IMPORTER
|
||||
bool onImportLightmap(TextureData& image);
|
||||
#endif
|
||||
};
|
||||
|
||||
class BuilderRenderTask : public SceneRenderTask
|
||||
{
|
||||
void OnRender(GPUContext* context) override
|
||||
{
|
||||
Builder::Instance()->onJobRender(context);
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
friend class ShadowsOfMordorBuilderService;
|
||||
CriticalSection _locker;
|
||||
bool _wasBuildCalled;
|
||||
bool _isActive;
|
||||
volatile int64 _wasBuildCancelled;
|
||||
|
||||
Array<SceneBuildCache*> _scenes;
|
||||
|
||||
volatile int64 _wasJobDone;
|
||||
BuildingStage _stage;
|
||||
bool _wasStageDone;
|
||||
int32 _workerActiveSceneIndex;
|
||||
int32 _workerStagePosition0;
|
||||
int32 _workerStagePosition1;
|
||||
int32 _giBounceRunningIndex;
|
||||
uint64 _lastJobFrame;
|
||||
float _hemisphereTexelsTotalWeight;
|
||||
int32 _bounceCount;
|
||||
int32 _hemispheresPerJob;
|
||||
DateTime _hemispheresPerJobUpdateTime;
|
||||
#if HEMISPHERES_BAKE_STATE_SAVE
|
||||
DateTime _lastStateSaveTime;
|
||||
bool _firstStateSave;
|
||||
#endif
|
||||
BuildProgressStep _lastStep;
|
||||
DateTime _lastStepStart;
|
||||
|
||||
BuilderRenderTask* _task = nullptr;
|
||||
GPUTexture* _output = nullptr;
|
||||
AssetReference<Shader> _shader;
|
||||
GPUPipelineState* _psRenderCacheModel = nullptr;
|
||||
GPUPipelineState* _psRenderCacheTerrain = nullptr;
|
||||
GPUPipelineState* _psBlurCache = nullptr;
|
||||
GPUBuffer* _irradianceReduction = nullptr;
|
||||
GPUTexture* _cachePositions = nullptr;
|
||||
GPUTexture* _cacheNormals = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
Builder();
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Called on building start
|
||||
/// </summary>
|
||||
Action OnBuildStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Called on building progress made
|
||||
/// Arguments: current step, current step progress, total progress
|
||||
/// </summary>
|
||||
Delegate<BuildProgressStep, float, float> OnBuildProgress;
|
||||
|
||||
/// <summary>
|
||||
/// Called on building finish (argument: true if build failed, otherwise false)
|
||||
/// </summary>
|
||||
Delegate<bool> OnBuildFinished;
|
||||
|
||||
public:
|
||||
|
||||
void CheckIfRestoreState();
|
||||
bool RestoreState();
|
||||
|
||||
/// <summary>
|
||||
/// Starts building lightmap.
|
||||
/// </summary>
|
||||
void Build();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if build is running.
|
||||
/// </summary>
|
||||
bool IsActive() const
|
||||
{
|
||||
return _isActive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends cancel current build signal.
|
||||
/// </summary>
|
||||
void CancelBuild();
|
||||
|
||||
void Dispose();
|
||||
|
||||
private:
|
||||
|
||||
void saveState();
|
||||
bool loadState();
|
||||
void deleteState();
|
||||
|
||||
void reportProgress(BuildProgressStep step, float stepProgress);
|
||||
void reportProgress(BuildProgressStep step, float stepProgress, int32 subSteps);
|
||||
void reportProgress(BuildProgressStep step, float stepProgress, float totalProgress);
|
||||
void onJobRender(GPUContext* context);
|
||||
bool checkBuildCancelled();
|
||||
bool runStage(BuildingStage stage, bool resetPosition = true);
|
||||
bool initResources();
|
||||
void releaseResources();
|
||||
bool waitForJobDataSync();
|
||||
static bool sortCharts(const LightmapUVsChart& a, const LightmapUVsChart& b);
|
||||
|
||||
int32 doWork();
|
||||
|
||||
void cacheEntries();
|
||||
void generateCharts();
|
||||
void packCharts();
|
||||
void updateLightmaps();
|
||||
void updateEntries();
|
||||
void generateHemispheres();
|
||||
|
||||
#if DEBUG_EXPORT_LIGHTMAPS_PREVIEW
|
||||
static void exportLightmapPreview(SceneBuildCache* scene, int32 lightmapIndex);
|
||||
#endif
|
||||
#if DEBUG_EXPORT_CACHE_PREVIEW
|
||||
void exportCachePreview(SceneBuildCache* scene, GenerateHemispheresData& cacheData, LightmapBuildCache& lightmapEntry) const;
|
||||
#endif
|
||||
#if DEBUG_EXPORT_HEMISPHERES_PREVIEW
|
||||
void addDebugHemisphere(GPUContext* context, GPUTextureView* radianceMap);
|
||||
void downloadDebugHemisphereAtlases(SceneBuildCache* scene);
|
||||
void releaseDebugHemisphereAtlases();
|
||||
#endif
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
||||
26
Source/Engine/ShadowsOfMordor/ShadowsOfMordor.Build.cs
Normal file
26
Source/Engine/ShadowsOfMordor/ShadowsOfMordor.Build.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Flax.Build;
|
||||
using Flax.Build.NativeCpp;
|
||||
|
||||
/// <summary>
|
||||
/// Lightmaps baking module.
|
||||
/// </summary>
|
||||
public class ShadowsOfMordor : EngineModule
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Setup(BuildOptions options)
|
||||
{
|
||||
base.Setup(options);
|
||||
|
||||
options.PublicDefinitions.Add("COMPILE_WITH_GI_BAKING");
|
||||
|
||||
options.PrivateDependencies.Add("ContentImporters");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void GetFilesToDeploy(List<string> files)
|
||||
{
|
||||
}
|
||||
}
|
||||
12
Source/Engine/ShadowsOfMordor/Types.h
Normal file
12
Source/Engine/ShadowsOfMordor/Types.h
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Renderer/Lightmaps.h"
|
||||
|
||||
class Scene;
|
||||
|
||||
namespace ShadowsOfMordor
|
||||
{
|
||||
class SceneLightmapsData;
|
||||
};
|
||||
Reference in New Issue
Block a user