/* * 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 #include #include #include #include #include #include #include #include namespace Terrain { void TerrainPhysicsColliderConfig::Reflect(AZ::ReflectContext* context) { if (auto serialize = azrtti_cast(context)) { serialize->Class() ->Version(1) ; if (auto edit = serialize->GetEditContext()) { edit->Class( "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(context)) { serialize->Class() ->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(baseConfig)) { m_configuration = *config; return true; } return false; } bool TerrainPhysicsColliderComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const { if (auto config = azrtti_cast(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 { 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. maxHeightBounds = heightfieldAabb.GetZExtent() / 2.0f; minHeightBounds = -maxHeightBounds; } float TerrainPhysicsColliderComponent::GetHeightfieldMinHeight() const { float minHeightBounds{ 0.0f }; float maxHeightBounds{ 0.0f }; GetHeightfieldHeightBounds(minHeightBounds, maxHeightBounds); return minHeightBounds; } float TerrainPhysicsColliderComponent::GetHeightfieldMaxHeight() const { float minHeightBounds{ 0.0f }; float maxHeightBounds{ 0.0f }; GetHeightfieldHeightBounds(minHeightBounds, maxHeightBounds); return maxHeightBounds; } 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); return AZ::Transform::CreateTranslation(translate); } void TerrainPhysicsColliderComponent::GenerateHeightsInBounds(AZStd::vector& 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& 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((bounds.GetMax().GetX() - bounds.GetMin().GetX()) / gridResolution.GetX()); numRows = aznumeric_cast((bounds.GetMax().GetY() - bounds.GetMin().GetY()) / gridResolution.GetY()); } int32_t TerrainPhysicsColliderComponent::GetHeightfieldGridColumns() const { int32_t numColumns{ 0 }; int32_t numRows{ 0 }; GetHeightfieldGridSize(numColumns, numRows); return numColumns; } int32_t TerrainPhysicsColliderComponent::GetHeightfieldGridRows() const { int32_t numColumns{ 0 }; int32_t numRows{ 0 }; GetHeightfieldGridSize(numColumns, numRows); return numRows; } AZStd::vector TerrainPhysicsColliderComponent::GetMaterialList() const { return AZStd::vector(); } AZStd::vector TerrainPhysicsColliderComponent::GetHeights() const { AZStd::vector heights; GenerateHeightsInBounds(heights); return heights; } AZStd::vector TerrainPhysicsColliderComponent::GetHeightsAndMaterials() const { AZStd::vector heightMaterials; GenerateHeightsAndMaterialsInBounds(heightMaterials); return heightMaterials; } }