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