Skip to content

Commit

Permalink
Fix hidden surface elimination in fluid rendering for waterlogged blo…
Browse files Browse the repository at this point in the history
…cks (#2907)
  • Loading branch information
douira authored Dec 4, 2024
1 parent 6adc89c commit d818a6e
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import net.minecraft.core.Direction;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.shapes.BooleanOp;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
Expand All @@ -31,9 +32,9 @@ public BlockOcclusionCache() {

/**
* @param selfBlockState The state of the block in the level
* @param view The block view for this render context
* @param selfPos The position of the block
* @param facing The facing direction of the side to check
* @param view The block view for this render context
* @param selfPos The position of the block
* @param facing The facing direction of the side to check
* @return True if the block side facing {@param dir} is not occluded, otherwise false
*/
public boolean shouldDrawSide(BlockState selfBlockState, BlockGetter view, BlockPos selfPos, Direction facing) {
Expand Down Expand Up @@ -88,6 +89,75 @@ private static boolean isEmptyShape(VoxelShape voxelShape) {
return voxelShape == Shapes.empty() || voxelShape.isEmpty();
}

/**
* Checks if a face of a fluid block should be rendered. It takes into account both occluding fluid face against its own waterlogged block state and the neighboring block state. This is an approximation that doesn't check voxel for shapes between the fluid and the neighboring block since that is handled by the fluid renderer separately and more accurately using actual fluid heights. It only uses voxel shape comparison for checking self-occlusion with the waterlogged block state.
*
* @param selfBlockState The state of the block in the level
* @param view The block view for this render context
* @param selfPos The position of the fluid
* @param facing The facing direction of the side to check
* @param fluid The fluid state
* @param fluidShape The non-empty shape of the fluid
* @return True if the fluid side facing {@param dir} is not occluded, otherwise false
*/
public boolean shouldDrawFullBlockFluidSide(BlockState selfBlockState, BlockGetter view, BlockPos selfPos, Direction facing, FluidState fluid, VoxelShape fluidShape) {
var fluidShapeIsBlock = fluidShape == Shapes.block();

// only perform self-occlusion if the own block state can't occlude
if (selfBlockState.canOcclude()) {
var selfShape = selfBlockState.getFaceOcclusionShape(facing);

// only a non-empty self-shape can occlude anything
if (!isEmptyShape(selfShape)) {
// a full self-shape occludes everything
if (isFullShape(selfShape) && fluidShapeIsBlock) {
return false;
}

// perform occlusion of the fluid by the block it's contained in
if (!this.lookup(fluidShape, selfShape)) {
return false;
}
}
}

// perform occlusion against the neighboring block
BlockPos.MutableBlockPos otherPos = this.cachedPositionObject;
otherPos.set(selfPos.getX() + facing.getStepX(), selfPos.getY() + facing.getStepY(), selfPos.getZ() + facing.getStepZ());
BlockState otherState = view.getBlockState(otherPos);

// don't render anything if the other blocks is the same fluid
if (otherState.getFluidState() == fluid) {
return false;
}

// check for special fluid occlusion behavior
if (PlatformBlockAccess.getInstance().shouldOccludeFluid(facing.getOpposite(), otherState, fluid)) {
return false;
}

// the up direction doesn't do occlusion with other block shapes
if (facing == Direction.UP) {
return true;
}

// only occlude against blocks that can potentially occlude in the first place
if (!otherState.canOcclude()) {
return true;
}

var otherShape = otherState.getFaceOcclusionShape(facing.getOpposite());

// If the other block has an empty cull shape, then it cannot hide any geometry
if (isEmptyShape(otherShape)) {
return true;
}

// If both blocks use a full-cube cull shape, then they will always hide the faces between each other.
// No voxel shape comparison is done after this point because it's redundant with the later more accurate check.
return !isFullShape(otherShape) || !fluidShapeIsBlock;
}

private boolean lookup(VoxelShape self, VoxelShape other) {
ShapeComparison comparison = this.cachedComparisonObject;
comparison.self = self;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import net.caffeinemc.mods.sodium.api.util.ColorARGB;
import net.caffeinemc.mods.sodium.api.util.NormI8;
import net.caffeinemc.mods.sodium.client.model.color.ColorProvider;
import net.caffeinemc.mods.sodium.client.model.color.ColorProviderRegistry;
import net.caffeinemc.mods.sodium.client.model.light.LightMode;
import net.caffeinemc.mods.sodium.client.model.light.LightPipeline;
import net.caffeinemc.mods.sodium.client.model.light.LightPipelineProvider;
Expand All @@ -28,8 +27,6 @@
import net.minecraft.tags.FluidTags;
import net.minecraft.util.Mth;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.LiquidBlock;
import net.minecraft.world.level.block.SupportType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
Expand All @@ -49,6 +46,8 @@ public class DefaultFluidRenderer {
private final MutableFloat scratchHeight = new MutableFloat(0);
private final MutableInt scratchSamples = new MutableInt();

private final BlockOcclusionCache occlusionCache = new BlockOcclusionCache();

private final ModelQuadViewMutable quad = new ModelQuad();

private final LightPipelineProvider lighters;
Expand All @@ -65,21 +64,10 @@ public DefaultFluidRenderer(LightPipelineProvider lighters) {
this.lighters = lighters;
}

private boolean isFluidOccluded(BlockAndTintGetter world, int x, int y, int z, Direction dir, BlockState blockState, FluidState fluid) {
//Test own block state first, this prevents waterlogged blocks from having hidden internal geometry
// which can result in z-fighting
var pos = this.scratchPos.set(x, y, z);
if (blockState.canOcclude() && blockState.isFaceSturdy(world, pos, dir, SupportType.FULL)) {
return true;
}

//Test neighboring block state
var adjPos = this.scratchPos.set(x + dir.getStepX(), y + dir.getStepY(), z + dir.getStepZ());
BlockState adjBlockState = world.getBlockState(adjPos);
if (PlatformBlockAccess.getInstance().shouldOccludeFluid(dir.getOpposite(), adjBlockState, fluid)) {
return true;
}
return adjBlockState.canOcclude() && dir != Direction.UP && adjBlockState.isFaceSturdy(world, adjPos, dir.getOpposite(), SupportType.FULL);
private boolean isFullBlockFluidOccluded(BlockAndTintGetter world, BlockPos pos, Direction dir, BlockState blockState, FluidState fluid) {
// check if this face of the fluid, assuming a full-block cull shape, is occluded by the block it's in or a neighboring block.
// it doesn't do a voxel shape comparison with the neighboring blocks since that is already done by isSideExposed
return !this.occlusionCache.shouldDrawFullBlockFluidSide(blockState, world, pos, dir, fluid, Shapes.block());
}

private boolean isSideExposed(BlockAndTintGetter world, int x, int y, int z, Direction dir, float height) {
Expand Down Expand Up @@ -109,15 +97,16 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat

Fluid fluid = fluidState.getType();

boolean sfUp = this.isFluidOccluded(level, posX, posY, posZ, Direction.UP, blockState, fluidState);
boolean sfDown = this.isFluidOccluded(level, posX, posY, posZ, Direction.DOWN, blockState, fluidState) ||
boolean cullUp = this.isFullBlockFluidOccluded(level, blockPos, Direction.UP, blockState, fluidState);
boolean cullDown = this.isFullBlockFluidOccluded(level, blockPos, Direction.DOWN, blockState, fluidState) ||
!this.isSideExposed(level, posX, posY, posZ, Direction.DOWN, 0.8888889F);
boolean sfNorth = this.isFluidOccluded(level, posX, posY, posZ, Direction.NORTH, blockState, fluidState);
boolean sfSouth = this.isFluidOccluded(level, posX, posY, posZ, Direction.SOUTH, blockState, fluidState);
boolean sfWest = this.isFluidOccluded(level, posX, posY, posZ, Direction.WEST, blockState, fluidState);
boolean sfEast = this.isFluidOccluded(level, posX, posY, posZ, Direction.EAST, blockState, fluidState);
boolean cullNorth = this.isFullBlockFluidOccluded(level, blockPos, Direction.NORTH, blockState, fluidState);
boolean cullSouth = this.isFullBlockFluidOccluded(level, blockPos, Direction.SOUTH, blockState, fluidState);
boolean cullWest = this.isFullBlockFluidOccluded(level, blockPos, Direction.WEST, blockState, fluidState);
boolean cullEast = this.isFullBlockFluidOccluded(level, blockPos, Direction.EAST, blockState, fluidState);

if (sfUp && sfDown && sfEast && sfWest && sfNorth && sfSouth) {
// stop rendering if all faces of the fluid are occluded
if (cullUp && cullDown && cullEast && cullWest && cullNorth && cullSouth) {
return;
}

Expand Down Expand Up @@ -149,7 +138,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat
.move(Direction.NORTH)
.move(Direction.EAST));
}
float yOffset = sfDown ? 0.0F : EPSILON;
float yOffset = cullDown ? 0.0F : EPSILON;

final ModelQuadViewMutable quad = this.quad;

Expand All @@ -158,7 +147,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat

quad.setFlags(0);

if (!sfUp && this.isSideExposed(level, posX, posY, posZ, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)))) {
if (!cullUp && this.isSideExposed(level, posX, posY, posZ, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)))) {
northWestHeight -= EPSILON;
southWestHeight -= EPSILON;
southEastHeight -= EPSILON;
Expand Down Expand Up @@ -243,7 +232,7 @@ && isAlignedEquals(southEastHeight, southWestHeight)
}
}

if (!sfDown) {
if (!cullDown) {
TextureAtlasSprite sprite = sprites[0];

float minU = sprite.getU0();
Expand Down Expand Up @@ -273,7 +262,7 @@ && isAlignedEquals(southEastHeight, southWestHeight)

switch (dir) {
case NORTH -> {
if (sfNorth) {
if (cullNorth) {
continue;
}
c1 = northWestHeight;
Expand All @@ -284,7 +273,7 @@ && isAlignedEquals(southEastHeight, southWestHeight)
z2 = z1;
}
case SOUTH -> {
if (sfSouth) {
if (cullSouth) {
continue;
}
c1 = southEastHeight;
Expand All @@ -295,7 +284,7 @@ && isAlignedEquals(southEastHeight, southWestHeight)
z2 = z1;
}
case WEST -> {
if (sfWest) {
if (cullWest) {
continue;
}
c1 = southWestHeight;
Expand All @@ -306,7 +295,7 @@ && isAlignedEquals(southEastHeight, southWestHeight)
z2 = 0.0f;
}
case EAST -> {
if (sfEast) {
if (cullEast) {
continue;
}
c1 = northEastHeight;
Expand Down

0 comments on commit d818a6e

Please sign in to comment.