Dynamic animation (animateTo) exampleΒΆ

/**
* This example demonstrates an 'animate to' functionality using a 'dynamic' AnimationNode.
* Dynamic animation node is a variant of AnimationNode which allows to change the animation keyframes in runtime. An 'animate to' functionality
* refers to a logic which is able to smoothly animate to any given value at any point in time, even if already in middle of animation to another value.
* This examples uses only 2 keyframes animation to achieve the 'animate to' functionality, this should be sufficient even for real world
* use cases but can be arbitrarily extended to more complex setups if needed.
*/

struct SceneAndNode
{
    ramses::Scene* scene;
    ramses::Node* node;
};

/**
* Helper method which creates a simple ramses scene. For more ramses
* examples, check the ramses docs at https://bmwcarit.github.io/ramses
*/
SceneAndNode CreateSceneWithTriangle(ramses::RamsesClient& client);

int main()
{
    /**
     * Use simple class to create ramses framework objects which are not essential for this example.
     * For more info on those, please refer to the ramses docs: https://bmwcarit.github.io/ramses
     */
    SimpleRenderer renderer;

    /**
     * Create a simple Ramses scene with a triangle.
     */
    auto [scene, tri] = CreateSceneWithTriangle(*renderer.getClient());

    rlogic::LogicEngine logicEngine;

    /**
    * Create a binding object which serves as a bridge between logic nodes and animations on one end
    * and a Ramses scene on the other end.
    */
    rlogic::RamsesNodeBinding* nodeBinding = logicEngine.createRamsesNodeBinding(*tri);

    /**
     * Create data arrays which contain the time stamp data and the keyframe data points.
     * Unlike with static animations the actual keyframe values will be set and dynamically changed from control script.
     */
    rlogic::DataArray* animTimestamps = logicEngine.createDataArray(std::vector<float>{ 0.f, 1.f });
    rlogic::DataArray* animKeyframes = logicEngine.createDataArray(std::vector<rlogic::vec3f>{ {0.f, 0.f, 0.f}, {1.f, 0.f, 0.f} });

    /**
     * Create a channel for animation.
     */
    const rlogic::AnimationChannel animChannel { "translation", animTimestamps, animKeyframes, rlogic::EInterpolationType::Linear };

    /**
     * Create a 'dynamic' animation node by passing in the channel data via config
     */
    rlogic::AnimationNodeConfig animConfig;
    animConfig.addChannel(animChannel);
    /// In order to implement 'animateTo' logic with dynamic changes of keyframes we need to create AnimationNode that allows
    /// modifications of animation data
    animConfig.setExposingOfChannelDataAsProperties(true);
    rlogic::AnimationNode* animNode = logicEngine.createAnimationNode(animConfig);

    /**
    * Connect the animation channel 'translation' output with the translation property of the RamsesNodeBinding object.
    * After this, the value computed in the animation output channel will be propagated to the ramses node's rotation property.
    */
    logicEngine.link(
        *animNode->getOutputs()->getChild("translation"),
        *nodeBinding->getInputs()->getChild("translation"));

    /**
    * Create an 'animate to' control script which will control not only the animation progress but also modify animation data when needed
    */
    rlogic::LuaConfig scriptConfig;
    scriptConfig.addStandardModuleDependency(rlogic::EStandardModule::Math);
    rlogic::LuaScript* controlScript = logicEngine.createLuaScript(R"(
        function init()
            GLOBAL.startTick = nil
            GLOBAL.currentDirection = 0
        end

        function interface(IN,OUT)
            IN.ticker = Type:Int64()
            IN.animationDirection = Type:Int32()    -- represents animateTo target, -1 for left, 1 for right direction in X axis translation animation
            IN.currentAnimationValue = Type:Vec3f() -- current animation value output (see below how it is used and how to get the value)

            OUT.animProgress = Type:Float()
            OUT.keyframeFrom = Type:Vec3f()  -- keyframe value to animate from (will be linked to animation node's data)
            OUT.keyframeTo = Type:Vec3f()    -- keyframe value to animate to (will be linked to animation node's data)
        end

        function run(IN,OUT)
            -- initialize internal and some output values
            if GLOBAL.startTick == nil then
                GLOBAL.startTick = IN.ticker
                OUT.keyframeFrom = {0, 0, 0}
                OUT.keyframeTo = {0, 0, 0}
            end

            -- core 'animateTo' logic, check if animation target changed
            -- (in this simplified case represented as 'direction' of triangle movement)
            if IN.animationDirection ~= GLOBAL.currentDirection then
                -- reset ticker to now (we reset animation and start from beginning
                -- (this is just one possible solution how to handle time in an animateTo script)
                GLOBAL.startTick = IN.ticker
                -- reset source keyframe to current value, this is crucial so that the animation continues
                -- where it was to new target (otherwise it would 'jump' to initial starting keyframe)
                OUT.keyframeFrom = IN.currentAnimationValue
                -- set new animation target (in this example we just animate to left or right edge of screen
                -- which is defined by X translation axis)
                OUT.keyframeTo = {IN.animationDirection, 0, 0}

                GLOBAL.currentDirection = IN.animationDirection
            end

            local elapsedTime = IN.ticker - GLOBAL.startTick
            -- ticker from TimerNode is in microseconds, our animation duration is in seconds, conversion is needed
            elapsedTime = elapsedTime / 1000000

            local duration = 5   -- means 5 seconds for animation to reach target value, adjust to make animation slower/faster
            local animProgress = elapsedTime / duration

            -- clamp normalized progress
            OUT.animProgress = math.min(math.max(animProgress, 0), 1)
        end
    )", scriptConfig);

    /**
    * We need to provide time information to the control script, we can either provide system or custom time ticker from application
    * or we can create a TimerNode which generates system time for us. Note that its 'ticker_us' output is in microseconds, control script needs
    * to convert it to whatever units are used in the animation timestamps (in this example seconds).
    */
    rlogic::TimerNode* timer = logicEngine.createTimerNode();
    logicEngine.link(
        *timer->getOutputs()->getChild("ticker_us"),
        *controlScript->getInputs()->getChild("ticker"));

    /**
    * Whenever changing a direction of the triangle animation the script logic needs to reset several values (see script code above),
    * one of them (keyframeFrom) depends on the current (or rather last produced) value of animation output.
    * Note that the default data flow in this example logic network is control script -> animation node -> binding/meshnode. In order to get
    * the last animation output we need to create a link which goes against this data flow and creates a loop in logic network dependency graph, this is not allowed
    * unless we use a 'weak' link, which this example is a perfect use case for. See rlogic::LogicEngine::linkWeak for more details and limitations.
    */
    logicEngine.linkWeak(
        *animNode->getOutputs()->getChild("translation"),
        *controlScript->getInputs()->getChild("currentAnimationValue"));

    /**
    * Link animation data from control script to animation node so that control script can dynamically change keyframes of the animation
    **/
    logicEngine.link(
        *controlScript->getOutputs()->getChild("keyframeFrom"),
        *animNode->getInputs()->getChild("channelsData")->getChild("translation")->getChild("keyframes")->getChild(0u)); // value of first keyframe (animate from)
    logicEngine.link(
        *controlScript->getOutputs()->getChild("keyframeTo"),
        *animNode->getInputs()->getChild("channelsData")->getChild("translation")->getChild("keyframes")->getChild(1u)); // value of second keyframe (animate to)

    /**
    * And finally, link control script to animation node to control its progress
    **/
    logicEngine.link(
        *controlScript->getOutputs()->getChild("animProgress"),
        *animNode->getInputs()->getChild("progress"));

    /**
    * Show the scene on the renderer
    */
    renderer.showScene(scene->getSceneId());

    /**
     * Simulate an application loop.
     */
    int nextChangeLoop = 0;
    int numChanges = 0;
    int loop = 0;
    while (!renderer.isWindowClosed())
    {
        // change direction every now and then
        if (loop == nextChangeLoop)
        {
            int32_t currentDirection = *controlScript->getInputs()->getChild("animationDirection")->get<int32_t>();
            if (currentDirection == 0)
                currentDirection = -1;
            controlScript->getInputs()->getChild("animationDirection")->set(-currentDirection);

            nextChangeLoop = loop + std::vector<int>{ 150, 100, 80, 120, 50 }[numChanges % 5];
            numChanges++;
        }

        /**
        * Update the LogicEngine. This will apply changes to Ramses scene from any running animation.
        */
        logicEngine.update();

        /**
        * In order to commit the changes to Ramses scene caused by animations logic we need to "flush" them.
        */
        scene->flush();

        /**
        * Process window events, check if window was closed
        */
        renderer.processEvents();

        /**
        * Throttle the simulation loop by sleeping for a bit.
        */
        std::this_thread::sleep_for(std::chrono::milliseconds(5));

        loop = (loop + 1) % 1000;
    }