• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excessive / cpml / 6132588926

09 Sep 2023 06:38PM UTC coverage: 14.013% (-44.7%) from 58.701%
6132588926

push

github

FatalError42O
fixed Busted support (hopefully)

975 of 6958 relevant lines covered (14.01%)

10.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.19
/modules/mat4.lua
1
--- double 4x4, 1-based, column major matrices
2
-- @module mat4
3
local constants = require(modules .. "constants")
3✔
4
local vec2      = require(modules .. "vec2")
×
5
local vec3      = require(modules .. "vec3")
×
6
local quat      = require(modules .. "quat")
×
7
local utils     = require(modules .. "utils")
×
8
local precond   = require(modules .. "_private_precond")
×
9
local private   = require(modules .. "_private_utils")
×
10
local sqrt      = math.sqrt
×
11
local cos       = math.cos
×
12
local sin       = math.sin
×
13
local tan       = math.tan
×
14
local rad       = math.rad
×
15
local mat4      = {}
×
16
local mat4_mt   = {}
×
17

18
-- Private constructor.
19
local function new(m)
20
        m = m or {
×
21
                0, 0, 0, 0,
22
                0, 0, 0, 0,
23
                0, 0, 0, 0,
24
                0, 0, 0, 0
×
25
        }
26
        m._m = m
×
27
        return setmetatable(m, mat4_mt)
×
28
end
29

30
-- Convert matrix into identity
31
local function identity(m)
32
        m[1],  m[2],  m[3],  m[4]  = 1, 0, 0, 0
×
33
        m[5],  m[6],  m[7],  m[8]  = 0, 1, 0, 0
×
34
        m[9],  m[10], m[11], m[12] = 0, 0, 1, 0
×
35
        m[13], m[14], m[15], m[16] = 0, 0, 0, 1
×
36
        return m
×
37
end
38

39
-- Do the check to see if JIT is enabled. If so use the optimized FFI structs.
40
local status, ffi
41
if type(jit) == "table" and jit.status() then
×
42
        --  status, ffi = pcall(require, "ffi")
43
        if status then
×
44
                ffi.cdef "typedef struct { double _m[16]; } cpml_mat4;"
×
45
                new = ffi.typeof("cpml_mat4")
×
46
        end
47
end
48

49
-- Statically allocate a temporary variable used in some of our functions.
50
local tmp = new()
×
51
local tm4 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
×
52
local tv4 = { 0, 0, 0, 0 }
×
53

54
--- The public constructor.
55
-- @param a Can be of four types: </br>
56
-- table Length 16 (4x4 matrix)
57
-- table Length 9 (3x3 matrix)
58
-- table Length 4 (4 vec4s)
59
-- nil
60
-- @treturn mat4 out
61
function mat4.new(a)
×
62
        local out = new()
×
63

64
        -- 4x4 matrix
65
        if type(a) == "table" and #a == 16 then
×
66
                for i = 1, 16 do
×
67
                        out[i] = tonumber(a[i])
×
68
                end
69

70
        -- 3x3 matrix
71
        elseif type(a) == "table" and #a == 9 then
×
72
                out[1], out[2],  out[3]  = a[1], a[2], a[3]
×
73
                out[5], out[6],  out[7]  = a[4], a[5], a[6]
×
74
                out[9], out[10], out[11] = a[7], a[8], a[9]
×
75
                out[16] = 1
×
76

77
        -- 4 vec4s
78
        elseif type(a) == "table" and type(a[1]) == "table" then
×
79
                local idx = 1
×
80
                for i = 1, 4 do
×
81
                        for j = 1, 4 do
×
82
                                out[idx] = a[i][j]
×
83
                                idx = idx + 1
×
84
                        end
85
                end
86

87
        -- nil
88
        else
89
                out[1]  = 1
×
90
                out[6]  = 1
×
91
                out[11] = 1
×
92
                out[16] = 1
×
93
        end
94

95
        return out
×
96
end
97

98
--- Create an identity matrix.
99
-- @tparam mat4 a Matrix to overwrite
100
-- @treturn mat4 out
101
function mat4.identity(a)
×
102
        return identity(a or new())
×
103
end
104

105
--- Create a matrix from an angle/axis pair.
106
-- @tparam number angle Angle of rotation
107
-- @tparam vec3 axis Axis of rotation
108
-- @treturn mat4 out
109
function mat4.from_angle_axis(angle, axis)
×
110
        local l = axis:len()
×
111
        if l == 0 then
×
112
                return new()
×
113
        end
114

115
        local x, y, z = axis.x / l, axis.y / l, axis.z / l
×
116
        local c = cos(angle)
×
117
        local s = sin(angle)
×
118

119
        return new {
×
120
                x*x*(1-c)+c,   y*x*(1-c)+z*s, x*z*(1-c)-y*s, 0,
×
121
                x*y*(1-c)-z*s, y*y*(1-c)+c,   y*z*(1-c)+x*s, 0,
×
122
                x*z*(1-c)+y*s, y*z*(1-c)-x*s, z*z*(1-c)+c,   0,
×
123
                0, 0, 0, 1
×
124
        }
