# Draw filled surfaces on sphere

I know there's already topics on this subject (for example here and here). But, as a result of the excellent solutions brought here and here to other questions, I wonder if there's a way to do that in this framework.

• It depends on what your question precisely is. It is certainly possible to shade the visible and hidden surfaces in a different color. However, it might require to do certain things "by hand". The idea would be to use the pgfplots (!) library fillbetween and shade the area bounded by some path and the boundary of the sphere. But you'd have to decide yourself whether or not the area is partially hidden. – user121799 Jun 21 '18 at 16:09
• @marmot I was hoping you would see this discussion ;-). I don't have example of using fillbetween in 3D. I looked a bit \fill[intersection segments={of=A and B,sequence={R2 -- L2 -- ...}}]; but in 3D it's hard to use (especially to find the sequence) and, as you mentioned it, I don't know if there's an automatic way to do it. – kipgon Jun 21 '18 at 17:12
• I made an update below in which I believe to have automatized a lot of things. But there is one thing that cannot be automatized without further input: which area to fill. A closed contour on a sphere always encloses two areas, and it is a matter of choice which one should be called inner. Any idea how to make this choice? – user121799 Jun 22 '18 at 18:39

In principle it is possible to do that with the fillbetween library. The technical challenge is that TikZ find too many intersections and the number of intersections depends on the number of samples used in order to draw the path of the sphere. So in principle you can draw arbitrary shapes on a sphere (not just arcs!) and distinguish between hidden and visible lines and surfaces. While for lines it simply works with the spherical smooth style, for the surfaces one unfortunately has to fiddle around by hand. Here is an example:

\documentclass[tikz,border=3.14mm]{standalone}
\usetikzlibrary{intersections}
\usepackage{pgfplots}
\pgfplotsset{compat=1.16}
\usepgfplotslibrary{fillbetween}
\usepackage{tikz-3dplot}

\pgfkeys{/tikz/.cd,
hidden opacity/.store in=\HiddenOpacity,
hidden opacity=0.3,
}

\makeatletter

% from https://tex.stackexchange.com/a/375604/121799

