Files
agentmc/scaffold/WorldRender.java
JianFeeeee 541fe232a8 P8 完成:世界数据 → 官方渲染器 → 真实出图
不起完整客户端、不连服、不登录,只初始化 MC 渲染栈并喂入我们自己造的世界数据。

已验证(全绿):
  countRenderedChunks=29  distinct colors=1450
  画面:草地/原木/树叶/天空/HUD/英文文字(sample-world-render.png)

打通的关卡:
  - 反混淆(client-named.jar)+ GL 离屏 + new Minecraft(cfg) 不进主循环
  - 资源同步重载:22 listener(ModelManager/FontManager/Dispatcher)
    ★ 不能 join reloadResourcePacks(等主循环→死锁),直接调各 listener reload()
  - 注册表从客户端自带 data/ 加载(dimension_type=4)
  - 区块注入:LevelChunk + setBlockState → ClientChunkCache.Storage.chunks
    + LightEngine.queueSectionData(全亮)→ allChanged()
  - 每帧 mc.tick() 推进网格重建队列(不跑则 countRenderedChunks=0)

踩坑记录(10 条,README 详列):
  LoadingOverlay/ReceivingLevelScreen 遮挡、FolderData 参数序、
  assetIndex 用落盘名、reloadShaders 包私有、读像素必须绑 FBO、
  判据用 distinct colors 而非 non-black

更正附录 D:state_id=1003 → note_block 不是误映射(note_block 有 800 状态)
  —— 真客户端给完整状态,是 1.20.1 上 mod 方块语义的唯一权威来源
2026-09-25 17:01:49 +08:00

345 lines
18 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<RegistryDataLoader.RegistryData<?>> 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<DimensionType> 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 <T> java.util.concurrent.CompletableFuture<T> 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<net.minecraft.server.packs.resources.PreparableReloadListener> 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<net.minecraft.server.packs.resources.PreparableReloadListener>) 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<LevelChunk>) 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<Integer> 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;
}
}