歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> OpenGL超級寶典學習筆記——NURBS與曲面細分

OpenGL超級寶典學習筆記——NURBS與曲面細分

日期:2017/3/1 9:32:14   编辑:Linux編程

NURBS

貝塞爾曲線的缺點是當我們增加很多控制點的時候,曲線變得不可控,其連續性會變差差。如果控制點很多(高階曲線),當我們調整一個控制點的位置,對整個曲線的影響是很大的。要獲得更高級的控制,可以使用GLU庫提供的NURBS(非均勻有理B樣條)。通過這些函數我們可以在求值器中調整控制點的影響力,在有大量控制點的情況下,依然可以產生平滑的曲線。

從貝塞爾到B樣條

貝塞爾曲線由起點、終點和其他控制點來影響曲線的形狀。在二次貝塞爾曲線和三次貝塞爾曲線中,可以通過調整控制點的位置而得到很好的平滑性(C2級連續性 曲率級)的曲線。當增加更多的控制點的時候,這種平滑性就被破壞了。如下圖所示,前兩個曲線很平滑(曲率級的連續性),第三個曲線在增加了一個控制點之後,曲線被拉伸了,其平滑性遭到了破壞。

B樣條的工作方式類似於貝塞爾曲線,但不同的是曲線被分成很多段。每段曲線的形狀只受到最近的四個控制點的影響,這樣曲線就像是4階的貝塞爾曲線拼接起來的。這樣很長的有很多控制點的曲線就會有固定的連續性,平滑性(每一段都是c2級的連續性)。

結點

NURBS(非均勻有理B樣條)的真正威力在於,可以調整任意一段曲線中的四個控制點的影響力,來產生較好的平滑性。這是通過一系列結點來控制的。每個控制點都定義了兩個結點的值。結點的取值范圍是u或v的定義域,而且必須是非遞減的。

結點的值決定了落在u、v參數定義域內的控制點的影響力。下圖的曲線表示控制點對一條在u參數定義域內的具有四個單位的曲線的影響。下圖表示中間點對曲線的影響更大,而且只有在[0,3]范圍內的控制點才會對曲線產生影響。

在u、v參數定義域內的控制點對曲線的形狀會有有影響,而且我們可以通過結點來控制控制點的影響力。非均勻性就是指一個控制點的影響力的范圍是可以改變的。

以下內容及節選自 http://www.rhino3d.com/cn/nurbs

節點 ( Knot ) 是一個 ( 階數 + N - 1 ) 的數字列表,N 代表控制點數目。有時候這個列表上的數字也稱為節點矢量 ( Knot Vector ),這裡的矢量並不是指 3D 方向。

節點列表上的數字必須符合幾個條件,確定條件是否符合的標准方式是在列表上往下時,數字必需維持不變或變大,而且數字重復的次數不可以比階數大。例如,階數 3 有 15 個控制點的 NURBS 曲線,列表數字為 0,0,0,1,2,2,2,3,7,7,9,9,9 是一個符合條件的節點列表。列表數字為 0,0,0,1,2,2,2,2,7,7,9,9,9 則不符合,因為此列表中有四個 2,而四比階數大 ( 階數為 3 )。

節點值重復的次數稱為節點的重數 ( Multiplicity ),在上面例子中符合條件的節點列表中,節點值 0 的重數值為三;節點值 1 的重數值為一;節點值 2 的重數為三;節點值 7 的重數值為二;節點值 9 的重數值為三。如果節點值重復的次數和階數一樣,該節點值稱為全復節點 ( Full-Multiplicity Knot )。在上面的例子中,節點值 0、2、9 有完整的重數,只出現一次的節點值稱為單純節點 ( Simple Knot ),節點值 1 和 3 為單純節點。

如果在節點列表中是以全復節點開始,接下來是單純節點,再以全復節點結束,而且節點值為等差,稱為均勻 ( Uniform )。例如,如果階數為 3 有 7 個控制點的 NURBS 曲線,其節點值為 0,0,0,1,2,3,4,4,4,那麼該曲線有均勻的節點。如果節點值是 0,0,0,1,2,5,6,6,6 不是均勻的,稱為非均勻 ( Non-Uniform )。在 NURBS 的 NU 代表“非均勻”,意味著在一條 NURBS 曲線中節點可以是非均勻的。

