Terrain Physics Heightfield support

* New Heightfield Components

Signed-off-by: John Jones-Steele <jjjoness@amazon.com>

* Misc PR fixes
* Fixed linux build failure from bad #include
* Renamed "Terrain Physics Collider" to "Terrain Physics Heightfield Collider" per physics team feedback
* Fixed 1/5 -> 1/4 typo in a comment
* Added missing member copies in HeightfieldShapeConfiguration

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Addressed PR feedback

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Changes from review

Signed-off-by: John Jones-Steele <jjjoness@amazon.com>

* Remove tabs accidently added

Signed-off-by: John Jones-Steele <jjjoness@amazon.com>

* Fixed overly complicated scaling math.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Added comments to make it more obvious what's happening on CreateEnd / DestroyBegin.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Moved Heightfield CreatePxGeometryFromConfig into its own function

Signed-off-by: John Jones-Steele <jjjoness@amazon.com>

Co-authored-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>
monroegm-disable-blank-issue-2
John Jones-Steele 4 years ago committed by GitHub
parent 775dda8ed2
commit 3d67be162c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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(Sphere);
REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder); REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder);
REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset); REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset);
REFLECT_SHAPETYPE_ENUM_VALUE(Heightfield);
#undef REFLECT_SHAPETYPE_ENUM_VALUE #undef REFLECT_SHAPETYPE_ENUM_VALUE
} }
@ -305,4 +306,125 @@ namespace Physics
m_cachedNativeMesh = nullptr; 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 #pragma once
#include <AzCore/Math/Vector3.h> #include <AzCore/Math/Vector3.h>
#include <AzCore/Math/Vector2.h>
#include <AzCore/Math/Quaternion.h> #include <AzCore/Math/Quaternion.h>
#include <AzCore/Asset/AssetCommon.h> #include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Serialization/SerializeContext.h> #include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Physics/HeightfieldProviderBus.h>
namespace Physics namespace Physics
{ {
/// Used to identify shape configuration type from base class. /// 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. Native, ///< Native shape configuration if user wishes to bypass generic shape configurations.
PhysicsAsset, ///< Shapes configured in the asset. PhysicsAsset, ///< Shapes configured in the asset.
CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine. CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine.
Heightfield ///< Interacts with the physics system heightfield
}; };
class ShapeConfiguration class ShapeConfiguration
@ -196,4 +200,52 @@ namespace Physics
mutable void* m_cachedNativeMesh = nullptr; 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 } // namespace Physics

@ -132,6 +132,10 @@ namespace Physics
virtual AZStd::shared_ptr<Material> CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) = 0; 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. /// Releases the mesh object created by the physics backend.
/// @param nativeMeshObject Pointer to the mesh object. /// @param nativeMeshObject Pointer to the mesh object.
virtual void ReleaseNativeMeshObject(void* nativeMeshObject) = 0; virtual void ReleaseNativeMeshObject(void* nativeMeshObject) = 0;

@ -107,6 +107,7 @@ namespace Physics
PhysicsAssetShapeConfiguration::Reflect(context); PhysicsAssetShapeConfiguration::Reflect(context);
NativeShapeConfiguration::Reflect(context); NativeShapeConfiguration::Reflect(context);
CookedMeshShapeConfiguration::Reflect(context); CookedMeshShapeConfiguration::Reflect(context);
HeightfieldShapeConfiguration::Reflect(context);
AzPhysics::SystemInterface::Reflect(context); AzPhysics::SystemInterface::Reflect(context);
AzPhysics::Scene::Reflect(context); AzPhysics::Scene::Reflect(context);
AzPhysics::CollisionLayer::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/SimulatedBodyConfiguration.cpp
Physics/Configuration/SystemConfiguration.h Physics/Configuration/SystemConfiguration.h
Physics/Configuration/SystemConfiguration.cpp Physics/Configuration/SystemConfiguration.cpp
Physics/HeightfieldProviderBus.h
Physics/SimulatedBodies/RigidBody.h Physics/SimulatedBodies/RigidBody.h
Physics/SimulatedBodies/RigidBody.cpp Physics/SimulatedBodies/RigidBody.cpp
Physics/SimulatedBodies/StaticRigidBody.h Physics/SimulatedBodies/StaticRigidBody.h
@ -251,6 +252,7 @@ set(FILES
Physics/Shape.h Physics/Shape.h
Physics/ShapeConfiguration.h Physics/ShapeConfiguration.h
Physics/ShapeConfiguration.cpp Physics/ShapeConfiguration.cpp
Physics/HeightfieldProviderBus.h
Physics/SystemBus.h Physics/SystemBus.h
Physics/ColliderComponentBus.h Physics/ColliderComponentBus.h
Physics/RagdollPhysicsBus.h Physics/RagdollPhysicsBus.h

@ -42,6 +42,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
NAMESPACE AZ NAMESPACE AZ
FILES_CMAKE FILES_CMAKE
Tests/framework_shared_tests_files.cmake Tests/framework_shared_tests_files.cmake
AzFramework/Physics/physics_mock_files.cmake
INCLUDE_DIRECTORIES INCLUDE_DIRECTORIES
PUBLIC PUBLIC
Tests Tests
@ -53,7 +54,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
AZ::AzTest AZ::AzTest
AZ::AzTestShared AZ::AzTestShared
) )
if(PAL_TRAIT_BUILD_HOST_TOOLS) if(PAL_TRAIT_BUILD_HOST_TOOLS)
ly_add_target( ly_add_target(

@ -209,6 +209,7 @@ namespace Blast
AZStd::shared_ptr<Physics::Shape>( AZStd::shared_ptr<Physics::Shape>(
const Physics::ColliderConfiguration&, const Physics::ShapeConfiguration&)); const Physics::ColliderConfiguration&, const Physics::ShapeConfiguration&));
MOCK_METHOD1(ReleaseNativeMeshObject, void(void*)); MOCK_METHOD1(ReleaseNativeMeshObject, void(void*));
MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void*));
MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr<Physics::Material>(const Physics::MaterialConfiguration&)); MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr<Physics::Material>(const Physics::MaterialConfiguration&));
MOCK_METHOD0(GetDefaultMaterial, AZStd::shared_ptr<Physics::Material>()); MOCK_METHOD0(GetDefaultMaterial, AZStd::shared_ptr<Physics::Material>());
MOCK_METHOD1( 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_METHOD2(CreateShape, AZStd::shared_ptr<Physics::Shape>(const Physics::ColliderConfiguration& colliderConfiguration, const Physics::ShapeConfiguration& configuration));
MOCK_METHOD1(ReleaseNativeMeshObject, void(void* nativeMeshObject)); 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_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(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)); MOCK_METHOD3(CookConvexMeshToMemory, bool(const AZ::Vector3* vertices, AZ::u32 vertexCount, AZStd::vector<AZ::u8>& result));

