feat(flight): enhance flight model with transonic drag and vortex lift features #1

Merged
richard merged 1 commits from transsonic_stuff into master 2026-10-30 10:05:43 +00:00
12 changed files with 177 additions and 26 deletions

No files matched your search

+1 -1
View File
@@ -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.2.0 mod_version=0.3.0
mod_group_id=dev.lumareth.improvedaerodynamics mod_group_id=dev.lumareth.improvedaerodynamics
mod_authors=Lumareth mod_authors=Lumareth
mod_description=Valkyrien Skies addon that keeps terrain streaming ahead of fast ships and smooths out Warium flight physics, with an optional flight model with real stalls and auto trim. mod_description=Valkyrien Skies addon that keeps terrain streaming ahead of fast ships and smooths out Warium flight physics, with an optional flight model with real stalls and auto trim.
@@ -92,6 +92,37 @@ public final class Config {
"with lift, so this only costs speed. Wings use Warium's 1.0, which also stands in for fuselage drag") "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); .defineInRange("stabilizerDrag", 0.2D, 0.0D, 2.0D);
public static final ForgeConfigSpec.BooleanValue TRANSONIC_DRAG = BUILDER
.comment("Improved flight model: zero-lift drag rises steeply around the speed of sound, so craft can no longer",
"cruise supersonic without a lot of thrust or a clean shape. Induced and stall drag are not affected")
.define("transonicDrag", true);
public static final ForgeConfigSpec.DoubleValue SPEED_OF_SOUND = BUILDER
.comment("Improved flight model: speed of sound in blocks per second, Mach 1 for the transonic drag")
.defineInRange("speedOfSound", 100.0D, 10.0D, 1000.0D);
public static final ForgeConfigSpec.DoubleValue TRANSONIC_PEAK_DRAG = BUILDER
.comment("Improved flight model: multiplier for zero-lift drag at its peak, at Mach 1.05")
.defineInRange("transonicPeakDrag", 4.0D, 1.0D, 20.0D);
public static final ForgeConfigSpec.DoubleValue SUPERSONIC_DRAG = BUILDER
.comment("Improved flight model: multiplier for zero-lift drag once fully supersonic, from Mach 1.3")
.defineInRange("supersonicDrag", 2.0D, 1.0D, 20.0D);
public static final ForgeConfigSpec.BooleanValue VORTEX_LIFT = BUILDER
.comment("Improved flight model: wings with a low aspect ratio (delta wings) stall later and get extra lift at",
"high angles of attack from leading-edge vortices. Wings with an aspect ratio of 3 or more are not affected")
.define("vortexLift", true);
public static final ForgeConfigSpec.DoubleValue VORTEX_STALL_BONUS = BUILDER
.comment("Improved flight model: extra stall angle of the lowest aspect ratio wings (1 or less) as a fraction of",
"the normal stall angle, 1.0 doubles it, for example from 16 to 32 degrees")
.defineInRange("vortexStallBonus", 1.0D, 0.0D, 3.0D);
public static final ForgeConfigSpec.DoubleValue VORTEX_LIFT_STRENGTH = BUILDER
.comment("Improved flight model: multiplier for the extra lift the vortices make at high angles of attack")
.defineInRange("vortexLiftStrength", 1.0D, 0.0D, 3.0D);
public static final ForgeConfigSpec.DoubleValue TRIM_AUTHORITY = BUILDER public static final ForgeConfigSpec.DoubleValue TRIM_AUTHORITY = BUILDER
.comment("Auto trim: largest pitch input (fraction of full deflection) the trim may hold") .comment("Auto trim: largest pitch input (fraction of full deflection) the trim may hold")
.defineInRange("trimAuthority", 0.5D, 0.0D, 1.0D); .defineInRange("trimAuthority", 0.5D, 0.0D, 1.0D);
@@ -8,6 +8,7 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands; import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.common.ForgeConfigSpec; import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.eventbus.api.SubscribeEvent;
@@ -17,6 +18,7 @@ import java.util.List;
/** /**
* /improvedaerodynamics (or /aero) to read and change the most important server config values in game. * /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. * {@code /aero} lists them, {@code /aero <option>} shows one, {@code /aero <option> <value>} sets and saves it.
* Needs operator rights, except for the owner of a singleplayer world, who may use them without cheats.
* All of them are read live, so changes apply immediately. * All of them are read live, so changes apply immediately.
*/ */
public final class ConfigCommands { public final class ConfigCommands {
@@ -32,6 +34,13 @@ public final class ConfigCommands {
new Option<>("lift_scale", Config.LIFT_SCALE, DoubleArgumentType.doubleArg(), Double.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<>("stall_angle_scale", Config.STALL_ANGLE_SCALE, DoubleArgumentType.doubleArg(), Double.class),
new Option<>("stabilizer_drag", Config.STABILIZER_DRAG, DoubleArgumentType.doubleArg(), Double.class), new Option<>("stabilizer_drag", Config.STABILIZER_DRAG, DoubleArgumentType.doubleArg(), Double.class),
new Option<>("transonic_drag", Config.TRANSONIC_DRAG, BoolArgumentType.bool(), Boolean.class),
new Option<>("speed_of_sound", Config.SPEED_OF_SOUND, DoubleArgumentType.doubleArg(), Double.class),
new Option<>("transonic_peak_drag", Config.TRANSONIC_PEAK_DRAG, DoubleArgumentType.doubleArg(), Double.class),
new Option<>("supersonic_drag", Config.SUPERSONIC_DRAG, DoubleArgumentType.doubleArg(), Double.class),
new Option<>("vortex_lift", Config.VORTEX_LIFT, BoolArgumentType.bool(), Boolean.class),
new Option<>("vortex_stall_bonus", Config.VORTEX_STALL_BONUS, DoubleArgumentType.doubleArg(), Double.class),
new Option<>("vortex_lift_strength", Config.VORTEX_LIFT_STRENGTH, DoubleArgumentType.doubleArg(), Double.class),
new Option<>("trim_authority", Config.TRIM_AUTHORITY, 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<>("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<>("air_placeholder", Config.ENABLE_AIR_PLACEHOLDER, BoolArgumentType.bool(), Boolean.class),
@@ -40,17 +49,26 @@ public final class ConfigCommands {
private ConfigCommands() { private ConfigCommands() {
} }
/** Operators, and the owner of a singleplayer world even when cheats are off. */
private static boolean mayUse(CommandSourceStack source) {
if (source.hasPermission(2)) {
return true;
}
return source.getEntity() instanceof ServerPlayer player
&& source.getServer().isSingleplayerOwner(player.getGameProfile());
}
@SubscribeEvent @SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) { public static void onRegisterCommands(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("improvedaerodynamics") LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("improvedaerodynamics")
.requires(source -> source.hasPermission(2)) .requires(ConfigCommands::mayUse)
.executes(context -> list(context.getSource())); .executes(context -> list(context.getSource()));
for (Option<?> option : OPTIONS) { for (Option<?> option : OPTIONS) {
root.then(optionNode(option)); root.then(optionNode(option));
} }
LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root); LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root);
event.getDispatcher().register(Commands.literal("aero") event.getDispatcher().register(Commands.literal("aero")
.requires(source -> source.hasPermission(2)) .requires(ConfigCommands::mayUse)
.executes(context -> list(context.getSource())) .executes(context -> list(context.getSource()))
.redirect(node)); .redirect(node));
} }
@@ -205,10 +205,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
private void evaluate(Frame frame, Vector3dc centerOfMass, double[] v, double[] w, boolean improved, boolean commit, Wrench out) { private void evaluate(Frame frame, Vector3dc centerOfMass, double[] v, double[] w, boolean improved, boolean commit, Wrench out) {
out.clear(); out.clear();
double weight = clock.weight(); double weight = clock.weight();
double liftConstant = ImprovedAero.LIFT_CONSTANT; ImprovedAero.Tuning tuning = improved ? ImprovedAero.Tuning.read() : null;
double stallAngleScale = Config.STALL_ANGLE_SCALE.get();
double liftScale = Config.LIFT_SCALE.get();
double thinDrag = Config.STABILIZER_DRAG.get();
double[] r = new double[3]; double[] r = new double[3];
if (commit) { if (commit) {
wingStalled = false; wingStalled = false;
@@ -227,7 +224,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
double input = s.type().controllable ? input(s, node, state, 0.0) : 0.0; double input = s.type().controllable ? input(s, node, state, 0.0) : 0.0;
if (improved) { if (improved) {
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, liftScale, stallAngleScale, thinDrag, stalled[i], out); boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], out);
if (commit) { if (commit) {
stalled[i] = nowStalled; stalled[i] = nowStalled;
wingStalled |= nowStalled && s.type() == SurfaceType.WING; wingStalled |= nowStalled && s.type() == SurfaceType.WING;
@@ -242,7 +239,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
probe.clear(); probe.clear();
double base = 0.0; double base = 0.0;
if (improved) { if (improved) {
ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, liftScale, stallAngleScale, thinDrag, stalled[i], probe); ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
} else { } else {
WariumAero.evaluate(s, v, w, r, input, weight, probe); WariumAero.evaluate(s, v, w, r, input, weight, probe);
} }
@@ -250,7 +247,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
probe.clear(); probe.clear();
double nudged = input(s, node, state, delta); double nudged = input(s, node, state, delta);
if (improved) { if (improved) {
ImprovedAero.evaluate(s, v, w, r, nudged, weight, liftConstant, liftScale, stallAngleScale, thinDrag, stalled[i], probe); ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
} else { } else {
WariumAero.evaluate(s, v, w, r, nudged, weight, probe); WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
} }
@@ -13,16 +13,17 @@ package dev.lumareth.improvedAerodynamics.flight;
* @param node BlockPos.asLong of the Vehicle Control Node the surface listens to, {@link #NO_NODE} if none * @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 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 * @param aspectRatio span squared over area of the wing the block belongs to, 0 if not known yet
* @param wingGroup whether the wing the block belongs to contains WING blocks, not only control surfaces or stabilizers
*/ */
public record AeroSurface(long key, SurfaceType type, boolean slab, double x, double y, double z, int facingX, int facingZ, 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, double pressure, double fluidFactor, double speedMultiplier,
long node, ControlAxis axis1, double multiplier1, ControlAxis axis2, double multiplier2, long node, ControlAxis axis1, double multiplier1, ControlAxis axis2, double multiplier2,
double brakeInput, double aspectRatio) { double brakeInput, double aspectRatio, boolean wingGroup) {
public static final long NO_NODE = Long.MIN_VALUE; public static final long NO_NODE = Long.MIN_VALUE;
AeroSurface withAspectRatio(double aspectRatio) { AeroSurface withWingShape(double aspectRatio, boolean wingGroup) {
return new AeroSurface(key, type, slab, x, y, z, facingX, facingZ, pressure, fluidFactor, speedMultiplier, return new AeroSurface(key, type, slab, x, y, z, facingX, facingZ, pressure, fluidFactor, speedMultiplier,
node, axis1, multiplier1, axis2, multiplier2, brakeInput, aspectRatio); node, axis1, multiplier1, axis2, multiplier2, brakeInput, aspectRatio, wingGroup);
} }
/** Warium's "Direction.getClockWise(Axis.Y)" of the facing, the span direction of a horizontal surface. */ /** Warium's "Direction.getClockWise(Axis.Y)" of the facing, the span direction of a horizontal surface. */
@@ -1,5 +1,7 @@
package dev.lumareth.improvedAerodynamics.flight; package dev.lumareth.improvedAerodynamics.flight;
import dev.lumareth.improvedAerodynamics.Config;
/** /**
* Flight model based on each surface's angle of attack, used instead of {@link WariumAero} when enabled. * Flight model based on each surface's angle of attack, used instead of {@link WariumAero} when enabled.
* <p> * <p>
@@ -41,6 +43,18 @@ final class ImprovedAero {
*/ */
static final double LIFT_CONSTANT = 160.0 / (REFERENCE_SPEED * LIFT_SLOPE * REFERENCE_ANGLE); static final double LIFT_CONSTANT = 160.0 / (REFERENCE_SPEED * LIFT_SLOPE * REFERENCE_ANGLE);
/** The config values the model reads, taken once per evaluation. */
record Tuning(double liftScale, double stallAngleScale, double thinDrag,
boolean transonic, double speedOfSound, double transonicPeakDrag, double supersonicDrag,
boolean vortex, double vortexStallBonus, double vortexLiftStrength) {
static Tuning read() {
return new Tuning(Config.LIFT_SCALE.get(), Config.STALL_ANGLE_SCALE.get(), Config.STABILIZER_DRAG.get(),
Config.TRANSONIC_DRAG.get(), Config.SPEED_OF_SOUND.get(), Config.TRANSONIC_PEAK_DRAG.get(),
Config.SUPERSONIC_DRAG.get(),
Config.VORTEX_LIFT.get(), Config.VORTEX_STALL_BONUS.get(), Config.VORTEX_LIFT_STRENGTH.get());
}
}
/** /**
* Adds the surface's forces to out. * Adds the surface's forces to out.
* *
@@ -49,12 +63,10 @@ final class ImprovedAero {
* @param r block center relative to the center of mass, ship axes * @param r block center relative to the center of mass, ship axes
* @param input control input of the surface, -1 to 1 * @param input control input of the surface, -1 to 1
* @param stalled whether the surface was stalled in the previous substep * @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 * @return whether the surface is stalled now
*/ */
static boolean evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double weight, static boolean evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double weight,
double liftConstant, double liftScale, double stallAngleScale, double thinDrag, boolean stalled, Wrench out) { Tuning tuning, boolean stalled, Wrench out) {
// Airflow at the block: ship velocity plus w x r // Airflow at the block: ship velocity plus w x r
double vx = v[0] + w[1] * r[2] - w[2] * r[1]; 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 vy = v[1] + w[2] * r[0] - w[0] * r[2];
@@ -71,8 +83,13 @@ final class ImprovedAero {
return false; return false;
} }
// Warium's own per-block drag, lower for thin surfaces since they stabilize with lift, not drag // Warium's own per-block drag, lower for thin surfaces since they stabilize with lift, not drag. Shock waves
double parasitic = -localSpeed * (s.type().thin ? thinDrag : s.type().parasiticDrag) * p / m; // multiply it near the speed of sound; this is the only zero-lift drag a craft has, induced and stall drag
// below are not affected
double shockDrag = tuning.transonic()
? TransonicDrag.multiplier(localSpeed / tuning.speedOfSound(), tuning.transonicPeakDrag(), tuning.supersonicDrag())
: 1.0;
double parasitic = -localSpeed * (s.type().thin ? tuning.thinDrag() : s.type().parasiticDrag) * shockDrag * p / m;
out.addForce(r, vx * parasitic, vy * parasitic, vz * parasitic); 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 // Chord c along the facing, normal n up for horizontal surfaces and to the side for vertical ones
@@ -99,7 +116,13 @@ final class ImprovedAero {
} }
alpha = Math.max(-Math.PI / 2.0, Math.min(Math.PI / 2.0, alpha)); alpha = Math.max(-Math.PI / 2.0, Math.min(Math.PI / 2.0, alpha));
double critical = Math.toRadians(s.slab() ? SurfaceType.SLAB_STALL_ANGLE : s.type().stallAngle) * stallAngleScale; // 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
double vortexFactor = tuning.vortex() && (s.type() == SurfaceType.WING || s.wingGroup() && s.type().controllable)
? VortexLift.factor(s.aspectRatio()) : 0.0;
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);
// Flips only where both blends agree, so the hysteresis never makes the force jump // Flips only where both blends agree, so the hysteresis never makes the force jump
if (!stalled && absAlpha > critical + STALL_BLEND) { if (!stalled && absAlpha > critical + STALL_BLEND) {
@@ -115,11 +138,14 @@ final class ImprovedAero {
double slope = aspectRatio > 0.0 ? 2.0 * Math.PI / (1.0 + 2.0 / aspectRatio) : LIFT_SLOPE; 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 = slope * alpha;
if (s.type() == SurfaceType.WING) {
attachedLift += VortexLift.liftCoefficient(alpha, vortexFactor, tuning.vortexLiftStrength());
}
double lift = attached * attachedLift + (1.0 - attached) * SEPARATED_LIFT * Math.sin(2.0 * alpha); double lift = attached * attachedLift + (1.0 - attached) * SEPARATED_LIFT * Math.sin(2.0 * alpha);
double drag = attached * inducedDrag * attachedLift * attachedLift double drag = attached * inducedDrag * attachedLift * attachedLift
+ (1.0 - attached) * SEPARATED_DRAG * Math.sin(alpha) * Math.sin(alpha); + (1.0 - attached) * SEPARATED_DRAG * Math.sin(alpha) * Math.sin(alpha);
double q = liftConstant * p * fd * planeSpeedSq; double q = LIFT_CONSTANT * p * fd * planeSpeedSq;
// Lift is perpendicular to the airflow in the chord/normal plane, on the side of n // Lift is perpendicular to the airflow in the chord/normal plane, on the side of n
double side = u < 0.0 ? -1.0 : 1.0; double side = u < 0.0 ? -1.0 : 1.0;
double lx = (Math.abs(u) * nx - side * vn * cx) / planeSpeed; double lx = (Math.abs(u) * nx - side * vn * cx) / planeSpeed;
@@ -129,13 +155,13 @@ final class ImprovedAero {
double dx = -(u * cx + vn * nx) / planeSpeed; double dx = -(u * cx + vn * nx) / planeSpeed;
double dy = -(vn * ny) / planeSpeed; double dy = -(vn * ny) / planeSpeed;
double dz = -(u * cz + vn * nz) / planeSpeed; double dz = -(u * cz + vn * nz) / planeSpeed;
double liftForce = q * lift * liftScale; double liftForce = q * lift * tuning.liftScale();
double dragForce = q * drag / m; double dragForce = q * drag / m;
out.addForce(r, lx * liftForce + dx * dragForce, ly * liftForce + dy * dragForce, lz * liftForce + dz * dragForce); out.addForce(r, lx * liftForce + dx * dragForce, ly * liftForce + dy * dragForce, lz * liftForce + dz * dragForce);
return stalled; return stalled;
} }
private 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);
} }
@@ -0,0 +1,33 @@
package dev.lumareth.improvedAerodynamics.flight;
/**
* Rise of zero-lift drag around the speed of sound: shock waves build up from Mach 0.8, drag peaks at Mach 1.05 and
* settles to a lower, still high, level once the flow is fully supersonic from Mach 1.3.
*/
final class TransonicDrag {
private static final double DRAG_RISE_START = 0.80;
private static final double PEAK = 1.05;
private static final double SUPERSONIC_START = 1.30;
private TransonicDrag() {
}
/**
* @param mach airspeed divided by the speed of sound
* @param peakDrag multiplier at the peak
* @param supersonicDrag multiplier once fully supersonic
* @return factor for parasitic drag, 1 in subsonic flight
*/
static double multiplier(double mach, double peakDrag, double supersonicDrag) {
if (mach <= DRAG_RISE_START) {
return 1.0;
}
if (mach < PEAK) {
return 1.0 + (peakDrag - 1.0) * ImprovedAero.smoothstep(DRAG_RISE_START, PEAK, mach);
}
if (mach < SUPERSONIC_START) {
return peakDrag + (supersonicDrag - peakDrag) * ImprovedAero.smoothstep(PEAK, SUPERSONIC_START, mach);
}
return supersonicDrag;
}
}
@@ -0,0 +1,29 @@
package dev.lumareth.improvedAerodynamics.flight;
/**
* Leading-edge vortices of low aspect ratio (delta) wings: the sharp edge rolls the flow up into a vortex that keeps
* the wing lifting far beyond the angle where a straight wing stalls.
*/
final class VortexLift {
// Polhamus' leading-edge suction analogy constant, about pi for a sharp edge
private static final double VORTEX_CONSTANT = Math.PI;
private static final double FULL_VORTEX_ASPECT_RATIO = 1.0;
private static final double NO_VORTEX_ASPECT_RATIO = 3.0;
private VortexLift() {
}
/** How much of the vortex effect a wing gets: 1 at aspect ratio 1 or less, 0 at 3 or more, 0 if its shape is unknown. */
static double factor(double aspectRatio) {
if (aspectRatio <= 0.0) {
return 0.0;
}
return 1.0 - ImprovedAero.smoothstep(FULL_VORTEX_ASPECT_RATIO, NO_VORTEX_ASPECT_RATIO, aspectRatio);
}
/** Lift coefficient added by the vortex, odd in the angle of attack like the normal lift. */
static double liftCoefficient(double alpha, double factor, double strength) {
double sin = Math.sin(alpha);
return strength * factor * VORTEX_CONSTANT * sin * Math.abs(sin) * Math.cos(alpha);
}
}
@@ -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, 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, facing.getStepX(), facing.getStepZ(), pressure, fluidFactor(level), speedMultiplier,
node, ControlAxis.of(key), ControlAxis.multiplier(key), ControlAxis.of(key2), ControlAxis.multiplier(key2), node, ControlAxis.of(key), ControlAxis.multiplier(key), ControlAxis.of(key2), ControlAxis.multiplier(key2),
brakeInput, 0.0); brakeInput, 0.0, false);
} }
/** Registers a node's inputs once per tick, and saves its trim to it now and then so it survives reloading. */ /** Registers a node's inputs once per tick, and saves its trim to it now and then so it survives reloading. */
@@ -61,6 +61,7 @@ final class WingShape {
int[] count = new int[n]; int[] count = new int[n];
int[] minSpan = new int[n]; int[] minSpan = new int[n];
int[] maxSpan = new int[n]; int[] maxSpan = new int[n];
boolean[] hasWing = new boolean[n];
java.util.Arrays.fill(minSpan, Integer.MAX_VALUE); java.util.Arrays.fill(minSpan, Integer.MAX_VALUE);
java.util.Arrays.fill(maxSpan, Integer.MIN_VALUE); java.util.Arrays.fill(maxSpan, Integer.MIN_VALUE);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
@@ -70,6 +71,7 @@ final class WingShape {
int root = find(parent, i); 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()); 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]++; count[root]++;
hasWing[root] |= all[i].type() == SurfaceType.WING;
minSpan[root] = Math.min(minSpan[root], span); minSpan[root] = Math.min(minSpan[root], span);
maxSpan[root] = Math.max(maxSpan[root], span); maxSpan[root] = Math.max(maxSpan[root], span);
} }
@@ -80,7 +82,7 @@ final class WingShape {
int root = find(parent, i); int root = find(parent, i);
double span = maxSpan[root] - minSpan[root] + 1; double span = maxSpan[root] - minSpan[root] + 1;
double aspectRatio = span * span / count[root]; double aspectRatio = span * span / count[root];
all[i] = all[i].withAspectRatio(Math.max(MIN_ASPECT_RATIO, Math.min(MAX_ASPECT_RATIO, aspectRatio))); all[i] = all[i].withWingShape(Math.max(MIN_ASPECT_RATIO, Math.min(MAX_ASPECT_RATIO, aspectRatio)), hasWing[root]);
} }
return all; return all;
} }
@@ -5,5 +5,12 @@
"message.improved_aerodynamics.auto_trim.off": "Automatische Trimmung aus", "message.improved_aerodynamics.auto_trim.off": "Automatische Trimmung aus",
"message.improved_aerodynamics.auto_trim.unavailable": "Automatische Trimmung an, sie braucht aber physicsRateAerodynamics in der Serverkonfiguration", "message.improved_aerodynamics.auto_trim.unavailable": "Automatische Trimmung an, sie braucht aber physicsRateAerodynamics in der Serverkonfiguration",
"message.improved_aerodynamics.auto_trim.no_seat": "Setz dich in einen Warium-Steuersitz, um die automatische Trimmung zu nutzen", "message.improved_aerodynamics.auto_trim.no_seat": "Setz dich in einen Warium-Steuersitz, um die automatische Trimmung zu nutzen",
"message.improved_aerodynamics.auto_trim.no_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden" "message.improved_aerodynamics.auto_trim.no_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden",
"config.improved_aerodynamics.transonicDrag": "Transsonischer Widerstand: Der Nullauftriebswiderstand steigt um die Schallgeschwindigkeit stark an (verbessertes Flugmodell)",
"config.improved_aerodynamics.speedOfSound": "Schallgeschwindigkeit in Blöcken pro Sekunde, Mach 1 für den transsonischen Widerstand",
"config.improved_aerodynamics.transonicPeakDrag": "Multiplikator für den Nullauftriebswiderstand an seinem Höchstwert bei Mach 1,05",
"config.improved_aerodynamics.supersonicDrag": "Multiplikator für den Nullauftriebswiderstand bei vollem Überschall, ab Mach 1,3",
"config.improved_aerodynamics.vortexLift": "Wirbelauftrieb: Flügel mit kleiner Streckung (Deltaflügel) überziehen später und liefern bei großen Anstellwinkeln mehr Auftrieb (verbessertes Flugmodell)",
"config.improved_aerodynamics.vortexStallBonus": "Zusätzlicher Überziehwinkel der Flügel mit der kleinsten Streckung, als Anteil des normalen Überziehwinkels",
"config.improved_aerodynamics.vortexLiftStrength": "Multiplikator für den zusätzlichen Auftrieb der Vorderkantenwirbel"
} }
@@ -5,5 +5,12 @@
"message.improved_aerodynamics.auto_trim.off": "Auto trim off", "message.improved_aerodynamics.auto_trim.off": "Auto trim off",
"message.improved_aerodynamics.auto_trim.unavailable": "Auto trim on, but it needs physicsRateAerodynamics enabled in the server config", "message.improved_aerodynamics.auto_trim.unavailable": "Auto trim on, but it needs physicsRateAerodynamics enabled in the server config",
"message.improved_aerodynamics.auto_trim.no_seat": "Sit in a Warium control seat to use auto trim", "message.improved_aerodynamics.auto_trim.no_seat": "Sit in a Warium control seat to use auto trim",
"message.improved_aerodynamics.auto_trim.no_node": "This seat is not linked to a vehicle control node" "message.improved_aerodynamics.auto_trim.no_node": "This seat is not linked to a vehicle control node",
"config.improved_aerodynamics.transonicDrag": "Transonic drag: zero-lift drag rises steeply around the speed of sound (improved flight model)",
"config.improved_aerodynamics.speedOfSound": "Speed of sound in blocks per second, Mach 1 for the transonic drag",
"config.improved_aerodynamics.transonicPeakDrag": "Zero-lift drag multiplier at its peak, Mach 1.05",
"config.improved_aerodynamics.supersonicDrag": "Zero-lift drag multiplier once fully supersonic, from Mach 1.3",
"config.improved_aerodynamics.vortexLift": "Vortex lift: low aspect ratio (delta) wings stall later and lift more at high angles of attack (improved flight model)",
"config.improved_aerodynamics.vortexStallBonus": "Extra stall angle of the lowest aspect ratio wings, as a fraction of the normal stall angle",
"config.improved_aerodynamics.vortexLiftStrength": "Multiplier for the extra lift the leading-edge vortices make"
} }