香草着色器入门教程!
引言
Minecraft 使用各式各样的着色器来渲染在屏幕上所看到的一切。通过在资源包中修改这些着色器,可以实现画面后处理、地图制作所需要的花哨特效或者许许多多新的创意,以使 Minecraft 更加绚丽多彩。
鉴于时间与能力所限,本教程仅仅简单介绍了一些已经部分过时的知识。请善用参考资料与搜索引擎,但最为重要的是亲自尝试、测试,才能探究出更多知识与技巧,加深对着色器的理解。
教程的前半部分主题介绍了理论知识。实际上手之前,请参阅准备工作。
着色器
什么是着色器
「着色器」 (Shader) 是用于后期处理、特效或者图形渲染的一种程序。
OpenGL 即「开放图形库」,是一套用于渲染图形的编程接口。Minecraft 使用 OpenGL 来渲染画面。
在 Minecraft 中,着色器一般指 OpenGL 所使用的着色器。
着色器在 Minecraft 中大体上可以分为两种:「后处理着色器」与「核心着色器」。
「后处理着色器」正如其名,会对渲染完毕的部分内容或整个画面进行后期处理,用于实体发光效果、极佳! 画质下内容的叠加、旁观者模式下附着在末影人等实体上的特殊视觉效果与已经被移除的 Super Secret Settings 等。
「核心着色器」是于 21w10a 被加入的新着色器类型,一般负责渲染画面的各个基本组成部分。
「包含着色器」并不是独立的着色器程序,它可能包含一些公用的函数或常量等,可以被其他着色器所引用。
渲染流程
一般来说,绘制一个图形需要分为以下几步。
- 顶点着色器 (Vertex Shader)
顶点着色器以图形的每个顶点坐标和一些用来变换位置的矩阵等作为输入,并输出变换后的顶点坐标以及一些后续流程所需要的额外的数据 (如顶点颜色)。后面会更详细地讨论它具体能做些什么。
- 几何着色器、裁切与光栅化
经顶点着色器变换后的顶点会装配成图元,输入几何着色器,裁切掉超出可视范围的部分,并且对图形进行像素采样,以供后续使用。该部分在 Minecraft 中不可编辑。
- 片段着色器 (Fragment Shader)
又称「片元着色器」或「像素着色器 (Pixel Shader)」。它接受来自上一步骤或由程序直接提供的信息,经过一些计算最终决定每个像素的颜色。后面会更详细地讨论它具体能做些什么。
一个顶点着色器与片段着色器及其参数,称为一个「着色器程序」。
而画面由方块、实体、用户界面等很多元素所构成。每个元素的绘制,一般都由一个着色器程序负责;有时,也可能涉及多个着色器程序。一系列渲染程序按顺序组织起来,称为「渲染管线」。
着色器读取与绘制的“画布”称为「缓冲区」。它的大小可以是屏幕尺寸,也可以是自定义的。通过在不同的缓冲区中进行绘制与处理,可以灵活地控制渲染过程。
一部分变量经过如下途径逐步传递:一些变量输入到顶点着色器,顶点着色器输出一些变量到片段着色器,片段着色器再输出一些变量到下一个步骤。但也有一部分常量无需传递,在一个着色器程序中的任意步骤都可以访问,这些常量被称为「Uniform 变量」。
Minecraft 绘制画面时,依次绘制完整不透明方块、透明方块、具有半透明像素的方块、实体、文字等(列举顺序不代表实际绘制顺序),这些步骤使用的着色器即为核心着色器。接下来,上面的步骤得到的缓冲区被后处理着色器进行处理并叠加,形成最终画面。
资源包结构
Minecraft 资源包中涉及到着色器有关内容的路径结构如下:
- assets
- minecraft
- shaders
- core
- 核心着色器的着色器程序
- include
- 包含着色器
- post
- 后处理着色器的渲染管线
- program
- 后处理着色器的着色器程序
只有位于 minecraft
命名空间下的着色器文件才是有效的。
通过资源包可以对后处理着色器自定义渲染管线与着色器程序,但对于核心着色器,只能更改已有的着色器程序。同时,由于可以自定义其渲染管线,后处理着色器也可以访问到更多的缓冲区,而核心着色器能访问的缓冲区是硬编码的。
GLSL
GLSL (OpenGL Shading Language),是 OpenGL 中描述着色器的编程语言。它在很多方面与 C 语言很相似,但由于其功能需要与硬件限制,有着很多不同的规则与语法。
鉴于篇幅所限,这里只介绍其独特之处。阅读该部分可能需要对 C 或与之类似的语言的有着一定的了解。
GLSL 文件的扩展名可以是任意的,但按照 Minecraft 惯例,顶点着色器、片段着色器与包含着色器通常分别以 .vsh
.fsh
.glsl
作为扩展名。
在 Minecraft 的着色器中常见的 GLSL 变量类型有:
bool
:布尔型,取值为 true
或 false
。
int
:有符号整数。
float
:浮点数。
vec2
、vec3
、vec4
:由2、3或4个 float
构成的向量。
bvec2
、bvec3
、bvec4
:由2、3或4个 bool
构成的向量。
ivec2
、ivec3
、ivec4
:由2、3或4个 int
构成的向量。
mat2
、mat3
、mat4
:由 float
构成的2×2、3×3或4×4矩阵。
sampler2D
:用于从二维缓冲区中采样的采样器。
vec2
、vec3
、vec4
中的数据可能有多种含义,基于这些含义,GLSL 提供了较为自由、方便的访问方式。按顺序,其中的四个元素(或不足四个元素)被命名为 xyzw
、rgba
或 stpq
,可以自由改变顺序组合为新的向量。例如:
vec4 m = vec4(0.1f, 0.2f, 0.3f, 0.4f);
vec2 n = m.xw; // vec2(0.1f, 0.4f);
vec3 u = m.agrb; // vec4(0.4f, 0.2f, 0.1f, 0.3f);
mat2
、mat3
、mat4
可以用类似于多维数组的形式来访问。例如,若 m
是一个 mat4
类型的变量,那么 m[3][2]
代表了 m
第四行第三列的元素。
向量、矩阵的运算包括但不限于:
- 一个同类型的数字与之相加减或相乘除。该运算遵循逐分量运算的原则,因此,与数学上不同,一个数字加上一个矩阵等于该矩阵的每个分量加上该数字得到的新矩阵。
- 两个向量相加或相乘。该运算遵循逐分量运算的原则,与数学中的向量乘法不同(要得到向量的数量积或向量积,请使用
dot()
与 cross()
函数)。只有两个同阶向量才能运算,得到的结果依然是阶数相同的向量。
- 两个矩阵相加或相乘。与向量不同,矩阵相加满足逐分量运算的原则,而矩阵乘法则与数学中的矩阵乘法相同。
- 向量与矩阵相乘。与数学中的向量与矩阵乘法相同。
出于安全因素,GLSL 不允许隐式类型转换。需要转换时,可以用类似 float(var)
的语法进行显式转换。
一个典型的 GLSL 文件通常具有这样的结构:
#version GLSL 版本
uniform 类型 Uniform 变量;
in 类型 输入变量;
out 类型 输出变量;
void main() {
// 一些操作
输出变量 = 输出的结果;
}
我们以 rendertype_entity_no_outline
核心着色器的片段着色器为例子分析,该着色器用于绘制旗帜方块的“旗”部分:
#version 150
#moj_import <fog.glsl>
uniform sampler2D Sampler0;
uniform vec4 ColorModulator;
uniform float FogStart;
uniform float FogEnd;
uniform vec4 FogColor;
in float vertexDistance;
in vec4 vertexColor;
in vec2 texCoord0;
in vec4 normal;
out vec4 fragColor;
void main() {
vec4 color = texture(Sampler0, texCoord0) * vertexColor * ColorModulator;
fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor);
}
首行的 #version 150
表明了 GLSL 语言版本。
#moj_import <fog.glsl>
引用了包含着色器 fog.glsl
。这是一个常用的包含着色器,提供了 linear_fog()
函数,用来实现迷雾效果。
uniform sampler2D Sampler0
表明该着色器访问了一个名为 Sampler0
的二维缓冲区。它通常是纹理、字符贴图等。
uniform vec4 ColorModulator
表明该着色器使用了一个类型为 vec4
的 Uniform 变量 ColorModulator
。从字面意义上看这是一个用于调整颜色的变量,在 Minecraft 中几乎每个核心着色器都用到了它,尽管它大部分时候都是常量 vec4(1.0f)
。其他的 Uniform 变量用于实现迷雾效果。
in float vertexDistance
表示该着色器以上一个步骤的输出中的 vertexDistance
变量为输入。对于片段着色器而言,上一个步骤代表对应的顶点着色器;对于顶点着色器,上一个步骤则是游戏提供的输入。通常顶点着色器只会运行很少的次数,片段着色器根据其片段所在位置对顶点着色器的输出进行插值,得到该输入。texCoord0
就是由顶点插值得到的该像素坐标。
out vec4 fragColor
表示该着色器以变量 fragColor
的最终值为输出。
接下来,main()
函数中:
texture(Sampler0, texCoord0)
是从缓冲区中取样的函数。Sampler0
中坐标为 texCoord0
的像素被取出,赋值到 color
变量中。
最后一行是在大部分核心着色器中都存在的实现迷雾效果的部分。通常无需在意这一部分。
核心着色器
核心着色器用于绘制游戏的各个基本部分。
画面上最终看到的每一部分,绘制经历的第一个步骤都是核心着色器。方块、实体、文字乃至用户界面皆是如此。
资源包中,对于核心着色器,只能更改已有的着色器程序,不能进行新增着色器程序、更改输入或输出变量等由渲染管线所控制的行为。
一个着色器程序以一个 JSON 文件定义。下面为原版的 rendertype_entity_no_outline.json
:
{
"blend": {
"func": "add",
"srcrgb": "srcalpha",
"dstrgb": "1-srcalpha"
},
"vertex": "rendertype_entity_no_outline",
"fragment": "rendertype_entity_no_outline",
"attributes": [
"Position",
"Color",
"UV0",
"UV2",
"Normal"
],
"samplers": [
{ "name": "Sampler0" },
{ "name": "Sampler2" }
],
"uniforms": [
{ "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
{ "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
{ "name": "IViewRotMat", "type": "matrix3x3", "count": 9, "values": [ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ] },
{ "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
{ "name": "Light0_Direction", "type": "float", "count": 3, "values": [0.0, 0.0, 0.0] },
{ "name": "Light1_Direction", "type": "float", "count": 3, "values": [0.0, 0.0, 0.0] },
{ "name": "FogStart", "type": "float", "count": 1, "values": [ 0.0 ] },
{ "name": "FogEnd", "type": "float", "count": 1, "values": [ 1.0 ] },
{ "name": "FogColor", "type": "float", "count": 4, "values": [ 0.0, 0.0, 0.0, 0.0 ] },
{ "name": "FogShape", "type": "int", "count": 1, "values": [ 0 ] }
]
}
blend
控制颜色混合的规则,通常不必对该部分做出更改。
vertex
与 fragment
指定了该着色器程序的顶点着色器与片段着色器文件的名称,扩展名分别为 .vsh
与 .fsh
。着色器名称不必与着色器程序名称相同,尽管原版核心着色器是相同的。
attributes
指定了顶点着色器的输入,可以填写的值是游戏指定的。
samplers
指定了输入的 Uniform 缓冲区,可以填写的值是游戏指定的。
uniforms
指定了其他 Uniform 变量。type
表示其类型,count
表示组成它的元素个数,values
表示默认值。
后处理着色器
编写着色器
准备工作
开发环境
正如大部分编程语言,GLSL 的编写可以使用任何文字处理软件,但好的开发环境对高效率的开发至关重要:通常,使用习惯的文本编辑器即可,提供了 GLSL 语法高亮的软件和插件可能会进一步提高效率。
开始编写前,需要先建立一个资源包模板。一个好的办法是将对应版本游戏 jar 文件内 assets/minecraft/shaders/
中的文件拷贝到一个空资源包里,便于对原版着色器进行修改和观察。
如果有,建议在 Minecraft 启动器中打开调试日志。加载资源包时进行着色器编译出现的错误会记录在日志中。面对隐蔽的语法错误,这可以成为调试的重要工具。
调试
技巧与陷阱
软件与工具
Suso 的 ShaderReload Mod
GeforceLegend 的 GLSL 语言服务器协议
Intel GPA
NVIDIA Nsight Graphics
参考资料
LearnOpenGL 中文
GLSL Specification
中文 Minecraft Wiki - 着色器
McTsts 原版着色器 Wiki
Minecraft Vanilla Shaders Guide
WIP
本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。