OpenGL学习小例子

——基于旋转,拾取,缩放,着色的实现

要求:根据OpenGL提供的直线,多边形绘制算法(橡皮筋效果),实现基于鼠标交互的卡通人物和其他环境物体的设计与绘制。使用颜色填充与反走样技术对卡通人物外貌以及衣着进行绘制,其他物体的绘制。实现对卡通人物或物体轮廓的交互控制,点击鼠标左键可以对人物或者物体进行拖拽移动调整。按“↑”按键能够实现卡通人物绕坐标原点(或指定点)进行旋转,按“z”键可实现对选中的人物或者物体进行放缩。 附加要求:选中其中的一个多边形区域,点击鼠标右键,弹出一个菜单,可以对该区域进行不同颜色的选择。可以设计发型、衣服的模版,当作文件进行存储,可以在窗口最右边设计一个模板库,显示保存的发型与衣服,拖拽到卡通人物上可以为卡通人物进行发型或者衣服的替换。

过程解析:

1.绘制身体的各个部分:脸、耳朵、眉毛等的单独函数;而且有些部位是可以移动的,所以把坐标传入作为函数的参数

2.绘制函数 drawObjects(GLenum mode),参数是模式,选择模式或者是绘制模式。当模式是绘制时,选择对应部位的颜色,并且把部位绘制出来;当模式是选择时,把部位的标号加载到名字栈中。然后在着色,绘制各个部位

3.绘制的最终回调函数myDisplay,作为glutDisplayFunc的回调函数,即最终绘制时所调用的函数,在本函数里面,先清除颜色缓存,然后转化为单位阵,在将旋转中心转移到中心,否则就是围绕着左下角的坐标原点旋转,写好旋转的函数之后,只需要根据上下的按键来改变theta的值,再然后重绘一次旋转的图形,就做到按键控制图形的旋转,再在渲染模式下绘制图形drawObjects(GL_RENDER);

4.myinit方法是把背景颜色设定成黑色

5.myReshape是为了在改变窗口的时候而设定的,首先调用函数glViewport函数来定义视口,然后进行窗口的裁剪,之后再开启反走样,保证视图变化之后的效果较好

6.myReshape是为了在改变窗口的时候而设定的,首先调用函数glViewport函数来定义视口,然后进行窗口的裁剪,之后再开启反走样,保证视图变化之后的效果较好

7.processHits方法是配合名字栈来使用,名字栈内存储着五官的标号,buffer即为名字栈,name代表着五官的标号,里面有两个for循环,第一个循环,hits代表转移到缓冲区中已命中的记录数,即已命中的记录条数

8.myMouseMove,首先是根据鼠标指定的位置选中区域,然后利用脸的颜色重绘原器官的位置,之后再在鼠标停留的位置调glutPostRedisplay函数重新绘制一次全图,造成一种移动的感官

9.myMouse把selectBuf作为拾取缓冲区,每一条命中记录都会存在这个缓冲区中,开启选择模式,初始化名字栈,并且在透视投影的条件下对图元进行操作,把矩阵保存,在初始化一个新的矩阵,这样做为了避免对矩阵操作之后对以后的矩阵变化造成困难,gluPickMatrix函数根据鼠标坐标x,y并圈定一块区域来进行操作,选择该区域的图元,drawObjects(GL_SELECT)函数,在选择模式下调用,把名字压栈并且绘制一次

10.创建菜单,根据glutAddMenuEntry函数来给菜单添加条目,第二个参数value的作用是,当你选择一项时,该值就会返回给glutCreateMenu里调用的函数。而在函数main_menu里面,选择的颜色参数传进去,在根据拾取图元的标记,把相应的颜色赋值给五官的颜色变量,最后调用重绘函数glutPostRedisplay,从而达到改变颜色的目的

代码

#include "stdafx.h"

//隐藏控制台
#pragma comment( linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")

//五官的标记
#define FACE 1  
#define EARS 2 
#define BROW 3
#define LEFT_EYE 4
#define RIGHT_EYE 5
#define NOSE 6
#define MOUTH 7
#define HAIR 8  
#define CROWN 9
#define CAP 10

//给个部位的颜色
static int FACE_COLOR = 7;
static int EARS_COLOR = 5;
static int BROW_COLOR = 0;
static int EYES_COLOR = 0;
static int NOSE_COLOR = 6;
static int MOUTH_COLOR = 1;
static int HAIR_COLOR = 3;
static int CROWN_COLOR = 6;
static int CAP_COLOR = 4;

static GLfloat theta = 0;//旋转的角度

static GLfloat scaleX = 1.0;
static GLfloat scaleY = 1.0;
static GLfloat scaleZ = 1.0;//缩放的尺寸

int select_part = 0; //点击时选中区域

static GLfloat colors[8][3] = {
    { 0.0, 0.0, 0.0 }, { 1.0, 0.0, 0.0 }, { 0.0, 1.0, 0.0 }, { 0.0, 0.0, 1.0 },
    { 0.0, 1.0, 1.0 }, { 1.0, 0.0, 1.0 }, { 1.0, 1.0, 0.0 }, { 1.0, 1.0, 1.0 } };
//黑色0  红色1  绿色2  蓝色3  青色4  紫色5  黄色6  白色7 
#define SIZE 512 
#define WIN_WIDTH 700  
#define WIN_HEIGHT 500  //裁剪窗口的大小
#define VIEW_WIDTH 700  
#define VIEW_HEIGHT 500  //视图窗口的大小,单位是像素

//画脸
void drawFace(){
    glBegin(GL_POLYGON);
    glVertex2f(150, 300);
    for (int i = 150; i <= 350; i = i + 1)
        glVertex2f(i, 0.008*(i - 250)*(i - 250) + 120);
    glVertex2f(350, 300);
    glEnd();
}
//画耳朵
void drawEars(){
    //right ear
    glBegin(GL_POLYGON);
    glVertex2f(150, 270);
    glVertex2f(140, 280);
    glVertex2f(140, 230);
    glVertex2f(150, 240);
    glEnd();
    //left ear
    glBegin(GL_POLYGON);
    glVertex2f(350, 270);
    glVertex2f(360, 280);
    glVertex2f(360, 230);
    glVertex2f(350, 240);
    glEnd();
}
//画眉毛
void drawBrow(){
    glLineWidth(10);
    glBegin(GL_LINES);
    glVertex2f(180, 290);
    glVertex2f(210, 270);
    glVertex2f(290, 270);
    glVertex2f(320, 290);
    glEnd();
}
//画眼睛
int rex = 200;
int rey = 250;
void drawRightEye(int Ax, int Ay){
    rex = Ax;
    rey = Ay;
    glBegin(GL_POLYGON);
    glVertex2f(rex - 20, rey + 20);
    glVertex2f(rex - 20, rey);
    glVertex2f(rex + 10, rey - 20);
    glVertex2f(rex + 10, rey);
    glEnd();
}
int lex = 300;
int ley = 250;
void drawLeftEye(int Ax, int Ay){
    lex = Ax;
    ley = Ay;
    glBegin(GL_POLYGON);
    glVertex2f(lex - 10, ley);
    glVertex2f(lex - 10, ley - 20);
    glVertex2f(lex + 20, ley);
    glVertex2f(lex + 20, ley + 20);
    glEnd();
}
//画鼻子
int nx = 250;
int ny = 220;
void drawNose(int Ax, int Ay){
    nx = Ax;
    ny = Ay;
    glBegin(GL_TRIANGLES);
    glVertex2f(nx + 0, ny + 10);
    glVertex2f(nx - 10, ny - 10);
    glVertex2f(nx + 10, ny - 10);
    glEnd();
}

//画嘴巴
int mx = 250;
int my = 170;
void drawMouth(int Ax, int Ay){
    mx = Ax;
    my = Ay;
    glBegin(GL_POLYGON);
    glVertex2f(mx - 20, my + 10);
    glVertex2f(mx - 10, my - 10);
    glVertex2f(mx + 10, my - 10);
    glVertex2f(mx + 20, my + 10);
    glEnd();
}
//画头发
void drawHair(){
    glBegin(GL_TRIANGLE_FAN);
    glVertex2f(160, 300);
    glVertex2f(140, 270);
    for (int i = 100; i <= 400; i = i + 10)
        glVertex2f(i, 400 - 0.004444*(i - 250)*(i - 250));//
    glVertex2f(380, 270);
    glEnd();
}

