GuoXin Li's Blog

Learnopengl-note

字数统计: 2.2k阅读时长: 9 min
2021/04/01 Share

着色器

顶点着色器

vertex shader

如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。

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

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

#version 330 core 声明版本和核心模式

in , out关键字,每个着色器都有输入和输出进行数据交换。

layout (location = 0) 设定了输入变量的位置值。

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

每个输入变量也叫做 顶点属性 vertex attribute,OpenGL 取保至少 16 个包含 4 分量的顶点属性可用。

GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。

GLSL两种容器类型:vector 和 matrix

如果打算从一个着色器向另一个着色器发送数据,必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。

顶点着色器

1
2
3
4
5
6
7
8
9
10
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0

out vec4 vertexColor; // 为片段着色器指定一个颜色输出

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

片段着色器

1
2
3
4
5
6
7
8
9
#version 330 core
out vec4 FragColor;

in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

void main()
{
FragColor = vertexColor;
}

Uniform

uniform 是一种从 CPU 中的应用向 GPU 中的着色器发送数据的方式。

uniform 是全局的,全局意味着 uniform 变量:

  • 必须在每个着色器程序对象中都是独一无二的
  • 它可以被着色器程序的任意着色器在任意阶段访问
  • 无论 uniform 被设置为什么,uniform 会一直保存它们的数据,直到它们被重置或者更新。
1
2
3
4
5
6
7
#version 330 core
out vec4 FragColor;

uniform vec4 ourColor;
void main(){
FragColor = ourColor;
}

声明一个 uniform vec4 的ourColor 全局变量,并在片段着色器的输出颜色设置为 uniform 的值。

因为 uniform 是全局变量,而无需通过顶点着色器作为中介

如果声明了 uniform却未曾使用,编译器会默认移除此变量,导致最后编译出的版本中不会包含它

给着色器中的 uniform 属性添加索引/位置,和数据

1
2
3
4
5
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()获取运行的秒数。

glGetUniformLocation 查询 uniform 中 ourColor 的位置值,如果返回 -1 代表没有找到这个值。

glUniform4f 函数设置 uniform 的值。

注意:在更新 uniform 之前必须首先使用此着色器程序,通过调用glUseProgram

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
while(!glfwWindowShouldClose(window))
{
//input
processInput(window);

//render
// clear color buffer
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

//activate shader
glUseProgram(shaderProgram);

//update uniform color
float timeValue = glfwGetTime();
float greenValue = sin(timeValue) / 2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUnifrom4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

//draw triangle
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

//exchange buffer and look up IO event
glfwSwapBuffers(window);
glfwPollEvents();
}

多个属性

1
2
3
4
5
6
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 // 顶部
}

设置 layout 标识符将 aColor 属性位置设置为1

1
2
3
4
5
6
7
8
9
10
11
#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设置为我们从顶点数据那里得到的输入颜色
}

image-20210404230945377

glVertexAttribPointer 来更新顶点格式

1
2
3
4
5
6
//position property
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//color property
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);

自己的着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>


class Shader
{
public:
// 程序ID
unsigned int ID;

// 构造器读取并构建着色器
Shader(const GLchar* vertexPath, const GLchar* 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

工具函数:use 来激活着色器程序,set 函数能够查询一个 unform 的位置并设置它的值。

从文件读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;
}
// vertexCode 转为 char *
// c_str()
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
[...]

编译和链接着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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;
};

// 片段着色器也类似
[...]

// 着色器程序
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);

纹理

我们可以为每个顶点添加颜色来增加图形细节,但是如果想要指定足够多的顶点,就需要足够多的颜色,开销会很大。

纹理环绕方式

环绕方式 描述
GL_REPEAT 对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。

Screen Shot 2021-04-10 at 22.31.44

纹理加载使用stb_image.h库进行加载

要使用stb_image.h加载图片,需要使用它的stbi_load函数:

1
2
int width, height, nrChannels;
unsigned char *data = stbi_load("image.jpg", &width, &height, &nrChannels, 0);

生成纹理

与生成的OpenGL对象一样,纹理也是使用ID进行引用的

1
2
unsigned int texture;
glGenTextures(1, &texture);

之后就是绑定纹理:

1
glBindTexture(GL_TEXTURE_2D, texture);

然后使用之前载入的图片数据生成一个纹理,纹理可以通过glTexImage2D来生成:

1
2
glTexImage2D(GL_TEXTURE2D, 0, GL_RGB, width, height, 0, GL_RGB,GL_UNSIGNED_BYTE,data);
glGenerateMipmap(GL_TEXTURE_2D);

参数含义:

  • 指定纹理目标
  • 为纹理置顶多级渐远纹理的级别,0为基本级别
  • 告诉OpenGL我们希望纹理储存为何种格式,我们的图像为RGB值格式
  • 图源格式和数据类型,并把它们储存为 char(byte) 数组
  • 真正的图像数据

生成了纹理和相应的多级渐远纹理后,释放图像的内存是一个好的习惯。

1
stbi_image_free(data);

完整的纹理生成代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);

变换

叉乘

叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量。

Screen Shot 2021-04-12 at 23.34.50

向量A和B的叉积:

image-20210412233726028

CATALOG
  1. 1. 着色器
    1. 1.1. 顶点着色器
    2. 1.2. 片段着色器
    3. 1.3. Uniform
    4. 1.4. 多个属性
    5. 1.5. 从文件读取
  2. 2. 纹理
    1. 2.1. 纹理加载使用stb_image.h库进行加载
    2. 2.2. 生成纹理
    3. 2.3. 完整的纹理生成代码
  3. 3. 变换
    1. 3.1. 叉乘