How to enable 3D / Volume Rendering viewport in Cornerstone3D v1 (cornerstonejs) for CT/MR data?

I am using Cornerstone3D v1 (@cornerstonejs/core, @cornerstonejs/tools) in a React application.
Currently, I have successfully implemented 2D stack and volume (MPR-style) viewports for CT/MR studies using cornerstoneStreamingImageVolumeLoader.

I can:

  • Load DICOM series from a WADO-RS endpoint
  • Create volumes using createAndCacheVolume
  • Convert stack viewports to volume viewports using convertStackToVolumeViewport
  • Scroll through slices and use tools (StackScroll, Crosshairs, ROI, etc.)

Now I want to implement true 3D construction / 3D volume rendering (VR) (e.g. shaded 3D view of CT data), but I’m facing difficulty understanding:
import * as cornerstone from ‘@cornerstonejs/core’;
import * as cornerstoneTools from ‘@cornerstonejs/tools’;
import cornerstoneDICOMImageLoader from ‘@cornerstonejs/dicom-image-loader’;
import { cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader } from ‘@cornerstonejs/streaming-image-volume-loader’;
import dicomParser from ‘dicom-parser’;
import createImageIdsAndCacheMetaData from ‘…/…/utils/createImageIdsAndCacheMetaData’;
import { CKEditor } from “@ckeditor/ckeditor5-react”;
import DecoupledEditor from “@ckeditor/ckeditor5-build-decoupled-document”;
// Toolgroup and rendering setup
const toolGroupId = “myToolGroup”;
const renderingEngineId = “myRenderingEngine”;
const viewportIds = [‘first’, ‘second’, ‘third’, ‘fourth’];
const indexMap = {first: ‘viewport1Index’}

