用OpenSceneGraph实现的NeHe OpenGL教程 - 第三十五课

52 篇文章 38 订阅
  • 简介

这节课介绍了如何在三维场景中类似播放一段视频的效果。我们都知道视频实际上是由一帧帧的图片来构成的,因此我们将视频中的图片解析成图片格式,并将这些图片赋给几何体表面,让它们在很短的间隔内进行切换就实现了动态的效果。

  • 实现

本课中使用了Windows解析视频格式AVI文件的API,代码中这部分内容都来自NeHe课程中的相应代码,读者可以查阅MSDN了解这些内容:

首先创建播放AVI文件的几何体(类似于电影的大屏幕)

osg::Geode*	createBackGroundGeode(osg::Texture2D *text)
{
    osg::Geode *geode = new osg::Geode;
    osg::Geometry *geometry = new osg::Geometry;
    geometry->setUpdateCallback(new DynamicTextureUpdateCallback);
 ...
	geometry->setVertexArray(vertexArray);
	geometry->setTexCoordArray(0, textureArray, osg::Array::BIND_PER_VERTEX);
	geometry->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS, 0, vertexArray->size()));
	geometry->getOrCreateStateSet()->setTextureAttributeAndModes(0, text);
	geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, false);
	geode->addDrawable(geometry);

	return geode;
}
动态的纹理回调DynamicTextureUpdateCallback实现了动态修改纹理:

class DynamicTextureUpdateCallback : public osg::Drawable::UpdateCallback
{
public:
	DynamicTextureUpdateCallback() : _lastUpdate(0), _frame(0), _passedTimeInMillSecond(0)
	{
		_startTime = osg::Timer::instance()->tick(); 
		_currentTime = _startTime;
	}

	void update(osg::NodeVisitor* nv, osg::Drawable* drawable)
	{
		osg::Geometry *geometry = dynamic_cast<osg::Geometry*>(drawable);
		if (!geometry)
...
        }
};
接着创建场景中的几何体:

osg::Node*	createBox()
osg::Node*	createSphere()
osg::Node*	createCylinder()
这部分内容与 第二十三课的内容很类似

将上面所有的几何体添加到一个Switch开关节点中实现切换操作:

