@deepblend/dsh-blender-provider-local 0.2.18 → 0.2.19

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.18",
3
+ "version": "0.2.19",
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.18"
14
+ "@deepblend/dsh-blender-contracts": "0.2.19"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {
@@ -1486,7 +1486,7 @@ 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, cloth_entities=None):
1489
+ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None, soft_entities=None):
1490
1490
  """Give the entities their physics, and BAKE the result into the checkpoint.
1491
1491
 
1492
1492
  WHY THE COMPILE BAKES: a rigid body that has not been baked is a SETTING, not a motion — Blender
@@ -1500,6 +1500,7 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None):
1500
1500
  exactly like one that fell, unless something reads where it ended up.
1501
1501
  """
1502
1502
  cloth_entities = cloth_entities or {}
1503
+ soft_entities = soft_entities or {}
1503
1504
  settings = spec.get("simulation") or {}
1504
1505
  scene = bpy.context.scene
1505
1506
  if "gravity" in settings:
@@ -1544,7 +1545,7 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None):
1544
1545
  obj.rigid_body.friction = float(entry["friction"])
1545
1546
  bodies[entity["id"]] = entry
1546
1547
 
1547
- if not bodies and not cloth_entities:
1548
+ if not bodies and not cloth_entities and not soft_entities:
1548
1549
  return {}
1549
1550
 
1550
1551
  # A SCENE WITH CLOTH AND NO RIGID BODIES STILL HAS CACHES TO BAKE. MEASURED: the first version
@@ -1563,6 +1564,13 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None):
1563
1564
  for entity_id, entry in cloth_entities.items():
1564
1565
  build_cloth(entry, entity_objects[entity_id], guard)
1565
1566
 
1567
+ # SOFT BODIES ARE BUILT IN THE SAME PLACE, AND FOR THE SAME MEASURED REASON: MEASURED, and unlike
1568
+ # cloth, a soft body does NOT move without a bake (1.75 -> 1.75 before, 1.75 -> 1.6252 after), so its
1569
+ # modifier has to exist before `bake_all` — the three mechanisms behave three different ways, and each
1570
+ # one was measured rather than assumed from the last.
1571
+ for entity_id, entry in soft_entities.items():
1572
+ build_soft_body(entry, entity_objects[entity_id], guard)
1573
+
1566
1574
  reading = {}
1567
1575
  first_frame = int((spec.get("project") or {}).get("frameStart", 1))
1568
1576
  last_frame = int((spec.get("project") or {}).get("frameEnd", first_frame))
@@ -1578,11 +1586,14 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None):
1578
1586
  "message": "the simulation could not be baked: %s" % (error_text(exc),),
1579
1587
  })
1580
1588
 
1589
+ for entity_id in soft_entities:
1590
+ reading[entity_id] = {"kind": "soft-body", **deform_reading(entity_objects[entity_id], first_frame, last_frame)}
1591
+
1581
1592
  for entity_id in cloth_entities:
1582
1593
  # INTO THE SAME READING as the rigid bodies: one report of what the physics did, whatever the
1583
1594
  # mechanism, because a caller asking "what happened in this scene" should not have to know which
1584
1595
  # kind of simulation each entity used.
1585
- reading[entity_id] = {"kind": "cloth", **cloth_reading(entity_objects[entity_id], first_frame, last_frame)}
1596
+ reading[entity_id] = {"kind": "cloth", **deform_reading(entity_objects[entity_id], first_frame, last_frame)}
1586
1597
 
1587
1598
  for entity_id in bodies:
1588
1599
  obj = entity_objects[entity_id]
@@ -1649,9 +1660,14 @@ def build_cloth(entry, obj, guard):
1649
1660
  return obj
1650
1661
 
1651
1662
 
