Skip to content

Commit

Permalink
Added better support for alembic export (#154)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ChristopherRemde authored Jan 31, 2022
1 parent 9c7ec16 commit d75ca03
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 56 deletions.
10 changes: 9 additions & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 21 additions & 1 deletion src/panels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
146 changes: 93 additions & 53 deletions src/stop_motion_obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import os
import re
import glob
import bmesh
from bpy.app.handlers import persistent
from .version import *

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

0 comments on commit d75ca03

Please sign in to comment.