第一章 入门
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、输入与输出
- 每个着色器都需要有输入和输出,利用关键字
in
和out
。 - 由于顶点着色器要从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中的顶点数据变化为:
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)所有片段着色器的输入变量。
1.3.4 着色器类
1、本节目的在于将着色器源代码写到txt文件中,然后构建着色器类,利用类从硬盘中读取着色器代码,编译并链接着色器,最后生成着色器程序对象。 源代码的读取流程为:
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);
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)。
3 、纹理环绕方式(Wrapping):纹理坐标如果超出(0,1)的范围,OpenGL会重复该纹理图像,设定对超出部分的颜色,。
- OpenGL中有四种不同的环绕方式,分别为:
GL_REPEAT
重复纹理图像。GL_MIRRORED_REPEAT
镜像重复纹理图像。GL_CLAMP_TO_EDGE
超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。GL_CLAMP_TO_BORDER
超出的坐标为用户指定的边缘颜色。
- 通过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
通过对邻近像素的线性插值,计算样本颜色
- 如果对一个很大的物体应用分辨率很低的纹理,那么纹理就需要被放大,利用不同的过滤方式进行计算,
GL_NEAREST
产生了颗粒状的图案,能够清晰看到组成纹理的像素,而GL_LINEAR
能够产生更平滑的图案,很难看出单个的纹理像素。
- 当进行放大(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会使用不同的多级渐远纹理,即最适合物体的距离的那个。
- 利用
glGenerateMipmaps
可以产生多级渐远纹理,在不同级别的纹理层之间,也需要利用NEAREST
和LINEAR
进行过滤。使用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中设置纹理坐标的属性;顶点着色器中读取纹理坐标;将对应纹理单元与采样器绑定;片段着色器中利用采样器从对应纹理单元中访问纹理对象。
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);