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