1. 程式人生 > >從零開始openGL—— 二、 基本圖形繪製

從零開始openGL—— 二、 基本圖形繪製

前言

這是從零開始openGL系列文章的第二篇,在上篇文章中介紹了基本的環境配置,這篇文章將介紹如何繪製基本圖形(圓、三角形、立方體、圓柱、圓錐)。

基本框架

下面這裡我先給出opengl的3D繪圖的基本框架

#include <windows.h>
#include <string.h>
#include <stdlib.h>
#include <gl\glui.h>
#include <math.h>
#include "common.h"

int g_xform_mode = TRANSFORM_NONE;
int   g_main_window;
double g_windows_width, g_windows_height;

CObj g_obj;
//the lighting
static GLfloat g_light0_ambient[] =  {0.0f, 0.0f, 0.0f, 1.0f};//環境光
static GLfloat g_light0_diffuse[] =  {1.0f, 1.0f, 1.0f, 1.0f};//散射光
static GLfloat g_light0_specular[] = {1.0f,1.0f,1.0f,1.0f}; //鏡面光
static GLfloat g_light0_position[] = {0.0f, 0.0f, 100.0f, 0.0f};//光源的位置。第4個引數為1,表示點光源;第4個引數量為0,表示平行光束{0.0f, 0.0f, 10.0f, 0.0f}

static GLfloat g_material[] = {0.96f, 0.8f, 0.69f, 1.0f};//材質
static GLfloat g_rquad = 0;
static GLfloat g_rquad_x = 0;
static GLfloat g_rquad_y = 0;

static float g_x_offset   = 0.0;
static float g_y_offset   = 0.0;
static float g_z_offset   = 0.0;
static float g_scale_size = 1; 
static int  g_press_x; //滑鼠按下時的x座標
static int  g_press_y; //滑鼠按下時的y座標

const int n = 1000;
const GLfloat R = 0.5f;
const GLfloat Pi = 3.1415926536f;
int g_view_type = VIEW_FLAT;
int g_draw_content = SHAPE_TRIANGLE;

void DrawTriangle() 
{//繪製三角形
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
    glBegin(GL_TRIANGLES);
        glNormal3f(0.0f, 0.0f, 1.0f);  //指定面法向
        glVertex3f( 0.0f, 1.0f, 0.0f);                    // 上頂點
        glVertex3f(-1.0f,-1.0f, 0.0f);                    // 左下
        glVertex3f( 1.0f,-1.0f, 0.0f);                    // 右下
    glEnd();
    glFlush();
}

void DrawCube() 
{//繪製立方體
    glBegin(GL_QUADS);  
        glNormal3f( 0.0f, 0.0f, 1.0f);  //指定面法向
        glVertex3f( 1.0f, 1.0f,1.0f);   //列舉面頂點資料,逆時針順序
        glVertex3f(-1.0f, 1.0f, 1.0f);  
        glVertex3f(-1.0f,-1.0f, 1.0f); 
        glVertex3f( 1.0f,-1.0f, 1.0f);  
    //前----------------------------  
        glNormal3f( 0.0f, 0.0f,-1.0f);  
        glVertex3f(-1.0f,-1.0f,-1.0f); 
        glVertex3f(-1.0f, 1.0f,-1.0f);  
        glVertex3f( 1.0f, 1.0f,-1.0f);  
        glVertex3f( 1.0f,-1.0f,-1.0f);  
    //後----------------------------  
        glNormal3f( 0.0f, 1.0f, 0.0f);  
        glVertex3f( 1.0f, 1.0f, 1.0f); 
        glVertex3f( 1.0f, 1.0f,-1.0f);  
        glVertex3f(-1.0f, 1.0f,-1.0f);  
        glVertex3f(-1.0f, 1.0f, 1.0f);  
    //上----------------------------  
        glNormal3f( 0.0f,-1.0f, 0.0f);
        glVertex3f(-1.0f,-1.0f,-1.0f);  
        glVertex3f( 1.0f,-1.0f,-1.0f);  
        glVertex3f( 1.0f,-1.0f, 1.0f);
        glVertex3f(-1.0f,-1.0f, 1.0f);  
    //下----------------------------  
        glNormal3f( 1.0f, 0.0f, 0.0f);  
        glVertex3f( 1.0f, 1.0f, 1.0f);
        glVertex3f( 1.0f,-1.0f, 1.0f);  
        glVertex3f( 1.0f,-1.0f,-1.0f);  
        glVertex3f( 1.0f, 1.0f,-1.0f);  
    //右----------------------------  
        glNormal3f(-1.0f, 0.0f, 0.0f);  
        glVertex3f(-1.0f,-1.0f,-1.0f);  
        glVertex3f(-1.0f,-1.0f, 1.0f); 
        glVertex3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f,-1.0f);  
    //左----------------------------*/  
    glEnd();  
    glFlush();
}