在与键盘的交互中实现切换纹理方式和切换节点的效果:

		case(osgGA::GUIEventAdapter::KEYDOWN):
			{
				if (ea.getKey() == osgGA::GUIEventAdapter::KEY_Space)
				{
					if (!g_SwitchNode)
						return false;

					static int i = 0;
					++i;
					if (i > 2)
						i = 0;
					g_SwitchNode->setSingleChildOn(i);
				}
				if (ea.getKey() == osgGA::GUIEventAdapter::KEY_E)
				{
					static bool flag = true;
					if(flag){
						g_SwitchNode->getOrCreateStateSet()->setTextureMode(0, GL_TEXTURE_GEN_S, osg::StateAttribute::OFF);
						g_SwitchNode->getOrCreateStateSet()->setTextureMode(0, GL_TEXTURE_GEN_T, osg::StateAttribute::OFF);
					}else{
						g_SwitchNode->getOrCreateStateSet()->setTextureMode(0, GL_TEXTURE_GEN_S, osg::StateAttribute::ON);
						g_SwitchNode->getOrCreateStateSet()->setTextureMode(0, GL_TEXTURE_GEN_T, osg::StateAttribute::ON);
					}
					flag = !flag;
				}
将以上所有内容添加到场景根节点下。编译运行程序:


附:本课源码(源码中可能存在错误和不足,仅供参考)

#include "../osgNeHe.h"

#include <QtCore/QTimer>
#include <QtGui/QApplication>
#include <QtGui/QVBoxLayout>

#include <osgViewer/Viewer>
#include <osgDB/ReadFile>
#include <osgQt/GraphicsWindowQt>
#include <osg/MatrixTransform>
#include <osg/Texture2D>
#include <osg/ShapeDrawable>

#include <osg/Switch>
#include <osg/AnimationPath>

#include <osg/TexGen>
#include <osg/TexGenNode>

#include <vfw.h>


AVISTREAMINFO		psi;					
PAVISTREAM			pavi;						
PGETFRAME			pgf;							
BITMAPINFOHEADER	bmih;
long				lastframe;								
int					width;								
int					height;								
char					*pdata;								
int					mpf;									

HDRAWDIB hdd;									
HBITMAP hBitmap;
HDC hdc = CreateCompatibleDC(0);		
unsigned char* data = 0;								

osg::Texture2D *g_Texture2D = NULL;
osg::Image *g_TextureImage = NULL;
osg::Switch *g_SwitchNode = NULL;

void flipIt(void* buffer)								
{
	void* b = buffer;	
	__asm												
	{
		mov ecx, 256*256
			mov ebx, b							
label:												
		mov al,[ebx+0]						
		mov ah,[ebx+2]						
		mov [ebx+2],al						
			mov [ebx+0],ah					

			add ebx,3								
			dec ecx									
			jnz label									
	}
}

void OpenAVI(LPCSTR szFile)
{
	TCHAR	title[100];

	AVIFileInit();
	if (AVIStreamOpenFromFileA(&pavi, szFile, streamtypeVIDEO, 0, OF_READ, NULL) !=0)
	{
		return;
	}

	AVIStreamInfo(pavi, &psi, sizeof(psi));
	width=psi.rcFrame.right-psi.rcFrame.left;
	height=psi.rcFrame.bottom-psi.rcFrame.top;

	lastframe=AVIStreamLength(pavi);

	mpf=AVIStreamSampleToTime(pavi,lastframe)/lastframe;

	bmih.biSize = sizeof (BITMAPINFOHEADER);
	bmih.biPlanes = 1;
	bmih.biBitCount = 24;
	bmih.biWidth = 256;
	bmih.biHeight = 256;
	bmih.biCompression = BI_RGB;

	hBitmap = CreateDIBSection (hdc, (BITMAPINFO*)(&bmih), DIB_RGB_COLORS, (void**)(&data), NULL, NULL);
	SelectObject (hdc, hBitmap);

	pgf=AVIStreamGetFrameOpen(pavi, NULL);
	if (pgf==NULL)
	{
		return;
	}
}


void GrabAVIFrame(int frame)
{
	LPBITMAPINFOHEADER lpbi;
	lpbi = (LPBITMAPINFOHEADER)AVIStreamGetFrame(pgf, frame);
	pdata=(char *)lpbi+lpbi->biSize+lpbi->biClrUsed * sizeof(RGBQUAD);

	DrawDibDraw (hdd, hdc, 0, 0, 256, 256, lpbi, pdata, 0, 0, width, height, 0);
	flipIt(data);

	g_TextureImage->setImage(256, 256, 1, 3, GL_RGB, GL_UNSIGNED_BYTE, data, osg::Image::NO_DELETE);
	g_Texture2D->setImage(g_TextureImage);
}


void CloseAVI(void)
{
	DeleteObject(hBitmap);
	DrawDibClose(hdd);
	AVIStreamGetFrameClose(pgf);
	AVIStreamRelease(pavi);
	AVIFileExit();
}


void initialize()
{
	hdd = DrawDibOpen();
	g_Texture2D = new osg::Texture2D;
	g_Texture2D->setFilter(osg::Texture::MAG_FILTER, osg::Texture::NEAREST);
	g_Texture2D->setFilter(osg::Texture::MIN_FILTER, osg::Texture::NEAREST);
	g_TextureImage = new osg::Image;
	g_TextureImage->allocateImage(256, 256, 1, GL_RGB, GL_UNSIGNED_BYTE);
	OpenAVI("Data/face3.avi");
}


class DynamicTextureUpdateCallback : public osg::Drawable::UpdateCallback
{
public:
	DynamicTextureUpdateCallback() : _lastUpdate(0), _frame(0), _passedTimeInMillSecond(0)
	{
		_startTime = osg::Timer::instance()->tick(); 
		_currentTime = _startTime;
	}

	void update(osg::NodeVisitor* nv, osg::Drawable* drawable)
	{
		osg::Geometry *geometry = dynamic_cast<osg::Geometry*>(drawable);
		if (!geometry)
			return;
		osg::Texture2D *texture2D = dynamic_cast<osg::Texture2D *>(geometry->getOrCreateStateSet()->getTextureAttribute(
																											0, osg::StateAttribute::TEXTURE));
		if (!texture2D)
			return;

		if (nv->getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR &&
			nv->getFrameStamp() &&
			nv->getFrameStamp()->getFrameNumber() != _lastUpdate) 
		{
			_lastUpdate = nv->getFrameStamp()->getFrameNumber();
			_currentTime = osg::Timer::instance()->tick();
			float t = osg::Timer::instance()->delta_m(_startTime, _currentTime);
			_passedTimeInMillSecond += t;
			
			if (_passedTimeInMillSecond >= mpf)
			{
				GrabAVIFrame(_frame);
				_frame++;

				if (_frame >= lastframe)
				{
					_frame = 0;
				}
				_passedTimeInMillSecond = 0;
			}
		}
	}

	osg::Timer_t	_startTime;
	osg::Timer_t	_currentTime;
	unsigned int		_lastUpdate;
	unsigned int	    _frame;
	int					_passedTimeInMillSecond;
};


//
///绘制几何体/
//
osg::Geode*	createBackGroundGeode(osg::Texture2D *text)
{
	osg::Geode *geode = new osg::Geode;
	osg::Geometry *geometry = new osg::Geometry;
	geometry->setUpdateCallback(new DynamicTextureUpdateCallback);
	osg::Vec3Array *vertexArray = new osg::Vec3Array;
	osg::Vec2Array *textureArray = new osg::Vec2Array;

	vertexArray->push_back(osg::Vec3(11.0f,  8.3f, -20.0f));
	vertexArray->push_back(osg::Vec3(-11.0f,  8.3f, -20.0f));
	vertexArray->push_back(osg::Vec3(-11.0f, -8.3f, -20.0f));
	vertexArray->push_back(osg::Vec3(11.0f, -8.3f, -20.0f));

	textureArray->push_back(osg::Vec2(1.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 0.0f));

	geometry->setVertexArray(vertexArray);
	geometry->setTexCoordArray(0, textureArray, osg::Array::BIND_PER_VERTEX);
	geometry->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS, 0, vertexArray->size()));
	geometry->getOrCreateStateSet()->setTextureAttributeAndModes(0, text);
	geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, false);
	geode->addDrawable(geometry);

	return geode;
}

osg::Node*	createBox(/*osg::Texture2D *text*/)
{
	osg::MatrixTransform *mtX = new osg::MatrixTransform;
	mtX->addUpdateCallback(new osg::AnimationPathCallback(osg::Vec3(), osg::X_AXIS, 1.5));
	osg::MatrixTransform *mtY = new osg::MatrixTransform;
	mtY->addUpdateCallback(new osg::AnimationPathCallback(osg::Vec3(), osg::Y_AXIS, 2.5));
	osg::MatrixTransform *mtZ = new osg::MatrixTransform;
	mtZ->addUpdateCallback(new osg::AnimationPathCallback(osg::Vec3(), osg::Z_AXIS, 3.5));

	osg::Geode *geode = new osg::Geode;
	osg::Geometry *geometry = new osg::Geometry;
	osg::Vec3Array *vertexArray = new osg::Vec3Array;
	osg::Vec2Array *textureArray = new osg::Vec2Array;
	osg::Vec3Array *normalArray = new osg::Vec3Array;
	
	for (int i = 0; i < 4; ++i)
		normalArray->push_back(osg::Vec3(0.0f, 0.0f, 0.5f));
	textureArray->push_back(osg::Vec2(0.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f, -1.0f,  1.0f));
	vertexArray->push_back(osg::Vec3(1.0f, -1.0f,  1.0f));
	vertexArray->push_back(osg::Vec3(1.0f,  1.0f,  1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f,  1.0f,  1.0f));
	
	for (int i = 0; i < 4; ++i)
		normalArray->push_back(osg::Vec3(0.0f, 0.0f,-0.5f));
	textureArray->push_back(osg::Vec2(1.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 0.0f));
	vertexArray->push_back(osg::Vec3(-1.0f, -1.0f,  -1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f, 1.0f,  -1.0f));
	vertexArray->push_back(osg::Vec3(1.0f,  1.0f, - 1.0f));
	vertexArray->push_back(osg::Vec3(1.0f,  -1.0f,  -1.0f));

	for (int i = 0; i < 4; ++i)
		normalArray->push_back(osg::Vec3(0.0f, 0.5f,0.0f));
	textureArray->push_back(osg::Vec2(0.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f, 1.0f,  -1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f, 1.0f,  1.0f));
	vertexArray->push_back(osg::Vec3(1.0f,  1.0f,  1.0f));
	vertexArray->push_back(osg::Vec3(1.0f,  1.0f,  -1.0f));

	for (int i = 0; i < 4; ++i)
		normalArray->push_back(osg::Vec3(0.0f, -0.5f,0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 0.0f));
	vertexArray->push_back(osg::Vec3(-1.0f, -1.0f,  -1.0f));
	vertexArray->push_back(osg::Vec3(1.0f, -1.0f,  -1.0f));
	vertexArray->push_back(osg::Vec3(1.0f,  -1.0f,  1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f,  -1.0f,  1.0f));

	for (int i = 0; i < 4; ++i)
		normalArray->push_back(osg::Vec3(0.5f, 0.0f,0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 0.0f));
	vertexArray->push_back(osg::Vec3(1.0f, -1.0f,  -1.0f));
	vertexArray->push_back(osg::Vec3(1.0f, 1.0f,  -1.0f));
	vertexArray->push_back(osg::Vec3(1.0f,  1.0f,  1.0f));
	vertexArray->push_back(osg::Vec3(1.0f,  -1.0f,  1.0f));

	for (int i = 0; i < 4; ++i)
		normalArray->push_back(osg::Vec3(-0.5f, 0.0f,0.0f));
	textureArray->push_back(osg::Vec2(0.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 0.0f));
	textureArray->push_back(osg::Vec2(1.0f, 1.0f));
	textureArray->push_back(osg::Vec2(0.0f, 1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f, -1.0f,  -1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f, -1.0f,  1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f,  1.0f,  1.0f));
	vertexArray->push_back(osg::Vec3(-1.0f,  1.0f,  -1.0f));

	geometry->setVertexArray(vertexArray);
	geometry->setNormalArray(normalArray, osg::Array::BIND_PER_VERTEX);
	geometry->setTexCoordArray(0, textureArray, osg::Array::BIND_PER_VERTEX);
	geometry->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS, 0, vertexArray->size()));
	geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, false);
	geode->addDrawable(geometry);

	mtX->addChild(mtY);
	mtY->addChild(mtZ);
	mtZ->addChild(geode);

	return mtX;
}

osg::Node*	createSphere()
{
	osg::MatrixTransform *mtX = new osg::MatrixTransform;
	mtX->addUpdateCallback(new osg::AnimationPathCallback(osg::Vec3(), osg::X_AXIS, 1.5));
	osg::MatrixTransform *mtY = new osg::MatrixTransform;
	mtY->addUpdateCallback(new osg::AnimationPathCallback(osg::Vec3(), osg::Y_AXIS, 2.5));
	osg::MatrixTransform *mtZ = new osg::MatrixTransform;
	mtZ->addUpdateCallback(new osg::AnimationPathCallback(osg::Vec3(), osg::Z_AXIS, 3.5));

	osg::Geode *sphere = new osg::Geode;
	osg::ShapeDrawable *sphereDrawable = new osg::ShapeDrawable(new osg::Sphere(osg::Vec3(0,0,0), 1.3));
	sphere->addDrawable(sphereDrawable);

	mtX->addChild(mtY);
	mtY->addChild(mtZ);
	mtZ->addChild(sphere);

	return mtX;
}

osg::Node*	createCylinder()
{
	osg::MatrixTransform *mtX = new osg::MatrixTransform;
	mtX->addUpdateCallback(new osg::AnimationPathCallback(osg::Vec3(), osg::X_AXIS, 1.5));
	osg::MatrixTransform *mtY = new osg::MatrixTransform;
	mtY->addUpdateCallback(new osg::AnimationPathCallback(osg::Vec3(), osg::Y_AXIS, 2.5));
	osg::MatrixTransform *mtZ = new osg::MatrixTransform;
	mtZ->addUpdateCallback(new osg::AnimationPathCallback(osg::Vec3(), osg::Z_AXIS, 3.5));

	osg::Geode *cylinder = new osg::Geode;
	osg::ShapeDrawable *cylinderDrawable = new osg::ShapeDrawable(new osg::Cylinder(osg::Vec3(0,0,0), 0.5, 2.0));
	cylinder->addDrawable(cylinderDrawable);

	mtX->addChild(mtY);
	mtY->addChild(mtZ);
	mtZ->addChild(cylinder);


	return mtX;
}

//
/场景交互//
//
class ManipulatorSceneHandler : public osgGA::GUIEventHandler
{
public:
	ManipulatorSceneHandler()
	{
	}

	virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
	{
		osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer*>(&aa);
		if (!viewer)
			return false;
		if (!viewer->getSceneData())
			return false;
		if (ea.getHandled()) 
			return false;

		switch(ea.getEventType())
		{
		case(osgGA::GUIEventAdapter::KEYDOWN):
			{
				if (ea.getKey() == osgGA::GUIEventAdapter::KEY_Space)
				{
					if (!g_SwitchNode)
						return false;

					static int i = 0;
					++i;
					if (i > 2)
						i = 0;
					g_SwitchNode->setSingleChildOn(i);
				}
				if (ea.getKey() == osgGA::GUIEventAdapter::KEY_E)
				{
					static bool flag = true;
					if(flag){
						g_SwitchNode->getOrCreateStateSet()->setTextureMode(0, GL_TEXTURE_GEN_S, osg::StateAttribute::OFF);
						g_SwitchNode->getOrCreateStateSet()->setTextureMode(0, GL_TEXTURE_GEN_T, osg::StateAttribute::OFF);
					}else{
						g_SwitchNode->getOrCreateStateSet()->setTextureMode(0, GL_TEXTURE_GEN_S, osg::StateAttribute::ON);
						g_SwitchNode->getOrCreateStateSet()->setTextureMode(0, GL_TEXTURE_GEN_T, osg::StateAttribute::ON);
					}
					flag = !flag;
				}
			}
		default: break;
		}
		return false;
	}
};
//



//
//osgNeHe框架
//
class ViewerWidget : public QWidget, public osgViewer::Viewer
{
public:
	ViewerWidget(osg::Node *scene = NULL)
	{
		QWidget* renderWidget = getRenderWidget( createGraphicsWindow(0,0,100,100), scene);

		QVBoxLayout* layout = new QVBoxLayout;
		layout->addWidget(renderWidget);
		layout->setContentsMargins(0, 0, 0, 1);
		setLayout( layout );

		connect( &_timer, SIGNAL(timeout()), this, SLOT(update()) );
		_timer.start( 10 );
	}

	QWidget* getRenderWidget( osgQt::GraphicsWindowQt* gw, osg::Node* scene )
	{
		osg::Camera* camera = this->getCamera();
		camera->setGraphicsContext( gw );

		const osg::GraphicsContext::Traits* traits = gw->getTraits();

		camera->setClearColor( osg::Vec4(0.0, 0.0, 0.0, 1.0) );
		camera->setViewport( new osg::Viewport(0, 0, traits->width, traits->height) );
		camera->setProjectionMatrixAsPerspective(45.0f, static_cast<double>(traits->width)/static_cast<double>(traits->height), 0.1f, 100.0f );
		camera->setViewMatrixAsLookAt(osg::Vec3d(0, 0, 0), osg::Vec3d(0, 0, -1), osg::Vec3d(0, 1, 0));

		this->setSceneData( scene );
		this->addEventHandler(new ManipulatorSceneHandler);

		return gw->getGLWidget();
	}

	osgQt::GraphicsWindowQt* createGraphicsWindow( int x, int y, int w, int h, const std::string& name="", bool windowDecoration=false )
	{
		osg::DisplaySettings* ds = osg::DisplaySettings::instance().get();
		osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
		traits->windowName = name;
		traits->windowDecoration = windowDecoration;
		traits->x = x;
		traits->y = y;
		traits->width = w;
		traits->height = h;
		traits->doubleBuffer = true;
		traits->alpha = ds->getMinimumNumAlphaBits();
		traits->stencil = ds->getMinimumNumStencilBits();
		traits->sampleBuffers = ds->getMultiSamples();
		traits->samples = ds->getNumMultiSamples();

		return new osgQt::GraphicsWindowQt(traits.get());
	}

	virtual void paintEvent( QPaintEvent* event )
	{ 
		frame(); 
	}

protected:

	QTimer _timer;
};



osg::Node*	buildScene()
{
	initialize();

	osg::Group *root = new osg::Group;
	
	osg::Switch *switchNode = new osg::Switch;
	g_SwitchNode = switchNode;
	switchNode->addChild(createBox());
	switchNode->addChild(createCylinder());
	switchNode->addChild(createSphere());
	switchNode->setSingleChildOn(0);

	osg::TexGen *texGen = new osg::TexGen;
	texGen->setMode(osg::TexGen::SPHERE_MAP);
	switchNode->getOrCreateStateSet()->setTextureAttributeAndModes(0, texGen);
	switchNode->getOrCreateStateSet()->setTextureAttributeAndModes(0, g_Texture2D);

	osg::MatrixTransform *zoomMT = new osg::MatrixTransform;
	zoomMT->setMatrix(osg::Matrix::translate(0.0f, 0.0f, -10.0f));
	zoomMT->addChild(switchNode);

	root->addChild(createBackGroundGeode(g_Texture2D));
	root->addChild(zoomMT);
	return root;
}

int main( int argc, char** argv )
{
	QApplication app(argc, argv);
	ViewerWidget* viewWidget = new ViewerWidget(buildScene());
	viewWidget->setGeometry( 100, 100, 640, 480 );
	viewWidget->show();
	return app.exec();
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值