Minecraft(我的世界)中文论坛

标题: 几个原版着色器示例 [打印本页]

作者: SPGoding    时间: 2019-10-3 22:57
标题: 几个原版着色器示例
本帖最后由 SPGoding 于 2019-10-4 18:53 编辑



前日,本人发布了译文原版着色器指导,其中涉及到了几个有趣的着色器实例,现将相关解析一并发布到技巧版。

建议收藏本帖,本人会在后续更新时使用论坛的通知功能向收藏者发送提醒。另外,请等本人全帖更新完成后再评分,免得我半路咕了。



屏幕方块



作者:u/SirBenet

原帖:reddit

功能:在玩家视野中有发光的实体时,在品红色羊毛上显示玩家屏幕。

原理:

作者首先修改了品红色羊毛的材质为一张 256×256 的图片:



这是一张看似魔幻,实则非常魔幻的图片。

为了与着色器保持统一,本文随后采用 (r, g, b, o)(分别代表红色、绿色、蓝色、不透明度)的方式表示颜色,每个数字的取值均为 0.01.0。如果不透明度为 1.0,本文中会将其省略不写。

这张图片左下角那个像素的颜色为红色 (1.0, 0.0, 0.0),左上角的像素为紫色 (1.0, 0.0, 1.0),右下角的像素为黄色 (1.0, 1.0, 0.0),右上角的像素为白色 (1.0, 1.0, 1.0),其余像素的颜色通过插值计算:



可以发现,这张图片中所有像素的 r 通道均为 1.0,而 gb 通道则看起来有些眼熟。没错,它们与屏幕坐标的 xy 完全对应(下图来自原版着色器指导):



通过这样一张图片,作者极其巧妙地将羊毛材质上的每个像素对应到了玩家屏幕上的像素:

品红色羊毛材质上一个像素的 xy 坐标 -> 该颜色的 gb 通道 -> 玩家屏幕上像素的 xy 坐标。

不过这时又有了一个问题:gb 通道的功能已经知道了,为什么 r 通道要固定为 1.0 呢?这是为了让后续着色器能够知道,这个像素是属于品红色羊毛材质上的,确实应当复制玩家屏幕上的像素到这里。如果着色器不对 r 通道进行限制,那么玩家屏幕上所有的像素都会按照 gb 通道的值显示为屏幕上对应坐标的像素:





现在在游戏中已经得到了这样一个方块:



接下来所要做的,就是编写一个片段着色器:检测当前像素的 r 通道是否为 1.0,如果是,则返回屏幕 (g, b) 位置的像素,否则返回正常的 (x, y) 位置的像素。

片段着色器 assets/minecraft/shaders/program/screens.fsh

  1. #version 120

  2. uniform sampler2D DiffuseSampler;

  3. varying vec2 texCoord;
  4. varying vec2 oneTexel;

  5. void main() {
  6.     vec4 currentPixel = texture2D(DiffuseSampler, texCoord);
  7.     if (currentPixel.r == 1.0) {
  8.         vec2 coordFromColor = currentPixel.gb;
  9.         gl_FragColor = texture2D(DiffuseSampler, coordFromColor);
  10.     } else {
  11.         gl_FragColor = texture2D(DiffuseSampler, texCoord);
  12.     }
  13. }
复制代码

