From 8ddfd0ca634de58800b42be8d2b112590a6676f8 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 4 Dec 2024 00:37:08 +0100 Subject: [PATCH 1/2] fix fluid occlusion by using voxel shape comparison with the waterlogged block using BlockOcclusionCache --- .../compile/pipeline/BlockOcclusionCache.java | 76 ++++++++++++++++++- .../pipeline/DefaultFluidRenderer.java | 53 +++++-------- 2 files changed, 94 insertions(+), 35 deletions(-) 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 index 5edb14622c..f9851a2b2f 100644 --- 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 @@ -8,6 +8,7 @@ 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; @@ -31,9 +32,9 @@ public BlockOcclusionCache() { /** * @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 + * @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) { @@ -88,6 +89,75 @@ 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 (!selfShape.isEmpty()) { + // a full self-shape occludes everything + if (selfShape == Shapes.block() && 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 (otherShape.isEmpty()) { + 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 otherShape != Shapes.block() || !fluidShapeIsBlock; + } + private boolean lookup(VoxelShape self, VoxelShape other) { ShapeComparison comparison = this.cachedComparisonObject; comparison.self = self; 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 7f58d733a0..cf89774909 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 @@ -4,7 +4,6 @@ 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; -import net.caffeinemc.mods.sodium.client.model.color.ColorProviderRegistry; import net.caffeinemc.mods.sodium.client.model.light.LightMode; import net.caffeinemc.mods.sodium.client.model.light.LightPipeline; import net.caffeinemc.mods.sodium.client.model.light.LightPipelineProvider; @@ -28,8 +27,6 @@ import net.minecraft.tags.FluidTags; import net.minecraft.util.Mth; import net.minecraft.world.level.BlockAndTintGetter; -import net.minecraft.world.level.block.LiquidBlock; -import net.minecraft.world.level.block.SupportType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.material.FluidState; @@ -49,6 +46,8 @@ public class DefaultFluidRenderer { private final MutableFloat scratchHeight = new MutableFloat(0); private final MutableInt scratchSamples = new MutableInt(); + private final BlockOcclusionCache occlusionCache = new BlockOcclusionCache(); + private final ModelQuadViewMutable quad = new ModelQuad(); private final LightPipelineProvider lighters; @@ -65,21 +64,10 @@ public DefaultFluidRenderer(LightPipelineProvider lighters) { this.lighters = lighters; } - private boolean isFluidOccluded(BlockAndTintGetter world, int x, int y, int z, Direction dir, BlockState blockState, FluidState fluid) { - //Test own block state first, this prevents waterlogged blocks from having hidden internal geometry - // which can result in z-fighting - var pos = this.scratchPos.set(x, y, z); - if (blockState.canOcclude() && blockState.isFaceSturdy(world, pos, dir, SupportType.FULL)) { - return true; - } - - //Test neighboring block state - var adjPos = this.scratchPos.set(x + dir.getStepX(), y + dir.getStepY(), z + dir.getStepZ()); - BlockState adjBlockState = world.getBlockState(adjPos); - if (PlatformBlockAccess.getInstance().shouldOccludeFluid(dir.getOpposite(), adjBlockState, fluid)) { - return true; - } - return adjBlockState.canOcclude() && dir != Direction.UP && adjBlockState.isFaceSturdy(world, adjPos, dir.getOpposite(), SupportType.FULL); + 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()); } private boolean isSideExposed(BlockAndTintGetter world, int x, int y, int z, Direction dir, float height) { @@ -109,15 +97,16 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat Fluid fluid = fluidState.getType(); - boolean sfUp = this.isFluidOccluded(level, posX, posY, posZ, Direction.UP, blockState, fluidState); - boolean sfDown = this.isFluidOccluded(level, posX, posY, posZ, Direction.DOWN, blockState, fluidState) || + 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 sfNorth = this.isFluidOccluded(level, posX, posY, posZ, Direction.NORTH, blockState, fluidState); - boolean sfSouth = this.isFluidOccluded(level, posX, posY, posZ, Direction.SOUTH, blockState, fluidState); - boolean sfWest = this.isFluidOccluded(level, posX, posY, posZ, Direction.WEST, blockState, fluidState); - boolean sfEast = this.isFluidOccluded(level, posX, posY, posZ, Direction.EAST, blockState, fluidState); + 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); - if (sfUp && sfDown && sfEast && sfWest && sfNorth && sfSouth) { + // stop rendering if all faces of the fluid are occluded + if (cullUp && cullDown && cullEast && cullWest && cullNorth && cullSouth) { return; } @@ -149,7 +138,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat .move(Direction.NORTH) .move(Direction.EAST)); } - float yOffset = sfDown ? 0.0F : EPSILON; + float yOffset = cullDown ? 0.0F : EPSILON; final ModelQuadViewMutable quad = this.quad; @@ -158,7 +147,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat quad.setFlags(0); - if (!sfUp && this.isSideExposed(level, posX, posY, posZ, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)))) { + if (!cullUp && this.isSideExposed(level, posX, posY, posZ, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)))) { northWestHeight -= EPSILON; southWestHeight -= EPSILON; southEastHeight -= EPSILON; @@ -243,7 +232,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) } } - if (!sfDown) { + if (!cullDown) { TextureAtlasSprite sprite = sprites[0]; float minU = sprite.getU0(); @@ -273,7 +262,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) switch (dir) { case NORTH -> { - if (sfNorth) { + if (cullNorth) { continue; } c1 = northWestHeight; @@ -284,7 +273,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = z1; } case SOUTH -> { - if (sfSouth) { + if (cullSouth) { continue; } c1 = southEastHeight; @@ -295,7 +284,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = z1; } case WEST -> { - if (sfWest) { + if (cullWest) { continue; } c1 = southWestHeight; @@ -306,7 +295,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = 0.0f; } case EAST -> { - if (sfEast) { + if (cullEast) { continue; } c1 = northEastHeight; From 4c7b2c98cf363a66e07942219381a1b96ec290de Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 4 Dec 2024 03:26:47 +0100 Subject: [PATCH 2/2] use existing utility methods when checking for empty and full shapes --- .../chunk/compile/pipeline/BlockOcclusionCache.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index f9851a2b2f..f157fac5b1 100644 --- 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 @@ -108,9 +108,9 @@ public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGett var selfShape = selfBlockState.getFaceOcclusionShape(facing); // only a non-empty self-shape can occlude anything - if (!selfShape.isEmpty()) { + if (!isEmptyShape(selfShape)) { // a full self-shape occludes everything - if (selfShape == Shapes.block() && fluidShapeIsBlock) { + if (isFullShape(selfShape) && fluidShapeIsBlock) { return false; } @@ -149,13 +149,13 @@ public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGett var otherShape = otherState.getFaceOcclusionShape(facing.getOpposite()); // If the other block has an empty cull shape, then it cannot hide any geometry - if (otherShape.isEmpty()) { + 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 otherShape != Shapes.block() || !fluidShapeIsBlock; + return !isFullShape(otherShape) || !fluidShapeIsBlock; } private boolean lookup(VoxelShape self, VoxelShape other) {