vtkImclicitModeler in conjunction with vtkPiecewiseFunction --> vtkOpenGLVolumeOpacityTable Error on Apple M1 Chip

First of all thanks to VTK team for the great library!

BACKGROUND
Our research group is developing surgical simulators, where with one of these we use a simple vtk-pipeline to generate “x-ray-like” images from STL-files. Currently I am working hard to provide a native software for Apple M1-chips. All things are working like a charm, BUT the pipeline for generating x-ray images fails.

PROBLEM
Compiling an an Intel-Based MacBook-Pro everything works as expected.
Compiling the same code (see below) on a MacStudio with the M1-Max chipset shows a box around the object. The corresponding message is

WARN| vtkOpenGLVolumeOpacityTable (0x600002070360): This OpenGL implementation does not support the required texture size of 65536, falling back to maximum allowed, 16384.This may cause an incorrect lookup table mapping

which indicates, that something goes wrong with the vtkPiecewiseFunction and its OpenGL Implementation which controls the volume opacity.

REMARKS
I was using VTK-9.2.2 compiled natively on both systems as listed below. When running the test program, which was compiled on the Intel-based system over the Rosetta2 framework on the Apple-M1-Chip system, everything works as expected. So the bad results show only on the Apple-M1-Chip system compiled there natively (Xcode Framework 13).

SYSTEMS

The used systems are:

ARM64:

Modellname:	Mac Studio
  Modell-Identifizierung:	Mac13,1
  Chip:	Apple M1 Max
  Gesamtanzahl der Kerne:	10 (8 Leistung und 2 Effizienz)
  Speicher:	32 GB

Apple M1 Max:

  Chipsatz-Modell:	Apple M1 Max
  Typ:	GPU
  Bus:	Integriert
  Gesamtanzahl der Kerne:	24
  Hersteller:	Apple (0x106b)
  Metal-Familie:	Unterstützt, Metal GPUFamily Apple 7


INTEL64:

Modellname:	MacBook Pro
  Modell-Identifizierung:	MacBookPro16,2
  Prozessortyp:	Quad-Core Intel Core i7
  Prozessorgeschwindigkeit:	2,3 GHz
  Anzahl der Prozessoren:	1
  Gesamtanzahl der Kerne:	4
  L2-Cache (pro Kern):	512 KB
  L3-Cache:	8 MB
  Hyper-Threading Technologie:	Aktiviert
  Speicher:	16 GB

Intel Iris Plus Graphics:

  Chipsatz-Modell:	Intel Iris Plus Graphics
  Typ:	GPU
  Bus:	Integriert
  VRAM (dynamisch, maximal):	1536 MB
  Hersteller:	Intel
  Geräte-ID:	0x8a53
  Versions-ID:	0x0007
  Metal-Familie:	Unterstützt, Metal GPUFamily macOS 2

CODE

For the pipeline the test program is as follows (File TestPipeline.cxx):

#include <vtkImplicitModeller.h>
#include <vtkNamedColors.h>
#include <vtkNew.h>
#include <vtkSmartVolumeMapper.h>
#include <vtkVolume.h>
#include <vtkVolumeProperty.h>
#include <vtkPiecewiseFunction.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
#include <vtkSphereSource.h>
#include <vtkCleanPolyData.h>
#include <vtkImageShiftScale.h>
#include <vtkImageData.h>
#include <vtkAppendPolyData.h>
#include <vtkImageGaussianSmooth.h>
#include <vtkSphereSource.h>
#include <vtksys/SystemTools.hxx>

