From a2766d0356ac6571b87ce4d64a1b7a092dce04e3 Mon Sep 17 00:00:00 2001 From: rsdadada Date: Fri, 10 Jul 2026 21:40:29 +0800 Subject: [PATCH] Support independent rotation axis binding --- .../com/xtracr/realcamera/RealCameraCore.java | 26 +++++- .../xtracr/realcamera/config/BindTarget.java | 22 ++++- .../config/codec/BindConfigAdapter.java | 53 ++++++++++++ .../realcamera/config/codec/ConfigCodec.java | 5 +- .../config/codec/ConfigCodec704.java | 43 ++++++++++ .../realcamera/gui/ModelViewScreen.java | 64 ++++++++++++-- .../util/CameraRotationResolver.java | 48 +++++++++++ .../util/ContinuousEulerAngleTracker.java | 86 +++++++++++++++++++ .../assets/realcamera/lang/en_us.json | 11 ++- .../assets/realcamera/lang/zh_cn.json | 11 ++- 10 files changed, 349 insertions(+), 20 deletions(-) create mode 100644 common/src/main/java/com/xtracr/realcamera/config/codec/BindConfigAdapter.java create mode 100644 common/src/main/java/com/xtracr/realcamera/config/codec/ConfigCodec704.java create mode 100644 common/src/main/java/com/xtracr/realcamera/util/CameraRotationResolver.java create mode 100644 common/src/main/java/com/xtracr/realcamera/util/ContinuousEulerAngleTracker.java diff --git a/common/src/main/java/com/xtracr/realcamera/RealCameraCore.java b/common/src/main/java/com/xtracr/realcamera/RealCameraCore.java index a08d45c8..d1d38aa0 100644 --- a/common/src/main/java/com/xtracr/realcamera/RealCameraCore.java +++ b/common/src/main/java/com/xtracr/realcamera/RealCameraCore.java @@ -11,9 +11,9 @@ import com.xtracr.realcamera.renderer.MultiVertexCatcher; import com.xtracr.realcamera.renderer.RoutingSubmitCollector; import com.xtracr.realcamera.renderer.state.VertexData; +import com.xtracr.realcamera.util.CameraRotationResolver; import com.xtracr.realcamera.util.CameraTransform; import com.xtracr.realcamera.util.LocUtil; -import com.xtracr.realcamera.util.MathUtil; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.SubmitNodeCollector; import net.minecraft.client.renderer.entity.EntityRenderDispatcher; @@ -23,11 +23,15 @@ import net.minecraft.world.phys.Vec3; import org.joml.Matrix3f; import org.joml.Matrix4f; +import org.joml.Vector3d; public final class RealCameraCore { private static final MultiVertexCatcher vertexCatcher = MultiVertexCatcher.create(); private static final CameraTransform smoothedCamera = new CameraTransform(); + private static final CameraRotationResolver rotationResolver = new CameraRotationResolver(); private static BindResult lastResult = BindResult.EMPTY, newResult = BindResult.EMPTY; + private static Object trackedLevel; + private static Entity trackedEntity; private static boolean active = false, rendering = false; private static int failureFrames = 0; @@ -47,12 +51,20 @@ public static void initialize(Minecraft client, boolean advanceGameTime) { Entity entity = client.getCameraEntity(); active = advanceGameTime && ConfigFile.config().enabled && client.options.getCameraType().isFirstPerson() && entity != null && !DisableHelper.MAIN_FEATURE.disabled(entity); rendering = ConfigFile.config().renderModel && !DisableHelper.RENDER_MODEL.disabled(entity); + if (!active || ConfigFile.config().isClassic) { + resetRotationTracking(); + } else if (trackedLevel != client.level || trackedEntity != entity) { + rotationResolver.reset(); + trackedLevel = client.level; + trackedEntity = entity; + } } public static void reset() { smoothedCamera.setPosition(Vec3.ZERO); smoothedCamera.setRotation(new Matrix3f()); failureFrames = 0; + resetRotationTracking(); } public static Vec3 getRawPos(Vec3 cameraPos, Vec3 entityPos) { @@ -62,9 +74,8 @@ public static Vec3 getRawPos(Vec3 cameraPos, Vec3 entityPos) { } public static Vec3 getEulerAngle(float pitch, float yaw, float roll) { - if (!currentTarget().bindConfig().bindRotation()) return new Vec3(pitch, yaw, roll); - double scale = Math.toDegrees(1); - return MathUtil.getEulerAngleYXZ(smoothedCamera.getRotation()).multiply(scale, -scale, scale); + Vector3d result = rotationResolver.resolve(smoothedCamera.getRotation(), pitch, yaw, roll, currentTarget().bindConfig()); + return new Vec3(result.x, result.y, result.z); } public static void computeCamera(Minecraft client, float partialTicks) { @@ -93,6 +104,7 @@ public static void computeCamera(Minecraft client, float partialTicks) { } if (!lastResult.available() || failureFrames > retentionFrames) { lastResult = BindResult.EMPTY; + resetRotationTracking(); active = false; return; } @@ -166,4 +178,10 @@ private static void computeBindResult(BuiltIterableBuffer builtBuffer) { return; } } + + private static void resetRotationTracking() { + rotationResolver.reset(); + trackedLevel = null; + trackedEntity = null; + } } diff --git a/common/src/main/java/com/xtracr/realcamera/config/BindTarget.java b/common/src/main/java/com/xtracr/realcamera/config/BindTarget.java index 10fb3f06..5316d264 100644 --- a/common/src/main/java/com/xtracr/realcamera/config/BindTarget.java +++ b/common/src/main/java/com/xtracr/realcamera/config/BindTarget.java @@ -1,5 +1,8 @@ package com.xtracr.realcamera.config; +import com.google.gson.annotations.JsonAdapter; +import com.xtracr.realcamera.config.codec.BindConfigAdapter; + import java.util.List; import java.util.function.Predicate; @@ -50,6 +53,23 @@ public DisableConfig[] filteredDisableConfigs(Predicate filter) { public record TargetConfig(float forwardU, float forwardV, float upwardU, float upwardV, float posU, float posV) { } - public record BindConfig(boolean bindX, boolean bindY, boolean bindZ, boolean bindRotation) { + @JsonAdapter(BindConfigAdapter.class) + public record BindConfig(boolean bindX, boolean bindY, boolean bindZ, boolean bindPitch, boolean bindYaw, boolean bindRoll) { + public BindConfig(boolean bindX, boolean bindY, boolean bindZ, boolean bindRotation) { + this(bindX, bindY, bindZ, bindRotation, bindRotation, bindRotation); + } + + /** Retains the legacy all-rotation accessor contract. */ + public boolean bindRotation() { + return bindPitch && bindYaw && bindRoll; + } + + public boolean bindAnyRotation() { + return bindPitch || bindYaw || bindRoll; + } + + public boolean bindNoRotation() { + return !bindAnyRotation(); + } } } diff --git a/common/src/main/java/com/xtracr/realcamera/config/codec/BindConfigAdapter.java b/common/src/main/java/com/xtracr/realcamera/config/codec/BindConfigAdapter.java new file mode 100644 index 00000000..58d88774 --- /dev/null +++ b/common/src/main/java/com/xtracr/realcamera/config/codec/BindConfigAdapter.java @@ -0,0 +1,53 @@ +package com.xtracr.realcamera.config.codec; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.xtracr.realcamera.config.BindTarget.BindConfig; + +import java.io.IOException; + +public final class BindConfigAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, BindConfig value) throws IOException { + out.beginObject(); + out.name("bindX").value(value.bindX()); + out.name("bindY").value(value.bindY()); + out.name("bindZ").value(value.bindZ()); + out.name("bindRotation").value(value.bindRotation()); + out.name("bindPitch").value(value.bindPitch()); + out.name("bindYaw").value(value.bindYaw()); + out.name("bindRoll").value(value.bindRoll()); + out.endObject(); + } + + @Override + public BindConfig read(JsonReader in) throws IOException { + boolean bindX = false, bindY = false, bindZ = false, legacyRotation = false; + Boolean bindPitch = null, bindYaw = null, bindRoll = null; + + in.beginObject(); + while (in.hasNext()) { + switch (in.nextName()) { + case "bindX" -> bindX = in.nextBoolean(); + case "bindY" -> bindY = in.nextBoolean(); + case "bindZ" -> bindZ = in.nextBoolean(); + case "bindRotation" -> legacyRotation = in.nextBoolean(); + case "bindPitch" -> bindPitch = in.nextBoolean(); + case "bindYaw" -> bindYaw = in.nextBoolean(); + case "bindRoll" -> bindRoll = in.nextBoolean(); + default -> in.skipValue(); + } + } + in.endObject(); + + return new BindConfig( + bindX, + bindY, + bindZ, + bindPitch == null ? legacyRotation : bindPitch, + bindYaw == null ? legacyRotation : bindYaw, + bindRoll == null ? legacyRotation : bindRoll + ); + } +} diff --git a/common/src/main/java/com/xtracr/realcamera/config/codec/ConfigCodec.java b/common/src/main/java/com/xtracr/realcamera/config/codec/ConfigCodec.java index 78b492e7..2f7807b5 100644 --- a/common/src/main/java/com/xtracr/realcamera/config/codec/ConfigCodec.java +++ b/common/src/main/java/com/xtracr/realcamera/config/codec/ConfigCodec.java @@ -17,9 +17,10 @@ import java.util.zip.InflaterInputStream; public final class ConfigCodec { - static final short CURRENT_VERSION = 703; + static final short CURRENT_VERSION = 704; static final Short2ReferenceMap> CODECS = Short2ReferenceMap.ofEntries( - Short2ReferenceMap.entry(CURRENT_VERSION, ConfigCodec703.CODEC) + Short2ReferenceMap.entry((short) 703, ConfigCodec703.CODEC), + Short2ReferenceMap.entry(CURRENT_VERSION, ConfigCodec704.CODEC) ); public static BindTarget readWithVersion(ByteBuf byteBuf) throws DecoderException, IllegalArgumentException { diff --git a/common/src/main/java/com/xtracr/realcamera/config/codec/ConfigCodec704.java b/common/src/main/java/com/xtracr/realcamera/config/codec/ConfigCodec704.java new file mode 100644 index 00000000..9b47e049 --- /dev/null +++ b/common/src/main/java/com/xtracr/realcamera/config/codec/ConfigCodec704.java @@ -0,0 +1,43 @@ +package com.xtracr.realcamera.config.codec; + +import com.xtracr.realcamera.config.BindTarget; +import com.xtracr.realcamera.config.BindTarget.BindConfig; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; + +final class ConfigCodec704 { + static final StreamCodec BIND_CONFIG_CODEC = StreamCodec.composite( + ByteBufCodecs.BYTE, + bindConfig -> { + byte bindFlags = 0; + if (bindConfig.bindX()) bindFlags |= 0x01; + if (bindConfig.bindY()) bindFlags |= 0x02; + if (bindConfig.bindZ()) bindFlags |= 0x04; + if (bindConfig.bindPitch()) bindFlags |= 0x08; + if (bindConfig.bindYaw()) bindFlags |= 0x10; + if (bindConfig.bindRoll()) bindFlags |= 0x20; + return bindFlags; + }, + bindFlags -> new BindConfig( + (bindFlags & 0x01) != 0, + (bindFlags & 0x02) != 0, + (bindFlags & 0x04) != 0, + (bindFlags & 0x08) != 0, + (bindFlags & 0x10) != 0, + (bindFlags & 0x20) != 0 + ) + ); + + static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, BindTarget::name, + ByteBufCodecs.STRING_UTF8, BindTarget::textureId, + ByteBufCodecs.VAR_INT, BindTarget::priority, + ByteBufCodecs.FLOAT, BindTarget::disablingDepth, + ConfigCodec703.TARGET_CONFIG_CODEC, BindTarget::targetConfig, + BIND_CONFIG_CODEC, BindTarget::bindConfig, + ConfigCodec703.OFFSET_CONFIG_CODEC, BindTarget::offsets, + ConfigCodec703.DISABLE_CONFIGS_CODEC, BindTarget::disableConfigs, + BindTarget::new + ); +} diff --git a/common/src/main/java/com/xtracr/realcamera/gui/ModelViewScreen.java b/common/src/main/java/com/xtracr/realcamera/gui/ModelViewScreen.java index eeaeba5f..e33e77a7 100644 --- a/common/src/main/java/com/xtracr/realcamera/gui/ModelViewScreen.java +++ b/common/src/main/java/com/xtracr/realcamera/gui/ModelViewScreen.java @@ -23,6 +23,7 @@ import net.minecraft.client.gui.layouts.FrameLayout; import net.minecraft.client.gui.layouts.GridLayout; import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.client.gui.layouts.LinearLayout; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.screens.Screen; @@ -84,7 +85,9 @@ public final class ModelViewScreen extends Screen { private final CycleIconButton bindXButton = new CycleIconButton(16, 16, 1, 2); private final CycleIconButton bindYButton = new CycleIconButton(16, 16, 0, 2); private final CycleIconButton bindZButton = new CycleIconButton(16, 16, 1, 2); - private final CycleIconButton bindRotButton = new CycleIconButton(16, 16, 1, 2); + private final CycleIconButton bindPitchButton = new CycleIconButton(16, 16, 1, 2); + private final CycleIconButton bindYawButton = new CycleIconButton(16, 16, 1, 2); + private final CycleIconButton bindRollButton = new CycleIconButton(16, 16, 1, 2); private final DoubleSlider entityPitchSlider = createSlider("pitch", wideWidgetWidth, -90.0, 90.0); private final DoubleSlider entityYawSlider = createSlider("yaw", wideWidgetWidth, -60.0, 60.0); private final NumberWidgetPair offsetXPair = new NumberWidgetPair(font, "offsetX", compactWidgetWidth, widgetHeight, ModConfig.MIN_OFFSET_F, ModConfig.MAX_OFFSET_F); @@ -97,6 +100,7 @@ public final class ModelViewScreen extends Screen { private final List rectWidgets = new ArrayList<>(); private final Map> hiddenNameMap = new HashMap<>(); private final List widgetPairs = List.of(offsetXPair, offsetYPair, offsetZPair, offsetPitchPair, offsetYawPair, offsetRollPair); + private final SimpleIconButton resetOffsetsButton = new SimpleIconButton(0, 0, _ -> widgetPairs.forEach(pair -> pair.setNumber(0))); private final CycleButton selectingButton = createCyclingButtonBuilder(ImmutableSortedMap.of( 0, LocUtil.MODEL_VIEW_WIDGET("forwardVector").withStyle(ChatFormatting.GREEN), 1, LocUtil.MODEL_VIEW_WIDGET("upwardVector").withStyle(ChatFormatting.RED), @@ -133,6 +137,14 @@ public final class ModelViewScreen extends Screen { public ModelViewScreen() { super(LocUtil.MODEL_VIEW_TITLE()); + configureBindButton(bindXButton, LocUtil.literal("X"), false); + configureBindButton(bindYButton, LocUtil.literal("Y"), false); + configureBindButton(bindZButton, LocUtil.literal("Z"), false); + configureBindButton(bindPitchButton, LocUtil.CONFIG_OPTION("pitch"), true); + configureBindButton(bindYawButton, LocUtil.CONFIG_OPTION("yaw"), true); + configureBindButton(bindRollButton, LocUtil.CONFIG_OPTION("roll"), true); + resetOffsetsButton.setMessage(LocUtil.MODEL_VIEW_WIDGET("resetOffsets")); + resetOffsetsButton.setTooltip(createTooltip("resetOffsets")); } @Override @@ -200,16 +212,20 @@ private void initLeftWidgets() { case PREVIEW -> { rows.addChild(toggleSliderButton, 2); LayoutSettings numericControlSettings = grid.newCellSettings().padding(-20, 2, 0, 0); - rows.addChild(bindXButton, smallSettings).setTooltip(createTooltip("bindButtons")); + rows.addChild(bindXButton, smallSettings); rows.addChild(offsetXPair, numericControlSettings); - rows.addChild(bindYButton, smallSettings).setTooltip(createTooltip("bindButtons")); + rows.addChild(bindYButton, smallSettings); rows.addChild(offsetYPair, numericControlSettings); - rows.addChild(bindZButton, smallSettings).setTooltip(createTooltip("bindButtons")); + rows.addChild(bindZButton, smallSettings); rows.addChild(offsetZPair, numericControlSettings); - rows.addChild(bindRotButton, smallSettings).setTooltip(createTooltip("bindButtons")); + rows.addChild(bindPitchButton, smallSettings); rows.addChild(offsetPitchPair, numericControlSettings); - rows.addChild(offsetYawPair, 2, grid.newCellSettings().padding(26, 2, 0, 0)); - rows.addChild(new SimpleIconButton(0, 0, _ -> widgetPairs.forEach(pair -> pair.setNumber(0))), smallSettings); + rows.addChild(bindYawButton, smallSettings); + rows.addChild(offsetYawPair, numericControlSettings); + LinearLayout rollButtons = LinearLayout.horizontal().spacing(2); + rollButtons.addChild(bindRollButton); + rollButtons.addChild(resetOffsetsButton); + rows.addChild(rollButtons, smallSettings); rows.addChild(offsetRollPair, numericControlSettings); rows.addChild(scaleField, smallSettings).setTooltip(createTooltip("scale")); rows.addChild(depthField, smallSettings).setTooltip(createTooltip("depth")); @@ -658,7 +674,10 @@ private void loadBindTarget(BindTarget target) { bindXButton.setValue(target.bindConfig().bindX() ? 0 : 1); bindYButton.setValue(target.bindConfig().bindY() ? 0 : 1); bindZButton.setValue(target.bindConfig().bindZ() ? 0 : 1); - bindRotButton.setValue(target.bindConfig().bindRotation() ? 0 : 1); + bindPitchButton.setValue(target.bindConfig().bindPitch() ? 0 : 1); + bindYawButton.setValue(target.bindConfig().bindYaw() ? 0 : 1); + bindRollButton.setValue(target.bindConfig().bindRoll() ? 0 : 1); + updateBindButtonMessages(); OffsetConfig offsets = target.offsets(); scaleField.setNumber(offsets.scale); offsetXPair.setNumber(offsets.x); @@ -680,7 +699,14 @@ private BindTarget genBindTarget() { newDisableConfigs.set(i, currentDisableConfig); } TargetConfig targetConfig = new TargetConfig(forwardUField.getNumber(), forwardVField.getNumber(), upwardUField.getNumber(), upwardVField.getNumber(), posUField.getNumber(), posVField.getNumber()); - BindConfig bindConfig = new BindConfig(bindXButton.getValue() == 0, bindYButton.getValue() == 0, bindZButton.getValue() == 0, bindRotButton.getValue() == 0); + BindConfig bindConfig = new BindConfig( + bindXButton.getValue() == 0, + bindYButton.getValue() == 0, + bindZButton.getValue() == 0, + bindPitchButton.getValue() == 0, + bindYawButton.getValue() == 0, + bindRollButton.getValue() == 0 + ); OffsetConfig offsets = new OffsetConfig(scaleField.getNumber(), offsetXPair.getNumber(), offsetYPair.getNumber(), offsetZPair.getNumber(), offsetPitchPair.getNumber(), offsetYawPair.getNumber(), offsetRollPair.getNumber()); return new BindTarget(nameField.getValue(), textureIdField.getValue(), priorityField.getNumber(), depthField.getNumber(), targetConfig, bindConfig, offsets, newDisableConfigs); } @@ -701,6 +727,26 @@ private Tooltip createTooltip(String key, Object... args) { return Tooltip.create(LocUtil.MODEL_VIEW_TOOLTIP(key, args)); } + private void configureBindButton(CycleIconButton button, Component axis, boolean rotation) { + button.setOnValueChange(_ -> updateBindButtonMessage(button, axis, rotation)); + updateBindButtonMessage(button, axis, rotation); + } + + private void updateBindButtonMessages() { + updateBindButtonMessage(bindXButton, LocUtil.literal("X"), false); + updateBindButtonMessage(bindYButton, LocUtil.literal("Y"), false); + updateBindButtonMessage(bindZButton, LocUtil.literal("Z"), false); + updateBindButtonMessage(bindPitchButton, LocUtil.CONFIG_OPTION("pitch"), true); + updateBindButtonMessage(bindYawButton, LocUtil.CONFIG_OPTION("yaw"), true); + updateBindButtonMessage(bindRollButton, LocUtil.CONFIG_OPTION("roll"), true); + } + + private void updateBindButtonMessage(CycleIconButton button, Component axis, boolean rotation) { + Component state = LocUtil.MODEL_VIEW_WIDGET(button.getValue() == 0 ? "enabled" : "disabled"); + button.setMessage(LocUtil.MODEL_VIEW_WIDGET(rotation ? "bindRotationAxis" : "bindPositionAxis", axis, state)); + button.setTooltip(createTooltip(rotation ? "bindRotationButton" : "bindPositionButton", axis)); + } + private UVRectangleWidget createRectWidget(UVRectangle rect) { return new UVRectangleWidget(rect.uMin(), rect.vMin(), rect.uMax(), rect.vMax()); } diff --git a/common/src/main/java/com/xtracr/realcamera/util/CameraRotationResolver.java b/common/src/main/java/com/xtracr/realcamera/util/CameraRotationResolver.java new file mode 100644 index 00000000..6323e9f8 --- /dev/null +++ b/common/src/main/java/com/xtracr/realcamera/util/CameraRotationResolver.java @@ -0,0 +1,48 @@ +package com.xtracr.realcamera.util; + +import com.xtracr.realcamera.config.BindTarget.BindConfig; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix3f; +import org.joml.Vector3d; + +/** Resolves each camera Y-X-Z coordinate from the adjusted model candidate or vanilla camera. */ +public final class CameraRotationResolver { + private final ContinuousEulerAngleTracker tracker = new ContinuousEulerAngleTracker(); + + public Vector3d resolve( + Matrix3f adjustedModelRotation, + double vanillaPitch, + double vanillaYaw, + double vanillaRoll, + BindConfig bindConfig + ) { + Vector3d reference = new Vector3d( + Math.toRadians(vanillaPitch), + Math.toRadians(-vanillaYaw), + Math.toRadians(vanillaRoll) + ); + Vector3d tracked = tracker.update(adjustedModelRotation, reference); + + if (bindConfig.bindNoRotation()) { + return new Vector3d(vanillaPitch, vanillaYaw, vanillaRoll); + } + if (bindConfig.bindRotation()) { + double scale = Math.toDegrees(1); + Vec3 existing = MathUtil.getEulerAngleYXZ(adjustedModelRotation).multiply(scale, -scale, scale); + return new Vector3d(existing.x, existing.y, existing.z); + } + + double modelPitch = Math.toDegrees(tracked.x); + double modelYaw = -Math.toDegrees(tracked.y); + double modelRoll = Math.toDegrees(tracked.z); + return new Vector3d( + bindConfig.bindPitch() ? modelPitch : vanillaPitch, + bindConfig.bindYaw() ? modelYaw : vanillaYaw, + bindConfig.bindRoll() ? modelRoll : vanillaRoll + ); + } + + public void reset() { + tracker.reset(); + } +} diff --git a/common/src/main/java/com/xtracr/realcamera/util/ContinuousEulerAngleTracker.java b/common/src/main/java/com/xtracr/realcamera/util/ContinuousEulerAngleTracker.java new file mode 100644 index 00000000..e07ed068 --- /dev/null +++ b/common/src/main/java/com/xtracr/realcamera/util/ContinuousEulerAngleTracker.java @@ -0,0 +1,86 @@ +package com.xtracr.realcamera.util; + +import org.joml.Matrix3fc; +import org.joml.Vector3d; +import org.joml.Vector3dc; + +/** + * Lifts a Y-X-Z rotation matrix onto the Euler branch nearest the previous frame. + * At gimbal lock, the previous frame also fixes the otherwise unobservable yaw/roll split. + */ +public final class ContinuousEulerAngleTracker { + private static final double HALF_PI = Math.PI / 2.0; + private static final double TWO_PI = Math.PI * 2.0; + private static final double SINGULAR_COS_EPSILON = 32.0 * Math.ulp(1.0f); + + private final Vector3d angles = new Vector3d(); + private boolean initialized; + + public Vector3d update(Matrix3fc rotation, Vector3dc reference) { + if (!rotation.isFinite() || !reference.isFinite()) return new Vector3d(angles); + + Vector3dc anchor = initialized ? angles : reference; + double sinPitch = Math.clamp(-rotation.m21(), -1.0, 1.0); + double cosPitch = Math.max( + Math.hypot(rotation.m20(), rotation.m22()), + Math.hypot(rotation.m01(), rotation.m11()) + ); + + if (cosPitch <= SINGULAR_COS_EPSILON) { + updateAtGimbalLock(rotation, anchor, sinPitch); + } else { + double pitch = Math.atan2(sinPitch, cosPitch); + double yaw = Math.atan2(rotation.m20(), rotation.m22()); + double roll = Math.atan2(rotation.m01(), rotation.m11()); + Vector3d principal = unwrap(new Vector3d(pitch, yaw, roll), anchor); + Vector3d alternate = unwrap(new Vector3d(Math.PI - pitch, yaw + Math.PI, roll + Math.PI), anchor); + angles.set(distanceSquared(principal, anchor) <= distanceSquared(alternate, anchor) ? principal : alternate); + } + + initialized = true; + return new Vector3d(angles); + } + + public void reset() { + angles.zero(); + initialized = false; + } + + private void updateAtGimbalLock(Matrix3fc rotation, Vector3dc anchor, double sinPitch) { + double pitch = unwrap(Math.copySign(HALF_PI, sinPitch), anchor.x()); + if (sinPitch >= 0.0) { + double difference = unwrap(Math.atan2(rotation.m10(), rotation.m00()), anchor.y() - anchor.z()); + angles.set( + pitch, + (anchor.y() + anchor.z() + difference) / 2.0, + (anchor.y() + anchor.z() - difference) / 2.0 + ); + } else { + double sum = unwrap(-Math.atan2(rotation.m10(), rotation.m00()), anchor.y() + anchor.z()); + angles.set( + pitch, + (anchor.y() - anchor.z() + sum) / 2.0, + (-anchor.y() + anchor.z() + sum) / 2.0 + ); + } + } + + private static Vector3d unwrap(Vector3d value, Vector3dc reference) { + return value.set( + unwrap(value.x, reference.x()), + unwrap(value.y, reference.y()), + unwrap(value.z, reference.z()) + ); + } + + private static double unwrap(double value, double reference) { + return value + TWO_PI * Math.rint((reference - value) / TWO_PI); + } + + private static double distanceSquared(Vector3dc value, Vector3dc reference) { + double pitch = value.x() - reference.x(); + double yaw = value.y() - reference.y(); + double roll = value.z() - reference.z(); + return pitch * pitch + yaw * yaw + roll * roll; + } +} 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 50fd10fe..9408a683 100644 --- a/common/src/main/resources/assets/realcamera/lang/en_us.json +++ b/common/src/main/resources/assets/realcamera/lang/en_us.json @@ -39,7 +39,7 @@ "config.tooltip.xtracr_realcamera.adjustStep": "Length or 1/100 of the angle of the adjustment per step", "config.tooltip.xtracr_realcamera.bindResultRetentionFrames": "For several frames after the binding failure, continue to use the previous successful binding result", "config.tooltip.xtracr_realcamera.cameraRotation": "Additional camera rotation in degrees", - "config.tooltip.xtracr_realcamera.cameraRotation_n": "Note: Pitch, yaw and roll are rotations on the left, up and front axes respectively", + "config.tooltip.xtracr_realcamera.cameraRotation_n": "Note: Pitch, yaw and roll are rotations on the left, up and front axes respectively. They form a local rigid rotation, so one offset can affect multiple final Euler coordinates", "config.tooltip.xtracr_realcamera.centerOffset": "Rotation center's offset from head", "config.tooltip.xtracr_realcamera.classicAdjustMode": "Determine which to adjust when pressing the adjustment key", "config.tooltip.xtracr_realcamera.classicOffset": "Camera's offset from its rotation center", @@ -71,7 +71,8 @@ "key.xtracr_realcamera.togglePerspective": "Enable/Disable", "message.xtracr_realcamera.bindingFailed": "[%s]: Binding failed, please go to the %s screen and configure manually (Key: %s)", "modmenu.descriptionTranslation.realcamera": "Make the camera more realistic in the first-person view.", - "screen.tooltip.xtracr_realcamera.modelView_bindButtons": "When disabled, the camera's relative relationship to the model will not change, but the camera's corresponding attributes will not be modified by this mod", + "screen.tooltip.xtracr_realcamera.modelView_bindPositionButton": "Bind position %s. Enabled: use the model-bound position; disabled: keep the vanilla camera position", + "screen.tooltip.xtracr_realcamera.modelView_bindRotationButton": "Bind the %s Euler component. Enabled: use the model-bound camera after rotation offsets; disabled: keep the vanilla camera component. At or near ±90° pitch, ambiguous yaw and roll are chosen continuously from previous frames; after a reset, the vanilla orientation is the reference", "screen.tooltip.xtracr_realcamera.modelView_configs": "Saved Configs", "screen.tooltip.xtracr_realcamera.modelView_deleteSelectedRectangle": "Delete the selected rectangle\n(Or use the Delete key)", "screen.tooltip.xtracr_realcamera.modelView_depth": "Planes where all vertex-to-screen distances are less than this value will not be rendered", @@ -89,6 +90,7 @@ "screen.tooltip.xtracr_realcamera.modelView_importSucceeded": "Import %s succeeded!", "screen.tooltip.xtracr_realcamera.modelView_preview": "In the preview mode, you can adjust the camera's offset and rotation", "screen.tooltip.xtracr_realcamera.modelView_priority": "Priority when binding, the larger the value the higher the priority, and the higher the sorting on the right side", + "screen.tooltip.xtracr_realcamera.modelView_resetOffsets": "Reset X/Y/Z position offsets and pitch/yaw/roll rotation offsets to zero", "screen.tooltip.xtracr_realcamera.modelView_saveAs": "Add/update this disable setting\nPress Save on the left to save the config", "screen.tooltip.xtracr_realcamera.modelView_scale": "Scale, which controls the size of the offsets", "screen.tooltip.xtracr_realcamera.modelView_selecting": "%s+Left Click to get the UV coordinates at the mouse pointer\nThe three sets of UV coordinates below are, from top to bottom, the UV coordinates of the forward vector, the upward vector, and the binding point on the target plane\nNote: %s+Scroll to switch between the different layers of the model. Manually inputting UV coordinates allows you to bind more accurately to the target position", @@ -97,10 +99,14 @@ "screen.tooltip.xtracr_realcamera.modelView_textureId": "The texture id of the selected model (not necessary to be the full id), can be shortened when saving to make it easier to recognize.\nFor example, shorten minecraft:textures/entity/player/slim/alex.png to minecraft:textures/entity/player/.", "screen.tooltip.xtracr_realcamera.modelView_toConfigScreen": "Open Global Config Screen\n(Cloth Config API required)", "screen.widget.xtracr_realcamera.modelView_all": "All", + "screen.widget.xtracr_realcamera.modelView_bindPositionAxis": "Bind position %s: %s", + "screen.widget.xtracr_realcamera.modelView_bindRotationAxis": "Bind rotation %s: %s", "screen.widget.xtracr_realcamera.modelView_configs": "Configs", "screen.widget.xtracr_realcamera.modelView_currentConfig": "Current Config", "screen.widget.xtracr_realcamera.modelView_disable": "Disable", "screen.widget.xtracr_realcamera.modelView_disableMode": "Disable Mode", + "screen.widget.xtracr_realcamera.modelView_disabled": "Disabled", + "screen.widget.xtracr_realcamera.modelView_enabled": "Enabled", "screen.widget.xtracr_realcamera.modelView_export": "Export", "screen.widget.xtracr_realcamera.modelView_forwardVector": "Forward Vector", "screen.widget.xtracr_realcamera.modelView_import": "Import", @@ -114,6 +120,7 @@ "screen.widget.xtracr_realcamera.modelView_preview": "Preview", "screen.widget.xtracr_realcamera.modelView_range": "Range", "screen.widget.xtracr_realcamera.modelView_roll": "Roll = %s", + "screen.widget.xtracr_realcamera.modelView_resetOffsets": "Reset all camera offsets", "screen.widget.xtracr_realcamera.modelView_save": "Save", "screen.widget.xtracr_realcamera.modelView_selecting": "Selecting", "screen.widget.xtracr_realcamera.modelView_selectionMode": "Selection Mode", 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 b5f22c3d..2fd4abce 100644 --- a/common/src/main/resources/assets/realcamera/lang/zh_cn.json +++ b/common/src/main/resources/assets/realcamera/lang/zh_cn.json @@ -39,7 +39,7 @@ "config.tooltip.xtracr_realcamera.adjustStep": "每步调整的角度的1/100或长度", "config.tooltip.xtracr_realcamera.bindResultRetentionFrames": "在绑定失败后的数帧内,沿用上个成功的绑定结果", "config.tooltip.xtracr_realcamera.cameraRotation": "额外的摄像头旋转角度", - "config.tooltip.xtracr_realcamera.cameraRotation_n": "注:俯仰、偏航和翻滚分别是以左方、上方和前方为轴的旋转", + "config.tooltip.xtracr_realcamera.cameraRotation_n": "注:俯仰、偏航和翻滚分别是以左方、上方和前方为轴的旋转。三者组成局部刚性旋转,因此一个偏移可能影响多个最终欧拉分量", "config.tooltip.xtracr_realcamera.centerOffset": "旋转中心相对头部的偏移值", "config.tooltip.xtracr_realcamera.classicAdjustMode": "决定当前按下调整键时调整哪一个", "config.tooltip.xtracr_realcamera.classicOffset": "摄像头相对旋转中心的偏移值", @@ -71,7 +71,8 @@ "key.xtracr_realcamera.togglePerspective": "开启/关闭", "message.xtracr_realcamera.bindingFailed": "[%s]: 绑定失败,请前往%s界面手动设置(键位:%s)", "modmenu.descriptionTranslation.realcamera": "使第一人称视角下的摄像头更加真实。", - "screen.tooltip.xtracr_realcamera.modelView_bindButtons": "当禁用时,摄像头与模型的相对关系不会变化,但摄像头的对应属性不会被本模组修改", + "screen.tooltip.xtracr_realcamera.modelView_bindPositionButton": "绑定%s位置。启用时使用模型绑定位置;禁用时保留原版相机位置", + "screen.tooltip.xtracr_realcamera.modelView_bindRotationButton": "绑定%s欧拉分量。启用时使用已应用旋转偏移的模型绑定相机分量;禁用时保留原版相机分量。俯仰角位于或接近 ±90° 时,存在歧义的偏航与翻滚会从前帧连续选解;重置后则以原版朝向为参照", "screen.tooltip.xtracr_realcamera.modelView_configs": "已保存的配置", "screen.tooltip.xtracr_realcamera.modelView_deleteSelectedRectangle": "删除选中的矩形\n(或使用Delete键)", "screen.tooltip.xtracr_realcamera.modelView_depth": "全部顶点到屏幕距离均小于该值的面不会被渲染", @@ -89,6 +90,7 @@ "screen.tooltip.xtracr_realcamera.modelView_importSucceeded": "导入%s成功!", "screen.tooltip.xtracr_realcamera.modelView_preview": "在预览模式可以调整相机的各个偏移量", "screen.tooltip.xtracr_realcamera.modelView_priority": "绑定时的优先级,值越大优先级越高,并且在右侧排序越靠上", + "screen.tooltip.xtracr_realcamera.modelView_resetOffsets": "将 X/Y/Z 位置偏移和俯仰/偏航/翻滚旋转偏移全部归零", "screen.tooltip.xtracr_realcamera.modelView_saveAs": "添加/更新此禁用设置\n再点左侧保存以保存到当前配置", "screen.tooltip.xtracr_realcamera.modelView_scale": "缩放比例,控制偏移量的大小", "screen.tooltip.xtracr_realcamera.modelView_selecting": "%s+左键可获取鼠标指针处的UV坐标\n下方三组UV坐标由上到下依次为向前矢量、向上矢量和目标平面上绑定点的UV坐标\n注:%s键+滚轮可在模型的不同层间切换,手动输入UV坐标可以更加精确地绑定到目标位置", @@ -97,10 +99,14 @@ "screen.tooltip.xtracr_realcamera.modelView_textureId": "被选中的模型的纹理的id(不是完整的id也可以识别),保存时可以缩短一部分以便于程序识别\n比如将minecraft:textures/entity/player/slim/alex.png缩短为minecraft:textures/entity/player/", "screen.tooltip.xtracr_realcamera.modelView_toConfigScreen": "打开全局配置屏幕\n(需要安装Cloth Config API)", "screen.widget.xtracr_realcamera.modelView_all": "全部", + "screen.widget.xtracr_realcamera.modelView_bindPositionAxis": "绑定%s位置:%s", + "screen.widget.xtracr_realcamera.modelView_bindRotationAxis": "绑定%s:%s", "screen.widget.xtracr_realcamera.modelView_configs": "配置", "screen.widget.xtracr_realcamera.modelView_currentConfig": "当前配置", "screen.widget.xtracr_realcamera.modelView_disable": "禁用", "screen.widget.xtracr_realcamera.modelView_disableMode": "禁用模式", + "screen.widget.xtracr_realcamera.modelView_disabled": "已禁用", + "screen.widget.xtracr_realcamera.modelView_enabled": "已启用", "screen.widget.xtracr_realcamera.modelView_export": "导出", "screen.widget.xtracr_realcamera.modelView_forwardVector": "向前矢量", "screen.widget.xtracr_realcamera.modelView_import": "导入", @@ -114,6 +120,7 @@ "screen.widget.xtracr_realcamera.modelView_preview": "预览", "screen.widget.xtracr_realcamera.modelView_range": "范围", "screen.widget.xtracr_realcamera.modelView_roll": "翻滚角 = %s", + "screen.widget.xtracr_realcamera.modelView_resetOffsets": "重置所有相机偏移", "screen.widget.xtracr_realcamera.modelView_save": "保存", "screen.widget.xtracr_realcamera.modelView_selecting": "选择", "screen.widget.xtracr_realcamera.modelView_selectionMode": "选择模式",