Browse Source

Fix stero-ized sounds creating reference loops.

Blender's `bpy.ops.object.duplicate()` operator changes the underlying
object that's selected to be the duplicate while the code expected for
the underlying object to remain unchanged. This uses a lower-level ID
copy mechanism that should hopefully give the intended result.
pull/374/head
Adam Johnson 1 year ago
parent
commit
1142793d9e
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 6
      korman/exporter/convert.py
  2. 32
      korman/helpers.py
  3. 39
      korman/properties/modifiers/sound.py

6
korman/exporter/convert.py

@ -468,6 +468,12 @@ class Exporter:
def _(temporary, parent): def _(temporary, parent):
return handle_temporary(temporary.release(), parent) return handle_temporary(temporary.release(), parent)
@handle_temporary.register(bpy.types.Action)
def _(temporary, parent):
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.actions.remove))
log_msg(f"'{parent.name}': generated Action '{temporary.name}'")
return temporary
@handle_temporary.register(bpy.types.Object) @handle_temporary.register(bpy.types.Object)
def _(temporary, parent): def _(temporary, parent):
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.objects.remove)) self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.objects.remove))

32
korman/helpers.py

@ -17,6 +17,7 @@ import bmesh
import bpy import bpy
from contextlib import contextmanager from contextlib import contextmanager
import math import math
from typing import *
@contextmanager @contextmanager
def bmesh_from_object(bl): def bmesh_from_object(bl):
@ -29,26 +30,17 @@ def bmesh_from_object(bl):
finally: finally:
mesh.free() mesh.free()
@contextmanager def copy_action(source):
def duplicate_object(bl, remove_on_success=False): if source is not None and source.animation_data is not None and source.animation_data.action is not None:
with UiHelper(bpy.context): source.animation_data.action = source.animation_data.action.copy()
for i in bpy.data.objects: return source.animation_data.action
i.select = False
bl.select = True def copy_object(bl, name: Optional[str] = None):
bpy.context.scene.objects.active = bl dupe_object = bl.copy()
bpy.ops.object.duplicate() if name is not None:
dupe_object = bpy.context.active_object dupe_object.name = name
bpy.context.scene.objects.link(dupe_object)
dupe_name = dupe_object.name return dupe_object
try:
yield dupe_object
except Exception:
if dupe_name in bpy.data.objects:
bpy.data.objects.remove(dupe_object)
raise
finally:
if remove_on_success and dupe_name in bpy.data.objects:
bpy.data.objects.remove(dupe_object)
class GoodNeighbor: class GoodNeighbor:
"""Leave Things the Way You Found Them! (TM)""" """Leave Things the Way You Found Them! (TM)"""

39
korman/properties/modifiers/sound.py

@ -27,7 +27,7 @@ from ... import korlib
from .base import PlasmaModifierProperties from .base import PlasmaModifierProperties
from .physics import surface_types from .physics import surface_types
from ...exporter import ExportError from ...exporter import ExportError
from ...helpers import duplicate_object, GoodNeighbor, TemporaryCollectionItem from ...helpers import copy_action, copy_object, GoodNeighbor, TemporaryCollectionItem
from ... import idprops from ... import idprops
_randomsound_modes = { _randomsound_modes = {
@ -536,13 +536,14 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
if self.have_3d_stereo and modifiers.random_sound.enabled: if self.have_3d_stereo and modifiers.random_sound.enabled:
raise ExportError(f"{self.id_data.name}: Random Sound modifier cannot be applied to a Sound Emitter with 3D Stereo sounds.") raise ExportError(f"{self.id_data.name}: Random Sound modifier cannot be applied to a Sound Emitter with 3D Stereo sounds.")
@contextmanager
def _generate_stereized_emitter(self, exporter, bo: bpy.types.Object, channel: str, attr: str): def _generate_stereized_emitter(self, exporter, bo: bpy.types.Object, channel: str, attr: str):
# Duplicate the current sound emitter as a non-linked object so that we have all the # Duplicate the current sound emitter as a non-linked object so that we have all the
# information that the parent emitter has, but we're free to turn off things as needed. # information that the parent emitter has, but we're free to turn off things as needed.
with duplicate_object(bo) as emitter_obj: # NOTE: Don't immediately yield the copied object - we need to work on it first. If you immediately
# yield it, we get infinite recursion due to the copied object also having 3D Stereo sounds.
try:
emitter_obj = copy_object(bo, f"{bo.name}_Stereo-Ize:{channel}")
emitter_obj.location = (0.0, 0.0, 0.0) emitter_obj.location = (0.0, 0.0, 0.0)
emitter_obj.name = f"{bo.name}_Stereo-Ize:{channel}"
emitter_obj.parent = bo emitter_obj.parent = bo
# In case some bozo is using a visual mesh as a sound emitter, clear the materials # In case some bozo is using a visual mesh as a sound emitter, clear the materials
@ -567,25 +568,23 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
sound.enabled = False sound.enabled = False
# And only sound volume animations! # And only sound volume animations!
if emitter_obj.animation_data is not None and emitter_obj.animation_data.action is not None: emitter_obj_action = yield copy_action(emitter_obj)
action = emitter_obj.animation_data.action if emitter_obj_action is not None:
volume_paths = frozenset((i.path_from_id("volume") for i in soundemit_mod.sounds if i.enabled)) volume_paths = frozenset((i.path_from_id("volume") for i in soundemit_mod.sounds if i.enabled))
toasty_fcurves = [i for i, fcurve in enumerate(action.fcurves) if fcurve.data_path not in volume_paths] toasty_fcurves = [fcurve for fcurve in emitter_obj_action.fcurves if fcurve.data_path not in volume_paths]
for i in reversed(toasty_fcurves): for i in reversed(toasty_fcurves):
action.fcurves.remove(i) emitter_obj_action.fcurves.remove(i)
# Again, only sound volume animations, which are handled above. # Again, only sound volume animations, which are handled above.
emitter_obj_data = emitter_obj.data emitter_data_action = yield copy_action(emitter_obj.data)
if emitter_obj_data is not None and emitter_obj_data.animation_data is not None and emitter_obj_data.animation_data.action is not None: if emitter_data_action is not None:
emitter_obj_data.animation_data.action.fcurves.clear() emitter_data_action.fcurves.clear()
except Exception:
# Temporarily save a pointer to this generated emitter object so that the parent soundemit bpy.data.objects.remove(emitter_obj)
# modifier can redirect requests to 3D sounds to the generated emitters. raise
else:
yield emitter_obj
setattr(self, attr, emitter_obj) setattr(self, attr, emitter_obj)
try:
yield emitter_obj
finally:
self.property_unset(attr)
def _find_animation_groups(self, bo: bpy.types.Object): def _find_animation_groups(self, bo: bpy.types.Object):
is_anim_group = lambda x: ( is_anim_group = lambda x: (
@ -616,8 +615,8 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
# Find any animation groups that we're a part of - we need to be a member of those, # Find any animation groups that we're a part of - we need to be a member of those,
# or create one *if* any animations # or create one *if* any animations
yield self._generate_stereized_emitter(exporter, bo, "L", "stereize_left") yield from self._generate_stereized_emitter(exporter, bo, "L", "stereize_left")
yield self._generate_stereized_emitter(exporter, bo, "R", "stereize_right") yield from self._generate_stereized_emitter(exporter, bo, "R", "stereize_right")
# If some animation data persisted on the new emitters, then we need to make certain # If some animation data persisted on the new emitters, then we need to make certain
# that those animations are targetted by anyone trying to control us. That's an # that those animations are targetted by anyone trying to control us. That's an

Loading…
Cancel
Save