React app load .vti file renders a blank space

Hi there,

I’m new to vtk.js and attempting to recreate the Volume Viewer demo inside a simple React app with one of the .vti files included in the demo. I’ve included the same .vti file inside the codebase for the React app, attempted to load it, and all I have been able to manage to render is a completely blank screen.

My App.js file is below and I am using the head-binary.vti file from here. You will see that it borrows heavily from the Volume Viewer demo and the vtk.js React demo components.

The result is this beautiful blank purple scene:

Could anyone please help point me in the right direction? Thank you!

function App() {
  const vtkContainerRef = useRef(null);
  const context = useRef(null);

  function makeRequest(method, url) {
    return new Promise(function (resolve, reject) {
      var xhr = new XMLHttpRequest();
      xhr.open(method, url);
      xhr.responseType = "blob";
      xhr.onload = function () {
        if (this.status >= 200 && this.status < 300) {
          resolve(xhr.response);
        } else {
          reject({
            status: this.status,
            statusText: xhr.statusText,
          });
        }
      };
      xhr.onerror = function () {
        reject({
          status: this.status,
          statusText: xhr.statusText,
        });
      };
      xhr.send();
    });
  }

  useEffect(() => {
    if (!context.current) {
      const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
        rootContainer: vtkContainerRef.current,
      });

      const createViewer = (res) => {
        const vtiReader = vtkXMLImageDataReader.newInstance();
        const renderer = fullScreenRenderer.getRenderer();
        const renderWindow = fullScreenRenderer.getRenderWindow();
        renderWindow.getInteractor().setDesiredUpdateRate(15);

        vtiReader.parseAsArrayBuffer(res);

        const mapper = vtkVolumeMapper.newInstance();

        const actor = vtkVolume.newInstance();

        actor.setMapper(mapper);
        mapper.setInputData(source);
        renderer.addActor(actor);

        const dataArray =
          source.getPointData().getScalars() ||
          source.getPointData().getArrays()[0];
        const dataRange = dataArray.getRange();

        const lookupTable = vtkColorTransferFunction.newInstance();
        const piecewiseFunction = vtkPiecewiseFunction.newInstance();

        const sampleDistance =
          0.7 *
          Math.sqrt(
            source
              .getSpacing()
              .map((v) => v * v)
              .reduce((a, b) => a + b, 0)
          );
        mapper.setSampleDistance(sampleDistance);
        actor.getProperty().setRGBTransferFunction(0, lookupTable);
        actor.getProperty().setScalarOpacity(0, piecewiseFunction);
        actor.getProperty().setInterpolationTypeToFastLinear();
        actor.getProperty().setInterpolationTypeToLinear();

        // For better looking volume rendering
        // - distance in world coordinates a scalar opacity of 1.0
        actor
          .getProperty()
          .setScalarOpacityUnitDistance(
            0,
            vtkBoundingBox.getDiagonalLength(source.getBounds()) /
              Math.max(...source.getDimensions())
          );
        // - control how we emphasize surface boundaries
        //  => max should be around the average gradient magnitude for the
        //     volume or maybe average plus one std dev of the gradient magnitude
        //     (adjusted for spacing, this is a world coordinate gradient, not a
        //     pixel gradient)
        //  => max hack: (dataRange[1] - dataRange[0]) * 0.05
        actor.getProperty().setGradientOpacityMinimumValue(0, 0);
        actor
          .getProperty()
          .setGradientOpacityMaximumValue(
            0,
            (dataRange[1] - dataRange[0]) * 0.05
          );
        // - Use shading based on gradient
        actor.getProperty().setShade(true);
        actor.getProperty().setUseGradientOpacity(0, true);
        // - generic good default
        actor.getProperty().setGradientOpacityMinimumOpacity(0, 0.0);
        actor.getProperty().setGradientOpacityMaximumOpacity(0, 1.0);
        actor.getProperty().setAmbient(0.2);
        actor.getProperty().setDiffuse(0.7);
        actor.getProperty().setSpecular(0.3);
        actor.getProperty().setSpecularPower(8.0);

        // Control UI
        const controllerWidget = vtkVolumeController.newInstance({
          size: [400, 150],
          rescaleColorMap: true,
        });

        // setUpContent above sets the size to the container.
        // We need to set the size after that.
        // controllerWidget.setExpanded(false);

        fullScreenRenderer.setResizeCallback(({ width, height }) => {
          // 2px padding + 2x1px boder + 5px edge = 14
          if (width > 414) {
            controllerWidget.setSize(400, 150);
          } else {
            controllerWidget.setSize(width - 14, 150);
          }
          controllerWidget.render();
        });

        renderer.resetCamera();
        renderWindow.render();

        global.pipeline = {
          actor,
          renderer,
          renderWindow,
          lookupTable,
          mapper,
          source,
          piecewiseFunction,
          fullScreenRenderer,
        };

        context.current = {
          fullScreenRenderer,
          renderWindow,
          renderer,
          actor,
          mapper,
        };
      };

      makeRequest("GET", "../head-binary.vti").then((res) => {
        const reader = new FileReader();
        reader.onload = function onLoad(e) {
          createViewer(reader.result);
        };
        const file = new File([res], "head-binary.vti");
        const fileObj = { file, ext: "vti" };
        reader.readAsArrayBuffer(fileObj.file);
      });
    }

    return () => {
      if (context.current) {
        const { fullScreenRenderer, actor, mapper } = context.current;
        actor.delete();
        mapper.delete();
        fullScreenRenderer.delete();
        context.current = null;
      }
    };
  }, [vtkContainerRef]);

  return (
    <div>
      <div ref={vtkContainerRef} />
    </div>
  );
}

export default App;

UPDATE: I have found the missing piece. It turns out, you need to call setContainer() and setupContent() to add the rendering to the RenderWindow.

        // Control UI
        const controllerWidget = vtkVolumeController.newInstance({
          size: [400, 150],
          rescaleColorMap: true,
        });
        controllerWidget.setContainer(vtkContainerRef.current);
        controllerWidget.setupContent(renderWindow, actor, true);

You need to load the rendering profile that you want to use.

Hey Sebastien, thanks for the response! I omitted all of the imports in my original message for brevity (and also because it was a mess).

I think I have things sufficiently figured out now and added an update. Hopefully this helps someone else in the future.