|
| 1 | +# Timeline And Keyframes |
| 2 | + |
| 3 | +The `Ra::GuiBase::Timeline` class provides display and management of `Ra::Core::Animation::KeyFramedValue`s. |
| 4 | + |
| 5 | +`Ra::Core::Animation::KeyFramedValue`s must be registered into the `Ra::GuiBase::Timeline`, which can be done using the |
| 6 | +`Ra::GuiBase::Timeline::registerKeyFramedValue` methods. |
| 7 | + |
| 8 | +We here consider two types of `Ra::Core::Animation::KeyFramedValue`: |
| 9 | + |
| 10 | +## Static KeyFramedValue: |
| 11 | +Those are `Ra::Core::Animation::KeyFramedValue`s that are an explicit part of a `Ra::Engine::Entity`, `Ra::Engine::Component` or |
| 12 | +`Ra::Engine::RenderObject` data, either filled upon construction or through the `Ra::GuiBase::Timeline`. |
| 13 | +Static `Ra::Core::Animation::KeyFramedValue`s must be registered in the `Ra::GuiBase::Timeline` after the object's |
| 14 | +construction. |
| 15 | +They usually are not bound to an *UpdateCallback* function since the object they |
| 16 | +belong to usually calls `Ra::Core::Animation::KeyFramedValue::at` to query the current value. |
| 17 | + |
| 18 | +Example Usage: |
| 19 | +```c++ |
| 20 | +using KeyFramedValue = Ra::Core::Animation::KeyFramedValue<Scalar>; |
| 21 | + |
| 22 | +struct MyComponentWithKeyFrame : public Ra::Engine::Component |
| 23 | +{ |
| 24 | + MyComponentWithKeyFrame( const std::string& name, Ra::Engine::Entity* entity, Scalar value = 0_ra ) |
| 25 | + : Ra::Engine::Component( name, entity ) |
| 26 | + , m_keyframes( value, 0_ra ) |
| 27 | + , m_currentValue( value ) |
| 28 | + { |
| 29 | + // use the linear interpolator instead of the default one |
| 30 | + m_keyframes.setInterpolator( Ra::Core::Animation::interpolate<Scalar> ); |
| 31 | + } |
| 32 | + |
| 33 | + void initialize() override {} |
| 34 | + void goTo( Scalar t ) { |
| 35 | + // Here we use the value at time t. |
| 36 | + m_currentValue = m_keyframes.at( t ); |
| 37 | + } |
| 38 | + void update() { |
| 39 | + // Here we can update other data that depend on the value. |
| 40 | + // Note: update is called after goTo, so we can use what was stored in m_currentValue. |
| 41 | + } |
| 42 | + |
| 43 | + KeyFramedValue m_keyframes; |
| 44 | + Scalar m_currentValue; |
| 45 | +}; |
| 46 | + |
| 47 | +struct MyTimeDependantSystem : public Ra::Engine::AbstractTimedSystem |
| 48 | +{ |
| 49 | + MyTimeDependantSystem( Ra::GuiBase::Timeline* timeline ) |
| 50 | + : Ra::Engine::AbstractTimedSystem() |
| 51 | + , m_timeline( timeline ) |
| 52 | + {} |
| 53 | + |
| 54 | + void generateTasks( Ra::Core::TaskQueue* taskQueue, const Ra::Engine::FrameInfo& ) override { |
| 55 | + // Update all Components data. |
| 56 | + for ( auto compEntry : m_components ) |
| 57 | + { |
| 58 | + auto comp = static_cast<MyComponentWithKeyFrame*>( compEntry.second ); |
| 59 | + auto func = std::bind( &MyComponentWithKeyFrame::update, comp ); |
| 60 | + auto task = new Ra::Core::FunctionTask( func, "MyUpdateTask" ); |
| 61 | + taskQueue->registerTask( task ); |
| 62 | + } |
| 63 | + } |
| 64 | + void AnimationSystem::handleAssetLoading( Ra::Engine::Entity* entity, const Ra::Core::Asset::FileData* fileData ) { |
| 65 | + // Add a Component whenever a file is loaded |
| 66 | + auto comp = new MyComponentWithKeyFrame( "MyComponentWithKeyFrame_", entity ); |
| 67 | + registerComponent( entity, comp ); |
| 68 | + if ( m_timeline ) |
| 69 | + { |
| 70 | + // on insertion we may want to get the frame value from elsewhere |
| 71 | + auto inserter = [comp]( const Scalar& t ) { |
| 72 | + // for example value(t) + 1 |
| 73 | + comp->m_keyframes.insertKeyFrame( t, comp->m_keyframes.at( t ) + 1_ra ); |
| 74 | + }; |
| 75 | + // No need for an update callback since the component owns the KeyFramedValue. |
| 76 | + m_timeline->registerKeyFramedValue( comp, "KeyFrame", &comp->m_keyframes, inserter ); |
| 77 | + } |
| 78 | + } |
| 79 | + void goTo( Scalar t ) override { |
| 80 | + // Update all Components' KeyFramedValue. |
| 81 | + for ( auto compEntry : m_components ) |
| 82 | + { |
| 83 | + compEntry.second->goTo( t ); |
| 84 | + } |
| 85 | + } |
| 86 | + void cacheFrame( const std::string& dir, uint frameID ) const override {} |
| 87 | + bool restoreFrame( const std::string& dir, uint frameID ) override {} |
| 88 | + |
| 89 | + Ra::GuiBase::Timeline* m_timeline{nullptr}; |
| 90 | +}; |
| 91 | +``` |
| 92 | +
|
| 93 | +## Dynamic KeyFramedValues: |
| 94 | +Those are `Ra::Core::Animation::KeyFramedValue`s that are not part of an `Ra::Engine::Entity`, `Ra::Engine::Component` or |
| 95 | +`Ra::Engine::RenderObject` data, but are used to keyframe some of its data. |
| 96 | +Dynamic `Ra::Core::Animation::KeyFramedValue`s must be created from and owned by the UI, and registered in the |
| 97 | +`Ra::GuiBase::Timeline`. |
| 98 | +They are usually bound to an UpdateCallback function since they have to update the |
| 99 | +object's data they are linked to. |
| 100 | +
|
| 101 | +Example Usage: |
| 102 | +```c++ |
| 103 | +/// Let's say there is a Component class defined as: |
| 104 | +struct MyComponent : public Component { |
| 105 | + MyComponent( const std::string& name, Ra::Engine::Entity* entity, Scalar value = 0_ra ) |
| 106 | + : Ra::Engine::Component( name, entity ) |
| 107 | + , m_currentValue( value ) |
| 108 | + {} |
| 109 | +
|
| 110 | + void initialize() override {} |
| 111 | + void update() { std::cout << m_currentValue << std::endl; } |
| 112 | +
|
| 113 | + Scalar m_currentValue; |
| 114 | +}; |
| 115 | +
|
| 116 | +/// We here create a plugin that will keyframe the MyComponent::m_currentValue |
| 117 | +/// attribute of selected MyComponents in the scene: |
| 118 | +
|
| 119 | +// Let's start with some simple UI: |
| 120 | +struct MyWidget : public QWidget { |
| 121 | + Q_OBJECT |
| 122 | +
|
| 123 | + MyWidget( QWigdet* parent = nulltpr ) : QWidget( parent ) { |
| 124 | + m_keyFrameValueCheckbox = new QCheckBox( "Keyframe current Value", this ); |
| 125 | + } |
| 126 | +
|
| 127 | + QCheckBox* m_keyFrameValueCheckbox; |
| 128 | +}; |
| 129 | +
|
| 130 | +// Then the plugin: |
| 131 | +struct MyPlugin : public QObject, Ra::Plugins::RadiumPluginInterface { |
| 132 | + Q_OBJECT |
| 133 | + Q_RADIUM_PLUGIN_METADATA |
| 134 | + Q_INTERFACES( Ra::Plugins::RadiumPluginInterface ) |
| 135 | +
|
| 136 | + MyPlugin() = default; |
| 137 | +
|
| 138 | + void registerPlugin( const Ra::Plugins::Context& context ) override { |
| 139 | + // get timeline |
| 140 | + m_timeline = context.m_timeline; |
| 141 | + // connect to selection manager |
| 142 | + m_selectionManager = context.m_selectionManager; |
| 143 | + if ( m_selectionManager ) |
| 144 | + { |
| 145 | + connect( m_selectionManager, &Ra::GuiBase::SelectionManager::currentChanged, this, &MyPlugin::onCurrentChanged ); |
| 146 | + } |
| 147 | + } |
| 148 | + bool doAddWidget( QString& name ) override { |
| 149 | + return true; |
| 150 | + } |
| 151 | + QWidget* getWidget() override { |
| 152 | + m_widget = new MyWidget; |
| 153 | + connect( m_widget->m_keyFrameValueCheckbox, &QCheckBox::toggled, this, &MyPlugin::keyFrameValue ); |
| 154 | + return m_widget; |
| 155 | + } |
| 156 | + bool doAddMenu() override { return false; } |
| 157 | + QMenu* getMenu() override { return nullptr; } |
| 158 | + bool doAddAction( int& nb ) override { return false; } |
| 159 | + QAction* getAction( int id ) override { return nullptr; } |
| 160 | + void onCurrentChanged( const QModelIndex& current, const QModelIndex& prev ) { |
| 161 | + // deal with selection |
| 162 | + if ( m_selectionManager->hasSelection() ) |
| 163 | + { |
| 164 | + const Ra::Engine::ItemEntry& ent = m_selectionManager->currentItem(); |
| 165 | + m_current = dynamic_cast<MyComponent*>( ent.m_component ); |
| 166 | + } |
| 167 | + else |
| 168 | + { |
| 169 | + m_current = nullptr; |
| 170 | + } |
| 171 | + m_widget->m_keyFrameValueCheckbox->setChecked( m_keyframes.find( m_current ) != m_keyframes.end() ); |
| 172 | + } |
| 173 | + void keyFrameValue( bool on ) { |
| 174 | + // deal with keyframing |
| 175 | + if ( m_current == nullptr ) { return; } |
| 176 | + if ( on ) |
| 177 | + { |
| 178 | + // keyframe m_current->m_currentValue |
| 179 | + auto keyframes = new KeyFramedValue( m_current->m_currentValue, 0, interpolate ); |
| 180 | + // on insertion we may want to get the frame value from elsewhere |
| 181 | + auto inserter = [keyframes]( const Scalar& t ) { |
| 182 | + // for example value(t) + 1 |
| 183 | + keyframes->insertKeyFrame( t, keyframes->at( t ) + 1_ra ); |
| 184 | + }; |
| 185 | + // on update we here want to modify the value since the MyComponent cannot do it by itself |
| 186 | + Scalar& value = &m_current->m_currentValue; |
| 187 | + auto updater = [keyframes, &value]( const Scalar& t ) { |
| 188 | + value = keyframes->at( t ); |
| 189 | + }; |
| 190 | + // register the new KeyFramedValue |
| 191 | + m_keyframes.push_back( keyframes ); |
| 192 | + m_timeline->registerKeyFramedValue( m_current, "KeyFrame", keyframes, inserter, updater ); |
| 193 | + } |
| 194 | + else |
| 195 | + { |
| 196 | + // release m_current->m_currentValue from the keyframe |
| 197 | + m_timeline->unregisterKeyFramedValue( m_current, "KeyFrame" ); |
| 198 | + } |
| 199 | + }; |
| 200 | +
|
| 201 | + MyWidget* m_widget{nullptr}; |
| 202 | + Ra::GuiBase::Timeline* m_timeline{nullptr}; |
| 203 | + MyComponent* m_current; |
| 204 | + std::map<MyComponent*,KeyFramedValue*> m_keyframes; |
| 205 | +}; |
| 206 | +``` |
0 commit comments