# On orthographic projection

Writing:

lineA = {{{0, 0, 1}, {2, 0, 1}},
{{0, 1, 1}, {2, 1, 1}},
{{0, 2, 1}, {2, 2, 1}},
{{0, 3, 1}, {2, 3, 1}}};

lineB = {{{0, 0, 1}, {0, 3, 1}},
{{1, 0, 1}, {1, 3, 1}},
{{2, 0, 1}, {2, 3, 1}}};

lineC = {{{0, 0, 0}, {0, 0, 1}},
{{0, 1, 0}, {0, 1, 1}},
{{0, 2, 0}, {0, 2, 1}},
{{0, 3, 0}, {0, 3, 1}},
{{1, 0, 0}, {1, 0, 1}},
{{1, 1, 0}, {1, 1, 1}},
{{1, 2, 0}, {1, 2, 1}},
{{1, 3, 0}, {1, 3, 1}},
{{2, 0, 0}, {2, 0, 1}},
{{2, 1, 0}, {2, 1, 1}},
{{2, 2, 0}, {2, 2, 1}},
{{2, 3, 0}, {2, 3, 1}}};

Graphics3D[{Thick, Green, Line[lineA],
Red, Line[lineB], Blue, Line[lineC]},
Axes -> True, AxesLabel -> {"x", "y", "z"},
AxesOrigin -> {0, 0, 0}, Boxed -> False,
Ticks -> {{0, 1, 2}, {0, 1, 2, 3}, {0, 1}},
ViewProjection -> "Orthographic"]


we get:

On the other hand, even writing:

lineA = {{{0, 247/529}, {319/577, 146/529}},
{{86/577, 341/529}, {405/577, 240/529}},
{{173/577, 435/529}, {491/577, 334/529}},
{{259/577, 1}, {576/577, 427/529}}};

lineB = {{{0, 247/529}, {259/577, 1}},
{{160/577, 197/529}, {417/577, 478/529}},
{{319/577, 146/529}, {576/577, 427/529}}};

lineC = {{{1/577, 102/529}, {0, 247/529}},
{{87/577, 196/529}, {86/577, 341/529}},
{{173/577, 290/529}, {173/577, 435/529}},
{{258/577, 383/529}, {259/577, 1}},
{{160/577, 51/529}, {160/577, 197/529}},
{{246/577, 145/529}, {245/577, 290/529}},
{{332/577, 239/529}, {331/577, 384/529}},
{{418/577, 333/529}, {417/577, 478/529}},
{{319/577, 0}, {319/577, 146/529}},
{{405/577, 94/529}, {405/577, 240/529}},
{{491/577, 188/529}, {491/577, 334/529}},
{{1, 282/529}, {576/577, 427/529}}};

Graphics[{Thick, Green, Line[lineA], Red,
Line[lineB], Blue, Line[lineC]}, Axes -> True,
AxesLabel -> {"x", "y"}, AxesOrigin -> {0, 0},
Ticks -> {{0, 1/4, 1/2, 3/4, 1}, {0, 1/4, 1/2, 3/4, 1}}]


we get:

which visually is the same, but with the advantage that it has been graphed by referring directly to the two-dimensional coordinates (approximated by hand).

With the intention of writing a geometric transformation that allows to transform three-dimensional coordinates into two-dimensional coordinates, I got an idea about the theory in this Wikipedia page and about the aspects related to Mathematica on this page.

Nevertheless, I have not been able to make progress, so I write here in the hope that someone would be interested in this and could direct me better, even just a few references.

Set an $$Oxyz$$ reference system, considering:

• a sphere with center $$O$$ and radius $$r > 0$$;

• a point of view on this sphere, ie with coordinates $$P = O + r\,\mathbf{n}$$, where $$\mathbf{n} \equiv \left(\cos u\,\cos v,\,\cos u\,\sin v,\,\sin u\right)$$ is a versor defined by latitude $$-\frac{\pi}{2} \le \color{red}{u} \le \frac{\pi}{2}$$ and longitude $$0 \le \color{red}{v} < 2\pi$$;

• the plane $$\pi$$ tangent to the sphere in $$P$$, ie passing through $$P$$ and of direction $$\mathbf{n}$$;

• a new reference system $$Px'y'z'$$ with axes parallel to the director vectors $$\frac{\partial \mathbf{n}}{\partial v}$$, $$\frac{\partial \mathbf{n}}{\partial u}$$, $$\mathbf{n}$$;

by projecting the points of the $$Oxyz$$ space onto $$\pi$$ it's possible to determine their new coordinates by calculating the respective distances with the $$x'$$, $$y'$$ axes (note that $$z' \equiv 0$$).

