原版着色器指导
- 原 文: Guide to vanilla shaders
- 原作者: SirBenet, with input from Godlander and Onnowhere
- 译 文: 指导到香草着色器
- 译 者: SPGoding
- 鸣 谢: 森林蝙蝠 - 提供了各种专业名词的专业机翻
- 截止翻译时,原文最后更新: 2021-03-10 for Minecraft 21w10a(1.17 快照)
相较于 MCBBS 上的旧译文,本文加入了有关核心着色器的信息。
已经原作者授权
前言
原版 Minecraft 有着许多的 GLSL(OpenGL Shading Language)着色器。这些着色器可以直接通过资源包更改,进而嵌在地图里,或者是在玩家进入服务器时自动启用。
着色器以 OpenGL Shading Language(GLSL)语言编写。这是一个类似 C 的语言,支持浮点数、向量、矩阵,以及函数、条件、循环以及其他各种你能想到的编程语言应有的特性。
后处理着色器在 1.7 中加入,会在游戏已经渲染好画面以后起效。它们能够接受整个屏幕的像素作为输入,然后逐像素地输出。除了一些有限的特例以外,着色器所能接受到的唯一数据就是正显示在屏幕上的内容。以下是一些原版自带的着色器示例:
后处理着色器在以下情况下启用:
- 以特定生物的视角旁观时(苦力怕、蜘蛛、末影人)
- 屏幕中有具有发光状态效果的实体时
- 图像品质设置为「极佳!」时
核心着色器在 1.17 中加入 —— 你屏幕上的所有东西都是由核心着色器渲染的。以下是一些修改核心着色器所能达到的效果:
(由 Onnowhere 制作)
(由 Aeldrion 制作)
(由 Onnowhere 制作)
可 在此(译注:该链接疑似遭到破坏,没有有关信息) 查看所有核心着色器的功能。
梗概:着色器的组成部分
后处理着色器使用 后处理(post) 文件(储存在 assets/minecraft/shaders/post
目录下)定义由一系列「着色器程序」(program)组成的渲染管线(pipeline)。下图展示了由 creeper.json
定义的管线:
每个「着色器程序」(例如 color_convolve
)都是定义在另一个 JSON 文件当中的(这回储存在 shaders/program
目录下)。该文件通常包括:
- 一个要使用的「顶点着色器」(vertex shader)的路径(一个以 GLSL 语言编写的
.vsh
文件) - 一个要使用的「片段着色器」(fragment shader)的路径(一个以 GLSL 语言编写的
.fsh
文件)
shaders/core
目录则包含由游戏直接调用的着色器程序。后处理着色器管线不使用这些程序。
「顶点着色器」会对每个顶点起效,将顶点的位置作为输入,并产生一个经过变换的位置作为输出。一个扭曲屏幕的顶点着色器示例见下:
「片段着色器」会对每个像素起效,并逐像素产生输出层。一个不改变任何内容的片段着色器将直接把输入的像素原样产生。一个交换红色和蓝色色道的片段着色器示例见下:
开始
- 建立一个资源包
- 在里面建好以下文件夹:
assets/minecraft/shaders/post
assets/minecraft/shaders/program
如果你想要参考原版的着色器,可以直接从游戏 jar 文件里面解压:%appdata%/.minecraft/versions/1.16.5/1.16.5.jar/assets/minecraft/shaders/
。
你可能还需要给你用的编辑器装一个 GLSL 的语法高亮插件。
打开游戏日志 —— 任何着色器的错误都会显示在这里。
创建一个后处理 JSON
后处理 JSON 文件应该放置在 assets/minecraft/shaders/post
目录当中,并且命名为:
creeper.json
将在以苦力怕视角旁观时启用invert.json
将在以末影人视角旁观时启用spider.json
将在以蜘蛛视角旁观时启用entity_outline.json
将在屏幕中有具有发光状态效果的实体时启用transparency.json
将在「极佳!」图像品质下启用
我们只能通过替换以上五个现存的文件来自定义后处理着色器。
后处理文件由两个数组构成:
{
"targets": [ … ],
"passes": [ … ]
}
Targets(目标)
我们用 "targets"
(目标)数组来声明所需的帧缓冲(frame buffers) —— 把它们想象成我们可以往上面读取或写入像素的画布。该数组中的每一项都可以是以下两者之一:
- 一个有着
"name"
(名称,字符串)、"width"
(宽度,整型数字)、"height"
(高度,整型数字)三个参数的对象。这三个参数用于定义该帧缓冲。 - 一个字符串用于定义名称。宽度和高度将会默认为游戏窗口的宽度和高度。
你可以在声明帧缓冲时随意混用两种方式:
"targets": [
"foo",
"bar",
{"name":"baz", "width":73, "height":10},
"qux"
]
除了这些手动声明的缓冲层,还有一些特殊的、预先定义好的缓冲。这些缓冲里面会自带内容,不需要声明就可以直接使用。它们是:
-
为发光着色器准备的、预先填充好的特殊缓冲区
minecraft:main
没有水、方块实体和其他一些东西
final
纯色的实体。颜色是该实体所在队伍的颜色 -
为实体视角着色器准备的、预先填充好的特殊缓冲区
minecraft:main
所有内容都已渲染完成
Passes(步骤)
"passes"
(步骤)数组定义了一系列的按顺序执行的步骤。每一个步骤都包含一个输入缓冲层("in_target"
),并运行一个着色器程序("name"
),来将数据写入到输出缓冲层("out_target"
)当中。一个着色器程序不能把数据输出到它读取的那个缓冲层。
"passes": [
{
"name": "prog1",
"intarget": "minecraft:main",
"outtarget": "foo"
},
{
"name": "prog2",
"intarget": "foo",
"auxtargets": [ … ],
"outtarget": "bar"
},
{
"name": "blit",
"uniforms": { … },
"intarget": "bar",
"outtarget": "minecraft:main"
}
]
"blit"
是一个不改变任何内容的着色器程序,它只会简单地把数据从一个缓冲复制到另一个缓冲当中(注意:在不同大小的缓冲之间复制数据时会有问题)。
你想要显示的内容应当最终输出到 "minecraft:main"
缓冲当中(如果是发光着色器,还可以进一步修改 final
缓冲,该缓冲的内容会覆盖到一切内容上面)
Passes.Auxtargets(辅助目标)
可选的 "auxtargets"
(辅助目标)数组提供了一系列补充的缓冲或图片。着色器程序能够读取它们。在辅助目标数组中的对象应当包含:
"id"
- 指定一个现存缓冲的名称(即定义在"targets"
中的名称),或者是一张位于资源包minecraft/textures/effect
目录下的图片的文件名。"name"
- 赋予该缓冲或图片的一个任意名称。着色器程序的 GLSL 代码中能够通过该名称访问该缓冲或图片。- 如果指定的是一张图片,还必须指定以下参数:
"width"
- 以像素为单位的图片宽度(似乎没有实际效果?)"height"
- 以像素为单位的图片高度(似乎没有实际效果?)"bilinear"
- 指定该图片被采样时使用的缩放算法
缩放算法:
一个示例辅助目标数组如下,它使得着色器程序能够访问 qux
缓冲,以及一张叫作 abc.png
的图片:
"auxtargets": [
{"id":"qux", "name":"QuxSampler"},
{"id":"abc", "name":"ImageSampler", "width":64, "height":64, "bilinear":false}
]
Passes.Uniforms(统一量)
可选的 "uniforms"
(统一量)参数能够向着色器程序传递一个浮点数数组。下方的示例使用统一量为 blur
着色器程序指定了一个半径(radius)和方向(direction):
{
"name": "blur",
"intarget": "swap",
"outtarget": "minecraft:main",
"uniforms": [
{
"name": "BlurDir",
"values": [ 0.0, 1.0 ]
},
{
"name": "Radius",
"values": [ 10.0 ]
}
]
},
统一量以及它们的默认值由着色器程序 JSON 文件定义。统一量的名字以及所传入浮点数数组的大小应当和着色器程序 JSON 中定义的一致。
可运作的示例
以下是一个可以正常运作的完整的 post JSON 文件。它添加了一个 "notch" 抖动效果,并减少了颜色饱和度。
assets/minecraft/shaders/post/spider.json
{
"targets": [
"swap"
],
"passes": [
{
"name": "notch",
"intarget": "minecraft:main",
"outtarget": "swap",
"auxtargets": [
{
"name": "DitherSampler",
"id": "dither",
"width": 4,
"height": 4,
"bilinear": false
}
]
},
{
"name": "color_convolve",
"intarget": "swap",
"outtarget": "minecraft:main",
"uniforms": [
{ "name": "Saturation", "values": [ 0.3 ] }
]
}
]
}
创建一个「着色器程序」JSON
着色器程序 JSON 文件应该放置在 assets/minecraft/shaders/program
文件夹中。可以使用任何名称(只要遵守正常资源包的文件命名规则即可,例如没有大写字母什么的)。
{
"blend": { … },
"vertex": "foo",
"fragment": "foo",
"attributes": [ … ],
"samplers": [ … ],
"uniforms": [ … ]
}
"vertex"
指定了将要使用的顶点着色器 .vsh
文件的文件名。
"fragment"
指定了将要使用的片段着色器 .fsh
文件的文件名。
"attributes"
是一个字符串数组,指定顶点的哪些属性能够被顶点着色器访问到。目前只能写 "Position"
(位置)。
"attributes": [ "Position" ],
"samplers"
定义了片段着色器想要访问缓冲需要用到的变量名(采样器)。"DiffuseSampler"
是被自动给予 "intarget"
中定义的缓冲的采样器。其他的任何名字都需要与你在 后处理文件的 "auxtargets"
中指定的一致。
"samplers": [
{ "name": "DiffuseSampler" },
{ "name": "DitherSampler" }
]
"uniforms"
定义了各种统一量(对每个顶点或像素都保持不变的值)的名称、类型和默认值。
"uniforms": [
{ "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": "InSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] },
{ "name": "OutSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] },
{ "name": "BlurDir", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] },
{ "name": "Radius", "type": "float", "count": 1, "values": [ 5.0 ] }
]
"name"
字符串定义了在 GLSL 代码中或者在向该着色器程序的该统一量传值时用的名称。游戏自动给了一些特殊的统一量:
"Time"
- 0 到 1 的一个值,表示在一秒内的时间,每秒自动重置"InSize"
- 以像素为单位的输入缓冲(input buffer)的宽度和高度"OutSize"
- 以像素为单位的输出缓冲(output buffer)的宽度和高度"ProjMat"
- 由顶点着色器使用的投影矩阵(projection matrix)- 核心着色器有更多统一量 —— 请参考原版 jar 文件
"values"
应为一个浮点数数组,而 "type"
则定义了这些浮点数会在 GLSL 代码中被解析为的数据类型:
"float"
- 一个float
或vec2
/vec3
/vec4
,具体取决于在"values"
中指定的数字个数"matrix4x4"
- 一个由"values"
中指定的 16 个值产生的mat4
"matrix3x3"
- 可用的类型,但仍然需要输入 16 个值?"matrix2x2"
- 可用的类型,但仍然需要输入 16 个值?
"blend"
理论上定义了着色器程序的输出应当怎样与目标缓冲(destination buffer)上已有的内容合并,但目前好像没有效果。
"blend": {
"func": "add",
"srcrgb": "one",
"dstrgb": "zero"
}
更多关于这些模式本应怎样运作的信息可以查看:khronos.org/opengl/wiki/Blending(英文)
可运作的示例
以下是一个可以正常运作的完整的着色器程序 JSON 文件。这是原版的 "wobble" 着色器,未经修改。
assets/minecraft/shaders/program/wobble.json
{
"blend": {
"func": "add",
"srcrgb": "one",
"dstrgb": "zero"
},
"vertex": "sobel",
"fragment": "wobble",
"attributes": [ "Position" ],
"samplers": [
{ "name": "DiffuseSampler" }
],
"uniforms": [
{ "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": "InSize", "type": "float", "count": 2,
"values": [ 1.0, 1.0 ] },
{ "name": "OutSize", "type": "float", "count": 2,
"values": [ 1.0, 1.0 ] },
{ "name": "Time", "type": "float", "count": 1,
"values": [ 0.0 ] },
{ "name": "Frequency", "type": "float", "count": 2,
"values": [ 512.0, 288.0 ] },
{ "name": "WobbleAmount", "type": "float", "count": 2,
"values": [ 0.002, 0.002 ] }
]
}
GLSL 基础
该部分假设读者已经熟悉了基本的编程概念(变量、函数、循环等),并且主要讲解 GLSL 特有的特性。如果你之前从来没有接触过编程,我不建议你用 GLSL 上手。
有关核心语言的更多信息:khronos.org/opengl/wiki/Core_Language_(GLSL)(英文)
有关所有可用函数的详细文档:docs.gl/sl4/all(英文)
注意: 原版 jar 文件中的着色器是用的 GLSL 版本是 110,本文为保持一致也将使用该版本。不过,由于 Minecraft 需要 OpenGL 4.4,你可以放心地使用最高到 440 版本的 GLSL。版本需要在 GLSL 代码的开头声明(见示例)。
数据类型
标量
bool
: 布尔值true
/false
int
/unit
: 有符号 / 无符号 32 位整型数字
float
/double
: 单精度 / 双精度浮点数向量
bvec
n
、ivec
n
、uvec
n
、vec
n
、dvec
n
:分别代表由
n
个bool
、int
、uint
、float
或double
组成的向量
n
必须为 2、3 或 4。矩阵
mat
n
: 由float
组成的矩阵,大小为n
×n
mat
n
x
m
: 由float
组成的矩阵,大小为n
×m
n
和m
都必须为 2、3 或 4。矩阵为列优先(3×2 = 3 列,2 行)。
在 GLSL 里最常用到的是 float
。
想要构造向量和矩阵的话,可以任意组合其他的标量 / 向量:
vec2 v2 = vec2(0.4, 0.5);
vec3 v3 = vec3(v2, 0.6);
mat4 m3 = mat3(0.1, 0.2, 0.3,
v3,
v2, 0.7);
请注意,列优先的矩阵可能有点反直觉:
mat2(a, b, // 第一列(不是第一行!)
c, d); // 第二列
除了普通的[索引]以外,混合 xyzw
或 rgba
是种方便的获取某个向量的 1 个或多个元素的方式:
vec3 v3 = vec3(0.1, 0.2, 0.3);
v3.x; // == 0.1
v3.yzyz; // == vec4(0.2, 0.3, 0.2, 0.3)
着色器程序工作流程
顶点着色器或片段着色器都有一个 void main()
函数,作为代码的起始点。该函数没有任何参数,也不返回任何东西,它只应更新一个现存的特殊变量(gl_Position
或 gl_FragColor
,将在下方解释。)
由于 GLSL 代码是在无法任意写入内存的硬件上执行的,GLSL 不支持递归操作。不过循环是可以的:
for (int i = 0; i < 10; i++) { ... }
while (x < 20) { ... }
属性、统一量、传递变量
如果你选择了更高版本的 GLSL,该部分在 GLSL 140 及以上版本有所改动。见:khronos.org/opengl/wiki/Type_Qualifier_(GLSL)#Shader_stage_inputs_and_outputs(英文)。
全局变量可以被声明为 attribute
(属性)、uniform
(统一量)或 varying
(传递变量)。
attributes
(属性)只能由顶点着色器读取,它自动包含了当前顶点的一些信息。对于 Minecraft 的着色器来说,只有 Position
(位置)属性可用。
uniform
(统一量)可以被顶点着色器与片段着色器读取,对所有的顶点或像素都会保持恒定不变。它们的值可以通过后处理 JSON 文件传入,如未指定将会用在着色器程序 JSON 文件中定义的默认值。
varying
(传递变量)由顶点着色器声明并赋值,然后可在片段着色器中被读取。这些值会在顶点间插值计算出来。举个例子:
varying vec2 texCoord;
补充:从 21w10a 起,原版着色器的 GLSL 版本从 120 变到了 150。因此,「传递变量」与「属性」被「传入变量」(in)与「传出变量」(out)取缔。
对顶点着色器来说,「属性」是「传入变量」,「传递变量」是「传出变量」。
对片段着色器来说,「传递变量」是「传入变量」,特殊变量fragColor
是「传出变量」。
统一量没有变动。我会在 1.17 发布以后更新该指导(译注:然后他鸽了)。
编写一个顶点着色器
顶点着色器会在每个顶点上运行,以对顶点进行变换。通常来讲这指的是世界几何体的每一个顶点 —— 但是在 Minecraft 中,它指的只是缓冲四个角上的顶点。这使得目前的顶点着色器十分受限。不对顶点缓冲器做任何修改是很常见的做法(绝大多数原版的着色器程序都重复使用了 sobel.vsh
)。
补充:从 21w10a 起,核心着色器引入了真正的顶点着色器,右侧的效果可以被实现了。该区域需要更新。
顶点着色器的主要工作是用一个「投影矩阵」乘坐标。一般来讲这会把一个处于视锥内的顶点转换到一个 2×2×2 的立方体中,这样能够更方便地拍扁到屏幕上:
不过在 Minecraft 中,顶点着色器直接就从一个处于 3 维空间内的平面开始,并且直接变换四个角上的顶点,而不是变换任何实际上的几何体的顶点。Minecraft 使用 ProjMat
这样一个特殊的统一量用来计算,虽然这完全可以用 4 个硬编码(hardcoded)的特例来替代(因为这些顶点总是会被计算到相同的位置上)*。
顶点的初始位置可以从 Position
属性获取,而计算结果应当储存到特殊的 gl_Position
变量当中(这两者都是 vec4
)。
顶点着色器还定义并赋值了一些有用的传递变量,以供片段着色器使用:
texCoord
: 范围从0,0
(左下角)到1,1
(右上角)的坐标。oneTexel
: 在texCoord
所表示的坐标中,一个像素所占的大小。例如:对于一个像素数为 4×2 的输入缓冲,其中每个像素的大小为 0.25×0.5(1/4 的高度,1/2 的宽度)
可运作的示例
sobel.vsh
顶点着色器被绝大多数原版的着色器程序所调用。它并没有什么特别涉及到 sobel(索伯算子?)的东西 —— 这么命名可能只是因为它是 Mojang 第一个使用的着色器程序。
#version 110
attribute vec4 Position;
uniform mat4 ProjMat;
uniform vec2 InSize;
uniform vec2 OutSize;
varying vec2 texCoord;
varying vec2 oneTexel;
void main(){
vec4 outPos = ProjMat * vec4(Position.xy, 0.0, 1.0);
gl_Position = vec4(outPos.xy, 0.2, 1.0);
oneTexel = 1.0 / InSize;
texCoord = Position.xy / OutSize;
}
编写一个片段着色器
片段着色器会为每一个输出像素执行一次,可以读取它的输入缓冲中的任何像素。
每一个像素都是异步计算的,因此片段着色器不能读取输出缓冲中的其他像素,因为那些像素有可能还没有被计算 *。
Sampler
texture2D
函数允许你用一个采样器来获取一个缓冲中的像素。例如:
vec4 centerPixel = texture2D(DiffuseSampler, vec2(0.5, 0.5));
在从缓冲中获取像素时,0.0, 0.0
是左下角,1.0 1.0
是右上角。
这和顶点着色器设置的 texCoord
传递变量一致,十分方便。因此对于当前正在输出的像素,你可以用以下代码来获取相应的位于输入缓冲中的像素:
texture2D(DiffuseSampler, texCoord)
该函数返回一个包含 red
(红)、green
(绿)、blue
(蓝)、opacity
(不透明度)的 vec4
—— 这四者都由一个从 0 到 1 的 float
表示。
oneTexel
代表一个像素(在输入缓冲中)的大小,所以可以用它来偏移指定数量的像素。例如:获取往上数第 3 个像素的颜色:
texture2D(DiffuseSampler, texCoord + vec2(0.0, 3 * oneTexel.y));
如果想要得到以像素为单位的坐标,而不是 0-1 的小数的话,用 OutSize
乘即可。例如:
OutSize; // == vec2(1920, 1080)
texCoord; // == vec2(0.5, 0.5)
OutSize * texCoord; // == vec2(960, 540)
片段着色器应当把输出像素写入到 gl_FragColor
中 —— 另一个由 red
(红)、green
(绿)、blue
(蓝)、opacity
(不透明度)构成的 vec4
。
可运作的示例
以下的着色器:
- 当距离中心的距离在 0.38 至 0.4 之间时,把这个像素变成橙色
- 当距离中心的距离小于 0.38 时,读取放大过的(距离中心更近的)像素
- 当距离中心的距离大于 0.38 时,正常读取对应的输入像素
#version 110
uniform sampler2D DiffuseSampler;
varying vec2 texCoord;
varying vec2 oneTexel;
void main(){
float distFromCenter = distance(texCoord, vec2(0.5, 0.5));
if (distFromCenter < 0.38) {
// 在圆圈里面
vec2 zoomedCoord = ((texCoord - vec2(0.5, 0.5)) * 0.2) + vec2(0.5, 0.5);
gl_FragColor = texture2D(DiffuseSampler, zoomedCoord);
} else if (distFromCenter >= 0.38 && distFromCenter < 0.4) {
// 橙色边框
gl_FragColor = vec4(0.7, 0.4, 0.1, 1.0);
} else {
// 在圆圈外面,正常的像素
gl_FragColor = texture2D(DiffuseSampler, texCoord);
}
}
技巧与难题
发光颜色
发光的实体是一种不易被察觉的向 entity_outline
着色器传递数据的方式,这是因为该着色器可以读取 final
缓冲,并且使其不渲染可见的发光边框效果。
一个具有发光状态效果的实体的轮廓颜色由它所处队伍的颜色决定。实体本身具有的透明度会保留。这使得片段着色器可以读取并使用 16 种不同的颜色以及 256 种不同的透明度值。
(图中添加了棋盘背景以显示出透明度)
阴影
片段着色器可以读取像素,因此我们可以通过像素的颜色来传递信息。不过请小心,有许多因素会使你的材质变暗:
光照等级
远离光源并且不在太阳照射下的方块或实体会变暗。
可以用夜视效果或光源避免。
面阴影
顶面是最亮的,其次是北面和南面,再其次是东面和西面,最暗的是底面。方块和实体都受到这一影响。
若想为方块禁用该阴影效果,可以在模型中将每个元素(element)设置为
"shade": false
。不能为实体禁用!
环境遮挡(「平滑光照」)
角落会变暗。只有方块受到这一影响。
若想为方块禁用该阴影效果,可以在模型中设置
"ambientocclusion":false
。
编译器优化问题
GLSL 编译器会优化那些它认为不会对着色器的输出有影响的代码与变量(输出指的是 gl_FragColor
、gl_Position
以及其他任何传递变量)。
GLSL 编译器找到一个没有被使用的采样器后会把它优化出去,但 Minecraft 仍然会传输同样的缓冲,这就导致了问题:所有的采样器都会读取前一个缓冲的内容。
如果想避免让编译器优化出某个变量,你可以让编译器认为该变量有可能影响着色器的输出结果。例如,texCoord.x
永远不会是 731031
,但编译器并不知道这一点:
if (texCoord.x == 731031) { gl_FragColor = texture2D(DiffuseSampler, texCoord); }
跨帧储存信息
缓冲并不会被自动清空,因此着色器可以在不同帧之间传递信息。
想要获取在上一帧存储的数据很简单:
- 在这一帧中,把数据写入到缓冲
- 在下一帧中,在新的数据被写入缓冲之前读取它
为了让缓冲中的数据能跨刻存在,向一个缓冲写入的着色器程序必须要能让该缓冲中的数据不变。这就有些麻烦了,因为一个着色器程序必须要写入每一个像素,并且在这个过程中不能读取它正在写入的那个缓冲。
一个解决方案是,首先把信息复制到另一个缓冲当中。这时着色器程序就可以读取被复制出来的那个缓冲,同时把新数据写到原有的缓冲当中了,进而能够为每个像素写入它们在上一帧的值。
"passes": [
{
"name": "blit",
"intarget": "foo",
"outtarget": "foo_copy"
},
{
"name": "maybe_update",
"intarget": "minecraft:main",
"auxtargets": [ { "name": "BackupSampler", "id": "foo_copy" } ],
"outtarget": "foo"
}
]
着色器启用顺序
- Minecraft 渲染普通方块和实体到
minecraft:main
- Minecraft 渲染纯色的发光实体到
final
- 运行发光着色器(
entity_outline.json
),其能够访问minecraft:main
与final
缓冲 - Minecraft 渲染其他的东西(手、水、方块实体)到
minecraft:main
- Minecraft 把
final
覆盖到minecraft:main
上 - 运行实体视角着色器,其能够访问
minecraft:main
缓冲
通过发光着色器向 minecraft:main
写入数据(第 3 步)似乎会破坏掉有关深度的信息,使得其他的东西(第 4 步)被渲染到 minecraft:main
的所有东西之上:
Blit 缩放问题
原版默认的 blit 着色器程序在相同大小的缓冲间复制数据时能正常运作,但是会在缓冲大小不一样时出问题。
这是因为顶点着色器 blit.vsh
没有匹配整个输出缓冲。例如,如果输出缓冲是输入缓冲一半的大小,只有一半的输出缓冲会被写入数据:
下方链接是一个 “Clone” 着色器,能够正确地拉伸输入,使其完全匹配到输出缓冲当中:
着色器程序 JSON 文件:
{
"blend": {
"func": "add",
"srcrgb": "one",
"dstrgb": "zero"
},
"vertex": "copy",
"fragment": "blit",
"attributes": [ "Position" ],
"samplers": [
{ "name": "DiffuseSampler" }
],
"uniforms": [
{ "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": "InSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] },
{ "name": "OutSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] },
{ "name": "Time", "type": "float", "count": 1, "values": [ 0.0 ] },
{ "name": "ColorModulate", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }
]
}
.vsh
GLSL 文件:
#version 120
attribute vec4 Position;
uniform mat4 ProjMat;
uniform vec2 OutSize;
uniform vec2 InSize;
varying vec2 texCoord;
// Modified blit to work for copying between buffers of different sizes
void main(){
float x = -1.0;
float y = -1.0;
if (Position.x > 0.001){
x = 1.0;
}
if (Position.y > 0.001){
y = 1.0;
}
gl_Position = vec4(x, y, 0.2, 1.0);
texCoord = Position.xy / OutSize;
}
读取玩家输入
玩家的 Motion
不会在服务端更新,但如果玩家尝试在骑着猪或者矿车时移动的话则会更新,尽管玩家事实上并没有移动。通过让玩家骑着猪或矿车,命令能够读取玩家的 Motion
,并(结合玩家的视角方向)确定玩家当前正按下的方向键。
TODO:示例命令
左键或右键的检测和平时一样(击打生物、右键使用胡萝卜钓竿或与村民交谈)。
任何由命令获取到的信息(例如玩家目前正按着的按键)可以通过一个发光实体的队伍的颜色传递给着色器。
TODO:朝向
旁观者死亡技巧
如果你在旁观一个实体时死亡(例如被 /kill @s
命令干掉),实体旁观着色器在你重生后仍然有效 —— 甚至从旁观者模式换到另外一个游戏模式也不会让该着色器失效。
配合在 1.15 快照中引入的 /spectate
命令与 doImmediateRespawn
游戏规则,我们可以任意启用任何一个实体旁观着色器。
该函数将会启用苦力怕旁观着色器:
TODO:示例命令
不过请注意:
- 这是一个漏洞,因此可能在任何时刻被修复
- 按 F5 等行为会移除该着色器的效果
该指导还未加入的新内容
20w22a 允许着色器访问深度缓冲(depth buffer)。参考原版 jar 文件中的 shaders/post/transparency.json
。
引用源文件以及任意文件:
#moj_import <fog.glsl>
#moj_import "average16x.glsl"
工具和参考
GLSL
教程:
https://www.shadertoy.com/view/Md23DV
https://thebookofshaders.com/
非 Minecraft 的示例:
https://www.shadertoy.com/
SpiderEye
使用 GLIntercept 做的小工具,能让你查看每个缓冲的数据。
补充:目前坏掉了
下载:(国外链接)
v0.1:初步发布
由于该软件的运作会操作游戏的 OpenGL32.dll 文件,可能会被杀软误报。
着色器示例
可以看过去的传送门(国外链接)
后处理着色器
从一个角度捕获画面并显示在传送门中
完整、复杂的着色器运用
调试文字(国外链接)
后处理着色器
通过着色器向屏幕上写入文字或数字
用来调试着色器的工具
屏幕方块(国外链接)
后处理着色器
简单的着色器,将 minecraft:main
缓存显示在一个方块上
易于理解的着色器
水模糊/折射(国外链接)
后处理着色器
加入水模糊与折射
起风了(国外链接)
核心着色器
让树叶与水摇动
由 Mojang 的 Felix Jones 制作
MipInf(国外链接)
核心着色器
将材质变为纯色
由 Mojang 的 Felix Jones 制作