├── .coveralls.yml ├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── runtest.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── cpml-scm-1.rockspec ├── doc ├── config.ld └── install_and_build_docs ├── init.lua ├── modules ├── _private_precond.lua ├── _private_utils.lua ├── bound2.lua ├── bound3.lua ├── bvh.lua ├── color.lua ├── constants.lua ├── intersect.lua ├── mat4.lua ├── mesh.lua ├── octree.lua ├── quat.lua ├── simplex.lua ├── utils.lua ├── vec2.lua └── vec3.lua └── spec ├── bound2_spec.lua ├── bound3_spec.lua ├── color_spec.lua ├── intersect_spec.lua ├── mat4_spec.lua ├── mesh_spec.lua ├── octree_spec.lua ├── quat_spec.lua ├── utils_spec.lua ├── vec2_spec.lua └── vec3_spec.lua /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: WcsY9jsU97Zt0ZIbGHJftGkC8DsD16FVl -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.lua] 8 | indent_style = tab 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Based on https://gist.github.com/domenic/ec8b0fc8ab45f39403dd 2 | name: Documentation 3 | 4 | on: 5 | pull_request: # Build on pull requests to ensure they don't break docs. 6 | branches: 7 | - master 8 | push: # We'll only push new docs when master is updated (see below). 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build: 14 | name: Build Docs 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Setup Lua 19 | uses: leafo/gh-actions-lua@v8 20 | with: 21 | luaVersion: 5.4 22 | - name: Setup Lua Rocks 23 | uses: leafo/gh-actions-luarocks@v4 24 | - name: Setup and run ldoc 25 | run: bash ./doc/install_and_build_docs 26 | - name: Deploy 27 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} 28 | uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./doc/out 32 | -------------------------------------------------------------------------------- /.github/workflows/runtest.yml: -------------------------------------------------------------------------------- 1 | name: Validate Code 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - refactor 8 | push: 9 | branches: 10 | - master 11 | - refactor 12 | 13 | jobs: 14 | test: 15 | name: Run Tests 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | luaVersion: ["5.1.5", "luajit-2.0.5", "luajit-2.1.0-beta3"] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Setup Lua 25 | uses: leafo/gh-actions-lua@v8.0.0 26 | with: 27 | luaVersion: ${{ matrix.luaVersion }} 28 | - name: Setup Lua Rocks 29 | uses: leafo/gh-actions-luarocks@v4 30 | - name: Install dependencies 31 | run: | 32 | luarocks --local install busted 33 | luarocks --local install luacov 34 | luarocks --local install luacov-coveralls 35 | - name: Run busted 36 | run: ~/.luarocks/bin/busted --verbose --coverage spec 37 | - name: Upload coverage 38 | continue-on-error: true # don't know why coveralls isn't uploading. For now, let this fail. 39 | run: | 40 | # ignore dotfile directories created by lua setup 41 | ~/.luarocks/bin/luacov-coveralls --exclude '^%.%a+$' --repo-token WcsY9jsU97Zt0ZIbGHJftGkC8DsD16FVl 42 | # - name: Run luacheck 43 | # run: luacheck --std max+busted *.lua spec 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # LDoc generated files. 2 | doc/out 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Licenses 2 | 3 | CPML is Copyright (c) 2016 Colby Klein . 4 | 5 | CPML is Copyright (c) 2016 Landon Manning . 6 | 7 | Code in vec3.lua is derived from hump.vector. (c) 2010-2013 Matthias Richter. MIT. 8 | 9 | Portions of mat4.lua are from LuaMatrix, (c) 2010 Michael Lutz. MIT. 10 | 11 | Code in simplex.lua is (c) 2011 Stefan Gustavson. MIT. 12 | 13 | Code in bound2.lua and bound3.lua are (c) 2018 Andi McClure. MIT. 14 | 15 | Code in quat.lua is from Andrew Stacey and covered under the CC0 license. 16 | 17 | Code in octree.lua is derived from UnityOctree. (c) 2014 Nition. BSD-2-Clause. 18 | 19 | # The MIT License (MIT) 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | # The BSD License (BSD-2-Clause) 40 | 41 | Redistribution and use in source and binary forms, with or without 42 | modification, are permitted provided that the following conditions are met: 43 | 44 | * Redistributions of source code must retain the above copyright notice, this 45 | list of conditions and the following disclaimer. 46 | 47 | * Redistributions in binary form must reproduce the above copyright notice, 48 | this list of conditions and the following disclaimer in the documentation 49 | and/or other materials provided with the distribution. 50 | 51 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 52 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 53 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 54 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 55 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 56 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 57 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 58 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 59 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 60 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cirno's Perfect Math Library 2 | ==== 3 | 4 | ![Build Status](https://github.com/excessive/cpml/actions/workflows/runtest.yml/badge.svg) 5 | [![Coverage Status](https://coveralls.io/repos/github/excessive/cpml/badge.svg?branch=master)](https://coveralls.io/github/excessive/cpml?branch=master) 6 | 7 | Various useful bits of game math. 3D line intersections, ray casting, 2d/3d vectors, 4x4 matrices, quaternions, etc. 8 | 9 | Intended to be used with LuaJIT and LÖVE (this is the backbone of LÖVE3D). 10 | 11 | Online documentation can be found [here](http://excessive.github.io/cpml/) or you can generate them yourself using `ldoc -c doc/config.ld -o index .` 12 | 13 | # Installation 14 | Clone the repository and require it, or if you prefer luarocks: `$ luarocks install --server=http://luarocks.org/dev cpml`. Add `--tree=whatever` for a local install. 15 | 16 | # Versions 17 | 18 | This library has a major compatibility break at version 1.0. Up to version 0.10, composition `a*b` means "apply b, then a" for quaternions and "apply a, then b" for matrices. Now as of version 1.0, the two are consistent and matrix `a*b` means "apply b, then a". 19 | -------------------------------------------------------------------------------- /cpml-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "cpml" 2 | version = "scm-1" 3 | source = { 4 | url = "git://github.com/excessive/cpml.git" 5 | } 6 | description = { 7 | summary = "Cirno's Perfect Math Library", 8 | detailed = "Various useful bits of game math. 3D line intersections, ray casting, vectors, matrices, quaternions, etc.", 9 | homepage = "http://github.com/excessive/cpml.git", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua ~> 5.1" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["cpml"] = "init.lua", 19 | ["cpml.modules.color"] = "modules/color.lua", 20 | ["cpml.modules.constants"] = "modules/constants.lua", 21 | ["cpml.modules.intersect"] = "modules/intersect.lua", 22 | ["cpml.modules.mat4"] = "modules/mat4.lua", 23 | ["cpml.modules.mesh"] = "modules/mesh.lua", 24 | ["cpml.modules.octree"] = "modules/octree.lua", 25 | ["cpml.modules.quat"] = "modules/quat.lua", 26 | ["cpml.modules.simplex"] = "modules/simplex.lua", 27 | ["cpml.modules.utils"] = "modules/utils.lua", 28 | ["cpml.modules.vec2"] = "modules/vec2.lua", 29 | ["cpml.modules.vec3"] = "modules/vec3.lua", 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /doc/config.ld: -------------------------------------------------------------------------------- 1 | project="CPML" 2 | title="CPML documentation" 3 | description="A math library with (hopefully) everything you need for 2D/3D games" 4 | format="markdown" 5 | backtick_references=false 6 | file = { 7 | "../init.lua", 8 | "../modules" 9 | } 10 | dir='./out' 11 | readme='../README.md' 12 | style='!new' 13 | -------------------------------------------------------------------------------- /doc/install_and_build_docs: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # on github, leafo/gh-actions-lua leafo/gh-actions-luarocks setup luarocks for us. 4 | #~ sudo apt-get install lua5.3 liblua5.3-dev luarocks 5 | 6 | # github ldoc is far ahead of the released version. 7 | echo ldoc version: 8 | git ls-remote https://github.com/lunarmodules/LDoc master 9 | luarocks --local install https://raw.githubusercontent.com/lunarmodules/LDoc/master/ldoc-scm-3.rockspec 10 | 11 | echo 12 | cd ./doc 13 | ~/.luarocks/bin/ldoc . 14 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ------------------------------------------------------------------------------- 3 | -- @author Colby Klein 4 | -- @author Landon Manning 5 | -- @copyright 2016 6 | -- @license MIT/X11 7 | ------------------------------------------------------------------------------- 8 | .'@@@@@@@@@@@@@@#: 9 | ,@@@@#; .'@@@@+ 10 | ,@@@' .@@@# 11 | +@@+ .... .@@@ 12 | ;@@; '@@@@@@@@@@@@. @@@ 13 | @@# @@@@@@@@++@@@@@@@; `@@; 14 | .@@` @@@@@# #@@@@@ @@@ 15 | `@@ @@@@@` Cirno's `@@@@# +@@ 16 | @@ `@@@@@ Perfect @@@@@ @@+ 17 | @@+ ;@@@@+ Math +@@@@+ @@ 18 | @@ `@@@@@ Library @@@@@@ #@' 19 | `@@ @@@@@@ @@@@@@@ `@@ 20 | :@@ #@@@@@@. .@@@@@@@@@ @@ 21 | .@@ #@@@@@@@@@@@@;;@@@@@ @@ 22 | @@ .;+@@#'. ;@@@@@ :@@ 23 | @@` +@@@@+ @@. 24 | ,@@ @@@@@ .@@ 25 | @@# ;;;;;. `@@@@@ @@ 26 | @@+ .@@@@@ @@@@@ @@` 27 | #@@ '@@@@@#` ;@@@@@@ ;@@ 28 | .@@' @@@@@@@@@@@@@@@ @@# 29 | +@@' '@@@@@@@; @@@ 30 | '@@@` '@@@ 31 | #@@@; .@@@@: 32 | :@@@@@@@++;;;+#@@@@@@+` 33 | .;'+++++;. 34 | --]] 35 | local modules = (...) and (...):gsub('%.init$', '') .. ".modules." or "" 36 | 37 | local cpml = { 38 | _LICENSE = "CPML is distributed under the terms of the MIT license. See LICENSE.md.", 39 | _URL = "https://github.com/excessive/cpml", 40 | _VERSION = "1.2.9", 41 | _DESCRIPTION = "Cirno's Perfect Math Library: Just about everything you need for 3D games. Hopefully." 42 | } 43 | 44 | local files = { 45 | "bvh", 46 | "color", 47 | "constants", 48 | "intersect", 49 | "mat4", 50 | "mesh", 51 | "octree", 52 | "quat", 53 | "simplex", 54 | "utils", 55 | "vec2", 56 | "vec3", 57 | "bound2", 58 | "bound3", 59 | } 60 | 61 | for _, file in ipairs(files) do 62 | cpml[file] = require(modules .. file) 63 | end 64 | 65 | return cpml 66 | -------------------------------------------------------------------------------- /modules/_private_precond.lua: -------------------------------------------------------------------------------- 1 | -- Preconditions for cpml functions. 2 | local precond = {} 3 | 4 | 5 | function precond.typeof(t, expected, msg) 6 | if type(t) ~= expected then 7 | error(("%s: %s (<%s> expected)"):format(msg, type(t), expected), 3) 8 | end 9 | end 10 | 11 | function precond.assert(cond, msg, ...) 12 | if not cond then 13 | error(msg:format(...), 3) 14 | end 15 | end 16 | 17 | return precond 18 | -------------------------------------------------------------------------------- /modules/_private_utils.lua: -------------------------------------------------------------------------------- 1 | -- Functions exported by utils.lua but needed by vec2 or vec3 (which utils.lua requires) 2 | 3 | local private = {} 4 | local floor = math.floor 5 | local ceil = math.ceil 6 | 7 | function private.round(value, precision) 8 | if precision then return private.round(value / precision) * precision end 9 | return value >= 0 and floor(value+0.5) or ceil(value-0.5) 10 | end 11 | 12 | function private.is_nan(a) 13 | return a ~= a 14 | end 15 | 16 | return private 17 | -------------------------------------------------------------------------------- /modules/bound2.lua: -------------------------------------------------------------------------------- 1 | --- A 2 component bounding box. 2 | -- @module bound2 3 | 4 | local modules = (...):gsub('%.[^%.]+$', '') .. "." 5 | local vec2 = require(modules .. "vec2") 6 | 7 | local bound2 = {} 8 | local bound2_mt = {} 9 | 10 | -- Private constructor. 11 | local function new(min, max) 12 | return setmetatable({ 13 | min=min, -- min: vec2, minimum value for each component 14 | max=max, -- max: vec2, maximum value for each component 15 | }, bound2_mt) 16 | end 17 | 18 | -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. 19 | local status, ffi 20 | if type(jit) == "table" and jit.status() then 21 | status, ffi = pcall(require, "ffi") 22 | if status then 23 | ffi.cdef "typedef struct { cpml_vec2 min, max; } cpml_bound2;" 24 | new = ffi.typeof("cpml_bound2") 25 | end 26 | end 27 | 28 | bound2.zero = new(vec2.zero, vec2.zero) 29 | 30 | --- The public constructor. 31 | -- @param min Can be of two types:
32 | -- vec2 min, minimum value for each component 33 | -- nil Create bound at single point 0,0 34 | -- @tparam vec2 max, maximum value for each component 35 | -- @treturn bound2 out 36 | function bound2.new(min, max) 37 | if min and max then 38 | return new(min:clone(), max:clone()) 39 | elseif min or max then 40 | error("Unexpected nil argument to bound2.new") 41 | else 42 | return new(vec2.zero, vec2.zero) 43 | end 44 | end 45 | 46 | --- Clone a bound. 47 | -- @tparam bound2 a bound to be cloned 48 | -- @treturn bound2 out 49 | function bound2.clone(a) 50 | return new(a.min, a.max) 51 | end 52 | 53 | --- Construct a bound covering one or two points 54 | -- @tparam vec2 a Any vector 55 | -- @tparam vec2 b Any second vector (optional) 56 | -- @treturn vec2 Minimum bound containing the given points 57 | function bound2.at(a, b) -- "bounded by". b may be nil 58 | if b then 59 | return bound2.new(a,b):check() 60 | else 61 | return bound2.zero:with_center(a) 62 | end 63 | end 64 | 65 | --- Extend bound to include point 66 | -- @tparam bound2 a bound 67 | -- @tparam vec2 point to include 68 | -- @treturn bound2 Bound covering current min, current max and new point 69 | function bound2.extend(a, center) 70 | return bound2.new(a.min:component_min(center), a.max:component_max(center)) 71 | end 72 | 73 | --- Extend bound to entirety of other bound 74 | -- @tparam bound2 a bound 75 | -- @tparam bound2 bound to cover 76 | -- @treturn bound2 Bound covering current min and max of each bound in the pair 77 | function bound2.extend_bound(a, b) 78 | return a:extend(b.min):extend(b.max) 79 | end 80 | 81 | --- Get size of bounding box as a vector 82 | -- @tparam bound2 a bound 83 | -- @treturn vec2 Vector spanning min to max points 84 | function bound2.size(a) 85 | return a.max - a.min 86 | end 87 | 88 | --- Resize bounding box from minimum corner 89 | -- @tparam bound2 a bound 90 | -- @tparam vec2 new size 91 | -- @treturn bound2 resized bound 92 | function bound2.with_size(a, size) 93 | return bound2.new(a.min, a.min + size) 94 | end 95 | 96 | --- Get half-size of bounding box as a vector. A more correct term for this is probably "apothem" 97 | -- @tparam bound2 a bound 98 | -- @treturn vec2 Vector spanning center to max point 99 | function bound2.radius(a) 100 | return a:size()/2 101 | end 102 | 103 | --- Get center of bounding box 104 | -- @tparam bound2 a bound 105 | -- @treturn bound2 Point in center of bound 106 | function bound2.center(a) 107 | return (a.min + a.max)/2 108 | end 109 | 110 | --- Move bounding box to new center 111 | -- @tparam bound2 a bound 112 | -- @tparam vec2 new center 113 | -- @treturn bound2 Bound with same size as input but different center 114 | function bound2.with_center(a, center) 115 | return bound2.offset(a, center - a:center()) 116 | end 117 | 118 | --- Resize bounding box from center 119 | -- @tparam bound2 a bound 120 | -- @tparam vec2 new size 121 | -- @treturn bound2 resized bound 122 | function bound2.with_size_centered(a, size) 123 | local center = a:center() 124 | local rad = size/2 125 | return bound2.new(center - rad, center + rad) 126 | end 127 | 128 | --- Convert possibly-invalid bounding box to valid one 129 | -- @tparam bound2 a bound 130 | -- @treturn bound2 bound with all components corrected for min-max property 131 | function bound2.check(a) 132 | if a.min.x > a.max.x or a.min.y > a.max.y then 133 | return bound2.new(vec2.component_min(a.min, a.max), vec2.component_max(a.min, a.max)) 134 | end 135 | return a 136 | end 137 | 138 | --- Shrink bounding box with fixed margin 139 | -- @tparam bound2 a bound 140 | -- @tparam vec2 a margin 141 | -- @treturn bound2 bound with margin subtracted from all edges. May not be valid, consider calling check() 142 | function bound2.inset(a, v) 143 | return bound2.new(a.min + v, a.max - v) 144 | end 145 | 146 | --- Expand bounding box with fixed margin 147 | -- @tparam bound2 a bound 148 | -- @tparam vec2 a margin 149 | -- @treturn bound2 bound with margin added to all edges. May not be valid, consider calling check() 150 | function bound2.outset(a, v) 151 | return bound2.new(a.min - v, a.max + v) 152 | end 153 | 154 | --- Offset bounding box 155 | -- @tparam bound2 a bound 156 | -- @tparam vec2 offset 157 | -- @treturn bound2 bound with same size, but position moved by offset 158 | function bound2.offset(a, v) 159 | return bound2.new(a.min + v, a.max + v) 160 | end 161 | 162 | --- Test if point in bound 163 | -- @tparam bound2 a bound 164 | -- @tparam vec2 point to test 165 | -- @treturn boolean true if point in bounding box 166 | function bound2.contains(a, v) 167 | return a.min.x <= v.x and a.min.y <= v.y 168 | and a.max.x >= v.x and a.max.y >= v.y 169 | end 170 | 171 | -- Round all components of all vectors to nearest int (or other precision). 172 | -- @tparam vec3 a bound to round. 173 | -- @tparam precision Digits after the decimal (round number if unspecified) 174 | -- @treturn vec3 Rounded bound 175 | function bound2.round(a, precision) 176 | return bound2.new(a.min:round(precision), a.max:round(precision)) 177 | end 178 | 179 | --- Return a formatted string. 180 | -- @tparam bound2 a bound to be turned into a string 181 | -- @treturn string formatted 182 | function bound2.to_string(a) 183 | return string.format("(%s-%s)", a.min, a.max) 184 | end 185 | 186 | bound2_mt.__index = bound2 187 | bound2_mt.__tostring = bound2.to_string 188 | 189 | function bound2_mt.__call(_, a, b) 190 | return bound2.new(a, b) 191 | end 192 | 193 | if status then 194 | xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded 195 | ffi.metatype(new, bound2_mt) 196 | end, function() end) 197 | end 198 | 199 | return setmetatable({}, bound2_mt) 200 | -------------------------------------------------------------------------------- /modules/bound3.lua: -------------------------------------------------------------------------------- 1 | --- A 3-component axis-aligned bounding box. 2 | -- @module bound3 3 | 4 | local modules = (...):gsub('%.[^%.]+$', '') .. "." 5 | local vec3 = require(modules .. "vec3") 6 | 7 | local bound3 = {} 8 | local bound3_mt = {} 9 | 10 | -- Private constructor. 11 | local function new(min, max) 12 | return setmetatable({ 13 | min=min, -- min: vec3, minimum value for each component 14 | max=max -- max: vec3, maximum value for each component 15 | }, bound3_mt) 16 | end 17 | 18 | -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. 19 | local status, ffi 20 | if type(jit) == "table" and jit.status() then 21 | status, ffi = pcall(require, "ffi") 22 | if status then 23 | ffi.cdef "typedef struct { cpml_vec3 min, max; } cpml_bound3;" 24 | new = ffi.typeof("cpml_bound3") 25 | end 26 | end 27 | 28 | bound3.zero = new(vec3.zero, vec3.zero) 29 | 30 | --- The public constructor. 31 | -- @param min Can be of two types:
32 | -- vec3 min, minimum value for each component 33 | -- nil Create bound at single point 0,0,0 34 | -- @tparam vec3 max, maximum value for each component 35 | -- @treturn bound3 out 36 | function bound3.new(min, max) 37 | if min and max then 38 | return new(min:clone(), max:clone()) 39 | elseif min or max then 40 | error("Unexpected nil argument to bound3.new") 41 | else 42 | return new(vec3.zero, vec3.zero) 43 | end 44 | end 45 | 46 | --- Clone a bound. 47 | -- @tparam bound3 a bound to be cloned 48 | -- @treturn bound3 out 49 | function bound3.clone(a) 50 | return new(a.min, a.max) 51 | end 52 | 53 | --- Construct a bound covering one or two points 54 | -- @tparam vec3 a Any vector 55 | -- @tparam vec3 b Any second vector (optional) 56 | -- @treturn vec3 Minimum bound containing the given points 57 | function bound3.at(a, b) -- "bounded by". b may be nil 58 | if b then 59 | return bound3.new(a,b):check() 60 | else 61 | return bound3.zero:with_center(a) 62 | end 63 | end 64 | 65 | --- Extend bound to include point 66 | -- @tparam bound3 a bound 67 | -- @tparam vec3 point to include 68 | -- @treturn bound3 Bound covering current min, current max and new point 69 | function bound3.extend(a, center) 70 | return bound3.new(a.min:component_min(center), a.max:component_max(center)) 71 | end 72 | 73 | --- Extend bound to entirety of other bound 74 | -- @tparam bound3 a bound 75 | -- @tparam bound3 bound to cover 76 | -- @treturn bound3 Bound covering current min and max of each bound in the pair 77 | function bound3.extend_bound(a, b) 78 | return a:extend(b.min):extend(b.max) 79 | end 80 | 81 | --- Get size of bounding box as a vector 82 | -- @tparam bound3 a bound 83 | -- @treturn vec3 Vector spanning min to max points 84 | function bound3.size(a) 85 | return a.max - a.min 86 | end 87 | 88 | --- Resize bounding box from minimum corner 89 | -- @tparam bound3 a bound 90 | -- @tparam vec3 new size 91 | -- @treturn bound3 resized bound 92 | function bound3.with_size(a, size) 93 | return bound3.new(a.min, a.min + size) 94 | end 95 | 96 | --- Get half-size of bounding box as a vector. A more correct term for this is probably "apothem" 97 | -- @tparam bound3 a bound 98 | -- @treturn vec3 Vector spanning center to max point 99 | function bound3.radius(a) 100 | return a:size()/2 101 | end 102 | 103 | --- Get center of bounding box 104 | -- @tparam bound3 a bound 105 | -- @treturn bound3 Point in center of bound 106 | function bound3.center(a) 107 | return (a.min + a.max)/2 108 | end 109 | 110 | --- Move bounding box to new center 111 | -- @tparam bound3 a bound 112 | -- @tparam vec3 new center 113 | -- @treturn bound3 Bound with same size as input but different center 114 | function bound3.with_center(a, center) 115 | return bound3.offset(a, center - a:center()) 116 | end 117 | 118 | --- Resize bounding box from center 119 | -- @tparam bound3 a bound 120 | -- @tparam vec3 new size 121 | -- @treturn bound3 resized bound 122 | function bound3.with_size_centered(a, size) 123 | local center = a:center() 124 | local rad = size/2 125 | return bound3.new(center - rad, center + rad) 126 | end 127 | 128 | --- Convert possibly-invalid bounding box to valid one 129 | -- @tparam bound3 a bound 130 | -- @treturn bound3 bound with all components corrected for min-max property 131 | function bound3.check(a) 132 | if a.min.x > a.max.x or a.min.y > a.max.y or a.min.z > a.max.z then 133 | return bound3.new(vec3.component_min(a.min, a.max), vec3.component_max(a.min, a.max)) 134 | end 135 | return a 136 | end 137 | 138 | --- Shrink bounding box with fixed margin 139 | -- @tparam bound3 a bound 140 | -- @tparam vec3 a margin 141 | -- @treturn bound3 bound with margin subtracted from all edges. May not be valid, consider calling check() 142 | function bound3.inset(a, v) 143 | return bound3.new(a.min + v, a.max - v) 144 | end 145 | 146 | --- Expand bounding box with fixed margin 147 | -- @tparam bound3 a bound 148 | -- @tparam vec3 a margin 149 | -- @treturn bound3 bound with margin added to all edges. May not be valid, consider calling check() 150 | function bound3.outset(a, v) 151 | return bound3.new(a.min - v, a.max + v) 152 | end 153 | 154 | --- Offset bounding box 155 | -- @tparam bound3 a bound 156 | -- @tparam vec3 offset 157 | -- @treturn bound3 bound with same size, but position moved by offset 158 | function bound3.offset(a, v) 159 | return bound3.new(a.min + v, a.max + v) 160 | end 161 | 162 | --- Test if point in bound 163 | -- @tparam bound3 a bound 164 | -- @tparam vec3 point to test 165 | -- @treturn boolean true if point in bounding box 166 | function bound3.contains(a, v) 167 | return a.min.x <= v.x and a.min.y <= v.y and a.min.z <= v.z 168 | and a.max.x >= v.x and a.max.y >= v.y and a.max.z >= v.z 169 | end 170 | 171 | -- Round all components of all vectors to nearest int (or other precision). 172 | -- @tparam vec3 a bound to round. 173 | -- @tparam precision Digits after the decimal (round number if unspecified) 174 | -- @treturn vec3 Rounded bound 175 | function bound3.round(a, precision) 176 | return bound3.new(a.min:round(precision), a.max:round(precision)) 177 | end 178 | 179 | --- Return a formatted string. 180 | -- @tparam bound3 a bound to be turned into a string 181 | -- @treturn string formatted 182 | function bound3.to_string(a) 183 | return string.format("(%s-%s)", a.min, a.max) 184 | end 185 | 186 | bound3_mt.__index = bound3 187 | bound3_mt.__tostring = bound3.to_string 188 | 189 | function bound3_mt.__call(_, a, b) 190 | return bound3.new(a, b) 191 | end 192 | 193 | if status then 194 | xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded 195 | ffi.metatype(new, bound3_mt) 196 | end, function() end) 197 | end 198 | 199 | return setmetatable({}, bound3_mt) 200 | -------------------------------------------------------------------------------- /modules/bvh.lua: -------------------------------------------------------------------------------- 1 | -- https://github.com/benraziel/bvh-tree 2 | 3 | --- BVH Tree 4 | -- @module bvh 5 | 6 | local modules = (...):gsub('%.[^%.]+$', '') .. "." 7 | local intersect = require(modules .. "intersect") 8 | local vec3 = require(modules .. "vec3") 9 | local EPSILON = 1e-6 10 | local BVH = {} 11 | local BVHNode = {} 12 | local Node 13 | 14 | BVH.__index = BVH 15 | BVHNode.__index = BVHNode 16 | 17 | local function new(triangles, maxTrianglesPerNode) 18 | local tree = setmetatable({}, BVH) 19 | local trianglesArray = {} 20 | 21 | for _, triangle in ipairs(triangles) do 22 | local p1 = triangle[1] 23 | local p2 = triangle[2] 24 | local p3 = triangle[3] 25 | 26 | table.insert(trianglesArray, p1.x or p1[1]) 27 | table.insert(trianglesArray, p1.y or p1[2]) 28 | table.insert(trianglesArray, p1.z or p1[3]) 29 | 30 | table.insert(trianglesArray, p2.x or p2[1]) 31 | table.insert(trianglesArray, p2.y or p2[2]) 32 | table.insert(trianglesArray, p2.z or p2[3]) 33 | 34 | table.insert(trianglesArray, p3.x or p3[1]) 35 | table.insert(trianglesArray, p3.y or p3[2]) 36 | table.insert(trianglesArray, p3.z or p3[3]) 37 | end 38 | 39 | tree._trianglesArray = trianglesArray 40 | tree._maxTrianglesPerNode = maxTrianglesPerNode or 10 41 | tree._bboxArray = tree.calcBoundingBoxes(trianglesArray) 42 | 43 | -- clone a helper array 44 | tree._bboxHelper = {} 45 | for _, bbox in ipairs(tree._bboxArray) do 46 | table.insert(tree._bboxHelper, bbox) 47 | end 48 | 49 | -- create the root node, add all the triangles to it 50 | local triangleCount = #triangles 51 | local extents = tree:calcExtents(1, triangleCount, EPSILON) 52 | tree._rootNode = Node(extents[1], extents[2], 1, triangleCount, 1) 53 | 54 | tree._nodes_to_split = { tree._rootNode } 55 | while #tree._nodes_to_split > 0 do 56 | local node = table.remove(tree._nodes_to_split) 57 | tree:splitNode(node) 58 | end 59 | return tree 60 | end 61 | 62 | function BVH:intersectAABB(aabb) 63 | local nodesToIntersect = { self._rootNode } 64 | local trianglesInIntersectingNodes = {} -- a list of nodes that intersect the ray (according to their bounding box) 65 | local intersectingTriangles = {} 66 | 67 | -- go over the BVH tree, and extract the list of triangles that lie in nodes that intersect the box. 68 | -- note: these triangles may not intersect the box themselves 69 | while #nodesToIntersect > 0 do 70 | local node = table.remove(nodesToIntersect) 71 | 72 | local node_aabb = { 73 | min = node._extentsMin, 74 | max = node._extentsMax 75 | } 76 | 77 | if intersect.aabb_aabb(aabb, node_aabb) then 78 | if node._node0 then 79 | table.insert(nodesToIntersect, node._node0) 80 | end 81 | 82 | if node._node1 then 83 | table.insert(nodesToIntersect, node._node1) 84 | end 85 | 86 | for i=node._startIndex, node._endIndex do 87 | table.insert(trianglesInIntersectingNodes, self._bboxArray[1+(i-1)*7]) 88 | end 89 | end 90 | end 91 | 92 | -- insert all node triangles, don't bother being more specific yet. 93 | local triangle = { vec3(), vec3(), vec3() } 94 | 95 | for i=1, #trianglesInIntersectingNodes do 96 | local triIndex = trianglesInIntersectingNodes[i] 97 | 98 | -- print(triIndex, #self._trianglesArray) 99 | triangle[1].x = self._trianglesArray[1+(triIndex-1)*9] 100 | triangle[1].y = self._trianglesArray[1+(triIndex-1)*9+1] 101 | triangle[1].z = self._trianglesArray[1+(triIndex-1)*9+2] 102 | triangle[2].x = self._trianglesArray[1+(triIndex-1)*9+3] 103 | triangle[2].y = self._trianglesArray[1+(triIndex-1)*9+4] 104 | triangle[2].z = self._trianglesArray[1+(triIndex-1)*9+5] 105 | triangle[3].x = self._trianglesArray[1+(triIndex-1)*9+6] 106 | triangle[3].y = self._trianglesArray[1+(triIndex-1)*9+7] 107 | triangle[3].z = self._trianglesArray[1+(triIndex-1)*9+8] 108 | 109 | table.insert(intersectingTriangles, { 110 | triangle = { triangle[1]:clone(), triangle[2]:clone(), triangle[3]:clone() }, 111 | triangleIndex = triIndex 112 | }) 113 | end 114 | 115 | return intersectingTriangles 116 | end 117 | 118 | function BVH:intersectRay(rayOrigin, rayDirection, backfaceCulling) 119 | local nodesToIntersect = { self._rootNode } 120 | local trianglesInIntersectingNodes = {} -- a list of nodes that intersect the ray (according to their bounding box) 121 | local intersectingTriangles = {} 122 | 123 | local invRayDirection = vec3( 124 | 1 / rayDirection.x, 125 | 1 / rayDirection.y, 126 | 1 / rayDirection.z 127 | ) 128 | 129 | -- go over the BVH tree, and extract the list of triangles that lie in nodes that intersect the ray. 130 | -- note: these triangles may not intersect the ray themselves 131 | while #nodesToIntersect > 0 do 132 | local node = table.remove(nodesToIntersect) 133 | 134 | if BVH.intersectNodeBox(rayOrigin, invRayDirection, node) then 135 | if node._node0 then 136 | table.insert(nodesToIntersect, node._node0) 137 | end 138 | 139 | if node._node1 then 140 | table.insert(nodesToIntersect, node._node1) 141 | end 142 | 143 | for i=node._startIndex, node._endIndex do 144 | table.insert(trianglesInIntersectingNodes, self._bboxArray[1+(i-1)*7]) 145 | end 146 | end 147 | end 148 | 149 | -- go over the list of candidate triangles, and check each of them using ray triangle intersection 150 | local triangle = { vec3(), vec3(), vec3() } 151 | local ray = { 152 | position = vec3(rayOrigin.x, rayOrigin.y, rayOrigin.z), 153 | direction = vec3(rayDirection.x, rayDirection.y, rayDirection.z) 154 | } 155 | 156 | for i=1, #trianglesInIntersectingNodes do 157 | local triIndex = trianglesInIntersectingNodes[i] 158 | 159 | -- print(triIndex, #self._trianglesArray) 160 | triangle[1].x = self._trianglesArray[1+(triIndex-1)*9] 161 | triangle[1].y = self._trianglesArray[1+(triIndex-1)*9+1] 162 | triangle[1].z = self._trianglesArray[1+(triIndex-1)*9+2] 163 | triangle[2].x = self._trianglesArray[1+(triIndex-1)*9+3] 164 | triangle[2].y = self._trianglesArray[1+(triIndex-1)*9+4] 165 | triangle[2].z = self._trianglesArray[1+(triIndex-1)*9+5] 166 | triangle[3].x = self._trianglesArray[1+(triIndex-1)*9+6] 167 | triangle[3].y = self._trianglesArray[1+(triIndex-1)*9+7] 168 | triangle[3].z = self._trianglesArray[1+(triIndex-1)*9+8] 169 | 170 | local intersectionPoint, intersectionDistance = intersect.ray_triangle(ray, triangle, backfaceCulling) 171 | 172 | if intersectionPoint then 173 | table.insert(intersectingTriangles, { 174 | triangle = { triangle[1]:clone(), triangle[2]:clone(), triangle[3]:clone() }, 175 | triangleIndex = triIndex, 176 | intersectionPoint = intersectionPoint, 177 | intersectionDistance = intersectionDistance 178 | }) 179 | end 180 | end 181 | 182 | return intersectingTriangles 183 | end 184 | 185 | function BVH.calcBoundingBoxes(trianglesArray) 186 | local p1x, p1y, p1z 187 | local p2x, p2y, p2z 188 | local p3x, p3y, p3z 189 | local minX, minY, minZ 190 | local maxX, maxY, maxZ 191 | 192 | local bboxArray = {} 193 | 194 | for i=1, #trianglesArray / 9 do 195 | p1x = trianglesArray[1+(i-1)*9] 196 | p1y = trianglesArray[1+(i-1)*9+1] 197 | p1z = trianglesArray[1+(i-1)*9+2] 198 | p2x = trianglesArray[1+(i-1)*9+3] 199 | p2y = trianglesArray[1+(i-1)*9+4] 200 | p2z = trianglesArray[1+(i-1)*9+5] 201 | p3x = trianglesArray[1+(i-1)*9+6] 202 | p3y = trianglesArray[1+(i-1)*9+7] 203 | p3z = trianglesArray[1+(i-1)*9+8] 204 | 205 | minX = math.min(p1x, p2x, p3x) 206 | minY = math.min(p1y, p2y, p3y) 207 | minZ = math.min(p1z, p2z, p3z) 208 | maxX = math.max(p1x, p2x, p3x) 209 | maxY = math.max(p1y, p2y, p3y) 210 | maxZ = math.max(p1z, p2z, p3z) 211 | 212 | BVH.setBox(bboxArray, i, i, minX, minY, minZ, maxX, maxY, maxZ) 213 | end 214 | 215 | return bboxArray 216 | end 217 | 218 | function BVH:calcExtents(startIndex, endIndex, expandBy) 219 | expandBy = expandBy or 0 220 | 221 | if startIndex > endIndex then 222 | return { vec3(), vec3() } 223 | end 224 | 225 | local minX = math.huge 226 | local minY = math.huge 227 | local minZ = math.huge 228 | local maxX = -math.huge 229 | local maxY = -math.huge 230 | local maxZ = -math.huge 231 | 232 | for i=startIndex, endIndex do 233 | minX = math.min(self._bboxArray[1+(i-1)*7+1], minX) 234 | minY = math.min(self._bboxArray[1+(i-1)*7+2], minY) 235 | minZ = math.min(self._bboxArray[1+(i-1)*7+3], minZ) 236 | maxX = math.max(self._bboxArray[1+(i-1)*7+4], maxX) 237 | maxY = math.max(self._bboxArray[1+(i-1)*7+5], maxY) 238 | maxZ = math.max(self._bboxArray[1+(i-1)*7+6], maxZ) 239 | end 240 | 241 | return { 242 | vec3(minX - expandBy, minY - expandBy, minZ - expandBy), 243 | vec3(maxX + expandBy, maxY + expandBy, maxZ + expandBy) 244 | } 245 | end 246 | 247 | function BVH:splitNode(node) 248 | local num_elements = node:elementCount() 249 | if (num_elements <= self._maxTrianglesPerNode) or (num_elements <= 0) then 250 | return 251 | end 252 | 253 | local startIndex = node._startIndex 254 | local endIndex = node._endIndex 255 | 256 | local leftNode = { {},{},{} } 257 | local rightNode = { {},{},{} } 258 | local extentCenters = { node:centerX(), node:centerY(), node:centerZ() } 259 | 260 | local extentsLength = { 261 | node._extentsMax.x - node._extentsMin.x, 262 | node._extentsMax.y - node._extentsMin.y, 263 | node._extentsMax.z - node._extentsMin.z 264 | } 265 | 266 | local objectCenter = {} 267 | for i=startIndex, endIndex do 268 | objectCenter[1] = (self._bboxArray[1+(i-1)*7+1] + self._bboxArray[1+(i-1)*7+4]) * 0.5 -- center = (min + max) / 2 269 | objectCenter[2] = (self._bboxArray[1+(i-1)*7+2] + self._bboxArray[1+(i-1)*7+5]) * 0.5 -- center = (min + max) / 2 270 | objectCenter[3] = (self._bboxArray[1+(i-1)*7+3] + self._bboxArray[1+(i-1)*7+6]) * 0.5 -- center = (min + max) / 2 271 | 272 | for j=1, 3 do 273 | if objectCenter[j] < extentCenters[j] then 274 | table.insert(leftNode[j], i) 275 | else 276 | table.insert(rightNode[j], i) 277 | end 278 | end 279 | end 280 | 281 | -- check if we couldn't split the node by any of the axes (x, y or z). halt 282 | -- here, dont try to split any more (cause it will always fail, and we'll 283 | -- enter an infinite loop 284 | local splitFailed = { 285 | #leftNode[1] == 0 or #rightNode[1] == 0, 286 | #leftNode[2] == 0 or #rightNode[2] == 0, 287 | #leftNode[3] == 0 or #rightNode[3] == 0 288 | } 289 | 290 | if splitFailed[1] and splitFailed[2] and splitFailed[3] then 291 | return 292 | end 293 | 294 | -- choose the longest split axis. if we can't split by it, choose next best one. 295 | local splitOrder = { 1, 2, 3 } 296 | table.sort(splitOrder, function(a, b) 297 | return extentsLength[a] > extentsLength[b] 298 | end) 299 | 300 | local leftElements 301 | local rightElements 302 | 303 | for i=1, 3 do 304 | local candidateIndex = splitOrder[i] 305 | if not splitFailed[candidateIndex] then 306 | leftElements = leftNode[candidateIndex] 307 | rightElements = rightNode[candidateIndex] 308 | break 309 | end 310 | end 311 | 312 | -- sort the elements in range (startIndex, endIndex) according to which node they should be at 313 | local node0Start = startIndex 314 | local node1Start = node0Start + #leftElements 315 | local node0End = node1Start - 1 316 | local node1End = endIndex 317 | local currElement 318 | 319 | local helperPos = node._startIndex 320 | local concatenatedElements = {} 321 | 322 | for _, element in ipairs(leftElements) do 323 | table.insert(concatenatedElements, element) 324 | end 325 | 326 | for _, element in ipairs(rightElements) do 327 | table.insert(concatenatedElements, element) 328 | end 329 | 330 | -- print(#leftElements, #rightElements, #concatenatedElements) 331 | 332 | for i=1, #concatenatedElements do 333 | currElement = concatenatedElements[i] 334 | BVH.copyBox(self._bboxArray, currElement, self._bboxHelper, helperPos) 335 | helperPos = helperPos + 1 336 | end 337 | 338 | -- copy results back to main array 339 | for i=1+(node._startIndex-1)*7, node._endIndex*7 do 340 | self._bboxArray[i] = self._bboxHelper[i] 341 | end 342 | 343 | -- create 2 new nodes for the node we just split, and add links to them from the parent node 344 | local node0Extents = self:calcExtents(node0Start, node0End, EPSILON) 345 | local node1Extents = self:calcExtents(node1Start, node1End, EPSILON) 346 | 347 | local node0 = Node(node0Extents[1], node0Extents[2], node0Start, node0End, node._level + 1) 348 | local node1 = Node(node1Extents[1], node1Extents[2], node1Start, node1End, node._level + 1) 349 | 350 | node._node0 = node0 351 | node._node1 = node1 352 | node:clearShapes() 353 | 354 | -- add new nodes to the split queue 355 | table.insert(self._nodes_to_split, node0) 356 | table.insert(self._nodes_to_split, node1) 357 | end 358 | 359 | function BVH._calcTValues(minVal, maxVal, rayOriginCoord, invdir) 360 | local res = { min=0, max=0 } 361 | 362 | if invdir >= 0 then 363 | res.min = ( minVal - rayOriginCoord ) * invdir 364 | res.max = ( maxVal - rayOriginCoord ) * invdir 365 | else 366 | res.min = ( maxVal - rayOriginCoord ) * invdir 367 | res.max = ( minVal - rayOriginCoord ) * invdir 368 | end 369 | 370 | return res 371 | end 372 | 373 | function BVH.intersectNodeBox(rayOrigin, invRayDirection, node) 374 | local t = BVH._calcTValues(node._extentsMin.x, node._extentsMax.x, rayOrigin.x, invRayDirection.x) 375 | local ty = BVH._calcTValues(node._extentsMin.y, node._extentsMax.y, rayOrigin.y, invRayDirection.y) 376 | 377 | if t.min > ty.max or ty.min > t.max then 378 | return false 379 | end 380 | 381 | -- These lines also handle the case where tmin or tmax is NaN 382 | -- (result of 0 * Infinity). x !== x returns true if x is NaN 383 | if ty.min > t.min or t.min ~= t.min then 384 | t.min = ty.min 385 | end 386 | 387 | if ty.max < t.max or t.max ~= t.max then 388 | t.max = ty.max 389 | end 390 | 391 | local tz = BVH._calcTValues(node._extentsMin.z, node._extentsMax.z, rayOrigin.z, invRayDirection.z) 392 | 393 | if t.min > tz.max or tz.min > t.max then 394 | return false 395 | end 396 | 397 | if tz.min > t.min or t.min ~= t.min then 398 | t.min = tz.min 399 | end 400 | 401 | if tz.max < t.max or t.max ~= t.max then 402 | t.max = tz.max 403 | end 404 | 405 | --return point closest to the ray (positive side) 406 | if t.max < 0 then 407 | return false 408 | end 409 | 410 | return true 411 | end 412 | 413 | function BVH.setBox(bboxArray, pos, triangleId, minX, minY, minZ, maxX, maxY, maxZ) 414 | bboxArray[1+(pos-1)*7] = triangleId 415 | bboxArray[1+(pos-1)*7+1] = minX 416 | bboxArray[1+(pos-1)*7+2] = minY 417 | bboxArray[1+(pos-1)*7+3] = minZ 418 | bboxArray[1+(pos-1)*7+4] = maxX 419 | bboxArray[1+(pos-1)*7+5] = maxY 420 | bboxArray[1+(pos-1)*7+6] = maxZ 421 | end 422 | 423 | function BVH.copyBox(sourceArray, sourcePos, destArray, destPos) 424 | destArray[1+(destPos-1)*7] = sourceArray[1+(sourcePos-1)*7] 425 | destArray[1+(destPos-1)*7+1] = sourceArray[1+(sourcePos-1)*7+1] 426 | destArray[1+(destPos-1)*7+2] = sourceArray[1+(sourcePos-1)*7+2] 427 | destArray[1+(destPos-1)*7+3] = sourceArray[1+(sourcePos-1)*7+3] 428 | destArray[1+(destPos-1)*7+4] = sourceArray[1+(sourcePos-1)*7+4] 429 | destArray[1+(destPos-1)*7+5] = sourceArray[1+(sourcePos-1)*7+5] 430 | destArray[1+(destPos-1)*7+6] = sourceArray[1+(sourcePos-1)*7+6] 431 | end 432 | 433 | function BVH.getBox(bboxArray, pos, outputBox) 434 | outputBox.triangleId = bboxArray[1+(pos-1)*7] 435 | outputBox.minX = bboxArray[1+(pos-1)*7+1] 436 | outputBox.minY = bboxArray[1+(pos-1)*7+2] 437 | outputBox.minZ = bboxArray[1+(pos-1)*7+3] 438 | outputBox.maxX = bboxArray[1+(pos-1)*7+4] 439 | outputBox.maxY = bboxArray[1+(pos-1)*7+5] 440 | outputBox.maxZ = bboxArray[1+(pos-1)*7+6] 441 | end 442 | 443 | local function new_node(extentsMin, extentsMax, startIndex, endIndex, level) 444 | return setmetatable({ 445 | _extentsMin = extentsMin, 446 | _extentsMax = extentsMax, 447 | _startIndex = startIndex, 448 | _endIndex = endIndex, 449 | _level = level 450 | --_node0 = nil 451 | --_node1 = nil 452 | }, BVHNode) 453 | end 454 | 455 | function BVHNode:elementCount() 456 | return (self._endIndex + 1) - self._startIndex 457 | end 458 | 459 | function BVHNode:centerX() 460 | return (self._extentsMin.x + self._extentsMax.x) * 0.5 461 | end 462 | 463 | function BVHNode:centerY() 464 | return (self._extentsMin.y + self._extentsMax.y) * 0.5 465 | end 466 | 467 | function BVHNode:centerZ() 468 | return (self._extentsMin.z + self._extentsMax.z) * 0.5 469 | end 470 | 471 | function BVHNode:clearShapes() 472 | self._startIndex = 0 473 | self._endIndex = -1 474 | end 475 | 476 | function BVHNode.ngSphereRadius(extentsMin, extentsMax) 477 | local centerX = (extentsMin.x + extentsMax.x) * 0.5 478 | local centerY = (extentsMin.y + extentsMax.y) * 0.5 479 | local centerZ = (extentsMin.z + extentsMax.z) * 0.5 480 | 481 | local extentsMinDistSqr = 482 | (centerX - extentsMin.x) * (centerX - extentsMin.x) + 483 | (centerY - extentsMin.y) * (centerY - extentsMin.y) + 484 | (centerZ - extentsMin.z) * (centerZ - extentsMin.z) 485 | 486 | local extentsMaxDistSqr = 487 | (centerX - extentsMax.x) * (centerX - extentsMax.x) + 488 | (centerY - extentsMax.y) * (centerY - extentsMax.y) + 489 | (centerZ - extentsMax.z) * (centerZ - extentsMax.z) 490 | 491 | return math.sqrt(math.max(extentsMinDistSqr, extentsMaxDistSqr)) 492 | end 493 | 494 | --[[ 495 | 496 | --- Draws node boundaries visually for debugging. 497 | -- @param cube Cube model to draw 498 | -- @param depth Used for recurcive calls to self method 499 | function OctreeNode:draw_bounds(cube, depth) 500 | depth = depth or 0 501 | local tint = depth / 7 -- Will eventually get values > 1. Color rounds to 1 automatically 502 | 503 | love.graphics.setColor(tint * 255, 0, (1 - tint) * 255) 504 | local m = mat4() 505 | :translate(self.center) 506 | :scale(vec3(self.adjLength, self.adjLength, self.adjLength)) 507 | 508 | love.graphics.updateMatrix("transform", m) 509 | love.graphics.setWireframe(true) 510 | love.graphics.draw(cube) 511 | love.graphics.setWireframe(false) 512 | 513 | for _, child in ipairs(self.children) do 514 | child:draw_bounds(cube, depth + 1) 515 | end 516 | 517 | love.graphics.setColor(255, 255, 255) 518 | end 519 | 520 | --- Draws the bounds of all objects in the tree visually for debugging. 521 | -- @param cube Cube model to draw 522 | -- @param filter a function returning true or false to determine visibility. 523 | function OctreeNode:draw_objects(cube, filter) 524 | local tint = self.baseLength / 20 525 | love.graphics.setColor(0, (1 - tint) * 255, tint * 255, 63) 526 | 527 | for _, object in ipairs(self.objects) do 528 | if filter and filter(object.data) or not filter then 529 | local m = mat4() 530 | :translate(object.bounds.center) 531 | :scale(object.bounds.size) 532 | 533 | love.graphics.updateMatrix("transform", m) 534 | love.graphics.draw(cube) 535 | end 536 | end 537 | 538 | for _, child in ipairs(self.children) do 539 | child:draw_objects(cube, filter) 540 | end 541 | 542 | love.graphics.setColor(255, 255, 255) 543 | end 544 | 545 | --]] 546 | 547 | Node = setmetatable({ 548 | new = new_node 549 | }, { 550 | __call = function(_, ...) return new_node(...) end 551 | }) 552 | 553 | return setmetatable({ 554 | new = new 555 | }, { 556 | __call = function(_, ...) return new(...) end 557 | }) 558 | -------------------------------------------------------------------------------- /modules/color.lua: -------------------------------------------------------------------------------- 1 | --- Color utilities 2 | -- @module color 3 | 4 | local modules = (...):gsub('%.[^%.]+$', '') .. "." 5 | local constants = require(modules .. "constants") 6 | local utils = require(modules .. "utils") 7 | local precond = require(modules .. "_private_precond") 8 | local color = {} 9 | local color_mt = {} 10 | 11 | local function new(r, g, b, a) 12 | local c = { r, g, b, a } 13 | c._c = c 14 | return setmetatable(c, color_mt) 15 | end 16 | 17 | -- HSV utilities (adapted from http://www.cs.rit.edu/~ncs/color/t_convert.html) 18 | -- hsv_to_color(hsv) 19 | -- Converts a set of HSV values to a color. hsv is a table. 20 | -- See also: hsv(h, s, v) 21 | local function hsv_to_color(hsv) 22 | local i 23 | local f, q, p, t 24 | local h, s, v 25 | local a = hsv[4] or 1 26 | s = hsv[2] 27 | v = hsv[3] 28 | 29 | if s == 0 then 30 | return new(v, v, v, a) 31 | end 32 | 33 | h = hsv[1] * 6 -- sector 0 to 5 34 | 35 | i = math.floor(h) 36 | f = h - i -- factorial part of h 37 | p = v * (1-s) 38 | q = v * (1-s*f) 39 | t = v * (1-s*(1-f)) 40 | 41 | if i == 0 then return new(v, t, p, a) 42 | elseif i == 1 then return new(q, v, p, a) 43 | elseif i == 2 then return new(p, v, t, a) 44 | elseif i == 3 then return new(p, q, v, a) 45 | elseif i == 4 then return new(t, p, v, a) 46 | else return new(v, p, q, a) 47 | end 48 | end 49 | 50 | -- color_to_hsv(c) 51 | -- Takes in a normal color and returns a table with the HSV values. 52 | local function color_to_hsv(c) 53 | local r = c[1] 54 | local g = c[2] 55 | local b = c[3] 56 | local a = c[4] or 1 57 | local h, s, v 58 | 59 | local min = math.min(r, g, b) 60 | local max = math.max(r, g, b) 61 | v = max 62 | 63 | local delta = max - min 64 | 65 | -- black, nothing else is really possible here. 66 | if min == 0 and max == 0 then 67 | return { 0, 0, 0, a } 68 | end 69 | 70 | if max ~= 0 then 71 | s = delta / max 72 | else 73 | -- r = g = b = 0 s = 0, v is undefined 74 | s = 0 75 | h = -1 76 | return { h, s, v, 1 } 77 | end 78 | 79 | -- Prevent division by zero. 80 | if delta == 0 then 81 | delta = constants.DBL_EPSILON 82 | end 83 | 84 | if r == max then 85 | h = ( g - b ) / delta -- yellow/magenta 86 | elseif g == max then 87 | h = 2 + ( b - r ) / delta -- cyan/yellow 88 | else 89 | h = 4 + ( r - g ) / delta -- magenta/cyan 90 | end 91 | 92 | h = h / 6 -- normalize from segment 0..5 93 | 94 | if h < 0 then 95 | h = h + 1 96 | end 97 | 98 | return { h, s, v, a } 99 | end 100 | 101 | --- The public constructor. 102 | -- @param x Can be of three types:
103 | -- number red component 0-1 104 | -- table {r, g, b, a} 105 | -- nil for {0,0,0,0} 106 | -- @tparam number g Green component 0-1 107 | -- @tparam number b Blue component 0-1 108 | -- @tparam number a Alpha component 0-1 109 | -- @treturn color out 110 | function color.new(r, g, b, a) 111 | -- number, number, number, number 112 | if r and g and b and a then 113 | precond.typeof(r, "number", "new: Wrong argument type for r") 114 | precond.typeof(g, "number", "new: Wrong argument type for g") 115 | precond.typeof(b, "number", "new: Wrong argument type for b") 116 | precond.typeof(a, "number", "new: Wrong argument type for a") 117 | 118 | return new(r, g, b, a) 119 | 120 | -- {r, g, b, a} 121 | elseif type(r) == "table" then 122 | local rr, gg, bb, aa = r[1], r[2], r[3], r[4] 123 | precond.typeof(rr, "number", "new: Wrong argument type for r") 124 | precond.typeof(gg, "number", "new: Wrong argument type for g") 125 | precond.typeof(bb, "number", "new: Wrong argument type for b") 126 | precond.typeof(aa, "number", "new: Wrong argument type for a") 127 | 128 | return new(rr, gg, bb, aa) 129 | end 130 | 131 | return new(0, 0, 0, 0) 132 | end 133 | 134 | --- Convert hue,saturation,value table to color object. 135 | -- @tparam table hsva {hue 0-1, saturation 0-1, value 0-1, alpha 0-1} 136 | -- @treturn color out 137 | color.hsv_to_color_table = hsv_to_color 138 | 139 | --- Convert color to hue,saturation,value table 140 | -- @tparam color in 141 | -- @treturn table hsva {hue 0-1, saturation 0-1, value 0-1, alpha 0-1} 142 | color.color_to_hsv_table = color_to_hsv 143 | 144 | --- Convert hue,saturation,value to color object. 145 | -- @tparam number h hue 0-1 146 | -- @tparam number s saturation 0-1 147 | -- @tparam number v value 0-1 148 | -- @treturn color out 149 | function color.from_hsv(h, s, v) 150 | return hsv_to_color { h, s, v } 151 | end 152 | 153 | --- Convert hue,saturation,value to color object. 154 | -- @tparam number h hue 0-1 155 | -- @tparam number s saturation 0-1 156 | -- @tparam number v value 0-1 157 | -- @tparam number a alpha 0-1 158 | -- @treturn color out 159 | function color.from_hsva(h, s, v, a) 160 | return hsv_to_color { h, s, v, a } 161 | end 162 | 163 | --- Invert a color. 164 | -- @tparam color to invert 165 | -- @treturn color out 166 | function color.invert(c) 167 | return new(1 - c[1], 1 - c[2], 1 - c[3], c[4]) 168 | end 169 | 170 | --- Lighten a color by a component-wise fixed amount (alpha unchanged) 171 | -- @tparam color to lighten 172 | -- @tparam number amount to increase each component by, 0-1 scale 173 | -- @treturn color out 174 | function color.lighten(c, v) 175 | return new( 176 | utils.clamp(c[1] + v, 0, 1), 177 | utils.clamp(c[2] + v, 0, 1), 178 | utils.clamp(c[3] + v, 0, 1), 179 | c[4] 180 | ) 181 | end 182 | 183 | --- Interpolate between two colors. 184 | -- @tparam color at start 185 | -- @tparam color at end 186 | -- @tparam number s in 0-1 progress between the two colors 187 | -- @treturn color out 188 | function color.lerp(a, b, s) 189 | return a + s * (b - a) 190 | end 191 | 192 | --- Unpack a color into individual components in 0-1. 193 | -- @tparam color to unpack 194 | -- @treturn number r in 0-1 195 | -- @treturn number g in 0-1 196 | -- @treturn number b in 0-1 197 | -- @treturn number a in 0-1 198 | function color.unpack(c) 199 | return c[1], c[2], c[3], c[4] 200 | end 201 | 202 | --- Unpack a color into individual components in 0-255. 203 | -- @tparam color to unpack 204 | -- @treturn number r in 0-255 205 | -- @treturn number g in 0-255 206 | -- @treturn number b in 0-255 207 | -- @treturn number a in 0-255 208 | function color.as_255(c) 209 | return c[1] * 255, c[2] * 255, c[3] * 255, c[4] * 255 210 | end 211 | 212 | --- Darken a color by a component-wise fixed amount (alpha unchanged) 213 | -- @tparam color to darken 214 | -- @tparam number amount to decrease each component by, 0-1 scale 215 | -- @treturn color out 216 | function color.darken(c, v) 217 | return new( 218 | utils.clamp(c[1] - v, 0, 1), 219 | utils.clamp(c[2] - v, 0, 1), 220 | utils.clamp(c[3] - v, 0, 1), 221 | c[4] 222 | ) 223 | end 224 | 225 | --- Multiply a color's components by a value (alpha unchanged) 226 | -- @tparam color to multiply 227 | -- @tparam number to multiply each component by 228 | -- @treturn color out 229 | function color.multiply(c, v) 230 | local t = color.new() 231 | for i = 1, 3 do 232 | t[i] = c[i] * v 233 | end 234 | 235 | t[4] = c[4] 236 | return t 237 | end 238 | 239 | -- directly set alpha channel 240 | -- @tparam color to alter 241 | -- @tparam number new alpha 0-1 242 | -- @treturn color out 243 | function color.alpha(c, v) 244 | local t = color.new() 245 | for i = 1, 3 do 246 | t[i] = c[i] 247 | end 248 | 249 | t[4] = v 250 | return t 251 | end 252 | 253 | --- Multiply a color's alpha by a value 254 | -- @tparam color to multiply 255 | -- @tparam number to multiply alpha by 256 | -- @treturn color out 257 | function color.opacity(c, v) 258 | local t = color.new() 259 | for i = 1, 3 do 260 | t[i] = c[i] 261 | end 262 | 263 | t[4] = c[4] * v 264 | return t 265 | end 266 | 267 | --- Set a color's hue (saturation, value, alpha unchanged) 268 | -- @tparam color to alter 269 | -- @tparam hue to set 0-1 270 | -- @treturn color out 271 | function color.hue(col, hue) 272 | local c = color_to_hsv(col) 273 | c[1] = (hue + 1) % 1 274 | return hsv_to_color(c) 275 | end 276 | 277 | --- Set a color's saturation (hue, value, alpha unchanged) 278 | -- @tparam color to alter 279 | -- @tparam saturation to set 0-1 280 | -- @treturn color out 281 | function color.saturation(col, percent) 282 | local c = color_to_hsv(col) 283 | c[2] = utils.clamp(percent, 0, 1) 284 | return hsv_to_color(c) 285 | end 286 | 287 | --- Set a color's value (saturation, hue, alpha unchanged) 288 | -- @tparam color to alter 289 | -- @tparam value to set 0-1 290 | -- @treturn color out 291 | function color.value(col, percent) 292 | local c = color_to_hsv(col) 293 | c[3] = utils.clamp(percent, 0, 1) 294 | return hsv_to_color(c) 295 | end 296 | 297 | -- https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ 298 | function color.gamma_to_linear(r, g, b, a) 299 | local function convert(c) 300 | if c > 1.0 then 301 | return 1.0 302 | elseif c < 0.0 then 303 | return 0.0 304 | elseif c <= 0.04045 then 305 | return c / 12.92 306 | else 307 | return math.pow((c + 0.055) / 1.055, 2.4) 308 | end 309 | end 310 | 311 | if type(r) == "table" then 312 | local c = {} 313 | for i = 1, 3 do 314 | c[i] = convert(r[i]) 315 | end 316 | 317 | c[4] = r[4] 318 | return c 319 | else 320 | return convert(r), convert(g), convert(b), a or 1 321 | end 322 | end 323 | 324 | -- https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB 325 | function color.linear_to_gamma(r, g, b, a) 326 | local function convert(c) 327 | if c > 1.0 then 328 | return 1.0 329 | elseif c < 0.0 then 330 | return 0.0 331 | elseif c < 0.0031308 then 332 | return c * 12.92 333 | else 334 | return 1.055 * math.pow(c, 0.41666) - 0.055 335 | end 336 | end 337 | 338 | if type(r) == "table" then 339 | local c = {} 340 | for i = 1, 3 do 341 | c[i] = convert(r[i]) 342 | end 343 | 344 | c[4] = r[4] 345 | return c 346 | else 347 | return convert(r), convert(g), convert(b), a or 1 348 | end 349 | end 350 | 351 | --- Check if color is valid 352 | -- @tparam color to test 353 | -- @treturn boolean is color 354 | function color.is_color(a) 355 | if type(a) ~= "table" then 356 | return false 357 | end 358 | 359 | for i = 1, 4 do 360 | if type(a[i]) ~= "number" then 361 | return false 362 | end 363 | end 364 | 365 | return true 366 | end 367 | 368 | --- Return a formatted string. 369 | -- @tparam color a color to be turned into a string 370 | -- @treturn string formatted 371 | function color.to_string(a) 372 | return string.format("[ %3.0f, %3.0f, %3.0f, %3.0f ]", a[1], a[2], a[3], a[4]) 373 | end 374 | 375 | color_mt.__index = color 376 | color_mt.__tostring = color.to_string 377 | 378 | function color_mt.__call(_, r, g, b, a) 379 | return color.new(r, g, b, a) 380 | end 381 | 382 | function color_mt.__add(a, b) 383 | return new(a[1] + b[1], a[2] + b[2], a[3] + b[3], a[4] + b[4]) 384 | end 385 | 386 | function color_mt.__sub(a, b) 387 | return new(a[1] - b[1], a[2] - b[2], a[3] - b[3], a[4] - b[4]) 388 | end 389 | 390 | function color_mt.__mul(a, b) 391 | if type(a) == "number" then 392 | return new(a * b[1], a * b[2], a * b[3], a * b[4]) 393 | elseif type(b) == "number" then 394 | return new(b * a[1], b * a[2], b * a[3], b * a[4]) 395 | else 396 | return new(a[1] * b[1], a[2] * b[2], a[3] * b[3], a[4] * b[4]) 397 | end 398 | end 399 | 400 | return setmetatable({}, color_mt) 401 | -------------------------------------------------------------------------------- /modules/constants.lua: -------------------------------------------------------------------------------- 1 | --- Various useful constants 2 | -- @module constants 3 | 4 | --- Constants 5 | -- @table constants 6 | -- @field FLT_EPSILON Floating point precision breaks down 7 | -- @field DBL_EPSILON Double-precise floating point precision breaks down 8 | -- @field DOT_THRESHOLD Close enough to 1 for interpolations to occur 9 | local constants = {} 10 | 11 | -- same as C's FLT_EPSILON 12 | constants.FLT_EPSILON = 1.19209290e-07 13 | 14 | -- same as C's DBL_EPSILON 15 | constants.DBL_EPSILON = 2.2204460492503131e-16 16 | 17 | -- used for quaternion.slerp 18 | constants.DOT_THRESHOLD = 0.9995 19 | 20 | return constants 21 | -------------------------------------------------------------------------------- /modules/intersect.lua: -------------------------------------------------------------------------------- 1 | --- Various geometric intersections 2 | -- @module intersect 3 | 4 | local modules = (...):gsub('%.[^%.]+$', '') .. "." 5 | local constants = require(modules .. "constants") 6 | local mat4 = require(modules .. "mat4") 7 | local vec3 = require(modules .. "vec3") 8 | local utils = require(modules .. "utils") 9 | local DBL_EPSILON = constants.DBL_EPSILON 10 | local sqrt = math.sqrt 11 | local abs = math.abs 12 | local min = math.min 13 | local max = math.max 14 | local intersect = {} 15 | 16 | -- https://blogs.msdn.microsoft.com/rezanour/2011/08/07/barycentric-coordinates-and-point-in-triangle-tests/ 17 | -- point is a vec3 18 | -- triangle[1] is a vec3 19 | -- triangle[2] is a vec3 20 | -- triangle[3] is a vec3 21 | function intersect.point_triangle(point, triangle) 22 | local u = triangle[2] - triangle[1] 23 | local v = triangle[3] - triangle[1] 24 | local w = point - triangle[1] 25 | 26 | local vw = v:cross(w) 27 | local vu = v:cross(u) 28 | 29 | if vw:dot(vu) < 0 then 30 | return false 31 | end 32 | 33 | local uw = u:cross(w) 34 | local uv = u:cross(v) 35 | 36 | if uw:dot(uv) < 0 then 37 | return false 38 | end 39 | 40 | local d = uv:len() 41 | local r = vw:len() / d 42 | local t = uw:len() / d 43 | 44 | return r + t <= 1 45 | end 46 | 47 | -- point is a vec3 48 | -- aabb.min is a vec3 49 | -- aabb.max is a vec3 50 | function intersect.point_aabb(point, aabb) 51 | return 52 | aabb.min.x <= point.x and 53 | aabb.max.x >= point.x and 54 | aabb.min.y <= point.y and 55 | aabb.max.y >= point.y and 56 | aabb.min.z <= point.z and 57 | aabb.max.z >= point.z 58 | end 59 | 60 | -- point is a vec3 61 | -- frustum.left is a plane { a, b, c, d } 62 | -- frustum.right is a plane { a, b, c, d } 63 | -- frustum.bottom is a plane { a, b, c, d } 64 | -- frustum.top is a plane { a, b, c, d } 65 | -- frustum.near is a plane { a, b, c, d } 66 | -- frustum.far is a plane { a, b, c, d } 67 | function intersect.point_frustum(point, frustum) 68 | local x, y, z = point:unpack() 69 | local planes = { 70 | frustum.left, 71 | frustum.right, 72 | frustum.bottom, 73 | frustum.top, 74 | frustum.near, 75 | frustum.far or false 76 | } 77 | 78 | -- Skip the last test for infinite projections, it'll never fail. 79 | if not planes[6] then 80 | table.remove(planes) 81 | end 82 | 83 | local dot 84 | for i = 1, #planes do 85 | dot = planes[i].a * x + planes[i].b * y + planes[i].c * z + planes[i].d 86 | if dot <= 0 then 87 | return false 88 | end 89 | end 90 | 91 | return true 92 | end 93 | 94 | -- http://www.lighthouse3d.com/tutorials/maths/ray-triangle-intersection/ 95 | -- ray.position is a vec3 96 | -- ray.direction is a vec3 97 | -- triangle[1] is a vec3 98 | -- triangle[2] is a vec3 99 | -- triangle[3] is a vec3 100 | -- backface_cull is a boolean (optional) 101 | function intersect.ray_triangle(ray, triangle, backface_cull) 102 | local e1 = triangle[2] - triangle[1] 103 | local e2 = triangle[3] - triangle[1] 104 | local h = ray.direction:cross(e2) 105 | local a = h:dot(e1) 106 | 107 | -- if a is negative, ray hits the backface 108 | if backface_cull and a < 0 then 109 | return false 110 | end 111 | 112 | -- if a is too close to 0, ray does not intersect triangle 113 | if abs(a) <= DBL_EPSILON then 114 | return false 115 | end 116 | 117 | local f = 1 / a 118 | local s = ray.position - triangle[1] 119 | local u = s:dot(h) * f 120 | 121 | -- ray does not intersect triangle 122 | if u < 0 or u > 1 then 123 | return false 124 | end 125 | 126 | local q = s:cross(e1) 127 | local v = ray.direction:dot(q) * f 128 | 129 | -- ray does not intersect triangle 130 | if v < 0 or u + v > 1 then 131 | return false 132 | end 133 | 134 | -- at this stage we can compute t to find out where 135 | -- the intersection point is on the line 136 | local t = q:dot(e2) * f 137 | 138 | -- return position of intersection and distance from ray origin 139 | if t >= DBL_EPSILON then 140 | return ray.position + ray.direction * t, t 141 | end 142 | 143 | -- ray does not intersect triangle 144 | return false 145 | end 146 | 147 | -- https://gamedev.stackexchange.com/questions/96459/fast-ray-sphere-collision-code 148 | -- ray.position is a vec3 149 | -- ray.direction is a vec3 150 | -- sphere.position is a vec3 151 | -- sphere.radius is a number 152 | function intersect.ray_sphere(ray, sphere) 153 | local offset = ray.position - sphere.position 154 | local b = offset:dot(ray.direction) 155 | local c = offset:dot(offset) - sphere.radius * sphere.radius 156 | 157 | -- ray's position outside sphere (c > 0) 158 | -- ray's direction pointing away from sphere (b > 0) 159 | if c > 0 and b > 0 then 160 | return false 161 | end 162 | 163 | local discr = b * b - c 164 | 165 | -- negative discriminant 166 | if discr < 0 then 167 | return false 168 | end 169 | 170 | -- Clamp t to 0 171 | local t = -b - sqrt(discr) 172 | t = t < 0 and 0 or t 173 | 174 | -- Return collision point and distance from ray origin 175 | return ray.position + ray.direction * t, t 176 | end 177 | 178 | -- http://gamedev.stackexchange.com/a/18459 179 | -- ray.position is a vec3 180 | -- ray.direction is a vec3 181 | -- aabb.min is a vec3 182 | -- aabb.max is a vec3 183 | function intersect.ray_aabb(ray, aabb) 184 | local dir = ray.direction:normalize() 185 | local dirfrac = vec3( 186 | 1 / dir.x, 187 | 1 / dir.y, 188 | 1 / dir.z 189 | ) 190 | 191 | local t1 = (aabb.min.x - ray.position.x) * dirfrac.x 192 | local t2 = (aabb.max.x - ray.position.x) * dirfrac.x 193 | local t3 = (aabb.min.y - ray.position.y) * dirfrac.y 194 | local t4 = (aabb.max.y - ray.position.y) * dirfrac.y 195 | local t5 = (aabb.min.z - ray.position.z) * dirfrac.z 196 | local t6 = (aabb.max.z - ray.position.z) * dirfrac.z 197 | 198 | local tmin = max(max(min(t1, t2), min(t3, t4)), min(t5, t6)) 199 | local tmax = min(min(max(t1, t2), max(t3, t4)), max(t5, t6)) 200 | 201 | -- ray is intersecting AABB, but whole AABB is behind us 202 | if tmax < 0 then 203 | return false 204 | end 205 | 206 | -- ray does not intersect AABB 207 | if tmin > tmax then 208 | return false 209 | end 210 | 211 | -- Return collision point and distance from ray origin 212 | return ray.position + ray.direction * tmin, tmin 213 | end 214 | 215 | -- http://stackoverflow.com/a/23976134/1190664 216 | -- ray.position is a vec3 217 | -- ray.direction is a vec3 218 | -- plane.position is a vec3 219 | -- plane.normal is a vec3 220 | function intersect.ray_plane(ray, plane) 221 | local denom = plane.normal:dot(ray.direction) 222 | 223 | -- ray does not intersect plane 224 | if abs(denom) < DBL_EPSILON then 225 | return false 226 | end 227 | 228 | -- distance of direction 229 | local d = plane.position - ray.position 230 | local t = d:dot(plane.normal) / denom 231 | 232 | if t < DBL_EPSILON then 233 | return false 234 | end 235 | 236 | -- Return collision point and distance from ray origin 237 | return ray.position + ray.direction * t, t 238 | end 239 | 240 | function intersect.ray_capsule(ray, capsule) 241 | local dist2, p1, p2 = intersect.closest_point_segment_segment( 242 | ray.position, 243 | ray.position + ray.direction * 1e10, 244 | capsule.a, 245 | capsule.b 246 | ) 247 | if dist2 <= capsule.radius^2 then 248 | return p1 249 | end 250 | 251 | return false 252 | end 253 | 254 | -- https://web.archive.org/web/20120414063459/http://local.wasp.uwa.edu.au/~pbourke//geometry/lineline3d/ 255 | -- a[1] is a vec3 256 | -- a[2] is a vec3 257 | -- b[1] is a vec3 258 | -- b[2] is a vec3 259 | -- e is a number 260 | function intersect.line_line(a, b, e) 261 | -- new points 262 | local p13 = a[1] - b[1] 263 | local p43 = b[2] - b[1] 264 | local p21 = a[2] - a[1] 265 | 266 | -- if lengths are negative or too close to 0, lines do not intersect 267 | if p43:len2() < DBL_EPSILON or p21:len2() < DBL_EPSILON then 268 | return false 269 | end 270 | 271 | -- dot products 272 | local d1343 = p13:dot(p43) 273 | local d4321 = p43:dot(p21) 274 | local d1321 = p13:dot(p21) 275 | local d4343 = p43:dot(p43) 276 | local d2121 = p21:dot(p21) 277 | local denom = d2121 * d4343 - d4321 * d4321 278 | 279 | -- if denom is too close to 0, lines do not intersect 280 | if abs(denom) < DBL_EPSILON then 281 | return false 282 | end 283 | 284 | local numer = d1343 * d4321 - d1321 * d4343 285 | local mua = numer / denom 286 | local mub = (d1343 + d4321 * mua) / d4343 287 | 288 | -- return positions of intersection on each line 289 | local out1 = a[1] + p21 * mua 290 | local out2 = b[1] + p43 * mub 291 | local dist = out1:dist(out2) 292 | 293 | -- if distance of the shortest segment between lines is less than threshold 294 | if e and dist > e then 295 | return false 296 | end 297 | 298 | return { out1, out2 }, dist 299 | end 300 | 301 | -- a[1] is a vec3 302 | -- a[2] is a vec3 303 | -- b[1] is a vec3 304 | -- b[2] is a vec3 305 | -- e is a number 306 | function intersect.segment_segment(a, b, e) 307 | local c, d = intersect.line_line(a, b, e) 308 | 309 | if c and (( 310 | a[1].x <= c[1].x and 311 | a[1].y <= c[1].y and 312 | a[1].z <= c[1].z and 313 | c[1].x <= a[2].x and 314 | c[1].y <= a[2].y and 315 | c[1].z <= a[2].z 316 | ) or ( 317 | a[1].x >= c[1].x and 318 | a[1].y >= c[1].y and 319 | a[1].z >= c[1].z and 320 | c[1].x >= a[2].x and 321 | c[1].y >= a[2].y and 322 | c[1].z >= a[2].z 323 | )) and (( 324 | b[1].x <= c[2].x and 325 | b[1].y <= c[2].y and 326 | b[1].z <= c[2].z and 327 | c[2].x <= b[2].x and 328 | c[2].y <= b[2].y and 329 | c[2].z <= b[2].z 330 | ) or ( 331 | b[1].x >= c[2].x and 332 | b[1].y >= c[2].y and 333 | b[1].z >= c[2].z and 334 | c[2].x >= b[2].x and 335 | c[2].y >= b[2].y and 336 | c[2].z >= b[2].z 337 | )) then 338 | return c, d 339 | end 340 | 341 | -- segments do not intersect 342 | return false 343 | end 344 | 345 | -- a.min is a vec3 346 | -- a.max is a vec3 347 | -- b.min is a vec3 348 | -- b.max is a vec3 349 | function intersect.aabb_aabb(a, b) 350 | return 351 | a.min.x <= b.max.x and 352 | a.max.x >= b.min.x and 353 | a.min.y <= b.max.y and 354 | a.max.y >= b.min.y and 355 | a.min.z <= b.max.z and 356 | a.max.z >= b.min.z 357 | end 358 | 359 | -- aabb.position is a vec3 360 | -- aabb.extent is a vec3 (half-size) 361 | -- obb.position is a vec3 362 | -- obb.extent is a vec3 (half-size) 363 | -- obb.rotation is a mat4 364 | function intersect.aabb_obb(aabb, obb) 365 | local a = aabb.extent 366 | local b = obb.extent 367 | local T = obb.position - aabb.position 368 | local rot = mat4():transpose(obb.rotation) 369 | local B = {} 370 | local t 371 | 372 | for i = 1, 3 do 373 | B[i] = {} 374 | for j = 1, 3 do 375 | assert((i - 1) * 4 + j < 16 and (i - 1) * 4 + j > 0) 376 | B[i][j] = abs(rot[(i - 1) * 4 + j]) + 1e-6 377 | end 378 | end 379 | 380 | t = abs(T.x) 381 | if not (t <= (b.x + a.x * B[1][1] + b.y * B[1][2] + b.z * B[1][3])) then return false end 382 | t = abs(T.x * B[1][1] + T.y * B[2][1] + T.z * B[3][1]) 383 | if not (t <= (b.x + a.x * B[1][1] + a.y * B[2][1] + a.z * B[3][1])) then return false end 384 | t = abs(T.y) 385 | if not (t <= (a.y + b.x * B[2][1] + b.y * B[2][2] + b.z * B[2][3])) then return false end 386 | t = abs(T.z) 387 | if not (t <= (a.z + b.x * B[3][1] + b.y * B[3][2] + b.z * B[3][3])) then return false end 388 | t = abs(T.x * B[1][2] + T.y * B[2][2] + T.z * B[3][2]) 389 | if not (t <= (b.y + a.x * B[1][2] + a.y * B[2][2] + a.z * B[3][2])) then return false end 390 | t = abs(T.x * B[1][3] + T.y * B[2][3] + T.z * B[3][3]) 391 | if not (t <= (b.z + a.x * B[1][3] + a.y * B[2][3] + a.z * B[3][3])) then return false end 392 | t = abs(T.z * B[2][1] - T.y * B[3][1]) 393 | if not (t <= (a.y * B[3][1] + a.z * B[2][1] + b.y * B[1][3] + b.z * B[1][2])) then return false end 394 | t = abs(T.z * B[2][2] - T.y * B[3][2]) 395 | if not (t <= (a.y * B[3][2] + a.z * B[2][2] + b.x * B[1][3] + b.z * B[1][1])) then return false end 396 | t = abs(T.z * B[2][3] - T.y * B[3][3]) 397 | if not (t <= (a.y * B[3][3] + a.z * B[2][3] + b.x * B[1][2] + b.y * B[1][1])) then return false end 398 | t = abs(T.x * B[3][1] - T.z * B[1][1]) 399 | if not (t <= (a.x * B[3][1] + a.z * B[1][1] + b.y * B[2][3] + b.z * B[2][2])) then return false end 400 | t = abs(T.x * B[3][2] - T.z * B[1][2]) 401 | if not (t <= (a.x * B[3][2] + a.z * B[1][2] + b.x * B[2][3] + b.z * B[2][1])) then return false end 402 | t = abs(T.x * B[3][3] - T.z * B[1][3]) 403 | if not (t <= (a.x * B[3][3] + a.z * B[1][3] + b.x * B[2][2] + b.y * B[2][1])) then return false end 404 | t = abs(T.y * B[1][1] - T.x * B[2][1]) 405 | if not (t <= (a.x * B[2][1] + a.y * B[1][1] + b.y * B[3][3] + b.z * B[3][2])) then return false end 406 | t = abs(T.y * B[1][2] - T.x * B[2][2]) 407 | if not (t <= (a.x * B[2][2] + a.y * B[1][2] + b.x * B[3][3] + b.z * B[3][1])) then return false end 408 | t = abs(T.y * B[1][3] - T.x * B[2][3]) 409 | if not (t <= (a.x * B[2][3] + a.y * B[1][3] + b.x * B[3][2] + b.y * B[3][1])) then return false end 410 | 411 | -- https://gamedev.stackexchange.com/questions/24078/which-side-was-hit 412 | -- Minkowski Sum 413 | local wy = (aabb.extent * 2 + obb.extent * 2) * (aabb.position.y - obb.position.y) 414 | local hx = (aabb.extent * 2 + obb.extent * 2) * (aabb.position.x - obb.position.x) 415 | 416 | if wy.x > hx.x and wy.y > hx.y and wy.z > hx.z then 417 | if wy.x > -hx.x and wy.y > -hx.y and wy.z > -hx.z then 418 | return vec3(obb.rotation * { 0, -1, 0, 1 }) 419 | else 420 | return vec3(obb.rotation * { -1, 0, 0, 1 }) 421 | end 422 | else 423 | if wy.x > -hx.x and wy.y > -hx.y and wy.z > -hx.z then 424 | return vec3(obb.rotation * { 1, 0, 0, 1 }) 425 | else 426 | return vec3(obb.rotation * { 0, 1, 0, 1 }) 427 | end 428 | end 429 | end 430 | 431 | -- http://stackoverflow.com/a/4579069/1190664 432 | -- aabb.min is a vec3 433 | -- aabb.max is a vec3 434 | -- sphere.position is a vec3 435 | -- sphere.radius is a number 436 | local axes = { "x", "y", "z" } 437 | function intersect.aabb_sphere(aabb, sphere) 438 | local dist2 = sphere.radius ^ 2 439 | 440 | for _, axis in ipairs(axes) do 441 | local pos = sphere.position[axis] 442 | local amin = aabb.min[axis] 443 | local amax = aabb.max[axis] 444 | 445 | if pos < amin then 446 | dist2 = dist2 - (pos - amin) ^ 2 447 | elseif pos > amax then 448 | dist2 = dist2 - (pos - amax) ^ 2 449 | end 450 | end 451 | 452 | return dist2 > 0 453 | end 454 | 455 | -- aabb.min is a vec3 456 | -- aabb.max is a vec3 457 | -- frustum.left is a plane { a, b, c, d } 458 | -- frustum.right is a plane { a, b, c, d } 459 | -- frustum.bottom is a plane { a, b, c, d } 460 | -- frustum.top is a plane { a, b, c, d } 461 | -- frustum.near is a plane { a, b, c, d } 462 | -- frustum.far is a plane { a, b, c, d } 463 | function intersect.aabb_frustum(aabb, frustum) 464 | -- Indexed for the 'index trick' later 465 | local box = { 466 | aabb.min, 467 | aabb.max 468 | } 469 | 470 | -- We have 6 planes defining the frustum, 5 if infinite. 471 | local planes = { 472 | frustum.left, 473 | frustum.right, 474 | frustum.bottom, 475 | frustum.top, 476 | frustum.near, 477 | frustum.far or false 478 | } 479 | 480 | -- Skip the last test for infinite projections, it'll never fail. 481 | if not planes[6] then 482 | table.remove(planes) 483 | end 484 | 485 | for i = 1, #planes do 486 | -- This is the current plane 487 | local p = planes[i] 488 | 489 | -- p-vertex selection (with the index trick) 490 | -- According to the plane normal we can know the 491 | -- indices of the positive vertex 492 | local px = p.a > 0.0 and 2 or 1 493 | local py = p.b > 0.0 and 2 or 1 494 | local pz = p.c > 0.0 and 2 or 1 495 | 496 | -- project p-vertex on plane normal 497 | -- (How far is p-vertex from the origin) 498 | local dot = (p.a * box[px].x) + (p.b * box[py].y) + (p.c * box[pz].z) 499 | 500 | -- Doesn't intersect if it is behind the plane 501 | if dot < -p.d then 502 | return false 503 | end 504 | end 505 | 506 | return true 507 | end 508 | 509 | -- outer.min is a vec3 510 | -- outer.max is a vec3 511 | -- inner.min is a vec3 512 | -- inner.max is a vec3 513 | function intersect.encapsulate_aabb(outer, inner) 514 | return 515 | outer.min.x <= inner.min.x and 516 | outer.max.x >= inner.max.x and 517 | outer.min.y <= inner.min.y and 518 | outer.max.y >= inner.max.y and 519 | outer.min.z <= inner.min.z and 520 | outer.max.z >= inner.max.z 521 | end 522 | 523 | -- a.position is a vec3 524 | -- a.radius is a number 525 | -- b.position is a vec3 526 | -- b.radius is a number 527 | function intersect.circle_circle(a, b) 528 | return a.position:dist(b.position) <= a.radius + b.radius 529 | end 530 | 531 | -- a.position is a vec3 532 | -- a.radius is a number 533 | -- b.position is a vec3 534 | -- b.radius is a number 535 | function intersect.sphere_sphere(a, b) 536 | return intersect.circle_circle(a, b) 537 | end 538 | 539 | -- http://realtimecollisiondetection.net/blog/?p=103 540 | -- sphere.position is a vec3 541 | -- sphere.radius is a number 542 | -- triangle[1] is a vec3 543 | -- triangle[2] is a vec3 544 | -- triangle[3] is a vec3 545 | function intersect.sphere_triangle(sphere, triangle) 546 | -- Sphere is centered at origin 547 | local A = triangle[1] - sphere.position 548 | local B = triangle[2] - sphere.position 549 | local C = triangle[3] - sphere.position 550 | 551 | -- Compute normal of triangle plane 552 | local V = (B - A):cross(C - A) 553 | 554 | -- Test if sphere lies outside triangle plane 555 | local rr = sphere.radius * sphere.radius 556 | local d = A:dot(V) 557 | local e = V:dot(V) 558 | local s1 = d * d > rr * e 559 | 560 | -- Test if sphere lies outside triangle vertices 561 | local aa = A:dot(A) 562 | local ab = A:dot(B) 563 | local ac = A:dot(C) 564 | local bb = B:dot(B) 565 | local bc = B:dot(C) 566 | local cc = C:dot(C) 567 | 568 | local s2 = (aa > rr) and (ab > aa) and (ac > aa) 569 | local s3 = (bb > rr) and (ab > bb) and (bc > bb) 570 | local s4 = (cc > rr) and (ac > cc) and (bc > cc) 571 | 572 | -- Test is sphere lies outside triangle edges 573 | local AB = B - A 574 | local BC = C - B 575 | local CA = A - C 576 | 577 | local d1 = ab - aa 578 | local d2 = bc - bb 579 | local d3 = ac - cc 580 | 581 | local e1 = AB:dot(AB) 582 | local e2 = BC:dot(BC) 583 | local e3 = CA:dot(CA) 584 | 585 | local Q1 = A * e1 - AB * d1 586 | local Q2 = B * e2 - BC * d2 587 | local Q3 = C * e3 - CA * d3 588 | 589 | local QC = C * e1 - Q1 590 | local QA = A * e2 - Q2 591 | local QB = B * e3 - Q3 592 | 593 | local s5 = (Q1:dot(Q1) > rr * e1 * e1) and (Q1:dot(QC) > 0) 594 | local s6 = (Q2:dot(Q2) > rr * e2 * e2) and (Q2:dot(QA) > 0) 595 | local s7 = (Q3:dot(Q3) > rr * e3 * e3) and (Q3:dot(QB) > 0) 596 | 597 | -- Return whether or not any of the tests passed 598 | return s1 or s2 or s3 or s4 or s5 or s6 or s7 599 | end 600 | 601 | -- sphere.position is a vec3 602 | -- sphere.radius is a number 603 | -- frustum.left is a plane { a, b, c, d } 604 | -- frustum.right is a plane { a, b, c, d } 605 | -- frustum.bottom is a plane { a, b, c, d } 606 | -- frustum.top is a plane { a, b, c, d } 607 | -- frustum.near is a plane { a, b, c, d } 608 | -- frustum.far is a plane { a, b, c, d } 609 | function intersect.sphere_frustum(sphere, frustum) 610 | local x, y, z = sphere.position:unpack() 611 | local planes = { 612 | frustum.left, 613 | frustum.right, 614 | frustum.bottom, 615 | frustum.top, 616 | frustum.near 617 | } 618 | 619 | if frustum.far then 620 | table.insert(planes, frustum.far, 5) 621 | end 622 | 623 | local dot 624 | for i = 1, #planes do 625 | dot = planes[i].a * x + planes[i].b * y + planes[i].c * z + planes[i].d 626 | 627 | if dot <= -sphere.radius then 628 | return false 629 | end 630 | end 631 | 632 | -- dot + radius is the distance of the object from the near plane. 633 | -- make sure that the near plane is the last test! 634 | return dot + sphere.radius 635 | end 636 | 637 | function intersect.capsule_capsule(c1, c2) 638 | local dist2, p1, p2 = intersect.closest_point_segment_segment(c1.a, c1.b, c2.a, c2.b) 639 | local radius = c1.radius + c2.radius 640 | 641 | if dist2 <= radius * radius then 642 | return p1, p2 643 | end 644 | 645 | return false 646 | end 647 | 648 | function intersect.closest_point_segment_segment(p1, p2, p3, p4) 649 | local s -- Distance of intersection along segment 1 650 | local t -- Distance of intersection along segment 2 651 | local c1 -- Collision point on segment 1 652 | local c2 -- Collision point on segment 2 653 | 654 | local d1 = p2 - p1 -- Direction of segment 1 655 | local d2 = p4 - p3 -- Direction of segment 2 656 | local r = p1 - p3 657 | local a = d1:dot(d1) 658 | local e = d2:dot(d2) 659 | local f = d2:dot(r) 660 | 661 | -- Check if both segments degenerate into points 662 | if a <= DBL_EPSILON and e <= DBL_EPSILON then 663 | s = 0 664 | t = 0 665 | c1 = p1 666 | c2 = p3 667 | return (c1 - c2):dot(c1 - c2), s, t, c1, c2 668 | end 669 | 670 | -- Check if segment 1 degenerates into a point 671 | if a <= DBL_EPSILON then 672 | s = 0 673 | t = utils.clamp(f / e, 0, 1) 674 | else 675 | local c = d1:dot(r) 676 | 677 | -- Check is segment 2 degenerates into a point 678 | if e <= DBL_EPSILON then 679 | t = 0 680 | s = utils.clamp(-c / a, 0, 1) 681 | else 682 | local b = d1:dot(d2) 683 | local denom = a * e - b * b 684 | 685 | if abs(denom) > 0 then 686 | s = utils.clamp((b * f - c * e) / denom, 0, 1) 687 | else 688 | s = 0 689 | end 690 | 691 | t = (b * s + f) / e 692 | 693 | if t < 0 then 694 | t = 0 695 | s = utils.clamp(-c / a, 0, 1) 696 | elseif t > 1 then 697 | t = 1 698 | s = utils.clamp((b - c) / a, 0, 1) 699 | end 700 | end 701 | end 702 | 703 | c1 = p1 + d1 * s 704 | c2 = p3 + d2 * t 705 | 706 | return (c1 - c2):dot(c1 - c2), c1, c2, s, t 707 | end 708 | 709 | return intersect 710 | -------------------------------------------------------------------------------- /modules/mesh.lua: -------------------------------------------------------------------------------- 1 | --- Mesh utilities 2 | -- @module mesh 3 | 4 | local modules = (...):gsub('%.[^%.]+$', '') .. "." 5 | local vec3 = require(modules .. "vec3") 6 | local mesh = {} 7 | 8 | -- vertices is an arbitrary list of vec3s 9 | function mesh.average(vertices) 10 | local out = vec3() 11 | for _, v in ipairs(vertices) do 12 | out = out + v 13 | end 14 | return out / #vertices 15 | end 16 | 17 | -- triangle[1] is a vec3 18 | -- triangle[2] is a vec3 19 | -- triangle[3] is a vec3 20 | function mesh.normal(triangle) 21 | local ba = triangle[2] - triangle[1] 22 | local ca = triangle[3] - triangle[1] 23 | return ba:cross(ca):normalize() 24 | end 25 | 26 | -- triangle[1] is a vec3 27 | -- triangle[2] is a vec3 28 | -- triangle[3] is a vec3 29 | function mesh.plane_from_triangle(triangle) 30 | return { 31 | origin = triangle[1], 32 | normal = mesh.normal(triangle) 33 | } 34 | end 35 | 36 | -- plane.origin is a vec3 37 | -- plane.normal is a vec3 38 | -- direction is a vec3 39 | function mesh.is_front_facing(plane, direction) 40 | return plane.normal:dot(direction) >= 0 41 | end 42 | 43 | -- point is a vec3 44 | -- plane.origin is a vec3 45 | -- plane.normal is a vec3 46 | -- plane.dot is a number 47 | function mesh.signed_distance(point, plane) 48 | return point:dot(plane.normal) - plane.normal:dot(plane.origin) 49 | end 50 | 51 | return mesh 52 | -------------------------------------------------------------------------------- /modules/quat.lua: -------------------------------------------------------------------------------- 1 | --- A quaternion and associated utilities. 2 | -- @module quat 3 | 4 | local modules = (...):gsub('%.[^%.]+$', '') .. "." 5 | local constants = require(modules .. "constants") 6 | local vec3 = require(modules .. "vec3") 7 | local precond = require(modules .. "_private_precond") 8 | local private = require(modules .. "_private_utils") 9 | local DOT_THRESHOLD = constants.DOT_THRESHOLD 10 | local DBL_EPSILON = constants.DBL_EPSILON 11 | local acos = math.acos 12 | local cos = math.cos 13 | local sin = math.sin 14 | local min = math.min 15 | local max = math.max 16 | local sqrt = math.sqrt 17 | local quat = {} 18 | local quat_mt = {} 19 | 20 | -- Private constructor. 21 | local function new(x, y, z, w) 22 | return setmetatable({ 23 | x = x or 0, 24 | y = y or 0, 25 | z = z or 0, 26 | w = w or 1 27 | }, quat_mt) 28 | end 29 | 30 | -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. 31 | local status, ffi 32 | if type(jit) == "table" and jit.status() then 33 | status, ffi = pcall(require, "ffi") 34 | if status then 35 | ffi.cdef "typedef struct { double x, y, z, w;} cpml_quat;" 36 | new = ffi.typeof("cpml_quat") 37 | end 38 | end 39 | 40 | -- Statically allocate a temporary variable used in some of our functions. 41 | local tmp = new() 42 | local qv, uv, uuv = vec3(), vec3(), vec3() 43 | 44 | --- Constants 45 | -- @table quat 46 | -- @field unit Unit quaternion 47 | -- @field zero Empty quaternion 48 | quat.unit = new(0, 0, 0, 1) 49 | quat.zero = new(0, 0, 0, 0) 50 | 51 | --- The public constructor. 52 | -- @param x Can be of two types:
53 | -- number x X component 54 | -- table {x, y, z, w} or {x=x, y=y, z=z, w=w} 55 | -- @tparam number y Y component 56 | -- @tparam number z Z component 57 | -- @tparam number w W component 58 | -- @treturn quat out 59 | function quat.new(x, y, z, w) 60 | -- number, number, number, number 61 | if x and y and z and w then 62 | precond.typeof(x, "number", "new: Wrong argument type for x") 63 | precond.typeof(y, "number", "new: Wrong argument type for y") 64 | precond.typeof(z, "number", "new: Wrong argument type for z") 65 | precond.typeof(w, "number", "new: Wrong argument type for w") 66 | 67 | return new(x, y, z, w) 68 | 69 | -- {x, y, z, w} or {x=x, y=y, z=z, w=w} 70 | elseif type(x) == "table" then 71 | local xx, yy, zz, ww = x.x or x[1], x.y or x[2], x.z or x[3], x.w or x[4] 72 | precond.typeof(xx, "number", "new: Wrong argument type for x") 73 | precond.typeof(yy, "number", "new: Wrong argument type for y") 74 | precond.typeof(zz, "number", "new: Wrong argument type for z") 75 | precond.typeof(ww, "number", "new: Wrong argument type for w") 76 | 77 | return new(xx, yy, zz, ww) 78 | end 79 | 80 | return new(0, 0, 0, 1) 81 | end 82 | 83 | --- Create a quaternion from an angle/axis pair. 84 | -- @tparam number angle Angle (in radians) 85 | -- @param axis/x -- Can be of two types, a vec3 axis, or the x component of that axis 86 | -- @param y axis -- y component of axis (optional, only if x component param used) 87 | -- @param z axis -- z component of axis (optional, only if x component param used) 88 | -- @treturn quat out 89 | function quat.from_angle_axis(angle, axis, a3, a4) 90 | if axis and a3 and a4 then 91 | local x, y, z = axis, a3, a4 92 | local s = sin(angle * 0.5) 93 | local c = cos(angle * 0.5) 94 | return new(x * s, y * s, z * s, c) 95 | else 96 | return quat.from_angle_axis(angle, axis.x, axis.y, axis.z) 97 | end 98 | end 99 | 100 | --- Create a quaternion from a normal/up vector pair. 101 | -- @tparam vec3 normal 102 | -- @tparam vec3 up (optional) 103 | -- @treturn quat out 104 | function quat.from_direction(normal, up) 105 | local u = up or vec3.unit_z 106 | local n = normal:normalize() 107 | local a = u:cross(n) 108 | local d = u:dot(n) 109 | return new(a.x, a.y, a.z, d + 1) 110 | end 111 | 112 | --- Clone a quaternion. 113 | -- @tparam quat a Quaternion to clone 114 | -- @treturn quat out 115 | function quat.clone(a) 116 | return new(a.x, a.y, a.z, a.w) 117 | end 118 | 119 | --- Add two quaternions. 120 | -- @tparam quat a Left hand operand 121 | -- @tparam quat b Right hand operand 122 | -- @treturn quat out 123 | function quat.add(a, b) 124 | return new( 125 | a.x + b.x, 126 | a.y + b.y, 127 | a.z + b.z, 128 | a.w + b.w 129 | ) 130 | end 131 | 132 | --- Subtract a quaternion from another. 133 | -- @tparam quat a Left hand operand 134 | -- @tparam quat b Right hand operand 135 | -- @treturn quat out 136 | function quat.sub(a, b) 137 | return new( 138 | a.x - b.x, 139 | a.y - b.y, 140 | a.z - b.z, 141 | a.w - b.w 142 | ) 143 | end 144 | 145 | --- Multiply two quaternions. 146 | -- @tparam quat a Left hand operand 147 | -- @tparam quat b Right hand operand 148 | -- @treturn quat quaternion equivalent to "apply b, then a" 149 | function quat.mul(a, b) 150 | return new( 151 | a.x * b.w + a.w * b.x + a.y * b.z - a.z * b.y, 152 | a.y * b.w + a.w * b.y + a.z * b.x - a.x * b.z, 153 | a.z * b.w + a.w * b.z + a.x * b.y - a.y * b.x, 154 | a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z 155 | ) 156 | end 157 | 158 | --- Multiply a quaternion and a vec3. 159 | -- @tparam quat a Left hand operand 160 | -- @tparam vec3 b Right hand operand 161 | -- @treturn vec3 out 162 | function quat.mul_vec3(a, b) 163 | qv.x = a.x 164 | qv.y = a.y 165 | qv.z = a.z 166 | uv = qv:cross(b) 167 | uuv = qv:cross(uv) 168 | return b + ((uv * a.w) + uuv) * 2 169 | end 170 | 171 | --- Raise a normalized quaternion to a scalar power. 172 | -- @tparam quat a Left hand operand (should be a unit quaternion) 173 | -- @tparam number s Right hand operand 174 | -- @treturn quat out 175 | function quat.pow(a, s) 176 | -- Do it as a slerp between identity and a (code borrowed from slerp) 177 | if a.w < 0 then 178 | a = -a 179 | end 180 | local dot = a.w 181 | 182 | dot = min(max(dot, -1), 1) 183 | 184 | local theta = acos(dot) * s 185 | local c = new(a.x, a.y, a.z, 0):normalize() * sin(theta) 186 | c.w = cos(theta) 187 | return c 188 | end 189 | 190 | --- Normalize a quaternion. 191 | -- @tparam quat a Quaternion to normalize 192 | -- @treturn quat out 193 | function quat.normalize(a) 194 | if a:is_zero() then 195 | return new(0, 0, 0, 0) 196 | end 197 | return a:scale(1 / a:len()) 198 | end 199 | 200 | --- Get the dot product of two quaternions. 201 | -- @tparam quat a Left hand operand 202 | -- @tparam quat b Right hand operand 203 | -- @treturn number dot 204 | function quat.dot(a, b) 205 | return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w 206 | end 207 | 208 | --- Return the length of a quaternion. 209 | -- @tparam quat a Quaternion to get length of 210 | -- @treturn number len 211 | function quat.len(a) 212 | return sqrt(a.x * a.x + a.y * a.y + a.z * a.z + a.w * a.w) 213 | end 214 | 215 | --- Return the squared length of a quaternion. 216 | -- @tparam quat a Quaternion to get length of 217 | -- @treturn number len 218 | function quat.len2(a) 219 | return a.x * a.x + a.y * a.y + a.z * a.z + a.w * a.w 220 | end 221 | 222 | --- Multiply a quaternion by a scalar. 223 | -- @tparam quat a Left hand operand 224 | -- @tparam number s Right hand operand 225 | -- @treturn quat out 226 | function quat.scale(a, s) 227 | return new( 228 | a.x * s, 229 | a.y * s, 230 | a.z * s, 231 | a.w * s 232 | ) 233 | end 234 | 235 | --- Alias of from_angle_axis. 236 | -- @tparam number angle Angle (in radians) 237 | -- @param axis/x -- Can be of two types, a vec3 axis, or the x component of that axis 238 | -- @param y axis -- y component of axis (optional, only if x component param used) 239 | -- @param z axis -- z component of axis (optional, only if x component param used) 240 | -- @treturn quat out 241 | function quat.rotate(angle, axis, a3, a4) 242 | return quat.from_angle_axis(angle, axis, a3, a4) 243 | end 244 | 245 | --- Return the conjugate of a quaternion. 246 | -- @tparam quat a Quaternion to conjugate 247 | -- @treturn quat out 248 | function quat.conjugate(a) 249 | return new(-a.x, -a.y, -a.z, a.w) 250 | end 251 | 252 | --- Return the inverse of a quaternion. 253 | -- @tparam quat a Quaternion to invert 254 | -- @treturn quat out 255 | function quat.inverse(a) 256 | tmp.x = -a.x 257 | tmp.y = -a.y 258 | tmp.z = -a.z 259 | tmp.w = a.w 260 | return tmp:normalize() 261 | end 262 | 263 | --- Return the reciprocal of a quaternion. 264 | -- @tparam quat a Quaternion to reciprocate 265 | -- @treturn quat out 266 | function quat.reciprocal(a) 267 | if a:is_zero() then 268 | error("Cannot reciprocate a zero quaternion") 269 | return false 270 | end 271 | 272 | tmp.x = -a.x 273 | tmp.y = -a.y 274 | tmp.z = -a.z 275 | tmp.w = a.w 276 | 277 | return tmp:scale(1 / a:len2()) 278 | end 279 | 280 | --- Lerp between two quaternions. 281 | -- @tparam quat a Left hand operand 282 | -- @tparam quat b Right hand operand 283 | -- @tparam number s Step value 284 | -- @treturn quat out 285 | function quat.lerp(a, b, s) 286 | return (a + (b - a) * s):normalize() 287 | end 288 | 289 | --- Slerp between two quaternions. 290 | -- @tparam quat a Left hand operand 291 | -- @tparam quat b Right hand operand 292 | -- @tparam number s Step value 293 | -- @treturn quat out 294 | function quat.slerp(a, b, s) 295 | local dot = a:dot(b) 296 | 297 | if dot < 0 then 298 | a = -a 299 | dot = -dot 300 | end 301 | 302 | if dot > DOT_THRESHOLD then 303 | return a:lerp(b, s) 304 | end 305 | 306 | dot = min(max(dot, -1), 1) 307 | 308 | local theta = acos(dot) * s 309 | local c = (b - a * dot):normalize() 310 | return a * cos(theta) + c * sin(theta) 311 | end 312 | 313 | --- Unpack a quaternion into individual components. 314 | -- @tparam quat a Quaternion to unpack 315 | -- @treturn number x 316 | -- @treturn number y 317 | -- @treturn number z 318 | -- @treturn number w 319 | function quat.unpack(a) 320 | return a.x, a.y, a.z, a.w 321 | end 322 | 323 | --- Return a boolean showing if a table is or is not a quat. 324 | -- @tparam quat a Quaternion to be tested 325 | -- @treturn boolean is_quat 326 | function quat.is_quat(a) 327 | if type(a) == "cdata" then 328 | return ffi.istype("cpml_quat", a) 329 | end 330 | 331 | return 332 | type(a) == "table" and 333 | type(a.x) == "number" and 334 | type(a.y) == "number" and 335 | type(a.z) == "number" and 336 | type(a.w) == "number" 337 | end 338 | 339 | --- Return a boolean showing if a table is or is not a zero quat. 340 | -- @tparam quat a Quaternion to be tested 341 | -- @treturn boolean is_zero 342 | function quat.is_zero(a) 343 | return 344 | a.x == 0 and 345 | a.y == 0 and 346 | a.z == 0 and 347 | a.w == 0 348 | end 349 | 350 | --- Return a boolean showing if a table is or is not a real quat. 351 | -- @tparam quat a Quaternion to be tested 352 | -- @treturn boolean is_real 353 | function quat.is_real(a) 354 | return 355 | a.x == 0 and 356 | a.y == 0 and 357 | a.z == 0 358 | end 359 | 360 | --- Return a boolean showing if a table is or is not an imaginary quat. 361 | -- @tparam quat a Quaternion to be tested 362 | -- @treturn boolean is_imaginary 363 | function quat.is_imaginary(a) 364 | return a.w == 0 365 | end 366 | 367 | --- Return whether any component is NaN 368 | -- @tparam quat a Quaternion to be tested 369 | -- @treturn boolean if x,y,z, or w is NaN 370 | function quat.has_nan(a) 371 | return private.is_nan(a.x) or 372 | private.is_nan(a.y) or 373 | private.is_nan(a.z) or 374 | private.is_nan(a.w) 375 | end 376 | 377 | --- Convert a quaternion into an angle plus axis components. 378 | -- @tparam quat a Quaternion to convert 379 | -- @tparam identityAxis vec3 of axis to use on identity/degenerate quaternions (optional, default returns 0,0,0,1) 380 | -- @treturn number angle 381 | -- @treturn x axis-x 382 | -- @treturn y axis-y 383 | -- @treturn z axis-z 384 | function quat.to_angle_axis_unpack(a, identityAxis) 385 | if a.w > 1 or a.w < -1 then 386 | a = a:normalize() 387 | end 388 | 389 | -- If length of xyz components is less than DBL_EPSILON, this is zero or close enough (an identity quaternion) 390 | -- Normally an identity quat would return a nonsense answer, so we return an arbitrary zero rotation early. 391 | -- FIXME: Is it safe to assume there are *no* valid quaternions with nonzero degenerate lengths? 392 | if a.x*a.x + a.y*a.y + a.z*a.z < constants.DBL_EPSILON*constants.DBL_EPSILON then 393 | if identityAxis then 394 | return 0,identityAxis:unpack() 395 | else 396 | return 0,0,0,1 397 | end 398 | end 399 | 400 | local x, y, z 401 | local angle = 2 * acos(a.w) 402 | local s = sqrt(1 - a.w * a.w) 403 | 404 | if s < DBL_EPSILON then 405 | x = a.x 406 | y = a.y 407 | z = a.z 408 | else 409 | x = a.x / s 410 | y = a.y / s 411 | z = a.z / s 412 | end 413 | 414 | return angle, x, y, z 415 | end 416 | 417 | --- Convert a quaternion into an angle/axis pair. 418 | -- @tparam quat a Quaternion to convert 419 | -- @tparam identityAxis vec3 of axis to use on identity/degenerate quaternions (optional, default returns 0,vec3(0,0,1)) 420 | -- @treturn number angle 421 | -- @treturn vec3 axis 422 | function quat.to_angle_axis(a, identityAxis) 423 | local angle, x, y, z = a:to_angle_axis_unpack(identityAxis) 424 | return angle, vec3(x, y, z) 425 | end 426 | 427 | --- Convert a quaternion into a vec3. 428 | -- @tparam quat a Quaternion to convert 429 | -- @treturn vec3 out 430 | function quat.to_vec3(a) 431 | return vec3(a.x, a.y, a.z) 432 | end 433 | 434 | --- Return a formatted string. 435 | -- @tparam quat a Quaternion to be turned into a string 436 | -- @treturn string formatted 437 | function quat.to_string(a) 438 | return string.format("(%+0.3f,%+0.3f,%+0.3f,%+0.3f)", a.x, a.y, a.z, a.w) 439 | end 440 | 441 | quat_mt.__index = quat 442 | quat_mt.__tostring = quat.to_string 443 | 444 | function quat_mt.__call(_, x, y, z, w) 445 | return quat.new(x, y, z, w) 446 | end 447 | 448 | function quat_mt.__unm(a) 449 | return a:scale(-1) 450 | end 451 | 452 | function quat_mt.__eq(a,b) 453 | if not quat.is_quat(a) or not quat.is_quat(b) then 454 | return false 455 | end 456 | return a.x == b.x and a.y == b.y and a.z == b.z and a.w == b.w 457 | end 458 | 459 | function quat_mt.__add(a, b) 460 | precond.assert(quat.is_quat(a), "__add: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 461 | precond.assert(quat.is_quat(b), "__add: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) 462 | return a:add(b) 463 | end 464 | 465 | function quat_mt.__sub(a, b) 466 | precond.assert(quat.is_quat(a), "__sub: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 467 | precond.assert(quat.is_quat(b), "__sub: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) 468 | return a:sub(b) 469 | end 470 | 471 | function quat_mt.__mul(a, b) 472 | precond.assert(quat.is_quat(a), "__mul: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 473 | assert(quat.is_quat(b) or vec3.is_vec3(b) or type(b) == "number", "__mul: Wrong argument type for right hand operand. ( or or expected)") 474 | 475 | if quat.is_quat(b) then 476 | return a:mul(b) 477 | end 478 | 479 | if type(b) == "number" then 480 | return a:scale(b) 481 | end 482 | 483 | return a:mul_vec3(b) 484 | end 485 | 486 | function quat_mt.__pow(a, n) 487 | precond.assert(quat.is_quat(a), "__pow: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 488 | precond.typeof(n, "number", "__pow: Wrong argument type for right hand operand.") 489 | return a:pow(n) 490 | end 491 | 492 | if status then 493 | xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded 494 | ffi.metatype(new, quat_mt) 495 | end, function() end) 496 | end 497 | 498 | return setmetatable({}, quat_mt) 499 | -------------------------------------------------------------------------------- /modules/simplex.lua: -------------------------------------------------------------------------------- 1 | --- Simplex Noise 2 | -- @module simplex 3 | 4 | -- 5 | -- Based on code in "Simplex noise demystified", by Stefan Gustavson 6 | -- www.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf 7 | -- 8 | -- Thanks to Mike Pall for some cleanup and improvements (and for LuaJIT!) 9 | -- 10 | -- Permission is hereby granted, free of charge, to any person obtaining 11 | -- a copy of this software and associated documentation files (the 12 | -- "Software"), to deal in the Software without restriction, including 13 | -- without limitation the rights to use, copy, modify, merge, publish, 14 | -- distribute, sublicense, and/or sell copies of the Software, and to 15 | -- permit persons to whom the Software is furnished to do so, subject to 16 | -- the following conditions: 17 | -- 18 | -- The above copyright notice and this permission notice shall be 19 | -- included in all copies or substantial portions of the Software. 20 | -- 21 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | -- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | -- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | -- 29 | -- [ MIT license: http://www.opensource.org/licenses/mit-license.php ] 30 | -- 31 | 32 | if _G.love and _G.love.math then 33 | return love.math.noise 34 | end 35 | 36 | -- Bail out with dummy module if FFI is missing. 37 | local has_ffi, ffi = pcall(require, "ffi") 38 | if not has_ffi then 39 | return function() 40 | return 0 41 | end 42 | end 43 | 44 | -- Modules -- 45 | local bit = require("bit") 46 | 47 | -- Imports -- 48 | local band = bit.band 49 | local bor = bit.bor 50 | local floor = math.floor 51 | local lshift = bit.lshift 52 | local max = math.max 53 | local rshift = bit.rshift 54 | 55 | -- Permutation of 0-255, replicated to allow easy indexing with sums of two bytes -- 56 | local Perms = ffi.new("uint8_t[512]", { 57 | 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 58 | 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 59 | 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 60 | 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 61 | 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 62 | 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 63 | 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 64 | 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 65 | 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 66 | 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 67 | 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 68 | 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 69 | 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 70 | 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 71 | 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 72 | 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 73 | }) 74 | 75 | -- The above, mod 12 for each element -- 76 | local Perms12 = ffi.new("uint8_t[512]") 77 | 78 | for i = 0, 255 do 79 | local x = Perms[i] % 12 80 | 81 | Perms[i + 256], Perms12[i], Perms12[i + 256] = Perms[i], x, x 82 | end 83 | 84 | -- Gradients for 2D, 3D case -- 85 | local Grads3 = ffi.new("const double[12][3]", 86 | { 1, 1, 0 }, { -1, 1, 0 }, { 1, -1, 0 }, { -1, -1, 0 }, 87 | { 1, 0, 1 }, { -1, 0, 1 }, { 1, 0, -1 }, { -1, 0, -1 }, 88 | { 0, 1, 1 }, { 0, -1, 1 }, { 0, 1, -1 }, { 0, -1, -1 } 89 | ) 90 | 91 | -- 2D weight contribution 92 | local function GetN2(bx, by, x, y) 93 | local t = .5 - x * x - y * y 94 | local index = Perms12[bx + Perms[by]] 95 | 96 | return max(0, (t * t) * (t * t)) * (Grads3[index][0] * x + Grads3[index][1] * y) 97 | end 98 | 99 | local function simplex_2d(x, y) 100 | --[[ 101 | 2D skew factors: 102 | F = (math.sqrt(3) - 1) / 2 103 | G = (3 - math.sqrt(3)) / 6 104 | G2 = 2 * G - 1 105 | ]] 106 | 107 | -- Skew the input space to determine which simplex cell we are in. 108 | local s = (x + y) * 0.366025403 -- F 109 | local ix, iy = floor(x + s), floor(y + s) 110 | 111 | -- Unskew the cell origin back to (x, y) space. 112 | local t = (ix + iy) * 0.211324865 -- G 113 | local x0 = x + t - ix 114 | local y0 = y + t - iy 115 | 116 | -- Calculate the contribution from the two fixed corners. 117 | -- A step of (1,0) in (i,j) means a step of (1-G,-G) in (x,y), and 118 | -- A step of (0,1) in (i,j) means a step of (-G,1-G) in (x,y). 119 | ix, iy = band(ix, 255), band(iy, 255) 120 | 121 | local n0 = GetN2(ix, iy, x0, y0) 122 | local n2 = GetN2(ix + 1, iy + 1, x0 - 0.577350270, y0 - 0.577350270) -- G2 123 | 124 | --[[ 125 | Determine other corner based on simplex (equilateral triangle) we are in: 126 | if x0 > y0 then 127 | ix, x1 = ix + 1, x1 - 1 128 | else 129 | iy, y1 = iy + 1, y1 - 1 130 | end 131 | ]] 132 | local xi = rshift(floor(y0 - x0), 31) -- y0 < x0 133 | local n1 = GetN2(ix + xi, iy + (1 - xi), x0 + 0.211324865 - xi, y0 - 0.788675135 + xi) -- x0 + G - xi, y0 + G - (1 - xi) 134 | 135 | -- Add contributions from each corner to get the final noise value. 136 | -- The result is scaled to return values in the interval [-1,1]. 137 | return 70.1480580019 * (n0 + n1 + n2) 138 | end 139 | 140 | -- 3D weight contribution 141 | local function GetN3(ix, iy, iz, x, y, z) 142 | local t = .6 - x * x - y * y - z * z 143 | local index = Perms12[ix + Perms[iy + Perms[iz]]] 144 | 145 | return max(0, (t * t) * (t * t)) * (Grads3[index][0] * x + Grads3[index][1] * y + Grads3[index][2] * z) 146 | end 147 | 148 | local function simplex_3d(x, y, z) 149 | --[[ 150 | 3D skew factors: 151 | F = 1 / 3 152 | G = 1 / 6 153 | G2 = 2 * G 154 | G3 = 3 * G - 1 155 | ]] 156 | 157 | -- Skew the input space to determine which simplex cell we are in. 158 | local s = (x + y + z) * 0.333333333 -- F 159 | local ix, iy, iz = floor(x + s), floor(y + s), floor(z + s) 160 | 161 | -- Unskew the cell origin back to (x, y, z) space. 162 | local t = (ix + iy + iz) * 0.166666667 -- G 163 | local x0 = x + t - ix 164 | local y0 = y + t - iy 165 | local z0 = z + t - iz 166 | 167 | -- Calculate the contribution from the two fixed corners. 168 | -- A step of (1,0,0) in (i,j,k) means a step of (1-G,-G,-G) in (x,y,z); 169 | -- a step of (0,1,0) in (i,j,k) means a step of (-G,1-G,-G) in (x,y,z); 170 | -- a step of (0,0,1) in (i,j,k) means a step of (-G,-G,1-G) in (x,y,z). 171 | ix, iy, iz = band(ix, 255), band(iy, 255), band(iz, 255) 172 | 173 | local n0 = GetN3(ix, iy, iz, x0, y0, z0) 174 | local n3 = GetN3(ix + 1, iy + 1, iz + 1, x0 - 0.5, y0 - 0.5, z0 - 0.5) -- G3 175 | 176 | --[[ 177 | Determine other corners based on simplex (skewed tetrahedron) we are in: 178 | 179 | if x0 >= y0 then -- ~A 180 | if y0 >= z0 then -- ~A and ~B 181 | i1, j1, k1, i2, j2, k2 = 1, 0, 0, 1, 1, 0 182 | elseif x0 >= z0 then -- ~A and B and ~C 183 | i1, j1, k1, i2, j2, k2 = 1, 0, 0, 1, 0, 1 184 | else -- ~A and B and C 185 | i1, j1, k1, i2, j2, k2 = 0, 0, 1, 1, 0, 1 186 | end 187 | else -- A 188 | if y0 < z0 then -- A and B 189 | i1, j1, k1, i2, j2, k2 = 0, 0, 1, 0, 1, 1 190 | elseif x0 < z0 then -- A and ~B and C 191 | i1, j1, k1, i2, j2, k2 = 0, 1, 0, 0, 1, 1 192 | else -- A and ~B and ~C 193 | i1, j1, k1, i2, j2, k2 = 0, 1, 0, 1, 1, 0 194 | end 195 | end 196 | ]] 197 | 198 | local xLy = rshift(floor(x0 - y0), 31) -- x0 < y0 199 | local yLz = rshift(floor(y0 - z0), 31) -- y0 < z0 200 | local xLz = rshift(floor(x0 - z0), 31) -- x0 < z0 201 | 202 | local i1 = band(1 - xLy, bor(1 - yLz, 1 - xLz)) -- x0 >= y0 and (y0 >= z0 or x0 >= z0) 203 | local j1 = band(xLy, 1 - yLz) -- x0 < y0 and y0 >= z0 204 | local k1 = band(yLz, bor(xLy, xLz)) -- y0 < z0 and (x0 < y0 or x0 < z0) 205 | 206 | local i2 = bor(1 - xLy, band(1 - yLz, 1 - xLz)) -- x0 >= y0 or (y0 >= z0 and x0 >= z0) 207 | local j2 = bor(xLy, 1 - yLz) -- x0 < y0 or y0 >= z0 208 | local k2 = bor(band(1 - xLy, yLz), band(xLy, bor(yLz, xLz))) -- (x0 >= y0 and y0 < z0) or (x0 < y0 and (y0 < z0 or x0 < z0)) 209 | 210 | local n1 = GetN3(ix + i1, iy + j1, iz + k1, x0 + 0.166666667 - i1, y0 + 0.166666667 - j1, z0 + 0.166666667 - k1) -- G 211 | local n2 = GetN3(ix + i2, iy + j2, iz + k2, x0 + 0.333333333 - i2, y0 + 0.333333333 - j2, z0 + 0.333333333 - k2) -- G2 212 | 213 | -- Add contributions from each corner to get the final noise value. 214 | -- The result is scaled to stay just inside [-1,1] 215 | return 28.452842 * (n0 + n1 + n2 + n3) 216 | end 217 | 218 | -- Gradients for 4D case -- 219 | local Grads4 = ffi.new("const double[32][4]", 220 | { 0, 1, 1, 1 }, { 0, 1, 1, -1 }, { 0, 1, -1, 1 }, { 0, 1, -1, -1 }, 221 | { 0, -1, 1, 1 }, { 0, -1, 1, -1 }, { 0, -1, -1, 1 }, { 0, -1, -1, -1 }, 222 | { 1, 0, 1, 1 }, { 1, 0, 1, -1 }, { 1, 0, -1, 1 }, { 1, 0, -1, -1 }, 223 | { -1, 0, 1, 1 }, { -1, 0, 1, -1 }, { -1, 0, -1, 1 }, { -1, 0, -1, -1 }, 224 | { 1, 1, 0, 1 }, { 1, 1, 0, -1 }, { 1, -1, 0, 1 }, { 1, -1, 0, -1 }, 225 | { -1, 1, 0, 1 }, { -1, 1, 0, -1 }, { -1, -1, 0, 1 }, { -1, -1, 0, -1 }, 226 | { 1, 1, 1, 0 }, { 1, 1, -1, 0 }, { 1, -1, 1, 0 }, { 1, -1, -1, 0 }, 227 | { -1, 1, 1, 0 }, { -1, 1, -1, 0 }, { -1, -1, 1, 0 }, { -1, -1, -1, 0 } 228 | ) 229 | 230 | -- 4D weight contribution 231 | local function GetN4(ix, iy, iz, iw, x, y, z, w) 232 | local t = .6 - x * x - y * y - z * z - w * w 233 | local index = band(Perms[ix + Perms[iy + Perms[iz + Perms[iw]]]], 0x1F) 234 | 235 | return max(0, (t * t) * (t * t)) * (Grads4[index][0] * x + Grads4[index][1] * y + Grads4[index][2] * z + Grads4[index][3] * w) 236 | end 237 | 238 | -- A lookup table to traverse the simplex around a given point in 4D. 239 | -- Details can be found where this table is used, in the 4D noise method. 240 | local Simplex = ffi.new("uint8_t[64][4]", 241 | { 0, 1, 2, 3 }, { 0, 1, 3, 2 }, {}, { 0, 2, 3, 1 }, {}, {}, {}, { 1, 2, 3 }, 242 | { 0, 2, 1, 3 }, {}, { 0, 3, 1, 2 }, { 0, 3, 2, 1 }, {}, {}, {}, { 1, 3, 2 }, 243 | {}, {}, {}, {}, {}, {}, {}, {}, 244 | { 1, 2, 0, 3 }, {}, { 1, 3, 0, 2 }, {}, {}, {}, { 2, 3, 0, 1 }, { 2, 3, 1 }, 245 | { 1, 0, 2, 3 }, { 1, 0, 3, 2 }, {}, {}, {}, { 2, 0, 3, 1 }, {}, { 2, 1, 3 }, 246 | {}, {}, {}, {}, {}, {}, {}, {}, 247 | { 2, 0, 1, 3 }, {}, {}, {}, { 3, 0, 1, 2 }, { 3, 0, 2, 1 }, {}, { 3, 1, 2 }, 248 | { 2, 1, 0, 3 }, {}, {}, {}, { 3, 1, 0, 2 }, {}, { 3, 2, 0, 1 }, { 3, 2, 1 } 249 | ) 250 | 251 | -- Convert the above indices to masks that can be shifted / anded into offsets -- 252 | for i = 0, 63 do 253 | Simplex[i][0] = lshift(1, Simplex[i][0]) - 1 254 | Simplex[i][1] = lshift(1, Simplex[i][1]) - 1 255 | Simplex[i][2] = lshift(1, Simplex[i][2]) - 1 256 | Simplex[i][3] = lshift(1, Simplex[i][3]) - 1 257 | end 258 | 259 | local function simplex_4d(x, y, z, w) 260 | --[[ 261 | 4D skew factors: 262 | F = (math.sqrt(5) - 1) / 4 263 | G = (5 - math.sqrt(5)) / 20 264 | G2 = 2 * G 265 | G3 = 3 * G 266 | G4 = 4 * G - 1 267 | ]] 268 | 269 | -- Skew the input space to determine which simplex cell we are in. 270 | local s = (x + y + z + w) * 0.309016994 -- F 271 | local ix, iy, iz, iw = floor(x + s), floor(y + s), floor(z + s), floor(w + s) 272 | 273 | -- Unskew the cell origin back to (x, y, z) space. 274 | local t = (ix + iy + iz + iw) * 0.138196601 -- G 275 | local x0 = x + t - ix 276 | local y0 = y + t - iy 277 | local z0 = z + t - iz 278 | local w0 = w + t - iw 279 | 280 | -- For the 4D case, the simplex is a 4D shape I won't even try to describe. 281 | -- To find out which of the 24 possible simplices we're in, we need to 282 | -- determine the magnitude ordering of x0, y0, z0 and w0. 283 | -- The method below is a good way of finding the ordering of x,y,z,w and 284 | -- then find the correct traversal order for the simplex we�re in. 285 | -- First, six pair-wise comparisons are performed between each possible pair 286 | -- of the four coordinates, and the results are used to add up binary bits 287 | -- for an integer index. 288 | local c1 = band(rshift(floor(y0 - x0), 26), 32) 289 | local c2 = band(rshift(floor(z0 - x0), 27), 16) 290 | local c3 = band(rshift(floor(z0 - y0), 28), 8) 291 | local c4 = band(rshift(floor(w0 - x0), 29), 4) 292 | local c5 = band(rshift(floor(w0 - y0), 30), 2) 293 | local c6 = rshift(floor(w0 - z0), 31) 294 | 295 | -- Simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order. 296 | -- Many values of c will never occur, since e.g. x>y>z>w makes x= size and value or 0 39 | end 40 | 41 | --- Check if value is equal or greater than threshold. 42 | -- @param value 43 | -- @param threshold 44 | -- @return boolean 45 | function utils.threshold(value, threshold) 46 | -- I know, it barely saves any typing at all. 47 | return abs(value) >= threshold 48 | end 49 | 50 | --- Check if value is equal or less than threshold. 51 | -- @param value 52 | -- @param threshold 53 | -- @return boolean 54 | function utils.tolerance(value, threshold) 55 | -- I know, it barely saves any typing at all. 56 | return abs(value) <= threshold 57 | end 58 | 59 | --- Scales a value from one range to another. 60 | -- @param value Input value 61 | -- @param min_in Minimum input value 62 | -- @param max_in Maximum input value 63 | -- @param min_out Minimum output value 64 | -- @param max_out Maximum output value 65 | -- @return number 66 | function utils.map(value, min_in, max_in, min_out, max_out) 67 | return ((value) - (min_in)) * ((max_out) - (min_out)) / ((max_in) - (min_in)) + (min_out) 68 | end 69 | 70 | --- Linear interpolation. 71 | -- Performs linear interpolation between 0 and 1 when `low` < `progress` < `high`. 72 | -- @param low value to return when `progress` is 0 73 | -- @param high value to return when `progress` is 1 74 | -- @param progress (0-1) 75 | -- @return number 76 | function utils.lerp(low, high, progress) 77 | return low * (1 - progress) + high * progress 78 | end 79 | 80 | --- Exponential decay 81 | -- @param low initial value 82 | -- @param high target value 83 | -- @param rate portion of the original value remaining per second 84 | -- @param dt time delta 85 | -- @return number 86 | function utils.decay(low, high, rate, dt) 87 | return utils.lerp(low, high, 1.0 - math.exp(-rate * dt)) 88 | end 89 | 90 | --- Hermite interpolation. 91 | -- Performs smooth Hermite interpolation between 0 and 1 when `low` < `progress` < `high`. 92 | -- @param progress (0-1) 93 | -- @param low value to return when `progress` is 0 94 | -- @param high value to return when `progress` is 1 95 | -- @return number 96 | function utils.smoothstep(progress, low, high) 97 | local t = utils.clamp((progress - low) / (high - low), 0.0, 1.0) 98 | return t * t * (3.0 - 2.0 * t) 99 | end 100 | 101 | --- Round number at a given precision. 102 | -- Truncates `value` at `precision` points after the decimal (whole number if 103 | -- left unspecified). 104 | -- @param value 105 | -- @param precision 106 | -- @return number 107 | utils.round = private.round 108 | 109 | --- Wrap `value` around if it exceeds `limit`. 110 | -- @param value 111 | -- @param limit 112 | -- @return number 113 | function utils.wrap(value, limit) 114 | if value < 0 then 115 | value = value + utils.round(((-value/limit)+1))*limit 116 | end 117 | return value % limit 118 | end 119 | 120 | --- Check if a value is a power-of-two. 121 | -- Returns true if a number is a valid power-of-two, otherwise false. 122 | -- @author undef 123 | -- @param value 124 | -- @return boolean 125 | function utils.is_pot(value) 126 | -- found here: https://love2d.org/forums/viewtopic.php?p=182219#p182219 127 | -- check if a number is a power-of-two 128 | return (frexp(value)) == 0.5 129 | end 130 | 131 | --- Check if a value is NaN 132 | -- Returns true if a number is not a valid number 133 | -- @param value 134 | -- @return boolean 135 | utils.is_nan = private.is_nan 136 | 137 | -- Originally from vec3 138 | function utils.project_on(a, b) 139 | local s = 140 | (a.x * b.x + a.y * b.y + a.z or 0 * b.z or 0) / 141 | (b.x * b.x + b.y * b.y + b.z or 0 * b.z or 0) 142 | 143 | if a.z and b.z then 144 | return vec3( 145 | b.x * s, 146 | b.y * s, 147 | b.z * s 148 | ) 149 | end 150 | 151 | return vec2( 152 | b.x * s, 153 | b.y * s 154 | ) 155 | end 156 | 157 | -- Originally from vec3 158 | function utils.project_from(a, b) 159 | local s = 160 | (b.x * b.x + b.y * b.y + b.z or 0 * b.z or 0) / 161 | (a.x * b.x + a.y * b.y + a.z or 0 * b.z or 0) 162 | 163 | if a.z and b.z then 164 | return vec3( 165 | b.x * s, 166 | b.y * s, 167 | b.z * s 168 | ) 169 | end 170 | 171 | return vec2( 172 | b.x * s, 173 | b.y * s 174 | ) 175 | end 176 | 177 | -- Originally from vec3 178 | function utils.mirror_on(a, b) 179 | local s = 180 | (a.x * b.x + a.y * b.y + a.z or 0 * b.z or 0) / 181 | (b.x * b.x + b.y * b.y + b.z or 0 * b.z or 0) * 2 182 | 183 | if a.z and b.z then 184 | return vec3( 185 | b.x * s - a.x, 186 | b.y * s - a.y, 187 | b.z * s - a.z 188 | ) 189 | end 190 | 191 | return vec2( 192 | b.x * s - a.x, 193 | b.y * s - a.y 194 | ) 195 | end 196 | 197 | -- Originally from vec3 198 | function utils.reflect(i, n) 199 | return i - (n * (2 * n:dot(i))) 200 | end 201 | 202 | -- Originally from vec3 203 | function utils.refract(i, n, ior) 204 | local d = n:dot(i) 205 | local k = 1 - ior * ior * (1 - d * d) 206 | 207 | if k >= 0 then 208 | return (i * ior) - (n * (ior * d + k ^ 0.5)) 209 | end 210 | 211 | return vec3() 212 | end 213 | 214 | --- Get the sign of a number 215 | -- returns 1 for positive values, -1 for negative and 0 for zero. 216 | -- @param value 217 | -- @return number 218 | function utils.sign(n) 219 | if n > 0 then 220 | return 1 221 | elseif n < 0 then 222 | return -1 223 | else 224 | return 0 225 | end 226 | end 227 | 228 | return utils 229 | -------------------------------------------------------------------------------- /modules/vec2.lua: -------------------------------------------------------------------------------- 1 | --- A 2 component vector. 2 | -- @module vec2 3 | 4 | local modules = (...):gsub('%.[^%.]+$', '') .. "." 5 | local vec3 = require(modules .. "vec3") 6 | local precond = require(modules .. "_private_precond") 7 | local private = require(modules .. "_private_utils") 8 | local acos = math.acos 9 | local atan2 = math.atan2 or math.atan 10 | local sqrt = math.sqrt 11 | local cos = math.cos 12 | local sin = math.sin 13 | local vec2 = {} 14 | local vec2_mt = {} 15 | 16 | -- Private constructor. 17 | local function new(x, y) 18 | return setmetatable({ 19 | x = x or 0, 20 | y = y or 0 21 | }, vec2_mt) 22 | end 23 | 24 | -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. 25 | local status, ffi 26 | if type(jit) == "table" and jit.status() then 27 | status, ffi = pcall(require, "ffi") 28 | if status then 29 | ffi.cdef "typedef struct { double x, y;} cpml_vec2;" 30 | new = ffi.typeof("cpml_vec2") 31 | end 32 | end 33 | 34 | --- Constants 35 | -- @table vec2 36 | -- @field unit_x X axis of rotation 37 | -- @field unit_y Y axis of rotation 38 | -- @field zero Empty vector 39 | vec2.unit_x = new(1, 0) 40 | vec2.unit_y = new(0, 1) 41 | vec2.zero = new(0, 0) 42 | 43 | --- The public constructor. 44 | -- @param x Can be of three types:
45 | -- number X component 46 | -- table {x, y} or {x = x, y = y} 47 | -- scalar to fill the vector eg. {x, x} 48 | -- @tparam number y Y component 49 | -- @treturn vec2 out 50 | function vec2.new(x, y) 51 | -- number, number 52 | if x and y then 53 | precond.typeof(x, "number", "new: Wrong argument type for x") 54 | precond.typeof(y, "number", "new: Wrong argument type for y") 55 | 56 | return new(x, y) 57 | 58 | -- {x, y} or {x=x, y=y} 59 | elseif type(x) == "table" or type(x) == "cdata" then -- table in vanilla lua, cdata in luajit 60 | local xx, yy = x.x or x[1], x.y or x[2] 61 | precond.typeof(xx, "number", "new: Wrong argument type for x") 62 | precond.typeof(yy, "number", "new: Wrong argument type for y") 63 | 64 | return new(xx, yy) 65 | 66 | -- number 67 | elseif type(x) == "number" then 68 | return new(x, x) 69 | else 70 | return new() 71 | end 72 | end 73 | 74 | --- Convert point from polar to cartesian. 75 | -- @tparam number radius Radius of the point 76 | -- @tparam number theta Angle of the point (in radians) 77 | -- @treturn vec2 out 78 | function vec2.from_cartesian(radius, theta) 79 | return new(radius * cos(theta), radius * sin(theta)) 80 | end 81 | 82 | --- Clone a vector. 83 | -- @tparam vec2 a Vector to be cloned 84 | -- @treturn vec2 out 85 | function vec2.clone(a) 86 | return new(a.x, a.y) 87 | end 88 | 89 | --- Add two vectors. 90 | -- @tparam vec2 a Left hand operand 91 | -- @tparam vec2 b Right hand operand 92 | -- @treturn vec2 out 93 | function vec2.add(a, b) 94 | return new( 95 | a.x + b.x, 96 | a.y + b.y 97 | ) 98 | end 99 | 100 | --- Subtract one vector from another. 101 | -- Order: If a and b are positions, computes the direction and distance from b 102 | -- to a. 103 | -- @tparam vec2 a Left hand operand 104 | -- @tparam vec2 b Right hand operand 105 | -- @treturn vec2 out 106 | function vec2.sub(a, b) 107 | return new( 108 | a.x - b.x, 109 | a.y - b.y 110 | ) 111 | end 112 | 113 | --- Multiply a vector by another vector. 114 | -- Component-size multiplication not matrix multiplication. 115 | -- @tparam vec2 a Left hand operand 116 | -- @tparam vec2 b Right hand operand 117 | -- @treturn vec2 out 118 | function vec2.mul(a, b) 119 | return new( 120 | a.x * b.x, 121 | a.y * b.y 122 | ) 123 | end 124 | 125 | --- Divide a vector by another vector. 126 | -- Component-size inv multiplication. Like a non-uniform scale(). 127 | -- @tparam vec2 a Left hand operand 128 | -- @tparam vec2 b Right hand operand 129 | -- @treturn vec2 out 130 | function vec2.div(a, b) 131 | return new( 132 | a.x / b.x, 133 | a.y / b.y 134 | ) 135 | end 136 | 137 | --- Get the normal of a vector. 138 | -- @tparam vec2 a Vector to normalize 139 | -- @treturn vec2 out 140 | function vec2.normalize(a) 141 | if a:is_zero() then 142 | return new() 143 | end 144 | return a:scale(1 / a:len()) 145 | end 146 | 147 | --- Trim a vector to a given length. 148 | -- @tparam vec2 a Vector to be trimmed 149 | -- @tparam number len Length to trim the vector to 150 | -- @treturn vec2 out 151 | function vec2.trim(a, len) 152 | return a:normalize():scale(math.min(a:len(), len)) 153 | end 154 | 155 | --- Get the cross product of two vectors. 156 | -- Order: Positive if a is clockwise from b. Magnitude is the area spanned by 157 | -- the parallelograms that a and b span. 158 | -- @tparam vec2 a Left hand operand 159 | -- @tparam vec2 b Right hand operand 160 | -- @treturn number magnitude 161 | function vec2.cross(a, b) 162 | return a.x * b.y - a.y * b.x 163 | end 164 | 165 | --- Get the dot product of two vectors. 166 | -- @tparam vec2 a Left hand operand 167 | -- @tparam vec2 b Right hand operand 168 | -- @treturn number dot 169 | function vec2.dot(a, b) 170 | return a.x * b.x + a.y * b.y 171 | end 172 | 173 | --- Get the length of a vector. 174 | -- @tparam vec2 a Vector to get the length of 175 | -- @treturn number len 176 | function vec2.len(a) 177 | return sqrt(a.x * a.x + a.y * a.y) 178 | end 179 | 180 | --- Get the squared length of a vector. 181 | -- @tparam vec2 a Vector to get the squared length of 182 | -- @treturn number len 183 | function vec2.len2(a) 184 | return a.x * a.x + a.y * a.y 185 | end 186 | 187 | --- Get the distance between two vectors. 188 | -- @tparam vec2 a Left hand operand 189 | -- @tparam vec2 b Right hand operand 190 | -- @treturn number dist 191 | function vec2.dist(a, b) 192 | local dx = a.x - b.x 193 | local dy = a.y - b.y 194 | return sqrt(dx * dx + dy * dy) 195 | end 196 | 197 | --- Get the squared distance between two vectors. 198 | -- @tparam vec2 a Left hand operand 199 | -- @tparam vec2 b Right hand operand 200 | -- @treturn number dist 201 | function vec2.dist2(a, b) 202 | local dx = a.x - b.x 203 | local dy = a.y - b.y 204 | return dx * dx + dy * dy 205 | end 206 | 207 | --- Scale a vector by a scalar. 208 | -- @tparam vec2 a Left hand operand 209 | -- @tparam number b Right hand operand 210 | -- @treturn vec2 out 211 | function vec2.scale(a, b) 212 | return new( 213 | a.x * b, 214 | a.y * b 215 | ) 216 | end 217 | 218 | --- Rotate a vector. 219 | -- @tparam vec2 a Vector to rotate 220 | -- @tparam number phi Angle to rotate vector by (in radians) 221 | -- @treturn vec2 out 222 | function vec2.rotate(a, phi) 223 | local c = cos(phi) 224 | local s = sin(phi) 225 | return new( 226 | c * a.x - s * a.y, 227 | s * a.x + c * a.y 228 | ) 229 | end 230 | 231 | --- Get the perpendicular vector of a vector. 232 | -- @tparam vec2 a Vector to get perpendicular axes from 233 | -- @treturn vec2 out 234 | function vec2.perpendicular(a) 235 | return new(-a.y, a.x) 236 | end 237 | 238 | --- Signed angle from one vector to another. 239 | -- Rotations from +x to +y are positive. 240 | -- @tparam vec2 a Vector 241 | -- @tparam vec2 b Vector 242 | -- @treturn number angle in (-pi, pi] 243 | function vec2.angle_to(a, b) 244 | if b then 245 | local angle = atan2(b.y, b.x) - atan2(a.y, a.x) 246 | -- convert to (-pi, pi] 247 | if angle > math.pi then 248 | angle = angle - 2 * math.pi 249 | elseif angle <= -math.pi then 250 | angle = angle + 2 * math.pi 251 | end 252 | return angle 253 | end 254 | 255 | return atan2(a.y, a.x) 256 | end 257 | 258 | --- Unsigned angle between two vectors. 259 | -- Directionless and thus commutative. 260 | -- @tparam vec2 a Vector 261 | -- @tparam vec2 b Vector 262 | -- @treturn number angle in [0, pi] 263 | function vec2.angle_between(a, b) 264 | if b then 265 | if vec2.is_vec2(a) then 266 | return acos(a:dot(b) / (a:len() * b:len())) 267 | end 268 | 269 | return acos(vec3.dot(a, b) / (vec3.len(a) * vec3.len(b))) 270 | end 271 | 272 | return 0 273 | end 274 | 275 | --- Lerp between two vectors. 276 | -- @tparam vec2 a Left hand operand 277 | -- @tparam vec2 b Right hand operand 278 | -- @tparam number s Step value 279 | -- @treturn vec2 out 280 | function vec2.lerp(a, b, s) 281 | return a + (b - a) * s 282 | end 283 | 284 | --- Unpack a vector into individual components. 285 | -- @tparam vec2 a Vector to unpack 286 | -- @treturn number x 287 | -- @treturn number y 288 | function vec2.unpack(a) 289 | return a.x, a.y 290 | end 291 | 292 | --- Return the component-wise minimum of two vectors. 293 | -- @tparam vec2 a Left hand operand 294 | -- @tparam vec2 b Right hand operand 295 | -- @treturn vec2 A vector where each component is the lesser value for that component between the two given vectors. 296 | function vec2.component_min(a, b) 297 | return new(math.min(a.x, b.x), math.min(a.y, b.y)) 298 | end 299 | 300 | --- Return the component-wise maximum of two vectors. 301 | -- @tparam vec2 a Left hand operand 302 | -- @tparam vec2 b Right hand operand 303 | -- @treturn vec2 A vector where each component is the lesser value for that component between the two given vectors. 304 | function vec2.component_max(a, b) 305 | return new(math.max(a.x, b.x), math.max(a.y, b.y)) 306 | end 307 | 308 | 309 | --- Return a boolean showing if a table is or is not a vec2. 310 | -- @tparam vec2 a Vector to be tested 311 | -- @treturn boolean is_vec2 312 | function vec2.is_vec2(a) 313 | if type(a) == "cdata" then 314 | return ffi.istype("cpml_vec2", a) 315 | end 316 | 317 | return 318 | type(a) == "table" and 319 | type(a.x) == "number" and 320 | type(a.y) == "number" 321 | end 322 | 323 | --- Return a boolean showing if a table is or is not a zero vec2. 324 | -- @tparam vec2 a Vector to be tested 325 | -- @treturn boolean is_zero 326 | function vec2.is_zero(a) 327 | return a.x == 0 and a.y == 0 328 | end 329 | 330 | --- Return whether either value is NaN 331 | -- @tparam vec2 a Vector to be tested 332 | -- @treturn boolean if x or y is nan 333 | function vec2.has_nan(a) 334 | return private.is_nan(a.x) or 335 | private.is_nan(a.y) 336 | end 337 | 338 | --- Convert point from cartesian to polar. 339 | -- @tparam vec2 a Vector to convert 340 | -- @treturn number radius 341 | -- @treturn number theta 342 | function vec2.to_polar(a) 343 | local radius = sqrt(a.x^2 + a.y^2) 344 | local theta = atan2(a.y, a.x) 345 | theta = theta > 0 and theta or theta + 2 * math.pi 346 | return radius, theta 347 | end 348 | 349 | -- Round all components to nearest int (or other precision). 350 | -- @tparam vec2 a Vector to round. 351 | -- @tparam precision Digits after the decimal (integer if unspecified) 352 | -- @treturn vec2 Rounded vector 353 | function vec2.round(a, precision) 354 | return vec2.new(private.round(a.x, precision), private.round(a.y, precision)) 355 | end 356 | 357 | -- Negate x axis only of vector. 358 | -- @tparam vec2 a Vector to x-flip. 359 | -- @treturn vec2 x-flipped vector 360 | function vec2.flip_x(a) 361 | return vec2.new(-a.x, a.y) 362 | end 363 | 364 | -- Negate y axis only of vector. 365 | -- @tparam vec2 a Vector to y-flip. 366 | -- @treturn vec2 y-flipped vector 367 | function vec2.flip_y(a) 368 | return vec2.new(a.x, -a.y) 369 | end 370 | 371 | -- Convert vec2 to vec3. 372 | -- @tparam vec2 a Vector to convert. 373 | -- @tparam number the new z component, or nil for 0 374 | -- @treturn vec3 Converted vector 375 | function vec2.to_vec3(a, z) 376 | return vec3(a.x, a.y, z or 0) 377 | end 378 | 379 | --- Return a formatted string. 380 | -- @tparam vec2 a Vector to be turned into a string 381 | -- @treturn string formatted 382 | function vec2.to_string(a) 383 | return string.format("(%+0.3f,%+0.3f)", a.x, a.y) 384 | end 385 | 386 | vec2_mt.__index = vec2 387 | vec2_mt.__tostring = vec2.to_string 388 | 389 | function vec2_mt.__call(_, x, y) 390 | return vec2.new(x, y) 391 | end 392 | 393 | function vec2_mt.__unm(a) 394 | return new(-a.x, -a.y) 395 | end 396 | 397 | function vec2_mt.__eq(a, b) 398 | if not vec2.is_vec2(a) or not vec2.is_vec2(b) then 399 | return false 400 | end 401 | return a.x == b.x and a.y == b.y 402 | end 403 | 404 | function vec2_mt.__add(a, b) 405 | precond.assert(vec2.is_vec2(a), "__add: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 406 | precond.assert(vec2.is_vec2(b), "__add: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) 407 | return a:add(b) 408 | end 409 | 410 | function vec2_mt.__sub(a, b) 411 | precond.assert(vec2.is_vec2(a), "__add: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 412 | precond.assert(vec2.is_vec2(b), "__add: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) 413 | return a:sub(b) 414 | end 415 | 416 | function vec2_mt.__mul(a, b) 417 | precond.assert(vec2.is_vec2(a), "__mul: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 418 | assert(vec2.is_vec2(b) or type(b) == "number", "__mul: Wrong argument type for right hand operand. ( or expected)") 419 | 420 | if vec2.is_vec2(b) then 421 | return a:mul(b) 422 | end 423 | 424 | return a:scale(b) 425 | end 426 | 427 | function vec2_mt.__div(a, b) 428 | precond.assert(vec2.is_vec2(a), "__div: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 429 | assert(vec2.is_vec2(b) or type(b) == "number", "__div: Wrong argument type for right hand operand. ( or expected)") 430 | 431 | if vec2.is_vec2(b) then 432 | return a:div(b) 433 | end 434 | 435 | return a:scale(1 / b) 436 | end 437 | 438 | if status then 439 | xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded 440 | ffi.metatype(new, vec2_mt) 441 | end, function() end) 442 | end 443 | 444 | return setmetatable({}, vec2_mt) 445 | -------------------------------------------------------------------------------- /modules/vec3.lua: -------------------------------------------------------------------------------- 1 | --- A 3 component vector. 2 | -- @module vec3 3 | 4 | local modules = (...):gsub('%.[^%.]+$', '') .. "." 5 | local precond = require(modules .. "_private_precond") 6 | local private = require(modules .. "_private_utils") 7 | local sqrt = math.sqrt 8 | local cos = math.cos 9 | local sin = math.sin 10 | local vec3 = {} 11 | local vec3_mt = {} 12 | 13 | -- Private constructor. 14 | local function new(x, y, z) 15 | return setmetatable({ 16 | x = x or 0, 17 | y = y or 0, 18 | z = z or 0 19 | }, vec3_mt) 20 | end 21 | 22 | -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. 23 | local status, ffi 24 | if type(jit) == "table" and jit.status() then 25 | status, ffi = pcall(require, "ffi") 26 | if status then 27 | ffi.cdef "typedef struct { double x, y, z;} cpml_vec3;" 28 | new = ffi.typeof("cpml_vec3") 29 | end 30 | end 31 | 32 | --- Constants 33 | -- @table vec3 34 | -- @field unit_x X axis of rotation 35 | -- @field unit_y Y axis of rotation 36 | -- @field unit_z Z axis of rotation 37 | -- @field zero Empty vector 38 | vec3.unit_x = new(1, 0, 0) 39 | vec3.unit_y = new(0, 1, 0) 40 | vec3.unit_z = new(0, 0, 1) 41 | vec3.zero = new(0, 0, 0) 42 | 43 | --- The public constructor. 44 | -- @param x Can be of three types:
45 | -- number X component 46 | -- table {x, y, z} or {x=x, y=y, z=z} 47 | -- scalar To fill the vector eg. {x, x, x} 48 | -- @tparam number y Y component 49 | -- @tparam number z Z component 50 | -- @treturn vec3 out 51 | function vec3.new(x, y, z) 52 | -- number, number, number 53 | if x and y and z then 54 | precond.typeof(x, "number", "new: Wrong argument type for x") 55 | precond.typeof(y, "number", "new: Wrong argument type for y") 56 | precond.typeof(z, "number", "new: Wrong argument type for z") 57 | 58 | return new(x, y, z) 59 | 60 | -- {x, y, z} or {x=x, y=y, z=z} 61 | elseif type(x) == "table" or type(x) == "cdata" then -- table in vanilla lua, cdata in luajit 62 | local xx, yy, zz = x.x or x[1], x.y or x[2], x.z or x[3] 63 | precond.typeof(xx, "number", "new: Wrong argument type for x") 64 | precond.typeof(yy, "number", "new: Wrong argument type for y") 65 | precond.typeof(zz, "number", "new: Wrong argument type for z") 66 | 67 | return new(xx, yy, zz) 68 | 69 | -- number 70 | elseif type(x) == "number" then 71 | return new(x, x, x) 72 | else 73 | return new() 74 | end 75 | end 76 | 77 | --- Clone a vector. 78 | -- @tparam vec3 a Vector to be cloned 79 | -- @treturn vec3 out 80 | function vec3.clone(a) 81 | return new(a.x, a.y, a.z) 82 | end 83 | 84 | --- Add two vectors. 85 | -- @tparam vec3 a Left hand operand 86 | -- @tparam vec3 b Right hand operand 87 | -- @treturn vec3 out 88 | function vec3.add(a, b) 89 | return new( 90 | a.x + b.x, 91 | a.y + b.y, 92 | a.z + b.z 93 | ) 94 | end 95 | 96 | --- Subtract one vector from another. 97 | -- Order: If a and b are positions, computes the direction and distance from b 98 | -- to a. 99 | -- @tparam vec3 a Left hand operand 100 | -- @tparam vec3 b Right hand operand 101 | -- @treturn vec3 out 102 | function vec3.sub(a, b) 103 | return new( 104 | a.x - b.x, 105 | a.y - b.y, 106 | a.z - b.z 107 | ) 108 | end 109 | 110 | --- Multiply a vector by another vector. 111 | -- Component-wise multiplication not matrix multiplication. 112 | -- @tparam vec3 a Left hand operand 113 | -- @tparam vec3 b Right hand operand 114 | -- @treturn vec3 out 115 | function vec3.mul(a, b) 116 | return new( 117 | a.x * b.x, 118 | a.y * b.y, 119 | a.z * b.z 120 | ) 121 | end 122 | 123 | --- Divide a vector by another. 124 | -- Component-wise inv multiplication. Like a non-uniform scale(). 125 | -- @tparam vec3 a Left hand operand 126 | -- @tparam vec3 b Right hand operand 127 | -- @treturn vec3 out 128 | function vec3.div(a, b) 129 | return new( 130 | a.x / b.x, 131 | a.y / b.y, 132 | a.z / b.z 133 | ) 134 | end 135 | 136 | --- Scale a vector to unit length (1). 137 | -- @tparam vec3 a vector to normalize 138 | -- @treturn vec3 out 139 | function vec3.normalize(a) 140 | if a:is_zero() then 141 | return new() 142 | end 143 | return a:scale(1 / a:len()) 144 | end 145 | 146 | --- Scale a vector to unit length (1), and return the input length. 147 | -- @tparam vec3 a vector to normalize 148 | -- @treturn vec3 out 149 | -- @treturn number input vector length 150 | function vec3.normalize_len(a) 151 | if a:is_zero() then 152 | return new(), 0 153 | end 154 | local len = a:len() 155 | return a:scale(1 / len), len 156 | end 157 | 158 | --- Trim a vector to a given length 159 | -- @tparam vec3 a vector to be trimmed 160 | -- @tparam number len Length to trim the vector to 161 | -- @treturn vec3 out 162 | function vec3.trim(a, len) 163 | return a:normalize():scale(math.min(a:len(), len)) 164 | end 165 | 166 | --- Get the cross product of two vectors. 167 | -- Resulting direction is right-hand rule normal of plane defined by a and b. 168 | -- Magnitude is the area spanned by the parallelograms that a and b span. 169 | -- Order: Direction determined by right-hand rule. 170 | -- @tparam vec3 a Left hand operand 171 | -- @tparam vec3 b Right hand operand 172 | -- @treturn vec3 out 173 | function vec3.cross(a, b) 174 | return new( 175 | a.y * b.z - a.z * b.y, 176 | a.z * b.x - a.x * b.z, 177 | a.x * b.y - a.y * b.x 178 | ) 179 | end 180 | 181 | --- Get the dot product of two vectors. 182 | -- @tparam vec3 a Left hand operand 183 | -- @tparam vec3 b Right hand operand 184 | -- @treturn number dot 185 | function vec3.dot(a, b) 186 | return a.x * b.x + a.y * b.y + a.z * b.z 187 | end 188 | 189 | --- Get the length of a vector. 190 | -- @tparam vec3 a Vector to get the length of 191 | -- @treturn number len 192 | function vec3.len(a) 193 | return sqrt(a.x * a.x + a.y * a.y + a.z * a.z) 194 | end 195 | 196 | --- Get the squared length of a vector. 197 | -- @tparam vec3 a Vector to get the squared length of 198 | -- @treturn number len 199 | function vec3.len2(a) 200 | return a.x * a.x + a.y * a.y + a.z * a.z 201 | end 202 | 203 | --- Get the distance between two vectors. 204 | -- @tparam vec3 a Left hand operand 205 | -- @tparam vec3 b Right hand operand 206 | -- @treturn number dist 207 | function vec3.dist(a, b) 208 | local dx = a.x - b.x 209 | local dy = a.y - b.y 210 | local dz = a.z - b.z 211 | return sqrt(dx * dx + dy * dy + dz * dz) 212 | end 213 | 214 | --- Get the squared distance between two vectors. 215 | -- @tparam vec3 a Left hand operand 216 | -- @tparam vec3 b Right hand operand 217 | -- @treturn number dist 218 | function vec3.dist2(a, b) 219 | local dx = a.x - b.x 220 | local dy = a.y - b.y 221 | local dz = a.z - b.z 222 | return dx * dx + dy * dy + dz * dz 223 | end 224 | 225 | --- Scale a vector by a scalar. 226 | -- @tparam vec3 a Left hand operand 227 | -- @tparam number b Right hand operand 228 | -- @treturn vec3 out 229 | function vec3.scale(a, b) 230 | return new( 231 | a.x * b, 232 | a.y * b, 233 | a.z * b 234 | ) 235 | end 236 | 237 | --- Rotate vector about an axis. 238 | -- @tparam vec3 a Vector to rotate 239 | -- @tparam number phi Angle to rotate vector by (in radians) 240 | -- @tparam vec3 axis Axis to rotate by 241 | -- @treturn vec3 out 242 | function vec3.rotate(a, phi, axis) 243 | if not vec3.is_vec3(axis) then 244 | return a 245 | end 246 | 247 | local u = axis:normalize() 248 | local c = cos(phi) 249 | local s = sin(phi) 250 | 251 | -- Calculate generalized rotation matrix 252 | local m1 = new((c + u.x * u.x * (1 - c)), (u.x * u.y * (1 - c) - u.z * s), (u.x * u.z * (1 - c) + u.y * s)) 253 | local m2 = new((u.y * u.x * (1 - c) + u.z * s), (c + u.y * u.y * (1 - c)), (u.y * u.z * (1 - c) - u.x * s)) 254 | local m3 = new((u.z * u.x * (1 - c) - u.y * s), (u.z * u.y * (1 - c) + u.x * s), (c + u.z * u.z * (1 - c)) ) 255 | 256 | return new( 257 | a:dot(m1), 258 | a:dot(m2), 259 | a:dot(m3) 260 | ) 261 | end 262 | 263 | --- Get the perpendicular vector of a vector. 264 | -- @tparam vec3 a Vector to get perpendicular axes from 265 | -- @treturn vec3 out 266 | function vec3.perpendicular(a) 267 | return new(-a.y, a.x, 0) 268 | end 269 | 270 | --- Lerp between two vectors. 271 | -- @tparam vec3 a Left hand operand 272 | -- @tparam vec3 b Right hand operand 273 | -- @tparam number s Step value 274 | -- @treturn vec3 out 275 | function vec3.lerp(a, b, s) 276 | return a + (b - a) * s 277 | end 278 | 279 | -- Round all components to nearest int (or other precision). 280 | -- @tparam vec3 a Vector to round. 281 | -- @tparam precision Digits after the decimal (round numebr if unspecified) 282 | -- @treturn vec3 Rounded vector 283 | function vec3.round(a, precision) 284 | return vec3.new(private.round(a.x, precision), private.round(a.y, precision), private.round(a.z, precision)) 285 | end 286 | 287 | --- Unpack a vector into individual components. 288 | -- @tparam vec3 a Vector to unpack 289 | -- @treturn number x 290 | -- @treturn number y 291 | -- @treturn number z 292 | function vec3.unpack(a) 293 | return a.x, a.y, a.z 294 | end 295 | 296 | --- Return the component-wise minimum of two vectors. 297 | -- @tparam vec3 a Left hand operand 298 | -- @tparam vec3 b Right hand operand 299 | -- @treturn vec3 A vector where each component is the lesser value for that component between the two given vectors. 300 | function vec3.component_min(a, b) 301 | return new(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z)) 302 | end 303 | 304 | --- Return the component-wise maximum of two vectors. 305 | -- @tparam vec3 a Left hand operand 306 | -- @tparam vec3 b Right hand operand 307 | -- @treturn vec3 A vector where each component is the lesser value for that component between the two given vectors. 308 | function vec3.component_max(a, b) 309 | return new(math.max(a.x, b.x), math.max(a.y, b.y), math.max(a.z, b.z)) 310 | end 311 | 312 | -- Negate x axis only of vector. 313 | -- @tparam vec3 a Vector to x-flip. 314 | -- @treturn vec3 x-flipped vector 315 | function vec3.flip_x(a) 316 | return vec3.new(-a.x, a.y, a.z) 317 | end 318 | 319 | -- Negate y axis only of vector. 320 | -- @tparam vec3 a Vector to y-flip. 321 | -- @treturn vec3 y-flipped vector 322 | function vec3.flip_y(a) 323 | return vec3.new(a.x, -a.y, a.z) 324 | end 325 | 326 | -- Negate z axis only of vector. 327 | -- @tparam vec3 a Vector to z-flip. 328 | -- @treturn vec3 z-flipped vector 329 | function vec3.flip_z(a) 330 | return vec3.new(a.x, a.y, -a.z) 331 | end 332 | 333 | function vec3.angle_to(a, b) 334 | local v = a:normalize():dot(b:normalize()) 335 | return math.acos(v) 336 | end 337 | 338 | --- Return a boolean showing if a table is or is not a vec3. 339 | -- @tparam vec3 a Vector to be tested 340 | -- @treturn boolean is_vec3 341 | function vec3.is_vec3(a) 342 | if type(a) == "cdata" then 343 | return ffi.istype("cpml_vec3", a) 344 | end 345 | 346 | return 347 | type(a) == "table" and 348 | type(a.x) == "number" and 349 | type(a.y) == "number" and 350 | type(a.z) == "number" 351 | end 352 | 353 | --- Return a boolean showing if a table is or is not a zero vec3. 354 | -- @tparam vec3 a Vector to be tested 355 | -- @treturn boolean is_zero 356 | function vec3.is_zero(a) 357 | return a.x == 0 and a.y == 0 and a.z == 0 358 | end 359 | 360 | --- Return whether any component is NaN 361 | -- @tparam vec3 a Vector to be tested 362 | -- @treturn boolean if x,y, or z are nan 363 | function vec3.has_nan(a) 364 | return private.is_nan(a.x) or 365 | private.is_nan(a.y) or 366 | private.is_nan(a.z) 367 | end 368 | 369 | --- Return a formatted string. 370 | -- @tparam vec3 a Vector to be turned into a string 371 | -- @treturn string formatted 372 | function vec3.to_string(a) 373 | return string.format("(%+0.3f,%+0.3f,%+0.3f)", a.x, a.y, a.z) 374 | end 375 | 376 | vec3_mt.__index = vec3 377 | vec3_mt.__tostring = vec3.to_string 378 | 379 | function vec3_mt.__call(_, x, y, z) 380 | return vec3.new(x, y, z) 381 | end 382 | 383 | function vec3_mt.__unm(a) 384 | return new(-a.x, -a.y, -a.z) 385 | end 386 | 387 | function vec3_mt.__eq(a, b) 388 | if not vec3.is_vec3(a) or not vec3.is_vec3(b) then 389 | return false 390 | end 391 | return a.x == b.x and a.y == b.y and a.z == b.z 392 | end 393 | 394 | function vec3_mt.__add(a, b) 395 | precond.assert(vec3.is_vec3(a), "__add: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 396 | precond.assert(vec3.is_vec3(b), "__add: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) 397 | return a:add(b) 398 | end 399 | 400 | function vec3_mt.__sub(a, b) 401 | precond.assert(vec3.is_vec3(a), "__sub: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 402 | precond.assert(vec3.is_vec3(b), "__sub: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) 403 | return a:sub(b) 404 | end 405 | 406 | function vec3_mt.__mul(a, b) 407 | precond.assert(vec3.is_vec3(a), "__mul: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 408 | precond.assert(vec3.is_vec3(b) or type(b) == "number", "__mul: Wrong argument type '%s' for right hand operand. ( or expected)", type(b)) 409 | 410 | if vec3.is_vec3(b) then 411 | return a:mul(b) 412 | end 413 | 414 | return a:scale(b) 415 | end 416 | 417 | function vec3_mt.__div(a, b) 418 | precond.assert(vec3.is_vec3(a), "__div: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 419 | precond.assert(vec3.is_vec3(b) or type(b) == "number", "__div: Wrong argument type '%s' for right hand operand. ( or expected)", type(b)) 420 | 421 | if vec3.is_vec3(b) then 422 | return a:div(b) 423 | end 424 | 425 | return a:scale(1 / b) 426 | end 427 | 428 | if status then 429 | xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded 430 | ffi.metatype(new, vec3_mt) 431 | end, function() end) 432 | end 433 | 434 | return setmetatable({}, vec3_mt) 435 | -------------------------------------------------------------------------------- /spec/bound2_spec.lua: -------------------------------------------------------------------------------- 1 | local bound2 = require "modules.bound2" 2 | local vec2 = require "modules.vec2" 3 | local DBL_EPSILON = require("modules.constants").DBL_EPSILON 4 | 5 | describe("bound2:", function() 6 | it("creates an empty bound2", function() 7 | local a = bound2() 8 | assert.is.equal(0, a.min.x) 9 | assert.is.equal(0, a.min.y) 10 | assert.is.equal(0, a.max.x) 11 | assert.is.equal(0, a.max.y) 12 | end) 13 | 14 | it("creates a bound2 from vec2s", function() 15 | local a = bound2(vec2(1,2), vec2(4,5)) 16 | assert.is.equal(1, a.min.x) 17 | assert.is.equal(2, a.min.y) 18 | assert.is.equal(4, a.max.x) 19 | assert.is.equal(5, a.max.y) 20 | end) 21 | 22 | it("creates a bound2 using new()", function() 23 | local a = bound2.new(vec2(1,2), vec2(4,5)) 24 | assert.is.equal(1, a.min.x) 25 | assert.is.equal(2, a.min.y) 26 | assert.is.equal(4, a.max.x) 27 | assert.is.equal(5, a.max.y) 28 | end) 29 | 30 | it("creates a bound2 using at()", function() 31 | local a = bound2.at(vec2(4,5), vec2(1,2)) 32 | assert.is.equal(1, a.min.x) 33 | assert.is.equal(2, a.min.y) 34 | assert.is.equal(4, a.max.x) 35 | assert.is.equal(5, a.max.y) 36 | end) 37 | 38 | it("clones a bound2", function() 39 | local a = bound2(vec2(1,2), vec2(4,5)) 40 | local b = a:clone() 41 | a.max = vec2.new(9,9) 42 | assert.is.equal(a.min, b.min) 43 | assert.is.not_equal(a.max, b.max) 44 | end) 45 | 46 | it("uses bound2 check()", function() 47 | local a = bound2(vec2(4,2), vec2(1,5)):check() 48 | assert.is.equal(1, a.min.x) 49 | assert.is.equal(2, a.min.y) 50 | assert.is.equal(4, a.max.x) 51 | assert.is.equal(5, a.max.y) 52 | end) 53 | 54 | it("queries a bound2 size", function() 55 | local a = bound2(vec2(1,2), vec2(4,6)) 56 | local v = a:size() 57 | local r = a:radius() 58 | assert.is.equal(3, v.x) 59 | assert.is.equal(4, v.y) 60 | 61 | assert.is.equal(1.5, r.x) 62 | assert.is.equal(2, r.y) 63 | end) 64 | 65 | it("sets a bound2 size", function() 66 | local a = bound2(vec2(1,2), vec2(4,5)) 67 | local b = a:with_size(vec2(1,1)) 68 | 69 | assert.is.equal(1, a.min.x) 70 | assert.is.equal(2, a.min.y) 71 | assert.is.equal(4, a.max.x) 72 | assert.is.equal(5, a.max.y) 73 | 74 | assert.is.equal(1, b.min.x) 75 | assert.is.equal(2, b.min.y) 76 | assert.is.equal(2, b.max.x) 77 | assert.is.equal(3, b.max.y) 78 | end) 79 | 80 | it("queries a bound2 center", function() 81 | local a = bound2(vec2(1,2), vec2(3,4)) 82 | local v = a:center() 83 | assert.is.equal(2, v.x) 84 | assert.is.equal(3, v.y) 85 | end) 86 | 87 | it("sets a bound2 center", function() 88 | local a = bound2(vec2(1,2), vec2(3,4)) 89 | local b = a:with_center(vec2(1,1)) 90 | 91 | assert.is.equal(1, a.min.x) 92 | assert.is.equal(2, a.min.y) 93 | assert.is.equal(3, a.max.x) 94 | assert.is.equal(4, a.max.y) 95 | 96 | assert.is.equal(0, b.min.x) 97 | assert.is.equal(0, b.min.y) 98 | assert.is.equal(2, b.max.x) 99 | assert.is.equal(2, b.max.y) 100 | end) 101 | 102 | it("sets a bound2 size centered", function() 103 | local a = bound2(vec2(1,2), vec2(3,4)) 104 | local b = a:with_size_centered(vec2(4,4)) 105 | 106 | assert.is.equal(1, a.min.x) 107 | assert.is.equal(2, a.min.y) 108 | assert.is.equal(3, a.max.x) 109 | assert.is.equal(4, a.max.y) 110 | 111 | assert.is.equal(0, b.min.x) 112 | assert.is.equal(1, b.min.y) 113 | assert.is.equal(4, b.max.x) 114 | assert.is.equal(5, b.max.y) 115 | end) 116 | 117 | it("insets a bound2", function() 118 | local a = bound2(vec2(1,2), vec2(5,10)) 119 | local b = a:inset(vec2(1,2)) 120 | 121 | assert.is.equal(1, a.min.x) 122 | assert.is.equal(2, a.min.y) 123 | assert.is.equal(5, a.max.x) 124 | assert.is.equal(10, a.max.y) 125 | 126 | assert.is.equal(2, b.min.x) 127 | assert.is.equal(4, b.min.y) 128 | assert.is.equal(4, b.max.x) 129 | assert.is.equal(8, b.max.y) 130 | end) 131 | 132 | it("outsets a bound2", function() 133 | local a = bound2(vec2(1,2), vec2(5,6)) 134 | local b = a:outset(vec2(1,2)) 135 | 136 | assert.is.equal(1, a.min.x) 137 | assert.is.equal(2, a.min.y) 138 | assert.is.equal(5, a.max.x) 139 | assert.is.equal(6, a.max.y) 140 | 141 | assert.is.equal(0, b.min.x) 142 | assert.is.equal(0, b.min.y) 143 | assert.is.equal(6, b.max.x) 144 | assert.is.equal(8, b.max.y) 145 | end) 146 | 147 | it("offsets a bound2", function() 148 | local a = bound2(vec2(1,2), vec2(5,6)) 149 | local b = a:offset(vec2(1,2)) 150 | 151 | assert.is.equal(1, a.min.x) 152 | assert.is.equal(2, a.min.y) 153 | assert.is.equal(5, a.max.x) 154 | assert.is.equal(6, a.max.y) 155 | 156 | assert.is.equal(2, b.min.x) 157 | assert.is.equal(4, b.min.y) 158 | assert.is.equal(6, b.max.x) 159 | assert.is.equal(8, b.max.y) 160 | end) 161 | 162 | it("tests for points inside bound2", function() 163 | local a = bound2(vec2(1,2), vec2(4,5)) 164 | 165 | assert.is_true(a:contains(vec2(1,2))) 166 | assert.is_true(a:contains(vec2(4,5))) 167 | assert.is_true(a:contains(vec2(2,3))) 168 | assert.is_not_true(a:contains(vec2(0,3))) 169 | assert.is_not_true(a:contains(vec2(5,3))) 170 | assert.is_not_true(a:contains(vec2(2,1))) 171 | assert.is_not_true(a:contains(vec2(2,6))) 172 | end) 173 | 174 | it("rounds a bound2", function() 175 | local a = bound2(vec2(1.1,1.9), vec2(3.9,5.1)):round() 176 | 177 | assert.is.equal(1, a.min.x) 178 | assert.is.equal(2, a.min.y) 179 | assert.is.equal(4, a.max.x) 180 | assert.is.equal(5, a.max.y) 181 | end) 182 | 183 | it("extends a bound2 with a point", function() 184 | local min = vec2(1,2) 185 | local max = vec2(4,5) 186 | local downright = vec2(8,8) 187 | local downleft = vec2(-4,8) 188 | local top = vec2(2, 0) 189 | 190 | local a = bound2(min, max) 191 | local temp 192 | 193 | temp = a:extend(downright) 194 | assert.is_true(a.min == min and a.max == max) 195 | assert.is_true(temp.min == min and temp.max == downright) 196 | temp = a:extend(downleft) 197 | assert.is_true(temp.min == vec2(-4,2) and temp.max == vec2(4,8)) 198 | temp = a:extend(top) 199 | assert.is_true(temp.min == vec2(1,0) and temp.max == max) 200 | end) 201 | 202 | it("extends a bound with another bound", function() 203 | local min = vec2(1,2) 204 | local max = vec2(4,5) 205 | local leftexpand = bound2.new(vec2(0,0), vec2(1.5, 6)) 206 | local rightexpand = bound2.new(vec2(1.5,0), vec2(5, 6)) 207 | 208 | local a = bound2(min, max) 209 | local temp 210 | 211 | temp = a:extend_bound(leftexpand) 212 | assert.is_equal(temp.min, vec2(0,0)) 213 | assert.is_equal(temp.max, vec2(4,6)) 214 | temp = temp:extend_bound(rightexpand) 215 | assert.is_equal(temp.min, vec2(0,0)) 216 | assert.is_equal(temp.max, vec2(5,6)) 217 | end) 218 | 219 | it("checks for bound2.zero", function() 220 | assert.is.equal(0, bound2.zero.min.x) 221 | assert.is.equal(0, bound2.zero.min.y) 222 | assert.is.equal(0, bound2.zero.max.x) 223 | assert.is.equal(0, bound2.zero.max.y) 224 | end) 225 | end) 226 | -------------------------------------------------------------------------------- /spec/bound3_spec.lua: -------------------------------------------------------------------------------- 1 | local bound3 = require "modules.bound3" 2 | local vec3 = require "modules.vec3" 3 | local DBL_EPSILON = require("modules.constants").DBL_EPSILON 4 | 5 | describe("bound3:", function() 6 | it("creates an empty bound3", function() 7 | local a = bound3() 8 | assert.is.equal(0, a.min.x) 9 | assert.is.equal(0, a.min.y) 10 | assert.is.equal(0, a.min.z) 11 | assert.is.equal(0, a.max.x) 12 | assert.is.equal(0, a.max.y) 13 | assert.is.equal(0, a.max.z) 14 | end) 15 | 16 | it("creates a bound3 from vec3s", function() 17 | local a = bound3(vec3(1,2,3), vec3(4,5,6)) 18 | assert.is.equal(1, a.min.x) 19 | assert.is.equal(2, a.min.y) 20 | assert.is.equal(3, a.min.z) 21 | assert.is.equal(4, a.max.x) 22 | assert.is.equal(5, a.max.y) 23 | assert.is.equal(6, a.max.z) 24 | end) 25 | 26 | it("creates a bound3 using new()", function() 27 | local a = bound3.new(vec3(1,2,3), vec3(4,5,6)) 28 | assert.is.equal(1, a.min.x) 29 | assert.is.equal(2, a.min.y) 30 | assert.is.equal(3, a.min.z) 31 | assert.is.equal(4, a.max.x) 32 | assert.is.equal(5, a.max.y) 33 | assert.is.equal(6, a.max.z) 34 | end) 35 | 36 | it("creates a bound3 using at()", function() 37 | local a = bound3.at(vec3(4,5,6), vec3(1,2,3)) 38 | assert.is.equal(1, a.min.x) 39 | assert.is.equal(2, a.min.y) 40 | assert.is.equal(3, a.min.z) 41 | assert.is.equal(4, a.max.x) 42 | assert.is.equal(5, a.max.y) 43 | assert.is.equal(6, a.max.z) 44 | end) 45 | 46 | it("clones a bound3", function() 47 | local a = bound3(vec3(1,2,3), vec3(4,5,6)) 48 | local b = a:clone() 49 | a.max = vec3.new(9,9,9) 50 | assert.is.equal(a.min, b.min) 51 | assert.is.not_equal(a.max, b.max) 52 | end) 53 | 54 | it("uses bound3 check()", function() 55 | local a = bound3(vec3(4,2,6), vec3(1,5,3)):check() 56 | assert.is.equal(1, a.min.x) 57 | assert.is.equal(2, a.min.y) 58 | assert.is.equal(3, a.min.z) 59 | assert.is.equal(4, a.max.x) 60 | assert.is.equal(5, a.max.y) 61 | assert.is.equal(6, a.max.z) 62 | end) 63 | 64 | it("queries a bound3 size", function() 65 | local a = bound3(vec3(1,2,3), vec3(4,6,8)) 66 | local v = a:size() 67 | local r = a:radius() 68 | assert.is.equal(3, v.x) 69 | assert.is.equal(4, v.y) 70 | assert.is.equal(5, v.z) 71 | 72 | assert.is.equal(1.5, r.x) 73 | assert.is.equal(2, r.y) 74 | assert.is.equal(2.5, r.z) 75 | end) 76 | 77 | it("sets a bound3 size", function() 78 | local a = bound3(vec3(1,2,3), vec3(4,5,6)) 79 | local b = a:with_size(vec3(1,1,1)) 80 | 81 | assert.is.equal(1, a.min.x) 82 | assert.is.equal(2, a.min.y) 83 | assert.is.equal(3, a.min.z) 84 | assert.is.equal(4, a.max.x) 85 | assert.is.equal(5, a.max.y) 86 | assert.is.equal(6, a.max.z) 87 | 88 | assert.is.equal(1, b.min.x) 89 | assert.is.equal(2, b.min.y) 90 | assert.is.equal(3, b.min.z) 91 | assert.is.equal(2, b.max.x) 92 | assert.is.equal(3, b.max.y) 93 | assert.is.equal(4, b.max.z) 94 | end) 95 | 96 | it("queries a bound3 center", function() 97 | local a = bound3(vec3(1,2,3), vec3(3,4,5)) 98 | local v = a:center() 99 | assert.is.equal(2, v.x) 100 | assert.is.equal(3, v.y) 101 | assert.is.equal(4, v.z) 102 | end) 103 | 104 | it("sets a bound3 center", function() 105 | local a = bound3(vec3(1,2,3), vec3(3,4,5)) 106 | local b = a:with_center(vec3(1,1,1)) 107 | 108 | assert.is.equal(1, a.min.x) 109 | assert.is.equal(2, a.min.y) 110 | assert.is.equal(3, a.min.z) 111 | assert.is.equal(3, a.max.x) 112 | assert.is.equal(4, a.max.y) 113 | assert.is.equal(5, a.max.z) 114 | 115 | assert.is.equal(0, b.min.x) 116 | assert.is.equal(0, b.min.y) 117 | assert.is.equal(0, b.min.z) 118 | assert.is.equal(2, b.max.x) 119 | assert.is.equal(2, b.max.y) 120 | assert.is.equal(2, b.max.z) 121 | end) 122 | 123 | it("sets a bound3 size centered", function() 124 | local a = bound3(vec3(1,2,3), vec3(3,4,5)) 125 | local b = a:with_size_centered(vec3(4,4,4)) 126 | 127 | assert.is.equal(1, a.min.x) 128 | assert.is.equal(2, a.min.y) 129 | assert.is.equal(3, a.min.z) 130 | assert.is.equal(3, a.max.x) 131 | assert.is.equal(4, a.max.y) 132 | assert.is.equal(5, a.max.z) 133 | 134 | assert.is.equal(0, b.min.x) 135 | assert.is.equal(1, b.min.y) 136 | assert.is.equal(2, b.min.z) 137 | assert.is.equal(4, b.max.x) 138 | assert.is.equal(5, b.max.y) 139 | assert.is.equal(6, b.max.z) 140 | end) 141 | 142 | it("insets a bound3", function() 143 | local a = bound3(vec3(1,2,3), vec3(5,10,11)) 144 | local b = a:inset(vec3(1,2,3)) 145 | 146 | assert.is.equal(1, a.min.x) 147 | assert.is.equal(2, a.min.y) 148 | assert.is.equal(3, a.min.z) 149 | assert.is.equal(5, a.max.x) 150 | assert.is.equal(10, a.max.y) 151 | assert.is.equal(11, a.max.z) 152 | 153 | assert.is.equal(2, b.min.x) 154 | assert.is.equal(4, b.min.y) 155 | assert.is.equal(6, b.min.z) 156 | assert.is.equal(4, b.max.x) 157 | assert.is.equal(8, b.max.y) 158 | assert.is.equal(8, b.max.z) 159 | end) 160 | 161 | it("outsets a bound3", function() 162 | local a = bound3(vec3(1,2,3), vec3(5,6,7)) 163 | local b = a:outset(vec3(1,2,3)) 164 | 165 | assert.is.equal(1, a.min.x) 166 | assert.is.equal(2, a.min.y) 167 | assert.is.equal(3, a.min.z) 168 | assert.is.equal(5, a.max.x) 169 | assert.is.equal(6, a.max.y) 170 | assert.is.equal(7, a.max.z) 171 | 172 | assert.is.equal(0, b.min.x) 173 | assert.is.equal(0, b.min.y) 174 | assert.is.equal(0, b.min.z) 175 | assert.is.equal(6, b.max.x) 176 | assert.is.equal(8, b.max.y) 177 | assert.is.equal(10, b.max.z) 178 | end) 179 | 180 | it("offsets a bound3", function() 181 | local a = bound3(vec3(1,2,3), vec3(5,6,7)) 182 | local b = a:offset(vec3(1,2,3)) 183 | 184 | assert.is.equal(1, a.min.x) 185 | assert.is.equal(2, a.min.y) 186 | assert.is.equal(3, a.min.z) 187 | assert.is.equal(5, a.max.x) 188 | assert.is.equal(6, a.max.y) 189 | assert.is.equal(7, a.max.z) 190 | 191 | assert.is.equal(2, b.min.x) 192 | assert.is.equal(4, b.min.y) 193 | assert.is.equal(6, b.min.z) 194 | assert.is.equal(6, b.max.x) 195 | assert.is.equal(8, b.max.y) 196 | assert.is.equal(10, b.max.z) 197 | end) 198 | 199 | it("tests for points inside bound3", function() 200 | local a = bound3(vec3(1,2,3), vec3(4,5,6)) 201 | 202 | assert.is_true(a:contains(vec3(1,2,3))) 203 | assert.is_true(a:contains(vec3(4,5,6))) 204 | assert.is_true(a:contains(vec3(2,3,4))) 205 | assert.is_not_true(a:contains(vec3(0,3,4))) 206 | assert.is_not_true(a:contains(vec3(5,3,4))) 207 | assert.is_not_true(a:contains(vec3(2,1,4))) 208 | assert.is_not_true(a:contains(vec3(2,6,4))) 209 | assert.is_not_true(a:contains(vec3(2,3,2))) 210 | assert.is_not_true(a:contains(vec3(2,3,7))) 211 | end) 212 | 213 | it("rounds a bound3", function() 214 | local a = bound3(vec3(1.1,1.9,3), vec3(3.9,5.1,6)):round() 215 | 216 | assert.is.equal(1, a.min.x) 217 | assert.is.equal(2, a.min.y) 218 | assert.is.equal(3, a.min.z) 219 | assert.is.equal(4, a.max.x) 220 | assert.is.equal(5, a.max.y) 221 | assert.is.equal(6, a.max.z) 222 | end) 223 | 224 | it("extends a bound3 with a point", function() 225 | local min = vec3(1,2,6) 226 | local max = vec3(4,5,9) 227 | local downright = vec3(8,8,10) 228 | local downleft = vec3(-4,8,10) 229 | local top = vec3(2, 0, 7) 230 | 231 | local a = bound3(min, max) 232 | local temp 233 | 234 | temp = a:extend(downright) 235 | assert.is_true(a.min == min and a.max == max) 236 | assert.is_true(temp.min == min and temp.max == downright) 237 | temp = a:extend(downleft) 238 | assert.is_true(temp.min == vec3(-4,2,6) and temp.max == vec3(4,8,10)) 239 | temp = a:extend(top) 240 | assert.is_true(temp.min == vec3(1,0,6) and temp.max == max) 241 | end) 242 | 243 | it("extends a bound with another bound", function() 244 | local min = vec3(1,2,3) 245 | local max = vec3(4,5,6) 246 | local leftexpand = bound3.new(vec3(0,0,4), vec3(1.5,6,5)) 247 | local rightexpand = bound3.new(vec3(1.5,0,1), vec3(5,6,7)) 248 | 249 | local a = bound3(min, max) 250 | local temp 251 | 252 | temp = a:extend_bound(leftexpand) 253 | assert.is_equal(temp.min, vec3(0,0,3)) 254 | assert.is_equal(temp.max, vec3(4,6,6)) 255 | temp = temp:extend_bound(rightexpand) 256 | assert.is_equal(temp.min, vec3(0,0,1)) 257 | assert.is_equal(temp.max, vec3(5,6,7)) 258 | end) 259 | 260 | it("checks for bound3.zero", function() 261 | assert.is.equal(0, bound3.zero.min.x) 262 | assert.is.equal(0, bound3.zero.min.y) 263 | assert.is.equal(0, bound3.zero.min.z) 264 | assert.is.equal(0, bound3.zero.max.x) 265 | assert.is.equal(0, bound3.zero.max.y) 266 | assert.is.equal(0, bound3.zero.max.z) 267 | end) 268 | end) 269 | -------------------------------------------------------------------------------- /spec/color_spec.lua: -------------------------------------------------------------------------------- 1 | local color = require "modules.color" 2 | local DBL_EPSILON = require("modules.constants").DBL_EPSILON 3 | 4 | local function assert_is_float_equal(a, b) 5 | if math.abs(a - b) > DBL_EPSILON then 6 | assert.is.equal(a, b) 7 | end 8 | end 9 | 10 | local function assert_is_approx_equal(a, b) 11 | if math.abs(a - b) > 0.001 then 12 | assert.is.equal(a, b) 13 | end 14 | end 15 | 16 | 17 | describe("color:", function() 18 | it("operators: add, subract, multiply", function() 19 | local c = color(1, 1, 1, 1) 20 | assert.is_true(c:is_color()) 21 | local r = c + c 22 | assert.is_true(r:is_color()) 23 | assert_is_float_equal(r[1], 2) 24 | assert_is_float_equal(r[2], 2) 25 | assert_is_float_equal(r[3], 2) 26 | r = c - c 27 | assert.is_true(r:is_color()) 28 | assert_is_float_equal(r[1], 0) 29 | assert_is_float_equal(r[2], 0) 30 | assert_is_float_equal(r[3], 0) 31 | r = c * 5 32 | assert.is_true(r:is_color()) 33 | assert_is_float_equal(r[1], 5) 34 | assert_is_float_equal(r[2], 5) 35 | assert_is_float_equal(r[3], 5) 36 | end) 37 | 38 | it("rgb -> hsv -> rgb", function() 39 | local c = color(1,1,1,1) 40 | local hsv = c:color_to_hsv_table() 41 | local c1 = color.hsv_to_color_table(hsv) 42 | local c2 = color.from_hsva(hsv[1], hsv[2], hsv[3], hsv[4]) 43 | local c3 = color.from_hsv(hsv[1], hsv[2], hsv[3]) 44 | c3[4] = c[4] 45 | for i=1,4 do 46 | assert_is_float_equal(c[i], c1[i]) 47 | assert_is_float_equal(c[i], c2[i]) 48 | assert_is_float_equal(c[i], c3[i]) 49 | end 50 | assert.is_true(c:is_color()) 51 | assert.is_true(c1:is_color()) 52 | assert.is_true(c2:is_color()) 53 | assert.is_true(c3:is_color()) 54 | end) 55 | 56 | it("hsv -> rgb -> hsv", function() 57 | local hsv1 = { 0, 0.3, 0.8, 0.9 } 58 | for h=0,1, 0.1 do 59 | hsv1[1] = h 60 | local cc = color.hsv_to_color_table(hsv1) 61 | local hsv2 = cc:color_to_hsv_table() 62 | for i=1,4 do 63 | assert_is_approx_equal(hsv1[i], hsv2[i]) 64 | end 65 | end 66 | end) 67 | 68 | it("unpack", function() 69 | local c = color(122/255, 20/255, 122/255, 255/255) 70 | local r, g, b, a = c:unpack() 71 | assert_is_float_equal(c[1], r) 72 | assert_is_float_equal(c[2], g) 73 | assert_is_float_equal(c[3], b) 74 | assert_is_float_equal(c[4], a) 75 | r, g, b, a = c:as_255() 76 | assert_is_float_equal(122, r) 77 | assert_is_float_equal(20, g) 78 | assert_is_float_equal(122, b) 79 | assert_is_float_equal(255, a) 80 | end) 81 | 82 | it("set hsv", function() 83 | -- hsv value conversion values from http://colorizer.org/ 84 | local c = color(122/255, 20/255, 122/255, 1) 85 | local hsv = c:color_to_hsv_table() 86 | assert_is_approx_equal(hsv[1], 300/360) 87 | assert_is_approx_equal(hsv[2], 0.8361) 88 | assert_is_approx_equal(hsv[3], 0.4784) 89 | local r = c:hue(200/360) 90 | assert_is_approx_equal(r[1], 20/255) 91 | assert_is_approx_equal(r[2], 88/255) 92 | assert_is_approx_equal(r[3], 122/255) 93 | r = c:saturation(0.2) 94 | assert_is_approx_equal(r[1], 122/255) 95 | assert_is_approx_equal(r[2], 97.6/255) 96 | assert_is_approx_equal(r[3], 122/255) 97 | r = c:value(0.2) 98 | assert_is_approx_equal(r[1], 51/255) 99 | assert_is_approx_equal(r[2], 8.36/255) 100 | assert_is_approx_equal(r[3], 51/255) 101 | end) 102 | 103 | it("lighten a color", function() 104 | local c = color(0, 0, 0, 0) 105 | local r = c:lighten(0.1) 106 | assert.is.equal(r[1], 0.1) 107 | r = c:lighten(1000) 108 | assert.is.equal(r[1], 1) 109 | end) 110 | 111 | it("darken a color", function() 112 | local c = color(1, 1, 1, 1) 113 | local r = c:darken(0.04) 114 | assert.is.equal(r[1], 0.96) 115 | r = c:darken(1000) 116 | assert.is.equal(r[1], 0) 117 | end) 118 | 119 | it("multiply a color by a scalar", function() 120 | local c = color(1, 1, 1, 1) 121 | local r = c:multiply(0.04) 122 | assert.is.equal(r[1], 0.04) 123 | 124 | r = c:multiply(0) 125 | for i=1,3 do 126 | assert.is.equal(0, r[i]) 127 | end 128 | assert.is.equal(1, r[4]) 129 | end) 130 | 131 | it("modify alpha", function() 132 | local c = color(1, 1, 1, 1) 133 | local r = c:alpha(0.1) 134 | assert.is.equal(r[4], 0.1) 135 | r = c:opacity(0.5) 136 | assert.is.equal(r[4], 0.5) 137 | r = c:opacity(0.5) 138 | :opacity(0.5) 139 | assert.is.equal(r[4], 0.25) 140 | end) 141 | 142 | it("invert", function() 143 | local c = color(1, 0.6, 0.25, 1) 144 | local r = c:invert() 145 | assert_is_float_equal(r[1], 0) 146 | assert_is_float_equal(r[2], 0.4) 147 | assert_is_float_equal(r[3], 0.75) 148 | assert_is_float_equal(r[4], 1) 149 | r = c:invert() 150 | :invert() 151 | for i=1,4 do 152 | assert.is.equal(c[i], r[i]) 153 | end 154 | end) 155 | 156 | it("lerp", function() 157 | local a = color(1, 0.6, 0.25, 1) 158 | local b = color(1, 0.8, 0.75, 0.5) 159 | local r = a:lerp(b, 0.5) 160 | assert_is_float_equal(r[1], 1) 161 | assert_is_float_equal(r[2], 0.7) 162 | assert_is_float_equal(r[3], 0.5) 163 | assert_is_float_equal(r[4], 0.75) 164 | local r_a = a:lerp(b, 0) 165 | local r_b = a:lerp(b, 1) 166 | for i=1,4 do 167 | assert.is.equal(a[i], r_a[i]) 168 | assert.is.equal(b[i], r_b[i]) 169 | end 170 | end) 171 | 172 | it("linear_to_gamma -> gamma_to_linear round trip", function() 173 | local c = color(0.25, 0.25, 0.25, 1) 174 | local r = color.gamma_to_linear(c:linear_to_gamma()) 175 | for i=1,4 do 176 | assert_is_approx_equal(c[i], r[i]) 177 | end 178 | end) 179 | 180 | end) 181 | 182 | --[[ 183 | to_string(a) 184 | --]] 185 | -------------------------------------------------------------------------------- /spec/intersect_spec.lua: -------------------------------------------------------------------------------- 1 | local intersect = require "modules.intersect" 2 | local vec3 = require "modules.vec3" 3 | local mat4 = require "modules.mat4" 4 | 5 | describe("intersect:", function() 6 | it("intersects a point with a triangle", function() 7 | local a = vec3() 8 | local b = vec3(0, 0, 5) 9 | local c = { 10 | vec3(-1, -1, 0), 11 | vec3( 1, -1, 0), 12 | vec3( 0.5, 1, 0) 13 | } 14 | assert.is_true(intersect.point_triangle(a, c)) 15 | assert.is_not_true(intersect.point_triangle(b, c)) 16 | end) 17 | 18 | it("intersects a point with an aabb", function() 19 | local a = vec3() 20 | local b = vec3(0, 0, 5) 21 | local c = { 22 | min = vec3(-1), 23 | max = vec3( 1) 24 | } 25 | assert.is_true(intersect.point_aabb(a, c)) 26 | assert.is_not_true(intersect.point_aabb(b, c)) 27 | end) 28 | 29 | it("intersects a point with a frustum", function() 30 | pending("TODO") 31 | end) 32 | 33 | it("intersects a ray with a triangle", function() 34 | local a = { 35 | position = vec3(0.5, 0.5, -1), 36 | direction = vec3(0, 0, 1) 37 | } 38 | local b = { 39 | position = vec3(0.5, 0.5, -1), 40 | direction = vec3(0, 0, -1) 41 | } 42 | local c = { 43 | vec3(-1, -1, 0), 44 | vec3( 1, -1, 0), 45 | vec3( 0.5, 1, 0) 46 | } 47 | assert.is_true(vec3.is_vec3(intersect.ray_triangle(a, c))) 48 | assert.is_not_true(intersect.ray_triangle(b, c)) 49 | end) 50 | 51 | it("intersects a ray with a sphere", function() 52 | local a = { 53 | position = vec3(0, 0, -2), 54 | direction = vec3(0, 0, 1) 55 | } 56 | local b = { 57 | position = vec3(0, 0, -2), 58 | direction = vec3(0, 0, -1) 59 | } 60 | local c = { 61 | position = vec3(), 62 | radius = 1 63 | } 64 | 65 | local w, x = intersect.ray_sphere(a, c) 66 | local y, z = intersect.ray_sphere(b, c) 67 | assert.is_true(vec3.is_vec3(w)) 68 | assert.is_not_true(y) 69 | end) 70 | 71 | it("intersects a ray with an aabb", function() 72 | local a = { 73 | position = vec3(0, 0, -2), 74 | direction = vec3(0, 0, 1) 75 | } 76 | local b = { 77 | position = vec3(0, 0, -2), 78 | direction = vec3(0, 0, -1) 79 | } 80 | local c = { 81 | min = vec3(-1), 82 | max = vec3( 1) 83 | } 84 | 85 | local w, x = intersect.ray_aabb(a, c) 86 | local y, z = intersect.ray_aabb(b, c) 87 | assert.is_true(vec3.is_vec3(w)) 88 | assert.is_not_true(y) 89 | end) 90 | 91 | it("intersects a ray with a plane", function() 92 | local a = { 93 | position = vec3(0, 0, 1), 94 | direction = vec3(0, 0, -1) 95 | } 96 | local b = { 97 | position = vec3(0, 0, 1), 98 | direction = vec3(0, 0, 1) 99 | } 100 | local c = { 101 | position = vec3(), 102 | normal = vec3(0, 0, 1) 103 | } 104 | 105 | local w, x = intersect.ray_plane(a, c) 106 | local y, z = intersect.ray_plane(b, c) 107 | assert.is_true(vec3.is_vec3(w)) 108 | assert.is_not_true(y) 109 | end) 110 | 111 | it("intersects a line with a line", function() 112 | local a = { 113 | vec3(0, 0, -1), 114 | vec3(0, 0, 1) 115 | } 116 | local b = { 117 | vec3(0, 0, -1), 118 | vec3(0, 1, -1) 119 | } 120 | local c = { 121 | vec3(-1, 0, 0), 122 | vec3( 1, 0, 0) 123 | } 124 | 125 | local w, x = intersect.line_line(a, c, 0.001) 126 | local y, z = intersect.line_line(b, c, 0.001) 127 | local u, v = intersect.line_line(b, c) 128 | assert.is_truthy(w) 129 | assert.is_not_truthy(y) 130 | assert.is_truthy(u) 131 | end) 132 | 133 | it("intersects a segment with a segment", function() 134 | local a = { 135 | vec3(0, 0, -1), 136 | vec3(0, 0, 1) 137 | } 138 | local b = { 139 | vec3(0, 0, -1), 140 | vec3(0, 1, -1) 141 | } 142 | local c = { 143 | vec3(-1, 0, 0), 144 | vec3( 1, 0, 0) 145 | } 146 | 147 | local w, x = intersect.segment_segment(a, c, 0.001) 148 | local y, z = intersect.segment_segment(b, c, 0.001) 149 | local u, v = intersect.segment_segment(b, c) 150 | assert.is_truthy(w) 151 | assert.is_not_truthy(y) 152 | assert.is_truthy(u) 153 | end) 154 | 155 | it("intersects an aabb with an aabb", function() 156 | local a = { 157 | min = vec3(-1), 158 | max = vec3( 1) 159 | } 160 | local b = { 161 | min = vec3(-5), 162 | max = vec3(-3) 163 | } 164 | local c = { 165 | min = vec3(), 166 | max = vec3(2) 167 | } 168 | assert.is_true(intersect.aabb_aabb(a, c)) 169 | assert.is_not_true(intersect.aabb_aabb(b, c)) 170 | end) 171 | 172 | it("intersects an aabb with an obb", function() 173 | local r = mat4():rotate(mat4(), math.pi / 4, vec3.unit_z) 174 | 175 | local a = { 176 | position = vec3(), 177 | extent = vec3(0.5) 178 | } 179 | local b = { 180 | position = vec3(), 181 | extent = vec3(0.5), 182 | rotation = r 183 | } 184 | local c = { 185 | position = vec3(0, 0, 2), 186 | extent = vec3(0.5), 187 | rotation = r 188 | } 189 | assert.is_true(vec3.is_vec3(intersect.aabb_obb(a, b))) 190 | assert.is_not_true(intersect.aabb_obb(a, c)) 191 | end) 192 | 193 | it("intersects an aabb with a sphere", function() 194 | local a = { 195 | min = vec3(-1), 196 | max = vec3( 1) 197 | } 198 | local b = { 199 | min = vec3(-5), 200 | max = vec3(-3) 201 | } 202 | local c = { 203 | position = vec3(0, 0, 3), 204 | radius = 3 205 | } 206 | assert.is_true(intersect.aabb_sphere(a, c)) 207 | assert.is_not_true(intersect.aabb_sphere(b, c)) 208 | end) 209 | 210 | it("intersects an aabb with a frustum", function() 211 | pending("TODO") 212 | end) 213 | 214 | it("encapsulates an aabb", function() 215 | local a = { 216 | min = vec3(-1), 217 | max = vec3( 1) 218 | } 219 | local b = { 220 | min = vec3(-1.5), 221 | max = vec3( 1.5) 222 | } 223 | local c = { 224 | min = vec3(-0.5), 225 | max = vec3( 0.5) 226 | } 227 | local d = { 228 | min = vec3(-1), 229 | max = vec3( 1) 230 | } 231 | assert.is_true(intersect.encapsulate_aabb(a, d)) 232 | assert.is_true(intersect.encapsulate_aabb(b, d)) 233 | assert.is_not_true(intersect.encapsulate_aabb(c, d)) 234 | end) 235 | 236 | it("intersects a circle with a circle", function() 237 | local a = { 238 | position = vec3(0, 0, 6), 239 | radius = 3 240 | } 241 | local b = { 242 | position = vec3(0, 0, 7), 243 | radius = 3 244 | } 245 | local c = { 246 | position = vec3(), 247 | radius = 3 248 | } 249 | assert.is_true(intersect.circle_circle(a, c)) 250 | assert.is_not_true(intersect.circle_circle(b, c)) 251 | end) 252 | 253 | it("intersects a sphere with a sphere", function() 254 | local a = { 255 | position = vec3(0, 0, 6), 256 | radius = 3 257 | } 258 | local b = { 259 | position = vec3(0, 0, 7), 260 | radius = 3 261 | } 262 | local c = { 263 | position = vec3(), 264 | radius = 3 265 | } 266 | assert.is_true(intersect.sphere_sphere(a, c)) 267 | assert.is_not_true(intersect.sphere_sphere(b, c)) 268 | end) 269 | 270 | it("intersects a sphere with a frustum", function() 271 | pending("TODO") 272 | end) 273 | 274 | it("intersects a capsule with another capsule", function() 275 | pending("TODO") 276 | end) 277 | end) 278 | -------------------------------------------------------------------------------- /spec/mat4_spec.lua: -------------------------------------------------------------------------------- 1 | local mat4 = require "modules.mat4" 2 | local vec3 = require "modules.vec3" 3 | local quat = require "modules.quat" 4 | local utils = require "modules.utils" 5 | local FLT_EPSILON = require("modules.constants").FLT_EPSILON 6 | 7 | describe("mat4:", function() 8 | it("creates an identity matrix", function() 9 | local a = mat4() 10 | assert.is.equal(1, a[1]) 11 | assert.is.equal(0, a[2]) 12 | assert.is.equal(0, a[3]) 13 | assert.is.equal(0, a[4]) 14 | assert.is.equal(0, a[5]) 15 | assert.is.equal(1, a[6]) 16 | assert.is.equal(0, a[7]) 17 | assert.is.equal(0, a[8]) 18 | assert.is.equal(0, a[9]) 19 | assert.is.equal(0, a[10]) 20 | assert.is.equal(1, a[11]) 21 | assert.is.equal(0, a[12]) 22 | assert.is.equal(0, a[13]) 23 | assert.is.equal(0, a[14]) 24 | assert.is.equal(0, a[15]) 25 | assert.is.equal(1, a[16]) 26 | assert.is_true(a:is_mat4()) 27 | end) 28 | 29 | it("creates a filled matrix", function() 30 | local a = mat4 { 31 | 3, 3, 3, 3, 32 | 4, 4, 4, 4, 33 | 5, 5, 5, 5, 34 | 6, 6, 6, 6 35 | } 36 | assert.is.equal(3, a[1]) 37 | assert.is.equal(3, a[2]) 38 | assert.is.equal(3, a[3]) 39 | assert.is.equal(3, a[4]) 40 | assert.is.equal(4, a[5]) 41 | assert.is.equal(4, a[6]) 42 | assert.is.equal(4, a[7]) 43 | assert.is.equal(4, a[8]) 44 | assert.is.equal(5, a[9]) 45 | assert.is.equal(5, a[10]) 46 | assert.is.equal(5, a[11]) 47 | assert.is.equal(5, a[12]) 48 | assert.is.equal(6, a[13]) 49 | assert.is.equal(6, a[14]) 50 | assert.is.equal(6, a[15]) 51 | assert.is.equal(6, a[16]) 52 | end) 53 | 54 | it("creates a filled matrix from vec4s", function() 55 | local a = mat4 { 56 | { 3, 3, 3, 3 }, 57 | { 4, 4, 4, 4 }, 58 | { 5, 5, 5, 5 }, 59 | { 6, 6, 6, 6 } 60 | } 61 | assert.is.equal(3, a[1]) 62 | assert.is.equal(3, a[2]) 63 | assert.is.equal(3, a[3]) 64 | assert.is.equal(3, a[4]) 65 | assert.is.equal(4, a[5]) 66 | assert.is.equal(4, a[6]) 67 | assert.is.equal(4, a[7]) 68 | assert.is.equal(4, a[8]) 69 | assert.is.equal(5, a[9]) 70 | assert.is.equal(5, a[10]) 71 | assert.is.equal(5, a[11]) 72 | assert.is.equal(5, a[12]) 73 | assert.is.equal(6, a[13]) 74 | assert.is.equal(6, a[14]) 75 | assert.is.equal(6, a[15]) 76 | assert.is.equal(6, a[16]) 77 | end) 78 | 79 | it("creates a filled matrix from a 3x3 matrix", function() 80 | local a = mat4 { 81 | 3, 3, 3, 82 | 4, 4, 4, 83 | 5, 5, 5 84 | } 85 | assert.is.equal(3, a[1]) 86 | assert.is.equal(3, a[2]) 87 | assert.is.equal(3, a[3]) 88 | assert.is.equal(0, a[4]) 89 | assert.is.equal(4, a[5]) 90 | assert.is.equal(4, a[6]) 91 | assert.is.equal(4, a[7]) 92 | assert.is.equal(0, a[8]) 93 | assert.is.equal(5, a[9]) 94 | assert.is.equal(5, a[10]) 95 | assert.is.equal(5, a[11]) 96 | assert.is.equal(0, a[12]) 97 | assert.is.equal(0, a[13]) 98 | assert.is.equal(0, a[14]) 99 | assert.is.equal(0, a[15]) 100 | assert.is.equal(1, a[16]) 101 | end) 102 | 103 | it("creates a matrix from perspective", function() 104 | local a = mat4.from_perspective(45, 1, 0.1, 1000) 105 | assert.is_true(utils.tolerance( 2.414-a[1], 0.001)) 106 | assert.is_true(utils.tolerance( 2.414-a[6], 0.001)) 107 | assert.is_true(utils.tolerance(-1 -a[11], 0.001)) 108 | assert.is_true(utils.tolerance(-1 -a[12], 0.001)) 109 | assert.is_true(utils.tolerance(-0.2 -a[15], 0.001)) 110 | end) 111 | 112 | it("creates a matrix from HMD perspective", function() 113 | local t = { 114 | LeftTan = 2.3465312, 115 | RightTan = 0.9616399, 116 | UpTan = 2.8664987, 117 | DownTan = 2.8664987 118 | } 119 | local a = mat4.from_hmd_perspective(t, 0.1, 1000, false, false) 120 | assert.is_true(utils.tolerance(a[1] - 0.605, 0.001)) 121 | assert.is_true(utils.tolerance(a[6] - 0.349, 0.001)) 122 | assert.is_true(utils.tolerance(a[9] - -0.419, 0.001)) 123 | assert.is_true(utils.tolerance(a[11]- -1.000, 0.001)) 124 | assert.is_true(utils.tolerance(a[12]- -1.000, 0.001)) 125 | assert.is_true(utils.tolerance(a[15]- -0.200, 0.001)) 126 | end) 127 | 128 | it("clones a matrix", function() 129 | local a = mat4.identity() 130 | local b = a:clone() 131 | assert.is.equal(a, b) 132 | end) 133 | 134 | it("multiplies two 4x4 matrices", function() 135 | local a = mat4 { 136 | 1, 5, 9, 13, 137 | 2, 6, 10, 14, 138 | 3, 7, 11, 15, 139 | 4, 8, 12, 16 140 | } 141 | local b = mat4 { 142 | 1, 2, 3, 4, 143 | 5, 6, 7, 8, 144 | 9, 10, 11, 12, 145 | 13, 14, 15, 16 146 | } 147 | local c = mat4():mul(a, b) 148 | local d = a * b 149 | local e = mat4():mul{a, b} 150 | assert.is.equal(30, c[1]) 151 | assert.is.equal(70, c[2]) 152 | assert.is.equal(110, c[3]) 153 | assert.is.equal(150, c[4]) 154 | assert.is.equal(70, c[5]) 155 | assert.is.equal(174, c[6]) 156 | assert.is.equal(278, c[7]) 157 | assert.is.equal(382, c[8]) 158 | assert.is.equal(110, c[9]) 159 | assert.is.equal(278, c[10]) 160 | assert.is.equal(446, c[11]) 161 | assert.is.equal(614, c[12]) 162 | assert.is.equal(150, c[13]) 163 | assert.is.equal(382, c[14]) 164 | assert.is.equal(614, c[15]) 165 | assert.is.equal(846, c[16]) 166 | assert.is.equal(c, d) 167 | assert.is.equal(c, e) 168 | end) 169 | 170 | it("multiplies a matrix and a vec4", function() 171 | local a = mat4 { 172 | 1, 2, 3, 4, 173 | 5, 6, 7, 8, 174 | 9, 10, 11, 12, 175 | 13, 14, 15, 16 176 | } 177 | local b = { 10, 20, 30, 40 } 178 | local c = mat4.mul_vec4(mat4(), a, b) 179 | local d = a * b 180 | assert.is.equal(900, c[1]) 181 | assert.is.equal(1000, c[2]) 182 | assert.is.equal(1100, c[3]) 183 | assert.is.equal(1200, c[4]) 184 | 185 | assert.is.equal(c[1], d[1]) 186 | assert.is.equal(c[2], d[2]) 187 | assert.is.equal(c[3], d[3]) 188 | assert.is.equal(c[4], d[4]) 189 | end) 190 | 191 | it("verifies mat4 composition order", function() 192 | local a = mat4 { 193 | 1, 5, 9, 13, 194 | 2, 6, 10, 14, 195 | 3, 7, 11, 15, 196 | 4, 8, 12, 16 197 | } 198 | local b = mat4 { 199 | 2, 3, 5, 7, 200 | 11, 13, 17, 19, 201 | 23, 29, 31, 37, 202 | 41, 43, 47, 53 203 | } 204 | local c = mat4():mul(a, b) 205 | local d = a * b 206 | 207 | local v = { 10, 20, 30, 40 } 208 | 209 | local cv = c * v 210 | local abv = a*(b*v) 211 | 212 | assert.is.equal(cv.x, abv.x) -- Verify (a*b)*v == a*(b*v) 213 | assert.is.equal(cv.y, abv.y) 214 | assert.is.equal(cv.z, abv.z) 215 | end) 216 | 217 | it("scales a matrix", function() 218 | local a = mat4():scale(mat4(), vec3(5, 5, 5)) 219 | assert.is.equal(5, a[1]) 220 | assert.is.equal(5, a[6]) 221 | assert.is.equal(5, a[11]) 222 | end) 223 | 224 | it("rotates a matrix", function() 225 | local a = mat4():rotate(mat4(), math.rad(45), vec3.unit_z) 226 | assert.is_true(utils.tolerance( 0.7071-a[1], 0.001)) 227 | assert.is_true(utils.tolerance( 0.7071-a[2], 0.001)) 228 | assert.is_true(utils.tolerance(-0.7071-a[5], 0.001)) 229 | assert.is_true(utils.tolerance( 0.7071-a[6], 0.001)) 230 | end) 231 | 232 | it("translates a matrix", function() 233 | local a = mat4():translate(mat4(), vec3(5, 5, 5)) 234 | assert.is.equal(5, a[13]) 235 | assert.is.equal(5, a[14]) 236 | assert.is.equal(5, a[15]) 237 | end) 238 | 239 | it("inverts a matrix", function() 240 | local a = mat4() 241 | a = a:rotate(a, math.pi/4, vec3.unit_y) 242 | a = a:translate(a, vec3(4, 5, 6)) 243 | 244 | local b = mat4.invert(mat4(), a) 245 | local c = a * b 246 | assert.is.equal(mat4(), c) 247 | 248 | local d = mat4() 249 | d:rotate(d, math.pi/4, vec3.unit_y) 250 | d:translate(d, vec3(4, 5, 6)) 251 | 252 | local e = -d 253 | local f = d * e 254 | assert.is.equal(mat4(), f) 255 | end) 256 | 257 | it("transposes a matrix", function() 258 | local a = mat4({ 259 | 1, 1, 1, 1, 260 | 2, 2, 2, 2, 261 | 3, 3, 3, 3, 262 | 4, 4, 4, 4 263 | }) 264 | a = a:transpose(a) 265 | assert.is.equal(1, a[1]) 266 | assert.is.equal(2, a[2]) 267 | assert.is.equal(3, a[3]) 268 | assert.is.equal(4, a[4]) 269 | assert.is.equal(1, a[5]) 270 | assert.is.equal(2, a[6]) 271 | assert.is.equal(3, a[7]) 272 | assert.is.equal(4, a[8]) 273 | assert.is.equal(1, a[9]) 274 | assert.is.equal(2, a[10]) 275 | assert.is.equal(3, a[11]) 276 | assert.is.equal(4, a[12]) 277 | assert.is.equal(1, a[13]) 278 | assert.is.equal(2, a[14]) 279 | assert.is.equal(3, a[15]) 280 | assert.is.equal(4, a[16]) 281 | end) 282 | 283 | it("shears a matrix", function() 284 | local yx, zx, xy, zy, xz, yz = 1, 1, 1, -1, -1, -1 285 | local a = mat4():shear(mat4(), yx, zx, xy, zy, xz, yz) 286 | assert.is.equal( 1, a[2]) 287 | assert.is.equal( 1, a[3]) 288 | assert.is.equal( 1, a[5]) 289 | assert.is.equal(-1, a[7]) 290 | assert.is.equal(-1, a[9]) 291 | assert.is.equal(-1, a[10]) 292 | end) 293 | 294 | it("reflects a matrix along a plane", function() 295 | local origin = vec3(5, 1, 0) 296 | local normal = vec3(0, -1, 0):normalize() 297 | local a = mat4():reflect(mat4(), origin, normal) 298 | local p = a * vec3(-5, 2, 5) 299 | assert.is.equal(p.x, -5) 300 | assert.is.equal(p.y, 0) 301 | assert.is.equal(p.z, 5) 302 | end) 303 | 304 | it("projects a point into screen space", function() 305 | local znear = 0.1 306 | local zfar = 1000 307 | local proj = mat4.from_perspective(45, 1, znear, zfar) 308 | local vp = { 0, 0, 400, 400 } 309 | 310 | -- -z is away from the viewer into the far plane 311 | local p1 = vec3(0, 0, -znear) 312 | local c1 = mat4.project(p1, proj, vp) 313 | assert.is.near(0, c1.z, 0.0001) 314 | assert.is.equal(200, c1.x) 315 | assert.is.equal(200, c1.y) 316 | 317 | local p2 = vec3(0, 0, -zfar) 318 | local c2 = mat4.project(p2, proj, vp) 319 | assert.is.near(1, c2.z, 0.0001) 320 | assert.is.equal(200, c2.x) 321 | assert.is.equal(200, c2.y) 322 | 323 | local p3 = vec3(0, 0, zfar) 324 | local c3 = mat4.project(p3, proj, vp) 325 | assert.is_true(c3.z < 0) 326 | assert.is.equal(200, c3.x) 327 | assert.is.equal(200, c3.y) 328 | end) 329 | 330 | it("unprojects a point into world space", function() 331 | local p = vec3(0, 0, -10) 332 | local proj = mat4.from_perspective(45, 1, 0.1, 1000) 333 | local vp = { 0, 0, 400, 400 } 334 | local c = mat4.project(p, proj, vp) 335 | local d = mat4.unproject(c, proj, vp) 336 | assert.is.near(0.0, p.x-d.x, 0.0001) 337 | assert.is.near(0.0, p.y-d.y, 0.0001) 338 | assert.is.near(0.0, p.z-d.z, 0.0001) 339 | end) 340 | 341 | it("transforms a matrix to look at a point", function() 342 | local e = vec3(0, 0, 1.55) 343 | local c = vec3(4, 7, 1) 344 | local u = vec3(0, 0, 1) 345 | local a = mat4():look_at(e, c, u) 346 | 347 | assert.is_true(utils.tolerance( 0.868-a[1], 0.001)) 348 | assert.is_true(utils.tolerance( 0.034-a[2], 0.001)) 349 | assert.is_true(utils.tolerance(-0.495-a[3], 0.001)) 350 | assert.is_true(utils.tolerance( 0 -a[4], 0.001)) 351 | 352 | assert.is_true(utils.tolerance(-0.496-a[5], 0.001)) 353 | assert.is_true(utils.tolerance( 0.059-a[6], 0.001)) 354 | assert.is_true(utils.tolerance(-0.866-a[7], 0.001)) 355 | assert.is_true(utils.tolerance( 0 -a[8], 0.001)) 356 | 357 | assert.is_true(utils.tolerance( 0 -a[9], 0.001)) 358 | assert.is_true(utils.tolerance( 0.998-a[10], 0.001)) 359 | assert.is_true(utils.tolerance( 0.068-a[11], 0.001)) 360 | assert.is_true(utils.tolerance( 0 -a[12], 0.001)) 361 | 362 | assert.is_true(utils.tolerance( 0 -a[13], 0.001)) 363 | assert.is_true(utils.tolerance(-1.546-a[14], 0.001)) 364 | assert.is_true(utils.tolerance(-0.106-a[15], 0.001)) 365 | assert.is_true(utils.tolerance( 1 -a[16], 0.001)) 366 | end) 367 | 368 | it("converts a matrix to vec4s", function() 369 | local a = mat4 { 370 | 1, 2, 3, 4, 371 | 5, 6, 7, 8, 372 | 9, 10, 11, 12, 373 | 13, 14, 15, 16 374 | } 375 | local v = a:to_vec4s() 376 | assert.is_true(type(v) == "table") 377 | assert.is_true(type(v[1]) == "table") 378 | assert.is_true(type(v[2]) == "table") 379 | assert.is_true(type(v[3]) == "table") 380 | assert.is_true(type(v[4]) == "table") 381 | 382 | assert.is.equal(1, v[1][1]) 383 | assert.is.equal(2, v[1][2]) 384 | assert.is.equal(3, v[1][3]) 385 | assert.is.equal(4, v[1][4]) 386 | 387 | assert.is.equal(5, v[2][1]) 388 | assert.is.equal(6, v[2][2]) 389 | assert.is.equal(7, v[2][3]) 390 | assert.is.equal(8, v[2][4]) 391 | 392 | assert.is.equal(9, v[3][1]) 393 | assert.is.equal(10, v[3][2]) 394 | assert.is.equal(11, v[3][3]) 395 | assert.is.equal(12, v[3][4]) 396 | 397 | assert.is.equal(13, v[4][1]) 398 | assert.is.equal(14, v[4][2]) 399 | assert.is.equal(15, v[4][3]) 400 | assert.is.equal(16, v[4][4]) 401 | end) 402 | 403 | it("converts a matrix to vec4s, column-wise", function() 404 | local a = mat4 { 405 | 1, 2, 3, 4, 406 | 5, 6, 7, 8, 407 | 9, 10, 11, 12, 408 | 13, 14, 15, 16 409 | } 410 | local v = a:to_vec4s_cols() 411 | assert.is_true(type(v) == "table") 412 | assert.is_true(type(v[1]) == "table") 413 | assert.is_true(type(v[2]) == "table") 414 | assert.is_true(type(v[3]) == "table") 415 | assert.is_true(type(v[4]) == "table") 416 | 417 | assert.is.equal(1, v[1][1]) 418 | assert.is.equal(5, v[1][2]) 419 | assert.is.equal(9, v[1][3]) 420 | assert.is.equal(13, v[1][4]) 421 | 422 | assert.is.equal(2, v[2][1]) 423 | assert.is.equal(6, v[2][2]) 424 | assert.is.equal(10, v[2][3]) 425 | assert.is.equal(14, v[2][4]) 426 | 427 | assert.is.equal(3, v[3][1]) 428 | assert.is.equal(7, v[3][2]) 429 | assert.is.equal(11, v[3][3]) 430 | assert.is.equal(15, v[3][4]) 431 | 432 | assert.is.equal(4, v[4][1]) 433 | assert.is.equal(8, v[4][2]) 434 | assert.is.equal(12, v[4][3]) 435 | assert.is.equal(16, v[4][4]) 436 | end) 437 | 438 | it("converts a matrix to a quaternion", function() 439 | local q = mat4({ 440 | 0, 0, 1, 0, 441 | 1, 0, 0, 0, 442 | 0, 1, 0, 0, 443 | 0, 0, 0, 0 444 | }):to_quat() 445 | assert.is.equal(-0.5, q.x) 446 | assert.is.equal(-0.5, q.y) 447 | assert.is.equal(-0.5, q.z) 448 | assert.is.equal( 0.5, q.w) 449 | end) 450 | 451 | it("converts a matrix to a frustum", function() 452 | local a = mat4() 453 | local b = mat4.from_perspective(45, 1, 0.1, 1000) 454 | local f = (b * a):to_frustum() 455 | 456 | assert.is_true(utils.tolerance( 0.9239-f.left.a, 0.001)) 457 | assert.is_true(utils.tolerance( 0 -f.left.b, 0.001)) 458 | assert.is_true(utils.tolerance(-0.3827-f.left.c, 0.001)) 459 | assert.is_true(utils.tolerance( 0 -f.left.d, 0.001)) 460 | 461 | assert.is_true(utils.tolerance(-0.9239-f.right.a, 0.001)) 462 | assert.is_true(utils.tolerance( 0 -f.right.b, 0.001)) 463 | assert.is_true(utils.tolerance(-0.3827-f.right.c, 0.001)) 464 | assert.is_true(utils.tolerance( 0 -f.right.d, 0.001)) 465 | 466 | assert.is_true(utils.tolerance( 0 -f.bottom.a, 0.001)) 467 | assert.is_true(utils.tolerance( 0.9239-f.bottom.b, 0.001)) 468 | assert.is_true(utils.tolerance(-0.3827-f.bottom.c, 0.001)) 469 | assert.is_true(utils.tolerance( 0 -f.bottom.d, 0.001)) 470 | 471 | assert.is_true(utils.tolerance( 0 -f.top.a, 0.001)) 472 | assert.is_true(utils.tolerance(-0.9239-f.top.b, 0.001)) 473 | assert.is_true(utils.tolerance(-0.3827-f.top.c, 0.001)) 474 | assert.is_true(utils.tolerance( 0 -f.top.d, 0.001)) 475 | 476 | assert.is_true(utils.tolerance( 0 -f.near.a, 0.001)) 477 | assert.is_true(utils.tolerance( 0 -f.near.b, 0.001)) 478 | assert.is_true(utils.tolerance(-1 -f.near.c, 0.001)) 479 | assert.is_true(utils.tolerance(-0.1-f.near.d, 0.001)) 480 | 481 | assert.is_true(utils.tolerance( 0 -f.far.a, 0.001)) 482 | assert.is_true(utils.tolerance( 0 -f.far.b, 0.001)) 483 | assert.is_true(utils.tolerance( 1 -f.far.c, 0.001)) 484 | assert.is_true(utils.tolerance( 1000-f.far.d, 0.001)) 485 | end) 486 | 487 | it("checks to see if data is a valid matrix (not a table)", function() 488 | assert.is_not_true(mat4.is_mat4(0)) 489 | end) 490 | 491 | it("checks to see if data is a valid matrix (invalid data)", function() 492 | assert.is_not_true(mat4.is_mat4({})) 493 | end) 494 | 495 | it("gets a string representation of a matrix", function() 496 | local a = mat4():to_string() 497 | local z = "+0.000" 498 | local o = "+1.000" 499 | local s = string.format( 500 | "[ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s ]", 501 | o, z, z, z, z, o, z, z, z, z, o, z, z, z ,z, o 502 | ) 503 | assert.is.equal(s, a) 504 | end) 505 | 506 | it("creates a matrix out of transform values", function() 507 | local scale = vec3(1, 2, 3) 508 | local rot = quat.from_angle_axis(math.pi * 0.5, vec3(0, 1, 0)) 509 | local trans = vec3(3, 4, 5) 510 | local a = mat4.from_transform(trans, rot, scale) 511 | 512 | local v = vec3(-2, 3, 4) 513 | -- scaled, rotated, then translated 514 | -- v * mT * mR * mS 515 | 516 | local result = a * v 517 | local expected = vec3(-9, 10, 3) 518 | 519 | -- float margin is considered 520 | assert.is_true(math.abs(expected.x - result.x) < FLT_EPSILON) 521 | assert.is_true(math.abs(expected.y - result.y) < FLT_EPSILON) 522 | assert.is_true(math.abs(expected.z - result.z) < FLT_EPSILON) 523 | end) 524 | 525 | end) 526 | 527 | --[[ 528 | from_angle_axis 529 | from_quaternion 530 | from_direction 531 | from_transform 532 | from_ortho 533 | --]] 534 | -------------------------------------------------------------------------------- /spec/mesh_spec.lua: -------------------------------------------------------------------------------- 1 | local mesh = require "modules.mesh" 2 | 3 | describe("mesh:", function() 4 | end) 5 | 6 | --[[ 7 | average(vertices) 8 | normal(triangle) 9 | plane_from_triangle(triangle) 10 | is_front_facing(plane, direction) 11 | signed_distance(point, plane) 12 | --]] -------------------------------------------------------------------------------- /spec/octree_spec.lua: -------------------------------------------------------------------------------- 1 | local octree = require "modules.octree" 2 | 3 | describe("octree:", function() 4 | end) 5 | 6 | --[[ 7 | local function new(initialWorldSize, initialWorldPos, minNodeSize, looseness) 8 | function Octree:add(obj, objBounds) 9 | function Octree:remove(obj) 10 | function Octree:is_colliding(checkBounds) 11 | function Octree:get_colliding(checkBounds) 12 | function Octree:cast_ray(ray, func, out) 13 | function Octree:draw_bounds(cube) 14 | function Octree:draw_objects(cube, filter) 15 | function Octree:grow(direction) 16 | function Octree:shrink() 17 | function Octree:get_root_pos_index(xDir, yDir, zDir) 18 | 19 | function OctreeNode:add(obj, objBounds) 20 | function OctreeNode:remove(obj) 21 | function OctreeNode:is_colliding(checkBounds) 22 | function OctreeNode:get_colliding(checkBounds, results) 23 | function OctreeNode:cast_ray(ray, func, out, depth) 24 | function OctreeNode:set_children(childOctrees) 25 | function OctreeNode:shrink_if_possible(minLength) 26 | function OctreeNode:set_values(baseLength, minSize, looseness, center) 27 | function OctreeNode:split() 28 | function OctreeNode:merge() 29 | function OctreeNode:best_fit_child(objBounds) 30 | function OctreeNode:should_merge() 31 | function OctreeNode:has_any_objects() 32 | function OctreeNode:draw_bounds(cube, depth) 33 | function OctreeNode:draw_objects(cube, filter) 34 | --]] -------------------------------------------------------------------------------- /spec/quat_spec.lua: -------------------------------------------------------------------------------- 1 | local quat = require "modules.quat" 2 | local vec3 = require "modules.vec3" 3 | local utils = require "modules.utils" 4 | local constants = require "modules.constants" 5 | 6 | describe("quat:", function() 7 | it("creates an identity quaternion", function() 8 | local a = quat() 9 | assert.is.equal(0, a.x) 10 | assert.is.equal(0, a.y) 11 | assert.is.equal(0, a.z) 12 | assert.is.equal(1, a.w) 13 | assert.is_true(a:is_quat()) 14 | assert.is_true(a:is_real()) 15 | end) 16 | 17 | it("creates a quaternion from numbers", function() 18 | local a = quat(0, 0, 0, 0) 19 | assert.is.equal(0, a.x) 20 | assert.is.equal(0, a.y) 21 | assert.is.equal(0, a.z) 22 | assert.is.equal(0, a.w) 23 | assert.is_true(a:is_zero()) 24 | assert.is_true(a:is_imaginary()) 25 | end) 26 | 27 | it("creates a quaternion from a list", function() 28 | local a = quat { 2, 3, 4, 1 } 29 | assert.is.equal(2, a.x) 30 | assert.is.equal(3, a.y) 31 | assert.is.equal(4, a.z) 32 | assert.is.equal(1, a.w) 33 | end) 34 | 35 | it("creates a quaternion from a record", function() 36 | local a = quat { x=2, y=3, z=4, w=1 } 37 | assert.is.equal(2, a.x) 38 | assert.is.equal(3, a.y) 39 | assert.is.equal(4, a.z) 40 | assert.is.equal(1, a.w) 41 | end) 42 | 43 | it("creates a quaternion from a direction", function() 44 | local v = vec3(-80, 80, -80):normalize() 45 | local a = quat.from_direction(v, vec3.unit_z) 46 | assert.is_true(utils.tolerance(-0.577-a.x, 0.001)) 47 | assert.is_true(utils.tolerance(-0.577-a.y, 0.001)) 48 | assert.is_true(utils.tolerance( 0 -a.z, 0.001)) 49 | assert.is_true(utils.tolerance( 0.423-a.w, 0.001)) 50 | end) 51 | 52 | it("clones a quaternion", function() 53 | local a = quat() 54 | local b = a:clone() 55 | assert.is.equal(a.x, b.x) 56 | assert.is.equal(a.y, b.y) 57 | assert.is.equal(a.z, b.z) 58 | assert.is.equal(a.w, b.w) 59 | end) 60 | 61 | it("adds a quaternion to another", function() 62 | local a = quat(2, 3, 4, 1) 63 | local b = quat(3, 6, 9, 1) 64 | local c = a:add(b) 65 | local d = a + b 66 | assert.is.equal(5, c.x) 67 | assert.is.equal(9, c.y) 68 | assert.is.equal(13, c.z) 69 | assert.is.equal(2, c.w) 70 | assert.is.equal(c, d) 71 | end) 72 | 73 | it("subtracts a quaternion from another", function() 74 | local a = quat(2, 3, 4, 1) 75 | local b = quat(3, 6, 9, 1) 76 | local c = a:sub(b) 77 | local d = a - b 78 | assert.is.equal(-1, c.x) 79 | assert.is.equal(-3, c.y) 80 | assert.is.equal(-5, c.z) 81 | assert.is.equal( 0, c.w) 82 | assert.is.equal(c, d) 83 | end) 84 | 85 | it("multiplies a quaternion by another", function() 86 | local a = quat(2, 3, 4, 1) 87 | local b = quat(3, 6, 9, 1) 88 | local c = a:mul(b) 89 | local d = a * b 90 | assert.is.equal( 8, c.x) 91 | assert.is.equal( 3, c.y) 92 | assert.is.equal( 16, c.z) 93 | assert.is.equal(-59, c.w) 94 | assert.is.equal(c, d) 95 | end) 96 | 97 | it("multiplies a quaternion by a scale factor", function() 98 | local a = quat(2, 3, 4, 1) 99 | local s = 3 100 | local b = a:scale(s) 101 | local c = a * s 102 | assert.is.equal(6, b.x) 103 | assert.is.equal(9, b.y) 104 | assert.is.equal(12, b.z) 105 | assert.is.equal(3, b.w) 106 | assert.is.equal(b, c) 107 | end) 108 | 109 | it("inverts a quaternion", function() 110 | local a = quat(2, 3, 4, 1) 111 | local b = -a 112 | assert.is.equal(-a.x, b.x) 113 | assert.is.equal(-a.y, b.y) 114 | assert.is.equal(-a.z, b.z) 115 | assert.is.equal(-a.w, b.w) 116 | end) 117 | 118 | it("multiplies a quaternion by a vec3", function() 119 | local a = quat(2, 3, 4, 1) 120 | local v = vec3(3, 4, 5) 121 | local b = a:mul_vec3(v) 122 | local c = a * v 123 | assert.is.equal(-21, c.x) 124 | assert.is.equal( 4, c.y) 125 | assert.is.equal( 17, c.z) 126 | assert.is.equal(b, c) 127 | end) 128 | 129 | it("verifies quat composition order", function() 130 | local a = quat(2, 3, 4, 1):normalize() -- Only the normal quaternions represent rotations 131 | local b = quat(3, 6, 9, 1):normalize() 132 | local c = a * b 133 | 134 | local v = vec3(3, 4, 5) 135 | 136 | local cv = c * v 137 | local abv = a * (b * v) 138 | 139 | assert.is_true((abv - cv):len() < 1e-07) -- Verify (a*b)*v == a*(b*v) within an epsilon 140 | end) 141 | 142 | it("multiplies a quaternion by an exponent of 0", function() 143 | local a = quat(2, 3, 4, 1):normalize() 144 | local e = 0 145 | local b = a:pow(e) 146 | local c = a^e 147 | 148 | assert.is.equal(0, b.x) 149 | assert.is.equal(0, b.y) 150 | assert.is.equal(0, b.z) 151 | assert.is.equal(1, b.w) 152 | assert.is.equal(b, c) 153 | end) 154 | 155 | it("multiplies a quaternion by a positive exponent", function() 156 | local a = quat(2, 3, 4, 1):normalize() 157 | local e = 0.75 158 | local b = a:pow(e) 159 | local c = a^e 160 | 161 | assert.is_true(utils.tolerance(-0.3204+b.x, 0.0001)) 162 | assert.is_true(utils.tolerance(-0.4805+b.y, 0.0001)) 163 | assert.is_true(utils.tolerance(-0.6407+b.z, 0.0001)) 164 | assert.is_true(utils.tolerance(-0.5059+b.w, 0.0001)) 165 | assert.is.equal( b, c) 166 | end) 167 | 168 | it("multiplies a quaternion by a negative exponent", function() 169 | local a = quat(2, 3, 4, 1):normalize() 170 | local e = -1 171 | local b = a:pow(e) 172 | local c = a^e 173 | 174 | assert.is_true(utils.tolerance( 0.3651+b.x, 0.0001)) 175 | assert.is_true(utils.tolerance( 0.5477+b.y, 0.0001)) 176 | assert.is_true(utils.tolerance( 0.7303+b.z, 0.0001)) 177 | assert.is_true(utils.tolerance(-0.1826+b.w, 0.0001)) 178 | assert.is.equal(b, c) 179 | end) 180 | 181 | it("inverts a quaternion", function() 182 | local a = quat(1, 1, 1, 1):inverse() 183 | assert.is.equal(-0.5, a.x) 184 | assert.is.equal(-0.5, a.y) 185 | assert.is.equal(-0.5, a.z) 186 | assert.is.equal( 0.5, a.w) 187 | end) 188 | 189 | it("normalizes a quaternion", function() 190 | local a = quat(1, 1, 1, 1):normalize() 191 | assert.is.equal(0.5, a.x) 192 | assert.is.equal(0.5, a.y) 193 | assert.is.equal(0.5, a.z) 194 | assert.is.equal(0.5, a.w) 195 | end) 196 | 197 | it("dots two quaternions", function() 198 | local a = quat(1, 1, 1, 1) 199 | local b = quat(4, 4, 4, 4) 200 | local c = a:dot(b) 201 | assert.is.equal(16, c) 202 | end) 203 | 204 | it("dots two quaternions (negative)", function() 205 | local a = quat(-1, 1, 1, 1) 206 | local b = quat(4, 4, 4, 4) 207 | local c = a:dot(b) 208 | assert.is.equal(8, c) 209 | end) 210 | 211 | it("dots two quaternions (tiny)", function() 212 | local a = quat(0.1, 0.1, 0.1, 0.1) 213 | local b = quat(0.4, 0.4, 0.4, 0.4) 214 | local c = a:dot(b) 215 | assert.is_true(utils.tolerance(0.16-c, 0.001)) 216 | end) 217 | 218 | it("gets the length of a quaternion", function() 219 | local a = quat(2, 3, 4, 5):len() 220 | assert.is.equal(math.sqrt(54), a) 221 | end) 222 | 223 | it("gets the square length of a quaternion", function() 224 | local a = quat(2, 3, 4, 5):len2() 225 | assert.is.equal(54, a) 226 | end) 227 | 228 | it("interpolates between two quaternions", function() 229 | local a = quat(3, 3, 3, 3) 230 | local b = quat(6, 6, 6, 6) 231 | local s = 0.1 232 | local c = a:lerp(b, s) 233 | assert.is.equal(0.5, c.x) 234 | assert.is.equal(0.5, c.y) 235 | assert.is.equal(0.5, c.z) 236 | assert.is.equal(0.5, c.w) 237 | end) 238 | 239 | it("interpolates between two quaternions (spherical)", function() 240 | local a = quat(3, 3, 3, 3) 241 | local b = quat(6, 6, 6, 6) 242 | local s = 0.1 243 | local c = a:slerp(b, s) 244 | assert.is.equal(0.5, c.x) 245 | assert.is.equal(0.5, c.y) 246 | assert.is.equal(0.5, c.z) 247 | assert.is.equal(0.5, c.w) 248 | end) 249 | 250 | it("unpacks a quaternion", function() 251 | local x, y, z, w = quat(2, 3, 4, 1):unpack() 252 | assert.is.equal(2, x) 253 | assert.is.equal(3, y) 254 | assert.is.equal(4, z) 255 | assert.is.equal(1, w) 256 | end) 257 | 258 | it("converts quaternion to a vec3", function() 259 | local v = quat(2, 3, 4, 1):to_vec3() 260 | assert.is.equal(2, v.x) 261 | assert.is.equal(3, v.y) 262 | assert.is.equal(4, v.z) 263 | end) 264 | 265 | it("gets the conjugate quaternion", function() 266 | local a = quat(2, 3, 4, 1):conjugate() 267 | assert.is.equal(-2, a.x) 268 | assert.is.equal(-3, a.y) 269 | assert.is.equal(-4, a.z) 270 | assert.is.equal( 1, a.w) 271 | end) 272 | 273 | it("gets the reciprocal quaternion", function() 274 | local a = quat(1, 1, 1, 1) 275 | local b = a:reciprocal() 276 | local c = b:reciprocal() 277 | 278 | assert.is_not.equal(a.x, b.x) 279 | assert.is_not.equal(a.y, b.y) 280 | assert.is_not.equal(a.z, b.z) 281 | assert.is_not.equal(a.w, b.w) 282 | 283 | assert.is.equal(a.x, c.x) 284 | assert.is.equal(a.y, c.y) 285 | assert.is.equal(a.z, c.z) 286 | assert.is.equal(a.w, c.w) 287 | end) 288 | 289 | it("converts between a quaternion and angle/axis", function() 290 | local a = quat.from_angle_axis(math.pi, vec3.unit_z) 291 | local angle, axis = a:to_angle_axis() 292 | assert.is.equal(math.pi, angle) 293 | assert.is.equal(vec3.unit_z, axis) 294 | end) 295 | 296 | it("converts between a quaternion and angle/axis (specify by component)", function() 297 | local a = quat.from_angle_axis(math.pi, vec3.unit_z.x, vec3.unit_z.y, vec3.unit_z.z) 298 | local angle, axis = a:to_angle_axis() 299 | assert.is.equal(math.pi, angle) 300 | assert.is.equal(vec3.unit_z, axis) 301 | end) 302 | 303 | it("converts between a quaternion and angle/axis (w=2)", function() 304 | local angle, axis = quat(1, 1, 1, 2):to_angle_axis() 305 | assert.is_true(utils.tolerance(1.427-angle, 0.001)) 306 | assert.is_true(utils.tolerance(0.577-axis.x, 0.001)) 307 | assert.is_true(utils.tolerance(0.577-axis.y, 0.001)) 308 | assert.is_true(utils.tolerance(0.577-axis.z, 0.001)) 309 | end) 310 | 311 | it("converts between a quaternion and angle/axis (w=2) (by component)", function() 312 | local angle, x,y,z = quat(1, 1, 1, 2):to_angle_axis_unpack() 313 | assert.is_true(utils.tolerance(1.427-angle, 0.001)) 314 | assert.is_true(utils.tolerance(0.577-x, 0.001)) 315 | assert.is_true(utils.tolerance(0.577-y, 0.001)) 316 | assert.is_true(utils.tolerance(0.577-z, 0.001)) 317 | end) 318 | 319 | it("converts between a quaternion and angle/axis (w=1)", function() 320 | local angle, axis = quat(1, 2, 3, 1):to_angle_axis() 321 | assert.is.equal(0, angle) 322 | assert.is.equal(1, axis.x) 323 | assert.is.equal(2, axis.y) 324 | assert.is.equal(3, axis.z) 325 | end) 326 | 327 | it("converts between a quaternion and angle/axis (identity quaternion) (by component)", function() 328 | local angle, x,y,z = quat():to_angle_axis_unpack() 329 | assert.is.equal(0, angle) 330 | assert.is.equal(0, x) 331 | assert.is.equal(0, y) 332 | assert.is.equal(1, z) 333 | end) 334 | 335 | it("converts between a quaternion and angle/axis (identity quaternion with fallback)", function() 336 | local angle, axis = quat():to_angle_axis(vec3(2,3,4)) 337 | assert.is.equal(0, angle) 338 | assert.is.equal(2, axis.x) 339 | assert.is.equal(3, axis.y) 340 | assert.is.equal(4, axis.z) 341 | end) 342 | 343 | it("gets a string representation of a quaternion", function() 344 | local a = quat():to_string() 345 | assert.is.equal("(+0.000,+0.000,+0.000,+1.000)", a) 346 | end) 347 | end) 348 | -------------------------------------------------------------------------------- /spec/utils_spec.lua: -------------------------------------------------------------------------------- 1 | local vec3 = require "modules.vec3" 2 | local utils = require "modules.utils" 3 | local constants = require "modules.constants" 4 | 5 | local function tolerance(v, t) 6 | return math.abs(v - t) < 1e-6 7 | end 8 | 9 | describe("utils:", function() 10 | it("interpolates between two numbers", function() 11 | assert.is_true(tolerance(utils.lerp(0, 1, 0.5), 0.5)) 12 | end) 13 | 14 | it("interpolates between two vectors", function() 15 | local a = vec3(0, 0, 0) 16 | local b = vec3(1, 1, 1) 17 | local c = vec3(0.5, 0.5, 0.5) 18 | assert.is.equal(utils.lerp(a, b, 0.5), c) 19 | 20 | a = vec3(5, 5, 5) 21 | b = vec3(0, 0, 0) 22 | c = vec3(2.5, 2.5, 2.5) 23 | assert.is.equal(utils.lerp(a, b, 0.5), c) 24 | end) 25 | 26 | it("decays exponentially", function() 27 | local v = utils.decay(0, 1, 0.5, 1) 28 | assert.is_true(tolerance(v, 0.39346934028737)) 29 | end) 30 | 31 | it("checks a nan", function() 32 | local a = 0/0 33 | assert.is_true(utils.is_nan(a)) 34 | end) 35 | 36 | it("rounds a number", function() 37 | -- round up 38 | local v = utils.round(1.3252525, 0.01) 39 | assert.is_true(tolerance(v, 1.33)) 40 | -- round down 41 | v = utils.round(1.3242525, 0.1) 42 | assert.is_true(tolerance(v, 1.3)) 43 | -- no precision 44 | v = utils.round(1.3242525) 45 | assert.is_true(tolerance(v, 1)) 46 | end) 47 | 48 | it("checks sign", function() 49 | assert.is.equal(utils.sign(-9), -1) 50 | assert.is.equal(utils.sign(0), 0) 51 | assert.is.equal(utils.sign(12), 1) 52 | end) 53 | end) 54 | 55 | --[[ 56 | clamp(value, min, max) 57 | deadzone(value, size) 58 | threshold(value, threshold) 59 | tolerance(value, threshold) 60 | map(value, min_in, max_in, min_out, max_out) 61 | lerp(progress, low, high) 62 | smoothstep(progress, low, high) 63 | wrap(value, limit) 64 | is_pot(value) 65 | project_on(out, a, b) 66 | project_from(out, a, b) 67 | mirror_on(out, a, b) 68 | reflect(out, i, n) 69 | refract(out, i, n, ior) 70 | --]] 71 | -------------------------------------------------------------------------------- /spec/vec2_spec.lua: -------------------------------------------------------------------------------- 1 | local vec2 = require "modules.vec2" 2 | local DBL_EPSILON = require("modules.constants").DBL_EPSILON 3 | local abs, sqrt = math.abs, math.sqrt 4 | 5 | describe("vec2:", function() 6 | it("creates an empty vector", function() 7 | local a = vec2() 8 | assert.is.equal(0, a.x) 9 | assert.is.equal(0, a.y) 10 | assert.is_true(a:is_vec2()) 11 | assert.is_true(a:is_zero()) 12 | end) 13 | 14 | it("creates a vector from a number", function() 15 | local a = vec2(3) 16 | assert.is.equal(3, a.x) 17 | assert.is.equal(3, a.y) 18 | end) 19 | 20 | it("creates a vector from numbers", function() 21 | local a = vec2(3, 5) 22 | assert.is.equal(3, a.x) 23 | assert.is.equal(5, a.y) 24 | end) 25 | 26 | it("creates a vector from a list", function() 27 | local a = vec2 { 3, 5 } 28 | assert.is.equal(3, a.x) 29 | assert.is.equal(5, a.y) 30 | end) 31 | 32 | it("creates a vector from a record", function() 33 | local a = vec2 { x=3, y=5 } 34 | assert.is.equal(3, a.x) 35 | assert.is.equal(5, a.y) 36 | end) 37 | 38 | it("creates a vector from nan", function() 39 | local a = vec2(0/0) 40 | assert.is_true(a:has_nan()) 41 | end) 42 | 43 | it("clones a vector", function() 44 | local a = vec2(3, 5) 45 | local b = a:clone() 46 | assert.is.equal(a, b) 47 | end) 48 | 49 | it("clones a vector using the constructor", function() 50 | local a = vec2(3, 5) 51 | local b = vec2(a) 52 | assert.is.equal(a, b) 53 | end) 54 | 55 | it("adds a vector to another", function() 56 | local a = vec2(3, 5) 57 | local b = vec2(7, 4) 58 | local c = a:add(b) 59 | local d = a + b 60 | assert.is.equal(10, c.x) 61 | assert.is.equal(9, c.y) 62 | assert.is.equal(c, d) 63 | end) 64 | 65 | it("subracts a vector from another", function() 66 | local a = vec2(3, 5) 67 | local b = vec2(7, 4) 68 | local c = a:sub(b) 69 | local d = a - b 70 | assert.is.equal(-4, c.x) 71 | assert.is.equal( 1, c.y) 72 | assert.is.equal( c, d) 73 | end) 74 | 75 | it("multiplies a vector by a scale factor", function() 76 | local a = vec2(3, 5) 77 | local s = 2 78 | local c = a:scale(s) 79 | local d = a * s 80 | assert.is.equal(6, c.x) 81 | assert.is.equal(10, c.y) 82 | assert.is.equal(c, d) 83 | end) 84 | 85 | it("divides a vector by another vector", function() 86 | local a = vec2(3, 5) 87 | local s = vec2(2, 2) 88 | local c = a:div(s) 89 | local d = a / s 90 | assert.is.equal(1.5, c.x) 91 | assert.is.equal(2.5, c.y) 92 | assert.is.equal(c, d) 93 | end) 94 | 95 | it("inverts a vector", function() 96 | local a = vec2(3, -5) 97 | local b = -a 98 | assert.is.equal(-a.x, b.x) 99 | assert.is.equal(-a.y, b.y) 100 | end) 101 | 102 | it("gets the length of a vector", function() 103 | local a = vec2(3, 5) 104 | assert.is.equal(sqrt(34), a:len()) 105 | end) 106 | 107 | it("gets the square length of a vector", function() 108 | local a = vec2(3, 5) 109 | assert.is.equal(34, a:len2()) 110 | end) 111 | 112 | it("normalizes a vector", function() 113 | local a = vec2(3, 5) 114 | local b = a:normalize() 115 | assert.is_true(abs(b:len()-1) < DBL_EPSILON) 116 | end) 117 | 118 | it("trims the length of a vector", function() 119 | local a = vec2(3, 5) 120 | local b = a:trim(0.5) 121 | assert.is_true(abs(b:len()-0.5) < DBL_EPSILON) 122 | end) 123 | 124 | it("gets the distance between two vectors", function() 125 | local a = vec2(3, 5) 126 | local b = vec2(7, 4) 127 | local c = a:dist(b) 128 | assert.is.equal(sqrt(17), c) 129 | end) 130 | 131 | it("gets the square distance between two vectors", function() 132 | local a = vec2(3, 5) 133 | local b = vec2(7, 4) 134 | local c = a:dist2(b) 135 | assert.is.equal(17, c) 136 | end) 137 | 138 | it("crosses two vectors", function() 139 | local a = vec2(3, 5) 140 | local b = vec2(7, 4) 141 | local c = a:cross(b) 142 | assert.is.equal(-23, c) 143 | end) 144 | 145 | it("dots two vectors", function() 146 | local a = vec2(3, 5) 147 | local b = vec2(7, 4) 148 | local c = a:dot(b) 149 | assert.is.equal(41, c) 150 | end) 151 | 152 | it("interpolates between two vectors", function() 153 | local a = vec2(3, 5) 154 | local b = vec2(7, 4) 155 | local s = 0.1 156 | local c = a:lerp(b, s) 157 | assert.is.equal(3.4, c.x) 158 | assert.is.equal(4.9, c.y) 159 | end) 160 | 161 | it("unpacks a vector", function() 162 | local a = vec2(3, 5) 163 | local x, y = a:unpack() 164 | assert.is.equal(3, x) 165 | assert.is.equal(5, y) 166 | end) 167 | 168 | it("rotates a vector", function() 169 | local a = vec2(3, 5) 170 | local b = a:rotate( math.pi) 171 | local c = b:rotate(-math.pi) 172 | assert.is_not.equal(a, b) 173 | assert.is.equal(a, c) 174 | end) 175 | 176 | it("converts between polar and cartesian coordinates", function() 177 | local a = vec2(3, 5) 178 | local r, t = a:to_polar() 179 | local b = vec2.from_cartesian(r, t) 180 | assert.is_true(abs(a.x - b.x) <= DBL_EPSILON*2) -- Allow 2X epsilon error because there were 2 operations. 181 | assert.is_true(abs(a.y - b.y) <= DBL_EPSILON*2) 182 | end) 183 | 184 | it("gets a perpendicular vector", function() 185 | local a = vec2(3, 5) 186 | local b = a:perpendicular() 187 | assert.is.equal(-5, b.x) 188 | assert.is.equal( 3, b.y) 189 | end) 190 | 191 | it("gets a string representation of a vector", function() 192 | local a = vec2() 193 | local b = a:to_string() 194 | assert.is.equal("(+0.000,+0.000)", b) 195 | end) 196 | 197 | it("rounds a 2-vector", function() 198 | local a = vec2(1.1,1.9):round() 199 | assert.is.equal(a.x, 1) 200 | assert.is.equal(a.y, 2) 201 | end) 202 | 203 | it("flips a 2-vector", function() 204 | local a = vec2(1,2) 205 | local temp = a:flip_x() 206 | assert.is.equal(temp, vec2(-1, 2)) 207 | temp = temp:flip_y() 208 | assert.is.equal(temp, vec2(-1, -2)) 209 | end) 210 | 211 | it("finds angle from one 2-vector to another", function() 212 | local d = { 213 | right = vec2(1, 0), 214 | down = vec2(0, -1), 215 | left = vec2(-1, 0), 216 | up = vec2(0, 1), 217 | } 218 | assert.is.equal(math.deg(d.right:angle_to(d.right)), 0.0) 219 | assert.is.equal(math.deg(d.right:angle_to(d.down)), -90.0) 220 | assert.is.equal(math.deg(d.right:angle_to(d.left)), 180.0) 221 | assert.is.equal(math.deg(d.right:angle_to(d.up)), 90.0) 222 | 223 | assert.is.equal(math.deg(d.down:angle_to(d.right)), 90.0) 224 | assert.is.equal(math.deg(d.down:angle_to(d.down)), 0.0) 225 | assert.is.equal(math.deg(d.down:angle_to(d.left)), -90.0) 226 | assert.is.equal(math.deg(d.down:angle_to(d.up)), 180.0) 227 | 228 | assert.is.equal(math.deg(d.left:angle_to(d.right)), 180.0) 229 | assert.is.equal(math.deg(d.left:angle_to(d.down)), 90.0) 230 | assert.is.equal(math.deg(d.left:angle_to(d.left)), 0.0) 231 | assert.is.equal(math.deg(d.left:angle_to(d.up)), -90.0) 232 | 233 | assert.is.equal(math.deg(d.up:angle_to(d.right)), -90.0) 234 | assert.is.equal(math.deg(d.up:angle_to(d.down)), 180.0) 235 | assert.is.equal(math.deg(d.up:angle_to(d.left)), 90.0) 236 | assert.is.equal(math.deg(d.up:angle_to(d.up)), 0.0) 237 | end) 238 | 239 | it("finds angle between two 2-vectors", function() 240 | local d = { 241 | right = vec2(1, 0), 242 | down = vec2(0, -1), 243 | left = vec2(-1, 0), 244 | up = vec2(0, 1), 245 | } 246 | assert.is.equal(math.deg(d.right:angle_between(d.right)), 0.0) 247 | assert.is.equal(math.deg(d.right:angle_between(d.down)), 90.0) 248 | assert.is.equal(math.deg(d.right:angle_between(d.left)), 180.0) 249 | assert.is.equal(math.deg(d.right:angle_between(d.up)), 90.0) 250 | 251 | assert.is.equal(math.deg(d.down:angle_between(d.right)), 90.0) 252 | assert.is.equal(math.deg(d.down:angle_between(d.down)), 0.0) 253 | assert.is.equal(math.deg(d.down:angle_between(d.left)), 90.0) 254 | assert.is.equal(math.deg(d.down:angle_between(d.up)), 180.0) 255 | 256 | assert.is.equal(math.deg(d.left:angle_between(d.right)), 180.0) 257 | assert.is.equal(math.deg(d.left:angle_between(d.down)), 90.0) 258 | assert.is.equal(math.deg(d.left:angle_between(d.left)), 0.0) 259 | assert.is.equal(math.deg(d.left:angle_between(d.up)), 90.0) 260 | 261 | assert.is.equal(math.deg(d.up:angle_between(d.right)), 90.0) 262 | assert.is.equal(math.deg(d.up:angle_between(d.down)), 180.0) 263 | assert.is.equal(math.deg(d.up:angle_between(d.left)), 90.0) 264 | assert.is.equal(math.deg(d.up:angle_between(d.up)), 0.0) 265 | end) 266 | 267 | -- Do this last, to insulate tests from accidental state contamination 268 | -- Do vec3 tests last, to insulate tests from accidental state contamination 269 | it("converts a 2-vector to a 3-vector", function() 270 | local vec3 = require "modules.vec3" 271 | local a = vec2(1,2) 272 | local b = a:to_vec3() 273 | local c = a:to_vec3(3) 274 | assert.is.equal(b, vec3(1,2,0)) 275 | assert.is.equal(c, vec3(1,2,3)) 276 | end) 277 | 278 | it("converts a vec3 to vec2 using the constructor", function() 279 | local vec3 = require "modules.vec3" 280 | local a = vec2(3, 5) 281 | local b = vec3(3, 5, 7) 282 | local c = vec2(b) 283 | assert.is.equal(a, c) 284 | end) 285 | end) 286 | -------------------------------------------------------------------------------- /spec/vec3_spec.lua: -------------------------------------------------------------------------------- 1 | local vec3 = require "modules.vec3" 2 | local DBL_EPSILON = require("modules.constants").DBL_EPSILON 3 | local abs, sqrt = math.abs, math.sqrt 4 | 5 | describe("vec3:", function() 6 | it("creates an empty vector", function() 7 | local a = vec3() 8 | assert.is.equal(0, a.x) 9 | assert.is.equal(0, a.y) 10 | assert.is.equal(0, a.z) 11 | assert.is_true(a:is_vec3()) 12 | assert.is_true(a:is_zero()) 13 | end) 14 | 15 | it("creates a vector from a number", function() 16 | local a = vec3(3) 17 | assert.is.equal(3, a.x) 18 | assert.is.equal(3, a.y) 19 | assert.is.equal(3, a.z) 20 | end) 21 | 22 | it("creates a vector from numbers", function() 23 | local a = vec3(3, 5, 7) 24 | assert.is.equal(3, a.x) 25 | assert.is.equal(5, a.y) 26 | assert.is.equal(7, a.z) 27 | end) 28 | 29 | it("creates a vector from a list", function() 30 | local a = vec3 { 3, 5, 7 } 31 | assert.is.equal(3, a.x) 32 | assert.is.equal(5, a.y) 33 | assert.is.equal(7, a.z) 34 | end) 35 | 36 | it("creates a vector from a record", function() 37 | local a = vec3 { x=3, y=5, z=7 } 38 | assert.is.equal(3, a.x) 39 | assert.is.equal(5, a.y) 40 | assert.is.equal(7, a.z) 41 | end) 42 | 43 | it("creates a vector from nan", function() 44 | local a = vec3(0/0) 45 | assert.is_true(a:has_nan()) 46 | end) 47 | 48 | it("clones a vector", function() 49 | local a = vec3(3, 5, 7) 50 | local b = a:clone() 51 | assert.is.equal(a, b) 52 | end) 53 | 54 | it("clones a vector using the constructor", function() 55 | local a = vec3(3, 5, 7) 56 | local b = vec3(a) 57 | assert.is.equal(a, b) 58 | end) 59 | 60 | it("adds a vector to another", function() 61 | local a = vec3(3, 5, 7) 62 | local b = vec3(7, 4, 1) 63 | local c = a:add(b) 64 | local d = a + b 65 | assert.is.equal(10, c.x) 66 | assert.is.equal(9, c.y) 67 | assert.is.equal(8, c.z) 68 | assert.is.equal(c, d) 69 | end) 70 | 71 | it("subracts a vector from another", function() 72 | local a = vec3(3, 5, 7) 73 | local b = vec3(7, 4, 1) 74 | local c = a:sub(b) 75 | local d = a - b 76 | assert.is.equal(-4, c.x) 77 | assert.is.equal( 1, c.y) 78 | assert.is.equal( 6, c.z) 79 | assert.is.equal( c, d) 80 | end) 81 | 82 | it("multiplies a vector by a scale factor", function() 83 | local a = vec3(3, 5, 7) 84 | local s = 2 85 | local c = a:scale(s) 86 | local d = a * s 87 | assert.is.equal(6, c.x) 88 | assert.is.equal(10, c.y) 89 | assert.is.equal(14, c.z) 90 | assert.is.equal(c, d) 91 | end) 92 | 93 | it("divides a vector by another vector", function() 94 | local a = vec3(3, 5, 7) 95 | local s = vec3(2, 2, 2) 96 | local c = a:div(s) 97 | local d = a / s 98 | assert.is.equal(1.5, c.x) 99 | assert.is.equal(2.5, c.y) 100 | assert.is.equal(3.5, c.z) 101 | assert.is.equal(c, d) 102 | end) 103 | 104 | it("inverts a vector", function() 105 | local a = vec3(3, -5, 7) 106 | local b = -a 107 | assert.is.equal(-a.x, b.x) 108 | assert.is.equal(-a.y, b.y) 109 | assert.is.equal(-a.z, b.z) 110 | end) 111 | 112 | it("gets the length of a vector", function() 113 | local a = vec3(3, 5, 7) 114 | assert.is.equal(sqrt(83), a:len()) 115 | end) 116 | 117 | it("gets the square length of a vector", function() 118 | local a = vec3(3, 5, 7) 119 | assert.is.equal(83, a:len2()) 120 | end) 121 | 122 | it("normalizes a vector", function() 123 | local a = vec3(3, 5, 7) 124 | local b = a:normalize() 125 | assert.is_true(abs(b:len()-1) < DBL_EPSILON) 126 | end) 127 | 128 | it("normalizes a vector and gets the length", function() 129 | local a = vec3(3, 5, 7) 130 | local b, l = a:normalize_len() 131 | assert.is_true(abs(b:len()-1) < DBL_EPSILON) 132 | assert.is.equal(sqrt(83), l) 133 | end) 134 | 135 | it("trims the length of a vector", function() 136 | local a = vec3(3, 5, 7) 137 | local b = a:trim(0.5) 138 | assert.is_true(abs(b:len()-0.5) < DBL_EPSILON) 139 | end) 140 | 141 | it("gets the distance between two vectors", function() 142 | local a = vec3(3, 5, 7) 143 | local b = vec3(7, 4, 1) 144 | local c = a:dist(b) 145 | assert.is.equal(sqrt(53), c) 146 | end) 147 | 148 | it("gets the square distance between two vectors", function() 149 | local a = vec3(3, 5, 7) 150 | local b = vec3(7, 4, 1) 151 | local c = a:dist2(b) 152 | assert.is.equal(53, c) 153 | end) 154 | 155 | it("crosses two vectors", function() 156 | local a = vec3(3, 5, 7) 157 | local b = vec3(7, 4, 1) 158 | local c = a:cross(b) 159 | assert.is.equal(-23, c.x) 160 | assert.is.equal( 46, c.y) 161 | assert.is.equal(-23, c.z) 162 | end) 163 | 164 | it("dots two vectors", function() 165 | local a = vec3(3, 5, 7) 166 | local b = vec3(7, 4, 1) 167 | local c = a:dot(b) 168 | assert.is.equal(48, c) 169 | end) 170 | 171 | it("interpolates between two vectors", function() 172 | local a = vec3(3, 5, 7) 173 | local b = vec3(7, 4, 1) 174 | local s = 0.1 175 | local c = a:lerp(b, s) 176 | assert.is.equal(3.4, c.x) 177 | assert.is.equal(4.9, c.y) 178 | assert.is.equal(6.4, c.z) 179 | end) 180 | 181 | it("unpacks a vector", function() 182 | local a = vec3(3, 5, 7) 183 | local x, y, z = a:unpack() 184 | assert.is.equal(3, x) 185 | assert.is.equal(5, y) 186 | assert.is.equal(7, z) 187 | end) 188 | 189 | it("rotates a vector", function() 190 | local a = vec3(3, 5, 7) 191 | local b = a:rotate( math.pi, vec3.unit_z) 192 | local c = b:rotate(-math.pi, vec3.unit_z) 193 | assert.is_not.equal(a, b) 194 | assert.is.equal(7, b.z) 195 | assert.is.equal(a, c) 196 | end) 197 | 198 | it("cannot rotate a vector without a valis axis", function() 199 | local a = vec3(3, 5, 7) 200 | local b = a:rotate(math.pi, 0) 201 | assert.is_equal(a, b) 202 | end) 203 | 204 | it("gets a perpendicular vector", function() 205 | local a = vec3(3, 5, 7) 206 | local b = a:perpendicular() 207 | assert.is.equal(-5, b.x) 208 | assert.is.equal( 3, b.y) 209 | assert.is.equal( 0, b.z) 210 | end) 211 | 212 | it("gets a string representation of a vector", function() 213 | local a = vec3() 214 | local b = a:to_string() 215 | assert.is.equal("(+0.000,+0.000,+0.000)", b) 216 | end) 217 | 218 | it("rounds a 3-vector", function() 219 | local a = vec3(1.1,1.9,3):round() 220 | assert.is.equal(a.x, 1) 221 | assert.is.equal(a.y, 2) 222 | assert.is.equal(a.z, 3) 223 | end) 224 | 225 | it("flips a 3-vector", function() 226 | local a = vec3(1,2,3) 227 | local temp = a:flip_x() 228 | assert.is.equal(temp, vec3(-1, 2, 3)) 229 | temp = temp:flip_y() 230 | assert.is.equal(temp, vec3(-1, -2, 3)) 231 | temp = temp:flip_z() 232 | assert.is.equal(temp, vec3(-1, -2, -3)) 233 | end) 234 | 235 | it("get two 3-vectors angle", function() 236 | local angle_to = function(a, b) 237 | local deg = math.deg(a:angle_to(b)) 238 | return string.format('%.2f', deg) 239 | end 240 | 241 | local a = vec3(1,2,3) 242 | assert.is.equal(angle_to(a, vec3(3, 2, 1)), '44.42') 243 | assert.is.equal(angle_to(a, vec3(0, 10, 0)), '57.69') 244 | assert.is.equal(angle_to(a, vec3(0, -12, -10)), '157.51') 245 | 246 | a = vec3.unit_z 247 | assert.is.equal(angle_to(a, vec3(0, 10, 0)), '90.00') 248 | assert.is.equal(angle_to(a, vec3(-123, 10, 0)), '90.00') 249 | assert.is.equal(angle_to(a, vec3(-10, 0, 10)), '45.00') 250 | assert.is.equal(angle_to(a, vec3(-10, 0, -10)), '135.00') 251 | assert.is.equal(angle_to(a, vec3(0, -10, -10)), '135.00') 252 | assert.is.equal(angle_to(a, vec3(0, 0, -10)), '180.00') 253 | assert.is.equal(angle_to(a, vec3(0, 0, 100)), '0.00') 254 | 255 | a = vec3(100, 100, 0) 256 | assert.is.equal(angle_to(a, vec3(0, 0, 100)), '90.00') 257 | assert.is.equal(angle_to(a, vec3(0, 0, -100)), '90.00') 258 | assert.is.equal(angle_to(a, vec3(-10, -10, 0)), '180.00') 259 | assert.is.equal(angle_to(a, vec3.unit_z), '90.00') 260 | end) 261 | end) 262 | --------------------------------------------------------------------------------