緩沖和紋理包含了OpenGL程序所需要的原材料,但是沒(méi)有著(zhù)色器,它們只是無(wú)效的字節塊。如果你還記得我們概要中的繪圖管線(xiàn),渲染需要一個(gè)頂點(diǎn)著(zhù)色器將我們的頂點(diǎn)映射到屏幕空間,還需要一個(gè)片元著(zhù)色器,對生成的三角形的光柵化片元進(jìn)行著(zhù)色。OpenGL中的著(zhù)色器是使用一種叫作GLSL(GL Shading Language)的語(yǔ)言寫(xiě)的,它看起來(lái)跟C語(yǔ)言很像。在這篇文章中,我們將展示我們的"hello world"程序的著(zhù)色器代碼,然后寫(xiě)C代碼來(lái)加載,編譯并將它鏈接到OpenGL。 頂點(diǎn)著(zhù)色器 這個(gè)是我們的頂點(diǎn)著(zhù)色器的GLSL代碼,在hello-gl.v.glsl中: #version 110 attribute vec2 position; varying vec2 texcoord; void main() { gl_Position = vec4(position, 0.0, 1.0); texcoord = position * vec2(0.5) + vec2(0.5); } 我先總結這個(gè)著(zhù)色器做什么事情,然后再給出關(guān)于GLSL更多的一些細節。這個(gè)著(zhù)色器首先將頂點(diǎn)的屏幕坐標賦值到gl_Position,它是GLSL提供的一個(gè)預定義變量。在屏幕空間中,坐標(-1,-1)和(1,1)分別代表framebuffer的左下角和右上角;由于我們的頂點(diǎn)數組也是這樣的矩形,我們可以直接拷貝每個(gè)頂點(diǎn)position值的x和y。gl_Position的另外兩個(gè)向量組成是用于深度測試和透視投影(譯者:perspective projection是專(zhuān)業(yè)術(shù)語(yǔ)吧?如何翻譯);我們將在下一節用于3D數學(xué)的時(shí)候好好看一下它們,F在,我們僅僅是將它們的值設為0和1。著(zhù)色器然后做了一些數學(xué)計算來(lái)將我們的屏幕空間點(diǎn)positions從屏幕空間(-1到1)映射到紋理空間(0到1)并將結果賦值給頂點(diǎn)的texcoord。 ![]() 跟C很相似,GLSL著(zhù)色器從main函數開(kāi)始執行,在GLSL中main函數不接受參數并返回void。GLSL借用了C的預處理關(guān)鍵字用于它自己的指令。#version指令表明下面源代碼的GLSL版本;我們的#version聲明了我們使用GLSL版本1.10(GLSL版本跟OpenGL版本綁定得很緊;1.10是對應于OpenGL 2.0)。GLSL去掉了指針和大多數的C中的各種大小的數值類(lèi)型,只保留了常用的bool,int和float類(lèi)型,但是它添加了一系列的向量和矩陣類(lèi)型,長(cháng)度最多為4個(gè)單元大小。這里你看到的vec2和vec4類(lèi)型分別是兩元素和四元素的float向量。類(lèi)型名也可以作為這些類(lèi)型的構造函數使用;你可以使用單值構造一個(gè)向量,構成的向量的每個(gè)元素都將是這個(gè)值,或者從向量和單值的混合構造,它們會(huì )綁到一起成為一個(gè)更大的向量。GLSL的數學(xué)操作和一些內置函數是定義在這些向量類(lèi)型之上的,可以執行元素級的計算。除了數值類(lèi)型,GLSL還提供特殊的sampler數據類(lèi)型用于紋理取樣,在下面片元著(zhù)色器中我們將會(huì )看到。這些基本類(lèi)型可以集合成數組和用戶(hù)自定義的struct類(lèi)型。 頂點(diǎn)著(zhù)色器使用GLSL程序中特殊定義的全局變量和繪圖管線(xiàn)環(huán)境進(jìn)行通信。它的輸入來(lái)自于uniform變量以及attribute變量,分別提供狀態(tài)值和頂點(diǎn)數組的每個(gè)頂點(diǎn)屬性。著(zhù)色器將它的每個(gè)頂點(diǎn)輸出賦值到varying變量。GLSL預定義了一些varying變量來(lái)接收繪圖管線(xiàn)中使用的特殊的輸出,包括這里我們使用的gl_Position變量。 片元著(zhù)色器 現在讓我們看一下片元著(zhù)色器源代碼,在hello-gl.f.glsl中: #version 110 uniform float fade_factor; uniform sampler2D textures[2]; varying vec2 texcoord; void main() { gl_FragColor = mix( texture2D(textures[0], texcoord), texture2D(textures[1], texcoord), fade_factor ); } 在片元著(zhù)色器中,有些輕微的變化。varying變量成了這里的輸入:每個(gè)片元著(zhù)色器中的varying變量是跟頂點(diǎn)著(zhù)色器中的同名變量鏈接在一起的,并且對這個(gè)變量,每個(gè)片元著(zhù)色器調用都接收到一個(gè)光柵化的頂點(diǎn)著(zhù)色器的輸出。片元著(zhù)色器也給出了一系列不同的gl*預定義變量。glFragColor是其中最重要的,著(zhù)色器將會(huì )給它一個(gè)vec4的RGBA顏色值。片元著(zhù)色器可以訪(fǎng)問(wèn)到跟頂點(diǎn)著(zhù)色器同樣的uniform系列,但是不能訪(fǎng)問(wèn)到attribute變量。 ![]() 我們的片元著(zhù)色器使用GLSL內置的texture2D函數來(lái)對兩個(gè)紋理從texcoord的uniform狀態(tài)進(jìn)行取樣。然后它調用內置的mix函數基于當前的fade_factor值對兩個(gè)紋理值進(jìn)行組合:0會(huì )輸出只有第一個(gè)紋理的取樣,1只會(huì )輸出第二個(gè)紋理的取樣,而中間的值會(huì )給出兩者的一個(gè)混色。 既然我們已經(jīng)察看了GLSL著(zhù)色器代碼,讓我們回到C并加載著(zhù)色器到OpenGL。 存儲我們的著(zhù)色器對象 static struct { /* ... fields for buffer and texture objects */ GLuint vertex_shader, fragment_shader, program; struct { GLint fade_factor; GLint textures[2]; } uniforms; struct { GLint position; } attributes; GLfloat fade_factor; } g_resources; 首先,讓我們添加一些域到我們的gresources結構體中,存儲我們的著(zhù)色器對象名字和創(chuàng )建后的程序對象。類(lèi)似緩沖和紋理對象,著(zhù)色器和程序對象也是用GLuint句柄命名。我們還添加了一些域來(lái)存放整型變量,我們需要在我們的著(zhù)色器的uniform和attribute變量引用它們。最后,我們添加了一個(gè)域來(lái)存浮點(diǎn)數值,我們將在每一幀把fadefactor賦值給它。 編譯著(zhù)色器對象 static GLuint make_shader(GLenum type, const char *filename) { GLint length; GLchar *source = file_contents(filename, &length); GLuint shader; GLint shader_ok; if (!source) return 0; OpenGL從GLSL源代碼編譯著(zhù)色器對象并保存生成的GPU機器碼。沒(méi)有一個(gè)標準的方式來(lái)將GLSL程序預編譯成一個(gè)二進(jìn)制--你必須每次都從源代碼編譯著(zhù)色器。這里我們在一個(gè)單獨的文件中寫(xiě)著(zhù)色器代碼,這樣每次我們改變著(zhù)色器代碼時(shí)就不用重編譯我們的C代碼。 shader = glCreateShader(type); glShaderSource(shader, 1, (const GLchar**)&source, &length); free(source); glCompileShader(shader); 著(zhù)色器和程序對象脫離了緩沖和紋理所使用的那套glGen和glBind協(xié)議。不像緩沖和紋理函數,操作著(zhù)色器和程序的函數直接使用對象的整數名作為參數。對象不需要綁定到任何目標。這里,我們對過(guò)調用glCreateShader創(chuàng )建一個(gè)著(zhù)色器對象,著(zhù)色器參數可以是GLVERTEXSHADER或者GLFRAGMENTSHADER。然后我們提供一個(gè)源代碼的字符串指針給glShaderSource,并告訴OpenGL去使用glCompileShader編譯著(zhù)色器。這一步跟C的編譯處理過(guò)程很類(lèi)型;編譯的著(zhù)色器對象也是類(lèi)型一個(gè).o或者.obj文件。正如C項目中一樣,任意多的頂點(diǎn)著(zhù)色器和片元著(zhù)色器可以被鏈接到一起形成一個(gè)工作的程序,每個(gè)著(zhù)色器對象引用到其它同類(lèi)型著(zhù)色器對象中定義的函數,只要被引用函數全部可以被解析并且頂點(diǎn)著(zhù)色器和片元著(zhù)色器的main函數都提供了。 glGetShaderiv(shader, GL_COMPILE_STATUS, &shader_ok); if (!shader_ok) { fprintf(stderr, "Failed to compile %s:\n", filename); show_info_log(shader, glGetShaderiv, glGetShaderInfoLog); glDeleteShader(shader); return 0; } return shader; } 同樣正如C程序,一個(gè)著(zhù)色器的代碼塊可能會(huì )由于語(yǔ)法錯誤,引用不存在的函數,或者類(lèi)型不匹配而鏈接失敗。OpenGL對每個(gè)著(zhù)色器對象維護一個(gè)由GLSL編譯器發(fā)出的錯誤或警告信息記錄。在編譯著(zhù)色器之后,我們需要使用glGetShaderiv檢查它的GLCOMPILESTATUS。如果編譯失敗了,我們使用showinfolog函數顯示信息記錄并放棄。下面是showinfolog函數: static void show_info_log( GLuint object, PFNGLGETSHADERIVPROC glGet__iv, PFNGLGETSHADERINFOLOGPROC glGet__InfoLog ) { GLint log_length; char *log; glGet__iv(object, GL_INFO_LOG_LENGTH, &log_length); log = malloc(log_length); glGet__InfoLog(object, log_length, NULL, log); fprintf(stderr, "%s", log); free(log); } 我們將glGetShaderiv和glGetShaderInfoLog函數作為參數傳給showinfolog,這樣我們可以在后面對程序對象重用函數(那些PFNGL*函數指針名是由GLEW提供的)。我們使用GLINFOLOG_LENGTH參數調用glGetShaderiv來(lái)得到信息記錄的長(cháng)度,分配緩沖來(lái)存放它,并使用glGetShaderInfoLog來(lái)得到它的內容。 鏈接程序對象 static GLuint make_program(GLuint vertex_shader, GLuint fragment_shader) { GLint program_ok; GLuint program = glCreateProgram(); glAttachShader(program, vertex_shader); glAttachShader(program, fragment_shader); glLinkProgram(program); 如果著(zhù)色器對象是GLSL編譯過(guò)程的對象文件,那么程序對象在完成時(shí)是可執行的。我們使用glCreateProgram創(chuàng )建一個(gè)程序對象,使用glAttachShader附上著(zhù)色器對象跟它進(jìn)行鏈接,最后使用glLinkProgram調用鏈接過(guò)程。 glGetProgramiv(program, GL_LINK_STATUS, &program_ok); if (!program_ok) { fprintf(stderr, "Failed to link shader program:\n"); show_info_log(program, glGetProgramiv, glGetProgramInfoLog); glDeleteProgram(program); return 0; } return program; } 當然,鏈接也可能會(huì )失敗,由于被引用函數未定義,缺少main函數,片元著(zhù)色器使用了非頂點(diǎn)著(zhù)色器提供的varying輸入,以及其它一些類(lèi)似C程序鏈接失敗的原因。我們檢查程序的GLLINKSTATUS并將它的日志信息使用showinfolog導出,這次使用用于program的glGetProgramiv和glGetProgramInfoLog函數。 現在我們將make_resources用來(lái)編譯和鏈接我們著(zhù)色器的最后一部分填上: static int make_resources(void) { /* make buffers and textures ... */ g_resources.vertex_shader = make_shader( GL_VERTEX_SHADER, "hello-gl.v.glsl" ); if (g_resources.vertex_shader == 0) return 0; g_resources.fragment_shader = make_shader( GL_FRAGMENT_SHADER, "hello-gl.f.glsl" ); if (g_resources.fragment_shader == 0) return 0; g_resources.program = make_program( g_resources.vertex_shader, g_resources.fragment_shader ); if (g_resources.program == 0) return 0; 查找著(zhù)色器變量位置 g_resources.uniforms.fade_factor = glGetUniformLocation(g_resources.program, "fade_factor"); g_resources.uniforms.textures[0] = glGetUniformLocation(g_resources.program, "textures[0]"); g_resources.uniforms.textures[1] = glGetUniformLocation(g_resources.program, "textures[1]"); g_resources.attributes.position = glGetAttribLocation(g_resources.program, "position"); return 1; } GLSL鏈接器將一個(gè)GLint位置賦值到每個(gè)uniform變量和頂點(diǎn)的attribute。uniforms或者attributes的結構體和數組會(huì )被繼續分解,每個(gè)域都會(huì )對它的位置賦值。當我們使用程序進(jìn)行渲染時(shí),將變量賦值到uniform變量以及映射頂點(diǎn)數組的屬性,我們將需要使用這些整數位置。這里,我們使用函數glGetUniformLocation和glGetAttribLocation來(lái)查找這些位置,以字符串形式給它們變量名,結構體域名,或者數組元素名字。我們然后在我們程序的g_resource結構體中記錄這些位置。程序鏈接在一起,并且記錄中有了uniform和attribute位置,我們可以準備好了使用程序進(jìn)行渲染。 下次,渲染 我知道我在吊你胃口,最后部分還沒(méi)完成,還沒(méi)有一個(gè)完整的可以運行的程序。我將在在下次,也就是本章最后一部分,修復它,到時(shí)我會(huì )寫(xiě)代碼讓繪圖管線(xiàn)運作起來(lái)渲染我們的場(chǎng)景。 |