OpenGL学习笔记(二)-着色器-纹理

参考网址:LearnOpenGL 中文版
哔哩哔哩教程

第一章 入门

1.3 着色器

1.3.1 基本结构

利用着色器语言编写着色器,以顶点着色器和片段着色器为例,在着在顶点着色器中输出颜色变量vertexColor,在片段着色器中输入变量vertexColor作为图案的颜色。

#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;
}

1、着色器开始要声明版本:#version 330 core

2、变量

  • 变量声明形式为in type in_variable_name,输入和输出变量:in vec4 vertexColor out vec4 vertexColor,uniform变量:uniform vec4 ourColor

  • 数据类型vecn表示包含n个float值的向量(n=2,3,4),可以通过x,y,z,w取得分量值,同时也可以对变量进行重组;

    vec2 someVec;
    vec4 diffVec=someVec.xyxx;
    vec2 vect=vec2(0.5,0.7);
    vec4 result=(vect,0.0,0.0);
    

3、输入与输出

  • 每个着色器都需要有输入和输出,利用关键字inout
  • 由于顶点着色器要从VAO中接受顶点数据,因此需要利用layout(location=0)来指定从哪个顶点属性编号中读取数据。并且顶点着色器有固定的输出gl_Position,无需定义。
  • 片段着色器中必须输出一个vec4颜色变量,定义渲染的颜色,名字自定义。
  • 为实现从一个着色器向另一个着色器发送数据,可在一个着色器中声明一个输出,另一个着色器阶段声明一个名字相同的输入变量,变量就会传递下去。

1.3.2 uniform

1、uniform关键字的变量是全局的,可以在任意的着色器中使用,该变量数据直接来源于CPU,与顶点数据不同,不经过VBO,VAO。

2、 在片段着色器中定义uniform型的颜色变量ourcolor,利用CPU中的数据给它赋值。

3、为对变量赋值,首先需要找到着色器中uniform属性变量的位置,然后让颜色值随着时间改变,就可得颜色渐变的图案。

#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
    FragColor = ourColor;
}
float timeValue = glfwGetTime();//获取时间数据
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;//改变值的取值范围
int vertexColorLoaction = glGetUniformLocation(shaderProgram, "ourColor");//获取变量的位置
glUseProgram(shaderProgram);//为更新变量,首先需要启用着色器程序
glUniform4f(vertexColorLoaction, 0.0f, greenValue, 0.0f, 1.0f);//改变uniform变量的值
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

1.3.3 传递颜色数据

1、在上述中顶点数据只包含了位置信息,现增加顶点的颜色信息进行传递。

float vertices[] = {
	-0.5f, -0.5f, 0.0f,	1.0f,0,0,
	0.5f, -0.5f, 0.0f,0,1.0f,0,
	0.0f,  0.5f, 0.0f,0,0,1.0f,
	0.8f,  0.8f, 0.0f,1.0f,0,1.0f
};

2、数据变化后,VBO中的顶点数据变化为:
OpenGL学习笔记(二)-着色器-纹理_第1张图片
3、顶点数据属性指针也需发生变化,分别读取位置属性和颜色属性,另外步长变化为6 * sizeof(float),颜色数据的偏移量为3 * sizeof(float)

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

4、虽然我们只提供了顶点的颜色数据,但是在片段着色器中会进行片段插值(Fragment Interpolation)。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置,插值(Interpolate)所有片段着色器的输入变量。
OpenGL学习笔记(二)-着色器-纹理_第2张图片
1.3.4 着色器类

1、本节目的在于将着色器源代码写到txt文件中,然后构建着色器类,利用类从硬盘中读取着色器代码,编译并链接着色器,最后生成着色器程序对象。 源代码的读取流程为:
OpenGL学习笔记(二)-着色器-纹理_第3张图片
2、定义类的构造函数,传入参数为文件路径:

Shader::Shader(const char* vertexPath, const char* fragmentPath)//构造函数
void use();//使用着色器程序

