Qt Quick 3D - Volumetric Rendering Example

 // Copyright (C) 2023 The Qt Company Ltd.
 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

 #include "volumetexturedata.h"
 #include "qthread.h"
 #include <QSize>
 #include <QFile>
 #include <QElapsedTimer>

 enum ExampleId { Helix, Box, Colormap };

 // Method to convert data from T to uint8_t
 template<typename T>
 static void convertData(QByteArray &imageData, const QByteArray &imageDataSource)
 {
     Q_ASSERT(imageDataSource.size() > 0);
     constexpr auto kScale = sizeof(T) / sizeof(uint8_t);
     auto imageDataSourceData = reinterpret_cast<const T *>(imageDataSource.constData());
     qsizetype imageDataSourceSize = imageDataSource.size() / kScale;
     imageData.resize(imageDataSourceSize);
     auto imageDataPtr = reinterpret_cast<uint8_t *>(imageData.data());

     T min = std::numeric_limits<T>::max();
     T max = std::numeric_limits<T>::min();

 #pragma omp parallel for
     for (int i = 0; i < imageDataSourceSize; i++) {
         if (imageDataSourceData[i] > max) {
 #pragma omp critical
             max = qMax(max, imageDataSourceData[i]);
         }
     }

 #pragma omp parallel for
     for (int i = 0; i < imageDataSourceSize; i++) {
         if (imageDataSourceData[i] < min) {
 #pragma omp critical
             min = qMin(min, imageDataSourceData[i]);
         }
     }
     const T range = max - min;
     const double rangeInv = 255.0 / range; // use double for optimal precision

 #pragma omp parallel for
     for (int i = 0; i < imageDataSourceSize; i++) {
         imageDataPtr[i] = (imageDataSourceData[i] - min) * rangeInv;
     }
 }

 static QByteArray createBuiltinVolume(int exampleId)
 {
     constexpr int size = 256;

     QByteArray byteArray(size * size * size, 0);
     uint8_t *data = reinterpret_cast<uint8_t *>(byteArray.data());
     const auto cellIndex = [size](int x, int y, int z) {
         Q_UNUSED(size); // MSVC specific
         const int index = x + size * (z + size * y);
         Q_ASSERT(index < size * size * size && index >= 0);
         return index;
     };

     const auto createHelix = [&](float zOffset, uint8_t color) {
         //  x = radius * cos(t)
         //  y = radius * sin(t)
         //  z = climb * t
         //
         // We go through t until z is outside of box

         constexpr float radius = 70.f;
         constexpr float climb = 15.f;
         constexpr float offset = 256 / 2;
         constexpr int thick = 6; // half radius

         int i = -1;
         QVector3D lastCell = QVector3D(0, 0, 0);
         while (true) {
             i++;
             const float t = i * 0.005f;
             const int cellX = offset + radius * qCos(t);
             const int cellY = offset + radius * qSin(t);
             const int cellZ = (climb * t) - zOffset;
             if (cellZ < 0) {
                 continue;
             }
             if (cellZ > 255)
                 break;

             QVector3D originalCell(cellX, cellY, cellZ);
             if (originalCell == lastCell)
                 continue;
             lastCell = originalCell;

 #pragma omp parallel for
             for (int z = cellZ - thick; z < cellZ + thick; z++) {
                 if (z < 0 || z > 255)
                     continue;
                 for (int y = cellY - thick; y < cellY + thick; y++) {
                     if (y < 0 || y > 255)
                         continue;
                     for (int x = cellX - thick; x < cellX + thick; x++) {
                         if (x < 0 || x > 255)
                             continue;
                         QVector3D currCell(x, y, z);
                         float dist = originalCell.distanceToPoint(currCell);
                         if (dist < thick) {
                             data[cellIndex(x, y, z)] = color;
                         }
                     }
                 }
             }
         }
     };

     if (exampleId == ExampleId::Helix) {
         // Fill with weird ball and holes
         QVector3D centreCell(size / 2, size / 2, size / 2);
 #pragma omp parallel for
         for (int z = 0; z < size; z++) {
             for (int y = 0; y < size; y++) {
                 for (int x = 0; x < size; x++) {
                     const float dist = centreCell.distanceToPoint(QVector3D(x, y, z));
                     const float value = dist * 0.5f - 40.f; // Negative value means cell is inside of sphere
                     data[cellIndex(x, y, z)] = value >= 0 ? quint8(qBound(value, 0.f, 80.f)) : 80;
                 }
             }
         }
         createHelix(0, 200);
         createHelix(30, 150);
         createHelix(60, 100);

     } else if (exampleId == ExampleId::Colormap) {
 #pragma omp parallel for
         for (int z = 0; z < 256; z++) {
             for (int y = 0; y < 256; y++) {
                 for (int x = 0; x < 256; x++) {
                     data[cellIndex(x, y, z)] = x;
                 }
             }
         }
     } else if (exampleId == ExampleId::Box) {
         std::array<int, 6> colors = { 50, 100, 255, 200, 150, 10 };
         constexpr int width = 10;
 #pragma omp parallel for
         for (int i = 0; i < width; i++) {
             int x0 = i;
             int x1 = 255 - i;
             for (int z = 0; z < 256; z++) {
                 for (int y = 0; y < 256; y++) {
                     data[cellIndex(x0, y, z)] = colors[0];
                     data[cellIndex(x1, y, z)] = colors[1];
                 }
             }
         }
 #pragma omp parallel for
         for (int i = 0; i < width; i++) {
             int y0 = i;
             int y1 = 255 - i;
             for (int z = 0; z < 256; z++) {
                 for (int x = 0; x < 256; x++) {
                     data[cellIndex(x, y0, z)] = colors[2];
                     data[cellIndex(x, y1, z)] = colors[3];
                 }
             }
         }
 #pragma omp parallel for
         for (int i = 0; i < width; i++) {
             int z0 = i;
             int z1 = 255 - i;
             for (int y = 0; y < 256; y++) {
                 for (int x = 0; x < 256; x++) {
                     data[cellIndex(x, y, z0)] = colors[4];
                     data[cellIndex(x, y, z1)] = colors[5];
                 }
             }
         }
     }

     return byteArray;
 }

 static VolumeTextureData::AsyncLoaderData loadVolume(const VolumeTextureData::AsyncLoaderData &input)
 {
     QByteArray imageDataSource;

     if (input.source == QUrl("file:///default_helix")) {
         imageDataSource = createBuiltinVolume(ExampleId::Helix);
     } else if (input.source == QUrl("file:///default_box")) {
         imageDataSource = createBuiltinVolume(ExampleId::Box);
     } else if (input.source == QUrl("file:///default_colormap")) {
         imageDataSource = createBuiltinVolume(ExampleId::Colormap);
     } else {
         // NOTE: we always assume a local file is opened
         QFile file(input.source.toLocalFile());
         if (!file.open(QIODevice::ReadOnly)) {
             qWarning() << "Could not open file: " << file.fileName();
             auto result = input;
             result.success = false;
             return result;
         }

         imageDataSource = file.readAll();
         file.close();
     }

     QByteArray imageData;

     // We scale the values to uint8_t data size
     if (input.dataType == "uint8") {
         imageData = imageDataSource;
     } else if (input.dataType == "uint16") {
         convertData<uint16_t>(imageData, imageDataSource);
     } else if (input.dataType == "int16") {
         convertData<int16_t>(imageData, imageDataSource);
     } else if (input.dataType == "float32") {
         convertData<float>(imageData, imageDataSource);
     } else if (input.dataType == "float64") {
         convertData<double>(imageData, imageDataSource);
     } else {
         qWarning() << "Unknown data type, assuming uint8";
         imageData = imageDataSource;
     }

     // If our source data is smaller than expected we need to expand the texture
     // and fill with something
     qsizetype dataSize = input.depth * input.width * input.height;
     if (imageData.size() < dataSize) {
         imageData.resize(dataSize, '0');
     }

     auto result = input;
     result.volumeData = imageData;
     result.success = true;
     return result;
 }

 class Worker : public QObject
 {
     Q_OBJECT

 public slots:
     void doWork(VolumeTextureData::AsyncLoaderData data)
     {
         auto result = loadVolume(data);
         emit resultReady(result);
     }

 signals:
     void resultReady(const VolumeTextureData::AsyncLoaderData result);
 };

 ///////////////////////////////////////////////////////////////////////

 VolumeTextureData::VolumeTextureData()
 {
     // Load a volume by default so we have something to render to avoid crashes
     m_source = QUrl("file:///default_colormap");
     m_width = 256;
     m_height = 256;
     m_depth = 256;
     m_dataType = "uint8";
     auto result = loadVolume(AsyncLoaderData { m_source, m_width, m_height, m_depth, m_dataType });
     setFormat(Format::R8);
     setTextureData(result.volumeData);
     setSize(QSize(m_width, m_height));
     QQuick3DTextureData::setDepth(m_depth);
 }

 VolumeTextureData::~VolumeTextureData()
 {
     workerThread.quit();
     workerThread.wait();
 }

 QUrl VolumeTextureData::source() const
 {
     return m_source;
 }

 void VolumeTextureData::setSource(const QUrl &newSource)
 {
     if (m_source == newSource)
         return;

     m_source = newSource;
     if (!m_isLoading && !m_source.isEmpty())
         loadAsync(m_source, m_width, m_height, m_depth, m_dataType);
     emit sourceChanged();
 }

 qsizetype VolumeTextureData::width() const
 {
     return m_width;
 }

 void VolumeTextureData::setWidth(qsizetype newWidth)
 {
     if (m_width == newWidth)
         return;

     m_width = newWidth;
     updateTextureDimensions();
     emit widthChanged();
 }

 qsizetype VolumeTextureData::height() const
 {
     return m_height;
 }

 void VolumeTextureData::setHeight(qsizetype newHeight)
 {
     if (m_height == newHeight)
         return;

     m_height = newHeight;
     updateTextureDimensions();
     emit heightChanged();
 }

 qsizetype VolumeTextureData::depth() const
 {
     return m_depth;
 }

 void VolumeTextureData::setDepth(qsizetype newDepth)
 {
     if (m_depth == newDepth)
         return;

     m_depth = newDepth;
     updateTextureDimensions();
     emit depthChanged();
 }

 QString VolumeTextureData::dataType() const
 {
     return m_dataType;
 }

 void VolumeTextureData::setDataType(const QString &newDataType)
 {
     if (m_dataType == newDataType)
         return;
     m_dataType = newDataType;
     if (!m_isLoading && !m_source.isEmpty())
         loadAsync(m_source, m_width, m_height, m_depth, m_dataType);
     emit dataTypeChanged();
 }

 void VolumeTextureData::updateTextureDimensions()
 {
     if (m_width * m_height * m_depth > m_currentDataSize)
         return;

     setSize(QSize(m_width, m_height));
     QQuick3DTextureData::setDepth(m_depth);
 }

 void VolumeTextureData::loadAsync(QUrl source, qsizetype width, qsizetype height, qsizetype depth, QString dataType)
 {
     loaderData.source = source;
     loaderData.width = width;
     loaderData.height = height;
     loaderData.depth = depth;
     loaderData.dataType = dataType;

     if (m_isLoading) {
         m_isAborting = true;
         return;
     }

     m_isLoading = true;
     Q_ASSERT(!workerThread.isRunning());
     initWorker();
 }

 void VolumeTextureData::initWorker()
 {
     Worker *worker = new Worker;
     worker->moveToThread(&workerThread);
     connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater); // delete worker on thread exit
     connect(this, &VolumeTextureData::startWorker, worker, &Worker::doWork);
     connect(worker, &Worker::resultReady, this, &VolumeTextureData::handleResults);
     workerThread.start();
     emit startWorker(loaderData);
 }

 void VolumeTextureData::handleResults(AsyncLoaderData result)
 {
     Q_ASSERT(workerThread.isRunning());
     workerThread.quit();
     workerThread.wait();

     if (m_isAborting) {
         m_isAborting = false;
         initWorker();
         return;
     }

     if (!result.success) {
         emit loadFailed(result.source, result.width, result.height, result.depth, result.dataType);
     }

     m_currentDataSize = result.volumeData.size();

     setSize(QSize(m_width, m_height));
     QQuick3DTextureData::setDepth(m_depth);
     setFormat(Format::R8);
     setTextureData(result.volumeData);
     updateTextureDimensions();

     setWidth(result.width);
     setHeight(result.height);
     setDepth(result.depth);
     setDataType(result.dataType);
     setSource(result.source);

     emit loadSucceeded(result.source, result.width, result.height, result.depth, result.dataType);
     m_isLoading = false;
 }


 #include "volumetexturedata.moc"