当我试图通过www.learnopengl.com上的 openGL wiki 和教程时,直觉永远无法理解整个概念是如何工作的。有人可以用更抽象的方式向我解释它是如何工作的吗?什么是顶点着色器和片段着色器,我们用它们做什么?
3 回答
OpenGL wiki给出了一个很好的定义:
着色器是一种用户定义的程序,旨在运行在图形处理器的某个阶段。
历史课
过去,显卡是执行一组固定算法的非可编程硅片:
- 输入:三角形的 3D 坐标、它们的颜色、光源
- 输出:二维图像
全部使用单个固定参数化算法,通常类似于Phong 反射模型。图片来自维基:
但这对于想要创建许多不同的复杂视觉效果的程序员来说限制太大了。
因此,随着半导体制造技术的进步,以及 GPU 设计人员能够在每平方毫米中压缩更多晶体管,供应商开始允许渲染管道的某些部分使用类似 C 的GLSL等编程语言进行编程。
然后将这些语言转换为半未记录的指令集,这些指令集在内置于这些较新 GPU 的小型“CPU”上运行。
一开始,那些着色器语言甚至都不是图灵完备的!
通用 GPU (GPGPU)一词是指现代 GPU 的这种增强的可编程性。
现代着色器管道概述
在 OpenGL 4 模型中,只有下图中的蓝色阶段是可编程的:
图片来源。
着色器从前一个管道阶段获取输入(例如顶点位置、颜色和光栅化像素),并将输出定制到下一个阶段。
最重要的两个是:
顶点着色器:
- 输入:点在 3D 空间中的位置
- 输出:点的 2D 投影(使用4D 矩阵乘法)
这个相关示例更清楚地显示了投影是什么:How to use glOrtho() in OpenGL?
片段着色器:
- 输入:三角形所有像素的二维位置+(边缘或纹理图像的颜色)+照明参数
- 输出:三角形每个像素的颜色(如果它没有被另一个更近的三角形遮挡),通常在顶点之间插值
这些片段是从先前计算的三角形投影中离散化的,请参见:
相关问题:什么是顶点和像素着色器?
从这里我们看到,“着色器”这个名字对于当前的架构来说并不是很有描述性。这个名字当然来源于“阴影”,它由我们现在所说的“片段着色器”处理。但是 GLSL 中的“着色器”现在也像顶点着色器一样管理顶点位置,更不用说 OpenGL 4.3GL_COMPUTE_SHADER
了,它允许进行与渲染完全无关的任意计算,就像 OpenCL 一样。
TODO 能否仅使用 OpenCL 有效地实现 OpenGL,即,使所有阶段都可编程?当然,必须在性能/灵活性之间进行权衡。
第一个带有着色器的 GPU 甚至使用不同的专用硬件进行顶点和片段着色,因为它们具有完全不同的工作负载。然而,当前架构对所有着色器类型使用单一类型的硬件(基本上是小型 CPU)的多次传递,这节省了一些硬件重复。这种设计被称为统一着色器模型:
源代码示例
要真正了解着色器及其所能做的一切,您必须查看许多示例并学习 API。例如https://github.com/JoeyDeVries/LearnOpenGL是一个很好的来源。
在现代 OpenGL 4 中,即使是 hello world 三角形程序也使用超级简单的着色器,而不是像glBegin
和glColor
.
考虑这个三角形 hello world 示例,它在单个程序中同时具有着色器和即时版本:https ://stackoverflow.com/a/36166310/895245
主程序
#include <stdio.h>
#include <stdlib.h>
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#define INFOLOG_LEN 512
static const GLuint WIDTH = 512, HEIGHT = 512;
/* vertex data is passed as input to this shader
* ourColor is passed as input to the to the fragment shader. */
static const GLchar* vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec3 color;\n"
"out vec3 ourColor;\n"
"void main() {\n"
" gl_Position = vec4(position, 1.0f);\n"
" ourColor = color;\n"
"}\n";
static const GLchar* fragmentShaderSource =
"#version 330 core\n"
"in vec3 ourColor;\n"
"out vec4 color;\n"
"void main() {\n"
" color = vec4(ourColor, 1.0f);\n"
"}\n";
GLfloat vertices[] = {
/* Positions Colors */
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
};
int main(int argc, char **argv) {
int immediate = (argc > 1) && argv[1][0] == '1';
/* Used in !immediate only. */
GLuint vao, vbo;
GLint shaderProgram;
glfwInit();
GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, __FILE__, NULL, NULL);
glfwMakeContextCurrent(window);
glewExperimental = GL_TRUE;
glewInit();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glViewport(0, 0, WIDTH, HEIGHT);
if (immediate) {
float ratio;
int width, height;
glfwGetFramebufferSize(window, &width, &height);
ratio = width / (float) height;
glClear(GL_COLOR_BUFFER_BIT);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(-ratio, ratio, -1.f, 1.f, 1.f, -1.f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glBegin(GL_TRIANGLES);
glColor3f( 1.0f, 0.0f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
glColor3f( 0.0f, 1.0f, 0.0f);
glVertex3f( 0.5f, -0.5f, 0.0f);
glColor3f( 0.0f, 0.0f, 1.0f);
glVertex3f( 0.0f, 0.5f, 0.0f);
glEnd();
} else {
/* Build and compile shader program. */
/* Vertex shader */
GLint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
GLint success;
GLchar infoLog[INFOLOG_LEN];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, INFOLOG_LEN, NULL, infoLog);
printf("ERROR::SHADER::VERTEX::COMPILATION_FAILED\n%s\n", infoLog);
}
/* Fragment shader */
GLint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(fragmentShader, INFOLOG_LEN, NULL, infoLog);
printf("ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n%s\n", infoLog);
}
/* Link shaders */
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, INFOLOG_LEN, NULL, infoLog);
printf("ERROR::SHADER::PROGRAM::LINKING_FAILED\n%s\n", infoLog);
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
/* Position attribute */
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
/* Color attribute */
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
glUseProgram(shaderProgram);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
}
glfwSwapBuffers(window);
/* Main loop. */
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
if (!immediate) {
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &vbo);
glDeleteProgram(shaderProgram);
}
glfwTerminate();
return EXIT_SUCCESS;
}
在 Ubuntu 20.04 上编译并运行:
sudo apt install libglew-dev libglfw3-dev
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lGL -lGLEW -lglfw
# Shader
./main.out
# Immediate
./main.out 1
两者的结果相同:
从中我们可以看到:
顶点和片段着色器程序在 CPU 上运行的常规 C 程序中表示为包含 GLSL 语言 (
vertexShaderSource
和) 的 C 样式字符串fragmentShaderSource
这个 C 程序进行 OpenGL 调用,将这些字符串编译成 GPU 代码,例如:
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);
着色器定义它们的预期输入,C 程序通过指向 GPU 代码的内存指针提供它们。例如,片段着色器将其预期输入定义为顶点位置和颜色的数组:
"layout (location = 0) in vec3 position;\n" "layout (location = 1) in vec3 color;\n" "out vec3 ourColor;\n"
并且还将其输出之一定义
ourColor
为颜色数组,然后成为片段着色器的输入:static const GLchar* fragmentShaderSource = "#version 330 core\n" "in vec3 ourColor;\n"
然后,C 程序将包含顶点位置和颜色的数组从 CPU 提供给 GPU
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
然而,在直接的非着色器示例中,我们看到进行了显式给出位置和颜色的魔术 API 调用:
glColor3f( 1.0f, 0.0f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
因此,我们理解这代表了一个更受限制的模型,因为位置和颜色不是内存中任意用户定义的数组,然后再由任意用户提供的程序处理,而只是输入到类似 Phong 的模型中。
在这两种情况下,渲染的输出通常直接进入视频,而不通过 CPU,尽管可以读取到 CPU,例如如果你想将它们保存到文件中:How to use GLUT/OpenGL to render to一份文件?
将非平凡的着色器应用程序冷却到 3D 图形
非平凡着色器的一个经典酷应用是动态阴影,即一个对象投射到另一个对象上的阴影,而不是仅依赖于三角形法线和光源之间角度的阴影,这已经在Phong 型号:
图片来源。
酷炫的非 3D 片段着色器应用程序
https://www.shadertoy.com/是“片段着色器的 Twitter”。它包含大量视觉上令人印象深刻的着色器,并且可以作为使用片段着色器的“零设置”方式。Shadertoy 在浏览器的 OpenGL 接口WebGL上运行,因此当您单击 shadertoy 时,它会在浏览器中呈现着色器代码。像大多数“片段着色器图形应用程序”一样,它们只有一个固定的简单顶点着色器,可以在相机前面的屏幕上绘制两个三角形:WebGL/GLSL - ShaderToy 是如何工作的?所以用户只对片段着色器进行编码。
以下是我亲手挑选的一些更具科学性的示例:
对于某些算法,图像处理可以比 CPU 更快地完成:是否可以以每秒 60 次的速度从点数据构建热图?
对于某些功能,绘图可以比在 CPU 上更快地完成:是否可以以每秒 60 次的速度从点数据构建热图?
着色器基本上根据几个光照方程为您提供要渲染的对象的正确颜色。因此,如果您有一个球体、一个灯光和一个相机,那么即使球体只有一种颜色,相机也应该看到一些阴影、一些闪亮的部分等。着色器执行光照方程计算以提供这些效果。
顶点着色器将每个顶点在虚拟空间(您的 3d 模型)中的 3D 位置转换为它出现在屏幕上的 2D 坐标。
片段着色器基本上通过光计算为您提供每个像素的颜色。
简而言之,GPU 例程提供挂钩/回调函数,以便您绘制面部纹理。这些钩子是着色器。