DramaticShapeVoxelMod archival mirror · read-only
lib/Mat4.lua

Mat4.lua

lib/Mat4.lua5.4 KB100644d2486d9248
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
-- Minimal 4x4 matrix math for voxel world mode.
--
-- Row-major, and sent to the shader with shader:send("mvp", "row", m).
-- LOVE 11.5's matrix uniform defaults to column-major, so the "row" layout
-- argument is what lets these tables read the same way they are written
-- here -- translation in the fourth column, m[4]/m[8]/m[12].
--
-- Only what the renderer actually needs: a perspective projection (the
-- camera), an orthographic one (the sun's shadow pass), an asymmetric one
-- (a headset's per-eye frustum), a look-based view, a quaternion rotation
-- (a headset's pose), and the translate/rotateY/scale a model matrix is
-- built from. No general inverse -- the VR view inverts its rigid pieces
-- one at a time.

local Mat4 = {}

function Mat4.identity()
  return { 1, 0, 0, 0,
           0, 1, 0, 0,
           0, 0, 1, 0,
           0, 0, 0, 1 }
end

-- a * b, both row-major
function Mat4.mul(a, b)
  local o = {}
  for r = 0, 3 do
    local a0, a1 = a[r * 4 + 1], a[r * 4 + 2]
    local a2, a3 = a[r * 4 + 3], a[r * 4 + 4]
    for c = 1, 4 do
      o[r * 4 + c] = a0 * b[c] + a1 * b[4 + c] + a2 * b[8 + c] + a3 * b[12 + c]
    end
  end
  return o
end

function Mat4.translate(x, y, z)
  return { 1, 0, 0, x,
           0, 1, 0, y,
           0, 0, 1, z,
           0, 0, 0, 1 }
end

function Mat4.scale(x, y, z)
  return { x, 0, 0, 0,
           0, y, 0, 0,
           0, 0, z, 0,
           0, 0, 0, 1 }
end

function Mat4.rotateY(a)
  local c, s = math.cos(a), math.sin(a)
  return { c, 0, s, 0,
           0, 1, 0, 0,
          -s, 0, c, 0,
           0, 0, 0, 1 }
end

function Mat4.rotateX(a)
  local c, s = math.cos(a), math.sin(a)
  return { 1, 0, 0, 0,
           0, c, -s, 0,
           0, s, c, 0,
           0, 0, 0, 1 }
end

function Mat4.rotateZ(a)
  local c, s = math.cos(a), math.sin(a)
  return { c, -s, 0, 0,
           s, c, 0, 0,
           0, 0, 1, 0,
           0, 0, 0, 1 }
end

-- The rotation a unit quaternion describes, row-major. The VR rig is what
-- needs it: an OpenXR eye pose arrives as position + orientation
-- quaternion, and both the eye's transform and its inverse (the view) are
-- built from this.
function Mat4.fromQuat(x, y, z, w)
  local xx, yy, zz = x * x, y * y, z * z
  local xy, xz, yz = x * y, x * z, y * z
  local wx, wy, wz = w * x, w * y, w * z
  return { 1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy), 0,
           2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx), 0,
           2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy), 0,
           0, 0, 0, 1 }
end

-- Transpose. For a pure rotation this IS the inverse, which is how the VR
-- view matrix is assembled without a general 4x4 inverse.
function Mat4.transpose(m)
  return { m[1], m[5], m[9], m[13],
           m[2], m[6], m[10], m[14],
           m[3], m[7], m[11], m[15],
           m[4], m[8], m[12], m[16] }
end

-- Right-handed perspective from an OpenXR-style asymmetric field of view:
-- four signed HALF-ANGLES off the view axis (left and down negative), onto
-- GL clip space (z in [-1, 1]). A headset's per-eye frustum is off-centre
-- -- the nose side is narrower than the temple side -- so the symmetric
-- perspective() above cannot express it.
function Mat4.fovProjection(angleLeft, angleRight, angleUp, angleDown,
                            near, far)
  local l, r = math.tan(angleLeft), math.tan(angleRight)
  local u, d = math.tan(angleUp), math.tan(angleDown)
  local w, h, dz = r - l, u - d, near - far
  return { 2 / w, 0, (r + l) / w, 0,
           0, 2 / h, (u + d) / h, 0,
           0, 0, (far + near) / dz, (2 * far * near) / dz,
           0, 0, -1, 0 }
end

-- Right-handed perspective onto GL clip space (z in [-1, 1]).
function Mat4.perspective(fovY, aspect, near, far)
  local f = 1 / math.tan(fovY / 2)
  local d = near - far
  return { f / aspect, 0, 0, 0,
           0, f, 0, 0,
           0, 0, (far + near) / d, (2 * far * near) / d,
           0, 0, -1, 0 }
end

-- Right-handed orthographic projection onto GL clip space (z in [-1, 1]).
-- The view-space box is x in [l, r], y in [b, t], z in [-f, -n] -- near and
-- far are DISTANCES down the view's -z, exactly as in perspective() above.
-- Parallel, so a sun is a direction and nothing else: no eye point, no
-- foreshortening, and clip z stays linear in world units, which is what
-- lets the shadow pass store depth as a plain number.
function Mat4.ortho(l, r, b, t, n, f)
  return { 2 / (r - l), 0, 0, -(r + l) / (r - l),
           0, 2 / (t - b), 0, -(t + b) / (t - b),
           0, 0, -2 / (f - n), -(f + n) / (f - n),
           0, 0, 0, 1 }
end

-- Right-handed look-at. eye/target/up are {x, y, z}.
function Mat4.lookAt(eye, target, up)
  local function sub(a, b) return { a[1] - b[1], a[2] - b[2], a[3] - b[3] } end
  local function norm(v)
    local l = math.sqrt(v[1] * v[1] + v[2] * v[2] + v[3] * v[3])
    if l == 0 then return { 0, 0, 0 } end
    return { v[1] / l, v[2] / l, v[3] / l }
  end
  local function cross(a, b)
    return { a[2] * b[3] - a[3] * b[2],
             a[3] * b[1] - a[1] * b[3],
             a[1] * b[2] - a[2] * b[1] }
  end
  local function dot(a, b) return a[1] * b[1] + a[2] * b[2] + a[3] * b[3] end

  local f = norm(sub(target, eye))     -- forward
  local s = norm(cross(f, up))         -- right
  local u = cross(s, f)                -- true up
  return { s[1], s[2], s[3], -dot(s, eye),
           u[1], u[2], u[3], -dot(u, eye),
          -f[1], -f[2], -f[3], dot(f, eye),
           0, 0, 0, 1 }
end

return Mat4