2f1346c719
* Breaking up terrain FP wip - macro materials decoupled from meshes. Mesh creation and rendering pulled out from MeshFeatureProcessor into TerrainMeshManager. Stubs added for macro and detail material managers. Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Separated macro material management from the terrain feature processor. Also separated bindless image array handling from terrain feature processor. Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Detail materials separated from terrain feature processor. Also pulled out Aabb2i and Vector2i into their own simple classes Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Changed some classes so that when the SRG changes the classes don't need to be completely reinitialized and can instead just update their indices and push data to the new srg. Fixed an issue where MacroMaterialData wasn't being exposed to ScriptCanvas. Terrain shader reloads should now work correctly. Also added some debug logging to help catch an issue where sometimes detail materials don't seem to load. Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Terrain PR reveiw updates Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Some small PR review fixes. More comments in TerrainDetailMaterialManager.h Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Fixing unused variable causing clang failure Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Fixing unused variable in release. Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Fixing a forward declare that oddly didn't work on linux. Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Fixing linux missing include... not sure why only linux failed on this. Signed-off-by: Ken Pruiksma <pruiksma@amazon.com> * Adding missing include Signed-off-by: Ken Pruiksma <pruiksma@amazon.com>
355 lines
15 KiB
C++
355 lines
15 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include <TerrainRenderer/TerrainMeshManager.h>
|
|
|
|
#include <AzCore/Math/Frustum.h>
|
|
|
|
#include <Atom/RHI.Reflect/BufferViewDescriptor.h>
|
|
|
|
#include <Atom/RPI.Public/MeshDrawPacket.h>
|
|
#include <Atom/RPI.Public/View.h>
|
|
#include <Atom/RPI.Public/Buffer/Buffer.h>
|
|
#include <Atom/RPI.Public/Model/Model.h>
|
|
#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
|
|
|
|
#include <Atom/RPI.Reflect/Buffer/BufferAsset.h>
|
|
#include <Atom/RPI.Reflect/Buffer/BufferAssetCreator.h>
|
|
#include <Atom/RPI.Reflect/Model/ModelAssetCreator.h>
|
|
#include <Atom/RPI.Reflect/Model/ModelLodAssetCreator.h>
|
|
|
|
#include <Atom/Feature/RenderCommon.h>
|
|
|
|
namespace Terrain
|
|
{
|
|
namespace
|
|
{
|
|
[[maybe_unused]] static const char* TerrainMeshManagerName = "TerrainMeshManager";
|
|
}
|
|
|
|
namespace ShaderInputs
|
|
{
|
|
static const char* const PatchData("m_patchData");
|
|
}
|
|
|
|
void TerrainMeshManager::Initialize()
|
|
{
|
|
if (!InitializePatchModel())
|
|
{
|
|
AZ_Error(TerrainMeshManagerName, false, "Failed to create Terrain render buffers!");
|
|
return;
|
|
}
|
|
|
|
OnTerrainDataChanged(AZ::Aabb::CreateNull(), TerrainDataChangedMask::HeightData);
|
|
AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect();
|
|
|
|
m_isInitialized = true;
|
|
}
|
|
|
|
bool TerrainMeshManager::IsInitialized() const
|
|
{
|
|
return m_isInitialized;
|
|
}
|
|
|
|
void TerrainMeshManager::Reset()
|
|
{
|
|
AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect();
|
|
m_patchModel = {};
|
|
m_sectorData.clear();
|
|
m_rebuildSectors = true;
|
|
m_isInitialized = false;
|
|
}
|
|
|
|
bool TerrainMeshManager::CheckRebuildSurfaces(MaterialInstance materialInstance, AZ::RPI::Scene& parentScene)
|
|
{
|
|
if (!m_rebuildSectors)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
m_rebuildSectors = false;
|
|
m_sectorData.clear();
|
|
|
|
const auto layout = materialInstance->GetAsset()->GetObjectSrgLayout();
|
|
|
|
AZ::RHI::ShaderInputConstantIndex patchDataIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::PatchData));
|
|
AZ_Error(TerrainMeshManagerName, patchDataIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::PatchData);
|
|
|
|
const float xFirstPatchStart = AZStd::floorf(m_worldBounds.GetMin().GetX() / GridMeters) * GridMeters;
|
|
const float xLastPatchStart = AZStd::floorf(m_worldBounds.GetMax().GetX() / GridMeters) * GridMeters;
|
|
const float yFirstPatchStart = AZStd::floorf(m_worldBounds.GetMin().GetY() / GridMeters) * GridMeters;
|
|
const float yLastPatchStart = AZStd::floorf(m_worldBounds.GetMax().GetY() / GridMeters) * GridMeters;
|
|
|
|
const auto& materialAsset = materialInstance->GetAsset();
|
|
const auto& shaderAsset = materialAsset->GetMaterialTypeAsset()->GetShaderAssetForObjectSrg();
|
|
|
|
for (float yPatch = yFirstPatchStart; yPatch <= yLastPatchStart; yPatch += GridMeters)
|
|
{
|
|
for (float xPatch = xFirstPatchStart; xPatch <= xLastPatchStart; xPatch += GridMeters)
|
|
{
|
|
ShaderTerrainData objectSrgData;
|
|
objectSrgData.m_xyTranslation = { xPatch, yPatch };
|
|
|
|
m_sectorData.push_back();
|
|
SectorData& sectorData = m_sectorData.back();
|
|
|
|
for (auto& lod : m_patchModel->GetLods())
|
|
{
|
|
objectSrgData.m_xyScale = m_sampleSpacing * GridSize;
|
|
|
|
auto objectSrg = AZ::RPI::ShaderResourceGroup::Create(shaderAsset, materialAsset->GetObjectSrgLayout()->GetName());
|
|
if (!objectSrg)
|
|
{
|
|
AZ_WarningOnce(TerrainMeshManagerName, false, "Failed to create a new shader resource group, skipping.");
|
|
continue;
|
|
}
|
|
objectSrg->SetConstant(patchDataIndex, objectSrgData);
|
|
objectSrg->Compile();
|
|
|
|
AZ::RPI::ModelLod& modelLod = *lod.get();
|
|
sectorData.m_drawPackets.emplace_back(modelLod, 0, materialInstance, objectSrg);
|
|
AZ::RPI::MeshDrawPacket& drawPacket = sectorData.m_drawPackets.back();
|
|
|
|
sectorData.m_srgs.emplace_back(objectSrg);
|
|
|
|
// set the shader option to select forward pass IBL specular if necessary
|
|
if (!drawPacket.SetShaderOption(AZ::Name("o_meshUseForwardPassIBLSpecular"), AZ::RPI::ShaderOptionValue{ false }))
|
|
{
|
|
AZ_Warning(TerrainMeshManagerName, false, "Failed to set o_meshUseForwardPassIBLSpecular on mesh draw packet");
|
|
}
|
|
const uint8_t stencilRef = AZ::Render::StencilRefs::UseDiffuseGIPass | AZ::Render::StencilRefs::UseIBLSpecularPass;
|
|
drawPacket.SetStencilRef(stencilRef);
|
|
drawPacket.Update(parentScene, true);
|
|
}
|
|
|
|
sectorData.m_aabb =
|
|
AZ::Aabb::CreateFromMinMax(
|
|
AZ::Vector3(xPatch, yPatch, m_worldBounds.GetMin().GetZ()),
|
|
AZ::Vector3(xPatch + GridMeters, yPatch + GridMeters, m_worldBounds.GetMax().GetZ())
|
|
);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void TerrainMeshManager::DrawMeshes(const AZ::RPI::FeatureProcessor::RenderPacket& process)
|
|
{
|
|
for (auto& sectorData : m_sectorData)
|
|
{
|
|
uint8_t lodChoice = AZ::RPI::ModelLodAsset::LodCountMax;
|
|
|
|
// Go through all cameras and choose an LOD based on the closest camera.
|
|
for (auto& view : process.m_views)
|
|
{
|
|
if ((view->GetUsageFlags() & AZ::RPI::View::UsageFlags::UsageCamera) > 0)
|
|
{
|
|
const AZ::Vector3 cameraPosition = view->GetCameraTransform().GetTranslation();
|
|
const AZ::Vector2 cameraPositionXY = AZ::Vector2(cameraPosition.GetX(), cameraPosition.GetY());
|
|
const AZ::Vector2 sectorCenterXY = AZ::Vector2(sectorData.m_aabb.GetCenter().GetX(), sectorData.m_aabb.GetCenter().GetY());
|
|
|
|
const float sectorDistance = sectorCenterXY.GetDistance(cameraPositionXY);
|
|
|
|
// This will be configurable later
|
|
const float minDistanceForLod0 = (GridMeters * 4.0f);
|
|
|
|
// For every distance doubling beyond a minDistanceForLod0, we only need half the mesh density. Each LOD
|
|
// is exactly half the resolution of the last.
|
|
const float lodForCamera = AZStd::floorf(AZ::GetMax(0.0f, log2f(sectorDistance / minDistanceForLod0)));
|
|
|
|
// All cameras should render the same LOD so effects like shadows are consistent.
|
|
lodChoice = AZ::GetMin(lodChoice, aznumeric_cast<uint8_t>(lodForCamera));
|
|
}
|
|
}
|
|
|
|
// Add the correct LOD draw packet for visible sectors.
|
|
for (auto& view : process.m_views)
|
|
{
|
|
AZ::Frustum viewFrustum = AZ::Frustum::CreateFromMatrixColumnMajor(view->GetWorldToClipMatrix());
|
|
if (viewFrustum.IntersectAabb(sectorData.m_aabb) != AZ::IntersectResult::Exterior)
|
|
{
|
|
const uint8_t lodToRender = AZ::GetMin(lodChoice, aznumeric_cast<uint8_t>(sectorData.m_drawPackets.size() - 1));
|
|
view->AddDrawPacket(sectorData.m_drawPackets.at(lodToRender).GetRHIDrawPacket());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TerrainMeshManager::RebuildDrawPackets(AZ::RPI::Scene& scene)
|
|
{
|
|
for (auto& sectorData : m_sectorData)
|
|
{
|
|
for (auto& drawPacket : sectorData.m_drawPackets)
|
|
{
|
|
drawPacket.Update(scene, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TerrainMeshManager::OnTerrainDataDestroyBegin()
|
|
{
|
|
Reset();
|
|
}
|
|
|
|
void TerrainMeshManager::OnTerrainDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask)
|
|
{
|
|
if ((dataChangedMask & (TerrainDataChangedMask::HeightData | TerrainDataChangedMask::Settings)) != 0)
|
|
{
|
|
AZ::Aabb worldBounds = AZ::Aabb::CreateNull();
|
|
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
|
|
worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
|
|
|
|
AZ::Vector2 queryResolution2D = AZ::Vector2(1.0f);
|
|
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
|
|
queryResolution2D, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution);
|
|
// Currently query resolution is multidimensional but the rendering system only supports this changing in one dimension.
|
|
float queryResolution = queryResolution2D.GetX();
|
|
|
|
// Sectors need to be rebuilt if the world bounds change in the x/y, or the sample spacing changes.
|
|
m_rebuildSectors = m_rebuildSectors ||
|
|
m_worldBounds.GetMin().GetX() != worldBounds.GetMin().GetX() ||
|
|
m_worldBounds.GetMin().GetY() != worldBounds.GetMin().GetY() ||
|
|
m_worldBounds.GetMax().GetX() != worldBounds.GetMax().GetX() ||
|
|
m_worldBounds.GetMax().GetY() != worldBounds.GetMax().GetY() ||
|
|
m_sampleSpacing != queryResolution;
|
|
|
|
m_worldBounds = worldBounds;
|
|
m_sampleSpacing = queryResolution;
|
|
}
|
|
}
|
|
|
|
void TerrainMeshManager::InitializeTerrainPatch(uint16_t gridSize, PatchData& patchdata)
|
|
{
|
|
patchdata.m_positions.clear();
|
|
patchdata.m_indices.clear();
|
|
|
|
const uint16_t gridVertices = gridSize + 1; // For m_gridSize quads, (m_gridSize + 1) vertices are needed.
|
|
const size_t size = gridVertices * gridVertices;
|
|
|
|
patchdata.m_positions.reserve(size);
|
|
|
|
for (uint16_t y = 0; y < gridVertices; ++y)
|
|
{
|
|
for (uint16_t x = 0; x < gridVertices; ++x)
|
|
{
|
|
patchdata.m_positions.push_back({ aznumeric_cast<float>(x) / gridSize, aznumeric_cast<float>(y) / gridSize });
|
|
}
|
|
}
|
|
|
|
patchdata.m_indices.reserve(gridSize * gridSize * 6); // total number of quads, 2 triangles with 6 indices per quad.
|
|
|
|
for (uint16_t y = 0; y < gridSize; ++y)
|
|
{
|
|
for (uint16_t x = 0; x < gridSize; ++x)
|
|
{
|
|
const uint16_t topLeft = y * gridVertices + x;
|
|
const uint16_t topRight = topLeft + 1;
|
|
const uint16_t bottomLeft = (y + 1) * gridVertices + x;
|
|
const uint16_t bottomRight = bottomLeft + 1;
|
|
|
|
patchdata.m_indices.emplace_back(topLeft);
|
|
patchdata.m_indices.emplace_back(topRight);
|
|
patchdata.m_indices.emplace_back(bottomLeft);
|
|
patchdata.m_indices.emplace_back(bottomLeft);
|
|
patchdata.m_indices.emplace_back(topRight);
|
|
patchdata.m_indices.emplace_back(bottomRight);
|
|
}
|
|
}
|
|
}
|
|
|
|
AZ::Outcome<AZ::Data::Asset<AZ::RPI::BufferAsset>> TerrainMeshManager::CreateBufferAsset(
|
|
const void* data, const AZ::RHI::BufferViewDescriptor& bufferViewDescriptor, const AZStd::string& bufferName)
|
|
{
|
|
AZ::RPI::BufferAssetCreator creator;
|
|
creator.Begin(AZ::Uuid::CreateRandom());
|
|
|
|
AZ::RHI::BufferDescriptor bufferDescriptor;
|
|
bufferDescriptor.m_bindFlags = AZ::RHI::BufferBindFlags::InputAssembly | AZ::RHI::BufferBindFlags::ShaderRead;
|
|
bufferDescriptor.m_byteCount = static_cast<uint64_t>(bufferViewDescriptor.m_elementSize) * static_cast<uint64_t>(bufferViewDescriptor.m_elementCount);
|
|
|
|
creator.SetBuffer(data, bufferDescriptor.m_byteCount, bufferDescriptor);
|
|
creator.SetBufferViewDescriptor(bufferViewDescriptor);
|
|
creator.SetUseCommonPool(AZ::RPI::CommonBufferPoolType::StaticInputAssembly);
|
|
|
|
AZ::Data::Asset<AZ::RPI::BufferAsset> bufferAsset;
|
|
if (creator.End(bufferAsset))
|
|
{
|
|
bufferAsset.SetHint(bufferName);
|
|
return AZ::Success(bufferAsset);
|
|
}
|
|
|
|
return AZ::Failure();
|
|
}
|
|
|
|
bool TerrainMeshManager::InitializePatchModel()
|
|
{
|
|
AZ::RPI::ModelAssetCreator modelAssetCreator;
|
|
modelAssetCreator.Begin(AZ::Uuid::CreateRandom());
|
|
|
|
uint16_t gridSize = GridSize;
|
|
float gridSpacing = GridSpacing;
|
|
|
|
for (uint32_t i = 0; i < AZ::RPI::ModelLodAsset::LodCountMax && gridSize > 0; ++i)
|
|
{
|
|
PatchData patchData;
|
|
InitializeTerrainPatch(gridSize, patchData);
|
|
|
|
const auto positionBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast<uint32_t>(patchData.m_positions.size()), AZ::RHI::Format::R32G32_FLOAT);
|
|
const auto positionsOutcome = CreateBufferAsset(patchData.m_positions.data(), positionBufferViewDesc, "TerrainPatchPositions");
|
|
|
|
const auto indexBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast<uint32_t>(patchData.m_indices.size()), AZ::RHI::Format::R16_UINT);
|
|
const auto indicesOutcome = CreateBufferAsset(patchData.m_indices.data(), indexBufferViewDesc, "TerrainPatchIndices");
|
|
|
|
if (!positionsOutcome.IsSuccess() || !indicesOutcome.IsSuccess())
|
|
{
|
|
AZ_Error(TerrainMeshManagerName, false, "Failed to create GPU buffers for Terrain");
|
|
return false;
|
|
}
|
|
|
|
AZ::RPI::ModelLodAssetCreator modelLodAssetCreator;
|
|
modelLodAssetCreator.Begin(AZ::Uuid::CreateRandom());
|
|
|
|
modelLodAssetCreator.BeginMesh();
|
|
modelLodAssetCreator.AddMeshStreamBuffer(AZ::RHI::ShaderSemantic{ "POSITION" }, AZ::Name(), {positionsOutcome.GetValue(), positionBufferViewDesc});
|
|
modelLodAssetCreator.SetMeshIndexBuffer({indicesOutcome.GetValue(), indexBufferViewDesc});
|
|
|
|
AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(0.0, 0.0, 0.0), AZ::Vector3(GridMeters, GridMeters, 0.0));
|
|
modelLodAssetCreator.SetMeshAabb(AZStd::move(aabb));
|
|
modelLodAssetCreator.SetMeshName(AZ::Name("Terrain Patch"));
|
|
modelLodAssetCreator.EndMesh();
|
|
|
|
AZ::Data::Asset<AZ::RPI::ModelLodAsset> modelLodAsset;
|
|
modelLodAssetCreator.End(modelLodAsset);
|
|
|
|
modelAssetCreator.AddLodAsset(AZStd::move(modelLodAsset));
|
|
|
|
gridSize = gridSize / 2;
|
|
gridSpacing *= 2.0f;
|
|
}
|
|
|
|
AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset;
|
|
bool success = modelAssetCreator.End(modelAsset);
|
|
|
|
m_patchModel = AZ::RPI::Model::FindOrCreate(modelAsset);
|
|
|
|
return success;
|
|
}
|
|
|
|
template<typename Callback>
|
|
void TerrainMeshManager::ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback)
|
|
{
|
|
for (SectorData& sectorData : m_sectorData)
|
|
{
|
|
if (sectorData.m_aabb.Overlaps(bounds))
|
|
{
|
|
callback(sectorData);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|