125
end
126

127
--- Create a matrix from a quaternion.
128
-- @tparam quat q Rotation quaternion
129
-- @treturn mat4 out
130
function mat4.from_quaternion(q)
×
131
        return mat4.from_angle_axis(q:to_angle_axis())
×
132
end
133

134
--- Create a matrix from a direction/up pair.
135
-- @tparam vec3 direction Vector direction
136
-- @tparam vec3 up Up direction
137
-- @treturn mat4 out
138
function mat4.from_direction(direction, up)
×
139
        local forward = vec3.normalize(direction)
×
140
        local side = vec3.cross(forward, up):normalize()
×
141
        local new_up = vec3.cross(side, forward):normalize()
×
142

143
        local out = new()
×
144
        out[1]    = side.x
×
145
        out[5]    = side.y
×
146
        out[9]    = side.z
×
147
        out[2]    = new_up.x
×
148
        out[6]    = new_up.y
×
149
        out[10]   = new_up.z
×
150
        out[3]    = forward.x
×
151
        out[7]    = forward.y
×
152
        out[11]   = forward.z
×
153
        out[16]   = 1
×
154

155
        return out
×
156
end
157

158
--- Create a matrix from a transform.
159
-- @tparam vec3 trans Translation vector
160
-- @tparam quat rot Rotation quaternion
161
-- @tparam vec3 scale Scale vector
162
-- @treturn mat4 out
163
function mat4.from_transform(trans, rot, scale)
×
164
        local rx, ry, rz, rw = rot.x, rot.y, rot.z, rot.w
×
165

166
        local sm = new {
×
167
                scale.x, 0,       0,       0,
×
168
                0,       scale.y, 0,       0,
×
169
                0,       0,       scale.z, 0,
×
170
                0,       0,       0,       1,
171
        }
172

173
        local rm = new {
×
174
                1-2*(ry*ry+rz*rz), 2*(rx*ry-rz*rw), 2*(rx*rz+ry*rw), 0,
×
175
                2*(rx*ry+rz*rw), 1-2*(rx*rx+rz*rz), 2*(ry*rz-rx*rw), 0,
×
176
                2*(rx*rz-ry*rw), 2*(ry*rz+rx*rw), 1-2*(rx*rx+ry*ry), 0,
×
177
                0, 0, 0, 1
×
178
        }
179

180
        local rsm = rm * sm
×
181

182
        rsm[13] = trans.x
×
183
        rsm[14] = trans.y
×
184
        rsm[15] = trans.z
×
185

186
        return rsm
×
187
end
188

189
--- Create matrix from orthogonal.
190
-- @tparam number left
191
-- @tparam number right
192
-- @tparam number top
193
-- @tparam number bottom
194
-- @tparam number near
195
-- @tparam number far
196
-- @treturn mat4 out
197
function mat4.from_ortho(left, right, top, bottom, near, far)
×
198
        local out = new()
×
199
        out[1]    =  2 / (right - left)
×
200
        out[6]    =  2 / (top - bottom)
×
201
        out[11]   = -2 / (far - near)
×
202
        out[13]   = -((right + left) / (right - left))
×
203
        out[14]   = -((top + bottom) / (top - bottom))
×
204
        out[15]   = -((far + near) / (far - near))
×
205
        out[16]   =  1
×
206

207
        return out
×
208
end
209

210
--- Create matrix from perspective.
211
-- @tparam number fovy Field of view
212
-- @tparam number aspect Aspect ratio
213
-- @tparam number near Near plane
214
-- @tparam number far Far plane
215
-- @treturn mat4 out
216
function mat4.from_perspective(fovy, aspect, near, far)
×
217
        assert(aspect ~= 0)
×
218
        assert(near   ~= far)
×
219

220
        local t   = tan(rad(fovy) / 2)
×
221
        local out = new()
×
222
        out[1]    =  1 / (t * aspect)
×
223
        out[6]    =  1 / t
×
224
        out[11]   = -(far + near) / (far - near)
×
225
        out[12]   = -1
×
226
        out[15]   = -(2 * far * near) / (far - near)
×
227
        out[16]   =  0
×
228

229
        return out
×
230
end
231

232
-- Adapted from the Oculus SDK.
233
--- Create matrix from HMD perspective.
234
-- @tparam number tanHalfFov Tangent of half of the field of view
235
-- @tparam number zNear Near plane
236
-- @tparam number zFar Far plane
237
-- @tparam boolean flipZ Z axis is flipped or not
238
-- @tparam boolean farAtInfinity Far plane is infinite or not
239
-- @treturn mat4 out
240
function mat4.from_hmd_perspective(tanHalfFov, zNear, zFar, flipZ, farAtInfinity)
×
241
        -- CPML is right-handed and intended for GL, so these don't need to be arguments.
242
        local rightHanded = true
×
243
        local isOpenGL    = true
×
244

245
        local function CreateNDCScaleAndOffsetFromFov(tanHalfFov)
246
                local x_scale  = 2 / (tanHalfFov.LeftTan + tanHalfFov.RightTan)
