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}