图形硬件 (graphics hardware)指的是,使用基于栅格化/射线追踪的硬件架构,将 3D 物体快速渲染到计算机屏幕上所需的硬件组件,也就是显卡上的一组芯片、晶体管、总线、处理器、计算核。图形硬件上的处理器,称为图形处理单元 (Graphics Processing Units, 简称 GPU),是高度并行的,而且提供了数千个并发执行线程。图形硬件为吞吐量 (throughput)而设计,以允许在更短的时间里处理更多的像素和顶点。
除了图形算法之外,GPU 还用于加速物理计算、发展实时射线追踪、求解模拟流体的 Navier-Stokes 方程、大气模拟等。有些 API 和 SDK 可以提供更直接的通用计算,如 OpenCL、CUDA。现有的硬件加速射线追踪 API 可以加速计算射线物体的交点。图形编程的标准 API,如 OpenGL 和 DirectX,也提供了利用图形硬件并行能力的机制。随着硬件的发展,上述许多 API 都会随之变化。
图形硬件是可编程的。现今的图形硬件以及相应的 API 支持完全可编程的流水线。开发者可以控制与几何、顶点以及片段处理相关的许多计算。此前,功能固定的栅格化流水线只能计算特定类型的顶点变换、光照和片段处理,以保证快速地执行基础着色、光照和纹理。无论如何,栅格化流水线的基础计算是相似的。通常,流水线有助于并行执行,GPU 核可同时处理顶点和片段。
异构多处理器(Heterogeneous Multiprocessing)
在讨论图形硬件时,一般将 CPU 以及它的线程和内存称为 host (主机),GPU 以及它的线程和内存称为 device (设备)。这种定义源于,大多图形硬件都是外部硬件,虽然有时也会是一个作为协处理器的独立芯片组。
所有使用图形硬件的程序都必须先建立一个 CPU 到 GPU 的内存映射,以保证操作系统中的图形硬件驱动可以将硬件、操作系统和窗口系统(windowing system)连接起来。操作系统、硬件驱动、硬件、窗口系统之间的这种映射称为图形上下文 (graphics context),它通常通过对窗口系统调用 API 来建立。建立上下文依赖于窗口系统,因而无法跨平台,但是存在更上层的 API 来开发可移植的交互应用。许多交互应用开发框架都支持查询输入设备,如键盘、鼠标。有些框架还可以访问网络、音频系统等系统资源。这些 API 是开发图形应用的首选。
跨平台硬件加速通常使用 OpenGL API 来实现。OpenGL 是一个开放的行业标准图形 API,它支持在许多图形硬件上进行硬件加速。OpenGL 最常用的图形硬件编程 API 之一。OpenGL 在许多操作系统和硬件架构上都可用,而 DirectX 专用于 Microsoft。因此,本章以 OpenGL 为例对硬件编程中的概念进行说明。
OpenGL 是一种 C 语言风格的 API,所有函数以 “gl” 为前缀。OpenGL 函数调用可以改变图形硬件状态、声明和定义几何、加载顶点和片段着色器、并决定数据传给硬件后应如何计算。一些老旧的 API 已经废弃了,比如:立即渲染模式 (immediate mode rendering)、矩阵栈等。立即渲染模式在每帧需要时将数据从 CPU 内存送入显卡内存,这种做法非常低效。现今的 API 侧重于提前将数据存储到显卡上,渲染时再实例化 (instancing)。正因如此,GLSL (OpenGL’s shader language)至关重要,在着色器中执行必要的矩阵变换、光照和着色。
图形硬件编程:缓冲(Buffers)、状态(States)、着色器(Shaders)
现代图形硬件编程涉及三个概念:缓冲 (buffer)、状态 (state)、着色器 (shader)。数据缓冲是设备上的一块线性区域,可以存储 GPU 将要操作各种数据。计算状态由显卡维护,它决定了如何在图形硬件上进行与场景数据、着色器有关的计算。状态可以从 host 传递到 device,甚至可以在着色器间传递。着色器是一种在 GPU 上逐顶点、逐像素处理相关计算的机制,它在现代图形硬件中扮演着重要角色。
除了顶点着色器(vertex shader)、片段着色器(fragment shader)之外,还有几何着色器(geometry shader)、计算着色器(compute shader)。
缓冲
缓冲是图形硬件上与几何、纹理等一切数据相关的内存。与硬件加速栅格化相关的计算会读写 GPU 上的各种数据缓冲,也就是需要在 host(CPU)和 device(GPU)间进行数据拷贝。
图形流水线最终生成的像素数据——显示缓冲 (display buffer)——可以连接到显示器上,也可以 PNG 格式写到硬盘上。所生成数据通常是二维颜色值阵列,但会以一维数组的形式存储。渲染图像需要通过图形 API 将显示缓冲的变化传递到图形硬件上。在栅格化流水线的最后,片段处理和混合阶段将数据写入显示缓冲存储器;同时,窗口系统读取显示缓冲并在显示器窗口上生成栅格图。
大多数应用更偏好于双缓冲显示 (double-buffered display),即,与图形窗口关联的有两个缓冲:前置缓冲 (front buffer)、后置缓冲 (back buffer)。应用可以将变化传递到后置缓冲,而前置缓冲用于驱动窗口上的像素颜色。
每次渲染循环结束时,就通过指针来交换两个缓冲。窗口系统会把最新的缓冲刷到窗口上。如果缓冲指针的交换和窗口系统对整个显示器的刷新不同步,可能出现几何体撕裂,因为场景几何、片段处理、输出显示缓冲的速度快于屏幕刷新。
如果把显示器看作是一个缓冲,那么最简单的操作就是清零 (zeros-out,也称为重置默认值)。对于图形程序,这可能意味着将窗口背景清除为特定颜色:
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); \begin{aligned}
&\text{glClearColor(0.0f, 0.0f, 0.0f, 1.0f);} \\
&\text{glClear(GL\_COLOR\_BUFFER\_BIT);}
\end{aligned} glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT);
除了颜色缓冲之外,可能还需要清除深度缓冲:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); \text{glClear(GL\_COLOR\_BUFFER\_BIT | GL\_DEPTH\_BUFFER\_BIT);} glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
在基本的交互式图形应用中,上述清除操作通常是处理几何、片段前执行的第一步操作。
状态
底色 (clear color)是一种图形硬件状态。glClearColor \text{glClearColor} glClearColor 函数用于设置该状态,调用 glClear \text{glClear} glClear 函数会把先前设置的底色写入颜色缓冲。同样地,z-缓冲算法状态也可以通过 glEnable \text{glEnable} glEnable 、glDisable \text{glDisable} glDisable 函数来开启、关闭。z-缓冲算法在 OpenGL 中也称为深度测试 (depth test)。glDepthFunc \text{glDepthFunc} glDepthFunc 函数用于指定深度测试的机制。
深度测试有时是不必要的,而且也可能会降低应用的性能。
glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); \begin{aligned}
&\text{glEnable(GL\_DEPTH\_TEST);} \\
&\text{glDepthFunc(GL\_LESS);}
\end{aligned} glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS);
OpenGL 中的状态类似面向对象类的静态变量。开发者可以按需开启、关闭显卡上的变量状态。通常,高效的程序尽可能减少状态变化,按需地开启状态,禁用那些无关渲染的状态。
着色器
现代 OpenGL 要求使用着色器处理顶点和片段。OpenGL 和 GLSL 中也支持高级着色器类型:几何着色器 (geometry shaders)、计算着色器 (compute shaders)。几何着色器用于处理图元,也可能会创建额外的图元,可以支持几何实例化操作。计算着色器可以在 GPU 上执行通用计算,可以连接到特定应用所需的着色器集上。
顶点着色器
顶点着色器可以控制顶点变换,通常用来为片段着色器准备数据。此外,顶点着色器还可用于在 GPU 上执行通用计算,比如模拟粒子运动。虽然这种方式在有些场景中有用,但高级通用计算更适合用计算着色器。下面的顶点着色器直接将数据透传(passthrough):
# version 330 core layout(location=0) in vec3 in_Position; void main(void) { gl_Position = vec4(in_Position, 1.0); } \begin{aligned}
&\#\text{version 330 core} \\
&\\
&\text{layout(location=0) in vec3 in\_Position;} \\
&\\
&\text{void main(void) \{} \\
&\quad\text{gl\_Position = vec4(in\_Position, 1.0);} \\
&\}
\end{aligned} # version 330 core layout(location=0) in vec3 in_Position; void main(void) { gl_Position = vec4(in_Position, 1.0); }
gl_Position \text{gl\_Position} gl_Position 是 OpenGL 内置的、用于栅格化的输出变量。顶点、片段着色器都是 SIMD (Single Instruction Multiple Data,单指令多数据)操作,它们分别处理流水线中所有的顶点和片段。关键字 in \text{in} in 表示数据是输入给着色器的。location \text{location} location 用于指定该顶点属性在所有属性中的索引,或是该数据在所有片段输出数据中的索引。
GLSL 中包含许多数据类型:vec2 \text{vec2} vec2 、vec3 \text{vec3} vec3 、vec4 \text{vec4} vec4 、mat2 \text{mat2} mat2 、mat3 \text{mat3} mat3 、mat4 \text{mat4} mat4 、float \text{float} float 、int \text{int} int 等。其中,vec4 \text{vec4} vec4 有四个分量,对应齐次坐标的 x x x 、y y y 、z z z 、w w w ,或者是颜色的 r r r 、g g g 、b b b 、a a a ;而且这些分量是重载的,且允许按需重排 (swizzling),如:in_Position.zyxw \text{in\_Position.zyxw} in_Position.zyxw 。许多内置类型都有一个可用于类型转换的构造函数。
片段着色器
# version 330 core layout(location=0) out vec4 out_FragmentColor; void main(void) { out_FragmentColor = vec4(0.49, 0.87, 0.59, 1.0); } \begin{aligned}
&\#\text{version 330 core} \\
&\\
&\text{layout(location=0) out vec4 out\_FragmentColor;} \\
&\\
&\text{void main(void) \{} \\
&\quad\text{out\_FragmentColor = vec4(0.49, 0.87, 0.59, 1.0);} \\
&\}
\end{aligned} # version 330 core layout(location=0) out vec4 out_FragmentColor; void main(void) { out_FragmentColor = vec4(0.49, 0.87, 0.59, 1.0); }
关键字 in \text{in} in 、out \text{out} out 表明数据流入、流出着色器。上面的 layout(location=0) \text{layout(location=0)} layout(location=0) 表示输出位置是颜色缓冲索引 0 0 0 。片段着色器也可以输出到多个缓冲。关键字 layout \text{layout} layout 、location \text{location} location 的使用将顶点着色器中应用程序几何数据和片段着色器中输出颜色缓冲显式地连接起来。
着色器加载、编译和使用
着色器以字符串形式传给图形硬件,然后可以编译并连接(link)。着色器结合成一个着色程序 (shader program)后可以一致地处理顶点和片段。每个着色器都得调用下面这些函数来创建:
glCreateShader \text{glCreateShader} glCreateShader ,为硬件上的着色器创建一个句柄(handle);
glShaderSource \text{glShaderSource} glShaderSource ,将字符串加载到图形硬件内存中;
glCompileShader \text{glCompileShader} glCompileShader ,在硬件上编译着色器。
着色器加载并编译之后,可以连接到一个着色程序上:
glCreateProgram \text{glCreateProgram} glCreateProgram ,创建一个程序对象;
glAttachShader \text{glAttachShader} glAttachShader ,将着色器附加到程序对象上;
glLinkProgram \text{glLinkProgram} glLinkProgram ,在所有着色器附加到程序对象之后,将着色器内部连接起来;
glUseProgram \text{glUseProgram} glUseProgram ,绑定着色程序以便在硬件上使用。
OpenGL 应用的基本设计
一个简单而基础的 OpenGL 应用会有一个显示循环 (display loop)作为其核心,要么尽可能快地调用,要么和显示设备刷新率保持一致地刷新。下面是一个基于 GLFW 库的例子:
while (!glfwWindowShouldClose(window)) { // OpenGL code is called here, each time this loop is executed. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Swap front and back buffers glfwSwapBuffers(window); // Poll for events glfwPollEvents(); if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); } \begin{aligned}
&\text{\textbf{while} (!glfwWindowShouldClose(window)) \{} \\
&\quad\begin{aligned}
&\text{// OpenGL code is called here, each time this loop is executed.} \\
&\text{glClear(GL\_COLOR\_BUFFER\_BIT | GL\_DEPTH\_BUFFER\_BIT);} \\
&\\
&\text{// Swap front and back buffers} \\
&\text{glfwSwapBuffers(window);} \\
&\\
&\text{// Poll for events} \\
&\text{glfwPollEvents();} \\
&\text{\textbf{if} (glfwGetKey(window, GLFW\_KEY\_ESCAPE) == GLFW\_PRESS)} \\
&\quad\text{glfwSetWindowShouldClose(window, 1);}
\end{aligned} \\
&\}
\end{aligned} while (!glfwWindowShouldClose(window)) { // OpenGL code is called here, each time this loop is executed. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Swap front and back buffers glfwSwapBuffers(window); // Poll for events glfwPollEvents(); if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); }
深度缓冲和颜色缓冲统称为帧缓冲 (framebuffer)。帧缓冲与相应图形上下文的窗口大小直接相关。OpenGL 需要窗口(或视口)尺寸构造 M v p M_{vp} M v p 矩阵。glViewport \text{glViewport} glViewport 函数可以设置窗口尺寸状态。
int nx, ny; glfwGetFramebufferSize(window, &nx, &ny); glViewport(0, 0, nx, ny); \begin{aligned}
&\text{int nx, ny;} \\
&\text{glfwGetFramebufferSize(window, \&nx, \&ny);} \\
&\text{glViewport(0, 0, nx, ny);}
\end{aligned} int nx, ny; glfwGetFramebufferSize(window, &nx, &ny); glViewport(0, 0, nx, ny);
技术上,OpenGL 在像素展示在用户显示器之前,将几何栅格化、片段处理的结果写入帧缓冲。
物体几何
类似显示缓冲,物体几何也会用数组存储顶点数据及其属性,并使用缓冲的概念在图形硬件上分配存储空间,将数据从 host 传到 device。图形硬件编程的挑战之一就是 3D 数据管理和主设间数据传输。
大多图形硬件仅支持某几类几何图元 (geometric primitive)。不同类型的图元会利用其复杂性提升其处理速度。图元越简单,处理起来越快。图元类型应该足够通用,以表示各种几何体。一般情况下,图元类型被限制为如下的一种或多种:
点 (points),每个点对应一个顶点,可用于表示粒子系统;
线 (lines),用于表示线条、轮廓或边缘高亮的顶点对;
三角形 (triangles),用于近似几何曲面的三角形、三角带、索引三角形、索引三角带、四边形、三角网格。
顶点缓冲对象(Vertex Buffer Objects)
图形硬件上存储顶点数据及其属性 (vertex attributes,如:颜色、法向量、纹理坐标等)的缓冲,称为顶点缓冲对象 (vertex buffer objects,简称为 VBOs)。以三角形为例,首先在应用程序的 host 中为图元顶点分配一块内存:
GLfloat vertices[ ] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f } ; \begin{aligned}
&\text{GLfloat vertices[ ] = \{} \\
&\quad\begin{aligned}
&\text{-0.5f, -0.5f, 0.0f,} \\
&\text{ 0.5f, -0.5f, 0.0f,} \\
&\text{ 0.0f, 0.5f, 0.0f}
\end{aligned} \\
&\};
\end{aligned} GLfloat vertices[ ] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f } ;
GLfloat \text{GLfloat} GLfloat 是 OpenGL 中的数据类型。处理顶点前,首先要在 device 上创建顶点缓冲以存储数据,然后将 host 中的顶点数据传给 device。VBO 是现代 OpenGL 在图形硬件内存上存储顶点数据的主要机制。为保证高效,一般在显示循环之前初始化 VBO 并传输顶点数据。
GLuint triangleVBO[1]; glGenBuffers(1, triangleVBO); glBindBuffer(GL_ARRAY_BUFFER, triangleVBO[0]); glBufferData(GL_ARRAY_BUFFER, 9 ∗ sizeof(GLfloat), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); \begin{aligned}
&\text{GLuint triangleVBO[1];} \\
&\text{glGenBuffers(1, triangleVBO);} \\
&\text{glBindBuffer(GL\_ARRAY\_BUFFER, triangleVBO[0]);} \\
&\text{glBufferData(GL\_ARRAY\_BUFFER, 9 $\ast$ sizeof(GLfloat), vertices, GL\_STATIC\_DRAW);} \\
&\text{glBindBuffer(GL\_ARRAY\_BUFFER, 0);}
\end{aligned} GLuint triangleVBO[1]; glGenBuffers(1, triangleVBO); glBindBuffer(GL_ARRAY_BUFFER, triangleVBO[0]); glBufferData(GL_ARRAY_BUFFER, 9 ∗ sizeof(GLfloat), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0);
glGenBuffers \text{glGenBuffers} glGenBuffers 创建了一个用来引用 VBO 的句柄,当然,也可以一次性创建多个 VBO 句柄。生成缓冲对象时,device 上并没有真正地分配空间。OpenGL 中计算和处理的主要目标就是像顶点缓冲对象、帧缓冲对象、纹理对象、着色程序这样的对象 (objects)。对象在使用时必须绑定到某个已知的 OpenGL 状态上,并在不使用时解绑。glBindBuffer \text{glBindBuffer} glBindBuffer 可以将 GL_ARRAY_BUFFER \text{GL\_ARRAY\_BUFFER} GL_ARRAY_BUFFER 状态绑定到 VBO 句柄上,以激活相应的 VBO。glBufferData \text{glBufferData} glBufferData 将顶点数据从 host 复制到 device,它的参数分别为:目标缓冲类型、将要复制的缓冲大小、host 缓冲指针、表示缓冲用途的枚举类型。GL_STATIC_DRAW \text{GL\_STATIC\_DRAW} GL_STATIC_DRAW 表示渲染过程中顶点不会变化。当不再需要读写 VBO 时,应将其解绑。通常,将 OpenGL 状态绑定到句柄 0 0 0 ,就是解绑相应的缓冲或对象。
顶点数组对象(Vertex Array Objects)
顶点缓冲对象是顶点及其属性的存储容器。而顶点数组对象 (vertex array objects,简称 VAOs)则是 OpenGL 将多个顶点缓冲包成一个顶点状态的机制,该状态可以在图形硬件上通信并连接到着色器上。和 VBO 一样,VAO 也要先创建并绑定状态才能使用:
GLuint VAO; glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, triangleVBO[0]); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 ∗ sizeof(GLfloat), 0); glBindVertexArray(0); \begin{aligned}
&\text{GLuint VAO;} \\
&\text{glGenVertexArrays(1, \&VAO);} \\
&\text{glBindVertexArray(VAO);} \\
&\text{glEnableVertexAttribArray(0);} \\
&\text{glBindBuffer(GL\_ARRAY\_BUFFER, triangleVBO[0]);} \\
&\text{glVertexAttribPointer(0, 3, GL\_FLOAT, GL\_FALSE, 3 $\ast$ sizeof(GLfloat), 0);} \\
&\text{glBindVertexArray(0);}
\end{aligned} GLuint VAO; glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, triangleVBO[0]); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 ∗ sizeof(GLfloat), 0); glBindVertexArray(0);
定义 VAO 时,可以使用 VBO 来指定顶点属性。glEnableVertexAttribArray \text{glEnableVertexAttribArray} glEnableVertexAttribArray 激活了相应索引的顶点属性,着色器中可通过 layout \text{layout} layout 、location \text{location} location 来获取该属性。glVertexAttribPointer \text{glVertexAttribPointer} glVertexAttribPointer 的参数分别为:属性索引、所包含的分量数目、每个分量的数据类型、是否需要归一化、步长(stride,相邻两顶点间距)、数据指针(绑定 VBO 时,表示相对于它的偏移量)。可以看出,顶点一个接一个地在内存中密排。在 VAO 将数据和着色器输入变量索引连接起来之后,便可以在显示循环中绘制:
glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); \begin{aligned}
&\text{glBindVertexArray(VAO);} \\
&\text{glDrawArrays(GL\_TRIANGLES, 0, 3);} \\
&\text{glBindVertexArray(0);}
\end{aligned} glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0);
glDrawArrays \text{glDrawArrays} glDrawArrays 函数为几何体启动流水线,三个参数分别为:绘制图元类型、起始顶点、绘制顶点数目。显示循环的主体代码如下:
// Set the viewport once int nx, ny; glfwGetFramebufferSize(window, &nx, &ny); glViewport(0, 0, nx, ny); // Set clear color state glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Create the Shader programs, VBO, VAO GLuint shaderID = loadPassthroughShader(); GLuint VAO = loadVertexData(); while (!glfwWindowShouldClose(window)) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(shaderID); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); glUseProgram(0); // Swap front and back buffers glfwSwapBuffers(window); // Poll for events glfwPollEvents(); if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); } \begin{aligned}
&\text{// Set the viewport once} \\
&\text{int nx, ny;} \\
&\text{glfwGetFramebufferSize(window, \&nx, \&ny);} \\
&\text{glViewport(0, 0, nx, ny);} \\
&\\
&\text{// Set clear color state} \\
&\text{glClearColor(0.0f, 0.0f, 0.0f, 1.0f);} \\
&\\
&\text{// Create the Shader programs, VBO, VAO} \\
&\text{GLuint shaderID = loadPassthroughShader();} \\
&\text{GLuint VAO = loadVertexData();} \\
&\\
&\text{\textbf{while} (!glfwWindowShouldClose(window)) \{} \\
&\quad\begin{aligned}
&\text{glClear(GL\_COLOR\_BUFFER\_BIT | GL\_DEPTH\_BUFFER\_BIT);} \\
&\\
&\text{glUseProgram(shaderID);} \\
&\\
&\text{glBindVertexArray(VAO);} \\
&\text{glDrawArrays(GL\_TRIANGLES, 0, 3);} \\
&\text{glBindVertexArray(0);} \\
&\\
&\text{glUseProgram(0);} \\
&\\
&\text{// Swap front and back buffers} \\
&\text{glfwSwapBuffers(window);} \\
&\\
&\text{// Poll for events} \\
&\text{glfwPollEvents();} \\
&\text{\textbf{if} (glfwGetKey(window, GLFW\_KEY\_ESCAPE) == GLFW\_PRESS)} \\
&\quad\text{glfwSetWindowShouldClose(window, 1);}
\end{aligned} \\
&\}
\end{aligned} // Set the viewport once int nx, ny; glfwGetFramebufferSize(window, &nx, &ny); glViewport(0, 0, nx, ny); // Set clear color state glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Create the Shader programs, VBO, VAO GLuint shaderID = loadPassthroughShader(); GLuint VAO = loadVertexData(); while (!glfwWindowShouldClose(window)) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(shaderID); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); glUseProgram(0); // Swap front and back buffers glfwSwapBuffers(window); // Poll for events glfwPollEvents(); if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); }
交错顶点属性
顶点属性可以和顶点坐标一起交错存储 (interleaving)到顶点缓冲上。对于带有颜色的顶点:
GLfloat vertexData[ ] = { 0.0f, 3.0f, 0.0f, 1.0f, 1.0f, 0.0f, -3.0f, -3.0f, 0.0f, 0.0f, 1.0f, 1.0f, 3.0f, -3.0f, 0.0f, 1.0f, 0.0f, 1.0f } \begin{aligned}
&\text{GLfloat vertexData[ ] = \{} \\
&\quad\begin{aligned}
&\text{ 0.0f, 3.0f, 0.0f, 1.0f, 1.0f, 0.0f,} \\
&\text{-3.0f, -3.0f, 0.0f, 0.0f, 1.0f, 1.0f,} \\
&\text{ 3.0f, -3.0f, 0.0f, 1.0f, 0.0f, 1.0f}
\end{aligned} \\
&\}
\end{aligned} GLfloat vertexData[ ] = { 0.0f, 3.0f, 0.0f, 1.0f, 1.0f, 0.0f, -3.0f, -3.0f, 0.0f, 0.0f, 1.0f, 1.0f, 3.0f, -3.0f, 0.0f, 1.0f, 0.0f, 1.0f }
VAO 的构造如下:
glBindBuffer(GL_ARRAY_BUFFER, m_triangleVBO[0]); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 ∗ sizeof(GLfloat), 0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 ∗ sizeof(GLfloat), (const GLvoid ∗ )12); \begin{aligned}
&\text{glBindBuffer(GL\_ARRAY\_BUFFER, m\_triangleVBO[0]);} \\
&\\
&\text{glEnableVertexAttribArray(0);} \\
&\text{glVertexAttribPointer(0, 3, GL\_FLOAT, GL\_FALSE, 6 $\ast$ sizeof(GLfloat), 0);} \\
&\\
&\text{glEnableVertexAttribArray(1);} \\
&\text{glVertexAttribPointer(1, 3, GL\_FLOAT, GL\_FALSE, 6 $\ast$ sizeof(GLfloat), (const GLvoid $\ast$)12);}
\end{aligned} glBindBuffer(GL_ARRAY_BUFFER, m_triangleVBO[0]); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 ∗ sizeof(GLfloat), 0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 ∗ sizeof(GLfloat), (const GLvoid ∗ )12);
顶点着色器如下:
#version 330 core layout(location=0) in vec3 in_Position; layout(location=1) in vec3 in_Color; out vec3 vColor; uniform mat4 projMatrix; void main(void) { vColor = in_Color; gl_Position = projMatrix ∗ vec4(in_Position, 1.0); } \begin{aligned}
&\text{\#version 330 core} \\
&\\
&\text{layout(location=0) in vec3 in\_Position;} \\
&\text{layout(location=1) in vec3 in\_Color;} \\
&\\
&\text{out vec3 vColor;} \\
&\\
&\text{uniform mat4 projMatrix;} \\
&\\
&\text{void main(void) \{} \\
&\quad\text{vColor = in\_Color;} \\
&\quad\text{gl\_Position = projMatrix $\ast$ vec4(in\_Position, 1.0);} \\
&\}
\end{aligned} #version 330 core layout(location=0) in vec3 in_Position; layout(location=1) in vec3 in_Color; out vec3 vColor; uniform mat4 projMatrix; void main(void) { vColor = in_Color; gl_Position = projMatrix ∗ vec4(in_Position, 1.0); }
关键字 in \text{in} in 、out \text{out} out 表明数据流入、流出着色器。流出顶点着色器的数据会流入到片段着色器中,两者通过变量名连接起来。out \text{out} out 变量会使用重心坐标对片段插值。片段着色器如下:
#version 330 core layout(location=0) out vec4 fragmentColor; in vec3 vColor; void main(void) { fragmentColor = vec4(vColor, 1.0); } \begin{aligned}
&\text{\#version 330 core} \\
&\\
&\text{layout(location=0) out vec4 fragmentColor;} \\
&\\
&\text{in vec3 vColor;} \\
&\\
&\text{void main(void) \{} \\
&\quad\text{fragmentColor = vec4(vColor, 1.0);} \\
&\}
\end{aligned} #version 330 core layout(location=0) out vec4 fragmentColor; in vec3 vColor; void main(void) { fragmentColor = vec4(vColor, 1.0); }
顶点数据结构体
由于顶点属性和顶点坐标在内存中紧密排列,着色器中处理交错数据时可以充分利用内存局部性。当模型较大时,交错数组变得复杂,而将顶点数据存储为结构体 向量则相当简单。上面带有颜色的顶点可用如下结构体表示:
struct vertexData { glm::vec3 pos; glm::vec3 color; } ; std::vector<vertexData> modelData; \begin{aligned}
&\text{struct vertexData \{} \\
&\quad\text{glm::vec3 pos;} \\
&\quad\text{glm::vec3 color;} \\
&\}; \\
&\text{std::vector<vertexData> modelData;}
\end{aligned} struct vertexData { glm::vec3 pos; glm::vec3 color; } ; std::vector<vertexData> modelData;
将持有顶点的 STL 向量加载到顶点缓冲对象:
int numBytes = modelData.size() ∗ sizeof(vertexData); glBufferData(GL_ARRAY_BUFFER, numBytes, modelData.data(), GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); \begin{aligned}
&\text{int numBytes = modelData.size() $\ast$ sizeof(vertexData);} \\
&\text{glBufferData(GL\_ARRAY\_BUFFER, numBytes, modelData.data(), GL\_STATIC\_DRAW);} \\
&\text{glBindBuffer(GL\_ARRAY\_BUFFER, 0);}
\end{aligned} int numBytes = modelData.size() ∗ sizeof(vertexData); glBufferData(GL_ARRAY_BUFFER, numBytes, modelData.data(), GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0);
STL 向量连续地存储数据,上面的 vertexData \text{vertexData} vertexData 结构体扁平地、连续地排布在内存中。
OBJ 格式是一种广泛使用的、简单的 3D 模型文件格式。结构体机制可以很好地处理 host 上的 OBJ 格式数据,然后传给 VBO、VAO。
变换矩阵
由于矩阵栈已经从 OpenGL 移除,因此开发者必须自己编写矩阵并传给顶点着色器,这可以借助 GLM(OpenGL Mathematics)库来实现。GLM 提供了一些对计算机图形学有用的基础数学类型。比如:
glm::vec3 \text{glm::vec3} glm::vec3 ,由 3 个浮点数构成的紧凑数组,可以着色器中相同的方式访问分量;
glm::vec4 \text{glm::vec4} glm::vec4 ,由 4 个浮点数构成的紧凑数组,可以着色器中相同的方式访问分量;
glm::mat4 \text{glm::mat4} glm::mat4 ,由 16 个浮点数构成的列主序 (column-major)存储的 4 × 4 矩阵。
还有一些创建矩阵的方法:
glm::ortho \text{glm::ortho} glm::ortho ,创建一个 4 × 4 正交投影矩阵;
glm::perspective \text{glm::perspective} glm::perspective ,创建一个 4 × 4 透视矩阵;
glm:lookAt \text{glm:lookAt} glm:lookAt ,创建一个对相机平移、旋转的 4 × 4 齐次变换。
使用 GLM 可以在 host 上创建一个正交投影:
glm::mat4 projMatrix = glm::ortho(-5.0f, 5.0f, -5.0f, 5.0f, -10.0f, 10.0f); \text{glm::mat4 projMatrix = glm::ortho(-5.0f, 5.0f, -5.0f, 5.0f, -10.0f, 10.0f);} glm::mat4 projMatrix = glm::ortho(-5.0f, 5.0f, -5.0f, 5.0f, -10.0f, 10.0f);
该矩阵可以通过 uniform 变量从 host 传递到 device。uniform 变量是着色程序执行过程中保持不变的静态数据,而且该数据对于所有元素都相同。uniform 数据通常表示应用中的图形状态,比如:投影矩阵、模型矩阵、光源信息等。
#version 330 core layout(location=0) in vec3 in_Position; uniform mat4 projMatrix; void main(void) { gl_Position = projMatrix ∗ vec4(in_Position, 1.0); } \begin{aligned}
&\text{\#version 330 core} \\
&\\
&\text{layout(location=0) in vec3 in\_Position;} \\
&\\
&\text{uniform mat4 projMatrix;} \\
&\\
&\text{void main(void) \{} \\
&\quad\text{gl\_Position = projMatrix $\ast$ vec4(in\_Position, 1.0);} \\
&\}
\end{aligned} #version 330 core layout(location=0) in vec3 in_Position; uniform mat4 projMatrix; void main(void) { gl_Position = projMatrix ∗ vec4(in_Position, 1.0); }
为了将 uniform 变量从 host 内存传到 device 的着色程序上,还需要在连接着色程序后,在应用的 host 侧获取 uniform 变量的句柄。
GLint pMatID = glGetUniformLocation(shaderProgram, "projMatrix"); \text{GLint pMatID = glGetUniformLocation(shaderProgram, "projMatrix");} GLint pMatID = glGetUniformLocation(shaderProgram, "projMatrix");
glGetUniformLocation \text{glGetUniformLocation} glGetUniformLocation 的参数分别为:着色程序对象句柄、着色器中的变量名字符串。设置 uniform 变量值之前需要先绑定着色程序:
glUseProgram(shaderID); glUniformMatrix4fv(pMatID, 1, GL_FALSE, glm::value_ptr(projMatrix)); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); glUseProgram(0); \begin{aligned}
&\text{glUseProgram(shaderID);} \\
&\\
&\text{glUniformMatrix4fv(pMatID, 1, GL\_FALSE, glm::value\_ptr(projMatrix));} \\
&\\
&\text{glBindVertexArray(VAO);} \\
&\text{glDrawArrays(GL\_TRIANGLES, 0, 3);} \\
&\text{glBindVertexArray(0);} \\
&\\
&\text{glUseProgram(0);}
\end{aligned} glUseProgram(shaderID); glUniformMatrix4fv(pMatID, 1, GL_FALSE, glm::value_ptr(projMatrix)); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); glUseProgram(0);
以 glUniform \text{glUniform} glUniform 开头的函数名后缀表明函数用途,Matrix4 \text{Matrix4} Matrix4 表示 4 × 4 矩阵,f \text{f} f 表示浮点数,v \text{v} v 表示向量,即数据以数组形式传入,而不是依次传入每个值。glUniformMatrix4fv \text{glUniformMatrix4fv} glUniformMatrix4fv 的参数分别为:uniform 变量句柄、矩阵个数、是否转置、矩阵指针。
实例化(Instancing)
许多 3D 模型都是定义在它们的局部坐标系中,因此需要各种变换将它们和 OpenGL 坐标系对齐。GLM 提供了一些函数用于生成局部模型变换:
glm::translate \text{glm::translate} glm::translate ,创建一个平移矩阵。
glm::rotate \text{glm::rotate} glm::rotate ,创建一个绕指定轴转指定角的旋转矩阵。
glm::scale \text{glm::scale} glm::scale ,创建一个伸缩矩阵。
与射线追踪不同的是,OpenGL 实例化是以 VAO 和 VBO 的形式加载单个副本,然后根据需要复用几何体。最终结果与射线追踪一样,内存中只加载了一份几何数据。几何变换和材质类型可以使用 uniform 变量从 host 传到 device 上。只要通用着色器嵌入这些变换,就可以使用相同的局部几何渲染出各种 3D 模型。
片段着色
利用现代图形硬件,在片段处理器中应用着色算法可以产生更好的视觉效果,更加精确地近似光照。逐顶点着色无法精细地分辨三角形内部。尽管提高几何体细分程度可以改善视觉效果,但是这一方法在实时图形学中应用有限,因为更精确的光照需要添加的几何数据让渲染变得很慢。
片段着色器操作的是经顶点变换、裁剪、栅格化后的片段。通常来讲,片段着色器必须向帧缓冲输出一个值:像素颜色、深度值。片段着色器中用于计算的数据来源于:
OpenGL 内置变量 。比如:gl_FragCoord \text{gl\_FragCoord} gl_FragCoord ,这些变量依赖于 OpenGL 和 GLSL 的版本。
uniform 变量 。uniform 变量从 host 传输到 device,可以根据需要来更改。这些变量由开发者声明和定义,以便在顶点和片段着色器中使用。
输入变量 。顶点着色器中可以使用关键字 out \text{out} out 把数据输出到下一着色阶段。当下一阶段使用关键字 in \text{in} in 和同类型、同名修饰符时,两个阶段的输出和输入可以连接起来。
任何通过 in-out \text{in-out} in-out 机制传入片段着色器的数据都会被重心插值。这一插值是在着色器之外由图形硬件计算的。下面以单个点光源 Blinn-Phong 着色程序来说明如何用片段着色器着色,着色方程如下:
L = k a I a + k d I m a x ( 0 , n ⃗ ⋅ l ⃗ ) + k s I m a x ( 0 , n ⃗ ⋅ h ⃗ ) p L = k_{a}I_{a} + k_{d}I\mathrm{max}(0, \vec{n}\cdot\vec{l}) + k_{s}I\mathrm{max}(0, \vec{n}\cdot\vec{h})^{p} L = k a I a + k d I max ( 0 , n ⋅ l ) + k s I max ( 0 , n ⋅ h ) p
几何变换、光源位置和强度、几何模型的材质性质 k a k_{a} k a 、k d k_{d} k d 、k s k_{s} k s 、I a I_{a} I a 、p p p 可使用 uniform 变量指定,然后再传给顶点和片段着色器。host 上加载数据的代码如下:
glUseProgram(BlinnPhongShaderID); // Describe the Local Transform Matrix glm::mat4 modelMatrix = glm::mat4(1.0); // Identity Matrix modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0f, 1.0f, 0.0f)); float rot = (-90.0f / 180.0f) ∗ M_PI; modelMatrix = glm::rotate(modelMatrix, rot, glm::vec3(1, 0, 0)); // Set the Normal Matrix glm::mat4 normalMatrix = glm::transpose(glm::inverse(viewMatrix ∗ modelMatrix)); // Pass the matrices to the GPU memory glUniformMatrix4fv(nMatID, 1, GL_FALSE, glm::value_ptr(normalMatrix)); glUniformMatrix4fv(pMatID, 1, GL_FALSE, glm::value_ptr(projMatrix)); glUniformMatrix4fv(vMatID, 1, GL_FALSE, glm::value_ptr(viewMatrix)); glUniformMatrix4fv(mMatID, 1, GL_FALSE, glm::value_ptr(modelMatrix)); // Set material for this object glm::vec3 kd(0.2, 0.2, 1.0); glm::vec3 ka = kd ∗ 0.15f; glm::vec3 ks(1.0, 1.0, 1.0); float phongExp = 32.0; glUniform3fv(kaID, 1, glm::value_ptr(ka)); glUniform3fv(kdID, 1, glm::value_ptr(kd)); glUniform3fv(ksID, 1, glm::value_ptr(ks)); glUniform1f(phongExpID, phongExp); // Process the object and note that modelData.size() holds // the number of vertices, not the number of triangles! glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, modelData.size()); glBindVertexArray(0); glUseProgram(0); \begin{aligned}
&\text{glUseProgram(BlinnPhongShaderID);} \\
&\\
&\text{// Describe the Local Transform Matrix} \\
&\text{glm::mat4 modelMatrix = glm::mat4(1.0); // Identity Matrix} \\
&\text{modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0f, 1.0f, 0.0f));} \\
&\text{float rot = (-90.0f / 180.0f) $\ast$ M\_PI;} \\
&\text{modelMatrix = glm::rotate(modelMatrix, rot, glm::vec3(1, 0, 0));} \\
&\\
&\text{// Set the Normal Matrix} \\
&\text{glm::mat4 normalMatrix = glm::transpose(glm::inverse(viewMatrix $\ast$ modelMatrix));} \\
&\\
&\text{// Pass the matrices to the GPU memory} \\
&\text{glUniformMatrix4fv(nMatID, 1, GL\_FALSE, glm::value\_ptr(normalMatrix));} \\
&\text{glUniformMatrix4fv(pMatID, 1, GL\_FALSE, glm::value\_ptr(projMatrix));} \\
&\text{glUniformMatrix4fv(vMatID, 1, GL\_FALSE, glm::value\_ptr(viewMatrix));} \\
&\text{glUniformMatrix4fv(mMatID, 1, GL\_FALSE, glm::value\_ptr(modelMatrix));} \\
&\\
&\text{// Set material for this object} \\
&\text{glm::vec3 kd(0.2, 0.2, 1.0);} \\
&\text{glm::vec3 ka = kd $\ast$ 0.15f;} \\
&\text{glm::vec3 ks(1.0, 1.0, 1.0);} \\
&\text{float phongExp = 32.0;} \\
&\\
&\text{glUniform3fv(kaID, 1, glm::value\_ptr(ka));} \\
&\text{glUniform3fv(kdID, 1, glm::value\_ptr(kd));} \\
&\text{glUniform3fv(ksID, 1, glm::value\_ptr(ks));} \\
&\text{glUniform1f(phongExpID, phongExp);} \\
&\\
&\text{// Process the object and note that modelData.size() holds} \\
&\text{// the number of vertices, not the number of triangles!} \\
&\text{glBindVertexArray(VAO);} \\
&\text{glDrawArrays(GL\_TRIANGLES, 0, modelData.size());} \\
&\text{glBindVertexArray(0);} \\
&\\
&\text{glUseProgram(0);}
\end{aligned} glUseProgram(BlinnPhongShaderID); // Describe the Local Transform Matrix glm::mat4 modelMatrix = glm::mat4(1.0); // Identity Matrix modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0f, 1.0f, 0.0f)); float rot = (-90.0f / 180.0f) ∗ M_PI; modelMatrix = glm::rotate(modelMatrix, rot, glm::vec3(1, 0, 0)); // Set the Normal Matrix glm::mat4 normalMatrix = glm::transpose(glm::inverse(viewMatrix ∗ modelMatrix)); // Pass the matrices to the GPU memory glUniformMatrix4fv(nMatID, 1, GL_FALSE, glm::value_ptr(normalMatrix)); glUniformMatrix4fv(pMatID, 1, GL_FALSE, glm::value_ptr(projMatrix)); glUniformMatrix4fv(vMatID, 1, GL_FALSE, glm::value_ptr(viewMatrix)); glUniformMatrix4fv(mMatID, 1, GL_FALSE, glm::value_ptr(modelMatrix)); // Set material for this object glm::vec3 kd(0.2, 0.2, 1.0); glm::vec3 ka = kd ∗ 0.15f; glm::vec3 ks(1.0, 1.0, 1.0); float phongExp = 32.0; glUniform3fv(kaID, 1, glm::value_ptr(ka)); glUniform3fv(kdID, 1, glm::value_ptr(kd)); glUniform3fv(ksID, 1, glm::value_ptr(ks)); glUniform1f(phongExpID, phongExp); // Process the object and note that modelData.size() holds // the number of vertices, not the number of triangles! glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, modelData.size()); glBindVertexArray(0); glUseProgram(0);
顶点着色器输出三个变量到片段阶段:
n ⃗ \vec{n} n ,相机空间中的顶点法向量。
h ⃗ \vec{h} h ,Blinn-Phong 着色所需的半向量。
l ⃗ \vec{l} l ,相机空间中光的方向。
#version 330 core // // Blinn-Phong Vertex Shader // layout(location=0) in vec3 in_Position; layout(location=1) in vec3 in_Normal; out vec4 normal; out vec3 half; out vec3 lightdir; struct LightData { vec3 position; vec3 intensity; } ; uniform LightData light; uniform mat4 projMatrix; uniform mat4 viewMatrix; uniform mat4 modelMatrix; uniform mat4 normalMatrix; void main(void) { // Calculate lighting in eye space: transform the local // position to world and then camera coordinates. vec4 pos = viewMatrix ∗ modelMatrix ∗ vec4(in_Position, 1.0); vec4 lightPos = viewMatrix ∗ vec4(light.position, 1.0); nomal = normalMatrix ∗ vec4(in_Normal, 0.0); vec3 v = normalize(-pos.xyz); lightdir = normalize(lightPos.xyz - pos.xyz); half = normalize(v + lightdir); gl_Position = projMatrix ∗ pos; } \begin{aligned}
&\text{\#version 330 core} \\
&\\
&\text{//} \\
&\text{// Blinn-Phong Vertex Shader} \\
&\text{//} \\
&\\
&\text{layout(location=0) in vec3 in\_Position;} \\
&\text{layout(location=1) in vec3 in\_Normal;} \\
&\\
&\text{out vec4 normal;} \\
&\text{out vec3 half;} \\
&\text{out vec3 lightdir;} \\
&\\
&\text{struct LightData \{} \\
&\quad\text{vec3 position;} \\
&\quad\text{vec3 intensity;} \\
&\}; \\
&\text{uniform LightData light;} \\
&\\
&\text{uniform mat4 projMatrix;} \\
&\text{uniform mat4 viewMatrix;} \\
&\text{uniform mat4 modelMatrix;} \\
&\text{uniform mat4 normalMatrix;} \\
&\\
&\text{void main(void) \{} \\
&\quad\begin{aligned}
&\text{// Calculate lighting in eye space: transform the local} \\
&\text{// position to world and then camera coordinates.} \\
&\text{vec4 pos = viewMatrix $\ast$ modelMatrix $\ast$ vec4(in\_Position, 1.0);} \\
&\text{vec4 lightPos = viewMatrix $\ast$ vec4(light.position, 1.0);} \\
&\\
&\text{nomal = normalMatrix $\ast$ vec4(in\_Normal, 0.0);} \\
&\\
&\text{vec3 v = normalize(-pos.xyz);} \\
&\text{lightdir = normalize(lightPos.xyz - pos.xyz);} \\
&\text{half = normalize(v + lightdir);} \\
&\\
&\text{gl\_Position = projMatrix $\ast$ pos;}
\end{aligned} \\
&\}
\end{aligned} #version 330 core // // Blinn-Phong Vertex Shader // layout(location=0) in vec3 in_Position; layout(location=1) in vec3 in_Normal; out vec4 normal; out vec3 half; out vec3 lightdir; struct LightData { vec3 position; vec3 intensity; } ; uniform LightData light; uniform mat4 projMatrix; uniform mat4 viewMatrix; uniform mat4 modelMatrix; uniform mat4 normalMatrix; void main(void) { // Calculate lighting in eye space: transform the local // position to world and then camera coordinates. vec4 pos = viewMatrix ∗ modelMatrix ∗ vec4(in_Position, 1.0); vec4 lightPos = viewMatrix ∗ vec4(light.position, 1.0); nomal = normalMatrix ∗ vec4(in_Normal, 0.0); vec3 v = normalize(-pos.xyz); lightdir = normalize(lightPos.xyz - pos.xyz); half = normalize(v + lightdir); gl_Position = projMatrix ∗ pos; }
片段着色器根据着色方程进行着色:
#version 330 core // // Blinn-Phong Fragment Shader // in vec4 normal; in vec3 half; in vec3 lightdir; layout(location=0) out vec4 fragmentColor; struct LightData { vec3 position; vec3 intensity; } ; uniform LightData light; uniform vec3 Ia; uniform vec3 ka, kd, ks; uniform float phongExp; void main(void) { vec3 n = normalize(normal.xyz); vec3 h = normalize(half); vec3 l = normalize(lightdir); vec3 intensity = ka ∗ Ia + kd ∗ light.intensity ∗ max(0.0, dot(n, l)) + ks ∗ light.intensity ∗ pow(max(0.0, dot(n, h)), phongExp); fragmentColor = vec4(intensity, 1.0); } \begin{aligned}
&\text{\#version 330 core} \\
&\\
&\text{//} \\
&\text{// Blinn-Phong Fragment Shader} \\
&\text{//} \\
&\\
&\text{in vec4 normal;} \\
&\text{in vec3 half;} \\
&\text{in vec3 lightdir;} \\
&\\
&\text{layout(location=0) out vec4 fragmentColor;} \\
&\\
&\text{struct LightData \{} \\
&\quad\text{vec3 position;} \\
&\quad\text{vec3 intensity;} \\
&\}; \\
&\text{uniform LightData light;} \\
&\\
&\text{uniform vec3 Ia;} \\
&\text{uniform vec3 ka, kd, ks;} \\
&\text{uniform float phongExp;} \\
&\\
&\text{void main(void) \{} \\
&\quad\begin{aligned}
&\text{vec3 n = normalize(normal.xyz);} \\
&\text{vec3 h = normalize(half);} \\
&\text{vec3 l = normalize(lightdir);} \\
&\\
&\begin{aligned}
\text{vec3 intensity = }&\text{ka $\ast$ Ia} \\
&\text{+ kd $\ast$ light.intensity $\ast$ max(0.0, dot(n, l))} \\
&\text{+ ks $\ast$ light.intensity} \\
&\qquad\text{$\ast$ pow(max(0.0, dot(n, h)), phongExp);}
\end{aligned} \\
&\text{fragmentColor = vec4(intensity, 1.0);}
\end{aligned} \\
&\}
\end{aligned} #version 330 core // // Blinn-Phong Fragment Shader // in vec4 normal; in vec3 half; in vec3 lightdir; layout(location=0) out vec4 fragmentColor; struct LightData { vec3 position; vec3 intensity; } ; uniform LightData light; uniform vec3 Ia; uniform vec3 ka, kd, ks; uniform float phongExp; void main(void) { vec3 n = normalize(normal.xyz); vec3 h = normalize(half); vec3 l = normalize(lightdir); vec3 intensity = ka ∗ Ia + kd ∗ light.intensity ∗ max(0.0, dot(n, l)) + ks ∗ light.intensity ∗ pow(max(0.0, dot(n, h)), phongExp); fragmentColor = vec4(intensity, 1.0); }
当 host 请求 uniform 变量的句柄时,必须使用完全限定名来引用结构体 uniform 变量。
lightPosID = shader.createUniform("light.position"); lightIntensityID = shader.createUniform("light.intensity"); \begin{aligned}
&\text{lightPosID = shader.createUniform("light.position");} \\
&\text{lightIntensityID = shader.createUniform("light.intensity");}
\end{aligned} lightPosID = shader.createUniform("light.position"); lightIntensityID = shader.createUniform("light.intensity");
法向量着色器
准备一组专用于调试的着色器是有帮助的,法向量着色程序 (normal shader program)就是其中之一。法向量着色通常有助于确认输入几何是否正确地组织,以及计算是否正确。
#version 330 core in vec4 normal; layout(location=0) out vec4 fragmentColor; void main(void) { // Notice the use of swizzling here to access // only the xyz values to convert the normal vec4 // into a vec3 type! vec3 intensity = normalize(normal.xyz) ∗ 0.5 + 0.5; fragmentColor = vec4(intensity, 1.0); } \begin{aligned}
&\text{\#version 330 core} \\
&\\
&\text{in vec4 normal;} \\
&\\
&\text{layout(location=0) out vec4 fragmentColor;} \\
&\\
&\text{void main(void) \{} \\
&\quad\begin{aligned}
&\text{// Notice the use of swizzling here to access} \\
&\text{// only the xyz values to convert the normal vec4} \\
&\text{// into a vec3 type!} \\
&\text{vec3 intensity = normalize(normal.xyz) $\ast$ 0.5 + 0.5;} \\
&\text{fragmentColor = vec4(intensity, 1.0);}
\end{aligned} \\
&\}
\end{aligned} #version 330 core in vec4 normal; layout(location=0) out vec4 fragmentColor; void main(void) { // Notice the use of swizzling here to access // only the xyz values to convert the normal vec4 // into a vec3 type! vec3 intensity = normalize(normal.xyz) ∗ 0.5 + 0.5; fragmentColor = vec4(intensity, 1.0); }
纹理对象
OpenGL 原生支持纹理对象 (Texture objects)。纹理对象必须从 host 复制到 GPU 中,并设置 OpenGL 状态,才能分配空间并初始化。纹理坐标通常集成在 VBO 上,并作为顶点属性传给着色程序。片段着色器通常使用插值纹理坐标执行纹理查询函数。
纹理数据要么从文件中加载,要么程序式地生成。硬件上纹理查询可以是 1D、2D、3D,而 OpenGL 总是以线性缓冲的形式加载纹理数据。
float ∗ imgData = new float[imgHeight ∗ imgWidth ∗ 3]; ... GLuint texID; glGenTextures(1, &texID); glBindTexture(GL_TEXTURE_2D, texID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, imgWidth, imgHeight, 0, GL_RGB, GL_FLOAT, imgData); glBindTexture(GL_TEXTURE_2D, 0); delete [ ] imgData; \begin{aligned}
&\text{float $\ast$imgData = new float[imgHeight $\ast$ imgWidth $\ast$ 3];} \\
&\text{...} \\
&\text{GLuint texID;} \\
&\text{glGenTextures(1, \&texID);} \\
&\text{glBindTexture(GL\_TEXTURE\_2D, texID);} \\
&\text{glTexParameteri(GL\_TEXTURE\_2D, GL\_TEXTURE\_WRAP\_S, GL\_CLAMP);} \\
&\text{glTexParameteri(GL\_TEXTURE\_2D, GL\_TEXTURE\_WRAP\_T, GL\_CLAMP);} \\
&\text{glTexParameteri(GL\_TEXTURE\_2D, GL\_TEXTURE\_MAG\_FILTER, GL\_LINEAR);} \\
&\text{glTexParameteri(GL\_TEXTURE\_2D, GL\_TEXTURE\_MIN\_FILTER, GL\_LINEAR);} \\
&\text{glTexImage2D(GL\_TEXTURE\_2D, 0, GL\_RGB, imgWidth, imgHeight, 0,} \\
&\qquad\qquad\qquad\quad\text{GL\_RGB, GL\_FLOAT, imgData);} \\
&\text{glBindTexture(GL\_TEXTURE\_2D, 0);} \\
&\\
&\text{delete [ ] imgData;}
\end{aligned} float ∗ imgData = new float[imgHeight ∗ imgWidth ∗ 3]; ... GLuint texID; glGenTextures(1, &texID); glBindTexture(GL_TEXTURE_2D, texID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, imgWidth, imgHeight, 0, GL_RGB, GL_FLOAT, imgData); glBindTexture(GL_TEXTURE_2D, 0); delete [ ] imgData;
glGenTextures \text{glGenTextures} glGenTextures 用于在 device 上为纹理对象生成一个句柄。glBindTexture \text{glBindTexture} glBindTexture 将句柄绑定到纹理目标 (texture target)上,GL_TEXTURE_2D \text{GL\_TEXTURE\_2D} GL_TEXTURE_2D 是其中之一,随后便可以操作纹理状态。OpenGL 纹理状态和参数会影响纹理坐标解释 (texture coordinate interpretation)和纹理查询滤波 (texture lookup filtering)。对于 OpenGL 纹理目标 GL_TEXTURE_2D \text{GL\_TEXTURE\_2D} GL_TEXTURE_2D ,纹理坐标是设备规范化的 (device normalized),即 [ 0 , 1 ] [0,1] [ 0 , 1 ] 区间上。纹理数据分配必须保证宽高尺寸是 2 的幂。glTexParameter \text{glTexParameter} glTexParameter 函数为当前所绑定的纹理设置参数;依赖于数据类型,这一函数签名有多种形式。GL_CLAMP \text{GL\_CLAMP} GL_CLAMP 表明纹理坐标被硬件截断到 [ 0 , 1 ] [0,1] [ 0 , 1 ] 上。GL_TEXTURE_MAG_FILTER \text{GL\_TEXTURE\_MAG\_FILTER} GL_TEXTURE_MAG_FILTER 、GL_TEXTURE_MIN_FILTER \text{GL\_TEXTURE\_MIN\_FILTER} GL_TEXTURE_MIN_FILTER 分别表示纹理对象的放大 (magnifying)和缩小 (minifying)滤波器。GL_LINEAR \text{GL\_LINEAR} GL_LINEAR 表示线性滤波器。通过设置相关的纹理状态,可以让图形硬件自动执行许多操作。
glTexImage2D \text{glTexImage2D} glTexImage2D 将纹理从 host 复制到 device 上,它的参数依次为:纹理目标、mipmap 等级、内部格式、宽度、高度、边框宽度、纹理元素(texel)数据格式、纹理元素(texel)数据类型、纹理数据源。
顶点数据结构如下:
struct vertexData { glm::vec3 pos; glm::vec3 normal; glm::vec3 texCoord; } \begin{aligned}
&\text{struct vertexData \{} \\
&\quad\begin{aligned}
&\text{glm::vec3 pos;} \\
&\text{glm::vec3 normal;} \\
&\text{glm::vec3 texCoord;}
\end{aligned} \\
&\}
\end{aligned} struct vertexData { glm::vec3 pos; glm::vec3 normal; glm::vec3 texCoord; }
host 上对 VBO、VAO 的处理如下:
glBindBuffer(GL_ARRAY_BUFFER, m_triangleVBO[0]); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 ∗ sizeof(GLfloat), 0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 ∗ sizeof(GLfloat), (const GLvoid ∗ )12); glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 ∗ sizeof(GLfloat), (const GLvoid ∗ )24); glBindVertexArray(0); \begin{aligned}
&\text{glBindBuffer(GL\_ARRAY\_BUFFER, m\_triangleVBO[0]);} \\
&\\
&\text{glEnableVertexAttribArray(0);} \\
&\text{glVertexAttribPointer(0, 3, GL\_FLOAT, GL\_FALSE, 8 $\ast$ sizeof(GLfloat), 0);} \\
&\\
&\text{glEnableVertexAttribArray(1);} \\
&\text{glVertexAttribPointer(1, 3, GL\_FLOAT, GL\_FALSE, 8 $\ast$ sizeof(GLfloat), (const GLvoid$\ast$)12);} \\
&\\
&\text{glEnableVertexAttribArray(2);} \\
&\text{glVertexAttribPointer(2, 2, GL\_FLOAT, GL\_FALSE, 8 $\ast$ sizeof(GLfloat), (const GLvoid$\ast$)24);} \\
&\\
&\text{glBindVertexArray(0);}
\end{aligned} glBindBuffer(GL_ARRAY_BUFFER, m_triangleVBO[0]); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 ∗ sizeof(GLfloat), 0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 ∗ sizeof(GLfloat), (const GLvoid ∗ )12); glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 ∗ sizeof(GLfloat), (const GLvoid ∗ )24); glBindVertexArray(0);
渲染 VAO 之前必须先激活或绑定纹理对象。通常,图形硬件允许在执行着色程序时使用多个纹理对象。纹理单元 (texture units)就是让着色器同时使用多个纹理的机制。要让着色器使用纹理,必须先关联上一个纹理单元。
glUseProgram(shaderID); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texID); glUniform1i(texUnitID, 0); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); glUseProgram(0); \begin{aligned}
&\text{glUseProgram(shaderID);} \\
&\\
&\text{glActiveTexture(GL\_TEXTURE0);} \\
&\text{glBindTexture(GL\_TEXTURE\_2D, texID);} \\
&\text{glUniform1i(texUnitID, 0);} \\
&\\
&\text{glBindVertexArray(VAO);} \\
&\text{glDrawArrays(GL\_TRIANGLES, 0, 3);} \\
&\text{glBindVertexArray(0);} \\
&\\
&\text{glBindTexture(GL\_TEXTURE\_2D, 0);} \\
&\\
&\text{glUseProgram(0);}
\end{aligned} glUseProgram(shaderID); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texID); glUniform1i(texUnitID, 0); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); glUseProgram(0);
glActiveTexture \text{glActiveTexture} glActiveTexture 函数用于激活纹理单元。纹理单元依序用 GL_TEXTURE0 \text{GL\_TEXTURE0} GL_TEXTURE0 、GL_TEXTURE1 \text{GL\_TEXTURE1} GL_TEXTURE1 、GL_TEXTURE2 \text{GL\_TEXTURE2} GL_TEXTURE2 … 表示。激活的纹理单元将会通过 uniform 变量提供给着色器。glUniform \text{glUniform} glUniform 函数用于将着色器中声明的 uniform 纹理变量连接到纹理单元索引上。
着色器纹理查询
为了在着色程序中执行所需的纹理查询与混合。顶点着色器需要做如下变化:
获取纹理坐标属性
layout(location=2) in vec2 in_TexCoord; \text{layout(location=2) in vec2 in\_TexCoord;} layout(location=2) in vec2 in_TexCoord;
定义输出纹理坐标变量
out vec2 tCoord; \text{out vec2 tCoord;} out vec2 tCoord;
将输入的顶点属性复制到输出变量上
tCoord = in_TexCoord; \text{tCoord = in\_TexCoord;} tCoord = in_TexCoord;
片段着色器中使用 sampler \text{sampler} sampler 类型获取纹理单元,sampler \text{sampler} sampler 是着色语言中的一种数据类型,它允许从单个纹理对象中查询数据。sampler \text{sampler} sampler 类型也有多种,其中 sampler2D \text{sampler2D} sampler2D 对应纹理状态 GL_TEXTURE_2D \text{GL\_TEXTURE\_2D} GL_TEXTURE_2D 。
片段着色器中所需的变化:
in vec2 tCoord; uniform sampler2D textureUnit; \begin{aligned}
&\text{in vec2 tCoord;} \\
&\text{uniform sampler2D textureUnit;} \\
\end{aligned} in vec2 tCoord; uniform sampler2D textureUnit;
GLSL 纹理查询函数 texture \text{texture} texture 可用于纹理采样。它的参数分别为:持有纹理单元的 sampler \text{sampler} sampler 类型变量、纹理坐标,返回值为 vec4 \text{vec4} vec4 类型的变量。
片段着色器主函数需做如下变动:
vec3 kdTexel = texture(textureUnit, tCoord).rgb; vec3 intensity = ka ∗ Ia + kdTexel ∗ light.intensity ∗ max(0.0, dot(n, l)) + ks ∗ light.intensity ∗ pow(max(0.0, dot(n, h)), phongExp); \begin{aligned}
&\text{vec3 kdTexel = texture(textureUnit, tCoord).rgb;} \\
&\begin{aligned}
\text{vec3 intensity = }&\text{ka $\ast$ Ia + kdTexel $\ast$ light.intensity} \\
&\text{$\ast$ max(0.0, dot(n, l)) + ks $\ast$ light.intensity} \\
&\text{$\ast$ pow(max(0.0, dot(n, h)), phongExp);}
\end{aligned}
\end{aligned} vec3 kdTexel = texture(textureUnit, tCoord).rgb; vec3 intensity = ka ∗ Ia + kdTexel ∗ light.intensity ∗ max(0.0, dot(n, l)) + ks ∗ light.intensity ∗ pow(max(0.0, dot(n, h)), phongExp);
GL_TEXTURE_RECTANGLE \text{GL\_TEXTURE\_RECTANGLE} GL_TEXTURE_RECTANGLE 是另一种有用的纹理目标,它是唯一一个宽高不必是 2 的幂,且使用非归一化纹理坐标的纹理对象。它不允许重复平铺。着色器中相应的类型为 sampler2DRect \text{sampler2DRect} sampler2DRect 。