4

I'm trying to shade 3D parametric surfaces using a basic 3D lighting model, but this answer says that TikZ has no support for lighting. The only option is to use color gradients to shade vertices. This may be acceptable for certain graphs, but not when you're trying to show the actual shape of a 3D object.

But TikZ clearly has all the data available to do this, though. Derivatives can be calculated numerically for each vertex (i.e. without the user needing to analytically derive them by hand for each surface). These can then be use to construct the normals. Let the user specify a point light location and presto, you have diffuse lighting. Snag the camera position and you have specular lighting as well.

I know that packages like Asymptote can create nice 3D images, but these solutions create raster graphics, and I need vector graphics.

How can I add 3D lighting and shading to TikZ? Is there a reason it doesn't exist already? I don't know how TikZ is implemented, and I haven't written any extensions before, so I have no idea how I would add this feature.

2
  • Welcome to TeX-SE. It is true that TikZ has all the data necessary to compute a realistic shading in principle. However, realizing this in practice is a very different story. Even though automatically hiding hidden surfaces is an in principle solved problem, realizing it in practice is tough. That's why asymptote is not super short package. And asymptote does allow you to produce 3d vector graphics, see p. 55 of this nice tutorial, albeit with limitations.
    – user121799
    Commented Mar 3, 2019 at 18:37
  • You might be able to use Sage combined with the sagetex package. Sage documentation here gives examples with 3D and lighting and parametric. You can see what code looks like by going here, copying and pasting code samples. Images can be saved as svg. Try code, for example, of " 4 spheres that illustrates various uses of the texture command". Worth mentioning Sage should be able to do the math you talk about and work into the tikzpicture.
    – DJP
    Commented Mar 3, 2019 at 23:07

1 Answer 1

5

enter image description here

It is obtained by direct computations inside TikZ. The surface is given parametrically, but the LaTeX memory limitations are almost touched already. The code is below, adapted from the answer I gave at Shading a torus in TikZ. There are some explanations there. I give the code again since I added the shadow.

The coordinates are shadowless; it's tempting to try the cast shadow of the Oz axis... Maybe DJP'ideea of using sagetex to perform computations is the reasonable one.

\documentclass[margin=10pt]{standalone}
\usepackage{ifthen}
\usepackage[rgb]{xcolor}
\usepackage{tikz}
\usetikzlibrary{cd, arrows, matrix, intersections, math, calc}
\xdefinecolor{O}{RGB}{255, 102, 17}
\xdefinecolor{B}{RGB}{17, 87, 221}

\begin{document}

\tikzmath{%
  real \slongit, \slatit, \sunx, \suny, \sunz;  % towards the light source 
  real \longit, \latit, \tox, \toy, \toz;
  real \newxx, \newxy, \newyx, \newyy, \newzx, \newzy;  
  \slongit = 100; \slatit = 45;
  \sunx = sin(\slongit)*cos(\slatit);
  \suny = sin(\slatit);
  \sunz = cos(\slongit)*cos(\slatit);
  \longit = 25;  \latit = 36;  % 35;
  \tox = sin(\longit)*cos(\latit);
  \toy = sin(\latit);
  \toz = cos(\longit)*cos(\latit);
  \newxx = cos(\longit); \newxy = -sin(\longit)*sin(\latit);
  \newyy = cos(\latit);
  \newzx = -sin(\longit); \newzy = -cos(\longit)*sin(\latit);
  real \ry, \rz;
  \ry = 4;
  \rz = 1.5;  
  integer \Ny, \Nz, \j, \k, \prevj, \prevk, \aj, \ak;
  % j moves around Oy and k moves around Oz.
  % They describe full circles of radii \ry and \rz respectively.
  \Nz = 48;  % 24;  % 60;
  \Ny = 80;  % 36;  % 120;
  \ktmp = \Nz-1; 
  \jtmp = \Ny-1;
  \aj = 10;
  \ak = 0;
  function isSeen(\j, \k) {
    let \px = cos(360*(\k/\Nz))*cos(360*(\j/\Ny));
    let \py = -sin(360*(\k/\Nz));
    let \pz = cos(360*(\k/\Nz))*sin(360*(\j/\Ny));
    let \res = \px*\tox + \py*\toy + \pz*\toz;
    if \res>0 then {return 1;} else {return 0;};
  };
  function inLight(\j, \k) {%
    let \px = cos(360*(\k/\Nz))*cos(360*(\j/\Ny));
    let \py = -sin(360*(\k/\Nz));
    let \pz = cos(360*(\k/\Nz))*sin(360*(\j/\Ny));
    return {\px*\sunx + \py*\suny + \pz*\sunz};
  };
  function projX(\j, \k) {%
    let \px = \ry+\rz*cos(360*(\k/\Nz))*cos(360*(\j/\Ny));
    let \py = -\rz*sin(360*(\k/\Nz));
    let \t = -(\rz+\py)/\suny;
    return {\px + \t*\sunx};
  };
  function projZ(\j, \k) {%
    let \py = -\rz*sin(360*(\k/\Nz));
    let \pz = \ry+\rz*cos(360*(\k/\Nz))*sin(360*(\j/\Ny));
    let \t = -(\rz+\py)/\suny;
    return {\pz + \t*\sunz};
  };
  function T(\j, \k) {%
    let \py = -\rz*sin(360*(\k/\Nz));
    let \pz = \ry+\rz*cos(360*(\k/\Nz))*sin(360*(\j/\Ny));
    return {\rz*(-1+sin(360*(\k/\Nz)))/\suny};
  };
}


