diff --git a/.yamato/MeshSyncDCCPlugins.metafile b/.yamato/MeshSyncDCCPlugins.metafile index e412bb4a..c6425a07 100644 --- a/.yamato/MeshSyncDCCPlugins.metafile +++ b/.yamato/MeshSyncDCCPlugins.metafile @@ -4,7 +4,7 @@ platforms: - platform_type: &platform_win name: win type: Unity::VM - image: package-ci/win10:v1.20.1 + image: package-ci/win10:v4 flavor: b1.medium cmd_prefix: - platform_type: &platform_mac diff --git a/Documentation~/index.md b/Documentation~/index.md index 6f7e382b..907a6fae 100644 --- a/Documentation~/index.md +++ b/Documentation~/index.md @@ -6,17 +6,17 @@ This allows devs to immediately see how things will look in-game while modelling ## Features -| | Maya | 3ds Max | MotionBuilder | Blender | Modo | Metasequoia | -| --------------------| -------------------- | -------------------- | -------------------- | -------------------- | -------------------- | -------------------- | -| Polygon mesh sync | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Camera sync | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Light sync | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Double-sided Mesh | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Negative Scale | :small_red_triangle: | :small_red_triangle: | :small_red_triangle: | :small_red_triangle: | :small_red_triangle: | | -| Multi UV | | :heavy_check_mark: | | :heavy_check_mark: | | | -| Scene Cache Export | :heavy_check_mark: | :heavy_check_mark: | | :heavy_check_mark: | :heavy_check_mark: | | -| Non-polygon shape | | | | | | | -| Bidirectional Sync | | | | :heavy_check_mark: | | | +| | Maya | 3ds Max | MotionBuilder | Blender | +| --------------------| -------------------- | -------------------- | -------------------- | -------------------- | +| Polygon mesh sync | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Camera sync | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Light sync | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Double-sided Mesh | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Negative Scale | :small_red_triangle: | :small_red_triangle: | :small_red_triangle: | :small_red_triangle: | +| Multi UV | | :heavy_check_mark: | | :heavy_check_mark: | +| Scene Cache Export | :heavy_check_mark: | :heavy_check_mark: | | :heavy_check_mark: | +| Non-polygon shape | | | | | +| Bidirectional Sync | | | | :heavy_check_mark: | ### Caveats @@ -75,6 +75,4 @@ Alternatively, [Manual Installation](Installation.md) is also available. 2. [3ds Max](3dsMax.md) 3. [MotionBuilder](MotionBuilder.md) 4. [Blender](Blender.md) -5. [Modo](Modo.md) -6. [Metasequoia](Metasequoia.md) diff --git a/Plugins~/Src/MeshSyncClient/Include/MeshSyncClient/msInstancesManager.h b/Plugins~/Src/MeshSyncClient/Include/MeshSyncClient/msInstancesManager.h index 8cd351da..79f495c3 100644 --- a/Plugins~/Src/MeshSyncClient/Include/MeshSyncClient/msInstancesManager.h +++ b/Plugins~/Src/MeshSyncClient/Include/MeshSyncClient/msInstancesManager.h @@ -15,9 +15,10 @@ struct InstancesManagerRecord { bool dirtyInstances = false; bool dirtyMesh = false; - InstanceInfoPtr instances = nullptr; TransformPtr entity = nullptr; bool updated = false; + std::map updatedParents; + std::map> instancesPerParent; }; /// diff --git a/Plugins~/Src/MeshSyncClient/msInstancesManager.cpp b/Plugins~/Src/MeshSyncClient/msInstancesManager.cpp index 06dff7a9..a4abfa05 100644 --- a/Plugins~/Src/MeshSyncClient/msInstancesManager.cpp +++ b/Plugins~/Src/MeshSyncClient/msInstancesManager.cpp @@ -15,13 +15,17 @@ namespace ms { } return ret; } + vector InstancesManager::getDirtyInstances() { vector ret; for (auto& p : m_records) { InstancesManagerRecord& r = p.second; if (r.dirtyInstances) { - ret.push_back(r.instances); + for (auto& instances : r.instancesPerParent) + { + ret.insert(ret.end(), instances.second.begin(), instances.second.end()); + } } } return ret; @@ -38,6 +42,7 @@ namespace ms { r.dirtyInstances = false; r.dirtyMesh = false; r.updated = false; + r.updatedParents.clear(); } m_deleted.clear(); @@ -60,13 +65,25 @@ namespace ms { void InstancesManager::add(InstanceInfoPtr info) { auto& rec = lockAndGet(info->path); + + for (int i = 0; i < rec.instancesPerParent[info->parent_path].size(); ++i) + { + const auto& instance = rec.instancesPerParent[info->parent_path][i]; + + if (instance->parent_path == info->parent_path) + { + // Remove instance from the list, the new one will be added below: + rec.instancesPerParent[info->parent_path].erase(rec.instancesPerParent[info->parent_path].begin() + i); - if (m_always_mark_dirty || rec.instances == nullptr) { - rec.dirtyInstances = true; + break; + } } - rec.updated = true; - rec.instances = info; + // instanceInfos need to be sent every update because it's how the server knows which instances are still alive: + rec.dirtyInstances = true; + + rec.instancesPerParent[info->parent_path].push_back(info); + rec.updatedParents[info->parent_path] = true; } void InstancesManager::clear() @@ -85,13 +102,34 @@ namespace ms { { for (auto it = m_records.begin(); it != m_records.end(); ) { if (!it->second.updated) { - if (it->second.instances) { - m_deleted.push_back(it->second.instances->getIdentifier()); - } m_records.erase(it++); } else + { + // Remove records to parents that were not updated: + auto& parentMap = it->second.instancesPerParent; + auto& updatedParents = it->second.updatedParents; + + for (auto parentMap_it = parentMap.cbegin(), nextParentMap_it = parentMap_it; + parentMap_it != parentMap.cend(); + parentMap_it = nextParentMap_it) + { + ++nextParentMap_it; + + auto& instances = parentMap_it->second; + + for (auto& instance : instances) + { + auto& parentPath = instance->parent_path; + if (!updatedParents[parentPath]) + { + parentMap.erase(parentPath); + } + } + } + ++it; + } } } } diff --git a/Plugins~/Src/MeshSyncClientBlender/msblenContext.cpp b/Plugins~/Src/MeshSyncClientBlender/msblenContext.cpp index 1fdd0a35..e3fda4fc 100644 --- a/Plugins~/Src/MeshSyncClientBlender/msblenContext.cpp +++ b/Plugins~/Src/MeshSyncClientBlender/msblenContext.cpp @@ -1796,6 +1796,22 @@ void msblenContext::flushPendingList(msblenContextState& state, msblenContextPat } } +void removeExistingByPath(std::vector& listToFilter, std::vector sceneList) +{ + for (size_t i = 0; i < listToFilter.size(); i++) + { + for (size_t j = 0; j < sceneList.size(); j++) + { + if (listToFilter[i]->path == sceneList[j]->path) + { + listToFilter.erase(listToFilter.begin() + i); + i--; + break; + } + } + } +} + void msblenContext::WaitAndKickAsyncExport() { m_asyncTasksController.Wait(); @@ -1841,7 +1857,16 @@ void msblenContext::WaitAndKickAsyncExport() t.instanceInfos = m_instances_manager.getDirtyInstances(); t.instanceMeshes.clear(); - deduplicateGeometry(m_instances_manager.getDirtyMeshes(), t.instanceMeshes, t.transforms); + + auto instanceMeshes = m_instances_manager.getDirtyMeshes(); + + // Remove instance meshes that already exist in scene meshes: + removeExistingByPath(instanceMeshes, t.geometries); + removeExistingByPath(instanceMeshes, t.transforms); + + std::vector duplicates; + + deduplicateGeometry(instanceMeshes, t.instanceMeshes, t.transforms, duplicates); t.propertyInfos = m_property_manager.getAllProperties(); t.animations = m_animations; @@ -1849,6 +1874,9 @@ void msblenContext::WaitAndKickAsyncExport() t.deleted_entities = m_entity_manager.getDeleted(); t.deleted_instances = m_instances_manager.getDeleted(); + // Any instanced meshes that were duplicates are now in t.transforms and no longer in t.instanceMeshes so we need to mark them as deleted from instances: + t.deleted_instances.insert(t.deleted_instances.end(), duplicates.begin(), duplicates.end()); + if (scale_factor != 1.0f) { ms::ScaleConverter cv(scale_factor); for (std::vector>::value_type& obj : t.transforms) { cv.convert(*obj); } @@ -1890,19 +1918,37 @@ void msblenContext::WaitAndKickAsyncExport() exporter->kick(); } -void msblenContext::deduplicateGeometry(const std::vector& input, std::vector& geometries, std::vector& transforms) +void msblenContext::deduplicateGeometry(const std::vector& input, + std::vector& geometries, + std::vector& transforms, + std::vector& duplicates) { std::unordered_map cache; for (auto& geometry : input) { auto checksum = geometry->checksumGeom(); auto entry = cache[checksum]; if (entry.length() > 0) { - // Create a new pointer to avoid issues with change checks - // in auto sync - auto ptr = ms::Transform::create(); - *ptr = *geometry; - ptr->reference = entry; - transforms.push_back(ptr); + bool found = false; + // If the transform is already in the list, update it: + for (auto& t : transforms) + { + if (t->path == geometry->path) + { + t->reference = entry; + found = true; + break; + } + } + + if (!found) { + // Create a new pointer to avoid issues with change checks + // in auto sync + auto ptr = ms::Transform::create(); + *ptr = *geometry; + ptr->reference = entry; + transforms.push_back(ptr); + duplicates.push_back(geometry->getIdentifier()); + } } else { cache[checksum] = geometry->path; diff --git a/Plugins~/Src/MeshSyncClientBlender/msblenContext.h b/Plugins~/Src/MeshSyncClientBlender/msblenContext.h index 59d3f310..9272ccc9 100644 --- a/Plugins~/Src/MeshSyncClientBlender/msblenContext.h +++ b/Plugins~/Src/MeshSyncClientBlender/msblenContext.h @@ -164,7 +164,7 @@ class msblenContext { void DoExportSceneCache(const std::vector& nodes); void WaitAndKickAsyncExport(); - void deduplicateGeometry(const std::vector& input, std::vector& geometries, std::vector& references); + void deduplicateGeometry(const std::vector& input, std::vector& geometries, std::vector& references, std::vector& duplicates); void deduplicateGeometry(const std::vector>& input, std::vector& geometries, std::vector& references); diff --git a/Plugins~/Src/MeshSyncClientBlender/msblenContextGeometryNodes.cpp b/Plugins~/Src/MeshSyncClientBlender/msblenContextGeometryNodes.cpp index 23d1afef..8f81f7ee 100644 --- a/Plugins~/Src/MeshSyncClientBlender/msblenContextGeometryNodes.cpp +++ b/Plugins~/Src/MeshSyncClientBlender/msblenContextGeometryNodes.cpp @@ -28,46 +28,60 @@ void msblenContext::exportInstances() { blender::BlenderPyScene scene = blender::BlenderPyScene(blender::BlenderPyContext::get().scene()); std::unordered_set scene_objects; + scene.each_objects([this, &scene_objects](Object* obj) { if (obj == nullptr || obj->data == nullptr) return; - auto id = (ID*)obj->data; + auto id = static_cast(obj->data); scene_objects.insert(id->name + 2); }); - - // Assume everything is now dirty - m_instances_state->manager.setAlwaysMarkDirty(true); - + std::unordered_map exportedTransforms; m_geometryNodeUtils.each_instanced_object( [this, &scene_objects, &exportedTransforms](blender::GeometryNodesUtils::Record& rec) { auto obj = rec.obj; + if (!rec.from_file) { auto settings = m_settings; settings.BakeModifiers = false; settings.multithreaded = false; auto transform = exportObject(*m_instances_state, m_intermediate_paths, settings, rec.obj, false, true); + + // Objects that aren't in the file should always be hidden: + transform->visibility = { false, false, false }; + transform->reset(); exportedTransforms[rec.id] = transform; } - else if (scene_objects.find(rec.name) == scene_objects.end()) { + else if (scene_objects.find(static_cast(obj->data)->name + 2) == scene_objects.end()) { auto settings = m_settings; settings.multithreaded = false; settings.BakeModifiers = false; exportedTransforms[rec.id] = exportObject(*m_instances_state, m_default_paths, settings, rec.obj, false, true); } else { - exportedTransforms[rec.id] = exportObject(*m_entities_state, m_default_paths, m_settings, rec.obj, true, true); + // The object was already synced as part of the scene + auto& sceneTransform = exportObject(*m_entities_state, m_default_paths, m_settings, rec.obj, true, true); + + if (sceneTransform) { + m_instances_state->manager.add(sceneTransform); + exportedTransforms[rec.id] = sceneTransform; + } } }, [this, &exportedTransforms](blender::GeometryNodesUtils::Record& rec) { + auto& transform = exportedTransforms[rec.id]; + + // If this transform was not exported (happens when the sync setting for lights, etc. is not enabled), skip it: + if(!transform) + return; + auto world_matrix = msblenEntityHandler::getWorldMatrix(rec.parent); auto inverse = mu::invert(world_matrix); - auto& transform = exportedTransforms[rec.id]; //parent is always part of the scene const auto& parent = exportObject(*m_entities_state, m_default_paths, m_settings, rec.parent, false); @@ -77,8 +91,7 @@ void msblenContext::exportInstances() { } else { exportInstances(transform, parent, std::move(rec.matrices), inverse, m_intermediate_paths); - } - + } }); m_geometryNodeUtils.setInstancesDirty(false); diff --git a/Plugins~/Src/MeshSyncClientBlender/msblenGeometryNodesUtils.cpp b/Plugins~/Src/MeshSyncClientBlender/msblenGeometryNodesUtils.cpp index 1915f3f6..ef4add6e 100644 --- a/Plugins~/Src/MeshSyncClientBlender/msblenGeometryNodesUtils.cpp +++ b/Plugins~/Src/MeshSyncClientBlender/msblenGeometryNodesUtils.cpp @@ -83,9 +83,7 @@ namespace blender { std::unordered_map records_by_session_id; std::unordered_map records_by_name; - // Collect object names in the file - auto ctx = blender::BlenderPyContext::get(); - auto objects = ctx.data()->objects; + // Collect object names in the scene std::unordered_set file_objects; auto get_path = [](Object* obj) { @@ -93,15 +91,14 @@ namespace blender { return string(data->name) + string(obj->id.name); }; - LISTBASE_FOREACH(Object*, obj, &objects) { - + BlenderPyScene scene = BlenderPyScene(BlenderPyContext::get().scene()); + scene.each_objects([&](Object* obj) { if (obj->data == nullptr) - continue; + return; auto path = get_path(obj); - file_objects.insert(path); - } + }); each_instance([&](Object* obj, Object* parent, float4x4 matrix) { @@ -110,43 +107,43 @@ namespace blender { //Some objects, i.e. lights, do not use a session uuid. bool useName = id->session_uuid == 0; - // An object might be sharing data with other objects, need to use the object name in keys - auto& rec = useName? records_by_name[std::string(id->name + 2) + obj->id.name] : records_by_session_id[std::to_string(id->session_uuid) + obj->id.name]; + // An object might be sharing data with other objects, need to use the object name in keys + auto& rec = useName ? records_by_name[std::string(parent->id.name) + "_" + std::string(id->name + 2) + "_" + std::string(obj->id.name + 2)] + : records_by_session_id[std::string(parent->id.name + 2) + "_" + std::to_string(id->session_uuid) + "_" + std::string(obj->id.name + 2)]; if (!rec.handled_object) { rec.handled_object = true; - rec.name = std::string(id->name + 2) + std::string(obj->id.name); + rec.name = std::string(id->name + 2) + "_" + std::string(obj->id.name + 2); rec.obj = obj; rec.parent = parent; - + rec.from_file = file_objects.find(get_path(obj)) != file_objects.end(); - rec.id = rec.name +"_" + std::to_string(id->session_uuid); + rec.id = std::string(parent->id.name + 2) + "_" + rec.name + "_" + std::to_string(id->session_uuid); obj_handler(rec); } - + rec.matrices.push_back(matrix); }); - // Export transforms - for (auto& rec : records_by_session_id) { - if (rec.second.handled_matrices) - continue; - - rec.second.handled_matrices = true; - matrix_handler(rec.second); - } + // Export transforms + for (auto& rec : records_by_session_id) { + if (rec.second.handled_matrices) + continue; - for (auto& rec : records_by_name) { - if (rec.second.handled_matrices) - continue; + rec.second.handled_matrices = true; + matrix_handler(rec.second); + } - rec.second.handled_matrices = true; - matrix_handler(rec.second); - } + for (auto& rec : records_by_name) { + if (rec.second.handled_matrices) + continue; + rec.second.handled_matrices = true; + matrix_handler(rec.second); + } } void GeometryNodesUtils::each_instance(std::function handler) @@ -162,6 +159,38 @@ namespace blender { depsgraph.object_instances_begin(&it); + // Blender returns the instances in a flattened list, which causes duplicate instances when we have nested instances. + // For example in an instance hierarchy like this: + // A + // |-B + // |-C + // |-D + // Blender would return the instances in this order: + // C on parent B + // D on parent B + // B on parent A + // C on parent A (Duplicate!) + // D on parent A (Duplicate!) + // To get around this, build a map of child instances to their parents and if we find a parent, we can skip the children of that instance. + + // Parent we're currently under: + std::string currentParent = ""; + + // Parents and their direct children: + std::map> instanceParentsToChildren; + + // Iterator to the current position of child instances: + vector::iterator currentObjectChildIterator; + + // currentObjectChildIterator cannot be null, this keeps track whether we have an iterator or not: + bool hasIterator = false; + + // Parent of the object for currentObjectChildIterator + std::string currentObjectChildIteratorParent = ""; + + // Keeps track of the parents we built a child hierarchy for. Once the parent changes, the previous parent's hierarchy is complete. + std::vector processedInstanceParents; + for (; it.valid; depsgraph.object_instances_next(&it)) { // Get the instance as a Pointer RNA. @@ -182,15 +211,62 @@ namespace blender { auto object = instance.object(); // Don't instance empties, they have no data we can use to get a session id: - if (object->type == OB_EMPTY) { + if (object->type == OB_EMPTY) continue; - } auto world_matrix = float4x4(); instance.world_matrix(&world_matrix); - + auto parent = instance.parent(); + auto parentName = msblenUtils::get_name(parent); + auto objectName = msblenUtils::get_name(object); + + if (currentParent != parentName || + objectName == currentObjectChildIteratorParent) + { + currentParent = parentName; + + if (instanceParentsToChildren.find(objectName) != instanceParentsToChildren.end()) { + hasIterator = true; + currentObjectChildIterator = instanceParentsToChildren[objectName].begin(); + currentObjectChildIteratorParent = objectName; + } + else + { + hasIterator = false; + } + } + + // build parentToChildren mapping: + if (std::find(processedInstanceParents.begin(), processedInstanceParents.end(), parentName) == processedInstanceParents.end()) { + processedInstanceParents.push_back(parentName); + } + + // If this is the current instance parent, add any instances to it as children + if (processedInstanceParents[processedInstanceParents.size() - 1] == parentName) { + instanceParentsToChildren[parentName].push_back(objectName); + } + + // If we're currently iterating over the children of an instance parent, skip this child: + if (objectName != currentObjectChildIteratorParent) { + if (hasIterator && + currentObjectChildIterator != instanceParentsToChildren[currentObjectChildIteratorParent].end()) { + if (*currentObjectChildIterator == objectName) + { + currentObjectChildIterator++; + continue; + } + + hasIterator = false; + currentObjectChildIteratorParent = ""; + } + else { + hasIterator = false; + currentParent = ""; + } + } + handler(object, parent, world_matrix); } diff --git a/Plugins~/Src/MeshSyncClientBlender/msblenMaterialsExportHelper.cpp b/Plugins~/Src/MeshSyncClientBlender/msblenMaterialsExportHelper.cpp index 76399b1e..fa061c23 100644 --- a/Plugins~/Src/MeshSyncClientBlender/msblenMaterialsExportHelper.cpp +++ b/Plugins~/Src/MeshSyncClientBlender/msblenMaterialsExportHelper.cpp @@ -331,7 +331,7 @@ void msblenMaterialsExportHelper::handleSocketValue(bNodeSocket* socket, } if (setColorHandler) { - mu::float4 colorValue; + mu::float4 colorValue = { 0,0,0,0 }; switch (socket->type) { diff --git a/Plugins~/Src/MeshSyncClientBlender/msblenModifiers.cpp b/Plugins~/Src/MeshSyncClientBlender/msblenModifiers.cpp index 39467c50..bf754e82 100644 --- a/Plugins~/Src/MeshSyncClientBlender/msblenModifiers.cpp +++ b/Plugins~/Src/MeshSyncClientBlender/msblenModifiers.cpp @@ -310,13 +310,13 @@ void msblenModifiers::importProperties(std::vector props) { // Apply returned properties: for (auto& receivedProp : props) { if (receivedProp.type == ms::PropertyInfo::Type::Int) - debug_log(Format("importing: %s: %d %d", receivedProp.name.c_str(), receivedProp.get(), receivedProp.sourceType)); + debug_log(mu::Format("importing: %s: %d %d", receivedProp.name.c_str(), receivedProp.get(), receivedProp.sourceType)); auto obj = msblenUtils::get_object_from_path(receivedProp.path); // Should never happen but just in case: if (!obj) { - debug_log(Format("cannot find object: %s", receivedProp.path.c_str())); + debug_log(mu::Format("cannot find object: %s", receivedProp.path.c_str())); continue; } diff --git a/Plugins~/Src/MeshSyncClientBlender/msblenUtils.cpp b/Plugins~/Src/MeshSyncClientBlender/msblenUtils.cpp index 817908ce..78bbbb0a 100644 --- a/Plugins~/Src/MeshSyncClientBlender/msblenUtils.cpp +++ b/Plugins~/Src/MeshSyncClientBlender/msblenUtils.cpp @@ -4,6 +4,7 @@ #include "msblenUtils.h" #include "BlenderPyObjects/BlenderPyScene.h" +#include "MeshUtils/muLog.h" namespace bl = blender; namespace msblenUtils { @@ -87,17 +88,21 @@ bool visible_in_viewport(const Object *obj) } bool visible_in_collection(LayerCollection* lc, const Object* obj) { + std::string objName = get_name(obj); + // Check if the object is in the layer collection, if it is, check if the layer is excluded: - for (auto collectionObject : blender::list_range((CollectionObject*)lc->collection->gobject.first)) { - if (collectionObject->ob == obj) { - if ((!(lc->flag & LAYER_COLLECTION_EXCLUDE)) && + if (lc->collection) { + for (auto collectionObject : blender::list_range((CollectionObject*)lc->collection->gobject.first)) { + if (get_name(collectionObject->ob) == objName) { + if ((!(lc->flag & LAYER_COLLECTION_EXCLUDE)) && #if BLENDER_VERSION >= 300 - (!(lc->collection->flag & COLLECTION_HIDE_RENDER)) + (!(lc->collection->flag & COLLECTION_HIDE_RENDER)) #else - (!(lc->collection->flag & COLLECTION_RESTRICT_RENDER)) + (!(lc->collection->flag & COLLECTION_RESTRICT_RENDER)) #endif - ) { - return true; + ) { + return true; + } } } } @@ -181,14 +186,14 @@ bool is_mesh(const Object *obj) { return obj->type == OB_MESH; } bool is_camera(const Object *obj) { return obj->type == OB_CAMERA; } bool is_light(const Object *obj) { return obj->type == OB_LAMP; } bool is_armature(const Object *obj) { return obj->type == OB_ARMATURE; } - +} #ifdef BLENDER_DEBUG_LOGS void debug_log(std::string message) { try { py::gil_scoped_acquire acquire; - auto statement = Format("print('%s')", message.c_str()); + auto statement = mu::Format("print('%s')", message.c_str()); py::eval( statement.c_str()); @@ -200,5 +205,3 @@ void debug_log(std::string message) { } } #endif - -} diff --git a/Plugins~/TestAssets/Test scenes/nested_instances_test.blend b/Plugins~/TestAssets/Test scenes/nested_instances_test.blend new file mode 100644 index 00000000..65397eb9 Binary files /dev/null and b/Plugins~/TestAssets/Test scenes/nested_instances_test.blend differ