Repository: excessive/cpml Branch: master Commit: 9edd67236be2 Files: 38 Total size: 232.8 KB Directory structure: gitextract__ayjrotp/ ├── .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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveralls.yml ================================================ repo_token: WcsY9jsU97Zt0ZIbGHJftGkC8DsD16FVl ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf insert_final_newline = true [*.lua] indent_style = tab ================================================ FILE: .github/workflows/build.yml ================================================ # Based on https://gist.github.com/domenic/ec8b0fc8ab45f39403dd name: Documentation on: pull_request: # Build on pull requests to ensure they don't break docs. branches: - master push: # We'll only push new docs when master is updated (see below). branches: - master jobs: build: name: Build Docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Lua uses: leafo/gh-actions-lua@v8 with: luaVersion: 5.4 - name: Setup Lua Rocks uses: leafo/gh-actions-luarocks@v4 - name: Setup and run ldoc run: bash ./doc/install_and_build_docs - name: Deploy if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./doc/out ================================================ FILE: .github/workflows/runtest.yml ================================================ name: Validate Code on: pull_request: branches: - master - refactor push: branches: - master - refactor jobs: test: name: Run Tests runs-on: ubuntu-latest strategy: matrix: luaVersion: ["5.1.5", "luajit-2.0.5", "luajit-2.1.0-beta3"] steps: - uses: actions/checkout@v2 - name: Setup Lua uses: leafo/gh-actions-lua@v8.0.0 with: luaVersion: ${{ matrix.luaVersion }} - name: Setup Lua Rocks uses: leafo/gh-actions-luarocks@v4 - name: Install dependencies run: | luarocks --local install busted luarocks --local install luacov luarocks --local install luacov-coveralls - name: Run busted run: ~/.luarocks/bin/busted --verbose --coverage spec - name: Upload coverage continue-on-error: true # don't know why coveralls isn't uploading. For now, let this fail. run: | # ignore dotfile directories created by lua setup ~/.luarocks/bin/luacov-coveralls --exclude '^%.%a+$' --repo-token WcsY9jsU97Zt0ZIbGHJftGkC8DsD16FVl # - name: Run luacheck # run: luacheck --std max+busted *.lua spec ================================================ FILE: .gitignore ================================================ # LDoc generated files. doc/out ================================================ FILE: LICENSE.md ================================================ # Licenses CPML is Copyright (c) 2016 Colby Klein . CPML is Copyright (c) 2016 Landon Manning . Code in vec3.lua is derived from hump.vector. (c) 2010-2013 Matthias Richter. MIT. Portions of mat4.lua are from LuaMatrix, (c) 2010 Michael Lutz. MIT. Code in simplex.lua is (c) 2011 Stefan Gustavson. MIT. Code in bound2.lua and bound3.lua are (c) 2018 Andi McClure. MIT. Code in quat.lua is from Andrew Stacey and covered under the CC0 license. Code in octree.lua is derived from UnityOctree. (c) 2014 Nition. BSD-2-Clause. # The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # The BSD License (BSD-2-Clause) Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ Cirno's Perfect Math Library ==== ![Build Status](https://github.com/excessive/cpml/actions/workflows/runtest.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/excessive/cpml/badge.svg?branch=master)](https://coveralls.io/github/excessive/cpml?branch=master) Various useful bits of game math. 3D line intersections, ray casting, 2d/3d vectors, 4x4 matrices, quaternions, etc. Intended to be used with LuaJIT and LÖVE (this is the backbone of LÖVE3D). 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 .` # Installation 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. # Versions 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". ================================================ FILE: cpml-scm-1.rockspec ================================================ package = "cpml" version = "scm-1" source = { url = "git://github.com/excessive/cpml.git" } description = { summary = "Cirno's Perfect Math Library", detailed = "Various useful bits of game math. 3D line intersections, ray casting, vectors, matrices, quaternions, etc.", homepage = "http://github.com/excessive/cpml.git", license = "MIT" } dependencies = { "lua ~> 5.1" } build = { type = "builtin", modules = { ["cpml"] = "init.lua", ["cpml.modules.color"] = "modules/color.lua", ["cpml.modules.constants"] = "modules/constants.lua", ["cpml.modules.intersect"] = "modules/intersect.lua", ["cpml.modules.mat4"] = "modules/mat4.lua", ["cpml.modules.mesh"] = "modules/mesh.lua", ["cpml.modules.octree"] = "modules/octree.lua", ["cpml.modules.quat"] = "modules/quat.lua", ["cpml.modules.simplex"] = "modules/simplex.lua", ["cpml.modules.utils"] = "modules/utils.lua", ["cpml.modules.vec2"] = "modules/vec2.lua", ["cpml.modules.vec3"] = "modules/vec3.lua", } } ================================================ FILE: doc/config.ld ================================================ project="CPML" title="CPML documentation" description="A math library with (hopefully) everything you need for 2D/3D games" format="markdown" backtick_references=false file = { "../init.lua", "../modules" } dir='./out' readme='../README.md' style='!new' ================================================ FILE: doc/install_and_build_docs ================================================ #! /bin/sh # on github, leafo/gh-actions-lua leafo/gh-actions-luarocks setup luarocks for us. #~ sudo apt-get install lua5.3 liblua5.3-dev luarocks # github ldoc is far ahead of the released version. echo ldoc version: git ls-remote https://github.com/lunarmodules/LDoc master luarocks --local install https://raw.githubusercontent.com/lunarmodules/LDoc/master/ldoc-scm-3.rockspec echo cd ./doc ~/.luarocks/bin/ldoc . ================================================ FILE: init.lua ================================================ --[[ ------------------------------------------------------------------------------- -- @author Colby Klein -- @author Landon Manning -- @copyright 2016 -- @license MIT/X11 ------------------------------------------------------------------------------- .'@@@@@@@@@@@@@@#: ,@@@@#; .'@@@@+ ,@@@' .@@@# +@@+ .... .@@@ ;@@; '@@@@@@@@@@@@. @@@ @@# @@@@@@@@++@@@@@@@; `@@; .@@` @@@@@# #@@@@@ @@@ `@@ @@@@@` Cirno's `@@@@# +@@ @@ `@@@@@ Perfect @@@@@ @@+ @@+ ;@@@@+ Math +@@@@+ @@ @@ `@@@@@ Library @@@@@@ #@' `@@ @@@@@@ @@@@@@@ `@@ :@@ #@@@@@@. .@@@@@@@@@ @@ .@@ #@@@@@@@@@@@@;;@@@@@ @@ @@ .;+@@#'. ;@@@@@ :@@ @@` +@@@@+ @@. ,@@ @@@@@ .@@ @@# ;;;;;. `@@@@@ @@ @@+ .@@@@@ @@@@@ @@` #@@ '@@@@@#` ;@@@@@@ ;@@ .@@' @@@@@@@@@@@@@@@ @@# +@@' '@@@@@@@; @@@ '@@@` '@@@ #@@@; .@@@@: :@@@@@@@++;;;+#@@@@@@+` .;'+++++;. --]] local modules = (...) and (...):gsub('%.init$', '') .. ".modules." or "" local cpml = { _LICENSE = "CPML is distributed under the terms of the MIT license. See LICENSE.md.", _URL = "https://github.com/excessive/cpml", _VERSION = "1.2.9", _DESCRIPTION = "Cirno's Perfect Math Library: Just about everything you need for 3D games. Hopefully." } local files = { "bvh", "color", "constants", "intersect", "mat4", "mesh", "octree", "quat", "simplex", "utils", "vec2", "vec3", "bound2", "bound3", } for _, file in ipairs(files) do cpml[file] = require(modules .. file) end return cpml ================================================ FILE: modules/_private_precond.lua ================================================ -- Preconditions for cpml functions. local precond = {} function precond.typeof(t, expected, msg) if type(t) ~= expected then error(("%s: %s (<%s> expected)"):format(msg, type(t), expected), 3) end end function precond.assert(cond, msg, ...) if not cond then error(msg:format(...), 3) end end return precond ================================================ FILE: modules/_private_utils.lua ================================================ -- Functions exported by utils.lua but needed by vec2 or vec3 (which utils.lua requires) local private = {} local floor = math.floor local ceil = math.ceil function private.round(value, precision) if precision then return private.round(value / precision) * precision end return value >= 0 and floor(value+0.5) or ceil(value-0.5) end function private.is_nan(a) return a ~= a end return private ================================================ FILE: modules/bound2.lua ================================================ --- A 2 component bounding box. -- @module bound2 local modules = (...):gsub('%.[^%.]+$', '') .. "." local vec2 = require(modules .. "vec2") local bound2 = {} local bound2_mt = {} -- Private constructor. local function new(min, max) return setmetatable({ min=min, -- min: vec2, minimum value for each component max=max, -- max: vec2, maximum value for each component }, bound2_mt) end -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. local status, ffi if type(jit) == "table" and jit.status() then status, ffi = pcall(require, "ffi") if status then ffi.cdef "typedef struct { cpml_vec2 min, max; } cpml_bound2;" new = ffi.typeof("cpml_bound2") end end bound2.zero = new(vec2.zero, vec2.zero) --- The public constructor. -- @param min Can be of two types:
-- vec2 min, minimum value for each component -- nil Create bound at single point 0,0 -- @tparam vec2 max, maximum value for each component -- @treturn bound2 out function bound2.new(min, max) if min and max then return new(min:clone(), max:clone()) elseif min or max then error("Unexpected nil argument to bound2.new") else return new(vec2.zero, vec2.zero) end end --- Clone a bound. -- @tparam bound2 a bound to be cloned -- @treturn bound2 out function bound2.clone(a) return new(a.min, a.max) end --- Construct a bound covering one or two points -- @tparam vec2 a Any vector -- @tparam vec2 b Any second vector (optional) -- @treturn vec2 Minimum bound containing the given points function bound2.at(a, b) -- "bounded by". b may be nil if b then return bound2.new(a,b):check() else return bound2.zero:with_center(a) end end --- Extend bound to include point -- @tparam bound2 a bound -- @tparam vec2 point to include -- @treturn bound2 Bound covering current min, current max and new point function bound2.extend(a, center) return bound2.new(a.min:component_min(center), a.max:component_max(center)) end --- Extend bound to entirety of other bound -- @tparam bound2 a bound -- @tparam bound2 bound to cover -- @treturn bound2 Bound covering current min and max of each bound in the pair function bound2.extend_bound(a, b) return a:extend(b.min):extend(b.max) end --- Get size of bounding box as a vector -- @tparam bound2 a bound -- @treturn vec2 Vector spanning min to max points function bound2.size(a) return a.max - a.min end --- Resize bounding box from minimum corner -- @tparam bound2 a bound -- @tparam vec2 new size -- @treturn bound2 resized bound function bound2.with_size(a, size) return bound2.new(a.min, a.min + size) end --- Get half-size of bounding box as a vector. A more correct term for this is probably "apothem" -- @tparam bound2 a bound -- @treturn vec2 Vector spanning center to max point function bound2.radius(a) return a:size()/2 end --- Get center of bounding box -- @tparam bound2 a bound -- @treturn bound2 Point in center of bound function bound2.center(a) return (a.min + a.max)/2 end --- Move bounding box to new center -- @tparam bound2 a bound -- @tparam vec2 new center -- @treturn bound2 Bound with same size as input but different center function bound2.with_center(a, center) return bound2.offset(a, center - a:center()) end --- Resize bounding box from center -- @tparam bound2 a bound -- @tparam vec2 new size -- @treturn bound2 resized bound function bound2.with_size_centered(a, size) local center = a:center() local rad = size/2 return bound2.new(center - rad, center + rad) end --- Convert possibly-invalid bounding box to valid one -- @tparam bound2 a bound -- @treturn bound2 bound with all components corrected for min-max property function bound2.check(a) if a.min.x > a.max.x or a.min.y > a.max.y then return bound2.new(vec2.component_min(a.min, a.max), vec2.component_max(a.min, a.max)) end return a end --- Shrink bounding box with fixed margin -- @tparam bound2 a bound -- @tparam vec2 a margin -- @treturn bound2 bound with margin subtracted from all edges. May not be valid, consider calling check() function bound2.inset(a, v) return bound2.new(a.min + v, a.max - v) end --- Expand bounding box with fixed margin -- @tparam bound2 a bound -- @tparam vec2 a margin -- @treturn bound2 bound with margin added to all edges. May not be valid, consider calling check() function bound2.outset(a, v) return bound2.new(a.min - v, a.max + v) end --- Offset bounding box -- @tparam bound2 a bound -- @tparam vec2 offset -- @treturn bound2 bound with same size, but position moved by offset function bound2.offset(a, v) return bound2.new(a.min + v, a.max + v) end --- Test if point in bound -- @tparam bound2 a bound -- @tparam vec2 point to test -- @treturn boolean true if point in bounding box function bound2.contains(a, v) return a.min.x <= v.x and a.min.y <= v.y and a.max.x >= v.x and a.max.y >= v.y end -- Round all components of all vectors to nearest int (or other precision). -- @tparam vec3 a bound to round. -- @tparam precision Digits after the decimal (round number if unspecified) -- @treturn vec3 Rounded bound function bound2.round(a, precision) return bound2.new(a.min:round(precision), a.max:round(precision)) end --- Return a formatted string. -- @tparam bound2 a bound to be turned into a string -- @treturn string formatted function bound2.to_string(a) return string.format("(%s-%s)", a.min, a.max) end bound2_mt.__index = bound2 bound2_mt.__tostring = bound2.to_string function bound2_mt.__call(_, a, b) return bound2.new(a, b) end if status then xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded ffi.metatype(new, bound2_mt) end, function() end) end return setmetatable({}, bound2_mt) ================================================ FILE: modules/bound3.lua ================================================ --- A 3-component axis-aligned bounding box. -- @module bound3 local modules = (...):gsub('%.[^%.]+$', '') .. "." local vec3 = require(modules .. "vec3") local bound3 = {} local bound3_mt = {} -- Private constructor. local function new(min, max) return setmetatable({ min=min, -- min: vec3, minimum value for each component max=max -- max: vec3, maximum value for each component }, bound3_mt) end -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. local status, ffi if type(jit) == "table" and jit.status() then status, ffi = pcall(require, "ffi") if status then ffi.cdef "typedef struct { cpml_vec3 min, max; } cpml_bound3;" new = ffi.typeof("cpml_bound3") end end bound3.zero = new(vec3.zero, vec3.zero) --- The public constructor. -- @param min Can be of two types:
-- vec3 min, minimum value for each component -- nil Create bound at single point 0,0,0 -- @tparam vec3 max, maximum value for each component -- @treturn bound3 out function bound3.new(min, max) if min and max then return new(min:clone(), max:clone()) elseif min or max then error("Unexpected nil argument to bound3.new") else return new(vec3.zero, vec3.zero) end end --- Clone a bound. -- @tparam bound3 a bound to be cloned -- @treturn bound3 out function bound3.clone(a) return new(a.min, a.max) end --- Construct a bound covering one or two points -- @tparam vec3 a Any vector -- @tparam vec3 b Any second vector (optional) -- @treturn vec3 Minimum bound containing the given points function bound3.at(a, b) -- "bounded by". b may be nil if b then return bound3.new(a,b):check() else return bound3.zero:with_center(a) end end --- Extend bound to include point -- @tparam bound3 a bound -- @tparam vec3 point to include -- @treturn bound3 Bound covering current min, current max and new point function bound3.extend(a, center) return bound3.new(a.min:component_min(center), a.max:component_max(center)) end --- Extend bound to entirety of other bound -- @tparam bound3 a bound -- @tparam bound3 bound to cover -- @treturn bound3 Bound covering current min and max of each bound in the pair function bound3.extend_bound(a, b) return a:extend(b.min):extend(b.max) end --- Get size of bounding box as a vector -- @tparam bound3 a bound -- @treturn vec3 Vector spanning min to max points function bound3.size(a) return a.max - a.min end --- Resize bounding box from minimum corner -- @tparam bound3 a bound -- @tparam vec3 new size -- @treturn bound3 resized bound function bound3.with_size(a, size) return bound3.new(a.min, a.min + size) end --- Get half-size of bounding box as a vector. A more correct term for this is probably "apothem" -- @tparam bound3 a bound -- @treturn vec3 Vector spanning center to max point function bound3.radius(a) return a:size()/2 end --- Get center of bounding box -- @tparam bound3 a bound -- @treturn bound3 Point in center of bound function bound3.center(a) return (a.min + a.max)/2 end --- Move bounding box to new center -- @tparam bound3 a bound -- @tparam vec3 new center -- @treturn bound3 Bound with same size as input but different center function bound3.with_center(a, center) return bound3.offset(a, center - a:center()) end --- Resize bounding box from center -- @tparam bound3 a bound -- @tparam vec3 new size -- @treturn bound3 resized bound function bound3.with_size_centered(a, size) local center = a:center() local rad = size/2 return bound3.new(center - rad, center + rad) end --- Convert possibly-invalid bounding box to valid one -- @tparam bound3 a bound -- @treturn bound3 bound with all components corrected for min-max property function bound3.check(a) if a.min.x > a.max.x or a.min.y > a.max.y or a.min.z > a.max.z then return bound3.new(vec3.component_min(a.min, a.max), vec3.component_max(a.min, a.max)) end return a end --- Shrink bounding box with fixed margin -- @tparam bound3 a bound -- @tparam vec3 a margin -- @treturn bound3 bound with margin subtracted from all edges. May not be valid, consider calling check() function bound3.inset(a, v) return bound3.new(a.min + v, a.max - v) end --- Expand bounding box with fixed margin -- @tparam bound3 a bound -- @tparam vec3 a margin -- @treturn bound3 bound with margin added to all edges. May not be valid, consider calling check() function bound3.outset(a, v) return bound3.new(a.min - v, a.max + v) end --- Offset bounding box -- @tparam bound3 a bound -- @tparam vec3 offset -- @treturn bound3 bound with same size, but position moved by offset function bound3.offset(a, v) return bound3.new(a.min + v, a.max + v) end --- Test if point in bound -- @tparam bound3 a bound -- @tparam vec3 point to test -- @treturn boolean true if point in bounding box function bound3.contains(a, v) return a.min.x <= v.x and a.min.y <= v.y and a.min.z <= v.z and a.max.x >= v.x and a.max.y >= v.y and a.max.z >= v.z end -- Round all components of all vectors to nearest int (or other precision). -- @tparam vec3 a bound to round. -- @tparam precision Digits after the decimal (round number if unspecified) -- @treturn vec3 Rounded bound function bound3.round(a, precision) return bound3.new(a.min:round(precision), a.max:round(precision)) end --- Return a formatted string. -- @tparam bound3 a bound to be turned into a string -- @treturn string formatted function bound3.to_string(a) return string.format("(%s-%s)", a.min, a.max) end bound3_mt.__index = bound3 bound3_mt.__tostring = bound3.to_string function bound3_mt.__call(_, a, b) return bound3.new(a, b) end if status then xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded ffi.metatype(new, bound3_mt) end, function() end) end return setmetatable({}, bound3_mt) ================================================ FILE: modules/bvh.lua ================================================ -- https://github.com/benraziel/bvh-tree --- BVH Tree -- @module bvh local modules = (...):gsub('%.[^%.]+$', '') .. "." local intersect = require(modules .. "intersect") local vec3 = require(modules .. "vec3") local EPSILON = 1e-6 local BVH = {} local BVHNode = {} local Node BVH.__index = BVH BVHNode.__index = BVHNode local function new(triangles, maxTrianglesPerNode) local tree = setmetatable({}, BVH) local trianglesArray = {} for _, triangle in ipairs(triangles) do local p1 = triangle[1] local p2 = triangle[2] local p3 = triangle[3] table.insert(trianglesArray, p1.x or p1[1]) table.insert(trianglesArray, p1.y or p1[2]) table.insert(trianglesArray, p1.z or p1[3]) table.insert(trianglesArray, p2.x or p2[1]) table.insert(trianglesArray, p2.y or p2[2]) table.insert(trianglesArray, p2.z or p2[3]) table.insert(trianglesArray, p3.x or p3[1]) table.insert(trianglesArray, p3.y or p3[2]) table.insert(trianglesArray, p3.z or p3[3]) end tree._trianglesArray = trianglesArray tree._maxTrianglesPerNode = maxTrianglesPerNode or 10 tree._bboxArray = tree.calcBoundingBoxes(trianglesArray) -- clone a helper array tree._bboxHelper = {} for _, bbox in ipairs(tree._bboxArray) do table.insert(tree._bboxHelper, bbox) end -- create the root node, add all the triangles to it local triangleCount = #triangles local extents = tree:calcExtents(1, triangleCount, EPSILON) tree._rootNode = Node(extents[1], extents[2], 1, triangleCount, 1) tree._nodes_to_split = { tree._rootNode } while #tree._nodes_to_split > 0 do local node = table.remove(tree._nodes_to_split) tree:splitNode(node) end return tree end function BVH:intersectAABB(aabb) local nodesToIntersect = { self._rootNode } local trianglesInIntersectingNodes = {} -- a list of nodes that intersect the ray (according to their bounding box) local intersectingTriangles = {} -- go over the BVH tree, and extract the list of triangles that lie in nodes that intersect the box. -- note: these triangles may not intersect the box themselves while #nodesToIntersect > 0 do local node = table.remove(nodesToIntersect) local node_aabb = { min = node._extentsMin, max = node._extentsMax } if intersect.aabb_aabb(aabb, node_aabb) then if node._node0 then table.insert(nodesToIntersect, node._node0) end if node._node1 then table.insert(nodesToIntersect, node._node1) end for i=node._startIndex, node._endIndex do table.insert(trianglesInIntersectingNodes, self._bboxArray[1+(i-1)*7]) end end end -- insert all node triangles, don't bother being more specific yet. local triangle = { vec3(), vec3(), vec3() } for i=1, #trianglesInIntersectingNodes do local triIndex = trianglesInIntersectingNodes[i] -- print(triIndex, #self._trianglesArray) triangle[1].x = self._trianglesArray[1+(triIndex-1)*9] triangle[1].y = self._trianglesArray[1+(triIndex-1)*9+1] triangle[1].z = self._trianglesArray[1+(triIndex-1)*9+2] triangle[2].x = self._trianglesArray[1+(triIndex-1)*9+3] triangle[2].y = self._trianglesArray[1+(triIndex-1)*9+4] triangle[2].z = self._trianglesArray[1+(triIndex-1)*9+5] triangle[3].x = self._trianglesArray[1+(triIndex-1)*9+6] triangle[3].y = self._trianglesArray[1+(triIndex-1)*9+7] triangle[3].z = self._trianglesArray[1+(triIndex-1)*9+8] table.insert(intersectingTriangles, { triangle = { triangle[1]:clone(), triangle[2]:clone(), triangle[3]:clone() }, triangleIndex = triIndex }) end return intersectingTriangles end function BVH:intersectRay(rayOrigin, rayDirection, backfaceCulling) local nodesToIntersect = { self._rootNode } local trianglesInIntersectingNodes = {} -- a list of nodes that intersect the ray (according to their bounding box) local intersectingTriangles = {} local invRayDirection = vec3( 1 / rayDirection.x, 1 / rayDirection.y, 1 / rayDirection.z ) -- go over the BVH tree, and extract the list of triangles that lie in nodes that intersect the ray. -- note: these triangles may not intersect the ray themselves while #nodesToIntersect > 0 do local node = table.remove(nodesToIntersect) if BVH.intersectNodeBox(rayOrigin, invRayDirection, node) then if node._node0 then table.insert(nodesToIntersect, node._node0) end if node._node1 then table.insert(nodesToIntersect, node._node1) end for i=node._startIndex, node._endIndex do table.insert(trianglesInIntersectingNodes, self._bboxArray[1+(i-1)*7]) end end end -- go over the list of candidate triangles, and check each of them using ray triangle intersection local triangle = { vec3(), vec3(), vec3() } local ray = { position = vec3(rayOrigin.x, rayOrigin.y, rayOrigin.z), direction = vec3(rayDirection.x, rayDirection.y, rayDirection.z) } for i=1, #trianglesInIntersectingNodes do local triIndex = trianglesInIntersectingNodes[i] -- print(triIndex, #self._trianglesArray) triangle[1].x = self._trianglesArray[1+(triIndex-1)*9] triangle[1].y = self._trianglesArray[1+(triIndex-1)*9+1] triangle[1].z = self._trianglesArray[1+(triIndex-1)*9+2] triangle[2].x = self._trianglesArray[1+(triIndex-1)*9+3] triangle[2].y = self._trianglesArray[1+(triIndex-1)*9+4] triangle[2].z = self._trianglesArray[1+(triIndex-1)*9+5] triangle[3].x = self._trianglesArray[1+(triIndex-1)*9+6] triangle[3].y = self._trianglesArray[1+(triIndex-1)*9+7] triangle[3].z = self._trianglesArray[1+(triIndex-1)*9+8] local intersectionPoint, intersectionDistance = intersect.ray_triangle(ray, triangle, backfaceCulling) if intersectionPoint then table.insert(intersectingTriangles, { triangle = { triangle[1]:clone(), triangle[2]:clone(), triangle[3]:clone() }, triangleIndex = triIndex, intersectionPoint = intersectionPoint, intersectionDistance = intersectionDistance }) end end return intersectingTriangles end function BVH.calcBoundingBoxes(trianglesArray) local p1x, p1y, p1z local p2x, p2y, p2z local p3x, p3y, p3z local minX, minY, minZ local maxX, maxY, maxZ local bboxArray = {} for i=1, #trianglesArray / 9 do p1x = trianglesArray[1+(i-1)*9] p1y = trianglesArray[1+(i-1)*9+1] p1z = trianglesArray[1+(i-1)*9+2] p2x = trianglesArray[1+(i-1)*9+3] p2y = trianglesArray[1+(i-1)*9+4] p2z = trianglesArray[1+(i-1)*9+5] p3x = trianglesArray[1+(i-1)*9+6] p3y = trianglesArray[1+(i-1)*9+7] p3z = trianglesArray[1+(i-1)*9+8] minX = math.min(p1x, p2x, p3x) minY = math.min(p1y, p2y, p3y) minZ = math.min(p1z, p2z, p3z) maxX = math.max(p1x, p2x, p3x) maxY = math.max(p1y, p2y, p3y) maxZ = math.max(p1z, p2z, p3z) BVH.setBox(bboxArray, i, i, minX, minY, minZ, maxX, maxY, maxZ) end return bboxArray end function BVH:calcExtents(startIndex, endIndex, expandBy) expandBy = expandBy or 0 if startIndex > endIndex then return { vec3(), vec3() } end local minX = math.huge local minY = math.huge local minZ = math.huge local maxX = -math.huge local maxY = -math.huge local maxZ = -math.huge for i=startIndex, endIndex do minX = math.min(self._bboxArray[1+(i-1)*7+1], minX) minY = math.min(self._bboxArray[1+(i-1)*7+2], minY) minZ = math.min(self._bboxArray[1+(i-1)*7+3], minZ) maxX = math.max(self._bboxArray[1+(i-1)*7+4], maxX) maxY = math.max(self._bboxArray[1+(i-1)*7+5], maxY) maxZ = math.max(self._bboxArray[1+(i-1)*7+6], maxZ) end return { vec3(minX - expandBy, minY - expandBy, minZ - expandBy), vec3(maxX + expandBy, maxY + expandBy, maxZ + expandBy) } end function BVH:splitNode(node) local num_elements = node:elementCount() if (num_elements <= self._maxTrianglesPerNode) or (num_elements <= 0) then return end local startIndex = node._startIndex local endIndex = node._endIndex local leftNode = { {},{},{} } local rightNode = { {},{},{} } local extentCenters = { node:centerX(), node:centerY(), node:centerZ() } local extentsLength = { node._extentsMax.x - node._extentsMin.x, node._extentsMax.y - node._extentsMin.y, node._extentsMax.z - node._extentsMin.z } local objectCenter = {} for i=startIndex, endIndex do objectCenter[1] = (self._bboxArray[1+(i-1)*7+1] + self._bboxArray[1+(i-1)*7+4]) * 0.5 -- center = (min + max) / 2 objectCenter[2] = (self._bboxArray[1+(i-1)*7+2] + self._bboxArray[1+(i-1)*7+5]) * 0.5 -- center = (min + max) / 2 objectCenter[3] = (self._bboxArray[1+(i-1)*7+3] + self._bboxArray[1+(i-1)*7+6]) * 0.5 -- center = (min + max) / 2 for j=1, 3 do if objectCenter[j] < extentCenters[j] then table.insert(leftNode[j], i) else table.insert(rightNode[j], i) end end end -- check if we couldn't split the node by any of the axes (x, y or z). halt -- here, dont try to split any more (cause it will always fail, and we'll -- enter an infinite loop local splitFailed = { #leftNode[1] == 0 or #rightNode[1] == 0, #leftNode[2] == 0 or #rightNode[2] == 0, #leftNode[3] == 0 or #rightNode[3] == 0 } if splitFailed[1] and splitFailed[2] and splitFailed[3] then return end -- choose the longest split axis. if we can't split by it, choose next best one. local splitOrder = { 1, 2, 3 } table.sort(splitOrder, function(a, b) return extentsLength[a] > extentsLength[b] end) local leftElements local rightElements for i=1, 3 do local candidateIndex = splitOrder[i] if not splitFailed[candidateIndex] then leftElements = leftNode[candidateIndex] rightElements = rightNode[candidateIndex] break end end -- sort the elements in range (startIndex, endIndex) according to which node they should be at local node0Start = startIndex local node1Start = node0Start + #leftElements local node0End = node1Start - 1 local node1End = endIndex local currElement local helperPos = node._startIndex local concatenatedElements = {} for _, element in ipairs(leftElements) do table.insert(concatenatedElements, element) end for _, element in ipairs(rightElements) do table.insert(concatenatedElements, element) end -- print(#leftElements, #rightElements, #concatenatedElements) for i=1, #concatenatedElements do currElement = concatenatedElements[i] BVH.copyBox(self._bboxArray, currElement, self._bboxHelper, helperPos) helperPos = helperPos + 1 end -- copy results back to main array for i=1+(node._startIndex-1)*7, node._endIndex*7 do self._bboxArray[i] = self._bboxHelper[i] end -- create 2 new nodes for the node we just split, and add links to them from the parent node local node0Extents = self:calcExtents(node0Start, node0End, EPSILON) local node1Extents = self:calcExtents(node1Start, node1End, EPSILON) local node0 = Node(node0Extents[1], node0Extents[2], node0Start, node0End, node._level + 1) local node1 = Node(node1Extents[1], node1Extents[2], node1Start, node1End, node._level + 1) node._node0 = node0 node._node1 = node1 node:clearShapes() -- add new nodes to the split queue table.insert(self._nodes_to_split, node0) table.insert(self._nodes_to_split, node1) end function BVH._calcTValues(minVal, maxVal, rayOriginCoord, invdir) local res = { min=0, max=0 } if invdir >= 0 then res.min = ( minVal - rayOriginCoord ) * invdir res.max = ( maxVal - rayOriginCoord ) * invdir else res.min = ( maxVal - rayOriginCoord ) * invdir res.max = ( minVal - rayOriginCoord ) * invdir end return res end function BVH.intersectNodeBox(rayOrigin, invRayDirection, node) local t = BVH._calcTValues(node._extentsMin.x, node._extentsMax.x, rayOrigin.x, invRayDirection.x) local ty = BVH._calcTValues(node._extentsMin.y, node._extentsMax.y, rayOrigin.y, invRayDirection.y) if t.min > ty.max or ty.min > t.max then return false end -- These lines also handle the case where tmin or tmax is NaN -- (result of 0 * Infinity). x !== x returns true if x is NaN if ty.min > t.min or t.min ~= t.min then t.min = ty.min end if ty.max < t.max or t.max ~= t.max then t.max = ty.max end local tz = BVH._calcTValues(node._extentsMin.z, node._extentsMax.z, rayOrigin.z, invRayDirection.z) if t.min > tz.max or tz.min > t.max then return false end if tz.min > t.min or t.min ~= t.min then t.min = tz.min end if tz.max < t.max or t.max ~= t.max then t.max = tz.max end --return point closest to the ray (positive side) if t.max < 0 then return false end return true end function BVH.setBox(bboxArray, pos, triangleId, minX, minY, minZ, maxX, maxY, maxZ) bboxArray[1+(pos-1)*7] = triangleId bboxArray[1+(pos-1)*7+1] = minX bboxArray[1+(pos-1)*7+2] = minY bboxArray[1+(pos-1)*7+3] = minZ bboxArray[1+(pos-1)*7+4] = maxX bboxArray[1+(pos-1)*7+5] = maxY bboxArray[1+(pos-1)*7+6] = maxZ end function BVH.copyBox(sourceArray, sourcePos, destArray, destPos) destArray[1+(destPos-1)*7] = sourceArray[1+(sourcePos-1)*7] destArray[1+(destPos-1)*7+1] = sourceArray[1+(sourcePos-1)*7+1] destArray[1+(destPos-1)*7+2] = sourceArray[1+(sourcePos-1)*7+2] destArray[1+(destPos-1)*7+3] = sourceArray[1+(sourcePos-1)*7+3] destArray[1+(destPos-1)*7+4] = sourceArray[1+(sourcePos-1)*7+4] destArray[1+(destPos-1)*7+5] = sourceArray[1+(sourcePos-1)*7+5] destArray[1+(destPos-1)*7+6] = sourceArray[1+(sourcePos-1)*7+6] end function BVH.getBox(bboxArray, pos, outputBox) outputBox.triangleId = bboxArray[1+(pos-1)*7] outputBox.minX = bboxArray[1+(pos-1)*7+1] outputBox.minY = bboxArray[1+(pos-1)*7+2] outputBox.minZ = bboxArray[1+(pos-1)*7+3] outputBox.maxX = bboxArray[1+(pos-1)*7+4] outputBox.maxY = bboxArray[1+(pos-1)*7+5] outputBox.maxZ = bboxArray[1+(pos-1)*7+6] end local function new_node(extentsMin, extentsMax, startIndex, endIndex, level) return setmetatable({ _extentsMin = extentsMin, _extentsMax = extentsMax, _startIndex = startIndex, _endIndex = endIndex, _level = level --_node0 = nil --_node1 = nil }, BVHNode) end function BVHNode:elementCount() return (self._endIndex + 1) - self._startIndex end function BVHNode:centerX() return (self._extentsMin.x + self._extentsMax.x) * 0.5 end function BVHNode:centerY() return (self._extentsMin.y + self._extentsMax.y) * 0.5 end function BVHNode:centerZ() return (self._extentsMin.z + self._extentsMax.z) * 0.5 end function BVHNode:clearShapes() self._startIndex = 0 self._endIndex = -1 end function BVHNode.ngSphereRadius(extentsMin, extentsMax) local centerX = (extentsMin.x + extentsMax.x) * 0.5 local centerY = (extentsMin.y + extentsMax.y) * 0.5 local centerZ = (extentsMin.z + extentsMax.z) * 0.5 local extentsMinDistSqr = (centerX - extentsMin.x) * (centerX - extentsMin.x) + (centerY - extentsMin.y) * (centerY - extentsMin.y) + (centerZ - extentsMin.z) * (centerZ - extentsMin.z) local extentsMaxDistSqr = (centerX - extentsMax.x) * (centerX - extentsMax.x) + (centerY - extentsMax.y) * (centerY - extentsMax.y) + (centerZ - extentsMax.z) * (centerZ - extentsMax.z) return math.sqrt(math.max(extentsMinDistSqr, extentsMaxDistSqr)) end --[[ --- Draws node boundaries visually for debugging. -- @param cube Cube model to draw -- @param depth Used for recurcive calls to self method function OctreeNode:draw_bounds(cube, depth) depth = depth or 0 local tint = depth / 7 -- Will eventually get values > 1. Color rounds to 1 automatically love.graphics.setColor(tint * 255, 0, (1 - tint) * 255) local m = mat4() :translate(self.center) :scale(vec3(self.adjLength, self.adjLength, self.adjLength)) love.graphics.updateMatrix("transform", m) love.graphics.setWireframe(true) love.graphics.draw(cube) love.graphics.setWireframe(false) for _, child in ipairs(self.children) do child:draw_bounds(cube, depth + 1) end love.graphics.setColor(255, 255, 255) end --- Draws the bounds of all objects in the tree visually for debugging. -- @param cube Cube model to draw -- @param filter a function returning true or false to determine visibility. function OctreeNode:draw_objects(cube, filter) local tint = self.baseLength / 20 love.graphics.setColor(0, (1 - tint) * 255, tint * 255, 63) for _, object in ipairs(self.objects) do if filter and filter(object.data) or not filter then local m = mat4() :translate(object.bounds.center) :scale(object.bounds.size) love.graphics.updateMatrix("transform", m) love.graphics.draw(cube) end end for _, child in ipairs(self.children) do child:draw_objects(cube, filter) end love.graphics.setColor(255, 255, 255) end --]] Node = setmetatable({ new = new_node }, { __call = function(_, ...) return new_node(...) end }) return setmetatable({ new = new }, { __call = function(_, ...) return new(...) end }) ================================================ FILE: modules/color.lua ================================================ --- Color utilities -- @module color local modules = (...):gsub('%.[^%.]+$', '') .. "." local constants = require(modules .. "constants") local utils = require(modules .. "utils") local precond = require(modules .. "_private_precond") local color = {} local color_mt = {} local function new(r, g, b, a) local c = { r, g, b, a } c._c = c return setmetatable(c, color_mt) end -- HSV utilities (adapted from http://www.cs.rit.edu/~ncs/color/t_convert.html) -- hsv_to_color(hsv) -- Converts a set of HSV values to a color. hsv is a table. -- See also: hsv(h, s, v) local function hsv_to_color(hsv) local i local f, q, p, t local h, s, v local a = hsv[4] or 1 s = hsv[2] v = hsv[3] if s == 0 then return new(v, v, v, a) end h = hsv[1] * 6 -- sector 0 to 5 i = math.floor(h) f = h - i -- factorial part of h p = v * (1-s) q = v * (1-s*f) t = v * (1-s*(1-f)) if i == 0 then return new(v, t, p, a) elseif i == 1 then return new(q, v, p, a) elseif i == 2 then return new(p, v, t, a) elseif i == 3 then return new(p, q, v, a) elseif i == 4 then return new(t, p, v, a) else return new(v, p, q, a) end end -- color_to_hsv(c) -- Takes in a normal color and returns a table with the HSV values. local function color_to_hsv(c) local r = c[1] local g = c[2] local b = c[3] local a = c[4] or 1 local h, s, v local min = math.min(r, g, b) local max = math.max(r, g, b) v = max local delta = max - min -- black, nothing else is really possible here. if min == 0 and max == 0 then return { 0, 0, 0, a } end if max ~= 0 then s = delta / max else -- r = g = b = 0 s = 0, v is undefined s = 0 h = -1 return { h, s, v, 1 } end -- Prevent division by zero. if delta == 0 then delta = constants.DBL_EPSILON end if r == max then h = ( g - b ) / delta -- yellow/magenta elseif g == max then h = 2 + ( b - r ) / delta -- cyan/yellow else h = 4 + ( r - g ) / delta -- magenta/cyan end h = h / 6 -- normalize from segment 0..5 if h < 0 then h = h + 1 end return { h, s, v, a } end --- The public constructor. -- @param x Can be of three types:
-- number red component 0-1 -- table {r, g, b, a} -- nil for {0,0,0,0} -- @tparam number g Green component 0-1 -- @tparam number b Blue component 0-1 -- @tparam number a Alpha component 0-1 -- @treturn color out function color.new(r, g, b, a) -- number, number, number, number if r and g and b and a then precond.typeof(r, "number", "new: Wrong argument type for r") precond.typeof(g, "number", "new: Wrong argument type for g") precond.typeof(b, "number", "new: Wrong argument type for b") precond.typeof(a, "number", "new: Wrong argument type for a") return new(r, g, b, a) -- {r, g, b, a} elseif type(r) == "table" then local rr, gg, bb, aa = r[1], r[2], r[3], r[4] precond.typeof(rr, "number", "new: Wrong argument type for r") precond.typeof(gg, "number", "new: Wrong argument type for g") precond.typeof(bb, "number", "new: Wrong argument type for b") precond.typeof(aa, "number", "new: Wrong argument type for a") return new(rr, gg, bb, aa) end return new(0, 0, 0, 0) end --- Convert hue,saturation,value table to color object. -- @tparam table hsva {hue 0-1, saturation 0-1, value 0-1, alpha 0-1} -- @treturn color out color.hsv_to_color_table = hsv_to_color --- Convert color to hue,saturation,value table -- @tparam color in -- @treturn table hsva {hue 0-1, saturation 0-1, value 0-1, alpha 0-1} color.color_to_hsv_table = color_to_hsv --- Convert hue,saturation,value to color object. -- @tparam number h hue 0-1 -- @tparam number s saturation 0-1 -- @tparam number v value 0-1 -- @treturn color out function color.from_hsv(h, s, v) return hsv_to_color { h, s, v } end --- Convert hue,saturation,value to color object. -- @tparam number h hue 0-1 -- @tparam number s saturation 0-1 -- @tparam number v value 0-1 -- @tparam number a alpha 0-1 -- @treturn color out function color.from_hsva(h, s, v, a) return hsv_to_color { h, s, v, a } end --- Invert a color. -- @tparam color to invert -- @treturn color out function color.invert(c) return new(1 - c[1], 1 - c[2], 1 - c[3], c[4]) end --- Lighten a color by a component-wise fixed amount (alpha unchanged) -- @tparam color to lighten -- @tparam number amount to increase each component by, 0-1 scale -- @treturn color out function color.lighten(c, v) return new( utils.clamp(c[1] + v, 0, 1), utils.clamp(c[2] + v, 0, 1), utils.clamp(c[3] + v, 0, 1), c[4] ) end --- Interpolate between two colors. -- @tparam color at start -- @tparam color at end -- @tparam number s in 0-1 progress between the two colors -- @treturn color out function color.lerp(a, b, s) return a + s * (b - a) end --- Unpack a color into individual components in 0-1. -- @tparam color to unpack -- @treturn number r in 0-1 -- @treturn number g in 0-1 -- @treturn number b in 0-1 -- @treturn number a in 0-1 function color.unpack(c) return c[1], c[2], c[3], c[4] end --- Unpack a color into individual components in 0-255. -- @tparam color to unpack -- @treturn number r in 0-255 -- @treturn number g in 0-255 -- @treturn number b in 0-255 -- @treturn number a in 0-255 function color.as_255(c) return c[1] * 255, c[2] * 255, c[3] * 255, c[4] * 255 end --- Darken a color by a component-wise fixed amount (alpha unchanged) -- @tparam color to darken -- @tparam number amount to decrease each component by, 0-1 scale -- @treturn color out function color.darken(c, v) return new( utils.clamp(c[1] - v, 0, 1), utils.clamp(c[2] - v, 0, 1), utils.clamp(c[3] - v, 0, 1), c[4] ) end --- Multiply a color's components by a value (alpha unchanged) -- @tparam color to multiply -- @tparam number to multiply each component by -- @treturn color out function color.multiply(c, v) local t = color.new() for i = 1, 3 do t[i] = c[i] * v end t[4] = c[4] return t end -- directly set alpha channel -- @tparam color to alter -- @tparam number new alpha 0-1 -- @treturn color out function color.alpha(c, v) local t = color.new() for i = 1, 3 do t[i] = c[i] end t[4] = v return t end --- Multiply a color's alpha by a value -- @tparam color to multiply -- @tparam number to multiply alpha by -- @treturn color out function color.opacity(c, v) local t = color.new() for i = 1, 3 do t[i] = c[i] end t[4] = c[4] * v return t end --- Set a color's hue (saturation, value, alpha unchanged) -- @tparam color to alter -- @tparam hue to set 0-1 -- @treturn color out function color.hue(col, hue) local c = color_to_hsv(col) c[1] = (hue + 1) % 1 return hsv_to_color(c) end --- Set a color's saturation (hue, value, alpha unchanged) -- @tparam color to alter -- @tparam saturation to set 0-1 -- @treturn color out function color.saturation(col, percent) local c = color_to_hsv(col) c[2] = utils.clamp(percent, 0, 1) return hsv_to_color(c) end --- Set a color's value (saturation, hue, alpha unchanged) -- @tparam color to alter -- @tparam value to set 0-1 -- @treturn color out function color.value(col, percent) local c = color_to_hsv(col) c[3] = utils.clamp(percent, 0, 1) return hsv_to_color(c) end -- https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ function color.gamma_to_linear(r, g, b, a) local function convert(c) if c > 1.0 then return 1.0 elseif c < 0.0 then return 0.0 elseif c <= 0.04045 then return c / 12.92 else return math.pow((c + 0.055) / 1.055, 2.4) end end if type(r) == "table" then local c = {} for i = 1, 3 do c[i] = convert(r[i]) end c[4] = r[4] return c else return convert(r), convert(g), convert(b), a or 1 end end -- https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB function color.linear_to_gamma(r, g, b, a) local function convert(c) if c > 1.0 then return 1.0 elseif c < 0.0 then return 0.0 elseif c < 0.0031308 then return c * 12.92 else return 1.055 * math.pow(c, 0.41666) - 0.055 end end if type(r) == "table" then local c = {} for i = 1, 3 do c[i] = convert(r[i]) end c[4] = r[4] return c else return convert(r), convert(g), convert(b), a or 1 end end --- Check if color is valid -- @tparam color to test -- @treturn boolean is color function color.is_color(a) if type(a) ~= "table" then return false end for i = 1, 4 do if type(a[i]) ~= "number" then return false end end return true end --- Return a formatted string. -- @tparam color a color to be turned into a string -- @treturn string formatted function color.to_string(a) return string.format("[ %3.0f, %3.0f, %3.0f, %3.0f ]", a[1], a[2], a[3], a[4]) end color_mt.__index = color color_mt.__tostring = color.to_string function color_mt.__call(_, r, g, b, a) return color.new(r, g, b, a) end function color_mt.__add(a, b) return new(a[1] + b[1], a[2] + b[2], a[3] + b[3], a[4] + b[4]) end function color_mt.__sub(a, b) return new(a[1] - b[1], a[2] - b[2], a[3] - b[3], a[4] - b[4]) end function color_mt.__mul(a, b) if type(a) == "number" then return new(a * b[1], a * b[2], a * b[3], a * b[4]) elseif type(b) == "number" then return new(b * a[1], b * a[2], b * a[3], b * a[4]) else return new(a[1] * b[1], a[2] * b[2], a[3] * b[3], a[4] * b[4]) end end return setmetatable({}, color_mt) ================================================ FILE: modules/constants.lua ================================================ --- Various useful constants -- @module constants --- Constants -- @table constants -- @field FLT_EPSILON Floating point precision breaks down -- @field DBL_EPSILON Double-precise floating point precision breaks down -- @field DOT_THRESHOLD Close enough to 1 for interpolations to occur local constants = {} -- same as C's FLT_EPSILON constants.FLT_EPSILON = 1.19209290e-07 -- same as C's DBL_EPSILON constants.DBL_EPSILON = 2.2204460492503131e-16 -- used for quaternion.slerp constants.DOT_THRESHOLD = 0.9995 return constants ================================================ FILE: modules/intersect.lua ================================================ --- Various geometric intersections -- @module intersect local modules = (...):gsub('%.[^%.]+$', '') .. "." local constants = require(modules .. "constants") local mat4 = require(modules .. "mat4") local vec3 = require(modules .. "vec3") local utils = require(modules .. "utils") local DBL_EPSILON = constants.DBL_EPSILON local sqrt = math.sqrt local abs = math.abs local min = math.min local max = math.max local intersect = {} -- Checks if a point belongs to the segment -- p is a vec3 -- seg[1] is a vec3 -- seg[2] is a vec3 -- Returns a boolean function intersect.point_segment(p, seg) local min, max = vec3.component_sort(seg[1], seg[2]) if min.x <= p.x and min.y <= p.y and min.z <= p.z and p.x <= max.x and p.y <= max.y and p.z <= max.z then return true end return false end -- https://blogs.msdn.microsoft.com/rezanour/2011/08/07/barycentric-coordinates-and-point-in-triangle-tests/ -- point is a vec3 -- triangle[1] is a vec3 -- triangle[2] is a vec3 -- triangle[3] is a vec3 function intersect.point_triangle(point, triangle) local u = triangle[2] - triangle[1] local v = triangle[3] - triangle[1] local w = point - triangle[1] local vw = v:cross(w) local vu = v:cross(u) if vw:dot(vu) < 0 then return false end local uw = u:cross(w) local uv = u:cross(v) if uw:dot(uv) < 0 then return false end local d = uv:len() local r = vw:len() / d local t = uw:len() / d return r + t <= 1 end -- point is a vec3 -- aabb.min is a vec3 -- aabb.max is a vec3 function intersect.point_aabb(point, aabb) return aabb.min.x <= point.x and aabb.max.x >= point.x and aabb.min.y <= point.y and aabb.max.y >= point.y and aabb.min.z <= point.z and aabb.max.z >= point.z end -- point is a vec3 -- frustum.left is a plane { a, b, c, d } -- frustum.right is a plane { a, b, c, d } -- frustum.bottom is a plane { a, b, c, d } -- frustum.top is a plane { a, b, c, d } -- frustum.near is a plane { a, b, c, d } -- frustum.far is a plane { a, b, c, d } function intersect.point_frustum(point, frustum) local x, y, z = point:unpack() local planes = { frustum.left, frustum.right, frustum.bottom, frustum.top, frustum.near, frustum.far or false } -- Skip the last test for infinite projections, it'll never fail. if not planes[6] then table.remove(planes) end local dot for i = 1, #planes do dot = planes[i].a * x + planes[i].b * y + planes[i].c * z + planes[i].d if dot <= 0 then return false end end return true end -- http://www.lighthouse3d.com/tutorials/maths/ray-triangle-intersection/ -- ray.position is a vec3 -- ray.direction is a vec3 -- triangle[1] is a vec3 -- triangle[2] is a vec3 -- triangle[3] is a vec3 -- backface_cull is a boolean (optional) function intersect.ray_triangle(ray, triangle, backface_cull) local e1 = triangle[2] - triangle[1] local e2 = triangle[3] - triangle[1] local h = ray.direction:cross(e2) local a = h:dot(e1) -- if a is negative, ray hits the backface if backface_cull and a < 0 then return false end -- if a is too close to 0, ray does not intersect triangle if abs(a) <= DBL_EPSILON then return false end local f = 1 / a local s = ray.position - triangle[1] local u = s:dot(h) * f -- ray does not intersect triangle if u < 0 or u > 1 then return false end local q = s:cross(e1) local v = ray.direction:dot(q) * f -- ray does not intersect triangle if v < 0 or u + v > 1 then return false end -- at this stage we can compute t to find out where -- the intersection point is on the line local t = q:dot(e2) * f -- return position of intersection and distance from ray origin if t >= DBL_EPSILON then return ray.position + ray.direction * t, t end -- ray does not intersect triangle return false end -- https://gamedev.stackexchange.com/questions/96459/fast-ray-sphere-collision-code -- ray.position is a vec3 -- ray.direction is a vec3 -- sphere.position is a vec3 -- sphere.radius is a number function intersect.ray_sphere(ray, sphere) local offset = ray.position - sphere.position local b = offset:dot(ray.direction) local c = offset:dot(offset) - sphere.radius * sphere.radius -- ray's position outside sphere (c > 0) -- ray's direction pointing away from sphere (b > 0) if c > 0 and b > 0 then return false end local discr = b * b - c -- negative discriminant if discr < 0 then return false end -- Clamp t to 0 local t = -b - sqrt(discr) t = t < 0 and 0 or t -- Return collision point and distance from ray origin return ray.position + ray.direction * t, t end -- http://gamedev.stackexchange.com/a/18459 -- ray.position is a vec3 -- ray.direction is a vec3 -- aabb.min is a vec3 -- aabb.max is a vec3 function intersect.ray_aabb(ray, aabb) local dir = ray.direction:normalize() local dirfrac = vec3( 1 / dir.x, 1 / dir.y, 1 / dir.z ) local t1 = (aabb.min.x - ray.position.x) * dirfrac.x local t2 = (aabb.max.x - ray.position.x) * dirfrac.x local t3 = (aabb.min.y - ray.position.y) * dirfrac.y local t4 = (aabb.max.y - ray.position.y) * dirfrac.y local t5 = (aabb.min.z - ray.position.z) * dirfrac.z local t6 = (aabb.max.z - ray.position.z) * dirfrac.z local tmin = max(max(min(t1, t2), min(t3, t4)), min(t5, t6)) local tmax = min(min(max(t1, t2), max(t3, t4)), max(t5, t6)) -- ray is intersecting AABB, but whole AABB is behind us if tmax < 0 then return false end -- ray does not intersect AABB if tmin > tmax then return false end -- Return collision point and distance from ray origin return ray.position + ray.direction * tmin, tmin end -- http://stackoverflow.com/a/23976134/1190664 -- ray.position is a vec3 -- ray.direction is a vec3 -- plane.position is a vec3 -- plane.normal is a vec3 function intersect.ray_plane(ray, plane) local denom = plane.normal:dot(ray.direction) -- ray does not intersect plane if abs(denom) < DBL_EPSILON then return false end -- distance of direction local d = plane.position - ray.position local t = d:dot(plane.normal) / denom if t < DBL_EPSILON then return false end -- Return collision point and distance from ray origin return ray.position + ray.direction * t, t end function intersect.ray_capsule(ray, capsule) local dist2, p1, p2 = intersect.closest_point_segment_segment( ray.position, ray.position + ray.direction * 1e10, capsule.a, capsule.b ) if dist2 <= capsule.radius^2 then return p1 end return false end -- https://web.archive.org/web/20120414063459/http://local.wasp.uwa.edu.au/~pbourke//geometry/lineline3d/ -- a[1] is a vec3 -- a[2] is a vec3 -- b[1] is a vec3 -- b[2] is a vec3 -- e is a number function intersect.line_line(a, b, e) -- new points local p13 = a[1] - b[1] local p43 = b[2] - b[1] local p21 = a[2] - a[1] -- if lengths are negative or too close to 0, lines do not intersect if p43:len2() < DBL_EPSILON or p21:len2() < DBL_EPSILON then return false end -- dot products local d1343 = p13:dot(p43) local d4321 = p43:dot(p21) local d1321 = p13:dot(p21) local d4343 = p43:dot(p43) local d2121 = p21:dot(p21) local denom = d2121 * d4343 - d4321 * d4321 -- if denom is too close to 0, lines do not intersect if abs(denom) < DBL_EPSILON then return false end local numer = d1343 * d4321 - d1321 * d4343 local mua = numer / denom local mub = (d1343 + d4321 * mua) / d4343 -- return positions of intersection on each line local out1 = a[1] + p21 * mua local out2 = b[1] + p43 * mub local dist = out1:dist(out2) -- if distance of the shortest segment between lines is less than threshold if e and dist > e then return false end return { out1, out2 }, dist end -- a[1] is a vec3 -- a[2] is a vec3 -- b[1] is a vec3 -- b[2] is a vec3 -- e is a number function intersect.segment_segment(a, b, e) local c, d = intersect.line_line(a, b, e) if c and intersect.point_segment(c[1], a) and intersect.point_segment(c[2], a) and intersect.point_segment(c[1], b) and intersect.point_segment(c[2], b) then return c, d end -- segments do not intersect return false end -- a.min is a vec3 -- a.max is a vec3 -- b.min is a vec3 -- b.max is a vec3 function intersect.aabb_aabb(a, b) return a.min.x <= b.max.x and a.max.x >= b.min.x and a.min.y <= b.max.y and a.max.y >= b.min.y and a.min.z <= b.max.z and a.max.z >= b.min.z end -- aabb.position is a vec3 -- aabb.extent is a vec3 (half-size) -- obb.position is a vec3 -- obb.extent is a vec3 (half-size) -- obb.rotation is a mat4 function intersect.aabb_obb(aabb, obb) local a = aabb.extent local b = obb.extent local T = obb.position - aabb.position local rot = mat4():transpose(obb.rotation) local B = {} local t for i = 1, 3 do B[i] = {} for j = 1, 3 do assert((i - 1) * 4 + j < 16 and (i - 1) * 4 + j > 0) B[i][j] = abs(rot[(i - 1) * 4 + j]) + 1e-6 end end t = abs(T.x) if not (t <= (b.x + a.x * B[1][1] + b.y * B[1][2] + b.z * B[1][3])) then return false end t = abs(T.x * B[1][1] + T.y * B[2][1] + T.z * B[3][1]) if not (t <= (b.x + a.x * B[1][1] + a.y * B[2][1] + a.z * B[3][1])) then return false end t = abs(T.y) if not (t <= (a.y + b.x * B[2][1] + b.y * B[2][2] + b.z * B[2][3])) then return false end t = abs(T.z) if not (t <= (a.z + b.x * B[3][1] + b.y * B[3][2] + b.z * B[3][3])) then return false end t = abs(T.x * B[1][2] + T.y * B[2][2] + T.z * B[3][2]) if not (t <= (b.y + a.x * B[1][2] + a.y * B[2][2] + a.z * B[3][2])) then return false end t = abs(T.x * B[1][3] + T.y * B[2][3] + T.z * B[3][3]) if not (t <= (b.z + a.x * B[1][3] + a.y * B[2][3] + a.z * B[3][3])) then return false end t = abs(T.z * B[2][1] - T.y * B[3][1]) 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 t = abs(T.z * B[2][2] - T.y * B[3][2]) 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 t = abs(T.z * B[2][3] - T.y * B[3][3]) 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 t = abs(T.x * B[3][1] - T.z * B[1][1]) 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 t = abs(T.x * B[3][2] - T.z * B[1][2]) 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 t = abs(T.x * B[3][3] - T.z * B[1][3]) 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 t = abs(T.y * B[1][1] - T.x * B[2][1]) 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 t = abs(T.y * B[1][2] - T.x * B[2][2]) 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 t = abs(T.y * B[1][3] - T.x * B[2][3]) 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 -- https://gamedev.stackexchange.com/questions/24078/which-side-was-hit -- Minkowski Sum local wy = (aabb.extent * 2 + obb.extent * 2) * (aabb.position.y - obb.position.y) local hx = (aabb.extent * 2 + obb.extent * 2) * (aabb.position.x - obb.position.x) if wy.x > hx.x and wy.y > hx.y and wy.z > hx.z then if wy.x > -hx.x and wy.y > -hx.y and wy.z > -hx.z then return vec3(obb.rotation * { 0, -1, 0, 1 }) else return vec3(obb.rotation * { -1, 0, 0, 1 }) end else if wy.x > -hx.x and wy.y > -hx.y and wy.z > -hx.z then return vec3(obb.rotation * { 1, 0, 0, 1 }) else return vec3(obb.rotation * { 0, 1, 0, 1 }) end end end -- http://stackoverflow.com/a/4579069/1190664 -- aabb.min is a vec3 -- aabb.max is a vec3 -- sphere.position is a vec3 -- sphere.radius is a number local axes = { "x", "y", "z" } function intersect.aabb_sphere(aabb, sphere) local dist2 = sphere.radius ^ 2 for _, axis in ipairs(axes) do local pos = sphere.position[axis] local amin = aabb.min[axis] local amax = aabb.max[axis] if pos < amin then dist2 = dist2 - (pos - amin) ^ 2 elseif pos > amax then dist2 = dist2 - (pos - amax) ^ 2 end end return dist2 > 0 end -- aabb.min is a vec3 -- aabb.max is a vec3 -- frustum.left is a plane { a, b, c, d } -- frustum.right is a plane { a, b, c, d } -- frustum.bottom is a plane { a, b, c, d } -- frustum.top is a plane { a, b, c, d } -- frustum.near is a plane { a, b, c, d } -- frustum.far is a plane { a, b, c, d } function intersect.aabb_frustum(aabb, frustum) -- Indexed for the 'index trick' later local box = { aabb.min, aabb.max } -- We have 6 planes defining the frustum, 5 if infinite. local planes = { frustum.left, frustum.right, frustum.bottom, frustum.top, frustum.near, frustum.far or false } -- Skip the last test for infinite projections, it'll never fail. if not planes[6] then table.remove(planes) end for i = 1, #planes do -- This is the current plane local p = planes[i] -- p-vertex selection (with the index trick) -- According to the plane normal we can know the -- indices of the positive vertex local px = p.a > 0.0 and 2 or 1 local py = p.b > 0.0 and 2 or 1 local pz = p.c > 0.0 and 2 or 1 -- project p-vertex on plane normal -- (How far is p-vertex from the origin) local dot = (p.a * box[px].x) + (p.b * box[py].y) + (p.c * box[pz].z) -- Doesn't intersect if it is behind the plane if dot < -p.d then return false end end return true end -- outer.min is a vec3 -- outer.max is a vec3 -- inner.min is a vec3 -- inner.max is a vec3 function intersect.encapsulate_aabb(outer, inner) return outer.min.x <= inner.min.x and outer.max.x >= inner.max.x and outer.min.y <= inner.min.y and outer.max.y >= inner.max.y and outer.min.z <= inner.min.z and outer.max.z >= inner.max.z end -- a.position is a vec3 -- a.radius is a number -- b.position is a vec3 -- b.radius is a number function intersect.circle_circle(a, b) return a.position:dist(b.position) <= a.radius + b.radius end -- a.position is a vec3 -- a.radius is a number -- b.position is a vec3 -- b.radius is a number function intersect.sphere_sphere(a, b) return intersect.circle_circle(a, b) end -- http://realtimecollisiondetection.net/blog/?p=103 -- sphere.position is a vec3 -- sphere.radius is a number -- triangle[1] is a vec3 -- triangle[2] is a vec3 -- triangle[3] is a vec3 function intersect.sphere_triangle(sphere, triangle) -- Sphere is centered at origin local A = triangle[1] - sphere.position local B = triangle[2] - sphere.position local C = triangle[3] - sphere.position -- Compute normal of triangle plane local V = (B - A):cross(C - A) -- Test if sphere lies outside triangle plane local rr = sphere.radius * sphere.radius local d = A:dot(V) local e = V:dot(V) local s1 = d * d > rr * e -- Test if sphere lies outside triangle vertices local aa = A:dot(A) local ab = A:dot(B) local ac = A:dot(C) local bb = B:dot(B) local bc = B:dot(C) local cc = C:dot(C) local s2 = (aa > rr) and (ab > aa) and (ac > aa) local s3 = (bb > rr) and (ab > bb) and (bc > bb) local s4 = (cc > rr) and (ac > cc) and (bc > cc) -- Test is sphere lies outside triangle edges local AB = B - A local BC = C - B local CA = A - C local d1 = ab - aa local d2 = bc - bb local d3 = ac - cc local e1 = AB:dot(AB) local e2 = BC:dot(BC) local e3 = CA:dot(CA) local Q1 = A * e1 - AB * d1 local Q2 = B * e2 - BC * d2 local Q3 = C * e3 - CA * d3 local QC = C * e1 - Q1 local QA = A * e2 - Q2 local QB = B * e3 - Q3 local s5 = (Q1:dot(Q1) > rr * e1 * e1) and (Q1:dot(QC) > 0) local s6 = (Q2:dot(Q2) > rr * e2 * e2) and (Q2:dot(QA) > 0) local s7 = (Q3:dot(Q3) > rr * e3 * e3) and (Q3:dot(QB) > 0) -- Return whether or not any of the tests passed return s1 or s2 or s3 or s4 or s5 or s6 or s7 end -- sphere.position is a vec3 -- sphere.radius is a number -- frustum.left is a plane { a, b, c, d } -- frustum.right is a plane { a, b, c, d } -- frustum.bottom is a plane { a, b, c, d } -- frustum.top is a plane { a, b, c, d } -- frustum.near is a plane { a, b, c, d } -- frustum.far is a plane { a, b, c, d } function intersect.sphere_frustum(sphere, frustum) local x, y, z = sphere.position:unpack() local planes = { frustum.left, frustum.right, frustum.bottom, frustum.top, frustum.near } if frustum.far then table.insert(planes, frustum.far, 5) end local dot for i = 1, #planes do dot = planes[i].a * x + planes[i].b * y + planes[i].c * z + planes[i].d if dot <= -sphere.radius then return false end end -- dot + radius is the distance of the object from the near plane. -- make sure that the near plane is the last test! return dot + sphere.radius end function intersect.capsule_capsule(c1, c2) local dist2, p1, p2 = intersect.closest_point_segment_segment(c1.a, c1.b, c2.a, c2.b) local radius = c1.radius + c2.radius if dist2 <= radius * radius then return p1, p2 end return false end function intersect.closest_point_segment_segment(p1, p2, p3, p4) local s -- Distance of intersection along segment 1 local t -- Distance of intersection along segment 2 local c1 -- Collision point on segment 1 local c2 -- Collision point on segment 2 local d1 = p2 - p1 -- Direction of segment 1 local d2 = p4 - p3 -- Direction of segment 2 local r = p1 - p3 local a = d1:dot(d1) local e = d2:dot(d2) local f = d2:dot(r) -- Check if both segments degenerate into points if a <= DBL_EPSILON and e <= DBL_EPSILON then s = 0 t = 0 c1 = p1 c2 = p3 return (c1 - c2):dot(c1 - c2), s, t, c1, c2 end -- Check if segment 1 degenerates into a point if a <= DBL_EPSILON then s = 0 t = utils.clamp(f / e, 0, 1) else local c = d1:dot(r) -- Check is segment 2 degenerates into a point if e <= DBL_EPSILON then t = 0 s = utils.clamp(-c / a, 0, 1) else local b = d1:dot(d2) local denom = a * e - b * b if abs(denom) > 0 then s = utils.clamp((b * f - c * e) / denom, 0, 1) else s = 0 end t = (b * s + f) / e if t < 0 then t = 0 s = utils.clamp(-c / a, 0, 1) elseif t > 1 then t = 1 s = utils.clamp((b - c) / a, 0, 1) end end end c1 = p1 + d1 * s c2 = p3 + d2 * t return (c1 - c2):dot(c1 - c2), c1, c2, s, t end return intersect ================================================ FILE: modules/mat4.lua ================================================ --- double 4x4, 1-based, column major matrices -- @module mat4 local modules = (...):gsub('%.[^%.]+$', '') .. "." local constants = require(modules .. "constants") local vec2 = require(modules .. "vec2") local vec3 = require(modules .. "vec3") local quat = require(modules .. "quat") local utils = require(modules .. "utils") local precond = require(modules .. "_private_precond") local private = require(modules .. "_private_utils") local sqrt = math.sqrt local cos = math.cos local sin = math.sin local tan = math.tan local rad = math.rad local mat4 = {} local mat4_mt = {} -- Private constructor. local function new(m) m = m or { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } m._m = m return setmetatable(m, mat4_mt) end -- Convert matrix into identity local function identity(m) m[1], m[2], m[3], m[4] = 1, 0, 0, 0 m[5], m[6], m[7], m[8] = 0, 1, 0, 0 m[9], m[10], m[11], m[12] = 0, 0, 1, 0 m[13], m[14], m[15], m[16] = 0, 0, 0, 1 return m end -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. local status, ffi if type(jit) == "table" and jit.status() then -- status, ffi = pcall(require, "ffi") if status then ffi.cdef "typedef struct { double _m[16]; } cpml_mat4;" new = ffi.typeof("cpml_mat4") end end -- Statically allocate a temporary variable used in some of our functions. local tmp = new() local tm4 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } local tv4 = { 0, 0, 0, 0 } --- The public constructor. -- @param a Can be of four types:
-- table Length 16 (4x4 matrix) -- table Length 9 (3x3 matrix) -- table Length 4 (4 vec4s) -- nil -- @treturn mat4 out function mat4.new(a) local out = new() -- 4x4 matrix if type(a) == "table" and #a == 16 then for i = 1, 16 do out[i] = tonumber(a[i]) end -- 3x3 matrix elseif type(a) == "table" and #a == 9 then out[1], out[2], out[3] = a[1], a[2], a[3] out[5], out[6], out[7] = a[4], a[5], a[6] out[9], out[10], out[11] = a[7], a[8], a[9] out[16] = 1 -- 4 vec4s elseif type(a) == "table" and type(a[1]) == "table" then local idx = 1 for i = 1, 4 do for j = 1, 4 do out[idx] = a[i][j] idx = idx + 1 end end -- nil else out[1] = 1 out[6] = 1 out[11] = 1 out[16] = 1 end return out end --- Create an identity matrix. -- @tparam mat4 a Matrix to overwrite -- @treturn mat4 out function mat4.identity(a) return identity(a or new()) end --- Create a matrix from an angle/axis pair. -- @tparam number angle Angle of rotation -- @tparam vec3 axis Axis of rotation -- @treturn mat4 out function mat4.from_angle_axis(angle, axis) local l = axis:len() if l == 0 then return new() end local x, y, z = axis.x / l, axis.y / l, axis.z / l local c = cos(angle) local s = sin(angle) return new { x*x*(1-c)+c, y*x*(1-c)+z*s, x*z*(1-c)-y*s, 0, x*y*(1-c)-z*s, y*y*(1-c)+c, y*z*(1-c)+x*s, 0, x*z*(1-c)+y*s, y*z*(1-c)-x*s, z*z*(1-c)+c, 0, 0, 0, 0, 1 } end --- Create a matrix from a quaternion. -- @tparam quat q Rotation quaternion -- @treturn mat4 out function mat4.from_quaternion(q) return mat4.from_angle_axis(q:to_angle_axis()) end --- Create a matrix from a direction/up pair. -- @tparam vec3 direction Vector direction -- @tparam vec3 up Up direction -- @treturn mat4 out function mat4.from_direction(direction, up) local forward = vec3.normalize(direction) local side = vec3.cross(forward, up):normalize() local new_up = vec3.cross(side, forward):normalize() local out = new() out[1] = side.x out[5] = side.y out[9] = side.z out[2] = new_up.x out[6] = new_up.y out[10] = new_up.z out[3] = forward.x out[7] = forward.y out[11] = forward.z out[16] = 1 return out end --- Create a matrix from a transform. -- @tparam vec3 trans Translation vector -- @tparam quat rot Rotation quaternion -- @tparam vec3 scale Scale vector -- @treturn mat4 out function mat4.from_transform(trans, rot, scale) local rx, ry, rz, rw = rot.x, rot.y, rot.z, rot.w local sm = new { scale.x, 0, 0, 0, 0, scale.y, 0, 0, 0, 0, scale.z, 0, 0, 0, 0, 1, } local rm = new { 1-2*(ry*ry+rz*rz), 2*(rx*ry-rz*rw), 2*(rx*rz+ry*rw), 0, 2*(rx*ry+rz*rw), 1-2*(rx*rx+rz*rz), 2*(ry*rz-rx*rw), 0, 2*(rx*rz-ry*rw), 2*(ry*rz+rx*rw), 1-2*(rx*rx+ry*ry), 0, 0, 0, 0, 1 } local rsm = rm * sm rsm[13] = trans.x rsm[14] = trans.y rsm[15] = trans.z return rsm end --- Create matrix from orthogonal. -- @tparam number left -- @tparam number right -- @tparam number top -- @tparam number bottom -- @tparam number near -- @tparam number far -- @treturn mat4 out function mat4.from_ortho(left, right, top, bottom, near, far) local out = new() out[1] = 2 / (right - left) out[6] = 2 / (top - bottom) out[11] = -2 / (far - near) out[13] = -((right + left) / (right - left)) out[14] = -((top + bottom) / (top - bottom)) out[15] = -((far + near) / (far - near)) out[16] = 1 return out end --- Create matrix from perspective. -- @tparam number fovy Field of view -- @tparam number aspect Aspect ratio -- @tparam number near Near plane -- @tparam number far Far plane -- @treturn mat4 out function mat4.from_perspective(fovy, aspect, near, far) assert(aspect ~= 0) assert(near ~= far) local t = tan(rad(fovy) / 2) local out = new() out[1] = 1 / (t * aspect) out[6] = 1 / t out[11] = -(far + near) / (far - near) out[12] = -1 out[15] = -(2 * far * near) / (far - near) out[16] = 0 return out end -- Adapted from the Oculus SDK. --- Create matrix from HMD perspective. -- @tparam number tanHalfFov Tangent of half of the field of view -- @tparam number zNear Near plane -- @tparam number zFar Far plane -- @tparam boolean flipZ Z axis is flipped or not -- @tparam boolean farAtInfinity Far plane is infinite or not -- @treturn mat4 out function mat4.from_hmd_perspective(tanHalfFov, zNear, zFar, flipZ, farAtInfinity) -- CPML is right-handed and intended for GL, so these don't need to be arguments. local rightHanded = true local isOpenGL = true local function CreateNDCScaleAndOffsetFromFov(tanHalfFov) local x_scale = 2 / (tanHalfFov.LeftTan + tanHalfFov.RightTan) local x_offset = (tanHalfFov.LeftTan - tanHalfFov.RightTan) * x_scale * 0.5 local y_scale = 2 / (tanHalfFov.UpTan + tanHalfFov.DownTan ) local y_offset = (tanHalfFov.UpTan - tanHalfFov.DownTan ) * y_scale * 0.5 local result = { Scale = vec2(x_scale, y_scale), Offset = vec2(x_offset, y_offset) } -- Hey - why is that Y.Offset negated? -- It's because a projection matrix transforms from world coords with Y=up, -- whereas this is from NDC which is Y=down. return result end if not flipZ and farAtInfinity then print("Error: Cannot push Far Clip to Infinity when Z-order is not flipped") farAtInfinity = false end -- A projection matrix is very like a scaling from NDC, so we can start with that. local scaleAndOffset = CreateNDCScaleAndOffsetFromFov(tanHalfFov) local handednessScale = rightHanded and -1.0 or 1.0 local projection = new() -- Produces X result, mapping clip edges to [-w,+w] projection[1] = scaleAndOffset.Scale.x projection[2] = 0 projection[3] = handednessScale * scaleAndOffset.Offset.x projection[4] = 0 -- Produces Y result, mapping clip edges to [-w,+w] -- Hey - why is that YOffset negated? -- It's because a projection matrix transforms from world coords with Y=up, -- whereas this is derived from an NDC scaling, which is Y=down. projection[5] = 0 projection[6] = scaleAndOffset.Scale.y projection[7] = handednessScale * -scaleAndOffset.Offset.y projection[8] = 0 -- Produces Z-buffer result - app needs to fill this in with whatever Z range it wants. -- We'll just use some defaults for now. projection[9] = 0 projection[10] = 0 if farAtInfinity then if isOpenGL then -- It's not clear this makes sense for OpenGL - you don't get the same precision benefits you do in D3D. projection[11] = -handednessScale projection[12] = 2.0 * zNear else projection[11] = 0 projection[12] = zNear end else if isOpenGL then -- Clip range is [-w,+w], so 0 is at the middle of the range. projection[11] = -handednessScale * (flipZ and -1.0 or 1.0) * (zNear + zFar) / (zNear - zFar) projection[12] = 2.0 * ((flipZ and -zFar or zFar) * zNear) / (zNear - zFar) else -- Clip range is [0,+w], so 0 is at the start of the range. projection[11] = -handednessScale * (flipZ and -zNear or zFar) / (zNear - zFar) projection[12] = ((flipZ and -zFar or zFar) * zNear) / (zNear - zFar) end end -- Produces W result (= Z in) projection[13] = 0 projection[14] = 0 projection[15] = handednessScale projection[16] = 0 return projection:transpose(projection) end --- Clone a matrix. -- @tparam mat4 a Matrix to clone -- @treturn mat4 out function mat4.clone(a) return new(a) end function mul_internal(out, a, b) tm4[1] = b[1] * a[1] + b[2] * a[5] + b[3] * a[9] + b[4] * a[13] tm4[2] = b[1] * a[2] + b[2] * a[6] + b[3] * a[10] + b[4] * a[14] tm4[3] = b[1] * a[3] + b[2] * a[7] + b[3] * a[11] + b[4] * a[15] tm4[4] = b[1] * a[4] + b[2] * a[8] + b[3] * a[12] + b[4] * a[16] tm4[5] = b[5] * a[1] + b[6] * a[5] + b[7] * a[9] + b[8] * a[13] tm4[6] = b[5] * a[2] + b[6] * a[6] + b[7] * a[10] + b[8] * a[14] tm4[7] = b[5] * a[3] + b[6] * a[7] + b[7] * a[11] + b[8] * a[15] tm4[8] = b[5] * a[4] + b[6] * a[8] + b[7] * a[12] + b[8] * a[16] tm4[9] = b[9] * a[1] + b[10] * a[5] + b[11] * a[9] + b[12] * a[13] tm4[10] = b[9] * a[2] + b[10] * a[6] + b[11] * a[10] + b[12] * a[14] tm4[11] = b[9] * a[3] + b[10] * a[7] + b[11] * a[11] + b[12] * a[15] tm4[12] = b[9] * a[4] + b[10] * a[8] + b[11] * a[12] + b[12] * a[16] tm4[13] = b[13] * a[1] + b[14] * a[5] + b[15] * a[9] + b[16] * a[13] tm4[14] = b[13] * a[2] + b[14] * a[6] + b[15] * a[10] + b[16] * a[14] tm4[15] = b[13] * a[3] + b[14] * a[7] + b[15] * a[11] + b[16] * a[15] tm4[16] = b[13] * a[4] + b[14] * a[8] + b[15] * a[12] + b[16] * a[16] for i = 1, 16 do out[i] = tm4[i] end end --- Multiply N matrices. -- @tparam mat4 out Matrix to store the result -- @tparam mat4 or {mat4, ...} left hand operand(s) -- @tparam mat4 right hand operand if a is not table -- @treturn mat4 out multiplied matrix result function mat4.mul(out, a, b) if mat4.is_mat4(a) then mul_internal(out, a, b) return out end if #a == 0 then identity(out) elseif #a == 1 then -- only one matrix, just copy for i = 1, 16 do out[i] = a[1][i] end else local ma = a[1] local mb = a[2] for i = 2, #a do mul_internal(out, ma, mb) ma = out end end return out end --- Multiply a matrix and a vec3, with perspective division. -- This function uses an implicit 1 for the fourth component. -- @tparam vec3 out vec3 to store the result -- @tparam mat4 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3 out function mat4.mul_vec3_perspective(out, a, b) local v4x = b.x * a[1] + b.y * a[5] + b.z * a[9] + a[13] local v4y = b.x * a[2] + b.y * a[6] + b.z * a[10] + a[14] local v4z = b.x * a[3] + b.y * a[7] + b.z * a[11] + a[15] local v4w = b.x * a[4] + b.y * a[8] + b.z * a[12] + a[16] local inv_w = 0 if v4w ~= 0 then inv_w = utils.sign(v4w) / v4w end out.x = v4x * inv_w out.y = v4y * inv_w out.z = v4z * inv_w return out end --- Multiply a matrix and a vec4. -- @tparam table out table to store the result -- @tparam mat4 a Left hand operand -- @tparam table b Right hand operand -- @treturn vec4 out function mat4.mul_vec4(out, a, b) tv4[1] = b[1] * a[1] + b[2] * a[5] + b [3] * a[9] + b[4] * a[13] tv4[2] = b[1] * a[2] + b[2] * a[6] + b [3] * a[10] + b[4] * a[14] tv4[3] = b[1] * a[3] + b[2] * a[7] + b [3] * a[11] + b[4] * a[15] tv4[4] = b[1] * a[4] + b[2] * a[8] + b [3] * a[12] + b[4] * a[16] for i = 1, 4 do out[i] = tv4[i] end return out end --- Invert a matrix. -- @tparam mat4 out Matrix to store the result -- @tparam mat4 a Matrix to invert -- @treturn mat4 out function mat4.invert(out, a) tm4[1] = a[6] * a[11] * a[16] - a[6] * a[12] * a[15] - a[10] * a[7] * a[16] + a[10] * a[8] * a[15] + a[14] * a[7] * a[12] - a[14] * a[8] * a[11] tm4[2] = -a[2] * a[11] * a[16] + a[2] * a[12] * a[15] + a[10] * a[3] * a[16] - a[10] * a[4] * a[15] - a[14] * a[3] * a[12] + a[14] * a[4] * a[11] tm4[3] = a[2] * a[7] * a[16] - a[2] * a[8] * a[15] - a[6] * a[3] * a[16] + a[6] * a[4] * a[15] + a[14] * a[3] * a[8] - a[14] * a[4] * a[7] tm4[4] = -a[2] * a[7] * a[12] + a[2] * a[8] * a[11] + a[6] * a[3] * a[12] - a[6] * a[4] * a[11] - a[10] * a[3] * a[8] + a[10] * a[4] * a[7] tm4[5] = -a[5] * a[11] * a[16] + a[5] * a[12] * a[15] + a[9] * a[7] * a[16] - a[9] * a[8] * a[15] - a[13] * a[7] * a[12] + a[13] * a[8] * a[11] tm4[6] = a[1] * a[11] * a[16] - a[1] * a[12] * a[15] - a[9] * a[3] * a[16] + a[9] * a[4] * a[15] + a[13] * a[3] * a[12] - a[13] * a[4] * a[11] tm4[7] = -a[1] * a[7] * a[16] + a[1] * a[8] * a[15] + a[5] * a[3] * a[16] - a[5] * a[4] * a[15] - a[13] * a[3] * a[8] + a[13] * a[4] * a[7] tm4[8] = a[1] * a[7] * a[12] - a[1] * a[8] * a[11] - a[5] * a[3] * a[12] + a[5] * a[4] * a[11] + a[9] * a[3] * a[8] - a[9] * a[4] * a[7] tm4[9] = a[5] * a[10] * a[16] - a[5] * a[12] * a[14] - a[9] * a[6] * a[16] + a[9] * a[8] * a[14] + a[13] * a[6] * a[12] - a[13] * a[8] * a[10] tm4[10] = -a[1] * a[10] * a[16] + a[1] * a[12] * a[14] + a[9] * a[2] * a[16] - a[9] * a[4] * a[14] - a[13] * a[2] * a[12] + a[13] * a[4] * a[10] tm4[11] = a[1] * a[6] * a[16] - a[1] * a[8] * a[14] - a[5] * a[2] * a[16] + a[5] * a[4] * a[14] + a[13] * a[2] * a[8] - a[13] * a[4] * a[6] tm4[12] = -a[1] * a[6] * a[12] + a[1] * a[8] * a[10] + a[5] * a[2] * a[12] - a[5] * a[4] * a[10] - a[9] * a[2] * a[8] + a[9] * a[4] * a[6] tm4[13] = -a[5] * a[10] * a[15] + a[5] * a[11] * a[14] + a[9] * a[6] * a[15] - a[9] * a[7] * a[14] - a[13] * a[6] * a[11] + a[13] * a[7] * a[10] tm4[14] = a[1] * a[10] * a[15] - a[1] * a[11] * a[14] - a[9] * a[2] * a[15] + a[9] * a[3] * a[14] + a[13] * a[2] * a[11] - a[13] * a[3] * a[10] tm4[15] = -a[1] * a[6] * a[15] + a[1] * a[7] * a[14] + a[5] * a[2] * a[15] - a[5] * a[3] * a[14] - a[13] * a[2] * a[7] + a[13] * a[3] * a[6] tm4[16] = a[1] * a[6] * a[11] - a[1] * a[7] * a[10] - a[5] * a[2] * a[11] + a[5] * a[3] * a[10] + a[9] * a[2] * a[7] - a[9] * a[3] * a[6] local det = a[1] * tm4[1] + a[2] * tm4[5] + a[3] * tm4[9] + a[4] * tm4[13] if det == 0 then return a end det = 1 / det for i = 1, 16 do out[i] = tm4[i] * det end return out end --- Scale a matrix. -- @tparam mat4 out Matrix to store the result -- @tparam mat4 a Matrix to scale -- @tparam vec3 s Scalar -- @treturn mat4 out function mat4.scale(out, a, s) identity(tmp) tmp[1] = s.x tmp[6] = s.y tmp[11] = s.z return out:mul(tmp, a) end --- Rotate a matrix. -- @tparam mat4 out Matrix to store the result -- @tparam mat4 a Matrix to rotate -- @tparam number angle Angle to rotate by (in radians) -- @tparam vec3 axis Axis to rotate on -- @treturn mat4 out function mat4.rotate(out, a, angle, axis) if type(angle) == "table" or type(angle) == "cdata" then angle, axis = angle:to_angle_axis() end local l = axis:len() if l == 0 then return a end local x, y, z = axis.x / l, axis.y / l, axis.z / l local c = cos(angle) local s = sin(angle) identity(tmp) tmp[1] = x * x * (1 - c) + c tmp[2] = y * x * (1 - c) + z * s tmp[3] = x * z * (1 - c) - y * s tmp[5] = x * y * (1 - c) - z * s tmp[6] = y * y * (1 - c) + c tmp[7] = y * z * (1 - c) + x * s tmp[9] = x * z * (1 - c) + y * s tmp[10] = y * z * (1 - c) - x * s tmp[11] = z * z * (1 - c) + c return out:mul(tmp, a) end --- Translate a matrix. -- @tparam mat4 out Matrix to store the result -- @tparam mat4 a Matrix to translate -- @tparam vec3 t Translation vector -- @treturn mat4 out function mat4.translate(out, a, t) identity(tmp) tmp[13] = t.x tmp[14] = t.y tmp[15] = t.z return out:mul(tmp, a) end --- Shear a matrix. -- @tparam mat4 out Matrix to store the result -- @tparam mat4 a Matrix to translate -- @tparam number yx -- @tparam number zx -- @tparam number xy -- @tparam number zy -- @tparam number xz -- @tparam number yz -- @treturn mat4 out function mat4.shear(out, a, yx, zx, xy, zy, xz, yz) identity(tmp) tmp[2] = yx or 0 tmp[3] = zx or 0 tmp[5] = xy or 0 tmp[7] = zy or 0 tmp[9] = xz or 0 tmp[10] = yz or 0 return out:mul(tmp, a) end --- Reflect a matrix across a plane. -- @tparam mat4 Matrix to store the result -- @tparam a Matrix to reflect -- @tparam vec3 position A point on the plane -- @tparam vec3 normal The (normalized!) normal vector of the plane function mat4.reflect(out, a, position, normal) local nx, ny, nz = normal:unpack() local d = -position:dot(normal) tmp[1] = 1 - 2 * nx ^ 2 tmp[2] = 2 * nx * ny tmp[3] = -2 * nx * nz tmp[4] = 0 tmp[5] = -2 * nx * ny tmp[6] = 1 - 2 * ny ^ 2 tmp[7] = -2 * ny * nz tmp[8] = 0 tmp[9] = -2 * nx * nz tmp[10] = -2 * ny * nz tmp[11] = 1 - 2 * nz ^ 2 tmp[12] = 0 tmp[13] = -2 * nx * d tmp[14] = -2 * ny * d tmp[15] = -2 * nz * d tmp[16] = 1 return out:mul(tmp, a) end --- Transform matrix to look at a point. -- @tparam mat4 out Matrix to store result -- @tparam vec3 eye Location of viewer's view plane -- @tparam vec3 center Location of object to view -- @tparam vec3 up Up direction -- @treturn mat4 out function mat4.look_at(out, eye, look_at, up) local z_axis = (eye - look_at):normalize() local x_axis = up:cross(z_axis):normalize() local y_axis = z_axis:cross(x_axis) out[1] = x_axis.x out[2] = y_axis.x out[3] = z_axis.x out[4] = 0 out[5] = x_axis.y out[6] = y_axis.y out[7] = z_axis.y out[8] = 0 out[9] = x_axis.z out[10] = y_axis.z out[11] = z_axis.z out[12] = 0 out[13] = -out[ 1]*eye.x - out[4+1]*eye.y - out[8+1]*eye.z out[14] = -out[ 2]*eye.x - out[4+2]*eye.y - out[8+2]*eye.z out[15] = -out[ 3]*eye.x - out[4+3]*eye.y - out[8+3]*eye.z out[16] = -out[ 4]*eye.x - out[4+4]*eye.y - out[8+4]*eye.z + 1 return out end --- Transform matrix to target a point. -- @tparam mat4 out Matrix to store result -- @tparam vec3 eye Location of viewer's view plane -- @tparam vec3 center Location of object to view -- @tparam vec3 up Up direction -- @treturn mat4 out function mat4.target(out, from, to, up) local z_axis = (from - to):normalize() local x_axis = up:cross(z_axis):normalize() local y_axis = z_axis:cross(x_axis) out[1] = x_axis.x out[2] = x_axis.y out[3] = x_axis.z out[4] = 0 out[5] = y_axis.x out[6] = y_axis.y out[7] = y_axis.z out[8] = 0 out[9] = z_axis.x out[10] = z_axis.y out[11] = z_axis.z out[12] = 0 out[13] = from.x out[14] = from.y out[15] = from.z out[16] = 1 return out end --- Transpose a matrix. -- @tparam mat4 out Matrix to store the result -- @tparam mat4 a Matrix to transpose -- @treturn mat4 out function mat4.transpose(out, a) tm4[1] = a[1] tm4[2] = a[5] tm4[3] = a[9] tm4[4] = a[13] tm4[5] = a[2] tm4[6] = a[6] tm4[7] = a[10] tm4[8] = a[14] tm4[9] = a[3] tm4[10] = a[7] tm4[11] = a[11] tm4[12] = a[15] tm4[13] = a[4] tm4[14] = a[8] tm4[15] = a[12] tm4[16] = a[16] for i = 1, 16 do out[i] = tm4[i] end return out end --- Project a point into screen space -- @tparam vec3 obj Object position in world space -- @tparam mat4 mvp Projection matrix -- @tparam table viewport XYWH of viewport -- @treturn vec3 win function mat4.project(obj, mvp, viewport) local point = mat4.mul_vec3_perspective(vec3(), mvp, obj) point.x = point.x * 0.5 + 0.5 point.y = point.y * 0.5 + 0.5 point.z = point.z * 0.5 + 0.5 point.x = point.x * viewport[3] + viewport[1] point.y = point.y * viewport[4] + viewport[2] return point end --- Unproject a point from screen space to world space. -- @tparam vec3 win Object position in screen space -- @tparam mat4 mvp Projection matrix -- @tparam table viewport XYWH of viewport -- @treturn vec3 obj function mat4.unproject(win, mvp, viewport) local point = vec3.clone(win) -- 0..n -> 0..1 point.x = (point.x - viewport[1]) / viewport[3] point.y = (point.y - viewport[2]) / viewport[4] -- 0..1 -> -1..1 point.x = point.x * 2 - 1 point.y = point.y * 2 - 1 point.z = point.z * 2 - 1 return mat4.mul_vec3_perspective(point, tmp:invert(mvp), point) end --- Return a boolean showing if a table is or is not a mat4. -- @tparam mat4 a Matrix to be tested -- @treturn boolean is_mat4 function mat4.is_mat4(a) if type(a) == "cdata" then return ffi.istype("cpml_mat4", a) end if type(a) ~= "table" then return false end for i = 1, 16 do if type(a[i]) ~= "number" then return false end end return true end --- Return whether any component is NaN -- @tparam mat4 a Matrix to be tested -- @treturn boolean if any component is NaN function vec2.has_nan(a) for i = 1, 16 do if private.is_nan(a[i]) then return true end end return false end --- Return a formatted string. -- @tparam mat4 a Matrix to be turned into a string -- @treturn string formatted function mat4.to_string(a) local str = "[ " for i = 1, 16 do str = str .. string.format("%+0.3f", a[i]) if i < 16 then str = str .. ", " end end str = str .. " ]" return str end --- Convert a matrix to row vec4s. -- @tparam mat4 a Matrix to be converted -- @treturn table vec4s function mat4.to_vec4s(a) return { { a[1], a[2], a[3], a[4] }, { a[5], a[6], a[7], a[8] }, { a[9], a[10], a[11], a[12] }, { a[13], a[14], a[15], a[16] } } end --- Convert a matrix to col vec4s. -- @tparam mat4 a Matrix to be converted -- @treturn table vec4s function mat4.to_vec4s_cols(a) return { { a[1], a[5], a[9], a[13] }, { a[2], a[6], a[10], a[14] }, { a[3], a[7], a[11], a[15] }, { a[4], a[8], a[12], a[16] } } end -- http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/ --- Convert a matrix to a quaternion. -- @tparam mat4 a Matrix to be converted -- @treturn quat out function mat4.to_quat(a) identity(tmp):transpose(a) local w = sqrt(1 + tmp[1] + tmp[6] + tmp[11]) / 2 local scale = w * 4 local q = quat.new( tmp[10] - tmp[7] / scale, tmp[3] - tmp[9] / scale, tmp[5] - tmp[2] / scale, w ) return q:normalize(q) end -- http://www.crownandcutlass.com/features/technicaldetails/frustum.html --- Convert a matrix to a frustum. -- @tparam mat4 a Matrix to be converted (projection * view) -- @tparam boolean infinite Infinite removes the far plane -- @treturn frustum out function mat4.to_frustum(a, infinite) local t local frustum = {} -- Extract the LEFT plane frustum.left = {} frustum.left.a = a[4] + a[1] frustum.left.b = a[8] + a[5] frustum.left.c = a[12] + a[9] frustum.left.d = a[16] + a[13] -- Normalize the result t = sqrt(frustum.left.a * frustum.left.a + frustum.left.b * frustum.left.b + frustum.left.c * frustum.left.c) frustum.left.a = frustum.left.a / t frustum.left.b = frustum.left.b / t frustum.left.c = frustum.left.c / t frustum.left.d = frustum.left.d / t -- Extract the RIGHT plane frustum.right = {} frustum.right.a = a[4] - a[1] frustum.right.b = a[8] - a[5] frustum.right.c = a[12] - a[9] frustum.right.d = a[16] - a[13] -- Normalize the result t = sqrt(frustum.right.a * frustum.right.a + frustum.right.b * frustum.right.b + frustum.right.c * frustum.right.c) frustum.right.a = frustum.right.a / t frustum.right.b = frustum.right.b / t frustum.right.c = frustum.right.c / t frustum.right.d = frustum.right.d / t -- Extract the BOTTOM plane frustum.bottom = {} frustum.bottom.a = a[4] + a[2] frustum.bottom.b = a[8] + a[6] frustum.bottom.c = a[12] + a[10] frustum.bottom.d = a[16] + a[14] -- Normalize the result t = sqrt(frustum.bottom.a * frustum.bottom.a + frustum.bottom.b * frustum.bottom.b + frustum.bottom.c * frustum.bottom.c) frustum.bottom.a = frustum.bottom.a / t frustum.bottom.b = frustum.bottom.b / t frustum.bottom.c = frustum.bottom.c / t frustum.bottom.d = frustum.bottom.d / t -- Extract the TOP plane frustum.top = {} frustum.top.a = a[4] - a[2] frustum.top.b = a[8] - a[6] frustum.top.c = a[12] - a[10] frustum.top.d = a[16] - a[14] -- Normalize the result t = sqrt(frustum.top.a * frustum.top.a + frustum.top.b * frustum.top.b + frustum.top.c * frustum.top.c) frustum.top.a = frustum.top.a / t frustum.top.b = frustum.top.b / t frustum.top.c = frustum.top.c / t frustum.top.d = frustum.top.d / t -- Extract the NEAR plane frustum.near = {} frustum.near.a = a[4] + a[3] frustum.near.b = a[8] + a[7] frustum.near.c = a[12] + a[11] frustum.near.d = a[16] + a[15] -- Normalize the result t = sqrt(frustum.near.a * frustum.near.a + frustum.near.b * frustum.near.b + frustum.near.c * frustum.near.c) frustum.near.a = frustum.near.a / t frustum.near.b = frustum.near.b / t frustum.near.c = frustum.near.c / t frustum.near.d = frustum.near.d / t if not infinite then -- Extract the FAR plane frustum.far = {} frustum.far.a = a[4] - a[3] frustum.far.b = a[8] - a[7] frustum.far.c = a[12] - a[11] frustum.far.d = a[16] - a[15] -- Normalize the result t = sqrt(frustum.far.a * frustum.far.a + frustum.far.b * frustum.far.b + frustum.far.c * frustum.far.c) frustum.far.a = frustum.far.a / t frustum.far.b = frustum.far.b / t frustum.far.c = frustum.far.c / t frustum.far.d = frustum.far.d / t end return frustum end function mat4_mt.__index(t, k) if type(t) == "cdata" then if type(k) == "number" then return t._m[k-1] end end return rawget(mat4, k) end function mat4_mt.__newindex(t, k, v) if type(t) == "cdata" then if type(k) == "number" then t._m[k-1] = v end end end mat4_mt.__tostring = mat4.to_string function mat4_mt.__call(_, a) return mat4.new(a) end function mat4_mt.__unm(a) return new():invert(a) end function mat4_mt.__eq(a, b) if not mat4.is_mat4(a) or not mat4.is_mat4(b) then return false end for i = 1, 16 do if not utils.tolerance(b[i]-a[i], constants.FLT_EPSILON) then return false end end return true end function mat4_mt.__mul(a, b) precond.assert(mat4.is_mat4(a), "__mul: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) if vec3.is_vec3(b) then return mat4.mul_vec3_perspective(vec3(), a, b) end assert(mat4.is_mat4(b) or #b == 4, "__mul: Wrong argument type for right hand operand. ( or table #4 expected)") if mat4.is_mat4(b) then return new():mul(a, b) end return mat4.mul_vec4({}, a, b) end if status then xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded ffi.metatype(new, mat4_mt) end, function() end) end return setmetatable({}, mat4_mt) ================================================ FILE: modules/mesh.lua ================================================ --- Mesh utilities -- @module mesh local modules = (...):gsub('%.[^%.]+$', '') .. "." local vec3 = require(modules .. "vec3") local mesh = {} -- vertices is an arbitrary list of vec3s function mesh.average(vertices) local out = vec3() for _, v in ipairs(vertices) do out = out + v end return out / #vertices end -- triangle[1] is a vec3 -- triangle[2] is a vec3 -- triangle[3] is a vec3 function mesh.normal(triangle) local ba = triangle[2] - triangle[1] local ca = triangle[3] - triangle[1] return ba:cross(ca):normalize() end -- triangle[1] is a vec3 -- triangle[2] is a vec3 -- triangle[3] is a vec3 function mesh.plane_from_triangle(triangle) return { origin = triangle[1], normal = mesh.normal(triangle) } end -- plane.origin is a vec3 -- plane.normal is a vec3 -- direction is a vec3 function mesh.is_front_facing(plane, direction) return plane.normal:dot(direction) >= 0 end -- point is a vec3 -- plane.origin is a vec3 -- plane.normal is a vec3 -- plane.dot is a number function mesh.signed_distance(point, plane) return point:dot(plane.normal) - plane.normal:dot(plane.origin) end return mesh ================================================ FILE: modules/octree.lua ================================================ -- https://github.com/Nition/UnityOctree -- https://github.com/Nition/UnityOctree/blob/master/LICENCE -- https://github.com/Nition/UnityOctree/blob/master/Scripts/BoundsOctree.cs -- https://github.com/Nition/UnityOctree/blob/master/Scripts/BoundsOctreeNode.cs --- Octree -- @module octree local modules = (...):gsub('%.[^%.]+$', '') .. "." local intersect = require(modules .. "intersect") local mat4 = require(modules .. "mat4") local utils = require(modules .. "utils") local vec3 = require(modules .. "vec3") local Octree = {} local OctreeNode = {} local Node Octree.__index = Octree OctreeNode.__index = OctreeNode --== Octree ==-- --- Constructor for the bounds octree. -- @param initialWorldSize Size of the sides of the initial node, in metres. The octree will never shrink smaller than this -- @param initialWorldPos Position of the centre of the initial node -- @param minNodeSize Nodes will stop splitting if the new nodes would be smaller than this (metres) -- @param looseness Clamped between 1 and 2. Values > 1 let nodes overlap local function new(initialWorldSize, initialWorldPos, minNodeSize, looseness) local tree = setmetatable({}, Octree) if minNodeSize > initialWorldSize then print("Minimum node size must be at least as big as the initial world size. Was: " .. minNodeSize .. " Adjusted to: " .. initialWorldSize) minNodeSize = initialWorldSize end -- The total amount of objects currently in the tree tree.count = 0 -- Size that the octree was on creation tree.initialSize = initialWorldSize -- Minimum side length that a node can be - essentially an alternative to having a max depth tree.minSize = minNodeSize -- Should be a value between 1 and 2. A multiplier for the base size of a node. -- 1.0 is a "normal" octree, while values > 1 have overlap tree.looseness = utils.clamp(looseness, 1, 2) -- Root node of the octree tree.rootNode = Node(tree.initialSize, tree.minSize, tree.looseness, initialWorldPos) return tree end --- Used when growing the octree. Works out where the old root node would fit inside a new, larger root node. -- @param xDir X direction of growth. 1 or -1 -- @param yDir Y direction of growth. 1 or -1 -- @param zDir Z direction of growth. 1 or -1 -- @return Octant where the root node should be local function get_root_pos_index(xDir, yDir, zDir) local result = xDir > 0 and 1 or 0 if yDir < 0 then return result + 4 end if zDir > 0 then return result + 2 end end --- Add an object. -- @param obj Object to add -- @param objBounds 3D bounding box around the object function Octree:add(obj, objBounds) -- Add object or expand the octree until it can be added local count = 0 -- Safety check against infinite/excessive growth while not self.rootNode:add(obj, objBounds) do count = count + 1 self:grow(objBounds.center - self.rootNode.center) if count > 20 then print("Aborted Add operation as it seemed to be going on forever (" .. count - 1 .. ") attempts at growing the octree.") return end self.count = self.count + 1 end end --- Remove an object. Makes the assumption that the object only exists once in the tree. -- @param obj Object to remove -- @return bool True if the object was removed successfully function Octree:remove(obj) local removed = self.rootNode:remove(obj) -- See if we can shrink the octree down now that we've removed the item if removed then self.count = self.count - 1 self:shrink() end return removed end --- Check if the specified bounds intersect with anything in the tree. See also: get_colliding. -- @param checkBounds bounds to check -- @return bool True if there was a collision function Octree:is_colliding(checkBounds) return self.rootNode:is_colliding(checkBounds) end --- Returns an array of objects that intersect with the specified bounds, if any. Otherwise returns an empty array. See also: is_colliding. -- @param checkBounds bounds to check -- @return table Objects that intersect with the specified bounds function Octree:get_colliding(checkBounds) return self.rootNode:get_colliding(checkBounds) end --- Cast a ray through the node and its children -- @param ray Ray with a position and a direction -- @param func Function to execute on any objects within child nodes -- @param out Table to store results of func in -- @return boolean True if an intersect detected function Octree:cast_ray(ray, func, out) assert(func) return self.rootNode:cast_ray(ray, func, out) end --- Draws node boundaries visually for debugging. function Octree:draw_bounds(cube) self.rootNode:draw_bounds(cube) end --- Draws the bounds of all objects in the tree visually for debugging. function Octree:draw_objects(cube, filter) self.rootNode:draw_objects(cube, filter) end --- Grow the octree to fit in all objects. -- @param direction Direction to grow function Octree:grow(direction) local xDirection = direction.x >= 0 and 1 or -1 local yDirection = direction.y >= 0 and 1 or -1 local zDirection = direction.z >= 0 and 1 or -1 local oldRoot = self.rootNode local half = self.rootNode.baseLength / 2 local newLength = self.rootNode.baseLength * 2 local newCenter = self.rootNode.center + vec3(xDirection * half, yDirection * half, zDirection * half) -- Create a new, bigger octree root node self.rootNode = Node(newLength, self.minSize, self.looseness, newCenter) -- Create 7 new octree children to go with the old root as children of the new root local rootPos = get_root_pos_index(xDirection, yDirection, zDirection) local children = {} for i = 0, 7 do if i == rootPos then children[i+1] = oldRoot else xDirection = i % 2 == 0 and -1 or 1 yDirection = i > 3 and -1 or 1 zDirection = (i < 2 or (i > 3 and i < 6)) and -1 or 1 children[i+1] = Node(self.rootNode.baseLength, self.minSize, self.looseness, newCenter + vec3(xDirection * half, yDirection * half, zDirection * half)) end end -- Attach the new children to the new root node self.rootNode:set_children(children) end --- Shrink the octree if possible, else leave it the same. function Octree:shrink() self.rootNode = self.rootNode:shrink_if_possible(self.initialSize) end --== Octree Node ==-- --- Constructor. -- @param baseLength Length of this node, not taking looseness into account -- @param minSize Minimum size of nodes in this octree -- @param looseness Multiplier for baseLengthVal to get the actual size -- @param center Centre position of this node local function new_node(baseLength, minSize, looseness, center) local node = setmetatable({}, OctreeNode) -- Objects in this node node.objects = {} -- Child nodes node.children = {} -- If there are already numObjectsAllowed in a node, we split it into children -- A generally good number seems to be something around 8-15 node.numObjectsAllowed = 8 node:set_values(baseLength, minSize, looseness, center) return node end local function new_bound(center, size) return { center = center, size = size, min = center - (size / 2), max = center + (size / 2) } end --- Add an object. -- @param obj Object to add -- @param objBounds 3D bounding box around the object -- @return boolean True if the object fits entirely within this node function OctreeNode:add(obj, objBounds) if not intersect.encapsulate_aabb(self.bounds, objBounds) then return false end -- We know it fits at this level if we've got this far -- Just add if few objects are here, or children would be below min size if #self.objects < self.numObjectsAllowed or self.baseLength / 2 < self.minSize then table.insert(self.objects, { data = obj, bounds = objBounds }) else -- Fits at this level, but we can go deeper. Would it fit there? local best_fit_child -- Create the 8 children if #self.children == 0 then self:split() if #self.children == 0 then print("Child creation failed for an unknown reason. Early exit.") return false end -- Now that we have the new children, see if this node's existing objects would fit there for i = #self.objects, 1, -1 do local object = self.objects[i] -- Find which child the object is closest to based on where the -- object's center is located in relation to the octree's center. best_fit_child = self:best_fit_child(object.bounds) -- Does it fit? if intersect.encapsulate_aabb(self.children[best_fit_child].bounds, object.bounds) then self.children[best_fit_child]:add(object.data, object.bounds) -- Go a level deeper table.remove(self.objects, i) -- Remove from here end end end -- Now handle the new object we're adding now best_fit_child = self:best_fit_child(objBounds) if intersect.encapsulate_aabb(self.children[best_fit_child].bounds, objBounds) then self.children[best_fit_child]:add(obj, objBounds) else table.insert(self.objects, { data = obj, bounds = objBounds }) end end return true end --- Remove an object. Makes the assumption that the object only exists once in the tree. -- @param obj Object to remove -- @return boolean True if the object was removed successfully function OctreeNode:remove(obj) local removed = false for i, object in ipairs(self.objects) do if object == obj then removed = table.remove(self.objects, i) and true or false break end end if not removed then for _, child in ipairs(self.children) do removed = child:remove(obj) if removed then break end end end if removed then -- Check if we should merge nodes now that we've removed an item if self:should_merge() then self:merge() end end return removed end --- Check if the specified bounds intersect with anything in the tree. See also: get_colliding. -- @param checkBounds Bounds to check -- @return boolean True if there was a collision function OctreeNode:is_colliding(checkBounds) -- Are the input bounds at least partially in this node? if not intersect.aabb_aabb(self.bounds, checkBounds) then return false end -- Check against any objects in this node for _, object in ipairs(self.objects) do if intersect.aabb_aabb(object.bounds, checkBounds) then return true end end -- Check children for _, child in ipairs(self.children) do if child:is_colliding(checkBounds) then return true end end return false end --- Returns an array of objects that intersect with the specified bounds, if any. Otherwise returns an empty array. See also: is_colliding. -- @param checkBounds Bounds to check. Passing by ref as it improve performance with structs -- @param results List results -- @return table Objects that intersect with the specified bounds function OctreeNode:get_colliding(checkBounds, results) results = results or {} -- Are the input bounds at least partially in this node? if not intersect.aabb_aabb(self.bounds, checkBounds) then return results end -- Check against any objects in this node for _, object in ipairs(self.objects) do if intersect.aabb_aabb(object.bounds, checkBounds) then table.insert(results, object.data) end end -- Check children for _, child in ipairs(self.children) do results = child:get_colliding(checkBounds, results) end return results end --- Cast a ray through the node and its children -- @param ray Ray with a position and a direction -- @param func Function to execute on any objects within child nodes -- @param out Table to store results of func in -- @param depth (used internally) -- @return boolean True if an intersect is detected function OctreeNode:cast_ray(ray, func, out, depth) depth = depth or 1 if intersect.ray_aabb(ray, self.bounds) then if #self.objects > 0 then local hit = func(ray, self.objects, out) if hit then return hit end end for _, child in ipairs(self.children) do local hit = child:cast_ray(ray, func, out, depth + 1) if hit then return hit end end end return false end --- Set the 8 children of this octree. -- @param childOctrees The 8 new child nodes function OctreeNode:set_children(childOctrees) if #childOctrees ~= 8 then print("Child octree array must be length 8. Was length: " .. #childOctrees) return end self.children = childOctrees end --- We can shrink the octree if: --- - This node is >= double minLength in length --- - All objects in the root node are within one octant --- - This node doesn't have children, or does but 7/8 children are empty --- We can also shrink it if there are no objects left at all! -- @param minLength Minimum dimensions of a node in this octree -- @return table The new root, or the existing one if we didn't shrink function OctreeNode:shrink_if_possible(minLength) if self.baseLength < 2 * minLength then return self end if #self.objects == 0 and #self.children == 0 then return self end -- Check objects in root local bestFit = 0 for i, object in ipairs(self.objects) do local newBestFit = self:best_fit_child(object.bounds) if i == 1 or newBestFit == bestFit then -- In same octant as the other(s). Does it fit completely inside that octant? if intersect.encapsulate_aabb(self.childBounds[newBestFit], object.bounds) then if bestFit < 1 then bestFit = newBestFit end else -- Nope, so we can't reduce. Otherwise we continue return self end else return self -- Can't reduce - objects fit in different octants end end -- Check objects in children if there are any if #self.children > 0 then local childHadContent = false for i, child in ipairs(self.children) do if child:has_any_objects() then if childHadContent then return self -- Can't shrink - another child had content already end if bestFit > 0 and bestFit ~= i then return self -- Can't reduce - objects in root are in a different octant to objects in child end childHadContent = true bestFit = i end end end -- Can reduce if #self.children == 0 then -- We don't have any children, so just shrink this node to the new size -- We already know that everything will still fit in it self:set_values(self.baseLength / 2, self.minSize, self.looseness, self.childBounds[bestFit].center) return self end -- We have children. Use the appropriate child as the new root node return self.children[bestFit] end --- Set values for this node. -- @param baseLength Length of this node, not taking looseness into account -- @param minSize Minimum size of nodes in this octree -- @param looseness Multiplier for baseLengthVal to get the actual size -- @param center Centre position of this node function OctreeNode:set_values(baseLength, minSize, looseness, center) -- Length of this node if it has a looseness of 1.0 self.baseLength = baseLength -- Minimum size for a node in this octree self.minSize = minSize -- Looseness value for this node self.looseness = looseness -- Centre of this node self.center = center -- Actual length of sides, taking the looseness value into account self.adjLength = self.looseness * self.baseLength -- Create the bounding box. self.size = vec3(self.adjLength, self.adjLength, self.adjLength) -- Bounding box that represents this node self.bounds = new_bound(self.center, self.size) self.quarter = self.baseLength / 4 self.childActualLength = (self.baseLength / 2) * self.looseness self.childActualSize = vec3(self.childActualLength, self.childActualLength, self.childActualLength) -- Bounds of potential children to this node. These are actual size (with looseness taken into account), not base size self.childBounds = { new_bound(self.center + vec3(-self.quarter, self.quarter, -self.quarter), self.childActualSize), new_bound(self.center + vec3( self.quarter, self.quarter, -self.quarter), self.childActualSize), new_bound(self.center + vec3(-self.quarter, self.quarter, self.quarter), self.childActualSize), new_bound(self.center + vec3( self.quarter, self.quarter, self.quarter), self.childActualSize), new_bound(self.center + vec3(-self.quarter, -self.quarter, -self.quarter), self.childActualSize), new_bound(self.center + vec3( self.quarter, -self.quarter, -self.quarter), self.childActualSize), new_bound(self.center + vec3(-self.quarter, -self.quarter, self.quarter), self.childActualSize), new_bound(self.center + vec3( self.quarter, -self.quarter, self.quarter), self.childActualSize) } end --- Splits the octree into eight children. function OctreeNode:split() if #self.children > 0 then return end local quarter = self.baseLength / 4 local newLength = self.baseLength / 2 table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, quarter, -quarter))) table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, quarter, -quarter))) table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, quarter, quarter))) table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, quarter, quarter))) table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, -quarter, -quarter))) table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, -quarter, -quarter))) table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, -quarter, quarter))) table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, -quarter, quarter))) end --- Merge all children into this node - the opposite of Split. --- Note: We only have to check one level down since a merge will never happen if the children already have children, --- since THAT won't happen unless there are already too many objects to merge. function OctreeNode:merge() for _, child in ipairs(self.children) do for _, object in ipairs(child.objects) do table.insert(self.objects, object) end end -- Remove the child nodes (and the objects in them - they've been added elsewhere now) self.children = {} end --- Find which child node this object would be most likely to fit in. -- @param objBounds The object's bounds -- @return number One of the eight child octants function OctreeNode:best_fit_child(objBounds) return (objBounds.center.x <= self.center.x and 0 or 1) + (objBounds.center.y >= self.center.y and 0 or 4) + (objBounds.center.z <= self.center.z and 0 or 2) + 1 end --- Checks if there are few enough objects in this node and its children that the children should all be merged into this. -- @return boolean True there are less or the same abount of objects in this and its children than numObjectsAllowed function OctreeNode:should_merge() local totalObjects = #self.objects for _, child in ipairs(self.children) do if #child.children > 0 then -- If any of the *children* have children, there are definitely too many to merge, -- or the child would have been merged already return false end totalObjects = totalObjects + #child.objects end return totalObjects <= self.numObjectsAllowed end --- Checks if this node or anything below it has something in it. -- @return boolean True if this node or any of its children, grandchildren etc have something in the function OctreeNode:has_any_objects() if #self.objects > 0 then return true end for _, child in ipairs(self.children) do if child:has_any_objects() then return true end end return false end --- Draws node boundaries visually for debugging. -- @param cube Cube model to draw -- @param depth Used for recurcive calls to this method function OctreeNode:draw_bounds(cube, depth) depth = depth or 0 local tint = depth / 7 -- Will eventually get values > 1. Color rounds to 1 automatically love.graphics.setColor(tint * 255, 0, (1 - tint) * 255) local m = mat4() :translate(self.center) :scale(vec3(self.adjLength, self.adjLength, self.adjLength)) love.graphics.updateMatrix("transform", m) love.graphics.setWireframe(true) love.graphics.draw(cube) love.graphics.setWireframe(false) for _, child in ipairs(self.children) do child:draw_bounds(cube, depth + 1) end love.graphics.setColor(255, 255, 255) end --- Draws the bounds of all objects in the tree visually for debugging. -- @param cube Cube model to draw -- @param filter a function returning true or false to determine visibility. function OctreeNode:draw_objects(cube, filter) local tint = self.baseLength / 20 love.graphics.setColor(0, (1 - tint) * 255, tint * 255, 63) for _, object in ipairs(self.objects) do if filter and filter(object.data) or not filter then local m = mat4() :translate(object.bounds.center) :scale(object.bounds.size) love.graphics.updateMatrix("transform", m) love.graphics.draw(cube) end end for _, child in ipairs(self.children) do child:draw_objects(cube, filter) end love.graphics.setColor(255, 255, 255) end Node = setmetatable({ new = new_node }, { __call = function(_, ...) return new_node(...) end }) return setmetatable({ new = new }, { __call = function(_, ...) return new(...) end }) ================================================ FILE: modules/quat.lua ================================================ --- A quaternion and associated utilities. -- @module quat local modules = (...):gsub('%.[^%.]+$', '') .. "." local constants = require(modules .. "constants") local vec3 = require(modules .. "vec3") local precond = require(modules .. "_private_precond") local private = require(modules .. "_private_utils") local DOT_THRESHOLD = constants.DOT_THRESHOLD local DBL_EPSILON = constants.DBL_EPSILON local acos = math.acos local cos = math.cos local sin = math.sin local min = math.min local max = math.max local sqrt = math.sqrt local quat = {} local quat_mt = {} -- Private constructor. local function new(x, y, z, w) return setmetatable({ x = x or 0, y = y or 0, z = z or 0, w = w or 1 }, quat_mt) end -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. local status, ffi if type(jit) == "table" and jit.status() then status, ffi = pcall(require, "ffi") if status then ffi.cdef "typedef struct { double x, y, z, w;} cpml_quat;" new = ffi.typeof("cpml_quat") end end -- Statically allocate a temporary variable used in some of our functions. local tmp = new() local qv, uv, uuv = vec3(), vec3(), vec3() --- Constants -- @table quat -- @field unit Unit quaternion -- @field zero Empty quaternion quat.unit = new(0, 0, 0, 1) quat.zero = new(0, 0, 0, 0) --- The public constructor. -- @param x Can be of two types:
-- number x X component -- table {x, y, z, w} or {x=x, y=y, z=z, w=w} -- @tparam number y Y component -- @tparam number z Z component -- @tparam number w W component -- @treturn quat out function quat.new(x, y, z, w) -- number, number, number, number if x and y and z and w then precond.typeof(x, "number", "new: Wrong argument type for x") precond.typeof(y, "number", "new: Wrong argument type for y") precond.typeof(z, "number", "new: Wrong argument type for z") precond.typeof(w, "number", "new: Wrong argument type for w") return new(x, y, z, w) -- {x, y, z, w} or {x=x, y=y, z=z, w=w} elseif type(x) == "table" or type (x) == "cdata" then 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] precond.typeof(xx, "number", "new: Wrong argument type for x") precond.typeof(yy, "number", "new: Wrong argument type for y") precond.typeof(zz, "number", "new: Wrong argument type for z") precond.typeof(ww, "number", "new: Wrong argument type for w") return new(xx, yy, zz, ww) else precond.assert(x == nil, "new: Wrong arguments") return new(0, 0, 0, 1) end end --- Create a quaternion from an angle/axis pair. -- @tparam number angle Angle (in radians) -- @param axis/x -- Can be of two types, a vec3 axis, or the x component of that axis -- @param y axis -- y component of axis (optional, only if x component param used) -- @param z axis -- z component of axis (optional, only if x component param used) -- @treturn quat out function quat.from_angle_axis(angle, axis, a3, a4) if axis and a3 and a4 then local x, y, z = axis, a3, a4 local s = sin(angle * 0.5) local c = cos(angle * 0.5) return new(x * s, y * s, z * s, c) else return quat.from_angle_axis(angle, axis.x, axis.y, axis.z) end end --- Create a quaternion from a normal/up vector pair. -- @tparam vec3 normal -- @tparam vec3 up (optional) -- @treturn quat out function quat.from_direction(normal, up) local u = up or vec3.unit_z local n = normal:normalize() local a = u:cross(n) local d = u:dot(n) return new(a.x, a.y, a.z, d + 1) end --- Clone a quaternion. -- @tparam quat a Quaternion to clone -- @treturn quat out function quat.clone(a) return new(a.x, a.y, a.z, a.w) end --- Add two quaternions. -- @tparam quat a Left hand operand -- @tparam quat b Right hand operand -- @treturn quat out function quat.add(a, b) return new( a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w ) end --- Subtract a quaternion from another. -- @tparam quat a Left hand operand -- @tparam quat b Right hand operand -- @treturn quat out function quat.sub(a, b) return new( a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w ) end --- Multiply two quaternions. -- @tparam quat a Left hand operand -- @tparam quat b Right hand operand -- @treturn quat quaternion equivalent to "apply b, then a" function quat.mul(a, b) return new( a.x * b.w + a.w * b.x + a.y * b.z - a.z * b.y, a.y * b.w + a.w * b.y + a.z * b.x - a.x * b.z, a.z * b.w + a.w * b.z + a.x * b.y - a.y * b.x, a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z ) end --- Multiply a quaternion and a vec3. -- @tparam quat a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3 out function quat.mul_vec3(a, b) qv.x = a.x qv.y = a.y qv.z = a.z uv = qv:cross(b) uuv = qv:cross(uv) return b + ((uv * a.w) + uuv) * 2 end --- Raise a normalized quaternion to a scalar power. -- @tparam quat a Left hand operand (should be a unit quaternion) -- @tparam number s Right hand operand -- @treturn quat out function quat.pow(a, s) -- Do it as a slerp between identity and a (code borrowed from slerp) if a.w < 0 then a = -a end local dot = a.w dot = min(max(dot, -1), 1) local theta = acos(dot) * s local c = new(a.x, a.y, a.z, 0):normalize() * sin(theta) c.w = cos(theta) return c end --- Normalize a quaternion. -- @tparam quat a Quaternion to normalize -- @treturn quat out function quat.normalize(a) if a:is_zero() then return new(0, 0, 0, 0) end return a:scale(1 / a:len()) end --- Get the dot product of two quaternions. -- @tparam quat a Left hand operand -- @tparam quat b Right hand operand -- @treturn number dot function quat.dot(a, b) return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w end --- Return the length of a quaternion. -- @tparam quat a Quaternion to get length of -- @treturn number len function quat.len(a) return sqrt(a.x * a.x + a.y * a.y + a.z * a.z + a.w * a.w) end --- Return the squared length of a quaternion. -- @tparam quat a Quaternion to get length of -- @treturn number len function quat.len2(a) return a.x * a.x + a.y * a.y + a.z * a.z + a.w * a.w end --- Multiply a quaternion by a scalar. -- @tparam quat a Left hand operand -- @tparam number s Right hand operand -- @treturn quat out function quat.scale(a, s) return new( a.x * s, a.y * s, a.z * s, a.w * s ) end --- Alias of from_angle_axis. -- @tparam number angle Angle (in radians) -- @param axis/x -- Can be of two types, a vec3 axis, or the x component of that axis -- @param y axis -- y component of axis (optional, only if x component param used) -- @param z axis -- z component of axis (optional, only if x component param used) -- @treturn quat out function quat.rotate(angle, axis, a3, a4) return quat.from_angle_axis(angle, axis, a3, a4) end --- Return the conjugate of a quaternion. -- @tparam quat a Quaternion to conjugate -- @treturn quat out function quat.conjugate(a) return new(-a.x, -a.y, -a.z, a.w) end --- Return the inverse of a quaternion. -- @tparam quat a Quaternion to invert -- @treturn quat out function quat.inverse(a) tmp.x = -a.x tmp.y = -a.y tmp.z = -a.z tmp.w = a.w return tmp:normalize() end --- Return the reciprocal of a quaternion. -- @tparam quat a Quaternion to reciprocate -- @treturn quat out function quat.reciprocal(a) if a:is_zero() then error("Cannot reciprocate a zero quaternion") return false end tmp.x = -a.x tmp.y = -a.y tmp.z = -a.z tmp.w = a.w return tmp:scale(1 / a:len2()) end --- Lerp between two quaternions. -- @tparam quat a Left hand operand -- @tparam quat b Right hand operand -- @tparam number s Step value -- @treturn quat out function quat.lerp(a, b, s) return (a + (b - a) * s):normalize() end --- Slerp between two quaternions. -- @tparam quat a Left hand operand -- @tparam quat b Right hand operand -- @tparam number s Step value -- @treturn quat out function quat.slerp(a, b, s) local dot = a:dot(b) if dot < 0 then a = -a dot = -dot end if dot > DOT_THRESHOLD then return a:lerp(b, s) end dot = min(max(dot, -1), 1) local theta = acos(dot) * s local c = (b - a * dot):normalize() return a * cos(theta) + c * sin(theta) end --- Unpack a quaternion into individual components. -- @tparam quat a Quaternion to unpack -- @treturn number x -- @treturn number y -- @treturn number z -- @treturn number w function quat.unpack(a) return a.x, a.y, a.z, a.w end --- Return a boolean showing if a table is or is not a quat. -- @tparam quat a Quaternion to be tested -- @treturn boolean is_quat function quat.is_quat(a) if type(a) == "cdata" then return ffi.istype("cpml_quat", a) end return type(a) == "table" and type(a.x) == "number" and type(a.y) == "number" and type(a.z) == "number" and type(a.w) == "number" end --- Return a boolean showing if a table is or is not a zero quat. -- @tparam quat a Quaternion to be tested -- @treturn boolean is_zero function quat.is_zero(a) return a.x == 0 and a.y == 0 and a.z == 0 and a.w == 0 end --- Return a boolean showing if a table is or is not a real quat. -- @tparam quat a Quaternion to be tested -- @treturn boolean is_real function quat.is_real(a) return a.x == 0 and a.y == 0 and a.z == 0 end --- Return a boolean showing if a table is or is not an imaginary quat. -- @tparam quat a Quaternion to be tested -- @treturn boolean is_imaginary function quat.is_imaginary(a) return a.w == 0 end --- Return whether any component is NaN -- @tparam quat a Quaternion to be tested -- @treturn boolean if x,y,z, or w is NaN function quat.has_nan(a) return private.is_nan(a.x) or private.is_nan(a.y) or private.is_nan(a.z) or private.is_nan(a.w) end --- Convert a quaternion into an angle plus axis components. -- @tparam quat a Quaternion to convert -- @tparam identityAxis vec3 of axis to use on identity/degenerate quaternions (optional, default returns 0,0,0,1) -- @treturn number angle -- @treturn x axis-x -- @treturn y axis-y -- @treturn z axis-z function quat.to_angle_axis_unpack(a, identityAxis) if a.w > 1 or a.w < -1 then a = a:normalize() end -- If length of xyz components is less than DBL_EPSILON, this is zero or close enough (an identity quaternion) -- Normally an identity quat would return a nonsense answer, so we return an arbitrary zero rotation early. -- FIXME: Is it safe to assume there are *no* valid quaternions with nonzero degenerate lengths? if a.x*a.x + a.y*a.y + a.z*a.z < constants.DBL_EPSILON*constants.DBL_EPSILON then if identityAxis then return 0,identityAxis:unpack() else return 0,0,0,1 end end local x, y, z local angle = 2 * acos(a.w) local s = sqrt(1 - a.w * a.w) if s < DBL_EPSILON then x = a.x y = a.y z = a.z else x = a.x / s y = a.y / s z = a.z / s end return angle, x, y, z end --- Convert a quaternion into an angle/axis pair. -- @tparam quat a Quaternion to convert -- @tparam identityAxis vec3 of axis to use on identity/degenerate quaternions (optional, default returns 0,vec3(0,0,1)) -- @treturn number angle -- @treturn vec3 axis function quat.to_angle_axis(a, identityAxis) local angle, x, y, z = a:to_angle_axis_unpack(identityAxis) return angle, vec3(x, y, z) end --- Convert a quaternion into a vec3. -- @tparam quat a Quaternion to convert -- @treturn vec3 out function quat.to_vec3(a) return vec3(a.x, a.y, a.z) end --- Return a formatted string. -- @tparam quat a Quaternion to be turned into a string -- @treturn string formatted function quat.to_string(a) return string.format("(%+0.3f,%+0.3f,%+0.3f,%+0.3f)", a.x, a.y, a.z, a.w) end quat_mt.__index = quat quat_mt.__tostring = quat.to_string function quat_mt.__call(_, x, y, z, w) return quat.new(x, y, z, w) end function quat_mt.__unm(a) return a:scale(-1) end function quat_mt.__eq(a,b) if not quat.is_quat(a) or not quat.is_quat(b) then return false end return a.x == b.x and a.y == b.y and a.z == b.z and a.w == b.w end function quat_mt.__add(a, b) precond.assert(quat.is_quat(a), "__add: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) precond.assert(quat.is_quat(b), "__add: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) return a:add(b) end function quat_mt.__sub(a, b) precond.assert(quat.is_quat(a), "__sub: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) precond.assert(quat.is_quat(b), "__sub: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) return a:sub(b) end function quat_mt.__mul(a, b) precond.assert(quat.is_quat(a), "__mul: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) 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)") if quat.is_quat(b) then return a:mul(b) end if type(b) == "number" then return a:scale(b) end return a:mul_vec3(b) end function quat_mt.__pow(a, n) precond.assert(quat.is_quat(a), "__pow: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) precond.typeof(n, "number", "__pow: Wrong argument type for right hand operand.") return a:pow(n) end if status then xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded ffi.metatype(new, quat_mt) end, function() end) end return setmetatable({}, quat_mt) ================================================ FILE: modules/simplex.lua ================================================ --- Simplex Noise -- @module simplex -- -- Based on code in "Simplex noise demystified", by Stefan Gustavson -- www.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf -- -- Thanks to Mike Pall for some cleanup and improvements (and for LuaJIT!) -- -- Permission is hereby granted, free of charge, to any person obtaining -- a copy of this software and associated documentation files (the -- "Software"), to deal in the Software without restriction, including -- without limitation the rights to use, copy, modify, merge, publish, -- distribute, sublicense, and/or sell copies of the Software, and to -- permit persons to whom the Software is furnished to do so, subject to -- the following conditions: -- -- The above copyright notice and this permission notice shall be -- included in all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- -- [ MIT license: http://www.opensource.org/licenses/mit-license.php ] -- if _G.love and _G.love.math then return love.math.noise end -- Bail out with dummy module if FFI is missing. local has_ffi, ffi = pcall(require, "ffi") if not has_ffi then return function() return 0 end end -- Modules -- local bit = require("bit") -- Imports -- local band = bit.band local bor = bit.bor local floor = math.floor local lshift = bit.lshift local max = math.max local rshift = bit.rshift -- Permutation of 0-255, replicated to allow easy indexing with sums of two bytes -- local Perms = ffi.new("uint8_t[512]", { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 }) -- The above, mod 12 for each element -- local Perms12 = ffi.new("uint8_t[512]") for i = 0, 255 do local x = Perms[i] % 12 Perms[i + 256], Perms12[i], Perms12[i + 256] = Perms[i], x, x end -- Gradients for 2D, 3D case -- local Grads3 = ffi.new("const double[12][3]", { 1, 1, 0 }, { -1, 1, 0 }, { 1, -1, 0 }, { -1, -1, 0 }, { 1, 0, 1 }, { -1, 0, 1 }, { 1, 0, -1 }, { -1, 0, -1 }, { 0, 1, 1 }, { 0, -1, 1 }, { 0, 1, -1 }, { 0, -1, -1 } ) -- 2D weight contribution local function GetN2(bx, by, x, y) local t = .5 - x * x - y * y local index = Perms12[bx + Perms[by]] return max(0, (t * t) * (t * t)) * (Grads3[index][0] * x + Grads3[index][1] * y) end local function simplex_2d(x, y) --[[ 2D skew factors: F = (math.sqrt(3) - 1) / 2 G = (3 - math.sqrt(3)) / 6 G2 = 2 * G - 1 ]] -- Skew the input space to determine which simplex cell we are in. local s = (x + y) * 0.366025403 -- F local ix, iy = floor(x + s), floor(y + s) -- Unskew the cell origin back to (x, y) space. local t = (ix + iy) * 0.211324865 -- G local x0 = x + t - ix local y0 = y + t - iy -- Calculate the contribution from the two fixed corners. -- A step of (1,0) in (i,j) means a step of (1-G,-G) in (x,y), and -- A step of (0,1) in (i,j) means a step of (-G,1-G) in (x,y). ix, iy = band(ix, 255), band(iy, 255) local n0 = GetN2(ix, iy, x0, y0) local n2 = GetN2(ix + 1, iy + 1, x0 - 0.577350270, y0 - 0.577350270) -- G2 --[[ Determine other corner based on simplex (equilateral triangle) we are in: if x0 > y0 then ix, x1 = ix + 1, x1 - 1 else iy, y1 = iy + 1, y1 - 1 end ]] local xi = rshift(floor(y0 - x0), 31) -- y0 < x0 local n1 = GetN2(ix + xi, iy + (1 - xi), x0 + 0.211324865 - xi, y0 - 0.788675135 + xi) -- x0 + G - xi, y0 + G - (1 - xi) -- Add contributions from each corner to get the final noise value. -- The result is scaled to return values in the interval [-1,1]. return 70.1480580019 * (n0 + n1 + n2) end -- 3D weight contribution local function GetN3(ix, iy, iz, x, y, z) local t = .6 - x * x - y * y - z * z local index = Perms12[ix + Perms[iy + Perms[iz]]] return max(0, (t * t) * (t * t)) * (Grads3[index][0] * x + Grads3[index][1] * y + Grads3[index][2] * z) end local function simplex_3d(x, y, z) --[[ 3D skew factors: F = 1 / 3 G = 1 / 6 G2 = 2 * G G3 = 3 * G - 1 ]] -- Skew the input space to determine which simplex cell we are in. local s = (x + y + z) * 0.333333333 -- F local ix, iy, iz = floor(x + s), floor(y + s), floor(z + s) -- Unskew the cell origin back to (x, y, z) space. local t = (ix + iy + iz) * 0.166666667 -- G local x0 = x + t - ix local y0 = y + t - iy local z0 = z + t - iz -- Calculate the contribution from the two fixed corners. -- A step of (1,0,0) in (i,j,k) means a step of (1-G,-G,-G) in (x,y,z); -- a step of (0,1,0) in (i,j,k) means a step of (-G,1-G,-G) in (x,y,z); -- a step of (0,0,1) in (i,j,k) means a step of (-G,-G,1-G) in (x,y,z). ix, iy, iz = band(ix, 255), band(iy, 255), band(iz, 255) local n0 = GetN3(ix, iy, iz, x0, y0, z0) local n3 = GetN3(ix + 1, iy + 1, iz + 1, x0 - 0.5, y0 - 0.5, z0 - 0.5) -- G3 --[[ Determine other corners based on simplex (skewed tetrahedron) we are in: if x0 >= y0 then -- ~A if y0 >= z0 then -- ~A and ~B i1, j1, k1, i2, j2, k2 = 1, 0, 0, 1, 1, 0 elseif x0 >= z0 then -- ~A and B and ~C i1, j1, k1, i2, j2, k2 = 1, 0, 0, 1, 0, 1 else -- ~A and B and C i1, j1, k1, i2, j2, k2 = 0, 0, 1, 1, 0, 1 end else -- A if y0 < z0 then -- A and B i1, j1, k1, i2, j2, k2 = 0, 0, 1, 0, 1, 1 elseif x0 < z0 then -- A and ~B and C i1, j1, k1, i2, j2, k2 = 0, 1, 0, 0, 1, 1 else -- A and ~B and ~C i1, j1, k1, i2, j2, k2 = 0, 1, 0, 1, 1, 0 end end ]] local xLy = rshift(floor(x0 - y0), 31) -- x0 < y0 local yLz = rshift(floor(y0 - z0), 31) -- y0 < z0 local xLz = rshift(floor(x0 - z0), 31) -- x0 < z0 local i1 = band(1 - xLy, bor(1 - yLz, 1 - xLz)) -- x0 >= y0 and (y0 >= z0 or x0 >= z0) local j1 = band(xLy, 1 - yLz) -- x0 < y0 and y0 >= z0 local k1 = band(yLz, bor(xLy, xLz)) -- y0 < z0 and (x0 < y0 or x0 < z0) local i2 = bor(1 - xLy, band(1 - yLz, 1 - xLz)) -- x0 >= y0 or (y0 >= z0 and x0 >= z0) local j2 = bor(xLy, 1 - yLz) -- x0 < y0 or y0 >= z0 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)) local n1 = GetN3(ix + i1, iy + j1, iz + k1, x0 + 0.166666667 - i1, y0 + 0.166666667 - j1, z0 + 0.166666667 - k1) -- G local n2 = GetN3(ix + i2, iy + j2, iz + k2, x0 + 0.333333333 - i2, y0 + 0.333333333 - j2, z0 + 0.333333333 - k2) -- G2 -- Add contributions from each corner to get the final noise value. -- The result is scaled to stay just inside [-1,1] return 28.452842 * (n0 + n1 + n2 + n3) end -- Gradients for 4D case -- local Grads4 = ffi.new("const double[32][4]", { 0, 1, 1, 1 }, { 0, 1, 1, -1 }, { 0, 1, -1, 1 }, { 0, 1, -1, -1 }, { 0, -1, 1, 1 }, { 0, -1, 1, -1 }, { 0, -1, -1, 1 }, { 0, -1, -1, -1 }, { 1, 0, 1, 1 }, { 1, 0, 1, -1 }, { 1, 0, -1, 1 }, { 1, 0, -1, -1 }, { -1, 0, 1, 1 }, { -1, 0, 1, -1 }, { -1, 0, -1, 1 }, { -1, 0, -1, -1 }, { 1, 1, 0, 1 }, { 1, 1, 0, -1 }, { 1, -1, 0, 1 }, { 1, -1, 0, -1 }, { -1, 1, 0, 1 }, { -1, 1, 0, -1 }, { -1, -1, 0, 1 }, { -1, -1, 0, -1 }, { 1, 1, 1, 0 }, { 1, 1, -1, 0 }, { 1, -1, 1, 0 }, { 1, -1, -1, 0 }, { -1, 1, 1, 0 }, { -1, 1, -1, 0 }, { -1, -1, 1, 0 }, { -1, -1, -1, 0 } ) -- 4D weight contribution local function GetN4(ix, iy, iz, iw, x, y, z, w) local t = .6 - x * x - y * y - z * z - w * w local index = band(Perms[ix + Perms[iy + Perms[iz + Perms[iw]]]], 0x1F) return max(0, (t * t) * (t * t)) * (Grads4[index][0] * x + Grads4[index][1] * y + Grads4[index][2] * z + Grads4[index][3] * w) end -- A lookup table to traverse the simplex around a given point in 4D. -- Details can be found where this table is used, in the 4D noise method. local Simplex = ffi.new("uint8_t[64][4]", { 0, 1, 2, 3 }, { 0, 1, 3, 2 }, {}, { 0, 2, 3, 1 }, {}, {}, {}, { 1, 2, 3 }, { 0, 2, 1, 3 }, {}, { 0, 3, 1, 2 }, { 0, 3, 2, 1 }, {}, {}, {}, { 1, 3, 2 }, {}, {}, {}, {}, {}, {}, {}, {}, { 1, 2, 0, 3 }, {}, { 1, 3, 0, 2 }, {}, {}, {}, { 2, 3, 0, 1 }, { 2, 3, 1 }, { 1, 0, 2, 3 }, { 1, 0, 3, 2 }, {}, {}, {}, { 2, 0, 3, 1 }, {}, { 2, 1, 3 }, {}, {}, {}, {}, {}, {}, {}, {}, { 2, 0, 1, 3 }, {}, {}, {}, { 3, 0, 1, 2 }, { 3, 0, 2, 1 }, {}, { 3, 1, 2 }, { 2, 1, 0, 3 }, {}, {}, {}, { 3, 1, 0, 2 }, {}, { 3, 2, 0, 1 }, { 3, 2, 1 } ) -- Convert the above indices to masks that can be shifted / anded into offsets -- for i = 0, 63 do Simplex[i][0] = lshift(1, Simplex[i][0]) - 1 Simplex[i][1] = lshift(1, Simplex[i][1]) - 1 Simplex[i][2] = lshift(1, Simplex[i][2]) - 1 Simplex[i][3] = lshift(1, Simplex[i][3]) - 1 end local function simplex_4d(x, y, z, w) --[[ 4D skew factors: F = (math.sqrt(5) - 1) / 4 G = (5 - math.sqrt(5)) / 20 G2 = 2 * G G3 = 3 * G G4 = 4 * G - 1 ]] -- Skew the input space to determine which simplex cell we are in. local s = (x + y + z + w) * 0.309016994 -- F local ix, iy, iz, iw = floor(x + s), floor(y + s), floor(z + s), floor(w + s) -- Unskew the cell origin back to (x, y, z) space. local t = (ix + iy + iz + iw) * 0.138196601 -- G local x0 = x + t - ix local y0 = y + t - iy local z0 = z + t - iz local w0 = w + t - iw -- For the 4D case, the simplex is a 4D shape I won't even try to describe. -- To find out which of the 24 possible simplices we're in, we need to -- determine the magnitude ordering of x0, y0, z0 and w0. -- The method below is a good way of finding the ordering of x,y,z,w and -- then find the correct traversal order for the simplex we�re in. -- First, six pair-wise comparisons are performed between each possible pair -- of the four coordinates, and the results are used to add up binary bits -- for an integer index. local c1 = band(rshift(floor(y0 - x0), 26), 32) local c2 = band(rshift(floor(z0 - x0), 27), 16) local c3 = band(rshift(floor(z0 - y0), 28), 8) local c4 = band(rshift(floor(w0 - x0), 29), 4) local c5 = band(rshift(floor(w0 - y0), 30), 2) local c6 = rshift(floor(w0 - z0), 31) -- Simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order. -- Many values of c will never occur, since e.g. x>y>z>w makes x= size and value or 0 end --- Check if value is equal or greater than threshold. -- @param value -- @param threshold -- @return boolean function utils.threshold(value, threshold) -- I know, it barely saves any typing at all. return abs(value) >= threshold end --- Check if value is equal or less than threshold. -- @param value -- @param threshold -- @return boolean function utils.tolerance(value, threshold) -- I know, it barely saves any typing at all. return abs(value) <= threshold end --- Scales a value from one range to another. -- @param value Input value -- @param min_in Minimum input value -- @param max_in Maximum input value -- @param min_out Minimum output value -- @param max_out Maximum output value -- @return number function utils.map(value, min_in, max_in, min_out, max_out) return ((value) - (min_in)) * ((max_out) - (min_out)) / ((max_in) - (min_in)) + (min_out) end --- Linear interpolation. -- Performs linear interpolation between 0 and 1 when `low` < `progress` < `high`. -- @param low value to return when `progress` is 0 -- @param high value to return when `progress` is 1 -- @param progress (0-1) -- @return number function utils.lerp(low, high, progress) return low * (1 - progress) + high * progress end --- Exponential decay -- @param low initial value -- @param high target value -- @param rate portion of the original value remaining per second -- @param dt time delta -- @return number function utils.decay(low, high, rate, dt) return utils.lerp(low, high, 1.0 - math.exp(-rate * dt)) end --- Hermite interpolation. -- Performs smooth Hermite interpolation between 0 and 1 when `low` < `progress` < `high`. -- @param progress (0-1) -- @param low value to return when `progress` is 0 -- @param high value to return when `progress` is 1 -- @return number function utils.smoothstep(progress, low, high) local t = utils.clamp((progress - low) / (high - low), 0.0, 1.0) return t * t * (3.0 - 2.0 * t) end --- Round number at a given precision. -- Truncates `value` at `precision` points after the decimal (whole number if -- left unspecified). -- @param value -- @param precision -- @return number utils.round = private.round --- Wrap `value` around if it exceeds `limit`. -- @param value -- @param limit -- @return number function utils.wrap(value, limit) if value < 0 then value = value + utils.round(((-value/limit)+1))*limit end return value % limit end --- Check if a value is a power-of-two. -- Returns true if a number is a valid power-of-two, otherwise false. -- @author undef -- @param value -- @return boolean function utils.is_pot(value) -- found here: https://love2d.org/forums/viewtopic.php?p=182219#p182219 -- check if a number is a power-of-two return (frexp(value)) == 0.5 end --- Check if a value is NaN -- Returns true if a number is not a valid number -- @param value -- @return boolean utils.is_nan = private.is_nan -- Originally from vec3 function utils.project_on(a, b) local s = (a.x * b.x + a.y * b.y + a.z or 0 * b.z or 0) / (b.x * b.x + b.y * b.y + b.z or 0 * b.z or 0) if a.z and b.z then return vec3( b.x * s, b.y * s, b.z * s ) end return vec2( b.x * s, b.y * s ) end -- Originally from vec3 function utils.project_from(a, b) local s = (b.x * b.x + b.y * b.y + b.z or 0 * b.z or 0) / (a.x * b.x + a.y * b.y + a.z or 0 * b.z or 0) if a.z and b.z then return vec3( b.x * s, b.y * s, b.z * s ) end return vec2( b.x * s, b.y * s ) end -- Originally from vec3 function utils.mirror_on(a, b) local s = (a.x * b.x + a.y * b.y + a.z or 0 * b.z or 0) / (b.x * b.x + b.y * b.y + b.z or 0 * b.z or 0) * 2 if a.z and b.z then return vec3( b.x * s - a.x, b.y * s - a.y, b.z * s - a.z ) end return vec2( b.x * s - a.x, b.y * s - a.y ) end -- Originally from vec3 function utils.reflect(i, n) return i - (n * (2 * n:dot(i))) end -- Originally from vec3 function utils.refract(i, n, ior) local d = n:dot(i) local k = 1 - ior * ior * (1 - d * d) if k >= 0 then return (i * ior) - (n * (ior * d + k ^ 0.5)) end return vec3() end --- Get the sign of a number -- returns 1 for positive values, -1 for negative and 0 for zero. -- @param value -- @return number function utils.sign(n) if n > 0 then return 1 elseif n < 0 then return -1 else return 0 end end return utils ================================================ FILE: modules/vec2.lua ================================================ --- A 2 component vector. -- @module vec2 local modules = (...):gsub('%.[^%.]+$', '') .. "." local vec3 = require(modules .. "vec3") local precond = require(modules .. "_private_precond") local private = require(modules .. "_private_utils") local acos = math.acos local atan2 = math.atan2 or math.atan local sqrt = math.sqrt local cos = math.cos local sin = math.sin local vec2 = {} local vec2_mt = {} -- Private constructor. local function new(x, y) return setmetatable({ x = x or 0, y = y or 0 }, vec2_mt) end -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. local status, ffi if type(jit) == "table" and jit.status() then status, ffi = pcall(require, "ffi") if status then ffi.cdef "typedef struct { double x, y;} cpml_vec2;" new = ffi.typeof("cpml_vec2") end end --- Constants -- @table vec2 -- @field unit_x X axis of rotation -- @field unit_y Y axis of rotation -- @field zero Empty vector vec2.unit_x = new(1, 0) vec2.unit_y = new(0, 1) vec2.zero = new(0, 0) --- The public constructor. -- @param x Can be of three types:
-- number X component -- table {x, y} or {x = x, y = y} -- scalar to fill the vector eg. {x, x} -- @tparam number y Y component -- @treturn vec2 out function vec2.new(x, y) -- number, number if x and y then precond.typeof(x, "number", "new: Wrong argument type for x") precond.typeof(y, "number", "new: Wrong argument type for y") return new(x, y) -- {x, y} or {x=x, y=y} elseif type(x) == "table" or type(x) == "cdata" then -- table in vanilla lua, cdata in luajit local xx, yy = x.x or x[1], x.y or x[2] precond.typeof(xx, "number", "new: Wrong argument type for x") precond.typeof(yy, "number", "new: Wrong argument type for y") return new(xx, yy) -- number elseif type(x) == "number" then return new(x, x) else return new() end end --- Convert point from polar to cartesian. -- @tparam number radius Radius of the point -- @tparam number theta Angle of the point (in radians) -- @treturn vec2 out function vec2.from_cartesian(radius, theta) return new(radius * cos(theta), radius * sin(theta)) end --- Clone a vector. -- @tparam vec2 a Vector to be cloned -- @treturn vec2 out function vec2.clone(a) return new(a.x, a.y) end --- Add two vectors. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn vec2 out function vec2.add(a, b) return new( a.x + b.x, a.y + b.y ) end --- Subtract one vector from another. -- Order: If a and b are positions, computes the direction and distance from b -- to a. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn vec2 out function vec2.sub(a, b) return new( a.x - b.x, a.y - b.y ) end --- Multiply a vector by another vector. -- Component-size multiplication not matrix multiplication. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn vec2 out function vec2.mul(a, b) return new( a.x * b.x, a.y * b.y ) end --- Divide a vector by another vector. -- Component-size inv multiplication. Like a non-uniform scale(). -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn vec2 out function vec2.div(a, b) return new( a.x / b.x, a.y / b.y ) end --- Get the normal of a vector. -- @tparam vec2 a Vector to normalize -- @treturn vec2 out function vec2.normalize(a) if a:is_zero() then return new() end return a:scale(1 / a:len()) end --- Trim a vector to a given length. -- @tparam vec2 a Vector to be trimmed -- @tparam number len Length to trim the vector to -- @treturn vec2 out function vec2.trim(a, len) return a:normalize():scale(math.min(a:len(), len)) end --- Get the cross product of two vectors. -- Order: Positive if a is clockwise from b. Magnitude is the area spanned by -- the parallelograms that a and b span. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn number magnitude function vec2.cross(a, b) return a.x * b.y - a.y * b.x end --- Get the dot product of two vectors. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn number dot function vec2.dot(a, b) return a.x * b.x + a.y * b.y end --- Get the length of a vector. -- @tparam vec2 a Vector to get the length of -- @treturn number len function vec2.len(a) return sqrt(a.x * a.x + a.y * a.y) end --- Get the squared length of a vector. -- @tparam vec2 a Vector to get the squared length of -- @treturn number len function vec2.len2(a) return a.x * a.x + a.y * a.y end --- Get the distance between two vectors. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn number dist function vec2.dist(a, b) local dx = a.x - b.x local dy = a.y - b.y return sqrt(dx * dx + dy * dy) end --- Get the squared distance between two vectors. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn number dist function vec2.dist2(a, b) local dx = a.x - b.x local dy = a.y - b.y return dx * dx + dy * dy end --- Scale a vector by a scalar. -- @tparam vec2 a Left hand operand -- @tparam number b Right hand operand -- @treturn vec2 out function vec2.scale(a, b) return new( a.x * b, a.y * b ) end --- Rotate a vector. -- @tparam vec2 a Vector to rotate -- @tparam number phi Angle to rotate vector by (in radians) -- @treturn vec2 out function vec2.rotate(a, phi) local c = cos(phi) local s = sin(phi) return new( c * a.x - s * a.y, s * a.x + c * a.y ) end --- Get the perpendicular vector of a vector. -- @tparam vec2 a Vector to get perpendicular axes from -- @treturn vec2 out function vec2.perpendicular(a) return new(-a.y, a.x) end --- Signed angle from one vector to another. -- Rotations from +x to +y are positive. -- @tparam vec2 a Vector -- @tparam vec2 b Vector -- @treturn number angle in (-pi, pi] function vec2.angle_to(a, b) if b then local angle = atan2(b.y, b.x) - atan2(a.y, a.x) -- convert to (-pi, pi] if angle > math.pi then angle = angle - 2 * math.pi elseif angle <= -math.pi then angle = angle + 2 * math.pi end return angle end return atan2(a.y, a.x) end --- Unsigned angle between two vectors. -- Directionless and thus commutative. -- @tparam vec2 a Vector -- @tparam vec2 b Vector -- @treturn number angle in [0, pi] function vec2.angle_between(a, b) if b then if vec2.is_vec2(a) then return acos(a:dot(b) / (a:len() * b:len())) end return acos(vec3.dot(a, b) / (vec3.len(a) * vec3.len(b))) end return 0 end --- Lerp between two vectors. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @tparam number s Step value -- @treturn vec2 out function vec2.lerp(a, b, s) return a + (b - a) * s end --- Unpack a vector into individual components. -- @tparam vec2 a Vector to unpack -- @treturn number x -- @treturn number y function vec2.unpack(a) return a.x, a.y end --- Return the component-wise minimum of two vectors. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn vec2 A vector where each component is the lesser value for that component between the two given vectors. function vec2.component_min(a, b) return new(math.min(a.x, b.x), math.min(a.y, b.y)) end --- Return the component-wise maximum of two vectors. -- @tparam vec2 a Left hand operand -- @tparam vec2 b Right hand operand -- @treturn vec2 A vector where each component is the lesser value for that component between the two given vectors. function vec2.component_max(a, b) return new(math.max(a.x, b.x), math.max(a.y, b.y)) end --- Return a boolean showing if a table is or is not a vec2. -- @tparam vec2 a Vector to be tested -- @treturn boolean is_vec2 function vec2.is_vec2(a) if type(a) == "cdata" then return ffi.istype("cpml_vec2", a) end return type(a) == "table" and type(a.x) == "number" and type(a.y) == "number" end --- Return a boolean showing if a table is or is not a zero vec2. -- @tparam vec2 a Vector to be tested -- @treturn boolean is_zero function vec2.is_zero(a) return a.x == 0 and a.y == 0 end --- Return whether either value is NaN -- @tparam vec2 a Vector to be tested -- @treturn boolean if x or y is nan function vec2.has_nan(a) return private.is_nan(a.x) or private.is_nan(a.y) end --- Convert point from cartesian to polar. -- @tparam vec2 a Vector to convert -- @treturn number radius -- @treturn number theta function vec2.to_polar(a) local radius = sqrt(a.x^2 + a.y^2) local theta = atan2(a.y, a.x) theta = theta > 0 and theta or theta + 2 * math.pi return radius, theta end -- Round all components to nearest int (or other precision). -- @tparam vec2 a Vector to round. -- @tparam precision Digits after the decimal (integer if unspecified) -- @treturn vec2 Rounded vector function vec2.round(a, precision) return vec2.new(private.round(a.x, precision), private.round(a.y, precision)) end -- Negate x axis only of vector. -- @tparam vec2 a Vector to x-flip. -- @treturn vec2 x-flipped vector function vec2.flip_x(a) return vec2.new(-a.x, a.y) end -- Negate y axis only of vector. -- @tparam vec2 a Vector to y-flip. -- @treturn vec2 y-flipped vector function vec2.flip_y(a) return vec2.new(a.x, -a.y) end -- Convert vec2 to vec3. -- @tparam vec2 a Vector to convert. -- @tparam number the new z component, or nil for 0 -- @treturn vec3 Converted vector function vec2.to_vec3(a, z) return vec3(a.x, a.y, z or 0) end --- Return a formatted string. -- @tparam vec2 a Vector to be turned into a string -- @treturn string formatted function vec2.to_string(a) return string.format("(%+0.3f,%+0.3f)", a.x, a.y) end vec2_mt.__index = vec2 vec2_mt.__tostring = vec2.to_string function vec2_mt.__call(_, x, y) return vec2.new(x, y) end function vec2_mt.__unm(a) return new(-a.x, -a.y) end function vec2_mt.__eq(a, b) if not vec2.is_vec2(a) or not vec2.is_vec2(b) then return false end return a.x == b.x and a.y == b.y end function vec2_mt.__add(a, b) precond.assert(vec2.is_vec2(a), "__add: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) precond.assert(vec2.is_vec2(b), "__add: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) return a:add(b) end function vec2_mt.__sub(a, b) precond.assert(vec2.is_vec2(a), "__add: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) precond.assert(vec2.is_vec2(b), "__add: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) return a:sub(b) end function vec2_mt.__mul(a, b) precond.assert(vec2.is_vec2(a), "__mul: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) assert(vec2.is_vec2(b) or type(b) == "number", "__mul: Wrong argument type for right hand operand. ( or expected)") if vec2.is_vec2(b) then return a:mul(b) end return a:scale(b) end function vec2_mt.__div(a, b) precond.assert(vec2.is_vec2(a), "__div: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) assert(vec2.is_vec2(b) or type(b) == "number", "__div: Wrong argument type for right hand operand. ( or expected)") if vec2.is_vec2(b) then return a:div(b) end return a:scale(1 / b) end if status then xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded ffi.metatype(new, vec2_mt) end, function() end) end return setmetatable({}, vec2_mt) ================================================ FILE: modules/vec3.lua ================================================ --- A 3 component vector. -- @module vec3 local modules = (...):gsub('%.[^%.]+$', '') .. "." local precond = require(modules .. "_private_precond") local private = require(modules .. "_private_utils") local sqrt = math.sqrt local cos = math.cos local sin = math.sin local vec3 = {} local vec3_mt = {} -- Private constructor. local function new(x, y, z) return setmetatable({ x = x or 0, y = y or 0, z = z or 0 }, vec3_mt) end -- Do the check to see if JIT is enabled. If so use the optimized FFI structs. local status, ffi if type(jit) == "table" and jit.status() then status, ffi = pcall(require, "ffi") if status then ffi.cdef "typedef struct { double x, y, z;} cpml_vec3;" new = ffi.typeof("cpml_vec3") end end --- Constants -- @table vec3 -- @field unit_x X axis of rotation -- @field unit_y Y axis of rotation -- @field unit_z Z axis of rotation -- @field zero Empty vector vec3.unit_x = new(1, 0, 0) vec3.unit_y = new(0, 1, 0) vec3.unit_z = new(0, 0, 1) vec3.zero = new(0, 0, 0) --- The public constructor. -- @param x Can be of three types:
-- number X component -- table {x, y, z} or {x=x, y=y, z=z} -- scalar To fill the vector eg. {x, x, x} -- @tparam number y Y component -- @tparam number z Z component -- @treturn vec3 out function vec3.new(x, y, z) -- number, number, number if x and y and z then precond.typeof(x, "number", "new: Wrong argument type for x") precond.typeof(y, "number", "new: Wrong argument type for y") precond.typeof(z, "number", "new: Wrong argument type for z") return new(x, y, z) -- {x, y, z} or {x=x, y=y, z=z} elseif type(x) == "table" or type(x) == "cdata" then -- table in vanilla lua, cdata in luajit local xx, yy, zz = x.x or x[1], x.y or x[2], x.z or x[3] precond.typeof(xx, "number", "new: Wrong argument type for x") precond.typeof(yy, "number", "new: Wrong argument type for y") precond.typeof(zz, "number", "new: Wrong argument type for z") return new(xx, yy, zz) -- number elseif type(x) == "number" then return new(x, x, x) else return new() end end --- Clone a vector. -- @tparam vec3 a Vector to be cloned -- @treturn vec3 out function vec3.clone(a) return new(a.x, a.y, a.z) end --- Add two vectors. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3 out function vec3.add(a, b) return new( a.x + b.x, a.y + b.y, a.z + b.z ) end --- Subtract one vector from another. -- Order: If a and b are positions, computes the direction and distance from b -- to a. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3 out function vec3.sub(a, b) return new( a.x - b.x, a.y - b.y, a.z - b.z ) end --- Multiply a vector by another vector. -- Component-wise multiplication not matrix multiplication. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3 out function vec3.mul(a, b) return new( a.x * b.x, a.y * b.y, a.z * b.z ) end --- Divide a vector by another. -- Component-wise inv multiplication. Like a non-uniform scale(). -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3 out function vec3.div(a, b) return new( a.x / b.x, a.y / b.y, a.z / b.z ) end --- Scale a vector to unit length (1). -- @tparam vec3 a vector to normalize -- @treturn vec3 out function vec3.normalize(a) if a:is_zero() then return new() end return a:scale(1 / a:len()) end --- Scale a vector to unit length (1), and return the input length. -- @tparam vec3 a vector to normalize -- @treturn vec3 out -- @treturn number input vector length function vec3.normalize_len(a) if a:is_zero() then return new(), 0 end local len = a:len() return a:scale(1 / len), len end --- Trim a vector to a given length -- @tparam vec3 a vector to be trimmed -- @tparam number len Length to trim the vector to -- @treturn vec3 out function vec3.trim(a, len) return a:normalize():scale(math.min(a:len(), len)) end --- Get the cross product of two vectors. -- Resulting direction is right-hand rule normal of plane defined by a and b. -- Magnitude is the area spanned by the parallelograms that a and b span. -- Order: Direction determined by right-hand rule. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3 out function vec3.cross(a, b) return new( a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x ) end --- Get the dot product of two vectors. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn number dot function vec3.dot(a, b) return a.x * b.x + a.y * b.y + a.z * b.z end --- Get the length of a vector. -- @tparam vec3 a Vector to get the length of -- @treturn number len function vec3.len(a) return sqrt(a.x * a.x + a.y * a.y + a.z * a.z) end --- Get the squared length of a vector. -- @tparam vec3 a Vector to get the squared length of -- @treturn number len function vec3.len2(a) return a.x * a.x + a.y * a.y + a.z * a.z end --- Get the distance between two vectors. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn number dist function vec3.dist(a, b) local dx = a.x - b.x local dy = a.y - b.y local dz = a.z - b.z return sqrt(dx * dx + dy * dy + dz * dz) end --- Get the squared distance between two vectors. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn number dist function vec3.dist2(a, b) local dx = a.x - b.x local dy = a.y - b.y local dz = a.z - b.z return dx * dx + dy * dy + dz * dz end --- Scale a vector by a scalar. -- @tparam vec3 a Left hand operand -- @tparam number b Right hand operand -- @treturn vec3 out function vec3.scale(a, b) return new( a.x * b, a.y * b, a.z * b ) end --- Rotate vector about an axis. -- @tparam vec3 a Vector to rotate -- @tparam number phi Angle to rotate vector by (in radians) -- @tparam vec3 axis Axis to rotate by -- @treturn vec3 out function vec3.rotate(a, phi, axis) if not vec3.is_vec3(axis) then return a end local u = axis:normalize() local c = cos(phi) local s = sin(phi) -- Calculate generalized rotation matrix 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)) 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)) 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)) ) return new( a:dot(m1), a:dot(m2), a:dot(m3) ) end --- Get the perpendicular vector of a vector. -- @tparam vec3 a Vector to get perpendicular axes from -- @treturn vec3 out function vec3.perpendicular(a) return new(-a.y, a.x, 0) end --- Lerp between two vectors. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @tparam number s Step value -- @treturn vec3 out function vec3.lerp(a, b, s) return a + (b - a) * s end -- Round all components to nearest int (or other precision). -- @tparam vec3 a Vector to round. -- @tparam precision Digits after the decimal (round numebr if unspecified) -- @treturn vec3 Rounded vector function vec3.round(a, precision) return vec3.new(private.round(a.x, precision), private.round(a.y, precision), private.round(a.z, precision)) end --- Unpack a vector into individual components. -- @tparam vec3 a Vector to unpack -- @treturn number x -- @treturn number y -- @treturn number z function vec3.unpack(a) return a.x, a.y, a.z end --- Return the component-wise minimum of two vectors. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3 A vector where each component is the lesser value for that component between the two given vectors. function vec3.component_min(a, b) return new(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z)) end --- Return the component-wise maximum of two vectors. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3 A vector where each component is the lesser value for that component between the two given vectors. function vec3.component_max(a, b) return new(math.max(a.x, b.x), math.max(a.y, b.y), math.max(a.z, b.z)) end --- Return the component-wise minimum and maximum of two vectors. -- @tparam vec3 a Left hand operand -- @tparam vec3 b Right hand operand -- @treturn vec3, vec3 sorted vectors function vec3.component_sort(a, b) return vec3.component_min(a, b), vec3.component_max(a, b) end -- Negate x axis only of vector. -- @tparam vec3 a Vector to x-flip. -- @treturn vec3 x-flipped vector function vec3.flip_x(a) return vec3.new(-a.x, a.y, a.z) end -- Negate y axis only of vector. -- @tparam vec3 a Vector to y-flip. -- @treturn vec3 y-flipped vector function vec3.flip_y(a) return vec3.new(a.x, -a.y, a.z) end -- Negate z axis only of vector. -- @tparam vec3 a Vector to z-flip. -- @treturn vec3 z-flipped vector function vec3.flip_z(a) return vec3.new(a.x, a.y, -a.z) end function vec3.angle_to(a, b) local v = a:normalize():dot(b:normalize()) return math.acos(v) end --- Return a boolean showing if a table is or is not a vec3. -- @tparam vec3 a Vector to be tested -- @treturn boolean is_vec3 function vec3.is_vec3(a) if type(a) == "cdata" then return ffi.istype("cpml_vec3", a) end return type(a) == "table" and type(a.x) == "number" and type(a.y) == "number" and type(a.z) == "number" end --- Return a boolean showing if a table is or is not a zero vec3. -- @tparam vec3 a Vector to be tested -- @treturn boolean is_zero function vec3.is_zero(a) return a.x == 0 and a.y == 0 and a.z == 0 end --- Return whether any component is NaN -- @tparam vec3 a Vector to be tested -- @treturn boolean if x,y, or z are nan function vec3.has_nan(a) return private.is_nan(a.x) or private.is_nan(a.y) or private.is_nan(a.z) end --- Return a formatted string. -- @tparam vec3 a Vector to be turned into a string -- @treturn string formatted function vec3.to_string(a) return string.format("(%+0.3f,%+0.3f,%+0.3f)", a.x, a.y, a.z) end vec3_mt.__index = vec3 vec3_mt.__tostring = vec3.to_string function vec3_mt.__call(_, x, y, z) return vec3.new(x, y, z) end function vec3_mt.__unm(a) return new(-a.x, -a.y, -a.z) end function vec3_mt.__eq(a, b) if not vec3.is_vec3(a) or not vec3.is_vec3(b) then return false end return a.x == b.x and a.y == b.y and a.z == b.z end function vec3_mt.__add(a, b) precond.assert(vec3.is_vec3(a), "__add: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) precond.assert(vec3.is_vec3(b), "__add: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) return a:add(b) end function vec3_mt.__sub(a, b) precond.assert(vec3.is_vec3(a), "__sub: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) precond.assert(vec3.is_vec3(b), "__sub: Wrong argument type '%s' for right hand operand. ( expected)", type(b)) return a:sub(b) end function vec3_mt.__mul(a, b) precond.assert(vec3.is_vec3(a), "__mul: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) precond.assert(vec3.is_vec3(b) or type(b) == "number", "__mul: Wrong argument type '%s' for right hand operand. ( or expected)", type(b)) if vec3.is_vec3(b) then return a:mul(b) end return a:scale(b) end function vec3_mt.__div(a, b) precond.assert(vec3.is_vec3(a), "__div: Wrong argument type '%s' for left hand operand. ( expected)", type(a)) precond.assert(vec3.is_vec3(b) or type(b) == "number", "__div: Wrong argument type '%s' for right hand operand. ( or expected)", type(b)) if vec3.is_vec3(b) then return a:div(b) end return a:scale(1 / b) end if status then xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded ffi.metatype(new, vec3_mt) end, function() end) end return setmetatable({}, vec3_mt) ================================================ FILE: spec/bound2_spec.lua ================================================ local bound2 = require "modules.bound2" local vec2 = require "modules.vec2" local DBL_EPSILON = require("modules.constants").DBL_EPSILON describe("bound2:", function() it("creates an empty bound2", function() local a = bound2() assert.is.equal(0, a.min.x) assert.is.equal(0, a.min.y) assert.is.equal(0, a.max.x) assert.is.equal(0, a.max.y) end) it("creates a bound2 from vec2s", function() local a = bound2(vec2(1,2), vec2(4,5)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) end) it("creates a bound2 using new()", function() local a = bound2.new(vec2(1,2), vec2(4,5)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) end) it("creates a bound2 using at()", function() local a = bound2.at(vec2(4,5), vec2(1,2)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) end) it("clones a bound2", function() local a = bound2(vec2(1,2), vec2(4,5)) local b = a:clone() a.max = vec2.new(9,9) assert.is.equal(a.min, b.min) assert.is.not_equal(a.max, b.max) end) it("uses bound2 check()", function() local a = bound2(vec2(4,2), vec2(1,5)):check() assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) end) it("queries a bound2 size", function() local a = bound2(vec2(1,2), vec2(4,6)) local v = a:size() local r = a:radius() assert.is.equal(3, v.x) assert.is.equal(4, v.y) assert.is.equal(1.5, r.x) assert.is.equal(2, r.y) end) it("sets a bound2 size", function() local a = bound2(vec2(1,2), vec2(4,5)) local b = a:with_size(vec2(1,1)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) assert.is.equal(1, b.min.x) assert.is.equal(2, b.min.y) assert.is.equal(2, b.max.x) assert.is.equal(3, b.max.y) end) it("queries a bound2 center", function() local a = bound2(vec2(1,2), vec2(3,4)) local v = a:center() assert.is.equal(2, v.x) assert.is.equal(3, v.y) end) it("sets a bound2 center", function() local a = bound2(vec2(1,2), vec2(3,4)) local b = a:with_center(vec2(1,1)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.max.x) assert.is.equal(4, a.max.y) assert.is.equal(0, b.min.x) assert.is.equal(0, b.min.y) assert.is.equal(2, b.max.x) assert.is.equal(2, b.max.y) end) it("sets a bound2 size centered", function() local a = bound2(vec2(1,2), vec2(3,4)) local b = a:with_size_centered(vec2(4,4)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.max.x) assert.is.equal(4, a.max.y) assert.is.equal(0, b.min.x) assert.is.equal(1, b.min.y) assert.is.equal(4, b.max.x) assert.is.equal(5, b.max.y) end) it("insets a bound2", function() local a = bound2(vec2(1,2), vec2(5,10)) local b = a:inset(vec2(1,2)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(5, a.max.x) assert.is.equal(10, a.max.y) assert.is.equal(2, b.min.x) assert.is.equal(4, b.min.y) assert.is.equal(4, b.max.x) assert.is.equal(8, b.max.y) end) it("outsets a bound2", function() local a = bound2(vec2(1,2), vec2(5,6)) local b = a:outset(vec2(1,2)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(5, a.max.x) assert.is.equal(6, a.max.y) assert.is.equal(0, b.min.x) assert.is.equal(0, b.min.y) assert.is.equal(6, b.max.x) assert.is.equal(8, b.max.y) end) it("offsets a bound2", function() local a = bound2(vec2(1,2), vec2(5,6)) local b = a:offset(vec2(1,2)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(5, a.max.x) assert.is.equal(6, a.max.y) assert.is.equal(2, b.min.x) assert.is.equal(4, b.min.y) assert.is.equal(6, b.max.x) assert.is.equal(8, b.max.y) end) it("tests for points inside bound2", function() local a = bound2(vec2(1,2), vec2(4,5)) assert.is_true(a:contains(vec2(1,2))) assert.is_true(a:contains(vec2(4,5))) assert.is_true(a:contains(vec2(2,3))) assert.is_not_true(a:contains(vec2(0,3))) assert.is_not_true(a:contains(vec2(5,3))) assert.is_not_true(a:contains(vec2(2,1))) assert.is_not_true(a:contains(vec2(2,6))) end) it("rounds a bound2", function() local a = bound2(vec2(1.1,1.9), vec2(3.9,5.1)):round() assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) end) it("extends a bound2 with a point", function() local min = vec2(1,2) local max = vec2(4,5) local downright = vec2(8,8) local downleft = vec2(-4,8) local top = vec2(2, 0) local a = bound2(min, max) local temp temp = a:extend(downright) assert.is_true(a.min == min and a.max == max) assert.is_true(temp.min == min and temp.max == downright) temp = a:extend(downleft) assert.is_true(temp.min == vec2(-4,2) and temp.max == vec2(4,8)) temp = a:extend(top) assert.is_true(temp.min == vec2(1,0) and temp.max == max) end) it("extends a bound with another bound", function() local min = vec2(1,2) local max = vec2(4,5) local leftexpand = bound2.new(vec2(0,0), vec2(1.5, 6)) local rightexpand = bound2.new(vec2(1.5,0), vec2(5, 6)) local a = bound2(min, max) local temp temp = a:extend_bound(leftexpand) assert.is_equal(temp.min, vec2(0,0)) assert.is_equal(temp.max, vec2(4,6)) temp = temp:extend_bound(rightexpand) assert.is_equal(temp.min, vec2(0,0)) assert.is_equal(temp.max, vec2(5,6)) end) it("checks for bound2.zero", function() assert.is.equal(0, bound2.zero.min.x) assert.is.equal(0, bound2.zero.min.y) assert.is.equal(0, bound2.zero.max.x) assert.is.equal(0, bound2.zero.max.y) end) end) ================================================ FILE: spec/bound3_spec.lua ================================================ local bound3 = require "modules.bound3" local vec3 = require "modules.vec3" local DBL_EPSILON = require("modules.constants").DBL_EPSILON describe("bound3:", function() it("creates an empty bound3", function() local a = bound3() assert.is.equal(0, a.min.x) assert.is.equal(0, a.min.y) assert.is.equal(0, a.min.z) assert.is.equal(0, a.max.x) assert.is.equal(0, a.max.y) assert.is.equal(0, a.max.z) end) it("creates a bound3 from vec3s", function() local a = bound3(vec3(1,2,3), vec3(4,5,6)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) assert.is.equal(6, a.max.z) end) it("creates a bound3 using new()", function() local a = bound3.new(vec3(1,2,3), vec3(4,5,6)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) assert.is.equal(6, a.max.z) end) it("creates a bound3 using at()", function() local a = bound3.at(vec3(4,5,6), vec3(1,2,3)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) assert.is.equal(6, a.max.z) end) it("clones a bound3", function() local a = bound3(vec3(1,2,3), vec3(4,5,6)) local b = a:clone() a.max = vec3.new(9,9,9) assert.is.equal(a.min, b.min) assert.is.not_equal(a.max, b.max) end) it("uses bound3 check()", function() local a = bound3(vec3(4,2,6), vec3(1,5,3)):check() assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) assert.is.equal(6, a.max.z) end) it("queries a bound3 size", function() local a = bound3(vec3(1,2,3), vec3(4,6,8)) local v = a:size() local r = a:radius() assert.is.equal(3, v.x) assert.is.equal(4, v.y) assert.is.equal(5, v.z) assert.is.equal(1.5, r.x) assert.is.equal(2, r.y) assert.is.equal(2.5, r.z) end) it("sets a bound3 size", function() local a = bound3(vec3(1,2,3), vec3(4,5,6)) local b = a:with_size(vec3(1,1,1)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) assert.is.equal(6, a.max.z) assert.is.equal(1, b.min.x) assert.is.equal(2, b.min.y) assert.is.equal(3, b.min.z) assert.is.equal(2, b.max.x) assert.is.equal(3, b.max.y) assert.is.equal(4, b.max.z) end) it("queries a bound3 center", function() local a = bound3(vec3(1,2,3), vec3(3,4,5)) local v = a:center() assert.is.equal(2, v.x) assert.is.equal(3, v.y) assert.is.equal(4, v.z) end) it("sets a bound3 center", function() local a = bound3(vec3(1,2,3), vec3(3,4,5)) local b = a:with_center(vec3(1,1,1)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(3, a.max.x) assert.is.equal(4, a.max.y) assert.is.equal(5, a.max.z) assert.is.equal(0, b.min.x) assert.is.equal(0, b.min.y) assert.is.equal(0, b.min.z) assert.is.equal(2, b.max.x) assert.is.equal(2, b.max.y) assert.is.equal(2, b.max.z) end) it("sets a bound3 size centered", function() local a = bound3(vec3(1,2,3), vec3(3,4,5)) local b = a:with_size_centered(vec3(4,4,4)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(3, a.max.x) assert.is.equal(4, a.max.y) assert.is.equal(5, a.max.z) assert.is.equal(0, b.min.x) assert.is.equal(1, b.min.y) assert.is.equal(2, b.min.z) assert.is.equal(4, b.max.x) assert.is.equal(5, b.max.y) assert.is.equal(6, b.max.z) end) it("insets a bound3", function() local a = bound3(vec3(1,2,3), vec3(5,10,11)) local b = a:inset(vec3(1,2,3)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(5, a.max.x) assert.is.equal(10, a.max.y) assert.is.equal(11, a.max.z) assert.is.equal(2, b.min.x) assert.is.equal(4, b.min.y) assert.is.equal(6, b.min.z) assert.is.equal(4, b.max.x) assert.is.equal(8, b.max.y) assert.is.equal(8, b.max.z) end) it("outsets a bound3", function() local a = bound3(vec3(1,2,3), vec3(5,6,7)) local b = a:outset(vec3(1,2,3)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(5, a.max.x) assert.is.equal(6, a.max.y) assert.is.equal(7, a.max.z) assert.is.equal(0, b.min.x) assert.is.equal(0, b.min.y) assert.is.equal(0, b.min.z) assert.is.equal(6, b.max.x) assert.is.equal(8, b.max.y) assert.is.equal(10, b.max.z) end) it("offsets a bound3", function() local a = bound3(vec3(1,2,3), vec3(5,6,7)) local b = a:offset(vec3(1,2,3)) assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(5, a.max.x) assert.is.equal(6, a.max.y) assert.is.equal(7, a.max.z) assert.is.equal(2, b.min.x) assert.is.equal(4, b.min.y) assert.is.equal(6, b.min.z) assert.is.equal(6, b.max.x) assert.is.equal(8, b.max.y) assert.is.equal(10, b.max.z) end) it("tests for points inside bound3", function() local a = bound3(vec3(1,2,3), vec3(4,5,6)) assert.is_true(a:contains(vec3(1,2,3))) assert.is_true(a:contains(vec3(4,5,6))) assert.is_true(a:contains(vec3(2,3,4))) assert.is_not_true(a:contains(vec3(0,3,4))) assert.is_not_true(a:contains(vec3(5,3,4))) assert.is_not_true(a:contains(vec3(2,1,4))) assert.is_not_true(a:contains(vec3(2,6,4))) assert.is_not_true(a:contains(vec3(2,3,2))) assert.is_not_true(a:contains(vec3(2,3,7))) end) it("rounds a bound3", function() local a = bound3(vec3(1.1,1.9,3), vec3(3.9,5.1,6)):round() assert.is.equal(1, a.min.x) assert.is.equal(2, a.min.y) assert.is.equal(3, a.min.z) assert.is.equal(4, a.max.x) assert.is.equal(5, a.max.y) assert.is.equal(6, a.max.z) end) it("extends a bound3 with a point", function() local min = vec3(1,2,6) local max = vec3(4,5,9) local downright = vec3(8,8,10) local downleft = vec3(-4,8,10) local top = vec3(2, 0, 7) local a = bound3(min, max) local temp temp = a:extend(downright) assert.is_true(a.min == min and a.max == max) assert.is_true(temp.min == min and temp.max == downright) temp = a:extend(downleft) assert.is_true(temp.min == vec3(-4,2,6) and temp.max == vec3(4,8,10)) temp = a:extend(top) assert.is_true(temp.min == vec3(1,0,6) and temp.max == max) end) it("extends a bound with another bound", function() local min = vec3(1,2,3) local max = vec3(4,5,6) local leftexpand = bound3.new(vec3(0,0,4), vec3(1.5,6,5)) local rightexpand = bound3.new(vec3(1.5,0,1), vec3(5,6,7)) local a = bound3(min, max) local temp temp = a:extend_bound(leftexpand) assert.is_equal(temp.min, vec3(0,0,3)) assert.is_equal(temp.max, vec3(4,6,6)) temp = temp:extend_bound(rightexpand) assert.is_equal(temp.min, vec3(0,0,1)) assert.is_equal(temp.max, vec3(5,6,7)) end) it("checks for bound3.zero", function() assert.is.equal(0, bound3.zero.min.x) assert.is.equal(0, bound3.zero.min.y) assert.is.equal(0, bound3.zero.min.z) assert.is.equal(0, bound3.zero.max.x) assert.is.equal(0, bound3.zero.max.y) assert.is.equal(0, bound3.zero.max.z) end) end) ================================================ FILE: spec/color_spec.lua ================================================ local color = require "modules.color" local DBL_EPSILON = require("modules.constants").DBL_EPSILON local function assert_is_float_equal(a, b) if math.abs(a - b) > DBL_EPSILON then assert.is.equal(a, b) end end local function assert_is_approx_equal(a, b) if math.abs(a - b) > 0.001 then assert.is.equal(a, b) end end describe("color:", function() it("operators: add, subract, multiply", function() local c = color(1, 1, 1, 1) assert.is_true(c:is_color()) local r = c + c assert.is_true(r:is_color()) assert_is_float_equal(r[1], 2) assert_is_float_equal(r[2], 2) assert_is_float_equal(r[3], 2) r = c - c assert.is_true(r:is_color()) assert_is_float_equal(r[1], 0) assert_is_float_equal(r[2], 0) assert_is_float_equal(r[3], 0) r = c * 5 assert.is_true(r:is_color()) assert_is_float_equal(r[1], 5) assert_is_float_equal(r[2], 5) assert_is_float_equal(r[3], 5) end) it("rgb -> hsv -> rgb", function() local c = color(1,1,1,1) local hsv = c:color_to_hsv_table() local c1 = color.hsv_to_color_table(hsv) local c2 = color.from_hsva(hsv[1], hsv[2], hsv[3], hsv[4]) local c3 = color.from_hsv(hsv[1], hsv[2], hsv[3]) c3[4] = c[4] for i=1,4 do assert_is_float_equal(c[i], c1[i]) assert_is_float_equal(c[i], c2[i]) assert_is_float_equal(c[i], c3[i]) end assert.is_true(c:is_color()) assert.is_true(c1:is_color()) assert.is_true(c2:is_color()) assert.is_true(c3:is_color()) end) it("hsv -> rgb -> hsv", function() local hsv1 = { 0, 0.3, 0.8, 0.9 } for h=0,1, 0.1 do hsv1[1] = h local cc = color.hsv_to_color_table(hsv1) local hsv2 = cc:color_to_hsv_table() for i=1,4 do assert_is_approx_equal(hsv1[i], hsv2[i]) end end end) it("unpack", function() local c = color(122/255, 20/255, 122/255, 255/255) local r, g, b, a = c:unpack() assert_is_float_equal(c[1], r) assert_is_float_equal(c[2], g) assert_is_float_equal(c[3], b) assert_is_float_equal(c[4], a) r, g, b, a = c:as_255() assert_is_float_equal(122, r) assert_is_float_equal(20, g) assert_is_float_equal(122, b) assert_is_float_equal(255, a) end) it("set hsv", function() -- hsv value conversion values from http://colorizer.org/ local c = color(122/255, 20/255, 122/255, 1) local hsv = c:color_to_hsv_table() assert_is_approx_equal(hsv[1], 300/360) assert_is_approx_equal(hsv[2], 0.8361) assert_is_approx_equal(hsv[3], 0.4784) local r = c:hue(200/360) assert_is_approx_equal(r[1], 20/255) assert_is_approx_equal(r[2], 88/255) assert_is_approx_equal(r[3], 122/255) r = c:saturation(0.2) assert_is_approx_equal(r[1], 122/255) assert_is_approx_equal(r[2], 97.6/255) assert_is_approx_equal(r[3], 122/255) r = c:value(0.2) assert_is_approx_equal(r[1], 51/255) assert_is_approx_equal(r[2], 8.36/255) assert_is_approx_equal(r[3], 51/255) end) it("lighten a color", function() local c = color(0, 0, 0, 0) local r = c:lighten(0.1) assert.is.equal(r[1], 0.1) r = c:lighten(1000) assert.is.equal(r[1], 1) end) it("darken a color", function() local c = color(1, 1, 1, 1) local r = c:darken(0.04) assert.is.equal(r[1], 0.96) r = c:darken(1000) assert.is.equal(r[1], 0) end) it("multiply a color by a scalar", function() local c = color(1, 1, 1, 1) local r = c:multiply(0.04) assert.is.equal(r[1], 0.04) r = c:multiply(0) for i=1,3 do assert.is.equal(0, r[i]) end assert.is.equal(1, r[4]) end) it("modify alpha", function() local c = color(1, 1, 1, 1) local r = c:alpha(0.1) assert.is.equal(r[4], 0.1) r = c:opacity(0.5) assert.is.equal(r[4], 0.5) r = c:opacity(0.5) :opacity(0.5) assert.is.equal(r[4], 0.25) end) it("invert", function() local c = color(1, 0.6, 0.25, 1) local r = c:invert() assert_is_float_equal(r[1], 0) assert_is_float_equal(r[2], 0.4) assert_is_float_equal(r[3], 0.75) assert_is_float_equal(r[4], 1) r = c:invert() :invert() for i=1,4 do assert.is.equal(c[i], r[i]) end end) it("lerp", function() local a = color(1, 0.6, 0.25, 1) local b = color(1, 0.8, 0.75, 0.5) local r = a:lerp(b, 0.5) assert_is_float_equal(r[1], 1) assert_is_float_equal(r[2], 0.7) assert_is_float_equal(r[3], 0.5) assert_is_float_equal(r[4], 0.75) local r_a = a:lerp(b, 0) local r_b = a:lerp(b, 1) for i=1,4 do assert.is.equal(a[i], r_a[i]) assert.is.equal(b[i], r_b[i]) end end) it("linear_to_gamma -> gamma_to_linear round trip", function() local c = color(0.25, 0.25, 0.25, 1) local r = color.gamma_to_linear(c:linear_to_gamma()) for i=1,4 do assert_is_approx_equal(c[i], r[i]) end end) end) --[[ to_string(a) --]] ================================================ FILE: spec/intersect_spec.lua ================================================ local intersect = require "modules.intersect" local vec3 = require "modules.vec3" local mat4 = require "modules.mat4" describe("intersect:", function() it("intersects a point with a triangle", function() local a = vec3() local b = vec3(0, 0, 5) local c = { vec3(-1, -1, 0), vec3( 1, -1, 0), vec3( 0.5, 1, 0) } assert.is_true(intersect.point_triangle(a, c)) assert.is_not_true(intersect.point_triangle(b, c)) end) it("intersects a point with an aabb", function() local a = vec3() local b = vec3(0, 0, 5) local c = { min = vec3(-1), max = vec3( 1) } assert.is_true(intersect.point_aabb(a, c)) assert.is_not_true(intersect.point_aabb(b, c)) end) it("intersects a point with a frustum", function() pending("TODO") end) it("intersects a ray with a triangle", function() local a = { position = vec3(0.5, 0.5, -1), direction = vec3(0, 0, 1) } local b = { position = vec3(0.5, 0.5, -1), direction = vec3(0, 0, -1) } local c = { vec3(-1, -1, 0), vec3( 1, -1, 0), vec3( 0.5, 1, 0) } assert.is_true(vec3.is_vec3(intersect.ray_triangle(a, c))) assert.is_not_true(intersect.ray_triangle(b, c)) end) it("intersects a ray with a sphere", function() local a = { position = vec3(0, 0, -2), direction = vec3(0, 0, 1) } local b = { position = vec3(0, 0, -2), direction = vec3(0, 0, -1) } local c = { position = vec3(), radius = 1 } local w, x = intersect.ray_sphere(a, c) local y, z = intersect.ray_sphere(b, c) assert.is_true(vec3.is_vec3(w)) assert.is_not_true(y) end) it("intersects a ray with an aabb", function() local a = { position = vec3(0, 0, -2), direction = vec3(0, 0, 1) } local b = { position = vec3(0, 0, -2), direction = vec3(0, 0, -1) } local c = { min = vec3(-1), max = vec3( 1) } local w, x = intersect.ray_aabb(a, c) local y, z = intersect.ray_aabb(b, c) assert.is_true(vec3.is_vec3(w)) assert.is_not_true(y) end) it("intersects a ray with a plane", function() local a = { position = vec3(0, 0, 1), direction = vec3(0, 0, -1) } local b = { position = vec3(0, 0, 1), direction = vec3(0, 0, 1) } local c = { position = vec3(), normal = vec3(0, 0, 1) } local w, x = intersect.ray_plane(a, c) local y, z = intersect.ray_plane(b, c) assert.is_true(vec3.is_vec3(w)) assert.is_not_true(y) end) it("intersects a line with a line", function() local a = { vec3(0, 0, -1), vec3(0, 0, 1) } local b = { vec3(0, 0, -1), vec3(0, 1, -1) } local c = { vec3(-1, 0, 0), vec3( 1, 0, 0) } local w, x = intersect.line_line(a, c, 0.001) local y, z = intersect.line_line(b, c, 0.001) local u, v = intersect.line_line(b, c) assert.is_truthy(w) assert.is_not_truthy(y) assert.is_truthy(u) end) it("intersects a segment with a segment", function() local a = { vec3(0, 0, -1), vec3(0, 0, 1) } local b = { vec3(0, 0, -1), vec3(0, 1, -1) } local c = { vec3(-1, 0, 0), vec3( 1, 0, 0) } local w, x = intersect.segment_segment(a, c, 0.001) local y, z = intersect.segment_segment(b, c, 0.001) local u, v = intersect.segment_segment(b, c) assert.is_truthy(w) assert.is_not_truthy(y) assert.is_truthy(u) end) it("intersects an aabb with an aabb", function() local a = { min = vec3(-1), max = vec3( 1) } local b = { min = vec3(-5), max = vec3(-3) } local c = { min = vec3(), max = vec3(2) } assert.is_true(intersect.aabb_aabb(a, c)) assert.is_not_true(intersect.aabb_aabb(b, c)) end) it("intersects an aabb with an obb", function() local r = mat4():rotate(mat4(), math.pi / 4, vec3.unit_z) local a = { position = vec3(), extent = vec3(0.5) } local b = { position = vec3(), extent = vec3(0.5), rotation = r } local c = { position = vec3(0, 0, 2), extent = vec3(0.5), rotation = r } assert.is_true(vec3.is_vec3(intersect.aabb_obb(a, b))) assert.is_not_true(intersect.aabb_obb(a, c)) end) it("intersects an aabb with a sphere", function() local a = { min = vec3(-1), max = vec3( 1) } local b = { min = vec3(-5), max = vec3(-3) } local c = { position = vec3(0, 0, 3), radius = 3 } assert.is_true(intersect.aabb_sphere(a, c)) assert.is_not_true(intersect.aabb_sphere(b, c)) end) it("intersects an aabb with a frustum", function() pending("TODO") end) it("encapsulates an aabb", function() local a = { min = vec3(-1), max = vec3( 1) } local b = { min = vec3(-1.5), max = vec3( 1.5) } local c = { min = vec3(-0.5), max = vec3( 0.5) } local d = { min = vec3(-1), max = vec3( 1) } assert.is_true(intersect.encapsulate_aabb(a, d)) assert.is_true(intersect.encapsulate_aabb(b, d)) assert.is_not_true(intersect.encapsulate_aabb(c, d)) end) it("intersects a circle with a circle", function() local a = { position = vec3(0, 0, 6), radius = 3 } local b = { position = vec3(0, 0, 7), radius = 3 } local c = { position = vec3(), radius = 3 } assert.is_true(intersect.circle_circle(a, c)) assert.is_not_true(intersect.circle_circle(b, c)) end) it("intersects a sphere with a sphere", function() local a = { position = vec3(0, 0, 6), radius = 3 } local b = { position = vec3(0, 0, 7), radius = 3 } local c = { position = vec3(), radius = 3 } assert.is_true(intersect.sphere_sphere(a, c)) assert.is_not_true(intersect.sphere_sphere(b, c)) end) it("intersects a sphere with a frustum", function() pending("TODO") end) it("intersects a capsule with another capsule", function() pending("TODO") end) end) ================================================ FILE: spec/mat4_spec.lua ================================================ local mat4 = require "modules.mat4" local vec3 = require "modules.vec3" local quat = require "modules.quat" local utils = require "modules.utils" local FLT_EPSILON = require("modules.constants").FLT_EPSILON describe("mat4:", function() it("creates an identity matrix", function() local a = mat4() assert.is.equal(1, a[1]) assert.is.equal(0, a[2]) assert.is.equal(0, a[3]) assert.is.equal(0, a[4]) assert.is.equal(0, a[5]) assert.is.equal(1, a[6]) assert.is.equal(0, a[7]) assert.is.equal(0, a[8]) assert.is.equal(0, a[9]) assert.is.equal(0, a[10]) assert.is.equal(1, a[11]) assert.is.equal(0, a[12]) assert.is.equal(0, a[13]) assert.is.equal(0, a[14]) assert.is.equal(0, a[15]) assert.is.equal(1, a[16]) assert.is_true(a:is_mat4()) end) it("creates a filled matrix", function() local a = mat4 { 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6 } assert.is.equal(3, a[1]) assert.is.equal(3, a[2]) assert.is.equal(3, a[3]) assert.is.equal(3, a[4]) assert.is.equal(4, a[5]) assert.is.equal(4, a[6]) assert.is.equal(4, a[7]) assert.is.equal(4, a[8]) assert.is.equal(5, a[9]) assert.is.equal(5, a[10]) assert.is.equal(5, a[11]) assert.is.equal(5, a[12]) assert.is.equal(6, a[13]) assert.is.equal(6, a[14]) assert.is.equal(6, a[15]) assert.is.equal(6, a[16]) end) it("creates a filled matrix from vec4s", function() local a = mat4 { { 3, 3, 3, 3 }, { 4, 4, 4, 4 }, { 5, 5, 5, 5 }, { 6, 6, 6, 6 } } assert.is.equal(3, a[1]) assert.is.equal(3, a[2]) assert.is.equal(3, a[3]) assert.is.equal(3, a[4]) assert.is.equal(4, a[5]) assert.is.equal(4, a[6]) assert.is.equal(4, a[7]) assert.is.equal(4, a[8]) assert.is.equal(5, a[9]) assert.is.equal(5, a[10]) assert.is.equal(5, a[11]) assert.is.equal(5, a[12]) assert.is.equal(6, a[13]) assert.is.equal(6, a[14]) assert.is.equal(6, a[15]) assert.is.equal(6, a[16]) end) it("creates a filled matrix from a 3x3 matrix", function() local a = mat4 { 3, 3, 3, 4, 4, 4, 5, 5, 5 } assert.is.equal(3, a[1]) assert.is.equal(3, a[2]) assert.is.equal(3, a[3]) assert.is.equal(0, a[4]) assert.is.equal(4, a[5]) assert.is.equal(4, a[6]) assert.is.equal(4, a[7]) assert.is.equal(0, a[8]) assert.is.equal(5, a[9]) assert.is.equal(5, a[10]) assert.is.equal(5, a[11]) assert.is.equal(0, a[12]) assert.is.equal(0, a[13]) assert.is.equal(0, a[14]) assert.is.equal(0, a[15]) assert.is.equal(1, a[16]) end) it("creates a matrix from perspective", function() local a = mat4.from_perspective(45, 1, 0.1, 1000) assert.is_true(utils.tolerance( 2.414-a[1], 0.001)) assert.is_true(utils.tolerance( 2.414-a[6], 0.001)) assert.is_true(utils.tolerance(-1 -a[11], 0.001)) assert.is_true(utils.tolerance(-1 -a[12], 0.001)) assert.is_true(utils.tolerance(-0.2 -a[15], 0.001)) end) it("creates a matrix from HMD perspective", function() local t = { LeftTan = 2.3465312, RightTan = 0.9616399, UpTan = 2.8664987, DownTan = 2.8664987 } local a = mat4.from_hmd_perspective(t, 0.1, 1000, false, false) assert.is_true(utils.tolerance(a[1] - 0.605, 0.001)) assert.is_true(utils.tolerance(a[6] - 0.349, 0.001)) assert.is_true(utils.tolerance(a[9] - -0.419, 0.001)) assert.is_true(utils.tolerance(a[11]- -1.000, 0.001)) assert.is_true(utils.tolerance(a[12]- -1.000, 0.001)) assert.is_true(utils.tolerance(a[15]- -0.200, 0.001)) end) it("clones a matrix", function() local a = mat4.identity() local b = a:clone() assert.is.equal(a, b) end) it("multiplies two 4x4 matrices", function() local a = mat4 { 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16 } local b = mat4 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 } local c = mat4():mul(a, b) local d = a * b local e = mat4():mul{a, b} assert.is.equal(30, c[1]) assert.is.equal(70, c[2]) assert.is.equal(110, c[3]) assert.is.equal(150, c[4]) assert.is.equal(70, c[5]) assert.is.equal(174, c[6]) assert.is.equal(278, c[7]) assert.is.equal(382, c[8]) assert.is.equal(110, c[9]) assert.is.equal(278, c[10]) assert.is.equal(446, c[11]) assert.is.equal(614, c[12]) assert.is.equal(150, c[13]) assert.is.equal(382, c[14]) assert.is.equal(614, c[15]) assert.is.equal(846, c[16]) assert.is.equal(c, d) assert.is.equal(c, e) end) it("multiplies a matrix and a vec4", function() local a = mat4 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 } local b = { 10, 20, 30, 40 } local c = mat4.mul_vec4(mat4(), a, b) local d = a * b assert.is.equal(900, c[1]) assert.is.equal(1000, c[2]) assert.is.equal(1100, c[3]) assert.is.equal(1200, c[4]) assert.is.equal(c[1], d[1]) assert.is.equal(c[2], d[2]) assert.is.equal(c[3], d[3]) assert.is.equal(c[4], d[4]) end) it("verifies mat4 composition order", function() local a = mat4 { 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16 } local b = mat4 { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53 } local c = mat4():mul(a, b) local d = a * b local v = { 10, 20, 30, 40 } local cv = c * v local abv = a*(b*v) assert.is.equal(cv.x, abv.x) -- Verify (a*b)*v == a*(b*v) assert.is.equal(cv.y, abv.y) assert.is.equal(cv.z, abv.z) end) it("scales a matrix", function() local a = mat4():scale(mat4(), vec3(5, 5, 5)) assert.is.equal(5, a[1]) assert.is.equal(5, a[6]) assert.is.equal(5, a[11]) end) it("rotates a matrix", function() local a = mat4():rotate(mat4(), math.rad(45), vec3.unit_z) assert.is_true(utils.tolerance( 0.7071-a[1], 0.001)) assert.is_true(utils.tolerance( 0.7071-a[2], 0.001)) assert.is_true(utils.tolerance(-0.7071-a[5], 0.001)) assert.is_true(utils.tolerance( 0.7071-a[6], 0.001)) end) it("translates a matrix", function() local a = mat4():translate(mat4(), vec3(5, 5, 5)) assert.is.equal(5, a[13]) assert.is.equal(5, a[14]) assert.is.equal(5, a[15]) end) it("inverts a matrix", function() local a = mat4() a = a:rotate(a, math.pi/4, vec3.unit_y) a = a:translate(a, vec3(4, 5, 6)) local b = mat4.invert(mat4(), a) local c = a * b assert.is.equal(mat4(), c) local d = mat4() d:rotate(d, math.pi/4, vec3.unit_y) d:translate(d, vec3(4, 5, 6)) local e = -d local f = d * e assert.is.equal(mat4(), f) end) it("transposes a matrix", function() local a = mat4({ 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4 }) a = a:transpose(a) assert.is.equal(1, a[1]) assert.is.equal(2, a[2]) assert.is.equal(3, a[3]) assert.is.equal(4, a[4]) assert.is.equal(1, a[5]) assert.is.equal(2, a[6]) assert.is.equal(3, a[7]) assert.is.equal(4, a[8]) assert.is.equal(1, a[9]) assert.is.equal(2, a[10]) assert.is.equal(3, a[11]) assert.is.equal(4, a[12]) assert.is.equal(1, a[13]) assert.is.equal(2, a[14]) assert.is.equal(3, a[15]) assert.is.equal(4, a[16]) end) it("shears a matrix", function() local yx, zx, xy, zy, xz, yz = 1, 1, 1, -1, -1, -1 local a = mat4():shear(mat4(), yx, zx, xy, zy, xz, yz) assert.is.equal( 1, a[2]) assert.is.equal( 1, a[3]) assert.is.equal( 1, a[5]) assert.is.equal(-1, a[7]) assert.is.equal(-1, a[9]) assert.is.equal(-1, a[10]) end) it("reflects a matrix along a plane", function() local origin = vec3(5, 1, 0) local normal = vec3(0, -1, 0):normalize() local a = mat4():reflect(mat4(), origin, normal) local p = a * vec3(-5, 2, 5) assert.is.equal(p.x, -5) assert.is.equal(p.y, 0) assert.is.equal(p.z, 5) end) it("projects a point into screen space", function() local znear = 0.1 local zfar = 1000 local proj = mat4.from_perspective(45, 1, znear, zfar) local vp = { 0, 0, 400, 400 } -- -z is away from the viewer into the far plane local p1 = vec3(0, 0, -znear) local c1 = mat4.project(p1, proj, vp) assert.is.near(0, c1.z, 0.0001) assert.is.equal(200, c1.x) assert.is.equal(200, c1.y) local p2 = vec3(0, 0, -zfar) local c2 = mat4.project(p2, proj, vp) assert.is.near(1, c2.z, 0.0001) assert.is.equal(200, c2.x) assert.is.equal(200, c2.y) local p3 = vec3(0, 0, zfar) local c3 = mat4.project(p3, proj, vp) assert.is_true(c3.z < 0) assert.is.equal(200, c3.x) assert.is.equal(200, c3.y) end) it("unprojects a point into world space", function() local p = vec3(0, 0, -10) local proj = mat4.from_perspective(45, 1, 0.1, 1000) local vp = { 0, 0, 400, 400 } local c = mat4.project(p, proj, vp) local d = mat4.unproject(c, proj, vp) assert.is.near(0.0, p.x-d.x, 0.0001) assert.is.near(0.0, p.y-d.y, 0.0001) assert.is.near(0.0, p.z-d.z, 0.0001) end) it("transforms a matrix to look at a point", function() local e = vec3(0, 0, 1.55) local c = vec3(4, 7, 1) local u = vec3(0, 0, 1) local a = mat4():look_at(e, c, u) assert.is_true(utils.tolerance( 0.868-a[1], 0.001)) assert.is_true(utils.tolerance( 0.034-a[2], 0.001)) assert.is_true(utils.tolerance(-0.495-a[3], 0.001)) assert.is_true(utils.tolerance( 0 -a[4], 0.001)) assert.is_true(utils.tolerance(-0.496-a[5], 0.001)) assert.is_true(utils.tolerance( 0.059-a[6], 0.001)) assert.is_true(utils.tolerance(-0.866-a[7], 0.001)) assert.is_true(utils.tolerance( 0 -a[8], 0.001)) assert.is_true(utils.tolerance( 0 -a[9], 0.001)) assert.is_true(utils.tolerance( 0.998-a[10], 0.001)) assert.is_true(utils.tolerance( 0.068-a[11], 0.001)) assert.is_true(utils.tolerance( 0 -a[12], 0.001)) assert.is_true(utils.tolerance( 0 -a[13], 0.001)) assert.is_true(utils.tolerance(-1.546-a[14], 0.001)) assert.is_true(utils.tolerance(-0.106-a[15], 0.001)) assert.is_true(utils.tolerance( 1 -a[16], 0.001)) end) it("converts a matrix to vec4s", function() local a = mat4 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 } local v = a:to_vec4s() assert.is_true(type(v) == "table") assert.is_true(type(v[1]) == "table") assert.is_true(type(v[2]) == "table") assert.is_true(type(v[3]) == "table") assert.is_true(type(v[4]) == "table") assert.is.equal(1, v[1][1]) assert.is.equal(2, v[1][2]) assert.is.equal(3, v[1][3]) assert.is.equal(4, v[1][4]) assert.is.equal(5, v[2][1]) assert.is.equal(6, v[2][2]) assert.is.equal(7, v[2][3]) assert.is.equal(8, v[2][4]) assert.is.equal(9, v[3][1]) assert.is.equal(10, v[3][2]) assert.is.equal(11, v[3][3]) assert.is.equal(12, v[3][4]) assert.is.equal(13, v[4][1]) assert.is.equal(14, v[4][2]) assert.is.equal(15, v[4][3]) assert.is.equal(16, v[4][4]) end) it("converts a matrix to vec4s, column-wise", function() local a = mat4 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 } local v = a:to_vec4s_cols() assert.is_true(type(v) == "table") assert.is_true(type(v[1]) == "table") assert.is_true(type(v[2]) == "table") assert.is_true(type(v[3]) == "table") assert.is_true(type(v[4]) == "table") assert.is.equal(1, v[1][1]) assert.is.equal(5, v[1][2]) assert.is.equal(9, v[1][3]) assert.is.equal(13, v[1][4]) assert.is.equal(2, v[2][1]) assert.is.equal(6, v[2][2]) assert.is.equal(10, v[2][3]) assert.is.equal(14, v[2][4]) assert.is.equal(3, v[3][1]) assert.is.equal(7, v[3][2]) assert.is.equal(11, v[3][3]) assert.is.equal(15, v[3][4]) assert.is.equal(4, v[4][1]) assert.is.equal(8, v[4][2]) assert.is.equal(12, v[4][3]) assert.is.equal(16, v[4][4]) end) it("converts a matrix to a quaternion", function() local q = mat4({ 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }):to_quat() assert.is.equal(-0.5, q.x) assert.is.equal(-0.5, q.y) assert.is.equal(-0.5, q.z) assert.is.equal( 0.5, q.w) end) it("converts a matrix to a frustum", function() local a = mat4() local b = mat4.from_perspective(45, 1, 0.1, 1000) local f = (b * a):to_frustum() assert.is_true(utils.tolerance( 0.9239-f.left.a, 0.001)) assert.is_true(utils.tolerance( 0 -f.left.b, 0.001)) assert.is_true(utils.tolerance(-0.3827-f.left.c, 0.001)) assert.is_true(utils.tolerance( 0 -f.left.d, 0.001)) assert.is_true(utils.tolerance(-0.9239-f.right.a, 0.001)) assert.is_true(utils.tolerance( 0 -f.right.b, 0.001)) assert.is_true(utils.tolerance(-0.3827-f.right.c, 0.001)) assert.is_true(utils.tolerance( 0 -f.right.d, 0.001)) assert.is_true(utils.tolerance( 0 -f.bottom.a, 0.001)) assert.is_true(utils.tolerance( 0.9239-f.bottom.b, 0.001)) assert.is_true(utils.tolerance(-0.3827-f.bottom.c, 0.001)) assert.is_true(utils.tolerance( 0 -f.bottom.d, 0.001)) assert.is_true(utils.tolerance( 0 -f.top.a, 0.001)) assert.is_true(utils.tolerance(-0.9239-f.top.b, 0.001)) assert.is_true(utils.tolerance(-0.3827-f.top.c, 0.001)) assert.is_true(utils.tolerance( 0 -f.top.d, 0.001)) assert.is_true(utils.tolerance( 0 -f.near.a, 0.001)) assert.is_true(utils.tolerance( 0 -f.near.b, 0.001)) assert.is_true(utils.tolerance(-1 -f.near.c, 0.001)) assert.is_true(utils.tolerance(-0.1-f.near.d, 0.001)) assert.is_true(utils.tolerance( 0 -f.far.a, 0.001)) assert.is_true(utils.tolerance( 0 -f.far.b, 0.001)) assert.is_true(utils.tolerance( 1 -f.far.c, 0.001)) assert.is_true(utils.tolerance( 1000-f.far.d, 0.001)) end) it("checks to see if data is a valid matrix (not a table)", function() assert.is_not_true(mat4.is_mat4(0)) end) it("checks to see if data is a valid matrix (invalid data)", function() assert.is_not_true(mat4.is_mat4({})) end) it("gets a string representation of a matrix", function() local a = mat4():to_string() local z = "+0.000" local o = "+1.000" local s = string.format( "[ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s ]", o, z, z, z, z, o, z, z, z, z, o, z, z, z ,z, o ) assert.is.equal(s, a) end) it("creates a matrix out of transform values", function() local scale = vec3(1, 2, 3) local rot = quat.from_angle_axis(math.pi * 0.5, vec3(0, 1, 0)) local trans = vec3(3, 4, 5) local a = mat4.from_transform(trans, rot, scale) local v = vec3(-2, 3, 4) -- scaled, rotated, then translated -- v * mT * mR * mS local result = a * v local expected = vec3(-9, 10, 3) -- float margin is considered assert.is_true(math.abs(expected.x - result.x) < FLT_EPSILON) assert.is_true(math.abs(expected.y - result.y) < FLT_EPSILON) assert.is_true(math.abs(expected.z - result.z) < FLT_EPSILON) end) end) --[[ from_angle_axis from_quaternion from_direction from_transform from_ortho --]] ================================================ FILE: spec/mesh_spec.lua ================================================ local mesh = require "modules.mesh" describe("mesh:", function() end) --[[ average(vertices) normal(triangle) plane_from_triangle(triangle) is_front_facing(plane, direction) signed_distance(point, plane) --]] ================================================ FILE: spec/octree_spec.lua ================================================ local octree = require "modules.octree" describe("octree:", function() end) --[[ local function new(initialWorldSize, initialWorldPos, minNodeSize, looseness) function Octree:add(obj, objBounds) function Octree:remove(obj) function Octree:is_colliding(checkBounds) function Octree:get_colliding(checkBounds) function Octree:cast_ray(ray, func, out) function Octree:draw_bounds(cube) function Octree:draw_objects(cube, filter) function Octree:grow(direction) function Octree:shrink() function Octree:get_root_pos_index(xDir, yDir, zDir) function OctreeNode:add(obj, objBounds) function OctreeNode:remove(obj) function OctreeNode:is_colliding(checkBounds) function OctreeNode:get_colliding(checkBounds, results) function OctreeNode:cast_ray(ray, func, out, depth) function OctreeNode:set_children(childOctrees) function OctreeNode:shrink_if_possible(minLength) function OctreeNode:set_values(baseLength, minSize, looseness, center) function OctreeNode:split() function OctreeNode:merge() function OctreeNode:best_fit_child(objBounds) function OctreeNode:should_merge() function OctreeNode:has_any_objects() function OctreeNode:draw_bounds(cube, depth) function OctreeNode:draw_objects(cube, filter) --]] ================================================ FILE: spec/quat_spec.lua ================================================ local quat = require "modules.quat" local vec3 = require "modules.vec3" local utils = require "modules.utils" local constants = require "modules.constants" describe("quat:", function() it("creates an identity quaternion", function() local a = quat() assert.is.equal(0, a.x) assert.is.equal(0, a.y) assert.is.equal(0, a.z) assert.is.equal(1, a.w) assert.is_true(a:is_quat()) assert.is_true(a:is_real()) end) it("creates a quaternion from numbers", function() local a = quat(0, 0, 0, 0) assert.is.equal(0, a.x) assert.is.equal(0, a.y) assert.is.equal(0, a.z) assert.is.equal(0, a.w) assert.is_true(a:is_zero()) assert.is_true(a:is_imaginary()) end) it("creates a quaternion from a list", function() local a = quat { 2, 3, 4, 1 } assert.is.equal(2, a.x) assert.is.equal(3, a.y) assert.is.equal(4, a.z) assert.is.equal(1, a.w) end) it("creates a quaternion from a record", function() local a = quat { x=2, y=3, z=4, w=1 } assert.is.equal(2, a.x) assert.is.equal(3, a.y) assert.is.equal(4, a.z) assert.is.equal(1, a.w) end) it("creates a quaternion from a quaternion", function() local a = quat (quat(2, 3, 4, 1)) assert.is.equal(2, a.x) assert.is.equal(3, a.y) assert.is.equal(4, a.z) assert.is.equal(1, a.w) end) it("creates a quaternion from a direction", function() local v = vec3(-80, 80, -80):normalize() local a = quat.from_direction(v, vec3.unit_z) assert.is_true(utils.tolerance(-0.577-a.x, 0.001)) assert.is_true(utils.tolerance(-0.577-a.y, 0.001)) assert.is_true(utils.tolerance( 0 -a.z, 0.001)) assert.is_true(utils.tolerance( 0.423-a.w, 0.001)) end) it("clones a quaternion", function() local a = quat() local b = a:clone() assert.is.equal(a.x, b.x) assert.is.equal(a.y, b.y) assert.is.equal(a.z, b.z) assert.is.equal(a.w, b.w) end) it("adds a quaternion to another", function() local a = quat(2, 3, 4, 1) local b = quat(3, 6, 9, 1) local c = a:add(b) local d = a + b assert.is.equal(5, c.x) assert.is.equal(9, c.y) assert.is.equal(13, c.z) assert.is.equal(2, c.w) assert.is.equal(c, d) end) it("subtracts a quaternion from another", function() local a = quat(2, 3, 4, 1) local b = quat(3, 6, 9, 1) local c = a:sub(b) local d = a - b assert.is.equal(-1, c.x) assert.is.equal(-3, c.y) assert.is.equal(-5, c.z) assert.is.equal( 0, c.w) assert.is.equal(c, d) end) it("multiplies a quaternion by another", function() local a = quat(2, 3, 4, 1) local b = quat(3, 6, 9, 1) local c = a:mul(b) local d = a * b assert.is.equal( 8, c.x) assert.is.equal( 3, c.y) assert.is.equal( 16, c.z) assert.is.equal(-59, c.w) assert.is.equal(c, d) end) it("multiplies a quaternion by a scale factor", function() local a = quat(2, 3, 4, 1) local s = 3 local b = a:scale(s) local c = a * s assert.is.equal(6, b.x) assert.is.equal(9, b.y) assert.is.equal(12, b.z) assert.is.equal(3, b.w) assert.is.equal(b, c) end) it("inverts a quaternion", function() local a = quat(2, 3, 4, 1) local b = -a assert.is.equal(-a.x, b.x) assert.is.equal(-a.y, b.y) assert.is.equal(-a.z, b.z) assert.is.equal(-a.w, b.w) end) it("multiplies a quaternion by a vec3", function() local a = quat(2, 3, 4, 1) local v = vec3(3, 4, 5) local b = a:mul_vec3(v) local c = a * v assert.is.equal(-21, c.x) assert.is.equal( 4, c.y) assert.is.equal( 17, c.z) assert.is.equal(b, c) end) it("verifies quat composition order", function() local a = quat(2, 3, 4, 1):normalize() -- Only the normal quaternions represent rotations local b = quat(3, 6, 9, 1):normalize() local c = a * b local v = vec3(3, 4, 5) local cv = c * v local abv = a * (b * v) assert.is_true((abv - cv):len() < 1e-07) -- Verify (a*b)*v == a*(b*v) within an epsilon end) it("multiplies a quaternion by an exponent of 0", function() local a = quat(2, 3, 4, 1):normalize() local e = 0 local b = a:pow(e) local c = a^e assert.is.equal(0, b.x) assert.is.equal(0, b.y) assert.is.equal(0, b.z) assert.is.equal(1, b.w) assert.is.equal(b, c) end) it("multiplies a quaternion by a positive exponent", function() local a = quat(2, 3, 4, 1):normalize() local e = 0.75 local b = a:pow(e) local c = a^e assert.is_true(utils.tolerance(-0.3204+b.x, 0.0001)) assert.is_true(utils.tolerance(-0.4805+b.y, 0.0001)) assert.is_true(utils.tolerance(-0.6407+b.z, 0.0001)) assert.is_true(utils.tolerance(-0.5059+b.w, 0.0001)) assert.is.equal( b, c) end) it("multiplies a quaternion by a negative exponent", function() local a = quat(2, 3, 4, 1):normalize() local e = -1 local b = a:pow(e) local c = a^e assert.is_true(utils.tolerance( 0.3651+b.x, 0.0001)) assert.is_true(utils.tolerance( 0.5477+b.y, 0.0001)) assert.is_true(utils.tolerance( 0.7303+b.z, 0.0001)) assert.is_true(utils.tolerance(-0.1826+b.w, 0.0001)) assert.is.equal(b, c) end) it("inverts a quaternion", function() local a = quat(1, 1, 1, 1):inverse() assert.is.equal(-0.5, a.x) assert.is.equal(-0.5, a.y) assert.is.equal(-0.5, a.z) assert.is.equal( 0.5, a.w) end) it("normalizes a quaternion", function() local a = quat(1, 1, 1, 1):normalize() assert.is.equal(0.5, a.x) assert.is.equal(0.5, a.y) assert.is.equal(0.5, a.z) assert.is.equal(0.5, a.w) end) it("dots two quaternions", function() local a = quat(1, 1, 1, 1) local b = quat(4, 4, 4, 4) local c = a:dot(b) assert.is.equal(16, c) end) it("dots two quaternions (negative)", function() local a = quat(-1, 1, 1, 1) local b = quat(4, 4, 4, 4) local c = a:dot(b) assert.is.equal(8, c) end) it("dots two quaternions (tiny)", function() local a = quat(0.1, 0.1, 0.1, 0.1) local b = quat(0.4, 0.4, 0.4, 0.4) local c = a:dot(b) assert.is_true(utils.tolerance(0.16-c, 0.001)) end) it("gets the length of a quaternion", function() local a = quat(2, 3, 4, 5):len() assert.is.equal(math.sqrt(54), a) end) it("gets the square length of a quaternion", function() local a = quat(2, 3, 4, 5):len2() assert.is.equal(54, a) end) it("interpolates between two quaternions", function() local a = quat(3, 3, 3, 3) local b = quat(6, 6, 6, 6) local s = 0.1 local c = a:lerp(b, s) assert.is.equal(0.5, c.x) assert.is.equal(0.5, c.y) assert.is.equal(0.5, c.z) assert.is.equal(0.5, c.w) end) it("interpolates between two quaternions (spherical)", function() local a = quat(3, 3, 3, 3) local b = quat(6, 6, 6, 6) local s = 0.1 local c = a:slerp(b, s) assert.is.equal(0.5, c.x) assert.is.equal(0.5, c.y) assert.is.equal(0.5, c.z) assert.is.equal(0.5, c.w) end) it("unpacks a quaternion", function() local x, y, z, w = quat(2, 3, 4, 1):unpack() assert.is.equal(2, x) assert.is.equal(3, y) assert.is.equal(4, z) assert.is.equal(1, w) end) it("converts quaternion to a vec3", function() local v = quat(2, 3, 4, 1):to_vec3() assert.is.equal(2, v.x) assert.is.equal(3, v.y) assert.is.equal(4, v.z) end) it("gets the conjugate quaternion", function() local a = quat(2, 3, 4, 1):conjugate() assert.is.equal(-2, a.x) assert.is.equal(-3, a.y) assert.is.equal(-4, a.z) assert.is.equal( 1, a.w) end) it("gets the reciprocal quaternion", function() local a = quat(1, 1, 1, 1) local b = a:reciprocal() local c = b:reciprocal() assert.is_not.equal(a.x, b.x) assert.is_not.equal(a.y, b.y) assert.is_not.equal(a.z, b.z) assert.is_not.equal(a.w, b.w) assert.is.equal(a.x, c.x) assert.is.equal(a.y, c.y) assert.is.equal(a.z, c.z) assert.is.equal(a.w, c.w) end) it("converts between a quaternion and angle/axis", function() local a = quat.from_angle_axis(math.pi, vec3.unit_z) local angle, axis = a:to_angle_axis() assert.is.equal(math.pi, angle) assert.is.equal(vec3.unit_z, axis) end) it("converts between a quaternion and angle/axis (specify by component)", function() local a = quat.from_angle_axis(math.pi, vec3.unit_z.x, vec3.unit_z.y, vec3.unit_z.z) local angle, axis = a:to_angle_axis() assert.is.equal(math.pi, angle) assert.is.equal(vec3.unit_z, axis) end) it("converts between a quaternion and angle/axis (w=2)", function() local angle, axis = quat(1, 1, 1, 2):to_angle_axis() assert.is_true(utils.tolerance(1.427-angle, 0.001)) assert.is_true(utils.tolerance(0.577-axis.x, 0.001)) assert.is_true(utils.tolerance(0.577-axis.y, 0.001)) assert.is_true(utils.tolerance(0.577-axis.z, 0.001)) end) it("converts between a quaternion and angle/axis (w=2) (by component)", function() local angle, x,y,z = quat(1, 1, 1, 2):to_angle_axis_unpack() assert.is_true(utils.tolerance(1.427-angle, 0.001)) assert.is_true(utils.tolerance(0.577-x, 0.001)) assert.is_true(utils.tolerance(0.577-y, 0.001)) assert.is_true(utils.tolerance(0.577-z, 0.001)) end) it("converts between a quaternion and angle/axis (w=1)", function() local angle, axis = quat(1, 2, 3, 1):to_angle_axis() assert.is.equal(0, angle) assert.is.equal(1, axis.x) assert.is.equal(2, axis.y) assert.is.equal(3, axis.z) end) it("converts between a quaternion and angle/axis (identity quaternion) (by component)", function() local angle, x,y,z = quat():to_angle_axis_unpack() assert.is.equal(0, angle) assert.is.equal(0, x) assert.is.equal(0, y) assert.is.equal(1, z) end) it("converts between a quaternion and angle/axis (identity quaternion with fallback)", function() local angle, axis = quat():to_angle_axis(vec3(2,3,4)) assert.is.equal(0, angle) assert.is.equal(2, axis.x) assert.is.equal(3, axis.y) assert.is.equal(4, axis.z) end) it("gets a string representation of a quaternion", function() local a = quat():to_string() assert.is.equal("(+0.000,+0.000,+0.000,+1.000)", a) end) end) ================================================ FILE: spec/utils_spec.lua ================================================ local vec3 = require "modules.vec3" local utils = require "modules.utils" local constants = require "modules.constants" local function tolerance(v, t) return math.abs(v - t) < 1e-6 end describe("utils:", function() it("interpolates between two numbers", function() assert.is_true(tolerance(utils.lerp(0, 1, 0.5), 0.5)) end) it("interpolates between two vectors", function() local a = vec3(0, 0, 0) local b = vec3(1, 1, 1) local c = vec3(0.5, 0.5, 0.5) assert.is.equal(utils.lerp(a, b, 0.5), c) a = vec3(5, 5, 5) b = vec3(0, 0, 0) c = vec3(2.5, 2.5, 2.5) assert.is.equal(utils.lerp(a, b, 0.5), c) end) it("decays exponentially", function() local v = utils.decay(0, 1, 0.5, 1) assert.is_true(tolerance(v, 0.39346934028737)) end) it("checks a nan", function() local a = 0/0 assert.is_true(utils.is_nan(a)) end) it("rounds a number", function() -- round up local v = utils.round(1.3252525, 0.01) assert.is_true(tolerance(v, 1.33)) -- round down v = utils.round(1.3242525, 0.1) assert.is_true(tolerance(v, 1.3)) -- no precision v = utils.round(1.3242525) assert.is_true(tolerance(v, 1)) end) it("checks sign", function() assert.is.equal(utils.sign(-9), -1) assert.is.equal(utils.sign(0), 0) assert.is.equal(utils.sign(12), 1) end) end) --[[ clamp(value, min, max) deadzone(value, size) threshold(value, threshold) tolerance(value, threshold) map(value, min_in, max_in, min_out, max_out) lerp(progress, low, high) smoothstep(progress, low, high) wrap(value, limit) is_pot(value) project_on(out, a, b) project_from(out, a, b) mirror_on(out, a, b) reflect(out, i, n) refract(out, i, n, ior) --]] ================================================ FILE: spec/vec2_spec.lua ================================================ local vec2 = require "modules.vec2" local DBL_EPSILON = require("modules.constants").DBL_EPSILON local abs, sqrt = math.abs, math.sqrt describe("vec2:", function() it("creates an empty vector", function() local a = vec2() assert.is.equal(0, a.x) assert.is.equal(0, a.y) assert.is_true(a:is_vec2()) assert.is_true(a:is_zero()) end) it("creates a vector from a number", function() local a = vec2(3) assert.is.equal(3, a.x) assert.is.equal(3, a.y) end) it("creates a vector from numbers", function() local a = vec2(3, 5) assert.is.equal(3, a.x) assert.is.equal(5, a.y) end) it("creates a vector from a list", function() local a = vec2 { 3, 5 } assert.is.equal(3, a.x) assert.is.equal(5, a.y) end) it("creates a vector from a record", function() local a = vec2 { x=3, y=5 } assert.is.equal(3, a.x) assert.is.equal(5, a.y) end) it("creates a vector from nan", function() local a = vec2(0/0) assert.is_true(a:has_nan()) end) it("clones a vector", function() local a = vec2(3, 5) local b = a:clone() assert.is.equal(a, b) end) it("clones a vector using the constructor", function() local a = vec2(3, 5) local b = vec2(a) assert.is.equal(a, b) end) it("adds a vector to another", function() local a = vec2(3, 5) local b = vec2(7, 4) local c = a:add(b) local d = a + b assert.is.equal(10, c.x) assert.is.equal(9, c.y) assert.is.equal(c, d) end) it("subracts a vector from another", function() local a = vec2(3, 5) local b = vec2(7, 4) local c = a:sub(b) local d = a - b assert.is.equal(-4, c.x) assert.is.equal( 1, c.y) assert.is.equal( c, d) end) it("multiplies a vector by a scale factor", function() local a = vec2(3, 5) local s = 2 local c = a:scale(s) local d = a * s assert.is.equal(6, c.x) assert.is.equal(10, c.y) assert.is.equal(c, d) end) it("divides a vector by another vector", function() local a = vec2(3, 5) local s = vec2(2, 2) local c = a:div(s) local d = a / s assert.is.equal(1.5, c.x) assert.is.equal(2.5, c.y) assert.is.equal(c, d) end) it("inverts a vector", function() local a = vec2(3, -5) local b = -a assert.is.equal(-a.x, b.x) assert.is.equal(-a.y, b.y) end) it("gets the length of a vector", function() local a = vec2(3, 5) assert.is.equal(sqrt(34), a:len()) end) it("gets the square length of a vector", function() local a = vec2(3, 5) assert.is.equal(34, a:len2()) end) it("normalizes a vector", function() local a = vec2(3, 5) local b = a:normalize() assert.is_true(abs(b:len()-1) < DBL_EPSILON) end) it("trims the length of a vector", function() local a = vec2(3, 5) local b = a:trim(0.5) assert.is_true(abs(b:len()-0.5) < DBL_EPSILON) end) it("gets the distance between two vectors", function() local a = vec2(3, 5) local b = vec2(7, 4) local c = a:dist(b) assert.is.equal(sqrt(17), c) end) it("gets the square distance between two vectors", function() local a = vec2(3, 5) local b = vec2(7, 4) local c = a:dist2(b) assert.is.equal(17, c) end) it("crosses two vectors", function() local a = vec2(3, 5) local b = vec2(7, 4) local c = a:cross(b) assert.is.equal(-23, c) end) it("dots two vectors", function() local a = vec2(3, 5) local b = vec2(7, 4) local c = a:dot(b) assert.is.equal(41, c) end) it("interpolates between two vectors", function() local a = vec2(3, 5) local b = vec2(7, 4) local s = 0.1 local c = a:lerp(b, s) assert.is.equal(3.4, c.x) assert.is.equal(4.9, c.y) end) it("unpacks a vector", function() local a = vec2(3, 5) local x, y = a:unpack() assert.is.equal(3, x) assert.is.equal(5, y) end) it("rotates a vector", function() local a = vec2(3, 5) local b = a:rotate( math.pi) local c = b:rotate(-math.pi) assert.is_not.equal(a, b) assert.is.equal(a, c) end) it("converts between polar and cartesian coordinates", function() local a = vec2(3, 5) local r, t = a:to_polar() local b = vec2.from_cartesian(r, t) assert.is_true(abs(a.x - b.x) <= DBL_EPSILON*2) -- Allow 2X epsilon error because there were 2 operations. assert.is_true(abs(a.y - b.y) <= DBL_EPSILON*2) end) it("gets a perpendicular vector", function() local a = vec2(3, 5) local b = a:perpendicular() assert.is.equal(-5, b.x) assert.is.equal( 3, b.y) end) it("gets a string representation of a vector", function() local a = vec2() local b = a:to_string() assert.is.equal("(+0.000,+0.000)", b) end) it("rounds a 2-vector", function() local a = vec2(1.1,1.9):round() assert.is.equal(a.x, 1) assert.is.equal(a.y, 2) end) it("flips a 2-vector", function() local a = vec2(1,2) local temp = a:flip_x() assert.is.equal(temp, vec2(-1, 2)) temp = temp:flip_y() assert.is.equal(temp, vec2(-1, -2)) end) it("finds angle from one 2-vector to another", function() local d = { right = vec2(1, 0), down = vec2(0, -1), left = vec2(-1, 0), up = vec2(0, 1), } assert.is.equal(math.deg(d.right:angle_to(d.right)), 0.0) assert.is.equal(math.deg(d.right:angle_to(d.down)), -90.0) assert.is.equal(math.deg(d.right:angle_to(d.left)), 180.0) assert.is.equal(math.deg(d.right:angle_to(d.up)), 90.0) assert.is.equal(math.deg(d.down:angle_to(d.right)), 90.0) assert.is.equal(math.deg(d.down:angle_to(d.down)), 0.0) assert.is.equal(math.deg(d.down:angle_to(d.left)), -90.0) assert.is.equal(math.deg(d.down:angle_to(d.up)), 180.0) assert.is.equal(math.deg(d.left:angle_to(d.right)), 180.0) assert.is.equal(math.deg(d.left:angle_to(d.down)), 90.0) assert.is.equal(math.deg(d.left:angle_to(d.left)), 0.0) assert.is.equal(math.deg(d.left:angle_to(d.up)), -90.0) assert.is.equal(math.deg(d.up:angle_to(d.right)), -90.0) assert.is.equal(math.deg(d.up:angle_to(d.down)), 180.0) assert.is.equal(math.deg(d.up:angle_to(d.left)), 90.0) assert.is.equal(math.deg(d.up:angle_to(d.up)), 0.0) end) it("finds angle between two 2-vectors", function() local d = { right = vec2(1, 0), down = vec2(0, -1), left = vec2(-1, 0), up = vec2(0, 1), } assert.is.equal(math.deg(d.right:angle_between(d.right)), 0.0) assert.is.equal(math.deg(d.right:angle_between(d.down)), 90.0) assert.is.equal(math.deg(d.right:angle_between(d.left)), 180.0) assert.is.equal(math.deg(d.right:angle_between(d.up)), 90.0) assert.is.equal(math.deg(d.down:angle_between(d.right)), 90.0) assert.is.equal(math.deg(d.down:angle_between(d.down)), 0.0) assert.is.equal(math.deg(d.down:angle_between(d.left)), 90.0) assert.is.equal(math.deg(d.down:angle_between(d.up)), 180.0) assert.is.equal(math.deg(d.left:angle_between(d.right)), 180.0) assert.is.equal(math.deg(d.left:angle_between(d.down)), 90.0) assert.is.equal(math.deg(d.left:angle_between(d.left)), 0.0) assert.is.equal(math.deg(d.left:angle_between(d.up)), 90.0) assert.is.equal(math.deg(d.up:angle_between(d.right)), 90.0) assert.is.equal(math.deg(d.up:angle_between(d.down)), 180.0) assert.is.equal(math.deg(d.up:angle_between(d.left)), 90.0) assert.is.equal(math.deg(d.up:angle_between(d.up)), 0.0) end) -- Do this last, to insulate tests from accidental state contamination -- Do vec3 tests last, to insulate tests from accidental state contamination it("converts a 2-vector to a 3-vector", function() local vec3 = require "modules.vec3" local a = vec2(1,2) local b = a:to_vec3() local c = a:to_vec3(3) assert.is.equal(b, vec3(1,2,0)) assert.is.equal(c, vec3(1,2,3)) end) it("converts a vec3 to vec2 using the constructor", function() local vec3 = require "modules.vec3" local a = vec2(3, 5) local b = vec3(3, 5, 7) local c = vec2(b) assert.is.equal(a, c) end) end) ================================================ FILE: spec/vec3_spec.lua ================================================ local vec3 = require "modules.vec3" local DBL_EPSILON = require("modules.constants").DBL_EPSILON local abs, sqrt = math.abs, math.sqrt describe("vec3:", function() it("creates an empty vector", function() local a = vec3() assert.is.equal(0, a.x) assert.is.equal(0, a.y) assert.is.equal(0, a.z) assert.is_true(a:is_vec3()) assert.is_true(a:is_zero()) end) it("creates a vector from a number", function() local a = vec3(3) assert.is.equal(3, a.x) assert.is.equal(3, a.y) assert.is.equal(3, a.z) end) it("creates a vector from numbers", function() local a = vec3(3, 5, 7) assert.is.equal(3, a.x) assert.is.equal(5, a.y) assert.is.equal(7, a.z) end) it("creates a vector from a list", function() local a = vec3 { 3, 5, 7 } assert.is.equal(3, a.x) assert.is.equal(5, a.y) assert.is.equal(7, a.z) end) it("creates a vector from a record", function() local a = vec3 { x=3, y=5, z=7 } assert.is.equal(3, a.x) assert.is.equal(5, a.y) assert.is.equal(7, a.z) end) it("creates a vector from nan", function() local a = vec3(0/0) assert.is_true(a:has_nan()) end) it("clones a vector", function() local a = vec3(3, 5, 7) local b = a:clone() assert.is.equal(a, b) end) it("clones a vector using the constructor", function() local a = vec3(3, 5, 7) local b = vec3(a) assert.is.equal(a, b) end) it("adds a vector to another", function() local a = vec3(3, 5, 7) local b = vec3(7, 4, 1) local c = a:add(b) local d = a + b assert.is.equal(10, c.x) assert.is.equal(9, c.y) assert.is.equal(8, c.z) assert.is.equal(c, d) end) it("subracts a vector from another", function() local a = vec3(3, 5, 7) local b = vec3(7, 4, 1) local c = a:sub(b) local d = a - b assert.is.equal(-4, c.x) assert.is.equal( 1, c.y) assert.is.equal( 6, c.z) assert.is.equal( c, d) end) it("multiplies a vector by a scale factor", function() local a = vec3(3, 5, 7) local s = 2 local c = a:scale(s) local d = a * s assert.is.equal(6, c.x) assert.is.equal(10, c.y) assert.is.equal(14, c.z) assert.is.equal(c, d) end) it("divides a vector by another vector", function() local a = vec3(3, 5, 7) local s = vec3(2, 2, 2) local c = a:div(s) local d = a / s assert.is.equal(1.5, c.x) assert.is.equal(2.5, c.y) assert.is.equal(3.5, c.z) assert.is.equal(c, d) end) it("inverts a vector", function() local a = vec3(3, -5, 7) local b = -a assert.is.equal(-a.x, b.x) assert.is.equal(-a.y, b.y) assert.is.equal(-a.z, b.z) end) it("gets the length of a vector", function() local a = vec3(3, 5, 7) assert.is.equal(sqrt(83), a:len()) end) it("gets the square length of a vector", function() local a = vec3(3, 5, 7) assert.is.equal(83, a:len2()) end) it("normalizes a vector", function() local a = vec3(3, 5, 7) local b = a:normalize() assert.is_true(abs(b:len()-1) < DBL_EPSILON) end) it("normalizes a vector and gets the length", function() local a = vec3(3, 5, 7) local b, l = a:normalize_len() assert.is_true(abs(b:len()-1) < DBL_EPSILON) assert.is.equal(sqrt(83), l) end) it("trims the length of a vector", function() local a = vec3(3, 5, 7) local b = a:trim(0.5) assert.is_true(abs(b:len()-0.5) < DBL_EPSILON) end) it("gets the distance between two vectors", function() local a = vec3(3, 5, 7) local b = vec3(7, 4, 1) local c = a:dist(b) assert.is.equal(sqrt(53), c) end) it("gets the square distance between two vectors", function() local a = vec3(3, 5, 7) local b = vec3(7, 4, 1) local c = a:dist2(b) assert.is.equal(53, c) end) it("crosses two vectors", function() local a = vec3(3, 5, 7) local b = vec3(7, 4, 1) local c = a:cross(b) assert.is.equal(-23, c.x) assert.is.equal( 46, c.y) assert.is.equal(-23, c.z) end) it("dots two vectors", function() local a = vec3(3, 5, 7) local b = vec3(7, 4, 1) local c = a:dot(b) assert.is.equal(48, c) end) it("interpolates between two vectors", function() local a = vec3(3, 5, 7) local b = vec3(7, 4, 1) local s = 0.1 local c = a:lerp(b, s) assert.is.equal(3.4, c.x) assert.is.equal(4.9, c.y) assert.is.equal(6.4, c.z) end) it("unpacks a vector", function() local a = vec3(3, 5, 7) local x, y, z = a:unpack() assert.is.equal(3, x) assert.is.equal(5, y) assert.is.equal(7, z) end) it("rotates a vector", function() local a = vec3(3, 5, 7) local b = a:rotate( math.pi, vec3.unit_z) local c = b:rotate(-math.pi, vec3.unit_z) assert.is_not.equal(a, b) assert.is.equal(7, b.z) assert.is.equal(a, c) end) it("cannot rotate a vector without a valis axis", function() local a = vec3(3, 5, 7) local b = a:rotate(math.pi, 0) assert.is_equal(a, b) end) it("gets a perpendicular vector", function() local a = vec3(3, 5, 7) local b = a:perpendicular() assert.is.equal(-5, b.x) assert.is.equal( 3, b.y) assert.is.equal( 0, b.z) end) it("gets a string representation of a vector", function() local a = vec3() local b = a:to_string() assert.is.equal("(+0.000,+0.000,+0.000)", b) end) it("rounds a 3-vector", function() local a = vec3(1.1,1.9,3):round() assert.is.equal(a.x, 1) assert.is.equal(a.y, 2) assert.is.equal(a.z, 3) end) it("flips a 3-vector", function() local a = vec3(1,2,3) local temp = a:flip_x() assert.is.equal(temp, vec3(-1, 2, 3)) temp = temp:flip_y() assert.is.equal(temp, vec3(-1, -2, 3)) temp = temp:flip_z() assert.is.equal(temp, vec3(-1, -2, -3)) end) it("get two 3-vectors angle", function() local angle_to = function(a, b) local deg = math.deg(a:angle_to(b)) return string.format('%.2f', deg) end local a = vec3(1,2,3) assert.is.equal(angle_to(a, vec3(3, 2, 1)), '44.42') assert.is.equal(angle_to(a, vec3(0, 10, 0)), '57.69') assert.is.equal(angle_to(a, vec3(0, -12, -10)), '157.51') a = vec3.unit_z assert.is.equal(angle_to(a, vec3(0, 10, 0)), '90.00') assert.is.equal(angle_to(a, vec3(-123, 10, 0)), '90.00') assert.is.equal(angle_to(a, vec3(-10, 0, 10)), '45.00') assert.is.equal(angle_to(a, vec3(-10, 0, -10)), '135.00') assert.is.equal(angle_to(a, vec3(0, -10, -10)), '135.00') assert.is.equal(angle_to(a, vec3(0, 0, -10)), '180.00') assert.is.equal(angle_to(a, vec3(0, 0, 100)), '0.00') a = vec3(100, 100, 0) assert.is.equal(angle_to(a, vec3(0, 0, 100)), '90.00') assert.is.equal(angle_to(a, vec3(0, 0, -100)), '90.00') assert.is.equal(angle_to(a, vec3(-10, -10, 0)), '180.00') assert.is.equal(angle_to(a, vec3.unit_z), '90.00') end) end)