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):
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)
def _(temporary, parent):
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.objects.remove))

32
korman/helpers.py

@ -17,6 +17,7 @@ import bmesh
import bpy
from contextlib import contextmanager
import math
from typing import *
@contextmanager
def bmesh_from_object(bl):
@ -29,26 +30,17 @@ def bmesh_from_object(bl):
finally:
mesh.free()
@contextmanager
def duplicate_object(bl, remove_on_success=False):
with UiHelper(bpy.context):
for i in bpy.data.objects:
i.select = False
bl.select = True
bpy.context.scene.objects.active = bl
bpy.ops.object.duplicate()
dupe_object = bpy.context.active_object
dupe_name = dupe_object.name
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)
def copy_action(source):
if source is not None and source.animation_data is not None and source.animation_data.action is not None:
source.animation_data.action = source.animation_data.action.copy()
return source.animation_data.action
def copy_object(bl, name: Optional[str] = None):
dupe_object = bl.copy()
if name is not None:
dupe_object.name = name
bpy.context.scene.objects.link(dupe_object)
return dupe_object
class GoodNeighbor:
"""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 .physics import surface_types
from ...exporter import ExportError
from ...helpers import duplicate_object, GoodNeighbor, TemporaryCollectionItem
from ...helpers import copy_action, copy_object, GoodNeighbor, TemporaryCollectionItem
from ... import idprops
_randomsound_modes = {
@ -536,13 +536,14 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
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.")
@contextmanager
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
# 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.name = f"{bo.name}_Stereo-Ize:{channel}"
emitter_obj.parent = bo
# 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
# And only sound volume animations!
if emitter_obj.animation_data is not None and emitter_obj.animation_data.action is not None:
action = emitter_obj.animation_data.action
emitter_obj_action = yield copy_action(emitter_obj)
if emitter_obj_action is not None:
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):
action.fcurves.remove(i)
emitter_obj_action.fcurves.remove(i)
# Again, only sound volume animations, which are handled above.
emitter_obj_data = 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:
emitter_obj_data.animation_data.action.fcurves.clear()
# Temporarily save a pointer to this generated emitter object so that the parent soundemit
# modifier can redirect requests to 3D sounds to the generated emitters.
setattr(self, attr, emitter_obj)
try:
emitter_data_action = yield copy_action(emitter_obj.data)
if emitter_data_action is not None:
emitter_data_action.fcurves.clear()
except Exception:
bpy.data.objects.remove(emitter_obj)
raise
else:
yield emitter_obj
finally:
self.property_unset(attr)
setattr(self, attr, emitter_obj)
def _find_animation_groups(self, bo: bpy.types.Object):
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,
# or create one *if* any animations
yield 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, "L", "stereize_left")
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
# that those animations are targetted by anyone trying to control us. That's an

Loading…
Cancel
Save