在一的基础上绘制一个三角形
理论:
opengl使用图形渲染管线来把2D坐标转换成3D坐标,他可以分为两部分:一是把3D坐标转换到屏幕的2D坐标,二是为这些2D坐标转变成有颜色的像素。
图形渲染管线接受一组3D坐标,将其转换成屏幕上的2D像素,其中经过这样几个阶段
3D顶点坐标输入->顶点着色器->形状图元装配->几何着色器->光栅化->片段着色器->混合与测试
1、顶点着色器:将输入的3D坐标转换成另一种3D坐标来给下个阶段使用
2、形状图元装配,将顶点着色器处理的3D坐标装配成指定的形状(比如三角形),然后输出给几何着色器
3、几何着色器,几何着色器可以为装配好的图元加上额外的顶点来构建新的图元,然后进行光栅化
4、光栅化,将图元映射为最终的屏幕上的像素,进过裁切后交给片段着色器
5、片段着色器,对留下的像素颜色进行处理的地方我们很多的效果便在这里实现(比如锐化),这里得到的颜色便是像素最终的颜色之后进入混合与测试
6、混合与测试又称alpha测试与混合,alpha即像素透明度,混合即把像素按照不同的透明关系混合;深度测试则是测试3D空间中物体的遮挡关系,决定最后剩下的哪些像素应该显示在前,哪些被遮挡舍弃;模板测试决定3D空间中物体那些部分应当显示(由程序员决定的)哪些被剔除。
在渲染一个三角形时我们首先会需要一组顶点坐标
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
由x/y/z坐标确定一个点的空间位置,三个点确定一个三角。此处所有顶点的Z坐标为0.0,故它们之间不存在遮挡关系。
接着我们会需要一个顶点缓冲VBO
顶点着色器会将上面的3D坐标转化为 “标准化设备坐标” ,标准化坐标系是一个2维坐标系,x,y轴的值被限定在(-1,1)内,故我们之前的三角的坐标均未超过(-1,1)的范畴。
“标准化设备坐标”之后会被转化成“屏幕空间坐标”,这通过 “视口变换” 来完成,也就是上一篇使用的glViewPort(width,height,null.null);
即把x(-1,1);y(-1,1)范围内的坐标转换到x(0,width);y(0,height)的范围里。之后的 “屏幕空间坐标” 再传递给片段着色器。
但首先,我们得把顶点数据发送到顶点着色器。
此时我们会需要:顶点缓冲对象VBO。
opengl采用的是c语言,我们首先用unsigned int创建一个VBO。
再使用glGenBuffers(1,VBO);来生成一个VBO“对象”。
再通过glBindBuffer(GL_ARRAY_BUFFER,VBO);告知我们当前使用的顶点缓冲为VBO。
最后用glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);来把顶点数据绑定到当前的顶点缓冲(GL_ARRAY_BUFFER意味着我们是为顶点缓冲绑定)也就是VBO上。
完整代码如下:
unsigned int VBO;
glGenBuffers(1,VBO);
//生成数量 要生成的缓冲对象
glBindBuffer(GL_ARRAY_BUFFER,VBO);
//缓冲类型 缓冲对象
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
//缓冲类型 缓冲数据大小 实际缓冲数据指针 管理缓冲数据的方式
此时VBO就已经拥有了顶点数据,特别提一下glBufferData中的GL_STATIC_DRAW,这个函数的第四个参数决定的是
“我们希望显卡管理缓冲数据的方式”
GL_STATIC_DRAW 几乎不改变顶点数据
GL_DYNAMIC_DRAW 顶点数据经常改变
GL_STREAM_DRAW 顶点数据每次绘制都改变
介于我们的这个三角不会改变顶点故选择GL_STATIC_DRAW
链接顶点数据
拥有顶点缓冲后我们需要告知opengl如何解析这些数据,但首先我们看看顶点缓冲中的数据应当怎么解析。
float vertices[] = { -0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
如上所示,所有的数据连续排列,
此处的顶点被存储为3组数据(3个顶点),
每组12字节;
从第一个数开始计算0-3,4-6,7-9为第一、第二、第三组,即没有起始偏移。
你大概注意到我做了没必要的转行,因为每行其实将对应链接顶点数据函数的参数的一项。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0 );
//此组属性在顶点着色器进行读取的位置 一组顶点的大小 数据类型 是否标准化 一组长度 起始偏移
glEnableVertexAttribArray(0);
//激活第“0”组属性,即之前的第一个参数设定的“组号”
使用如上代码进行链接数据以及顶点属性解释。
glVertexAttribPointer();这个函数的参数意思如下:
1、0意味着这一组属性的位置,其实就是这组属性的编号,那么我们都知道数组是从0开始计算的故使用0。
2、3意味着这一组属性有三个数据,即属性的“大小”。
3、GL_FLOAT解释了属性数据的类型,即每个都是float类型。
4、GL_FALSE表示的是不进行数据的标准化,标准化意味着把你的数据强行映射到(-1,1)(无符号是0,1)的范畴,那么我们这里不需要这么做,因为我们的顶点数据本来就在这范围。
5、3 * sizeof(float)即12个字节大小,说的是一组属性的“长度”,或者叫做“步长”,从一组属性的第一个数据到下一组属性的第一个数据的“长度”(请不要问我为什么opengl不自己计算,明明我们已经告知了顶点大小和每个数据的类型,它就是这么写的,我也没办法)。
6、(void *)0,首先,我们看看这句代码本身什么意思。(void *)是强制类型转换,而且是C风格的,接着是0,即转换到void*类型的空指针;然后我再告诉你,哎嘿这个东西表示的开始的偏移量,也就是说你的第一个数据是从这段地址的0,开始计算的。进一步地解释,存储这些数据的空间起始点是一个地址值begin,最后的这个参数offset决定你从begin + offset来取得第一组的第一个数据;举个例子,假设一组数据为1,2,3,4,5(int型)你不需要开头的1,你希望从2开始取,那么这个offset参数你应该写(void* )( sizeof(int) ),也就是4转化成void *指针后的地址值。为什么要传入一个地址值来让这个参数显得晦涩难懂而不是在函数里计算?同上不要问我,猜测一下就是GPU很珍贵,这种CPU做起来很快的运算尽量自己算。
啊哈,我写了这么长一段,最后可别忘了还有一个函数,glEnableVertexAttrib(0);激活位置0,或者说“组号”为0的属性,这样数据才有效。至此顶点数据链接完成,接下来我们会把解析好的数据存放到顶点数组对象。
顶点数组对象VAO
啊啊啊,是的,我知道这看起来让人头晕,顶点缓冲->链接->顶点对象,其实还是很简单的。顶点数组对象是什么呢?
其实绑定顶点缓冲并链接后你就已经让opengl知道了绘制需要的数据,也就是说你若是在立即模式下就已经可以绘制三角了,但是介于我们的是核心模式,而且立即模式是过时的方式所以就干脆无视它好了(没错,我就是懒的写!但是知道更多的细节也不会让人更明白,所以养成“忽略这些细节”的习惯)。
顶点数组对象还有一个好处,如果你不使用顶点对象的话那么你需要每次循环中告知opengl如何解析你的数据,也就是每次都需要glVertexAttribPointer与glEnableVertex还有glBufferData和glBindBuffer(需要这个是因为你可能会绑别的)。使用顶点对象后便只需要绑定顶点数组对象了。
而我们启用了顶点对象,它会存储:
1、glEnableVertexAttribArray和glDisableVertexAttribArray(没错,这东西还有disable!)
2、通过glVertexAttribPointer配置的顶点属性
3、通过glVertexAttribPointer调用的顶点缓冲对象
也就和我之前说的一样,省去了绑定、配置、链接、激活。
创建顶点数组对象的代码和VBO很相似,事实上我们得把创建VAO的代码和创建VBO还有链接数据的代码放一起。
unsigned int VBO, VAO; //增加VAO
glGenBuffers(1, VBO);
glGenVertexArrays(1, VAO); //生成顶点数组对象
glBindVertexArray(VAO); //绑定顶点数组对象
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0 );
只增加了glGenVertexArrays与glBindVertexArray两个函数。那么这样就将数据解析的方式绑定到了VAO上,之后绘制时opengl就会按照存储的模式运作,所以我们终于首次脱离了CPU来到GPU上运作的代码,也就是实际的顶点着色器的编写。
实际的顶点着色器
顶点着色器使用GLSL着色器语言编写,形式类似C语言,用以处理发来的数据。
#version 330 core
//使用3.3版本核心模式
layout (location = 0) in vec3 aPos;
//接收位置在0 这个顶点属性是接收进来的(in) vec3类型的变量 之后使用名字为aPos
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
//处理这个顶点的数据 = vec4(x坐标,y坐标,z坐标, 透视变形值);
}
顶点着色器中的代码是为了处理每一个顶点的数据而编写的,即我们的三角形有三个顶点,那么顶点着色器中的代码是对其中一个顶点进行处理的(而不是三个作为整体,也就是说这些代码要运行三次来处理每个顶点)。layout定义的是一个顶点属性,比如位置,那么aPos就代表输入的位置信息,但代表的是一个顶点的信息;也可以声明其他的顶点属性,比如颜色,那么也要提供和顶点同数量的颜色数值组,对于三角形来说需要三组数据,你也可以在代码中进行一组颜色值的处理,然后使用out关键字输出到片段着色器。
此处的vec4的第四个变量是在处理物体透视关系的时候使用的一个除数,总之设定成1.0就对了,即正常的视觉透视效果。vec本身意味着向量vec3即3维向量。
实际的片段着色器
片段着色器接受前阶段顶点着色器信息来计算像素最后的颜色,颜色格式为RGBA。同样的,是对每个像素的计算,代码可能会运行n次。
#version 330 core
//同顶点着色器
out vec4 FragColor;
//声明一个要输出(out)的变量 类型为vec4 名字为FragColor
void main()
{
FragColor = vec4(1.0f, 0.8f, 0.2f, 1.0f);
//输出的颜色 = vec4类型 red1.0f green0.8f blue0.2f alpha1.0f(最高1.0即不透明)混合的颜色
}
使用out关键字来声明一个要输出的变量(即颜色变量),vec4类型作为颜色的类型。
编译着色器
拥有了着色器的代码之后,我们需要让opengl知道去哪里找到并利用它。opengl需要动态编译着色器的代码以供使用。
大概的步骤是:
1、创建着色器对象
2、将源码附加到着色器对象上
3、编译着色器对象
4、检测是否成功,失败输出错误信息
编译顶点与片段着色器的代码相似
顶点:
const char* vertexShaderSource = "顶点着色器代码";
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//创建GL_VERTEX_SHADER
glShaderSource(vertexShader, 1, vertexShaderSouce, NULL);
//着色器对象 源码(文件)数量 着色器源码 总之先设置为NULL
glCompileShader(vertexShader);
//编译着色器对象
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
//需要获取信息的着色器 编译状态 存放是否编译成功的flag
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog)
//着色器对象 错误信息大小 总之设置为NULL 存放错误信息的数组
cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"<<infoLog<<endl;
//让你知道是顶点着色器出问题了,输出错误信息
}
片段:
cosnt char* fragmentShaderSource = "存放片段着色器的地址";
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//改为GL_FRAGMENT_SHADER
glShaderSource(fragmentShader, 1, fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED!\n"<<infoLog<<endl;
}
此时就已经完成了着色器对象的编译,之后需要将多个编译好的着色器对象链接(link)到一起
链接着色器对象
不同的着色器对象通过链接最后会成为一个 “着色器程序对象” ,激活这个着色器程序对象就告诉了GPU去使用你的着色器。
链接步骤大概如下:
1、创建着色器程序对象
2、按照顺序链接着色器对象,即顶点着色器->几何着色器(可以没有使用默认)->片段着色器
3、链接
4、检测是否成功,失败输出错误信息
5、激活着色器程序对象
6、删除着色器对象,注意是着色器对象,不是你刚刚创建的着色器程序对象。
代码:
unsigned int shaderProgram;
shaderProgrom = glCreateProgram();
//创建着色器程序对象
glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram,fragmentShader);
//按顺序附加着色器对象
gllinkProgrom(shaderProgram);
//链接着色器程序对象
//检测成功与否
glGetProgromiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgromInfoLog(shaderProgram, 512, NULL, infoLog);
cout<<"ERROR::LINK::LINK_FAILED!\n"<<infoLog<<endl;
}
glUseProgram(shaderProgram);
//激活着色器程序对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
//删除已经没用的着色器对象
至此,着色器发挥作用
最后,绘制!
每一次绘制,我们都会需要以下条件:
1、激活的着色器程序对象
2、绑定你当前要绘制的顶点数组对象
3、实际的绘制函数
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TANGLES, 0, 3);
//绘制的图形形式 开始绘制的顶点 总共绘制的顶点数
只有一个新函数glDrawArrays,也就是实际用来绘制的函数。
第一个参数决定你绘制的图形(图元)即绘制的最小单位,此处为GL_TANGLES,即你绘制的图形由一个个三角构成
第二个参数决定开始绘制的顶点,我们定义了三个顶点,如果你要从第二个顶点开始绘制(当然那可没有三角),就改成1
第三个参数决定参与绘制的顶点数量
最终
#include"main.h"
int main()
{
using namespace initialization;
GLFWwindow *window = init(SCR_WIDTH,SCR_HEIGHT);
//顶点数据
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
//顶点缓冲对象
unsigned int VBO,VAO;
glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//着色器对象,特别注意不要把顶点着色器与片段着色器文件读反,或者附加反,或者用了两个顶点/片段
string vertexCode, fragmentCode;
int success;
char infoLog[1024];//错误信息
try {
ifstream vFileStream, fFileStream; //创建文件流
//保证ifstream对象可以抛出异常:
vFileStream.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fFileStream.exceptions(std::ifstream::failbit | std::ifstream::badbit);
vFileStream.open("vertexShader.vs"); fFileStream.open("fragmentShader.fs");
//打开文件
stringstream vCodeStream, fCodeStream; //创建字符流
vCodeStream << vFileStream.rdbuf();
fCodeStream << fFileStream.rdbuf(); //读入文件信息
vFileStream.close();
fFileStream.close(); //关闭流
vertexCode = vCodeStream.str();
fragmentCode = fCodeStream.str(); //获取字符串
cout << vertexCode << endl;
cout << fragmentCode << endl;
}
catch (exception &e)
{
cout << e.what() << endl;
}
const char* vCode = vertexCode.c_str();
const char* fCode = fragmentCode.c_str();//opengl是C编写的,故需要转变
unsigned int vertexShader, fragmentShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(vertexShader, 1, &vCode, nullptr);
//请注意&vCode,这意味着这个参数是指向指针的指针
glCompileShader(vertexShader);
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 1024, nullptr, infoLog);
cout << "Error,vertex shader failed to compile!\n" << infoLog << endl;
}
glShaderSource(fragmentShader, 1, &fCode, nullptr);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 1024, nullptr, infoLog);
cout << "Error,fragment shader failed to compile!\n" << infoLog << endl;
}
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shaderProgram, 1024, nullptr, infoLog);
cout << "Error,shader program failed to link!\n" << infoLog << endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);//清除指定BUFFER
// input
// -----
processInput(window);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);//交换缓冲
glfwPollEvents();//处理事件响应
}
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
最后提一下报错信息
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
//获取信息的对象 获取的信息种类 存储是否成功的标志(int)
if (!success)
{
glGetShaderInfoLog(vertexShader, 1024, nullptr, infoLog);
//获取信息的对象 存储信息的数组长度(或信息长度) 反正空 存放实际错误的char[]
cout << "Error,vertex shader failed to compile!\n" << infoLog << endl;
}
所有opengl读报错的形式类型getXXXXiv然后glGetxxxxxxInfoLog。