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:
richard committed 2026-10-28 22:06:50 +01:00
1 parent 4b7fb0e1e8
commit 599ecae21f
10 files changed
+329 -48

No files matched your search

@@ -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);
}
}