/* * 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 #include #include namespace Terrain { void TerrainWorldDebuggerConfig::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serialize = azrtti_cast(context); if (serialize) { serialize->Class() ->Version(1) ->Field("DebugWireframe", &TerrainWorldDebuggerConfig::m_drawWireframe) ->Field("DebugWorldBounds", &TerrainWorldDebuggerConfig::m_drawWorldBounds) ; AZ::EditContext* edit = serialize->GetEditContext(); if (edit) { edit->Class( "Terrain World Debugger Component", "Optional component for enabling terrain debugging features.") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZStd::vector({ AZ_CRC_CE("Level") })) ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement(AZ::Edit::UIHandlers::Default, &TerrainWorldDebuggerConfig::m_drawWireframe, "Show Wireframe", "") ->DataElement(AZ::Edit::UIHandlers::Default, &TerrainWorldDebuggerConfig::m_drawWorldBounds, "Show World Bounds", ""); } } } void TerrainWorldDebuggerComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) { services.push_back(AZ_CRC_CE("TerrainDebugService")); } void TerrainWorldDebuggerComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services) { services.push_back(AZ_CRC_CE("TerrainDebugService")); } void TerrainWorldDebuggerComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services) { services.push_back(AZ_CRC_CE("TerrainService")); } void TerrainWorldDebuggerComponent::Reflect(AZ::ReflectContext* context) { TerrainWorldDebuggerConfig::Reflect(context); AZ::SerializeContext* serialize = azrtti_cast(context); if (serialize) { serialize->Class() ->Version(0) ->Field("Configuration", &TerrainWorldDebuggerComponent::m_configuration) ; } } TerrainWorldDebuggerComponent::TerrainWorldDebuggerComponent(const TerrainWorldDebuggerConfig& configuration) : m_configuration(configuration) { } TerrainWorldDebuggerComponent::~TerrainWorldDebuggerComponent() { } void TerrainWorldDebuggerComponent::Activate() { // Given the AuxGeom vertex limits, MaxSectorsToDraw is the max number of wireframe sectors we can draw without exceeding the // limits. Since we want an N x N sector grid, take the square root to get the number of sectors in each direction. m_sectorGridSize = aznumeric_cast(sqrtf(MaxSectorsToDraw)); // We're always going to keep the camera in the center square, so "round" downwards to an odd number of sectors if we currently // have an even number. (If we added a sector, we'll go above the max sectors that we can draw with our vertex limits) m_sectorGridSize = (m_sectorGridSize & 0x01) ? m_sectorGridSize : m_sectorGridSize - 1; // Create our fixed set of sectors that we'll draw. By default, they'll all be constructed as dirty, so they'll get refreshed // the first time we try to draw them. (If wireframe drawing is disabled, we'll never refresh them) m_wireframeSectors.clear(); m_wireframeSectors.resize(m_sectorGridSize * m_sectorGridSize); AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(GetEntityId()); AzFramework::BoundsRequestBus::Handler::BusConnect(GetEntityId()); AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); // Any time the world bounds potentially changes, notify that the terrain debugger's visibility bounds also changed. // Otherwise, DisplayEntityViewport() won't get called at the appropriate times, since the visibility could get incorrectly // culled out. AzFramework::IEntityBoundsUnionRequestBus::Broadcast( &AzFramework::IEntityBoundsUnionRequestBus::Events::RefreshEntityLocalBoundsUnion, GetEntityId()); } void TerrainWorldDebuggerComponent::Deactivate() { AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect(); AzFramework::BoundsRequestBus::Handler::BusDisconnect(); AzFramework::EntityDebugDisplayEventBus::Handler::BusDisconnect(); m_wireframeSectors.clear(); } bool TerrainWorldDebuggerComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) { if (auto config = azrtti_cast(baseConfig)) { m_configuration = *config; return true; } return false; } bool TerrainWorldDebuggerComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const { if (auto config = azrtti_cast(outBaseConfig)) { *config = m_configuration; return true; } return false; } AZ::Aabb TerrainWorldDebuggerComponent::GetWorldBounds() { AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero()); AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb); return terrainAabb; } AZ::Aabb TerrainWorldDebuggerComponent::GetLocalBounds() { // This is a level component, so the local bounds will always be the same as the world bounds. return GetWorldBounds(); } void TerrainWorldDebuggerComponent::MarkDirtySectors(const AZ::Aabb& dirtyRegion) { // Create a 2D version of dirtyRegion that has Z set to min/max float values, so that we can just check for XY overlap with // each sector. const AZ::Aabb dirtyRegion2D = AZ::Aabb::CreateFromMinMaxValues( dirtyRegion.GetMin().GetX(), dirtyRegion.GetMin().GetY(), AZStd::numeric_limits::lowest(), dirtyRegion.GetMax().GetX(), dirtyRegion.GetMax().GetY(), AZStd::numeric_limits::max()); // For each sector that overlaps the dirty region (or all of them if the region is invalid), mark them as dirty so that // they'll get refreshed the next time we need to draw them. for (auto& sector : m_wireframeSectors) { AZStd::lock_guard lock(sector.m_sectorStateMutex); if (!dirtyRegion2D.IsValid() || dirtyRegion2D.Overlaps(sector.m_aabb)) { sector.SetDirty(); } } } void TerrainWorldDebuggerComponent::DrawWorldBounds(AzFramework::DebugDisplayRequests& debugDisplay) { if (!m_configuration.m_drawWorldBounds) { return; } // Draw a wireframe box around the entire terrain world bounds AZ::Color outlineColor(1.0f, 0.0f, 0.0f, 1.0f); AZ::Aabb aabb = GetWorldBounds(); debugDisplay.SetColor(outlineColor); debugDisplay.DrawWireBox(aabb.GetMin(), aabb.GetMax()); } void TerrainWorldDebuggerComponent::DrawWireframe( const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) { AZ_PROFILE_FUNCTION(Entity); if (!m_configuration.m_drawWireframe) { return; } /* This draws a wireframe centered on the camera that extends out to a certain distance at all times. To reduce the amount of * recalculations we need to do on each camera movement, we divide the world into a conceptual grid of sectors, where each sector * contains a fixed number of terrain height points. So for example, if the terrain has height data at 1 m spacing, the sectors * might be 10 m x 10 m in size. If the height data is spaced at 0.5 m, the sectors might be 5 m x 5 m in size. The wireframe * draws N x N sectors centered around the camera, as determined by m_sectorGridSize. So a gridSize of 7 with a sector size of * 10 m means that we'll be drawing 7 x 7 sectors, or 70 m x 70 m, centered around the camera. Each time the camera moves into * a new sector, we refresh the changed sectors before drawing them. * * The only tricky bit to this design is the way the sectors are stored and indexed. They're stored in a single vector as NxN * entries, so they would normally be indexed as (y * N) + x. Since we want this to be centered on the camera, the easy answer * would be to take the camera position - (N / 2) (since we're centering) as the relative offset to the first entry. But this * would mean that the entire set of entries would change every time we move the camera. For example, if we had 5 entries, * they might map to 0-4, 1-5, 2-6, 3-7, etc as the camera moves. * * Instead, we use mod (%) to rotate our indices around, so it would go (0 1 2 3 4), (5 1 2 3 4), (5 6 2 3 4), (5 6 7 3 4), etc * as the camera moves. For negative entries, we rotate the indices in reverse, so that we get results like (0 1 2 3 4), * (0 1 2 3 -1), (0 1 2 -2 -1), (0 1 -3 -2 -1), etc. This way we always have the correct range of sectors, and sectors that have * remained visible are left alone and don't need to be updated again. */ // Get the terrain world bounds AZ::Aabb worldBounds = GetWorldBounds(); float worldMinZ = worldBounds.GetMin().GetZ(); // Get the terrain height data resolution float heightDataResolution = 1.0f; AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( heightDataResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution); // Get the size of a wireframe sector in world space const AZ::Vector2 sectorSize = AZ::Vector2(heightDataResolution * SectorSizeInGridPoints); // Try to get the current camera position, or default to (0,0) if we can't. AZ::Vector3 cameraPos = AZ::Vector3::CreateZero(); if (auto viewportContextRequests = AZ::RPI::ViewportContextRequests::Get(); viewportContextRequests) { AZ::RPI::ViewportContextPtr viewportContext = viewportContextRequests->GetViewportContextById(viewportInfo.m_viewportId); cameraPos = viewportContext->GetCameraTransform().GetTranslation(); } // Convert our camera position to a wireframe grid sector. We first convert from world space to sector space by dividing by // sectorSize, so that integer values are sectors, and fractional values are the distance within the sector. Then we get the // floor, so that we consistently get the next lowest integer - i.e. 2.3 -> 2, and -2.3 -> -3. This gives us consistent behavior // across both positive and negative positions. AZ::Vector2 gridPosition = AZ::Vector2(cameraPos.GetX(), cameraPos.GetY()) / sectorSize; int32_t cameraSectorX = aznumeric_cast(gridPosition.GetFloor().GetX()); int32_t cameraSectorY = aznumeric_cast(gridPosition.GetFloor().GetY()); // Loop through each sector that we *want* to draw, based on camera position. If the current sector at that index in // m_wireframeSectors doesn't match the world position we want, update its world position and mark it as dirty. // (We loop from -gridSize/2 to gridSize/2 so that the camera is always in the center sector.) for (int32_t sectorY = cameraSectorY - (m_sectorGridSize / 2); sectorY <= cameraSectorY + (m_sectorGridSize / 2); sectorY++) { for (int32_t sectorX = cameraSectorX - (m_sectorGridSize / 2); sectorX <= cameraSectorX + (m_sectorGridSize / 2); sectorX++) { // Calculate the index in m_wireframeSectors for this sector. Our indices should rotate through 0 - gridSize, but just // using a single mod will produce a negative result for negative sector indices. Using abs() will give us incorrect // "backwards" indices for negative numbers, so instead we add the grid size and mod a second time. // Ex: For a grid size of 5, we want the indices to map like this: // Index 0 1 2 3 4 // Values -10 -9 -8 -7 -6 // -5 -4 -3 -2 -1 // 0 1 2 3 4 // 5 6 7 8 9 // For -9, (-9 % 5) = -4, then (-4 + 5) % 5 = 1. If we used abs(), we'd get 4, which is backwards from what we want. int32_t sectorYIndex = ((sectorY % m_sectorGridSize) + m_sectorGridSize) % m_sectorGridSize; int32_t sectorXIndex = ((sectorX % m_sectorGridSize) + m_sectorGridSize) % m_sectorGridSize; int32_t sectorIndex = (sectorYIndex * m_sectorGridSize) + sectorXIndex; WireframeSector& sector = m_wireframeSectors[sectorIndex]; // Calculate the new world space box for this sector. AZ::Aabb sectorAabb = AZ::Aabb::CreateFromMinMax( AZ::Vector3(sectorX * sectorSize.GetX(), sectorY * sectorSize.GetY(), worldMinZ), AZ::Vector3((sectorX + 1) * sectorSize.GetX(), (sectorY + 1) * sectorSize.GetY(), worldMinZ)); // Clamp it to the terrain world bounds. sectorAabb.Clamp(worldBounds); // If the world space box for the sector doesn't match, set it and mark the sector as dirty so we refresh the height data. { AZStd::lock_guard lock(sector.m_sectorStateMutex); if (sector.m_aabb != sectorAabb) { sector.m_aabb = sectorAabb; sector.SetDirty(); } } } } // Finally, for each sector, rebuild the data if it's dirty, then draw it assuming it has valid data. // (Sectors that are outside the world bounds won't have any valid data, so they'll get skipped) for (auto& sector : m_wireframeSectors) { AZStd::lock_guard lock(sector.m_sectorStateMutex); if (sector.m_jobContext) { // The previous async request for this sector has yet to complete. continue; } if (sector.m_isDirty) { RebuildSectorWireframe(sector, heightDataResolution); } else if (!sector.m_lineVertices.empty()) { const AZ::Color primaryColor = AZ::Color(0.25f, 0.25f, 0.25f, 1.0f); debugDisplay.DrawLines(sector.m_lineVertices, primaryColor); } } } void TerrainWorldDebuggerComponent::DisplayEntityViewport( const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) { DrawWorldBounds(debugDisplay); DrawWireframe(viewportInfo, debugDisplay); } void TerrainWorldDebuggerComponent::RebuildSectorWireframe(WireframeSector& sector, float gridResolution) { AZStd::lock_guard lock(sector.m_sectorStateMutex); if (!sector.m_isDirty) { return; } sector.m_isDirty = false; // To rebuild the wireframe, we walk through the sector by X, then by Y. For each point, we add two lines in a _| shape. // To do that, we'll need to cache the height from the previous point to draw the _ line, and from the previous row to draw // the | line. // When walking through the bounding box, the loops will be inclusive on one side, and exclusive on the other. However, since // our box is exactly aligned with grid points, we want to get the grid points on both sides in each direction, so we need to // expand our query region by one extra point. // For example, if our AABB is 2 m and our grid resolution is 1 m, we'll want to query (*--*--*--), not (*--*--). // Since we're processing lines based on the grid points and going backwards, this will give us (*--*--*). AZ::Aabb region = sector.m_aabb; region.SetMax(region.GetMax() + AZ::Vector3(gridResolution, gridResolution, 0.0f)); // We need 4 vertices for each grid point in our sector to hold the _| shape. const size_t numSamplesX = aznumeric_cast(ceil(region.GetExtents().GetX() / gridResolution)); const size_t numSamplesY = aznumeric_cast(ceil(region.GetExtents().GetY() / gridResolution)); sector.m_lineVertices.clear(); sector.m_lineVertices.reserve(numSamplesX * numSamplesY * 4); // This keeps track of the height from the previous point for the _ line. sector.m_previousHeight = 0.0f; // This keeps track of the heights from the previous row for the | line. sector.m_rowHeights.clear(); sector.m_rowHeights.resize(numSamplesX); // For each terrain height value in the region, create the _| grid lines for that point and cache off the height value // for use with subsequent grid line calculations. auto ProcessHeightValue = [gridResolution, §or] (size_t xIndex, size_t yIndex, const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) { AZStd::lock_guard lock(sector.m_sectorStateMutex); if (sector.m_isDirty) { // Bail out if this sector has become dirty again since the async request started. return; } // Don't add any vertices for the first column or first row. These grid lines will be handled by an adjacent sector, if // there is one. if ((xIndex > 0) && (yIndex > 0)) { float x = surfacePoint.m_position.GetX() - gridResolution; float y = surfacePoint.m_position.GetY() - gridResolution; sector.m_lineVertices.emplace_back(AZ::Vector3(x, surfacePoint.m_position.GetY(), sector.m_previousHeight)); sector.m_lineVertices.emplace_back(surfacePoint.m_position); sector.m_lineVertices.emplace_back(AZ::Vector3(surfacePoint.m_position.GetX(), y, sector.m_rowHeights[xIndex])); sector.m_lineVertices.emplace_back(surfacePoint.m_position); } // Save off the heights so that we can use them to draw subsequent columns and rows. sector.m_previousHeight = surfacePoint.m_position.GetZ(); sector.m_rowHeights[xIndex] = surfacePoint.m_position.GetZ(); }; auto completionCallback = [§or](AZStd::shared_ptr) { // This must happen outside the lock, // otherwise we will get a deadlock if // WireframeSector::Reset is waiting for // the completion event to be signalled. sector.m_jobCompletionEvent->release(); // Reset the job context once the async request has completed, // clearing the way for future requests to be made for this sector. AZStd::lock_guard lock(sector.m_sectorStateMutex); sector.m_jobContext.reset(); }; AZStd::shared_ptr asyncParams = AZStd::make_shared(); asyncParams->m_completionCallback = completionCallback; sector.m_jobCompletionEvent = AZStd::make_unique(); AZ::Vector2 stepSize = AZ::Vector2(gridResolution); AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( sector.m_jobContext, &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegionAsync, region, stepSize, ProcessHeightValue, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, asyncParams); } void TerrainWorldDebuggerComponent::OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) { if (dataChangedMask & (TerrainDataChangedMask::Settings | TerrainDataChangedMask::HeightData)) { MarkDirtySectors(dirtyRegion); } if (dataChangedMask & TerrainDataChangedMask::Settings) { // Any time the world bounds potentially changes, notify that the terrain debugger's visibility bounds also changed. AzFramework::IEntityBoundsUnionRequestBus::Broadcast( &AzFramework::IEntityBoundsUnionRequestBus::Events::RefreshEntityLocalBoundsUnion, GetEntityId()); } } TerrainWorldDebuggerComponent::WireframeSector::WireframeSector(const WireframeSector& other) { AZStd::lock_guard lock(m_sectorStateMutex); m_jobContext = other.m_jobContext; m_aabb = other.m_aabb; m_lineVertices = other.m_lineVertices; m_rowHeights = other.m_rowHeights; m_previousHeight = other.m_previousHeight; m_isDirty = other.m_isDirty; } TerrainWorldDebuggerComponent::WireframeSector::WireframeSector(WireframeSector&& other) { AZStd::lock_guard lock(m_sectorStateMutex); m_jobContext = AZStd::move(other.m_jobContext); m_aabb = AZStd::move(other.m_aabb); m_lineVertices = AZStd::move(other.m_lineVertices); m_rowHeights = AZStd::move(other.m_rowHeights); m_previousHeight = AZStd::move(other.m_previousHeight); m_isDirty = AZStd::move(other.m_isDirty); } TerrainWorldDebuggerComponent::WireframeSector& TerrainWorldDebuggerComponent::WireframeSector::operator=(const WireframeSector& other) { AZStd::lock_guard lock(m_sectorStateMutex); m_jobContext = other.m_jobContext; m_aabb = other.m_aabb; m_lineVertices = other.m_lineVertices; m_rowHeights = other.m_rowHeights; m_previousHeight = other.m_previousHeight; m_isDirty = other.m_isDirty; return *this; } TerrainWorldDebuggerComponent::WireframeSector& TerrainWorldDebuggerComponent::WireframeSector::operator=(WireframeSector&& other) { AZStd::lock_guard lock(m_sectorStateMutex); m_jobContext = AZStd::move(other.m_jobContext); m_aabb = AZStd::move(other.m_aabb); m_lineVertices = AZStd::move(other.m_lineVertices); m_rowHeights = AZStd::move(other.m_rowHeights); m_previousHeight = AZStd::move(other.m_previousHeight); m_isDirty = AZStd::move(other.m_isDirty); return *this; } void TerrainWorldDebuggerComponent::WireframeSector::Reset() { AZStd::lock_guard lock(m_sectorStateMutex); if (m_jobContext) { // Cancel the job and wait until it completes. m_jobContext->Cancel(); m_jobCompletionEvent->acquire(); m_jobCompletionEvent.reset(); m_jobContext.reset(); } m_aabb = AZ::Aabb::CreateNull(); m_lineVertices.clear(); m_rowHeights.clear(); m_previousHeight = 0.0f; m_isDirty = true; } void TerrainWorldDebuggerComponent::WireframeSector::SetDirty() { m_isDirty = true; if (m_jobContext) { m_jobContext->Cancel(); } } } // namespace Terrain