import com.mojang.blaze3d.pipeline.MainTarget; import com.mojang.blaze3d.platform.DisplayData; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.authlib.GameProfile; import com.mojang.authlib.properties.PropertyMap; import net.minecraft.SharedConstants; import net.minecraft.client.Minecraft; import net.minecraft.client.User; import net.minecraft.client.main.GameConfig; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.telemetry.TelemetryEventSender; import net.minecraft.client.telemetry.WorldSessionTelemetryManager; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.RegistryAccess; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.Connection; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.resources.RegistryDataLoader; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.Bootstrap; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.profiling.InactiveProfiler; import net.minecraft.world.Difficulty; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.dimension.DimensionType; import org.lwjgl.opengl.GL11; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.net.Proxy; import java.time.Duration; import java.util.Optional; import java.util.OptionalInt; import java.util.UUID; /** * 世界渲染器 —— 把世界数据喂给真客户端渲染器并出图。 * * 实测要点(2026-09-25): * 1. 不跑主循环 ⇒ 渲染栈手动初始化(tryDetectVersion/Bootstrap/RenderSystem/new Minecraft) * 2. 注册表从客户端自带 data/ 加载(RegistryDataLoader + vanilla pack) * 3. shader 需显式编译(GameRenderer.reloadShaders 包私有 → 同包 bridge) * 4. LoadingOverlay 必须摘掉,否则永远渲出加载屏 * 5. 区块靠 LevelChunk + setBlockState 注入 + setSectionDirty 触发网格重建 * 6. 读像素必须绑 FBO(glBindFramebuffer(frameBufferId)),GL_BACK 读到的是残留 */ public class WorldRender { public static void main(String[] args) throws Exception { String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); String assetIdx = arg(args, "--assetIndex", "1.20.1"); String out = arg(args, "--out", "/tmp/scaffold-world.png"); int W = Integer.parseInt(arg(args, "--width", "854")); int H = Integer.parseInt(arg(args, "--height", "480")); SharedConstants.tryDetectVersion(); SharedConstants.enableDataFixerOptimizations(); Bootstrap.bootStrap(); File runDir = new File(gameDir); User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam", Optional.empty(), Optional.empty(), User.Type.LEGACY); PropertyMap props = new PropertyMap(); GameConfig cfg = new GameConfig( new GameConfig.UserData(user, props, props, Proxy.NO_PROXY), new DisplayData(W, H, OptionalInt.empty(), OptionalInt.empty(), false), new GameConfig.FolderData(runDir, new File(runDir, "resourcepacks"), new File(assets), assetIdx), new GameConfig.GameData(false, "mcfold", "release", false, false), new GameConfig.QuickPlayData(null, null, null, null)); RenderSystem.initRenderThread(); RenderSystem.beginInitialization(); Minecraft mc = new Minecraft(cfg); RenderSystem.finishInitialization(); System.out.println("[wr] Minecraft ok"); // ── 资源:字体/贴图/模型 ── // createReload 的 future 不该在主线程 join(会死锁),但字体加载是同步的, // 这里用官方 FontManager.reload 直接同步加载字体(修「文字成方框」) // ★ 资源重载:modelManager/fontManager/textureManager 全是 reload listener, // 不跑重载则方块模型/字体/贴图全空(地形渲不出来)。 // 驱动方式照抄 LoadingOverlay:轮询 isDone(),不能 join(会死锁)。 reloadResources(mc); System.out.println("[wr] resources reloaded"); // ── listener ── Connection conn = new Connection(PacketFlow.CLIENTBOUND); GameProfile profile = new GameProfile(UUID.randomUUID(), "mcfold_cam"); WorldSessionTelemetryManager telemetry = new WorldSessionTelemetryManager( TelemetryEventSender.DISABLED, false, Duration.ZERO, ""); ClientPacketListener listener = new ClientPacketListener(mc, null, conn, null, profile, telemetry); // ── 注册表:从客户端自带 data/ 加载 ── var packs = net.minecraft.server.packs.repository.ServerPacksSource.createPackRepository( new File(runDir, "datapacks").toPath()); packs.reload(); ResourceManager dataRm = new net.minecraft.server.packs.resources.MultiPackResourceManager( net.minecraft.server.packs.PackType.SERVER_DATA, java.util.List.of(packs.getPack("vanilla").open())); java.util.List> all = new java.util.ArrayList<>(); all.addAll(RegistryDataLoader.WORLDGEN_REGISTRIES); all.addAll(RegistryDataLoader.DIMENSION_REGISTRIES); RegistryAccess.Frozen loaded = RegistryDataLoader.load(dataRm, listener.registryAccess(), all); var layered = net.minecraft.client.multiplayer.ClientRegistryLayer.createRegistryAccess() .replaceFrom(net.minecraft.client.multiplayer.ClientRegistryLayer.REMOTE, new RegistryAccess.Frozen[]{loaded}); java.lang.reflect.Field rf = ClientPacketListener.class.getDeclaredField("registryAccess"); rf.setAccessible(true); rf.set(listener, layered); System.out.println("[wr] registries loaded: dimension_type=" + loaded.registryOrThrow(Registries.DIMENSION_TYPE).size()); Holder dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE) .getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, new ResourceLocation("overworld"))); // ── ClientLevel ── var levelData = new ClientLevel.ClientLevelData(Difficulty.NORMAL, false, false); ClientLevel level = new ClientLevel(listener, levelData, Level.OVERWORLD, dimHolder, 8, 8, () -> InactiveProfiler.INSTANCE, mc.levelRenderer, false, 0L); mc.setLevel(level); var player = new net.minecraft.client.player.LocalPlayer(mc, level, listener, new net.minecraft.stats.StatsCounter(), new net.minecraft.client.ClientRecipeBook(), false, false); // 站在区块中心上方 player.setPos(8.0, 70.0, 8.0); mc.player = player; mc.setCameraEntity(player); mc.gameMode = new net.minecraft.client.multiplayer.MultiPlayerGameMode(mc, listener); System.out.println("[wr] level + player ok"); // ── shader 编译(包私有,走同包 bridge)── net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager()); System.out.println("[wr] shaders ok: " + (RenderSystem.getShader() != null)); // ── LoadingOverlay 必须摘掉 ── mc.setOverlay(null); // ★ "Joining world..." 是 ReceivingLevelScreen;不清它客户端始终认为未进世界 mc.setScreen(null); // ── 注入区块 ── int injected = injectWorld(level, mc); System.out.println("[wr] injected chunks=" + injected); // ── 第三人称(跳过 renderItemInHand)── mc.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK); // ── 渲染(每帧先 tick:区块网格重建队列靠 tick 推进)── MainTarget target = (MainTarget) mc.getMainRenderTarget(); for (int f = 0; f < 60; f++) { try { mc.tick(); } catch (Throwable t) { if (f == 0) System.out.println("[wr] tick err: " + t); } RenderSystem.clear(16640, Minecraft.ON_OSX); target.bindWrite(true); mc.gameRenderer.render(0.0F, (long) f * 16_000_000L, true); target.unbindWrite(); target.blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight()); mc.getWindow().updateDisplay(); } System.out.println("[wr] rendered 20 frames"); // ── 诊断:区块与网格状态 ── System.out.println("[wr] hasRenderedAllChunks=" + mc.levelRenderer.hasRenderedAllChunks() + " countRenderedChunks=" + mc.levelRenderer.countRenderedChunks()); var got = level.getChunkSource().getChunk(0, 0, net.minecraft.world.level.chunk.ChunkStatus.FULL, false); System.out.println("[wr] getChunk(0,0)=" + (got != null ? got.getClass().getSimpleName() : "null") + " block(0,64,0)=" + (got != null ? got.getBlockState(new BlockPos(0,64,0)) : "-")); System.out.println("[wr] isChunkCompiled(0,64,0)=" + mc.levelRenderer.isChunkCompiled(new BlockPos(0,64,0))); save(target, W, H, out); System.exit(0); } /** * 同步驱动各 reload listener。 * * 为什么不用 Minecraft.reloadResourcePacks(): * 它的 future 靠主循环 tick 推进(LoadingOverlay),我们不跑主循环 ⇒ 死锁。 * 为什么不用 createReload(): * prepare 阶段仍走真后台池,实测 120s 超时。 * 正解:直接调各 listener 的 public reload(),barrier 同步返回, * 且 apply 用 Runnable::run 保证在渲染线程执行。 */ private static void reloadResources(Minecraft mc) throws Exception { var rmF = Minecraft.class.getDeclaredField("resourceManager"); rmF.setAccessible(true); var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc); var packs = mc.getResourcePackRepository(); packs.reload(); // 同步 barrier:prepare 完成后立刻交出,不排队 var barrier = new net.minecraft.server.packs.resources.PreparableReloadListener.PreparationBarrier() { @Override public java.util.concurrent.CompletableFuture wait(T t) { return java.util.concurrent.CompletableFuture.completedFuture(t); } }; var prof = InactiveProfiler.INSTANCE; // prepare 用当前线程(同步跑),apply 也用当前线程(渲染线程) java.util.concurrent.Executor sync = Runnable::run; var all = reloadListeners(mc); System.out.println("[wr] listeners=" + all.size()); for (var listener : all) { String name = listener.getName(); System.out.println("[wr] -> " + name + " ..."); try { var fut = listener.reload(barrier, rm, prof, prof, sync, sync); boolean done = false; long dl = System.currentTimeMillis() + 30_000; while (!done && System.currentTimeMillis() < dl) { done = fut.isDone(); if (!done) Thread.sleep(10); } if (!done) { System.out.println("[wr] TIMEOUT " + name); continue; } fut.join(); System.out.println("[wr] ok: " + name); } catch (Throwable e) { System.out.println("[wr] FAILED " + name + ": " + e); } } } /** 取出注册过的 listener(private 字段) */ @SuppressWarnings("unchecked") private static java.util.List reloadListeners(Minecraft mc) throws Exception { var rmF = Minecraft.class.getDeclaredField("resourceManager"); rmF.setAccessible(true); var rm = rmF.get(mc); for (var f : rm.getClass().getDeclaredFields()) { if (java.util.List.class.isAssignableFrom(f.getType())) { f.setAccessible(true); Object v = f.get(rm); if (v instanceof java.util.List l && !l.isEmpty() && l.get(0) instanceof net.minecraft.server.packs.resources.PreparableReloadListener) { return (java.util.List) l; } } } throw new IllegalStateException("reload listeners not found"); } /** 造地形并注入:区块 + 光照 + 网格重建 */ private static int injectWorld(ClientLevel level, Minecraft mc) throws Exception { int radius = 4; // 9x9 区块 = 144x144 格 int yBottom = level.getMinBuildHeight(); var cache = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource(); var light = cache.getLightEngine(); // 反射拿 Storage.chunks / getIndex(包私有) var storageF = net.minecraft.client.multiplayer.ClientChunkCache.class.getDeclaredField("storage"); storageF.setAccessible(true); Object storage = storageF.get(cache); var chunksF = storage.getClass().getDeclaredField("chunks"); chunksF.setAccessible(true); var chunks = (java.util.concurrent.atomic.AtomicReferenceArray) chunksF.get(storage); var getIndex = storage.getClass().getDeclaredMethod("getIndex", int.class, int.class); getIndex.setAccessible(true); System.out.println("[wr] storage chunks.length=" + chunks.length()); int count = 0; for (int cx = -radius; cx <= radius; cx++) { for (int cz = -radius; cz <= radius; cz++) { LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz)); for (int lx = 0; lx < 16; lx++) { for (int lz = 0; lz < 16; lz++) { int wx = cx * 16 + lx, wz = cz * 16 + lz; for (int y = yBottom; y < 63; y++) chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.STONE.defaultBlockState(), false); chunk.setBlockState(new BlockPos(lx, 63, lz), Blocks.DIRT.defaultBlockState(), false); chunk.setBlockState(new BlockPos(lx, 64, lz), Blocks.GRASS_BLOCK.defaultBlockState(), false); if ((wx % 7 == 0 && wz % 7 == 0) && (wx != 0 || wz != 0)) { for (int y = 65; y < 72; y++) chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.OAK_LOG.defaultBlockState(), false); for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) if (lx+dx >= 0 && lx+dx < 16 && lz+dz >= 0 && lz+dz < 16 && Math.abs(dx) + Math.abs(dz) <= 2) chunk.setBlockState(new BlockPos(lx+dx, 72, lz+dz), Blocks.OAK_LEAVES.defaultBlockState(), false); } } } // ★ 核心:直接放进 ClientChunkCache.Storage.chunks int idx = (int) getIndex.invoke(storage, cx, cz); chunks.set(idx, chunk); count++; // ★ 光照:全亮(否则地形全黑)。section y 0..19 覆盖 build height for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) { var sky = new net.minecraft.world.level.chunk.DataLayer(); for (int x = 0; x < 16; x++) for (int y = 0; y < 16; y++) for (int z = 0; z < 16; z++) sky.set(x, y, z, 15); light.queueSectionData(net.minecraft.world.level.LightLayer.SKY, net.minecraft.core.SectionPos.of(cx, sy, cz), sky); light.queueSectionData(net.minecraft.world.level.LightLayer.BLOCK, net.minecraft.core.SectionPos.of(cx, sy, cz), sky); } light.setLightEnabled(new ChunkPos(cx, cz), true); chunk.setLightCorrect(true); } } int upd = light.runLightUpdates(); System.out.println("[wr] light updates=" + upd); // 触发区块网格重建 mc.levelRenderer.allChanged(); System.out.println("[wr] allChanged done"); return count; } private static void save(MainTarget target, int W, int H, String out) throws Exception { org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, target.frameBufferId); GL11.glReadBuffer(org.lwjgl.opengl.GL30.GL_COLOR_ATTACHMENT0); var buf = org.lwjgl.BufferUtils.createByteBuffer(W * H * 4); GL11.glReadPixels(0, 0, W, H, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf); org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, 0); BufferedImage img = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB); for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) { int i = ((H - 1 - y) * W + x) * 4; img.setRGB(x, y, ((buf.get(i) & 0xFF) << 16) | ((buf.get(i+1) & 0xFF) << 8) | (buf.get(i+2) & 0xFF)); } ImageIO.write(img, "png", new File(out)); java.util.Set colors = new java.util.HashSet<>(); for (int p : img.getRGB(0, 0, W, H, null, 0, W)) colors.add(p & 0xFFFFFF); System.out.println("[wr] wrote " + out + " distinct colors=" + colors.size()); } private static String arg(String[] a, String key, String def) { for (int i = 0; i < a.length - 1; i++) if (a[i].equals(key)) return a[i + 1]; return def; } }