@deepblend/dsh-blender-provider-local 0.2.19 → 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, soft_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
|
|
@@ -1501,6 +1501,10 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None, soft_en
|
|
|
1501
1501
|
"""
|
|
1502
1502
|
cloth_entities = cloth_entities or {}
|
|
1503
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"}
|
|
1504
1508
|
settings = spec.get("simulation") or {}
|
|
1505
1509
|
scene = bpy.context.scene
|
|
1506
1510
|
if "gravity" in settings:
|
|
@@ -1545,7 +1549,7 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None, soft_en
|
|
|
1545
1549
|
obj.rigid_body.friction = float(entry["friction"])
|
|
1546
1550
|
bodies[entity["id"]] = entry
|
|
1547
1551
|
|
|
1548
|
-
if not bodies and not cloth_entities and not soft_entities:
|
|
1552
|
+
if not bodies and not cloth_entities and not soft_entities and not fluid_entities:
|
|
1549
1553
|
return {}
|
|
1550
1554
|
|
|
1551
1555
|
# A SCENE WITH CLOTH AND NO RIGID BODIES STILL HAS CACHES TO BAKE. MEASURED: the first version
|
|
@@ -1571,6 +1575,11 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None, soft_en
|
|
|
1571
1575
|
for entity_id, entry in soft_entities.items():
|
|
1572
1576
|
build_soft_body(entry, entity_objects[entity_id], guard)
|
|
1573
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
|
+
|
|
1574
1583
|
reading = {}
|
|
1575
1584
|
first_frame = int((spec.get("project") or {}).get("frameStart", 1))
|
|
1576
1585
|
last_frame = int((spec.get("project") or {}).get("frameEnd", first_frame))
|
|
@@ -1586,9 +1595,28 @@ def build_rigid_bodies(spec, entity_objects, guard, cloth_entities=None, soft_en
|
|
|
1586
1595
|
"message": "the simulation could not be baked: %s" % (error_text(exc),),
|
|
1587
1596
|
})
|
|
1588
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
|
+
|
|
1589
1614
|
for entity_id in soft_entities:
|
|
1590
1615
|
reading[entity_id] = {"kind": "soft-body", **deform_reading(entity_objects[entity_id], first_frame, last_frame)}
|
|
1591
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
|
+
|
|
1592
1620
|
for entity_id in cloth_entities:
|
|
1593
1621
|
# INTO THE SAME READING as the rigid bodies: one report of what the physics did, whatever the
|
|
1594
1622
|
# mechanism, because a caller asking "what happened in this scene" should not have to know which
|
|
@@ -1738,6 +1766,100 @@ def build_soft_body(entry, obj, guard):
|
|
|
1738
1766
|
return obj
|
|
1739
1767
|
|
|
1740
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
|
+
|
|
1741
1863
|
def build_scene(spec, options, guard):
|
|
1742
1864
|
"""Compile a SceneSpec into a live Blender scene.
|
|
1743
1865
|
|
|
@@ -1941,7 +2063,23 @@ def build_scene(spec, options, guard):
|
|
|
1941
2063
|
'entity "%s" declares a soft body but has no object to simulate' % (entity["id"],),
|
|
1942
2064
|
)
|
|
1943
2065
|
soft_entities[entity["id"]] = entry
|
|
1944
|
-
|
|
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)
|
|
1945
2083
|
|
|
1946
2084
|
report_progress("configure_render", 85)
|
|
1947
2085
|
render_config = configure_scene(scene, spec, profile, guard)
|