LearnOpenGL 1.4 着色器

来源:互联网 发布:jquery 对象添加数据 编辑:程序博客网 时间:2024/06/12 02:48

着色器shaders

本文整理自LearnOpenGL 及LearnOpenGL CN ,后者为前者的中文版, 完整学习的话建议前往原网站。

着色器只是一种运行在GPU上,把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。

GLSL(OpenGL Shader Language)

类似C的语言,必备组成部分:声明版本,输入和输出变量、uniform和main函数(main函数只能为void)。

#version version_numberin type in_variable_name;in type in_variable_name;out type out_variable_name;uniform type uniform_name;void main(){  // 处理输入并进行一些图形操作  ...  // 输出处理过的结果到输出变量  out_variable_name = weird_stuff_we_processed;}

顶点着色器中,输入变量又叫顶点属性(Vertex Attribute),可以声明的顶点属性是有限的,与硬件有关
获取顶点属性:

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

通常为16个。

数据类型

包含普通语言的大部分默认数据类型,intfloatdoubleuintbool
容器类型,向量(Vector)和矩阵(Matrix)。

向量

类型 含义 vecn 包含n个float分量的默认向量 bvecn 包含n个bool分量的向量 ivecn 包含n个int分量的向量 uvecn 包含n个unsigned int分量的向量 dvecn 包含n个double分量的向量

vecn已经足够用了

可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4个分量。GLSL也允许你对颜色使用rgba,或是对纹理坐标使用stpq访问相同的分量

向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)

vec2 vect = vec2(0.5f, 0.7f);vec4 result = vec4(vect, 0.0f, 0.0f);vec4 otherResult = vec4(result.xyz, 1.0f);

输入与输出

着色器之间都是独立的小程序模块,连成一个整体,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。关键字inout可以实现这个目的。只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。

顶点着色器比较特殊,它从顶点数据中直接读取数据,location这一元数据指定输入变量,
layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。

也可以忽略`layout (location = 0)`标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),这样可以节省你(和OpenGL)的工作量。

另一个例外是片段着色器,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色,如果每天定义,则会渲染成黑色或(白色)。

链接例子
顶点着色器

#version 330 corelayout (location = 0) in vec3 position; // position变量的属性位置值为0out vec4 vertexColor; // 为片段着色器指定一个颜色输出void main(){    gl_Position = vec4(position, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数    vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出变量设置为暗红色}

gl_Position为内建变量,表示变换后点的空间位置。顶点着色器从应用程序中获得原始的顶点位置数据,这些原始的顶点数据在顶点着色器中经过平移、旋转、缩放等数学变换后,生成新的顶点位置。新的顶点位置通过在顶点着色器中写入gl_Position传递到渲染管线的后继阶段继续处理。

片段着色器

#version 330 corein vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)out vec4 color; // 片段着色器输出的变量名可以任意命名,类型必须是vec4void main(){    color = vertexColor;}

结果

Uniform

全局的(Global):可理解为c的全局变量,允许各个着色器随时访问,

警告:如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!

GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); 寻找着色器中uniform的位置
glUniform()设置uniform值

因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数,glUniform后面分别跟f、i、ui、3f、fv时表示需要一个float、int、unsigned int、3个float、float向量。

例子

while(!glfwWindowShouldClose(window)){    // 检测并调用事件    glfwPollEvents();    // 渲染    // 清空颜色缓冲    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);    glClear(GL_COLOR_BUFFER_BIT);    // 记得激活着色器    glUseProgram(shaderProgram);    // 更新uniform颜色    GLfloat timeValue = glfwGetTime();    GLfloat greenValue = (sin(timeValue) / 2) + 0.5;    GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);    // 绘制三角形    glBindVertexArray(VAO);    glDrawArrays(GL_TRIANGLES, 0, 3);    glBindVertexArray(0);}

代码地址

更多属性

把颜色加进顶点数组中,把三角形的三个角分别指定为红色、绿色和蓝色:

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    // 顶部};

调整顶点着色器
layout把color属性的位置设置为1

#version 330 corelayout (location = 0) in vec3 position; // 位置变量的属性位置值为 0 layout (location = 1) in vec3 color;    // 颜色变量的属性位置值为 1out vec3 ourColor; // 向片段着色器输出一个颜色void main(){    gl_Position = vec4(position, 1.0);    ourColor = color; // 将ourColor设置为我们从顶点数据那里得到的输入颜色}

使用outColor代替uniform传递颜色
修改片段着色器

#version 330 corein vec3 ourColor;out vec4 color;void main(){    color = vec4(ourColor, 1.0f);}

因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:

知道了现在使用的布局,我们就可以使用glVertexAttribPointer函数更新顶点
格式,

// 位置属性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);

由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。
同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3 * sizeof(GLfloat),用字节来计算就是12字节。

结果:

这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。
这正是在这个三角形中发生了什么。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50000左右的片段,片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了:红首先变成到紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上。

编写自己的着色器类

从磁盘文件读取着色器
定义类结构

#ifndef SHADER_H#define SHADER_H#include <string>#include <fstream>#include <sstream>#include <iostream>#include <GL/glew.h>; // 包含glew来获取所有的必须OpenGL头文件class Shader{public:    // 程序ID    GLuint Program;    // 构造器读取并构建着色器    Shader(const GLchar* vertexPath, const GLchar* fragmentPath);    // 使用程序    void Use();};#endif

实现文件

Shader(const GLchar* vertexPath, const GLchar* fragmentPath){    // 1. 从文件路径中获取顶点/片段着色器    std::string vertexCode;    std::string fragmentCode;    std::ifstream vShaderFile;    std::ifstream fShaderFile;    // 保证ifstream对象可以抛出异常:    vShaderFile.exceptions(std::ifstream::badbit);    fShaderFile.exceptions(std::ifstream::badbit);    try     {        // 打开文件        vShaderFile.open(vertexPath);        fShaderFile.open(fragmentPath);        std::stringstream vShaderStream, fShaderStream;        // 读取文件的缓冲内容到流中        vShaderStream << vShaderFile.rdbuf();        fShaderStream << fShaderFile.rdbuf();               // 关闭文件        vShaderFile.close();        fShaderFile.close();        // 转换流至GLchar数组        vertexCode = vShaderStream.str();        fragmentCode = fShaderStream.str();         }    catch(std::ifstream::failure e)    {        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;    }    const GLchar* vShaderCode = vertexCode.c_str();    const GLchar* fShaderCode = fragmentCode.c_str();

检查是否失败,打印错误日志

// 2. 编译着色器GLuint vertex, fragment;GLint success;GLchar 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;};// 片段着色器也类似[...]// 着色器程序this->Program = glCreateProgram();glAttachShader(this->Program, vertex);glAttachShader(this->Program, fragment);glLinkProgram(this->Program);// 打印连接错误(如果有的话)glGetProgramiv(this->Program, GL_LINK_STATUS, &success);if(!success){    glGetProgramInfoLog(this->Program, 512, NULL, infoLog);    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;}// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了glDeleteShader(vertex);glDeleteShader(fragment);

Use()函数

void Use(){    glUseProgram(this->Program);}

调用

Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.frag");...while(...){    ourShader.Use();    glUniform1f(glGetUniformLocation(ourShader.Program, "someUniform"), 1.0f);    DrawStuff();}
0 0