class ClinetViwer extends Component {
constructor(props) {
super(props);
this.state = {

};

allowDrop(event) {
event.preventDefault();
}

async openImageInViewport(ID, modality, targetViewportId) {
try {
let viewport = this.renderingEngine.getViewport(targetViewportId);

  // If viewport doesn't exist, enable it first
  if (!viewport) {
    const viewportConfig = this.viewport_list[targetViewportId];
    if (viewportConfig) {
      this.renderingEngine.enableElement(viewportConfig);
      viewport = this.renderingEngine.getViewport(targetViewportId);
    } else {
      console.error(`Viewport ${targetViewportId} not found in viewport_list`);
      return;
    }
  }

  if (!viewport) {
    console.error(`Viewport ${targetViewportId} is not properly initialized`);
    return;
  }

  if (viewport.type === cornerstone.Enums.ViewportType.VOLUME) {
    viewport.setVolumes([]);
  }

  if (modality === 'CT' || modality === 'MR') {
    const volume = cornerstone.cache.getVolume(ID);
    if (!volume) {
      console.error(`Volume ${ID} not found in cache`);
      return;
    }
    
    const hasMultipleSlices = volume && volume.imageIds.length > 1;

    if (hasMultipleSlices) {
      const newViewport = await cornerstone.utilities.convertStackToVolumeViewport({
        options: { volumeId: ID, viewportId: targetViewportId, orientation: cornerstone.Enums.OrientationAxis.ACQUISITION },
        viewport: viewport,
      });

      newViewport.setProperties({ rotation: 0 });
      
      if (this.toolGroup) {
        try {
          this.toolGroup.addViewport(newViewport.id, renderingEngineId);
        } catch (e) {
          // Ignore if already exists
        }
      }
      
      newViewport.render();

      const newImageListener = () => {
        const index = newViewport.getSliceIndex() + 1;
        const indexElement = document.getElementById(indexMap[newViewport.id]);
        if (indexElement) {
          indexElement.innerHTML = 'Image: ' + index;
        }
      };
      
      newViewport.element.addEventListener(cornerstone.EVENTS.VOLUME_NEW_IMAGE, newImageListener);
      this.eventListeners.push({element: newViewport.element, type: cornerstone.EVENTS.VOLUME_NEW_IMAGE, listener: newImageListener});
    } else {
      viewport.setStack(volume.imageIds);
      viewport.render();
    }
  } else {
    if (viewport.type === cornerstone.Enums.ViewportType.STACK) {
      const imageIds = this.nonCT_ImageIds[Number(ID)];
      if (imageIds) {
        viewport.setStack(imageIds);
        viewport.render();
      } else {
        console.error(`Non-CT image IDs not found for ID: ${ID}`);
      }
    } else {
      this.renderingEngine.disableElement(targetViewportId);
      let curr = this.viewport_list[targetViewportId];
      if (curr) {
        curr.type = cornerstone.Enums.ViewportType.STACK;
        delete curr.defaultOptions;

        this.renderingEngine.enableElement(curr);
        
        const newViewport = this.renderingEngine.getViewport(targetViewportId);
        if (newViewport) {
          newViewport.setProperties({ rotation: 0 });
          
          if (this.toolGroup) {
            try {
              this.toolGroup.addViewport(newViewport.id, renderingEngineId);
            } catch (e) {
              // Ignore if already exists
            }
          }
          
          const imageIds = this.nonCT_ImageIds[Number(ID)];
          if (imageIds) {
            newViewport.setStack(imageIds);
            newViewport.render();

            const newImageListener = () => {
              const index = newViewport.getCurrentImageIdIndex() + 1;
              const indexElement = document.getElementById(indexMap[newViewport.id]);
              if (indexElement) {
                indexElement.innerHTML = 'Image: ' + index;
              }
            };
            
            newViewport.element.addEventListener(cornerstone.EVENTS.STACK_NEW_IMAGE, newImageListener);
            this.eventListeners.push({element: newViewport.element, type: cornerstone.EVENTS.STACK_NEW_IMAGE, listener: newImageListener});
          } else {
            console.error(`Non-CT image IDs not found for ID: ${ID}`);
          }
        }
      } else {
        console.error(`Viewport configuration not found for: ${targetViewportId}`);
      }
    }
  }
} catch (error) {
  console.error("Error opening image in viewport:", error);
}

}

drop(event) {
event.preventDefault();

// Get dropped item info
const obj = JSON.parse(event.dataTransfer.getData('text'));
const ID = obj[0];
const modality = obj[1];

// Get current viewport
const parentElement = event.target.parentElement;
const viewport_ID = parentElement.getAttribute('data-value');

// Force a small delay to let React finish its updates
setTimeout(() => {
  this.openImageInViewport(ID, modality, viewport_ID);
}, 10);

}

async cornerstone(PARAM) {
try {
const previewTab = document.getElementById(‘previewTab’);
if (!previewTab) {
console.error(“Preview tab element not found”);
return;
}

  const studyid = PARAM;

  // Create a loading message div
  const loadingMessage = document.createElement('div');
  loadingMessage.id = 'loadingMessage';
  loadingMessage.innerText = 'Please wait, images are loading...';
  loadingMessage.style.position = 'absolute';
  loadingMessage.style.top = '50%';
  loadingMessage.style.left = '50%';
  loadingMessage.style.transform = 'translate(-50%, -50%)';
  loadingMessage.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  loadingMessage.style.color = '#fff';
  loadingMessage.style.padding = '10px 20px';
  loadingMessage.style.borderRadius = '5px';
  loadingMessage.style.fontSize = '16px';
  loadingMessage.style.zIndex = '1000';
  loadingMessage.style.pointerEvents = 'none';
  document.body.appendChild(loadingMessage);

  // Timeout for the loading message - 1 minute
  const loadingMessageTimeout = setTimeout(() => {
    const message = document.getElementById('loadingMessage');
    if (message) {
      document.body.removeChild(message);
    }
  }, 60000);

  // Use the patient data from state instead of fetching again
  const { patientData } = this.state;
  
  if (!patientData) {
    throw new Error('Patient data not available');
  }

  const { study_instance_uid } = patientData;

  // Since we don't have series data from the new API, we'll need to adapt
  // For now, let's assume we need to fetch series data separately or modify the API
  // This part needs to be adjusted based on your actual data structure
  const response = await fetch(`${API_BASE_URL}/serverdata/`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: `studyid=${encodeURIComponent(studyid)}`
  });

  if (!response.ok) throw new Error(`Server error: ${response.status}`);

  const data = await response.json();
  this.setState({ studyData: data });

  const { study_uid, series } = data;

  let k = 0;
  let imageIdIndex = 0;
  let loadedSeriesCount = 0;

  // Handle each series asynchronously
  const imagePromises = series.map(async (item, index) => {
    const startTime = Date.now();
    
    let imageId = await createImageIdsAndCacheMetaData({
      StudyInstanceUID: study_uid,
      SeriesInstanceUID: item[0],
      wadoRsRoot: '*******',
    });

    let imageCount = 0;
    if (Array.isArray(imageId)) {
      imageCount = imageId.length;
    }

    // Create wrapper container (card for image + text)
    const cardContainer = document.createElement('div');
    cardContainer.style.display = 'flex';
    cardContainer.style.alignItems = 'center';
    cardContainer.style.border = '2px solid #ef4444';
    cardContainer.style.borderRadius = '6px';
    cardContainer.style.padding = '6px';
    cardContainer.style.marginBottom = '8px';
    cardContainer.style.backgroundColor = '#1D1D1F';
    cardContainer.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
    cardContainer.style.cursor = 'pointer';

    // Create image
    let image = document.createElement('img');
    image.src = item[3];
    image.style.height = '80px';
    image.style.width = '100px';
    image.style.borderRadius = '4px';
    image.style.border = '1px solid #444';
    image.style.marginRight = '10px';

    // Create text container (common for both CT/MR and non-CT)
    const textContainer = document.createElement('div');
    textContainer.style.flex = '1';
    textContainer.style.color = '#f1f1f1';
    textContainer.style.fontSize = '11px';
    textContainer.style.lineHeight = '1.4';
    textContainer.innerHTML = `
      <p style="margin:0;">
        <strong>${item[1]}</strong>
      </p>
      <p style="margin:0;">${item[2]}</p>
      <p style="margin:2px 0; font-size:10px; color:#bbb;">
        Image Count: <strong>${imageCount}</strong>
      </p>
    `;

    // Process based on modality type
if ((item[1] === 'CT' || item[1] === 'MR') && Array.isArray(imageId) && imageId.length > 0) {

// Patch metadata for all frames to avoid vec3 errors
imageId.forEach((id) => {
const imagePlaneMeta = cornerstone.metaData.get(‘imagePlaneModule’, id);

if (!imagePlaneMeta || !Array.isArray(imagePlaneMeta.imageOrientationPatient)) {
  cornerstone.metaData.addProvider((type, metaId) => {
    if (type !== 'imagePlaneModule' || metaId !== id) return undefined;

    return {
      ...imagePlaneMeta,
      imageOrientationPatient: imagePlaneMeta?.imageOrientationPatient || [1, 0, 0, 0, 1, 0],
      imagePositionPatient: imagePlaneMeta?.imagePositionPatient || [0, 0, 0],
      rowCosines: [1, 0, 0],
      columnCosines: [0, 1, 0],
      frameOfReferenceUID: imagePlaneMeta?.frameOfReferenceUID || '1.2.840.10008.1.2.1.999999',
      rows: imagePlaneMeta?.rows || 512,
      columns: imagePlaneMeta?.columns || 512,
      pixelSpacing: imagePlaneMeta?.pixelSpacing || [1.0, 1.0],
    };
  }, 1000);
}

});

// Assign a unique volumeId
let volumeId = ‘cornerstoneStreamingImageVolume: myVolume’ + k;
k += 1;
image.dataset.value = volumeId;

try {
// Create and cache the volume safely
let volume = await cornerstone.volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageId });
cornerstone.utilities.cacheUtils.performCacheOptimizationForVolume(volumeId);
volume.load();
} catch (err) {
console.error(‘Error creating volume for series:’, item, err);
return; // skip this series if volume creation fails
}

image.dataset.modality = item[1];
image.dataset.description = item[2];
image.draggable = true;

image.addEventListener(‘load’, () => {
loadedSeriesCount++;
});

} else {
// Non-CT/MR processing
this.nonCT_ImageIds.push(imageId);
image.dataset.value = imageIdIndex;
imageIdIndex += 1;

      image.dataset.modality = item[1];
      image.dataset.description = item[2];
      image.draggable = true;
      
      image.addEventListener('load', () => {
        loadedSeriesCount++;
      });
    }

    // Add click event listener (common for both types)
    const clickListener = (event) => {
      const ID = event.target.dataset.value;
      const modality = event.target.dataset.modality;
       const targetViewportId = this.selected_viewport;
      this.openImageInViewport(ID, modality, targetViewportId);
    };
    
    image.addEventListener('click', clickListener);
    this.eventListeners.push({element: image, type: 'click', listener: clickListener});

    // Append children to card container (ONCE for both types)
    cardContainer.appendChild(image);
    cardContainer.appendChild(textContainer);

    // Add the card to preview tab (SINGLE APPEND POINT)
    previewTab.appendChild(cardContainer);

    const endTime = Date.now();
    const elapsedTime = endTime - startTime;
    console.log(`Series ${index + 1} (modality: ${item[1]}): Load Time = ${elapsedTime} ms`);
    return item;
  });

