Initial commit

This commit is contained in:
Daniel Snider 2022-06-02 11:28:52 -07:00
commit 1c172fcde2

490
Perspective.ipynb Normal file
View File

@ -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 <code>gluPerspective</code> 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
}