Reduce abstraction for lift calculation; remove RigidWings struct

This commit is contained in:
Ludvig Böklin 2021-04-24 18:11:18 +02:00
parent 96168b5654
commit 7186569259
4 changed files with 114 additions and 176 deletions

View File

@ -1,6 +1,6 @@
use super::{ use super::{
body::{object, Body}, body::{object, Body},
Density, Ori, Vel, CharacterState, Density, Ori, Vel,
}; };
use crate::{ use crate::{
consts::{AIR_DENSITY, WATER_DENSITY}, consts::{AIR_DENSITY, WATER_DENSITY},
@ -90,10 +90,9 @@ impl Default for Fluid {
impl Body { impl Body {
pub fn aerodynamic_forces( pub fn aerodynamic_forces(
&self, &self,
ori: &Ori,
rel_flow: &Vel, rel_flow: &Vel,
fluid_density: f32, fluid_density: f32,
wings: Option<&RigidWings>, character_state: Option<&CharacterState>,
) -> Vec3<f32> { ) -> Vec3<f32> {
let v_sq = rel_flow.0.magnitude_squared(); let v_sq = rel_flow.0.magnitude_squared();
if v_sq < 0.25 { if v_sq < 0.25 {
@ -104,39 +103,41 @@ impl Body {
// All the coefficients come pre-multiplied by their reference area // All the coefficients come pre-multiplied by their reference area
0.5 * fluid_density 0.5 * fluid_density
* v_sq * v_sq
* wings * character_state
.map(|wings| { .and_then(|cs| match cs {
// Since we have wings, we proceed to calculate the lift and drag CharacterState::Glide(data) => {
Some((data.aspect_ratio, data.planform_area, data.ori))
},
_ => None,
})
.map(|(ar, area, ori)| {
// We have an elliptical wing; proceed to calculate its lift and drag
let ar = wings.aspect_ratio();
// aoa will be positive when we're pitched up and negative otherwise // aoa will be positive when we're pitched up and negative otherwise
let aoa = angle_of_attack(ori, &rel_flow_dir); let aoa = angle_of_attack(&ori, &rel_flow_dir);
// c_l will be positive when aoa is positive (we have positive lift, // c_l will be positive when aoa is positive (we have positive lift,
// producing an upward force) and negative otherwise // producing an upward force) and negative otherwise
let c_l = wings.lift_coefficient(aoa); let c_l = lift_coefficient(ar, area, aoa);
// lift dir will be orthogonal to the local relative flow vector. // lift dir will be orthogonal to the local relative flow vector.
// Local relative flow is the resulting vector of (relative) freestream flow // Local relative flow is the resulting vector of (relative) freestream
// + downwash (created by the vortices of the wing tips) // flow + downwash (created by the vortices
// of the wing tips)
let lift_dir: Dir = { let lift_dir: Dir = {
// induced angle of attack // induced angle of attack
let aoa_i = c_l / (PI * ar); let aoa_i = c_l / (PI * ar);
// effective angle of attack; the aoa as seen by aerofoil after downwash // effective angle of attack; the aoa as seen by aerofoil after
// downwash
let aoa_eff = aoa - aoa_i; let aoa_eff = aoa - aoa_i;
/*println!( // Angle between chord line and local relative wind is aoa_eff
"CL={:.1}, α={:.1}°, αᵢ={:.1}°, αₑ={:.1}°, AR={:.1}", // radians. Direction of lift is
c_l, // perpendicular to local relative wind.
aoa.to_degrees(), // At positive lift, local relative wind will be below our cord line
aoa_i.to_degrees(), // at an angle of aoa_eff. Thus if
aoa_eff.to_degrees(), // we pitch down by aoa_eff radians then
ar // our chord line will be colinear with local relative wind vector
);*/ // and our up will be the direction
// Angle between chord line and local relative wind is aoa_eff radians. // of lift.
// Direction of lift is perpendicular to local relative wind.
// At positive lift, local relative wind will be below our cord line at
// an angle of aoa_eff. Thus if we pitch down by aoa_eff radians then
// our chord line will be colinear with local relative wind vector and
// our up will be the direction of lift.
ori.pitched_down(aoa_eff).up() ori.pitched_down(aoa_eff).up()
}; };
@ -146,20 +147,12 @@ impl Body {
// (this definition should not be used for aspect ratios > 25) // (this definition should not be used for aspect ratios > 25)
let e = 1.78 * (1.0 - 0.045 * ar.powf(0.68)) - 0.64; let e = 1.78 * (1.0 - 0.045 * ar.powf(0.68)) - 0.64;
wings.zero_lift_drag_coefficient() zero_lift_drag_coefficient(area)
+ self.parasite_drag_coefficient() + self.parasite_drag_coefficient()
+ c_l.powi(2) / (PI * e * ar) + c_l.powi(2) / (PI * e * ar)
}; };
debug_assert!(c_d.is_sign_positive()); debug_assert!(c_d.is_sign_positive());
debug_assert!(c_l.is_sign_positive() || aoa.is_sign_negative()); debug_assert!(c_l.is_sign_positive() || aoa.is_sign_negative());
/*println!(
"L/D (at α={:.1}, AR={:.1}) = {:.1}/{:.1} = {:.1}",
aoa.to_degrees(),
ar,
0.5 * fluid_density * v_sq * c_l,
0.5 * fluid_density * v_sq * c_d,
c_l / c_d
);*/
c_l * *lift_dir + c_d * *rel_flow_dir c_l * *lift_dir + c_d * *rel_flow_dir
}) })
@ -192,24 +185,28 @@ impl Body {
} else { } else {
1.0 1.0
}; };
cd * std::f32::consts::PI * dim.x * dim.z cd * PI * dim.x * dim.z
}, },
// Cross-section, zero-lift angle; exclude the wings (width * 0.2) // Cross-section, zero-lift angle; exclude the wings (width * 0.2)
Body::BirdMedium(_) | Body::BirdLarge(_) | Body::Dragon(_) => { Body::BirdMedium(_) | Body::BirdLarge(_) | Body::Dragon(_) => {
let dim = self.dimensions().map(|a| a * 0.5); let dim = self.dimensions().map(|a| a * 0.5);
// "Field Estimates of Body Drag Coefficient on the Basis of Dives in Passerine
// Birds", Anders Hedenström and Felix Liechti, 2001
let cd = match self { let cd = match self {
Body::BirdMedium(_) => 0.2, Body::BirdLarge(_) | Body::BirdMedium(_) => 0.2,
Body::BirdLarge(_) => 0.4, // arbitrary
_ => 0.7, _ => 0.7,
}; };
cd * std::f32::consts::PI * dim.x * 0.2 * dim.z cd * PI * dim.x * 0.2 * dim.z
}, },
// Cross-section, zero-lift angle; exclude the fins (width * 0.2) // Cross-section, zero-lift angle; exclude the fins (width * 0.2)
Body::FishMedium(_) | Body::FishSmall(_) => { Body::FishMedium(_) | Body::FishSmall(_) => {
let dim = self.dimensions().map(|a| a * 0.5); let dim = self.dimensions().map(|a| a * 0.5);
0.031 * std::f32::consts::PI * dim.x * 0.2 * dim.z // "A Simple Method to Determine Drag Coefficients in Aquatic Animals",
// D. Bilo and W. Nachtigall, 1980
0.031 * PI * dim.x * 0.2 * dim.z
}, },
Body::Object(object) => match object { Body::Object(object) => match object {
@ -225,7 +222,7 @@ impl Body {
| object::Body::FireworkYellow | object::Body::FireworkYellow
| object::Body::MultiArrow => { | object::Body::MultiArrow => {
let dim = self.dimensions().map(|a| a * 0.5); let dim = self.dimensions().map(|a| a * 0.5);
0.02 * std::f32::consts::PI * dim.x * dim.z 0.02 * PI * dim.x * dim.z
}, },
// spherical-ish objects // spherical-ish objects
@ -243,12 +240,12 @@ impl Body {
| object::Body::Pumpkin4 | object::Body::Pumpkin4
| object::Body::Pumpkin5 => { | object::Body::Pumpkin5 => {
let dim = self.dimensions().map(|a| a * 0.5); let dim = self.dimensions().map(|a| a * 0.5);
0.5 * std::f32::consts::PI * dim.x * dim.z 0.5 * PI * dim.x * dim.z
}, },
_ => { _ => {
let dim = self.dimensions(); let dim = self.dimensions();
2.0 * (std::f32::consts::PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0) 2.0 * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
}, },
}, },
@ -256,7 +253,7 @@ impl Body {
// Airships tend to use the square of the cube root of its volume for // Airships tend to use the square of the cube root of its volume for
// reference area // reference area
let dim = self.dimensions(); let dim = self.dimensions();
(std::f32::consts::PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0) (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
}, },
} }
} }
@ -267,56 +264,21 @@ fn angle_of_attack(ori: &Ori, rel_flow_dir: &Dir) -> f32 {
PI / 2.0 - ori.up().angle_between(rel_flow_dir.to_vec()) PI / 2.0 - ori.up().angle_between(rel_flow_dir.to_vec())
} }
/// An elliptical fixed rigid wing. Plurally named simply because it's a shape
/// typically composed of two wings forming an elliptical lift distribution.
//
// Animal wings are technically flexible, not rigid, (difference being that the
// former's shape is affected by the flow) and usually has the ability to
// assume complex shapes with properties like curved camber line, span-wise
// twist, dihedral angle, sweep angle, and partitioned sections. However, we
// could make do with this model for fully extended animal wings, enabling them
// to glide.
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RigidWings {
aspect_ratio: f32,
planform_area: f32,
// sweep_angle: Option<f32>,
}
impl RigidWings {
/// Wings from total span (wing-tip to wing-tip) and
/// chord length (leading edge to trailing edge)
pub fn new(span_length: f32, chord_length: f32) -> Self {
let planform_area = std::f32::consts::PI * chord_length * span_length * 0.25;
Self {
aspect_ratio: span_length.powi(2) / planform_area,
planform_area,
}
}
/// The aspect ratio is the ratio of the span squared to actual planform
/// area
pub fn aspect_ratio(&self) -> f32 { self.aspect_ratio }
pub fn planform_area(&self) -> f32 { self.planform_area }
}
impl RigidWings {
/// Total lift coefficient for a finite wing of symmetric aerofoil shape and /// Total lift coefficient for a finite wing of symmetric aerofoil shape and
/// elliptical pressure distribution. /// elliptical pressure distribution.
pub fn lift_coefficient(&self, aoa: f32) -> f32 { pub fn lift_coefficient(aspect_ratio: f32, planform_area: f32, aoa: f32) -> f32 {
let aoa_abs = aoa.abs(); let aoa_abs = aoa.abs();
let stall_angle = PI * 0.1; let stall_angle = PI * 0.1;
inline_tweak::tweak!(1.0) inline_tweak::tweak!(1.0)
* self.planform_area() * planform_area
* if aoa_abs < stall_angle { * if aoa_abs < stall_angle {
self.lift_slope(None) * aoa lift_slope(aspect_ratio, None) * aoa
} else if inline_tweak::tweak!(true) { } else if inline_tweak::tweak!(true) {
// This is when flow separation and turbulence starts to kick in. // This is when flow separation and turbulence starts to kick in.
// Going to just make something up (based on some data), as the alternative is // Going to just make something up (based on some data), as the alternative is
// to just throw your hands up and return 0 // to just throw your hands up and return 0
let aoa_s = aoa.signum(); let aoa_s = aoa.signum();
let c_l_max = self.lift_slope(None) * stall_angle; let c_l_max = lift_slope(aspect_ratio, None) * stall_angle;
let deg_45 = PI / 4.0; let deg_45 = PI / 4.0;
if aoa_abs < deg_45 { if aoa_abs < deg_45 {
// drop directly to 0.6 * max lift at stall angle // drop directly to 0.6 * max lift at stall angle
@ -333,56 +295,42 @@ impl RigidWings {
/// The zero-lift profile drag coefficient is the parasite drag on the wings /// The zero-lift profile drag coefficient is the parasite drag on the wings
/// at the angle of attack which generates no lift /// at the angle of attack which generates no lift
pub fn zero_lift_drag_coefficient(&self) -> f32 { pub fn zero_lift_drag_coefficient(planform_area: f32) -> f32 {
// avg value for Harris' hawk (Parabuteo unicinctus) [1] // avg value for Harris' hawk (Parabuteo unicinctus) [1]
self.planform_area() * 0.02 planform_area * 0.02
} }
/// The change in lift over change in angle of attack¹. Multiplying by angle /// The change in lift over change in angle of attack¹. Multiplying by angle
/// of attack gives the lift coefficient (for a finite wing, not aerofoil). /// of attack gives the lift coefficient (for a finite wing, not aerofoil).
///
/// Aspect ratio is the ratio of total wing span squared over planform area. /// Aspect ratio is the ratio of total wing span squared over planform area.
/// ///
/// # Notes /// # Notes
///
/// Only valid for symmetric, elliptical wings at small² angles of attack³. /// Only valid for symmetric, elliptical wings at small² angles of attack³.
/// Does not apply to twisted, cambered or delta wings. (It still gives a /// Does not apply to twisted, cambered or delta wings. (It still gives a
/// reasonably accurate approximation if the wing shape is not truly /// reasonably accurate approximation if the wing shape is not truly
/// elliptical.) /// elliptical.)
///
/// 1. geometric angle of attack, i.e. the pitch angle relative to /// 1. geometric angle of attack, i.e. the pitch angle relative to
/// freestream flow /// freestream flow
/// 2. up to around ~18°, at which point maximum lift has been achieved and /// 2. up to around ~18°, at which point maximum lift has been achieved and
/// thereafter falls precipitously, causing a stall (this is the stall /// thereafter falls precipitously, causing a stall (this is the stall
/// angle) 3. effective aoa, i.e. geometric aoa - induced aoa; assumes /// angle) 3. effective aoa, i.e. geometric aoa - induced aoa; assumes
/// no sideslip /// no sideslip
fn lift_slope(&self, sweep_angle: Option<f32>) -> f32 { fn lift_slope(aspect_ratio: f32, sweep_angle: Option<f32>) -> f32 {
// lift slope for a thin aerofoil, given by Thin Aerofoil Theory // lift slope for a thin aerofoil, given by Thin Aerofoil Theory
let ar = self.aspect_ratio();
let a0 = 2.0 * PI; let a0 = 2.0 * PI;
if let Some(sweep) = sweep_angle { if let Some(sweep) = sweep_angle {
// for swept wings we use Kuchemann's modification to Helmbold's // for swept wings we use Kuchemann's modification to Helmbold's
// equation // equation
let a0_cos_sweep = a0 * sweep.cos(); let a0_cos_sweep = a0 * sweep.cos();
let x = a0_cos_sweep / (PI * ar); let x = a0_cos_sweep / (PI * aspect_ratio);
a0_cos_sweep / ((1.0 + x.powi(2)).sqrt() + x) a0_cos_sweep / ((1.0 + x.powi(2)).sqrt() + x)
} else if ar < 4.0 { } else if aspect_ratio < 4.0 {
// for low aspect ratio wings (AR < 4) we use Helmbold's equation // for low aspect ratio wings (AR < 4) we use Helmbold's equation
let x = a0 / (PI * ar); let x = a0 / (PI * aspect_ratio);
a0 / ((1.0 + x.powi(2)).sqrt() + x) a0 / ((1.0 + x.powi(2)).sqrt() + x)
} else { } else {
// for high aspect ratio wings (AR > 4) we use the equation given by // for high aspect ratio wings (AR > 4) we use the equation given by
// Prandtl's lifting-line theory // Prandtl's lifting-line theory
a0 / (1.0 + (a0 / (PI * ar))) a0 / (1.0 + (a0 / (PI * aspect_ratio)))
} }
} }
}
/*
## References:
1. "Field Estimates of Body Drag Coefficient on the Basis of Dives in Passerine Birds",
Anders Hedenström and Felix Liechti, 2001
2. "A Simple Method to Determine Drag Coefficients in Aquatic Animals",
D. Bilo and W. Nachtigall, 1980
*/

View File

@ -68,7 +68,7 @@ pub use self::{
InputKind, InventoryAction, InventoryEvent, InventoryManip, MountState, Mounting, InputKind, InventoryAction, InventoryEvent, InventoryManip, MountState, Mounting,
}, },
energy::{Energy, EnergyChange, EnergySource}, energy::{Energy, EnergyChange, EnergySource},
fluid_dynamics::{Fluid, RigidWings}, fluid_dynamics::Fluid,
group::Group, group::Group,
home_chunk::HomeChunk, home_chunk::HomeChunk,
inputs::CanBuild, inputs::CanBuild,

View File

@ -1,6 +1,6 @@
use super::utils::handle_climb; use super::utils::handle_climb;
use crate::{ use crate::{
comp::{inventory::slot::EquipSlot, CharacterState, Ori, RigidWings, StateUpdate}, comp::{inventory::slot::EquipSlot, CharacterState, Ori, StateUpdate},
states::behavior::{CharacterBehavior, JoinData}, states::behavior::{CharacterBehavior, JoinData},
util::Dir, util::Dir,
}; };
@ -9,14 +9,19 @@ use vek::*;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Data { pub struct Data {
pub wings: RigidWings, /// The aspect ratio is the ratio of the span squared to actual planform
/// area
pub aspect_ratio: f32,
pub planform_area: f32,
pub ori: Ori, pub ori: Ori,
} }
impl Data { impl Data {
pub fn new(span_length: f32, chord_length: f32, ori: Ori) -> Self { pub fn new(span_length: f32, chord_length: f32, ori: Ori) -> Self {
let planform_area = std::f32::consts::PI * chord_length * span_length * 0.25;
Self { Self {
wings: RigidWings::new(span_length, chord_length), aspect_ratio: span_length.powi(2) / planform_area,
planform_area,
ori, ori,
} }
} }

View File

@ -9,7 +9,6 @@ use common::{
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
outcome::Outcome, outcome::Outcome,
resources::DeltaTime, resources::DeltaTime,
states,
terrain::{Block, TerrainGrid}, terrain::{Block, TerrainGrid},
uid::Uid, uid::Uid,
util::{Projection, SpatialGrid}, util::{Projection, SpatialGrid},
@ -43,7 +42,6 @@ fn fluid_density(height: f32, fluid: &Fluid) -> Density {
fn integrate_forces( fn integrate_forces(
dt: &DeltaTime, dt: &DeltaTime,
mut vel: Vel, mut vel: Vel,
ori: &Ori,
body: &Body, body: &Body,
density: &Density, density: &Density,
mass: &Mass, mass: &Mass,
@ -61,17 +59,7 @@ fn integrate_forces(
// Aerodynamic/hydrodynamic forces // Aerodynamic/hydrodynamic forces
if !rel_flow.0.is_approx_zero() { if !rel_flow.0.is_approx_zero() {
debug_assert!(!rel_flow.0.map(|a| a.is_nan()).reduce_or()); debug_assert!(!rel_flow.0.map(|a| a.is_nan()).reduce_or());
let glider: Option<&states::glide::Data> = character_state.and_then(|cs| match cs { let impulse = dt.0 * body.aerodynamic_forces(&rel_flow, fluid_density.0, character_state);
CharacterState::Glide(data) => Some(data),
_ => None,
});
let impulse = dt.0
* body.aerodynamic_forces(
glider.map(|g| &g.ori).unwrap_or(ori),
&rel_flow,
fluid_density.0,
glider.map(|g| g.wings).as_ref(),
);
debug_assert!(!impulse.map(|a| a.is_nan()).reduce_or()); debug_assert!(!impulse.map(|a| a.is_nan()).reduce_or());
if !impulse.is_approx_zero() { if !impulse.is_approx_zero() {
let new_v = vel.0 + impulse / mass.0; let new_v = vel.0 + impulse / mass.0;
@ -576,7 +564,6 @@ impl<'a> PhysicsData<'a> {
( (
positions, positions,
velocities, velocities,
&write.orientations,
read.stickies.maybe(), read.stickies.maybe(),
&read.bodies, &read.bodies,
read.character_states.maybe(), read.character_states.maybe(),
@ -595,7 +582,6 @@ impl<'a> PhysicsData<'a> {
( (
pos, pos,
vel, vel,
ori,
sticky, sticky,
body, body,
character_state, character_state,
@ -627,7 +613,6 @@ impl<'a> PhysicsData<'a> {
vel.0 = integrate_forces( vel.0 = integrate_forces(
&dt, &dt,
*vel, *vel,
ori,
body, body,
density, density,
mass, mass,