×
247
                local x_offset =     (tanHalfFov.LeftTan - tanHalfFov.RightTan) * x_scale * 0.5
×
248
                local y_scale  = 2 / (tanHalfFov.UpTan   + tanHalfFov.DownTan )
×
249
                local y_offset =     (tanHalfFov.UpTan   - tanHalfFov.DownTan ) * y_scale * 0.5
×
250

251
                local result = {
×
252
                        Scale  = vec2(x_scale, y_scale),
253
                        Offset = vec2(x_offset, y_offset)
×
254
                }
255

256
                -- Hey - why is that Y.Offset negated?
257
                -- It's because a projection matrix transforms from world coords with Y=up,
258
                -- whereas this is from NDC which is Y=down.
259
                 return result
×
260
        end
261

262
        if not flipZ and farAtInfinity then
×
263
                print("Error: Cannot push Far Clip to Infinity when Z-order is not flipped")
×
264
                farAtInfinity = false
×
265
        end
266

267
         -- A projection matrix is very like a scaling from NDC, so we can start with that.
268
        local scaleAndOffset  = CreateNDCScaleAndOffsetFromFov(tanHalfFov)
×
269
        local handednessScale = rightHanded and -1.0 or 1.0
×
270
        local projection      = new()
×
271

272
        -- Produces X result, mapping clip edges to [-w,+w]
273
        projection[1] = scaleAndOffset.Scale.x
×
274
        projection[2] = 0
×
275
        projection[3] = handednessScale * scaleAndOffset.Offset.x
×
276
        projection[4] = 0
×
277

278
        -- Produces Y result, mapping clip edges to [-w,+w]
279
        -- Hey - why is that YOffset negated?
280
        -- It's because a projection matrix transforms from world coords with Y=up,
281
        -- whereas this is derived from an NDC scaling, which is Y=down.
282
        projection[5] = 0
×
283
        projection[6] = scaleAndOffset.Scale.y
×
284
        projection[7] = handednessScale * -scaleAndOffset.Offset.y
×
285
        projection[8] = 0
×
286

287
        -- Produces Z-buffer result - app needs to fill this in with whatever Z range it wants.
288
        -- We'll just use some defaults for now.
289
        projection[9]  = 0
×
290
        projection[10] = 0
×
291

292
        if farAtInfinity then
×
293
                if isOpenGL then
×
294
                        -- It's not clear this makes sense for OpenGL - you don't get the same precision benefits you do in D3D.
295
                        projection[11] = -handednessScale
×
296
                        projection[12] = 2.0 * zNear
×
297
                else
298
                        projection[11] = 0
×
299
                        projection[12] = zNear
×
300
                end
301
        else
302
                if isOpenGL then
×
303
                        -- Clip range is [-w,+w], so 0 is at the middle of the range.
304
                        projection[11] = -handednessScale * (flipZ and -1.0 or 1.0) * (zNear + zFar) / (zNear - zFar)
×
305
                        projection[12] = 2.0 * ((flipZ and -zFar or zFar) * zNear) / (zNear - zFar)
×
306
                else
307
                        -- Clip range is [0,+w], so 0 is at the start of the range.
308
                        projection[11] = -handednessScale * (flipZ and -zNear or zFar) / (zNear - zFar)
×
309
                        projection[12] = ((flipZ and -zFar or zFar) * zNear) / (zNear - zFar)
×
310
                end
311
        end
312

313
        -- Produces W result (= Z in)
314
        projection[13] = 0
×
315
        projection[14] = 0
×
316
        projection[15] = handednessScale
×
317
        projection[16] = 0
×
318

319
        return projection:transpose(projection)
×
320
end
321

322
--- Clone a matrix.
323
-- @tparam mat4 a Matrix to clone
324
-- @treturn mat4 out
325
function mat4.clone(a)
×
326
        return new(a)
×
327
end
328

329
function mul_internal(out, a, b)
×
330
        tm4[1]  = b[1]  * a[1] + b[2]  * a[5] + b[3]  * a[9]  + b[4]  * a[13]
×
331
        tm4[2]  = b[1]  * a[2] + b[2]  * a[6] + b[3]  * a[10] + b[4]  * a[14]
×
332
        tm4[3]  = b[1]  * a[3] + b[2]  * a[7] + b[3]  * a[11] + b[4]  * a[15]
×
333
        tm4[4]  = b[1]  * a[4] + b[2]  * a[8] + b[3]  * a[12] + b[4]  * a[16]
×
334
        tm4[5]  = b[5]  * a[1] + b[6]  * a[5] + b[7]  * a[9]  + b[8]  * a[13]
×
335
        tm4[6]  = b[5]  * a[2] + b[6]  * a[6] + b[7]  * a[10] + b[8]  * a[14]
×
336
        tm4[7]  = b[5]  * a[3] + b[6]  * a[7] + b[7]  * a[11] + b[8]  * a[15]
×
337
        tm4[8]  = b[5]  * a[4] + b[6]  * a[8] + b[7]  * a[12] + b[8]  * a[16]
