Pixel Buffer.cpp

本文介绍了一个简单的像素缓冲区(Pixel Buffer)应用实例,通过创建缓冲区对象并利用其进行像素数据的读取与写入,实现了图像从屏幕到纹理的传递。此过程有助于理解和实践OpenGL中的像素缓冲区技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  像素缓冲区,pixel buffer.好几个月以前看书看到这就看得我迷迷糊糊,当时手头没有电脑,书被翻译得好多根本不是人话,我看了两遍也只是知道一个大概意思.大概一个月前,决定把上本书从头带着写代码再学习一遍.一个星期前看到了像素缓冲区这块,起初也是没看懂,示例代码是运动模糊特效,信息量比较大,理解起来很难,对我那时来说.于是我把问题简化,简化到先是渲染一个球,用内置的着色器,之后创建缓冲区对象,绑定到GL_PIXEL_PACK_BUFFER,把像素读取到刚创建的缓冲区中.再将刚创建的缓冲区绑定到GL_PIXEL_UNPACK_BUFFER,把缓冲区中的数据写到纹理中.结果就是刚刚渲染出的图像去到了纹理中,数据的传递在显存之间,也达到了得到渲染出的图像的目的,做一些后期处理什么的.从代码上来看就是渲染好一个白色的球之后,在同一帧对整个屏幕这个四边形进行再次渲染,投影矩阵用到了正投影矩阵,在片段着色器上提取出纹理后将其乘以0.5.最终看到的图像就是灰色的球.这个实现逻辑简单,如果单纯是学习使用缓冲区对象这样的事,还是从这样的例子入手比较好.学习也就是这样,自己动手实践出来,也就掌握得不错了.

  最近工作上事情多多,很多时候压得我很累.我也不知道该对谁去说.跟朋友说,平时不怎么联系,见面就喝酒,听听他们的不开心,我也很少说我自己的不开心.毕竟工作上的事情跟他们说我觉得挺无聊的.亲人就更不用说.跟女朋友说,我也不想办她老相好办过的事,整天和她说什么工作的事情,而她似乎也不会主动关心我这些.前几天装个虚拟机,一整好几天,后来发现之前美术愚蠢地组织贴图的方式,又弄了一个问题,昨天又开始弄SVN管理客户端外管理不到的代码.事情都是我给自己找的.我TM也不是程序的头,我有什么事情还得跟一个美术程序员说清楚,很多事必须他明白了我才能通过.很多时候觉得委屈无处去说,也得不到什么温暖.一天天就这么过着,似乎不管到什么时候,还是我自己最能慰藉我自己,给我自己温暖和动力.也可能是,我更习惯这样.不说了,贴出来代码,洗洗睡了.

//	PixelBuffers.cpp - 2013/09/23 - 22:40
#include "stdafx.h"

#include <GLTools.h>
#include <GLMatrixStack.h>
#include <GLFrame.h>
#include <GLFrustum.h>
#include <GLGeometryTransform.h>
#include <StopWatch.h>

#define FREEGLUT_STATIC
#include <GL/glut.h>

GLFrame viewFrame;
GLFrustum viewFrustum;
GLTriangleBatch sphereBatch;
GLBatch screenQuad;
GLMatrixStack modelViewMatrix;
GLMatrixStack  projectionMatrix;
M3DMatrix44f orthoMatrix;
GLGeometryTransform transformPipeline;
GLShaderManager shaderManager;
CStopWatch timer ;

GLuint textures[1] ;
GLuint pixelBufferObjects[1];

GLuint pixelDataSize ;
void * pixelData ;

GLuint writeScreen;

GLsizei screenWidth ;
GLsizei screenHeight ;

