全景视频播放器代码分析

来总结一下最近研究的全景视频播放器代码

平台:Windows
软件:vs2019
代码来源:OpenGL全景视频.

一、前期准备

刚开始的时候想先从代码入手,和想象的不太一样,本来以为C语言的代码撑死每句指令都百度,打断点看变量应该也能看懂。于是先在b站找到了C++编写视频播放器的视频,看的我一头雾水,里面用了vlc的库,代码可以说是一句不懂。之后在csdn,博客园搜了好多,发现全景视频播放器好多都是基于安卓开发的,里面许多名词也都不懂。

大作业+期末的日子结束,回家后美赛练习过程中才又重新开始找资料看,看过一些音视频入门的文章后才知道音视频开发,进行的是采集、渲染、处理、传输等一系列的开发和应用,这时候才有大致框架,有了解封装,解码,音频流,视频流等相关概念。和做大作业的时候一样,习惯了直接上手,有什么问题再说,但其实首先做的应该是弄清楚大的框架和要求,再着眼细节

再之后就是知道了FFmpeg和OpenGL,抛下代码先学了相关的理论。

FFmpeg参考了雷神的博客和在b站的视频讲解
把库和函数列出来了:FFmpeg原理介绍与代码实现.
后来发现了一个写的比较系统的教程:ffmpeg和SDL教程.

OpenGL有一个很系统的讲解:LearnOpenGL CN.
万字长文详解如何用 Python 玩转 OpenGL | CSDN 博文精选.坐标系、投影、变换的概念都有

另外有一些找到的写的不错的文章:
最简单的视音频播放示例5:OpenGL播放RGB/YUV.有OpenGL渲染管线的步骤,窗口相关函数都有提到。
OpenGL正面剔除,深度测试,混合.有实际图片示例

还有《OpenGL编程指南》好像也多人推荐
但我的感觉是代码里好多东西这书和网站都没有讲到,像glut库里函数都是分散的百度出来的,很杂很散的样子。

除了这些,零零碎碎搜到的一些东西也大致记录了一下。

(1)FFmpeg新旧接口对照使用一览

FFmpeg新旧接口对照使用一览.

(2)libswscale图片像素数据处理类库

sws_getContext():初始化一个SwsContext。
sws_scale():处理图像数据。
sws_freeContext():释放一个SwsContext。

关于avpicture_fill与sws_scale:
关于avpicture_fill与sws_scale.对应于FFmpeg解码之后像素格式变换的代码
FFmpeg解码H264及swscale缩放详解.函数解释很细

(3)OpenGL相关记录

总结到了另一篇:全景视频播放器中OpenGL的相关记录.

(4)列队与线程

总结到了:列队与线程(全景视频播放器).

二、代码分析

主要的思路是将全景视频利用FFmpeg解封装解码后,将视频帧利用OpenGL渲染显示在一个球上。
值得注意的点:
(1)FFmpeg解码后还进行了像素帧的格式转换,好像OpenGL只能渲染RGB,yuv要转成rgb?

(2)线程及列队的使用
程序中使用两个线程分别实现解码和渲染,它们之间相互独立。但解码和渲染的速度我们无法控制,就通过列队实现均衡。我们自己设定列队的size,手动将解码出的数据存到列队中。解码出的视频帧保存在列队中,即向列队中输入使size增加,而OpenGL的渲染消耗列队中的元素,使size减小。当解码的数据将列队容量占满时,解码线程会稍作等待,等渲染的线程继续执行使列队中有剩余位置时,解码线程才会继续运行。
另外,临界区就是一段不会被中断的代码,可以避免数据冲突而使程序崩溃的情况。在本程序中,列队中元素增加和减少是都会在临界区进行操作。OpenGL用解码出的图像生成纹理后就已经消耗掉了列队中的元素,就可以离开临界区了,之后计算各点坐标将纹理对应成像素点并进行绘制。

(3)OpenGL绘制球体
通过设置经线和纬线的数量,我们可将一个球分成数个长方形,OpenGL基本的绘制单元是三角形,一个矩形分两个三角形,即6个顶点。每个顶点有xyz三维的空间坐标和(s, t)二维纹理坐标。空间坐标就是数学上的坐标表示,而纹理坐标是根据
[0,1]分份数算出来的。