std::string vertexString;//着色器源代码字符串
std::string fragmentString;
const char* vertexSource;//着色器源代码字符数组
const char* fragmentSource;
unsigned int ID;//着色器程序

3、读入代码、链接、编译、生成程序

Shader::Shader(const char* vertexPath, const char* fragmentPath)
{
	ifstream vertexFile, fragmentFile;
	stringstream vertexSStream, fragmentSStream;
	
	vertexFile.open(vertexPath);//Open表示该程序在使用该文件,但还没进行文件拷贝
	fragmentFile.open(fragmentPath);
	
	vertexFile.exceptions(ifstream::failbit || ifstream::badbit);//逻辑上打开失败或者文件损坏
	fragmentFile.exceptions(ifstream::failbit || ifstream::badbit);
	
	try
	{
		if (!vertexFile.is_open() || !fragmentFile.is_open())
		{
			throw exception("open file fail");//打开失败
		}
	
		/*读取着色器源代码,将文件内容读取到内存中的stringstream中*/
		vertexSStream << vertexFile.rdbuf();
		fragmentSStream << fragmentFile.rdbuf();
		//将内容转到string
		vertexString = vertexSStream.str();
		fragmentString = fragmentSStream.str();
		//将内容转成char*
		vertexSource = vertexString.c_str();
		fragmentSource = fragmentString.c_str();
	
		/*编译着色器源代码*/
		unsigned int vertex, fragment;
		
		vertex = glCreateShader(GL_VERTEX_SHADER);
		glShaderSource(vertex, 1, &vertexSource,NULL);
		glCompileShader(vertex);
		
		fragment = glCreateShader(GL_FRAGMENT_SHADER);
		glShaderSource(fragment, 1, &fragmentSource, NULL);
		glCompileShader(fragment);
	
		/*产生着色器程序*/
		ID = glCreateProgram();
		glAttachShader(ID, vertex);
		glAttachShader(ID, fragment);
		glLinkProgram(ID);
	
		/*程序产生后,着色器对象就可以删除了*/
		glDeleteShader(vertex);
		glDeleteShader(fragment);
	}
	catch (const std::exception& ex)
	{
		printf(ex.what());
	}
	{
		
}

4、运行着色器程序

void Shader::use()
{
	glUseProgram(ID);
}

5、检查着色器编译、链接是否有错

//错误检查程序
void Shader::checkCompileErrors(unsigned int ID, std::string type)
{
	int success;
	char infoLog[512];

	if (type != "PROGRAM")//检查着色器对象错误
	{
		glGetShaderiv(ID, GL_COMPILE_STATUS, &success);//获取着色器编译状态,将成功与否写入success
		if (!success)
		{
			glGetShaderInfoLog(ID, 512, NULL, infoLog);//不成功,写入错误日志
			cout << "shader compile error:" << infoLog << endl;
		}
	}
	else//检查着色器程序错误
	{
		glGetProgramiv(ID, GL_LINK_STATUS, &success);//获取着色器程序链接状态,将成功与否写入success
		if (!success)
		{
			glGetProgramInfoLog(ID, 512, NULL, infoLog);//不成功,写入错误日志
			cout << "program linking error:" << infoLog << endl;
		}
	}
}

//函数使用
checkCompileErrors(vertex, "VERTEX");
checkCompileErrors(fragment, "FRAGMENT");
checkCompileErrors(ID, "PROGRAM");

6、设置uniform变量函数

void Shader::setFloat(const std::string &uniform_name, float value)const
{
	glUniform1f(glGetUniformLocation(ID, uniform_name.c_str()), value);//找到着色器程序里program的位置,然后赋值
}

7、练习:通过uniform变量使得位置发生偏移

/***顶点着色器中增加uniform***/
#version 330 core									
layout(location = 0) in vec3 aPos;					
layout(location = 1) in vec3 aColor;				
out vec3 vertexColor;
uniform  float x0offset;						
void main()											
{													
	gl_Position = vec4(aPos.x+x0offset, aPos.y, aPos.z, 1.0);
	vertexColor=aColor;								
}

/***渲染时调用uniform设置函数***/
myShader->setFloat("x0offset", 0.1f);
myShader->use();
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

OpenGL学习笔记(二)-着色器-纹理_第4张图片

1.4 纹理

1.4.1 图像库

1、stb_image.h为单头文件图像库,下载地址,只需将头文件加入到项目中,使用以下代码包含:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

图像数据的获取与释放:

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);