×
338
        tm4[9]  = b[9]  * a[1] + b[10] * a[5] + b[11] * a[9]  + b[12] * a[13]
×
339
        tm4[10] = b[9]  * a[2] + b[10] * a[6] + b[11] * a[10] + b[12] * a[14]
×
340
        tm4[11] = b[9]  * a[3] + b[10] * a[7] + b[11] * a[11] + b[12] * a[15]
×
341
        tm4[12] = b[9]  * a[4] + b[10] * a[8] + b[11] * a[12] + b[12] * a[16]
×
342
        tm4[13] = b[13] * a[1] + b[14] * a[5] + b[15] * a[9]  + b[16] * a[13]
×
343
        tm4[14] = b[13] * a[2] + b[14] * a[6] + b[15] * a[10] + b[16] * a[14]
×
344
        tm4[15] = b[13] * a[3] + b[14] * a[7] + b[15] * a[11] + b[16] * a[15]
×
345
        tm4[16] = b[13] * a[4] + b[14] * a[8] + b[15] * a[12] + b[16] * a[16]
×
346

347
        for i = 1, 16 do
×
348
                out[i] = tm4[i]
×
349
        end
350
end
351

352
--- Multiply N matrices.
353
-- @tparam mat4 out Matrix to store the result
354
-- @tparam mat4 or {mat4, ...} left hand operand(s)
355
-- @tparam mat4 right hand operand if a is not table
356
-- @treturn mat4 out multiplied matrix result
357
function mat4.mul(out, a, b)
×
358
        if mat4.is_mat4(a) then
×
359
                mul_internal(out, a, b)
×
360
                return out
×
361
        end
362
        if #a == 0 then
×
363
                identity(out)
×
364
        elseif #a == 1 then
×
365
                -- only one matrix, just copy
366
                for i = 1, 16 do
×
367
                        out[i] = a[1][i]
×
368
                end
369
        else
370
                local ma = a[1]
×
371
                local mb = a[2]
×
372
                for i = 2, #a do
×
373
                        mul_internal(out, ma, mb)
×
374
                        ma = out
×
375
                end
376
        end
377
        return out
×
378
end
379

380
--- Multiply a matrix and a vec3, with perspective division.
381
-- This function uses an implicit 1 for the fourth component.
382
-- @tparam vec3 out vec3 to store the result
383
-- @tparam mat4 a Left hand operand
384
-- @tparam vec3 b Right hand operand
385
-- @treturn vec3 out
386
function mat4.mul_vec3_perspective(out, a, b)
×
387
        local v4x = b.x * a[1] + b.y * a[5] + b.z * a[9]  + a[13]
×
388
        local v4y = b.x * a[2] + b.y * a[6] + b.z * a[10] + a[14]
×
389
        local v4z = b.x * a[3] + b.y * a[7] + b.z * a[11] + a[15]
×
390
        local v4w = b.x * a[4] + b.y * a[8] + b.z * a[12] + a[16]
×
391
        local inv_w = 0
×
392
        if v4w ~= 0 then
×
393
                inv_w = utils.sign(v4w) / v4w
×
394
        end
395
        out.x = v4x * inv_w
×
396
        out.y = v4y * inv_w
×
397
        out.z = v4z * inv_w
×
398
        return out
×
399
end
400

401
--- Multiply a matrix and a vec4.
402
-- @tparam table out table to store the result
403
-- @tparam mat4 a Left hand operand
404
-- @tparam table b Right hand operand
405
-- @treturn vec4 out
406
function mat4.mul_vec4(out, a, b)
×
407
        tv4[1] = b[1] * a[1] + b[2] * a[5] + b [3] * a[9]  + b[4] * a[13]
×
408
        tv4[2] = b[1] * a[2] + b[2] * a[6] + b [3] * a[10] + b[4] * a[14]
×
409
        tv4[3] = b[1] * a[3] + b[2] * a[7] + b [3] * a[11] + b[4] * a[15]
×
410
        tv4[4] = b[1] * a[4] + b[2] * a[8] + b [3] * a[12] + b[4] * a[16]
×
411

412
        for i = 1, 4 do
×
413
                out[i] = tv4[i]
×
414
        end
415

416
        return out
×
417
end
418

419
--- Invert a matrix.
420
-- @tparam mat4 out Matrix to store the result
421
-- @tparam mat4 a Matrix to invert
422
-- @treturn mat4 out
423
function mat4.invert(out, a)
×
424
        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]
×
425
        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]
×
426
        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]
×
427
        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]
×
428
        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]
×
429
        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]
×
430
        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]
×
431
        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]
×
432
        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]
×
433
        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]
×
434
        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]
×
435
        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]
×
436
        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]
×
437
        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]
×
438
        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]
×
439
        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]
×
440

441
        local det = a[1] * tm4[1] + a[2] * tm4[5] + a[3] * tm4[9] + a[4] * tm4[13]
×
442

443
        if det == 0 then return a end
×
444

445
        det = 1 / det
×
446

447
        for i = 1, 16 do
×
448
                out[i] = tm4[i] * det
×
449
        end
450

451
        return out