In particular, being orthographic projections with respect to $$\pi$$, the dependence on $$r$$ is lost, therefore for simplicity of calculation it's possible to consider the unitary sphere.

Finally, all that remains is to rotate the new axes $$x'$$, $$y'$$ by an angle $$0 \le \color{red}{w} < 2\pi$$ with respect to $$\mathbf{n}$$, so that $$w = 0$$ corresponds to choosing $$(0,\,0,\,1)$$ as the vertical direction in $$Oxyz$$.

After the theory lesson, all that remains is to put it into practice:

lineA3d = {{{0, 0, 1}, {2, 0, 1}}, {{0, 1, 1}, {2, 1, 1}},
{{0, 2, 1}, {2, 2, 1}}, {{0, 3, 1}, {2, 3, 1}}};

lineB3d = {{{0, 0, 1}, {0, 3, 1}}, {{1, 0, 1}, {1, 3, 1}},
{{2, 0, 1}, {2, 3, 1}}};

lineC3d = {{{0, 0, 0}, {0, 0, 1}}, {{0, 1, 0}, {0, 1, 1}},
{{0, 2, 0}, {0, 2, 1}}, {{0, 3, 0}, {0, 3, 1}},
{{1, 0, 0}, {1, 0, 1}}, {{1, 1, 0}, {1, 1, 1}},
{{1, 2, 0}, {1, 2, 1}}, {{1, 3, 0}, {1, 3, 1}},
{{2, 0, 0}, {2, 0, 1}}, {{2, 1, 0}, {2, 1, 1}},
{{2, 2, 0}, {2, 2, 1}}, {{2, 3, 0}, {2, 3, 1}}};

pts3d = Flatten[Union[lineA3d, lineB3d, lineC3d], 1];
min = Table[Min[pts3d[[All, i]], 0], {i, 3}];
max = Table[Max[pts3d[[All, i]], 0], {i, 3}];
br = (max - min) / Min[max - min];

axes3d = 1.05 Transpose[{IdentityMatrix[3] min, IdentityMatrix[3] max}];

OrthographicProjection[pts3d_, u_, v_, w_] := Module[{pts2d, x, y, z},
pts2d = Table[{x, y, z} = pts3d[[i]];
{Cos[w] (y Cos[v] - x Sin[v]) - Sin[w] (z Cos[u] -
Sin[u] (x Cos[v] + y Sin[v])),
Sin[w] (y Cos[v] - x Sin[v]) + Cos[w] (z Cos[u] -
Sin[u] (x Cos[v] + y Sin[v]))},
{i, Length[pts3d]}];
Return[pts2d]];

Manipulate[

lineA2d = Partition[OrthographicProjection[Flatten[lineA3d, 1], u, v, w], 2];
lineB2d = Partition[OrthographicProjection[Flatten[lineB3d, 1], u, v, w], 2];
lineC2d = Partition[OrthographicProjection[Flatten[lineC3d, 1], u, v, w], 2];
axes2d = Partition[OrthographicProjection[Flatten[axes3d, 1], u, v, w], 2];

vp = {Cos[u] Cos[v], Cos[u] Sin[v], Sin[u]};
vv = {(Cos[u] - Sin[u] Cos[w]) Cos[v] - Sin[v] Sin[w],
(Cos[u] - Sin[u] Cos[w]) Sin[v] + Cos[v] Sin[w],
Sin[u] + Cos[u] Cos[w]} / br;

gr3d = Graphics3D[{Thick, Green, Line[lineA3d], Red,
Line[lineB3d], Blue, Line[lineC3d],
Thin, Black, Arrow[axes3d],
Text["x", 1.02 axes3d[[1, 2]]],
Text["y", 1.02 axes3d[[2, 2]]],
Text["z", 1.02 axes3d[[3, 2]]]},
Boxed -> False, Method -> {"ShrinkWrap" -> True},
ViewProjection -> "Orthographic",
ViewPoint -> vp, ViewVertical -> vv];

gr2d = Graphics[{Thick, Green, Line[lineA2d], Red,
Line[lineB2d], Blue, Line[lineC2d],
Thin, Black, Arrow[axes2d],
Text["x", 1.02 axes2d[[1, 2]]],
Text["y", 1.02 axes2d[[2, 2]]],
Text["z", 1.02 axes2d[[3, 2]]]},
Method -> {"ShrinkWrap" -> True}];

Grid[{{gr3d, gr2d}}, BaseStyle -> ImageSizeMultipliers -> 1],

Row[{Control[{{u, ArcTan[4 Sqrt[5/149]]}, -π/2, π/2}],
Control[{{v, 2π - ArcTan[24/13]}, 0, 2π}],
Control[{{w, 0}, 0, 2π}]}, Spacer[30]],

ControlPlacement -> Top]