@ -107,7 +107,16 @@ endif()
# Tests # Tests
################################################################################ ################################################################################
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) 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( ly_add_target(
NAME GradientSignal.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAME GradientSignal.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE Gem NAMESPACE Gem
@ -122,6 +131,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
AZ::AzTest AZ::AzTest
Gem::GradientSignal.Static Gem::GradientSignal.Static
Gem::LmbrCentral Gem::LmbrCentral
Gem::GradientSignal.Mocks
) )
ly_add_googletest( ly_add_googletest(
NAME Gem::GradientSignal.Tests 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 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. /// Provide a Component interface for AxisAlignedBoxShape functionality.
class AxisAlignedBoxShapeComponent class AxisAlignedBoxShapeComponent
: public AZ::Component : public AZ::Component

@ -24,6 +24,12 @@ namespace LmbrCentral
/// Type ID for the BoxShapeConfig /// Type ID for the BoxShapeConfig
static const AZ::Uuid BoxShapeConfigTypeId = "{F034FBA2-AC2F-4E66-8152-14DFB90D6283}"; 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 /// Configuration data for BoxShapeComponent
class BoxShapeConfig class BoxShapeConfig
: public ShapeComponentConfig : 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 const AZ::Vector3& colliderScale) const
{ {
// Apply entity world transform scale to collider offset // Apply entity world transform scale to collider offset

@ -104,7 +104,15 @@ namespace PhysX
const AZ::Vector3& meshScale, const AZ::Vector3& meshScale,
AZ::u32 geomIndex) const; 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; const Physics::ColliderConfiguration& colliderConfig, const AZStd::vector<AZ::Vector3>& points) const;
AZ::Transform GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig, AZ::Transform GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig,

@ -16,19 +16,21 @@ namespace AzPhysics
{ {
class CollisionGroup; class CollisionGroup;
class CollisionLayer; class CollisionLayer;
} } // namespace AzPhysics
namespace physx namespace physx
{ {
class PxScene; class PxScene;
class PxSceneDesc; class PxSceneDesc;
class PxConvexMesh; class PxConvexMesh;
class PxHeightField;
class PxTriangleMesh; class PxTriangleMesh;
class PxShape; class PxShape;
class PxCooking; class PxCooking;
class PxControllerManager; class PxControllerManager;
struct PxFilterData; struct PxFilterData;
} struct PxHeightFieldSample;
} // namespace physx
namespace PhysX namespace PhysX
{ {
@ -63,6 +65,13 @@ namespace PhysX
/// @return Pointer to the created mesh. /// @return Pointer to the created mesh.
virtual physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) = 0; 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. /// Creates PhysX collision filter data from generic collision filtering settings.
/// @param layer The collision layer the object belongs to. /// @param layer The collision layer the object belongs to.
/// @param group The set of collision layers the object will interact with. /// @param group The set of collision layers the object will interact with.

@ -12,6 +12,7 @@
#include <Source/BaseColliderComponent.h> #include <Source/BaseColliderComponent.h>
#include <Source/MeshColliderComponent.h> #include <Source/MeshColliderComponent.h>
#include <Source/BoxColliderComponent.h> #include <Source/BoxColliderComponent.h>
#include <Source/HeightfieldColliderComponent.h>
#include <Source/SphereColliderComponent.h> #include <Source/SphereColliderComponent.h>
#include <Source/CapsuleColliderComponent.h> #include <Source/CapsuleColliderComponent.h>
#include <Source/ShapeColliderComponent.h> #include <Source/ShapeColliderComponent.h>
@ -36,6 +37,7 @@ namespace PhysX
BaseColliderComponent::CreateDescriptor(), BaseColliderComponent::CreateDescriptor(),
MeshColliderComponent::CreateDescriptor(), MeshColliderComponent::CreateDescriptor(),
BoxColliderComponent::CreateDescriptor(), BoxColliderComponent::CreateDescriptor(),
HeightfieldColliderComponent::CreateDescriptor(),
SphereColliderComponent::CreateDescriptor(), SphereColliderComponent::CreateDescriptor(),
CapsuleColliderComponent::CreateDescriptor(), CapsuleColliderComponent::CreateDescriptor(),
ShapeColliderComponent::CreateDescriptor(), ShapeColliderComponent::CreateDescriptor(),

@ -12,6 +12,7 @@
#include <Source/EditorColliderComponent.h> #include <Source/EditorColliderComponent.h>
#include <Source/EditorFixedJointComponent.h> #include <Source/EditorFixedJointComponent.h>
#include <Source/EditorForceRegionComponent.h> #include <Source/EditorForceRegionComponent.h>
#include <Source/EditorHeightfieldColliderComponent.h>
#include <Source/EditorHingeJointComponent.h> #include <Source/EditorHingeJointComponent.h>
#include <Source/EditorJointComponent.h> #include <Source/EditorJointComponent.h>
#include <Source/EditorRigidBodyComponent.h> #include <Source/EditorRigidBodyComponent.h>
@ -35,6 +36,7 @@ namespace PhysX
EditorColliderComponent::CreateDescriptor(), EditorColliderComponent::CreateDescriptor(),
EditorFixedJointComponent::CreateDescriptor(), EditorFixedJointComponent::CreateDescriptor(),
EditorForceRegionComponent::CreateDescriptor(), EditorForceRegionComponent::CreateDescriptor(),
EditorHeightfieldColliderComponent::CreateDescriptor(),
EditorHingeJointComponent::CreateDescriptor(), EditorHingeJointComponent::CreateDescriptor(),
EditorJointComponent::CreateDescriptor(), EditorJointComponent::CreateDescriptor(),
EditorRigidBodyComponent::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; 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) bool SystemComponent::CookConvexMeshToFile(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount)
{ {
AZStd::vector<AZ::u8> physxData; AZStd::vector<AZ::u8> physxData;
@ -342,6 +358,14 @@ namespace PhysX
return AZStd::make_shared<PhysX::Material>(materialConfiguration); 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) void SystemComponent::ReleaseNativeMeshObject(void* nativeMeshObject)
{ {
if (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* 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::PxConvexMesh* CreateConvexMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) override;
physx::PxTriangleMesh* CreateTriangleMeshFromCooked(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; 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; AZStd::shared_ptr<Physics::Material> CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) override;
void ReleaseNativeMeshObject(void* nativeMeshObject) override; void ReleaseNativeMeshObject(void* nativeMeshObject) override;
void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) override;
// Assets related data // Assets related data
AZStd::vector<AZStd::unique_ptr<AZ::Data::AssetHandler>> m_assetHandlers; AZStd::vector<AZStd::unique_ptr<AZ::Data::AssetHandler>> m_assetHandlers;

@ -9,6 +9,7 @@
#include <AzCore/std/smart_ptr/make_shared.h> #include <AzCore/std/smart_ptr/make_shared.h>
#include <AzCore/Component/TransformBus.h> #include <AzCore/Component/TransformBus.h>
#include <AzCore/Component/NonUniformScaleBus.h> #include <AzCore/Component/NonUniformScaleBus.h>
#include <AzCore/Casting/lossy_cast.h>
#include <AzCore/EBus/Results.h> #include <AzCore/EBus/Results.h>
#include <AzCore/Interface/Interface.h> #include <AzCore/Interface/Interface.h>
#include <AzCore/RTTI/BehaviorContext.h> #include <AzCore/RTTI/BehaviorContext.h>
@ -25,6 +26,7 @@
#include <AzFramework/Physics/PhysicsScene.h> #include <AzFramework/Physics/PhysicsScene.h>
#include <AzFramework/Physics/PhysicsSystem.h> #include <AzFramework/Physics/PhysicsSystem.h>
#include <AzFramework/Physics/SimulatedBodies/StaticRigidBody.h> #include <AzFramework/Physics/SimulatedBodies/StaticRigidBody.h>
#include <AzFramework/Physics/HeightfieldProviderBus.h>
#include <PhysX/ColliderShapeBus.h> #include <PhysX/ColliderShapeBus.h>
#include <PhysX/SystemComponentBus.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) bool CreatePxGeometryFromConfig(const Physics::ShapeConfiguration& shapeConfiguration, physx::PxGeometryHolder& pxGeometry)
{ {
if (!shapeConfiguration.m_scale.IsGreaterThan(AZ::Vector3::CreateZero())) 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."); "Please iterate over m_colliderShapes in the asset and call this function for each of them.");
return false; return false;
} }
case Physics::ShapeType::Heightfield:
{
const Physics::HeightfieldShapeConfiguration& heightfieldConfig =
static_cast<const Physics::HeightfieldShapeConfiguration&>(shapeConfiguration);
CreatePxGeometryFromHeightfield(heightfieldConfig, pxGeometry);
break;
}
default: default:
AZ_Warning("PhysX Rigid Body", false, "Shape not supported in PhysX. Shape Type: %d", shapeType); AZ_Warning("PhysX Rigid Body", false, "Shape not supported in PhysX. Shape Type: %d", shapeType);
return false; return false;
@ -219,6 +359,26 @@ namespace PhysX
physx::PxQuat pxQuat(AZ::Constants::HalfPi, physx::PxVec3(0.0f, 1.0f, 0.0f)); physx::PxQuat pxQuat(AZ::Constants::HalfPi, physx::PxVec3(0.0f, 1.0f, 0.0f));
shape->setLocalPose(physx::PxTransform(pxQuat)); 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. // 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); shape->setFlag(physx::PxShapeFlag::eSIMULATION_SHAPE, colliderConfiguration.m_isSimulated && !colliderConfiguration.m_isTrigger);
@ -1357,6 +1517,39 @@ namespace PhysX
return entityWorldTransformWithoutScale * jointLocalTransformWithoutScale; 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 Utils
namespace ReflectionUtils namespace ReflectionUtils

@ -188,6 +188,8 @@ namespace PhysX
//! Returns defaultValue if the input is infinite or NaN, otherwise returns the input unchanged. //! 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()); 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 namespace Geometry
{ {
using PointList = AZStd::vector<AZ::Vector3>; using PointList = AZStd::vector<AZ::Vector3>;

@ -27,6 +27,8 @@ set(FILES
Source/EditorFixedJointComponent.h Source/EditorFixedJointComponent.h
Source/EditorHingeJointComponent.cpp Source/EditorHingeJointComponent.cpp
Source/EditorHingeJointComponent.h Source/EditorHingeJointComponent.h
Source/EditorHeightfieldColliderComponent.cpp
Source/EditorHeightfieldColliderComponent.h
Source/EditorJointComponent.cpp Source/EditorJointComponent.cpp
Source/EditorJointComponent.h Source/EditorJointComponent.h
Source/Pipeline/MeshExporter.cpp Source/Pipeline/MeshExporter.cpp

@ -35,6 +35,8 @@ set(FILES
Source/MeshColliderComponent.h Source/MeshColliderComponent.h
Source/BoxColliderComponent.h Source/BoxColliderComponent.h
Source/BoxColliderComponent.cpp Source/BoxColliderComponent.cpp
Source/HeightfieldColliderComponent.h
Source/HeightfieldColliderComponent.cpp
Source/SphereColliderComponent.h Source/SphereColliderComponent.h
Source/SphereColliderComponent.cpp Source/SphereColliderComponent.cpp
Source/CapsuleColliderComponent.h Source/CapsuleColliderComponent.h

@ -111,6 +111,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
AZ::AzTest AZ::AzTest
AZ::AzFramework AZ::AzFramework
Gem::LmbrCentral.Mocks Gem::LmbrCentral.Mocks
Gem::GradientSignal.Mocks
Gem::Terrain.Mocks Gem::Terrain.Mocks
Gem::Terrain.Static Gem::Terrain.Static
) )

@ -10,11 +10,12 @@
#include <gmock/gmock.h> #include <gmock/gmock.h>
#include <AzCore/Component/ComponentApplication.h> #include <AzCore/Component/ComponentApplication.h>
#include <AzFramework/Physics/HeightfieldProviderBus.h>
#include <AzFramework/Terrain/TerrainDataRequestBus.h> #include <AzFramework/Terrain/TerrainDataRequestBus.h>
#include <TerrainSystem/TerrainSystemBus.h>
namespace UnitTest namespace UnitTest
{ {
class MockTerrainSystemService : private Terrain::TerrainSystemServiceRequestBus::Handler class MockTerrainSystemService : private Terrain::TerrainSystemServiceRequestBus::Handler
{ {
public: public:
@ -69,11 +70,7 @@ namespace UnitTest
Terrain::TerrainAreaHeightRequestBus::Handler::BusDisconnect(); Terrain::TerrainAreaHeightRequestBus::Handler::BusDisconnect();
} }
MOCK_METHOD3(GetHeight, void( MOCK_METHOD3(GetHeight, void(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists));
const AZ::Vector3& inPosition,
AZ::Vector3& outPosition,
bool& terrainExists));
}; };
class MockTerrainSpawnerRequests : public Terrain::TerrainSpawnerRequestBus::Handler class MockTerrainSpawnerRequests : public Terrain::TerrainSpawnerRequestBus::Handler
@ -92,4 +89,35 @@ namespace UnitTest
MOCK_METHOD2(GetPriority, void(AZ::u32& outLayer, AZ::u32& outPriority)); MOCK_METHOD2(GetPriority, void(AZ::u32& outLayer, AZ::u32& outPriority));
MOCK_METHOD0(GetUseGroundPlane, bool()); 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) void TerrainHeightGradientListComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services)
{ {
services.push_back(AZ_CRC_CE("TerrainAreaService")); 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) void TerrainHeightGradientListComponent::Reflect(AZ::ReflectContext* context)

@ -102,7 +102,6 @@ namespace Terrain
void TerrainLayerSpawnerComponent::Activate() void TerrainLayerSpawnerComponent::Activate()
{ {
AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId());
LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(GetEntityId()); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(GetEntityId());
TerrainSpawnerRequestBus::Handler::BusConnect(GetEntityId()); TerrainSpawnerRequestBus::Handler::BusConnect(GetEntityId());
@ -114,8 +113,6 @@ namespace Terrain
TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::UnregisterArea, GetEntityId()); TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::UnregisterArea, GetEntityId());
TerrainSpawnerRequestBus::Handler::BusDisconnect(); TerrainSpawnerRequestBus::Handler::BusDisconnect();
LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect(); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect();
AZ::TransformNotificationBus::Handler::BusDisconnect();
} }
bool TerrainLayerSpawnerComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) bool TerrainLayerSpawnerComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
@ -138,13 +135,12 @@ namespace Terrain
return false; 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) 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(); RefreshArea();
} }