×
452
end
453

454
--- Scale a matrix.
455
-- @tparam mat4 out Matrix to store the result
456
-- @tparam mat4 a Matrix to scale
457
-- @tparam vec3 s Scalar
458
-- @treturn mat4 out
459
function mat4.scale(out, a, s)
×
460
        identity(tmp)
×
461
        tmp[1]  = s.x or s[1]
×
462
        tmp[6]  = s.y or s[2]
×
463
        tmp[11] = s.z or s[3]
×
464

465
        return out:mul(tmp, a)
×
466
end
467

468
--- Rotate a matrix.
469
-- @tparam mat4 out Matrix to store the result
470
-- @tparam mat4 a Matrix to rotate
471
-- @tparam number angle Angle to rotate by (in radians)
472
-- @tparam vec3 axis Axis to rotate on
473
-- @treturn mat4 out
474
function mat4.rotate(out, a, angle, axis)
×
475
        if type(angle) == "table" or type(angle) == "cdata" then
×
476
                angle, axis = angle:to_angle_axis()
×
477
        end
478

479
        local l = axis:len()
×
480

481
        if l == 0 then
×
482
                return a
×
483
        end
484

485
        local x, y, z = axis.x / l, axis.y / l, axis.z / l
×
486
        local c = cos(angle)
×
487
        local s = sin(angle)
×
488

489
        identity(tmp)
×
490
        tmp[1]  = x * x * (1 - c) + c
×
491
        tmp[2]  = y * x * (1 - c) + z * s
×
492
        tmp[3]  = x * z * (1 - c) - y * s
×
493
        tmp[5]  = x * y * (1 - c) - z * s
×
494
        tmp[6]  = y * y * (1 - c) + c
×
495
        tmp[7]  = y * z * (1 - c) + x * s
×
496
        tmp[9]  = x * z * (1 - c) + y * s
×
497
        tmp[10] = y * z * (1 - c) - x * s
×
498
        tmp[11] = z * z * (1 - c) + c
×
499

500
        return out:mul(tmp, a)
×
501
end
502

503
--- Translate a matrix.
504
-- @tparam mat4 out Matrix to store the result
505
-- @tparam mat4 a Matrix to translate
506
-- @tparam vec3 t Translation vector
507
-- @treturn mat4 out
508
function mat4.translate(out, a, t)
×
509
        identity(tmp)
×
510
        tmp[13] = t.x or t[1]
×
511
        tmp[14] = t.y or t[2]
×
512
        tmp[15] = t.z or t[3]
×
513

514
        return out:mul(tmp, a)
×
515
end
516

517
--- Shear a matrix.
518
-- @tparam mat4 out Matrix to store the result
519
-- @tparam mat4 a Matrix to translate
520
-- @tparam number yx
521
-- @tparam number zx
522
-- @tparam number xy
523
-- @tparam number zy
524
-- @tparam number xz
525
-- @tparam number yz
526
-- @treturn mat4 out
527
function mat4.shear(out, a, yx, zx, xy, zy, xz, yz)
×
528
        identity(tmp)
×
529
        tmp[2]  = yx or 0
×
530
        tmp[3]  = zx or 0
×
531
        tmp[5]  = xy or 0
×
532
        tmp[7]  = zy or 0
×
533
        tmp[9]  = xz or 0
×
534
        tmp[10] = yz or 0
×
535

536
        return out:mul(tmp, a)
×
537
end
538

539
--- Reflect a matrix across a plane.
540
-- @tparam mat4 Matrix to store the result
541
-- @tparam a Matrix to reflect
542
-- @tparam vec3 position A point on the plane
543
-- @tparam vec3 normal The (normalized!) normal vector of the plane
544
function mat4.reflect(out, a, position, normal)
×
545
        local nx, ny, nz = normal:unpack()
×
546
        local d = -position:dot(normal)
×
547
        tmp[1] = 1 - 2 * nx ^ 2
×
548
        tmp[2] = 2 * nx * ny
×
549
        tmp[3] = -2 * nx * nz
×
550
        tmp[4] = 0
×
551
        tmp[5] = -2 * nx * ny
×
552
        tmp[6] = 1 - 2 * ny ^ 2
×
553
        tmp[7] = -2 * ny * nz
×
554
        tmp[8] = 0
×
555
        tmp[9] = -2 * nx * nz
×
556
        tmp[10] = -2 * ny * nz
×
557
        tmp[11] = 1 - 2 * nz ^ 2
×
558
        tmp[12] = 0
×
559
        tmp[13] = -2 * nx * d
×
560
        tmp[14] = -2 * ny * d
×
561
        tmp[15] = -2 * nz * d
×
562
        tmp[16] = 1
×
563

564
        return out:mul(tmp, a)
×
565
end
566

567
--- Transform matrix to look at a point.
568
-- @tparam mat4 out Matrix to store result
569
-- @tparam vec3 eye Location of viewer's view plane
570
-- @tparam vec3 center Location of object to view
571
-- @tparam vec3 up Up direction
572
-- @treturn mat4 out
573
function mat4.look_at(out, eye, look_at, up)
×
574
        local z_axis = (eye - look_at):normalize()
