feat(flight): add aspect ratio lift and altitude-hold trim
- Derive lift slope and induced drag from each wing's aspect ratio, found by grouping connected wing blocks across short span gaps. - Give stabilizers and vertical wings their own parasitic drag from the stabilizerDrag config. - Replace designSpeed with a fixed lift constant and a liftScale multiplier. - Make auto trim hold the altitude the craft has when the pilot releases pitch, using nested altitude, climb and attitude loops. - Add /improvedaerodynamics (alias /aero) to list, read and set the main config values in game.
This commit is contained in:
1 parent
4b7fb0e1e8
commit
599ecae21f
10 files changed
+329
-48
No files matched your search
@@ -72,20 +72,26 @@ public final class Config {
|
||||
|
||||
public static final ForgeConfigSpec.BooleanValue IMPROVED_FLIGHT_MODEL = BUILDER
|
||||
.comment("Replace Warium's lift and damping with a model based on each surface's angle of attack:",
|
||||
"lift grows with the square of airspeed, surfaces stall one by one when their angle of attack gets",
|
||||
"lift grows with the square of airspeed, the air pressure at the altitude and the wing's shape (long narrow",
|
||||
"wings lift better), surfaces stall one by one when their angle of attack gets",
|
||||
"too high, and stalled surfaces stop damping rotation, which allows post-stall maneuvers with",
|
||||
"thrust vectoring. Changes how craft fly")
|
||||
.define("improvedFlightModel", false);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue DESIGN_SPEED = BUILDER
|
||||
.comment("Improved flight model: airspeed (blocks per second) at which a level wing makes the same lift as in Warium.",
|
||||
"Slower flight needs a higher angle of attack, faster flight a lower one")
|
||||
.defineInRange("designSpeed", 40.0D, 5.0D, 300.0D);
|
||||
public static final ForgeConfigSpec.DoubleValue LIFT_SCALE = BUILDER
|
||||
.comment("Improved flight model: multiplier for the lift of all surfaces (drag is unchanged). Raise it if a craft",
|
||||
"cannot stay up at the speed it should fly at, lower it for craft that climb too easily")
|
||||
.defineInRange("liftScale", 1.0D, 0.1D, 10.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue STALL_ANGLE_SCALE = BUILDER
|
||||
.comment("Improved flight model: multiplier for the angle of attack at which surfaces stall")
|
||||
.defineInRange("stallAngleScale", 1.0D, 0.5D, 2.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue STABILIZER_DRAG = BUILDER
|
||||
.comment("Improved flight model: parasitic drag coefficient of stabilizers and vertical wings. They stabilize",
|
||||
"with lift, so this only costs speed. Wings use Warium's 1.0, which also stands in for fuselage drag")
|
||||
.defineInRange("stabilizerDrag", 0.2D, 0.0D, 2.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue TRIM_AUTHORITY = BUILDER
|
||||
.comment("Auto trim: largest pitch input (fraction of full deflection) the trim may hold")
|
||||
.defineInRange("trimAuthority", 0.5D, 0.0D, 1.0D);
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package dev.lumareth.improvedAerodynamics;
|
||||
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /improvedaerodynamics (or /aero) to read and change the most important server config values in game.
|
||||
* {@code /aero} lists them, {@code /aero <option>} shows one, {@code /aero <option> <value>} sets and saves it.
|
||||
* All of them are read live, so changes apply immediately.
|
||||
*/
|
||||
public final class ConfigCommands {
|
||||
private record Option<T>(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type) {
|
||||
}
|
||||
|
||||
private static final List<Option<?>> OPTIONS = List.of(
|
||||
new Option<>("realistic_flight_model", Config.IMPROVED_FLIGHT_MODEL, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("smooth_force_delivery", Config.SMOOTH_FORCE_DELIVERY, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("physics_rate_aerodynamics", Config.PHYSICS_RATE_AERODYNAMICS, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("control_smoothing", Config.CONTROL_SMOOTHING, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("implicit_damping", Config.IMPLICIT_DAMPING, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("lift_scale", Config.LIFT_SCALE, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("stall_angle_scale", Config.STALL_ANGLE_SCALE, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("stabilizer_drag", Config.STABILIZER_DRAG, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("trim_authority", Config.TRIM_AUTHORITY, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("trim_min_speed", Config.TRIM_MIN_SPEED, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("air_placeholder", Config.ENABLE_AIR_PLACEHOLDER, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("lookahead_seconds", Config.LOOKAHEAD_SECONDS, DoubleArgumentType.doubleArg(), Double.class));
|
||||
|
||||
private ConfigCommands() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onRegisterCommands(RegisterCommandsEvent event) {
|
||||
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("improvedaerodynamics")
|
||||
.requires(source -> source.hasPermission(2))
|
||||
.executes(context -> list(context.getSource()));
|
||||
for (Option<?> option : OPTIONS) {
|
||||
root.then(optionNode(option));
|
||||
}
|
||||
LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root);
|
||||
event.getDispatcher().register(Commands.literal("aero")
|
||||
.requires(source -> source.hasPermission(2))
|
||||
.executes(context -> list(context.getSource()))
|
||||
.redirect(node));
|
||||
}
|
||||
|
||||
private static <T> LiteralArgumentBuilder<CommandSourceStack> optionNode(Option<T> option) {
|
||||
return Commands.literal(option.name())
|
||||
.executes(context -> {
|
||||
context.getSource().sendSuccess(() -> Component.literal(option.name() + " = " + option.value().get()), false);
|
||||
return 1;
|
||||
})
|
||||
.then(Commands.argument("value", option.argument())
|
||||
.executes(context -> set(context.getSource(), option, context.getArgument("value", option.type()))));
|
||||
}
|
||||
|
||||
private static <T> int set(CommandSourceStack source, Option<T> option, T newValue) {
|
||||
ForgeConfigSpec.ValueSpec spec = Config.SPEC.getSpec().get(option.value().getPath());
|
||||
if (!spec.test(newValue)) {
|
||||
ForgeConfigSpec.Range<?> range = spec.getRange();
|
||||
source.sendFailure(Component.literal(range != null
|
||||
? option.name() + " must be between " + range.getMin() + " and " + range.getMax()
|
||||
: "Invalid value for " + option.name()));
|
||||
return 0;
|
||||
}
|
||||
option.value().set(newValue);
|
||||
option.value().save();
|
||||
source.sendSuccess(() -> Component.literal(option.name() + " set to " + newValue), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int list(CommandSourceStack source) {
|
||||
StringBuilder text = new StringBuilder("Improved Aerodynamics settings:");
|
||||
for (Option<?> option : OPTIONS) {
|
||||
text.append("\n ").append(option.name()).append(" = ").append(option.value().get());
|
||||
}
|
||||
source.sendSuccess(() -> Component.literal(text.toString()), false);
|
||||
return OPTIONS.size();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public class ImprovedAerodynamics {
|
||||
public ImprovedAerodynamics() {
|
||||
MinecraftForge.EVENT_BUS.register(LookaheadLoader.class);
|
||||
MinecraftForge.EVENT_BUS.register(PhysicsBridge.class);
|
||||
MinecraftForge.EVENT_BUS.register(ConfigCommands.class);
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
|
||||
Network.register();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
|
||||
final Vector3d pitchResponse = new Vector3d();
|
||||
final Vector3d pitchResponseBuilding = new Vector3d();
|
||||
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
|
||||
volatile double savedTrim;
|
||||
|
||||
NodeState(NodeData data) {
|
||||
@@ -102,7 +103,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
|
||||
@Override
|
||||
public void publish(long tick) {
|
||||
published = new Frame(tick, buildingSurfaces.values().toArray(new AeroSurface[0]), buildingNodes);
|
||||
published = new Frame(tick, WingShape.withAspectRatios(buildingSurfaces.values()), buildingNodes);
|
||||
buildingSurfaces = new Long2ObjectOpenHashMap<>();
|
||||
buildingNodes = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
@@ -131,7 +132,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
boolean improved = Config.IMPROVED_FLIGHT_MODEL.get();
|
||||
Matrix3dc inertia = ship.getMomentOfInertia();
|
||||
|
||||
updateNodes(frame, worldVelocity, rotation, inertia, w, dt, improved);
|
||||
updateNodes(frame, worldVelocity, transform.getPositionInWorld().y(), rotation, inertia, w, dt, improved);
|
||||
evaluate(frame, centerOfMass, v, w, improved, true, wrench);
|
||||
|
||||
if (Config.IMPLICIT_DAMPING.get()) {
|
||||
@@ -204,8 +205,10 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
private void evaluate(Frame frame, Vector3dc centerOfMass, double[] v, double[] w, boolean improved, boolean commit, Wrench out) {
|
||||
out.clear();
|
||||
double weight = clock.weight();
|
||||
double liftConstant = ImprovedAero.liftConstant(Config.DESIGN_SPEED.get());
|
||||
double liftConstant = ImprovedAero.LIFT_CONSTANT;
|
||||
double stallAngleScale = Config.STALL_ANGLE_SCALE.get();
|
||||
double liftScale = Config.LIFT_SCALE.get();
|
||||
double thinDrag = Config.STABILIZER_DRAG.get();
|
||||
double[] r = new double[3];
|
||||
if (commit) {
|
||||
wingStalled = false;
|
||||
@@ -224,7 +227,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
double input = s.type().controllable ? input(s, node, state, 0.0) : 0.0;
|
||||
|
||||
if (improved) {
|
||||
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, stallAngleScale, stalled[i], out);
|
||||
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, liftScale, stallAngleScale, thinDrag, stalled[i], out);
|
||||
if (commit) {
|
||||
stalled[i] = nowStalled;
|
||||
wingStalled |= nowStalled && s.type() == SurfaceType.WING;
|
||||
@@ -239,7 +242,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
probe.clear();
|
||||
double base = 0.0;
|
||||
if (improved) {
|
||||
ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, stallAngleScale, stalled[i], probe);
|
||||
ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, liftScale, stallAngleScale, thinDrag, stalled[i], probe);
|
||||
} else {
|
||||
WariumAero.evaluate(s, v, w, r, input, weight, probe);
|
||||
}
|
||||
@@ -247,7 +250,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
probe.clear();
|
||||
double nudged = input(s, node, state, delta);
|
||||
if (improved) {
|
||||
ImprovedAero.evaluate(s, v, w, r, nudged, weight, liftConstant, stallAngleScale, stalled[i], probe);
|
||||
ImprovedAero.evaluate(s, v, w, r, nudged, weight, liftConstant, liftScale, stallAngleScale, thinDrag, stalled[i], probe);
|
||||
} else {
|
||||
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
|
||||
}
|
||||
@@ -285,7 +288,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
};
|
||||
}
|
||||
|
||||
private void updateNodes(Frame frame, Vector3dc worldVelocity, Quaterniondc rotation, Matrix3dc inertia,
|
||||
private void updateNodes(Frame frame, Vector3dc worldVelocity, double altitude, Quaterniondc rotation, Matrix3dc inertia,
|
||||
double[] w, double dt, boolean improved) {
|
||||
double step = Config.CONTROL_SMOOTHING.get() ? INPUT_RATE_PER_TICK * clock.weight() : Double.POSITIVE_INFINITY;
|
||||
for (NodeData node : frame.nodes.values()) {
|
||||
@@ -296,7 +299,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
state.pitch = moveTowards(state.pitch, node.pitch(), step);
|
||||
state.yaw = moveTowards(state.yaw, node.yaw(), step);
|
||||
state.roll = moveTowards(state.roll, node.roll(), step);
|
||||
AutoTrim.update(state, node, worldVelocity, rotation, inertia, w, dt, improved && wingStalled);
|
||||
AutoTrim.update(state, node, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,19 @@ package dev.lumareth.improvedAerodynamics.flight;
|
||||
* @param speedMultiplier Warium's aerodynamicSpeedMultiplier game rule / 100, at least 1
|
||||
* @param node BlockPos.asLong of the Vehicle Control Node the surface listens to, {@link #NO_NODE} if none
|
||||
* @param brakeInput air brakes only: Warium's input value, they ignore smoothing and trim
|
||||
* @param aspectRatio span squared over area of the wing the block belongs to, 0 if not known yet
|
||||
*/
|
||||
public record AeroSurface(long key, SurfaceType type, boolean slab, double x, double y, double z, int facingX, int facingZ,
|
||||
double pressure, double fluidFactor, double speedMultiplier,
|
||||
long node, ControlAxis axis1, double multiplier1, ControlAxis axis2, double multiplier2,
|
||||
double brakeInput) {
|
||||
double brakeInput, double aspectRatio) {
|
||||
public static final long NO_NODE = Long.MIN_VALUE;
|
||||
|
||||
AeroSurface withAspectRatio(double aspectRatio) {
|
||||
return new AeroSurface(key, type, slab, x, y, z, facingX, facingZ, pressure, fluidFactor, speedMultiplier,
|
||||
node, axis1, multiplier1, axis2, multiplier2, brakeInput, aspectRatio);
|
||||
}
|
||||
|
||||
/** Warium's "Direction.getClockWise(Axis.Y)" of the facing, the span direction of a horizontal surface. */
|
||||
int rightX() {
|
||||
return -facingZ;
|
||||
|
||||
@@ -7,19 +7,31 @@ import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* Auto trim for the pitch control surfaces of one control node: while the pilot gives no pitch input it adds a
|
||||
* pitch offset that brings the flight path back to level and then holds it there, cancelling whatever pitching
|
||||
* moment the craft has (off-center weight, thrust line, speed changes).
|
||||
* Auto trim for the pitch control surfaces of one control node: while the pilot gives no pitch input it holds the
|
||||
* altitude the craft had when the pilot let go, cancelling whatever pitching moment the craft has (off-center weight,
|
||||
* thrust line, speed changes).
|
||||
* <p>
|
||||
* Two loops: the flight path angle sets a gentle target pitch rate, and a PI controller moves the trim to reach it.
|
||||
* The trim's effect is measured every substep (torque per unit of pitch input), which gives the pitch axis and lets
|
||||
* the gains be expressed as angular acceleration, so the same tuning works for any craft, speed and altitude.
|
||||
* Three nested loops: altitude error sets a gentle climb rate, climb rate error sets a nose attitude (never more than
|
||||
* {@link #MAX_ATTITUDE} from level), attitude error sets a pitch rate, and a PI controller moves the trim to reach
|
||||
* that rate. Bounding the attitude matters: in Warium's physics the flight path follows the nose only slowly, so
|
||||
* steering the flight path directly with pitch rate keeps rotating the nose until the craft dives.
|
||||
* <p>
|
||||
* Attitude is measured from the ship's own up axis, so it assumes the craft is built level, as Warium craft are. The
|
||||
* trim's effect is measured every substep (torque per unit of pitch input), which gives the pitch axis and lets the
|
||||
* inner gains be expressed as angular acceleration, so the same tuning works for any craft, speed and altitude.
|
||||
* The trim holds still while the pilot pitches, when too slow, and when wings stall, so it never fights maneuvers.
|
||||
*/
|
||||
final class AutoTrim {
|
||||
// Target pitch rate per radian of flight path angle, and its limit (rad/s)
|
||||
private static final double PATH_GAIN = 0.8;
|
||||
private static final double MAX_PITCH_RATE = 0.1;
|
||||
// Climb rate (blocks/s) per block of altitude error, and its limit
|
||||
private static final double ALTITUDE_GAIN = 0.3;
|
||||
private static final double MAX_CLIMB_RATE = 4.0;
|
||||
// Nose attitude (rad) per block/s of climb rate error, its integral gain, and its limit
|
||||
private static final double CLIMB_GAIN = 0.02;
|
||||
private static final double CLIMB_INTEGRAL_GAIN = 0.01;
|
||||
private static final double MAX_ATTITUDE = Math.toRadians(15.0);
|
||||
// Pitch rate (rad/s) per radian of attitude error, and its limit
|
||||
private static final double ATTITUDE_GAIN = 1.0;
|
||||
private static final double MAX_PITCH_RATE = 0.15;
|
||||
// Angular acceleration per rad/s of pitch rate error, and integral gain
|
||||
private static final double PROPORTIONAL_GAIN = 3.0;
|
||||
private static final double INTEGRAL_GAIN = 1.5;
|
||||
@@ -30,12 +42,21 @@ final class AutoTrim {
|
||||
private AutoTrim() {
|
||||
}
|
||||
|
||||
static void update(AeroState.NodeState state, NodeData node, Vector3dc worldVelocity, Quaterniondc rotation,
|
||||
Matrix3dc inertia, double[] w, double dt, boolean stalled) {
|
||||
/** Altitude to hold and attitude integral, kept per node next to the trim. */
|
||||
static final class Targets {
|
||||
double altitude = Double.NaN;
|
||||
double attitudeIntegral;
|
||||
}
|
||||
|
||||
static void update(AeroState.NodeState state, NodeData node, Vector3dc worldVelocity, double altitude,
|
||||
Quaterniondc rotation, Matrix3dc inertia, double[] w, double dt, boolean stalled) {
|
||||
Targets targets = state.trimTargets;
|
||||
if (!node.autoTrim()) {
|
||||
state.trimIntegral = 0.0;
|
||||
state.trimOutput = AeroState.moveTowards(state.trimOutput, 0.0, TRIM_RATE * dt);
|
||||
state.savedTrim = 0.0;
|
||||
targets.altitude = Double.NaN;
|
||||
targets.attitudeIntegral = 0.0;
|
||||
return;
|
||||
}
|
||||
double authority = Config.TRIM_AUTHORITY.get();
|
||||
@@ -43,6 +64,11 @@ final class AutoTrim {
|
||||
double speed = worldVelocity.length();
|
||||
boolean hold = Math.abs(node.pitch()) > PILOT_DEADBAND || speed < Config.TRIM_MIN_SPEED.get() || stalled
|
||||
|| response < 1.0E-6;
|
||||
if (hold || Double.isNaN(targets.altitude)) {
|
||||
// Hold whatever altitude the craft is at once the pilot lets go
|
||||
targets.altitude = altitude;
|
||||
targets.attitudeIntegral = 0.0;
|
||||
}
|
||||
|
||||
double target = state.trimIntegral;
|
||||
if (!hold) {
|
||||
@@ -51,14 +77,23 @@ final class AutoTrim {
|
||||
double axisInertia = axis.dot(inertia.transform(axis, new Vector3d()));
|
||||
double pitchRate = axis.x * w[0] + axis.y * w[1] + axis.z * w[2];
|
||||
|
||||
// Does rotating about that axis raise the flight path? Sign of the vertical part of axis x direction
|
||||
// Does rotating about that axis raise the nose? Sign of the vertical part of axis x flight direction
|
||||
Vector3d worldAxis = rotation.transform(axis, new Vector3d());
|
||||
Vector3d direction = worldVelocity.normalize(new Vector3d());
|
||||
double noseUp = Math.signum(worldAxis.cross(direction, new Vector3d()).y);
|
||||
double pathAngle = Math.asin(Math.max(-1.0, Math.min(1.0, direction.y)));
|
||||
double attitude = attitude(axis, rotation);
|
||||
if (noseUp == 0.0 || Double.isNaN(attitude)) {
|
||||
return;
|
||||
}
|
||||
|
||||
double targetRate = Math.max(-MAX_PITCH_RATE, Math.min(MAX_PITCH_RATE, -PATH_GAIN * pathAngle * noseUp));
|
||||
double error = targetRate - pitchRate;
|
||||
double climbTarget = clamp(ALTITUDE_GAIN * (targets.altitude - altitude), MAX_CLIMB_RATE);
|
||||
double climbError = climbTarget - worldVelocity.y();
|
||||
targets.attitudeIntegral = clamp(targets.attitudeIntegral + CLIMB_INTEGRAL_GAIN * climbError * dt, MAX_ATTITUDE);
|
||||
// Nose elevation wanted, turned into rotation about the axis
|
||||
double attitudeTarget = noseUp * clamp(targets.attitudeIntegral + CLIMB_GAIN * climbError, MAX_ATTITUDE);
|
||||
double rateTarget = clamp(ATTITUDE_GAIN * (attitudeTarget - attitude), MAX_PITCH_RATE);
|
||||
|
||||
double error = rateTarget - pitchRate;
|
||||
// Pitch input needed per rad/s² of angular acceleration
|
||||
double gain = axisInertia / response;
|
||||
state.trimIntegral = clamp(state.trimIntegral + INTEGRAL_GAIN * error * dt * gain, authority);
|
||||
@@ -68,6 +103,22 @@ final class AutoTrim {
|
||||
state.savedTrim = state.trimIntegral;
|
||||
}
|
||||
|
||||
/**
|
||||
* How far the ship is rotated about the axis away from level (ship up = world up), positive in the axis' direction.
|
||||
* NaN if the axis points nearly straight up, where pitch attitude is undefined.
|
||||
*/
|
||||
private static double attitude(Vector3d axis, Quaterniondc rotation) {
|
||||
Vector3d shipUp = new Vector3d(0.0, 1.0, 0.0);
|
||||
Vector3d worldUp = rotation.transformInverse(new Vector3d(0.0, 1.0, 0.0));
|
||||
shipUp.fma(-shipUp.dot(axis), axis);
|
||||
worldUp.fma(-worldUp.dot(axis), axis);
|
||||
if (shipUp.lengthSquared() < 1.0E-4 || worldUp.lengthSquared() < 1.0E-4) {
|
||||
return Double.NaN;
|
||||
}
|
||||
// Rotating the ship by +a about the axis turns world up, seen from the ship, by -a
|
||||
return -Math.atan2(shipUp.cross(worldUp, new Vector3d()).dot(axis), shipUp.dot(worldUp));
|
||||
}
|
||||
|
||||
private static double clamp(double value, double limit) {
|
||||
return Math.max(-limit, Math.min(limit, value));
|
||||
}
|
||||
|
||||
@@ -27,14 +27,19 @@ final class ImprovedAero {
|
||||
private static final double FLAP_EFFECTIVENESS = 0.75;
|
||||
// Angle of attack of a level cambered wing, where K is calibrated
|
||||
private static final double REFERENCE_ANGLE = Math.toRadians(4.0);
|
||||
private static final double REFERENCE_SPEED = 40.0;
|
||||
// Oswald efficiency of a finite wing
|
||||
private static final double EFFICIENCY = 0.85;
|
||||
|
||||
private ImprovedAero() {
|
||||
}
|
||||
|
||||
/** Lift constant K for the given design speed. */
|
||||
static double liftConstant(double designSpeed) {
|
||||
return 160.0 / (designSpeed * LIFT_SLOPE * REFERENCE_ANGLE);
|
||||
}
|
||||
/**
|
||||
* Lift constant K, fixed for every craft: a level wing of average proportions makes Warium's lift at 40 blocks per
|
||||
* second. Faster flight needs less angle of attack, slower flight more, and how fast a craft has to fly depends
|
||||
* only on its weight, wing area, wing shape and the air pressure at its altitude.
|
||||
*/
|
||||
static final double LIFT_CONSTANT = 160.0 / (REFERENCE_SPEED * LIFT_SLOPE * REFERENCE_ANGLE);
|
||||
|
||||
/**
|
||||
* Adds the surface's forces to out.
|
||||
@@ -44,10 +49,12 @@ final class ImprovedAero {
|
||||
* @param r block center relative to the center of mass, ship axes
|
||||
* @param input control input of the surface, -1 to 1
|
||||
* @param stalled whether the surface was stalled in the previous substep
|
||||
* @param liftScale multiplier for the lift only, not the drag
|
||||
* @param thinDrag parasitic drag coefficient of stabilizers and vertical wings
|
||||
* @return whether the surface is stalled now
|
||||
*/
|
||||
static boolean evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double weight,
|
||||
double liftConstant, double stallAngleScale, boolean stalled, Wrench out) {
|
||||
double liftConstant, double liftScale, double stallAngleScale, double thinDrag, boolean stalled, Wrench out) {
|
||||
// Airflow at the block: ship velocity plus w x r
|
||||
double vx = v[0] + w[1] * r[2] - w[2] * r[1];
|
||||
double vy = v[1] + w[2] * r[0] - w[0] * r[2];
|
||||
@@ -64,8 +71,8 @@ final class ImprovedAero {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Warium's own per-block drag
|
||||
double parasitic = -localSpeed * s.type().parasiticDrag * p / m;
|
||||
// Warium's own per-block drag, lower for thin surfaces since they stabilize with lift, not drag
|
||||
double parasitic = -localSpeed * (s.type().thin ? thinDrag : s.type().parasiticDrag) * p / m;
|
||||
out.addForce(r, vx * parasitic, vy * parasitic, vz * parasitic);
|
||||
|
||||
// Chord c along the facing, normal n up for horizontal surfaces and to the side for vertical ones
|
||||
@@ -103,9 +110,13 @@ final class ImprovedAero {
|
||||
double center = stalled ? critical - STALL_HYSTERESIS : critical;
|
||||
double attached = 1.0 - smoothstep(center - STALL_BLEND, center + STALL_BLEND, absAlpha);
|
||||
|
||||
double attachedLift = LIFT_SLOPE * alpha;
|
||||
// Finite wing theory: lift slope and induced drag follow from the wing's aspect ratio
|
||||
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 attachedLift = slope * alpha;
|
||||
double lift = attached * attachedLift + (1.0 - attached) * SEPARATED_LIFT * Math.sin(2.0 * alpha);
|
||||
double drag = attached * INDUCED_DRAG * attachedLift * attachedLift
|
||||
double drag = attached * inducedDrag * attachedLift * attachedLift
|
||||
+ (1.0 - attached) * SEPARATED_DRAG * Math.sin(alpha) * Math.sin(alpha);
|
||||
|
||||
double q = liftConstant * p * fd * planeSpeedSq;
|
||||
@@ -118,7 +129,7 @@ final class ImprovedAero {
|
||||
double dx = -(u * cx + vn * nx) / planeSpeed;
|
||||
double dy = -(vn * ny) / planeSpeed;
|
||||
double dz = -(u * cz + vn * nz) / planeSpeed;
|
||||
double liftForce = q * lift;
|
||||
double liftForce = q * lift * liftScale;
|
||||
double dragForce = q * drag / m;
|
||||
out.addForce(r, lx * liftForce + dx * dragForce, ly * liftForce + dy * dragForce, lz * liftForce + dz * dragForce);
|
||||
return stalled;
|
||||
|
||||
@@ -4,15 +4,16 @@ package dev.lumareth.improvedAerodynamics.flight;
|
||||
* Kinds of Warium aerodynamic blocks, one per Warium procedure. The numbers are only used by the improved flight
|
||||
* model: the angle of attack (degrees) at which the surface stalls, how far its airfoil's lift is shifted towards
|
||||
* positive angles (Warium calls wings asymmetric aerofoils, stabilizers symmetric ones) and its parasitic drag
|
||||
* coefficient, which is Warium's own per-block drag.
|
||||
* coefficient, which is Warium's own per-block drag. Thin surfaces (stabilizers, vertical wings) take their parasitic
|
||||
* drag from the stabilizerDrag config instead.
|
||||
*/
|
||||
public enum SurfaceType {
|
||||
WING(false, false, 16.0, 4.0, 1.0),
|
||||
STABILIZER(false, false, 14.0, 0.0, 1.0),
|
||||
VERTICAL_WING(true, false, 15.0, 0.0, 0.5),
|
||||
CONTROL_SURFACE(false, true, 15.0, 0.0, 0.0),
|
||||
VERTICAL_CONTROL_SURFACE(true, true, 15.0, 0.0, 0.0),
|
||||
AIR_BRAKE(false, false, 0.0, 0.0, 0.0);
|
||||
WING(false, false, false, 16.0, 4.0, 1.0),
|
||||
STABILIZER(false, false, true, 14.0, 0.0, 0.0),
|
||||
VERTICAL_WING(true, false, true, 15.0, 0.0, 0.0),
|
||||
CONTROL_SURFACE(false, true, false, 15.0, 0.0, 0.0),
|
||||
VERTICAL_CONTROL_SURFACE(true, true, false, 15.0, 0.0, 0.0),
|
||||
AIR_BRAKE(false, false, false, 0.0, 0.0, 0.0);
|
||||
|
||||
// Slab variants are thinner and stall earlier
|
||||
static final double SLAB_STALL_ANGLE = 12.0;
|
||||
@@ -21,13 +22,16 @@ public enum SurfaceType {
|
||||
final boolean vertical;
|
||||
/** Deflects with pilot input. */
|
||||
final boolean controllable;
|
||||
/** Parasitic drag comes from the stabilizerDrag config. */
|
||||
final boolean thin;
|
||||
final double stallAngle;
|
||||
final double camber;
|
||||
final double parasiticDrag;
|
||||
|
||||
SurfaceType(boolean vertical, boolean controllable, double stallAngle, double camber, double parasiticDrag) {
|
||||
SurfaceType(boolean vertical, boolean controllable, boolean thin, double stallAngle, double camber, double parasiticDrag) {
|
||||
this.vertical = vertical;
|
||||
this.controllable = controllable;
|
||||
this.thin = thin;
|
||||
this.stallAngle = stallAngle;
|
||||
this.camber = camber;
|
||||
this.parasiticDrag = parasiticDrag;
|
||||
|
||||
@@ -97,7 +97,7 @@ public final class WariumCapture {
|
||||
return new AeroSurface(pos.asLong(), type, slab, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5,
|
||||
facing.getStepX(), facing.getStepZ(), pressure, fluidFactor(level), speedMultiplier,
|
||||
node, ControlAxis.of(key), ControlAxis.multiplier(key), ControlAxis.of(key2), ControlAxis.multiplier(key2),
|
||||
brakeInput);
|
||||
brakeInput, 0.0);
|
||||
}
|
||||
|
||||
/** Registers a node's inputs once per tick, and saves its trim to it now and then so it survives reloading. */
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package dev.lumareth.improvedAerodynamics.flight;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Finds the aspect ratio (span squared over area) of every wing in a ship, which sets how well it makes lift and how
|
||||
* much drag that lift costs: long narrow wings are efficient, short stubby ones are not.
|
||||
* <p>
|
||||
* Neighbouring blocks of the same kind and facing form one wing. Horizontal wings also join across gaps of up to
|
||||
* {@link #MAX_GAP} blocks along the span, so the two halves of a wing either side of a fuselage count as one wing and
|
||||
* the span is the full wingspan. Air brakes are not wings and are left alone.
|
||||
*/
|
||||
final class WingShape {
|
||||
private static final int MAX_GAP = 6;
|
||||
private static final double MIN_ASPECT_RATIO = 0.5;
|
||||
private static final double MAX_ASPECT_RATIO = 12.0;
|
||||
|
||||
private WingShape() {
|
||||
}
|
||||
|
||||
static AeroSurface[] withAspectRatios(Collection<AeroSurface> surfaces) {
|
||||
AeroSurface[] all = surfaces.toArray(new AeroSurface[0]);
|
||||
int n = all.length;
|
||||
int[] cell = new int[n * 3];
|
||||
Long2IntOpenHashMap index = new Long2IntOpenHashMap(n * 2);
|
||||
index.defaultReturnValue(-1);
|
||||
int[] parent = new int[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
parent[i] = i;
|
||||
cell[i * 3] = (int) Math.floor(all[i].x());
|
||||
cell[i * 3 + 1] = (int) Math.floor(all[i].y());
|
||||
cell[i * 3 + 2] = (int) Math.floor(all[i].z());
|
||||
if (all[i].type() != SurfaceType.AIR_BRAKE) {
|
||||
index.put(key(all[i], cell[i * 3], cell[i * 3 + 1], cell[i * 3 + 2]), i);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
AeroSurface s = all[i];
|
||||
if (s.type() == SurfaceType.AIR_BRAKE) {
|
||||
continue;
|
||||
}
|
||||
int x = cell[i * 3];
|
||||
int y = cell[i * 3 + 1];
|
||||
int z = cell[i * 3 + 2];
|
||||
// Direct neighbours
|
||||
for (int d = 0; d < 6; d++) {
|
||||
int axis = d >> 1;
|
||||
int step = (d & 1) == 0 ? 1 : -1;
|
||||
union(parent, i, index.get(key(s, x + (axis == 0 ? step : 0), y + (axis == 1 ? step : 0), z + (axis == 2 ? step : 0))));
|
||||
}
|
||||
// Gaps along the span of horizontal wings, only in one direction since the other side finds this block
|
||||
if (!s.type().vertical) {
|
||||
for (int gap = 2; gap <= MAX_GAP + 1; gap++) {
|
||||
union(parent, i, index.get(key(s, x + s.rightX() * gap, y, z + s.rightZ() * gap)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[] count = new int[n];
|
||||
int[] minSpan = new int[n];
|
||||
int[] maxSpan = new int[n];
|
||||
java.util.Arrays.fill(minSpan, Integer.MAX_VALUE);
|
||||
java.util.Arrays.fill(maxSpan, Integer.MIN_VALUE);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (all[i].type() == SurfaceType.AIR_BRAKE) {
|
||||
continue;
|
||||
}
|
||||
int root = find(parent, i);
|
||||
int span = all[i].type().vertical ? cell[i * 3 + 1] : cell[i * 3] * Math.abs(all[i].rightX()) + cell[i * 3 + 2] * Math.abs(all[i].rightZ());
|
||||
count[root]++;
|
||||
minSpan[root] = Math.min(minSpan[root], span);
|
||||
maxSpan[root] = Math.max(maxSpan[root], span);
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (all[i].type() == SurfaceType.AIR_BRAKE) {
|
||||
continue;
|
||||
}
|
||||
int root = find(parent, i);
|
||||
double span = maxSpan[root] - minSpan[root] + 1;
|
||||
double aspectRatio = span * span / count[root];
|
||||
all[i] = all[i].withAspectRatio(Math.max(MIN_ASPECT_RATIO, Math.min(MAX_ASPECT_RATIO, aspectRatio)));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/** Position plus everything that must match for two blocks to be the same wing. */
|
||||
private static long key(AeroSurface s, int x, int y, int z) {
|
||||
long kind = (s.type().vertical ? 2 : 0) | (s.facingX() != 0 ? 1 : 0);
|
||||
return net.minecraft.core.BlockPos.asLong(x, y, z) * 4 + kind;
|
||||
}
|
||||
|
||||
private static int find(int[] parent, int i) {
|
||||
while (parent[i] != i) {
|
||||
parent[i] = parent[parent[i]];
|
||||
i = parent[i];
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
private static void union(int[] parent, int a, int b) {
|
||||
if (b < 0) {
|
||||
return;
|
||||
}
|
||||
parent[find(parent, a)] = find(parent, b);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user