【OpenGL学习笔记】着色器篇

一、图形渲染管线

在OpenGL中,所有事物都在3D空间中,而我们的屏幕是2D的,这导致OpenGL大部分工作都是在进行3D转2D。而这个过程是由OpenGL的图形渲染管线(Graphic Pipeline)管理的。图形渲染管线分以下几个阶段:

图形渲染管线

顶点数据:3D空间中顶点的坐标值
顶点着色器:实现顶点坐标系转换
形状(图元)装配:将顶点组装成指定图元形状
几何着色器:可添加新的顶点构造新的图元
光栅化:生成片段(指渲染一个像素所需所有数据)
片段着色器:计算顶点的输出颜色
测试与混合:进行深度,模板,混合等测试

二、着色器语言

为图形计算量身定制的语言 ( OpenGL Shading Language,GLSL),它和C语言很像,将在第四节详细讲解。下面是着色器的使用流程。

先了解一些概念
顶点着色器:描述如何绘制顶点的文件
片段着色器:描述如何绘制顶点颜色的文件
PS:通常我们将着色器写在独立的文件里,然后在主程序里动态加载和编译。

  • 顶点着色器

//版本号说明
#version 330 core
//layout (location = 0):是标记position位置 
//in关键字:输入变量
//vec3: 3分量向量
layout (location = 0) in vec3 position;
//程序入口
void main()
{
	//gl_Position: 预定义变量(不可修改),输出顶点
	//vec4: 4分量向量(第4个分量w不是用来表示空间的,咱玩的是3D不是4D)
	//w分量也叫做齐次坐标,简单来说是用来位移用的[矩阵运算]
    gl_Position = vec4(position.x, position.y, position.z, 1.0);
}
  • 片段着色器

//版本号说明
#version 330 core
//out关键字:输出变量
out vec4 color;

void main()
{
    color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
  • 编译和链接着色器

现在我们写了最简单的顶点着色器和片段着色器,那么我们将如何使用它们呢。

----------------------   创建着色器   -----------------
//创建一个顶点着色器
GLuint vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//加载和编译顶点着色器
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
//创建一个片段着色器
GLuint fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//加载和编译片段着色器
glShaderSource(fragmentShader, 1, &fragmentShaderSource, null);
glCompileShader(fragmentShader);

-----------------   glShaderSource解释   -----------------
//第一参数:着色器
//第二参数:指定了传递的源码字符串数量,这里只有一个。
//第三参数:是顶点着色器真正的源码(指上边写的GLSL)(char *类型)
//第四参数:我们先设置为NULL。

---------------------   获取编译信息  --------------------
GLint success;
GLchar infoLog[512];
//检测着色器编译是否成功
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);

if(!success)
{
	//获取错误信息
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    //do someting...
}

------------------------  链接过程  --------------------
//创建着色器程序
GLuint shaderProgram;
shaderProgram = glCreateProgram();
//链接上边写的着色器
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

---------------------   获取链接信息  --------------------
GLint success;
GLchar infoLog[512];
//检测着色器链接是否成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);

if(!success)
{
	//获取错误信息
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    //do someting...
}
//激活着色器程序
glUseProgram(shaderProgram);
//删除着色器
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

来,我们这里复习一下思路

  1. 创建着色器源码
  2. 加载,编译和链接着色器源码
  3. 激活着色器程序和内存释放(删除着色器)

三、数据输入

我们已经在上边做好渲染工作,是时候输入我们要画的东西了。
了解一些概念先
顶点缓冲对象(Vertex Buffer Objects, VBO):管理顶点数据的对象,在GPU中保存大量顶点
顶点数组对象(Vertex Array Object, VAO):保存VBO的配置过程(可多个)
索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO):用于减少顶点数据量

  • 顶点缓冲对象(VBO) 使用

--------------------   准备数据   -----------------
//顶点坐标数据 这里指的是物体坐标 值限制在[-1,1]
GLfloat vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};
//创建顶点缓冲对象
GLuint VBO;
glGenBuffers(1, &VBO);  
//绑定缓冲对象
glBindBuffer(GL_ARRAY_BUFFER, VBO); 
//复制数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 
//解析数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
//使能顶点属性 参数和glVertexAttribPointer第一个参数相同
glEnableVertexAttribArray(0);

-----------------   glBufferData解释   -----------------
//第一参数: 缓冲对象类型
//第二参数: 数据大小
//第三参数: 数据
//第四参数: 管理数据的方式
//	GL_STATIC_DRAW :数据不会或几乎不会改变。
//	GL_DYNAMIC_DRAW:数据会被改变很多。
//	GL_STREAM_DRAW :数据每次绘制时都会改变。

