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:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
|
||||
+20
-4
@@ -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;
|
||||
|
||||
+2
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user