//画皇冠
int crownx = 250;
int crowny = 436;
void drawCrown(int movex, int movey){

    crownx = movex;
    crowny = movey;

    glBegin(GL_POLYGON);
    glVertex2f(-40 + crownx, crowny - 36);
    glVertex2f(-52 + crownx, 24 + crowny);
    glVertex2f(-20 + crownx, crowny - 8);
    glVertex2f(0 + crownx, 36 + crowny);
    glVertex2f(20 + crownx, crowny - 8);
    glVertex2f(52 + crownx, 24 + crowny);
    glVertex2f(40 + crownx, crowny - 36);
    glEnd();
}

//绘制模板库
void drawFormBoard(){

    glBegin(GL_LINES);
    glVertex2f(500, 0);
    glVertex2f(500, 700);
    glEnd();

    glBegin(GL_LINES);
    glVertex2f(500, 250);
    glVertex2f(700, 250);
    glEnd();
}

//绘制模板库里面的帽子
int capX = 600;
int capY = 400;
void drawCap(int moveX, int moveY){

    capX = moveX;
    capY = moveY;

    glBegin(GL_POLYGON);
    glVertex2f(0 + capX, capY - 20);
    glVertex2f(-52 + capX, capY - 40);
    glVertex2f(-100 + capX, capY - 40);
    glVertex2f(-48 + capX, capY - 8);
    glVertex2f(0 + capX, capY + 80);
    glVertex2f(48 + capX, capY - 8);
    glVertex2f(100 + capX, capY - 40);
    glVertex2f(52 + capX, capY - 40);
    glEnd();
}

//绘制函数  
void drawObjects(GLenum mode)
{
    //绘制模板库的线
    glColor3f(1, 1, 1);
    drawFormBoard();

    //画脸 
    if (mode == GL_SELECT) //此时模式是选取模式而不是渲染模式
        glLoadName(FACE);
    glColor3f(colors[FACE_COLOR][0], colors[FACE_COLOR][1], colors[FACE_COLOR][2]);
    drawFace();

    //画耳朵
    if (mode == GL_SELECT)
        glLoadName(EARS);
    glColor3f(colors[EARS_COLOR][0], colors[EARS_COLOR][1], colors[EARS_COLOR][2]);
    drawEars();

    //画眉毛
    if (mode == GL_SELECT)
        glLoadName(BROW);
    glColor3f(colors[BROW_COLOR][0], colors[BROW_COLOR][1], colors[BROW_COLOR][2]);
    drawBrow();

    //画眼睛
    if (mode == GL_SELECT)
        glLoadName(LEFT_EYE);
    glColor3f(colors[EYES_COLOR][0], colors[EYES_COLOR][1], colors[EYES_COLOR][2]);
    drawLeftEye(lex, ley);
    if (mode == GL_SELECT)
        glLoadName(RIGHT_EYE);
    glColor3f(colors[EYES_COLOR][0], colors[EYES_COLOR][1], colors[EYES_COLOR][2]);
    drawRightEye(rex, rey);

    //画鼻子  
    if (mode == GL_SELECT)
        glLoadName(NOSE);
    glColor3f(colors[NOSE_COLOR][0], colors[NOSE_COLOR][1], colors[NOSE_COLOR][2]);
    drawNose(nx, ny);

    //画嘴巴
    if (mode == GL_SELECT)
        glLoadName(MOUTH);
    glColor3f(colors[MOUTH_COLOR][0], colors[MOUTH_COLOR][1], colors[MOUTH_COLOR][2]);
    drawMouth(mx, my);

    //画头发
    if (mode == GL_SELECT)
        glLoadName(HAIR);
    glColor3f(colors[HAIR_COLOR][0], colors[HAIR_COLOR][1], colors[HAIR_COLOR][2]);
    drawHair();

    //画皇冠
    if (mode == GL_SELECT)
        glLoadName(CROWN);
    glColor3f(colors[CROWN_COLOR][0], colors[CROWN_COLOR][1], colors[CROWN_COLOR][2]);
    drawCrown(crownx,crowny);

    //画模板库里面的帽子
    if (mode == GL_SELECT)
        glLoadName(CAP);
    glColor3f(colors[CAP_COLOR][0], colors[CAP_COLOR][1], colors[CAP_COLOR][2]);
    drawCap(capX, capY);
}