在節點值列表中段有重復節點值的 NURBS 曲線比較不平滑,最不平滑的情形是節點列表中段出現全復節點,代表曲線有銳角。因此,有些設計師喜歡在曲線插入或移除節點,然後調整控制點,使曲線的造型變得平滑或尖銳。因為節點數等於 ( N + 階數 - 1 ),N 代表控制點的數量,所以插入一個節點會增加一個控制點,移除一個節點也會減少一個控制點。插入節點時可以不改變 NURBS 曲線的形狀,但通常移除節點必定會改變 NURBS 曲線的形狀。

節點(Knot)與控制點

控制點和節點是一對一成對的是常見的錯誤概念,這種情形只發生在 1 階的 NURBS ( 多重直線 )。較高階數的 NURBS 的每 ( 2 x 階數 ) 個節點是一個群組,每 ( 階數 + 1 ) 個控制點是一個群組。例如,一條 3 階 7 個控制點的 NURBS 曲線,節點是 0,0,0,1,2,5,8,8,8,前四個控制點是對應至前六個節點;第二至第五個控制點是對應至第二至第七個節點 0,0,1,2,5,8;第三至第六個控制點是對應至第三至第八個節點 0,1,2,5,8,8;最後四個控制點是對應至最後六個節點

創建NURBS表面

GLU庫中提供了易用高級的繪制NURBS表面的函數。我們不需要顯示地調用求值函數或建立網格。渲染一個NURBS表面的步驟如下:

  1. 創建一個NURBS渲染對象 gluNewNurbsRenderer()
  2. 調用相關的NURBS函數來修改曲線或曲面的外觀 gluNurbsProperty
  3. 定義表面,渲染NURBS gluBeginSurface gluNurbsSurface gluEndSurface
  4. 銷毀NURBS渲染對象 gluDeleteNurbsRenderer();

我們通過gluNewNurbsRenderer函數來為NURBS創建一個渲染器對象,在最後使用gluDeleteNurbsRenderer銷毀它。代碼如下:

// NURBS 對象指針 GLUnurbsObj * pNurb = NULL; ... ... // 創建NURBS對象 pNurb = gluNewNurbsRenderer(); ... if (pNurb) gluDeleteNurbsRenderer(pNurb);

NURBS屬性

在創建了NURBS渲染器之後,我們需要設置NURBS的屬性。

//設置采樣容差

gluNurbsProperty(pNurb, GLU_SAMPLING_TOLERANCE, 25.0f);

//填充一個實體的表面

gluNurbsProperty(pNurb, GLU_DISPLAY_MODE, (GLfloat)GLU_FILL);

GLU_SAMPLING_TOLERANCE決定了網格的精細程度。GLU_FILL表示使用填充模式,相應的GLU_OUTLINE_POLYGON是線框模式。

定義表面

表面通過一組控制點和一個結點序列來定義。使用gluNurbsSurface函數來定義表面,這個函數要在gluBeginSurface和gluEndSurface中間:

// 渲染NURB // 開始NURB表面的定義 gluBeginSurface(pNurb); gluNurbsSurface(pNurb, // 指針指向NURBS渲染器 8 , Knots, // u定義域內的結點個數,以及結點序列 8 , Knots, // v定義域內的結點個數,以及結點序列 4 * 3 , // u方向上控制點的間隔 3 , // v方向上控制點的間隔 & ctrlPoints[ 0 ][ 0 ][ 0 ], // 控制點數組 4 , 4 , // u,v 的次數 GL_MAP2_VERTEX_3); // 表面的類型 // 完成定義 gluEndSurface(pNurb);

我們可以通過gluNurbsSurface來定義多個NURBS表面,但NURBS渲染器的屬性不會改變。一般情況下我們連續畫兩個不同的屬性的表面,比如很少需要相鄰的兩個曲面一個是填充型的一個是線框性的。控制點和結點的序列如下:

GLint nNumPoints = 4; // 4 X 4

