跳转至内容
  • 版块
  • 最新
  • 热门
  • 标签
  • 群组
皮肤
  • Light
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • 默认(Zephyr)
  • 不使用皮肤
折叠
品牌标识

VariedMC 魔改论坛

真冬M

真冬

@Mafuyu
Modding
关于
帖子
9
主题
3
分享
0
群组
1
粉丝
2
关注
3
Blog

帖子

最新 最佳 有争议的

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

    总览
    • 楔子
    • 一、如何搭建最简单的着色器实例
    • 二、如何调用着色器
    • 三、如何向着色器传递参数
    • 四、如何管理着色器
    • 五、常见兼容性问题
    • 六、结语

    楔子

    故事要从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还是靠谱的。
    就这样,如果觉得我的教程有用处,请夸我两句。

    魔改早教 渲染

  • [1.20.1]强制加载并渲染特定区块
    真冬M 真冬

    我也不知道有什么实用价值但反正先写着。

    思路:
    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其实就是玩家坐标与实体坐标构成的向量,设为零向量即可。

    结语

    内容就这么多了,其实是相当鸡肋的东西,之后想到什么再来补充吧。

    灵感大王 渲染 forge

  • [原创][KubeJS]简单的以tick实现的嘲讽
    真冬M 真冬

    I know, man, 但你必须先攻击那个……

    灵感大王 kubejs
  • 1 / 1
  • 登录

  • 没有帐号? 注册

  • 登录或注册以进行搜索。
  • 第一个帖子
    最后一个帖子
0
  • 版块
  • 最新
  • 热门
  • 标签
  • 群组