@ -18,7 +18,6 @@
#include <AzFramework/Terrain/TerrainDataRequestBus.h> #include <AzFramework/Terrain/TerrainDataRequestBus.h>
#include <TerrainSystem/TerrainSystemBus.h> #include <TerrainSystem/TerrainSystemBus.h>
#include <AzCore/Component/TransformBus.h>
#include <LmbrCentral/Shape/ShapeComponentBus.h> #include <LmbrCentral/Shape/ShapeComponentBus.h>
#include <AzCore/Math/Aabb.h> #include <AzCore/Math/Aabb.h>
@ -56,7 +55,6 @@ namespace Terrain
class TerrainLayerSpawnerComponent class TerrainLayerSpawnerComponent
: public AZ::Component : public AZ::Component
, private AZ::TransformNotificationBus::Handler
, private LmbrCentral::ShapeComponentNotificationsBus::Handler , private LmbrCentral::ShapeComponentNotificationsBus::Handler
, private Terrain::TerrainSpawnerRequestBus::Handler , private Terrain::TerrainSpawnerRequestBus::Handler
{ {
@ -81,10 +79,6 @@ namespace Terrain
bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override;
protected: protected:
//////////////////////////////////////////////////////////////////////////
// AZ::TransformNotificationBus::Handler
void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override;
// ShapeComponentNotificationsBus // ShapeComponentNotificationsBus
void OnShapeChanged(ShapeChangeReasons changeReason) override; 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/EditorTerrainSystemComponent.h>
#include <EditorComponents/EditorTerrainWorldComponent.h> #include <EditorComponents/EditorTerrainWorldComponent.h>
#include <EditorComponents/EditorTerrainWorldDebuggerComponent.h> #include <EditorComponents/EditorTerrainWorldDebuggerComponent.h>
#include <EditorComponents/EditorTerrainPhysicsColliderComponent.h>
#include <EditorComponents/EditorTerrainWorldRendererComponent.h> #include <EditorComponents/EditorTerrainWorldRendererComponent.h>
#include <TerrainRenderer/EditorComponents/EditorTerrainSurfaceMaterialsListComponent.h> #include <TerrainRenderer/EditorComponents/EditorTerrainSurfaceMaterialsListComponent.h>
#include <TerrainRenderer/EditorComponents/EditorTerrainMacroMaterialComponent.h> #include <TerrainRenderer/EditorComponents/EditorTerrainMacroMaterialComponent.h>
@ -32,6 +33,7 @@ namespace Terrain
Terrain::EditorTerrainSurfaceMaterialsListComponent::CreateDescriptor(), Terrain::EditorTerrainSurfaceMaterialsListComponent::CreateDescriptor(),
Terrain::EditorTerrainWorldComponent::CreateDescriptor(), Terrain::EditorTerrainWorldComponent::CreateDescriptor(),
Terrain::EditorTerrainWorldDebuggerComponent::CreateDescriptor(), Terrain::EditorTerrainWorldDebuggerComponent::CreateDescriptor(),
Terrain::EditorTerrainPhysicsColliderComponent::CreateDescriptor(),
Terrain::EditorTerrainWorldRendererComponent::CreateDescriptor(), Terrain::EditorTerrainWorldRendererComponent::CreateDescriptor(),
}); });

