Rework cursor aim: cursor is world-fixed, pilot turns until reticle meets it
- Move cursor state to a world direction so the offset updates as the craft turns, not on mouse input alone - Move cursor aim tunables from separate TOML files into Config, under cursor_aim sections - Pilot banks by a level-wings cursor angle and pulls a level-turn rate, capped by live lift and measured control rate - Measure each cursor-flown node's steady pitch rate from control torque over pitch damping - Accumulate maximum stall-limited lift per tick for the pilot's turn-rate and bank limits - Bump mod version to 0.4.2
This commit is contained in:
1 parent
23f0f91ede
commit
3478a04885
10 files changed
+314
-177
No files matched your search
+1
-1
@@ -12,7 +12,7 @@ mapping_version=2023.09.03-1.20.1
|
|||||||
mod_id=improved_aerodynamics
|
mod_id=improved_aerodynamics
|
||||||
mod_name=VS: Improved Aerodynamics
|
mod_name=VS: Improved Aerodynamics
|
||||||
mod_license=MIT
|
mod_license=MIT
|
||||||
mod_version=0.4.0
|
mod_version=0.4.2
|
||||||
azurelib_version=3.2.7
|
azurelib_version=3.2.7
|
||||||
mod_group_id=dev.lumareth.improvedaerodynamics
|
mod_group_id=dev.lumareth.improvedaerodynamics
|
||||||
mod_authors=Lumareth
|
mod_authors=Lumareth
|
||||||
|
|||||||
@@ -204,6 +204,47 @@ public final class Config {
|
|||||||
.comment("The nozzle gives off light while its afterburner is lit")
|
.comment("The nozzle gives off light while its afterburner is lit")
|
||||||
.define("lightEnabled", true);
|
.define("lightEnabled", true);
|
||||||
|
|
||||||
|
static {
|
||||||
|
BUILDER.pop();
|
||||||
|
BUILDER.comment("Cursor aim: the mouse moves a cursor on screen, a virtual pilot flies the control node's craft by it").push("cursor_aim");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.BooleanValue CURSOR_AIM_ENABLED = BUILDER.comment("Whether pilots may use cursor aim on this server")
|
||||||
|
.define("enabled", true);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_FULL_DEFLECTION_DEGREES = BUILDER.comment("Angle of the cursor from the gun reticle at which the virtual pilot turns as hard as it can: the maximum bank",
|
||||||
|
"sideways, the full pitch rate up or down. Closer than that it turns more gently, so the reticle settles onto",
|
||||||
|
"the cursor. How fast the craft can turn is worked out from the craft itself")
|
||||||
|
.defineInRange("fullDeflectionDegrees", 30.0, 5.0, 90.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_DEADZONE_DEGREES = BUILDER.comment("Radius around the gun reticle in degrees inside which the pilot stops steering towards the cursor. The reticle",
|
||||||
|
"then settles this far from the cursor. Outside it the command grows from zero, there is no jump at the edge")
|
||||||
|
.defineInRange("deadzoneDegrees", 0.0, 0.0, 15.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MAX_ROLL_RATE_DEGREES = BUILDER.comment("Fastest roll in degrees per second the pilot asks for when it rolls to a bank angle")
|
||||||
|
.defineInRange("maxRollRateDegrees", 150.0, 10.0, 720.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MAX_BANK_DEGREES = BUILDER.comment("Bank angle the pilot rolls to when the cursor is at full sideways deflection, at most. Craft whose wings",
|
||||||
|
"cannot hold that bank in a level turn get less")
|
||||||
|
.defineInRange("maxBankDegrees", 70.0, 10.0, 90.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_BANK_GAIN = BUILDER.comment("Roll rate in rad/s per radian of bank angle error, how eagerly the pilot rolls to the wanted bank and levels the wings")
|
||||||
|
.defineInRange("bankGain", 3.0, 0.2, 20.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_RATE_GAIN = BUILDER.comment("How hard the pilot corrects its turn rate, in angular acceleration per rad/s of rate error. Lower is smoother,",
|
||||||
|
"higher follows the cursor tighter and can start to oscillate")
|
||||||
|
.defineInRange("rateGain", 5.0, 0.5, 40.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_PUSH_FACTOR = BUILDER.comment("A cursor below the gun reticle pushes the nose down. This is how much of the pull rate that may be, since",
|
||||||
|
"negative g is harder on the craft and the pilot")
|
||||||
|
.defineInRange("pushFactor", 0.4, 0.0, 1.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_STALLED_PITCH_FACTOR = BUILDER.comment("Multiplier for the pitch rate the pilot asks for while the wings are stalled, so it does not keep pulling")
|
||||||
|
.defineInRange("stalledPitchFactor", 0.3, 0.0, 1.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MIN_SPEED = BUILDER.comment("Below this speed in blocks per second the pilot does nothing, the surfaces have no effect anyway")
|
||||||
|
.defineInRange("minSpeed", 15.0, 0.0, 200.0);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
BUILDER.pop();
|
BUILDER.pop();
|
||||||
}
|
}
|
||||||
@@ -225,6 +266,40 @@ public final class Config {
|
|||||||
"fixed 20 degree steps. Needs physicsRateAerodynamics on the server")
|
"fixed 20 degree steps. Needs physicsRateAerodynamics on the server")
|
||||||
.define("animateControlSurfaces", true);
|
.define("animateControlSurfaces", true);
|
||||||
|
|
||||||
|
static {
|
||||||
|
CLIENT_BUILDER.comment("Cursor aim, client side").push("cursor_aim");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_LOOK_AHEAD = CLIENT_BUILDER.comment("How strongly the camera looks into the direction of the turn. 0 never moves it, 1 (100%) turns it quickly",
|
||||||
|
"to look where the cursor points. In between it follows more slowly")
|
||||||
|
.defineInRange("lookAhead", 0.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MOUSE_SENSITIVITY = CLIENT_BUILDER.comment("Multiplier for how far the mouse moves the cursor (or the camera while the free look key is held)")
|
||||||
|
.defineInRange("mouseSensitivity", 1.0, 0.05, 10.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_SPRING_BACK_SECONDS = CLIENT_BUILDER.comment("The cursor drifts to the gun reticle with this time constant in seconds, which ends the turn by itself.",
|
||||||
|
"0, the default, keeps it where you put it in the world, so the craft turns until the reticle is on it")
|
||||||
|
.defineInRange("springBackSeconds", 0.0, 0.0, 10.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MAX_ANGLE_DEGREES = CLIENT_BUILDER.comment("Furthest the cursor can go from the gun reticle, 135 is a 270 degree cone")
|
||||||
|
.defineInRange("maxCursorAngleDegrees", 135.0, 10.0, 180.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_ZOOM_STEP = CLIENT_BUILDER.comment("Blocks the camera moves per mouse wheel notch")
|
||||||
|
.defineInRange("zoomStep", 2.0, 0.1, 20.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_ZOOM_MAX = CLIENT_BUILDER.comment("Furthest the camera can be pulled back from its normal distance, in blocks")
|
||||||
|
.defineInRange("zoomMax", 80.0, 0.0, 500.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MARKER_SIZE = CLIENT_BUILDER.comment("Radius of the cursor ring on screen")
|
||||||
|
.defineInRange("markerSize", 0.04, 0.01, 0.3);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.BooleanValue CURSOR_AIM_SHOW_RETICLE = CLIENT_BUILDER.comment("Draw a small cross where the gun reticle points, the zero of the cursor, so you can see it when the camera looks away")
|
||||||
|
.define("showReticle", true);
|
||||||
|
|
||||||
|
static {
|
||||||
|
CLIENT_BUILDER.pop();
|
||||||
|
}
|
||||||
|
|
||||||
static final ForgeConfigSpec CLIENT_SPEC = CLIENT_BUILDER.build();
|
static final ForgeConfigSpec CLIENT_SPEC = CLIENT_BUILDER.build();
|
||||||
|
|
||||||
private Config() {
|
private Config() {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package dev.lumareth.improvedAerodynamics;
|
package dev.lumareth.improvedAerodynamics;
|
||||||
|
|
||||||
import com.mojang.logging.LogUtils;
|
import com.mojang.logging.LogUtils;
|
||||||
import dev.lumareth.improvedAerodynamics.client.aim.CursorAimClientConfig;
|
|
||||||
import dev.lumareth.improvedAerodynamics.client.pipper.PipperConfig;
|
import dev.lumareth.improvedAerodynamics.client.pipper.PipperConfig;
|
||||||
import dev.lumareth.improvedAerodynamics.exhaust.ExhaustSync;
|
import dev.lumareth.improvedAerodynamics.exhaust.ExhaustSync;
|
||||||
import dev.lumareth.improvedAerodynamics.exhaust.NozzleContent;
|
import dev.lumareth.improvedAerodynamics.exhaust.NozzleContent;
|
||||||
import dev.lumareth.improvedAerodynamics.flight.CursorAimConfig;
|
|
||||||
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
|
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
|
||||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
import dev.lumareth.improvedAerodynamics.network.Network;
|
||||||
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
|
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
|
||||||
@@ -34,8 +32,6 @@ public class ImprovedAerodynamics {
|
|||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.CLIENT_SPEC);
|
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.CLIENT_SPEC);
|
||||||
// Own file name: the default one is taken by Config.CLIENT_SPEC
|
// Own file name: the default one is taken by Config.CLIENT_SPEC
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, PipperConfig.SPEC, "improved_aerodynamics-pipper-client.toml");
|
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, PipperConfig.SPEC, "improved_aerodynamics-pipper-client.toml");
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, CursorAimConfig.SPEC, "improved_aerodynamics-cursor-aim.toml");
|
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, CursorAimClientConfig.SPEC, "improved_aerodynamics-cursor-aim-client.toml");
|
|
||||||
NozzleContent.register(FMLJavaModLoadingContext.get().getModEventBus());
|
NozzleContent.register(FMLJavaModLoadingContext.get().getModEventBus());
|
||||||
Network.register();
|
Network.register();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.aim;
|
package dev.lumareth.improvedAerodynamics.client.aim;
|
||||||
|
|
||||||
|
import dev.lumareth.improvedAerodynamics.Config;
|
||||||
import com.mojang.blaze3d.systems.RenderSystem;
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
import com.mojang.blaze3d.vertex.BufferUploader;
|
||||||
@@ -44,9 +45,10 @@ import java.lang.reflect.Method;
|
|||||||
/**
|
/**
|
||||||
* Cursor aim, the client side. While it is on:
|
* Cursor aim, the client side. While it is on:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>the mouse moves a cursor instead of the camera. The cursor is a direction relative to the gun reticle (the
|
* <li>the mouse moves a cursor instead of the camera. The cursor is a fixed direction in the world: the craft
|
||||||
* craft's boresight), at most {@code maxCursorAngleDegrees} away from it, and springs back to it when the mouse
|
* turning does not drag it along, so the virtual pilot on the server steers until the gun reticle (the craft's
|
||||||
* rests. It is sent to the server every tick, where a virtual pilot flies the craft by it;</li>
|
* boresight) sits on the cursor again. It is kept at most {@code maxCursorAngleDegrees} from the reticle and, if
|
||||||
|
* spring back is set, drifts back to it. Every tick it is sent to the server as an offset from the reticle;</li>
|
||||||
* <li>the camera is a chase view, looking along the reticle, or, with look ahead, towards the cursor;</li>
|
* <li>the camera is a chase view, looking along the reticle, or, with look ahead, towards the cursor;</li>
|
||||||
* <li>holding the free look key moves the camera instead of the cursor, it eases back when released;</li>
|
* <li>holding the free look key moves the camera instead of the cursor, it eases back when released;</li>
|
||||||
* <li>the mouse wheel changes how far the camera is behind the craft.</li>
|
* <li>the mouse wheel changes how far the camera is behind the craft.</li>
|
||||||
@@ -66,7 +68,9 @@ public final class CursorAimClient {
|
|||||||
private static boolean active;
|
private static boolean active;
|
||||||
// Chosen with /aero controls, told by the server
|
// Chosen with /aero controls, told by the server
|
||||||
private static boolean cursorAimMode;
|
private static boolean cursorAimMode;
|
||||||
// Cursor as components along the craft's forward (gun reticle), right and up axes
|
// Cursor as a world direction, null until the first frame puts it on the reticle
|
||||||
|
private static Vector3d cursorWorld;
|
||||||
|
// The same cursor as components along the craft's forward (gun reticle), right and up axes, as of the last update
|
||||||
private static double cf = 1.0;
|
private static double cf = 1.0;
|
||||||
private static double cr;
|
private static double cr;
|
||||||
private static double cu;
|
private static double cu;
|
||||||
@@ -109,9 +113,49 @@ public final class CursorAimClient {
|
|||||||
deactivate(minecraft);
|
deactivate(minecraft);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The craft has turned since the last frame: the cursor stays put in the world, its offset changes
|
||||||
|
Entity seat = AimSeat.seatOf(player);
|
||||||
|
AimBasis basis = minecraft.level != null ? AimBasis.of(minecraft.level, seat.blockPosition()) : null;
|
||||||
|
Quaterniondc rotation = basis != null ? shipRotation(minecraft.level, seat, false) : null;
|
||||||
|
if (rotation != null) {
|
||||||
|
cursorFromWorld(basis, rotation);
|
||||||
|
}
|
||||||
send(minecraft, true);
|
send(minecraft, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rotation of the ship the seat is on, its render transform when drawing, null if it is not on a ship. */
|
||||||
|
private static Quaterniondc shipRotation(ClientLevel level, Entity seat, boolean render) {
|
||||||
|
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, BlockPos.containing(seat.getX(), seat.getY(), seat.getZ()));
|
||||||
|
if (ship == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return render && ship instanceof ClientShip clientShip
|
||||||
|
? clientShip.getRenderTransform().getShipToWorldRotation() : ship.getTransform().getShipToWorldRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the cursor's components from its world direction, putting it on the reticle the first time. */
|
||||||
|
private static void cursorFromWorld(AimBasis basis, Quaterniondc rotation) {
|
||||||
|
if (cursorWorld == null) {
|
||||||
|
double[] forward = basis.forward();
|
||||||
|
cursorWorld = rotation.transform(new Vector3d(forward[0], forward[1], forward[2]));
|
||||||
|
}
|
||||||
|
Vector3d ship = rotation.transformInverse(cursorWorld, new Vector3d());
|
||||||
|
double[] f = basis.forward();
|
||||||
|
double[] r = basis.right();
|
||||||
|
double[] u = basis.up();
|
||||||
|
cf = ship.x * f[0] + ship.y * f[1] + ship.z * f[2];
|
||||||
|
cr = ship.x * r[0] + ship.y * r[1] + ship.z * r[2];
|
||||||
|
cu = ship.x * u[0] + ship.y * u[1] + ship.z * u[2];
|
||||||
|
normalizeCursor();
|
||||||
|
limitCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stores the cursor's components back as its world direction. */
|
||||||
|
private static void cursorToWorld(AimBasis basis, Quaterniondc rotation) {
|
||||||
|
double[] ship = basis.toShip(cf, cr, cu);
|
||||||
|
cursorWorld = rotation.transform(new Vector3d(ship[0], ship[1], ship[2]));
|
||||||
|
}
|
||||||
|
|
||||||
/** Called when the server tells the client how its player flies. */
|
/** Called when the server tells the client how its player flies. */
|
||||||
public static void setMode(boolean cursorAim) {
|
public static void setMode(boolean cursorAim) {
|
||||||
cursorAimMode = cursorAim;
|
cursorAimMode = cursorAim;
|
||||||
@@ -135,6 +179,7 @@ public final class CursorAimClient {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
active = true;
|
active = true;
|
||||||
|
cursorWorld = null;
|
||||||
cf = 1.0;
|
cf = 1.0;
|
||||||
cr = 0.0;
|
cr = 0.0;
|
||||||
cu = 0.0;
|
cu = 0.0;
|
||||||
@@ -184,7 +229,8 @@ public final class CursorAimClient {
|
|||||||
}
|
}
|
||||||
Entity seat = AimSeat.seatOf(player);
|
Entity seat = AimSeat.seatOf(player);
|
||||||
AimBasis basis = AimBasis.of(level, seat.blockPosition());
|
AimBasis basis = AimBasis.of(level, seat.blockPosition());
|
||||||
if (basis == null) {
|
Quaterniondc rotation = basis != null ? shipRotation(level, seat, true) : null;
|
||||||
|
if (basis == null || rotation == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (minecraft.options.getCameraType() != CameraType.THIRD_PERSON_BACK) {
|
if (minecraft.options.getCameraType() != CameraType.THIRD_PERSON_BACK) {
|
||||||
@@ -207,10 +253,11 @@ public final class CursorAimClient {
|
|||||||
deltaPitch = 0.0;
|
deltaPitch = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
double sensitivity = CursorAimClientConfig.MOUSE_SENSITIVITY.get();
|
double sensitivity = Config.CURSOR_AIM_MOUSE_SENSITIVITY.get();
|
||||||
boolean freeLook = minecraft.screen == null && AimKeys.FREE_LOOK.isDown();
|
boolean freeLook = minecraft.screen == null && AimKeys.FREE_LOOK.isDown();
|
||||||
|
|
||||||
double spring = CursorAimClientConfig.SPRING_BACK_SECONDS.get();
|
cursorFromWorld(basis, rotation);
|
||||||
|
double spring = Config.CURSOR_AIM_SPRING_BACK_SECONDS.get();
|
||||||
if (spring > 0.0) {
|
if (spring > 0.0) {
|
||||||
shrinkCursor(Math.exp(-dt / spring));
|
shrinkCursor(Math.exp(-dt / spring));
|
||||||
}
|
}
|
||||||
@@ -223,9 +270,10 @@ public final class CursorAimClient {
|
|||||||
offsetYaw *= decay;
|
offsetYaw *= decay;
|
||||||
offsetPitch *= decay;
|
offsetPitch *= decay;
|
||||||
}
|
}
|
||||||
|
cursorToWorld(basis, rotation);
|
||||||
|
|
||||||
// Camera: along the reticle, or turned towards the cursor by the look ahead setting
|
// Camera: along the reticle, or turned towards the cursor by the look ahead setting
|
||||||
double lookAhead = CursorAimClientConfig.LOOK_AHEAD.get();
|
double lookAhead = Config.CURSOR_AIM_LOOK_AHEAD.get();
|
||||||
double[] forward = basis.forward();
|
double[] forward = basis.forward();
|
||||||
if (cameraDirection == null || lookAhead <= 0.0) {
|
if (cameraDirection == null || lookAhead <= 0.0) {
|
||||||
cameraDirection = forward.clone();
|
cameraDirection = forward.clone();
|
||||||
@@ -295,7 +343,7 @@ public final class CursorAimClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void limitCursor() {
|
private static void limitCursor() {
|
||||||
double limit = Math.toRadians(CursorAimClientConfig.MAX_CURSOR_ANGLE_DEGREES.get());
|
double limit = Math.toRadians(Config.CURSOR_AIM_MAX_ANGLE_DEGREES.get());
|
||||||
double angle = Math.acos(Mth.clamp(cf, -1.0, 1.0));
|
double angle = Math.acos(Mth.clamp(cf, -1.0, 1.0));
|
||||||
if (angle <= limit) {
|
if (angle <= limit) {
|
||||||
return;
|
return;
|
||||||
@@ -318,7 +366,7 @@ public final class CursorAimClient {
|
|||||||
if (!active || minecraft.screen != null) {
|
if (!active || minecraft.screen != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
zoom = Mth.clamp(zoom - event.getScrollDelta() * CursorAimClientConfig.ZOOM_STEP.get(), -3.0, CursorAimClientConfig.ZOOM_MAX.get());
|
zoom = Mth.clamp(zoom - event.getScrollDelta() * Config.CURSOR_AIM_ZOOM_STEP.get(), -3.0, Config.CURSOR_AIM_ZOOM_MAX.get());
|
||||||
event.setCanceled(true);
|
event.setCanceled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,14 +402,10 @@ public final class CursorAimClient {
|
|||||||
}
|
}
|
||||||
Entity seat = AimSeat.seatOf(player);
|
Entity seat = AimSeat.seatOf(player);
|
||||||
AimBasis basis = AimBasis.of(level, seat.blockPosition());
|
AimBasis basis = AimBasis.of(level, seat.blockPosition());
|
||||||
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, BlockPos.containing(seat.getX(), seat.getY(), seat.getZ()));
|
Quaterniondc rotation = basis != null ? shipRotation(level, seat, true) : null;
|
||||||
if (basis == null || ship == null) {
|
if (rotation == null || cursorWorld == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Quaterniondc rotation = ship instanceof ClientShip clientShip
|
|
||||||
? clientShip.getRenderTransform().getShipToWorldRotation() : ship.getTransform().getShipToWorldRotation();
|
|
||||||
double[] cursorShip = basis.toShip(cf, cr, cu);
|
|
||||||
Vector3d cursorWorld = rotation.transform(new Vector3d(cursorShip[0], cursorShip[1], cursorShip[2]));
|
|
||||||
double[] forward = basis.forward();
|
double[] forward = basis.forward();
|
||||||
Vector3d reticleWorld = rotation.transform(new Vector3d(forward[0], forward[1], forward[2]));
|
Vector3d reticleWorld = rotation.transform(new Vector3d(forward[0], forward[1], forward[2]));
|
||||||
|
|
||||||
@@ -375,8 +419,8 @@ public final class CursorAimClient {
|
|||||||
RenderSystem.setShader(GameRenderer::getPositionColorShader);
|
RenderSystem.setShader(GameRenderer::getPositionColorShader);
|
||||||
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
|
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
|
||||||
|
|
||||||
float size = (float) (CursorAimClientConfig.MARKER_SIZE.get() * MARKER_DISTANCE);
|
float size = (float) (Config.CURSOR_AIM_MARKER_SIZE.get() * MARKER_DISTANCE);
|
||||||
if (CursorAimClientConfig.SHOW_RETICLE.get()) {
|
if (Config.CURSOR_AIM_SHOW_RETICLE.get()) {
|
||||||
plus(builder, matrix, camera, reticleWorld, size * 0.35F, RETICLE_COLOR);
|
plus(builder, matrix, camera, reticleWorld, size * 0.35F, RETICLE_COLOR);
|
||||||
}
|
}
|
||||||
ring(builder, matrix, camera, cursorWorld, size, CURSOR_COLOR);
|
ring(builder, matrix, camera, cursorWorld, size, CURSOR_COLOR);
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.aim;
|
|
||||||
|
|
||||||
import net.minecraftforge.common.ForgeConfigSpec;
|
|
||||||
|
|
||||||
/** Client side settings of cursor aim. Lives in improved_aerodynamics-cursor-aim-client.toml. */
|
|
||||||
public final class CursorAimClientConfig {
|
|
||||||
public static final ForgeConfigSpec SPEC;
|
|
||||||
|
|
||||||
public static final ForgeConfigSpec.DoubleValue LOOK_AHEAD;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MOUSE_SENSITIVITY;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue SPRING_BACK_SECONDS;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MAX_CURSOR_ANGLE_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue ZOOM_STEP;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue ZOOM_MAX;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MARKER_SIZE;
|
|
||||||
public static final ForgeConfigSpec.BooleanValue SHOW_RETICLE;
|
|
||||||
|
|
||||||
static {
|
|
||||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
|
||||||
builder.comment("Cursor aim, client side").push("cursor_aim");
|
|
||||||
LOOK_AHEAD = builder.comment("How strongly the camera looks into the direction of the turn. 0 never moves it, 1 (100%) turns it quickly",
|
|
||||||
"to look where the cursor points. In between it follows more slowly")
|
|
||||||
.defineInRange("lookAhead", 0.0, 0.0, 1.0);
|
|
||||||
MOUSE_SENSITIVITY = builder.comment("Multiplier for how far the mouse moves the cursor (or the camera while the free look key is held)")
|
|
||||||
.defineInRange("mouseSensitivity", 1.0, 0.05, 10.0);
|
|
||||||
SPRING_BACK_SECONDS = builder.comment("The cursor returns to the gun reticle with this time constant in seconds when you stop moving the mouse.",
|
|
||||||
"0 makes it stay where you left it")
|
|
||||||
.defineInRange("springBackSeconds", 1.0, 0.0, 10.0);
|
|
||||||
MAX_CURSOR_ANGLE_DEGREES = builder.comment("Furthest the cursor can go from the gun reticle, 135 is a 270 degree cone")
|
|
||||||
.defineInRange("maxCursorAngleDegrees", 135.0, 10.0, 180.0);
|
|
||||||
ZOOM_STEP = builder.comment("Blocks the camera moves per mouse wheel notch")
|
|
||||||
.defineInRange("zoomStep", 2.0, 0.1, 20.0);
|
|
||||||
ZOOM_MAX = builder.comment("Furthest the camera can be pulled back from its normal distance, in blocks")
|
|
||||||
.defineInRange("zoomMax", 80.0, 0.0, 500.0);
|
|
||||||
MARKER_SIZE = builder.comment("Radius of the cursor ring on screen")
|
|
||||||
.defineInRange("markerSize", 0.04, 0.01, 0.3);
|
|
||||||
SHOW_RETICLE = builder.comment("Draw a small cross where the gun reticle points, the zero of the cursor, so you can see it when the camera looks away")
|
|
||||||
.define("showReticle", true);
|
|
||||||
builder.pop();
|
|
||||||
SPEC = builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private CursorAimClientConfig() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
private static final double INPUT_RATE_PER_TICK = 0.2;
|
private static final double INPUT_RATE_PER_TICK = 0.2;
|
||||||
private static final double VELOCITY_EPSILON = 0.05;
|
private static final double VELOCITY_EPSILON = 0.05;
|
||||||
private static final double ANGULAR_EPSILON = 0.005;
|
private static final double ANGULAR_EPSILON = 0.005;
|
||||||
|
// Time constant of the smoothing of measured control rates
|
||||||
|
private static final double RATE_SMOOTHING_SECONDS = 0.3;
|
||||||
private static final ControlAxis[] MEASURED_AXES = {ControlAxis.PITCH, ControlAxis.YAW, ControlAxis.ROLL};
|
private static final ControlAxis[] MEASURED_AXES = {ControlAxis.PITCH, ControlAxis.YAW, ControlAxis.ROLL};
|
||||||
|
|
||||||
private final SubstepClock clock;
|
private final SubstepClock clock;
|
||||||
@@ -41,6 +43,9 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
private Frame current;
|
private Frame current;
|
||||||
private boolean[] stalled = new boolean[0];
|
private boolean[] stalled = new boolean[0];
|
||||||
private boolean wingStalled;
|
private boolean wingStalled;
|
||||||
|
// Lift the wings could make at the edge of the stall in the airflow of the last substep, NaN without the improved
|
||||||
|
// flight model, which has no stall
|
||||||
|
private double maxLift = Double.NaN;
|
||||||
private final Map<Long, NodeState> nodeStates = new ConcurrentHashMap<>();
|
private final Map<Long, NodeState> nodeStates = new ConcurrentHashMap<>();
|
||||||
// Every ship's node states by node position, for the control surface animation on the server thread
|
// Every ship's node states by node position, for the control surface animation on the server thread
|
||||||
private static final Map<Long, NodeState> ALL_NODE_STATES = new ConcurrentHashMap<>();
|
private static final Map<Long, NodeState> ALL_NODE_STATES = new ConcurrentHashMap<>();
|
||||||
@@ -71,6 +76,9 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
final Vector3d yawResponseBuilding = new Vector3d();
|
final Vector3d yawResponseBuilding = new Vector3d();
|
||||||
final Vector3d rollResponse = new Vector3d();
|
final Vector3d rollResponse = new Vector3d();
|
||||||
final Vector3d rollResponseBuilding = new Vector3d();
|
final Vector3d rollResponseBuilding = new Vector3d();
|
||||||
|
// Fastest steady pitch rate (rad/s) the pitch surfaces can hold against the craft's pitch damping, smoothed,
|
||||||
|
// measured while cursor aim flies the node. NaN until measured
|
||||||
|
double controlRate = Double.NaN;
|
||||||
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
|
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
|
||||||
volatile double savedTrim;
|
volatile double savedTrim;
|
||||||
|
|
||||||
@@ -155,9 +163,14 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
double dt = clock.substepSeconds();
|
double dt = clock.substepSeconds();
|
||||||
boolean improved = Config.IMPROVED_FLIGHT_MODEL.get();
|
boolean improved = Config.IMPROVED_FLIGHT_MODEL.get();
|
||||||
Matrix3dc inertia = ship.getMomentOfInertia();
|
Matrix3dc inertia = ship.getMomentOfInertia();
|
||||||
|
// Same frames and scaling as Warium applies its forces in
|
||||||
|
double scale = transform.getShipToWorldScaling().x();
|
||||||
|
double forceScale = autoScale ? scale * scale * scale : 1.0;
|
||||||
|
double liftAcceleration = maxLift * forceScale / ship.getMass();
|
||||||
|
|
||||||
updateNodes(frame, worldVelocity, transform.getPositionInWorld().y(), rotation, inertia, w, dt, improved);
|
updateNodes(frame, worldVelocity, transform.getPositionInWorld().y(), rotation, inertia, w, dt, improved, liftAcceleration);
|
||||||
evaluate(frame, centerOfMass, v, w, improved, true, wrench);
|
evaluate(frame, centerOfMass, v, w, improved, true, wrench);
|
||||||
|
measureControlRates(frame, centerOfMass, v, w, improved, dt);
|
||||||
|
|
||||||
if (Config.IMPLICIT_DAMPING.get()) {
|
if (Config.IMPLICIT_DAMPING.get()) {
|
||||||
// Implicit Euler for the damping part: scale each axis by 1 / (1 + dt * damping / inertia), using the
|
// Implicit Euler for the damping part: scale each axis by 1 / (1 + dt * damping / inertia), using the
|
||||||
@@ -184,9 +197,6 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same frames and scaling as Warium applies its forces in
|
|
||||||
double scale = transform.getShipToWorldScaling().x();
|
|
||||||
double forceScale = autoScale ? scale * scale * scale : 1.0;
|
|
||||||
Vector3d force = new Vector3d(wrench.force[0], wrench.force[1], wrench.force[2]).mul(forceScale);
|
Vector3d force = new Vector3d(wrench.force[0], wrench.force[1], wrench.force[2]).mul(forceScale);
|
||||||
Vector3d forceTorque = new Vector3d(wrench.forceTorque[0], wrench.forceTorque[1], wrench.forceTorque[2]).mul(forceScale);
|
Vector3d forceTorque = new Vector3d(wrench.forceTorque[0], wrench.forceTorque[1], wrench.forceTorque[2]).mul(forceScale);
|
||||||
if (force.isFinite() && forceTorque.isFinite()) {
|
if (force.isFinite() && forceTorque.isFinite()) {
|
||||||
@@ -239,6 +249,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
double[] r = new double[3];
|
double[] r = new double[3];
|
||||||
if (commit) {
|
if (commit) {
|
||||||
wingStalled = false;
|
wingStalled = false;
|
||||||
|
maxLift = improved ? 0.0 : Double.NaN;
|
||||||
for (NodeState state : nodeStates.values()) {
|
for (NodeState state : nodeStates.values()) {
|
||||||
state.pitchResponseBuilding.zero();
|
state.pitchResponseBuilding.zero();
|
||||||
state.yawResponseBuilding.zero();
|
state.yawResponseBuilding.zero();
|
||||||
@@ -261,6 +272,9 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
if (commit) {
|
if (commit) {
|
||||||
stalled[i] = nowStalled;
|
stalled[i] = nowStalled;
|
||||||
wingStalled |= nowStalled && s.type() == SurfaceType.WING;
|
wingStalled |= nowStalled && s.type() == SurfaceType.WING;
|
||||||
|
if (s.type() == SurfaceType.WING || s.type() == SurfaceType.CONTROL_SURFACE && s.wingGroup()) {
|
||||||
|
maxLift += ImprovedAero.maxLift(s, v, w, r, flap, weight, tuning);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
WariumAero.evaluate(s, v, w, r, input, weight, out);
|
WariumAero.evaluate(s, v, w, r, input, weight, out);
|
||||||
@@ -306,6 +320,37 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For every node cursor aim flies: the steady pitch rate its pitch surfaces can hold, full deflection torque over
|
||||||
|
* the craft's pitch damping, both measured about the axis the pitch surfaces turn the craft about.
|
||||||
|
*/
|
||||||
|
private void measureControlRates(Frame frame, Vector3dc centerOfMass, double[] v, double[] w, boolean improved, double dt) {
|
||||||
|
for (NodeData node : frame.nodes.values()) {
|
||||||
|
NodeState state = nodeStates.get(node.key());
|
||||||
|
if (state == null || !CursorAimRegistry.isActive(node.key())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double response = state.pitchResponse.length();
|
||||||
|
if (response < 1.0E-9) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Vector3d axis = state.pitchResponse.div(response, new Vector3d());
|
||||||
|
double[] turned = {w[0] + ANGULAR_EPSILON * axis.x, w[1] + ANGULAR_EPSILON * axis.y, w[2] + ANGULAR_EPSILON * axis.z};
|
||||||
|
evaluate(frame, centerOfMass, v, turned, improved, false, probe);
|
||||||
|
double damping = 0.0;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
damping -= (probe.totalTorque(i) - wrench.totalTorque(i)) * axis.get(i);
|
||||||
|
}
|
||||||
|
damping /= ANGULAR_EPSILON;
|
||||||
|
if (!(damping > 1.0E-9)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double rate = response / damping;
|
||||||
|
state.controlRate = Double.isNaN(state.controlRate) ? rate
|
||||||
|
: state.controlRate + (rate - state.controlRate) * Math.min(1.0, dt / RATE_SMOOTHING_SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** A control surface's input, combined from its two keys and clamped like Warium does. */
|
/** A control surface's input, combined from its two keys and clamped like Warium does. */
|
||||||
private static double input(AeroSurface s, NodeData node, NodeState state, ControlAxis nudged, double extra) {
|
private static double input(AeroSurface s, NodeData node, NodeState state, ControlAxis nudged, double extra) {
|
||||||
if (node == null || state == null) {
|
if (node == null || state == null) {
|
||||||
@@ -328,7 +373,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateNodes(Frame frame, Vector3dc worldVelocity, double altitude, Quaterniondc rotation, Matrix3dc inertia,
|
private void updateNodes(Frame frame, Vector3dc worldVelocity, double altitude, Quaterniondc rotation, Matrix3dc inertia,
|
||||||
double[] w, double dt, boolean improved) {
|
double[] w, double dt, boolean improved, double liftAcceleration) {
|
||||||
double step = Config.CONTROL_SMOOTHING.get() ? INPUT_RATE_PER_TICK * clock.weight() : Double.POSITIVE_INFINITY;
|
double step = Config.CONTROL_SMOOTHING.get() ? INPUT_RATE_PER_TICK * clock.weight() : Double.POSITIVE_INFINITY;
|
||||||
// Torque of the last substep, in ship axes, which the flight computer separates from its own surfaces' share
|
// Torque of the last substep, in ship axes, which the flight computer separates from its own surfaces' share
|
||||||
double[] torque = {wrench.totalTorque(0), wrench.totalTorque(1), wrench.totalTorque(2)};
|
double[] torque = {wrench.totalTorque(0), wrench.totalTorque(1), wrench.totalTorque(2)};
|
||||||
@@ -338,7 +383,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Cursor aim, if the pilot has it on, supplies the inputs the pilot is not giving with the keys
|
// Cursor aim, if the pilot has it on, supplies the inputs the pilot is not giving with the keys
|
||||||
NodeData effective = CursorPilot.apply(state, node, rotation, inertia, w, worldVelocity.length(), wingStalled);
|
NodeData effective = CursorPilot.apply(state, node, rotation, inertia, w, worldVelocity.length(), wingStalled,
|
||||||
|
liftAcceleration);
|
||||||
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
|
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
|
||||||
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
|
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
|
||||||
state.roll = moveTowards(state.roll, effective.roll(), step);
|
state.roll = moveTowards(state.roll, effective.roll(), step);
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
|
||||||
|
|
||||||
import net.minecraftforge.common.ForgeConfigSpec;
|
|
||||||
|
|
||||||
/** Server side settings of the cursor aim virtual pilot. Lives in improved_aerodynamics-cursor-aim.toml. */
|
|
||||||
public final class CursorAimConfig {
|
|
||||||
public static final ForgeConfigSpec SPEC;
|
|
||||||
|
|
||||||
public static final ForgeConfigSpec.BooleanValue ENABLED;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue FULL_DEFLECTION_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue DEADZONE_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MAX_TURN_RATE_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MAX_ROLL_RATE_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MAX_BANK_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue BANK_GAIN;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue RATE_GAIN;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue PUSH_FACTOR;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue STALLED_PITCH_FACTOR;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MIN_SPEED;
|
|
||||||
|
|
||||||
static {
|
|
||||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
|
||||||
builder.comment("Cursor aim: the mouse moves a cursor on screen, a virtual pilot flies the control node's craft by it").push("cursor_aim");
|
|
||||||
ENABLED = builder.comment("Whether pilots may use cursor aim on this server")
|
|
||||||
.define("enabled", true);
|
|
||||||
FULL_DEFLECTION_DEGREES = builder.comment("Angle of the cursor from the gun reticle at which the virtual pilot asks for full deflection: the maximum bank",
|
|
||||||
"sideways, the full pitch rate up or down")
|
|
||||||
.defineInRange("fullDeflectionDegrees", 30.0, 5.0, 90.0);
|
|
||||||
DEADZONE_DEGREES = builder.comment("Radius around the gun reticle in degrees inside which the cursor commands nothing, so the craft flies straight.",
|
|
||||||
"Outside it the command grows from zero, there is no jump at the edge")
|
|
||||||
.defineInRange("deadzoneDegrees", 3.0, 0.0, 15.0);
|
|
||||||
MAX_TURN_RATE_DEGREES = builder.comment("Turn rate in degrees per second the pilot asks for at full deflection, both for the turn at maximum bank and for pitching")
|
|
||||||
.defineInRange("maxTurnRateDegrees", 40.0, 5.0, 360.0);
|
|
||||||
MAX_ROLL_RATE_DEGREES = builder.comment("Fastest roll in degrees per second the pilot asks for when it rolls to a bank angle")
|
|
||||||
.defineInRange("maxRollRateDegrees", 150.0, 10.0, 720.0);
|
|
||||||
MAX_BANK_DEGREES = builder.comment("Bank angle the pilot rolls to when the cursor is at full sideways deflection")
|
|
||||||
.defineInRange("maxBankDegrees", 70.0, 10.0, 90.0);
|
|
||||||
BANK_GAIN = builder.comment("Roll rate in rad/s per radian of bank angle error, how eagerly the pilot rolls to the wanted bank and levels the wings")
|
|
||||||
.defineInRange("bankGain", 3.0, 0.2, 20.0);
|
|
||||||
RATE_GAIN = builder.comment("How hard the pilot corrects its turn rate, in angular acceleration per rad/s of rate error. Lower is smoother,",
|
|
||||||
"higher follows the cursor tighter and can start to oscillate")
|
|
||||||
.defineInRange("rateGain", 5.0, 0.5, 40.0);
|
|
||||||
PUSH_FACTOR = builder.comment("A cursor below the gun reticle pushes the nose down. This is how much of the pull rate that may be, since",
|
|
||||||
"negative g is harder on the craft and the pilot")
|
|
||||||
.defineInRange("pushFactor", 0.4, 0.0, 1.0);
|
|
||||||
STALLED_PITCH_FACTOR = builder.comment("Multiplier for the pitch rate the pilot asks for while the wings are stalled, so it does not keep pulling")
|
|
||||||
.defineInRange("stalledPitchFactor", 0.3, 0.0, 1.0);
|
|
||||||
MIN_SPEED = builder.comment("Below this speed in blocks per second the pilot does nothing, the surfaces have no effect anyway")
|
|
||||||
.defineInRange("minSpeed", 15.0, 0.0, 200.0);
|
|
||||||
builder.pop();
|
|
||||||
SPEC = builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private CursorAimConfig() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
package dev.lumareth.improvedAerodynamics.flight;
|
||||||
|
|
||||||
|
import dev.lumareth.improvedAerodynamics.Config;
|
||||||
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
|
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.entity.Entity;
|
import net.minecraft.world.entity.Entity;
|
||||||
@@ -24,7 +25,7 @@ public final class CursorAimServer {
|
|||||||
}
|
}
|
||||||
long nodeKey = basis.nodePos().asLong();
|
long nodeKey = basis.nodePos().asLong();
|
||||||
double length = Math.sqrt(cf * cf + cr * cr + cu * cu);
|
double length = Math.sqrt(cf * cf + cr * cr + cu * cu);
|
||||||
if (!active || !CursorAimConfig.ENABLED.get() || !(length > 1.0E-6) || !Double.isFinite(length)) {
|
if (!active || !Config.CURSOR_AIM_ENABLED.get() || !(length > 1.0E-6) || !Double.isFinite(length)) {
|
||||||
CursorAimRegistry.clear(nodeKey);
|
CursorAimRegistry.clear(nodeKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,45 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
package dev.lumareth.improvedAerodynamics.flight;
|
||||||
|
|
||||||
|
import dev.lumareth.improvedAerodynamics.Config;
|
||||||
import org.joml.Matrix3d;
|
import org.joml.Matrix3d;
|
||||||
import org.joml.Matrix3dc;
|
import org.joml.Matrix3dc;
|
||||||
import org.joml.Quaterniondc;
|
import org.joml.Quaterniondc;
|
||||||
import org.joml.Vector3d;
|
import org.joml.Vector3d;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The virtual pilot of cursor aim. The cursor is a stick that springs back to the gun reticle:
|
* The virtual pilot of cursor aim. The cursor is a direction fixed in the world, and the pilot turns the craft until
|
||||||
|
* the gun reticle sits on it, so the offset it gets shrinks to nothing as the turn completes:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>sideways it commands a bank angle and turn: the further out, the steeper the bank and the harder the pull
|
* <li>it banks towards the cursor, judged with the wings level so the bank does not hide where the cursor is: the
|
||||||
* that comes with it. Centered it levels the wings, so a centered cursor is straight flight;</li>
|
* further the cursor is to the side, the steeper the bank. While banked it pulls as much as a level turn at that
|
||||||
* <li>up and down it commands a pitch rate;</li>
|
* bank needs, so the craft turns without losing height;</li>
|
||||||
* <li>close to the reticle it steers with yaw and pitch directly instead of banking, so aiming does not rock the
|
* <li>it pitches towards the cursor as seen from the cockpit, so once banked the cursor is above the nose and the
|
||||||
* wings.</li>
|
* pull brings the nose round onto it;</li>
|
||||||
|
* <li>close to the reticle it levels the wings and puts the reticle on the cursor with rudder and elevator, so
|
||||||
|
* aiming does not rock the wings.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* The wanted rotation is turned into stick inputs by the measured effect of each input on the craft (torque per unit
|
* The wanted rotation is turned into stick inputs by the measured effect of each input on the craft (torque per unit
|
||||||
* of input, see {@link AeroState}), so it works for any craft however its surfaces are mixed or oriented: angular
|
* of input, see {@link AeroState}), so it works for any craft however its surfaces are mixed or oriented: angular
|
||||||
* acceleration wanted times inertia, solved for the three inputs with a little regularization for axes that have no
|
* acceleration wanted times inertia, solved for the three inputs with a little regularization for axes that have no
|
||||||
* surfaces. Keys the pilot holds win over it, per axis.
|
* surfaces. Keys the pilot holds win over it, per axis.
|
||||||
|
* <p>
|
||||||
|
* How hard the craft can turn is worked out live, not configured: the lift its wings can make at the edge of the stall
|
||||||
|
* at the current speed and altitude, over its mass and speed, and the steady pitch rate its pitch surfaces can hold
|
||||||
|
* against its pitch damping, whichever is lower. The same lift limit caps the bank to what the craft can hold level, so
|
||||||
|
* a heavy bomber banks gently and turns wide while a light fighter racks into a steep, tight turn.
|
||||||
*/
|
*/
|
||||||
final class CursorPilot {
|
final class CursorPilot {
|
||||||
private static final double FINE_START = Math.toRadians(4.0);
|
private static final double FINE_START = Math.toRadians(4.0);
|
||||||
private static final double FINE_END = Math.toRadians(16.0);
|
private static final double FINE_END = Math.toRadians(16.0);
|
||||||
private static final double MANUAL_OVERRIDE = 0.05;
|
private static final double MANUAL_OVERRIDE = 0.05;
|
||||||
private static final double MAX_PULL_BANK = Math.toRadians(90.0);
|
// Gravity of VS
|
||||||
|
private static final double GRAVITY = 10.0;
|
||||||
|
// Share of the stall limited lift the pilot plans with, so it does not ride the stall
|
||||||
|
private static final double LIFT_MARGIN = 0.85;
|
||||||
|
// Bank the pilot rolls to at full sideways deflection even when the wings cannot hold the turn level
|
||||||
|
private static final double MIN_BANK = Math.toRadians(20.0);
|
||||||
|
// Turn rate when neither limit can be measured, like the first moments of a flight
|
||||||
|
private static final double FALLBACK_TURN_RATE = Math.toRadians(30.0);
|
||||||
|
|
||||||
private CursorPilot() {
|
private CursorPilot() {
|
||||||
}
|
}
|
||||||
@@ -32,57 +48,83 @@ final class CursorPilot {
|
|||||||
* @return the node with the pilot's wanted inputs in place of the keys, or the node itself if cursor aim is off
|
* @return the node with the pilot's wanted inputs in place of the keys, or the node itself if cursor aim is off
|
||||||
*/
|
*/
|
||||||
static NodeData apply(AeroState.NodeState state, NodeData node, Quaterniondc rotation, Matrix3dc inertia, double[] w,
|
static NodeData apply(AeroState.NodeState state, NodeData node, Quaterniondc rotation, Matrix3dc inertia, double[] w,
|
||||||
double speed, boolean wingStalled) {
|
double speed, boolean wingStalled, double liftAcceleration) {
|
||||||
if (!CursorAimConfig.ENABLED.get()) {
|
if (!Config.CURSOR_AIM_ENABLED.get()) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
CursorAimRegistry.Aim aim = CursorAimRegistry.get(node.key());
|
CursorAimRegistry.Aim aim = CursorAimRegistry.get(node.key());
|
||||||
if (aim == null || speed < CursorAimConfig.MIN_SPEED.get()) {
|
if (aim == null || speed < Config.CURSOR_AIM_MIN_SPEED.get()) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
double fullAngle = Math.toRadians(CursorAimConfig.FULL_DEFLECTION_DEGREES.get());
|
double fullAngle = Math.toRadians(Config.CURSOR_AIM_FULL_DEFLECTION_DEGREES.get());
|
||||||
double maxTurn = Math.toRadians(CursorAimConfig.MAX_TURN_RATE_DEGREES.get());
|
double maxRoll = Math.toRadians(Config.CURSOR_AIM_MAX_ROLL_RATE_DEGREES.get());
|
||||||
double maxRoll = Math.toRadians(CursorAimConfig.MAX_ROLL_RATE_DEGREES.get());
|
double maxBank = Math.toRadians(Config.CURSOR_AIM_MAX_BANK_DEGREES.get());
|
||||||
double maxBank = Math.toRadians(CursorAimConfig.MAX_BANK_DEGREES.get());
|
|
||||||
|
// What this craft can do right now. Lift: pitch rate at the highest usable lift (a = L / m, rate = a / v), and
|
||||||
|
// the bank whose level turn needs that load factor. Without a stall model only the control limit applies
|
||||||
|
double usableLift = LIFT_MARGIN * liftAcceleration;
|
||||||
|
double liftRate = usableLift > 0.0 ? usableLift / speed : Double.POSITIVE_INFINITY;
|
||||||
|
double controlRate = state.controlRate > 0.0 ? state.controlRate : Double.POSITIVE_INFINITY;
|
||||||
|
double maxTurn = Math.min(liftRate, controlRate);
|
||||||
|
if (!Double.isFinite(maxTurn)) {
|
||||||
|
maxTurn = FALLBACK_TURN_RATE;
|
||||||
|
}
|
||||||
|
if (Double.isFinite(liftAcceleration)) {
|
||||||
|
double loadFactor = usableLift / GRAVITY;
|
||||||
|
double levelBank = loadFactor > 1.0 ? Math.acos(1.0 / loadFactor) : 0.0;
|
||||||
|
maxBank = Math.max(Math.min(MIN_BANK, maxBank), Math.min(maxBank, levelBank));
|
||||||
|
}
|
||||||
|
|
||||||
double[] f = aim.forward();
|
double[] f = aim.forward();
|
||||||
double[] r = aim.right();
|
double[] r = aim.right();
|
||||||
double[] u = aim.up();
|
double[] u = aim.up();
|
||||||
|
|
||||||
// Cursor as angles from the reticle: sideways (positive right) and up, both through any 270 degree cone
|
|
||||||
double sideways = Math.atan2(aim.cr(), aim.cf());
|
|
||||||
double vertical = Math.atan2(aim.cu(), Math.hypot(aim.cf(), aim.cr()));
|
|
||||||
// Deadzone around the reticle: shrink the cursor offset by its radius, direction kept
|
|
||||||
double offset = Math.hypot(sideways, vertical);
|
|
||||||
double deadzone = Math.toRadians(CursorAimConfig.DEADZONE_DEGREES.get());
|
|
||||||
if (offset > 1.0E-9) {
|
|
||||||
double scale = Math.max(0.0, offset - deadzone) / offset;
|
|
||||||
sideways *= scale;
|
|
||||||
vertical *= scale;
|
|
||||||
}
|
|
||||||
double lateral = saturate(sideways / fullAngle);
|
|
||||||
double blend = smoothstep(Math.abs(sideways), FINE_START, FINE_END);
|
|
||||||
|
|
||||||
// Bank of the craft: positive when the right wing is down, through upside down
|
// Bank of the craft: positive when the right wing is down, through upside down
|
||||||
Vector3d rightWorld = rotation.transform(new Vector3d(r[0], r[1], r[2]));
|
Vector3d rightWorld = rotation.transform(new Vector3d(r[0], r[1], r[2]));
|
||||||
Vector3d upWorld = rotation.transform(new Vector3d(u[0], u[1], u[2]));
|
Vector3d upWorld = rotation.transform(new Vector3d(u[0], u[1], u[2]));
|
||||||
double bank = Math.atan2(-rightWorld.y, upWorld.y);
|
double bank = Math.atan2(-rightWorld.y, upWorld.y);
|
||||||
|
|
||||||
double bankCommand = maxBank * lateral * blend;
|
// Deadzone around the reticle: shrink the cursor's offset by its radius, direction kept
|
||||||
double rollRate = clamp(CursorAimConfig.BANK_GAIN.get() * (bankCommand - bank), -maxRoll, maxRoll);
|
double cf = aim.cf();
|
||||||
|
double cr = aim.cr();
|
||||||
// Pitch: the stick, plus the pull that carries the turn while banked
|
double cu = aim.cu();
|
||||||
double pitchStick = saturate(vertical / fullAngle);
|
double deadzone = Math.toRadians(Config.CURSOR_AIM_DEADZONE_DEGREES.get());
|
||||||
double pitchRate = maxTurn * (pitchStick >= 0.0 ? pitchStick : pitchStick * CursorAimConfig.PUSH_FACTOR.get());
|
double offset = Math.acos(clamp(cf, -1.0, 1.0));
|
||||||
if (Math.abs(bank) < MAX_PULL_BANK) {
|
double across = Math.hypot(cr, cu);
|
||||||
pitchRate += maxTurn * Math.abs(lateral) * blend * Math.sin(Math.abs(bank));
|
if (deadzone > 0.0 && offset > 1.0E-9 && across > 1.0E-9) {
|
||||||
|
double shrunk = Math.max(0.0, offset - deadzone);
|
||||||
|
cf = Math.cos(shrunk);
|
||||||
|
cr *= Math.sin(shrunk) / across;
|
||||||
|
cu *= Math.sin(shrunk) / across;
|
||||||
}
|
}
|
||||||
|
// Cursor as angles from the reticle as seen from the cockpit: sideways (positive right) and up, through any
|
||||||
|
// 270 degree cone
|
||||||
|
double sideways = Math.atan2(cr, cf);
|
||||||
|
double vertical = Math.atan2(cu, Math.hypot(cf, cr));
|
||||||
|
// Sideways as it would be with the wings level, what the bank is chosen by
|
||||||
|
double levelSideways = Math.atan2(cr * Math.cos(bank) + cu * Math.sin(bank), cf);
|
||||||
|
double blend = smoothstep(Math.abs(levelSideways), FINE_START, FINE_END);
|
||||||
|
|
||||||
|
double bankCommand = maxBank * saturate(levelSideways / fullAngle) * blend;
|
||||||
|
double rollRate = clamp(Config.CURSOR_AIM_BANK_GAIN.get() * (bankCommand - bank), -maxRoll, maxRoll);
|
||||||
|
|
||||||
|
// Pitch: the pull of a level turn at the current bank, the turn rate g tan(bank) / v seen about the pitch axis,
|
||||||
|
// plus a pull towards the cursor. Past 90 degrees of bank the level turn pull would only dive, so it is left out
|
||||||
|
double pitchStick = saturate(vertical / fullAngle);
|
||||||
|
double push = Config.CURSOR_AIM_PUSH_FACTOR.get();
|
||||||
|
double pitchRate = maxTurn * (pitchStick >= 0.0 ? pitchStick : pitchStick * push);
|
||||||
|
double cosBank = Math.cos(bank);
|
||||||
|
if (cosBank > 0.0) {
|
||||||
|
double sinBank = Math.sin(bank);
|
||||||
|
pitchRate += GRAVITY * sinBank * sinBank / (speed * Math.max(cosBank, 0.1));
|
||||||
|
}
|
||||||
|
pitchRate = clamp(pitchRate, -maxTurn * push, maxTurn);
|
||||||
if (wingStalled && pitchRate > 0.0) {
|
if (wingStalled && pitchRate > 0.0) {
|
||||||
pitchRate *= CursorAimConfig.STALLED_PITCH_FACTOR.get();
|
pitchRate *= Config.CURSOR_AIM_STALLED_PITCH_FACTOR.get();
|
||||||
}
|
}
|
||||||
// Close to the reticle the nose follows the cursor with the rudder
|
// Close to the reticle the nose follows the cursor with the rudder
|
||||||
double yawRate = maxTurn * lateral * (1.0 - blend);
|
double yawRate = maxTurn * saturate(sideways / fullAngle) * (1.0 - blend);
|
||||||
|
|
||||||
// Wanted angular velocity in ship axes: roll about forward, pitch about right, yaw (nose right) about -up
|
// Wanted angular velocity in ship axes: roll about forward, pitch about right, yaw (nose right) about -up
|
||||||
Vector3d wanted = new Vector3d(
|
Vector3d wanted = new Vector3d(
|
||||||
@@ -90,7 +132,7 @@ final class CursorPilot {
|
|||||||
rollRate * f[1] + pitchRate * r[1] - yawRate * u[1],
|
rollRate * f[1] + pitchRate * r[1] - yawRate * u[1],
|
||||||
rollRate * f[2] + pitchRate * r[2] - yawRate * u[2]);
|
rollRate * f[2] + pitchRate * r[2] - yawRate * u[2]);
|
||||||
Vector3d error = wanted.sub(w[0], w[1], w[2]);
|
Vector3d error = wanted.sub(w[0], w[1], w[2]);
|
||||||
Vector3d acceleration = error.mul(CursorAimConfig.RATE_GAIN.get());
|
Vector3d acceleration = error.mul(Config.CURSOR_AIM_RATE_GAIN.get());
|
||||||
Vector3d torque = inertia.transform(acceleration, new Vector3d());
|
Vector3d torque = inertia.transform(acceleration, new Vector3d());
|
||||||
|
|
||||||
double[] inputs = solve(state.pitchResponse, state.yawResponse, state.rollResponse, torque);
|
double[] inputs = solve(state.pitchResponse, state.yawResponse, state.rollResponse, torque);
|
||||||
|
|||||||
@@ -129,11 +129,8 @@ final class ImprovedAero {
|
|||||||
|
|
||||||
// Low aspect ratio wings keep their flow attached longer thanks to leading-edge vortices. Control surfaces
|
// Low aspect ratio wings keep their flow attached longer thanks to leading-edge vortices. Control surfaces
|
||||||
// in a wing's group stall with the wing instead of before it
|
// in a wing's group stall with the wing instead of before it
|
||||||
double vortexFactor = tuning.vortex() && (s.type() == SurfaceType.WING || s.wingGroup() && s.type().controllable)
|
double vortexFactor = vortexFactor(s, tuning);
|
||||||
? VortexLift.factor(s.aspectRatio()) : 0.0;
|
double critical = criticalAngle(s, tuning, vortexFactor);
|
||||||
double baseStall = s.slab() ? SurfaceType.SLAB_STALL_ANGLE
|
|
||||||
: s.wingGroup() && s.type().controllable ? SurfaceType.WING.stallAngle : s.type().stallAngle;
|
|
||||||
double critical = Math.toRadians(baseStall) * (1.0 + tuning.vortexStallBonus() * vortexFactor) * tuning.stallAngleScale();
|
|
||||||
double absAlpha = Math.abs(alpha);
|
double absAlpha = Math.abs(alpha);
|
||||||
if (alpha >= 0.0) {
|
if (alpha >= 0.0) {
|
||||||
critical += FLAP_STALL_GAIN * flapShift;
|
critical += FLAP_STALL_GAIN * flapShift;
|
||||||
@@ -149,9 +146,8 @@ final class ImprovedAero {
|
|||||||
|
|
||||||
// Finite wing theory: lift slope and induced drag follow from the wing's aspect ratio
|
// Finite wing theory: lift slope and induced drag follow from the wing's aspect ratio
|
||||||
double aspectRatio = s.aspectRatio();
|
double aspectRatio = s.aspectRatio();
|
||||||
double slope = aspectRatio > 0.0 ? 2.0 * Math.PI / (1.0 + 2.0 / aspectRatio) : LIFT_SLOPE;
|
|
||||||
double inducedDrag = aspectRatio > 0.0 ? 1.0 / (Math.PI * EFFICIENCY * aspectRatio) : INDUCED_DRAG;
|
double inducedDrag = aspectRatio > 0.0 ? 1.0 / (Math.PI * EFFICIENCY * aspectRatio) : INDUCED_DRAG;
|
||||||
double attachedLift = slope * alpha;
|
double attachedLift = liftSlope(s) * alpha;
|
||||||
if (s.type() == SurfaceType.WING) {
|
if (s.type() == SurfaceType.WING) {
|
||||||
attachedLift += VortexLift.liftCoefficient(alpha, vortexFactor, tuning.vortexLiftStrength());
|
attachedLift += VortexLift.liftCoefficient(alpha, vortexFactor, tuning.vortexLiftStrength());
|
||||||
}
|
}
|
||||||
@@ -175,6 +171,44 @@ final class ImprovedAero {
|
|||||||
return stalled;
|
return stalled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lift (along the surface's normal) the surface would make in the airflow it sees now at the highest angle of
|
||||||
|
* attack it can fly without starting to stall, flaps included. Only meaningful for horizontal surfaces.
|
||||||
|
*/
|
||||||
|
static double maxLift(AeroSurface s, double[] v, double[] w, double[] r, double flap, double weight, Tuning tuning) {
|
||||||
|
double vx = v[0] + w[1] * r[2] - w[2] * r[1];
|
||||||
|
double vy = v[1] + w[2] * r[0] - w[0] * r[2];
|
||||||
|
double vz = v[2] + w[0] * r[1] - w[1] * r[0];
|
||||||
|
double u = vx * s.facingX() + vz * s.facingZ();
|
||||||
|
double planeSpeedSq = u * u + vy * vy;
|
||||||
|
double vortexFactor = vortexFactor(s, tuning);
|
||||||
|
double flapShift = FLAP_ALPHA_FACTOR * flap;
|
||||||
|
double alpha = criticalAngle(s, tuning, vortexFactor) + FLAP_STALL_GAIN * flapShift - STALL_BLEND;
|
||||||
|
double lift = liftSlope(s) * alpha;
|
||||||
|
if (s.type() == SurfaceType.WING) {
|
||||||
|
lift += VortexLift.liftCoefficient(alpha, vortexFactor, tuning.vortexLiftStrength());
|
||||||
|
}
|
||||||
|
return LIFT_CONSTANT * s.pressure() * weight * s.fluidFactor() * planeSpeedSq * lift * tuning.liftScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double vortexFactor(AeroSurface s, Tuning tuning) {
|
||||||
|
return tuning.vortex() && (s.type() == SurfaceType.WING || s.wingGroup() && s.type().controllable)
|
||||||
|
? VortexLift.factor(s.aspectRatio()) : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Angle of attack (radians, camber and flaps included) at which the surface starts to stall, without flaps. */
|
||||||
|
private static double criticalAngle(AeroSurface s, Tuning tuning, double vortexFactor) {
|
||||||
|
double baseStall = s.slab() ? SurfaceType.SLAB_STALL_ANGLE
|
||||||
|
: s.wingGroup() && s.type().controllable ? SurfaceType.WING.stallAngle : s.type().stallAngle;
|
||||||
|
return Math.toRadians(baseStall) * (1.0 + tuning.vortexStallBonus() * vortexFactor) * tuning.stallAngleScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lift slope per radian from finite wing theory, from the wing's aspect ratio. */
|
||||||
|
private static double liftSlope(AeroSurface s) {
|
||||||
|
double aspectRatio = s.aspectRatio();
|
||||||
|
return aspectRatio > 0.0 ? 2.0 * Math.PI / (1.0 + 2.0 / aspectRatio) : LIFT_SLOPE;
|
||||||
|
}
|
||||||
|
|
||||||
static double smoothstep(double edge0, double edge1, double x) {
|
static double smoothstep(double edge0, double edge1, double x) {
|
||||||
double t = Math.max(0.0, Math.min(1.0, (x - edge0) / (edge1 - edge0)));
|
double t = Math.max(0.0, Math.min(1.0, (x - edge0) / (edge1 - edge0)));
|
||||||
return t * t * (3.0 - 2.0 * t);
|
return t * t * (3.0 - 2.0 * t);
|
||||||
|
|||||||
Reference in new issue
Block a user