void DrawCircle() 
{//繪製圓
    glBegin(GL_POLYGON);
        glNormal3f(0.0f, 0.0f, 1.0f);
        for (int i = 0; i < n; ++i) {
            glVertex2f(R*cos(2 * Pi / n * i), R*sin(2 * Pi / n * i));
        }
    glEnd();
}

void DrawCylinder()
{//繪製圓柱

}

void DrawTorus()
{

}void myInit()
{
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);//用白色清屏

    glLightfv(GL_LIGHT0, GL_AMBIENT, g_light0_ambient);//設定場景的環境光
    glLightfv(GL_LIGHT0, GL_DIFFUSE, g_light0_diffuse);//設定場景的散射光
    glLightfv(GL_LIGHT0, GL_POSITION, g_light0_position);//設定場景的位置

    glMaterialfv(GL_FRONT, GL_DIFFUSE, g_material);//指定用於光照計算的當前材質屬性
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_LIGHTING);//開啟燈光
    glEnable(GL_LIGHT0);//開啟光照0

    glShadeModel(GL_SMOOTH); //設定著色模式為光滑著色
    glEnable(GL_DEPTH_TEST);//啟用深度測試

    glMatrixMode(GL_MODELVIEW); //指定當前矩陣為模型視景矩陣
    glLoadIdentity(); //將當前的使用者座標系的原點移到了螢幕中心:類似於一個復位操作
    gluLookAt(0.0, 0.0, 8.0, 0, 0, 0, 0, 1.0, 0);//該函式定義一個檢視矩陣,並與當前矩陣相乘.
    //第一組eyex, eyey,eyez 相機在世界座標的位置;第二組centerx,centery,centerz 相機鏡頭對準的物體在世界座標的位置;第三組upx,upy,upz 相機向上的方向在世界座標中的方向
}void myGlutDisplay() //繪圖函式, 作業系統在必要時刻就會對窗體進行重新繪製操作
{
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); //清除顏色緩衝以及深度緩衝
    glEnable(GL_NORMALIZE); //開啟法線向量歸一化,確保了法線的長度為1
    
    glMatrixMode(GL_MODELVIEW);//模型檢視矩陣
    glPushMatrix(); //壓入當前矩陣堆疊


    if (g_draw_content == SHAPE_MODEL)
    {//繪製模型
        
    }
    else if (g_draw_content == SHAPE_TRIANGLE)  //畫三角形
    {
        glLoadIdentity();
        glTranslatef(0.0f, 0.0f, -6.0f);  
        DrawTriangle();
    }
    else if(g_draw_content == SHAPE_CUBE)  //畫立方體
    {
        glLoadIdentity();
        glTranslatef(0.0f, 0.0f, -6.0f);  
        glRotatef(g_rquad, g_rquad, g_rquad, 1.0f);    // 在XYZ軸上旋轉立方體
        DrawCube();
        g_rquad+=0.2f;// 增加旋轉變數
    }
    else if (g_draw_content == SHAPE_CIRCLE) // 畫圓
    {
        glLoadIdentity();
        glTranslatef(0.0f, 0.0f, -6.0f);  
        DrawCircle();
    }
    else if (g_draw_content == SHAPE_CYLINDER)  
    {//TODO: 新增畫圓柱的程式碼

    }
    else if (g_draw_content == SHAPE_TORUS) 
    {//TODO:新增畫圓環的程式碼

    }
    glPopMatrix();
    glutSwapBuffers(); //雙緩衝
}

