现代OpenGL系列教程(一)---旋转的三角形

【写在前面】

本章主要内容:

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 的渲染管线,它是一系列数据处理的过程,并且将应用程序的数据转换到最终渲染的图像。

现代OpenGL系列教程(一)---旋转的三角形_第1张图片

 

其中,蓝色的为可编程着色器,其中顶点着色器和片元着色器没有默认实现,因此必须由我们自己实现。

着色器使用一种类似c语言的GLSL来编写,我们来看一下[创建->使用]一个着色器程序具体流程:

现代OpenGL系列教程(一)---旋转的三角形_第2张图片

 现在回过头来看我们的 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);
}

效果图如下:

现代OpenGL系列教程(一)---旋转的三角形_第3张图片


 【结语】

这一章的内容实在太多了,我已经很尽力地讲清楚了,但就像之前说过的,这并非零基础的教程,所以很多细节没有讲到。。不过应该不影响。

然后系列代码地址:https://github.com/mengps/OpenGL-Totural/

最后,推荐两本书:《OpenGL编程指南》和《OpenGL超级宝典》。

你可能感兴趣的:(现代,OpenGL,开发之旅,GLSL)