@deepblend/dsh-blender-provider-local 0.2.13 → 0.2.15
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 +27 -0
- package/python/deepblend_scene.py +127 -0
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.15",
|
|
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.15"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/python/bootstrap.py
CHANGED
|
@@ -227,6 +227,33 @@ def action_compile_scene(request, options):
|
|
|
227
227
|
"engine": report["engine"],
|
|
228
228
|
"requestedEngine": report["requestedEngine"],
|
|
229
229
|
"profileName": report["profileName"],
|
|
230
|
+
# THE RIGS THIS SCENE DECLARES, with their bones. A count alone would say an armature exists;
|
|
231
|
+
# a rig is only useful if the bones are the ones the spec named, so the names travel with it and
|
|
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
|
+
],
|
|
244
|
+
"armatures": [
|
|
245
|
+
{
|
|
246
|
+
"id": armature_id,
|
|
247
|
+
"object": report["armatureNames"].get(armature_id),
|
|
248
|
+
"bones": [
|
|
249
|
+
{"name": bone["name"], "parent": bone.get("parent")}
|
|
250
|
+
for bone in armature_entry["bones"]
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
for armature_id, armature_entry in (
|
|
254
|
+
(entry["id"], entry) for entry in (spec.get("armatures") or [])
|
|
255
|
+
)
|
|
256
|
+
],
|
|
230
257
|
"validation": validation,
|
|
231
258
|
"sceneFingerprint": scene_fingerprint(spec, describe_objects()),
|
|
232
259
|
}
|
|
@@ -1324,6 +1324,96 @@ def assign_view_transform(scene, name):
|
|
|
1324
1324
|
# ---------------------------------------------------------------------------
|
|
1325
1325
|
|
|
1326
1326
|
|
|
1327
|
+
ARMATURE_PREFIX = "db_arm__"
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def build_armature(entry, guard):
|
|
1331
|
+
"""Create one armature and its bones, and return the object.
|
|
1332
|
+
|
|
1333
|
+
WHY BONES ARE RESOLVED IN A SECOND PASS: the schema lets a bone name its parent without requiring
|
|
1334
|
+
the parent to be declared first, because a rig's declaration should read like the skeleton rather
|
|
1335
|
+
than like its storage order. So every bone is created as a root, and the parents are applied
|
|
1336
|
+
afterwards — with an unknown parent reported rather than silently ignored, since a bone that
|
|
1337
|
+
quietly has no parent is a rig that deforms differently from what the file says.
|
|
1338
|
+
"""
|
|
1339
|
+
armature_id = entry["id"]
|
|
1340
|
+
data = bpy.data.armatures.new("%s%s" % (ARMATURE_PREFIX, armature_id))
|
|
1341
|
+
obj = bpy.data.objects.new("%s%s" % (ARMATURE_PREFIX, armature_id), data)
|
|
1342
|
+
bpy.context.scene.collection.objects.link(obj)
|
|
1343
|
+
obj["deepblend_id"] = armature_id
|
|
1344
|
+
obj["deepblend_kind"] = "armature"
|
|
1345
|
+
|
|
1346
|
+
transform = entry.get("transform") or {}
|
|
1347
|
+
apply_transform(obj, transform)
|
|
1348
|
+
|
|
1349
|
+
# Edit mode is the only place `edit_bones` exists, and Blender's own context management is what
|
|
1350
|
+
# makes it available — a manual mode switch without this leaves the object in a state the rest of
|
|
1351
|
+
# the compile does not expect.
|
|
1352
|
+
bpy.context.view_layer.objects.active = obj
|
|
1353
|
+
bpy.ops.object.mode_set(mode="EDIT")
|
|
1354
|
+
try:
|
|
1355
|
+
created = {}
|
|
1356
|
+
for bone in entry["bones"]:
|
|
1357
|
+
edit_bone = data.edit_bones.new(bone["name"])
|
|
1358
|
+
edit_bone.head = tuple(float(value) for value in bone["head"])
|
|
1359
|
+
edit_bone.tail = tuple(float(value) for value in bone["tail"])
|
|
1360
|
+
created[bone["name"]] = edit_bone
|
|
1361
|
+
for bone in entry["bones"]:
|
|
1362
|
+
parent = bone.get("parent")
|
|
1363
|
+
if parent is None:
|
|
1364
|
+
continue
|
|
1365
|
+
if parent not in created:
|
|
1366
|
+
raise ActionError(
|
|
1367
|
+
"SCENE_SPEC_INVALID",
|
|
1368
|
+
'bone "%s" of armature "%s" names parent "%s", which the same armature does not declare'
|
|
1369
|
+
% (bone["name"], armature_id, parent),
|
|
1370
|
+
)
|
|
1371
|
+
created[bone["name"]].parent = created[parent]
|
|
1372
|
+
finally:
|
|
1373
|
+
bpy.ops.object.mode_set(mode="OBJECT")
|
|
1374
|
+
|
|
1375
|
+
return obj
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
def skin_entity(obj, armature_obj, armature_id, guard):
|
|
1379
|
+
"""Bind a mesh to an armature with Blender's own automatic weights.
|
|
1380
|
+
|
|
1381
|
+
WHY AUTOMATIC WEIGHTS RATHER THAN A WEIGHT TABLE. A per-vertex weight table in the SceneSpec would
|
|
1382
|
+
be a second, hand-written copy of something Blender already computes from the geometry — and the
|
|
1383
|
+
copy would be the one that goes stale, because nothing regenerates it when a generator's parameters
|
|
1384
|
+
change. What the spec states is the RELATIONSHIP ("this entity is skinned to that armature"); the
|
|
1385
|
+
weights are Blender's answer to it.
|
|
1386
|
+
|
|
1387
|
+
Bone heat can fail on a degenerate mesh (Blender reports "failed to find solution for one or more
|
|
1388
|
+
bones"), and that is a WARNING rather than a refusal: the binding still exists and the bones that
|
|
1389
|
+
did solve still deform, so the honest outcome is a scene that mostly works with the failure named.
|
|
1390
|
+
"""
|
|
1391
|
+
for target in bpy.context.selected_objects:
|
|
1392
|
+
target.select_set(False)
|
|
1393
|
+
obj.select_set(True)
|
|
1394
|
+
bpy.context.view_layer.objects.active = armature_obj
|
|
1395
|
+
try:
|
|
1396
|
+
bpy.ops.object.parent_set(type="ARMATURE_AUTO")
|
|
1397
|
+
except Exception as exc:
|
|
1398
|
+
guard.warnings.append({
|
|
1399
|
+
"code": "SKIN_WEIGHTS_FAILED",
|
|
1400
|
+
"message": 'entity "%s" could not be skinned to armature "%s": %s'
|
|
1401
|
+
% (obj.get("deepblend_id"), armature_id, error_text(exc)),
|
|
1402
|
+
})
|
|
1403
|
+
return False
|
|
1404
|
+
finally:
|
|
1405
|
+
obj.select_set(False)
|
|
1406
|
+
|
|
1407
|
+
if len(obj.vertex_groups) == 0:
|
|
1408
|
+
guard.warnings.append({
|
|
1409
|
+
"code": "SKIN_WEIGHTS_EMPTY",
|
|
1410
|
+
"message": 'entity "%s" is bound to armature "%s" but Blender produced no vertex groups, so '
|
|
1411
|
+
"it will not deform" % (obj.get("deepblend_id"), armature_id),
|
|
1412
|
+
})
|
|
1413
|
+
return False
|
|
1414
|
+
return True
|
|
1415
|
+
|
|
1416
|
+
|
|
1327
1417
|
def build_scene(spec, options, guard):
|
|
1328
1418
|
"""Compile a SceneSpec into a live Blender scene.
|
|
1329
1419
|
|
|
@@ -1372,6 +1462,13 @@ def build_scene(spec, options, guard):
|
|
|
1372
1462
|
|
|
1373
1463
|
assets = {asset["id"]: asset for asset in spec.get("assets") or []}
|
|
1374
1464
|
|
|
1465
|
+
# ARMATURES BEFORE ENTITIES: an entity that is skinned to one has to find it already there. Nothing
|
|
1466
|
+
# binds yet — that is the next slice — but the order is the one that will not have to change.
|
|
1467
|
+
report_progress("build_armatures", 30)
|
|
1468
|
+
armature_objects = {}
|
|
1469
|
+
for entry in spec.get("armatures") or []:
|
|
1470
|
+
armature_objects[entry["id"]] = build_armature(entry, guard)
|
|
1471
|
+
|
|
1375
1472
|
report_progress("build_entities", 35)
|
|
1376
1473
|
entity_objects = {}
|
|
1377
1474
|
entity_positions = {}
|
|
@@ -1492,6 +1589,25 @@ def build_scene(spec, options, guard):
|
|
|
1492
1589
|
bpy.context.view_layer.update()
|
|
1493
1590
|
|
|
1494
1591
|
report_progress("compile_done", 90)
|
|
1592
|
+
# SKINNING LAST: it needs both the mesh and the rig, and doing it before either exists would mean
|
|
1593
|
+
# ordering the compile around a step that is about the RELATIONSHIP between them.
|
|
1594
|
+
report_progress("skin_entities", 60)
|
|
1595
|
+
skinned_entities = {}
|
|
1596
|
+
for entity in spec.get("entities") or []:
|
|
1597
|
+
armature_id = entity.get("armatureId")
|
|
1598
|
+
if armature_id is None:
|
|
1599
|
+
continue
|
|
1600
|
+
obj = entity_objects.get(entity["id"])
|
|
1601
|
+
armature_obj = armature_objects.get(armature_id)
|
|
1602
|
+
if obj is None or armature_obj is None:
|
|
1603
|
+
raise ActionError(
|
|
1604
|
+
"SCENE_SPEC_INVALID",
|
|
1605
|
+
'entity "%s" is skinned to armature "%s", which the same SceneSpec does not declare'
|
|
1606
|
+
% (entity["id"], armature_id),
|
|
1607
|
+
)
|
|
1608
|
+
skin_entity(obj, armature_obj, armature_id, guard)
|
|
1609
|
+
skinned_entities[entity["id"]] = entity
|
|
1610
|
+
|
|
1495
1611
|
return {
|
|
1496
1612
|
"objects": describe_objects(),
|
|
1497
1613
|
"actions": actions,
|
|
@@ -1501,6 +1617,17 @@ def build_scene(spec, options, guard):
|
|
|
1501
1617
|
"requestedEngine": requested_engine,
|
|
1502
1618
|
"profileName": profile_name,
|
|
1503
1619
|
"entityObjectNames": {key: value.name for key, value in entity_objects.items()},
|
|
1620
|
+
"armatureNames": {key: value.name for key, value in armature_objects.items()},
|
|
1621
|
+
"skinned": {
|
|
1622
|
+
key: {
|
|
1623
|
+
"armatureId": entry["armatureId"],
|
|
1624
|
+
# THE GROUPS COME FROM THE OBJECT, not from the spec entry: what the file asked for is
|
|
1625
|
+
# the relationship, and what Blender produced is the weights. Reading the latter back is
|
|
1626
|
+
# the whole point — an entity bound with no groups renders as a static object.
|
|
1627
|
+
"vertexGroups": sorted(group.name for group in entity_objects[key].vertex_groups),
|
|
1628
|
+
}
|
|
1629
|
+
for key, entry in skinned_entities.items()
|
|
1630
|
+
},
|
|
1504
1631
|
"cameraNames": {key: value.name for key, value in cameras.items()},
|
|
1505
1632
|
}
|
|
1506
1633
|
|