@deepblend/dsh-blender-provider-local 0.2.16 → 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 +2 -2
- package/python/__pycache__/bootstrap.cpython-313.pyc +0 -0
- package/python/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_scene.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_scene.cpython-314.pyc +0 -0
- package/python/bootstrap.py +7 -0
- package/python/deepblend_scene.py +211 -0
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.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.
|
|
14
|
+
"@deepblend/dsh-blender-contracts": "0.2.18"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/python/bootstrap.py
CHANGED
|
@@ -233,6 +233,13 @@ def action_compile_scene(request, options):
|
|
|
233
233
|
# WHICH ENTITIES ARE SKINNED, AND TO WHAT. The vertex groups are the evidence that weights
|
|
234
234
|
# exist at all: an entity bound to an armature with no groups renders as a static object while
|
|
235
235
|
# the file says it is rigged, and the reading is where that difference becomes visible.
|
|
236
|
+
# WHAT THE PHYSICS DID, per entity. Not the settings: a body that is configured and never
|
|
237
|
+
# enabled looks exactly like one that fell, and only a reading of where it ended up tells them
|
|
238
|
+
# apart.
|
|
239
|
+
"simulation": [
|
|
240
|
+
{"entityId": entity_id, **entry}
|
|
241
|
+
for entity_id, entry in sorted((report.get("simulation") or {}).items())
|
|
242
|
+
],
|
|
236
243
|
"skinned": [
|
|
237
244
|
{
|
|
238
245
|
"entityId": entity_id,
|
|
@@ -1486,6 +1486,200 @@ 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):
|
|
1490
|
+
"""Give the entities their physics, and BAKE the result into the checkpoint.
|
|
1491
|
+
|
|
1492
|
+
WHY THE COMPILE BAKES: a rigid body that has not been baked is a SETTING, not a motion — Blender
|
|
1493
|
+
evaluates it on the fly in the viewport and a batch render of an unbaked simulation renders the
|
|
1494
|
+
initial pose. Baking here is what makes the checkpoint mean "this is what the simulation did", and
|
|
1495
|
+
it is why `simulation.bake: false` exists for a build that only wants the setup.
|
|
1496
|
+
|
|
1497
|
+
Returns a reading of what the physics DID, not of what it was told: each active body's height at the
|
|
1498
|
+
first and the last frame of the scene's range. MEASURED, and the lesson from the animation slice is
|
|
1499
|
+
why this is not just "the setup exists": a body that is configured correctly and never enabled looks
|
|
1500
|
+
exactly like one that fell, unless something reads where it ended up.
|
|
1501
|
+
"""
|
|
1502
|
+
cloth_entities = cloth_entities or {}
|
|
1503
|
+
settings = spec.get("simulation") or {}
|
|
1504
|
+
scene = bpy.context.scene
|
|
1505
|
+
if "gravity" in settings:
|
|
1506
|
+
scene.gravity[2] = -abs(float(settings["gravity"]))
|
|
1507
|
+
|
|
1508
|
+
# THE PHYSICS WORLD HAS TO EXIST BEFORE A BODY CAN BE ATTACHED TO IT. MEASURED: without this line
|
|
1509
|
+
# `obj.rigid_body` is None and the assignment fails with an AttributeError — Blender's rigid-body
|
|
1510
|
+
# settings live on a scene-level world, and a scene that has never had one has nowhere to put them.
|
|
1511
|
+
if scene.rigidbody_world is None:
|
|
1512
|
+
bpy.ops.rigidbody.world_add()
|
|
1513
|
+
# AND THE WORLD NEEDS A COLLECTION, which a fresh one does not have. MEASURED, in three steps that
|
|
1514
|
+
# each looked like it should work: without the world there is nowhere to put a body; with the world
|
|
1515
|
+
# but no collection, `world.collection` is None; and only an object LINKED INTO that collection
|
|
1516
|
+
# gets its `rigid_body` materialised. The Blender UI does all three when a user clicks "Rigid Body".
|
|
1517
|
+
if scene.rigidbody_world.collection is None:
|
|
1518
|
+
collection = bpy.data.collections.new("DeepBlendRigidBodies")
|
|
1519
|
+
scene.collection.children.link(collection)
|
|
1520
|
+
scene.rigidbody_world.collection = collection
|
|
1521
|
+
|
|
1522
|
+
bodies = {}
|
|
1523
|
+
for entity in spec.get("entities") or []:
|
|
1524
|
+
entry = entity.get("rigidBody")
|
|
1525
|
+
if entry is None:
|
|
1526
|
+
continue
|
|
1527
|
+
obj = entity_objects.get(entity["id"])
|
|
1528
|
+
if obj is None:
|
|
1529
|
+
raise ActionError(
|
|
1530
|
+
"SCENE_SPEC_INVALID",
|
|
1531
|
+
'entity "%s" declares a rigid body but has no object to attach it to' % (entity["id"],),
|
|
1532
|
+
)
|
|
1533
|
+
# JOINING THE PHYSICS COLLECTION IS WHAT CREATES `obj.rigid_body`. MEASURED, twice: adding the
|
|
1534
|
+
# world is not enough — Blender materialises the rigid-body settings when the object is linked
|
|
1535
|
+
# into the world's own collection, so an object that has "a rigid body" the obvious way is an
|
|
1536
|
+
# object with `None`.
|
|
1537
|
+
if obj.name not in scene.rigidbody_world.collection.objects:
|
|
1538
|
+
scene.rigidbody_world.collection.objects.link(obj)
|
|
1539
|
+
|
|
1540
|
+
obj.rigid_body.type = "ACTIVE" if entry["kind"] == "active" else "PASSIVE"
|
|
1541
|
+
if entry.get("mass") is not None:
|
|
1542
|
+
obj.rigid_body.mass = float(entry["mass"])
|
|
1543
|
+
if entry.get("friction") is not None:
|
|
1544
|
+
obj.rigid_body.friction = float(entry["friction"])
|
|
1545
|
+
bodies[entity["id"]] = entry
|
|
1546
|
+
|
|
1547
|
+
if not bodies and not cloth_entities:
|
|
1548
|
+
return {}
|
|
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
|
+
|
|
1555
|
+
# THE GROUND BODIES DO NOT MOVE, so they need the physics world to have something to collide against
|
|
1556
|
+
# and Blender needs to be told the world exists at all.
|
|
1557
|
+
scene.use_gravity = True
|
|
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
|
+
|
|
1566
|
+
reading = {}
|
|
1567
|
+
first_frame = int((spec.get("project") or {}).get("frameStart", 1))
|
|
1568
|
+
last_frame = int((spec.get("project") or {}).get("frameEnd", first_frame))
|
|
1569
|
+
if settings.get("bake", True):
|
|
1570
|
+
scene.frame_set(first_frame)
|
|
1571
|
+
# `bake_all` walks every cache in the scene, which is what a compile wants: the caller asked for
|
|
1572
|
+
# a checkpoint that shows the simulation, not for one cache to be fresh.
|
|
1573
|
+
try:
|
|
1574
|
+
bpy.ops.ptcache.bake_all(bake=True)
|
|
1575
|
+
except Exception as exc:
|
|
1576
|
+
guard.warnings.append({
|
|
1577
|
+
"code": "SIMULATION_BAKE_FAILED",
|
|
1578
|
+
"message": "the simulation could not be baked: %s" % (error_text(exc),),
|
|
1579
|
+
})
|
|
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
|
+
|
|
1587
|
+
for entity_id in bodies:
|
|
1588
|
+
obj = entity_objects[entity_id]
|
|
1589
|
+
scene.frame_set(first_frame)
|
|
1590
|
+
bpy.context.view_layer.update()
|
|
1591
|
+
first = float((obj.matrix_world.translation)[2])
|
|
1592
|
+
scene.frame_set(last_frame)
|
|
1593
|
+
bpy.context.view_layer.update()
|
|
1594
|
+
last = float((obj.matrix_world.translation)[2])
|
|
1595
|
+
reading[entity_id] = {
|
|
1596
|
+
"kind": bodies[entity_id]["kind"],
|
|
1597
|
+
"firstFrameZ": round(first, 6),
|
|
1598
|
+
"lastFrameZ": round(last, 6),
|
|
1599
|
+
"droppedZ": round(first - last, 6),
|
|
1600
|
+
}
|
|
1601
|
+
scene.frame_set(first_frame)
|
|
1602
|
+
bpy.context.view_layer.update()
|
|
1603
|
+
return reading
|
|
1604
|
+
|
|
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
|
+
|
|
1489
1683
|
def build_scene(spec, options, guard):
|
|
1490
1684
|
"""Compile a SceneSpec into a live Blender scene.
|
|
1491
1685
|
|
|
@@ -1664,6 +1858,22 @@ def build_scene(spec, options, guard):
|
|
|
1664
1858
|
"dataPaths": sorted({curve.data_path for curve in action_fcurves(action)}),
|
|
1665
1859
|
})
|
|
1666
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.
|
|
1863
|
+
report_progress("build_simulation", 70)
|
|
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)
|
|
1876
|
+
|
|
1667
1877
|
report_progress("configure_render", 85)
|
|
1668
1878
|
render_config = configure_scene(scene, spec, profile, guard)
|
|
1669
1879
|
|
|
@@ -1708,6 +1918,7 @@ def build_scene(spec, options, guard):
|
|
|
1708
1918
|
"profileName": profile_name,
|
|
1709
1919
|
"entityObjectNames": {key: value.name for key, value in entity_objects.items()},
|
|
1710
1920
|
"armatureNames": {key: value.name for key, value in armature_objects.items()},
|
|
1921
|
+
"simulation": simulation_reading,
|
|
1711
1922
|
"skinned": {
|
|
1712
1923
|
key: {
|
|
1713
1924
|
"armatureId": entry["armatureId"],
|