基于C++的OpenGL 02 之着色器
1. 概述
本文基于C++语言,描述OpenGL的着色器
环境搭建以及绘制流程可参考:
笔者这里不过多描述每个名词、函数和细节,更详细的文档可以参考:
2. 着色器
着色器(Shader)是运行在GPU上的小程序,这些小程序为图形渲染管线的某个特定部分而运行
从基本意义上来说,着色器只是一种把输入转化为输出的程序
着色器也是一种非常独立的程序,因为它们之间不能相互通信,它们之间唯一的沟通只有通过输入和输出
着色器语言(英语:Shader Language)也叫着色语言(英语:Shading Language),是一类专门用来为着色器编程的编程语言
Shader Language目前主要有3种语言:
- 基于 OpenGL 的 OpenGL Shading Language,简称 GLSL
- 基于 DirectX 的 High Level Shading Language,简称 HLSL
- 还有 NVIDIA 公司的 C for Graphic,简称 Cg 语言
3. GLSL
OpenGL着色器是使用一种叫GLSL的类C语言写成的,为图形计算量身定制的,包含一些针对向量和矩阵操作的有用特性
着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数
每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中
uniform可以理解为globe,即全局变量
一个典型的OpenGL着色器的GLSL结构如下:
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
int main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = result_we_processed;
}
4. 数据类型
GLSL包含的基础变量类型:
- int
- uint
- float
- double
- bool
GLSL包含的容器类型:
- Vector(向量)
- Matrix(矩阵)
4.1 向量
GLSL中的向量可以是包含2-4个分量的容器,分量的类型可以是任意基础类型
向量的类型大致可以表示为:<基础类型首字母>+vec+<分量个数>
如包含3个double分量的向量:dvec3
例外的是float类型,float向量是GLSL中最常用的变量,其类型为:vec+<分量个数>
向量的分量支持.x、.y、.z的方式获取,向量支持重组,例如:
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
5. 输入与输出
OpenGL中着色器是独立的小程序,每个着色器具有输入与输出,从而进行数据传递
GLSL定义了in和out两个关键字实现这种流程
前面的着色器使用out定义的变量会传递到后面的着色器中用in声明且类型与变量名相同的变量
例外的是:
- 顶点着色器使用
layout (location = <n>)指定输入变量(也可以用glGetAttribLocation()查询属性位置) - 片段着色器需要
vec4类型的颜色输出
综述就是,一般而言:
in和out两个关键字主要用于顶点着色器与片段着色器的数据传递- 顶点着色器需要使用
location进行输入,输出中要指定gl_Position - 片段着色器需要指定输出颜色
一个简单的示例代码如下:
顶点着色器:
#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(1.0, 0.0, 0.0, 1.0); // 把输出变量设置为红色
}
片段着色器:
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
FragColor = vertexColor;
}
这里只是修改GLSL,环境代码在这片文章的结尾:
实现的效果:

6. Uniform
in和out两个关键字主要用于着色器的数据传递,Uniform则主要用于CPU与GPU的数据传递
Uniform是全局变量,变量名不可重复,可以被任何着色器随时读取
以下是在片段着色器中声明一个Uniform的颜色变量:
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
FragColor = ourColor;
}
接下来是在渲染中动态设置这个Uniform变量的代码:
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
一切正常的话,将会出现一个三角形逐渐由绿变黑再变回绿色:

一些想法:
- 既然说Uniform是全局变量,那是否可以在没有声明Uniform的着色器中访问呢?经过笔者的实验,似乎不可以
7. 绑定更多属性
利用Uniform可以实现颜色从CPU到GPU的传递,但是变量多时声明很多Uniform就不那么合适,另一个方案是将颜色属性绑定到顶点数据中,从顶点着色器传递到片段着色器中,代码如下
设置顶点与对应的颜色:
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
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
在片段着色器中接收颜色属性:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}
绑定属性:
现在的VBO内存布局如下:

绑定VBO的顶点格式:
// 位置属性
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);
运行程序结果:

8. 封装着色器
着色器程序的生成步骤大致都是:
- 编写GLSL
- 创建Shader
- 加载GLSL
- 编译Shader
- 创建着色器程序
- 附加Shader到着色器程序
- 链接着色器程序
封装一个着色器类,可以有效简化创建一个着色器程序的步骤
这是封装的Shader.hpp:
#ifndef SHADER_HPP
#define SHADER_HPP
#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)
{
// 1. 从文件路径中获取顶点/片段着色器
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// 保证ifstream对象可以抛出异常:
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
// 打开文件
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// 读取文件的缓冲内容到数据流中
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_SUCCESFULLY_READ" << std::endl;
}
const char *vShaderCode = vertexCode.c_str();
const char *fShaderCode = fragmentCode.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_FAILED\n"
<< infoLog << std::endl;
};
// 片段着色器也类似
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
// 打印编译错误(如果有的话)
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragment, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\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_FAILED\n"
<< infoLog << std::endl;
}
// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);
}
// 使用/激活程序
void use()
{
glUseProgram(ID);
}
// uniform工具函数
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);
}
};
#endif
以下是一个简单的调用程序test.cpp:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <math.h>
#include "Shader.hpp"
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void process_input(GLFWwindow *window);
unsigned int *renderInit();
void render(unsigned int shaderProgram, unsigned int VAO);
bool checkCompile(unsigned int shader);
bool checkProgram(unsigned int shaderProgram);
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow *window = glfwCreateWindow(800, 600, "hello triangle", nullptr, nullptr);
if (window == nullptr)
{
std::cout << "Faild to create window" << std::endl;
glfwTerminate();
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Faild to initialize glad" << std::endl;
return -1;
}
glad_glViewport(0, 0, 800, 600);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
unsigned int *arr = renderInit();
while (!glfwWindowShouldClose(window))
{
process_input(window);
// render
std::cout << arr[0] << " " << arr[1] << " " << arr[2] << std::endl;
render(arr[0], arr[1]);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteProgram(arr[0]);
glDeleteVertexArrays(1, &arr[1]);
glDeleteBuffers(1, &arr[2]);
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
glViewport(0, 0, width, height);
}
void process_input(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
unsigned int *renderInit()
{
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
float vertices[] = {
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f};
unsigned int 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(float), (void *)0);
glEnableVertexAttribArray(0);
Shader shaderProgram = Shader("test.vs", "test.fs");
shaderProgram.use();
return new unsigned int[3]{shaderProgram.ID, VAO, VBO};
}
void render(unsigned int shaderProgram, unsigned int VAO)
{
glClearColor(0.2, 0.3, 0.3, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
test.vs如下:
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
void main()
{
gl_Position = vec4(aPos, 1.0);
}
test.fs如下:
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
9. 参考资料
[1]着色器 - LearnOpenGL CN (learnopengl-cn.github.io)
[3]三大 Shader 编程语言(CG/HLSL/GLSL) - 知乎 (zhihu.com)
[4]OpenGL学习笔记(四)着色器 - 知乎 (zhihu.com)
[5]着色器语言 GLSL (opengl-shader-language)入门大全 - 善未易明 - 博客园 (cnblogs.com)
基于C++的OpenGL 02 之着色器的更多相关文章
- OpenGL官方教程——着色器语言概述
OpenGL官方教程——着色器语言概述 OpenGL官方教程——着色器语言概述 可编程图形硬件管线(流水线) 可编程顶点处理器 可编程几何处理器 可编程片元处理器 语言 可编程图形硬件管线(流水线) ...
- OpenGL之shader着色器的应用,三色渐变的三角形
学习自: https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/#_7 首先放一张效果图: 本次教程,将着色器单独定 ...
- OpenGl中使用着色器的基本步骤及GLSL渲染简单示例
OpenGL着色语言(OpenGL Shading Language,GLSL)是用来在OpenGL中着色编程的语言,是一种具有C/C++风格的高级过程语言,同样也以main函数开始,只不过执行过程是 ...
- 基于Unity实现油画风格的着色器
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "Cust ...
- 基于Unity实现像素化风格的着色器
Shader "MyShaderTest/SimplePixelationShader" { Properties { _MainTex ("Base (RGB)&quo ...
- 【OPENGL】第三篇 着色器基础(一)
在这一章,我们会学习什么是着色器(Shader),什么是着色器语言(OpenGL Shading Language-GLSL),以及着色器怎么和OpenGL程序交互. 首先我们先来看看什么叫着色器. ...
- Android OpenGL ES 开发(八): OpenGL ES 着色器语言GLSL
前面的文章主要是整理的Android 官方文档对OpenGL ES支持的介绍.通过之前的文章,我们基本上可以完成的基本的形状的绘制. 这是本人做的整理笔记: https://github.com/re ...
- 编写Unity3D着色器的三种方式
不管你会不会写Unity3D的shader,估计你会知道,Unity3D编写shader有三种方式,这篇东西主要就是说一下这三种东西有什么区别,和大概是怎样用的. 先来列一下这三种方式: fixed ...
- Unity3D - 图形性能优化:优化着色器载入时间
Unity官方文档之"图形性能优化-优化着色器载入时间"的翻译,E文链接. Optimizing Shader Load Time 优化着色器载入时间 Shaders are sm ...
- [GEiv]第七章:着色器 高效GPU渲染方案
第七章:着色器 高效GPU渲染方案 本章介绍着色器的基本知识以及Geiv下对其提供的支持接口.并以"渐变高斯模糊"为线索进行实例的演示解说. [背景信息] [计算机中央处理器的局限 ...
随机推荐
- python 爬取豆瓣电影评论,并进行词云展示
python 爬取豆瓣电影评论,并进行词云展示 本文旨在提供爬取豆瓣电影<我不是药神>评论和词云展示的代码样例 1.分析URL 2.爬取前10页评论 3.进行词云展示 1.分析URL 我不 ...
- 一步步带你设计MySQL索引数据结构
前言 MySQL的索引是一个非常重要的知识点,也基本上是面试必考的一个技术点,所以非常重要.那你了解MySQL索引的数据结构是怎么样的吗?为什么要采用这样的数据结构? 现在化身为MySQL的架构师,一 ...
- Chaos 测试下的若干 NebulaGraph Raft 问题分析
Raft 是一种广泛使用的分布式共识算法.NebulaGraph 底层采用 Raft 算法实现 metad 和 storaged 的分布式功能.Raft 算法使 NebulaGraph 中的 meta ...
- http转成https工具类
工具类代码如下: 点击查看代码 package com.astronaut.auction.modules.oss.utils; import org.apache.commons.collectio ...
- Vue DevUI v1.4 版本发布:从体验、效率、质量三个方面做了全方位的优化🎉
2022年9月1日,我们正式宣布 Vue DevUI 组件库发布 v1.0 版本. Vue DevUI 1.0 正式发布 经过100多天的持续迭代,我们正式发布 v1.4.0 版本,共新增: 11位贡 ...
- axios 中get 和post传参
axios中get和ppost传参的方式: params是添加到url的请求字符串中的,一般用于get请求. data是添加到请求体(body)中的, 一般用于post请求. 上面,只是一般情况. 其 ...
- Git强制覆盖master
场景 由于公司的项目中,有一个开发分支(这里假设dev)是一个严重偏离master,需要我去强制覆盖master. 问题 这个场景带来了两个问题: master是受保护不能强推 dev分 ...
- 【Java】线程池梳理
[Java]线程池梳理 前言 线程池:本质上是一种对象池,用于管理线程资源.在任务执行前,需要从线程池中拿出线程来执行.在任务执行完成之后,需要把线程放回线程池.通过线程的这种反复利用机制,可以有效地 ...
- 使用Zolom内存解析运行python脚本(不落地)
在目标机器运行python工具 好多工具都是python写的,如果目标机器是linux的话自带python环境可以很方便的运行这些工具,但是windows下是不自带python环境的,所以一种办法是直 ...
- 2023牛客寒假算法基础集训营2 ABCDEFHJKL
比赛链接 A 题解 知识点:数学. 用 \(n\) 减去区间1的端点得到匹配的一个区间,求一下与区间2的交集. 一个小公式,两区间 \([L_1,R_1]\) 和 \([L_2,R_2]\) 的交集长 ...