peter-tanner.github.io/legacy_site/maths/2.5d rotations.html

475 lines
22 KiB
HTML
Raw Normal View History

2023-08-05 01:05:35 +08:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>📝 2.5d rotations wtih shear transformation</title>
<meta
name="description"
content="2.5d rotations wtih shear transformation"
/>
<meta name="viewport" content="width=device-width" />
<title>MathJax example</title>
<script>
MathJax = {
tex: { macros: { cis: "\\mathop{\\rm{cis}}\\nolimits" } },
chtml: { displayAlign: "center", scale: 1.1 },
};
</script>
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.4.1/highlight.min.js"></script>
<script>
hljs.initHighlightingOnLoad();
</script>
2023-08-05 01:51:30 +08:00
<script src="/legacy_site/js/tikzjax/tikzjax.js"></script>
2023-08-05 01:05:35 +08:00
<script
id="MathJax-script"
async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.4.1/styles/default.min.css"
/>
<link rel="stylesheet" href="style.css" />
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
type="text/css"
href="https://tikzjax.com/v1/fonts.css"
/>
<meta name="robots" content="noindex, nofollow" />
</head>
<body>
<b
>PLEASE DO NOT USE ANY CONTENT FROM HERE! ALL UNMAINTAINED AND THERE'S
PROBABLY NOTHING USEFUL HERE! CONTENT IS NOW UN-INDEXED TO PREVENT
CONFUSION, SO YOU CAN ONLY ACCESS THIS PART OF THE SITE THROUGH LINKS.
CONTENT IS ONLY KEPT HERE FOR HISTORY OF THE SITE -2023</b
>
<center>
<h1>2.5d rotations with matrix transformations</h1>
<h3>
<s>(WACE) Mathematics Specialist ATAR</s><b style="color: red">**</b>
</h3>
</center>
<div class="card bg-danger">
<div class="card-body" style="color: white">
<center>
<h4><b>⚠WARNING⚠</b></h4>
</center>
<h5>
At this point this article is probably outside of the curriculum. The
transformations are in the curriculum, but most of this article is on
a real math problem I've experienced which of course requires a bit of
programming.
</h5>
</div>
</div>
<a
class="link"
style="left: 1%; top: 1%"
href="https://peter-tanner.github.io/legacy_site/maths"
>🔗 Back to MATHS home page</a
><br />
<a
class="link"
style="left: 1%; top: 1%"
href="https://peter-tanner.github.io/legacy_site"
>🔗 Back to home page</a
><br />
Warning: This page requires javascript to render the math. This website runs
better on a chromium browser. Untested on Firefox.
<b>TikZ graphics may not render on other platforms!</b>
<hr />
<br />
<div class="card">
<div class="card-body">
<center><b>Introduction - What is '2.5d'?</b></center>
'2.5d' is not an actual dimension, but refers to techniques in computer
graphics which aim to create the presence of 3d depth, but in a 2d level
or environment.<br /><br />
<center>
<script type="text/tikz">
\begin{tikzpicture}[thick,scale=1, every node/.style={scale=1.9,inner sep=0pt}]
\draw (-2.5,0) rectangle (2.5,3);
\draw [fill=gray] (-2.5,0) rectangle (2.5,-3);
\node [label=left:{[-2.5, 0]}] at (-2.5,0) {\textbullet};
\node [label=right:{[2.5, 0]}] at (2.5,0) {\textbullet};
\node [label=right:{[2.5, 3]}] at (2.5,3) {\textbullet};
\node [label=right:{[2.5, -3]}] at (2.5,-3) {\textbullet};
\node [label=left:{[-2.5, 3]}] at (-2.5,3) {\textbullet};
\node [label=left:{[-2.5, -3]}] at (-2.5,-3) {\textbullet};
\end{tikzpicture}
</script>
</center>
This box is a simplified example of 2.5d graphics.<br />
The gray on the front-facing side and the white top side is an example
of shading, and creates the illusion of a 3d environment as a result of
basic lighting. The camera is placed at a 45 degree angle, as we see
equal proportions of the top and front faces of the box<br />
However, this is not real 3d graphics, as we're representing this box
within a 2d space with only 2d position vectors.<br />
Therefore, this is 2.5d graphics.
</div>
</div>
<br />
<div class="card">
<div class="card-body">
<center><b>The problem</b></center>
I was making a mod for a game that added in rockets. The rockets took a
ballistic trajectory to reach a target point<br />
Of course, because this is a 2.5d game I couldn't simply model the
flight with the actual equations, because there is no concept of
'height' or 'altitude' in the game<br />
<br />
<center>
<script type="text/tikz">
\begin{tikzpicture}[thick,scale=1, every node/.style={scale=1.9,inner sep=0pt}]
\draw[->] (0, 0) -- (6, 0) node[right] {$\vec{i}$};
\draw[->] (0, 0) -- (0, 6) node[above] {$\vec{j}$};
\node [label=below:{$\mathbf{S}[1, 1.5]$}] at (1,1.5) {\textbullet};
\node [label=below:{$\mathbf{T}[5, 1.5]$}] at (5,1.5) {\textbullet};
\draw[scale=1, domain=1:5, smooth, variable=\x, red] plot ({\x}, {-(\x-1)*(\x-5)+1.5});
\draw[->, red, dashed] (1,1.5) -- (5,1.5);
\end{tikzpicture}
</script>
</center>
This parabola works fine for targets on the same \(\vec{j}\) as the
silo<br />
What if we want a trajectory like this?
<center>
<script type="text/tikz">
\begin{tikzpicture}[thick,scale=1, every node/.style={scale=1.9,inner sep=0pt}]
\draw[->] (0, 0) -- (6, 0) node[right] {$\vec{i}$};
\draw[->] (0, 0) -- (0, 6) node[above] {$\vec{j}$};
\node [label=below:{$\mathbf{S}[1, 2]$}] at (1,2) {\textbullet};
\node [label=above:{$\mathbf{T}[5, 4]$}] at (5,4) {\textbullet};
\draw[->, red, dashed] (1,2) -- (5,4);
\end{tikzpicture}
</script>
</center>
We can try rotating the parabola, but that results in a trajectory which
looks very unrealistic.
<center>
<script type="text/tikz">
\begin{tikzpicture}[thick,scale=1, every node/.style={scale=1.9,inner sep=0pt}]
\draw[scale=1, domain=0:4.47213595, smooth, variable=\x, blue, dashed] plot ({\x}, {-(\x)*(\x-4.47213595)});
\draw[->] (0, 0) -- (6, 0) node[right] {$\vec{i}$};
\draw[->] (0, 0) -- (0, 6) node[above] {$\vec{j}$};
\node [label=below:{$\mathbf{S}[1, 2]$}] at (1,2) {\textbullet};
\node [label=above:{$\mathbf{T}[5, 4]$}] at (5,4) {\textbullet};
\draw[shift={(1,2)}, scale=1, domain=0:4.5, smooth, variable=\x, red, rotate=30] plot ({\x}, {-(\x)*(\x-4.47213595)});
\draw[->, red, dashed] (1,2) -- (5,4);
\end{tikzpicture}
</script>
</center>
The blue dashed parabola represents the pre-transformed trajectory (we
are only given the distance \(d\) from target \(\mathbf{T}\) to silo
\(\mathbf{S}\))<br />
<div class="alert alert-warning" role="alert">
<center>How this transformation is achieved [Advanced]</center>
<br />
Our transformation requires a shift. We can't just add two matrices of
different dimensions so we need to somehow represent the shift as a
matrix multiplication. For this we need a <b>homogeneous coordinate</b
><br />
We represent a 2d vector as a 3d vector, but the \(\vec{k}\) component
is a constant \(1\). This constant allows the addition of the shift
through matrix multiplication. <br />
\[\mathbf{X}=\begin{bmatrix} x_0 & x_1 & x_2 & x_3 & x_4 & x_5 & \dots
\\ y_0 & y_1 & y_2 & y_3 & y_4 & y_5 & \dots \\ 1 & 1 & 1 & 1 & 1 & 1
& \dots \end{bmatrix}\] In this case, we applied the following
transformations to <b>rotate</b> then <b>translate</b> \(\mathbf{T}\)
into 'inaccurate' trajectory, \(\mathbf{T'}\) \begin{align}
\mathbf{X'} &= \begin{bmatrix} 1 & 0 & \Delta{x} \\ 0 & 1 & \Delta{y}
\\ 0 & 0 & 1 \\ \end{bmatrix}\begin{bmatrix} \cos(\theta) &
-\sin(\theta) & 0 \\ \sin(\theta) & \cos(\theta) & 0 \\ 0 & 0 & 1 \\
\end{bmatrix}\mathbf{X} \\\\ &= \begin{bmatrix} 1 & 0 & 1 \\ 0 & 1 & 2
\\ 0 & 0 & 1 \\ \end{bmatrix}\begin{bmatrix} \cos(\frac{\pi}{6}) &
-\sin(\frac{\pi}{6}) & 0 \\ \sin(\frac{\pi}{6}) & \cos(\frac{\pi}{6})
& 0 \\ 0 & 0 & 1 \\ \end{bmatrix}\mathbf{X} \end{align} However, in
reality I would just use a for loop and iterate through each pair of
points.<br />
<pre><code class="python">import math
import numpy as np
def rotate(theta):
return np.array([[cos(theta), -sin(theta)],
[sin(theta), cos(theta)]])
T = [[0,0], ... ,[4,0]] #Our starting list of points T
T_ = []
C = [1,2]
R = rotate(math.pi/6) #Rotation matrix when theta=pi/6. Store to R to save function calls
for P in T:
P = np.array(P) #convert python list (A point P, like [0,0]) to numpy array
P_ = np.add( np.matmul(R,P), C ) #multiply with rotation matrix R (matmul), then add C
T_.append(P_) #Add our transformed point P_ to our transformed list, T_
end
# T_ is now our transformed matrix.
</code></pre>
</div>
</div>
</div>
<br />
<div class="card">
<div class="card-body">
<center><b>Shear matrices</b></center>
\begin{align} \text{Parallel to the y-axis, Vertical shear}\quad
\mathbf{X'} &= \begin{bmatrix} 1 & 0\\ m & 1 \end{bmatrix}\mathbf{X}\\
&= \begin{bmatrix} x\\ mx+y \end{bmatrix} \\\\ \text{Parallel to the
x-axis, Horizontal shear}\quad \mathbf{X'} &= \begin{bmatrix} 1 & m\\ 0
& 1 \end{bmatrix}\mathbf{X} \\ &= \begin{bmatrix} x+my\\ y
\end{bmatrix}\\\\ \text{Where}\quad\mathbf{X} &= \begin{bmatrix} x\\ y
\end{bmatrix}\\ \end{align}
</div>
</div>
<br />
<hr />
<br />
<div class="card">
<div class="card-body">
<center><b>The solution: shear matrices</b></center>
<br />
A shear matrix is better explained visually.
<center>
<script type="text/tikz">
\begin{tikzpicture}[thick,scale=1, every node/.style={scale=1.9,inner sep=0pt}]
\draw[->] (-6, 1) -- (-1, 1) node[right] {$\vec{i}$};
\draw[->] (-6, 1) -- (-6, 6) node[above] {$\vec{j}$};
\draw[->] (2, 1) -- (7, 1) node[right] {$\vec{i}$};
\draw[->] (2, 1) -- (2, 6) node[above] {$\vec{j}$};
\draw[->] (-0.7,3) -- (1.7,3);
\draw (-6,1) rectangle (-2,3);
\draw (6,3) -- (2,1) -- (2,3) -- (6,5) -- cycle;
\end{tikzpicture}
</script>
</center>
In this case we applied a vertical shear (parallel to the y-axis). Our
x-values are the same, but our y-values have been transformed in a way
that they match the line \(y'=mx+y\) (see the matrix representation).<br />
It has almost all of the properties required for our 2.5d rotation -
we're only distorting the matrix on one axis and preserving the other.
The only issue is that our transformed object will appear longer.<br />
<br />
In my case, I do not need to know the angle - I can use the gradient
between the silo \(\mathbf{S}\) and target \(\mathbf{T}\).
\[m=\frac{\mathbf{S}_y-\mathbf{T}_y}{\mathbf{S}_x-\mathbf{T}_x}\] \(m\)
can also be obtained with a trig ratio, yielding the following shear
transformation that involve angles rather than gradients. \begin{align}
\text{Parallel to the y-axis, Vertical shear}\quad \mathbf{X'} &=
\begin{bmatrix} 1 & 0\\ \sin(\theta) & 1 \end{bmatrix}\mathbf{X}\\\\
\text{Parallel to the x-axis, Horizontal shear}\quad \mathbf{X'} &=
\begin{bmatrix} 1 & \sin(\theta)\\ 0 & 1 \end{bmatrix}\mathbf{X}\\\\
\end{align} To resolve our issue with the transformed object being
'lengthened', we need to scale it back.<br />
<center>
<script type="text/tikz">
\begin{tikzpicture}[thick,scale=1, every node/.style={scale=1.9,inner sep=0pt}]
\draw[->] (-6, 1) -- (-1, 1) node[right] {$\vec{i}$};
\draw[->] (-6, 1) -- (-6, 6) node[above] {$\vec{j}$};
\draw[->] (2, 1) -- (7, 1) node[right] {$\vec{i}$};
\draw[->] (2, 1) -- (2, 6) node[above] {$\vec{j}$};
\draw[->] (-0.7,3) -- (1.7,3);
\draw (-6,1) rectangle (-2,3);
\draw (6,3) -- (2,1) -- (2,3) -- (6,5) -- cycle;
\node [label=below:{$\mathbf{P_1}$}] at (-6,1) {\textbullet};
\node [label=below:{$\mathbf{P_2}$}] at (-2,1) {\textbullet};
\node [label=below:{$\mathbf{P'_1}$}] at (2,1) {\textbullet};
\node [label=right:{$\mathbf{P'_2}$}] at (6,3) {\textbullet};
\end{tikzpicture}
</script>
</center>
Let's choose two points, \(\mathbf{P_1}\) and \(\mathbf{P_2}\).<br />
We obtain the original distance, \(d\) and the transformed distance,
\(d'\) \begin{align} d &= \|\mathbf{P_1}-\mathbf{P_2}\| \\ &=
\sqrt{(\mathbf{P_1}_x-\mathbf{P_2}_x)^2+(\mathbf{P_1}_y-\mathbf{P_2}_y)^2}
\\ d' &= \|\mathbf{P'_1}-\mathbf{P'_2}\| \\ &=
\sqrt{(\mathbf{P'_1}_x-\mathbf{P'_2}_x)^2+(\mathbf{P'_1}_y-\mathbf{P'_2}_y)^2}\\\\
\text{ where }\quad\mathbf{P'_1},\mathbf{P'_2} &= \begin{bmatrix} 1 &
0\\ \frac{\mathbf{S}_y-\mathbf{T}_y}{\mathbf{S}_x-\mathbf{T}_x} & 1
\end{bmatrix}\mathbf{P_1},\mathbf{P_2}\\ &= \begin{bmatrix} 1 & 0\\
\sin(\theta) & 1 \end{bmatrix}\mathbf{P_1},\mathbf{P_2}\\ \end{align}
Note that \(d\) is also the distance from the silo \(\mathbf{S}\) and
target \(\mathbf{T}\)<br />
\(\mathbf{P_1}\) and \(\mathbf{P_2}\) are not the same as \(\mathbf{S}\)
and \(\mathbf{T}\), because we've created these points with
\(\begin{bmatrix}0\\0\end{bmatrix}\) as the origin,
<b>not</b> \(\mathbf{S}\). We will translate all of the points later so
that our starting point is \(\mathbf{S}\) after rotating.<br />
However, the distances are the same, so we might as well use
\(\mathbf{S}\) and \(\mathbf{T}\) to calculate \(d\). \[d =
\|\mathbf{P_1}-\mathbf{P_2}\| = \|\mathbf{T}-\mathbf{S}\|\] Our scale
factor is simply the ratio between the two. \begin{align} s &=
\frac{d}{d'} \\ &=
\frac{\|\mathbf{T}-\mathbf{S}\|}{\|\mathbf{P'_1}-\mathbf{P'_2}\|}
\end{align} Putting it all together, our 2.5d rotation is now:
\begin{align} \mathbf{X'} &= s\begin{bmatrix} 1 & 0\\ m & 0
\end{bmatrix}\mathbf{X}\\ &=
\frac{\|\mathbf{T}-\mathbf{S}\|}{\|\mathbf{P'_1}-\mathbf{P'_2}\|}\cdot\begin{bmatrix}
1 & 0\\ \frac{\mathbf{S}_y-\mathbf{T}_y}{\mathbf{S}_x-\mathbf{T}_x} & 1
\end{bmatrix}\mathbf{X}\\ \text{ or }\quad &=
\frac{\|\mathbf{T}-\mathbf{S}\|}{\|\mathbf{P'_1}-\mathbf{P'_2}\|}\cdot\begin{bmatrix}
1 & 0\\ \sin(\theta) & 1 \end{bmatrix}\mathbf{X}\\\\ \end{align} So we
need a 'pre transformation' on two points, \(\mathbf{P_1}\) and
\(\mathbf{P_2}\) in order to obtain our scale factor
<center>
<script type="text/tikz">
\begin{tikzpicture}[thick,scale=1, every node/.style={scale=1.9,inner sep=0pt}]
\draw[->] (-6, 1) -- (-1, 1) node[right] {$\vec{i}$};
\draw[->] (-6, 1) -- (-6, 6) node[above] {$\vec{j}$};
\draw[->] (2, 1) -- (7, 1) node[right] {$\vec{i}$};
\draw[->] (2, 1) -- (2, 6) node[above] {$\vec{j}$};
\draw[->] (-0.7,3) -- (1.7,3);
\draw (-6,1) rectangle (-2,3);
\draw (6,3) -- (2,1) -- (2,3) -- (6,5) -- cycle;
\node [label=below:{$\mathbf{P_1}$}] at (-6,1) {\textbullet};
\node [label=below:{$\mathbf{P_2}$}] at (-2,1) {\textbullet};
\node [label=below:{$\mathbf{P'_1}$}] at (2,1) {\textbullet};
\node [label=right:{$\mathbf{P'_2}$}] at (6,3) {\textbullet};
\end{tikzpicture}
</script>
</center>
<br />
<center>
<script type="text/tikz">
\begin{tikzpicture}[thick,scale=1, every node/.style={scale=1.9,inner sep=0pt}]
\draw[->] (-6, 1) -- (-1, 1) node[right] {$\vec{i}$};
\draw[->] (-6, 1) -- (-6, 6) node[above] {$\vec{j}$};
\draw[->] (2, 1) -- (7, 1) node[right] {$\vec{i}$};
\draw[->] (2, 1) -- (2, 6) node[above] {$\vec{j}$};
\draw[->] (-0.7,3) -- (1.7,3);
\draw (-6,1) rectangle (-2,3);
\draw (5.36656315,2.68328157) -- (2,1) -- (2,3) -- (5.36656315,4.47213595) -- cycle;
\node [label=below:{$\mathbf{P_1}$}] at (-6,1) {\textbullet};
\node [label=below:{$\mathbf{P_2}$}] at (-2,1) {\textbullet};
\node [label=below:{$\mathbf{P''_1}$}] at (2,1) {\textbullet};
\node [label=right:{$\mathbf{P''_2}$}] at (5.36656315,2.68328157) {\textbullet};
\end{tikzpicture}
</script>
</center>
The limitation of this technique is that we need to perform the rotation
about \(\begin{bmatrix}0\\0\end{bmatrix}\), the origin. After this we
can translate the rotated matrix to match the silo and target
location.<br />
You can use homogeneous coordinates to achieve this (see the yellow box
earlier), or programatically do it.<br />
<br />
We also have to solve one last issue with this method: it only works
within the domain \(\left(\frac{\pi}{2}, -\frac{\pi}{2}\right)\).<br />
This is because the gradient can only describe so much information.<br />
For instance, the line \(f(x)=1\cdot x\).<br />
We cannot tell if the line is going from the top-right to bottom-left,
or bottom-left to top-right because it describes both situations.<br />
We need to adjust the signs on the shear matrix according to the
quadrant our target is in.
<pre><code class="python">import math
import numpy as np
def quad(A,B): #A is our origin/fix point, B is our other point.
if B[0] > A[0] and B[1] > A[1]:
return 1 #First quadrant
elif B[0] < A[0] and B[1] > A[1]:
return 2 #Second quadrant
elif B[0] < A[0] and B[1] < A[1]:
return 3 #Third quadrant
else:
return 4 #Fourth quadrant
#By elimination. We could do another if but it's unnecessary
# elif B[0] < A[0] and B[1] > A[1]:
def trajectory(distance, n):
# Here we create a list of points. n is the amount we create
# (higher n results in greater precision)
# Not going to include it here - uses bezier interpolation and stuff to create
# the illusion of a ballistic trajectory with gravity (not a simple quadratic function)
#
return points
S = np.array([10,4]) #Silo position vector
T = np.array([5204,954]) #Target position vector
d = np.linalg.norm(T - S) #Get the distance: d = ((Tx-Sx)^2+(Ty-Sy)^2)^(0.5)
m = (T[1]-S[1])/(T[0]-S[0]) #Gradient of line between target and silo.
X = trajectory(d, 50) #50 is a constant for n. only affects trajectory precision
#X now contains a 'flat trajectory', a list of points which have the correct
#distance but the wrong angle and offset.
#It does not intersect with the target T.
def shear(m, X):
q = quad(S,T) #Silo and target is passed to the quadrant finding function.
#q is the quadrant
if q == 1 or q == 4:
return np.array([[1,0],
[m,1]]) #shear matrix.
else:
return np.array([[-1,0],
[m,-1]]) #shear matrix, but for quadrants 2 and 3 (negative x).
# AN easier way to do this would be to just compare S[0] > T[0]. Forget about the quadrant stuff.
# because we only need to know if it's in the negative or positive x.
SHEAR = shear(m, X) #we get the shear matrix, which the function returns
P_ = np.matmul(SHEAR, X[(0,-1),]) #first, take a slice of the array X to obtain the first (P1)
# and last (P2) points. Then multiply this new 2x2 matrix
# containing P1 and P2 by the shear matrix
# to get P_, which contains P'1 and P'2
d_ = np.linalg.norm(P_[1] - P_[0]) #adjusted distance
s = d/d_ #scale factor
X_ = s * np.matmul(SHEAR, X) + S # shear, then scale every point by s.
# Then add S, the silo position, to each position to yield the
# correct positions
# X_ (X') is now a numpy array containing the shifted position vectors
# that have been transformed through the 2.5d rotation.
# The trajectory will still look 'realistic', not curving to the side but still
# 'visually correct' and will be functionally correct in that it
# starts at silo S and ends at target T.
</code></pre>
</div>
</div>
<br />
<a
class="link"
style="left: 1%; bottom: 1%"
href="https://peter-tanner.github.io/legacy_site/maths"
>🔗 Back to MATHS home page</a
><br />
<a
class="link"
style="left: 1%; bottom: 1%"
href="https://peter-tanner.github.io/legacy_site"
>🔗 Back to home page</a
><br />
</body>
</html>