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>
This commit is contained in:
committed by
GitHub
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
|
||||
@@ -37,6 +37,7 @@ namespace Physics
|
||||
REFLECT_SHAPETYPE_ENUM_VALUE(Sphere);
|
||||
REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder);
|
||||
REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset);
|
||||
REFLECT_SHAPETYPE_ENUM_VALUE(Heightfield);
|
||||
|
||||
#undef REFLECT_SHAPETYPE_ENUM_VALUE
|
||||
}
|
||||
@@ -305,4 +306,125 @@ namespace Physics
|
||||
m_cachedNativeMesh = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void HeightfieldShapeConfiguration::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext
|
||||
->RegisterGenericType<AZStd::shared_ptr<HeightfieldShapeConfiguration>>();
|
||||
|
||||
serializeContext->Class<HeightfieldShapeConfiguration, ShapeConfiguration>()
|
||||
->Version(1);
|
||||
}
|
||||
}
|
||||
|
||||
HeightfieldShapeConfiguration::~HeightfieldShapeConfiguration()
|
||||
{
|
||||
SetCachedNativeHeightfield(nullptr);
|
||||
}
|
||||
|
||||
HeightfieldShapeConfiguration::HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration& other)
|
||||
: ShapeConfiguration(other)
|
||||
, m_gridResolution(other.m_gridResolution)
|
||||
, m_numColumns(other.m_numColumns)
|
||||
, m_numRows(other.m_numRows)
|
||||
, m_samples(other.m_samples)
|
||||
, m_minHeightBounds(other.m_minHeightBounds)
|
||||
, m_maxHeightBounds(other.m_maxHeightBounds)
|
||||
, m_cachedNativeHeightfield(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
HeightfieldShapeConfiguration& HeightfieldShapeConfiguration::operator=(const HeightfieldShapeConfiguration& other)
|
||||
{
|
||||
ShapeConfiguration::operator=(other);
|
||||
|
||||
m_gridResolution = other.m_gridResolution;
|
||||
m_numColumns = other.m_numColumns;
|
||||
m_numRows = other.m_numRows;
|
||||
m_samples = other.m_samples;
|
||||
m_minHeightBounds = other.m_minHeightBounds;
|
||||
m_maxHeightBounds = other.m_maxHeightBounds;
|
||||
|
||||
// Prevent raw pointer from being copied
|
||||
m_cachedNativeHeightfield = nullptr;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void* HeightfieldShapeConfiguration::GetCachedNativeHeightfield() const
|
||||
{
|
||||
return m_cachedNativeHeightfield;
|
||||
}
|
||||
|
||||
void HeightfieldShapeConfiguration::SetCachedNativeHeightfield(void* cachedNativeHeightfield) const
|
||||
{
|
||||
if (m_cachedNativeHeightfield)
|
||||
{
|
||||
Physics::SystemRequestBus::Broadcast(&Physics::SystemRequests::ReleaseNativeHeightfieldObject, m_cachedNativeHeightfield);
|
||||
}
|
||||
|
||||
m_cachedNativeHeightfield = cachedNativeHeightfield;
|
||||
}
|
||||
|
||||
AZ::Vector2 HeightfieldShapeConfiguration::GetGridResolution() const
|
||||
{
|
||||
return m_gridResolution;
|
||||
}
|
||||
|
||||
void HeightfieldShapeConfiguration::SetGridResolution(const AZ::Vector2& gridResolution)
|
||||
{
|
||||
m_gridResolution = gridResolution;
|
||||
}
|
||||
|
||||
int32_t HeightfieldShapeConfiguration::GetNumColumns() const
|
||||
{
|
||||
return m_numColumns;
|
||||
}
|
||||
|
||||
void HeightfieldShapeConfiguration::SetNumColumns(int32_t numColumns)
|
||||
{
|
||||
m_numColumns = numColumns;
|
||||
}
|
||||
|
||||
int32_t HeightfieldShapeConfiguration::GetNumRows() const
|
||||
{
|
||||
return m_numRows;
|
||||
}
|
||||
|
||||
void HeightfieldShapeConfiguration::SetNumRows(int32_t numRows)
|
||||
{
|
||||
m_numRows = numRows;
|
||||
}
|
||||
|
||||
const AZStd::vector<Physics::HeightMaterialPoint>& HeightfieldShapeConfiguration::GetSamples() const
|
||||
{
|
||||
return m_samples;
|
||||
}
|
||||
|
||||
void HeightfieldShapeConfiguration::SetSamples(const AZStd::vector<Physics::HeightMaterialPoint>& samples)
|
||||
{
|
||||
m_samples = samples;
|
||||
}
|
||||
|
||||
float HeightfieldShapeConfiguration::GetMinHeightBounds() const
|
||||
{
|
||||
return m_minHeightBounds;
|
||||
}
|
||||
|
||||
void HeightfieldShapeConfiguration::SetMinHeightBounds(float minBounds)
|
||||
{
|
||||
m_minHeightBounds = minBounds;
|
||||
}
|
||||
|
||||
float HeightfieldShapeConfiguration::GetMaxHeightBounds() const
|
||||
{
|
||||
return m_maxHeightBounds;
|
||||
}
|
||||
|
||||
void HeightfieldShapeConfiguration::SetMaxHeightBounds(float maxBounds)
|
||||
{
|
||||
m_maxHeightBounds = maxBounds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Math/Vector3.h>
|
||||
#include <AzCore/Math/Vector2.h>
|
||||
#include <AzCore/Math/Quaternion.h>
|
||||
#include <AzCore/Asset/AssetCommon.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
|
||||
#include <AzFramework/Physics/HeightfieldProviderBus.h>
|
||||
|
||||
namespace Physics
|
||||
{
|
||||
/// Used to identify shape configuration type from base class.
|
||||
@@ -27,6 +30,7 @@ namespace Physics
|
||||
Native, ///< Native shape configuration if user wishes to bypass generic shape configurations.
|
||||
PhysicsAsset, ///< Shapes configured in the asset.
|
||||
CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine.
|
||||
Heightfield ///< Interacts with the physics system heightfield
|
||||
};
|
||||
|
||||
class ShapeConfiguration
|
||||
@@ -196,4 +200,52 @@ namespace Physics
|
||||
mutable void* m_cachedNativeMesh = nullptr;
|
||||
};
|
||||
|
||||
class HeightfieldShapeConfiguration
|
||||
: public ShapeConfiguration
|
||||
{
|
||||
public:
|
||||
AZ_CLASS_ALLOCATOR(HeightfieldShapeConfiguration, AZ::SystemAllocator, 0);
|
||||
AZ_RTTI(HeightfieldShapeConfiguration, "{8DF47C83-D2A9-4E7C-8620-5E173E43C0B3}", ShapeConfiguration);
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
HeightfieldShapeConfiguration() = default;
|
||||
HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration&);
|
||||
HeightfieldShapeConfiguration& operator=(const HeightfieldShapeConfiguration&);
|
||||
~HeightfieldShapeConfiguration();
|
||||
|
||||
ShapeType GetShapeType() const override
|
||||
{
|
||||
return ShapeType::Heightfield;
|
||||
}
|
||||
|
||||
void* GetCachedNativeHeightfield() const;
|
||||
void SetCachedNativeHeightfield(void* cachedNativeHeightfield) const;
|
||||
AZ::Vector2 GetGridResolution() const;
|
||||
void SetGridResolution(const AZ::Vector2& gridSpacing);
|
||||
int32_t GetNumColumns() const;
|
||||
void SetNumColumns(int32_t numColumns);
|
||||
int32_t GetNumRows() const;
|
||||
void SetNumRows(int32_t numRows);
|
||||
const AZStd::vector<Physics::HeightMaterialPoint>& GetSamples() const;
|
||||
void SetSamples(const AZStd::vector<Physics::HeightMaterialPoint>& samples);
|
||||
float GetMinHeightBounds() const;
|
||||
void SetMinHeightBounds(float minBounds);
|
||||
float GetMaxHeightBounds() const;
|
||||
void SetMaxHeightBounds(float maxBounds);
|
||||
|
||||
private:
|
||||
//! The number of meters between each heightfield sample.
|
||||
AZ::Vector2 m_gridResolution{ 1.0f };
|
||||
//! The number of columns in the heightfield sample grid.
|
||||
int32_t m_numColumns{ 0 };
|
||||
//! The number of rows in the heightfield sample grid.
|
||||
int32_t m_numRows{ 0 };
|
||||
//! The minimum and maximum heights that can be used by this heightfield.
|
||||
//! This can be used by the physics system to choose a more optimal heightfield data type internally (ex: int16, uint8)
|
||||
float m_minHeightBounds{AZStd::numeric_limits<float>::lowest()};
|
||||
float m_maxHeightBounds{AZStd::numeric_limits<float>::max()};
|
||||
//! The grid of sample points for the heightfield.
|
||||
AZStd::vector<Physics::HeightMaterialPoint> m_samples;
|
||||
//! An optional storage pointer for the physics system to cache its native heightfield representation.
|
||||
mutable void* m_cachedNativeHeightfield{ nullptr };
|
||||
};
|
||||
} // namespace Physics
|
||||
|
||||
@@ -132,6 +132,10 @@ namespace Physics
|
||||
|
||||
virtual AZStd::shared_ptr<Material> CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) = 0;
|
||||
|
||||
/// Releases the height field object created by the physics backend.
|
||||
/// @param nativeHeightfieldObject Pointer to the height field object.
|
||||
virtual void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) = 0;
|
||||
|
||||
/// Releases the mesh object created by the physics backend.
|
||||
/// @param nativeMeshObject Pointer to the mesh object.
|
||||
virtual void ReleaseNativeMeshObject(void* nativeMeshObject) = 0;
|
||||
|
||||
@@ -107,6 +107,7 @@ namespace Physics
|
||||
PhysicsAssetShapeConfiguration::Reflect(context);
|
||||
NativeShapeConfiguration::Reflect(context);
|
||||
CookedMeshShapeConfiguration::Reflect(context);
|
||||
HeightfieldShapeConfiguration::Reflect(context);
|
||||
AzPhysics::SystemInterface::Reflect(context);
|
||||
AzPhysics::Scene::Reflect(context);
|
||||
AzPhysics::CollisionLayer::Reflect(context);
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -228,6 +228,7 @@ set(FILES
|
||||
Physics/Configuration/SimulatedBodyConfiguration.cpp
|
||||
Physics/Configuration/SystemConfiguration.h
|
||||
Physics/Configuration/SystemConfiguration.cpp
|
||||
Physics/HeightfieldProviderBus.h
|
||||
Physics/SimulatedBodies/RigidBody.h
|
||||
Physics/SimulatedBodies/RigidBody.cpp
|
||||
Physics/SimulatedBodies/StaticRigidBody.h
|
||||
@@ -251,6 +252,7 @@ set(FILES
|
||||
Physics/Shape.h
|
||||
Physics/ShapeConfiguration.h
|
||||
Physics/ShapeConfiguration.cpp
|
||||
Physics/HeightfieldProviderBus.h
|
||||
Physics/SystemBus.h
|
||||
Physics/ColliderComponentBus.h
|
||||
Physics/RagdollPhysicsBus.h
|
||||
|
||||
@@ -42,6 +42,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
NAMESPACE AZ
|
||||
FILES_CMAKE
|
||||
Tests/framework_shared_tests_files.cmake
|
||||
AzFramework/Physics/physics_mock_files.cmake
|
||||
INCLUDE_DIRECTORIES
|
||||
PUBLIC
|
||||
Tests
|
||||
@@ -53,7 +54,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
AZ::AzTest
|
||||
AZ::AzTestShared
|
||||
)
|
||||
|
||||
|
||||
if(PAL_TRAIT_BUILD_HOST_TOOLS)
|
||||
|
||||
ly_add_target(
|
||||
|
||||
@@ -209,6 +209,7 @@ namespace Blast
|
||||
AZStd::shared_ptr<Physics::Shape>(
|
||||
const Physics::ColliderConfiguration&, const Physics::ShapeConfiguration&));
|
||||
MOCK_METHOD1(ReleaseNativeMeshObject, void(void*));
|
||||
MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void*));
|
||||
MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr<Physics::Material>(const Physics::MaterialConfiguration&));
|
||||
MOCK_METHOD0(GetDefaultMaterial, AZStd::shared_ptr<Physics::Material>());
|
||||
MOCK_METHOD1(
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Physics
|
||||
}
|
||||
MOCK_METHOD2(CreateShape, AZStd::shared_ptr<Physics::Shape>(const Physics::ColliderConfiguration& colliderConfiguration, const Physics::ShapeConfiguration& configuration));
|
||||
MOCK_METHOD1(ReleaseNativeMeshObject, void(void* nativeMeshObject));
|
||||
MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void* nativeMeshObject));
|
||||
MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr<Physics::Material>(const Physics::MaterialConfiguration& materialConfiguration));
|
||||
MOCK_METHOD3(CookConvexMeshToFile, bool(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount));
|
||||
MOCK_METHOD3(CookConvexMeshToMemory, bool(const AZ::Vector3* vertices, AZ::u32 vertexCount, AZStd::vector<AZ::u8>& result));
|
||||
|
||||
@@ -107,7 +107,16 @@ endif()
|
||||
# Tests
|
||||
################################################################################
|
||||
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
|
||||
ly_add_target(
|
||||
NAME GradientSignal.Mocks HEADERONLY
|
||||
NAMESPACE Gem
|
||||
FILES_CMAKE
|
||||
gradientsignal_mocks_files.cmake
|
||||
INCLUDE_DIRECTORIES
|
||||
INTERFACE
|
||||
Mocks
|
||||
)
|
||||
|
||||
ly_add_target(
|
||||
NAME GradientSignal.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
|
||||
NAMESPACE Gem
|
||||
@@ -122,6 +131,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
AZ::AzTest
|
||||
Gem::GradientSignal.Static
|
||||
Gem::LmbrCentral
|
||||
Gem::GradientSignal.Mocks
|
||||
)
|
||||
ly_add_googletest(
|
||||
NAME Gem::GradientSignal.Tests
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -15,12 +15,6 @@
|
||||
|
||||
namespace LmbrCentral
|
||||
{
|
||||
/// Type ID for the AxisAlignedBoxShapeComponent
|
||||
static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}";
|
||||
|
||||
/// Type ID for the EditorAxisAlignedBoxShapeComponent
|
||||
static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}";
|
||||
|
||||
/// Provide a Component interface for AxisAlignedBoxShape functionality.
|
||||
class AxisAlignedBoxShapeComponent
|
||||
: public AZ::Component
|
||||
|
||||
@@ -24,6 +24,12 @@ namespace LmbrCentral
|
||||
/// Type ID for the BoxShapeConfig
|
||||
static const AZ::Uuid BoxShapeConfigTypeId = "{F034FBA2-AC2F-4E66-8152-14DFB90D6283}";
|
||||
|
||||
/// Type ID for the AxisAlignedBoxShapeComponent
|
||||
static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}";
|
||||
|
||||
/// Type ID for the EditorAxisAlignedBoxShapeComponent
|
||||
static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}";
|
||||
|
||||
/// Configuration data for BoxShapeComponent
|
||||
class BoxShapeConfig
|
||||
: public ShapeComponentConfig
|
||||
|
||||
@@ -676,7 +676,17 @@ namespace PhysX
|
||||
}
|
||||
}
|
||||
|
||||
AZ::Transform Collider::GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig,
|
||||
void Collider::DrawHeightfield(
|
||||
[[maybe_unused]] AzFramework::DebugDisplayRequests& debugDisplay,
|
||||
[[maybe_unused]] const Physics::ColliderConfiguration& colliderConfig,
|
||||
[[maybe_unused]] const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig,
|
||||
[[maybe_unused]] const AZ::Vector3& colliderScale,
|
||||
[[maybe_unused]] const bool forceUniformScaling) const
|
||||
{
|
||||
}
|
||||
|
||||
AZ::Transform Collider::GetColliderLocalTransform(
|
||||
const Physics::ColliderConfiguration& colliderConfig,
|
||||
const AZ::Vector3& colliderScale) const
|
||||
{
|
||||
// Apply entity world transform scale to collider offset
|
||||
|
||||
@@ -104,7 +104,15 @@ namespace PhysX
|
||||
const AZ::Vector3& meshScale,
|
||||
AZ::u32 geomIndex) const;
|
||||
|
||||
void DrawPolygonPrism(AzFramework::DebugDisplayRequests& debugDisplay,
|
||||
void DrawHeightfield(
|
||||
AzFramework::DebugDisplayRequests& debugDisplay,
|
||||
const Physics::ColliderConfiguration& colliderConfig,
|
||||
const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig,
|
||||
const AZ::Vector3& colliderScale = AZ::Vector3::CreateOne(),
|
||||
const bool forceUniformScaling = false) const;
|
||||
|
||||
void DrawPolygonPrism(
|
||||
AzFramework::DebugDisplayRequests& debugDisplay,
|
||||
const Physics::ColliderConfiguration& colliderConfig, const AZStd::vector<AZ::Vector3>& points) const;
|
||||
|
||||
AZ::Transform GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig,
|
||||
|
||||
@@ -16,19 +16,21 @@ namespace AzPhysics
|
||||
{
|
||||
class CollisionGroup;
|
||||
class CollisionLayer;
|
||||
}
|
||||
} // namespace AzPhysics
|
||||
|
||||
namespace physx
|
||||
{
|
||||
class PxScene;
|
||||
class PxSceneDesc;
|
||||
class PxConvexMesh;
|
||||
class PxHeightField;
|
||||
class PxTriangleMesh;
|
||||
class PxShape;
|
||||
class PxCooking;
|
||||
class PxControllerManager;
|
||||
struct PxFilterData;
|
||||
}
|
||||
struct PxHeightFieldSample;
|
||||
} // namespace physx
|
||||
|
||||
namespace PhysX
|
||||
{
|
||||
@@ -63,6 +65,13 @@ namespace PhysX
|
||||
/// @return Pointer to the created mesh.
|
||||
virtual physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) = 0;
|
||||
|
||||
/// Creates a new heightfield.
|
||||
/// @param samples Pointer to beginning of heightfield sample data.
|
||||
/// @param numRows Number of rows in the heightfield.
|
||||
/// @param numColumns Number of columns in the heightfield.
|
||||
/// @return Pointer to the created heightfield.
|
||||
virtual physx::PxHeightField* CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) = 0;
|
||||
|
||||
/// Creates PhysX collision filter data from generic collision filtering settings.
|
||||
/// @param layer The collision layer the object belongs to.
|
||||
/// @param group The set of collision layers the object will interact with.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <Source/BaseColliderComponent.h>
|
||||
#include <Source/MeshColliderComponent.h>
|
||||
#include <Source/BoxColliderComponent.h>
|
||||
#include <Source/HeightfieldColliderComponent.h>
|
||||
#include <Source/SphereColliderComponent.h>
|
||||
#include <Source/CapsuleColliderComponent.h>
|
||||
#include <Source/ShapeColliderComponent.h>
|
||||
@@ -36,6 +37,7 @@ namespace PhysX
|
||||
BaseColliderComponent::CreateDescriptor(),
|
||||
MeshColliderComponent::CreateDescriptor(),
|
||||
BoxColliderComponent::CreateDescriptor(),
|
||||
HeightfieldColliderComponent::CreateDescriptor(),
|
||||
SphereColliderComponent::CreateDescriptor(),
|
||||
CapsuleColliderComponent::CreateDescriptor(),
|
||||
ShapeColliderComponent::CreateDescriptor(),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <Source/EditorColliderComponent.h>
|
||||
#include <Source/EditorFixedJointComponent.h>
|
||||
#include <Source/EditorForceRegionComponent.h>
|
||||
#include <Source/EditorHeightfieldColliderComponent.h>
|
||||
#include <Source/EditorHingeJointComponent.h>
|
||||
#include <Source/EditorJointComponent.h>
|
||||
#include <Source/EditorRigidBodyComponent.h>
|
||||
@@ -35,6 +36,7 @@ namespace PhysX
|
||||
EditorColliderComponent::CreateDescriptor(),
|
||||
EditorFixedJointComponent::CreateDescriptor(),
|
||||
EditorForceRegionComponent::CreateDescriptor(),
|
||||
EditorHeightfieldColliderComponent::CreateDescriptor(),
|
||||
EditorHingeJointComponent::CreateDescriptor(),
|
||||
EditorJointComponent::CreateDescriptor(),
|
||||
EditorRigidBodyComponent::CreateDescriptor(),
|
||||
|
||||
@@ -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
|
||||
@@ -252,6 +252,22 @@ namespace PhysX
|
||||
return convex;
|
||||
}
|
||||
|
||||
physx::PxHeightField* SystemComponent::CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns)
|
||||
{
|
||||
physx::PxHeightFieldDesc desc;
|
||||
desc.format = physx::PxHeightFieldFormat::eS16_TM;
|
||||
desc.nbColumns = numColumns;
|
||||
desc.nbRows = numRows;
|
||||
desc.samples.data = samples;
|
||||
desc.samples.stride = sizeof(physx::PxHeightFieldSample);
|
||||
|
||||
physx::PxHeightField* heightfield =
|
||||
m_physXSystem->GetPxCooking()->createHeightField(desc, m_physXSystem->GetPxPhysics()->getPhysicsInsertionCallback());
|
||||
AZ_Error("PhysX", heightfield, "Error. Unable to create heightfield");
|
||||
|
||||
return heightfield;
|
||||
}
|
||||
|
||||
bool SystemComponent::CookConvexMeshToFile(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount)
|
||||
{
|
||||
AZStd::vector<AZ::u8> physxData;
|
||||
@@ -342,6 +358,14 @@ namespace PhysX
|
||||
return AZStd::make_shared<PhysX::Material>(materialConfiguration);
|
||||
}
|
||||
|
||||
void SystemComponent::ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject)
|
||||
{
|
||||
if (nativeHeightfieldObject)
|
||||
{
|
||||
static_cast<physx::PxBase*>(nativeHeightfieldObject)->release();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemComponent::ReleaseNativeMeshObject(void* nativeMeshObject)
|
||||
{
|
||||
if (nativeMeshObject)
|
||||
|
||||
@@ -76,6 +76,7 @@ namespace PhysX
|
||||
physx::PxConvexMesh* CreateConvexMesh(const void* vertices, AZ::u32 vertexNum, AZ::u32 vertexStride) override; // should we use AZ::Vector3* or physx::PxVec3 here?
|
||||
physx::PxConvexMesh* CreateConvexMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) override;
|
||||
physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) override;
|
||||
physx::PxHeightField* CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) override;
|
||||
|
||||
|
||||
bool CookConvexMeshToFile(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount) override;
|
||||
@@ -112,6 +113,7 @@ namespace PhysX
|
||||
AZStd::shared_ptr<Physics::Material> CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) override;
|
||||
|
||||
void ReleaseNativeMeshObject(void* nativeMeshObject) override;
|
||||
void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) override;
|
||||
|
||||
// Assets related data
|
||||
AZStd::vector<AZStd::unique_ptr<AZ::Data::AssetHandler>> m_assetHandlers;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
#include <AzCore/Component/TransformBus.h>
|
||||
#include <AzCore/Component/NonUniformScaleBus.h>
|
||||
#include <AzCore/Casting/lossy_cast.h>
|
||||
#include <AzCore/EBus/Results.h>
|
||||
#include <AzCore/Interface/Interface.h>
|
||||
#include <AzCore/RTTI/BehaviorContext.h>
|
||||
@@ -25,6 +26,7 @@
|
||||
#include <AzFramework/Physics/PhysicsScene.h>
|
||||
#include <AzFramework/Physics/PhysicsSystem.h>
|
||||
#include <AzFramework/Physics/SimulatedBodies/StaticRigidBody.h>
|
||||
#include <AzFramework/Physics/HeightfieldProviderBus.h>
|
||||
|
||||
#include <PhysX/ColliderShapeBus.h>
|
||||
#include <PhysX/SystemComponentBus.h>
|
||||
@@ -64,6 +66,136 @@ namespace PhysX
|
||||
}
|
||||
}
|
||||
|
||||
void CreatePxGeometryFromHeightfield(
|
||||
const Physics::HeightfieldShapeConfiguration& heightfieldConfig, physx::PxGeometryHolder& pxGeometry)
|
||||
{
|
||||
physx::PxHeightField* heightfield = nullptr;
|
||||
|
||||
const AZ::Vector2 gridSpacing = heightfieldConfig.GetGridResolution();
|
||||
|
||||
const int32_t numCols = heightfieldConfig.GetNumColumns();
|
||||
const int32_t numRows = heightfieldConfig.GetNumRows();
|
||||
|
||||
const float rowScale = gridSpacing.GetX();
|
||||
const float colScale = gridSpacing.GetY();
|
||||
|
||||
const float minHeightBounds = heightfieldConfig.GetMinHeightBounds();
|
||||
const float maxHeightBounds = heightfieldConfig.GetMaxHeightBounds();
|
||||
const float halfBounds{ (maxHeightBounds - minHeightBounds) / 2.0f };
|
||||
|
||||
// We're making the assumption right now that the min/max bounds are centered around 0.
|
||||
// If we ever want to allow off-center bounds, we'll need to fix up the float-to-int16 height math below
|
||||
// to account for it.
|
||||
AZ_Assert(
|
||||
AZ::IsClose(-halfBounds, minHeightBounds) && AZ::IsClose(halfBounds, maxHeightBounds),
|
||||
"Min/Max height bounds aren't centered around 0, the height conversions below will be incorrect.");
|
||||
|
||||
AZ_Assert(
|
||||
maxHeightBounds >= minHeightBounds,
|
||||
"Max height bounds is less than min height bounds, the height conversions below will be incorrect.");
|
||||
|
||||
// To convert our floating-point heights to fixed-point representation inside of an int16, we need a scale factor
|
||||
// for the conversion. The scale factor is used to map the most important bits of our floating-point height to the
|
||||
// full 16-bit range.
|
||||
// Note that the scaleFactor choice here affects overall precision. For each bit that the integer part of our max
|
||||
// height uses, that's one less bit for the fractional part.
|
||||
const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits<int16_t>::max() / halfBounds;
|
||||
const float heightScale{ 1.0f / scaleFactor };
|
||||
|
||||
const uint8_t physxMaximumMaterialIndex = 0x7f;
|
||||
|
||||
// Delete the cached heightfield object if it is there, and create a new one and save in the shape configuration
|
||||
heightfieldConfig.SetCachedNativeHeightfield(nullptr);
|
||||
|
||||
const AZStd::vector<Physics::HeightMaterialPoint>& samples = heightfieldConfig.GetSamples();
|
||||
AZ_Assert(samples.size() == numRows * numCols, "GetHeightsAndMaterials returned wrong sized heightfield");
|
||||
|
||||
if (!samples.empty())
|
||||
{
|
||||
AZStd::vector<physx::PxHeightFieldSample> physxSamples(samples.size());
|
||||
|
||||
for (int32_t row = 0; row < numRows; row++)
|
||||
{
|
||||
const bool lastRowIndex = (row == (numRows - 1));
|
||||
|
||||
for (int32_t col = 0; col < numCols; col++)
|
||||
{
|
||||
const bool lastColumnIndex = (col == (numCols - 1));
|
||||
|
||||
auto GetIndex = [numCols](int32_t row, int32_t col)
|
||||
{
|
||||
return (row * numCols) + col;
|
||||
};
|
||||
|
||||
const int32_t sampleIndex = GetIndex(row, col);
|
||||
|
||||
const Physics::HeightMaterialPoint& currentSample = samples[sampleIndex];
|
||||
physx::PxHeightFieldSample& currentPhysxSample = physxSamples[sampleIndex];
|
||||
AZ_Assert(currentSample.m_materialIndex < physxMaximumMaterialIndex, "MaterialIndex must be less than 128");
|
||||
currentPhysxSample.height = azlossy_cast<physx::PxI16>(
|
||||
AZ::GetClamp(currentSample.m_height, minHeightBounds, maxHeightBounds) * scaleFactor);
|
||||
if (lastRowIndex || lastColumnIndex)
|
||||
{
|
||||
// In PhysX, the material indices refer to the quad down and to the right of the sample.
|
||||
// If we're in the last row or last column, there aren't any quads down or to the right,
|
||||
// so just clear these out.
|
||||
currentPhysxSample.materialIndex0 = 0;
|
||||
currentPhysxSample.materialIndex1 = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Our source data is providing one material index per vertex, but PhysX wants one material index
|
||||
// per triangle. The heuristic that we'll go with for selecting the material index is to choose
|
||||
// the material for the vertex that's not on the diagonal of each triangle.
|
||||
// Ex: A *---* B
|
||||
// | / | For this, we'll use A for index0 and D for index1.
|
||||
// C *---* D
|
||||
//
|
||||
// Ex: A *---* B
|
||||
// | \ | For this, we'll use C for index0 and B for index1.
|
||||
// C *---* D
|
||||
//
|
||||
// This is a pretty arbitrary choice, so the heuristic might need to be revisited over time if this
|
||||
// causes incorrect or unpredictable physics material mappings.
|
||||
|
||||
switch (currentSample.m_quadMeshType)
|
||||
{
|
||||
case Physics::QuadMeshType::SubdivideUpperLeftToBottomRight:
|
||||
currentPhysxSample.materialIndex0 = samples[GetIndex(row + 1, col)].m_materialIndex;
|
||||
currentPhysxSample.materialIndex1 = samples[GetIndex(row, col + 1)].m_materialIndex;
|
||||
// Set the tesselation flag to say that we need to go from UL to BR
|
||||
currentPhysxSample.materialIndex0.setBit();
|
||||
break;
|
||||
case Physics::QuadMeshType::SubdivideBottomLeftToUpperRight:
|
||||
currentPhysxSample.materialIndex0 = currentSample.m_materialIndex;
|
||||
currentPhysxSample.materialIndex1 = samples[GetIndex(row + 1, col + 1)].m_materialIndex;
|
||||
break;
|
||||
case Physics::QuadMeshType::Hole:
|
||||
currentPhysxSample.materialIndex0 = physx::PxHeightFieldMaterial::eHOLE;
|
||||
currentPhysxSample.materialIndex1 = physx::PxHeightFieldMaterial::eHOLE;
|
||||
break;
|
||||
default:
|
||||
AZ_Warning("PhysX Heightfield", false, "Unhandled case in CreatePxGeometryFromConfig");
|
||||
currentPhysxSample.materialIndex0 = 0;
|
||||
currentPhysxSample.materialIndex1 = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemRequestsBus::BroadcastResult(heightfield, &SystemRequests::CreateHeightField, physxSamples.data(), numRows, numCols);
|
||||
}
|
||||
if (heightfield)
|
||||
{
|
||||
heightfieldConfig.SetCachedNativeHeightfield(heightfield);
|
||||
|
||||
physx::PxHeightFieldGeometry hfGeom(heightfield, physx::PxMeshGeometryFlags(), heightScale, rowScale, colScale);
|
||||
|
||||
pxGeometry.storeAny(hfGeom);
|
||||
}
|
||||
}
|
||||
|
||||
bool CreatePxGeometryFromConfig(const Physics::ShapeConfiguration& shapeConfiguration, physx::PxGeometryHolder& pxGeometry)
|
||||
{
|
||||
if (!shapeConfiguration.m_scale.IsGreaterThan(AZ::Vector3::CreateZero()))
|
||||
@@ -170,6 +302,14 @@ namespace PhysX
|
||||
"Please iterate over m_colliderShapes in the asset and call this function for each of them.");
|
||||
return false;
|
||||
}
|
||||
case Physics::ShapeType::Heightfield:
|
||||
{
|
||||
const Physics::HeightfieldShapeConfiguration& heightfieldConfig =
|
||||
static_cast<const Physics::HeightfieldShapeConfiguration&>(shapeConfiguration);
|
||||
|
||||
CreatePxGeometryFromHeightfield(heightfieldConfig, pxGeometry);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
AZ_Warning("PhysX Rigid Body", false, "Shape not supported in PhysX. Shape Type: %d", shapeType);
|
||||
return false;
|
||||
@@ -219,6 +359,26 @@ namespace PhysX
|
||||
physx::PxQuat pxQuat(AZ::Constants::HalfPi, physx::PxVec3(0.0f, 1.0f, 0.0f));
|
||||
shape->setLocalPose(physx::PxTransform(pxQuat));
|
||||
}
|
||||
else if (pxGeomHolder.getType() == physx::PxGeometryType::eHEIGHTFIELD)
|
||||
{
|
||||
const Physics::HeightfieldShapeConfiguration& heightfieldConfig =
|
||||
static_cast<const Physics::HeightfieldShapeConfiguration&>(shapeConfiguration);
|
||||
|
||||
// PhysX heightfields have the origin at the corner, not the center, so add an offset to the passed-in transform
|
||||
// to account for this difference.
|
||||
const AZ::Vector2 gridSpacing = heightfieldConfig.GetGridResolution();
|
||||
AZ::Vector3 offset(
|
||||
-(gridSpacing.GetX() * heightfieldConfig.GetNumColumns() / 2.0f),
|
||||
-(gridSpacing.GetY() * heightfieldConfig.GetNumRows() / 2.0f),
|
||||
0.0f);
|
||||
|
||||
// PhysX heightfields are always defined to have the height in the Y direction, not the Z direction, so we need
|
||||
// to provide additional rotations to make it Z-up.
|
||||
physx::PxQuat pxQuat = PxMathConvert(
|
||||
AZ::Quaternion::CreateFromEulerAnglesRadians(AZ::Vector3(AZ::Constants::HalfPi, AZ::Constants::HalfPi, 0.0f)));
|
||||
physx::PxTransform pxHeightfieldTransform = physx::PxTransform(PxMathConvert(offset), pxQuat);
|
||||
shape->setLocalPose(pxHeightfieldTransform);
|
||||
}
|
||||
|
||||
// Handle a possible misconfiguration when a shape is set to be both simulated & trigger. This is illegal in PhysX.
|
||||
shape->setFlag(physx::PxShapeFlag::eSIMULATION_SHAPE, colliderConfiguration.m_isSimulated && !colliderConfiguration.m_isTrigger);
|
||||
@@ -1357,6 +1517,39 @@ namespace PhysX
|
||||
|
||||
return entityWorldTransformWithoutScale * jointLocalTransformWithoutScale;
|
||||
}
|
||||
|
||||
void InitHeightfieldShapeConfiguration(AZ::EntityId entityId, Physics::HeightfieldShapeConfiguration& configuration)
|
||||
{
|
||||
configuration = Physics::HeightfieldShapeConfiguration();
|
||||
|
||||
AZ::Vector2 gridSpacing(1.0f);
|
||||
Physics::HeightfieldProviderRequestsBus::EventResult(
|
||||
gridSpacing, entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSpacing);
|
||||
|
||||
configuration.SetGridResolution(gridSpacing);
|
||||
|
||||
int32_t numRows = 0;
|
||||
int32_t numColumns = 0;
|
||||
Physics::HeightfieldProviderRequestsBus::Event(
|
||||
entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows);
|
||||
|
||||
configuration.SetNumRows(numRows);
|
||||
configuration.SetNumColumns(numColumns);
|
||||
|
||||
float minHeightBounds = 0.0f;
|
||||
float maxHeightBounds = 0.0f;
|
||||
Physics::HeightfieldProviderRequestsBus::Event(
|
||||
entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds, maxHeightBounds);
|
||||
|
||||
configuration.SetMinHeightBounds(minHeightBounds);
|
||||
configuration.SetMaxHeightBounds(maxHeightBounds);
|
||||
|
||||
AZStd::vector<Physics::HeightMaterialPoint> samples;
|
||||
Physics::HeightfieldProviderRequestsBus::EventResult(
|
||||
samples, entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials);
|
||||
|
||||
configuration.SetSamples(samples);
|
||||
}
|
||||
} // namespace Utils
|
||||
|
||||
namespace ReflectionUtils
|
||||
|
||||
@@ -188,6 +188,8 @@ namespace PhysX
|
||||
//! Returns defaultValue if the input is infinite or NaN, otherwise returns the input unchanged.
|
||||
const AZ::Vector3& Sanitize(const AZ::Vector3& input, const AZ::Vector3& defaultValue = AZ::Vector3::CreateZero());
|
||||
|
||||
void InitHeightfieldShapeConfiguration(AZ::EntityId entityId, Physics::HeightfieldShapeConfiguration& configuration);
|
||||
|
||||
namespace Geometry
|
||||
{
|
||||
using PointList = AZStd::vector<AZ::Vector3>;
|
||||
|
||||
@@ -27,6 +27,8 @@ set(FILES
|
||||
Source/EditorFixedJointComponent.h
|
||||
Source/EditorHingeJointComponent.cpp
|
||||
Source/EditorHingeJointComponent.h
|
||||
Source/EditorHeightfieldColliderComponent.cpp
|
||||
Source/EditorHeightfieldColliderComponent.h
|
||||
Source/EditorJointComponent.cpp
|
||||
Source/EditorJointComponent.h
|
||||
Source/Pipeline/MeshExporter.cpp
|
||||
|
||||
@@ -35,6 +35,8 @@ set(FILES
|
||||
Source/MeshColliderComponent.h
|
||||
Source/BoxColliderComponent.h
|
||||
Source/BoxColliderComponent.cpp
|
||||
Source/HeightfieldColliderComponent.h
|
||||
Source/HeightfieldColliderComponent.cpp
|
||||
Source/SphereColliderComponent.h
|
||||
Source/SphereColliderComponent.cpp
|
||||
Source/CapsuleColliderComponent.h
|
||||
|
||||
@@ -111,6 +111,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
AZ::AzTest
|
||||
AZ::AzFramework
|
||||
Gem::LmbrCentral.Mocks
|
||||
Gem::GradientSignal.Mocks
|
||||
Gem::Terrain.Mocks
|
||||
Gem::Terrain.Static
|
||||
)
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <AzCore/Component/ComponentApplication.h>
|
||||
#include <AzFramework/Physics/HeightfieldProviderBus.h>
|
||||
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
||||
#include <TerrainSystem/TerrainSystemBus.h>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
|
||||
class MockTerrainSystemService : private Terrain::TerrainSystemServiceRequestBus::Handler
|
||||
{
|
||||
public:
|
||||
@@ -69,11 +70,7 @@ namespace UnitTest
|
||||
Terrain::TerrainAreaHeightRequestBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
MOCK_METHOD3(GetHeight, void(
|
||||
const AZ::Vector3& inPosition,
|
||||
AZ::Vector3& outPosition,
|
||||
bool& terrainExists));
|
||||
|
||||
MOCK_METHOD3(GetHeight, void(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists));
|
||||
};
|
||||
|
||||
class MockTerrainSpawnerRequests : public Terrain::TerrainSpawnerRequestBus::Handler
|
||||
@@ -92,4 +89,35 @@ namespace UnitTest
|
||||
MOCK_METHOD2(GetPriority, void(AZ::u32& outLayer, AZ::u32& outPriority));
|
||||
MOCK_METHOD0(GetUseGroundPlane, bool());
|
||||
};
|
||||
}
|
||||
|
||||
class MockTerrainDataRequests : public AzFramework::Terrain::TerrainDataRequestBus::Handler
|
||||
{
|
||||
public:
|
||||
MockTerrainDataRequests()
|
||||
{
|
||||
AzFramework::Terrain::TerrainDataRequestBus::Handler::BusConnect();
|
||||
}
|
||||
|
||||
~MockTerrainDataRequests()
|
||||
{
|
||||
AzFramework::Terrain::TerrainDataRequestBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
MOCK_CONST_METHOD0(GetTerrainHeightQueryResolution, AZ::Vector2());
|
||||
MOCK_METHOD1(SetTerrainHeightQueryResolution, void(AZ::Vector2));
|
||||
MOCK_CONST_METHOD0(GetTerrainAabb, AZ::Aabb());
|
||||
MOCK_METHOD1(SetTerrainAabb, void(const AZ::Aabb&));
|
||||
MOCK_CONST_METHOD3(GetHeight, float(AZ::Vector3, Sampler, bool*));
|
||||
MOCK_CONST_METHOD4(GetHeightFromFloats, float(float, float, Sampler, bool*));
|
||||
MOCK_CONST_METHOD3(GetMaxSurfaceWeight, AzFramework::SurfaceData::SurfaceTagWeight(AZ::Vector3, Sampler, bool*));
|
||||
MOCK_CONST_METHOD3(GetMaxSurfaceWeightFromVector2, AzFramework::SurfaceData::SurfaceTagWeight(const AZ::Vector2&, Sampler, bool*));
|
||||
MOCK_CONST_METHOD4(GetMaxSurfaceWeightFromFloats, AzFramework::SurfaceData::SurfaceTagWeight(float, float, Sampler, bool*));
|
||||
MOCK_CONST_METHOD4(GetSurfaceWeights, void(const AZ::Vector3&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*));
|
||||
MOCK_CONST_METHOD4(GetSurfaceWeightsFromVector2, void(const AZ::Vector2&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*));
|
||||
MOCK_CONST_METHOD5(GetSurfaceWeightsFromFloats, void(float, float, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*));
|
||||
MOCK_CONST_METHOD3(GetMaxSurfaceName, const char*(AZ::Vector3, Sampler, bool*));
|
||||
MOCK_CONST_METHOD3(GetIsHoleFromFloats, bool(float, float, Sampler));
|
||||
MOCK_CONST_METHOD3(GetNormal, AZ::Vector3(AZ::Vector3, Sampler, bool*));
|
||||
MOCK_CONST_METHOD4(GetNormalFromFloats, AZ::Vector3(float, float, Sampler, bool*));
|
||||
};
|
||||
} // namespace UnitTest
|
||||
|
||||
@@ -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
|
||||
@@ -65,7 +65,7 @@ namespace Terrain
|
||||
void TerrainHeightGradientListComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services)
|
||||
{
|
||||
services.push_back(AZ_CRC_CE("TerrainAreaService"));
|
||||
services.push_back(AZ_CRC_CE("BoxShapeService"));
|
||||
services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService"));
|
||||
}
|
||||
|
||||
void TerrainHeightGradientListComponent::Reflect(AZ::ReflectContext* context)
|
||||
|
||||
@@ -102,7 +102,6 @@ namespace Terrain
|
||||
|
||||
void TerrainLayerSpawnerComponent::Activate()
|
||||
{
|
||||
AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId());
|
||||
LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(GetEntityId());
|
||||
TerrainSpawnerRequestBus::Handler::BusConnect(GetEntityId());
|
||||
|
||||
@@ -114,8 +113,6 @@ namespace Terrain
|
||||
TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::UnregisterArea, GetEntityId());
|
||||
TerrainSpawnerRequestBus::Handler::BusDisconnect();
|
||||
LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect();
|
||||
AZ::TransformNotificationBus::Handler::BusDisconnect();
|
||||
|
||||
}
|
||||
|
||||
bool TerrainLayerSpawnerComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
|
||||
@@ -138,13 +135,12 @@ namespace Terrain
|
||||
return false;
|
||||
}
|
||||
|
||||
void TerrainLayerSpawnerComponent::OnTransformChanged([[maybe_unused]] const AZ::Transform& local, [[maybe_unused]] const AZ::Transform& world)
|
||||
{
|
||||
RefreshArea();
|
||||
}
|
||||
|
||||
void TerrainLayerSpawnerComponent::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.
|
||||
|
||||
RefreshArea();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
||||
#include <TerrainSystem/TerrainSystemBus.h>
|
||||
|
||||
#include <AzCore/Component/TransformBus.h>
|
||||
#include <LmbrCentral/Shape/ShapeComponentBus.h>
|
||||
#include <AzCore/Math/Aabb.h>
|
||||
|
||||
@@ -56,7 +55,6 @@ namespace Terrain
|
||||
|
||||
class TerrainLayerSpawnerComponent
|
||||
: public AZ::Component
|
||||
, private AZ::TransformNotificationBus::Handler
|
||||
, private LmbrCentral::ShapeComponentNotificationsBus::Handler
|
||||
, private Terrain::TerrainSpawnerRequestBus::Handler
|
||||
{
|
||||
@@ -81,10 +79,6 @@ namespace Terrain
|
||||
bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override;
|
||||
|
||||
protected:
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// AZ::TransformNotificationBus::Handler
|
||||
void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override;
|
||||
|
||||
// ShapeComponentNotificationsBus
|
||||
void OnShapeChanged(ShapeChangeReasons changeReason) override;
|
||||
|
||||
|
||||
@@ -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 = "";
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <EditorComponents/EditorTerrainSystemComponent.h>
|
||||
#include <EditorComponents/EditorTerrainWorldComponent.h>
|
||||
#include <EditorComponents/EditorTerrainWorldDebuggerComponent.h>
|
||||
#include <EditorComponents/EditorTerrainPhysicsColliderComponent.h>
|
||||
#include <EditorComponents/EditorTerrainWorldRendererComponent.h>
|
||||
#include <TerrainRenderer/EditorComponents/EditorTerrainSurfaceMaterialsListComponent.h>
|
||||
#include <TerrainRenderer/EditorComponents/EditorTerrainMacroMaterialComponent.h>
|
||||
@@ -32,6 +33,7 @@ namespace Terrain
|
||||
Terrain::EditorTerrainSurfaceMaterialsListComponent::CreateDescriptor(),
|
||||
Terrain::EditorTerrainWorldComponent::CreateDescriptor(),
|
||||
Terrain::EditorTerrainWorldDebuggerComponent::CreateDescriptor(),
|
||||
Terrain::EditorTerrainPhysicsColliderComponent::CreateDescriptor(),
|
||||
Terrain::EditorTerrainWorldRendererComponent::CreateDescriptor(),
|
||||
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <Components/TerrainLayerSpawnerComponent.h>
|
||||
#include <Components/TerrainSurfaceGradientListComponent.h>
|
||||
#include <Components/TerrainSurfaceDataSystemComponent.h>
|
||||
#include <Components/TerrainPhysicsColliderComponent.h>
|
||||
#include <TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h>
|
||||
#include <TerrainRenderer/Components/TerrainMacroMaterialComponent.h>
|
||||
|
||||
@@ -37,6 +38,7 @@ namespace Terrain
|
||||
TerrainMacroMaterialComponent::CreateDescriptor(),
|
||||
TerrainSurfaceGradientListComponent::CreateDescriptor(),
|
||||
TerrainSurfaceDataSystemComponent::CreateDescriptor(),
|
||||
TerrainPhysicsColliderComponent::CreateDescriptor()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include <AzCore/Component/ComponentApplication.h>
|
||||
#include <AzCore/Component/TransformBus.h>
|
||||
#include <AzCore/Memory/MemoryComponent.h>
|
||||
|
||||
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
||||
@@ -195,8 +196,10 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerTransformChangedUpdatesTerrainSyst
|
||||
|
||||
m_entity->Activate();
|
||||
|
||||
AZ::TransformNotificationBus::Event(
|
||||
m_entity->GetId(), &AZ::TransformNotificationBus::Events::OnTransformChanged, AZ::Transform(), AZ::Transform());
|
||||
// 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->Deactivate();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
|
||||
#include <TerrainSystem/TerrainSystem.h>
|
||||
#include <Components/TerrainLayerSpawnerComponent.h>
|
||||
#include <Components/TerrainHeightGradientListComponent.h>
|
||||
|
||||
#include <GradientSignal/Ebuses/MockGradientRequestBus.h>
|
||||
#include <Terrain/MockTerrain.h>
|
||||
#include <Terrain/MockTerrainAreaSurfaceRequestBus.h>
|
||||
#include <MockAxisAlignedBoxShapeComponent.h>
|
||||
|
||||
using ::testing::AtLeast;
|
||||
@@ -25,284 +26,284 @@ using ::testing::IsFalse;
|
||||
using ::testing::Ne;
|
||||
using ::testing::NiceMock;
|
||||
using ::testing::Return;
|
||||
using ::testing::SetArgReferee;
|
||||
|
||||
class TerrainSystemTest : public ::testing::Test
|
||||
namespace UnitTest
|
||||
{
|
||||
protected:
|
||||
// Defines a structure for defining both an XY position and the expected height for that position.
|
||||
struct HeightTestPoint
|
||||
class TerrainSystemTest : public ::testing::Test
|
||||
{
|
||||
AZ::Vector2 m_testLocation;
|
||||
float m_expectedHeight;
|
||||
protected:
|
||||
// Defines a structure for defining both an XY position and the expected height for that position.
|
||||
struct HeightTestPoint
|
||||
{
|
||||
AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero();
|
||||
float m_expectedHeight = 0.0f;
|
||||
};
|
||||
|
||||
AZ::ComponentApplication m_app;
|
||||
AZStd::unique_ptr<Terrain::TerrainSystem> m_terrainSystem;
|
||||
|
||||
AZStd::unique_ptr<NiceMock<UnitTest::MockBoxShapeComponentRequests>> m_boxShapeRequests;
|
||||
AZStd::unique_ptr<NiceMock<UnitTest::MockShapeComponentRequests>> m_shapeRequests;
|
||||
AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainAreaHeightRequests>> m_terrainAreaHeightRequests;
|
||||
|
||||
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_terrainSystem.reset();
|
||||
m_boxShapeRequests.reset();
|
||||
m_shapeRequests.reset();
|
||||
m_terrainAreaHeightRequests.reset();
|
||||
m_app.Destroy();
|
||||
}
|
||||
|
||||
AZStd::unique_ptr<AZ::Entity> CreateEntity()
|
||||
{
|
||||
return AZStd::make_unique<AZ::Entity>();
|
||||
}
|
||||
|
||||
void ActivateEntity(AZ::Entity* entity)
|
||||
{
|
||||
entity->Init();
|
||||
EXPECT_EQ(AZ::Entity::State::Init, entity->GetState());
|
||||
|
||||
entity->Activate();
|
||||
EXPECT_EQ(AZ::Entity::State::Active, entity->GetState());
|
||||
}
|
||||
|
||||
template<typename Component, typename Configuration>
|
||||
AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config)
|
||||
{
|
||||
m_app.RegisterComponentDescriptor(Component::CreateDescriptor());
|
||||
return entity->CreateComponent<Component>(config);
|
||||
}
|
||||
|
||||
template<typename Component>
|
||||
AZ::Component* CreateComponent(AZ::Entity* entity)
|
||||
{
|
||||
m_app.RegisterComponentDescriptor(Component::CreateDescriptor());
|
||||
return entity->CreateComponent<Component>();
|
||||
}
|
||||
|
||||
// Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults
|
||||
// on a test-by-test basis.
|
||||
void CreateAndActivateTerrainSystem(
|
||||
AZ::Vector2 queryResolution = AZ::Vector2(1.0f),
|
||||
AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f)))
|
||||
{
|
||||
// Create the terrain system and give it one tick to fully initialize itself.
|
||||
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
||||
m_terrainSystem->SetTerrainAabb(worldBounds);
|
||||
m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution);
|
||||
m_terrainSystem->Activate();
|
||||
AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{});
|
||||
}
|
||||
|
||||
AZStd::unique_ptr<AZ::Entity> CreateAndActivateMockTerrainLayerSpawner(
|
||||
const AZ::Aabb& spawnerBox, const AZStd::function<void(AZ::Vector3& position, bool& terrainExists)>& mockHeights)
|
||||
{
|
||||
// Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider.
|
||||
auto entity = CreateEntity();
|
||||
CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>(entity.get());
|
||||
CreateComponent<Terrain::TerrainLayerSpawnerComponent>(entity.get());
|
||||
|
||||
m_boxShapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockBoxShapeComponentRequests>>(entity->GetId());
|
||||
m_shapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockShapeComponentRequests>>(entity->GetId());
|
||||
|
||||
// Set up the box shape to return whatever spawnerBox was passed in.
|
||||
ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox));
|
||||
|
||||
// Set up a mock height provider to use the passed-in mock height function to generate a height.
|
||||
m_terrainAreaHeightRequests = AZStd::make_unique<NiceMock<UnitTest::MockTerrainAreaHeightRequests>>(entity->GetId());
|
||||
ON_CALL(*m_terrainAreaHeightRequests, GetHeight)
|
||||
.WillByDefault(
|
||||
[mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists)
|
||||
{
|
||||
// By default, set the outPosition to the input position and terrain to always exist.
|
||||
outPosition = inPosition;
|
||||
terrainExists = true;
|
||||
// Let the test function modify these values based on the needs of the specific test.
|
||||
mockHeights(outPosition, terrainExists);
|
||||
});
|
||||
|
||||
ActivateEntity(entity.get());
|
||||
return entity;
|
||||
}
|
||||
};
|
||||
|
||||
AZ::ComponentApplication m_app;
|
||||
AZStd::unique_ptr<Terrain::TerrainSystem> m_terrainSystem;
|
||||
|
||||
AZStd::unique_ptr<NiceMock<UnitTest::MockBoxShapeComponentRequests>> m_boxShapeRequests;
|
||||
AZStd::unique_ptr<NiceMock<UnitTest::MockShapeComponentRequests>> m_shapeRequests;
|
||||
AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainAreaHeightRequests>> m_terrainAreaHeightRequests;
|
||||
|
||||
|
||||
void SetUp() override
|
||||
TEST_F(TerrainSystemTest, TrivialCreateDestroy)
|
||||
{
|
||||
AZ::ComponentApplication::Descriptor appDesc;
|
||||
appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024;
|
||||
appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS;
|
||||
appDesc.m_stackRecordLevels = 20;
|
||||
// Trivially verify that the terrain system can successfully be constructed and destructed without errors.
|
||||
|
||||
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, TrivialActivateDeactivate)
|
||||
{
|
||||
// Verify that the terrain system can be activated and deactivated without errors.
|
||||
|
||||
m_app.Create(appDesc);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
m_terrainSystem.reset();
|
||||
m_boxShapeRequests.reset();
|
||||
m_shapeRequests.reset();
|
||||
m_terrainAreaHeightRequests.reset();
|
||||
m_app.Destroy();
|
||||
}
|
||||
|
||||
AZStd::unique_ptr<AZ::Entity> CreateEntity()
|
||||
{
|
||||
return AZStd::make_unique<AZ::Entity>();
|
||||
}
|
||||
|
||||
void ActivateEntity(AZ::Entity* entity)
|
||||
{
|
||||
entity->Init();
|
||||
EXPECT_EQ(AZ::Entity::State::Init, entity->GetState());
|
||||
|
||||
entity->Activate();
|
||||
EXPECT_EQ(AZ::Entity::State::Active, entity->GetState());
|
||||
}
|
||||
|
||||
template<typename Component, typename Configuration>
|
||||
AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config)
|
||||
{
|
||||
m_app.RegisterComponentDescriptor(Component::CreateDescriptor());
|
||||
return entity->CreateComponent<Component>(config);
|
||||
}
|
||||
|
||||
template<typename Component>
|
||||
AZ::Component* CreateComponent(AZ::Entity* entity)
|
||||
{
|
||||
m_app.RegisterComponentDescriptor(Component::CreateDescriptor());
|
||||
return entity->CreateComponent<Component>();
|
||||
}
|
||||
|
||||
// Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults
|
||||
// on a test-by-test basis.
|
||||
void CreateAndActivateTerrainSystem(
|
||||
AZ::Vector2 queryResolution = AZ::Vector2(1.0f),
|
||||
AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f)))
|
||||
{
|
||||
// Create the terrain system and give it one tick to fully initialize itself.
|
||||
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
||||
m_terrainSystem->SetTerrainAabb(worldBounds);
|
||||
m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution);
|
||||
m_terrainSystem->Activate();
|
||||
AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{});
|
||||
m_terrainSystem->Deactivate();
|
||||
}
|
||||
|
||||
|
||||
AZStd::unique_ptr<AZ::Entity> CreateAndActivateMockTerrainLayerSpawner(
|
||||
const AZ::Aabb& spawnerBox,
|
||||
const AZStd::function<void(AZ::Vector3& position, bool& terrainExists)>& mockHeights)
|
||||
TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation)
|
||||
{
|
||||
// Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider.
|
||||
auto entity = CreateEntity();
|
||||
CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>(entity.get());
|
||||
CreateComponent<Terrain::TerrainLayerSpawnerComponent>(entity.get());
|
||||
// Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated.
|
||||
|
||||
m_boxShapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockBoxShapeComponentRequests>>(entity->GetId());
|
||||
m_shapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockShapeComponentRequests>>(entity->GetId());
|
||||
NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
|
||||
EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1));
|
||||
EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1));
|
||||
|
||||
// Set up the box shape to return whatever spawnerBox was passed in.
|
||||
ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox));
|
||||
|
||||
// Set up a mock height provider to use the passed-in mock height function to generate a height.
|
||||
m_terrainAreaHeightRequests = AZStd::make_unique<NiceMock<UnitTest::MockTerrainAreaHeightRequests>>(entity->GetId());
|
||||
ON_CALL(*m_terrainAreaHeightRequests, GetHeight)
|
||||
.WillByDefault(
|
||||
[mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists)
|
||||
{
|
||||
// By default, set the outPosition to the input position and terrain to always exist.
|
||||
outPosition = inPosition;
|
||||
terrainExists = true;
|
||||
// Let the test function modify these values based on the needs of the specific test.
|
||||
mockHeights(outPosition, terrainExists);
|
||||
});
|
||||
|
||||
ActivateEntity(entity.get());
|
||||
return entity;
|
||||
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
||||
m_terrainSystem->Activate();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TerrainSystemTest, TrivialCreateDestroy)
|
||||
{
|
||||
// Trivially verify that the terrain system can successfully be constructed and destructed without errors.
|
||||
|
||||
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, TrivialActivateDeactivate)
|
||||
{
|
||||
// Verify that the terrain system can be activated and deactivated without errors.
|
||||
|
||||
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
||||
m_terrainSystem->Activate();
|
||||
m_terrainSystem->Deactivate();
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation)
|
||||
{
|
||||
// Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated.
|
||||
|
||||
NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
|
||||
EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1));
|
||||
EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1));
|
||||
|
||||
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
||||
m_terrainSystem->Activate();
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation)
|
||||
{
|
||||
// Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated.
|
||||
|
||||
NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
|
||||
EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1));
|
||||
EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1));
|
||||
|
||||
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
||||
m_terrainSystem->Activate();
|
||||
m_terrainSystem->Deactivate();
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered)
|
||||
{
|
||||
// For the terrain system, terrain should only exist where terrain layer spawners are present.
|
||||
|
||||
// Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point
|
||||
// will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns
|
||||
// a normal facing up the Z axis.
|
||||
|
||||
// Create and activate the terrain system with our testing defaults for world bounds and query resolution.
|
||||
CreateAndActivateTerrainSystem();
|
||||
|
||||
AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb();
|
||||
|
||||
// Loop through several points within the world bounds, including on the edges, and verify that they all return false for
|
||||
// terrainExists with default heights and normals.
|
||||
for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f))
|
||||
TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation)
|
||||
{
|
||||
for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f))
|
||||
{
|
||||
AZ::Vector3 position(x, y, 0.0f);
|
||||
bool terrainExists = true;
|
||||
float height = m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
|
||||
EXPECT_FALSE(terrainExists);
|
||||
EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ());
|
||||
// Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated.
|
||||
|
||||
terrainExists = true;
|
||||
AZ::Vector3 normal = m_terrainSystem->GetNormal(
|
||||
position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
|
||||
EXPECT_FALSE(terrainExists);
|
||||
EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ());
|
||||
NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
|
||||
EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1));
|
||||
EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1));
|
||||
|
||||
bool isHole = m_terrainSystem->GetIsHoleFromFloats(
|
||||
position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
|
||||
EXPECT_TRUE(isHole);
|
||||
}
|
||||
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
||||
m_terrainSystem->Activate();
|
||||
m_terrainSystem->Deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds)
|
||||
{
|
||||
// Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the TerrainLayerSpawner
|
||||
// is defined.
|
||||
|
||||
// The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the
|
||||
// TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region.
|
||||
|
||||
// Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5.
|
||||
constexpr float spawnerHeight = 5.0f;
|
||||
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f);
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
spawnerBox,
|
||||
[](AZ::Vector3& position, bool& terrainExists)
|
||||
{
|
||||
position.SetZ(spawnerHeight);
|
||||
terrainExists = true;
|
||||
});
|
||||
|
||||
// Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it.
|
||||
|
||||
// Create and activate the terrain system with our testing defaults for world bounds and query resolution.
|
||||
CreateAndActivateTerrainSystem();
|
||||
|
||||
// Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain
|
||||
// terrain and the expected height & normal values, and points outside the layer box don't contain terrain.
|
||||
const AZ::Aabb encompassingBox =
|
||||
AZ::Aabb::CreateFromMinMax(spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f),
|
||||
spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f));
|
||||
|
||||
for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f)
|
||||
TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered)
|
||||
{
|
||||
for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f)
|
||||
{
|
||||
AZ::Vector3 position(x, y, 0.0f);
|
||||
bool heightQueryTerrainExists = false;
|
||||
float height =
|
||||
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists);
|
||||
bool isHole = m_terrainSystem->GetIsHoleFromFloats(
|
||||
position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
|
||||
// For the terrain system, terrain should only exist where terrain layer spawners are present.
|
||||
|
||||
if (spawnerBox.Contains(AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ())))
|
||||
// Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point
|
||||
// will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns
|
||||
// a normal facing up the Z axis.
|
||||
|
||||
// Create and activate the terrain system with our testing defaults for world bounds and query resolution.
|
||||
CreateAndActivateTerrainSystem();
|
||||
|
||||
AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb();
|
||||
|
||||
// Loop through several points within the world bounds, including on the edges, and verify that they all return false for
|
||||
// terrainExists with default heights and normals.
|
||||
for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f))
|
||||
{
|
||||
for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f))
|
||||
{
|
||||
EXPECT_TRUE(heightQueryTerrainExists);
|
||||
EXPECT_FALSE(isHole);
|
||||
EXPECT_FLOAT_EQ(height, spawnerHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
EXPECT_FALSE(heightQueryTerrainExists);
|
||||
AZ::Vector3 position(x, y, 0.0f);
|
||||
bool terrainExists = true;
|
||||
float height =
|
||||
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
|
||||
EXPECT_FALSE(terrainExists);
|
||||
EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ());
|
||||
|
||||
terrainExists = true;
|
||||
AZ::Vector3 normal =
|
||||
m_terrainSystem->GetNormal(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
|
||||
EXPECT_FALSE(terrainExists);
|
||||
EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ());
|
||||
|
||||
bool isHole = m_terrainSystem->GetIsHoleFromFloats(
|
||||
position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
|
||||
EXPECT_TRUE(isHole);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid)
|
||||
{
|
||||
// Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact
|
||||
// requested location, instead of the position being quantized to the height query grid.
|
||||
|
||||
// Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave
|
||||
// using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0
|
||||
// every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity.
|
||||
constexpr float amplitudeMeters = 10.0f;
|
||||
constexpr float frequencyMeters = 1.0f;
|
||||
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f);
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
spawnerBox,
|
||||
[](AZ::Vector3& position, bool& terrainExists)
|
||||
{
|
||||
position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters)));
|
||||
terrainExists = true;
|
||||
});
|
||||
|
||||
// Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches
|
||||
// the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0.
|
||||
const AZ::Vector2 queryResolution(frequencyMeters);
|
||||
CreateAndActivateTerrainSystem(queryResolution);
|
||||
|
||||
// Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the
|
||||
// query resolution, or with the 0 points on the sine wave.
|
||||
const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) };
|
||||
for (auto& nonZeroPoint : nonZeroPoints)
|
||||
TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds)
|
||||
{
|
||||
// Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the
|
||||
// TerrainLayerSpawner is defined.
|
||||
|
||||
// The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the
|
||||
// TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region.
|
||||
|
||||
// Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5.
|
||||
constexpr float spawnerHeight = 5.0f;
|
||||
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f);
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
spawnerBox,
|
||||
[](AZ::Vector3& position, bool& terrainExists)
|
||||
{
|
||||
position.SetZ(spawnerHeight);
|
||||
terrainExists = true;
|
||||
});
|
||||
|
||||
// Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it.
|
||||
|
||||
// Create and activate the terrain system with our testing defaults for world bounds and query resolution.
|
||||
CreateAndActivateTerrainSystem();
|
||||
|
||||
// Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain
|
||||
// terrain and the expected height & normal values, and points outside the layer box don't contain terrain.
|
||||
const AZ::Aabb encompassingBox = AZ::Aabb::CreateFromMinMax(
|
||||
spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f));
|
||||
|
||||
for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f)
|
||||
{
|
||||
for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f)
|
||||
{
|
||||
AZ::Vector3 position(x, y, 0.0f);
|
||||
bool heightQueryTerrainExists = false;
|
||||
float height = m_terrainSystem->GetHeight(
|
||||
position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists);
|
||||
bool isHole = m_terrainSystem->GetIsHoleFromFloats(
|
||||
position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
|
||||
|
||||
if (spawnerBox.Contains(AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ())))
|
||||
{
|
||||
EXPECT_TRUE(heightQueryTerrainExists);
|
||||
EXPECT_FALSE(isHole);
|
||||
EXPECT_FLOAT_EQ(height, spawnerHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
EXPECT_FALSE(heightQueryTerrainExists);
|
||||
EXPECT_TRUE(isHole);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid)
|
||||
{
|
||||
// Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact
|
||||
// requested location, instead of the position being quantized to the height query grid.
|
||||
|
||||
// Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave
|
||||
// using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0
|
||||
// every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity.
|
||||
constexpr float amplitudeMeters = 10.0f;
|
||||
constexpr float frequencyMeters = 1.0f;
|
||||
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f);
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
spawnerBox,
|
||||
[](AZ::Vector3& position, bool& terrainExists)
|
||||
{
|
||||
position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters)));
|
||||
terrainExists = true;
|
||||
});
|
||||
|
||||
// Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches
|
||||
// the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0.
|
||||
const AZ::Vector2 queryResolution(frequencyMeters);
|
||||
CreateAndActivateTerrainSystem(queryResolution);
|
||||
|
||||
// Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the
|
||||
// query resolution, or with the 0 points on the sine wave.
|
||||
const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) };
|
||||
for (auto& nonZeroPoint : nonZeroPoints)
|
||||
{
|
||||
AZ::Vector3 position(nonZeroPoint.GetX(), nonZeroPoint.GetY(), 0.0f);
|
||||
bool heightQueryTerrainExists = false;
|
||||
float height =
|
||||
@@ -311,165 +312,253 @@ TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid)
|
||||
// We've chosen a bunch of places on the sine wave that should return a non-zero positive or negative value.
|
||||
constexpr float epsilon = 0.0001f;
|
||||
EXPECT_GT(fabsf(height), epsilon);
|
||||
}
|
||||
}
|
||||
|
||||
// Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on the
|
||||
// sine wave, regardless of whether or not they align to the query resolution.
|
||||
const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) };
|
||||
for (auto& zeroPoint : zeroPoints)
|
||||
{
|
||||
AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f);
|
||||
bool heightQueryTerrainExists = false;
|
||||
float height =
|
||||
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists);
|
||||
|
||||
constexpr float epsilon = 0.0001f;
|
||||
EXPECT_NEAR(height, 0.0f, epsilon);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid)
|
||||
{
|
||||
// Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching
|
||||
// the height.
|
||||
|
||||
// Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal
|
||||
// to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result.
|
||||
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
spawnerBox,
|
||||
[](AZ::Vector3& position, bool& terrainExists)
|
||||
// Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on
|
||||
// the sine wave, regardless of whether or not they align to the query resolution.
|
||||
const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) };
|
||||
for (auto& zeroPoint : zeroPoints)
|
||||
{
|
||||
position.SetZ(position.GetX() + position.GetY());
|
||||
terrainExists = true;
|
||||
});
|
||||
AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f);
|
||||
bool heightQueryTerrainExists = false;
|
||||
float height =
|
||||
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists);
|
||||
|
||||
// Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter intervals.
|
||||
const AZ::Vector2 queryResolution(0.25f);
|
||||
CreateAndActivateTerrainSystem(queryResolution);
|
||||
|
||||
// Test some points and verify that the results always go "downward", whether they're in positive or negative space.
|
||||
// (Z contains the the expected result for convenience).
|
||||
const HeightTestPoint testPoints[] =
|
||||
{
|
||||
{ AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00
|
||||
{ AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25
|
||||
{ AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75
|
||||
{ AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50
|
||||
{ AZ::Vector2(7.7f, 7.7f), 15.0f }, // Should return a height of 7.50 + 7.50
|
||||
|
||||
{ AZ::Vector2(-0.3f, -0.3f), -1.0f }, // Should return a height of -0.50 + -0.50
|
||||
{ AZ::Vector2(-2.8f, -2.8f), -6.0f }, // Should return a height of -3.00 + -3.00
|
||||
{ AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50
|
||||
{ AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75
|
||||
};
|
||||
for (auto& testPoint : testPoints)
|
||||
{
|
||||
const float expectedHeight = testPoint.m_expectedHeight;
|
||||
|
||||
AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f);
|
||||
bool heightQueryTerrainExists = false;
|
||||
float height =
|
||||
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists);
|
||||
|
||||
constexpr float epsilon = 0.0001f;
|
||||
EXPECT_NEAR(height, expectedHeight, epsilon);
|
||||
constexpr float epsilon = 0.0001f;
|
||||
EXPECT_NEAR(height, 0.0f, epsilon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate)
|
||||
{
|
||||
// Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid.
|
||||
TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid)
|
||||
{
|
||||
// Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching
|
||||
// the height.
|
||||
|
||||
// Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal
|
||||
// to the X + Y position, so we'll have heights that look like this on our grid:
|
||||
// 0 *---* 1
|
||||
// | |
|
||||
// 1 *---* 2
|
||||
// However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid
|
||||
// points, but it will noticeably affect the expected height values if any points get sampled in-between grid points.
|
||||
// Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal
|
||||
// to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result.
|
||||
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
spawnerBox,
|
||||
[](AZ::Vector3& position, bool& terrainExists)
|
||||
{
|
||||
position.SetZ(position.GetX() + position.GetY());
|
||||
terrainExists = true;
|
||||
});
|
||||
|
||||
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
|
||||
const float amplitudeMeters = 10.0f;
|
||||
const float frequencyMeters = 1.0f;
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
spawnerBox,
|
||||
[amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists)
|
||||
// Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter
|
||||
// intervals.
|
||||
const AZ::Vector2 queryResolution(0.25f);
|
||||
CreateAndActivateTerrainSystem(queryResolution);
|
||||
|
||||
// Test some points and verify that the results always go "downward", whether they're in positive or negative space.
|
||||
// (Z contains the the expected result for convenience).
|
||||
const HeightTestPoint testPoints[] = {
|
||||
{ AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00
|
||||
{ AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25
|
||||
{ AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75
|
||||
{ AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50
|
||||
{ AZ::Vector2(7.7f, 7.7f), 15.0f }, // Should return a height of 7.50 + 7.50
|
||||
|
||||
{ AZ::Vector2(-0.3f, -0.3f), -1.0f }, // Should return a height of -0.50 + -0.50
|
||||
{ AZ::Vector2(-2.8f, -2.8f), -6.0f }, // Should return a height of -3.00 + -3.00
|
||||
{ AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50
|
||||
{ AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75
|
||||
};
|
||||
for (auto& testPoint : testPoints)
|
||||
{
|
||||
// Our generated height will be X + Y.
|
||||
float expectedHeight = position.GetX() + position.GetY();
|
||||
const float expectedHeight = testPoint.m_expectedHeight;
|
||||
|
||||
// If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height.
|
||||
// This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries.
|
||||
float unexpectedVariance = amplitudeMeters *
|
||||
(fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters));
|
||||
position.SetZ(expectedHeight + unexpectedVariance);
|
||||
terrainExists = true;
|
||||
});
|
||||
AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f);
|
||||
bool heightQueryTerrainExists = false;
|
||||
float height =
|
||||
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists);
|
||||
|
||||
// Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
|
||||
const AZ::Vector2 queryResolution(frequencyMeters);
|
||||
CreateAndActivateTerrainSystem(queryResolution);
|
||||
|
||||
// Test some points and verify that the results are the expected bilinear filtered result,
|
||||
// whether they're in positive or negative space.
|
||||
// (Z contains the the expected result for convenience).
|
||||
const HeightTestPoint testPoints[] = {
|
||||
|
||||
// Queries directly on grid points. These should return values of X + Y.
|
||||
{ AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0
|
||||
{ AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0
|
||||
{ AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1
|
||||
{ AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1
|
||||
{ AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5
|
||||
|
||||
{ AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0
|
||||
{ AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1
|
||||
{ AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1
|
||||
{ AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5
|
||||
|
||||
// Queries that are on a grid edge (one axis on the grid, the other somewhere in-between).
|
||||
// These should just be a linear interpolation of the points, so it should still be X + Y.
|
||||
|
||||
{ AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0
|
||||
{ AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0
|
||||
{ AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25
|
||||
{ AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75
|
||||
|
||||
{ AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75
|
||||
{ AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4
|
||||
|
||||
{ AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0
|
||||
{ AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0
|
||||
{ AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25
|
||||
{ AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75
|
||||
|
||||
{ AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75
|
||||
{ AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4
|
||||
|
||||
// Queries inside a grid square (both axes are in-between grid points)
|
||||
// This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values
|
||||
// should *still* be X + Y assuming the points were sampled correctly from the grid points.
|
||||
|
||||
{ AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25
|
||||
{ AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74
|
||||
|
||||
{ AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25
|
||||
{ AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74
|
||||
};
|
||||
|
||||
// Loop through every test point and validate it.
|
||||
for (auto& testPoint : testPoints)
|
||||
{
|
||||
const float expectedHeight = testPoint.m_expectedHeight;
|
||||
|
||||
AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f);
|
||||
bool heightQueryTerrainExists = false;
|
||||
float height =
|
||||
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists);
|
||||
|
||||
// Verify that our height query returned the bilinear filtered result we expect.
|
||||
constexpr float epsilon = 0.0001f;
|
||||
EXPECT_NEAR(height, expectedHeight, epsilon);
|
||||
constexpr float epsilon = 0.0001f;
|
||||
EXPECT_NEAR(height, expectedHeight, epsilon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate)
|
||||
{
|
||||
// Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid.
|
||||
|
||||
// Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal
|
||||
// to the X + Y position, so we'll have heights that look like this on our grid:
|
||||
// 0 *---* 1
|
||||
// | |
|
||||
// 1 *---* 2
|
||||
// However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid
|
||||
// points, but it will noticeably affect the expected height values if any points get sampled in-between grid points.
|
||||
|
||||
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
|
||||
const float amplitudeMeters = 10.0f;
|
||||
const float frequencyMeters = 1.0f;
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
spawnerBox,
|
||||
[amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists)
|
||||
{
|
||||
// Our generated height will be X + Y.
|
||||
float expectedHeight = position.GetX() + position.GetY();
|
||||
|
||||
// If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height.
|
||||
// This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries.
|
||||
float unexpectedVariance =
|
||||
amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters));
|
||||
position.SetZ(expectedHeight + unexpectedVariance);
|
||||
terrainExists = true;
|
||||
});
|
||||
|
||||
// Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
|
||||
const AZ::Vector2 queryResolution(frequencyMeters);
|
||||
CreateAndActivateTerrainSystem(queryResolution);
|
||||
|
||||
// Test some points and verify that the results are the expected bilinear filtered result,
|
||||
// whether they're in positive or negative space.
|
||||
// (Z contains the the expected result for convenience).
|
||||
const HeightTestPoint testPoints[] = {
|
||||
|
||||
// Queries directly on grid points. These should return values of X + Y.
|
||||
{ AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0
|
||||
{ AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0
|
||||
{ AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1
|
||||
{ AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1
|
||||
{ AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5
|
||||
|
||||
{ AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0
|
||||
{ AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1
|
||||
{ AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1
|
||||
{ AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5
|
||||
|
||||
// Queries that are on a grid edge (one axis on the grid, the other somewhere in-between).
|
||||
// These should just be a linear interpolation of the points, so it should still be X + Y.
|
||||
|
||||
{ AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0
|
||||
{ AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0
|
||||
{ AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25
|
||||
{ AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75
|
||||
|
||||
{ AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75
|
||||
{ AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4
|
||||
|
||||
{ AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0
|
||||
{ AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0
|
||||
{ AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25
|
||||
{ AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75
|
||||
|
||||
{ AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75
|
||||
{ AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4
|
||||
|
||||
// Queries inside a grid square (both axes are in-between grid points)
|
||||
// This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values
|
||||
// should *still* be X + Y assuming the points were sampled correctly from the grid points.
|
||||
|
||||
{ AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25
|
||||
{ AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74
|
||||
|
||||
{ AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25
|
||||
{ AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74
|
||||
};
|
||||
|
||||
// Loop through every test point and validate it.
|
||||
for (auto& testPoint : testPoints)
|
||||
{
|
||||
const float expectedHeight = testPoint.m_expectedHeight;
|
||||
|
||||
AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f);
|
||||
bool heightQueryTerrainExists = false;
|
||||
float height = m_terrainSystem->GetHeight(
|
||||
position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists);
|
||||
|
||||
// Verify that our height query returned the bilinear filtered result we expect.
|
||||
constexpr float epsilon = 0.0001f;
|
||||
EXPECT_NEAR(height, expectedHeight, epsilon);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, GetSurfaceWeightsReturnsAllValidSurfaceWeights)
|
||||
{
|
||||
CreateAndActivateTerrainSystem();
|
||||
|
||||
const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne());
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
aabb,
|
||||
[](AZ::Vector3& position, bool& terrainExists)
|
||||
{
|
||||
position.SetZ(1.0f);
|
||||
terrainExists = true;
|
||||
});
|
||||
|
||||
const AZ::Crc32 tag1("tag1");
|
||||
const AZ::Crc32 tag2("tag2");
|
||||
|
||||
AzFramework::SurfaceData::OrderedSurfaceTagWeightSet orderedSurfaceWeights;
|
||||
|
||||
AzFramework::SurfaceData::SurfaceTagWeight tagWeight1;
|
||||
tagWeight1.m_surfaceType = tag1;
|
||||
tagWeight1.m_weight = 1.0f;
|
||||
orderedSurfaceWeights.emplace(tagWeight1);
|
||||
|
||||
AzFramework::SurfaceData::SurfaceTagWeight tagWeight2;
|
||||
tagWeight2.m_surfaceType = tag2;
|
||||
tagWeight2.m_weight = 0.8f;
|
||||
orderedSurfaceWeights.emplace(tagWeight2);
|
||||
|
||||
NiceMock<UnitTest::MockTerrainAreaSurfaceRequestBus> mockSurfaceRequests(entity->GetId());
|
||||
ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights));
|
||||
|
||||
AzFramework::SurfaceData::OrderedSurfaceTagWeightSet outSurfaceWeights;
|
||||
|
||||
// Asking for values outside the layer spawner bounds, should result in no results.
|
||||
m_terrainSystem->GetSurfaceWeights(aabb.GetMax() + AZ::Vector3::CreateOne(), outSurfaceWeights);
|
||||
EXPECT_TRUE(outSurfaceWeights.empty());
|
||||
|
||||
// Inside the layer spawner box should give us both the added surface weights.
|
||||
m_terrainSystem->GetSurfaceWeights(aabb.GetCenter(), outSurfaceWeights);
|
||||
|
||||
EXPECT_EQ(outSurfaceWeights.size(), 2);
|
||||
}
|
||||
|
||||
TEST_F(TerrainSystemTest, GetMaxSurfaceWeightsReturnsBiggestValidSurfaceWeight)
|
||||
{
|
||||
CreateAndActivateTerrainSystem();
|
||||
|
||||
const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne());
|
||||
auto entity = CreateAndActivateMockTerrainLayerSpawner(
|
||||
aabb,
|
||||
[](AZ::Vector3& position, bool& terrainExists)
|
||||
{
|
||||
position.SetZ(1.0f);
|
||||
terrainExists = true;
|
||||
});
|
||||
|
||||
const AZ::Crc32 tag1("tag1");
|
||||
const AZ::Crc32 tag2("tag2");
|
||||
|
||||
AzFramework::SurfaceData::OrderedSurfaceTagWeightSet orderedSurfaceWeights;
|
||||
|
||||
AzFramework::SurfaceData::SurfaceTagWeight tagWeight1;
|
||||
tagWeight1.m_surfaceType = tag1;
|
||||
tagWeight1.m_weight = 1.0f;
|
||||
orderedSurfaceWeights.emplace(tagWeight1);
|
||||
|
||||
AzFramework::SurfaceData::SurfaceTagWeight tagWeight2;
|
||||
tagWeight2.m_surfaceType = tag2;
|
||||
tagWeight2.m_weight = 0.8f;
|
||||
orderedSurfaceWeights.emplace(tagWeight2);
|
||||
|
||||
NiceMock<UnitTest::MockTerrainAreaSurfaceRequestBus> mockSurfaceRequests(entity->GetId());
|
||||
ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights));
|
||||
|
||||
// Asking for values outside the layer spawner bounds, should result in an invalid result.
|
||||
AzFramework::SurfaceData::SurfaceTagWeight tagWeight =
|
||||
m_terrainSystem->GetMaxSurfaceWeight(aabb.GetMax() + AZ::Vector3::CreateOne());
|
||||
|
||||
EXPECT_EQ(tagWeight.m_surfaceType, AZ::Crc32(AzFramework::SurfaceData::Constants::s_unassignedTagName));
|
||||
|
||||
// Inside the layer spawner box should give us the highest weighted tag (tag1).
|
||||
tagWeight = m_terrainSystem->GetMaxSurfaceWeight(aabb.GetCenter());
|
||||
|
||||
EXPECT_EQ(tagWeight.m_surfaceType, tagWeight1.m_surfaceType);
|
||||
EXPECT_NEAR(tagWeight.m_weight, tagWeight1.m_weight, 0.01f);
|
||||
}
|
||||
} // namespace UnitTest
|
||||
|
||||
@@ -11,6 +11,8 @@ set(FILES
|
||||
Source/EditorComponents/EditorTerrainHeightGradientListComponent.h
|
||||
Source/EditorComponents/EditorTerrainLayerSpawnerComponent.cpp
|
||||
Source/EditorComponents/EditorTerrainLayerSpawnerComponent.h
|
||||
Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp
|
||||
Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h
|
||||
Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.cpp
|
||||
Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.h
|
||||
Source/EditorComponents/EditorTerrainWorldComponent.cpp
|
||||
|
||||
@@ -12,6 +12,8 @@ set(FILES
|
||||
Source/Components/TerrainHeightGradientListComponent.h
|
||||
Source/Components/TerrainLayerSpawnerComponent.cpp
|
||||
Source/Components/TerrainLayerSpawnerComponent.h
|
||||
Source/Components/TerrainPhysicsColliderComponent.cpp
|
||||
Source/Components/TerrainPhysicsColliderComponent.h
|
||||
Source/Components/TerrainSurfaceDataSystemComponent.cpp
|
||||
Source/Components/TerrainSurfaceDataSystemComponent.h
|
||||
Source/Components/TerrainSurfaceGradientListComponent.cpp
|
||||
|
||||
@@ -8,4 +8,6 @@
|
||||
|
||||
set(FILES
|
||||
Mocks/Terrain/MockTerrain.h
|
||||
Mocks/Terrain/MockTerrainLayerSpawner.h
|
||||
Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h
|
||||
)
|
||||
|
||||
@@ -10,6 +10,9 @@ set(FILES
|
||||
Tests/TerrainTest.cpp
|
||||
Tests/TerrainSystemTest.cpp
|
||||
Tests/LayerSpawnerTests.cpp
|
||||
Tests/TerrainPhysicsColliderTests.cpp
|
||||
Tests/SurfaceMaterialsListTest.cpp
|
||||
Tests/MockAxisAlignedBoxShapeComponent.h
|
||||
Tests/TerrainHeightGradientListTests.cpp
|
||||
Tests/TerrainSurfaceGradientListTests.cpp
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user