This commit is contained in:
2025-08-18 14:19:28 +09:00
parent 75021b3360
commit 4652f58415
9 changed files with 197 additions and 122 deletions

View File

@@ -10,8 +10,9 @@ public class KeplerClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
var renderer = new TrajectoryRenderer();
ClientEntityEvents.ENTITY_LOAD.register((entity, _a) -> {
if (entity instanceof PlayerEntity) TrajectoryRenderer.setupRenderLine((PlayerEntity) entity);
if (entity instanceof PlayerEntity) renderer.setupRenderLine((PlayerEntity) entity);
});
}
}

View File

@@ -0,0 +1,56 @@
package org.walruslab.kepler.render;
import net.minecraft.entity.projectile.ArrowEntity;
import org.jetbrains.annotations.Nullable;
import org.walruslab.kepler.trajectory.Trajectory;
import org.walruslab.kepler.trajectory.TrajectoryHit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class ParallelTrajectoryManager {
private boolean isDataAvailable = false;
private List<TrajectoryHit> lastHits;
public void calculateTrajectories(List<ArrowEntity> arrows) {
if (!isDataAvailable) {
AtomicInteger i = new AtomicInteger();
lastHits = new ArrayList<>();
for (var arrow : arrows) {
var t = new Thread(() -> {
var hit = calculateArrow(arrow);
if (hit != null) {
lastHits.add(hit);
}
i.addAndGet(1);
if (arrows.size() == i.get()) {
isDataAvailable = true;
}
});
t.start();
}
}
}
public List<TrajectoryHit> getLastHits() {
if (isDataAvailable) {
isDataAvailable = false;
return lastHits;
} else return null;
}
private @Nullable TrajectoryHit calculateArrow(ArrowEntity arrow) {
if (arrow.groundCollision) return null;
var pos = arrow.getPos();
var velocity = arrow.getVelocity();
var tr = new Trajectory(pos.toVector3f(), velocity.toVector3f());
var hit = tr.getTrajectoryPoints(arrow.getWorld(), 0.1f, arrow.getOwner());
return hit;
}
}

View File

