Auf dieser Seite

Qt Quick 3D - Beispiel für volumetrisches Rendering

Zeigt, wie man volumetrisches Rendering in „ Qt Quick 3D “ durchführt.

3D-Volumenrendering des Kopfes mit Farbpalette und Schnittsteuerung

Einführung

Dieses Beispiel zeigt, wie volumetrisches Rendering mithilfe eines benutzerdefinierten Shaders und einer 3D-Volumen-Textur unter Verwendung einer Technik namens „Volume Ray Casting“ durchgeführt wird. Bei diesem Beispiel handelt es sich um eine Anwendung, die Roh-Volumendateien lesen und rendern kann, während verschiedene Rendering-Einstellungen wie die verwendete Farbpalette, die Alpha- und die Schnitt-Ebenen interaktiv angepasst werden können. Sie ist so konzipiert, dass sie gut mit den unter https://klacansky.com/open-scivis-datasets/ gehosteten Volumen zusammenarbeitet und die korrekten Abmessungen und Skalierungen automatisch einstellt.

Implementierung

Die Anwendung nutzt QML und ist eine „ ApplicationWindow “ mit einem „ View3D “, der das Volumen enthält, sowie einem „ ScrollView “, der die Einstellungen enthält. Um unser Volumen zu rendern, erstellen wir in unserem „ View3D “-Objekt eine Szene mit lediglich einem Würfelmodell in der Mitte.

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)
    }
}

Dieser Würfel verwendet einen benutzerdefinierten Shader mit einer 3D-Textur für das Volumen und einer Bildtextur für die Farbpalette. Außerdem gibt es verschiedene Eigenschaften für die Übertragungsfunktion, Schnittebenen usw. Die `textureData` der Volumentextur ist ein benutzerdefinierter QML-Typ namens „ VolumeTextureData “ und wird in „ volumetexturedata.cpp “ und „ volumetexturedata.h “ definiert.

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
    }
}

Er enthält die Optionen „ source “, „ dataType “, „ width “, „ height “ und „ depth “, die festlegen, wie die Rohdatei des Volumens interpretiert werden soll. „ VolumeTextureData “ enthält zudem die Funktion „ loadAsync “ zum asynchronen Laden eines Volumens. Diese sendet entweder das Signal „ loadSucceeded “ oder „ loadFailed “.

Dieses Würfelmodell enthält außerdem zwei Modelle mit einem „ LineBoxGeometry “. Dabei handelt es sich um Kästchen, die den Begrenzungsrahmen des Volumens und die Schnittebenen darstellen.

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)
}

Werfen wir einen Blick auf die Shader. Der Vertex-Shader ist sehr einfach und berechnet, abgesehen von der MVP-Projektion der Position, die Richtung des Strahls von der Kamera zum Modell im Modellraum:

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

Der Fragment-Shader berechnet zunächst, wo unser Ray-Marching-Strahl im Modellraum unter Berücksichtigung der Schnittebenen beginnt. Die „ while “-Schleife durchläuft den Strahl Schritt für Schritt, nimmt in gleichen Abständen Proben der Voxel und fügt die Farbe und Deckkraft entsprechend dem Wert des Voxels in der Farbkarte hinzu.

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;
    }
}

Zur Steuerung der Ansicht fügen wir ein benutzerdefiniertes Element namens „ArcballController“ hinzu, das einen Arcball-Controller implementiert, sodass wir das Modell frei drehen und die Kamera zoomen können.

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
    }
}

Wir haben ein weiteres benutzerdefiniertes Element namens „OriginGizmo“, ein kleines Gizmo, das die Ausrichtung des gedrehten Modells anzeigt.

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)
                   }
}

Zur Steuerung aller Einstellungen gibt es links „ ScrollView “ mit einer Reihe von UI-Elementen:

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()
        }
    }
}

Da all diese Komponenten zusammenwirken, kann die Anwendung unsere Volumina darstellen und interaktiv steuern. Beachten Sie, dass sowohl die Größe der Volumina, die in diesem Beispiel dargestellt werden können, als auch die Leistung durch Ihre jeweilige GPU begrenzt sind.

Beispielprojekt @ 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.