(4)关于glut中的回调函数,以对该事件或条件进行响应

(5)两个线程打断点调试不能反映真实的程序运行情况,断点打下这个线程不动了另一个跑,但实际是两个线程都在跑。(或者确实可以真实反映程序的调试俺不知道)

具体代码及注释如下:

// glPanorama.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#define PI 3.1415926

GLfloat  xangle = 0.0;    //X 旋转量,之后可通过鼠标或键盘的控制改变
GLfloat  yangle = 0.0;    //Y 旋转量
GLfloat  zangle = 0.0;    //Z 旋转量

//交叉点的坐标
int cx = 0;
int cy = 0;

GLfloat  distance =0;//0或1100.0;   

GLuint  texturesArr;

int cap_H = 1;//必须大于0,且cap_H应等于cap_W
int cap_W = 1;//绘制球体时,每次增加的角度

float* verticals;
float* UV_TEX_VERTEX;

void init(void);
void reshape(int w, int h);
void display(void);
void getPointMatrix(GLfloat radius);

#define MAXSIZE 10//列队的最大容量

//定义了一个结构体Frame用于保存一帧视频画面、音频
typedef struct Vid_Frame {
	AVFrame *frame;//视频或音频的解码数据
	int serial;
	double pts;           /* presentation timestamp for the frame */
	double duration;      /* estimated duration of the frame */
	int64_t pos;          /* byte position of the frame in the input file */

	uint8_t *buffer;

	int width;
	int height;
	AVRational sar;
} Vid_Frame;

//FrameQueue不是用链表实现队列,而是用数组实现队列(环形缓冲区)。
typedef struct FrameQueue{
	Vid_Frame queue[MAXSIZE];队列元素,用数组模拟队列,其中就有AVFrame的解码数据
	int front;
	int rear;//后
	int size;//当前存储的节点个数(或者说,当前已写入的节点个数)
	CRITICAL_SECTION cs;//critica_section定义一个临界区对象cs,它是全局变量
}FrameQueue;

FrameQueue frame_queue;
//frame_queue是一个循环队列,解码的时候入队,渲染的时候出队


void initQueue(FrameQueue *q)//初始化列队
{
	int i;
	for (i = 0; i<MAXSIZE; i++){
		if (!(q->queue[i].frame = av_frame_alloc()))//为数组queue中的每个元素的frame(AVFrame*)的字段分配内存
			return ;
		q->queue[i].buffer = NULL;
	}

	q->front = 0;
	q->rear = 0;
	q->size = 0;
	InitializeCriticalSection(&q->cs);//初始化临界区,创立了一个叫cs的临界区对象
}

void deQueue(FrameQueue *q)
{
	free(q);
}

void init(void)
{
	initQueue(&frame_queue);//初始化列队
	//创建纹理,输入生成纹理的数量1,然后把它们储存在第二个参数的unsigned int数组中
	glGenTextures(1, &texturesArr); 
	glBindTexture(GL_TEXTURE_2D, texturesArr);//绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理

	//IplImage *image = cvLoadImage("5.png", 1);
	//生成一个纹理
	//glTexImage2D(GL_TEXTURE_2D, 0, 3, image->width, image->height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, image->imageData);

	//我们需要自己告诉OpenGL在纹理中采取哪种采样方式
	//纹理被放大和缩小时都使用了线性过滤
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);    //线形滤波
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);    //线形滤波


	glClearColor(0.0, 0.0, 0.0, 0.0);//设置当前使用的清除颜色值,这里为黑色,参数为RGBa
	glClearDepth(1);// 清除深度缓存 1.0是最大深度([0.0,1.0])
	glShadeModel(GL_SMOOTH);//设定opengl中绘制指定两点间其他点颜色的过渡模式,启用栅格化

	glEnable(GL_TEXTURE_2D);//允许采用 2D 纹理技术
	glEnable(GL_DEPTH_TEST);//启用深度测试,决定何时覆盖一个像素
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);//表示颜色和纹理坐标插补的质量。 如果角度更正参数插值不有效地支持由 OpenGL 实现,提示 GL_DONT_CARE 或 GL_FASTEST 可以导致简单线性插值的颜色和/或纹理坐标。

	getPointMatrix(500);//用函数得到顶点坐标和纹理坐标
}

