一、图形渲染管线:
1、图形渲染管线:
OpenGL的大部分工作:把3D坐标转变为适应你屏幕的2D像素。
功能:指原始图形数据经过期间各种变化最后出现在屏幕上。
分类:①3D坐标→2D坐标;②2D坐标→实际有颜色像素。
【注】:2D坐标≠像素,2D坐标——一个点在2D空间中的位置。2D像素——这个点的近似值,受屏幕/窗口分辨率的限制。
阶段:(蓝色代表可以自定义着色器)
OpenGL的着色器由OpenGL着色器语言(GLSL)写成。
工作大致流程介绍(以上图所示为例):
①顶点着色器:顶点数据(3个3D坐标和颜色值(顶点属性))传送到定点着色器,将3D坐标转为另一种3D坐标,并可以对顶点属性做一些基本处理。
②几何着色器:将选择性输入的一组顶点形成图元,并发出新的顶点形成新的图元生成形状。(上图生成两个三角形)。
③形状(图元)装配:将顶点着色器(或几何着色器)输出的所有点输入,并装配成指定的图元形状。(上图生成两个三角形)。
④光栅化:把图元映射成屏幕上相应的像素,生成供片段着色器使用的片段。
⑤裁剪:丢弃超出视图之外的所有像素,提高执行效率。
⑥片段着色器(OpenGL高级效果产地):计算一个像素的最终颜色,片段着色器包含3D场景数据,可被用来计算最终像素颜色。
⑦Alpha测试和混合阶段:检测片段对应深度(和模板)值,判断这个像素是其他物体前还是后,是保留还是丢弃。检查Alpha值(一个物体透明度)并对物体进行混合。
【注】现代OpenGL必须定义至少一个顶点着色器和一个片段着色器。
2、顶点的输入:
【注】:顶点着色器处理后的顶点坐标(标准化设备坐标NDC)需要满足的条件:①3D坐标,②x, y, z 轴在范围-1.0到1.0上。
//标准化设备坐标
//得到2D,只需要把深度值设为0,及z轴为0
float vertices[] = {
-0.5f,-0.5f,0.0f,
0.5f,-0.5f,0.0f,
0.0f,0.5f,0.0f
}
顶点着色器工作原理:在GPU上创建内存储存顶点数据,还要配置OpenGL如何解释这些内存,并指定其如何发送给显卡。然后,会处理我们在内存中指定数量的顶点。
VBO(顶点缓存对象):用于管理内存,并会在GPU内存(显存)中存储大量的顶点,这样可以一次性发送大批数据到显卡上(原因:CPU数据发送显卡速度较慢,大批发送可以在发送完毕后顶点着色器立即访问顶点)。它是OpenGL的对象,使用glGenBuffers函数生成:
//生成一个带有缓冲ID的VBO对象
unsigned int VBO;
glGenBuffers(1,&VBO);
不同缓冲类型可以同时绑定,顶点缓冲对象的缓冲类型:GL_ARRAY_BUFFER,使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上。
glBindBuffer(GL_ARRAY_BUFFER,VBO);
//此刻起,使用的任何GL_ARRAY_BUFFER目标上的缓冲调用都会用来配置当前绑定的VBO
使用glBufferData函数把定义的顶点数据复制到缓冲内存中
//将定义的数据复制到绑定的缓冲中
//第一个参数:目标缓冲类型
//第二个参数:传输数据的大小(以字节为单位),用sizeof计算顶点数据的大小即可
//第三个参数:希望发送的实际数据
//第四个参数:指定我们希望显卡如何管理给定的数据
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
管理给定数据有三种形式:
① GL_STATIC_DRAW:数据不会或几乎不会改变。
② GL_DYNAMIC_DRAW:数据会被改变很多。
③ GL_STREAM_DRAW:数据每次绘制时都会改变。
3、顶点着色器:
简易顶点着色器编写(GLSL语言):
//代表GLSL版本3.3,与OpenGL一致
#version 330 core
//因为只需要位置数据,且是3D,所以用 in vec3 aPos
//layout(location = 0)设定输入变量的位置值
layout(location = 0) in vec3 aPos;
void main()
{
//GLSL的一个向量有4个分量,vec.x, vec.y, vec.z, vec.w, 最后一个代表透视除法
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
将上述代码硬编码在代码文件顶部的C风格字符串中:
const char *vertexShaderSource = "#version 330 code\n"
"layout(location = 0) in vec3 aPos;\n "
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0"
让OpenGL使用它并编译:
//创建着色器对象
unsigned int vertexShader;
//把创建的着色器类型以参数GL_VERTEX_SHADER形式传递给glCreateShader
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//把着色器源码附加到着色器对象上
//第一个参数:要编译的着色器对象
//第二个参数:指定了传递的源码字符串数量
//第三个参数:定点着色器真正的源码
glShaderSource(vertexShader,1,&vertexShaderSource,NULL);
glCompileShader(vertexShader);
判断编译是否成功:
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;
}
4、片段着色器:
简易片段着色器编写(GLSL语言):
//计算像素最后的颜色输出
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
让OpenGL使用它并编译:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource,NULL);
glCompileShader(fragmentShader);
5、着色器程序:
着色器程序原理 :若要使用创建的着色器,需要将编译的着色器链接成着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活的会在发送渲染调用时使用。
创建一个程序对象shaderProgram:
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
链接顶点着色器和片元着色器:
glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram,fragmentShader);
glLiinkProgram(shaderProgram);
判断链接着色器程序是否成功:
int success;
char infoLog[512];
glGetProgramiv(shaderProgram,GL_LINK_STATUS,&success);
if(!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::Program::LINK_FAILED\n" << infoLog << std::endl;
}
激活程序对象:
glUseProgram(shaderProgram);
删除顶点着色器和片段着色器:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
6、链接顶点属性:
指定在渲染前OpenGL如何解释顶点数据:
//第一个参数:指定我们要配置的顶点属性,在前面layout(location = 0)定义position顶点属性的位置值为0,这里也为0
//第二个参数:指定顶点属性的大小,顶点属性为vec3,由3个值组成
//第三个参数:指定数据类型,用的浮点数
//第四个参数:是否希望数据被标准化
//第五个参数:步长,也就是在连续顶点属性组之间的间隔,这里是3个float,且没有间隔,所以用这个,可以用0,这时候让OpenGL自己决定(前提数值是紧密排列才可用)
//第六个参数:表示位置数据在缓冲中起始位置的偏移量,由于位置数据在数组的开头,所以用0
glVertexAttribPointer(0,3,GL_FALSE,3*sizeof(float),(void*)0);
//以顶点属性位置作为参数,启用顶点属性,顶点属性默认禁用
glEnableVertexAttribuArray(0);
在OpenGL上绘制一个物体
//复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
//设置顶点属性指针:
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
//当我们渲染一个物体时要使用着色器程序:
glUserProgram(shaderProgram);
//绘制物体
someOpenGLFunctionThatDrawOurTriangle();
7、顶点数组对象(VAO):
一个顶点数组对象会存数以下内容:
①glEnableVertexAttribArray和glDisableVertexAttribArray的调用
②通过glVertexAttribPointer设置顶点属性配置
③通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象
创建一个VAO和创建一个VBO:
unsigned int VAO;
glGenVertexArrays(1,&VAO);
把VAO绑定到希望使用的设定上:
//...初始化代码(只运行一次(除非你的物体频繁改变))
//1、绑定VAO
glBindVertexArray(VAO);
//2、把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
//3、设置顶点属性指针:
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttrbArray(0);
[...]
//...绘制代码(渲染循环中)...
//4、绘制物体
glUserProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawTriangle();
绘制物体glDrawArrays函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元:
glUserProgram(shaderProgram);
glBindVertexArray(VAO);
//第一个参数:绘制三角形
//第二个参数:顶点数组的起始索引
//第三个参数:最后绘制多少个顶点
glDrawArrays(GL_TRIANGLE,0,3);
8.元素缓冲对象:
绘制矩形:
//列出所有的点
float vertices[] = {
0.5f,0.5f,0.0f, //右上角
0.5f,-0.5f,0.0f, //右下角
-0.5f,-0.5f,0.0f //左下角
-0.5f,0.5f,0.0f //左上角
};
//用两个三角形绘制一个矩形
unsigned int indices[] = {
//注意索引从0开始
//此例的索引(0,1,2,3)就是顶点数组vertices的下标
//这样可以由下标代表顶点组合成矩形
0,1,2,//第一个三角形
1,2,3 //第二个三角形
};
创建元素缓冲对象
unsigned int EBO;
glGenBuffers(1,&EBO)
用glBufferData把索引复制到缓冲里
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);
从索引缓冲区渲染三角形
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
//第一个参数:绘制模式
//第二个参数:绘制6个顶点
//第三个参数:索引的类型
//第四个参数:EBO的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们在这会填写0
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
绘制代码:
// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,
6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
呈现结果:
二、着色器:
定义:着色器只是一种把输入转化为输出的程序,着色器也是一种非常独立的程序,因为它们之间不能相互通信,它们之间唯一的沟通只有通过输入和输出。
1、GLSL(着色器语言,类C):
着色器典型结构:
//版本号
#version version_number
//输入
in type in_variable_name;
in type in_variable_name;
//输出
out type out_variable_name;
//uniform函数
uniform type uniform_name;
//main()函数
void main()
{
//处理输入并进行一些图形操作
...
//输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
用GL_MAX_VERTEX_ATTRIBS查询顶点属性
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS,&nrAttributes);
std::cout<<"Maximum nr of vertex attributes supported: "<< nrAttributes << std::endl;
//通常情况下至少返回16个,大部分情况下是够用的了
【注】顶点属性的声明是有上限的,取决于硬件,OpenGL至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的属性。
2、数据类型
数据类型:int、float、double、uint、bool,Vector和Matrix(两个容器类型)。
向量
GLSL的向量:可以包含2,3或4个分量容器,分量类型可以是默认基础类型的任意一个。
类型 | 含义 |
---|---|
vecn(常用) | 包含n 个float分量的默认向量 |
bvecn | 包含n 个bool分量的向量 |
ivecn | 包含n 个int分量的向量 |
uvecn | 包含n 个unsigned int分量的向量 |
dvecn | 包含n 个double分量的向量 |
重组:
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
向量传参:
vec2 vect = vec2(0.5,0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz,1.0);
3、输入和输出:
(1)顶点着色器:顶点数据直接接收输入。
使用location这一元数据指定输入变量,这样可以在CPU上配置顶点属性。而且需要为它输入提供一个额外的layout标识,也可以忽略layout(location = 0)标识符,用glGetAttribLocation查询。
#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); //把输出变量设置为红色
}
(2)片段着色器:生成一个最终输出的颜色。
若没有定义输出颜色,默认会渲染为黑色或白色。
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
FragColor = vertexColor;
}
4、Uniform:
从应用程序在CPU上传递数据到GPU上的着色器方式,但与顶点属性不同。
特点:
①uniform是全局的,意味着uniform变量必须在每个着色器程序中是独一无二的,而且可以被着色器程序在任意着色器在任意阶段访问。
②无论你把uniform值设成什么,uniform会一直保存它们的数据,直到被重置或更新。
uniform设置三角形颜色:
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; //在OpenGL程序代码中设定这个变量
void main()
{
FragColor = ourColor;
}
【注】若声明的uniform 在GLSL 中未用,编译器会静默移除这个变量,可能会导致几个非常麻烦的错误!!!
随时间改变颜色
//获取运行秒数
float timeValue = glfwGetTime();
//用sin函数让颜色在0.0到1.0之间改变,最后将结果储存在greenValue里
float greenValue = (sin(timeValue)/2.0f)+0.5f;
//查询uniform ourColor位置值,返回-1就代表没有
int vertexColorLocation = glGetUniformLocation(shaderProgram,"ourColor");
glUseProgram(shaderProgram);
//设置uniform值
glUniform4f(vertexColorLocation,0.0f,greenValue,0.0f,1.0f);
//注意:
//查询uniform地址不需要你之前使用过着色器程序,但是更新一个uniform之前必须先使用程序,因为它是在当前激活的着色器中设置的。
glUniform的后缀,标识设定的uniform类型。
后缀 | 含义 |
---|---|
f | 函数需要一个float作为它的值 |
i | 函数需要一个int作为它的值 |
ui | 函数需要一个unsigned int作为它的值 |
3f | 函数需要3个float作为它的值 |
fv | 函数需要一个float向量/数组作为它的值 |
颜色慢慢变化
while(!glfwWindowShouldClose(window))
{
//输入
processInput(window);
//渲染
//清除颜色缓冲
glClearColor(0.2f,0.3f,0.3f,1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//记得激活着色器
glUseProgram(shaderProgram);
//更新uniform颜色
float timeValue = glfwTime();
float greenValue = sin(timeValue)/2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram,"ourColor");
//绘制三角形
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES,0,3);
//交换缓冲并查询IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}
5、顶点和颜色属性:
定义顶点和颜色:
float vertices[] = {
//位置 //颜色
0.5f , -0.5f , 0.0f, 1.0f, 0.0f, 0.0f, //右下
-0.5f, -0.5f , 0.0f, 0.0f, 1.0f, 0.0f, //左下
0.0f , 0.5f , 0.0f, 0.0f, 0.0f, 1.0f //顶部
}
调整顶点着色器:
#version 330 core
//位置变量的属性位置值为0
layout(location = 0) in vec3 aPos;
//颜色变量的属性位置值为1
layout(location = 1) in vec3 aColor;
//向片段着色器输出一个颜色
out vec3 ourColor;
void main()
{
gl_Position = vec4(aPos, 1.0);
//将ourColor设置为我们从顶点数据那里得到的输入颜色
ourColor = aColor;
}
调整片段着色器:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor,1.0);
}
由于添加了另一个顶点属性,并更新了VBO的内存,就需要配置顶点属性指针:
//位置属性,位置属性在前,偏移量为0
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
//颜色属性,位置属性在位置数据之后,偏移量为3*sizeof(float)
glVertexAttribPointer(1,3,GL_FlOAT,GL_FALSE,6*sizeof(float),(void*)(3*sizeof(float));
glEnableVertexAttribArray(1);
【注】呈现结果会出现片段插值现象,是由于光栅化阶段通常会造成比原指定顶点更多的片段。
6、把着色器放在头文件里:
#ifndef SHADER_H
#define SHADER_H
#include<glad/glad.h>; // 包含glad类来获取所有的必须OpenGL头文件
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
class Shader
{
public:
//程序ID
unsigned int ID;
//构建器读取并构建着色器
Shader(const char* vertexPath, const char* fragmentPath);
//使用/激活程序
void use();
//uniform工具函数
//可以查询位置值,并设置它的值
void setBool(const std::string &name, bool value) const;
void setInt(const std::string &name, int value) const;
void setFloat(const std::string &name, float value) const;
}
#endif
7、写一个完整的着色器类:
①使用C++文件流读取着色器内容,储存到几个string对象里:
Shader(const char* vertexPath, const char* fragmentPath)
{
//1、文件路径中获取顶点/片段着色器
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
//保证ifStream对象可以抛出异常:
vShaderFile.exception(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exception(std::ifstream::failbit | std::ifstream::badbit);
try{
//打开文件
vShaderFile.open(vertexPath);
fShaderFile.open(vertexPath);
//读取文件的缓冲内容到数据流中
vShaderStream<< vShaderFile.rdbuf();
fShaderStream<< fShaderFile.rdbuf();
//关闭文件处理器
vShaderFile.close();
fShaderFile.close();
//转换数据流到string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch(std::ifstream::failure e)
{
std::cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = vertexCode.c_str();
[...]
}
②编译和链接着色器
//2、编译着着色器
unsigned int vertex, fragment;
int success;
char infoLog[512];
//顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex,1,&vShaderCode,NULL);
glCompileShader(vertex);
//打印编译错误(如果有的话)
glGetShaderiv(vertex,GL_COMPILE_STATUS,&success);
if(!success)
{
glGetShaderInfoLog(vertex,512,NULL,infoLog);
std::cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILURE\n"<<infoLog<<std::endl;
}
//片段着色器也类似
[...]
//着色器程序
ID = glCreateProgram();
glAttachShader(ID,vertex);
glAttachShader(ID,fragment);
glLinkProgram(ID);
//打印连接错误(如果有的话)
glGetProgramiv(ID,GL_LINK_STATUS,&success);
if(!success)
{
glGetProgramInfoLog(ID,512,NULL,infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILURE\n"<<infoLog<<std::endl;
}
//删除着色器,它们已经链接到我们的程序中,已经不再需要
glDeleteShader(vertex);
glDeleteShader(fragment);
③use函数
void use()
{
glUseProgram(ID);
}
④uniform的setter函数
void setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID,name.c_str()),(int)value);
}
void setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID,name.c_str()),value);
}
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID,name.c_str()),value);
}
使用上述着色器:
Shader ourShader("path/to/shaders/shader.vs","path/to/shader.fs");
...
while(...)
{
ourShader.use();
ourShader.setFloat("someUniform",1.0f);
DrawStuff();
}