From 31b689dc2012a1267feda68bcf0c6e82cca482a0 Mon Sep 17 00:00:00 2001 From: Starlight <74122979+Yuru-Yurufu@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:19:11 +0900 Subject: [PATCH 1/6] fix: rebuild glass meshes on resource reload, skip glass work for normal halves - Track built state of the connected-glass mesh caches with explicit flags. The old check looked at a face that is never generated for some axes, so the meshes were rebuilt on every bake (review #11). - Clear the static mesh caches whenever models are (re)loaded, so meshes with baked UVs are not reused after the texture atlas changes (review #12). - Return early from the connected-glass model when neither half is glass, skipping the neighbour scan and allocations (review #10). - Scene capture: wait until the number of built chunks stops changing before shooting or measuring, and add material-pair reference shots. --- .../model/AbstractConnectGlassModel.java | 25 +++++++++++++ .../AbstractDoubleSlabConnectGlassModel.java | 17 ++++++--- .../DoubleSlabBlockConnectGlassModel.java | 3 +- .../DoubleSlabBlockModelLoadingPlugin.java | 3 ++ .../yurufu/slabee/devclient/SceneBuilder.java | 36 +++++++++++++++++++ .../yurufu/slabee/devclient/SceneCapture.java | 17 +++++++-- 6 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/client/java/com/forestotzka/yurufu/slabee/model/AbstractConnectGlassModel.java b/src/client/java/com/forestotzka/yurufu/slabee/model/AbstractConnectGlassModel.java index 9390954d..dc5f64b6 100644 --- a/src/client/java/com/forestotzka/yurufu/slabee/model/AbstractConnectGlassModel.java +++ b/src/client/java/com/forestotzka/yurufu/slabee/model/AbstractConnectGlassModel.java @@ -46,6 +46,31 @@ public class AbstractConnectGlassModel implements UnbakedModel, BakedModel, Fabr protected static final Mesh[][][] END_MESHES = new Mesh[VARIANT_COUNT][STAINED_GLASS_PATTERN_COUNT][DIRECTION_COUNT]; protected static final Mesh[][][][][] QUARTER_MESHES = new Mesh[VARIANT_COUNT][SLAB_PATTERN_COUNT][DIRECTION_COUNT][QUARTER_COUNT][UV_AXIS_COUNT]; + // 生成済みかどうか。メッシュ配列の特定の要素が null かどうかで判定すると、その面を + // そもそも生成しない場合(例:横のハーフの上下の面)に毎回作り直してしまうため、別に持つ。 + protected static final boolean[][] SIDE_POSITIVE_BUILT = new boolean[AXIS_COUNT][VARIANT_COUNT]; + protected static final boolean[][] SIDE_NEGATIVE_BUILT = new boolean[AXIS_COUNT][VARIANT_COUNT]; + protected static final boolean[][] END_POSITIVE_BUILT = new boolean[AXIS_COUNT][VARIANT_COUNT]; + protected static final boolean[][] END_NEGATIVE_BUILT = new boolean[AXIS_COUNT][VARIANT_COUNT]; + protected static final boolean[] QUARTER_BUILT = new boolean[VARIANT_COUNT]; + + /** + * 作り置きのメッシュをすべて捨てる。メッシュはテクスチャの貼り付け位置(UV)を焼き込んでいるため、 + * リソースの再読み込み(リソースパックの変更など)でテクスチャの配置が変わったら作り直す必要がある。 + * モデルの読み込みのたびに呼ぶ。 + */ + public static synchronized void clearMeshCaches() { + for (Mesh[][][][] a : new Mesh[][][][][]{SIDE_POSITIVE_MESHES, SIDE_NEGATIVE_MESHES, END_POSITIVE_MESHES, END_NEGATIVE_MESHES}) { + for (Mesh[][][] b : a) for (Mesh[][] c : b) for (Mesh[] d : c) java.util.Arrays.fill(d, null); + } + for (Mesh[][] b : END_MESHES) for (Mesh[] c : b) java.util.Arrays.fill(c, null); + for (Mesh[][][][] b : QUARTER_MESHES) for (Mesh[][][] c : b) for (Mesh[][] d : c) for (Mesh[] e : d) java.util.Arrays.fill(e, null); + for (boolean[][] f : new boolean[][][]{SIDE_POSITIVE_BUILT, SIDE_NEGATIVE_BUILT, END_POSITIVE_BUILT, END_NEGATIVE_BUILT}) { + for (boolean[] g : f) java.util.Arrays.fill(g, false); + } + java.util.Arrays.fill(QUARTER_BUILT, false); + } + protected static final SpriteIdentifier nullSpriteIdentifier = new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, Identifier.ofVanilla("block/stone")); @Override diff --git a/src/client/java/com/forestotzka/yurufu/slabee/model/AbstractDoubleSlabConnectGlassModel.java b/src/client/java/com/forestotzka/yurufu/slabee/model/AbstractDoubleSlabConnectGlassModel.java index 12adfe6d..a7b8f35a 100644 --- a/src/client/java/com/forestotzka/yurufu/slabee/model/AbstractDoubleSlabConnectGlassModel.java +++ b/src/client/java/com/forestotzka/yurufu/slabee/model/AbstractDoubleSlabConnectGlassModel.java @@ -138,7 +138,8 @@ protected void setUV(QuadEmitter emitter, float u0, float u1, float v0, float v1 @Override public @Nullable BakedModel bake(Baker baker, Function textureGetter, ModelBakeSettings rotationContainer) { - if (this.positiveId != null && SIDE_POSITIVE_MESHES[axis][positiveVariantIndex][0][0] == null) { + if (this.positiveId != null && !SIDE_POSITIVE_BUILT[axis][positiveVariantIndex]) { + SIDE_POSITIVE_BUILT[axis][positiveVariantIndex] = true; for (int patternIndex = 0; patternIndex < SLAB_PATTERN_COUNT; patternIndex++) { for (Direction dir : Direction.values()) { if (isEndFace(dir)) { @@ -155,7 +156,8 @@ protected void setUV(QuadEmitter emitter, float u0, float u1, float v0, float v1 } } - if (this.negativeId != null && SIDE_NEGATIVE_MESHES[axis][negativeVariantIndex][0][0] == null) { + if (this.negativeId != null && !SIDE_NEGATIVE_BUILT[axis][negativeVariantIndex]) { + SIDE_NEGATIVE_BUILT[axis][negativeVariantIndex] = true; for (int patternIndex = 0; patternIndex < SLAB_PATTERN_COUNT; patternIndex++) { for (Direction dir : Direction.values()) { if (isEndFace(dir)) { @@ -172,7 +174,8 @@ protected void setUV(QuadEmitter emitter, float u0, float u1, float v0, float v1 } } - if (this.positiveId != null && END_POSITIVE_MESHES[axis][positiveVariantIndex][0][0] == null) { + if (this.positiveId != null && !END_POSITIVE_BUILT[axis][positiveVariantIndex]) { + END_POSITIVE_BUILT[axis][positiveVariantIndex] = true; for (int patternIndex = 0; patternIndex < (isGlassPositive ? GLASS_PATTERN_COUNT : STAINED_GLASS_PATTERN_COUNT); patternIndex++) { for (Direction dir : Direction.values()) { if (!isEndFace(dir)) { @@ -192,7 +195,8 @@ protected void setUV(QuadEmitter emitter, float u0, float u1, float v0, float v1 } } - if (this.negativeId != null && END_NEGATIVE_MESHES[axis][negativeVariantIndex][0][0] == null) { + if (this.negativeId != null && !END_NEGATIVE_BUILT[axis][negativeVariantIndex]) { + END_NEGATIVE_BUILT[axis][negativeVariantIndex] = true; for (int patternIndex = 0; patternIndex < (isGlassNegative ? GLASS_PATTERN_COUNT : STAINED_GLASS_PATTERN_COUNT); patternIndex++) { for (Direction dir : Direction.values()) { if (!isEndFace(dir)) { @@ -229,6 +233,11 @@ protected void setUV(QuadEmitter emitter, float u0, float u1, float v0, float v1 @Override public void emitBlockQuads(BlockRenderView blockRenderView, BlockState blockState, BlockPos blockPos, Supplier supplier, RenderContext renderContext) { + // 描くガラスのハーフがない(両側とも普通のハーフ)なら、周りのガラスを調べる必要はない + if (this.positiveId == null && this.negativeId == null) { + return; + } + NeighborState ns = new NeighborState(blockRenderView, blockPos, positiveSlab, negativeSlab, getDoubleSlabType()); if (this.positiveId != null) { diff --git a/src/client/java/com/forestotzka/yurufu/slabee/model/DoubleSlabBlockConnectGlassModel.java b/src/client/java/com/forestotzka/yurufu/slabee/model/DoubleSlabBlockConnectGlassModel.java index 23552bbd..ab73eaab 100644 --- a/src/client/java/com/forestotzka/yurufu/slabee/model/DoubleSlabBlockConnectGlassModel.java +++ b/src/client/java/com/forestotzka/yurufu/slabee/model/DoubleSlabBlockConnectGlassModel.java @@ -120,7 +120,8 @@ protected DoubleSlabType getDoubleSlabType() { @Override public @Nullable BakedModel bake(Baker baker, Function textureGetter, ModelBakeSettings rotationContainer) { - if (this.positiveId != null && QUARTER_MESHES[positiveVariantIndex][0][0][0][0] == null) { + if (this.positiveId != null && !QUARTER_BUILT[positiveVariantIndex]) { + QUARTER_BUILT[positiveVariantIndex] = true; for (int patternIndex = 0; patternIndex < SLAB_PATTERN_COUNT; patternIndex++) { for (Direction dir : Direction.values()) { for (int quarterIndex = 0; quarterIndex < QUARTER_COUNT; quarterIndex++) { diff --git a/src/client/java/com/forestotzka/yurufu/slabee/model/DoubleSlabBlockModelLoadingPlugin.java b/src/client/java/com/forestotzka/yurufu/slabee/model/DoubleSlabBlockModelLoadingPlugin.java index 3e4c3c4b..69164fa6 100644 --- a/src/client/java/com/forestotzka/yurufu/slabee/model/DoubleSlabBlockModelLoadingPlugin.java +++ b/src/client/java/com/forestotzka/yurufu/slabee/model/DoubleSlabBlockModelLoadingPlugin.java @@ -24,6 +24,9 @@ record SlabVariantKey(Block positiveSlab, Block negativeSlab, int axis) {} @Override public void onInitializeModelLoader(Context context) { + // モデルの読み込み(起動時・リソースの再読み込み時)のたびに、古いテクスチャ配置のメッシュを捨てる + AbstractConnectGlassModel.clearMeshCaches(); + // 【描画の試作】焼き上がったダブルハーフのモデルを包み、普通のハーフもチャンクの描画に焼き込む context.modifyModelAfterBake().register((model, ctx) -> { ModelIdentifier topId = ctx.topLevelId(); diff --git a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneBuilder.java b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneBuilder.java index 14a76f85..6ce42e26 100644 --- a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneBuilder.java +++ b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneBuilder.java @@ -144,6 +144,42 @@ static void buildReference(ServerWorld world, int y, List glassSlabs) { } } + static final int PAIR_Z = 75; + + /** 素材の組み合わせごとの比較用(上, 下)。葉・光るブロック・ガラスなど、面の省略が分かれやすいもの */ + static final String[][] PAIRS = { + {"slabee:oak_leaf_slab", "slabee:glowstone_slab"}, + {"slabee:glowstone_slab", "slabee:oak_leaf_slab"}, + {"slabee:oak_leaf_slab", "minecraft:stone_slab"}, + {"slabee:oak_leaf_slab", "slabee:birch_leaf_slab"}, + {"slabee:glass_slab", "slabee:glowstone_slab"}, + {"slabee:glass_slab", "slabee:oak_leaf_slab"}, + {"slabee:magma_block_slab", "slabee:oak_leaf_slab"}, + }; + + /** + * 組み合わせごとに、左に通常ブロック(下・上の2段)、右に同じ組み合わせのダブルハーフを、 + * それぞれ 2x2 の広さで並べる。上から見下ろすと、境目の面が見えるかを比べられる。 + */ + static void buildPairs(ServerWorld world, int y) { + for (int k = 0; k < PAIRS.length; k++) { + Block top = Registries.BLOCK.get(net.minecraft.util.Identifier.of(PAIRS[k][0])); + Block bottom = Registries.BLOCK.get(net.minecraft.util.Identifier.of(PAIRS[k][1])); + Block topFull = ModBlockMap.slabToOriginal(top); + Block bottomFull = ModBlockMap.slabToOriginal(bottom); + if (topFull == null) topFull = Blocks.STONE; + if (bottomFull == null) bottomFull = Blocks.STONE; + int x0 = k * 8; + for (int dx = 0; dx < 2; dx++) { + for (int dz = 0; dz < 2; dz++) { + world.setBlockState(new BlockPos(x0 + dx, y, PAIR_Z + dz), bottomFull.getDefaultState(), 3); + world.setBlockState(new BlockPos(x0 + dx, y + 1, PAIR_Z + dz), topFull.getDefaultState(), 3); + placeDouble(world, new BlockPos(x0 + 4 + dx, y, PAIR_Z + dz), top, bottom); + } + } + } + } + /** 重さの測定用:Slabee のダブルハーフを敷き詰めた 100x100 */ static void buildSlabeeField(ServerWorld world, int y) { for (int x = 0; x < FIELD_SIZE; x++) { diff --git a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java index 4502040f..c3c4380c 100644 --- a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java +++ b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java @@ -74,6 +74,7 @@ private record View(String name, double x, double y, double z, float yaw, float private int stepTicks = 0; private int stableTicks = 0; private boolean teleported = false; + private int lastCompletedChunks = -1; private boolean recording = false; private long lastFrameNanos = 0; @@ -197,6 +198,7 @@ private void buildScene(IntegratedServer server) { SceneBuilder.buildCatalog(world, baseY, slabs); SceneBuilder.buildAdjacent(world, baseY, glass); SceneBuilder.buildReference(world, baseY, glass); + SceneBuilder.buildPairs(world, baseY); SceneBuilder.buildSlabeeField(world, baseY); SceneBuilder.buildVanillaField(world, baseY); LOGGER.info("Built scene with {} slab types ({} glass) at y={} in {} ms", @@ -234,6 +236,12 @@ private void planViews(List slabs, List glass) { shots.add(new View(String.format("reference_%02d", k), centerX, baseY + 3.5, SceneBuilder.REFERENCE_Z - 6, 0f, 18f, note)); } + for (int k = 0; k < SceneBuilder.PAIRS.length; k++) { + double centerX = k * 8 + 3.0; + String note = "上=" + SceneBuilder.PAIRS[k][0] + "、下=" + SceneBuilder.PAIRS[k][1] + ":左=通常ブロック(2段)、右=ダブルハーフ"; + shots.add(new View(String.format("pair_%02d", k), centerX, baseY + 4.5, SceneBuilder.PAIR_Z - 3, 0f, 40f, note)); + } + double fieldCenterX = SceneBuilder.FIELD_SIZE / 2.0; perfStages.add(new View("empty", fieldCenterX, baseY + 25, SceneBuilder.EMPTY_Z - 15, 0f, 40f, "何も置いていない場所")); perfStages.add(new View("slabee_field", fieldCenterX, baseY + 25, SceneBuilder.SLABEE_FIELD_Z - 15, 0f, 40f, @@ -266,16 +274,20 @@ private boolean moveAndSettle(MinecraftClient client, View view, int minTicks) { } boolean arrived = client.player != null && client.player.squaredDistanceTo(view.x(), view.y(), view.z()) < 0.25; - if (arrived && client.worldRenderer.isTerrainRenderComplete()) { + // 描画範囲のチャンクが読み込み・組み立てを終えて、完成数が増えなくなるまで待つ + // (実機では遠くの地形が読み込み途中のまま測ってしまうことがあったため) + int completed = client.worldRenderer.getCompletedChunkCount(); + if (arrived && client.worldRenderer.isTerrainRenderComplete() && completed == lastCompletedChunks) { stableTicks++; } else { stableTicks = 0; } + lastCompletedChunks = completed; if (stepTicks > 20 * 60) { LOGGER.warn("View {} did not settle in time; continuing anyway", view.name()); return true; } - return stepTicks >= minTicks && stableTicks >= 20; + return stepTicks >= minTicks && stableTicks >= 40; } private void nextStep() { @@ -326,6 +338,7 @@ private void stepPerf(MinecraftClient client) { long[] sorted = frameNanos.stream().mapToLong(Long::longValue).sorted().toArray(); double avg = Arrays.stream(sorted).average().orElse(0) / 1e6; result.addProperty("frames", sorted.length); + result.addProperty("completedChunks", client.worldRenderer.getCompletedChunkCount()); result.addProperty("frameMsAvg", round(avg)); result.addProperty("frameMsP50", round(percentile(sorted, 0.50) / 1e6)); result.addProperty("frameMsP95", round(percentile(sorted, 0.95) / 1e6)); From f2444e0bd56aefdd3cea3eae983bed87b66e32ce Mon Sep 17 00:00:00 2001 From: Starlight <74122979+Yuru-Yurufu@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:32:25 +0900 Subject: [PATCH 2/6] fix: decide inner face visibility from the full-block analog The face where the two halves touch was hidden whenever both halves had the same DoubleSlabVariant, but NON_OPAQUE groups different materials (leaves, glowstone, ...). A glowstone half under a leaf half lost its top face, so the ground showed through the leaves (review #7). Now the face is drawn exactly when the same two blocks stacked as full blocks would draw it: hidden under an opaque block or between matching glass, visible otherwise. A GameTest compares 100 material pairs against full blocks placed in the world. --- .../slabee/gametest/InnerFaceGameTests.java | 82 +++++++++++++++++++ src/gametest/resources/fabric.mod.json | 3 +- .../yurufu/slabee/block/DoubleSlabUtils.java | 22 +++++ .../yurufu/slabee/mixin/BlockMixin.java | 50 +++++------ 4 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 src/gametest/java/com/forestotzka/yurufu/slabee/gametest/InnerFaceGameTests.java diff --git a/src/gametest/java/com/forestotzka/yurufu/slabee/gametest/InnerFaceGameTests.java b/src/gametest/java/com/forestotzka/yurufu/slabee/gametest/InnerFaceGameTests.java new file mode 100644 index 00000000..c560bb5e --- /dev/null +++ b/src/gametest/java/com/forestotzka/yurufu/slabee/gametest/InnerFaceGameTests.java @@ -0,0 +1,82 @@ +package com.forestotzka.yurufu.slabee.gametest; + +import com.forestotzka.yurufu.slabee.block.AbstractDoubleSlabBlockEntity; +import com.forestotzka.yurufu.slabee.block.ModBlockMap; +import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.registry.Registries; +import net.minecraft.test.GameTest; +import net.minecraft.test.TestContext; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; + +import java.util.ArrayList; +import java.util.List; + +import static com.forestotzka.yurufu.slabee.gametest.SlabeeTestUtil.placeDouble; + +/** + * ダブルハーフの内側の面(上下のハーフが接している面)を描くかどうかが、 + * 「同じ並びを通常ブロックで作ったとき」と同じになることを確かめる(MODの基本方針)。 + * 正解は、その場で通常ブロックを実際に置いて Block#shouldDrawSide で求める。 + */ +public class InnerFaceGameTests implements FabricGameTest { + private static final Block[] FULL_BLOCKS = { + Blocks.STONE, Blocks.OAK_PLANKS, Blocks.GLOWSTONE, Blocks.OAK_LEAVES, Blocks.BIRCH_LEAVES, + Blocks.GLASS, Blocks.RED_STAINED_GLASS, Blocks.BLUE_STAINED_GLASS, Blocks.TINTED_GLASS, Blocks.MAGMA_BLOCK, + }; + + @GameTest(templateName = EMPTY_STRUCTURE) + public void innerFacesMatchFullBlocks(TestContext context) { + BlockPos full = new BlockPos(1, 1, 1); + BlockPos dbl = new BlockPos(3, 1, 1); + BlockPos fullAbs = context.getAbsolutePos(full); + BlockPos dblAbs = context.getAbsolutePos(dbl); + List mismatches = new ArrayList<>(); + int checked = 0; + + for (Block bottomFull : FULL_BLOCKS) { + for (Block topFull : FULL_BLOCKS) { + Block bottomSlab = ModBlockMap.originalToSlab(bottomFull); + Block topSlab = ModBlockMap.originalToSlab(topFull); + if (bottomSlab == null || topSlab == null) continue; + + // 正解:通常ブロックを上下に置いたときの、接している面 + context.setBlockState(full, bottomFull); + context.setBlockState(full.up(), topFull); + boolean expectBottomUp = Block.shouldDrawSide(bottomFull.getDefaultState(), context.getWorld(), fullAbs, Direction.UP, fullAbs.up()); + boolean expectTopDown = Block.shouldDrawSide(topFull.getDefaultState(), context.getWorld(), fullAbs.up(), Direction.DOWN, fullAbs); + + // ダブルハーフ + placeDouble(context, dbl, topSlab, bottomSlab); + if (!(context.getWorld().getBlockEntity(dblAbs) instanceof AbstractDoubleSlabBlockEntity entity)) { + mismatches.add("ダブルハーフを置けない: " + id(topSlab) + " / " + id(bottomSlab)); + continue; + } + BlockState bottomHalf = entity.getNegativeSlabState(); + BlockState topHalf = entity.getPositiveSlabState(); + boolean actualBottomUp = Block.shouldDrawSide(bottomHalf, context.getWorld(), dblAbs, Direction.UP, dblAbs.up()); + boolean actualTopDown = Block.shouldDrawSide(topHalf, context.getWorld(), dblAbs, Direction.DOWN, dblAbs.down()); + + checked++; + if (actualBottomUp != expectBottomUp) { + mismatches.add("上=" + id(topFull) + " 下=" + id(bottomFull) + ":下の上面 通常=" + expectBottomUp + " ダブル=" + actualBottomUp); + } + if (actualTopDown != expectTopDown) { + mismatches.add("上=" + id(topFull) + " 下=" + id(bottomFull) + ":上の下面 通常=" + expectTopDown + " ダブル=" + actualTopDown); + } + } + } + + context.assertTrue(checked > 50, "比べられた組み合わせが少なすぎる: " + checked); + context.assertTrue(mismatches.isEmpty(), + "通常ブロックと違う面 " + mismatches.size() + " 件(" + checked + " 組中):\n" + String.join("\n", mismatches)); + context.complete(); + } + + private static String id(Block block) { + return Registries.BLOCK.getId(block).getPath(); + } +} diff --git a/src/gametest/resources/fabric.mod.json b/src/gametest/resources/fabric.mod.json index 583c7ea6..4f46e96b 100644 --- a/src/gametest/resources/fabric.mod.json +++ b/src/gametest/resources/fabric.mod.json @@ -10,7 +10,8 @@ "com.forestotzka.yurufu.slabee.gametest.StackingGameTests", "com.forestotzka.yurufu.slabee.gametest.HalfBreakGameTests", "com.forestotzka.yurufu.slabee.gametest.MixinGameTests", - "com.forestotzka.yurufu.slabee.gametest.LightingGameTests" + "com.forestotzka.yurufu.slabee.gametest.LightingGameTests", + "com.forestotzka.yurufu.slabee.gametest.InnerFaceGameTests" ] }, "depends": { diff --git a/src/main/java/com/forestotzka/yurufu/slabee/block/DoubleSlabUtils.java b/src/main/java/com/forestotzka/yurufu/slabee/block/DoubleSlabUtils.java index a5a69baf..a0bda25b 100644 --- a/src/main/java/com/forestotzka/yurufu/slabee/block/DoubleSlabUtils.java +++ b/src/main/java/com/forestotzka/yurufu/slabee/block/DoubleSlabUtils.java @@ -3,7 +3,9 @@ import com.forestotzka.yurufu.slabee.SlabeeUtils; import com.forestotzka.yurufu.slabee.block.enums.DoubleSlabVariant; import com.forestotzka.yurufu.slabee.registry.tag.ModBlockTags; +import net.minecraft.block.Block; import net.minecraft.block.BlockState; +import net.minecraft.util.math.Direction; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.ItemPlacementContext; import net.minecraft.item.ItemStack; @@ -23,6 +25,26 @@ public static boolean canPlace(ItemPlacementContext context, VoxelShape voxelSha return voxelShape.isEmpty() || context.getWorld().doesNotIntersectEntities(null, voxelShape.offset(pos.getX(), pos.getY(), pos.getZ())); } + /** + * ダブルハーフの内側の面(もう片方のハーフに接している面)を描くかどうか。 + * 「同じ並びを通常ブロックで作ったら、この面は見えるか」に合わせる(MODの基本方針)。 + * - 相手の元のブロックが不透明なら見えない(例:上が石) + * - 同じガラスどうしのように、元のブロックどうしで面を省く組み合わせなら見えない + * - それ以外は見える(例:葉の下のグロウストーン、色の違う色付きガラスどうし、葉どうし) + */ + public static boolean shouldDrawInnerFace(BlockState half, BlockState otherHalf, Direction side) { + Block thisFull = ModBlockMap.toOriginal(half.getBlock()); + Block otherFull = ModBlockMap.toOriginal(otherHalf.getBlock()); + if (thisFull == null || otherFull == null) { + return true; + } + BlockState other = otherFull.getDefaultState(); + if (other.isOpaque()) { + return false; + } + return !thisFull.getDefaultState().isSideInvisible(other, side); + } + public static boolean isPositiveSeeThrough(BlockState state) { if (!SlabeeUtils.isDoubleSlab(state)) { return false; diff --git a/src/main/java/com/forestotzka/yurufu/slabee/mixin/BlockMixin.java b/src/main/java/com/forestotzka/yurufu/slabee/mixin/BlockMixin.java index 2aae75b0..c815cc16 100644 --- a/src/main/java/com/forestotzka/yurufu/slabee/mixin/BlockMixin.java +++ b/src/main/java/com/forestotzka/yurufu/slabee/mixin/BlockMixin.java @@ -1,5 +1,7 @@ package com.forestotzka.yurufu.slabee.mixin; +import com.forestotzka.yurufu.slabee.block.AbstractDoubleSlabBlockEntity; + import com.forestotzka.yurufu.slabee.block.*; import com.forestotzka.yurufu.slabee.block.enums.DoubleSlabVariant; import com.forestotzka.yurufu.slabee.block.enums.VerticalSlabAxis; @@ -57,39 +59,27 @@ public void getPickStack(WorldView world, BlockPos pos, BlockState state, Callba @Inject(method = "shouldDrawSide", at = @At("HEAD"), cancellable = true) private static void onShouldDrawSide(BlockState state, BlockView world, BlockPos pos, Direction side, BlockPos otherPos, CallbackInfoReturnable cir) { - Block block = state.getBlock(); + // ダブルハーフの中の片方のハーフについて、もう片方に接している内側の面を描くかどうか + BlockState here = world.getBlockState(pos); + if (!(world.getBlockEntity(pos) instanceof AbstractDoubleSlabBlockEntity entity)) { + return; + } - if (block instanceof SlabBlock) { - BlockState blockState = world.getBlockState(pos); + Block block = state.getBlock(); + if (block instanceof SlabBlock && here.isOf(ModBlocks.DOUBLE_SLAB_BLOCK)) { SlabType slabType = state.get(Properties.SLAB_TYPE); - - if (blockState.isOf(ModBlocks.DOUBLE_SLAB_BLOCK)) { - if (slabType == SlabType.BOTTOM) { - if (side == Direction.UP) { - cir.setReturnValue(DoubleSlabUtils.isPositiveSeeThrough(blockState) && DoubleSlabVariant.fromBlock(state.getBlock()) != blockState.get(AbstractDoubleSlabBlock.POSITIVE_SLAB)); - cir.cancel(); - } - } else if (slabType == SlabType.TOP) { - if (side == Direction.DOWN) { - cir.setReturnValue(DoubleSlabUtils.isNegativeSeeThrough(blockState) && DoubleSlabVariant.fromBlock(state.getBlock()) != blockState.get(AbstractDoubleSlabBlock.NEGATIVE_SLAB)); - cir.cancel(); - } - } + if (slabType == SlabType.BOTTOM && side == Direction.UP) { + cir.setReturnValue(DoubleSlabUtils.shouldDrawInnerFace(state, entity.getPositiveSlabState(), side)); + } else if (slabType == SlabType.TOP && side == Direction.DOWN) { + cir.setReturnValue(DoubleSlabUtils.shouldDrawInnerFace(state, entity.getNegativeSlabState(), side)); } - - } else if (block instanceof VerticalSlabBlock) { - BlockState blockState = world.getBlockState(pos); - - if (blockState.isOf(ModBlocks.DOUBLE_VERTICAL_SLAB_BLOCK)) { - Direction d = state.get(Properties.HORIZONTAL_FACING); - if (d.getOpposite() == side) { - if (d == Direction.EAST || d == Direction.SOUTH) { - cir.setReturnValue(DoubleSlabUtils.isNegativeSeeThrough(blockState) && DoubleSlabVariant.fromBlock(state.getBlock()) != blockState.get(AbstractDoubleSlabBlock.NEGATIVE_SLAB)); - cir.cancel(); - } else if (d == Direction.WEST || d == Direction.NORTH) { - cir.setReturnValue(DoubleSlabUtils.isPositiveSeeThrough(blockState) && DoubleSlabVariant.fromBlock(state.getBlock()) != blockState.get(AbstractDoubleSlabBlock.POSITIVE_SLAB)); - cir.cancel(); - } + } else if (block instanceof VerticalSlabBlock && here.isOf(ModBlocks.DOUBLE_VERTICAL_SLAB_BLOCK)) { + Direction d = state.get(Properties.HORIZONTAL_FACING); + if (d.getOpposite() == side) { + if (d == Direction.EAST || d == Direction.SOUTH) { + cir.setReturnValue(DoubleSlabUtils.shouldDrawInnerFace(state, entity.getNegativeSlabState(), side)); + } else { + cir.setReturnValue(DoubleSlabUtils.shouldDrawInnerFace(state, entity.getPositiveSlabState(), side)); } } } From be9534ad87cf1555de6387c4521557da89dedc9a Mon Sep 17 00:00:00 2001 From: Starlight <74122979+Yuru-Yurufu@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:11:03 +0900 Subject: [PATCH 3/6] perf: exit the inner-face check early for non-slab blocks shouldDrawSide runs for every face of every block while building chunk meshes. Check the block type before touching the world or block entities. Scene capture: allow fixing the FPS cap with -Dslabee.scene.maxFps and disable autosave while capturing. --- .../yurufu/slabee/devclient/SceneCapture.java | 7 ++++++- .../forestotzka/yurufu/slabee/mixin/BlockMixin.java | 12 ++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java index c3c4380c..193f1765 100644 --- a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java +++ b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java @@ -156,7 +156,9 @@ private static void configureOptions(GameOptions options) { options.pauseOnLostFocus = false; options.getViewDistance().setValue(8); options.getSimulationDistance().setValue(8); - options.getMaxFps().setValue(260); + // -Dslabee.scene.maxFps=60 のように指定すると、フレーム数の上限を固定して測れる + // (描画が速くなるとフレーム数が増えてCPUを多く使い、同じマシンのサーバー側の測定に影響するため) + options.getMaxFps().setValue(Integer.getInteger("slabee.scene.maxFps", 260)); options.getEnableVsync().setValue(false); options.getCloudRenderMode().setValue(CloudRenderMode.OFF); options.getBobView().setValue(false); @@ -189,6 +191,8 @@ private void buildScene(IntegratedServer server) { server.submitAndJoin(() -> { ServerWorld world = server.getOverworld(); world.setTimeOfDay(6000); + // 撮影中は自動保存を止める(保存中の表示が写り込んだり、測定の数字が揺れたりするため) + world.savingDisabled = true; world.setWeather(1000000, 0, false, false); baseY = world.getTopY(Heightmap.Type.MOTION_BLOCKING, 0, 0); @@ -373,6 +377,7 @@ private void writeResults(MinecraftClient client) { root.addProperty("glVersion", GlDebugInfo.getVersion()); root.addProperty("window", client.getWindow().getFramebufferWidth() + "x" + client.getWindow().getFramebufferHeight()); root.addProperty("baseY", baseY); + root.addProperty("maxFps", client.options.getMaxFps().getValue()); root.add("shots", shotResults); root.add("perf", perfResults); Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); diff --git a/src/main/java/com/forestotzka/yurufu/slabee/mixin/BlockMixin.java b/src/main/java/com/forestotzka/yurufu/slabee/mixin/BlockMixin.java index c815cc16..2cefadba 100644 --- a/src/main/java/com/forestotzka/yurufu/slabee/mixin/BlockMixin.java +++ b/src/main/java/com/forestotzka/yurufu/slabee/mixin/BlockMixin.java @@ -1,5 +1,6 @@ package com.forestotzka.yurufu.slabee.mixin; +import com.forestotzka.yurufu.slabee.block.AbstractDoubleSlabBlock; import com.forestotzka.yurufu.slabee.block.AbstractDoubleSlabBlockEntity; import com.forestotzka.yurufu.slabee.block.*; @@ -59,13 +60,20 @@ public void getPickStack(WorldView world, BlockPos pos, BlockState state, Callba @Inject(method = "shouldDrawSide", at = @At("HEAD"), cancellable = true) private static void onShouldDrawSide(BlockState state, BlockView world, BlockPos pos, Direction side, BlockPos otherPos, CallbackInfoReturnable cir) { - // ダブルハーフの中の片方のハーフについて、もう片方に接している内側の面を描くかどうか + // ダブルハーフの中の片方のハーフについて、もう片方に接している内側の面を描くかどうか。 + // この判定はすべてのブロックの面で呼ばれるので、ハーフ以外はワールドを見る前にすぐ抜ける。 + Block block = state.getBlock(); + if (!(block instanceof SlabBlock) && !(block instanceof VerticalSlabBlock)) { + return; + } BlockState here = world.getBlockState(pos); + if (!(here.getBlock() instanceof AbstractDoubleSlabBlock)) { + return; + } if (!(world.getBlockEntity(pos) instanceof AbstractDoubleSlabBlockEntity entity)) { return; } - Block block = state.getBlock(); if (block instanceof SlabBlock && here.isOf(ModBlocks.DOUBLE_SLAB_BLOCK)) { SlabType slabType = state.get(Properties.SLAB_TYPE); if (slabType == SlabType.BOTTOM && side == Direction.UP) { From 0d2e62334775c631cb9d794be4934455177aa006 Mon Sep 17 00:00:00 2001 From: Starlight <74122979+Yuru-Yurufu@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:11:34 +0900 Subject: [PATCH 4/6] test: read the scene FPS cap from SLABEE_SCENE_MAX_FPS --- .../yurufu/slabee/devclient/SceneCapture.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java index 193f1765..bea6d6c2 100644 --- a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java +++ b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java @@ -156,9 +156,9 @@ private static void configureOptions(GameOptions options) { options.pauseOnLostFocus = false; options.getViewDistance().setValue(8); options.getSimulationDistance().setValue(8); - // -Dslabee.scene.maxFps=60 のように指定すると、フレーム数の上限を固定して測れる + // 環境変数 SLABEE_SCENE_MAX_FPS=60(または -Dslabee.scene.maxFps=60)で、フレーム数の上限を固定して測れる // (描画が速くなるとフレーム数が増えてCPUを多く使い、同じマシンのサーバー側の測定に影響するため) - options.getMaxFps().setValue(Integer.getInteger("slabee.scene.maxFps", 260)); + options.getMaxFps().setValue(maxFps()); options.getEnableVsync().setValue(false); options.getCloudRenderMode().setValue(CloudRenderMode.OFF); options.getBobView().setValue(false); @@ -168,6 +168,17 @@ private static void configureOptions(GameOptions options) { options.getAo().setValue(true); } + private static int maxFps() { + String env = System.getenv("SLABEE_SCENE_MAX_FPS"); + if (env != null) { + try { + return Integer.parseInt(env.trim()); + } catch (NumberFormatException ignored) { + } + } + return Integer.getInteger("slabee.scene.maxFps", 260); + } + private static void createWorld(MinecraftClient client) { GameRules rules = new GameRules(); rules.get(GameRules.DO_DAYLIGHT_CYCLE).set(false, null); From 4c9fc83580c8a0c783623c2b23bae5df0debcd5e Mon Sep 17 00:00:00 2001 From: Starlight <74122979+Yuru-Yurufu@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:28:47 +0900 Subject: [PATCH 5/6] test: wait for light updates to finish before capturing Light propagation runs after blocks are placed. Shooting before it ends made the ground shading around double slabs differ from run to run. --- .../forestotzka/yurufu/slabee/devclient/SceneCapture.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java index bea6d6c2..14e5798a 100644 --- a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java +++ b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java @@ -292,7 +292,10 @@ private boolean moveAndSettle(MinecraftClient client, View view, int minTicks) { // 描画範囲のチャンクが読み込み・組み立てを終えて、完成数が増えなくなるまで待つ // (実機では遠くの地形が読み込み途中のまま測ってしまうことがあったため) int completed = client.worldRenderer.getCompletedChunkCount(); - if (arrived && client.worldRenderer.isTerrainRenderComplete() && completed == lastCompletedChunks) { + // 光の計算も終わるまで待つ(ブロックを置いたあと遅れて進むため、終わる前に撮ると地面の陰影が毎回ずれる) + boolean lightQuiet = client.world != null && !client.world.getLightingProvider().hasUpdates() + && (client.getServer() == null || !client.getServer().getOverworld().getLightingProvider().hasUpdates()); + if (arrived && lightQuiet && client.worldRenderer.isTerrainRenderComplete() && completed == lastCompletedChunks) { stableTicks++; } else { stableTicks = 0; From b6607ab9d5610965030f146e8246d6cdc668a39d Mon Sep 17 00:00:00 2001 From: Starlight <74122979+Yuru-Yurufu@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:52:12 +0900 Subject: [PATCH 6/6] test: keep autosave enabled during scene capture Disabling saving appears to make chunks that are unloaded while the camera moves come back with recomputed light, so the ground shading differed between runs. Under investigation; determinism check in progress. --- .../com/forestotzka/yurufu/slabee/devclient/SceneCapture.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java index 14e5798a..6ac10b09 100644 --- a/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java +++ b/src/devclient/java/com/forestotzka/yurufu/slabee/devclient/SceneCapture.java @@ -202,8 +202,6 @@ private void buildScene(IntegratedServer server) { server.submitAndJoin(() -> { ServerWorld world = server.getOverworld(); world.setTimeOfDay(6000); - // 撮影中は自動保存を止める(保存中の表示が写り込んだり、測定の数字が揺れたりするため) - world.savingDisabled = true; world.setWeather(1000000, 0, false, false); baseY = world.getTopY(Heightmap.Type.MOTION_BLOCKING, 0, 0);