Skip to content

Commit d75ca03

Browse files
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
1 parent 9c7ec16 commit d75ca03

File tree

4 files changed

+124
-56
lines changed

4 files changed

+124
-56
lines changed

src/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"name": "Stop motion OBJ",
2626
"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.",
2727
"author": "Justin Jensen",
28-
"version": (2, 2, 0, "alpha.16"),
28+
"version": (2, 2, 0, "alpha.17"),
2929
"blender": (2, 83, 0),
3030
"location": "File > Import > Mesh Sequence",
3131
"warning": "",
@@ -45,9 +45,15 @@ def register():
4545
bpy.types.Object.mesh_sequence_settings = bpy.props.PointerProperty(type=MeshSequenceSettings)
4646
bpy.app.handlers.load_post.append(initializeSequences)
4747
bpy.app.handlers.frame_change_pre.append(updateFrame)
48+
bpy.app.handlers.frame_change_pre.append(updateFrameSingleMesh)
4849

4950
# note: Blender tends to crash in Rendered viewport mode if we set the depsgraph_update_post instead of depsgraph_update_pre
51+
52+
# Alembic exporter crashes blender when the depsgraph_update_pre function is registered. depsgraph_update_post doesn't crash it
53+
# Is it needed?
5054
bpy.app.handlers.depsgraph_update_pre.append(updateFrame)
55+
#Workaround, updating Single mesh in Depsgraph_Update_Pre crashes Blender during alembic support
56+
bpy.app.handlers.depsgraph_update_post.append(updateFrameSingleMesh)
5157
bpy.utils.register_class(ReloadMeshSequence)
5258
bpy.utils.register_class(BatchShadeSmooth)
5359
bpy.utils.register_class(BatchShadeFlat)
@@ -88,7 +94,9 @@ def register():
8894
def unregister():
8995
bpy.app.handlers.load_post.remove(initializeSequences)
9096
bpy.app.handlers.frame_change_pre.remove(updateFrame)
97+
bpy.app.handlers.frame_change_pre.remove(updateFrameSingleMesh)
9198
bpy.app.handlers.depsgraph_update_pre.remove(updateFrame)
99+
bpy.app.handlers.depsgraph_update_post.remove(updateFrameSingleMesh)
92100
bpy.app.handlers.render_init.remove(renderInitHandler)
93101
bpy.app.handlers.render_complete.remove(renderCompleteHandler)
94102
bpy.app.handlers.render_cancel.remove(renderCancelHandler)

src/panels.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ class SequenceImportSettings(bpy.types.PropertyGroup):
180180
name="Relative Paths",
181181
description="Store relative paths for Streaming sequences and for reloading Cached sequences",
182182
default=True)
183+
showAsSingleMesh: bpy.props.BoolProperty(
184+
name='Show as Single Mesh',
185+
description='All frames will be shown in the same mesh. Useful when exporting the frames as alembic.',
186+
default=False)
183187

184188

185189
@orientation_helper(axis_forward='-Z', axis_up='Y')
@@ -230,7 +234,11 @@ def execute(self, context):
230234
if countMatchingFiles(dirPath, basenamePrefix, fileExtensionFromType(self.sequenceSettings.fileFormat)) > 0:
231235
# the input parameters should be stored on 'self'
232236
# create a new mesh sequence
233-
seqObj = newMeshSequence()
237+
if self.sequenceSettings.showAsSingleMesh:
238+
seqObj = newMeshSequence('SingleMesh')
239+
else:
240+
seqObj = newMeshSequence('EmptyMesh')
241+
234242
global_matrix = axis_conversion(from_forward=b_axis_forward,from_up=b_axis_up).to_4x4()
235243
seqObj.matrix_world = global_matrix
236244

@@ -243,6 +251,8 @@ def execute(self, context):
243251
mss.cacheMode = self.sequenceSettings.cacheMode
244252
mss.fileFormat = self.sequenceSettings.fileFormat
245253
mss.dirPathIsRelative = self.sequenceSettings.dirPathIsRelative
254+
mss.showAsSingleMesh = self.sequenceSettings.showAsSingleMesh
255+
246256

247257
# this needs to be set to True if dirPath is supposed to be relative
248258
# once the path is made relative, it will be set to False
@@ -272,6 +282,14 @@ def execute(self, context):
272282
firstMeshName = os.path.splitext(mss.meshNameArray[1].basename)[0].rstrip('._0123456789')
273283
seqObj.name = createUniqueName(firstMeshName + '_sequence', bpy.data.objects)
274284
seqObj.mesh_sequence_settings.isImported = True
285+
286+
# If we import the sequence as a single mesh, the user most likey
287+
# wants to export it as an alembic. Without a modifier attached,
288+
# blender won't export the alembic correctly, so we add a harmless one
289+
if mss.showAsSingleMesh:
290+
arrayModifier = seqObj.modifiers.new(name='Array', type='ARRAY')
291+
arrayModifier.count = 1
292+
275293
else:
276294
# this filename prefix had no matching files
277295
noMatchFileNames.append(fileName)
@@ -391,6 +409,8 @@ def draw(self, context):
391409
col.prop(op.sequenceSettings, "cacheMode")
392410
col.prop(op.sequenceSettings, "perFrameMaterial")
393411
col.prop(op.sequenceSettings, "dirPathIsRelative")
412+
col.prop(op.sequenceSettings, "showAsSingleMesh")
413+
394414

395415

396416
def menu_func_import_sequence(self, context):

src/stop_motion_obj.py

Lines changed: 93 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import os
2424
import re
2525
import glob
26+
import bmesh
2627
from bpy.app.handlers import persistent
2728
from .version import *
2829

@@ -78,7 +79,16 @@ def updateFrame(scene):
7879
global loadingSequenceLock
7980
if loadingSequenceLock is False:
8081
scn = bpy.context.scene
81-
setFrameNumber(scn.frame_current)
82+
setFrameNumber(scn.frame_current, False)
83+
84+
# Workaround for a bug where Blender crashes when Depsgraph_Update_Pre calls updateFrame and a Single Mesh is used.
85+
# Don't call this function in Depsgraph_Update_Pre. See PR #154 for more infos
86+
@persistent
87+
def updateFrameSingleMesh(scene):
88+
global loadingSequenceLock
89+
if loadingSequenceLock is False:
90+
scn = bpy.context.scene
91+
setFrameNumber(scn.frame_current, True)
8292

8393

8494
@persistent
@@ -334,6 +344,13 @@ class MeshSequenceSettings(bpy.types.PropertyGroup):
334344
name='Material per Frame',
335345
default=False)
336346

347+
# With this option enabled, all frames will always be shown in the same mesh container.
348+
# Also adds an array modifier, as the alembic export won't correctly work otherwise
349+
showAsSingleMesh: bpy.props.BoolProperty(
350+
name='Enable Alembic Export',
351+
description='All frames will be shown in the same mesh. Recommended when exporting the frames as Alembic',
352+
default=False)
353+
337354
# Whether to load the entire sequence into memory or to load meshes on-demand
338355
cacheMode: bpy.props.EnumProperty(
339356
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):
430447
mesh.materials.clear()
431448

432449

433-
def newMeshSequence():
450+
def newMeshSequence(meshName):
434451
bpy.ops.object.add(type='MESH')
435452
# this new object should be the currently-selected object
436453
theObj = bpy.context.object
437454
theObj.name = 'sequence'
438455
theMesh = theObj.data
439-
theMesh.name = createUniqueName('emptyMesh', bpy.data.meshes)
456+
theMesh.name = createUniqueName(meshName, bpy.data.meshes)
440457
theMesh.use_fake_user = True
441458
theMesh.inMeshSequence = True
442459

@@ -497,7 +514,7 @@ def loadStreamingSequenceFromMeshFiles(obj, directory, filePrefix):
497514

498515
if numFrames > 0:
499516
mss.loaded = True
500-
setFrameObjStreamed(obj, bpy.context.scene.frame_current, True, False)
517+
setFrameObjStreamed(obj, bpy.context.scene.frame_current, True, True, False)
501518
obj.select_set(state=True)
502519
return numFrames
503520

