什么是着色器?

简介

你已经决定尝试编写着色器了。你可能已经听说过,它们可用来快速创造非常有趣的效果。可能也听说过它们很可怕。以上两种说法都对。

着色器可以用来创建广泛的效果(事实上,现代渲染引擎中绘制的所有东西都是用着色器绘制的)。

对于不熟悉着色器的人来说,编写着色器也是非常困难的。Godot试图通过公开许多有用的内置特性,并为您处理一些较低级别的初始化工作,使编写着色器变得更容易一些。然而,GLSL (OpenGL着色语言,也是Godot使用的)仍然是不直观和受限制的,特别是对于习惯使用GDScript的用户。

可是它是什么?

着色器是一种在图形处理单元(GPU)上运行的特殊程序。大多数电脑都有GPU,一个集成到他们的CPU或离散的(这意味着它是一个单独的硬件组件,例如,标准显卡)。GPU对于呈现特别有用,因为它们被优化为并行运行数千条指令。

着色器的输出通常是绘制到视图端口的对象的彩色像素。但是一些着色器允许特殊的输出(对于像Vulkan这样的应用程序接口尤其如此)。着色器在着色器管道中操作。标准流程是顶点->片段着色器管道。顶点着色器用于决定每个顶点的位置(3D模型中的点,或者精灵的一角)去和片段着色器决定什么颜色个别像素接收。

假设你想要将纹理中的所有像素更新为给定的颜色,你可以在CPU上这样写:

for x in range(width):
  for y in range(height):
    set_color(x, y, some_color)

在着色器中,你只能访问循环的内部,所以你写的是这样的:

// function called for each pixel
void fragment() {
  COLOR = some_color;
}

您无法控制如何调用此函数。您必须设计不同于在中央处理器上设计程序的着色器。

着色器管道的一个后果是,您无法访问着色器上次运行的结果,您无法从正在绘制的像素中访问其他像素,而且不能在当前绘制的像素之外编写。这使得GPU能够并行地为不同的像素执行着色器,因为他们彼此不耦合。这种灵活性的缺乏是为GPU的工作设计,使着色器难以置信的快。

它能做什么

  • 快速定位顶点
  • 快速计算颜色
  • 快速计算光照
  • 进行大量的数学计算

它不能做什么

  • 绘制外部网格
  • 从当前像素(或顶点)访问其他像素
  • 储存之前的迭代
  • 动态更新(可以,但是需要编译)

着色器的结构

在Godot中,着色器由3个主要函数组成:“vertex()”函数,“fragment()”和“light()”函数。

“vertex()”函数运行在网格中的所有顶点上,并设置它们的位置以及部分其他每个顶点的变量。

函数的作用是:为网格所覆盖的每个像素运行“fragment()”函数。它使用“vertex()”函数中的变量来运行。“vertex()”函数中的变量在顶点之间进行插值,以提供“fragment()”函数的值。

“light()”函数用于每个像素和每束光照。它从“fragment()”函数和以前的运行中获取变量。

For more information about how shaders operate specifically in Godot, see the Shaders doc.

警告

The light() function won’t be run if the vertex_lighting render mode is enabled, or if Rendering > Quality > Shading > Force Vertex Shading is enabled in the Project Settings. (It’s enabled by default on mobile platforms.)

技术概述

GPU渲染图形的速度要比CPU快得多,原因如下:但最值得注意的是,它们能够大规模并行运行计算。CPU通常有4或8个内核,而GPU通常有数千个。这意味着GPU可以一次完成数百项任务。GPU架构师已经利用了这种方式,允许快速地进行许多计算,但只有当多个或所有核心同时进行相同的计算时,但数据不同。

That is where shaders come in. The GPU will call the shader a bunch of times simultaneously, and then operate on different bits of data (vertices, or pixels). These bunches of data are often called wavefronts. A shader will run the same for every thread in the wavefront. For example, if a given GPU can handle 100 threads per wavefront, a wavefront will run on a 10×10 block of pixels together. It will continue to run for all pixels in that wavefront until they are complete. Accordingly, if you have one pixel slower than the rest (due to excessive branching), the entire block will be slowed down, resulting in massively slower render times.

This is different from CPU-based operations. On a CPU, if you can speed up even one pixel, the entire rendering time will decrease. On a GPU, you have to speed up the entire wavefront to speed up rendering.