歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> OpenGL超級寶典學習筆記——紋理映射Mipmap

OpenGL超級寶典學習筆記——紋理映射Mipmap

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

Mipmapping

Mipmap是一個功能強大的紋理技術,它可以提高渲染的性能以及提升場景的視覺質量。它可以用來解決使用一般的紋理貼圖會出現的兩個常見的問題:

  • 閃爍,當屏幕上被渲染物體的表面與它所應用的紋理圖像相比顯得非常小時,就會出現閃爍。尤其當相機和物體在移動的時候,這種負面效果更容易被看到。
  • 性能問題。加載了大量的紋理數據之後,還要對其進行過濾處理(縮小),在屏幕上顯示的只是一小部分。紋理越大,所造成的性能影響就越大。

Mipmap就可以解決上面那兩個問題。當加載紋理的時候,不單單是加載一個紋理,而是加載一系列從大到小的紋理當mipmapped紋理狀態中。然後OpenGl會根據給定的幾何圖像的大小選擇最合適的紋理。Mipmap是把紋理按照2的倍數進行縮放,直到圖像為1x1的大小,然後把這些圖都存儲起來,當要使用的就選擇一個合適的圖像。這會增加一些額外的內存。在正方形的紋理貼圖中使用mipmap技術,大概要比原先多出三分之一的內存空間。

mipmap有多少個層級是有glTexImage的第二個參數level決定的。層級從0開始,0,1,2,3這樣遞增。如果沒有使用mipmap技術,只有第0層的紋理會被加載。在默認情況下,為了使用mipmap,所有層級都會被加載。但我們可以通過紋理參數來控制要加載的層級范圍,使用glTexParameteri, 第二個參數為GL_TEXTURE_BASE_LEVEL來指定最低層級的level,第二個參數為GL_TEXTURE_MAX_LEVEL指定最高層級的level。例如我只需要加載0到4層級的紋理:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4);

除此之外,我們還可通過GL_TEXTURE_MIN_LOD和GL_TEXTURE_MAX_LOD來限制紋理的使用范圍(最底層和最高層)。

Mipmap過濾

Mipmap的紋理過濾模式如下表:

常量 描述 GL_NEAREST 在mip基層上使用最鄰近過濾 GL_LINEAR 在mip基層上使用線性過濾 GL_NEAREST_MIPMAP_NEAREST 選擇最鄰近的mip層,並使用最鄰近過濾 GL_NEAREST_MIPMAP_LINEAR 在mip層之間使用線性插值和最鄰近過濾 GL_LINEAR_MIPMAP_NEAREST 選擇最鄰近的mip層,使用線性過濾 GL_LINEAR_MIPMAP_LINEAR 在mip層之間使用線性插值和使用線性過濾,又稱三線性mipmap

如果紋理過濾選擇為GL_NEAREST或GL_LINEAR模式,那麼只有基層的紋理會被加載,其他的紋理將會被忽略。我們必須指定其中一個mipmap過濾器,這樣才能使用所有已加載的紋理。這個mipmap過濾器的常量是GL_FILTER_MIPMAP_SELECTOR的形式。其中FLILTER指定了過濾模式,SELECTOR指定了如何選擇mipmap層。例如GL_NEAREST_MIPMAP_LINEAR模式,它的SELECTOR是GL_LINEAR,它會在兩個最鄰近的mip層中執行線性插值,然後得出的結果又由被選擇的過濾器GL_NEAREST進行過濾。

其中GL_NEAREST_MIPMAP_NEAAREST具有很好的性能,也能夠解決閃爍的問題,但在視覺效果上會比較差。其中GL_LINEAR_MIPMAP_NEAREST常用於游戲加速,使用了質量較高的線性過濾,和快速的選擇的方式(最鄰近方式)。