@@ -554,7 +571,7 @@ def loadSequenceFromMeshFiles(_obj, _dir, _file):
554571
mss.numMeshes = numFrames + 1
555572
mss.numMeshesInMemory = numFrames
556573
if(numFrames > 0):
557-
setFrameObj(_obj, bpy.context.scene.frame_current)
574+
setFrameObj(_obj, bpy.context.scene.frame_current, True)
558575

559576
_obj.select_set(state=True)
560577
mss.loaded = True
@@ -603,7 +620,7 @@ def loadSequenceFromBlendFile(_obj):
603620
deselectAll()
604621

605622
_obj.select_set(state=True)
606-
setFrameObj(_obj, scn.frame_current)
623+
setFrameObj(_obj, scn.frame_current, True)
607624
mss.loaded = True
608625

609626

@@ -656,16 +673,16 @@ def getMeshPropFromIndex(obj, idx):
656673
return obj.mesh_sequence_settings.meshNameArray[idx]
657674

658675

659-
def setFrameNumber(frameNum):
676+
def setFrameNumber(frameNum, updateSingleMesh):
660677
for obj in bpy.data.objects:
661678
mss = obj.mesh_sequence_settings
662679
if mss.initialized is True and mss.loaded is True:
663680
cacheMode = mss.cacheMode
664681
if cacheMode == 'cached':
665-
setFrameObj(obj, frameNum)
682+
setFrameObj(obj, frameNum, updateSingleMesh)
666683
elif cacheMode == 'streaming':
667684
global forceMeshLoad
668-
setFrameObjStreamed(obj, frameNum, forceLoad=forceMeshLoad, deleteMaterials=not mss.perFrameMaterial)
685+
setFrameObjStreamed(obj, frameNum, updateSingleMesh, forceLoad=forceMeshLoad, deleteMaterials=not mss.perFrameMaterial)
669686

670687

671688
def getMeshIdxFromFrameNumber(_obj, frameNum):
@@ -729,58 +746,81 @@ def getMeshIdxFromFrameNumber(_obj, frameNum):
729746
return finalIdx + 1
730747

731748

732-
def setFrameObj(_obj, frameNum):
733-
# store the current mesh for grabbing the material later
734-
prev_mesh = _obj.data
735-
idx = getMeshIdxFromFrameNumber(_obj, frameNum)
736-
next_mesh = getMeshFromIndex(_obj, idx)
749+
def setFrameObj(_obj, frameNum, updateSingleMesh):
750+
751+
mss = _obj.mesh_sequence_settings
752+
753+
# Update single mesh frames only when we explicitly want to
754+
if not (updateSingleMesh is False and mss.showAsSingleMesh is True):
755+
# store the current materials for grabbing them later
756+
prev_mesh_materials = []
757+
for material in _obj.data.materials:
758+
prev_mesh_materials.append(material)
737759

738-
if (next_mesh != prev_mesh):
739-
# swap the meshes
740-
_obj.data = next_mesh
760+
mss = _obj.mesh_sequence_settings
761+
idx = getMeshIdxFromFrameNumber(_obj, frameNum)
762+
next_mesh = getMeshFromIndex(_obj, idx)
741763

742-
if _obj.mesh_sequence_settings.perFrameMaterial is False:
743-
# if the previous mesh had a material, copy it to the new one
744-
if(len(prev_mesh.materials) > 0):
745-
_obj.data.materials.clear()
746-
for material in prev_mesh.materials:
747-
_obj.data.materials.append(material)
764+
swapMeshAndMaterials(_obj, next_mesh, prev_mesh_materials, mss.showAsSingleMesh)
748765

749766

750-
def setFrameObjStreamed(obj, frameNum, forceLoad=False, deleteMaterials=False):
767+
def setFrameObjStreamed(obj, frameNum, updateSingleMesh, forceLoad=False, deleteMaterials=False):
768+
751769
mss = obj.mesh_sequence_settings
752-
idx = getMeshIdxFromFrameNumber(obj, frameNum)
753-
nextMeshProp = getMeshPropFromIndex(obj, idx)
754770

