[Volume Rendering] Incorrect Depth Occlusion Between Two Intersecting Spheres in VTK 9.1.0

[Note: English is not my first language, so please let me know if anything is unclear.]
I’m working on a scientific visualization project using VTK 9.1.0 with C++, where I need to render two intersecting, completely opaque spheres (one red, one blue). I’m facing significant challenges with achieving physically correct depth occlusion.

Current Problem:
The spheres don’t occlude each other based on their actual geometric relationships. Additionally, the occlusion pattern remains static when I rotate the camera, which breaks the 3D perception.

What Should Happen:

  • The sphere (or part of a sphere) that’s geometrically closer to the camera should appear in front
  • This occlusion relationship should update dynamically as I rotate the viewpoint

What I’ve Tried:

  1. vtkMultiBlockVolumeMapper: I combined both spheres into a multi-block dataset, but this didn’t improve depth ordering.
  2. Depth Peeling: Enabled via renderer->SetUseDepthPeeling(1), but this technique appears ineffective for volume rendering scenarios.

Minimal Code Example:

#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkColorTransferFunction.h>
#include <vtkImageData.h>
#include <vtkInteractorStyleTrackballCamera.h>
#include <vtkNew.h>
#include <vtkNamedColors.h>
#include <vtkPiecewiseFunction.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkVolume.h>
#include <vtkVolumeProperty.h>
#include <vtkOpenGLGPUVolumeRayCastMapper.h>
#include <vtkImageMathematics.h>
#include <vtkImageBlend.h>

#include <iostream>
#include <cmath>

int main(int argc, char* argv[])
{
    vtkNew<vtkNamedColors> colors;
    vtkNew<vtkRenderer> renderer;
    renderer->SetBackground(colors->GetColor3d("Black").GetData());
    vtkNew<vtkRenderWindow> renderWindow;
    renderWindow->AddRenderer(renderer);
    renderWindow->SetSize(800, 600);
    renderWindow->SetWindowName("Two Solid Interpenetrating Spheres");
    renderWindow->SetMultiSamples(8);

    vtkNew<vtkRenderWindowInteractor> renderWindowInteractor;
    renderWindowInteractor->SetRenderWindow(renderWindow);
    vtkNew<vtkInteractorStyleTrackballCamera> style;
    renderWindowInteractor->SetInteractorStyle(style);

    const double sphereRadius = 1.0;
    const int volumeSize = 128;
    const double spacing = 0.05;
    double center1[3] = { 0.0, 0.0, 0.0 };
    double center2[3] = { sphereRadius * 0.6, sphereRadius * 0.1, 0.0 };

    vtkNew<vtkImageData> sphere1Volume;
    sphere1Volume->SetDimensions(volumeSize, volumeSize, volumeSize);
    sphere1Volume->SetSpacing(spacing, spacing, spacing);
    sphere1Volume->SetOrigin(
        center1[0] - sphereRadius * 1.2,
        center1[1] - sphereRadius * 1.2,
        center1[2] - sphereRadius * 1.2
    );
    sphere1Volume->AllocateScalars(VTK_UNSIGNED_CHAR, 1);

    vtkNew<vtkImageData> sphere2Volume;
    sphere2Volume->SetDimensions(volumeSize, volumeSize, volumeSize);
    sphere2Volume->SetSpacing(spacing, spacing, spacing);
    sphere2Volume->SetOrigin(
        center2[0] - sphereRadius * 1.2, 
        center2[1] - sphereRadius * 1.2,
        center2[2] - sphereRadius * 1.2
    );
    sphere2Volume->AllocateScalars(VTK_UNSIGNED_CHAR, 1);

    unsigned char* sphere1Data = static_cast<unsigned char*>(sphere1Volume->GetScalarPointer());
    unsigned char* sphere2Data = static_cast<unsigned char*>(sphere2Volume->GetScalarPointer());
    for (int z = 0; z < volumeSize; z++) {
        double worldZ = sphere1Volume->GetOrigin()[2] + z * spacing;
        for (int y = 0; y < volumeSize; y++) {
            double worldY = sphere1Volume->GetOrigin()[1] + y * spacing;
            for (int x = 0; x < volumeSize; x++) {
                double worldX = sphere1Volume->GetOrigin()[0] + x * spacing;
                int idx = z * volumeSize * volumeSize + y * volumeSize + x;

                double dx1 = worldX - center1[0];
                double dy1 = worldY - center1[1];
                double dz1 = worldZ - center1[2];
                double dist1 = sqrt(dx1 * dx1 + dy1 * dy1 + dz1 * dz1);
                double dx2 = worldX - center2[0];
                double dy2 = worldY - center2[1];
                double dz2 = worldZ - center2[2];
                double dist2 = sqrt(dx2 * dx2 + dy2 * dy2 + dz2 * dz2);

                if (dist1 <= sphereRadius) {
                    sphere1Data[idx] = 255;
                }
                else {
                    sphere1Data[idx] = 0;
                }
                if (dist2 <= sphereRadius) {
                    sphere2Data[idx] = 255;
                }
                else {
                    sphere2Data[idx] = 0;
                }
            }
        }
    }

    vtkNew<vtkOpenGLGPUVolumeRayCastMapper> volumeMapper1;
    volumeMapper1->SetInputData(sphere1Volume);
    volumeMapper1->SetSampleDistance(0.01);
    volumeMapper1->AutoAdjustSampleDistancesOff();

    vtkNew<vtkOpenGLGPUVolumeRayCastMapper> volumeMapper2;
    volumeMapper2->SetInputData(sphere2Volume);
    volumeMapper2->SetSampleDistance(0.01);
    volumeMapper2->AutoAdjustSampleDistancesOff();

    vtkNew<vtkColorTransferFunction> colorTransferFunction1;
    colorTransferFunction1->AddRGBPoint(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
    colorTransferFunction1->AddRGBPoint(1.0, 0.7, 0.0, 0.0, 0.0, 0.0);
    colorTransferFunction1->AddRGBPoint(255.0, 1.0, 0.0, 0.0, 1.0, 1.0);
    vtkNew<vtkPiecewiseFunction> opacityTransferFunction1;
    opacityTransferFunction1->AddPoint(0.0, 0.0);
    opacityTransferFunction1->AddPoint(1.0, 0.0);
    opacityTransferFunction1->AddPoint(255.0, 1.0);

    vtkNew<vtkColorTransferFunction> colorTransferFunction2;
    colorTransferFunction2->AddRGBPoint(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
    colorTransferFunction2->AddRGBPoint(1.0, 0.0, 0.0, 0.7, 0.0, 0.0); 
    colorTransferFunction2->AddRGBPoint(255.0, 0.0, 0.0, 1.0, 1.0, 1.0);
    vtkNew<vtkPiecewiseFunction> opacityTransferFunction2;
    opacityTransferFunction2->AddPoint(0.0, 0.0);
    opacityTransferFunction2->AddPoint(1.0, 0.0);
    opacityTransferFunction2->AddPoint(255.0, 1.0);

    vtkNew<vtkVolumeProperty> volumeProperty1;
    volumeProperty1->SetColor(colorTransferFunction1);
    volumeProperty1->SetScalarOpacity(opacityTransferFunction1);
    volumeProperty1->SetInterpolationTypeToLinear();
    volumeProperty1->ShadeOn(); 
    volumeProperty1->SetAmbient(0.5);
    volumeProperty1->SetDiffuse(0.8);
    volumeProperty1->SetSpecular(0.5);
    volumeProperty1->SetSpecularPower(20.0);

    vtkNew<vtkVolumeProperty> volumeProperty2;
    volumeProperty2->SetColor(colorTransferFunction2);
    volumeProperty2->SetScalarOpacity(opacityTransferFunction2);
    volumeProperty2->SetInterpolationTypeToLinear();
    volumeProperty2->ShadeOn();
    volumeProperty2->SetAmbient(0.5);
    volumeProperty2->SetDiffuse(0.8);
    volumeProperty2->SetSpecular(0.5);
    volumeProperty2->SetSpecularPower(20.0);

    vtkNew<vtkVolume> volume1;
    volume1->SetMapper(volumeMapper1);
    volume1->SetProperty(volumeProperty1);
    vtkNew<vtkVolume> volume2;
    volume2->SetMapper(volumeMapper2);
    volume2->SetProperty(volumeProperty2);
    renderer->AddVolume(volume1);
    renderer->AddVolume(volume2);

    vtkCamera* camera = renderer->GetActiveCamera();
    camera->SetPosition(4, 3, 4);
    camera->SetFocalPoint(0.3, 0.05, 0);
    camera->SetViewUp(0, 0, 1); 
    camera->Azimuth(30); 
    camera->Elevation(20);
    renderer->ResetCamera();

    renderWindow->Render();
    renderWindowInteractor->Start();
    return 0;
}

Incorrect occlusion screenshots:
The blue sphere always occludes the red sphere, regardless of viewpoint.

Thank you for any guidance!