OpenGL教程(五)

2023-11-03

前言

正如之前章节所提到的,着色器就是运行在GPU上的小程序,简单来说,着色器就是仅仅是一个将输入数据经过一定转换然后输出的过程,着色器之间是非常独立的,彼此之间除了输入输出之外没有其他交流,这篇文章将会详细介绍着色器以及编写着色器的语言GLSL。

GLSL

着色器是使用一种类似C语言的语言GLSL编写的,GLSL是为显卡使用而设计的,包含了很多针对向量和矩阵操作的特性。着色器一般以版本声明为开头,紧接着就是一些输入和输出的变量,uniform以及main函数,每一个着色器的入口都是main函数,在main函数中处理输入变量然后将处理的结果输出到输出变量,至于uinform,之后我们会详细介绍,一个典型的着色器的结构如下

#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
void main()
    {
    // process input(s) and do some weird graphics stuff
        ...
    // output processed stuff to output variable
        out_variable_name = weird_stuff_we_processed;
    }

当提及顶点着色器时我们都知道它的输入变量是顶点属性,硬件支持我们声明的顶点属性的数量有一个最大值,OpenGL规定至少有16个4分量的顶点属性可用,一些硬件可能会提供更多,这个值可以通过GL_MAX_VERTEX_ATTRIBS查询:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout<< "Maxium nr of vertex attributes supported: " <<nrAttributes<<std::endl

类型

与其他编程语言相同,GLSL也拥有数据类型去指定我们使用的数据的类型,GLSL提供了C语言提供的默认基本类型如int, float, double, uint以及bool。GLSL同样提供了两种容器类型vectors和matrices,matrices之后我们再讨论。

Vectors

GLSL的vector拥有1,2,3或4个之前提到的基本类型的分量,它们的形式有以下几种:

  • vecn:拥有n个浮点数的vector
  • bvecn:包含n个布尔值的vector
  • ivecn:包含n个整型的vector
  • uvecn:包含n个无符号整型的vector
  • dvecn:包含n个double的vector
    一般情况下vecn已经能够满足使用需求。

vector分量的获取有很多方式,也非常的灵活,例如vec.x就可以获取vector的第一个分量,使用.x, .y, .z 和.w可以分别获取vector的第一、第二、第三和第四个分量,同样的GLSL也允许通过rgba去获取颜色分量,通过stpq获取纹理的坐标,vector的赋值与计算也非常的灵活,就像这样:

vec2 someVec;
vec4 differentVec;
vec3 anotherVec = differentVec.zyw;
vec4 othorVec = someVec.xxxx + anotherVec.yxzy;

输入与输出

着色器本身是非常小的程序,但是是整个图形管线的一部分,所以每一个着色器都必须有输入和输出,GLSL提供了in和out关键字来指定输入和输出,一个着色器的输出将会是另一个着色器的输入。

顶点着色器需要接受特定形式的输入否则效率会特别低下,而且顶点着色器是直接从顶点数据获取输入,为了解决这个问题,可以通过指定变量的位置就像layout(location = 0)来说明顶点数据是如何组织的。

其实也可以通过glGetAttribLocation省略指定布局(layout(location=0)),但是还是建议写在顶点着色器中,这样易于理解,也可以减少我们和OpenGL的工作。

除了指定输入有时候我们还需要指定输出,例如片段着色器需要输出每一个像素的颜色,如果片段着色器没有输出颜色,OpenGL将会渲染成白色或黑色。所以如果想要从一个着色器向另一个着色器传递数据,我们需要在一个着色器声明输出,在一个着色器声明输入,当变量的类型和名字都相同时,OpenGL将会链接这些数据从而实现数据的传递(这个过程一般是在链接项目对象时实现的),为了说明这个过程,我们通过一下例子说明,在顶点着色器就定义颜色:
顶点着色器

#version 330 core
layout(location = 0) in vec3 aPos;

out vec4 vertexColor;

void main()
{
    gl_Postion = vec4(aPos, 1.0);
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0);
}

片段着色器

out vec4 FragColor;

in vec4 vertexColor;

void main() {
    FragColor = vertexColor;
}

Uniforms

uniform是另一种方式将CPU上的数据传递到GPU的着色器上,uniform与顶点属性差异很大,首先,uniform是全局的。全局则意味着在每一个着色器程序(shader program)中都是唯一的,其次无论给uniform设置了什么值,uniform都会保持这个值除非重置或更新这个值。

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor;

void main() {
    FragColor = ourColor;
}

我们需要使用uniform关键字去声明,从上面代码可以看出我们使用在着色器中使用uniform,并且使用uniform定义三角形的颜色,由于uniform是一个全局变量,我们可以在任意的着色器中去定义。

需要注意的是如果声明了一个uniform变量,但是GLSL代码中并没有使用,那么在编译阶段将会移除变量,这会引发严重错误。

那么该如何给一个uniform变量赋值呢,首先需要知道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);

首先我们需要通过glfwGetTime()获取到当时的运行时间(以秒为单位),然后通过sin函数将greenValue的值设置在0.0到1.0之间,然后通过glGetUniformLocation函数查询uniform变量的位置,其第一个参数是着色器程序,第二个参数是uinform变量的名称,如如果其返回值是-1则说明没有找到位置。然后通过glUniform4f函数来设置uniform变量的值。需要注意的是在查询uniform变量的位置时不需要使用该着色器程序,但是在更新uniform变量的之前需要使用着色器程序(通过调用glUseProgram),这是因为设置uniform变量是在当前活跃的着色器程序。

因为OpenGL是一个C库,所以不支持函数的重载,所以一个函数支持不同的数据类型,就需要为每一个数据类型定义一个新的函数,glUniform函数就是一个很好的例子,这个函数通过不同的后缀来区分不同类型的参数。

  • f:入参是一个float
  • i:入参是一个int
  • ui:入参是一个unsigned int
  • 3分:入参是3个float
  • fv:入参是一个float类型的vector或array
    我们使用的glUniform4f就是需要四个float类型入参。

现在我们已经知道了如何设置uniform变量的值,我们可以使用它们去渲染,如果我们想改变颜色,还需要在每一帧渲染的时候更新uniform,所以我们需要在渲染循环中计算和跟新greenValue。

while(!glfwWindowShouldClose(window)) {
    processInput(window);

    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(shaderProgram);

    float timeValue = glfwGetTime();
    float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
i   nt vertexColorLocation = glGetUniformLocation(shaderProgram,     "ourColor");
    glUseProgram(shaderProgram);
    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    glfwSwapBuffers(window);
    glfwPollEvents();
}

以上可以看到uinform是为每一帧动态设置属性很好的方法,但是如果我们想要为每一个顶点设置颜色呢,通过以上的方法可以需要为每一个顶点声明一个uniform变量,显然这种方法比较繁琐,更好的方式是在顶点属性中定义更多的数据。

更多的属性

之前的章节我们介绍了如何填充VBO、配置顶点属性指针以及将它们全部存储到VAO中,这一次我们也可以将颜色数据添加到顶点数据中,所以我们需要将3个浮点数颜色数据添加到顶点数组中。

float vertices[] = {
// positions         // colors
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
0.0f,  0.5f, 0.0f, 0.0f, 0.0f, 1.0f  // top
};

因为我们需要传递更多的数据给顶点着色器,所以我们需要调整顶点着色器去接受颜色值作为顶点属性的输入,以下代码则是修改后的顶点着色器,可以注意到,我们将颜色属性的layout设置为1。

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;

out vec3 ourColor;

void main() {
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
}

由于我们不在片段着色器中使用uniform而是使用ourColor变量设置颜色,所以我们也需要修改片段着色器的内容:

#version 330 core
out vec4 FragColor;
in vec3 ourColor;

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

因为我们新增了其他的顶点属性,我们需要配置顶点属性指针,在VBO中更新后的数据的组织情况如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tq9nMPUP-1668824944502)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54a8eb096643428585a03c683f11387b~tplv-k3u1fbpfcp-watermark.image?)]

已经知道了现在数据的布局,我们可以通过glVertexAttribPointer来格式化。

glVertexAttribPointer(0, 3, GL_FLOAT, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_POINTER, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

需要注意的是我们将位置属性的layout设置为0,颜色属性的layout设置为1,颜色属性在位置属性的厚片,位置属性是三个float,所以颜色属性的偏移量就是3 * sizeof(float)。

最后的结果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6vwocAEY-1668824944502)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5a6b7470da8c4e4ead8cb2f6749522a2~tplv-k3u1fbpfcp-watermark.image?)]

代码如下

# include<glad/glad.h>
# include<GLFW/glfw3.h>
# include<iostream>

void framebuffer_size_callback(GLFWwindow *window, int width, int height);

const unsigned int WIDTH = 800;
const unsigned int HEIGHT = 600;

const char *vertexShaderSource ="#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "layout (location = 1) in vec3 aColor;\n"
    "out vec3 ourColor;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos, 1.0);\n"
    "   ourColor = aColor;\n"
    "}\0";

const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "in vec3 ourColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(ourColor, 1.0f);\n"
    "}\n\0";

int main() {
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, "QStackOpenGL", NULL, NULL);
    if (window == NULL) {
        std::cout<<"creat window failed"<<std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout<<"failed to init glad"<<std::endl;
        glfwTerminate();
        return -1;
    }

    //定义顶点着色器
    unsigned int 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::COMPLIATION_FAILED\n"<<infoLog<<std::endl;
    }
    //定义片段着色器
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"<<infoLog<<std::endl;
    }

    //链接着色器
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    //检查错误
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout<<"ERROR""SHADER::PROGRAM::LINKING_FAILED\n" <<infoLog<<std::endl;
    }

    //删除着色器
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    //顶点数据
    float vertices[] = {
        // positions         // colors
         0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // bottom right
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // bottom left
         0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f   // top 

    };

    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    //先绑定VAO
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //位置数据
    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);

    glUseProgram(shaderProgram);

    while (!glfwWindowShouldClose(window))
    {
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);
        //因为我们只有一个三角形,不需要每次绑定VAO,为了看起更合理,我依然每次都绑定
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    //清除数据
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    glfwTerminate();
    return -1;

}

void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
        glViewport(0, 0, width, height);
    }

定义自己的着色器类

编写、编译和管理着色器十分的繁琐,为了方便管理我们通过建立一个着色器的类从硬盘读取着色器,然后编译并且链接它们,检查是否有错误,十分方便使用。

出于学习的目的,我们将着色器的类定义在一个头文件内,其结构如下

#ifndef SHADER_H
#define SHADER_H
#include <glad/glad.h> // include glad to get the required OpenGL headers
#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

着色器的类需要获取着色器项目的ID,它的构造函数需要顶点着色器和片段着色器源码文件的地址。

最后

这篇文章主要介绍了编写着色器的语言GLSL,更多内容可以关注公众号QStack。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

OpenGL教程(五) 的相关文章