//                 u  v  (x,y,z)    
GLfloat ctrlPoints[4][4][3]= {{{  -6.0f, -6.0f, 0.0f},    // u = 0,    v = 0
{      -6.0f, -2.0f, 0.0f},    //            v = 1
{   -6.0f,  2.0f, 0.0f},    //            v = 2    
{   -6.0f,  6.0f, 0.0f}}, //            v = 3

{{  -2.0f, -6.0f, 0.0f},    // u = 1    v = 0
{   -2.0f, -2.0f, 8.0f},    //            v = 1
{   -2.0f,  2.0f, 8.0f},    //            v = 2
{   -2.0f,  6.0f, 0.0f}},    //            v = 3

{{   2.0f, -6.0f, 0.0f }, // u =2        v = 0
{    2.0f, -2.0f, 8.0f }, //            v = 1
{    2.0f,  2.0f, 8.0f },    //            v = 2
{    2.0f,  6.0f, 0.0f }},//            v = 3

{{   6.0f, -6.0f, 0.0f},    // u = 3    v = 0
{    6.0f, -2.0f, 0.0f},    //            v = 1
{    6.0f,  2.0f, 0.0f},    //            v = 2
{    6.0f,  6.0f, 0.0f}}};//            v = 3

效果圖:

修剪

修剪的功能常用語消減NURBS表面的銳利的邊緣。我們也可以使用修剪的功能在表面上剪一個洞。下面的示例在表面剪一個三角形的洞:

static GLUnurbsObj *pNurb = NULL;

GLint nNumPoints = 4; // 4 X 4

//                 u  v  (x,y,z)    
GLfloat ctrlPoints[4][4][3]= {{{  -6.0f, -6.0f, 0.0f},    // u = 0,    v = 0
{      -6.0f, -2.0f, 0.0f},    //            v = 1
{   -6.0f,  2.0f, 0.0f},    //            v = 2    
{   -6.0f,  6.0f, 0.0f}}, //            v = 3

{{  -2.0f, -6.0f, 0.0f},    // u = 1    v = 0
{   -2.0f, -2.0f, 8.0f},    //            v = 1
{   -2.0f,  2.0f, 8.0f},    //            v = 2
{   -2.0f,  6.0f, 0.0f}},    //            v = 3

{{   2.0f, -6.0f, 0.0f }, // u =2        v = 0
{    2.0f, -2.0f, 8.0f }, //            v = 1
{    2.0f,  2.0f, 8.0f },    //            v = 2
{    2.0f,  6.0f, 0.0f }},//            v = 3

{{   6.0f, -6.0f, 0.0f},    // u = 3    v = 0
{    6.0f, -2.0f, 0.0f},    //            v = 1
{    6.0f,  2.0f, 0.0f},    //            v = 2
{    6.0f,  6.0f, 0.0f}}};//            v = 3

// Knot sequence for the NURB
GLfloat Knots[8] = {0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f};


void DrawPoints(void)
{
  glColor3ub(255, 0, 0);
  glPointSize(5.0f);
  glBegin(GL_POINTS);
    for (int i = 0; i < 4; ++i)
    {
      for (int j = 0; j < 4; ++j)
      {
        glVertex3fv(ctrlPoints[i][j]);
      }
    }
  glEnd();
}

// NURBS 出錯時的回調函數 
void CALLBACK NurbsErrorHandler(GLenum nErrorCode)
{
  char cMessage[100] = {0,};

  strcpy(cMessage, "NURBS error : ");
  strcat(cMessage, (char*)gluErrorString(nErrorCode));

  glutSetWindowTitle(cMessage);
}


void RenderScene(void)
{
  glColor3ub(0,0,220);

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();

  glRotatef(330.0f, 1.0f, 0.0f, 0.0f);
  //修剪框是一個閉合的環
  //外修剪框
  GLfloat outSidePts[5][2] = 
  {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}, {0.0f, 0.0f}};
  //內修剪框
  GLfloat inSidePts[4][2] =
  {{0.25f, 0.25f}, { 0.5f, 0.5f}, {0.75f, 0.25f},{0.25f, 0.25f} };

  gluBeginSurface(pNurb);
  //定義NURBS表面
  gluNurbsSurface(pNurb,
    8, Knots, //u定義域內的結點個數,以及結點序列
    8, Knots,//v定義域內的結點個數,以及結點序列
    4 * 3,  //u方向上的控制點間隔
    3,  //v方向上的控制點間隔
    &ctrlPoints[0][0][0], //控制點數組
    4, 4, //u v的次數
    GL_MAP2_VERTEX_3);//產生的類型

  //開始修剪
  gluBeginTrim(pNurb);
  gluPwlCurve(pNurb,
    5,  //修剪點的個數
    &outSidePts[0][0], //修剪點數組
    2, //點之間的間隔
    GLU_MAP1_TRIM_2);//修剪的類型
  gluEndTrim(pNurb);

  gluBeginTrim(pNurb);
  gluPwlCurve(pNurb, 4, &inSidePts[0][0], 2, GLU_MAP1_TRIM_2);
  gluEndTrim(pNurb);
  gluEndSurface(pNurb);

  DrawPoints();
  glPopMatrix();

  glutSwapBuffers();
}