--------- ----   glVertexAttribPointer解释   -----------------
//第一参数: 要配置的顶点属性。就是在顶点着色器源码中layout(location = 0)的那个值
//第二参数: 顶点属性大小 这里顶点属性是一个vec3 它由3个值组成,所以大小是3
//第三参数: 顶点数据类型
//第四参数: 是否归一化 就是把顶点数据映射到[0,1](有符号是[-1,1])
//第五参数: 步长 两个顶点属性之间的距离
//第六参数: 数据位置的偏移量
  • 顶点数组对象(VAO) 使用

VAO的使用非常简单,这里套用上边VBO的代码
问:为啥要用VAO?
答:如果不用VAO,那么VBO的绑定到解绑的过程全部都要写在绘制图形的过程中,这样好麻烦呀。使用VAO可以记录这个过程,那么后面写代码就轻松啦

--------------------   准备数据   -----------------
//顶点坐标数据 这里指的是物体坐标 值限制在[-1,1]
GLfloat vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

//创建顶点数组对象
GLuint VAO;
glGenVertexArrays(1, &VAO);  
	//创建顶点缓冲对象
	GLuint VBO;
	glGenBuffers(1, &VBO);  
//绑定顶点数组缓冲
glBindVertexArray(VAO);
	//绑定缓冲对象
	glBindBuffer(GL_ARRAY_BUFFER, VBO); 
	//复制数据
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 
	//解析数据
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
	//使能顶点属性 参数和glVertexAttribPointer第一个参数相同
	glEnableVertexAttribArray(0);
//解绑顶点数组对象
glBindVertexArray(0);
  • 绘制图形

--------------------   绘制过程   -----------------
//使用着色器程序
glUseProgram(shaderProgram);
//绑定VAO
glBindVertexArray(VAO);
//画个三角形
glDrawArrays(GL_TRIANGLES, 0, 3);
//解绑VAO
glBindVertexArray(0);  

--------------------   glDrawArrays解释   -----------------
//第一参数:绘制类型
//第二参数:指定顶点数组的起始索引
//第三参数:需要画几个顶点

  • 索引缓冲对象(EBO)使用

举个例子:当我们要画得不是一个三角形而是一个矩形时,用两个三角形组成矩形(OpenGL主要处理三角形),那么顶点数据是这样的:

GLfloat 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,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

这显然有点不对劲,一个矩形只需要4个顶点,然而却用了6个顶点,显然多了50%的开销。那么我们使用另一种方法来表示顶点,像这样子

GLfloat 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   // 左上角
};

