@deepblend/dsh-blender-provider-local 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/python/__pycache__/bootstrap.cpython-313.pyc +0 -0
- package/python/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_scene.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_scene.cpython-314.pyc +0 -0
- package/python/bootstrap.py +11 -0
- package/python/deepblend_scene.py +164 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-provider-local",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local Blender Provider: the BlenderRuntime Service over ctx.subprocess (batch execution).",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"./package.json": "./package.json"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@deepblend/dsh-blender-contracts": "0.2.
|
|
14
|
+
"@deepblend/dsh-blender-contracts": "0.2.16"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/python/bootstrap.py
CHANGED
|
@@ -230,6 +230,17 @@ def action_compile_scene(request, options):
|
|
|
230
230
|
# THE RIGS THIS SCENE DECLARES, with their bones. A count alone would say an armature exists;
|
|
231
231
|
# a rig is only useful if the bones are the ones the spec named, so the names travel with it and
|
|
232
232
|
# a test can read them back rather than trust the count.
|
|
233
|
+
# WHICH ENTITIES ARE SKINNED, AND TO WHAT. The vertex groups are the evidence that weights
|
|
234
|
+
# exist at all: an entity bound to an armature with no groups renders as a static object while
|
|
235
|
+
# the file says it is rigged, and the reading is where that difference becomes visible.
|
|
236
|
+
"skinned": [
|
|
237
|
+
{
|
|
238
|
+
"entityId": entity_id,
|
|
239
|
+
"armatureId": entry["armatureId"],
|
|
240
|
+
"vertexGroups": entry["vertexGroups"],
|
|
241
|
+
}
|
|
242
|
+
for entity_id, entry in sorted((report.get("skinned") or {}).items())
|
|
243
|
+
],
|
|
233
244
|
"armatures": [
|
|
234
245
|
{
|
|
235
246
|
"id": armature_id,
|
|
@@ -1030,6 +1030,82 @@ def _apply_interpolation(action, data_path, axis, keyframes, track, guard):
|
|
|
1030
1030
|
)
|
|
1031
1031
|
|
|
1032
1032
|
|
|
1033
|
+
def _insert_keyframes(target, bpy_attr, axis, keyframes):
|
|
1034
|
+
"""Insert one component's keyframes on a target that has `keyframe_insert`.
|
|
1035
|
+
|
|
1036
|
+
EXTRACTED RATHER THAN COPIED: an object and a pose bone are keyframed by the same three lines, and
|
|
1037
|
+
a second copy is the one that would miss the next fix — the identity-first rule below is exactly the
|
|
1038
|
+
kind of thing that gets learned once and applied in one place.
|
|
1039
|
+
"""
|
|
1040
|
+
for keyframe in keyframes:
|
|
1041
|
+
frame = int(keyframe["frame"])
|
|
1042
|
+
value = float(keyframe["value"])
|
|
1043
|
+
setattr(target, bpy_attr, _with_component(getattr(target, bpy_attr), axis, value))
|
|
1044
|
+
target.keyframe_insert(data_path=bpy_attr, index=axis, frame=frame)
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def build_bone_animation(armature_obj, track, guard):
|
|
1048
|
+
"""Animate ONE pose bone of an armature.
|
|
1049
|
+
|
|
1050
|
+
WHY A BONE IS NOT AN OBJECT: the values live on the ARMATURE's pose, so the keyframes are inserted
|
|
1051
|
+
through the pose bone while the action belongs to the armature. Everything else — the property
|
|
1052
|
+
vocabulary, the two-keyframe minimum, the interpolation — is the same, which is why the insertion
|
|
1053
|
+
itself is shared rather than re-implemented.
|
|
1054
|
+
|
|
1055
|
+
The bone is named by the track; a name the armature does not have is refused, because a track that
|
|
1056
|
+
silently animates nothing is a scene that claims to move and does not.
|
|
1057
|
+
"""
|
|
1058
|
+
bone_name = track.get("boneName")
|
|
1059
|
+
if not isinstance(bone_name, str) or bone_name == "":
|
|
1060
|
+
raise ActionError(
|
|
1061
|
+
"SCENE_VALIDATION_FAILED",
|
|
1062
|
+
'animation track "%s" targets a bone but names none' % (track.get("id"),),
|
|
1063
|
+
)
|
|
1064
|
+
pose_bone = armature_obj.pose.bones.get(bone_name)
|
|
1065
|
+
if pose_bone is None:
|
|
1066
|
+
raise ActionError(
|
|
1067
|
+
"SCENE_VALIDATION_FAILED",
|
|
1068
|
+
'animation track "%s" targets bone "%s", which armature "%s" does not have'
|
|
1069
|
+
% (track.get("id"), bone_name, armature_obj.get("deepblend_id")),
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
property_name = track.get("property", "")
|
|
1073
|
+
if "." not in property_name:
|
|
1074
|
+
raise ActionError(
|
|
1075
|
+
"SCENE_VALIDATION_FAILED",
|
|
1076
|
+
'animation track "%s" names property "%s" without a component' % (track.get("id"), property_name),
|
|
1077
|
+
)
|
|
1078
|
+
attr, _, component = property_name.rpartition(".")
|
|
1079
|
+
bpy_attr = TRACK_PROPERTY_ATTR.get(attr)
|
|
1080
|
+
if bpy_attr is None or component not in ("x", "y", "z"):
|
|
1081
|
+
raise ActionError(
|
|
1082
|
+
"SCENE_VALIDATION_FAILED",
|
|
1083
|
+
'animation track "%s" names unsupported property "%s"' % (track.get("id"), property_name),
|
|
1084
|
+
{"supported": sorted("%s.%s" % (name, axis) for name in TRACK_PROPERTY_ATTR for axis in "xyz")},
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
keyframes = track.get("keyframes") or []
|
|
1088
|
+
if len(keyframes) < 2:
|
|
1089
|
+
raise ActionError(
|
|
1090
|
+
"SCENE_VALIDATION_FAILED",
|
|
1091
|
+
'animation track "%s" has %d keyframe(s); at least 2 are required to describe motion'
|
|
1092
|
+
% (track.get("id"), len(keyframes)),
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
axis = "xyz".index(component)
|
|
1096
|
+
pose_bone.rotation_mode = "XYZ"
|
|
1097
|
+
_insert_keyframes(pose_bone, bpy_attr, axis, keyframes)
|
|
1098
|
+
|
|
1099
|
+
action = armature_obj.animation_data.action if armature_obj.animation_data is not None else None
|
|
1100
|
+
if action is None:
|
|
1101
|
+
raise ActionError(
|
|
1102
|
+
"BLENDER_SCRIPT_ERROR",
|
|
1103
|
+
'animation track "%s" inserted keyframes but produced no action' % (track.get("id"),),
|
|
1104
|
+
)
|
|
1105
|
+
_apply_interpolation(action, bpy_attr, axis, keyframes, track, guard)
|
|
1106
|
+
return action
|
|
1107
|
+
|
|
1108
|
+
|
|
1033
1109
|
def build_animation(obj, track, guard):
|
|
1034
1110
|
"""Key one animation track onto an object, returning the action name.
|
|
1035
1111
|
|
|
@@ -1072,11 +1148,7 @@ def build_animation(obj, track, guard):
|
|
|
1072
1148
|
# values, or the first insert would capture a value from a different property.
|
|
1073
1149
|
obj.rotation_mode = "XYZ"
|
|
1074
1150
|
|
|
1075
|
-
|
|
1076
|
-
frame = int(keyframe["frame"])
|
|
1077
|
-
value = float(keyframe["value"])
|
|
1078
|
-
setattr(obj, bpy_attr, _with_component(getattr(obj, bpy_attr), axis, value))
|
|
1079
|
-
obj.keyframe_insert(data_path=bpy_attr, index=axis, frame=frame)
|
|
1151
|
+
_insert_keyframes(obj, bpy_attr, axis, keyframes)
|
|
1080
1152
|
|
|
1081
1153
|
action = obj.animation_data.action if obj.animation_data is not None else None
|
|
1082
1154
|
if action is None:
|
|
@@ -1375,6 +1447,45 @@ def build_armature(entry, guard):
|
|
|
1375
1447
|
return obj
|
|
1376
1448
|
|
|
1377
1449
|
|
|
1450
|
+
def skin_entity(obj, armature_obj, armature_id, guard):
|
|
1451
|
+
"""Bind a mesh to an armature with Blender's own automatic weights.
|
|
1452
|
+
|
|
1453
|
+
WHY AUTOMATIC WEIGHTS RATHER THAN A WEIGHT TABLE. A per-vertex weight table in the SceneSpec would
|
|
1454
|
+
be a second, hand-written copy of something Blender already computes from the geometry — and the
|
|
1455
|
+
copy would be the one that goes stale, because nothing regenerates it when a generator's parameters
|
|
1456
|
+
change. What the spec states is the RELATIONSHIP ("this entity is skinned to that armature"); the
|
|
1457
|
+
weights are Blender's answer to it.
|
|
1458
|
+
|
|
1459
|
+
Bone heat can fail on a degenerate mesh (Blender reports "failed to find solution for one or more
|
|
1460
|
+
bones"), and that is a WARNING rather than a refusal: the binding still exists and the bones that
|
|
1461
|
+
did solve still deform, so the honest outcome is a scene that mostly works with the failure named.
|
|
1462
|
+
"""
|
|
1463
|
+
for target in bpy.context.selected_objects:
|
|
1464
|
+
target.select_set(False)
|
|
1465
|
+
obj.select_set(True)
|
|
1466
|
+
bpy.context.view_layer.objects.active = armature_obj
|
|
1467
|
+
try:
|
|
1468
|
+
bpy.ops.object.parent_set(type="ARMATURE_AUTO")
|
|
1469
|
+
except Exception as exc:
|
|
1470
|
+
guard.warnings.append({
|
|
1471
|
+
"code": "SKIN_WEIGHTS_FAILED",
|
|
1472
|
+
"message": 'entity "%s" could not be skinned to armature "%s": %s'
|
|
1473
|
+
% (obj.get("deepblend_id"), armature_id, error_text(exc)),
|
|
1474
|
+
})
|
|
1475
|
+
return False
|
|
1476
|
+
finally:
|
|
1477
|
+
obj.select_set(False)
|
|
1478
|
+
|
|
1479
|
+
if len(obj.vertex_groups) == 0:
|
|
1480
|
+
guard.warnings.append({
|
|
1481
|
+
"code": "SKIN_WEIGHTS_EMPTY",
|
|
1482
|
+
"message": 'entity "%s" is bound to armature "%s" but Blender produced no vertex groups, so '
|
|
1483
|
+
"it will not deform" % (obj.get("deepblend_id"), armature_id),
|
|
1484
|
+
})
|
|
1485
|
+
return False
|
|
1486
|
+
return True
|
|
1487
|
+
|
|
1488
|
+
|
|
1378
1489
|
def build_scene(spec, options, guard):
|
|
1379
1490
|
"""Compile a SceneSpec into a live Blender scene.
|
|
1380
1491
|
|
|
@@ -1521,6 +1632,10 @@ def build_scene(spec, options, guard):
|
|
|
1521
1632
|
target = cameras.get(target_id)
|
|
1522
1633
|
elif kind == "material":
|
|
1523
1634
|
target = materials.get(target_id)
|
|
1635
|
+
elif kind == "bone":
|
|
1636
|
+
# A BONE TRACK TARGETS THE ARMATURE, because the bones belong to it: the armature id is
|
|
1637
|
+
# what resolves, and `boneName` picks the bone inside it.
|
|
1638
|
+
target = armature_objects.get(target_id)
|
|
1524
1639
|
else:
|
|
1525
1640
|
target = entity_objects.get(target_id)
|
|
1526
1641
|
if target is None:
|
|
@@ -1531,9 +1646,23 @@ def build_scene(spec, options, guard):
|
|
|
1531
1646
|
)
|
|
1532
1647
|
if kind == "material":
|
|
1533
1648
|
action = build_material_animation(target, track, guard)
|
|
1649
|
+
elif kind == "bone":
|
|
1650
|
+
action = build_bone_animation(target, track, guard)
|
|
1534
1651
|
else:
|
|
1535
1652
|
action = build_animation(target, track, guard)
|
|
1536
|
-
actions.append({
|
|
1653
|
+
actions.append({
|
|
1654
|
+
"id": track.get("id"),
|
|
1655
|
+
"targetKind": kind,
|
|
1656
|
+
"targetEntityId": target_id,
|
|
1657
|
+
"action": action.name,
|
|
1658
|
+
**({"boneName": track.get("boneName")} if kind == "bone" else {}),
|
|
1659
|
+
# WHAT THE ACTION ACTUALLY DROVE, read off its own fcurves. MEASURED, and a surviving
|
|
1660
|
+
# mutation is why: keying the ARMATURE OBJECT instead of the pose bone still moves the mesh
|
|
1661
|
+
# — the mesh is parented to the armature, so the two frames differ either way — and the
|
|
1662
|
+
# two-frame proof passed. The data paths are the difference: `pose.bones["chest"]…` is the
|
|
1663
|
+
# bone, `rotation_euler` on the object is not.
|
|
1664
|
+
"dataPaths": sorted({curve.data_path for curve in action_fcurves(action)}),
|
|
1665
|
+
})
|
|
1537
1666
|
|
|
1538
1667
|
report_progress("configure_render", 85)
|
|
1539
1668
|
render_config = configure_scene(scene, spec, profile, guard)
|
|
@@ -1550,6 +1679,25 @@ def build_scene(spec, options, guard):
|
|
|
1550
1679
|
bpy.context.view_layer.update()
|
|
1551
1680
|
|
|
1552
1681
|
report_progress("compile_done", 90)
|
|
1682
|
+
# SKINNING LAST: it needs both the mesh and the rig, and doing it before either exists would mean
|
|
1683
|
+
# ordering the compile around a step that is about the RELATIONSHIP between them.
|
|
1684
|
+
report_progress("skin_entities", 60)
|
|
1685
|
+
skinned_entities = {}
|
|
1686
|
+
for entity in spec.get("entities") or []:
|
|
1687
|
+
armature_id = entity.get("armatureId")
|
|
1688
|
+
if armature_id is None:
|
|
1689
|
+
continue
|
|
1690
|
+
obj = entity_objects.get(entity["id"])
|
|
1691
|
+
armature_obj = armature_objects.get(armature_id)
|
|
1692
|
+
if obj is None or armature_obj is None:
|
|
1693
|
+
raise ActionError(
|
|
1694
|
+
"SCENE_SPEC_INVALID",
|
|
1695
|
+
'entity "%s" is skinned to armature "%s", which the same SceneSpec does not declare'
|
|
1696
|
+
% (entity["id"], armature_id),
|
|
1697
|
+
)
|
|
1698
|
+
skin_entity(obj, armature_obj, armature_id, guard)
|
|
1699
|
+
skinned_entities[entity["id"]] = entity
|
|
1700
|
+
|
|
1553
1701
|
return {
|
|
1554
1702
|
"objects": describe_objects(),
|
|
1555
1703
|
"actions": actions,
|
|
@@ -1560,6 +1708,16 @@ def build_scene(spec, options, guard):
|
|
|
1560
1708
|
"profileName": profile_name,
|
|
1561
1709
|
"entityObjectNames": {key: value.name for key, value in entity_objects.items()},
|
|
1562
1710
|
"armatureNames": {key: value.name for key, value in armature_objects.items()},
|
|
1711
|
+
"skinned": {
|
|
1712
|
+
key: {
|
|
1713
|
+
"armatureId": entry["armatureId"],
|
|
1714
|
+
# THE GROUPS COME FROM THE OBJECT, not from the spec entry: what the file asked for is
|
|
1715
|
+
# the relationship, and what Blender produced is the weights. Reading the latter back is
|
|
1716
|
+
# the whole point — an entity bound with no groups renders as a static object.
|
|
1717
|
+
"vertexGroups": sorted(group.name for group in entity_objects[key].vertex_groups),
|
|
1718
|
+
}
|
|
1719
|
+
for key, entry in skinned_entities.items()
|
|
1720
|
+
},
|
|
1563
1721
|
"cameraNames": {key: value.name for key, value in cameras.items()},
|
|
1564
1722
|
}
|
|
1565
1723
|
|