Node core: - MathNode/TrigonometryNode combo strings realigned with Operation enums - mathbase scalar/vector operand pick no longer uses bitwise type checks - NodeSetPositionAndDependenciesRecursively moves dependencies again - RemoveAllKeyframes undo actually restores keyframes - NodeGroup GetInputName null-deref guard, passthrough ids use input id - NodeValueTable::Has is an exact type match; tag fallback only for empty tags; kStrCombo/kPushButton get data type names - delete_all_keyframes no longer loops forever on unparented keyframes; keyframe-load failures propagate; rational interpolation falls back to double; OpacityEffect no longer leaks its internal MathNode Audio/footage: - AudioVisualWaveform: GetSummaryFromTime underflow OOB read, TrimIn prepend length bookkeeping, OverwriteSums source channel indexing - PanNode inserts the pan value into the sample job (keyframed pan works); OutputParamsChanged is emitted on device change; PortAudio device indices are validated before Pa_GetDeviceInfo - Footage: AdjustTimeByLoopMode no longer hangs/UBs on degenerate lengths, GetStreamIndex bounds-checked, CheckFootage clears stale state on missing files, failed probes are not cached, FootageDescription::Load requires its own root element Render/track: - ViewerOutput pushes the tagged samples value; TrackList disconnects the track-height lambda; GetTrackFromReference validity check - RenderManager dummy backend: null-initialized threads, guarded decoder-cache/timer paths; Renderer::Destroy releases color cache shaders/textures; unknown dynamic backends no longer alias to oakgl - SharedMemoryRegion POSIX attach validates segment size; ReadMessage skips blank lines instead of failing; GC counter clamped; IsRenderingCustomRange implemented; TimeOffsetNode gets a true inverse OutputTimeAdjustment; zero-speed clips return the held frame Plugin/nodes: - OliveClip: stored default region of definition is honored, on-demand images are cached; OliveHost sets host identity properties and logs instead of showing modal dialogs offscreen; Plugin.h dead decls gone - DespillNode guards graph-less use with Rec.709 fallback; description typos fixed (despill, swirl); mosaic applies when only one axis matches; Windows-only Project filename separator test fixed
1779 lines
62 KiB
C++
1779 lines
62 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <QMatrix4x4>
|
|
#include <QPointF>
|
|
#include <QStringList>
|
|
#include <QVector2D>
|
|
#include <QVector3D>
|
|
|
|
#include "node/color/colormanager/colormanager.h"
|
|
#include "node/distort/cornerpin/cornerpindistortnode.h"
|
|
#include "node/distort/crop/cropdistortnode.h"
|
|
#include "node/distort/flip/flipdistortnode.h"
|
|
#include "node/distort/mask/mask.h"
|
|
#include "node/distort/ripple/rippledistortnode.h"
|
|
#include "node/distort/swirl/swirldistortnode.h"
|
|
#include "node/distort/tile/tiledistortnode.h"
|
|
#include "node/distort/transform/transformdistortnode.h"
|
|
#include "node/distort/wave/wavedistortnode.h"
|
|
#include "node/filter/blur/blur.h"
|
|
#include "node/generator/polygon/polygon.h"
|
|
#include "node/generator/shape/generatorwithmerge.h"
|
|
#include "node/globals.h"
|
|
#include "node/project.h"
|
|
#include "node/traverser.h"
|
|
#include "olive/core/util/color.h"
|
|
#include "render/job/generatejob.h"
|
|
#include "render/job/shaderjob.h"
|
|
#include "render/loopmode.h"
|
|
#include "render/texture.h"
|
|
#include "render/videoparams.h"
|
|
#include "widget/slider/floatslider.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
// Node that pushes a fixed dummy texture, used to feed the texture input of
|
|
// distort nodes without any renderer (same pattern as node_generator_test).
|
|
class ConstantTextureNode : public olive::Node {
|
|
public:
|
|
ConstantTextureNode() = default;
|
|
|
|
NODE_DEFAULT_FUNCTIONS(ConstantTextureNode)
|
|
|
|
virtual QString Name() const override
|
|
{
|
|
return QStringLiteral("Test Texture");
|
|
}
|
|
|
|
virtual QString id() const override
|
|
{
|
|
return QStringLiteral("org.oak.test.distort_texture");
|
|
}
|
|
|
|
virtual QVector<CategoryID> Category() const override
|
|
{
|
|
return { kCategoryGenerator };
|
|
}
|
|
|
|
void SetTexture(const olive::TexturePtr &texture)
|
|
{
|
|
texture_ = texture;
|
|
}
|
|
|
|
virtual void Value(const olive::NodeValueRow &value,
|
|
const olive::NodeGlobals &globals,
|
|
olive::NodeValueTable *table) const override
|
|
{
|
|
Q_UNUSED(value)
|
|
Q_UNUSED(globals)
|
|
|
|
table->Push(olive::NodeValue(olive::NodeValue::kTexture, texture_, this));
|
|
}
|
|
|
|
private:
|
|
olive::TexturePtr texture_;
|
|
};
|
|
|
|
template <typename T> T *AddNode(olive::Project *project)
|
|
{
|
|
T *node = new T();
|
|
node->setParent(project);
|
|
return node;
|
|
}
|
|
|
|
olive::TimeRange FirstFrame()
|
|
{
|
|
return olive::TimeRange(olive::rational(0), olive::rational(1, 30));
|
|
}
|
|
|
|
// A fresh traverser per call: NodeTraverser caches tables per node/range, so
|
|
// reusing one would return stale results after changing standard values.
|
|
olive::NodeValueTable GenerateTable(const olive::Node *node,
|
|
const olive::VideoParams &vparams)
|
|
{
|
|
olive::NodeTraverser traverser;
|
|
traverser.SetCacheVideoParams(vparams);
|
|
return traverser.GenerateTable(node, FirstFrame());
|
|
}
|
|
|
|
// A "dummy" texture has no renderer backend and is therefore safe to pass
|
|
// around in a headless, CPU-only test.
|
|
olive::TexturePtr MakeDummyTexture(int width, int height)
|
|
{
|
|
return std::make_shared<olive::Texture>(
|
|
olive::VideoParams(width, height, olive::core::PixelFormat::U8,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
}
|
|
|
|
olive::VideoParams SequenceParams(int width, int height)
|
|
{
|
|
return olive::VideoParams(width, height, olive::core::PixelFormat::F32,
|
|
olive::VideoParams::kRGBAChannelCount);
|
|
}
|
|
|
|
olive::NodeValue TextureValue(const olive::TexturePtr &texture)
|
|
{
|
|
return olive::NodeValue(olive::NodeValue::kTexture, texture);
|
|
}
|
|
|
|
olive::NodeValue FloatValue(double v)
|
|
{
|
|
return olive::NodeValue(olive::NodeValue::kFloat, v);
|
|
}
|
|
|
|
olive::NodeValue BoolValue(bool b)
|
|
{
|
|
return olive::NodeValue(olive::NodeValue::kBoolean, b);
|
|
}
|
|
|
|
olive::NodeValue Vec2Value(const QVector2D &v)
|
|
{
|
|
return olive::NodeValue(olive::NodeValue::kVec2, v);
|
|
}
|
|
|
|
olive::NodeValueRow MakeTextureRow(const QString &input,
|
|
const olive::TexturePtr &tex)
|
|
{
|
|
olive::NodeValueRow row;
|
|
row.insert(input, TextureValue(tex));
|
|
return row;
|
|
}
|
|
|
|
olive::TexturePtr GetOutputTexture(const olive::NodeValueTable &table)
|
|
{
|
|
return table.Get(olive::NodeValue::kTexture).toTexture();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// TransformDistortNode
|
|
// -----------------------------------------------------------------------------
|
|
|
|
TEST(TransformDistortNode, MetadataIsCorrect)
|
|
{
|
|
olive::TransformDistortNode node;
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.olivevideoeditor.Olive.transform"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Transform"));
|
|
// ShortName() overrides MatrixGenerator's "Ortho"
|
|
EXPECT_EQ(node.ShortName(), QStringLiteral("Transform"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
|
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
|
EXPECT_EQ(node.GetEffectInputID(), olive::TransformDistortNode::kTextureInput);
|
|
}
|
|
|
|
TEST(TransformDistortNode, InputDefinitionsAndDefaults)
|
|
{
|
|
olive::TransformDistortNode node;
|
|
|
|
// Texture is prepended, so it is the primary effect input and cannot be
|
|
// keyframed
|
|
ASSERT_TRUE(node.HasInputWithID(olive::TransformDistortNode::kTextureInput));
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::TransformDistortNode::kTextureInput)),
|
|
int(olive::NodeValue::kTexture));
|
|
EXPECT_FALSE(
|
|
node.IsInputKeyframable(olive::TransformDistortNode::kTextureInput));
|
|
|
|
ASSERT_TRUE(node.HasInputWithID(olive::TransformDistortNode::kParentInput));
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::TransformDistortNode::kParentInput)),
|
|
int(olive::NodeValue::kMatrix));
|
|
|
|
ASSERT_TRUE(node.HasInputWithID(olive::TransformDistortNode::kAutoscaleInput));
|
|
EXPECT_EQ(
|
|
int(node.GetInputDataType(olive::TransformDistortNode::kAutoscaleInput)),
|
|
int(olive::NodeValue::kCombo));
|
|
EXPECT_EQ(node.GetStandardValue(olive::TransformDistortNode::kAutoscaleInput)
|
|
.toInt(),
|
|
int(olive::TransformDistortNode::kAutoScaleNone));
|
|
|
|
ASSERT_TRUE(
|
|
node.HasInputWithID(olive::TransformDistortNode::kInterpolationInput));
|
|
EXPECT_EQ(int(node.GetInputDataType(
|
|
olive::TransformDistortNode::kInterpolationInput)),
|
|
int(olive::NodeValue::kCombo));
|
|
// 2 = mipmapped bilinear
|
|
EXPECT_EQ(node.GetStandardValue(
|
|
olive::TransformDistortNode::kInterpolationInput)
|
|
.toInt(),
|
|
int(olive::Texture::kMipmappedLinear));
|
|
|
|
// MatrixGenerator inputs are inherited
|
|
EXPECT_TRUE(node.HasInputWithID(olive::MatrixGenerator::kPositionInput));
|
|
EXPECT_TRUE(node.HasInputWithID(olive::MatrixGenerator::kRotationInput));
|
|
EXPECT_TRUE(node.HasInputWithID(olive::MatrixGenerator::kScaleInput));
|
|
EXPECT_TRUE(node.HasInputWithID(olive::MatrixGenerator::kUniformScaleInput));
|
|
EXPECT_TRUE(node.HasInputWithID(olive::MatrixGenerator::kAnchorInput));
|
|
}
|
|
|
|
TEST(TransformDistortNode, RetranslateSetsNamesAndComboStrings)
|
|
{
|
|
olive::TransformDistortNode node;
|
|
node.Retranslate();
|
|
|
|
EXPECT_EQ(node.GetInputName(olive::TransformDistortNode::kTextureInput),
|
|
QStringLiteral("Texture"));
|
|
EXPECT_EQ(node.GetInputName(olive::TransformDistortNode::kParentInput),
|
|
QStringLiteral("Parent"));
|
|
EXPECT_EQ(node.GetInputName(olive::TransformDistortNode::kAutoscaleInput),
|
|
QStringLiteral("Auto-Scale"));
|
|
EXPECT_EQ(node.GetInputName(olive::TransformDistortNode::kInterpolationInput),
|
|
QStringLiteral("Interpolation"));
|
|
|
|
// Inherited names from MatrixGenerator
|
|
EXPECT_EQ(node.GetInputName(olive::MatrixGenerator::kPositionInput),
|
|
QStringLiteral("Position"));
|
|
EXPECT_EQ(node.GetInputName(olive::MatrixGenerator::kAnchorInput),
|
|
QStringLiteral("Anchor Point"));
|
|
|
|
const QStringList autoscale = node.GetComboBoxStrings(
|
|
olive::TransformDistortNode::kAutoscaleInput);
|
|
ASSERT_EQ(autoscale.size(), 4);
|
|
EXPECT_EQ(autoscale.at(int(olive::TransformDistortNode::kAutoScaleNone)),
|
|
QStringLiteral("None"));
|
|
EXPECT_EQ(autoscale.at(int(olive::TransformDistortNode::kAutoScaleFit)),
|
|
QStringLiteral("Fit"));
|
|
EXPECT_EQ(autoscale.at(int(olive::TransformDistortNode::kAutoScaleFill)),
|
|
QStringLiteral("Fill"));
|
|
EXPECT_EQ(autoscale.at(int(olive::TransformDistortNode::kAutoScaleStretch)),
|
|
QStringLiteral("Stretch"));
|
|
|
|
const QStringList interpolation = node.GetComboBoxStrings(
|
|
olive::TransformDistortNode::kInterpolationInput);
|
|
ASSERT_EQ(interpolation.size(), 3);
|
|
EXPECT_EQ(interpolation.at(int(olive::Texture::kNearest)),
|
|
QStringLiteral("Nearest Neighbor"));
|
|
EXPECT_EQ(interpolation.at(int(olive::Texture::kLinear)),
|
|
QStringLiteral("Bilinear"));
|
|
EXPECT_EQ(interpolation.at(int(olive::Texture::kMipmappedLinear)),
|
|
QStringLiteral("Mipmapped Bilinear"));
|
|
}
|
|
|
|
TEST(TransformDistortNode, GetShaderCodeReturnsEmptyCode)
|
|
{
|
|
olive::TransformDistortNode node;
|
|
|
|
// The transform is applied through the ove_mvpmat uniform of the default
|
|
// shader, so the node provides no shader code of its own
|
|
const olive::ShaderCode code = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("anything")));
|
|
EXPECT_TRUE(code.frag_code().isEmpty());
|
|
EXPECT_TRUE(code.vert_code().isEmpty());
|
|
}
|
|
|
|
TEST(TransformDistortNode, AdjustMatrixByResolutionsIdentityWhenMatching)
|
|
{
|
|
// With identical sequence and texture resolutions, no offset and an
|
|
// identity input matrix, the adjusted matrix must remain identity (the
|
|
// scale to clip space and back cancels out)
|
|
const QMatrix4x4 adjusted = olive::TransformDistortNode::AdjustMatrixByResolutions(
|
|
QMatrix4x4(), QVector2D(1024.0f, 1024.0f), QVector2D(1024.0f, 1024.0f),
|
|
QVector2D(0.0f, 0.0f), olive::TransformDistortNode::kAutoScaleNone);
|
|
EXPECT_TRUE(adjusted.isIdentity());
|
|
}
|
|
|
|
TEST(TransformDistortNode, AdjustMatrixByResolutionsAppliesOffset)
|
|
{
|
|
// The offset lives in texture pixel space: with matching 1024x1024
|
|
// resolutions, offsetting by (128, 256) maps the origin to clip space
|
|
// (128*2/1024, 256*2/1024) = (0.25, 0.5)
|
|
const QMatrix4x4 adjusted = olive::TransformDistortNode::AdjustMatrixByResolutions(
|
|
QMatrix4x4(), QVector2D(1024.0f, 1024.0f), QVector2D(1024.0f, 1024.0f),
|
|
QVector2D(128.0f, 256.0f), olive::TransformDistortNode::kAutoScaleNone);
|
|
|
|
const QVector3D mapped = adjusted.map(QVector3D(0.0f, 0.0f, 0.0f));
|
|
EXPECT_FLOAT_EQ(mapped.x(), 0.25f);
|
|
EXPECT_FLOAT_EQ(mapped.y(), 0.5f);
|
|
EXPECT_FLOAT_EQ(mapped.z(), 0.0f);
|
|
}
|
|
|
|
TEST(TransformDistortNode, AdjustMatrixByResolutionsScalesToClipSpace)
|
|
{
|
|
// Without auto-scale a 512x256 texture in a 1024x512 sequence covers only
|
|
// half the frame in each axis
|
|
const QMatrix4x4 adjusted = olive::TransformDistortNode::AdjustMatrixByResolutions(
|
|
QMatrix4x4(), QVector2D(1024.0f, 512.0f), QVector2D(512.0f, 256.0f),
|
|
QVector2D(0.0f, 0.0f), olive::TransformDistortNode::kAutoScaleNone);
|
|
|
|
const QVector3D corner = adjusted.map(QVector3D(1.0f, 1.0f, 0.0f));
|
|
EXPECT_FLOAT_EQ(corner.x(), 0.5f);
|
|
EXPECT_FLOAT_EQ(corner.y(), 0.5f);
|
|
}
|
|
|
|
TEST(TransformDistortNode, AdjustMatrixByResolutionsStretchFillsSequence)
|
|
{
|
|
// Stretch distorts the texture to the sequence aspect ratio exactly
|
|
const QMatrix4x4 adjusted = olive::TransformDistortNode::AdjustMatrixByResolutions(
|
|
QMatrix4x4(), QVector2D(1024.0f, 512.0f), QVector2D(512.0f, 256.0f),
|
|
QVector2D(0.0f, 0.0f), olive::TransformDistortNode::kAutoScaleStretch);
|
|
|
|
const QVector3D corner = adjusted.map(QVector3D(1.0f, 1.0f, 0.0f));
|
|
EXPECT_FLOAT_EQ(corner.x(), 1.0f);
|
|
EXPECT_FLOAT_EQ(corner.y(), 1.0f);
|
|
}
|
|
|
|
TEST(TransformDistortNode, AdjustMatrixByResolutionsFitWideFootage)
|
|
{
|
|
// Footage wider than the sequence (AR 4.0 in AR 2.0) is scaled by width,
|
|
// leaving letterbox bars: the vertical clip extent shrinks to 0.5
|
|
const QMatrix4x4 fit = olive::TransformDistortNode::AdjustMatrixByResolutions(
|
|
QMatrix4x4(), QVector2D(1024.0f, 512.0f), QVector2D(1024.0f, 256.0f),
|
|
QVector2D(0.0f, 0.0f), olive::TransformDistortNode::kAutoScaleFit);
|
|
|
|
const QVector3D corner = fit.map(QVector3D(1.0f, 1.0f, 0.0f));
|
|
EXPECT_FLOAT_EQ(corner.x(), 1.0f);
|
|
EXPECT_FLOAT_EQ(corner.y(), 0.5f);
|
|
}
|
|
|
|
TEST(TransformDistortNode, AdjustMatrixByResolutionsFillWideFootage)
|
|
{
|
|
// Fill scales the same footage by height instead, cropping the sides
|
|
const QMatrix4x4 fill = olive::TransformDistortNode::AdjustMatrixByResolutions(
|
|
QMatrix4x4(), QVector2D(1024.0f, 512.0f), QVector2D(1024.0f, 256.0f),
|
|
QVector2D(0.0f, 0.0f), olive::TransformDistortNode::kAutoScaleFill);
|
|
|
|
const QVector3D corner = fill.map(QVector3D(1.0f, 1.0f, 0.0f));
|
|
EXPECT_FLOAT_EQ(corner.x(), 2.0f);
|
|
EXPECT_FLOAT_EQ(corner.y(), 1.0f);
|
|
}
|
|
|
|
TEST(TransformDistortNode, AdjustMatrixByResolutionsFitTallFootage)
|
|
{
|
|
// Footage narrower than the sequence (AR 0.5 in AR 2.0) is scaled by
|
|
// height, leaving pillarbox bars
|
|
const QMatrix4x4 fit = olive::TransformDistortNode::AdjustMatrixByResolutions(
|
|
QMatrix4x4(), QVector2D(1024.0f, 512.0f), QVector2D(256.0f, 512.0f),
|
|
QVector2D(0.0f, 0.0f), olive::TransformDistortNode::kAutoScaleFit);
|
|
|
|
const QVector3D corner = fit.map(QVector3D(1.0f, 1.0f, 0.0f));
|
|
EXPECT_FLOAT_EQ(corner.x(), 0.25f);
|
|
EXPECT_FLOAT_EQ(corner.y(), 1.0f);
|
|
}
|
|
|
|
TEST(TransformDistortNode, ValueWithoutTexturePushesMatrixOnly)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TransformDistortNode>(&project);
|
|
|
|
olive::NodeValueTable table = GenerateTable(node, SequenceParams(1920, 1080));
|
|
|
|
// The generated matrix is always pushed; with no texture connected the
|
|
// re-pushed texture value is a null texture
|
|
const olive::NodeValue matrix = table.Get(olive::NodeValue::kMatrix);
|
|
ASSERT_EQ(int(matrix.type()), int(olive::NodeValue::kMatrix));
|
|
EXPECT_TRUE(matrix.toMatrix().isIdentity());
|
|
|
|
EXPECT_TRUE(table.Get(olive::NodeValue::kTexture).toTexture() == nullptr);
|
|
}
|
|
|
|
TEST(TransformDistortNode, ValueWithIdentityTransformPassesTextureThrough)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TransformDistortNode>(&project);
|
|
auto *constant = AddNode<ConstantTextureNode>(&project);
|
|
|
|
// Texture matching the sequence resolution with default transform values
|
|
// produces an identity adjusted matrix, which the node treats as a no-op
|
|
const olive::TexturePtr base = MakeDummyTexture(1024, 1024);
|
|
constant->SetTexture(base);
|
|
olive::Node::ConnectEdge(
|
|
constant,
|
|
olive::NodeInput(node, olive::TransformDistortNode::kTextureInput));
|
|
|
|
olive::NodeValueTable table = GenerateTable(node, SequenceParams(1024, 1024));
|
|
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
EXPECT_EQ(out, base);
|
|
EXPECT_FALSE(out->IsJob());
|
|
|
|
EXPECT_TRUE(table.Get(olive::NodeValue::kMatrix).toMatrix().isIdentity());
|
|
}
|
|
|
|
TEST(TransformDistortNode, ValueWithTexturePushesMatrixJob)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TransformDistortNode>(&project);
|
|
auto *constant = AddNode<ConstantTextureNode>(&project);
|
|
|
|
// A 64x48 texture in a 1920x1080 sequence yields a non-identity matrix
|
|
const olive::TexturePtr base = MakeDummyTexture(64, 48);
|
|
constant->SetTexture(base);
|
|
olive::Node::ConnectEdge(
|
|
constant,
|
|
olive::NodeInput(node, olive::TransformDistortNode::kTextureInput));
|
|
|
|
olive::NodeValueTable table = GenerateTable(node, SequenceParams(1920, 1080));
|
|
|
|
olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
|
|
// The job adopts the sequence resolution, not the texture's, since the
|
|
// transform may change the apparent size
|
|
EXPECT_EQ(out->params().width(), 1920);
|
|
EXPECT_EQ(out->params().height(), 1080);
|
|
|
|
auto *job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
|
|
// The original texture is fed in as ove_maintex
|
|
EXPECT_EQ(job->Get(QStringLiteral("ove_maintex")).toTexture(), base);
|
|
|
|
// The mvp matrix scales the texture into sequence clip space:
|
|
// 64/1920 on X and 48/1080 on Y
|
|
const QMatrix4x4 mvp = job->Get(QStringLiteral("ove_mvpmat")).toMatrix();
|
|
EXPECT_NEAR(mvp(0, 0), 64.0 / 1920.0, 1e-6);
|
|
EXPECT_NEAR(mvp(1, 1), 48.0 / 1080.0, 1e-6);
|
|
EXPECT_FLOAT_EQ(mvp(2, 2), 1.0f);
|
|
EXPECT_FLOAT_EQ(mvp(3, 3), 1.0f);
|
|
|
|
// The raw generated matrix (identity here) is pushed alongside the job
|
|
EXPECT_TRUE(table.Get(olive::NodeValue::kMatrix).toMatrix().isIdentity());
|
|
|
|
// Interpolation defaults to mipmapped bilinear and follows the input
|
|
EXPECT_EQ(int(job->GetInterpolation(QStringLiteral("ove_maintex"))),
|
|
int(olive::Texture::kMipmappedLinear));
|
|
|
|
node->SetStandardValue(olive::TransformDistortNode::kInterpolationInput,
|
|
int(olive::Texture::kNearest));
|
|
table = GenerateTable(node, SequenceParams(1920, 1080));
|
|
out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_EQ(int(job->GetInterpolation(QStringLiteral("ove_maintex"))),
|
|
int(olive::Texture::kNearest));
|
|
}
|
|
|
|
TEST(TransformDistortNode, ValueBakesPositionIntoJobMatrix)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TransformDistortNode>(&project);
|
|
node->SetStandardValue(olive::MatrixGenerator::kPositionInput,
|
|
QVector2D(100.0f, 50.0f));
|
|
|
|
auto *constant = AddNode<ConstantTextureNode>(&project);
|
|
constant->SetTexture(MakeDummyTexture(64, 48));
|
|
olive::Node::ConnectEdge(
|
|
constant,
|
|
olive::NodeInput(node, olive::TransformDistortNode::kTextureInput));
|
|
|
|
olive::NodeValueTable table = GenerateTable(node, SequenceParams(1920, 1080));
|
|
|
|
// The table matrix is the pure transform: a 100x50 pixel translation
|
|
const QMatrix4x4 generated = table.Get(olive::NodeValue::kMatrix).toMatrix();
|
|
const QVector3D raw = generated.map(QVector3D(0.0f, 0.0f, 0.0f));
|
|
EXPECT_FLOAT_EQ(raw.x(), 100.0f);
|
|
EXPECT_FLOAT_EQ(raw.y(), 50.0f);
|
|
|
|
// The job matrix expresses the same translation in clip space
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
auto *job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
|
|
const QVector3D clip = job->Get(QStringLiteral("ove_mvpmat"))
|
|
.toMatrix()
|
|
.map(QVector3D(0.0f, 0.0f, 0.0f));
|
|
EXPECT_NEAR(clip.x(), 100.0 * 2.0 / 1920.0, 1e-6);
|
|
EXPECT_NEAR(clip.y(), 50.0 * 2.0 / 1080.0, 1e-6);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// CropDistortNode
|
|
// -----------------------------------------------------------------------------
|
|
|
|
TEST(CropDistortNode, MetadataIsCorrect)
|
|
{
|
|
olive::CropDistortNode node;
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.olivevideoeditor.Olive.crop"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Crop"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
|
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
|
EXPECT_EQ(node.GetEffectInputID(), olive::CropDistortNode::kTextureInput);
|
|
}
|
|
|
|
TEST(CropDistortNode, InputDefinitionsAndDefaults)
|
|
{
|
|
olive::CropDistortNode node;
|
|
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::CropDistortNode::kTextureInput)),
|
|
int(olive::NodeValue::kTexture));
|
|
EXPECT_FALSE(node.IsInputKeyframable(olive::CropDistortNode::kTextureInput));
|
|
|
|
// All four sides are 0..1 percentage sliders defaulting to zero
|
|
const QString sides[] = { olive::CropDistortNode::kLeftInput,
|
|
olive::CropDistortNode::kTopInput,
|
|
olive::CropDistortNode::kRightInput,
|
|
olive::CropDistortNode::kBottomInput };
|
|
for (const QString &side : sides) {
|
|
EXPECT_EQ(int(node.GetInputDataType(side)), int(olive::NodeValue::kFloat));
|
|
EXPECT_DOUBLE_EQ(node.GetStandardValue(side).toDouble(), 0.0);
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetInputProperty(side, QStringLiteral("min")).toDouble(), 0.0);
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetInputProperty(side, QStringLiteral("max")).toDouble(), 1.0);
|
|
EXPECT_EQ(node.GetInputProperty(side, QStringLiteral("view")).toInt(),
|
|
int(olive::FloatSlider::kPercentage));
|
|
}
|
|
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetStandardValue(olive::CropDistortNode::kFeatherInput).toDouble(),
|
|
0.0);
|
|
EXPECT_DOUBLE_EQ(node.GetInputProperty(olive::CropDistortNode::kFeatherInput,
|
|
QStringLiteral("min"))
|
|
.toDouble(),
|
|
0.0);
|
|
}
|
|
|
|
TEST(CropDistortNode, RetranslateSetsInputNames)
|
|
{
|
|
olive::CropDistortNode node;
|
|
node.Retranslate();
|
|
|
|
EXPECT_EQ(node.GetInputName(olive::CropDistortNode::kTextureInput),
|
|
QStringLiteral("Texture"));
|
|
EXPECT_EQ(node.GetInputName(olive::CropDistortNode::kLeftInput),
|
|
QStringLiteral("Left"));
|
|
EXPECT_EQ(node.GetInputName(olive::CropDistortNode::kTopInput),
|
|
QStringLiteral("Top"));
|
|
EXPECT_EQ(node.GetInputName(olive::CropDistortNode::kRightInput),
|
|
QStringLiteral("Right"));
|
|
EXPECT_EQ(node.GetInputName(olive::CropDistortNode::kBottomInput),
|
|
QStringLiteral("Bottom"));
|
|
EXPECT_EQ(node.GetInputName(olive::CropDistortNode::kFeatherInput),
|
|
QStringLiteral("Feather"));
|
|
}
|
|
|
|
TEST(CropDistortNode, GetShaderCodeLoadsCropShader)
|
|
{
|
|
olive::CropDistortNode node;
|
|
|
|
const olive::ShaderCode code = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("anything")));
|
|
EXPECT_FALSE(code.frag_code().isEmpty());
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("left_in")));
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("feather_in")));
|
|
EXPECT_TRUE(code.vert_code().isEmpty());
|
|
}
|
|
|
|
TEST(CropDistortNode, ValueWithoutTexturePushesNothing)
|
|
{
|
|
olive::CropDistortNode node;
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
|
|
|
|
EXPECT_EQ(table.Count(), 0);
|
|
}
|
|
|
|
TEST(CropDistortNode, ValueWithZeroCropPassesTextureThrough)
|
|
{
|
|
olive::CropDistortNode node;
|
|
|
|
const olive::TexturePtr tex = MakeDummyTexture(120, 80);
|
|
olive::NodeValueTable table;
|
|
node.Value(MakeTextureRow(olive::CropDistortNode::kTextureInput, tex),
|
|
olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
EXPECT_EQ(out, tex);
|
|
EXPECT_FALSE(out->IsJob());
|
|
}
|
|
|
|
TEST(CropDistortNode, ValueWithOnlyFeatherPassesTextureThrough)
|
|
{
|
|
olive::CropDistortNode node;
|
|
|
|
// NOTE: the node only checks the four crop sides when deciding to run the
|
|
// shader; a feather without any crop is silently ignored
|
|
const olive::TexturePtr tex = MakeDummyTexture(120, 80);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::CropDistortNode::kTextureInput, tex);
|
|
row.insert(olive::CropDistortNode::kFeatherInput, FloatValue(5.0));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
EXPECT_EQ(out, tex);
|
|
EXPECT_FALSE(out->IsJob());
|
|
}
|
|
|
|
TEST(CropDistortNode, ValueWithCropPushesShaderJob)
|
|
{
|
|
olive::CropDistortNode node;
|
|
|
|
const olive::TexturePtr tex = MakeDummyTexture(120, 80);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::CropDistortNode::kTextureInput, tex);
|
|
row.insert(olive::CropDistortNode::kLeftInput, FloatValue(0.25));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
|
|
// The job reuses the input texture's params
|
|
EXPECT_EQ(out->params().width(), tex->params().width());
|
|
EXPECT_EQ(out->params().height(), tex->params().height());
|
|
|
|
auto *job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_DOUBLE_EQ(job->Get(olive::CropDistortNode::kLeftInput).toDouble(),
|
|
0.25);
|
|
EXPECT_EQ(job->Get(QStringLiteral("resolution_in")).toVec2(),
|
|
QVector2D(120.0f, 80.0f));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// FlipDistortNode
|
|
// -----------------------------------------------------------------------------
|
|
|
|
TEST(FlipDistortNode, MetadataIsCorrect)
|
|
{
|
|
olive::FlipDistortNode node;
|
|
// NOTE: unlike most Olive nodes ("org.olivevideoeditor.Olive.*"), the
|
|
// flip/ripple/swirl/tile/wave nodes use the "org.oliveeditor.Olive.*"
|
|
// domain (inconsistent ID, documented here as a suspected bug)
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.oliveeditor.Olive.flip"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Flip"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
|
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
|
EXPECT_EQ(node.GetEffectInputID(), olive::FlipDistortNode::kTextureInput);
|
|
}
|
|
|
|
TEST(FlipDistortNode, InputDefaults)
|
|
{
|
|
olive::FlipDistortNode node;
|
|
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::FlipDistortNode::kTextureInput)),
|
|
int(olive::NodeValue::kTexture));
|
|
EXPECT_FALSE(node.IsInputKeyframable(olive::FlipDistortNode::kTextureInput));
|
|
|
|
EXPECT_EQ(
|
|
int(node.GetInputDataType(olive::FlipDistortNode::kHorizontalInput)),
|
|
int(olive::NodeValue::kBoolean));
|
|
EXPECT_FALSE(node.GetStandardValue(olive::FlipDistortNode::kHorizontalInput)
|
|
.toBool());
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::FlipDistortNode::kVerticalInput)),
|
|
int(olive::NodeValue::kBoolean));
|
|
EXPECT_FALSE(node.GetStandardValue(olive::FlipDistortNode::kVerticalInput)
|
|
.toBool());
|
|
}
|
|
|
|
TEST(FlipDistortNode, RetranslateSetsInputNames)
|
|
{
|
|
olive::FlipDistortNode node;
|
|
node.Retranslate();
|
|
|
|
EXPECT_EQ(node.GetInputName(olive::FlipDistortNode::kTextureInput),
|
|
QStringLiteral("Input"));
|
|
EXPECT_EQ(node.GetInputName(olive::FlipDistortNode::kHorizontalInput),
|
|
QStringLiteral("Horizontal"));
|
|
EXPECT_EQ(node.GetInputName(olive::FlipDistortNode::kVerticalInput),
|
|
QStringLiteral("Vertical"));
|
|
}
|
|
|
|
TEST(FlipDistortNode, GetShaderCodeLoadsFlipShader)
|
|
{
|
|
olive::FlipDistortNode node;
|
|
|
|
const olive::ShaderCode code = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("anything")));
|
|
EXPECT_FALSE(code.frag_code().isEmpty());
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("horiz_in")));
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("vert_in")));
|
|
}
|
|
|
|
TEST(FlipDistortNode, ValueWithoutTexturePushesNothing)
|
|
{
|
|
olive::FlipDistortNode node;
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
|
|
|
|
EXPECT_EQ(table.Count(), 0);
|
|
}
|
|
|
|
TEST(FlipDistortNode, ValueWithNoFlipPassesTextureThrough)
|
|
{
|
|
olive::FlipDistortNode node;
|
|
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
olive::NodeValueTable table;
|
|
node.Value(MakeTextureRow(olive::FlipDistortNode::kTextureInput, tex),
|
|
olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
EXPECT_EQ(out, tex);
|
|
EXPECT_FALSE(out->IsJob());
|
|
}
|
|
|
|
TEST(FlipDistortNode, ValueWithFlipPushesShaderJob)
|
|
{
|
|
olive::FlipDistortNode node;
|
|
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::FlipDistortNode::kTextureInput, tex);
|
|
row.insert(olive::FlipDistortNode::kHorizontalInput, BoolValue(true));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
EXPECT_EQ(out->params().width(), tex->params().width());
|
|
|
|
auto *job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_TRUE(job->Get(olive::FlipDistortNode::kHorizontalInput).toBool());
|
|
EXPECT_FALSE(job->Get(olive::FlipDistortNode::kVerticalInput).toBool());
|
|
|
|
// Vertical flip on its own also triggers the shader
|
|
row.insert(olive::FlipDistortNode::kHorizontalInput, BoolValue(false));
|
|
row.insert(olive::FlipDistortNode::kVerticalInput, BoolValue(true));
|
|
|
|
olive::NodeValueTable vertical_table;
|
|
node.Value(row, olive::NodeGlobals(), &vertical_table);
|
|
|
|
ASSERT_EQ(vertical_table.Count(), 1);
|
|
out = GetOutputTexture(vertical_table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_FALSE(job->Get(olive::FlipDistortNode::kHorizontalInput).toBool());
|
|
EXPECT_TRUE(job->Get(olive::FlipDistortNode::kVerticalInput).toBool());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// CornerPinDistortNode
|
|
// -----------------------------------------------------------------------------
|
|
|
|
TEST(CornerPinDistortNode, MetadataIsCorrect)
|
|
{
|
|
olive::CornerPinDistortNode node;
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.olivevideoeditor.Olive.cornerpin"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Corner Pin"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
|
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
|
EXPECT_EQ(node.GetEffectInputID(), olive::CornerPinDistortNode::kTextureInput);
|
|
}
|
|
|
|
TEST(CornerPinDistortNode, InputDefaults)
|
|
{
|
|
olive::CornerPinDistortNode node;
|
|
|
|
EXPECT_EQ(
|
|
int(node.GetInputDataType(olive::CornerPinDistortNode::kTextureInput)),
|
|
int(olive::NodeValue::kTexture));
|
|
EXPECT_FALSE(
|
|
node.IsInputKeyframable(olive::CornerPinDistortNode::kTextureInput));
|
|
|
|
EXPECT_EQ(int(node.GetInputDataType(
|
|
olive::CornerPinDistortNode::kPerspectiveInput)),
|
|
int(olive::NodeValue::kBoolean));
|
|
EXPECT_TRUE(node.GetStandardValue(
|
|
olive::CornerPinDistortNode::kPerspectiveInput)
|
|
.toBool());
|
|
|
|
// All four corners are pixel offsets relative to their respective image
|
|
// corner and default to no offset
|
|
const QString corners[] = { olive::CornerPinDistortNode::kTopLeftInput,
|
|
olive::CornerPinDistortNode::kTopRightInput,
|
|
olive::CornerPinDistortNode::kBottomRightInput,
|
|
olive::CornerPinDistortNode::kBottomLeftInput };
|
|
for (const QString &corner : corners) {
|
|
EXPECT_EQ(int(node.GetInputDataType(corner)),
|
|
int(olive::NodeValue::kVec2));
|
|
EXPECT_EQ(node.GetStandardValue(corner).value<QVector2D>(),
|
|
QVector2D(0.0f, 0.0f));
|
|
}
|
|
}
|
|
|
|
TEST(CornerPinDistortNode, RetranslateSetsInputNames)
|
|
{
|
|
olive::CornerPinDistortNode node;
|
|
node.Retranslate();
|
|
|
|
EXPECT_EQ(node.GetInputName(olive::CornerPinDistortNode::kTextureInput),
|
|
QStringLiteral("Texture"));
|
|
EXPECT_EQ(node.GetInputName(olive::CornerPinDistortNode::kPerspectiveInput),
|
|
QStringLiteral("Perspective"));
|
|
EXPECT_EQ(node.GetInputName(olive::CornerPinDistortNode::kTopLeftInput),
|
|
QStringLiteral("Top Left"));
|
|
EXPECT_EQ(node.GetInputName(olive::CornerPinDistortNode::kTopRightInput),
|
|
QStringLiteral("Top Right"));
|
|
EXPECT_EQ(node.GetInputName(olive::CornerPinDistortNode::kBottomRightInput),
|
|
QStringLiteral("Bottom Right"));
|
|
EXPECT_EQ(node.GetInputName(olive::CornerPinDistortNode::kBottomLeftInput),
|
|
QStringLiteral("Bottom Left"));
|
|
}
|
|
|
|
TEST(CornerPinDistortNode, GetShaderCodeLoadsFragAndVertShaders)
|
|
{
|
|
olive::CornerPinDistortNode node;
|
|
|
|
const olive::ShaderCode code = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("anything")));
|
|
EXPECT_FALSE(code.frag_code().isEmpty());
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("perspective_in")));
|
|
// Corner Pin is one of the few nodes with a custom vertex shader
|
|
EXPECT_FALSE(code.vert_code().isEmpty());
|
|
EXPECT_TRUE(code.vert_code().contains(QStringLiteral("top_left_in")));
|
|
}
|
|
|
|
TEST(CornerPinDistortNode, ValueToPixelConvertsOffsetsToPixels)
|
|
{
|
|
olive::CornerPinDistortNode node;
|
|
|
|
olive::NodeValueRow row;
|
|
row.insert(olive::CornerPinDistortNode::kTopLeftInput,
|
|
Vec2Value(QVector2D(10.0f, 20.0f)));
|
|
row.insert(olive::CornerPinDistortNode::kTopRightInput,
|
|
Vec2Value(QVector2D(-30.0f, 40.0f)));
|
|
row.insert(olive::CornerPinDistortNode::kBottomRightInput,
|
|
Vec2Value(QVector2D(-50.0f, -60.0f)));
|
|
row.insert(olive::CornerPinDistortNode::kBottomLeftInput,
|
|
Vec2Value(QVector2D(70.0f, -80.0f)));
|
|
|
|
const QVector2D resolution(200.0f, 100.0f);
|
|
|
|
// Top-left offsets are relative to (0, 0)
|
|
const QPointF top_left = node.ValueToPixel(0, row, resolution);
|
|
EXPECT_DOUBLE_EQ(top_left.x(), 10.0);
|
|
EXPECT_DOUBLE_EQ(top_left.y(), 20.0);
|
|
|
|
// Top-right offsets are relative to (width, 0)
|
|
const QPointF top_right = node.ValueToPixel(1, row, resolution);
|
|
EXPECT_DOUBLE_EQ(top_right.x(), 170.0);
|
|
EXPECT_DOUBLE_EQ(top_right.y(), 40.0);
|
|
|
|
// Bottom-right offsets are relative to (width, height)
|
|
const QPointF bottom_right = node.ValueToPixel(2, row, resolution);
|
|
EXPECT_DOUBLE_EQ(bottom_right.x(), 150.0);
|
|
EXPECT_DOUBLE_EQ(bottom_right.y(), 40.0);
|
|
|
|
// Bottom-left offsets are relative to (0, height)
|
|
const QPointF bottom_left = node.ValueToPixel(3, row, resolution);
|
|
EXPECT_DOUBLE_EQ(bottom_left.x(), 70.0);
|
|
EXPECT_DOUBLE_EQ(bottom_left.y(), 20.0);
|
|
}
|
|
|
|
TEST(CornerPinDistortNode, ValueWithoutTexturePushesNothing)
|
|
{
|
|
olive::CornerPinDistortNode node;
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
|
|
|
|
EXPECT_EQ(table.Count(), 0);
|
|
}
|
|
|
|
TEST(CornerPinDistortNode, ValueWithDefaultCornersPassesTextureThrough)
|
|
{
|
|
olive::CornerPinDistortNode node;
|
|
|
|
const olive::TexturePtr tex = MakeDummyTexture(100, 100);
|
|
olive::NodeValueTable table;
|
|
node.Value(MakeTextureRow(olive::CornerPinDistortNode::kTextureInput, tex),
|
|
olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
EXPECT_EQ(out, tex);
|
|
EXPECT_FALSE(out->IsJob());
|
|
}
|
|
|
|
TEST(CornerPinDistortNode, ValueWithMovedCornerPushesVertexCoordinates)
|
|
{
|
|
olive::CornerPinDistortNode node;
|
|
|
|
const olive::TexturePtr tex = MakeDummyTexture(100, 100);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::CornerPinDistortNode::kTextureInput, tex);
|
|
row.insert(olive::CornerPinDistortNode::kTopLeftInput,
|
|
Vec2Value(QVector2D(10.0f, 20.0f)));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
EXPECT_EQ(out->params().width(), tex->params().width());
|
|
|
|
auto *job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_EQ(job->Get(QStringLiteral("resolution_in")).toVec2(),
|
|
QVector2D(100.0f, 100.0f));
|
|
|
|
// Slider offsets are converted to pixel positions and then to clip space
|
|
// (-1..1): top-left (10, 20) -> (-0.8, -0.6), the untouched corners land
|
|
// on the default quad
|
|
const QVector<float> &vertices = job->GetVertexCoordinates();
|
|
ASSERT_EQ(int(vertices.size()), 18);
|
|
|
|
// First triangle
|
|
EXPECT_FLOAT_EQ(vertices.at(0), -0.8f);
|
|
EXPECT_FLOAT_EQ(vertices.at(1), -0.6f);
|
|
EXPECT_FLOAT_EQ(vertices.at(2), 0.0f);
|
|
EXPECT_FLOAT_EQ(vertices.at(3), 1.0f);
|
|
EXPECT_FLOAT_EQ(vertices.at(4), -1.0f);
|
|
EXPECT_FLOAT_EQ(vertices.at(5), 0.0f);
|
|
EXPECT_FLOAT_EQ(vertices.at(6), 1.0f);
|
|
EXPECT_FLOAT_EQ(vertices.at(7), 1.0f);
|
|
EXPECT_FLOAT_EQ(vertices.at(8), 0.0f);
|
|
|
|
// Second triangle
|
|
EXPECT_FLOAT_EQ(vertices.at(9), -0.8f);
|
|
EXPECT_FLOAT_EQ(vertices.at(10), -0.6f);
|
|
EXPECT_FLOAT_EQ(vertices.at(12), -1.0f);
|
|
EXPECT_FLOAT_EQ(vertices.at(13), 1.0f);
|
|
EXPECT_FLOAT_EQ(vertices.at(15), 1.0f);
|
|
EXPECT_FLOAT_EQ(vertices.at(16), 1.0f);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// MaskDistortNode
|
|
// -----------------------------------------------------------------------------
|
|
|
|
TEST(MaskDistortNode, MetadataIsCorrect)
|
|
{
|
|
olive::MaskDistortNode node;
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.olivevideoeditor.Olive.mask"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Mask"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
|
|
|
// From GeneratorWithMerge: the base texture is the effect input
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
|
EXPECT_EQ(node.GetEffectInputID(), olive::GeneratorWithMerge::kBaseInput);
|
|
}
|
|
|
|
TEST(MaskDistortNode, InputDefinitionsAndDefaults)
|
|
{
|
|
olive::MaskDistortNode node;
|
|
|
|
EXPECT_EQ(
|
|
int(node.GetInputDataType(olive::MaskDistortNode::kInvertInput)),
|
|
int(olive::NodeValue::kBoolean));
|
|
EXPECT_FALSE(node.GetStandardValue(olive::MaskDistortNode::kInvertInput)
|
|
.toBool());
|
|
|
|
EXPECT_EQ(
|
|
int(node.GetInputDataType(olive::MaskDistortNode::kFeatherInput)),
|
|
int(olive::NodeValue::kFloat));
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetStandardValue(olive::MaskDistortNode::kFeatherInput).toDouble(),
|
|
0.0);
|
|
EXPECT_DOUBLE_EQ(node.GetInputProperty(olive::MaskDistortNode::kFeatherInput,
|
|
QStringLiteral("min"))
|
|
.toDouble(),
|
|
0.0);
|
|
|
|
// From PolygonGenerator: the color is hidden because the mask must stay
|
|
// white for the multiply to work, and the shape defaults to a pentagon
|
|
EXPECT_TRUE(node.IsInputHidden(olive::PolygonGenerator::kColorInput));
|
|
EXPECT_TRUE(node.InputIsArray(olive::PolygonGenerator::kPointsInput));
|
|
EXPECT_EQ(node.InputArraySize(olive::PolygonGenerator::kPointsInput), 5);
|
|
}
|
|
|
|
TEST(MaskDistortNode, RetranslateSetsInputNames)
|
|
{
|
|
olive::MaskDistortNode node;
|
|
node.Retranslate();
|
|
|
|
// The base input is renamed from GeneratorWithMerge's "Base"
|
|
EXPECT_EQ(node.GetInputName(olive::GeneratorWithMerge::kBaseInput),
|
|
QStringLiteral("Texture"));
|
|
EXPECT_EQ(node.GetInputName(olive::MaskDistortNode::kInvertInput),
|
|
QStringLiteral("Invert"));
|
|
EXPECT_EQ(node.GetInputName(olive::MaskDistortNode::kFeatherInput),
|
|
QStringLiteral("Feather"));
|
|
|
|
// Inherited names from PolygonGenerator
|
|
EXPECT_EQ(node.GetInputName(olive::PolygonGenerator::kPointsInput),
|
|
QStringLiteral("Points"));
|
|
EXPECT_EQ(node.GetInputName(olive::PolygonGenerator::kColorInput),
|
|
QStringLiteral("Color"));
|
|
}
|
|
|
|
TEST(MaskDistortNode, GetShaderCodeSelectsShaderByRequestId)
|
|
{
|
|
olive::MaskDistortNode node;
|
|
|
|
const olive::ShaderCode merge = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("mrg")));
|
|
EXPECT_FALSE(merge.frag_code().isEmpty());
|
|
EXPECT_TRUE(merge.frag_code().contains(QStringLiteral("tex_a")));
|
|
|
|
const olive::ShaderCode feather = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("feather")));
|
|
EXPECT_FALSE(feather.frag_code().isEmpty());
|
|
EXPECT_TRUE(feather.frag_code().contains(QStringLiteral("radius_in")));
|
|
|
|
const olive::ShaderCode invert = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("invert")));
|
|
EXPECT_FALSE(invert.frag_code().isEmpty());
|
|
EXPECT_TRUE(invert.frag_code().contains(QStringLiteral("tex_in")));
|
|
|
|
// Unknown ids fall through to PolygonGenerator, which serves "rgb"
|
|
const olive::ShaderCode rgb = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("rgb")));
|
|
EXPECT_FALSE(rgb.frag_code().isEmpty());
|
|
EXPECT_TRUE(rgb.frag_code().contains(QStringLiteral("texture_in")));
|
|
|
|
const olive::ShaderCode unknown = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("bogus")));
|
|
EXPECT_TRUE(unknown.frag_code().isEmpty());
|
|
EXPECT_TRUE(unknown.vert_code().isEmpty());
|
|
}
|
|
|
|
TEST(MaskDistortNode, ValueWithoutTexturePushesGeneratePipeline)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::MaskDistortNode>(&project);
|
|
|
|
const olive::VideoParams vparams = SequenceParams(320, 240);
|
|
olive::NodeValueTable table = GenerateTable(node, vparams);
|
|
|
|
// Without a base the mask still generates its polygon, wrapped in the
|
|
// "rgb" conversion job
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
EXPECT_EQ(out->params().width(), vparams.width());
|
|
EXPECT_EQ(out->params().height(), vparams.height());
|
|
|
|
auto *rgb = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(rgb);
|
|
EXPECT_EQ(rgb->GetShaderID(), QStringLiteral("rgb"));
|
|
|
|
// The polygon color is forced to white
|
|
const olive::core::Color color =
|
|
rgb->Get(QStringLiteral("color_in")).toColor();
|
|
EXPECT_FLOAT_EQ(color.red(), 1.0f);
|
|
EXPECT_FLOAT_EQ(color.green(), 1.0f);
|
|
EXPECT_FLOAT_EQ(color.blue(), 1.0f);
|
|
EXPECT_FLOAT_EQ(color.alpha(), 1.0f);
|
|
|
|
// The nested generation job renders to an 8-bit buffer
|
|
const olive::TexturePtr generate =
|
|
rgb->Get(QStringLiteral("texture_in")).toTexture();
|
|
ASSERT_TRUE(generate);
|
|
ASSERT_TRUE(generate->IsJob());
|
|
EXPECT_EQ(int(generate->params().format()), int(olive::core::PixelFormat::U8));
|
|
EXPECT_TRUE(dynamic_cast<olive::GenerateJob *>(generate->job()));
|
|
}
|
|
|
|
TEST(MaskDistortNode, ValueWithInvertWrapsGenerationInInvertJob)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::MaskDistortNode>(&project);
|
|
node->SetStandardValue(olive::MaskDistortNode::kInvertInput, true);
|
|
|
|
olive::NodeValueTable table = GenerateTable(node, SequenceParams(320, 240));
|
|
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
|
|
auto *invert = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(invert);
|
|
EXPECT_EQ(invert->GetShaderID(), QStringLiteral("invert"));
|
|
|
|
// The inverted texture is the usual rgb generation pipeline
|
|
const olive::TexturePtr rgb_tex =
|
|
invert->Get(QStringLiteral("tex_in")).toTexture();
|
|
ASSERT_TRUE(rgb_tex);
|
|
ASSERT_TRUE(rgb_tex->IsJob());
|
|
auto *rgb = dynamic_cast<olive::ShaderJob *>(rgb_tex->job());
|
|
ASSERT_TRUE(rgb);
|
|
EXPECT_EQ(rgb->GetShaderID(), QStringLiteral("rgb"));
|
|
}
|
|
|
|
TEST(MaskDistortNode, ValueWithTexturePushesMergeJob)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::MaskDistortNode>(&project);
|
|
auto *constant = AddNode<ConstantTextureNode>(&project);
|
|
|
|
const olive::TexturePtr base = MakeDummyTexture(64, 48);
|
|
constant->SetTexture(base);
|
|
olive::Node::ConnectEdge(
|
|
constant, olive::NodeInput(node, olive::GeneratorWithMerge::kBaseInput));
|
|
|
|
olive::NodeValueTable table = GenerateTable(node, SequenceParams(320, 240));
|
|
|
|
// With a base the mask multiplies it by the generated polygon
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
EXPECT_EQ(out->params().width(), base->params().width());
|
|
EXPECT_EQ(out->params().height(), base->params().height());
|
|
|
|
auto *merge = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(merge);
|
|
EXPECT_EQ(merge->GetShaderID(), QStringLiteral("mrg"));
|
|
EXPECT_EQ(merge->Get(QStringLiteral("tex_a")).toTexture(), base);
|
|
|
|
// Without a feather, tex_b is the rgb generation pipeline directly
|
|
const olive::TexturePtr tex_b =
|
|
merge->Get(QStringLiteral("tex_b")).toTexture();
|
|
ASSERT_TRUE(tex_b);
|
|
ASSERT_TRUE(tex_b->IsJob());
|
|
auto *rgb = dynamic_cast<olive::ShaderJob *>(tex_b->job());
|
|
ASSERT_TRUE(rgb);
|
|
EXPECT_EQ(rgb->GetShaderID(), QStringLiteral("rgb"));
|
|
}
|
|
|
|
TEST(MaskDistortNode, ValueWithFeatherNestsBlurJob)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::MaskDistortNode>(&project);
|
|
node->SetStandardValue(olive::MaskDistortNode::kFeatherInput, 10.0);
|
|
|
|
auto *constant = AddNode<ConstantTextureNode>(&project);
|
|
const olive::TexturePtr base = MakeDummyTexture(64, 48);
|
|
constant->SetTexture(base);
|
|
olive::Node::ConnectEdge(
|
|
constant, olive::NodeInput(node, olive::GeneratorWithMerge::kBaseInput));
|
|
|
|
olive::NodeValueTable table = GenerateTable(node, SequenceParams(320, 240));
|
|
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
|
|
auto *merge = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(merge);
|
|
EXPECT_EQ(merge->GetShaderID(), QStringLiteral("mrg"));
|
|
|
|
// With a feather, tex_b becomes a two-iteration gaussian blur of the mask
|
|
const olive::TexturePtr tex_b =
|
|
merge->Get(QStringLiteral("tex_b")).toTexture();
|
|
ASSERT_TRUE(tex_b);
|
|
ASSERT_TRUE(tex_b->IsJob());
|
|
|
|
auto *feather = dynamic_cast<olive::ShaderJob *>(tex_b->job());
|
|
ASSERT_TRUE(feather);
|
|
EXPECT_EQ(feather->GetShaderID(), QStringLiteral("feather"));
|
|
EXPECT_EQ(feather->GetIterationCount(), 2);
|
|
EXPECT_EQ(feather->GetIterativeInput(), olive::BlurFilterNode::kTextureInput);
|
|
EXPECT_DOUBLE_EQ(
|
|
feather->Get(olive::BlurFilterNode::kRadiusInput).toDouble(), 10.0);
|
|
EXPECT_EQ(feather->Get(olive::BlurFilterNode::kMethodInput).toInt(),
|
|
int(olive::BlurFilterNode::kGaussian));
|
|
EXPECT_EQ(feather->Get(QStringLiteral("resolution_in")).toVec2(),
|
|
base->virtual_resolution());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// RippleDistortNode
|
|
// -----------------------------------------------------------------------------
|
|
|
|
TEST(RippleDistortNode, MetadataIsCorrect)
|
|
{
|
|
olive::RippleDistortNode node;
|
|
// NOTE: "org.oliveeditor.*" domain, inconsistent with most Olive nodes
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.oliveeditor.Olive.ripple"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Ripple"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
|
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
|
EXPECT_EQ(node.GetEffectInputID(), olive::RippleDistortNode::kTextureInput);
|
|
}
|
|
|
|
TEST(RippleDistortNode, InputDefaults)
|
|
{
|
|
olive::RippleDistortNode node;
|
|
|
|
EXPECT_EQ(
|
|
int(node.GetInputDataType(olive::RippleDistortNode::kTextureInput)),
|
|
int(olive::NodeValue::kTexture));
|
|
EXPECT_FALSE(node.IsInputKeyframable(olive::RippleDistortNode::kTextureInput));
|
|
|
|
EXPECT_DOUBLE_EQ(node.GetStandardValue(olive::RippleDistortNode::kEvolutionInput)
|
|
.toDouble(),
|
|
0.0);
|
|
EXPECT_DOUBLE_EQ(node.GetStandardValue(olive::RippleDistortNode::kIntensityInput)
|
|
.toDouble(),
|
|
100.0);
|
|
EXPECT_DOUBLE_EQ(node.GetStandardValue(olive::RippleDistortNode::kFrequencyInput)
|
|
.toDouble(),
|
|
1.0);
|
|
EXPECT_DOUBLE_EQ(node.GetInputProperty(olive::RippleDistortNode::kFrequencyInput,
|
|
QStringLiteral("base"))
|
|
.toDouble(),
|
|
0.01);
|
|
EXPECT_EQ(node.GetStandardValue(olive::RippleDistortNode::kPositionInput)
|
|
.value<QVector2D>(),
|
|
QVector2D(0.0f, 0.0f));
|
|
EXPECT_FALSE(node.GetStandardValue(olive::RippleDistortNode::kStretchInput)
|
|
.toBool());
|
|
}
|
|
|
|
TEST(RippleDistortNode, RetranslateSetsInputNames)
|
|
{
|
|
olive::RippleDistortNode node;
|
|
node.Retranslate();
|
|
|
|
EXPECT_EQ(node.GetInputName(olive::RippleDistortNode::kTextureInput),
|
|
QStringLiteral("Input"));
|
|
EXPECT_EQ(node.GetInputName(olive::RippleDistortNode::kFrequencyInput),
|
|
QStringLiteral("Frequency"));
|
|
EXPECT_EQ(node.GetInputName(olive::RippleDistortNode::kIntensityInput),
|
|
QStringLiteral("Intensity"));
|
|
EXPECT_EQ(node.GetInputName(olive::RippleDistortNode::kEvolutionInput),
|
|
QStringLiteral("Evolution"));
|
|
EXPECT_EQ(node.GetInputName(olive::RippleDistortNode::kPositionInput),
|
|
QStringLiteral("Position"));
|
|
EXPECT_EQ(node.GetInputName(olive::RippleDistortNode::kStretchInput),
|
|
QStringLiteral("Stretch"));
|
|
}
|
|
|
|
TEST(RippleDistortNode, GetShaderCodeLoadsRippleShader)
|
|
{
|
|
olive::RippleDistortNode node;
|
|
|
|
const olive::ShaderCode code = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("anything")));
|
|
EXPECT_FALSE(code.frag_code().isEmpty());
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("evolution_in")));
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("intensity_in")));
|
|
}
|
|
|
|
TEST(RippleDistortNode, ValueWithoutTexturePushesNothing)
|
|
{
|
|
olive::RippleDistortNode node;
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
|
|
|
|
EXPECT_EQ(table.Count(), 0);
|
|
}
|
|
|
|
TEST(RippleDistortNode, ValueWithZeroIntensityPassesTextureThrough)
|
|
{
|
|
olive::RippleDistortNode node;
|
|
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::RippleDistortNode::kTextureInput, tex);
|
|
row.insert(olive::RippleDistortNode::kIntensityInput, FloatValue(0.0));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
EXPECT_EQ(out, tex);
|
|
EXPECT_FALSE(out->IsJob());
|
|
}
|
|
|
|
TEST(RippleDistortNode, ValueWithIntensityPushesShaderJob)
|
|
{
|
|
olive::RippleDistortNode node;
|
|
|
|
// With a non-zero intensity the shader runs and receives the texture's
|
|
// virtual resolution
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::RippleDistortNode::kTextureInput, tex);
|
|
row.insert(olive::RippleDistortNode::kIntensityInput, FloatValue(100.0));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
EXPECT_EQ(out->params().width(), tex->params().width());
|
|
|
|
auto *job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_DOUBLE_EQ(job->Get(olive::RippleDistortNode::kIntensityInput).toDouble(),
|
|
100.0);
|
|
EXPECT_EQ(job->Get(QStringLiteral("resolution_in")).toVec2(),
|
|
tex->virtual_resolution());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// SwirlDistortNode
|
|
// -----------------------------------------------------------------------------
|
|
|
|
TEST(SwirlDistortNode, MetadataIsCorrect)
|
|
{
|
|
olive::SwirlDistortNode node;
|
|
// NOTE: "org.oliveeditor.*" domain, inconsistent with most Olive nodes
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.oliveeditor.Olive.swirl"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Swirl"));
|
|
EXPECT_EQ(node.Description(),
|
|
QStringLiteral("Distorts an image by swirling it around a center point."));
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
|
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
|
EXPECT_EQ(node.GetEffectInputID(), olive::SwirlDistortNode::kTextureInput);
|
|
}
|
|
|
|
TEST(SwirlDistortNode, InputDefaults)
|
|
{
|
|
olive::SwirlDistortNode node;
|
|
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::SwirlDistortNode::kTextureInput)),
|
|
int(olive::NodeValue::kTexture));
|
|
EXPECT_FALSE(node.IsInputKeyframable(olive::SwirlDistortNode::kTextureInput));
|
|
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetStandardValue(olive::SwirlDistortNode::kRadiusInput).toDouble(),
|
|
200.0);
|
|
EXPECT_DOUBLE_EQ(node.GetInputProperty(olive::SwirlDistortNode::kRadiusInput,
|
|
QStringLiteral("min"))
|
|
.toDouble(),
|
|
0.0);
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetStandardValue(olive::SwirlDistortNode::kAngleInput).toDouble(),
|
|
10.0);
|
|
EXPECT_DOUBLE_EQ(node.GetInputProperty(olive::SwirlDistortNode::kAngleInput,
|
|
QStringLiteral("base"))
|
|
.toDouble(),
|
|
0.1);
|
|
EXPECT_EQ(node.GetStandardValue(olive::SwirlDistortNode::kPositionInput)
|
|
.value<QVector2D>(),
|
|
QVector2D(0.0f, 0.0f));
|
|
}
|
|
|
|
TEST(SwirlDistortNode, RetranslateSetsInputNames)
|
|
{
|
|
olive::SwirlDistortNode node;
|
|
node.Retranslate();
|
|
|
|
EXPECT_EQ(node.GetInputName(olive::SwirlDistortNode::kTextureInput),
|
|
QStringLiteral("Input"));
|
|
EXPECT_EQ(node.GetInputName(olive::SwirlDistortNode::kRadiusInput),
|
|
QStringLiteral("Radius"));
|
|
EXPECT_EQ(node.GetInputName(olive::SwirlDistortNode::kAngleInput),
|
|
QStringLiteral("Angle"));
|
|
EXPECT_EQ(node.GetInputName(olive::SwirlDistortNode::kPositionInput),
|
|
QStringLiteral("Position"));
|
|
}
|
|
|
|
TEST(SwirlDistortNode, GetShaderCodeLoadsSwirlShader)
|
|
{
|
|
olive::SwirlDistortNode node;
|
|
|
|
const olive::ShaderCode code = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("anything")));
|
|
EXPECT_FALSE(code.frag_code().isEmpty());
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("radius_in")));
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("angle_in")));
|
|
}
|
|
|
|
TEST(SwirlDistortNode, ValueWithoutTexturePushesNothing)
|
|
{
|
|
olive::SwirlDistortNode node;
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
|
|
|
|
EXPECT_EQ(table.Count(), 0);
|
|
}
|
|
|
|
TEST(SwirlDistortNode, ValueWithZeroAngleOrRadiusPassesTextureThrough)
|
|
{
|
|
olive::SwirlDistortNode node;
|
|
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
|
|
// Zero angle neutralizes the swirl
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::SwirlDistortNode::kTextureInput, tex);
|
|
row.insert(olive::SwirlDistortNode::kAngleInput, FloatValue(0.0));
|
|
row.insert(olive::SwirlDistortNode::kRadiusInput, FloatValue(200.0));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
EXPECT_EQ(GetOutputTexture(table), tex);
|
|
|
|
// So does a zero radius
|
|
row.insert(olive::SwirlDistortNode::kAngleInput, FloatValue(10.0));
|
|
row.insert(olive::SwirlDistortNode::kRadiusInput, FloatValue(0.0));
|
|
|
|
olive::NodeValueTable zero_radius_table;
|
|
node.Value(row, olive::NodeGlobals(), &zero_radius_table);
|
|
|
|
ASSERT_EQ(zero_radius_table.Count(), 1);
|
|
EXPECT_EQ(GetOutputTexture(zero_radius_table), tex);
|
|
}
|
|
|
|
TEST(SwirlDistortNode, ValueWithAngleAndRadiusPushesShaderJob)
|
|
{
|
|
olive::SwirlDistortNode node;
|
|
|
|
// With a non-zero angle and radius the shader runs and receives the
|
|
// texture's virtual resolution
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::SwirlDistortNode::kTextureInput, tex);
|
|
row.insert(olive::SwirlDistortNode::kAngleInput, FloatValue(10.0));
|
|
row.insert(olive::SwirlDistortNode::kRadiusInput, FloatValue(200.0));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
|
|
auto *job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_DOUBLE_EQ(job->Get(olive::SwirlDistortNode::kAngleInput).toDouble(),
|
|
10.0);
|
|
EXPECT_EQ(job->Get(QStringLiteral("resolution_in")).toVec2(),
|
|
tex->virtual_resolution());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// TileDistortNode
|
|
// -----------------------------------------------------------------------------
|
|
|
|
TEST(TileDistortNode, MetadataIsCorrect)
|
|
{
|
|
olive::TileDistortNode node;
|
|
// NOTE: "org.oliveeditor.*" domain, inconsistent with most Olive nodes
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.oliveeditor.Olive.tile"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Tile"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
|
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
|
EXPECT_EQ(node.GetEffectInputID(), olive::TileDistortNode::kTextureInput);
|
|
}
|
|
|
|
TEST(TileDistortNode, InputDefaults)
|
|
{
|
|
olive::TileDistortNode node;
|
|
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::TileDistortNode::kTextureInput)),
|
|
int(olive::NodeValue::kTexture));
|
|
EXPECT_FALSE(node.IsInputKeyframable(olive::TileDistortNode::kTextureInput));
|
|
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetStandardValue(olive::TileDistortNode::kScaleInput).toDouble(),
|
|
0.5);
|
|
EXPECT_DOUBLE_EQ(node.GetInputProperty(olive::TileDistortNode::kScaleInput,
|
|
QStringLiteral("min"))
|
|
.toDouble(),
|
|
0.0);
|
|
EXPECT_EQ(node.GetInputProperty(olive::TileDistortNode::kScaleInput,
|
|
QStringLiteral("view"))
|
|
.toInt(),
|
|
int(olive::FloatSlider::kPercentage));
|
|
|
|
EXPECT_EQ(node.GetStandardValue(olive::TileDistortNode::kPositionInput)
|
|
.value<QVector2D>(),
|
|
QVector2D(0.0f, 0.0f));
|
|
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::TileDistortNode::kAnchorInput)),
|
|
int(olive::NodeValue::kCombo));
|
|
// The Anchor enum is private; 4 is kMiddleCenter
|
|
EXPECT_EQ(node.GetStandardValue(olive::TileDistortNode::kAnchorInput).toInt(),
|
|
4);
|
|
|
|
EXPECT_FALSE(node.GetStandardValue(olive::TileDistortNode::kMirrorXInput)
|
|
.toBool());
|
|
EXPECT_FALSE(node.GetStandardValue(olive::TileDistortNode::kMirrorYInput)
|
|
.toBool());
|
|
}
|
|
|
|
TEST(TileDistortNode, RetranslateSetsNamesAndAnchorComboStrings)
|
|
{
|
|
olive::TileDistortNode node;
|
|
node.Retranslate();
|
|
|
|
EXPECT_EQ(node.GetInputName(olive::TileDistortNode::kTextureInput),
|
|
QStringLiteral("Input"));
|
|
EXPECT_EQ(node.GetInputName(olive::TileDistortNode::kScaleInput),
|
|
QStringLiteral("Scale"));
|
|
EXPECT_EQ(node.GetInputName(olive::TileDistortNode::kPositionInput),
|
|
QStringLiteral("Position"));
|
|
EXPECT_EQ(node.GetInputName(olive::TileDistortNode::kAnchorInput),
|
|
QStringLiteral("Anchor"));
|
|
EXPECT_EQ(node.GetInputName(olive::TileDistortNode::kMirrorXInput),
|
|
QStringLiteral("Mirror Horizontally"));
|
|
EXPECT_EQ(node.GetInputName(olive::TileDistortNode::kMirrorYInput),
|
|
QStringLiteral("Mirror Vertically"));
|
|
|
|
const QStringList anchors =
|
|
node.GetComboBoxStrings(olive::TileDistortNode::kAnchorInput);
|
|
ASSERT_EQ(anchors.size(), 9);
|
|
EXPECT_EQ(anchors.at(0), QStringLiteral("Top-Left"));
|
|
EXPECT_EQ(anchors.at(1), QStringLiteral("Top-Center"));
|
|
EXPECT_EQ(anchors.at(2), QStringLiteral("Top-Right"));
|
|
EXPECT_EQ(anchors.at(3), QStringLiteral("Middle-Left"));
|
|
EXPECT_EQ(anchors.at(4), QStringLiteral("Middle-Center"));
|
|
EXPECT_EQ(anchors.at(5), QStringLiteral("Middle-Right"));
|
|
EXPECT_EQ(anchors.at(6), QStringLiteral("Bottom-Left"));
|
|
EXPECT_EQ(anchors.at(7), QStringLiteral("Bottom-Center"));
|
|
EXPECT_EQ(anchors.at(8), QStringLiteral("Bottom-Right"));
|
|
}
|
|
|
|
TEST(TileDistortNode, GetShaderCodeLoadsTileShader)
|
|
{
|
|
olive::TileDistortNode node;
|
|
|
|
const olive::ShaderCode code = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("anything")));
|
|
EXPECT_FALSE(code.frag_code().isEmpty());
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("mirrorx_in")));
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("anchor_in")));
|
|
}
|
|
|
|
TEST(TileDistortNode, ValueWithoutTexturePushesNothing)
|
|
{
|
|
olive::TileDistortNode node;
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
|
|
|
|
EXPECT_EQ(table.Count(), 0);
|
|
}
|
|
|
|
TEST(TileDistortNode, ValueWithUnitScalePassesTextureThrough)
|
|
{
|
|
olive::TileDistortNode node;
|
|
|
|
// A scale of exactly 1.0 is a no-op
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::TileDistortNode::kTextureInput, tex);
|
|
row.insert(olive::TileDistortNode::kScaleInput, FloatValue(1.0));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
EXPECT_EQ(out, tex);
|
|
EXPECT_FALSE(out->IsJob());
|
|
}
|
|
|
|
TEST(TileDistortNode, ValueWithNonUnitScalePushesShaderJob)
|
|
{
|
|
olive::TileDistortNode node;
|
|
|
|
// Any scale other than 1.0 runs the shader
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::TileDistortNode::kTextureInput, tex);
|
|
row.insert(olive::TileDistortNode::kScaleInput, FloatValue(0.5));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
|
|
auto *job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_DOUBLE_EQ(job->Get(olive::TileDistortNode::kScaleInput).toDouble(),
|
|
0.5);
|
|
EXPECT_EQ(job->Get(QStringLiteral("resolution_in")).toVec2(),
|
|
tex->virtual_resolution());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// WaveDistortNode
|
|
// -----------------------------------------------------------------------------
|
|
|
|
TEST(WaveDistortNode, MetadataIsCorrect)
|
|
{
|
|
olive::WaveDistortNode node;
|
|
// NOTE: "org.oliveeditor.*" domain, inconsistent with most Olive nodes
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.oliveeditor.Olive.wave"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Wave"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
|
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
|
EXPECT_EQ(node.GetEffectInputID(), olive::WaveDistortNode::kTextureInput);
|
|
}
|
|
|
|
TEST(WaveDistortNode, InputDefaults)
|
|
{
|
|
olive::WaveDistortNode node;
|
|
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::WaveDistortNode::kTextureInput)),
|
|
int(olive::NodeValue::kTexture));
|
|
EXPECT_FALSE(node.IsInputKeyframable(olive::WaveDistortNode::kTextureInput));
|
|
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetStandardValue(olive::WaveDistortNode::kFrequencyInput).toDouble(),
|
|
10.0);
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetStandardValue(olive::WaveDistortNode::kIntensityInput).toDouble(),
|
|
10.0);
|
|
EXPECT_DOUBLE_EQ(
|
|
node.GetStandardValue(olive::WaveDistortNode::kEvolutionInput).toDouble(),
|
|
0.0);
|
|
|
|
EXPECT_EQ(int(node.GetInputDataType(olive::WaveDistortNode::kVerticalInput)),
|
|
int(olive::NodeValue::kCombo));
|
|
EXPECT_EQ(node.GetStandardValue(olive::WaveDistortNode::kVerticalInput).toInt(),
|
|
0);
|
|
}
|
|
|
|
TEST(WaveDistortNode, RetranslateSetsNamesAndComboStrings)
|
|
{
|
|
olive::WaveDistortNode node;
|
|
node.Retranslate();
|
|
|
|
EXPECT_EQ(node.GetInputName(olive::WaveDistortNode::kTextureInput),
|
|
QStringLiteral("Input"));
|
|
EXPECT_EQ(node.GetInputName(olive::WaveDistortNode::kFrequencyInput),
|
|
QStringLiteral("Frequency"));
|
|
EXPECT_EQ(node.GetInputName(olive::WaveDistortNode::kIntensityInput),
|
|
QStringLiteral("Intensity"));
|
|
EXPECT_EQ(node.GetInputName(olive::WaveDistortNode::kEvolutionInput),
|
|
QStringLiteral("Evolution"));
|
|
EXPECT_EQ(node.GetInputName(olive::WaveDistortNode::kVerticalInput),
|
|
QStringLiteral("Direction"));
|
|
|
|
const QStringList directions =
|
|
node.GetComboBoxStrings(olive::WaveDistortNode::kVerticalInput);
|
|
ASSERT_EQ(directions.size(), 2);
|
|
EXPECT_EQ(directions.at(0), QStringLiteral("Horizontal"));
|
|
EXPECT_EQ(directions.at(1), QStringLiteral("Vertical"));
|
|
}
|
|
|
|
TEST(WaveDistortNode, GetShaderCodeLoadsWaveShader)
|
|
{
|
|
olive::WaveDistortNode node;
|
|
|
|
const olive::ShaderCode code = node.GetShaderCode(
|
|
olive::Node::ShaderRequest(QStringLiteral("anything")));
|
|
EXPECT_FALSE(code.frag_code().isEmpty());
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("vertical_in")));
|
|
EXPECT_TRUE(code.frag_code().contains(QStringLiteral("intensity_in")));
|
|
}
|
|
|
|
TEST(WaveDistortNode, ValueWithoutTexturePushesNothing)
|
|
{
|
|
olive::WaveDistortNode node;
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
|
|
|
|
EXPECT_EQ(table.Count(), 0);
|
|
}
|
|
|
|
TEST(WaveDistortNode, ValueWithZeroIntensityPassesTextureThrough)
|
|
{
|
|
olive::WaveDistortNode node;
|
|
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::WaveDistortNode::kTextureInput, tex);
|
|
row.insert(olive::WaveDistortNode::kIntensityInput, FloatValue(0.0));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
EXPECT_EQ(out, tex);
|
|
EXPECT_FALSE(out->IsJob());
|
|
}
|
|
|
|
TEST(WaveDistortNode, ValueWithIntensityPushesShaderJob)
|
|
{
|
|
olive::WaveDistortNode node;
|
|
|
|
// With a non-zero intensity the shader runs
|
|
const olive::TexturePtr tex = MakeDummyTexture(64, 48);
|
|
olive::NodeValueRow row =
|
|
MakeTextureRow(olive::WaveDistortNode::kTextureInput, tex);
|
|
row.insert(olive::WaveDistortNode::kIntensityInput, FloatValue(10.0));
|
|
|
|
olive::NodeValueTable table;
|
|
node.Value(row, olive::NodeGlobals(), &table);
|
|
|
|
ASSERT_EQ(table.Count(), 1);
|
|
const olive::TexturePtr out = GetOutputTexture(table);
|
|
ASSERT_TRUE(out);
|
|
ASSERT_TRUE(out->IsJob());
|
|
|
|
// Unlike the other distorters, WaveDistortNode does not insert a
|
|
// resolution_in; the job simply carries the row values with the input
|
|
// texture's params
|
|
EXPECT_EQ(out->params().width(), tex->params().width());
|
|
EXPECT_EQ(out->params().height(), tex->params().height());
|
|
|
|
auto *job = dynamic_cast<olive::ShaderJob *>(out->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_DOUBLE_EQ(job->Get(olive::WaveDistortNode::kIntensityInput).toDouble(),
|
|
10.0);
|
|
EXPECT_TRUE(job->Get(QStringLiteral("resolution_in")).toVec2().isNull());
|
|
}
|