\chapter{Building a scene} \section{The scene model} lua-tikz3dtools is easiest to use when one thinks in terms of a scene rather than a sequence of immediate drawing commands. A point, curve, surface, or triangle appended to the scene is not drawn at once. Instead, it is stored as part of a temporary collection of simplices. When \verb|\displaysimplices| is finally called, that collection is processed and emitted. This distinction is important because it affects how one reasons about order. The order in which objects are appended is not generally the order in which they appear on the page. The package reserves the right to partition and sort the geometry before producing the final paths. \section{Named objects and reusable expressions} The command \verb|\setobject| binds a Lua value into the expression environment. In practice, this is the natural way to give a name to a matrix, vector, or reusable geometric object. Once defined, that name becomes available to later expressions in the same document session. \begin{verbatim} \setobject[ name=spin, object={Matrix.zrotation3(pi/6):multiply( Matrix.xrotation3(pi/8) )} ] \appendtriangle[ A={Vector:new{0,0,0,1}}, B={Vector:new{1,0,0,1}}, C={Vector:new{0,1,0,1}}, transformation={spin:multiply(Matrix.translate3(0,0,0.5))}, fill options={fill=ltdtbrightness, draw=black} ] \end{verbatim} One should not be shy about naming intermediate objects. A manual source file becomes significantly easier to read when rotations, translations, reference points, and clipping vectors are given names rather than repeated as raw expressions. \section{Transformations} Transformations are expressed with the package's \verb|Matrix| type. The helpers currently exposed by the package are \verb|Matrix.identity3()|, \verb|Matrix.translate3(dx,dy,dz)|, \verb|Matrix.scale3(sx,sy,sz)|, \verb|Matrix.xrotation3(theta)|, \verb|Matrix.yrotation3(theta)|, \verb|Matrix.zrotation3(theta)|, and \verb|Matrix.zyzrotation3(alpha,beta,gamma)|. The package uses a row-vector convention. A point is multiplied on the left by its transformation matrix. Consequently, composed transformations read from left to right in the order in which they are applied to the point. If one writes \verb|Matrix.scale3(...):multiply(Matrix.translate3(...))|, the point is first scaled and then translated. \manualnote{This left-to-right composition rule is one of the first things to verify when a figure appears in the wrong place. Many users come to the package expecting the opposite convention from other graphics systems.} \begin{figure}[tbp] \centering \begin{tikzpicture} \setobject[name=rot, object={Matrix.zrotation3(pi/4)}] \setobject[name=shift, object={Matrix.translate3(3,1,0)}] \appendtriangle[ A={Vector:new{0,0,0.01,1}}, B={Vector:new{1.5,0,0.01,1}}, C={Vector:new{0.75,1.3,0.01,1}}, fill options={fill=blue!25, draw=blue!70!black, thick} ] \appendtriangle[ A={Vector:new{0,0,0.02,1}}, B={Vector:new{1.5,0,0.02,1}}, C={Vector:new{0.75,1.3,0.02,1}}, transformation={rot}, fill options={fill=green!25, draw=green!60!black, thick} ] \appendtriangle[ A={Vector:new{0,0,0.03,1}}, B={Vector:new{1.5,0,0.03,1}}, C={Vector:new{0.75,1.3,0.03,1}}, transformation={rot:multiply(shift)}, fill options={fill=red!25, draw=red!70!black, thick} ] \appendlabel[ v={return Vector:new{0.5,-0.5,0,1}}, text={original} ] \appendlabel[ v={return Vector:new{-1,0.8,0,1}}, text={rotated} ] \appendlabel[ v={return Vector:new{3,2.6,0,1}}, text={rotated$+$translated} ] \displaysimplices \end{tikzpicture} \caption{Transformation order for a simple object. The blue triangle is in its base position, the green one has been rotated by~$\pi/4$ about the $z$-axis, and the red one has been rotated and then translated. Because the package uses row-vector convention, composed matrices read left to right.} \end{figure} \section{Filters} Filters are small Lua predicates supplied through the \verb|filter| key. They are evaluated after tessellation but before the occlusion sort. This gives the user a convenient way to discard simplices that fall outside a desired region. The filter environment depends on the simplex type. Points and labels expose \verb|A|. Line segments expose \verb|A| and \verb|B|. Triangles expose \verb|A|, \verb|B|, and \verb|C|. Each of these is a \verb|Vector| object in homogeneous coordinates. A filter should return \verb|true| for simplices that are to remain and \verb|false| for simplices that are to be discarded. \begin{verbatim} \appendsurface[ ustart=-1, ustop=1, usamples=24, vstart=-1, vstop=1, vsamples=24, v={return Vector:new{u,v,0.3*(u^2-v^2),1}}, fill options={fill=ltdtbrightness, draw=black, very thin}, filter={ return A[3] >= 0 and B[3] >= 0 and C[3] >= 0 } ] \end{verbatim} This style of predicate is intentionally direct. It keeps the clipping rule in an algebraic form that can be inspected at a glance. In more elaborate scenes, it is often better to define the relevant vectors or matrices once with \verb|\setobject| and then write the filter in terms of those named objects. \begin{figure}[tbp] \centering \begin{tikzpicture} \setobject[ name=view, object={Matrix.xrotation3(-pi/5):multiply(Matrix.zrotation3(pi/6))} ] \appendlight[v={return Vector:new{1,0.5,1.5,1}}] \appendsurface[ ustart=-1.2, ustop=1.2, usamples=28, vstart=-1.2, vstop=1.2, vsamples=28, v={return Vector:new{2*u, 2*v, 0.6*(u*u-v*v), 1}}, transformation={view}, fill options={fill=ltdtbrightness, draw=black, very thin}, filter={ return A[3] >= 0 and B[3] >= 0 and C[3] >= 0 } ] \displaysimplices \end{tikzpicture} \caption{A saddle surface with the region below the~$xy$-plane removed by a symbolic filter. The predicate \texttt{return A[3] >= 0 and B[3] >= 0 and C[3] >= 0} discards any triangle whose projected vertices have negative $z$-coordinates. Filtering acts on the tessellated simplices, not on the symbolic surface.} \end{figure} Filters become significantly more powerful when they operate in a coordinate system other than world space. A common technique is to define an inverse map that projects each simplex centroid back into the parameter domain of the surface, and then test membership in a region described in that domain. This lets one cut elaborate shapes from a surface even when the corresponding world-space boundary would be impractical to express analytically. The following example demonstrates this approach on a torus. The object \verb|torusinverse| maps a world-space point back to the torus parameters $(u,v)\in[0,2\pi)^2$, and the filter discards any triangle whose centroid is too close to a reference point in parameter space. A second surface fills the excised region with a contrasting colour, making the boundary clearly visible. \begin{verbatim} \setobject[ name = {torusinverse}, object = { function(p) local x, y, z = p[1], p[2], p[3] local theta = atan2(y, x) if theta < 0 then theta = theta + tau end local phi = atan2(sqrt(x^2+y^2) - 3, z) if phi < 0 then phi = phi + tau end return Vector:new{theta, phi, 0, 1} end } ] \appendsurface[ ..., filter = { torusinverse( A:hadd(B):hadd(C):hscale(1/3) :multiply(viewinverse) ):hdistance(Vector:new{2,2,0,1}) > 0.5 } ] \end{verbatim} \begin{figure}[tbp] \centering \begin{tikzpicture} \setobject[ name = {view}, object = { Matrix.zyzrotation3(pi/2, pi/2+0.1, 3.75*pi/6) } ] \setobject[ name = {viewinverse}, object = {view:inverse()} ] \setobject[ name = {torusinverse}, object = { function(p) local x, y, z = p[1], p[2], p[3] local theta = atan2(y, x) if theta < 0 then theta = theta + tau end local phi = atan2(sqrt(x^2 + y^2) - 3, z) if phi < 0 then phi = phi + tau end return Vector:new{theta, phi, 0, 1} end } ] \setobject[ name = {holecentre}, object = {Vector:new{2, 2, 0, 1}} ] \setobject[ name = {holeradius}, object = {0.5} ] \appendlight[v={return Vector:new{1, 1, 1, 1}}] % Patch filling the excised region \appendsurface[ ustart = {0}, ustop = {tau}, usamples = {20}, vstart = {-1}, vstop = {0.5}, vsamples = {4}, v = {return Vector:new{ 3*cos(cos(u)/2+2) + Vector.hsphere3(cos(u)/2+2, sin(u)/2+2, 1+v)[1], 3*sin(cos(u)/2+2) + Vector.hsphere3(cos(u)/2+2, sin(u)/2+2, 1+v)[2], Vector.hsphere3(cos(u)/2+2, sin(u)/2+2, 1+v)[3], 1 }}, transformation = {view}, filter = {false}, fill options = { preaction = {fill opacity=1, fill=gray}, postaction = {draw=red, line width=0.2pt, line join=round, line cap=round} } ] % Main torus with hole \appendsurface[ ustart = {0}, ustop = {tau}, usamples = {20}, vstart = {0}, vstop = {tau}, vsamples = {14}, v = {return Vector:new{ 3*cos(u) + Vector.hsphere3(u, v, 1)[1], 3*sin(u) + Vector.hsphere3(u, v, 1)[2], Vector.hsphere3(u, v, 1)[3], 1 }}, transformation = {view}, fill options = { preaction = {fill opacity=1, fill=ltdtbrightness}, postaction = {draw, line width=0.2pt, line join=round, line cap=round} }, filter = { torusinverse( A:hadd(B):hadd(C):hscale(1/3) :multiply(viewinverse) ):hdistance(holecentre) > holeradius } ] \displaysimplices \end{tikzpicture} \caption{A torus with an excised region defined in parameter space. The named function \texttt{torusinverse} maps the centroid of each triangle back to the torus parameters~$(u,v)$; triangles whose image lies within a disc of radius~$0.5$ around the point~$(2,2)$ are discarded. A second, coarser surface fills the hole with a contrasting colour to make the boundary visible. This technique generalises to any surface for which an inverse parametrization can be written.} \end{figure} \section{Lighting and final rendering} Directional lights are appended with \verb|\appendlight|. During rendering, each triangle's normal is compared to the light directions, and the package defines a TikZ color called \verb|ltdtbrightness| from the resulting average intensity. The falloff is linear in the angle between the triangle normal and the light direction: a triangle facing the light is bright, and one perpendicular to it is dark. In practical use, this means that surface fill options often take the form \verb|fill=ltdtbrightness| together with a draw style. If no lights are present, the brightness resolves to black. Lights are cleared after each call to \verb|\displaysimplices|, just as the scene geometry itself is cleared. Labels are handled differently from other scene elements. They pass through filtering but are emitted after the rest of the geometry. A label should therefore be thought of as an annotation layer rather than as an occluded object. \begin{figure}[tbp] \centering \begin{tikzpicture} \setobject[ name = {view}, object = {Matrix.xrotation3(-pi/4):multiply(Matrix.zrotation3(pi/5))} ] \appendlight[v={return Vector:new{1, 0.8, 1.5, 1}}] % A torus \appendsurface[ ustart = {0}, ustop = {tau}, usamples = {28}, vstart = {0}, vstop = {tau}, vsamples = {16}, v = {return Vector:new{ (2 + 0.7*cos(v))*cos(u), (2 + 0.7*cos(v))*sin(u), 0.7*sin(v), 1 }}, transformation = {view}, fill options = { preaction = {fill=ltdtbrightness}, postaction = {draw=black, line width=0.1pt, line join=round} } ] \displaysimplices \end{tikzpicture} \caption{A torus illuminated by a single directional light. The colour \texttt{ltdtbrightness} is computed per-triangle from the angle between the triangle normal and the light direction. Regions facing the light appear white, while those perpendicular to it approach black.} \end{figure}