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
|
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:",
|
.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",
|
"too high, and stalled surfaces stop damping rotation, which allows post-stall maneuvers with",
|
||||||
"thrust vectoring. Changes how craft fly")
|
"thrust vectoring. Changes how craft fly")
|
||||||
.define("improvedFlightModel", false);
|
.define("improvedFlightModel", false);
|
||||||
|
|
||||||
public static final ForgeConfigSpec.DoubleValue DESIGN_SPEED = BUILDER
|
public static final ForgeConfigSpec.DoubleValue LIFT_SCALE = BUILDER
|
||||||
.comment("Improved flight model: airspeed (blocks per second) at which a level wing makes the same lift as in Warium.",
|
.comment("Improved flight model: multiplier for the lift of all surfaces (drag is unchanged). Raise it if a craft",
|
||||||
"Slower flight needs a higher angle of attack, faster flight a lower one")
|
"cannot stay up at the speed it should fly at, lower it for craft that climb too easily")
|
||||||
.defineInRange("designSpeed", 40.0D, 5.0D, 300.0D);
|
.defineInRange("liftScale", 1.0D, 0.1D, 10.0D);
|
||||||
|
|
||||||
public static final ForgeConfigSpec.DoubleValue STALL_ANGLE_SCALE = BUILDER
|
public static final ForgeConfigSpec.DoubleValue STALL_ANGLE_SCALE = BUILDER
|
||||||
.comment("Improved flight model: multiplier for the angle of attack at which surfaces stall")
|
.comment("Improved flight model: multiplier for the angle of attack at which surfaces stall")
|
||||||
.defineInRange("stallAngleScale", 1.0D, 0.5D, 2.0D);
|
.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
|
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);
|
||||||
|
|||||||
@@ -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() {
|
public ImprovedAerodynamics() {
|
||||||
MinecraftForge.EVENT_BUS.register(LookaheadLoader.class);
|
MinecraftForge.EVENT_BUS.register(LookaheadLoader.class);
|
||||||
MinecraftForge.EVENT_BUS.register(PhysicsBridge.class);
|
MinecraftForge.EVENT_BUS.register(PhysicsBridge.class);
|
||||||
|
MinecraftForge.EVENT_BUS.register(ConfigCommands.class);
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
|
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
|
||||||
Network.register();
|
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
|
// 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 pitchResponse = new Vector3d();
|
||||||
final Vector3d pitchResponseBuilding = new Vector3d();
|
final Vector3d pitchResponseBuilding = new Vector3d();
|
||||||
|
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
|
||||||
volatile double savedTrim;
|
volatile double savedTrim;
|
||||||
|
|
||||||
NodeState(NodeData data) {
|
NodeState(NodeData data) {
|
||||||
@@ -102,7 +103,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void publish(long tick) {
|
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<>();
|
buildingSurfaces = new Long2ObjectOpenHashMap<>();
|
||||||
buildingNodes = new Long2ObjectOpenHashMap<>();
|
buildingNodes = new Long2ObjectOpenHashMap<>();
|
||||||
}
|
}
|
||||||
@@ -131,7 +132,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
boolean improved = Config.IMPROVED_FLIGHT_MODEL.get();
|
boolean improved = Config.IMPROVED_FLIGHT_MODEL.get();
|
||||||
Matrix3dc inertia = ship.getMomentOfInertia();
|
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);
|
evaluate(frame, centerOfMass, v, w, improved, true, wrench);
|
||||||
|
|
||||||
if (Config.IMPLICIT_DAMPING.get()) {
|
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) {
|
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.liftConstant(Config.DESIGN_SPEED.get());
|
double liftConstant = ImprovedAero.LIFT_CONSTANT;
|
||||||
double stallAngleScale = Config.STALL_ANGLE_SCALE.get();
|
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;
|
||||||
@@ -224,7 +227,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, stallAngleScale, stalled[i], out);
|
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, liftScale, stallAngleScale, thinDrag, 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;
|
||||||
@@ -239,7 +242,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, stallAngleScale, stalled[i], probe);
|
ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, liftScale, stallAngleScale, thinDrag, stalled[i], probe);
|
||||||
} else {
|
} else {
|
||||||
WariumAero.evaluate(s, v, w, r, input, weight, probe);
|
WariumAero.evaluate(s, v, w, r, input, weight, probe);
|
||||||
}
|
}
|
||||||
@@ -247,7 +250,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, stallAngleScale, stalled[i], probe);
|
ImprovedAero.evaluate(s, v, w, r, nudged, weight, liftConstant, liftScale, stallAngleScale, thinDrag, stalled[i], probe);
|
||||||
} else {
|
} else {
|
||||||
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
|
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[] w, double dt, boolean improved) {
|
||||||
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;
|
||||||
for (NodeData node : frame.nodes.values()) {
|
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.pitch = moveTowards(state.pitch, node.pitch(), step);
|
||||||
state.yaw = moveTowards(state.yaw, node.yaw(), step);
|
state.yaw = moveTowards(state.yaw, node.yaw(), step);
|
||||||
state.roll = moveTowards(state.roll, node.roll(), 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 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 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
|
||||||
*/
|
*/
|
||||||
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 brakeInput, double aspectRatio) {
|
||||||
public static final long NO_NODE = Long.MIN_VALUE;
|
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. */
|
/** Warium's "Direction.getClockWise(Axis.Y)" of the facing, the span direction of a horizontal surface. */
|
||||||
int rightX() {
|
int rightX() {
|
||||||
return -facingZ;
|
return -facingZ;
|
||||||
|
|||||||
@@ -7,19 +7,31 @@ import org.joml.Vector3d;
|
|||||||
import org.joml.Vector3dc;
|
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
|
* Auto trim for the pitch control surfaces of one control node: while the pilot gives no pitch input it holds the
|
||||||
* pitch offset that brings the flight path back to level and then holds it there, cancelling whatever pitching
|
* altitude the craft had when the pilot let go, cancelling whatever pitching moment the craft has (off-center weight,
|
||||||
* moment the craft has (off-center weight, thrust line, speed changes).
|
* thrust line, speed changes).
|
||||||
* <p>
|
* <p>
|
||||||
* Two loops: the flight path angle sets a gentle target pitch rate, and a PI controller moves the trim to reach it.
|
* Three nested loops: altitude error sets a gentle climb rate, climb rate error sets a nose attitude (never more than
|
||||||
* The trim's effect is measured every substep (torque per unit of pitch input), which gives the pitch axis and lets
|
* {@link #MAX_ATTITUDE} from level), attitude error sets a pitch rate, and a PI controller moves the trim to reach
|
||||||
* the gains be expressed as angular acceleration, so the same tuning works for any craft, speed and altitude.
|
* 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.
|
* The trim holds still while the pilot pitches, when too slow, and when wings stall, so it never fights maneuvers.
|
||||||
*/
|
*/
|
||||||
final class AutoTrim {
|
final class AutoTrim {
|
||||||
// Target pitch rate per radian of flight path angle, and its limit (rad/s)
|
// Climb rate (blocks/s) per block of altitude error, and its limit
|
||||||
private static final double PATH_GAIN = 0.8;
|
private static final double ALTITUDE_GAIN = 0.3;
|
||||||
private static final double MAX_PITCH_RATE = 0.1;
|
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
|
// Angular acceleration per rad/s of pitch rate error, and integral gain
|
||||||
private static final double PROPORTIONAL_GAIN = 3.0;
|
private static final double PROPORTIONAL_GAIN = 3.0;
|
||||||
private static final double INTEGRAL_GAIN = 1.5;
|
private static final double INTEGRAL_GAIN = 1.5;
|
||||||
@@ -30,12 +42,21 @@ final class AutoTrim {
|
|||||||
private AutoTrim() {
|
private AutoTrim() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void update(AeroState.NodeState state, NodeData node, Vector3dc worldVelocity, Quaterniondc rotation,
|
/** Altitude to hold and attitude integral, kept per node next to the trim. */
|
||||||
Matrix3dc inertia, double[] w, double dt, boolean stalled) {
|
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()) {
|
if (!node.autoTrim()) {
|
||||||
state.trimIntegral = 0.0;
|
state.trimIntegral = 0.0;
|
||||||
state.trimOutput = AeroState.moveTowards(state.trimOutput, 0.0, TRIM_RATE * dt);
|
state.trimOutput = AeroState.moveTowards(state.trimOutput, 0.0, TRIM_RATE * dt);
|
||||||
state.savedTrim = 0.0;
|
state.savedTrim = 0.0;
|
||||||
|
targets.altitude = Double.NaN;
|
||||||
|
targets.attitudeIntegral = 0.0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
double authority = Config.TRIM_AUTHORITY.get();
|
double authority = Config.TRIM_AUTHORITY.get();
|
||||||
@@ -43,6 +64,11 @@ final class AutoTrim {
|
|||||||
double speed = worldVelocity.length();
|
double speed = worldVelocity.length();
|
||||||
boolean hold = Math.abs(node.pitch()) > PILOT_DEADBAND || speed < Config.TRIM_MIN_SPEED.get() || stalled
|
boolean hold = Math.abs(node.pitch()) > PILOT_DEADBAND || speed < Config.TRIM_MIN_SPEED.get() || stalled
|
||||||
|| response < 1.0E-6;
|
|| 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;
|
double target = state.trimIntegral;
|
||||||
if (!hold) {
|
if (!hold) {
|
||||||
@@ -51,14 +77,23 @@ final class AutoTrim {
|
|||||||
double axisInertia = axis.dot(inertia.transform(axis, new Vector3d()));
|
double axisInertia = axis.dot(inertia.transform(axis, new Vector3d()));
|
||||||
double pitchRate = axis.x * w[0] + axis.y * w[1] + axis.z * w[2];
|
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 worldAxis = rotation.transform(axis, new Vector3d());
|
||||||
Vector3d direction = worldVelocity.normalize(new Vector3d());
|
Vector3d direction = worldVelocity.normalize(new Vector3d());
|
||||||
double noseUp = Math.signum(worldAxis.cross(direction, new Vector3d()).y);
|
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 climbTarget = clamp(ALTITUDE_GAIN * (targets.altitude - altitude), MAX_CLIMB_RATE);
|
||||||
double error = targetRate - pitchRate;
|
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
|
// Pitch input needed per rad/s² of angular acceleration
|
||||||
double gain = axisInertia / response;
|
double gain = axisInertia / response;
|
||||||
state.trimIntegral = clamp(state.trimIntegral + INTEGRAL_GAIN * error * dt * gain, authority);
|
state.trimIntegral = clamp(state.trimIntegral + INTEGRAL_GAIN * error * dt * gain, authority);
|
||||||
@@ -68,6 +103,22 @@ final class AutoTrim {
|
|||||||
state.savedTrim = state.trimIntegral;
|
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) {
|
private static double clamp(double value, double limit) {
|
||||||
return Math.max(-limit, Math.min(limit, value));
|
return Math.max(-limit, Math.min(limit, value));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,19 @@ final class ImprovedAero {
|
|||||||
private static final double FLAP_EFFECTIVENESS = 0.75;
|
private static final double FLAP_EFFECTIVENESS = 0.75;
|
||||||
// Angle of attack of a level cambered wing, where K is calibrated
|
// 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_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() {
|
private ImprovedAero() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Lift constant K for the given design speed. */
|
/**
|
||||||
static double liftConstant(double designSpeed) {
|
* Lift constant K, fixed for every craft: a level wing of average proportions makes Warium's lift at 40 blocks per
|
||||||
return 160.0 / (designSpeed * LIFT_SLOPE * REFERENCE_ANGLE);
|
* 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.
|
* 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 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 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
|
// 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];
|
||||||
@@ -64,8 +71,8 @@ final class ImprovedAero {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warium's own per-block drag
|
// Warium's own per-block drag, lower for thin surfaces since they stabilize with lift, not drag
|
||||||
double parasitic = -localSpeed * s.type().parasiticDrag * p / m;
|
double parasitic = -localSpeed * (s.type().thin ? thinDrag : s.type().parasiticDrag) * 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
|
||||||
@@ -103,9 +110,13 @@ final class ImprovedAero {
|
|||||||
double center = stalled ? critical - STALL_HYSTERESIS : critical;
|
double center = stalled ? critical - STALL_HYSTERESIS : critical;
|
||||||
double attached = 1.0 - smoothstep(center - STALL_BLEND, center + STALL_BLEND, absAlpha);
|
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 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);
|
+ (1.0 - attached) * SEPARATED_DRAG * Math.sin(alpha) * Math.sin(alpha);
|
||||||
|
|
||||||
double q = liftConstant * p * fd * planeSpeedSq;
|
double q = liftConstant * p * fd * planeSpeedSq;
|
||||||
@@ -118,7 +129,7 @@ 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;
|
double liftForce = q * lift * 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;
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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
|
* 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 {
|
public enum SurfaceType {
|
||||||
WING(false, false, 16.0, 4.0, 1.0),
|
WING(false, false, false, 16.0, 4.0, 1.0),
|
||||||
STABILIZER(false, false, 14.0, 0.0, 1.0),
|
STABILIZER(false, false, true, 14.0, 0.0, 0.0),
|
||||||
VERTICAL_WING(true, false, 15.0, 0.0, 0.5),
|
VERTICAL_WING(true, false, true, 15.0, 0.0, 0.0),
|
||||||
CONTROL_SURFACE(false, true, 15.0, 0.0, 0.0),
|
CONTROL_SURFACE(false, true, false, 15.0, 0.0, 0.0),
|
||||||
VERTICAL_CONTROL_SURFACE(true, true, 15.0, 0.0, 0.0),
|
VERTICAL_CONTROL_SURFACE(true, true, false, 15.0, 0.0, 0.0),
|
||||||
AIR_BRAKE(false, false, 0.0, 0.0, 0.0);
|
AIR_BRAKE(false, false, false, 0.0, 0.0, 0.0);
|
||||||
|
|
||||||
// Slab variants are thinner and stall earlier
|
// Slab variants are thinner and stall earlier
|
||||||
static final double SLAB_STALL_ANGLE = 12.0;
|
static final double SLAB_STALL_ANGLE = 12.0;
|
||||||
@@ -21,13 +22,16 @@ public enum SurfaceType {
|
|||||||
final boolean vertical;
|
final boolean vertical;
|
||||||
/** Deflects with pilot input. */
|
/** Deflects with pilot input. */
|
||||||
final boolean controllable;
|
final boolean controllable;
|
||||||
|
/** Parasitic drag comes from the stabilizerDrag config. */
|
||||||
|
final boolean thin;
|
||||||
final double stallAngle;
|
final double stallAngle;
|
||||||
final double camber;
|
final double camber;
|
||||||
final double parasiticDrag;
|
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.vertical = vertical;
|
||||||
this.controllable = controllable;
|
this.controllable = controllable;
|
||||||
|
this.thin = thin;
|
||||||
this.stallAngle = stallAngle;
|
this.stallAngle = stallAngle;
|
||||||
this.camber = camber;
|
this.camber = camber;
|
||||||
this.parasiticDrag = parasiticDrag;
|
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,
|
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);
|
brakeInput, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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. */
|
||||||
|
|||||||
@@ -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