osg realizes the movement of objects along the Cardinal spline trajectory curve generated by control points.

Table of Contents

1 Introduction

2. Preliminary knowledge

3. Use osg to implement three-dimensional Cardinal curve

3.1. Tools/Materials

3.2. Code implementation

4. Description


1. Preface

When designing vector patterns, we often need to use curves to express the shape of objects. Simply drawing with mouse tracks is obviously insufficient. So we hope to implement such a method: the designer manually selects control points, and then obtains a smooth curve passing through the control points (or near them) through interpolation. Under such demand, spline curves were born. In short, a spline is a polynomial function composed of multiple polynomials with proportional coefficients, and the proportional coefficients are determined by control points. Hermite curves and Cardinal curves are often used to simulate the trajectory of moving objects in daily development, as follows:

The above is the two-dimensional Cardinal curve effect. How to use osg to realize a three-dimensional Cardinal curve, and the object (in this case, a sphere) moves along the Cardinal curve? That is like below:

Right now:

  1. Click the “Pick Point” button, and the button text changes to “Close Pick Point”. At this time, you can click on the checkerboard with the mouse, and the clicked point is represented by a red circle.
  2. When all points have been picked, click “Draw” to draw a three-dimensional Cardinal curve.
  3. After drawing the three-dimensional Cardinal curve, click on the checkerboard again with the mouse and click “Draw” to draw a new three-dimensional Cardinal curve.
  4. Click the “Close Picking Points” button and points cannot be picked up when the mouse is clicked on the checkerboard.
  5. Adjusting the threshold can change the roundness of the curve from round to straight.
  6. Click the “Movement” button to generate a blue sphere that moves along the Cardinal curve.

2. Preliminary knowledge

Before reading this blog post, please read the following blog post, otherwise it will be difficult to understand this blog post.

  • [Computer animation] Path curve and moving object control (Cardinal spline).
  • Cubic parametric splines and Cardinal curves.
  • Qt implements cubic spline Cardinal curve.
  • osg implements cubic spline Cardinal curve.

3. Use osg to realize the three-dimensional Cardinal curve

3.1. Tools/Materials

The development environment is as follows:

  • Qt 5.14.1.
  • Visual Studio 2022.
  • OpenSceneGraph 3.6.2.

3.2. Code Implementation

main.cpp

#include "osgCardinal.h"
#include <QtWidgets/QApplication>
 
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    osgCardinal w;
    w.show();
    return a.exec();
}

myEventHandler.cpp

#include "myEventHandler.h"
#include<osgViewer/Viewer>
bool myEventHandler::handle(const osgGA::GUIEventAdapter & ea,
                            osgGA::GUIActionAdapter & aa,
                            osg::Object* obj, osg::NodeVisitor* nv)
{
    auto pViewer = dynamic_cast<osgViewer::Viewer*>( & amp;aa);
    auto eventType = ea.getEventType();
    switch(eventType)
    {
        case GUIEventAdapter::PUSH:
        {
            if(_bPickPoint & amp; & amp; (GUIEventAdapter::LEFT_MOUSE_BUTTON == ea.getButton()))
            {
                osgUtil::LineSegmentIntersector::Intersections intersections;
                auto bRet = pViewer->computeIntersections(ea, intersections);
                if (!bRet) // Determine whether it intersects
                {
                    return false;
                }

                auto iter = intersections.begin(); // Get the first point of intersection
                auto interPointCoord = iter->getLocalIntersectPoint();
                _pOsgCardinal->drawEllipse(interPointCoord);
                
            }
        }
        break;
    } // end switch

    return false;
}


void myEventHandler::setPickPoint(bool bPickPoint)
{
    _bPickPoint = bPickPoint;
}

myEventHandler.h

#ifndef MYEVENTHANDLER_H
#defineMYEVENTHANDLER_H
#include<osgGA/GUIEventHandler>
#include<osgCardinal.h>
using namespace osgGA;

class myEventHandler:public GUIEventHandler
{
public:
    myEventHandler(osgCardinal* p) { _pOsgCardinal = p; }
public:

