diff --git a/app/node/distort/CMakeLists.txt b/app/node/distort/CMakeLists.txt index 2962e21ca..375232ccc 100644 --- a/app/node/distort/CMakeLists.txt +++ b/app/node/distort/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(cornerpin) add_subdirectory(crop) add_subdirectory(flip) add_subdirectory(transform) diff --git a/app/node/distort/cornerpin/CMakeLists.txt b/app/node/distort/cornerpin/CMakeLists.txt new file mode 100644 index 000000000..e89b0f94f --- /dev/null +++ b/app/node/distort/cornerpin/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/cornerpin/cornerpindistortnode.cpp + node/distort/cornerpin/cornerpindistortnode.h + PARENT_SCOPE +) diff --git a/app/node/distort/cornerpin/cornerpindistortnode.cpp b/app/node/distort/cornerpin/cornerpindistortnode.cpp new file mode 100644 index 000000000..b1e9b573e --- /dev/null +++ b/app/node/distort/cornerpin/cornerpindistortnode.cpp @@ -0,0 +1,216 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "cornerpindistortnode.h" + +#include "common/lerp.h" +#include "core.h" +#include "widget/slider/floatslider.h" + +namespace olive { + +const QString CornerPinDistortNode::kTextureInput = QStringLiteral("tex_in"); +const QString CornerPinDistortNode::kTopLeftInput = QStringLiteral("top_left_in"); +const QString CornerPinDistortNode::kTopRightInput = QStringLiteral("top_right_in"); +const QString CornerPinDistortNode::kBottomRightInput = QStringLiteral("bottom_right_in"); +const QString CornerPinDistortNode::kBottomLeftInput = QStringLiteral("bottom_left_in"); +const QString CornerPinDistortNode::kPerspectiveInput = QStringLiteral("perspective_in"); + +CornerPinDistortNode::CornerPinDistortNode() +{ + AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); + AddInput(kPerspectiveInput, NodeValue::kBoolean, true); + AddInput(kTopLeftInput, NodeValue::kVec2, QVector2D(0.0, 0.0)); + AddInput(kTopRightInput, NodeValue::kVec2, QVector2D(0.0, 0.0)); + AddInput(kBottomRightInput, NodeValue::kVec2, QVector2D(0.0, 0.0)); + AddInput(kBottomLeftInput, NodeValue::kVec2, QVector2D(0.0, 0.0)); +} + +void CornerPinDistortNode::Retranslate() +{ + SetInputName(kTextureInput, tr("Texture")); + SetInputName(kPerspectiveInput, tr("Perspective")); + SetInputName(kTopLeftInput, tr("Top Left")); + SetInputName(kTopRightInput, tr("Top Right")); + SetInputName(kBottomRightInput, tr("Bottom Right")); + SetInputName(kBottomLeftInput, tr("Bottom Left")); +} + +void CornerPinDistortNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const +{ + ShaderJob job; + job.InsertValue(value); + job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); + job.InsertValue(QStringLiteral("resolution_in"), NodeValue(NodeValue::kVec2, globals.resolution(), this)); + + // Convert slider values to their pixel values and then convert to clip space (-1.0 ... 1.0) for overriding the + // vertex coordinates. + const QVector2D &resolution = globals.resolution(); + QVector2D half_resolution = resolution * 0.5; + QVector2D top_left = QVector2D(ValueToPixel(0, value, resolution)) / half_resolution - QVector2D(1.0, 1.0); + QVector2D top_right = QVector2D(ValueToPixel(1, value, resolution)) / half_resolution - QVector2D(1.0, 1.0); + QVector2D bottom_right = QVector2D(ValueToPixel(2, value, resolution)) / half_resolution - QVector2D(1.0, 1.0); + QVector2D bottom_left = QVector2D(ValueToPixel(3, value, resolution)) / half_resolution - QVector2D(1.0, 1.0); + + // Override default vertex coordinates. + QVector adjusted_vertices = {top_left.x(), top_left.y(), 0.0f, + top_right.x(), top_right.y(), 0.0f, + bottom_right.x(), bottom_right.y(), 0.0f, + + top_left.x(), top_left.y(), 0.0f, + bottom_left.x(), bottom_left.y(), 0.0f, + bottom_right.x(), bottom_right.y(), 0.0f}; + job.SetVertexCoordinates(adjusted_vertices); + + // If no texture do nothing + if (!job.GetValue(kTextureInput).data().isNull()) { + // In the special case that all sliders are in their default position just + // push the texture. + if (!(job.GetValue(kTopLeftInput).data().value().isNull() + && job.GetValue(kTopRightInput).data().value().isNull() && + job.GetValue(kBottomRightInput).data().value().isNull() && + job.GetValue(kBottomLeftInput).data().value().isNull())) { + table->Push(NodeValue::kShaderJob, QVariant::fromValue(job), this); + } else { + table->Push(NodeValue::kTexture, job.GetValue(kTextureInput).data(), this); + } + } +} + +ShaderCode CornerPinDistortNode::GetShaderCode(const QString &shader_id) const +{ + Q_UNUSED(shader_id) + return ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/cornerpin.frag")), + FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/cornerpin.vert"))); +} + +QPointF CornerPinDistortNode::ValueToPixel(int value, const NodeValueRow& row, const QVector2D &resolution) const +{ + Q_ASSERT(value >= 0 && value <= 3); + switch (value) { + case 0: // Top left + return QPointF(row[kTopLeftInput].data().value().x(), + row[kTopLeftInput].data().value().y()); + break; + case 1: // Top right + return QPointF(resolution.x() + row[kTopRightInput].data().value().x(), + row[kTopRightInput].data().value().y()); + break; + case 2: // Bottom right + return QPointF(resolution.x() + row[kBottomRightInput].data().value().x(), + resolution.y() + row[kBottomRightInput].data().value().y()); + break; + case 3: //Bottom left + return QPointF(row[kBottomLeftInput].data().value().x(), + row[kBottomLeftInput].data().value().y() + resolution.y()); + break; + default: // We should never get here + return QPointF(); + } +} + +void CornerPinDistortNode::DrawGizmos(const NodeValueRow &row, const NodeGlobals &globals, QPainter *p) +{ + const QVector2D &resolution = globals.resolution(); + + const double handle_radius = GetGizmoHandleRadius(p->transform()); + + p->setPen(QPen(Qt::white, 0)); + + QPointF top_left = ValueToPixel(0, row, resolution); + QPointF top_right = ValueToPixel(1, row, resolution); + QPointF bottom_right = ValueToPixel(2, row, resolution); + QPointF bottom_left = ValueToPixel(3, row, resolution); + + // Add the correct offset to each slider + SetInputProperty(kTopLeftInput, QStringLiteral("offset"), QVector2D(0.0, 0.0)); + SetInputProperty(kTopRightInput, QStringLiteral("offset"), QVector2D(resolution.x() , 0.0)); + SetInputProperty(kBottomRightInput, QStringLiteral("offset"), resolution); + SetInputProperty(kBottomLeftInput, QStringLiteral("offset"), QVector2D(0.0, resolution.y())); + + // Draw bounding box + p->drawLine(QLineF(top_left, top_right)); + p->drawLine(QLineF(top_right, bottom_right)); + p->drawLine(QLineF(bottom_right, bottom_left)); + p->drawLine(QLineF(bottom_left, top_left)); + + // Create handles + gizmo_resize_handle_[0] = CreateGizmoHandleRect(top_left, handle_radius); + gizmo_resize_handle_[1] = CreateGizmoHandleRect(top_right, handle_radius); + gizmo_resize_handle_[2] = CreateGizmoHandleRect(bottom_right, handle_radius); + gizmo_resize_handle_[3] = CreateGizmoHandleRect(bottom_left, handle_radius); + + // Draw handles + DrawAndExpandGizmoHandles(p, handle_radius, gizmo_resize_handle_, kGizmoCornerCount); +} + +bool CornerPinDistortNode::GizmoPress(const NodeValueRow &row, const NodeGlobals &globals, const QPointF &p) +{ + bool gizmo_active[kGizmoCornerCount] = {false}; + + for (int i = 0; i < kGizmoCornerCount; i++) { + gizmo_active[i] = gizmo_resize_handle_[i].contains(p); + + if (gizmo_active[i]) { + gizmo_drag_start_ = p; + gizmo_res_ = globals.resolution(); + gizmo_drag_ = i; + return true; + } + } + + return false; +} + +void CornerPinDistortNode::GizmoMove(const QPointF &p, const rational &time, const Qt::KeyboardModifiers &modifiers) +{ + if (gizmo_dragger_.isEmpty()) { + gizmo_dragger_.resize(2); + if (gizmo_drag_ == 0) { + gizmo_dragger_[0].Start(NodeKeyframeTrackReference(NodeInput(this, kTopLeftInput), 0), time); + gizmo_dragger_[1].Start(NodeKeyframeTrackReference(NodeInput(this, kTopLeftInput), 1), time); + } + if (gizmo_drag_ == 1) { + gizmo_dragger_[0].Start(NodeKeyframeTrackReference(NodeInput(this, kTopRightInput), 0), time); + gizmo_dragger_[1].Start(NodeKeyframeTrackReference(NodeInput(this, kTopRightInput), 1), time); + } + if (gizmo_drag_ == 2) { + gizmo_dragger_[0].Start(NodeKeyframeTrackReference(NodeInput(this, kBottomRightInput), 0), time); + gizmo_dragger_[1].Start(NodeKeyframeTrackReference(NodeInput(this, kBottomRightInput), 1), time); + } + if (gizmo_drag_ == 3) { + gizmo_dragger_[0].Start(NodeKeyframeTrackReference(NodeInput(this, kBottomLeftInput), 0), time); + gizmo_dragger_[1].Start(NodeKeyframeTrackReference(NodeInput(this, kBottomLeftInput), 1), time); + } + } + + QPointF diff = p - gizmo_drag_start_; + gizmo_dragger_[0].Drag(gizmo_dragger_[0].GetStartValue().toDouble() + diff.x()); + gizmo_dragger_[1].Drag(gizmo_dragger_[1].GetStartValue().toDouble() + diff.y()); +} + +void CornerPinDistortNode::GizmoRelease(MultiUndoCommand *command) { + for (NodeInputDragger &i : gizmo_dragger_) { + i.End(command); + } + gizmo_dragger_.clear(); +} + +} diff --git a/app/node/distort/cornerpin/cornerpindistortnode.h b/app/node/distort/cornerpin/cornerpindistortnode.h new file mode 100644 index 000000000..6261d7e1a --- /dev/null +++ b/app/node/distort/cornerpin/cornerpindistortnode.h @@ -0,0 +1,109 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef CORNERPINDISTORTNODE_H +#define CORNERPINDISTORTNODE_H + +#include + +#include "node/inputdragger.h" +#include "node/node.h" + +namespace olive { +class CornerPinDistortNode : public Node +{ + Q_OBJECT +public: + CornerPinDistortNode(); + + NODE_DEFAULT_DESTRUCTOR(CornerPinDistortNode) + + virtual Node* copy() const override + { + return new CornerPinDistortNode(); + } + + virtual QString Name() const override + { + return tr("Corner Pin"); + } + + virtual QString id() const override + { + return QStringLiteral("org.olivevideoeditor.Olive.cornerpin"); + } + + virtual QVector Category() const override + { + return {kCategoryDistort}; + } + + virtual QString Description() const override + { + return tr("Distort the image by dragging the corners."); + } + + virtual void Retranslate() override; + + virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override; + + virtual ShaderCode GetShaderCode(const QString &shader_id) const override; + + virtual bool HasGizmos() const override + { + return true; + } + + virtual void DrawGizmos(const NodeValueRow& row, const NodeGlobals &globals, QPainter *p) override; + + virtual bool GizmoPress(const NodeValueRow& row, const NodeGlobals &globals, const QPointF &p) override; + virtual void GizmoMove(const QPointF &p, const rational &time, const Qt::KeyboardModifiers &modifiers) override; + virtual void GizmoRelease(MultiUndoCommand *command) override; + + /** + * @brief Convenience function - converts the 2D slider values from being + * an offset to the actual pixel value. + */ + QPointF ValueToPixel(int value, const NodeValueRow &row, const QVector2D &resolution) const; + + static const QString kTextureInput; + static const QString kPerspectiveInput; + static const QString kTopLeftInput; + static const QString kTopRightInput; + static const QString kBottomRightInput; + static const QString kBottomLeftInput; + +private: + // Gizmo variables + static const int kGizmoCornerCount = 4; + QRectF gizmo_resize_handle_[kGizmoCornerCount]; + QRectF gizmo_whole_rect_; + + int gizmo_drag_; + QVector gizmo_dragger_; + QPointF gizmo_drag_start_; + QVector2D gizmo_res_; + +}; + +} + + +#endif // CORNERPINDISTORTNODE_H diff --git a/app/node/factory.cpp b/app/node/factory.cpp index 7afcec6d9..ed28bc535 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -29,6 +29,7 @@ #include "block/subtitle/subtitle.h" #include "block/transition/crossdissolve/crossdissolvetransition.h" #include "block/transition/diptocolor/diptocolortransition.h" +#include "distort/cornerpin/cornerpindistortnode.h" #include "distort/crop/cropdistortnode.h" #include "distort/flip/flipdistortnode.h" #include "distort/transform/transformdistortnode.h" @@ -264,6 +265,8 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) return new NoiseGeneratorNode(); case kTimeOffsetNode: return new TimeOffsetNode(); + case kCornerPinDistort: + return new CornerPinDistortNode(); case kInternalNodeCount: break; diff --git a/app/node/factory.h b/app/node/factory.h index 5e627841c..77fe474eb 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -66,6 +66,7 @@ public: kFlipDistort, kNoiseGenerator, kTimeOffsetNode, + kCornerPinDistort, // Count value kInternalNodeCount diff --git a/app/render/job/shaderjob.h b/app/render/job/shaderjob.h index 3bbbac19d..77a08ae9f 100644 --- a/app/render/job/shaderjob.h +++ b/app/render/job/shaderjob.h @@ -22,6 +22,7 @@ #define SHADERJOB_H #include +#include #include "generatejob.h" #include "render/texture.h" @@ -87,6 +88,16 @@ public: interpolation_.insert(id, interp); } + void SetVertexCoordinates(const QVector &vertex_coords) + { + vertex_overrides_ = vertex_coords; + } + + const QVector& GetVertexCoordinates() + { + return vertex_overrides_; + } + private: QString shader_id_; @@ -96,6 +107,8 @@ private: QHash interpolation_; + QVector vertex_overrides_; + }; } diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index 37e43e88d..7aa0955cb 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -569,7 +569,13 @@ void OpenGLRenderer::Blit(QVariant s, ShaderJob job, Texture *destination, Video QOpenGLBuffer vert_vbo_; vert_vbo_.create(); vert_vbo_.bind(); - vert_vbo_.allocate(blit_vertices.constData(), blit_vertices.size() * sizeof(GLfloat)); + // If the job has vertex coordinate overrides use them instead of the defaults. + if (!job.GetVertexCoordinates().isEmpty()) { + Q_ASSERT(job.GetVertexCoordinates().size() == 18); + vert_vbo_.allocate(job.GetVertexCoordinates().constData(), job.GetVertexCoordinates().size() * sizeof(float)); + } else { + vert_vbo_.allocate(blit_vertices.constData(), blit_vertices.size() * sizeof(GLfloat)); + } vert_vbo_.release(); QOpenGLBuffer frag_vbo_; @@ -886,7 +892,11 @@ GLuint OpenGLRenderer::CompileShader(GLenum type, const QString &code) QStringLiteral("#version 120\n\n"); #endif - QString complete_code = shader_preamble; + QString complete_code; + + if (!code.startsWith(QStringLiteral("#version"))) { + complete_code = shader_preamble; + } if (code.isEmpty()) { // Use default code diff --git a/app/shaders/cornerpin.frag b/app/shaders/cornerpin.frag new file mode 100644 index 000000000..7dd7e3306 --- /dev/null +++ b/app/shaders/cornerpin.frag @@ -0,0 +1,48 @@ +// Input texture +uniform sampler2D ove_maintex; +uniform sampler2D tex_in; +uniform bool perspective_in; + +// Input texture coordinate +varying vec2 ove_texcoord; + +varying vec2 q; +varying vec2 b1; +varying vec2 b2; +varying vec2 b3; + +float Wedge2D(vec2 v, vec2 w) { + return (v.x*w.y) - (v.y*w.x); +} + +void main() { + if(perspective_in){ + gl_FragColor = texture2D(tex_in, ove_texcoord); + } else { + float A = Wedge2D(b2, b3); + float B = Wedge2D(b3, q) - Wedge2D(b1, b2); + float C = Wedge2D(b1, q); + + vec2 uv; + + // solve for v + if (abs(A) < 0.001) { + uv.y = -C/B; + } else { + float discrim = B*B - 4.0*A*C; + uv.y = 0.5 * (-B + sqrt(discrim)) / A; + } + + // solve for u + vec2 denom = b1 + uv.y * b3; + if (abs(denom.x) > abs(denom.y)) { + uv.x = (q.x - b2.x * uv.y) / denom.x; + } else { + uv.x = (q.y - b2.y * uv.y) / denom.y; + } + + uv.y = 1.0 - uv.y; + + gl_FragColor = texture2D(tex_in, uv); + } +} diff --git a/app/shaders/cornerpin.vert b/app/shaders/cornerpin.vert new file mode 100644 index 000000000..21c2b0527 --- /dev/null +++ b/app/shaders/cornerpin.vert @@ -0,0 +1,101 @@ +#version 130 + +uniform bool perspective_in; +uniform vec2 top_left_in; +uniform vec2 top_right_in; +uniform vec2 bottom_left_in; +uniform vec2 bottom_right_in; + +uniform vec2 resolution_in; + +uniform mat4 ove_mvpmat; + +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 ove_texcoord; + +varying vec2 q; +varying vec2 b1; +varying vec2 b2; +varying vec2 b3; + +void main() { + // The slider inputs only contain the amount they have changed rather than + // their pixel locations so we adjust them here. + vec2 t_l = top_left_in; + vec2 t_r = top_right_in + vec2(resolution_in.x, 0.0); + vec2 b_r = bottom_right_in + resolution_in; + vec2 b_l = bottom_left_in + vec2(0.0, resolution_in.y); + + gl_Position = ove_mvpmat * a_position; + + if (perspective_in){ + // Find the center of the quadrilateral by finding where the two diagonals intersect. + // https://www.reedbeta.com/blog/quadrilateral-interpolation-part-1/ + + // Here we calculate the gradient and constant (y = mx + c) for each diagonal. + float m1 = (t_r.y - b_l.y)/(t_r.x - b_l.x); + float c1 = b_l.y - m1 * b_l.x; + float m2 = (b_r.y - t_l.y)/(b_r.x - t_l.x); + float c2 = t_l.y - m2 * t_l.x; + + // Find the intersection by setting the two line equations equal and rearrange. + float mid_x = (c2 - c1) / (m1 - m2); + float mid_y = m1 * mid_x + c1; + + // Find the distance from each corner to our center point + float d0 = length(vec2(mid_x - b_l.x, mid_y - b_l.y)); + float d1 = length(vec2(b_r.x - mid_x, mid_y - b_r.y)); + float d2 = length(vec2(t_r.x - mid_x, t_r.y - mid_y)); + float d3 = length(vec2(mid_x - t_l.x, t_l.y - mid_y)); + + float q = 1.0; + + /* + Vertex IDs (aspect ratio irrelevant): + 0_____1 + 3|\ | + | \ | + | \ | + | \ | + |____\|2 + 4 5 + */ + + if (gl_VertexID == 0 || gl_VertexID == 3) { + q = (d1+d3)/d3; + } else if (gl_VertexID == 1) { + q = (d0+d2)/d2; + } else if (gl_VertexID == 2 || gl_VertexID == 5) { + q = (d3+d1)/d1; + } else { + q = (d2+d0)/d0; + } + + gl_Position[0] *= q; + gl_Position[1] *= q; + gl_Position[3] = q; + } else{ + // https://www.reedbeta.com/blog/quadrilateral-interpolation-part-2/ + vec2 pos; + + if (gl_VertexID == 0 || gl_VertexID == 3) { // top left + pos = t_l; + } else if (gl_VertexID == 1) { // top right + pos = t_r; + } else if (gl_VertexID == 2 || gl_VertexID == 5) { // bottom right + pos = b_r; + } else if (gl_VertexID == 4) { // bottom left + pos = b_l; + } + + q = pos - b_l; + b1 = b_r - b_l; + b2 = t_l - b_l; + b3 = b_l - b_r - t_l + t_r; + } + + + ove_texcoord = a_texcoord; +}