void SetupRC(void)
{
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f );

	glEnable(GL_DEPTH_TEST);
	glEnable(GL_CULL_FACE);

	shaderManager.InitializeStockShaders();
	viewFrame.MoveForward(4.0f);

	gltMakeSphere(sphereBatch, 1.0f, 26, 13);

	gltGenerateOrtho2DMat(screenWidth, screenHeight, orthoMatrix, screenQuad);

	writeScreen =  gltLoadShaderPairWithAttributes(
		"writeScreen.vs", "writeScreen.fs",
		2,
		GLT_ATTRIBUTE_VERTEX, "vVertex",
		GLT_ATTRIBUTE_TEXTURE0, "texCoord0");

	glGenTextures(1, textures) ;

	GLuint pixelDataSize = screenWidth * screenHeight * sizeof(GL_UNSIGNED_BYTE) ;
	void * textureData = (void *)malloc(pixelDataSize) ;
	memset(textureData, NULL, pixelDataSize) ;

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, textures[0]);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, screenWidth, screenHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, textureData);

	pixelData = malloc(pixelDataSize) ;
	glGenBuffers(1, pixelBufferObjects) ;
	glBindBuffer(GL_PIXEL_PACK_BUFFER, pixelBufferObjects[0]) ;
	glBufferData(GL_PIXEL_PACK_BUFFER, screenWidth * screenHeight * sizeof(GL_UNSIGNED_BYTE), NULL, GL_DYNAMIC_COPY) ;
	glBindBuffer(GL_PIXEL_PACK_BUFFER, 0) ;

	gltGenerateOrtho2DMat(screenWidth, screenHeight, orthoMatrix, screenQuad);

	timer.Reset() ;
}

void ShutdownRC(void)
{
	glDeleteTextures(1, textures) ;
	glDeleteBuffers(1, pixelBufferObjects) ;
}

void RenderScene(void)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	modelViewMatrix.PushMatrix(viewFrame);

	float elapsedSceonds = timer.GetElapsedSeconds() ;

	modelViewMatrix.Translate(elapsedSceonds, 0, 0) ;

	if (elapsedSceonds > 1.0f)
		timer.Reset() ;
	
	GLfloat vAmbientColor[] = { 1.0f, 1.0f, 1.0f, 1.0f };

	shaderManager.UseStockShader(GLT_SHADER_FLAT,
		transformPipeline.GetModelViewProjectionMatrix(),
		vAmbientColor);

	sphereBatch.Draw();

	modelViewMatrix.PopMatrix();

	glBindBuffer(GL_PIXEL_PACK_BUFFER, pixelBufferObjects[0]) ;
	glReadPixels(0, 0, screenWidth, screenHeight, GL_RGB, GL_UNSIGNED_BYTE, NULL) ;
	glBindBuffer(GL_PIXEL_PACK_BUFFER, 0) ;

	glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixelBufferObjects[0]) ;
	glActiveTexture(GL_TEXTURE0) ;
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, screenWidth, screenHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL) ;
	glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) ;

	projectionMatrix.PushMatrix() ;
	projectionMatrix.LoadIdentity() ;
	projectionMatrix.LoadMatrix(orthoMatrix) ;
	modelViewMatrix.PushMatrix() ;
	modelViewMatrix.LoadIdentity() ;
	glDisable(GL_DEPTH_TEST) ;
	glUseProgram(writeScreen) ;
	glUniformMatrix4fv(glGetUniformLocation(writeScreen, "mvpMatrix"), 1, GL_FALSE, transformPipeline.GetModelViewProjectionMatrix()) ;
	glUniform1i(glGetUniformLocation(writeScreen, "textureUnit0"), 0) ;
	screenQuad.Draw() ;
	glEnable(GL_DEPTH_TEST) ;
	modelViewMatrix.PopMatrix() ;
	projectionMatrix.PopMatrix() ;

	glutSwapBuffers();
	glutPostRedisplay();
}

