跳转至内容
  • 版块
  • 最新
  • 热门
  • 标签
  • 群组
皮肤
  • 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 魔改论坛

  1. 主页
  2. 灵感大王
  3. 强制加载并渲染特定区块

强制加载并渲染特定区块

已定时 已固定 已锁定 已移动 灵感大王
区块渲染forgemixin
5 帖子 3 发布者 47 浏览 1 关注中
  • 从旧到新
  • 从新到旧
  • 最多赞同
    回复
    • 在新帖中回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • 真冬M 在线
      真冬M 在线
      真冬
      Modding
      编写于 最后由 Mafuyu 编辑
      #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其实就是玩家坐标与实体坐标构成的向量,设为零向量即可。

      结语

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

      zhenshizZ 1 条回复 最后回复
      1
      • 忆然忆 离线
        忆然忆 离线
        忆然
        KubeJS
        编写于 最后由 编辑
        #2

        前排瞻仰伟大的真冬女神,真是太有用了(爱你)

        真冬M 1 条回复 最后回复
        1
        • 忆然忆 忆然

          前排瞻仰伟大的真冬女神,真是太有用了(爱你)

          真冬M 在线
          真冬M 在线
          真冬
          Modding
          编写于 最后由 编辑
          #3

          @忆然 除了勤劳跟踪狂,我都不知道在哪能用上2333

          忆然忆 1 条回复 最后回复
          0
          • 真冬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其实就是玩家坐标与实体坐标构成的向量,设为零向量即可。

            结语

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

            zhenshizZ 离线
            zhenshizZ 离线
            zhenshiz
            编写于 最后由 编辑
            #4

            @Mafuyu 太高级了我希望我不要用到(

            1 条回复 最后回复
            0
            • 真冬M 真冬

              @忆然 除了勤劳跟踪狂,我都不知道在哪能用上2333

              忆然忆 离线
              忆然忆 离线
              忆然
              KubeJS
              编写于 最后由 编辑
              #5

              @Mafuyu 区块强制加载有用(点头

              1 条回复 最后回复
              0
              回复
              • 在新帖中回复
              登录后回复
              • 从旧到新
              • 从新到旧
              • 最多赞同


                • 登录

                • 没有帐号? 注册

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