在一對gluBeginSurface/gluEndSurface調用內部,調用gluBeginTrim函數開始修剪,調用gluPwlCurve函數指定一條修剪曲線,然後調用gluEndTrim完成曲線的修剪。這些修剪曲線必須根據單位參數方程u和v空間指定。這意味著u/v定義域被縮放到0.0到1.0之間。

gluPwlCurve函數定義一條由片段拼接成的線性曲線,實質上就是一些首尾相連的點。對曲線進行修剪時,順時針方向修剪的曲線將會丟棄它的內部。一般情況下,應該指定一條外部修剪曲線,它包圍了整個NURBS參數空間,然後在這個區域內部指定一個較小的修剪區域(順時針圍繞)。

曲面細分(Tesselation)

為了使OpenGl的速度盡可能快,所有的幾何圖元都必須是凸的。如果我們遇到復雜的幾何圖形(如下圖),要把它手工切分為多個凸多邊形工作量較大。OpenGL的GLU庫提供了曲面細分的特性,幫助我們處理復雜的圖形。

對上面的兩個幾何圖形我們可以進行如下圖的細分(這種分法不是唯一的)

鑲嵌器(Tessellatror)

曲面細分通過鑲嵌器對象來工作。鑲嵌器對象類似於二次方程狀態對象需要創建以及銷毀。

GLUtesselator *pTess = gluNewTes(); //注意不是GLUtessellator 少了個l

gluDeleteTess(pTess);

所有的曲面細分函數的第一個參數都是鑲嵌器對象。這樣就允許我們構造多個鑲嵌器,並在需要的時候很方便的進行切換,這也使得鑲嵌器只影響當前的工作對象。

鑲嵌器分解多邊形並渲染的步驟如下:

  1. 創建鑲嵌器對象
  2. 設置鑲嵌器的回調函數和鑲嵌器的狀態
  3. 開始一個多邊形
  4. 開始一個輪廓
  5. 把輪廓的頂點發送給鑲嵌器
  6. 結束一個輪廓
  7. 如果有更多的輪廓回到步驟4
  8. 結束多邊形

每個多邊形由一個或多個輪廓組成。上圖左邊的多邊形就只有一個輪廓,右邊的有個洞所以有兩個輪廓。曲線細分的工作是到步驟8才進行的。曲線細分會帶來一定性能的開銷。如果圖形是靜態的最好是把函數調用放到顯示列表中。

鑲嵌器的回調函數

在曲面細分的過程中,鑲嵌器會調用一系列你提供的回調函數。這些回調函數指定了頂點的信息以及開始和結束圖元。注冊回調函數的原型如下:

void gluTessCallback(GLUTesselator *tobj, GLenum which, void (*fn)());

第一個參數是鑲嵌器對象,第二個指定回調函數的類型,第三個是回調函數指針。

例如:

typedef GLvoid (_stdcall *CallBack)();

gluTessCallback(pTess, GLU_TESS_BEGIN, (CallBack)glBegin);

gluTessCallback(pTess, GLU_TESS_VERTEX, (CallBack)glVertex3dv);

gluTessCallback(pTess, GLU_TESS_END, (CallBack)glEnd);

上面三個函數分別指定了,新圖元開始的回調函數,為每一個頂點調用glVertex3dv,以及結束的回調函數。上面只是簡單的指定了OpenGL的函數,你也可以自定一個回調函數,裡面實現你想要的功能。還可以注冊出錯時的回調函數:

