mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-11 18:34:58 +01:00
Management of inputs in Control, Management of callbacks creator per input in Source, Saving and Loading in Session, Unified renaming of SourceCallbacks, User interface window for creating and editing input mapping from Keyboard and Numerical keypad, with appropriate Settings.
1033 lines
36 KiB
C++
1033 lines
36 KiB
C++
/*
|
|
* This file is part of vimix - video live mixer
|
|
*
|
|
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
**/
|
|
|
|
#include <algorithm>
|
|
#include <locale>
|
|
#include <tinyxml2.h>
|
|
#include <glm/gtc/matrix_access.hpp>
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
|
|
#include "defines.h"
|
|
#include "Log.h"
|
|
#include "FrameBuffer.h"
|
|
#include "Decorations.h"
|
|
#include "Resource.h"
|
|
#include "SearchVisitor.h"
|
|
#include "ImageShader.h"
|
|
#include "ImageProcessingShader.h"
|
|
#include "BaseToolkit.h"
|
|
#include "SystemToolkit.h"
|
|
#include "MixingGroup.h"
|
|
#include "ControlManager.h"
|
|
#include "SourceCallback.h"
|
|
|
|
#include "CloneSource.h"
|
|
#include "Source.h"
|
|
|
|
SourceCore::SourceCore()
|
|
{
|
|
// default nodes
|
|
groups_[View::RENDERING] = new Group;
|
|
groups_[View::RENDERING]->visible_ = false;
|
|
groups_[View::MIXING] = new Group;
|
|
groups_[View::MIXING]->visible_ = false;
|
|
groups_[View::GEOMETRY] = new Group;
|
|
groups_[View::GEOMETRY]->visible_ = false;
|
|
groups_[View::LAYER] = new Group;
|
|
groups_[View::LAYER]->visible_ = false;
|
|
groups_[View::TEXTURE] = new Group;
|
|
groups_[View::TEXTURE]->visible_ = false;
|
|
groups_[View::TRANSITION] = new Group;
|
|
// temp node
|
|
stored_status_ = new Group;
|
|
|
|
// filtered image shader (with texturing and processing) for rendering
|
|
processingshader_ = new ImageProcessingShader;
|
|
// default rendering with image processing disabled
|
|
renderingshader_ = static_cast<Shader *>(new ImageShader);
|
|
}
|
|
|
|
SourceCore::SourceCore(SourceCore const& other) : SourceCore()
|
|
{
|
|
copy(other);
|
|
}
|
|
|
|
SourceCore::~SourceCore()
|
|
{
|
|
// all groups and their children are deleted in the scene
|
|
// this deletes renderingshader_ (and all source-attached nodes
|
|
// e.g. rendersurface_, overlays, blendingshader_, etc.)
|
|
delete groups_[View::RENDERING];
|
|
delete groups_[View::MIXING];
|
|
delete groups_[View::GEOMETRY];
|
|
delete groups_[View::LAYER];
|
|
delete groups_[View::TEXTURE];
|
|
delete groups_[View::TRANSITION];
|
|
delete stored_status_;
|
|
|
|
// don't forget that the processing shader
|
|
// could be created but not used and not deleted above
|
|
if ( renderingshader_ != processingshader_ )
|
|
delete processingshader_;
|
|
|
|
groups_.clear();
|
|
}
|
|
|
|
void SourceCore::copy(SourceCore const& other)
|
|
{
|
|
// copy groups properties
|
|
// groups_[View::RENDERING]->copyTransform( other.group(View::RENDERING) );
|
|
groups_[View::MIXING]->copyTransform( other.group(View::MIXING) );
|
|
groups_[View::GEOMETRY]->copyTransform( other.group(View::GEOMETRY) );
|
|
groups_[View::LAYER]->copyTransform( other.group(View::LAYER) );
|
|
groups_[View::TEXTURE]->copyTransform( other.group(View::TEXTURE) );
|
|
groups_[View::TRANSITION]->copyTransform( other.group(View::TRANSITION) );
|
|
stored_status_->copyTransform( other.stored_status_ );
|
|
|
|
// copy shader properties
|
|
processingshader_->copy(*other.processingshader_);
|
|
renderingshader_->copy(*other.renderingshader_);
|
|
}
|
|
|
|
void SourceCore::store (View::Mode m)
|
|
{
|
|
stored_status_->copyTransform(groups_[m]);
|
|
}
|
|
|
|
SourceCore& SourceCore::operator= (SourceCore const& other)
|
|
{
|
|
if (this != &other) // no self assignment
|
|
copy(other);
|
|
return *this;
|
|
}
|
|
|
|
|
|
Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(nullptr),
|
|
active_(true), locked_(false), need_update_(true), dt_(16.f), workspace_(STAGE)
|
|
{
|
|
// create unique id
|
|
if (id_ == 0)
|
|
id_ = BaseToolkit::uniqueId();
|
|
|
|
sprintf(initials_, "__");
|
|
name_ = "Source";
|
|
mode_ = Source::UNINITIALIZED;
|
|
|
|
// create groups and overlays for each view
|
|
|
|
// default mixing nodes
|
|
groups_[View::MIXING]->scale_ = glm::vec3(0.15f, 0.15f, 1.f);
|
|
groups_[View::MIXING]->translation_ = glm::vec3(DEFAULT_MIXING_TRANSLATION, 0.f);
|
|
|
|
frames_[View::MIXING] = new Switch;
|
|
Frame *frame = new Frame(Frame::ROUND, Frame::THIN, Frame::DROP);
|
|
frame->translation_.z = 0.1;
|
|
frame->color = glm::vec4( COLOR_DEFAULT_SOURCE, 0.95f);
|
|
frames_[View::MIXING]->attach(frame);
|
|
frame = new Frame(Frame::ROUND, Frame::LARGE, Frame::DROP);
|
|
frame->translation_.z = 0.01;
|
|
frame->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
frames_[View::MIXING]->attach(frame);
|
|
groups_[View::MIXING]->attach(frames_[View::MIXING]);
|
|
|
|
// Glyphs show letters from the intials, with Font index 4 (LARGE)
|
|
initial_0_ = new Glyph(4);
|
|
initial_0_->translation_ = glm::vec3(0.2f, 0.8f, 0.1f);
|
|
initial_0_->scale_.y = 0.2f;
|
|
groups_[View::MIXING]->attach(initial_0_);
|
|
initial_1_ = new Glyph(4);
|
|
initial_1_->translation_ = glm::vec3(0.4f, 0.8f, 0.1f);
|
|
initial_1_->scale_.y = 0.2f;
|
|
groups_[View::MIXING]->attach(initial_1_);
|
|
|
|
overlays_[View::MIXING] = new Group;
|
|
overlays_[View::MIXING]->translation_.z = 0.1;
|
|
overlays_[View::MIXING]->visible_ = false;
|
|
Symbol *center = new Symbol(Symbol::CIRCLE_POINT, glm::vec3(0.f, 0.f, 0.1f));
|
|
overlays_[View::MIXING]->attach(center);
|
|
groups_[View::MIXING]->attach(overlays_[View::MIXING]);
|
|
|
|
overlay_mixinggroup_ = new Switch;
|
|
overlay_mixinggroup_->translation_.z = 0.1;
|
|
center = new Symbol(Symbol::CIRCLE_POINT, glm::vec3(0.f, 0.f, 0.1f));
|
|
center->scale_= glm::vec3(1.6f, 1.6f, 1.f);
|
|
center->color = glm::vec4( COLOR_MIXING_GROUP, 0.96f);
|
|
overlay_mixinggroup_->attach(center);
|
|
rotation_mixingroup_ = new Symbol(Symbol::ROTATION, glm::vec3(0.f, 0.f, 0.1f));
|
|
rotation_mixingroup_->color = glm::vec4( COLOR_MIXING_GROUP, 0.94f);
|
|
rotation_mixingroup_->scale_ = glm::vec3(3.f, 3.f, 1.f);
|
|
overlay_mixinggroup_->attach(rotation_mixingroup_);
|
|
groups_[View::MIXING]->attach(overlay_mixinggroup_);
|
|
|
|
// default geometry nodes
|
|
frames_[View::GEOMETRY] = new Switch;
|
|
frame = new Frame(Frame::SHARP, Frame::THIN, Frame::NONE);
|
|
frame->translation_.z = 0.1;
|
|
frame->color = glm::vec4( COLOR_DEFAULT_SOURCE, 0.8f);
|
|
frames_[View::GEOMETRY]->attach(frame);
|
|
frame = new Frame(Frame::SHARP, Frame::LARGE, Frame::GLOW);
|
|
frame->translation_.z = 0.1;
|
|
frame->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
frames_[View::GEOMETRY]->attach(frame);
|
|
groups_[View::GEOMETRY]->attach(frames_[View::GEOMETRY]);
|
|
|
|
overlays_[View::GEOMETRY] = new Group;
|
|
overlays_[View::GEOMETRY]->translation_.z = 0.15;
|
|
overlays_[View::GEOMETRY]->visible_ = false;
|
|
handles_[View::GEOMETRY][Handles::RESIZE] = new Handles(Handles::RESIZE);
|
|
handles_[View::GEOMETRY][Handles::RESIZE]->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
handles_[View::GEOMETRY][Handles::RESIZE]->translation_.z = 0.1;
|
|
overlays_[View::GEOMETRY]->attach(handles_[View::GEOMETRY][Handles::RESIZE]);
|
|
handles_[View::GEOMETRY][Handles::RESIZE_H] = new Handles(Handles::RESIZE_H);
|
|
handles_[View::GEOMETRY][Handles::RESIZE_H]->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
handles_[View::GEOMETRY][Handles::RESIZE_H]->translation_.z = 0.1;
|
|
overlays_[View::GEOMETRY]->attach(handles_[View::GEOMETRY][Handles::RESIZE_H]);
|
|
handles_[View::GEOMETRY][Handles::RESIZE_V] = new Handles(Handles::RESIZE_V);
|
|
handles_[View::GEOMETRY][Handles::RESIZE_V]->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
handles_[View::GEOMETRY][Handles::RESIZE_V]->translation_.z = 0.1;
|
|
overlays_[View::GEOMETRY]->attach(handles_[View::GEOMETRY][Handles::RESIZE_V]);
|
|
handles_[View::GEOMETRY][Handles::ROTATE] = new Handles(Handles::ROTATE);
|
|
handles_[View::GEOMETRY][Handles::ROTATE]->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
handles_[View::GEOMETRY][Handles::ROTATE]->translation_.z = 0.1;
|
|
overlays_[View::GEOMETRY]->attach(handles_[View::GEOMETRY][Handles::ROTATE]);
|
|
handles_[View::GEOMETRY][Handles::SCALE] = new Handles(Handles::SCALE);
|
|
handles_[View::GEOMETRY][Handles::SCALE]->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
handles_[View::GEOMETRY][Handles::SCALE]->translation_.z = 0.1;
|
|
overlays_[View::GEOMETRY]->attach(handles_[View::GEOMETRY][Handles::SCALE]);
|
|
handles_[View::GEOMETRY][Handles::MENU] = new Handles(Handles::MENU);
|
|
handles_[View::GEOMETRY][Handles::MENU]->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
handles_[View::GEOMETRY][Handles::MENU]->translation_.z = 0.1;
|
|
overlays_[View::GEOMETRY]->attach(handles_[View::GEOMETRY][Handles::MENU]);
|
|
handles_[View::GEOMETRY][Handles::CROP] = new Handles(Handles::CROP);
|
|
handles_[View::GEOMETRY][Handles::CROP]->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
handles_[View::GEOMETRY][Handles::CROP]->translation_.z = 0.1;
|
|
overlays_[View::GEOMETRY]->attach(handles_[View::GEOMETRY][Handles::CROP]);
|
|
|
|
frame = new Frame(Frame::SHARP, Frame::THIN, Frame::NONE);
|
|
frame->translation_.z = 0.1;
|
|
frame->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 0.7f);
|
|
overlays_[View::GEOMETRY]->attach(frame);
|
|
groups_[View::GEOMETRY]->attach(overlays_[View::GEOMETRY]);
|
|
|
|
// default layer nodes
|
|
groups_[View::LAYER]->translation_.z = -1.f;
|
|
|
|
frames_[View::LAYER] = new Switch;
|
|
frame = new Frame(Frame::ROUND, Frame::THIN, Frame::PERSPECTIVE);
|
|
frame->translation_.z = 0.1;
|
|
frame->color = glm::vec4( COLOR_DEFAULT_SOURCE, 0.95f);
|
|
frames_[View::LAYER]->attach(frame);
|
|
frame = new Frame(Frame::ROUND, Frame::LARGE, Frame::PERSPECTIVE);
|
|
frame->translation_.z = 0.1;
|
|
frame->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f);
|
|
frames_[View::LAYER]->attach(frame);
|
|
groups_[View::LAYER]->attach(frames_[View::LAYER]);
|
|
|
|
groups_[View::LAYER]->attach(initial_0_);
|
|
groups_[View::LAYER]->attach(initial_1_);
|
|
|
|
overlays_[View::LAYER] = new Group;
|
|
overlays_[View::LAYER]->translation_.z = 0.15;
|
|
overlays_[View::LAYER]->visible_ = false;
|
|
groups_[View::LAYER]->attach(overlays_[View::LAYER]);
|
|
|
|
// default appearance node
|
|
frames_[View::TEXTURE] = new Switch;
|
|
frame = new Frame(Frame::SHARP, Frame::THIN, Frame::NONE);
|
|
frame->translation_.z = 0.1;
|
|
frame->color = glm::vec4( COLOR_APPEARANCE_SOURCE, 0.7f);
|
|
frames_[View::TEXTURE]->attach(frame);
|
|
frame = new Frame(Frame::SHARP, Frame::LARGE, Frame::NONE);
|
|
frame->translation_.z = 0.1;
|
|
frame->color = glm::vec4( COLOR_APPEARANCE_SOURCE, 1.f);
|
|
frames_[View::TEXTURE]->attach(frame);
|
|
groups_[View::TEXTURE]->attach(frames_[View::TEXTURE]);
|
|
|
|
overlays_[View::TEXTURE] = new Group;
|
|
overlays_[View::TEXTURE]->translation_.z = 0.1;
|
|
overlays_[View::TEXTURE]->visible_ = false;
|
|
handles_[View::TEXTURE][Handles::RESIZE] = new Handles(Handles::RESIZE);
|
|
handles_[View::TEXTURE][Handles::RESIZE]->color = glm::vec4( COLOR_APPEARANCE_SOURCE, 1.f);
|
|
handles_[View::TEXTURE][Handles::RESIZE]->translation_.z = 0.1;
|
|
overlays_[View::TEXTURE]->attach(handles_[View::TEXTURE][Handles::RESIZE]);
|
|
handles_[View::TEXTURE][Handles::RESIZE_H] = new Handles(Handles::RESIZE_H);
|
|
handles_[View::TEXTURE][Handles::RESIZE_H]->color = glm::vec4( COLOR_APPEARANCE_SOURCE, 1.f);
|
|
handles_[View::TEXTURE][Handles::RESIZE_H]->translation_.z = 0.1;
|
|
overlays_[View::TEXTURE]->attach(handles_[View::TEXTURE][Handles::RESIZE_H]);
|
|
handles_[View::TEXTURE][Handles::RESIZE_V] = new Handles(Handles::RESIZE_V);
|
|
handles_[View::TEXTURE][Handles::RESIZE_V]->color = glm::vec4( COLOR_APPEARANCE_SOURCE, 1.f);
|
|
handles_[View::TEXTURE][Handles::RESIZE_V]->translation_.z = 0.1;
|
|
overlays_[View::TEXTURE]->attach(handles_[View::TEXTURE][Handles::RESIZE_V]);
|
|
handles_[View::TEXTURE][Handles::ROTATE] = new Handles(Handles::ROTATE);
|
|
handles_[View::TEXTURE][Handles::ROTATE]->color = glm::vec4( COLOR_APPEARANCE_SOURCE, 1.f);
|
|
handles_[View::TEXTURE][Handles::ROTATE]->translation_.z = 0.1;
|
|
overlays_[View::TEXTURE]->attach(handles_[View::TEXTURE][Handles::ROTATE]);
|
|
handles_[View::TEXTURE][Handles::SCALE] = new Handles(Handles::SCALE);
|
|
handles_[View::TEXTURE][Handles::SCALE]->color = glm::vec4( COLOR_APPEARANCE_SOURCE, 1.f);
|
|
handles_[View::TEXTURE][Handles::SCALE]->translation_.z = 0.1;
|
|
overlays_[View::TEXTURE]->attach(handles_[View::TEXTURE][Handles::SCALE]);
|
|
handles_[View::TEXTURE][Handles::MENU] = new Handles(Handles::MENU);
|
|
handles_[View::TEXTURE][Handles::MENU]->color = glm::vec4( COLOR_APPEARANCE_SOURCE, 1.f);
|
|
handles_[View::TEXTURE][Handles::MENU]->translation_.z = 0.1;
|
|
overlays_[View::TEXTURE]->attach(handles_[View::TEXTURE][Handles::MENU]);
|
|
groups_[View::TEXTURE]->attach(overlays_[View::TEXTURE]);
|
|
|
|
// locker switch button : locked / unlocked icons
|
|
locker_ = new Switch;
|
|
lock_ = new Handles(Handles::LOCKED);
|
|
lock_->color.a = 0.6;
|
|
locker_->attach(lock_);
|
|
unlock_ = new Handles(Handles::UNLOCKED);
|
|
unlock_->color.a = 0.6;
|
|
locker_->attach(unlock_);
|
|
|
|
// simple image shader (with texturing) for blending
|
|
blendingshader_ = new ImageShader;
|
|
// mask produced by dedicated shader
|
|
maskshader_ = new MaskShader;
|
|
masksurface_ = new Surface(maskshader_);
|
|
|
|
// for drawing in mixing view
|
|
mixingshader_ = new ImageShader;
|
|
mixingshader_->stipple = 1.0f;
|
|
mixinggroup_ = nullptr;
|
|
|
|
// create media surface:
|
|
// - textured with original texture from media player
|
|
// - crop & repeat UV can be managed here
|
|
// - additional custom shader can be associated
|
|
texturesurface_ = new Surface(renderingshader_);
|
|
|
|
// will be created at init
|
|
renderbuffer_ = nullptr;
|
|
rendersurface_ = nullptr;
|
|
mixingsurface_ = nullptr;
|
|
activesurface_ = nullptr;
|
|
maskbuffer_ = nullptr;
|
|
maskimage_ = nullptr;
|
|
mask_need_update_ = false;
|
|
}
|
|
|
|
|
|
Source::~Source()
|
|
{
|
|
// inform links that they lost their target
|
|
while ( !links_.empty() )
|
|
links_.front()->disconnect();
|
|
|
|
// inform clones that they lost their origin
|
|
for (auto it = clones_.begin(); it != clones_.end(); ++it)
|
|
(*it)->detach();
|
|
clones_.clear();
|
|
|
|
// delete objects
|
|
if (renderbuffer_)
|
|
delete renderbuffer_;
|
|
if (maskbuffer_)
|
|
delete maskbuffer_;
|
|
if (maskimage_)
|
|
delete maskimage_;
|
|
if (masksurface_)
|
|
delete masksurface_; // deletes maskshader_
|
|
|
|
delete texturesurface_;
|
|
|
|
overlays_.clear();
|
|
frames_.clear();
|
|
handles_.clear();
|
|
|
|
// clear and delete callbacks
|
|
access_callbacks_.lock();
|
|
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); ) {
|
|
SourceCallback *callback = *iter;
|
|
iter = update_callbacks_.erase(iter);
|
|
delete callback;
|
|
}
|
|
access_callbacks_.unlock();
|
|
|
|
// clear and delete key_callbacks
|
|
for (auto iter = input_callbacks_.begin(); iter != input_callbacks_.end();
|
|
iter = input_callbacks_.erase(iter)) {
|
|
if ( iter->second.model_ != nullptr)
|
|
delete iter->second.model_;
|
|
if ( iter->second.reverse_ != nullptr)
|
|
delete iter->second.reverse_;
|
|
}
|
|
|
|
}
|
|
|
|
void Source::setName (const std::string &name)
|
|
{
|
|
if (!name.empty())
|
|
name_ = BaseToolkit::unspace( BaseToolkit::transliterate(name) );
|
|
|
|
initials_[0] = std::toupper( name_.front(), std::locale("C") );
|
|
initials_[1] = std::toupper( name_.back(), std::locale("C") );
|
|
|
|
initial_0_->setChar(initials_[0]);
|
|
initial_1_->setChar(initials_[1]);
|
|
}
|
|
|
|
void Source::accept(Visitor& v)
|
|
{
|
|
v.visit(*this);
|
|
}
|
|
|
|
Source::Mode Source::mode() const
|
|
{
|
|
return mode_;
|
|
}
|
|
|
|
void Source::setMode(Source::Mode m)
|
|
{
|
|
// make visible on first time
|
|
if ( mode_ == Source::UNINITIALIZED ) {
|
|
for (auto g = groups_.begin(); g != groups_.end(); ++g)
|
|
(*g).second->visible_ = true;
|
|
}
|
|
|
|
// choose frame 0 if visible, 1 if selected
|
|
uint index_frame = m == Source::VISIBLE ? 0 : 1;
|
|
for (auto f = frames_.begin(); f != frames_.end(); ++f)
|
|
(*f).second->setActive(index_frame);
|
|
|
|
// show overlay if current
|
|
bool current = m >= Source::CURRENT;
|
|
for (auto o = overlays_.begin(); o != overlays_.end(); ++o)
|
|
(*o).second->visible_ = (current && !locked_);
|
|
|
|
// the opacity of the initials changes if current
|
|
initial_0_->color.w = current ? 1.0 : 0.7;
|
|
initial_1_->color.w = current ? 1.0 : 0.7;
|
|
|
|
// the lock icon
|
|
locker_->setActive( locked_ ? 0 : 1);
|
|
|
|
// the mixing group overlay
|
|
overlay_mixinggroup_->visible_ = mixinggroup_!= nullptr && !locked_;
|
|
overlay_mixinggroup_->setActive(current);
|
|
|
|
// show in appearance view if current
|
|
groups_[View::TEXTURE]->visible_ = m > Source::VISIBLE;
|
|
|
|
mode_ = m;
|
|
}
|
|
|
|
|
|
void Source::setImageProcessingEnabled (bool on)
|
|
{
|
|
// avoid repeating
|
|
if ( on == imageProcessingEnabled() )
|
|
return;
|
|
|
|
// set pointer
|
|
if (on) {
|
|
// set the current rendering shader to be the
|
|
// (previously prepared) processing shader
|
|
renderingshader_ = static_cast<Shader *>(processingshader_);
|
|
}
|
|
else {
|
|
// clone the current Image processing shader
|
|
// (because the one currently attached to the source
|
|
// will be deleted in replaceRenderngShader().)
|
|
ImageProcessingShader *tmp = new ImageProcessingShader(*processingshader_);
|
|
// loose reference to current processing shader (to delete)
|
|
// and keep reference to the newly created one
|
|
// and keep it for later
|
|
processingshader_ = tmp;
|
|
// set the current rendering shader to a simple one
|
|
renderingshader_ = static_cast<Shader *>(new ImageShader);
|
|
}
|
|
|
|
// apply to nodes in subclasses
|
|
// this calls replaceShader() on the Primitive and
|
|
// will delete the previously attached shader
|
|
texturesurface_->replaceShader(renderingshader_);
|
|
}
|
|
|
|
void Source::setTextureMirrored (bool on)
|
|
{
|
|
texturesurface_->setMirrorTexture(on);
|
|
}
|
|
|
|
bool Source::textureMirrored ()
|
|
{
|
|
return texturesurface_->mirrorTexture();
|
|
}
|
|
|
|
bool Source::imageProcessingEnabled()
|
|
{
|
|
return ( renderingshader_ == processingshader_ );
|
|
}
|
|
|
|
void Source::render()
|
|
{
|
|
if ( renderbuffer_ == nullptr )
|
|
init();
|
|
else {
|
|
// render the view into frame buffer
|
|
renderbuffer_->begin();
|
|
texturesurface_->draw(glm::identity<glm::mat4>(), renderbuffer_->projection());
|
|
renderbuffer_->end();
|
|
ready_ = true;
|
|
}
|
|
}
|
|
|
|
void Source::attach(FrameBuffer *renderbuffer)
|
|
{
|
|
// invalid argument
|
|
if (renderbuffer == nullptr)
|
|
return;
|
|
|
|
// replace renderbuffer_
|
|
if (renderbuffer_)
|
|
delete renderbuffer_;
|
|
renderbuffer_ = renderbuffer;
|
|
|
|
// create the surfaces to draw the frame buffer in the views
|
|
rendersurface_ = new FrameBufferSurface(renderbuffer_, blendingshader_);
|
|
groups_[View::RENDERING]->attach(rendersurface_);
|
|
groups_[View::GEOMETRY]->attach(rendersurface_);
|
|
|
|
// for mixing and layer views, add another surface to overlay
|
|
// (stippled view on top with transparency)
|
|
mixingsurface_ = new FrameBufferSurface(renderbuffer_, mixingshader_);
|
|
groups_[View::MIXING]->attach(mixingsurface_);
|
|
groups_[View::LAYER]->attach(mixingsurface_);
|
|
|
|
// for views showing a scaled mixing surface, a dedicated transparent surface allows grabbing
|
|
activesurface_ = new Surface();
|
|
activesurface_->setTextureIndex(Resource::getTextureTransparent());
|
|
groups_[View::TEXTURE]->attach(activesurface_);
|
|
groups_[View::MIXING]->attach(activesurface_);
|
|
groups_[View::LAYER]->attach(activesurface_);
|
|
|
|
// Transition group node is optionnal
|
|
if (groups_[View::TRANSITION]->numChildren() > 0)
|
|
groups_[View::TRANSITION]->attach(mixingsurface_);
|
|
|
|
// if a symbol is available, add it to overlay
|
|
if (symbol_) {
|
|
overlays_[View::MIXING]->attach( symbol_ );
|
|
overlays_[View::LAYER]->attach( symbol_ );
|
|
// hack to place the symbols in the corner independently of aspect ratio
|
|
symbol_->translation_.x += 0.1f * (renderbuffer_->aspectRatio()-1.f);
|
|
}
|
|
|
|
// hack to place the initials in the corner independently of aspect ratio
|
|
initial_0_->translation_.x -= renderbuffer_->aspectRatio();
|
|
initial_1_->translation_.x -= renderbuffer_->aspectRatio();
|
|
|
|
// add lock icon to views (displayed on front)
|
|
groups_[View::LAYER]->attach( locker_ );
|
|
groups_[View::MIXING]->attach( locker_ );
|
|
groups_[View::GEOMETRY]->attach( locker_ );
|
|
groups_[View::TEXTURE]->attach( locker_ );
|
|
|
|
// scale all icon nodes to match aspect ratio
|
|
for (int v = View::MIXING; v < View::INVALID; v++) {
|
|
NodeSet::iterator node;
|
|
for (node = groups_[(View::Mode) v]->begin();
|
|
node != groups_[(View::Mode) v]->end(); ++node) {
|
|
(*node)->scale_.x = renderbuffer_->aspectRatio();
|
|
}
|
|
}
|
|
|
|
// (re) create the masking buffer
|
|
if (maskbuffer_)
|
|
delete maskbuffer_;
|
|
maskbuffer_ = new FrameBuffer( glm::vec3(0.5) * renderbuffer->resolution() );
|
|
|
|
// make the source visible
|
|
if ( mode_ == UNINITIALIZED )
|
|
setMode(VISIBLE);
|
|
|
|
// request update
|
|
need_update_ = true;
|
|
}
|
|
|
|
|
|
void Source::setActive (bool on)
|
|
{
|
|
// request update
|
|
need_update_ |= active_ != on;
|
|
|
|
// activate
|
|
active_ = on;
|
|
|
|
// do not disactivate if a clone depends on it
|
|
for(auto clone = clones_.begin(); clone != clones_.end(); ++clone) {
|
|
if ( (*clone)->active() )
|
|
active_ = true;
|
|
}
|
|
|
|
// an inactive source is visible only in the MIXING view
|
|
groups_[View::RENDERING]->visible_ = active_;
|
|
groups_[View::GEOMETRY]->visible_ = active_;
|
|
groups_[View::LAYER]->visible_ = active_;
|
|
}
|
|
|
|
void Source::setActive (float threshold)
|
|
{
|
|
setActive( glm::length( glm::vec2(groups_[View::MIXING]->translation_) ) < threshold );
|
|
}
|
|
|
|
void Source::setLocked (bool on)
|
|
{
|
|
locked_ = on;
|
|
|
|
setMode(mode_);
|
|
}
|
|
|
|
// Transfer functions from coordinates to alpha (1 - transparency)
|
|
|
|
//// linear distance
|
|
//float linear_(float x, float y) {
|
|
// return 1.f - CLAMP( sqrt( ( x * x ) + ( y * y ) ), 0.f, 1.f );
|
|
//}
|
|
|
|
//// quadratic distance
|
|
//float quad_(float x, float y) {
|
|
// return 1.f - CLAMP( ( x * x ) + ( y * y ), 0.f, 1.f );
|
|
//}
|
|
|
|
//// best alpha transfer function: quadratic sinusoidal shape
|
|
//float sin_quad_(float x, float y) {
|
|
// float D = sqrt( ( x * x ) + ( y * y ) );
|
|
// return 0.5f + 0.5f * cos( M_PI * CLAMP( D * sqrt(D), 0.f, 1.f ) );
|
|
//// return 0.5f + 0.5f * cos( M_PI * CLAMP( ( ( x * x ) + ( y * y ) ), 0.f, 1.f ) );
|
|
//}
|
|
|
|
float SourceCore::alphaFromCordinates(float x, float y)
|
|
{
|
|
float D = sqrt( ( x * x ) + ( y * y ) );
|
|
return 0.5f + 0.5f * cos( M_PI * CLAMP( D * sqrt(D), 0.f, 1.f ) );
|
|
}
|
|
|
|
|
|
float Source::depth() const
|
|
{
|
|
return group(View::RENDERING)->translation_.z;
|
|
}
|
|
|
|
|
|
float Source::alpha() const
|
|
{
|
|
return blendingShader()->color.a;
|
|
}
|
|
|
|
void Source::call(SourceCallback *callback, bool override)
|
|
{
|
|
if (callback != nullptr) {
|
|
|
|
// lock access to callbacks list
|
|
access_callbacks_.lock();
|
|
|
|
bool add = true;
|
|
|
|
// look for callbacks of same type
|
|
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
|
|
{
|
|
// there can be only one callback of same type in a source
|
|
SourceCallback *c = *iter;
|
|
if (callback->type() == c->type() ) {
|
|
if (override) {
|
|
// either remove and delete all callbacks of same type if override
|
|
iter = update_callbacks_.erase(iter);
|
|
delete c;
|
|
}
|
|
else {
|
|
// or cancel adding callbacks of same type if not override
|
|
add = false;
|
|
break;
|
|
}
|
|
}
|
|
// iterate over other types
|
|
else
|
|
++iter;
|
|
}
|
|
|
|
// we can add the callback as there is none of that type or we override
|
|
if (add) {
|
|
// add callback to callbacks list
|
|
update_callbacks_.push_back(callback);
|
|
}
|
|
// or delete it if couln't be added (not override)
|
|
else
|
|
delete callback;
|
|
|
|
// release access to callbacks list
|
|
access_callbacks_.unlock();
|
|
}
|
|
|
|
}
|
|
|
|
void Source::addInputCallback(uint input, SourceCallback *callback)
|
|
{
|
|
std::multimap<uint, InputCallback>::iterator added = input_callbacks_.emplace(input, InputCallback() );
|
|
added->second.model_ = callback;
|
|
}
|
|
|
|
void Source::removeInputCallback(SourceCallback *callback)
|
|
{
|
|
for (auto k = input_callbacks_.begin(); k != input_callbacks_.end(); ++k)
|
|
{
|
|
if ( k->second.model_ == callback) {
|
|
delete callback;
|
|
input_callbacks_.erase(k);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::list<SourceCallback *> Source::inputCallbacks(uint input)
|
|
{
|
|
std::list<SourceCallback *> ret;
|
|
|
|
if ( input_callbacks_.count(input) > 0 ) {
|
|
auto result = input_callbacks_.equal_range(input);
|
|
for (auto it = result.first; it != result.second; ++it)
|
|
ret.push_back( it->second.model_ );
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
std::list<uint> Source::callbackInputs()
|
|
{
|
|
std::list<uint> inputs;
|
|
|
|
// fill with list of keys
|
|
for(const auto& [key, value] : input_callbacks_) {
|
|
inputs.push_back(key);
|
|
}
|
|
|
|
// remove duplicates
|
|
inputs.unique();
|
|
|
|
return inputs;
|
|
}
|
|
|
|
void Source::updateCallbacks(float dt)
|
|
{
|
|
// add callbacks from listener
|
|
for (auto k = input_callbacks_.begin(); k != input_callbacks_.end(); ++k)
|
|
{
|
|
// if the Reaction is valid
|
|
if ( k->second.model_ != nullptr) {
|
|
|
|
bool activate = Control::inputActive(k->first);
|
|
|
|
// if the value referenced as pressed changed state
|
|
// or repeat key if there is no reverse callback
|
|
if ( activate != k->second.active_ || k->second.reverse_ == nullptr) {
|
|
|
|
// ON PRESS
|
|
if (activate) {
|
|
// delete the reverse if was not released
|
|
if (k->second.reverse_ != nullptr)
|
|
delete k->second.reverse_;
|
|
// generate a new callback from the model
|
|
SourceCallback *C = k->second.model_->clone();
|
|
// add callback to the source (force override)
|
|
call( C, true );
|
|
// get the reverse if the callback, and remember it (can be null)
|
|
k->second.reverse_ = C->reverse(this);
|
|
}
|
|
// ON RELEASE
|
|
else {
|
|
// call the reverse (NB: invalid value tested in call)
|
|
call( k->second.reverse_, true );
|
|
// do not keep reference to reverse: will be deleted when terminated
|
|
k->second.reverse_ = nullptr;
|
|
}
|
|
|
|
k->second.active_ = activate;
|
|
}
|
|
}
|
|
}
|
|
|
|
// lock access to callbacks list
|
|
access_callbacks_.lock();
|
|
// call callback functions
|
|
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
|
|
{
|
|
SourceCallback *callback = *iter;
|
|
// call update for active callbacks
|
|
if (callback->active()) {
|
|
callback->update(this, dt);
|
|
need_update_ = true;
|
|
}
|
|
|
|
// remove and delete finished callbacks
|
|
if (callback->finished()) {
|
|
iter = update_callbacks_.erase(iter);
|
|
delete callback;
|
|
}
|
|
// iterate
|
|
else
|
|
++iter;
|
|
}
|
|
// release access to callbacks list
|
|
access_callbacks_.unlock();
|
|
}
|
|
|
|
CloneSource *Source::clone(uint64_t id)
|
|
{
|
|
CloneSource *s = new CloneSource(this, id);
|
|
|
|
clones_.push_back(s);
|
|
|
|
return s;
|
|
}
|
|
|
|
void Source::update(float dt)
|
|
{
|
|
// keep delta-t
|
|
dt_ = dt;
|
|
|
|
// if update is possible
|
|
if (renderbuffer_ && mixingsurface_ && maskbuffer_)
|
|
{
|
|
// react to input and call active callbacks
|
|
updateCallbacks(dt);
|
|
|
|
// update nodes if needed
|
|
if (need_update_)
|
|
{
|
|
// ADJUST alpha based on MIXING node
|
|
// read position of the mixing node and interpret this as transparency of render output
|
|
glm::vec2 dist = glm::vec2(groups_[View::MIXING]->translation_);
|
|
// use the sinusoidal transfer function
|
|
blendingshader_->color = glm::vec4(1.f, 1.f, 1.f, SourceCore::alphaFromCordinates( dist.x, dist.y ));
|
|
mixingshader_->color = blendingshader_->color;
|
|
// adjust scale of mixing icon : smaller if not active
|
|
groups_[View::MIXING]->scale_ = glm::vec3(MIXING_ICON_SCALE) - ( active_ ? glm::vec3(0.f, 0.f, 0.f) : glm::vec3(0.03f, 0.03f, 0.f) );
|
|
|
|
// MODIFY geometry based on GEOMETRY node
|
|
groups_[View::RENDERING]->translation_ = groups_[View::GEOMETRY]->translation_;
|
|
groups_[View::RENDERING]->rotation_ = groups_[View::GEOMETRY]->rotation_;
|
|
glm::vec3 s = groups_[View::GEOMETRY]->scale_;
|
|
// avoid any null scale
|
|
s.x = CLAMP_SCALE(s.x);
|
|
s.y = CLAMP_SCALE(s.y);
|
|
s.z = 1.f;
|
|
groups_[View::GEOMETRY]->scale_ = s;
|
|
groups_[View::RENDERING]->scale_ = s;
|
|
|
|
// MODIFY CROP projection based on GEOMETRY crop
|
|
renderbuffer_->setProjectionArea( glm::vec2(groups_[View::GEOMETRY]->crop_) );
|
|
|
|
// Mixing and layer icons scaled based on GEOMETRY crop
|
|
mixingsurface_->scale_ = groups_[View::GEOMETRY]->crop_;
|
|
mixingsurface_->scale_.x *= renderbuffer_->aspectRatio();
|
|
mixingsurface_->update(dt_);
|
|
|
|
// Layers icons are displayed in Perspective (diagonal)
|
|
groups_[View::LAYER]->translation_.x = -groups_[View::LAYER]->translation_.z;
|
|
groups_[View::LAYER]->translation_.y = groups_[View::LAYER]->translation_.x / LAYER_PERSPECTIVE;
|
|
|
|
// Update workspace based on depth, and
|
|
// adjust vertical position of icon depending on workspace
|
|
if (groups_[View::LAYER]->translation_.x < -LAYER_FOREGROUND) {
|
|
groups_[View::LAYER]->translation_.y -= 0.3f;
|
|
workspace_ = Source::FOREGROUND;
|
|
}
|
|
else if (groups_[View::LAYER]->translation_.x < -LAYER_BACKGROUND) {
|
|
groups_[View::LAYER]->translation_.y -= 0.15f;
|
|
workspace_ = Source::STAGE;
|
|
}
|
|
else
|
|
workspace_ = Source::BACKGROUND;
|
|
|
|
// MODIFY depth based on LAYER node
|
|
groups_[View::MIXING]->translation_.z = groups_[View::LAYER]->translation_.z;
|
|
groups_[View::GEOMETRY]->translation_.z = groups_[View::LAYER]->translation_.z;
|
|
groups_[View::RENDERING]->translation_.z = groups_[View::LAYER]->translation_.z;
|
|
|
|
// MODIFY texture projection based on APPEARANCE node
|
|
// UV to node coordinates
|
|
static glm::mat4 UVtoScene = GlmToolkit::transform(glm::vec3(1.f, -1.f, 0.f),
|
|
glm::vec3(0.f, 0.f, 0.f),
|
|
glm::vec3(-2.f, 2.f, 1.f));
|
|
// Aspect Ratio correction transform : coordinates of Appearance Frame are scaled by render buffer width
|
|
glm::mat4 Ar = glm::scale(glm::identity<glm::mat4>(), glm::vec3(renderbuffer_->aspectRatio(), 1.f, 1.f) );
|
|
// Translation : same as Appearance Frame (modified by Ar)
|
|
glm::mat4 Tra = glm::translate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->translation_);
|
|
// Scaling : inverse scaling (larger UV when smaller Appearance Frame)
|
|
glm::vec2 scale = glm::vec2(groups_[View::TEXTURE]->scale_.x,groups_[View::TEXTURE]->scale_.y);
|
|
scale = glm::sign(scale) * glm::max( glm::vec2(glm::epsilon<float>()), glm::abs(scale));
|
|
glm::mat4 Sca = glm::scale(glm::identity<glm::mat4>(), glm::vec3(scale, 1.f));
|
|
// Rotation : same angle than Appearance Frame, inverted axis
|
|
glm::mat4 Rot = glm::rotate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->rotation_.z, glm::vec3(0.f, 0.f, -1.f) );
|
|
// Combine transformations (non transitive) in this order:
|
|
// 1. switch to Scene coordinate system
|
|
// 2. Apply the aspect ratio correction
|
|
// 3. Apply the translation
|
|
// 4. Apply the rotation (centered after translation)
|
|
// 5. Revert aspect ration correction
|
|
// 6. Apply the Scaling (independent of aspect ratio)
|
|
// 7. switch back to UV coordinate system
|
|
texturesurface_->shader()->iTransform = glm::inverse(UVtoScene) * glm::inverse(Sca) * glm::inverse(Ar) * Rot * Tra * Ar * UVtoScene;
|
|
|
|
// if a mask image was given to be updated
|
|
if (mask_need_update_) {
|
|
// fill the mask buffer (once)
|
|
if (maskbuffer_->fill(maskimage_) )
|
|
mask_need_update_ = false;
|
|
}
|
|
// otherwise, render the mask buffer
|
|
else
|
|
{
|
|
// draw mask in mask frame buffer
|
|
maskbuffer_->begin(false);
|
|
// loopback maskbuffer texture for painting
|
|
masksurface_->setTextureIndex(maskbuffer_->texture());
|
|
// fill surface with mask texture
|
|
masksurface_->draw(glm::identity<glm::mat4>(), maskbuffer_->projection());
|
|
maskbuffer_->end();
|
|
}
|
|
|
|
// set the rendered mask as mask for blending
|
|
blendingshader_->mask_texture = maskbuffer_->texture();
|
|
|
|
// inform mixing group
|
|
if (mixinggroup_)
|
|
mixinggroup_->setAction(MixingGroup::ACTION_UPDATE);
|
|
|
|
// do not update next frame
|
|
need_update_ = false;
|
|
}
|
|
|
|
if (processingshader_link_.connected() && imageProcessingEnabled()) {
|
|
Source *ref_source = processingshader_link_.source();
|
|
if (ref_source!=nullptr) {
|
|
if (ref_source->imageProcessingEnabled())
|
|
processingshader_->copy( *ref_source->processingShader() );
|
|
else
|
|
processingshader_link_.disconnect();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
FrameBuffer *Source::frame() const
|
|
{
|
|
if ( mode_ > Source::UNINITIALIZED && renderbuffer_)
|
|
{
|
|
return renderbuffer_;
|
|
}
|
|
else {
|
|
static FrameBuffer *black = new FrameBuffer(320,180);
|
|
return black;
|
|
}
|
|
}
|
|
|
|
bool Source::contains(Node *node) const
|
|
{
|
|
if ( node == nullptr )
|
|
return false;
|
|
|
|
hasNode tester(node);
|
|
return tester(this);
|
|
}
|
|
|
|
|
|
void Source::storeMask(FrameBufferImage *img)
|
|
{
|
|
// free the output mask storage
|
|
if (maskimage_ != nullptr) {
|
|
delete maskimage_;
|
|
maskimage_ = nullptr;
|
|
}
|
|
|
|
// if no image is provided
|
|
if (img == nullptr) {
|
|
// if ready
|
|
if (maskbuffer_!=nullptr) {
|
|
// get & store image from mask buffer
|
|
maskimage_ = maskbuffer_->image();
|
|
}
|
|
}
|
|
else
|
|
// store the given image
|
|
maskimage_ = img;
|
|
|
|
// maskimage_ can now be accessed with Source::getStoredMask
|
|
}
|
|
|
|
void Source::setMask(FrameBufferImage *img)
|
|
{
|
|
// if a valid image is given
|
|
if (img != nullptr && img->width>0 && img->height>0) {
|
|
|
|
// remember this new image as the current mask
|
|
// NB: will be freed when replaced
|
|
storeMask(img);
|
|
|
|
// ask Source::update to use it at next update for filling mask buffer
|
|
mask_need_update_ = true;
|
|
|
|
// ask to update the source
|
|
touch();
|
|
}
|
|
else
|
|
mask_need_update_ = false;
|
|
}
|
|
|
|
bool Source::hasNode::operator()(const Source* elem) const
|
|
{
|
|
if (_n && elem)
|
|
{
|
|
// quick case (most frequent and easy to answer)
|
|
if (elem->rendersurface_ == _n)
|
|
return true;
|
|
|
|
// general case: traverse tree of all Groups recursively using a SearchVisitor
|
|
SearchVisitor sv(_n);
|
|
// search in groups for all views
|
|
for (auto g = elem->groups_.begin(); g != elem->groups_.end(); ++g) {
|
|
(*g).second->accept(sv);
|
|
if (sv.found())
|
|
return true;
|
|
}
|
|
// search in overlays for all views
|
|
for (auto g = elem->overlays_.begin(); g != elem->overlays_.end(); ++g) {
|
|
(*g).second->accept(sv);
|
|
if (sv.found())
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
void Source::clearMixingGroup()
|
|
{
|
|
mixinggroup_ = nullptr;
|
|
overlay_mixinggroup_->visible_ = false;
|
|
}
|
|
|
|
|
|
|