20

Preamble

First let me explain the why. You can skip this and go directly to the question if want to answer it right away. We had more than one discussion about how to produce nice looking 3d plots which can be exported to pdf or some other vector format. Some details why this is not really possible are explained in a post of mine in the question Exporting graphics to PDF - huge file.
To solve this issue, there is in my opinion a lot of work to do, since to make it really fancy one would have to reimplement the rendering and 2d-projection process which is done by Mathematica when it displays a Graphics3D.
A simple solution, which is maybe OK in a lot of cases, would be to separate the surface of a 3d plot from its box, labels and axes. Then one could create a high-resolution image of the surface and combine this again with the (still in vector format and infinitely high resolved) axes and labels. I showed in the linked post how to do this.
To make this working reliable one has to ensure, that it is possible to place the surface which is now a raster image at the exact same position where the original surface was before. To place the image Wolfram gave us Inset at hand which kind of does a good job, but when you look at it closely, there is some behavior I don't understand.

Question

Please consider the following two Plot3D where I tried to deny any padding, margin or whatsoever. One plot is rasterized and I fixed the ImageSize so that the outcome should be completely equal

s = {300, 300};
opts = {ImageSize -> s,
   PlotStyle -> None,
   PlotRangePadding -> None,
   ImagePadding -> None,
   ImageMargins -> None,
   Axes -> False,
   Boxed -> False};
gr1 = Plot3D[Sin[x + y^2], {x, -3, 3}, {y, -2, 2},
   PlotStyle -> None,
   Evaluate[Sequence @@ opts]
   ];
img1 =
  Rasterize[
   Plot3D[Sin[x + y^2], {x, -3, 3}, {y, -2, 2},
    PlotStyle -> Opacity[0.4],
    Evaluate[Sequence @@ opts]
    ],
   "Image",
   Background -> None,
   ImageSize -> s,
   RasterSize -> s
   ];

If you like, use Framed to put a frame around both graphics and try to see differences in placement or size. I could not find any. Creating now a combined Graphics3D with Inset where I used the AbsoluteOptions of gr1 for the graphics does show, that the inset graphics is not correctly placed. I could have used Show for the joining, the outcome would have been the same. Please note, that I initialized the variables {dx,dy,dz} to values which give a nice match on my machine. The image shows the zero position.

