Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add hitboxes inside some voxelshapes of cuboids #79

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package me.jellysquid.mods.lithium.common.shapes;

import net.minecraft.util.math.AxisCycleDirection;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.shape.VoxelSet;
import net.minecraft.util.shape.VoxelShape;

/**
* An efficient implementation of {@link VoxelShape} for a shape with one simple cuboid.
* This is an alternative to VoxelShapeSimpleCube with extra hitboxes inside.
* Vanilla has extra hitboxes at steps of 1/8th or 1/4th of a block depending on the exact coordinates of the shape.
* We are mimicking the effect on collisions here, as otherwise some contraptions would not behave like vanilla.
* @author 2No2Name
*/
public class VoxelShapeAlignedCuboid extends VoxelShapeSimpleCube {
//EPSILON for use in cases where it must be a lot smaller than 1/256 and larger than EPSILON
static final double LARGE_EPSILON = 10 * EPSILON;

//In bit aligned shapes the bitset adds segments are between minX/Y/Z and maxX/Y/Z.
//Segments all have the same size. There is an additional collision box between two adjacent segments (if both are inside the shape)
protected final int xSegments;
protected final int ySegments;
protected final int zSegments;

public VoxelShapeAlignedCuboid(VoxelSet voxels, double minX, double minY, double minZ, double maxX, double maxY, double maxZ, int xRes, int yRes, int zRes) {
super(voxels, minX, minY, minZ, maxX, maxY, maxZ);
//If the VoxelShape doesn't contain any extra collision boxes in vanilla on the given axis (only one segment in total)
//We set the segment count to 1 to signal that there are no inside shape segment borders, which is the fast branch in calculatePenetration
this.xSegments = xRes <= 1 ? 1 : (1 << xRes);
this.ySegments = yRes <= 1 ? 1 : (1 << yRes);
this.zSegments = zRes <= 1 ? 1 : (1 << zRes);
}

/**
* Constructor for use in offset() calls.
*/
public VoxelShapeAlignedCuboid(VoxelSet voxels, int xSegments, int ySegments, int zSegments, double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
super(voxels, minX, minY, minZ, maxX, maxY, maxZ);

this.xSegments = xSegments;
this.ySegments = ySegments;
this.zSegments = zSegments;
}

@Override
public VoxelShape offset(double x, double y, double z) {
return new VoxelShapeAlignedCuboid_Offset(this, this.voxels, x, y, z);
}


@Override
public double calculateMaxDistance(AxisCycleDirection cycleDirection, Box box, double maxDist) {
if (Math.abs(maxDist) < EPSILON) {
return 0.0D;
}

double penetration = this.calculatePenetration(cycleDirection, box, maxDist);

if ((penetration != maxDist) && this.intersects(cycleDirection, box)) {
return penetration;
}

return maxDist;
}

private double calculatePenetration(AxisCycleDirection dir, Box box, double maxDist) {
switch (dir) {
case NONE:
return VoxelShapeAlignedCuboid.calculatePenetration(this.minX, this.maxX, this.xSegments, box.minX, box.maxX, maxDist);
case FORWARD:
return VoxelShapeAlignedCuboid.calculatePenetration(this.minZ, this.maxZ, this.zSegments, box.minZ, box.maxZ, maxDist);
case BACKWARD:
return VoxelShapeAlignedCuboid.calculatePenetration(this.minY, this.maxY, this.ySegments, box.minY, box.maxY, maxDist);
default:
throw new IllegalArgumentException();
}
}

/**
* Determine how far the movement is possible.
*/
private static double calculatePenetration(double aMin, double aMax, final int segmentsPerUnit, double bMin, double bMax, double maxDist) {
double gap;

if (maxDist > 0.0D) {
gap = aMin - bMax;

if (gap >= -EPSILON) {
//outside the shape/within margin, move up to/back to boundary
return Math.min(gap, maxDist);
} else {
//already far enough inside this shape to not collide with the surface
if (segmentsPerUnit == 1) {
//no extra segments to collide with, because only one segment in total
return maxDist;
}
//extra segment walls / hitboxes inside this shape, evenly spaced out in 0..1
//round to the next segment wall, but with epsilon margin like vanilla
double wallPos = MathHelper.ceil((bMax - EPSILON) * segmentsPerUnit) / (double)segmentsPerUnit;
//only use the wall when it is actually inside the shape, and not a border / outside the shape
if (wallPos < aMax - LARGE_EPSILON)
return Math.min(maxDist, wallPos - bMax);
return maxDist;
}
} else {
//whole code again, just negated for the other direction
gap = aMax - bMin;

if (gap <= EPSILON) {
//outside the shape/within margin, move up to/back to boundary
return Math.max(gap, maxDist);
} else {
//already far enough inside this shape to not collide with the surface
if (segmentsPerUnit == 1) {
//no extra segments to collide with, because only one segment in total
return maxDist;
}
//extra segment walls / hitboxes inside this shape, evenly spaced out in 0..1
//round to the next segment wall, but with epsilon margin like vanilla
double wallPos = MathHelper.floor((bMin + EPSILON) * segmentsPerUnit) / (double)segmentsPerUnit;
//only use the wall when it is actually inside the shape, and not a border / outside the shape
if (wallPos > aMin + LARGE_EPSILON)
return Math.max(maxDist, wallPos - bMin);
return maxDist;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package me.jellysquid.mods.lithium.common.shapes;

import net.minecraft.util.math.AxisCycleDirection;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.shape.VoxelSet;
import net.minecraft.util.shape.VoxelShape;

public class VoxelShapeAlignedCuboid_Offset extends VoxelShapeAlignedCuboid {
//keep track on how much the voxelSet was offset. minX,maxX,minY are stored offset already
//This is only required to calculate the position of the walls inside the VoxelShape.
//For shapes that are not offset the alignment is 0, but for offset shapes the walls move together with the shape.
private final double xOffset, yOffset, zOffset;
//instead of keeping those variables, equivalent information can probably be recovered from minX, minY, minZ (which are 1/8th of a block aligned), but possibly with additional floating point error

public VoxelShapeAlignedCuboid_Offset(VoxelShapeAlignedCuboid originalShape, VoxelSet voxels, double xOffset, double yOffset, double zOffset) {
super(voxels, originalShape.xSegments, originalShape.ySegments, originalShape.zSegments,
originalShape.minX + xOffset, originalShape.minY + yOffset, originalShape.minZ + zOffset,
originalShape.maxX + xOffset, originalShape.maxY + yOffset, originalShape.maxZ + zOffset);

if (originalShape instanceof VoxelShapeAlignedCuboid_Offset) {
this.xOffset = ((VoxelShapeAlignedCuboid_Offset) originalShape).xOffset + xOffset;
this.yOffset = ((VoxelShapeAlignedCuboid_Offset) originalShape).yOffset + yOffset;
this.zOffset = ((VoxelShapeAlignedCuboid_Offset) originalShape).zOffset + zOffset;
} else {
this.xOffset = xOffset;
this.yOffset = yOffset;
this.zOffset = zOffset;
}
}

@Override
public VoxelShape offset(double x, double y, double z) {
return new VoxelShapeAlignedCuboid_Offset(this, this.voxels, x, y, z);
}

@Override
public double calculateMaxDistance(AxisCycleDirection cycleDirection, Box box, double maxDist) {
if (Math.abs(maxDist) < EPSILON) {
return 0.0D;
}

double penetration = this.calculatePenetration(cycleDirection, box, maxDist);

if ((penetration != maxDist) && this.intersects(cycleDirection, box)) {
return penetration;
}

return maxDist;
}

private double calculatePenetration(AxisCycleDirection dir, Box box, double maxDist) {
switch (dir) {
case NONE:
return VoxelShapeAlignedCuboid_Offset.calculatePenetration(this.minX, this.maxX, this.xSegments, this.xOffset, box.minX, box.maxX, maxDist);
case FORWARD:
return VoxelShapeAlignedCuboid_Offset.calculatePenetration(this.minZ, this.maxZ, this.zSegments, this.zOffset, box.minZ, box.maxZ, maxDist);
case BACKWARD:
return VoxelShapeAlignedCuboid_Offset.calculatePenetration(this.minY, this.maxY, this.ySegments, this.yOffset, box.minY, box.maxY, maxDist);
default:
throw new IllegalArgumentException();
}
}


/**
* Determine how far the movement is possible.
*/
private static double calculatePenetration(double aMin, double aMax, final int segmentsPerUnit, double shapeOffset, double bMin, double bMax, double maxDist) {
double gap;

if (maxDist > 0.0D) {
gap = aMin - bMax;

if (gap >= -EPSILON) {
//outside the shape/within margin, move up to/back to boundary
return Math.min(gap, maxDist);
} else {
//already far enough inside this shape to not collide with the surface
if (segmentsPerUnit == 1) {
//no extra segments to collide with, because only one segment in total
return maxDist;
}
//extra segment walls / hitboxes inside this shape, evenly spaced out in 0..1 + shapeOffset
//round to the next segment wall, but with epsilon margin like vanilla

//using large epsilon and extra check here because +- shapeOffset can cause larger floating point errors
int segment = MathHelper.ceil((bMax - LARGE_EPSILON - shapeOffset) * segmentsPerUnit);
double wallPos = segment / (double) segmentsPerUnit + shapeOffset;
if (wallPos < bMax - EPSILON) {
++segment;
wallPos = segment / (double) segmentsPerUnit + shapeOffset;
}
//only use the wall when it is actually inside the shape, and not a border / outside the shape
if (wallPos < aMax - LARGE_EPSILON)
return Math.min(maxDist, wallPos - bMax);
return maxDist;
}
} else {
//whole code again, just negated for the other direction
gap = aMax - bMin;

if (gap <= EPSILON) {
//outside the shape/within margin, move up to/back to boundary
return Math.max(gap, maxDist);
} else {
//already far enough inside this shape to not collide with the surface
if (segmentsPerUnit == 1) {
//no extra segments to collide with, because only one segment in total
return maxDist;
}
//extra segment walls / hitboxes inside this shape, evenly spaced out in 0..1
//round to the next segment wall, but with epsilon margin like vanilla

//using large epsilon and extra check here because +- shapeOffset can cause larger floating point errors
int segment = MathHelper.floor((bMin + LARGE_EPSILON - shapeOffset) * segmentsPerUnit);
double wallPos = segment / (double) segmentsPerUnit + shapeOffset;
if (wallPos > bMin + EPSILON) {
--segment;
wallPos = segment / (double) segmentsPerUnit + shapeOffset;
}
//only use the wall when it is actually inside the shape, and not a border / outside the shape
if (wallPos > aMin + LARGE_EPSILON)
return Math.max(maxDist, wallPos - bMin);
return maxDist;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import net.minecraft.util.math.Direction;
import net.minecraft.util.shape.VoxelSet;
import net.minecraft.util.shape.VoxelShape;
import net.minecraft.util.shape.VoxelShapes;

import java.util.List;

Expand All @@ -21,9 +22,9 @@
* handling in most cases as block shapes are often nothing more than a single cuboid.
*/
public class VoxelShapeSimpleCube extends VoxelShape implements VoxelShapeCaster {
private static final double EPSILON = 1.0E-7D;
static final double EPSILON = 1.0E-7D;

private final double minX, minY, minZ, maxX, maxY, maxZ;
final double minX, minY, minZ, maxX, maxY, maxZ;

public VoxelShapeSimpleCube(VoxelSet voxels, double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
super(voxels);
Expand Down Expand Up @@ -59,17 +60,17 @@ public double calculateMaxDistance(AxisCycleDirection cycleDirection, Box box, d
private double calculatePenetration(AxisCycleDirection dir, Box box, double maxDist) {
switch (dir) {
case NONE:
return this.calculatePenetration(this.minX, this.maxX, box.minX, box.maxX, maxDist);
return VoxelShapeSimpleCube.calculatePenetration(this.minX, this.maxX, box.minX, box.maxX, maxDist);
case FORWARD:
return this.calculatePenetration(this.minZ, this.maxZ, box.minZ, box.maxZ, maxDist);
return VoxelShapeSimpleCube.calculatePenetration(this.minZ, this.maxZ, box.minZ, box.maxZ, maxDist);
case BACKWARD:
return this.calculatePenetration(this.minY, this.maxY, box.minY, box.maxY, maxDist);
return VoxelShapeSimpleCube.calculatePenetration(this.minY, this.maxY, box.minY, box.maxY, maxDist);
default:
throw new IllegalArgumentException();
}
}

private boolean intersects(AxisCycleDirection dir, Box box) {
boolean intersects(AxisCycleDirection dir, Box box) {
switch (dir) {
case NONE:
return lessThan(this.minY, box.maxY) && lessThan(box.minY, this.maxY) && lessThan(this.minZ, box.maxZ) && lessThan(box.minZ, this.maxZ);
Expand All @@ -82,29 +83,25 @@ private boolean intersects(AxisCycleDirection dir, Box box) {
}
}

private double calculatePenetration(double a1, double a2, double b1, double b2, double maxDist) {
private static double calculatePenetration(double a1, double a2, double b1, double b2, double maxDist) {
double penetration;

if (maxDist > 0.0D) {
penetration = a1 - b2;

if ((penetration < -EPSILON) || (maxDist < penetration)) {
//already far enough inside this shape to not collide with the surface or
//outside the shape and still far enough away for no collision at all
return maxDist;
}

if (penetration < EPSILON) {
return 0.0D;
}
//allow moving up to the shape but not into it. This also includes going backwards by at most EPSILON.
} else {
//whole code again, just negated for the other direction
penetration = a2 - b1;

if ((penetration > EPSILON) || (maxDist > penetration)) {
return maxDist;
}

if (penetration > -EPSILON) {
return 0.0D;
}
}

return penetration;
Expand Down Expand Up @@ -195,4 +192,10 @@ public boolean intersects(Box box, double x, double y, double z) {
(box.minY < (this.maxY + y)) && (box.maxY > (this.minY + y)) &&
(box.minZ < (this.maxZ + z)) && (box.maxZ > (this.minZ + z));
}


@Override
public void forEachBox(VoxelShapes.BoxConsumer boxConsumer) {
boxConsumer.consume(this.minX, this.minY, this.minZ, this.maxX, this.maxY, this.maxZ);
}
}
Loading