本章主要内容:
1、基本的矩阵变换
2、基本的OpenGL Buffer Object
3、基本的GLSL(OpenGL着色语言)
在正式开始学习之前,我必须要说明的是:
接上一章,我假设你已经搭建好 glfw3 + glad + opengl 的环境。
为了简化开发,我把一些比较繁琐的、乱七八糟的一些 Api 简单的封装了一下,因此在后面的教程中,我将只使用这些,当然也会慢慢完善(如果有需要的话)。。
首先我封装了 glfw 常用的一些 set*/get*/create*,这个 class 为 OpenGLWindow,具体的代码见源码(关注点不应该在这),它有一个虚函数 void render(),在事件循环中自动调用,所以我们自己的 Window 只需要继承 OpenGLWindow 并重新实现render()。
#ifndef MYWINDOW_H
#define MYWINDOW_H
#include "OpenGLWindow.h"
class MyRender;
class MyWindow : public OpenGLWindow
{
public:
MyWindow();
~MyWindow();
protected:
void render();
void resizeEvent(int width, int height);
private:
MyRender *m_render;
};
#endif
哦,这里还有一个 void resizeEvent(int width, int height),它在窗口大小改变后自动调用,而 MyRender 就是我们实际的渲染类了,这里只是简单的调用 m_render 的 render() 和 resizeGL(),如下:
#include "MyWindow.h"
#include "MyRender.h"
MyWindow::MyWindow()
{
m_render = new MyRender();
}
MyWindow::~MyWindow()
{
if (m_render)
delete m_render;
}
void MyWindow::render()
{
m_render->render();
}
void MyWindow::resizeEvent(int width, int height)
{
OpenGLWindow::resizeEvent(width, height);
m_render->resizeGL(width, height);
}
接下来是 OpenGLRender 类了:
#ifndef OPENGLRENDER_H
#define OPENGLRENDER_H
#include
#include
#include
using std::string;
class OpenGLRender
{
public:
enum ShaderType
{
Vertex = GL_VERTEX_SHADER,
Fragment = GL_FRAGMENT_SHADER
};
OpenGLRender();
~OpenGLRender();
public:
virtual void render() { }
virtual void resizeGL(int w, int h) { }
virtual void initializeGL() { }
virtual void initializeShader() { }
protected:
GLuint compileShader(ShaderType type, const string &source);
GLuint compileShaderFile(ShaderType type, const string &filename);
};
#endif
作为基类,还是四个虚函数,继承它并实现即可,这里我们主要看 GLuint compileShader(ShaderType type, const string &source)。
首先,我们要知道现代 OpenGL 的渲染管线,它是一系列数据处理的过程,并且将应用程序的数据转换到最终渲染的图像。
其中,蓝色的为可编程着色器,其中顶点着色器和片元着色器没有默认实现,因此必须由我们自己实现。
着色器使用一种类似c语言的GLSL来编写,我们来看一下[创建->使用]一个着色器程序具体流程:
现在回过头来看我们的 compileShader():
#include "OpenGLRender.h"
#include
#include
#include
OpenGLRender::OpenGLRender()
{
}
OpenGLRender::~OpenGLRender()
{
}
GLuint OpenGLRender::compileShader(ShaderType type, const string &source)
{
if (!source.empty())
{
GLuint shader = glCreateShader((GLenum)type);
const GLchar *shaderSource = source.c_str();
glShaderSource(shader, 1, &shaderSource, nullptr);
glCompileShader(shader);
GLint success;
GLchar infoLog[512];
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog);
if (type == Vertex)
std::cerr << "Compile Vertex Shader Error :" << infoLog << std::endl;
else if (type == Fragment)
std::cerr << "Compile Fragment Shader Error :" << infoLog << std::endl;
return 0;
}
return shader;
}
return 0;
}
GLuint OpenGLRender::compileShaderFile(ShaderType type, const string &filename)
{
std::ifstream fin;
fin.open(filename, std::ios_base::in);
if (!fin.is_open())
std::cerr << "Shader File " + filename + " Open Failed! " << std::endl;
std::stringstream buffer;
buffer << fin.rdbuf();
string source(buffer.str());
if (source.empty())
std::cerr << "Shader File " + filename + " is Empty! " << std::endl;
fin.close();
return compileShader(type, source);
}
1、我们使用 GLuint glCreateShader(GLenum shaderType)创建了一个着色器对象,shaderType 为着色器类型,可以为以下几个值:GL_VERTEX_SHADER,GL_FRAGMENT_SHADER,GL_TESS_CONTROL_SHADER,GL_TESS_EVALUATION_SHADER,GL_GEOMETRY_SHADER,正确返回非 0 值,返回 0 则发生错误。
2、使用 GLuint glShaderSource(GLuint shader, GLsizei count, const GLchar** string, const GLint* length) 将着色器代码string 关联到一个 shader 着色器对象上,count 表示 string 的行数,length 为 string 的字符数,如果 length 为 null,则假设 string 以 null 结尾。
3、使用 void glCompileShader(GLuint shader) 编译一个着色器对象,由 void glGetShaderiv() 判断编译状态,由 void glGetShaderInfoLog() 获取最后的编译结果。
最后,成功则返回正确的着色器对象,失败返回0,而我写的 OpenGLRender::compileShaderFile() 则可以让我们从本地文本文件来读取着色器代码。
接下来我们需要看 MyRender,这是真正执行一系列操作的地方:
#ifndef MYRENDER_H
#define MYRENDER_H
#include "OpenGLRender.h"
#include
#include
class MyRender : public OpenGLRender
{
public:
MyRender();
~MyRender();
void render();
void resizeGL(int w, int h);
void initializeGL();
void initializeShader();
void initializeTriangle();
private:
GLuint m_vbo;
GLuint m_program;
glm::mat4x4 m_projection;
};
#endif
老样子,继承 OpenGLRender 并实现它的几个虚函数,来看其实现:
#include "MyRender.h"
#include
#include
#include
struct VertexData
{
glm::vec3 postion;
glm::vec3 color;
};
MyRender::MyRender()
{
initializeGL();
}
MyRender::~MyRender()
{
glDeleteBuffers(1, &m_vbo);
glDeleteProgram(m_program);
}
void MyRender::render()
{
static GLfloat angle = 0.0f;
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
angle += 1.0f;
glm::mat4 modelMatrix(1.0f);
modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0f, 0.0f, -5.0f));
modelMatrix = glm::rotate(modelMatrix, glm::radians(angle), glm::vec3(0.0f, 1.0f, 0.0f));
glUseProgram(m_program);
GLuint mvp = glGetUniformLocation(m_program, "mvp");
glUniformMatrix4fv(mvp, 1, GL_FALSE, glm::value_ptr(m_projection * modelMatrix));
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
void MyRender::resizeGL(int w, int h)
{
GLfloat aspect = (GLfloat)w / (GLfloat)h;
m_projection = glm::perspective(glm::radians(30.0f), aspect, 1.0f, 10.0f);
}
void MyRender::initializeGL()
{
initializeShader();
initializeTriangle();
}
void MyRender::initializeShader()
{
GLuint vertexShader = compileShaderFile(Vertex, "../GLSL/vertex_glsl.vert");
GLuint fragmentShader = compileShaderFile(Fragment, "../GLSL/fragment_glsl.frag");
m_program = glCreateProgram();
glAttachShader(m_program, vertexShader);
glAttachShader(m_program, fragmentShader);
glLinkProgram(m_program);
int success;
char infoLog[512];
glGetProgramiv(m_program, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(m_program, sizeof(infoLog), nullptr, infoLog);
std::cerr << "Shader Program Linking Error :" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}
void MyRender::initializeTriangle()
{
VertexData vertices[] =
{
{ glm::vec3(-0.5f, -0.5f, 0.0f), glm::vec3(0.9f, 0.0f, 0.9f) },
{ glm::vec3( 0.0f, 0.5f, 0.0f), glm::vec3(0.9f, 0.9f, 0.0f) },
{ glm::vec3( 0.5f, -0.5f, 0.0f), glm::vec3(0.0f, 0.9f, 0.9f) }
};
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
int location = 0;
glVertexAttribPointer(location, 3, GL_FLOAT, GL_TRUE, sizeof(VertexData), (void *)0);
glEnableVertexAttribArray(location);
glVertexAttribPointer(location + 1, 3, GL_FLOAT, GL_TRUE, sizeof(VertexData), (void *)(sizeof(glm::vec3)));
glEnableVertexAttribArray(location + 1);
}
我们按着流程走,首先调用 initializeGL() ->调用 initializeShader(),initializeTriangle()。
先看 initializeShader() :
首先我们创建并编译了两个着色器对象,然后使用glCreateProgram()创建了一个着色器程序,
接下来使用 glAttachShader(GLuint program, GLuint shader) 将着色器对象附加到着色器程序上,然后使用glLinkProgram() 来处理所有附加的着色器并生成一个完整的着色器程序,
链接结果由 glGetProgramiv() 和 glGetProgramInfoLog() 查询,最后使用 glDeleteShader() 删除这两个着色器对象。
然后我们来看 initializeTriangle():
1、使用 glGenBuffers(GLsizei n, GLuint *buffers) 生成n个未使用的缓存对象,保存到buffers数组中。
2、glBindBuffer(GLenum target, GLuint buffer),我们使用的任何对(target)缓冲调用都会用来配置当前绑定的缓冲对象。
2、glBufferData(GLenum target, GLsizeptr size, const GLvoid* data, GLenum usage) 是真正为缓存对象分配存储空间的函数。target 是目标缓冲的类型,缓冲对象当前绑定到 target 类型上,size 指定传输数据的大小(以字节为单位),用一个简单的 sizeof() 计算出顶点数据大小就行。data 是我们希望发送的实际数据,usage 则是缓存使用的策略。
这里我们需要存储顶点数据数组,所以这里分配的是数组缓存对象 GL_ARRAY_BUFFER。
4、glVertexAttribPointer() 函数的参数非常多,第一个参数指定我们要配置的顶点属性,这里我们放一下,先来看顶点着色器:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color0;
out vec3 color;
uniform mat4 mvp;
void main(void)
{
gl_Position = mvp * vec4(position, 1.0f);
color = color0;
}
GLSL首行使用 #version 设置当前使用的 GLSL 版本和模式,这里使用 GLSL 330 (对应OpenGL 3.3) 核心模式 Core,
layout:布局控制,这里控制一个 in vec3 position 的位置为 0,in vec3 color0 的位置为1,
现在回到 glVertexAttribPointer() 函数,
它的第一个参数正是着色器中的 location,我们设置了 0 和 1 ,因此 0 控制顶点位置,1 控制顶点颜色,
第二个参数指定顶点属性的大小,顶点属性是一个vec3,它由3个值组成,所以大小是3,
第三个参数指定数据的类型,这里是 GL_FLOAT ( GLSL中 vec* 都是由浮点数值组成的 ),
第四个参数定义我们是否希望数据被标准化( Normalize )。如果我们设置为 GL_TRUE,所有数据都会被映射到 0(对于有符号型 signed 数据是 -1 )到 1 之间,
第五个参数叫做步长( Stride ),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在 3 个 float 之后,我们把步长设置为 sizeof(VertexData) 即可。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙),当然,我们也可以设置为 0 来让 OpenGL 决定具体步长是多少(只有当数值是紧密排列时才可用),
最后一个参数的类型是 void*,所以需要我们进行强制类型转换。它表示位置数据在缓冲中起始位置的偏移量( Offset )。由于 postion 数据在数组的开头,所以这里是 0,
glEnableVertexAttribArray(location):启用location对应的顶点属性数组,顶点属性数组下章再讲,
而第二个 glVertexAttribPointer() 调用我们改变 location = 1,color数据的偏移量 = position ( 它前面只有 position ),
继续回到着色器:
in out uniform:类型修饰符,in out 字面意思,输入输出修饰,unifom 表示修饰的变量由用户传递,对于着色器而言,这个变量是全局的(Global 不可重复),并且不可修改(常量),
vec3 mat4:GLSL内置类型,vec->vector向量( vec3 三维向量 ),mat->matrix 矩阵( mat4 4x4矩阵:四行四列 ),
我们的顶点着色器输出一个顶点位置 gl_Position,它是 GLSL 内置变量,类型为 vec4,一个颜色 color,类型为 vec3,
这里有一个 uniform mat4 mvp,它是所谓的 model(模型) - view(观察) - projection(投影) 矩阵,
这三个矩阵干嘛的呢?(啊我好累.....给两个链接自己看吧T T....)
OpenGL坐标系统:https://learnopengl-cn.github.io/01%20Getting%20started/08%20Coordinate%20Systems/
矩阵:https://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-3-matrices/
我们在 initializeTriangle() 中的坐标是( Local Space 局部空间 ),所以我们需要分别使用 model(模型) - view(观察) - projection ( 投影 ) 进行变换,所以将 mvp矩阵 右乘 position 得到裁剪空间的坐标,最终经 glViewport() 变换到( Screen Space 屏幕空间 )。
回到MyRender:
我们 在resizeGL() 中计算了 projection (投影矩阵),通过 glm::perspective() 生成透视投影矩阵,
【你应该#include
】它包含了一系列的矩阵变换。
后面的章节将会有非常多的 glm 的函数,我想你应该下载了 glm 的 doc (文档),所以相关的函数说明我就不再一一介绍了。
接着就是 render() 了:
glClearColor() 设置清屏颜色( 是的它是设置函数 ),然后我们调用 glClear() 来清除颜色缓冲。
在每个新的渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果(这可能是你想要的效果,但通常来说不是)。我们可以通过调用 glClear() 函数来清空屏幕的颜色缓冲,它接受一个缓冲位( Buffer Bit )来指定要清空的缓冲,可能的缓冲位有 GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT。由于现在我们只关心颜色值,所以我们只清空颜色缓冲
接着,我们使用 glm 生成一个 mat4 modelMatrix(模型矩阵),接着把它平移到 ( 0.0f, 0.0f, -5.0f ),接着绕 y 轴旋转 glm::radians(angle) 弧度,
【glm 0.9.9版之后】,所有的角度为弧度。
使用 glUseProgram() 启用一个链接过的着色器程序,然后使用 glGetUniformLocation() 获取一个着色器程序中 uniform 的 location 索引,通过此索引,使用 glUniform*(),可以设置它的值,*为后缀,这里是 Matrix4fv,即设置一个 4x4 矩阵(指针形式,第三个参数指示行主序 GL_TRUE,还是列主序 GL_FALSE )。
【glm::value_ptr()】返回一个变量的指针形式。
【OpenGL中的矩阵是列主序】
最后,设置好 mvp 矩阵后,绑定 m_vbo,使用 glDrawArrays() 执行绘制,它使用数组元素建立连续的几何图元序列。
最终,顶点的输出会送至片元着色器,这里直接将接收到的 color 作为最终的颜色输出( 它是 vec4,GLSL 是强类型的,vec3->vec4必须显式转换 ):
#version 330 core
in vec3 color;
out vec4 FragColor;
void main(void)
{
FragColor = vec4(color, 1.0f);
}
效果图如下:
这一章的内容实在太多了,我已经很尽力地讲清楚了,但就像之前说过的,这并非零基础的教程,所以很多细节没有讲到。。不过应该不影响。
然后系列代码地址:https://github.com/mengps/OpenGL-Totural/
最后,推荐两本书:《OpenGL编程指南》和《OpenGL超级宝典》。