Qt Quick 3D - 体积渲染示例
演示如何在 Qt Quick 3D 中进行体积渲染。
简介
本示例演示如何使用自定义着色器和三维体积纹理以及名为“体积光投射”(Volume ray casting)的技术进行体积渲染。这个示例是一个应用程序,它可以读取原始体积文件并在渲染的同时能够交互式地修改各种渲染设置,如色图、alpha 值和切片平面等。它旨在与位于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: 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: sliceSliderMin( xSliceSlider.value, xSliceWidthSlider.value, ySliceSlider.value, ySliceWidthSlider.value, zSliceSlider.value, zSliceWidthSlider.value) property vector3d sliceMax: 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: DefaultMaterial { diffuseColor: "#323232" lighting: DefaultMaterial.NoLighting } receivesShadows: false castsShadows: false } Model { visible: drawBoundingBox.checked geometry: LineBoxGeometry {} materials: DefaultMaterial { diffuseColor: "#323232" lighting: DefaultMaterial.NoLighting } receivesShadows: false castsShadows: false position: sliceBoxPosition(xSliceSlider.value, ySliceSlider.value, zSliceSlider.value, xSliceWidthSlider.value, ySliceWidthSlider.value, zSliceWidthSlider.value) scale: Qt.vector3d(xSliceWidthSlider.value, ySliceWidthSlider.value, zSliceWidthSlider.value) } }
这个立方体使用一个自定义着色器和一个用于体积的三维纹理以及一个用于色图的图像纹理。还有许多用于转换函数、切片平面等的属性。体积纹理的纹理数据是一个名为 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
的模型。这些方块显示了体积的边界框和切片平面。
Model { visible: drawBoundingBox.checked geometry: LineBoxGeometry {} materials: DefaultMaterial { diffuseColor: "#323232" lighting: DefaultMaterial.NoLighting } receivesShadows: false castsShadows: false } Model { visible: drawBoundingBox.checked geometry: LineBoxGeometry {} materials: DefaultMaterial { diffuseColor: "#323232" lighting: DefaultMaterial.NoLighting } receivesShadows: false castsShadows: false position: 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; } }
为了控制体积模型,我们添加了一个自定义的Item,称为ArcballController,它实现了一个弧球控制器,这样我们就可以自由地旋转模型。当我们在模型上点击并移动鼠标时,DragHandler将向ArcballController发送命令。The WheelHandler向相机添加缩放功能。
ArcballController { id: arcballController 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 = 100 cameraRotation.start() } QuaternionAnimation { id: cameraRotation target: arcballController.controlledObject property: "rotation" type: QuaternionAnimation.Slerp running: false loops: 1 } } DragHandler { id: dragHandler target: null acceptedModifiers: Qt.NoModifier onCentroidChanged: { arcballController.mouseMoved(toNDC(centroid.position.x, centroid.position.y)) } onActiveChanged: { if (active) { view.forceActiveFocus() arcballController.mousePressed(toNDC(centroid.position.x, centroid.position.y)) } else arcballController.mouseReleased(toNDC(centroid.position.x, centroid.position.y)) } function toNDC(x, y) { return Qt.vector2d((2.0 * x / width) - 1.0, 1.0 - (2.0 * y / height)) } } WheelHandler { id: wheelHandler orientation: Qt.Vertical target: null acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad onWheel: event => { let delta = -event.angleDelta.y * 0.01 cameraNode.z += cameraNode.z * 0.1 * delta } }
我们还有一个名为OriginGizmo的自定义Item,它是一个用来显示旋转后模型方向的简单Gizmo。
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: width to: 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: 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() } } }
当所有这些部分协同工作时,应用程序能够渲染并对我们的体积进行交互式控制。请注意,此示例可以渲染的体积大小以及性能将受您的显卡限制。
文件
- volumeraycaster/ArcballController.qml
- volumeraycaster/CMakeLists.txt
- volumeraycaster/Main.qml
- volumeraycaster/OriginGizmo.qml
- volumeraycaster/Spinner.qml
- volumeraycaster/alpha_blending.frag
- volumeraycaster/alpha_blending.vert
- volumeraycaster/lineboxgeometry.cpp
- volumeraycaster/lineboxgeometry.h
- volumeraycaster/main.cpp
- volumeraycaster/qmldir
- volumeraycaster/volumeraycaster.pro
- volumeraycaster/volumetexturedata.cpp
- volumeraycaster/volumetexturedata.h
图片
- volumeraycaster/images/circle.png
- volumeraycaster/images/colormap-coolwarm.png
- volumeraycaster/images/colormap-gist_rainbow.png
- volumeraycaster/images/colormap-gnuplot.png
- volumeraycaster/images/colormap-plasma.png
- volumeraycaster/images/colormap-rainbow.png
- volumeraycaster/images/colormap-viridis.png
© 2024 The Qt Company Ltd。此处包含的文档贡献分别是各自所有者的版权。此处提供的文档是根据免费软件基金会发布的GNU自由文档许可协议(版本1.3)条款许可的。Qt和相应的标志是芬兰和/或其他国家的The Qt Company Ltd.的商标。所有其他商标均为各自所有者的财产。