From e0b04f0efd088c8c065884080f313d32a0e750e8 Mon Sep 17 00:00:00 2001 From: Adrien1106 Date: Mon, 22 Jul 2024 18:54:46 +0200 Subject: [PATCH] feat: added complex shape raycasting --- build.gradle | 1 + .../java/fr/adrien1106/reframed/ReFramed.java | 1 + .../adrien1106/reframed/block/TestBlock.java | 36 +++++++ .../reframed/client/util/RaycastResult.java | 7 ++ .../reframed/client/voxel/MorphBox.java | 42 ++++++++ .../reframed/client/voxel/MorphVoxel.java | 100 ++++++++++++++++++ .../reframed/client/voxel/Triangle.java | 78 ++++++++++++++ .../reframed/generator/GBlockstate.java | 2 + .../reframed/generator/block/Test.java | 26 +++++ src/main/resources/fabric.mod.json | 1 + src/main/resources/reframed.accesswidener | 4 + 11 files changed, 298 insertions(+) create mode 100644 src/main/java/fr/adrien1106/reframed/block/TestBlock.java create mode 100644 src/main/java/fr/adrien1106/reframed/client/util/RaycastResult.java create mode 100644 src/main/java/fr/adrien1106/reframed/client/voxel/MorphBox.java create mode 100644 src/main/java/fr/adrien1106/reframed/client/voxel/MorphVoxel.java create mode 100644 src/main/java/fr/adrien1106/reframed/client/voxel/Triangle.java create mode 100644 src/main/java/fr/adrien1106/reframed/generator/block/Test.java create mode 100644 src/main/resources/reframed.accesswidener diff --git a/build.gradle b/build.gradle index 1cd7aff..045adfc 100755 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ sourceSets { } loom { + accessWidenerPath = file("src/main/resources/reframed.accesswidener") runs { // This adds a new gradle task that runs the datagen API: "gradlew runDatagen" datagen { diff --git a/src/main/java/fr/adrien1106/reframed/ReFramed.java b/src/main/java/fr/adrien1106/reframed/ReFramed.java index 04c08ff..81a2ca0 100644 --- a/src/main/java/fr/adrien1106/reframed/ReFramed.java +++ b/src/main/java/fr/adrien1106/reframed/ReFramed.java @@ -103,6 +103,7 @@ public class ReFramed implements ModInitializer { BLUEPRINT = registerItem("blueprint" , new ReFramedBlueprintItem(new Item.Settings())); BLUEPRINT_WRITTEN = registerItem("blueprint_written" , new ReFramedBlueprintWrittenItem(new Item.Settings().maxCount(1))); + registerBlock("test", new TestBlock(cp(Blocks.OAK_PLANKS).nonOpaque())); // TODO remove REFRAMED_BLOCK_ENTITY = Registry.register(Registries.BLOCK_ENTITY_TYPE, id("camo"), BlockEntityType.Builder.create( diff --git a/src/main/java/fr/adrien1106/reframed/block/TestBlock.java b/src/main/java/fr/adrien1106/reframed/block/TestBlock.java new file mode 100644 index 0000000..220e50c --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/block/TestBlock.java @@ -0,0 +1,36 @@ +package fr.adrien1106.reframed.block; + +import fr.adrien1106.reframed.client.voxel.MorphVoxel; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.ShapeContext; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.BlockView; + +public class TestBlock extends Block { + + public static MorphVoxel voxel = new MorphVoxel(createCuboidShape(1, 1, 1, 15, 15, 15), + new Vec3d[] { + new Vec3d(0, 0, 0), + new Vec3d(1, 0, 0), + new Vec3d(1, 1, 1), + new Vec3d(0, 1, 1), + new Vec3d(0, 1, 1), + new Vec3d(1, 1, 1), + new Vec3d(1, 0, 1), + new Vec3d(0, 0, 1) + + }); + + public TestBlock(Settings settings) { + super(settings); + } + + @Override + @SuppressWarnings("deprecation") + public VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) { + return voxel; + } +} diff --git a/src/main/java/fr/adrien1106/reframed/client/util/RaycastResult.java b/src/main/java/fr/adrien1106/reframed/client/util/RaycastResult.java new file mode 100644 index 0000000..7435356 --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/client/util/RaycastResult.java @@ -0,0 +1,7 @@ +package fr.adrien1106.reframed.client.util; + +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; + +public record RaycastResult(Vec3d pos, Direction face, boolean inside) { +} diff --git a/src/main/java/fr/adrien1106/reframed/client/voxel/MorphBox.java b/src/main/java/fr/adrien1106/reframed/client/voxel/MorphBox.java new file mode 100644 index 0000000..461f8d6 --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/client/voxel/MorphBox.java @@ -0,0 +1,42 @@ +package fr.adrien1106.reframed.client.voxel; + +import fr.adrien1106.reframed.client.util.RaycastResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; + +import java.util.List; +import java.util.Objects; + +public class MorphBox extends Box { + protected List triangles; + + public MorphBox(List triangles) { + super(new BlockPos(0, 0, 0)); + this.triangles = triangles; + } + + public RaycastResult triangleRaycast(Vec3d min, Vec3d max) { + return triangles.stream() + .map(triangle -> { + Vec3d inter = triangle.intersection(min, max); + if (inter == null) return null; + return new RaycastResult( + inter, + triangle.face(), + false + ); + }) + .filter(Objects::nonNull) + .findFirst() + .orElseGet(() -> isInside(min) ? new RaycastResult( + min, + triangles.get(0).face(), + true + ) : null); + } + + public boolean isInside(Vec3d point) { + return triangles.stream().allMatch(triangle -> triangle.after(point)); + } +} diff --git a/src/main/java/fr/adrien1106/reframed/client/voxel/MorphVoxel.java b/src/main/java/fr/adrien1106/reframed/client/voxel/MorphVoxel.java new file mode 100644 index 0000000..c1ecadd --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/client/voxel/MorphVoxel.java @@ -0,0 +1,100 @@ +package fr.adrien1106.reframed.client.voxel; + +import fr.adrien1106.reframed.client.util.RaycastResult; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.*; +import net.minecraft.util.shape.SimpleVoxelShape; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * Represents a VoxelShape that can be morphed using its 8 vertices. + * the vertices are distributed as follows: + *

+ *       E--------F   ABCD ->  NORTH
+ *    ^ /|       /|   EFGH ->  SOUTH
+ *    |/ |      / |   AHED ->  EAST
+ *    D--------C  |   BCGF ->  WEST
+ *    |  |/    |  |   ABGH ->  DOWN
+ *    |  H-----|--G   CDFE ->  UP
+ *    | /      | /
+ *    |/       |/
+ *    A--------B->
+ * 
+ */ +public class MorphVoxel extends SimpleVoxelShape { + + private static final int[][] EDGE_INDICES = { + {0, 1}, + {1, 2}, + {2, 3}, + {3, 0}, + {0, 7}, + {1, 6}, + {2, 5}, + {3, 4}, + {4, 5}, + {5, 6}, + {6, 7}, + {7, 4} + }; + + private static final int[][] FACE_INDICES = { + {7, 6, 1, 0}, + {5, 4, 3, 2}, + {0, 1, 2, 3}, + {4, 5, 6, 7}, + {0, 3, 4, 7}, + {6, 5, 2, 1} + }; + + protected final Vec3d[] v; + MorphBox box; + + public MorphVoxel(VoxelShape wrapper, Vec3d[] vertices) { + super(wrapper.voxels); + List triangles = new ArrayList<>(); + Stream.of(Direction.values()).forEach(dir -> { + int i = dir.ordinal(); + List v = IntStream.range(0, 4).mapToObj(j -> { // remove matching vertices + if (vertices[FACE_INDICES[i][j]].equals(vertices[FACE_INDICES[i][(j+1)%4]])) return null; + return vertices[FACE_INDICES[i][j]]; + }).filter(Objects::nonNull).toList(); + + if (v.size() < 3) return; // skip if there are less than 3 vertices (e.g. a line/dot) + triangles.add(Triangle.of(dir,v)); + }); + box = new MorphBox(triangles); + v = vertices; + } + + @Nullable + @Override + public BlockHitResult raycast(Vec3d start, Vec3d end, BlockPos pos) { + RaycastResult result = box.triangleRaycast(start.subtract(pos.getX(), pos.getY(), pos.getZ()), end.subtract(pos.getX(), pos.getY(), pos.getZ())); + return result == null + ? null + : new BlockHitResult( + result.pos().add(pos.getX(), pos.getY(), pos.getZ()), + result.face(), + pos, + result.inside() + ); + } + + @Override + public void forEachEdge(VoxelShapes.BoxConsumer consumer) { + IntStream.range(0, 12).forEach(i -> { + Vec3d a = v[EDGE_INDICES[i][0]]; + Vec3d b = v[EDGE_INDICES[i][1]]; + consumer.consume(a.x, a.y, a.z, b.x, b.y, b.z); + }); + } +} diff --git a/src/main/java/fr/adrien1106/reframed/client/voxel/Triangle.java b/src/main/java/fr/adrien1106/reframed/client/voxel/Triangle.java new file mode 100644 index 0000000..29b1278 --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/client/voxel/Triangle.java @@ -0,0 +1,78 @@ +package fr.adrien1106.reframed.client.voxel; + +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; + +import java.util.List; +import java.util.stream.IntStream; + +/** + * Represents a Face of a MorphVoxel. + * @param v - list of vertices on the same plane + * @param e - list of the outer edges + * @param n - normal of the face + * @param face - the direction of the face + */ +public record Triangle(Vec3d[] v, Vec3d[] e, Vec3d n, Direction face) { + + /** + * Creates a Triangle from a list of vertices. + * @param face - the direction of the face + * @param vertices - list of vertices on the same plane + * @return the Triangle + */ + public static Triangle of(Direction face, List vertices) { + assert vertices.size() >= 3; + Vec3d[] v = vertices.toArray(new Vec3d[0]); + + // compute the edges + Vec3d[] e = new Vec3d[v.length]; + IntStream.range(0, v.length).forEach(i -> e[i] = v[(i + 1) % v.length].subtract(v[i])); + + // compute the normal + Vec3d n = v[1].subtract(v[0]).crossProduct(v[2].subtract(v[0])).normalize(); + + return new Triangle(v, e, n, face); + } + + /** + * Computes the intersection of a ray with the triangle. + * @param start - the start of the ray + * @param end - the end of the ray + * @return the intersection point or null if there is no intersection + */ + public Vec3d intersection(Vec3d start, Vec3d end) { + Vec3d start_v0 = start.subtract(v[0]); + Vec3d end_v1 = end.subtract(v[1]); + + // check if the ray intersects the plane + if (n.dotProduct(start_v0) * n.dotProduct(end_v1) >= 0) return null; + + Vec3d ray = end.subtract(start); + double direction = n.dotProduct(ray); + + // plane normal is facing away from the ray + if (direction < 0) return null; + + // get Intersection point + double t = -n.dotProduct(start_v0) / direction; + Vec3d intersection = start.add(ray.multiply(t)); + + // check if the intersection is inside the triangle + for (int i = 0; i < v.length; i++) { + Vec3d edge = e[i]; + Vec3d edge_intersection = intersection.subtract(v[i]); + if (edge_intersection.length() < 1e-6) break; // intersection is on the vertex + + double a = n.dotProduct(edge.crossProduct(edge_intersection)); + if (a <= 0) return null; // intersection is outside the triangle + if (a <= edge.length() * 1e-6) break; // intersection is on the edge + } + + return intersection; + } + + public boolean after(Vec3d point) { + return n.dotProduct(point.subtract(v[0])) > 0; + } +} diff --git a/src/main/java/fr/adrien1106/reframed/generator/GBlockstate.java b/src/main/java/fr/adrien1106/reframed/generator/GBlockstate.java index 87c9a71..bec62a8 100644 --- a/src/main/java/fr/adrien1106/reframed/generator/GBlockstate.java +++ b/src/main/java/fr/adrien1106/reframed/generator/GBlockstate.java @@ -53,6 +53,8 @@ public class GBlockstate extends FabricModelProvider { providers.put(ReFramedPostBlock.class, new Post()); providers.put(ReFramedFenceBlock.class, new Fence()); providers.put(ReFramedPostFenceBlock.class, new PostFence()); + + providers.put(TestBlock.class, new Test()); } public GBlockstate(FabricDataOutput output) { diff --git a/src/main/java/fr/adrien1106/reframed/generator/block/Test.java b/src/main/java/fr/adrien1106/reframed/generator/block/Test.java new file mode 100644 index 0000000..77173b3 --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/generator/block/Test.java @@ -0,0 +1,26 @@ +package fr.adrien1106.reframed.generator.block; + +import fr.adrien1106.reframed.generator.BlockStateProvider; +import fr.adrien1106.reframed.generator.GBlockstate; +import net.minecraft.block.Block; +import net.minecraft.data.client.BlockStateSupplier; +import net.minecraft.data.client.VariantsBlockStateSupplier; +import net.minecraft.util.Identifier; + +import static net.minecraft.data.client.VariantSettings.Rotation.R0; + +public class Test implements BlockStateProvider { + + + @Override + public BlockStateSupplier getMultipart(Block block) { + return VariantsBlockStateSupplier.create( + block, + GBlockstate.variant( + new Identifier("block/barrier"), + true, + R0, R0 + ) + ); + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index a7e1264..090bb65 100755 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -25,6 +25,7 @@ "mixins": [ "reframed.mixins.json" ], + "accessWidener": "reframed.accesswidener", "depends": { "minecraft": "${minecraft_version}", "fabricloader": "^${loader_version}", diff --git a/src/main/resources/reframed.accesswidener b/src/main/resources/reframed.accesswidener new file mode 100644 index 0000000..22e99fe --- /dev/null +++ b/src/main/resources/reframed.accesswidener @@ -0,0 +1,4 @@ +accessWidener v2 named + +accessible field net/minecraft/util/shape/VoxelShape voxels Lnet/minecraft/util/shape/VoxelSet; +extendable class net/minecraft/util/shape/SimpleVoxelShape \ No newline at end of file