stbi_image_free(data);

1.4.2 纹理介绍

1、 为了使得图形更加真实,需要增加足够多的顶点,并且顶点必须包含颜色信息,导致计算量剧增。因此,可使用纹理来增加物体的细节,纹理为一般为2D图像。

2、为增加纹理,顶点数据中必须包含纹理坐标,指明每个顶点从纹理图像的哪部分采样,然后进行片段插值。纹理坐标的原点为图像左下角,取值范围为(0,1)。
OpenGL学习笔记(二)-着色器-纹理_第5张图片
3 、纹理环绕方式(Wrapping):纹理坐标如果超出(0,1)的范围,OpenGL会重复该纹理图像,设定对超出部分的颜色,。

  • OpenGL中有四种不同的环绕方式,分别为:GL_REPEAT 重复纹理图像。GL_MIRRORED_REPEAT 镜像重复纹理图像。GL_CLAMP_TO_EDGE 超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。

OpenGL学习笔记(二)-着色器-纹理_第6张图片

  • 通过glTexParameter*函数对每个坐标轴方向上的重复方式进行定义,参数分别为:指定纹理目标为2D纹理;指定设置选项为wrap,方向为s/t;指定环绕方式。
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_S,GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_S,GL_MIRRORED_REPEAT);
  • 如果指定环绕方式为自行定义边缘颜色,需要利用不同的函数,传递一个float数组来作为边缘的颜色值
float borderColor[]={1.0f,1.0f,0.0f,1.0f};
glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_BODER_COLOR,borderColor);

4、纹理过滤:顶点数据中包含了纹理坐标,OpenGL通过纹理坐标去查找纹理图像上的像素(纹理像素),然后进行采样提取纹理像素的颜色。

  • 纹理过滤的方式有:GL_NEAREST,邻近过滤,图中加号代表纹理坐标,选择距离纹理坐标最近的像素点,其颜色作为样本颜色。GL_LINEAR通过对邻近像素的线性插值,计算样本颜色

OpenGL学习笔记(二)-着色器-纹理_第7张图片OpenGL学习笔记(二)-着色器-纹理_第8张图片

  • 如果对一个很大的物体应用分辨率很低的纹理,那么纹理就需要被放大,利用不同的过滤方式进行计算,GL_NEAREST产生了颗粒状的图案,能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。
    OpenGL学习笔记(二)-着色器-纹理_第9张图片
  • 当进行放大(Magnify)和缩小(Minify)操作的时候,需要使用glTexParameter*函数指定过滤方式,与纹理环绕方式的设置相似:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

5、多级渐远纹理:假设场景中有些物体会很远,但其纹理与近处物体的分辨率同样高。由于远处的物体可能只产生很少的片段,从高分辨率纹理中为这些片段获取正确的颜色值,需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色,导致效果差、内存浪费。

  • OpenGL利用多级渐远纹理(Mipmap),产生一系列的纹理图像,后一个纹理图像是前一个的二分之一。当观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。
    OpenGL学习笔记(二)-着色器-纹理_第10张图片
  • 利用glGenerateMipmaps可以产生多级渐远纹理,在不同级别的纹理层之间,也需要利用NEARESTLINEAR进行过滤。使用glTexParameteri进行设置,可以同时设定纹理过滤方式和纹理级别间的过滤方式。
    GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
    GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
    GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
    GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
glParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MINMAP_LINEAR);
glParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

多级渐远纹理是在纹理缩小时需要使用的,所以在放大时无需设置多级渐远纹理的过滤方式。

1.4.3 应用纹理

纹理的应用流程为:生成纹理对象;将图片数据放到纹理对象中;将纹理对象绑定到对应的纹理单元中;VBO中设置纹理坐标的属性;顶点着色器中读取纹理坐标;将对应纹理单元与采样器绑定;片段着色器中利用采样器从对应纹理单元中访问纹理对象。
OpenGL学习笔记(二)-着色器-纹理_第11张图片

1、创建两个纹理对象TextureA和TextureB:与VAO等OpenGL对象一样,纹理对象也是使用ID引用来创建对象。利用glGenTextures函数,第一个参数为生成纹理的数量,第二个参数为指向纹理对象的指针(如果纹理对象是数组,那么就是数组名,如果纹理对象只有一个,需加取地址符&)。

unsigned int TextureA;
glGenTextures(1, &TextureA);//纹理数量,纹理对象指针
unsigned int TextureB;
glGenTextures(1, &TextureB);//纹理数量,纹理对象指针

2、 生成纹理对象后,需要将纹理对象放在指定的纹理单元中,此时TextureA使用纹理单元为1,先激活纹理单元1然后绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理中。

glActiveTexture(GL_TEXTURE1);//激活纹理单元1
glBindTexture(GL_TEXTURE_2D, TextureA);//绑定当前纹理对象到上下文中

3、利用之前的图片生成纹理,glTexImage2D函数的参数为:纹理目标;多级渐远纹理级别;纹理储存格式;纹理宽度;纹理高度;图片格式;图片数据类型;图像数据。
利用glGencreateMipmap生成多级渐远纹理。

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);//将图像复制到纹理中
glGenerateMipmap(GL_TEXTURE_2D);//生成多级渐远纹理

4、设置顶点属性指针:为了着色器能够进行纹理采样,在顶点数据中必须包含纹理坐标:

float vertices[] = {
//     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
     0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上
     0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
    -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
};
  • 顶点数据变化后,顶点属性读取方式也需改变
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);//顶点坐标
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);//颜色
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);//纹理坐标

5、顶点着色器中需增加一个顶点属性,接受纹理坐标

#version 330 core									
layout(location = 0) in vec3 aPos;					
layout(location = 1) in vec3 aColor;	
layout(location = 2) in vec2 aTexCoord;		
	
out vec3 vertexColor;
out vec2 TexCoord;
					
void main()											
{													
	gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
	vertexColor=aColor;		
	TexCoord=aTexCoord;						
}

6、片段着色器输入纹理坐标作为采样点,并且需要访问纹理对象。片段着色器通过采样器,访问对应纹理单元里的纹理。

  • 采样器声明为uniform类型,如果纹理只有一个,那么采样器也只需声明一个,那么采样器与纹理对象会默认绑定在一起。如果有多个,需要在渲染前将采样器与对应的纹理单元绑定。
/*将uniform变量纹理采样器,与纹理单元进行对应*/
myShader->use();
myShader->setInt("TextureA", 1);
myShader->setInt("TextureB", 2);
  • 片段着色器:
#version 330 core																
in vec3 vertexColor;
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D TextureA;	
uniform sampler2D TextureB;
																	
void main() 										
{										
		FragColor= mix(texture(TextureA, TexCoord),texture(TextureB, TexCoord),0.2);//从外部输入的纹理图像ourTexture中,使用纹理坐标TexCoord进行采样			
}

7、通过VAO、EBO和着色器绘制图像,在渲染前,和VAO对象一样,需要绑定一下对象。

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TextureA);
glActiveTexture(GL_TEXTURE2);//将该纹理与纹理单元2进行绑定
glBindTexture(GL_TEXTURE_2D, TextureB);


myShader->use();
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

OpenGL学习笔记(二)-着色器-纹理_第12张图片

你可能感兴趣的:(OpenGL,着色器)