diff --git a/src/main/java/cam72cam/mod/ModCore.java b/src/main/java/cam72cam/mod/ModCore.java index aceb15296..c3c33452e 100644 --- a/src/main/java/cam72cam/mod/ModCore.java +++ b/src/main/java/cam72cam/mod/ModCore.java @@ -6,6 +6,7 @@ import cam72cam.mod.event.ClientEvents; import cam72cam.mod.gui.GuiRegistry; import cam72cam.mod.input.Mouse; +import cam72cam.mod.render.SmoothFloat; import cam72cam.mod.net.Packet; import cam72cam.mod.net.PacketDirection; import cam72cam.mod.render.BlockRender; @@ -290,6 +291,7 @@ public void clientEvent(ModEvent event) { }); BlockRender.onPostColorSetup(); ClientEvents.fireReload(); + ClientEvents.TICK.subscribe(SmoothFloat::onClientTick); break; } diff --git a/src/main/java/cam72cam/mod/event/ClientEvents.java b/src/main/java/cam72cam/mod/event/ClientEvents.java index bf8480080..109da14d3 100644 --- a/src/main/java/cam72cam/mod/event/ClientEvents.java +++ b/src/main/java/cam72cam/mod/event/ClientEvents.java @@ -6,6 +6,7 @@ import cam72cam.mod.gui.helpers.GUIHelpers; import cam72cam.mod.input.Mouse; import cam72cam.mod.math.Vec3d; +import cam72cam.mod.render.CameraUtils; import cam72cam.mod.render.EntityRenderer; import cam72cam.mod.render.GlobalRender; import cam72cam.mod.render.opengl.CustomTexture; @@ -212,6 +213,15 @@ public static void onRenderMouseover(DrawBlockHighlightEvent event) { RENDER_MOUSEOVER.execute(x -> x.accept(event.getPartialTicks())); } + @SubscribeEvent + public static void onCameraSetup(EntityViewRenderEvent.CameraSetup event) { + CameraUtils.applyTranslation(event); + } + + public static void onFOVSetup(EntityViewRenderEvent.FOVModifier event) { + CameraUtils.applyFov(event); + } + @SubscribeEvent public static void onSoundLoad(SoundLoadEvent event) { SOUND_LOAD.execute(x -> x.accept(event)); diff --git a/src/main/java/cam72cam/mod/render/CameraUtils.java b/src/main/java/cam72cam/mod/render/CameraUtils.java new file mode 100644 index 000000000..d9949c7f1 --- /dev/null +++ b/src/main/java/cam72cam/mod/render/CameraUtils.java @@ -0,0 +1,148 @@ +package cam72cam.mod.render; + +import cam72cam.mod.MinecraftClient; +import cam72cam.mod.math.Vec3d; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraftforge.client.event.EntityViewRenderEvent; + +import java.util.*; + +public class CameraUtils { + private static final List controllers = new ArrayList<>(); + private static float cameraRoll; + private static boolean cameraColliding; + + public static Perspective getPerspective() { + if (MinecraftClient.isReady()) { + switch (Minecraft.getMinecraft().gameSettings.thirdPersonView) { + case 0: + return Perspective.FIRST_PERSON; + case 1: + return Perspective.THIRD_PERSON; + case 2: + return Perspective.THIRD_PERSON_INVERTED; + } + } + + return Perspective.FIRST_PERSON; + } + + public static void setPerspective(Perspective perspective) { + if (MinecraftClient.isReady()) { + switch (perspective) { + case FIRST_PERSON: + Minecraft.getMinecraft().gameSettings.thirdPersonView = 0; + return; + case THIRD_PERSON: + Minecraft.getMinecraft().gameSettings.thirdPersonView = 1; + return; + case THIRD_PERSON_INVERTED: + Minecraft.getMinecraft().gameSettings.thirdPersonView = 2; + } + } + } + + public static float getFov() { + return Minecraft.getMinecraft().gameSettings.fovSetting; + } + + /** Get global position of the player's eyes (with partialTicks taken into account) */ + public static Vec3d getCameraPos(float partialTicks) { + net.minecraft.entity.Entity playerRender = Minecraft.getMinecraft().getRenderViewEntity(); + double d0 = playerRender.lastTickPosX + (playerRender.posX - playerRender.lastTickPosX) * partialTicks; + double d1 = playerRender.lastTickPosY + (playerRender.posY - playerRender.lastTickPosY) * partialTicks; + double d2 = playerRender.lastTickPosZ + (playerRender.posZ - playerRender.lastTickPosZ) * partialTicks; + return new Vec3d(d0, d1, d2); + } + + public static float getCameraYaw(float partialTicks) { + net.minecraft.entity.Entity playerRender = Minecraft.getMinecraft().getRenderViewEntity(); + return playerRender.prevRotationYaw + (playerRender.rotationYaw - playerRender.prevRotationYaw) * partialTicks; + } + + public static float getCameraPitch(float partialTicks) { + net.minecraft.entity.Entity playerRender = Minecraft.getMinecraft().getRenderViewEntity(); + return playerRender.prevRotationPitch + (playerRender.rotationPitch - playerRender.prevRotationPitch) * partialTicks; + } + + public static float getCameraRoll() { + return cameraRoll; + } + + public static Controller newController(Perspective... activeIn) { + return new Controller(Arrays.asList(activeIn)); + } + + public static void applyTranslation(EntityViewRenderEvent.CameraSetup event) { + cameraRoll = event.getRoll(); + + float x = 0, y = 0, z = 0; + float yaw = 0, pitch = 0, roll = 0; + + float partialTicks = (float) event.getRenderPartialTicks(); + + Perspective perspective = getPerspective(); + for (Controller controller : controllers) { + if (!controller.perspectives.contains(perspective)) + continue; + x += controller.xOffset.getValue(partialTicks); + y += controller.yOffset.getValue(partialTicks); + z += controller.zOffset.getValue(partialTicks); + yaw += controller.yawOffset.getValue(partialTicks); + pitch += controller.pitchOffset.getValue(partialTicks); + roll += controller.rollOffset.getValue(partialTicks); + } + + GlStateManager.translate(x, y, z); + event.setYaw(yaw); + event.setPitch(pitch); + event.setRoll(roll); + } + + public static void applyFov(EntityViewRenderEvent.FOVModifier event) { + float fov = 0; + for (Controller controller : controllers) { + fov += controller.fovOffset.getValue((float) event.getRenderPartialTicks()); + } + event.setFOV(fov); + } + + public enum Perspective { + FIRST_PERSON, + THIRD_PERSON, + THIRD_PERSON_INVERTED, + } + + public static class Controller { + //+X is left, +Y is top, +Z is back + public final SmoothFloat xOffset; + public final SmoothFloat yOffset; + public final SmoothFloat zOffset; + + //Roll is the first to be applied, then pitch, then yaw + public final SmoothFloat rollOffset; + public final SmoothFloat pitchOffset; + public final SmoothFloat yawOffset; + + public final SmoothFloat fovOffset; + + final List perspectives; + + private Controller(List perspectives) { + this.xOffset = new SmoothFloat(0); + this.yOffset = new SmoothFloat(0); + this.zOffset = new SmoothFloat(0); + + this.yawOffset = new SmoothFloat(0); + this.pitchOffset = new SmoothFloat(0); + this.rollOffset = new SmoothFloat(0); + + this.fovOffset = new SmoothFloat(0); + + this.perspectives = perspectives; + + controllers.add(this); + } + } +} diff --git a/src/main/java/cam72cam/mod/render/GlobalRender.java b/src/main/java/cam72cam/mod/render/GlobalRender.java index 865bbe960..53f0501c1 100644 --- a/src/main/java/cam72cam/mod/render/GlobalRender.java +++ b/src/main/java/cam72cam/mod/render/GlobalRender.java @@ -76,7 +76,11 @@ public static boolean isTransparentPass() { return MinecraftForgeClient.getRenderPass() != 0; } - /** Get global position of the player's eyes (with partialTicks taken into account) */ + /** + * Get global position of the player's eyes (with partialTicks taken into account) + * @deprecated use the one in CameraUtils + * */ + @Deprecated public static Vec3d getCameraPos(float partialTicks) { net.minecraft.entity.Entity playerRender = Minecraft.getMinecraft().getRenderViewEntity(); double d0 = playerRender.lastTickPosX + (playerRender.posX - playerRender.lastTickPosX) * partialTicks; diff --git a/src/main/java/cam72cam/mod/render/SmoothFloat.java b/src/main/java/cam72cam/mod/render/SmoothFloat.java new file mode 100644 index 000000000..c310d8a1f --- /dev/null +++ b/src/main/java/cam72cam/mod/render/SmoothFloat.java @@ -0,0 +1,169 @@ +package cam72cam.mod.render; + +import net.minecraft.util.math.MathHelper; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import java.lang.ref.WeakReference; +import java.util.Deque; +import java.util.Iterator; +import java.util.concurrent.ConcurrentLinkedDeque; + +/** + * A class for smoothing input values with Hermite function (3t^2 - 2t^3) + */ +@SideOnly(Side.CLIENT) +public class SmoothFloat { + private static final Deque> instances = new ConcurrentLinkedDeque<>(); + + private float currentValue; + private float currentVelocity; + + private float startValue; + private float startVelocity; + + private float targetValue; + private float targetVelocity; + + private float durationTicks; + private float elapsedTicks; + private boolean active; + + public SmoothFloat(float value) { + this.currentValue = value; + this.currentVelocity = 0.0f; + + this.startValue = value; + this.startVelocity = 0.0f; + + this.targetValue = value; + this.targetVelocity = 0.0f; + + this.durationTicks = 0.0f; + this.elapsedTicks = 0.0f; + this.active = false; + + instances.add(new WeakReference<>(this)); + } + + public float getValue(float partialTicks) { + if (!active || durationTicks <= 0.0f) { + return currentValue; + } + + float t = MathHelper.clamp((elapsedTicks + MathHelper.clamp(partialTicks, 0, 1)) / durationTicks, 0, 1); + return evaluatePosition(t); + } + + public float getTargetValue() { + return targetValue; + } + + public void setNewValue(float newValue, float expectedTicks) { + if (expectedTicks <= 0.0f) { + //If we want to set it instantly + currentValue = newValue; + currentVelocity = 0.0f; + + startValue = newValue; + startVelocity = 0.0f; + + targetValue = newValue; + targetVelocity = 0.0f; + + durationTicks = 0.0f; + elapsedTicks = 0.0f; + active = false; + return; + } + + //Or use current velocity + float v = getCurrentVelocityAtTime(elapsedTicks, durationTicks); + + currentValue = getValue(0.0f); + + startValue = currentValue; + startVelocity = v; + + targetValue = newValue; + targetVelocity = 0.0f; + + durationTicks = expectedTicks; + elapsedTicks = 0.0f; + active = true; + } + + private void tick() { + if (!active || durationTicks <= 0.0f) { + return; + } + + elapsedTicks += 1.0f; + + if (elapsedTicks >= durationTicks) { + currentValue = targetValue; + currentVelocity = targetVelocity; + active = false; + return; + } + + float t = MathHelper.clamp(elapsedTicks / durationTicks, 0, 1); + currentValue = evaluatePosition(t); + currentVelocity = evaluateVelocity(t) / durationTicks; + } + + public static void onClientTick() { + Iterator> it = instances.iterator(); + while (it.hasNext()) { + WeakReference ref = it.next(); + SmoothFloat instance = ref.get(); + if (instance != null) { + instance.tick(); + } else { + it.remove(); + } + } + } + + private float evaluatePosition(float t) { + t = MathHelper.clamp(t, 0, 1); + + float t2 = t * t; + float t3 = t2 * t; + + float h00 = 2.0f * t3 - 3.0f * t2 + 1.0f; + float h10 = t3 - 2.0f * t2 + t; + float h01 = -2.0f * t3 + 3.0f * t2; + float h11 = t3 - t2; + + return h00 * startValue + + h10 * startVelocity * durationTicks + + h01 * targetValue + + h11 * targetVelocity * durationTicks; + } + + private float evaluateVelocity(float t) { + t = MathHelper.clamp(t, 0, 1); + + float t2 = t * t; + + float dh00 = 6.0f * t2 - 6.0f * t; + float dh10 = 3.0f * t2 - 4.0f * t + 1.0f; + float dh01 = -6.0f * t2 + 6.0f * t; + float dh11 = 3.0f * t2 - 2.0f * t; + + return dh00 * startValue + + dh10 * startVelocity * durationTicks + + dh01 * targetValue + + dh11 * targetVelocity * durationTicks; + } + + private float getCurrentVelocityAtTime(float elapsedTicks, float durationTicks) { + if (!active || durationTicks <= 0.0f) { + return currentVelocity; + } + + float t = MathHelper.clamp(elapsedTicks / durationTicks, 0, 1); + return evaluateVelocity(t) / durationTicks; + } +} \ No newline at end of file