  // Once all promises are resolved, proceed to cleanup
  Promise.all(imagePromises)
    .then(() => {
      clearTimeout(loadingMessageTimeout);
      const message = document.getElementById('loadingMessage');
      if (message) {
        document.body.removeChild(message);
      }
      const firstImage = previewTab.querySelector('img');
      if (firstImage) {
        const ID = firstImage.dataset.value;
        const modality = firstImage.dataset.modality;
        const targetViewportId = viewportIds[0];
        this.openImageInViewport(ID, modality, targetViewportId);
      }
    })
    .catch(error => {
      clearTimeout(loadingMessageTimeout);
      const message = document.getElementById('loadingMessage');
      if (message) {
        document.body.removeChild(message);
      }
      console.error('Error while loading images:', error);
    });

  const dragStartListener = (event) => {
    if (event.target.tagName === 'IMG') {
      const transferData = [event.target.dataset.value, event.target.dataset.modality, event.target.dataset.description];
      event.dataTransfer.setData("text", JSON.stringify(transferData));
    }
  };
  
  previewTab.addEventListener('dragstart', dragStartListener);
  this.eventListeners.push({element: previewTab, type: 'dragstart', listener: dragStartListener});

} catch (error) {
  console.error(error);
  const message = document.getElementById('loadingMessage');
  if (message) {
    document.body.removeChild(message);
  }
}

}