%along z axis
\define@key{z sphericalkeys}{theta}{\def\mytheta{#1}}
\define@key{z sphericalkeys}{phi}{\def\myphi{#1}}
\tikzdeclarecoordinatesystem{z spherical}{%
\setkeys{z sphericalkeys}{#1}%
\pgfmathsetmacro{\Xtest}{cos(90-\tdplotmaintheta)*cos(\tdplotmainphi-90)*cos(\mytheta)*cos(\myphi)
+cos(90-\tdplotmaintheta)*sin(\tdplotmainphi-90)*cos(\mytheta)*sin(\myphi)
+sin(90-\tdplotmaintheta)*sin(\mytheta)}
% \Xtest is the projection of the coordinate on the normal vector of the visible plane
\pgfmathsetmacro{\ntest}{ifthenelse(\Xtest<0,0,1)}
\ifnum\ntest=0
\xdef\MCheatOpa{\HiddenOpacity}
\else
\xdef\MCheatOpa{1}
\fi
%\typeout{\mytheta,\tdplotmaintheta;\myphi,\tdplotmainphi:\ntest}

%%%%%%%%%%%%%%%%%
% define "new" plot handler
\tikzoption{spherical smooth}[]{\let\tikz@plot@handler=\pgfplothandlersphericalcurveto}

\pgfdeclareplothandler{\pgfplothandlersphericalcurveto}{}{%
point macro=\pgf@plot@curveto@handler@spherical@initial,
jump macro=\pgf@plot@smooth@next@spherical@moveto,
end macro=\pgf@plot@curveto@handler@spherical@finish
}

\def\pgf@plot@smooth@next@spherical@moveto{%
\pgf@plot@curveto@handler@spherical@finish%
\global\pgf@plot@startedfalse%
\global\let\pgf@plotstreampoint\pgf@plot@curveto@handler@spherical@initial%
}

\def\pgf@plot@curveto@handler@spherical@initial#1{%
\pgf@process{#1}%
\ifx\tikz@textcolor\pgfutil@empty%
\else
\pgfsetstrokecolor{\tikz@textcolor}
\fi
\pgf@xa=\pgf@x%
\pgf@ya=\pgf@y%
\pgf@plot@first@action{\pgfqpoint{\pgf@xa}{\pgf@ya}}%
\xdef\pgf@plot@curveto@first{\noexpand\pgfqpoint{\the\pgf@xa}{\the\pgf@ya}}%
\global\let\pgf@plot@curveto@first@support=\pgf@plot@curveto@first%
\global\let\pgf@plotstreampoint=\pgf@plot@curveto@handler@spherical@second%
}

\def\pgf@plot@curveto@handler@spherical@second#1{%
\pgf@process{#1}%
\xdef\pgf@plot@curveto@second{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}%
\global\let\pgf@plotstreampoint=\pgf@plot@curveto@handler@spherical@third%
\global\pgf@plot@startedtrue%
}

\def\pgf@plot@curveto@handler@spherical@third#1{%
\pgf@process{#1}%
\xdef\pgf@plot@curveto@current{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}%
% compute difference vector:
\pgf@xa=\pgf@x%
\pgf@ya=\pgf@y%
\pgf@process{\pgf@plot@curveto@first}
% compute support directions:
\pgf@xa=\pgf@plottension\pgf@xa%
\pgf@ya=\pgf@plottension\pgf@ya%
% first marshal:
\pgf@process{\pgf@plot@curveto@second}%
\pgf@xb=\pgf@x%
\pgf@yb=\pgf@y%
\pgf@xc=\pgf@x%
\pgf@yc=\pgf@y%
\@ifundefined{MCheatOpa}{}{%
\pgf@plotstreamspecial{\pgfsetstrokeopacity{\MCheatOpa}}}
\edef\pgf@marshal{\noexpand\pgfsetstrokeopacity{\noexpand\MCheatOpa}
\noexpand\pgfpathcurveto{\noexpand\pgf@plot@curveto@first@support}%
{\noexpand\pgfqpoint{\the\pgf@xb}{\the\pgf@yb}}{\noexpand\pgf@plot@curveto@second}
\noexpand\pgfusepathqstroke
\noexpand\pgfpathmoveto{\noexpand\pgf@plot@curveto@second}}%
{\pgf@marshal}%
%\pgfusepathqstroke%
% Prepare next:
\global\let\pgf@plot@curveto@first=\pgf@plot@curveto@second%
\global\let\pgf@plot@curveto@second=\pgf@plot@curveto@current%
\xdef\pgf@plot@curveto@first@support{\noexpand\pgfqpoint{\the\pgf@xc}{\the\pgf@yc}}%
}

\def\pgf@plot@curveto@handler@spherical@finish{%
\ifpgf@plot@started%
\pgfpathcurveto{\pgf@plot@curveto@first@support}{\pgf@plot@curveto@second}{\pgf@plot@curveto@second}%
\fi%
}
\makeatother

\begin{document}

\begin{tikzpicture}
\shade[name path=sphere,ball color = gray!40, opacity = 0.5]

\tdplotsetmaincoords{42}{205}
\begin{scope}[tdplot_main_coords,samples=60]
%
% phi={\tdplotmainphi-90},theta={90-\tdplotmaintheta});
% \draw[-latex] (0,0,0) -- (\RadiusSphere,0,0) node[below]{$x$};
% \draw[-latex] (0,0,0) -- (0,\RadiusSphere,0) node[left]{$y$};
% \draw[-latex] (0,0,0) -- (0,0,\RadiusSphere) node[left]{$z$};

\draw plot[spherical smooth,variable=\x,domain=-180:180]
% I modified the plot handler in order to change the opacity along the paths
% therefore I need to redraw it if I want to use it in fills :-(
\path[name path=circle,fill=blue,opacity=0.2]  plot[smooth,variable=\x,domain=-180:180]

% main technical challenge: TikZ finds tons of intersections instead of 2
% \draw[red, name intersections={of=sphere and circle,name=i, total=\t}]
% \foreach \s in {1,...,\t}{node[fill,circle,scale=0.3,label=above:\s] at (i-\s) {}}
% \pgfextra{\typeout{\t}};

%
\path [%draw,yellow,ultra thick,opacity=1,
fill=blue,opacity=0.4,
name path=visible surface,
intersection segments={
of=circle and sphere,
sequence={A5--B0[reverse]--B7[reverse]}
}];

\end{scope}
\end{tikzpicture}
\end{document}


You can increase the samples and change the viewing angle, but this comes at the prize that the number of intersections changes and you have to find the relevant paths again. For instance, for

\shade[name path=sphere,ball color = gray!40, opacity = 0.5]
\tdplotsetmaincoords{52}{200}


the number of intersections found by TikZ is 5 and you need to do

  \path [%draw,yellow,ultra thick,opacity=1,
fill=blue,opacity=0.4,
name path=visible surface,
intersection segments={
of=circle and sphere,
sequence={A4--B0[reverse]--B5[reverse]}
}];


to get

If you get sick of adjusting these things by hand, switch to asymptote ;-)

MINI-UPDATE: A first version without fillbetween.

\documentclass[tikz,border=3.14mm]{standalone}
\usetikzlibrary{intersections,decorations.markings}
\usepackage{tikz-3dplot}
\tikzset{endmark/.style={postaction={decorate,decoration={markings,
mark=at position 0 with {\coordinate (X0); },
mark=at position 1 with {\coordinate (X1); }}}}}

\pgfkeys{/tikz/.cd,
hidden opacity/.store in=\HiddenOpacity,
hidden opacity=0.3,
}

\makeatletter

\xdef\prevhidden@toggle{0}
\xdef\spherical@plot@start{0}
% the spherical coordinates are from https://tex.stackexchange.com/a/375604/121799
% but the routine got modified to
% (i) decide whether a point is "visible" or "hidden"
% (ii)

%along z axis
\define@key{z sphericalkeys}{theta}{\def\mytheta{#1}}
\define@key{z sphericalkeys}{phi}{\def\myphi{#1}}
\tikzdeclarecoordinatesystem{z spherical}{%
\setkeys{z sphericalkeys}{#1}%
\pgfmathsetmacro{\Xtest}{cos(90-\tdplotmaintheta)*cos(\tdplotmainphi-90)*cos(\mytheta)*cos(\myphi)
+cos(90-\tdplotmaintheta)*sin(\tdplotmainphi-90)*cos(\mytheta)*sin(\myphi)
+sin(90-\tdplotmaintheta)*sin(\mytheta)}
% \Xtest is the projection of the coordinate on the normal vector of the visible plane
\pgfmathsetmacro{\ntest}{ifthenelse(\Xtest<0,0,1)}
\ifnum\ntest=0
\xdef\MCheatOpa{\HiddenOpacity}
%\typeout{point\space hidden}
\ifnum\prevhidden@toggle=1 % previous point was also hidden
\else % finish visible path
\ifnum\spherical@plot@start=1 % this is the first point of the plot
%\typeout{start}
\else
\ifnum\thevis@paths=0
\xdef\lst@vis@paths{{\vis@path}}
\else
\xdef\lst@vis@paths{\lst@vis@paths,{\vis@path}}
\fi
\stepcounter{vis@paths}
\fi  % and start a new hidden path
\fi
\xdef\prevhidden@toggle{1}
\else
\xdef\MCheatOpa{1}
%\typeout{point\space visible}
\ifnum\prevhidden@toggle=0 % previous point was also visible
\else % finish hidden path
\ifnum\spherical@plot@start=1
%\typeout{start}
\else
\ifnum\thehid@paths=0
\xdef\lst@hid@paths{{\hid@path}}
\else
\xdef\lst@hid@paths{\lst@hid@paths,{\hid@path}}
\fi
\stepcounter{hid@paths}
\fi % and start a new visible path
\fi
\xdef\prevhidden@toggle{0}
\fi
%\typeout{\mytheta,\tdplotmaintheta;\myphi,\tdplotmainphi:\ntest}

%%%%%%%%%%%%%%%%%
% define "new" plot handler
\newcounter{vis@paths}
\newcounter{hid@paths}
\tikzoption{spherical smooth}[]{\xdef\lst@vis@paths{}
\xdef\lst@hid@paths{}
\xdef\hid@path{}
\xdef\vis@path{}
\let\tikz@plot@handler=\pgfplothandlersphericalcurveto}

\pgfdeclareplothandler{\pgfplothandlersphericalcurveto}{}{%
point macro=\pgf@plot@curveto@handler@spherical@initial,
jump macro=\pgf@plot@smooth@next@spherical@moveto,
end macro=\pgf@plot@curveto@handler@spherical@finish
}

\def\pgf@plot@smooth@next@spherical@moveto{%
\pgf@plot@curveto@handler@spherical@finish%
\global\pgf@plot@startedfalse%
\global\let\pgf@plotstreampoint\pgf@plot@curveto@handler@spherical@initial%
}

\def\pgf@plot@curveto@handler@spherical@initial#1{%
\pgf@process{#1}%
\ifx\tikz@textcolor\pgfutil@empty%
\else
\pgfsetstrokecolor{\tikz@textcolor}
\fi
\setcounter{vis@paths}{0}
\setcounter{hid@paths}{0}
\xdef\spherical@plot@start{1}
\pgf@xa=\pgf@x%
\pgf@ya=\pgf@y%
\pgf@plot@first@action{\pgfqpoint{\pgf@xa}{\pgf@ya}}%
\xdef\pgf@plot@curveto@first{\noexpand\pgfqpoint{\the\pgf@xa}{\the\pgf@ya}}%
\global\let\pgf@plot@curveto@first@support=\pgf@plot@curveto@first%
\global\let\pgf@plotstreampoint=\pgf@plot@curveto@handler@spherical@second%
}

\def\pgf@plot@curveto@handler@spherical@second#1{%
\xdef\spherical@plot@start{0}
\pgf@process{#1}%
\xdef\pgf@plot@curveto@second{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}%
\global\let\pgf@plotstreampoint=\pgf@plot@curveto@handler@spherical@third%
\global\pgf@plot@startedtrue%
}

\def\pgf@plot@curveto@handler@spherical@third#1{%
\pgf@process{#1}%
\xdef\pgf@plot@curveto@current{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}%
% compute difference vector:
\pgf@xa=\pgf@x%
\pgf@ya=\pgf@y%
\pgf@process{\pgf@plot@curveto@first}
% compute support directions:
\pgf@xa=\pgf@plottension\pgf@xa%
\pgf@ya=\pgf@plottension\pgf@ya%
% first marshal:
\pgf@process{\pgf@plot@curveto@second}%
\pgf@xb=\pgf@x%
\pgf@yb=\pgf@y%
\pgf@xc=\pgf@x%
\pgf@yc=\pgf@y%
\edef\pgf@marshal{\noexpand\pgfsetstrokeopacity{\noexpand\MCheatOpa}
\noexpand\pgfpathcurveto{\noexpand\pgf@plot@curveto@first@support}%
{\noexpand\pgfqpoint{\the\pgf@xb}{\the\pgf@yb}}{\noexpand\pgf@plot@curveto@second}
\noexpand\pgfusepathqstroke
\noexpand\pgfpathmoveto{\noexpand\pgf@plot@curveto@second}}%
{\pgf@marshal}%
%\pgfusepathqstroke%
% Prepare next:
\global\let\pgf@plot@curveto@first=\pgf@plot@curveto@second%
\global\let\pgf@plot@curveto@second=\pgf@plot@curveto@current%
\xdef\pgf@plot@curveto@first@support{\noexpand\pgfqpoint{\the\pgf@xc}{\the\pgf@yc}}%
}

\def\pgf@plot@curveto@handler@spherical@finish{%
\ifpgf@plot@started%
\pgfpathcurveto{\pgf@plot@curveto@first@support}{\pgf@plot@curveto@second}{\pgf@plot@curveto@second}%
\fi%
\ifnum\prevhidden@toggle=1
\xdef\lstvispaths{\lst@vis@paths}
\ifx\hid@path\empty
\else
%\typeout{closing\space hidden}
\ifx\lst@hid@paths\empty
\xdef\lst@hid@paths{{\hid@path}}
\else
\xdef\lst@hid@paths{\lst@hid@paths,{\hid@path}}
\fi
\foreach \X [count=\Y] in \lst@hid@paths
{\xdef\my@len{\Y}}
%\typeout{\my@len\space hidden\space patches}
\xdef\lsthidpaths{}
\foreach \X [count=\Y] in \lst@hid@paths
{\ifnum\Y=1 % save the first stretch
\xdef\tmppath{\X}
\ifnum\my@len=1
\xdef\lsthidpaths{{\X}}
\fi
\else
\ifnum\Y=\my@len
\ifnum\Y=2
\xdef\lsthidpaths{{\X\tmppath}}
\else
\xdef\lsthidpaths{\lsthidpaths,{\X\tmppath}}
\fi
%\typeout{result:\lsthidpaths}
\else
\xdef\lsthidpaths{\lsthidpaths,{\X}}
\fi
\fi}
%\typeout{hidden\space paths:\lst@hid@paths}
\fi
\else
\xdef\lsthidpaths{\lst@hid@paths}
\ifx\vis@path\empty
\else
%\typeout{closing\space visible}
\xdef\lst@vis@paths{\lst@vis@paths,{\vis@path}}
\foreach \X [count=\Y] in \lst@vis@paths
{\xdef\my@len{\Y}}
%\typeout{\my@len\space hidden\space coordinates:\vis@path}
\xdef\lstvispaths{}
\foreach \X [count=\Y] in \lst@vis@paths
{\ifnum\Y=1 % save the first stretch
\xdef\tmppath{\X}
\ifnum\my@len=1
\xdef\lstvispaths{{\X}}
\fi
\else
\ifnum\Y=\my@len
\ifnum\my@len=1
\xdef\lstvispaths{{\X}}
\else
\ifnum\Y=2
\xdef\lstvispaths{{\X\tmppath}}
\else
\xdef\lstvispaths{\lstvispaths,{\X\tmppath}}
\fi
%\typeout{result:\lstvispaths}
\fi
\else
\xdef\lstvispaths{\lstvispaths,{\X}}
\fi
\fi}
\fi
\fi
}
\makeatother

\newcommand{\FillVisibleSurfaces}[1][]{\xdef\numvis{0}
\foreach \X [count=\Y] in \lstvispaths
{\ifx\X\empty
\else
\xdef\numvis{\Y}
\fi}
\ifnum\numvis=0%
\else
\foreach \X [count=\Y] in \lstvispaths
{%\typeout{processing\space \X}
\path[endmark] plot[tdplot_main_coords,samples=60] coordinates {\X};
\fill[#1] let
\p1=(X0),\p2=(X1),\n1={mod(atan2(\y1,\x1)+720,360)},\n2={mod(atan2(\y2,\x2)+720,360)}
in % \pgfextra{\typeout{visible:\space start:\n1,end:\n2}}
plot[tdplot_main_coords,samples=60] coordinates {\X} -- (X1) arc(\n2:\n1:\RadiusSphere);
}
\fi
}
\newcommand{\FillHiddenSurfaces}[1][]{\xdef\numhid{0}
\foreach \X [count=\Y] in \lsthidpaths
{\ifx\X\empty
\else
\xdef\numhid{\Y}
\fi}
\ifnum\numhid=0%
\else
\foreach \X [count=\Y] in \lsthidpaths
{
\path[endmark] plot[tdplot_main_coords,samples=60] coordinates {\X};
\fill[#1] let
\p1=(X0),\p2=(X1),\n1={mod(atan2(\y1,\x1)+720,360)},\n2={mod(atan2(\y2,\x2)+720,360)}
in %\pgfextra{\typeout{hidden\space start:\n1,end:\n2}}
plot[tdplot_main_coords,samples=60] coordinates {\X} -- (X1) arc(\n2:\n1:\RadiusSphere)
--cycle;
}
\fi
}

\begin{document}

\foreach \X in {0,10,...,350}
{\begin{tikzpicture}
\shade[name path=sphere,ball color = gray!40, opacity = 0.5]

\tdplotsetmaincoords{110}{\X}
\begin{scope}[tdplot_main_coords,samples=60]
%
% phi={\tdplotmainphi-90},theta={90-\tdplotmaintheta});
% \draw[-latex] (0,0,0) -- (\RadiusSphere,0,0) node[below]{$x$};
% \draw[-latex] (0,0,0) -- (0,\RadiusSphere,0) node[left]{$y$};
% \draw[-latex] (0,0,0) -- (0,0,\RadiusSphere) node[left]{$z$};

\draw plot[spherical smooth,variable=\x,domain=-180:180]
\end{scope}
% note that these commands need to be placed *outside* the tdplot_main_coords scope
\FillHiddenSurfaces[blue,opacity=0.2]
\FillVisibleSurfaces[blue,opacity=0.5]
\end{tikzpicture}
}
\end{document}


I believe to have solved several issues with tons of \if statements. However, clearly the above animation does not seem right. (Notice that the extra bits appearing on the bottom and on the right are due to the conversion to gif, they are not present in the pdf.) But this is because on a sphere the "interior" of a closed surface is not well defined, rather there are two options. And always filling the smaller area does also not seem right because people may want to fill the larger area. Does anybody reading this have an idea how to define that mathematically or even in TikZ?

Finally I'd like to present a "cheating" solution, which is almost as powerful as the above but does not require to modify the plot handler and works with any \draw commands, not just plot. Just draw the thing twice and let the coordinate system decide whether the point is visible. If not, return a point on the boundary of the circle defining the sphere.

\documentclass[tikz,border=3.14mm]{standalone}
\usetikzlibrary{intersections,decorations.markings}
\usepackage{tikz-3dplot}

\makeatletter

%along z axis
\define@key{z sphericalkeys}{theta}{\def\mytheta{#1}}
\define@key{z sphericalkeys}{phi}{\def\myphi{#1}}
\tikzdeclarecoordinatesystem{z spherical visible}{%
\setkeys{z sphericalkeys}{#1}%
\pgfmathsetmacro{\Xtest}{cos(90-\tdplotmaintheta)*cos(\tdplotmainphi-90)*cos(\mytheta)*cos(\myphi)
+cos(90-\tdplotmaintheta)*sin(\tdplotmainphi-90)*cos(\mytheta)*sin(\myphi)
+sin(90-\tdplotmaintheta)*sin(\mytheta)}
% \Xtest is the projection of the coordinate on the normal vector of the visible plane
\pgfmathsetmacro{\ntest}{ifthenelse(\Xtest<0,0,1)}
\ifnum\ntest=0
\else
\fi
}

\tikzdeclarecoordinatesystem{z spherical invisible}{%
\setkeys{z sphericalkeys}{#1}%
\pgfmathsetmacro{\Xtest}{cos(90-\tdplotmaintheta)*cos(\tdplotmainphi-90)*cos(\mytheta)*cos(\myphi)
+cos(90-\tdplotmaintheta)*sin(\tdplotmainphi-90)*cos(\mytheta)*sin(\myphi)
+sin(90-\tdplotmaintheta)*sin(\mytheta)}
% \Xtest is the projection of the coordinate on the normal vector of the visible plane
%\typeout{\raarot,\rbarot,\rabrot,\rbbrot,\racrot, \rbcrot}
\pgfmathsetmacro{\ntest}{ifthenelse(\Xtest<0,0,1)}
\ifnum\ntest=1
\else
\fi
}

%%%%%%%%%%%%%%%%%

\makeatother
% decoration
\begin{document}

\foreach \X in {0,10,...,350}
{\begin{tikzpicture}

\tdplotsetmaincoords{110}{\X}
\begin{scope}[tdplot_main_coords,samples=60]
%
% phi={\tdplotmainphi-90},theta={90-\tdplotmaintheta});
% \draw[-latex] (0,0,0) -- (\RadiusSphere,0,0) node[below]{$x$};
% \draw[-latex] (0,0,0) -- (0,\RadiusSphere,0) node[left]{$y$};
% \draw[-latex] (0,0,0) -- (0,0,\RadiusSphere) node[left]{$z$};
\pgfmathtruncatemacro{\Dis}{ifthenelse(\X<50,1,0)+ifthenelse(\X>130,1,0)}
\ifnum\Dis=0
\else
\draw[opacity=0.3,fill opacity=0.2,fill=blue] plot[smooth,variable=\x,domain=-180:180]
\fi

\pgfmathtruncatemacro{\Dis}{ifthenelse(\X<230,1,0)+ifthenelse(\X>320,1,0)}
\ifnum\Dis=0
\else
\draw[fill opacity=0.5,fill=blue] plot[smooth,variable=\x,domain=-180:180]
\fi