×
575
        local x_axis = up:cross(z_axis):normalize()
×
576
        local y_axis = z_axis:cross(x_axis)
×
577
        out[1] = x_axis.x
×
578
        out[2] = y_axis.x
×
579
        out[3] = z_axis.x
×
580
        out[4] = 0
×
581
        out[5] = x_axis.y
×
582
        out[6] = y_axis.y
×
583
        out[7] = z_axis.y
×
584
        out[8] = 0
×
585
        out[9] = x_axis.z
×
586
        out[10] = y_axis.z
×
587
        out[11] = z_axis.z
×
588
        out[12] = 0
×
589
        out[13] = -out[  1]*eye.x - out[4+1]*eye.y - out[8+1]*eye.z
×
590
        out[14] = -out[  2]*eye.x - out[4+2]*eye.y - out[8+2]*eye.z
×
591
        out[15] = -out[  3]*eye.x - out[4+3]*eye.y - out[8+3]*eye.z
×
592
        out[16] = -out[  4]*eye.x - out[4+4]*eye.y - out[8+4]*eye.z + 1
×
593
        return out
×
594
end
595

596
--- Transform matrix to target a point.
597
-- @tparam mat4 out Matrix to store result
598
-- @tparam vec3 eye Location of viewer's view plane
599
-- @tparam vec3 center Location of object to view
600
-- @tparam vec3 up Up direction
601
-- @treturn mat4 out
602
function mat4.target(out, from, to, up)
×
603
        local z_axis = (from - to):normalize()
×
604
        local x_axis = up:cross(z_axis):normalize()
×
605
        local y_axis = z_axis:cross(x_axis)
×
606
        out[1] = x_axis.x
×
607
        out[2] = x_axis.y
×
608
        out[3] = x_axis.z
×
609
        out[4] = 0
×
610
        out[5] = y_axis.x
×
611
        out[6] = y_axis.y
×
612
        out[7] = y_axis.z
×
613
        out[8] = 0
×
614
        out[9] = z_axis.x
×
615
        out[10] = z_axis.y
×
616
        out[11] = z_axis.z
×
617
        out[12] = 0
×
618
        out[13] = from.x
×
619
        out[14] = from.y
×
620
        out[15] = from.z
×
621
        out[16] = 1
×
622
        return out
×
623
end
624

625
--- Transpose a matrix.
626
-- @tparam mat4 out Matrix to store the result
627
-- @tparam mat4 a Matrix to transpose
628
-- @treturn mat4 out
629
function mat4.transpose(out, a)
×
630
        tm4[1]  = a[1]
×
631
        tm4[2]  = a[5]
×
632
        tm4[3]  = a[9]
×
633
        tm4[4]  = a[13]
×
634
        tm4[5]  = a[2]
×
635
        tm4[6]  = a[6]
×
636
        tm4[7]  = a[10]
×
637
        tm4[8]  = a[14]
×
638
        tm4[9]  = a[3]
×
639
        tm4[10] = a[7]
×
640
        tm4[11] = a[11]
×
641
        tm4[12] = a[15]
×
642
        tm4[13] = a[4]
×
643
        tm4[14] = a[8]
×
644
        tm4[15] = a[12]
×
645
        tm4[16] = a[16]
×
646

647
        for i = 1, 16 do
×
648
                out[i] = tm4[i]
×
649
        end
650

651
        return out
×
652
end
653

654
--- Project a point into screen space
655
-- @tparam vec3 obj Object position in world space
656
-- @tparam mat4 mvp Projection matrix
657
-- @tparam table viewport XYWH of viewport
658
-- @treturn vec3 win
659
function mat4.project(obj, mvp, viewport)
×
660
        local point = mat4.mul_vec3_perspective(vec3(), mvp, obj)
×
661
        point.x = point.x * 0.5 + 0.5
×
662
        point.y = point.y * 0.5 + 0.5
×
663
        point.z = point.z * 0.5 + 0.5
×
664
        point.x = point.x * viewport[3] + viewport[1]
×
665
        point.y = point.y * viewport[4] + viewport[2]
×
666
        return point
×
667
end
668

669
--- Unproject a point from screen space to world space.
670
-- @tparam vec3 win Object position in screen space
671
-- @tparam mat4 mvp Projection matrix
672
-- @tparam table viewport XYWH of viewport
673
-- @treturn vec3 obj
674
function mat4.unproject(win, mvp, viewport)
×
675
        local point = vec3.clone(win)
×
676

677
        -- 0..n -> 0..1
678
        point.x = (point.x - viewport[1]) / viewport[3]
×
679
        point.y = (point.y - viewport[2]) / viewport[4]
×
680

681
        -- 0..1 -> -1..1
682
        point.x = point.x * 2 - 1
×
683
        point.y = point.y * 2 - 1
×
684
        point.z = point.z * 2 - 1
×
685

686
        return mat4.mul_vec3_perspective(point, tmp:invert(mvp), point)
×
687
end
688

