@mihono 例子里的效果就是屏幕色彩亮度提高50%
真冬
-
[原创]在MC中如何使用简单的着色器 -
[原创]在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还是靠谱的。
就这样,如果觉得我的教程有用处,请夸我两句。 -
[原创][KubeJS]简单的以tick实现的嘲讽I know, man, 但你必须先攻击那个……
-
已经,没有modding的理由了@不是椰浆 尼的牌子豪多
-
[1.20.1]强制加载并渲染特定区块@忆然 除了勤劳跟踪狂,我都不知道在哪能用上2333
-
[1.20.1]强制加载并渲染特定区块我也不知道有什么实用价值但反正先写着。
思路:
1)强加载区块
2)主动发送区块数据
3)强制渲染区块强加载
首先是强加载区块,我写了一个简单的类实现:
public class ChunkLoader { private static final HashMap<String, ArrayList<ChunkPos>> loaders = new HashMap<>(); public static void add(ServerLevel level, ChunkPos center) { String key = level.dimension().toString(); if (!loaders.containsKey(key)) loaders.put(key, new ArrayList<>()); if (!loaders.get(key).contains(center)) { loaders.get(key).add(center); ForgeChunkManager.forceChunk(level, MODID, center.getMiddleBlockPosition(0), center.x, center.z, true, true); } } public static void removeAll(ServerLevel level) { String key = level.dimension().toString(); if (!loaders.containsKey(key)) return; if (loaders.get(key).isEmpty()) return; Iterator<ChunkPos> iterator = loaders.get(key).iterator(); while (iterator.hasNext()) { ChunkPos center = iterator.next(); iterator.remove(); ForgeChunkManager.forceChunk(level, MODID, center.getMiddleBlockPosition(0), center.x, center.z, false, false); } } }写得比较草率,主打一个能用就行。
这是在服务端运行的,区块要加载到服务端的区块缓存(ServerChunkCache)中,才能发送对应的区块数据。
如果想安全地强加载区块,请使用Ticket系统,我这里只是分享思路,就简单用forceChunk方法敷衍过去了。发送区块数据
这部分是mixin得到的,把ChunkMap中的playerLoadedChunk方法拿出来用,最终发送的是一个ClientboundLevelChunkWithLightPacket包。
@Mixin(value = ChunkMap.class) @Implements(@Interface(iface = IChunkMap .class, prefix = "lazy$")) public abstract class ChunkMapMixin implements IChunkMap { @Shadow @Nullable protected abstract ChunkHolder getVisibleChunkIfPresent(long p_140328_); @Shadow protected abstract void playerLoadedChunk(ServerPlayer p_183761_, MutableObject<ClientboundLevelChunkWithLightPacket> p_183762_, LevelChunk p_183763_); public void loadLevelChunk(ServerPlayer player, ChunkPos chunkPos) { ChunkHolder chunkholder = this.getVisibleChunkIfPresent(chunkPos.toLong()); if (chunkholder == null) return; LevelChunk levelchunk = chunkholder.getTickingChunk(); if (levelchunk == null) return; this.playerLoadedChunk(player, new MutableObject<>(), levelchunk); } }默认情况下,区块更新是惰性的,要使用playerLoadedChunk方法,除了单独拎出来用,也可以插入到move方法中:
@Inject(method = "move", at = @At("HEAD")) private void justMove(ServerPlayer player, CallbackInfo ci) { loadLevelChunk(player, ChunkPos.ZERO); }这个方法是默认情况下玩家更新区块的方法,插入到这里,等同于为玩家更新额外的区块。
强制渲染
这部分是最复杂的,需要进行非常非常多的mixin。
让我们一步步走。
首先,客户端收到ClientboundLevelChunkWithLightPacket包后需要进行处理,将区块数据存进客户端的区块缓存(ClientChunkCache)中,等待帧渲染将它抓出来渲染。
这里出现了第一次渲染判定,读取区块缓存的时候,要检测区块坐标是否在视距范围内。
那么我们将它干掉。@Mixin(targets = "net.minecraft.client.multiplayer.ClientChunkCache$Storage") public class ClientChunkCache$StorageMixin { @Inject(method = "inRange", at = @At("HEAD"), cancellable = true) private void modifyRange(int x, int z, CallbackInfoReturnable<Boolean> cir) { if (new ChunkPos(*****).equals(new ChunkPos(x, z))) { cir.setReturnValue(true); } } }如果轮到检测的这个区块和你想渲染的区块是同一个,就强制通过检测。
说完了缓存,接下来就是渲染。
最核心的渲染是在LevelRenderer里,这个类超级超级长。
我其实不太想介绍这个部分,因为Embeddium在这个类中用Overwrite重写了非常多的方法,比如我下面要说的setupRender:@Mixin(LevelRenderer.class) public class LevelRendererMixin { @ModifyVariable(method = "setupRender",at = @At("STORE"), ordinal = 0) private double modifyX(double x) { // 改一下x。 return x; } // 把y和z也改了,此处省略。 }这么做可以强制转移渲染的中心点,转移到你想渲染的地方。
是的,这个方法被重写了,重写后以客户端实例的摄像机位置为渲染中心了。Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();如果要修改渲染中心就去改摄像机位置吧,反正强制渲染特定区块大概率是伴随着摄像机移动的。
如果要考虑不安装Embeddium的情况,就用MixinPlugin区分一下:public class MixinPlugin implements IMixinConfigPlugin { @Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { if (mixinClassName.equals("com.mafuyu404.examplemod.mixin.LevelRendererMixin")) { return !isClassLoaded("me.jellysquid.mods.sodium.client.SodiumClientMod"); // Sodium/Embeddium核心类 } return true; } private static boolean isClassLoaded(String className) { try { Class.forName(className, false, getClassLoader()); return true; } catch (ClassNotFoundException e) { return false; } } // 必要的补全此处省略。 }不过,真有人会不装Embeddium吗?我测试的时候32视距给我提了快100帧……
在处理完区块渲染,其实还有最后一步,那就是实体渲染。
@Mixin(targets = "net.minecraft.server.level.ChunkMap$TrackedEntity") public class ChunkMap$TrackedEntityMixin { @ModifyVariable(method = "updatePlayer", at = @At(value = "STORE")) private Vec3 wwa(Vec3 direction) { // 改一下direction。 return direction; } }很多优化模组都有远处实体剔除的功能,实体渲染其实可以看作与区块渲染独立。
上面这个方法叫updatePlayer,其实是指更新本地玩家区块数据中的实体追踪数据,这里面会计算实体与玩家的距离,从而决定实体是否要渲染。
direction其实就是玩家坐标与实体坐标构成的向量,设为零向量即可。结语
内容就这么多了,其实是相当鸡肋的东西,之后想到什么再来补充吧。
-
程序猿梗图






-
怎么没有板式斑块原本我发了一个,被重置了捏。
-
我永远喜欢kubejs!@忆然 但是真冬永远不会……