Mathematica: 3D wire frames

2019-01-19 00:42发布

问题:

Does Mathematica support hidden line removal for wire frame images? If this isn't the case, has anybody here ever come across a way to do it? Lets start with this:

Plot3D[Sin[x+y^2], {x, -3, 3}, {y, -2, 2}, Boxed -> False]

To create a wire frame we can do:

Plot3D[Sin[x+y^2], {x, -3, 3}, {y, -2, 2}, Boxed -> False, PlotStyle -> None]

One thing we can do to achieve the effect is to color the all the surfaces white. This however, is undesirable. The reason is because if we export this hidden line wire frame model to pdf we will have all of those white polygons that Mathematica uses to render the image. I want to be able to obtain a wire frame with hidden line removal in pdf and/or eps format.


UPDATE:

I have posted a solution to this problem. The problem is that the code runs very slow. In its current state it is unable to generate the wireframe for the image in this question. Feel free to play with my code. I added a link to it at the end of my post. You can also find the code in this link

回答1:

Here I present a solution. First I will show how to use the function that generates the wire frame, then I will proceed to explain in detail the rest of the functions that compose the algorithm.


wireFrame

wireFrame[g_] := Module[{figInfo, opt, pts},
   {figInfo, opt} = G3ToG2Info[g];
   pts = getHiddenLines[figInfo];
   Graphics[Map[setPoints[#] &, getFrame[figInfo, pts]], opt]
]

The input of this function is a Graphics3D object preferably with no axes.

fig = ListPlot3D[
   {{0, -1, 0}, {0, 1, 0}, {-1, 0, 1}, {1, 0, 1}, {-1, 1, 1}},
   Mesh -> {10, 10},
   Boxed -> False,
   Axes -> False,
   ViewPoint -> {2, -2, 1},
   ViewVertical -> {0, 0, 1},
   MeshStyle -> Directive[RGBColor[0, 0.5, 0, 0.5]],
   BoundaryStyle -> Directive[RGBColor[1, 0.5, 0, 0.5]]
]

Now we apply the function wireFrame.

wireFrame[fig]

As you can see wireFrame obtained most of the lines and its colors. There is a green line that was not included in the wireframe. This is most likely due to my threshold settings.

Before I proceed to explain the details of the functions G3ToG2Info, getHiddenLines, getFrame and setPoints I will show you why wire frames with hidden line removal can be useful.

The image shown above is a screenshot of a pdf file generated by using the technique described in rasters in 3D graphics combined with the wire frame generated here. This can be advantageous in various ways. There is no need to keep the information for the triangles to show a colorful surface. Instead we show a raster image of the surface. All of the lines are very smooth, with the exception of the boundaries of the raster plot not covered by lines. We also have a reduction of file size. In this case the pdf file size reduced from 1.9mb to 78kb using the combination of the raster plot and the wire frame. It takes less time to display in the pdf viewer and the image quality is great.

Mathematica does a pretty good job at exporting 3D images to pdf files. When we import the pdf files we obtain a Graphics object composed of line segments and triangles. In some cases this objects overlap and thus we have hidden lines. To make a wire frame model with no surfaces we first need to remove this overlap and then remove the polygons. I will start by describing how to obtain the information from a Graphics3D image.


G3ToG2Info

getPoints[obj_] := Switch[Head[obj], 
   Polygon, obj[[1]], 
   JoinedCurve, obj[[2]][[1]], 
   RGBColor, {Table[obj[[i]], {i, 1, 3}]}
  ];
setPoints[obj_] := Switch[Length@obj, 
   3, Polygon[obj], 
   2, Line[obj], 
   1, RGBColor[obj[[1]]]
  ];
G3ToG2Info[g_] := Module[{obj, opt},
   obj = ImportString[ExportString[g, "PDF", Background -> None], "PDF"][[1]];
   opt = Options[obj];
   obj = Flatten[First[obj /. Style[expr_, opts___] :> {opts, expr}], 2];
   obj = Cases[obj, _Polygon | _JoinedCurve | _RGBColor, Infinity];
   obj = Map[getPoints[#] &, obj];
   {obj, opt}
  ]

This code is for Mathematica 8 in version 7 you would replace JoinedCurve in the function getPoints by Line. The function getPoints assumes that you are giving a primitive Graphics object. It will see what type of object it recieves and then extract the information it needs from it. If it is a polygon it gets a list of 3 points, for a line it obtains a list of 2 points and if it is a color then it gets a list of a single list containing 3 points. This has been done like this in order to maintain consistency with the lists.

The function setPoints does the reverse of getPoints. You input a list of points and it will determine if it should return a polygon, a line or a color.

To obtain a list of triangles, lines and colors we use G3ToG2Info. This function will use ExportString and ImportString to obtain a Graphics object from the Graphics3D version. This info is store in obj. There is some clean up that we need to perform, first we get the options of the obj. This part is necessary because it may contain the PlotRange of the image. Then we obtain all the Polygon, JoinedCurve and RGBColor objects as described in obtaining graphics primitives and directives. Finally we apply the function getPoints on all of these objects to get a list of triangles, lines and colors. This part covers the line {figInfo, opt} = G3ToG2Info[g].


getHiddenLines

We want to be able to know what part of a line will not be displayed. To do this we need to know point of intersection between two line segments. The algorithm I'm using to find the intersection can be found here.

lineInt[L_, M_, EPS_: 10^-6] := Module[
  {x21, y21, x43, y43, x13, y13, numL, numM, den},
  {x21, y21} = L[[2]] - L[[1]];
  {x43, y43} = M[[2]] - M[[1]];
  {x13, y13} = L[[1]] - M[[1]];
  den = y43*x21 - x43*y21;
  If[den*den < EPS, Return[-Infinity]];
  numL = (x43*y13 - y43*x13)/den;
  numM = (x21*y13 - y21*x13)/den;
  If[numM < 0 || numM > 1, Return[-Infinity], Return[numL]];
 ]

lineInt assumes that the line L and M do not coincide. It will return -Infinity if the lines are parallel or if the line containing the segment L does not cross the line segment M. If the line containing L intersects the line segment M then it returns a scalar. Suppose this scalar is u, then the point of intersection is L[[1]] + u (L[[2]]-L[[1]]). Notice that it is perfectly fine for u to be any real number. You can play with this manipulate function to test how lineInt works.

Manipulate[
   Grid[{{
      Graphics[{
        Line[{p1, p2}, VertexColors -> {Red, Red}],
        Line[{p3, p4}]
       },
       PlotRange -> 3, Axes -> True],
      lineInt[{p1, p2}, {p3, p4}]
     }}],
   {{p1, {-1, 1}}, Locator, Appearance -> "L1"},
   {{p2, {2, 1}}, Locator, Appearance -> "L2"},
   {{p3, {1, -1}}, Locator, Appearance -> "M1"},
   {{p4, {1, 2}}, Locator, Appearance -> "M2"}
]

Now that we know how to far we have to travel from L[[1]] to the line segment M we can find out what portion of a line segment lies within a triangle.

lineInTri[L_, T_] := Module[{res},
  If[Length@DeleteDuplicates[Flatten[{T, L}, 1], SquaredEuclideanDistance[#1, #2] < 10^-6 &] == 3, Return[{}]];
  res = Sort[Map[lineInt[L, #] &, {{T[[1]], T[[2]]}, {T[[2]], T[[3]]},  {T[[3]], T[[1]]} }]];
  If[res[[3]] == Infinity || res == {-Infinity, -Infinity, -Infinity}, Return[{}]];
  res = DeleteDuplicates[Cases[res, _Real | _Integer | _Rational], Chop[#1 - #2] == 0 &];
  If[Length@res == 1, Return[{}]];
  If[(Chop[res[[1]]] == 0 && res[[2]] > 1) || (Chop[res[[2]] - 1] == 0 && res[[1]] < 0), Return[{0, 1}]];
  If[(Chop[res[[2]]] == 0 && res[[1]] < 0) || (Chop[res[[1]] - 1] == 0 && res[[2]] > 1), Return[{}]];
  res = {Max[res[[1]], 0], Min[res[[2]], 1]};
  If[res[[1]] > 1 || res[[1]] < 0 || res[[2]] > 1 || res[[2]] < 0, Return[{}], Return[res]];
 ]

This function returns the the portion of the line L that needs to be deleted. For instance, if it returns {.5, 1} this means that you will delete 50 percent of the line, starting from half the segment to the ending point of the segment. If L = {A, B} and the function returns {u, v} then this means that the line segment {A+(B-A)u, A+(B-A)v} is the section of the line that its contained in the triangle T.

When implementing lineInTri you need to be careful that the line L is not one of the edges of T, if this is the case then the line does not lie inside the triangle. This is where rounding erros can be bad. When Mathematica exports the image sometimes a line lies on the edge of the triangle but these coordinates differ by some amount. It is up to us to decide how close the line lies on the edge, otherwise the function will see that the line lies almost completely inside the triangle. This is the reason of the first line in the function. To see if a line lies on an edge of a triangle we can list all the points of the triangle and the line, and delete all the duplicates. You need to specify what a duplicate is in this case. In the end, if we end up with a list of 3 points this means that a line lies on an edge. The next part is a little complicated. What we do is check for the intersection of the line L with each edge of the triangle T and store this the results in a list. Next we sort the list and find out what section, if any, of the line lies in the triangle. Try to make sense out of it by playing with this, some of the tests include checking if an endpoint of the line is a vertex of the triangle, if the line is completely inside the triangle, partly inside or completely outside.

Manipulate[
  Grid[{{
    Graphics[{
      RGBColor[0, .5, 0, .5], Polygon[{p3, p4, p5}],
      Line[{p1, p2}, VertexColors -> {Red, Red}]
     },
     PlotRange -> 3, Axes -> True],
    lineInTri[{p1, p2}, {p3, p4, p5}]
   }}],
 {{p1, {-1, -2}}, Locator, Appearance -> "L1"},
 {{p2, {0, 0}}, Locator, Appearance -> "L2"},
 {{p3, {-2, -2}}, Locator, Appearance -> "T1"},
 {{p4, {2, -2}}, Locator, Appearance -> "T2"},
 {{p5, {-1, 1}}, Locator, Appearance -> "T3"}
]

lineInTri will be used to see what portion of the line will not be drawn. This line will most likely be covered by many triangles. For this reason, we need to keep a list of all the portions of each line that will not be drawn. These lists will not have an order. All we know is that this lists are one dimensional segments. Each one consisting of numbers in the [0,1] interval. I'm not aware of a union function for one dimensional segments so here is my implementation.

union[obj_] := Module[{p, tmp, dummy, newp, EPS = 10^-3},
  p = Sort[obj];
  tmp = p[[1]];
  If[tmp[[1]] < EPS, tmp[[1]] = 0];
  {dummy, newp} = Reap[
    Do[
     If[(p[[i, 1]] - tmp[[2]]) > EPS && (tmp[[2]] - tmp[[1]]) > EPS, 
       Sow[tmp]; tmp = p[[i]], 
       tmp[[2]] = Max[p[[i, 2]], tmp[[2]]]
      ];
     , {i, 2, Length@p}
    ];
    If[1 - tmp[[2]] < EPS, tmp[[2]] = 1];
    If[(tmp[[2]] - tmp[[1]]) > EPS, Sow[tmp]];
   ];
  If[Length@newp == 0, {}, newp[[1]]]
 ]

This function would be shorter but here I have included some if statements to check if a number is close to zero or one. If one number is EPS apart from zero then we make this number zero, the same applies for one. Another aspect that I'm covering here is that if there is a relatively small portion of the segment to be displayed then it is most likely that it needs to be deleted. For instance if we have {{0,.5}, {.500000000001}} this means that we need to draw {{.5, .500000000001}}. But this segment is very small to be even noticed specially in a large line segment, for all we know those two numbers are the same. All of this things need to be taken into account when implementing union.

Now we are ready to see what needs to be deleted from a line segment. The next requires the list of objects generated from G3ToG2Info, an object from this list and an index.

getSections[L_, obj_, start_ ] := Module[{dummy, p, seg},
  {dummy, p} = Reap[
    Do[
     If[Length@obj[[i]] == 3,
      seg =  lineInTri[L, obj[[i]]];
      If[Length@seg != 0, Sow[seg]];
     ]
     , {i, start, Length@obj}
    ]
   ];
  If[Length@p == 0, Return[{}], Return[union[First@p]]];
 ]

getSections returns a list containing the portions that need to be deleted from L. We know that obj is the list of triangles, lines and colors, we know that objects in the list with a higher index will be drawn on top of ones with lower index. For this reason we need the index start. This is the index we will start looking for triangles in obj. Once we find a triangle we will obtain the portion of the segment that lies in the triangle using the function lineInTri. At the end we will end up with a list of sections which we can combine by using union.

Finally, we get to getHiddenLines. All this requires is to look at each object in the list returned by G3ToG2Info and apply the function getSections. getHiddenLines will return a list of lists. Each element is a list of sections that need to be deleted.

getHiddenLines[obj_] := Module[{pts},
  pts = Table[{}, {Length@obj}];
  Do[
   If[Length@obj[[j]] == 2,
      pts[[j]] = getSections[obj[[j]], obj, j + 1]
    ];
    , {j, Length@obj}
   ];
   Return[pts];
  ]

getFrame

If you have manage to understand the concepts up to here I'm sure you know what will be done next. If we have the list of triangles, lines and colors and the sections of the lines that need to be deleted we need to draw only the colors and the sections of the lines that are visible. First we make a complement function, this will tell us exactly what to draw.

complement[obj_] := Module[{dummy, p},
  {dummy, p} = Reap[
    If[obj[[1, 1]] != 0, Sow[{0, obj[[1, 1]]}]];
    Do[
     Sow[{obj[[i - 1, 2]], obj[[i, 1]]}]
     , {i, 2, Length@obj}
    ];
    If[obj[[-1, 2]] != 1, Sow[{obj[[-1, 2]], 1}]];
   ];
  If[Length@p == 0, {}, Flatten@ First@p]
 ]

Now the getFrame function

getFrame[obj_, pts_] := Module[{dummy, lines, L, u, d},
  {dummy, lines} = Reap[
    Do[
     L = obj[[i]];
     If[Length@L == 2,
      If[Length@pts[[i]] == 0, Sow[L]; Continue[]];
      u = complement[pts[[i]]];
      If[Length@u > 0, 
       Do[
        d = L[[2]] - L[[1]];
        Sow[{L[[1]] + u[[j - 1]] d, L[[1]] + u[[j]] d}]
        , {j, 2, Length@u, 2 }]
      ];
    ];
    If[Length@L == 1, Sow[L]];
    , {i, Length@obj}]
  ];
 First@lines
]

Final words

I'm somewhat happy with the results of the algorithm. What I do not like is the execution speed. I have written this as I would in C/C++/java using loops. I tried my best to use Reap and Sow to create growing lists instead of using the function Append. Regardless of all of this I still had to use loops. It should be noted that the wire frame picture posted here took 63 seconds to generate. I tried doing a wire frame for the picture in the question but this 3D object contains about 32000 objects. It was taking about 13 seconds to compute the portions that need to be displayed for a line. If we assume that we have 32000 lines and it takes 13 seconds to do all the computations that will be about 116 hours of computational time.

I'm sure this time can be reduced if we use the function Compile on all of the routines and maybe finding a way not to use the Do loops. Can I get some help here Stack Overflow?

For your convinience I have uploaded the code to the web. You can find it here. If you can apply a modified version of this code to the plot in the question and show the wire frame I will mark your solution as the answer to this post.

Best, J Manuel Lopez



回答2:

This isn't right, but somewhat interesting:

Plot3D[Sin[x + y^2], {x, -3, 3}, {y, -2, 2}, Boxed -> False, PlotStyle -> {EdgeForm[None], FaceForm[Red, None]}, Mesh -> False]

With a FaceForm of None, the polygon isn't rendered. I'm not sure there's a way to do this with the Mesh lines.