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}