/*** Olive - Non-Linear Video Editor Copyright (C) 2019 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 "effect.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "panels/panels.h" #include "panels/viewer.h" #include "ui/viewerwidget.h" #include "ui/collapsiblewidget.h" #include "panels/project.h" #include "undo/undo.h" #include "timeline/sequence.h" #include "timeline/clip.h" #include "panels/timeline.h" #include "panels/effectcontrols.h" #include "panels/grapheditor.h" #include "global/debug.h" #include "global/path.h" #include "ui/mainwindow.h" #include "global/math.h" #include "global/clipboard.h" #include "global/config.h" #include "transition.h" #include "undo/undostack.h" #include "rendering/shadergenerators.h" #include "effects/internal/transformeffect.h" #include "effects/internal/texteffect.h" #include "effects/internal/timecodeeffect.h" #include "effects/internal/solideffect.h" #include "effects/internal/audionoiseeffect.h" #include "effects/internal/toneeffect.h" #include "effects/internal/volumeeffect.h" #include "effects/internal/paneffect.h" #include "effects/internal/shakeeffect.h" #include "effects/internal/cornerpineffect.h" #include "effects/internal/vsthost.h" #include "effects/internal/fillleftrighteffect.h" #include "effects/internal/richtexteffect.h" QVector olive::effects; QVector olive::blend_modes; QString olive::generated_blending_shader; EffectPtr Effect::Create(Clip* c, const EffectMeta* em) { if (em->internal >= 0 && em->internal < EFFECT_INTERNAL_COUNT) { // must be an internal effect switch (em->internal) { case EFFECT_INTERNAL_TRANSFORM: return std::make_shared(c, em); case EFFECT_INTERNAL_TEXT: return std::make_shared(c, em); case EFFECT_INTERNAL_TIMECODE: return std::make_shared(c, em); case EFFECT_INTERNAL_SOLID: return std::make_shared(c, em); case EFFECT_INTERNAL_NOISE: return std::make_shared(c, em); case EFFECT_INTERNAL_VOLUME: return std::make_shared(c, em); case EFFECT_INTERNAL_PAN: return std::make_shared(c, em); case EFFECT_INTERNAL_TONE: return std::make_shared(c, em); case EFFECT_INTERNAL_SHAKE: return std::make_shared(c, em); case EFFECT_INTERNAL_CORNERPIN: return std::make_shared(c, em); case EFFECT_INTERNAL_FILLLEFTRIGHT: return std::make_shared(c, em); case EFFECT_INTERNAL_VST: return std::make_shared(c, em); case EFFECT_INTERNAL_RICHTEXT: return std::make_shared(c, em); } } else if (!em->filename.isEmpty()) { // load effect from file return std::make_shared(c, em); } else { qCritical() << "Invalid effect data"; QMessageBox::critical(olive::MainWindow, QCoreApplication::translate("Effect", "Invalid effect"), QCoreApplication::translate("Effect", "No candidate for effect '%1'. This effect may be corrupt. Try reinstalling it or Olive.").arg(em->name)); } return nullptr; } const EffectMeta* Effect::GetInternalMeta(int internal_id, int type) { for (int i=0;iname; if (!em->filename.isEmpty() && em->internal == -1) { QFile effect_file(em->filename); if (effect_file.open(QFile::ReadOnly)) { QXmlStreamReader reader(&effect_file); while (!reader.atEnd()) { if (reader.name() == "row" && reader.isStartElement()) { QString row_name; const QXmlStreamAttributes& attributes = reader.attributes(); for (int i=0;ifilename << "- ID cannot be empty."; } else { EffectField* field = nullptr; switch (type) { case EffectField::EFFECT_FIELD_DOUBLE: { DoubleField* double_field = new DoubleField(row, id); for (int i=0;iSetDefault(attr.value().toDouble()); } else if (attr.name() == "min") { double_field->SetMinimum(attr.value().toDouble()); } else if (attr.name() == "max") { double_field->SetMaximum(attr.value().toDouble()); } } field = double_field; } break; case EffectField::EFFECT_FIELD_COLOR: { QColor color; field = new ColorField(row, id); for (int i=0;iSetValueAt(0, color); } break; case EffectField::EFFECT_FIELD_STRING: field = new StringField(row, id); for (int i=0;iSetValueAt(0, attr.value().toString()); } } break; case EffectField::EFFECT_FIELD_BOOL: field = new BoolField(row, id); for (int i=0;iSetValueAt(0, attr.value() == "1"); } } break; case EffectField::EFFECT_FIELD_COMBO: { ComboField* combo_field = new ComboField(row, id); int combo_default_index = 0; for (int i=0;iAddItem(reader.text().toString(), combo_item_count); combo_item_count++; } } combo_field->SetValueAt(0, combo_default_index); field = combo_field; } break; case EffectField::EFFECT_FIELD_FONT: field = new FontField(row, id); for (int i=0;iSetValueAt(0, attr.value().toString()); } } break; case EffectField::EFFECT_FIELD_FILE: field = new FileField(row, id); for (int i=0;iSetValueAt(0, attr.value().toString()); } } break; } } } } } } else if (reader.name() == "shader" && reader.isStartElement()) { SetFlags(Flags() | ShaderFlag); const QXmlStreamAttributes& attributes = reader.attributes(); for (int i=0;ifilename; enable_superimpose = false; } break; } } }*/ reader.readNext(); } effect_file.close(); } else { qCritical() << "Failed to open effect file" << em->filename; } } } } Effect::~Effect() { if (isOpen) { close(); } // Clear graph editor if it's using one of these rows if (panel_graph_editor != nullptr) { for (int i=0;iget_row()) { panel_graph_editor->set_row(nullptr); break; } } } } void Effect::AddRow(EffectRow *row) { row->setParent(this); rows.append(row); } void Effect::copy_field_keyframes(EffectPtr e) { for (int i=0;irows.at(i); copy_row->SetKeyframingInternal(row->IsKeyframing()); for (int j=0;jFieldCount();j++) { // Get field from this (the source) effect EffectField* field = row->Field(j); // Get field from the destination effect EffectField* copy_field = copy_row->Field(j); // Copy keyframes between effects copy_field->keyframes = field->keyframes; // Copy persistet data between effects copy_field->persistent_data_ = field->persistent_data_; } } } EffectRow* Effect::row(int i) { return rows.at(i); } int Effect::row_count() { return rows.size(); } EffectGizmo *Effect::add_gizmo(int type) { EffectGizmo* gizmo = new EffectGizmo(this, type); gizmos.append(gizmo); return gizmo; } EffectGizmo *Effect::gizmo(int i) { return gizmos.at(i); } int Effect::gizmo_count() { return gizmos.size(); } void Effect::refresh() {} void Effect::FieldChanged() { update_ui(false); } void Effect::delete_self() { olive::undo_stack.push(new EffectDeleteCommand(this)); update_ui(true); } void Effect::move_up() { int index_of_effect = parent_clip->IndexOfEffect(this); if (index_of_effect == 0) { return; } MoveEffectCommand* command = new MoveEffectCommand(); command->clip = parent_clip; command->from = index_of_effect; command->to = command->from - 1; olive::undo_stack.push(command); panel_effect_controls->Reload(); panel_sequence_viewer->viewer_widget()->frame_update(); } void Effect::move_down() { int index_of_effect = parent_clip->IndexOfEffect(this); if (index_of_effect == parent_clip->effects.size()-1) { return; } MoveEffectCommand* command = new MoveEffectCommand(); command->clip = parent_clip; command->from = index_of_effect; command->to = command->from + 1; olive::undo_stack.push(command); panel_effect_controls->Reload(); panel_sequence_viewer->viewer_widget()->frame_update(); } void Effect::save_to_file() { // save effect settings to file QString file = QFileDialog::getSaveFileName(olive::MainWindow, tr("Save Effect Settings"), QString(), tr("Effect XML Settings %1").arg("(*.xml)")); // if the user picked a file if (!file.isEmpty()) { // ensure file ends with .xml extension if (!file.endsWith(".xml", Qt::CaseInsensitive)) { file.append(".xml"); } QFile file_handle(file); if (file_handle.open(QFile::WriteOnly)) { file_handle.write(save_to_string()); file_handle.close(); } else { QMessageBox::critical(olive::MainWindow, tr("Save Settings Failed"), tr("Failed to open \"%1\" for writing.").arg(file), QMessageBox::Ok); } } } void Effect::load_from_file() { // load effect settings from file QString file = QFileDialog::getOpenFileName(olive::MainWindow, tr("Load Effect Settings"), QString(), tr("Effect XML Settings %1").arg("(*.xml)")); // if the user picked a file if (!file.isEmpty()) { QFile file_handle(file); if (file_handle.open(QFile::ReadOnly)) { olive::undo_stack.push(new SetEffectData(this, file_handle.readAll())); file_handle.close(); update_ui(false); } else { QMessageBox::critical(olive::MainWindow, tr("Load Settings Failed"), tr("Failed to open \"%1\" for reading.").arg(file), QMessageBox::Ok); } } } bool Effect::AlwaysUpdate() { return false; } bool Effect::IsEnabled() { return enabled_; } bool Effect::IsExpanded() { return expanded_; } void Effect::SetExpanded(bool e) { expanded_ = e; } void Effect::SetEnabled(bool b) { enabled_ = b; emit EnabledChanged(b); } void Effect::load(QXmlStreamReader& stream) { int row_count = 0; QString tag = stream.name().toString(); while (!stream.atEnd() && !(stream.name() == tag && stream.isEndElement())) { stream.readNext(); if (stream.name() == "row" && stream.isStartElement()) { if (row_count < rows.size()) { EffectRow* row = rows.at(row_count); while (!stream.atEnd() && !(stream.name() == "row" && stream.isEndElement())) { stream.readNext(); // read field if (stream.name() == "field" && stream.isStartElement()) { int field_number = -1; // match field using ID for (int k=0;kFieldCount();l++) { if (row->Field(l)->id() == attr.value()) { field_number = l; break; } } break; } } if (field_number > -1) { EffectField* field = row->Field(field_number); // get current field value for (int k=0;kpersistent_data_ = field->ConvertStringToValue(attr.value().toString()); break; } } while (!stream.atEnd() && !(stream.name() == "field" && stream.isEndElement())) { stream.readNext(); // read keyframes if (stream.name() == "key" && stream.isStartElement()) { row->SetKeyframingInternal(true); EffectKeyframe key; for (int k=0;kConvertStringToValue(attr.value().toString()); } else if (attr.name() == "frame") { key.time = attr.value().toLong(); } else if (attr.name() == "type") { key.type = attr.value().toInt(); } else if (attr.name() == "prehx") { key.pre_handle_x = attr.value().toDouble(); } else if (attr.name() == "prehy") { key.pre_handle_y = attr.value().toDouble(); } else if (attr.name() == "posthx") { key.post_handle_x = attr.value().toDouble(); } else if (attr.name() == "posthy") { key.post_handle_y = attr.value().toDouble(); } } field->keyframes.append(key); } } field->Changed(); } } } } else { qCritical() << "Too many rows for effect" << id << ". Project might be corrupt. (Got" << row_count << ", expected <" << rows.size()-1 << ")"; } row_count++; } else if (stream.isStartElement()) { custom_load(stream); } } } void Effect::custom_load(QXmlStreamReader &) {} void Effect::save(QXmlStreamWriter& stream) { stream.writeAttribute("name", meta->category + "/" + meta->name); stream.writeAttribute("enabled", QString::number(IsEnabled())); for (int i=0;iIsSavable()) { stream.writeStartElement("row"); // row for (int j=0;jFieldCount();j++) { EffectField* field = row->Field(j); if (!field->id().isEmpty()) { stream.writeStartElement("field"); // field stream.writeAttribute("id", field->id()); stream.writeAttribute("value", field->ConvertValueToString(field->persistent_data_)); for (int k=0;kkeyframes.size();k++) { const EffectKeyframe& key = field->keyframes.at(k); stream.writeStartElement("key"); stream.writeAttribute("value", field->ConvertValueToString(key.data)); stream.writeAttribute("frame", QString::number(key.time)); stream.writeAttribute("type", QString::number(key.type)); stream.writeAttribute("prehx", QString::number(key.pre_handle_x)); stream.writeAttribute("prehy", QString::number(key.pre_handle_y)); stream.writeAttribute("posthx", QString::number(key.post_handle_x)); stream.writeAttribute("posthy", QString::number(key.post_handle_y)); stream.writeEndElement(); // key } stream.writeEndElement(); // field } } stream.writeEndElement(); // row } } } void Effect::load_from_string(const QByteArray &s) { // clear existing keyframe data for (int i=0;iSetKeyframingInternal(false); for (int j=0;jFieldCount();j++) { EffectField* field = row->Field(j); field->keyframes.clear(); } } // write settings with xml writer QXmlStreamReader stream(s); while (!stream.atEnd()) { stream.readNext(); // find the effect opening tag if (stream.name() == "effect" && stream.isStartElement()) { // check the name to see if it matches this effect const QXmlStreamAttributes& attributes = stream.attributes(); for (int i=0;ipath.isEmpty() || (shader_vert_path_.isEmpty() && shader_frag_path_.isEmpty())) return; QList effects_paths = get_effects_paths(); const QString& test_fn = shader_vert_path_.isEmpty() ? shader_frag_path_ : shader_vert_path_; for (int i=0;ipath).filePath(shader_frag_path_); QFile frag_file(frag_file_url); if (frag_file.open(QFile::ReadOnly)) { frag_shader_str = frag_file.readAll(); frag_file.close(); } else { qWarning() << "Failed to open" << frag_file_url; } if (!frag_shader_str.isEmpty()) { QString shader_func; if (!shader_function_name_.isEmpty()) { shader_func = shader_function_name_; } else { shader_func = "process"; } shader_program_ = olive::shader::GetPipeline(shader_func, frag_shader_str); } /* bool shader_compiled = true; if (!shader_vert_path_.isEmpty()) { if (shader_program_->addShaderFromSourceFile(QOpenGLShader::Vertex, meta->path + "/" + shader_vert_path_)) { qInfo() << "Vertex shader added successfully"; } else { shader_compiled = false; qWarning() << "Vertex shader could not be added"; } } if (!shader_frag_path_.isEmpty()) { if (shader_program_->addShaderFromSourceFile(QOpenGLShader::Fragment, meta->path + "/" + shader_frag_path_)) { qInfo() << "Fragment shader added successfully"; } else { shader_compiled = false; qWarning() << "Fragment shader could not be added"; } } if (shader_compiled) { if (shader_program_->link()) { qInfo() << "Shader program linked successfully"; } else { qWarning() << "Shader program failed to link"; } } */ isOpen = true; } } else { isOpen = true; } } void Effect::close() { if (!isOpen) { qWarning() << "Tried to close an effect that was already closed"; } delete_texture(); shader_program_ = nullptr; isOpen = false; } bool Effect::is_shader_linked() { return shader_program_ != nullptr && shader_program_->isLinked(); } QOpenGLShaderProgram *Effect::GetShaderPipeline() { return shader_program_.get(); } int Effect::Flags() { return flags_; } void Effect::SetFlags(int flags) { flags_ = flags; } int Effect::getIterations() { return iterations; } void Effect::setIterations(int i) { iterations = i; } void Effect::process_image(double, uint8_t *, uint8_t *, int){} EffectPtr Effect::copy(Clip *c) { EffectPtr copy = Effect::Create(c, meta); copy->SetEnabled(IsEnabled()); copy_field_keyframes(copy); return copy; } void Effect::process_shader(double timecode, GLTextureCoords&, int iteration) { shader_program_->bind(); shader_program_->setUniformValue("resolution", parent_clip->media_width(), parent_clip->media_height()); shader_program_->setUniformValue("time", GLfloat(timecode)); shader_program_->setUniformValue("iteration", iteration); for (int i=0;iFieldCount();j++) { EffectField* field = row->Field(j); if (!field->id().isEmpty()) { switch (field->type()) { case EffectField::EFFECT_FIELD_DOUBLE: { DoubleField* double_field = static_cast(field); shader_program_->setUniformValue(double_field->id().toUtf8().constData(), GLfloat(double_field->GetDoubleAt(timecode))); } break; case EffectField::EFFECT_FIELD_COLOR: { ColorField* color_field = static_cast(field); shader_program_->setUniformValue( color_field->id().toUtf8().constData(), GLfloat(color_field->GetColorAt(timecode).redF()), GLfloat(color_field->GetColorAt(timecode).greenF()), GLfloat(color_field->GetColorAt(timecode).blueF()) ); } break; case EffectField::EFFECT_FIELD_BOOL: shader_program_->setUniformValue(field->id().toUtf8().constData(), field->GetValueAt(timecode).toBool()); break; case EffectField::EFFECT_FIELD_COMBO: shader_program_->setUniformValue(field->id().toUtf8().constData(), field->GetValueAt(timecode).toInt()); break; // can you even send a string to a uniform value? case EffectField::EFFECT_FIELD_STRING: case EffectField::EFFECT_FIELD_FONT: case EffectField::EFFECT_FIELD_FILE: case EffectField::EFFECT_FIELD_UI: break; } } } } shader_program_->release(); } void Effect::process_coords(double, GLTextureCoords&, int) {} GLuint Effect::process_superimpose(QOpenGLContext* ctx, double timecode) { bool dimensions_changed = false; bool redrew_image = false; int width = parent_clip->media_width(); int height = parent_clip->media_height(); if (width != img.width() || height != img.height()) { img = QImage(width, height, QImage::Format_RGBA8888_Premultiplied); dimensions_changed = true; } if (valueHasChanged(timecode) || dimensions_changed || AlwaysUpdate()) { redraw(timecode); redrew_image = true; } QOpenGLFunctions* f = ctx->functions(); if (texture == 0 || tex_width_ != img.width() || tex_height_ != img.height()) { delete_texture(); tex_width_ = img.width(); tex_height_ = img.height(); // create texture object f->glGenTextures(1, &texture); f->glBindTexture(GL_TEXTURE_2D, texture); // set texture filtering to bilinear f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); f->glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, tex_width_, tex_height_, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.constBits() ); f->glBindTexture(GL_TEXTURE_2D, 0); redrew_image = false; } if (redrew_image) { f->glBindTexture(GL_TEXTURE_2D, texture); f->glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, tex_width_, tex_height_, GL_RGBA, GL_UNSIGNED_BYTE, img.constBits()); f->glBindTexture(GL_TEXTURE_2D, 0); } return texture; } void Effect::process_audio(double, double, float **, int, int, int) {} void Effect::gizmo_draw(double, GLTextureCoords &) {} void Effect::gizmo_move(EffectGizmo* gizmo, int x_movement, int y_movement, double timecode, bool done) { // Loop through each gizmo to find `gizmo` for (int i=0;ix_field1 != nullptr) { gizmo_dragging_actions_.append(new KeyframeDataChange(gizmo->x_field1)); } if (gizmo->y_field1 != nullptr) { gizmo_dragging_actions_.append(new KeyframeDataChange(gizmo->y_field1)); } if (gizmo->x_field2 != nullptr) { gizmo_dragging_actions_.append(new KeyframeDataChange(gizmo->x_field2)); } if (gizmo->y_field2 != nullptr) { gizmo_dragging_actions_.append(new KeyframeDataChange(gizmo->y_field2)); } } // Update the field values if (gizmo->x_field1 != nullptr) { gizmo->x_field1->SetValueAt(timecode, gizmo->x_field1->GetDoubleAt(timecode) + x_movement*gizmo->x_field_multi1); } if (gizmo->y_field1 != nullptr) { gizmo->y_field1->SetValueAt(timecode, gizmo->y_field1->GetDoubleAt(timecode) + y_movement*gizmo->y_field_multi1); } if (gizmo->x_field2 != nullptr) { gizmo->x_field2->SetValueAt(timecode, gizmo->x_field2->GetDoubleAt(timecode) + x_movement*gizmo->x_field_multi2); } if (gizmo->y_field2 != nullptr) { gizmo->y_field2->SetValueAt(timecode, gizmo->y_field2->GetDoubleAt(timecode) + y_movement*gizmo->y_field_multi2); } // If (done && !gizmo_dragging_actions_.isEmpty()), that means the drag just ended and we're going to save // the new state of the attach fields' keyframes in KeyframeDataChange objects to make the changes undoable // by the user later. if (done && !gizmo_dragging_actions_.isEmpty()) { // Store all the KeyframeDataChange objects into a ComboAction to send to the undo stack (makes them all // undoable together rather than having to be undone individually). ComboAction* ca = new ComboAction(); for (int j=0;jSetNewKeyframes(); // Add this KeyframeDataChange object to the ComboAction ca->append(gizmo_dragging_actions_.at(j)); } olive::undo_stack.push(ca); gizmo_dragging_actions_.clear(); } break; } } } void Effect::gizmo_world_to_screen(const QMatrix4x4& matrix, const QMatrix4x4& projection) { for (int i=0;iget_point_count();j++) { // Convert the world point from the gizmo into a screen point relative to the sequence's dimensions QVector3D screen_pos = g->world_pos.at(j).project(matrix, projection, QRect(0, 0, parent_clip->track()->sequence()->width, parent_clip->track()->sequence()->height)); g->screen_pos[j] = QPoint(screen_pos.x(), parent_clip->track()->sequence()->height-screen_pos.y()); } } } bool Effect::are_gizmos_enabled() { return (gizmos.size() > 0); } void Effect::redraw(double) { /* // run javascript QPainter p(&img); painter_wrapper.img = &img; painter_wrapper.painter = &p; jsEngine.globalObject().setProperty("painter", wrapper_obj); jsEngine.globalObject().setProperty("width", parent_clip->media_width()); jsEngine.globalObject().setProperty("height", parent_clip->media_height()); for (int i=0;ifieldCount();j++) { EffectField* field = row->field(j); if (!field->id.isEmpty()) { switch (field->type) { case EFFECT_FIELD_DOUBLE: jsEngine.globalObject().setProperty(field->id, field->get_double_value(timecode)); break; case EFFECT_FIELD_COLOR: jsEngine.globalObject().setProperty(field->id, field->get_color_value(timecode).name()); break; case EFFECT_FIELD_STRING: jsEngine.globalObject().setProperty(field->id, field->get_string_value(timecode)); break; case EFFECT_FIELD_BOOL: jsEngine.globalObject().setProperty(field->id, field->get_bool_value(timecode)); break; case EFFECT_FIELD_COMBO: jsEngine.globalObject().setProperty(field->id, field->get_combo_index(timecode)); break; case EFFECT_FIELD_FONT: jsEngine.globalObject().setProperty(field->id, field->get_font_name(timecode)); break; } } } } jsEngine.evaluate(script); */ } bool Effect::valueHasChanged(double timecode) { if (cachedValues.isEmpty()) { for (int i=0;iFieldCount();j++) { cachedValues.append(crow->Field(j)->GetValueAt(timecode)); } } return true; } else { bool changed = false; int index = 0; for (int i=0;iFieldCount();j++) { EffectField* field = crow->Field(j); if (cachedValues.at(index) != field->GetValueAt(timecode)) { changed = true; } cachedValues[index] = field->GetValueAt(timecode); index++; } } return changed; } } void Effect::delete_texture() { if (texture_ctx != nullptr) { texture_ctx->functions()->glDeleteTextures(1, &texture); texture = 0; texture_ctx = nullptr; } } const EffectMeta* Effect::GetMetaFromName(const QString& input) { int split_index = input.indexOf('/'); QString category; if (split_index > -1) { category = input.left(split_index); } QString name = input.mid(split_index + 1); for (int j=0;j