- Qt 6.7
- Qt Quick 3D
- Qt Quick 3D Examples and Tutorials
- Qt Quick 3D - Volumetric Rendering Example
- Qt Quick 3D | Commercial or GPLv3

# 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"