Encapsulated gradient transform logic into separate class (#6586)

* First version of GradientTransform class.
The gradient transform logic is getting encapsulated into a class so that it can be cached and used by components in a much more optimal way than making ebus calls to the GradientTransform component on every transformed point.

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

* Moved GradientTransform into its own source files.

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

* Clean up and simplify GradientTransform logic.
Added extensive commenting and split TransformPositionToUVW into a separate method for normalizing (TransformPositionToUVWNormalized) so that there doesn't need to be any conditional logic.  There's no runtime variance as to which one needs to be called from a given call site.

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

* Added unit tests for GradientTransform.

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

* Add comparison operators to GradientTransform so we can easily tell when it has changed.

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

* Updated comments to be more Doxygen-friendly.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>
This commit is contained in:
Mike Balfour
2021-12-29 13:44:03 -06:00
committed by GitHub
parent 49dd17f410
commit a10bf92739
17 changed files with 663 additions and 180 deletions
@@ -299,9 +299,8 @@ namespace FastNoiseGem
AZ::Vector3 uvw = sampleParams.m_position;
bool wasPointRejected = false;
const bool shouldNormalizeOutput = false;
GradientSignal::GradientTransformRequestBus::Event(
GetEntityId(), &GradientSignal::GradientTransformRequestBus::Events::TransformPositionToUVW, sampleParams.m_position, uvw, shouldNormalizeOutput, wasPointRejected);
GetEntityId(), &GradientSignal::GradientTransformRequestBus::Events::TransformPositionToUVW, sampleParams.m_position, uvw, wasPointRejected);
if (!wasPointRejected)
{
+10 -2
View File
@@ -46,8 +46,16 @@ public:
////////////////////////////////////////////////////////////////////////////
//// GradientTransformRequestBus
void TransformPositionToUVW([[maybe_unused]] const AZ::Vector3& inPosition, [[maybe_unused]] AZ::Vector3& outUVW, [[maybe_unused]] const bool shouldNormalizeOutput, [[maybe_unused]] bool& wasPointRejected) const override {}
void GetGradientLocalBounds([[maybe_unused]] AZ::Aabb& bounds) const override {}
void TransformPositionToUVW([[maybe_unused]] const AZ::Vector3& inPosition, [[maybe_unused]] AZ::Vector3& outUVW, [[maybe_unused]] bool& wasPointRejected) const override {}
void TransformPositionToUVWNormalized(
[[maybe_unused]] const AZ::Vector3& inPosition,
[[maybe_unused]] AZ::Vector3& outUVW,
[[maybe_unused]] bool& wasPointRejected) const override
{
}
void GetGradientLocalBounds([[maybe_unused]] AZ::Aabb& bounds) const override
{
}
void GetGradientEncompassingBounds([[maybe_unused]] AZ::Aabb& bounds) const override {}
//////////////////////////////////////////////////////////////////////////
+2
View File
@@ -136,6 +136,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
BUILD_DEPENDENCIES
PRIVATE
AZ::AzTest
AZ::AzTestShared
Gem::GradientSignal.Static
Gem::LmbrCentral
Gem::GradientSignal.Mocks
@@ -157,6 +158,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
BUILD_DEPENDENCIES
PRIVATE
AZ::AzTest
AZ::AzTestShared
Gem::GradientSignal.Static
Gem::GradientSignal.Editor.Static
Gem::LmbrCentral.Editor
@@ -13,14 +13,30 @@
namespace GradientSignal
{
//! TransformType describes where the gradient's origin is mapped to.
enum class TransformType : AZ::u8
{
//! The gradient's origin is the world position of this entity.
World_ThisEntity = 0,
//! The gradient's origin is the local position of this entity, but in world space.
//! i.e. If the parent is at (2, 2), and the gradient is at (3,3) in local space, the gradient entity itself will be at (5,5) in
//! world space but its origin will frozen at (3,3) in world space, no matter how much the parent moves around.
Local_ThisEntity,
//! The gradient's origin is the world position of the reference entity.
World_ReferenceEntity,
//! The gradient's origin is the local position of the reference entity, but in world space.
Local_ReferenceEntity,
//! The gradient's origin is at (0,0,0) in world space.
World_Origin,
//! The gradient's origin is in translated world space relative to the reference entity.
Relative,
};
class GradientTransformModifierRequests
: public AZ::ComponentBus
{
public:
/**
* Overrides the default AZ::EBusTraits handler policy to allow one
* listener only.
*/
//! Overrides the default AZ::EBusTraits handler policy to allow only one listener.
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
virtual bool GetAllowReference() const = 0;
@@ -27,7 +27,8 @@ namespace GradientSignal
virtual ~GradientTransformRequests() = default;
virtual void TransformPositionToUVW(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, const bool shouldNormalizeOutput, bool& wasPointRejected) const = 0;
virtual void TransformPositionToUVW(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const = 0;
virtual void TransformPositionToUVWNormalized(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const = 0;
virtual void GetGradientLocalBounds(AZ::Aabb& bounds) const = 0;
virtual void GetGradientEncompassingBounds(AZ::Aabb& bounds) const = 0;
};
@@ -0,0 +1,157 @@
/*
* 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/Aabb.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/Math/Matrix3x4.h>
#include <AzCore/Math/Transform.h>
#include <AzCore/std/functional.h>
namespace GradientSignal
{
//! Controls how the gradient repeats itself when queried outside the bounds of the shape.
enum class WrappingType : AZ::u8
{
None = 0, //! Unbounded - the gradient ignores the shape bounds.
ClampToEdge, //! The values on the edge of the shape will be extended outward in each direction.
Mirror, //! The gradient signal will be repeated but mirrored on every repeat.
Repeat, //! The gradient signal will be repeated in every direction.
ClampToZero, //! The value will always be 0 outside of the shape.
};
class GradientTransform
{
public:
GradientTransform() = default;
/**
* Create a GradientTransform with the given parameters.
* GradientTransform is a utility class that converts world space positions to gradient space UVW values which can be used
* to look up deterministic gradient values for the input spatial locations.
* \param shapeBounds The bounds of the shape associated with the gradient, in local space.
* \param transform The transform to use to convert from world space to gradient space.
* \param use3d True for 3D gradient lookup outputs, false for 2D gradient lookup outputs. (i.e. output W will be nonzero or zero)
* \param frequencyZoom Amount to scale the UVW results after wrapping is applied.
* \param wrappingType The way in which the gradient repeats itself outside the shape bounds.
*/
GradientTransform(
const AZ::Aabb& shapeBounds,
const AZ::Matrix3x4& transform,
bool use3d,
float frequencyZoom,
GradientSignal::WrappingType wrappingType);
/**
* Checks to see if two GradientTransform instances are equivalent.
* Useful for being able to send out notifications when a GradientTransform has changed.
* \param rhs The second GradientTranform to compare against.
* \return True if they're equal, False if they aren't.
*/
bool operator==(const GradientTransform& rhs) const
{
return (
(m_shapeBounds == rhs.m_shapeBounds) &&
(m_inverseTransform == rhs.m_inverseTransform) &&
(m_alwaysAcceptPoint == rhs.m_alwaysAcceptPoint) &&
(m_frequencyZoom == rhs.m_frequencyZoom) &&
(m_wrappingType == rhs.m_wrappingType) &&
(m_normalizeExtentsReciprocal == rhs.m_normalizeExtentsReciprocal));
}
/**
* Checks to see if two GradientTransform instances aren't equivalent.
* Useful for being able to send out notifications when a GradientTransform has changed.
* \param rhs The second GradientTranform to compare against.
* \return True if they're not equal, False if they are.
*/
bool operator!=(const GradientTransform& rhs) const
{
return !(*this == rhs);
}
/**
* Transform the given world space position to a gradient space UVW lookup value.
* \param inPosition The input world space position to transform.
* \param outUVW [out] The UVW value that can be used to look up a deterministic gradient value.
* \param wasPointRejected [out] True if the input position doesn't have a gradient value, false if it does.
* Most gradients have values mapped to infinite world space, so wasPointRejected will almost always be false.
* It will only be true when using ClampToZero and the world space position falls outside the shape bounds.
*/
void TransformPositionToUVW(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const;
/**
* Transform the given world space position to a gradient space UVW lookup value and normalize to the shape bounds.
* "Normalizing" in this context means that regardless of the world space coordinates, (0,0,0) represents the minimum
* shape bounds corner, and (1,1,1) represents the maximum shape bounds corner. Depending on the wrapping type, it's possible
* (and even likely) to get values outside the 0-1 range.
* \param inPosition The input world space position to transform.
* \param outUVW [out] The UVW value that can be used to look up a deterministic gradient value.
* \param wasPointRejected [out] True if the input position doesn't have a gradient value, false if it does.
* Most gradients have values mapped to infinite world space, so wasPointRejected will almost always be false.
* It will only be true when using ClampToZero and the world space position falls outside the shape bounds.
*/
void TransformPositionToUVWNormalized(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const;
/**
* Epsilon value to allow our UVW range to go to [min, max) by using the range [min, max - epsilon].
* To keep things behaving consistently between clamped and unbounded uv ranges, we want our clamped uvs to use a
* range of [min, max), so we'll actually clamp to [min, max - epsilon]. Since our floating-point numbers are likely in the
* -16384 to 16384 range, an epsilon of 0.001 will work without rounding to 0.
* (This constant is public so that it can be used from unit tests for validating transformation results)
*/
static constexpr float UvEpsilon = 0.001f;
private:
//! These are the various transformations that will be performed, based on wrapping type.
using WrappingTransformFunction = AZStd::function<AZ::Vector3(const AZ::Vector3& point, const AZ::Aabb& bounds)>;
static AZ::Vector3 NoTransform(const AZ::Vector3& point, const AZ::Aabb& bounds);
static AZ::Vector3 GetUnboundedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds);
static AZ::Vector3 GetClampedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds);
static AZ::Vector3 GetMirroredPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds);
static AZ::Vector3 GetRelativePointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds);
static AZ::Vector3 GetWrappedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds);
//! The shape bounds are used for determining the wrapping bounds, and to normalize the UVW results into if requested.
AZ::Aabb m_shapeBounds = AZ::Aabb::CreateNull();
/**
* The relative transform to use for converting from world space to gradient space, stored as an inverse transform.
* We only ever need to use the inverse transform, so we compute it once and store it instead of keeping the original
* transform around. Note that the GradientTransformComponent has many options for choosing which relative space to use
* for the transform, so the transform passed in to this class might already have many modifications applied to it.
* The inverse transform will also get its 3rd row cleared out if "use3d" is false and we're only performing 2D gradient
* transformations, so that the W component of the UVW output will always be 0.
*/
AZ::Matrix3x4 m_inverseTransform = AZ::Matrix3x4::CreateIdentity();
/**
* Whether or not to always accept the input point as a valid output point.
* Most of the time, the gradient exists everywhere in world space, so we always accept the input point.
* The one exception is ClampToZero, which will return that the point is rejected if it falls outside the shape bounds.
*/
bool m_alwaysAcceptPoint = true;
//! Apply a scale to the point *after* the wrapping is applied.
float m_frequencyZoom = 1.0f;
//! How the gradient should repeat itself outside of the shape bounds.
WrappingType m_wrappingType = WrappingType::None;
WrappingTransformFunction m_wrappingTransform = NoTransform;
/**
* Cached reciprocal for performing an inverse lerp back to shape bounds.
* When normalizing the output UVW back into the shape bounds, we perform an inverse lerp. The inverse lerp
* equation is (point - min) * (1 / (max-min)), so we save off the (1 / (max-min)) term to avoid recalculating it on every point.
*/
AZ::Vector3 m_normalizeExtentsReciprocal = AZ::Vector3(1.0f);
};
} // namespace GradientSignal
@@ -13,49 +13,10 @@
#include <AzCore/Math/Matrix3x4.h>
#include <AzCore/Math/Transform.h>
#include <LmbrCentral/Shape/ShapeComponentBus.h>
#include <GradientSignal/GradientTransform.h>
namespace GradientSignal
{
enum class WrappingType : AZ::u8
{
None = 0,
ClampToEdge,
Mirror,
Repeat,
ClampToZero,
};
enum class TransformType : AZ::u8
{
World_ThisEntity = 0,
Local_ThisEntity,
World_ReferenceEntity,
Local_ReferenceEntity,
World_Origin,
Relative,
};
AZ::Vector3 GetUnboundedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds);
AZ::Vector3 GetClampedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds);
AZ::Vector3 GetMirroredPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds);
AZ::Vector3 GetRelativePointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds);
inline AZ::Vector3 GetWrappedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds)
{
return AZ::Vector3(
AZ::Wrap(point.GetX(), bounds.GetMin().GetX(), bounds.GetMax().GetX()),
AZ::Wrap(point.GetY(), bounds.GetMin().GetY(), bounds.GetMax().GetY()),
AZ::Wrap(point.GetZ(), bounds.GetMin().GetZ(), bounds.GetMax().GetZ()));
}
inline AZ::Vector3 GetNormalizedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds)
{
return AZ::Vector3(
AZ::LerpInverse(bounds.GetMin().GetX(), bounds.GetMax().GetX(), point.GetX()),
AZ::LerpInverse(bounds.GetMin().GetY(), bounds.GetMax().GetY(), point.GetY()),
AZ::LerpInverse(bounds.GetMin().GetZ(), bounds.GetMax().GetZ(), point.GetZ()));
}
inline void GetObbParamsFromShape(const AZ::EntityId& entity, AZ::Aabb& bounds, AZ::Matrix3x4& worldToBoundsTransform)
{
//get bound and transform data for associated shape
@@ -115,4 +76,5 @@ namespace GradientSignal
return AZ::Lerp(outputMin, outputMax, inputCorrected);
}
} // namespace GradientSignal
@@ -322,55 +322,17 @@ namespace GradientSignal
return false;
}
void GradientTransformComponent::TransformPositionToUVW(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, const bool shouldNormalizeOutput, bool& wasPointRejected) const
void GradientTransformComponent::TransformPositionToUVW(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const
{
AZStd::lock_guard<decltype(m_cacheMutex)> lock(m_cacheMutex);
m_gradientTransform.TransformPositionToUVW(inPosition, outUVW, wasPointRejected);
}
//transforming coordinate into "local" relative space of shape bounds
outUVW = m_shapeTransformInverse * inPosition;
if (!m_configuration.m_advancedMode || !m_configuration.m_is3d)
{
outUVW.SetZ(0.0f);
}
wasPointRejected = false;
if (m_shapeBounds.IsValid())
{
//all wrap types and transformations are applied after the coordinate is transformed into shape relative space
//this allows all calculations to be simplified and done using the shapes untransformed aabb
//outputting a value that can be used to sample a gradient in its local space
switch (m_configuration.m_wrappingType)
{
default:
case WrappingType::None:
outUVW = GetUnboundedPointInAabb(outUVW, m_shapeBounds);
break;
case WrappingType::ClampToEdge:
outUVW = GetClampedPointInAabb(outUVW, m_shapeBounds);
break;
case WrappingType::ClampToZero:
// We don't want to use m_shapeBounds.Contains() here because Contains() is inclusive on all edges.
// For uv consistency between clamped and unclamped states, we only want to accept uv ranges of [min, max),
// so we specifically need to exclude the max edges here.
wasPointRejected = !(outUVW.IsGreaterEqualThan(m_shapeBounds.GetMin()) && outUVW.IsLessThan(m_shapeBounds.GetMax()));
outUVW = GetClampedPointInAabb(outUVW, m_shapeBounds);
break;
case WrappingType::Mirror:
outUVW = GetMirroredPointInAabb(outUVW, m_shapeBounds);
break;
case WrappingType::Repeat:
outUVW = GetWrappedPointInAabb(outUVW, m_shapeBounds);
break;
}
}
outUVW *= m_configuration.m_frequencyZoom;
if (shouldNormalizeOutput)
{
outUVW = GetNormalizedPointInAabb(outUVW, m_shapeBounds);
}
void GradientTransformComponent::TransformPositionToUVWNormalized(
const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const
{
AZStd::lock_guard<decltype(m_cacheMutex)> lock(m_cacheMutex);
m_gradientTransform.TransformPositionToUVWNormalized(inPosition, outUVW, wasPointRejected);
}
void GradientTransformComponent::GetGradientLocalBounds(AZ::Aabb& bounds) const
@@ -499,6 +461,11 @@ namespace GradientSignal
shapeTransformFinal.SetTranslation(m_configuration.m_translate);
shapeTransformFinal.MultiplyByScale(m_configuration.m_scale);
m_shapeTransformInverse = shapeTransformFinal.GetInverseFull();
// Set everything up on the Gradient Transform
const bool use3dGradients = m_configuration.m_advancedMode && m_configuration.m_is3d;
m_gradientTransform = GradientTransform(
m_shapeBounds, shapeTransformFinal, use3dGradients, m_configuration.m_frequencyZoom, m_configuration.m_wrappingType);
}
AZ::EntityId GradientTransformComponent::GetShapeEntityId() const
@@ -101,7 +101,8 @@ namespace GradientSignal
//////////////////////////////////////////////////////////////////////////
// GradientTransformRequestBus
void TransformPositionToUVW(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, const bool shouldNormalizeOutput, bool& wasPointRejected) const override;
void TransformPositionToUVW(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const override;
void TransformPositionToUVWNormalized(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const override;
void GetGradientLocalBounds(AZ::Aabb& bounds) const override;
void GetGradientEncompassingBounds(AZ::Aabb& bounds) const override;
@@ -172,5 +173,6 @@ namespace GradientSignal
AZ::Matrix3x4 m_shapeTransformInverse = AZ::Matrix3x4::CreateIdentity();
LmbrCentral::DependencyMonitor m_dependencyMonitor;
AZStd::atomic_bool m_dirty{ false };
GradientTransform m_gradientTransform;
};
} //namespace GradientSignal
@@ -194,9 +194,8 @@ namespace GradientSignal
AZ::Vector3 uvw = sampleParams.m_position;
bool wasPointRejected = false;
const bool shouldNormalizeOutput = true;
GradientTransformRequestBus::Event(
GetEntityId(), &GradientTransformRequestBus::Events::TransformPositionToUVW, sampleParams.m_position, uvw, shouldNormalizeOutput, wasPointRejected);
GetEntityId(), &GradientTransformRequestBus::Events::TransformPositionToUVWNormalized, sampleParams.m_position, uvw, wasPointRejected);
if (!wasPointRejected)
{
@@ -179,9 +179,8 @@ namespace GradientSignal
AZ::Vector3 uvw = sampleParams.m_position;
bool wasPointRejected = false;
const bool shouldNormalizeOutput = false;
GradientTransformRequestBus::Event(
GetEntityId(), &GradientTransformRequestBus::Events::TransformPositionToUVW, sampleParams.m_position, uvw, shouldNormalizeOutput, wasPointRejected);
GetEntityId(), &GradientTransformRequestBus::Events::TransformPositionToUVW, sampleParams.m_position, uvw, wasPointRejected);
if (!wasPointRejected)
{
@@ -142,9 +142,8 @@ namespace GradientSignal
AZ::Vector3 uvw = sampleParams.m_position;
bool wasPointRejected = false;
const bool shouldNormalizeOutput = false;
GradientTransformRequestBus::Event(
GetEntityId(), &GradientTransformRequestBus::Events::TransformPositionToUVW, sampleParams.m_position, uvw, shouldNormalizeOutput, wasPointRejected);
GetEntityId(), &GradientTransformRequestBus::Events::TransformPositionToUVW, sampleParams.m_position, uvw, wasPointRejected);
if (!wasPointRejected)
{
@@ -0,0 +1,163 @@
/*
* 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/Math/MathUtils.h>
#include <GradientSignal/GradientTransform.h>
namespace GradientSignal
{
GradientTransform::GradientTransform(
const AZ::Aabb& shapeBounds, const AZ::Matrix3x4& transform, bool use3d,
float frequencyZoom, GradientSignal::WrappingType wrappingType)
: m_shapeBounds(shapeBounds)
, m_inverseTransform(transform.GetInverseFull())
, m_frequencyZoom(frequencyZoom)
, m_wrappingType(wrappingType)
, m_wrappingTransform(NoTransform)
, m_alwaysAcceptPoint(true)
{
// If we want this to be a 2D gradient lookup, we always want to set the W result in the output to 0.
// The easiest / cheapest way to make this happen is just to clear out the third row in the inverseTransform.
if (!use3d)
{
m_inverseTransform.SetRow(2, AZ::Vector4::CreateZero());
}
// Set up the appropriate wrapping transform function for the the given wrapping type.
// Also note that ClampToZero is the only wrapping type that allows us to return a "pointIsRejected" result
// for points that fall outside the shape bounds.
if (m_shapeBounds.IsValid())
{
switch (wrappingType)
{
default:
case WrappingType::None:
m_wrappingTransform = GetUnboundedPointInAabb;
break;
case WrappingType::ClampToEdge:
m_wrappingTransform = GetClampedPointInAabb;
break;
case WrappingType::ClampToZero:
m_alwaysAcceptPoint = false;
m_wrappingTransform = GetClampedPointInAabb;
break;
case WrappingType::Mirror:
m_wrappingTransform = GetMirroredPointInAabb;
break;
case WrappingType::Repeat:
m_wrappingTransform = GetWrappedPointInAabb;
break;
}
}
m_normalizeExtentsReciprocal = AZ::Vector3(
AZ::IsClose(0.0f, m_shapeBounds.GetXExtent()) ? 0.0f : (1.0f / m_shapeBounds.GetXExtent()),
AZ::IsClose(0.0f, m_shapeBounds.GetYExtent()) ? 0.0f : (1.0f / m_shapeBounds.GetYExtent()),
AZ::IsClose(0.0f, m_shapeBounds.GetZExtent()) ? 0.0f : (1.0f / m_shapeBounds.GetZExtent()));
}
void GradientTransform::TransformPositionToUVW(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const
{
// Transform coordinate into "local" relative space of shape bounds, and set W to 0 if this is a 2D gradient.
outUVW = m_inverseTransform * inPosition;
// For most wrapping types, we always accept the point, but for ClampToZero we only accept it if it's within
// the shape bounds. We don't use m_shapeBounds.Contains() here because Contains() is inclusive on all edges.
// For uv consistency between clamped and unclamped states, we only want to accept uv ranges of [min, max),
// so we specifically need to exclude the max edges here.
bool wasPointAccepted = m_alwaysAcceptPoint ||
(outUVW.IsGreaterEqualThan(m_shapeBounds.GetMin()) && outUVW.IsLessThan(m_shapeBounds.GetMax()));
wasPointRejected = !wasPointAccepted;
outUVW = m_wrappingTransform(outUVW, m_shapeBounds);
outUVW *= m_frequencyZoom;
}
void GradientTransform::TransformPositionToUVWNormalized(const AZ::Vector3& inPosition, AZ::Vector3& outUVW, bool& wasPointRejected) const
{
TransformPositionToUVW(inPosition, outUVW, wasPointRejected);
// This effectively does AZ::LerpInverse(bounds.GetMin(), bounds.GetMax(), point) if shouldNormalize is true,
// and just returns outUVW if shouldNormalize is false.
outUVW = m_normalizeExtentsReciprocal * (outUVW - m_shapeBounds.GetMin());
}
AZ::Vector3 GradientTransform::NoTransform(const AZ::Vector3& point, const AZ::Aabb& /*bounds*/)
{
return point;
}
AZ::Vector3 GradientTransform::GetUnboundedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& /*bounds*/)
{
return point;
}
AZ::Vector3 GradientTransform::GetClampedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds)
{
// We want the clamped sampling states to clamp uvs to the [min, max) range.
return point.GetClamp(bounds.GetMin(), bounds.GetMax() - AZ::Vector3(UvEpsilon));
}
AZ::Vector3 GradientTransform::GetWrappedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds)
{
return AZ::Vector3(
AZ::Wrap(point.GetX(), bounds.GetMin().GetX(), bounds.GetMax().GetX()),
AZ::Wrap(point.GetY(), bounds.GetMin().GetY(), bounds.GetMax().GetY()),
AZ::Wrap(point.GetZ(), bounds.GetMin().GetZ(), bounds.GetMax().GetZ()));
}
AZ::Vector3 GradientTransform::GetMirroredPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds)
{
/* For mirroring, we want to produce the following pattern:
* [min, max) : value
* [max, min) : max - value - epsilon
* [min, max) : value
* [max, min) : max - value - epsilon
* ...
* The epsilon is because we always want to keep our output values in the [min, max) range. We apply the epsilon to all
* the mirrored values so that we get consistent spacing between the values.
*/
auto GetMirror = [](float value, float min, float max) -> float
{
// To calculate the mirror value, we move our value into relative space of [0, rangeX2), then use
// the first half of the range for our "[min, max)" range, and the second half for our "[max, min)" mirrored range.
float relativeValue = value - min;
float range = max - min;
float rangeX2 = range * 2.0f;
// A positive relativeValue will produce a value of [0, rangeX2) from a single mod, but a negative relativeValue
// will produce a value of (-rangeX2, 0]. Adding rangeX2 to the result and taking the mod again puts us back in
// the range of [0, rangeX2) for both negative and positive values. This keeps our mirroring pattern consistent and
// unbroken across both negative and positive coordinate space.
relativeValue = AZ::Mod(AZ::Mod(relativeValue, rangeX2) + rangeX2, rangeX2);
// [range, rangeX2) is our mirrored range, so flip the value when we're in this range and apply the epsilon so that
// we never return the max value, and so that our mirrored values have consistent spacing in the results.
if (relativeValue >= range)
{
relativeValue = rangeX2 - (relativeValue + UvEpsilon);
}
return relativeValue + min;
};
return AZ::Vector3(
GetMirror(point.GetX(), bounds.GetMin().GetX(), bounds.GetMax().GetX()),
GetMirror(point.GetY(), bounds.GetMin().GetY(), bounds.GetMax().GetY()),
GetMirror(point.GetZ(), bounds.GetMin().GetZ(), bounds.GetMax().GetZ()));
}
AZ::Vector3 GradientTransform::GetRelativePointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds)
{
return point - bounds.GetMin();
}
}
-77
View File
@@ -1,77 +0,0 @@
/*
* 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/TransformBus.h>
#include <AzCore/Debug/Profiler.h>
#include <AzCore/Math/MathUtils.h>
#include <LmbrCentral/Shape/ShapeComponentBus.h>
#include <GradientSignal/Util.h>
namespace GradientSignal
{
// To keep things behaving consistently between clamped and unbounded uv ranges, we
// we want our clamped uvs to use a range of [min, max), so we'll actually clamp to
// [min, max - epsilon]. Since our floating-point numbers are likely in the
// -16384 to 16384 range, an epsilon of 0.001 will work without rounding to 0.
static const float uvEpsilon = 0.001f;
AZ::Vector3 GetUnboundedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& /*bounds*/)
{
return point;
}
AZ::Vector3 GetClampedPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds)
{
// We want the clamped sampling states to clamp uvs to the [min, max) range.
return AZ::Vector3(
AZ::GetClamp(point.GetX(), bounds.GetMin().GetX(), bounds.GetMax().GetX() - uvEpsilon),
AZ::GetClamp(point.GetY(), bounds.GetMin().GetY(), bounds.GetMax().GetY() - uvEpsilon),
AZ::GetClamp(point.GetZ(), bounds.GetMin().GetZ(), bounds.GetMax().GetZ() - uvEpsilon));
}
float GetMirror(float value, float min, float max)
{
float relativeValue = value - min;
float range = max - min;
float rangeX2 = range * 2.0f;
if (relativeValue < 0.0)
{
relativeValue = rangeX2 - fmod(-relativeValue, rangeX2);
}
else
{
relativeValue = fmod(relativeValue, rangeX2);
}
if (relativeValue >= range)
{
// Since we want our uv range to stay in the [min, max) range,
// it means that for mirroring, we want both the "forward" values
// and the "mirrored" values to be in [0, range). We don't want
// relativeValue == range, so we shift relativeValue by a small epsilon
// in the mirrored case.
relativeValue = rangeX2 - (relativeValue + uvEpsilon);
}
return relativeValue + min;
}
AZ::Vector3 GetMirroredPointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds)
{
return AZ::Vector3(
GetMirror(point.GetX(), bounds.GetMin().GetX(), bounds.GetMax().GetX()),
GetMirror(point.GetY(), bounds.GetMin().GetY(), bounds.GetMax().GetY()),
GetMirror(point.GetZ(), bounds.GetMin().GetZ(), bounds.GetMax().GetZ()));
}
AZ::Vector3 GetRelativePointInAabb(const AZ::Vector3& point, const AZ::Aabb& bounds)
{
return point - bounds.GetMin();
}
}
@@ -0,0 +1,284 @@
/*
* 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 "Tests/GradientSignalTestMocks.h"
#include <AzTest/AzTest.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/Memory/PoolAllocator.h>
#include <AzCore/Math/Vector2.h>
#include <AZTestShared/Math/MathTestHelpers.h>
#include <Source/Components/GradientTransformComponent.h>
namespace UnitTest
{
struct GradientSignalTransformTestsFixture : public GradientSignalTest
{
// By default, we'll use a shape half extents of (5, 10, 20) for every test, and a world translation of (100, 200, 300).
struct GradientTransformSetupData
{
GradientSignal::WrappingType m_wrappingType{ GradientSignal::WrappingType::None };
AZ::Vector3 m_shapeHalfExtents{ 5.0f, 10.0f, 20.0f };
AZ::Vector3 m_worldTranslation{ 100.0f, 200.0f, 300.0f };
float m_frequencyZoom{ 1.0f };
};
struct GradientTransformTestData
{
AZ::Vector3 m_positionToTest;
AZ::Vector3 m_expectedOutputUVW;
bool m_expectedOutputRejectionResult;
};
static constexpr float UvEpsilon = GradientSignal::GradientTransform::UvEpsilon;
void TestGradientTransform(const GradientTransformSetupData& setup, const GradientTransformTestData& test)
{
AZ::Aabb shapeBounds = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), setup.m_shapeHalfExtents);
AZ::Matrix3x4 transform = AZ::Matrix3x4::CreateTranslation(setup.m_worldTranslation);
float frequencyZoom = setup.m_frequencyZoom;
GradientSignal::WrappingType wrappingType = setup.m_wrappingType;
AZ::Vector3 outUVW;
bool wasPointRejected;
// Perform the query with a 3D gradient and verify that the results match expectations.
GradientSignal::GradientTransform gradientTransform3d(shapeBounds, transform, true, frequencyZoom, wrappingType);
gradientTransform3d.TransformPositionToUVW(test.m_positionToTest, outUVW, wasPointRejected);
EXPECT_THAT(outUVW, IsClose(test.m_expectedOutputUVW));
EXPECT_EQ(wasPointRejected, test.m_expectedOutputRejectionResult);
// Perform the query with a 2D gradient and verify that the results match, but always returns a W value of 0.
GradientSignal::GradientTransform gradientTransform2d(shapeBounds, transform, false, frequencyZoom, wrappingType);
gradientTransform2d.TransformPositionToUVW(test.m_positionToTest, outUVW, wasPointRejected);
EXPECT_THAT(outUVW, IsClose(AZ::Vector3(test.m_expectedOutputUVW.GetX(), test.m_expectedOutputUVW.GetY(), 0.0f)));
EXPECT_EQ(wasPointRejected, test.m_expectedOutputRejectionResult);
}
};
TEST_F(GradientSignalTransformTestsFixture, UnboundedWrappingReturnsTranslatedInput)
{
GradientTransformSetupData setup = { GradientSignal::WrappingType::None };
GradientTransformTestData test = {
// Input position to query
{ 0.0f, 0.0f, 0.0f },
// Output: For no wrapping, the output is just the input position offset by the world translation.
{ -100.0f, -200.0f, -300.0f }, false
};
TestGradientTransform(setup, test);
}
TEST_F(GradientSignalTransformTestsFixture, ClampToEdgeReturnsValuesClampedToShapeBounds)
{
GradientTransformSetupData setup = { GradientSignal::WrappingType::ClampToEdge };
GradientTransformTestData tests[] = {
// Test: Input point far below minimum shape bounds
// Our input point is below the minimum of shape bounds, so the result should be the minimum corner of the shape.
{ { 0.0f, 0.0f, 0.0f }, { -5.0f, -10.0f, -20.0f }, false },
// Test: Input point directly on minimum shape bounds
// Our input point is directly on the minimum of shape bounds, so the result should be the minimum corner of the shape.
{ { 95.0f, 190.0f, 280.0f }, { -5.0f, -10.0f, -20.0f }, false },
// Test: Input point inside shape bounds
// Our input point is inside the shape bounds, so the result is just input - translation.
{ { 101.0f, 202.0f, 303.0f }, { 1.0f, 2.0f, 3.0f }, false },
// Test: Input point directly on maximum shape bounds
// On the maximum side, GradientTransform clamps to "max - epsilon" for consistency with other wrapping types, so our
// expected results are the max shape corner - epsilon.
{ { 105.0f, 210.0f, 320.0f }, { 5.0f - UvEpsilon, 10.0f - UvEpsilon, 20.0f - UvEpsilon }, false },
// Test: Input point far above maximum shape bounds
// On the maximum side, GradientTransform clamps to "max - epsilon" for consistency with other wrapping types, so our
// expected results are the max shape corner - epsilon.
{ { 1000.0f, 1000.0f, 1000.0f }, { 5.0f - UvEpsilon, 10.0f - UvEpsilon, 20.0f - UvEpsilon }, false },
};
for (auto& test : tests)
{
TestGradientTransform(setup, test);
}
}
TEST_F(GradientSignalTransformTestsFixture, MirrorReturnsValuesMirroredBasedOnShapeBounds)
{
/* Here's how the results are expected to work for various inputs when using Mirror wrapping.
* This assumes shape half extents of (5, 10, 20), and a center translation of (100, 200, 300):
* Inputs: Outputs:
* ... ...
* (75, 150, 200) - (85, 170, 240) (-5, -10, -20) to (5, 10, 20) // forward mirror
* (85, 170, 240) - (95, 190, 280) (5, 10, 20) to (-5, -10, -20) // back mirror
* (95, 190, 280) - (105, 210, 320) (-5, -10, -20) to (5, 10, 20) // starting point
* (105, 210, 320) - (115, 230, 360) (5, 10, 20) to (-5, -10, -20) // back mirror
* (115, 230, 360) - (125, 250, 400) (-5, -10, -20) to (5, 10, 20) // forward mirror
* ... ...
* When below the starting point, both forward and back mirrors will be adjusted by UvEpsilon except for points that fall on the
* shape minimums.
* When above the starting point, only back mirrors will be adjusted by UvEpsilon.
*/
GradientTransformSetupData setup = { GradientSignal::WrappingType::Mirror };
GradientTransformTestData tests[] = {
// Test: Input exactly 2x below minimum bounds
// When landing exactly on the 2x boundary, we return the minumum shape bounds. There is no adjustment by epsilon
// on the minimum side of the bounds, even when we're in a mirror below the shape bounds.
{ { 75.0f, 150.0f, 200.0f }, { -5.0f, -10.0f, -20.0f }, false },
// Test: Input within 2nd mirror repeat below minimum bounds
// The second mirror repeat should go forward in values, but will be adjusted by UvEpsilon since we're below the
// minimum bounds.
{ { 84.0f, 168.0f, 237.0f }, { 4.0f - UvEpsilon, 8.0f - UvEpsilon, 17.0f - UvEpsilon }, false },
// Test: Input exactly 1x below minimum bounds.
// When landing exactly on the 1x boundary, we return the maximum shape bounds minus epsilon.
{ { 85.0f, 170.0f, 240.0f }, { 5.0f - UvEpsilon, 10.0f - UvEpsilon, 20.0f - UvEpsilon }, false },
// Test: Input within 1st mirror repeat below minimum bounds
// The first mirror repeat should go backwards in values, but will be adjusted by UvEpsilon since we're below the
// minimum bounds.
{ { 94.0f, 188.0f, 277.0f }, { -4.0f - UvEpsilon, -8.0f - UvEpsilon, -17.0f - UvEpsilon }, false },
// Test: Input inside shape bounds
// The translated input position is (1, 2, 3) is inside the shape bounds, so we should just get the translated
// position back as output.
{ { 101.0f, 202.0f, 303.0f }, { 1.0f, 2.0f, 3.0f }, false },
// Test: Input within 1st mirror repeat above maximum bounds
// The first mirror repeat should go backwards in values. We're above the maximum bounds, so the expected result
// is (4, 8, 17) minus an epsilon.
{ { 106.0f, 212.0f, 323.0f }, { 4.0f - UvEpsilon, 8.0f - UvEpsilon, 17.0f - UvEpsilon }, false },
// Test: Input exactly 2x above minimum bounds.
// When landing exactly on the 2x boundary, we return the exact minimum value again.
{ { 115.0f, 230.0f, 360.0f }, { -5.0f, -10.0f, -20.0f }, false },
// Test: Input within 2nd mirror repeat above maximum bounds
// The second mirror repeat should go forwards in values. We're above the maximum bounds, so the expected result
// is (-4, -8, -17) with no epsilon.
{ { 116.0f, 232.0f, 363.0f }, { -4.0f, -8.0f, -17.0f }, false },
// Test: Input exactly 2x above maximum bounds
// When landing exactly on the 2x boundary, we return the maximum adjusted by the epsilon again.
{ { 125.0f, 250.0f, 400.0f }, { 5.0f - UvEpsilon, 10.0f - UvEpsilon, 20.0f - UvEpsilon }, false }
};
for (auto& test : tests)
{
TestGradientTransform(setup, test);
}
}
TEST_F(GradientSignalTransformTestsFixture, RepeatReturnsRepeatingValuesBasedOnShapeBounds)
{
/* Here's how the results are expected to work for various inputs when using Repeat wrapping.
* This assumes shape half extents of (5, 10, 20), and a center translation of (100, 200, 300):
* Inputs: Outputs:
* ... ...
* (75, 150, 200) - (85, 170, 240) (-5, -10, -20) to (5, 10, 20)
* (85, 170, 240) - (95, 190, 280) (-5, -10, -20) to (5, 10, 20)
* (95, 190, 280) - (105, 210, 320) (-5, -10, -20) to (5, 10, 20) // starting point
* (105, 210, 320) - (115, 230, 360) (-5, -10, -20) to (5, 10, 20)
* (115, 230, 360) - (125, 250, 400) (-5, -10, -20) to (5, 10, 20)
* ... ...
* Every shape min/max boundary point below the starting point will have the max shape value.
* Every shape min/max boundary point above the starting point with have the min shape value.
*/
GradientTransformSetupData setup = { GradientSignal::WrappingType::Repeat };
GradientTransformTestData tests[] = {
// Test: 2x below minimum shape bounds
// We're on a shape boundary below the minimum bounds, so it should return the maximum.
{ { 75.0f, 150.0f, 200.0f }, { 5.0f, 10.0f, 20.0f }, false },
// Test: Input within 2nd repeat below minimum shape bounds
// Every repeat should go forwards in values.
{ { 76.0f, 152.0f, 203.0f }, { -4.0f, -8.0f, -17.0f }, false },
// Test: 1x below minimum shape bounds
// We're on a shape boundary below the minimum bounds, so it should return the maximum.
{ { 85.0f, 170.0f, 240.0f }, { 5.0f, 10.0f, 20.0f }, false },
// Test: Input within 1st repeat below minimum shape bounds
// Every repeat should go forwards in values.
{ { 86.0f, 172.0f, 243.0f }, { -4.0f, -8.0f, -17.0f }, false },
// Test: Input exactly on minimum shape bounds
// This should return the actual minimum bounds.
{ { 95.0f, 190.0f, 280.0f }, { -5.0f, -10.0f, -20.0f }, false },
// Test: Input inside shape bounds
// This should return the mapped value.
{ { 101.0f, 202.0f, 303.0f }, { 1.0f, 2.0f, 3.0f }, false },
// Test: Input exactly on maximum shape bounds
// We're on a shape boundary above the minimum bounds, so it should return the minimum.
{ { 105.0f, 210.0f, 320.0f }, { -5.0f, -10.0f, -20.0f }, false },
// Test: Input within 1st repeat above maximum shape bounds
// Every repeat should go forwards in values.
{ { 106.0f, 212.0f, 323.0f }, { -4.0f, -8.0f, -17.0f }, false },
// Test: 1x above maximum shape bounds
// We're on a shape boundary above the minimum bounds, so it should return the minimum.
{ { 105.0f, 210.0f, 320.0f }, { -5.0f, -10.0f, -20.0f }, false },
// Test: Input within 2nd repeat above maximum shape bounds
// Every repeat should go forwards in values.
{ { 106.0f, 212.0f, 323.0f }, { -4.0f, -8.0f, -17.0f }, false },
};
for (auto& test : tests)
{
TestGradientTransform(setup, test);
}
}
TEST_F(GradientSignalTransformTestsFixture, ClampToZeroReturnsClampedValuesBasedOnShapeBounds)
{
GradientTransformSetupData setup = { GradientSignal::WrappingType::ClampToZero };
GradientTransformTestData tests[] = {
// Test: Input point far below minimum shape bounds
// Our input point is below the minimum of shape bounds, so the result should be the minimum corner of the shape.
// Points outside the shape bounds should return "true" for rejected.
{ { 0.0f, 0.0f, 0.0f }, { -5.0f, -10.0f, -20.0f }, true },
// Test: Input point directly on minimum shape bounds
// Our input point is directly on the minimum of shape bounds, so the result should be the minimum corner of the shape.
{ { 95.0f, 190.0f, 280.0f }, { -5.0f, -10.0f, -20.0f }, false },
// Test: Input point inside shape bounds
// Our input point is inside the shape bounds, so the result is just input - translation.
{ { 101.0f, 202.0f, 303.0f }, { 1.0f, 2.0f, 3.0f }, false },
// Test: Input point directly on maximum shape bounds
// On the maximum side, GradientTransform clamps to "max - epsilon" for consistency with other wrapping types, so our
// expected results are the max shape corner - epsilon.
// Points outside the shape bounds (which includes the maximum edge of the shape bounds) should return "true" for rejected.
{ { 105.0f, 210.0f, 320.0f }, { 5.0f - UvEpsilon, 10.0f - UvEpsilon, 20.0f - UvEpsilon }, true },
// Test: Input point far above maximum shape bounds
// On the maximum side, GradientTransform clamps to "max - epsilon" for consistency with other wrapping types, so our
// expected results are the max shape corner - epsilon.
// Points outside the shape bounds should return "true" for rejected.
{ { 1000.0f, 1000.0f, 1000.0f }, { 5.0f - UvEpsilon, 10.0f - UvEpsilon, 20.0f - UvEpsilon }, true },
};
for (auto& test : tests)
{
TestGradientTransform(setup, test);
}
}
}
@@ -8,6 +8,7 @@
set(FILES
Include/GradientSignal/GradientSampler.h
Include/GradientSignal/GradientTransform.h
Include/GradientSignal/SmoothStep.h
Include/GradientSignal/ImageAsset.h
Include/GradientSignal/ImageSettings.h
@@ -77,10 +78,10 @@ set(FILES
Source/GradientSampler.cpp
Source/GradientSignalSystemComponent.cpp
Source/GradientSignalSystemComponent.h
Source/GradientTransform.cpp
Source/SmoothStep.cpp
Source/ImageAsset.cpp
Source/ImageSettings.cpp
Source/PerlinImprovedNoise.cpp
Source/Util.cpp
Source/GradientImageConversion.cpp
)
@@ -11,6 +11,7 @@ set(FILES
Tests/GradientSignalReferencesTests.cpp
Tests/GradientSignalServicesTests.cpp
Tests/GradientSignalSurfaceTests.cpp
Tests/GradientSignalTransformTests.cpp
Tests/GradientSignalTestMocks.h
Tests/GradientSignalTest.cpp
Tests/ImageAssetTests.cpp