void getPointMatrix(GLfloat radius)
{
	//开辟空间用来储存顶点坐标和纹理坐标,顶点坐标每个顶点有3个维度,纹理坐标2个(一个矩形中按6个顶点)
	verticals = new float[(180 / cap_H) * (360 / cap_W) * 6 * 3];
	UV_TEX_VERTEX = new float[(180 / cap_H) * (360 / cap_W) * 6 * 2];

	float x = 0;
	float y = 0;
	float z = 0;

	int index = 0;
	int index1 = 0;
	float r = radius;//球体半径
	double d = cap_H * PI / 180;//每次递增的弧度
	for (int i = 0; i < 180; i += cap_H) {

		double d1 = i * PI / 180;

		for (int j = 0; j < 360; j += cap_W) {

			double d2 = j * PI / 180;

			//获得球体上切分的超小片矩形的顶点坐标(两个三角形组成,所以有六点顶点)	
			verticals[index++] = (float)(x + r * sin(d1 + d) * cos(d2 + d));
			verticals[index++] = (float)(y + r * cos(d1 + d));
			verticals[index++] = (float)(z + r * sin(d1 + d) * sin(d2 + d));
			//获得球体上切分的超小片三角形的纹理坐标,纹理坐标范围是(0,1)
			UV_TEX_VERTEX[index1++] = (j + cap_W) * 1.0f / 360;
			UV_TEX_VERTEX[index1++] = (i + cap_H) * 1.0f / 180;

			verticals[index++] = (float)(x + r * sin(d1) * cos(d2));
			verticals[index++] = (float)(y + r * cos(d1));
			verticals[index++] = (float)(z + r * sin(d1) * sin(d2));

			UV_TEX_VERTEX[index1++] = j * 1.0f / 360;
			UV_TEX_VERTEX[index1++] = i * 1.0f / 180;

			verticals[index++] = (float)(x + r * sin(d1) * cos(d2 + d));
			verticals[index++] = (float)(y + r * cos(d1));
			verticals[index++] = (float)(z + r * sin(d1) * sin(d2 + d));

			UV_TEX_VERTEX[index1++] = (j + cap_W) * 1.0f / 360;
			UV_TEX_VERTEX[index1++] = i * 1.0f / 180;

			verticals[index++] = (float)(x + r * sin(d1 + d) * cos(d2 + d));
			verticals[index++] = (float)(y + r * cos(d1 + d));
			verticals[index++] = (float)(z + r * sin(d1 + d) * sin(d2 + d));

			UV_TEX_VERTEX[index1++] = (j + cap_W) * 1.0f / 360;
			UV_TEX_VERTEX[index1++] = (i + cap_H) * 1.0f / 180;

			verticals[index++] = (float)(x + r * sin(d1 + d) * cos(d2));
			verticals[index++] = (float)(y + r * cos(d1 + d));
			verticals[index++] = (float)(z + r * sin(d1 + d) * sin(d2));

			UV_TEX_VERTEX[index1++] = j * 1.0f / 360;
			UV_TEX_VERTEX[index1++] = (i + cap_H) * 1.0f / 180;

			verticals[index++] = (float)(x + r * sin(d1) * cos(d2));
			verticals[index++] = (float)(y + r * cos(d1));
			verticals[index++] = (float)(z + r * sin(d1) * sin(d2));

			UV_TEX_VERTEX[index1++] = j * 1.0f / 360;
			UV_TEX_VERTEX[index1++] = i * 1.0f / 180;
		}
	}
}


void reshape(int w, int h)
{
	glViewport(0, 0, (GLsizei)w, (GLsizei)h);
	glMatrixMode(GL_PROJECTION);//接下来要做投影相关的操作
	glLoadIdentity();//在进行变换前把当前矩阵设置为单位矩阵
	//glOrtho(-250.0, 250, -250.0, 250, -500, 500);
	//glFrustum(-250.0, 250, -250.0, 250, -5, -500);
	gluPerspective(60, (GLfloat)w / h, 1.0f, 1000.0f);    //设置投影矩阵
	glMatrixMode(GL_MODELVIEW);//对模型视景的操作
	glLoadIdentity();
}


//渲染时把解出来的数据从队列中取出生成新的纹理。
//渲染采用glDrawArrays函数,使用的GL_TRIANGLES参数,使用这个参数
//对于计算球的顶点坐标和纹理坐标来说不需要考虑很多,比较方便,就是点数过多的时候可能会影响渲染的效率。
void display(void)
{
	glLoadIdentity();//恢复初始坐标系   注释掉后会一直闪
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除两个缓冲区,clear all pixels像素

	gluLookAt(0, 0, distance, 0, 0, 500, 0, 1, 0);//在球里面的时候,500处的值越小,离得越近
	//前三个是脑袋的位置,中间三个是眼睛看向的位置,后三个是头顶朝向的位置
	printf("distance: %f \n", distance);

	//glRotatef(Angle, Xvector, Yvector, Zvector) 用于绕轴旋转物体。 
	//Angle 是一个用于指定旋转角度的数字(通常存储于变量中)。 
	//Xvector, Yvector 和 Zvector 这三个参数用于描述一条向量, 以规定物体的旋转轴。
	//在鼠标或键盘操控时,xangle会随之变化所以才会旋转
	glRotatef(xangle, 1.0f, 0.0f, 0.0f);    //绕X轴旋转
	glRotatef(yangle, 0.0f, 1.0f, 0.0f);    //绕Y轴旋转
	glRotatef(zangle, 0.0f, 0.0f, 1.0f);    //绕Z轴旋转

	EnterCriticalSection(&frame_queue.cs);

	printf("display size = %d \n", frame_queue.size);
	if (frame_queue.size > 0)
	{
		Vid_Frame *vp = &frame_queue.queue[frame_queue.front];//vp指向队首
		//glGenTextures(1, &texturesArr);
		glBindTexture(GL_TEXTURE_2D, texturesArr);

		//glTexImage2D第七第八个参数定义了源图的格式和数据类型,最后一个参数是真正的图像数据
		//当前绑定的纹理对象就会被附加上纹理图像,生成纹理
		glTexImage2D(GL_TEXTURE_2D, 0, 3, vp->width, vp->height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, vp->buffer);

		frame_queue.size--;
		frame_queue.front = (frame_queue.front + 1) % MAXSIZE;
	}

	LeaveCriticalSection(&frame_queue.cs);

	//glColor3f(1.0, 0.0, 0.0);  //绘制物体所使用的颜色
	// 启用顶点数组
	glEnableClientState(GL_VERTEX_ARRAY);
	//启用纹理数组     顶点坐标+纹理坐标
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);

	
	/*glVertexPointer指定顶点数组的位置,3表示每个顶点由三个量构成(x, y,z),
	GL_FLOAT表示每个量都是一个GLfloat类型的值。第三个参数0表示紧密排列。
	最后一个指明了数组实际的位置。*/
	glVertexPointer(3, GL_FLOAT, 0, verticals);
	glTexCoordPointer(2, GL_FLOAT, 0, UV_TEX_VERTEX);
	
	//绘制,第二个参数是从数组缓存中的哪一位开始绘制,一般为0。第三个参数为数组中顶点的数量。
	glDrawArrays(GL_TRIANGLES, 0, (180 / cap_H) * (360 / cap_W) * 6);

	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	glDisableClientState(GL_VERTEX_ARRAY);  // disable vertex arrays

	glFlush();//保证绘图命令将实际进行,而不是存储在缓冲区等待其他命令

}



