veloren_common/comp/
fluid_dynamics.rs

1use super::{
2    Density, Ori, Vel,
3    body::{Body, object},
4};
5use crate::{
6    consts::{AIR_DENSITY, 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            0.5 * fluid_density * v_sq * power_vec
211        }
212    }
213
214    /// Physically incorrect (but relatively dt-independent) way to calculate
215    /// drag coefficients for liquids.
216    // TODO: Remove this in favour of `aerodynamic_forces` (see: `integrate_forces`)
217    pub fn drag_coefficient_liquid(&self, fluid_density: f32, scale: f32) -> f32 {
218        fluid_density * self.parasite_drag(scale)
219    }
220
221    /// Parasite drag is the sum of pressure drag and skin friction.
222    /// Skin friction is the drag arising from the shear forces between a fluid
223    /// and a surface, while pressure drag is due to flow separation. Both are
224    /// viscous effects.
225    fn parasite_drag(&self, scale: f32) -> f32 {
226        // Reference area and drag coefficient assumes best-case scenario of the
227        // orientation producing least amount of drag
228        match self {
229            // Cross-section, head/feet first
230            Body::BipedLarge(_) | Body::BipedSmall(_) | Body::Golem(_) | Body::Humanoid(_) => {
231                let dim = self.dimensions().xy().map(|a| a * 0.5 * scale);
232                const CD: f32 = 0.7;
233                CD * PI * dim.x * dim.y
234            },
235
236            // Cross-section, nose/tail first
237            Body::Theropod(_)
238            | Body::QuadrupedMedium(_)
239            | Body::QuadrupedSmall(_)
240            | Body::QuadrupedLow(_)
241            | Body::Arthropod(_) => {
242                let dim = self.dimensions().map(|a| a * 0.5 * scale);
243                let cd: f32 = if matches!(self, Body::QuadrupedLow(_)) {
244                    0.7
245                } else {
246                    1.0
247                };
248                cd * PI * dim.x * dim.z
249            },
250
251            // Cross-section, zero-lift angle; exclude the wings (width * 0.2)
252            Body::BirdMedium(_) | Body::BirdLarge(_) | Body::Dragon(_) => {
253                let dim = self.dimensions().map(|a| a * 0.5 * scale);
254                let cd: f32 = match self {
255                    // "Field Estimates of Body Drag Coefficient
256                    // on the Basis of Dives in Passerine Birds",
257                    // Anders Hedenström and Felix Liechti, 2001
258                    Body::BirdLarge(_) | Body::BirdMedium(_) => 0.2,
259                    // arbitrary
260                    _ => 0.7,
261                };
262                cd * PI * dim.x * 0.2 * dim.z
263            },
264
265            // Cross-section, zero-lift angle; exclude the fins (width * 0.2)
266            Body::FishMedium(_) | Body::FishSmall(_) | Body::Crustacean(_) => {
267                let dim = self.dimensions().map(|a| a * 0.5 * scale);
268                // "A Simple Method to Determine Drag Coefficients in Aquatic Animals",
269                // D. Bilo and W. Nachtigall, 1980
270                const CD: f32 = 0.031;
271                CD * PI * dim.x * 0.2 * dim.z
272            },
273
274            Body::Object(object) => match object {
275                // very streamlined objects
276                object::Body::Arrow
277                | object::Body::ArrowSnake
278                | object::Body::ArrowTurret
279                | object::Body::ArrowClay
280                | object::Body::FireworkBlue
281                | object::Body::FireworkGreen
282                | object::Body::FireworkPurple
283                | object::Body::FireworkRed
284                | object::Body::FireworkWhite
285                | object::Body::FireworkYellow
286                | object::Body::MultiArrow
287                | object::Body::BoltBesieger
288                | object::Body::Dart
289                | object::Body::BubbleBomb => {
290                    let dim = self.dimensions().map(|a| a * 0.5 * scale);
291                    const CD: f32 = 0.02;
292                    CD * PI * dim.x * dim.z
293                },
294
295                // spherical-ish objects
296                object::Body::BoltFire
297                | object::Body::BoltFireBig
298                | object::Body::BoltNature
299                | object::Body::Bomb
300                | object::Body::PotionBlue
301                | object::Body::PotionGreen
302                | object::Body::PotionRed
303                | object::Body::Pouch
304                | object::Body::Pumpkin
305                | object::Body::Pumpkin2
306                | object::Body::Pumpkin3
307                | object::Body::Pumpkin4
308                | object::Body::Pumpkin5
309                | object::Body::Pebble
310                | object::Body::IronPikeBomb
311                | object::Body::StrigoiHead => {
312                    let dim = self.dimensions().map(|a| a * 0.5 * scale);
313                    const CD: f32 = 0.5;
314                    CD * PI * dim.x * dim.z
315                },
316
317                _ => {
318                    let dim = self.dimensions().map(|a| a * scale);
319                    const CD: f32 = 2.0;
320                    CD * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
321                },
322            },
323
324            Body::ItemDrop(_) => {
325                let dim = self.dimensions().map(|a| a * scale);
326                const CD: f32 = 2.0;
327                CD * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
328            },
329
330            Body::Ship(_) => {
331                // Airships tend to use the square of the cube root of its volume for
332                // reference area
333                let dim = self.dimensions().map(|a| a * scale);
334                (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
335            },
336
337            Body::Plugin(body) => body.parasite_drag(),
338        }
339    }
340}
341
342/// Geometric angle of attack
343///
344/// # Note
345/// This ignores spanwise flow (i.e. we remove the spanwise flow component).
346/// With greater yaw comes greater loss of accuracy as more flow goes
347/// unaccounted for.
348pub fn angle_of_attack(ori: &Ori, rel_flow_dir: &Dir) -> f32 {
349    rel_flow_dir
350        .projected(&Plane::from(ori.right()))
351        .map(|flow_dir| PI / 2.0 - ori.up().angle_between(flow_dir.to_vec()))
352        .unwrap_or(0.0)
353}
354
355/// Total lift coefficient for a finite wing of symmetric aerofoil shape and
356/// elliptical pressure distribution.
357pub fn lift_coefficient(aspect_ratio: f32, aoa: f32) -> f32 {
358    let aoa_abs = aoa.abs();
359    let stall_angle = PI * 0.1;
360    if aoa_abs < stall_angle {
361        lift_slope(aspect_ratio, None) * aoa
362    } else {
363        // This is when flow separation and turbulence starts to kick in.
364        // Going to just make something up (based on some data), as the alternative is
365        // to just throw your hands up and return 0
366        let aoa_s = aoa.signum();
367        let c_l_max = lift_slope(aspect_ratio, None) * stall_angle;
368        let deg_45 = PI / 4.0;
369        if aoa_abs < deg_45 {
370            // drop directly to 0.6 * max lift at stall angle
371            // then climb back to max at 45°
372            Lerp::lerp(0.6 * c_l_max, c_l_max, aoa_abs / deg_45) * aoa_s
373        } else {
374            // let's just say lift goes down linearly again until we're at 90°
375            Lerp::lerp(c_l_max, 0.0, (aoa_abs - deg_45) / deg_45) * aoa_s
376        }
377    }
378}
379
380/// The zero-lift profile drag coefficient is the parasite drag on the wings
381/// at the angle of attack which generates no lift
382pub fn zero_lift_drag_coefficient() -> f32 { 0.026 }
383
384/// The change in lift over change in angle of attack¹. Multiplying by angle
385/// of attack gives the lift coefficient (for a finite wing, not aerofoil).
386/// Aspect ratio is the ratio of total wing span squared over planform area.
387///
388/// # Notes
389/// Only valid for symmetric, elliptical wings at small² angles of attack³.
390/// Does not apply to twisted, cambered or delta wings. (It still gives a
391/// reasonably accurate approximation if the wing shape is not truly
392/// elliptical.)
393///   1. geometric angle of attack, i.e. the pitch angle relative to freestream
394///      flow
395///   2. up to around ~18°, at which point maximum lift has been achieved and
396///      thereafter falls precipitously, causing a stall (this is the stall
397///      angle)
398///   3. effective aoa, i.e. geometric aoa - induced aoa; assumes no sideslip
399// TODO: Look into handling tapered wings
400fn lift_slope(aspect_ratio: f32, sweep_angle: Option<f32>) -> f32 {
401    // lift slope for a thin aerofoil, given by Thin Aerofoil Theory
402    let a0 = 2.0 * PI;
403    if let Some(sweep) = sweep_angle {
404        // for swept wings we use Kuchemann's modification to Helmbold's
405        // equation
406        let a0_cos_sweep = a0 * sweep.cos();
407        let x = a0_cos_sweep / (PI * aspect_ratio);
408        a0_cos_sweep / ((1.0 + x.powi(2)).sqrt() + x)
409    } else if aspect_ratio < 4.0 {
410        // for low aspect ratio wings (AR < 4) we use Helmbold's equation
411        let x = a0 / (PI * aspect_ratio);
412        a0 / ((1.0 + x.powi(2)).sqrt() + x)
413    } else {
414        // for high aspect ratio wings (AR > 4) we use the equation given by
415        // Prandtl's lifting-line theory
416        a0 / (1.0 + (a0 / (PI * aspect_ratio)))
417    }
418}