mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-08-06 10:18:42 +02:00
[ui] Viewer3D: add "Sync with Image Selection" camera mode
* Inspector * changed "SETTINGS" to "DISPLAY" * new "CAMERA" section: activate camera synchronization + control image overlay * ImageOverlay: new component to display (undistorted) image on top of the 3D view * ViewpointCamera: new component that sets up a Camera based on a Viewpoint internal parameter * Viewer3D: additional ViewpointCamera to perform synchronization with image selection * Viewer3DSettings: new properties related to camera synchronization mode
This commit is contained in:
parent
774675bd65
commit
8dce4fc72f
5 changed files with 280 additions and 35 deletions
99
meshroom/ui/qml/Viewer3D/ImageOverlay.qml
Normal file
99
meshroom/ui/qml/Viewer3D/ImageOverlay.qml
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ImageOverlay enables to display a Viewpoint image on top of a 3D View.
|
||||||
|
* It takes the principal point correction into account and handle image ratio to
|
||||||
|
* correclty fit or crop according to original image ratio and parent Item ratio.
|
||||||
|
*/
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/// The url of the image to display
|
||||||
|
property alias source: image.source
|
||||||
|
/// Source image ratio
|
||||||
|
property real imageRatio: 1.0
|
||||||
|
/// Principal Point correction as UV coordinates offset
|
||||||
|
property alias uvCenterOffset: shader.uvCenterOffset
|
||||||
|
/// Whether to display the frame around the image
|
||||||
|
property bool showFrame
|
||||||
|
/// Opacity of the image
|
||||||
|
property alias imageOpacity: shader.opacity
|
||||||
|
|
||||||
|
implicitWidth: 300
|
||||||
|
implicitHeight: 300
|
||||||
|
|
||||||
|
// Display frame
|
||||||
|
RowLayout {
|
||||||
|
id: frameBG
|
||||||
|
spacing: 1
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.showFrame && image.status === Image.Ready
|
||||||
|
Rectangle {
|
||||||
|
color: "black"
|
||||||
|
opacity: 0.5
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: image.paintedHeight
|
||||||
|
Layout.preferredWidth: image.paintedWidth
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
color: "black"
|
||||||
|
opacity: 0.5
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: image
|
||||||
|
asynchronous: true
|
||||||
|
smooth: false
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: false
|
||||||
|
// Preserver aspect fit while display ratio is aligned with image ratio, crop otherwise
|
||||||
|
fillMode: width/height >= imageRatio ? Image.PreserveAspectFit : Image.PreserveAspectCrop
|
||||||
|
autoTransform: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Custom shader for displaying undistorted images
|
||||||
|
// with principal point correction
|
||||||
|
ShaderEffect {
|
||||||
|
id: shader
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: image.status === Image.Ready
|
||||||
|
width: image.paintedWidth
|
||||||
|
height: image.paintedHeight
|
||||||
|
property variant src: image
|
||||||
|
property variant uvCenterOffset
|
||||||
|
|
||||||
|
vertexShader: "
|
||||||
|
#version 330 core
|
||||||
|
uniform highp mat4 qt_Matrix;
|
||||||
|
attribute highp vec4 qt_Vertex;
|
||||||
|
attribute highp vec2 qt_MultiTexCoord0;
|
||||||
|
out highp vec2 coord;
|
||||||
|
void main() {
|
||||||
|
coord = qt_MultiTexCoord0;
|
||||||
|
gl_Position = qt_Matrix * qt_Vertex;
|
||||||
|
}"
|
||||||
|
fragmentShader: "
|
||||||
|
#version 330 core
|
||||||
|
in highp vec2 coord;
|
||||||
|
uniform sampler2D src;
|
||||||
|
uniform lowp vec2 uvCenterOffset;
|
||||||
|
uniform lowp float qt_Opacity;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() {
|
||||||
|
vec2 xy = coord + uvCenterOffset;
|
||||||
|
fragColor = texture2D(src, xy);
|
||||||
|
fragColor.rgb *= qt_Opacity;
|
||||||
|
fragColor.a = qt_Opacity;
|
||||||
|
// remove undistortion black pixels
|
||||||
|
fragColor.a *= step(0.001, fragColor.r + fragColor.g + fragColor.b);
|
||||||
|
}"
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,15 +32,37 @@ FloatingPane {
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
title: "SETTINGS"
|
title: "DISPLAY"
|
||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
columns: 2
|
columns: 2
|
||||||
columnSpacing: 6
|
columnSpacing: 6
|
||||||
rowSpacing: 3
|
rowSpacing: 3
|
||||||
|
Flow {
|
||||||
MaterialLabel { font.family: MaterialIcons.fontFamily; text: MaterialIcons.grain; padding: 2 }
|
Layout.columnSpan: 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 1
|
||||||
|
MaterialToolButton {
|
||||||
|
text: MaterialIcons.grid_on
|
||||||
|
ToolTip.text: "Display Grid"
|
||||||
|
checked: Viewer3DSettings.displayGrid
|
||||||
|
onClicked: Viewer3DSettings.displayGrid = !Viewer3DSettings.displayGrid
|
||||||
|
}
|
||||||
|
MaterialToolButton {
|
||||||
|
text: MaterialIcons.adjust
|
||||||
|
checked: Viewer3DSettings.displayGizmo
|
||||||
|
ToolTip.text: "Display Trackball"
|
||||||
|
onClicked: Viewer3DSettings.displayGizmo = !Viewer3DSettings.displayGizmo
|
||||||
|
}
|
||||||
|
MaterialToolButton {
|
||||||
|
text: MaterialIcons.call_merge
|
||||||
|
ToolTip.text: "Display Origin"
|
||||||
|
checked: Viewer3DSettings.displayOrigin
|
||||||
|
onClicked: Viewer3DSettings.displayOrigin = !Viewer3DSettings.displayOrigin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MaterialLabel { text: MaterialIcons.grain; padding: 2 }
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Slider {
|
Slider {
|
||||||
Layout.fillWidth: true; from: 0; to: 5; stepSize: 0.1
|
Layout.fillWidth: true; from: 0; to: 5; stepSize: 0.1
|
||||||
|
@ -60,7 +82,7 @@ FloatingPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
MaterialLabel { font.family: MaterialIcons.fontFamily; text: MaterialIcons.videocam; padding: 2 }
|
MaterialLabel { text: MaterialIcons.videocam; padding: 2 }
|
||||||
Slider {
|
Slider {
|
||||||
value: Viewer3DSettings.cameraScale
|
value: Viewer3DSettings.cameraScale
|
||||||
from: 0
|
from: 0
|
||||||
|
@ -73,27 +95,60 @@ FloatingPane {
|
||||||
ToolTip.visible: hovered || pressed
|
ToolTip.visible: hovered || pressed
|
||||||
ToolTip.delay: 150
|
ToolTip.delay: 150
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Group {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
title: "CAMERA"
|
||||||
|
ColumnLayout {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
// Image/Camera synchronization
|
||||||
Flow {
|
Flow {
|
||||||
Layout.columnSpan: 2
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 2
|
spacing: 2
|
||||||
CheckBox {
|
// Synchronization
|
||||||
text: "Grid"
|
MaterialToolButton {
|
||||||
padding: 2
|
id: syncViewpointCamera
|
||||||
checked: Viewer3DSettings.displayGrid
|
enabled: _reconstruction.sfmReport
|
||||||
onClicked: Viewer3DSettings.displayGrid = !Viewer3DSettings.displayGrid
|
text: MaterialIcons.linked_camera
|
||||||
|
ToolTip.text: "Sync with Image Selection"
|
||||||
|
checked: enabled && Viewer3DSettings.syncViewpointCamera
|
||||||
|
onClicked: Viewer3DSettings.syncViewpointCamera = !Viewer3DSettings.syncViewpointCamera
|
||||||
}
|
}
|
||||||
CheckBox {
|
// Image Overlay controls
|
||||||
text: "Gizmo"
|
RowLayout {
|
||||||
padding: 2
|
visible: syncViewpointCamera.enabled && Viewer3DSettings.syncViewpointCamera
|
||||||
checked: Viewer3DSettings.displayGizmo
|
spacing: 2
|
||||||
onClicked: Viewer3DSettings.displayGizmo = !Viewer3DSettings.displayGizmo
|
// Activation
|
||||||
|
MaterialToolButton {
|
||||||
|
text: MaterialIcons.image
|
||||||
|
ToolTip.text: "Image Overlay"
|
||||||
|
checked: Viewer3DSettings.viewpointImageOverlay
|
||||||
|
onClicked: Viewer3DSettings.viewpointImageOverlay = !Viewer3DSettings.viewpointImageOverlay
|
||||||
|
}
|
||||||
|
// Opacity
|
||||||
|
Slider {
|
||||||
|
visible: Viewer3DSettings.showViewpointImageOverlay
|
||||||
|
implicitWidth: 60
|
||||||
|
from: 0
|
||||||
|
to: 100
|
||||||
|
value: Viewer3DSettings.viewpointImageOverlayOpacity * 100
|
||||||
|
onValueChanged: Viewer3DSettings.viewpointImageOverlayOpacity = value / 100
|
||||||
|
ToolTip.text: "Image Opacity: " + Viewer3DSettings.viewpointImageOverlayOpacity.toFixed(2)
|
||||||
|
ToolTip.visible: hovered || pressed
|
||||||
|
ToolTip.delay: 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CheckBox {
|
// Image Frame control
|
||||||
text: "Origin"
|
MaterialToolButton {
|
||||||
padding: 2
|
visible: syncViewpointCamera.enabled && Viewer3DSettings.showViewpointImageOverlay
|
||||||
checked: Viewer3DSettings.displayOrigin
|
enabled: Viewer3DSettings.syncViewpointCamera
|
||||||
onClicked: Viewer3DSettings.displayOrigin = !Viewer3DSettings.displayOrigin
|
text: MaterialIcons.crop_free
|
||||||
|
ToolTip.text: "Frame Overlay"
|
||||||
|
checked: Viewer3DSettings.viewpointImageFrame
|
||||||
|
onClicked: Viewer3DSettings.viewpointImageFrame = !Viewer3DSettings.viewpointImageFrame
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,7 +169,6 @@ FloatingPane {
|
||||||
implicitHeight: parent.height
|
implicitHeight: parent.height
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: true
|
checked: true
|
||||||
padding: 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
|
@ -300,21 +354,13 @@ FloatingPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media unavailability indicator
|
|
||||||
MaterialToolButton {
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
enabled: false
|
|
||||||
visible: !model.valid
|
|
||||||
text: MaterialIcons.no_sim
|
|
||||||
font.pointSize: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove media from library button
|
// Remove media from library button
|
||||||
MaterialToolButton {
|
MaterialToolButton {
|
||||||
id: removeButton
|
id: removeButton
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
visible: !loading
|
visible: hovered || mouseArea.containsMouse && !loading
|
||||||
text: MaterialIcons.clear
|
text: MaterialIcons.clear
|
||||||
font.pointSize: 10
|
font.pointSize: 10
|
||||||
ToolTip.text: "Remove"
|
ToolTip.text: "Remove"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import QtQml.Models 2.2
|
||||||
import QtQuick.Scene3D 2.0
|
import QtQuick.Scene3D 2.0
|
||||||
import Qt3D.Core 2.1
|
import Qt3D.Core 2.1
|
||||||
import Qt3D.Render 2.1
|
import Qt3D.Render 2.1
|
||||||
import Qt3D.Extras 2.1
|
import Qt3D.Extras 2.10
|
||||||
import Qt3D.Input 2.1 as Qt3DInput // to avoid clash with Controls2 Action
|
import Qt3D.Input 2.1 as Qt3DInput // to avoid clash with Controls2 Action
|
||||||
|
|
||||||
import MaterialIcons 2.2
|
import MaterialIcons 2.2
|
||||||
|
@ -25,6 +25,9 @@ FocusScope {
|
||||||
readonly property vector3d defaultCamUpVector: Qt.vector3d(0.0, 1.0, 0.0)
|
readonly property vector3d defaultCamUpVector: Qt.vector3d(0.0, 1.0, 0.0)
|
||||||
readonly property vector3d defaultCamViewCenter: Qt.vector3d(0.0, 0.0, 0.0)
|
readonly property vector3d defaultCamViewCenter: Qt.vector3d(0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
readonly property var viewpoint: _reconstruction.selectedViewpoint
|
||||||
|
readonly property bool doSyncViewpointCamera: Viewer3DSettings.syncViewpointCamera && (viewpoint && viewpoint.isReconstructed)
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
function resetCameraPosition() {
|
function resetCameraPosition() {
|
||||||
mainCamera.position = defaultCamPosition;
|
mainCamera.position = defaultCamPosition;
|
||||||
|
@ -88,6 +91,7 @@ FocusScope {
|
||||||
Camera {
|
Camera {
|
||||||
id: mainCamera
|
id: mainCamera
|
||||||
projectionType: CameraLens.PerspectiveProjection
|
projectionType: CameraLens.PerspectiveProjection
|
||||||
|
enabled: cameraSelector.camera == mainCamera
|
||||||
fieldOfView: 45
|
fieldOfView: 45
|
||||||
nearPlane : 0.01
|
nearPlane : 0.01
|
||||||
farPlane : 10000.0
|
farPlane : 10000.0
|
||||||
|
@ -113,10 +117,17 @@ FocusScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ViewpointCamera {
|
||||||
|
id: viewpointCamera
|
||||||
|
enabled: cameraSelector.camera === camera
|
||||||
|
viewpoint: root.viewpoint
|
||||||
|
camera.aspectRatio: width/height
|
||||||
|
}
|
||||||
|
|
||||||
TrackballGizmo {
|
TrackballGizmo {
|
||||||
beamRadius: 4.0/root.height
|
beamRadius: 4.0/root.height
|
||||||
alpha: cameraController.moving ? 1.0 : 0.7
|
alpha: cameraController.moving ? 1.0 : 0.7
|
||||||
enabled: Viewer3DSettings.displayGizmo
|
enabled: Viewer3DSettings.displayGizmo && cameraSelector.camera == mainCamera
|
||||||
xColor: Colors.red
|
xColor: Colors.red
|
||||||
yColor: Colors.green
|
yColor: Colors.green
|
||||||
zColor: Colors.blue
|
zColor: Colors.blue
|
||||||
|
@ -129,6 +140,8 @@ FocusScope {
|
||||||
|
|
||||||
DefaultCameraController {
|
DefaultCameraController {
|
||||||
id: cameraController
|
id: cameraController
|
||||||
|
enabled: cameraSelector.camera == mainCamera
|
||||||
|
|
||||||
windowSize {
|
windowSize {
|
||||||
width: root.width
|
width: root.width
|
||||||
height: root.height
|
height: root.height
|
||||||
|
@ -178,7 +191,7 @@ FocusScope {
|
||||||
normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
|
normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
|
||||||
CameraSelector {
|
CameraSelector {
|
||||||
id: cameraSelector
|
id: cameraSelector
|
||||||
camera: mainCamera
|
camera: doSyncViewpointCamera ? viewpointCamera.camera : mainCamera
|
||||||
FrustumCulling {
|
FrustumCulling {
|
||||||
ClearBuffers {
|
ClearBuffers {
|
||||||
clearColor: "transparent"
|
clearColor: "transparent"
|
||||||
|
@ -228,6 +241,24 @@ FocusScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image overlay when navigating reconstructed cameras
|
||||||
|
Loader {
|
||||||
|
id: imageOverlayLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
active: doSyncViewpointCamera
|
||||||
|
visible: Viewer3DSettings.showViewpointImageOverlay
|
||||||
|
|
||||||
|
sourceComponent: ImageOverlay {
|
||||||
|
id: imageOverlay
|
||||||
|
source: root.viewpoint.undistortedImageSource
|
||||||
|
imageRatio: root.viewpoint.orientedImageSize.width / root.viewpoint.orientedImageSize.height
|
||||||
|
uvCenterOffset: root.viewpoint.uvCenterOffset
|
||||||
|
showFrame: Viewer3DSettings.showViewpointImageFrame
|
||||||
|
imageOpacity: Viewer3DSettings.viewpointImageOverlayOpacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Media loading overlay
|
// Media loading overlay
|
||||||
// (Scene3D is frozen while a media is being loaded)
|
// (Scene3D is frozen while a media is being loaded)
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
|
@ -24,7 +24,7 @@ Item {
|
||||||
// Available render modes
|
// Available render modes
|
||||||
readonly property var renderModes: [ // Can't use ListModel because of MaterialIcons expressions
|
readonly property var renderModes: [ // Can't use ListModel because of MaterialIcons expressions
|
||||||
{"name": "Solid", "icon": MaterialIcons.crop_din },
|
{"name": "Solid", "icon": MaterialIcons.crop_din },
|
||||||
{"name": "Wireframe", "icon": MaterialIcons.grid_on },
|
{"name": "Wireframe", "icon": MaterialIcons.details },
|
||||||
{"name": "Textured", "icon": MaterialIcons.texture },
|
{"name": "Textured", "icon": MaterialIcons.texture },
|
||||||
]
|
]
|
||||||
// Current render mode
|
// Current render mode
|
||||||
|
@ -39,4 +39,11 @@ Item {
|
||||||
property bool displayGrid: true
|
property bool displayGrid: true
|
||||||
property bool displayGizmo: true
|
property bool displayGizmo: true
|
||||||
property bool displayOrigin: false
|
property bool displayOrigin: false
|
||||||
|
// Camera
|
||||||
|
property bool syncViewpointCamera: false
|
||||||
|
property bool viewpointImageOverlay: true
|
||||||
|
property real viewpointImageOverlayOpacity: 0.5
|
||||||
|
readonly property bool showViewpointImageOverlay: syncViewpointCamera && viewpointImageOverlay
|
||||||
|
property bool viewpointImageFrame: false
|
||||||
|
readonly property bool showViewpointImageFrame: syncViewpointCamera && viewpointImageFrame
|
||||||
}
|
}
|
||||||
|
|
62
meshroom/ui/qml/Viewer3D/ViewpointCamera.qml
Normal file
62
meshroom/ui/qml/Viewer3D/ViewpointCamera.qml
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import QtQuick 2.12
|
||||||
|
import Qt3D.Core 2.12
|
||||||
|
import Qt3D.Render 2.12
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewpointCamera sets up a Camera to match a Viewpoint's internal parameters.
|
||||||
|
*/
|
||||||
|
Entity {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property variant viewpoint
|
||||||
|
|
||||||
|
property Camera camera: Camera {
|
||||||
|
|
||||||
|
nearPlane : 0.1
|
||||||
|
farPlane : 10000.0
|
||||||
|
viewCenter: Qt.vector3d(0.0, 0.0, -1.0)
|
||||||
|
|
||||||
|
// Scene light, attached to the camera
|
||||||
|
Entity {
|
||||||
|
components: [
|
||||||
|
PointLight {
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
components: [
|
||||||
|
Transform {
|
||||||
|
id: transform
|
||||||
|
|
||||||
|
Behavior on rotation {
|
||||||
|
PropertyAnimation { duration: 200}
|
||||||
|
}
|
||||||
|
Behavior on translation {
|
||||||
|
Vector3dAnimation { duration: 200}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
StateGroup {
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "valid"
|
||||||
|
when: root.viewpoint !== null
|
||||||
|
PropertyChanges {
|
||||||
|
target: camera
|
||||||
|
fieldOfView: root.viewpoint.fieldOfView
|
||||||
|
upVector: root.viewpoint.upVector
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: transform
|
||||||
|
rotation: root.viewpoint.rotation
|
||||||
|
translation: root.viewpoint.translation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue