@deepblend/dsh-blender-provider-local 0.2.18 → 0.2.20
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.20",
|
|
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.20"
|
|
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, cloth_entities=None):
|
|
1489
|
+
def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None, soft_entities=None, fluid_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,11 @@ 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 {}
|
|
1504
|
+
fluid_entities = fluid_entities or {}
|
|
1505
|
+
# ONLY THE DOMAINS HAVE A READING: the flows are what pours in, and what the caller wants to know is
|
|
1506
|
+
# how much liquid there is, which is a property of the domain.
|
|
1507
|
+
fluid_domains = {key: value for key, value in fluid_entities.items() if value.get("role") == "domain"}
|
|
1503
1508
|
settings = spec.get("simulation") or {}
|
|
1504
1509
|
scene = bpy.context.scene
|
|
1505
1510
|
if "gravity" in settings:
|
|
@@ -1544,7 +1549,7 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None):
|
|
|
1544
1549
|
obj.rigid_body.friction = float(entry["friction"])
|
|
1545
1550
|
bodies[entity["id"]] = entry
|
|
1546
1551
|
|
|
1547
|
-
if not bodies and not cloth_entities:
|
|
1552
|
+
if not bodies and not cloth_entities and not soft_entities and not fluid_entities:
|
|
1548
1553
|
return {}
|
|
1549
1554
|
|
|
1550
1555
|
# A SCENE WITH CLOTH AND NO RIGID BODIES STILL HAS CACHES TO BAKE. MEASURED: the first version
|
|
@@ -1563,6 +1568,18 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None):
|
|
|
1563
1568
|
for entity_id, entry in cloth_entities.items():
|
|
1564
1569
|
build_cloth(entry, entity_objects[entity_id], guard)
|
|
1565
1570
|
|
|
1571
|
+
# SOFT BODIES ARE BUILT IN THE SAME PLACE, AND FOR THE SAME MEASURED REASON: MEASURED, and unlike
|
|
1572
|
+
# cloth, a soft body does NOT move without a bake (1.75 -> 1.75 before, 1.75 -> 1.6252 after), so its
|
|
1573
|
+
# modifier has to exist before `bake_all` — the three mechanisms behave three different ways, and each
|
|
1574
|
+
# one was measured rather than assumed from the last.
|
|
1575
|
+
for entity_id, entry in soft_entities.items():
|
|
1576
|
+
build_soft_body(entry, entity_objects[entity_id], guard)
|
|
1577
|
+
|
|
1578
|
+
# FLUIDS ARE BUILT BEFORE THE BAKE TOO, for the same reason as everything else: `bake_all` walks the
|
|
1579
|
+
# caches that exist, and a modifier created afterwards has no cache to walk.
|
|
1580
|
+
for entity_id, entry in fluid_entities.items():
|
|
1581
|
+
build_fluid(entry, entity_objects[entity_id], guard)
|
|
1582
|
+
|
|
1566
1583
|
reading = {}
|
|
1567
1584
|
first_frame = int((spec.get("project") or {}).get("frameStart", 1))
|
|
1568
1585
|
last_frame = int((spec.get("project") or {}).get("frameEnd", first_frame))
|
|
@@ -1578,11 +1595,33 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None):
|
|
|
1578
1595
|
"message": "the simulation could not be baked: %s" % (error_text(exc),),
|
|
1579
1596
|
})
|
|
1580
1597
|
|
|
1598
|
+
# THE FLUID BAKE IS A DIFFERENT OPERATOR FROM THE POINT CACHES, and this is the second measurement
|
|
1599
|
+
# carried over from the last round: `ptcache.bake_all` bakes cloth and soft bodies, while a fluid
|
|
1600
|
+
# domain needs `fluid.bake_all` — and THAT operator reads the CONTEXT, so the domain has to be the
|
|
1601
|
+
# active object or it refuses with "Invalid domain" (the first carried measurement).
|
|
1602
|
+
if fluid_domains:
|
|
1603
|
+
for entity_id in fluid_domains:
|
|
1604
|
+
bpy.context.view_layer.objects.active = entity_objects[entity_id]
|
|
1605
|
+
try:
|
|
1606
|
+
bpy.ops.fluid.bake_all()
|
|
1607
|
+
except Exception as exc:
|
|
1608
|
+
guard.warnings.append({
|
|
1609
|
+
"code": "FLUID_BAKE_FAILED",
|
|
1610
|
+
"message": 'the fluid domain "%s" could not be baked: %s' % (entity_id, error_text(exc)),
|
|
1611
|
+
})
|
|
1612
|
+
break
|
|
1613
|
+
|
|
1614
|
+
for entity_id in soft_entities:
|
|
1615
|
+
reading[entity_id] = {"kind": "soft-body", **deform_reading(entity_objects[entity_id], first_frame, last_frame)}
|
|
1616
|
+
|
|
1617
|
+
# AFTER THE BAKE, because the liquid only exists once the mesh cache has been written.
|
|
1618
|
+
reading.update(fluid_reading(fluid_domains, entity_objects, first_frame, last_frame, guard))
|
|
1619
|
+
|
|
1581
1620
|
for entity_id in cloth_entities:
|
|
1582
1621
|
# INTO THE SAME READING as the rigid bodies: one report of what the physics did, whatever the
|
|
1583
1622
|
# mechanism, because a caller asking "what happened in this scene" should not have to know which
|
|
1584
1623
|
# kind of simulation each entity used.
|
|
1585
|
-
reading[entity_id] = {"kind": "cloth", **
|
|
1624
|
+
reading[entity_id] = {"kind": "cloth", **deform_reading(entity_objects[entity_id], first_frame, last_frame)}
|
|
1586
1625
|
|
|
1587
1626
|
for entity_id in bodies:
|
|
1588
1627
|
obj = entity_objects[entity_id]
|
|
@@ -1649,9 +1688,14 @@ def build_cloth(entry, obj, guard):
|
|
|
1649
1688
|
return obj
|
|
1650
1689
|
|
|
1651
1690
|
|
|
1652
|
-
def
|
|
1691
|
+
def deform_reading(obj, first_frame, last_frame):
|
|
1653
1692
|
"""The lowest and the highest simulated vertex, at each end of the range.
|
|
1654
1693
|
|
|
1694
|
+
SHARED BY EVERYTHING THAT DEFORMS A MESH rather than copied per mechanism: cloth and soft bodies ask
|
|
1695
|
+
the same question of the same evaluated geometry, and a second copy is the one that would miss the
|
|
1696
|
+
next fix — this reading already needed one when it turned out to be taken from the base mesh.
|
|
1697
|
+
|
|
1698
|
+
|
|
1655
1699
|
Read off the EVALUATED mesh rather than the modifier: what the fabric did is the geometry, and the
|
|
1656
1700
|
modifier only says what was asked for. The highest point is the pinned edge, which is how the
|
|
1657
1701
|
reading says "it draped FROM somewhere" rather than merely "it moved".
|
|
@@ -1670,16 +1714,152 @@ def cloth_reading(obj, first_frame, last_frame):
|
|
|
1670
1714
|
if heights:
|
|
1671
1715
|
reading[label + "LowestZ"] = round(min(heights), 6)
|
|
1672
1716
|
reading[label + "HighestZ"] = round(max(heights), 6)
|
|
1673
|
-
# THE VERTEX
|
|
1674
|
-
#
|
|
1675
|
-
#
|
|
1676
|
-
#
|
|
1717
|
+
# THE VERTEX COUNTS ARE PART OF THE READING, not diagnostics. MEASURED, and a surviving
|
|
1718
|
+
# mutation is why there are TWO of them: "the fabric had at least 64 vertices" passed on a
|
|
1719
|
+
# generator that already had that many, so it proved nothing about the subdivision. What
|
|
1720
|
+
# proves it is the RATIO — how many vertices the evaluated mesh has against how many the
|
|
1721
|
+
# object was built with — and that needs both numbers to be read.
|
|
1677
1722
|
reading[label + "Vertices"] = len(heights)
|
|
1723
|
+
reading[label + "SourceVertices"] = len(obj.data.vertices)
|
|
1678
1724
|
if "firstFrameLowestZ" in reading and "lastFrameLowestZ" in reading:
|
|
1679
1725
|
reading["droppedZ"] = round(reading["firstFrameLowestZ"] - reading["lastFrameLowestZ"], 6)
|
|
1680
1726
|
return reading
|
|
1681
1727
|
|
|
1682
1728
|
|
|
1729
|
+
def build_soft_body(entry, obj, guard):
|
|
1730
|
+
"""Give a mesh soft-body physics: it deforms, and it remembers its shape.
|
|
1731
|
+
|
|
1732
|
+
THE GOAL IS WHAT MAKES IT A SOFT BODY rather than cloth — `goal` is how strongly each vertex wants to
|
|
1733
|
+
return to where it started, and Blender's default is high (0.7). MEASURED: a 0.5m cube dropped 25
|
|
1734
|
+
frames falls 0.125m and keeps its 0.5m spread, which is exactly what a body with a goal should do,
|
|
1735
|
+
and both halves of that are asserted so a future default change cannot quietly turn it into cloth.
|
|
1736
|
+
"""
|
|
1737
|
+
settings = entry or {}
|
|
1738
|
+
|
|
1739
|
+
# THE SUBDIVISION IS NOT OPTIONAL, for the same measured reason as cloth: a primitive has no interior
|
|
1740
|
+
# to deform, so a soft body on an eight-vertex cube is a cube that moves.
|
|
1741
|
+
subdivision = obj.modifiers.new(name="DeepBlendSoftBodySubdivision", type="SUBSURF")
|
|
1742
|
+
subdivision.subdivision_type = "SIMPLE"
|
|
1743
|
+
subdivision.levels = int(settings.get("subdivisions", 3))
|
|
1744
|
+
subdivision.render_levels = subdivision.levels
|
|
1745
|
+
|
|
1746
|
+
modifier = obj.modifiers.new(name="DeepBlendSoftBody", type="SOFT_BODY")
|
|
1747
|
+
body_settings = modifier.settings
|
|
1748
|
+
if settings.get("mass") is not None:
|
|
1749
|
+
body_settings.mass = float(settings["mass"])
|
|
1750
|
+
if settings.get("goal") is not None:
|
|
1751
|
+
body_settings.goal_default = float(settings["goal"])
|
|
1752
|
+
|
|
1753
|
+
# PINNING IS EXPRESSED AS A GOAL, which is this modifier's own word for it: a vertex group at goal
|
|
1754
|
+
# 1.0 holds its vertices exactly, and one at 0.0 lets them go.
|
|
1755
|
+
if settings.get("pinTop"):
|
|
1756
|
+
ys = [vertex.co.y for vertex in obj.data.vertices]
|
|
1757
|
+
if ys:
|
|
1758
|
+
top = max(ys)
|
|
1759
|
+
pinned = [vertex.index for vertex in obj.data.vertices if abs(vertex.co.y - top) < 1e-4]
|
|
1760
|
+
group = obj.vertex_groups.new(name="DeepBlendSoftBodyPin")
|
|
1761
|
+
group.add(pinned, 1.0, "REPLACE")
|
|
1762
|
+
body_settings.use_goal = True
|
|
1763
|
+
body_settings.vertex_group_goal = group.name
|
|
1764
|
+
body_settings.goal_min = 1.0
|
|
1765
|
+
body_settings.goal_max = 1.0
|
|
1766
|
+
return obj
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
DOMAIN_TYPE_BY_NAME = {"liquid": "LIQUID", "gas": "GAS"}
|
|
1770
|
+
FLOW_TYPE_BY_NAME = {"liquid": "LIQUID", "smoke": "SMOKE", "fire": "FIRE", "both": "BOTH"}
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
def build_fluid(entry, obj, guard):
|
|
1774
|
+
"""Give one object its part in a fluid simulation.
|
|
1775
|
+
|
|
1776
|
+
THE ROLE IS THE WHOLE REASON THIS IS ONE FIELD RATHER THAN A FLAG: a fluid is at least two entities —
|
|
1777
|
+
a domain, which is the volume the liquid lives in, and something inside it that emits. There is no
|
|
1778
|
+
such thing as a single entity that is a fluid.
|
|
1779
|
+
|
|
1780
|
+
AND THE DOMAIN TYPE IS THE POINT OF THIS SLICE. MEASURED, and it is why the product's default is
|
|
1781
|
+
`liquid` rather than Blender's `gas`: a LIQUID inflow into a GAS domain produces NOTHING — the bake
|
|
1782
|
+
reports success, both caches report baked, and the domain's evaluated mesh is its own cube. Nothing
|
|
1783
|
+
in that chain says "you chose the wrong domain type", which is exactly the kind of silence this
|
|
1784
|
+
product is built to avoid.
|
|
1785
|
+
"""
|
|
1786
|
+
role = entry.get("role")
|
|
1787
|
+
modifier = obj.modifiers.new(name="DeepBlendFluid", type="FLUID")
|
|
1788
|
+
if role == "domain":
|
|
1789
|
+
modifier.fluid_type = "DOMAIN"
|
|
1790
|
+
settings = modifier.domain_settings
|
|
1791
|
+
settings.domain_type = DOMAIN_TYPE_BY_NAME.get(entry.get("domainType", "liquid"), "LIQUID")
|
|
1792
|
+
settings.resolution_max = int(entry.get("resolution", 32))
|
|
1793
|
+
# THE SURFACE HAS TO BE ASKED FOR. `use_mesh` defaults to true and the mesh cache is baked by
|
|
1794
|
+
# `fluid.bake_all`, but a build that turns either off gets a domain with no liquid surface and no
|
|
1795
|
+
# indication that anything is missing, so both are set explicitly rather than inherited.
|
|
1796
|
+
settings.use_mesh = True
|
|
1797
|
+
elif role in ("inflow", "outflow"):
|
|
1798
|
+
modifier.fluid_type = "FLOW"
|
|
1799
|
+
settings = modifier.flow_settings
|
|
1800
|
+
settings.flow_type = FLOW_TYPE_BY_NAME.get(entry.get("flowType", "liquid"), "LIQUID")
|
|
1801
|
+
settings.flow_behavior = "INFLOW" if role == "inflow" else "OUTFLOW"
|
|
1802
|
+
else:
|
|
1803
|
+
raise ActionError(
|
|
1804
|
+
"SCENE_SPEC_INVALID",
|
|
1805
|
+
'fluid entity "%s" has role "%s"; expected domain, inflow or outflow'
|
|
1806
|
+
% (obj.get("deepblend_id"), role),
|
|
1807
|
+
)
|
|
1808
|
+
return obj
|
|
1809
|
+
|
|
1810
|
+
|
|
1811
|
+
def fluid_reading(domain_entries, entity_objects, first_frame, last_frame, guard):
|
|
1812
|
+
"""How much liquid there is, read as the DOMAIN's evaluated mesh against its own cube.
|
|
1813
|
+
|
|
1814
|
+
THE RATIO IS THE READING, for the same measured reason as the subdivision check: a count says nothing
|
|
1815
|
+
on its own, because a domain with no liquid still evaluates to its own eight-vertex cube. Comparing
|
|
1816
|
+
the evaluated mesh against the mesh the object was built with is what tells "there is liquid in here"
|
|
1817
|
+
apart from "there is a box here".
|
|
1818
|
+
"""
|
|
1819
|
+
scene = bpy.context.scene
|
|
1820
|
+
reading = {}
|
|
1821
|
+
for entity_id in domain_entries:
|
|
1822
|
+
obj = entity_objects[entity_id]
|
|
1823
|
+
entry = {}
|
|
1824
|
+
for label, frame in (("firstFrame", first_frame), ("lastFrame", last_frame)):
|
|
1825
|
+
scene.frame_set(frame)
|
|
1826
|
+
bpy.context.view_layer.update()
|
|
1827
|
+
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
1828
|
+
evaluated = obj.evaluated_get(depsgraph)
|
|
1829
|
+
mesh = evaluated.to_mesh()
|
|
1830
|
+
entry[label + "Vertices"] = len(mesh.vertices)
|
|
1831
|
+
evaluated.to_mesh_clear()
|
|
1832
|
+
entry["sourceVertices"] = len(obj.data.vertices)
|
|
1833
|
+
settings = obj.modifiers["DeepBlendFluid"].domain_settings
|
|
1834
|
+
entry["domainType"] = settings.domain_type
|
|
1835
|
+
entry["meshCacheBaked"] = bool(settings.has_cache_baked_mesh)
|
|
1836
|
+
entry["dataCacheBaked"] = bool(settings.has_cache_baked_data)
|
|
1837
|
+
entry["resolution"] = settings.resolution_max
|
|
1838
|
+
entry["useMesh"] = bool(settings.use_mesh)
|
|
1839
|
+
flows = [other for other in obj.users_scene[0].objects
|
|
1840
|
+
if other.modifiers.get("DeepBlendFluid") is not None
|
|
1841
|
+
and other.modifiers["DeepBlendFluid"].fluid_type == "FLOW"]
|
|
1842
|
+
entry["flows"] = [(other.name, other.modifiers["DeepBlendFluid"].flow_settings.flow_behavior,
|
|
1843
|
+
len(other.data.vertices) if other.type == "MESH" else 0) for other in flows]
|
|
1844
|
+
# A DOMAIN THAT PRODUCED NO LIQUID IS A WARNING, NOT A SILENCE. Every signal along the way says
|
|
1845
|
+
# it worked — the bake returns, both caches report baked, the domain type is what was asked for —
|
|
1846
|
+
# and the evaluated mesh is still the domain's own box. MEASURED, and it is the reason this
|
|
1847
|
+
# product's `domainType` default is `liquid` rather than Blender's `gas`; a build that asks for
|
|
1848
|
+
# something else, or whose inflow is finer than the grid, lands here and should be told.
|
|
1849
|
+
if entry["lastFrameVertices"] <= entry["sourceVertices"]:
|
|
1850
|
+
guard.warnings.append({
|
|
1851
|
+
"code": "FLUID_NO_LIQUID",
|
|
1852
|
+
"message": 'fluid domain "%s" baked but its evaluated mesh is still the domain itself '
|
|
1853
|
+
"(%d vertices against %d), so no liquid was produced — check that the domain "
|
|
1854
|
+
"type matches what the flows emit, and that its resolution is fine enough for "
|
|
1855
|
+
"them to be seen"
|
|
1856
|
+
% (entity_id, entry["lastFrameVertices"], entry["sourceVertices"]),
|
|
1857
|
+
"detail": {key: entry[key] for key in ("domainType", "resolution", "flows") if key in entry},
|
|
1858
|
+
})
|
|
1859
|
+
reading[entity_id] = {"kind": "fluid", **entry}
|
|
1860
|
+
return reading
|
|
1861
|
+
|
|
1862
|
+
|
|
1683
1863
|
def build_scene(spec, options, guard):
|
|
1684
1864
|
"""Compile a SceneSpec into a live Blender scene.
|
|
1685
1865
|
|
|
@@ -1872,7 +2052,34 @@ def build_scene(spec, options, guard):
|
|
|
1872
2052
|
'entity "%s" declares cloth but has no object to hang it on' % (entity["id"],),
|
|
1873
2053
|
)
|
|
1874
2054
|
cloth_entities[entity["id"]] = entry
|
|
1875
|
-
|
|
2055
|
+
soft_entities = {}
|
|
2056
|
+
for entity in spec.get("entities") or []:
|
|
2057
|
+
entry = entity.get("softBody")
|
|
2058
|
+
if entry is None:
|
|
2059
|
+
continue
|
|
2060
|
+
if entity_objects.get(entity["id"]) is None:
|
|
2061
|
+
raise ActionError(
|
|
2062
|
+
"SCENE_SPEC_INVALID",
|
|
2063
|
+
'entity "%s" declares a soft body but has no object to simulate' % (entity["id"],),
|
|
2064
|
+
)
|
|
2065
|
+
soft_entities[entity["id"]] = entry
|
|
2066
|
+
fluid_entities = {}
|
|
2067
|
+
for entity in spec.get("entities") or []:
|
|
2068
|
+
entry = entity.get("fluid")
|
|
2069
|
+
if entry is None:
|
|
2070
|
+
continue
|
|
2071
|
+
if entity_objects.get(entity["id"]) is None:
|
|
2072
|
+
raise ActionError(
|
|
2073
|
+
"SCENE_SPEC_INVALID",
|
|
2074
|
+
'entity "%s" declares a fluid role but has no object to give it to' % (entity["id"],),
|
|
2075
|
+
)
|
|
2076
|
+
fluid_entities[entity["id"]] = entry
|
|
2077
|
+
if len([entry for entry in fluid_entities.values() if entry.get("role") == "domain"]) == 0 and fluid_entities:
|
|
2078
|
+
raise ActionError(
|
|
2079
|
+
"SCENE_SPEC_INVALID",
|
|
2080
|
+
"the scene declares fluid objects but no domain, so the liquid would have nowhere to be",
|
|
2081
|
+
)
|
|
2082
|
+
simulation_reading = build_rigid_bodies(spec, entity_objects, guard, cloth_entities, soft_entities, fluid_entities)
|
|
1876
2083
|
|
|
1877
2084
|
report_progress("configure_render", 85)
|
|
1878
2085
|
render_config = configure_scene(scene, spec, profile, guard)
|