void ChangeSize(int w, int h)
{
	if(h == 0)
		h = 1;

	screenWidth = w ;
	screenHeight = h ;

	gltGenerateOrtho2DMat(screenWidth, screenHeight, orthoMatrix, screenQuad);

	glViewport(0, 0, w, h);

	viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f);

	projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
	transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);

	free(pixelData) ;
	pixelDataSize = screenWidth * screenHeight * sizeof(GL_UNSIGNED_BYTE) ;
	pixelData = (void *)malloc(pixelDataSize) ;

	glBindBuffer(GL_PIXEL_PACK_BUFFER, pixelBufferObjects[0]) ;
	glBufferData(GL_PIXEL_PACK_BUFFER, pixelDataSize, pixelData, GL_DYNAMIC_DRAW) ;
	glBindBuffer(GL_PIXEL_PACK_BUFFER, 0) ;
}

int main(int argc, char* argv[])
{
	gltSetWorkingDirectory(argv[0]);

	screenWidth = 800 ;
	screenHeight = 600 ;

	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
	glutInitWindowSize(screenWidth, screenHeight);
	glutCreateWindow("Pixel Buffer");
	glutReshapeFunc(ChangeSize);
	glutDisplayFunc(RenderScene);

	GLenum err = glewInit();
	if (err != GLEW_OK)
	{
		fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
		return 1;
	}

	SetupRC();    
	glutMainLoop();
	ShutdownRC();

	return 0;
}


 

