banished_core/math.rs
1//! Engine math helpers (`Runtime-steam-x64.dll` `Math::Scalar`, `Math::Vector2/3`) with the
2//! original's precision: the C runtime's `sinf`/`cosf`/`atan2f` ([`crate::crt`]), `sqrtf`
3//! (correctly rounded), and the operations in the original's order.
4
5// DAT_1801647ec
6/// Degrees to radians as an f32.
7pub const DEG_TO_RAD: f32 = 0.017453292;
8// DAT_180164878
9/// Radians to degrees.
10pub const RAD_TO_DEG: f32 = 57.29578;
11
12/// The sine of `x` (radians), with the C runtime's rounding ([`crate::crt::sinf`]).
13///
14/// Original: `FUN_140137160`.
15pub fn sinf(x: f32) -> f32 {
16 crate::crt::sinf(x)
17}
18
19/// The cosine of `x` (radians), with the C runtime's rounding ([`crate::crt::cosf`]).
20///
21/// Original: `FUN_140136560`.
22pub fn cosf(x: f32) -> f32 {
23 crate::crt::cosf(x)
24}
25
26/// The length of a 3D vector: `sqrtf((x·x + y·y) + z·z)`, added in that order.
27///
28/// Original: `Math::Vector3::Length`, Runtime `0x180022280`.
29pub fn length3(v: [f32; 3]) -> f32 {
30 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
31}
32
33/// A 3D vector scaled to length 1 (times `1 / length`), and its length. A zero vector is returned
34/// as it is.
35///
36/// Original: `Math::Vector3::Normalize`, Runtime `0x180022300`.
37pub fn normalize3_len(v: [f32; 3]) -> ([f32; 3], f32) {
38 let l = length3(v);
39 if l == 0.0 {
40 return (v, l);
41 }
42 let inv = 1.0 / l;
43 ([v[0] * inv, v[1] * inv, v[2] * inv], l)
44}
45
46/// A 3D vector scaled to length 1. A zero vector is returned as it is.
47pub fn normalize3(v: [f32; 3]) -> [f32; 3] {
48 normalize3_len(v).0
49}
50
51/// A 2D vector scaled to length 1. A zero vector is returned as it is.
52///
53/// Original: `Math::Vector2::Normalize`, Runtime `0x18006d0d0`.
54pub fn normalize2(v: [f32; 2]) -> [f32; 2] {
55 let l = (v[0] * v[0] + v[1] * v[1]).sqrt();
56 if l == 0.0 {
57 return v;
58 }
59 let inv = 1.0 / l;
60 [v[0] * inv, v[1] * inv]
61}
62
63/// The direction of a yaw and a pitch, in degrees: `(−cos p · sin y, cos p · cos y, sin p)`.
64///
65/// Original: `Math::Vector3::FromYawPitch`, Runtime `0x180022990`.
66pub fn from_yaw_pitch(yaw: f32, pitch: f32) -> [f32; 3] {
67 let (ry, rp) = (yaw * DEG_TO_RAD, pitch * DEG_TO_RAD);
68 let (sy, sp, cy, cp) = (sinf(ry), sinf(rp), cosf(ry), cosf(rp));
69 [-cp * sy, cp * cy, sp]
70}
71
72/// The heading of a 2D direction, in degrees: `atan2f(−x, y)`.
73///
74/// Original: `Math::Vector3::Heading`, Runtime `0x18007d3d0`.
75pub fn heading(x: f32, y: f32) -> f32 {
76 crate::crt::atan2f(-x, y) * RAD_TO_DEG
77}
78
79/// The sine and cosine of an angle in degrees.
80///
81/// Original: `Math::Scalar::SinCos`.
82pub fn sin_cos_deg(degrees: f32) -> (f32, f32) {
83 let r = degrees * DEG_TO_RAD;
84 (sinf(r), cosf(r))
85}
86
87// The entity transform: ModelComponent +0x148 (rotation), +0x158 (offset).
88/// The engine's transform: a rotation (a quaternion) and an offset. A placed model keeps its
89/// position this way.
90///
91/// Original: `Math::Transform`.
92#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct Transform {
94 /// The rotation, a unit quaternion `(x, y, z, w)`.
95 pub q: [f32; 4],
96 /// The offset, applied after the rotation.
97 pub p: [f32; 3],
98}
99
100impl Default for Transform {
101 /// The identity.
102 fn default() -> Transform {
103 Transform {
104 q: [0.0, 0.0, 0.0, 1.0],
105 p: [0.0; 3],
106 }
107 }
108}
109
110/// The product of two quaternions, its terms in the original's order.
111///
112/// Original: Runtime `0x18001eb60`.
113fn qmul(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
114 let [ax, ay, az, aw] = a;
115 let [bx, by, bz, bw] = b;
116 [
117 ((ax * bw + bx * aw) + az * by) - ay * bz,
118 ((ay * bw - az * bx) + by * aw) + bz * ax,
119 ((az * bw + ay * bx) - by * ax) + bz * aw,
120 ((bw * aw - ax * bx) - ay * by) - bz * az,
121 ]
122}
123
124impl Transform {
125 /// A transform from a yaw, a pitch (degrees) and an offset. The rotation uses the sines and
126 /// cosines of the half angles: `q = (−cy·sp, −sy·sp, −cp·sy, cp·cy)`.
127 ///
128 /// Original: `Math::Transform::FromYawPitchOffset`, Runtime `0x18007e540`.
129 pub fn from_yaw_pitch_offset(yaw: f32, pitch: f32, offset: [f32; 3]) -> Transform {
130 let hy = (yaw * 0.5) * DEG_TO_RAD;
131 let hp = (pitch * 0.5) * DEG_TO_RAD;
132 let (sy, sp, cy, cp) = (sinf(hy), sinf(hp), cosf(hy), cosf(hp));
133 Transform {
134 q: [-(cy * sp), -(sy * sp), -(cp * sy), cp * cy],
135 p: offset,
136 }
137 }
138
139 /// The rotation a CharacterComponent's update writes: `(+0, +0, −sinf(h), cosf(h))` with `h =
140 /// (yaw × DEG_TO_RAD) × 0.5`, rounded in that order (a model's `FromYawPitchOffset` halves
141 /// first, and its zeros are negative).
142 ///
143 /// Original: `FUN_14010e990`.
144 pub fn character_rotation(yaw: f32) -> [f32; 4] {
145 let h = (yaw * DEG_TO_RAD) * 0.5;
146 [0.0, 0.0, -sinf(h), cosf(h)]
147 }
148
149 /// A placed entity's model transform: its yaw, pitch 0, its position.
150 pub fn of_model(yaw: f32, position: [f32; 3]) -> Transform {
151 Transform::from_yaw_pitch_offset(yaw, 0.0, position)
152 }
153
154 /// `v` turned by the rotation: `q · (v, 0) · q̄`.
155 ///
156 /// Original: `Math::Quaternion::Rotate`, Runtime `0x18001ea20`.
157 pub fn rotate(&self, v: [f32; 3]) -> [f32; 3] {
158 let q = self.q;
159 let conj = [-q[0], -q[1], -q[2], q[3]];
160 let t = qmul(q, [v[0], v[1], v[2], 0.0]);
161 let r = qmul(t, conj);
162 [r[0], r[1], r[2]]
163 }
164
165 /// A point moved by the transform: rotated, then offset.
166 ///
167 /// Original: `Math::Transform::operator*(Vector3)`, Runtime `0x18001eae0`.
168 pub fn apply(&self, v: [f32; 3]) -> [f32; 3] {
169 let r = self.rotate(v);
170 [r[0] + self.p[0], self.p[1] + r[1], r[2] + self.p[2]]
171 }
172
173 /// The inverse transform: the conjugate rotation, and the negated offset turned by it.
174 ///
175 /// Original: `Math::Transform::Inverse`, Runtime `0x18005e620`.
176 pub fn inverse(&self) -> Transform {
177 let q = self.q;
178 let conj = Transform {
179 q: [-q[0], -q[1], -q[2], q[3]],
180 p: [0.0; 3],
181 };
182 Transform {
183 q: conj.q,
184 p: conj.rotate([-self.p[0], -self.p[1], -self.p[2]]),
185 }
186 }
187
188 /// A heading in the model's own space, turned into the world: the heading of
189 /// `from_yaw_pitch(local_yaw, 0)` after the rotation.
190 ///
191 /// Original: `FUN_140128d50`.
192 pub fn heading_of(&self, local_yaw: f32) -> f32 {
193 let v = self.rotate(from_yaw_pitch(local_yaw, 0.0));
194 heading(v[0], v[1])
195 }
196}