[Tex/LaTex] 3D solids with Tikz – or an alternative

3dpst-3dplotpst-solides3dtikz-3dplottikz-pgf

I want to draw 3D shapes in latex. I have tried out different strategies, but nothing has led me to a good solution yet. First what I want to do, then what I have done so far.

In prioritized order, these are my constraints:

  1. Output (the figure) must be vector graphics
  2. I would REALLY like to compile my main document with pdflatex
  3. Text should preferably be scalable, i.e., the text stays the same, even if I decide to make the figure bigger in my document.
  4. Objects should have a shading that makes them look like they are lighted from somewhere (otherwise I cannot distinguish between a circle and a sphere).
  5. I want to use colors defined in my main files preamble – thus dynamic use of main file's preamble content (this is in an individual file).

This is probably not possible at the same time, but a trade off is welcome. What works for you when drawing 3D objects?

My progress so far

I've made friends with Tikz, but when familiarizing myself with its shortcomings (such as 3d solids it seems), I've flirted with PStricks – but this was a messy first date, so unless you guys REALLY advocate her as a part of the solution, I'd rather stay with Tikz (but hey, beggars can't be choosers).

I've made a MWE, where everything is included (in my case it is split in many different files (a PhD-thesis becomes quite large, after all).

This is the code that generates a sphere using the trick of implementing the "ball", which is not inherent 3D:

\documentclass{standalone}
\usepackage{pgfplots}
\pgfplotsset{compat=1.4}
\usepackage{tikz-3dplot}
\tdplotsetmaincoords{60}{-30}
\tdplotsetrotatedcoords{0}{90}{90}%

%\usepackage[rgb]{xcolor}
\definecolor{c1}{rgb}{0.2,0.4,0.6} % Blue-ish
\definecolor{c2}{rgb}{1.0,0.0,0.6} % Pink-is
\definecolor{c3}{rgb}{0.6,0.0,0.0} % Red

\begin{document}

\begin{tikzpicture}
  [tdplot_rotated_coords,
    scale=3,
    cube/.style={color=c1,thick,draw=gray, fill opacity=0.5,line join=round},
    mds/.style={ball color=c2, c2, opacity=.8},
    helplines/.style={gray,line cap=round},
    length/.style={<->,thick,line cap=round},
    axis/.style={->,c3,ultra thick,line cap=round},
    textlabel/.style={fill opacity=.7,text opacity=1,fill=white,rounded corners}]
  \def\d{1}
  \def\r{\d*.45}
  \def\af{\d*.5}

    % Draw backside of the cube
    \fill[cube] (0,0,\d) -- (0,\d,\d) -- (\d,\d,\d) -- (\d,0,\d) -- cycle;
    \fill[cube] (0,0,0) -- (0,0,\d) -- (\d,0,\d) -- (\d,0,0) -- cycle;
    \fill[cube] (\d,0,0) -- (\d,0,\d) -- (\d,\d,\d) -- (\d,\d,0) -- cycle;

    % Draw helplines
  \foreach \t in {0,12,...,348} % circle
    \draw[helplines] ({cos(\t   )*\r+\d/2}, \d/2, {sin(\t   )*\r+\d/2})
                      -- ({cos(\t+12)*\r+\d/2}, \d/2, {sin(\t+12)*\r+\d/2});
  \draw[helplines] (\d/2,\d/2-\r,\d/2) -- (\d/2,\d/2+\r,\d/2); % vertical line

    % Cylinder 
  \shade[mds] (\d/2,\d/2,\d/2) circle (\r cm); % <= the little " cm" is needed to "trick" (?) everything into working...

    % Draw front of cube
    \fill[cube,fill=none] (0,0,0) -- (0,\d,0) -- (\d,\d,0) -- (\d,0,0) -- cycle;
    \fill[cube,fill=none] (0,\d,0) -- (0,\d,\d) -- (\d,\d,\d) -- (\d,\d,0) -- cycle;
    \fill[cube,fill=none] (0,0,0) -- (0,0,\d) -- (0,\d,\d) -- (0,\d,0) -- cycle;

    % Draw the axis arrows and annotations
    \draw[axis] (0,0,0) -- (\af,0,0) node[textlabel,anchor=east]{$x$};
    \draw[axis] (0,0,0) -- (0,\af,0) node[textlabel,anchor=south]{$y$};
    \draw[axis] (0,0,0) -- (0,0,\af) node[textlabel,anchor=west]{$z$};

    % Draw radius arrow
    \draw[mds,length,draw] (\d/2,\d/2,\d/2) -- (\d/2,\d/2,\d/2+\r) node[textlabel,pos=0.5, auto=above]{$r$};

    % Draw cube lattice length measures
    \draw[cube,length,c1] (0,0,\d*5/6) -- (0,\d,\d*5/6) node[textlabel,pos=0.5, auto=above]{$d$};
    \draw[cube,length,c1] (\d*5/6,0,0) -- (\d*5/6,\d,0) node[textlabel,pos=0.5, auto=above]{$d$};
    \draw[cube,length,c1] (\d*5/6,\d,0) -- (\d*5/6,\d,\d) node[textlabel,pos=0.5, auto=above]{$d$};

    % Material parameters label
    \draw[mds] (\d/2,\d/2+\r/2,\d/2) node[textlabel]{$\varepsilon_c,\mu_c$};