void tessError(GLenum code)
{
  const char *str = (const char*)gluErrorString(code);
  glutSetWindowTitle(str);
}

gluTessCallback(pTess, GLU_TESS_ERROR, (CallBack)tessError);

指定頂點數據

void gluTessBeginPolygon(GLUTesselator *tobj, void *data);

第一個參數為鑲嵌器對象指針,第二個參數是指向曲面細分處理相關聯的用戶自定義數據,這個數據可以用回調函數在細分的過程中發送回來。一般情況下,這個參數常常設置為NULL. 完成一個多邊形後調用下面的函數開始細分。

void gluTessEndPolygon(GLUTesslator *tobj);

下面兩個函數開始和結束輪廓,對應於步驟4和6:

void gluTessBeginContour(GLUTesselator *tobj);

void gluTessEndContour(GLUTesselator *tobj);

在這兩個函數中,添加輪廓的頂點:

void gluTessVertex(GLUTesselator *tobj, GLdouble v[3], void *data);

v參數包含了用於鑲嵌器計算的真實的頂點數據,data參數是指向頂點數據的指針,傳給指定的GLU_VERTEX的回調函數。第二個參數可以包含除了頂點之外的一些信息如顏色,法線等。如果我們自己定義了GLU_VERTEX的回調函數,那麼就可以使用data的數據了。

示例

一個佛羅裡達州的簡單輪廓,這個州裡面還有個奧基喬比湖的輪廓。通過右鍵菜單,我們可以在簡單的畫線環繞模式,外圍輪廓曲線細分模式,和復雜模式之間切換。

這裡面有個新函數gluTessProperty(pTess, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD);這個指定了輪廓線為奇數的是填充的,輪廓線是偶數的是镂空的。我們的湖的輪廓線在裡面,是第二個輪廓線,所以是镂空的。

代碼如下:

#include "gltools.h"
#include <math.h>

//外圍輪廓線
#define COAST_POINTS 24
GLdouble vCoast[COAST_POINTS][3] = {{-70.0, 30.0, 0.0 },
{-50.0, 30.0, 0.0 },
{-50.0, 27.0, 0.0 },
{ -5.0, 27.0, 0.0 },
{  0.0, 20.0, 0.0 },
{  8.0, 10.0, 0.0 },
{ 12.0,  5.0, 0.0 },
{ 10.0,  0.0, 0.0 },
{ 15.0,-10.0, 0.0 },
{ 20.0,-20.0, 0.0 },
{ 20.0,-35.0, 0.0 },
{ 10.0,-40.0, 0.0 },
{  0.0,-30.0, 0.0 },
{ -5.0,-20.0, 0.0 },
{-12.0,-10.0, 0.0 },
{-13.0, -5.0, 0.0 },
{-12.0,  5.0, 0.0 },
{-20.0, 10.0, 0.0 },
{-30.0, 20.0, 0.0 },
{-40.0, 15.0, 0.0 },
{-50.0, 15.0, 0.0 },
{-55.0, 20.0, 0.0 },
{-60.0, 25.0, 0.0 },
{-70.0, 25.0, 0.0 }};

//湖的輪廓線
#define LAKE_POINTS 4
GLdouble vLake[LAKE_POINTS][3] = {{ 10.0, -20.0, 0.0 },
{ 15.0, -25.0, 0.0 },
{ 10.0, -30.0, 0.0 },
{  5.0, -25.0, 0.0 }};

#define LINE_LOOP 1
#define TESS 2
#define COMPLEX 3
static int iMode = LINE_LOOP;

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

