/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "LmbrCentral_precompiled.h" #include "TubeShape.h" #include #include #if LMBR_CENTRAL_EDITOR #include #endif namespace LmbrCentral { float Lerpf(const float from, const float to, const float fraction) { return AZ::Lerp(from, to, fraction); } AZ::Vector3 CalculateNormal( const AZ::Vector3& previousNormal, const AZ::Vector3& previousTangent, const AZ::Vector3& currentTangent) { // Rotates the previous normal by the angle difference between two tangent segments. Ensures // The normal is continuous along the tube. AZ::Vector3 normal = previousNormal; auto cosAngleBetweenTangentSegments = currentTangent.Dot(previousTangent); if (cosAngleBetweenTangentSegments.GetAbs().IsLessThan(AZ::VectorFloat::CreateOne())) { AZ::Vector3 axis = previousTangent.Cross(currentTangent); if (!axis.IsZero()) { axis.Normalize(); float angle = acosf(cosAngleBetweenTangentSegments); AZ::Quaternion rotationTangentDelta = AZ::Quaternion::CreateFromAxisAngle(axis, angle); normal = rotationTangentDelta * normal; normal.Normalize(); } } return normal; } void TubeShape::Reflect(AZ::SerializeContext& context) { context.Class () ->Version(1) ->Field("Radius", &TubeShape::m_radius) ->Field("VariableRadius", &TubeShape::m_variableRadius) ; if (auto editContext = context.GetEditContext()) { editContext->Class( "Tube Shape", "") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->DataElement(AZ::Edit::UIHandlers::Default, &TubeShape::m_radius, "Radius", "Radius of the tube") ->Attribute(AZ::Edit::Attributes::Min, 0.1f) ->Attribute(AZ::Edit::Attributes::Step, 0.5f) ->Attribute(AZ::Edit::Attributes::ChangeNotify, &TubeShape::BaseRadiusChanged) ->DataElement(AZ::Edit::UIHandlers::Default, &TubeShape::m_variableRadius, "Variable Radius", "Variable radius along the tube") ->Attribute(AZ::Edit::Attributes::ChangeNotify, &TubeShape::VariableRadiusChanged) ; } } void TubeShape::Activate(AZ::EntityId entityId) { m_entityId = entityId; AZ::TransformNotificationBus::Handler::BusConnect(m_entityId); ShapeComponentRequestsBus::Handler::BusConnect(m_entityId); TubeShapeComponentRequestsBus::Handler::BusConnect(m_entityId); SplineComponentNotificationBus::Handler::BusConnect(m_entityId); AZ::TransformBus::EventResult(m_currentTransform, m_entityId, &AZ::TransformBus::Events::GetWorldTM); SplineComponentRequestBus::EventResult(m_spline, m_entityId, &SplineComponentRequests::GetSpline); m_variableRadius.Activate(entityId); } void TubeShape::Deactivate() { m_variableRadius.Deactivate(); SplineComponentNotificationBus::Handler::BusDisconnect(); TubeShapeComponentRequestsBus::Handler::BusDisconnect(); ShapeComponentRequestsBus::Handler::BusDisconnect(); AZ::TransformNotificationBus::Handler::BusDisconnect(); } void TubeShape::OnTransformChanged(const AZ::Transform& /*local*/, const AZ::Transform& world) { m_currentTransform = world; ShapeComponentNotificationsBus::Event( m_entityId, &ShapeComponentNotificationsBus::Events::OnShapeChanged, ShapeComponentNotifications::ShapeChangeReasons::TransformChanged); } void TubeShape::SetRadius(float radius) { m_radius = radius; ValidateAllVariableRadii(); ShapeComponentNotificationsBus::Event( m_entityId, &ShapeComponentNotificationsBus::Events::OnShapeChanged, ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); } float TubeShape::GetRadius() const { return m_radius; } void TubeShape::SetVariableRadius(int vertIndex, float radius) { m_variableRadius.SetElement(vertIndex, radius); ValidateVariableRadius(vertIndex); ShapeComponentNotificationsBus::Event( m_entityId, &ShapeComponentNotificationsBus::Events::OnShapeChanged, ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); } void TubeShape::SetAllVariableRadii(float radius) { for (size_t vertIndex = 0; vertIndex < m_variableRadius.Size(); ++vertIndex) { m_variableRadius.SetElement(vertIndex, radius); ValidateVariableRadius(vertIndex); } ShapeComponentNotificationsBus::Event( m_entityId, &ShapeComponentNotificationsBus::Events::OnShapeChanged, ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); } float TubeShape::GetVariableRadius(int vertIndex) const { return m_variableRadius.GetElement(vertIndex); } float TubeShape::GetTotalRadius(const AZ::SplineAddress& address) const { return m_radius + m_variableRadius.GetElementInterpolated(address, Lerpf); } const SplineAttribute& TubeShape::GetRadiusAttribute() const { return m_variableRadius; } void TubeShape::OnSplineChanged() { SplineComponentRequestBus::EventResult(m_spline, m_entityId, &SplineComponentRequests::GetSpline); ShapeComponentNotificationsBus::Event(m_entityId, &ShapeComponentNotificationsBus::Events::OnShapeChanged, ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); } AZ::SplinePtr TubeShape::GetSpline() { if (!m_spline) { SplineComponentRequestBus::EventResult(m_spline, m_entityId, &SplineComponentRequests::GetSpline); AZ_Assert(m_spline, "A TubeShape must have a Spline to work"); } return m_spline; } AZ::Aabb TubeShape::GetEncompassingAabb() { if (m_spline == nullptr) { return AZ::Aabb(); } AZ::Transform worldFromLocalUniformScale = m_currentTransform; const AZ::VectorFloat maxScale = worldFromLocalUniformScale.ExtractScale().GetMaxElement(); worldFromLocalUniformScale *= AZ::Transform::CreateScale(AZ::Vector3(maxScale)); // approximate aabb - not exact but is guaranteed to encompass tube AZ::VectorFloat maxRadius = GetTotalRadius(AZ::SplineAddress(0, 0.0f)) * maxScale; for (size_t i = 0; i < m_spline->GetVertexCount(); ++i) { maxRadius = AZ::GetMax(maxRadius, GetTotalRadius(AZ::SplineAddress(i, 1.0f)) * maxScale); } AZ::Aabb aabb; m_spline->GetAabb(aabb, worldFromLocalUniformScale); aabb.Expand(AZ::Vector3(maxRadius, maxRadius, maxRadius)); return aabb; } bool TubeShape::IsPointInside(const AZ::Vector3& point) { if (m_spline == nullptr) { return false; } AZ::Transform worldFromLocalNormalized = m_currentTransform; const AZ::Vector3 scale = AZ::Vector3(worldFromLocalNormalized.ExtractScale().GetMaxElement()); const AZ::Transform localFromWorldNormalized = worldFromLocalNormalized.GetInverseFast(); const AZ::Vector3 localPoint = localFromWorldNormalized * point * scale.GetReciprocal(); const auto address = m_spline->GetNearestAddressPosition(localPoint).m_splineAddress; const float radiusSq = powf(m_radius, 2.0f); const float variableRadiusSq = powf(m_variableRadius.GetElementInterpolated(address, Lerpf), 2.0f); return (m_spline->GetPosition(address) - localPoint).GetLengthSq() < (radiusSq + variableRadiusSq) * scale.GetMaxElement(); } float TubeShape::DistanceSquaredFromPoint(const AZ::Vector3& point) { AZ::Transform worldFromLocalNormalized = m_currentTransform; const AZ::Vector3 maxScale = AZ::Vector3(worldFromLocalNormalized.ExtractScale().GetMaxElement()); const AZ::Transform localFromWorldNormalized = worldFromLocalNormalized.GetInverseFast(); const AZ::Vector3 localPoint = localFromWorldNormalized * point * maxScale.GetReciprocal(); const auto splineQueryResult = m_spline->GetNearestAddressPosition(localPoint); const float variableRadius = m_variableRadius.GetElementInterpolated(splineQueryResult.m_splineAddress, Lerpf); return powf((sqrtf(splineQueryResult.m_distanceSq) - (m_radius + variableRadius)) * maxScale.GetMaxElement(), 2.0f); } bool TubeShape::IntersectRay(const AZ::Vector3& src, const AZ::Vector3& dir, AZ::VectorFloat& distance) { AZ::Transform transformUniformScale = m_currentTransform; const AZ::VectorFloat maxScale = transformUniformScale.ExtractScale().GetMaxElement(); transformUniformScale *= AZ::Transform::CreateScale(AZ::Vector3(maxScale)); const auto splineQueryResult = IntersectSpline(transformUniformScale, src, dir, *m_spline); const float variableRadius = m_variableRadius.GetElementInterpolated( splineQueryResult.m_splineAddress, Lerpf); const float totalRadius = m_radius + variableRadius; distance = (splineQueryResult.m_rayDistance - totalRadius) * m_currentTransform.RetrieveScale().GetMaxElement(); return static_cast(sqrtf(splineQueryResult.m_distanceSq)) < totalRadius; } /// Generates all vertex positions. Assumes the vertex pointer is valid static void GenerateSolidTubeMeshVertices( const AZ::SplinePtr& spline, const SplineAttribute& variableRadius, const float radius, const AZ::u32 sides, const AZ::u32 capSegments, AZ::Vector3* vertices) { // start cap auto address = spline->GetAddressByFraction(0.0f); AZ::Vector3 normal = spline->GetNormal(address); AZ::Vector3 previousTangent = spline->GetTangent(address); if (capSegments > 0) { vertices = CapsuleTubeUtil::GenerateSolidStartCap( spline->GetPosition(address), previousTangent, normal, radius + variableRadius.GetElementInterpolated(address, Lerpf), sides, capSegments, vertices); } // middle segments (body) const float stepDelta = 1.0f / static_cast(spline->GetSegmentGranularity()); const auto endIndex = address.m_segmentIndex + spline->GetSegmentCount(); while (address.m_segmentIndex < endIndex) { for (auto step = 0; step <= spline->GetSegmentGranularity(); ++step) { const AZ::Vector3 currentTangent = spline->GetTangent(address); normal = CalculateNormal(normal, previousTangent, currentTangent); vertices = CapsuleTubeUtil::GenerateSegmentVertices( spline->GetPosition(address), currentTangent, normal, radius + variableRadius.GetElementInterpolated(address, Lerpf), sides, vertices); address.m_segmentFraction += stepDelta; previousTangent = currentTangent; } address.m_segmentIndex++; address.m_segmentFraction = 0.f; } // end cap if (capSegments > 0) { const auto endAddress = spline->GetAddressByFraction(1.0f); CapsuleTubeUtil::GenerateSolidEndCap( spline->GetPosition(endAddress), spline->GetTangent(endAddress), normal, radius + variableRadius.GetElementInterpolated(endAddress, Lerpf), sides, capSegments, vertices); } } /// Generates vertices and indices for a tube shape /// Split into two stages: /// - Generate vertex positions /// - Generate indices (faces) /// Heres a rough diagram of how it is built: /// ____________ /// /_|__|__|__|_\ /// \_|__|__|__|_/ /// - A single vertex at each end of the tube /// - Angled end cap segments /// - Middle segments void GenerateSolidTubeMesh( const AZ::SplinePtr& spline, const SplineAttribute& variableRadius, const float radius, const AZ::u32 capSegments, const AZ::u32 sides, AZStd::vector& vertexBufferOut, AZStd::vector& indexBufferOut) { const size_t segmentCount = spline->GetSegmentCount(); if (segmentCount == 0) { // clear the buffers so we no longer draw anything vertexBufferOut.clear(); indexBufferOut.clear(); return; } const AZ::u32 segments = segmentCount * spline->GetSegmentGranularity() + segmentCount - 1; const AZ::u32 totalSegments = segments + capSegments * 2; const AZ::u32 capSegmentTipVerts = capSegments > 0 ? 2 : 0; const size_t numVerts = sides * (totalSegments + 1) + capSegmentTipVerts; const size_t numTriangles = (sides * totalSegments) * 2 + (sides * capSegmentTipVerts); vertexBufferOut.resize(numVerts); indexBufferOut.resize(numTriangles * 3); GenerateSolidTubeMeshVertices( spline, variableRadius, radius, sides, capSegments, &vertexBufferOut[0]); CapsuleTubeUtil::GenerateSolidMeshIndices( sides, segments, capSegments, &indexBufferOut[0]); } /// Compose Caps, Lines and Loops to produce a final wire mesh matching the style of other /// debug draw components. void GenerateWireTubeMesh( const AZ::SplinePtr& spline, const SplineAttribute& variableRadius, const float radius, const AZ::u32 capSegments, const AZ::u32 sides, AZStd::vector& vertexBufferOut) { // notes on vert buffer size // total end segments // 2 verts for each segment // 2 * capSegments for one full half arc // 2 arcs per end // 2 ends // total segments // 2 verts for each segment // 2 lines - top and bottom // 2 lines - left and right // total loops // 2 verts for each segment // loops == sides // 2 loops per segment const AZ::u32 segments = spline->GetSegmentCount() * spline->GetSegmentGranularity(); const AZ::u32 totalEndSegments = capSegments * 2 * 2 * 2 * 2; const AZ::u32 totalSegments = segments * 2 * 2 * 2; const AZ::u32 totalLoops = 2 * sides * segments * 2; const bool hasEnds = capSegments > 0; const size_t numVerts = totalEndSegments + totalSegments + totalLoops; vertexBufferOut.resize(numVerts); AZ::Vector3* vertices = vertexBufferOut.begin(); // start cap auto address = spline->GetAddressByFraction(0.0f); AZ::Vector3 side = spline->GetNormal(address); AZ::Vector3 nextSide = spline->GetNormal(address); AZ::Vector3 previousDirection = spline->GetTangent(address); if (hasEnds) { vertices = CapsuleTubeUtil::GenerateWireCap( spline->GetPosition(address), -previousDirection, side, radius + variableRadius.GetElementInterpolated(address, Lerpf), capSegments, vertices); } // body const float stepDelta = 1.0f / static_cast(spline->GetSegmentGranularity()); auto nextAddress = address; const auto endIndex = address.m_segmentIndex + spline->GetSegmentCount(); while (address.m_segmentIndex < endIndex) { address.m_segmentFraction = 0.f; nextAddress.m_segmentFraction = stepDelta; for (auto step = 0; step < spline->GetSegmentGranularity(); ++step) { const auto position = spline->GetPosition(address); const auto nextPosition = spline->GetPosition(nextAddress); const auto direction = spline->GetTangent(address); const auto nextDirection = spline->GetTangent(nextAddress); side = spline->GetNormal(address); nextSide = spline->GetNormal(nextAddress); const auto up = side.Cross(direction); const auto nextUp = nextSide.Cross(nextDirection); const auto finalRadius = radius + variableRadius.GetElementInterpolated(address, Lerpf); const auto nextFinalRadius = radius + variableRadius.GetElementInterpolated(nextAddress, Lerpf); // left line vertices = WriteVertex( CapsuleTubeUtil::CalculatePositionOnSphere( position, direction, side, finalRadius, 0.0f), vertices); vertices = WriteVertex( CapsuleTubeUtil::CalculatePositionOnSphere( nextPosition, nextDirection, nextSide, nextFinalRadius, 0.0f), vertices); // right line vertices = WriteVertex( CapsuleTubeUtil::CalculatePositionOnSphere( position, -direction, side, finalRadius, AZ::Constants::Pi), vertices); vertices = WriteVertex( CapsuleTubeUtil::CalculatePositionOnSphere( nextPosition, -nextDirection, nextSide, nextFinalRadius, AZ::Constants::Pi), vertices); // top line vertices = WriteVertex( CapsuleTubeUtil::CalculatePositionOnSphere( position, direction, up, finalRadius, 0.0f), vertices); vertices = WriteVertex( CapsuleTubeUtil::CalculatePositionOnSphere( nextPosition, nextDirection, nextUp, nextFinalRadius, 0.0f), vertices); // bottom line vertices = WriteVertex( CapsuleTubeUtil::CalculatePositionOnSphere( position, -direction, up, finalRadius, AZ::Constants::Pi), vertices); vertices = WriteVertex( CapsuleTubeUtil::CalculatePositionOnSphere( nextPosition, -nextDirection, nextUp, nextFinalRadius, AZ::Constants::Pi), vertices); // loops along each segment vertices = CapsuleTubeUtil::GenerateWireLoop( position, direction, side, sides, finalRadius, vertices); vertices = CapsuleTubeUtil::GenerateWireLoop( nextPosition, nextDirection, nextSide, sides, nextFinalRadius, vertices); address.m_segmentFraction += stepDelta; nextAddress.m_segmentFraction += stepDelta; previousDirection = direction; } address.m_segmentIndex++; nextAddress.m_segmentIndex++; } if (hasEnds) { const auto endAddress = spline->GetAddressByFraction(1.0f); const auto endPosition = spline->GetPosition(endAddress); const auto endDirection = spline->GetTangent(endAddress); const auto endRadius = radius + variableRadius.GetElementInterpolated(endAddress, Lerpf); // end cap CapsuleTubeUtil::GenerateWireCap( endPosition, endDirection, nextSide, endRadius, capSegments, vertices); } } void GenerateTubeMesh( const AZ::SplinePtr& spline, const SplineAttribute& variableRadius, const float radius, const AZ::u32 capSegments, const AZ::u32 sides, AZStd::vector& vertexBufferOut, AZStd::vector& indexBufferOut, AZStd::vector& lineBufferOut) { GenerateSolidTubeMesh( spline, variableRadius, radius, capSegments, sides, vertexBufferOut, indexBufferOut); GenerateWireTubeMesh( spline, variableRadius, radius, capSegments, sides, lineBufferOut); } void TubeShapeMeshConfig::Reflect(AZ::ReflectContext* context) { if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Version(2) ->Field("EndSegments", &TubeShapeMeshConfig::m_endSegments) ->Field("Sides", &TubeShapeMeshConfig::m_sides) ->Field("ShapeConfig", &TubeShapeMeshConfig::m_shapeComponentConfig) ; if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { editContext->Class("Configuration", "Tube Shape Mesh Configuration") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement(AZ::Edit::UIHandlers::Slider, &TubeShapeMeshConfig::m_endSegments, "End Segments", "Number Of segments at each end of the tube in the editor") ->Attribute(AZ::Edit::Attributes::Min, 1) ->Attribute(AZ::Edit::Attributes::Max, 10) ->Attribute(AZ::Edit::Attributes::Step, 1) ->DataElement(AZ::Edit::UIHandlers::Slider, &TubeShapeMeshConfig::m_sides, "Sides", "Number of Sides of the tube in the editor") ->Attribute(AZ::Edit::Attributes::Min, 3) ->Attribute(AZ::Edit::Attributes::Max, 32) ->Attribute(AZ::Edit::Attributes::Step, 1) ; } } } static void RefreshUI() { #if LMBR_CENTRAL_EDITOR AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast( &AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh, AzToolsFramework::PropertyModificationRefreshLevel::Refresh_Values); #endif } void TubeShape::BaseRadiusChanged() { // ensure all variable radii stay in bounds should the base radius // change and cause the resulting total radius to be negative ValidateAllVariableRadii(); RefreshUI(); } void TubeShape::VariableRadiusChanged(size_t vertIndex) { ValidateVariableRadius(vertIndex); RefreshUI(); } void TubeShape::ValidateVariableRadius(const size_t vertIndex) { // if the total radius is less than 0, adjust the variable radius // to ensure the total radius stays positive if (GetTotalRadius(AZ::SplineAddress(vertIndex)) < 0.0f) { SetVariableRadius(vertIndex, -GetRadius()); } } void TubeShape::ValidateAllVariableRadii() { for (size_t vertIndex = 0; vertIndex < m_spline->GetVertexCount(); ++vertIndex) { ValidateVariableRadius(vertIndex); } } } // namespace LmbrCentral