使用最鄰近的方式作為mipmap選擇器的效果依然不能令人滿意。從某一個角度去看,常常可以看到物體表面從一個mip層到另一個mip層的轉變。GL_LINEAR_MIPMAP_LINEAR和GL_NEAREST_MIPMAP_LINEAR過濾器在mip層之間執行一些額外的線性插值,以消除不同層之間的變換痕跡,但也需要一些額外的性能開銷。GL_LINEAR_MIPMAP_LINEAR具有最高的精度。

構建Mip層

mip貼圖需要加載更小的基本紋理圖像以便使用。但我們手頭上沒有這些更小的紋理圖像,怎麼辦呢。GLU函數庫提供了一個很方便的方法gluBuildMipmaps,它會幫我們縮放圖像並通過類似glTexImage的函數加載圖像。支持1維、2維、3維的圖像,函數原型如下:

int gluBuild1DMipmaps(GLenum target, GLint internalFormat, GLint width, GLenum format, GLenum type, const void *data);

int gluBuild2DMipmaps(GLenum target, GLint internalFormat, GLint width, GLint height, GLenum format, GLenum type, const void *data);

int gluBuild3DMipmaps(GLenum target, GLint internalFormat, GLint width, GLint height, GLint depth, GLenum format, GLenum type, const void *data);

參數的意義與glTexImage相同。但沒有level參數來指定mipmap的層級,也不支持紋理邊界。使用這個函數未必能夠獲得高質量的較小的紋理貼圖,只是比較方便。要使用高質量的不同比例的紋理貼圖,最好是自己手工制作,然後加載。GLU庫是使用box過濾器(簡單地就是對給定范圍的像素進行加權平均,例如7X7的box filter,你就需要對49個像素進行平均)

新版的GLU庫中可以使用gluBuild*MipmapLevels來更好的控制加載的紋理層級

int gluBuild1DMipmapLevels(GLenum target, GLint internalFormat, GLint width, GLenum format, GLenum type, GLint base, GLint max, const void *data);

int gluBuild2DMipmapLevels(GLenum target, GLint internalFormat, GLint width, GLint height, GLenum format, GLenum type, GLint base, GLint max, const void *data);

int gluBuild3DMipmapLevels(GLenum target, GLint internalFormat, GLint widht, GLint height, GLint depth, GLenum format, GLenum type, GLint base, GLint max, const void *data);

創建從base到max層的紋理數據。

Mipmaps 硬件生成

使用OpenGL的硬件加速來生成所需要的紋理。函數調用如下:

glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);

當這個參數被設置為GL_TRUE時,所有調用glTexImage或者glTexSubImage都會自動更新紋理貼圖(第0級)和所有更低層的紋理。通過使用硬件加速會比使用glu庫中的gluBuildMipmap要快,但這個特性本來只是個擴展,在OpenGL1.4才被納入OpenGL核心API的。

LOD(多細節層次)偏好

當mipmapping被啟用時,OpenGL會根據各個mip層的大小和幾何圖形在屏幕上的面積來決定哪一個mip層被選擇。OpenGL會選擇最合適的mip貼圖層與屏幕上的紋理表示形式進行匹配。我們可以告訴OpenGL向後(選擇更大的mip層)或向前(選擇更小的mipmap層)來調整選擇的標准。使用更小的mip層可以提高性能,選擇更大的mip層可以銳化紋理映射的對象。這個偏好設置示例如下:

glTexEnvf(GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, –1.5);

上面會使的細節紋理層傾向於使用更高層的細節(更小的level層參數),從而使得紋理的外觀更為銳利,代價是紋理處理的開銷大一些。

紋理對象

glTexImage, glTexSubImage和gluBuildMipmaps這些函數的調用消耗的時間特別多。這些函數大量的移動內存,有時需要重新調整數據的格式來適應一些內部的表示。在紋理之間切換或者重新加載不同的紋理圖片會帶來較大的性能開銷。