void myDisplay(){
    //清除缓存  
    glClear(GL_COLOR_BUFFER_BIT);

    //旋转
    glLoadIdentity();//将当前的用户坐标系的原点移到了屏幕中心
    //绕系统原点
    //旋转函数的后三个坐标是指定围绕哪一个方向旋转,而不是围绕哪一个点旋转,同时也引入缩放函数
    glTranslatef(250, 250, 0.0);
    glRotatef(theta, 0.0, 0.0, 1.0);
    glScalef(scaleX, scaleY, scaleZ);
    glTranslatef(-250, -250, 0.0);

    //RANDER模式绘制物体  
    drawObjects(GL_RENDER);

    //绘制  
    glFlush();
}

void myInit(){
    glClearColor(0.0, 0.0, 0.0, 1.0);  //指定清除之后的颜色,即设定背景是黑色
}

void myReshape(int w, int h){
    glViewport(0, 0, w, h);    //定义视口区域
    glMatrixMode(GL_PROJECTION);    //投影模型
    glLoadIdentity();

    //gluOrtho2D是二位裁剪函数,规定了窗口坐标与像素的比例,相当于一个比例尺
    //此时像素跟坐标的比例是一比一,因为二者的设定都是500*500
    gluOrtho2D(0, VIEW_WIDTH, 0, VIEW_HEIGHT);

    glMatrixMode(GL_MODELVIEW);    //模型视图
    glLoadIdentity();
    //开启反走样  
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);
    glEnable(GL_POLYGON_SMOOTH);
    glEnable(GL_POINT_SMOOTH);
    glEnable(GL_LINE_SMOOTH);
}

static bool left_down = false;

void processHits(GLint hits, GLuint buffer[])
{
    unsigned int i, j;
    GLuint names, *ptr;
    printf("本次选中%d个对象!\n", hits);
    ptr = (GLuint *)buffer;
    for (i = 0; i < hits; i++) {// for each hit  
        names = *ptr;
        ptr = ptr + 3;
        for (j = 0; j < names; j++)
        {
            if (*ptr == 1){
                printf("打脸\n");
            }
            else if (*ptr == 2){
                printf("耳朵\n");
                //select_part =2;
                select_part = EARS;
            }
            else if (*ptr == 3){
                printf("眉毛\n");
                //select_part =3;
                select_part = BROW;
            }
            else if (*ptr == 4){
                printf("左眼\n");
                //select_part =4;
                select_part = LEFT_EYE;
            }
            else if (*ptr == 5){
                printf("右眼\n");
                //select_part =5;
                select_part = RIGHT_EYE;
            }
            else if (*ptr == 6){
                printf("鼻子\n");
                //select_part =6;
                select_part = NOSE;
            }
            else if (*ptr == 7){
                printf("嘴巴\n");
                //select_part =7;
                select_part = MOUTH;
            }
            else if (*ptr == 8){
                printf("头发\n");
                //select_part =8;
                select_part = HAIR;
            }
            else if (*ptr == 9){
                printf("皇冠\n");
                //select_part =8;
                select_part = CROWN;
            }
            else if (*ptr == 10){
                printf("帽子\n");
                //select_part =8;
                select_part = CAP;
            }
            ptr++;
        }
    }
    printf("select_part:%d\n", select_part);
}

void myMouseMove(int x, int y){
    if (select_part == RIGHT_EYE){//(180 < x) && (x < 210) && (230 < (500 - y)) && ((500 - y) < 270)
        //500-y是因为视口坐标系跟像素矩形坐标系不一样
        //覆盖原图
        glColor3ub(colors[FACE_COLOR][0], colors[FACE_COLOR][1], colors[FACE_COLOR][2]);
        drawRightEye(rex, rey);
        //重绘
        rex = x;
        rey = (500 - y);
        glColor3f(1.0, 1.0, 1.0);
        drawRightEye(rex, rey);
        glutPostRedisplay();//需要标记当前窗口需要重新绘制
    }
    else if (select_part == LEFT_EYE){//(290 < x) && (x < 320) && (230 < (500 - y)) && ((500 - y) < 270)
        //cout<<"移动右眼"<
        //覆盖原图
        glColor3ub(colors[FACE_COLOR][0], colors[FACE_COLOR][1], colors[FACE_COLOR][2]);
        drawLeftEye(lex, ley);
        //重绘
        lex = x;
        ley = (500 - y);
        glColor3f(1.0, 1.0, 1.0);
        drawLeftEye(lex, ley);
        glutPostRedisplay();
    }
    else if (select_part == MOUTH){//(230 < x) && (x < 270) && (160 < (500 - y)) && ((500 - y) < 180)
        //cout<<"移动嘴巴"<
        //覆盖原图
        glColor3ub(colors[FACE_COLOR][0], colors[FACE_COLOR][1], colors[FACE_COLOR][2]);
        drawMouth(mx, my);
        //重绘
        mx = x;
        my = (500 - y);
        glColor3f(1.0, 1.0, 1.0);
        drawMouth(mx, my);
        glutPostRedisplay();
    }
    else if (select_part == NOSE){//(240
        //cout<<"移动鼻子"<
        //覆盖原图
        glColor3ub(colors[FACE_COLOR][0], colors[FACE_COLOR][1], colors[FACE_COLOR][2]);
        drawNose(mx, my);
        //重绘
        nx = x;
        ny = (500 - y);
        glColor3f(colors[NOSE_COLOR][0], colors[NOSE_COLOR][1], colors[NOSE_COLOR][2]);
        drawNose(nx, ny);
        glutPostRedisplay();
    }
    else if (select_part ==CROWN){//(230
        //利用背景黑色覆盖皇冠
        glColor3f(0.0, 0.0, 0.0);
        drawCrown(crownx, crowny);
        //重绘
        crownx = x;
        crowny = (500 - y);
        glColor3f(colors[CROWN_COLOR][0], colors[CROWN_COLOR][1], colors[CROWN_COLOR][2]);
        glutPostRedisplay();
    }
    else if (select_part == CAP){
        //利用背景黑色覆盖帽子
        glColor3f(0.0, 0.0, 0.0);
        drawCap(capX, capX);
        //重绘
        capX = x;
        capY = (500 - y);
        glColor3f(colors[CAP_COLOR][0], colors[CAP_COLOR][1], colors[CAP_COLOR][2]);
        glutPostRedisplay();
    }
}

//鼠标响应  ,抓取图元
void myMouse(int button, int state, int x, int y){
    GLuint selectBuf[SIZE];
    GLint hits;
    GLint viewport[4];

    //进行颜色选择
    if (button == GLUT_LEFT_BUTTON && state == GLUT_UP) {
        left_down = false;

        //设置一个用于拾取操作的观察空间,viewport的四个参数分别是坐标和尺寸
        glGetIntegerv(GL_VIEWPORT, viewport);

        // 创建名称缓冲区
        glSelectBuffer(SIZE, selectBuf);

        //切换到选择模式
        glRenderMode(GL_SELECT);

        // 初始化名称栈
        glInitNames();
        glPushName(0);
        glMatrixMode(GL_PROJECTION);

        //进行矩阵压栈操作,操作拾取图元之后进行弹栈
        glPushMatrix();
        glLoadIdentity();

        //viewport是视口的y的坐标,要与鼠标的坐标进行转换
        gluPickMatrix((GLdouble)x, (GLdouble)(viewport[3] - y), 3.0, 3.0, viewport);

        //二位物体投射到二位平面上
        gluOrtho2D(0, VIEW_WIDTH, 0, VIEW_HEIGHT);
        drawObjects(GL_SELECT);
        glPopMatrix();
        glMatrixMode(GL_MODELVIEW);

        //绘制模式(GL_RENDER),选择模式(GL_SELECT),反馈模式(GL_FEEDBACK),函数的返回值可以确定选择模式下的命中次数
        /*返回值hits表示The number of hit records transferred to the select buffer(转移到缓冲区中已命中的记录数)。hits = glRenderMode(GL_RENDER); 返回的是命中的记录条数。根据条数用循环可以读出缓冲区中每个命中物体的名字,根据名字即可画出来。*/
        hits = glRenderMode(GL_RENDER);
        processHits(hits, selectBuf);

        //再次绘制
        glutPostRedisplay();
    }
}

