MCJE着色器教程:从开发入门到游戏崩溃(五)——很多的栗子
准备好了吗?下面就是实战阶段啦~
我想把这玩意儿染成绿的!
我们的需求:让屏幕变成一片绿色!
要把整个视野染成绿色,也就是说,每一个像素都是直接输出为绿色的。那么,我们的代码就非常好实现了,只需要把片段着色器中用于输出像素颜色的out类型变量fragColor
赋值为绿色对应的颜色分量就可以了。即fragColor = vec4(0.0,1.0,0.0,1.0);
。没错,着色器就只有这一行代码起作用。完整的代码如下:
#version 140 uniform sampler2D DiffuseSampler; in vec2 texCoord; out vec4 fragColor; void main(){ fragColor = vec4(0.0,1.0,0.0,1.0); }
首先声明版本号,然后是一些必须的变量,不管它有没有被使用,先写上再说。接下来是程序的入口main函数,在里面写上赋值fragColor的那一行代码就完事。我将这个着色器文件命名为green.fsh。
嗯,着色器就是这么简单。接下来就是着色器程序JSON。我们没有用到额外的uniform变量或者采样器,因此只需要关注vertex和fragment,即顶点着色器和片段着色器即可。我们没有对游戏窗口的显示区域进行修改,因此就用原版的顶点着色器就好了,即sobel着色器。片段着色器就是我们刚刚写的着色器,即green。注意,这里是不用写文件拓展名的。于是,我们的着色器程序JSON就是长下面这样的:
{ "blend": { "func": "add", "srcrgb": "1", "dstrgb": "0" }, "vertex": "sobel", "fragment":"green", "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 ] } ] }
如果不能完全看懂这个JSON文件,建议回到上一节复习一下。这个着色器程序JSON我们命名为green.json
最后就是后处理JSON。这里,我们都会选取entity_outline.json
作为替换对象。
显然,我们只需要对像素进行一步处理即可,因此我们只需要一个缓冲。因为entity_outline.json
必须含有一个名为final
的缓冲。我们将minecraft:main
输入我们刚刚的green着色器中并输出到final,然后将final
用blit
着色器复制到minecraft:main
中。因此我们的后处理JSON是这样的:
{ "targets": [ "final" ], "passes": [ { "name": "green", "intarget": "minecraft:main", "outtarget": "final" }, { "name": "blit", "intarget": "final", "outtarget": "minecraft:main" } ] }
最后,检查一下你的JSON有没有写错别字什么的,还有文件文件夹是不是都正确了。强烈建议把日志输出打开,这样就能很清楚地看到你的着色器出了什么问题。
如果一切正确,进入游戏,加载(或者F3+T重载)资源包以后,放下一个盔甲架,给予它发光(glowing)效果。当盔甲架进入你的视野时,你会看到你的屏幕变成了一片绿色,就像开头我们的看到的效果图一样。
狐萝卜
我们的需求:在屏幕上显示一张图片!
首先我们来写后处理Json。这次我们使用的是transparency.json作为替换对象。值得注意的是,使用这个作为替换对象的时候,应当注意它的targets中必须有一些固定的缓冲。因此你的后处理Json文件将会变得很长。不过,我们要做的只需要在原版的transparency.json上进行修改即可,添加自己的缓冲,然后在原版的渲染过程之后添加自己的着色器,对已经原版渲染好的画面再加工。
{ "targets": [ "water", "translucent", "itemEntity", "particles", "clouds", "weather", "final", "awa" ], "passes": [ { "name": "transparency", "intarget": "minecraft:main", "outtarget": "final", "auxtargets": [ { "name": "DiffuseDepthSampler", "id": "minecraft:main:depth" }, { "name": "TranslucentSampler", "id": "translucent" }, { "name": "TranslucentDepthSampler", "id": "translucent:depth" }, { "name": "ItemEntitySampler", "id": "itemEntity" }, { "name": "ItemEntityDepthSampler", "id": "itemEntity:depth" }, { "name": "ParticlesSampler", "id": "particles" }, { "name": "ParticlesDepthSampler", "id": "particles:depth" }, { "name": "CloudsSampler", "id": "clouds" }, { "name": "CloudsDepthSampler", "id": "clouds:depth" }, { "name": "WeatherSampler", "id": "weather" }, { "name": "WeatherDepthSampler", "id": "weather:depth" } ] }, { "name": "blit", "intarget": "final", "outtarget": "minecraft:main" }, { "name": "pic", "intarget": "minecraft:main", "outtarget": "awa", "auxtargets":[ { "name":"Picture", "id":"test", "width":100, "height":100, "bilinear":true } ] }, { "name": "blit", "intarget": "awa", "outtarget": "minecraft:main" } ] }
仔细观察即可发现,我们只是添加了一个名为awa的缓冲,然后在原版的渲染之后哦添加了两个新的项作为我们自己的渲染过程。在我们添加的两项中,第一个才起到了真正的作用,在屏幕上输出图片,而第二个只是将我们渲染好的缓冲重新复制到minecraft:main中。毕竟,输入缓冲和输出缓冲不能相同。那么我们现在着重来看第一个。
{ "name": "pic", "intarget": "minecraft:main", "outtarget": "awa", "auxtargets":[ { "name":"Picture", "id":"test", "width":100, "height":100, "bilinear":true } ] }
如果我们把它和前面的例子比较,会发现我们额外写了一个auxtargets。前面说过,auxtargets是用于额外声明一些需要传递到着色器程序Json中的缓冲区。它可以是targets中已经声明过的缓冲,也可以是资源包中texture/effect下的一个图片。这里我们调用的是一个图片,即test.png。因为我们把它放在了minecraft命名空间下,因此不用在前面加上命名空间前缀。我们的图片像素是100*100,因此把width和height均设置为100。你也可以试试不设置成100会产生的效果。
接下来我们来写着色器程序Json。
{ "blend": { "func": "add", "srcrgb": "srcalpha", "dstrgb": "1-srcalpha" }, "vertex": "sobel", "fragment":"pic", "attributes":["Position"], "samplers":[ {"name":"DiffuseSampler"}, {"name":"Picture"} ], "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 ] } ] }
事实上,除了在samplers中多了一个Picture以外,这个着色器处理Json和上面的例子几乎没有区别(请忽略blend中的差异)。这个Picture就是我们前面后处理Json中auxtargets定义的缓冲区的名字。它将会传入我们的片段着色器中,让着色器得以调用。而DiffuseSampler则是Minecraft原来的画面。
最后,就是片段着色器的编写了。因为现在有两个sampler,因此除了DiffuseSampler,还会有一个名为Picture的uniform变量。在效果演示图片中,我们以屏幕为中心,周围的一定半径内显示的是我们的图片,而半径之外则显示的是正常的minecraft画面。因此,我们需要判断目前渲染的像素位置是否在半径以内。我们知道,vec2类型的texCoord即为着色器目前渲染的像素的位置,那么我们要做的就是,计算texCoord到屏幕中央,即(0.5,0.5)的距离,然后用if语句判断距离是否小于半径。而在GLSL中,提供了一个名为distance的函数,它以两个坐标作为参数,用于计算两个点之间的距离。于是,我们很容易便能写出下面的代码。
#version 150 uniform sampler2D DiffuseSampler; uniform sampler2D Picture; in vec2 texCoord; out vec4 fragColor; void main(){ float distFromCenter = distance(texCoord, vec2(0.5, 0.5)); if (distFromCenter < 0.4) { // 在圆圈里面 fragColor = texture2D(Picture, texCoord); } else { // 在圆圈外面,正常的像素 fragColor = texture2D(DiffuseSampler, texCoord); } }
texture2D函数的作用便是,获取某一个sampler(第一个参数)的某一个位置(第二个参数)的像素。我们在if和else中分别获取了图片和minecraft原画面中的缓冲对应位置的像素,并输出到屏幕上的这个位置。当半径小于0.4时,将会获取到的是图片上的像素并输出;而半径大于0.4时,将会获取到的是原来的画面上的像素并进行输出。这里fragColor对应的也是texCoord所决定的位置的像素,因此半径之外,画面还是原来的画面,而半径之内的画面则被替换为图片。
现在,打开极佳画质,让我们看看我们得到的效果。
嗯?图片为什么反过来了呢?我们的图片明明是正的呀。这是因为OpenGL要求y轴0.0
坐标是在图片的底部的,但是图片的y轴0.0
坐标通常在顶部。因此,我们需要手动纠正一下图片的坐标。
// 在圆圈里面 //fragColor = texture2D(Picture, texCoord); vec2 invertedTexCoord = vec2(texCoord.x, 1 - texCoord.y); fragColor = texture2D(Picture, invertedTexCoord);
我们声明了另一个二维向量用来储存我们翻转厚的坐标。它的x值和texCoord一样,但是y值是1-texCoord的y值。这样,我们就把y轴手动翻转了一下,然后用翻转后的坐标获取图片的像素,现在图片就是正向的啦。