From 1c172fcde24131a95df600f49b9d7aaaf8276213 Mon Sep 17 00:00:00 2001 From: Daniel Snider Date: Thu, 2 Jun 2022 11:28:52 -0700 Subject: [PATCH] Initial commit --- Perspective.ipynb | 490 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 Perspective.ipynb diff --git a/Perspective.ipynb b/Perspective.ipynb new file mode 100644 index 0000000..46da1fc --- /dev/null +++ b/Perspective.ipynb @@ -0,0 +1,490 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5f5e1e09-487e-421d-ad34-94a8e3b42629", + "metadata": { + "tags": [] + }, + "source": [ + "# Deriving a perspective matrix given field of view and near & far z-planes\n", + "\n", + "I am looking for a function $f(\\theta, Z_n, Z_f) = \\begin{bmatrix}\\ddots\\end{bmatrix}$ that will return a 4x4 perspective matrix, where:\n", + "\n", + " * $\\theta$ is the vertical field of view,\n", + " * $Z_n$ is the near z clipping plane, and\n", + " * $Z_f$ is the far z clipping plane." + ] + }, + { + "cell_type": "markdown", + "id": "575394e6-9924-4691-b212-e98fbb824bf8", + "metadata": { + "tags": [] + }, + "source": [ + "## Vertex Pipeline\n", + "\n", + "Suppose I start with a set of untransformed vertices, $V$, that make up my 3D scene, where $v_w = 1$ for all $v \\in V$.\n", + "\n", + "OpenGL will follow these steps when transforming vertices:\n", + " \n", + " 1. Allow me to **transform my vertices**, $V$, into transformed vertices, $V'$.\n", + " 1. **Perspective divide** my transformed vertices, $V'$, into projected vertices, $V''$.\n", + " 1. **Clip each point** $v''$ using a $2\\times2\\times2$ cube centered at the origin.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "95b7bd1c-93a9-4504-a60b-08b57c2b0aab", + "metadata": {}, + "source": [ + "### Transforming\n", + "\n", + "I want to get away with a single matrix multiply per vertex:\n", + "$$\n", + "v' = f(\\theta, Z_n, Z_f) \\cdot v\n", + "$$\n", + "so I need to carefully build the function $f$ while giving consideration to the steps that come after." + ] + }, + { + "cell_type": "markdown", + "id": "7901a150-bd3a-48e0-8d11-a4b12a2c2ca3", + "metadata": {}, + "source": [ + "### Perspective divide\n", + "\n", + "OpenGL will divide each $v'$'s $x, y, z$ by its $w$:\n", + "$$\n", + "v''= <\\frac{v'_x}{v'_w}, \\frac{v'_y}{v'_w}, \\frac{v'_z}{v'_w}, v'_{w}>\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "18f57f5a-c1a4-4bd1-b572-9965039ca2c6", + "metadata": {}, + "source": [ + "### Clipping\n", + "\n", + "After the perspective divide, all values are clipped to be inside a $2\\times2\\times2$ cube centered at the origin so that. So that\n", + "\n", + "$$\n", + "-1 \\le v''_x \\le 1\\\\\n", + "-1 \\le v''_y \\le 1\\\\\n", + "-1 \\le v''_z \\le 1\\\\\n", + "$$\n", + "\n", + "*Side note*: Direct3D's clipping bounds are different than OpenGL's, so $f$ needs to be different for it.\n" + ] + }, + { + "cell_type": "markdown", + "id": "1e7ac1fc-5dc1-4a40-95e4-2fb2ee5d2357", + "metadata": { + "tags": [] + }, + "source": [ + "## Finding $v'$\n", + "\n", + "I'll start by putting variables ($a$ through $p$) in each spot in the matrix which we can solve for below:\n", + "\n", + "$$\n", + "f(\\theta, Z_n, Z_f) = \\begin{bmatrix}\n", + "a & b & c & d\\\\\n", + "e & f & g & h\\\\\n", + "i & j & k & l\\\\\n", + "m & n & o & p\n", + "\\end{bmatrix}\n", + "$$\n", + "\n", + "Since $v'$ = $f(\\ldots) \\cdot v$, I can multiply that out to get:\n", + "\n", + "$$\n", + "\\begin{align}\n", + "v'_x &= av_x + bv_y + cv_z + d\\\\\n", + "v'_y &= ev_x + fv_y + gv_z + h\\\\\n", + "v'_z &= iv_x + jv_y + kv_z + l\\\\\n", + "v'_w &= mv_x + nv_y + ov_z + p\n", + "\\end{align}\n", + "$$\n", + "\n", + "(I'm using the fact that $v_w = 1$ here)\n" + ] + }, + { + "cell_type": "markdown", + "id": "8efa1546-9022-4d3a-9f46-162ee45cb22f", + "metadata": {}, + "source": [ + "## Now finding $v''$\n", + "\n", + "I said earlier that:\n", + "\n", + "$$\n", + "v''= <\\frac{v'_x}{v'_w}, \\frac{v'_y}{v'_w}, \\frac{v'_z}{v'_w}, v'_{w}>\n", + "$$\n", + "\n", + "so substituting $v'$ in from above gives me:\n", + "\n", + "$$\n", + "\\begin{align}\n", + "v''_x &= \\frac{av_x + bv_y + cv_z + d}{mv_x + nv_y + ov_z + p}\\\\\n", + "v''_y &= \\frac{ev_x + fv_y + gv_z + h}{mv_x + nv_y + ov_z + p}\\\\\n", + "v''_z &= \\frac{iv_x + jv_y + kv_z + l}{mv_x + nv_y + ov_z + p}\\\\\n", + "\\end{align}\n", + "$$\n", + "\n", + "I don't really care what happens to $v''_w$ so I left it out." + ] + }, + { + "cell_type": "markdown", + "id": "6795607c-23ab-43ea-ae1a-16ba655b015d", + "metadata": {}, + "source": [ + "## Figuring out what I want $v''_x$ to actually be\n", + "\n", + "I'll skip the derivation for now, but knowing field of view, $\\theta$, and the size of the clipping box, i.e. $2\\times2\\times2$, I can use a pinhole camera model to say:\n", + "\n", + "$$\n", + "v''_x = d\\frac{v_x}{v_z}\n", + "$$\n", + "\n", + "where $d$ is the distance to the back of the pinhole camera. I can form a right triangle with an angle of $\\frac{\\theta}{2}$ and an opposite edge being the extent of the clipping cube, i.e. $1$, plus the definition of $\\tan(\\theta)$, to get:\n", + "\n", + "$$\n", + "\\tan(\\frac{\\theta}{2}) = \\frac{1}{d}\n", + "$$\n", + "\n", + "Solving for $d$ gives me:\n", + "\n", + "$$\n", + "d = \\frac{1}{\\tan(\\frac{\\theta}{2})}\n", + "$$\n", + "\n", + "(Insert cool diagram here)\n", + "\n", + "Putting $d$ back in, I can find what I want $v''_x$ to be:\n", + "\n", + "$$\n", + "v''_x = \\frac{1}{\\tan(\\frac{\\theta}{2})}\\frac{v_x}{v_z}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "98b168fe-b93e-481e-b747-3ea0517d5907", + "metadata": {}, + "source": [ + "## Deriving the first row of $f(\\ldots)$\n", + "\n", + "I'm finally ready to try to figure out what $a$, $b$, $c$, & $d$ are to put in my matrix for function $f$.\n", + "\n", + "I can set my desired $v''_x$ equal to my multiplied-out version of $v''_x$ and solve to find the values in my matrix:\n", + "\n", + "$$\n", + "\\frac{av_x + bv_y + cv_z + d}{mv_x + nv_y + ov_z + p} = \\frac{1}{\\tan(\\frac{\\theta}{2})}\\frac{v_x}{v_z}\n", + "$$\n", + "\n", + "It's immediately obvious that $b$, $c$, $d$, $m$, $n$ & $p$ must all be zero. That leaves me with:\n", + "\n", + "$$\n", + "\\frac{a}{o}\\frac{v_x}{v_z} = \\frac{1}{\\tan(\\frac{\\theta}{2})}\\frac{v_x}{v_z}\n", + "$$\n", + "\n", + "So algebra tells me that:\n", + "$$\n", + "\\frac{a}{o} = \\frac{1}{\\tan(\\frac{\\theta}{2})}\n", + "$$\n", + "\n", + "I'm going to use a bit of foresight here realize that the variable $o$ is used in 4 equations, where $a$ is only used in this one, so I'll save figuring out $o$ for later by pushing it to the other side of the equation:\n", + "\n", + "$$\n", + "a = \\frac{1}{\\tan(\\frac{\\theta}{2})}o\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "e817fdab-92ef-4a7b-bd92-3dd1b2c260cb", + "metadata": {}, + "source": [ + "## Checking in on $f$ after working on $v''_x$\n", + "\n", + "Let's see how $f$ is doing:\n", + "\n", + "$$\n", + "f(\\theta, Z_n, Z_f) = \\begin{bmatrix}\n", + "\\frac{1}{\\tan(\\frac{\\theta}{2})}o & 0 & 0 & 0\\\\\n", + "e & f & g & h\\\\\n", + "i & j & k & l\\\\\n", + "0 & 0 & o & 0\n", + "\\end{bmatrix}\n", + "$$\n", + "\n", + "So, $v''_y$ is going to be nearly identical except $e$ will be zero instead of $f$, letting me skip straight to this:\n", + "\n", + "$$\n", + "f(\\theta, Z_n, Z_f) = \\begin{bmatrix}\n", + "\\frac{1}{\\tan(\\frac{\\theta}{2})}o & 0 & 0 & 0\\\\\n", + "0 & \\frac{1}{\\tan(\\frac{\\theta}{2})}o & 0 & 0\\\\\n", + "i & j & k & l\\\\\n", + "0 & 0 & o & 0\n", + "\\end{bmatrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "54508209-1324-4d54-aa5b-eebcf9a31032", + "metadata": {}, + "source": [ + "## Figuring out what I want $v''_z$ to actually be\n", + "\n", + "This is where it gets tricky. I want to remap any $v_z$ that's between $Z_n$ through $Z_f$ to $-1$ to $1$ (i.e. stuffing it into the $2\\times2\\times2$ clipping cube). That seems simple enough. First, I'll turn $v_z$ into a ratio like:\n", + "\n", + "$$\n", + "\\frac{v_z - Z_n}{Z_f - Z_n}\n", + "$$\n", + "\n", + "Now remaping it to $-1$ through $1$, I get what I want $v''_z$ be:\n", + "\n", + "$$\n", + "v''_z = 2\\frac{v_z - Z_n}{Z_f - Z_n}-1\n", + "$$\n", + "\n", + "I'm going to use a little bit of code to rearrange this for me:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "821cac4d-cf5a-452e-b776-09536ddaec85", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from sympy import *\n", + "init_printing(use_unicode=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e463c065-f51b-468c-a90e-7d5da5aac035", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\displaystyle \\left\\{ q : \\frac{2}{Z_{f} - Z_{n}}, \\ r : - \\frac{Z_{f} + Z_{n}}{Z_{f} - Z_{n}}\\right\\}$" + ], + "text/plain": [ + "⎧ 2 -(Z_f + Zₙ) ⎫\n", + "⎨q: ────────, r: ────────────⎬\n", + "⎩ Z_f - Zₙ Z_f - Zₙ ⎭" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "q, r, v_z, Z_n, Z_f = symbols('q, r, v_z, Z_n, Z_f')\n", + "solve(Eq(\n", + " q*v_z + r,\n", + " 2*(v_z - Z_n)/(Z_f - Z_n) - 1\n", + "), q, r)" + ] + }, + { + "cell_type": "markdown", + "id": "8cec6c52-f3f5-48b7-98cf-c3351e3af7a0", + "metadata": {}, + "source": [ + "That gives me a desired $v''_z$ like:\n", + "\n", + "$$\n", + "v''_z = \\frac{2}{Z_f-Z_n}v_z - \\frac{Z_f+Z_n}{Z_f-Z_n}\n", + "$$\n", + "\n", + "Neat.\n" + ] + }, + { + "cell_type": "markdown", + "id": "80171350-d1ae-429d-a57f-a6d2cafe9970", + "metadata": {}, + "source": [ + "## Deriving the third row of $f(\\ldots)$\n", + "\n", + "I'll do the same thing I did earlier and set my multiplied-out $v''_z$ equal to my desired $v''_z$ and figure out what values the matrix should have for $f$:\n", + "\n", + "$$\n", + "\\frac{iv_x + jv_y + kv_z + l}{mv_x + nv_y + ov_z + p} = \\frac{2}{Z_f-Z_n}v_z - \\frac{Z_f+Z_n}{Z_f-Z_n}\n", + "$$\n", + "\n", + "It's immediately obvious that $i$ & $j$ must both be zero. I already established that $m$ & $n$ & $p$ must be zero too. That leaves me with:\n", + "\n", + "$$\n", + "\\frac{kv_z + l}{ov_z} = \\frac{2}{Z_f-Z_n}v_z - \\frac{Z_f+Z_n}{Z_f-Z_n}\n", + "$$\n", + "\n", + "There are two $v_z$s on the left side, so I will rearrange it:\n", + "\n", + "$$\n", + "\\frac{l}{o}v_z^{-1} + k = \\frac{2}{Z_f-Z_n}v_z - \\frac{Z_f+Z_n}{Z_f-Z_n}\n", + "$$\n", + "\n", + "Well that's not going to work! I could try $k = -\\frac{Z_f+Z_n}{Z_f-Z_n}$ and $\\frac{l}{o} = \\frac{2}{Z_f-Z_n}$, but I'm never going to escape the fact that $v_z^{-1} \\ne v_z$. 😞" + ] + }, + { + "cell_type": "markdown", + "id": "7ab42c13-f71d-4381-b0be-cbaed3b63649", + "metadata": {}, + "source": [ + "## Rethinking what I want $v''_z$ to be\n", + "\n", + "My goal is to remap $v_z$ from the range $Z_n$ through $Z_f$ to the range $-1$ through $1$. I'll call this remapping function $r(z)$.\n", + "\n", + "The only other constraint is that if $s \\le t$ then $r(s) \\le r(t)$ as well.\n", + "\n", + "If that's the case then we can just as easily remap $v_z^{-1}$ from the range $Z_n^{-1}$ through $Z_f^{-1}$ to the range $-1$ through $1$ and the constraints will hold. A very important thing to note is that doing this means that z-depth is no longer a *linear* interpolation, which explains the warnings about not letting $Z_n$ get too close to zero for fear of losing bits of precision.\n", + "\n", + "Working it out looks almost the same as before:\n", + "\n", + "$$\n", + "v''_z = 2\\frac{v_z^{-1} - Z_n^{-1}}{Z_f^{-1} - Z_n^{-1}} - 1\n", + "$$\n", + "\n", + "I'll have sympy rearrange it for me:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "716e1825-f31e-4796-a6a9-aaaa7da3f42b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\displaystyle \\left\\{ q : - \\frac{2 Z_{f} Z_{n}}{Z_{f} - Z_{n}}, \\ r : \\frac{Z_{f} + Z_{n}}{Z_{f} - Z_{n}}\\right\\}$" + ], + "text/plain": [ + "⎧ -2⋅Z_f⋅Zₙ Z_f + Zₙ⎫\n", + "⎨q: ──────────, r: ────────⎬\n", + "⎩ Z_f - Zₙ Z_f - Zₙ⎭" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solve(Eq(\n", + " q*v_z**(-1) + r,\n", + " 2*(v_z**(-1) - Z_n**(-1))/(Z_f**(-1) - Z_n**(-1)) - 1\n", + "), q, r)" + ] + }, + { + "cell_type": "markdown", + "id": "14d13eae-9015-473c-a718-3e56a82d5ec7", + "metadata": {}, + "source": [ + "That gives me a desired $v''_z$ like:\n", + "\n", + "$$\n", + "v''_z = \\frac{2 Z_f Z_n}{Z_n-Z_f}v_z^{-1} + \\frac{Z_f+Z_n}{Z_f-Z_n}\n", + "$$\n", + "\n", + "Neat. This should work." + ] + }, + { + "cell_type": "markdown", + "id": "1a7691cb-14b7-40d0-bd7e-b8ca170518d4", + "metadata": {}, + "source": [ + "## Return to the third row of $f(\\ldots)$\n", + "\n", + "I'll do the same thing I did earlier and set my multiplied-out $v''_z$ equal to my new-and-improved $v''_z$ and figure out what values the matrix should have for $f$:\n", + "\n", + "$$\n", + "\\frac{l}{o}v_z^{-1} + k = \\frac{2 Z_f Z_n}{Z_n-Z_f}v_z^{-1} + \\frac{Z_f+Z_n}{Z_f-Z_n}\n", + "$$\n", + "\n", + "I'll throw $o$ over to the other side:\n", + "\n", + "$$\n", + "lv_z^{-1} + k = \\frac{2 Z_f Z_n}{Z_n-Z_f}v_z^{-1}o + \\frac{Z_f+Z_n}{Z_f-Z_n}o\n", + "$$\n", + "\n", + "Now I can set $l = \\frac{2 Z_f Z_n}{Z_n-Z_f}o$ and $k = \\frac{Z_f+Z_n}{Z_f-Z_n}o$.\n", + "\n", + "That should be all of them except $o$. This is how it looks:\n", + "\n", + "$$\n", + "f(\\theta, Z_n, Z_f) = \\begin{bmatrix}\n", + "\\frac{1}{\\tan(\\frac{\\theta}{2})}o & 0 & 0 & 0\\\\\n", + "0 & \\frac{1}{\\tan(\\frac{\\theta}{2})}o & 0 & 0\\\\\n", + "0 & 0 & \\frac{Z_f+Z_n}{Z_f-Z_n}o & \\frac{2 Z_f Z_n}{Z_n-Z_f}o\\\\\n", + "0 & 0 & o & 0\n", + "\\end{bmatrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "12219eba-6a44-49ef-a987-cfb395e1d6c5", + "metadata": {}, + "source": [ + "# Completing $f$\n", + "\n", + "What to do $o$? I put factors of $o$ in all of my other values, so no matter what $o$ actually is, so long as it's not $0$, it can be anything. I will choose $1$.\n", + "\n", + "Here's the completed function:\n", + "\n", + "$$\n", + "f(\\theta, Z_n, Z_f) = \\begin{bmatrix}\n", + "\\frac{1}{\\tan(\\frac{\\theta}{2})} & 0 & 0 & 0\\\\\n", + "0 & \\frac{1}{\\tan(\\frac{\\theta}{2})} & 0 & 0\\\\\n", + "0 & 0 & \\frac{Z_f+Z_n}{Z_f-Z_n} & \\frac{2 Z_f Z_n}{Z_n-Z_f}\\\\\n", + "0 & 0 & 1 & 0\n", + "\\end{bmatrix}\n", + "$$\n", + "\n", + "What does gluPerspective do? Check it out: https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/gluPerspective.xml" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}