mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-06-05 04:12:15 +02:00
[ui] Viewer3D: multi 3D media support
* use new media loading backend through MediaLibrary * Inspector3D: new overlay UI that displays and allows to manipulate MediaLibrary content
This commit is contained in:
parent
e35076ef97
commit
97fcdf67bf
6 changed files with 380 additions and 401 deletions
9
meshroom/ui/qml/Utils/format.js
Normal file
9
meshroom/ui/qml/Utils/format.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.pragma library
|
||||||
|
|
||||||
|
|
||||||
|
function intToString(v) {
|
||||||
|
// use EN locale to get comma separated thousands
|
||||||
|
// + remove automatically added trailing decimals
|
||||||
|
// (this 'toLocaleString' does not take any option)
|
||||||
|
return v.toLocaleString(Qt.locale('en-US')).split('.')[0]
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ module Utils
|
||||||
|
|
||||||
SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml
|
SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml
|
||||||
Request 1.0 request.js
|
Request 1.0 request.js
|
||||||
|
Format 1.0 format.js
|
||||||
# causes random crash at application exit
|
# causes random crash at application exit
|
||||||
# singleton Filepath 1.0 Filepath.qml
|
# singleton Filepath 1.0 Filepath.qml
|
||||||
# singleton Scene3DHelper 1.0 Scene3DHelper.qml
|
# singleton Scene3DHelper 1.0 Scene3DHelper.qml
|
||||||
|
|
268
meshroom/ui/qml/Viewer3D/Inspector3D.qml
Normal file
268
meshroom/ui/qml/Viewer3D/Inspector3D.qml
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import MaterialIcons 2.2
|
||||||
|
import Qt3D.Core 2.0
|
||||||
|
import Qt3D.Render 2.1
|
||||||
|
import QtQuick.Controls.Material 2.4
|
||||||
|
import Controls 1.0
|
||||||
|
import Utils 1.0
|
||||||
|
|
||||||
|
FloatingPane {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitWidth: 200
|
||||||
|
|
||||||
|
property int renderMode: 2
|
||||||
|
property Transform targetTransform
|
||||||
|
property Locator3D origin: null
|
||||||
|
property Grid3D grid: null
|
||||||
|
property MediaLibrary mediaLibrary
|
||||||
|
property Camera camera
|
||||||
|
|
||||||
|
signal mediaFocusRequest(var index)
|
||||||
|
signal mediaRemoveRequest(var index)
|
||||||
|
|
||||||
|
MouseArea { anchors.fill: parent; onWheel: wheel.accepted = true }
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Label { text: "RENDER"; font.bold: true; font.pointSize: 8 }
|
||||||
|
Flow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Repeater {
|
||||||
|
model: Viewer3DSettings.renderModes
|
||||||
|
|
||||||
|
delegate: MaterialToolButton {
|
||||||
|
text: modelData["icon"]
|
||||||
|
ToolTip.text: modelData["name"] + " (" + (index+1) + ")"
|
||||||
|
onClicked: Viewer3DSettings.renderMode = index
|
||||||
|
checked: Viewer3DSettings.renderMode === index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label { text: "SCENE"; font.bold: true; font.pointSize: 8 }
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
id: controlsLayout
|
||||||
|
Layout.fillWidth: true
|
||||||
|
columns: 3
|
||||||
|
columnSpacing: 6
|
||||||
|
Flow {
|
||||||
|
Layout.columnSpan: 3
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
CheckBox {
|
||||||
|
text: "Grid"
|
||||||
|
checked: Viewer3DSettings.displayGrid
|
||||||
|
onClicked: Viewer3DSettings.displayGrid = !Viewer3DSettings.displayGrid
|
||||||
|
}
|
||||||
|
CheckBox {
|
||||||
|
text: "Locator"
|
||||||
|
checked: Viewer3DSettings.displayLocator
|
||||||
|
onClicked: Viewer3DSettings.displayLocator = !Viewer3DSettings.displayLocator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotation Controls
|
||||||
|
Label {
|
||||||
|
font.family: MaterialIcons.fontFamily
|
||||||
|
text: MaterialIcons.rotation3D
|
||||||
|
font.pointSize: 14
|
||||||
|
Layout.rowSpan: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationX = value}
|
||||||
|
Label { text: "X" }
|
||||||
|
|
||||||
|
Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationY = value}
|
||||||
|
Label { text: "Y" }
|
||||||
|
|
||||||
|
Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationZ = value }
|
||||||
|
Label { text: "Z" }
|
||||||
|
|
||||||
|
Label { text: "Points" }
|
||||||
|
RowLayout {
|
||||||
|
Layout.columnSpan: 2
|
||||||
|
Slider {
|
||||||
|
Layout.fillWidth: true; from: 1; to: 20;stepSize: 0.1
|
||||||
|
value: Viewer3DSettings.pointSize
|
||||||
|
onValueChanged: Viewer3DSettings.pointSize = value
|
||||||
|
}
|
||||||
|
CheckBox {
|
||||||
|
text: "Fixed";
|
||||||
|
checked: Viewer3DSettings.fixedPointSize
|
||||||
|
onClicked: Viewer3DSettings.fixedPointSize = !Viewer3DSettings.fixedPointSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Label { text: "MEDIA"; font.bold: true; font.pointSize: 8 }
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: mediaListView
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
clip: true
|
||||||
|
model: mediaLibrary.model
|
||||||
|
spacing: 2
|
||||||
|
//section.property: "section"
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar { id: scrollBar }
|
||||||
|
|
||||||
|
section.delegate: Pane {
|
||||||
|
width: parent.width
|
||||||
|
padding: 1
|
||||||
|
background: null
|
||||||
|
|
||||||
|
Label {
|
||||||
|
width: parent.width
|
||||||
|
padding: 4
|
||||||
|
background: Rectangle { color: Qt.darker(parent.palette.base, 1.15) }
|
||||||
|
text: section
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: mediaLibrary
|
||||||
|
onLoadRequest: {
|
||||||
|
mediaListView.positionViewAtIndex(idx, ListView.Visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: RowLayout {
|
||||||
|
// add mediaLibrary.count in the binding to ensure 'entity'
|
||||||
|
// is re-evaluated when mediaLibrary delegates are modified
|
||||||
|
property bool loading: model.status === SceneLoader.Loading
|
||||||
|
spacing: 2
|
||||||
|
width: parent.width - scrollBar.width / 2
|
||||||
|
|
||||||
|
property string src: model.source
|
||||||
|
onSrcChanged: focusAnim.restart()
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
enabled: model.status === SceneLoader.Ready
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
MaterialToolButton {
|
||||||
|
text: model.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off
|
||||||
|
font.pointSize: 10
|
||||||
|
ToolTip.text: model.visible ? "Hide" : "Show"
|
||||||
|
flat: true
|
||||||
|
opacity: model.visible ? 1.0 : 0.6
|
||||||
|
onClicked: {
|
||||||
|
if(hoverArea.modifiers & Qt.ControlModifier)
|
||||||
|
mediaLibrary.solo(index);
|
||||||
|
else
|
||||||
|
model.visible = !model.visible
|
||||||
|
}
|
||||||
|
// Handle modifiers on button click
|
||||||
|
MouseArea {
|
||||||
|
id: hoverArea
|
||||||
|
property int modifiers
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onPositionChanged: modifiers = mouse.modifiers
|
||||||
|
onExited: modifiers = Qt.NoModifier
|
||||||
|
onPressed: {
|
||||||
|
modifiers = mouse.modifiers;
|
||||||
|
mouse.accepted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MaterialToolButton {
|
||||||
|
text: MaterialIcons.filter_center_focus
|
||||||
|
font.pointSize: 10
|
||||||
|
ToolTip.text: "Frame"
|
||||||
|
onClicked: camera.viewEntity(mediaLibrary.entityAt(index))
|
||||||
|
flat: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 1
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: label
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
topPadding: 3
|
||||||
|
bottomPadding: topPadding
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: model.label
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
background: Rectangle {
|
||||||
|
Connections {
|
||||||
|
target: mediaLibrary
|
||||||
|
onLoadRequest: if(idx == index) focusAnim.restart()
|
||||||
|
}
|
||||||
|
ColorAnimation on color {
|
||||||
|
id: focusAnim
|
||||||
|
from: label.palette.highlight
|
||||||
|
to: "transparent"
|
||||||
|
duration: 2000
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onDoubleClicked: camera.viewEntity(mediaLibrary.entityAt(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: childrenRect.height
|
||||||
|
RowLayout {
|
||||||
|
visible: model.status === SceneLoader.Ready
|
||||||
|
MaterialLabel { visible: model.vertexCount; text: MaterialIcons.grain }
|
||||||
|
Label { visible: model.vertexCount; text: Format.intToString(model.vertexCount) }
|
||||||
|
MaterialLabel { visible: model.faceCount; text: MaterialIcons.details; rotation: -180 }
|
||||||
|
Label { visible: model.faceCount; text: Format.intToString(model.faceCount) }
|
||||||
|
MaterialLabel { visible: model.cameraCount; text: MaterialIcons.videocam }
|
||||||
|
Label { visible: model.cameraCount; text: model.cameraCount }
|
||||||
|
MaterialLabel { visible: model.textureCount; text: MaterialIcons.texture }
|
||||||
|
Label { visible: model.textureCount; text: model.textureCount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialToolButton {
|
||||||
|
id: requestMediaButton
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
|
||||||
|
enabled: !loading
|
||||||
|
text: loading || !model.requested ? MaterialIcons.radio_button_unchecked : MaterialIcons.radio_button_checked
|
||||||
|
font.pointSize: 10
|
||||||
|
palette.buttonText: model.valid ? "#4CAF50" : label.palette.buttonText
|
||||||
|
ToolTip.text: ""
|
||||||
|
onClicked: model.requested = !model.requested
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialToolButton {
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
|
||||||
|
visible: !loading
|
||||||
|
text: MaterialIcons.clear
|
||||||
|
font.pointSize: 10
|
||||||
|
ToolTip.text: "Remove"
|
||||||
|
onClicked: mediaLibrary.remove(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
visible: loading
|
||||||
|
running: visible
|
||||||
|
padding: 0
|
||||||
|
implicitHeight: 14
|
||||||
|
implicitWidth: requestMediaButton.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Controls 1.4 as Controls1
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
|
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
|
||||||
|
@ -11,22 +13,12 @@ import MaterialIcons 2.2
|
||||||
|
|
||||||
import Controls 1.0
|
import Controls 1.0
|
||||||
|
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias source: modelLoader.source
|
|
||||||
property alias abcSource: modelLoader.abcSource
|
|
||||||
property alias depthMapSource: modelLoader.depthMapSource
|
|
||||||
|
|
||||||
property int renderMode: 2
|
property int renderMode: 2
|
||||||
readonly property alias loading: modelLoader.loading
|
property alias library: mediaLibrary
|
||||||
readonly property alias polyCount: modelLoader.polyCount
|
|
||||||
|
|
||||||
// Alembic optional support => won't be available if AlembicEntity plugin is not available
|
|
||||||
readonly property Component abcLoaderComp: Qt.createComponent("AlembicLoader.qml")
|
|
||||||
readonly property bool supportAlembic: abcLoaderComp.status == Component.Ready
|
|
||||||
readonly property Component depthMapLoaderComp: Qt.createComponent("DepthMapLoader.qml")
|
|
||||||
readonly property bool supportDepthMap: depthMapLoaderComp.status == Component.Ready
|
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
function resetCameraCenter() {
|
function resetCameraCenter() {
|
||||||
|
@ -40,122 +32,41 @@ FocusScope {
|
||||||
mainCamera.viewCenter = Qt.vector3d(0.0, 0.0, 0.0);
|
mainCamera.viewCenter = Qt.vector3d(0.0, 0.0, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findChildrenByProperty(node, propertyName, container)
|
function load(filepath) {
|
||||||
{
|
mediaLibrary.load(filepath);
|
||||||
if(!node || !node.childNodes)
|
|
||||||
return;
|
|
||||||
for(var i=0; i < node.childNodes.length; ++i)
|
|
||||||
{
|
|
||||||
var childNode = node.childNodes[i];
|
|
||||||
if(!childNode)
|
|
||||||
continue;
|
|
||||||
if(childNode[propertyName] !== undefined)
|
|
||||||
container.push(childNode);
|
|
||||||
else
|
|
||||||
findChildrenByProperty(childNode, propertyName, container)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove automatically created DiffuseMapMaterial and
|
function view(attribute) {
|
||||||
// instantiate a MaterialSwitcher instead
|
mediaLibrary.view(attribute)
|
||||||
function setupMaterialSwitchers(rootEntity)
|
|
||||||
{
|
|
||||||
var materials = [];
|
|
||||||
findChildrenByProperty(rootEntity, "diffuse", materials);
|
|
||||||
var entities = []
|
|
||||||
materials.forEach(function(mat){
|
|
||||||
entities.push(mat.parent)
|
|
||||||
})
|
|
||||||
|
|
||||||
entities.forEach(function(entity) {
|
|
||||||
var mats = []
|
|
||||||
var hasTextures = false
|
|
||||||
// Create as many MaterialSwitcher as individual materials for this entity
|
|
||||||
// NOTE: we let each MaterialSwitcher modify the components of the entity
|
|
||||||
// and therefore remove the default material spawned by the sceneLoader
|
|
||||||
for(var i=0; i < entity.components.length; ++i)
|
|
||||||
{
|
|
||||||
var comp = entity.components[i]
|
|
||||||
// handle DiffuseMapMaterials created by SceneLoader
|
|
||||||
if(comp.toString().indexOf("QDiffuseMapMaterial") > -1)
|
|
||||||
{
|
|
||||||
// store material definition
|
|
||||||
var m = {
|
|
||||||
"diffuseMap": comp.diffuse.data[0].source,
|
|
||||||
"shininess": comp.shininess,
|
|
||||||
"specular": comp.specular,
|
|
||||||
"ambient": comp.ambient,
|
|
||||||
"mode": root.renderMode
|
|
||||||
}
|
|
||||||
mats.push(m)
|
|
||||||
hasTextures = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if(comp.toString().indexOf("QPhongMaterial") > -1) {
|
|
||||||
// create MaterialSwitcher with default colors
|
|
||||||
mats.push({})
|
|
||||||
}
|
|
||||||
// Retrieve polycount using vertexPosition buffer
|
|
||||||
if(comp.toString().indexOf("Geometry") > -1) {
|
|
||||||
for(var k = 0; k < comp.geometry.attributes.length; ++k)
|
|
||||||
{
|
|
||||||
if(comp.geometry.attributes[k].name == "vertexPosition")
|
|
||||||
modelLoader.polyCount += comp.geometry.attributes[k].count / 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modelLoader.meshHasTexture = mats.length > 0
|
|
||||||
mats.forEach(function(m){
|
|
||||||
// create a material switcher for each material definition
|
|
||||||
var matSwitcher = materialSwitcherComponent.createObject(entity, m)
|
|
||||||
matSwitcher.mode = Qt.binding(function(){ return root.renderMode })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
function clear() {
|
||||||
id: materialSwitcherComponent
|
mediaLibrary.clear()
|
||||||
MaterialSwitcher {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear()
|
|
||||||
{
|
|
||||||
clearScene()
|
|
||||||
clearAbc()
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearScene()
|
|
||||||
{
|
|
||||||
source = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAbc()
|
|
||||||
{
|
|
||||||
abcSource = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearDepthMap()
|
|
||||||
{
|
|
||||||
depthMapSource = 'no_file'
|
|
||||||
depthMapSource = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemPalette { id: activePalette }
|
SystemPalette { id: activePalette }
|
||||||
|
|
||||||
|
|
||||||
Scene3D {
|
Scene3D {
|
||||||
id: scene3D
|
id: scene3D
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cameraAspectRatioMode: Scene3D.AutomaticAspectRatio // vs. UserAspectRatio
|
cameraAspectRatioMode: Scene3D.AutomaticAspectRatio // vs. UserAspectRatio
|
||||||
hoverEnabled: false // if true, will trigger positionChanged events in attached MouseHandler
|
hoverEnabled: true // if true, will trigger positionChanged events in attached MouseHandler
|
||||||
aspects: ["logic", "input"]
|
aspects: ["logic", "input"]
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
|
|
||||||
Keys.onPressed: {
|
Keys.onPressed: {
|
||||||
if (event.key == Qt.Key_F) {
|
if (event.key == Qt.Key_F) {
|
||||||
resetCameraCenter();
|
resetCameraCenter();
|
||||||
resetCameraPosition();
|
resetCameraPosition();
|
||||||
event.accepted = true;
|
}
|
||||||
|
else if(Qt.Key_1 <= event.key && event.key <= Qt.Key_3)
|
||||||
|
{
|
||||||
|
Viewer3DSettings.renderMode = event.key - Qt.Key_1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
event.accepted = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,16 +87,30 @@ FocusScope {
|
||||||
Behavior on viewCenter {
|
Behavior on viewCenter {
|
||||||
Vector3dAnimation { duration: 250 }
|
Vector3dAnimation { duration: 250 }
|
||||||
}
|
}
|
||||||
|
// Scene light, attached to the camera
|
||||||
|
Entity {
|
||||||
|
components: [
|
||||||
|
PointLight {
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scene light, attached to the camera
|
|
||||||
Entity {
|
Entity {
|
||||||
components: [
|
components: [
|
||||||
PointLight {
|
SphereMesh {
|
||||||
color: "white"
|
|
||||||
},
|
},
|
||||||
Transform {
|
Transform {
|
||||||
translation: mainCamera.position
|
id: viewCenterTransform
|
||||||
|
translation: mainCamera.viewCenter
|
||||||
|
scale: 0.005 * mainCamera.viewCenter.minus(mainCamera.position).length()
|
||||||
|
},
|
||||||
|
PhongMaterial {
|
||||||
|
ambient: "#FFF"
|
||||||
|
shininess: 0.2
|
||||||
|
diffuse: activePalette.highlight
|
||||||
|
specular: activePalette.highlight
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -257,272 +182,49 @@ FocusScope {
|
||||||
Qt3DInput.InputSettings { }
|
Qt3DInput.InputSettings { }
|
||||||
]
|
]
|
||||||
|
|
||||||
Entity {
|
MediaLibrary {
|
||||||
id: modelLoader
|
id: mediaLibrary
|
||||||
property string source
|
renderMode: Viewer3DSettings.renderMode
|
||||||
property string abcSource
|
// Picking to set focus point (camera view center)
|
||||||
property string depthMapSource
|
// Only activate it when a double click may happen or when the 'Control' key is pressed
|
||||||
property int polyCount
|
pickingEnabled: cameraController.pickingActive || doubleClickTimer.running
|
||||||
property bool meshHasTexture: false
|
|
||||||
// SceneLoader status is not reliable when loading a 3D file
|
|
||||||
property bool loading: false
|
|
||||||
onSourceChanged: {
|
|
||||||
polyCount = 0
|
|
||||||
meshHasTexture = false
|
|
||||||
loading = true
|
|
||||||
}
|
|
||||||
onAbcSourceChanged: {
|
|
||||||
if(root.supportAlembic)
|
|
||||||
loading = true
|
|
||||||
}
|
|
||||||
|
|
||||||
components: [sceneLoaderEntity, transform, picker]
|
components: [
|
||||||
|
Transform {
|
||||||
// ObjectPicker used for view re-centering
|
id: transform
|
||||||
ObjectPicker {
|
|
||||||
id: picker
|
|
||||||
// Triangle picking is expensive
|
|
||||||
// Only activate it when a double click may happen or when the 'Control' key is pressed
|
|
||||||
enabled: cameraController.pickingActive || doubleClickTimer.running
|
|
||||||
hoverEnabled: false
|
|
||||||
onPressed: {
|
|
||||||
if(pick.button == Qt.LeftButton)
|
|
||||||
mainCamera.viewCenter = pick.worldIntersection
|
|
||||||
doubleClickTimer.stop()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Transform {
|
|
||||||
id: transform
|
|
||||||
}
|
|
||||||
|
|
||||||
Entity {
|
|
||||||
id: sceneLoaderEntity
|
|
||||||
enabled: showMeshCheckBox.checked
|
|
||||||
|
|
||||||
components: [
|
|
||||||
SceneLoader {
|
|
||||||
id: scene
|
|
||||||
source: modelLoader.source
|
|
||||||
onStatusChanged: {
|
|
||||||
if(scene.status != SceneLoader.Loading)
|
|
||||||
modelLoader.loading = false;
|
|
||||||
if(scene.status == SceneLoader.Ready)
|
|
||||||
{
|
|
||||||
setupMaterialSwitchers(modelLoader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Entity {
|
|
||||||
id: abcLoaderEntity
|
|
||||||
// Instantiate the AlembicEntity dynamically
|
|
||||||
// to avoid import errors if the plugin is not available
|
|
||||||
property Entity abcLoader: null
|
|
||||||
enabled: showSfMCheckBox.checked
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if(!root.supportAlembic) // Alembic plugin not available
|
|
||||||
return
|
|
||||||
|
|
||||||
// destroy previously created entity
|
|
||||||
if(abcLoader != undefined)
|
|
||||||
abcLoader.destroy()
|
|
||||||
|
|
||||||
abcLoader = abcLoaderComp.createObject(abcLoaderEntity, {
|
|
||||||
'url': Qt.binding(function() { return modelLoader.abcSource } ),
|
|
||||||
'particleSize': Qt.binding(function() { return 0.01 * transform.scale }),
|
|
||||||
'locatorScale': Qt.binding(function() { return 0.2})
|
|
||||||
});
|
|
||||||
// urlChanged signal is emitted once the Alembic file is loaded
|
|
||||||
// set the 'loading' property to false when it's emitted
|
|
||||||
// TODO: AlembicEntity should expose a status
|
|
||||||
abcLoader.onUrlChanged.connect(function(){
|
|
||||||
modelLoader.loading = false
|
|
||||||
spawnCameraSelectors()
|
|
||||||
})
|
|
||||||
modelLoader.loading = false
|
|
||||||
}
|
|
||||||
function spawnCameraSelectors() {
|
|
||||||
// spawn camera selector for each camera
|
|
||||||
for(var i = 0; i < abcLoader.cameras.length; ++i)
|
|
||||||
{
|
|
||||||
var cam = abcLoader.cameras[i]
|
|
||||||
// retrieve view id
|
|
||||||
var viewId = cam.userProperties["mvg_viewId"]
|
|
||||||
if(viewId == undefined)
|
|
||||||
continue
|
|
||||||
var obj = camSelectionComponent.createObject(cam)
|
|
||||||
obj.viewId = viewId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Camera selection picking and display
|
|
||||||
property Component camSelectionComponent: Component {
|
|
||||||
id: camSelectionComponent
|
|
||||||
Entity {
|
|
||||||
property string viewId
|
|
||||||
property alias ambient: mat.ambient
|
|
||||||
components: [
|
|
||||||
CuboidMesh { xExtent: 0.2; yExtent: 0.2; zExtent: 0.2},
|
|
||||||
PhongMaterial{
|
|
||||||
id: mat
|
|
||||||
ambient: viewId == _reconstruction.selectedViewId ? activePalette.highlight : "#CCC"
|
|
||||||
diffuse: ambient
|
|
||||||
},
|
|
||||||
ObjectPicker {
|
|
||||||
onClicked: _reconstruction.selectedViewId = viewId
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Entity {
|
|
||||||
id: depthMapLoaderEntity
|
|
||||||
// Instantiate the DepthMapEntity dynamically
|
|
||||||
// to avoid import errors if the plugin is not available
|
|
||||||
property Entity depthMapLoader: null
|
|
||||||
enabled: showDepthMapCheckBox.checked
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if(!root.supportDepthMap) // DepthMap plugin not available
|
|
||||||
return
|
|
||||||
|
|
||||||
// destroy previously created entity
|
|
||||||
if(depthMapLoader != undefined)
|
|
||||||
depthMapLoader.destroy()
|
|
||||||
|
|
||||||
depthMapLoader = depthMapLoaderComp.createObject(depthMapLoaderEntity, {
|
|
||||||
'source': Qt.binding(function() { return modelLoader.depthMapSource } )
|
|
||||||
});
|
|
||||||
// 'sourceChanged' signal is emitted once the depthMap file is loaded
|
|
||||||
// set the 'loading' property to false when it's emitted
|
|
||||||
// TODO: DepthMapEntity should expose a status
|
|
||||||
depthMapLoader.onSourceChanged.connect(function(){ modelLoader.loading = false })
|
|
||||||
modelLoader.loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Locator3D { enabled: locatorCheckBox.checked }
|
|
||||||
}
|
|
||||||
Grid3D { enabled: gridCheckBox.checked }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// UI Overlay
|
|
||||||
//
|
|
||||||
|
|
||||||
// Rotation/Scale
|
|
||||||
FloatingPane {
|
|
||||||
anchors { top: parent.top; left: parent.left }
|
|
||||||
|
|
||||||
GridLayout {
|
|
||||||
id: controlsLayout
|
|
||||||
columns: 3
|
|
||||||
columnSpacing: 6
|
|
||||||
|
|
||||||
property int sliderWidth: 70
|
|
||||||
|
|
||||||
// Rotation Controls
|
|
||||||
Label {
|
|
||||||
font.family: MaterialIcons.fontFamily
|
|
||||||
text: MaterialIcons.rotation3D
|
|
||||||
font.pointSize: 14
|
|
||||||
Layout.rowSpan: 3
|
|
||||||
}
|
|
||||||
|
|
||||||
Slider { implicitWidth: controlsLayout.sliderWidth; from: -180; to: 180; onPositionChanged: transform.rotationX = value }
|
|
||||||
Label { text: "X" }
|
|
||||||
|
|
||||||
Slider { implicitWidth: controlsLayout.sliderWidth; from: -180; to: 180; onPositionChanged: transform.rotationY = value }
|
|
||||||
Label { text: "Y" }
|
|
||||||
|
|
||||||
Slider { implicitWidth: controlsLayout.sliderWidth; from: -180; to: 180; onPositionChanged: transform.rotationZ = value }
|
|
||||||
Label { text: "Z" }
|
|
||||||
|
|
||||||
// Scale Control
|
|
||||||
Label { text: "Scale" }
|
|
||||||
Slider { Layout.columnSpan: 2; implicitWidth: controlsLayout.sliderWidth; from: 1; to: 10; onPositionChanged: transform.scale = value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outliner
|
|
||||||
FloatingPane {
|
|
||||||
anchors { top: parent.top; right: parent.right }
|
|
||||||
|
|
||||||
Column {
|
|
||||||
Row {
|
|
||||||
visible: root.supportAlembic
|
|
||||||
CheckBox { id: showSfMCheckBox; text: "SfM"; checked: true; visible: root.supportAlembic; opacity: root.abcSource ? 1.0 : 0.6 }
|
|
||||||
ToolButton {
|
|
||||||
text: MaterialIcons.clear; font.family: MaterialIcons.fontFamily; visible: root.abcSource != '';
|
|
||||||
onClicked: clearAbc()
|
|
||||||
ToolTip.text: "Unload"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
visible: root.depthMapSource != ''
|
|
||||||
CheckBox { id: showDepthMapCheckBox; text: "DepthMap"; checked: true; }
|
|
||||||
ToolButton {
|
|
||||||
text: MaterialIcons.clear; font.family: MaterialIcons.fontFamily;
|
|
||||||
onClicked: clearDepthMap()
|
|
||||||
ToolTip.text: "Unload"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
CheckBox { id: showMeshCheckBox; text: "Mesh"; checked: true; opacity: root.source ? 1.0 : 0.6 }
|
|
||||||
ToolButton {
|
|
||||||
text: MaterialIcons.clear; font.family: MaterialIcons.fontFamily; visible: root.source != '';
|
|
||||||
onClicked: clearScene()
|
|
||||||
ToolTip.text: "Unload"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CheckBox { id: gridCheckBox; text: "Grid"; checked: true }
|
|
||||||
CheckBox { id: locatorCheckBox; text: "Locator"; checked: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render Mode
|
|
||||||
FloatingPane {
|
|
||||||
anchors { bottom: parent.bottom; left: parent.left }
|
|
||||||
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
Repeater {
|
|
||||||
model: [ // Can't use ListModel because of MaterialIcons expressions
|
|
||||||
{"name": "Solid", "icon": MaterialIcons.crop_din},
|
|
||||||
{"name": "Wireframe", "icon": MaterialIcons.grid_on},
|
|
||||||
{"name": "Textured", "icon": MaterialIcons.texture },
|
|
||||||
]
|
]
|
||||||
delegate: ToolButton {
|
|
||||||
text: modelData["icon"]
|
onPressed: {
|
||||||
ToolTip.text: modelData["name"]
|
if(pick.button == Qt.LeftButton)
|
||||||
ToolTip.visible: hovered
|
{
|
||||||
font.family: MaterialIcons.fontFamily
|
mainCamera.viewCenter = pick.worldIntersection;
|
||||||
font.pointSize: 11
|
}
|
||||||
padding: 4
|
doubleClickTimer.stop();
|
||||||
onClicked: root.renderMode = index
|
|
||||||
checkable: !checked // hack to disable check toggle on click
|
|
||||||
checked: renderMode === index
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Locator3D { enabled: Viewer3DSettings.displayLocator }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Grid3D { enabled: Viewer3DSettings.displayGrid }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FloatingPane {
|
// UI Overlay
|
||||||
anchors.right: parent.right
|
Controls1.SplitView {
|
||||||
anchors.bottom: parent.bottom
|
id: overlaySplitView
|
||||||
visible: modelLoader.polyCount > 0
|
anchors.fill: parent
|
||||||
Label {
|
|
||||||
text: modelLoader.polyCount + " faces"
|
Item { Layout.fillWidth: true; Layout.minimumWidth: parent.width * 0.5 }
|
||||||
|
|
||||||
|
Inspector3D {
|
||||||
|
id: inspector
|
||||||
|
width: 220
|
||||||
|
Layout.minimumWidth: 5
|
||||||
|
|
||||||
|
camera: mainCamera
|
||||||
|
targetTransform: transform
|
||||||
|
mediaLibrary: mediaLibrary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,10 @@ Item {
|
||||||
property int renderMode: 2
|
property int renderMode: 2
|
||||||
|
|
||||||
// Rasterized point size
|
// Rasterized point size
|
||||||
property real pointSize: 4
|
property real pointSize: 1.5
|
||||||
// Whether point size is fixed or view dependent
|
// Whether point size is fixed or view dependent
|
||||||
property bool fixedPointSize: false
|
property bool fixedPointSize: false
|
||||||
|
// Helpers display
|
||||||
|
property bool displayGrid: true
|
||||||
|
property bool displayLocator: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,35 +21,33 @@ Item {
|
||||||
readonly property variant cameraInits: _reconstruction.cameraInits
|
readonly property variant cameraInits: _reconstruction.cameraInits
|
||||||
property bool readOnly: false
|
property bool readOnly: false
|
||||||
|
|
||||||
|
|
||||||
implicitWidth: 300
|
implicitWidth: 300
|
||||||
implicitHeight: 400
|
implicitHeight: 400
|
||||||
|
|
||||||
|
|
||||||
// Load a 3D media file in the 3D viewer
|
// Load a 3D media file in the 3D viewer
|
||||||
function load3DMedia(filepath)
|
function load3DMedia(filepath) {
|
||||||
{
|
viewer3D.load(filepath);
|
||||||
if(!Filepath.exists(Filepath.urlToString(filepath)))
|
}
|
||||||
return
|
|
||||||
switch(Filepath.extension(filepath))
|
function viewAttribute(attr) {
|
||||||
{
|
viewer3D.view(attr);
|
||||||
case ".abc": viewer3D.abcSource = filepath; break;
|
|
||||||
case ".exr": viewer3D.depthMapSource = filepath; break;
|
|
||||||
case ".obj": viewer3D.source = filepath; break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: reconstruction
|
target: reconstruction
|
||||||
onGraphChanged: {
|
onGraphChanged: viewer3D.clear()
|
||||||
viewer3D.clear()
|
onSfmChanged: viewSfM()
|
||||||
viewer2D.clear()
|
onSfmReportChanged: viewSfM()
|
||||||
}
|
}
|
||||||
onSfmReportChanged: {
|
Component.onCompleted: viewSfM()
|
||||||
viewer3D.abcSource = ''
|
|
||||||
if(!reconstruction.sfm)
|
// Load reconstruction's current SfM file
|
||||||
return
|
function viewSfM() {
|
||||||
load3DMedia(Filepath.stringToUrl(reconstruction.sfm.attribute('output').value))
|
if(!reconstruction.sfm)
|
||||||
}
|
return;
|
||||||
|
viewAttribute(reconstruction.sfm.attribute('output'));
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemPalette { id: activePalette }
|
SystemPalette { id: activePalette }
|
||||||
|
@ -115,12 +113,16 @@ Item {
|
||||||
|
|
||||||
Panel {
|
Panel {
|
||||||
title: "3D Viewer"
|
title: "3D Viewer"
|
||||||
implicitWidth: Math.round(parent.width * 0.33)
|
implicitWidth: Math.round(parent.width * 0.45)
|
||||||
Layout.minimumWidth: 20
|
Layout.minimumWidth: 20
|
||||||
Layout.minimumHeight: 80
|
Layout.minimumHeight: 80
|
||||||
|
|
||||||
Viewer3D {
|
Viewer3D {
|
||||||
id: viewer3D
|
id: viewer3D
|
||||||
|
readonly property var outputAttribute: _reconstruction.endNode ? _reconstruction.endNode.attribute("outputMesh") : null
|
||||||
|
readonly property bool outputReady: outputAttribute && _reconstruction.endNode.globalStatus === "SUCCESS"
|
||||||
|
readonly property int outputMediaIndex: library.find(outputAttribute)
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
DropArea {
|
DropArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -129,20 +131,14 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Loading..."
|
|
||||||
visible: viewer3D.loading
|
|
||||||
padding: 6
|
|
||||||
background: Rectangle { color: parent.palette.base; opacity: 0.5 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load reconstructed model
|
// Load reconstructed model
|
||||||
Button {
|
Button {
|
||||||
text: "Load Model"
|
text: "Load Model"
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.bottomMargin: 10
|
anchors.bottomMargin: 10
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
visible: viewer3D.outputReady && viewer3D.outputMediaIndex == -1
|
||||||
|
onClicked: viewAttribute(_reconstruction.endNode.attribute("outputMesh"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue