My perspective projection is messed up?
The math for the projection matrix is (with fov as $\alpha$):

$q \leftarrow \frac{1}{tan(\frac{\alpha}{2})}$

$a \leftarrow \frac{q}{aspect}$

$b \leftarrow \frac{(far + near)}{(near - far)}$

$c \leftarrow \frac{(2 * far * near)}{(near - far)}$

Notice that there're some things you're doing that are differently, such as the order of your subtractions between near and far, how you organize the matrix values, and your multiplication between your r * t.

Using the variables above, the column-major matrix below would be the resulting perspective projection matrix:

\begin{bmatrix}
  a &amp; 0 &amp;  0 &amp; 0 \\
  0 &amp; q &amp;  0 &amp; 0 \\
  0 &amp; 0 &amp;  b &amp; c \\
  0 &amp; 0 &amp; -1 &amp; 0
\end{bmatrix}

From the above, we get:

def perspective_projection_matrix(fov, aspect, near, far):
    q = 1 / tan(radians(fov * 0.5))
    a = q / aspect
    b = (far + near) / (near - far)
    c = (2*near*far) / (near - far)

    # construct column-major matrix here...


NOTE: I left the last part out because I'm not familiar enough with numpy to know whether it expects row-major or column-major order.

Also, you should validate all your arguments (e.g. both near &gt; 0 and far &gt; 0, far &gt; near, etc.) if you want to avoid future headaches.