-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathmatrix4.lua
180 lines (163 loc) · 5 KB
/
matrix4.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
-- Simple 4x4 matrix for 3d transformations (translation, rotation, scale);
-- provides exactly the methods needed to calculate inverse bind matrices (for b3d -> glTF conversion)
local mat4 = {}
local metatable = {__index = mat4}
function mat4.new(rows)
assert(#rows == 4)
for i = 1, 4 do
assert(#rows[i] == 4)
end
return setmetatable(rows, metatable)
end
function mat4.identity()
return mat4.new{
{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1},
}
end
-- Matrices can't properly represent translation:
-- => work with 4d vectors, assume w = 1.
function mat4.translation(vec)
assert(#vec == 3)
local x, y, z = unpack(vec)
return mat4.new{
{1, 0, 0, x},
{0, 1, 0, y},
{0, 0, 1, z},
{0, 0, 0, 1},
}
end
-- See https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
function mat4.rotation(unit_quat)
assert(#unit_quat == 4)
local x, y, z, w = unpack(unit_quat) -- TODO (?) assert unit quaternion
return mat4.new{
{1 - 2*(y^2 + z^2), 2*(x*y - z*w), 2*(x*z + y*w), 0},
{2*(x*y + z*w), 1 - 2*(x^2 + z^2), 2*(y*z - x*w), 0},
{2*(x*z - y*w), 2*(y*z + x*w), 1 - 2*(x^2 + y^2), 0},
{0, 0, 0, 1},
}
end
function mat4.scale(vec)
assert(#vec == 3)
local x, y, z = unpack(vec)
return mat4.new{
{x, 0, 0, 0},
{0, y, 0, 0},
{0, 0, z, 0},
{0, 0, 0, 1},
}
end
-- Apply `self` to a 4d modlib vector `vec`
function mat4:apply(vec)
assert(#vec == 4)
local res = {}
for i = 1, 4 do
local sum = 0
for j = 1, 4 do
sum = sum + self[i][j] * vec[j]
end
res[i] = sum
end
return vec.new(res)
end
-- Multiplication: First apply other, then self
--> Matrix product `self * other`
function mat4:multiply(other)
local res = {}
for i = 1, 4 do
res[i] = {}
for j = 1, 4 do
local sum = 0 -- dot product of row & col vec
for k = 1, 4 do
sum = sum + self[i][k] * other[k][j]
end
res[i][j] = sum
end
end
return mat4.new(res)
end
-- Composition: First apply self, then other
function mat4:compose(other)
return other:multiply(self) -- equivalent to `other * self` in terms of matrix multiplication
end
-- Matrix inversion using Gauss-Jordan elimination
do
-- Fundamental operations
local function _swap_rows(mat, i, j)
mat[i], mat[j] = mat[j], mat[i]
end
local function _scale_row(mat, factor, row_idx)
for i = 1, 4 do
mat[row_idx][i] = factor * mat[row_idx][i]
end
end
local function _add_row_with_factor(mat, factor, src_row_idx, dst_row_idx)
assert(src_row_idx ~= dst_row_idx)
for i = 1, 4 do
mat[dst_row_idx][i] = mat[dst_row_idx][i] + factor * mat[src_row_idx][i]
end
end
local epsilon = 1e-6 -- small threshold; values below this are considered zero
function mat4:inverse()
local inv = mat4.identity() -- inverse matrix: all elimination operations will also be applied to this
local copy = {} -- copy of `self` the Gaussian elimination is being executed on
for i = 1, 4 do
copy[i] = {}
for j = 1, 4 do
copy[i][j] = self[i][j]
end
end
-- All operations must be mirrored to the inverse matrix
local function swap_rows(i, j)
_swap_rows(copy, i, j)
_swap_rows(inv, i, j)
end
local function scale_row(factor, row_idx)
_scale_row(copy, factor, row_idx)
_scale_row(inv, factor, row_idx)
end
local function add_with_factor(factor, src_row_idx, dst_row_idx)
_add_row_with_factor(copy, factor, src_row_idx, dst_row_idx)
_add_row_with_factor(inv, factor, src_row_idx, dst_row_idx)
end
-- Elimination phase
for col_idx = 1, 4 do
-- Find a pivot row: Choose the row with the largest absolute component
local max_row_idx = col_idx
local max_abs_comp = math.abs(copy[max_row_idx][col_idx])
for row_idx = col_idx, 4 do
local cand_comp = math.abs(copy[row_idx][col_idx])
if cand_comp > max_abs_comp then
max_row_idx, max_abs_comp = row_idx, cand_comp
end
end
-- Assert that there is a row that has this component "nonzero"
assert(max_abs_comp >= epsilon, "matrix not invertible!")
swap_rows(col_idx, max_row_idx) -- swap row to correct position
-- Eliminate the `col_idx`-th component in all rows *below* the pivot row
local pivot_value = copy[col_idx][col_idx]
for row_idx = col_idx + 1, 4 do
local factor = -copy[row_idx][col_idx] / pivot_value
add_with_factor(factor, col_idx, row_idx)
assert(math.abs(copy[row_idx][col_idx]) < epsilon) -- should be eliminated now
end
end
-- Resubstitution phase - pretty much the same but in reverse and without swapping
for col_idx = 4, 1, -1 do
local pivot_value = copy[col_idx][col_idx]
-- Eliminate the `col_idx`-th component in all rows *above* the pivot row
for row_idx = col_idx - 1, 1, -1 do
local factor = -copy[row_idx][col_idx] / pivot_value
add_with_factor(factor, col_idx, row_idx)
assert(math.abs(copy[row_idx][col_idx]) < epsilon) -- should be eliminated now
end
scale_row(1/pivot_value, col_idx) -- normalize row
end
-- Done: `copy` should now be the identity matrix <=> `inv` is the inverse.
return inv
end
end
return mat4