Manipulate[
 Graphics3D[{Inset[img1, {dx, dy, dz}], First[gr1]}, 
  AbsoluteOptions[gr1]],
 {{dx, -0.13`}, -.4, .4},
 {{dy, -0.14`}, -.4, .4},
 {{dz, -0.375`}, -.4, .4}
]

enter image description here

Can someone explain or solve this problem? Maybe someone has an idea where exactly these offsets come from.

Solution

It's worth looking at every answer below, because there are many valuable information, but the key point to the solution of my question was given by Heike who noticed, that the explicit setting of SphericalRegion influences the correct placement.

Nevertheless, working with Rasterize, ImageSize, ImageResolution, RasterSize and all the options influencing margins and paddings around graphics often leads to surprising results. So be warned.

With SphericalRegion->True in the plot commands, I could quite reliable create nice pdf's from various Graphics3D. Here the creation of a high-resolution pdf which has a filesize of 860Kb.

I have not included an image, because the png image comes not even close to how brilliant this looks in the pdf. Try it!

RasterizedGraphics3D[gr_Graphics3D, rastersize_Integer] := 
  Module[{dim = ImageDimensions[gr], factor},
   factor = rastersize/First[dim];
   Graphics3D[{Inset[
      GaussianFilter[
       Rasterize[Show[gr,
         Boxed -> False, Axes -> False, AbsoluteOptions[gr]],
        "Image", Background -> None, RasterSize -> factor*dim],
       .5 factor
       ],
      {0, 0, 0}, {Center, Center}, dim]},
    AbsoluteOptions[gr]]
   ];

grout = RasterizedGraphics3D[
  Plot3D[Im[ArcSin[(x + I y)^4]], {x, -2, 2}, {y, -2, 2}, 
   Mesh -> None, 
   PlotStyle -> 
    Directive[Yellow, Specularity[White, 20], Opacity[0.8]], 
   ExclusionsStyle -> {None, Red}, SphericalRegion -> True], 2400]
Export["tmp/tmp.pdf", grout, "AllowRasterization" -> False]

The GaussianFilter does basically antialiasing because it smooths the high-res raster-image version of the surface a bit. Thank's for all the answers and comments.

halirutan
  • 112,764
  • 7
  • 263
  • 474
  • 1
    If you use Options[gr1] instead of AbsoluteOptions[gr1] and an offset of {dx, dy, dz} == {0,0,0} the two graphs align perfectly so there seems to be a discrepancy between Options[gr] and AbsoluteOptions[gr]. – Heike Jun 06 '12 at 08:17
  • Related: http://stackoverflow.com/q/6301676/618728 – Mr.Wizard Jun 08 '12 at 02:31

3 Answers3

7

It seems that if you use Options[gr1] instead of AbsoluteOptions[gr1] and an offset of {dx, dy, dz} == {0,0,0} the two graphs align perfectly:

s = {300, 300};
opts = {ImageSize -> s, PlotStyle -> None, PlotRangePadding -> None, 
   ImagePadding -> None, ImageMargins -> None, Axes -> False, Boxed -> False};
gr1 = Plot3D[Sin[x + y^2], {x, -3, 3}, {y, -2, 2}, PlotStyle -> None, 
     Mesh -> None, Boxed -> True, ##] & @@ opts;
img1 = Rasterize[Plot3D[Sin[x + y^2], {x, -3, 3}, {y, -2, 2}, 
      PlotStyle -> Opacity[.5], ##] & @@ opts, "Image", 
   Background -> None, ImageSize -> s, RasterSize -> s];

Graphics3D[{Inset[img1, {0, 0, 0}], First[gr1]}, Options[gr1]]

Mathematica graphics

The next question is then what setting in AbsoluteOptions[gr1] causes the two graphs to misalign. After a bit of experimentation it turns out that the culprit is SphericalRegion. If this option is not set explicitly AbsoluteOptions[gr1, SphericalRegion] returns False by default but the actual setting used is the value of SphericalRegion in the $FrontEnd option Graphics3DBoxOptions. You can check this setting with

CurrentValue[$FrontEnd, {Graphics3DBoxOptions, SphericalRegion}]
Heike
  • 35,858
  • 3
  • 108
  • 157
5

The following works perfectly:

Graphics[{Inset[img1, {0, 0}], Inset[gr1, {0, 0}]}]

To see that it really works, just add Mesh -> False to the definition of img1.

Since the stated motivation is to export to PDF, it may sometimes be unnecessary to fit an Inset into a Graphics3D object (even though it was nice to see that it could be done in principle). That's why I proceeded the other way around and put the 3d graphics into a 2D object instead.

Edit As Heike points out, it's important to keep in mind that the mesh 2D lines aren't hidden when in the background. For that reason, if one wants to use my approach one would have to find another way to mask hidden lines.

Edit 2

The problem with my approach is that hidden mesh lines will be visible. The problem with the opposite approach (placing a rasterized image into the 3D scene) is exactly the opposite but no less annoying: if the surface is opaque (Opacity[1]), then mesh lines that are drawn as vector lines can end up on either side of the surface, depending on perspectve. To see this, try the following modification of Heike's (and celtschk's) solution:

s1 = 300; s2 = 300; s = {s1, s2};
opts = {SphericalRegion -> True, ImageSize -> s, PlotStyle -> None, 
   PlotRangePadding -> None, ImagePadding -> None, 
   ImageMargins -> None, Axes -> False, Boxed -> False, 
   ViewAngle -> .3};
gr1 = Show[
   Plot3D[Sin[x + y^2], {x, -3, 3}, {y, -2, 2}, PlotStyle -> None, 
      Mesh -> True, Boxed -> True, ##] & @@ opts];
img1 = Show[
   Rasterize[
    Show[Plot3D[Sin[x + y^2], {x, -3, 3}, {y, -2, 2}, Mesh -> False, 
        PlotStyle -> Opacity[1], ##] & @@ opts, 
     Graphics3D[{Red, Sphere[{0, 0, 0}, 0.1]}]], "Image", 
    Background -> None, ImageSize -> s, RasterSize -> s]];

Graphics3D[{Inset[img1, {0, 0, 0}], First[gr1]}, Options[gr1]]

wrongmesh

Therefore, using an inset in Graphics3D to achieve a surface mesh doesn't look very robust to me. Not to mention the fact that the result doesn't necessarily look any better than the rasterized version because the mesh lines are zig-zaggy, too... I keep coming back to rasterization as the best option.

Edit 3 After experimenting with this some more, I can't make Heike's approach work reliably with mesh lines on the surface - they're showing in the wrong places and hidden in other wrong places.

This means that mesh lines have to remain part of the rasterized portion of the plot if they are desired at all. What is left is the alignment of raster image and vectorized decorations such as the box and frame. My initial approach leaves all vectorized lines visible, but it has advantages too: In contrast to the other solution, my approach does not have to assume that the Graphics3D is prepared using SphericalRegion->True. I can also add 3D axes and tick marks with a non-zero ImagePadding (anything other than Automatic seems to work fine):

s = {600, 600};
opts = {ImageSize -> s, PlotStyle -> None, PlotRangePadding -> None, 
   ImagePadding -> 50, ImageMargins -> None};
gr1 = Plot3D[Sin[x + y^2], {x, -3, 3}, {y, -2, 2}, Mesh -> False, 
   Evaluate[Sequence @@ opts]];
img1 = Rasterize[
   Plot3D[Sin[x + y^2], {x, -3, 3}, {y, -2, 2}, Mesh -> True, 
    PlotStyle -> Opacity[1], BoxStyle -> Opacity[0], 
    AxesStyle -> Opacity[0], Evaluate[Sequence @@ opts]], "Image", 
   ImageSize -> s, RasterSize -> s];

Graphics[{Inset[img1, {0, 0}], Inset[gr1, {0, 0}]}, 
 PlotRange -> {{-.25, .25}, {-.25, .25}}, ImageSize -> 650]

Export["plot.pdf", %]

Here is the PDF:

PDF screenshot

Edit 4: Comparison to rasterized export

Using halirutan's example, let's see what a rasterized 3d plot looks like, using the trick I describe here:

SetOptions[Plot3D, 
  Prolog -> {{EdgeForm[], Texture[{{{0, 0, 0, 0}}}], 
     Polygon[#, VertexTextureCoordinates -> #] &[{{0, 0}, {1, 0}, {1, 
        1}}]}}];

p = Plot3D[Im[ArcSin[(x + I y)^4]], {x, -2, 2}, {y, -2, 2}, 
  Mesh -> None, 
  PlotStyle -> 
   Directive[Yellow, Specularity[White, 20], Opacity[0.8]], 
  ExclusionsStyle -> {None, Red}]

Export["plot3d.pdf", Magnify@p]

rasterized plot

The PDF is for all practical purposes (mine at least) as good as a PDF, and takes only 400 kB - half of the files size of halirutan's partially rasterized PDF with the same plot. There are no restrictions as to how the 3D plot is prepared (SphericalRegion or not, etc.), and you can even rotate the plot before exporting, e.g., by doing a copy and paste like so:

usage example

Jens
  • 97,245
  • 7
  • 213
  • 499
  • 2
    The problem with using 2D insets for both the (vectorized) axes and (rasterized) surface is that the axes will appear either completely on top or completely behind the surface and not partially in front and partially behind like in an actual 3D plot. – Heike Jun 06 '12 at 08:31
  • My conclusion from these complications is that currently I would in practice always go back to my answer in Exporting graphics to PDF - huge file and export everything as high-res bitmap. It's so much easier. – Jens Jun 06 '12 at 15:14
  • @Jens, my goal was not to put a vector-mesh over a raster surface! It was only to show the offset when joining the two plots. I just wanted to place the rasterized surface into a vector axes-box. – halirutan Jun 07 '12 at 00:51
  • @halirutan I just think that there's something fishy going on with Inset in Graphics3D. For example, I also can't seem to get the graphics to rotate interactively. I tried various things to make it work more robustly - e.g., literally replaced Inset by Text. It gets the same result as Heike, and still doesn't align properly without SphericalRegion or rotate interactively. So I don't quite trust that approach... – Jens Jun 07 '12 at 02:47
  • I cannot even select this graphic here on my Linux box, but that does not matter. I want small and excellent pdf's. If this is the price, so be it. Afaik Text and Inset is kind of the same. And I have to set SphericalRegion explicitly in the plot too, but look at my update in the question and try the code. It really looks very nice. – halirutan Jun 07 '12 at 03:31
  • Given that Inset inserts a 2-dimensional image, it's no surprise that the resulting graphics cannot be rotated any more (neither interactively, nor with Show[graphics, Viewpoint->{x,y,z}] or other options). Neither can the completely rasterized graphics, of course. Note that this also explains the grid lines on your first image: You've placed a 2D image facing to the viewer in the middle of the plot. All the grid lines in front of the 2D image are visible, all the grid line behind are not. Thus the grid lines end at the intersection of the graph with the image plane. – celtschk Jun 07 '12 at 12:07
  • @celtschk Thanks, I've edited my interpretation. There is still something wrong, though. See my last comment above regarding Text. Usually Text in 3D doesn't prevent rotation or selection. – Jens Jun 07 '12 at 15:10
  • @celtschk, whether or not a 2d image inset into a 3d graphics can be rotated does depend on how they implemented it. I could easily put the image as texture on a polygon and include it in the 3d graphics. This could create the same outcome but would be rotatable, etc. – halirutan Jun 07 '12 at 15:17
  • @celtschk Of course. I'm aware of all these things. If there were no alternative to the Inset approach, I'd want to push ahead with it, too. But rasterization of the whole plot is a perfectly good alternative, so I'll sit this one out. – Jens Jun 07 '12 at 15:31
  • Ah, so you say you cannot rotate the graph at all (that is, not even the non-inset part rotates)? – celtschk Jun 07 '12 at 15:36
  • @celtschk I think sometimes I was able to rotate the non-inset part, sometimes it didn't work (with the 3D inset approach when I tested yesterday). As halirutan also mentioned above, you can't even select the 3d graphics by clicking on it. – Jens Jun 07 '12 at 15:43
  • In my experience rasterized plots tend to look very bad if the resolution doesn't exactly fit the final resolution. Maybe it's caused by bad resize algorithms in the display program, but in the end it doesn't matter why it is so, only that it is so. Moreover, with rasterizing you lose the ability to zoom in without loss of quality. – celtschk Jun 07 '12 at 15:44
  • @celtschk See my edit 4 above to show you that rasterization works very well. – Jens Jun 07 '12 at 15:57
  • Depends on what you consider "working very well". For me, the numbers on the axes look quite blurred. YMMV. – celtschk Jun 07 '12 at 16:36
  • @celtschk What PDF viewer are you using? It doesn't look blurred at all to me. Of course it's blurry in the screenshot I posted, but that's obviously an artifact. In the end, it all boils down to choosing a high enough resolution (or magnification) - so this blurriness can't be considered a real issue. – Jens Jun 07 '12 at 18:42
5

The reason for this misplacement is that due to the perspective, the center of the surface is not at the center of the image. You can see that quite nicely if you manipulate the image to have an X cross, and the 3D graph to have a sphere at the origin (I've also slightly darkened the image to make it more obvious):

s1 = 300;s2 = 300;s = {s1, s2};
opts = {ImageSize->s, PlotStyle->None, PlotRangePadding->None, ImagePadding->None,
        ImageMargins->None, Axes->False, Boxed->False};
gr1 = Show[Plot3D[Sin[x+y^2], {x, -3, 3}, {y, -2, 2},
                  PlotStyle->None, Mesh->None, Boxed->True, ##]&@@opts,
           Graphics3D[{Green,Sphere[{0,0,0},0.1]}]];
img1 = Show[ImageAdd[#, 0.2]&@
              Rasterize[Show[Plot3D[Sin[x+y^2], {x, -3, 3}, {y, -2, 2},
                                    PlotStyle->Opacity[.5], ##]&@@opts,
                             Graphics3D[{Red, Sphere[{0,0,0},0.1]}]],
                        "Image", Background->None, ImageSize->s, RasterSize->s],
            Graphics[{Red, Line[{{0, 0}, {s1, s2}}], Line[{{0, s1}, {s2, 0}}]}]];

Graphics3D[{Inset[img1,{0,0,0}],First[gr1]},Options[gr1]]

The center of the red X cross (which also is the center of the image) is at the green sphere (marking the {0,0,0} of the actual plot) but the red sphere (the place in the 2D image the {0,0,0} coordinates ended up to be shown) is not.

However, Heike's observation that it is related to SphericalRegion is true, because if you add ShphericalRegion->True to opts, the plots sit on top of each other perfectly. Note that this is not because of some mismatch (giving SphericalRegion->False explicitly does not give matching graphs), but because SphericalRegion->True actually puts the center of the 3D image in the center of the 2D image (which makes sense because a sphere has the same distance to the border in all directions). However note that {0,0,0} for the inset coordinates will only work if {0,0,0} is really at the center (use {x,-3,1} instead of {x,-3,3} for the plot to see the effect). Scaled@{0.5,0.5,0.5} should always work, however.

celtschk
  • 19,133
  • 1
  • 51
  • 106