@@ -1,149 +1,125 @@
package org.walruslab.kepler.render;
import com.mojang.blaze3d.opengl.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.VertexFormat;
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
import net.jpountz.util.Utils;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.Tessellator;
import net.minecraft.client.render.VertexFormats;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.projectile.ArrowEntity;
import net.minecraft.item.BowItem;
import net.minecraft.item.Items;
import net.minecraft.predicate.entity.EntityPredicate;
import net.minecraft.predicate.entity.EntityPredicates;
import net.minecraft.text.Text;
import net.minecraft.util.TypeFilter;
import net.minecraft.util.Util;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.math.Vec3i;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.lwjgl.opengl.GL11;
import org.walruslab.kepler.trajectory.Trajectory;
import org.walruslab.kepler.trajectory.TrajectoryHit;
import org.walruslab.kepler.trajectory.item.ShootableItemFactory;
import java.util.List;
import java.util.function.Predicate;
public class TrajectoryRenderer {
public static void setupRenderLine(PlayerEntity player) {
private final ParallelTrajectoryManager trajectoryManager = new ParallelTrajectoryManager();
public void setupRenderLine(PlayerEntity player) {
player.sendMessage(Text.of("Kepler: TrajectoryRenderer setupRenderLine"), false);
WorldRenderEvents.END.register((drawContext) -> {
// get all error entity in player world
var world = player.getWorld();
var entities = world.getEntitiesByClass(LivingEntity.class, new Box(player.getPos().subtract(Vec3d.of(new Vec3i(100, 100, 100))), player.getPos().add(Vec3d.of(new Vec3i(100, 100, 100)))), EntityPredicates.VALID_ENTITY);
var arrows = world.getEntitiesByType(TypeFilter.instanceOf(ArrowEntity.class), new Box(player.getPos().subtract(Vec3d.of(new Vec3i(100, 100, 100))), player.getPos().add(Vec3d.of(new Vec3i(100, 100, 100)))), EntityPredicates.VALID_ENTITY);
for (ArrowEntity arrow : arrows) {
if (arrow.groundCollision) return;
trajectoryManager.calculateTrajectories(arrows);
var pos = arrow.getPos();
var velocity = arrow.getVelocity();
var tr = new Trajectory(pos.toVector3f(), velocity.toVector3f());
var hit = tr.getTrajectoryPoints(world, 0.1f, player);
var points = hit.points();
if (points.size() < 2) {
continue; // Skip if not enough points
}
// Render the trajectory line
if (hit.hitEntity()) {
renderLine(drawContext, points, 0.0f, 1.0f, 0.0f); // Red for hit
} else {
renderLine(drawContext, points, 1.0f, 0.0f, 1.0f); // Green for no hit
var hits = trajectoryManager.getLastHits();
if (hits != null) {
for (var hit : hits) {
if (hit != null)
renderForArrow(hit, drawContext);
}
}
var livingEntities = world.getOtherEntities(player, new Box(player.getPos().subtract(Vec3d.of(new Vec3i(100, 100, 100))), player.getPos().add(Vec3d.of(new Vec3i(100, 100, 100)))), EntityPredicates.VALID_ENTITY);
for (var entity : livingEntities) {
if (entity instanceof ArrowEntity) continue;
var pos = entity.getPos();
var velocity = entity.getVelocity();
if (velocity.length() < 0.1) continue; // Skip if velocity is too low
var predictedPos = new Vector3f(
(float) (entity.getX() + velocity.x * 20),
(float) (entity.getY() + velocity.y * 20),
(float) (entity.getZ() + velocity.z * 20)
);
var points = List.of(
new Vector3f((float) pos.x, (float) pos.y, (float) pos.z),
predictedPos
);
// Render the trajectory line
renderLine(drawContext, points, 0.0f, 0.0f, 1.0f); // Blue for entity
}
// check player is shooting bow
var activeItem = player.getMainHandStack();
if (activeItem != null && (activeItem.isOf(Items.BOW) || activeItem.isOf(Items.CROSSBOW))) {
float speed;
if (activeItem.isOf(Items.BOW)) {
var item = (BowItem) activeItem.getItem();
var f = BowItem.getPullProgress(player.getItemUseTime());
speed = f * 3.0f;
} else {
speed = 3.15f;
}
var dirDouble = player.getRotationVec(1.0f);
var dir = new Vector3f(
(float) dirDouble.x,
(float) dirDouble.y,
(float) dirDouble.z
).normalize();
var pos = player.getEyePos();
var playerVelocity = player.getVelocity();
var velocity = new Vector3f(
(float) (dir.x * speed + playerVelocity.x),
(float) (dir.y * speed + playerVelocity.y),
(float) (dir.z * speed + playerVelocity.z)
);
var tr = new Trajectory(pos.toVector3f(), velocity);
var hit = tr.getTrajectoryPoints(world, 0.1f, player);
var points = hit.points();
if (points.size() >= 2) {
var playerRight = dir.cross(new Vector3f(0, 1, 0)).normalize();
// move points to the right, closest point -> move 1, farthest point -> move 0, linear
for (int i = 0; i < points.size(); i++) {
var point = points.get(i);
var rightOffset = 0.05;
var dx = (float) (point.x + playerRight.x * rightOffset * (1 - ((float) i / points.size())));
var dy = (float) (point.y + playerRight.y * rightOffset * (1 - ((float) i / points.size())));
var dz = (float) (point.z + playerRight.z * rightOffset * (1 - ((float) i / points.size())));
points.set(i, new Vector3f(dx, dy, dz));
}
// Render the trajectory line
if (hit.hitEntity()) {
renderLine(drawContext, points, 0.0f, 1.0f, 0.0f); // Red for hit
} else {
renderLine(drawContext, points, 1.0f, 0.0f, 1.0f); // Green for no hit
}
for (LivingEntity entity : entities) {
var activeItem = entity.getMainHandStack();
if (activeItem != null && (activeItem.isOf(Items.BOW) || activeItem.isOf(Items.CROSSBOW))) {
renderForPlayer(entity, drawContext);
}
}
});
}
private static void renderLine(WorldRenderContext drawContext, List<Vector3f> points, float r, float g, float b) {
private void renderForArrow(TrajectoryHit hit, WorldRenderContext drawContext) {
var points = hit.points();
if (points.size() >= 2) {
if (hit.hitEntity() != null) {
renderLine(drawContext, points, 0.0f, 1.0f, 0.0f); // Red for hit
} else {
renderLine(drawContext, points, 1.0f, 0.0f, 1.0f); // Green for no hit
}
}
}
private void renderForPlayer(LivingEntity player, WorldRenderContext drawContext) {
// get all error entity in player world
var world = player.getWorld();
var activeItem = player.getMainHandStack();
float progress = 1.0f;
if (activeItem.isOf(Items.BOW)) {
progress = BowItem.getPullProgress(player.getItemUseTime());
}
var dirDouble = player.getRotationVec(1.0f);
var dir = new Vector3f(
(float) dirDouble.x,
(float) dirDouble.y,
(float) dirDouble.z
).normalize();
var pos = player.getEyePos();
var playerVelocity = player.getVelocity();
var item = ShootableItemFactory.create(activeItem);
if (item != null) {
var tr = item.makeTrajectory(pos.toVector3f(), dir, playerVelocity.toVector3f(), progress);
var hit = tr.getTrajectoryPoints(world, 0.1f, player);
var points = hit.points();
if (points.size() >= 2) {
var playerRight = dir.cross(new Vector3f(0, 1, 0)).normalize();
// move points to the right, closest point -> move 1, farthest point -> move 0, linear
for (int i = 0; i < points.size(); i++) {
var point = points.get(i);
var rightOffset = 0.05;
var dx = (float) (point.x + playerRight.x * rightOffset * (1 - ((float) i / points.size())));
var dy = (float) (point.y + playerRight.y * rightOffset * (1 - ((float) i / points.size())));
var dz = (float) (point.z + playerRight.z * rightOffset * (1 - ((float) i / points.size())));
points.set(i, new Vector3f(dx, dy, dz));
}
// Render the trajectory line
if (hit.hitEntity() != null) {
renderLine(drawContext, points, 0.0f, 1.0f, 0.0f); // Red for hit
} else {
renderLine(drawContext, points, 1.0f, 0.0f, 1.0f); // Green for no hit
}
}
}
}
private void renderLine(WorldRenderContext drawContext, List<Vector3f> points, float r, float g, float b) {
// enable z-buffer
GlStateManager._enableDepthTest();
GlStateManager._depthFunc(GL11.GL_LEQUAL);
@@ -155,7 +131,7 @@ public class TrajectoryRenderer {
var playerPos = MinecraftClient.getInstance().getCameraEntity().getCameraPosVec(0);
for (var point: points) {
for (var point : points) {
float dx = (float) (point.x - playerPos.x);
float dy = (float) (point.y - playerPos.y);
float dz = (float) (point.z - playerPos.z);

View File

@@ -53,21 +53,11 @@ public class Trajectory {
var bb = entity.getBoundingBox();
var entityVelocity = entity.getVelocity();
// predict entity position by adding its velocity * seconds(by tick)
var curEntityPos = new Vector3f(
(float) (entity.getX()),
(float) (entity.getY()),
(float) (entity.getZ())
);
var predictEntityPos = new Vector3f(
(float) (entity.getX() + entityVelocity.x * currentTick),
(float) (entity.getY() + entityVelocity.y * currentTick),
(float) (entity.getZ() + entityVelocity.z * currentTick)
);
var entityStart = new Vec3d(bb.minX, bb.minY, bb.minZ).add(entityVelocity.x * currentTick, entityVelocity.y * currentTick, entityVelocity.z * currentTick);
var entityEnd = new Vec3d(bb.maxX, bb.maxY, bb.maxZ).add(entityVelocity.x * currentTick, entityVelocity.y * currentTick, entityVelocity.z * currentTick);
var entityBox = new Box(entityStart, entityEnd);
if (this.boxIntersectsLine(start, end, entityBox)) {
return new TrajectoryHit(points, true); // Hit an entity
return new TrajectoryHit(points, entity, currentTick); // Hit an entity
}
}
@@ -83,7 +73,7 @@ public class Trajectory {
if (hitResult.getType() != HitResult.Type.MISS) {
// Check if the hit result is a block
if (hitResult.getType() == HitResult.Type.BLOCK) {
return new TrajectoryHit(points, false); // Hit a block
return new TrajectoryHit(points, null, currentTick); // Hit a block
}
}
@@ -99,7 +89,7 @@ public class Trajectory {
break; // Prevent infinite loop
}
}
return new TrajectoryHit(points, false); // No hit
return new TrajectoryHit(points, null, currentTick); // No hit
}
private boolean boxIntersectsLine(Vec3d start, Vec3d end, Box box) {

View File

@@ -1,9 +1,10 @@
package org.walruslab.kepler.trajectory;
import net.minecraft.entity.Entity;
import net.minecraft.util.Pair;
import org.joml.Vector3f;
import java.util.List;
public record TrajectoryHit(List<Vector3f> points, boolean hitEntity) {
public record TrajectoryHit(List<Vector3f> points, Entity hitEntity, int ticks) {
}

View File

@@ -0,0 +1,14 @@
package org.walruslab.kepler.trajectory.item;
import org.joml.Vector3f;
import org.walruslab.kepler.trajectory.Trajectory;
public class ShootableBow implements ShootableItem {
public final float speed = 3.0f;
@Override
public Trajectory makeTrajectory(Vector3f pos, Vector3f dir, Vector3f bias, float mul) {
var velocity = dir.mul(speed * mul).add(bias);
return new Trajectory(pos, velocity);
}
}

View File

@@ -0,0 +1,14 @@
package org.walruslab.kepler.trajectory.item;
import org.joml.Vector3f;
import org.walruslab.kepler.trajectory.Trajectory;
public class ShootableCrossbow implements ShootableItem {
public final float speed = 3.15f;
@Override
public Trajectory makeTrajectory(Vector3f pos, Vector3f dir, Vector3f bias, float mul) {
var velocity = dir.mul(speed * mul).add(bias);
return new Trajectory(pos, velocity);
}
}

View File

@@ -0,0 +1,8 @@
package org.walruslab.kepler.trajectory.item;
import org.joml.Vector3f;
import org.walruslab.kepler.trajectory.Trajectory;
public interface ShootableItem {
public Trajectory makeTrajectory(Vector3f pos, Vector3f dir, Vector3f bias, float mul);
}

View File

@@ -0,0 +1,15 @@
package org.walruslab.kepler.trajectory.item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import org.jetbrains.annotations.Nullable;
public class ShootableItemFactory {
public static @Nullable ShootableItem create(ItemStack itemStack) {
if (itemStack.isOf(Items.BOW))
return new ShootableBow();
else if (itemStack.isOf(Items.CROSSBOW))
return new ShootableCrossbow();
else return null;
}
}