1652
- def cloth_reading(obj, first_frame, last_frame):
1663
+ def deform_reading(obj, first_frame, last_frame):
1653
1664
  """The lowest and the highest simulated vertex, at each end of the range.
1654
1665
 
1666
+ SHARED BY EVERYTHING THAT DEFORMS A MESH rather than copied per mechanism: cloth and soft bodies ask
1667
+ the same question of the same evaluated geometry, and a second copy is the one that would miss the
1668
+ next fix — this reading already needed one when it turned out to be taken from the base mesh.
1669
+
1670
+
1655
1671
  Read off the EVALUATED mesh rather than the modifier: what the fabric did is the geometry, and the
1656
1672
  modifier only says what was asked for. The highest point is the pinned edge, which is how the
1657
1673
  reading says "it draped FROM somewhere" rather than merely "it moved".
@@ -1670,16 +1686,58 @@ def cloth_reading(obj, first_frame, last_frame):
1670
1686
  if heights:
1671
1687
  reading[label + "LowestZ"] = round(min(heights), 6)
1672
1688
  reading[label + "HighestZ"] = round(max(heights), 6)
1673
- # THE VERTEX COUNT IS PART OF THE READING, not a diagnostic: cloth needs geometry to bend and
1674
- # the product's primitives are low-poly, so the compiler subdivides — and a reading that says
1675
- # how many vertices the fabric had is how "it draped" can be told apart from "it had nothing
1676
- # to drape with".
1689
+ # THE VERTEX COUNTS ARE PART OF THE READING, not diagnostics. MEASURED, and a surviving
1690
+ # mutation is why there are TWO of them: "the fabric had at least 64 vertices" passed on a
1691
+ # generator that already had that many, so it proved nothing about the subdivision. What
1692
+ # proves it is the RATIO — how many vertices the evaluated mesh has against how many the
1693
+ # object was built with — and that needs both numbers to be read.
1677
1694
  reading[label + "Vertices"] = len(heights)
1695
+ reading[label + "SourceVertices"] = len(obj.data.vertices)
1678
1696
  if "firstFrameLowestZ" in reading and "lastFrameLowestZ" in reading:
1679
1697
  reading["droppedZ"] = round(reading["firstFrameLowestZ"] - reading["lastFrameLowestZ"], 6)
1680
1698
  return reading
1681
1699
 
1682
1700
 
1701
+ def build_soft_body(entry, obj, guard):
1702
+ """Give a mesh soft-body physics: it deforms, and it remembers its shape.
1703
+
1704
+ THE GOAL IS WHAT MAKES IT A SOFT BODY rather than cloth — `goal` is how strongly each vertex wants to
1705
+ return to where it started, and Blender's default is high (0.7). MEASURED: a 0.5m cube dropped 25
1706
+ frames falls 0.125m and keeps its 0.5m spread, which is exactly what a body with a goal should do,
1707
+ and both halves of that are asserted so a future default change cannot quietly turn it into cloth.
1708
+ """
1709
+ settings = entry or {}
1710
+
1711
+ # THE SUBDIVISION IS NOT OPTIONAL, for the same measured reason as cloth: a primitive has no interior
1712
+ # to deform, so a soft body on an eight-vertex cube is a cube that moves.
1713
+ subdivision = obj.modifiers.new(name="DeepBlendSoftBodySubdivision", type="SUBSURF")
1714
+ subdivision.subdivision_type = "SIMPLE"
1715
+ subdivision.levels = int(settings.get("subdivisions", 3))
1716
+ subdivision.render_levels = subdivision.levels
1717
+
1718
+ modifier = obj.modifiers.new(name="DeepBlendSoftBody", type="SOFT_BODY")
1719
+ body_settings = modifier.settings
1720
+ if settings.get("mass") is not None:
1721
+ body_settings.mass = float(settings["mass"])
1722
+ if settings.get("goal") is not None:
1723
+ body_settings.goal_default = float(settings["goal"])
1724
+
1725
+ # PINNING IS EXPRESSED AS A GOAL, which is this modifier's own word for it: a vertex group at goal
1726
+ # 1.0 holds its vertices exactly, and one at 0.0 lets them go.
1727
+ if settings.get("pinTop"):
1728
+ ys = [vertex.co.y for vertex in obj.data.vertices]
1729
+ if ys:
1730
+ top = max(ys)
1731
+ pinned = [vertex.index for vertex in obj.data.vertices if abs(vertex.co.y - top) < 1e-4]
1732
+ group = obj.vertex_groups.new(name="DeepBlendSoftBodyPin")
1733
+ group.add(pinned, 1.0, "REPLACE")
1734
+ body_settings.use_goal = True
1735
+ body_settings.vertex_group_goal = group.name
1736
+ body_settings.goal_min = 1.0
1737
+ body_settings.goal_max = 1.0
1738
+ return obj
1739
+
1740
+
1683
1741
  def build_scene(spec, options, guard):
1684
1742
  """Compile a SceneSpec into a live Blender scene.
1685
1743
 
@@ -1872,7 +1930,18 @@ def build_scene(spec, options, guard):
1872
1930
  'entity "%s" declares cloth but has no object to hang it on' % (entity["id"],),
1873
1931
  )
1874
1932
  cloth_entities[entity["id"]] = entry
1875
- simulation_reading = build_rigid_bodies(spec, entity_objects, guard, cloth_entities)
1933
+ soft_entities = {}
1934
+ for entity in spec.get("entities") or []:
1935
+ entry = entity.get("softBody")
1936
+ if entry is None:
1937
+ continue
1938
+ if entity_objects.get(entity["id"]) is None:
1939
+ raise ActionError(
1940
+ "SCENE_SPEC_INVALID",
1941
+ 'entity "%s" declares a soft body but has no object to simulate' % (entity["id"],),
1942
+ )
1943
+ soft_entities[entity["id"]] = entry
1944
+ simulation_reading = build_rigid_bodies(spec, entity_objects, guard, cloth_entities, soft_entities)
1876
1945
 
1877
1946
  report_progress("configure_render", 85)
1878
1947
  render_config = configure_scene(scene, spec, profile, guard)