このページでは

Qt Quick 3D - ボリュームレンダリングの例

Qt Quick 3Dでのボリュームレンダリングの実行方法について解説します。

カラーマップとスライス制御を備えた3Dヘッドボリュームレンダリング

はじめに

この例では、「ボリューム・レイキャスティング」と呼ばれる手法を用い、カスタムシェーダーと3Dボリュームテクスチャを使用してボリュームレンダリングを行う方法を示します。 このサンプルは、生のボリュームファイルを読み込み、カラーマップ、アルファ、スライスプレーンなど、さまざまなレンダリング設定をインタラクティブに変更しながらレンダリングできるアプリケーションです。https://klacansky.com/open-scivis-datasets/にホストされているボリュームと連携し、正しい寸法とスケーリングを自動的に設定するように設計されています。

実装

このアプリケーションはQMLを使用しており、ApplicationWindow で構成されています。View3D にはボリュームが、ScrollView には設定情報が格納されています。ボリュームをレンダリングするために、View3D オブジェクト内に、中央に立方体モデルだけを配置したシーンを作成します。

Model {
    id: cubeModel
    source: "#Cube"
    visible: true
    materials: CustomMaterial {
        shadingMode: CustomMaterial.Unshaded
        vertexShader: "alpha_blending.vert"
        fragmentShader: "alpha_blending.frag"

        property TextureInput volume: TextureInput {
            texture: Texture {
                textureData: VolumeTextureData {
                    id: volumeTextureData
                    source: "file:///default_colormap"
                    dataType: dataTypeComboBox.currentText ? dataTypeComboBox.currentText : "uint8"
                    width: parseInt(dataWidth.text)
                    height: parseInt(dataHeight.text)
                    depth: parseInt(dataDepth.text)
                }
                minFilter: Texture.Nearest
                mipFilter: Texture.None
                magFilter: Texture.Nearest
                tilingModeHorizontal: Texture.ClampToEdge
                tilingModeVertical: Texture.ClampToEdge
                //tilingModeDepth: Texture.ClampToEdge // Qt 6.7
            }
        }

        property TextureInput colormap: TextureInput {
            enabled: true
            texture: Texture {
                id: colormapTexture
                tilingModeHorizontal: Texture.ClampToEdge
                source: window.getColormapSource(colormapCombo.currentIndex)
            }
        }
        property real stepLength: Math.max(0.0001, parseFloat(
                                               stepLengthText.text,
                                               1 / cubeModel.maxSide))
        property real minSide: 1 / cubeModel.minSide
        property real stepAlpha: stepAlphaSlider.value
        property bool multipliedAlpha: multipliedAlphaBox.checked

        property real tMin: tSlider.first.value
        property real tMax: tSlider.second.value
        property vector3d sliceMin: window.sliceSliderMin(
                                        xSliceSlider.value,
                                        xSliceWidthSlider.value,
                                        ySliceSlider.value,
                                        ySliceWidthSlider.value,
                                        zSliceSlider.value,
                                        zSliceWidthSlider.value)
        property vector3d sliceMax: window.sliceSliderMax(
                                        xSliceSlider.value,
                                        xSliceWidthSlider.value,
                                        ySliceSlider.value,
                                        ySliceWidthSlider.value,
                                        zSliceSlider.value,
                                        zSliceWidthSlider.value)

        sourceBlend: CustomMaterial.SrcAlpha
        destinationBlend: CustomMaterial.OneMinusSrcAlpha
    }
    property real maxSide: Math.max(parseInt(dataWidth.text),
                                    parseInt(dataHeight.text),
                                    parseInt(dataDepth.text))
    property real minSide: Math.min(parseInt(dataWidth.text),
                                    parseInt(dataHeight.text),
                                    parseInt(dataDepth.text))
    scale: Qt.vector3d(parseFloat(scaleWidth.text),
                       parseFloat(scaleHeight.text),
                       parseFloat(scaleDepth.text))

    Model {
        visible: drawBoundingBox.checked
        geometry: LineBoxGeometry {}
        materials: PrincipledMaterial {
            baseColor: "#323232"
            lighting: PrincipledMaterial.NoLighting
        }
        receivesShadows: false
        castsShadows: false
    }

    Model {
        visible: drawBoundingBox.checked
        geometry: LineBoxGeometry {}
        materials: PrincipledMaterial {
            baseColor: "#323232"
            lighting: PrincipledMaterial.NoLighting
        }
        receivesShadows: false
        castsShadows: false
        position: window.sliceBoxPosition(xSliceSlider.value,
                                   ySliceSlider.value,
                                   zSliceSlider.value,
                                   xSliceWidthSlider.value,
                                   ySliceWidthSlider.value,
                                   zSliceWidthSlider.value)
        scale: Qt.vector3d(xSliceWidthSlider.value,
                           ySliceWidthSlider.value,
                           zSliceWidthSlider.value)
    }
}

この立方体は、ボリューム用に3Dテクスチャ、カラーマップ用に画像テクスチャを使用したカスタムシェーダーを採用しています。また、転送関数やスライス面などに関する様々なプロパティも用意されています。ボリュームテクスチャのtextureDataは、「VolumeTextureData 」と呼ばれるカスタムQML型であり、volumetexturedata.cpp およびvolumetexturedata.h で定義されています。

property TextureInput volume: TextureInput {
    texture: Texture {
        textureData: VolumeTextureData {
            id: volumeTextureData
            source: "file:///default_colormap"
            dataType: dataTypeComboBox.currentText ? dataTypeComboBox.currentText : "uint8"
            width: parseInt(dataWidth.text)
            height: parseInt(dataHeight.text)
            depth: parseInt(dataDepth.text)
        }
        minFilter: Texture.Nearest
        mipFilter: Texture.None
        magFilter: Texture.Nearest
        tilingModeHorizontal: Texture.ClampToEdge
        tilingModeVertical: Texture.ClampToEdge
        //tilingModeDepth: Texture.ClampToEdge // Qt 6.7
    }
}

これには、生のボリュームファイルの解釈方法を定義するオプションsource 、dataType 、width 、height 、depth が含まれています。VolumeTextureData には、ボリュームを非同期で読み込むための関数loadAsync も含まれています。この関数は、loadSucceeded またはloadFailed シグナルのいずれかを送信します。

このキューブモデルには、LineBoxGeometry を含む2つのモデルも含まれています。これらは、ボリュームのバウンディングボックスとスライス平面を示すボックスです。

Model {
    visible: drawBoundingBox.checked
    geometry: LineBoxGeometry {}
    materials: PrincipledMaterial {
        baseColor: "#323232"
        lighting: PrincipledMaterial.NoLighting
    }
    receivesShadows: false
    castsShadows: false
}

Model {
    visible: drawBoundingBox.checked
    geometry: LineBoxGeometry {}
    materials: PrincipledMaterial {
        baseColor: "#323232"
        lighting: PrincipledMaterial.NoLighting
    }
    receivesShadows: false
    castsShadows: false
    position: window.sliceBoxPosition(xSliceSlider.value,
                               ySliceSlider.value,
                               zSliceSlider.value,
                               xSliceWidthSlider.value,
                               ySliceWidthSlider.value,
                               zSliceWidthSlider.value)
    scale: Qt.vector3d(xSliceWidthSlider.value,
                       ySliceWidthSlider.value,
                       zSliceWidthSlider.value)
}

シェーダーを見てみましょう。頂点シェーダーは非常に単純で、位置のMVP投影に加え、モデル空間におけるカメラからモデルへのレイの方向を計算します:

void MAIN()
{
    POSITION = MODELVIEWPROJECTION_MATRIX * vec4(VERTEX, 1.0);
    ray_direction_model = VERTEX - (inverse(MODEL_MATRIX) * vec4(CAMERA_POSITION, 1.0)).xyz;
}

フラグメントシェーダーは、スライス平面を考慮して、レイマーチングのレイがモデル空間のどこから始まるかを計算することから始まります。while ループはレイに沿ってステップを進め、等間隔でボクセルをサンプリングし、カラーマップ内のボクセルの値に応じた色と不透明度を追加します。

void MAIN()
{
    FRAGCOLOR = vec4(0);

    // The camera position (eye) in model space
    const vec3 ray_origin_model = (inverse(MODEL_MATRIX) * vec4(CAMERA_POSITION, 1)).xyz;

    // Get the ray intersection with the sliced box
    float t_0, t_1;
    const vec3 top_sliced = vec3(100)*sliceMax - vec3(50);
    const vec3 bottom_sliced = vec3(100)*sliceMin - vec3(50);
    if (!ray_box_intersection(ray_origin_model, ray_direction_model, bottom_sliced, top_sliced, t_0, t_1))
        return; // No ray intersection with sliced box, nothing to render

    // Get the start/end points of the ray in original box
    const vec3 top = vec3(50, 50, 50);
    const vec3 bottom = vec3(-50, -50, -50);
    const vec3 ray_start = (ray_origin_model + ray_direction_model * t_0 - bottom) / (top - bottom);
    const vec3 ray_stop =  (ray_origin_model + ray_direction_model * t_1 - bottom) / (top - bottom);

    vec3 ray = ray_stop - ray_start;
    float ray_length = length(ray);
    vec3 step_vector = stepLength * ray / ray_length;

    vec3 position = ray_start;

    // Ray march until reaching the end of the volume, or color saturation
    while (ray_length > 0) {
        ray_length -= stepLength;
        position += step_vector;

        float val = textureLod(volume, position, 0).r;
        if (val == 0 || val < tMin || val > tMax)
            continue;

        const float alpha = multipliedAlpha ? val * stepAlpha : stepAlpha;
        vec4 val_color = vec4(textureLod(colormap, vec2(val, 0.5), 0).rgb, alpha);
        // Opacity correction
        val_color.a = 1.0 - pow(max(0.0, 1.0 - val_color.a), 1.0);
        FRAGCOLOR.rgb += (1.0 - FRAGCOLOR.a) * val_color.a * val_color.rgb;
        FRAGCOLOR.a += (1.0 - FRAGCOLOR.a) * val_color.a;
        if (FRAGCOLOR.a >= 0.95)
            break;
    }
}

ビューを制御するために、ArcballControllerというカスタムアイテムを追加します。これはアークボールコントローラを実装しており、モデルを自由に回転させたり、カメラをズームしたりすることができます。

ArcballController {
    id: arcballController
    camera: cameraNode
    controlledObject: cubeModel

    function jumpToAxis(axis) {
        cameraRotation.from = arcballController.controlledObject.rotation
        cameraRotation.to = originGizmo.quaternionForAxis(
                    axis, arcballController.controlledObject.rotation)
        cameraRotation.duration = 200
        cameraRotation.start()
    }

    function jumpToRotation(qRotation) {
        cameraRotation.from = arcballController.controlledObject.rotation
        cameraRotation.to = qRotation
        cameraRotation.duration = 200
        cameraRotation.start()
    }

    QuaternionAnimation {
        id: cameraRotation
        target: arcballController.controlledObject
        property: "rotation"
        type: QuaternionAnimation.Slerp
        running: false
        loops: 1
    }
}

また、「OriginGizmo」という別のカスタムアイテムを用意しています。これは、回転したモデルの向きを表示するための小さなギズモです。

OriginGizmo {
    id: originGizmo
    anchors.top: parent.top
    anchors.right: parent.right
    anchors.margins: 10
    width: 120
    height: 120
    targetNode: cubeModel

    onAxisClicked: axis => {
                       arcballController.jumpToAxis(axis)
                   }
}

すべての設定を制御するために、左側に「ScrollView 」があり、そこには多数の UI 要素が配置されています:

ScrollView {
    id: settingsPane
    height: parent.height
    property bool hidden: false

    function toggleHide() {
        if (settingsPane.hidden) {
            settingsPaneAnimation.from = settingsPane.x
            settingsPaneAnimation.to = 0
        } else {
            settingsPaneAnimation.from = settingsPane.x
            settingsPaneAnimation.to = -settingsPane.width
        }
        settingsPane.hidden = !settingsPane.hidden
        settingsPaneAnimation.running = true
    }

    NumberAnimation on x {
        id: settingsPaneAnimation
        running: false
        from: view.width
        to: view.width
        duration: 100
    }

    Column {
        topPadding: 10
        bottomPadding: 10
        leftPadding: 20
        rightPadding: 20

        spacing: 10

        Label {
            text: qsTr("Visible value-range:")
        }

        RangeSlider {
            id: tSlider
            from: 0
            to: 1
            first.value: 0
            second.value: 1
        }

        Image {
            width: tSlider.width
            height: 20
            source: window.getColormapSource(colormapCombo.currentIndex)
        }

        Label {
            text: qsTr("Colormap:")
        }

        ComboBox {
            id: colormapCombo
            model: [qsTr("Cool Warm"), qsTr("Plasma"), qsTr("Viridis"), qsTr("Rainbow"), qsTr("Gnuplot")]
        }

        Label {
            text: qsTr("Step alpha:")
        }

        Slider {
            id: stepAlphaSlider
            from: 0
            value: 0.2
            to: 1
        }

        Grid {
            horizontalItemAlignment: Grid.AlignHCenter
            verticalItemAlignment: Grid.AlignVCenter
            spacing: 5
            Label {
                text: qsTr("Step length:")
            }

            TextField {
                id: stepLengthText
                text: "0.00391" // ~1/256
                width: 100
            }
        }

        CheckBox {
            id: multipliedAlphaBox
            text: qsTr("Multiplied alpha")
            checked: true
        }

        CheckBox {
            id: drawBoundingBox
            text: qsTr("Draw Bounding Box")
            checked: true
        }

        CheckBox {
            id: autoRotateCheckbox
            text: qsTr("Auto-rotate model")
            checked: false
        }

        // X plane
        Label {
            text: qsTr("X plane slice (position, width):")
        }

        Slider {
            id: xSliceSlider
            from: 0
            to: 1
            value: 0.5
        }

        Slider {
            id: xSliceWidthSlider
            from: 0
            value: 1
            to: 1
        }

        // Y plane
        Label {
            text: qsTr("Y plane slice (position, width):")
        }

        Slider {
            id: ySliceSlider
            from: 0
            to: 1
            value: 0.5
        }

        Slider {
            id: ySliceWidthSlider
            from: 0
            value: 1
            to: 1
        }

        // Z plane
        Label {
            text: qsTr("Z plane slice (position, width):")
        }

        Slider {
            id: zSliceSlider
            from: 0
            to: 1
            value: 0.5
        }

        Slider {
            id: zSliceWidthSlider
            from: 0
            value: 1
            to: 1
        }

        // Dimensions
        Label {
            text: qsTr("Dimensions (width, height, depth):")
        }

        Row {
            spacing: 5
            TextField {
                id: dataWidth
                text: "256"
                validator: IntValidator {
                    bottom: 1
                    top: 2048
                }
            }
            TextField {
                id: dataHeight
                text: "256"
                validator: IntValidator {
                    bottom: 1
                    top: 2048
                }
            }
            TextField {
                id: dataDepth
                text: "256"
                validator: IntValidator {
                    bottom: 1
                    top: 2048
                }
            }
        }

        Label {
            text: qsTr("Scale (x, y, z):")
        }

        Row {
            spacing: 5
            TextField {
                id: scaleWidth
                text: "1"
                validator: DoubleValidator {
                    bottom: 0.001
                    top: 1000
                    decimals: 4
                }
            }
            TextField {
                id: scaleHeight
                text: "1"
                validator: DoubleValidator {
                    bottom: 0.001
                    top: 1000
                    decimals: 4
                }
            }
            TextField {
                id: scaleDepth
                text: "1"
                validator: DoubleValidator {
                    bottom: 0.001
                    top: 1000
                    decimals: 4
                }
            }
        }

        Label {
            text: qsTr("Data type:")
        }

        ComboBox {
            id: dataTypeComboBox
            model: ["uint8", "uint16", "int16", "float32", "float64"]
        }

        Label {
            text: qsTr("Load Built-in Volume:")
        }

        Row {
            spacing: 5

            Button {
                text: qsTr("Helix")
                onClicked: {
                    volumeTextureData.loadAsync("file:///default_helix",
                                                256, 256, 256, "uint8")
                    spinner.running = true
                }
            }

            Button {
                text: qsTr("Box")
                onClicked: {
                    volumeTextureData.loadAsync("file:///default_box", 256,
                                                256, 256, "uint8")
                    spinner.running = true
                }
            }

            Button {
                text: qsTr("Colormap")
                onClicked: {
                    volumeTextureData.loadAsync("file:///default_colormap",
                                                256, 256, 256, "uint8")
                    spinner.running = true
                }
            }
        }

        Button {
            text: qsTr("Load Volume...")
            onClicked: fileDialog.open()
        }
    }
}

これらすべてのパーツが連携することで、このアプリケーションはボリュームをレンダリングし、インタラクティブに制御できるようになります。なお、このサンプルでレンダリングできるボリュームのサイズやパフォーマンスは、お使いのGPUによって制限される点にご注意ください。

サンプルプロジェクト @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.