567156b85a
Currently, the first time a raycast is attempted for a model, the raycast will fail and the model's kdtree will asynchronously get built. This breaks the vegetation system, which expects the queries to always work. This adds in a brute-force fallback for use while the kdtree is building. However, other use cases like the Editor mouse cursor selection raycast still should get the current "silent failure" behavior, because otherwise the Editor will lock up for several seconds the first time the mouse moves over an extremely complex model.
337 lines
13 KiB
C++
337 lines
13 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include <Atom/RPI.Reflect/Model/ModelAsset.h>
|
|
#include <Atom/RPI.Reflect/Model/ModelKdTree.h>
|
|
#include <AzCore/Debug/EventTrace.h>
|
|
#include <AzCore/Jobs/JobFunction.h>
|
|
#include <AzCore/Math/IntersectSegment.h>
|
|
#include <AzCore/std/limits.h>
|
|
|
|
#include <AzCore/RTTI/ReflectContext.h>
|
|
#include <AzCore/Serialization/SerializeContext.h>
|
|
|
|
namespace AZ
|
|
{
|
|
namespace RPI
|
|
{
|
|
const char* ModelAsset::DisplayName = "ModelAsset";
|
|
const char* ModelAsset::Group = "Model";
|
|
const char* ModelAsset::Extension = "azmodel";
|
|
|
|
void ModelAsset::Reflect(ReflectContext* context)
|
|
{
|
|
if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
|
|
{
|
|
serializeContext->Class<ModelAsset, Data::AssetData>()
|
|
->Version(0)
|
|
->Field("Name", &ModelAsset::m_name)
|
|
->Field("Aabb", &ModelAsset::m_aabb)
|
|
->Field("LodAssets", &ModelAsset::m_lodAssets)
|
|
;
|
|
}
|
|
}
|
|
|
|
ModelAsset::ModelAsset()
|
|
{
|
|
// c-tor and d-tor have to be defined in .cpp in order to have AZStd::unique_ptr<ModelKdTree> without having to include the header of KDTree
|
|
}
|
|
|
|
ModelAsset::~ModelAsset()
|
|
{
|
|
// c-tor and d-tor have to be defined in .cpp in order to have AZStd::unique_ptr<ModelKdTree> without having to include the header of KDTree
|
|
}
|
|
|
|
const Name& ModelAsset::GetName() const
|
|
{
|
|
return m_name;
|
|
}
|
|
|
|
const Aabb& ModelAsset::GetAabb() const
|
|
{
|
|
return m_aabb;
|
|
}
|
|
|
|
size_t ModelAsset::GetLodCount() const
|
|
{
|
|
return m_lodAssets.size();
|
|
}
|
|
|
|
AZStd::array_view<Data::Asset<ModelLodAsset>> ModelAsset::GetLodAssets() const
|
|
{
|
|
return AZStd::array_view<Data::Asset<ModelLodAsset>>(m_lodAssets);
|
|
}
|
|
|
|
void ModelAsset::SetReady()
|
|
{
|
|
m_status = Data::AssetData::AssetStatus::Ready;
|
|
}
|
|
|
|
bool ModelAsset::LocalRayIntersectionAgainstModel(
|
|
const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, bool allowBruteForce,
|
|
float& distanceNormalized, AZ::Vector3& normal) const
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzRender);
|
|
|
|
if (!m_modelTriangleCount)
|
|
{
|
|
// [GFX TODO][ATOM-4343 Bake mesh spatial information during AP processing]
|
|
m_modelTriangleCount = CalculateTriangleCount();
|
|
}
|
|
|
|
// check the total vertex count for this model and skip kd-tree if the model is simple enough
|
|
if (*m_modelTriangleCount > s_minimumModelTriangleCountToOptimize)
|
|
{
|
|
if (!m_kdTree)
|
|
{
|
|
BuildKdTree();
|
|
|
|
AZ_WarningOnce("Model", false, "ray intersection against a model that is still creating spatial information");
|
|
return allowBruteForce ? BruteForceRayIntersect(rayStart, rayDir, distanceNormalized, normal) : false;
|
|
}
|
|
else
|
|
{
|
|
return m_kdTree->RayIntersection(rayStart, rayDir, distanceNormalized, normal);
|
|
}
|
|
}
|
|
|
|
return BruteForceRayIntersect(rayStart, rayDir, distanceNormalized, normal);
|
|
}
|
|
|
|
void ModelAsset::BuildKdTree() const
|
|
{
|
|
AZStd::lock_guard<AZStd::mutex> lock(m_kdTreeLock);
|
|
if (m_isKdTreeCalculationRunning == false)
|
|
{
|
|
m_isKdTreeCalculationRunning = true;
|
|
|
|
// ModelAsset can go away while the job is queued up or is in progress, keep it alive until the job is done
|
|
const_cast<ModelAsset*>(this)->Acquire();
|
|
|
|
// [GFX TODO][ATOM-4343 Bake mesh spatial information during AP processing]
|
|
// This is a temporary workaround to enable interactive Editor experience.
|
|
// For runtime approach is to do this during asset processing and serialized spatial information alongside with mesh model assets
|
|
const auto jobLambda = [&]() -> void
|
|
{
|
|
AZ_TRACE_METHOD();
|
|
|
|
AZStd::unique_ptr<ModelKdTree> tree = AZStd::make_unique<ModelKdTree>();
|
|
tree->Build(this);
|
|
|
|
AZStd::lock_guard<AZStd::mutex> jobLock(m_kdTreeLock);
|
|
m_isKdTreeCalculationRunning = false;
|
|
m_kdTree = AZStd::move(tree);
|
|
|
|
const_cast<ModelAsset*>(this)->Release();
|
|
};
|
|
|
|
Job* executeGroupJob = aznew JobFunction<decltype(jobLambda)>(jobLambda, true, nullptr); // Auto-deletes
|
|
executeGroupJob->Start();
|
|
}
|
|
}
|
|
|
|
bool ModelAsset::BruteForceRayIntersect(
|
|
const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const
|
|
{
|
|
// brute force - check every triangle
|
|
if (GetLodAssets().empty() == false)
|
|
{
|
|
// intersect against the highest level of detail
|
|
if (ModelLodAsset* loadAssetPtr = GetLodAssets()[0].Get())
|
|
{
|
|
bool anyHit = false;
|
|
AZ::Vector3 intersectionNormal;
|
|
float shortestDistanceNormalized = AZStd::numeric_limits<float>::max();
|
|
for (const ModelLodAsset::Mesh& mesh : loadAssetPtr->GetMeshes())
|
|
{
|
|
float currentDistanceNormalized;
|
|
if (LocalRayIntersectionAgainstMesh(mesh, rayStart, rayDir, currentDistanceNormalized, intersectionNormal))
|
|
{
|
|
anyHit = true;
|
|
|
|
if (currentDistanceNormalized < shortestDistanceNormalized)
|
|
{
|
|
normal = intersectionNormal;
|
|
shortestDistanceNormalized = currentDistanceNormalized;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (anyHit)
|
|
{
|
|
distanceNormalized = shortestDistanceNormalized;
|
|
}
|
|
|
|
return anyHit;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ModelAsset::LocalRayIntersectionAgainstMesh(
|
|
const ModelLodAsset::Mesh& mesh,
|
|
const AZ::Vector3& rayStart,
|
|
const AZ::Vector3& rayDir,
|
|
float& distanceNormalized,
|
|
AZ::Vector3& normal) const
|
|
{
|
|
const BufferAssetView& indexBufferView = mesh.GetIndexBufferAssetView();
|
|
const AZStd::array_view<ModelLodAsset::Mesh::StreamBufferInfo>& streamBufferList = mesh.GetStreamBufferInfoList();
|
|
|
|
// find position semantic
|
|
const ModelLodAsset::Mesh::StreamBufferInfo* positionBuffer = nullptr;
|
|
|
|
for (const ModelLodAsset::Mesh::StreamBufferInfo& bufferInfo : streamBufferList)
|
|
{
|
|
if (bufferInfo.m_semantic.m_name == m_positionName)
|
|
{
|
|
positionBuffer = &bufferInfo;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (positionBuffer && positionBuffer->m_bufferAssetView.GetBufferAsset().Get())
|
|
{
|
|
BufferAsset* bufferAssetViewPtr = positionBuffer->m_bufferAssetView.GetBufferAsset().Get();
|
|
BufferAsset* indexAssetViewPtr = indexBufferView.GetBufferAsset().Get();
|
|
|
|
if (!bufferAssetViewPtr || !indexAssetViewPtr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
RHI::BufferViewDescriptor positionBufferViewDesc = bufferAssetViewPtr->GetBufferViewDescriptor();
|
|
AZStd::array_view<uint8_t> positionRawBuffer = bufferAssetViewPtr->GetBuffer();
|
|
|
|
const uint32_t positionElementSize = positionBufferViewDesc.m_elementSize;
|
|
const uint32_t positionElementCount = positionBufferViewDesc.m_elementCount;
|
|
|
|
// Position is 3 floats
|
|
if (positionElementSize != sizeof(float) * 3)
|
|
{
|
|
AZ_Warning("ModelAsset", false, "unsupported mesh posiiton format, only full 3 floats per vertex are supported at the moment");
|
|
return false;
|
|
}
|
|
|
|
AZStd::array_view<uint8_t> indexRawBuffer = indexAssetViewPtr->GetBuffer();
|
|
RHI::BufferViewDescriptor indexRawDesc = indexAssetViewPtr->GetBufferViewDescriptor();
|
|
|
|
bool anyHit = false;
|
|
|
|
const AZ::Vector3 rayEnd = rayStart + rayDir;
|
|
AZ::Vector3 a, b, c;
|
|
AZ::Vector3 intersectionNormal;
|
|
|
|
float shortestDistanceNormalized = AZStd::numeric_limits<float>::max();
|
|
const AZ::u32* indexPtr = reinterpret_cast<const AZ::u32*>(indexRawBuffer.data());
|
|
for (uint32_t indexIter = 0; indexIter <= indexRawDesc.m_elementCount - 3; indexIter += 3, indexPtr += 3)
|
|
{
|
|
AZ::u32 index0 = indexPtr[0];
|
|
AZ::u32 index1 = indexPtr[1];
|
|
AZ::u32 index2 = indexPtr[2];
|
|
|
|
if (index0 >= positionElementCount || index1 >= positionElementCount || index2 >= positionElementCount)
|
|
{
|
|
AZ_Warning("ModelAsset", false, "mesh has a bad vertex index");
|
|
return false;
|
|
}
|
|
|
|
const float* p = reinterpret_cast<const float*>(&positionRawBuffer[index0 * positionElementSize]);
|
|
a.Set(const_cast<float*>(p)); // faster than AZ::Vector3 c-tor
|
|
|
|
p = reinterpret_cast<const float*>(&positionRawBuffer[index1 * positionElementSize]);
|
|
b.Set(const_cast<float*>(p));
|
|
|
|
p = reinterpret_cast<const float*>(&positionRawBuffer[index2 * positionElementSize]);
|
|
c.Set(const_cast<float*>(p));
|
|
|
|
float currentDistanceNormalized;
|
|
if (AZ::Intersect::IntersectSegmentTriangleCCW(rayStart, rayEnd, a, b, c, intersectionNormal, currentDistanceNormalized))
|
|
{
|
|
anyHit = true;
|
|
|
|
if (currentDistanceNormalized < shortestDistanceNormalized)
|
|
{
|
|
normal = intersectionNormal;
|
|
shortestDistanceNormalized = currentDistanceNormalized;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (anyHit)
|
|
{
|
|
distanceNormalized = shortestDistanceNormalized;
|
|
}
|
|
|
|
return anyHit;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
AZStd::size_t ModelAsset::CalculateTriangleCount() const
|
|
{
|
|
AZStd::size_t modelTriangleCount = 0;
|
|
|
|
if (GetLodAssets().empty() == false)
|
|
{
|
|
if (ModelLodAsset* loadAssetPtr = GetLodAssets()[0].Get())
|
|
{
|
|
for (const ModelLodAsset::Mesh& mesh : loadAssetPtr->GetMeshes())
|
|
{
|
|
const AZStd::array_view<ModelLodAsset::Mesh::StreamBufferInfo>& streamBufferList = mesh.GetStreamBufferInfoList();
|
|
|
|
// find position semantic
|
|
const ModelLodAsset::Mesh::StreamBufferInfo* positionBuffer = nullptr;
|
|
|
|
for (const ModelLodAsset::Mesh::StreamBufferInfo& bufferInfo : streamBufferList)
|
|
{
|
|
if (bufferInfo.m_semantic.m_name == m_positionName)
|
|
{
|
|
positionBuffer = &bufferInfo;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (positionBuffer)
|
|
{
|
|
const RHI::BufferViewDescriptor& desc = positionBuffer->m_bufferAssetView.GetBufferViewDescriptor();
|
|
modelTriangleCount += desc.m_elementCount / 3;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AZ_Warning("Model", modelTriangleCount < ((2<<23) / 3), "Model has too many vertices for the spatial optimization. Currently only up to 16,777,216 is supported");
|
|
|
|
return modelTriangleCount;
|
|
}
|
|
|
|
bool ModelAssetHandler::HasConflictingProducts(const AZStd::vector<AZ::Data::AssetType>& productAssetTypes) const
|
|
{
|
|
size_t modelAssetCount = 0;
|
|
size_t actorAssetCount = 0;
|
|
for (const AZ::Data::AssetType& assetType : productAssetTypes)
|
|
{
|
|
if (assetType == azrtti_typeid<ModelAsset>())
|
|
{
|
|
modelAssetCount++;
|
|
}
|
|
else if (assetType == AZ::Data::AssetType("{F67CC648-EA51-464C-9F5D-4A9CE41A7F86}")) // ActorAsset
|
|
{
|
|
actorAssetCount++;
|
|
}
|
|
}
|
|
|
|
// When dropping a well-defined character, consisting of a mesh and a skeleton/actor,
|
|
// do not create an entity with a mesh component.
|
|
return modelAssetCount == 1 && actorAssetCount == 1;
|
|
}
|
|
} // namespace RPI
|
|
} // namespace AZ
|