void myGlutReshape(int x,int y) //當改變視窗大小時的回撥函式
{
    if (y == 0)
    {
        y = 1;
    }
    
    g_windows_width = x;
    g_windows_height = y;
    double xy_aspect = (float)x / (float)y;
    GLUI_Master.auto_set_viewport(); //自動設定視口大小

    glMatrixMode( GL_PROJECTION );//當前矩陣為投影矩陣
    glLoadIdentity();
    gluPerspective(60.0, xy_aspect, 0.01, 1000.0);//視景體

    glutPostRedisplay(); //標記當前視窗需要重新繪製
}

void myGlutKeyboard(unsigned char Key, int x, int y)
{//鍵盤時間回撥函式

}

void myGlutMouse(int button, int state, int x, int y)
{
    if (state == GLUT_DOWN) //滑鼠的狀態為按下
    {
        g_press_x = x;
        g_press_y = y; 
        if (button == GLUT_LEFT_BUTTON) 
        {//按下滑鼠的左鍵表示對模型進行旋轉操作
            g_xform_mode = TRANSFORM_ROTATE;
        }
        else if (button == GLUT_RIGHT_BUTTON)
        {//按下滑鼠的右鍵表示對模型進行平移操作
            g_xform_mode = TRANSFORM_TRANSLATE; 
        }
        else if (button == GLUT_MIDDLE_BUTTON)
        {//按下滑鼠的滑輪表示按下滑鼠的右鍵表示對模型進行縮放操作
            g_xform_mode = TRANSFORM_SCALE; 
        }
    }
    else if (state == GLUT_UP)  
    {//如果沒有按滑鼠,則不對模型進行任何操作
        g_xform_mode = TRANSFORM_NONE; 
    }
}

void myGlutMotion(int x, int y) //處理當滑鼠鍵摁下時,滑鼠拖動的事件
{
    if (g_xform_mode == TRANSFORM_ROTATE) //旋轉
    {//TODO:新增滑鼠移動控制模型旋轉引數的程式碼

    }
    else if(g_xform_mode == TRANSFORM_SCALE) //縮放
    {//TODO:新增滑鼠移動控制模型縮放參數的程式碼

    }
    else if(g_xform_mode == TRANSFORM_TRANSLATE) //平移
    {//TODO:新增滑鼠移動控制模型平移引數的程式碼

    }

    // force the redraw function
    glutPostRedisplay(); 
}

void myGlutIdle(void) //空閒回撥函式
{
    if ( glutGetWindow() != g_main_window ) 
        glutSetWindow(g_main_window);  

    glutPostRedisplay();
}

void glui_control(int control ) //處理控制元件的返回值
{
    switch(control)
    {
    case CRTL_LOAD://選擇“open”控制元件
        loadObjFile();
        g_draw_content = SHAPE_MODEL;
        break;
    case CRTL_CHANGE://選擇Type面板
        if (g_view_type == VIEW_POINT)  
        {
            glPolygonMode(GL_FRONT_AND_BACK, GL_POINT); // 設定兩面均為頂點繪製方式
        }
        else if (g_view_type == VIEW_WIRE)
        {
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 設定兩面均為線段繪製方式
        }
        else
        {
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 設定兩面為填充方式
        }    
        break;
    case CRTL_TRIANGLE:
        g_draw_content = SHAPE_TRIANGLE;
        break;
    case CRTL_CUBE:
        g_draw_content = SHAPE_CUBE;
        break;
    case CRTL_CIRCLE:
        g_draw_content = SHAPE_CIRCLE;
        break;
    case CRTL_CYLINDER:
        g_draw_content = SHAPE_CYLINDER;
        break;
    case CRTL_CONE:
        g_draw_content = SHAPE_TORUS;
        break;
    case CRTL_MODEL:
        g_draw_content = SHAPE_MODEL;
        break;
    default:
        break;
    }
}