@ -18,6 +18,7 @@
#include <Components/TerrainLayerSpawnerComponent.h> #include <Components/TerrainLayerSpawnerComponent.h>
#include <Components/TerrainSurfaceGradientListComponent.h> #include <Components/TerrainSurfaceGradientListComponent.h>
#include <Components/TerrainSurfaceDataSystemComponent.h> #include <Components/TerrainSurfaceDataSystemComponent.h>
#include <Components/TerrainPhysicsColliderComponent.h>
#include <TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h> #include <TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h>
#include <TerrainRenderer/Components/TerrainMacroMaterialComponent.h> #include <TerrainRenderer/Components/TerrainMacroMaterialComponent.h>
@ -37,6 +38,7 @@ namespace Terrain
TerrainMacroMaterialComponent::CreateDescriptor(), TerrainMacroMaterialComponent::CreateDescriptor(),
TerrainSurfaceGradientListComponent::CreateDescriptor(), TerrainSurfaceGradientListComponent::CreateDescriptor(),
TerrainSurfaceDataSystemComponent::CreateDescriptor(), TerrainSurfaceDataSystemComponent::CreateDescriptor(),
TerrainPhysicsColliderComponent::CreateDescriptor()
}); });
} }

@ -7,6 +7,7 @@
*/ */
#include <AzCore/Component/ComponentApplication.h> #include <AzCore/Component/ComponentApplication.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/Memory/MemoryComponent.h> #include <AzCore/Memory/MemoryComponent.h>
#include <AzFramework/Terrain/TerrainDataRequestBus.h> #include <AzFramework/Terrain/TerrainDataRequestBus.h>
@ -195,8 +196,10 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerTransformChangedUpdatesTerrainSyst
m_entity->Activate(); m_entity->Activate();
AZ::TransformNotificationBus::Event( // The component gets transform change notifications via the shape bus.
m_entity->GetId(), &AZ::TransformNotificationBus::Events::OnTransformChanged, AZ::Transform(), AZ::Transform()); LmbrCentral::ShapeComponentNotificationsBus::Event(
m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged);
m_entity->Deactivate(); 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 <TerrainSystem/TerrainSystem.h>
#include <Components/TerrainLayerSpawnerComponent.h> #include <Components/TerrainLayerSpawnerComponent.h>
#include <Components/TerrainHeightGradientListComponent.h>
#include <GradientSignal/Ebuses/MockGradientRequestBus.h>
#include <Terrain/MockTerrain.h> #include <Terrain/MockTerrain.h>
#include <Terrain/MockTerrainAreaSurfaceRequestBus.h>
#include <MockAxisAlignedBoxShapeComponent.h> #include <MockAxisAlignedBoxShapeComponent.h>
using ::testing::AtLeast; using ::testing::AtLeast;
@ -25,284 +26,284 @@ using ::testing::IsFalse;
using ::testing::Ne; using ::testing::Ne;
using ::testing::NiceMock; using ::testing::NiceMock;
using ::testing::Return; using ::testing::Return;
using ::testing::SetArgReferee;
class TerrainSystemTest : public ::testing::Test namespace UnitTest
{ {
protected: class TerrainSystemTest : public ::testing::Test
// Defines a structure for defining both an XY position and the expected height for that position.
struct HeightTestPoint
{ {
AZ::Vector2 m_testLocation; protected:
float m_expectedHeight; // 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; AZ::ComponentApplication m_app;
AZStd::unique_ptr<Terrain::TerrainSystem> m_terrainSystem; AZStd::unique_ptr<Terrain::TerrainSystem> m_terrainSystem;
AZStd::unique_ptr<NiceMock<UnitTest::MockBoxShapeComponentRequests>> m_boxShapeRequests; AZStd::unique_ptr<NiceMock<UnitTest::MockBoxShapeComponentRequests>> m_boxShapeRequests;
AZStd::unique_ptr<NiceMock<UnitTest::MockShapeComponentRequests>> m_shapeRequests; AZStd::unique_ptr<NiceMock<UnitTest::MockShapeComponentRequests>> m_shapeRequests;
AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainAreaHeightRequests>> m_terrainAreaHeightRequests; 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;
void SetUp() override m_app.Create(appDesc);
{ }
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();
}
void TearDown() override AZStd::unique_ptr<AZ::Entity> CreateEntity()
{ {
m_terrainSystem.reset(); return AZStd::make_unique<AZ::Entity>();
m_boxShapeRequests.reset(); }
m_shapeRequests.reset();
m_terrainAreaHeightRequests.reset();
m_app.Destroy();
}
AZStd::unique_ptr<AZ::Entity> CreateEntity() void ActivateEntity(AZ::Entity* entity)
{ {
return AZStd::make_unique<AZ::Entity>(); entity->Init();
} EXPECT_EQ(AZ::Entity::State::Init, entity->GetState());
void ActivateEntity(AZ::Entity* entity) entity->Activate();
{ EXPECT_EQ(AZ::Entity::State::Active, entity->GetState());
entity->Init(); }
EXPECT_EQ(AZ::Entity::State::Init, entity->GetState());
entity->Activate(); template<typename Component, typename Configuration>
EXPECT_EQ(AZ::Entity::State::Active, entity->GetState()); AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config)
} {
m_app.RegisterComponentDescriptor(Component::CreateDescriptor());
return entity->CreateComponent<Component>(config);
}
template<typename Component, typename Configuration> template<typename Component>
AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config) AZ::Component* CreateComponent(AZ::Entity* entity)
{ {
m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); m_app.RegisterComponentDescriptor(Component::CreateDescriptor());
return entity->CreateComponent<Component>(config); return entity->CreateComponent<Component>();
} }
template<typename Component> // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults
AZ::Component* CreateComponent(AZ::Entity* entity) // 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;
}
};
TEST_F(TerrainSystemTest, TrivialCreateDestroy)
{ {
m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); // Trivially verify that the terrain system can successfully be constructed and destructed without errors.
return entity->CreateComponent<Component>();
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
} }
// Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults TEST_F(TerrainSystemTest, TrivialActivateDeactivate)
// 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. // Verify that the terrain system can be activated and deactivated without errors.
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>(); m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
m_terrainSystem->SetTerrainAabb(worldBounds);
m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution);
m_terrainSystem->Activate(); m_terrainSystem->Activate();
AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); m_terrainSystem->Deactivate();
} }
TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation)
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. // Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated.
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;
}
};
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) NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
{ EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1));
// Verify that the terrain system can be activated and deactivated without errors. EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1));
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; m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1)); m_terrainSystem->Activate();
EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1)); }
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
m_terrainSystem->Activate();
}
TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation) TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation)
{ {
// Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated. // Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated.
NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener; NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1)); EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1));
EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1)); EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1));
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>(); m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
m_terrainSystem->Activate(); m_terrainSystem->Activate();
m_terrainSystem->Deactivate(); m_terrainSystem->Deactivate();
} }
TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered) TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered)
{ {
// For the terrain system, terrain should only exist where terrain layer spawners are present. // 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 // 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 // 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. // a normal facing up the Z axis.
// Create and activate the terrain system with our testing defaults for world bounds and query resolution. // Create and activate the terrain system with our testing defaults for world bounds and query resolution.
CreateAndActivateTerrainSystem(); CreateAndActivateTerrainSystem();
AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb(); 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 // 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. // terrainExists with default heights and normals.
for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f)) 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))
{ {
AZ::Vector3 position(x, y, 0.0f); for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f))
bool terrainExists = true; {
float height = m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); AZ::Vector3 position(x, y, 0.0f);
EXPECT_FALSE(terrainExists); bool terrainExists = true;
EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ()); float height =
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
terrainExists = true; EXPECT_FALSE(terrainExists);
AZ::Vector3 normal = m_terrainSystem->GetNormal( EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ());
position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
EXPECT_FALSE(terrainExists); terrainExists = true;
EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ()); AZ::Vector3 normal =
m_terrainSystem->GetNormal(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
bool isHole = m_terrainSystem->GetIsHoleFromFloats( EXPECT_FALSE(terrainExists);
position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ());
EXPECT_TRUE(isHole);
bool isHole = m_terrainSystem->GetIsHoleFromFloats(
position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
EXPECT_TRUE(isHole);
}
} }
} }
}
TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds)
{ {
// Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the TerrainLayerSpawner // Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the
// is defined. // TerrainLayerSpawner is defined.
// The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the // 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. // 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. // 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; constexpr float spawnerHeight = 5.0f;
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f);
auto entity = CreateAndActivateMockTerrainLayerSpawner( auto entity = CreateAndActivateMockTerrainLayerSpawner(
spawnerBox, spawnerBox,
[](AZ::Vector3& position, bool& terrainExists) [](AZ::Vector3& position, bool& terrainExists)
{ {
position.SetZ(spawnerHeight); position.SetZ(spawnerHeight);
terrainExists = true; terrainExists = true;
}); });
// Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it. // 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. // Create and activate the terrain system with our testing defaults for world bounds and query resolution.
CreateAndActivateTerrainSystem(); 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 // 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. // terrain and the expected height & normal values, and points outside the layer box don't contain terrain.
const AZ::Aabb encompassingBox = const AZ::Aabb encompassingBox = AZ::Aabb::CreateFromMinMax(
AZ::Aabb::CreateFromMinMax(spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), spawnerBox.GetMax() + (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 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); for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.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); AZ::Vector3 position(x, y, 0.0f);
EXPECT_TRUE(isHole); 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) 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)
{ {
// 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); AZ::Vector3 position(nonZeroPoint.GetX(), nonZeroPoint.GetY(), 0.0f);
bool heightQueryTerrainExists = false; bool heightQueryTerrainExists = false;
float height = 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. // 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; constexpr float epsilon = 0.0001f;
EXPECT_GT(fabsf(height), epsilon); 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 an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on the TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid)
// 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); // Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching
bool heightQueryTerrainExists = false; // the height.
float height =
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); // 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;
});
// 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; constexpr float epsilon = 0.0001f;
EXPECT_NEAR(height, 0.0f, epsilon); EXPECT_NEAR(height, expectedHeight, epsilon);
}
} }
}
TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid) TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate)
{ {
// Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching // Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid.
// the height.
// Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal
// 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:
// to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result. // 0 *---* 1
const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); // | |
auto entity = CreateAndActivateMockTerrainLayerSpawner( // 1 *---* 2
spawnerBox, // However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid
[](AZ::Vector3& position, bool& terrainExists) // 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)
{ {
position.SetZ(position.GetX() + position.GetY()); const float expectedHeight = testPoint.m_expectedHeight;
terrainExists = true;
});
// Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter intervals. AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f);
const AZ::Vector2 queryResolution(0.25f); bool heightQueryTerrainExists = false;
CreateAndActivateTerrainSystem(queryResolution); float height = m_terrainSystem->GetHeight(
position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists);
// Test some points and verify that the results always go "downward", whether they're in positive or negative space. // Verify that our height query returned the bilinear filtered result we expect.
// (Z contains the the expected result for convenience). constexpr float epsilon = 0.0001f;
const HeightTestPoint testPoints[] = EXPECT_NEAR(height, expectedHeight, epsilon);
{ }
{ 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 TEST_F(TerrainSystemTest, GetSurfaceWeightsReturnsAllValidSurfaceWeights)
{ 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; CreateAndActivateTerrainSystem();
AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne());
bool heightQueryTerrainExists = false; auto entity = CreateAndActivateMockTerrainLayerSpawner(
float height = aabb,
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists); [](AZ::Vector3& position, bool& terrainExists)
{
position.SetZ(1.0f);
terrainExists = true;
});
constexpr float epsilon = 0.0001f; const AZ::Crc32 tag1("tag1");
EXPECT_NEAR(height, expectedHeight, epsilon); const AZ::Crc32 tag2("tag2");
}
}
TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate) AzFramework::SurfaceData::OrderedSurfaceTagWeightSet orderedSurfaceWeights;
{
// Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid. AzFramework::SurfaceData::SurfaceTagWeight tagWeight1;
tagWeight1.m_surfaceType = tag1;
// Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal tagWeight1.m_weight = 1.0f;
// to the X + Y position, so we'll have heights that look like this on our grid: orderedSurfaceWeights.emplace(tagWeight1);
// 0 *---* 1
// | | AzFramework::SurfaceData::SurfaceTagWeight tagWeight2;
// 1 *---* 2 tagWeight2.m_surfaceType = tag2;
// However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid tagWeight2.m_weight = 0.8f;
// points, but it will noticeably affect the expected height values if any points get sampled in-between grid points. orderedSurfaceWeights.emplace(tagWeight2);
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. NiceMock<UnitTest::MockTerrainAreaSurfaceRequestBus> mockSurfaceRequests(entity->GetId());
for (auto& testPoint : testPoints) 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)
{ {
const float expectedHeight = testPoint.m_expectedHeight; 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));
AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); // Inside the layer spawner box should give us the highest weighted tag (tag1).
bool heightQueryTerrainExists = false; tagWeight = m_terrainSystem->GetMaxSurfaceWeight(aabb.GetCenter());
float height =
m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists);
// Verify that our height query returned the bilinear filtered result we expect. EXPECT_EQ(tagWeight.m_surfaceType, tagWeight1.m_surfaceType);
constexpr float epsilon = 0.0001f; EXPECT_NEAR(tagWeight.m_weight, tagWeight1.m_weight, 0.01f);
EXPECT_NEAR(height, expectedHeight, epsilon);
} }
} } // namespace UnitTest

