[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:
Yann Lanthony 2018-11-23 21:58:30 +01:00
parent 2f50587904
commit 109b980ae5
7 changed files with 587 additions and 0 deletions

View file

@ -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
}
]
}
}
}

View 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);
});
}
}

View 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];
}
}
}

View 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;
}
}

View 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
}

View 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
}

View file

@ -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