本篇文章由 TeaConMC 采用知识共享-署名-相同方式共享 4.0 国际许可协议进行许可。
引言
发光效果于 Minecraft 1.9 正式引入。发光效果的引入是划时代的:它使得基于着色器的可编程图形管线(Programmable Graphics Pipeline)正式作为不可或缺的游戏特性被引入,而非仅仅通过点击 Super Secret Settings 这一若有若无的按钮,或是当玩家在旁观模式观察生物时才会引起玩家的注意。
发光效果的实际渲染方式需要首先计算特定边缘,然后在计算得到的边缘处绘制外框。这一操作固然可以使用 CPU 完成,但是交给 GPU 计算显然是更好的选择,着色器(Shader)便是用于交给 GPU 计算的小程序,与之有关的编程语言被称为 OpenGL Shader Language,简称 GLSL。
因为计算边缘这一特定需求,因此发光效果必须单独渲染,不能和已有的世界渲染等直接混合(否则世界中其他的「边缘」便会一并囊括进来),这也是我们需要在渲染过程中引入额外帧缓冲(Framebuffer)的必要性所在。
本篇文章将以使工作中的熔炉(Furnace)和高炉(Blast Furnace)发光为目标,演示整个渲染过程。以下是大致的渲染流程:

本文中的示例 Mod ID 为 examplelitfurnacehl
。
Minecraft 中的着色器和帧缓冲
在 Minecraft 1.15.2 中,控制着色器的类为 net.minecraft.client.shader.ShaderGroup
,我们会用到它的以下几个方法:
createBindFramebuffers
:用于调整着色器对应的帧缓冲的长宽。getFramebufferRaw
:用于获取着色器相关联的帧缓冲。render
:为特定的帧缓冲应用着色器。close
:清理内存。
帧缓冲相关的类为 net.minecraft.client.shader.Framebuffer
,我们会用到:
framebufferRenderExt
:把一个帧缓冲中的渲染数据全部渲染到另一个帧缓冲上。bindFramebuffer
:绑定该帧缓冲(亦即接下来的渲染操作全部针对该帧缓冲)。framebufferClear
:清空帧缓冲中的渲染数据。
每个 ShaderGroup
的实例都对应到一个 JSON 文件。通常该 JSON 文件位于资源包中特定 Mod ID 所处资源路径下的 shaders/post
目录中,本文为 assets/examplelitfurnacehl/shaders/post
目录下的 furnace_outline.json
。以下是该 JSON 的全部内容:
1 | { |
targets
代表创建多少相关联的帧缓冲,这里创建了两个:
- 第一个帧缓冲名为
examplelitfurnacehl:swap
。 - 第二个帧缓冲名为
examplelitfurnacehl:final
。
passes
代表应用着色器的渲染次数,这里一共四次,由三组着色器控制:
- 第一次由
minecraft:entity_outline
控制,负责边缘探测。 - 第二次和第三次由
minecraft:blur
控制,负责动态模糊。 - 最后一次由
minecraft:blit
控制,负责单纯复制。
注意动态模糊一共两次,一次是水平方向的,一次是竖直方向的,由下面 uniforms
中 BlurDir
对应的值确定。事实上 uniforms
将会作为 GLSL 的 uniform
输入传递给着色器。
每一组着色器的控制文件位于资源包中特定 Mod ID 所处资源路径下的 shaders/program
目录,比如 assets/minecraft/shaders/program
目录下的 blur.json
。该文件由 Minecraft 本身提供,对应 minecraft:blur
,其中定义了每一次渲染是如何进行的。以下是该文件的大致内容:
1 | { |
blend
代表混合模式。vertex
代表顶点着色器的位置。fragment
代表片元着色器的位置。attributes
代表着色器的attribute
输入,通常只用得到Position
。samplers
代表着色器的sampler2D
输入,通常只用得到DiffuseSampler
。uniforms
代表着色器的uniform
输入和默认值,通常而言它们是固定的。
ShaderGroup
中的每一次渲染,本质上都是将一个帧缓冲中的渲染数据提取出来,重新绘制到另一个帧缓冲上,这使得顶点着色器虽然不是完全没有用处,但一定程度上也有一点鸡肋——只有固定的 1 个面和 4 个顶点,因此不同的 ShaderGroup
复用同一个顶点着色器是很常发生的事情,不过片元着色器相对而言要有用得多。
可能有读者对边缘探测的算法感兴趣,其实就是相当于对整个渲染数据做了一次差分计算,感兴趣的可以进一步了解 Sobel Filter 相关的资料。
Mod 主类
以下是最初的 Mod 主类(已略去 package
和 import
):
1 |
|
我们把 onModelRegistry
和 onRenderWorldLast
两个方法的方法引用作为事件监听器,稍后我们再完善这两个方法的实现。
加载着色器和帧缓冲
由于 ShaderGroup
的相关定义位于资源包中,因此我们需要在资源包重新加载(如按下 F3 + T
)时生成新的 ShaderGroup
,因此我们需要寻找每次重新加载时都触发的事件。在 Minecraft Forge 中,我们可以监听 net.minecraftforge.client.event.ModelRegistryEvent
。
以下是 onModelRegistry
的实现:
1 | private int framebufferWidth = -1; |
注意这里我们还没有调整着色器对应的帧缓冲的长宽,因此我们新建了两个名为 framebufferWidth
和 framebufferHeight
的字段,并且把它们都设成 -1
,稍后我们会在渲染的时候填入正确的值。
mainFramebuffer
是游戏的主帧缓冲,所有玩家能看得到的画面,对应的都是这一帧缓冲的渲染数据。
完成渲染
我们需要在世界渲染完成后在我们自己的帧缓冲上完成渲染,并叠加到游戏的主帧缓冲上,因此我们需要 Minecraft Forge 提供的名为 net.minecraftforge.client.event.RenderWorldLastEvent
的事件。
收集方块数据
首先我们检查 ShaderGroup
是否受支持:
1 | // step 0: check if shaders are supported |
然后遍历客户端世界所有的 TileEntity
,从而确定所有工作中的熔炉和高炉:
1 | // step 1: collect furnaces |
如果不存在这样的 TileEntity
,那么也没有进行下一步渲染的必要了。
设置帧缓冲的长宽
我们还没设置帧缓冲的长宽,我们把长宽缓存到两个字段中,如果发现不一样(比如说玩家调整了窗口的大小等)则重新设置一次。
1 | // step 2: resize our framebuffer |
收集顶点数据
Minecraft 自身提供了 net.minecraft.client.renderer.BufferBuilder
用于收集顶点数据。
1 | private final BufferBuilder bufferBuilder = new BufferBuilder(256); |
开始收集数据(begin
方法)需要两个参数。其中,第一个参数是 GL11.GL_QUADS
,因为是方块数据的默认形式,而第二个参数我们采用了 DefaultVertexFormats.POSITION
,因为我们根本不需要顶点位置之外的任何数据(通常情况下的渲染还需要颜色材质等其他数据)。
此外,注意 matrixStack
需要平移两次,一次针对玩家位置,一次针对方块位置。
渲染到我们的帧缓冲
首先需要绑定我们的帧缓冲。通过分析上面提到的 JSON,我们可以注意到,我们需要绑定的帧缓冲的名称是 examplelitfurnacehl:final
:
1 | // step 4: bind our framebuffer |
然后执行渲染,注意我们:
- 不需要和已有的渲染数据混合
- 不需要绑定任何材质
- 不需要透明度测试
- 不需要深度数据
- 重置颜色
1 | // step 5: render block faces to our framebuffer |
上面有一些设置不是针对可编程图形管线的,但是由于 Minecraft 目前并没有采用纯粹的可编程图形管线(亦即 OpenGL Core Profile),因此还是需要设置一下。
使用着色器渲染
使用着色器渲染不需要绑定特定的帧缓冲。
1 | // step 6: apply shaders |
刚才的 JSON 告诉我们,我们最终仍然渲染到 examplelitfurnacehl:final
,稍后我们会重新用到这一帧缓冲。
渲染到主帧缓冲
渲染之前首先要绑定主帧缓冲:
1 | // step 7: bind main framebuffer |
然后把混合打开,执行最终渲染。注意 Dst
是主帧缓冲,Src
是我们自己的帧缓冲:
1 | // step 8: render our framebuffer to main framebuffer |
收尾
记得把弄乱了的设置复原回去:
1 | // step 9: clean up |
最终效果


TeaConMC 旗下的开源项目 Slide Show 已经将上述特性写进相关代码中,并作为方便创造模式玩家寻找被埋藏的方块的一种解决方案。
作者:TeaCon
- 本文作者: GermMC
- 本文链接: http://blog.germmc.com/2021/02/19/minecraftRender/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!