\begin{tikzpicture}[every node/.style={scale=.8},
  x={(\newxx cm, \newxy cm)},
  y={(0 cm, \newyy cm)},
  z={(\newzx cm, \newzy cm)},
  evaluate={%
    % int \j, \k;
    real \tmp;
    for \j in {0, 1, ..., \Ny}{%
      for \k in {0, 1, ..., \Nz}{%
        \test{\j,\k} = isSeen(\j, \k);
        if \test{\j,\k}>0 then {%
          \tmp{\j,\k} = int(100*inLight(\j,\k)));
          if \tmp{\j,\k}>0 then {%
            \tmpW{\j,\k}=int(100*inLight(\j,\k)^2);
          }
          else {%
            \tmpK{\j,\k}=-int(100*inLight(\j,\k));
          };
        } else {};
      };
    };
  }]

  % points (P-\j-\k)
  \foreach \j in {0, ..., \Ny}{%
    \foreach \k in {0, ..., \Nz}{%
      \path
      ( {( \ry+\rz*cos(360*(\k/\Nz)) )*cos(360*(\j/\Ny))},
      {-\rz*sin(360*(\k/\Nz))},
      {( \ry+\rz*cos(360*(\k/\Nz)) )*sin(360*(\j/\Ny))} )
      coordinate (P-\j-\k);
    }
  }


  % shadow
  \foreach \k [remember=\k as \prevk (initially 0)] in {1, ..., \Nz}{%
    \foreach \j [remember=\j as \prevj (initially 0)] in {1, ..., \Ny}{%
      \fill[gray!70!black, opacity={.4*abs(inLight(\j,\k))}]
      ($(P-\j-\prevk)+T(\j,\prevk)*(\sunx, \suny, \sunz)$)
      -- ($(P-\prevj-\prevk)+T(\prevj,\prevk)*(\sunx, \suny, \sunz)$)
      -- ($(P-\prevj-\k)+T(\prevj,\k)*(\sunx, \suny, \sunz)$)
      -- ($(P-\j-\k)+T(\j,\k)*(\sunx, \suny, \sunz)$) -- cycle;
    }
  }
  
  % coordinate system $Oxyz$; first layer
  \draw[green!50!black]
  (0, 0, 0) -- (\ry, 0, 0)
  (0, 0, 0) -- (0, 0, \ry);

  % "squares"---the mesh
  \foreach \k [remember=\k as \prevk (initially 0)] in {1, ..., \Nz}{%
    \foreach \j [remember=\j as \prevj (initially 0)] in {1, ..., \Ny}{%
      \ifthenelse{\test{\j,\k}=1}{
        \ifthenelse{\tmp{\j,\k}>0}{
          \filldraw[white!\tmpW{\j,\k}!B]
          (P-\j-\prevk) -- (P-\prevj-\prevk)
          -- (P-\prevj-\k) --(P-\j-\k) -- cycle;
        }{%
          \filldraw[black!\tmpK{\j,\k}!B]
          (P-\j-\prevk) -- (P-\prevj-\prevk)
          -- (P-\prevj-\k) --(P-\j-\k) -- cycle;
        }
      }{}
    }
  }

  % longitude cycle
  \foreach \k [remember=\k as \prevk (initially 0)] in {1, ..., \Nz}{%
    \ifthenelse{\test{\aj,\k}=1}{
      \draw[red, thick] (P-\aj-\k) -- (P-\aj-\prevk);
    }{
      \draw[red, very thin, opacity=.4] (P-\aj-\k) -- (P-\aj-\prevk);
    }
  }

  % latitude cycle
  \foreach \j [remember=\j as \prevj (initially 0)] in {1, ..., \Ny}{%
    \ifthenelse{\test{\j,\ak}=1}{
      \draw[red, thick] (P-\j-\ak) -- (P-\prevj-\ak);
    }{
      \draw[red, very thin, opacity=.3] (P-\j-\ak) -- (P-\prevj-\ak);
    }
  }
  
  % coordinate system $Oxyz$; second layer
  \draw[green!50!black, -{Latex[length=5pt, width=5pt]}]
  (\ry+\rz, 0, 0) -- (8, 0, 0) node[right] {$x$};
  \draw[green!50!black, -{Latex[length=5pt, width=5pt]}]
  (0, 0, 0) -- (0, 6, 0) node[above] {$y$};
  \draw[green!50!black, -{Latex[length=5pt, width=5pt]}]
  (0, 0, \ry+\rz) -- (0, 0, 8) node[below left] {$z$};
\end{tikzpicture} 
\end{document}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .