Minecraft GLSL Shader着色器基础教程#1后处理着色器的编写和解析

前言
本教程面向的是JE1.17+的原版着色器,并不是Optifine的光影,资料大量引用至下文链接(目前b站无法直接引用站外链接),我将内容作了一定修改,以适应于1.17+的新版着色器
原版着色器指导(旧版):https://docs.google.com/document/d/15TOAOVLgSNEoHGzpNlkez5cryH3hFF3awXL5Py81EMk/edit,翻译:https://spgoding.com/translation/2021/03/12/guite-to-vanilla-shader.html
本教程编者并未进行系统的GLSL学习,面向的也是第一次接触着色器的人,所以只会分享一些基础知识和在Minecraft中的运用,可以基本满足资源包的制作(如屏幕方块、精美天空,水面反射、树叶摇动等)
在本教程中第一次出现的重要名词或注意事项将会以红色粗体标出
无特殊说明,本教程提到的文件路径都是相对于"资源包/assets/命名空间"的,如"shaders/post/creeper.json"代表的在资源包中的路径是"资源包/assets/命名空间/shaders/post/creeper.json"
除了着色器的基本教程外,我还会在此系列更新一些实用技巧和玩家自制着色器的解析
注:本文大量提到的spgoding.com是一个个人网站,但该教程引用的资料翻译于外网,这里仅做图片等资源引用的标注

着色器
着色器(Shader)是资源包功能的一部分,主要由JSON格式和GLSL编写其文件,GLSL是一种类C语言,这意味着基本的语法和C语言相似。如果有C语言基础的读者可以选择性的略读有关代码书写规范的部分。
后处理着色器早在1.7.2就已经加入游戏,顾名思义,后处理着色器将在屏幕内容已经渲染完成后,读取所有的像素并通过一系列程序修改,从程序内部角度,后处理着色器将同时读取屏幕上的所有像素,在编写时要考虑到这一点。以下是一些原版自带的后处理着色器示例:

后处理着色器将在以下情况被使用:
(上文提到了后处理着色器的工作对象,这意味着当屏幕出现发光实体时,entity_outline后处理着色器将自动启动,并对整个屏幕生效,而不是只对发光实体生效)
后处理着色器储存在shaders/post目录下的后处理文件(post),定义了由着色器程序(program)组成的渲染管线(pipeline)。下图展示的是由creeper.json后处理着色器所定义的管线:
(如果没有阅读外网资料的需求,不用特别记忆英文名称)

