龙云尧个人博客,转载请注明出处。
CSDN地址:http://blog.csdn.net/michael753951/article/details/72810534
个人blog地址:http://yaoyl.cn/nehexue-xi-bi-ji-shi/
概述
本部分博客将以nehe教程第2课,笔记(三)为蓝本,将Windows中完成的基础实验在Ubuntu中进行实现。
在【Ubuntu环境配置】中我们已经对Ubuntu中的OpenGL环境进行了配置,并且完成了最基础的茶壶demo,接下来我们将进行实验相关的后续开发。
需求分析
因为实验中我们需要终端接收到的数据能够在图形界面中实时显示出来,这里我们使用nehe教程的第二课内容,绘制一个矩形作为进度条,起始为0%,最高为100%。接着我们将让这个进度条能够对传输过来的信号产生反馈。将整个过程进行拆分,我们可以按照如下步骤进行实现。
- 构建一个OpenGL窗口,能够根据本地按键实现进度条控制
- 让OpenGL的窗口能够接收其他终端发送过来的消息
- 让OpenGL窗口对接收到的信息进行一定的实时反馈(比如进度条变换)
实验
OpenGL窗口搭建
本次使用的代码是以nehe教程第二课中,Linux代码为蓝本,进行修改实现的。【代码链接】
首先我们将窗口显示中的三角形去掉,留下一个长方形,同时将长方形的右边两个点和左边两个点重合以做出进度条为0%的感觉。代码如下:
按钮控制的实现
我在初始化InitGL的时候,将square_len初始化为0,当有按键触发的时候,square_len++,这样就能够完成进度条的前进工作。
在原始代码中,我们可以看到main函数中,有一个glutKeyboardFunc
方法,传入了keyPressed
的地址,在keyPressed
中,定义了使用ESC
按钮进行退出的方法。我们将在这里进行尝试,试试方向键左和方向键右能不能让窗口出现一些反馈。
在经过不短的一段时间的寻找之后,我终于找到了在OpenGL中,各种按键的键值是在glut.h
中预定义好的。
参考一片CSDN博客【pengl键盘控制一】,我们可以发现在本次程序中,ESC按键确实也刚好是27,这是不是也就意味着我们可以直接按照上面的方法进行修改了?首先我们将ESC的宏定义值修改为102(十进制,对应0x66),尝试使用左键退出窗体程序。
但是很意外的,没有成功。是不是按键本身的键值并不是102?
我对代码进行进一步修改,当有按键活动的时候,记录下来当前按键的键值,将其存进本地文件中。(亲测不能直接printf,因为根本不会显示出来,至于原因待会会有解释)代码如下:
/* The function called whenever a key is pressed. */
void keyPressed(unsigned char key, int x, int y) {
/* avoid thrashing this procedure */
//usleep(100);
fp = fopen("key_value.txt", "a+"); // a+意味着在文本最后追加
fprintf(fp, "%d\n", key);
fclose(fp);
/* If escape is pressed, kill everything. */
if (key == ESCAPE) {
/* shut down our window */
glutDestroyWindow(window);
/* exit the program...normal termination. */
exit(0);
}
}
尝试按下F1~F12的按键,以及上下左右等按键,以及数字按键之后,我们发现txt文档中只记录下来了数字键值,根本没有其他的键值。
为了解决这个问题,我特地打开了nehe的lesson10的linux代码(因为这一课会用到方向键进行控制)。发现原来上下左右这类按键需要在main函数中使用glutSpecialFunc
方法,传入一个操作函数进行操作。这里我定义了一个specialKeyPressed
方法。在尝试获取键值,并且成功之后,我开始在这里进行进度条的控制。
void specialKeyPressed(int key, int x, int y) {
//usleep(100);
/*
fp = fopen("key_value.txt", "a+");
fprintf(fp, "%d\n", key);
fclose(fp);
*/
switch(key) {
case GLUT_KEY_LEFT:
square_len--;
if(square_len <= 0) square_len = 0;
break;
case GLUT_KEY_RIGHT:
square_len++;
if(square_len >= 100) square_len = 100;
break;
}
}
为了避免越界,我们需要将square_len控制在0-100之间。同时我们直接使用glut中宏定义的键值,进行按键判断(我已经对键值进行过测试。发现和宏定义的键值确实一致)。
到这里,我们完成了本次demo的step1,一个使用按键进行进度条控制的OpenGL窗口已经构建成功。
在OpenGL创建的控制台窗口中使用网络协议传输
首先,我们需要知道,在之前的Socket编程中,我们使用的一直都是控制台窗口程序进行的测试,但是在本次实验中,我的理想状态是使用OpenGL建立的窗口作为server,新建一个控制台作为client,然后实验中使用client对server进行控制。
我先定义了一个tcp_server.h头文件。
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#define MYPORT 8887
#define QUEUE 20
bool tcp_server_init(int &server_sockfd, int &conn) {
///定义sockfd
server_sockfd = socket(AF_INET,SOCK_STREAM, 0);
///定义sockaddr_in
struct sockaddr_in server_sockaddr;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(MYPORT);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
///bind,成功返回0,出错返回-1
if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1) {
perror("bind");
return 0;
}
///listen,成功返回0,出错返回-1
if(listen(server_sockfd,QUEUE) == -1) {
perror("listen");
return 0;
}
///客户端套接字
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
///成功返回非负描述字,出错返回-1
conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length);
if(conn<0) {
perror("connect");
return 0;
}
//printf("before_conn\n");
return 1;
}
bool tcp_server_close(int &server_sockfd, int &conn) {
close(conn);
close(server_sockfd);
return 1;
}
方法一,DrawGLScene中接收消息
首先我尝试直接在main函数中没有进入glutMainLoop之前,建立tcp连接。(tcp_server_init(server_sockfd, conn);
方法在上面已经给出来了)然后在DrawGLScene中接受消息,代码如下。
/* The main drawing function. */
void DrawGLScene() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer
glLoadIdentity(); // Reset The View
memset(buffer,0,sizeof(buffer));
int len = recv(conn, buffer, sizeof(buffer),0);
for(int i = 0; i<len; ++i) {
if(buffer[0] == 0x66) {
square_len++;
if(square_len >= 100) square_len = 100;
} else if(buffer[0] == 0x64) {
square_len--;
if(square_len <= 0) square_len = 0;
}
}
fputs(buffer, stdout);
send(conn, buffer, len, 0);
glTranslatef(-1.5f,0.0f,-6.0f); // Move Right 3 Units
// draw a square (quadrilateral)
glBegin(GL_QUADS); // start drawing a polygon (4 sided)
glVertex3f(-1.0f, 1.0f, 0.0f); // Top Left
glVertex3f(-1.0f+square_len*0.05, 1.0f, 0.0f); // Top Right
glVertex3f(-1.0f+square_len*0.05,-1.0f, 0.0f); // Bottom Right
glVertex3f(-1.0f,-1.0f, 0.0f); // Bottom Left
glEnd(); // done with the polygon
// swap buffers to display, since we're double buffered.
glutSwapBuffers();
}
运行结果如图。
刚看到这种情况的时候我以为是程序运行出错,于是经过很长一段时间的搜索才找到了一种解决办法,这个办法我待会会说,这里先说目前的这个问题的解决办法。
首先我们要知道,为什么会出现这个问题,它其实是TCP_\server_init函数中,执行到conn = accept(server_sockfd, (struct sockaddr*)&client\_addr, &length);
的时候,在那里停止了,结果导致glut没有继续绘制窗口,最终造成我们看到的窗口很奇怪。对accept稍作了解便知道,这个方法是提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。(参见【socket编程之accept()函数】)这个时候,所以,OpenGL中绘制的窗口会这么奇怪,其实就只是因为server在等待客户端的连接,所以才会继续没有往下执行而已。
我们打开之前的tcp编程中,tcp_client_demo2项目,编译并且运行,和server成功建立上连接成功,理想中,这个时候应该是没有问题了,但是运行以后,显示依然有问题。
这里我猜测是因为recv阻塞了整个进程,造成后续画笔绘制没办法绘制。因为当我在client中发送一个f之后,server中的窗口就立刻能够正常移动进度条了。
最终的代码如下:
//
// This code was created by Jeff Molofee '99 (ported to Linux/GLUT by Richard Campbell '99)
//
// If you've found this code useful, please let me know.
//
// Visit me at www.demonews.com/hosted/nehe
// (email Richard Campbell at [email protected])
//
#include <GL/glut.h> // Header File For The GLUT Library
#include <GL/gl.h> // Header File For The OpenGL32 Library
#include <GL/glu.h> // Header File For The GLu32 Library
#include <unistd.h> // Header File For sleeping.
#include <stdio.h>
#include "tcp_server.h"
/* ASCII code for the escape key. */
#define ESCAPE 27
#define VK_LEFT 37
#define VK_RIGHT 39
FILE *fp = NULL;//需要注意
int square_len;
/* TCP 链接 */
#define BUFFER_SIZE 1024
int server_sockfd, conn;
char buffer[BUFFER_SIZE];
bool server_init_flag;
/* The number of our GLUT window */
int window;
/* A general OpenGL initialization function. Sets all of the initial parameters. */
void InitGL(int Width, int Height) { // We call this right after our OpenGL window is created.
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // This Will Clear The Background Color To Black
glClearDepth(1.0); // Enables Clearing Of The Depth Buffer
glDepthFunc(GL_LESS); // The Type Of Depth Test To Do
glEnable(GL_DEPTH_TEST); // Enables Depth Testing
glShadeModel(GL_SMOOTH); // Enables Smooth Color Shading
glMatrixMode(GL_PROJECTION);
glLoadIdentity(); // Reset The Projection Matrix
gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f); // Calculate The Aspect Ratio Of The Window
glMatrixMode(GL_MODELVIEW);
square_len = 0;
server_init_flag = false;
}
/* The function called when our window is resized (which shouldn't happen, because we're fullscreen) */
void ReSizeGLScene(int Width, int Height) {
if (Height==0) // Prevent A Divide By Zero If The Window Is Too Small
Height=1;
glViewport(0, 0, Width, Height); // Reset The Current Viewport And Perspective Transformation
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
glMatrixMode(GL_MODELVIEW);
}
/* The main drawing function. */
void DrawGLScene() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer
glLoadIdentity(); // Reset The View
memset(buffer,0,sizeof(buffer));
int len = recv(conn, buffer, sizeof(buffer),0);
for(int i = 0; i<len; ++i) {
if(buffer[0] == 0x66) {
square_len++;
if(square_len >= 100) square_len = 100;
} else if(buffer[0] == 0x64) {
square_len--;
if(square_len <= 0) square_len = 0;
}
}
fputs(buffer, stdout);
send(conn, buffer, len, 0);
glTranslatef(-1.5f,0.0f,-6.0f); // Move Right 3 Units
// draw a square (quadrilateral)
glBegin(GL_QUADS); // start drawing a polygon (4 sided)
glVertex3f(-1.0f, 1.0f, 0.0f); // Top Left
glVertex3f(-1.0f+square_len*0.05, 1.0f, 0.0f); // Top Right
glVertex3f(-1.0f+square_len*0.05,-1.0f, 0.0f); // Bottom Right
glVertex3f(-1.0f,-1.0f, 0.0f); // Bottom Left
glEnd(); // done with the polygon
// swap buffers to display, since we're double buffered.
glutSwapBuffers();
}
/* The function called whenever a key is pressed. */
void keyPressed(unsigned char key, int x, int y) {
/* If escape is pressed, kill everything. */
if (key == ESCAPE) {
/* shut down our window */
glutDestroyWindow(window);
if(!tcp_server_close(server_sockfd, conn)) {
exit(1);
}
/* exit the program...normal termination. */
exit(0);
}
}
void specialKeyPressed(int key, int x, int y) {
//usleep(100);
switch(key) {
case GLUT_KEY_LEFT:
square_len--;
if(square_len <= 0) square_len = 0;
break;
case GLUT_KEY_RIGHT:
square_len++;
if(square_len >= 100) square_len = 100;
break;
}
}
int main(int argc, char **argv) {
/* Initialize GLUT state - glut will take any command line arguments that pertain to it or
X Windows - look at its documentation at http://reality.sgi.com/mjk/spec3/spec3.html */
glutInit(&argc, argv);
/* Select type of Display mode:
Double buffer
RGBA color
Alpha components supported
Depth buffer */
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH);
/* get a 640 x 480 window */
glutInitWindowSize(640, 480);
/* the window starts at the upper left corner of the screen */
glutInitWindowPosition(0, 0);
/* Open a window */
window = glutCreateWindow("我的第一个长方形进度条demo");
/* Register the function to do all our OpenGL drawing. */
glutDisplayFunc(&DrawGLScene);
// 全屏
/* Go fullscreen. This is the soonest we could possibly go fullscreen. */
//glutFullScreen();
/* Even if there are no events, redraw our gl scene. */
glutIdleFunc(&DrawGLScene);
/* Register the function called when our window is resized. */
glutReshapeFunc(&ReSizeGLScene);
/* Register the function called when the keyboard is pressed. */
glutKeyboardFunc(&keyPressed);
/* Register the function called when special keys (arrows, page down, etc) are pressed. */
glutSpecialFunc(&specialKeyPressed);
/* Initialize our window. */
InitGL(640, 480);
// 尝试在glutMainLoop之外建立tcp连接
tcp_server_init(server_sockfd, conn);
/* Start Event Processing Engine */
glutMainLoop();
return 1;
}
方法二,在空闲回调函数中接受消息
使用空闲回调函数glutIdleFunc
。参考博客【Idle回调函数的使用】(原出处未知),以及一篇很有用的博客【OpenGL下图形的交互控制[转]】
在nehe的所有教程中,图像的转变均是在DrawGLScene
实现的,这种方法在单纯的图像变换,不存在任何等待的时候,是没有问题的。但是一旦需要等待的时候,就会出现之前截图中那样,图片绘制上出现问题,画面显示会很不流畅,我刚开始接触的时候也以为是自己的代码写的有问题。为了解决这个问题,我们可以试试其他的方法。
在博客中我们也知道,一般更新场景数据的时候,使用的就是Idle Callback。刚好符合我们的需求。下面我将说明一下代码的编写。
首先在main函数中已经定义好的部分回调函数后面加上一行空闲回调函数。
//tcp_server_init(server_sockfd, conn);
glutIdleFunc(&IdleFun); // idle 回调函数
有趣的是,我们发现,DrawGLScene
方法也是在空闲回调函数中执行的。不过在main函数中出现两个DrawGLScene函数的时候,对程序的执行并不影响。
接下来我们定义IdleFun
。
void IdleFun() { // 回调函数,在控制台中的一些操作,需要在本部分进行控制
//printf("test\n");
if(!server_init_flag) {
//square_len++;
//if(square_len >= 100) square_len = 0;
//printf("init\n");
if(tcp_server_init(server_sockfd, conn)) printf("success\n");
else printf("false\n");
server_init_flag = true;
glutPostRedisplay();
} else {
memset(buffer,0,sizeof(buffer));
int len = recv(conn, buffer, sizeof(buffer),0);
for(int i = 0; i<len; ++i) {
if(buffer[0] == 0x66) {
square_len++;
if(square_len >= 100) square_len = 100;
} else if(buffer[0] == 0x64) {
square_len--;
if(square_len <= 0) square_len = 0;
}
}
fputs(buffer, stdout);
send(conn, buffer, len, 0);
glutPostRedisplay();
}
}
为了避免反复创建tcp连接,我们使用一个全局bool变量来标志是否已经创建连接。并且我们在刷新完场景数据之后,一定要调用glutPostRedisplay
刷新当前屏幕,否则当前屏幕不会自动刷新,你也将看不到场景变化。具体参考可见【[译]GLUT教程 - glutPostRedisplay函数】。(这是一篇很好的博客)最终的参考代码如下。
#include <GL/glut.h> // Header File For The GLUT Library
#include <GL/gl.h> // Header File For The OpenGL32 Library
#include <GL/glu.h> // Header File For The GLu32 Library
#include <unistd.h> // Header File For sleeping.
#include <stdio.h>
#include "tcp_server.h"
/* ASCII code for the escape key. */
#define ESCAPE 27
#define VK_LEFT 37
#define VK_RIGHT 39
FILE *fp = NULL;//需要注意
int square_len;
/* TCP 链接 */
#define BUFFER_SIZE 1024
int server_sockfd, conn;
char buffer[BUFFER_SIZE];
bool server_init_flag;
/* The number of our GLUT window */
int window;
/* A general OpenGL initialization function. Sets all of the initial parameters. */
void InitGL(int Width, int Height) { // We call this right after our OpenGL window is created.
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // This Will Clear The Background Color To Black
glClearDepth(1.0); // Enables Clearing Of The Depth Buffer
glDepthFunc(GL_LESS); // The Type Of Depth Test To Do
glEnable(GL_DEPTH_TEST); // Enables Depth Testing
glShadeModel(GL_SMOOTH); // Enables Smooth Color Shading
glMatrixMode(GL_PROJECTION);
glLoadIdentity(); // Reset The Projection Matrix
gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f); // Calculate The Aspect Ratio Of The Window
glMatrixMode(GL_MODELVIEW);
square_len = 0;
server_init_flag = false;
//if(!tcp_server_init(server_sockfd, conn)) exit(1);
}
/* The function called when our window is resized (which shouldn't happen, because we're fullscreen) */
void ReSizeGLScene(int Width, int Height) {
if (Height==0) // Prevent A Divide By Zero If The Window Is Too Small
Height=1;
glViewport(0, 0, Width, Height); // Reset The Current Viewport And Perspective Transformation
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
glMatrixMode(GL_MODELVIEW);
}
/* The main drawing function. */
void DrawGLScene() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer
glLoadIdentity(); // Reset The View
glTranslatef(-1.5f,0.0f,-6.0f); // Move Right 3 Units
// draw a square (quadrilateral)
glBegin(GL_QUADS); // start drawing a polygon (4 sided)
glVertex3f(-1.0f, 1.0f, 0.0f); // Top Left
glVertex3f(-1.0f+square_len*0.05, 1.0f, 0.0f); // Top Right
glVertex3f(-1.0f+square_len*0.05,-1.0f, 0.0f); // Bottom Right
glVertex3f(-1.0f,-1.0f, 0.0f); // Bottom Left
glEnd(); // done with the polygon
// swap buffers to display, since we're double buffered.
glutSwapBuffers();
}
/* The function called whenever a key is pressed. */
void keyPressed(unsigned char key, int x, int y) {
/* avoid thrashing this procedure */
//usleep(100);
/* If escape is pressed, kill everything. */
if (key == ESCAPE) {
/* shut down our window */
glutDestroyWindow(window);
if(!tcp_server_close(server_sockfd, conn)) {
exit(1);
}
/* exit the program...normal termination. */
exit(0);
}
}
void specialKeyPressed(int key, int x, int y) {
//usleep(100);
switch(key) {
case GLUT_KEY_LEFT:
square_len--;
if(square_len <= 0) square_len = 0;
break;
case GLUT_KEY_RIGHT:
square_len++;
if(square_len >= 100) square_len = 100;
break;
}
}
void IdleFun() { // 回调函数,在控制台中的一些操作,需要在本部分进行控制
//printf("test\n");
if(!server_init_flag) {
//square_len++;
//if(square_len >= 100) square_len = 0;
//printf("init\n");
if(tcp_server_init(server_sockfd, conn)) printf("success\n");
else printf("false\n");
server_init_flag = true;
glutPostRedisplay();
} else {
memset(buffer,0,sizeof(buffer));
int len = recv(conn, buffer, sizeof(buffer),0);
for(int i = 0; i<len; ++i) {
if(buffer[0] == 0x66) {
square_len++;
if(square_len >= 100) square_len = 100;
} else if(buffer[0] == 0x64) {
square_len--;
if(square_len <= 0) square_len = 0;
}
}
fputs(buffer, stdout);
send(conn, buffer, len, 0);
}
glutPostRedisplay();
}
int main(int argc, char **argv) {
/* Initialize GLUT state - glut will take any command line arguments that pertain to it or
X Windows - look at its documentation at http://reality.sgi.com/mjk/spec3/spec3.html */
glutInit(&argc, argv);
/* Select type of Display mode:
Double buffer
RGBA color
Alpha components supported
Depth buffer */
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH);
/* get a 640 x 480 window */
glutInitWindowSize(640, 480);
/* the window starts at the upper left corner of the screen */
glutInitWindowPosition(0, 0);
/* Open a window */
window = glutCreateWindow("我的第一个长方形进度条demo");
/* Register the function to do all our OpenGL drawing. */
glutDisplayFunc(&DrawGLScene);
// 全屏
/* Go fullscreen. This is the soonest we could possibly go fullscreen. */
//glutFullScreen();
/* Even if there are no events, redraw our gl scene. */
glutIdleFunc(&DrawGLScene);
/* Register the function called when our window is resized. */
glutReshapeFunc(&ReSizeGLScene);
/* Register the function called when the keyboard is pressed. */
glutKeyboardFunc(&keyPressed);
//tcp_server_init(server_sockfd, conn);
glutIdleFunc(&IdleFun); // idle 回调函数
/* Register the function called when special keys (arrows, page down, etc) are pressed. */
glutSpecialFunc(&specialKeyPressed);
/* Initialize our window. */
InitGL(640, 480);
/* Start Event Processing Engine */
glutMainLoop();
return 1;
}
完成这一步之后,基本上你就能够完成一个能够通过网络通信控制窗口界面的小demo了。