為了減少這些開銷,我們可以使用紋理對象。紋理對象允許你一次性加載多個紋理狀態(包括紋理圖像),然後在它們之間快速切換。紋理狀態由當前綁定的紋理對象來維護。紋理的名稱由unsigned int類型來標識。使用下面的函數來生成一定數量的紋理對象:

void glGenTextures(GLsizei n, GLuint *textures);

上面的函數調用指定了紋理對象的數量,和存儲紋理對象名稱的數組。我們可以通過紋理對象名稱來操作紋理狀態。綁定其中的一個紋理狀態的函數調用如下:

void glBindTexture(GLenum target, GLuint texture);

target參數必須是GL_TEXTURE_1D,GL_TEXTURE_2D或者GL_TEXTURE_3D.texture是紋理名稱指定要綁定的紋理對象。在此函數之後,紋理圖像的加載和紋理參數的設置都只影響當前綁定的紋理對象。最後刪除紋理對象的函數如下:

void glDeleteTextures(GLsizei n, GLuint *texture);

參數的意義與glGenTextures相同。不一定需要每次產生紋理對象使用後就刪除所有的紋理對象。多次調用glGenTextures的開銷較小,但多次調用glDeleteTextures會有導致一些延遲,原因是需要釋放大量的能存空間。在不再需要此紋理對象時,要把該紋理對象刪除,防止內存洩露。

判斷紋理對象名稱是否可用可以通過下面的函數調用來判斷:

GLboolean glIsTexture(GLuint texture);

返回GL_TRUE代表可用,GL_FALSE代表不可用。

管理多個紋理

一般而言,在程序初始化時加載多個紋理對象,然後在渲染期間不斷地切換,在不再使用時刪除紋理對象。下面是一個通道tunnel的例子,此例在啟動時加載三個紋理對象,然後通過切換來繪制通道的磚牆,天花板和地板。此例中還演示了不同的mipmap模式,通過右鍵菜單來切換,通過上下箭頭鍵來在通道中移動,通過左右鍵來旋轉通道。

完整示例如下:

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

//定義宏常量
#define CEILING 0
#define BRICK 1
#define FLOOR 2
#define TEXTURENUM 3

//紋理圖像的路徑
const char* texFileName[] = {"..\\ceiling.tga","..\\brick.tga","..\\floor.tga"};

//紋理對象名稱
static GLuint textureName[TEXTURENUM];

//旋轉與移動
static GLfloat yRot = 0.0f;
static GLfloat zPos = 0.0f;

//切換不同的紋理模式
void ProcessMenu(int value)
{
switch (value)
{
case 0:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
break;
case 1:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
break;
case 2:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
break;
case 3:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
break;
case 4:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
break;
case 5:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
break;
case 6:
if (gltIsExtSupported("GL_EXT_texture_filter_anisotropic"))
{

//開啟各向異性過濾
GLfloat fLargest;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &fLargest);
printf("anisotropic:%f\n", fLargest);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, fLargest);
}
break;
default:
break;
}

glutPostRedisplay();
}

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

//開啟深度測試,消除隱藏面,避免後畫的牆畫到前面來
glEnable(GL_DEPTH_TEST);

//紋理圖像的信息
GLint iWidth, iHeight, iComponents;
GLenum eFormat;

//設置紋理環境
glTexEnvi(GL_TEXTURE_2D, GL_TEXTURE_ENV, GL_REPLACE);

//生成紋理對象
glGenTextures(TEXTURENUM, textureName);

for (int i = 0; i < TEXTURENUM; ++i)
{
void *pImage = gltLoadTGA(texFileName[i], &iWidth, &iHeight, &iComponents, &eFormat);

if (pImage)
{

//綁定紋理對象,生成mipmap
glBindTexture(GL_TEXTURE_2D, textureName[i]);
gluBuild2DMipmaps(GL_TEXTURE_2D, iComponents, iWidth, iHeight, eFormat, GL_UNSIGNED_BYTE, pImage);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
free(pImage);
}

glEnable(GL_TEXTURE_2D);

}


void ShutdownRC()
{
//最後刪除紋理對象
glDeleteTextures(TEXTURENUM, textureName);
}

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

//移動和旋轉
glTranslatef(0.0f, 0.0f, zPos);
glRotatef(yRot, 0.0f, 1.0f, 0.0f);

for(GLfloat z = -60.0f; z <= 0.0f; z += 10.0f)
{
//綁定地板紋理繪制地板,注意glBeindTexture在glBegin和glEnd中是無效的
glBindTexture(GL_TEXTURE_2D, textureName[FLOOR]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z);

glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z + 10.0f);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z + 10.0f);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z);

glEnd();

//綁定天花板紋理
glBindTexture(GL_TEXTURE_2D, textureName[CEILING]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z);

glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z + 10.0f);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z + 10.0f);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z);
glEnd();

//綁定磚牆的紋理
glBindTexture(GL_TEXTURE_2D, textureName[BRICK]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z);

glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(-10.0f, 10.0f, z + 10.0f);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(-10.0f, -10.0f, z + 10.0f);

glTexCoord2f(0.0f, 0.0f);
glVertex3f(10.0f, -10.0f, z);

glTexCoord2f(1.0f, 0.0f);
glVertex3f(10.0f, 10.0f, z);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z + 10.0f);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z + 10.0f);
glEnd();
}

//GLclampf prioritize[TEXTURENUM] = {0.0f, 0.0f, 1.0f};
//glPrioritizeTextures(TEXTURENUM, textureName, prioritize);
//GLboolean isResident[TEXTURENUM];
//if (glAreTexturesResident(TEXTURENUM, textureName, isResident))
//{
// printf("all texture is resident\n");
//}
//else
//{
// printf("texture resident is : %d %d %d", isResident[0], isResident[1], isResident[2]);
//}
glPopMatrix();
glutSwapBuffers();
}

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

glViewport(0, 0, w, h);

GLfloat aspect = (GLfloat)w/(GLfloat)h;

glMatrixMode(GL_PROJECTION);
glLoadIdentity();

gluPerspective(35.5, aspect, 1.0, 150.0);

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

glutPostRedisplay();
}

void SpecialKey(int value, int x, int y)
{
if (value == GLUT_KEY_LEFT)
{
yRot += 0.5f;
}

if (value == GLUT_KEY_RIGHT)
{
yRot -= 0.5f;
}

if (value == GLUT_KEY_UP)
{
zPos += 0.5f;
}

if (value == GLUT_KEY_DOWN)
{
zPos -= 0.5f;
}

if (yRot > 365.5f)
{
yRot = 0.0f;
}

glutPostRedisplay();
}

int main(int arg, char **argv)
{
glutInit(&arg, argv);
glutInitDisplayMode(GL_RGB | GL_DOUBLE | GL_DEPTH);
glutInitWindowSize(800, 600);
glutCreateWindow("tunel");

glutReshapeFunc(ChangeSize);
glutDisplayFunc(RenderScene);
glutSpecialFunc(SpecialKey);
glutCreateMenu(ProcessMenu);
glutAddMenuEntry("GL_NEAREST", 0);
glutAddMenuEntry("GL_LINEAR", 1);
glutAddMenuEntry("GL_NEAREST_MIPMAP_NEAREST", 2);
glutAddMenuEntry("GL_LINEAR_MIPMAP_NEAREST", 3);
glutAddMenuEntry("GL_NEAREST_MIPMAP_LINEAR", 4);
glutAddMenuEntry("GL_LINEAR_MIPMAP_LINEAR", 5);
glutAddMenuEntry("ANISOTROPIC", 6);
glutAttachMenu(GLUT_RIGHT_BUTTON);

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

GL_NEAREST效果圖(紋理比較銳利):

GL_LINEAR_MIPMAP_LINEAR效果圖(線性插值後的紋理過渡較平滑):

在我們程序初始化時,生成了多個紋理對象,加載了多個紋理。這樣我們在使用時就非常的方便,只要通過glBindTexutre切換紋理就可以了。使用完之後就釋放紋理。

for(GLfloat z = -60.0f; z <= 0.0f; z += 10.0f)
{
//綁定地板紋理繪制地板,注意glBeindTexture在glBegin和glEnd中是無效的
glBindTexture(GL_TEXTURE_2D, textureName[FLOOR]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z);

glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z + 10.0f);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z + 10.0f);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z);

