着色器
顶点着色器
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 是一种从 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设置为我们从顶点数据那里得到的输入颜色 }
|
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
| 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 |
超出的坐标为用户指定的边缘颜色。 |
纹理加载使用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 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个互相正交的向量。
向量A和B的叉积: