From d75ca03e342911b52382db6620329b90c085589e Mon Sep 17 00:00:00 2001 From: Christopher Remde <39704202+ChristopherRemde@users.noreply.github.com> Date: Mon, 31 Jan 2022 15:48:49 +0100 Subject: [PATCH] Added better support for alembic export (#154) * Added better support for alembic export * Added a workaround for a crash when Depsgraph_Update_Pre is calling UpdateFrame * Fixed code format * Renamed UI naming for this feature --- src/__init__.py | 10 ++- src/panels.py | 22 ++++++- src/stop_motion_obj.py | 146 ++++++++++++++++++++++++++--------------- src/version.py | 2 +- 4 files changed, 124 insertions(+), 56 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index a81cdeb..24314af 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -25,7 +25,7 @@ "name": "Stop motion OBJ", "description": "Import a sequence of OBJ (or STL or PLY or X3D) files and display them each as a single frame of animation. This add-on also supports the .STL, .PLY, and .X3D file formats.", "author": "Justin Jensen", - "version": (2, 2, 0, "alpha.16"), + "version": (2, 2, 0, "alpha.17"), "blender": (2, 83, 0), "location": "File > Import > Mesh Sequence", "warning": "", @@ -45,9 +45,15 @@ def register(): bpy.types.Object.mesh_sequence_settings = bpy.props.PointerProperty(type=MeshSequenceSettings) bpy.app.handlers.load_post.append(initializeSequences) bpy.app.handlers.frame_change_pre.append(updateFrame) + bpy.app.handlers.frame_change_pre.append(updateFrameSingleMesh) # note: Blender tends to crash in Rendered viewport mode if we set the depsgraph_update_post instead of depsgraph_update_pre + + # Alembic exporter crashes blender when the depsgraph_update_pre function is registered. depsgraph_update_post doesn't crash it + # Is it needed? bpy.app.handlers.depsgraph_update_pre.append(updateFrame) + #Workaround, updating Single mesh in Depsgraph_Update_Pre crashes Blender during alembic support + bpy.app.handlers.depsgraph_update_post.append(updateFrameSingleMesh) bpy.utils.register_class(ReloadMeshSequence) bpy.utils.register_class(BatchShadeSmooth) bpy.utils.register_class(BatchShadeFlat) @@ -88,7 +94,9 @@ def register(): def unregister(): bpy.app.handlers.load_post.remove(initializeSequences) bpy.app.handlers.frame_change_pre.remove(updateFrame) + bpy.app.handlers.frame_change_pre.remove(updateFrameSingleMesh) bpy.app.handlers.depsgraph_update_pre.remove(updateFrame) + bpy.app.handlers.depsgraph_update_post.remove(updateFrameSingleMesh) bpy.app.handlers.render_init.remove(renderInitHandler) bpy.app.handlers.render_complete.remove(renderCompleteHandler) bpy.app.handlers.render_cancel.remove(renderCancelHandler) diff --git a/src/panels.py b/src/panels.py index 4f262f9..1fd1d8c 100644 --- a/src/panels.py +++ b/src/panels.py @@ -180,6 +180,10 @@ class SequenceImportSettings(bpy.types.PropertyGroup): name="Relative Paths", description="Store relative paths for Streaming sequences and for reloading Cached sequences", default=True) + showAsSingleMesh: bpy.props.BoolProperty( + name='Show as Single Mesh', + description='All frames will be shown in the same mesh. Useful when exporting the frames as alembic.', + default=False) @orientation_helper(axis_forward='-Z', axis_up='Y') @@ -230,7 +234,11 @@ def execute(self, context): if countMatchingFiles(dirPath, basenamePrefix, fileExtensionFromType(self.sequenceSettings.fileFormat)) > 0: # the input parameters should be stored on 'self' # create a new mesh sequence - seqObj = newMeshSequence() + if self.sequenceSettings.showAsSingleMesh: + seqObj = newMeshSequence('SingleMesh') + else: + seqObj = newMeshSequence('EmptyMesh') + global_matrix = axis_conversion(from_forward=b_axis_forward,from_up=b_axis_up).to_4x4() seqObj.matrix_world = global_matrix @@ -243,6 +251,8 @@ def execute(self, context): mss.cacheMode = self.sequenceSettings.cacheMode mss.fileFormat = self.sequenceSettings.fileFormat mss.dirPathIsRelative = self.sequenceSettings.dirPathIsRelative + mss.showAsSingleMesh = self.sequenceSettings.showAsSingleMesh + # this needs to be set to True if dirPath is supposed to be relative # once the path is made relative, it will be set to False @@ -272,6 +282,14 @@ def execute(self, context): firstMeshName = os.path.splitext(mss.meshNameArray[1].basename)[0].rstrip('._0123456789') seqObj.name = createUniqueName(firstMeshName + '_sequence', bpy.data.objects) seqObj.mesh_sequence_settings.isImported = True + + # If we import the sequence as a single mesh, the user most likey + # wants to export it as an alembic. Without a modifier attached, + # blender won't export the alembic correctly, so we add a harmless one + if mss.showAsSingleMesh: + arrayModifier = seqObj.modifiers.new(name='Array', type='ARRAY') + arrayModifier.count = 1 + else: # this filename prefix had no matching files noMatchFileNames.append(fileName) @@ -391,6 +409,8 @@ def draw(self, context): col.prop(op.sequenceSettings, "cacheMode") col.prop(op.sequenceSettings, "perFrameMaterial") col.prop(op.sequenceSettings, "dirPathIsRelative") + col.prop(op.sequenceSettings, "showAsSingleMesh") + def menu_func_import_sequence(self, context): diff --git a/src/stop_motion_obj.py b/src/stop_motion_obj.py index 372f2c3..d6592dd 100644 --- a/src/stop_motion_obj.py +++ b/src/stop_motion_obj.py @@ -23,6 +23,7 @@ import os import re import glob +import bmesh from bpy.app.handlers import persistent from .version import * @@ -78,7 +79,16 @@ def updateFrame(scene): global loadingSequenceLock if loadingSequenceLock is False: scn = bpy.context.scene - setFrameNumber(scn.frame_current) + setFrameNumber(scn.frame_current, False) + +# Workaround for a bug where Blender crashes when Depsgraph_Update_Pre calls updateFrame and a Single Mesh is used. +# Don't call this function in Depsgraph_Update_Pre. See PR #154 for more infos +@persistent +def updateFrameSingleMesh(scene): + global loadingSequenceLock + if loadingSequenceLock is False: + scn = bpy.context.scene + setFrameNumber(scn.frame_current, True) @persistent @@ -334,6 +344,13 @@ class MeshSequenceSettings(bpy.types.PropertyGroup): name='Material per Frame', default=False) + # With this option enabled, all frames will always be shown in the same mesh container. + # Also adds an array modifier, as the alembic export won't correctly work otherwise + showAsSingleMesh: bpy.props.BoolProperty( + name='Enable Alembic Export', + description='All frames will be shown in the same mesh. Recommended when exporting the frames as Alembic', + default=False) + # Whether to load the entire sequence into memory or to load meshes on-demand cacheMode: bpy.props.EnumProperty( items=[('cached', 'Cached', 'The full sequence is loaded into memory and saved in the .blend file'), @@ -430,13 +447,13 @@ def deleteLinkedMeshMaterials(mesh, maxMaterialUsers=1, maxImageUsers=0): mesh.materials.clear() -def newMeshSequence(): +def newMeshSequence(meshName): bpy.ops.object.add(type='MESH') # this new object should be the currently-selected object theObj = bpy.context.object theObj.name = 'sequence' theMesh = theObj.data - theMesh.name = createUniqueName('emptyMesh', bpy.data.meshes) + theMesh.name = createUniqueName(meshName, bpy.data.meshes) theMesh.use_fake_user = True theMesh.inMeshSequence = True @@ -497,7 +514,7 @@ def loadStreamingSequenceFromMeshFiles(obj, directory, filePrefix): if numFrames > 0: mss.loaded = True - setFrameObjStreamed(obj, bpy.context.scene.frame_current, True, False) + setFrameObjStreamed(obj, bpy.context.scene.frame_current, True, True, False) obj.select_set(state=True) return numFrames @@ -554,7 +571,7 @@ def loadSequenceFromMeshFiles(_obj, _dir, _file): mss.numMeshes = numFrames + 1 mss.numMeshesInMemory = numFrames if(numFrames > 0): - setFrameObj(_obj, bpy.context.scene.frame_current) + setFrameObj(_obj, bpy.context.scene.frame_current, True) _obj.select_set(state=True) mss.loaded = True @@ -603,7 +620,7 @@ def loadSequenceFromBlendFile(_obj): deselectAll() _obj.select_set(state=True) - setFrameObj(_obj, scn.frame_current) + setFrameObj(_obj, scn.frame_current, True) mss.loaded = True @@ -656,16 +673,16 @@ def getMeshPropFromIndex(obj, idx): return obj.mesh_sequence_settings.meshNameArray[idx] -def setFrameNumber(frameNum): +def setFrameNumber(frameNum, updateSingleMesh): for obj in bpy.data.objects: mss = obj.mesh_sequence_settings if mss.initialized is True and mss.loaded is True: cacheMode = mss.cacheMode if cacheMode == 'cached': - setFrameObj(obj, frameNum) + setFrameObj(obj, frameNum, updateSingleMesh) elif cacheMode == 'streaming': global forceMeshLoad - setFrameObjStreamed(obj, frameNum, forceLoad=forceMeshLoad, deleteMaterials=not mss.perFrameMaterial) + setFrameObjStreamed(obj, frameNum, updateSingleMesh, forceLoad=forceMeshLoad, deleteMaterials=not mss.perFrameMaterial) def getMeshIdxFromFrameNumber(_obj, frameNum): @@ -729,58 +746,81 @@ def getMeshIdxFromFrameNumber(_obj, frameNum): return finalIdx + 1 -def setFrameObj(_obj, frameNum): - # store the current mesh for grabbing the material later - prev_mesh = _obj.data - idx = getMeshIdxFromFrameNumber(_obj, frameNum) - next_mesh = getMeshFromIndex(_obj, idx) +def setFrameObj(_obj, frameNum, updateSingleMesh): + + mss = _obj.mesh_sequence_settings + + # Update single mesh frames only when we explicitly want to + if not (updateSingleMesh is False and mss.showAsSingleMesh is True): + # store the current materials for grabbing them later + prev_mesh_materials = [] + for material in _obj.data.materials: + prev_mesh_materials.append(material) - if (next_mesh != prev_mesh): - # swap the meshes - _obj.data = next_mesh + mss = _obj.mesh_sequence_settings + idx = getMeshIdxFromFrameNumber(_obj, frameNum) + next_mesh = getMeshFromIndex(_obj, idx) - if _obj.mesh_sequence_settings.perFrameMaterial is False: - # if the previous mesh had a material, copy it to the new one - if(len(prev_mesh.materials) > 0): - _obj.data.materials.clear() - for material in prev_mesh.materials: - _obj.data.materials.append(material) + swapMeshAndMaterials(_obj, next_mesh, prev_mesh_materials, mss.showAsSingleMesh) -def setFrameObjStreamed(obj, frameNum, forceLoad=False, deleteMaterials=False): +def setFrameObjStreamed(obj, frameNum, updateSingleMesh, forceLoad=False, deleteMaterials=False): + mss = obj.mesh_sequence_settings - idx = getMeshIdxFromFrameNumber(obj, frameNum) - nextMeshProp = getMeshPropFromIndex(obj, idx) - # if we want to load new meshes as needed and it's not already loaded - if nextMeshProp.inMemory is False and (mss.streamDuringPlayback is True or forceLoad is True): - importStreamedFile(obj, idx) - if deleteMaterials is True: + if not (updateSingleMesh is False and mss.showAsSingleMesh is True): + idx = getMeshIdxFromFrameNumber(obj, frameNum) + nextMeshProp = getMeshPropFromIndex(obj, idx) + + # if we want to load new meshes as needed and it's not already loaded + if nextMeshProp.inMemory is False and (mss.streamDuringPlayback is True or forceLoad is True): + importStreamedFile(obj, idx) + if deleteMaterials is True: + next_mesh = getMeshFromIndex(obj, idx) + deleteLinkedMeshMaterials(next_mesh) + + # if the mesh is in memory, show it + if nextMeshProp.inMemory is True: next_mesh = getMeshFromIndex(obj, idx) - deleteLinkedMeshMaterials(next_mesh) - - # if the mesh is in memory, show it - if nextMeshProp.inMemory is True: - next_mesh = getMeshFromIndex(obj, idx) - - # store the current mesh for grabbing the material later - prev_mesh = obj.data - if next_mesh != prev_mesh: - # swap the old one with the new one - obj.data = next_mesh - - # if we need to, copy the materials from the old one onto the new one - if obj.mesh_sequence_settings.perFrameMaterial is False: - if len(prev_mesh.materials) > 0: - obj.data.materials.clear() - for material in prev_mesh.materials: - obj.data.materials.append(material) - - if mss.cacheSize > 0 and mss.numMeshesInMemory > mss.cacheSize: - idxToDelete = nextCachedMeshToDelete(obj, idx) - if idxToDelete >= 0: - removeMeshFromCache(obj, idxToDelete) + # store the current materials for grabbing them later + prev_mesh_materials = [] + for material in obj.data.materials: + prev_mesh_materials.append(material.copy()) + + #Set meshes and materials + swapMeshAndMaterials(obj, next_mesh, prev_mesh_materials, mss.showAsSingleMesh) + + if mss.cacheSize > 0 and mss.numMeshesInMemory > mss.cacheSize: + idxToDelete = nextCachedMeshToDelete(obj, idx) + if idxToDelete >= 0: + removeMeshFromCache(obj, idxToDelete) + +def swapMeshAndMaterials(oldObject, newMesh, oldMeshMaterials, forSingleMesh): + if (newMesh != oldObject.data): + # For normal sequences we simply swap the mesh container + if(forSingleMesh is False): + oldObject.data = newMesh + + # For single mesh sequences, we need to copy the mesh data via a bmesh, so that the container stays the same + else: + bmNew = bmesh.new() + bmNew.from_mesh(newMesh) + bmNew.to_mesh(oldObject.data) + bmNew.free() + + # Also copy the materials from the previous mesh container + if(len(newMesh.materials) > 0): + oldObject.data.materials.clear() + for material in newMesh.materials: + oldObject.data.materials.append(material) + + if oldObject.mesh_sequence_settings.perFrameMaterial is False: + # If the previous mesh had a material, copy it to the new one + if(len(oldMeshMaterials) > 0): + oldObject.data.materials.clear() + for material in oldMeshMaterials: + oldObject.data.materials.append(material) def nextCachedMeshToDelete(obj, currentMeshIdx): mss = obj.mesh_sequence_settings diff --git a/src/version.py b/src/version.py index 29a7189..929d13d 100644 --- a/src/version.py +++ b/src/version.py @@ -2,5 +2,5 @@ # (major, minor, revision, development) # example dev version: (1, 2, 3, "beta.4") # example release version: (2, 3, 4) -currentScriptVersion = (2, 2, 0, "alpha.16") +currentScriptVersion = (2, 2, 0, "alpha.17") legacyScriptVersion = (2, 0, 2, "legacy")