diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockOcclusionCache.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockOcclusionCache.java deleted file mode 100644 index f157fac5b1..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockOcclusionCache.java +++ /dev/null @@ -1,227 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline; - -import it.unimi.dsi.fastutil.Hash; -import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenCustomHashMap; -import net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess; -import net.caffeinemc.mods.sodium.client.util.DirectionUtil; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.level.BlockGetter; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.material.FluidState; -import net.minecraft.world.phys.shapes.BooleanOp; -import net.minecraft.world.phys.shapes.Shapes; -import net.minecraft.world.phys.shapes.VoxelShape; - -public class BlockOcclusionCache { - private static final int CACHE_SIZE = 512; - - private static final int ENTRY_ABSENT = -1; - private static final int ENTRY_FALSE = 0; - private static final int ENTRY_TRUE = 1; - - - private final Object2IntLinkedOpenCustomHashMap comparisonLookupTable; - private final ShapeComparison cachedComparisonObject = new ShapeComparison(); - private final BlockPos.MutableBlockPos cachedPositionObject = new BlockPos.MutableBlockPos(); - - public BlockOcclusionCache() { - this.comparisonLookupTable = new Object2IntLinkedOpenCustomHashMap<>(CACHE_SIZE, 0.5F, new ShapeComparison.ShapeComparisonStrategy()); - this.comparisonLookupTable.defaultReturnValue(ENTRY_ABSENT); - } - - /** - * @param selfBlockState The state of the block in the level - * @param view The block view for this render context - * @param selfPos The position of the block - * @param facing The facing direction of the side to check - * @return True if the block side facing {@param dir} is not occluded, otherwise false - */ - public boolean shouldDrawSide(BlockState selfBlockState, BlockGetter view, BlockPos selfPos, Direction facing) { - BlockPos.MutableBlockPos neighborPos = this.cachedPositionObject; - neighborPos.setWithOffset(selfPos, facing); - - // The block state of the neighbor - BlockState neighborBlockState = view.getBlockState(neighborPos); - - // The cull shape of the neighbor between the block being rendered and it - VoxelShape neighborShape = neighborBlockState.getFaceOcclusionShape(DirectionUtil.getOpposite(facing)); - - // Minecraft enforces that if the neighbor has a full-block occlusion shape, the face is always hidden - if (isFullShape(neighborShape)) { - return false; - } - - // Blocks can define special behavior to control whether their faces are rendered. - // This is mostly used by transparent blocks (Leaves, Glass, etc.) to not render interior faces between blocks - // of the same type. - if (selfBlockState.skipRendering(neighborBlockState, facing)) { - return false; - } else if (PlatformBlockAccess.getInstance() - .shouldSkipRender(view, selfBlockState, neighborBlockState, selfPos, neighborPos, facing)) { - return false; - } - - // After any custom behavior has been handled, check if the neighbor block is transparent or has an empty - // cull shape. These blocks cannot hide any geometry. - if (isEmptyShape(neighborShape) || !neighborBlockState.canOcclude()) { - return true; - } - - // The cull shape between of the block being rendered, between it and the neighboring block - VoxelShape selfShape = selfBlockState.getFaceOcclusionShape(facing); - - // If the block being rendered has an empty cull shape, there will be no intersection with the neighboring - // block's cull shape, so no geometry can be hidden. - if (isEmptyShape(selfShape)) { - return true; - } - - // No other simplifications apply, so we need to perform a full shape comparison, which is very slow - return this.lookup(selfShape, neighborShape); - } - - private static boolean isFullShape(VoxelShape selfShape) { - return selfShape == Shapes.block(); - } - - private static boolean isEmptyShape(VoxelShape voxelShape) { - return voxelShape == Shapes.empty() || voxelShape.isEmpty(); - } - - /** - * Checks if a face of a fluid block should be rendered. It takes into account both occluding fluid face against its own waterlogged block state and the neighboring block state. This is an approximation that doesn't check voxel for shapes between the fluid and the neighboring block since that is handled by the fluid renderer separately and more accurately using actual fluid heights. It only uses voxel shape comparison for checking self-occlusion with the waterlogged block state. - * - * @param selfBlockState The state of the block in the level - * @param view The block view for this render context - * @param selfPos The position of the fluid - * @param facing The facing direction of the side to check - * @param fluid The fluid state - * @param fluidShape The non-empty shape of the fluid - * @return True if the fluid side facing {@param dir} is not occluded, otherwise false - */ - public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGetter view, BlockPos selfPos, Direction facing, FluidState fluid, VoxelShape fluidShape) { - var fluidShapeIsBlock = fluidShape == Shapes.block(); - - // only perform self-occlusion if the own block state can't occlude - if (selfBlockState.canOcclude()) { - var selfShape = selfBlockState.getFaceOcclusionShape(facing); - - // only a non-empty self-shape can occlude anything - if (!isEmptyShape(selfShape)) { - // a full self-shape occludes everything - if (isFullShape(selfShape) && fluidShapeIsBlock) { - return false; - } - - // perform occlusion of the fluid by the block it's contained in - if (!this.lookup(fluidShape, selfShape)) { - return false; - } - } - } - - // perform occlusion against the neighboring block - BlockPos.MutableBlockPos otherPos = this.cachedPositionObject; - otherPos.set(selfPos.getX() + facing.getStepX(), selfPos.getY() + facing.getStepY(), selfPos.getZ() + facing.getStepZ()); - BlockState otherState = view.getBlockState(otherPos); - - // don't render anything if the other blocks is the same fluid - if (otherState.getFluidState() == fluid) { - return false; - } - - // check for special fluid occlusion behavior - if (PlatformBlockAccess.getInstance().shouldOccludeFluid(facing.getOpposite(), otherState, fluid)) { - return false; - } - - // the up direction doesn't do occlusion with other block shapes - if (facing == Direction.UP) { - return true; - } - - // only occlude against blocks that can potentially occlude in the first place - if (!otherState.canOcclude()) { - return true; - } - - var otherShape = otherState.getFaceOcclusionShape(facing.getOpposite()); - - // If the other block has an empty cull shape, then it cannot hide any geometry - if (isEmptyShape(otherShape)) { - return true; - } - - // If both blocks use a full-cube cull shape, then they will always hide the faces between each other. - // No voxel shape comparison is done after this point because it's redundant with the later more accurate check. - return !isFullShape(otherShape) || !fluidShapeIsBlock; - } - - private boolean lookup(VoxelShape self, VoxelShape other) { - ShapeComparison comparison = this.cachedComparisonObject; - comparison.self = self; - comparison.other = other; - - // Entries at the cache are promoted to the top of the table when accessed - // The entries at the bottom of the table are removed when it gets too large - return switch (this.comparisonLookupTable.getAndMoveToFirst(comparison)) { - case ENTRY_FALSE -> false; - case ENTRY_TRUE -> true; - default -> this.calculate(comparison); - }; - } - - private boolean calculate(ShapeComparison comparison) { - boolean result = Shapes.joinIsNotEmpty(comparison.self, comparison.other, BooleanOp.ONLY_FIRST); - - // Remove entries while the table is too large - while (this.comparisonLookupTable.size() >= CACHE_SIZE) { - this.comparisonLookupTable.removeLastInt(); - } - - this.comparisonLookupTable.putAndMoveToFirst(comparison.copy(), (result ? ENTRY_TRUE : ENTRY_FALSE)); - - return result; - } - - private static final class ShapeComparison { - private VoxelShape self, other; - - private ShapeComparison() { - - } - - private ShapeComparison(VoxelShape self, VoxelShape other) { - this.self = self; - this.other = other; - } - - public static class ShapeComparisonStrategy implements Hash.Strategy { - @Override - public int hashCode(ShapeComparison value) { - int result = System.identityHashCode(value.self); - result = 31 * result + System.identityHashCode(value.other); - - return result; - } - - @Override - public boolean equals(ShapeComparison a, ShapeComparison b) { - if (a == b) { - return true; - } - - if (a == null || b == null) { - return false; - } - - return a.self == b.self && a.other == b.other; - } - } - - public ShapeComparison copy() { - return new ShapeComparison(this.self, this.other); - } - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/DefaultFluidRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/DefaultFluidRenderer.java index cf89774909..be1ba3cfb8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/DefaultFluidRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/DefaultFluidRenderer.java @@ -1,6 +1,8 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline; +import it.unimi.dsi.fastutil.bytes.ByteArrayList; +import it.unimi.dsi.fastutil.bytes.ByteList; import net.caffeinemc.mods.sodium.api.util.ColorARGB; import net.caffeinemc.mods.sodium.api.util.NormI8; import net.caffeinemc.mods.sodium.client.model.color.ColorProvider; @@ -27,26 +29,46 @@ import net.minecraft.tags.FluidTags; import net.minecraft.util.Mth; import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; -import org.apache.commons.lang3.mutable.MutableFloat; -import org.apache.commons.lang3.mutable.MutableInt; +// TODO: fix perfectly joining stairs from being fluid-exposed to each other and making weird fluid shapes, use shape operation BooleanOp.AND (extend cache to support it) + +/** + * The default fluid renderer implementation generates fluid geometry for each fluid block based on its fluid state, the block state, and other blocks around it. + *

+ * First, preliminary culling for the six fluid faces determines whether they are visible at all. Visibility refers to a whether a face is rendered. Faces are not rendered if the neighboring block is of the same fluid type or if the face is occluded by the block it's contained in, i.e. water logging, or the neighboring block. Self-visibility refers to whether the block the fluid is inside is preventing the fluid face from rendering. + *

+ * If the fluid block is not a full fluid block, the corner fluid heights are calculated from the fluid heights of the surrounding blocks. Each corner is calculated separately and takes weighted samples, which are then averaged into the final height. The fluid block itself contributes a sample, alongside directly neighboring and diagonally neighboring blocks, depending on connectivity. Samples from neighboring blocks are only taken if the blocks' shapes don't occlude the path between them. A sample is also taken from the diagonally neighboring block, if there is a path to it through one of the direct neighbors. + *

+ * Before visible fluid faces are rendered into quads, they must also be tested as exposed. Exposed means that there is an open path through this face of the fluid block. This is independent of whether there's another block of this type of fluid there. The exposed test is done against the neighboring block's occlusion shape. + *

+ * Visible implies self-visible implies exposed but not the other way around. + *

+ * The top fluid face is additionally culled if the flooded cave heuristic determines that the fluid is within a flooded cave. Within flooded caves the top face isn't rendered for performance and because it would look ugly. + */ public class DefaultFluidRenderer { // TODO: allow this to be changed by vertex format, WARNING: make sure TranslucentGeometryCollector knows about EPSILON // TODO: move fluid rendering to a separate render pass and control glPolygonOffset and glDepthFunc to fix this properly public static final float EPSILON = 0.001f; private static final float ALIGNED_EQUALS_EPSILON = 0.011f; + private static final float DISCARD_SAMPLE = -1.0f; + private static final float FULL_HEIGHT = 0.8888889f; + private final BlockPos.MutableBlockPos scratchPos = new BlockPos.MutableBlockPos(); - private final MutableFloat scratchHeight = new MutableFloat(0); - private final MutableInt scratchSamples = new MutableInt(); + private final BlockPos.MutableBlockPos occlusionScratchPos = new BlockPos.MutableBlockPos(); + private float scratchHeight = 0.0f; + private int scratchSamples = 0; + private final ByteList stack = new ByteArrayList(); + private long visited = 0; - private final BlockOcclusionCache occlusionCache = new BlockOcclusionCache(); + private final ShapeComparisonCache occlusionCache = new ShapeComparisonCache(); private final ModelQuadViewMutable quad = new ModelQuad(); @@ -64,55 +86,285 @@ public DefaultFluidRenderer(LightPipelineProvider lighters) { this.lighters = lighters; } - private boolean isFullBlockFluidOccluded(BlockAndTintGetter world, BlockPos pos, Direction dir, BlockState blockState, FluidState fluid) { - // check if this face of the fluid, assuming a full-block cull shape, is occluded by the block it's in or a neighboring block. - // it doesn't do a voxel shape comparison with the neighboring blocks since that is already done by isSideExposed - return !this.occlusionCache.shouldDrawFullBlockFluidSide(blockState, world, pos, dir, fluid, Shapes.block()); + /** + * Checks if a fluid face should be considered for rendering based on the neighboring block state and its occlusion shape, but without comparing the occlusion shapes with each other. This only calculates visibility, not exposure. + * + * @param view The block view for this render context + * @param selfPos The position of the fluid + * @param facing The facing direction of the side to check + * @param fluid The fluid state + * @return True if the fluid side facing {@param facing} is visible, otherwise false + */ + public boolean isFullBlockFluidSideVisible(BlockGetter view, BlockPos selfPos, Direction facing, FluidState fluid) { + // perform occlusion against the neighboring block + BlockState otherState = view.getBlockState(this.occlusionScratchPos.setWithOffset(selfPos, facing)); + + // check for special fluid occlusion behavior + if (PlatformBlockAccess.getInstance().shouldOccludeFluid(facing.getOpposite(), otherState, fluid)) { + return false; + } + + // don't render anything if the other blocks is the same fluid + // NOTE: this check is already included in the default implementation of the above shouldOccludeFluid + if (otherState.getFluidState().getType().isSame(fluid.getType())) { + return false; + } + + // the up direction doesn't do occlusion with other block shapes + if (facing == Direction.UP) { + return true; + } + + // only occlude against blocks that can potentially occlude in the first place + if (!otherState.canOcclude()) { + return true; + } + + var otherShape = otherState.getFaceOcclusionShape(DirectionUtil.getOpposite(facing)); + + // If the other block has an empty cull shape, then it cannot hide any geometry + if (ShapeComparisonCache.isEmptyShape(otherShape)) { + return true; + } + + // If both blocks use a full-cube cull shape, then they will always hide the faces between each other. + // No voxel shape comparison is done after this point because it's redundant with the later more accurate check. + return !ShapeComparisonCache.isFullShape(otherShape); } - private boolean isSideExposed(BlockAndTintGetter world, int x, int y, int z, Direction dir, float height) { - BlockPos pos = this.scratchPos.set(x + dir.getStepX(), y + dir.getStepY(), z + dir.getStepZ()); - BlockState blockState = world.getBlockState(pos); + /** + * Checks if a face of the fluid is self-visible and not occluded by the block it's contained in. + * + * @param selfBlockState The state of the block in the level + * @param facing The facing direction of the side to check + * @param fluidShape The shape of the fluid + * @return True if the fluid side facing {@param facing} is self-visible, otherwise false + */ + public boolean isFluidSelfVisible(BlockState selfBlockState, Direction facing, VoxelShape fluidShape) { + // only perform self-occlusion if the own block state can't occlude + if (selfBlockState.canOcclude()) { + var selfShape = selfBlockState.getFaceOcclusionShape(facing); + + // only a non-empty self-shape can occlude anything + if (!ShapeComparisonCache.isEmptyShape(selfShape)) { + // a full self-shape occludes everything + if (ShapeComparisonCache.isFullShape(selfShape) && ShapeComparisonCache.isFullShape(fluidShape)) { + return false; + } + + // perform occlusion of the fluid by the block it's contained in + return this.occlusionCache.lookup(fluidShape, selfShape); + } + } + + return true; + } - if (blockState.canOcclude()) { - VoxelShape shape = blockState.getOcclusionShape(); + private boolean isFullBlockFluidSelfVisible(BlockState blockState, Direction dir) { + return this.isFluidSelfVisible(blockState, dir, Shapes.block()); + } - // Hoist these checks to avoid allocating the shape below - if (shape.isEmpty()) { - return true; + /** + * Checks if a face of a fluid block with a specific height should be rendered based on the neighboring block state. + * + * @param world The block view for this render context + * @param neighborPos The position of the neighboring block + * @param facing The facing direction of the side to check + * @param height The height of the fluid + * @return True if the fluid side facing {@param facing} is not occluded, otherwise false + */ + public boolean isFluidSideExposed(BlockAndTintGetter world, BlockPos neighborPos, Direction facing, float height) { + var neighborBlockState = world.getBlockState(neighborPos); + + // zero-height fluids don't render anything anyway + if (height <= 0.0F) { + return false; + } + + // only perform occlusion against blocks that can potentially occlude + if (!neighborBlockState.canOcclude()) { + return true; + } + + // if it's an up-fluid and the height is not 1, it can't be occluded + if (facing == Direction.UP && height < 1.0F) { + return true; + } + + VoxelShape neighborShape = neighborBlockState.getFaceOcclusionShape(DirectionUtil.getOpposite(facing)); + + // empty neighbor occlusion shape can't occlude anything + if (ShapeComparisonCache.isEmptyShape(neighborShape)) { + return true; + } + + // full neighbor occlusion shape occludes everything + if (ShapeComparisonCache.isFullShape(neighborShape)) { + return false; + } + + VoxelShape fluidShape; + if (height >= 1.0F) { + fluidShape = Shapes.block(); + } else { + fluidShape = Shapes.box(0.0D, 0.0D, 0.0D, 1.0D, height, 1.0D); + } + + return this.occlusionCache.lookup(fluidShape, neighborShape); + } + + private boolean isSideExposedOffset(BlockAndTintGetter world, BlockPos originPos, Direction dir, float height) { + return this.isFluidSideExposed(world, this.scratchPos.setWithOffset(originPos, dir), dir, height); + } + + /** + * Calculates the combined visibility of a fluid face based on the neighboring block states and the fluid state. + */ + private boolean isFullBlockFluidVisible(BlockAndTintGetter world, BlockPos pos, Direction dir, BlockState blockState, FluidState fluid) { + return isFullBlockFluidSelfVisible(blockState, dir) && this.isFullBlockFluidSideVisible(world, pos, dir, fluid); + } + + /** + * Gets a fluid height sample for a specific block position for a given fluid. If the fluid is a different type and the block is solid, the sample is discarded. + * + * @param world The block view for this render context + * @param fluid The requesting block's fluid type + * @param blockPos The position of the block + * @return The fluid height sample + */ + private float sampleFluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockPos) { + BlockState blockState = world.getBlockState(blockPos); + FluidState fluidState = blockState.getFluidState(); + + if (fluid.isSame(fluidState.getType())) { + FluidState fluidStateUp = world.getFluidState(this.scratchPos.setWithOffset(blockPos, Direction.UP)); + + if (fluid.isSame(fluidStateUp.getType())) { + return 1.0f; + } else { + return fluidState.getOwnHeight(); } + } - VoxelShape threshold = Shapes.box(0.0D, 0.0D, 0.0D, 1.0D, height, 1.0D); + if (!blockState.isSolid()) { + return 0.0f; + } - return !Shapes.blockOccudes(threshold, shape, dir); + return DISCARD_SAMPLE; + } + + private float sampleFluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos origin, Direction offset) { + return this.sampleFluidHeight(world, fluid, this.scratchPos.setWithOffset(origin, offset)); + } + + private void addHeightSample(float sample) { + if (sample >= 0.8f) { + this.scratchHeight += sample * 10.0f; + this.scratchSamples += 10; + } else if (sample >= 0.0f) { + this.scratchHeight += sample; + this.scratchSamples++; } - return true; + // else -> sample == DISCARD_SAMPLE } - public void render(LevelSlice level, BlockState blockState, FluidState fluidState, BlockPos blockPos, BlockPos offset, TranslucentGeometryCollector collector, ChunkModelBuilder meshBuilder, Material material, ColorProvider colorProvider, TextureAtlasSprite[] sprites) { - int posX = blockPos.getX(); - int posY = blockPos.getY(); - int posZ = blockPos.getZ(); + /** + * Calculates the corner height of a fluid block based on the fluid heights of the surrounding blocks by taking samples of connected blocks. + * + * @param world The block view for this render context + * @param origin The position of the fluid block + * @param fluid The fluid type of the fluid block + * @param fluidHeight The fluid block's own fluid height + * @param dirA The first direction of the corner + * @param dirB The second direction of the corner + * @param fluidHeightA The fluid height of the block in direction A + * @param fluidHeightB The fluid height of the block in direction B + * @param exposedA Whether the block in direction A is exposed + * @param exposedB Whether the block in direction B is exposed + * @return The calculated corner height + */ + private float fluidCornerHeight(BlockAndTintGetter world, BlockPos origin, Fluid fluid, float fluidHeight, Direction dirA, Direction dirB, float fluidHeightA, float fluidHeightB, boolean exposedA, boolean exposedB) { + // if both sides are full height fluids, the corner is full height too + float filteredHeightA = exposedA ? fluidHeightA : DISCARD_SAMPLE; + float filteredHeightB = exposedB ? fluidHeightB : DISCARD_SAMPLE; + if (filteredHeightA >= 1.0f || filteredHeightB >= 1.0f) { + return 1.0f; + } + + // "D" stands for diagonal + boolean exposedADB = false; + + // if there is any fluid on either side, check if the diagonal has any + if (filteredHeightA > 0.0f || filteredHeightB > 0.0f) { + // check that there's an accessible path to the diagonal + var aNeighbor = this.scratchPos.setWithOffset(origin, dirA); + var exposedAD = this.isFullBlockFluidSelfVisible(world.getBlockState(aNeighbor), dirB) && + this.isSideExposedOffset(world, aNeighbor, dirB, 1.0f); + var bNeighbor = this.scratchPos.setWithOffset(origin, dirB); + var exposedBD = this.isFullBlockFluidSelfVisible(world.getBlockState(bNeighbor), dirA) && + this.isSideExposedOffset(world, bNeighbor, dirA, 1.0f); + + exposedADB = exposedAD && exposedBD; + if (exposedA && exposedAD || exposedB && exposedBD) { + // add a sample using diagonal block's fluid height + var abNeighbor = this.scratchPos.set(origin).move(dirA).move(dirB); + float height = this.sampleFluidHeight(world, fluid, abNeighbor); + + if (height >= 1.0f) { + return 1.0f; + } + this.addHeightSample(height); + } + } + + // add samples for the sides if they're exposed or if there's a path through the diagonal to them + if (exposedA || exposedB && exposedADB) { + this.addHeightSample(fluidHeightA); + } + if (exposedB || exposedA && exposedADB) { + this.addHeightSample(fluidHeightB); + } + + this.addHeightSample(fluidHeight); + + // gather the samples and reset + float result = this.scratchHeight / this.scratchSamples; + + this.scratchHeight = 0.0f; + this.scratchSamples = 0; + + return result; + } + + public void render(LevelSlice level, BlockState blockState, FluidState fluidState, BlockPos blockPos, BlockPos offset, TranslucentGeometryCollector collector, ChunkModelBuilder meshBuilder, Material material, ColorProvider colorProvider, TextureAtlasSprite[] sprites) { Fluid fluid = fluidState.getType(); - boolean cullUp = this.isFullBlockFluidOccluded(level, blockPos, Direction.UP, blockState, fluidState); - boolean cullDown = this.isFullBlockFluidOccluded(level, blockPos, Direction.DOWN, blockState, fluidState) || - !this.isSideExposed(level, posX, posY, posZ, Direction.DOWN, 0.8888889F); - boolean cullNorth = this.isFullBlockFluidOccluded(level, blockPos, Direction.NORTH, blockState, fluidState); - boolean cullSouth = this.isFullBlockFluidOccluded(level, blockPos, Direction.SOUTH, blockState, fluidState); - boolean cullWest = this.isFullBlockFluidOccluded(level, blockPos, Direction.WEST, blockState, fluidState); - boolean cullEast = this.isFullBlockFluidOccluded(level, blockPos, Direction.EAST, blockState, fluidState); + boolean upVisible = this.isFullBlockFluidVisible(level, blockPos, Direction.UP, blockState, fluidState); + boolean downVisible = this.isFullBlockFluidVisible(level, blockPos, Direction.DOWN, blockState, fluidState) && + this.isSideExposedOffset(level, blockPos, Direction.DOWN, FULL_HEIGHT); + + // self-visibility and visibility are kept separate because self-visibility is used by the corner height sampling + // while visibility would be too strict (as faces are not visible if there's an adjacent fluid of the same type) + boolean northSelfVisible = this.isFullBlockFluidSelfVisible(blockState, Direction.NORTH); + boolean southSelfVisible = this.isFullBlockFluidSelfVisible(blockState, Direction.SOUTH); + boolean westSelfVisible = this.isFullBlockFluidSelfVisible(blockState, Direction.WEST); + boolean eastSelfVisible = this.isFullBlockFluidSelfVisible(blockState, Direction.EAST); + + boolean northVisible = northSelfVisible && this.isFullBlockFluidSideVisible(level, blockPos, Direction.NORTH, fluidState); + boolean southVisible = southSelfVisible && this.isFullBlockFluidSideVisible(level, blockPos, Direction.SOUTH, fluidState); + boolean westVisible = westSelfVisible && this.isFullBlockFluidSideVisible(level, blockPos, Direction.WEST, fluidState); + boolean eastVisible = eastSelfVisible && this.isFullBlockFluidSideVisible(level, blockPos, Direction.EAST, fluidState); // stop rendering if all faces of the fluid are occluded - if (cullUp && cullDown && cullEast && cullWest && cullNorth && cullSouth) { + if (!upVisible && !downVisible && !eastVisible && !westVisible && !northVisible && !southVisible) { return; } boolean isWater = fluidState.is(FluidTags.WATER); - float fluidHeight = this.fluidHeight(level, fluid, blockPos, Direction.UP); + float fluidHeight = this.sampleFluidHeight(level, fluid, blockPos); float northWestHeight, southWestHeight, southEastHeight, northEastHeight; if (fluidHeight >= 1.0f) { northWestHeight = 1.0f; @@ -120,25 +372,29 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat southEastHeight = 1.0f; northEastHeight = 1.0f; } else { - var scratchPos = new BlockPos.MutableBlockPos(); - float heightNorth = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.NORTH), Direction.NORTH); - float heightSouth = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.SOUTH), Direction.SOUTH); - float heightEast = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.EAST), Direction.EAST); - float heightWest = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.WEST), Direction.WEST); - northWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightWest, scratchPos.set(blockPos) - .move(Direction.NORTH) - .move(Direction.WEST)); - southWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightWest, scratchPos.set(blockPos) - .move(Direction.SOUTH) - .move(Direction.WEST)); - southEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightEast, scratchPos.set(blockPos) - .move(Direction.SOUTH) - .move(Direction.EAST)); - northEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightEast, scratchPos.set(blockPos) - .move(Direction.NORTH) - .move(Direction.EAST)); - } - float yOffset = cullDown ? 0.0F : EPSILON; + // calculate the exposure of the side faces using a conservative estimate (1.0f) of the fluid height for deciding what neighbor samples to take + boolean northExposed = northSelfVisible && this.isSideExposedOffset(level, blockPos, Direction.NORTH, 1.0f); + boolean southExposed = southSelfVisible && this.isSideExposedOffset(level, blockPos, Direction.SOUTH, 1.0f); + boolean westExposed = westSelfVisible && this.isSideExposedOffset(level, blockPos, Direction.WEST, 1.0f); + boolean eastExposed = eastSelfVisible && this.isSideExposedOffset(level, blockPos, Direction.EAST, 1.0f); + + float heightNorth = this.sampleFluidHeight(level, fluid, blockPos, Direction.NORTH); + float heightSouth = this.sampleFluidHeight(level, fluid, blockPos, Direction.SOUTH); + float heightEast = this.sampleFluidHeight(level, fluid, blockPos, Direction.EAST); + float heightWest = this.sampleFluidHeight(level, fluid, blockPos, Direction.WEST); + + northWestHeight = this.fluidCornerHeight(level, blockPos, fluid, fluidHeight, Direction.NORTH, Direction.WEST, heightNorth, heightWest, northExposed, westExposed); + southWestHeight = this.fluidCornerHeight(level, blockPos, fluid, fluidHeight, Direction.SOUTH, Direction.WEST, heightSouth, heightWest, southExposed, westExposed); + southEastHeight = this.fluidCornerHeight(level, blockPos, fluid, fluidHeight, Direction.SOUTH, Direction.EAST, heightSouth, heightEast, southExposed, eastExposed); + northEastHeight = this.fluidCornerHeight(level, blockPos, fluid, fluidHeight, Direction.NORTH, Direction.EAST, heightNorth, heightEast, northExposed, eastExposed); + + // use the approximate exposure data to maybe cull faces + northVisible &= northExposed; + southVisible &= southExposed; + westVisible &= westExposed; + eastVisible &= eastExposed; + } + float yOffset = !downVisible ? 0.0F : EPSILON; final ModelQuadViewMutable quad = this.quad; @@ -147,7 +403,21 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat quad.setFlags(0); - if (!cullUp && this.isSideExposed(level, posX, posY, posZ, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)))) { + // calculate up fluid face exposure + if (upVisible) { + float totalMinHeight = Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)); + upVisible = this.isSideExposedOffset(level, blockPos, Direction.UP, totalMinHeight); + } + + // apply heuristic to not render up face it's in a flooded cave + boolean inwardsUpFaceVisible = true; + if (upVisible) { + var exposureResult = getUpFaceExposureByNeighbors(level, blockPos, fluidState); + upVisible = exposureResult != NO_EXPOSURE; + inwardsUpFaceVisible = exposureResult == BOTH_EXPOSED; + } + + if (upVisible) { northWestHeight -= EPSILON; southWestHeight -= EPSILON; southEastHeight -= EPSILON; @@ -205,6 +475,7 @@ && isAlignedEquals(northWestHeight, southEastHeight) && isAlignedEquals(southEastHeight, southWestHeight) && isAlignedEquals(southWestHeight, northEastHeight); + // calculate in which direction the crease is best placed and rotate the quad accordingly boolean creaseNorthEastSouthWest = aligned || northEastHeight > northWestHeight && northEastHeight > southEastHeight || northEastHeight < northWestHeight && northEastHeight < southEastHeight @@ -226,13 +497,12 @@ && isAlignedEquals(southEastHeight, southWestHeight) this.updateQuad(quad, level, blockPos, lighter, Direction.UP, ModelQuadFacing.POS_Y, 1.0F, colorProvider, fluidState); this.writeQuad(meshBuilder, collector, material, offset, quad, aligned ? ModelQuadFacing.POS_Y : ModelQuadFacing.UNASSIGNED, false); - if (fluidState.shouldRenderBackwardUpFace(level, this.scratchPos.set(posX, posY + 1, posZ))) { - this.writeQuad(meshBuilder, collector, material, offset, quad, - aligned ? ModelQuadFacing.NEG_Y : ModelQuadFacing.UNASSIGNED, true); + if (inwardsUpFaceVisible) { + this.writeQuad(meshBuilder, collector, material, offset, quad, aligned ? ModelQuadFacing.NEG_Y : ModelQuadFacing.UNASSIGNED, true); } } - if (!cullDown) { + if (downVisible) { TextureAtlasSprite sprite = sprites[0]; float minU = sprite.getU0(); @@ -248,6 +518,13 @@ && isAlignedEquals(southEastHeight, southWestHeight) this.updateQuad(quad, level, blockPos, lighter, Direction.DOWN, ModelQuadFacing.NEG_Y, 1.0F, colorProvider, fluidState); this.writeQuad(meshBuilder, collector, material, offset, quad, ModelQuadFacing.NEG_Y, false); + + // render inwards facing down fluid face using the same heuristic as the side faces. + // this fixes a number of inconsistencies between the top and side faces + var blockStateBelow = level.getBlockState(this.scratchPos.setWithOffset(blockPos, Direction.DOWN)); + if (!PlatformBlockAccess.getInstance().shouldShowFluidOverlay(blockStateBelow, level, this.scratchPos, fluidState)) { + this.writeQuad(meshBuilder, collector, material, offset, quad, ModelQuadFacing.POS_Y, true); + } } quad.setFlags(ModelQuadFlags.IS_PARALLEL | ModelQuadFlags.IS_ALIGNED); @@ -262,7 +539,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) switch (dir) { case NORTH -> { - if (cullNorth) { + if (!northVisible) { continue; } c1 = northWestHeight; @@ -273,7 +550,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = z1; } case SOUTH -> { - if (cullSouth) { + if (!southVisible) { continue; } c1 = southEastHeight; @@ -284,7 +561,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = z1; } case WEST -> { - if (cullWest) { + if (!westVisible) { continue; } c1 = southWestHeight; @@ -295,7 +572,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = 0.0f; } case EAST -> { - if (cullEast) { + if (!eastVisible) { continue; } c1 = northEastHeight; @@ -310,20 +587,18 @@ && isAlignedEquals(southEastHeight, southWestHeight) } } - if (this.isSideExposed(level, posX, posY, posZ, dir, Math.max(c1, c2))) { - int adjX = posX + dir.getStepX(); - int adjY = posY + dir.getStepY(); - int adjZ = posZ + dir.getStepZ(); + var sideFluidHeight = Math.max(c1, c2); + this.scratchPos.setWithOffset(blockPos, dir); + if (this.isFluidSideExposed(level, this.scratchPos, dir, sideFluidHeight)) { TextureAtlasSprite sprite = sprites[1]; boolean isOverlay = false; if (sprites.length > 2 && sprites[2] != null) { - BlockPos adjPos = this.scratchPos.set(adjX, adjY, adjZ); - BlockState adjBlock = level.getBlockState(adjPos); + BlockState adjBlock = level.getBlockState(this.scratchPos); - if (PlatformBlockAccess.getInstance().shouldShowFluidOverlay(adjBlock, level, adjPos, fluidState)) { + if (PlatformBlockAccess.getInstance().shouldShowFluidOverlay(adjBlock, level, this.scratchPos, fluidState)) { sprite = sprites[2]; isOverlay = true; } @@ -352,9 +627,123 @@ && isAlignedEquals(southEastHeight, southWestHeight) if (!isOverlay) { this.writeQuad(meshBuilder, collector, material, offset, quad, facing.getOpposite(), true); } + } + } + } + + private static final int NO_EXPOSURE = 0b00; + private static final int OUTWARDS_EXPOSED = NO_EXPOSURE | 0b01; + private static final int BOTH_EXPOSED = OUTWARDS_EXPOSED | 0b10; + + /** + * This flooded cave heuristic performs a depth-first search looking for a block that causes a fluid quad to be visible and is reachable through a path of same-type fluid source blocks. If such a block exists, the fluid is considered exposed. If it can't be reached, the fluid is considered occluded. + *

+ * Since in some cases only the outward fluid face should be rendered, it returns a bitmask indicating if the inwards and outwards faces are exposed. + *

+ * NOTE: Sometimes this suffers from missing block updates because neighboring chunks aren't rebuild if the block receiving the update wasn't on the chunk border. + */ + private int getUpFaceExposureByNeighbors(BlockAndTintGetter level, BlockPos origin, FluidState fluidState) { + // performs a simple DFS using a stack and a visited bit mask + this.visited = 0; + var stack = this.stack; + stack.clear(); + + var result = 0; + result |= visitExposureNeighbor(level, origin, fluidState, stack, (byte) 0, (byte) 0); + if (result == BOTH_EXPOSED) { + return result; + } + while (!stack.isEmpty()) { + // remove coordinates from the stack in reverse order to preserve their format + var z = stack.removeByte(stack.size() - 1); + var x = stack.removeByte(stack.size() - 1); + + // traverse into unvisited neighbors, return immediately if both faces are exposed (no further change possible) + if (x < 2) { + result |= visitExposureNeighbor(level, origin, fluidState, stack, (byte) (x + 1), z); + if (result == BOTH_EXPOSED) { + return result; + } + } + if (x > -2) { + result |= visitExposureNeighbor(level, origin, fluidState, stack, (byte) (x - 1), z); + if (result == BOTH_EXPOSED) { + return result; + } + } + if (z < 2) { + result |= visitExposureNeighbor(level, origin, fluidState, stack, x, (byte) (z + 1)); + if (result == BOTH_EXPOSED) { + return result; + } + } + if (z > -2) { + result |= visitExposureNeighbor(level, origin, fluidState, stack, x, (byte) (z - 1)); + if (result == BOTH_EXPOSED) { + return result; + } } } + + return result; + } + + private long offsetToMask(int x, int z) { + return 1L << ((x + 2) + (z + 2) * 5); + } + + private int visitExposureNeighbor(BlockAndTintGetter level, BlockPos origin, FluidState fluidState, ByteList stack, byte xOffset, byte zOffset) { + // stop if position was already visited previously + var upNeighborMask = offsetToMask(xOffset, zOffset); + if ((this.visited & upNeighborMask) != 0) { + return NO_EXPOSURE; + } + this.visited |= upNeighborMask; + + // stop at solid blocks, don't propagate but also not considered exposed + var neighborBlockState = level.getBlockState(this.scratchPos.setWithOffset(origin, xOffset, 0, zOffset)); + if (neighborBlockState.isSolidRender()) { + return NO_EXPOSURE; + } + + var fluid = fluidState.getType(); + var aboveBlockState = level.getBlockState(this.scratchPos.move(Direction.UP)); + var aboveIsSameFluid = aboveBlockState.getFluidState().isSourceOfType(fluid); + + var result = NO_EXPOSURE; + + // propagate connectedness through same-type fluid blocks. + // only propagate if the block above is not the same fluid. Since if it is, the propagation stops + // since the potential fluid surface is not connected. + if (neighborBlockState.getFluidState().isSourceOfType(fluid)) { + if (!aboveIsSameFluid) { + stack.add(xOffset); + stack.add(zOffset); + } + } + + // if the block is not solid, and not the same fluid, render at least the outwards face, and the inwards face if the block shows them + else if (!PlatformBlockAccess.getInstance().shouldShowFluidOverlay(neighborBlockState, level, this.scratchPos, fluidState)) { + return BOTH_EXPOSED; + } else { + result = OUTWARDS_EXPOSED; + } + + // expose faces if the block above is not the same fluid and not solid, i.e. the up face is visible. + // If it's a block that should have fluid faces rendered against it, expose both. Otherwise just the outwards face is rendered + // to prevent the inwards face from being visible from within the water. However, the outwards face is still visible from the outside + // and needs to be rendered in any case the block is not solid. + if (!aboveIsSameFluid && !aboveBlockState.isSolidRender()) { + if (!PlatformBlockAccess.getInstance().shouldShowFluidOverlay(aboveBlockState, level, this.scratchPos, fluidState)) { + return BOTH_EXPOSED; + } else { + // propagation can't stop immediately since the inwards face might still become exposed through further traversal + result |= OUTWARDS_EXPOSED; + } + } + + return result; } private static boolean isAlignedEquals(float a, float b) { @@ -377,7 +766,7 @@ private void updateQuad(ModelQuadViewMutable quad, LevelSlice level, BlockPos po lighter.calculate(quad, pos, light, null, dir, false, false); - colorProvider.getColors(level, pos, scratchPos, fluidState, quad, this.quadColors); + colorProvider.getColors(level, pos, this.scratchPos, fluidState, quad, this.quadColors); // multiply the per-vertex color against the combined brightness // the combined brightness is the per-vertex brightness multiplied by the block's brightness @@ -438,59 +827,4 @@ private static void setVertex(ModelQuadViewMutable quad, int i, float x, float y quad.setTexU(i, u); quad.setTexV(i, v); } - - private float fluidCornerHeight(BlockAndTintGetter world, Fluid fluid, float fluidHeight, float fluidHeightX, float fluidHeightY, BlockPos blockPos) { - if (fluidHeightY >= 1.0f || fluidHeightX >= 1.0f) { - return 1.0f; - } - - if (fluidHeightY > 0.0f || fluidHeightX > 0.0f) { - float height = this.fluidHeight(world, fluid, blockPos, Direction.UP); - - if (height >= 1.0f) { - return 1.0f; - } - - this.modifyHeight(this.scratchHeight, this.scratchSamples, height); - } - - this.modifyHeight(this.scratchHeight, this.scratchSamples, fluidHeight); - this.modifyHeight(this.scratchHeight, this.scratchSamples, fluidHeightY); - this.modifyHeight(this.scratchHeight, this.scratchSamples, fluidHeightX); - - float result = this.scratchHeight.floatValue() / this.scratchSamples.intValue(); - this.scratchHeight.setValue(0); - this.scratchSamples.setValue(0); - - return result; - } - - private void modifyHeight(MutableFloat totalHeight, MutableInt samples, float target) { - if (target >= 0.8f) { - totalHeight.add(target * 10.0f); - samples.add(10); - } else if (target >= 0.0f) { - totalHeight.add(target); - samples.increment(); - } - } - - private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockPos, Direction direction) { - BlockState blockState = world.getBlockState(blockPos); - FluidState fluidState = blockState.getFluidState(); - - if (fluid.isSame(fluidState.getType())) { - FluidState fluidStateUp = world.getFluidState(blockPos.above()); - - if (fluid.isSame(fluidStateUp.getType())) { - return 1.0f; - } else { - return fluidState.getOwnHeight(); - } - } - if (!blockState.isSolid()) { - return 0.0f; - } - return -1.0f; - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/ShapeComparisonCache.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/ShapeComparisonCache.java new file mode 100644 index 0000000000..44ebc78a11 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/ShapeComparisonCache.java @@ -0,0 +1,106 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline; + +import it.unimi.dsi.fastutil.Hash; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenCustomHashMap; +import net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess; +import net.caffeinemc.mods.sodium.client.util.DirectionUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class ShapeComparisonCache { + private static final int CACHE_SIZE = 512; + + private static final int ENTRY_ABSENT = -1; + private static final int ENTRY_FALSE = 0; + private static final int ENTRY_TRUE = 1; + + private final Object2IntLinkedOpenCustomHashMap comparisonLookupTable; + private final ShapeComparison cachedComparisonObject = new ShapeComparison(); + + public ShapeComparisonCache() { + this.comparisonLookupTable = new Object2IntLinkedOpenCustomHashMap<>(CACHE_SIZE, 0.5F, new ShapeComparison.ShapeComparisonStrategy()); + this.comparisonLookupTable.defaultReturnValue(ENTRY_ABSENT); + } + + public static boolean isFullShape(VoxelShape selfShape) { + return selfShape == Shapes.block(); + } + + public static boolean isEmptyShape(VoxelShape voxelShape) { + return voxelShape == Shapes.empty() || voxelShape.isEmpty(); + } + + + public boolean lookup(VoxelShape self, VoxelShape other) { + ShapeComparison comparison = this.cachedComparisonObject; + comparison.self = self; + comparison.other = other; + + // Entries at the cache are promoted to the top of the table when accessed + // The entries at the bottom of the table are removed when it gets too large + return switch (this.comparisonLookupTable.getAndMoveToFirst(comparison)) { + case ENTRY_FALSE -> false; + case ENTRY_TRUE -> true; + default -> this.calculate(comparison); + }; + } + + private boolean calculate(ShapeComparison comparison) { + boolean result = Shapes.joinIsNotEmpty(comparison.self, comparison.other, BooleanOp.ONLY_FIRST); + + // Remove entries while the table is too large + while (this.comparisonLookupTable.size() >= CACHE_SIZE) { + this.comparisonLookupTable.removeLastInt(); + } + + this.comparisonLookupTable.putAndMoveToFirst(comparison.copy(), (result ? ENTRY_TRUE : ENTRY_FALSE)); + + return result; + } + + private static final class ShapeComparison { + private VoxelShape self, other; + + private ShapeComparison() { + + } + + private ShapeComparison(VoxelShape self, VoxelShape other) { + this.self = self; + this.other = other; + } + + public static class ShapeComparisonStrategy implements Hash.Strategy { + @Override + public int hashCode(ShapeComparison value) { + int result = System.identityHashCode(value.self); + result = 31 * result + System.identityHashCode(value.other); + + return result; + } + + @Override + public boolean equals(ShapeComparison a, ShapeComparison b) { + if (a == b) { + return true; + } + + if (a == null || b == null) { + return false; + } + + return a.self == b.self && a.other == b.other; + } + } + + public ShapeComparison copy() { + return new ShapeComparison(this.self, this.other); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractBlockRenderContext.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractBlockRenderContext.java index 84e20f5f0b..c97355f263 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractBlockRenderContext.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractBlockRenderContext.java @@ -4,7 +4,7 @@ import net.caffeinemc.mods.sodium.client.model.light.LightPipeline; import net.caffeinemc.mods.sodium.client.model.light.LightPipelineProvider; import net.caffeinemc.mods.sodium.client.model.light.data.QuadLightData; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockOcclusionCache; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.ShapeComparisonCache; import net.caffeinemc.mods.sodium.client.render.frapi.SodiumRenderer; import net.caffeinemc.mods.sodium.client.render.frapi.helper.ColorHelper; import net.caffeinemc.mods.sodium.client.render.frapi.mesh.EncodingFormat; @@ -12,6 +12,7 @@ import net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess; import net.caffeinemc.mods.sodium.client.services.PlatformModelAccess; import net.caffeinemc.mods.sodium.client.services.SodiumModelData; +import net.caffeinemc.mods.sodium.client.util.DirectionUtil; import net.caffeinemc.mods.sodium.client.world.LevelSlice; import net.fabricmc.fabric.api.renderer.v1.material.BlendMode; import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; @@ -27,9 +28,10 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.util.RandomSource; -import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -110,7 +112,8 @@ public void emitDirectly() { */ protected SodiumModelData modelData; - private final BlockOcclusionCache occlusionCache = new BlockOcclusionCache(); + private final ShapeComparisonCache occlusionCache = new ShapeComparisonCache(); + private final BlockPos.MutableBlockPos cachedPositionObject = new BlockPos.MutableBlockPos(); private boolean enableCulling = true; // Cull cache (as it's checked per-quad instead of once per side like in vanilla) private int cullCompletionFlags; @@ -138,6 +141,54 @@ public QuadEmitter getEmitter() { return this.editorQuad; } + /** + * @param facing The facing direction of the side to check + * @return True if the block side facing {@param facing} is not occluded, otherwise false + */ + public boolean shouldDrawSide(Direction facing) { + BlockPos.MutableBlockPos neighborPos = this.cachedPositionObject; + neighborPos.setWithOffset(this.pos, facing); + + // The block state of the neighbor + BlockState neighborBlockState = this.level.getBlockState(neighborPos); + + // The cull shape of the neighbor between the block being rendered and it + VoxelShape neighborShape = neighborBlockState.getFaceOcclusionShape(DirectionUtil.getOpposite(facing)); + + // Minecraft enforces that if the neighbor has a full-block occlusion shape, the face is always hidden + if (ShapeComparisonCache.isFullShape(neighborShape)) { + return false; + } + + // Blocks can define special behavior to control whether their faces are rendered. + // This is mostly used by transparent blocks (Leaves, Glass, etc.) to not render interior faces between blocks + // of the same type. + if (this.state.skipRendering(neighborBlockState, facing)) { + return false; + } else if (PlatformBlockAccess.getInstance() + .shouldSkipRender(this.level, this.state, neighborBlockState, this.pos, neighborPos, facing)) { + return false; + } + + // After any custom behavior has been handled, check if the neighbor block is transparent or has an empty + // cull shape. These blocks cannot hide any geometry. + if (ShapeComparisonCache.isEmptyShape(neighborShape) || !neighborBlockState.canOcclude()) { + return true; + } + + // The cull shape between of the block being rendered, between it and the neighboring block + VoxelShape selfShape = this.state.getFaceOcclusionShape(facing); + + // If the block being rendered has an empty cull shape, there will be no intersection with the neighboring + // block's cull shape, so no geometry can be hidden. + if (ShapeComparisonCache.isEmptyShape(selfShape)) { + return true; + } + + // No other simplifications apply, so we need to perform a full shape comparison, which is very slow + return this.occlusionCache.lookup(selfShape, neighborShape); + } + public boolean isFaceCulled(@Nullable Direction face) { if (face == null || !this.enableCulling) { return false; @@ -148,7 +199,7 @@ public boolean isFaceCulled(@Nullable Direction face) { if ((this.cullCompletionFlags & mask) == 0) { this.cullCompletionFlags |= mask; - if (this.occlusionCache.shouldDrawSide(this.state, this.level, this.pos, face)) { + if (this.shouldDrawSide(face)) { this.cullResultFlags |= mask; return false; } else {