I refer to this function: source/blender/blenkernel/intern/armature.c :: vec_roll_to_mat3_normalized()
The conversion of roll to matrix breaks when a bone has very small values in x and z. In that case we get a division by very small numbers which can cause a broken bone roll matrix.
As already mentioned in the comments:
due to float precision errors, we can have nor = (0.0, 0.99999994, 0.0)...
I have found other situations where nor == (x, 0.99999994, z) with x and/or z very close to 0, but not exactly 0. However the test (nor || nor) can be true for tiny numbers. So further down in the function we see:
/* If nor is too close to -Y, apply the special case. */ theta = nor * nor + nor * nor;
So when x and/or z are tiny numbers, then theta becomes tiny as well. And since we only check for (x or z != 0):
if (theta > THETA_SAFE || ((nor || nor) && theta > THETA_CRITICAL)) ...
You can see that theta can potentially drop below the THETA_THRESHOLD_NEGY_CLOSE if both nor and nor are very close to 0. And this finally leads to potentially very huge numbers for example here (division by almost zero):
theta = nor * nor + nor * nor; bMatrix = (nor + nor) * (nor - nor) / -theta;
Because of this i believe it is better to not only check (nor or nor != 0) but also to make sure that (nor * nor + nor * nor) is above THETA_THRESHOLD_NEGY_CLOSE.
Below is a python script that generates a rig that suffers from this bug. Run the script, then switch from edit mode to pose mode and see how "bad bone" disappears. Because its pose bone matrix is totally broken. The problem originates in the super small bone roll values which finally lead to the bug:
import bpy bone_data = [ ["good bone", (0.0, 0.0, 1.2038891315460205), (0.0, 0.01536799967288971, 1.3546786308288574), -4.1202467175263267e-16], ["bad bone", (0.0, 0.01536799967288971, 1.3546786308288574), (0.0, 0.0, 1.2038891315460205), -1.570796257510665e-07], ] def make_armature(ctx, name): data = bpy.data.armatures.new(name) obj = bpy.data.objects.new(name, data) ctx.collection.objects.link(obj) ctx.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') parent = None for bname, head, tail, roll in bone_data: bone = obj.data.edit_bones.new(bname) if parent: bone.parent = parent parent=bone bone.head=head bone.tail=tail bone.roll = roll return obj armobj=make_armature(bpy.context, "armature")
This is a follow up task of the refactoring handled in https://developer.blender.org/D9410