async initializeCornerstone() {
if (this.state.cornerstoneInitialized) return;

await cornerstone.init();
cornerstoneTools.init();

// Add metadata providers
cornerstone.metaData.addProvider(
  cornerstone.utilities.calibratedPixelSpacingMetadataProvider.get.bind(
    cornerstone.utilities.calibratedPixelSpacingMetadataProvider
  ),
  11000
);

cornerstone.metaData.addProvider(
  cornerstone.utilities.genericMetadataProvider.get.bind(
    cornerstone.utilities.genericMetadataProvider
  ),
  10000
);

const { preferSizeOverAccuracy, useNorm16Texture } =
  cornerstone.getConfiguration().rendering;

  window.cornerstone = cornerstone;
  window.cornerstoneTools = cornerstoneTools;
// Register volume loaders
cornerstone.volumeLoader.registerVolumeLoader(
  "cornerstoneStreamingImageVolume",
  cornerstoneStreamingImageVolumeLoader
);
cornerstone.volumeLoader.registerUnknownVolumeLoader(
  cornerstoneStreamingImageVolumeLoader
);
cornerstone.volumeLoader.registerVolumeLoader(
  "cornerstoneStreamingDynamicImageVolume",
  cornerstoneStreamingDynamicImageVolumeLoader
);

// Configure DICOM image loader
cornerstoneDICOMImageLoader.external.cornerstone = cornerstone;
cornerstoneDICOMImageLoader.external.dicomParser = dicomParser;
cornerstoneDICOMImageLoader.configure({
  useWebWorkers: true,
  decodeConfig: {
    convertFloatPixelDataToInt: false,
    use16BitDataType: preferSizeOverAccuracy || useNorm16Texture,
  },
});

let maxWebWorkers = 1;
if (navigator.hardwareConcurrency) {
  maxWebWorkers = Math.min(navigator.hardwareConcurrency, 10);
}

// Web worker settings
const config = {
  maxWebWorkers,
  startWebWorkersOnDemand: false,
  taskConfiguration: {
    decodeTask: {
      initializeCodecsOnStartup: false,
      strict: false,
    },
  },
};

cornerstoneDICOMImageLoader.webWorkerManager.initialize(config);

this.setState({ cornerstoneInitialized: true });

}

