opengl着色器的简单使用(一)

在前面的小结中,我并没有提到着色器(虽然我用到了它,并进行了粗略的解释)。那么在这里,我将专门讲讲着色器的相关知识。

着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

#version version_number       //版本
in type in_variable_name;     //输入
in type in_variable_name;     //输入

out type out_variable_name;   //输出

uniform type uniform_name;    //uniform

int main()                    //main函数
{
  // 处理输入并进行一些图形操作
  ...
  // 输出处理过的结果到输出变量
  out_variable_name = weird_stuff_we_processed;
}

 当我们特别谈论到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute)。我们能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

 和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:intfloatdoubleuintbool。GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix)

类型含义
vecn包含n个float分量的默认向量
bvecn包含n个bool分量的向量
ivecn包含n个int分量的向量
uvecn包含n个unsigned int分量的向量
dvecn包含n个double分量的向量

 

 

 

 

 

 

 

大多数时候我们使用vecn,因为float足够满足大多数要求了。

一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量。你可以分别使用.x.y.z.w来获取它们的第1、2、3、4个分量。GLSL也允许你对颜色使用rgba,或是对纹理坐标使用stpq访问相同的分量。

 向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。重组允许这样的语法:

vec2 someVec;//someVec只有2个分量  x和y
vec4 differentVec = someVec.xyxx;//differentVec有4个分量,由someVec中的x y x x组成。
vec3 anotherVec = differentVec.zyw;//anotherVec的3个分量有differentVec的zyw组成
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
//otherVec的4个分量分别是: someVec.x + anotherVec.y
//                        ...
//                        ...
//                        someVec.x + anotherVec.y

还可以这么赋值 

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了inout关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。

但在顶点和片段着色器中会有点不同。

顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。

另一个例外是片段着色器,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。

所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。为了展示这是如何工作的,我稍微改动一下之前画三角形教程里的那个着色器,让顶点着色器为片段着色器决定颜色。

 顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0

out vec4 vertexColor; // 为片段着色器指定一个颜色输出

void main()
{
    gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}

片段着色器

#version 330 core
out vec4 FragColor;

in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

void main()
{
    FragColor = vertexColor;
}

 全部代码

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>



void framebuffer_size_callback(GLFWwindow* window, int width, int height);


void processInput(GLFWwindow* window);


int main() {

	glfwInit();


	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);


	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	if (window == NULL) {
		std::cout << "打开窗口失败" << std::endl;
		glfwTerminate();
		return -1;
	}

	glfwMakeContextCurrent(window);

	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "初始化GLAD库失败" << std::endl;
		return -1;
	}

	//自定义顶点着色器
	const char* vertexShaderSource = "#version 330 core\n"
		"layout (location = 0) in vec3 aPos;\n"
		"out vec4 vertexColor;\n"
		"void main()\n"
		"{\n"
		"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
		"	vertexColor = vec4(0.5, 0.0, 0.0, 1.0);\n"
		"}\0";


	//创建一个着色器对象,注意还是用ID来引用的。所以我们储存这个顶点着色器为unsigned int
	//	 然后用glCreateShader创建这个着色器:
	unsigned int vertexShader;
	vertexShader = glCreateShader(GL_VERTEX_SHADER);
	//我们把需要创建的着色器类型以参数形式提供给glCreateShader。
	//	由于我们正在创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER。


	//下一步我们把这个着色器源码附加到着色器对象上,然后编译它:
	glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
	glCompileShader(vertexShader);
	//glShaderSource函数把要编译的着色器对象作为第一个参数。
	//	第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,
	//	第四个参数我们先设置为NULL。

	//检测glCompileShader编译是否成功了
	int  success;
	char infoLog[512];
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
	}


	//自定义片段着色器
	const char* fragmentShaderSource = "#version 330 core\n"
		"out vec4 FragColor;\n"
		"in vec4 vertexColor;\n"
		"void main()\n"
		"{\n"
		"   FragColor = vertexColor;\n"
		"}\n\0";

	//跟定点着色器一样,要创建对象,要绑定
	//	只不过我们使用GL_FRAGMENT_SHADER常量作为着色器类型:
	unsigned int fragmentShader;
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
	glCompileShader(fragmentShader);

	//检测glCompileShader编译是否成功了
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}


	//两个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中。
	//创建对象
	unsigned int shaderProgram;
	shaderProgram = glCreateProgram();
	//glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用。
	//	现在我们需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们:
	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);
	glLinkProgram(shaderProgram);

	//检测连接着色程序是否成功
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
	}

	//得到的结果就是一个程序对象,我们可以调用glUseProgram函数,
	//	用刚创建的程序对象作为它的参数,以激活这个程序对象:
	//	glUseProgram(shaderProgram);(现在还不需要激活它)

	//在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。
	//	对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:
	glDeleteShader(vertexShader);
	glDeleteShader(fragmentShader);



	//三角形
	float vertices[] = {
		-0.5f, -0.5f, 0.0f,//左下角
		 0.5f, -0.5f, 0.0f,//右下角
		 0.0f,  0.5f, 0.0f//顶角
	};//分别是x,y,z坐标

	unsigned int VBO, VAO;
	glGenVertexArrays(1, &VAO);//数组,装不用的绘制的对象
	glGenBuffers(1, &VBO);//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象:

	// 1. 绑定VAO
	glBindVertexArray(VAO);
	// 2. 把顶点数组复制到缓冲中供OpenGL使用
	//		OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。
	//		OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	//		从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
	//		然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	//GL_STATIC_DRAW :数据不会或几乎不会改变。
	//GL_DYNAMIC_DRAW:数据会被改变很多。
	//GL_STREAM_DRAW :数据每次绘制时都会改变。

//顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,
//	它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
//	所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	//glVertexAttribPointer函数的参数
	//	第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?
	//			它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
	//	第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
	//	第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
	//	第四个参数定义我们是否希望数据被标准化(Normalize)。
	//			如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
	//	第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。
	//			我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。
	//	最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。
	//			由于位置数据在数组的开头,所以这里是0。

	glBindBuffer(GL_ARRAY_BUFFER, 0);

	glBindVertexArray(0);



	while (!glfwWindowShouldClose(window)) {

		processInput(window);

		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		// 4. 绘制物体
		//OpenGL给我们提供了glDrawArrays函数,它使用当前激活的着色器,
		//	之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。
		glUseProgram(shaderProgram);
		glBindVertexArray(VAO);
		glDrawArrays(GL_TRIANGLES, 0, 3);
		/*glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。
		由于我们在一开始时说过,我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。
		第二个参数指定了顶点数组的起始索引,我们这里填0。
		最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。*/

		glfwSwapBuffers(window);
		glfwPollEvents();

	}

	//5.绘制结束,解放资源
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	glDeleteProgram(shaderProgram);


	glfwTerminate();


	return 0;
}


void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window) {
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值