接下来还需要编写一个调用该片断着色器的着色器程序 assets/minecraft/shaders/program/screens.json

  1. {
  2.     "blend": {
  3.         "func": "add",
  4.         "srcrgb": "one",
  5.         "dstrgb": "zero"
  6.     },
  7.     "vertex": "blit",
  8.     "fragment": "screens",
  9.     "attributes": [ "Position" ],
  10.     "samplers": [
  11.         { "name": "DiffuseSampler" }
  12.     ],
  13.     "uniforms": [
  14.         { "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 ] },
  15.         { "name": "InSize",    "type": "float",     "count": 2,  "values": [ 1.0, 1.0 ] },
  16.         { "name": "OutSize",   "type": "float",     "count": 2,  "values": [ 1.0, 1.0 ] }
  17.     ]
  18. }
复制代码

最后是替换掉原有的发光着色器,让它使用我们的着色器程序:assets/minecraft/shaders/post/entity_outline.json

  1. {
  2.     "targets": [
  3.         "swap",
  4.         "final"
  5.     ],
  6.     "passes": [
  7.         {
  8.             "name": "screens",
  9.             "intarget": "minecraft:main",
  10.             "outtarget": "swap"
  11.         },
  12.         {
  13.             "name": "blit",
  14.             "intarget": "swap",
  15.             "outtarget": "minecraft:main"
  16.         }
  17.     ]
  18. }
复制代码

这里借助了一个 swap 缓冲,是因为着色器程序不能往它读取的那个缓冲里面写入数据。

F3 + T 重载资源包,在视野内生成一个发光的实体。然后,你会发现品红色羊毛并没有显示我们屏幕上的内容。为什么?!这是因为,尽管已经在羊毛的模型当中设置了 "ambientocclusion": false"shade": false,方块的材质仍然会不可避免地变暗。事实上,显示出来的 r 通道的数值只有 0.96 左右。因此我们不得不修改一下判断 r 通道大小的那行代码:

  1. -   if (currentPixel.r == 1.0) {
  2. +   if (currentPixel.r > 0.9) {
复制代码

再次重载资源包。



现在屏幕上的内容显示在了原先的品红色羊毛上了。但是我们可以发现,品红色羊毛上显示的「品红色羊毛」仍然是那个魔幻的材质。如果想要做到「递归」那样的效果,应该怎么做呢?一种很简单的做法就是,在 post 文件里面复制粘贴,让该着色器程序多处理几遍:

  1. {
  2.     "targets": [
  3.         "swap",
  4.         "final"
  5.     ],
  6.     "passes": [
  7.         {
  8.             "name": "screens",
  9.             "intarget": "minecraft:main",
  10.             "outtarget": "swap"
  11.         },
  12.         {
  13.             "name": "screens",
  14.             "intarget": "swap",
  15.             "outtarget": "minecraft:main"
  16.         },
  17.         {
  18.             "name": "screens",
  19.             "intarget": "minecraft:main",
  20.             "outtarget": "swap"
  21.         },
  22.         {
  23.             "name": "screens",
  24.             "intarget": "swap",
  25.             "outtarget": "minecraft:main"
  26.         },
  27.         {
  28.             "name": "screens",
  29.             "intarget": "minecraft:main",
  30.             "outtarget": "swap"
  31.         },
  32.         {
  33.             "name": "blit",
  34.             "intarget": "swap",
  35.             "outtarget": "minecraft:main"
  36.         }
  37.     ]
  38. }
复制代码

重载资源包。现在看起来似乎好多了,但又好像有些不对的地方。



其中,本应在地下矿洞中的水竟然显示到了屏幕上,方块选择边框也透过了指向的草方块。但是,原作者的着色器就是这个效果,后续改进是本人自己的私货,放至下一节。





这一现象其实原作者 SirBenet 在写教程的时候就有提及了,以下引自译文的「着色器启用顺序」部分:

  • Minecraft 渲染普通方块和实体到 minecraft:main
  • Minecraft 渲染纯色的发光实体到 final
  • 运行发光着色器(entity_outline.json),其能够访问 > minecraft:mainfinal 缓冲
  • Minecraft 渲染其他的东西(手、水、方块实体)到 minecraft:main
  • Minecraft 把 final 覆盖到 minecraft:main
  • 运行实体视角着色器,其能够访问 minecraft:main 缓冲

通过发光着色器向 minecraft:main 写入数据(第 3 步)似乎会破坏掉有关深度的信息,使得其他的东西(第 4 步)被渲染到 minecraft:main 的所有东西之上。

解决办法似乎很容易想到:不往 minecraft:main,而是往 final 写入数据,不就可以了?我们把 post 文件中的 "outtarget"minecraft:main 改成 final,如下(为了简洁,我把递归的部分暂时先删掉了):

  1. {
  2.     "targets": [
  3.         "swap",
  4.         "final"
  5.     ],
  6.     "passes": [
  7.         {
  8.             "name": "screens",
  9.             "intarget": "minecraft:main",
  10.             "outtarget": "final"
  11.         }
  12.     ]
  13. }
复制代码

但这样又有了一个问题,游戏在第 4 步中渲染的东西(手、水、方块实体)会在第 5 步内被我们输出的 final 覆盖掉,导致玩家看不到这些东西了。因此我们不能让 final 里全部填满内容,而是只修改对应品红色羊毛上显示内容的那几个像素,剩下的像素全部保留原样。有关保留原样,SirBenet 大佬在原版着色器指南的跨帧储存信息一节已经有过讲解。

修改着色器程序,加入新的采样器,用于访问我们过会儿复制的缓冲。

  1. {
  2.     "samplers": [
  3.         { "name": "DiffuseSampler" },
  4.         { "name": "BackupSampler" }
  5.     ],
  6.     ...
  7. }
复制代码

修改片段着色器:

  1. #version 120

  2. uniform sampler2D DiffuseSampler;
  3. uniform sampler2D BackupSampler; // 新加的采样器

  4. varying vec2 texCoord;
  5. varying vec2 oneTexel;

  6. void main() {
  7.     vec4 currentPixel = texture2D(DiffuseSampler, texCoord);
  8.     if (currentPixel.r > 0.9) {
  9.         vec2 coordFromColor = currentPixel.gb;
  10.         gl_FragColor = texture2D(DiffuseSampler, coordFromColor);
  11.     } else {
  12.         gl_FragColor = texture2D(BackupSampler, texCoord); // 改用新加的 BackupSampler 采样器,输出原有像素
  13.     }
  14. }
复制代码

最后改一下 post 文件的 passes,在处理前把 final 缓冲复制到 backup 缓冲当中,然后指定 "auxtargets",使着色器程序能够访问 backup

  1. {
  2.     "targets": [
  3.         "swap",
  4.         "final",
  5.         "backup"
  6.     ],
  7.     "passes": [
  8.         {
  9.             "name": "blit",
  10.             "intarget": "final",
  11.             "outtarget": "backup"
  12.         },
  13.         {
  14.             "name": "screens",
  15.             "auxtargets": [{ "id": "backup", "name": "BackupSampler" }],
  16.             "intarget": "minecraft:main",
  17.             "outtarget": "final"
  18.         }
  19.     ]
  20. }
复制代码



重载后发现,(在视频设置为「流畅」时)树叶莫名变粉了。以下是本人的臆测,需要进一步证实或否定,请不要轻信:

树叶中有部分像素是透明的。之前这一部分像素绘制在了 minecraft:main 缓冲中,游戏会在显示时把它们显示为纯黑色。而现在我们将它绘制到了 final 缓冲中,游戏正常处理这些透明像素,导致 final 缓冲下层的品红色羊毛的材质显示了出来,树叶变成了「粉色」。

因此,在片段着色器中,如果坐标对应 gb 的像素是透明的,我们应当改为输出黑色像素,这样才能显示出和原先树叶一致的效果。

  1. #version 120

  2. uniform sampler2D DiffuseSampler;
  3. uniform sampler2D BackupSampler;

  4. varying vec2 texCoord;
  5. varying vec2 oneTexel;

  6. void main() {
  7.     vec4 currentPixel = texture2D(DiffuseSampler, texCoord);
  8.     if (currentPixel.r > 0.9) {
  9.         vec2 coordFromColor = currentPixel.gb;
  10.         vec2 ans = texture2D(DiffuseSampler, coordFromColor);
  11.         // 在此处加一个判断,看对应像素是不是透明的。
  12.         if (ans[3] < 1) {
  13.             // 是的话,就输出黑色像素。
  14.             gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  15.         } else {
  16.             // 否则仍跟原来一样,输出对应像素。
  17.             gl_FragColor = ans;
  18.         }
  19.     } else {
  20.         gl_FragColor = texture2D(BackupSampler, texCoord);
  21.     }
  22. }
复制代码



现在似乎正常了:屏幕中的水、玩家的手、羊毛中的树叶都正常显示了。

现在剩下的只有递归显示了。这时的递归比之前要麻烦一些。「屏幕方块」上的内容是绘制在 final 缓冲的,而内容的来源却是 minecraft:main 缓冲,因此我们需要在递归时,让片段着色器检测 final 缓冲中像素是否符合品红色羊毛材质的颜色,符合的话则读取 minecraft:main 缓冲中的对应像素。我们规定,使用默认的 DiffuseSampler 采样器获取可能带有品红色羊毛材质的缓冲,使用新定义的 SourceSampler 采样器来获取带有玩家屏幕内容的缓冲(即 minecraft:main 缓冲)。

在着色器程序的 "samplers" 中补上要新定义的 SourceSampler

  1. {
  2.     "samplers": [
  3.         { "name": "DiffuseSampler" },
  4.         { "name": "BackupSampler" },
  5.         { "name": "SourceSampler" }
  6.     ],
  7.     ...
  8. }
复制代码

然后魔改一下片段着色器:

  1. #version 120

  2. uniform sampler2D DiffuseSampler;
  3. uniform sampler2D BackupSampler;
  4. uniform sampler2D SourceSampler; // 定义个新采样器

  5. varying vec2 texCoord;
  6. varying vec2 oneTexel;

  7. void main() {
  8.     vec4 currentPixel = texture2D(DiffuseSampler, texCoord);
  9.     if (currentPixel.r > 0.9) {
  10.         vec2 coordFromColor = currentPixel.gb;
  11.         vec4 ans = texture2D(SourceSampler, coordFromColor); // 改成从 SourceSampler 采样器获取屏幕内容
  12.         if (ans[3] < 1) {
  13.             gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  14.         } else {
  15.             gl_FragColor = ans;
  16.         }
  17.     } else {
  18.         gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
  19.     }
  20. }
复制代码

最后,修改 post 文件,完成递归:

  1. {
  2.     "targets": [
  3.         "swap",
  4.         "final",
  5.         "backup"
  6.     ],
  7.     "passes": [
  8.         {
  9.             "name": "blit",
  10.             "intarget": "final",
  11.             "outtarget": "backup"
  12.         },
  13.         {
  14.             "name": "screens",
  15.             "auxtargets": [
  16.                 { "id": "backup", "name": "BackupSampler" },
  17.                 { "id": "minecraft:main", "name": "SourceSampler" }
  18.             ],
  19.             "intarget": "minecraft:main",
  20.             "outtarget": "swap"
  21.         },
  22.         {
  23.             "name": "blit",
  24.             "intarget": "swap",
  25.             "outtarget": "backup"
  26.         },
  27.         {
  28.             "name": "screens",
  29.             "auxtargets": [
  30.                 { "id": "backup", "name": "BackupSampler" },
  31.                 { "id": "minecraft:main", "name": "SourceSampler" }
  32.             ],
  33.             "intarget": "swap",
  34.             "outtarget": "final"
  35.         },
  36.         {
  37.             "name": "blit",
  38.             "intarget": "final",
  39.             "outtarget": "backup"
  40.         },
  41.         {
  42.             "name": "screens",
  43.             "auxtargets": [
  44.                 { "id": "backup", "name": "BackupSampler" },
  45.                 { "id": "minecraft:main", "name": "SourceSampler" }
  46.             ],
  47.             "intarget": "final",
  48.             "outtarget": "swap"
  49.         },
  50.         {
  51.             "name": "blit",
  52.             "intarget": "swap",
  53.             "outtarget": "backup"
  54.         },
  55.         {
  56.             "name": "screens",
  57.             "auxtargets": [
  58.                 { "id": "backup", "name": "BackupSampler" },
  59.                 { "id": "minecraft:main", "name": "SourceSampler" }
  60.             ],
  61.             "intarget": "swap",
  62.             "outtarget": "final"
  63.         },
  64.         {
  65.             "name": "blit",
  66.             "intarget": "final",
  67.             "outtarget": "backup"
  68.         },
  69.         {
  70.             "name": "screens",
  71.             "auxtargets": [
  72.                 { "id": "backup", "name": "BackupSampler" },
  73.                 { "id": "minecraft:main", "name": "SourceSampler" }
  74.             ],
  75.             "intarget": "final",
  76.             "outtarget": "swap"
  77.         },
  78.         {
  79.             "name": "blit",
  80.             "intarget": "swap",
  81.             "outtarget": "final"
  82.         }
  83.     ]
  84. }
复制代码

(这是真的长…)



那么,本节我们到底都做了些什么呢?


经过如此一通改良的屏幕方块,总算是避免了原先水等东西的透视。但它仍有一个问题,chyx 在本帖板凳就有指出:

也不是没用过这个…但是用了之后所有红色分量够高的地方都在闪………
包括白色,红色什么的地方……
倒是这个方块自己在天黑之后就不认了XD

在上图中我们也能发现,本身应是白色的花、应有白色发光的盔甲架的白色部分都变了。这是因为我们的片段着色器中只判定了 r 的大小是否 > 0.9,因此很容易误伤红色分量够高的像素。

每一个像素实际上是一个 vec4 向量,目前我们只利用到了 rgb 三个分量。如果把 a 分量也用上,将其与 r 分量结合起来一同判定是否应当修改这个像素,是不是会更加准确呢?

下一节将会以此思路,重新对「屏幕方块」进行改良。



然后,又咕了。

[groupid=546]Command Block Logic[/groupid]
作者: wshycaa    时间: 2019-10-4 00:39
坐等屏幕着色器完整版出炉~~
作者: chyx    时间: 2019-10-4 05:44
也不是没用过这个…但是用了之后所有红色分量够高的地方都在闪………
包括白色,红色什么的地方……
倒是这个方块自己在天黑之后就不认了XD
作者: mc能吃吗    时间: 2019-10-4 10:11
XD?为什么我想到了刘慈欣的那个什么量子计算机里的宇宙模型。。

话说可以做镜子出来。。。吧
还有多人游戏复制其他玩家的视角?强大XD
作者: chyx    时间: 2019-10-4 16:47
mc能吃吗 发表于 2019-10-4 10:11
XD?为什么我想到了刘慈欣的那个什么量子计算机里的宇宙模型。。

话说可以做镜子出来。。。吧

做你所说的效果估计得高频tp。
作者: 神经幺    时间: 2020-3-6 20:26
不愧是spg!普通人根本做不到的事情,轻轻松松就可以做出来!实在是太厉害了!(咕咕咕)
作者: 夜·北冥    时间: 2020-4-29 14:25
这个我感觉跟摄像头差不多,就是可以被看到
作者: 若梦丶浮生    时间: 2020-12-12 19:28
提示: 作者被禁止或删除 内容自动屏蔽




欢迎光临 Minecraft(我的世界)中文论坛 (https://www.mcbbs.net/) Powered by Discuz! X3.5