DWORD WINAPI ThreadFunc(LPVOID n)
{
	AVFormatContext* pFormatCtx;
	int				i, videoindex;
	AVCodec* pCodec;//解码器
	AVCodecContext* pCodecCtx = NULL;

	char filepath[] = "cuc_ieschool.mp4";

	av_register_all();//注册组件
	avformat_network_init();//支持网络流
	pFormatCtx = avformat_alloc_context();//创建AVFormatContext结构体

	//该函数读取文件头并将有关文件格式的信息存储在我们提供的AVFormatContext结构中。
	//最后两个参数用于指定文件格式,缓冲区大小和格式选项,但是通过将其设置为NULL或0,libavformat将自动检测它们。
	if (avformat_open_input(&pFormatCtx, filepath, NULL, NULL) != 0){//打开一个输入流
		printf("Couldn't open input stream.(无法打开输入流)\n");
		return -1;
	}

	if (avformat_find_stream_info(pFormatCtx, NULL) < 0)//获取流信息
	{
		printf("Couldn't find stream information.(无法获取流信息)\n");
		return -1;
	}

	videoindex = -1;
	for (i = 0; i < pFormatCtx->nb_streams; i++){//找到流队列中,视频流所在位置
		if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
		{
			videoindex = i;
			break;
		}
	}
	if (videoindex == -1)
	{
		printf("Didn't find a video stream.(没有找到视频流)\n");
		return -1;
	}

	//查找解码器
	pCodecCtx = pFormatCtx->streams[videoindex]->codec;
	pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

	if (pCodec == NULL)
	{
		printf("Codec not found.(没有找到解码器)\n");
		return -1;
	}
	if (avcodec_open2(pCodecCtx, pCodec, NULL)<0)
	{
		printf("Could not open codec.(无法打开解码器)\n");
		return -1;
	}

	AVFrame	*pFrame;
	pFrame = av_frame_alloc();//分配视频帧,存储从packet中解码出来的原始视频帧
	int ret, got_picture;
	AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));

	AVFrame *pFrameBGR = NULL;
	pFrameBGR = av_frame_alloc();

	struct SwsContext *img_convert_ctx;

	int index = 0;
	while (av_read_frame(pFormatCtx, packet) >= 0)//return 0 if OK, < 0 on error or end of file
	{
		if (packet->stream_index == videoindex)//判断是不是来自视频流的数据包,不是会直接跳出
		{
			//解码,将数据包转换为帧。输入为packet,输出为original_video_frame
			//其中的pFrame存储解码视频的AVFrame。
			ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
			if (ret < 0)
			{
				printf("Decode Error.(解码错误)\n");
				continue;
			}
			if (got_picture)
			{
				index++;

flag_wait:
				if (frame_queue.size >= MAXSIZE)//如果解码过快列队存储不下,则此线程暂缓,让主线程渲染后再继续运行
				{
					printf("size = %d   I'm WAITING ... \n", frame_queue.size);
					Sleep(100);
					goto flag_wait;
				}

				EnterCriticalSection(&frame_queue.cs);//防止数据错乱

				Vid_Frame *vp;
				vp = &frame_queue.queue[frame_queue.rear];//vp指向列队的尾部,自动就知道rear?

				//vp->frame->pts = pFrame->pts;

				/* alloc or resize hardware picture buffer */
				//令vp的buffer的size width height都等于pFrame,就是给存储数据的buffer赋值
				if (vp->buffer == NULL || vp->width != pFrame->width || vp->height != pFrame->height)
				{
					if (vp->buffer != NULL)
					{
						av_free(vp->buffer);
						vp->buffer = NULL;
					}
					
					//int iSize = avpicture_get_size(AV_PIX_FMT_BGR24, pFrame->width, pFrame->height);
					int iSize = av_image_get_buffer_size(AV_PIX_FMT_BGR24, pFrame->width, pFrame->height, 1);
					av_free(vp->buffer);
					vp->buffer = (uint8_t *)av_mallocz(iSize);

					vp->width = pFrame->width;
					vp->height = pFrame->height;

				}

				av_image_fill_arrays(vp->frame->data, vp->frame->linesize, vp->buffer, AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height, 1);
				//frame和buffer都是已经申请到的一段内存, 会将frame的数据按BGR24的格式自动"关联"到buffer。
				//avpicture_fill((AVPicture *)vp->frame, vp->buffer, AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);

				if (vp->buffer)
				{
					//用sws_getContext初始化SwsContex
					/*srcW:源图像的宽
						srcH:源图像的高
						srcFormat:源图像的像素格式
						dstW:目标图像的宽
						dstH:目标图像的高
						dstFormat:目标图像的像素格式
						flags:设定图像拉伸使用的算法*/
					img_convert_ctx = sws_getContext(vp->width, vp->height, (AVPixelFormat)pFrame->format, vp->width, vp->height,
						AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL); //AV_PIX_FMT_YUV420P, AV_PIX_FMT_BGR24
					//转换一帧图像,转换完成的数据保存到了vp,也自动到了buffer里面。
					sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, vp->height, vp->frame->data, vp->frame->linesize);
					//释放SwsContext结构体
					sws_freeContext(img_convert_ctx);
					//vp->pts = pFrame->pts;
				}
					
				frame_queue.size++;
				frame_queue.rear = (frame_queue.rear + 1) % MAXSIZE;

				LeaveCriticalSection(&frame_queue.cs);

				
			}
		}
		av_free_packet(packet);
	}

	avcodec_close(pCodecCtx);
	avformat_close_input(&pFormatCtx);

	return 0;
}

