昨天在学Vulkan tutorial的时候发现了这个神奇操作,于是就生出了在OpenGL里也试试看的想法。写完之后发现还是有一定区别的1。
由于我写的文章基本是在同学圈子里传,第一篇文章发完之后几个同学吐槽讲的太简单,看不懂。那么这一篇我就稍微讲细一点吧= =。
当然也可以用FILE指针
STL里用ifstream
来读取文件。有stream后缀的,一般操作都会和cin
、cout
类似。打开文件以后可以再用stringstream
直接把读取的字符写到一个字符串里面。io操作经常出事(找不到文件、写了只读文件啥的),所以用一个try-catch捕获一下问题,出了事也好找一些。这里就直接贴代码了:
string readString(string filePath) {
string result;
ifstream in;
in.exceptions(ifstream::failbit | ifstream::badbit);
try {
in.open(filePath);
stringstream ss;
ss << in.rdbuf();
in.close();
result = ss.str();
} catch (ifstream::failure& e) {
cerr << e.what() << endl;
}
return result;
}
只能是简介,因为我太菜了
据我所学啊,显卡的渲染是一条流水线。也叫做渲染管线
整个管线的流程一般是先从显存拿到顶点和索引的数据的数据2,然后这些数据就会被交给顶点着色器。接着会依次经过几何细分着色器、几何着色器、光栅化(把图形确定为一个个的片段。PS:片段这里可以简单理解为像素。)、片段着色器和最后的颜色混合(把在同一个像素点上的透明片段混合起来)。完成之后就是一张图片了,可以交给显示器显示了。
着色器是什么东西?简单地说就是在显卡上跑的程序。
显卡的核心数特别多,主要靠的是“人多力量大”,单独拿出来就不行了。有一个比喻是CPU的每一个核心都相当于一个高中生,而GPU的每个核心只能算小学生,画一张图相当于让所有高中生和小学生同时做一本加减乘除口算题册,即使高中生算的远比小学生快也比不上小学生们一人做一道题来的更快。编写着色器就相当于是给每一组小学生分配任务。
OpenGL的着色器们都很相似,都以一个void main()
函数作为入口。使用的是一个叫做GLSL的专用语言来编写,不过市面上的着色语言都是由C语言魔改来的,没必要专门去学。
顶点着色器会拿到顶点的各种信息(位置,颜色,纹理坐标等)。它一般要负责做的事是确定顶点的位置,以及把颜色和纹理坐标发给片段着色器。接收和发送的变量会用in
、out
关键字修饰,不过在本文不会有接收数据的操作。先贴代码:
// vert.glsl
#version 460 core
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
void main()
{
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}
最上面的是#version
预编译命令,用来表示使用的是什么版本,我用的版本是OpenGL 4.6 core,所以写上了#version 460 core
声明,如果不声明的话可能会有一些新功能不支持。
我们在顶点着色器中新建了一个向量数组positions
,数组的每一个元素都是一个顶点的坐标。因为要画的是个三角形,所以这里用的是二维向量。
接着就是main
函数,注意这里的main
函数是没有返回值的,和Unity用的cg不一样。glsl传递数据都是通过给变量赋值来实现的。gl_Position
就是我们要传出的数据之一,OpenGL通过看这个数据的值来确定顶点的最终位置。我们给OpenGL最终传递的是一个4维向量,所以要补充上去两个维度的坐标。至于为什么是4维这里就不做展开了,想了解的自行搜索“透视除法”。gl_VertexID
这个变量表示了OpenGL正在绘制第几个顶点,于是就用它为索引确定不同顶点的坐标。
由于顶点和片段的数量差距很大,所以由顶点着色器传递给片段着色器的数据都会经过一个叫“线性插值”的过程,会根据顶点的坐标来算出片段的坐标(这个东西困惑了我很久= =)。不过因为顶点着色器没有传值,所有这篇文章也不会看到这个。上代码:
// frag.glsl
#version 460 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
很简单,片段着色器在这里就是发送了一个叫做FragColor的四维向量。这个四维向量的分量会被解释为RGBA颜色,在OpenGL里,每个颜色分量的取值范围都是[0.0, 1.0],和ps里的[0, 255]不同。不过也就是个换算的事。
最终的着色器会汇集为着色器程序,用来表示一个画图的方式。可以用glUseProgram
来让OpenGL使用它。
终于到写代码的部分了,不容易不容易。OpenGL里的所有对象都是由一个无符号整数作为句柄(类似于指针)来间接访问的。
第一步是写一个加载着色器程序的过程。
unsigned int loadShaderProgram(string vertPath, string fragPath) {
return 0;
}
这个过程要先创建着色器对象,这首先要从源文件中读取着色器的代码,然后将其存到一个字符串里。就用先前的readString
函数即可。OpenGL是一个C语言的API,所以还需要一个C风格的字符指针来指向字符串的数据。
auto vertStr = readString(vertPath);
auto fragStr = readString(fragPath);
const char* vertSrc = vertStr.c_str();
const char* fragSrc = fragStr.c_str();
接着就是用glCreateShader
创建一个空的着色器对象,然后用glShaderSource(shader, count, source, length)
把源代码传给这个空对象并且用glCompileShader(shader)
编译。
auto vert = glCreateShader(GL_VERTEX_SHADER);
auto frag = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(vert, 1, &vertSrc, nullptr);
glShaderSource(frag, 1, &fragSrc, nullptr);
glCompileShader(vert);
glCompileShader(frag);
编译完成之后就可以把着色器附着到着色器程序上去了。这里创建一个着色器程序
auto program = glCreateProgram();
然后用glAttachShader(program, shader)
命令附着着色器到着色器程序上,并用glLinkProgram(program)
链接。
glAttachShader(program, vert);
glAttachShader(program, frag);
glLinkProgram(programm);
着色器在着色器程序完成链接之后就没有存在的必要了,因此在函数的最后删除它们。
glDeleteShader(vert);
glDeleteShader(frag);
最后返回着色器程序:
return program;
}
回到我们的main函数。在主循环之前加载好着色器程序,
auto program = loadShaderProgram("../shader/vert.glsl", "../shader/frag.glsl");
最后在主循环中使用这个着色器程序,调用glDrawArrays()。
glUseProgram(program);
glDrawArrays(GL_TRIANGLES, 0, 3);
运行之后,就可以看到一个红色的倒三角形了!
完整源码
gl_VertexIndex变量在OpenGL里是gl_VertexID,链接 ↩︎
这个叫输入装配器(Input Assembler)阶段(借用一下DX的教程hhh) ↩︎