/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // 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>& m_renderMesh) { return azrtti_cast((*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(GetEntityId())); const AZStd::string componentIdStr = AZStd::string::format("%llu", GetId()); const AZStd::string shapeTypeStr = AZStd::string::format("%d", aznumeric_cast(m_defaultShape)); const AZStd::vector 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, "@devroot@/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 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(context, "EditorMeshAsset"); if (editorMeshAssetIndex != -1) { // insert the existing WhiteBoxMeshAsset into the new EditorWhiteBoxMeshAsset classElement.GetSubElement(editorMeshAssetIndex) .AddElementWithData>(context, "MeshAsset", meshAsset); } else { return false; } } return true; } void EditorWhiteBoxComponent::Reflect(AZ::ReflectContext* context) { EditorWhiteBoxMeshAsset::Reflect(context); if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() ->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("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")); } 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( 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()) { // 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::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) { const char* projectFolder = nullptr; AzToolsFramework::AssetSystemRequestBus::BroadcastResult( projectFolder, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetAbsoluteDevGameFolderPath); return AZStd::string::format( "%s\\%.*s_whitebox.%.*s", projectFolder, aznumeric_cast(name.size()), name.data(), aznumeric_cast(extension.size()), extension.data()); } 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 TrySaveAs( const AZStd::string_view entityName, const AZStd::function& absoluteSavePathFn, const AZStd::function(const AZStd::string&)>& relativePathFn, const AZStd::function& 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 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 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 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 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()) { distance = std::numeric_limits::max(); 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; distance = std::numeric_limits::max(); 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()); 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