void reDraw(int millisec)
{
		glutTimerFunc(millisec, reDraw, millisec);
		glutPostRedisplay();
}

void keyboard(unsigned char key, int x, int y)
{
	switch (key)
	{
	case 'x':        //当按下键盘上x时,以沿X轴旋转为主
		xangle += 1.0f;    //设置旋转增量
		break;
	case 'X':
		xangle -= 1.0f;    //设置旋转增量
		break;
	case 'y':
		yangle += 1.0f;
		break;
	case 'Y':
		yangle -= 1.0f;
		break;
	case 'z':
		zangle += 1.0f;
		break;
	case 'Z':
		zangle -= 1.0f;
		break;
	case 'd':
		distance += 10.0f;
		break;
	case 'D':
		distance -= 10.0f;
		break;
	default:
		return;
	}
	glutPostRedisplay();    //重绘函数
}

//处理鼠标点击
void Mouse(int button, int state, int x, int y)
{
	if (state == GLUT_DOWN) //第一次鼠标按下时,记录鼠标在窗口中的初始坐标
	{
		//记住鼠标点击后光标坐标
		cx = x;
		cy = y;
	}
}

//处理鼠标拖动
void onMouseMove(int x, int y)
{
	float offset =0.3;// 0.18;值越大,到相同位置转的角度就越大,即需要拖得越长才能到相应的位置
	//计算拖动后的偏移量,然后进行xy叠加减
	yangle -= ((x - cx) * offset);

	if ( y > cy) {//往下拉
		xangle += ((y - cy) * offset);
	}
	else if ( y < cy) {//往上拉
		xangle += ((y - cy) * offset);
	}

	glutPostRedisplay();

	//保存好当前拖放后光标坐标点
	cx = x;
	cy = y;
}

int main(int argc, char* argv[])

{
	printf("可通过按键或者鼠标控制视频旋转\n");
	glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);//创建窗口的模式:单缓冲区和RGB模式、使用深度缓存
	glutInitWindowSize(1280, 720);//窗口大小
	glutInitWindowPosition(50, 50);//窗口左上角屏幕位置
	glutCreateWindow("OpenGL全景");
	init();
	glutReshapeFunc(reshape); //窗口大小改变或窗口位置改变时候要调用的函数
	glutDisplayFunc(display); //指定当窗口内容需要重绘时要调用的函数,在窗口刚打开,弹出,改变位置,点击等,都会触发事件。
	glutKeyboardFunc(keyboard);//当一个能生成ASCII字符的键按下时,keyboard函数会被调用
	glutMouseFunc(Mouse);//当一个鼠标按钮按下或释放,调用Mouse函数
	glutMotionFunc(onMouseMove);//当鼠标按下并在窗口移动鼠标时,调用onMouseMove函数
	glutTimerFunc(25, reDraw,1);//原最后一个参数为25

	HANDLE hThrd = NULL;
	DWORD threadId;
	hThrd = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId);//创建一个新线程
	
    //该函数才真正进入GLUT事件循环,语句阻塞在此。
	//当对应的事件发生时,被注册的回调函数如glutDisplayFunc中注册的就会被调用。
	glutMainLoop();//无限执行的循环,判断窗口是否需要重绘

	//WaitForSingleObject(hThrd, INFINITE);

	if (hThrd)
	{
		CloseHandle(hThrd);//线程句柄生命周期结束
	}

	return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值