void myGlui()
{
    GLUI_Master.set_glutDisplayFunc( myGlutDisplay ); //註冊渲染事件回撥函式, 系統在需要對窗體進行重新繪製操作時呼叫
    GLUI_Master.set_glutReshapeFunc( myGlutReshape );  //註冊視窗大小改變事件回撥函式
    GLUI_Master.set_glutKeyboardFunc( myGlutKeyboard );//註冊鍵盤輸入事件回撥函式
    glutMotionFunc( myGlutMotion);//註冊滑鼠移動事件回撥函式
    GLUI_Master.set_glutMouseFunc( myGlutMouse );//註冊滑鼠點選事件回撥函式
    GLUI_Master.set_glutIdleFunc(myGlutIdle); //為GLUI註冊一個標準的GLUT空閒回撥函式,當系統處於空閒時,就會呼叫該註冊的函式

    //GLUI
    GLUI *glui = GLUI_Master.create_glui_subwindow( g_main_window, GLUI_SUBWINDOW_RIGHT); //新建子窗體,位於主窗體的右部 
    new GLUI_StaticText(glui, "GLUI" ); //在GLUI下新建一個靜態文字框,輸出內容為“GLUI”
    new GLUI_Separator(glui); //新建分隔符
    new GLUI_Button(glui,"Open", CRTL_LOAD, glui_control); //新建按鈕控制元件,引數分別為:所屬窗體、名字、ID、回撥函式,當按鈕被觸發時,它會被呼叫.
    new GLUI_Button(glui, "Quit", 0,(GLUI_Update_CB)exit );//新建退出按鈕,當按鈕被觸發時,退出程式

    GLUI_Panel *type_panel = glui->add_panel("Type" ); //在子窗體glui中新建面板,名字為“Type”
    GLUI_RadioGroup *radio = glui->add_radiogroup_to_panel(type_panel, &g_view_type, CRTL_CHANGE, glui_control); //在Type面板中新增一組單選按鈕
    glui->add_radiobutton_to_group(radio, "points"); 
    glui->add_radiobutton_to_group(radio, "wire");
    glui->add_radiobutton_to_group(radio, "flat");

    GLUI_Panel *draw_panel = glui->add_panel("Draw" ); //在子窗體glui中新建面板,名字為“Draw”
    new GLUI_Button(draw_panel,"Triangle",CRTL_TRIANGLE,glui_control);
    new GLUI_Button(draw_panel,"Cube",CRTL_CUBE,glui_control);
    new GLUI_Button(draw_panel,"Circle",CRTL_CIRCLE,glui_control);
    new GLUI_Button(draw_panel,"Cylinder",CRTL_CYLINDER,glui_control);
    new GLUI_Button(draw_panel,"Torus",CRTL_CONE,glui_control);
    new GLUI_Button(draw_panel,"Model",CRTL_MODEL,glui_control);

    glui->set_main_gfx_window(g_main_window ); //將子窗體glui與主窗體main_window繫結,當窗體glui中的控制元件的值發生過改變,則該glui視窗被重繪
    GLUI_Master.set_glutIdleFunc( myGlutIdle ); 
}

int main(int argc, char* argv[]) //程式入口
{
    /****************************************/
    /*   Initialize GLUT and create window  */
    /****************************************/

    freopen("log.txt", "w", stdout);//重定位,將輸出放入log.txt檔案中
    glutInit(&argc, argv);//初始化glut
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH);//初始化渲染模式
    glutInitWindowPosition(200, 200); //初始化視窗位置
    glutInitWindowSize(800, 600); //初始化視窗大小

    g_main_window = glutCreateWindow("Model Viewer"); //建立主窗體Model Viewer

    myGlui();
    myInit();

    glutMainLoop();//進入glut訊息迴圈

    return EXIT_SUCCESS;
}
#ifndef COMMON
#define COMMON

#define VIEW_POINT            0x00
#define VIEW_WIRE            0x01
#define VIEW_FLAT            0x02