    void setPickPoint(bool bPickPoint);

private:
    virtual bool handle(const osgGA::GUIEventAdapter & amp; ea,
                        osgGA::GUIActionAdapter & aa,
                        osg::Object* obj, osg::NodeVisitor* nv) override;


private:
    bool _bPickPoint{false};
    osgCardinal* _pOsgCardinal{nullptr};
};

#endifMYEVENTHANDLER_H

osgCardinal.h

#pragma once

#include <QtWidgets/QWidget>
#include "ui_osgCardinal.h"
using std::list;

QT_BEGIN_NAMESPACE
namespace Ui { class osgCardinalClass; };
QT_END_NAMESPACE

class myEventHandler;

class osgCardinal : public QWidget
{
    Q_OBJECT

public:
    osgCardinal(QWidget *parent = nullptr);
    ~osgCardinal();

public:

    // Draw dots. Represented by small circles
    void drawEllipse(const osg::Vec3d & amp; pt);

private:

    void motion();

    // generate a ball
    osg::Geode* createSphere();

    void addBaseScene();

    osg::Geode* createGrid();

    void valueChanged(double dfValue);

    void startDraw();

    void pickPoint();

    void clear();

    // Calculate MC matrix
    void calMcMatrix(double s);

    //Press in two points at the head and tail for calculation
    void pushHeadAndTailPoint();

    //Draw Cardinal curve
    void drawCardinal();

    void drawLines(osg::Vec3Array* pVertArray);

    //Create ball animation path
    osg::AnimationPath* createSphereAnimationPath();

    //Insert control points into the animation path
    void insertCtrlPoint(const osg::Vec3d & amp;currrentPoint , const osg::Vec3d & amp; nextPoint, osg::AnimationPath* sphereAnimationPath, double time);
private:
    Ui::osgCardinalClass *ui;
    myEventHandler*_myEventHandler{nullptr};
    bool _startPickPoint{false};
    bool _lastPointHasPoped{ false }; // Whether the last point is popped (deleted)
    bool _hasDrawed{ false }; // Whether the Cardinal curve has been drawn before
    double _dfMcMatrix[4][4];
    list<osg::Vec3d> _lstInterPoint;
    osg::Vec3Array*_pVertArray{ nullptr }; // Container used to save the points for drawing the Cardinal curve
    osg::Geometry* _pCardinalCurveGemo{ nullptr }; // Cardinal curve
    float _radius{0.2};
    osg::Geode* _pSphere{ nullptr }; // Sphere
    osg::AnimationPathCallback* _sphereAnimationPathCb{nullptr}; // Ball animation path callback function
    double _twoPi{ 2 * 3.1415926 };
};

osgCardinal.cpp

#include "osgCardinal.h"
#include"myEventHandler.h"
#include<osg/MatrixTransform>
#include<osg/PositionAttitudeTransform>
#include<osg/PolygonMode>
#include<osg/LineWidth>
#include<osg/ShapeDrawable>
#include<osg/PolygonOffset>
#include<vector>
using std::vector;

osgCardinal::osgCardinal(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::osgCardinalClass())
{
    ui->setupUi(this);

setWindowState(Qt::WindowMaximized);

addBaseScene();

    ui->doubleSpinBox->setMinimum(0);
    ui->doubleSpinBox->setMaximum(1);
    ui->doubleSpinBox->setValue(0.5);
    ui->doubleSpinBox->setSingleStep(0.1);

    connect(ui->doubleSpinBox, QOverload<double>::of( & amp;QDoubleSpinBox::valueChanged), this, & amp;osgCardinal::valueChanged);
    connect(ui->startDrawBtn, & amp;QAbstractButton::clicked, this, & amp;osgCardinal::startDraw);
    connect(ui->clearBtn, & amp;QAbstractButton::clicked, this, & amp;osgCardinal::clear);
    connect(ui->pickPointBtn, & amp;QAbstractButton::clicked, this, & amp;osgCardinal::pickPoint);
    connect(ui->motionBtn, & amp;QAbstractButton::clicked, this, & amp;osgCardinal::motion);
    calMcMatrix(0.5);
}

osgCardinal::~osgCardinal()
{
    delete ui;
}

// generate a ball
osg::Geode* osgCardinal::createSphere()
{
    if (_lstInterPoint.size() < 2)
    {
        return nullptr;
    }

    auto it = _lstInterPoint.begin();
     + + it; // past the manually inserted control point
    auto center = osg::Vec3(0, 0, 0)/**it*/;
    center.z() + = _radius;
    auto pSphere = new osg::Sphere(center, _radius);

    auto pSphereGeode = new osg::Geode();
    auto pTlh = new osg::TessellationHints();
    pTlh->setDetailRatio(0.5);
    auto pSphereDrawable = new osg::ShapeDrawable(pSphere, pTlh);
    pSphereDrawable->setColor(osg::Vec4(0.0, 0.0, 1.0, 1.0));
    pSphereGeode->addDrawable(pSphereDrawable);

    return pSphereGeode;
}

//Insert control points into the animation path
void osgCardinal::insertCtrlPoint(const osg::Vec3d & currrentPoint, const osg::Vec3d & amp;nextPoint, osg::AnimationPath* sphereAnimationPath, double time)
{
    auto angle = atan2f((nextPoint.y() - currrentPoint.y()), (nextPoint.x() - currrentPoint.x()));
    if (angle < 0)
    {
        angle = _twoPi + angle;
    }

    osg::Quat quat(angle, osg::Vec3(0, 0, 1));
    osg::AnimationPath::ControlPoint ctrlPoint(currrentPoint, quat);
    sphereAnimationPath->insert(time, ctrlPoint);
}

//Create ball animation path
osg::AnimationPath* osgCardinal::createSphereAnimationPath()
{
    auto sphereAnimationPath = new osg::AnimationPath();
    sphereAnimationPath->setLoopMode(osg::AnimationPath::NO_LOOPING);
    double time = 0.0;
    for (auto iPointIndex = 0; iPointIndex < _pVertArray->size() - 2; + + iPointIndex)
    {
        auto currrentPoint = (*_pVertArray)[iPointIndex];
        auto nextPoint = (*_pVertArray)[iPointIndex + 1]; // next point
        insertCtrlPoint(currrentPoint, nextPoint, sphereAnimationPath, time);
        time + = 0.02;
    }

    // last point
    auto currrentPoint = (*_pVertArray)[_pVertArray->size() - 1];
    auto prevPoint = (*_pVertArray)[_pVertArray->size() - 2];
    insertCtrlPoint(prevPoint, currrentPoint, sphereAnimationPath, time);

    return sphereAnimationPath;
}

void osgCardinal::motion()
{
    // Cardinal curve has not been generated yet
    if (nullptr == _pVertArray)
    {
        return;
    }

    auto pMatrixTransform = ui->osg_widget->getSceneData()->asGroup()->getChild(0)->asTransform()->asMatrixTransform();
  
    _pSphere = createSphere();

    auto sphereAnimationPath = createSphereAnimationPath();
    osg::ref_ptr<osg::PositionAttitudeTransform> pat = new osg::PositionAttitudeTransform();
    pat->setPosition((*_pVertArray)[0]); // The first point pressed by the mouse
    pat->removeChild(_pSphere); // Delete the last generated ball
    _sphereAnimationPathCb = new osg::AnimationPathCallback(sphereAnimationPath, 0.0);

    if (nullptr != _sphereAnimationPathCb)
    {
        _pSphere->removeUpdateCallback(_sphereAnimationPathCb);
    }

    pat->setUpdateCallback(_sphereAnimationPathCb);
    pat->addChild(_pSphere);
    pMatrixTransform->addChild(pat);
}

void osgCardinal::valueChanged(double dfValue)
{
    auto s = (1 - dfValue) / 2.0;

    // Calculate MC matrix
    calMcMatrix(s);

    drawCardinal();
}

