osg implements cubic spline Cardinal curve

Table of Contents

1 Introduction

2. Preliminary knowledge

3. Two-dimensional Cardinal curve implemented by Qt

4. Use osg to implement three-dimensional Cardinal curve

4.1. Tools/Materials

4.2. Code implementation


1. Foreword

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 the three-dimensional 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 arc of the curve from smooth to straight.

2. Preliminary knowledge

For the mathematical theory of Hermite curve and Cardinal curve, please see the following blog post:

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

3. Two-dimensional Cardinal curve implemented by Qt

The following blog post is a two-dimensional Cardinal curve implemented in Qt:

Qt implements cubic spline Cardinal curve

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

4.1. Tools/Materials

The development environment is as follows:

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

4.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 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);
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 };
    osg::Geometry* _pCardinalCurveGemo{ nullptr };
};

osgCardinal.cpp

#include "osgCardinal.h"
#include"myEventHandler.h"
#include<osg/MatrixTransform>
#include<osg/PositionAttitudeTransform>
#include<osg/PolygonMode>
#include<osg/LineWidth>
#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);
    calMcMatrix(0.5);
}

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

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())
    {
        _lstInterPoint.pop_back();
        _lastPointHasPoped = true;
    }

    _lstInterPoint.emplace_back(pt);

    auto pGeometry = new osg::Geometry;
    auto pPgo = new osg::PolygonOffset();
    pPgo->setFactor(-1.0);
    pPgo->setUnits(-1.0);
    pGeometry->getOrCreateStateSet()->setAttributeAndModes(pPgo);
    auto pVertArray = new osg::Vec3Array;
    auto 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);
        auto z = pt.z()/* + 0.001*/; // Note: Add points appropriately, otherwise they will overlap with the grid, which will cause the circle to be drawn abnormally.
        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()
        * Remove the two user-controlled points at the head and tail that were pushed in, and re-press in the two points controlled by the user at the head and tail.
        *, 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);
    }
}

illustrate:

In the drawEllipse function of the above code, polygon drift is added by constructing an osg::PolygonOffset object, thus 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

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.