int main(int argc, char* argv[])
{
  vtkNew<vtkNamedColors> colors;

  // sphere
  vtkNew<vtkSphereSource> sphereSource;
  sphereSource->SetCenter(0.0, 0.0, 0.0);
  sphereSource->SetRadius(5.0);

  // Make the surface smooth.
  sphereSource->SetPhiResolution(100);
  sphereSource->SetThetaResolution(100);

  // append filter
  vtkSmartPointer<vtkAppendPolyData> appendFilter = vtkSmartPointer<vtkAppendPolyData>::New();
  appendFilter->SetInputConnection(sphereSource->GetOutputPort());

  // clean filter
  vtkSmartPointer<vtkCleanPolyData> cleanFilter = vtkSmartPointer<vtkCleanPolyData>::New();
  cleanFilter->SetInputConnection(appendFilter->GetOutputPort());
  cleanFilter->Update();

  // implicit modeler
  vtkNew<vtkImplicitModeller> implicitModeller;
  implicitModeller->SetSampleDimensions(80, 80, 80);
  implicitModeller->SetInputData(cleanFilter->GetOutput());
  implicitModeller->AdjustBoundsOn();
  implicitModeller->SetAdjustDistance(.1); // Adjust by 10%
  implicitModeller->SetMaximumDistance(.01);
  implicitModeller->Update();

  // sfift and scale
  vtkSmartPointer<vtkImageShiftScale> ImageCast = vtkSmartPointer<vtkImageShiftScale>::New();
  ImageCast->SetInputData(implicitModeller->GetOutput());
  ImageCast->SetShift(1);
  ImageCast->SetScale(127);
  ImageCast->SetOutputScalarTypeToUnsignedShort();
  ImageCast->Update();

  // gaussian smooth
  vtkSmartPointer<vtkImageGaussianSmooth> gaussianSmoothFilter =
    vtkSmartPointer<vtkImageGaussianSmooth>::New();
  gaussianSmoothFilter->SetInputConnection(ImageCast->GetOutputPort());
  gaussianSmoothFilter->SetStandardDeviations(1, 1, 1);
  gaussianSmoothFilter->Update();

  // mapper
  vtkNew<vtkSmartVolumeMapper> volumeMapper;
  volumeMapper->SetRequestedRenderModeToRayCast();
  volumeMapper->SetRequestedRenderModeToGPU();
  volumeMapper->SetInterpolationModeToLinear();
  volumeMapper->SetBlendModeToComposite();
  volumeMapper->SetInputData(gaussianSmoothFilter->GetOutput());

  vtkNew<vtkVolume> volume;
  // Opacity transfer function
  vtkSmartPointer<vtkPiecewiseFunction> opacityTransferFunction =
      vtkSmartPointer<vtkPiecewiseFunction>::New();
  opacityTransferFunction->AddPoint(0, 0.0);
  opacityTransferFunction->AddPoint(1, 0.1);

  // Volume property
  vtkSmartPointer<vtkVolumeProperty> volumeProperty = vtkSmartPointer<vtkVolumeProperty>::New();
  volumeProperty->SetScalarOpacity(opacityTransferFunction);
  volumeProperty->ShadeOn();
  volumeProperty->SetInterpolationTypeToLinear();
  volume->SetMapper(volumeMapper);
  volume->SetProperty(volumeProperty);

  // Renderer, Window and Interactor
  vtkNew<vtkRenderer> renderer;
  vtkNew<vtkRenderWindow> renderWindow;
  renderWindow->AddRenderer(renderer);
  vtkNew<vtkRenderWindowInteractor> interactor;
  interactor->SetRenderWindow(renderWindow);

  // add volume
  renderer->AddVolume(volume);
  renderer->SetBackground(colors->GetColor3d("Wheat").GetData());

  renderWindow->SetSize(640, 480);
  renderWindow->SetWindowName("Test Pipeline");
  renderWindow->Render();
  interactor->Start();

  return EXIT_SUCCESS;
}

The corresponding CMake file (CMakeLists.txt) is:

cmake_minimum_required(VERSION 3.12 FATAL_ERROR)

project(TestPipeline)

find_package(VTK COMPONENTS 
  CommonColor
  CommonCore
  CommonDataModel
  FiltersCore
  FiltersHybrid
  FiltersSources
  IOGeometry
  IOLegacy
  IOPLY
  IOXML
  ImagingCore
  ImagingGeneral
  ImagingHybrid
  ImagingMath
  InteractionStyle
  RenderingContextOpenGL2
  RenderingCore
  RenderingFreeType
  RenderingGL2PSOpenGL2
  RenderingOpenGL2
  RenderingVolumeOpenGL2
)

if (NOT VTK_FOUND)
  message(FATAL_ERROR "TestPipeline: Unable to find the VTK build folder.")
endif()

# Prevent a "command line is too long" failure in Windows.
set(CMAKE_NINJA_FORCE_RESPONSE_FILE "ON" CACHE BOOL "Force Ninja to use response files.")
add_executable(TestPipeline MACOSX_BUNDLE TestPipeline.cxx )
  target_link_libraries(TestPipeline PRIVATE ${VTK_LIBRARIES}
)
# vtk_module_autoinit is needed
vtk_module_autoinit(
  TARGETS TestPipeline
  MODULES ${VTK_LIBRARIES}
)

Did you try (as a temporary workaround or just to better understand what is happening) to use a smaller image to provide as an input ?

I am thinking of that because the error message be indicates a texture size issue in the apple OpenGL implementation (they seem to not be able to handle more than 16KB size textures).