If there is still any doubt left, Overlay[{gr3d, gr2d}] will dissolve it. ^_^

• That's exactly what I wanted, thank you! Jun 10, 2021 at 16:03

You want a mapping that maps points in 3D to points on a plane that is perpendicular to the view direction.

Call the view direction: n. Then we need to project every point along this direction onto a plane through the origin that is perpendicular to n. Finally we rotate this plane onto the x-y plane.

This is implemented in the following function.

Input: pts0= list of 3D points and n= view direction

pr[pts0_, n_] := Module[{pts, rot, proj, nx, ny, nz},
{nx, ny, nz} = Normalize[n];
proj[{x_, y_,
z_}] := {-(-ny^2 x - nz^2 x + nx ny y + nx nz z), -(nx ny x -
nx^2 y - nz^2 y + ny nz z), -(nx nz x + ny nz y - nx^2 z -
ny^2 z)} // Simplify;
pts = proj /@ pts0;
rot = RotationMatrix[{n, {0, 0, 1}}];
(rot . #)[[;; 2]] & /@ pts
]


Explanation:

First we normalize n0. Then we define a function that projects a point to the plane perpendicular to n. This is done by solving the 2 equations:

n.projection == 0
Cross[(point-projection),n]==0


The first says, that the vector from the origin to the projected point is perpendicular to n (remember the plane contains the origin). The second says, that the vector from the projection to the original point is parallel to n. For speed this is precalculated. Then this function is applied to every point. Next a rotation matrix is calculated and applied to the projected points, that rotates points on the plane to points in the x-y plane.

We may test this, taking care to use the same view direction. The 3D graphics is:

lineA = {{{0, 0, 1}, {2, 0, 1}}, {{0, 1, 1}, {2, 1, 1}}, {{0, 2,
1}, {2, 2, 1}}, {{0, 3, 1}, {2, 3, 1}}};

lineB = {{{0, 0, 1}, {0, 3, 1}}, {{1, 0, 1}, {1, 3, 1}}, {{2, 0,
1}, {2, 3, 1}}};

lineC = {{{0, 0, 0}, {0, 0, 1}}, {{0, 1, 0}, {0, 1, 1}}, {{0, 2,
0}, {0, 2, 1}}, {{0, 3, 0}, {0, 3, 1}}, {{1, 0, 0}, {1, 0,
1}}, {{1, 1, 0}, {1, 1, 1}}, {{1, 2, 0}, {1, 2, 1}}, {{1, 3,
0}, {1, 3, 1}}, {{2, 0, 0}, {2, 0, 1}}, {{2, 1, 0}, {2, 1,
1}}, {{2, 2, 0}, {2, 2, 1}}, {{2, 3, 0}, {2, 3, 1}}};
gr = Graphics3D[{Thick, Green, Line[lineA], Red, Line[lineB], Blue,
Line[lineC]}, Axes -> True, AxesLabel -> {"x", "y", "z"},
AxesOrigin -> {0, 0, 0}, Boxed -> False,
Ticks -> {{0, 1, 2}, {0, 1, 2, 3}, {0, 1}},
ViewPoint -> {1., -2., 2.}


]

Now for the 2D graphics we transform the 3D points, taking care to use the same view direction. From the 3D graphics we can extract the ViewPoint and the ViewCenter. The difference is the view direction (I am not 100% sure about this, if anybody knows more, please let me known):

vp = ViewPoint /. FullOptions[gr];
vc = ViewCenter /. FullOptions[gr];
vdir = (vp - vc);

la = pr[#, vdir] & /@ lineA;
lb = pr[#, vdir] & /@ lineB;
lc = pr[#, vdir] & /@ lineC;


The 2D graphics is now:

Graphics[{Thick, Green, Line[la], Red, Line[lb], Blue, Line[lc]},
Axes -> True, AxesLabel -> {"x", "y"}, AxesOrigin -> {0, 0}]


It does not look exactly the same, because: in the 2D image there is no perspective and the x/y axes are horizontal/vertical.

• Thank you so much for the time spent! Beyond that it doesn't come out exactly I wanted to understand, the rest can be fixed along the way. I have a doubt, for now: is the plan passing through the origin a convenient position or is it completely general? Jun 8, 2021 at 20:45
• It is general: If you project perpendicular to the plane, it does not matter if the plane is translated along n. But it makes calculations simpler by assuming that the plane contains the origin. Jun 8, 2021 at 21:04