glEnd();

//綁定天花板紋理
glBindTexture(GL_TEXTURE_2D, textureName[CEILING]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z);

glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z + 10.0f);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z + 10.0f);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z);
glEnd();

//綁定磚牆的紋理
glBindTexture(GL_TEXTURE_2D, textureName[BRICK]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z);

glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(-10.0f, 10.0f, z + 10.0f);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(-10.0f, -10.0f, z + 10.0f);

glTexCoord2f(0.0f, 0.0f);
glVertex3f(10.0f, -10.0f, z);

glTexCoord2f(1.0f, 0.0f);
glVertex3f(10.0f, 10.0f, z);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z + 10.0f);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z + 10.0f);
glEnd();
}
tunnel示例中,切換mipmap紋理過濾器時,只修改了縮小過濾器

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

典型的情況下,在OpenGL選擇了最大的可用mip層之後,就沒有更大的mip層可供選擇了。這相當於設置了一條門檻,更大的圖形也只能使用這個mip層,沒有更大的mip層了。

常駐紋理

在大多數的OpenGL實現中,都提供了一定數量的常駐紋理來提高性能。顯卡的內存(顯存)是有限的,在紋理切換中當顯存不夠時,就要把一些紋理數據從顯存中移除(暫時存放到硬盤或內存中)。

為了優化性能,OpenGL會自動的把最經常使用的紋理保存在顯存中作為常駐紋理。判斷當前紋理是否是常駐紋理,通過下面的函數調用來測試:

GLboolean glAreTexturesResident(GLsizei n, const GLuint *texture, GLboolean *residences);

第一個參數是紋理對象的個數,第二是紋理對象名稱數組,第三個是輸出參數,記錄著對應的紋理是否是常駐紋理。如果所有的紋理對象都是常駐紋理則glAreTexutresResident返回GL_TRUE。

紋理優先級

默認情況下,OpenGL實現是使用最經常使用(MFFU)來決定紋理是否常駐顯存。如果最經常使用的紋理很小,而大紋理不經常使用,那麼就會導致小紋理常駐顯存,而大紋理卻經常被移除。為了避免此問題,我們可以手動設置紋理的優先級。通過下面的函數調用:

void glPrioritizeTextures(GLsizei n, const GLuint *texture, const GLclampf *priorites);

第一個參數是紋理對象的個數,第二個是紋理名稱數組,第三個是紋理的優先級參數,范圍為[0.0,1.0]。低優先級告訴OpenGL當顯存不夠時有限考慮移除此低優先級的紋理。示例:

GLclampf prioritize[TEXTURENUM] = {0.0f, 0.0f, 1.0f};
glPrioritizeTextures(TEXTURENUM, textureName, prioritize);
GLboolean isResident[TEXTURENUM];
if (glAreTexturesResident(TEXTURENUM, textureName, isResident))
{
printf("all texture is resident\n");
}
else
{
printf("texture resident is : %d %d %d", isResident[0], isResident[1], isResident[2]);
}

回顧

在紋理這一章,我們學習如何加載紋理glTexImage,設置紋理參數glTexParameter,設置紋理環境glTexEnv,使用mipmap,管理紋理對象,多紋理的使用和切換,紋理過濾器等等。詳細的介紹了紋理的各種參數。

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