diff --git a/changelog.md b/changelog.md index d3344f9..c70c3ab 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,9 @@ ### Test Versions ### +* 0.6.0-beta: +* 0.6.0-alpha.1: + * 0.5.0-beta: * Redesigned config screen * Removed commands because it's no longer useful @@ -23,6 +26,9 @@ * Added feature #15, fixed #35 * Changed the way of clipping to space to avoid #23 and #32 * 0.5.5-beta: + * Changed the way of clipping to space to avoid #37 + * Deleted the option of disabling "clip to space" + * Separate the part that modifies camera from the part that computes it * 0.4.0-alpha: * Fixed an issue where the camera would not follow the model action when the player started/ended sneaking @@ -84,6 +90,9 @@ ### 测试版 ### +* 0.6.0-beta: +* 0.6.0-alpha.1: + * 0.5.0-beta: * 重新设计了配置屏幕 * 删除了命令,因为它不再有用 @@ -101,6 +110,9 @@ * 添加了功能#15, 修复#35 * 修改了clip to space的方式来避免#23和#32 * 0.5.5-beta: + * 修改了clip to space的方式来避免#37 + * 删除了禁用"clip to space"的选项 + * 将修改摄像头的部分与计算它的部分分离开来 * 0.4.0-alpha: * 修复了玩家开始/结束潜行时摄像头未跟上模型动作的问题 diff --git a/common/src/main/java/com/xtracr/realcamera/RealCameraCore.java b/common/src/main/java/com/xtracr/realcamera/RealCameraCore.java index 2923d41..3620ae4 100644 --- a/common/src/main/java/com/xtracr/realcamera/RealCameraCore.java +++ b/common/src/main/java/com/xtracr/realcamera/RealCameraCore.java @@ -7,9 +7,12 @@ import com.xtracr.realcamera.config.ModConfig; import com.xtracr.realcamera.mixins.PlayerEntityRendererAccessor; import com.xtracr.realcamera.utils.MathUtils; +import com.xtracr.realcamera.utils.VertexDataAnalyser; +import com.xtracr.realcamera.utils.VertexDataCatcher; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.AbstractClientPlayerEntity; import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.render.entity.EntityRenderDispatcher; import net.minecraft.client.render.entity.PlayerEntityRenderer; import net.minecraft.client.render.entity.model.PlayerEntityModel; import net.minecraft.client.util.math.MatrixStack; @@ -21,10 +24,14 @@ import org.joml.Matrix3f; import org.joml.Vector4f; +import java.util.*; + public class RealCameraCore { private static final ModConfig config = ConfigFile.modConfig; - + private static final List normalList = new ArrayList<>(); + private static final List posList = new ArrayList<>(); private static String status = "Successful"; + private static boolean vRendering = false; private static float pitch = 0.0F; private static float yaw = 0.0F; private static float roll = 0.0F; @@ -35,6 +42,10 @@ public static String getStatus() { return status; } + public static boolean isvRendering() { + return vRendering; + } + public static float getPitch() { return pitch; } @@ -72,15 +83,20 @@ public static void computeCamera(MinecraftClient client, float tickDelta) { // GameRenderer.renderWorld MatrixStack matrixStack = new MatrixStack(); - virtualRender(client, tickDelta, matrixStack); + vRendering = true; + VertexDataCatcher catcher = setupCatcher(); + virtualRender(client, tickDelta, matrixStack, catcher); + vRendering = false; // ModelPart$Cuboid.renderCuboid - Vector4f offset = matrixStack.peek().getPositionMatrix().transform(new Vector4f((float) (config.getBindingZ() * config.getScale()), - -(float) (config.getBindingY() * config.getScale()), - -(float) (config.getBindingX() * config.getScale()), 1.0F)); + Vector4f offset = matrixStack.peek().getPositionMatrix().transform(new Vector4f(0, 0, 0, 1.0F)); pos = new Vec3d(offset.x(), offset.y(), offset.z()); - Matrix3f normal = matrixStack.peek().getNormalMatrix().scale(1.0F, -1.0F, -1.0F); + if (!VertexDataAnalyser.isAnalysing() && config.binding.experimental) try { + applyAnalysisResult(normal, catcher); + } catch (Exception ignored) { + } + normal.rotateLocal((float) Math.toRadians(config.getBindingYaw()), normal.m10, normal.m11, normal.m12); normal.rotateLocal((float) Math.toRadians(config.getBindingPitch()), normal.m00, normal.m01, normal.m02); normal.rotateLocal((float) Math.toRadians(config.getBindingRoll()), normal.m20, normal.m21, normal.m22); @@ -90,7 +106,44 @@ public static void computeCamera(MinecraftClient client, float tickDelta) { roll = config.isRollingBound() ? (float) eulerAngle.getZ() : config.getBindingRoll(); } - private static void virtualRender(MinecraftClient client, float tickDelta, MatrixStack matrixStack) { + private static VertexDataCatcher setupCatcher() { + normalList.clear(); + posList.clear(); + if (VertexDataAnalyser.isAnalysing()) return VertexDataAnalyser.catcher; + if (config.binding.experimental) try { + List list = config.binding.indexListMap.get(config.binding.nameOfList); + posList.addAll(list.subList(3, list.size())); + int leftSgn = list.get(2) >= 0 ? 1 : -1; + int upSgn = list.get(1) >= 0 ? 1 : -1; + int leftIndex = list.get(2) * leftSgn + (leftSgn - 1) / 2; + int upIndex = list.get(1) * upSgn + (upSgn - 1) / 2; + normalList.addAll(List.of(list.get(0), upIndex, leftIndex)); + if (upSgn == -1) normalList.add(-1); + if (leftSgn == -1) normalList.add(-2); + } catch (Exception ignored) { + } + return new VertexDataCatcher(normalList::contains, posList::contains); + } + + private static void applyAnalysisResult(Matrix3f normal, VertexDataCatcher catcher) { + if (catcher.posRecorder.isEmpty()) throw new NullPointerException("Target vertices not found"); + Vec3d average = Vec3d.ZERO; + for (Vec3d vec : catcher.posRecorder) { + average = average.add(vec); + } + pos = average.multiply(1 / (double) catcher.posRecorder.size()); + List sorted = new ArrayList<>(List.copyOf(normalList.subList(0, 3))); + List order = new ArrayList<>(); + sorted.sort(Comparator.comparingInt(i -> i)); + order.add(sorted.indexOf(normalList.get(0))); + order.add(sorted.indexOf(normalList.get(1))); + order.add(sorted.indexOf(normalList.get(2))); + normal.set(catcher.normalRecorder.get(order.get(2)).multiply(normalList.contains(-2) ? -1 : 1).toVector3f(), + catcher.normalRecorder.get(order.get(1)).multiply(normalList.contains(-1) ? -1 : 1).toVector3f(), + catcher.normalRecorder.get(order.get(0)).toVector3f()); + } + + private static void virtualRender(MinecraftClient client, float tickDelta, MatrixStack matrixStack, VertexDataCatcher catcher) { ClientPlayerEntity player = client.player; // WorldRenderer.render if (player.age == 0) { @@ -102,6 +155,13 @@ private static void virtualRender(MinecraftClient client, float tickDelta, Matri Vec3d renderOffset = new Vec3d(MathHelper.lerp(tickDelta, player.lastRenderX, player.getX()), MathHelper.lerp(tickDelta, player.lastRenderY, player.getY()), MathHelper.lerp(tickDelta, player.lastRenderZ, player.getZ())); + matrixStack.push(); + EntityRenderDispatcher dispatcher = client.getEntityRenderDispatcher(); + dispatcher.configure(client.world, client.gameRenderer.getCamera(), player); + if (VertexDataAnalyser.preAnalysing() || config.binding.experimental) dispatcher.render(player, renderOffset.getX(), + renderOffset.getY(), renderOffset.getZ(), 0, tickDelta, matrixStack, layer -> catcher, 0xF000F0); + VertexDataAnalyser.analyse(player, tickDelta); + matrixStack.pop(); // EntityRenderDispatcher.render if (config.compatPhysicsMod()) PhysicsModCompat.renderStart(client.getEntityRenderDispatcher(), player, renderOffset.getX(), renderOffset.getY(), diff --git a/common/src/main/java/com/xtracr/realcamera/command/ClientCommand.java b/common/src/main/java/com/xtracr/realcamera/command/ClientCommand.java new file mode 100644 index 0000000..353a880 --- /dev/null +++ b/common/src/main/java/com/xtracr/realcamera/command/ClientCommand.java @@ -0,0 +1,99 @@ +package com.xtracr.realcamera.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.xtracr.realcamera.RealCamera; +import com.xtracr.realcamera.config.ConfigFile; +import com.xtracr.realcamera.config.ModConfig; +import com.xtracr.realcamera.utils.VertexDataAnalyser; +import net.minecraft.client.MinecraftClient; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.CommandSource; +import net.minecraft.text.Text; + +import java.util.List; + +public class ClientCommand{ + private static final String KEY_COMMAND = "message.xtracr_" + RealCamera.MODID + "_command_"; + private static final ModConfig config = ConfigFile.modConfig; + + public void register(CommandDispatcher dispatcher, CommandRegistryAccess access) { + final LiteralArgumentBuilder builder = literal(RealCamera.MODID); + builder.then(literal("analyse").executes(context -> this.startAnalysis(context, 0)) + .then(argument("targetIndex", IntegerArgumentType.integer(0)) + .then(argument("accuracy", IntegerArgumentType.integer(10, 10000000)).executes(context -> this.startAnalysis(context, 1))))); + builder.then(literal("autoBind") + .then(argument("name", StringArgumentType.string()).executes(this::autoBind))); + builder.then(literal("config") + .then(literal("delete") + .then(argument("name", StringArgumentType.string()).executes(this::deleteList))) + .then(literal("listAll").executes(this::listAll))); + builder.then(literal("showResult") + .then(argument("number", IntegerArgumentType.integer(1)).executes(this::showResult))); + + dispatcher.register(builder); + } + + private int startAnalysis(CommandContext context, int mode) { + if (mode == 1) mode += IntegerArgumentType.getInteger(context, "targetIndex"); + final float accuracy = mode == 0 ? 100 : IntegerArgumentType.getInteger(context, "accuracy"); + VertexDataAnalyser.start(mode, mode == 0 ? 80 : 120, 1 / accuracy); + return 1; + } + + private int autoBind(CommandContext context) { + List list = VertexDataAnalyser.getFinalResults(-1); + if (list == null) return 0; + config.binding.indexListMap.put(StringArgumentType.getString(context, "name"), list); + ConfigFile.save(); + printGameMessage(Text.translatable(KEY_COMMAND + "autoBind", list.get(0), list.get(0), list.get(1), list.get(2))); + return 1; + } + + private int deleteList(CommandContext context) { + final String name = StringArgumentType.getString(context, "name"); + if (!config.binding.indexListMap.containsKey(name)) { + printGameMessage(Text.translatable(KEY_COMMAND + "delete_failure", name)); + return 0; + } + config.binding.indexListMap.remove(name); + ConfigFile.save(); + printGameMessage(Text.translatable(KEY_COMMAND + "delete_success", name)); + return 1; + } + + private int listAll(CommandContext context) { + StringBuffer buffer = new StringBuffer(); + config.binding.indexListMap.forEach((name, list) -> { + buffer.append("\n'").append(name).append("' -> [ "); + list.forEach(i -> buffer.append(i).append(" ")); + buffer.append("]"); + }); + printGameMessage(Text.translatable(KEY_COMMAND + "listAll", config.binding.indexListMap.size(), buffer.toString())); + return 1; + } + + private int showResult(CommandContext context) { + final int number = IntegerArgumentType.getInteger(context, "number"); + VertexDataAnalyser.showResult(number, true); + return 1; + } + + private static void printGameMessage(Text text) { + MinecraftClient.getInstance().getMessageHandler().onGameMessage(text, false); + } + + private RequiredArgumentBuilder argument(final String name, final ArgumentType type) { + return RequiredArgumentBuilder.argument(name, type); + } + + private LiteralArgumentBuilder literal(final String name) { + return LiteralArgumentBuilder.literal(name); + } +} diff --git a/common/src/main/java/com/xtracr/realcamera/config/ConfigScreen.java b/common/src/main/java/com/xtracr/realcamera/config/ConfigScreen.java index 8751fe1..ae33dd7 100644 --- a/common/src/main/java/com/xtracr/realcamera/config/ConfigScreen.java +++ b/common/src/main/java/com/xtracr/realcamera/config/ConfigScreen.java @@ -43,6 +43,7 @@ public static Screen create(Screen parent) { ConfigCategory classic = builder.getOrCreateCategory(Text.translatable(CATEGORY + "classic")); ConfigCategory compats = builder.getOrCreateCategory(Text.translatable(CATEGORY + "compats")); ConfigCategory disable = builder.getOrCreateCategory(Text.translatable(CATEGORY + "disable")); + ConfigCategory experimental = builder.getOrCreateCategory(Text.literal("Experimental")); general.addEntry(entryBuilder.startBooleanToggle(Text.translatable(OPTION + "enabled"), config.general.enabled) .setSaveConsumer(b -> config.general.enabled = b) @@ -315,6 +316,15 @@ public static Screen create(Screen parent) { .build()); disable.addEntry(disableModWhen.build()); + experimental.addEntry(entryBuilder.startBooleanToggle(Text.literal("Enabled"), config.binding.experimental) + .setDefaultValue(false) + .setSaveConsumer(b -> config.binding.experimental = b) + .build()); + experimental.addEntry(entryBuilder.startStrField(Text.literal("Name Of List"), config.binding.nameOfList) + .setDefaultValue("minecraft_head") + .setSaveConsumer(s -> config.binding.nameOfList = s) + .build()); + return builder.build(); } } diff --git a/common/src/main/java/com/xtracr/realcamera/config/ModConfig.java b/common/src/main/java/com/xtracr/realcamera/config/ModConfig.java index 011d7bb..f8d481c 100644 --- a/common/src/main/java/com/xtracr/realcamera/config/ModConfig.java +++ b/common/src/main/java/com/xtracr/realcamera/config/ModConfig.java @@ -5,10 +5,7 @@ import net.minecraft.registry.Registries; import net.minecraft.util.math.MathHelper; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; public class ModConfig { protected static final double MIN_DOUBLE = -64.0D; @@ -297,9 +294,14 @@ private void clamp() { } public static class Binding { + public static final Map> defaultIndexListMap = Map.of( + "minecraft_head", Arrays.asList(12, 0, -9, 12, 13, 14, 15)); public VanillaModelPart vanillaModelPart = VanillaModelPart.head; + public boolean experimental = false; public boolean adjustOffset = true; public boolean offsetModel = false; + public String nameOfList = "minecraft_head"; + public Map> indexListMap = defaultIndexListMap; public double cameraX = 3.25D; public double cameraY = 2.0D; public double cameraZ = 0.0D; @@ -312,6 +314,7 @@ public static class Binding { private void clamp() { if (vanillaModelPart == null) vanillaModelPart = VanillaModelPart.head; + if (indexListMap == null) indexListMap = defaultIndexListMap; cameraX = MathHelper.clamp(cameraX, MIN_DOUBLE, MAX_DOUBLE); cameraY = MathHelper.clamp(cameraY, MIN_DOUBLE, MAX_DOUBLE); cameraZ = MathHelper.clamp(cameraZ, MIN_DOUBLE, MAX_DOUBLE); diff --git a/common/src/main/java/com/xtracr/realcamera/mixins/MixinCamera.java b/common/src/main/java/com/xtracr/realcamera/mixins/MixinCamera.java index 1b5e9f2..01f65dd 100644 --- a/common/src/main/java/com/xtracr/realcamera/mixins/MixinCamera.java +++ b/common/src/main/java/com/xtracr/realcamera/mixins/MixinCamera.java @@ -22,7 +22,6 @@ @Mixin(Camera.class) public abstract class MixinCamera { - @Shadow private BlockView area; @Shadow @@ -39,41 +38,46 @@ public abstract class MixinCamera { @Inject(method = "update", at = @At("RETURN")) private void realCamera$updateCamera(BlockView area, Entity focusedEntity, boolean thirdPerson, boolean inverseView, float tickDelta, CallbackInfo cInfo) { - if (RealCameraCore.isActive()) { - final ModConfig config = ConfigFile.modConfig; - if (config.isRendering() && !config.shouldDisableRendering(MinecraftClient.getInstance())) { - this.thirdPerson = true; - } + if (!RealCameraCore.isActive()) return; + final ModConfig config = ConfigFile.modConfig; + if (config.isRendering() && !config.shouldDisableRendering(MinecraftClient.getInstance())) { + this.thirdPerson = true; + } - Vec3d startVec = pos; - if (config.isClassic()) { - Vec3d offset = new Vec3d(config.getClassicX(), config.getClassicY(), config.getClassicZ()).multiply(config.getScale()); - Vec3d center = new Vec3d(config.getCenterX(), config.getCenterY(), config.getCenterZ()).multiply(config.getScale()); - float newPitch = pitch + config.getClassicPitch(); - float newYaw = yaw - config.getClassicYaw(); - if (config.compatPehkui()) { - offset = PehkuiCompat.scaleVec3d(offset, focusedEntity, tickDelta); - center = PehkuiCompat.scaleVec3d(center, focusedEntity, tickDelta); - } + Vec3d startVec = pos; + if (config.isClassic()) { + Vec3d offset = new Vec3d(config.getClassicX(), config.getClassicY(), config.getClassicZ()).multiply(config.getScale()); + Vec3d center = new Vec3d(config.getCenterX(), config.getCenterY(), config.getCenterZ()).multiply(config.getScale()); + float newPitch = pitch + config.getClassicPitch(); + float newYaw = yaw - config.getClassicYaw(); + if (config.compatPehkui()) { + offset = PehkuiCompat.scaleVec3d(offset, focusedEntity, tickDelta); + center = PehkuiCompat.scaleVec3d(center, focusedEntity, tickDelta); + } - setRotation(yaw, 0.0F); - moveBy(center.getX(), center.getY(), center.getZ()); - setRotation(newYaw, newPitch); - moveBy(offset.getX(), offset.getY(), offset.getZ()); + setRotation(yaw, 0.0F); + moveBy(center.getX(), center.getY(), center.getZ()); + setRotation(newYaw, newPitch); + moveBy(offset.getX(), offset.getY(), offset.getZ()); + realCamera$clipToSpace(startVec); + } else { + Vec3d prevPos = pos; + Vec3d offset = new Vec3d(config.getBindingX(), config.getBindingY(), config.getBindingZ()).multiply(config.getScale()); + if (config.compatPehkui()) offset = PehkuiCompat.scaleVec3d(offset, focusedEntity, tickDelta); + setPos(RealCameraCore.getPos()); + setRotation(config.isYawingBound() ? RealCameraCore.getYaw() : yaw - config.getBindingYaw(), + config.isPitchingBound() ? RealCameraCore.getPitch() : pitch + config.getBindingPitch()); + moveBy(offset.getX(), offset.getY(), offset.getZ()); + Vec3d modifiedPos = pos; + setPos(prevPos); + Box box = focusedEntity.getBoundingBox(); + double restrictedY = MathHelper.clamp(modifiedPos.getY(), box.minY + 0.1D, box.maxY - 0.1D); + startVec = new Vec3d(pos.getX(), restrictedY, pos.getZ()); + if (!config.doOffsetModel()) { + setPos(modifiedPos); realCamera$clipToSpace(startVec); - } else { - Vec3d prevPos = RealCameraCore.getPos(); - Box box = focusedEntity.getBoundingBox(); - double restrictedY = MathHelper.clamp(prevPos.getY(), box.minY + 0.1D, box.maxY - 0.1D); - startVec = new Vec3d(pos.getX(), restrictedY, pos.getZ()); - if (!config.doOffsetModel()) { - setPos(prevPos); - realCamera$clipToSpace(startVec); - } - RealCameraCore.setModelOffset(pos.subtract(prevPos)); - setRotation(config.isYawingBound() ? RealCameraCore.getYaw() : yaw - config.getBindingYaw(), - config.isPitchingBound() ? RealCameraCore.getPitch() : pitch + config.getBindingPitch()); } + RealCameraCore.setModelOffset(pos.subtract(modifiedPos)); } } diff --git a/common/src/main/java/com/xtracr/realcamera/mixins/MixinEntityRenderDispatcher.java b/common/src/main/java/com/xtracr/realcamera/mixins/MixinEntityRenderDispatcher.java index 944ce34..eb18cae 100644 --- a/common/src/main/java/com/xtracr/realcamera/mixins/MixinEntityRenderDispatcher.java +++ b/common/src/main/java/com/xtracr/realcamera/mixins/MixinEntityRenderDispatcher.java @@ -9,7 +9,9 @@ import net.minecraft.util.math.Vec3d; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(EntityRenderDispatcher.class) public abstract class MixinEntityRenderDispatcher { @@ -21,4 +23,11 @@ public abstract class MixinEntityRenderDispatcher { } return vec; } + + @Inject(method = "render", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/render/entity/EntityRenderer;render(Lnet/minecraft/entity/Entity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", + shift = At.Shift.AFTER), cancellable = true) + private void realCamera$cancelRender(CallbackInfo cInfo) { + if (RealCameraCore.isvRendering()) cInfo.cancel(); + } } diff --git a/common/src/main/java/com/xtracr/realcamera/utils/VertexDataAnalyser.java b/common/src/main/java/com/xtracr/realcamera/utils/VertexDataAnalyser.java new file mode 100644 index 0000000..8c3fa39 --- /dev/null +++ b/common/src/main/java/com/xtracr/realcamera/utils/VertexDataAnalyser.java @@ -0,0 +1,233 @@ +package com.xtracr.realcamera.utils; + +import com.xtracr.realcamera.RealCamera; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Pair; +import net.minecraft.util.math.RotationAxis; +import net.minecraft.util.math.Vec3d; +import org.joml.Matrix2f; +import org.joml.Matrix3f; + +import java.util.*; + +public abstract class VertexDataAnalyser { + private static final String KEY_ANALYSER = "message.xtracr_" + RealCamera.MODID + "_analyser_"; + private static final float defaultAccuracy = 0.00001f; + private static final Map> indexResult = new HashMap<>(); + private static final Map> orthogonalResult = new HashMap<>(); + private static final List processedResults = new ArrayList<>(); + private static List equivalenceClass = new ArrayList<>(); + private static boolean analysing; + private static float accuracy = defaultAccuracy; + private static int count; + private static int mode; + private static int ticks; + public static final VertexDataCatcher catcher = new VertexDataCatcher(i -> analysing, i -> analysing); + + public static boolean isAnalysing() { + return analysing; + } + + public static boolean preAnalysing() { + catcher.clear(); + return analysing; + } + + public static List getFinalResults(int index) { + if (processedResults.isEmpty()) { + printGameMessage(Text.translatable(KEY_ANALYSER + "notReady")); + return null; + } + List ret = new ArrayList<>(processedResults); + final int target = index == -1 ? processedResults.get(0) : index; + ret.add(target); + if (equivalenceClass.contains(target)) { + for (int i = 1; i <= Math.max(target, equivalenceClass.get(equivalenceClass.size() - 1) - target); i++) { + boolean add = equivalenceClass.contains(target + i); + if (add) ret.add(target + i); + if (equivalenceClass.contains(target - i)) ret.add(target + i); + else if (!add) break; + } + } + return ret; + } + + public static void tick() { + if (!analysing) return; + ticks--; + if (ticks > 0) return; + if (mode == 0) { + int target = getResult(1, false).get(0); + start(target + 1, 80, accuracy); + return; + } + analysing = false; + showResult(12, false); + } + + public static void start(int mode, int ticks, float accuracy) { + indexResult.clear(); + orthogonalResult.clear(); + processedResults.clear(); + equivalenceClass.clear(); + analysing = true; + count = 0; + VertexDataAnalyser.accuracy = accuracy; + VertexDataAnalyser.mode = mode; + VertexDataAnalyser.ticks = ticks; + equivalenceClass.add(Math.max(0, mode - 1)); + if (mode >= 0) printGameMessage(Text.translatable(KEY_ANALYSER + "start")); + } + + public static void showResult(int number, boolean detail) { + if (indexResult.isEmpty()) { + printGameMessage(Text.translatable(KEY_ANALYSER + "notReady")); + return; + } + printGameMessage(Text.translatable(KEY_ANALYSER + "showResult")); + List sorted = getResult(number, true); + StringBuilder buffer = new StringBuilder(); + for (int i : sorted) { + buffer.append(" [").append(i).append("]"); + if (!detail) continue; + float f = Math.round(1000 * indexResult.get(i).getLeft() / (float) count) / 1000f; + buffer.append(": ").append(f); + } + sorted = getResult(number, false); + printGameMessage(Text.translatable(KEY_ANALYSER + "byFrequency", buffer.toString())); + buffer = new StringBuilder(); + for (int i : sorted) { + buffer.append(" [").append(i).append("]"); + if (!detail) continue; + float c = Math.round(1000 * indexResult.get(i).getRight() / (float) count) / 1000f; + buffer.append(": ").append(c); + } + printGameMessage(Text.translatable(KEY_ANALYSER + "byCorrelation", buffer.toString())); + printGameMessage(Text.translatable(KEY_ANALYSER + "numberOfResults", + Math.min(number, indexResult.keySet().size()), indexResult.keySet().size())); + if (orthogonalResult.isEmpty()) return; + if (processedResults.isEmpty()) { + sorted = new ArrayList<>(orthogonalResult.keySet()); + sorted.sort(Comparator.comparingInt(i -> -Math.abs(orthogonalResult.get(i).getRight()))); + int leftIndex = sorted.get(0); + sorted.sort(Comparator.comparingInt(i -> -Math.abs(orthogonalResult.get(i).getLeft()))); + int upIndex = sorted.get(0); + processedResults.add(equivalenceClass.get(0)); + processedResults.add(orthogonalResult.get(upIndex).getLeft() > 0 ? upIndex : -upIndex - 1); + processedResults.add(orthogonalResult.get(leftIndex).getLeft()> 0 ? leftIndex : -leftIndex - 1); + } + if (!equivalenceClass.contains(-1)) { + equivalenceClass = simplifyList(equivalenceClass, Comparator.comparingInt(i -> i), (int) (count * 0.9)); + equivalenceClass = equivalenceClass.subList(0, Math.min(number, equivalenceClass.size())); + equivalenceClass.add(-1); + } + if (detail) { + buffer = new StringBuilder().append("[ "); + for (int i : equivalenceClass) { + if (i < 0) continue; + buffer.append(i).append(" "); + } + printGameMessage(Text.translatable(KEY_ANALYSER + "equivalenceClass", buffer.append("]").toString())); + } + printGameMessage(Text.translatable(KEY_ANALYSER + "bindSuggestion", Text.literal("'autoBind'") + .styled(s -> s.withColor(Formatting.GREEN)), processedResults.get(0), processedResults.get(1), processedResults.get(2))); + } + + public static void analyse(ClientPlayerEntity player, float tickDelta) { + if (!analysing) return; + count++; + Vec3d viewVector = player.getRotationVec(tickDelta); + List> indexCash = new ArrayList<>(); + Matrix3f viewRotation = new Matrix3f().rotate(RotationAxis.POSITIVE_X.rotationDegrees(-player.getPitch(tickDelta))) + .rotate(RotationAxis.POSITIVE_Y.rotationDegrees(player.getYaw(tickDelta))); + analyseVertices(viewVector, viewRotation, indexCash); + for (Pair pair : indexCash) { + int index = pair.getLeft(); + float dot = pair.getRight(); + if (indexResult.containsKey(index)) { + Pair value = indexResult.get(index); + value.setLeft(value.getLeft() + 1); + value.setRight(value.getRight() + dot); + } else { + indexResult.put(index, new Pair<>(1, dot)); + } + } + } + + private static List getResult(int number , boolean byFrequency) { + List indexList = new ArrayList<>(indexResult.keySet()); + if (byFrequency) indexList.sort((i, j) -> indexResult.get(j).getLeft() - indexResult.get(i).getLeft()); + else indexList.sort(Comparator.comparingDouble(i -> -indexResult.get(i).getRight())); + return indexList.subList(0, Math.min(number, indexList.size())); + } + + private static List simplifyList(List list, Comparator comparator, int times) { + Map listCash = new HashMap<>(); + List ret = new ArrayList<>(); + list.forEach(t -> listCash.put(t, listCash.getOrDefault(t, 0) + 1)); + listCash.forEach((t, i) -> { if (i >= times) ret.add(t); }); + ret.sort(comparator); + return ret; + } + + private static void analyseVertices(Vec3d viewVector, Matrix3f viewRotation, List> indexCash) { + final float accuracy = mode == 0 ? defaultAccuracy : VertexDataAnalyser.accuracy; + List recorder = catcher.normalRecorder; + List cash = new ArrayList<>(); + List orthogonalCash = new ArrayList<>(); + final int index0 = Math.min(equivalenceClass.get(0), recorder.size()); + if (equivalenceClass.size() == 1) { + equivalenceClass.clear(); + equivalenceClass.add(index0); + } + Vec3d element0 = recorder.get(index0); + cash.add(element0); + double dotWithView0 = element0.dotProduct(viewVector); + indexCash.add(new Pair<>(index0, (float) dotWithView0)); + for (int i = 0; i < recorder.size(); i++) { + Vec3d element = recorder.get(i); + double dotWith0 = element.dotProduct(element0); + if (i == index0 || dotWith0 >= 1- defaultAccuracy) equivalenceClass.add(i); + else if (dotWith0 < defaultAccuracy && -dotWith0 < defaultAccuracy) { + boolean shouldAdd = true; + if (!orthogonalCash.isEmpty()) for (int index : orthogonalCash) { + if (Math.abs(element.dotProduct(recorder.get(index))) > defaultAccuracy) shouldAdd = false; + } + if (shouldAdd) orthogonalCash.add(i); + } + + double dotWithView = element.dotProduct(viewVector); + if (i == index0 || -dotWithView > accuracy) continue; + boolean skip = false; + for (Vec3d vec : cash) { + double dotAbs = Math.abs(vec.dotProduct(element)); + if (dotAbs >= 1-accuracy) skip = true; + } + if (skip) continue; + cash.add(element); + indexCash.add(new Pair<>(i, (float) dotWithView)); + } + if (orthogonalCash.size() >= 2) { + int index2 = orthogonalCash.get(1); + int index1 = orthogonalCash.get(0); + Matrix3f rotation = new Matrix3f(recorder.get(index2).toVector3f(), recorder.get(index1).toVector3f(), + recorder.get(equivalenceClass.get(0)).toVector3f()); + Pair pair2 = orthogonalResult.getOrDefault(index2, new Pair<>(0, 0)); + Pair pair1 = orthogonalResult.getOrDefault(index1, new Pair<>(0, 0)); + Matrix2f dotProduct = new Matrix2f(viewRotation.mul(rotation, new Matrix3f())); + pair2.setRight(pair2.getRight() + Math.round(dotProduct.m00())); + pair2.setLeft(pair2.getLeft() + Math.round(dotProduct.m01())); + pair1.setRight(pair1.getRight() + Math.round(dotProduct.m10())); + pair1.setLeft(pair1.getLeft() + Math.round(dotProduct.m11())); + orthogonalResult.putIfAbsent(index2, pair2); + orthogonalResult.putIfAbsent(index1, pair1); + } + } + + private static void printGameMessage(Text text) { + MinecraftClient.getInstance().getMessageHandler().onGameMessage(text, false); + } +} diff --git a/common/src/main/java/com/xtracr/realcamera/utils/VertexDataCatcher.java b/common/src/main/java/com/xtracr/realcamera/utils/VertexDataCatcher.java new file mode 100644 index 0000000..0c34478 --- /dev/null +++ b/common/src/main/java/com/xtracr/realcamera/utils/VertexDataCatcher.java @@ -0,0 +1,72 @@ +package com.xtracr.realcamera.utils; + +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.util.math.Vec3d; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntPredicate; + +public class VertexDataCatcher implements VertexConsumer { + private final IntPredicate normalPredicate; + private final IntPredicate posPredicate; + public final List normalRecorder = new ArrayList<>(); + public final List posRecorder = new ArrayList<>(); + private int index = -1; + + public VertexDataCatcher(IntPredicate normalPredicate, IntPredicate posPredicate) { + this.normalPredicate = normalPredicate; + this.posPredicate = posPredicate; + } + + protected void clear() { + index = -1; + normalRecorder.clear(); + posRecorder.clear(); + } + + @Override + public VertexConsumer vertex(double x, double y, double z) { + index++; + if (posPredicate.test(index)) posRecorder.add(new Vec3d(x, y, z)); + return this; + } + + @Override + public VertexConsumer color(int red, int green, int blue, int alpha) { + return this; + } + + @Override + public VertexConsumer texture(float u, float v) { + return this; + } + + @Override + public VertexConsumer overlay(int u, int v) { + return this; + } + + @Override + public VertexConsumer light(int u, int v) { + return this; + } + + @Override + public VertexConsumer normal(float x, float y, float z) { + if (normalPredicate.test(index)) normalRecorder.add(new Vec3d(x, y, z)); + return this; + } + + @Override + public void next() { + } + + @Override + public void fixedColor(int red, int green, int blue, int alpha) { + } + + @Override + public void unfixColor() { + } +} diff --git a/common/src/main/resources/assets/realcamera/lang/en_us.json b/common/src/main/resources/assets/realcamera/lang/en_us.json index ee65f70..1bc0d25 100644 --- a/common/src/main/resources/assets/realcamera/lang/en_us.json +++ b/common/src/main/resources/assets/realcamera/lang/en_us.json @@ -93,5 +93,18 @@ "config.tooltip.xtracr_realcamera_modModelPart": "Name of the model part to bind camera to", "config.tooltip.xtracr_realcamera_disabledModelParts": "Possible options(There may be other options not listed here): %s", - "config.tooltip.xtracr_realcamera_customConditions_actions": "Available options: [disable_mod, disable_rendering, allow_rendering_hand] and Model Part Names" + "config.tooltip.xtracr_realcamera_customConditions_actions": "Available options: [disable_mod, disable_rendering, allow_rendering_hand] and Model Part Names", + + "message.xtracr_realcamera_analyser_start": "------Start Analysis------", + "message.xtracr_realcamera_analyser_notReady": "There is no analysis result now. You have to start analysis or wait for the analysis to finish", + "message.xtracr_realcamera_analyser_showResult": "------Analysis Result------", + "message.xtracr_realcamera_analyser_byFrequency": "Sort by frequency: %s", + "message.xtracr_realcamera_analyser_byCorrelation": "Sort by Correlation: %s", + "message.xtracr_realcamera_analyser_numberOfResults": "Total number of results displayed: %d / %d", + "message.xtracr_realcamera_analyser_equivalenceClass": "Vertices with same direction of the given vertex: %s", + "message.xtracr_realcamera_analyser_bindSuggestion": "Now you can run %s command to bind to: (position, front) [%d], (up) [%d] and (left) [%d]", + "message.xtracr_realcamera_command_autoBind": "Bound successfully: (position) [%d], (front) [%d], (up) [%d] and (left) [%d]", + "message.xtracr_realcamera_command_delete_failure": "Failed to find '%s'", + "message.xtracr_realcamera_command_delete_success": "Successfully deleted '%s'", + "message.xtracr_realcamera_command_listAll": "Found %d results: %s" } \ No newline at end of file diff --git a/common/src/main/resources/assets/realcamera/lang/zh_cn.json b/common/src/main/resources/assets/realcamera/lang/zh_cn.json index fd6afc8..b2e2f27 100644 --- a/common/src/main/resources/assets/realcamera/lang/zh_cn.json +++ b/common/src/main/resources/assets/realcamera/lang/zh_cn.json @@ -93,5 +93,18 @@ "config.tooltip.xtracr_realcamera_modModelPart": "摄像头绑定的模型部位的名称", "config.tooltip.xtracr_realcamera_disabledModelParts": "可能的选项(可能还有其它选项未被列举): %s", - "config.tooltip.xtracr_realcamera_customConditions_actions": "可用选项: [disable_mod, disable_rendering, allow_rendering_hand] 以及 模型部位的名称" + "config.tooltip.xtracr_realcamera_customConditions_actions": "可用选项: [disable_mod, disable_rendering, allow_rendering_hand] 以及 模型部位的名称", + + "message.xtracr_realcamera_analyser_start": "------开始分析------", + "message.xtracr_realcamera_analyser_notReady": "当前没有分析结果,你需要启动分析或等待分析完成", + "message.xtracr_realcamera_analyser_showResult": "------分析结果------", + "message.xtracr_realcamera_analyser_byFrequency": "以频率排序: %s", + "message.xtracr_realcamera_analyser_byCorrelation": "以相关度排序: %s", + "message.xtracr_realcamera_analyser_numberOfResults": "显示的结果总数: %d / %d", + "message.xtracr_realcamera_analyser_equivalenceClass": "与给定的顶点同方向的顶点: %s", + "message.xtracr_realcamera_analyser_bindSuggestion": "现在你可以运行%s命令来绑定至: (位置, 前方) [%d], (上方) [%d] and (左方) [%d]", + "message.xtracr_realcamera_command_autoBind": "绑定成功: (位置) [%d], (前方) [%d], (上方) [%d] and (左方) [%d]", + "message.xtracr_realcamera_command_delete_failure": "未能找到'%s'", + "message.xtracr_realcamera_command_delete_success": "成功删除'%s'", + "message.xtracr_realcamera_command_listAll": "找到了%d个结果: %s" } \ No newline at end of file diff --git a/fabric/src/main/java/com/xtracr/realcamera/RealCameraFabric.java b/fabric/src/main/java/com/xtracr/realcamera/RealCameraFabric.java index 25326b4..12ed00a 100644 --- a/fabric/src/main/java/com/xtracr/realcamera/RealCameraFabric.java +++ b/fabric/src/main/java/com/xtracr/realcamera/RealCameraFabric.java @@ -1,8 +1,12 @@ package com.xtracr.realcamera; +import com.xtracr.realcamera.command.ClientCommand; +import com.xtracr.realcamera.utils.VertexDataAnalyser; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; @@ -14,7 +18,9 @@ public void onInitializeClient() { RealCamera.setup(); ClientTickEvents.END_CLIENT_TICK.register(KeyBindings::handle); + ClientTickEvents.END_CLIENT_TICK.register(client -> VertexDataAnalyser.tick()); WorldRenderEvents.START.register(EventHandler::onWorldRenderStart); + ClientCommandRegistrationCallback.EVENT.register(new ClientCommand()::register); KeyBindingHelper.registerKeyBinding(KeyBindings.TOGGLE_PERSPECTIVE); KeyBindingHelper.registerKeyBinding(KeyBindings.TOGGLE_ADJUST_MODE); diff --git a/forge/src/main/java/com/xtracr/realcamera/EventHandler.java b/forge/src/main/java/com/xtracr/realcamera/EventHandler.java index f133933..e831133 100644 --- a/forge/src/main/java/com/xtracr/realcamera/EventHandler.java +++ b/forge/src/main/java/com/xtracr/realcamera/EventHandler.java @@ -1,11 +1,16 @@ package com.xtracr.realcamera; +import com.xtracr.realcamera.command.ClientCommand; import com.xtracr.realcamera.config.ConfigFile; import com.xtracr.realcamera.utils.CrosshairUtils; +import com.xtracr.realcamera.utils.VertexDataAnalyser; import net.minecraft.client.MinecraftClient; +import net.minecraft.server.command.ServerCommandSource; import net.minecraftforge.client.event.InputEvent.Key; +import net.minecraftforge.client.event.RegisterClientCommandsEvent; import net.minecraftforge.client.event.RenderLevelStageEvent; -import net.minecraftforge.client.event.ViewportEvent.ComputeCameraAngles; +import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; public class EventHandler { @@ -15,7 +20,7 @@ public static void onKeyInput(Key event) { } @SubscribeEvent - public static void onCameraUpdate(ComputeCameraAngles event) { + public static void onCameraUpdate(ViewportEvent.ComputeCameraAngles event) { if (RealCameraCore.isActive()) { event.setPitch(RealCameraCore.getPitch()); event.setYaw(RealCameraCore.getYaw()); @@ -23,6 +28,16 @@ public static void onCameraUpdate(ComputeCameraAngles event) { } } + @SubscribeEvent + public static void onClientTick(TickEvent.ClientTickEvent event) { + VertexDataAnalyser.tick(); + } + + @SubscribeEvent + public static void onClientCommandRegister(RegisterClientCommandsEvent event) { + new ClientCommand().register(event.getDispatcher(), event.getBuildContext()); + } + @SubscribeEvent public static void onRenderWorldStage(RenderLevelStageEvent event) { if (RenderLevelStageEvent.Stage.AFTER_SKY.equals(event.getStage())) { diff --git a/forge/src/main/java/com/xtracr/realcamera/RealCameraForge.java b/forge/src/main/java/com/xtracr/realcamera/RealCameraForge.java index ee6f42e..c4eed6b 100644 --- a/forge/src/main/java/com/xtracr/realcamera/RealCameraForge.java +++ b/forge/src/main/java/com/xtracr/realcamera/RealCameraForge.java @@ -28,6 +28,8 @@ public void clientSetup(FMLClientSetupEvent event) { MinecraftForge.EVENT_BUS.addListener(EventHandler::onKeyInput); MinecraftForge.EVENT_BUS.addListener(EventHandler::onCameraUpdate); + MinecraftForge.EVENT_BUS.addListener(EventHandler::onClientTick); + MinecraftForge.EVENT_BUS.addListener(EventHandler::onClientCommandRegister); MinecraftForge.EVENT_BUS.addListener(EventHandler::onRenderWorldStage); if (ModList.get().isLoaded("cloth_config")) { diff --git a/gradle.properties b/gradle.properties index 0ea289d..ab980ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ forge_version=1.20.2-48.1.0 fabric_loader_version=0.15.1 yarn_mappings=1.20.2+build.4 # Mod Properties -mod_version=0.5.5-beta +mod_version=0.6.0-alpha.1 maven_group=com.xtracr.realcamera archives_base_name=realcamera # Dependencies