#define CRTL_LOAD            0x00
#define CRTL_CHANGE            0x01
#define CRTL_TRIANGLE        0x02
#define CRTL_CUBE            0x03
#define CRTL_CIRCLE            0x04
#define CRTL_CYLINDER        0x05
#define CRTL_CONE            0x06
#define CRTL_MODEL            0x07

#define SHAPE_TRIANGLE        0x00
#define SHAPE_CUBE            0x01
#define SHAPE_CIRCLE        0x02
#define SHAPE_CYLINDER        0x03
#define SHAPE_TORUS            0x04
#define SHAPE_MODEL            0x05

#define TRANSFORM_NONE      0x51 
#define TRANSFORM_ROTATE    0x52
#define TRANSFORM_SCALE     0x53 
#define TRANSFORM_TRANSLATE 0x54

#endif 
common.h

執行這段程式碼可以得到如下所示的結果

 

 

 圖形繪製

在上面那段程式碼中,已經給出了三角形、圓、正方體的繪製程式碼,下面還將介紹圓柱與圓環的繪製

在opengl中並不能直接繪製圓,那麼,此時想到了極限的方法,如果把圓分割成很多個扇形,這個扇形的角度足夠小的話,那麼曲線自然可以看作直線。有了這個思路,程式碼就很好寫了。

void DrawCircle() 
{//繪製圓
    glBegin(GL_POLYGON);
        glNormal3f(0.0f, 0.0f, 1.0f);
        for (int i = 0; i < n; ++i) {
            glVertex2f(R*cos(2 * Pi / n * i), R*sin(2 * Pi / n * i));
        }
    glEnd();
}

三角形

三角形的繪製就十分的簡單了,確定三個頂點,然後連線

void DrawTriangle() 
{//繪製三角形
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBegin(GL_TRIANGLES);
        glNormal3f(0.0f, 0.0f, 1.0f);  //指定面法向
        glVertex3f( 0.0f, 1.0f, 0.0f);                    // 上頂點
        glVertex3f(-1.0f,-1.0f, 0.0f);                    // 左下
        glVertex3f( 1.0f,-1.0f, 0.0f);                    // 右下
    glEnd();
    glFlush();
}

立方體

原理同三角形,確定八個頂點座標,然後連線,不過這裡要注意的是立方體為3D圖形,要展示光照效果的話需要在繪製的時候確定各個面的法向量。

void DrawCube() 
{//繪製立方體
    glBegin(GL_QUADS);  
        glNormal3f( 0.0f, 0.0f, 1.0f);  //指定面法向
        glVertex3f( 1.0f, 1.0f,1.0f);   //列舉面頂點資料,逆時針順序
        glVertex3f(-1.0f, 1.0f, 1.0f);  
        glVertex3f(-1.0f,-1.0f, 1.0f); 
        glVertex3f( 1.0f,-1.0f, 1.0f);  
    //前----------------------------  
        glNormal3f( 0.0f, 0.0f,-1.0f);  
        glVertex3f(-1.0f,-1.0f,-1.0f); 
        glVertex3f(-1.0f, 1.0f,-1.0f);  
        glVertex3f( 1.0f, 1.0f,-1.0f);  
        glVertex3f( 1.0f,-1.0f,-1.0f);  
    //後----------------------------  
        glNormal3f( 0.0f, 1.0f, 0.0f);  
        glVertex3f( 1.0f, 1.0f, 1.0f); 
        glVertex3f( 1.0f, 1.0f,-1.0f);  
        glVertex3f(-1.0f, 1.0f,-1.0f);  
        glVertex3f(-1.0f, 1.0f, 1.0f);  
    //上----------------------------  
        glNormal3f( 0.0f,-1.0f, 0.0f);
        glVertex3f(-1.0f,-1.0f,-1.0f);  
        glVertex3f( 1.0f,-1.0f,-1.0f);  
        glVertex3f( 1.0f,-1.0f, 1.0f);
        glVertex3f(-1.0f,-1.0f, 1.0f);  
    //下----------------------------  
        glNormal3f( 1.0f, 0.0f, 0.0f);  
        glVertex3f( 1.0f, 1.0f, 1.0f);
        glVertex3f( 1.0f,-1.0f, 1.0f);  
        glVertex3f( 1.0f,-1.0f,-1.0f);  
        glVertex3f( 1.0f, 1.0f,-1.0f);  
    //右----------------------------  
        glNormal3f(-1.0f, 0.0f, 0.0f);  
        glVertex3f(-1.0f,-1.0f,-1.0f);  
        glVertex3f(-1.0f,-1.0f, 1.0f); 
        glVertex3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f,-1.0f);  
    //左----------------------------*/  
    glEnd();  
    glFlush();
}

