├── LICENSE ├── README.md └── maf.lua /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Bjorn Swenson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | maf 2 | === 3 | 4 | A small 3D math library for Lua. 5 | 6 | **Features** 7 | 8 | - Single file 9 | - Uses optimized LuaJIT structs if available 10 | - Supports vectors and quaternions 11 | - Provides an ugly API that is garbage-free as well as a pretty API that generates garbage. 12 | 13 | Check out [cpml](https://github.com/excessive/cpml) if you're looking for something with more features. 14 | 15 | Example 16 | --- 17 | 18 | ```lua 19 | local maf = require 'maf' 20 | 21 | local a = maf.vector(1, 2, 3) 22 | local b = maf.vector(4, 5, 6) 23 | 24 | local c = a + b 25 | local d = maf.rotation.between(a, b) 26 | 27 | print(c:unpack()) 28 | print(d:getAngleAxis()) 29 | ``` 30 | 31 | Documentation 32 | --- 33 | 34 | In general, any function that returns a vector or a rotation will write its result into the last 35 | parameter, `out`. The function also returns `out` for convenience, to allow for method chaining. 36 | If `out` is unspecified, then the result will be written into the first parameter instead. 37 | 38 | ### Vectors 39 | 40 | ##### `maf.vector(x, y, z)` or `maf.vec3(x, y, z)` 41 | 42 | Create a new vector. Omitted elements will be set to zero. 43 | 44 | The `x`, `y`, and `z` keys contain the individual components of the vector. 45 | 46 | The following metamethods are defined for vectors: 47 | 48 | - `v + v` - Add two vectors. 49 | - `v - v` - Subtract two vectors. 50 | - `v * v` - Multiply two vectors. 51 | - `v * n` - Scale a vector by a number. 52 | - `v / v` - Divide two vectors. 53 | - `-v` - Negate a vector. 54 | - `#v` - Get the length of a vector. 55 | 56 | ##### `vector:unpack()` 57 | 58 | Get the individual components of the vector. 59 | 60 | ##### `vector:set(x, y, z)` 61 | 62 | Set the individual components of a vector. `x` can also be a vector. 63 | 64 | ##### `vector:add(v2, [out])` 65 | 66 | Add two vectors. 67 | 68 | ##### `vector:sub(v2, [out])` 69 | 70 | Subtract two vectors. 71 | 72 | ##### `vector:mul(v2, [out])` 73 | 74 | Multiply two vectors. 75 | 76 | ##### `vector:div(v2, [out])` 77 | 78 | Divide two vectors. 79 | 80 | ##### `vector:scale(s, [out])` 81 | 82 | Multiply each component of the vector by a number `s`. 83 | 84 | ##### `vector:length()` 85 | 86 | Return the length of the vector. 87 | 88 | ##### `vector:normalize([out])` 89 | 90 | Set the length of a vector to 1. 91 | 92 | ##### `vector:distance(v2)` 93 | 94 | Return the distance between two vectors. 95 | 96 | ##### `vector:angle(v2)` 97 | 98 | Return the angle between two vectors, in radians. 99 | 100 | ##### `vector:dot(v2)` 101 | 102 | Return the dot product of two vectors, defined as `v1.x * v2.x + v1.y * v2.y + v1.z * v2.z`. 103 | 104 | ##### `vector:cross(v2, [out])` 105 | 106 | Return the cross product of two vectors. The cross product is a vector that is perpendicular to the 107 | two input vectors. 108 | 109 | ##### `vector:lerp(v2, t, [out])` 110 | 111 | Mixes two vectors together using linear interpolation. The parameter `t` controls how they are 112 | mixed (0 will return `vector`, 1 will return `v2`, .5 will return a 50/50 blend of the two vectors, 113 | etc.). 114 | 115 | ##### `vector:project(u, [out])` 116 | 117 | Projects `vector` onto `u`, storing the result in `out`. 118 | 119 | ##### `vector:rotate(r, [out])` 120 | 121 | Applies a rotation `r` to the vector. 122 | 123 | ### Rotations 124 | 125 | ##### `maf.rotation(x, y, z, w)` or `maf.quat(x, y, z, w)` 126 | 127 | Creates a new rotation (quaternion) from x, y, z, w components. Unless you are some sort of wizard 128 | that understands quaternions, it's probably easier to use `.fromAngleAxis`, `.fromDirection`, or 129 | `.between`. 130 | 131 | The following metamethods are defined for rotations: 132 | 133 | - `q + q` - Add two rotations. 134 | - `q - q` - Subtract two rotations. 135 | - `q * q` - Multiply two rotations, resulting in a rotation that is equivalent to applying the first 136 | rotation followed by the second. 137 | - `q * v` - Multiply a rotation by a vector, returning the rotated vector. 138 | - `-q` - Negate a rotation. 139 | - `#q` - Get the length of a rotation. 140 | 141 | ##### `rotation:unpack()` 142 | 143 | Get the individual components of the rotation. 144 | 145 | ##### `rotation:set(x, y, z, w)` 146 | 147 | Set the individual components of a rotation. `x` can also be a rotation. 148 | 149 | ##### `rotation.fromAngleAxis(angle, x, y, z)` 150 | 151 | Creates a new rotation from an angle/axis pair. This is the same as creating a new rotation and 152 | calling `:setAngleAxis` on it. `x` can also be a `vec3`. 153 | 154 | ##### `rotation:setAngleAxis(angle, x, y, z)` 155 | 156 | Set the rotation's values using angle/axis representation. `x` can also be a `vec3`. 157 | 158 | ##### `angle, x, y, z = rotation:getAngleAxis()` 159 | 160 | Get the angle/axis representation of the rotation. 161 | 162 | ##### `rotation.between(v1, v2)` 163 | 164 | Create a new rotation that represents the rotation between `v1` and `v2`. Both vectors should be 165 | normalized. 166 | 167 | ##### `rotation:setBetween(v1, v2)` 168 | 169 | Update an existing rotation to represent the rotation between `v1` and `v2`. Both vectors should be 170 | normalized. 171 | 172 | ##### `rotation.fromDirection(x, y, z)` 173 | 174 | Create a new rotation from a direction vector. The rotation will represent the rotation from the 175 | forward vector (0, 0, -1) to the direction vector. x can also be a `vec3`. 176 | 177 | ##### `rotation:setDirection(x, y, z)` 178 | 179 | Set a rotation's value to those of the direction vector. `x` can also be a `vec3`. 180 | 181 | ##### `rotation:add(r2, [out])` 182 | 183 | Add two rotations together. To combine two rotations together, multiply them instead. 184 | 185 | ##### `rotation:sub(r2, [out])` 186 | 187 | Subtract two rotations. 188 | 189 | ##### `rotation:mul(r2, [out])` 190 | 191 | Multiply two rotations together, returning a rotation that combines the two. 192 | 193 | ##### `rotation:scale(s, [out])` 194 | 195 | Multiply each component of the rotation by a number `s`. 196 | 197 | ##### `rotation:length()` 198 | 199 | Return the length of the rotation. 200 | 201 | ##### `rotation:normalize([out])` 202 | 203 | Normalize a rotation, setting its length to 1. 204 | 205 | ##### `rotation:lerp(r2, t, [out])` 206 | 207 | Mix two rotations together using linear interpolation. This is faster than `:slerp` but less 208 | accurate. 209 | 210 | ##### `rotation:slerp(r2, t, [out])` 211 | 212 | Mix two rotations together using spherical linear interpolation. 213 | 214 | Garbage Collection Is Scary 215 | --- 216 | 217 | Keep in mind that each time a new vector or rotation is created, a small memory allocation is made. 218 | Normally you don't need to worry about this, since Lua has a garbage collector that will clean up 219 | after you. However, allocating a large number of objects can put stress on the garbage collector, 220 | which can lead to performance problems. This can happen more often that you'd think if you're, for 221 | example, creating new vectors in the update loop of a game. 222 | 223 | It gets worse. Although it's convenient to be able to write `a + b` to add vectors together, a new 224 | vector must be allocated to store the result. This makes it really easy to allocate a bunch of 225 | vectors without even knowing it! To avoid this sneaky performance trap, you can add two vectors 226 | using `a:add(b)`, which will reuse `a` to store the result. Another strategy is to allocate a set 227 | of reusable temporary vectors and use `a:add(b, tmp)`. 228 | 229 | License 230 | --- 231 | 232 | MIT, see [`LICENSE`](LICENSE) for details. 233 | -------------------------------------------------------------------------------- /maf.lua: -------------------------------------------------------------------------------- 1 | -- maf 2 | -- https://github.com/bjornbytes/maf 3 | -- MIT License 4 | 5 | local ffi = type(jit) == 'table' and jit.status() and require 'ffi' 6 | local vec3, quat 7 | 8 | local forward 9 | local vtmp1 10 | local vtmp2 11 | local qtmp1 12 | 13 | vec3 = { 14 | __call = function(_, x, y, z) 15 | return setmetatable({ x = x or 0, y = y or 0, z = z or 0 }, vec3) 16 | end, 17 | 18 | __tostring = function(v) 19 | return string.format('(%f, %f, %f)', v.x, v.y, v.z) 20 | end, 21 | 22 | __add = function(v, u) return v:add(u, vec3()) end, 23 | __sub = function(v, u) return v:sub(u, vec3()) end, 24 | __mul = function(v, u) 25 | if vec3.isvec3(u) then return v:mul(u, vec3()) 26 | elseif type(u) == 'number' then return v:scale(u, vec3()) 27 | else error('vec3s can only be multiplied by vec3s and numbers') end 28 | end, 29 | __div = function(v, u) 30 | if vec3.isvec3(u) then return v:div(u, vec3()) 31 | elseif type(u) == 'number' then return v:scale(1 / u, vec3()) 32 | else error('vec3s can only be divided by vec3s and numbers') end 33 | end, 34 | __unm = function(v) return v:scale(-1) end, 35 | __len = function(v) return v:length() end, 36 | 37 | __index = { 38 | isvec3 = function(x) 39 | return ffi and ffi.istype('vec3', x) or getmetatable(x) == vec3 40 | end, 41 | 42 | clone = function(v) 43 | return vec3(v.x, v.y, v.z) 44 | end, 45 | 46 | unpack = function(v) 47 | return v.x, v.y, v.z 48 | end, 49 | 50 | set = function(v, x, y, z) 51 | if vec3.isvec3(x) then x, y, z = x.x, x.y, x.z end 52 | v.x = x 53 | v.y = y 54 | v.z = z 55 | return v 56 | end, 57 | 58 | add = function(v, u, out) 59 | out = out or v 60 | out.x = v.x + u.x 61 | out.y = v.y + u.y 62 | out.z = v.z + u.z 63 | return out 64 | end, 65 | 66 | sub = function(v, u, out) 67 | out = out or v 68 | out.x = v.x - u.x 69 | out.y = v.y - u.y 70 | out.z = v.z - u.z 71 | return out 72 | end, 73 | 74 | mul = function(v, u, out) 75 | out = out or v 76 | out.x = v.x * u.x 77 | out.y = v.y * u.y 78 | out.z = v.z * u.z 79 | return out 80 | end, 81 | 82 | div = function(v, u, out) 83 | out = out or v 84 | out.x = v.x / u.x 85 | out.y = v.y / u.y 86 | out.z = v.z / u.z 87 | return out 88 | end, 89 | 90 | scale = function(v, s, out) 91 | out = out or v 92 | out.x = v.x * s 93 | out.y = v.y * s 94 | out.z = v.z * s 95 | return out 96 | end, 97 | 98 | length = function(v) 99 | return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) 100 | end, 101 | 102 | normalize = function(v, out) 103 | out = out or v 104 | local len = v:length() 105 | return len == 0 and v or v:scale(1 / len, out) 106 | end, 107 | 108 | distance = function(v, u) 109 | return vec3.sub(v, u, vtmp1):length() 110 | end, 111 | 112 | angle = function(v, u) 113 | return math.acos(v:dot(u) / (v:length() + u:length())) 114 | end, 115 | 116 | dot = function(v, u) 117 | return v.x * u.x + v.y * u.y + v.z * u.z 118 | end, 119 | 120 | cross = function(v, u, out) 121 | out = out or v 122 | local a, b, c = v.x, v.y, v.z 123 | out.x = b * u.z - c * u.y 124 | out.y = c * u.x - a * u.z 125 | out.z = a * u.y - b * u.x 126 | return out 127 | end, 128 | 129 | lerp = function(v, u, t, out) 130 | out = out or v 131 | out.x = v.x + (u.x - v.x) * t 132 | out.y = v.y + (u.y - v.y) * t 133 | out.z = v.z + (u.z - v.z) * t 134 | return out 135 | end, 136 | 137 | project = function(v, u, out) 138 | out = out or v 139 | local unorm = vtmp1 140 | u:normalize(unorm) 141 | local dot = v:dot(unorm) 142 | out.x = unorm.x * dot 143 | out.y = unorm.y * dot 144 | out.z = unorm.z * dot 145 | return out 146 | end, 147 | 148 | rotate = function(v, q, out) 149 | out = out or v 150 | local u, c, o = vtmp1, vtmp2, out 151 | u.x, u.y, u.z = q.x, q.y, q.z 152 | o.x, o.y, o.z = v.x, v.y, v.z 153 | u:cross(v, c) 154 | local uu = u:dot(u) 155 | local uv = u:dot(v) 156 | o:scale(q.w * q.w - uu) 157 | u:scale(2 * uv) 158 | c:scale(2 * q.w) 159 | return o:add(u:add(c)) 160 | end 161 | } 162 | } 163 | 164 | quat = { 165 | __call = function(_, x, y, z, w) 166 | return setmetatable({ x = x, y = y, z = z, w = w }, quat) 167 | end, 168 | 169 | __tostring = function(q) 170 | return string.format('(%f, %f, %f, %f)', q.x, q.y, q.z, q.w) 171 | end, 172 | 173 | __add = function(q, r) return q:add(r, quat()) end, 174 | __sub = function(q, r) return q:sub(r, quat()) end, 175 | __mul = function(q, r) 176 | if quat.isquat(r) then return q:mul(r, quat()) 177 | elseif vec3.isvec3(r) then return r:rotate(q, vec3()) 178 | else error('quats can only be multiplied by quats and vec3s') end 179 | end, 180 | __unm = function(q) return q:scale(-1) end, 181 | __len = function(q) return q:length() end, 182 | 183 | __index = { 184 | isquat = function(x) 185 | return ffi and ffi.istype('quat', x) or getmetatable(x) == quat 186 | end, 187 | 188 | clone = function(q) 189 | return quat(q.x, q.y, q.z, q.w) 190 | end, 191 | 192 | unpack = function(q) 193 | return q.x, q.y, q.z, q.w 194 | end, 195 | 196 | set = function(q, x, y, z, w) 197 | if quat.isquat(x) then x, y, z, w = x.x, x.y, x.z, x.w end 198 | q.x = x 199 | q.y = y 200 | q.z = z 201 | q.w = w 202 | return q 203 | end, 204 | 205 | fromAngleAxis = function(angle, x, y, z) 206 | return quat():setAngleAxis(angle, x, y, z) 207 | end, 208 | 209 | setAngleAxis = function(q, angle, x, y, z) 210 | if vec3.isvec3(x) then x, y, z = x.x, x.y, x.z end 211 | local s = math.sin(angle * .5) 212 | local c = math.cos(angle * .5) 213 | q.x = x * s 214 | q.y = y * s 215 | q.z = z * s 216 | q.w = c 217 | return q 218 | end, 219 | 220 | getAngleAxis = function(q) 221 | if q.w > 1 or q.w < -1 then q:normalize() end 222 | local s = math.sqrt(1 - q.w * q.w) 223 | s = s < .0001 and 1 or 1 / s 224 | return 2 * math.acos(q.w), q.x * s, q.y * s, q.z * s 225 | end, 226 | 227 | between = function(u, v) 228 | return quat():setBetween(u, v) 229 | end, 230 | 231 | setBetween = function(q, u, v) 232 | local dot = u:dot(v) 233 | if dot > .99999 then 234 | q.x, q.y, q.z, q.w = 0, 0, 0, 1 235 | return q 236 | elseif dot < -.99999 then 237 | vtmp1.x, vtmp1.y, vtmp1.z = 1, 0, 0 238 | vtmp1:cross(u) 239 | if #vtmp1 < .00001 then 240 | vtmp1.x, vtmp1.y, vtmp1.z = 0, 1, 0 241 | vtmp1:cross(u) 242 | end 243 | vtmp1:normalize() 244 | return q:setAngleAxis(math.pi, vtmp1) 245 | end 246 | 247 | q.x, q.y, q.z = u.x, u.y, u.z 248 | vec3.cross(q, v) 249 | q.w = 1 + dot 250 | return q:normalize() 251 | end, 252 | 253 | fromDirection = function(x, y, z) 254 | return quat():setDirection(x, y, z) 255 | end, 256 | 257 | setDirection = function(q, x, y, z) 258 | if vec3.isvec3(x) then x, y, z = x.x, x.y, x.z end 259 | vtmp2.x, vtmp2.y, vtmp2.z = x, y, z 260 | return q:setBetween(forward, vtmp2) 261 | end, 262 | 263 | add = function(q, r, out) 264 | out = out or q 265 | out.x = q.x + r.x 266 | out.y = q.y + r.y 267 | out.z = q.z + r.z 268 | out.w = q.w + r.w 269 | return out 270 | end, 271 | 272 | sub = function(q, r, out) 273 | out = out or q 274 | out.x = q.x - r.x 275 | out.y = q.y - r.y 276 | out.z = q.z - r.z 277 | out.w = q.w - r.w 278 | return out 279 | end, 280 | 281 | mul = function(q, r, out) 282 | out = out or q 283 | local qx, qy, qz, qw = q:unpack() 284 | local rx, ry, rz, rw = r:unpack() 285 | out.x = qx * rw + qw * rx + qy * rz - qz * ry 286 | out.y = qy * rw + qw * ry + qz * rx - qx * rz 287 | out.z = qz * rw + qw * rz + qx * ry - qy * rx 288 | out.w = qw * rw - qx * rx - qy * ry - qz * rz 289 | return out 290 | end, 291 | 292 | scale = function(q, s, out) 293 | out = out or q 294 | out.x = q.x * s 295 | out.y = q.y * s 296 | out.z = q.z * s 297 | out.w = q.w * s 298 | return out 299 | end, 300 | 301 | length = function(q) 302 | return math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w) 303 | end, 304 | 305 | normalize = function(q, out) 306 | out = out or q 307 | local len = q:length() 308 | return len == 0 and q or q:scale(1 / len, out) 309 | end, 310 | 311 | lerp = function(q, r, t, out) 312 | out = out or q 313 | r:scale(t, qtmp1) 314 | q:scale(1 - t, out) 315 | return out:add(qtmp1) 316 | end, 317 | 318 | slerp = function(q, r, t, out) 319 | out = out or q 320 | 321 | local dot = q.x * r.x + q.y * r.y + q.z * r.z + q.w * r.w 322 | if dot < 0 then 323 | dot = -dot 324 | r:scale(-1) 325 | end 326 | 327 | if 1 - dot < .0001 then 328 | return q:lerp(r, t, out) 329 | end 330 | 331 | local theta = math.acos(dot) 332 | q:scale(math.sin((1 - t) * theta), out) 333 | r:scale(math.sin(t * theta), qtmp1) 334 | return out:add(qtmp1):scale(1 / math.sin(theta)) 335 | end 336 | } 337 | } 338 | 339 | if ffi then 340 | ffi.cdef [[ 341 | typedef struct { double x, y, z; } vec3; 342 | typedef struct { double x, y, z, w; } quat; 343 | ]] 344 | 345 | vec3 = ffi.metatype('vec3', vec3) 346 | quat = ffi.metatype('quat', quat) 347 | else 348 | setmetatable(vec3, vec3) 349 | setmetatable(quat, quat) 350 | end 351 | 352 | forward = vec3(0, 0, -1) 353 | vtmp1 = vec3() 354 | vtmp2 = vec3() 355 | qtmp1 = quat() 356 | 357 | return { 358 | vec3 = vec3, 359 | quat = quat, 360 | 361 | vector = vec3, 362 | rotation = quat 363 | } 364 | --------------------------------------------------------------------------------