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.
918 lines
38 KiB
C++
918 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/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/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,
|
|
"@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<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"));
|
|
}
|
|
|
|
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)
|
|
{
|
|
const char* projectFolder = nullptr;
|
|
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
|
|
projectFolder, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetAbsoluteDevGameFolderPath);
|
|
|
|
return AZStd::string::format(
|
|
"%s\\%.*s_whitebox.%.*s", projectFolder, aznumeric_cast<int>(name.size()), name.data(),
|
|
aznumeric_cast<int>(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<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())
|
|
{
|
|
distance = std::numeric_limits<float>::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<float>::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<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
|