Creating a rasterized 3D surface plots inside vectorized figure

I need to create a multi-panel figure which consists of 3D surface plots and normal 2D plots. In order to reduce file size, I want to rasterize the 3D surface plots, while keeping everything else (axes, texts, line plots, markers, etc) vectorized.

I managed to achieve this in matplotlib using ax.plot_surf(...,rasterized=True) and fig.savefig('test.pdf', dpi=500). The resulting plot is pretty good (left plot in attached figure), except the lighting is a little bit dull (I did try adjusting the lighting angle, view angle, etc).

I then tried MATLAB, and its gives really nice 3D rendering (right plot in attached figure) even without further customization. However, I’m struggling to rasterize only the surface plot. It’s either bitmap or vector figures for the whole plot. Someone told me to try exporting the surface plot alone and then import it back to the vector graphics, but I have very limited success.

Since I use Julia to generate the data anyway, so I’m wondering if Plots or Makie.jl can gives figures that meet my requirement. Basically, I want to the surface plot to look like the one in MATLAB (with the specular reflection), but with the option to rasterize only the surface plots, and the customizability of matplotlib (personally I prefer using matplotlib to MATLAB for making figure). I had a quick look at Makie.jl, but looks like I can either make vector graphics with CairoMakie which has limited support for 3D or GLMakie which doesn’t export vectorized figure as .pdf. So before I invest more time in checking out Makie.jl, I would like to ask if I’m looking for is achievable in Makie.jl or if it is actually impossible to make something that look better than one in matplotlib.

Yes this can be done with Makie, even with CairoMakie if your 3D scene is not too complex and can be drawn with simple z-sorting and layering.

I’ve tried to make an example sort of similar to what you showed here, but it’s not quite perfect yet. I would have liked to move the light source, and that’s supposed to be possible, but I’m not sure how to do it. Maybe @sdanisch or @ffreyer know. Just setting lightposition = Vec3f(.. on the surface call didn’t do anything.

Also the shading was wrong until I switched X and Y, and this must be because the normals were facing inwards first. Again I’m not sure how to control that better than trying out until it works :wink:

Here’s the example and a screenshot of the resulting pdf:

using CairoMakie

f = Figure()

r = range(0, 1.25, 100)
p = range(0, 2*pi, 100)
R = r' .* ones(length(r))
P = ones(length(p))' .* p
Z = ((R.^2 .- 1.5) .^ 2)

X = R .* cos.(P)
Y = R .* sin.(P)

ax3 = Axis3(f[1, 1], azimuth = 4.005530633326986 - pi)
surface!(ax3, Y, X, Z,
    shininess = 100f0,
    specular = Vec3f(0.8), colormap = :lightrainbow,
    rasterize = 3
)

scatter(f[1, 2], randn(100, 2))
lines(f[2, 1:2], cumsum(randn(100)))

save("test.pdf", f)

2 Likes

Lightposition is now a scene attribute, and Axis3 doesn’t forward anything to the scene… There are two ways:

set_theme!(lightposition=Vec3f(19, -0.0, 10.0))

and:

display(f)
ax3.scene.lights[1].position[] = Vec3f(19, -0.0, 10.0)

I’ve to investigate why one has to display the scene first, seems like the lightposition gets overwritten somewhere by the theme…

What’s the unit for Vec3f()? Are (10,0,10) the coordinates of the light source, the angles or something else? I tried adjusting those numbers and the lighting direction did change, but I’m struggling to figure the correlation between the two.

I think coordinates, yes.