@deepblend/dsh-blender-provider-local 0.2.17 → 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.
|
|
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.
|
|
14
|
+
"@deepblend/dsh-blender-contracts": "0.2.19"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
Binary file
|
|
Binary file
|
|
@@ -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, 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
|
|
@@ -1499,6 +1499,8 @@ 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 {}
|
|
1503
|
+
soft_entities = soft_entities or {}
|
|
1502
1504
|
settings = spec.get("simulation") or {}
|
|
1503
1505
|
scene = bpy.context.scene
|
|
1504
1506
|
if "gravity" in settings:
|
|
@@ -1543,13 +1545,32 @@ def build_rigid_bodies(spec, entity_objects, guard):
|
|
|
1543
1545
|
obj.rigid_body.friction = float(entry["friction"])
|
|
1544
1546
|
bodies[entity["id"]] = entry
|
|
1545
1547
|
|
|
1546
|
-
if not bodies:
|
|
1548
|
+
if not bodies and not cloth_entities and not soft_entities:
|
|
1547
1549
|
return {}
|
|
1548
1550
|
|
|
1551
|
+
# A SCENE WITH CLOTH AND NO RIGID BODIES STILL HAS CACHES TO BAKE. MEASURED: the first version
|
|
1552
|
+
# returned early when there were no bodies, and a cloth-only scene reported an empty reading — the
|
|
1553
|
+
# bake and the whole reading sat behind a condition that had nothing to do with cloth.
|
|
1554
|
+
_ = bodies
|
|
1555
|
+
|
|
1549
1556
|
# THE GROUND BODIES DO NOT MOVE, so they need the physics world to have something to collide against
|
|
1550
1557
|
# and Blender needs to be told the world exists at all.
|
|
1551
1558
|
scene.use_gravity = True
|
|
1552
1559
|
|
|
1560
|
+
# THE CLOTH MODIFIERS ARE CREATED BEFORE THE BAKE, which is the whole reason they are built here
|
|
1561
|
+
# rather than in `build_scene`. MEASURED, and the comment above this loop said so before the code
|
|
1562
|
+
# did: created after it, the bake had nothing to walk and the reading saw an object with no
|
|
1563
|
+
# modifiers — a fabric that never moved, reported as a simulation that did nothing.
|
|
1564
|
+
for entity_id, entry in cloth_entities.items():
|
|
1565
|
+
build_cloth(entry, entity_objects[entity_id], guard)
|
|
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
|
+
|
|
1553
1574
|
reading = {}
|
|
1554
1575
|
first_frame = int((spec.get("project") or {}).get("frameStart", 1))
|
|
1555
1576
|
last_frame = int((spec.get("project") or {}).get("frameEnd", first_frame))
|
|
@@ -1565,6 +1586,15 @@ def build_rigid_bodies(spec, entity_objects, guard):
|
|
|
1565
1586
|
"message": "the simulation could not be baked: %s" % (error_text(exc),),
|
|
1566
1587
|
})
|
|
1567
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
|
+
|
|
1592
|
+
for entity_id in cloth_entities:
|
|
1593
|
+
# INTO THE SAME READING as the rigid bodies: one report of what the physics did, whatever the
|
|
1594
|
+
# mechanism, because a caller asking "what happened in this scene" should not have to know which
|
|
1595
|
+
# kind of simulation each entity used.
|
|
1596
|
+
reading[entity_id] = {"kind": "cloth", **deform_reading(entity_objects[entity_id], first_frame, last_frame)}
|
|
1597
|
+
|
|
1568
1598
|
for entity_id in bodies:
|
|
1569
1599
|
obj = entity_objects[entity_id]
|
|
1570
1600
|
scene.frame_set(first_frame)
|
|
@@ -1584,6 +1614,130 @@ def build_rigid_bodies(spec, entity_objects, guard):
|
|
|
1584
1614
|
return reading
|
|
1585
1615
|
|
|
1586
1616
|
|
|
1617
|
+
def build_cloth(entry, obj, guard):
|
|
1618
|
+
"""Hang a mesh as fabric, and report where its lowest point ended up.
|
|
1619
|
+
|
|
1620
|
+
WHY THE PIN GROUP IS BUILT FROM GEOMETRY: Blender pins cloth through a vertex group, and a
|
|
1621
|
+
SceneSpec that named one would be referring to something no generator creates. What the spec says
|
|
1622
|
+
is the RELATIONSHIP ("hang this from its top edge"); the group is the compiler's answer to it.
|
|
1623
|
+
|
|
1624
|
+
MEASURED, AND IT DIFFERS FROM RIGID BODIES: cloth is evaluated LIVE, so it drapes with or without a
|
|
1625
|
+
bake — but the BAKED result differs from the unbaked one, because cloth integrates frame by frame
|
|
1626
|
+
and a bake walks every frame while a jump to the last one does not. That is why nothing here claims
|
|
1627
|
+
"no bake means no motion": for cloth that claim would be false, and a false assertion is worse than
|
|
1628
|
+
none.
|
|
1629
|
+
"""
|
|
1630
|
+
settings = entry or {}
|
|
1631
|
+
pin_top = settings.get("pinTop", True)
|
|
1632
|
+
group_name = None
|
|
1633
|
+
if pin_top:
|
|
1634
|
+
ys = [vertex.co.y for vertex in obj.data.vertices]
|
|
1635
|
+
if ys:
|
|
1636
|
+
top = max(ys)
|
|
1637
|
+
pinned = [vertex.index for vertex in obj.data.vertices if abs(vertex.co.y - top) < 1e-4]
|
|
1638
|
+
group = obj.vertex_groups.new(name="DeepBlendClothPin")
|
|
1639
|
+
group.add(pinned, 1.0, "REPLACE")
|
|
1640
|
+
group_name = group.name
|
|
1641
|
+
|
|
1642
|
+
# CLOTH NEEDS GEOMETRY TO BEND, AND THE PRODUCT'S PRIMITIVES ARE LOW-POLY. MEASURED: a cube has eight
|
|
1643
|
+
# vertices, so a cloth modifier on one has nothing to drape with — the reading came back with the
|
|
1644
|
+
# fabric exactly where it started, and it looked like a broken simulation rather than a mesh with no
|
|
1645
|
+
# interior. A SIMPLE subdivision keeps the shape and gives the solver something to work with; the
|
|
1646
|
+
# reading reports the vertex count so the subdivision is visible rather than assumed.
|
|
1647
|
+
subdivision = obj.modifiers.new(name="DeepBlendClothSubdivision", type="SUBSURF")
|
|
1648
|
+
subdivision.subdivision_type = "SIMPLE"
|
|
1649
|
+
subdivision.levels = int(settings.get("subdivisions", 3))
|
|
1650
|
+
subdivision.render_levels = subdivision.levels
|
|
1651
|
+
|
|
1652
|
+
modifier = obj.modifiers.new(name="DeepBlendCloth", type="CLOTH")
|
|
1653
|
+
cloth_settings = modifier.settings
|
|
1654
|
+
if group_name is not None:
|
|
1655
|
+
cloth_settings.vertex_group_mass = group_name
|
|
1656
|
+
if settings.get("mass") is not None:
|
|
1657
|
+
cloth_settings.mass = float(settings["mass"])
|
|
1658
|
+
if settings.get("stiffness") is not None:
|
|
1659
|
+
cloth_settings.tension_stiffness = float(settings["stiffness"]) * 15.0
|
|
1660
|
+
return obj
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
def deform_reading(obj, first_frame, last_frame):
|
|
1664
|
+
"""The lowest and the highest simulated vertex, at each end of the range.
|
|
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
|
+
|
|
1671
|
+
Read off the EVALUATED mesh rather than the modifier: what the fabric did is the geometry, and the
|
|
1672
|
+
modifier only says what was asked for. The highest point is the pinned edge, which is how the
|
|
1673
|
+
reading says "it draped FROM somewhere" rather than merely "it moved".
|
|
1674
|
+
"""
|
|
1675
|
+
scene = bpy.context.scene
|
|
1676
|
+
reading = {}
|
|
1677
|
+
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
1678
|
+
for label, frame in (("firstFrame", first_frame), ("lastFrame", last_frame)):
|
|
1679
|
+
scene.frame_set(frame)
|
|
1680
|
+
bpy.context.view_layer.update()
|
|
1681
|
+
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
1682
|
+
evaluated = obj.evaluated_get(depsgraph)
|
|
1683
|
+
mesh = evaluated.to_mesh()
|
|
1684
|
+
heights = [(obj.matrix_world @ vertex.co).z for vertex in mesh.vertices]
|
|
1685
|
+
evaluated.to_mesh_clear()
|
|
1686
|
+
if heights:
|
|
1687
|
+
reading[label + "LowestZ"] = round(min(heights), 6)
|
|
1688
|
+
reading[label + "HighestZ"] = round(max(heights), 6)
|
|
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.
|
|
1694
|
+
reading[label + "Vertices"] = len(heights)
|
|
1695
|
+
reading[label + "SourceVertices"] = len(obj.data.vertices)
|
|
1696
|
+
if "firstFrameLowestZ" in reading and "lastFrameLowestZ" in reading:
|
|
1697
|
+
reading["droppedZ"] = round(reading["firstFrameLowestZ"] - reading["lastFrameLowestZ"], 6)
|
|
1698
|
+
return reading
|
|
1699
|
+
|
|
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
|
+
|
|
1587
1741
|
def build_scene(spec, options, guard):
|
|
1588
1742
|
"""Compile a SceneSpec into a live Blender scene.
|
|
1589
1743
|
|
|
@@ -1762,8 +1916,32 @@ def build_scene(spec, options, guard):
|
|
|
1762
1916
|
"dataPaths": sorted({curve.data_path for curve in action_fcurves(action)}),
|
|
1763
1917
|
})
|
|
1764
1918
|
|
|
1919
|
+
# CLOTH ENTRIES ARE RESOLVED HERE, and the modifiers are created inside the simulation step so that
|
|
1920
|
+
# everything the bake has to walk is in place before it runs.
|
|
1765
1921
|
report_progress("build_simulation", 70)
|
|
1766
|
-
|
|
1922
|
+
cloth_entities = {}
|
|
1923
|
+
for entity in spec.get("entities") or []:
|
|
1924
|
+
entry = entity.get("cloth")
|
|
1925
|
+
if entry is None:
|
|
1926
|
+
continue
|
|
1927
|
+
if entity_objects.get(entity["id"]) is None:
|
|
1928
|
+
raise ActionError(
|
|
1929
|
+
"SCENE_SPEC_INVALID",
|
|
1930
|
+
'entity "%s" declares cloth but has no object to hang it on' % (entity["id"],),
|
|
1931
|
+
)
|
|
1932
|
+
cloth_entities[entity["id"]] = entry
|
|
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)
|
|
1767
1945
|
|
|
1768
1946
|
report_progress("configure_render", 85)
|
|
1769
1947
|
render_config = configure_scene(scene, spec, profile, guard)
|