void tessError(GLenum code)
{
  const char *str = (const char*)gluErrorString(code);
  glutSetWindowTitle(str);
}

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

  glColor3f(1.0f, 0.0f, 1.0f);
  glPushMatrix();

  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
  switch(iMode)
  {
  case LINE_LOOP:
    {
      glBegin(GL_LINE_LOOP);
      for (int i = 0; i < COAST_POINTS; ++i)
      {
        glVertex3dv(vCoast[i]);
      }
      glEnd();
    }
    break;
  case TESS:
    {
      //創建鑲嵌器對象
      GLUtesselator *pTess = gluNewTess();
      //設置回調函數
      gluTessCallback(pTess, GLU_TESS_BEGIN, (CallBack)glBegin);
      gluTessCallback(pTess, GLU_TESS_END, (CallBack)glEnd);
      gluTessCallback(pTess, GLU_TESS_VERTEX, (CallBack)glVertex3dv);
      gluTessCallback(pTess, GLU_TESS_ERROR, (CallBack)tessError);

      //開始一個多邊形
      gluTessBeginPolygon(pTess, NULL);
      //開始一個輪廓
      gluTessBeginContour(pTess);
      //設置輪廓的頂點
      for (int i = 0; i < COAST_POINTS; ++i)
      {
        gluTessVertex(pTess, vCoast[i], vCoast[i]);
      }
      gluTessEndContour(pTess);
      gluTessEndPolygon(pTess);
      gluDeleteTess(pTess);
    }
    break;
  case COMPLEX:
    {
      GLUtesselator *pTess = gluNewTess();
      gluTessCallback(pTess, GLU_TESS_BEGIN, (CallBack)glBegin);
      gluTessCallback(pTess, GLU_TESS_END, (CallBack)glEnd);
      gluTessCallback(pTess, GLU_TESS_VERTEX, (CallBack)glVertex3dv);
      gluTessCallback(pTess, GLU_TESS_ERROR, (CallBack)tessError);

      //指定奇數的輪廓為填充,偶數的輪廓是镂空的。這也是默認的設置
      gluTessProperty(pTess, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD);

      gluTessBeginPolygon(pTess, NULL);

      gluTessBeginContour(pTess);
      for (int i = 0; i < COAST_POINTS; ++i)
      {
        gluTessVertex(pTess, vCoast[i], vCoast[i]);
      }
      gluTessEndContour(pTess);

      gluTessBeginContour(pTess);
      for (int i = 0; i < LAKE_POINTS; ++i)
      {
        gluTessVertex(pTess, vLake[i], vLake[i]);
      }
      gluTessEndContour(pTess);
      gluTessEndPolygon(pTess);
      gluDeleteTess(pTess);
    }
    break;
  default:
    break;
  }

  glPopMatrix();

  glutSwapBuffers();
}


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

  glViewport(0, 0, w, h);

  GLfloat aspect = (GLfloat)w/(GLfloat)h;
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  gluOrtho2D(-100.0, 100.0, -100.0, 100.0);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}

void ProcessMenu(int value)
{
  iMode = value;
  glutPostRedisplay();
}


int main(int args, char *argv[])
{
  glutInit(&args, argv);
  glutInitWindowSize(800, 600);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
  glutCreateWindow("florida");

  glutDisplayFunc(RenderScene);
  glutReshapeFunc(ChangeSize);
  glutCreateMenu(ProcessMenu);
  glutAddMenuEntry("LINE_LOOP", LINE_LOOP);
  glutAddMenuEntry("Tess", TESS);
  glutAddMenuEntry("Complex", COMPLEX);
  glutAttachMenu(GLUT_RIGHT_BUTTON);

  SetupRC();
  glutMainLoop();
  return 0;
}

用線環繞的模式:

單輪廓模式:

包含湖的輪廓

OpenGL超級寶典 第4版 中文版PDF+英文版+源代碼 見 http://www.linuxidc.com/Linux/2013-10/91413.htm

OpenGL編程指南(原書第7版)中文掃描版PDF 下載 http://www.linuxidc.com/Linux/2012-08/67925.htm

OpenGL 渲染篇 http://www.linuxidc.com/Linux/2011-10/45756.htm

Ubuntu 13.04 安裝 OpenGL http://www.linuxidc.com/Linux/2013-05/84815.htm

OpenGL三維球體數據生成與繪制【附源碼】 http://www.linuxidc.com/Linux/2013-04/83235.htm

Ubuntu下OpenGL編程基礎解析 http://www.linuxidc.com/Linux/2013-03/81675.htm

如何在Ubuntu使用eclipse for c++配置OpenGL http://www.linuxidc.com/Linux/2012-11/74191.htm

更多《OpenGL超級寶典學習筆記》相關知識 見 http://www.linuxidc.com/search.aspx?where=nkey&keyword=34581

Copyright © Linux教程網 All Rights Reserved