689
--- Return a boolean showing if a table is or is not a mat4.
690
-- @tparam mat4 a Matrix to be tested
691
-- @treturn boolean is_mat4
692
function mat4.is_mat4(a)
×
693
        if type(a) == "cdata" then
×
694
                return ffi.istype("cpml_mat4", a)
×
695
        end
696

697
        if type(a) ~= "table" then
×
698
                return false
×
699
        end
700

701
        for i = 1, 16 do
×
702
                if type(a[i]) ~= "number" then
×
703
                        return false
×
704
                end
705
        end
706

707
        return true
×
708
end
709

710
--- Return whether any component is NaN
711
-- @tparam mat4 a Matrix to be tested
712
-- @treturn boolean if any component is NaN
713
function vec2.has_nan(a)
×
714
        for i = 1, 16 do
×
715
                if private.is_nan(a[i]) then
×
716
                        return true
×
717
                end
718
        end
719
        return false
×
720
end
721

722
--- Return a formatted string.
723
-- @tparam mat4 a Matrix to be turned into a string
724
-- @treturn string formatted
725
function mat4.to_string(a)
×
726
        local str = "[ "
×
727
        for i = 1, 16 do
×
728
                str = str .. string.format("%+0.3f", a[i])
×
729
                if i < 16 then
×
730
                        str = str .. ", "
×
731
                end
732
        end
733
        str = str .. " ]"
×
734
        return str
×
735
end
736

737
--- Convert a matrix to row vec4s.
738
-- @tparam mat4 a Matrix to be converted
739
-- @treturn table vec4s
740
function mat4.to_vec4s(a)
×
741
        return {
×
742
                { a[1],  a[2],  a[3],  a[4]  },
×
743
                { a[5],  a[6],  a[7],  a[8]  },
×
744
                { a[9],  a[10], a[11], a[12] },
×
745
                { a[13], a[14], a[15], a[16] }
×
746
        }
747
end
748

749
--- Convert a matrix to col vec4s.
750
-- @tparam mat4 a Matrix to be converted
751
-- @treturn table vec4s
752
function mat4.to_vec4s_cols(a)
×
753
        return {
×
754
                { a[1], a[5], a[9],  a[13] },
×
755
                { a[2], a[6], a[10], a[14] },
×
756
                { a[3], a[7], a[11], a[15] },
×
757
                { a[4], a[8], a[12], a[16] }
×
758
        }
759
end
760

761
-- http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/
762
--- Convert a matrix to a quaternion.
763
-- @tparam mat4 a Matrix to be converted
764
-- @treturn quat out
765
function mat4.to_quat(a)
×
766
        identity(tmp):transpose(a)
×
767

768
        local w     = sqrt(1 + tmp[1] + tmp[6] + tmp[11]) / 2
×
769
        local scale = w * 4
×
770
        local q     = quat.new(
×
771
                tmp[10] - tmp[7] / scale,
×
772
                tmp[3]  - tmp[9] / scale,
×
773
                tmp[5]  - tmp[2] / scale,
×
774
                w
775
        )
776

777
        return q:normalize(q)
×
778
end
779

780
-- http://www.crownandcutlass.com/features/technicaldetails/frustum.html
781
--- Convert a matrix to a frustum.
782
-- @tparam mat4 a Matrix to be converted (projection * view)
783
-- @tparam boolean infinite Infinite removes the far plane
784
-- @treturn frustum out
785
function mat4.to_frustum(a, infinite)
×
786
        local t
787
        local frustum = {}
×
788

789
        -- Extract the LEFT plane
790
        frustum.left   = {}
×
791
        frustum.left.a = a[4]  + a[1]
×
792
        frustum.left.b = a[8]  + a[5]
×
793
        frustum.left.c = a[12] + a[9]
×
794
        frustum.left.d = a[16] + a[13]
×
795

796
        -- Normalize the result
797
        t = sqrt(frustum.left.a * frustum.left.a + frustum.left.b * frustum.left.b + frustum.left.c * frustum.left.c)
×
798
        frustum.left.a = frustum.left.a / t
×
799
        frustum.left.b = frustum.left.b / t
×
800
        frustum.left.c = frustum.left.c / t
×
801
        frustum.left.d = frustum.left.d / t
×
802

803
        -- Extract the RIGHT plane
804
        frustum.right   = {}
×
805
        frustum.right.a = a[4]  - a[1]
×
806
        frustum.right.b = a[8]  - a[5]
×
807
        frustum.right.c = a[12] - a[9]
×
808
        frustum.right.d = a[16] - a[13]
×
809

810
        -- Normalize the result
811
        t = sqrt(frustum.right.a * frustum.right.a + frustum.right.b * frustum.right.b + frustum.right.c * frustum.right.c)
×
812
        frustum.right.a = frustum.right.a / t
×
813
        frustum.right.b = frustum.right.b / t
×
814
        frustum.right.c = frustum.right.c / t
×
815
        frustum.right.d = frustum.right.d / t
×
816

817
        -- Extract the BOTTOM plane
818
        frustum.bottom   = {}
×
819
        frustum.bottom.a = a[4]  + a[2]
×
820
        frustum.bottom.b = a[8]  + a[6]
×
821
        frustum.bottom.c = a[12] + a[10]
×
822
        frustum.bottom.d = a[16] + a[14]
×
823

824
        -- Normalize the result
825
        t = sqrt(frustum.bottom.a * frustum.bottom.a + frustum.bottom.b * frustum.bottom.b + frustum.bottom.c * frustum.bottom.c)
×
826
        frustum.bottom.a = frustum.bottom.a / t
×
827
        frustum.bottom.b = frustum.bottom.b / t
×
828
        frustum.bottom.c = frustum.bottom.c / t
×
829
        frustum.bottom.d = frustum.bottom.d / t
×
830

831
        -- Extract the TOP plane
832
        frustum.top   = {}
×
833
        frustum.top.a = a[4]  - a[2]
×
834
        frustum.top.b = a[8]  - a[6]
×
835
        frustum.top.c = a[12] - a[10]
×
836
        frustum.top.d = a[16] - a[14]
×
837

838
        -- Normalize the result
839
        t = sqrt(frustum.top.a * frustum.top.a + frustum.top.b * frustum.top.b + frustum.top.c * frustum.top.c)
×
840
        frustum.top.a = frustum.top.a / t
×
841
        frustum.top.b = frustum.top.b / t
×
842
        frustum.top.c = frustum.top.c / t
×
843
        frustum.top.d = frustum.top.d / t
×
844

845
        -- Extract the NEAR plane
846
        frustum.near   = {}
×
847
        frustum.near.a = a[4]  + a[3]
×
848
        frustum.near.b = a[8]  + a[7]
×
849
        frustum.near.c = a[12] + a[11]
×
850
        frustum.near.d = a[16] + a[15]
×
851

852
        -- Normalize the result
853
        t = sqrt(frustum.near.a * frustum.near.a + frustum.near.b * frustum.near.b + frustum.near.c * frustum.near.c)
×
854
        frustum.near.a = frustum.near.a / t
×
855
        frustum.near.b = frustum.near.b / t
×
856
        frustum.near.c = frustum.near.c / t
×
857
        frustum.near.d = frustum.near.d / t
×
858

859
        if not infinite then
×
860
                -- Extract the FAR plane
861
                frustum.far   = {}
×
862
                frustum.far.a = a[4]  - a[3]
×
863
                frustum.far.b = a[8]  - a[7]
×
864
                frustum.far.c = a[12] - a[11]
×
865
                frustum.far.d = a[16] - a[15]
×
866

867
                -- Normalize the result
868
                t = sqrt(frustum.far.a * frustum.far.a + frustum.far.b * frustum.far.b + frustum.far.c * frustum.far.c)
×
869
                frustum.far.a = frustum.far.a / t
×
870
                frustum.far.b = frustum.far.b / t
×
871
                frustum.far.c = frustum.far.c / t
×
872
                frustum.far.d = frustum.far.d / t
×
873
        end
874

875
        return frustum
×
876
end
877

878
function mat4_mt.__index(t, k)
×
879
        if type(t) == "cdata" then
×
880
                if type(k) == "number" then
×
881
                        return t._m[k-1]
×
882
                end
883
        end
884

885
        return rawget(mat4, k)
×
886
end
887

888
function mat4_mt.__newindex(t, k, v)
×
889
        if type(t) == "cdata" then
×
890
                if type(k) == "number" then
×
891
                        t._m[k-1] = v
×
892
                end
893
        end
894
end
895

896
mat4_mt.__tostring = mat4.to_string
×
897

898
function mat4_mt.__call(_, a)
×
899
        return mat4.new(a)
×
900
end
901

902
function mat4_mt.__unm(a)
×
903
        return new():invert(a)
×
904
end
905

906
function mat4_mt.__eq(a, b)
×
907
        if not mat4.is_mat4(a) or not mat4.is_mat4(b) then
×
908
                return false
×
909
        end
910

911
        for i = 1, 16 do
×
912
                if not utils.tolerance(b[i]-a[i], constants.FLT_EPSILON) then
×
913
                        return false
×
914
                end
915
        end
916

917
        return true
×
918
end
919

920
function mat4_mt.__mul(a, b)
×
921
        precond.assert(mat4.is_mat4(a), "__mul: Wrong argument type '%s' for left hand operand. (<cpml.mat4> expected)", type(a))
×
922

923
        if vec3.is_vec3(b) then
×
924
                return mat4.mul_vec3_perspective(vec3(), a, b)
×
925
        end
926

927
        assert(mat4.is_mat4(b) or #b == 4, "__mul: Wrong argument type for right hand operand. (<cpml.mat4> or table #4 expected)")
×
928

929
        if mat4.is_mat4(b) then
×
930
                return new():mul(a, b)
×
931
        end
932

933
        return mat4.mul_vec4({}, a, b)
×
934
end
935

936
if status then
×
937
        xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded
×
938
                ffi.metatype(new, mat4_mt)
×
939
        end, function() end)
×
940
end
941

942
return setmetatable({}, mat4_mt)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc