Merge branch 'blender-v5.0-release'

This commit is contained in:
Weizhen Huang
2025-10-13 12:15:33 +02:00
4 changed files with 241 additions and 17 deletions

View File

@@ -1104,7 +1104,9 @@ void VolumeManager::update_step_size(const Scene *scene, DeviceScene *dscene) co
{
assert(scene->integrator->get_volume_ray_marching());
if (!dscene->volume_step_size.is_modified() && last_algorithm == RAY_MARCHING) {
if (!dscene->volume_step_size.is_modified() &&
!scene->integrator->volume_step_rate_is_modified() && last_algorithm == RAY_MARCHING)
{
return;
}

View File

@@ -657,7 +657,6 @@ static void armature_finalize_restpose(ListBase *bonelist, ListBase *editbonelis
void ED_armature_from_edit(Main *bmain, bArmature *arm)
{
EditBone *eBone, *neBone;
Bone *newBone;
Object *obt;
@@ -666,22 +665,62 @@ void ED_armature_from_edit(Main *bmain, bArmature *arm)
BKE_armature_bonelist_free(&arm->bonebase, true);
arm->act_bone = nullptr;
/* Remove zero sized bones, this gives unstable rest-poses. */
constexpr float bone_length_threshold = 0.000001f * 0.000001f;
for (eBone = static_cast<EditBone *>(arm->edbo->first); eBone; eBone = neBone) {
float len_sq = len_squared_v3v3(eBone->head, eBone->tail);
neBone = eBone->next;
if (len_sq <= bone_length_threshold) { /* FLT_EPSILON is too large? */
/* Find any bones that refer to this bone */
LISTBASE_FOREACH (EditBone *, fBone, arm->edbo) {
if (fBone->parent == eBone) {
fBone->parent = eBone->parent;
/* Avoid (almost) zero sized bones, this gives unstable rest-poses. */
{
/* If this threshold is adjusted, also update the `bl_animation_armature.py` test. */
constexpr float bone_length_threshold = 0.000001f;
constexpr float bone_length_threshold_sq = bone_length_threshold * bone_length_threshold;
constexpr float adjusted_bone_length = 2 * bone_length_threshold;
/* Build a map from parent to its children, to speed up the loop below. */
blender::Map<EditBone *, blender::VectorSet<EditBone *>> parent_to_children;
LISTBASE_FOREACH (EditBone *, eBone, arm->edbo) {
parent_to_children.lookup_or_add_default(eBone->parent).add_new(eBone);
}
LISTBASE_FOREACH (EditBone *, eBone, arm->edbo) {
const float len_sq = len_squared_v3v3(eBone->head, eBone->tail);
if (len_sq > bone_length_threshold_sq) {
continue;
}
/* Move the tail away from the head, to ensure the bone has at least some length.
* Historical note: until 5.0, Blender used to delete these bones. However, this was an issue
* with importers that assume that the bones they import actually will exist on the Armature.
* So instead, the bones are elongated a bit for numerical stability. These are very small
* adjustments, and so are unlikely to cause issues in practice. */
float offset[3];
if (len_sq == 0.0f) {
/* The bone is actually zero-length, which means it has no direction. Just pick one. */
offset[0] = 0.0f;
offset[1] = 0.0f;
offset[2] = adjusted_bone_length;
}
else {
sub_v3_v3v3(offset, eBone->tail, eBone->head);
normalize_v3_length(offset, adjusted_bone_length);
}
/* Apply this offset to the bone's tail to make it long enough for numerical stability. And
* disconnect it so that the children don't have to be updated, and can remain at their
* current location.
*
* Disconnecting the children is a lot simpler than the alternative: offsetting the children
* themselves. That would create subtle issues, for example if there are two bone chains that
* would initially exactly align, but one of them has a tiny bone; if all children were
* shifted, they would no longer align. */
add_v3_v3v3(eBone->tail, eBone->head, offset);
if (G.debug & G_DEBUG) {
printf("Warning: elongated (almost) zero sized bone: %s\n", eBone->name);
}
blender::VectorSet<EditBone *> *children = parent_to_children.lookup_ptr(eBone);
if (children) {
for (EditBone *child : *children) {
child->flag &= ~BONE_CONNECTED;
}
}
if (G.debug & G_DEBUG) {
printf("Warning: removed zero sized bone: %s\n", eBone->name);
}
bone_free(arm, eBone);
}
}

View File

@@ -1560,6 +1560,7 @@ static void node_id_remap(ID *old_id, ID *new_id, SpaceNode *snode)
if (snode->treepath.last) {
path = (bNodeTreePath *)snode->treepath.last;
snode->edittree = path->nodetree;
ED_node_set_active_viewer_key(snode);
}
else {
snode->edittree = nullptr;

View File

@@ -7,8 +7,13 @@ blender -b --factory-startup --python tests/python/bl_animation_armature.py
"""
import unittest
from typing import TypeAlias
import bpy
from mathutils import Vector
from bpy.types import EditBone, Object, Armature
Vectorish: TypeAlias = Vector | tuple[float, float, float]
class BoneCollectionTest(unittest.TestCase):
@@ -222,6 +227,183 @@ class BoneCollectionTest(unittest.TestCase):
self.assertEqual({'agent': 327}, bcolls_all['child3']['dict'].to_dict())
class ArmatureCreationTest(unittest.TestCase):
arm_ob: Object
arm: Armature
def setUp(self) -> None:
print("\033[92mloading empty homefile\033[0m")
bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
self.arm_ob, self.arm = self.create_armature()
@staticmethod
def create_armature() -> tuple[Object, Armature]:
"""Create an Armature without any bones."""
arm = bpy.data.armatures.new('Armature')
arm_ob = bpy.data.objects.new('ArmObject', arm)
# Remove any pre-existing bone.
while arm.bones:
arm.bones.remove(arm.bones[0])
bpy.context.scene.collection.objects.link(arm_ob)
bpy.context.view_layer.objects.active = arm_ob
return arm_ob, arm
def create_bone(self, name: str, parent: EditBone | None, head: Vectorish, tail: Vectorish) -> EditBone:
bone = self.arm.edit_bones.new(name)
bone.parent = parent
bone.head = head
bone.tail = tail
bone.use_connect = True
return bone
def test_tiny_bones(self) -> None:
"""Tiny bones should be elongated."""
# 'bpy.context.active_object' does not exist when Blender is running in
# GUI mode. That's not the normal way to run this test, but very useful
# to be able to do for debugging purposes.
with bpy.context.temp_override(active_object=self.arm_ob):
bpy.ops.object.mode_set(mode='EDIT')
# Constants defined in `ED_armature_from_edit()`:
bone_length_threshold = 0.000001
adjusted_bone_length = 2 * bone_length_threshold
# A value for which the vector (under_threshold, 0, under_threshold)
# is still shorter than the bone length threshold.
under_threshold = 0.0000006
root = self.create_bone("root", None, (0, 0, 0), (0, 0, 1))
tinychild_1 = self.create_bone(
"tinychild_1",
root,
root.tail,
root.tail + Vector((0, under_threshold, 0)),
)
self.create_bone(
"tinychild_2",
root,
root.tail,
root.tail + Vector((under_threshold, 0, under_threshold)),
)
self.create_bone(
"zerochild_3",
root,
root.tail,
root.tail,
)
# Give a tiny child a grandchild that is also tiny, in a perpendicular direction.
self.create_bone(
"tinygrandchild_1_1",
tinychild_1,
tinychild_1.tail,
tinychild_1.tail + Vector((under_threshold, 0, 0)),
)
# Add a grandchild that is long enough.
grandchild_1_2 = self.create_bone(
"grandchild_1_2",
tinychild_1,
tinychild_1.tail,
tinychild_1.tail + Vector((1, 0, 0)),
)
# Add a great-grandchild, it should remain connected to its parent.
self.create_bone(
"great_grandchild_1_2_1",
grandchild_1_2,
grandchild_1_2.tail,
grandchild_1_2.tail + Vector((1, 0, 0)),
)
# Switch out and back into Armature Edit mode, to see how the bones survived the round-trip.
with bpy.context.temp_override(active_object=self.arm_ob):
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='EDIT')
# Check that all bones still exist, and have the expected head/tail. This
# comparison is done again in Armature Edit mode, so that all the numbers
# mean the same thing as they meant when creating the bones.
actual_names = sorted(bone.name for bone in self.arm.edit_bones)
expect_names = sorted(["root", "tinychild_1", "tinychild_2", "zerochild_3", "tinygrandchild_1_1",
"grandchild_1_2", "great_grandchild_1_2_1"])
self.assertEqual(expect_names, actual_names)
def check_bone(
name: str,
expect_head: Vectorish,
expect_tail: Vectorish,
*,
expect_connected: bool,
msg: str,
places=7,
) -> None:
bone = self.arm.edit_bones[name]
# Convert to tuples for nicer printing in failure messages.
actual_head = bone.head.to_tuple()
actual_tail = bone.tail.to_tuple()
head_msg = "\n{}:\n Expected head ({:.8f}, {:.8f}, {:.8f}),\n Actual is ({:.8f}, {:.8f}, {:.8f}).\n {}".format(
name, expect_head[0], expect_head[1], expect_head[2], actual_head[0], actual_head[1], actual_head[2], msg)
self.assertAlmostEqual(expect_head[0], actual_head[0], places=places, msg=head_msg)
self.assertAlmostEqual(expect_head[1], actual_head[1], places=places, msg=head_msg)
self.assertAlmostEqual(expect_head[2], actual_head[2], places=places, msg=head_msg)
# print("\n{}:\n Head is ({:.8f}, {:.8f}, {:.8f})".format(
# name, actual_head[0], actual_head[1], actual_head[2]))
tail_msg = "\n{}:\n Expected tail ({:.8f}, {:.8f}, {:.8f}),\n Actual is ({:.8f}, {:.8f}, {:.8f}).\n {}".format(
name, expect_tail[0], expect_tail[1], expect_tail[2], actual_tail[0], actual_tail[1], actual_tail[2], msg)
self.assertAlmostEqual(expect_tail[0], actual_tail[0], places=places, msg=tail_msg)
self.assertAlmostEqual(expect_tail[1], actual_tail[1], places=places, msg=tail_msg)
self.assertAlmostEqual(expect_tail[2], actual_tail[2], places=places, msg=tail_msg)
# print(" Tail is ({:.8f}, {:.8f}, {:.8f})".format(
# actual_tail[0], actual_tail[1], actual_tail[2]))
self.assertEqual(expect_connected, bone.use_connect, msg="{}: {}".format(bone.name, msg))
check_bone("root", (0, 0, 0), (0, 0, 1),
expect_connected=True, msg="Should not have changed.")
check_bone("tinychild_1", (0, 0, 1), (0, adjusted_bone_length, 1),
expect_connected=True, msg="Should have been elongated in the Y-direction")
adjust = (Vector((under_threshold, 0, under_threshold)).normalized() * adjusted_bone_length).x
check_bone("tinychild_2",
(0, 0, 1),
(adjust, 0, 1 + adjust),
expect_connected=True,
msg="Should have been elongated in the XZ-direction")
check_bone("zerochild_3",
(0, 0, 1),
(0, 0, 1 + adjusted_bone_length),
expect_connected=True,
msg="Should have been elongated in the Z-direction")
check_bone("tinygrandchild_1_1",
(0, under_threshold, 1),
(adjusted_bone_length, under_threshold, 1),
expect_connected=False,
msg="Should have been elongated in the X-direction and disconnected")
check_bone("grandchild_1_2",
(0, under_threshold, 1),
(1, under_threshold, 1),
expect_connected=False,
msg="Should been disconnected")
check_bone("great_grandchild_1_2_1",
(1, under_threshold, 1),
(2, under_threshold, 1),
expect_connected=True,
msg="Should been kept connected")
def main():
import sys
@@ -231,7 +413,7 @@ def main():
# Avoid passing all of Blender's arguments to unittest.main()
argv = [sys.argv[0]]
unittest.main(argv=argv)
unittest.main(argv=argv, exit=False)
if __name__ == "__main__":