2023-08-16 00:20:26 +10:00
|
|
|
# SPDX-FileCopyrightText: 2011-2023 Blender Authors
|
2023-06-15 13:09:04 +10:00
|
|
|
#
|
2022-02-11 09:07:11 +11:00
|
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
2011-09-22 22:51:54 +00:00
|
|
|
|
|
|
|
|
__all__ = (
|
Anim: Move "Copy Global Transform" extension to internal scripts
Move the Copy Global Transform core add-on into Blender's code.
- The entire extension was one Python file. This PR basically splits
it into two, one for operators (in `bl_operators`) and the other for
UI panels. Those panels are registered in the 3D viewport's sidebar,
which were registered in `space_view3d`, but I made the decision
here to create a new file `space_view3d_sidebar`, because the main
file is getting too large and difficult to navigate. This PR puts
the global transform panel in this file. After this is merged, I
will do refactors to move the rest of the sidebar panels here as
well.
- `AutoKeying` class was moved into `bpy_extras/anim_utils.py` so that
it's reusable and also accessible from API, since it's generally
very useful. There were discussions about putting this somewhere,
but for now, I chose against it because creating a new file would
also mean PR would have to affect documentation generation, and
would complicate things. If we want to, we can probably create a new
module in the future.
- Little tweaks to labels and descriptions. Now that they exist
outside of the add-on context, and exist without the user explicitly
enabling them, they need to be more descriptive and tell users what
they actually do. They also need to conform to Blender's GUI
guidelines. Also tried organizing files a little by grouping
objects.
- Add-on properties (which included word `addon` in the name) have
been registered in C++, on `scene.tool_settings` with `anim_`
prefix.
Pull Request: https://projects.blender.org/blender/blender/pulls/145414
2025-10-03 17:42:04 +02:00
|
|
|
"AutoKeying",
|
|
|
|
|
|
2011-09-22 22:51:54 +00:00
|
|
|
"bake_action",
|
2017-09-10 16:58:04 +10:00
|
|
|
"bake_action_objects",
|
|
|
|
|
|
|
|
|
|
"bake_action_iter",
|
|
|
|
|
"bake_action_objects_iter",
|
2024-12-02 11:29:05 +11:00
|
|
|
|
|
|
|
|
"BakeOptions",
|
2017-09-10 16:58:04 +10:00
|
|
|
)
|
2011-09-22 22:51:54 +00:00
|
|
|
|
Anim: Move "Copy Global Transform" extension to internal scripts
Move the Copy Global Transform core add-on into Blender's code.
- The entire extension was one Python file. This PR basically splits
it into two, one for operators (in `bl_operators`) and the other for
UI panels. Those panels are registered in the 3D viewport's sidebar,
which were registered in `space_view3d`, but I made the decision
here to create a new file `space_view3d_sidebar`, because the main
file is getting too large and difficult to navigate. This PR puts
the global transform panel in this file. After this is merged, I
will do refactors to move the rest of the sidebar panels here as
well.
- `AutoKeying` class was moved into `bpy_extras/anim_utils.py` so that
it's reusable and also accessible from API, since it's generally
very useful. There were discussions about putting this somewhere,
but for now, I chose against it because creating a new file would
also mean PR would have to affect documentation generation, and
would complicate things. If we want to, we can probably create a new
module in the future.
- Little tweaks to labels and descriptions. Now that they exist
outside of the add-on context, and exist without the user explicitly
enabling them, they need to be more descriptive and tell users what
they actually do. They also need to conform to Blender's GUI
guidelines. Also tried organizing files a little by grouping
objects.
- Add-on properties (which included word `addon` in the name) have
been registered in C++, on `scene.tool_settings` with `anim_`
prefix.
Pull Request: https://projects.blender.org/blender/blender/pulls/145414
2025-10-03 17:42:04 +02:00
|
|
|
import contextlib
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from typing import Iterable, Optional, Union, Iterator
|
2024-10-23 12:48:09 +11:00
|
|
|
from collections.abc import (
|
2022-12-15 09:26:40 +11:00
|
|
|
Mapping,
|
|
|
|
|
Sequence,
|
|
|
|
|
)
|
|
|
|
|
|
2025-10-07 09:15:20 +11:00
|
|
|
import bpy
|
|
|
|
|
from bpy.types import (
|
|
|
|
|
Context, Action, ActionSlot, ActionChannelbag,
|
|
|
|
|
Object, PoseBone, KeyingSet,
|
|
|
|
|
)
|
|
|
|
|
|
2023-12-29 17:59:24 +01:00
|
|
|
from rna_prop_ui import (
|
|
|
|
|
rna_idprop_value_to_python,
|
|
|
|
|
)
|
|
|
|
|
|
2024-10-23 12:48:09 +11:00
|
|
|
FCurveKey = tuple[
|
2022-12-15 09:26:40 +11:00
|
|
|
# `fcurve.data_path`.
|
|
|
|
|
str,
|
|
|
|
|
# `fcurve.array_index`.
|
|
|
|
|
int,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# List of `[frame0, value0, frame1, value1, ...]` pairs.
|
2024-10-23 12:48:09 +11:00
|
|
|
ListKeyframes = list[float]
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2022-12-05 12:54:00 +11:00
|
|
|
|
2023-09-28 16:17:27 +02:00
|
|
|
@dataclass
|
|
|
|
|
class BakeOptions:
|
|
|
|
|
only_selected: bool
|
|
|
|
|
"""Only bake selected bones."""
|
|
|
|
|
|
|
|
|
|
do_pose: bool
|
|
|
|
|
"""Bake pose channels"""
|
|
|
|
|
|
|
|
|
|
do_object: bool
|
|
|
|
|
"""Bake objects."""
|
|
|
|
|
|
|
|
|
|
do_visual_keying: bool
|
|
|
|
|
"""Use the final transformations for baking ('visual keying')."""
|
|
|
|
|
|
|
|
|
|
do_constraint_clear: bool
|
|
|
|
|
"""Remove constraints after baking."""
|
|
|
|
|
|
|
|
|
|
do_parents_clear: bool
|
|
|
|
|
"""Unparent after baking objects."""
|
|
|
|
|
|
|
|
|
|
do_clean: bool
|
|
|
|
|
"""Remove redundant keyframes after baking."""
|
|
|
|
|
|
2023-09-29 15:38:24 +02:00
|
|
|
do_location: bool
|
|
|
|
|
"""Bake location channels"""
|
|
|
|
|
|
|
|
|
|
do_rotation: bool
|
|
|
|
|
"""Bake rotation channels"""
|
|
|
|
|
|
|
|
|
|
do_scale: bool
|
|
|
|
|
"""Bake scale channels"""
|
|
|
|
|
|
|
|
|
|
do_bbone: bool
|
|
|
|
|
"""Bake b-bone channels"""
|
|
|
|
|
|
2023-12-29 17:59:24 +01:00
|
|
|
do_custom_props: bool
|
|
|
|
|
"""Bake custom properties."""
|
|
|
|
|
|
2023-09-28 16:17:27 +02:00
|
|
|
|
2025-04-08 11:10:46 +02:00
|
|
|
def action_get_channelbag_for_slot(action: Action | None, slot: ActionSlot | None) -> ActionChannelbag | None:
|
2025-02-27 14:46:36 +01:00
|
|
|
"""
|
|
|
|
|
Returns the first channelbag found for the slot.
|
|
|
|
|
In case there are multiple layers or strips they are iterated until a
|
|
|
|
|
channelbag for that slot is found. In case no matching channelbag is found, returns None.
|
|
|
|
|
"""
|
2025-04-08 11:10:46 +02:00
|
|
|
if not action or not slot:
|
|
|
|
|
# This is just for convenience so that you can call
|
|
|
|
|
# action_get_channelbag_for_slot(adt.action, adt.action_slot) and check
|
|
|
|
|
# the return value for None, without having to also check the action and
|
|
|
|
|
# the slot for None.
|
|
|
|
|
return None
|
|
|
|
|
|
2024-10-29 16:39:10 +01:00
|
|
|
for layer in action.layers:
|
|
|
|
|
for strip in layer.strips:
|
2025-02-03 20:19:00 +01:00
|
|
|
channelbag = strip.channelbag(slot)
|
2025-02-27 14:46:36 +01:00
|
|
|
if channelbag:
|
|
|
|
|
return channelbag
|
|
|
|
|
return None
|
2024-10-29 16:39:10 +01:00
|
|
|
|
|
|
|
|
|
2025-09-26 15:26:21 +02:00
|
|
|
def action_get_first_suitable_slot(action: Action | None, target_id_type: str) -> ActionSlot | None:
|
|
|
|
|
"""Return the first Slot of the given Action that's suitable for the given ID type.
|
|
|
|
|
|
|
|
|
|
Typically you should not need this function; when an Action is assigned to a
|
|
|
|
|
data-block, just use the slot that was assigned along with it.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if not action:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
slot_types = ('UNSPECIFIED', target_id_type)
|
|
|
|
|
for slot in action.slots:
|
|
|
|
|
if slot.target_id_type in slot_types:
|
|
|
|
|
return slot
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
Anim: make it easier to convert from legacy to current Action API
The changes:
1. Add `group_name` to the `channelbag.fcurves.new()` and
`action.fcurve_ensure_for_datablock()` RNA functions.
2. Add `anim_utils.action_ensure_channelbag_for_slot(action, slot)`.
3. Add `channelbag.fcurves.ensure()` RNA function.
This makes it possible to replace this legacy code:
```py
fcurve = action.fcurves.new("location", index=2, action_group="Name")
```
with this code:
```py
channelbag = action_ensure_channelbag_for_slot(action, action_slot)
fcurve = channelbag.fcurves.new("location", index=2, group_name="Name")
```
or replace this legacy code:
```py
fcurve = action.fcurves.find("location", index=2, action_group="Name")
if not fcurve:
fcurve = action.fcurves.new("location", index=2, action_group="Name")
```
with this code:
```py
channelbag = action_ensure_channelbag_for_slot(action, action_slot)
fcurve = channelbag.fcurves.ensure("location", index=2, group_name="Name")
```
Note that the parameter name is different (`action_group` became
`group_name`). This clarifies that this is the name of the group, and
not a reference to the group itself.
This is part of #146586
Pull Request: https://projects.blender.org/blender/blender/pulls/146977
2025-09-30 14:43:56 +02:00
|
|
|
def action_ensure_channelbag_for_slot(action: Action, slot: ActionSlot) -> ActionChannelbag:
|
2025-10-01 23:22:42 +00:00
|
|
|
"""Ensure a layer and a keyframe strip exists, then ensure that strip has a channelbag for the slot."""
|
Anim: make it easier to convert from legacy to current Action API
The changes:
1. Add `group_name` to the `channelbag.fcurves.new()` and
`action.fcurve_ensure_for_datablock()` RNA functions.
2. Add `anim_utils.action_ensure_channelbag_for_slot(action, slot)`.
3. Add `channelbag.fcurves.ensure()` RNA function.
This makes it possible to replace this legacy code:
```py
fcurve = action.fcurves.new("location", index=2, action_group="Name")
```
with this code:
```py
channelbag = action_ensure_channelbag_for_slot(action, action_slot)
fcurve = channelbag.fcurves.new("location", index=2, group_name="Name")
```
or replace this legacy code:
```py
fcurve = action.fcurves.find("location", index=2, action_group="Name")
if not fcurve:
fcurve = action.fcurves.new("location", index=2, action_group="Name")
```
with this code:
```py
channelbag = action_ensure_channelbag_for_slot(action, action_slot)
fcurve = channelbag.fcurves.ensure("location", index=2, group_name="Name")
```
Note that the parameter name is different (`action_group` became
`group_name`). This clarifies that this is the name of the group, and
not a reference to the group itself.
This is part of #146586
Pull Request: https://projects.blender.org/blender/blender/pulls/146977
2025-09-30 14:43:56 +02:00
|
|
|
|
2025-03-12 11:46:34 +01:00
|
|
|
try:
|
|
|
|
|
layer = action.layers[0]
|
|
|
|
|
except IndexError:
|
|
|
|
|
layer = action.layers.new("Layer")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
strip = layer.strips[0]
|
|
|
|
|
except IndexError:
|
|
|
|
|
strip = layer.strips.new(type='KEYFRAME')
|
|
|
|
|
|
|
|
|
|
return strip.channelbag(slot, ensure=True)
|
2024-10-29 16:39:10 +01:00
|
|
|
|
|
|
|
|
|
2017-09-10 14:30:03 +10:00
|
|
|
def bake_action(
|
|
|
|
|
obj,
|
2017-09-10 16:58:04 +10:00
|
|
|
*,
|
2024-11-02 17:46:42 +11:00
|
|
|
action,
|
|
|
|
|
frames,
|
|
|
|
|
bake_options,
|
2017-09-10 16:58:04 +10:00
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
:arg obj: Object to bake.
|
|
|
|
|
:type obj: :class:`bpy.types.Object`
|
|
|
|
|
:arg action: An action to bake the data into, or None for a new action
|
|
|
|
|
to be created.
|
2024-11-03 15:42:19 +11:00
|
|
|
:type action: :class:`bpy.types.Action` | None
|
2017-09-10 16:58:04 +10:00
|
|
|
:arg frames: Frames to bake.
|
2024-11-03 15:42:19 +11:00
|
|
|
:type frames: int
|
2024-11-02 17:46:42 +11:00
|
|
|
:arg bake_options: Options for baking.
|
|
|
|
|
:type bake_options: :class:`anim_utils.BakeOptions`
|
2024-11-03 15:42:19 +11:00
|
|
|
:return: Action or None.
|
|
|
|
|
:rtype: :class:`bpy.types.Action` | None
|
2017-09-10 16:58:04 +10:00
|
|
|
"""
|
2023-09-28 16:17:27 +02:00
|
|
|
if not (bake_options.do_pose or bake_options.do_object):
|
2017-09-10 16:58:04 +10:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
action, = bake_action_objects(
|
|
|
|
|
[(obj, action)],
|
2019-01-10 13:11:48 +11:00
|
|
|
frames=frames,
|
2023-09-28 16:17:27 +02:00
|
|
|
bake_options=bake_options
|
2017-09-10 16:58:04 +10:00
|
|
|
)
|
|
|
|
|
return action
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bake_action_objects(
|
|
|
|
|
object_action_pairs,
|
|
|
|
|
*,
|
|
|
|
|
frames,
|
2024-11-02 17:46:42 +11:00
|
|
|
bake_options,
|
2017-09-10 16:58:04 +10:00
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
A version of :func:`bake_action_objects_iter` that takes frames and returns the output.
|
|
|
|
|
|
|
|
|
|
:arg frames: Frames to bake.
|
|
|
|
|
:type frames: iterable of int
|
2024-11-02 17:46:42 +11:00
|
|
|
:arg bake_options: Options for baking.
|
|
|
|
|
:type bake_options: :class:`anim_utils.BakeOptions`
|
2017-09-10 16:58:04 +10:00
|
|
|
|
2024-11-06 10:49:51 +11:00
|
|
|
:return: A sequence of Action or None types (aligned with ``object_action_pairs``)
|
2024-11-03 15:42:19 +11:00
|
|
|
:rtype: Sequence[:class:`bpy.types.Action`]
|
2017-09-10 16:58:04 +10:00
|
|
|
"""
|
2024-02-08 13:55:01 +01:00
|
|
|
if not (bake_options.do_pose or bake_options.do_object):
|
|
|
|
|
return []
|
|
|
|
|
|
2023-09-28 16:17:27 +02:00
|
|
|
iter = bake_action_objects_iter(object_action_pairs, bake_options=bake_options)
|
2017-09-10 16:58:04 +10:00
|
|
|
iter.send(None)
|
|
|
|
|
for frame in frames:
|
|
|
|
|
iter.send(frame)
|
|
|
|
|
return iter.send(None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bake_action_objects_iter(
|
|
|
|
|
object_action_pairs,
|
2024-11-02 17:46:42 +11:00
|
|
|
bake_options,
|
2017-09-10 16:58:04 +10:00
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
An coroutine that bakes actions for multiple objects.
|
|
|
|
|
|
|
|
|
|
:arg object_action_pairs: Sequence of object action tuples,
|
|
|
|
|
action is the destination for the baked data. When None a new action will be created.
|
|
|
|
|
:type object_action_pairs: Sequence of (:class:`bpy.types.Object`, :class:`bpy.types.Action`)
|
2024-11-02 17:46:42 +11:00
|
|
|
:arg bake_options: Options for baking.
|
|
|
|
|
:type bake_options: :class:`anim_utils.BakeOptions`
|
2017-09-10 16:58:04 +10:00
|
|
|
"""
|
|
|
|
|
scene = bpy.context.scene
|
|
|
|
|
frame_back = scene.frame_current
|
|
|
|
|
iter_all = tuple(
|
2023-09-28 16:17:27 +02:00
|
|
|
bake_action_iter(obj, action=action, bake_options=bake_options)
|
2017-09-10 16:58:04 +10:00
|
|
|
for (obj, action) in object_action_pairs
|
|
|
|
|
)
|
|
|
|
|
for iter in iter_all:
|
|
|
|
|
iter.send(None)
|
|
|
|
|
while True:
|
|
|
|
|
frame = yield None
|
|
|
|
|
if frame is None:
|
|
|
|
|
break
|
|
|
|
|
scene.frame_set(frame)
|
2019-05-17 10:40:44 +02:00
|
|
|
bpy.context.view_layer.update()
|
2017-09-10 16:58:04 +10:00
|
|
|
for iter in iter_all:
|
|
|
|
|
iter.send(frame)
|
|
|
|
|
scene.frame_set(frame_back)
|
|
|
|
|
yield tuple(iter.send(None) for iter in iter_all)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# XXX visual keying is actually always considered as True in this code...
|
|
|
|
|
def bake_action_iter(
|
|
|
|
|
obj,
|
|
|
|
|
*,
|
|
|
|
|
action,
|
2024-11-02 17:46:42 +11:00
|
|
|
bake_options,
|
2017-09-10 14:30:03 +10:00
|
|
|
):
|
2011-09-22 22:51:54 +00:00
|
|
|
"""
|
2017-09-10 16:58:04 +10:00
|
|
|
An coroutine that bakes action for a single object.
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2017-09-10 14:30:03 +10:00
|
|
|
:arg obj: Object to bake.
|
|
|
|
|
:type obj: :class:`bpy.types.Object`
|
2017-09-10 16:58:04 +10:00
|
|
|
:arg action: An action to bake the data into, or None for a new action
|
|
|
|
|
to be created.
|
2024-11-03 15:42:19 +11:00
|
|
|
:type action: :class:`bpy.types.Action` | None
|
2023-09-28 16:17:27 +02:00
|
|
|
:arg bake_options: Boolean options of what to include into the action bake.
|
2024-11-02 17:46:42 +11:00
|
|
|
:type bake_options: :class:`anim_utils.BakeOptions`
|
2011-09-26 15:39:15 +00:00
|
|
|
|
2011-09-22 22:51:54 +00:00
|
|
|
:return: an action or None
|
|
|
|
|
:rtype: :class:`bpy.types.Action`
|
|
|
|
|
"""
|
|
|
|
|
# -------------------------------------------------------------------------
|
2012-12-28 13:34:19 +00:00
|
|
|
# Helper Functions and vars
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2017-11-20 01:32:03 +13:00
|
|
|
# Note: BBONE_PROPS is a list so we can preserve the ordering
|
|
|
|
|
BBONE_PROPS = [
|
2022-12-15 09:26:40 +11:00
|
|
|
"bbone_curveinx", "bbone_curveoutx",
|
|
|
|
|
"bbone_curveinz", "bbone_curveoutz",
|
|
|
|
|
"bbone_rollin", "bbone_rollout",
|
|
|
|
|
"bbone_scalein", "bbone_scaleout",
|
2022-12-15 17:24:23 +11:00
|
|
|
"bbone_easein", "bbone_easeout",
|
2017-11-20 01:32:03 +13:00
|
|
|
]
|
2022-11-24 11:26:17 -08:00
|
|
|
BBONE_PROPS_LENGTHS = {
|
|
|
|
|
"bbone_curveinx": 1,
|
|
|
|
|
"bbone_curveoutx": 1,
|
|
|
|
|
"bbone_curveinz": 1,
|
|
|
|
|
"bbone_curveoutz": 1,
|
|
|
|
|
"bbone_rollin": 1,
|
|
|
|
|
"bbone_rollout": 1,
|
|
|
|
|
"bbone_scalein": 3,
|
|
|
|
|
"bbone_scaleout": 3,
|
|
|
|
|
"bbone_easein": 1,
|
|
|
|
|
"bbone_easeout": 1,
|
|
|
|
|
}
|
2017-11-20 01:32:03 +13:00
|
|
|
|
Fix #117988: Anim, Crash when baking action
When baking custom properties, avoid keeping references to custom
property values that are known to be impossible to animate anyway.
The crash was caused by custom properties containing collections of ID
properties. Keeping Python references around for too long and then
accessing them caused Blender to crash.
My solution is to only keep track of custom property values that might
be keyable. For some this is certain: floats, ints, bools are keyable,
whereas lists, dicts, etc. are not. However, strings can be the RNA
value for an enum property, which is keyed via its integer
representation. So, the new function `can_be_keyed()` can return `True`,
`False`, or `None`. By skipping those values where it returns `False`
the crash is already resolved, making it good enough for now.
Pull Request: https://projects.blender.org/blender/blender/pulls/117993
2024-02-08 15:40:55 +01:00
|
|
|
def can_be_keyed(value):
|
|
|
|
|
"""Returns a tri-state boolean.
|
|
|
|
|
|
|
|
|
|
- True: known to be keyable.
|
|
|
|
|
- False: known to not be keyable.
|
|
|
|
|
- None: unknown, might be an enum property for which RNA uses a string to
|
|
|
|
|
indicate a specific item (keyable) or an actual string property (not
|
|
|
|
|
keyable).
|
|
|
|
|
"""
|
|
|
|
|
if isinstance(value, (int, float, bool)):
|
|
|
|
|
# These types are certainly keyable.
|
|
|
|
|
return True
|
|
|
|
|
if isinstance(value, (list, tuple, set, dict)):
|
|
|
|
|
# These types are certainly not keyable.
|
|
|
|
|
return False
|
|
|
|
|
# Maybe this could be made stricter, as also ID pointer properties and
|
|
|
|
|
# some other types cannot be keyed. However, the above checks are enough
|
|
|
|
|
# to fix the crash that this code was written for (#117988).
|
|
|
|
|
return None
|
|
|
|
|
|
2023-12-29 17:59:24 +01:00
|
|
|
# Convert rna_prop types (IDPropertyArray, etc) to python types.
|
|
|
|
|
def clean_custom_properties(obj):
|
Fix #117988: Anim, Crash when baking action
When baking custom properties, avoid keeping references to custom
property values that are known to be impossible to animate anyway.
The crash was caused by custom properties containing collections of ID
properties. Keeping Python references around for too long and then
accessing them caused Blender to crash.
My solution is to only keep track of custom property values that might
be keyable. For some this is certain: floats, ints, bools are keyable,
whereas lists, dicts, etc. are not. However, strings can be the RNA
value for an enum property, which is keyed via its integer
representation. So, the new function `can_be_keyed()` can return `True`,
`False`, or `None`. By skipping those values where it returns `False`
the crash is already resolved, making it good enough for now.
Pull Request: https://projects.blender.org/blender/blender/pulls/117993
2024-02-08 15:40:55 +01:00
|
|
|
if not bake_options.do_custom_props:
|
|
|
|
|
# Don't bother remembering any custom properties when they're not
|
|
|
|
|
# going to be baked anyway.
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
# Be careful about which properties to actually consider for baking, as
|
2024-02-10 22:35:35 +11:00
|
|
|
# keeping references to complex Blender data-structures around for too long
|
Fix #117988: Anim, Crash when baking action
When baking custom properties, avoid keeping references to custom
property values that are known to be impossible to animate anyway.
The crash was caused by custom properties containing collections of ID
properties. Keeping Python references around for too long and then
accessing them caused Blender to crash.
My solution is to only keep track of custom property values that might
be keyable. For some this is certain: floats, ints, bools are keyable,
whereas lists, dicts, etc. are not. However, strings can be the RNA
value for an enum property, which is keyed via its integer
representation. So, the new function `can_be_keyed()` can return `True`,
`False`, or `None`. By skipping those values where it returns `False`
the crash is already resolved, making it good enough for now.
Pull Request: https://projects.blender.org/blender/blender/pulls/117993
2024-02-08 15:40:55 +01:00
|
|
|
# can cause crashes. See #117988.
|
2023-12-29 17:59:24 +01:00
|
|
|
clean_props = {
|
|
|
|
|
key: rna_idprop_value_to_python(value)
|
|
|
|
|
for key, value in obj.items()
|
Fix #117988: Anim, Crash when baking action
When baking custom properties, avoid keeping references to custom
property values that are known to be impossible to animate anyway.
The crash was caused by custom properties containing collections of ID
properties. Keeping Python references around for too long and then
accessing them caused Blender to crash.
My solution is to only keep track of custom property values that might
be keyable. For some this is certain: floats, ints, bools are keyable,
whereas lists, dicts, etc. are not. However, strings can be the RNA
value for an enum property, which is keyed via its integer
representation. So, the new function `can_be_keyed()` can return `True`,
`False`, or `None`. By skipping those values where it returns `False`
the crash is already resolved, making it good enough for now.
Pull Request: https://projects.blender.org/blender/blender/pulls/117993
2024-02-08 15:40:55 +01:00
|
|
|
if can_be_keyed(value) is not False
|
2023-12-29 17:59:24 +01:00
|
|
|
}
|
|
|
|
|
return clean_props
|
|
|
|
|
|
|
|
|
|
def bake_custom_properties(obj, *, custom_props, frame, group_name=""):
|
2024-10-16 10:42:32 +02:00
|
|
|
import idprop
|
2023-12-29 17:59:24 +01:00
|
|
|
if frame is None or not custom_props:
|
|
|
|
|
return
|
|
|
|
|
for key, value in custom_props.items():
|
2024-05-10 17:03:52 +02:00
|
|
|
if key in obj.bl_rna.properties and not obj.bl_rna.properties[key].is_animatable:
|
|
|
|
|
continue
|
2024-10-16 10:42:32 +02:00
|
|
|
if isinstance(obj[key], idprop.types.IDPropertyGroup):
|
|
|
|
|
continue
|
2023-12-29 17:59:24 +01:00
|
|
|
obj[key] = value
|
2025-03-06 15:51:44 +01:00
|
|
|
# The check for `is_runtime` is needed in case the custom property has the same
|
|
|
|
|
# name as a built in property, e.g. `scale`. In that case the simple check
|
|
|
|
|
# `key in ...` would be true and the square brackets would never get added.
|
|
|
|
|
if key in obj.bl_rna.properties and obj.bl_rna.properties[key].is_runtime:
|
2024-05-10 17:03:52 +02:00
|
|
|
rna_path = key
|
|
|
|
|
else:
|
2024-10-02 15:42:47 +10:00
|
|
|
rna_path = "[\"{:s}\"]".format(bpy.utils.escape_identifier(key))
|
2023-12-29 17:59:24 +01:00
|
|
|
try:
|
2024-05-10 17:03:52 +02:00
|
|
|
obj.keyframe_insert(rna_path, frame=frame, group=group_name)
|
2023-12-29 17:59:24 +01:00
|
|
|
except TypeError:
|
2024-05-10 17:03:52 +02:00
|
|
|
# The is_animatable check above is per property. A property in isolation
|
|
|
|
|
# may be considered animatable, but it could be owned by a data-block that
|
|
|
|
|
# itself cannot be animated.
|
2023-12-29 17:59:24 +01:00
|
|
|
continue
|
|
|
|
|
|
2015-02-17 07:16:59 +11:00
|
|
|
def pose_frame_info(obj):
|
2012-12-28 13:34:19 +00:00
|
|
|
matrix = {}
|
2017-11-20 01:32:03 +13:00
|
|
|
bbones = {}
|
2023-12-29 17:59:24 +01:00
|
|
|
custom_props = {}
|
2012-12-28 13:34:19 +00:00
|
|
|
for name, pbone in obj.pose.bones.items():
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_visual_keying:
|
2012-12-28 13:34:19 +00:00
|
|
|
# Get the final transform of the bone in its own local space...
|
2025-10-12 03:31:31 +00:00
|
|
|
matrix[name] = obj.convert_space(
|
|
|
|
|
pose_bone=pbone, matrix=pbone.matrix,
|
|
|
|
|
from_space='POSE', to_space='LOCAL',
|
|
|
|
|
)
|
2012-12-28 13:34:19 +00:00
|
|
|
else:
|
|
|
|
|
matrix[name] = pbone.matrix_basis.copy()
|
2017-11-20 01:32:03 +13:00
|
|
|
|
|
|
|
|
# Bendy Bones
|
|
|
|
|
if pbone.bone.bbone_segments > 1:
|
2018-07-03 06:27:53 +02:00
|
|
|
bbones[name] = {bb_prop: getattr(pbone, bb_prop) for bb_prop in BBONE_PROPS}
|
2023-12-29 17:59:24 +01:00
|
|
|
|
|
|
|
|
# Custom Properties
|
|
|
|
|
custom_props[name] = clean_custom_properties(pbone)
|
|
|
|
|
|
|
|
|
|
return matrix, bbones, custom_props
|
|
|
|
|
|
|
|
|
|
def armature_frame_info(obj):
|
|
|
|
|
if obj.type != 'ARMATURE':
|
|
|
|
|
return {}
|
|
|
|
|
return clean_custom_properties(obj)
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_parents_clear:
|
|
|
|
|
if bake_options.do_visual_keying:
|
2015-02-17 07:16:59 +11:00
|
|
|
def obj_frame_info(obj):
|
2023-12-29 17:59:24 +01:00
|
|
|
return obj.matrix_world.copy(), clean_custom_properties(obj)
|
2015-02-17 07:16:59 +11:00
|
|
|
else:
|
|
|
|
|
def obj_frame_info(obj):
|
|
|
|
|
parent = obj.parent
|
|
|
|
|
matrix = obj.matrix_basis
|
|
|
|
|
if parent:
|
2023-12-29 17:59:24 +01:00
|
|
|
return parent.matrix_world @ matrix, clean_custom_properties(obj)
|
2015-02-17 07:16:59 +11:00
|
|
|
else:
|
2023-12-29 17:59:24 +01:00
|
|
|
return matrix.copy(), clean_custom_properties(obj)
|
2015-02-03 15:41:34 +01:00
|
|
|
else:
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_visual_keying:
|
2015-02-17 07:16:59 +11:00
|
|
|
def obj_frame_info(obj):
|
|
|
|
|
parent = obj.parent
|
|
|
|
|
matrix = obj.matrix_world
|
|
|
|
|
if parent:
|
2023-12-29 17:59:24 +01:00
|
|
|
return parent.matrix_world.inverted_safe() @ matrix, clean_custom_properties(obj)
|
2015-02-17 07:16:59 +11:00
|
|
|
else:
|
2023-12-29 17:59:24 +01:00
|
|
|
return matrix.copy(), clean_custom_properties(obj)
|
2015-02-17 07:16:59 +11:00
|
|
|
else:
|
|
|
|
|
def obj_frame_info(obj):
|
2023-12-29 17:59:24 +01:00
|
|
|
return obj.matrix_basis.copy(), clean_custom_properties(obj)
|
2011-09-22 22:51:54 +00:00
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
|
# Setup the Context
|
|
|
|
|
|
2012-12-28 13:34:19 +00:00
|
|
|
if obj.pose is None:
|
2023-09-28 16:17:27 +02:00
|
|
|
bake_options.do_pose = False
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2023-09-28 16:17:27 +02:00
|
|
|
if not (bake_options.do_pose or bake_options.do_object):
|
2017-09-10 16:58:04 +10:00
|
|
|
raise Exception("Pose and object baking is disabled, no action needed")
|
2011-09-22 22:51:54 +00:00
|
|
|
|
|
|
|
|
pose_info = []
|
2023-12-29 17:59:24 +01:00
|
|
|
armature_info = []
|
2011-09-22 22:51:54 +00:00
|
|
|
obj_info = []
|
|
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
|
# Collect transformations
|
|
|
|
|
|
2017-09-10 16:58:04 +10:00
|
|
|
while True:
|
|
|
|
|
# Caller is responsible for setting the frame and updating the scene.
|
|
|
|
|
frame = yield None
|
|
|
|
|
|
|
|
|
|
# Signal we're done!
|
|
|
|
|
if frame is None:
|
|
|
|
|
break
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_pose:
|
2017-11-20 01:32:03 +13:00
|
|
|
pose_info.append((frame, *pose_frame_info(obj)))
|
2023-12-29 17:59:24 +01:00
|
|
|
armature_info.append((frame, armature_frame_info(obj)))
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_object:
|
2023-12-29 17:59:24 +01:00
|
|
|
obj_info.append((frame, *obj_frame_info(obj)))
|
2011-09-22 22:51:54 +00:00
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
|
# Create action
|
|
|
|
|
|
2012-02-08 04:37:37 +00:00
|
|
|
# in case animation data hasn't been created
|
2011-09-22 22:51:54 +00:00
|
|
|
atd = obj.animation_data_create()
|
2025-04-03 10:18:15 +02:00
|
|
|
old_slot_name = atd.last_slot_identifier[2:]
|
2022-11-24 11:26:17 -08:00
|
|
|
is_new_action = action is None
|
|
|
|
|
if is_new_action:
|
2011-09-22 22:51:54 +00:00
|
|
|
action = bpy.data.actions.new("Action")
|
2016-07-24 03:18:40 +02:00
|
|
|
|
2023-02-12 14:37:16 +11:00
|
|
|
# Only leave tweak mode if we actually need to modify the action (#57159)
|
2020-01-14 15:49:30 +03:00
|
|
|
if action != atd.action:
|
2023-02-12 14:37:16 +11:00
|
|
|
# Leave tweak mode before trying to modify the action (#48397)
|
2020-01-14 15:49:30 +03:00
|
|
|
if atd.use_tweak_mode:
|
|
|
|
|
atd.use_tweak_mode = False
|
|
|
|
|
atd.action = action
|
2025-03-12 11:46:34 +01:00
|
|
|
|
|
|
|
|
# A slot needs to be assigned.
|
|
|
|
|
if not atd.action_slot:
|
2025-04-03 10:18:15 +02:00
|
|
|
slot = action.slots.new(obj.id_type, old_slot_name or obj.name)
|
2025-03-12 11:46:34 +01:00
|
|
|
atd.action_slot = slot
|
2016-07-24 03:18:40 +02:00
|
|
|
|
2023-02-12 14:37:16 +11:00
|
|
|
# Baking the action only makes sense in Replace mode, so force it (#69105)
|
2020-01-14 16:04:23 +03:00
|
|
|
if not atd.use_tweak_mode:
|
|
|
|
|
atd.action_blend_type = 'REPLACE'
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2025-09-23 16:29:11 +02:00
|
|
|
# If any data is going to be baked, there will be a channelbag created, so
|
|
|
|
|
# might just as well create it now and have a clear, unambiguous reference
|
|
|
|
|
# to it. If it is created here, it will have no F-Curves, and so certain
|
|
|
|
|
# loops below will just be no-ops.
|
|
|
|
|
channelbag: ActionChannelbag = action_ensure_channelbag_for_slot(atd.action, atd.action_slot)
|
|
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
|
# Clean (store initial data)
|
|
|
|
|
if bake_options.do_clean:
|
|
|
|
|
clean_orig_data = {fcu: {p.co[1] for p in fcu.keyframe_points} for fcu in channelbag.fcurves}
|
|
|
|
|
else:
|
|
|
|
|
clean_orig_data = {}
|
|
|
|
|
|
2011-09-22 22:51:54 +00:00
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
|
# Apply transformations to action
|
|
|
|
|
|
|
|
|
|
# pose
|
2025-09-23 16:29:11 +02:00
|
|
|
lookup_fcurves = {(fcurve.data_path, fcurve.array_index): fcurve for fcurve in channelbag.fcurves}
|
2024-10-29 16:39:10 +01:00
|
|
|
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_pose:
|
2023-12-29 17:59:24 +01:00
|
|
|
for f, armature_custom_properties in armature_info:
|
2025-10-12 03:31:31 +00:00
|
|
|
bake_custom_properties(
|
|
|
|
|
obj,
|
|
|
|
|
custom_props=armature_custom_properties,
|
|
|
|
|
frame=f,
|
|
|
|
|
group_name="Armature Custom Properties"
|
|
|
|
|
)
|
2023-12-29 17:59:24 +01:00
|
|
|
|
2012-12-28 13:34:19 +00:00
|
|
|
for name, pbone in obj.pose.bones.items():
|
2025-10-10 17:29:08 +02:00
|
|
|
if bake_options.only_selected and not pbone.select:
|
2012-12-28 13:34:19 +00:00
|
|
|
continue
|
|
|
|
|
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_constraint_clear:
|
2012-12-28 13:34:19 +00:00
|
|
|
while pbone.constraints:
|
|
|
|
|
pbone.constraints.remove(pbone.constraints[0])
|
|
|
|
|
|
2023-09-03 21:35:03 +10:00
|
|
|
# Create compatible euler & quaternion rotation values.
|
2012-12-28 13:34:19 +00:00
|
|
|
euler_prev = None
|
2019-11-27 01:57:14 +11:00
|
|
|
quat_prev = None
|
2012-12-28 13:34:19 +00:00
|
|
|
|
2022-11-24 11:26:17 -08:00
|
|
|
base_fcurve_path = pbone.path_from_id() + "."
|
|
|
|
|
path_location = base_fcurve_path + "location"
|
|
|
|
|
path_quaternion = base_fcurve_path + "rotation_quaternion"
|
|
|
|
|
path_axis_angle = base_fcurve_path + "rotation_axis_angle"
|
|
|
|
|
path_euler = base_fcurve_path + "rotation_euler"
|
|
|
|
|
path_scale = base_fcurve_path + "scale"
|
|
|
|
|
paths_bbprops = [(base_fcurve_path + bbprop) for bbprop in BBONE_PROPS]
|
|
|
|
|
|
|
|
|
|
keyframes = KeyframesCo()
|
|
|
|
|
|
2023-09-29 15:38:24 +02:00
|
|
|
if bake_options.do_location:
|
|
|
|
|
keyframes.add_paths(path_location, 3)
|
|
|
|
|
if bake_options.do_rotation:
|
|
|
|
|
keyframes.add_paths(path_quaternion, 4)
|
|
|
|
|
keyframes.add_paths(path_axis_angle, 4)
|
|
|
|
|
keyframes.add_paths(path_euler, 3)
|
|
|
|
|
if bake_options.do_scale:
|
|
|
|
|
keyframes.add_paths(path_scale, 3)
|
|
|
|
|
|
|
|
|
|
if bake_options.do_bbone and pbone.bone.bbone_segments > 1:
|
2022-11-24 11:26:17 -08:00
|
|
|
for prop_name, path in zip(BBONE_PROPS, paths_bbprops):
|
|
|
|
|
keyframes.add_paths(path, BBONE_PROPS_LENGTHS[prop_name])
|
|
|
|
|
|
|
|
|
|
rotation_mode = pbone.rotation_mode
|
|
|
|
|
total_new_keys = len(pose_info)
|
2023-12-29 17:59:24 +01:00
|
|
|
for (f, matrix, bbones, custom_props) in pose_info:
|
2012-12-28 13:34:19 +00:00
|
|
|
pbone.matrix_basis = matrix[name].copy()
|
|
|
|
|
|
2023-09-29 15:38:24 +02:00
|
|
|
if bake_options.do_location:
|
|
|
|
|
keyframes.extend_co_values(path_location, 3, f, pbone.location)
|
|
|
|
|
|
|
|
|
|
if bake_options.do_rotation:
|
|
|
|
|
if rotation_mode == 'QUATERNION':
|
|
|
|
|
if quat_prev is not None:
|
|
|
|
|
quat = pbone.rotation_quaternion.copy()
|
|
|
|
|
quat.make_compatible(quat_prev)
|
|
|
|
|
pbone.rotation_quaternion = quat
|
|
|
|
|
quat_prev = quat
|
|
|
|
|
del quat
|
|
|
|
|
else:
|
|
|
|
|
quat_prev = pbone.rotation_quaternion.copy()
|
|
|
|
|
keyframes.extend_co_values(path_quaternion, 4, f, pbone.rotation_quaternion)
|
|
|
|
|
elif rotation_mode == 'AXIS_ANGLE':
|
|
|
|
|
keyframes.extend_co_values(path_axis_angle, 4, f, pbone.rotation_axis_angle)
|
|
|
|
|
else: # euler, XYZ, ZXY etc
|
|
|
|
|
if euler_prev is not None:
|
|
|
|
|
euler = pbone.matrix_basis.to_euler(pbone.rotation_mode, euler_prev)
|
|
|
|
|
pbone.rotation_euler = euler
|
|
|
|
|
del euler
|
|
|
|
|
euler_prev = pbone.rotation_euler.copy()
|
|
|
|
|
keyframes.extend_co_values(path_euler, 3, f, pbone.rotation_euler)
|
|
|
|
|
|
|
|
|
|
if bake_options.do_scale:
|
|
|
|
|
keyframes.extend_co_values(path_scale, 3, f, pbone.scale)
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2017-11-20 01:32:03 +13:00
|
|
|
# Bendy Bones
|
2023-09-29 15:38:24 +02:00
|
|
|
if bake_options.do_bbone and pbone.bone.bbone_segments > 1:
|
2017-11-20 01:32:03 +13:00
|
|
|
bbone_shape = bbones[name]
|
2022-11-24 11:26:17 -08:00
|
|
|
for prop_index, prop_name in enumerate(BBONE_PROPS):
|
|
|
|
|
prop_len = BBONE_PROPS_LENGTHS[prop_name]
|
|
|
|
|
if prop_len > 1:
|
|
|
|
|
keyframes.extend_co_values(
|
|
|
|
|
paths_bbprops[prop_index], prop_len, f, bbone_shape[prop_name]
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
keyframes.extend_co_value(
|
|
|
|
|
paths_bbprops[prop_index], f, bbone_shape[prop_name]
|
|
|
|
|
)
|
2023-12-29 17:59:24 +01:00
|
|
|
# Custom Properties
|
|
|
|
|
if bake_options.do_custom_props:
|
2023-12-30 01:01:15 +01:00
|
|
|
bake_custom_properties(pbone, custom_props=custom_props[name], frame=f, group_name=name)
|
2022-11-24 11:26:17 -08:00
|
|
|
|
|
|
|
|
if is_new_action:
|
2025-09-23 16:29:11 +02:00
|
|
|
keyframes.insert_keyframes_into_new_action(total_new_keys, channelbag, name)
|
2022-11-24 11:26:17 -08:00
|
|
|
else:
|
2024-10-29 16:39:10 +01:00
|
|
|
keyframes.insert_keyframes_into_existing_action(
|
2025-09-23 16:29:11 +02:00
|
|
|
lookup_fcurves, total_new_keys, channelbag)
|
2017-11-20 01:32:03 +13:00
|
|
|
|
2011-09-22 22:51:54 +00:00
|
|
|
# object. TODO. multiple objects
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_object:
|
|
|
|
|
if bake_options.do_constraint_clear:
|
2011-09-22 22:51:54 +00:00
|
|
|
while obj.constraints:
|
|
|
|
|
obj.constraints.remove(obj.constraints[0])
|
|
|
|
|
|
2023-09-05 10:49:20 +10:00
|
|
|
# Create compatible euler & quaternion rotations.
|
2012-08-25 12:37:15 +00:00
|
|
|
euler_prev = None
|
2019-11-27 01:57:14 +11:00
|
|
|
quat_prev = None
|
2012-08-25 12:37:15 +00:00
|
|
|
|
2022-11-24 11:26:17 -08:00
|
|
|
path_location = "location"
|
|
|
|
|
path_quaternion = "rotation_quaternion"
|
|
|
|
|
path_axis_angle = "rotation_axis_angle"
|
|
|
|
|
path_euler = "rotation_euler"
|
|
|
|
|
path_scale = "scale"
|
|
|
|
|
|
|
|
|
|
keyframes = KeyframesCo()
|
2023-09-29 15:38:24 +02:00
|
|
|
if bake_options.do_location:
|
|
|
|
|
keyframes.add_paths(path_location, 3)
|
|
|
|
|
if bake_options.do_rotation:
|
|
|
|
|
keyframes.add_paths(path_quaternion, 4)
|
|
|
|
|
keyframes.add_paths(path_axis_angle, 4)
|
|
|
|
|
keyframes.add_paths(path_euler, 3)
|
|
|
|
|
if bake_options.do_scale:
|
|
|
|
|
keyframes.add_paths(path_scale, 3)
|
2022-11-24 11:26:17 -08:00
|
|
|
|
|
|
|
|
rotation_mode = obj.rotation_mode
|
|
|
|
|
total_new_keys = len(obj_info)
|
2023-12-29 17:59:24 +01:00
|
|
|
for (f, matrix, custom_props) in obj_info:
|
2013-03-28 19:33:14 +00:00
|
|
|
name = "Action Bake" # XXX: placeholder
|
2013-01-21 02:40:51 +00:00
|
|
|
obj.matrix_basis = matrix
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2023-09-29 15:38:24 +02:00
|
|
|
if bake_options.do_location:
|
|
|
|
|
keyframes.extend_co_values(path_location, 3, f, obj.location)
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2023-09-29 15:38:24 +02:00
|
|
|
if bake_options.do_rotation:
|
|
|
|
|
if rotation_mode == 'QUATERNION':
|
|
|
|
|
if quat_prev is not None:
|
|
|
|
|
quat = obj.rotation_quaternion.copy()
|
|
|
|
|
quat.make_compatible(quat_prev)
|
|
|
|
|
obj.rotation_quaternion = quat
|
|
|
|
|
quat_prev = quat
|
|
|
|
|
del quat
|
|
|
|
|
else:
|
|
|
|
|
quat_prev = obj.rotation_quaternion.copy()
|
|
|
|
|
keyframes.extend_co_values(path_quaternion, 4, f, obj.rotation_quaternion)
|
|
|
|
|
|
|
|
|
|
elif rotation_mode == 'AXIS_ANGLE':
|
|
|
|
|
keyframes.extend_co_values(path_axis_angle, 4, f, obj.rotation_axis_angle)
|
|
|
|
|
else: # euler, XYZ, ZXY etc
|
|
|
|
|
if euler_prev is not None:
|
|
|
|
|
obj.rotation_euler = matrix.to_euler(obj.rotation_mode, euler_prev)
|
|
|
|
|
euler_prev = obj.rotation_euler.copy()
|
|
|
|
|
keyframes.extend_co_values(path_euler, 3, f, obj.rotation_euler)
|
|
|
|
|
|
|
|
|
|
if bake_options.do_scale:
|
|
|
|
|
keyframes.extend_co_values(path_scale, 3, f, obj.scale)
|
2022-11-24 11:26:17 -08:00
|
|
|
|
2023-12-29 17:59:24 +01:00
|
|
|
if bake_options.do_custom_props:
|
2023-12-30 01:01:15 +01:00
|
|
|
bake_custom_properties(obj, custom_props=custom_props, frame=f, group_name=name)
|
2023-12-29 17:59:24 +01:00
|
|
|
|
2022-11-24 11:26:17 -08:00
|
|
|
if is_new_action:
|
2025-09-23 16:29:11 +02:00
|
|
|
keyframes.insert_keyframes_into_new_action(total_new_keys, channelbag, name)
|
2022-11-24 11:26:17 -08:00
|
|
|
else:
|
2025-09-23 16:29:11 +02:00
|
|
|
keyframes.insert_keyframes_into_existing_action(lookup_fcurves, total_new_keys, channelbag)
|
2011-09-22 22:51:54 +00:00
|
|
|
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_parents_clear:
|
2013-04-11 08:42:25 +00:00
|
|
|
obj.parent = None
|
|
|
|
|
|
2011-09-22 22:51:54 +00:00
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
|
# Clean
|
|
|
|
|
|
2023-09-28 16:17:27 +02:00
|
|
|
if bake_options.do_clean:
|
2025-09-23 16:29:11 +02:00
|
|
|
for fcu in channelbag.fcurves:
|
2015-09-08 03:59:03 +10:00
|
|
|
fcu_orig_data = clean_orig_data.get(fcu, set())
|
|
|
|
|
|
2011-09-22 22:51:54 +00:00
|
|
|
keyframe_points = fcu.keyframe_points
|
|
|
|
|
i = 1
|
2015-09-08 03:59:03 +10:00
|
|
|
while i < len(keyframe_points) - 1:
|
|
|
|
|
val = keyframe_points[i].co[1]
|
|
|
|
|
|
|
|
|
|
if val in fcu_orig_data:
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
|
2011-09-22 22:51:54 +00:00
|
|
|
val_prev = keyframe_points[i - 1].co[1]
|
|
|
|
|
val_next = keyframe_points[i + 1].co[1]
|
|
|
|
|
|
|
|
|
|
if abs(val - val_prev) + abs(val - val_next) < 0.0001:
|
|
|
|
|
keyframe_points.remove(keyframe_points[i])
|
|
|
|
|
else:
|
|
|
|
|
i += 1
|
|
|
|
|
|
2017-09-10 16:58:04 +10:00
|
|
|
yield action
|
2022-11-24 11:26:17 -08:00
|
|
|
|
2022-12-05 12:54:00 +11:00
|
|
|
|
2022-11-24 11:26:17 -08:00
|
|
|
class KeyframesCo:
|
2022-12-15 09:26:40 +11:00
|
|
|
"""
|
|
|
|
|
A buffer for keyframe Co unpacked values per ``FCurveKey``. ``FCurveKeys`` are added using
|
|
|
|
|
``add_paths()``, Co values stored using extend_co_values(), then finally use
|
|
|
|
|
``insert_keyframes_into_*_action()`` for efficiently inserting keys into the F-curves.
|
2022-11-24 11:26:17 -08:00
|
|
|
|
|
|
|
|
Users are limited to one Action Group per instance.
|
|
|
|
|
"""
|
2022-12-15 09:26:40 +11:00
|
|
|
__slots__ = (
|
|
|
|
|
"keyframes_from_fcurve",
|
|
|
|
|
)
|
2022-11-24 11:26:17 -08:00
|
|
|
|
2022-12-15 09:26:40 +11:00
|
|
|
# `keyframes[(rna_path, array_index)] = list(time0,value0, time1,value1,...)`.
|
2022-11-24 11:26:17 -08:00
|
|
|
keyframes_from_fcurve: Mapping[FCurveKey, ListKeyframes]
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.keyframes_from_fcurve = {}
|
|
|
|
|
|
|
|
|
|
def add_paths(
|
|
|
|
|
self,
|
|
|
|
|
rna_path: str,
|
|
|
|
|
total_indices: int,
|
|
|
|
|
) -> None:
|
|
|
|
|
keyframes_from_fcurve = self.keyframes_from_fcurve
|
|
|
|
|
for array_index in range(0, total_indices):
|
|
|
|
|
keyframes_from_fcurve[(rna_path, array_index)] = []
|
|
|
|
|
|
|
|
|
|
def extend_co_values(
|
|
|
|
|
self,
|
|
|
|
|
rna_path: str,
|
|
|
|
|
total_indices: int,
|
|
|
|
|
frame: float,
|
|
|
|
|
values: Sequence[float],
|
|
|
|
|
) -> None:
|
|
|
|
|
keyframes_from_fcurve = self.keyframes_from_fcurve
|
|
|
|
|
for array_index in range(0, total_indices):
|
|
|
|
|
keyframes_from_fcurve[(rna_path, array_index)].extend((frame, values[array_index]))
|
|
|
|
|
|
|
|
|
|
def extend_co_value(
|
|
|
|
|
self,
|
|
|
|
|
rna_path: str,
|
|
|
|
|
frame: float,
|
|
|
|
|
value: float,
|
|
|
|
|
) -> None:
|
|
|
|
|
self.keyframes_from_fcurve[(rna_path, 0)].extend((frame, value))
|
|
|
|
|
|
|
|
|
|
def insert_keyframes_into_new_action(
|
|
|
|
|
self,
|
|
|
|
|
total_new_keys: int,
|
2025-09-23 16:29:11 +02:00
|
|
|
channelbag: ActionChannelbag,
|
|
|
|
|
group_name: str,
|
2022-11-24 11:26:17 -08:00
|
|
|
) -> None:
|
2022-12-15 09:26:40 +11:00
|
|
|
"""
|
|
|
|
|
Assumes the action is new, that it has no F-curves. Otherwise, the only difference between versions is
|
2022-11-24 11:26:17 -08:00
|
|
|
performance and implementation simplicity.
|
|
|
|
|
|
2025-09-23 16:29:11 +02:00
|
|
|
:arg group_name: Name of the Group that F-curves are added to.
|
2022-11-24 11:26:17 -08:00
|
|
|
"""
|
|
|
|
|
linear_enum_values = [
|
|
|
|
|
bpy.types.Keyframe.bl_rna.properties["interpolation"].enum_items["LINEAR"].value
|
|
|
|
|
] * total_new_keys
|
|
|
|
|
|
|
|
|
|
for fc_key, key_values in self.keyframes_from_fcurve.items():
|
|
|
|
|
if len(key_values) == 0:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
data_path, array_index = fc_key
|
2025-09-23 16:29:11 +02:00
|
|
|
keyframe_points = channelbag.fcurves.new(
|
|
|
|
|
data_path, index=array_index, group_name=group_name
|
2022-11-24 11:26:17 -08:00
|
|
|
).keyframe_points
|
|
|
|
|
|
|
|
|
|
keyframe_points.add(total_new_keys)
|
|
|
|
|
keyframe_points.foreach_set("co", key_values)
|
|
|
|
|
keyframe_points.foreach_set("interpolation", linear_enum_values)
|
|
|
|
|
|
|
|
|
|
# There's no need to do fcurve.update() because the keys are already ordered, have
|
|
|
|
|
# no duplicates and all handles are Linear.
|
|
|
|
|
|
|
|
|
|
def insert_keyframes_into_existing_action(
|
|
|
|
|
self,
|
|
|
|
|
lookup_fcurves: Mapping[FCurveKey, bpy.types.FCurve],
|
|
|
|
|
total_new_keys: int,
|
2025-09-23 16:29:11 +02:00
|
|
|
channelbag: ActionChannelbag,
|
2022-11-24 11:26:17 -08:00
|
|
|
) -> None:
|
2022-12-15 09:26:40 +11:00
|
|
|
"""
|
|
|
|
|
Assumes the action already exists, that it might already have F-curves. Otherwise, the
|
2022-11-24 11:26:17 -08:00
|
|
|
only difference between versions is performance and implementation simplicity.
|
|
|
|
|
|
2022-12-15 09:26:40 +11:00
|
|
|
:arg lookup_fcurves: : This is only used for efficiency.
|
2025-09-23 16:29:11 +02:00
|
|
|
It's a substitute for ``channelbag.fcurves.find()`` which is a potentially expensive linear search.
|
2022-11-24 11:26:17 -08:00
|
|
|
"""
|
|
|
|
|
linear_enum_values = [
|
|
|
|
|
bpy.types.Keyframe.bl_rna.properties["interpolation"].enum_items["LINEAR"].value
|
|
|
|
|
] * total_new_keys
|
|
|
|
|
|
|
|
|
|
for fc_key, key_values in self.keyframes_from_fcurve.items():
|
|
|
|
|
if len(key_values) == 0:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
fcurve = lookup_fcurves.get(fc_key, None)
|
|
|
|
|
if fcurve is None:
|
|
|
|
|
data_path, array_index = fc_key
|
2024-10-29 16:39:10 +01:00
|
|
|
fcurve = channelbag.fcurves.new(data_path, index=array_index)
|
2022-11-24 11:26:17 -08:00
|
|
|
|
|
|
|
|
keyframe_points = fcurve.keyframe_points
|
|
|
|
|
|
2022-12-15 09:12:17 +11:00
|
|
|
co_buffer = [0] * (2 * len(keyframe_points))
|
2022-11-24 11:26:17 -08:00
|
|
|
keyframe_points.foreach_get("co", co_buffer)
|
|
|
|
|
co_buffer.extend(key_values)
|
|
|
|
|
|
|
|
|
|
ipo_buffer = [None] * len(keyframe_points)
|
|
|
|
|
keyframe_points.foreach_get("interpolation", ipo_buffer)
|
|
|
|
|
ipo_buffer.extend(linear_enum_values)
|
|
|
|
|
|
|
|
|
|
# XXX: Currently baking inserts the same number of keys for all baked properties.
|
|
|
|
|
# This block of code breaks if that's no longer true since we then will not be properly
|
|
|
|
|
# initializing all the data.
|
|
|
|
|
keyframe_points.add(total_new_keys)
|
|
|
|
|
keyframe_points.foreach_set("co", co_buffer)
|
|
|
|
|
keyframe_points.foreach_set("interpolation", ipo_buffer)
|
|
|
|
|
|
2023-06-09 11:29:06 +02:00
|
|
|
# This also deduplicates keys where baked keys were inserted on the
|
|
|
|
|
# same frame as existing ones.
|
|
|
|
|
fcurve.update()
|
Anim: Move "Copy Global Transform" extension to internal scripts
Move the Copy Global Transform core add-on into Blender's code.
- The entire extension was one Python file. This PR basically splits
it into two, one for operators (in `bl_operators`) and the other for
UI panels. Those panels are registered in the 3D viewport's sidebar,
which were registered in `space_view3d`, but I made the decision
here to create a new file `space_view3d_sidebar`, because the main
file is getting too large and difficult to navigate. This PR puts
the global transform panel in this file. After this is merged, I
will do refactors to move the rest of the sidebar panels here as
well.
- `AutoKeying` class was moved into `bpy_extras/anim_utils.py` so that
it's reusable and also accessible from API, since it's generally
very useful. There were discussions about putting this somewhere,
but for now, I chose against it because creating a new file would
also mean PR would have to affect documentation generation, and
would complicate things. If we want to, we can probably create a new
module in the future.
- Little tweaks to labels and descriptions. Now that they exist
outside of the add-on context, and exist without the user explicitly
enabling them, they need to be more descriptive and tell users what
they actually do. They also need to conform to Blender's GUI
guidelines. Also tried organizing files a little by grouping
objects.
- Add-on properties (which included word `addon` in the name) have
been registered in C++, on `scene.tool_settings` with `anim_`
prefix.
Pull Request: https://projects.blender.org/blender/blender/pulls/145414
2025-10-03 17:42:04 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class AutoKeying:
|
|
|
|
|
"""Auto-keying support."""
|
|
|
|
|
|
|
|
|
|
# Use AutoKeying.keytype() or Authkeying.options() context to change those.
|
|
|
|
|
_keytype = 'KEYFRAME'
|
|
|
|
|
_force_autokey = False # Allow use without the user activating auto-keying.
|
|
|
|
|
_use_loc = True
|
|
|
|
|
_use_rot = True
|
|
|
|
|
_use_scale = True
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
|
def keytype(cls, the_keytype: str) -> Iterator[None]:
|
|
|
|
|
"""Context manager to set the key type that's inserted."""
|
|
|
|
|
default_keytype = cls._keytype
|
|
|
|
|
try:
|
|
|
|
|
cls._keytype = the_keytype
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
|
|
|
|
cls._keytype = default_keytype
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
|
def options(
|
|
|
|
|
cls,
|
|
|
|
|
*,
|
|
|
|
|
keytype: str = "",
|
|
|
|
|
use_loc: bool = True,
|
|
|
|
|
use_rot: bool = True,
|
|
|
|
|
use_scale: bool = True,
|
|
|
|
|
force_autokey: bool = False) -> Iterator[None]:
|
|
|
|
|
"""Context manager to set various keyframing options."""
|
|
|
|
|
default_keytype = cls._keytype
|
|
|
|
|
default_use_loc = cls._use_loc
|
|
|
|
|
default_use_rot = cls._use_rot
|
|
|
|
|
default_use_scale = cls._use_scale
|
|
|
|
|
default_force_autokey = cls._force_autokey
|
|
|
|
|
try:
|
|
|
|
|
cls._keytype = keytype
|
|
|
|
|
cls._use_loc = use_loc
|
|
|
|
|
cls._use_rot = use_rot
|
|
|
|
|
cls._use_scale = use_scale
|
|
|
|
|
cls._force_autokey = force_autokey
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
|
|
|
|
cls._keytype = default_keytype
|
|
|
|
|
cls._use_loc = default_use_loc
|
|
|
|
|
cls._use_rot = default_use_rot
|
|
|
|
|
cls._use_scale = default_use_scale
|
|
|
|
|
cls._force_autokey = default_force_autokey
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def keying_options(cls, context: Context) -> set[str]:
|
|
|
|
|
"""Retrieve the general keyframing options from user preferences."""
|
|
|
|
|
|
|
|
|
|
prefs = context.preferences
|
|
|
|
|
ts = context.scene.tool_settings
|
|
|
|
|
options = set()
|
|
|
|
|
|
|
|
|
|
if prefs.edit.use_visual_keying:
|
|
|
|
|
options.add('INSERTKEY_VISUAL')
|
|
|
|
|
if prefs.edit.use_keyframe_insert_needed:
|
|
|
|
|
options.add('INSERTKEY_NEEDED')
|
|
|
|
|
if ts.use_keyframe_cycle_aware:
|
|
|
|
|
options.add('INSERTKEY_CYCLE_AWARE')
|
|
|
|
|
return options
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def keying_options_from_keyingset(cls, context: Context, keyingset: KeyingSet) -> set[str]:
|
|
|
|
|
"""Retrieve the general keyframing options from user preferences."""
|
|
|
|
|
|
|
|
|
|
ts = context.scene.tool_settings
|
|
|
|
|
options = set()
|
|
|
|
|
|
|
|
|
|
if keyingset.use_insertkey_visual:
|
|
|
|
|
options.add('INSERTKEY_VISUAL')
|
|
|
|
|
if keyingset.use_insertkey_needed:
|
|
|
|
|
options.add('INSERTKEY_NEEDED')
|
|
|
|
|
if ts.use_keyframe_cycle_aware:
|
|
|
|
|
options.add('INSERTKEY_CYCLE_AWARE')
|
|
|
|
|
return options
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def autokeying_options(cls, context: Context) -> Optional[set[str]]:
|
|
|
|
|
"""Retrieve the Auto Keyframe options, or None if disabled."""
|
|
|
|
|
|
|
|
|
|
ts = context.scene.tool_settings
|
|
|
|
|
|
|
|
|
|
if not (cls._force_autokey or ts.use_keyframe_insert_auto):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
active_keyingset = context.scene.keying_sets_all.active
|
|
|
|
|
if ts.use_keyframe_insert_keyingset and active_keyingset:
|
|
|
|
|
# No support for keying sets in this function
|
|
|
|
|
raise RuntimeError("This function should not be called when there is an active keying set")
|
|
|
|
|
|
|
|
|
|
prefs = context.preferences
|
|
|
|
|
options = cls.keying_options(context)
|
|
|
|
|
|
|
|
|
|
if prefs.edit.use_keyframe_insert_available:
|
|
|
|
|
options.add('INSERTKEY_AVAILABLE')
|
|
|
|
|
if ts.auto_keying_mode == 'REPLACE_KEYS':
|
|
|
|
|
options.add('INSERTKEY_REPLACE')
|
|
|
|
|
return options
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_4d_rotlock(bone: PoseBone) -> Iterable[bool]:
|
|
|
|
|
"Retrieve the lock status for 4D rotation."
|
|
|
|
|
if bone.lock_rotations_4d:
|
|
|
|
|
return [bone.lock_rotation_w, *bone.lock_rotation]
|
2025-10-07 09:15:20 +11:00
|
|
|
return [all(bone.lock_rotation)] * 4
|
Anim: Move "Copy Global Transform" extension to internal scripts
Move the Copy Global Transform core add-on into Blender's code.
- The entire extension was one Python file. This PR basically splits
it into two, one for operators (in `bl_operators`) and the other for
UI panels. Those panels are registered in the 3D viewport's sidebar,
which were registered in `space_view3d`, but I made the decision
here to create a new file `space_view3d_sidebar`, because the main
file is getting too large and difficult to navigate. This PR puts
the global transform panel in this file. After this is merged, I
will do refactors to move the rest of the sidebar panels here as
well.
- `AutoKeying` class was moved into `bpy_extras/anim_utils.py` so that
it's reusable and also accessible from API, since it's generally
very useful. There were discussions about putting this somewhere,
but for now, I chose against it because creating a new file would
also mean PR would have to affect documentation generation, and
would complicate things. If we want to, we can probably create a new
module in the future.
- Little tweaks to labels and descriptions. Now that they exist
outside of the add-on context, and exist without the user explicitly
enabling them, they need to be more descriptive and tell users what
they actually do. They also need to conform to Blender's GUI
guidelines. Also tried organizing files a little by grouping
objects.
- Add-on properties (which included word `addon` in the name) have
been registered in C++, on `scene.tool_settings` with `anim_`
prefix.
Pull Request: https://projects.blender.org/blender/blender/pulls/145414
2025-10-03 17:42:04 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def keyframe_channels(
|
|
|
|
|
cls,
|
|
|
|
|
target: Union[Object, PoseBone],
|
|
|
|
|
options: set[str],
|
|
|
|
|
data_path: str,
|
|
|
|
|
group: str,
|
|
|
|
|
locks: Iterable[bool],
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Keyframe channels, avoiding keying locked channels."""
|
|
|
|
|
if all(locks):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not any(locks):
|
|
|
|
|
target.keyframe_insert(data_path, group=group, options=options, keytype=cls._keytype)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
for index, lock in enumerate(locks):
|
|
|
|
|
if lock:
|
|
|
|
|
continue
|
|
|
|
|
target.keyframe_insert(data_path, index=index, group=group, options=options, keytype=cls._keytype)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def key_transformation(
|
|
|
|
|
cls,
|
|
|
|
|
target: Union[Object, PoseBone],
|
|
|
|
|
options: set[str],
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Keyframe transformation properties, avoiding keying locked channels."""
|
|
|
|
|
|
|
|
|
|
is_bone = isinstance(target, PoseBone)
|
|
|
|
|
if is_bone:
|
|
|
|
|
group = target.name
|
|
|
|
|
else:
|
|
|
|
|
group = "Object Transforms"
|
|
|
|
|
|
|
|
|
|
def keyframe(data_path: str, locks: Iterable[bool]) -> None:
|
|
|
|
|
cls.keyframe_channels(target, options, data_path, group, locks)
|
|
|
|
|
|
|
|
|
|
if cls._use_loc and not (is_bone and target.bone.use_connect):
|
|
|
|
|
keyframe("location", target.lock_location)
|
|
|
|
|
|
|
|
|
|
if cls._use_rot:
|
|
|
|
|
if target.rotation_mode == 'QUATERNION':
|
|
|
|
|
keyframe("rotation_quaternion", cls.get_4d_rotlock(target))
|
|
|
|
|
elif target.rotation_mode == 'AXIS_ANGLE':
|
|
|
|
|
keyframe("rotation_axis_angle", cls.get_4d_rotlock(target))
|
|
|
|
|
else:
|
|
|
|
|
keyframe("rotation_euler", target.lock_rotation)
|
|
|
|
|
|
|
|
|
|
if cls._use_scale:
|
|
|
|
|
keyframe("scale", target.lock_scale)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def key_transformation_via_keyingset(cls,
|
|
|
|
|
context: Context,
|
|
|
|
|
target: Union[Object, PoseBone],
|
|
|
|
|
keyingset: KeyingSet) -> None:
|
|
|
|
|
"""Auto-key transformation properties with the given keying set."""
|
|
|
|
|
|
|
|
|
|
keyingset.refresh()
|
|
|
|
|
|
|
|
|
|
is_bone = isinstance(target, PoseBone)
|
|
|
|
|
options = cls.keying_options_from_keyingset(context, keyingset)
|
|
|
|
|
|
|
|
|
|
paths_to_key = {keysetpath.data_path: keysetpath for keysetpath in keyingset.paths}
|
|
|
|
|
|
|
|
|
|
def keyframe(data_path: str, locks: Iterable[bool]) -> None:
|
|
|
|
|
# Keying sets are relative to the ID.
|
|
|
|
|
full_data_path = target.path_from_id(data_path)
|
|
|
|
|
try:
|
|
|
|
|
keysetpath = paths_to_key[full_data_path]
|
|
|
|
|
except KeyError:
|
|
|
|
|
# No biggie, just means this property shouldn't be keyed.
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
match keysetpath.group_method:
|
|
|
|
|
case 'NAMED':
|
|
|
|
|
group = keysetpath.group
|
|
|
|
|
case 'KEYINGSET':
|
|
|
|
|
group = keyingset.name
|
|
|
|
|
case 'NONE', _:
|
|
|
|
|
group = ""
|
|
|
|
|
|
|
|
|
|
cls.keyframe_channels(target, options, data_path, group, locks)
|
|
|
|
|
|
|
|
|
|
if cls._use_loc and not (is_bone and target.bone.use_connect):
|
|
|
|
|
keyframe("location", target.lock_location)
|
|
|
|
|
|
|
|
|
|
if cls._use_rot:
|
|
|
|
|
if target.rotation_mode == 'QUATERNION':
|
|
|
|
|
keyframe("rotation_quaternion", cls.get_4d_rotlock(target))
|
|
|
|
|
elif target.rotation_mode == 'AXIS_ANGLE':
|
|
|
|
|
keyframe("rotation_axis_angle", cls.get_4d_rotlock(target))
|
|
|
|
|
else:
|
|
|
|
|
keyframe("rotation_euler", target.lock_rotation)
|
|
|
|
|
|
|
|
|
|
if cls._use_scale:
|
|
|
|
|
keyframe("scale", target.lock_scale)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def active_keyingset(cls, context: Context) -> KeyingSet | None:
|
|
|
|
|
"""Return the active keying set, if it should be used.
|
|
|
|
|
|
|
|
|
|
Only returns the active keying set when the auto-key settings indicate
|
|
|
|
|
it should be used, and when it is not using absolute paths (because
|
|
|
|
|
that's not supported by the Copy Global Transform add-on).
|
|
|
|
|
"""
|
|
|
|
|
ts = context.scene.tool_settings
|
|
|
|
|
if not ts.use_keyframe_insert_keyingset:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
active_keyingset = context.scene.keying_sets_all.active
|
|
|
|
|
if not active_keyingset:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
active_keyingset.refresh()
|
|
|
|
|
if active_keyingset.is_path_absolute:
|
|
|
|
|
# Absolute-path keying sets are not supported (yet?).
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
return active_keyingset
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def autokey_transformation(cls, context: Context, target: Union[Object, PoseBone]) -> None:
|
|
|
|
|
"""Auto-key transformation properties."""
|
|
|
|
|
|
|
|
|
|
# See if the active keying set should be used.
|
|
|
|
|
keyingset = cls.active_keyingset(context)
|
|
|
|
|
if keyingset:
|
|
|
|
|
cls.key_transformation_via_keyingset(context, target, keyingset)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Use regular autokeying options.
|
|
|
|
|
options = cls.autokeying_options(context)
|
|
|
|
|
if options is None:
|
|
|
|
|
return
|
|
|
|
|
cls.key_transformation(target, options)
|