// Draw dots. Represented by small circles
void osgCardinal::drawEllipse(const osg::Vec3d & pt)
{
    if (!_lastPointHasPoped & amp; & amp; _hasDrawed & amp; & amp; !_lstInterPoint.empty())
    {
        /* When the Cardinal curve was drawn last time, pass pushHeadAndTailPoint()
        * The pushed-in tail user control point is removed so that the tail user control point can be re-pressed in the start drawing function (see startDraw)
        *, easy to draw this curve
        */
        _lstInterPoint.pop_back();
        _lastPointHasPoped = true;
    }

    _lstInterPoint.emplace_back(pt);

    auto pGeometry = new osg::Geometry;

    // Note: To prevent Z value conflicts, set the polygon offset attribute for the geometry. For details, see: https://blog.csdn.net/danshiming/article/details/133958200?spm=1001.2014.3001.5502
    auto pPgo = new osg::PolygonOffset();
    pPgo->setFactor(-1.0);
    pPgo->setUnits(-1.0);
    pGeometry->getOrCreateStateSet()->setAttributeAndModes(pPgo);
    auto pVertArray = new osg::Vec3Array;
    _radius = 0.2;
    auto twoPi = 2 * 3.1415926;
    for (auto iAngle = 0.0; iAngle < twoPi; iAngle + = 0.001)
    {
        auto x = pt.x() + _radius * std::cosf(iAngle);
        auto y = pt.y() + _radius * std::sinf(iAngle);

        // Note: Add points appropriately, otherwise they will overlap with the grid, which will cause the circle to be drawn abnormally.
        auto z = pt.z()/* + 0.001*/;
        pVertArray->push_back(osg::Vec3d(x, y, z));
    }

    pGeometry->setVertexArray(pVertArray);
 
    auto pColorArray = new osg::Vec4Array;
    pColorArray->push_back(osg::Vec4d(1.0, 0.0, 0.0, 1.0));
    pGeometry->setColorArray(pColorArray/*, osg::Array::BIND_OVERALL*/);
    pGeometry->setColorBinding(osg::Geometry::BIND_OVERALL);

    pGeometry->addPrimitiveSet(new osg::DrawArrays(GL_POLYGON, 0, pVertArray->size()));
    auto pMatrixTransform = ui->osg_widget->getSceneData()->asGroup()->getChild(0)->asTransform()->asMatrixTransform();
    pMatrixTransform->addChild(pGeometry);
}

void osgCardinal::pickPoint()
{
    _startPickPoint = !_startPickPoint;
    _myEventHandler->setPickPoint(_startPickPoint);
    if (_startPickPoint)
    {
        ui->pickPointBtn->setText(QString::fromLocal8Bit("Close pick point"));
    }
    else
    {
        ui->pickPointBtn->setText(QString::fromLocal8Bit("pick point"));
    }
}

void osgCardinal::startDraw()
{
    if (nullptr != _pCardinalCurveGemo) // If the Cardinal curve has been drawn before
    {
        /* When the Cardinal curve was drawn last time, pass pushHeadAndTailPoint()
        * The pushed-in head points are removed to re-press the two points of the head user-controlled
        *, easy to draw this curve
        */
        if (_lstInterPoint.size() >= 0 )
        {
            _lstInterPoint.pop_front();
        }
    }

    pushHeadAndTailPoint();

    drawCardinal();

    _hasDrawed = true;
}

//Press in two points at the head and tail for calculation
void osgCardinal::pushHeadAndTailPoint()
{
    // Construct two points at will
    auto ptBegin = _lstInterPoint.begin();
    auto x = ptBegin->x() + 20;
    auto y = ptBegin->y() + 20;
    auto z = ptBegin->z();
    _lstInterPoint.insert(_lstInterPoint.begin(), osg::Vec3d(x, y, z));

    auto ptEnd = _lstInterPoint.back();
    x = ptEnd.x() + 20;
    y = ptEnd.y() + 20;
    z = ptBegin->z();
    _lstInterPoint.insert(_lstInterPoint.end(), osg::Vec3d(x, y, z));
}


//Draw Cardinal curve
void osgCardinal::drawCardinal()
{
    if (_lstInterPoint.size() < 4)
    {
        return;
    }
    
    if (nullptr == _pVertArray)
    {
        _pVertArray = new osg::Vec3Array();
    }
    else
    {
        _pVertArray->clear();
    }

    auto iter = _lstInterPoint.begin();
     + + iter; // 1st point (0-based index)
    _pVertArray->push_back(*iter);
    --iter;

    auto endIter = _lstInterPoint.end();
    int nIndex = 0;
    while(true)
    {
        --endIter;
         + + nIndex;
        if (3 == nIndex)
        {
            break;
        }
    }

    for (; iter != endIter; + + iter)
    {
        auto & amp; p0 = *iter;
        auto & amp; p1 = *( + + iter);
        auto & amp; p2 = *( + + iter);
        auto & amp; p3 = *( + + iter);

        --iter;
        --iter;
        --iter;

        vector<osg::Vec3d>vtTempPoint;
        vtTempPoint.push_back(p0);
        vtTempPoint.push_back(p1);
        vtTempPoint.push_back(p2);
        vtTempPoint.push_back(p3);

        for (auto i = 0; i < 4; + + i)
        {
           vtTempPoint[i] = p0 * _dfMcMatrix[i][0] + p1 * _dfMcMatrix[i][1] + p2 * _dfMcMatrix[i][2] + p3 * _dfMcMatrix[i][3];
        }

        float t3, t2, t1, t0;
        for (double t = 0.0; t < 1; t + = 0.01)
        {
            t3 = t * t * t; t2 = t * t; t1 = t; t0 = 1;
            osg::Vec3d newPoint;
            newPoint = vtTempPoint[0] * t3 + vtTempPoint[1] * t2 + vtTempPoint[2] * t1 + vtTempPoint[3] * t0;
            _pVertArray->push_back(newPoint);
        }
    }

    drawLines(_pVertArray);
}

void osgCardinal::drawLines(osg::Vec3Array* pVertArray)
{
    if (nullptr == _pCardinalCurveGemo)
    {
        _pCardinalCurveGemo = new osg::Geometry;

        auto pLineWidth = new osg::LineWidth(50);
        _pCardinalCurveGemo->getOrCreateStateSet()->setAttributeAndModes(pLineWidth);

        auto pColorArray = new osg::Vec4Array;
        pColorArray->push_back(osg::Vec4d(0.0, 1.0, 0.0, 1.0));
        _pCardinalCurveGemo->setColorArray(pColorArray/*, osg::Array::BIND_OVERALL*/);
        _pCardinalCurveGemo->setColorBinding(osg::Geometry::BIND_OVERALL);

        auto pMatrixTransform = ui->osg_widget->getSceneData()->asGroup()->getChild(0)->asTransform()->asMatrixTransform();
        pMatrixTransform->addChild(_pCardinalCurveGemo);
    }

     // The curve may have changed, delete the last curve first
    _pCardinalCurveGemo->removePrimitiveSet(0);
    _pCardinalCurveGemo->setVertexArray(pVertArray);

    //Draw a new curve using new points
    _pCardinalCurveGemo->addPrimitiveSet(new osg::DrawArrays(GL_LINE_STRIP, 0, pVertArray->size()));
}

// Calculate MC matrix
void osgCardinal::calMcMatrix(double s)
{
    _dfMcMatrix[0][0] = -s, _dfMcMatrix[0][1] = 2 - s, _dfMcMatrix[0][2] = s - 2, _dfMcMatrix[0][3] = s;
    _dfMcMatrix[1][0] = 2 * s, _dfMcMatrix[1][1] = s - 3, _dfMcMatrix[1][2] = 3 - 2 * s, _dfMcMatrix[1][3] = -s;
    _dfMcMatrix[2][0] = -s, _dfMcMatrix[2][1] = 0, _dfMcMatrix[2][2] = s, _dfMcMatrix[2][3] = 0;
    _dfMcMatrix[3][0] = 0, _dfMcMatrix[3][1] = 1, _dfMcMatrix[3][2] = 0, _dfMcMatrix[3][3] = 0;
}

void osgCardinal::clear()
{
    _lstInterPoint.clear();
    _hasDrawed = false;
    _lastPointHasPoped = false;
}