\end{tikzpicture}

\end{document}

Which produces:
Sphere with tikz

I have worked on some code for manually coding a cylinder, but I encounter two main problems:

  1. I cannot dynamically change the fill, to create a shading effect.
  2. I cannot draw the top/bottom circles (ellipses in the viewpoint perspective)

Here's my code:

\documentclass{standalone}
\usepackage{pgfplots}
\pgfplotsset{compat=1.4}
\usepackage{tikz-3dplot}
\tdplotsetmaincoords{60}{-30}
\tdplotsetrotatedcoords{0}{90}{90}%

%\usepackage[rgb]{xcolor}
\definecolor{c1}{rgb}{0.2,0.4,0.6} % Blue-ish
\definecolor{c2}{rgb}{1.0,0.0,0.6} % Pink-is
\definecolor{c3}{rgb}{0.6,0.0,0.0} % Red

\begin{document}

\begin{tikzpicture}
  [tdplot_rotated_coords,
    scale=3,
    cube/.style={color=c1,thick,draw=gray, fill opacity=0.5,line join=round},
    mdc/.style={fill=c2, color=c2,draw=none, opacity=.4,line join=round},
    helplines/.style={gray,line cap=round},
    length/.style={<->,thick,line cap=round},
    axis/.style={->,c3,ultra thick,line cap=round},
    textlabel/.style={fill opacity=.7,text opacity=1,fill=white,rounded corners}]
  \def\d{1}
  \def\r{\d*.45}
  \def\af{\d*.5}

    % Draw backside of the cube
    \fill[cube] (0,0,\d) -- (0,\d,\d) -- (\d,\d,\d) -- (\d,0,\d) -- cycle;
    \fill[cube] (0,0,0) -- (0,0,\d) -- (\d,0,\d) -- (\d,0,0) -- cycle;
    \fill[cube] (\d,0,0) -- (\d,0,\d) -- (\d,\d,\d) -- (\d,\d,0) -- cycle;

    % Draw helplines
  \foreach \t in {0,12,...,348} % circle
    \draw[helplines] ({cos(\t   )*\r+\d/2}, \d/2, {sin(\t   )*\r+\d/2})
                      -- ({cos(\t+12)*\r+\d/2}, \d/2, {sin(\t+12)*\r+\d/2});
  \draw[helplines] (\d/2,\d/2-\r,\d/2) -- (\d/2,\d/2+\r,\d/2); % vertical line

    % Cylinder 
  \foreach \t in {0,12,...,348}
    \draw[mdc] ({cos(\t   )*\r+\d/2},  0, {sin(\t   )*\r+\d/2}) % side vertice of cylinder
            -- ({cos(\t+12)*\r+\d/2},  0, {sin(\t+12)*\r+\d/2})
            -- ({cos(\t+12)*\r+\d/2}, \d, {sin(\t+12)*\r+\d/2})
            -- ({cos(\t   )*\r+\d/2}, \d, {sin(\t   )*\r+\d/2})
            -- cycle;

    % Draw front of cube
    \fill[cube,fill=none] (0,0,0) -- (0,\d,0) -- (\d,\d,0) -- (\d,0,0) -- cycle;
    \fill[cube,fill=none] (0,\d,0) -- (0,\d,\d) -- (\d,\d,\d) -- (\d,\d,0) -- cycle;
    \fill[cube,fill=none] (0,0,0) -- (0,0,\d) -- (0,\d,\d) -- (0,\d,0) -- cycle;

    % Draw the axis arrows and annotations
    \draw[axis] (0,0,0) -- (\af,0,0) node[textlabel,anchor=east]{$x$};
    \draw[axis] (0,0,0) -- (0,\af,0) node[textlabel,anchor=south]{$y$};
    \draw[axis] (0,0,0) -- (0,0,\af) node[textlabel,anchor=west]{$z$};

    % Draw radius arrow
    \draw[mdc,length,draw] (\d/2,\d/2,\d/2) -- (\d/2,\d/2,\d/2+\r) node[textlabel,pos=0.5, auto=above]{$r$};

    % Draw cube lattice length measures
    \draw[cube,length,c1] (0,0,\d*5/6) -- (0,\d,\d*5/6) node[textlabel,pos=0.5, auto=above]{$d$};
    \draw[cube,length,c1] (\d*5/6,0,0) -- (\d*5/6,\d,0) node[textlabel,pos=0.5, auto=above]{$d$};
    \draw[cube,length,c1] (\d*5/6,\d,0) -- (\d*5/6,\d,\d) node[textlabel,pos=0.5, auto=above]{$d$};

    % Material parameters label
    \draw[mdc] (\d/2,\d/2+\r/2,\d/2) node[textlabel]{$\varepsilon_c,\mu_c$};
\end{tikzpicture}

\end{document}

Which produces:
Cylinder with Tikz

I am definitely open to a whole new approach to my work routine, but it must be advantageous, otherwise I will just drop the shading requirement (which is half the fun though, one looses depth perception without it).

Thank you so much for taking the time to read this LOOOONG question.

Best Answer

Here's an alternative using Asymptote. It fulfills some version of most of your requests; for instance, the colors are defined in the preamble (but in Asymptote code rather than TeX code, so you might need to define TeX versions separately).

% To run: pdflatex --shell-escape filename.tex

\documentclass[margin=10pt,convert]{standalone}
\usepackage{asypictureB}

\begin{asyheader}
pen c1 = rgb(0.2,0.4,0.6); // Blue-ish
pen c2 = rgb(1.0,0.0,0.6); // Pink-ish
pen c3 = rgb(0.6,0.0,0.0); // Red

texpreamble("\newbox\BoxForRules
\newcommand{\boxToRule}[1]{%
    \setbox\BoxForRules=\hbox{\hspace{2pt}#1\hspace{2pt}}%
    $\rlap{\rule[\dimexpr-\dp\BoxForRules-2pt]{\wd\BoxForRules}{\dimexpr\ht\BoxForRules+\dp\BoxForRules+4pt}}\mbox{\hspace{2pt}#1\hspace{2pt}}$%
}");

import three;

void framed_label(string s, triple position, pen p = currentpen) {
    label("\boxToRule{" + s + "}", position=position, p=white + opacity(0.6));
    label(s, position=position, p=p);
}
\end{asyheader}

\begin{document}
\begin{asypicture}{name=cylinder}
    settings.outformat = "pdf";
    settings.render = 0;

    real unit = 4cm;
    unitsize(unit);

    currentprojection = orthographic((-4,2.8,-2), up=Y);

    real d = 1;
    real r = 0.45 d;
    real af = 0.5 d;

    pen cubedraw = linewidth(0.8pt) + gray;
    pen cubefill = opacity(0.5) + c1;
    pen axis = linewidth(1.0 pt) + c3;

    //Draw backside of cube
    draw( surface((0,0,d) -- (0,d,d) -- (d,d,d) -- (d,0,d) -- cycle), 
        meshpen = cubedraw, 
        surfacepen = emissive(cubefill));
    draw( surface((0,0,0) -- (0,0,d) -- (d,0,d) -- (d,0,0) -- cycle), meshpen = cubedraw, surfacepen = emissive(cubefill));
    draw( surface((d,0,0) -- (d,0,d) -- (d,d,d) -- (d,d,0) -- cycle), meshpen = cubedraw, surfacepen = emissive(cubefill));

    //Draw cylinder as surface of revolution
    path3 cyl_center = shift(d/2,0,d/2) * (O -- d*Y);
    path3 cyl_edge = shift(r*X) * cyl_center;
    int n = 10;
    guide3 to_revolve = point(cyl_edge, 0);
    for (int i = 1; i <= n; ++i)
        to_revolve = to_revolve -- point(cyl_edge, i/n);
    surface cylinder = surface(to_revolve, c=(d/2,0,d/2), axis=Y);
    draw(cylinder, meshpen = 0.3 c2 + 0.5 gray, 
            surfacepen = material(c2 + opacity(0.7), emissivepen = 0.2 c2));


    //Draw the radius arrow and center line
    draw(cyl_center, 0.8 c2);
    //draw(circle(c=(d/2,d/2,d/2), r=r, normal=Y), 0.8 c2);
    draw( (d/2,d,d/2) -- (d/2,d,d/2+r), c2 + linewidth(0.8), arrow=Arrows3(TeXHead2));
    framed_label("$r$", position=(d/2, d, d/2 + r/2), p=c2);

    //Draw front of cube
    draw( (0,0,0) -- (0,d,0) ^^ (d,d,0) -- (0,d,0) ^^ (0,d,d) -- (0,d,0), cubedraw);

    //Draw the axis arrows and annotations
    draw(O -- af*X, axis, arrow=Arrow3(TeXHead2));
    framed_label("$x$", position=(1 + 18pt/unit)*af*X, p=axis);
    draw(O -- af*Y, axis, arrow=Arrow3(TeXHead2));
    framed_label("$y$", position=(1 + 12pt/unit)*af*Y, p=axis);
    draw(O -- af*Z, axis, arrow=Arrow3(TeXHead2));
    framed_label("$z$", position=(1 + 10pt/unit)*af*Z, p=axis);

    //Draw cube lattice length measures
    draw((0,0,d*5/6) -- (0,d,d*5/6), p=linewidth(0.8pt) + c1, arrow=Arrows3(TeXHead2));
    framed_label("$d$", (0,d/2,d*5/6), c1);
    draw((d*5/6,0,0) -- (d*5/6,d,0), p=linewidth(0.8pt) + c1, arrow=Arrows3(TeXHead2));
    framed_label("$d$", (d*5/6, d/2, 0), c1);
    draw((d*5/6, d, 0) -- (d*5/6, d, d), p=linewidth(0.8pt) + c1, arrow=Arrows3(TeXHead2));
    framed_label("$d$", (d*5/6, d, d/2), c1);

    //Material parameters label
    framed_label("$\varepsilon_c, \mu_c$", position=(d/2, d/2+r/2, d/2), p=c1);
\end{asypicture}

\begin{asypicture}{name=sphere}
    settings.outformat = "pdf";
    settings.render = 0;
    import graph3;

    real unit = 4cm;
    unitsize(unit);

    currentprojection = orthographic((-4,2.8,-2), up=Y);

    real d = 1;
    real r = 0.45 d;
    real af = 0.5 d;

    pen cubedraw = linewidth(0.8pt) + gray;
    pen cubefill = opacity(0.5) + c1;
    pen axis = linewidth(1.0 pt) + c3;

    //Draw backside of cube
    draw( surface((0,0,d) -- (0,d,d) -- (d,d,d) -- (d,0,d) -- cycle), 
        meshpen = cubedraw, 
        surfacepen = emissive(cubefill));
    draw( surface((0,0,0) -- (0,0,d) -- (d,0,d) -- (d,0,0) -- cycle), meshpen = cubedraw, surfacepen = emissive(cubefill));
    draw( surface((d,0,0) -- (d,0,d) -- (d,d,d) -- (d,d,0) -- cycle), meshpen = cubedraw, surfacepen = emissive(cubefill));

    //Draw sphere as surface of revolution
    triple centerpoint = (d/2, d/2, d/2);
    path3 centerline = (centerpoint - r*Y) -- (centerpoint + r*Y);
    path3 to_revolve = Arc(centerpoint - r*Y, centerpoint + r*Y, c=centerpoint, normal=X, n=16);
    surface sphere = surface(to_revolve, c=centerpoint, axis=Y);
    draw(sphere, meshpen = 0.3 c2 + 0.5 gray, 
            surfacepen = material(c2 + opacity(0.7), emissivepen = 0.2 c2));


    //Draw the radius arrow and helper lines
    draw(centerline, 0.8 c2);
    draw(circle(c=(d/2,d/2,d/2), r=r, normal=Y), 0.8 c2);
    draw( (d/2,d/2,d/2) -- (d/2,d/2,d/2+r), c2 + linewidth(0.8), arrow=Arrows3(TeXHead2));
    framed_label("$r$", position=(d/2, d/2, d/2 + r/2), p=c2);

    //Draw front of cube
    draw( (0,0,0) -- (0,d,0) ^^ (d,d,0) -- (0,d,0) ^^ (0,d,d) -- (0,d,0), cubedraw);

    //Draw the axis arrows and annotations
    draw(O -- af*X, axis, arrow=Arrow3(TeXHead2));
    framed_label("$x$", position=(1 + 18pt/unit)*af*X, p=axis);
    draw(O -- af*Y, axis, arrow=Arrow3(TeXHead2));
    framed_label("$y$", position=(1 + 12pt/unit)*af*Y, p=axis);
    draw(O -- af*Z, axis, arrow=Arrow3(TeXHead2));
    framed_label("$z$", position=(1 + 10pt/unit)*af*Z, p=axis);

    //Draw cube lattice length measures
    draw((0,0,d*5/6) -- (0,d,d*5/6), p=linewidth(0.8pt) + c1, arrow=Arrows3(TeXHead2));
    framed_label("$d$", (0,d/2,d*5/6), c1);
    draw((d*5/6,0,0) -- (d*5/6,d,0), p=linewidth(0.8pt) + c1, arrow=Arrows3(TeXHead2));
    framed_label("$d$", (d*5/6, d/2, 0), c1);
    draw((d*5/6, d, 0) -- (d*5/6, d, d), p=linewidth(0.8pt) + c1, arrow=Arrows3(TeXHead2));
    framed_label("$d$", (d*5/6, d, d/2), c1);

    //Material parameters label
    framed_label("$\varepsilon_c, \mu_c$", position=(d/2, d/2+r/2, d/2), p=c1);
\end{asypicture}

\end{document}

Here's the result:

enter image description here

From a technical standpoint, the most difficult aspect was the white boxes framing the text; as you can see, I gave up on the rounded corners.

I'd also note that a lot of this would be easier if you were willing to use high-resolution rasterized graphics: it would be determined automatically what objects go in front of what others (independent of the drawing order), and the gridlines would not be necessary.


A note on workflows using Asymptote: here are some of the available options. (All of them require a working Asymptote installation, although that is automatic with TeXLive.)

  1. Use a standard latex command (e.g., pdflatex) with the -shell-escape option enabled. This requires the asypictureB package. On any given run, only those pictures that have changed (or been deleted) since the last run will be recompiled. You can create a "common preamble" that is shared among all your Asymptote files including e.g. color definitions, but definitions in your TeX preamble do not carry over automatically.
    This is the option I have suggested using above.

  2. Use the latexmk script, with the configuration file augmented as described in this documentation. For this, you should probably use the asymptote package rather than asypictureB, which is not designed to be used this way. You still have the option of a common Asymptote preamble. With the inline option of the asymptote package, you also have access to all the packages and macros of your main document in the labels.

  3. Use either asymptote or asypictureB with multiple runs to compile the pictures separately. The command sequence
    pdflatex filename
    asy filename-*.asy
    pdflatex filename
    will usually work with either package (or both), although every image will be recompiled--including those that have not changed since the last run. A more efficient alternative to the middle line is offered by the asypictureB package; see the package documentation, section 2.1, page 4.

Personally, I prefer the first option, because it offers the best debugging support by a fair margin. At the same time, I recommend that LaTeX users in general be familiar with using latexmk, since it can be used to automate an entire workflow, including making the index (if necessary) and multiple runs to define labels and the table of contents (again, only if necessary; latexmk is good at detecting this).