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

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.
© 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.