随机推荐

  • 苹果cmsV10采集插件&&一键配置定时任务采集

    苹果cmsV10采集插件 下载地址 https pan baidu com s 1NuY0sTQbp CjGaXYH2fdsg 支持断点采集 支持添加播放器自定义解析接口 支持批量修改播放器接口 支持全网搜索资源 支持一键配置定时任务 安装
  • 介绍8421码,5421码,2421码

    8421码 5421码 2421码都是用编码中的bcd码组成的 而bcd码是用 4 位二进制来表示 1 位十进制 即使用 4 个位来存储一个十进制的值 使二进制和十进制之间得到快速转换 bcd码又可分为有权码和无权码两类 无权码包括了余3码
  • 一周AIGC丨国内首个估值 100 亿级大模型独角兽诞生,腾讯混元、蚂蚁金融大模型亮相...

    成立于 2019 年的智谱 AI 晋升为国内首个估值超 100 亿人民币的大模型领域独角兽 早一个月把电灯泡拿出来 不重要 的腾讯 最终还是免不了随大流 腾讯混元大模型正式推出 但用户还要排队申请 蚂蚁金融大模在外滩大会亮相 宣称解决产业真
  • C语言必背代码大全

    对于刚学计算机编程的同学来说 每一个编程知识都觉得很重要 下面小编为大家整理了c语言必背代码 希望大家喜欢 1 输出9 9口诀 共9行9列 i控制行 j控制列 include stdio h main int i j result for
  • 2014传智播客C++第三期基础班+就业班至9月份 完整版

    课程简介 第一阶段C语言10天 此阶段兼顾基础班升级学员与直接报就业班学员 突出面试辅导为主 1 常量与变量 数据类型 数据类型转换 数据输入与输出 面试辅导 2 C语言运算符 C语言操作符 C语言表达式 表达式优先级 面试辅导 3 C语言
  • 图的深度优先遍历DFS (邻接矩阵实现) c语言

    图的遍历是指从图中的某一顶点出发 按照一定的策略访问图中的每一个顶点 每个顶点有且只能被访问一次 深度优先遍历也叫深度优先搜索 Depth First Search 它的遍历规则 先选择一个初始顶点 再规定一个方向 例如往右边一直遍历 于是
  • canvas基本用法

    首先创建canvas元素
  • 在Windows下编译扩展OpenCV 3.4.2 + opencv_contrib

    请参考 https www cnblogs com jliangqiu2016 p 5597501 html 这里主要说一下注意点 1 编译过程中会提示缺少以下文件 vgg generated 48 i vgg generated 64 i
  • 欧拉角的详解

    转自 https blog csdn net schrodinger1900 article details 52734568 关于旋转永远是做游戏的难点和混乱点 我们知道表示一个旋转有多种方式 简单的欧拉角 复杂点的四元数 再复杂点的矩阵
  • PS 2023 24.7 Beta Ai 如何解决橙色错误弹窗问题?

    距离Adobe软件公司首次将图像编辑及数字绘画软件Photoshop推出到大众面前已经过去35年 最近该公司又再次书写了属于Photoshop的历史新篇章 Adobe 发布的 Photoshop Beta 新增 创意填充 Generativ
  • Linux操作系统shell指令详解

    shell指令基本概念 命令行提示符 ubuntu 用户名 分隔符 ubuntu 主机名 家目录 当前所在的路径 普通用户权限 root 切换用户 su 用户名 gt 切换到指定用户 su gt 默认切换到超级用户 sudo 加在指令前 g
  • 【Hyperledger Fabric 开发学习1】 环境搭建

    1 概览 准备工作 1 系统环境准备 Ubuntu 20 02 LTC 2 apt 更换镜像源 3 辅助工具安装 4 安装go 5 安装容器docker和docker compose Hyperledger Fabric安装 方法1 以bo
  • Unity常见平台汇总

    UNITY EDITOR Unity编辑器 UNITY EDITOR WIN Windows 操作系统 UNITY EDITOR OSX macos操作系统 UNITY STANDALONE OSX 专门为macos 包括Universal
  • std::atomic和std::mutex区别

    std atomic介绍 模板类std atomic是C 11提供的原子操作类型 头文件 include
  • (附源码)node.js蒲公英旅游系统 毕业设计15565

    nodejs 蒲公英旅游系统 摘 要 随着社会的发展 社会的各行各业都在利用信息化时代的优势 计算机的优势和普及使得各种信息系统的开发成为必需 蒲公英旅游系统设计 主要的模块包括查看后台首页 轮播图 轮播图管理 公告管理 公告 资源管理 旅
  • 华为鸿蒙电脑操作系统测试版,华为鸿蒙OS测试

    软件介绍 华为鸿蒙OS测试平台是一款华为测试手机新系统的测试平台 这里可以让申请的用户最新体验华为的最新鸿蒙系统 让数百万用户去发现华为系统的不足 然后华为官方进行优化 我相信很多人都愿意做这个小白鼠 快来关注吧 华为鸿蒙OS测试软件简介
  • 【满分】【华为OD机试真题2023 JAVA&JS】Excel单元格数值统计

    华为OD机试真题 2023年度机试题库全覆盖 刷题指南点这里 Excel单元格数值统计 知识点递归循环数组 时间限制 2s 空间限制 256MB 限定语言 不限 题目描述 Excel工作表中对选定区域的数值进行统计的功能非常实用 仿照Exc
  • 使用Simulink进行stm32开发2

    使用Simulink进行stm32开发2 小车直流电机控制 1 配置工程文件 simulink 模块搭建 stm32驱动模块 配置数据字典 配置电机控制函数 模块封装 加入输入并生成代码 基于对模型开发的学习 在这里用simulink搭建小
  • MyBatis 使用数组作为参数

  • OpenGL教程(五)

    前言 正如之前章节所提到的 着色器就是运行在GPU上的小程序 简单来说 着色器就是仅仅是一个将输入数据经过一定转换然后输出的过程 着色器之间是非常独立的 彼此之间除了输入输出之外没有其他交流 这篇文章将会详细介绍着色器以及编写着色器的语言G