OpenGL学习 02 第一个三角形

三角形示例代码,补全了LoadShader等部分(使用vs2015 控制台程序)。

#include "stdafx.h"
#include <iostream>
#include <vector>
using namespace std;

#include <GL/glew.h>
#include <GL/glut.h>
#include <string>       
#include <vector>
#include <iostream>
#include <fstream>

#pragma lib("glu32.lib")
#pragma lib("glew32.lib")

enum VAO_IDs{Triangles, NumVAOs};
enum Buffer_IDs{ArrayBuffer, Numbuffers};
enum Attrib_IDs{vPosition = 0};

GLuint VAOs[NumVAOs];
GLuint Buffers[Numbuffers];

const GLuint NumVertices = 6;

struct ShaderInfo
{
	GLenum shaderType;
	const char* filePath;
	ShaderInfo(GLenum type, const char* path)
		:shaderType(type), filePath(path) {}
};

/*
* 读取着色器程序源码
*/
bool loadShaderSource(const char* filePath, std::string& source)
{
	source.clear();
	std::ifstream in_stream(filePath);
	if (!in_stream)
	{
		return false;
	}
	source.assign(std::istreambuf_iterator<char>(in_stream),
		std::istreambuf_iterator<char>()); // 文件流迭代器构造字符串
	return true;
}

/*
* 从文件加载顶点和片元着色器
* 传递参数为 [(着色器文件类型,着色器文件路径)+]
*/
GLuint LoadShaders(ShaderInfo shaderFileVec[], size_t shaderCount)
{
	GLuint programId;
	std::vector<GLuint> shaderObjectIdVec;
	std::string vertexSource, fragSource;
	std::vector<std::string> sourceVec;
	
	// 读取文件源代码
	for (size_t i = 0; i < shaderCount; ++i)
	{
		std::string shaderSource;
		if (!loadShaderSource(shaderFileVec[i].filePath, shaderSource))
		{
			std::cout << "Error::Shader could not load file:" << shaderFileVec[i].filePath << std::endl;
			return 0;
		}
		sourceVec.push_back(shaderSource);
	}
	bool bSuccess = true;
	// 编译shader object
	for (size_t i = 0; i < shaderCount; ++i)
	{
		GLuint shaderId = glCreateShader(shaderFileVec[i].shaderType);
		const char *c_str = sourceVec[i].c_str();
		glShaderSource(shaderId, 1, &c_str, NULL);
		glCompileShader(shaderId);
		GLint compileStatus = 0;
		glGetShaderiv(shaderId, GL_COMPILE_STATUS, &compileStatus); // 检查编译状态
		if (compileStatus == GL_FALSE) // 获取错误报告
		{
			GLint maxLength = 0;
			glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &maxLength);
			std::vector<GLchar> errLog(maxLength);
			glGetShaderInfoLog(shaderId, maxLength, &maxLength, &errLog[0]);
			std::cout << "Error::Shader file [" << shaderFileVec[i].filePath << " ] compiled failed,"
				<< &errLog[0] << std::endl;
			bSuccess = false;
		}
		shaderObjectIdVec.push_back(shaderId);
	}
	// 链接shader program
	if (bSuccess)
	{
		programId = glCreateProgram();
		for (size_t i = 0; i < shaderCount; ++i)
		{
			glAttachShader(programId, shaderObjectIdVec[i]);
		}
		glLinkProgram(programId);
		GLint linkStatus;
		glGetProgramiv(programId, GL_LINK_STATUS, &linkStatus);
		if (linkStatus == GL_FALSE)
		{
			GLint maxLength = 0;
			glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &maxLength);
			std::vector<GLchar> errLog(maxLength);
			glGetProgramInfoLog(programId, maxLength, &maxLength, &errLog[0]);
			std::cout << "Error::shader link failed," << &errLog[0] << std::endl;
		}
	}
	// 链接完成后detach 并释放shader object
	for (size_t i = 0; i < shaderCount; ++i)
	{
		if (programId != 0)
		{
			glDetachShader(programId, shaderObjectIdVec[i]);
		}
		glDeleteShader(shaderObjectIdVec[i]);
	}
	return programId;
}

void init(void)
{
	glGenVertexArrays(NumVAOs, VAOs);
	glBindVertexArray(VAOs[Triangles]);
	GLfloat vertices[NumVertices][2] = {
		{-0.90, -0.90},//Triangle 1
		{ 0.85, -0.90},
		{-0.90,  0.85},
		{ 0.90, -0.85}, //Triangle
		{ 0.90,  0.90},
		{-0.85,  0.90}
	};
	glGenBuffers(Numbuffers, Buffers);
	glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	ShaderInfo shaders[] = {
		{GL_VERTEX_SHADER, "triangle.vert"},
		{GL_FRAGMENT_SHADER, "triangle.frag"},
		{GL_NONE, NULL}
	};
	GLuint program = LoadShaders(shaders, 2);
	glUseProgram(program);
	glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(vPosition);
}

void display()
{
	glClear(GL_COLOR_BUFFER_BIT);
	glBindVertexArray(VAOs[Triangles]);
	glDrawArrays(GL_TRIANGLES, 0, NumVertices);
	glFlush();
}
int main(int argc, char** argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA);
	glutInitWindowSize(512, 512);
	//glutInitContextVersion(4, 3);  //这两个函数找不到定义,注释掉没有问题
	//glutInitContextProfile(GLUT_CORE_PROFILE);
	glutCreateWindow(argv[0]);

	if (glewInit()) {
		cerr << "Unable to initialize GLEW ... exiting" << endl; exit(EXIT_FAILURE);
	}
	init();

	glutDisplayFunc(display);

	glutMainLoop();
    return 0;
}
(注意设置glut glew的路径和lib 以及dll)运行结果如下:

完整代码下载:点击打开链接

OpenGL语法:

OpenGL中的函数都会以字符“gl”作为前缀。例如glBindVertexArray()

有的以“glut”开头,来自于第三方库“OpenGL Utility Toolkit”(GLUT)。 

函数glewInit()来自于OpenGL Extension Wrangler。

OpenGL库中定义的常量也是GL_COLOR_BUFFER_BIT的形式,以GL_作为前缀。

由于OpenGL是一个“C”语言形式的库,因此他不能使用函数重载来处理不同各类型的数据,此时它使用函数名称的细微变化来管理实现同一类功能的函数集。例如glUniform*()它有多种变化形式,例如glUniform2f()和glUniform3fv()。2代表参数值,f代表GLfloat, V代表 vector。常见的类型如下表:

代码讲解:main()函数:

glutInit()负责初始化GLUT库,他必须是应用程序调用的第一个GLUT函数,它会负责设置其他GLUT例程所必需的数据结构。

glutInitDisplayMode()设置了程序所用的窗口的类型。

glutInitWindowSize()设置了所需的窗口的大小。

glutInitContextVersion()和glutInitContextProfile()设置了我们所需要的OpenGL环境(context)——这是OpenGL内部用于记录状态设置和操作的数据结构。

如果当前系统环境可以满足glutInitDisplayMode()的显示模式要求,使用glutCreateWindow()会创建一个窗口(此时会调用计算机窗口系统的接口),只有在GLUT创建了一个窗口之后(其中也包含创建OpenGL环境的过程),我们才可以使用OpenGL相关的函数。

glewInit()是GLEW(OpenGL Extension Wrangler)。GLEW可以简化获取函数地址的过程,并且包含了可以跨平台使用的其他一些OpenGL编程方法。如果没有GLEW,我们可能还需要执行相当多的工作才能够运行程序。

glutDisplayFunc()他设置了显示回调(display callback),即GLUT在每次更新窗口内容的时候会自动调用的例程。这里传入display()这个函数的地址。GLUT可以使用一系列回调函数来处理诸如用户输入,重设窗口尺寸等不同操作。

glutMainLoop()是一个无限执行的循环,它会负责一直处理窗口和操作系统的用户输入等操作。gltuMainLoop()会判断窗口是否需要重新绘制,然后就会调用glutDisplayFunc()中注册的函数。特别需要注意的是,glutMainLoop()是一个无限循环,因此不会执行在它之后的所有命令。


init()函数:

初始化顶点数组对象(Vertex-Array Object  VAO)

使用glGenVertexArrays()分配顶点数组对象。

void glGenVertexArrays(GLsizei n, GLuint* arrays); 返回n个未使用的对象到数组arrays中,用作定点数组对象。

OpenGL中使用glGen*来分配不同类型的OpenGL对象的名称。这名称类似C语言中的一个指针变量,我们必须分配内存并且用名称引用它之后,名称才有意义。在OpenGL中,这个分配的机制叫做绑定对象(bind an object 也就是把这个对象名称绑定到分配的对象内存),它是通过一些列glBind*的函数集合去实现的。在例子中,我们是通过glBindVertexArray()函数创建并且绑定了一个顶点数组对象。

void glBindVertexArray(GLuint array); 如果array非0且是glGenVertexArrays()所返回的,那么它将创建一个新的顶点数组对象并且与其名称关联起来。如果array已经创建,则激活这个顶点数组对象。如果array为0,那么OpenGL将不再使用程序所分配的任何顶点数组对象,并且讲渲染状态重设为顶点数组的默认值。如果array不是glGenVertexArrays()返回的数值,或者已经被glDeleteVertexArrays()函数释放了,那么将会产生一个GL_INVALID_OPERATION错误。

当我们第一次绑定对象时,OpenGL内部会分配这个对象所需的内存并且将它作为当前对象,即所有后继的操作都会作用于这个被绑定的对象。在第一次调用glBind*()函数之后,新创建的对象都会初始化为其默认状态,而我们通常需要一些额外的初始化工作来确保这个对象可用。绑定对象的过程有点类似设置铁路的岔道开关,一旦设置了开关,从这条线路通过的所有列车都会驶向对应的轨道。如果我们将开关设置到另一个状态,那么所有之后经过的列车都会驶向另一条轨道。总体上来说:两种情况下我们需要绑定一个对象。(1)创建对象并初始化它对应的数据时;(2)每次准备使用这个对象时,而它并不是当前绑定的对象时。

当我们完成对顶点数据对象的操作之后,可以调用glDeleteVertexArrays()将它进行释放。

void glDeleteVertexArrays(GLsizei n, GLuint *arrays);删除n个在arrays中定义的顶点数组对象,这样所有的名称可以再次用作顶点数组。如果绑定的顶点数组已经被删除,那么当前绑定的顶点数组对象被重设为0(类似执行了glBindBuffer()函数,并且输入参数为0),而默认的顶点数组会变成当前对象。在arrays当中未使用的名称都会被释放,但是当前顶点数组的状态不会发生任何变化。

最后为了保证程序的完整性,我们可以调用gllsVertexArray()检查某个名称是否已经被保留为一个顶点数组对象了。

GLboolean gllsVertexArray(GLuint array);如果array是一个用glGenVertexArrays()创建并且没有被删除的VAO名称,那么返回GL_TRUE,如果array为0或者不是任何VAO的名称,那么返回GL_FALSE。

对于OpenGL中其他的类型,都有glDelete*和glls*的函数。


分配顶点缓存对象(Vertex Buffer Objects)

    VAO负责保存一系列的顶点的数据,这些数据保存到缓存对象当中,并且由当前绑定的VAO管理。我们只有一种顶点数组对象类型,但是却有很多种类型的对象,并且其中一部分对象并不负责处理顶点数据。(缓存对象就是OpenGL服务端分配和管理的一块内存区域,并且几乎所有传入OpenGL的数据都是存储在缓存对象当中的)

    VBO的初始化过程与VAO类似,不过需要有向缓存中添加数据的一个过程。

    void glGenBuffers(GLsizei n, GLuint* buffers);返回n个当前未使用的缓存对象名称,并保存到buffers数组中。返回到buffers中的名称不一定是连续的整型数据。这里返回的名称只用于分配其他缓存对象,它们在绑定之后只会记录一个可用的状态。0是一个保留的缓存对象名称。之后可以用glBindBuffer()来绑定它们了,由于OpenGL中有很多种不同类型的缓存对象,因此在绑定时,需要指定对应的类型。如GL_ARRAY_BUFFER类型。

    void glBindBuffer(GLenum target, GLuint buffer);指定当前激活的缓存对象,target必须设置为一下类型中的一个:(GL_*_BUFFER 总共有8种: ARRAY ELEMENT_ARRAY PIXEL_PACK PIXEL_UNPACK COPY_READ COPY_WRITE TRANSFORM_FEEDBACK UNIFORM )。 glBindBuffer()完成了三项工作:(1)如果是第一次绑定buffer,buffer不等于0,那么将创建一个与该名称相对应的新缓存对象;(2)如果buffer已经创建,那么它将成为当前被激活的缓存对象。(3)如果buffer=0,那么OpenGL将不再对当前target应用任何缓存对象。

    所有的缓存对象都可以用glDeleteBuffers()直接释放掉。 

    void glDeleteBuffers(GLsizei n, const GLuint * buffers); 删除n个保存在buffers数组中的缓存对象。被释放的缓存对象可以重用(使用glGenBuffers())。我们可以用gllsBuffer()来判断一个整数值是否是一个缓存对象名称。

将数据载入缓存对象

    glBufferData():它主要有两个任务:分配顶点数据所需的存储空间,然后将数据从应用程序的数组中拷贝到OpenGL服务端的内存中。

    void glBufferData(GLenum target, GLsizeptr size, const GLvoid* data, GLenum ussage);在OpenGL服务端内存中分配size个存储单元(通常为byte),用于存储数据或者索引。如果当前绑定的对象已经存在了关联的数据,那么会首先删除这些数据。

    target的取值对应的含义分别是:顶点属性数据 ARRAY     索引数据ELEMENT_ARRAY     从OpenGL中获取的像素数据PIXEL_PACK  OpenGL的像素数据PIXEL_UNPACK 缓存之间的复制数据COPY_READ和COPY_WRITE  通过transform feedback着色器获得的结果TRANSFORM_FEEDBACK 一致变量UNIFORM  纹理缓存中存储的纹理数据TEXTURE)。

    size表示存储数据的总数量。这个数值等于data中存储的元素的总数乘以单位元素存储空间的结果。

    data要么是一个客户端内存的指针,以便初始化缓存对象,要么是NULL。如果传入的指针合法,那么将会有size大小的数据从客户端拷贝到服务端。如果传入NULL,那么将保留size大小的未初始化数据,以备后用。

    usage用于设置分配数据之后的读写和写入方式。可用的方式包括: GL_STREAM_DRAW STREAM_READ STREAM_COPY STATIC_DRAW STATIC_READ STATIC_COPY DYNAMIC_DRAW DYNAMIC_READ DYNAMIC_COPY( stream static dynamic 各三种 draw read copy  )

    如果所需的size大小超过了服务器能够分配的额度,那么glBufferData()将产生一个GL_OUT_OF_MEMORY错误。如果usage设置的不是可用的模式值,那么将产生GL_INVALID_VALUE。

    在本文的例子中,因为顶点数据就保存在一个vertices数组当中。如果需要静态地从程序中加载顶点数据,那么而我们可能需要从模型文件中读取这些数据,或者通过某些算法来生成。由于我们的数据是顶点属性数据,因此设置这个缓存为GL_ARRAY_BUFFER类型,用sizeof(vertices)来计算大小,最后因为我们只是用它来绘制几何体,不会再运行时对它做出修改,所以设置usage为GL_STATIC_DRAW。

    仔细观察vertices数组中的值,可以发现x,y方向都被限定在[-1,1]的范围内。实际上OpenGL只能够绘制坐标空间内的几何体图元。而具有该范围限制的坐标系也成为规格化设备坐标系统(Normalized Device Coordinate NDC)

    初始化顶点与片元着色器

    对于每一个OpenGL程序,当它所使用的OpenGL版本高于或等于3.1时,都需要指定至少两个着色器:顶点着色器和片元着色器。对于OpenGL程序员而言,着色器就是使用OpenGL着色语言(OpenGL Shading Language,GLSL)编写的一个小型函数。GLSL是构成所有OpenGL着色器的语言,它与C++语言非常类似,尽管GLSL中的所有特性并不能用于OpenGL的每个着色阶段。

    triangles.vert内容如下:

#version 430 core  
layout(location = 0) in vec4 vPosition;  
void  
main()  
 {  
     gl_Position = vPosition;  
}  

    事实上,这就是我们之前所说的传递着色器(pass-through shader)的例子。它只负责将数据拷贝到输出数据中。

    “#version 430 core ”指定了我们所用的OpenGL着色语言的版本4.3。 core表示使用OpenGL核心模式(core profile),这与之前GLUT的函数glInitContextProfile()设置的内容应当一致。每个着色器的第一行都应该设置“#version”,否则系统会假设使用“110”版本。但是这与OpenGL核心模式并不兼容。

    下一步,我们申明了一个着色器变量 vPosition。着色器变量是着色器与外部世界的联系所在。也就是说,着色器并不知道自己的数据从哪里来,它只是在每次运行时,直接获取数据对应的输入变量。而我们必须自己完成着色管线的装配(在外部程序中设置变量对应的属性值),然后才可以将应用程序的数据与不同OpenGL着色阶段相互关联

    vPosition保存的是顶点的位置信息。 vec4是vPosition的类型。它代表GLSL的四维浮点数向量。在程序中我们只使用了两个坐标值来代表一个点,OpenGL会用默认数值自动填充这些缺失的坐标值。vec4的默认值为(0,0,0,1),因此当仅仅指定了x和y的坐标的时候,其他两个坐标值(z和w)将被自动指定为0和1。 in指定了数据进入着色器的流向,(在其他地方可以看到out)。layout(location=0) 叫做布局限定符(layout qualifier),目的是为变量提供元数据(meta data)。我们可以用限定符来设置很多不同的属性,其中有些是与不同的着色阶段相关的。这里vPosition的位置属性location为0,这个设置与init()函数的最后两行会共同起作用。(enum Attrib_IDs{vPosition = 0}; 设置了位置为0,glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, 0);是指定0对应的数据指针,glEnableVertexAttribArray(vPosition);开启属性。)

    最后,在着色器main()函数中实现它的主体部分。OpenGL的所有着色器,无论是处理哪个着色阶段,都会有一个main()函数。对于这个着色器而言,他所实现的就是将输入的顶点位置复制到顶点着色器的指定输出位置gl_position中。

    triangles.frag:

#version 430 core  
out vec4 fColor;  
void  
main()  
{  
fColor = vec4(0.0, 0.0, 1.0, 1.0);  
}  

    这个代码与上述的很类似。重点内容如下:声明的变量名为fcolor。使用了out限定符!在这里,着色器会把fcolor对应的数值输出,而这也就是片元所对应的颜色值。每个片元都会设置一个四维的向量。OpenGL中的颜色是通过RGBA颜色空间来表示的。每个分量的范围都是[0,1]。alpha的值被设置为1.0代表颜色是完全不透明的。

    为了输入顶点着色器的数据,也就是OpenGL将要处理的所有顶点数据,需要在着色器中申明一个in变量,然后使用glVertexAttribPointer()将它关联到一个顶点属性数组。

    void glVertexAttribPointer(GLunit index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* pointer);

    设置index(着色器中的属性位置,即location的值)位置对应的数据值。pointer表示缓存对象中(即当前bind的VBO对象),从起始位置开始计算的数组数据的偏移值(假设起始位置地址为0),使用基本的系统单位(byte)。size表示每个顶点需要更新的分量数目,可以是(1、2、3、4)或者GL_BGRA(本例中每个顶点只使用了xy表示,即2个数据)。type指定了数组中每个元素的数据类型(GL_* : BYTE、UNSIGNED_BYTE、SHORT、UNSIGNED_SHORT、INT、UNSIGNED_INT、FIXED、HALF_FLOAT、FLOAT 、DOUBLE)。normalized设置顶点数据在缓存前是否需要进行归一化(或者使用glVertexAttribFourN*()函数)。stride是数组中每两个元素之间的大小偏移值(byte)。如果stride为0,那么数据应该紧密地封装在一起(即两个相邻数据之间没有其他数据)。
    #define BUFFER_OFFSET(offset) ((void*)offset) 在以往版本的OpenGL当中并不需要用到这个宏,不过现在我们希望使用它来设置数据在缓存对象中的偏移量,而不是像glVertexAttribPointer()原型那样直接设置一个指向内存块的指针。

    最后我们通过glEnableVertexAttribArray()来启用顶点属性数组。 

    void glEnableVertexAttribArray(GLuint index)   void glDisableVertexAttribArray(GLuint index)设置是否启用与index索引相关联的顶点数组。index必须介于0到GL_MAX_VERTEX_ATTRIBS-1之间。

    渲染

    display()代码:

void display()
{
	glClear(GL_COLOR_BUFFER_BIT);
	glBindVertexArray(VAOs[Triangles]);
	glDrawArrays(GL_TRIANGLES, 0, NumVertices);
	glFlush();
}

    首先我们要清除帧缓存的数据,在进行渲染。清除的工作有glClear()完成。

    void glClear(GLbitfield mask); mask是一个可以通过逻辑“或”操作来指定多个数值的参数。 颜色缓存:GL_COLOR_BUFFER_BIT 深度缓存:GL_DEPTH_BUFFER_BIT 模板缓存:GL_STENCIL_BUFFER_BIT。glClear()默认使用黑色清除数值。如果要改变清除颜色的数值,可以使用glClearColor();

    void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);设置当前使用的清除颜色值,用于RGBA模式下对颜色缓存的清除工作。这里的red green blue alpha都会被截断到[0,1]的范围内。默认的清除颜色是(0,0,0,0),在RGBA模式下它表示黑色。清除颜色本身也是OPenGL状态机制的一个例子,它的数值会一直保留在当前OpenGL环境当中。OpenGL有一个庞大的状态量列表,当创建一个新的OpenGL环境时,所有的状态量都会被初始化为默认数值。

    绘制

    首先调用glBindVertexArray()来选择作为顶点数据使用的顶点数据,其次调用glDrawArrays()来实现顶点数据向OpenGL管线的传输

    void glDrawArrays(GLenum mode, GLiint first, GLsizei count);使用当前绑定的顶点数组元素来建立一系列的几何图元,起始位置为first,而结束位置为first+count-1。mode设置了构建图元的类型,它可以是:GL_POINTS、GL_LINES GL_LINE_STRIP gL_LINE_LOOP GL_TRIANGLES GL_TRIANGLE_STRIP GL_TRIANGLE_FAN 和GL_PATCHES(不会输出任何结果,用于细分着色器)中的任意一种。

    最后调用glFlush(),即强制所有进行中的OpenGL命令立即完成并传输到OpenGL服务端处理。

    void glFlush(void); 强制之前的OpenGL命令立即执行,这样就可以保证它们在一定时间内全部完成。glFlush()只是强制所有运行中的命令送入OpenGL服务端而已,并且它会立即返回——它并不会等待所有的命令完成,而等待却是我们所需要的。为此我们需要使用glFinish()命令,它会一直等待所有当前的OpenGL操作完成后,再返回。(你最好只是在开发阶段使用glFinish(),虽然它对于判断OpenGL命令运行效率很有帮助,但是对于程序的整体性能却有着相当的拖累。)

    启用和禁用OpenGL的操作

   void glEnable(GLenum capability); void glDisable(GLenum capability); 开启或者关闭一个模式。例如:深度测试 GL_DEPTH_TEST 控制融合 GL_BLEND  transform feedback过程中的高级渲染控制 GL_RASTERIZER_DISCARD。根据自己的需要来判断是否开启某个特性,可以使用gllsEnabled()来返回是否启用指定模式的信息。

猜你喜欢

转载自blog.csdn.net/u200814342A/article/details/79356263