Artifact at pole of 3D shape with makie shading

Hello everyone,

I’m plotting a 3D red blood cell (RBC) shape using Makie and encountered a shading artifact—a black patch appears at the top pole of the shape. Here’s a minimal reproducible example:

using GLMakie
GLMakie.activate!()
GLMakie.closeall()

N = 16
nlat = N + 1
nlon = 2nlat
theta = range(0, step=π/(nlat-1), length=nlat)
phi = range(0, step=2π/nlon, length=nlon+1)

Xi = Array{Float64}(undef, nlat, nlon+1, 3)

# Compute the RBC shape in spherical coordinates
alpha = 1.3858189
Xi[:,:,1] .= alpha .* (sin.(theta) .* cos.(phi)')
Xi[:,:,2] .= alpha .* (sin.(theta) .* sin.(phi)')
Xi[:,:,3] .= (0.5 * alpha) .* (0.207 .+ 2.003 .* sin.(theta).^2 .- 1.123 .* sin.(theta).^4) .* cos.(theta) .* ones(1, nlon+1)

fig = Figure(size = (1200, 800))
axis = Axis3(fig[1, 1], aspect = :data)
surface!(axis, Xi[:,:,1], Xi[:,:,2], Xi[:,:,3]; colormap=[:red, :red], transparency=true, backlight=1.0f0, shading=MultiLightShading)
wireframe!(axis, Xi[:,:,1], Xi[:,:,2], Xi[:,:,3], color=:black, linewidth=1, transparency=true)

fig

The “black patch” artifact is visible at the top pole. If I set shading=NoShading, the artifact disappears, but the figure looks too flat for my purpose.

Does anyone have suggestions for resolving this issue? I’d like to keep the shading (e.g., MultiLightShading) but have a smooth surface without the black artifact. Are there any adjustments to the mesh, lighting, or parameterization that might help?

Any suggestions or best practices for smooth, artifact-free shading at poles?

Thanks in advance for your insights!

1 Like

For N=16 the grid points are sparse and the surface is not smooth. The artifact is a visible disk, due to the large distance between the northernmost two parallels on the deformed sphere.
Take N=96 or even 100, and define X, Y, Z instead of the 3d Array, Xi, if you are using the array only for this plot.

X = @. alpha * sin(theta) * cos(phi)'
Y = @. alpha * sin(theta) * sin(phi)'
Z = @. ((0.5 * alpha) * (0.207 + 2.003 * sin(theta)^2 - 1.123 * sin(theta)^4) * cos(theta)) * ones(nlon+1)'

Thanks a lot for your suggestion.

I’ll be creating an animation of the motion of a red blood cell, so increasing (N) significantly isn’t feasible due to the computational cost. I’ve found that N = 16 provides a good balance between accuracy and efficiency for my simulations.

However, your solution seems to minimize the artifact by making it less noticeable with a denser grid, rather than fully resolving the shading issue. Ideally, I’d like to eliminate the artifact entirely, even with a smaller N. Do you have any further suggestions, perhaps related to lighting or shading parameters, that could help?

With N=16, the surface normal has a discontinuity at the disk boundary, and this particularity influences how light falls around the north pole. But maybe someone can suggest a more convenient solution than increasing the density of points in the meshgrid.

For me it seems to work to avoid the zero value for theta, I’m not sure why this breaks the normals, maybe it’s a bug in the automatic normal mesh generation, because the other pole doesn’t have the problem.

If I make that zero value a small positive value instead, I get this:

using GLMakie
GLMakie.activate!()
GLMakie.closeall()

N = 16
nlat = N + 1
nlon = 2nlat
theta = range(0.001, π, length=nlat)
phi = range(0, 2π, length=nlon+1)

Xi = Array{Float64}(undef, nlat, nlon+1, 3)

# Compute the RBC shape in spherical coordinates
alpha = 1.3858189
Xi[:,:,1] .= alpha .* (sin.(theta) .* cos.(phi)')
Xi[:,:,2] .= alpha .* (sin.(theta) .* sin.(phi)')
Xi[:,:,3] .= (0.5 * alpha) .* (0.207 .+ 2.003 .* sin.(theta).^2 .- 1.123 .* sin.(theta).^4) .* cos.(theta) .* ones(1, nlon+1)

fig = Figure(size = (1200, 800))
axis = Axis3(fig[1, 1], aspect = :data)
surface!(axis, Xi[:,:,1], Xi[:,:,2], Xi[:,:,3]; colormap=[:red, :red], transparency=true, backlight=1.0f0, shading=MultiLightShading)
wireframe!(axis, Xi[:,:,1], Xi[:,:,2], Xi[:,:,3], color=:black, linewidth=1, transparency=true)

fig

Another option would be to generate and specify the normals yourself and use a mesh instead of a surface.