WARN| vtkOpenGLVolumeOpacityTable (0x600002070360): This OpenGL implementation does not support the required texture size of 65536, falling back to maximum allowed, 16384.This may cause an incorrect lookup table mapping

Thanks for your reply!

I changed the sample dimension for the implicit modeler as follows:

 implicitModeller->SetSampleDimensions(10, 10, 10);

The warning disappeared, however the pipeline does not work as expected (see first image with box). The same code compiled on an intel based Mac provided a correct result (see second image).

As already mentioned in my initial post, the pipline also works as expected, when running the program compiled on an intel based mac over the Rosetta2 framework on the M1-Mac.

I assume the problem arises from the native compilation of vtk (9.2.2) on an M1-Mac (arm64).

I found the solution. It turns out, that ClampOverFlowOn() must be set in vtkImageShiftScale to obtain the same results on different platforms/compilers.

The modified/extended code is as follows:

<#include <vtkImplicitModeller.h>
#include <vtkNamedColors.h>
#include <vtkNew.h>
#include <vtkSmartVolumeMapper.h>
#include <vtkVolume.h>
#include <vtkVolumeProperty.h>
#include <vtkPiecewiseFunction.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkOpenGLRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkOpenGLRenderer.h>
#include <vtkSmartPointer.h>
#include <vtkSphereSource.h>
#include <vtkCleanPolyData.h>
#include <vtkImageShiftScale.h>
#include <vtkImageData.h>
#include <vtkAppendPolyData.h>
#include <vtkImageGaussianSmooth.h>
#include <vtkSphereSource.h>
#include <vtksys/SystemTools.hxx>
#include <iostream>
#include <vtkOpenGLState.h>
#include <vtkPointData.h>
#include <vtkColorTransferFunction.h>
#include <vtkPoints.h>
#include <vtkDoubleArray.h>
#include <vtkPlanes.h>
#include <vtkClipVolume.h>
#include <vtkDataSetMapper.h>
#include <vtkActor.h>
#include <vtkScalarBarActor.h>
#include <vtkActor2D.h>

