[原创]在MC中如何使用简单的着色器
-
楔子
故事要从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还是靠谱的。
就这样,如果觉得我的教程有用处,请夸我两句。