圓柱

對於圓柱的繪製,思想同圓十分相似,就是分割。不過需要注意的是上下兩個圓面和一個側面需要分開來繪製。這裡需要思考的是這個側面該如何繪製呢?想象以下,把圓柱側面展開,我們得到的是一個矩形,那分割成小片段的話也就是矩形了,即繪製無數個矩形,然後拼接形成側面。

注:繪製時注意法向量的選取

void DrawCylinder()
{//繪製圓柱
    //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glBegin(GL_POLYGON);
    glNormal3f(0.0f, 1.0f, 0.0f);
    for (int i = 1; i < n; i++) {
        glVertex3f(R*cos(2 * Pi / n * i), 1.0f, R*sin(2 * Pi / n * i));
    }
    glEnd();
    glBegin(GL_POLYGON);
    glNormal3f(0.0f, -1.0f, 0.0f);
    for (int i = 1; i < n; i++) {
        glVertex3f(R*cos(2 * Pi / n * i), 0.0f, R*sin(2 * Pi / n * i));
    }
    glEnd();
    glBegin(GL_QUADS);
    for (int i = 1; i <= n; i++)
    {
        glNormal3f(R*cos(2 * Pi / n * i), 0.0f, R*sin(2 * Pi / n * i));
        glVertex3f(R*cos(2 * Pi / n * i), 1.0f, R*sin(2 * Pi / n * i));
        glVertex3f(R*cos(2 * Pi / n * i), 0.0f, R*sin(2 * Pi / n * i));
        glNormal3f(R*cos(2 * Pi / n * (i + 1)), 0.0f, R*sin(2 * Pi / n * (i + 1)));
        glVertex3f(R*cos(2 * Pi / n * (i + 1)), 0.0f, R*sin(2 * Pi / n * (i + 1)));
        glVertex3f(R*cos(2 * Pi / n * (i + 1)), 1.0f, R*sin(2 * Pi / n * (i + 1)));
    }
    glEnd();
}

圓環

圓環的繪製稍微有些麻煩,先來看看下面這個圓環的線圖

 

這裡的難點就是各點的座標表示,首先我們需要做的是把圓環壓縮成一個圓面

 

 壓縮之後的表示為 R+r*cos(θ),然後再把壓縮完後的點對映到x y軸上

 

X軸:(R+r*cos(θ))*cosα

Y軸:(R+r*cos(θ))*sinα

Z軸:r*sin(θ)

這樣,我們的圓環就可以實現了

void DrawTorus()
{
    int num = n / 50;
    for (int i = 0; i < num; i++)
    {    
        glBegin(GL_QUAD_STRIP);
        for (int j = 0; j <= num; j++)
        {
            for (int k = 1; k >= 0; k--)
            {
                double s = (i + k) % num + 0.5;
                double t = j % num;
                glNormal3f(cos(2 * Pi / num * s) * cos(2 * Pi / num * t), cos(2 * Pi / num * s)*sin(2 * Pi / num * t), sin(2 * Pi / num * s));
                glVertex3f((1 + R*cos(2 * Pi / num * s))*cos(2 * Pi / num * t), (1 + R*cos(2 * Pi / num * s))*sin(2 * Pi / num * t), R*sin(2 * Pi / num * s));
            }
        }
        glEnd();
    }
}

小節

以上介紹瞭如何使用opengl繪製基本圖形,下篇文章中將介紹如何使用opengl載入繪製模型,以及滑鼠互動的實