int main(int argc, char* argv[])
{
  vtkNew<vtkNamedColors> colors;

  // sphere
  vtkNew<vtkSphereSource> sphereSource;
  sphereSource->SetCenter(0, 0, 0);
  sphereSource->SetRadius(0.5);

  // Make the surface smooth.
  sphereSource->SetPhiResolution(100);
  sphere<Source->SetThetaResolution(100);

  // append filter
  vtkSmartPointer<vtkAppendPolyData> appendFilter = vtkSmartPointer<vtkAppendPolyData>::New();
  appendFilter->SetInputConnection(sphereSource->GetOutputPort());

  // clean filter
  vtkSmartPointer<vtkCleanPolyData> cleanFilter = vtkSmartPointer<vtkCleanPolyData>::New();
  cleanFilter->SetInputConnection(appendFilter->GetOutputPort());
  cleanFilter->Update();

  // implicit modeler
  double maxDistanceRatio = 0.05;
  int sampleSize = 127;
  vtkNew<vtkImplicitModeller> implicitModeller;
  implicitModeller->SetSampleDimensions(sampleSize, sampleSize, sampleSize);
  implicitModeller->SetInputData(appendFilter->GetOutput());
  implicitModeller->AdjustBoundsOn();
  implicitModeller->SetAdjustDistance(.1); // Adjust by 10%
  implicitModeller->SetMaximumDistance(maxDistanceRatio);
  implicitModeller->Update();
 
  // get the bounds and compute diagonal
  double bounds[6];
  appendFilter->GetInput()->GetCellsBounds(bounds);

  std::cout << "bounds:" << bounds[0] << " " << bounds[1] << std::endl
                         << bounds[2] << " " << bounds[3] << std::endl
                         << bounds[4] << " " << bounds[5] << std::endl;

  double diagonal = sqrt(pow(bounds[1]-bounds[0],2)+
                     pow(bounds[3]-bounds[2],2)+
                     pow(bounds[5]-bounds[4],2));

  std::cout << "diagonal:" << diagonal <<  std::endl;

  // compute shift and scale for image cast in order to keep values in 
  // the range [0, 255] - unsigned char
  double shift = 0;
  double scale = 127.0 / (diagonal>1E-6?diagonal*maxDistanceRatio:1.0);
  std::cout << "shift:" << shift << " scale:" << scale << std::endl;

  // sfift and scale
  vtkSmartPointer<vtkImageShiftScale> ImageCast = vtkSmartPointer<vtkImageShiftScale>::New();
  ImageCast->SetInputData(implicitModeller->GetOutput());
  ImageCast->SetShift(shift);
  ImageCast->SetScale(scale);
  // Enable clamping seems to important to achieve same results on
  // different platforms (macOS Intel/M1, Windows, ...)
  ImageCast->ClampOverflowOn(); // IMPORTANT
  // use unsigned char as data type for output
  ImageCast->SetOutputScalarTypeToUnsignedShort();
  ImageCast->Update();

  // gaussian smooth
  vtkSmartPointer<vtkImageGaussianSmooth> gaussianSmoothFilter =
    vtkSmartPointer<vtkImageGaussianSmooth>::New();
  gaussianSmoothFilter->SetInputConnection(ImageCast->GetOutputPort());
  gaussianSmoothFilter->SetStandardDeviations(1, 1, 1);
  gaussianSmoothFilter->Update();

  // mapper
  vtkNew<vtkSmartVolumeMapper> volumeMapper;
  volumeMapper->SetRequestedRenderModeToRayCast();
  volumeMapper->SetRequestedRenderModeToGPU();
  volumeMapper->SetInterpolationModeToLinear();
  volumeMapper->SetBlendModeToComposite();
  volumeMapper->SetInputData(gaussianSmoothFilter->GetOutput());

  vtkNew<vtkVolume> volume;
  // Volume property
  vtkSmartPointer<vtkVolumeProperty> volumeProperty = vtkSmartPointer<vtkVolumeProperty>::New();
  volumeProperty->ShadeOn();
  volumeProperty->SetInterpolationTypeToLinear();

  // Opacity transfer function
  double opacity = 0.95;
  double edgeValue = 255;
  vtkSmartPointer<vtkPiecewiseFunction> opacityTransferFunction =
      vtkSmartPointer<vtkPiecewiseFunction>::New();
  opacityTransferFunction->AddPoint(0, opacity);
  opacityTransferFunction->AddPoint(edgeValue, opacity);
  opacityTransferFunction->AddPoint(edgeValue+1, 0);
  opacityTransferFunction->AddPoint(edgeValue+2, 0);
  volumeProperty->SetScalarOpacity(opacityTransferFunction);
  
  // set mapper and volume property
  volume->SetMapper(volumeMapper);
  volume->SetProperty(volumeProperty);

  // clip the sampled region
  vtkNew<vtkDoubleArray> normals;
  vtkNew<vtkPoints> clipPts;
  normals->SetNumberOfComponents(3);
  double xnorm[3] = {-1., 0., 0.};
  double xpt[3] = {0, 0., 0.};
  normals->InsertNextTuple(xnorm);
  clipPts->InsertNextPoint(xpt);
  vtkNew<vtkPlanes> clipPlanes;
  clipPlanes->SetNormals(normals);
  clipPlanes->SetPoints(clipPts);

  // clip the region
  vtkNew<vtkClipVolume> clipper;
  clipper->SetClipFunction(clipPlanes);
  clipper->SetInputData(ImageCast->GetOutput());

  // Image mapper for data from Implicit Modeler (over imagecast)
  vtkNew<vtkDataSetMapper> imageMapper;
  imageMapper->SetScalarRange(0,255);
  imageMapper->CreateDefaultLookupTable();
  vtkNew<vtkActor> imageActor;
  imageActor->SetMapper(imageMapper);
  
  // Create a scalar bar
  vtkNew<vtkScalarBarActor> scalarBar;
  scalarBar->SetLookupTable(imageMapper->GetLookupTable());
  scalarBar->SetTitle("Values");
  scalarBar->UnconstrainedFontSizeOn();
  scalarBar->SetNumberOfLabels(5);
  scalarBar->SetMaximumWidthInPixels(640 / 8);
  scalarBar->SetMaximumHeightInPixels(640 / 3);


  // Renderer, Window and Interactor
  vtkNew<vtkRenderer> renderer;
  vtkNew<vtkRenderWindow> renderWindow;
  renderWindow->AddRenderer(renderer);
  vtkNew<vtkRenderWindowInteractor> interactor;
  interactor->SetRenderWindow(renderWindow);

  // add volume
  renderer->AddVolume(volume);
  renderer->AddViewProp(imageActor);
  renderer->AddActor2D(scalarBar);
  imageMapper->SetInputConnection(clipper->GetOutputPort());

  // renderer background
  renderer->SetBackground(colors->GetColor3d("Wheat").GetData());

  // render window
  renderWindow->SetSize(640, 480);
  renderWindow->SetWindowName("Test Pipeline");
  renderWindow->Render();

  interactor->Start();

  return EXIT_SUCCESS;
}
1 Like