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

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.17",
3
+ "version": "0.2.18",
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.17"
14
+ "@deepblend/dsh-blender-contracts": "0.2.18"
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):
1489
+ def build_rigid_bodies(spec, entity_objects, guard, cloth_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
@@ -1499,6 +1499,7 @@ def build_rigid_bodies(spec, entity_objects, guard):
1499
1499
  why this is not just "the setup exists": a body that is configured correctly and never enabled looks
1500
1500
  exactly like one that fell, unless something reads where it ended up.
1501
1501
  """
1502
+ cloth_entities = cloth_entities or {}
1502
1503
  settings = spec.get("simulation") or {}
1503
1504
  scene = bpy.context.scene
1504
1505
  if "gravity" in settings:
@@ -1543,13 +1544,25 @@ def build_rigid_bodies(spec, entity_objects, guard):
1543
1544
  obj.rigid_body.friction = float(entry["friction"])
1544
1545
  bodies[entity["id"]] = entry
1545
1546
 
1546
- if not bodies:
1547
+ if not bodies and not cloth_entities:
1547
1548
  return {}
1548
1549
 
1550
+ # A SCENE WITH CLOTH AND NO RIGID BODIES STILL HAS CACHES TO BAKE. MEASURED: the first version
1551
+ # returned early when there were no bodies, and a cloth-only scene reported an empty reading — the
1552
+ # bake and the whole reading sat behind a condition that had nothing to do with cloth.
1553
+ _ = bodies
1554
+
1549
1555
  # THE GROUND BODIES DO NOT MOVE, so they need the physics world to have something to collide against
1550
1556
  # and Blender needs to be told the world exists at all.
1551
1557
  scene.use_gravity = True
1552
1558
 
1559
+ # THE CLOTH MODIFIERS ARE CREATED BEFORE THE BAKE, which is the whole reason they are built here
1560
+ # rather than in `build_scene`. MEASURED, and the comment above this loop said so before the code
1561
+ # did: created after it, the bake had nothing to walk and the reading saw an object with no
1562
+ # modifiers — a fabric that never moved, reported as a simulation that did nothing.
1563
+ for entity_id, entry in cloth_entities.items():
1564
+ build_cloth(entry, entity_objects[entity_id], guard)
1565
+
1553
1566
  reading = {}
1554
1567
  first_frame = int((spec.get("project") or {}).get("frameStart", 1))
1555
1568
  last_frame = int((spec.get("project") or {}).get("frameEnd", first_frame))
@@ -1565,6 +1578,12 @@ def build_rigid_bodies(spec, entity_objects, guard):
1565
1578
  "message": "the simulation could not be baked: %s" % (error_text(exc),),
1566
1579
  })
1567
1580
 
1581
+ for entity_id in cloth_entities:
1582
+ # INTO THE SAME READING as the rigid bodies: one report of what the physics did, whatever the
1583
+ # mechanism, because a caller asking "what happened in this scene" should not have to know which
1584
+ # kind of simulation each entity used.
1585
+ reading[entity_id] = {"kind": "cloth", **cloth_reading(entity_objects[entity_id], first_frame, last_frame)}
1586
+
1568
1587
  for entity_id in bodies:
1569
1588
  obj = entity_objects[entity_id]
1570
1589
  scene.frame_set(first_frame)
@@ -1584,6 +1603,83 @@ def build_rigid_bodies(spec, entity_objects, guard):
1584
1603
  return reading
1585
1604
 
1586
1605
 
1606
+ def build_cloth(entry, obj, guard):
1607
+ """Hang a mesh as fabric, and report where its lowest point ended up.
1608
+
1609
+ WHY THE PIN GROUP IS BUILT FROM GEOMETRY: Blender pins cloth through a vertex group, and a
1610
+ SceneSpec that named one would be referring to something no generator creates. What the spec says
1611
+ is the RELATIONSHIP ("hang this from its top edge"); the group is the compiler's answer to it.
1612
+
1613
+ MEASURED, AND IT DIFFERS FROM RIGID BODIES: cloth is evaluated LIVE, so it drapes with or without a
1614
+ bake — but the BAKED result differs from the unbaked one, because cloth integrates frame by frame
1615
+ and a bake walks every frame while a jump to the last one does not. That is why nothing here claims
1616
+ "no bake means no motion": for cloth that claim would be false, and a false assertion is worse than
1617
+ none.
1618
+ """
1619
+ settings = entry or {}
1620
+ pin_top = settings.get("pinTop", True)
1621
+ group_name = None
1622
+ if pin_top:
1623
+ ys = [vertex.co.y for vertex in obj.data.vertices]
1624
+ if ys:
1625
+ top = max(ys)
1626
+ pinned = [vertex.index for vertex in obj.data.vertices if abs(vertex.co.y - top) < 1e-4]
1627
+ group = obj.vertex_groups.new(name="DeepBlendClothPin")
1628
+ group.add(pinned, 1.0, "REPLACE")
1629
+ group_name = group.name
1630
+
1631
+ # CLOTH NEEDS GEOMETRY TO BEND, AND THE PRODUCT'S PRIMITIVES ARE LOW-POLY. MEASURED: a cube has eight
1632
+ # vertices, so a cloth modifier on one has nothing to drape with — the reading came back with the
1633
+ # fabric exactly where it started, and it looked like a broken simulation rather than a mesh with no
1634
+ # interior. A SIMPLE subdivision keeps the shape and gives the solver something to work with; the
1635
+ # reading reports the vertex count so the subdivision is visible rather than assumed.
1636
+ subdivision = obj.modifiers.new(name="DeepBlendClothSubdivision", type="SUBSURF")
1637
+ subdivision.subdivision_type = "SIMPLE"
1638
+ subdivision.levels = int(settings.get("subdivisions", 3))
1639
+ subdivision.render_levels = subdivision.levels
1640
+
1641
+ modifier = obj.modifiers.new(name="DeepBlendCloth", type="CLOTH")
1642
+ cloth_settings = modifier.settings
1643
+ if group_name is not None:
1644
+ cloth_settings.vertex_group_mass = group_name
1645
+ if settings.get("mass") is not None:
1646
+ cloth_settings.mass = float(settings["mass"])
1647
+ if settings.get("stiffness") is not None:
1648
+ cloth_settings.tension_stiffness = float(settings["stiffness"]) * 15.0
1649
+ return obj
1650
+
1651
+
1652
+ def cloth_reading(obj, first_frame, last_frame):
1653
+ """The lowest and the highest simulated vertex, at each end of the range.
1654
+
1655
+ Read off the EVALUATED mesh rather than the modifier: what the fabric did is the geometry, and the
1656
+ modifier only says what was asked for. The highest point is the pinned edge, which is how the
1657
+ reading says "it draped FROM somewhere" rather than merely "it moved".
1658
+ """
1659
+ scene = bpy.context.scene
1660
+ reading = {}
1661
+ depsgraph = bpy.context.evaluated_depsgraph_get()
1662
+ for label, frame in (("firstFrame", first_frame), ("lastFrame", last_frame)):
1663
+ scene.frame_set(frame)
1664
+ bpy.context.view_layer.update()
1665
+ depsgraph = bpy.context.evaluated_depsgraph_get()
1666
+ evaluated = obj.evaluated_get(depsgraph)
1667
+ mesh = evaluated.to_mesh()
1668
+ heights = [(obj.matrix_world @ vertex.co).z for vertex in mesh.vertices]
1669
+ evaluated.to_mesh_clear()
1670
+ if heights:
1671
+ reading[label + "LowestZ"] = round(min(heights), 6)
1672
+ 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".
1677
+ reading[label + "Vertices"] = len(heights)
1678
+ if "firstFrameLowestZ" in reading and "lastFrameLowestZ" in reading:
1679
+ reading["droppedZ"] = round(reading["firstFrameLowestZ"] - reading["lastFrameLowestZ"], 6)
1680
+ return reading
1681
+
1682
+
1587
1683
  def build_scene(spec, options, guard):
1588
1684
  """Compile a SceneSpec into a live Blender scene.
1589
1685
 
@@ -1762,8 +1858,21 @@ def build_scene(spec, options, guard):
1762
1858
  "dataPaths": sorted({curve.data_path for curve in action_fcurves(action)}),
1763
1859
  })
1764
1860
 
1861
+ # CLOTH ENTRIES ARE RESOLVED HERE, and the modifiers are created inside the simulation step so that
1862
+ # everything the bake has to walk is in place before it runs.
1765
1863
  report_progress("build_simulation", 70)
1766
- simulation_reading = build_rigid_bodies(spec, entity_objects, guard)
1864
+ cloth_entities = {}
1865
+ for entity in spec.get("entities") or []:
1866
+ entry = entity.get("cloth")
1867
+ if entry is None:
1868
+ continue
1869
+ if entity_objects.get(entity["id"]) is None:
1870
+ raise ActionError(
1871
+ "SCENE_SPEC_INVALID",
1872
+ 'entity "%s" declares cloth but has no object to hang it on' % (entity["id"],),
1873
+ )
1874
+ cloth_entities[entity["id"]] = entry
1875
+ simulation_reading = build_rigid_bodies(spec, entity_objects, guard, cloth_entities)
1767
1876
 
1768
1877
  report_progress("configure_render", 85)
1769
1878
  render_config = configure_scene(scene, spec, profile, guard)