3D Volume Plots in Julia
How to make 3D Volume Plots in Julia with Plotly.
A volume plot with volume
shows several partially transparent isosurfaces for volume rendering. The API of volume
is close to the one of isosurface
. However, whereas isosurface plots show all surfaces with the same opacity, tweaking the opacityscale
parameter of volume
results in a depth effect and better volume rendering.
Basic volume plot
In the three examples below, note that the default colormap is different whether isomin and isomax have the same sign or not.
using PlotlyJS
data = range(-8, stop=8, length=40)
X, Y, Z = mgrid(data, data, data)
values = sin.(X .* Y .* Z) ./ (X .* Y .* Z)
plot(volume(
x=X[:],
y=Y[:],
z=Z[:],
value=values[:],
isomin=0.1,
isomax=0.8,
opacity=0.1, # needs to be small to see through all surfaces
surface_count=17, # needs to be a large number for good volume rendering
))
using PlotlyJS
data = range(-1, stop=1, length=30)
X, Y, Z = mgrid(data, data, data)
values = sin.(pi .* X) .* cos.(pi .* Z) .* sin.(pi .* Y)
plot(volume(
x=X[:],
y=Y[:],
z=Z[:],
value=values[:],
isomin=-0.1,
isomax=0.8,
opacity=0.1, # needs to be small to see through all surfaces
surface_count=21, # needs to be a large number for good volume rendering
))
using PlotlyJS, ImageFiltering
# Generate nicely looking random 3D-field
l = 30
data = 1:l
X, Y, Z = mgrid(data, data, data)
vol = zeros((l, l, l))
vol[rand(1:length(vol), 15)] .= 1
vol = imfilter(vol, Kernel.gaussian((4 ,4, 4)), "symmetric")
vol ./= maximum(vol)
trace = volume(
x=X[:], y=Y[:], z=Z[:],
value=vol[:],
isomin=0.2,
isomax=0.7,
opacity=0.1,
surface_count=25,
)
layout = Layout(scene_xaxis_showticklabels=false,
scene_yaxis_showticklabels=false,
scene_zaxis_showticklabels=false)
plot(trace, layout)
Defining the opacity scale of volume plots
In order to see through the volume, the different isosurfaces need to be partially transparent. This transparency is controlled by a global parameter, opacity
, as well as an opacity scale mapping scalar values to opacity levels. The figure below shows that changing the opacity scale changes a lot the visualization, so that opacityscale
should be chosen carefully (uniform
corresponds to a uniform opacity, min
/max
maps the minimum/maximum value to a maximal opacity, and extremes
maps both the minimum and maximum values to maximal opacity, with a dip in between).
using PlotlyJS
opacity_scales = ["uniform" "extremes"; "min" "max"]
fig = make_subplots(
rows=2, cols=2,
specs=fill(Spec(kind="scene"), 2,2),
subplot_titles=opacity_scales
)
data = range(-8, stop=8, length=30)
X, Y, Z = mgrid(data, data, data)
values = sin.(X .* Y .* Z) ./ (X .* Y .* Z)
for (rc, opacicty_scale) in zip(CartesianIndices((2, 2)), opacity_scales)
row, col = rc.I
add_trace!(fig,
volume(
opacityscale=opacicty_scale,
x=X[:],
y=Y[:],
z=Z[:],
value=values[:],
isomin=0.15,
isomax=0.9,
opacity=0.1,
surface_count=15,
),
row=row, col=col
)
end
fig
Defining a custom opacity scale
It is also possible to define a custom opacity scale, mapping scalar values to relative opacity values (between 0 and 1, the maximum opacity is given by the opacity keyword). This is useful to make a range of values completely transparent, as in the example below between -0.2 and 0.2.
using PlotlyJS
data = range(-1, stop=1, length=30)
X, Y, Z = mgrid(data, data, data)
values = sin.(pi .* X) .* cos.(pi .* Z) .* sin.(pi .* Y)
plot(volume(
x=X[:],
y=Y[:],
z=Z[:],
value=values[:],
isomin=-0.5,
isomax=0.5,
opacity=0.1, # max opacity
opacityscale=[[-0.5, 1], [-0.2, 0], [0.2, 0], [0.5, 1]],
surface_count=21,
colorscale=colors.RdBu_3
))
Adding caps to a volume plot
For a clearer visualization of internal surfaces, it is possible to remove the caps (color-coded surfaces on the sides of the visualization domain). Caps are visible by default. Compare below with and without caps.
using PlotlyJS
data = range(0, stop=1, length=20)
X, Y, Z = mgrid(data, data, data)
vol = (X .- 1) .^ 2 .+ (Y .- 1) .^ 2 .+ Z .^ 2
trace = volume(
x=X[:], y=Y[:], z=Z[:],
value=vol[:],
isomin=0.2,
isomax=0.7,
opacity=0.2,
surface_count=21,
caps= attr(x_show=true, y_show=true, z_show=true, x_fill=1), # with caps (default mode)
)
# Change camera view for a better view of the sides, XZ plane
layout = Layout(scene_camera = attr(
up=attr(x=0, y=0, z=1),
center=attr(x=0, y=0, z=0),
eye=attr(x=0.1, y=2.5, z=0.1)
))
plot(trace, layout)
using PlotlyJS
data = range(0, stop=1, length=20)
X, Y, Z = mgrid(data, data, data)
vol = (X .- 1) .^ 2 .+ (Y .- 1) .^ 2 .+ Z .^ 2
trace = volume(
x=X[:], y=Y[:], z=Z[:],
value=vol[:],
isomin=0.2,
isomax=0.7,
opacity=0.2,
surface_count=21,
caps= attr(x_show=false, y_show=false, z_show=false, x_fill=1), # no caps
)
layout = Layout(scene_camera = attr(
up=attr(x=0, y=0, z=1),
center=attr(x=0, y=0, z=0),
eye=attr(x=0.1, y=2.5, z=0.1)
))
plot(trace, layout)
Adding slices to a volume plot
Slices through the volume can be added to the volume plot. In this example the isosurfaces are only partially filled so that the slice is more visible, and the caps were removed for the same purpose.
using PlotlyJS
data = range(0, stop=1, length=20)
X, Y, Z = mgrid(data, data, data)
vol = (X .- 1) .^ 2 .+ (Y .- 1) .^ 2 .+ Z .^ 2
plot(volume(
x=X[:], y=Y[:], z=Z[:],
value=vol[:],
isomin=0.2,
isomax=0.7,
opacity=0.2,
surface_count=21,
slices_z=attr(show=true, locations=[0.4]),
surface=attr(fill=0.5, pattern="odd"),
caps= attr(x_show=false, y_show=false, z_show=false), # no caps
))
Reference
See https://plotly.com/julia/reference/volume/ for more information and chart attribute options!