GLuint indices[] = { // 注意索引从0开始! 
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

这样看起来顶点数据就没有多余的开销了,那么我们怎样使用这样的顶点数据呢?

//创建索引缓冲对象
GLuint EBO;
glGenBuffers(1, &EBO);
	//绑定顶点数组对象
	glBindVertexArray(VAO);
    //绑定顶点缓冲对象 和 复制数据
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//绑定索引缓冲对象 和 复制数据
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    //解析数据
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    //使能
    glEnableVertexAttribArray(0);
	//解绑顶点数组对象
	glBindVertexArray(0);

那么新的绘制图形会变成这样子

--------------------   绘制过程   -----------------
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
//绘制矩形
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

--------------------   glDrawElements解释   -----------------
//第一参数:绘制类型
//第二参数:需要画几个顶点
//第三参数:索引类型
//第死参数:指定EBO的偏移量或这新的索引数组
  • 其他

//线模式
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//默认模式
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

到这里就是简单着色器使用的所有内容啦~

四、GLSL详解

  • 典型的着色器

#version version_number
//顶点着色器的输入变量(也叫顶点属性),我们能声明的in 是有限的(16个),由硬件决定,有些会更多
//在主程序中,我们可以这样获取它
//	GLint nrAttributes;
//	glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
//	std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

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

类型含义
int float double uint bool基本类型
vecn包含n个float分量的默认向量
bvecn包含n个bool分量的向量
ivecn包含n个int分量的向量
uvecn包含n个unsigned int分量的向量
dvecn包含n个double分量的向量
Matrix矩阵

通常使用vec就够用了,我们可以这样子玩向量。

//xyzw 分别是向量的1,2,3,4分量
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
vec2 vect = vec2(0.5f, 0.7f);
vec4 result = vec4(vect, 0.0f, 0.0f);
vec4 otherResult = vec4(result.xyz, 1.0f);
  • Uniform 关键字

#version 330 core
out vec4 color;
//这是个全局变量
uniform vec4 ourColor; 

void main()
{
    color = ourColor;
}  

在程序中我们该如何给它赋值呢?

//根据当前时间来设置颜色的G通道
GLfloat timeValue = glfwGetTime();
GLfloat greenValue = (sin(timeValue) / 2) + 0.5;
//查询 ourColor 变量
GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
//选择着色器程序
glUseProgram(shaderProgram);
//赋值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

OpenGL核心是个C库,不支持重载,所以像glUniform 函数通过加不同后缀来实现不同输入参数

后缀含义
f函数需要一个float作为它的值
i函数需要一个int作为它的值
ui函数需要一个unsigned int作为它的值
3f函数需要3个float作为它的值
fv函数需要一个float向量/数组作为它的值
  • 设置更多属性的例子

这也是着色器间数据传输的例子【篇幅很长,只列出关键部分】,首先顶点着色器和片段着色器是这样子的

//顶点着色器
#version 330 core
// 位置变量的属性位置值为 0 
layout (location = 0) in vec3 position; 
// 颜色变量的属性位置值为 1
layout (location = 1) in vec3 color;    
// 向片段着色器输出一个颜色
out vec3 ourColor; 

void main()
{
    gl_Position = vec4(position, 1.0);
    ourColor = color; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
//片段着色器
#version 330 core
in vec3 ourColor;
out vec4 color;

void main()
{
    color = vec4(ourColor, 1.0f);
}

然后主程序是这样子

//顶点数据
GLfloat 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    // 顶部
};

// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat)));
glEnableVertexAttribArray(1);

--------------------   glVertexAttribPointer解释   -----------------
//前面几个参数已经很熟悉了,说一下最后一个参数
//由顶点数据可以看出,颜色数据在第4位,那么就应该跳过3位来表明颜色数据的起始位置。
  • 自定义着色器类

从前边可以看出编写,编译,管理着色器是真他喵麻烦,所以让我们来把他封装成类吧。
https://github.com/AutoCatFuuuu/OpenGL
[ShaderHelper.cpp] & [ShaderHelper.h]

五、写在最后

本文章只是写给自己看的,看着别人的文章,根据自己的理解,以使用理解代码为主,做下记录,仅此而已。想要教程的话
https://learnopengl-cn.github.io
https://learnopengl-cn.readthedocs.io/zh/latest/
欢迎相互学习讨论:QQ:673315140

热门文章

暂无图片
编程学习 ·

exe4j详细使用教程(附下载安装链接)

一、exe4j介绍 ​ exe4j是一个帮助你集成Java应用程序到Windows操作环境的java可执行文件生成工具&#xff0c;无论这些应用是用于服务器&#xff0c;还是图形用户界面&#xff08;GUI&#xff09;或命令行的应用程序。如果你想在任务管理器中及Windows XP分组的用户友好任务栏…
暂无图片
编程学习 ·

AUTOSAR从入门到精通100讲(126)-浅谈车载充电系统通信方案

01 引言 本文深入研究车载充电系统策略,设计出一套基于电动汽车电池管理系统与车载充电机的CAN通信协议,可供电动汽车设计人员参考借鉴。 02 电动汽车充电系统通讯网络 电动汽车整车控制系统中采用的是CAN总线通信方式,由一个整车内部高速CAN网络、内部低速CAN网络和一个充电…
暂无图片
编程学习 ·

CMake(九):生成器表达式

当运行CMake时&#xff0c;开发人员倾向于认为它是一个简单的步骤&#xff0c;需要读取项目的CMakeLists.txt文件&#xff0c;并生成相关的特定于生成器的项目文件集(例如Visual Studio解决方案和项目文件&#xff0c;Xcode项目&#xff0c;Unix Makefiles或Ninja输入文件)。然…
暂无图片
编程学习 ·

47.第十章 网络协议和管理配置 -- 网络配置(八)

4.3.3 route 命令 路由表管理命令 路由表主要构成: Destination: 目标网络ID,表示可以到达的目标网络ID,0.0.0.0/0 表示所有未知网络,又称为默认路由,优先级最低Genmask:目标网络对应的netmaskIface: 到达对应网络,应该从当前主机哪个网卡发送出来Gateway: 到达非直连的网络,…
暂无图片
编程学习 ·

元宇宙技术基础

请看图&#xff1a; 1、通过AR、VR等交互技术提升游戏的沉浸感 回顾游戏的发展历程&#xff0c;沉浸感的提升一直是技术突破的主要方向。从《愤怒的小鸟》到CSGO,游戏建模方式从2D到3D的提升使游戏中的物体呈现立体感。玩家在游戏中可以只有切换视角&#xff0c;进而提升沉浸…
暂无图片
编程学习 ·

flink的伪分布式搭建

一 flink的伪分布式搭建 1.1 执行架构图 1.Flink程序需要提交给 Job Client2.Job Client将作业提交给 Job Manager3.Job Manager负责协调资源分配和作业执行。 资源分配完成后&#xff0c;任务将提交给相应的 Task Manage。4.Task Manager启动一个线程以开始执行。Task Manage…
暂无图片
编程学习 ·

十进制正整数与二进制字符串的转换(C++)

Function one&#xff1a; //十进制数字转成二进制字符串 string Binary(int x) {string s "";while(x){if(x % 2 0) s 0 s;else s 1 s;x / 2;}return s; } Function two&#xff1a; //二进制字符串变为十进制数字 int Decimal(string s) {int num 0, …
暂无图片
编程学习 ·

[含lw+源码等]微信小程序校园辩论管理平台+后台管理系统[包运行成功]Java毕业设计计算机毕设

项目功能简介: 《微信小程序校园辩论管理平台后台管理系统》该项目含有源码、论文等资料、配套开发软件、软件安装教程、项目发布教程等 本系统包含微信小程序做的辩论管理前台和Java做的后台管理系统&#xff1a; 微信小程序——辩论管理前台涉及技术&#xff1a;WXML 和 WXS…
暂无图片
编程学习 ·

树莓派驱动DHT11温湿度传感器

1&#xff0c;直接使用python库 代码如下 import RPi.GPIO as GPIO import dht11 import time import datetimeGPIO.setwarnings(True) GPIO.setmode(GPIO.BCM)instance dht11.DHT11(pin14)try:while True:result instance.read()if result.is_valid():print(ok)print(&quo…
暂无图片
编程学习 ·

ELK简介

ELK简介 ELK是三个开源软件的缩写&#xff0c;Elasticsearch、Logstash、Kibana。它们都是开源软件。不过现在还新增了一个 Beats&#xff0c;它是一个轻量级的日志收集处理工具(Agent)&#xff0c;Beats 占用资源少&#xff0c;适合于在各个服务器上搜集日志后传输给 Logstas…
暂无图片
编程学习 ·

Linux 基础

通常大数据框架都部署在 Linux 服务器上&#xff0c;所以需要具备一定的 Linux 知识。Linux 书籍当中比较著名的是 《鸟哥私房菜》系列&#xff0c;这个系列很全面也很经典。但如果你希望能够快速地入门&#xff0c;这里推荐《Linux 就该这么学》&#xff0c;其网站上有免费的电…
暂无图片
编程学习 ·

Windows2022 无线网卡装不上驱动

想来 Windows2022 和 windows10/11 的驱动应该差不多通用的&#xff0c;但是死活装不上呢&#xff1f; 搜一下&#xff0c;有人提到 “默认安装时‘无线LAN服务’是关闭的&#xff0c;如果需要开启&#xff0c;只需要在“添加角色和功能”中&#xff0c;选择开启“无线LAN服务…
暂无图片
编程学习 ·

【嵌入式面试宝典】版本控制工具Git常用命令总结

目录 创建仓库 查看信息 版本回退 版本检出 远程库 Git 创建仓库 git initgit add <file> 可反复多次使用&#xff0c;添加多个文件git commit -m <message> 查看信息 git status 仓库当前的状态git diff 差异对比git log 历史记录&#xff0c;提交日志--pret…
暂无图片
编程学习 ·

用Postman生成测试报告

newman newman是一款基于nodejs开发的可以运行postman脚本的工具&#xff0c;使用Newman&#xff0c;可以直接从命令运行和测试postman集合。 安装nodejs 下载地址&#xff1a;https://nodejs.org/en/download/ 选择自己系统相对应的版本内容进行下载&#xff0c;然后傻瓜式安…
暂无图片
编程学习 ·

Java面向对象之多态、向上转型和向下转型

文章目录前言一、多态二、引用类型之间的转换Ⅰ.向上转型Ⅱ.向下转型总结前言 今天继续Java面向对象的学习&#xff0c;学习面向对象的第三大特征&#xff1a;多态&#xff0c;了解多态的意义&#xff0c;以及两种引用类型之间的转换&#xff1a;向上转型、向下转型。  希望能…