若该文为原创文章,未经允许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/97109615
目录
前言
OSG拾取动作概念:pick
鼠标拖动球体旋转的实现原理
光照亮度
代码
初始化模型代码
事件管理器代码
入坑
入坑一:场景中心结点随物体缩放变换了
入坑二:获取几何图形的半径不对
入坑三:屏幕坐标到世界坐标不正确
入坑四:鼠标拖动旋转错位
工程模板:对应版本号1.15.0
《OSG开发笔记(一):OSG介绍、编译》
《OSG开发笔记(二):OSG帮助文档编译》
《OSG开发笔记(三):OSG使用osgQt嵌入Qt应用程序》
《OSG开发笔记(四):OSG不使用osgQt重写类嵌入Qt应用程序》:
《OSG开发笔记(五):OSG场景理解与基础类概述》
《OSG开发笔记(六):OSG内存管理》
《OSG开发笔记(七):OSG复现OpenGL入门示例和OSG坐标系》
《OSG开发笔记(八):OSG模型文件存储与读取》
《OSG开发笔记(九):OSG模型的基本操作之添加/删除、显示/隐藏、开关节点开/关》:
《OSG开发笔记(十):OSG模型的变换之平移、旋转和缩放》
《OSG开发笔记(十一):OSG渲染状态与2D纹理映射》
《OSG开发笔记(十二):OSG基本几何图形、内置几何类型》
《OSG开发笔记(十三):OSG三维纹理映射(体渲染)》
《OSG开发笔记(十四):OSG交互》
《OSG开发笔记(十五):OSG光照》
《OSG开发笔记(十六):OSG视口、相机和视点》
《OSG开发笔记(十七):OSG中的相机移动》
《OSG开发笔记(十八):OSG鼠标拾取pick、拽托球体以及多光源》
《OSG开发笔记(十九):OSG文字显示》
《OSG开发笔记(二十):OSG使用HUD显示文字》
《OSG开发笔记(二十一):OSG使用HUD绘制图形以及纹理混合模式》
《OSG开发笔记(二十二):OSG场景背景》
《OSG开发笔记(二十三):Qt使用QOpenGLWidget渲染OSG和地球仪》
《OSG开发笔记(二十四):OSG漫游之平移、转向和低抬头》
《OSG开发笔记(二十五):OSG漫游之CS移动、碰撞检测与跳跃》
《OSG开发笔记(二十六):OSG漫游之上下楼梯》
《OSG开发笔记(二十七):OSG路径漫游之录制播放固定路径动画》
《OSG开发笔记(二十八):OSG模型固定路径动画》
持续补充中…
地球的变换都实现了,还差一个鼠标点击地球拖动旋转,交互之前已经实现,但如何确定点击的是球体,然后拖动旋转球体,是本篇章的内容。
Pick主要是通过鼠标的点击来拾取一些问题,或者判断鼠标所点的位置在哪里。在OSG中pick首先肯定需要相应鼠标单击时间,pick本身并不是一个OSG的库函数。Pick有两个重要的参数,即:点击时的屏幕位置,在pick中由这个屏幕上的点发射线到场景中的物体交互,可能与多个物体交互,然后逐一判断与那些物体相交。可以得到这些问题的节点,然后以区别他们。
Pick拾取是重写交互的类osgGA::GUIEventHandler,然后在类的虚函数中是否与物体相交,然后拾取出相交的物体,可以理解为概念上Pick。
原先想通过视口点与球心的距离,拖动自己得到屏幕视口上(视口界面)的坐标,入坑三解决后,发现笔者陷入误区,应该拾取球体上的点,拖动移动球体上的点,计算这两点间的角度,对球体进行旋转。
具体方法是:获取物体变换矩阵,分别计算x,y,z三个方向的偏移角度,然后变换之后设置变换矩阵。
光照对比目标软件,比较暗,优化增加了4个光源,提升亮度。
增加光源后
osg::ref_ptr OsgWidget::getPick()
{
osg::ref_ptr pGroup = new osg::Group();
// 该结点是为了开关
osg::ref_ptr pSwitch = new osg::Switch;
// 该结点是为了变换
osg::ref_ptr pTrans = new osg::MatrixTransform;
// 该结点是为了灯光
osg::ref_ptr pLightSource0 = new osg::LightSource;
osg::ref_ptr pLightSource1 = new osg::LightSource;
osg::ref_ptr pLightSource2 = new osg::LightSource;
osg::ref_ptr pLightSource3 = new osg::LightSource;
osg::ref_ptr pLightSource4 = new osg::LightSource;
#if 1
{
// 绘制球体
// 步骤一:绘制几何类型(几何体)
osg::ref_ptr pGeode = new osg::Geode;
qreal radius = 1.0;
pGeode->addDrawable(new osg::ShapeDrawable(
new osg::Sphere(osg::Vec3(0, 0, 0), 1.0f)));
osg::ref_ptr pSphere = new osg::Sphere(osg::Vec3(0, 0, 0), radius);
qDebug() << "几何结点中心点:" << pSphere->getRadius()
<< "设置的半径" << radius;
// 打印几何体中心坐标与半径
qDebug() << "变换结点中心点:" << pGeode->getBound().center().x()
<< pGeode->getBound().center().y()
<< pGeode->getBound().center().z();
qDebug() << "变换结点半径:" << pGeode->getBound().radius()
<< pGeode->getBound().radius2();
// 步骤二:加载图片添加纹理
osg::ref_ptr pImage;
pImage = osgDB::readImageFile("D:/qtProject/osgDemo/" \
"osgDemo/modules/osgWidget/image/earth.bmp");
osg::ref_ptr pTexture2D = new osg::Texture2D;
pTexture2D->setImage(pImage.get());
// 步骤三:渲染设置
osg::ref_ptr pStateSet = pGeode->getOrCreateStateSet();
pStateSet->setTextureAttribute(0, pTexture2D.get());
pStateSet->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON);
// 步骤四:手动设置属性,让法线随着模型大小变化而变化。
// osg中光照只会对有法线的模型起作用,而模型经过缩放后法线是不会变得,
pStateSet->setMode(GL_RESCALE_NORMAL, osg::StateAttribute::ON);
pTrans->addChild(pGeode);
// 步骤五:打开光照(开关节点得光照)
pStateSet = pSwitch->getOrCreateStateSet();
pStateSet->setMode(GL_LIGHTING, osg::StateAttribute::ON);
pStateSet->setMode(GL_LIGHT0, osg::StateAttribute::ON);
pStateSet->setMode(GL_LIGHT1, osg::StateAttribute::ON);
pStateSet->setMode(GL_LIGHT2, osg::StateAttribute::ON);
pStateSet->setMode(GL_LIGHT3, osg::StateAttribute::ON);
pStateSet->setMode(GL_LIGHT4, osg::StateAttribute::ON);
// 步骤六:创建光照对象
osg::ref_ptr pLight = new osg::Light();
// 设置光照对应的光照点,对应GL_LIGHT0
pLight->setLightNum(0);
// 设置光照方向
pLight->setDirection(osg::Vec3(0.0, 1.0, 0.0));
// 设置光照位置,最后一个参数0-平行光,1-点光源
pLight->setPosition(osg::Vec4(0.0, -5.0, 0.0, 1.0));
// pLight->setPosition(osg::Vec4(0.0, -40.0, 0.0, 0.0));
// 设置散射光颜色: 红光/白光
pLight->setDiffuse(osg::Vec4(1.0, 1.0, 1.0, 1.0));
// 设置常量衰减指数
pLight->setConstantAttenuation(1.0);
// 设置线性衰减指数
pLight->setLinearAttenuation(0.0);
// 设置二次衰减指数
pLight->setQuadraticAttenuation(0.0);
// 创建光源
pLightSource0->setLight(pLight.get());
#if 1
// 步骤七:增加上下左右4个光源
pLight = new osg::Light(*pLight.get());
pLight->setLightNum(1);
pLight->setPosition(osg::Vec4(5.0, -5.0, 0.0, 1.0));
// pLight->setDiffuse(osg::Vec4(1.0, 0.0, 0.0, 1.0));
pLightSource1->setLight(pLight.get());
pLight = new osg::Light(*pLight.get());
pLight->setLightNum(2);
pLight->setPosition(osg::Vec4(-5.0, -5.0, 0.0, 1.0));
// pLight->setDiffuse(osg::Vec4(0.0, 1.0, 0.0, 1.0));
pLightSource2->setLight(pLight.get());
pLight = new osg::Light(*pLight.get());
pLight->setLightNum(3);
pLight->setPosition(osg::Vec4(0.0, -5.0, -5.0, 1.0));
// pLight->setDiffuse(osg::Vec4(0.0, 0.0, 1.0, 1.0));
pLightSource3->setLight(pLight.get());
pLight = new osg::Light(*pLight.get());
pLight->setLightNum(4);
pLight->setPosition(osg::Vec4(0.0, -5.0, 5.0, 1.0));
// pLight->setDiffuse(osg::Vec4(1.0, 0.0, 1.0, 1.0));
pLightSource4->setLight(pLight.get());
#endif
// 步骤八:添加事件管理器
// _pViewer->addEventHandler(new MyUserEventHandler);
// _pViewer->addEventHandler(new MyUserCameraEventHandler);
// _pViewer->addEventHandler(new MyUserCameraScaleEventHandler);
// 事件管理器中对WSAD控制摄像机平移、上下左右旋转地球仪、鼠标滚轮缩放、鼠标中间重置
_pViewer->addEventHandler(new MyUserPickEventHandler);
#if 1
{
// 步骤九:设置相机参数
osg::Vec3d eyeVect3D;
osg::Vec3d centerVect3D;
osg::Vec3d upVect3D;
// 获取当前视口到图像中心的距离
double radius = pSwitch->getBound().radius();
// 将距离扩大5倍
double viewDistance = radius * 5;
// 参考点坐标,通常是物体中心
centerVect3D = pSwitch->getBound().center();
// 相机向上:尤其注意,相机向上的方向在世界坐标中的方向
upVect3D = osg::Vec3d(0, 0, 1);
// 视点及相机位置与物理位置分离
eyeVect3D = centerVect3D + osg::Vec3d(0, 1, 0) * viewDistance;
_pViewer->getCamera()->setViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
// 这里不使用漫游器,需要自己手动关闭,因为OSG默认了会调用TrackballManipulator
_pViewer->setCameraManipulator(0);
}
#else
_pViewer->setCameraManipulator(new osgGA::TrackballManipulator);
#endif
}
#endif
pSwitch->addChild(pTrans.get());
// 添加光源(光源与球体平级结点,球体变换,光源不变换)
pGroup->addChild(pSwitch.get());
pGroup->addChild(pLightSource0.get());
pGroup->addChild(pLightSource1.get());
pGroup->addChild(pLightSource2.get());
pGroup->addChild(pLightSource3.get());
pGroup->addChild(pLightSource4.get());
return pGroup.get();
}
MyUserPickEventHandler.h
#ifndef MYUSERPICKEVENTHANDLER_H
#define MYUSERPICKEVENTHANDLER_H
#include
#include
#define ZOOM_OUT_SCALE (0.95) // 缩小比例系数
#define ZOOM_IN_SCALE (1.05) // 放大比例系数
#define MIN_SCALE (0.5) // 相机距离变小最小比例,视觉为地球仪放大
#define MAX_SCALE (2.5) // 相机距离变大最大比例,视觉为地球仪缩小
#define STEP_DISTANCE (0.005) // 相机上下左右单步移动距离
class MyUserPickEventHandler : public osgGA::GUIEventHandler
{
public:
MyUserPickEventHandler();
public:
/** Deprecated, Handle events, return true if handled, false otherwise. */
virtual bool handle(const osgGA::GUIEventAdapter& guiEventAdapter, osgGA::GUIActionAdapter& guiActionAdapter);
protected:
bool pick(const double x, const double y, osgViewer::Viewer *pViewer, osg::Vec3dArray *pVec3dArrayOut);
osg::Vec3d screen2Word(osg::Vec3d screenVec3d, osgViewer::Viewer *pViewer);
private:
float _scale; // 保存当前相对于原始比例的缩放系数
bool _pickEarth;
osg::Vec3d _originVec3d;
osg::Vec3d _lastVec3d;
};
#endif // MYUSEREVENTHANDLER_H
MyUserPickEventHandler.cpp
#include "MyUserPickEventHandler.h"
#include "define.h"
#include "osg/MatrixTransform"
#include
#include "MyMath.h"
MyUserPickEventHandler::MyUserPickEventHandler()
: osgGA::GUIEventHandler(),
_scale(1.0),
_pickEarth(false)
{
}
bool MyUserPickEventHandler::handle(const osgGA::GUIEventAdapter
&guiEventAdapter, osgGA::GUIActionAdapter &guiActionAdapter)
{
bool flag = false;
// 获取viwerer
osg::ref_ptr pViewer
= dynamic_cast(&guiActionAdapter);
if(!pViewer)
{
return false;
}
osg::ref_ptr pGroup = pViewer->getSceneData()->asGroup();
if(!pGroup)
{
return false;
}
osg::ref_ptr pSwitch;
// 遍历结点,获取开关结点,两个结点:开关结点和灯光结点
for(int index = 0; index < pGroup->getNumChildren(); index++)
{
pSwitch = pGroup->getChild(index)->asSwitch();
if(pSwitch)
{
break;
}
if(index == pGroup->getNumChildren() - 1)
{
return false;
}
}
osg::ref_ptr pTransform = pSwitch->getChild(0)->asTransform();
if(!pTransform)
{
return false;
}
osg::ref_ptr pTrans = pTransform->asMatrixTransform();
if(!pTrans)
{
return false;
}
osg::ref_ptr pGeode = pTrans->getChild(0)->asGeode();
if(!pGeode)
{
return false;
}
osg::Vec3d eyeVect3D;
osg::Vec3d centerVect3D;
osg::Vec3d upVect3D;
qreal radius = qSqrt(pSwitch->getBound().radius2() / 3);
osg::Matrix matrix = pTrans->getMatrix();
osg::Vec3d vec3d;
osg::ref_ptr pVec3dArray = new osg::Vec3dArray();
qreal offsetAngle;
switch (guiEventAdapter.getEventType())
{
case osgGA::GUIEventAdapter::EventType::KEYDOWN:
switch (guiEventAdapter.getKey()) // 左右上下 ADWS
{
case osgGA::GUIEventAdapter::KEY_Left: // 左方向键-向左旋转1度
matrix *= osg::Matrix::rotate(osg::DegreesToRadians(-1.0), 0, 0, 1);
pTrans->setMatrix(matrix);
flag = true;
break;
case osgGA::GUIEventAdapter::KEY_Right: // 右方向键-向右旋转1度
matrix *= osg::Matrix::rotate(osg::DegreesToRadians(1.0), 0, 0, 1);
pTrans->setMatrix(matrix);
flag = true;
break;
case osgGA::GUIEventAdapter::KEY_Up: // 上方向键-向上旋转1度
matrix *= osg::Matrix::rotate(osg::DegreesToRadians(-1.0), 1, 0, 0);
pTrans->setMatrix(matrix);
flag = true;
break;
case osgGA::GUIEventAdapter::KEY_Down: // 下方向键-向下旋转1度
matrix *= osg::Matrix::rotate(osg::DegreesToRadians(1.0), 1, 0, 0);
pTrans->setMatrix(matrix);
flag = true;
break;
case osgGA::GUIEventAdapter::KEY_A: // A键-向右移动1个位置
pViewer->getCamera()->getViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
eyeVect3D += osg::Vec3d(-STEP_DISTANCE, 0, 0);
pViewer->getCamera()->setViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
break;
case osgGA::GUIEventAdapter::KEY_D: // D键-向右移动1个位置
pViewer->getCamera()->getViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
eyeVect3D += osg::Vec3d(STEP_DISTANCE, 0, 0);
pViewer->getCamera()->setViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
break;
case osgGA::GUIEventAdapter::KEY_W: // W键-向上移动1个位置
pViewer->getCamera()->getViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
eyeVect3D += osg::Vec3d(0, 0, STEP_DISTANCE);
pViewer->getCamera()->setViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
break;
case osgGA::GUIEventAdapter::KEY_S: // S键-向下移动1个位置
pViewer->getCamera()->getViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
eyeVect3D += osg::Vec3d(0, 0, -STEP_DISTANCE);
pViewer->getCamera()->setViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
break;
case osgGA::GUIEventAdapter::KEY_Space: // 空格-开关结点
pSwitch->setNodeMask(pSwitch->getNodeMask() == 0? 1:0);
break;
default:
break;
}
break;
case osgGA::GUIEventAdapter::EventType::SCROLL:
switch (guiEventAdapter.getScrollingMotion())
{
case osgGA::GUIEventAdapter::SCROLL_UP: // 鼠标滚轮向上放大
_scale *= ZOOM_IN_SCALE;
if(_scale > MAX_SCALE)
{
_scale = MAX_SCALE;
}
matrix = matrix * matrix.scale(_scale/matrix.getScale().x(),
_scale/matrix.getScale().x(),
_scale/matrix.getScale().x());
pTrans->setMatrix(matrix);
flag = true;
break;
case osgGA::GUIEventAdapter::SCROLL_DOWN: // 鼠标滚轮向下缩小
_scale *= ZOOM_OUT_SCALE;
if(_scale < MIN_SCALE)
{
_scale = MIN_SCALE;
}
matrix = matrix * matrix.scale(_scale/matrix.getScale().x(),
_scale/matrix.getScale().x(),
_scale/matrix.getScale().x());
pTrans->setMatrix(matrix);
flag = true;
break;
default:
break;
}
break;
case osgGA::GUIEventAdapter::EventType::PUSH:
switch (guiEventAdapter.getButton())
{
case osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON:
if(pick(guiEventAdapter.getX(), guiEventAdapter.getY(), pViewer, pVec3dArray.get()))
{
// 拾取到物体
_pickEarth = true;
_lastVec3d = pVec3dArray->at(0);
}
break;
case osgGA::GUIEventAdapter::MIDDLE_MOUSE_BUTTON: // 鼠标中间:重置
// 显示
pSwitch->setNodeMask(1);
// 恢复默认大小
_scale = 1.0;
matrix = matrix * matrix.scale(_scale/matrix.getScale().x(),
_scale/matrix.getScale().x(),
_scale/matrix.getScale().x());
matrix = matrix.rotate(osg::DegreesToRadians(0.0), 1, 1, 1);
pTrans->setMatrix(matrix);
// 恢复默认相机位置
pViewer->getCamera()->getViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
eyeVect3D = osg::Vec3d(0, -5, 0);
pViewer->getCamera()->setViewMatrixAsLookAt(eyeVect3D,
centerVect3D,
upVect3D);
break;
default:
break;
}
break;
case osgGA::GUIEventAdapter::EventType::DRAG:
if(_pickEarth)
{
#if 0
qDebug() << "===================================================";
qDebug() << "变换结点中心:" << pTrans->getBound().center().x()
<< pTrans->getBound().center().y()
<< pTrans->getBound().center().z();
qDebug() << "变换结点半径:" << qSqrt(pTrans->getBound().radius2() / 3);
// 此时获取到的坐标为鼠标在屏幕上的坐标
vec3d = osg::Vec3d(guiEventAdapter.getX(), guiEventAdapter.getY(), 0);
// vec3d = osg::Vec3d(0, 0, 0);
qDebug() << "传入屏幕坐标" << vec3d.x() << vec3d.y() << vec3d.z();
vec3d = screen2Word(vec3d, pViewer);
qDebug() << "输出世界坐标" << vec3d.x() << vec3d.y() << vec3d.z();
// 修成世界坐标
vec3d.set(vec3d.x(), -5, vec3d.y());
qDebug() << "修改世界坐标" << vec3d.x() << vec3d.y() << vec3d.z();
#endif
if(pick(guiEventAdapter.getX(), guiEventAdapter.getY(), pViewer, pVec3dArray.get()))
{
// 相交点
vec3d = pVec3dArray->at(0);
// 计算x轴方向角度
offsetAngle = MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(vec3d.y(), vec3d.z()))
-MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(_lastVec3d.y(), _lastVec3d.z()));
matrix *= osg::Matrix::rotate(osg::DegreesToRadians(-offsetAngle), 1, 0, 0);
// 计算y轴方向角度
// offsetAngle = MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(vec3d.x(), vec3d.z()))
// -MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(_lastVec3d.x(), _lastVec3d.z()));
// matrix *= osg::Matrix::rotate(osg::DegreesToRadians(offsetAngle), 0, 1, 0);
// 计算z轴方向角度
offsetAngle = MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(vec3d.y(), vec3d.x()))
-MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(_lastVec3d.y(), _lastVec3d.x()));
matrix *= osg::Matrix::rotate(osg::DegreesToRadians(offsetAngle), 0, 0, 1);
pTrans->setMatrix(matrix);
_lastVec3d = vec3d;
}
}
break;
case osgGA::GUIEventAdapter::EventType::RELEASE:
_pickEarth = false;
break;
default:
break;
}
// 返回false会继续向下个事件处理类传递
// return flag;
// 返回true则消息被截取,不会向下个事件处理类传递
return true;
}
bool MyUserPickEventHandler::pick(const double x, const double y, osgViewer::Viewer *pViewer, osg::Vec3dArray *pVec3dArrayOut)
{
bool ret = false;
// 判断场景
if(!pViewer->getSceneData())
{
return false;
}
// 判断是否拾取到物体
osgUtil::LineSegmentIntersector::Intersections intersections;
if(pViewer->computeIntersections(x, y, intersections))
{
for(auto iter = intersections.begin();
iter != intersections.end();
iter++)
{
pVec3dArrayOut->push_back(iter->getWorldIntersectPoint());
ret = true;
}
}
return ret;
}
osg::Vec3d MyUserPickEventHandler::screen2Word(osg::Vec3d screenVec3d, osgViewer::Viewer *pViewer)
{
osg::ref_ptr pCamera = pViewer->getCamera();
osg::Matrix matrix = pCamera->getViewMatrix() *
pCamera->getProjectionMatrix() *
pCamera->getViewport()->computeWindowMatrix();
osg::Matrix intertMatrix = osg::Matrix::inverse(matrix);
osg::Vec3d worldVec3d = screenVec3d * intertMatrix;
return worldVec3d;
}
预期应该是不变换的。
解决方法:
由此判断,开关结点与变换结点中心距离不一样,变换结点时相同的,应该时开关结点是否还有其他结点,经检查时添加了光照结点,如下图:
设置半径为1,获取上层结点半径为1.73205
导致原因:
解决方法:
不论缩放与移动,屏幕的世界坐标y轴应该一直是-5,初始化相机的时候设置了。
实测结果:
解决方法:
可能存在理解错误,屏幕上的点,x和z转出来的应该是对的(依据移动点判断。
y轴值是-1.08,球体的位置为0,0,0,半径为1,若是相交最近的y值就是Y轴上,也就是(0,-1,0)是最近y值,所以屏幕上的中心点(200,200,0)应该对应坐标是(0,-5,0),实际x和z轴转出是对的,y轴是不对的,暂时先强制将y轴的深度设为-5,得出的姑且暂时认为是正确的吧。
原因:主要是X轴和Z轴的变换,笔者加入了Y轴
解决方法:
去掉Y轴旋转
对应版本号1.15.0
欢迎技术交流和帮助,提供IT相关服务,索要源码请联系博主QQ: 21497936,若该文为原创文章,未经允许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/97109615