@ -11,6 +11,8 @@ set(FILES
Source/EditorComponents/EditorTerrainHeightGradientListComponent.h Source/EditorComponents/EditorTerrainHeightGradientListComponent.h
Source/EditorComponents/EditorTerrainLayerSpawnerComponent.cpp Source/EditorComponents/EditorTerrainLayerSpawnerComponent.cpp
Source/EditorComponents/EditorTerrainLayerSpawnerComponent.h Source/EditorComponents/EditorTerrainLayerSpawnerComponent.h
Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp
Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h
Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.cpp Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.cpp
Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.h Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.h
Source/EditorComponents/EditorTerrainWorldComponent.cpp Source/EditorComponents/EditorTerrainWorldComponent.cpp

@ -12,6 +12,8 @@ set(FILES
Source/Components/TerrainHeightGradientListComponent.h Source/Components/TerrainHeightGradientListComponent.h
Source/Components/TerrainLayerSpawnerComponent.cpp Source/Components/TerrainLayerSpawnerComponent.cpp
Source/Components/TerrainLayerSpawnerComponent.h Source/Components/TerrainLayerSpawnerComponent.h
Source/Components/TerrainPhysicsColliderComponent.cpp
Source/Components/TerrainPhysicsColliderComponent.h
Source/Components/TerrainSurfaceDataSystemComponent.cpp Source/Components/TerrainSurfaceDataSystemComponent.cpp
Source/Components/TerrainSurfaceDataSystemComponent.h Source/Components/TerrainSurfaceDataSystemComponent.h
Source/Components/TerrainSurfaceGradientListComponent.cpp Source/Components/TerrainSurfaceGradientListComponent.cpp

@ -8,4 +8,6 @@
set(FILES set(FILES
Mocks/Terrain/MockTerrain.h Mocks/Terrain/MockTerrain.h
Mocks/Terrain/MockTerrainLayerSpawner.h
Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h
) )

@ -10,6 +10,9 @@ set(FILES
Tests/TerrainTest.cpp Tests/TerrainTest.cpp
Tests/TerrainSystemTest.cpp Tests/TerrainSystemTest.cpp
Tests/LayerSpawnerTests.cpp Tests/LayerSpawnerTests.cpp
Tests/TerrainPhysicsColliderTests.cpp
Tests/SurfaceMaterialsListTest.cpp Tests/SurfaceMaterialsListTest.cpp
Tests/MockAxisAlignedBoxShapeComponent.h Tests/MockAxisAlignedBoxShapeComponent.h
Tests/TerrainHeightGradientListTests.cpp
Tests/TerrainSurfaceGradientListTests.cpp
) )

Loading…
Cancel
Save