跳转至内容

[原创]在MC中如何使用简单的着色器

魔改早教
3 2 201 2
  • 楔子

    故事要从gta5说起,我很早以前就想做一个gta5那样的击杀动画,正反馈特别足,又不会遮挡屏幕。
    效果上,说复杂也并不复杂,除去淡入淡出,其实只是色彩饱和度降低,然后色彩亮度提高。
    但是技术上,略微麻烦,最后选择搓着色器。
    着色器的来头我就不介绍了,作为娱乐向开发人员,先用着再去学就好。你说系统学习的学院派?那还看我这个作甚?
    注意,本篇教程并不是正经教你使用着色器,只是面向新人的简单引导,引导你在mc中使用简单的着色器,据大佬所说,着色器教程遍地都是,根本不用写。
    那么,就请多指教了。

    一、如何搭建最简单的着色器实例

    首先,在资源目录中准备以下文件:

    resources/
      assets/modid/
        shaders/
          post/light.json
          program/light.json
          program/light.fsh
    

    没错,只需要准备三个文件,两个json,一个fsh。
    首先是这个post/light.json,简单来理解就是shader的资源声明。
    注意,我说的shader是指mc的shader资源,与着色器区分开。

    {
      "targets": ["swap"],
      "passes": [
        {
          "name": "modid:light",
          "intarget": "minecraft:main",
          "outtarget": "swap"
        },
        {
          "name": "blit",
          "intarget": "swap",
          "outtarget": "minecraft:main"
        }
      ]
    }
    

    想要详细学习的话,还是看看正经教程吧,本文只作基本的说明。
    上面这个json描述了这个着色器是如何运作的:
    把minecraft:main缓冲区作为输入,用modid:light着色器处理,结束后输出到swap缓冲区;
    把swap缓冲区作为输入,用blit着色器处理,结束后输出到minecraft:main缓冲区。
    这里的blit着色器是mc自带的,别管,直接用就好,本文的技术水准还没到“blit又不合适,局限性太大了”的水平。
    总之,照抄即可,唯一要改动的就是"modid:light"。

    接着是program/light.json,上面不是说,shader和着色器做一下区分嘛,那post里的是shader的资源声明,这里的就是着色器的资源声明了。
    post/light.json里说到要用modid:light这个着色器处理,所以我们来声明这个着色器资源了。

    {
      "vertex": "blit",
      "fragment": "modid:light",
      "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": "OutSize", "type": "float",     "count": 2,  "values": [ 1.0, 1.0 ] }
      ]
    }
    

    这里面需要重点关注的就只有fragment这一项,所谓vertex和fragment,就是指顶点着色器和片段着色器,学院派可以找教程详细学习一下。
    这个fragment对应的就是program/light.fsh,不过可以不加后缀。
    uniform后面再讲,对于一个着色器来说,倒也不是必须的。

    最后是program/light.fsh,在program/light.json中有声明使用的片段着色器是modid:light,现在就要把它写出来。
    真正的着色器写法是OpenGL的范畴,我写的是“在MC中如何使用简单的着色器”,因而太OpenGL的部分我就不介绍了。
    在本文的环境前提下,一个最简单的片段着色器是这样的:

    #version 150
    
    uniform sampler2D DiffuseSampler;
    in vec2 texCoord;
    out vec4 finalColor;
    
    void main() {
        vec4 color = texture(DiffuseSampler, texCoord);
        vec4 finalColor = color;
    }
    

    version嘛,以本文的技术水准,其实无所谓。
    片段着色器干了什么事呢?着色器把当前帧画面的每个像素都拿出来让它处理一下再贴回去,就这样。
    别的就不说了,重点是获取当前处理的像素,和输出的结果。
    这里面的color就是获取到的像素颜色,照抄就行。像素的颜色,这边用的是rgba色系,红绿蓝加一个透明度。
    可能大多数人更习惯色相+饱和度+亮度的hsv格式,你可以让鲸鱼娘帮你转换一下。
    这rgba嘛,按我这样来,四个数都是0.0 ~ 1.0,而不是传统0 ~ 255,不过也不影响使用。
    举个例子,我要让屏幕色彩变亮,可以这么写:

    vec4 color = texture(DiffuseSampler, texCoord);
    vec3 lightColor = vec3(color) * 1.5;
    vec4 finalColor = vec4(lightColor, 1.0);
    

    啥,vec也看不懂?我可不教线性代数或matlab哦,多看看例子会用了就好,大不了求助鲸鱼娘。
    总之,我这里把红绿蓝三种颜色值都乘了1.5,众所周知,rbg(255 255 255)是白色,所以我这里是把整体色彩亮度提高了。
    然后交给finalColor输出。这个输出是由out vec4 finalColor定义的,照抄就好。
    至此,我们便完成了很简单的着色器实例,效果是让屏幕色彩变亮。

    二、如何调用着色器

    虽然,但我们用的这个,其实不是正派shader,ShaderInstance我也没研究是咋用的,本文涉及的叫EffectInstance。
    废话不多说,先看看最简单的调用:

    Minecraft.getInstance().gameRenderer.loadEffect(new ResourceLocation(MODID, "shader/post/light.json"));
    

    调试时,你也可以用mc自带的着色器先试试:

    Minecraft.getInstance().gameRenderer.loadEffect(new ResourceLocation("minecraft", "shader/post/invert.json"));
    

    这个invert是负片效果。如果自带的能跑而你写的不能跑,那就肯定是你有问题了。

    如果要关闭着色器:

    Minecraft.getInstance().gameRenderer.shutdownEffect();
    

    只是调试和简单使用的话,这套就够用了,但局限性在于,这样的加载着色器是替换式的,如果你有多个着色器或其它模组也这样用,那就只能同时加载一个,加载新的就会替换掉旧的。
    不过,往好的方面想,大家嫌不好用,都不用这个方法,只有你用,就没人跟你冲突了!

    正经的着色器管理后面会再讲到。

    三、如何向着色器传递参数

    当你知道如何调试着色器之后,就该研究如何拓展业务了,首先就该推出一个动态变化的新产品并享有三个月内公司资源倾斜的新品优先排货特权
    楔子中说到,最终想做的击杀特效有淡入淡出的效果,这就要求着色器能动态变化,这部分要用前文略过的uniform来传递参数。
    首先跟我一起改造一下着色器的资源文件:
    post/light.json

    // ......
        {
          "name": "mafuyusflashlight:flashlight",
          "intarget": "minecraft:main",
          "outtarget": "swap",
          "uniforms": [{
              "name": "IntensityAmount",
              "values": [ 1.0 ]
          }]
        },
    // ......
    

    program/light.json

    // ......
      "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": "OutSize",       "type": "float",     "count": 2,  "values": [ 1.0, 1.0 ] },
        { "name": "IntensityAmount", "type": "float", "count": 1, "values": [ 1.0 ] }
      ]
    // ......
    

    上面这两处修改是声明了一个新的uniform,并给了一个默认值,IntensityAmount就是之后传参的变量名。
    接着再把这个变量加到片段着色器中:
    program/light.fsh

    // ......
    uniform float IntensityAmount;
    uniform sampler2D DiffuseSampler;
    in vec2 texCoord;
    out vec4 fragColor;
    // ......
    

    这样一来,片段着色器中就能接收渲染线程中传过来的参数,并进行一些神秘的动态变化。
    我这里的IntensityAmount指的是效果强度,那我就可以这样使用:

    vec3 lightColor = vec3(color) * (1 + 0.5 * IntensityAmount);
    

    如此这般,咱们的着色器就可以进行动态变化了,如果IntensityAmount为零,效果等于没有。
    需要注意的是,传进来的uniform一定要用上,用不上就删了,不然会报错。

    现在着色器能处理传参了,剩下的就是如何传参。
    设置uniform的方法是有的,不过我看来看去都比较麻烦,我用的是mixin的方法:

    @Mixin(value = PostChain.class)
    @Implements(@Interface(iface = PostChainAccessor.class, prefix = "lazy$"))
    public class PostChainMixin implements PostChainAccessor {
        @Shadow @Final private List<PostPass> passes;
        public List<PostPass> getPasses() {
            return passes;
        }
    }
    
    public interface PostChainAccessor {
        List<PostPass> getPasses();
    }
    

    最后是详细用法,简单封装了一下。

    public static void setUniform(String name, String key, float value) {
        Minecraft mc = Minecraft.getInstance();
        if (mc.gameRenderer.currentEffect() != null) {
            PostChainAccessor postChain = (PostChainAccessor) mc.gameRenderer.currentEffect();
            for (int i = 0; i < postChain.getPasses().size(); i++) {
                EffectInstance effect = postChain.getPasses().get(i).getEffect();
                if (effect.getName().equals(new ResourceLocation(MODID, name).toString())) {
                    effect.safeGetUniform(key).set(value);
                }
            }
        }
    }
    setUniform("light", "IntensityAmount", 0.5f);
    

    具体的着色器名称是"modid:light"这样的,取决于post/light.json里的声明。
    有更方便的改法欢迎评论,我单纯是懒得翻源码了。上面这一段,如果学院派对原理有兴趣,也可以看看源码是怎么一回事,但一般来说能用就行了。

    最后,在tick事件里动点手脚,就能实现淡入淡出了,我懒,就不写了。

    四、如何管理着色器

    我的轮子拿去凑合用就行:

    @Mod.EventBusSubscriber(modid = MODID, value = Dist.CLIENT)
    public class EffectManager {
        private static final Minecraft mc = Minecraft.getInstance();
        public static final Map<String, PostChain> CHAINS = new LinkedHashMap<>();
    
        @SubscribeEvent
        public static void onRegisterClientReloadListeners(RegisterClientReloadListenersEvent event) {
            event.registerReloadListener((ResourceManagerReloadListener) resourceManager -> {
                mc.execute(EffectManager::initAll);
            });
        }
    
        public static List<PostPass> getEffect(String name) {
            PostChainAccessor postChain = (PostChainAccessor) CHAINS.get(name);
            return postChain.getPasses();
        }
    
        public static void loadEffect(String name, String jsonPath) {
            if (!CHAINS.containsKey(name)) CHAINS.put(name, createPostChain(jsonPath));
        }
        public static boolean isLoading(String name) {
            return CHAINS.containsKey(name);
        }
    
        public static void initAll() {
            CHAINS.replaceAll((name, chain) -> createPostChain(chain.getName()));
        }
    
        @SubscribeEvent
        public static void onRenderLevelStage(RenderLevelStageEvent event) {
            if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_LEVEL) {
                CHAINS.values().forEach(chain -> {
                    chain.resize(mc.getWindow().getWidth(), mc.getWindow().getHeight());
                    chain.process(event.getPartialTick());
                });
                mc.getMainRenderTarget().bindWrite(false);
            }
        }
    
    
        public static void clean(String name) {
            if (CHAINS.containsKey(name)) {
                CHAINS.get(name).close();
                CHAINS.remove(name);
            }
        }
    
        public static void cleanup() {
            CHAINS.values().forEach(PostChain::close);
            CHAINS.clear();
        }
    
        private static PostChain createPostChain(String name) {
            ResourceLocation rl = new ResourceLocation(MODID, name);
            try {
                return new PostChain(mc.getTextureManager(), mc.getResourceManager(), mc.getMainRenderTarget(), rl);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    

    原理嘛,其实就是不用mc给的loadEffect方法,而是自己在渲染事件里按顺序手动处理。
    也因此可以同时加载多个着色器,但还是要有个先来后到的。
    至于uniform,可以这样用:

    EffectManager.loadEffect("light", "shader/post/light.json");
    EffectManager.getEffect("light").forEach(postPass -> {
        EffectInstance effect = postPass.getEffect();
        if (effect.getName().equals("modid:light")) {
            effect.safeGetUniform("IntensityAmount").set(0.5);
        }
    });
    

    这当然不是最简的写法,大伙用的时候可以再封装一下,我只是举例子,就摆了。

    五、常见兼容性问题

    首当其冲的就是和oculus的冲突,如果你的着色器在开光影的时候黑屏了,请不要灰心,因为原版的部分着色器也会在开光影时黑屏,比如blur,这并不少见。
    具体原因具体分析,多半是写法有冲突,比如blur后来我重写了一个高斯模糊,就不黑屏了。
    没思路建议求助鲸鱼娘。
    还有就是和其它改动了渲染的模组冲突,比如超分辨率之类的,也是具体情况具体分析,我只是提一嘴。
    虽然感觉问题不少,但能列的还真不多,之后再补充吧。

    六、结语

    本文只是一个简单的引导,引导你在mc中使用简单的着色器,正派的着色器教程请搜索GLSL。
    如果本文达不到你的预期,你可以撰写更好的替代教程,毕竟本文的技术水准真不高,完全是面向娱乐魔改萌新的。
    大学也没有mc魔改专业……
    着色器的进阶使用,学院派可以开始翻阅各种文档了,据说遍地都是,而实证派,我推荐可以多请教鲸鱼娘,写写简单的GLSL还是靠谱的。
    就这样,如果觉得我的教程有用处,请夸我两句。

  • 最后效果是什么

  • 最后效果是什么

    @mihono 例子里的效果就是屏幕色彩亮度提高50%


相关推荐