osg::Geode* osgCardinal::createGrid()
{
    auto pGeode = new osg::Geode;

    auto pVertArray = new osg::Vec3Array;
    for (auto y = -10; y < 10; + + y)
    {
        for (auto x = -10; x < 10; + + x)
        {
            pVertArray->push_back(osg::Vec3d(x, y, 0.0));
            pVertArray->push_back(osg::Vec3d(x + 1, y, 0.0));
            pVertArray->push_back(osg::Vec3d(x + 1, y + 1, 0.0));
            pVertArray->push_back(osg::Vec3d(x, y + 1, 0.0));
        }
    }

    auto iSize = pVertArray->size();
    osg::DrawElementsUShort* pEle{ nullptr };
    osg::Geometry* pGeomerty{ nullptr };
    osg::Vec4Array* pColorArray{ nullptr };

    auto nQuardIndex = 0;
    bool bNewLineQuard = true; // New line of quadrilateral
    for (auto iVertIndex = 0; iVertIndex < iSize; + + iVertIndex)
    {
        if (0 == (iVertIndex % 4))
        {
            pEle = new osg::DrawElementsUShort(GL_QUADS);

            pGeomerty = new osg::Geometry;
            pGeomerty->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
            pGeomerty->addPrimitiveSet(pEle);
            pGeode->addDrawable(pGeomerty);

            pGeomerty->setVertexArray(pVertArray);
            pColorArray = new osg::Vec4Array();
            if (bNewLineQuard)
            {
                pColorArray->push_back(osg::Vec4d(1.0, 1.0, 1.0, 1.0));
            }
            else
            {
                pColorArray->push_back(osg::Vec4d(0.0, 0.0, 0.0, 1.0));
            }

             + + nQuardIndex;

            if (0 != (nQuardIndex % 20))
            {
                bNewLineQuard = !bNewLineQuard;
            }

            pGeomerty->setColorArray(pColorArray, osg::Array::Binding::BIND_PER_PRIMITIVE_SET);
        }

        pEle->push_back(iVertIndex);
    } // end for

    return pGeode;
}

void osgCardinal::addBaseScene()
{
    auto pAxis = osgDB::readRefNodeFile(R"(E:\osg\OpenSceneGraph-Data\axes.osgt)");
    if (nullptr == pAxis)
    {
        OSG_WARN << "axes node is nullpr!";
        return;
    }

    auto pRoot = new osg::Group();

    pRoot->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
    auto pMatrixRoot = new osg::MatrixTransform;
    auto pGrid = createGrid();
    pMatrixRoot->addChild(pGrid);
    pMatrixRoot->addChild(pAxis);
    pRoot->addChild(pMatrixRoot);

    pMatrixRoot->setMatrix(osg::Matrix::rotate(osg::inDegrees(60.0), osg::Vec3(1, 0, 0)));

    ui->osg_widget->setSceneData(pRoot);

    // The controller must be added, otherwise the scene will not be displayed
    ui->osg_widget->setCameraManipulator(new osgGA::TrackballManipulator);
    ui->osg_widget->addEventHandler(new osgViewer::WindowSizeHandler);
    ui->osg_widget->addEventHandler(new osgViewer::StatsHandler);
    _myEventHandler = new myEventHandler(this);
    ui->osg_widget->addEventHandler(_myEventHandler);

    //Simulate the mouse wheel to roll towards the person three times so that the scene appears closer to the person
    for (auto iLoop = 0; iLoop < 3; + + iLoop)
    {
        ui->osg_widget->getEventQueue()->mouseScroll(osgGA::GUIEventAdapter::SCROLL_DOWN);
    }
}

4. Description

In the drawEllipse function of the above code, polygon drift is added by constructing an osg::PolygonOffset object, thereby solving the Z conflict problem. Otherwise, drawing a circle will not be normal. For details, see:

How to avoid ghosting or abnormal drawing due to Z conflict when osg draws a scene

The movement of the three-dimensional ball uses the animation path and animation path control points in osg. The control points are used to control the trajectory of the ball to keep the ball always moving along the cardinal curve.

Note: To animate an object through an animation path, you must add the object as a child node to osg::PositionAttitudeTransform or osg::MatrixTransform, and then call their setUpdateCallback to set the animation path callback function. Calling it directly on the object will not turn on the animation. That is, if you set an animation callback directly on the ball like the following code in this example, the ball will not move:

_pSphere->setUpdateCallback(_sphereAnimationPathCb);

ui->osg_widget is a QtOsgView type pointer. For QtOsgView.h and QtOsgView.cpp files, see: Embedding osg into Qt forms to realize Qt and osg mixed programming blog post.