【LearnOpenGL】06.着色器基础(着色器设置)
liuian 2025-05-08 02:44 42 浏览
开发环境
- Windows 10
- Visual Studio Community 2022
准备项目
1.新建一个空的C++项目:06_Shader,并设置为启动项目。
2.配置GLEW和GLFW。根据【LearnOpenGL】01.创建窗口和【LearnOpenGL】03.配置GLEW,对这2项进行配置,保证编译通过。
简介
OpenGL 着色语言(OpenGL Shading Language,简称为GLSL)是 OpenGL 中使用了一种高级的图形编程语言。其中可以在 OpenGL 可编程处理器上执行的 OpenGL 着色语言代码被称为“着色器(Shader)”。
可编程着色器主要是对顶点和片元进行处理。顶点处理涉及在各个顶点发生的操作,尤其是变换和光照。片元处理是在各个片元(各个像素的数据结构)基础上进行的操作,尤其是从纹理内存读取和在各个片元上应用纹理值的操作组成的。
语法
OpenGL 着色语言是基于 ANSI C 编程语言的语法。使用 OpenGL 着色语言编写的程序的基本结构与使用 C 编写的程序的基本结构相同:
- 一组着色器的入口点是函数 void main(),而这个函数的主体被界定在括号中。
- OpenGL 着色语言中的常量、标识符、运算符、表达式和语句与 C 是基本相同的。
- 用于循环的控制流、if-then-else 以及函数调用与 C 几乎完全相同。
OpenGL 着色语言也具有其专用性。添加了一些与 ANSI C 不同的语言特性:
- 支持用于浮点数、整数和布尔值的矢量类型。
- 运算符可以像处理标量一样处理矢量类型。
- 使用数组语法或者作为一个结构的字段可以访问矢量的单个组分。
- 增加了浮点矩阵,使用数组语法可以选定矩阵的列,得到一个矢量。
- 增加了取样器(sampler)的基本类型,可以用来访问纹理内存。
- 增加了attribute、uniform 和 varying 限定符。
数据类型:标量
类型 | 描述 |
float | 声明一个单独的浮点数 |
int | 声明一个单独的整数 |
bool | 声明一个单独的布尔数 |
数据类型:矢量
类型 | 描述 |
vec2 | 包含2个浮点数的矢量 |
vec3 | 包含3个浮点数的矢量 |
vec4 | 包含4个浮点数的矢量 |
ivec2 | 包含2个整数的矢量 |
ivec3 | 包含3个整数的矢量 |
ivec4 | 包含4个整数的矢量 |
bvec2 | 包含2个布尔数的矢量 |
bvec3 | 包含3个布尔数的矢量 |
bvec4 | 包含4个布尔数的矢量 |
数据类型:矩阵
mat2 或 mat2x2 | mat2x3 | mat2x4 |
mat3x2 | mat3 或 mat3x3 | mat3x4 |
mat4x2 | mat4x3 | mat4 或 mat4x4 |
数据类型:取样器
类型 | 描述 |
sampler1D | 访问一个一维纹理 |
sampler2D | 访问一个二维纹理 |
sampler3D | 访问一个三维纹理 |
samplerCube | 访问一个立方贴图纹理 |
sampler1DShadow | 访问一个带对比的一维深度纹理 |
sampler2DShadow | 访问一个带对比的二维深度纹理 |
限定符:Attribute
属性限定符(或者属性变量)用来使应用程序将经常修改的数据传递到顶点着色器中。
- 局限于浮点数标量、浮点数矢量和矩阵。
- 不能在片元着色器中声明。
限定符:Uniform
一致限定变量(或一致变量)只能在着色器外部设置,并且是用于较少更改的数据。
- 支持所有数据类型和所有数据类型的数组。
- 一致变量不能被写入着色器。
限定符:Varying
易变限定变量(或者易变变量)是可以让顶点着色器与片元着色器交流结果的唯一方式。用于从顶点着色器传递为片元着色器的插值得到的信息。
调用约定:in、out
OpenGL 着色语言使用按值-返回调用作为其调用约定。这里要与按值调用做一下区分。
- 按值调用:限定为输入参数的参数将被复制到函数中,并且不会作为引用传递。
- 按值-返回调用:限定为输出参数的参数将会返回到调用程序。
关键字 | 说明 |
in | 复制进函数但不在返回时复制,在函数内部仍然是可写的。 |
out | 只在返回时复制,是可读的,但是在进入函数时是未定义的。 |
inout | 复制进函数并在返回时复制。 |
顶点着色器
顶点着色器(Vertex Shader)包含了在提供给 OpenGL 的各个顶点上发生的操作。
#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
out vec3 v_Color;
void main()
{
gl_Position = vec4(position, 1.0);
v_Color = color;
}
第1行:版本声明。这里表示适用于 OpenGL 3.3以及更高版本中。
第2行:输入位置顶点属性,以及定义该属性位置值(Location)为0。
第3行:输入颜色顶点属性,以及定义该属性位置值为1。
第4行:为片元着色器指定一个颜色输出。
第8行:将顶点属性输入到顶点着色器。这里表示将位置属性输入到 gl_Position 中。
第9行:将顶点属性中颜色变量赋值给颜色输出。
片元着色器
片元着色器的目的是计算要应用到片元上的颜色或者计算片元的深度值,或者二者都计算。
#version 330 core
out vec4 color;
in vec3 v_Color;
void main()
{
color = vec4(v_Color, 1.0);
}
第1行:版本声明。这里表示适用于 OpenGL 3.3以及更高版本中。
第2行:声明最终片元上的颜色值。
第3行:从顶点着色器传来的输入变量(名称相同、类型相同)。
第7行:将计算得到的颜色值输出。
着色器的编译
对于着色器程序,我们使用编译器来解析程序,检查是否存在错误,然后将它翻译为目标代码。然后,将一系列目标文件合并,并产生最终的可执行程序。
对于每个着色器对象:
1.创建着色器对象
GLunit glCreateShader(GLenum type);
参数:
- type
指定着色器对象类型。必须是 GL_VERTEX_SHADER、GL_FRAGMENT_SHADER、GL_GEOMETRY_SHADER、GL_COMPUTE_SHADER等中的一个。
- 返回值:无符号整数。如果为0则说明发生了错误。
2.将着色器源代码附加到着色器对象上
void glShaderSource(GLuint shader, GLsizei count, const GLchar** string, const GLint* length);
参数:
- shader
指定要替换源代码的着色器对象的句柄。
- count
指定 string 和 length 数组中元素的个数。
- string
指定一个指向字符串的指针数组,其中包含加载到着色器中的源代码。
- length
指定字符串长度的数组。
3.编译着色器源代码
void glCompileShader(GLuint shader);
参数:
- shader
指定要编译的着色器对象。
对于着色器程序:
1.创建一个着色器程序
GLuint glCreateProgram(void);
- 返回值:无符号整数。如果为0则说明发生了错误。
2.将着色器对象关联到着色器程序
void glAttachShader(GLuint program, GLuint shader);
参数:
- program
关联到的着色器程序。
- shader
着色器对象。
着色器对象可以在任何时候关联到着色器程序,但是只有经过程序的成功链接之后,着色器对象的功能才能可用。
着色器对象可以同时关联到多个不同的着色器程序上。
3.链接着色器程序
void glLinkProgram(GLuint program);
参数:
- program
指定链接的着色器程序。
5.使用着色器来处理顶点和片元
void glUseProgram(GLuint program);
参数:
- program
指定要运行的着色器程序。
如果 program 为0,那么所有当前使用的着色器都会被清除。
Shader类
着色器源代码编写、编译、加载、管理都是比较麻烦的事,所以构建一个类专门处理这些事情很有必要。
Shader.h
#pragma once
#include <string>
class Shader
{
public:
/// <summary>
/// 加载源代码文件,构造着色器
/// </summary>
/// <param name="vertexPath">顶点着色器源代码文件路径</param>
/// <param name="fragmentPath">片元着色器源代码文件路径</param>
Shader(const std::string& vertexPath, const std::string &fragmentPath);
/// <summary>
/// 析构函数
/// </summary>
~Shader();
/// <summary>
/// 启用着色器程序
/// </summary>
void Bind() const;
/// <summary>
/// 取消使用着色器程序
/// </summary>
void Unbind() const;
private:
/// <summary>
/// 着色器程序ID
/// </summary>
unsigned int m_ID;
/// <summary>
/// 根据着色器源代码字符串,构建着色器对象
/// </summary>
/// <param name="vertexSource">顶点着色器源代码字符串</param>
/// <param name="fragmentSource">片元着色器源代码字符串</param>
void CreateShader(const std::string& vertexSource, const std::string& fragmentSource);
/// <summary>
/// 把着色器源代码字符串,根据指定的着色器类型,编译成着色器对象。
/// </summary>
/// <param name="type">着色器类型</param>
/// <param name="source">着色器源代码字符串</param>
/// <returns>返回着色器对象</returns>
unsigned int CompileShader(unsigned int type, const std::string& source);
};
Shader.cpp
#include "Shader.h"
#include <GL/glew.h>
#include <fstream>
#include <sstream>
#include <iostream>
Shader::Shader(const std::string& vertexPath, const std::string &fragmentPath) :m_ID(0)
{
// 着色器源代码字符串
std::string vertexCode;
std::string fragmentCode;
std::ifstream vertexFile;
std::ifstream fragmentFile;
// 保证ifstream对象可以抛出异常:
vertexFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fragmentFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
// 打开文件
vertexFile.open(vertexPath);
fragmentFile.open(fragmentPath);
std::stringstream vertexStream, fragmentStream;
// 读取文件的缓冲内容到数据流中
vertexStream << vertexFile.rdbuf();
fragmentStream << fragmentFile.rdbuf();
// 关闭文件处理器
vertexFile.close();
fragmentFile.close();
// 转换数据流到string
vertexCode = vertexStream.str();
fragmentCode = fragmentStream.str();
}
catch (const std::exception&)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
CreateShader(vertexCode, fragmentCode);
}
Shader::~Shader()
{
glDeleteProgram(m_ID);
}
void Shader::CreateShader(const std::string& vertexSource, const std::string& fragmentSource)
{
unsigned int vertexShader, fragmentShader;
// 顶点着色器对象
vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource);
// 片元着色器对象
fragmentShader = CompileShader(GL_FRAGMENT_SHADER, fragmentSource);
if (vertexShader > 0 || fragmentShader > 0)
{
// 创建着色器程序
m_ID = glCreateProgram();
// 关联着色器对象
if (vertexShader > 0) glAttachShader(m_ID, vertexShader);
if (fragmentShader > 0) glAttachShader(m_ID, fragmentShader);
// 链接着色器程序
glLinkProgram(m_ID);
}
// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
if (vertexShader > 0) glDeleteShader(vertexShader);
if (fragmentShader > 0) glDeleteShader(fragmentShader);
}
unsigned int Shader::CompileShader(unsigned int type, const std::string& source)
{
unsigned int shader;
// 创建着色器对象
shader = glCreateShader(type);
const char* src = source.c_str();
// 附加着色器源码
glShaderSource(shader, 1, &src, NULL);
// 编译着色器对象
glCompileShader(shader);
return shader;
}
void Shader::Bind() const
{
glUseProgram(m_ID);
}
void Shader::Unbind() const
{
glUseProgram(0);
}
顶点数据
有了着色器,我们就可以把每个顶点上的颜色属性展示出来了。
// 顶点数据
float vertices[] = {
// --- position --- --- color ---
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 左上角
0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 右上角
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f // 右下角
};
因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:
// --- 位置属性 ---
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), 0);
glEnableVertexAttribArray(0);
// --- 颜色属性 ---
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), (void*)(3 * sizeof(GL_FLOAT)));
glEnableVertexAttribArray(1);
由于我们现在有了两个顶点属性,我们不得不重新计算步长值:6*sizeof(GL_FLOAT)。对于每个顶点,位置属性的偏移值不变,还是0;而颜色属性的偏移值是位置属性的字节长度,即:3*sizeof(GL_FLOAT)。
完整项目
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "src/Shader.h"
unsigned int VBO;
unsigned int IBO;
void init() {
// 顶点数据
float vertices[] = {
// --- position --- --- color ---
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 左上角
0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 右上角
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f // 右下角
};
// 绘制顶点的顺序
unsigned int indices[] = {
0, 1, 2, // 第一个三角形的顶点数组下标
2, 0, 3 // 第二个三角形的顶点数组下标
};
// 创建一个缓存对象名,并存储到VBO
glGenBuffers(1, &VBO);
// 将缓存对象绑定到GL_ARRAY_BUFFER
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 把顶点数据复制到当前绑定的缓存对象
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 创建一个缓存对象名,并存储到IBO
glGenBuffers(1, &IBO);
// 将缓存对象绑定到GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
// 把顶点索引数据复制到当前绑定的缓存对象
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// --- 位置属性 ---
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), 0);
glEnableVertexAttribArray(0);
// --- 颜色属性 ---
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), (void*)(3 * sizeof(GL_FLOAT)));
glEnableVertexAttribArray(1);
// 着色器
Shader shader("res/Basic.vert", "res/Basic.frag");
shader.Bind();
}
void display() {
// 绘制
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}
void clear() {
// 删除缓存对象
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &IBO);
}
// main
int main(void)
{
GLFWwindow* window;
/* Initialize the library */
if (!glfwInit())
return -1;
/* Create a windowed mode window and its OpenGL context */
window = glfwCreateWindow(640, 480, "Shader", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
/* Make the window's context current */
glfwMakeContextCurrent(window);
// glew init
if (glewInit() != GLEW_OK) {
std::cout << "glew init error!" << std::endl;
}
// print opengl version
std::cout << "OpenGL version :" << glGetString(GL_VERSION) << std::endl;
/* init */
init();
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
glClear(GL_COLOR_BUFFER_BIT);
/* display */
display();
/* Swap front and back buffers */
glfwSwapBuffers(window);
/* Poll for and process events */
glfwPollEvents();
}
glfwTerminate();
/* clear */
clear();
return 0;
}
运行结果:
GitHub
https://github.com/zGameDeveloper/LearnOpenGL/tree/main/Project/06_Shader
相关推荐
- 基于STM32的四旋翼飞行器控制系统设计
-
摘要:四旋翼飞行器控制系统的性能决定了飞行效果的优劣,如何改善飞行控制系统使其拥有更良好的表现成为近几年的研究热点。根据四旋翼飞行器的飞行原理,设计了一种新型四旋翼飞行器控制系统。该系统以STM32...
- 单片机差分升级(STM32,M0,M3,M4适用)
-
参考文档:https://blog.csdn.net/darling757267/article/details/80652267https://www.cnblogs.com/idreamo/p/9...
- STM32入门: Step3 UART简介(stm32的uart和usart)
-
Step3UART简介UART和新板介绍34分钟目标描述完成本教程后,您将:熟悉L475IoTNodeDiscovery板,了解如何在以下位置对RS232串行链路进行编程和使用:以前使...
- 如何使用GCC手动编译stm32程序(如何在gcc上编译并运行代码)
-
如何不使用任何IDE(集成开发环境)编译stm32程序?集成开发环境将编辑器、编译器、链接器、调试器等开发工具集成在一个统一的软件中,使得开发人员可以更加简单、高效地完成软件开发过程。如果我们不使用K...
- STM32单片机从零开始使用教程(二) 使用Cube搭建跑马灯工程并下载
-
通过cube建立工程RCC设置为外部晶振高速晶振对应的引脚会亮起进入clockconfiguration进行时钟配置,输入频率8M盒子PLLSourceMu改为使用外部时钟HSE,系统时钟Sys...
- 超详细的FreeRTOS移植全教程——基于stm32
-
准备在移植之前,我们首先要获取到FreeRTOS的官方的源码包。这里我们提供两个下载链接:一个是官网:http://www.freertos.org/另外一个是代码托管网站:https://sourc...
- 用ESP32和STM32设计了一块主控板,却用来养鱼?
-
前言我做了一个智能鱼缸系统的控制板。基于ESP32和STM32设计。成本不到200元。全文导航功能描述、电路设计图、主要模块选型、软件说明、获取开源资料、结语。功能描述①自动投食。②自动过滤供氧。③灯...
- STM32物联网套件基础版03-控制继电器
-
前言继电器是一个生活中比较常用的元器件,有了继电器,我们可以使用单片机输出的低电平控制高电平期间工作,比如继电器接到220V用电器上,可通过单片机智能控制用电器,本节我们开始正式学习如何使用继电器。一...
- STM32单片机详细教学(三):STM32单片机的开发方法
-
大家好,今天给大家介绍STM32单片机的开发方法,文章末尾附有本毕业设计的论文和源码的获取方式,可进群免费领取。前言经过前两章节对STM32的简单介绍,在接下来的几个章节中开始进行STM32单片机的软...
- 原来STM32单片机的开发如此的简单
-
大家好,我是华维今天我们讲下用STM32CubeMX和Keil5点亮一个LED,这个项目比较简单,大家都可以尝试下。这个就是我们今天的主角,这款单片机芯片是STM32F030K6T6。这个开发板非常简...
- STM32 F103 使用HAL库配置PVD(stm32l0 hal库)
-
PVD(ProgrammableVotageDetector),即可编程电压监测器PVD可以检测电压变化并触发中断,一般用于判断断电并进行数据保存工程是使用STM32CubeMx生成的,在ST...
- STM32F4芯片嵌入式学习7(stm32f4芯片包安装)
-
文档是本人学习总结的文档,有些乱,勿怪。1、STM32F407时钟系统在STM32F4中,有5个最重要的时钟源,为HSI、HSE、LSI、LSE、PLL。其中PLL实际是分为两个时钟源,...
- ARM和STM32,嵌入式是什么关系(arm嵌入式和单片机的区别)
-
ARM,STM和嵌入式的关系就是上图,其中ARM负责内核架构,指令集的设计,提供给IC设计厂商内核和编译器等支持(如ARM11,Cortex-M,Cortex-A系列处理器)STM32是基于ARM...
- STM32CUBEMX 使用教程2 — GPIO的使用、输入/输出
-
学习和使用任何一款MCU,最初开始往往都是从GPIO入手的,学会如何配置IO,能让IO输出想要的电平状态,能读取IO口的电平。本篇介绍一下STM32的GPIO。GPIO(General-Purpose...
- 入手STM32单片机的知识点总结(stm32单片机的介绍)
-
文章下方附学习资源,自助领取本文将以STM32F10x为例,对标准库开发进行概览。主要分为三块内容:STM32系统结构寄存器通过点灯案例,详解如何基于标准库构建STM32工程STM32系统结构上图,S...
- 一周热门
-
-
Python实现人事自动打卡,再也不会被批评
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
系统C盘清理:微信PC端文件清理,扩大C盘可用空间步骤
-
10款高性能NAS丨双十一必看,轻松搞定虚拟机、Docker、软路由
-
飞牛OS入门安装遇到问题,如何解决?
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- table.render (33)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)