755-
# if we want to load new meshes as needed and it's not already loaded
756-
if nextMeshProp.inMemory is False and (mss.streamDuringPlayback is True or forceLoad is True):
757-
importStreamedFile(obj, idx)
758-
if deleteMaterials is True:
771+
if not (updateSingleMesh is False and mss.showAsSingleMesh is True):
772+
idx = getMeshIdxFromFrameNumber(obj, frameNum)
773+
nextMeshProp = getMeshPropFromIndex(obj, idx)
774+
775+
# if we want to load new meshes as needed and it's not already loaded
776+
if nextMeshProp.inMemory is False and (mss.streamDuringPlayback is True or forceLoad is True):
777+
importStreamedFile(obj, idx)
778+
if deleteMaterials is True:
779+
next_mesh = getMeshFromIndex(obj, idx)
780+
deleteLinkedMeshMaterials(next_mesh)
781+
782+
# if the mesh is in memory, show it
783+
if nextMeshProp.inMemory is True:
759784
next_mesh = getMeshFromIndex(obj, idx)
760-
deleteLinkedMeshMaterials(next_mesh)
761-
762-
# if the mesh is in memory, show it
763-
if nextMeshProp.inMemory is True:
764-
next_mesh = getMeshFromIndex(obj, idx)
765-
766-
# store the current mesh for grabbing the material later
767-
prev_mesh = obj.data
768-
if next_mesh != prev_mesh:
769-
# swap the old one with the new one
770-
obj.data = next_mesh
771-
772-
# if we need to, copy the materials from the old one onto the new one
773-
if obj.mesh_sequence_settings.perFrameMaterial is False:
774-
if len(prev_mesh.materials) > 0:
775-
obj.data.materials.clear()
776-
for material in prev_mesh.materials:
777-
obj.data.materials.append(material)
778-
779-
if mss.cacheSize > 0 and mss.numMeshesInMemory > mss.cacheSize:
780-
idxToDelete = nextCachedMeshToDelete(obj, idx)
781-
if idxToDelete >= 0:
782-
removeMeshFromCache(obj, idxToDelete)
783785

786+
# store the current materials for grabbing them later
787+
prev_mesh_materials = []
788+
for material in obj.data.materials:
789+
prev_mesh_materials.append(material.copy())
790+
791+
#Set meshes and materials
792+
swapMeshAndMaterials(obj, next_mesh, prev_mesh_materials, mss.showAsSingleMesh)
793+
794+
if mss.cacheSize > 0 and mss.numMeshesInMemory > mss.cacheSize:
795+
idxToDelete = nextCachedMeshToDelete(obj, idx)
796+
if idxToDelete >= 0:
797+
removeMeshFromCache(obj, idxToDelete)
798+
799+
def swapMeshAndMaterials(oldObject, newMesh, oldMeshMaterials, forSingleMesh):
800+
if (newMesh != oldObject.data):
801+
# For normal sequences we simply swap the mesh container
802+
if(forSingleMesh is False):
803+
oldObject.data = newMesh
804+
805+
# For single mesh sequences, we need to copy the mesh data via a bmesh, so that the container stays the same
806+
else:
807+
bmNew = bmesh.new()
808+
bmNew.from_mesh(newMesh)
809+
bmNew.to_mesh(oldObject.data)
810+
bmNew.free()
811+
812+
# Also copy the materials from the previous mesh container
813+
if(len(newMesh.materials) > 0):
814+
oldObject.data.materials.clear()
815+
for material in newMesh.materials:
816+
oldObject.data.materials.append(material)
817+
818+
if oldObject.mesh_sequence_settings.perFrameMaterial is False:
819+
# If the previous mesh had a material, copy it to the new one
820+
if(len(oldMeshMaterials) > 0):
821+
oldObject.data.materials.clear()
822+
for material in oldMeshMaterials:
823+
oldObject.data.materials.append(material)
784824

785825
def nextCachedMeshToDelete(obj, currentMeshIdx):
786826
mss = obj.mesh_sequence_settings

src/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
# (major, minor, revision, development)
33
# example dev version: (1, 2, 3, "beta.4")
44
# example release version: (2, 3, 4)
5-
currentScriptVersion = (2, 2, 0, "alpha.16")
5+
currentScriptVersion = (2, 2, 0, "alpha.17")
66
legacyScriptVersion = (2, 0, 2, "legacy")

0 commit comments

Comments
 (0)