Terrain Physics Heightfield support
* New Heightfield Components Signed-off-by: John Jones-Steele <jjjoness@amazon.com> * Misc PR fixes * Fixed linux build failure from bad #include * Renamed "Terrain Physics Collider" to "Terrain Physics Heightfield Collider" per physics team feedback * Fixed 1/5 -> 1/4 typo in a comment * Added missing member copies in HeightfieldShapeConfiguration Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Addressed PR feedback Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Changes from review Signed-off-by: John Jones-Steele <jjjoness@amazon.com> * Remove tabs accidently added Signed-off-by: John Jones-Steele <jjjoness@amazon.com> * Fixed overly complicated scaling math. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Added comments to make it more obvious what's happening on CreateEnd / DestroyBegin. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Moved Heightfield CreatePxGeometryFromConfig into its own function Signed-off-by: John Jones-Steele <jjjoness@amazon.com> Co-authored-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>monroegm-disable-blank-issue-2
parent
775dda8ed2
commit
3d67be162c
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Math/Vector3.h>
|
||||
#include <AzCore/EBus/EBus.h>
|
||||
#include <AzCore/Component/ComponentBus.h>
|
||||
#include <AzCore/Math/Aabb.h>
|
||||
#include <AzFramework/Physics/Material.h>
|
||||
|
||||
namespace Physics
|
||||
{
|
||||
//! The QuadMeshType specifies the property of the heightfield quad.
|
||||
enum class QuadMeshType : uint8_t
|
||||
{
|
||||
SubdivideUpperLeftToBottomRight, //!< Subdivide the quad, from upper left to bottom right |\|, into two triangles.
|
||||
SubdivideBottomLeftToUpperRight, //!< Subdivide the quad, from bottom left to upper right |/|, into two triangles.
|
||||
Hole //!< The quad should be treated as a hole in the heightfield.
|
||||
};
|
||||
|
||||
struct HeightMaterialPoint
|
||||
{
|
||||
float m_height{ 0.0f }; //!< Holds the height of this point in the heightfield relative to the heightfield entity location.
|
||||
QuadMeshType m_quadMeshType{ QuadMeshType::SubdivideUpperLeftToBottomRight }; //!< By default, create two triangles like this |\|, where this point is in the upper left corner.
|
||||
uint8_t m_materialIndex{ 0 }; //!< The surface material index for the upper left corner of this quad.
|
||||
uint16_t m_padding{ 0 }; //!< available for future use.
|
||||
};
|
||||
|
||||
//! An interface to provide heightfield values.
|
||||
class HeightfieldProviderRequests
|
||||
: public AZ::ComponentBus
|
||||
{
|
||||
public:
|
||||
//! Returns the distance between each height in the map.
|
||||
//! @return Vector containing Column Spacing, Rows Spacing.
|
||||
virtual AZ::Vector2 GetHeightfieldGridSpacing() const = 0;
|
||||
|
||||
//! Returns the height field gridsize.
|
||||
//! @param numColumns contains the size of the grid in the x direction.
|
||||
//! @param numRows contains the size of the grid in the y direction.
|
||||
virtual void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const = 0;
|
||||
|
||||
//! Returns the height field min and max height bounds.
|
||||
//! @param minHeightBounds contains the minimum height that the heightfield can contain.
|
||||
//! @param maxHeightBounds contains the maximum height that the heightfield can contain.
|
||||
virtual void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const = 0;
|
||||
|
||||
//! Returns the AABB of the heightfield.
|
||||
//! This is provided separately from the shape AABB because the heightfield might choose to modify the AABB bounds.
|
||||
//! @return AABB of the heightfield.
|
||||
virtual AZ::Aabb GetHeightfieldAabb() const = 0;
|
||||
|
||||
//! Returns the world transform for the heightfield.
|
||||
//! This is provided separately from the entity transform because the heightfield might want to clear out the rotation or scale.
|
||||
//! @return world transform that should be used with the heightfield data.
|
||||
virtual AZ::Transform GetHeightfieldTransform() const = 0;
|
||||
|
||||
//! Returns the list of materials used by the height field.
|
||||
//! @return returns a vector of all materials.
|
||||
virtual AZStd::vector<MaterialId> GetMaterialList() const = 0;
|
||||
|
||||
//! Returns the list of heights used by the height field.
|
||||
//! @return the rows*columns vector of the heights.
|
||||
virtual AZStd::vector<float> GetHeights() const = 0;
|
||||
|
||||
//! Returns the list of heights and materials used by the height field.
|
||||
//! @return the rows*columns vector of the heights and materials.
|
||||
virtual AZStd::vector<Physics::HeightMaterialPoint> GetHeightsAndMaterials() const = 0;
|
||||
};
|
||||
|
||||
using HeightfieldProviderRequestsBus = AZ::EBus<HeightfieldProviderRequests>;
|
||||
|
||||
//! Broadcasts notifications when heightfield data changes - heightfield providers implement HeightfieldRequests bus.
|
||||
class HeightfieldProviderNotifications
|
||||
: public AZ::ComponentBus
|
||||
{
|
||||
public:
|
||||
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
|
||||
|
||||
//! Called whenever the heightfield data changes.
|
||||
//! @param the AABB of the area of data that changed.
|
||||
virtual void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
~HeightfieldProviderNotifications() = default;
|
||||
};
|
||||
|
||||
using HeightfieldProviderNotificationBus = AZ::EBus<HeightfieldProviderNotifications>;
|
||||
} // namespace Physics
|
||||
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <AzFramework/Physics/HeightfieldProviderBus.h>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
class MockHeightfieldProviderNotificationBusListener
|
||||
: private Physics::HeightfieldProviderNotificationBus::Handler
|
||||
{
|
||||
public:
|
||||
MockHeightfieldProviderNotificationBusListener(AZ::EntityId entityid)
|
||||
{
|
||||
Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityid);
|
||||
}
|
||||
|
||||
~MockHeightfieldProviderNotificationBusListener()
|
||||
{
|
||||
Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
MOCK_METHOD1(OnHeightfieldDataChanged, void(const AZ::Aabb&));
|
||||
};
|
||||
} // namespace UnitTest
|
||||
@ -0,0 +1,11 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Mocks/MockHeightfieldProviderBus.h
|
||||
)
|
||||
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <AzCore/Component/ComponentApplication.h>
|
||||
#include <GradientSignal/Ebuses/GradientRequestBus.h>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
class MockGradientRequests
|
||||
: private GradientSignal::GradientRequestBus::Handler
|
||||
{
|
||||
public:
|
||||
MockGradientRequests(AZ::EntityId entityId)
|
||||
{
|
||||
GradientSignal::GradientRequestBus::Handler::BusConnect(entityId);
|
||||
}
|
||||
|
||||
~MockGradientRequests()
|
||||
{
|
||||
GradientSignal::GradientRequestBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
MOCK_CONST_METHOD1(GetValue, float(const GradientSignal::GradientSampleParams&));
|
||||
MOCK_CONST_METHOD1(IsEntityInHierarchy, bool(const AZ::EntityId&));
|
||||
};
|
||||
} // namespace UnitTest
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h
|
||||
)
|
||||
@ -0,0 +1,341 @@
|
||||
/*
|
||||
* 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 <AzCore/std/smart_ptr/make_shared.h>
|
||||
#include <Editor/ColliderComponentMode.h>
|
||||
#include <EditorHeightfieldColliderComponent.h>
|
||||
#include <AzFramework/Physics/Configuration/StaticRigidBodyConfiguration.h>
|
||||
#include <AzFramework/Physics/Shape.h>
|
||||
#include <Source/HeightfieldColliderComponent.h>
|
||||
#include <Source/Utils.h>
|
||||
#include <System/PhysXSystem.h>
|
||||
|
||||
namespace PhysX
|
||||
{
|
||||
void EditorHeightfieldColliderComponent::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<EditorHeightfieldColliderComponent, EditorComponentBase>()
|
||||
->Version(1)
|
||||
->Field("ColliderConfiguration", &EditorHeightfieldColliderComponent::m_colliderConfig)
|
||||
->Field("DebugDrawSettings", &EditorHeightfieldColliderComponent::m_colliderDebugDraw)
|
||||
->Field("ShapeConfig", &EditorHeightfieldColliderComponent::m_shapeConfig)
|
||||
;
|
||||
|
||||
if (auto editContext = serializeContext->GetEditContext())
|
||||
{
|
||||
editContext->Class<EditorHeightfieldColliderComponent>(
|
||||
"PhysX Heightfield Collider", "Creates geometry in the PhysX simulation based on an attached heightfield component")
|
||||
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
||||
->Attribute(AZ::Edit::Attributes::Category, "PhysX")
|
||||
->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/PhysXCollider.svg")
|
||||
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/PhysXCollider.svg")
|
||||
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game"))
|
||||
->Attribute(
|
||||
AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/physx/heightfield-collider/")
|
||||
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
|
||||
->DataElement(
|
||||
AZ::Edit::UIHandlers::Default, &EditorHeightfieldColliderComponent::m_colliderConfig, "Collider configuration",
|
||||
"Configuration of the collider")
|
||||
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
|
||||
->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorHeightfieldColliderComponent::OnConfigurationChanged)
|
||||
->DataElement(
|
||||
AZ::Edit::UIHandlers::Default, &EditorHeightfieldColliderComponent::m_colliderDebugDraw, "Debug draw settings",
|
||||
"Debug draw settings")
|
||||
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
|
||||
{
|
||||
provided.push_back(AZ_CRC_CE("PhysicsWorldBodyService"));
|
||||
provided.push_back(AZ_CRC_CE("PhysXColliderService"));
|
||||
provided.push_back(AZ_CRC_CE("PhysXHeightfieldColliderService"));
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
|
||||
{
|
||||
required.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService"));
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
|
||||
{
|
||||
incompatible.push_back(AZ_CRC_CE("PhysXColliderService"));
|
||||
incompatible.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService"));
|
||||
incompatible.push_back(AZ_CRC_CE("PhysXRigidBodyService"));
|
||||
}
|
||||
|
||||
EditorHeightfieldColliderComponent::EditorHeightfieldColliderComponent()
|
||||
: m_physXConfigChangedHandler(
|
||||
[]([[maybe_unused]] const AzPhysics::SystemConfiguration* config)
|
||||
{
|
||||
AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast(
|
||||
&AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh,
|
||||
AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues);
|
||||
})
|
||||
, m_onMaterialLibraryChangedEventHandler(
|
||||
[this](const AZ::Data::AssetId& defaultMaterialLibrary)
|
||||
{
|
||||
m_colliderConfig.m_materialSelection.OnMaterialLibraryChanged(defaultMaterialLibrary);
|
||||
Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged);
|
||||
|
||||
AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast(
|
||||
&AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh,
|
||||
AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues);
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
EditorHeightfieldColliderComponent ::~EditorHeightfieldColliderComponent()
|
||||
{
|
||||
ClearHeightfield();
|
||||
}
|
||||
|
||||
// AZ::Component
|
||||
void EditorHeightfieldColliderComponent::Activate()
|
||||
{
|
||||
AzToolsFramework::Components::EditorComponentBase::Activate();
|
||||
|
||||
// Heightfields don't support the following:
|
||||
// - Offset: There shouldn't be a need to offset the data, since the heightfield provider is giving a physics representation
|
||||
// - IsTrigger: PhysX heightfields don't support acting as triggers
|
||||
// - MaterialSelection: The heightfield provider provides per-vertex material selection
|
||||
m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::Offset, false);
|
||||
m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::IsTrigger, false);
|
||||
m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::MaterialSelection, false);
|
||||
|
||||
m_sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get();
|
||||
if (m_sceneInterface)
|
||||
{
|
||||
m_attachedSceneHandle = m_sceneInterface->GetSceneHandle(AzPhysics::EditorPhysicsSceneName);
|
||||
}
|
||||
|
||||
const AZ::EntityId entityId = GetEntityId();
|
||||
|
||||
AzToolsFramework::EntitySelectionEvents::Bus::Handler::BusConnect(entityId);
|
||||
|
||||
// Debug drawing
|
||||
m_colliderDebugDraw.Connect(entityId);
|
||||
m_colliderDebugDraw.SetDisplayCallback(this);
|
||||
|
||||
Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityId);
|
||||
PhysX::ColliderShapeRequestBus::Handler::BusConnect(entityId);
|
||||
AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(entityId);
|
||||
|
||||
RefreshHeightfield();
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::Deactivate()
|
||||
{
|
||||
AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect();
|
||||
PhysX::ColliderShapeRequestBus::Handler::BusDisconnect();
|
||||
Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect();
|
||||
|
||||
m_colliderDebugDraw.Disconnect();
|
||||
AzToolsFramework::EntitySelectionEvents::Bus::Handler::BusDisconnect();
|
||||
AzToolsFramework::Components::EditorComponentBase::Deactivate();
|
||||
|
||||
ClearHeightfield();
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::BuildGameEntity(AZ::Entity* gameEntity)
|
||||
{
|
||||
auto* heightfieldColliderComponent = gameEntity->CreateComponent<HeightfieldColliderComponent>();
|
||||
heightfieldColliderComponent->SetShapeConfiguration(
|
||||
{ AZStd::make_shared<Physics::ColliderConfiguration>(m_colliderConfig), m_shapeConfig });
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion)
|
||||
{
|
||||
RefreshHeightfield();
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::ClearHeightfield()
|
||||
{
|
||||
// There are two references to the heightfield data, we need to clear both to make the heightfield clear out and deallocate:
|
||||
// - The simulated body has a pointer to the shape, which has a GeometryHolder, which has the Heightfield inside it
|
||||
// - The shape config is also holding onto a pointer to the Heightfield
|
||||
|
||||
// We remove the simulated body first, since we don't want the heightfield to exist any more.
|
||||
if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle)
|
||||
{
|
||||
m_sceneInterface->RemoveSimulatedBody(m_attachedSceneHandle, m_staticRigidBodyHandle);
|
||||
}
|
||||
|
||||
// Now we can safely clear out the cached heightfield pointer.
|
||||
m_shapeConfig->SetCachedNativeHeightfield(nullptr);
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::InitStaticRigidBody()
|
||||
{
|
||||
// Get the transform from the HeightfieldProvider. Because rotation and scale can indirectly affect how the heightfield itself
|
||||
// is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform
|
||||
// back to us than the one that's directly on that entity.
|
||||
AZ::Transform transform = AZ::Transform::CreateIdentity();
|
||||
Physics::HeightfieldProviderRequestsBus::EventResult(
|
||||
transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform);
|
||||
|
||||
AzPhysics::StaticRigidBodyConfiguration configuration;
|
||||
configuration.m_orientation = transform.GetRotation();
|
||||
configuration.m_position = transform.GetTranslation();
|
||||
configuration.m_entityId = GetEntityId();
|
||||
configuration.m_debugName = GetEntity()->GetName();
|
||||
|
||||
AzPhysics::ShapeColliderPairList colliderShapePairs;
|
||||
colliderShapePairs.emplace_back(AZStd::make_shared<Physics::ColliderConfiguration>(m_colliderConfig), m_shapeConfig);
|
||||
configuration.m_colliderAndShapeData = colliderShapePairs;
|
||||
|
||||
if (m_sceneInterface)
|
||||
{
|
||||
m_staticRigidBodyHandle = m_sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &configuration);
|
||||
}
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::InitHeightfieldShapeConfiguration()
|
||||
{
|
||||
Physics::HeightfieldShapeConfiguration& configuration = static_cast<Physics::HeightfieldShapeConfiguration&>(*m_shapeConfig);
|
||||
|
||||
Utils::InitHeightfieldShapeConfiguration(GetEntityId(), configuration);
|
||||
}
|
||||
|
||||
void EditorHeightfieldColliderComponent::RefreshHeightfield()
|
||||
{
|
||||
ClearHeightfield();
|
||||
InitHeightfieldShapeConfiguration();
|
||||
InitStaticRigidBody();
|
||||
Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged);
|
||||
}
|
||||
|
||||
AZ::u32 EditorHeightfieldColliderComponent::OnConfigurationChanged()
|
||||
{
|
||||
RefreshHeightfield();
|
||||
return AZ::Edit::PropertyRefreshLevels::None;
|
||||
}
|
||||
|
||||
// AzToolsFramework::EntitySelectionEvents
|
||||
void EditorHeightfieldColliderComponent::OnSelected()
|
||||
{
|
||||
if (auto* physXSystem = GetPhysXSystem())
|
||||
{
|
||||
if (!m_physXConfigChangedHandler.IsConnected())
|
||||
{
|
||||
physXSystem->RegisterSystemConfigurationChangedEvent(m_physXConfigChangedHandler);
|
||||
}
|
||||
if (!m_onMaterialLibraryChangedEventHandler.IsConnected())
|
||||
{
|
||||
physXSystem->RegisterOnMaterialLibraryChangedEventHandler(m_onMaterialLibraryChangedEventHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AzToolsFramework::EntitySelectionEvents
|
||||
void EditorHeightfieldColliderComponent::OnDeselected()
|
||||
{
|
||||
m_onMaterialLibraryChangedEventHandler.Disconnect();
|
||||
m_physXConfigChangedHandler.Disconnect();
|
||||
}
|
||||
|
||||
// DisplayCallback
|
||||
void EditorHeightfieldColliderComponent::Display(AzFramework::DebugDisplayRequests& debugDisplay) const
|
||||
{
|
||||
const auto& heightfieldConfig = static_cast<const Physics::HeightfieldShapeConfiguration&>(*m_shapeConfig);
|
||||
m_colliderDebugDraw.DrawHeightfield(debugDisplay, m_colliderConfig, heightfieldConfig);
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
void EditorHeightfieldColliderComponent::EnablePhysics()
|
||||
{
|
||||
if (!IsPhysicsEnabled() && m_sceneInterface)
|
||||
{
|
||||
m_sceneInterface->EnableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
void EditorHeightfieldColliderComponent::DisablePhysics()
|
||||
{
|
||||
if (m_sceneInterface)
|
||||
{
|
||||
m_sceneInterface->DisableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
bool EditorHeightfieldColliderComponent::IsPhysicsEnabled() const
|
||||
{
|
||||
if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle)
|
||||
{
|
||||
if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle))
|
||||
{
|
||||
return body->m_simulating;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
AzPhysics::SimulatedBodyHandle EditorHeightfieldColliderComponent::GetSimulatedBodyHandle() const
|
||||
{
|
||||
return m_staticRigidBodyHandle;
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
AzPhysics::SimulatedBody* EditorHeightfieldColliderComponent::GetSimulatedBody()
|
||||
{
|
||||
if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle)
|
||||
{
|
||||
if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle))
|
||||
{
|
||||
return body;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
AzPhysics::SceneQueryHit EditorHeightfieldColliderComponent::RayCast(const AzPhysics::RayCastRequest& request)
|
||||
{
|
||||
if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle)
|
||||
{
|
||||
if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle))
|
||||
{
|
||||
return body->RayCast(request);
|
||||
}
|
||||
}
|
||||
return AzPhysics::SceneQueryHit();
|
||||
}
|
||||
|
||||
// ColliderShapeRequestBus
|
||||
AZ::Aabb EditorHeightfieldColliderComponent::GetColliderShapeAabb()
|
||||
{
|
||||
// Get the Collider AABB directly from the heightfield provider.
|
||||
AZ::Aabb colliderAabb = AZ::Aabb::CreateNull();
|
||||
Physics::HeightfieldProviderRequestsBus::EventResult(
|
||||
colliderAabb, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldAabb);
|
||||
|
||||
return colliderAabb;
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
AZ::Aabb EditorHeightfieldColliderComponent::GetAabb() const
|
||||
{
|
||||
// On the SimulatedBodyComponentRequestsBus, get the AABB from the simulated body instead of the collider.
|
||||
if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle)
|
||||
{
|
||||
if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle))
|
||||
{
|
||||
return body->GetAabb();
|
||||
}
|
||||
}
|
||||
return AZ::Aabb::CreateNull();
|
||||
}
|
||||
|
||||
} // namespace PhysX
|
||||
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
|
||||
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
|
||||
#include <Editor/DebugDraw.h>
|
||||
|
||||
#include <AzFramework/Physics/Components/SimulatedBodyComponentBus.h>
|
||||
#include <AzFramework/Physics/HeightfieldProviderBus.h>
|
||||
#include <AzFramework/Physics/PhysicsScene.h>
|
||||
#include <AzFramework/Physics/Shape.h>
|
||||
|
||||
#include <PhysX/ColliderShapeBus.h>
|
||||
|
||||
namespace PhysX
|
||||
{
|
||||
//! Editor PhysX Heightfield Collider Component.
|
||||
class EditorHeightfieldColliderComponent
|
||||
: public AzToolsFramework::Components::EditorComponentBase
|
||||
, protected AzToolsFramework::EntitySelectionEvents::Bus::Handler
|
||||
, protected DebugDraw::DisplayCallback
|
||||
, protected AzPhysics::SimulatedBodyComponentRequestsBus::Handler
|
||||
, protected PhysX::ColliderShapeRequestBus::Handler
|
||||
, protected Physics::HeightfieldProviderNotificationBus::Handler
|
||||
{
|
||||
public:
|
||||
AZ_EDITOR_COMPONENT(
|
||||
EditorHeightfieldColliderComponent,
|
||||
"{C388C3DB-8D2E-4D26-96D3-198EDC799B77}",
|
||||
AzToolsFramework::Components::EditorComponentBase);
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
|
||||
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
|
||||
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
|
||||
|
||||
EditorHeightfieldColliderComponent();
|
||||
~EditorHeightfieldColliderComponent();
|
||||
|
||||
// AZ::Component
|
||||
void Activate() override;
|
||||
void Deactivate() override;
|
||||
|
||||
// EditorComponentBase
|
||||
void BuildGameEntity(AZ::Entity* gameEntity) override;
|
||||
|
||||
protected:
|
||||
|
||||
// AzToolsFramework::EntitySelectionEvents
|
||||
void OnSelected() override;
|
||||
void OnDeselected() override;
|
||||
|
||||
// DisplayCallback
|
||||
void Display(AzFramework::DebugDisplayRequests& debugDisplay) const;
|
||||
|
||||
// ColliderShapeRequestBus
|
||||
AZ::Aabb GetColliderShapeAabb() override;
|
||||
bool IsTrigger() override
|
||||
{
|
||||
// PhysX Heightfields don't support triggers.
|
||||
return false;
|
||||
}
|
||||
|
||||
// AzPhysics::SimulatedBodyComponentRequestsBus::Handler overrides ...
|
||||
void EnablePhysics() override;
|
||||
void DisablePhysics() override;
|
||||
bool IsPhysicsEnabled() const override;
|
||||
AZ::Aabb GetAabb() const override;
|
||||
AzPhysics::SimulatedBody* GetSimulatedBody() override;
|
||||
AzPhysics::SimulatedBodyHandle GetSimulatedBodyHandle() const override;
|
||||
AzPhysics::SceneQueryHit RayCast(const AzPhysics::RayCastRequest& request) override;
|
||||
|
||||
// Physics::HeightfieldProviderNotificationBus
|
||||
void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) override;
|
||||
|
||||
private:
|
||||
AZ::u32 OnConfigurationChanged();
|
||||
|
||||
void ClearHeightfield();
|
||||
void InitHeightfieldShapeConfiguration();
|
||||
void InitStaticRigidBody();
|
||||
void RefreshHeightfield();
|
||||
|
||||
DebugDraw::Collider m_colliderDebugDraw; //!< Handles drawing the collider
|
||||
AzPhysics::SceneInterface* m_sceneInterface{ nullptr };
|
||||
|
||||
AzPhysics::SystemEvents::OnConfigurationChangedEvent::Handler m_physXConfigChangedHandler;
|
||||
AzPhysics::SystemEvents::OnMaterialLibraryChangedEvent::Handler m_onMaterialLibraryChangedEventHandler;
|
||||
|
||||
Physics::ColliderConfiguration m_colliderConfig; //!< Stores collision layers, whether the collider is a trigger, etc.
|
||||
AZStd::shared_ptr<Physics::HeightfieldShapeConfiguration> m_shapeConfig{ new Physics::HeightfieldShapeConfiguration() };
|
||||
|
||||
AzPhysics::SimulatedBodyHandle m_staticRigidBodyHandle =
|
||||
AzPhysics::InvalidSimulatedBodyHandle; //!< Handle to the body in the editor physics scene if there is no rigid body component.
|
||||
AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle;
|
||||
};
|
||||
|
||||
} // namespace PhysX
|
||||
@ -0,0 +1,365 @@
|
||||
/*
|
||||
* 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 <AzCore/Component/Entity.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
#include <AzFramework/Physics/Collision/CollisionGroups.h>
|
||||
#include <AzFramework/Physics/Collision/CollisionLayers.h>
|
||||
#include <AzFramework/Physics/Common/PhysicsSimulatedBody.h>
|
||||
#include <AzFramework/Physics/Configuration/StaticRigidBodyConfiguration.h>
|
||||
#include <AzFramework/Physics/Utils.h>
|
||||
|
||||
#include <Source/HeightfieldColliderComponent.h>
|
||||
#include <Source/RigidBodyStatic.h>
|
||||
#include <Source/SystemComponent.h>
|
||||
#include <Source/Utils.h>
|
||||
|
||||
#include <PhysX/MathConversion.h>
|
||||
#include <PhysX/PhysXLocks.h>
|
||||
#include <Scene/PhysXScene.h>
|
||||
|
||||
namespace PhysX
|
||||
{
|
||||
void HeightfieldColliderComponent::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<HeightfieldColliderComponent, AZ::Component>()
|
||||
->Version(1)
|
||||
->Field("ShapeConfig", &HeightfieldColliderComponent::m_shapeConfig)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
|
||||
{
|
||||
provided.push_back(AZ_CRC_CE("PhysicsWorldBodyService"));
|
||||
provided.push_back(AZ_CRC_CE("PhysXColliderService"));
|
||||
provided.push_back(AZ_CRC_CE("PhysXHeightfieldColliderService"));
|
||||
provided.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService"));
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
|
||||
{
|
||||
required.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService"));
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
|
||||
{
|
||||
incompatible.push_back(AZ_CRC_CE("PhysXColliderService"));
|
||||
incompatible.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService"));
|
||||
incompatible.push_back(AZ_CRC_CE("PhysXRigidBodyService"));
|
||||
}
|
||||
|
||||
HeightfieldColliderComponent::~HeightfieldColliderComponent()
|
||||
{
|
||||
ClearHeightfield();
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::Activate()
|
||||
{
|
||||
const AZ::EntityId entityId = GetEntityId();
|
||||
|
||||
Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityId);
|
||||
ColliderComponentRequestBus::Handler::BusConnect(entityId);
|
||||
ColliderShapeRequestBus::Handler::BusConnect(entityId);
|
||||
Physics::CollisionFilteringRequestBus::Handler::BusConnect(entityId);
|
||||
AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(entityId);
|
||||
|
||||
RefreshHeightfield();
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::Deactivate()
|
||||
{
|
||||
AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect();
|
||||
Physics::CollisionFilteringRequestBus::Handler::BusDisconnect();
|
||||
ColliderShapeRequestBus::Handler::BusDisconnect();
|
||||
ColliderComponentRequestBus::Handler::BusDisconnect();
|
||||
Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect();
|
||||
|
||||
ClearHeightfield();
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion)
|
||||
{
|
||||
RefreshHeightfield();
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::ClearHeightfield()
|
||||
{
|
||||
// There are two references to the heightfield data, we need to clear both to make the heightfield clear out and deallocate:
|
||||
// - The simulated body has a pointer to the shape, which has a GeometryHolder, which has the Heightfield inside it
|
||||
// - The shape config is also holding onto a pointer to the Heightfield
|
||||
|
||||
// We remove the simulated body first, since we don't want the heightfield to exist any more.
|
||||
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get();
|
||||
sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle)
|
||||
{
|
||||
sceneInterface->RemoveSimulatedBody(m_attachedSceneHandle, m_staticRigidBodyHandle);
|
||||
}
|
||||
|
||||
// Now we can safely clear out the cached heightfield pointer.
|
||||
Physics::HeightfieldShapeConfiguration& configuration = static_cast<Physics::HeightfieldShapeConfiguration&>(*m_shapeConfig.second);
|
||||
configuration.SetCachedNativeHeightfield(nullptr);
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::InitStaticRigidBody()
|
||||
{
|
||||
// Get the transform from the HeightfieldProvider. Because rotation and scale can indirectly affect how the heightfield itself
|
||||
// is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform
|
||||
// back to us than the one that's directly on that entity.
|
||||
AZ::Transform transform = AZ::Transform::CreateIdentity();
|
||||
Physics::HeightfieldProviderRequestsBus::EventResult(
|
||||
transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform);
|
||||
|
||||
AzPhysics::StaticRigidBodyConfiguration configuration;
|
||||
configuration.m_orientation = transform.GetRotation();
|
||||
configuration.m_position = transform.GetTranslation();
|
||||
configuration.m_entityId = GetEntityId();
|
||||
configuration.m_debugName = GetEntity()->GetName();
|
||||
configuration.m_colliderAndShapeData = GetShapeConfigurations();
|
||||
|
||||
if (m_attachedSceneHandle == AzPhysics::InvalidSceneHandle)
|
||||
{
|
||||
Physics::DefaultWorldBus::BroadcastResult(m_attachedSceneHandle, &Physics::DefaultWorldRequests::GetDefaultSceneHandle);
|
||||
}
|
||||
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
|
||||
{
|
||||
m_staticRigidBodyHandle = sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &configuration);
|
||||
}
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::InitHeightfieldShapeConfiguration()
|
||||
{
|
||||
Physics::HeightfieldShapeConfiguration& configuration = static_cast<Physics::HeightfieldShapeConfiguration&>(*m_shapeConfig.second);
|
||||
|
||||
Utils::InitHeightfieldShapeConfiguration(GetEntityId(), configuration);
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::RefreshHeightfield()
|
||||
{
|
||||
ClearHeightfield();
|
||||
InitHeightfieldShapeConfiguration();
|
||||
InitStaticRigidBody();
|
||||
Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged);
|
||||
}
|
||||
|
||||
void HeightfieldColliderComponent::SetShapeConfiguration(const AzPhysics::ShapeColliderPair& shapeConfig)
|
||||
{
|
||||
if (GetEntity()->GetState() == AZ::Entity::State::Active)
|
||||
{
|
||||
AZ_Warning(
|
||||
"PhysX", false, "Trying to call SetShapeConfiguration for entity \"%s\" while entity is active.",
|
||||
GetEntity()->GetName().c_str());
|
||||
return;
|
||||
}
|
||||
m_shapeConfig = shapeConfig;
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
void HeightfieldColliderComponent::EnablePhysics()
|
||||
{
|
||||
if (IsPhysicsEnabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
|
||||
{
|
||||
sceneInterface->EnableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
void HeightfieldColliderComponent::DisablePhysics()
|
||||
{
|
||||
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
|
||||
{
|
||||
sceneInterface->DisableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
bool HeightfieldColliderComponent::IsPhysicsEnabled() const
|
||||
{
|
||||
if (m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle)
|
||||
{
|
||||
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get();
|
||||
sceneInterface != nullptr && sceneInterface->IsEnabled(m_attachedSceneHandle)) // check if the scene is enabled
|
||||
{
|
||||
if (AzPhysics::SimulatedBody* body =
|
||||
sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle))
|
||||
{
|
||||
return body->m_simulating;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
AzPhysics::SimulatedBodyHandle HeightfieldColliderComponent::GetSimulatedBodyHandle() const
|
||||
{
|
||||
return m_staticRigidBodyHandle;
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
AzPhysics::SimulatedBody* HeightfieldColliderComponent::GetSimulatedBody()
|
||||
{
|
||||
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
|
||||
{
|
||||
return sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
AzPhysics::SceneQueryHit HeightfieldColliderComponent::RayCast(const AzPhysics::RayCastRequest& request)
|
||||
{
|
||||
if (auto* body = azdynamic_cast<PhysX::StaticRigidBody*>(GetSimulatedBody()))
|
||||
{
|
||||
return body->RayCast(request);
|
||||
}
|
||||
return AzPhysics::SceneQueryHit();
|
||||
}
|
||||
|
||||
// ColliderComponentRequestBus
|
||||
AzPhysics::ShapeColliderPairList HeightfieldColliderComponent::GetShapeConfigurations()
|
||||
{
|
||||
AzPhysics::ShapeColliderPairList shapeConfigurationList({ m_shapeConfig });
|
||||
return shapeConfigurationList;
|
||||
}
|
||||
|
||||
AZStd::shared_ptr<Physics::Shape> HeightfieldColliderComponent::GetHeightfieldShape()
|
||||
{
|
||||
if (auto* body = azdynamic_cast<PhysX::StaticRigidBody*>(GetSimulatedBody()))
|
||||
{
|
||||
// Heightfields should only have one shape
|
||||
AZ_Assert(body->GetShapeCount() == 1, "Heightfield rigid body has the wrong number of shapes: %zu", body->GetShapeCount());
|
||||
return body->GetShape(0);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// ColliderComponentRequestBus
|
||||
AZStd::vector<AZStd::shared_ptr<Physics::Shape>> HeightfieldColliderComponent::GetShapes()
|
||||
{
|
||||
return { GetHeightfieldShape() };
|
||||
}
|
||||
|
||||
// PhysX::ColliderShapeBus
|
||||
AZ::Aabb HeightfieldColliderComponent::GetColliderShapeAabb()
|
||||
{
|
||||
// Get the Collider AABB directly from the heightfield provider.
|
||||
AZ::Aabb colliderAabb = AZ::Aabb::CreateNull();
|
||||
Physics::HeightfieldProviderRequestsBus::EventResult(
|
||||
colliderAabb, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldAabb);
|
||||
|
||||
return colliderAabb;
|
||||
}
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
AZ::Aabb HeightfieldColliderComponent::GetAabb() const
|
||||
{
|
||||
// On the SimulatedBodyComponentRequestsBus, get the AABB from the simulated body instead of the collider.
|
||||
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
|
||||
{
|
||||
if (AzPhysics::SimulatedBody* body = sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle))
|
||||
{
|
||||
return body->GetAabb();
|
||||
}
|
||||
}
|
||||
return AZ::Aabb::CreateNull();
|
||||
}
|
||||
|
||||
// CollisionFilteringRequestBus
|
||||
void HeightfieldColliderComponent::SetCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag)
|
||||
{
|
||||
if (auto heightfield = GetHeightfieldShape())
|
||||
{
|
||||
if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag))
|
||||
{
|
||||
bool success = false;
|
||||
AzPhysics::CollisionLayer layer;
|
||||
Physics::CollisionRequestBus::BroadcastResult(
|
||||
success, &Physics::CollisionRequests::TryGetCollisionLayerByName, layerName, layer);
|
||||
if (success)
|
||||
{
|
||||
heightfield->SetCollisionLayer(layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CollisionFilteringRequestBus
|
||||
AZStd::string HeightfieldColliderComponent::GetCollisionLayerName()
|
||||
{
|
||||
AZStd::string layerName;
|
||||
if (auto heightfield = GetHeightfieldShape())
|
||||
{
|
||||
Physics::CollisionRequestBus::BroadcastResult(
|
||||
layerName, &Physics::CollisionRequests::GetCollisionLayerName, heightfield->GetCollisionLayer());
|
||||
}
|
||||
return layerName;
|
||||
}
|
||||
|
||||
// CollisionFilteringRequestBus
|
||||
void HeightfieldColliderComponent::SetCollisionGroup(const AZStd::string& groupName, AZ::Crc32 colliderTag)
|
||||
{
|
||||
if (auto heightfield = GetHeightfieldShape())
|
||||
{
|
||||
if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag))
|
||||
{
|
||||
bool success = false;
|
||||
AzPhysics::CollisionGroup group;
|
||||
Physics::CollisionRequestBus::BroadcastResult(
|
||||
success, &Physics::CollisionRequests::TryGetCollisionGroupByName, groupName, group);
|
||||
if (success)
|
||||
{
|
||||
heightfield->SetCollisionGroup(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CollisionFilteringRequestBus
|
||||
AZStd::string HeightfieldColliderComponent::GetCollisionGroupName()
|
||||
{
|
||||
AZStd::string groupName;
|
||||
if (auto heightfield = GetHeightfieldShape())
|
||||
{
|
||||
Physics::CollisionRequestBus::BroadcastResult(
|
||||
groupName, &Physics::CollisionRequests::GetCollisionGroupName, heightfield->GetCollisionGroup());
|
||||
}
|
||||
|
||||
return groupName;
|
||||
}
|
||||
|
||||
// CollisionFilteringRequestBus
|
||||
void HeightfieldColliderComponent::ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag, bool enabled)
|
||||
{
|
||||
if (auto heightfield = GetHeightfieldShape())
|
||||
{
|
||||
if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag))
|
||||
{
|
||||
bool success = false;
|
||||
AzPhysics::CollisionLayer layer;
|
||||
Physics::CollisionRequestBus::BroadcastResult(
|
||||
success, &Physics::CollisionRequests::TryGetCollisionLayerByName, layerName, layer);
|
||||
if (success)
|
||||
{
|
||||
auto group = heightfield->GetCollisionGroup();
|
||||
group.SetLayer(layer, enabled);
|
||||
heightfield->SetCollisionGroup(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace PhysX
|
||||
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Component/Component.h>
|
||||
|
||||
#include <AzFramework/Physics/CollisionBus.h>
|
||||
#include <AzFramework/Physics/Components/SimulatedBodyComponentBus.h>
|
||||
#include <AzFramework/Physics/HeightfieldProviderBus.h>
|
||||
|
||||
#include <PhysX/ColliderComponentBus.h>
|
||||
#include <PhysX/ColliderShapeBus.h>
|
||||
|
||||
namespace AzPhysics
|
||||
{
|
||||
struct SimulatedBody;
|
||||
}
|
||||
|
||||
namespace PhysX
|
||||
{
|
||||
class StaticRigidBody;
|
||||
|
||||
//! Component that provides a Heightfield Collider and associated Static Rigid Body.
|
||||
//! The heightfield collider is a bit different from the other shape colliders in that it gets the heightfield data from a
|
||||
//! HeightfieldProvider, which can control position, rotation, size, and even change its data at runtime.
|
||||
//!
|
||||
//! Due to these differences, this component directly implements both the collider and static rigid body services instead of
|
||||
//! using BaseColliderComponent and StaticRigidBodyComponent.
|
||||
class HeightfieldColliderComponent
|
||||
: public AZ::Component
|
||||
, public ColliderComponentRequestBus::Handler
|
||||
, public AzPhysics::SimulatedBodyComponentRequestsBus::Handler
|
||||
, protected PhysX::ColliderShapeRequestBus::Handler
|
||||
, protected Physics::CollisionFilteringRequestBus::Handler
|
||||
, protected Physics::HeightfieldProviderNotificationBus::Handler
|
||||
{
|
||||
public:
|
||||
using Configuration = Physics::HeightfieldShapeConfiguration;
|
||||
AZ_COMPONENT(HeightfieldColliderComponent, "{9A42672C-281A-4CE8-BFDD-EAA1E0FCED76}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
HeightfieldColliderComponent() = default;
|
||||
~HeightfieldColliderComponent() override;
|
||||
|
||||
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
|
||||
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
|
||||
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
|
||||
|
||||
void Activate() override;
|
||||
void Deactivate() override;
|
||||
|
||||
void SetShapeConfiguration(const AzPhysics::ShapeColliderPair& shapeConfig);
|
||||
|
||||
protected:
|
||||
// ColliderComponentRequestBus
|
||||
AzPhysics::ShapeColliderPairList GetShapeConfigurations() override;
|
||||
AZStd::vector<AZStd::shared_ptr<Physics::Shape>> GetShapes() override;
|
||||
|
||||
// ColliderShapeRequestBus
|
||||
AZ::Aabb GetColliderShapeAabb() override;
|
||||
bool IsTrigger() override
|
||||
{
|
||||
// PhysX Heightfields don't support triggers.
|
||||
return false;
|
||||
}
|
||||
|
||||
// CollisionFilteringRequestBus
|
||||
void SetCollisionLayer(const AZStd::string& layerName, AZ::Crc32 filterTag) override;
|
||||
AZStd::string GetCollisionLayerName() override;
|
||||
void SetCollisionGroup(const AZStd::string& groupName, AZ::Crc32 filterTag) override;
|
||||
AZStd::string GetCollisionGroupName() override;
|
||||
void ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 filterTag, bool enabled) override;
|
||||
|
||||
// SimulatedBodyComponentRequestsBus
|
||||
void EnablePhysics() override;
|
||||
void DisablePhysics() override;
|
||||
bool IsPhysicsEnabled() const override;
|
||||
AZ::Aabb GetAabb() const override;
|
||||
AzPhysics::SimulatedBodyHandle GetSimulatedBodyHandle() const override;
|
||||
AzPhysics::SimulatedBody* GetSimulatedBody() override;
|
||||
AzPhysics::SceneQueryHit RayCast(const AzPhysics::RayCastRequest& request) override;
|
||||
|
||||
// HeightfieldProviderNotificationBus
|
||||
void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) override;
|
||||
|
||||
private:
|
||||
AZStd::shared_ptr<Physics::Shape> GetHeightfieldShape();
|
||||
|
||||
void ClearHeightfield();
|
||||
void InitHeightfieldShapeConfiguration();
|
||||
void InitStaticRigidBody();
|
||||
void RefreshHeightfield();
|
||||
|
||||
AzPhysics::ShapeColliderPair m_shapeConfig;
|
||||
AzPhysics::SimulatedBodyHandle m_staticRigidBodyHandle = AzPhysics::InvalidSimulatedBodyHandle;
|
||||
AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle;
|
||||
};
|
||||
} // namespace PhysX
|
||||
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Component/EntityId.h>
|
||||
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
||||
#include <Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
class MockTerrainAreaSurfaceRequestBus : public Terrain::TerrainAreaSurfaceRequestBus::Handler
|
||||
{
|
||||
public:
|
||||
MockTerrainAreaSurfaceRequestBus(AZ::EntityId entityId)
|
||||
{
|
||||
Terrain::TerrainAreaSurfaceRequestBus::Handler::BusConnect(entityId);
|
||||
}
|
||||
|
||||
~MockTerrainAreaSurfaceRequestBus()
|
||||
{
|
||||
Terrain::TerrainAreaSurfaceRequestBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
MOCK_METHOD0(Activate, void());
|
||||
MOCK_METHOD0(Deactivate, void());
|
||||
MOCK_CONST_METHOD2(GetSurfaceWeights, void(const AZ::Vector3&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&));
|
||||
};
|
||||
|
||||
} // namespace UnitTest
|
||||
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Component/Component.h>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
class MockTerrainLayerSpawnerComponent
|
||||
: public AZ::Component
|
||||
{
|
||||
public:
|
||||
AZ_COMPONENT(MockTerrainLayerSpawnerComponent, "{9F27C980-9826-4063-86D8-E981C1E842A3}");
|
||||
|
||||
static void Reflect([[maybe_unused]] AZ::ReflectContext* context)
|
||||
{
|
||||
}
|
||||
|
||||
void Activate() override
|
||||
{
|
||||
}
|
||||
|
||||
void Deactivate() override
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
|
||||
{
|
||||
services.push_back(AZ_CRC_CE("TerrainAreaService"));
|
||||
}
|
||||
};
|
||||
|
||||
} //namespace UnitTest
|
||||
@ -0,0 +1,321 @@
|
||||
/*
|
||||
* 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 <Components/TerrainPhysicsColliderComponent.h>
|
||||
|
||||
#include <AzCore/Asset/AssetManagerBus.h>
|
||||
#include <AzCore/Component/Entity.h>
|
||||
#include <AzCore/Component/TransformBus.h>
|
||||
#include <AzCore/Casting/lossy_cast.h>
|
||||
#include <AzCore/RTTI/BehaviorContext.h>
|
||||
#include <AzCore/Serialization/EditContext.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
|
||||
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
||||
|
||||
namespace Terrain
|
||||
{
|
||||
void TerrainPhysicsColliderConfig::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto serialize = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serialize->Class<TerrainPhysicsColliderConfig, AZ::ComponentConfig>()
|
||||
->Version(1)
|
||||
;
|
||||
|
||||
if (auto edit = serialize->GetEditContext())
|
||||
{
|
||||
edit->Class<TerrainPhysicsColliderConfig>(
|
||||
"Terrain Physics Collider Component",
|
||||
"Provides terrain data to a physics collider with configurable surface mappings.")
|
||||
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
||||
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
|
||||
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
|
||||
{
|
||||
services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService"));
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services)
|
||||
{
|
||||
services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService"));
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services)
|
||||
{
|
||||
services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService"));
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
TerrainPhysicsColliderConfig::Reflect(context);
|
||||
|
||||
if (auto serialize = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serialize->Class<TerrainPhysicsColliderComponent, AZ::Component>()
|
||||
->Version(0)
|
||||
->Field("Configuration", &TerrainPhysicsColliderComponent::m_configuration)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration)
|
||||
: m_configuration(configuration)
|
||||
{
|
||||
}
|
||||
|
||||
TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::Activate()
|
||||
{
|
||||
const auto entityId = GetEntityId();
|
||||
LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(entityId);
|
||||
Physics::HeightfieldProviderRequestsBus::Handler::BusConnect(entityId);
|
||||
AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect();
|
||||
|
||||
NotifyListenersOfHeightfieldDataChange();
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::Deactivate()
|
||||
{
|
||||
AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect();
|
||||
Physics::HeightfieldProviderRequestsBus::Handler ::BusDisconnect();
|
||||
LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
bool TerrainPhysicsColliderComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
|
||||
{
|
||||
if (auto config = azrtti_cast<const TerrainPhysicsColliderConfig*>(baseConfig))
|
||||
{
|
||||
m_configuration = *config;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TerrainPhysicsColliderComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const
|
||||
{
|
||||
if (auto config = azrtti_cast<TerrainPhysicsColliderConfig*>(outBaseConfig))
|
||||
{
|
||||
*config = m_configuration;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::NotifyListenersOfHeightfieldDataChange()
|
||||
{
|
||||
AZ::Aabb worldSize = AZ::Aabb::CreateNull();
|
||||
|
||||
LmbrCentral::ShapeComponentRequestsBus::EventResult(
|
||||
worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
|
||||
|
||||
Physics::HeightfieldProviderNotificationBus::Broadcast(
|
||||
&Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, worldSize);
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::OnShapeChanged([[maybe_unused]] ShapeChangeReasons changeReason)
|
||||
{
|
||||
// This will notify us of both shape changes and transform changes.
|
||||
// It's important to use this event for transform changes instead of listening to OnTransformChanged, because we need to guarantee
|
||||
// the shape has received the transform change message and updated its internal state before passing it along to us.
|
||||
|
||||
NotifyListenersOfHeightfieldDataChange();
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::OnTerrainDataCreateEnd()
|
||||
{
|
||||
// The terrain system has finished creating itself, so we should now have data for creating a heightfield.
|
||||
NotifyListenersOfHeightfieldDataChange();
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::OnTerrainDataDestroyBegin()
|
||||
{
|
||||
// The terrain system is starting to destroy itself, so notify listeners of a change since the heightfield
|
||||
// will no longer have any valid data.
|
||||
NotifyListenersOfHeightfieldDataChange();
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::OnTerrainDataChanged(
|
||||
[[maybe_unused]] const AZ::Aabb& dirtyRegion, [[maybe_unused]] TerrainDataChangedMask dataChangedMask)
|
||||
{
|
||||
NotifyListenersOfHeightfieldDataChange();
|
||||
}
|
||||
|
||||
AZ::Aabb TerrainPhysicsColliderComponent::GetHeightfieldAabb() const
|
||||
{
|
||||
AZ::Aabb worldSize = AZ::Aabb::CreateNull();
|
||||
|
||||
LmbrCentral::ShapeComponentRequestsBus::EventResult(
|
||||
worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
|
||||
|
||||
auto vector2Floor = [](const AZ::Vector2& in)
|
||||
{
|
||||
return AZ::Vector2(floor(in.GetX()), floor(in.GetY()));
|
||||
};
|
||||
auto vector2Ceil = [](const AZ::Vector2& in)
|
||||
{
|
||||
return AZ::Vector2(ceil(in.GetX()), ceil(in.GetY()));
|
||||
};
|
||||
|
||||
const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
|
||||
const AZ::Vector3 boundsMin = worldSize.GetMin();
|
||||
const AZ::Vector3 boundsMax = worldSize.GetMax();
|
||||
|
||||
const AZ::Vector2 gridMinBoundLower = vector2Floor(AZ::Vector2(boundsMin) / gridResolution) * gridResolution;
|
||||
const AZ::Vector2 gridMaxBoundUpper = vector2Ceil(AZ::Vector2(boundsMax) / gridResolution) * gridResolution;
|
||||
|
||||
return AZ::Aabb::CreateFromMinMaxValues(
|
||||
gridMinBoundLower.GetX(), gridMinBoundLower.GetY(), boundsMin.GetZ(),
|
||||
gridMaxBoundUpper.GetX(), gridMaxBoundUpper.GetY(), boundsMax.GetZ()
|
||||
);
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const
|
||||
{
|
||||
AZ::Aabb heightfieldAabb = GetHeightfieldAabb();
|
||||
|
||||
// Because our terrain heights are relative to the center of the bounding box, the min and max allowable heights are also
|
||||
// relative to the center. They are also clamped to the size of the bounding box.
|
||||
minHeightBounds = -(heightfieldAabb.GetZExtent() / 2.0f);
|
||||
maxHeightBounds = heightfieldAabb.GetZExtent() / 2.0f;
|
||||
}
|
||||
|
||||
AZ::Transform TerrainPhysicsColliderComponent::GetHeightfieldTransform() const
|
||||
{
|
||||
// We currently don't support rotation of terrain heightfields.
|
||||
AZ::Vector3 translate;
|
||||
AZ::TransformBus::EventResult(translate, GetEntityId(), &AZ::TransformBus::Events::GetWorldTranslation);
|
||||
|
||||
AZ::Transform transform = AZ::Transform::CreateTranslation(translate);
|
||||
|
||||
return transform;
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::GenerateHeightsInBounds(AZStd::vector<float>& heights) const
|
||||
{
|
||||
const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
|
||||
|
||||
AZ::Aabb worldSize = GetHeightfieldAabb();
|
||||
|
||||
const float worldCenterZ = worldSize.GetCenter().GetZ();
|
||||
|
||||
int32_t gridWidth, gridHeight;
|
||||
GetHeightfieldGridSize(gridWidth, gridHeight);
|
||||
|
||||
heights.clear();
|
||||
heights.reserve(gridWidth * gridHeight);
|
||||
|
||||
for (int32_t row = 0; row < gridHeight; row++)
|
||||
{
|
||||
const float y = row * gridResolution.GetY() + worldSize.GetMin().GetY();
|
||||
for (int32_t col = 0; col < gridWidth; col++)
|
||||
{
|
||||
const float x = col * gridResolution.GetX() + worldSize.GetMin().GetX();
|
||||
float height = 0.0f;
|
||||
|
||||
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
|
||||
height, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y,
|
||||
AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, nullptr);
|
||||
|
||||
heights.emplace_back(height - worldCenterZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::GenerateHeightsAndMaterialsInBounds(
|
||||
AZStd::vector<Physics::HeightMaterialPoint>& heightMaterials) const
|
||||
{
|
||||
const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
|
||||
|
||||
AZ::Aabb worldSize = GetHeightfieldAabb();
|
||||
|
||||
const float worldCenterZ = worldSize.GetCenter().GetZ();
|
||||
const float worldHeightBoundsMin = worldSize.GetMin().GetZ();
|
||||
const float worldHeightBoundsMax = worldSize.GetMax().GetZ();
|
||||
|
||||
int32_t gridWidth, gridHeight;
|
||||
GetHeightfieldGridSize(gridWidth, gridHeight);
|
||||
|
||||
heightMaterials.clear();
|
||||
heightMaterials.reserve(gridWidth * gridHeight);
|
||||
|
||||
for (int32_t row = 0; row < gridHeight; row++)
|
||||
{
|
||||
const float y = row * gridResolution.GetY() + worldSize.GetMin().GetY();
|
||||
for (int32_t col = 0; col < gridWidth; col++)
|
||||
{
|
||||
const float x = col * gridResolution.GetX() + worldSize.GetMin().GetX();
|
||||
float height = 0.0f;
|
||||
|
||||
bool terrainExists = true;
|
||||
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
|
||||
height, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y,
|
||||
AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, &terrainExists);
|
||||
|
||||
// Any heights that fall outside the range of our bounding box will get turned into holes.
|
||||
if ((height < worldHeightBoundsMin) || (height > worldHeightBoundsMax))
|
||||
{
|
||||
height = worldHeightBoundsMin;
|
||||
terrainExists = false;
|
||||
}
|
||||
|
||||
Physics::HeightMaterialPoint point;
|
||||
point.m_height = height - worldCenterZ;
|
||||
point.m_quadMeshType = terrainExists ? Physics::QuadMeshType::SubdivideUpperLeftToBottomRight : Physics::QuadMeshType::Hole;
|
||||
heightMaterials.emplace_back(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AZ::Vector2 TerrainPhysicsColliderComponent::GetHeightfieldGridSpacing() const
|
||||
{
|
||||
AZ::Vector2 gridResolution = AZ::Vector2(1.0f);
|
||||
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
|
||||
gridResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution);
|
||||
|
||||
return gridResolution;
|
||||
}
|
||||
|
||||
void TerrainPhysicsColliderComponent::GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const
|
||||
{
|
||||
const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
|
||||
const AZ::Aabb bounds = GetHeightfieldAabb();
|
||||
|
||||
numColumns = aznumeric_cast<int32_t>((bounds.GetMax().GetX() - bounds.GetMin().GetX()) / gridResolution.GetX());
|
||||
numRows = aznumeric_cast<int32_t>((bounds.GetMax().GetY() - bounds.GetMin().GetY()) / gridResolution.GetY());
|
||||
}
|
||||
|
||||
AZStd::vector<Physics::MaterialId> TerrainPhysicsColliderComponent::GetMaterialList() const
|
||||
{
|
||||
return AZStd::vector<Physics::MaterialId>();
|
||||
}
|
||||
|
||||
AZStd::vector<float> TerrainPhysicsColliderComponent::GetHeights() const
|
||||
{
|
||||
AZStd::vector<float> heights;
|
||||
GenerateHeightsInBounds(heights);
|
||||
|
||||
return heights;
|
||||
}
|
||||
|
||||
AZStd::vector<Physics::HeightMaterialPoint> TerrainPhysicsColliderComponent::GetHeightsAndMaterials() const
|
||||
{
|
||||
AZStd::vector<Physics::HeightMaterialPoint> heightMaterials;
|
||||
GenerateHeightsAndMaterialsInBounds(heightMaterials);
|
||||
|
||||
return heightMaterials;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Component/Component.h>
|
||||
|
||||
#include <AzFramework/Physics/HeightfieldProviderBus.h>
|
||||
#include <AzFramework/Physics/Material.h>
|
||||
#include <TerrainSystem/TerrainSystemBus.h>
|
||||
|
||||
#include <LmbrCentral/Shape/ShapeComponentBus.h>
|
||||
|
||||
namespace LmbrCentral
|
||||
{
|
||||
template<typename, typename>
|
||||
class EditorWrappedComponentBase;
|
||||
}
|
||||
|
||||
namespace Terrain
|
||||
{
|
||||
class TerrainPhysicsColliderConfig
|
||||
: public AZ::ComponentConfig
|
||||
{
|
||||
public:
|
||||
AZ_CLASS_ALLOCATOR(TerrainPhysicsColliderConfig, AZ::SystemAllocator, 0);
|
||||
AZ_RTTI(TerrainPhysicsColliderConfig, "{E9EADB8F-C3A5-4B9C-A62D-2DBC86B4CE59}", AZ::ComponentConfig);
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
};
|
||||
|
||||
|
||||
class TerrainPhysicsColliderComponent
|
||||
: public AZ::Component
|
||||
, public Physics::HeightfieldProviderRequestsBus::Handler
|
||||
, protected LmbrCentral::ShapeComponentNotificationsBus::Handler
|
||||
, protected AzFramework::Terrain::TerrainDataNotificationBus::Handler
|
||||
{
|
||||
public:
|
||||
template<typename, typename>
|
||||
friend class LmbrCentral::EditorWrappedComponentBase;
|
||||
AZ_COMPONENT(TerrainPhysicsColliderComponent, "{33C20287-1D37-44D0-96A0-2C3766E23624}");
|
||||
|
||||
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
|
||||
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services);
|
||||
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services);
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration);
|
||||
TerrainPhysicsColliderComponent();
|
||||
~TerrainPhysicsColliderComponent() = default;
|
||||
|
||||
// HeightfieldProviderRequestsBus
|
||||
AZ::Vector2 GetHeightfieldGridSpacing() const override;
|
||||
void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const override;
|
||||
void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const override;
|
||||
AZ::Aabb GetHeightfieldAabb() const override;
|
||||
AZ::Transform GetHeightfieldTransform() const override;
|
||||
AZStd::vector<Physics::MaterialId> GetMaterialList() const override;
|
||||
AZStd::vector<float> GetHeights() const override;
|
||||
AZStd::vector<Physics::HeightMaterialPoint> GetHeightsAndMaterials() const override;
|
||||
|
||||
protected:
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// AZ::Component interface implementation
|
||||
void Activate() override;
|
||||
void Deactivate() override;
|
||||
bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override;
|
||||
bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override;
|
||||
|
||||
void GenerateHeightsInBounds(AZStd::vector<float>& heights) const;
|
||||
void GenerateHeightsAndMaterialsInBounds(AZStd::vector<Physics::HeightMaterialPoint>& heightMaterials) const;
|
||||
|
||||
void NotifyListenersOfHeightfieldDataChange();
|
||||
|
||||
// ShapeComponentNotificationsBus
|
||||
void OnShapeChanged(ShapeChangeReasons changeReason) override;
|
||||
|
||||
void OnTerrainDataCreateEnd() override;
|
||||
void OnTerrainDataDestroyBegin() override;
|
||||
void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override;
|
||||
|
||||
private:
|
||||
TerrainPhysicsColliderConfig m_configuration;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 <EditorComponents/EditorTerrainPhysicsColliderComponent.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/Serialization/EditContext.h>
|
||||
|
||||
namespace Terrain
|
||||
{
|
||||
void EditorTerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
// Call ReflectSubClass in EditorWrappedComponentBase to handle all the boilerplate reflection.
|
||||
BaseClassType::ReflectSubClass<EditorTerrainPhysicsColliderComponent, BaseClassType>(
|
||||
context, 1,
|
||||
&LmbrCentral::EditorWrappedComponentBaseVersionConverter<typename BaseClassType::WrappedComponentType,
|
||||
typename BaseClassType::WrappedConfigType, 1>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Components/TerrainPhysicsColliderComponent.h>
|
||||
#include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
|
||||
#include <LmbrCentral/Component/EditorWrappedComponentBase.h>
|
||||
|
||||
namespace Terrain
|
||||
{
|
||||
class EditorTerrainPhysicsColliderComponent
|
||||
: public LmbrCentral::EditorWrappedComponentBase<TerrainPhysicsColliderComponent, TerrainPhysicsColliderConfig>
|
||||
{
|
||||
public:
|
||||
using BaseClassType = LmbrCentral::EditorWrappedComponentBase<TerrainPhysicsColliderComponent, TerrainPhysicsColliderConfig>;
|
||||
AZ_EDITOR_COMPONENT(EditorTerrainPhysicsColliderComponent, "{C43FAB8F-3968-46A6-920E-E84AEDED3DF5}", BaseClassType);
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
static constexpr auto s_categoryName = "Terrain";
|
||||
static constexpr auto s_componentName = "Terrain Physics Heightfield Collider";
|
||||
static constexpr auto s_componentDescription = "Provides terrain data to a physics collider in the form of a heightfield and surface->material mapping.";
|
||||
static constexpr auto s_icon = "Editor/Icons/Components/TerrainLayerSpawner.svg";
|
||||
static constexpr auto s_viewportIcon = "Editor/Icons/Components/Viewport/TerrainLayerSpawner.svg";
|
||||
static constexpr auto s_helpUrl = "";
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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 <AzCore/Component/ComponentApplication.h>
|
||||
#include <AzCore/Memory/MemoryComponent.h>
|
||||
#include <AzTest/AzTest.h>
|
||||
|
||||
#include <Components/TerrainHeightGradientListComponent.h>
|
||||
|
||||
#include <MockAxisAlignedBoxShapeComponent.h>
|
||||
#include <GradientSignal/Ebuses/MockGradientRequestBus.h>
|
||||
#include <LmbrCentral/Shape/MockShapes.h>
|
||||
#include <Terrain/MockTerrainLayerSpawner.h>
|
||||
#include <Terrain/MockTerrain.h>
|
||||
|
||||
using ::testing::_;
|
||||
using ::testing::AtLeast;
|
||||
using ::testing::Mock;
|
||||
using ::testing::NiceMock;
|
||||
using ::testing::Return;
|
||||
|
||||
class TerrainHeightGradientListComponentTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
AZ::ComponentApplication m_app;
|
||||
|
||||
AZStd::unique_ptr<AZ::Entity> m_entity;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
AZ::ComponentApplication::Descriptor appDesc;
|
||||
appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024;
|
||||
appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS;
|
||||
appDesc.m_stackRecordLevels = 20;
|
||||
|
||||
m_app.Create(appDesc);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
m_app.Destroy();
|
||||
}
|
||||
|
||||
void CreateEntity()
|
||||
{
|
||||
m_entity = AZStd::make_unique<AZ::Entity>();
|
||||
ASSERT_TRUE(m_entity);
|
||||
|
||||
// Create the required box component.
|
||||
UnitTest::MockAxisAlignedBoxShapeComponent* boxComponent = m_entity->CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>();
|
||||
m_app.RegisterComponentDescriptor(boxComponent->CreateDescriptor());
|
||||
|
||||
// Create the TerrainHeightGradientListComponent with an entity in its configuration.
|
||||
Terrain::TerrainHeightGradientListConfig config;
|
||||
config.m_gradientEntities.push_back(m_entity->GetId());
|
||||
|
||||
Terrain::TerrainHeightGradientListComponent* heightGradientListComponent = m_entity->CreateComponent<Terrain::TerrainHeightGradientListComponent>(config);
|
||||
m_app.RegisterComponentDescriptor(heightGradientListComponent->CreateDescriptor());
|
||||
|
||||
// Create a MockTerrainLayerSpawnerComponent to provide the required TerrainAreaService.
|
||||
UnitTest::MockTerrainLayerSpawnerComponent* layerSpawner = m_entity->CreateComponent<UnitTest::MockTerrainLayerSpawnerComponent>();
|
||||
m_app.RegisterComponentDescriptor(layerSpawner->CreateDescriptor());
|
||||
|
||||
m_entity->Init();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TerrainHeightGradientListComponentTest, ActivateEntityActivateSuccess)
|
||||
{
|
||||
// Check that the entity activates.
|
||||
CreateEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientRefreshesTerrainSystem)
|
||||
{
|
||||
// Check that the HeightGradientListComponent informs the TerrainSystem when the composition changes.
|
||||
CreateEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
NiceMock<UnitTest::MockTerrainSystemService> terrainSystem;
|
||||
|
||||
// As the TerrainHeightGradientListComponent subscribes to the dependency monitor, RefreshArea will be called twice:
|
||||
// once due to OnCompositionChanged being picked up by the the dependency monitor and resending the notification,
|
||||
// and once when the HeightGradientListComponent gets the OnCompositionChanged directly through the DependencyNotificationBus.
|
||||
EXPECT_CALL(terrainSystem, RefreshArea(_)).Times(2);
|
||||
|
||||
LmbrCentral::DependencyNotificationBus::Event(m_entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
|
||||
|
||||
// Stop the EXPECT_CALL check now, as OnCompositionChanged will get called twice again during the reset.
|
||||
Mock::VerifyAndClearExpectations(&terrainSystem);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientListReturnsHeights)
|
||||
{
|
||||
// Check that the HeightGradientListComponent returns expected height values.
|
||||
CreateEntity();
|
||||
|
||||
NiceMock<UnitTest::MockTerrainAreaHeightRequests> heightfieldRequestBus(m_entity->GetId());
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
const float mockGradientValue = 0.25f;
|
||||
NiceMock<UnitTest::MockGradientRequests> gradientRequests(m_entity->GetId());
|
||||
ON_CALL(gradientRequests, GetValue).WillByDefault(Return(mockGradientValue));
|
||||
|
||||
// Setup a mock to provide the encompassing Aabb to the HeightGradientListComponent.
|
||||
const float min = 0.0f;
|
||||
const float max = 1000.0f;
|
||||
const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(max));
|
||||
NiceMock<UnitTest::MockShapeComponentRequests> mockShapeRequests(m_entity->GetId());
|
||||
ON_CALL(mockShapeRequests, GetEncompassingAabb).WillByDefault(Return(aabb));
|
||||
|
||||
const float worldMax = 10000.0f;
|
||||
const AZ::Aabb worldAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(worldMax));
|
||||
NiceMock<UnitTest::MockTerrainDataRequests> mockterrainDataRequests;
|
||||
ON_CALL(mockterrainDataRequests, GetTerrainHeightQueryResolution).WillByDefault(Return(AZ::Vector2(1.0f)));
|
||||
ON_CALL(mockterrainDataRequests, GetTerrainAabb).WillByDefault(Return(worldAabb));
|
||||
|
||||
// Ensure the cached values in the HeightGradientListComponent are up to date.
|
||||
LmbrCentral::DependencyNotificationBus::Event(m_entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
|
||||
|
||||
const AZ::Vector3 inPosition = AZ::Vector3::CreateZero();
|
||||
AZ::Vector3 outPosition = AZ::Vector3::CreateZero();
|
||||
bool terrainExists = false;
|
||||
Terrain::TerrainAreaHeightRequestBus::Event(m_entity->GetId(), &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, terrainExists);
|
||||
|
||||
const float height = outPosition.GetZ();
|
||||
|
||||
EXPECT_NEAR(height, mockGradientValue * max, 0.01f);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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 <AzCore/Casting/lossy_cast.h>
|
||||
#include <AzCore/Component/ComponentApplication.h>
|
||||
#include <AzCore/Component/TransformBus.h>
|
||||
#include <AzCore/Memory/MemoryComponent.h>
|
||||
|
||||
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
||||
#include <AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h>
|
||||
|
||||
#include <Components/TerrainPhysicsColliderComponent.h>
|
||||
#include <LmbrCentral/Shape/ShapeComponentBus.h>
|
||||
#include <LmbrCentral/Shape/BoxShapeComponentBus.h>
|
||||
#include <LmbrCentral/Shape/MockShapes.h>
|
||||
#include <AzTest/AzTest.h>
|
||||
|
||||
#include <MockAxisAlignedBoxShapeComponent.h>
|
||||
#include <Terrain/MockTerrain.h>
|
||||
|
||||
using ::testing::NiceMock;
|
||||
using ::testing::AtLeast;
|
||||
using ::testing::_;
|
||||
using ::testing::Return;
|
||||
|
||||
class TerrainPhysicsColliderComponentTest
|
||||
: public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
AZ::ComponentApplication m_app;
|
||||
|
||||
AZStd::unique_ptr<AZ::Entity> m_entity;
|
||||
Terrain::TerrainPhysicsColliderComponent* m_colliderComponent;
|
||||
UnitTest::MockAxisAlignedBoxShapeComponent* m_boxComponent;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
AZ::ComponentApplication::Descriptor appDesc;
|
||||
appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024;
|
||||
appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS;
|
||||
appDesc.m_stackRecordLevels = 20;
|
||||
|
||||
m_app.Create(appDesc);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
m_app.Destroy();
|
||||
}
|
||||
|
||||
void CreateEntity()
|
||||
{
|
||||
m_entity = AZStd::make_unique<AZ::Entity>();
|
||||
ASSERT_TRUE(m_entity);
|
||||
|
||||
m_entity->Init();
|
||||
}
|
||||
|
||||
void AddTerrainPhysicsColliderAndShapeComponentToEntity()
|
||||
{
|
||||
m_boxComponent = m_entity->CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>();
|
||||
m_app.RegisterComponentDescriptor(m_boxComponent->CreateDescriptor());
|
||||
|
||||
m_colliderComponent = m_entity->CreateComponent<Terrain::TerrainPhysicsColliderComponent>(Terrain::TerrainPhysicsColliderConfig());
|
||||
m_app.RegisterComponentDescriptor(m_colliderComponent->CreateDescriptor());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TerrainPhysicsColliderComponentTest, ActivateEntityActivateSuccess)
|
||||
{
|
||||
// Check that the entity activates with a collider and the required shape attached.
|
||||
CreateEntity();
|
||||
AddTerrainPhysicsColliderAndShapeComponentToEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderTransformChangedNotifiesHeightfieldBus)
|
||||
{
|
||||
// Check that the HeightfieldBus is notified when the transform of the entity changes.
|
||||
CreateEntity();
|
||||
|
||||
AddTerrainPhysicsColliderAndShapeComponentToEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
NiceMock<UnitTest::MockHeightfieldProviderNotificationBusListener> heightfieldListener(m_entity->GetId());
|
||||
EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_)).Times(1);
|
||||
|
||||
// The component gets transform change notifications via the shape bus.
|
||||
LmbrCentral::ShapeComponentNotificationsBus::Event(
|
||||
m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
|
||||
LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderShapeChangedNotifiesHeightfieldBus)
|
||||
{
|
||||
// Check that the Heightfield bus is notified when the shape component changes.
|
||||
CreateEntity();
|
||||
|
||||
AddTerrainPhysicsColliderAndShapeComponentToEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
NiceMock<UnitTest::MockHeightfieldProviderNotificationBusListener> heightfieldListener(m_entity->GetId());
|
||||
EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_)).Times(1);
|
||||
|
||||
LmbrCentral::ShapeComponentNotificationsBus::Event(
|
||||
m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
|
||||
LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsAlignedRowBoundsCorrectly)
|
||||
{
|
||||
// Check that the heightfield grid size is correct when the shape bounds match the grid resolution.
|
||||
CreateEntity();
|
||||
|
||||
AddTerrainPhysicsColliderAndShapeComponentToEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
const float boundsMin = 0.0f;
|
||||
const float boundsMax = 1024.0f;
|
||||
|
||||
NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
|
||||
const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
|
||||
ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
|
||||
|
||||
const AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f);
|
||||
NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
|
||||
ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
|
||||
|
||||
int32_t cols, rows;
|
||||
Physics::HeightfieldProviderRequestsBus::Event(
|
||||
m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
|
||||
|
||||
// With the bounds set at 0-1024 and a resolution of 1.0, the heightfield grid should be 1024x1024.
|
||||
EXPECT_EQ(cols, 1024);
|
||||
EXPECT_EQ(rows, 1024);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMinBoundsCorrectly)
|
||||
{
|
||||
// Check that the heightfield grid is correctly expanded if the minimum value of the bounds needs expanding
|
||||
// to correctly encompass it.
|
||||
CreateEntity();
|
||||
|
||||
AddTerrainPhysicsColliderAndShapeComponentToEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
const float boundsMin = 0.1f;
|
||||
const float boundsMax = 1024.0f;
|
||||
|
||||
NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
|
||||
const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
|
||||
ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
|
||||
|
||||
AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f);
|
||||
NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
|
||||
ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
|
||||
|
||||
int32_t cols, rows;
|
||||
Physics::HeightfieldProviderRequestsBus::Event(
|
||||
m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
|
||||
|
||||
// If the heightfield is not expanded to ensure it encompasses the shape bounds,
|
||||
// the values returned would be 1023.
|
||||
EXPECT_EQ(cols, 1024);
|
||||
EXPECT_EQ(rows, 1024);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMaxBoundsCorrectly)
|
||||
{
|
||||
// Check that the heightfield grid is correctly expanded if the maximum value of the bounds needs expanding
|
||||
// to correctly encompass it.
|
||||
CreateEntity();
|
||||
|
||||
AddTerrainPhysicsColliderAndShapeComponentToEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
const float boundsMin = 0.0f;
|
||||
const float boundsMax = 1023.5f;
|
||||
|
||||
NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
|
||||
const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
|
||||
ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
|
||||
|
||||
AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f);
|
||||
NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
|
||||
ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
|
||||
|
||||
int32_t cols, rows;
|
||||
Physics::HeightfieldProviderRequestsBus::Event(
|
||||
m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
|
||||
|
||||
// If the heightfield is not expanded to ensure it encompasses the shape bounds,
|
||||
// the values returned would be 1023.
|
||||
EXPECT_EQ(cols, 1024);
|
||||
EXPECT_EQ(rows, 1024);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsReturnsHeights)
|
||||
{
|
||||
// Check that the TerrainPhysicsCollider returns a heightfield of the expected size.
|
||||
CreateEntity();
|
||||
|
||||
AddTerrainPhysicsColliderAndShapeComponentToEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
const float boundsMin = 0.0f;
|
||||
const float boundsMax = 1024.0f;
|
||||
|
||||
NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
|
||||
const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
|
||||
ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
|
||||
|
||||
AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f);
|
||||
NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
|
||||
ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
|
||||
|
||||
int32_t cols, rows;
|
||||
Physics::HeightfieldProviderRequestsBus::Event(
|
||||
m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
|
||||
|
||||
AZStd::vector<float> heights;
|
||||
|
||||
Physics::HeightfieldProviderRequestsBus::EventResult(
|
||||
heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights);
|
||||
|
||||
EXPECT_EQ(cols, 1024);
|
||||
EXPECT_EQ(rows, 1024);
|
||||
EXPECT_EQ(heights.size(), cols * rows);
|
||||
|
||||
m_entity.reset();
|
||||
}
|
||||
|
||||
TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsRelativeHeightsCorrectly)
|
||||
{
|
||||
// Check that the values stored in the heightfield returned by the TerrainPhysicsCollider are correct.
|
||||
CreateEntity();
|
||||
|
||||
AddTerrainPhysicsColliderAndShapeComponentToEntity();
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
const AZ::Vector3 boundsMin = AZ::Vector3(0.0f);
|
||||
const AZ::Vector3 boundsMax = AZ::Vector3(256.0f, 256.0f, 32768.0f);
|
||||
|
||||
const float mockHeight = 32768.0f;
|
||||
AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f);
|
||||
|
||||
NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
|
||||
ON_CALL(terrainListener, GetHeightFromFloats).WillByDefault(Return(mockHeight));
|
||||
ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
|
||||
|
||||
// Just return the bounds as setup. This is equivalent to the box being at the origin.
|
||||
NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
|
||||
const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
|
||||
ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
|
||||
|
||||
AZStd::vector<float> heights;
|
||||
|
||||
Physics::HeightfieldProviderRequestsBus::EventResult(heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights);
|
||||
|
||||
ASSERT_FALSE(heights.empty());
|
||||
|
||||
const float expectedHeightValue = 16384.0f;
|
||||
EXPECT_NEAR(heights[0], expectedHeightValue, 0.01f);
|
||||
|
||||
m_entity->Reset();
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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 <Components/TerrainSurfaceGradientListComponent.h>
|
||||
#include <Terrain/MockTerrainLayerSpawner.h>
|
||||
#include <GradientSignal/Ebuses/MockGradientRequestBus.h>
|
||||
|
||||
using ::testing::NiceMock;
|
||||
using ::testing::AtLeast;
|
||||
using ::testing::_;
|
||||
using ::testing::Return;
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
class TerrainSurfaceGradientListTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
AZ::ComponentApplication m_app;
|
||||
|
||||
AZStd::unique_ptr<AZ::Entity> m_entity;
|
||||
UnitTest::MockTerrainLayerSpawnerComponent* m_layerSpawnerComponent = nullptr;
|
||||
AZStd::unique_ptr<AZ::Entity> m_gradientEntity1, m_gradientEntity2;
|
||||
|
||||
const AZStd::string surfaceTag1 = "testtag1";
|
||||
const AZStd::string surfaceTag2 = "testtag2";
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
AZ::ComponentApplication::Descriptor appDesc;
|
||||
appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024;
|
||||
appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS;
|
||||
appDesc.m_stackRecordLevels = 20;
|
||||
|
||||
m_app.Create(appDesc);
|
||||
|
||||
CreateEntities();
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
m_gradientEntity2.reset();
|
||||
m_gradientEntity1.reset();
|
||||
m_entity.reset();
|
||||
|
||||
m_app.Destroy();
|
||||
}
|
||||
|
||||
void CreateEntities()
|
||||
{
|
||||
m_entity = AZStd::make_unique<AZ::Entity>();
|
||||
ASSERT_TRUE(m_entity);
|
||||
|
||||
m_entity->Init();
|
||||
|
||||
m_gradientEntity1 = AZStd::make_unique<AZ::Entity>();
|
||||
ASSERT_TRUE(m_gradientEntity1);
|
||||
|
||||
m_gradientEntity1->Init();
|
||||
|
||||
m_gradientEntity2 = AZStd::make_unique<AZ::Entity>();
|
||||
ASSERT_TRUE(m_gradientEntity2);
|
||||
|
||||
m_gradientEntity2->Init();
|
||||
}
|
||||
|
||||
void AddSurfaceGradientListToEntities()
|
||||
{
|
||||
m_layerSpawnerComponent = m_entity->CreateComponent<UnitTest::MockTerrainLayerSpawnerComponent>();
|
||||
m_app.RegisterComponentDescriptor(m_layerSpawnerComponent->CreateDescriptor());
|
||||
|
||||
Terrain::TerrainSurfaceGradientListConfig config;
|
||||
|
||||
Terrain::TerrainSurfaceGradientMapping mapping1;
|
||||
mapping1.m_gradientEntityId = m_gradientEntity1->GetId();
|
||||
mapping1.m_surfaceTag = SurfaceData::SurfaceTag(surfaceTag1);
|
||||
config.m_gradientSurfaceMappings.emplace_back(mapping1);
|
||||
|
||||
Terrain::TerrainSurfaceGradientMapping mapping2;
|
||||
mapping2.m_gradientEntityId = m_gradientEntity2->GetId();
|
||||
mapping2.m_surfaceTag = SurfaceData::SurfaceTag(surfaceTag2);
|
||||
config.m_gradientSurfaceMappings.emplace_back(mapping2);
|
||||
|
||||
Terrain::TerrainSurfaceGradientListComponent* terrainSurfaceGradientListComponent =
|
||||
m_entity->CreateComponent<Terrain::TerrainSurfaceGradientListComponent>(config);
|
||||
m_app.RegisterComponentDescriptor(terrainSurfaceGradientListComponent->CreateDescriptor());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TerrainSurfaceGradientListTest, SurfaceGradientReturnsSurfaceWeightsInOrder)
|
||||
{
|
||||
// When there is more that one surface/weight defined and added to the component, they should all
|
||||
// be returned in descending weight order.
|
||||
AddSurfaceGradientListToEntities();
|
||||
|
||||
m_entity->Activate();
|
||||
m_gradientEntity1->Activate();
|
||||
m_gradientEntity2->Activate();
|
||||
|
||||
const float gradient1Value = 0.3f;
|
||||
NiceMock<UnitTest::MockGradientRequests> mockGradientRequests1(m_gradientEntity1->GetId());
|
||||
ON_CALL(mockGradientRequests1, GetValue).WillByDefault(Return(gradient1Value));
|
||||
|
||||
const float gradient2Value = 1.0f;
|
||||
NiceMock<UnitTest::MockGradientRequests> mockGradientRequests2(m_gradientEntity2->GetId());
|
||||
ON_CALL(mockGradientRequests2, GetValue).WillByDefault(Return(gradient2Value));
|
||||
|
||||
AzFramework::SurfaceData::OrderedSurfaceTagWeightSet weightSet;
|
||||
Terrain::TerrainAreaSurfaceRequestBus::Event(
|
||||
m_entity->GetId(), &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeights, AZ::Vector3::CreateZero(), weightSet);
|
||||
|
||||
AZ::Crc32 expectedCrcList[] = { AZ::Crc32(surfaceTag2), AZ::Crc32(surfaceTag1) };
|
||||
const float expectedWeightList[] = { gradient2Value, gradient1Value };
|
||||
|
||||
int index = 0;
|
||||
for (const auto& surfaceWeight : weightSet)
|
||||
{
|
||||
EXPECT_EQ(surfaceWeight.m_surfaceType, expectedCrcList[index]);
|
||||
EXPECT_NEAR(surfaceWeight.m_weight, expectedWeightList[index], 0.01f);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
} // namespace UnitTest
|
||||
|
||||
|
||||
Loading…
Reference in New Issue