//根据鼠标按键来进行旋转
void mySpecial(int key, int x, int y){
    switch (key)
    {
    case GLUT_KEY_UP:
        theta = (theta + 10);
        glutPostRedisplay();
        break;
    case GLUT_KEY_DOWN:
        theta = (theta - 10);
        glutPostRedisplay();
        break;
    }
}

//根据键盘按键来操作缩放,先用Ctrl+空格锁定输入法,之后Z/A整体缩放,S/X横向缩放,Y/H纵向缩放
void myKeyBoard(unsigned char key, int x, int y){
    switch (key)
    {
    case 'x':
    case 'X':
        scaleX = scaleX + 0.1;
        glutPostRedisplay();
        break;
    case 's':
    case 'S':
        scaleX = scaleX - 0.1;
        glutPostRedisplay();
        break;
    case 'Y':
    case 'y':
        scaleY = scaleY + 0.1;
        glutPostRedisplay();
        break;
    case 'h':
    case 'H':
        scaleY = scaleY - 0.1;
        glutPostRedisplay();
        break;
    case 'Z':
    case 'z':
        scaleY = scaleY + 0.1;
        scaleX = scaleX + 0.1;
        glutPostRedisplay();
        break;
    case 'a':
    case 'A':
        scaleY = scaleY - 0.1;
        scaleX = scaleX - 0.1;
        glutPostRedisplay();
        break;
    }
}

void main_menu(int index) {
    if (index == -1)
        return;

    switch (select_part) {
    case FACE:
        FACE_COLOR = index;
        break;
    case EARS:
        EARS_COLOR = index;
        break;
    case BROW:
        BROW_COLOR = index;
        break;
    case LEFT_EYE:
        EYES_COLOR = index;
        break;
    case RIGHT_EYE:
        EYES_COLOR = index;
        break;
    case NOSE:
        NOSE_COLOR = index;
        break;
    case MOUTH:
        MOUTH_COLOR = index;
        break;
    case HAIR:
        HAIR_COLOR = index;
        break;
    case CROWN:
        CROWN_COLOR = index;
        break;
    case CAP:
        CAP_COLOR = index;
        break;
    }
    glutPostRedisplay();
}

void main(int argc, char** argv){
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);

    glutInitWindowSize(700, 500);
    glutInitWindowPosition(500, 100);
    glutCreateWindow("test1");
    myInit();

    /*glutReshapeFunc是窗口改变的时候调用的函数,
    在这个里面可以根据缩放后的窗口重新设置camera的内部参数*/
    glutReshapeFunc(myReshape);

    glutDisplayFunc(myDisplay);

    /*使用glutMouseFunc函数注册鼠标响应事件,
    使用glutKeyboardFunc函数注册键盘响应事件,
    对键盘上特殊的4个方向按键的响应函数是glutSpecialFunc。
    使用glutKeyboardFunc对键盘进行侦听,并作出对应按键的函数响应*/
    glutMouseFunc(myMouse);
    glutSpecialFunc(mySpecial);
    glutKeyboardFunc(myKeyBoard);

    /*这个函数是处理当鼠标键摁下时,鼠标拖动的事件。当鼠标拖动时,将每一帧都调用一次这个函数*/
    glutMotionFunc(myMouseMove);


    //右击事件  
    glutCreateMenu(main_menu);
    glutAddMenuEntry("选择颜色", -1);
    glutAddMenuEntry("黑色", 0);
    glutAddMenuEntry("红色", 1);
    glutAddMenuEntry("绿色", 2);
    glutAddMenuEntry("蓝色", 3);
    glutAddMenuEntry("青色", 4);
    glutAddMenuEntry("紫色", 5);
    glutAddMenuEntry("黄色", 6);
    glutAddMenuEntry("白色", 7);

    glutAttachMenu(GLUT_RIGHT_BUTTON);
    glutMainLoop();

}

![实验的最终结果,按键可以进行相应的变换](http://img.blog.csdn.net/20170407113852806?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXFfMzM5NDUyNDY=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

你可能感兴趣的:(OpenGL学习)