async componentDidMount() {
// Fetch patient data first
const patientData = await this.fetchPatientData();

  if (!patientData) {
    console.error("Failed to fetch patient data");
    return;
  }
  console.log("Patient Data:", patientData);
  
  await this.fetchCurrentUser();
  
  try {
  // 1️⃣ Fetch current user first
  await this.fetchCurrentUser();

  // 2️⃣ Then fetch their services
  if (this.state.currentUser?.id) {
    await this.fetchServices(this.state.currentUser.id);
  } else {
    console.error("❌ No user ID found in currentUser");
  }

  await this.initializeCornerstone();
  
  // Use study_id from patient data instead of hardcoded value
  const studyid = patientData.study_id || "3c475322-dd16d05c-ceaf7688-4d64be59-57de70f0";

  // Setting cache size
  
   cornerstone.cache.setMaxCacheSize(3 * 1024 * 1024 * 1024); // 3 GB safe for most systems
cornerstone.setUseSharedArrayBuffer(false);
  
  const elements = [
    document.getElementById('viewport1'), 
   
  ];
  
  // Create rendering engine if it doesn't exist
  if (!this.renderingEngine) {
    this.renderingEngine = new cornerstone.RenderingEngine(renderingEngineId);
  }

  // Check if tool group already exists
  this.toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId);
  if (!this.toolGroup) {
    // Create tool groups for storing all tools
    this.toolGroup = cornerstoneTools.ToolGroupManager.createToolGroup(toolGroupId);

     for (const [key, value] of Object.entries(Tools)){
            cornerstoneTools.addTool(value);
            this.toolGroup.addTool(value.toolName);
          }
    
    // Enable tools
    this.toolGroup.setToolConfiguration(cornerstoneTools.StackScrollTool.toolName, {
      bindings: [
        {
          mouseButton: cornerstoneTools.Enums.MouseBindings.Primary,
        },
      ],
    });

    // Set scroll active
    this.toolGroup.setToolActive(cornerstoneTools.StackScrollMouseWheelTool.toolName);
    this.toolGroup.setToolConfiguration(cornerstoneTools.CrosshairsTool.toolName, {
      bindings: [
        {
          mouseButton: cornerstoneTools.Enums.MouseBindings.Primary,
        },
      ],
    });
    
    this.toolGroup.setToolConfiguration(cornerstoneTools.PlanarFreehandROITool.toolName, {
      calculateStats: true
    });
    this.toolGroup.setToolConfiguration(cornerstoneTools.HeightTool.toolName, {
      calculateStats: true
    });
  }
  
  // Define 4 stack viewports with viewport id, viewport type, DOM element to be used
  const first_viewport = {
    viewportId: viewportIds[0],
    type: cornerstone.Enums.ViewportType.STACK,
    element: elements[0],
  };



  this.viewport_list = {
    first: first_viewport, 
   
  };

  // Enable first_viewport, make it the previously selected viewport, set its properties, add the toolgroup
  this.renderingEngine.enableElement(first_viewport);
  this.selected_viewport = viewportIds[0];
  this.prev_selected_element = elements[0];

  const viewport = this.renderingEngine.getViewport(this.selected_viewport);
  viewport.setProperties({rotation: 0});
  
  // Add viewport to toolgroup if not already added
  this.toolGroup.addViewport(viewportIds[0], renderingEngineId);

  // Function to cache images and metadata, create volumes if needed
  if (!this.cornerstoneProcessed) {
    this.cornerstoneProcessed = true;
    this.cornerstone(studyid);
  }

  // Event listeners for viewports
  elements.forEach((item, i) => {
    // Initial event listener for stack viewport to capture image index
    const stackNewImageListener = function() {
      let currViewport = this.renderingEngine.getViewport(viewportIds[i]);
      let index = currViewport.getCurrentImageIdIndex() + 1;

      // Update image index
      let indexElem = document.getElementById(indexMap[currViewport.id]);
      if (indexElem) {
        indexElem.innerHTML = index;
      }
    }.bind(this);
    
    item.addEventListener(cornerstone.EVENTS.STACK_NEW_IMAGE, stackNewImageListener);
    this.eventListeners.push({element: item, type: cornerstone.EVENTS.STACK_NEW_IMAGE, listener: stackNewImageListener});

    const clickListener = function() {
      // Get clicked viewport ID
      const clickedViewportId = viewportIds[i];
    
      if (this.selected_viewport !== clickedViewportId) {
        // Update selected viewport
        this.selected_viewport = clickedViewportId;
    
        // Reset border style
        if (this.prev_selected_element) {
          this.prev_selected_element.style.borderColor = 'white';
        }
        item.style.borderColor = 'red';
        this.prev_selected_element = item;
    
        // Dynamically update reference lines config
        const targetViewports = viewportIds.filter(id => id !== this.selected_viewport);
    
        this.toolGroup.setToolConfiguration(cornerstoneTools.ReferenceLinesTool.toolName, {
          sourceViewportId: this.selected_viewport,
          targetViewportIds: targetViewports,
        });
    
        // Force render update
        this.renderingEngine.render();
      }
    }.bind(this);
    
    item.addEventListener('click', clickListener);
    this.eventListeners.push({element: item, type: 'click', listener: clickListener});
  });

} catch (error) {
  console.error("Error in componentDidMount:", error);
  this.setState({ 
    error: error.message,
    loading: false 
  });
}

}

render() {
const { showDetails, status, showReportEditor, studyData, reportFrmData, splitterPosition, isResizing, loading, error, patientData } = this.state;

          <div className="viewport-div">
            <div className="viewport" id='viewport1' data-value='first' onDragOver={e => this.allowDrop(e)} onDrop={e => this.drop(e)}></div>
            
        </div>

      </div>
    </div>
  </div>
);

}
}

export default ClinetViwer;

Why don’t you migrate your app to cornerstone3D?