mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-08-04 01:08:26 +02:00
[ui] Viewer3D: introduce new 3D media loading backend
This commit adds several components to centralize and extend 3D media loading. They are not yet integrated into Viewer3D. The entry point to this system is the MediaLibrary component that: * can load N medias based on a filepath or load-and-watch node attributes * provides a cache mechanism to instant-reload medias that were unloaded under certain conditions * gives access to statistics (vertex/face/camera/textureCount) through a unified interface
This commit is contained in:
parent
2f50587904
commit
109b980ae5
7 changed files with 587 additions and 0 deletions
|
@ -1,4 +1,8 @@
|
|||
import AlembicEntity 1.0
|
||||
import QtQuick 2.9
|
||||
import Qt3D.Core 2.1
|
||||
import Qt3D.Render 2.1
|
||||
import Qt3D.Extras 2.1
|
||||
|
||||
/**
|
||||
* Support for Alembic files in Qt3d.
|
||||
|
@ -6,4 +10,54 @@ import AlembicEntity 1.0
|
|||
*/
|
||||
AlembicEntity {
|
||||
id: root
|
||||
|
||||
signal cameraSelected(var viewId)
|
||||
|
||||
function spawnCameraSelectors() {
|
||||
var validCameras = 0;
|
||||
// spawn camera selector for each camera
|
||||
for(var i = 0; i < root.cameras.length; ++i)
|
||||
{
|
||||
var cam = root.cameras[i];
|
||||
// retrieve view id
|
||||
var viewId = cam.userProperties["mvg_viewId"];
|
||||
if(viewId === undefined)
|
||||
continue;
|
||||
// filter out non-reconstructed cameras
|
||||
if(cam.parent.parent.objectName === "mvgCamerasUndefined") {
|
||||
cam.enabled = false;
|
||||
continue;
|
||||
}
|
||||
camSelectionComponent.createObject(cam, {"viewId": viewId});
|
||||
validCameras++;
|
||||
}
|
||||
return validCameras;
|
||||
}
|
||||
|
||||
SystemPalette {
|
||||
id: activePalette
|
||||
}
|
||||
|
||||
// Camera selection picking and display
|
||||
Component {
|
||||
id: camSelectionComponent
|
||||
Entity {
|
||||
id: camSelector
|
||||
property string viewId
|
||||
|
||||
components: [
|
||||
CuboidMesh { xExtent: 0.2; yExtent: 0.2; zExtent: 0.2;},
|
||||
PhongMaterial{
|
||||
id: mat
|
||||
ambient: viewId === _reconstruction.selectedViewId ? activePalette.highlight : "#CCC"
|
||||
diffuse: cameraPicker.containsMouse ? Qt.lighter(activePalette.highlight, 1.2) : ambient
|
||||
},
|
||||
ObjectPicker {
|
||||
id: cameraPicker
|
||||
enabled: root.enabled
|
||||
onClicked: _reconstruction.selectedViewId = camSelector.viewId
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
76
meshroom/ui/qml/Viewer3D/MediaCache.qml
Normal file
76
meshroom/ui/qml/Viewer3D/MediaCache.qml
Normal file
|
@ -0,0 +1,76 @@
|
|||
import Qt3D.Core 2.1
|
||||
import Qt3D.Render 2.1
|
||||
|
||||
import Utils 1.0
|
||||
|
||||
Entity {
|
||||
id: root
|
||||
|
||||
enabled: false // disabled entity
|
||||
|
||||
property int cacheSize: 2
|
||||
property var mediaCache: {[]}
|
||||
|
||||
/// The current number of managed entities
|
||||
function currentSize() {
|
||||
return Object.keys(mediaCache).length;
|
||||
}
|
||||
|
||||
/// Whether the cache contains an entity for the given source
|
||||
function contains(source) {
|
||||
return mediaCache[source] !== undefined;
|
||||
}
|
||||
|
||||
/// Add an entity to the cache
|
||||
function add(source, object){
|
||||
if(!Filepath.exists(source))
|
||||
return false;
|
||||
if(contains(source))
|
||||
return true;
|
||||
// console.debug("[cache] add: " + source)
|
||||
mediaCache[source] = object;
|
||||
object.parent = root;
|
||||
// remove oldest entry in cache
|
||||
if(currentSize() > cacheSize)
|
||||
shrink();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Pop an entity from the cache based on its source
|
||||
function pop(source){
|
||||
if(!contains(source))
|
||||
return undefined;
|
||||
|
||||
var obj = mediaCache[source];
|
||||
delete mediaCache[source];
|
||||
// console.debug("[cache] pop: " + source)
|
||||
// delete cached obj if file does not exist on disk anymore
|
||||
if(!Filepath.exists(source))
|
||||
{
|
||||
obj.destroy();
|
||||
obj = undefined;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// Remove and destroy an entity from cache
|
||||
function destroyEntity(source) {
|
||||
var obj = pop(source);
|
||||
if(obj)
|
||||
obj.destroy();
|
||||
}
|
||||
|
||||
|
||||
// Shrink cache to fit max size
|
||||
function shrink() {
|
||||
while(currentSize() > cacheSize)
|
||||
destroyEntity(Object.keys(mediaCache)[0]);
|
||||
}
|
||||
|
||||
// Clear cache and destroy all managed entities
|
||||
function clear() {
|
||||
Object.keys(mediaCache).forEach(function(key){
|
||||
destroyEntity(key);
|
||||
});
|
||||
}
|
||||
}
|
241
meshroom/ui/qml/Viewer3D/MediaLibrary.qml
Normal file
241
meshroom/ui/qml/Viewer3D/MediaLibrary.qml
Normal file
|
@ -0,0 +1,241 @@
|
|||
import QtQuick 2.9
|
||||
import Qt3D.Core 2.1
|
||||
import Qt3D.Render 2.1
|
||||
import Utils 1.0
|
||||
|
||||
|
||||
/**
|
||||
* MediaLibrary is an Entity that loads and manages a list of 3D media.
|
||||
* It also uses an internal cache to instantly reload media.
|
||||
*/
|
||||
Entity {
|
||||
id: root
|
||||
|
||||
readonly property alias model: m.mediaModel
|
||||
property int renderMode
|
||||
property bool pickingEnabled: false
|
||||
readonly property alias count: instantiator.count // number of instantiated media delegates
|
||||
|
||||
signal pressed(var pick)
|
||||
signal loadRequest(var idx)
|
||||
|
||||
QtObject {
|
||||
id: m
|
||||
property ListModel mediaModel: ListModel {}
|
||||
property var sourceToEntity: ({})
|
||||
|
||||
readonly property var mediaElement: ({
|
||||
"source": "",
|
||||
"valid": true,
|
||||
"label": "",
|
||||
"visible": true,
|
||||
"section": "",
|
||||
"attribute": null,
|
||||
"entity": null,
|
||||
"requested": true,
|
||||
"vertexCount": 0,
|
||||
"faceCount": 0,
|
||||
"cameraCount": 0,
|
||||
"textureCount": 0,
|
||||
"status": SceneLoader.None
|
||||
})
|
||||
}
|
||||
|
||||
function makeElement(values) {
|
||||
return Object.assign({}, JSON.parse(JSON.stringify(m.mediaElement)), values);
|
||||
}
|
||||
|
||||
function ensureVisible(source) {
|
||||
var idx = find(source);
|
||||
if(idx === -1)
|
||||
return
|
||||
// load / make media visible
|
||||
m.mediaModel.get(idx).requested = true;
|
||||
m.mediaModel.get(idx).visible = true;
|
||||
loadRequest(idx);
|
||||
}
|
||||
|
||||
function find(source) {
|
||||
for(var i=0; i<m.mediaModel.count; ++i) {
|
||||
var elt = m.mediaModel.get(i);
|
||||
if( elt.source === source || elt.attribute === source)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function load(filepath) {
|
||||
var pathStr = Filepath.urlToString(filepath);
|
||||
if(!Filepath.exists(pathStr))
|
||||
{
|
||||
console.warn("Media Error: File " + pathStr + " does not exist.");
|
||||
return;
|
||||
}
|
||||
// file already loaded, return
|
||||
if(m.sourceToEntity[pathStr]) {
|
||||
ensureVisible(pathStr);
|
||||
return;
|
||||
}
|
||||
|
||||
// add file to the internal ListModel
|
||||
m.mediaModel.append(makeElement({
|
||||
"source": pathStr,
|
||||
"label": Filepath.basename(pathStr),
|
||||
"section": "External"
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function view(attribute) {
|
||||
if(m.sourceToEntity[attribute]) {
|
||||
ensureVisible(attribute);
|
||||
return;
|
||||
}
|
||||
|
||||
var attrLabel = attribute.isOutput ? "" : attribute.fullName.replace(attribute.node.name, "");
|
||||
var section = attribute.node.label;
|
||||
// add file to the internal ListModel
|
||||
m.mediaModel.append(makeElement({
|
||||
"label": section + attrLabel,
|
||||
"section": section,
|
||||
"attribute": attribute,
|
||||
}));
|
||||
}
|
||||
|
||||
function remove(index) {
|
||||
// remove corresponding entry from model
|
||||
m.mediaModel.remove(index);
|
||||
}
|
||||
|
||||
/// Get entity based on source
|
||||
function entity(source) {
|
||||
return sourceToEntity[source];
|
||||
}
|
||||
|
||||
function entityAt(index) {
|
||||
return instantiator.objectAt(index);
|
||||
}
|
||||
|
||||
function solo(index) {
|
||||
for(var i=0; i<m.mediaModel.count; ++i) {
|
||||
m.mediaModel.setProperty(i, "visible", i === index);
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
m.mediaModel.clear();
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
// Cache that keeps in memory the last unloaded 3D media
|
||||
MediaCache {
|
||||
id: cache
|
||||
}
|
||||
|
||||
NodeInstantiator {
|
||||
id: instantiator
|
||||
model: m.mediaModel
|
||||
|
||||
delegate: MediaLoader {
|
||||
id: mediaLoader
|
||||
|
||||
// explicitely store some attached model properties for outside use and ease binding
|
||||
readonly property var attribute: model.attribute
|
||||
readonly property int idx: index
|
||||
readonly property var modelSource: attribute || model.source
|
||||
|
||||
// multi-step binding to ensure MediaLoader source is properly
|
||||
// updated when needed, whether raw source is valid or not
|
||||
|
||||
// raw source path
|
||||
readonly property string rawSource: attribute ? attribute.value : model.source
|
||||
// whether dependencies are statified (applies for output/connected input attributes only)
|
||||
readonly property bool dependencyReady: {
|
||||
if(attribute && attribute.isOutput)
|
||||
return attribute.node.globalStatus === "SUCCESS";
|
||||
if(attribute && attribute.isLink)
|
||||
return attribute.linkParam.node.globalStatus === "SUCCESS";
|
||||
return true;
|
||||
}
|
||||
// source based on raw source + dependency status
|
||||
readonly property string currentSource: dependencyReady ? rawSource : ""
|
||||
// source based on currentSource + "requested" property
|
||||
readonly property string finalSource: model.requested ? currentSource : ""
|
||||
|
||||
renderMode: root.renderMode
|
||||
enabled: model.visible
|
||||
|
||||
// QObject.destroyed signal is not accessible
|
||||
// Use the object as NodeInstantiator model to be notified of its deletion
|
||||
NodeInstantiator {
|
||||
model: attribute
|
||||
delegate: Entity { objectName: "DestructionWatcher [" + attribute.toString() + "]" }
|
||||
onObjectRemoved: remove(idx)
|
||||
}
|
||||
|
||||
function updateModelAndCache(forceRequest) {
|
||||
// don't cache explicitely unloaded media
|
||||
if(model.requested && object && dependencyReady) {
|
||||
// cache current object
|
||||
if(cache.add(Filepath.urlToString(mediaLoader.source), object));
|
||||
object = null;
|
||||
}
|
||||
// update model's source path if input is an attribute
|
||||
if(attribute) {
|
||||
model.source = rawSource;
|
||||
}
|
||||
// auto-restore entity if raw source is in cache
|
||||
model.requested = forceRequest || (!model.valid && model.requested) || cache.contains(rawSource);
|
||||
model.valid = Filepath.exists(rawSource) && dependencyReady;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// keep 'source' -> 'entity' reference
|
||||
m.sourceToEntity[modelSource] = mediaLoader;
|
||||
// always request media loading when delegate has been created
|
||||
updateModelAndCache(true);
|
||||
// if external media failed to open, remove element from model
|
||||
if(!attribute && !object)
|
||||
remove(index)
|
||||
}
|
||||
|
||||
onCurrentSourceChanged: updateModelAndCache()
|
||||
|
||||
onFinalSourceChanged: {
|
||||
var cachedObject = cache.pop(rawSource);
|
||||
cached = cachedObject !== undefined;
|
||||
if(cached) {
|
||||
object = cachedObject;
|
||||
object.parent = mediaLoader;
|
||||
}
|
||||
mediaLoader.source = Filepath.stringToUrl(finalSource);
|
||||
if(object) {
|
||||
// bind media info to corresponding model roles
|
||||
// (test for object validity to avoid error messages right after object has been deleted)
|
||||
var boundProperties = ["vertexCount", "faceCount", "cameraCount", "textureCount"];
|
||||
boundProperties.forEach( function(prop){
|
||||
model[prop] = Qt.binding(function() { return object ? object[prop] : 0; });
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
model.status = status
|
||||
// remove model entry for external media that failed to load
|
||||
if(status === SceneLoader.Error && !model.attribute)
|
||||
remove(index);
|
||||
}
|
||||
|
||||
components: [
|
||||
ObjectPicker {
|
||||
enabled: parent.enabled && pickingEnabled
|
||||
hoverEnabled: false
|
||||
onPressed: root.pressed(pick)
|
||||
}
|
||||
]
|
||||
}
|
||||
onObjectRemoved: {
|
||||
delete m.sourceToEntity[object.modelSource];
|
||||
}
|
||||
}
|
||||
}
|
179
meshroom/ui/qml/Viewer3D/MediaLoader.qml
Normal file
179
meshroom/ui/qml/Viewer3D/MediaLoader.qml
Normal file
|
@ -0,0 +1,179 @@
|
|||
import QtQuick 2.9
|
||||
import Qt3D.Core 2.1
|
||||
import Qt3D.Render 2.1
|
||||
import Qt3D.Extras 2.1
|
||||
import QtQuick.Scene3D 2.0
|
||||
import "Materials"
|
||||
import Utils 1.0
|
||||
|
||||
|
||||
/**
|
||||
* MediaLoader provides a single entry point for 3D media loading.
|
||||
* It encapsulates all available plugins/loaders.
|
||||
*/
|
||||
Entity {
|
||||
id: root
|
||||
|
||||
property url source
|
||||
property bool loading: false
|
||||
property int status: SceneLoader.None
|
||||
property var object: null
|
||||
property int renderMode
|
||||
|
||||
property bool cached: false
|
||||
|
||||
onSourceChanged: {
|
||||
if(cached) {
|
||||
root.status = SceneLoader.Ready;
|
||||
return;
|
||||
}
|
||||
|
||||
// clear previously created objet if any
|
||||
if(object) {
|
||||
object.destroy();
|
||||
object = null;
|
||||
}
|
||||
|
||||
var component = undefined;
|
||||
status = SceneLoader.Loading;
|
||||
|
||||
if(!Filepath.exists(source)) {
|
||||
status = SceneLoader.None;
|
||||
return;
|
||||
}
|
||||
|
||||
switch(Filepath.extension(source)) {
|
||||
case ".abc": if(Viewer3DSettings.supportAlembic) component = abcLoaderEntityComponent; break;
|
||||
case ".exr": if(Viewer3DSettings.supportDepthMap) component = depthMapLoaderComponent; break;
|
||||
case ".obj":
|
||||
default: component = sceneLoaderEntityComponent; break;
|
||||
}
|
||||
|
||||
// Media loader available
|
||||
if(component) {
|
||||
object = component.createObject(root, {"source": source});
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: sceneLoaderEntityComponent
|
||||
MediaLoaderEntity {
|
||||
id: sceneLoaderEntity
|
||||
objectName: "SceneLoader"
|
||||
|
||||
components: [
|
||||
SceneLoader {
|
||||
source: parent.source
|
||||
onStatusChanged: {
|
||||
if(status == SceneLoader.Ready) {
|
||||
textureCount = sceneLoaderPostProcess(sceneLoaderEntity);
|
||||
faceCount = Scene3DHelper.faceCount(sceneLoaderEntity)
|
||||
}
|
||||
root.status = status;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: abcLoaderEntityComponent
|
||||
MediaLoaderEntity {
|
||||
id: abcLoaderEntity
|
||||
Component.onCompleted: {
|
||||
|
||||
var obj = Viewer3DSettings.abcLoaderComp.createObject(abcLoaderEntity, {
|
||||
'source': source,
|
||||
'pointSize': Qt.binding(function() { return 0.01 * Viewer3DSettings.pointSize }),
|
||||
'locatorScale': 0.3
|
||||
});
|
||||
|
||||
obj.statusChanged.connect(function() {
|
||||
if(obj.status === SceneLoader.Ready) {
|
||||
for(var i = 0; i < obj.pointClouds.length; ++i) {
|
||||
vertexCount += Scene3DHelper.vertexCount(obj.pointClouds[i]);
|
||||
}
|
||||
cameraCount = obj.spawnCameraSelectors();
|
||||
}
|
||||
root.status = obj.status;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: depthMapLoaderComponent
|
||||
MediaLoaderEntity {
|
||||
id: depthMapLoaderEntity
|
||||
Component.onCompleted: {
|
||||
var obj = Viewer3DSettings.depthMapLoaderComp.createObject(depthMapLoaderEntity, {
|
||||
'source': source
|
||||
});
|
||||
faceCount = Scene3DHelper.faceCount(obj);
|
||||
root.status = SceneLoader.Ready;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: materialSwitcherComponent
|
||||
MaterialSwitcher { }
|
||||
}
|
||||
|
||||
// Remove automatically created DiffuseMapMaterial and
|
||||
// instantiate a MaterialSwitcher instead. Returns the faceCount
|
||||
function sceneLoaderPostProcess(rootEntity)
|
||||
{
|
||||
var materials = Scene3DHelper.findChildrenByProperty(rootEntity, "diffuse");
|
||||
var entities = [];
|
||||
var texCount = 0;
|
||||
materials.forEach(function(mat){
|
||||
entities.push(mat.parent);
|
||||
})
|
||||
|
||||
entities.forEach(function(entity) {
|
||||
var mats = [];
|
||||
var componentsToRemove = [];
|
||||
// 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
|
||||
}
|
||||
texCount++;
|
||||
mats.push(m)
|
||||
componentsToRemove.push(comp);
|
||||
}
|
||||
|
||||
if(comp.toString().indexOf("QPhongMaterial") > -1) {
|
||||
// create MaterialSwitcher with default colors
|
||||
mats.push({})
|
||||
componentsToRemove.push(comp);
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
})
|
||||
|
||||
// remove replaced components
|
||||
componentsToRemove.forEach(function(comp){
|
||||
Scene3DHelper.removeComponent(entity, comp);
|
||||
});
|
||||
})
|
||||
return texCount;
|
||||
}
|
||||
}
|
20
meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml
Normal file
20
meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml
Normal file
|
@ -0,0 +1,20 @@
|
|||
import QtQuick 2.9
|
||||
import Qt3D.Core 2.1
|
||||
|
||||
|
||||
/**
|
||||
* MediaLoaderEntity provides a unified interface for accessing statistics
|
||||
* of a 3D media independently from the way it was loaded.
|
||||
*/
|
||||
Entity {
|
||||
property url source
|
||||
|
||||
/// Number of vertices
|
||||
property int vertexCount
|
||||
/// Number of faces
|
||||
property int faceCount
|
||||
/// Number of cameras
|
||||
property int cameraCount
|
||||
/// Number of textures
|
||||
property int textureCount
|
||||
}
|
16
meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml
Normal file
16
meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml
Normal file
|
@ -0,0 +1,16 @@
|
|||
pragma Singleton
|
||||
import QtQuick 2.9
|
||||
|
||||
/**
|
||||
* Viewer3DSettings singleton gathers properties related to the 3D Viewer capabilities, state and display options.
|
||||
*/
|
||||
Item {
|
||||
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
|
||||
|
||||
// Rasterized point size
|
||||
property real pointSize: 4
|
||||
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
module Viewer3D
|
||||
|
||||
Viewer3D 1.0 Viewer3D.qml
|
||||
singleton Viewer3DSettings 1.0 Viewer3DSettings.qml
|
||||
DefaultCameraController 1.0 DefaultCameraController.qml
|
||||
Locator3D 1.0 Locator3D.qml
|
||||
Grid3D 1.0 Grid3D.qml
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue