From 3f8254dbc8167eae324117e16fd1298c408250b3 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Dec 2024 00:10:53 +0100 Subject: [PATCH 01/29] fix same fluid type test --- .../chunk/compile/pipeline/BlockOcclusionCache.java | 9 +++++---- 1 file changed, 5 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 f157fac5b1..adfe8b9455 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 @@ -126,13 +126,14 @@ public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGett 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) { + // check for special fluid occlusion behavior + if (PlatformBlockAccess.getInstance().shouldOccludeFluid(facing.getOpposite(), otherState, fluid)) { return false; } - // check for special fluid occlusion behavior - if (PlatformBlockAccess.getInstance().shouldOccludeFluid(facing.getOpposite(), otherState, fluid)) { + // 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; } From cee72df1f3f7e449b8e929e8df702e484c48be63 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Dec 2024 00:11:53 +0100 Subject: [PATCH 02/29] remove unnecessary direction parameter from fluidHeight method --- .../compile/pipeline/DefaultFluidRenderer.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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..d35ec466bb 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 @@ -112,7 +112,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat boolean isWater = fluidState.is(FluidTags.WATER); - float fluidHeight = this.fluidHeight(level, fluid, blockPos, Direction.UP); + float fluidHeight = this.fluidHeight(level, fluid, blockPos); float northWestHeight, southWestHeight, southEastHeight, northEastHeight; if (fluidHeight >= 1.0f) { northWestHeight = 1.0f; @@ -121,10 +121,10 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat 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); + float heightNorth = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.NORTH)); + float heightSouth = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.SOUTH)); + float heightEast = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.EAST)); + float heightWest = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.WEST)); northWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightWest, scratchPos.set(blockPos) .move(Direction.NORTH) .move(Direction.WEST)); @@ -445,7 +445,7 @@ private float fluidCornerHeight(BlockAndTintGetter world, Fluid fluid, float flu } if (fluidHeightY > 0.0f || fluidHeightX > 0.0f) { - float height = this.fluidHeight(world, fluid, blockPos, Direction.UP); + float height = this.fluidHeight(world, fluid, blockPos); if (height >= 1.0f) { return 1.0f; @@ -475,7 +475,7 @@ private void modifyHeight(MutableFloat totalHeight, MutableInt samples, float ta } } - private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockPos, Direction direction) { + private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockPos) { BlockState blockState = world.getBlockState(blockPos); FluidState fluidState = blockState.getFluidState(); From dd17ebdf318b95f76d9008a07da5451e8778a2a2 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Dec 2024 02:29:14 +0100 Subject: [PATCH 03/29] fix block occlusion cache java doc --- .../compile/pipeline/BlockOcclusionCache.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 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 adfe8b9455..f169cdaf7c 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 @@ -35,7 +35,7 @@ public BlockOcclusionCache() { * @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 + * @return True if the block side facing {@param facing} is not occluded, otherwise false */ public boolean shouldDrawSide(BlockState selfBlockState, BlockGetter view, BlockPos selfPos, Direction facing) { BlockPos.MutableBlockPos neighborPos = this.cachedPositionObject; @@ -92,13 +92,13 @@ private static boolean isEmptyShape(VoxelShape voxelShape) { /** * 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 + * @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 facing} 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(); From 2b703abd110e481ef891931a64e77066a1d9d47b Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Dec 2024 02:30:02 +0100 Subject: [PATCH 04/29] move accurate fluid culling to BlockOcclusionCache as well --- .../compile/pipeline/BlockOcclusionCache.java | 37 ++++++ .../pipeline/DefaultFluidRenderer.java | 117 ++++++++---------- 2 files changed, 88 insertions(+), 66 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 f169cdaf7c..59f8919c67 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 @@ -159,6 +159,43 @@ public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGett return !isFullShape(otherShape) || !fluidShapeIsBlock; } + // TODO: do we want all of these shapes to end up in the cache? + /** + * Checks if a face of a fluid block with a specific height should be rendered based on the neighboring block state. + * + * @param neighborBlockState The state 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 shouldDrawAccurateFluidSide(BlockState neighborBlockState, Direction facing, float height) { + // 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; + } + + VoxelShape neighborShape = neighborBlockState.getFaceOcclusionShape(facing); + + // empty neighbor occlusion shape can't occlude anything + if (isEmptyShape(neighborShape)) { + return true; + } + + // full neighbor occlusion shape occludes everything + if (isFullShape(neighborShape)) { + return false; + } + + VoxelShape fluidShape = Shapes.box(0.0D, 0.0D, 0.0D, 1.0D, height, 1.0D); + + return this.lookup(fluidShape, neighborShape); + } + 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 d35ec466bb..d552800cd3 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 @@ -32,7 +32,6 @@ 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; @@ -72,22 +71,64 @@ private boolean isFullBlockFluidOccluded(BlockAndTintGetter world, BlockPos pos, 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); + return this.occlusionCache.shouldDrawAccurateFluidSide(world.getBlockState(pos), dir, height); + } + + 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 (blockState.canOcclude()) { - VoxelShape shape = blockState.getOcclusionShape(); + if (fluidHeightY > 0.0f || fluidHeightX > 0.0f) { + float height = this.fluidHeight(world, fluid, blockPos); - // Hoist these checks to avoid allocating the shape below - if (shape.isEmpty()) { - return true; + if (height >= 1.0f) { + return 1.0f; } - VoxelShape threshold = Shapes.box(0.0D, 0.0D, 0.0D, 1.0D, height, 1.0D); + 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; + } - return !Shapes.blockOccudes(threshold, shape, dir); + private static final float DISCARD_SAMPLE = -1.0f; + + private void modifyHeight(MutableFloat totalHeight, MutableInt samples, float sample) { + if (sample >= 0.8f) { + totalHeight.add(sample * 10.0f); + samples.add(10); + } else if (sample >= 0.0f) { + totalHeight.add(sample); + samples.increment(); + } + + // else -> sample == DISCARD_SAMPLE + } + + private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockPos) { + 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(); + } } - return true; + return blockState.isSolid() ? DISCARD_SAMPLE : 0.0f; } public void render(LevelSlice level, BlockState blockState, FluidState fluidState, BlockPos blockPos, BlockPos offset, TranslucentGeometryCollector collector, ChunkModelBuilder meshBuilder, Material material, ColorProvider colorProvider, TextureAtlasSprite[] sprites) { @@ -352,7 +393,6 @@ && isAlignedEquals(southEastHeight, southWestHeight) if (!isOverlay) { this.writeQuad(meshBuilder, collector, material, offset, quad, facing.getOpposite(), true); } - } } } @@ -438,59 +478,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); - - 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) { - 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; - } } From 8a86f7523216ebdea5f958f18dbc6ba739a12965 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Dec 2024 03:01:03 +0100 Subject: [PATCH 05/29] consistent scratch pos usage --- .../pipeline/DefaultFluidRenderer.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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 d552800cd3..dfbf81516d 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 @@ -161,21 +161,20 @@ 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)); - float heightSouth = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.SOUTH)); - float heightEast = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.EAST)); - float heightWest = this.fluidHeight(level, fluid, scratchPos.setWithOffset(blockPos, Direction.WEST)); - northWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightWest, scratchPos.set(blockPos) + float heightNorth = this.fluidHeight(level, fluid, this.scratchPos.setWithOffset(blockPos, Direction.NORTH)); + float heightSouth = this.fluidHeight(level, fluid, this.scratchPos.setWithOffset(blockPos, Direction.SOUTH)); + float heightEast = this.fluidHeight(level, fluid, this.scratchPos.setWithOffset(blockPos, Direction.EAST)); + float heightWest = this.fluidHeight(level, fluid, this.scratchPos.setWithOffset(blockPos, Direction.WEST)); + northWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightWest, this.scratchPos.set(blockPos) .move(Direction.NORTH) .move(Direction.WEST)); - southWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightWest, scratchPos.set(blockPos) + southWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightWest, this.scratchPos.set(blockPos) .move(Direction.SOUTH) .move(Direction.WEST)); - southEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightEast, scratchPos.set(blockPos) + southEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightEast, this.scratchPos.set(blockPos) .move(Direction.SOUTH) .move(Direction.EAST)); - northEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightEast, scratchPos.set(blockPos) + northEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightEast, this.scratchPos.set(blockPos) .move(Direction.NORTH) .move(Direction.EAST)); } @@ -417,7 +416,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 From eb1e0ad14ec821f92b9a2bec7fad133958a083ed Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Dec 2024 03:38:30 +0100 Subject: [PATCH 06/29] refactor block pos handling, use correct facing for occlusion shape isSideExposed --- .../compile/pipeline/BlockOcclusionCache.java | 8 +++--- .../pipeline/DefaultFluidRenderer.java | 26 +++++++++---------- 2 files changed, 16 insertions(+), 18 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 59f8919c67..c34b93a767 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 @@ -122,9 +122,7 @@ public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGett } // 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); + BlockState otherState = view.getBlockState(this.cachedPositionObject.setWithOffset(selfPos, facing)); // check for special fluid occlusion behavior if (PlatformBlockAccess.getInstance().shouldOccludeFluid(facing.getOpposite(), otherState, fluid)) { @@ -147,7 +145,7 @@ public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGett return true; } - var otherShape = otherState.getFaceOcclusionShape(facing.getOpposite()); + var otherShape = otherState.getFaceOcclusionShape(DirectionUtil.getOpposite(facing)); // If the other block has an empty cull shape, then it cannot hide any geometry if (isEmptyShape(otherShape)) { @@ -179,7 +177,7 @@ public boolean shouldDrawAccurateFluidSide(BlockState neighborBlockState, Direct return true; } - VoxelShape neighborShape = neighborBlockState.getFaceOcclusionShape(facing); + VoxelShape neighborShape = neighborBlockState.getFaceOcclusionShape(DirectionUtil.getOpposite(facing)); // empty neighbor occlusion shape can't occlude anything if (isEmptyShape(neighborShape)) { 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 dfbf81516d..83330e9de6 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 @@ -69,11 +69,14 @@ private boolean isFullBlockFluidOccluded(BlockAndTintGetter world, BlockPos pos, 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) { - BlockPos pos = this.scratchPos.set(x + dir.getStepX(), y + dir.getStepY(), z + dir.getStepZ()); + private boolean isSideExposed(BlockAndTintGetter world, BlockPos pos, Direction dir, float height) { return this.occlusionCache.shouldDrawAccurateFluidSide(world.getBlockState(pos), dir, height); } + private boolean isSideExposedOffset(BlockAndTintGetter world, BlockPos originPos, Direction dir, float height) { + return this.isSideExposed(world, this.scratchPos.setWithOffset(originPos, dir), dir, height); + } + 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; @@ -132,15 +135,11 @@ private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockP } 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(); - 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); + !this.isSideExposedOffset(level, blockPos, 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); @@ -187,7 +186,7 @@ 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)))) { + if (!cullUp && this.isSideExposedOffset(level, blockPos, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)))) { northWestHeight -= EPSILON; southWestHeight -= EPSILON; southEastHeight -= EPSILON; @@ -266,7 +265,7 @@ && 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))) { + if (fluidState.shouldRenderBackwardUpFace(level, this.scratchPos.setWithOffset(blockPos, Direction.UP))) { this.writeQuad(meshBuilder, collector, material, offset, quad, aligned ? ModelQuadFacing.NEG_Y : ModelQuadFacing.UNASSIGNED, true); } @@ -350,10 +349,11 @@ && 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(); + this.scratchPos.setWithOffset(blockPos, dir); + if (this.isSideExposed(level, this.scratchPos, dir, Math.max(c1, c2))) { + int adjX = this.scratchPos.getX(); + int adjY = this.scratchPos.getY(); + int adjZ = this.scratchPos.getZ(); TextureAtlasSprite sprite = sprites[1]; From 446df98093e31f34a8e0f53ed0a345e71ee17945 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Dec 2024 05:57:34 +0100 Subject: [PATCH 07/29] break out self occlusion into own method --- .../compile/pipeline/BlockOcclusionCache.java | 51 +++++++++++-------- .../pipeline/DefaultFluidRenderer.java | 3 +- 2 files changed, 33 insertions(+), 21 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 c34b93a767..dbb8cc4200 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 @@ -101,24 +101,8 @@ private static boolean isEmptyShape(VoxelShape voxelShape) { * @return True if the fluid side facing {@param facing} 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; - } - } + if (isFluidSelfOccluded(selfBlockState, facing, fluidShape)) { + return false; } // perform occlusion against the neighboring block @@ -154,7 +138,29 @@ public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGett // 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; + return !isFullShape(otherShape) || !isFullShape(fluidShape); + } + + public boolean isFluidSelfOccluded(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 (!isEmptyShape(selfShape)) { + // a full self-shape occludes everything + if (isFullShape(selfShape) && isFullShape(fluidShape)) { + return true; + } + + // perform occlusion of the fluid by the block it's contained in + if (!this.lookup(fluidShape, selfShape)) { + return true; + } + } + } + + return false; } // TODO: do we want all of these shapes to end up in the cache? @@ -189,7 +195,12 @@ public boolean shouldDrawAccurateFluidSide(BlockState neighborBlockState, Direct return false; } - VoxelShape fluidShape = Shapes.box(0.0D, 0.0D, 0.0D, 1.0D, height, 1.0D); + 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.lookup(fluidShape, neighborShape); } 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 83330e9de6..da94353275 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 @@ -186,7 +186,8 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat quad.setFlags(0); - if (!cullUp && this.isSideExposedOffset(level, blockPos, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)))) { + float totalMinHeight = Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)); + if (!cullUp && this.isSideExposedOffset(level, blockPos, Direction.UP, totalMinHeight)) { northWestHeight -= EPSILON; southWestHeight -= EPSILON; southEastHeight -= EPSILON; From ea0655b0c4cc252531be5aa95ae3596e52b301af Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 8 Dec 2024 00:20:02 +0100 Subject: [PATCH 08/29] fix fluid occlusion for non-full height blocks --- .../render/chunk/compile/pipeline/BlockOcclusionCache.java | 5 +++++ 1 file changed, 5 insertions(+) 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 dbb8cc4200..d3013405af 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 @@ -183,6 +183,11 @@ public boolean shouldDrawAccurateFluidSide(BlockState neighborBlockState, Direct 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 From e9c80875eacebcfeed8ee9c3d363aa4d4ae01b96 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Dec 2024 05:59:54 +0100 Subject: [PATCH 09/29] change fluid behavior around opaque waterlogged blocks to not slant the fluids towards each other unless they're actually connected --- .../pipeline/DefaultFluidRenderer.java | 105 ++++++++++++------ 1 file changed, 71 insertions(+), 34 deletions(-) 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 da94353275..d8b19b6054 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 @@ -63,10 +63,18 @@ public DefaultFluidRenderer(LightPipelineProvider lighters) { this.lighters = lighters; } + private boolean isFullBlockFluidSelfOccluded(BlockState blockState, Direction dir) { + return this.occlusionCache.isFluidSelfOccluded(blockState, dir, Shapes.block()); + } + + private boolean isFullBlockFluidNeighborOccluded(BlockAndTintGetter world, BlockPos pos, Direction dir, BlockState blockState, FluidState fluid) { + return !this.occlusionCache.shouldDrawFullBlockFluidSide(blockState, world, pos, dir, fluid, Shapes.block()); + } + 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()); + return this.isFullBlockFluidSelfOccluded(blockState, dir) || this.isFullBlockFluidNeighborOccluded(world, pos, dir, blockState, fluid); } private boolean isSideExposed(BlockAndTintGetter world, BlockPos pos, Direction dir, float height) { @@ -77,24 +85,34 @@ private boolean isSideExposedOffset(BlockAndTintGetter world, BlockPos originPos return this.isSideExposed(world, this.scratchPos.setWithOffset(originPos, dir), dir, height); } - private float fluidCornerHeight(BlockAndTintGetter world, Fluid fluid, float fluidHeight, float fluidHeightX, float fluidHeightY, BlockPos blockPos) { - if (fluidHeightY >= 1.0f || fluidHeightX >= 1.0f) { + private float fluidCornerHeight(BlockAndTintGetter world, Fluid fluid, float fluidHeight, float fluidHeightA, float fluidHeightB, BlockPos origin, Direction dirA, Direction dirB, boolean aExposed, boolean bExposed) { + if (fluidHeightB >= 1.0f || fluidHeightA >= 1.0f) { return 1.0f; } - if (fluidHeightY > 0.0f || fluidHeightX > 0.0f) { - float height = this.fluidHeight(world, fluid, blockPos); + if (fluidHeightB > 0.0f || fluidHeightA > 0.0f) { + // check that there's an accessible path to the diagonal + var aNeighbor = this.scratchPos.setWithOffset(origin, dirA); + var exposedAToAB = !this.isFullBlockFluidSelfOccluded(world.getBlockState(aNeighbor), dirB) && this.isSideExposedOffset(world, aNeighbor, dirB, 1.0f); + var bNeighbor = this.scratchPos.setWithOffset(origin, dirB); + var exposedBToAB = !this.isFullBlockFluidSelfOccluded(world.getBlockState(bNeighbor), dirA) && this.isSideExposedOffset(world, bNeighbor, dirA, 1.0f); - if (height >= 1.0f) { - return 1.0f; - } + if (aExposed && exposedAToAB || bExposed && exposedBToAB) { + // diagonal block's fluid height + var abNeighbor = this.scratchPos.set(origin).move(dirA).move(dirB); + float height = this.fluidHeight(world, fluid, abNeighbor); - this.modifyHeight(this.scratchHeight, this.scratchSamples, height); + 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); + this.modifyHeight(this.scratchHeight, this.scratchSamples, fluidHeightB); + this.modifyHeight(this.scratchHeight, this.scratchSamples, fluidHeightA); float result = this.scratchHeight.floatValue() / this.scratchSamples.intValue(); this.scratchHeight.setValue(0); @@ -131,7 +149,15 @@ private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockP } } - return blockState.isSolid() ? DISCARD_SAMPLE : 0.0f; + return 0.0f; + } + + private float fluidHeightDiscardOccluded(BlockAndTintGetter world, Fluid fluid, BlockPos origin, Direction offset, boolean keepSample) { + if (!keepSample) { + return DISCARD_SAMPLE; + } + + return this.fluidHeight(world, fluid, this.scratchPos.setWithOffset(origin, offset)); } public void render(LevelSlice level, BlockState blockState, FluidState fluidState, BlockPos blockPos, BlockPos offset, TranslucentGeometryCollector collector, ChunkModelBuilder meshBuilder, Material material, ColorProvider colorProvider, TextureAtlasSprite[] sprites) { @@ -140,10 +166,15 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat boolean cullUp = this.isFullBlockFluidOccluded(level, blockPos, Direction.UP, blockState, fluidState); boolean cullDown = this.isFullBlockFluidOccluded(level, blockPos, Direction.DOWN, blockState, fluidState) || !this.isSideExposedOffset(level, blockPos, 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 northSelfOcclusion = this.isFullBlockFluidSelfOccluded(blockState, Direction.NORTH); + boolean southSelfOcclusion = this.isFullBlockFluidSelfOccluded(blockState, Direction.SOUTH); + boolean westSelfOcclusion = this.isFullBlockFluidSelfOccluded(blockState, Direction.WEST); + boolean eastSelfOcclusion = this.isFullBlockFluidSelfOccluded(blockState, Direction.EAST); + boolean cullNorth = northSelfOcclusion || this.isFullBlockFluidNeighborOccluded(level, blockPos, Direction.NORTH, blockState, fluidState); + boolean cullSouth = southSelfOcclusion || this.isFullBlockFluidNeighborOccluded(level, blockPos, Direction.SOUTH, blockState, fluidState); + boolean cullWest = westSelfOcclusion || this.isFullBlockFluidNeighborOccluded(level, blockPos, Direction.WEST, blockState, fluidState); + boolean cullEast = eastSelfOcclusion || this.isFullBlockFluidNeighborOccluded(level, blockPos, Direction.EAST, blockState, fluidState); // stop rendering if all faces of the fluid are occluded if (cullUp && cullDown && cullEast && cullWest && cullNorth && cullSouth) { @@ -153,29 +184,31 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat boolean isWater = fluidState.is(FluidTags.WATER); float fluidHeight = this.fluidHeight(level, fluid, blockPos); + boolean fullFluidBlock = fluidHeight >= 1.0f; float northWestHeight, southWestHeight, southEastHeight, northEastHeight; - if (fluidHeight >= 1.0f) { + boolean northExposed = !northSelfOcclusion; + boolean southExposed = !southSelfOcclusion; + boolean westExposed = !westSelfOcclusion; + boolean eastExposed = !eastSelfOcclusion; + if (fullFluidBlock) { northWestHeight = 1.0f; southWestHeight = 1.0f; southEastHeight = 1.0f; northEastHeight = 1.0f; } else { - float heightNorth = this.fluidHeight(level, fluid, this.scratchPos.setWithOffset(blockPos, Direction.NORTH)); - float heightSouth = this.fluidHeight(level, fluid, this.scratchPos.setWithOffset(blockPos, Direction.SOUTH)); - float heightEast = this.fluidHeight(level, fluid, this.scratchPos.setWithOffset(blockPos, Direction.EAST)); - float heightWest = this.fluidHeight(level, fluid, this.scratchPos.setWithOffset(blockPos, Direction.WEST)); - northWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightWest, this.scratchPos.set(blockPos) - .move(Direction.NORTH) - .move(Direction.WEST)); - southWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightWest, this.scratchPos.set(blockPos) - .move(Direction.SOUTH) - .move(Direction.WEST)); - southEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightEast, this.scratchPos.set(blockPos) - .move(Direction.SOUTH) - .move(Direction.EAST)); - northEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightEast, this.scratchPos.set(blockPos) - .move(Direction.NORTH) - .move(Direction.EAST)); + northExposed = northExposed && this.isSideExposedOffset(level, blockPos, Direction.NORTH, 1.0f); + southExposed = southExposed && this.isSideExposedOffset(level, blockPos, Direction.SOUTH, 1.0f); + westExposed = westExposed && this.isSideExposedOffset(level, blockPos, Direction.WEST, 1.0f); + eastExposed = eastExposed && this.isSideExposedOffset(level, blockPos, Direction.EAST, 1.0f); + + float heightNorth = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.NORTH, northExposed); + float heightSouth = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.SOUTH, southExposed); + float heightEast = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.EAST, eastExposed); + float heightWest = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.WEST, westExposed); + northWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightWest, blockPos, Direction.NORTH, Direction.WEST, northExposed, westExposed); + southWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightWest, blockPos, Direction.SOUTH, Direction.WEST, southExposed, westExposed); + southEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightEast, blockPos, Direction.SOUTH, Direction.EAST, southExposed, eastExposed); + northEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightEast, blockPos, Direction.NORTH, Direction.EAST, northExposed, eastExposed); } float yOffset = cullDown ? 0.0F : EPSILON; @@ -350,8 +383,12 @@ && isAlignedEquals(southEastHeight, southWestHeight) } } + var sideFluidHeight = Math.max(c1, c2); this.scratchPos.setWithOffset(blockPos, dir); - if (this.isSideExposed(level, this.scratchPos, dir, Math.max(c1, c2))) { + + // allow skipping the side exposed check if it's not a full block, which would have skipped the early exposure check, and the fluid height is 1 meaning the side exposure was already checked earlier + // (!fullFluidBlock && sideFluidHeight == 1.0f) || + if (this.isSideExposed(level, this.scratchPos, dir, sideFluidHeight)) { int adjX = this.scratchPos.getX(); int adjY = this.scratchPos.getY(); int adjZ = this.scratchPos.getZ(); From 17f2c3c91d39aa6a0ea6f9c6e9255e6dd666b2ba Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 10 Dec 2024 04:31:22 +0100 Subject: [PATCH 10/29] fix fluid mesh continuity and refactor corner fluid height calculation, move occlusion test methods out of BlockOcclusionCache and rename to ShapeComparisonCache, --- .../compile/pipeline/BlockOcclusionCache.java | 279 ----------------- .../pipeline/DefaultFluidRenderer.java | 288 +++++++++++++----- .../pipeline/ShapeComparisonCache.java | 106 +++++++ .../render/AbstractBlockRenderContext.java | 59 +++- 4 files changed, 373 insertions(+), 359 deletions(-) delete mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockOcclusionCache.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/ShapeComparisonCache.java 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 d3013405af..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockOcclusionCache.java +++ /dev/null @@ -1,279 +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 facing} 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 facing} is not occluded, otherwise false - */ - public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGetter view, BlockPos selfPos, Direction facing, FluidState fluid, VoxelShape fluidShape) { - if (isFluidSelfOccluded(selfBlockState, facing, fluidShape)) { - return false; - } - - // perform occlusion against the neighboring block - BlockState otherState = view.getBlockState(this.cachedPositionObject.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 (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) || !isFullShape(fluidShape); - } - - public boolean isFluidSelfOccluded(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 (!isEmptyShape(selfShape)) { - // a full self-shape occludes everything - if (isFullShape(selfShape) && isFullShape(fluidShape)) { - return true; - } - - // perform occlusion of the fluid by the block it's contained in - if (!this.lookup(fluidShape, selfShape)) { - return true; - } - } - } - - return false; - } - - // TODO: do we want all of these shapes to end up in the cache? - /** - * Checks if a face of a fluid block with a specific height should be rendered based on the neighboring block state. - * - * @param neighborBlockState The state 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 shouldDrawAccurateFluidSide(BlockState neighborBlockState, Direction facing, float height) { - // 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 (isEmptyShape(neighborShape)) { - return true; - } - - // full neighbor occlusion shape occludes everything - if (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.lookup(fluidShape, neighborShape); - } - - 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 d8b19b6054..3c46b9c37e 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 @@ -27,13 +27,13 @@ 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 org.apache.commons.lang3.mutable.MutableFloat; -import org.apache.commons.lang3.mutable.MutableInt; +import net.minecraft.world.phys.shapes.VoxelShape; public class DefaultFluidRenderer { // TODO: allow this to be changed by vertex format, WARNING: make sure TranslucentGeometryCollector knows about EPSILON @@ -42,10 +42,11 @@ public class DefaultFluidRenderer { private static final float ALIGNED_EQUALS_EPSILON = 0.011f; 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 BlockOcclusionCache occlusionCache = new BlockOcclusionCache(); + private final ShapeComparisonCache occlusionCache = new ShapeComparisonCache(); private final ModelQuadViewMutable quad = new ModelQuad(); @@ -63,12 +64,140 @@ public DefaultFluidRenderer(LightPipelineProvider lighters) { this.lighters = lighters; } + /** + * 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 facing} is not occluded, otherwise false + */ + public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGetter view, BlockPos selfPos, Direction facing, FluidState fluid, VoxelShape fluidShape) { + if (isFluidSelfOccluded(selfBlockState, facing, fluidShape)) { + return false; + } + + // 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) || !ShapeComparisonCache.isFullShape(fluidShape); + } + + /** + * Checks if a face of the fluid is 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 occluded by the block it's contained in, otherwise false + */ + public boolean isFluidSelfOccluded(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 true; + } + + // perform occlusion of the fluid by the block it's contained in + if (!this.occlusionCache.lookup(fluidShape, selfShape)) { + return true; + } + } + } + + return false; + } + + /** + * Checks if a face of a fluid block with a specific height should be rendered based on the neighboring block state. + * + * @param neighborBlockState The state 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 shouldDrawAccurateFluidSide(BlockState neighborBlockState, Direction facing, float height) { + // 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 isFullBlockFluidSelfOccluded(BlockState blockState, Direction dir) { - return this.occlusionCache.isFluidSelfOccluded(blockState, dir, Shapes.block()); + return this.isFluidSelfOccluded(blockState, dir, Shapes.block()); } private boolean isFullBlockFluidNeighborOccluded(BlockAndTintGetter world, BlockPos pos, Direction dir, BlockState blockState, FluidState fluid) { - return !this.occlusionCache.shouldDrawFullBlockFluidSide(blockState, world, pos, dir, fluid, Shapes.block()); + return !this.shouldDrawFullBlockFluidSide(blockState, world, pos, dir, fluid, Shapes.block()); } private boolean isFullBlockFluidOccluded(BlockAndTintGetter world, BlockPos pos, Direction dir, BlockState blockState, FluidState fluid) { @@ -78,86 +207,99 @@ private boolean isFullBlockFluidOccluded(BlockAndTintGetter world, BlockPos pos, } private boolean isSideExposed(BlockAndTintGetter world, BlockPos pos, Direction dir, float height) { - return this.occlusionCache.shouldDrawAccurateFluidSide(world.getBlockState(pos), dir, height); + return this.shouldDrawAccurateFluidSide(world.getBlockState(pos), dir, height); } private boolean isSideExposedOffset(BlockAndTintGetter world, BlockPos originPos, Direction dir, float height) { return this.isSideExposed(world, this.scratchPos.setWithOffset(originPos, dir), dir, height); } - private float fluidCornerHeight(BlockAndTintGetter world, Fluid fluid, float fluidHeight, float fluidHeightA, float fluidHeightB, BlockPos origin, Direction dirA, Direction dirB, boolean aExposed, boolean bExposed) { - if (fluidHeightB >= 1.0f || fluidHeightA >= 1.0f) { - return 1.0f; - } - - if (fluidHeightB > 0.0f || fluidHeightA > 0.0f) { - // check that there's an accessible path to the diagonal - var aNeighbor = this.scratchPos.setWithOffset(origin, dirA); - var exposedAToAB = !this.isFullBlockFluidSelfOccluded(world.getBlockState(aNeighbor), dirB) && this.isSideExposedOffset(world, aNeighbor, dirB, 1.0f); - var bNeighbor = this.scratchPos.setWithOffset(origin, dirB); - var exposedBToAB = !this.isFullBlockFluidSelfOccluded(world.getBlockState(bNeighbor), dirA) && this.isSideExposedOffset(world, bNeighbor, dirA, 1.0f); + private static final float DISCARD_SAMPLE = -1.0f; - if (aExposed && exposedAToAB || bExposed && exposedBToAB) { - // diagonal block's fluid height - var abNeighbor = this.scratchPos.set(origin).move(dirA).move(dirB); - float height = this.fluidHeight(world, fluid, abNeighbor); + private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockPos) { + BlockState blockState = world.getBlockState(blockPos); + FluidState fluidState = blockState.getFluidState(); - if (height >= 1.0f) { - return 1.0f; - } + if (fluid.isSame(fluidState.getType())) { + FluidState fluidStateUp = world.getFluidState(blockPos.above()); - this.modifyHeight(this.scratchHeight, this.scratchSamples, height); + if (fluid.isSame(fluidStateUp.getType())) { + return 1.0f; + } else { + return fluidState.getOwnHeight(); } } - this.modifyHeight(this.scratchHeight, this.scratchSamples, fluidHeight); - this.modifyHeight(this.scratchHeight, this.scratchSamples, fluidHeightB); - this.modifyHeight(this.scratchHeight, this.scratchSamples, fluidHeightA); - - float result = this.scratchHeight.floatValue() / this.scratchSamples.intValue(); - this.scratchHeight.setValue(0); - this.scratchSamples.setValue(0); - - return result; + return 0.0f; } - private static final float DISCARD_SAMPLE = -1.0f; + private float fluidHeightDiscardOccluded(BlockAndTintGetter world, Fluid fluid, BlockPos origin, Direction offset) { + return this.fluidHeight(world, fluid, this.scratchPos.setWithOffset(origin, offset)); + } - private void modifyHeight(MutableFloat totalHeight, MutableInt samples, float sample) { + private void addHeightSample(float sample) { if (sample >= 0.8f) { - totalHeight.add(sample * 10.0f); - samples.add(10); + this.scratchHeight += sample * 10.0f; + this.scratchSamples += 10; } else if (sample >= 0.0f) { - totalHeight.add(sample); - samples.increment(); + this.scratchHeight += sample; + this.scratchSamples++; } // else -> sample == DISCARD_SAMPLE } - private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockPos) { - BlockState blockState = world.getBlockState(blockPos); - FluidState fluidState = blockState.getFluidState(); + 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; + } - if (fluid.isSame(fluidState.getType())) { - FluidState fluidStateUp = world.getFluidState(blockPos.above()); + // "D" stands for diagonal + boolean exposedADB = false; - if (fluid.isSame(fluidStateUp.getType())) { - return 1.0f; - } else { - return fluidState.getOwnHeight(); + // 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.isFullBlockFluidSelfOccluded(world.getBlockState(aNeighbor), dirB) && + this.isSideExposedOffset(world, aNeighbor, dirB, 1.0f); + var bNeighbor = this.scratchPos.setWithOffset(origin, dirB); + var exposedBD = !this.isFullBlockFluidSelfOccluded(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.fluidHeight(world, fluid, abNeighbor); + + if (height >= 1.0f) { + return 1.0f; + } + + this.addHeightSample(height); } } - return 0.0f; - } + this.addHeightSample(fluidHeight); - private float fluidHeightDiscardOccluded(BlockAndTintGetter world, Fluid fluid, BlockPos origin, Direction offset, boolean keepSample) { - if (!keepSample) { - return DISCARD_SAMPLE; + // add samples for the sides if they're exposed or if there's a path through the diagonal to them + if (exposedB || exposedA && exposedADB) { + this.addHeightSample(fluidHeightB); + } + if (exposedA || exposedB && exposedADB) { + this.addHeightSample(fluidHeightA); } - return this.fluidHeight(world, fluid, this.scratchPos.setWithOffset(origin, offset)); + // 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) { @@ -186,29 +328,25 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat float fluidHeight = this.fluidHeight(level, fluid, blockPos); boolean fullFluidBlock = fluidHeight >= 1.0f; float northWestHeight, southWestHeight, southEastHeight, northEastHeight; - boolean northExposed = !northSelfOcclusion; - boolean southExposed = !southSelfOcclusion; - boolean westExposed = !westSelfOcclusion; - boolean eastExposed = !eastSelfOcclusion; if (fullFluidBlock) { northWestHeight = 1.0f; southWestHeight = 1.0f; southEastHeight = 1.0f; northEastHeight = 1.0f; } else { - northExposed = northExposed && this.isSideExposedOffset(level, blockPos, Direction.NORTH, 1.0f); - southExposed = southExposed && this.isSideExposedOffset(level, blockPos, Direction.SOUTH, 1.0f); - westExposed = westExposed && this.isSideExposedOffset(level, blockPos, Direction.WEST, 1.0f); - eastExposed = eastExposed && this.isSideExposedOffset(level, blockPos, Direction.EAST, 1.0f); - - float heightNorth = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.NORTH, northExposed); - float heightSouth = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.SOUTH, southExposed); - float heightEast = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.EAST, eastExposed); - float heightWest = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.WEST, westExposed); - northWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightWest, blockPos, Direction.NORTH, Direction.WEST, northExposed, westExposed); - southWestHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightWest, blockPos, Direction.SOUTH, Direction.WEST, southExposed, westExposed); - southEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightSouth, heightEast, blockPos, Direction.SOUTH, Direction.EAST, southExposed, eastExposed); - northEastHeight = this.fluidCornerHeight(level, fluid, fluidHeight, heightNorth, heightEast, blockPos, Direction.NORTH, Direction.EAST, northExposed, eastExposed); + boolean northExposed = !northSelfOcclusion && this.isSideExposedOffset(level, blockPos, Direction.NORTH, 1.0f); + boolean southExposed = !southSelfOcclusion && this.isSideExposedOffset(level, blockPos, Direction.SOUTH, 1.0f); + boolean westExposed = !westSelfOcclusion && this.isSideExposedOffset(level, blockPos, Direction.WEST, 1.0f); + boolean eastExposed = !eastSelfOcclusion && this.isSideExposedOffset(level, blockPos, Direction.EAST, 1.0f); + float heightNorth = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.NORTH); + float heightSouth = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.SOUTH); + float heightEast = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.EAST); + float heightWest = this.fluidHeightDiscardOccluded(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); } float yOffset = cullDown ? 0.0F : EPSILON; @@ -386,8 +524,6 @@ && isAlignedEquals(southEastHeight, southWestHeight) var sideFluidHeight = Math.max(c1, c2); this.scratchPos.setWithOffset(blockPos, dir); - // allow skipping the side exposed check if it's not a full block, which would have skipped the early exposure check, and the fluid height is 1 meaning the side exposure was already checked earlier - // (!fullFluidBlock && sideFluidHeight == 1.0f) || if (this.isSideExposed(level, this.scratchPos, dir, sideFluidHeight)) { int adjX = this.scratchPos.getX(); int adjY = this.scratchPos.getY(); 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 { From 52088fb77150ca7d96d82c93b382eb6f484377c2 Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 10 Dec 2024 05:16:12 +0100 Subject: [PATCH 11/29] refactor flags to make them less confusing, rename methods, remove redundant checks and methods, simplify methods where possible --- .../pipeline/DefaultFluidRenderer.java | 125 ++++++++---------- 1 file changed, 57 insertions(+), 68 deletions(-) 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 3c46b9c37e..027e24f0df 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 @@ -35,6 +35,7 @@ import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; +// TODO: fix perfectly joining stairs from being water-transmitting and making weird fluid shapes, use shape operation BooleanOp.AND (extend cache to support it) 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 @@ -65,21 +66,15 @@ public DefaultFluidRenderer(LightPipelineProvider lighters) { } /** - * 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. + * Checks if a face of a fluid block, assumed to be a full block for now, should be considered for rendering based on the neighboring block state, but not the voxel shapes (that test is done later). * - * @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 + * @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 not occluded, otherwise false */ - public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGetter view, BlockPos selfPos, Direction facing, FluidState fluid, VoxelShape fluidShape) { - if (isFluidSelfOccluded(selfBlockState, facing, fluidShape)) { - return 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)); @@ -113,7 +108,7 @@ public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGett // 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) || !ShapeComparisonCache.isFullShape(fluidShape); + return !ShapeComparisonCache.isFullShape(otherShape); } /** @@ -124,7 +119,7 @@ public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGett * @param fluidShape The shape of the fluid * @return True if the fluid side facing {@param facing} is occluded by the block it's contained in, otherwise false */ - public boolean isFluidSelfOccluded(BlockState selfBlockState, Direction facing, VoxelShape fluidShape) { + 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); @@ -133,28 +128,33 @@ public boolean isFluidSelfOccluded(BlockState selfBlockState, Direction facing, if (!ShapeComparisonCache.isEmptyShape(selfShape)) { // a full self-shape occludes everything if (ShapeComparisonCache.isFullShape(selfShape) && ShapeComparisonCache.isFullShape(fluidShape)) { - return true; + return false; } // perform occlusion of the fluid by the block it's contained in - if (!this.occlusionCache.lookup(fluidShape, selfShape)) { - return true; - } + return this.occlusionCache.lookup(fluidShape, selfShape); } } - return false; + return true; + } + + private boolean isFullBlockFluidSelfVisible(BlockState blockState, Direction dir) { + return this.isFluidSelfVisible(blockState, dir, Shapes.block()); } /** * Checks if a face of a fluid block with a specific height should be rendered based on the neighboring block state. * - * @param neighborBlockState The state of the neighboring block - * @param facing The facing direction of the side to check - * @param height The height of the fluid + * @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 shouldDrawAccurateFluidSide(BlockState neighborBlockState, Direction facing, float height) { + 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; @@ -192,26 +192,12 @@ public boolean shouldDrawAccurateFluidSide(BlockState neighborBlockState, Direct return this.occlusionCache.lookup(fluidShape, neighborShape); } - private boolean isFullBlockFluidSelfOccluded(BlockState blockState, Direction dir) { - return this.isFluidSelfOccluded(blockState, dir, Shapes.block()); - } - - private boolean isFullBlockFluidNeighborOccluded(BlockAndTintGetter world, BlockPos pos, Direction dir, BlockState blockState, FluidState fluid) { - return !this.shouldDrawFullBlockFluidSide(blockState, world, pos, dir, fluid, Shapes.block()); - } - - 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.isFullBlockFluidSelfOccluded(blockState, dir) || this.isFullBlockFluidNeighborOccluded(world, pos, dir, blockState, fluid); - } - - private boolean isSideExposed(BlockAndTintGetter world, BlockPos pos, Direction dir, float height) { - return this.shouldDrawAccurateFluidSide(world.getBlockState(pos), dir, height); + private boolean isSideExposedOffset(BlockAndTintGetter world, BlockPos originPos, Direction dir, float height) { + return this.isFluidSideExposed(world, this.scratchPos.setWithOffset(originPos, dir), dir, height); } - private boolean isSideExposedOffset(BlockAndTintGetter world, BlockPos originPos, Direction dir, float height) { - return this.isSideExposed(world, this.scratchPos.setWithOffset(originPos, dir), dir, height); + private boolean isFullBlockFluidVisible(BlockAndTintGetter world, BlockPos pos, Direction dir, BlockState blockState, FluidState fluid) { + return isFullBlockFluidSelfVisible(blockState, dir) && this.isFullBlockFluidSideVisible(world, pos, dir, fluid); } private static final float DISCARD_SAMPLE = -1.0f; @@ -230,7 +216,8 @@ private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockP } } - return 0.0f; + // NOTE: returning 0 here makes shallow water shallower and bends water surfaces towards non-occluding blocks like glass or non-waterlogged stairs + return DISCARD_SAMPLE; } private float fluidHeightDiscardOccluded(BlockAndTintGetter world, Fluid fluid, BlockPos origin, Direction offset) { @@ -246,6 +233,7 @@ private void addHeightSample(float sample) { this.scratchSamples++; } + // else -> sample == DISCARD_SAMPLE } @@ -264,10 +252,10 @@ private float fluidCornerHeight(BlockAndTintGetter world, BlockPos origin, Fluid 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.isFullBlockFluidSelfOccluded(world.getBlockState(aNeighbor), dirB) && + 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.isFullBlockFluidSelfOccluded(world.getBlockState(bNeighbor), dirA) && + var exposedBD = this.isFullBlockFluidSelfVisible(world.getBlockState(bNeighbor), dirA) && this.isSideExposedOffset(world, bNeighbor, dirA, 1.0f); exposedADB = exposedAD && exposedBD; @@ -305,21 +293,22 @@ private float fluidCornerHeight(BlockAndTintGetter world, BlockPos origin, Fluid 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.isSideExposedOffset(level, blockPos, Direction.DOWN, 0.8888889F); + 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, 0.8888889F); - boolean northSelfOcclusion = this.isFullBlockFluidSelfOccluded(blockState, Direction.NORTH); - boolean southSelfOcclusion = this.isFullBlockFluidSelfOccluded(blockState, Direction.SOUTH); - boolean westSelfOcclusion = this.isFullBlockFluidSelfOccluded(blockState, Direction.WEST); - boolean eastSelfOcclusion = this.isFullBlockFluidSelfOccluded(blockState, Direction.EAST); - boolean cullNorth = northSelfOcclusion || this.isFullBlockFluidNeighborOccluded(level, blockPos, Direction.NORTH, blockState, fluidState); - boolean cullSouth = southSelfOcclusion || this.isFullBlockFluidNeighborOccluded(level, blockPos, Direction.SOUTH, blockState, fluidState); - boolean cullWest = westSelfOcclusion || this.isFullBlockFluidNeighborOccluded(level, blockPos, Direction.WEST, blockState, fluidState); - boolean cullEast = eastSelfOcclusion || this.isFullBlockFluidNeighborOccluded(level, blockPos, Direction.EAST, blockState, fluidState); + // TODO: disentangle why there are so many checks here. Can we just combine everything into one set of "visible/exposed" flags? Why does it seem to break when I do that, is it necessary to have self-visibility separate? + 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.isFullBlockFluidVisible(level, blockPos, Direction.NORTH, blockState, fluidState); + boolean southVisible = southSelfVisible && this.isFullBlockFluidVisible(level, blockPos, Direction.SOUTH, blockState, fluidState); + boolean westVisible = westSelfVisible && this.isFullBlockFluidVisible(level, blockPos, Direction.WEST, blockState, fluidState); + boolean eastVisible = eastSelfVisible && this.isFullBlockFluidVisible(level, blockPos, Direction.EAST, blockState, 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; } @@ -334,10 +323,10 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat southEastHeight = 1.0f; northEastHeight = 1.0f; } else { - boolean northExposed = !northSelfOcclusion && this.isSideExposedOffset(level, blockPos, Direction.NORTH, 1.0f); - boolean southExposed = !southSelfOcclusion && this.isSideExposedOffset(level, blockPos, Direction.SOUTH, 1.0f); - boolean westExposed = !westSelfOcclusion && this.isSideExposedOffset(level, blockPos, Direction.WEST, 1.0f); - boolean eastExposed = !eastSelfOcclusion && this.isSideExposedOffset(level, blockPos, Direction.EAST, 1.0f); + 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.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.NORTH); float heightSouth = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.SOUTH); float heightEast = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.EAST); @@ -348,7 +337,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat 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); } - float yOffset = cullDown ? 0.0F : EPSILON; + float yOffset = !downVisible ? 0.0F : EPSILON; final ModelQuadViewMutable quad = this.quad; @@ -358,7 +347,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat quad.setFlags(0); float totalMinHeight = Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)); - if (!cullUp && this.isSideExposedOffset(level, blockPos, Direction.UP, totalMinHeight)) { + if (upVisible && this.isSideExposedOffset(level, blockPos, Direction.UP, totalMinHeight)) { northWestHeight -= EPSILON; southWestHeight -= EPSILON; southEastHeight -= EPSILON; @@ -443,7 +432,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) } } - if (!cullDown) { + if (downVisible) { TextureAtlasSprite sprite = sprites[0]; float minU = sprite.getU0(); @@ -473,7 +462,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) switch (dir) { case NORTH -> { - if (cullNorth) { + if (!northVisible) { continue; } c1 = northWestHeight; @@ -484,7 +473,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = z1; } case SOUTH -> { - if (cullSouth) { + if (!southVisible) { continue; } c1 = southEastHeight; @@ -495,7 +484,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = z1; } case WEST -> { - if (cullWest) { + if (!westVisible) { continue; } c1 = southWestHeight; @@ -506,7 +495,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = 0.0f; } case EAST -> { - if (cullEast) { + if (!eastVisible) { continue; } c1 = northEastHeight; @@ -524,7 +513,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) var sideFluidHeight = Math.max(c1, c2); this.scratchPos.setWithOffset(blockPos, dir); - if (this.isSideExposed(level, this.scratchPos, dir, sideFluidHeight)) { + if (this.isFluidSideExposed(level, this.scratchPos, dir, sideFluidHeight)) { int adjX = this.scratchPos.getX(); int adjY = this.scratchPos.getY(); int adjZ = this.scratchPos.getZ(); From 1159cf5eb03403562ff43f3de02f5bc2c298a579 Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 10 Dec 2024 16:12:43 +0100 Subject: [PATCH 12/29] add flowing water flattening factor to approximate the previous look of thin flowing water end-pieces --- .../chunk/compile/pipeline/DefaultFluidRenderer.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 027e24f0df..5639b0b41d 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 @@ -42,6 +42,10 @@ public class DefaultFluidRenderer { 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 static final float FLATTENING_FACTOR = 0.07f; + private final BlockPos.MutableBlockPos scratchPos = new BlockPos.MutableBlockPos(); private final BlockPos.MutableBlockPos occlusionScratchPos = new BlockPos.MutableBlockPos(); private float scratchHeight = 0.0f; @@ -200,8 +204,6 @@ private boolean isFullBlockFluidVisible(BlockAndTintGetter world, BlockPos pos, return isFullBlockFluidSelfVisible(blockState, dir) && this.isFullBlockFluidSideVisible(world, pos, dir, fluid); } - private static final float DISCARD_SAMPLE = -1.0f; - private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockPos) { BlockState blockState = world.getBlockState(blockPos); FluidState fluidState = blockState.getFluidState(); @@ -284,6 +286,9 @@ private float fluidCornerHeight(BlockAndTintGetter world, BlockPos origin, Fluid // gather the samples and reset float result = this.scratchHeight / this.scratchSamples; + if (result < FULL_HEIGHT) { + result -= (FULL_HEIGHT - result) * FLATTENING_FACTOR; + } this.scratchHeight = 0.0f; this.scratchSamples = 0; @@ -295,7 +300,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat 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, 0.8888889F); + this.isSideExposedOffset(level, blockPos, Direction.DOWN, FULL_HEIGHT); // TODO: disentangle why there are so many checks here. Can we just combine everything into one set of "visible/exposed" flags? Why does it seem to break when I do that, is it necessary to have self-visibility separate? boolean northSelfVisible = this.isFullBlockFluidSelfVisible(blockState, Direction.NORTH); From 2f17b314addaefb935b1f98f373b0b9e179df319 Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 10 Dec 2024 18:56:40 +0100 Subject: [PATCH 13/29] improved neighbor check for up face occlusion --- .../pipeline/DefaultFluidRenderer.java | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) 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 5639b0b41d..a9f50bd19d 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 @@ -351,8 +351,21 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat quad.setFlags(0); - float totalMinHeight = Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)); - if (upVisible && this.isSideExposedOffset(level, blockPos, Direction.UP, totalMinHeight)) { + // calculate up fluid face visibility + 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 inner up face and outer up face if there's solid or same-fluid blocks around it + boolean innerUpFaceVisible = true; + if (upVisible) { + innerUpFaceVisible = isUpFaceExposedByNeighbors(level, blockPos, fluid, 1, 1, -1) || + isUpFaceExposedByNeighbors(level, blockPos, fluid, 0, 1, 0); + upVisible = innerUpFaceVisible || isUpFaceExposedByNeighbors(level, blockPos, fluid, 1, 2, 1); + } + + if (upVisible) { northWestHeight -= EPSILON; southWestHeight -= EPSILON; southEastHeight -= EPSILON; @@ -431,7 +444,7 @@ && 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.setWithOffset(blockPos, Direction.UP))) { + if (innerUpFaceVisible) { this.writeQuad(meshBuilder, collector, material, offset, quad, aligned ? ModelQuadFacing.NEG_Y : ModelQuadFacing.UNASSIGNED, true); } @@ -564,6 +577,23 @@ && isAlignedEquals(southEastHeight, southWestHeight) } } + private boolean isUpFaceExposedByNeighbors(LevelSlice level, BlockPos blockPos, Fluid fluid, int yOffset, int range, int skipRange) { + for (int i = -range; i <= range; ++i) { + for (int j = -range; j <= range; ++j) { + if (skipRange >= 0 && i <= skipRange && i >= -skipRange && j <= skipRange && j >= -skipRange) { + continue; + } + + // the face is visible if any of the blocks + BlockPos blockPos2 = this.scratchPos.setWithOffset(blockPos, i, yOffset, j); + if (!level.getFluidState(blockPos2).getType().isSame(fluid) && !level.getBlockState(blockPos2).isSolidRender()) { + return true; + } + } + } + return false; + } + private static boolean isAlignedEquals(float a, float b) { return Math.abs(a - b) <= ALIGNED_EQUALS_EPSILON; } From bad5d51395c2445f832944766324d609b57361b7 Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 10 Dec 2024 20:05:36 +0100 Subject: [PATCH 14/29] cleanup ordering and formatting --- .../pipeline/DefaultFluidRenderer.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 a9f50bd19d..e580e5be39 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 @@ -222,7 +222,7 @@ private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockP return DISCARD_SAMPLE; } - private float fluidHeightDiscardOccluded(BlockAndTintGetter world, Fluid fluid, BlockPos origin, Direction offset) { + private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos origin, Direction offset) { return this.fluidHeight(world, fluid, this.scratchPos.setWithOffset(origin, offset)); } @@ -235,7 +235,6 @@ private void addHeightSample(float sample) { this.scratchSamples++; } - // else -> sample == DISCARD_SAMPLE } @@ -274,15 +273,15 @@ private float fluidCornerHeight(BlockAndTintGetter world, BlockPos origin, Fluid } } - this.addHeightSample(fluidHeight); - // add samples for the sides if they're exposed or if there's a path through the diagonal to them - if (exposedB || exposedA && exposedADB) { - this.addHeightSample(fluidHeightB); - } 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; @@ -332,10 +331,10 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat 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.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.NORTH); - float heightSouth = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.SOUTH); - float heightEast = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.EAST); - float heightWest = this.fluidHeightDiscardOccluded(level, fluid, blockPos, Direction.WEST); + float heightNorth = this.fluidHeight(level, fluid, blockPos, Direction.NORTH); + float heightSouth = this.fluidHeight(level, fluid, blockPos, Direction.SOUTH); + float heightEast = this.fluidHeight(level, fluid, blockPos, Direction.EAST); + float heightWest = this.fluidHeight(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); From d59f674561ee899a24fb01d79484328d754b9520 Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 10 Dec 2024 20:37:01 +0100 Subject: [PATCH 15/29] fix shaping of fluids around diagonal waterfalls, add comments --- .../compile/pipeline/DefaultFluidRenderer.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 e580e5be39..f7b46679bf 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 @@ -209,7 +209,7 @@ private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockP FluidState fluidState = blockState.getFluidState(); if (fluid.isSame(fluidState.getType())) { - FluidState fluidStateUp = world.getFluidState(blockPos.above()); + FluidState fluidStateUp = world.getFluidState(this.scratchPos.setWithOffset(blockPos, Direction.UP)); if (fluid.isSame(fluidStateUp.getType())) { return 1.0f; @@ -218,7 +218,15 @@ private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockP } } - // NOTE: returning 0 here makes shallow water shallower and bends water surfaces towards non-occluding blocks like glass or non-waterlogged stairs + // if the block is air and there's either air or water below it, actually return a valid 0 sample that will bend the fluid downwards. + // this is important to maintain the sloped shape of diagonal waterfalls + if (blockState.isAir()) { + var downBlockState = world.getBlockState(this.scratchPos.setWithOffset(blockPos, Direction.DOWN)); + if (downBlockState.isAir() || downBlockState.getFluidState().getType().isSame(fluid)) { + return 0.0f; + } + } + return DISCARD_SAMPLE; } @@ -285,8 +293,12 @@ private float fluidCornerHeight(BlockAndTintGetter world, BlockPos origin, Fluid // gather the samples and reset float result = this.scratchHeight / this.scratchSamples; + + // shallow water is flattened somewhat to compensate for the fact that many air samples (height zero) + // that are otherwise taken into account with the reference implementation are discarded if (result < FULL_HEIGHT) { result -= (FULL_HEIGHT - result) * FLATTENING_FACTOR; + result = Math.max(result, 0.0f); } this.scratchHeight = 0.0f; this.scratchSamples = 0; From 1d9495f25715f5fc83c90f327cc48c67ff1b7963 Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 10 Dec 2024 20:52:36 +0100 Subject: [PATCH 16/29] add inwards facing quads for fluid down faces, fixes https://github.com/CaffeineMC/sodium/issues/1210 --- .../render/chunk/compile/pipeline/DefaultFluidRenderer.java | 1 + 1 file changed, 1 insertion(+) 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 f7b46679bf..d6f26a721b 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 @@ -477,6 +477,7 @@ && 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); + this.writeQuad(meshBuilder, collector, material, offset, quad, ModelQuadFacing.POS_Y, true); } quad.setFlags(ModelQuadFlags.IS_PARALLEL | ModelQuadFlags.IS_ALIGNED); From baba0582271d0ea2c56b58262a83b16f7440e9ad Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 11 Dec 2024 02:39:04 +0100 Subject: [PATCH 17/29] remove unnecessary self visibility checks --- .../chunk/compile/pipeline/DefaultFluidRenderer.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/DefaultFluidRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/DefaultFluidRenderer.java index d6f26a721b..364985b7af 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 @@ -318,10 +318,10 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat 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.isFullBlockFluidVisible(level, blockPos, Direction.NORTH, blockState, fluidState); - boolean southVisible = southSelfVisible && this.isFullBlockFluidVisible(level, blockPos, Direction.SOUTH, blockState, fluidState); - boolean westVisible = westSelfVisible && this.isFullBlockFluidVisible(level, blockPos, Direction.WEST, blockState, fluidState); - boolean eastVisible = eastSelfVisible && this.isFullBlockFluidVisible(level, blockPos, Direction.EAST, blockState, fluidState); + 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 (!upVisible && !downVisible && !eastVisible && !westVisible && !northVisible && !southVisible) { From 6972baf379ebd23372d5f61e5e09e6349d00679d Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 11 Dec 2024 02:39:18 +0100 Subject: [PATCH 18/29] improve top face hiding heuristic to also check for source blocks --- .../render/chunk/compile/pipeline/DefaultFluidRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 364985b7af..2f11c5b6dd 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 @@ -597,8 +597,8 @@ private boolean isUpFaceExposedByNeighbors(LevelSlice level, BlockPos blockPos, } // the face is visible if any of the blocks - BlockPos blockPos2 = this.scratchPos.setWithOffset(blockPos, i, yOffset, j); - if (!level.getFluidState(blockPos2).getType().isSame(fluid) && !level.getBlockState(blockPos2).isSolidRender()) { + BlockPos otherBlockPos = this.scratchPos.setWithOffset(blockPos, i, yOffset, j); + if (!(level.getFluidState(otherBlockPos).isSourceOfType(fluid) || level.getBlockState(otherBlockPos).isSolidRender())) { return true; } } From a7c43f27257380915ed4c4c7deec2310f01ee5c5 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 11 Dec 2024 03:52:39 +0100 Subject: [PATCH 19/29] weight samples like reference implementation to prevent weird flow patterns --- .../compile/pipeline/DefaultFluidRenderer.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) 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 2f11c5b6dd..1fed3dca86 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 @@ -218,13 +218,8 @@ private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockP } } - // if the block is air and there's either air or water below it, actually return a valid 0 sample that will bend the fluid downwards. - // this is important to maintain the sloped shape of diagonal waterfalls - if (blockState.isAir()) { - var downBlockState = world.getBlockState(this.scratchPos.setWithOffset(blockPos, Direction.DOWN)); - if (downBlockState.isAir() || downBlockState.getFluidState().getType().isSame(fluid)) { - return 0.0f; - } + if (!blockState.isSolid()) { + return 0.0f; } return DISCARD_SAMPLE; @@ -294,12 +289,6 @@ private float fluidCornerHeight(BlockAndTintGetter world, BlockPos origin, Fluid // gather the samples and reset float result = this.scratchHeight / this.scratchSamples; - // shallow water is flattened somewhat to compensate for the fact that many air samples (height zero) - // that are otherwise taken into account with the reference implementation are discarded - if (result < FULL_HEIGHT) { - result -= (FULL_HEIGHT - result) * FLATTENING_FACTOR; - result = Math.max(result, 0.0f); - } this.scratchHeight = 0.0f; this.scratchSamples = 0; From bc0215d1fb75b37a19c28826f2384251c96063b8 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 11 Dec 2024 04:34:33 +0100 Subject: [PATCH 20/29] add documentation for what the visibility concepts are, opportunistically reduce visibility as available --- .../compile/pipeline/DefaultFluidRenderer.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 1fed3dca86..1e9c2edf8f 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 @@ -44,7 +44,6 @@ public class DefaultFluidRenderer { private static final float DISCARD_SAMPLE = -1.0f; private static final float FULL_HEIGHT = 0.8888889f; - private static final float FLATTENING_FACTOR = 0.07f; private final BlockPos.MutableBlockPos scratchPos = new BlockPos.MutableBlockPos(); private final BlockPos.MutableBlockPos occlusionScratchPos = new BlockPos.MutableBlockPos(); @@ -302,7 +301,11 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat boolean downVisible = this.isFullBlockFluidVisible(level, blockPos, Direction.DOWN, blockState, fluidState) && this.isSideExposedOffset(level, blockPos, Direction.DOWN, FULL_HEIGHT); - // TODO: disentangle why there are so many checks here. Can we just combine everything into one set of "visible/exposed" flags? Why does it seem to break when I do that, is it necessary to have self-visibility separate? + // There are different concepts here: + // "visible" means the fluid face is rendered. Fluid faces are not rendered if it has neighbors of the same fluid type. + // "exposed" means there's 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. + // "self-visible" means the block the fluid is inside isn't preventing the fluid face from rendering. + // Visible implies self-visible implies exposed but not the other way around. boolean northSelfVisible = this.isFullBlockFluidSelfVisible(blockState, Direction.NORTH); boolean southSelfVisible = this.isFullBlockFluidSelfVisible(blockState, Direction.SOUTH); boolean westSelfVisible = this.isFullBlockFluidSelfVisible(blockState, Direction.WEST); @@ -341,6 +344,12 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat 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; From 4c635e9a71cbd4e3213181d160f1f6243a464b4e Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 11 Dec 2024 19:18:58 +0100 Subject: [PATCH 21/29] improve inwards facing bottom fluid face test, it was too frequent --- .../chunk/compile/pipeline/DefaultFluidRenderer.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 1e9c2edf8f..826f9112bf 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 @@ -310,6 +310,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat 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); @@ -335,6 +336,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat 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.fluidHeight(level, fluid, blockPos, Direction.NORTH); float heightSouth = this.fluidHeight(level, fluid, blockPos, Direction.SOUTH); float heightEast = this.fluidHeight(level, fluid, blockPos, Direction.EAST); @@ -475,7 +477,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); - this.writeQuad(meshBuilder, collector, material, offset, quad, ModelQuadFacing.POS_Y, true); + + // render inwards facing down face if the block below is not sturdy + // this is a better heuristic than using !isSolid (water rendered on too few blocks) or !isSolidRender (water rendered on too many blocks) + var below = this.scratchPos.setWithOffset(blockPos, Direction.DOWN); + if (!level.getBlockState(below).isFaceSturdy(level, below, Direction.UP)) { + this.writeQuad(meshBuilder, collector, material, offset, quad, ModelQuadFacing.POS_Y, true); + } } quad.setFlags(ModelQuadFlags.IS_PARALLEL | ModelQuadFlags.IS_ALIGNED); From 75edd07da6b6247880a07eab784d013fe121d39a Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 13 Dec 2024 19:57:55 +0100 Subject: [PATCH 22/29] avoid unnecessary coordinate fetch --- .../chunk/compile/pipeline/DefaultFluidRenderer.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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 826f9112bf..80bf9e1d48 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 @@ -550,19 +550,14 @@ && isAlignedEquals(southEastHeight, southWestHeight) this.scratchPos.setWithOffset(blockPos, dir); if (this.isFluidSideExposed(level, this.scratchPos, dir, sideFluidHeight)) { - int adjX = this.scratchPos.getX(); - int adjY = this.scratchPos.getY(); - int adjZ = this.scratchPos.getZ(); - 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; } From 6232e945fed98ed016f9c840b561d27d8ed08c13 Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 15 Dec 2024 17:14:21 +0100 Subject: [PATCH 23/29] use same inwards water quad heuristic for down face as for side faces --- .../chunk/compile/pipeline/DefaultFluidRenderer.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 80bf9e1d48..d04962731d 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 @@ -478,10 +478,9 @@ && 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 face if the block below is not sturdy - // this is a better heuristic than using !isSolid (water rendered on too few blocks) or !isSolidRender (water rendered on too many blocks) - var below = this.scratchPos.setWithOffset(blockPos, Direction.DOWN); - if (!level.getBlockState(below).isFaceSturdy(level, below, Direction.UP)) { + // render inwards facing down fluid face using the same heuristic as the 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); } } From 20fcd6389a3bd1e9b78606752ef5287c169f4d6a Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 15 Dec 2024 20:13:28 +0100 Subject: [PATCH 24/29] better cave water occlusion heuristic using floodfill --- .../pipeline/DefaultFluidRenderer.java | 73 ++++++++++++++----- 1 file changed, 55 insertions(+), 18 deletions(-) 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 d04962731d..e4bf26676a 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,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline; +import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; 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; @@ -49,6 +50,8 @@ public class DefaultFluidRenderer { private final BlockPos.MutableBlockPos occlusionScratchPos = new BlockPos.MutableBlockPos(); private float scratchHeight = 0.0f; private int scratchSamples = 0; + private final ByteArrayFIFOQueue queue = new ByteArrayFIFOQueue(); + private long visited = 0; private final ShapeComparisonCache occlusionCache = new ShapeComparisonCache(); @@ -369,11 +372,8 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat } // apply heuristic to not render inner up face and outer up face if there's solid or same-fluid blocks around it - boolean innerUpFaceVisible = true; if (upVisible) { - innerUpFaceVisible = isUpFaceExposedByNeighbors(level, blockPos, fluid, 1, 1, -1) || - isUpFaceExposedByNeighbors(level, blockPos, fluid, 0, 1, 0); - upVisible = innerUpFaceVisible || isUpFaceExposedByNeighbors(level, blockPos, fluid, 1, 2, 1); + upVisible = isUpFaceExposedByNeighbors(level, blockPos, fluid); } if (upVisible) { @@ -454,11 +454,7 @@ && 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 (innerUpFaceVisible) { - this.writeQuad(meshBuilder, collector, material, offset, quad, - aligned ? ModelQuadFacing.NEG_Y : ModelQuadFacing.UNASSIGNED, true); - } + this.writeQuad(meshBuilder, collector, material, offset, quad, aligned ? ModelQuadFacing.NEG_Y : ModelQuadFacing.UNASSIGNED, true); } if (downVisible) { @@ -589,20 +585,61 @@ && isAlignedEquals(southEastHeight, southWestHeight) } } - private boolean isUpFaceExposedByNeighbors(LevelSlice level, BlockPos blockPos, Fluid fluid, int yOffset, int range, int skipRange) { - for (int i = -range; i <= range; ++i) { - for (int j = -range; j <= range; ++j) { - if (skipRange >= 0 && i <= skipRange && i >= -skipRange && j <= skipRange && j >= -skipRange) { - continue; - } + private long offsetToMask(int x, int y, int z) { + return 1L << ((x + 2) + (y << 5) + (z + 2) * 5); + } - // the face is visible if any of the blocks - BlockPos otherBlockPos = this.scratchPos.setWithOffset(blockPos, i, yOffset, j); - if (!(level.getFluidState(otherBlockPos).isSourceOfType(fluid) || level.getBlockState(otherBlockPos).isSolidRender())) { + private boolean visitExposureNeighbor(BlockGetter level, BlockPos origin, Fluid fluid, ByteArrayFIFOQueue queue, byte xOffset, byte yOffset, byte zOffset) { + var upNeighborMask = offsetToMask(xOffset, yOffset, zOffset); + if ((this.visited & upNeighborMask) == 0) { + this.visited |= upNeighborMask; + + var other = this.scratchPos.setWithOffset(origin, xOffset, yOffset, zOffset); + var blockState = level.getBlockState(other); + if (!blockState.isSolidRender()) { + if (!blockState.getFluidState().isSourceOfType(fluid)) { return true; + } else { + queue.enqueue(xOffset); + queue.enqueue(yOffset); + queue.enqueue(zOffset); } } } + + return false; + } + + private boolean isUpFaceExposedByNeighbors(BlockGetter level, BlockPos origin, Fluid fluid) { + this.visited = 1L << offsetToMask(0, 0, 0); + var queue = this.queue; + queue.clear(); + queue.enqueue((byte) 0); + queue.enqueue((byte) 0); + queue.enqueue((byte) 0); + + while (!queue.isEmpty()) { + var x = queue.dequeueByte(); + var y = queue.dequeueByte(); + var z = queue.dequeueByte(); + + if (y == 0 && visitExposureNeighbor(level, origin, fluid, queue, x, (byte) 1, z)) { + return true; + } + if (x >= 0 && x < 2 && visitExposureNeighbor(level, origin, fluid, queue, (byte) (x + 1), y, z)) { + return true; + } + if (x <= 0 && x > -2 && visitExposureNeighbor(level, origin, fluid, queue, (byte) (x - 1), y, z)) { + return true; + } + if (z >= 0 && z < 2 && visitExposureNeighbor(level, origin, fluid, queue, x, y, (byte) (z + 1))) { + return true; + } + if (z <= 0 && z > -2 && visitExposureNeighbor(level, origin, fluid, queue, x, y, (byte) (z - 1))) { + return true; + } + } + return false; } From 833e41f42d27093d4d84e8634d4e85c3138d8c9c Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 15 Dec 2024 21:22:47 +0100 Subject: [PATCH 25/29] make cave water occlusion faster by using a stack instead of a queue for the search --- .../pipeline/DefaultFluidRenderer.java | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) 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 e4bf26676a..a907df1026 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 @@ -2,6 +2,8 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; +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; @@ -50,7 +52,7 @@ public class DefaultFluidRenderer { private final BlockPos.MutableBlockPos occlusionScratchPos = new BlockPos.MutableBlockPos(); private float scratchHeight = 0.0f; private int scratchSamples = 0; - private final ByteArrayFIFOQueue queue = new ByteArrayFIFOQueue(); + private final ByteList stack = new ByteArrayList(); private long visited = 0; private final ShapeComparisonCache occlusionCache = new ShapeComparisonCache(); @@ -589,7 +591,7 @@ private long offsetToMask(int x, int y, int z) { return 1L << ((x + 2) + (y << 5) + (z + 2) * 5); } - private boolean visitExposureNeighbor(BlockGetter level, BlockPos origin, Fluid fluid, ByteArrayFIFOQueue queue, byte xOffset, byte yOffset, byte zOffset) { + private boolean visitExposureNeighbor(BlockGetter level, BlockPos origin, Fluid fluid, ByteList stack, byte xOffset, byte yOffset, byte zOffset) { var upNeighborMask = offsetToMask(xOffset, yOffset, zOffset); if ((this.visited & upNeighborMask) == 0) { this.visited |= upNeighborMask; @@ -600,9 +602,9 @@ private boolean visitExposureNeighbor(BlockGetter level, BlockPos origin, Fluid if (!blockState.getFluidState().isSourceOfType(fluid)) { return true; } else { - queue.enqueue(xOffset); - queue.enqueue(yOffset); - queue.enqueue(zOffset); + stack.add(xOffset); + stack.add(yOffset); + stack.add(zOffset); } } } @@ -611,31 +613,37 @@ private boolean visitExposureNeighbor(BlockGetter level, BlockPos origin, Fluid } private boolean isUpFaceExposedByNeighbors(BlockGetter level, BlockPos origin, Fluid fluid) { + // search for an accessible non-solid block that's reachable through a path of same-type fluid source blocks. + // if such a block exists, the fluid is exposed. If it can't be reached, the fluid is considered occluded. + + // performs a simple DFS using a stack and a visited bit mask this.visited = 1L << offsetToMask(0, 0, 0); - var queue = this.queue; - queue.clear(); - queue.enqueue((byte) 0); - queue.enqueue((byte) 0); - queue.enqueue((byte) 0); - - while (!queue.isEmpty()) { - var x = queue.dequeueByte(); - var y = queue.dequeueByte(); - var z = queue.dequeueByte(); - - if (y == 0 && visitExposureNeighbor(level, origin, fluid, queue, x, (byte) 1, z)) { + var stack = this.stack; + stack.clear(); + stack.add((byte) 0); + stack.add((byte) 0); + stack.add((byte) 0); + + while (!stack.isEmpty()) { + // remove coordinates from the stack in reverse order to preserve their format + var z = stack.removeByte(stack.size() - 1); + var y = stack.removeByte(stack.size() - 1); + var x = stack.removeByte(stack.size() - 1); + + // traverse into unvisited neighbors in outwards direction + if (y == 0 && visitExposureNeighbor(level, origin, fluid, stack, x, (byte) 1, z)) { return true; } - if (x >= 0 && x < 2 && visitExposureNeighbor(level, origin, fluid, queue, (byte) (x + 1), y, z)) { + if (x >= 0 && x < 2 && visitExposureNeighbor(level, origin, fluid, stack, (byte) (x + 1), y, z)) { return true; } - if (x <= 0 && x > -2 && visitExposureNeighbor(level, origin, fluid, queue, (byte) (x - 1), y, z)) { + if (x <= 0 && x > -2 && visitExposureNeighbor(level, origin, fluid, stack, (byte) (x - 1), y, z)) { return true; } - if (z >= 0 && z < 2 && visitExposureNeighbor(level, origin, fluid, queue, x, y, (byte) (z + 1))) { + if (z >= 0 && z < 2 && visitExposureNeighbor(level, origin, fluid, stack, x, y, (byte) (z + 1))) { return true; } - if (z <= 0 && z > -2 && visitExposureNeighbor(level, origin, fluid, queue, x, y, (byte) (z - 1))) { + if (z <= 0 && z > -2 && visitExposureNeighbor(level, origin, fluid, stack, x, y, (byte) (z - 1))) { return true; } } From e2f8d63ffa95470e38d295d38b7eb1759456ec8e Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 15 Dec 2024 21:23:05 +0100 Subject: [PATCH 26/29] cleanup --- .../render/chunk/compile/pipeline/DefaultFluidRenderer.java | 1 - 1 file changed, 1 deletion(-) 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 a907df1026..59b0e5fff5 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,7 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline; -import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.bytes.ByteList; import net.caffeinemc.mods.sodium.api.util.ColorARGB; From 4a5d88e5674329f4ea0a5be3e358ade3461cb9b5 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 18 Dec 2024 02:42:06 +0100 Subject: [PATCH 27/29] Fix bug with cave water heuristic --- .../render/chunk/compile/pipeline/DefaultFluidRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 59b0e5fff5..909c08777f 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 @@ -616,7 +616,7 @@ private boolean isUpFaceExposedByNeighbors(BlockGetter level, BlockPos origin, F // if such a block exists, the fluid is exposed. If it can't be reached, the fluid is considered occluded. // performs a simple DFS using a stack and a visited bit mask - this.visited = 1L << offsetToMask(0, 0, 0); + this.visited = offsetToMask(0, 0, 0); var stack = this.stack; stack.clear(); stack.add((byte) 0); From 6c4f940243f5ec6d9604844fc420f9fbbc5b9ebf Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 18 Dec 2024 03:30:38 +0100 Subject: [PATCH 28/29] add documentation --- .../pipeline/DefaultFluidRenderer.java | 143 +++++++++++------- 1 file changed, 91 insertions(+), 52 deletions(-) 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 909c08777f..37b71f8fa8 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 @@ -37,7 +37,21 @@ import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; -// TODO: fix perfectly joining stairs from being water-transmitting and making weird fluid shapes, use shape operation BooleanOp.AND (extend cache to support it) +// 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 @@ -73,13 +87,13 @@ public DefaultFluidRenderer(LightPipelineProvider lighters) { } /** - * Checks if a face of a fluid block, assumed to be a full block for now, should be considered for rendering based on the neighboring block state, but not the voxel shapes (that test is done later). + * 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 not occluded, otherwise false + * @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 @@ -119,12 +133,12 @@ public boolean isFullBlockFluidSideVisible(BlockGetter view, BlockPos selfPos, D } /** - * Checks if a face of the fluid is occluded by the block it's contained in. + * 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 occluded by the block it's contained in, otherwise false + * @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 @@ -203,11 +217,22 @@ private boolean isSideExposedOffset(BlockAndTintGetter world, BlockPos originPos 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); } - private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockPos) { + /** + * 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(); @@ -228,8 +253,8 @@ private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockP return DISCARD_SAMPLE; } - private float fluidHeight(BlockAndTintGetter world, Fluid fluid, BlockPos origin, Direction offset) { - return this.fluidHeight(world, fluid, this.scratchPos.setWithOffset(origin, offset)); + 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) { @@ -244,6 +269,21 @@ private void addHeightSample(float sample) { // else -> sample == DISCARD_SAMPLE } + /** + * 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; @@ -269,7 +309,7 @@ private float fluidCornerHeight(BlockAndTintGetter world, BlockPos origin, Fluid 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.fluidHeight(world, fluid, abNeighbor); + float height = this.sampleFluidHeight(world, fluid, abNeighbor); if (height >= 1.0f) { return 1.0f; @@ -305,11 +345,8 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat boolean downVisible = this.isFullBlockFluidVisible(level, blockPos, Direction.DOWN, blockState, fluidState) && this.isSideExposedOffset(level, blockPos, Direction.DOWN, FULL_HEIGHT); - // There are different concepts here: - // "visible" means the fluid face is rendered. Fluid faces are not rendered if it has neighbors of the same fluid type. - // "exposed" means there's 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. - // "self-visible" means the block the fluid is inside isn't preventing the fluid face from rendering. - // Visible implies self-visible implies exposed but not the other way around. + // 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); @@ -327,24 +364,24 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat boolean isWater = fluidState.is(FluidTags.WATER); - float fluidHeight = this.fluidHeight(level, fluid, blockPos); - boolean fullFluidBlock = fluidHeight >= 1.0f; + float fluidHeight = this.sampleFluidHeight(level, fluid, blockPos); float northWestHeight, southWestHeight, southEastHeight, northEastHeight; - if (fullFluidBlock) { + if (fluidHeight >= 1.0f) { northWestHeight = 1.0f; southWestHeight = 1.0f; southEastHeight = 1.0f; northEastHeight = 1.0f; } else { + // 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.fluidHeight(level, fluid, blockPos, Direction.NORTH); - float heightSouth = this.fluidHeight(level, fluid, blockPos, Direction.SOUTH); - float heightEast = this.fluidHeight(level, fluid, blockPos, Direction.EAST); - float heightWest = this.fluidHeight(level, fluid, blockPos, Direction.WEST); + 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); @@ -366,13 +403,13 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat quad.setFlags(0); - // calculate up fluid face visibility + // 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 inner up face and outer up face if there's solid or same-fluid blocks around it + // apply heuristic to not render up face it's in a flooded cave if (upVisible) { upVisible = isUpFaceExposedByNeighbors(level, blockPos, fluid); } @@ -435,6 +472,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 @@ -475,7 +513,8 @@ && 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 + // 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); @@ -586,35 +625,10 @@ && isAlignedEquals(southEastHeight, southWestHeight) } } - private long offsetToMask(int x, int y, int z) { - return 1L << ((x + 2) + (y << 5) + (z + 2) * 5); - } - - private boolean visitExposureNeighbor(BlockGetter level, BlockPos origin, Fluid fluid, ByteList stack, byte xOffset, byte yOffset, byte zOffset) { - var upNeighborMask = offsetToMask(xOffset, yOffset, zOffset); - if ((this.visited & upNeighborMask) == 0) { - this.visited |= upNeighborMask; - - var other = this.scratchPos.setWithOffset(origin, xOffset, yOffset, zOffset); - var blockState = level.getBlockState(other); - if (!blockState.isSolidRender()) { - if (!blockState.getFluidState().isSourceOfType(fluid)) { - return true; - } else { - stack.add(xOffset); - stack.add(yOffset); - stack.add(zOffset); - } - } - } - - return false; - } - + /** + * This flooded cave heuristic performs a depth-first search looking for a non-solid block that's 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. + */ private boolean isUpFaceExposedByNeighbors(BlockGetter level, BlockPos origin, Fluid fluid) { - // search for an accessible non-solid block that's reachable through a path of same-type fluid source blocks. - // if such a block exists, the fluid is exposed. If it can't be reached, the fluid is considered occluded. - // performs a simple DFS using a stack and a visited bit mask this.visited = offsetToMask(0, 0, 0); var stack = this.stack; @@ -650,6 +664,31 @@ private boolean isUpFaceExposedByNeighbors(BlockGetter level, BlockPos origin, F return false; } + private long offsetToMask(int x, int y, int z) { + return 1L << ((x + 2) + (y << 5) + (z + 2) * 5); + } + + private boolean visitExposureNeighbor(BlockGetter level, BlockPos origin, Fluid fluid, ByteList stack, byte xOffset, byte yOffset, byte zOffset) { + var upNeighborMask = offsetToMask(xOffset, yOffset, zOffset); + if ((this.visited & upNeighborMask) == 0) { + this.visited |= upNeighborMask; + + var other = this.scratchPos.setWithOffset(origin, xOffset, yOffset, zOffset); + var blockState = level.getBlockState(other); + if (!blockState.isSolidRender()) { + if (!blockState.getFluidState().isSourceOfType(fluid)) { + return true; + } else { + stack.add(xOffset); + stack.add(yOffset); + stack.add(zOffset); + } + } + } + + return false; + } + private static boolean isAlignedEquals(float a, float b) { return Math.abs(a - b) <= ALIGNED_EQUALS_EPSILON; } From 9c9b7559b466bcd5b6ec0d1c299fcb48ff57231c Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 21 Dec 2024 17:50:14 +0100 Subject: [PATCH 29/29] use overlay test for rendering flush inward upper fluid quads, refactor flooded cave heuristic iteration behavior, make inner and outer up quad independently activated for better results in some cases --- .../pipeline/DefaultFluidRenderer.java | 137 +++++++++++++----- 1 file changed, 97 insertions(+), 40 deletions(-) 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 37b71f8fa8..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 @@ -410,8 +410,11 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat } // apply heuristic to not render up face it's in a flooded cave + boolean inwardsUpFaceVisible = true; if (upVisible) { - upVisible = isUpFaceExposedByNeighbors(level, blockPos, fluid); + var exposureResult = getUpFaceExposureByNeighbors(level, blockPos, fluidState); + upVisible = exposureResult != NO_EXPOSURE; + inwardsUpFaceVisible = exposureResult == BOTH_EXPOSED; } if (upVisible) { @@ -493,7 +496,10 @@ && 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); - 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 (downVisible) { @@ -625,68 +631,119 @@ && isAlignedEquals(southEastHeight, southWestHeight) } } + 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 non-solid block that's 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. + * 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 boolean isUpFaceExposedByNeighbors(BlockGetter level, BlockPos origin, Fluid fluid) { + private int getUpFaceExposureByNeighbors(BlockAndTintGetter level, BlockPos origin, FluidState fluidState) { // performs a simple DFS using a stack and a visited bit mask - this.visited = offsetToMask(0, 0, 0); + this.visited = 0; var stack = this.stack; stack.clear(); - stack.add((byte) 0); - stack.add((byte) 0); - stack.add((byte) 0); + + 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 y = stack.removeByte(stack.size() - 1); var x = stack.removeByte(stack.size() - 1); - // traverse into unvisited neighbors in outwards direction - if (y == 0 && visitExposureNeighbor(level, origin, fluid, stack, x, (byte) 1, z)) { - return true; - } - if (x >= 0 && x < 2 && visitExposureNeighbor(level, origin, fluid, stack, (byte) (x + 1), y, z)) { - return true; + // 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 <= 0 && x > -2 && visitExposureNeighbor(level, origin, fluid, stack, (byte) (x - 1), y, z)) { - return true; + if (x > -2) { + result |= visitExposureNeighbor(level, origin, fluidState, stack, (byte) (x - 1), z); + if (result == BOTH_EXPOSED) { + return result; + } } - if (z >= 0 && z < 2 && visitExposureNeighbor(level, origin, fluid, stack, x, y, (byte) (z + 1))) { - return true; + if (z < 2) { + result |= visitExposureNeighbor(level, origin, fluidState, stack, x, (byte) (z + 1)); + if (result == BOTH_EXPOSED) { + return result; + } } - if (z <= 0 && z > -2 && visitExposureNeighbor(level, origin, fluid, stack, x, y, (byte) (z - 1))) { - return true; + if (z > -2) { + result |= visitExposureNeighbor(level, origin, fluidState, stack, x, (byte) (z - 1)); + if (result == BOTH_EXPOSED) { + return result; + } } } - return false; + return result; } - private long offsetToMask(int x, int y, int z) { - return 1L << ((x + 2) + (y << 5) + (z + 2) * 5); + private long offsetToMask(int x, int z) { + return 1L << ((x + 2) + (z + 2) * 5); } - private boolean visitExposureNeighbor(BlockGetter level, BlockPos origin, Fluid fluid, ByteList stack, byte xOffset, byte yOffset, byte zOffset) { - var upNeighborMask = offsetToMask(xOffset, yOffset, zOffset); - if ((this.visited & upNeighborMask) == 0) { - this.visited |= upNeighborMask; - - var other = this.scratchPos.setWithOffset(origin, xOffset, yOffset, zOffset); - var blockState = level.getBlockState(other); - if (!blockState.isSolidRender()) { - if (!blockState.getFluidState().isSourceOfType(fluid)) { - return true; - } else { - stack.add(xOffset); - stack.add(yOffset); - stack.add(zOffset); - } + 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); } } - return false; + // 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) {