osg实现物体沿着控制点生成的Cardinal样条轨迹曲线运动

目录

1. 前言

2. 预备知识

3. 用osg实现三维Cardinal曲线

 3.1. 工具/ 原料

 3.2. 代码实现

4. 说明


1. 前言

       在设计矢量图案的时候,我们常常需要用到曲线来表达物体造型,单纯用鼠标轨迹绘制显然是不足的。于是我们希望能够实现这样的方法:通过设计师手工选择控制点,再通过插值得到过控制点(或在附近)的一条平滑曲线。在这样的需求下,样条曲线诞生了。简而言之,样条曲线是由多个多项式按比例系数组成的多项式函数,而比例系数是由控制点决定的。Hermite曲线、Cardinal曲线在平时的开发中,经常用于模拟运动物体的轨迹,如下:
 

以上是二维Cardinal曲线效果,如何用osg实现三维的Cardinal曲线,且物体(本例指球体)沿着Cardinal曲线运动呢?即像下面那样: 

即:

  1. 单击“拾取点”按钮,该按钮文字变为“关闭拾取点”,此时可以用鼠标在棋盘格上单击,点击的点用红色圆圈表示。
  2. 当所有的点都拾取完,单击“绘制”,可以绘制三维Cardinal曲线。
  3. 当绘制完三维Cardinal曲线后,再次用鼠标在棋盘格上单击,单击“绘制”,可以绘制新的三维Cardinal曲线。
  4. 单击“关闭拾取点”按钮,鼠标在棋盘格上单击时,无法拾取点。
  5. 调整阈值,可以更改曲线的圆滑度,使曲线从圆滑变为直线。
  6.  单击“运动”按钮,生成一个蓝色的球体,且该球体沿着Cardinal曲线运动。

2. 预备知识

      在阅读本博文之前,请阅读以下博文,否则很难看懂本博文。

3. 用osg实现三维Cardinal曲线

 3.1. 工具/ 原料

开发环境如下:

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

 3.2. 代码实现

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*>(&aa);
    auto eventType = ea.getEventType();
    switch (eventType)
    {
        case GUIEventAdapter::PUSH:
        {
            if(_bPickPoint && (GUIEventAdapter::LEFT_MOUSE_BUTTON == ea.getButton()))
            {
                osgUtil::LineSegmentIntersector::Intersections intersections;
                auto bRet = pViewer->computeIntersections(ea, intersections);
                if (!bRet) // 判断是否相交
                {
                    return false;
                }

                auto iter = intersections.begin();  // 取相交的第1个点
                auto interPointCoord = iter->getLocalIntersectPoint();
                _pOsgCardinal->drawEllipse(interPointCoord);
                
            }
        }
        break;
    } // end switch

    return false;
}


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

myEventHandler.h

#ifndef MYEVENTHANDLER_H
#define MYEVENTHANDLER_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& ea, 
                        osgGA::GUIActionAdapter& aa, 
                        osg::Object* obj, osg::NodeVisitor* nv) override;


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

#endif MYEVENTHANDLER_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:

    // 画点。以小圆表示
    void  drawEllipse(const osg::Vec3d& pt);

private:

    void motion();

    // 生成一个球
    osg::Geode* createSphere();

    void addBaseScene();

    osg::Geode* createGrid();

    void valueChanged(double dfValue);

    void startDraw();

    void pickPoint();

    void clear();

    // 计算MC矩阵
    void calMcMatrix(double s);

    // 压入头部和尾部两个点,用于计算
    void pushHeadAndTailPoint();

    // 画Cardinal曲线
    void drawCardinal();

    void drawLines(osg::Vec3Array* pVertArray);

    // 创建球动画路径
    osg::AnimationPath* createSphereAnimationPath();

    // 插入控制点到动画路径
    void insertCtrlPoint(const osg::Vec3d&currrentPoint , const osg::Vec3d& nextPoint, osg::AnimationPath* sphereAnimationPath, double time);
private:
    Ui::osgCardinalClass *ui;
    myEventHandler*_myEventHandler{nullptr};
    bool _startPickPoint{false};
    bool _lastPointHasPoped{ false }; // 最后一个点是否被弹出(删除)
    bool _hasDrawed{ false }; // 以前是否绘制过Cardinal曲线
    double _dfMcMatrix[4][4];
    list<osg::Vec3d> _lstInterPoint;
    osg::Vec3Array*_pVertArray{ nullptr };          // 用于保存绘制Cardinal曲线的点的容器
    osg::Geometry* _pCardinalCurveGemo{ nullptr }; // Cardinal曲线
    float _radius{0.2};
    osg::Geode* _pSphere{ nullptr };      // 球
    osg::AnimationPathCallback* _sphereAnimationPathCb{nullptr}; // 球动画路径回调函数
    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(&QDoubleSpinBox::valueChanged), this, &osgCardinal::valueChanged);
    connect(ui->startDrawBtn, &QAbstractButton::clicked, this, &osgCardinal::startDraw);
    connect(ui->clearBtn, &QAbstractButton::clicked, this, &osgCardinal::clear);
    connect(ui->pickPointBtn, &QAbstractButton::clicked, this, &osgCardinal::pickPoint);
    connect(ui->motionBtn, &QAbstractButton::clicked, this, &osgCardinal::motion);
    calMcMatrix(0.5);
}

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

