Oriented bouncing and sliding

- Slime blocks on a moving Contraption are now bouncy
- Ice blocks on moving Contraptions are now very slippery
This commit is contained in:
simibubi 2021-03-31 03:16:29 +02:00
parent 4ec25798a5
commit 0bb18db4b6
4 changed files with 227 additions and 70 deletions

View file

@ -10,19 +10,18 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.simibubi.create.foundation.render.backend.instancing.IFlywheelWorld;
import com.simibubi.create.foundation.render.backend.light.GridAlignedBB;
import com.simibubi.create.foundation.utility.*;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
@ -55,7 +54,15 @@ import com.simibubi.create.content.logistics.block.inventories.AdjustableCrateBl
import com.simibubi.create.content.logistics.block.redstone.RedstoneContactBlock;
import com.simibubi.create.foundation.config.AllConfigs;
import com.simibubi.create.foundation.fluid.CombinedTankWrapper;
import com.simibubi.create.foundation.render.backend.instancing.IFlywheelWorld;
import com.simibubi.create.foundation.render.backend.light.EmptyLighter;
import com.simibubi.create.foundation.render.backend.light.GridAlignedBB;
import com.simibubi.create.foundation.utility.BlockFace;
import com.simibubi.create.foundation.utility.Coordinate;
import com.simibubi.create.foundation.utility.Iterate;
import com.simibubi.create.foundation.utility.NBTHelper;
import com.simibubi.create.foundation.utility.NBTProcessors;
import com.simibubi.create.foundation.utility.UniqueLinkedList;
import com.simibubi.create.foundation.utility.worldWrappers.WrappedWorld;
import net.minecraft.block.AbstractButtonBlock;
@ -86,6 +93,9 @@ import net.minecraft.util.Rotation;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.math.shapes.IBooleanFunction;
import net.minecraft.util.math.shapes.VoxelShape;
import net.minecraft.util.math.shapes.VoxelShapes;
import net.minecraft.util.palette.PaletteHashMap;
import net.minecraft.village.PointOfInterestType;
import net.minecraft.world.IWorld;
@ -106,6 +116,7 @@ import net.minecraftforge.registries.GameData;
public abstract class Contraption {
public Optional<List<AxisAlignedBB>> simplifiedEntityColliders;
public AbstractContraptionEntity entity;
public CombinedInvWrapper inventory;
public CombinedTankWrapper fluidInventory;
@ -126,6 +137,8 @@ public abstract class Contraption {
private Map<BlockPos, Entity> initialPassengers;
private List<BlockFace> pendingSubContraptions;
private CompletableFuture<List<AxisAlignedBB>> simplifiedEntityColliderProvider;
// Client
public Map<BlockPos, TileEntity> presentTileEntities;
public List<TileEntity> maybeInstancedTileEntities;
@ -148,13 +161,12 @@ public abstract class Contraption {
specialRenderedTileEntities = new ArrayList<>();
pendingSubContraptions = new ArrayList<>();
stabilizedSubContraptions = new HashMap<>();
simplifiedEntityColliders = Optional.empty();
}
public ContraptionWorld getContraptionWorld() {
if (world == null) {
if (world == null)
world = new ContraptionWorld(entity.world, this);
}
return world;
}
@ -181,6 +193,7 @@ public abstract class Contraption {
String type = nbt.getString("Type");
Contraption contraption = ContraptionType.fromType(type);
contraption.readNBT(world, nbt, spawnData);
contraption.gatherBBsOffThread();
return contraption;
}
@ -244,6 +257,7 @@ public abstract class Contraption {
.collect(Collectors.toList());
fluidInventory = new CombinedTankWrapper(
Arrays.copyOf(fluidHandlers.toArray(), fluidHandlers.size(), IFluidHandler[].class));
gatherBBsOffThread();
}
public void onEntityInitialize(World world, AbstractContraptionEntity contraptionEntity) {
@ -268,6 +282,15 @@ public abstract class Contraption {
}
public void onEntityTick(World world) {
if (simplifiedEntityColliderProvider != null && simplifiedEntityColliderProvider.isDone()) {
try {
simplifiedEntityColliders = Optional.of(simplifiedEntityColliderProvider.join());
} catch (Exception e) {
e.printStackTrace();
}
simplifiedEntityColliderProvider = null;
}
fluidStorage.forEach((pos, mfs) -> mfs.tick(entity, pos, world.isRemote));
}
@ -1158,6 +1181,22 @@ public abstract class Contraption {
return new EmptyLighter(this);
}
private void gatherBBsOffThread() {
simplifiedEntityColliderProvider = CompletableFuture.supplyAsync(() -> {
VoxelShape combinedShape = VoxelShapes.empty();
for (Entry<BlockPos, BlockInfo> entry : blocks.entrySet()) {
BlockInfo info = entry.getValue();
BlockPos localPos = entry.getKey();
VoxelShape collisionShape = info.state.getCollisionShape(this.world, localPos);
if (collisionShape.isEmpty())
continue;
combinedShape = VoxelShapes.combineAndSimplify(combinedShape,
collisionShape.withOffset(localPos.getX(), localPos.getY(), localPos.getZ()), IBooleanFunction.OR);
}
return combinedShape.toBoundingBoxList();
});
}
public static float getRadius(Set<BlockPos> blocks, Direction.Axis axis) {
switch (axis) {
case X:

View file

@ -23,6 +23,7 @@ import com.simibubi.create.foundation.collision.ContinuousOBBCollider.Continuous
import com.simibubi.create.foundation.collision.Matrix3d;
import com.simibubi.create.foundation.collision.OrientedBB;
import com.simibubi.create.foundation.networking.AllPackets;
import com.simibubi.create.foundation.utility.BlockHelper;
import com.simibubi.create.foundation.utility.Iterate;
import com.simibubi.create.foundation.utility.VecHelper;
@ -37,6 +38,8 @@ import net.minecraft.util.Direction;
import net.minecraft.util.Direction.Axis;
import net.minecraft.util.Direction.AxisDirection;
import net.minecraft.util.ReuseableStream;
import net.minecraft.util.SoundCategory;
import net.minecraft.util.SoundEvents;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
@ -104,40 +107,45 @@ public class ContraptionCollider {
AxisAlignedBB entityBounds = entity.getBoundingBox();
Vec3d motion = entity.getMotion();
float yawOffset = rotation.getYawOffset();
Vec3d position = getWorldToLocalTranslation(entity, anchorVec, rotationMatrix, yawOffset);
// Find all potential block shapes to collide with
// Prepare entity bounds
AxisAlignedBB localBB = entityBounds.offset(position)
.grow(1.0E-7D);
ReuseableStream<VoxelShape> potentialHits =
getPotentiallyCollidedShapes(world, contraption, localBB.expand(motion));
if (potentialHits.createStream()
.count() == 0)
continue;
// Prepare entity bounds
OrientedBB obb = new OrientedBB(localBB);
obb.setRotation(rotationMatrix);
motion = motion.subtract(contraptionMotion);
motion = rotationMatrix.transform(motion);
// Use simplified bbs when present
// TODO: is it worth filtering out far away bbs?
final Vec3d motionCopy = motion;
List<AxisAlignedBB> collidableBBs = contraption.simplifiedEntityColliders.orElseGet(() -> {
// Else find 'nearby' individual block shapes to collide with
List<AxisAlignedBB> bbs = new ArrayList<>();
ReuseableStream<VoxelShape> potentialHits =
getPotentiallyCollidedShapes(world, contraption, localBB.expand(motionCopy));
potentialHits.createStream()
.forEach(shape -> shape.toBoundingBoxList()
.forEach(bbs::add));
return bbs;
});
MutableObject<Vec3d> collisionResponse = new MutableObject<>(Vec3d.ZERO);
MutableObject<Vec3d> normal = new MutableObject<>(Vec3d.ZERO);
MutableObject<Vec3d> location = new MutableObject<>(Vec3d.ZERO);
MutableBoolean surfaceCollision = new MutableBoolean(false);
MutableFloat temporalResponse = new MutableFloat(1);
Vec3d obbCenter = obb.getCenter();
// Apply separation maths
List<AxisAlignedBB> bbs = new ArrayList<>();
potentialHits.createStream()
.forEach(shape -> shape.toBoundingBoxList()
.forEach(bbs::add));
boolean doHorizontalPass = !rotation.hasVerticalRotation();
for (boolean horizontalPass : Iterate.trueAndFalse) {
boolean verticalPass = !horizontalPass || !doHorizontalPass;
for (AxisAlignedBB bb : bbs) {
for (AxisAlignedBB bb : collidableBBs) {
Vec3d currentResponse = collisionResponse.getValue();
obb.setCenter(obbCenter.add(currentResponse));
ContinuousSeparationManifold intersect = obb.intersect(bb, motion);
@ -148,15 +156,28 @@ public class ContraptionCollider {
surfaceCollision.setValue(intersect.isSurfaceCollision());
double timeOfImpact = intersect.getTimeOfImpact();
if (timeOfImpact > 0 && timeOfImpact < 1) {
if (temporalResponse.getValue() > timeOfImpact)
temporalResponse.setValue(timeOfImpact);
continue;
boolean isTemporal = timeOfImpact > 0 && timeOfImpact < 1;
Vec3d collidingNormal = intersect.getCollisionNormal();
Vec3d collisionPosition = intersect.getCollisionPosition();
if (!isTemporal) {
Vec3d separation = intersect.asSeparationVec(entity.stepHeight);
if (separation != null && !separation.equals(Vec3d.ZERO)) {
collisionResponse.setValue(currentResponse.add(separation));
timeOfImpact = 0;
}
}
Vec3d separation = intersect.asSeparationVec(entity.stepHeight);
if (separation != null && !separation.equals(Vec3d.ZERO))
collisionResponse.setValue(currentResponse.add(separation));
boolean nearest = timeOfImpact >= 0 && temporalResponse.getValue() > timeOfImpact;
if (collidingNormal != null && nearest)
normal.setValue(collidingNormal);
if (collisionPosition != null && nearest)
location.setValue(collisionPosition);
if (isTemporal) {
if (temporalResponse.getValue() > timeOfImpact)
temporalResponse.setValue(timeOfImpact);
}
}
if (verticalPass)
@ -175,6 +196,9 @@ public class ContraptionCollider {
// Resolve collision
Vec3d entityMotion = entity.getMotion();
Vec3d entityMotionNoTemporal = entityMotion;
Vec3d collisionNormal = normal.getValue();
Vec3d collisionLocation = location.getValue();
Vec3d totalResponse = collisionResponse.getValue();
boolean hardCollision = !totalResponse.equals(Vec3d.ZERO);
boolean temporalCollision = temporalResponse.getValue() != 1;
@ -187,8 +211,47 @@ public class ContraptionCollider {
.add(contraptionMotion);
totalResponse = rotationMatrix.transform(totalResponse);
totalResponse = VecHelper.rotate(totalResponse, yawOffset, Axis.Y);
collisionNormal = rotationMatrix.transform(collisionNormal);
collisionNormal = VecHelper.rotate(collisionNormal, yawOffset, Axis.Y);
collisionLocation = rotationMatrix.transform(collisionLocation);
collisionLocation = VecHelper.rotate(collisionLocation, yawOffset, Axis.Y);
rotationMatrix.transpose();
double bounce = 0;
double slide = 0;
if (!collisionLocation.equals(Vec3d.ZERO)) {
collisionLocation = collisionLocation.add(entity.getPositionVec()
.add(entity.getBoundingBox()
.getCenter())
.scale(.5f));
if (temporalCollision)
collisionLocation = collisionLocation.add(0, motionResponse.y, 0);
BlockPos pos = new BlockPos(contraptionEntity.toLocalVector(collisionLocation, 0));
if (contraption.getBlocks()
.containsKey(pos)) {
BlockState blockState = contraption.getBlocks()
.get(pos).state;
bounce = BlockHelper.getBounceMultiplier(blockState.getBlock());
slide = Math.max(0, blockState.getSlipperiness(contraption.world, pos, entity) - .6f);
}
}
boolean hasNormal = !collisionNormal.equals(Vec3d.ZERO);
boolean anyCollision = hardCollision || temporalCollision;
if (bounce > 0 && hasNormal && anyCollision) {
collisionNormal = collisionNormal.normalize();
Vec3d newNormal = collisionNormal.crossProduct(collisionNormal.crossProduct(entityMotionNoTemporal))
.normalize();
if (bounceEntity(entity, newNormal, contraptionEntity, bounce)) {
entity.world.playSound(playerType == PlayerType.CLIENT ? (PlayerEntity) entity : null,
entity.getX(), entity.getY(), entity.getZ(), SoundEvents.BLOCK_SLIME_BLOCK_FALL,
SoundCategory.BLOCKS, .5f, 1);
continue;
}
}
if (temporalCollision) {
double idealVerticalMotion = motionResponse.y;
if (idealVerticalMotion != entityMotion.y) {
@ -216,6 +279,18 @@ public class ContraptionCollider {
entityMotion = entityMotion.mul(1, 1, 0);
}
if (bounce == 0 && slide > 0 && hasNormal && anyCollision && rotation.hasVerticalRotation()) {
collisionNormal = collisionNormal.normalize();
Vec3d motionIn = entityMotionNoTemporal.mul(0, 1, 0)
.add(0, -.01f, 0);
Vec3d slideNormal = collisionNormal.crossProduct(motionIn.crossProduct(collisionNormal))
.normalize();
entity.setMotion(entityMotion.mul(.8, 0, .8)
.add(slideNormal.scale((.2f + slide) * motionIn.length())
.add(0, -0.1, 0)));
entityMotion = entity.getMotion();
}
if (!hardCollision && surfaceCollision.isFalse())
continue;
@ -229,10 +304,14 @@ public class ContraptionCollider {
if (surfaceCollision.isTrue()) {
entity.fallDistance = 0;
entity.onGround = true;
contraptionEntity.collidingEntities.put(entity, new MutableInt(0));
if (entity instanceof ItemEntity)
entityMotion = entityMotion.mul(.5f, 1, .5f);
boolean canWalk = bounce != 0 || slide == 0;
if (canWalk || !rotation.hasVerticalRotation()) {
if (canWalk)
entity.onGround = true;
if (entity instanceof ItemEntity)
entityMotion = entityMotion.mul(.5f, 1, .5f);
}
contactPointMotion = contraptionEntity.getContactPointMotion(entityPosition);
allowedMovement = getAllowedMovement(contactPointMotion, entity);
entity.setPosition(entityPosition.x + allowedMovement.x, entityPosition.y,
@ -254,6 +333,42 @@ public class ContraptionCollider {
}
static boolean bounceEntity(Entity entity, Vec3d normal, AbstractContraptionEntity contraption, double factor) {
if (factor == 0)
return false;
if (entity.bypassesLandingEffects())
return false;
if (normal.equals(Vec3d.ZERO))
return false;
Vec3d contraptionVec = Vec3d.ZERO;
Vec3d contactPointMotion = contraption.getContactPointMotion(entity.getPositionVec());
Vec3d motion = entity.getMotion()
.subtract(contactPointMotion);
Vec3d v2 = motion.crossProduct(normal)
.normalize();
if (v2 != Vec3d.ZERO)
contraptionVec = normal.scale(contraptionVec.dotProduct(normal))
.add(v2.scale(contraptionVec.dotProduct(v2)));
else
v2 = normal.crossProduct(normal.add(Math.random(), Math.random(), Math.random()))
.normalize();
Vec3d v3 = normal.crossProduct(v2);
motion = motion.subtract(contraptionVec);
Vec3d lr = new Vec3d(factor * motion.dotProduct(normal), -motion.dotProduct(v2), -motion.dotProduct(v3));
if (lr.dotProduct(lr) > 1 / 16f) {
Vec3d newMot = contactPointMotion.add(normal.x * lr.x + v2.x * lr.y + v3.x * lr.z,
normal.y * lr.x + v2.y * lr.y + v3.y * lr.z, normal.z * lr.x + v2.z * lr.y + v3.z * lr.z);
entity.setMotion(newMot);
return true;
}
return false;
}
public static Vec3d getWorldToLocalTranslation(Entity entity, AbstractContraptionEntity contraptionEntity) {
return getWorldToLocalTranslation(entity, contraptionEntity.getAnchorVec(), contraptionEntity.getRotationState());
}

View file

@ -35,24 +35,25 @@ public class ContinuousOBBCollider extends OBBCollider {
checkCount = 0;
mf.stepSeparationAxis = uB1;
mf.stepSeparation = Double.MAX_VALUE;
mf.normalSeparation = Double.MAX_VALUE;
if (
// Separate along A's local axes (global XYZ)
!(separate(mf, uA0, diff.x, eA.x, a00 * eB.x + a01 * eB.y + a02 * eB.z, motion.x)
|| separate(mf, uA1, diff.y, eA.y, a10 * eB.x + a11 * eB.y + a12 * eB.z, motion.y)
|| separate(mf, uA2, diff.z, eA.z, a20 * eB.x + a21 * eB.y + a22 * eB.z, motion.z)
!(separate(mf, uA0, diff.x, eA.x, a00 * eB.x + a01 * eB.y + a02 * eB.z, motion.x, true)
|| separate(mf, uA1, diff.y, eA.y, a10 * eB.x + a11 * eB.y + a12 * eB.z, motion.y, true)
|| separate(mf, uA2, diff.z, eA.z, a20 * eB.x + a21 * eB.y + a22 * eB.z, motion.z, true)
// Separate along B's local axes
|| separate(mf, uB0, diff2.x, eA.x * a00 + eA.y * a10 + eA.z * a20, eB.x, motion2.x)
|| separate(mf, uB1, diff2.y, eA.x * a01 + eA.y * a11 + eA.z * a21, eB.y, motion2.y)
|| separate(mf, uB2, diff2.z, eA.x * a02 + eA.y * a12 + eA.z * a22, eB.z, motion2.z)))
|| separate(mf, uB0, diff2.x, eA.x * a00 + eA.y * a10 + eA.z * a20, eB.x, motion2.x, false)
|| separate(mf, uB1, diff2.y, eA.x * a01 + eA.y * a11 + eA.z * a21, eB.y, motion2.y, false)
|| separate(mf, uB2, diff2.z, eA.x * a02 + eA.y * a12 + eA.z * a22, eB.z, motion2.z, false)))
return mf;
return null;
}
static boolean separate(ContinuousSeparationManifold mf, Vec3d axis, double TL, double rA, double rB,
double projectedMotion) {
double projectedMotion, boolean axisOfObjA) {
checkCount++;
double distance = abs(TL);
double diff = distance - (rA + rB);
@ -81,7 +82,12 @@ public class ContinuousOBBCollider extends OBBCollider {
Vec3d normalizedAxis = axis.normalize();
boolean isBestSeperation = distance != 0 && -(diff) <= abs(mf.separation);
// boolean isBestSeperation = discreteCollision && checkCount == 5; // Debug specific separations
// boolean isBestSeperation = discreteCollision && checkCount == 5; // Debug specific separations
if (axisOfObjA && distance != 0 && -(diff) <= abs(mf.normalSeparation)) {
mf.normalAxis = normalizedAxis;
mf.normalSeparation = seperation;
}
double dot = mf.stepSeparationAxis.dotProduct(axis);
if (dot != 0 && discreteCollision) {
@ -97,45 +103,19 @@ public class ContinuousOBBCollider extends OBBCollider {
stepSeparationVec =
sepVec.subtract(axisPlane.scale(sepVec.dotProduct(stepPlane) / axisPlane.dotProduct(stepPlane)));
stepSeparation = stepSeparationVec.length();
if (abs(mf.stepSeparation) > abs(stepSeparation) && stepSeparation != 0) {
// CollisionDebugger.showDebugLine(Vec3d.ZERO, sepVec, 0x111155, "stepsep", -16);
if (abs(mf.stepSeparation) > abs(stepSeparation) && stepSeparation != 0)
mf.stepSeparation = stepSeparation;
}
} else {
if (abs(mf.stepSeparation) > stepSeparation) {
if (abs(mf.stepSeparation) > stepSeparation)
mf.stepSeparation = stepSeparation;
// CollisionDebugger.showDebugLine(Vec3d.ZERO, stepSeparationVec, 0xff9999, "axis", -16);
}
}
// if (abs(mf.separation) < abs(stepSeparation) && stepSeparation != 0)
}
if (isBestSeperation) {
mf.axis = normalizedAxis;
mf.separation = seperation;
// Visualize values
// if (CollisionDebugger.AABB != null) {
// Vec3d normalizedAxis = axis.normalize();
// showDebugLine(Vec3d.ZERO, normalizedAxis.scale(projectedMotion), 0x111155, "motion", 5);
// showDebugLine(Vec3d.ZERO, normalizedAxis.scale(TL), 0xbb00bb, "tl", 4);
// showDebugLine(Vec3d.ZERO, normalizedAxis.scale(sTL * rA), 0xff4444, "ra", 3);
// showDebugLine(normalizedAxis.scale(sTL * rA),
// normalizedAxis.scale(sTL * rA - entryTime * projectedMotion), 0x44ff44, "entry", 0);
// showDebugLine(normalizedAxis.scale(sTL * rA - entryTime * projectedMotion),
// normalizedAxis.scale(sTL * rA - entryTime * projectedMotion + exitTime * projectedMotion), 0x44ffff,
// "exit", -1);
// showDebugLine(normalizedAxis.scale(sTL * (distance - rB)), normalizedAxis.scale(TL), 0x4444ff, "rb", 2);
// showDebugLine(normalizedAxis.scale(sTL * (distance - rB)),
// normalizedAxis.scale(sTL * (distance - rB) + value), 0xff9966, "separation", 1);
//// System.out.println("TL:" + TL + ", rA: " + rA + ", rB: " + rB);
// }
mf.collisionPosition = normalizedAxis.scale(signum(TL) * (axisOfObjA ? -rB : -rB) - signum(seperation) * .125f);
}
return false;
@ -147,10 +127,14 @@ public class ContinuousOBBCollider extends OBBCollider {
double latestCollisionEntryTime = UNDEFINED;
double earliestCollisionExitTime = Double.MAX_VALUE;
boolean isDiscreteCollision = true;
Vec3d collisionPosition;
Vec3d stepSeparationAxis;
double stepSeparation;
Vec3d normalAxis;
double normalSeparation;
public double getTimeOfImpact() {
if (latestCollisionEntryTime == UNDEFINED)
return UNDEFINED;
@ -163,6 +147,14 @@ public class ContinuousOBBCollider extends OBBCollider {
return true;
}
public Vec3d getCollisionNormal() {
return normalAxis == null ? null : createSeparationVec(normalSeparation, normalAxis);
}
public Vec3d getCollisionPosition() {
return collisionPosition;
}
public Vec3d asSeparationVec(double obbStepHeight) {
if (isDiscreteCollision) {
if (stepSeparation <= obbStepHeight)

View file

@ -8,10 +8,13 @@ import org.apache.commons.lang3.mutable.MutableInt;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.content.contraptions.base.KineticTileEntity;
import com.simibubi.create.content.contraptions.components.actors.SeatBlock;
import net.minecraft.block.BedBlock;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.SlimeBlock;
import net.minecraft.client.particle.DiggingParticle;
import net.minecraft.client.particle.ParticleManager;
import net.minecraft.entity.player.PlayerEntity;
@ -256,4 +259,12 @@ public class BlockHelper {
}
}
public static double getBounceMultiplier(Block block) {
if (block instanceof SlimeBlock)
return 0.8D;
if (block instanceof BedBlock || block instanceof SeatBlock)
return 0.66 * 0.8D;
return 0;
}
}