You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/WhiteBox/Code/Source/EditorWhiteBoxComponent.cpp

919 lines
38 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 "Asset/EditorWhiteBoxMeshAsset.h"
#include "Asset/WhiteBoxMeshAssetHandler.h"
#include "EditorWhiteBoxComponent.h"
#include "EditorWhiteBoxComponentMode.h"
#include "EditorWhiteBoxComponentModeBus.h"
#include "Rendering/WhiteBoxNullRenderMesh.h"
#include "Rendering/WhiteBoxRenderMeshInterface.h"
#include "Util/WhiteBoxEditorUtil.h"
#include "WhiteBoxComponent.h"
#include <AzCore/Asset/AssetSerializer.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/Console/Console.h>
#include <AzCore/Math/IntersectSegment.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/std/numeric.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzQtComponents/Components/Widgets/FileDialog.h>
#include <AzToolsFramework/API/ComponentEntitySelectionBus.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/Maths/TransformUtils.h>
#include <AzToolsFramework/UI/UICore/WidgetHelpers.h>
#include <QMessageBox>
#include <WhiteBox/EditorWhiteBoxColliderBus.h>
#include <WhiteBox/WhiteBoxBus.h>
// developer debug properties for the White Box mesh to globally enable/disable
AZ_CVAR(bool, cl_whiteBoxDebugVertexHandles, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Display vertex handles");
AZ_CVAR(bool, cl_whiteBoxDebugNormals, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Display normals");
AZ_CVAR(
bool, cl_whiteBoxDebugHalfedgeHandles, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Display halfedge handles");
AZ_CVAR(bool, cl_whiteBoxDebugEdgeHandles, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Display edge handles");
AZ_CVAR(bool, cl_whiteBoxDebugFaceHandles, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Display face handles");
AZ_CVAR(bool, cl_whiteBoxDebugAabb, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Display Aabb for the White Box");
namespace WhiteBox
{
static const char* const AssetSavedUndoRedoDesc = "White Box Mesh asset saved";
static const char* const ObjExtension = "obj";
static void RefreshProperties()
{
AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast(
&AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh,
AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues);
}
// build intermediate data to be passed to WhiteBoxRenderMeshInterface
// to be used to generate concrete render mesh
static WhiteBoxRenderData CreateWhiteBoxRenderData(const WhiteBoxMesh& whiteBox, const WhiteBoxMaterial& material)
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
WhiteBoxRenderData renderData;
WhiteBoxFaces& faceData = renderData.m_faces;
const size_t faceCount = Api::MeshFaceCount(whiteBox);
faceData.reserve(faceCount);
const auto createWhiteBoxFaceFromHandle = [&whiteBox](const Api::FaceHandle& faceHandle) -> WhiteBoxFace
{
const auto copyVertex = [&whiteBox](const Api::HalfedgeHandle& in, WhiteBoxVertex& out)
{
const auto vh = Api::HalfedgeVertexHandleAtTip(whiteBox, in);
out.m_position = Api::VertexPosition(whiteBox, vh);
out.m_uv = Api::HalfedgeUV(whiteBox, in);
};
WhiteBoxFace face;
face.m_normal = Api::FaceNormal(whiteBox, faceHandle);
const auto faceHalfedgeHandles = Api::FaceHalfedgeHandles(whiteBox, faceHandle);
copyVertex(faceHalfedgeHandles[0], face.m_v1);
copyVertex(faceHalfedgeHandles[1], face.m_v2);
copyVertex(faceHalfedgeHandles[2], face.m_v3);
return face;
};
const auto faceHandles = Api::MeshFaceHandles(whiteBox);
for (const auto& faceHandle : faceHandles)
{
faceData.push_back(createWhiteBoxFaceFromHandle(faceHandle));
}
renderData.m_material = material;
return renderData;
}
static bool IsWhiteBoxNullRenderMesh(const AZStd::optional<AZStd::unique_ptr<RenderMeshInterface>>& m_renderMesh)
{
return azrtti_cast<WhiteBoxNullRenderMesh*>((*m_renderMesh).get()) != nullptr;
}
static bool DisplayingAsset(const DefaultShapeType defaultShapeType)
{
// checks if the default shape is set to a custom asset
return defaultShapeType == DefaultShapeType::Asset;
}
// callback for when the default shape field is changed
void EditorWhiteBoxComponent::OnDefaultShapeChange()
{
const AZStd::string entityIdStr = AZStd::string::format("%llu", static_cast<AZ::u64>(GetEntityId()));
const AZStd::string componentIdStr = AZStd::string::format("%llu", GetId());
const AZStd::string shapeTypeStr = AZStd::string::format("%d", aznumeric_cast<int>(m_defaultShape));
const AZStd::vector<AZStd::string_view> scriptArgs{entityIdStr, componentIdStr, shapeTypeStr};
// if the shape type has just changed and it is no longer an asset type, check if a mesh asset
// is in use and clear it if so (switch back to using the component serialized White Box mesh)
if (!DisplayingAsset(m_defaultShape) && m_editorMeshAsset->InUse())
{
m_editorMeshAsset->Reset();
}
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(
&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilenameWithArgs,
"@engroot@/Gems/WhiteBox/Editor/Scripts/default_shapes.py", scriptArgs);
EditorWhiteBoxComponentNotificationBus::Event(
AZ::EntityComponentIdPair(GetEntityId(), GetId()),
&EditorWhiteBoxComponentNotificationBus::Events::OnDefaultShapeTypeChanged, m_defaultShape);
}
bool EditorWhiteBoxVersionConverter(
AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement)
{
if (classElement.GetVersion() <= 1)
{
// find the old WhiteBoxMeshAsset stored directly on the component
AZ::Data::Asset<Pipeline::WhiteBoxMeshAsset> meshAsset;
const int meshAssetIndex = classElement.FindElement(AZ_CRC_CE("MeshAsset"));
if (meshAssetIndex != -1)
{
classElement.GetSubElement(meshAssetIndex).GetData(meshAsset);
classElement.RemoveElement(meshAssetIndex);
}
else
{
return false;
}
// add the new EditorWhiteBoxMeshAsset which will contain the previous WhiteBoxMeshAsset
const int editorMeshAssetIndex =
classElement.AddElement<EditorWhiteBoxMeshAsset>(context, "EditorMeshAsset");
if (editorMeshAssetIndex != -1)
{
// insert the existing WhiteBoxMeshAsset into the new EditorWhiteBoxMeshAsset
classElement.GetSubElement(editorMeshAssetIndex)
.AddElementWithData<AZ::Data::Asset<Pipeline::WhiteBoxMeshAsset>>(context, "MeshAsset", meshAsset);
}
else
{
return false;
}
}
return true;
}
void EditorWhiteBoxComponent::Reflect(AZ::ReflectContext* context)
{
EditorWhiteBoxMeshAsset::Reflect(context);
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<EditorWhiteBoxComponent, EditorComponentBase>()
->Version(2, &EditorWhiteBoxVersionConverter)
->Field("WhiteBoxData", &EditorWhiteBoxComponent::m_whiteBoxData)
->Field("DefaultShape", &EditorWhiteBoxComponent::m_defaultShape)
->Field("EditorMeshAsset", &EditorWhiteBoxComponent::m_editorMeshAsset)
->Field("Material", &EditorWhiteBoxComponent::m_material)
->Field("RenderData", &EditorWhiteBoxComponent::m_renderData)
->Field("ComponentMode", &EditorWhiteBoxComponent::m_componentModeDelegate);
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<EditorWhiteBoxComponent>("White Box", "White Box level editing")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "Shape")
->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/WhiteBox.svg")
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/WhiteBox.svg")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c))
->Attribute(
AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/shape/white-box/")
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
->DataElement(
AZ::Edit::UIHandlers::ComboBox, &EditorWhiteBoxComponent::m_defaultShape, "Default Shape",
"Default shape of the white box mesh.")
->EnumAttribute(DefaultShapeType::Cube, "Cube")
->EnumAttribute(DefaultShapeType::Tetrahedron, "Tetrahedron")
->EnumAttribute(DefaultShapeType::Icosahedron, "Icosahedron")
->EnumAttribute(DefaultShapeType::Cylinder, "Cylinder")
->EnumAttribute(DefaultShapeType::Sphere, "Sphere")
->EnumAttribute(DefaultShapeType::Asset, "Mesh Asset")
->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorWhiteBoxComponent::OnDefaultShapeChange)
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c))
->DataElement(
AZ::Edit::UIHandlers::Default, &EditorWhiteBoxComponent::m_editorMeshAsset, "Editor Mesh Asset",
"Editor Mesh Asset")
->Attribute(AZ::Edit::Attributes::Visibility, &EditorWhiteBoxComponent::AssetVisibility)
->UIElement(AZ::Edit::UIHandlers::Button, "Save as asset", "Save as asset")
->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorWhiteBoxComponent::SaveAsAsset)
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c))
->Attribute(AZ::Edit::Attributes::ButtonText, "Save As ...")
->DataElement(
AZ::Edit::UIHandlers::Default, &EditorWhiteBoxComponent::m_material, "White Box Material",
"The properties of the White Box material.")
->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorWhiteBoxComponent::OnMaterialChange)
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(
AZ::Edit::UIHandlers::Default, &EditorWhiteBoxComponent::m_componentModeDelegate,
"Component Mode", "White Box Tool Component Mode")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->UIElement(AZ::Edit::UIHandlers::Button, "", "Export to obj")
->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorWhiteBoxComponent::ExportToFile)
->Attribute(AZ::Edit::Attributes::ButtonText, "Export");
}
}
}
void EditorWhiteBoxComponent::OnMaterialChange()
{
if (m_renderMesh.has_value())
{
(*m_renderMesh)->UpdateMaterial(m_material);
}
RebuildRenderMesh();
}
AZ::Crc32 EditorWhiteBoxComponent::AssetVisibility() const
{
return DisplayingAsset(m_defaultShape) ? AZ::Edit::PropertyVisibility::ShowChildrenOnly
: AZ::Edit::PropertyVisibility::Hide;
}
void EditorWhiteBoxComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
{
required.push_back(AZ_CRC_CE("TransformService"));
}
void EditorWhiteBoxComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC_CE("WhiteBoxService"));
}
void EditorWhiteBoxComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC_CE("NonUniformScaleService"));
incompatible.push_back(AZ_CRC_CE("MeshService"));
incompatible.push_back(AZ_CRC_CE("WhiteBoxService"));
}
EditorWhiteBoxComponent::EditorWhiteBoxComponent() = default;
EditorWhiteBoxComponent::~EditorWhiteBoxComponent()
{
// note: m_editorMeshAsset is (usually) serialized so it is created by the reflection system
// in Reflect (no explicit `new`) - we must still clean-up the resource on destruction though
// to not leak resources.
delete m_editorMeshAsset;
}
void EditorWhiteBoxComponent::Init()
{
if (m_editorMeshAsset)
{
return;
}
// if the m_editorMeshAsset has not been created by the serialization system
// create a new EditorWhiteBoxMeshAsset here
m_editorMeshAsset = aznew EditorWhiteBoxMeshAsset();
}
void EditorWhiteBoxComponent::Activate()
{
const AZ::EntityId entityId = GetEntityId();
const AZ::EntityComponentIdPair entityComponentIdPair{entityId, GetId()};
AzToolsFramework::Components::EditorComponentBase::Activate();
EditorWhiteBoxComponentRequestBus::Handler::BusConnect(entityComponentIdPair);
EditorWhiteBoxComponentNotificationBus::Handler::BusConnect(entityComponentIdPair);
AZ::TransformNotificationBus::Handler::BusConnect(entityId);
AzFramework::BoundsRequestBus::Handler::BusConnect(entityId);
AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(entityId);
AzToolsFramework::EditorComponentSelectionRequestsBus::Handler::BusConnect(entityId);
AzToolsFramework::EditorVisibilityNotificationBus::Handler::BusConnect(entityId);
m_componentModeDelegate.ConnectWithSingleComponentMode<EditorWhiteBoxComponent, EditorWhiteBoxComponentMode>(
entityComponentIdPair, this);
AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity();
AZ::TransformBus::EventResult(worldFromLocal, entityId, &AZ::TransformBus::Events::GetWorldTM);
m_worldFromLocal = AzToolsFramework::TransformUniformScale(worldFromLocal);
m_editorMeshAsset->Associate(entityComponentIdPair);
// deserialize the white box data into a mesh object or load the serialized asset ref
DeserializeWhiteBox();
if (AzToolsFramework::IsEntityVisible(entityId))
{
ShowRenderMesh();
}
}
void EditorWhiteBoxComponent::Deactivate()
{
AzToolsFramework::EditorVisibilityNotificationBus::Handler::BusDisconnect();
AzToolsFramework::EditorComponentSelectionRequestsBus::Handler::BusDisconnect();
AzFramework::EntityDebugDisplayEventBus::Handler::BusDisconnect();
AzFramework::BoundsRequestBus::Handler::BusDisconnect();
AZ::TransformNotificationBus::Handler::BusDisconnect();
EditorWhiteBoxComponentRequestBus::Handler::BusDisconnect();
EditorWhiteBoxComponentNotificationBus::Handler::BusDisconnect();
AzToolsFramework::Components::EditorComponentBase::Deactivate();
m_componentModeDelegate.Disconnect();
m_editorMeshAsset->Release();
m_renderMesh.reset();
m_whiteBox.reset();
}
void EditorWhiteBoxComponent::DeserializeWhiteBox()
{
// create WhiteBoxMesh object from internal data
m_whiteBox = Api::CreateWhiteBoxMesh();
if (m_editorMeshAsset->InUse())
{
m_editorMeshAsset->Load();
}
else
{
// attempt to load the mesh
const auto result = Api::ReadMesh(*m_whiteBox, m_whiteBoxData);
AZ_Error("EditorWhiteBoxComponent", result != WhiteBox::Api::ReadResult::Error, "Error deserializing white box mesh stream");
// if the read was successful but the byte stream is empty
// (there was nothing to load), create a default mesh
if (result == Api::ReadResult::Empty)
{
Api::InitializeAsUnitCube(*m_whiteBox);
}
}
}
void EditorWhiteBoxComponent::RebuildWhiteBox()
{
RebuildRenderMesh();
RebuildPhysicsMesh();
}
void EditorWhiteBoxComponent::BuildGameEntity(AZ::Entity* gameEntity)
{
if (auto* whiteBoxComponent = gameEntity->CreateComponent<WhiteBoxComponent>())
{
// note: it is important no edit time only functions are called here as BuildGameEntity
// will be called by the Asset Processor when creating dynamic slices
whiteBoxComponent->GenerateWhiteBoxMesh(m_renderData);
}
}
WhiteBoxMesh* EditorWhiteBoxComponent::GetWhiteBoxMesh()
{
if (WhiteBoxMesh* whiteBox = m_editorMeshAsset->GetWhiteBoxMesh())
{
return whiteBox;
}
return m_whiteBox.get();
}
void EditorWhiteBoxComponent::OnWhiteBoxMeshModified()
{
// if using an asset, notify other editor mesh assets using the same id that
// the asset has been modified, this will in turn cause all components to update
// their render and physics meshes
if (m_editorMeshAsset->InUse())
{
WhiteBoxMeshAssetNotificationBus::Event(
m_editorMeshAsset->GetWhiteBoxMeshAssetId(),
&WhiteBoxMeshAssetNotificationBus::Events::OnWhiteBoxMeshAssetModified,
m_editorMeshAsset->GetWhiteBoxMeshAsset());
}
// otherwise, update the render and physics mesh immediately
else
{
RebuildWhiteBox();
}
}
void EditorWhiteBoxComponent::RebuildRenderMesh()
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
// reset caches when the mesh changes
m_worldAabb.reset();
m_localAabb.reset();
m_faces.reset();
AZ::Interface<AzFramework::IEntityBoundsUnion>::Get()->RefreshEntityLocalBoundsUnion(GetEntityId());
// must have been created in Activate or have had the Entity made visible again
if (m_renderMesh.has_value())
{
// cache the white box render data
m_renderData = CreateWhiteBoxRenderData(*GetWhiteBoxMesh(), m_material);
// it's possible the white box mesh data isn't yet ready (for example if it's stored
// in an asset which hasn't finished loading yet) so don't attempt to create a render
// mesh with no data
if (!m_renderData.m_faces.empty())
{
// check if we need to instantiate a concrete render mesh implementation
if (IsWhiteBoxNullRenderMesh(m_renderMesh))
{
// create a concrete implementation of the render mesh
WhiteBoxRequestBus::BroadcastResult(m_renderMesh, &WhiteBoxRequests::CreateRenderMeshInterface);
}
// generate the mesh
// TODO: LYN-786
(*m_renderMesh)->BuildMesh(m_renderData, m_worldFromLocal, GetEntityId());
}
}
EditorWhiteBoxComponentModeRequestBus::Event(
AZ::EntityComponentIdPair{GetEntityId(), GetId()},
&EditorWhiteBoxComponentModeRequests::MarkWhiteBoxIntersectionDataDirty);
}
void EditorWhiteBoxComponent::WriteAssetToComponent()
{
if (m_editorMeshAsset->Loaded())
{
Api::WriteMesh(*m_editorMeshAsset->GetWhiteBoxMesh(), m_whiteBoxData);
}
}
void EditorWhiteBoxComponent::SerializeWhiteBox()
{
if (m_editorMeshAsset->Loaded())
{
m_editorMeshAsset->Serialize();
}
else
{
Api::WriteMesh(*m_whiteBox, m_whiteBoxData);
}
}
void EditorWhiteBoxComponent::SetDefaultShape(const DefaultShapeType defaultShape)
{
m_defaultShape = defaultShape;
OnDefaultShapeChange();
}
void EditorWhiteBoxComponent::OnTransformChanged(
[[maybe_unused]] const AZ::Transform& local, const AZ::Transform& world)
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
m_worldAabb.reset();
m_localAabb.reset();
const AZ::Transform worldUniformScale = AzToolsFramework::TransformUniformScale(world);
m_worldFromLocal = worldUniformScale;
if (m_renderMesh.has_value())
{
(*m_renderMesh)->UpdateTransform(worldUniformScale);
}
}
void EditorWhiteBoxComponent::RebuildPhysicsMesh()
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
EditorWhiteBoxColliderRequestBus::Event(
GetEntityId(), &EditorWhiteBoxColliderRequests::CreatePhysics, *GetWhiteBoxMesh());
}
static AZStd::string WhiteBoxPathAtProjectRoot(const AZStd::string_view name, const AZStd::string_view extension)
{
AZ::IO::Path whiteBoxPath;
if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
{
settingsRegistry->Get(whiteBoxPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath);
}
whiteBoxPath /= AZ::IO::FixedMaxPathString::format("%.*s.%.*s", AZ_STRING_ARG(name), AZ_STRING_ARG(extension));
return whiteBoxPath.Native();
}
void EditorWhiteBoxComponent::ExportToFile()
{
const AZStd::string initialAbsolutePathToExport =
WhiteBoxPathAtProjectRoot(GetEntity()->GetName(), ObjExtension);
const QString fileFilter = AZStd::string::format("*.%s", ObjExtension).c_str();
const QString absoluteSaveFilePath = AzQtComponents::FileDialog::GetSaveFileName(
nullptr, "Save As...", QString(initialAbsolutePathToExport.c_str()), fileFilter);
const auto absoluteSaveFilePathUtf8 = absoluteSaveFilePath.toUtf8();
const auto absoluteSaveFilePathCstr = absoluteSaveFilePathUtf8.constData();
if (WhiteBox::Api::SaveToObj(*GetWhiteBoxMesh(), absoluteSaveFilePathCstr))
{
AZ_Printf("EditorWhiteBoxComponent", "Exported white box mesh to: %s", absoluteSaveFilePathCstr);
RequestEditSourceControl(absoluteSaveFilePathCstr);
}
else
{
AZ_Warning(
"EditorWhiteBoxComponent", false, "Failed to export white box mesh to: %s", absoluteSaveFilePathCstr);
}
}
AZStd::optional<WhiteBoxSaveResult> TrySaveAs(
const AZStd::string_view entityName,
const AZStd::function<AZStd::string(const AZStd::string&)>& absoluteSavePathFn,
const AZStd::function<AZStd::optional<AZStd::string>(const AZStd::string&)>& relativePathFn,
const AZStd::function<int()>& saveDecisionFn)
{
const AZStd::string initialAbsolutePathToSave =
WhiteBoxPathAtProjectRoot(entityName, Pipeline::WhiteBoxMeshAssetHandler::AssetFileExtension);
const QString absoluteSaveFilePath = QString(absoluteSavePathFn(initialAbsolutePathToSave).c_str());
// user pressed cancel
if (absoluteSaveFilePath.isEmpty())
{
return AZStd::nullopt;
}
const auto absoluteSaveFilePathUtf8 = absoluteSaveFilePath.toUtf8();
const auto absoluteSaveFilePathCstr = absoluteSaveFilePathUtf8.constData();
const AZStd::optional<AZStd::string> relativePath =
relativePathFn(AZStd::string(absoluteSaveFilePathCstr, absoluteSaveFilePathUtf8.length()));
if (!relativePath.has_value())
{
int saveDecision = saveDecisionFn();
// save the file but do not attempt to create an asset
if (saveDecision == QMessageBox::Save)
{
return WhiteBoxSaveResult{AZStd::nullopt, AZStd::string(absoluteSaveFilePathCstr)};
}
// the user decided not to save the asset outside the project folder after the prompt
return AZStd::nullopt;
}
return WhiteBoxSaveResult{relativePath, AZStd::string(absoluteSaveFilePathCstr)};
}
void EditorWhiteBoxComponent::SaveAsAsset()
{
// let the user select final location of the saved asset
const auto absoluteSavePathFn = [](const AZStd::string& initialAbsolutePath)
{
const QString fileFilter =
AZStd::string::format("*.%s", Pipeline::WhiteBoxMeshAssetHandler::AssetFileExtension).c_str();
const QString absolutePath = AzQtComponents::FileDialog::GetSaveFileName(
nullptr, "Save As Asset...", QString(initialAbsolutePath.c_str()), fileFilter);
return AZStd::string(absolutePath.toUtf8());
};
// ask the asset system to try and convert the absolutePath to a cache relative path
const auto relativePathFn = [](const AZStd::string& absolutePath) -> AZStd::optional<AZStd::string>
{
AZStd::string relativePath;
bool foundRelativePath = false;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
foundRelativePath,
&AzToolsFramework::AssetSystem::AssetSystemRequest::GetRelativeProductPathFromFullSourceOrProductPath,
absolutePath, relativePath);
if (foundRelativePath)
{
return relativePath;
}
return AZStd::nullopt;
};
// present the user with the option of accepting saving outside the project folder or allow them to cancel the
// operation
const auto saveDecisionFn = []()
{
return QMessageBox::warning(
AzToolsFramework::GetActiveWindow(), "Warning",
"Saving a White Box Mesh Asset (.wbm) outside of the project root will not create an Asset for the "
"Component to use. The file will be saved but will not be processed. For live updates to happen the "
"asset must be saved somewhere in the current project folder. Would you like to continue?",
(QMessageBox::Save | QMessageBox::Cancel), QMessageBox::Cancel);
};
const AZStd::optional<WhiteBoxSaveResult> saveResult =
TrySaveAs(GetEntity()->GetName(), absoluteSavePathFn, relativePathFn, saveDecisionFn);
// user pressed cancel
if (!saveResult.has_value())
{
return;
}
const char* const absoluteSaveFilePath = saveResult.value().m_absoluteFilePath.c_str();
if (saveResult.value().m_relativeAssetPath.has_value())
{
const auto& relativeAssetPath = saveResult.value().m_relativeAssetPath.value();
// notify undo system the entity has been changed (m_meshAsset)
AzToolsFramework::ScopedUndoBatch undoBatch(AssetSavedUndoRedoDesc);
// if there was a previous asset selected, it has to be cloned to a new one
// otherwise the internal mesh can simply be moved into the new asset
m_editorMeshAsset->TakeOwnershipOfWhiteBoxMesh(
relativeAssetPath,
m_editorMeshAsset->InUse() ? Api::CloneMesh(*GetWhiteBoxMesh())
: AZStd::exchange(m_whiteBox, Api::CreateWhiteBoxMesh()));
// change default shape to asset
m_defaultShape = DefaultShapeType::Asset;
// ensure this change gets tracked
undoBatch.MarkEntityDirty(GetEntityId());
RefreshProperties();
m_editorMeshAsset->Save(absoluteSaveFilePath);
}
else
{
// save the asset to disk outside the project folder
if (Api::SaveToWbm(*GetWhiteBoxMesh(), absoluteSaveFilePath))
{
RequestEditSourceControl(absoluteSaveFilePath);
}
}
}
template<typename TransformFn>
AZ::Aabb CalculateAabb(const WhiteBoxMesh& whiteBox, TransformFn&& transformFn)
{
const auto vertexHandles = Api::MeshVertexHandles(whiteBox);
return AZStd::accumulate(
AZStd::cbegin(vertexHandles), AZStd::cend(vertexHandles), AZ::Aabb::CreateNull(), transformFn);
}
AZ::Aabb EditorWhiteBoxComponent::GetEditorSelectionBoundsViewport(
[[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo)
{
return GetWorldBounds();
}
AZ::Aabb EditorWhiteBoxComponent::GetWorldBounds()
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
if (!m_worldAabb.has_value())
{
m_worldAabb = GetLocalBounds();
m_worldAabb->ApplyTransform(m_worldFromLocal);
}
return m_worldAabb.value();
}
AZ::Aabb EditorWhiteBoxComponent::GetLocalBounds()
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
if (!m_localAabb.has_value())
{
auto& whiteBoxMesh = *GetWhiteBoxMesh();
m_localAabb = CalculateAabb(
whiteBoxMesh,
[&whiteBox = whiteBoxMesh](AZ::Aabb aabb, const Api::VertexHandle vertexHandle)
{
aabb.AddPoint(Api::VertexPosition(whiteBox, vertexHandle));
return aabb;
});
}
return m_localAabb.value();
}
bool EditorWhiteBoxComponent::EditorSelectionIntersectRayViewport(
[[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo, const AZ::Vector3& src, const AZ::Vector3& dir,
float& distance)
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
if (!m_faces.has_value())
{
m_faces = Api::MeshFaces(*GetWhiteBoxMesh());
}
// must have at least one triangle
if (m_faces->empty())
{
return false;
}
// transform ray into local space
const AZ::Transform worldFromLocalUniform = AzToolsFramework::TransformUniformScale(m_worldFromLocal);
const AZ::Transform localFromWorldUniform = worldFromLocalUniform.GetInverse();
// setup beginning/end of segment
const float rayLength = 1000.0f;
const AZ::Vector3 localRayOrigin = localFromWorldUniform.TransformPoint(src);
const AZ::Vector3 localRayDirection = localFromWorldUniform.TransformVector(dir);
const AZ::Vector3 localRayEnd = localRayOrigin + localRayDirection * rayLength;
bool intersection = false;
for (const auto& face : m_faces.value())
{
float t;
AZ::Vector3 normal;
if (AZ::Intersect::IntersectSegmentTriangle(
localRayOrigin, localRayEnd, face[0], face[1], face[2], normal, t))
{
intersection = true;
// find closest intersection
const float dist = t * rayLength;
if (dist < distance)
{
distance = dist;
}
}
}
return intersection;
}
void EditorWhiteBoxComponent::OnEntityVisibilityChanged(const bool visibility)
{
if (visibility)
{
ShowRenderMesh();
}
else
{
HideRenderMesh();
}
}
void EditorWhiteBoxComponent::ShowRenderMesh()
{
// if we wish to display the render mesh, set a null render mesh indicating a mesh can exist
// note: if the optional remains empty, no render mesh will be created
m_renderMesh.emplace(AZStd::make_unique<WhiteBoxNullRenderMesh>());
RebuildRenderMesh();
}
void EditorWhiteBoxComponent::HideRenderMesh()
{
// clear the optional
m_renderMesh.reset();
}
bool EditorWhiteBoxComponent::AssetInUse() const
{
return m_editorMeshAsset->InUse();
}
bool EditorWhiteBoxComponent::HasRenderMesh() const
{
// if the optional has a value we know a render mesh exists
// note: This implicitly implies that the Entity is visible
return m_renderMesh.has_value();
}
void EditorWhiteBoxComponent::OverrideEditorWhiteBoxMeshAsset(EditorWhiteBoxMeshAsset* editorMeshAsset)
{
// ensure we do not leak resources
delete m_editorMeshAsset;
m_editorMeshAsset = editorMeshAsset;
}
static bool DebugDrawingEnabled()
{
return cl_whiteBoxDebugVertexHandles || cl_whiteBoxDebugNormals || cl_whiteBoxDebugHalfedgeHandles ||
cl_whiteBoxDebugEdgeHandles || cl_whiteBoxDebugFaceHandles || cl_whiteBoxDebugAabb;
}
static void WhiteBoxDebugRendering(
const WhiteBoxMesh& whiteBoxMesh, const AZ::Transform& worldFromLocal,
AzFramework::DebugDisplayRequests& debugDisplay, const AZ::Aabb& editorBounds)
{
const AZ::Quaternion worldOrientationFromLocal = worldFromLocal.GetRotation();
debugDisplay.DepthTestOn();
for (const auto& faceHandle : Api::MeshFaceHandles(whiteBoxMesh))
{
const auto faceHalfedgeHandles = Api::FaceHalfedgeHandles(whiteBoxMesh, faceHandle);
const AZ::Vector3 localFaceCenter =
AZStd::accumulate(
faceHalfedgeHandles.cbegin(), faceHalfedgeHandles.cend(), AZ::Vector3::CreateZero(),
[&whiteBoxMesh](AZ::Vector3 start, const Api::HalfedgeHandle halfedgeHandle)
{
return start +
Api::VertexPosition(
whiteBoxMesh, Api::HalfedgeVertexHandleAtTip(whiteBoxMesh, halfedgeHandle));
}) /
3.0f;
for (const auto& halfedgeHandle : faceHalfedgeHandles)
{
const Api::VertexHandle vertexHandleAtTip =
Api::HalfedgeVertexHandleAtTip(whiteBoxMesh, halfedgeHandle);
const Api::VertexHandle vertexHandleAtTail =
Api::HalfedgeVertexHandleAtTail(whiteBoxMesh, halfedgeHandle);
const AZ::Vector3 localTailPoint = Api::VertexPosition(whiteBoxMesh, vertexHandleAtTail);
const AZ::Vector3 localTipPoint = Api::VertexPosition(whiteBoxMesh, vertexHandleAtTip);
const AZ::Vector3 localFaceNormal = Api::FaceNormal(whiteBoxMesh, faceHandle);
const AZ::Vector3 localHalfedgeCenter = (localTailPoint + localTipPoint) * 0.5f;
// offset halfedge slightly based on the face it is associated with
const AZ::Vector3 localHalfedgePositionWithOffset =
localHalfedgeCenter + ((localFaceCenter - localHalfedgeCenter).GetNormalized() * 0.1f);
const AZ::Vector3 worldVertexPosition = worldFromLocal.TransformPoint(localTipPoint);
const AZ::Vector3 worldHalfedgePosition =
worldFromLocal.TransformPoint(localHalfedgePositionWithOffset);
const AZ::Vector3 worldNormal =
(worldOrientationFromLocal.TransformVector(localFaceNormal)).GetNormalized();
if (cl_whiteBoxDebugVertexHandles)
{
debugDisplay.SetColor(AZ::Colors::Cyan);
const AZStd::string vertex = AZStd::string::format("%d", vertexHandleAtTip.Index());
debugDisplay.DrawTextLabel(worldVertexPosition, 3.0f, vertex.c_str(), true, 0, 1);
}
if (cl_whiteBoxDebugHalfedgeHandles)
{
debugDisplay.SetColor(AZ::Colors::LawnGreen);
const AZStd::string halfedge = AZStd::string::format("%d", halfedgeHandle.Index());
debugDisplay.DrawTextLabel(worldHalfedgePosition, 2.0f, halfedge.c_str(), true);
}
if (cl_whiteBoxDebugNormals)
{
debugDisplay.SetColor(AZ::Colors::White);
debugDisplay.DrawBall(worldVertexPosition, 0.025f);
debugDisplay.DrawLine(worldVertexPosition, worldVertexPosition + worldNormal * 0.4f);
}
}
if (cl_whiteBoxDebugFaceHandles)
{
debugDisplay.SetColor(AZ::Colors::White);
const AZ::Vector3 worldFacePosition = worldFromLocal.TransformPoint(localFaceCenter);
const AZStd::string face = AZStd::string::format("%d", faceHandle.Index());
debugDisplay.DrawTextLabel(worldFacePosition, 2.0f, face.c_str(), true);
}
}
if (cl_whiteBoxDebugEdgeHandles)
{
for (const auto& edgeHandle : Api::MeshEdgeHandles(whiteBoxMesh))
{
const AZ::Vector3 localEdgeMidpoint = Api::EdgeMidpoint(whiteBoxMesh, edgeHandle);
const AZ::Vector3 worldEdgeMidpoint = worldFromLocal.TransformPoint(localEdgeMidpoint);
debugDisplay.SetColor(AZ::Colors::CornflowerBlue);
const AZStd::string edge = AZStd::string::format("%d", edgeHandle.Index());
debugDisplay.DrawTextLabel(worldEdgeMidpoint, 2.0f, edge.c_str(), true);
}
}
if (cl_whiteBoxDebugAabb)
{
debugDisplay.SetColor(AZ::Colors::Blue);
debugDisplay.DrawWireBox(editorBounds.GetMin(), editorBounds.GetMax());
}
}
void EditorWhiteBoxComponent::DisplayEntityViewport(
[[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay)
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
if (DebugDrawingEnabled())
{
WhiteBoxDebugRendering(
*GetWhiteBoxMesh(), m_worldFromLocal, debugDisplay,
GetEditorSelectionBoundsViewport(AzFramework::ViewportInfo{}));
}
}
} // namespace WhiteBox