// 生成一个球
osg::Geode* osgCardinal::createSphere()
{
    if (_lstInterPoint.size() < 2)
    {
        return nullptr;
    }

    auto it = _lstInterPoint.begin(); 
    ++it; // 越过人为插入的控制点
    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;
}

// 插入控制点到动画路径
void osgCardinal::insertCtrlPoint(const osg::Vec3d& currrentPoint, const osg::Vec3d&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);
}

// 创建球动画路径
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];     // 下一个点
        insertCtrlPoint(currrentPoint, nextPoint, sphereAnimationPath, time);
        time += 0.02;
    }

    // 最后一个点
    auto currrentPoint = (*_pVertArray)[_pVertArray->size() - 1];
    auto prevPoint = (*_pVertArray)[_pVertArray->size() - 2];
    insertCtrlPoint(prevPoint, currrentPoint, sphereAnimationPath, time);

    return sphereAnimationPath;
}

void osgCardinal::motion()
{
    // Cardinal曲线还未生成
    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]); // 鼠标按下的第1个点
    pat->removeChild(_pSphere); // 删除上次生成的球
    _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;

    // 计算MC矩阵
    calMcMatrix(s);

    drawCardinal();
}

// 画点。以小圆表示
void osgCardinal::drawEllipse(const osg::Vec3d& pt)
{
    if (!_lastPointHasPoped && _hasDrawed && !_lstInterPoint.empty())
    {
        /* 在上次绘制Cardinal曲线时,通过pushHeadAndTailPoint()
        *  压入的尾部用户控制点去掉,以便在开始绘制函数(参见startDraw)中重新压入尾部用户控制点
        * ,便于绘制本次曲线
        */
        _lstInterPoint.pop_back();
        _lastPointHasPoped = true;
    }

    _lstInterPoint.emplace_back(pt);

    auto pGeometry = new osg::Geometry;

    // 注意:为了防止Z值冲突,为几何体设置多边形偏移属性。具体参见: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);

        // 注意:适当增加点,否则和网格重合了,会导致圆形绘制不正常
        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("关闭拾取点"));
    }
    else
    {
        ui->pickPointBtn->setText(QString::fromLocal8Bit("拾取点"));
    }
}

void osgCardinal::startDraw()
{
    if (nullptr != _pCardinalCurveGemo) // 如果以前绘制过Cardinal曲线
    {
        /* 在上次绘制Cardinal曲线时,通过pushHeadAndTailPoint()
        *  压入的头部点去掉,以重新压入头部用户控制的两个点
        * ,便于绘制本次曲线
        */
        if (_lstInterPoint.size() >= 0 ) 
        {
            _lstInterPoint.pop_front();
        }
    }

    pushHeadAndTailPoint();

    drawCardinal();

    _hasDrawed = true;
}

// 压入头部和尾部两个点,用于计算
void osgCardinal::pushHeadAndTailPoint()
{
    // 随便构造两个点
    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));
}


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

    auto iter = _lstInterPoint.begin();
    ++iter; // 第1个点(基于0的索引)
    _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& p0 = *iter;
        auto& p1 = *(++iter);
        auto& p2 = *(++iter);
        auto& 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);
    }

     // 曲线可能变了,先删除上次的曲线
    _pCardinalCurveGemo->removePrimitiveSet(0);
    _pCardinalCurveGemo->setVertexArray(pVertArray);

    // 再用新点绘制新曲线
    _pCardinalCurveGemo->addPrimitiveSet(new osg::DrawArrays(GL_LINE_STRIP, 0, pVertArray->size()));
}

// 计算MC矩阵
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;  // 新的一行四边形
    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);

    // 操控器一定要加上,否则场景不会显示
    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);

    // 模拟鼠标滚轮朝向人滚动三次,以便场景离人显得更近些
    for (auto iLoop = 0; iLoop < 3; ++iLoop)
    {
        ui->osg_widget->getEventQueue()->mouseScroll(osgGA::GUIEventAdapter::SCROLL_DOWN);
    }  
}

4. 说明

       上述代码drawEllipse的函数中,通过构造osg::PolygonOffset对象,加入了多边形漂移,从而解决了Z冲突问题 ,否则绘制圆形不正常,具体参见:

如何避免osg绘制场景时因Z冲突导致绘制重影或不正常

        三维球的运动用到了osg中的动画路径和动画路径控制点,通过控制点来控制小球运动的轨迹,以保持小球始终沿着cardinal曲线运动。

        注意:要想使物体通过动画路径来进行动画,必须将物体作为子节点加入到osg::PositionAttitudeTransform或osg::MatrixTransform,然后调用它们的setUpdateCallback设置动画路径回调函数。直接在物体上调用是不会开启动画的,即如果在本例中像下面代码那样直接对球设置动画回调,则球不会运动:

_pSphere->setUpdateCallback(_sphereAnimationPathCb);

     ui->osg_widget是QtOsgView类型指针,QtOsgView.h、QtOsgView.cpp文件参见:osg嵌入到Qt窗体,实现Qt和osg混合编程 博文。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值