上一次,我們得到了一個打開的窗口并等待渲染我們的hello world程序的指令。但是在我們實際畫任何東西之前,我們必須通過創建各種各樣的對象并將它們作為數據傳給OpenGL。讓我們過一遍我們需要設置的對象: 再看管線 回顧一下我們第一章中的[圖像管線](),這次從我們的"hello world"程序的視角,我們需要哪些對象就很清晰了。從輸入結束開始,我們的頂點數組包含四個頂點,頂點著色器將會把它們賦值給窗口的各個角。元素數組會將這四個頂點組合成兩個三角形,形成一個覆蓋窗口的矩形。我們會創建一些小的緩沖對象來將這些數組存儲在顯存中。我們 uniform 狀態將由我們的兩個"hello"圖片以及用于將它們混色的因子組成。這些圖片每個需要一個紋理對象。除了將我們的頂點映射到屏幕的角上,頂點著色器還會將一系列的紋理坐標賦值給每個頂點,將頂點映射到它所對應的紋理的角上。然后光柵化過程將會使用紋理坐標對矩形區域表面進行插值,這樣,最終我們的像素著色器可以對兩個紋理進行取樣并將它們使用一個混合因子進行混色。為了將著色器加入到OpenGL里面,我們創建一個program對象來將頂點著色器和像素著色器鏈接起來。在這篇文章中,我們將設置好緩沖對象和紋理對象;下一次,我們將操作著色器。 OpenGL中的C類型 OpenGL定義了它自己的跟標準C類型相對應的GL*類型:GLubyte,GLbyte,GLushort,GLshort,GLuint,GLint,GLfloat和GLdouble。OpenGL還提供了一些更具有語義的類型定義: GLchar*,用于處理以null結束的ASCII字符串 GLclampf和GLclampd,它們只是GLfloat和GLdouble的typedef,但是用于表示范圍在0到1之間的值 GLsizei,是整型的typedef,用于表示內存塊的大小,類型于標準C庫中的size_t GLboolean,是GLbyte的typedef目的是存GLTRUE或者GLFALSE,類似于C++或者C99中的bool GLenum,是GLuint的typedef用于存一個預定義的 GL_* 常量 GLbitfield,又是一個GLuint的typedef,用于存位組或者一個或多個GL*BIT mask 存儲我們的資源 static struct { GLuint vertex_buffer, element_buffer; GLuint textures[2]; /* fields for shader objects ... */ } g_resources; 在這里,使用一個像gresources這樣的全局結構體變量用于在我們的初始化代碼和GLUT回調之間共享數據是最簡單的。OpenGL使用GLuint值作為對象的句柄。我們的gresources結構體中包含兩個GLuint域,我們將用它存放我們的頂點名和緩沖對象的元素數組。我們將添加更多的域來存放我們的著色器對象,當我們在下篇文章中創建它們時。 OpenGL對象模型 OpenGL操作對象的約定有點不同尋常。你可以通過使用glGen*s函數(例如glGenBuffers或者glGenTextures)來創建一個或多個對象。正如前面提到的,得到的句柄是GLuint值。任何由對象所擁有或者關聯的數據都是由OpenGL內部管理的。這是很典型的。你如何使用這些句柄就是不一樣的地方:為了操作一個對象,你先要通過調用相應的glBind*函數(glBindBuffer或者glBindTexture)綁定到一個OpenGL定義的目標。然后你將target作為參數提供給OpenGL調用,這個OpenGL調用會設置屬性或者上傳數據到綁定的對象中。目標綁定還影響到一些不顯示使用目標作為參數的相關的OpenGL調用,后面我們討論渲染的時候會看到的。現在,我們看看創建完緩沖對象的模板是什么樣子的: 緩沖對象 static GLuint make_buffer( GLenum target, const void *buffer_data, GLsizei buffer_size ) { GLuint buffer; glGenBuffers(1, &buffer); glBindBuffer(target, buffer); glBufferData(target, buffer_size, buffer_data, GL_STATIC_DRAW); return buffer; } 緩沖對象是交給OpenGL管理的內存。它們用于存儲頂點數組(使用GLARRAYBUFFER)和元素數組(使用GLEMEMENTARRAY)。當你使用glBufferData分配一個緩沖時,你提供一個使用提示來表明你想要改變緩沖中數據的頻率,OpenGL將基于這個提示決定最好是將它的數據存儲在CPU還是GPU。這個提示實際上并不會限制緩沖的使用方式,但是以與提示不符的方式去使用會導致性能低下。在我們的程序中,我們的頂點和元素數組都是常量,不需要改變,因此我們給了glBufferData一個GLSTATICDRAW的提示。其中STATIC部分表明我們不會想去改變數據。緩沖的提示還可以設置為DYNAMIC,表明我們頻繁地寫到這個緩沖里,或者STREAM,表明我們將周期性地替換掉緩沖的內容。DRAW部分表明我們希望緩沖只會被GPU讀取。與DRAW相對的是READ,表明一個緩沖主要會被CPU讀回去,還有COPY,表明這個緩沖是CPU和GPU之間的一個管道,不應該偏重于任一方。頂點數組和元素數組幾乎總是使用GL*DRAW提示。 static const GLfloat g_vertex_buffer_data[] = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f }; static const GLushort g_element_buffer_data[] = { 0, 1, 2, 3 }; glBufferData看待你的數據源很類似memcpy:僅僅就是一串沒有特別意義的字節流。直到我們渲染它們之前,我們不會告訴OpenGL我們數組的結構。這允許緩沖以幾乎任何格式存儲頂點屬性以及其它數據,或者同一份數據給不同的渲染任務以不同的方式去處理。在這里,我們僅僅是以四個兩元素向量的集合指定我們的矩形的角。 我們的元素矩陣也很簡單,一個GLushorts數組依次索引四個頂點元素,這樣就可以將它們匯編成一個矩形的三角形帶。在桌面版OpenGL,一個元素數組可以由8位GLubyte,16位GLushort,或者32位GLuint成員組成;對于OpenGL ES,只可以使用GLubyte或者GLushort。我們現在像下面這樣在我們的make_resource中調用make_buffer來分配和填充我們的緩沖: static int make_resources(void) { g_resources.vertex_buffer = make_buffer( GL_ARRAY_BUFFER, g_vertex_buffer_data, sizeof(g_vertex_buffer_data) ); g_resources.element_buffer = make_buffer( GL_ELEMENT_ARRAY_BUFFER, g_element_buffer_data, sizeof(g_element_buffer_data) ); /* make textures and shaders ... */ } 紋理對象 static GLuint make_texture(const char *filename) { GLuint texture; int width, height; void *pixels = read_tga(filename, &width, &height); if (!pixels) return 0; 就像我在上篇文章中提到的,我使用TGA格式來存儲我們的"hello world"圖片。我不會在這里浪費時間分析代碼;如果你想看它的話,它在Github倉庫的util.c。TGA的像素數據以順序的,未壓縮的三字節RGB一組打包的數組存儲(實際上是以BGR的順序),像素的順序是從圖片的左下角開始,然后從那里向右,再然后向上。接下來我們將看到,這種格式用于OpenGL紋理非常好。如果讀圖片失敗,我們返回0,它是絕不會被真正的OpenGL對象使用的"空對象"名字。 glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); 紋理對象提供處理結構化數組的GPU內存專門用于存儲紋理數據。OpenGL支持多種類型的紋理,每種都有它自己的紋理目標,包括1D(GLTEXTURE1D),2D(GLTEXTURE2D)和3D(GLTEXTURE3D)紋理。還有一些更特殊的紋理類型我們在接下來可能會遇到。2D紋理目前是最常見的類型。這里我們為我們的圖片生成并綁定一個GLTEXTURE2D紋理。紋理對象和緩沖對象不同,因為GPU處理紋理內存和緩存內存有著很大的著別。 紋理取樣和紋理參數 頂點數組是一次一個元素地提供給頂點著色器,并且頂點著色器沒有任何方式訪問到其它的元素。然而在頂點著色器或者像素著色器的任何調用中,整個紋理內容都是可用的。著色器在一個或多個浮點數紋理坐標中取樣。紋理數組中的元素均勻地分布到紋理空間中,紋理空間是一個正方型的坐標跨度從(0,0)到(1,1)(或者一個0-1的線性劃分,對于1D紋理,或者是一個正方體的劃分從(0,0,0)到(1,1,1)對于3D紋理)。為了和對象空間的x,y,z坐標進行區分,OpenGL像紋理空間的坐標軸標記為s,t,r。紋理空間均勻地分布在軸線上形成矩形的單元格,與原數組的寬高一至。格子邊界(0,0)映射到紋理空間的第一個元素,隨后的元素沿s坐標軸和t坐標軸向右和向上分布。在這些格子中心對紋理進行取樣得到相應的紋理數組中的元素。 注意,t坐標軸可以被看作向上或者向下(事實上或者是任何方向)增長的,依賴于底層數組表示。紋理空間的另外一個坐標軸也同樣是任意的。由于TGA圖片將它的像素自左向右,自下向上的存儲,這就是我所描繪的坐標軸。 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_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); 如何對紋理格子之間的紋理,或者是坐標在0-1范圍之外的紋理進行取樣,是由glTexParameteri函數的紋理參數控制的。 參數 GLTEXTUREMINFILTER 和 GLTEXTUREMAGFILTER 分別控制當分辨率高于或低于紋理自身的分辨率時,落于樣本的像素點之間的取樣。我們將它們設置為 GLLINEAR 來告訴GPU我們使用線性插值來對最接近取樣點的四個點進行平滑的混色。如果用戶改變窗口大小,紋理圖片將平滑地縮放。設置填充參數為GLNEAREST將告訴GPU返回離取樣點最近的紋理元素,這會導到縮放時像素縮放時會有鋸齒。 參數 GLTEXTUREWRAPS 和 GLTEXTUREWRAPT 控制當坐標超出坐標軸中0-1范圍時如何處理;在這里,我們不打算對范圍之外進行取樣,因此我們使用GLCLAMPTOEDGE,它將坐標限制在(0,0)到(1,1)。如果一個或者兩個坐標軸的參數是GLWRAP將造成紋理圖片在紋理空間中沿坐標軸無限地重復。 如果抽象地說,紋理取樣可能聽起來就像復雜的2D數組索引。如果我們看一下我們的像素著色器是如何采樣紋理的可能會更有意義: 在我們的頂點著色器中,我們會將紋理空間的角賦值給我們的矩形頂點。當光柵化的矩形的大小和紋理大小匹配時(也就是,我們的窗口大小和圖片大小一致),片元著色器會一像素一像素地取樣,正如左圖中你所看到的。如果矩形的光柵化大小和紋理不匹配,每個片元將會在我們的紋理格子中心取樣,線性濾波將使我們在紋理元素之間得到一個平滑的梯度,正如右邊所示。 分配紋理 glTexImage2D( GL_TEXTURE_2D, 0, /* target, level of detail */ GL_RGB8, /* internal format */ width, height, 0, /* width, height, border */ GL_BGR, GL_UNSIGNED_BYTE, /* external format, type */ pixels /* pixels */ ); free(pixels); return texture; } glTexImage2D(或者-1D或-3D)函數為紋理分配內存。紋理可以有多個levels of detail,當從更低分辨率取樣時可以依次從更小的"mipmaps"層次中取樣,但是在這里我們只是提供基本的第0級。不像glBufferData,glTexImage2D要求對分配內存的所有的格式信息預先提出。internal format參數告訴GPU每個紋理元素使用的顏色組分,以及以什么樣的精度存儲。OpenGL支持各種的不同圖片格式;這里我將只提一下我們所使用的。我們的TGA文件使用24位的RGB像素,換句話說,每個像素由三個8位組成。這個對應于GL_RGB8內部格式。寬度和高度參數指定紋理元素在s和t坐標軸上的數目(border參數是廢棄的并且總是應該設置為0)。外部格式和類型參數聲明了我們的像素的組成順序和類型,我們的像素指向一個width*height打包的特定格式的紋理元素。TGA以BGR順序采用unsigned byte存儲它的像素,因此我們的外部格式參數使用GLBGR,類型使用GLUNSIGNED_BYTE。 讓我們在我們的make_resources函數中添加一些make_texture調用來創建我們的紋理對象: static int make_resources(void) { /* ... make buffers */ g_resources.textures[0] = make_texture("hello1.tga"); g_resources.textures[1] = make_texture("hello2.tga"); if (g_resources.textures[0] == 0 || g_resources.textures[1] == 0) return 0; /* make shaders ... */ } 接下來,著色器 我們現在已經準備好我們的頂點和圖片數據了,并且準備好啟動我們的繪圖管線。下一步將是寫著色器來通過GPU操控數據并將它加載到屏幕上。這將是我們這一章的下一部分要做的。 |