着色器程序存放在shaders/program目录下,一个完整的着色器包含:
shaders/core 下则储存了由游戏(而非上面提到的后处理管线)直接调用的着色器程序,称为核心着色器(Core)。这些着色器程序会在后处理着色器之前使用并渲染游戏内的整个内容,尽管他们的书写是一样的,但运算的内容则完全不一样。
编写一个后处理JSON
后处理JSON应该存放在shaders/post中,并且命名为上文提到的5种能被自动调用的着色器中的一种
(如果要参考原版文件,可以到 版本.jar/assets/minecraft/shaders中查看)
后处理JSON仅由两个数组组成:
"targets"数组声明了一系列帧缓冲(画布),以便使用着色器之后指定存储的位置(不允许由缓冲存储到自身),缓冲可以是"swap"这样的格式,宽度高度默认是窗口的宽度和高度,也可以是{"name":"swap","width
":"40","height
":"30"}这样的格式以限制大小。两种格式可以随意混用。缓冲的名字应该仅由可选的命名空间、小写英文字母、数字和下划线组成。
除了这些手动声明的缓冲层,还有一些特殊的、预先定义好的缓冲。这些缓冲里面会自带内容,不需要声明就可以直接使用。它们是:
Passes数组声明了一系列的渲染管线,依照自上而下的顺序依次执行,由一个缓冲层(intarget)输入,经过一个着色器程序(name)更改,并输出到一个新的缓冲层(outtarget)。有时需要用auxtargets和uniforms提供参数,在后面会介绍到。
其中,blit着色器是一个不改变任何内容的着色器,用于在缓冲间传递数据。最终的数据应当传递到minecraft:main中
调用的着色器程序是一个json文件,由该json文件调用一个片段着色器和一个顶点着色器,大多数情况下这两个着色器和json同名,如blit.json调用了blit.fsh和blit.vsh,但没有硬性规定,比如不修改顶点但是修改颜色的着色器会调用blit.vsh和另一个自定的fsh。该程序的书写在后文介绍。
着色器是每帧循环运行的,minecraft:main每次会在首个更新,如果要跨帧传递信息,需要在结尾将信息传递到一个缓冲,在下次覆盖前读取它。因为循环的流程,文件后面的缓冲会来到文件前面作为上一帧的缓冲。下文的代码块是通过tmp缓冲将上一帧的minecraft:main传递到了下一帧的swap中,并且给minecraft:main使用了color_convolve着色器

可选的auxtargets数组提供了一系列补充的缓冲或图片,使得着色器程序能够访问它们(默认状态下着色器程序只能访问intarget提供的一个缓冲)。在 auxtargets数组中的对象应当包含:(本节靠后位置有示例)
如果定义了一个图片,还应该包含

图片来自spgoding.com,文字因为不清晰经过修改
一个示例后处理JSON如下,它使得着色器程序能够访问 qux 缓冲,以及一张叫作 abc.png 的图片:
用这个参数导入的缓冲或者图片被称为采样器,在代码中一般以texture2D(采样器,坐标)的方式读取内容,具体的用法会在下一节讲到。
借此,我们可以让着色器访问到无需被渲染到屏幕上的本地图片,这个功能在一个实用的“调试信息”资源包中被实用,我会在结尾附上链接,之后我会发布对该简单着色器的解析。
Uniforms数组将会起到对着色器程序传递参数的作用,具体参数在不同着色器程序中有不同作用,我之后会出一篇专栏,列出各个着色器程序所需的参数,如果你有编程基础,可以直接在着色器代码中查看。
可运作的后处理着色器实例(来自spgoding.com):
以下是一个可以正常运作的完整的后处理 JSON 文件。它添加了一个 “notch” 抖动效果,并减少了颜色饱和度。

assets/minecraft/shaders/post/spider.json
熟悉上面的内容后,你将能够像修改材质包贴图一样修改原版的着色器。
如果还需要进一步修改再阅读下面的内容。(这完全不必一次就看完!)
创建一个着色器程序JSON
着色器程序应该存放于shaders/programs中,并命名为后处理JSON调用的名称,其文件内容为:
blend:理论上定义了着色器程序的输出应当怎样与目标缓冲(destination buffer)上已有的内容合并,但被游戏硬编码,修改没有效果
vertex:指定了将要使用的 顶点着色器.vsh 文件的文件名。
fragment:指定了将要使用的 片段着色器.fsh 文件的文件名。
attributes:一个字符串数组,指定顶点的哪些属性能够被顶点着色器访问到,但被游戏硬编码,修改没有效果
samplers:定义了片段着色器想要访问缓冲需要用到的变量名(采样器)。"DiffuseSampler" 是被自动给予 "intarget" 中定义的缓冲的采样器,该采样器允许使用texture2D函数访问缓冲上的某一像素,并返回一个rgb的值,具体将在下节讲到。其他的任何名字都需要与你在 后处理文件的 "auxtargets" 中指定的一致。(如前文的图片dither指定的DitherSampler)
uniforms:定义了各种全局量(又译统一量。对每个顶点或像素都保持不变的值)的名称、类型和默认值。(如果这里没有指定着色器调用的uniform变量则会报错)
uniform中name字符串定义了在 GLSL 代码中或者在向该着色器程序的该全局量传值时用的名称。特殊的有:(这些特殊的变量并不会在所有着色器中生效,注意生效位置)
"values" 应为一个浮点数数组,而 "type" 则定义了这些浮点数会在 GLSL 代码中被解析为的数据类型,不同的数据类型在上面的代码块中已列出
vec2/vec3/vec4指的是向量类型,即拥有 2 / 3 / 4 个成员的浮点型列表,matrixn或matrixnxm指的是矩阵类型,同样为有nxn或nxm个成员的浮点型列表。矩阵类型在着色器中为matn或matnxm
可运作的示例
以下是一个可以正常运作的完整的着色器程序 JSON 文件。这是原版的 “wobble” 着色器,未经修改。
assets/minecraft/shaders/program/wobble.json
下节预告:核心着色器和编写fsh和vsh。


下载地址:https://drive.google.com/open?id=1n-SjTRv6D7CnYXok7A4cB9sqnSw00wGS(需科学上网)
旁观苦力怕时生效,能够显示屏幕中心像素的RGBA4个值。声明了effects/shader_font.png为FontSampler,在fsh中使用textrue2D访问了这张贴图,做到了打印文字的效果,由于代码中不支持字符串,只能使用一个一个的单字拼接,不支持中文,但读者可以尝试添加。