Skip to main content

veloren_common/comp/
fluid_dynamics.rs

1use super::{
2    Density, Ori, Vel,
3    body::{Body, object},
4};
5use crate::{
6    consts::{AIR_DENSITY, GRAVITY, LAVA_DENSITY, WATER_DENSITY},
7    util::{Dir, Plane, Projection},
8};
9use serde::{Deserialize, Serialize};
10use std::f32::consts::PI;
11use vek::*;
12
13#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, strum::EnumString)]
14pub enum LiquidKind {
15    Water,
16    Lava,
17}
18
19impl LiquidKind {
20    /// If an entity is in multiple overlapping liquid blocks, which one takes
21    /// precedence? (should be a rare edge case, since checkerboard patterns of
22    /// water and lava shouldn't show up in worldgen)
23    #[inline]
24    #[must_use]
25    pub fn merge(self, other: Self) -> Self {
26        use LiquidKind::{Lava, Water};
27        match (self, other) {
28            (Water, Water) => Water,
29            (Water, Lava) => Lava,
30            (Lava, _) => Lava,
31        }
32    }
33}
34
35/// Fluid medium in which the entity exists
36#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
37pub enum Fluid {
38    Air {
39        vel: Vel,
40        elevation: f32,
41    },
42    Liquid {
43        kind: LiquidKind,
44        vel: Vel,
45        depth: f32,
46    },
47}
48
49impl Fluid {
50    /// Specific mass
51    pub fn density(&self) -> Density {
52        match self {
53            Self::Air { .. } => Density(AIR_DENSITY),
54            Self::Liquid {
55                kind: LiquidKind::Water,
56                ..
57            } => Density(WATER_DENSITY),
58            Self::Liquid {
59                kind: LiquidKind::Lava,
60                ..
61            } => Density(LAVA_DENSITY),
62        }
63    }
64
65    /// Pressure from entity velocity
66    pub fn dynamic_pressure(&self, vel: &Vel) -> f32 {
67        0.5 * self.density().0 * self.relative_flow(vel).0.magnitude_squared()
68    }
69
70    /*
71        pub fn static_pressure(&self) -> f32 {
72            match self {
73                Self::Air { elevation, .. } => Self::air_pressure(*elevation),
74                Self::Water { depth, .. } => Self::water_pressure(*depth),
75            }
76        }
77
78        /// Absolute static pressure of air at elevation
79        pub fn air_pressure(elevation: f32) -> f32 {
80            // At low altitudes above sea level, the pressure decreases by about 1.2 kPa for
81            // every 100 metres.
82            // https://en.wikipedia.org/wiki/Atmospheric_pressure#Altitude_variation
83            ATMOSPHERE - elevation / 12.0
84        }
85
86        /// Absolute static pressure of water at depth
87        pub fn water_pressure(depth: f32) -> f32 { WATER_DENSITY * GRAVITY * depth + ATMOSPHERE }
88    */
89    /// Velocity of fluid, if applicable
90    pub fn flow_vel(&self) -> Vel {
91        match self {
92            Self::Air { vel, .. } => *vel,
93            Self::Liquid { vel, .. } => *vel,
94        }
95    }
96
97    // Very simple but useful in reducing mental overhead
98    pub fn relative_flow(&self, vel: &Vel) -> Vel { Vel(self.flow_vel().0 - vel.0) }
99
100    pub fn is_liquid(&self) -> bool { matches!(self, Fluid::Liquid { .. }) }
101
102    pub fn elevation(&self) -> Option<f32> {
103        match self {
104            Fluid::Air { elevation, .. } => Some(*elevation),
105            _ => None,
106        }
107    }
108
109    pub fn depth(&self) -> Option<f32> {
110        match self {
111            Fluid::Liquid { depth, .. } => Some(*depth),
112            _ => None,
113        }
114    }
115}
116
117impl Default for Fluid {
118    fn default() -> Self {
119        Self::Air {
120            elevation: 0.0,
121            vel: Vel::zero(),
122        }
123    }
124}
125
126pub struct Wings {
127    pub aspect_ratio: f32,
128    pub planform_area: f32,
129    pub ori: Ori,
130}
131
132impl Body {
133    pub fn aerodynamic_forces(
134        &self,
135        rel_flow: &Vel,
136        fluid_density: f32,
137        wings: Option<&Wings>,
138        scale: f32,
139    ) -> Vec3<f32> {
140        let v_sq = rel_flow.0.magnitude_squared();
141        if v_sq < 0.25 {
142            // don't bother with minuscule forces
143            Vec3::zero()
144        } else {
145            let rel_flow_dir = Dir::new(rel_flow.0 / v_sq.sqrt());
146            let power_vec = match wings {
147                Some(&Wings {
148                    aspect_ratio,
149                    planform_area,
150                    ori,
151                }) => {
152                    if aspect_ratio > 25.0 {
153                        tracing::warn!(
154                            "Calculating lift for wings with an aspect ratio of {}. The formulas \
155                             are only valid for aspect ratios below 25.",
156                            aspect_ratio
157                        )
158                    };
159                    let ar = aspect_ratio.min(24.0);
160                    // We have an elliptical wing; proceed to calculate its lift and drag
161
162                    // aoa will be positive when we're pitched up and negative otherwise
163                    let aoa = angle_of_attack(&ori, &rel_flow_dir);
164                    // c_l will be positive when aoa is positive (we have positive lift,
165                    // producing an upward force) and negative otherwise
166                    let c_l = lift_coefficient(ar, aoa);
167
168                    // lift dir will be orthogonal to the local relative flow vector.
169                    // Local relative flow is the resulting vector of (relative) freestream
170                    // flow + downwash (created by the vortices
171                    // of the wing tips)
172                    let lift_dir: Dir = {
173                        // induced angle of attack
174                        let aoa_i = c_l / (PI * ar);
175                        // effective angle of attack; the aoa as seen by aerofoil after
176                        // downwash
177                        let aoa_eff = aoa - aoa_i;
178                        // Angle between chord line and local relative wind is aoa_eff
179                        // radians. Direction of lift is
180                        // perpendicular to local relative wind.
181                        // At positive lift, local relative wind will be below our cord line
182                        // at an angle of aoa_eff. Thus if
183                        // we pitch down by aoa_eff radians then
184                        // our chord line will be colinear with local relative wind vector
185                        // and our up will be the direction
186                        // of lift.
187                        ori.pitched_down(aoa_eff).up()
188                    };
189
190                    // induced drag coefficient (drag due to lift)
191                    let cdi = {
192                        // Oswald's efficiency factor (empirically derived--very magical)
193                        // (this definition should not be used for aspect ratios > 25)
194                        let e = 1.78 * (1.0 - 0.045 * ar.powf(0.68)) - 0.64;
195                        c_l.powi(2) / (PI * e * ar)
196                    };
197
198                    // drag coefficient
199                    let c_d = zero_lift_drag_coefficient() + cdi;
200                    debug_assert!(c_d.is_sign_positive());
201                    debug_assert!(c_l.is_sign_positive() || aoa.is_sign_negative());
202
203                    planform_area * scale.powf(2.0) * (c_l * *lift_dir + c_d * *rel_flow_dir)
204                        + self.parasite_drag(scale) * *rel_flow_dir
205                },
206
207                _ => self.parasite_drag(scale) * *rel_flow_dir,
208            };
209
210            // This is the drag equation for a body in a fluid.
211            // F_d = 0.5 * rho * velocity^2 * drag_coefficient * reference_area,
212            // where rho is the fluid density, velocity is the velocity of the body
213            // relative to the fluid, drag_coefficient is a dimensionless coefficient
214            // that we assume is 1.0, and reference_area is related to the cross-section
215            // of the body in the direction of the flow. power_vec is the "reference area"
216            // in 3D, accounting for the flow direction. If the body has wings,
217            // this accounts for both lift and parasite drag. If the body
218            // does not have wings, this is for parasite drag only.
219            0.5 * fluid_density * v_sq * power_vec
220        }
221    }
222
223    /// Physically incorrect (but relatively dt-independent) way to calculate
224    /// drag coefficients for liquids.
225    // TODO: Remove this in favour of `aerodynamic_forces` (see: `integrate_forces`)
226    pub fn drag_coefficient_liquid(&self, fluid_density: f32, scale: f32) -> f32 {
227        fluid_density * self.parasite_drag(scale)
228    }
229
230    /// Parasite drag is the sum of pressure drag and skin friction.
231    /// Skin friction is the drag arising from the shear forces between a fluid
232    /// and a surface, while pressure drag is due to flow separation. Both are
233    /// viscous effects. The returned value is alternatively called the
234    /// Reference Area of the body, and is used in the drag force equation.
235    pub fn parasite_drag(&self, scale: f32) -> f32 {
236        let from_terminal_velocity =
237            |vel: f32| 2.0 * self.mass().0 * GRAVITY * scale * scale / (vel * vel * AIR_DENSITY);
238
239        // Reference area and drag coefficient assumes best-case scenario of the
240        // orientation producing least amount of drag
241        match self {
242            Body::Humanoid(_) => from_terminal_velocity(90.0),
243
244            Body::QuadrupedSmall(_) => from_terminal_velocity(20.0),
245
246            Body::QuadrupedMedium(_) => from_terminal_velocity(70.0),
247
248            Body::BirdMedium(_) => from_terminal_velocity(100.0),
249
250            Body::FishMedium(_) => from_terminal_velocity(120.0),
251
252            Body::Dragon(_) => from_terminal_velocity(150.0),
253
254            Body::BirdLarge(_) => from_terminal_velocity(130.0),
255
256            Body::FishSmall(_) => from_terminal_velocity(100.0),
257
258            Body::BipedLarge(_) => from_terminal_velocity(120.0),
259
260            Body::BipedSmall(_) => from_terminal_velocity(50.0),
261
262            Body::Golem(_) => from_terminal_velocity(200.0),
263
264            Body::Theropod(_) => from_terminal_velocity(130.0),
265
266            Body::QuadrupedLow(_) => from_terminal_velocity(60.0),
267
268            Body::Arthropod(_) => from_terminal_velocity(50.0),
269
270            Body::Crustacean(_) => from_terminal_velocity(50.0),
271
272            Body::Object(object) => match object {
273                // very streamlined objects
274                object::Body::Arrow
275                | object::Body::ArrowSnake
276                | object::Body::ArrowTurret
277                | object::Body::ArrowClay
278                | object::Body::FireworkBlue
279                | object::Body::FireworkGreen
280                | object::Body::FireworkPurple
281                | object::Body::FireworkRed
282                | object::Body::FireworkWhite
283                | object::Body::FireworkYellow
284                | object::Body::MultiArrow
285                | object::Body::BoltBesieger
286                | object::Body::Dart
287                | object::Body::BubbleBomb => {
288                    let dim = self.dimensions().map(|a| a * 0.5 * scale);
289                    const CD: f32 = 0.02;
290                    CD * PI * dim.x * dim.z
291                },
292
293                // spherical-ish objects
294                object::Body::BoltFire
295                | object::Body::BoltFireBig
296                | object::Body::BoltNature
297                | object::Body::Bomb
298                | object::Body::Pumpkin
299                | object::Body::Pebble
300                | object::Body::IronPikeBomb
301                | object::Body::StrigoiHead => {
302                    let dim = self.dimensions().map(|a| a * 0.5 * scale);
303                    const CD: f32 = 0.5;
304                    CD * PI * dim.x * dim.z
305                },
306
307                // frictionless
308                object::Body::FireRing => 0.0,
309                object::Body::PyroclasmBolt => 0.0,
310
311                _ => {
312                    let dim = self.dimensions().map(|a| a * scale);
313                    const CD: f32 = 2.0;
314                    CD * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
315                },
316            },
317
318            Body::Item(_) => {
319                let dim = self.dimensions().map(|a| a * scale);
320                const CD: f32 = 2.0;
321                CD * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
322            },
323
324            Body::Ship(_) => {
325                // Airships tend to use the square of the cube root of its volume for
326                // reference area
327                let dim = self.dimensions().map(|a| a * scale);
328                (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
329            },
330
331            Body::Plugin(body) => body.parasite_drag(),
332        }
333    }
334}
335
336/// Geometric angle of attack
337///
338/// # Note
339/// This ignores spanwise flow (i.e. we remove the spanwise flow component).
340/// With greater yaw comes greater loss of accuracy as more flow goes
341/// unaccounted for.
342pub fn angle_of_attack(ori: &Ori, rel_flow_dir: &Dir) -> f32 {
343    rel_flow_dir
344        .projected(&Plane::from(ori.right()))
345        .map(|flow_dir| PI / 2.0 - ori.up().angle_between(flow_dir.to_vec()))
346        .unwrap_or(0.0)
347}
348
349/// Total lift coefficient for a finite wing of symmetric aerofoil shape and
350/// elliptical pressure distribution.
351pub fn lift_coefficient(aspect_ratio: f32, aoa: f32) -> f32 {
352    let aoa_abs = aoa.abs();
353    let stall_angle = PI * 0.1;
354    if aoa_abs < stall_angle {
355        lift_slope(aspect_ratio, None) * aoa
356    } else {
357        // This is when flow separation and turbulence starts to kick in.
358        // Going to just make something up (based on some data), as the alternative is
359        // to just throw your hands up and return 0
360        let aoa_s = aoa.signum();
361        let c_l_max = lift_slope(aspect_ratio, None) * stall_angle;
362        let deg_45 = PI / 4.0;
363        if aoa_abs < deg_45 {
364            // drop directly to 0.6 * max lift at stall angle
365            // then climb back to max at 45°
366            Lerp::lerp(0.6 * c_l_max, c_l_max, aoa_abs / deg_45) * aoa_s
367        } else {
368            // let's just say lift goes down linearly again until we're at 90°
369            Lerp::lerp(c_l_max, 0.0, (aoa_abs - deg_45) / deg_45) * aoa_s
370        }
371    }
372}
373
374/// The zero-lift profile drag coefficient is the parasite drag on the wings
375/// at the angle of attack which generates no lift
376pub fn zero_lift_drag_coefficient() -> f32 { 0.026 }
377
378/// The change in lift over change in angle of attack¹. Multiplying by angle
379/// of attack gives the lift coefficient (for a finite wing, not aerofoil).
380/// Aspect ratio is the ratio of total wing span squared over planform area.
381///
382/// # Notes
383/// Only valid for symmetric, elliptical wings at small² angles of attack³.
384/// Does not apply to twisted, cambered or delta wings. (It still gives a
385/// reasonably accurate approximation if the wing shape is not truly
386/// elliptical.)
387///   1. geometric angle of attack, i.e. the pitch angle relative to freestream
388///      flow
389///   2. up to around ~18°, at which point maximum lift has been achieved and
390///      thereafter falls precipitously, causing a stall (this is the stall
391///      angle)
392///   3. effective aoa, i.e. geometric aoa - induced aoa; assumes no sideslip
393// TODO: Look into handling tapered wings
394fn lift_slope(aspect_ratio: f32, sweep_angle: Option<f32>) -> f32 {
395    // lift slope for a thin aerofoil, given by Thin Aerofoil Theory
396    let a0 = 2.0 * PI;
397    if let Some(sweep) = sweep_angle {
398        // for swept wings we use Kuchemann's modification to Helmbold's
399        // equation
400        let a0_cos_sweep = a0 * sweep.cos();
401        let x = a0_cos_sweep / (PI * aspect_ratio);
402        a0_cos_sweep / ((1.0 + x.powi(2)).sqrt() + x)
403    } else if aspect_ratio < 4.0 {
404        // for low aspect ratio wings (AR < 4) we use Helmbold's equation
405        let x = a0 / (PI * aspect_ratio);
406        a0 / ((1.0 + x.powi(2)).sqrt() + x)
407    } else {
408        // for high aspect ratio wings (AR > 4) we use the equation given by
409        // Prandtl's lifting-line theory
410        a0 / (1.0 + (a0 / (PI * aspect_ratio)))
411    }
412}