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