You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/LmbrCentral/Code/Source/Shape/TubeShape.cpp

610 lines
24 KiB
C++

/*
* 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 "TubeShape.h"
#include <AzCore/Math/Transform.h>
#include <Shape/ShapeGeometryUtil.h>
#if LMBR_CENTRAL_EDITOR
#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI.h>
#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 (std::fabs(cosAngleBetweenTangentSegments) < 1.0f)
{
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.TransformVector(normal);
normal.Normalize();
}
}
return normal;
}
void TubeShape::Reflect(AZ::SerializeContext& context)
{
context.Class <TubeShape>()
->Version(1)
->Field("Radius", &TubeShape::m_radius)
->Field("VariableRadius", &TubeShape::m_variableRadius)
;
if (auto editContext = context.GetEditContext())
{
editContext->Class<TubeShape>(
"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<float>& 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::ConstSplinePtr TubeShape::GetConstSpline() const
{
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;
}
static AZ::Aabb CalculateTubeBounds(const TubeShape& tubeShape, const AZ::Transform& transform)
{
const auto maxScale = transform.GetUniformScale();
const auto scaledRadiusFn =
[&tubeShape, maxScale](const AZ::SplineAddress& splineAddress)
{
return tubeShape.GetTotalRadius(splineAddress) * maxScale;
};
const auto& spline = tubeShape.GetConstSpline();
// approximate aabb - not exact but is guaranteed to encompass tube
float maxRadius = scaledRadiusFn(AZ::SplineAddress(0, 0.0f));
for (size_t i = 0; i < spline->GetVertexCount(); ++i)
{
maxRadius = AZ::GetMax(maxRadius, scaledRadiusFn(AZ::SplineAddress(i, 1.0f)));
}
AZ::Aabb aabb;
spline->GetAabb(aabb, transform);
aabb.Expand(AZ::Vector3(maxRadius));
return aabb;
}
AZ::Aabb TubeShape::GetEncompassingAabb()
{
if (m_spline == nullptr)
{
return AZ::Aabb::CreateNull();
}
AZ::Transform worldFromLocalUniformScale = m_currentTransform;
worldFromLocalUniformScale.SetUniformScale(worldFromLocalUniformScale.GetUniformScale());
return CalculateTubeBounds(*this, worldFromLocalUniformScale);
}
void TubeShape::GetTransformAndLocalBounds(AZ::Transform& transform, AZ::Aabb& bounds)
{
bounds = CalculateTubeBounds(*this, AZ::Transform::CreateIdentity());
transform = m_currentTransform;
}
bool TubeShape::IsPointInside(const AZ::Vector3& point)
{
if (m_spline == nullptr)
{
return false;
}
AZ::Transform worldFromLocalNormalized = m_currentTransform;
const float scale = worldFromLocalNormalized.ExtractUniformScale();
const AZ::Transform localFromWorldNormalized = worldFromLocalNormalized.GetInverse();
const AZ::Vector3 localPoint = localFromWorldNormalized.TransformPoint(point) / scale;
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;
}
float TubeShape::DistanceSquaredFromPoint(const AZ::Vector3& point)
{
AZ::Transform worldFromLocalNormalized = m_currentTransform;
const float uniformScale = worldFromLocalNormalized.ExtractUniformScale();
const AZ::Transform localFromWorldNormalized = worldFromLocalNormalized.GetInverse();
const AZ::Vector3 localPoint = localFromWorldNormalized.TransformPoint(point) / uniformScale;
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)) * uniformScale, 2.0f);
}
bool TubeShape::IntersectRay(const AZ::Vector3& src, const AZ::Vector3& dir, float& distance)
{
AZ::Transform transformUniformScale = m_currentTransform;
transformUniformScale.SetUniformScale(transformUniformScale.GetUniformScale());
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.GetUniformScale();
return static_cast<float>(sqrtf(splineQueryResult.m_distanceSq)) < totalRadius;
}
/// Generates all vertex positions. Assumes the vertex pointer is valid
static void GenerateSolidTubeMeshVertices(
const AZ::SplinePtr& spline, const SplineAttribute<float>& 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<float>(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<float>& variableRadius,
const float radius, const AZ::u32 capSegments, const AZ::u32 sides,
AZStd::vector<AZ::Vector3>& vertexBufferOut,
AZStd::vector<AZ::u32>& 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 = static_cast<AZ::u32>(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<float>& variableRadius,
const float radius, const AZ::u32 capSegments, const AZ::u32 sides,
AZStd::vector<AZ::Vector3>& vertexBufferOut)
{
const size_t segmentCount = spline->GetSegmentCount();
if (segmentCount == 0)
{
// clear the buffer so we no longer draw anything
vertexBufferOut.clear();
return;
}
// 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 = static_cast<AZ::u32>(segmentCount * 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.data();
// 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<float>(spline->GetSegmentGranularity());
auto nextAddress = address;
const auto endIndex = address.m_segmentIndex + segmentCount;
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<float>& variableRadius,
const float radius, const AZ::u32 capSegments, const AZ::u32 sides,
AZStd::vector<AZ::Vector3>& vertexBufferOut, AZStd::vector<AZ::u32>& indexBufferOut,
AZStd::vector<AZ::Vector3>& 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<AZ::SerializeContext*>(context))
{
serializeContext->Class<TubeShapeMeshConfig>()
->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<TubeShapeMeshConfig>("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(static_cast<int>(vertIndex), -GetRadius());
}
}
void TubeShape::ValidateAllVariableRadii()
{
for (size_t vertIndex = 0; vertIndex < m_spline->GetVertexCount(); ++vertIndex)
{
ValidateVariableRadius(vertIndex);
}
}
} // namespace LmbrCentral