@deepblend/dsh-blender-provider-local 0.2.16 → 0.2.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-provider-local",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
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.16"
14
+ "@deepblend/dsh-blender-contracts": "0.2.17"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {
@@ -233,6 +233,13 @@ def action_compile_scene(request, options):
233
233
  # WHICH ENTITIES ARE SKINNED, AND TO WHAT. The vertex groups are the evidence that weights
234
234
  # exist at all: an entity bound to an armature with no groups renders as a static object while
235
235
  # the file says it is rigged, and the reading is where that difference becomes visible.
236
+ # WHAT THE PHYSICS DID, per entity. Not the settings: a body that is configured and never
237
+ # enabled looks exactly like one that fell, and only a reading of where it ended up tells them
238
+ # apart.
239
+ "simulation": [
240
+ {"entityId": entity_id, **entry}
241
+ for entity_id, entry in sorted((report.get("simulation") or {}).items())
242
+ ],
236
243
  "skinned": [
237
244
  {
238
245
  "entityId": entity_id,
@@ -1486,6 +1486,104 @@ def skin_entity(obj, armature_obj, armature_id, guard):
1486
1486
  return True
1487
1487
 
1488
1488
 
1489
+ def build_rigid_bodies(spec, entity_objects, guard):
1490
+ """Give the entities their physics, and BAKE the result into the checkpoint.
1491
+
1492
+ WHY THE COMPILE BAKES: a rigid body that has not been baked is a SETTING, not a motion — Blender
1493
+ evaluates it on the fly in the viewport and a batch render of an unbaked simulation renders the
1494
+ initial pose. Baking here is what makes the checkpoint mean "this is what the simulation did", and
1495
+ it is why `simulation.bake: false` exists for a build that only wants the setup.
1496
+
1497
+ Returns a reading of what the physics DID, not of what it was told: each active body's height at the
1498
+ first and the last frame of the scene's range. MEASURED, and the lesson from the animation slice is
1499
+ why this is not just "the setup exists": a body that is configured correctly and never enabled looks
1500
+ exactly like one that fell, unless something reads where it ended up.
1501
+ """
1502
+ settings = spec.get("simulation") or {}
1503
+ scene = bpy.context.scene
1504
+ if "gravity" in settings:
1505
+ scene.gravity[2] = -abs(float(settings["gravity"]))
1506
+
1507
+ # THE PHYSICS WORLD HAS TO EXIST BEFORE A BODY CAN BE ATTACHED TO IT. MEASURED: without this line
1508
+ # `obj.rigid_body` is None and the assignment fails with an AttributeError — Blender's rigid-body
1509
+ # settings live on a scene-level world, and a scene that has never had one has nowhere to put them.
1510
+ if scene.rigidbody_world is None:
1511
+ bpy.ops.rigidbody.world_add()
1512
+ # AND THE WORLD NEEDS A COLLECTION, which a fresh one does not have. MEASURED, in three steps that
1513
+ # each looked like it should work: without the world there is nowhere to put a body; with the world
1514
+ # but no collection, `world.collection` is None; and only an object LINKED INTO that collection
1515
+ # gets its `rigid_body` materialised. The Blender UI does all three when a user clicks "Rigid Body".
1516
+ if scene.rigidbody_world.collection is None:
1517
+ collection = bpy.data.collections.new("DeepBlendRigidBodies")
1518
+ scene.collection.children.link(collection)
1519
+ scene.rigidbody_world.collection = collection
1520
+
1521
+ bodies = {}
1522
+ for entity in spec.get("entities") or []:
1523
+ entry = entity.get("rigidBody")
1524
+ if entry is None:
1525
+ continue
1526
+ obj = entity_objects.get(entity["id"])
1527
+ if obj is None:
1528
+ raise ActionError(
1529
+ "SCENE_SPEC_INVALID",
1530
+ 'entity "%s" declares a rigid body but has no object to attach it to' % (entity["id"],),
1531
+ )
1532
+ # JOINING THE PHYSICS COLLECTION IS WHAT CREATES `obj.rigid_body`. MEASURED, twice: adding the
1533
+ # world is not enough — Blender materialises the rigid-body settings when the object is linked
1534
+ # into the world's own collection, so an object that has "a rigid body" the obvious way is an
1535
+ # object with `None`.
1536
+ if obj.name not in scene.rigidbody_world.collection.objects:
1537
+ scene.rigidbody_world.collection.objects.link(obj)
1538
+
1539
+ obj.rigid_body.type = "ACTIVE" if entry["kind"] == "active" else "PASSIVE"
1540
+ if entry.get("mass") is not None:
1541
+ obj.rigid_body.mass = float(entry["mass"])
1542
+ if entry.get("friction") is not None:
1543
+ obj.rigid_body.friction = float(entry["friction"])
1544
+ bodies[entity["id"]] = entry
1545
+
1546
+ if not bodies:
1547
+ return {}
1548
+
1549
+ # THE GROUND BODIES DO NOT MOVE, so they need the physics world to have something to collide against
1550
+ # and Blender needs to be told the world exists at all.
1551
+ scene.use_gravity = True
1552
+
1553
+ reading = {}
1554
+ first_frame = int((spec.get("project") or {}).get("frameStart", 1))
1555
+ last_frame = int((spec.get("project") or {}).get("frameEnd", first_frame))
1556
+ if settings.get("bake", True):
1557
+ scene.frame_set(first_frame)
1558
+ # `bake_all` walks every cache in the scene, which is what a compile wants: the caller asked for
1559
+ # a checkpoint that shows the simulation, not for one cache to be fresh.
1560
+ try:
1561
+ bpy.ops.ptcache.bake_all(bake=True)
1562
+ except Exception as exc:
1563
+ guard.warnings.append({
1564
+ "code": "SIMULATION_BAKE_FAILED",
1565
+ "message": "the simulation could not be baked: %s" % (error_text(exc),),
1566
+ })
1567
+
1568
+ for entity_id in bodies:
1569
+ obj = entity_objects[entity_id]
1570
+ scene.frame_set(first_frame)
1571
+ bpy.context.view_layer.update()
1572
+ first = float((obj.matrix_world.translation)[2])
1573
+ scene.frame_set(last_frame)
1574
+ bpy.context.view_layer.update()
1575
+ last = float((obj.matrix_world.translation)[2])
1576
+ reading[entity_id] = {
1577
+ "kind": bodies[entity_id]["kind"],
1578
+ "firstFrameZ": round(first, 6),
1579
+ "lastFrameZ": round(last, 6),
1580
+ "droppedZ": round(first - last, 6),
1581
+ }
1582
+ scene.frame_set(first_frame)
1583
+ bpy.context.view_layer.update()
1584
+ return reading
1585
+
1586
+
1489
1587
  def build_scene(spec, options, guard):
1490
1588
  """Compile a SceneSpec into a live Blender scene.
1491
1589
 
@@ -1664,6 +1762,9 @@ def build_scene(spec, options, guard):
1664
1762
  "dataPaths": sorted({curve.data_path for curve in action_fcurves(action)}),
1665
1763
  })
1666
1764
 
1765
+ report_progress("build_simulation", 70)
1766
+ simulation_reading = build_rigid_bodies(spec, entity_objects, guard)
1767
+
1667
1768
  report_progress("configure_render", 85)
1668
1769
  render_config = configure_scene(scene, spec, profile, guard)
1669
1770
 
@@ -1708,6 +1809,7 @@ def build_scene(spec, options, guard):
1708
1809
  "profileName": profile_name,
1709
1810
  "entityObjectNames": {key: value.name for key, value in entity_objects.items()},
1710
1811
  "armatureNames": {key: value.name for key, value in armature_objects.items()},
1812
+ "simulation": simulation_reading,
1711
1813
  "skinned": {
1712
1814
  key: {
1713
1815
  "armatureId": entry["armatureId"],