capturerunner.h 如下 #ifndef CAPTURERUNNER_H #define CAPTURERUNNER_H #include <thread> #include <atomic> #include "../common/XThreadPool.h" #include "../logger/logger.h" extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/fifo.h> } #define min_time_interval 40 struct CaptureRunnerParam { Capture* capt; // 视频捕获对象指针 AVFifo* fifo; // AVFifo帧数据队列 }; class CaptureRunner : public XTask { public: int Init(void* param); void Deinit(void); int Start(int time); int Stop(void); int Run(); private: std::atomic<bool> m_is_running; Logger::ptr m_logger; Capture* m_capt; AVFifo* m_fifo; int m_time_interval; }; #endif // CAPTURERUNNER_H capturerunner.cpp如下 #include "capturerunner.h" #include <chrono> using namespace std; int CaptureRunner::Init(void* param) { CaptureRunnerParam* c = (CaptureRunnerParam*)param; this->m_capt = c->capt; this->m_fifo = c->fifo; this->m_logger = DefaultLogger(); return 0; } void CaptureRunner::Deinit() { if(m_is_running) { this->Stop(); } return; } int CaptureRunner::Start(int time) { this->m_time_interval = time; Run(); return 0; } int CaptureRunner::Stop() { m_is_running = false; return 0; } int CaptureRunner::Run() { if(m_is_running) { m_logger->Error("Running now"); return -1; } m_is_running = true; int ret = 0; while(m_is_running) { AVFrame* frame = m_capt->CaptureFrame(); if(!frame) { m_logger->Error("readfile failed"); this_thread::sleep_for(chrono::seconds(1)); continue; } // 将帧写入AVFifo ret = av_fifo_write(m_fifo, &frame, 1); if(ret < 0) { m_logger->Error("write to fifo failed"); av_frame_free(&frame); continue; } if(m_time_interval > min_time_interval) { this_thread::sleep_for(chrono::milliseconds(m_time_interval)); } else { this_thread::sleep_for(chrono::milliseconds(min_time_interval)); } } return 0; } capt_test.cpp如下 #include <signal.h> #include <atomic> #include "../common/XThreadPool.h" #include "test.h" #include "../capt/capture.h" #include "../capt/capturerunner.h" extern "C" { #include <libavutil/fifo.h> #include <libavutil/imgutils.h> } using namespace std; Logger::ptr Logger2 = DefaultLogger(); class capt_write : public XTask { public: int Run(); void Stop(); int Init(CaptureRunnerParam* param); void Deinit(); atomic<bool> is_running; AVFifo* fifo; FILE* fp; AVPixelFormat pix_fmt; int width, height; }; int capt_write::Init(CaptureRunnerParam* par) { this->fifo = par->fifo; this->fp = fopen("/home/kylin/zxk/test20241029/output.yuv", "wb"); if(!fp) { Logger2->Error("Failed to open output file"); return -1; } // 从捕获对象获取参数 this->width = par->capt->m_width; this->height = par->capt->m_height; this->pix_fmt = par->capt->m_pixel_fmt; return 0; } int capt_write::Run() { if(is_running) { Logger2->Error("Running now"); return -1; } is_running = true; while(is_running) { if(av_fifo_can_read(fifo) <= 0) { Logger2->Warning("Fifo empty"); this_thread::sleep_for(chrono::seconds(1)); continue; } AVFrame* frame = nullptr; av_fifo_read(fifo, &frame, 1); if(!frame) { Logger2->Warning("frame is NULL"); continue; } // 计算各分量大小 size_t y_bytes = width * height; size_t u_bytes = 0, v_bytes = 0; switch(pix_fmt) { case AV_PIX_FMT_YUYV422: y_bytes = width * height * 2; // YUYV packed格式 break; case AV_PIX_FMT_YUV420P: u_bytes = width * height / 4; v_bytes = width * height / 4; break; case AV_PIX_FMT_BGR0: y_bytes = width * height * 4; // BGR0 packed格式 break; } // 写入文件 fwrite(frame->data[0], 1, y_bytes, fp); if(u_bytes > 0) fwrite(frame->data[1], 1, u_bytes, fp); if(v_bytes > 0) fwrite(frame->data[2], 1, v_bytes, fp); av_frame_free(&frame); } return 0; } void capt_write::Deinit() { if(is_running) { Stop(); } if(fp) { fclose(fp); } } void capt_write::Stop() { is_running = false; } bool stop2 = false; void sigint_handler02(int sig_num) { stop2 = true; } // 初始化V4L2捕获参数 void init_v4l2(CaptParam& c) { c.width = 1280; c.height = 720; c.file_name = "/dev/video0"; c.pixel_fmt = AV_PIX_FMT_YUYV422; c.v4l2_buffer_type = V4L2_BUF_TYPE_VIDEO_CAPTURE; c.v4l2_memory_type = V4L2_MEMORY_MMAP; c.v4l2_pixel_fmt = V4L2_PIX_FMT_YUYV; } // 初始化文件捕获参数 void init_file(CaptParam& c) { c.file_name = "/home/kylin/zxk/test20241021/testyuv.yuv"; c.width = 1280; c.height = 720; c.pixel_fmt = AV_PIX_FMT_YUV420P; } // 初始化屏幕捕获参数 void init_screen(CaptParam& c) { c.width = 1920; c.height = 1080; c.pixel_fmt = AV_PIX_FMT_BGR0; } int capt_test() { CaptParam c; // 选择一种捕获方式初始化 (取消注释对应的行) //init_v4l2(c); // V4L2捕获 init_file(c); // 文件捕获 //init_screen(c); // 屏幕捕获 // 创建AVFifo AVFifo* fifo = av_fifo_alloc2(10 * 1024 * 1024, 1, 0); // 10MB缓冲区 // 创建捕获对象 (与上面选择的初始化方式对应) //Capture* capt = new V4L2Capture; Capture* capt = new FileCapture; //Capture* capt = new ScreenCapture; capt->Init(&c); // 设置任务参数 CaptureRunnerParam param; param.capt = capt; param.fifo = fifo; // 创建任务 auto c1 = make_shared<CaptureRunner>(); auto c2 = make_shared<capt_write>(); c1->Init(&param); c2->Init(&param); // 启动线程池 XThreadPool pool; pool.Init(2); pool.Start(); pool.AddTask(c1); pool.AddTask(c2); // 主循环 signal(SIGINT, sigint_handler02); signal(SIGKILL, sigint_handler02); while(!stop2) { this_thread::sleep_for(chrono::milliseconds(100)); } // 清理 c2->Deinit(); this_thread::sleep_for(chrono::milliseconds(1000)); c1->Deinit(); capt->Deinit(); av_fifo_freep2(&fifo); pool.Stop(); return 0; } CMakeLists.txt 如下 cmake_minimum_required(VERSION 3.15) project(CaptureTest) set(CMAKE_CXX_STANDARD 11) #添加头文件搜索路径 include_directories( ${CMAKE_SOURCE_DIR}/include /home/zhanghan/include /home/zhanghan/Log/Log-lib/include /usr/local/include ) #添加静态库路径 link_directories( /home/zhanghan/Log ) # 查找FFmpeg相关库 find_package(PkgConfig REQUIRED) pkg_check_modules(FFMPEG REQUIRED libavcodec libavformat libavutil libswscale libavdevice ) # 添加FFmpeg头文件 include_directories(${FFMPEG_INCLUDE_DIRS}) # 所有源文件 set(SOURCES main.cpp capt_test.cpp ../logger/logger.cpp ../logger/formatter.cpp ../logger/logsink.cpp ../logger/asynlopper.cpp ../capt/capturerunner.cpp ../capt/capture.cpp ../common/XThreadPool.cpp ../capt/ScreenCapture.cpp ../capt/V4L2Capture.cpp ../capt/FileCapture.cpp ) add_executable(capture_test ${SOURCES}) # 链接库 target_link_libraries(capture_test pthread ${FFMPEG_LIBRARIES} log ) 错误信息如下 zhanghan@zhanghan-virtual-machine:~/daima/2/test2/build$ make -- Configuring done -- Generating done -- Build files have been written to: /home/zhanghan/daima/2/test2/build Consolidate compiler generated dependencies of target capture_test [ 7%] Building CXX object CMakeFiles/capture_test.dir/capt_test.cpp.o In file included from /home/zhanghan/daima/2/test2/capt_test.cpp:6: /home/zhanghan/daima/2/test2/../capt/capturerunner.h:19:5: error: ‘AVFifo’ does not name a type 19 | AVFifo* fifo; // AVFifo帧数据队列 | ^~~~~~ /home/zhanghan/daima/2/test2/../capt/capturerunner.h:34:5: error: ‘AVFifo’ does not name a type 34 | AVFifo* m_fifo; | ^~~~~~ /home/zhanghan/daima/2/test2/capt_test.cpp:25:5: error: ‘AVFifo’ does not name a type 25 | AVFifo* fifo; | ^~~~~~ /home/zhanghan/daima/2/test2/capt_test.cpp: In member function ‘int capt_write::Init(CaptureRunnerParam*)’: /home/zhanghan/daima/2/test2/capt_test.cpp:32:11: error: ‘class capt_write’ has no member named ‘fifo’ 32 | this->fifo = par->fifo; | ^~~~ /home/zhanghan/daima/2/test2/capt_test.cpp:32:23: error: ‘struct CaptureRunnerParam’ has no member named ‘fifo’ 32 | this->fifo = par->fifo; | ^~~~ /home/zhanghan/daima/2/test2/capt_test.cpp:40:30: error: ‘int Capture::m_width’ is protected within this context 40 | this->width = par->capt->m_width; | ^~~~~~~ In file included from /home/zhanghan/daima/2/test2/capt_test.cpp:5: /home/zhanghan/daima/2/test2/../capt/capture.h:87:9: note: declared protected here 87 | int m_width; | ^~~~~~~ /home/zhanghan/daima/2/test2/capt_test.cpp:40:30: note: field ‘int Capture::m_width’ can be accessed via ‘int Capture::GetWidth() const’ 40 | this->width = par->capt->m_width; | ^~~~~~~ | GetWidth() /home/zhanghan/daima/2/test2/capt_test.cpp:41:31: error: ‘int Capture::m_height’ is protected within this context 41 | this->height = par->capt->m_height; | ^~~~~~~~ In file included from /home/zhanghan/daima/2/test2/capt_test.cpp:5: /home/zhanghan/daima/2/test2/../capt/capture.h:88:9: note: declared protected here 88 | int m_height; | ^~~~~~~~ /home/zhanghan/daima/2/test2/capt_test.cpp:41:31: note: field ‘int Capture::m_height’ can be accessed via ‘int Capture::GetHeight() const’ 41 | this->height = par->capt->m_height; | ^~~~~~~~ | GetHeight() /home/zhanghan/daima/2/test2/capt_test.cpp:42:32: error: ‘AVPixelFormat Capture::m_pixel_fmt’ is protected within this context 42 | this->pix_fmt = par->capt->m_pixel_fmt; | ^~~~~~~~~~~ In file included from /home/zhanghan/daima/2/test2/capt_test.cpp:5: /home/zhanghan/daima/2/test2/../capt/capture.h:89:19: note: declared protected here 89 | AVPixelFormat m_pixel_fmt; | ^~~~~~~~~~~ /home/zhanghan/daima/2/test2/capt_test.cpp:42:32: note: field ‘AVPixelFormat Capture::m_pixel_fmt’ can be accessed via ‘AVPixelFormat Capture::GetPixelFmt() const’ 42 | this->pix_fmt = par->capt->m_pixel_fmt; | ^~~~~~~~~~~ | GetPixelFmt() /home/zhanghan/daima/2/test2/capt_test.cpp: In member function ‘virtual int capt_write::Run()’: /home/zhanghan/daima/2/test2/capt_test.cpp:55:29: error: ‘fifo’ was not declared in this scope; did you mean ‘mkfifo’? 55 | if(av_fifo_can_read(fifo) <= 0) { | ^~~~ | mkfifo /home/zhanghan/daima/2/test2/capt_test.cpp:55:12: error: ‘av_fifo_can_read’ was not declared in this scope; did you mean ‘av_fifo_generic_read’? 55 | if(av_fifo_can_read(fifo) <= 0) { | ^~~~~~~~~~~~~~~~ | av_fifo_generic_read /home/zhanghan/daima/2/test2/capt_test.cpp:62:22: error: ‘fifo’ was not declared in this scope; did you mean ‘mkfifo’? 62 | av_fifo_read(fifo, &frame, 1); | ^~~~ | mkfifo /home/zhanghan/daima/2/test2/capt_test.cpp:62:9: error: ‘av_fifo_read’ was not declared in this scope; did you mean ‘avio_read’? 62 | av_fifo_read(fifo, &frame, 1); | ^~~~~~~~~~~~ | avio_read /home/zhanghan/daima/2/test2/capt_test.cpp: In function ‘int capt_test()’: /home/zhanghan/daima/2/test2/capt_test.cpp:149:5: error: ‘AVFifo’ was not declared in this scope 149 | AVFifo* fifo = av_fifo_alloc2(10 * 1024 * 1024, 1, 0); // 10MB缓冲区 | ^~~~~~ /home/zhanghan/daima/2/test2/capt_test.cpp:149:13: error: ‘fifo’ was not declared in this scope; did you mean ‘mkfifo’? 149 | AVFifo* fifo = av_fifo_alloc2(10 * 1024 * 1024, 1, 0); // 10MB缓冲区 | ^~~~ | mkfifo /home/zhanghan/daima/2/test2/capt_test.cpp:149:20: error: ‘av_fifo_alloc2’ was not declared in this scope; did you mean ‘av_fifo_alloc’? 149 | AVFifo* fifo = av_fifo_alloc2(10 * 1024 * 1024, 1, 0); // 10MB缓冲区 | ^~~~~~~~~~~~~~ | av_fifo_alloc /home/zhanghan/daima/2/test2/capt_test.cpp:161:11: error: ‘struct CaptureRunnerParam’ has no member named ‘fifo’ 161 | param.fifo = fifo; | ^~~~ /home/zhanghan/daima/2/test2/capt_test.cpp:188:5: error: ‘av_fifo_freep2’ was not declared in this scope; did you mean ‘av_fifo_freep’? 188 | av_fifo_freep2(&fifo); | ^~~~~~~~~~~~~~ | av_fifo_freep 如何解决?
最新发布
06-06
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值