1322 lines
48 KiB
C++
1322 lines
48 KiB
C++
/*
|
|
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
|
* its licensors.
|
|
*
|
|
* For complete copyright and license terms please see the LICENSE at the root of this
|
|
* distribution (the "License"). All use of this software is governed by the License,
|
|
* or, if provided, by the license below or the license accompanying this file. Do not
|
|
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
*
|
|
*/
|
|
// Original file Copyright Crytek GMBH or its affiliates, used under license.
|
|
|
|
// Description : CPU side SVO
|
|
|
|
|
|
#include "SVOGI_precompiled.h"
|
|
|
|
#include <AzCore/std/parallel/thread.h>
|
|
#include <AzCore/std/parallel/atomic.h>
|
|
#include <AzCore/std/parallel/lock.h>
|
|
|
|
#include "SvoTree.h"
|
|
|
|
#include "TextureBlockPacker.h"
|
|
#include "IRenderAuxGeom.h"
|
|
|
|
#include "FrameProfiler.h"
|
|
|
|
#include "MathConversion.h"
|
|
|
|
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
|
|
|
namespace SVOGI
|
|
{
|
|
//Since the Job System has no priority or size scheduling hints we don't want to starve out other jobs with
|
|
//the longer running GI jobs. This will keep us to a 'reasonable' number of jobs.
|
|
static AZ::u32 s_svoMaxJobCount = (AZStd::thread::hardware_concurrency() / 4 > 0) ? AZStd::thread::hardware_concurrency() / 4 : 1;
|
|
//Scratch working buffer for GI calculations.
|
|
static DataBrick<GISubVoxels>* s_scratchData;
|
|
static AZStd::atomic_bool* s_freeScratch;
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////
|
|
// SVO ENVIRONMENT
|
|
////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
const AZ::s32 SvoEnvironment::s_uninitializedTexturePoolId = 0;
|
|
|
|
AZ::s32 SvoEnvironment::m_currentPassFrameId = 0;
|
|
AZ::s32 SvoEnvironment::m_brickTexturePoolDimXY = 128;
|
|
AZ::s32 SvoEnvironment::m_brickTexturePoolDimZ = 256;
|
|
|
|
SvoEnvironment::SvoEnvironment()
|
|
{
|
|
m_brickTextureFormat = eTF_R8G8B8A8; // eTF_BC3
|
|
|
|
AllocateTexturePools();
|
|
|
|
m_prevCheckVal = -1000000;
|
|
|
|
m_svoRoot.reset();
|
|
m_globalSpecularCM = nullptr;
|
|
m_globalSpecularCM_Mult = 1;
|
|
m_activeVoxels = 0;
|
|
m_evictGpu = false;
|
|
|
|
m_blockPacker = aznew TextureBlockPacker3D(nAtlasDimMaxXY, nAtlasDimMaxXY, nAtlasDimMaxZ, true);
|
|
m_blockIndex = 0;
|
|
s_scratchData = (DataBrick<GISubVoxels>*)azmalloc(sizeof(DataBrick<GISubVoxels>)*s_svoMaxJobCount);
|
|
s_scratchData = new(s_scratchData) DataBrick<GISubVoxels>[s_svoMaxJobCount];
|
|
s_freeScratch = new AZStd::atomic_bool[s_svoMaxJobCount];
|
|
for (AZ::s32 i = 0; i < s_svoMaxJobCount; ++i)
|
|
{
|
|
s_freeScratch[i].store(true);
|
|
}
|
|
|
|
EBUS_EVENT_RESULT(m_jobContext, AZ::JobManagerBus, GetGlobalContext);
|
|
}
|
|
|
|
SvoEnvironment::~SvoEnvironment()
|
|
{
|
|
m_voxelJobsCompletion.StartAndWaitForCompletion();
|
|
// To avoid potential memory issues during tear down, ensure that all voxels are destroyed
|
|
// before destroying the block packer as they refer to memory owned by the block packer.
|
|
m_processingQueue.clear();
|
|
m_brickUpdateQueue.clear();
|
|
m_svoRoot.reset();
|
|
|
|
delete m_blockPacker;
|
|
delete[] s_freeScratch;
|
|
azfree(s_scratchData);
|
|
|
|
SvoEnvironment::m_currentPassFrameId = 0;
|
|
|
|
DeallocateTexturePools();
|
|
}
|
|
|
|
void SvoEnvironment::ReconstructTree()
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
if (gEnv->pConsole->GetCVar("e_svoTI_Active")->GetIVal())
|
|
{
|
|
m_brickUpdateQueue.clear();
|
|
m_processingQueue.clear();
|
|
|
|
m_svoRoot.reset();
|
|
|
|
DeallocateTexturePools();
|
|
AllocateTexturePools();
|
|
|
|
// Keep pending insertions and include inserted meshes to it.
|
|
// They will be reconsidered on the first update after the recreation of the root.
|
|
m_globalInsertions.insert(m_globalInsertedMeshes.begin(), m_globalInsertedMeshes.end());
|
|
m_globalInsertedMeshes.clear();
|
|
m_globalRemovals.clear();
|
|
|
|
AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
|
|
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
|
|
m_svoRoot = AZStd::make_shared<Voxel>(terrainAabb, nullptr, this, 0);
|
|
}
|
|
}
|
|
|
|
//Push all nodes that need to be updated into the update queue.
|
|
void SvoEnvironment::UpdateVoxels()
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
if (!m_svoRoot)
|
|
{
|
|
if (!gEnv->p3DEngine->LevelLoadingInProgress() || gEnv->IsEditor())
|
|
{
|
|
AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
|
|
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
|
|
m_svoRoot = AZStd::make_shared<Voxel>(terrainAabb, nullptr, this, 0);
|
|
}
|
|
}
|
|
|
|
if (m_svoRoot)
|
|
{
|
|
float maxSize = gEnv->pConsole->GetCVar("e_svoMaxNodeSize")->GetFVal();
|
|
float minSize = gEnv->pConsole->GetCVar("e_svoMinNodeSize")->GetFVal();
|
|
|
|
bool hasNewInsertionsOrRemovals = !(m_globalInsertions.empty() && m_globalRemovals.empty());
|
|
if (hasNewInsertionsOrRemovals)
|
|
{
|
|
m_svoRoot->EnqueueMeshes(m_globalInsertions, m_globalRemovals, maxSize);
|
|
m_globalInsertedMeshes.insert(m_globalInsertions.begin(), m_globalInsertions.end());
|
|
m_globalInsertions.clear();
|
|
m_globalRemovals.clear();
|
|
}
|
|
|
|
m_svoRoot->Update(m_processingQueue, m_svoRoot, maxSize, minSize);
|
|
}
|
|
}
|
|
|
|
//Process nodes that need updating and remove outdated nodes.
|
|
void SvoEnvironment::ProcessVoxels()
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
AZ::s32 maxLoadedNodes = gEnv->pConsole->GetCVar("e_svoMaxBricksOnCPU")->GetIVal();
|
|
AZ::s32 maxNodesPerJob = gEnv->pConsole->GetCVar("e_svoMaxVoxelUpdatesPerJob")->GetIVal();
|
|
float maxSize = gEnv->pConsole->GetCVar("e_svoMaxNodeSize")->GetFVal();
|
|
|
|
while (!m_processingQueue.empty())
|
|
{
|
|
//Compute Free Scratch Space location
|
|
AZ::u32 offset = 0;
|
|
for (; offset < s_svoMaxJobCount; ++offset)
|
|
{
|
|
if (s_freeScratch[offset])
|
|
{
|
|
s_freeScratch[offset].store(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
//No free scratch space stop processing this frame.
|
|
if (offset == s_svoMaxJobCount)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Build working voxel set for job
|
|
AZStd::vector<AZStd::shared_ptr<Voxel>> voxels;
|
|
voxels.reserve(maxNodesPerJob);
|
|
AZ::u32 count = 0;
|
|
while (!m_processingQueue.empty() && count < maxNodesPerJob)
|
|
{
|
|
voxels.push_back(m_processingQueue.front());
|
|
m_processingQueue.pop_front();
|
|
++count;
|
|
}
|
|
|
|
auto voxelJobFunc = [this, offset, voxels, maxSize, maxLoadedNodes]()
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::Renderer, "SvoEnvironment::ProcessVoxels:JobFunc");
|
|
for (auto voxel : voxels)
|
|
{
|
|
voxel->UpdateBrickData(maxSize, maxLoadedNodes, s_scratchData[offset]);
|
|
|
|
voxel->m_cpuEnqueued.store(false);
|
|
}
|
|
|
|
s_freeScratch[offset] = true;
|
|
};
|
|
|
|
AZ::Job* job = AZ::CreateJobFunction(voxelJobFunc, true, m_jobContext);
|
|
job->SetDependent(&m_voxelJobsCompletion);
|
|
job->Start();
|
|
}
|
|
}
|
|
|
|
void SvoEnvironment::EvictVoxels()
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
|
|
if (!m_svoRoot)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AZ::s32 maxLoadedNodes = gEnv->pConsole->GetCVar("e_svoMaxBricksOnCPU")->GetIVal();
|
|
|
|
if ((m_lastEvictionFrame + m_delayToEvictInFrames) < GetCurrPassMainFrameID() ||
|
|
m_activeVoxels >= maxLoadedNodes)
|
|
{
|
|
m_lastEvictionFrame = GetCurrPassMainFrameID();
|
|
|
|
float minSize = gEnv->pConsole->GetCVar("e_svoMinNodeSize")->GetFVal();
|
|
|
|
// Evict voxels older than m_evictionDelay
|
|
m_svoRoot->Evict(m_delayToEvictInFrames, false, minSize);
|
|
|
|
// Warn the user that the value of e_svoMaxBricksOnCPU is not high enough.
|
|
// NOTE: Because Evict() does not release nodes that are or will be processed, we will only
|
|
// warn the user when it keeps happening after a reasonable amount of continuous frames.
|
|
if (m_activeVoxels >= maxLoadedNodes)
|
|
{
|
|
++m_numFramesReachingMaxBricksOnCPU;
|
|
|
|
if (m_numFramesReachingMaxBricksOnCPU >= m_delayToWarnReachingMaxBricksOnCPUInFrames)
|
|
{
|
|
AZ_Warning("SVOGI", false, "Maximum number of active voxels reached (%d). Increment the value of e_svoMaxBricksOnCPU.", maxLoadedNodes);
|
|
m_numFramesReachingMaxBricksOnCPU = 0; // Avoid warn every frame
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_numFramesReachingMaxBricksOnCPU = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SvoEnvironment::EvictGpuData()
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
if (m_evictGpu)
|
|
{
|
|
if (m_svoRoot)
|
|
{
|
|
m_svoRoot->EvictGpuData(m_evictionDelayInFrames, false);
|
|
}
|
|
m_evictGpu = false;
|
|
}
|
|
}
|
|
|
|
void SvoEnvironment::UploadVoxels(bool showVoxels)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
AZ::s32 maxBricksPerFrame = gEnv->pConsole->GetCVar("e_svoMaxBrickUpdates")->GetIVal();
|
|
AZ::s32 bricksUploaded = 0;
|
|
{
|
|
while (!m_brickUpdateQueue.empty() && bricksUploaded <= maxBricksPerFrame)
|
|
{
|
|
AZStd::shared_ptr<Voxel> voxel = m_brickUpdateQueue.front();
|
|
voxel->UpdateBrickRenderData();
|
|
voxel->UpdateTreeRenderData();
|
|
//Mark block as needing to be processed.
|
|
if (voxel->m_block)
|
|
{
|
|
voxel->m_block->m_staticDirty = true;
|
|
voxel->m_block->m_dynamicDirty = true;
|
|
}
|
|
voxel->m_gpuEnqueued = false;
|
|
m_brickUpdateQueue.pop_front();
|
|
++bricksUploaded;
|
|
}
|
|
}
|
|
|
|
if (m_svoRoot)
|
|
{
|
|
float maxSize = gEnv->pConsole->GetCVar("e_svoMaxNodeSize")->GetFVal();
|
|
m_svoRoot->ReserveGPUMemory(m_svoRoot, maxSize);
|
|
m_svoRoot->UpdateGpuTree(m_svoRoot);
|
|
if (showVoxels)
|
|
{
|
|
m_svoRoot->DrawVoxels();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SvoEnvironment::GetSvoStaticTextures(I3DEngine::SSvoStaticTexInfo& svoInfo, PodArray<I3DEngine::SLightTI>* pLightsTI_S, PodArray<I3DEngine::SLightTI>* pLightsTI_D)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
svoInfo.pTexTree = gEnv->pRenderer->EF_GetTextureByID(m_nTexNodePoolId);
|
|
svoInfo.pTexOpac = gEnv->pRenderer->EF_GetTextureByID(m_nTexOpasPoolId);
|
|
svoInfo.pTexRgb0 = gEnv->pRenderer->EF_GetTextureByID(m_nTexRgb0PoolId);
|
|
svoInfo.pTexRgb1 = gEnv->pRenderer->EF_GetTextureByID(m_nTexRgb1PoolId);
|
|
svoInfo.pTexDynl = gEnv->pRenderer->EF_GetTextureByID(m_nTexDynlPoolId);
|
|
svoInfo.pTexRgb2 = gEnv->pRenderer->EF_GetTextureByID(m_nTexRgb2PoolId);
|
|
svoInfo.pTexRgb3 = gEnv->pRenderer->EF_GetTextureByID(m_nTexRgb3PoolId);
|
|
svoInfo.pTexNorm = gEnv->pRenderer->EF_GetTextureByID(m_nTexNormPoolId);
|
|
svoInfo.pTexAldi = gEnv->pRenderer->EF_GetTextureByID(m_nTexAldiPoolId);
|
|
svoInfo.pGlobalSpecCM = m_globalSpecularCM;
|
|
|
|
svoInfo.fGlobalSpecCM_Mult = m_globalSpecularCM_Mult;
|
|
|
|
svoInfo.nTexDimXY = m_brickTexturePoolDimXY;
|
|
svoInfo.nTexDimZ = m_brickTexturePoolDimZ;
|
|
svoInfo.nBrickSize = brickDimension;
|
|
|
|
svoInfo.bSvoReady = true;
|
|
|
|
*pLightsTI_S = m_lightsTI_S;
|
|
*pLightsTI_D = m_lightsTI_D;
|
|
}
|
|
|
|
void SvoEnvironment::GetSvoBricksForUpdate(PodArray<I3DEngine::SSvoNodeInfo>& arrNodeInfo, bool getDynamic)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
arrNodeInfo.Clear();
|
|
|
|
if (!m_blockPacker)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!gEnv->pConsole->GetCVar("e_svoTI_Active")->GetIVal())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const AZ::u32 numberOfBlocks = m_blockPacker->GetNumBlocks();
|
|
|
|
AZ::u32 maxUpdatesPerFrame = gEnv->pConsole->GetCVar("e_svoMaxBrickUpdates")->GetIVal();
|
|
AZ::u32 blocksAdded = 0;
|
|
AZ::u32 oldStart = m_blockIndex;
|
|
for (; blocksAdded < maxUpdatesPerFrame;)
|
|
{
|
|
if (TextureBlock3D* block = m_blockPacker->GetBlockInfo(m_blockIndex))
|
|
{
|
|
if ((!getDynamic && block->m_staticDirty) || (getDynamic && block->m_dynamicDirty))
|
|
{
|
|
I3DEngine::SSvoNodeInfo nodeInfo;
|
|
|
|
nodeInfo.wsBox = AZAabbToLyAABB(block->m_worldBox);
|
|
nodeInfo.tcBox = AZAabbToLyAABB(block->m_textureBox);
|
|
nodeInfo.nAtlasOffset = block->m_atlasOffset;
|
|
++blocksAdded;
|
|
arrNodeInfo.Add(nodeInfo);
|
|
|
|
if (!getDynamic)
|
|
{
|
|
block->m_staticDirty = false;
|
|
}
|
|
else
|
|
{
|
|
block->m_dynamicDirty = false;
|
|
}
|
|
block->m_lastUpdatedFrame = GetCurrPassMainFrameID();
|
|
}
|
|
}
|
|
m_blockIndex = (m_blockIndex + 1) % numberOfBlocks;
|
|
//Walked full block list back to where we started. Break for now.
|
|
if (m_blockIndex == oldStart)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
//Due to the fact that we are not tracking lighting changes
|
|
//refresh the block if it hasn't been updated in a while.
|
|
//Once we have a system for detecting if a light has changed
|
|
//with respect to a given voxel region we can remove this behavior.
|
|
for (AZ::u32 blockIndex = 0; blockIndex < numberOfBlocks; blockIndex++)
|
|
{
|
|
if (TextureBlock3D* block = m_blockPacker->GetBlockInfo(blockIndex))
|
|
{
|
|
if (GetCurrPassMainFrameID() - block->m_lastUpdatedFrame > m_blockUpdateDelayInFrames)
|
|
{
|
|
block->m_staticDirty = true;
|
|
block->m_dynamicDirty = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static AZ::s32 SLightTI_Compare(const void* v1, const void* v2)
|
|
{
|
|
I3DEngine::SLightTI* p[2] = { (I3DEngine::SLightTI*)v1, (I3DEngine::SLightTI*)v2 };
|
|
|
|
if (p[0]->fSortVal > p[1]->fSortVal)
|
|
{
|
|
return 1;
|
|
}
|
|
if (p[0]->fSortVal < p[1]->fSortVal)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void SvoEnvironment::CollectLights()
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
AABB nodeBox;
|
|
nodeBox.Reset();
|
|
|
|
nodeBox.Add(gEnv->pSystem->GetViewCamera().GetPosition());
|
|
|
|
nodeBox.Expand(Vec3(256, 256, 256));
|
|
|
|
m_lightsTI_S.Clear();
|
|
m_lightsTI_D.Clear();
|
|
|
|
if (AZ::s32 nCount = gEnv->p3DEngine->GetObjectsByTypeInBox(eERType_Light, nodeBox, (IRenderNode**)0))
|
|
{
|
|
AZStd::vector<IRenderNode*> arrObjects(nCount);
|
|
nCount = gEnv->p3DEngine->GetObjectsByTypeInBox(eERType_Light, nodeBox, &arrObjects[0]);
|
|
|
|
for (AZ::s32 nL = 0; nL < nCount; nL++)
|
|
{
|
|
ILightSource* pRN = (ILightSource*)arrObjects[nL];
|
|
|
|
CDLight& rLight = pRN->GetLightProperties();
|
|
|
|
I3DEngine::SLightTI lightTI;
|
|
memset(&lightTI, 0, sizeof(lightTI));
|
|
|
|
|
|
IRenderNode::EVoxelGIMode eVoxMode = pRN->GetVoxelGIMode();
|
|
|
|
if (eVoxMode)
|
|
{
|
|
lightTI.vPosR = Vec4(rLight.m_Origin, rLight.m_fRadius);
|
|
|
|
if ((rLight.m_Flags & DLF_PROJECT) && (rLight.m_fLightFrustumAngle < 90.f) && rLight.m_pLightImage)
|
|
{
|
|
lightTI.vDirF = Vec4(pRN->GetMatrix().GetColumn(0), rLight.m_fLightFrustumAngle * 2);
|
|
}
|
|
else
|
|
{
|
|
lightTI.vDirF = Vec4(0, 0, 0, 0);
|
|
}
|
|
|
|
if (eVoxMode == IRenderNode::VM_Dynamic)
|
|
{
|
|
lightTI.vCol = rLight.m_Color.toVec4();
|
|
}
|
|
else
|
|
{
|
|
lightTI.vCol = rLight.m_BaseColor.toVec4();
|
|
}
|
|
|
|
lightTI.vCol.w = (rLight.m_Flags & DLF_CASTSHADOW_MAPS) ? 1.f : 0.f;
|
|
|
|
if (rLight.m_Flags & DLF_SUN)
|
|
{
|
|
lightTI.fSortVal = -1;
|
|
}
|
|
else
|
|
{
|
|
|
|
Vec3 vCamPos = m_camera.GetPosition();
|
|
lightTI.fSortVal = vCamPos.GetDistance(rLight.m_Origin) / max(24.f, rLight.m_fRadius);
|
|
}
|
|
|
|
if (eVoxMode == IRenderNode::VM_Dynamic)
|
|
{
|
|
if ((pRN->GetDrawFrame(0) > 10) && (pRN->GetDrawFrame(0) >= (AZ::s32)GetCurrPassMainFrameID()))
|
|
{
|
|
m_lightsTI_D.Add(lightTI);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_lightsTI_S.Add(lightTI);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_lightsTI_S.Count() > 1)
|
|
{
|
|
qsort(m_lightsTI_S.GetElements(), m_lightsTI_S.Count(), sizeof(m_lightsTI_S[0]), SLightTI_Compare);
|
|
}
|
|
|
|
if (m_lightsTI_D.Count() > 1)
|
|
{
|
|
qsort(m_lightsTI_D.GetElements(), m_lightsTI_D.Count(), sizeof(m_lightsTI_D[0]), SLightTI_Compare);
|
|
}
|
|
|
|
if (m_lightsTI_D.Count() > 8)
|
|
{
|
|
m_lightsTI_D.PreAllocate(8);
|
|
}
|
|
}
|
|
|
|
m_globalSpecularCM = 0;
|
|
m_globalSpecularCM_Mult = 0;
|
|
AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
|
|
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
|
|
AABB areaBox = AZAabbToLyAABB(terrainAabb);
|
|
|
|
if (AZ::s32 nCount = gEnv->p3DEngine->GetObjectsByTypeInBox(eERType_Light, areaBox, (IRenderNode**)0))
|
|
{
|
|
AZStd::vector<IRenderNode*> arrObjects(nCount, nullptr);
|
|
nCount = gEnv->p3DEngine->GetObjectsByTypeInBox(eERType_Light, areaBox, &arrObjects[0]);
|
|
|
|
float fMaxRadius = 999;
|
|
|
|
for (AZ::s32 nL = 0; nL < nCount; nL++)
|
|
{
|
|
ILightSource* pRN = (ILightSource*)arrObjects[nL];
|
|
|
|
CDLight& rLight = pRN->GetLightProperties();
|
|
|
|
if (rLight.m_fRadius > fMaxRadius && rLight.m_Flags & DLF_DEFERRED_CUBEMAPS)
|
|
{
|
|
fMaxRadius = rLight.m_fRadius;
|
|
m_globalSpecularCM = rLight.GetSpecularCubemap();
|
|
m_globalSpecularCM_Mult = rLight.m_SpecMult;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SvoEnvironment::UpsertMesh(AZ::EntityId entityId, AZ::Transform transform, AZ::Aabb worldAabb,
|
|
AZ::Data::Asset<LmbrCentral::MeshAsset> meshAsset, _smart_ptr<IMaterial> material)
|
|
{
|
|
// Both cases insertion and update of a mesh are treated the same
|
|
// way, as a new insertion, so remove the mesh first.
|
|
RemoveMesh(entityId);
|
|
|
|
AZStd::shared_ptr<MeshData> data = AZStd::make_shared<MeshData>(entityId, transform, worldAabb, meshAsset, material);
|
|
|
|
[[maybe_unused]] bool inserted = m_globalInsertions.insert({ entityId, data }).second;
|
|
AZ_Assert(inserted, "Tried to double insert mesh.");
|
|
|
|
// NOTE: At this point the mesh will be added to:
|
|
// - m_globalInsertions
|
|
// - m_globalRemovals (if the mesh was already inserted, this is a mesh update)
|
|
}
|
|
|
|
void SvoEnvironment::RemoveMesh(AZ::EntityId entityId)
|
|
{
|
|
//Check if mesh is in the list of current objects
|
|
auto searchInserted = m_globalInsertedMeshes.find(entityId);
|
|
if (searchInserted != m_globalInsertedMeshes.end())
|
|
{
|
|
m_globalRemovals.insert(*searchInserted);
|
|
m_globalInsertedMeshes.erase(searchInserted);
|
|
}
|
|
|
|
//Remove pending insertions.
|
|
auto searchInsertions = m_globalInsertions.find(entityId);
|
|
if (searchInsertions != m_globalInsertions.end())
|
|
{
|
|
m_globalInsertions.erase(searchInsertions);
|
|
}
|
|
}
|
|
|
|
void SvoEnvironment::AllocateTexturePool(AZ::s32& texPoolId, AZ::s32 width, AZ::s32 height, AZ::s32 depth, ETEX_Format texFormat, AZ::s32 filter, AZ::s32 flags)
|
|
{
|
|
if (texPoolId == s_uninitializedTexturePoolId)
|
|
{
|
|
texPoolId = gEnv->pRenderer->DownLoadToVideoMemory3D(NULL, width, height, depth, texFormat, texFormat, 1, false, filter, 0, 0, flags);
|
|
}
|
|
}
|
|
|
|
void SvoEnvironment::AllocateTexturePools()
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
AZ::s32 nFlagsReadOnly = FT_DONT_STREAM;
|
|
AZ::s32 nFlagsReadWrite = FT_DONT_STREAM | FT_USAGE_UNORDERED_ACCESS | FT_USAGE_UAV_RWTEXTURE;
|
|
|
|
AllocateTexturePool(m_nTexRgb0PoolId, m_brickTexturePoolDimXY, m_brickTexturePoolDimXY, m_brickTexturePoolDimZ, m_brickTextureFormat, FILTER_LINEAR, nFlagsReadWrite);
|
|
|
|
if (gEnv->pConsole->GetCVar("e_svoTI_Active")->GetIVal() &&
|
|
gEnv->pConsole->GetCVar("e_svoTI_IntegrationMode")->GetIVal())
|
|
{
|
|
// direct lighting
|
|
AllocateTexturePool(m_nTexRgb1PoolId, m_brickTexturePoolDimXY, m_brickTexturePoolDimXY, m_brickTexturePoolDimZ, m_brickTextureFormat, FILTER_LINEAR, nFlagsReadWrite);
|
|
|
|
// dyn direct lighting
|
|
AllocateTexturePool(m_nTexDynlPoolId, m_brickTexturePoolDimXY, m_brickTexturePoolDimXY, m_brickTexturePoolDimZ, m_brickTextureFormat, FILTER_LINEAR, nFlagsReadWrite);
|
|
|
|
// propagation
|
|
AllocateTexturePool(m_nTexRgb2PoolId, m_brickTexturePoolDimXY, m_brickTexturePoolDimXY, m_brickTexturePoolDimZ, m_brickTextureFormat, FILTER_LINEAR, nFlagsReadWrite);
|
|
|
|
// propagation
|
|
AllocateTexturePool(m_nTexRgb3PoolId, m_brickTexturePoolDimXY, m_brickTexturePoolDimXY, m_brickTexturePoolDimZ, m_brickTextureFormat, FILTER_LINEAR, nFlagsReadWrite);
|
|
}
|
|
|
|
AllocateTexturePool(m_nTexNormPoolId, m_brickTexturePoolDimXY, m_brickTexturePoolDimXY, m_brickTexturePoolDimZ, m_brickTextureFormat, FILTER_LINEAR, nFlagsReadWrite);
|
|
|
|
AllocateTexturePool(m_nTexAldiPoolId, m_brickTexturePoolDimXY, m_brickTexturePoolDimXY, m_brickTexturePoolDimZ, m_brickTextureFormat, FILTER_LINEAR, nFlagsReadWrite);
|
|
|
|
AllocateTexturePool(m_nTexOpasPoolId, m_brickTexturePoolDimXY, m_brickTexturePoolDimXY, m_brickTexturePoolDimZ, m_brickTextureFormat, FILTER_LINEAR, nFlagsReadWrite);
|
|
|
|
AllocateTexturePool(m_nTexNodePoolId, nVoxNodPoolDimXY, nVoxNodPoolDimXY, nVoxNodPoolDimZ, eTF_R32G32B32A32F, FILTER_POINT, nFlagsReadOnly);
|
|
}
|
|
|
|
void SvoEnvironment::DeallocateTexturePool(AZ::s32& texPoolId)
|
|
{
|
|
if (texPoolId != s_uninitializedTexturePoolId)
|
|
{
|
|
gEnv->pRenderer->RemoveTexture(texPoolId);
|
|
texPoolId = s_uninitializedTexturePoolId;
|
|
}
|
|
}
|
|
|
|
void SvoEnvironment::DeallocateTexturePools()
|
|
{
|
|
DeallocateTexturePool(m_nTexRgb0PoolId);
|
|
DeallocateTexturePool(m_nTexRgb1PoolId);
|
|
DeallocateTexturePool(m_nTexDynlPoolId);
|
|
DeallocateTexturePool(m_nTexRgb2PoolId);
|
|
DeallocateTexturePool(m_nTexRgb3PoolId);
|
|
DeallocateTexturePool(m_nTexNormPoolId);
|
|
DeallocateTexturePool(m_nTexAldiPoolId);
|
|
DeallocateTexturePool(m_nTexOpasPoolId);
|
|
DeallocateTexturePool(m_nTexNodePoolId);
|
|
}
|
|
|
|
void SvoEnvironment::SetCamera(const CCamera& newCam)
|
|
{
|
|
m_camera = newCam;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Voxels
|
|
////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
Voxel::Voxel(const AZ::Aabb& box, AZStd::shared_ptr<Voxel> parent, SvoEnvironment* env, AZ::u8 childIndex)
|
|
{
|
|
AZ_Assert(env, "Invalid SVO Environment");
|
|
AZ_Assert(childIndex < s_numVoxelChildren, "Invalid child index: %d", childIndex);
|
|
|
|
m_parentNode = parent;
|
|
m_nodeBox = box;
|
|
m_boxSize = box.GetZExtent();
|
|
m_svoEnv = env;
|
|
m_cpuEnqueued.store(false);
|
|
m_gpuEnqueued = false;
|
|
m_childIndex = childIndex;
|
|
m_childOffsetsDirty = true;
|
|
m_blockID = TextureBlockPacker3D::s_invalidBlockID;
|
|
m_block = nullptr;
|
|
m_brick = nullptr;
|
|
|
|
// Use current frame to avoid store frame 0 when voxel is created.
|
|
m_lastVisibleFrameId = GetCurrPassMainFrameID();
|
|
m_lastUpdatedFrameId = m_lastVisibleFrameId;
|
|
|
|
m_queueId = 0;
|
|
}
|
|
|
|
Voxel::~Voxel()
|
|
{
|
|
ReleaseBlock();
|
|
|
|
if (m_brick)
|
|
{
|
|
if (m_brick->HasBrickData())
|
|
{
|
|
--m_svoEnv->m_activeVoxels;
|
|
}
|
|
|
|
delete m_brick;
|
|
m_brick = nullptr;
|
|
}
|
|
}
|
|
|
|
void Voxel::ReleaseBlock()
|
|
{
|
|
if (m_blockID != TextureBlockPacker3D::s_invalidBlockID)
|
|
{
|
|
m_svoEnv->m_blockPacker->RemoveBlock(m_blockID);
|
|
m_blockID = TextureBlockPacker3D::s_invalidBlockID;
|
|
}
|
|
m_block = nullptr;
|
|
}
|
|
|
|
void Voxel::Update(AZStd::deque<AZStd::shared_ptr<Voxel>>& processingQueue, AZStd::shared_ptr<Voxel> self, float maxSize, float minSize)
|
|
{
|
|
//If the node is not visible stop updating.
|
|
if (!m_svoEnv->m_camera.IsAABBVisible_E(AZAabbToLyAABB(m_nodeBox)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_lastVisibleFrameId = GetCurrPassMainFrameID();
|
|
|
|
//If voxel is "small" relative to camera distance do not upload data to GPU.
|
|
{
|
|
AZStd::shared_ptr<Voxel> parent = m_parentNode.lock();
|
|
bool voxelIsSmall = GetLodRatio() > m_svoEnv->m_voxelLodCutoff;
|
|
bool parentHasObjectData = parent && parent->m_brick && parent->m_brick->HasBrickData() && !parent->m_brick->m_terrainOnly;
|
|
if (voxelIsSmall && !parentHasObjectData)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_lastUpdatedFrameId = m_lastVisibleFrameId;
|
|
|
|
//Enqueue the node for processing if necessary.
|
|
{
|
|
//Lock to prevent buffer swapping during write
|
|
AZStd::lock_guard<AZStd::mutex> queueLock(m_queueMutex);
|
|
AZ::u32 queueId = m_queueId;
|
|
//If the voxel is not equeued and has either never been processed before (!(m_brick || m_block)) or has entities
|
|
//to be processed enqueue it.
|
|
const bool voxelNotQueued = !m_cpuEnqueued;
|
|
const bool voxelNeverProcessed = !(m_brick || m_block);
|
|
const bool hasInsertionsOrRemovals = (!m_insertions[queueId].empty() || !m_removals[queueId].empty());
|
|
|
|
if (voxelNotQueued && (hasInsertionsOrRemovals || voxelNeverProcessed))
|
|
{
|
|
processingQueue.push_back(self);
|
|
m_cpuEnqueued.store(true);
|
|
}
|
|
}
|
|
|
|
//If node has brick data generate children.
|
|
//If the node is bigger than max size then generate the children automatically.
|
|
if ((m_brick && m_brick->HasBrickData()) || m_boxSize > maxSize)
|
|
{
|
|
AllocateChildren(self, maxSize, minSize);
|
|
}
|
|
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; childIndex++)
|
|
{
|
|
AZStd::shared_ptr<Voxel> child = m_children[childIndex];
|
|
if (child)
|
|
{
|
|
child->Update(processingQueue, child, maxSize, minSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Voxel::Evict(AZ::u32 frameDelay, bool forceEvict, float minSize)
|
|
{
|
|
//This function assumes the root is not evictable.
|
|
//To evict the entire tree reset the root pointer.
|
|
AZStd::shared_ptr<Voxel> parent = m_parentNode.lock();
|
|
|
|
if (parent && (m_lastVisibleFrameId < (GetCurrPassMainFrameID() - frameDelay) || forceEvict))
|
|
{
|
|
// Remove voxel from the tree.
|
|
//
|
|
// When the last shared pointer is destroyed then this voxel will be destroyed and
|
|
// therefore all its children as well. That includes all bricks' data and gpu blocks
|
|
// of this voxel and all its children.
|
|
// The last shared pointer can be:
|
|
// - This function's caller.
|
|
// - If this voxel will be processed a shared pointer is queued in m_processingQueue or m_brickUpdateQueue.
|
|
// - If this Voxel is being processed by a job a shared pointer will be in vector "voxels" (see function SvoEnvironment::ProcessVoxels()).
|
|
//
|
|
// NOTE: It's possible to remove the voxel from m_processingQueue and m_brickUpdateQueue now
|
|
// to get the memory back faster, but it would not be accurate unless walking its children to
|
|
// remove them as well. The cost of walking the children and remove them from the queues is not
|
|
// worth it, eventually they will be processed and destroyed in a few frames anyway.
|
|
parent->m_children[m_childIndex].reset();
|
|
|
|
parent->m_childOffsetsDirty = true;
|
|
|
|
parent.reset();
|
|
}
|
|
else
|
|
{
|
|
parent.reset();
|
|
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; childIndex++)
|
|
{
|
|
if (m_children[childIndex])
|
|
{
|
|
m_children[childIndex]->Evict(frameDelay, forceEvict, minSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Voxel::EvictGpuData(AZ::u32 frameDelay, bool forceEvict)
|
|
{
|
|
if (m_lastVisibleFrameId < (GetCurrPassMainFrameID() - frameDelay) || forceEvict)
|
|
{
|
|
if (m_block)
|
|
{
|
|
ReleaseBlock();
|
|
|
|
AZStd::shared_ptr<Voxel> parent = m_parentNode.lock();
|
|
if (parent)
|
|
{
|
|
parent->m_childOffsetsDirty = true;
|
|
}
|
|
|
|
// Force evict all children's GPU data since current voxel's was evicted
|
|
forceEvict = true;
|
|
}
|
|
}
|
|
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; childIndex++)
|
|
{
|
|
if (m_children[childIndex])
|
|
{
|
|
m_children[childIndex]->EvictGpuData(frameDelay, forceEvict);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Voxel::EnqueueMeshes(const EntityMeshDataMap& insertions, const EntityMeshDataMap& removals, float maxSize)
|
|
{
|
|
EntityMeshDataMap newInsertions;
|
|
EntityMeshDataMap newRemovals;
|
|
|
|
{
|
|
AZStd::lock_guard<AZStd::mutex> writeQueueLock(m_queueMutex);
|
|
AZ::u32 queueId = m_queueId;
|
|
auto& insertionQueue = m_insertions[queueId];
|
|
auto& removalQueue = m_removals[queueId];
|
|
|
|
for (auto& meshToRemove : removals)
|
|
{
|
|
//Check if mesh is in the list of inserted or pending insertion meshes
|
|
auto searchInserted = m_insertedAndPendingInsertionMeshes.find(meshToRemove.first);
|
|
if (searchInserted != m_insertedAndPendingInsertionMeshes.end())
|
|
{
|
|
newRemovals.insert(*searchInserted);
|
|
removalQueue.insert(*searchInserted);
|
|
m_insertedAndPendingInsertionMeshes.erase(searchInserted);
|
|
}
|
|
|
|
//Remove pending insertions from current queue.
|
|
auto searchInsertions = insertionQueue.find(meshToRemove.first);
|
|
if (searchInsertions != insertionQueue.end())
|
|
{
|
|
insertionQueue.erase(searchInsertions);
|
|
}
|
|
}
|
|
|
|
for (auto& meshToInsert : insertions)
|
|
{
|
|
//Check if the mesh overlaps the voxel
|
|
if (meshToInsert.second->m_worldAabb.Overlaps(m_nodeBox))
|
|
{
|
|
newInsertions.insert(meshToInsert);
|
|
|
|
bool inserted = insertionQueue.insert(meshToInsert).second;
|
|
AZ_Assert(inserted, "Tried to double insert mesh");
|
|
|
|
// Adding the mesh to the inserted + pending insertion.
|
|
inserted = m_insertedAndPendingInsertionMeshes.insert(meshToInsert).second;
|
|
AZ_Assert(inserted, "Tried to double insert mesh.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enqueue to children new mesh insertions/removals that affected this voxel.
|
|
bool hasNewInsertionsOrRemovals = !(newInsertions.empty() && newRemovals.empty());
|
|
if (hasNewInsertionsOrRemovals)
|
|
{
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; childIndex++)
|
|
{
|
|
if (m_children[childIndex])
|
|
{
|
|
m_children[childIndex]->EnqueueMeshes(newInsertions, newRemovals, maxSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Voxel::UpdateBrickData(float maxSize, AZ::s32 maxLoadedNodes, DataBrick<GISubVoxels>& scratchData)
|
|
{
|
|
// If this voxel doesn't have brick data yet
|
|
// do not allocate more memory if we reached the
|
|
// maximum number of active voxels.
|
|
if (m_boxSize <= maxSize)
|
|
{
|
|
if (!m_brick || !m_brick->HasBrickData())
|
|
{
|
|
if (m_svoEnv->m_activeVoxels >= maxLoadedNodes)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Swap buffers.
|
|
AZ::u32 queueId;
|
|
{
|
|
AZStd::lock_guard<AZStd::mutex> swapQueueLock(m_queueMutex);
|
|
queueId = m_queueId;
|
|
m_queueId = (m_queueId + 1) % s_bufferCount;
|
|
}
|
|
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Renderer);
|
|
|
|
auto& insertions = m_insertions[queueId];
|
|
auto& removals = m_removals[queueId];
|
|
if (m_boxSize <= maxSize)
|
|
{
|
|
if (!m_brick)
|
|
{
|
|
m_brick = aznew Brick();
|
|
}
|
|
|
|
bool hadBrickData = m_brick->HasBrickData();
|
|
|
|
m_brick->m_brickAabb = m_nodeBox;
|
|
|
|
//Convert bounding box to local coordinates
|
|
AZ::Vector3 vCenter = m_nodeBox.GetCenter();
|
|
m_brick->m_brickAabb.Translate(-vCenter);
|
|
m_brick->m_brickOrigin = vCenter;
|
|
m_brick->ProcessMeshes(insertions, removals, scratchData);
|
|
++(m_brick->m_lastUpdated);
|
|
|
|
if (!hadBrickData && m_brick->HasBrickData())
|
|
{
|
|
++m_svoEnv->m_activeVoxels;
|
|
}
|
|
}
|
|
|
|
insertions.clear();
|
|
removals.clear();
|
|
}
|
|
|
|
AZ::Aabb Voxel::GetChildBBox(AZ::u8 childIndex)
|
|
{
|
|
AZ::u8 x = (childIndex / 4);
|
|
AZ::u8 y = (childIndex - x * 4) / 2;
|
|
AZ::u8 z = (childIndex - x * 4 - y * 2);
|
|
AZ::Vector3 vSize = m_nodeBox.GetExtents() * 0.5f;
|
|
AZ::Vector3 vOffset = vSize;
|
|
vOffset *= AZ::Vector3(x, y, z);
|
|
AZ::Aabb childBox;
|
|
childBox.SetMin(m_nodeBox.GetMin() + vOffset);
|
|
childBox.SetMax(childBox.GetMin() + vSize);
|
|
return childBox;
|
|
}
|
|
|
|
void Voxel::AllocateChildren(AZStd::shared_ptr<Voxel> self, float maxSize, float minSize)
|
|
{
|
|
//Do not allocate children if we are at minimal size.
|
|
if (m_nodeBox.GetZExtent() <= minSize)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const EntityMeshDataMap noRemovals;
|
|
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; ++childIndex)
|
|
{
|
|
//Check if child needs to be allocated.
|
|
if (!m_children[childIndex])
|
|
{
|
|
AZ::Aabb childBox = GetChildBBox(childIndex);
|
|
|
|
//If the child node is not visible skip its creation.
|
|
if (!m_svoEnv->m_camera.IsAABBVisible_E(AZAabbToLyAABB(childBox)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
m_children[childIndex] = AZStd::make_shared<Voxel>(childBox, self, m_svoEnv, childIndex);
|
|
|
|
//Propagate meshes to the child
|
|
if (!m_insertedAndPendingInsertionMeshes.empty())
|
|
{
|
|
m_children[childIndex]->EnqueueMeshes(m_insertedAndPendingInsertionMeshes, noRemovals, maxSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Voxel::ReserveGPUMemory(AZStd::shared_ptr<Voxel> self, float maxSize)
|
|
{
|
|
if (!m_svoEnv->m_camera.IsAABBVisible_E(AZAabbToLyAABB(m_nodeBox)))
|
|
{
|
|
// If voxel is not visible, its children aren't either
|
|
return;
|
|
}
|
|
|
|
//If the voxel has data but hasn't been uploaded we will check if it needs to be uploaded.
|
|
|
|
//Note: due to how the gpu offsets are being calculated we have to allocate blocks to the larger nodes
|
|
//This will need to be fixed later.
|
|
if (!m_block && ((m_brick && m_brick->HasBrickData()) || (m_boxSize > maxSize)))
|
|
{
|
|
AZ::s32 blockWidth = 1;
|
|
AZ::s32 blockHeight = 1;
|
|
AZ::s32 blockDepth = 1;
|
|
|
|
m_blockID = m_svoEnv->m_blockPacker->AddBlock(blockWidth, blockHeight, blockDepth, m_nodeBox);
|
|
if (m_blockID != TextureBlockPacker3D::s_invalidBlockID)
|
|
{
|
|
m_block = m_svoEnv->m_blockPacker->GetBlockInfo(m_blockID);
|
|
AZ_Assert(m_block, "Invalid block ID %d", m_blockID);
|
|
}
|
|
else
|
|
{
|
|
m_svoEnv->m_evictGpu = true;
|
|
return; //Unable to reserve a block.
|
|
}
|
|
|
|
AZStd::shared_ptr<Voxel> parent = m_parentNode.lock();
|
|
if (parent)
|
|
{
|
|
parent->m_childOffsetsDirty = true;
|
|
}
|
|
}
|
|
|
|
if (m_block)
|
|
{
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; childIndex++)
|
|
{
|
|
AZStd::shared_ptr<Voxel> child = m_children[childIndex];
|
|
if (child)
|
|
{
|
|
child->ReserveGPUMemory(child, maxSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Voxel::UpdateGpuTree(AZStd::shared_ptr<Voxel> self)
|
|
{
|
|
// if we don't have a block on the gpu memory we should do nothing.
|
|
if (m_block)
|
|
{
|
|
// Check voxel is not already inside brick update queue
|
|
// to prevent unnecesary texture block writes.
|
|
if (!m_gpuEnqueued)
|
|
{
|
|
bool brickDataDirty = false;
|
|
if (m_brick && m_brick->m_lastUpdated != m_brick->m_lastUploaded)
|
|
{
|
|
brickDataDirty = true;
|
|
m_brick->m_lastUploaded.store(m_brick->m_lastUpdated);
|
|
}
|
|
if (m_childOffsetsDirty || brickDataDirty)
|
|
{
|
|
m_childOffsetsDirty = false;
|
|
m_svoEnv->m_brickUpdateQueue.push_back(self);
|
|
m_gpuEnqueued = true;
|
|
}
|
|
}
|
|
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; childIndex++)
|
|
{
|
|
AZStd::shared_ptr<Voxel> child = m_children[childIndex];
|
|
if (child)
|
|
{
|
|
child->UpdateGpuTree(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AZ::s32 Voxel::GetOffset()
|
|
{
|
|
return m_block ? m_block->m_atlasOffset : -2;
|
|
}
|
|
|
|
void Voxel::UpdateTreeRenderData()
|
|
{
|
|
if (!m_block)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vec3i vOffset(m_block->m_minX, m_block->m_minY, m_block->m_minZ);
|
|
|
|
Vec4 treeData[nVoxNodMaxDim * nVoxNodMaxDim * nVoxNodMaxDim];
|
|
memset(treeData, 0x00, sizeof(treeData));
|
|
|
|
AZ::s32 childOffsets[s_numVoxelChildren] = { -2, -2, -2, -2, -2, -2, -2, -2 };
|
|
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; childIndex++)
|
|
{
|
|
if (m_children[childIndex])
|
|
{
|
|
childOffsets[childIndex] = m_children[childIndex]->GetOffset();
|
|
}
|
|
}
|
|
|
|
treeData[0] = Vec4(AZVec3ToLYVec3(m_nodeBox.GetMin()), 0);
|
|
treeData[1] = treeData[0] + Vec4(Vec3(1, 1, 1) * m_nodeBox.GetZExtent(), 0);
|
|
treeData[0].w = m_nodeBox.GetZExtent();
|
|
{
|
|
AZStd::shared_ptr<Voxel> parent = m_parentNode.lock();
|
|
treeData[1].w = parent ? (0.1f + (float)parent->GetOffset()) : -2.f;
|
|
}
|
|
|
|
for (AZ::s32 c = 0; c < 4; c++)
|
|
{
|
|
if (childOffsets[c + 0] >= 0)
|
|
{
|
|
treeData[2][c] = 0.1f + (float)childOffsets[c + 0];
|
|
}
|
|
else
|
|
{
|
|
treeData[2][c] = -0.1f + (float)childOffsets[c + 0];
|
|
}
|
|
|
|
if (childOffsets[c + 4] >= 0)
|
|
{
|
|
treeData[3][c] = 0.1f + (float)childOffsets[c + 4];
|
|
}
|
|
else
|
|
{
|
|
treeData[3][c] = -0.1f + (float)childOffsets[c + 4];
|
|
}
|
|
}
|
|
|
|
treeData[4][0] = 0.1f + (float)gEnv->pRenderer->GetFrameID(false);
|
|
|
|
gEnv->pRenderer->UpdateTextureInVideoMemory(
|
|
m_svoEnv->m_nTexNodePoolId,
|
|
(AZ::u8*)&treeData[0],
|
|
vOffset.x * nVoxNodMaxDim,
|
|
vOffset.y * nVoxNodMaxDim,
|
|
nVoxNodMaxDim,
|
|
nVoxNodMaxDim,
|
|
eTF_R32G32B32A32F,
|
|
vOffset.z * nVoxNodMaxDim,
|
|
nVoxNodMaxDim);
|
|
}
|
|
|
|
void Voxel::UpdateBrickRenderData()
|
|
{
|
|
if (!m_block)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!m_brick || !m_brick->HasBrickData())
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Lock and read what data is currently there. If a job is half way through processing then it could potentially
|
|
//cause a partial upload, but the job will re-mark the data as dirty for the next possible frame.
|
|
//This is to avoid writing to the data while it is uploading.
|
|
AZStd::shared_lock<AZStd::shared_mutex> uploadLock(m_brick->m_brickDataMutex);
|
|
|
|
Vec3i vOffset(m_block->m_minX, m_block->m_minY, m_block->m_minZ);
|
|
|
|
const byte* pImgRgb = reinterpret_cast<const byte*>(m_brick->m_colors->m_data);
|
|
const byte* pImgNor = reinterpret_cast<const byte*>(m_brick->m_normals->m_data);
|
|
const byte* pImgOpa = reinterpret_cast<const byte*>(m_brick->m_opacities->m_data);
|
|
|
|
Vec3i vSizeFin;
|
|
vSizeFin.x = (brickDimension);
|
|
vSizeFin.y = (brickDimension);
|
|
vSizeFin.z = (brickDimension);
|
|
|
|
gEnv->pRenderer->UpdateTextureInVideoMemory(
|
|
m_svoEnv->m_nTexRgb0PoolId,
|
|
pImgRgb,
|
|
vOffset.x * nVoxBloMaxDim,
|
|
vOffset.y * nVoxBloMaxDim,
|
|
vSizeFin.x,
|
|
vSizeFin.y,
|
|
m_svoEnv->m_brickTextureFormat,
|
|
vOffset.z * nVoxBloMaxDim,
|
|
vSizeFin.z);
|
|
|
|
gEnv->pRenderer->UpdateTextureInVideoMemory(
|
|
m_svoEnv->m_nTexNormPoolId,
|
|
pImgNor,
|
|
vOffset.x * nVoxBloMaxDim,
|
|
vOffset.y * nVoxBloMaxDim,
|
|
vSizeFin.x,
|
|
vSizeFin.y,
|
|
m_svoEnv->m_brickTextureFormat,
|
|
vOffset.z * nVoxBloMaxDim,
|
|
vSizeFin.z);
|
|
|
|
gEnv->pRenderer->UpdateTextureInVideoMemory(
|
|
m_svoEnv->m_nTexOpasPoolId,
|
|
pImgOpa,
|
|
vOffset.x * nVoxBloMaxDim,
|
|
vOffset.y * nVoxBloMaxDim,
|
|
vSizeFin.x,
|
|
vSizeFin.y,
|
|
m_svoEnv->m_brickTextureFormat,
|
|
vOffset.z * nVoxBloMaxDim,
|
|
vSizeFin.z);
|
|
}
|
|
|
|
float Voxel::GetLodRatio()
|
|
{
|
|
const CCamera& cam = m_svoEnv->m_camera;
|
|
const float dist = m_nodeBox.GetCenter().GetDistance(LYVec3ToAZVec3(cam.GetPosition()));
|
|
return dist / m_boxSize;
|
|
}
|
|
|
|
//Debug drawing functionality.
|
|
void Voxel::DrawVoxels()
|
|
{
|
|
bool drawSelf = true;
|
|
|
|
if (GetLodRatio() > m_svoEnv->m_voxelLodCutoff)
|
|
{
|
|
drawSelf = false;
|
|
}
|
|
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; childIndex++)
|
|
{
|
|
if (m_children[childIndex])
|
|
{
|
|
m_children[childIndex]->DrawVoxels();
|
|
}
|
|
}
|
|
|
|
if (drawSelf)
|
|
{
|
|
DrawBrickData();
|
|
}
|
|
}
|
|
|
|
void Voxel::DrawBrickData()
|
|
{
|
|
const CCamera& cam = m_svoEnv->m_camera;
|
|
if (cam.IsAABBVisible_F(AZAabbToLyAABB(m_nodeBox)))
|
|
{
|
|
if (m_brick && m_brick->HasBrickData())
|
|
{
|
|
AZStd::shared_lock<AZStd::shared_mutex> readLock(m_brick->m_brickDataMutex);
|
|
|
|
AZ::u8 boxLog = static_cast<AZ::u8>(log2(m_boxSize));
|
|
ColorF brickColor;
|
|
ColorF voxelColor = Col_Black;
|
|
|
|
switch ((boxLog % 3) + (m_block ? 3 : 0))
|
|
{
|
|
case 0:
|
|
brickColor = Col_Red;
|
|
break;
|
|
case 1:
|
|
brickColor = Col_Lime;
|
|
break;
|
|
case 2:
|
|
brickColor = Col_Blue;
|
|
break;
|
|
case 3:
|
|
brickColor = Col_Magenta;
|
|
break;
|
|
case 4:
|
|
brickColor = Col_Yellow;
|
|
break;
|
|
case 5:
|
|
brickColor = Col_Cyan;
|
|
break;
|
|
default:
|
|
brickColor = Col_DarkGrey;
|
|
break;
|
|
}
|
|
gEnv->pRenderer->GetIRenderAuxGeom()->SetRenderFlags(SAuxGeomRenderFlags());
|
|
gEnv->pRenderer->GetIRenderAuxGeom()->DrawAABB(AZAabbToLyAABB(m_nodeBox), false, voxelColor, eBBD_Faceted);
|
|
|
|
for (int x = 0; x < brickDimension; x++)
|
|
{
|
|
for (int y = 0; y < brickDimension; y++)
|
|
{
|
|
for (int z = 0; z < brickDimension; z++)
|
|
{
|
|
int id = z * brickDimension * brickDimension + y * brickDimension + x;
|
|
|
|
if ((*m_brick->m_counts)[id] > 0)
|
|
{
|
|
AZ::Vector3 vMin = m_nodeBox.GetMin() + (m_nodeBox.GetMax() - m_nodeBox.GetMin()) * AZ::Vector3((float)x / brickDimension, (float)y / brickDimension, (float)z / brickDimension);
|
|
AZ::Vector3 vMax = m_nodeBox.GetMin() + (m_nodeBox.GetMax() - m_nodeBox.GetMin()) * AZ::Vector3((float)(x + 1) / brickDimension, (float)(y + 1) / brickDimension, (float)(z + 1) / brickDimension);
|
|
|
|
AZ::Aabb brickBox;
|
|
brickBox.SetMin(vMin);
|
|
brickBox.SetMax(vMax);
|
|
|
|
bool doNotDraw = false;
|
|
|
|
for (AZ::u32 childIndex = 0; childIndex < s_numVoxelChildren; childIndex++)
|
|
{
|
|
Voxel* child = m_children[childIndex].get();
|
|
if (child && child->GetLodRatio() <= m_svoEnv->m_voxelLodCutoff)
|
|
{
|
|
if (Overlap::AABB_AABB(AZAabbToLyAABB(child->m_nodeBox), AZAabbToLyAABB(brickBox)))
|
|
{
|
|
doNotDraw = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (doNotDraw)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
gEnv->pRenderer->GetIRenderAuxGeom()->SetRenderFlags(SAuxGeomRenderFlags());
|
|
gEnv->pRenderer->GetIRenderAuxGeom()->DrawAABB(AZAabbToLyAABB(brickBox), false, brickColor, eBBD_Faceted);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace SVOGI
|