@deepblend/dsh-blender-provider-local 0.2.20 → 0.3.0
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/lib/index.js +50 -35
- package/package.json +2 -2
- package/python/bootstrap.py +4 -2
- package/python/deepblend_anisotropy.py +110 -0
- package/python/deepblend_asset_bundle.py +193 -0
- package/python/deepblend_avif_headers.py +222 -0
- package/python/deepblend_bridge.py +46 -4
- package/python/deepblend_capabilities.py +7 -0
- package/python/deepblend_frames.py +11 -1
- package/python/deepblend_geometry.py +210 -0
- package/python/deepblend_gltf_images.py +183 -0
- package/python/deepblend_gltf_import.py +95 -0
- package/python/deepblend_gltf_roles.py +56 -0
- package/python/deepblend_image_headers.py +294 -0
- package/python/deepblend_image_roles.py +2 -0
- package/python/deepblend_images.py +203 -0
- package/python/deepblend_imported_image_headers.py +364 -0
- package/python/deepblend_mesh_checks.py +145 -0
- package/python/deepblend_obj_images.py +54 -0
- package/python/deepblend_obj_import.py +58 -0
- package/python/deepblend_obj_resources.py +79 -0
- package/python/deepblend_parts.py +106 -0
- package/python/deepblend_render.py +53 -0
- package/python/deepblend_scene.py +307 -49
- package/python/deepblend_vessel.py +192 -0
- package/python/deepblend_vessel_math.py +116 -0
- package/python/deepblend_vessel_parameters.py +65 -0
- package/python/deepblend_views.py +18 -19
- package/python/__pycache__/bootstrap.cpython-313.pyc +0 -0
- package/python/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_bridge.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_bridge.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_capabilities.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_frames.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_frames.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_render.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_render.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/__pycache__/deepblend_util.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_util.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_validate.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_views.cpython-313.pyc +0 -0
|
@@ -29,12 +29,20 @@ Runs inside Blender's embedded interpreter; ``bpy`` is available at call time.
|
|
|
29
29
|
|
|
30
30
|
import json
|
|
31
31
|
import math
|
|
32
|
+
import hashlib
|
|
32
33
|
import os
|
|
34
|
+
import re
|
|
33
35
|
|
|
34
36
|
import bpy
|
|
35
37
|
from mathutils import Vector
|
|
36
38
|
|
|
37
39
|
from deepblend_util import ActionError, Guard, as_text, error_text, report_progress, warning
|
|
40
|
+
from deepblend_asset_bundle import verify_asset_bundle, verify_unbundled_gltf_asset, verify_unbundled_obj_asset
|
|
41
|
+
from deepblend_geometry import create_lathe, create_curve, apply_model_modifiers
|
|
42
|
+
from deepblend_images import build_image_maps, build_environment, pack_imported_material_images, check_scene_image_budget
|
|
43
|
+
from deepblend_parts import isolated_import_names, stamp_imported_parts, apply_material_bindings
|
|
44
|
+
from deepblend_anisotropy import build_anisotropy, validate_anisotropy_material, validate_anisotropy_usage, validate_native_material_usage
|
|
45
|
+
from deepblend_images import validate_image_uv_usage
|
|
38
46
|
|
|
39
47
|
#: SceneSpec engine key -> Blender render engine identifier.
|
|
40
48
|
BLENDER_ENGINE_BY_KEY = {
|
|
@@ -65,6 +73,8 @@ PRINCIPLED_SOCKETS = {
|
|
|
65
73
|
"emissionStrength": ["Emission Strength"],
|
|
66
74
|
"coatWeight": ["Coat Weight", "Clearcoat"],
|
|
67
75
|
"transmissionWeight": ["Transmission Weight", "Transmission"],
|
|
76
|
+
"anisotropic": ["Anisotropic"],
|
|
77
|
+
"anisotropicRotation": ["Anisotropic Rotation"],
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
#: Layer-weight socket name for the Mix Shader node.
|
|
@@ -122,7 +132,7 @@ def reset_scene():
|
|
|
122
132
|
return bpy.context.scene
|
|
123
133
|
|
|
124
134
|
|
|
125
|
-
def build_world(scene, spec):
|
|
135
|
+
def build_world(scene, spec, options=None):
|
|
126
136
|
"""Apply the SceneSpec's `world` block, or the documented default.
|
|
127
137
|
|
|
128
138
|
The background a viewer sees behind the product is the World, and until this
|
|
@@ -155,7 +165,13 @@ def build_world(scene, spec):
|
|
|
155
165
|
if background is not None:
|
|
156
166
|
background.inputs[0].default_value = color
|
|
157
167
|
background.inputs[1].default_value = strength
|
|
158
|
-
|
|
168
|
+
report = {"declared": declared is not None, "color": list(color), "strength": strength}
|
|
169
|
+
if declared and declared.get('environment'):
|
|
170
|
+
image = build_environment(world, background, declared['environment'],
|
|
171
|
+
{asset['id']: asset for asset in spec.get('assets') or []},
|
|
172
|
+
(options or {}).get('project_root'))
|
|
173
|
+
report['environment'] = {**declared['environment'], 'image': image}
|
|
174
|
+
return report
|
|
159
175
|
raise ActionError(
|
|
160
176
|
"BLENDER_SCRIPT_ERROR",
|
|
161
177
|
"the scene world has no addressable Background node, so the declared world "
|
|
@@ -326,9 +342,8 @@ def _shade_smooth(obj):
|
|
|
326
342
|
def create_generator(name, spec):
|
|
327
343
|
"""Create the mesh for one ``generator`` entity and return the object.
|
|
328
344
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
call can be replayed to produce the same topology.
|
|
345
|
+
Primitive operators and deterministic profile meshes can be replayed to
|
|
346
|
+
produce the same topology from the same specification.
|
|
332
347
|
"""
|
|
333
348
|
shape = spec.get("shape")
|
|
334
349
|
size = float(spec.get("size", 2.0))
|
|
@@ -341,9 +356,6 @@ def create_generator(name, spec):
|
|
|
341
356
|
bpy.ops.mesh.primitive_cube_add(size=size)
|
|
342
357
|
elif shape == "rounded_box":
|
|
343
358
|
bpy.ops.mesh.primitive_cube_add(size=size)
|
|
344
|
-
bevel = spec.get("bevel") or {}
|
|
345
|
-
if bevel:
|
|
346
|
-
_apply_bevel(bpy.context.active_object, bevel)
|
|
347
359
|
elif shape == "uv_sphere":
|
|
348
360
|
bpy.ops.mesh.primitive_uv_sphere_add(radius=radius, segments=segments, ring_count=ring_count)
|
|
349
361
|
elif shape == "cylinder":
|
|
@@ -359,16 +371,27 @@ def create_generator(name, spec):
|
|
|
359
371
|
major_segments=segments,
|
|
360
372
|
minor_segments=ring_count,
|
|
361
373
|
)
|
|
374
|
+
elif shape == "lathe":
|
|
375
|
+
create_lathe(name, spec)
|
|
376
|
+
elif shape == "curve":
|
|
377
|
+
create_curve(name, spec)
|
|
378
|
+
elif shape == "handled_cup":
|
|
379
|
+
from deepblend_vessel import create_handled_cup
|
|
380
|
+
create_handled_cup(name, spec)
|
|
362
381
|
else:
|
|
363
382
|
raise ActionError(
|
|
364
383
|
"BLENDER_SCRIPT_ERROR",
|
|
365
384
|
'unknown generator shape "%s"' % (shape,),
|
|
366
|
-
{"supported": ["cube", "rounded_box", "uv_sphere", "cylinder", "cone", "plane", "torus"]},
|
|
385
|
+
{"supported": ["cube", "rounded_box", "uv_sphere", "cylinder", "cone", "plane", "torus", "lathe", "curve", "handled_cup"]},
|
|
367
386
|
)
|
|
368
387
|
|
|
369
388
|
obj = bpy.context.active_object
|
|
370
389
|
if obj is None:
|
|
371
390
|
raise ActionError("BLENDER_SCRIPT_ERROR", 'primitive for shape "%s" produced no object' % (shape,))
|
|
391
|
+
# Bevel is a generator property, not a rounded_box-only option. Silently
|
|
392
|
+
# ignoring it on cylinders removes the edge highlights product shots need.
|
|
393
|
+
if spec.get("bevel"):
|
|
394
|
+
_apply_bevel(obj, spec["bevel"])
|
|
372
395
|
obj.name = name
|
|
373
396
|
obj.data.name = "%s_mesh" % (name,)
|
|
374
397
|
return obj
|
|
@@ -384,23 +407,56 @@ def import_asset_into_scene(name, asset_path, asset_type, guard):
|
|
|
384
407
|
format's add-on — is classified as the format being unavailable rather than as
|
|
385
408
|
a generic script error, because the two need different fixes.
|
|
386
409
|
"""
|
|
387
|
-
before = set(bpy.data.objects
|
|
410
|
+
before = set(bpy.data.objects)
|
|
388
411
|
|
|
389
412
|
if asset_type in ("glb", "gltf"):
|
|
390
|
-
|
|
413
|
+
from deepblend_gltf_import import import_gltf
|
|
414
|
+
call = lambda: import_gltf(asset_path) # noqa: E731
|
|
391
415
|
elif asset_type == "fbx":
|
|
392
416
|
call = lambda: bpy.ops.import_scene.fbx(filepath=asset_path) # noqa: E731
|
|
393
417
|
elif asset_type == "obj":
|
|
394
|
-
|
|
418
|
+
from deepblend_obj_import import import_obj
|
|
419
|
+
call = lambda: import_obj(asset_path) # noqa: E731
|
|
395
420
|
elif asset_type == "usd":
|
|
396
421
|
call = lambda: bpy.ops.wm.usd_import(filepath=asset_path) # noqa: E731
|
|
397
422
|
elif asset_type == "blend":
|
|
398
|
-
call
|
|
423
|
+
def call():
|
|
424
|
+
# Read datablocks without opening the file or executing registered
|
|
425
|
+
# text scripts. One scene identifies its real members and excludes
|
|
426
|
+
# orphan object datablocks; an object-only library is also supported.
|
|
427
|
+
with bpy.data.libraries.load(asset_path, link=False) as (source, destination):
|
|
428
|
+
if len(source.scenes) > 1:
|
|
429
|
+
raise ValueError('.blend assets currently require at most one scene')
|
|
430
|
+
has_scene = bool(source.scenes)
|
|
431
|
+
if has_scene:
|
|
432
|
+
destination.scenes = list(source.scenes)
|
|
433
|
+
else:
|
|
434
|
+
destination.objects = list(source.objects)
|
|
435
|
+
source_scene = destination.scenes[0] if has_scene else None
|
|
436
|
+
imported_objects = list(source_scene.objects) if source_scene else [obj for obj in destination.objects if obj is not None]
|
|
437
|
+
if any(obj.instance_type == 'COLLECTION' for obj in imported_objects):
|
|
438
|
+
raise ValueError('.blend collection instances are not supported; realize them before importing')
|
|
439
|
+
if source_scene is not None:
|
|
440
|
+
if len(source_scene.view_layers) > 1:
|
|
441
|
+
raise ValueError('.blend multiple view layers are not supported')
|
|
442
|
+
def hidden_collection(layer):
|
|
443
|
+
return (layer.exclude or layer.hide_viewport or layer.collection.hide_render or layer.collection.hide_viewport
|
|
444
|
+
or any(hidden_collection(child) for child in layer.children))
|
|
445
|
+
if any(hidden_collection(layer.layer_collection) for layer in source_scene.view_layers):
|
|
446
|
+
raise ValueError('.blend collection or view-layer visibility overrides are not supported; use object visibility')
|
|
447
|
+
for obj in imported_objects:
|
|
448
|
+
bpy.context.scene.collection.objects.link(obj)
|
|
449
|
+
if source_scene is not None:
|
|
450
|
+
bpy.data.scenes.remove(source_scene)
|
|
451
|
+
return imported_objects
|
|
399
452
|
else:
|
|
400
453
|
raise ActionError("ASSET_FORMAT_UNAVAILABLE", 'unsupported asset type "%s"' % (asset_type,))
|
|
401
454
|
|
|
402
455
|
try:
|
|
403
|
-
|
|
456
|
+
with isolated_import_names(name):
|
|
457
|
+
loaded = call()
|
|
458
|
+
imported = loaded if asset_type == 'blend' else [obj for obj in bpy.data.objects if obj not in before]
|
|
459
|
+
stamp_imported_parts(imported)
|
|
404
460
|
except Exception as exc:
|
|
405
461
|
raise ActionError(
|
|
406
462
|
"ASSET_FORMAT_UNAVAILABLE",
|
|
@@ -408,7 +464,6 @@ def import_asset_into_scene(name, asset_path, asset_type, guard):
|
|
|
408
464
|
{"assetPath": asset_path, "assetType": asset_type},
|
|
409
465
|
)
|
|
410
466
|
|
|
411
|
-
imported = [bpy.data.objects[key] for key in bpy.data.objects.keys() if key not in before]
|
|
412
467
|
if not imported:
|
|
413
468
|
raise ActionError(
|
|
414
469
|
"ASSET_MISSING",
|
|
@@ -416,16 +471,27 @@ def import_asset_into_scene(name, asset_path, asset_type, guard):
|
|
|
416
471
|
{"assetPath": asset_path},
|
|
417
472
|
)
|
|
418
473
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
474
|
+
if asset_type in ('obj', 'gltf', 'glb'):
|
|
475
|
+
pack_imported_material_images(imported)
|
|
476
|
+
|
|
477
|
+
# The SceneSpec transform belongs to an instance container. Choosing the
|
|
478
|
+
# first imported object as the root overwrites its authored transform and
|
|
479
|
+
# flattening every parent destroys assemblies, rigs and nested transforms.
|
|
480
|
+
bpy.context.view_layer.update()
|
|
481
|
+
imported_set = set(imported)
|
|
482
|
+
root = bpy.data.objects.new(name, None)
|
|
483
|
+
bpy.context.scene.collection.objects.link(root)
|
|
484
|
+
for obj in imported:
|
|
485
|
+
if obj.parent not in imported_set:
|
|
486
|
+
world = obj.matrix_world.copy()
|
|
487
|
+
obj.parent = root
|
|
488
|
+
obj.matrix_world = world
|
|
423
489
|
guard.note(
|
|
424
490
|
"SCENE_ASSET_IMPORTED",
|
|
425
491
|
'entity "%s" imported %d object(s) from %s' % (name, len(imported), asset_path),
|
|
426
492
|
{"assetPath": asset_path, "objectCount": len(imported)},
|
|
427
493
|
)
|
|
428
|
-
return imported
|
|
494
|
+
return [root, *imported]
|
|
429
495
|
|
|
430
496
|
|
|
431
497
|
# ---------------------------------------------------------------------------
|
|
@@ -532,8 +598,8 @@ def _build_texture_graph(material, principled, texture, guard, material_id):
|
|
|
532
598
|
|
|
533
599
|
A material carrying only scalar parameters shades as one flat colour, which is
|
|
534
600
|
why paper, wood and glazed ceramic all read as the same plastic. This builds the
|
|
535
|
-
relief and variation that tells them apart
|
|
536
|
-
|
|
601
|
+
relief and variation that tells them apart. Object space is the legacy
|
|
602
|
+
default; UV space follows an authored surface layout without an image.
|
|
537
603
|
|
|
538
604
|
Every socket is looked up by name and every stage is optional: a build missing
|
|
539
605
|
one node degrades to a plainer material and says so, rather than failing the
|
|
@@ -563,12 +629,19 @@ def _build_texture_graph(material, principled, texture, guard, material_id):
|
|
|
563
629
|
)
|
|
564
630
|
return
|
|
565
631
|
|
|
566
|
-
|
|
632
|
+
mode = texture.get("coordinates", "object")
|
|
633
|
+
if mode not in ("object", "uv") or ("uvMap" in texture and
|
|
634
|
+
(mode != "uv" or not isinstance(texture['uvMap'], str) or not texture['uvMap'].strip())):
|
|
635
|
+
raise ActionError("SCENE_VALIDATION_FAILED", "procedural uvMap requires UV coordinates",
|
|
636
|
+
{"materialId": material_id})
|
|
637
|
+
coord = nodes.new("ShaderNodeUVMap" if mode == "uv" else "ShaderNodeTexCoord")
|
|
638
|
+
if mode == "uv":
|
|
639
|
+
coord.uv_map = texture.get("uvMap", "")
|
|
567
640
|
coord.location = (-1080, -240)
|
|
568
641
|
mapping = nodes.new("ShaderNodeMapping")
|
|
569
642
|
mapping.location = (-900, -240)
|
|
570
643
|
pattern.location = (-700, -240)
|
|
571
|
-
links.new(coord.outputs["Object"], mapping.inputs["Vector"])
|
|
644
|
+
links.new(coord.outputs["UV" if mode == "uv" else "Object"], mapping.inputs["Vector"])
|
|
572
645
|
links.new(mapping.outputs["Vector"], pattern.inputs["Vector"])
|
|
573
646
|
|
|
574
647
|
stretch = texture.get("stretch") or [1.0, 1.0, 1.0]
|
|
@@ -659,12 +732,16 @@ def _build_texture_graph(material, principled, texture, guard, material_id):
|
|
|
659
732
|
links.new(multiply.outputs["Vector"], base_socket)
|
|
660
733
|
|
|
661
734
|
|
|
662
|
-
def build_material(spec, guard):
|
|
735
|
+
def build_material(spec, guard, assets=None, project_root=None):
|
|
663
736
|
"""Create one Blender material from a SceneSpec material entry."""
|
|
664
737
|
material_id = spec["id"]
|
|
665
738
|
shader = spec.get("shader", "principled")
|
|
666
739
|
parameters = spec.get("parameters") or {}
|
|
667
740
|
|
|
741
|
+
if shader == 'emission' and (spec.get('texture') or {}).get('coordinates') == 'uv':
|
|
742
|
+
raise ActionError('SCENE_VALIDATION_FAILED', 'UV procedural texture requires a principled or glass material',
|
|
743
|
+
{'materialId': material_id})
|
|
744
|
+
|
|
668
745
|
material = bpy.data.materials.new(name="%s%s" % (MATERIAL_PREFIX, material_id))
|
|
669
746
|
# A material has `use_nodes` enabled by default in 5.x and arrives with a
|
|
670
747
|
# Principled BSDF already wired to the output. Setting it again only emits a
|
|
@@ -698,15 +775,18 @@ def build_material(spec, guard):
|
|
|
698
775
|
|
|
699
776
|
for key, value in parameters.items():
|
|
700
777
|
_set_socket(principled, key, value, guard, material_id)
|
|
778
|
+
build_anisotropy(material, principled, spec)
|
|
701
779
|
|
|
702
780
|
# After the scalars, so the pattern varies around the authored values
|
|
703
781
|
# rather than replacing them.
|
|
704
782
|
texture = spec.get("texture")
|
|
705
783
|
if isinstance(texture, dict):
|
|
706
784
|
_build_texture_graph(material, principled, texture, guard, material_id)
|
|
785
|
+
if spec.get('images'):
|
|
786
|
+
build_image_maps(material, principled, spec['images'], assets or {}, project_root)
|
|
707
787
|
|
|
708
788
|
alpha = parameters.get("alpha")
|
|
709
|
-
if isinstance(alpha, (int, float)) and float(alpha) < 1.0:
|
|
789
|
+
if (isinstance(alpha, (int, float)) and float(alpha) < 1.0) or 'alpha' in (spec.get('images') or {}):
|
|
710
790
|
# Non-opaque alpha needs an explicit blend mode, or the viewport and the
|
|
711
791
|
# render disagree about whether the surface is see-through.
|
|
712
792
|
try:
|
|
@@ -1869,24 +1949,48 @@ def build_scene(spec, options, guard):
|
|
|
1869
1949
|
Returns a report describing what was built. Raises ``ActionError`` on any
|
|
1870
1950
|
failure that must not be silently tolerated.
|
|
1871
1951
|
"""
|
|
1872
|
-
report_progress("reset_scene", 5)
|
|
1873
|
-
scene = reset_scene()
|
|
1874
|
-
|
|
1875
|
-
report_progress("build_world", 10)
|
|
1876
|
-
world_report = build_world(scene, spec)
|
|
1877
|
-
|
|
1878
|
-
report_progress("configure_scene", 12)
|
|
1879
1952
|
profile_name = options.get("profile") or "preview"
|
|
1880
|
-
|
|
1881
|
-
profile = profiles.get(profile_name)
|
|
1882
|
-
if profile is None:
|
|
1883
|
-
profile = profiles.get("preview")
|
|
1953
|
+
profile = (spec.get("renderProfiles") or {}).get(profile_name)
|
|
1884
1954
|
if profile is None:
|
|
1885
1955
|
raise ActionError(
|
|
1886
1956
|
"RENDER_PROFILE_MISSING",
|
|
1887
|
-
'the SceneSpec defines no render profile named "%s"
|
|
1957
|
+
'the SceneSpec defines no render profile named "%s"' % (profile_name,),
|
|
1888
1958
|
)
|
|
1889
1959
|
|
|
1960
|
+
# Check pinned source bytes again at the actual compile boundary. This also
|
|
1961
|
+
# protects lazy checkpoint rebuilds, which do not pass through asset.add.
|
|
1962
|
+
for asset in spec.get("assets") or []:
|
|
1963
|
+
verify_asset_bundle(options.get("project_root") or "", asset)
|
|
1964
|
+
addressed = re.fullmatch(r"assets/raw/([a-f0-9]{64})\.[a-z0-9]+", asset.get("path", ""))
|
|
1965
|
+
declared = addressed.group(1) if addressed else asset.get("sha256")
|
|
1966
|
+
if not declared:
|
|
1967
|
+
verify_unbundled_gltf_asset(options.get("project_root") or "", asset)
|
|
1968
|
+
verify_unbundled_obj_asset(options.get("project_root") or "", asset)
|
|
1969
|
+
continue
|
|
1970
|
+
asset_path = os.path.join(options.get("project_root") or "", asset.get("path", ""))
|
|
1971
|
+
digest = hashlib.sha256()
|
|
1972
|
+
try:
|
|
1973
|
+
with open(asset_path, "rb") as source:
|
|
1974
|
+
for chunk in iter(lambda: source.read(64 * 1024), b""):
|
|
1975
|
+
digest.update(chunk)
|
|
1976
|
+
except OSError as error:
|
|
1977
|
+
raise ActionError("ASSET_MISSING", 'cannot read pinned asset "%s": %s' % (asset.get("id"), error))
|
|
1978
|
+
actual = digest.hexdigest()
|
|
1979
|
+
if actual != declared or (asset.get("sha256") and asset["sha256"] != declared):
|
|
1980
|
+
raise ActionError("ASSET_HASH_MISMATCH", 'asset "%s" changed since its revision was declared' % asset.get("id"),
|
|
1981
|
+
{"assetId": asset.get("id"), "declared": declared, "actual": actual})
|
|
1982
|
+
|
|
1983
|
+
verify_unbundled_gltf_asset(options.get("project_root") or "", asset)
|
|
1984
|
+
verify_unbundled_obj_asset(options.get("project_root") or "", asset)
|
|
1985
|
+
|
|
1986
|
+
check_scene_image_budget(spec, options.get('project_root'))
|
|
1987
|
+
report_progress("reset_scene", 5)
|
|
1988
|
+
scene = reset_scene()
|
|
1989
|
+
|
|
1990
|
+
report_progress("build_world", 10)
|
|
1991
|
+
world_report = build_world(scene, spec, options)
|
|
1992
|
+
|
|
1993
|
+
report_progress("configure_scene", 12)
|
|
1890
1994
|
requested_engine = profile.get("engine", "cycles")
|
|
1891
1995
|
engine, downgrade = resolve_engine(requested_engine, guard)
|
|
1892
1996
|
try:
|
|
@@ -1902,8 +2006,10 @@ def build_scene(spec, options, guard):
|
|
|
1902
2006
|
# Materials first: entities reference them by id during construction.
|
|
1903
2007
|
report_progress("build_materials", 20)
|
|
1904
2008
|
materials = {}
|
|
2009
|
+
assets = {asset["id"]: asset for asset in spec.get("assets") or []}
|
|
1905
2010
|
for entry in spec.get("materials") or []:
|
|
1906
|
-
|
|
2011
|
+
validate_anisotropy_material(entry, spec.get('animationTracks') or [])
|
|
2012
|
+
materials[entry["id"]] = build_material(entry, guard, assets, options.get('project_root'))
|
|
1907
2013
|
default_material = build_default_material(guard)
|
|
1908
2014
|
|
|
1909
2015
|
assets = {asset["id"]: asset for asset in spec.get("assets") or []}
|
|
@@ -1917,6 +2023,9 @@ def build_scene(spec, options, guard):
|
|
|
1917
2023
|
|
|
1918
2024
|
report_progress("build_entities", 35)
|
|
1919
2025
|
entity_objects = {}
|
|
2026
|
+
entity_meshes = {}
|
|
2027
|
+
native_material_surfaces = []
|
|
2028
|
+
simulation_objects = {}
|
|
1920
2029
|
entity_positions = {}
|
|
1921
2030
|
for entity in spec.get("entities") or []:
|
|
1922
2031
|
entity_id = entity["id"]
|
|
@@ -1955,14 +2064,18 @@ def build_scene(spec, options, guard):
|
|
|
1955
2064
|
apply_transform(root, entity.get("transform") or {})
|
|
1956
2065
|
apply_visibility(root, visible)
|
|
1957
2066
|
for obj in objects:
|
|
1958
|
-
|
|
2067
|
+
# The instance wrapper follows the entity flag. A visible asset keeps
|
|
2068
|
+
# each source object's hidden helpers; explicit false hides all of it.
|
|
2069
|
+
if entity.get('type') != 'asset-instance' or not visible:
|
|
2070
|
+
apply_visibility(obj, visible)
|
|
1959
2071
|
|
|
1960
2072
|
material_id = entity.get("materialId")
|
|
1961
|
-
|
|
2073
|
+
preserve_materials = entity.get("type") == "asset-instance" and material_id is None
|
|
2074
|
+
if material_id is None and entity.get("type") not in ("empty", "asset-instance"):
|
|
1962
2075
|
material_id = "default"
|
|
1963
2076
|
materials.setdefault("default", default_material)
|
|
1964
2077
|
material = materials.get(material_id)
|
|
1965
|
-
if material is None:
|
|
2078
|
+
if material is None and not preserve_materials and entity.get("type") != "empty":
|
|
1966
2079
|
raise ActionError(
|
|
1967
2080
|
"SCENE_VALIDATION_FAILED",
|
|
1968
2081
|
'entity "%s" references material "%s", which was not built' % (entity_id, material_id),
|
|
@@ -1970,17 +2083,72 @@ def build_scene(spec, options, guard):
|
|
|
1970
2083
|
for obj in objects:
|
|
1971
2084
|
if not hasattr(obj.data, "materials"):
|
|
1972
2085
|
continue
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2086
|
+
if not preserve_materials and not entity.get("materialBindings"):
|
|
2087
|
+
obj.data.materials.clear()
|
|
2088
|
+
obj.data.materials.append(material)
|
|
2089
|
+
# Whole-asset override has one slot; imported face indices may
|
|
2090
|
+
# still refer to several slots from the original asset.
|
|
2091
|
+
for polygon in getattr(obj.data, "polygons", []):
|
|
2092
|
+
polygon.material_index = 0
|
|
2093
|
+
generator = entity.get("generator", {})
|
|
2094
|
+
if generator.get("bevel") or generator.get("shape") in ("uv_sphere", "cylinder", "cone", "torus", "lathe", "curve", "handled_cup"):
|
|
1976
2095
|
_shade_smooth(obj)
|
|
1977
2096
|
|
|
2097
|
+
if entity.get("materialBindings"):
|
|
2098
|
+
apply_material_bindings(entity, objects, materials)
|
|
2099
|
+
|
|
1978
2100
|
for obj in objects:
|
|
1979
2101
|
obj["deepblend_id"] = entity_id
|
|
1980
2102
|
obj["deepblend_kind"] = "entity"
|
|
1981
2103
|
entity_objects[entity_id] = root
|
|
2104
|
+
meshes = [obj for obj in objects if obj.type == "MESH"]
|
|
2105
|
+
entity_meshes[entity_id] = meshes
|
|
2106
|
+
native_material_surfaces.extend(obj for obj in objects if obj.type in ('CURVE', 'SURFACE', 'FONT', 'META'))
|
|
2107
|
+
if any(entity.get(field) is not None for field in ("rigidBody", "cloth", "softBody", "fluid")):
|
|
2108
|
+
if len(meshes) != 1:
|
|
2109
|
+
raise ActionError(
|
|
2110
|
+
"SCENE_SPEC_INVALID",
|
|
2111
|
+
'entity "%s" needs exactly one mesh for entity-level physics; found %d. '
|
|
2112
|
+
"Declare separately simulated parts as separate entities." % (entity_id, len(meshes)),
|
|
2113
|
+
)
|
|
2114
|
+
simulation_objects[entity_id] = meshes[0]
|
|
1982
2115
|
entity_positions[entity_id] = [float(root.location[0]), float(root.location[1]), float(root.location[2])]
|
|
1983
2116
|
|
|
2117
|
+
apply_model_modifiers(spec.get("entities") or [], entity_meshes)
|
|
2118
|
+
scene['deepblend_requires_cycles'] = False
|
|
2119
|
+
for entry in spec.get('materials') or []:
|
|
2120
|
+
for surface in native_material_surfaces:
|
|
2121
|
+
if validate_native_material_usage(entry, materials[entry['id']], surface, scene.render.engine,
|
|
2122
|
+
spec.get('animationTracks') or [], bpy.context.evaluated_depsgraph_get()):
|
|
2123
|
+
scene['deepblend_requires_cycles'] = True
|
|
2124
|
+
for entity_id, meshes in entity_meshes.items():
|
|
2125
|
+
for mesh in meshes:
|
|
2126
|
+
if (entry.get('texture') or {}).get('coordinates') == 'uv':
|
|
2127
|
+
if validate_native_material_usage(entry, materials[entry['id']], mesh, scene.render.engine,
|
|
2128
|
+
spec.get('animationTracks') or [], bpy.context.evaluated_depsgraph_get()):
|
|
2129
|
+
scene['deepblend_requires_cycles'] = True
|
|
2130
|
+
continue
|
|
2131
|
+
used_slots = {polygon.material_index for polygon in mesh.data.polygons}
|
|
2132
|
+
if not any(slot.material == materials[entry['id']] and index in used_slots
|
|
2133
|
+
for index, slot in enumerate(mesh.material_slots)):
|
|
2134
|
+
continue
|
|
2135
|
+
if validate_anisotropy_usage(entry, mesh, scene.render.engine, spec.get('animationTracks') or []):
|
|
2136
|
+
scene['deepblend_requires_cycles'] = True
|
|
2137
|
+
validate_image_uv_usage(entry, mesh, mesh.data.uv_layers)
|
|
2138
|
+
for entity in spec.get("entities") or []:
|
|
2139
|
+
generator = entity.get("generator") or {}
|
|
2140
|
+
if entity.get("modifiers") and entity.get("type") == "generator" and (
|
|
2141
|
+
generator.get("bevel") or any(entry["type"] == "bevel" for entry in entity["modifiers"])
|
|
2142
|
+
or generator.get("shape") in ("uv_sphere", "cylinder", "cone", "torus", "lathe", "curve", "handled_cup")
|
|
2143
|
+
):
|
|
2144
|
+
for mesh in entity_meshes[entity["id"]]:
|
|
2145
|
+
hidden = mesh.hide_viewport
|
|
2146
|
+
mesh.hide_viewport = False
|
|
2147
|
+
try:
|
|
2148
|
+
_shade_smooth(mesh)
|
|
2149
|
+
finally:
|
|
2150
|
+
mesh.hide_viewport = hidden
|
|
2151
|
+
|
|
1984
2152
|
report_progress("build_lights", 55)
|
|
1985
2153
|
for light in spec.get("lights") or []:
|
|
1986
2154
|
obj = build_light(light)
|
|
@@ -2079,7 +2247,7 @@ def build_scene(spec, options, guard):
|
|
|
2079
2247
|
"SCENE_SPEC_INVALID",
|
|
2080
2248
|
"the scene declares fluid objects but no domain, so the liquid would have nowhere to be",
|
|
2081
2249
|
)
|
|
2082
|
-
simulation_reading = build_rigid_bodies(spec,
|
|
2250
|
+
simulation_reading = build_rigid_bodies(spec, simulation_objects, guard, cloth_entities, soft_entities, fluid_entities)
|
|
2083
2251
|
|
|
2084
2252
|
report_progress("configure_render", 85)
|
|
2085
2253
|
render_config = configure_scene(scene, spec, profile, guard)
|
|
@@ -2112,7 +2280,18 @@ def build_scene(spec, options, guard):
|
|
|
2112
2280
|
'entity "%s" is skinned to armature "%s", which the same SceneSpec does not declare'
|
|
2113
2281
|
% (entity["id"], armature_id),
|
|
2114
2282
|
)
|
|
2115
|
-
|
|
2283
|
+
meshes = entity_meshes[entity["id"]]
|
|
2284
|
+
if not meshes:
|
|
2285
|
+
raise ActionError("SCENE_SPEC_INVALID", 'entity "%s" has no mesh to skin' % entity["id"])
|
|
2286
|
+
for mesh in meshes:
|
|
2287
|
+
previous_parent = mesh.parent
|
|
2288
|
+
previous_world = mesh.matrix_world.copy()
|
|
2289
|
+
skin_entity(mesh, armature_obj, armature_id, guard)
|
|
2290
|
+
if entity.get("type") == "asset-instance":
|
|
2291
|
+
# Automatic weights also reparents the mesh. Keep the authored
|
|
2292
|
+
# assembly and instance transform; the armature modifier deforms it.
|
|
2293
|
+
mesh.parent = previous_parent
|
|
2294
|
+
mesh.matrix_world = previous_world
|
|
2116
2295
|
skinned_entities[entity["id"]] = entity
|
|
2117
2296
|
|
|
2118
2297
|
return {
|
|
@@ -2132,7 +2311,7 @@ def build_scene(spec, options, guard):
|
|
|
2132
2311
|
# THE GROUPS COME FROM THE OBJECT, not from the spec entry: what the file asked for is
|
|
2133
2312
|
# the relationship, and what Blender produced is the weights. Reading the latter back is
|
|
2134
2313
|
# the whole point — an entity bound with no groups renders as a static object.
|
|
2135
|
-
"vertexGroups": sorted(group.name for
|
|
2314
|
+
"vertexGroups": sorted({group.name for mesh in entity_meshes[key] for group in mesh.vertex_groups}),
|
|
2136
2315
|
}
|
|
2137
2316
|
for key, entry in skinned_entities.items()
|
|
2138
2317
|
},
|
|
@@ -2140,10 +2319,58 @@ def build_scene(spec, options, guard):
|
|
|
2140
2319
|
}
|
|
2141
2320
|
|
|
2142
2321
|
|
|
2322
|
+
def _uv_map_facts(mesh, source):
|
|
2323
|
+
"""Names and finite coordinates are inventory, not an overlap/unwrap verdict."""
|
|
2324
|
+
return [{'name': layer.name, 'activeRender': bool(layer.active_render),
|
|
2325
|
+
'loopCount': len(layer.data),
|
|
2326
|
+
'finite': all(math.isfinite(value) for loop in layer.data for value in loop.uv),
|
|
2327
|
+
'source': source}
|
|
2328
|
+
for layer in mesh.uv_layers]
|
|
2329
|
+
|
|
2330
|
+
|
|
2331
|
+
def _render_collection_members(layer, inherited_excluded=False, members=None):
|
|
2332
|
+
"""An object can have several collection paths; one render-enabled path suffices."""
|
|
2333
|
+
members = set() if members is None else members
|
|
2334
|
+
excluded = inherited_excluded or layer.exclude or layer.collection.hide_render
|
|
2335
|
+
if not excluded:
|
|
2336
|
+
members.update(layer.collection.objects)
|
|
2337
|
+
for child in layer.children:
|
|
2338
|
+
_render_collection_members(child, excluded, members)
|
|
2339
|
+
return members
|
|
2340
|
+
|
|
2341
|
+
|
|
2342
|
+
def _object_world_bounds(obj, depsgraph):
|
|
2343
|
+
"""Conservative world AABB of an evaluated mesh; wrappers have no geometry.
|
|
2344
|
+
|
|
2345
|
+
Bounds use the evaluated local bounding box transformed by matrix_world.
|
|
2346
|
+
They include the object hierarchy and modifiers, but are not a tight vertex
|
|
2347
|
+
hull or a claim that every enclosed pixel is visible to the camera.
|
|
2348
|
+
"""
|
|
2349
|
+
if obj.type != 'MESH':
|
|
2350
|
+
return None
|
|
2351
|
+
evaluated = obj.evaluated_get(depsgraph)
|
|
2352
|
+
if not evaluated.data.vertices:
|
|
2353
|
+
return None
|
|
2354
|
+
local = [tuple(corner) for corner in evaluated.bound_box]
|
|
2355
|
+
if not local or all(corner == (-1.0, -1.0, -1.0) for corner in local):
|
|
2356
|
+
return None
|
|
2357
|
+
corners = [evaluated.matrix_world @ Vector(corner) for corner in local]
|
|
2358
|
+
if not all(math.isfinite(value) for corner in corners for value in corner):
|
|
2359
|
+
return None
|
|
2360
|
+
return {'min': [min(float(corner[axis]) for corner in corners) for axis in range(3)],
|
|
2361
|
+
'max': [max(float(corner[axis]) for corner in corners) for axis in range(3)]}
|
|
2362
|
+
|
|
2363
|
+
|
|
2143
2364
|
def describe_objects():
|
|
2144
2365
|
"""Inventory of every object in the scene, for validation and golden checks."""
|
|
2145
2366
|
entries = []
|
|
2367
|
+
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
2368
|
+
frame = int(bpy.context.scene.frame_current)
|
|
2369
|
+
render_members = _render_collection_members(bpy.context.view_layer.layer_collection)
|
|
2146
2370
|
for obj in bpy.data.objects:
|
|
2371
|
+
# Disabled viewport objects can retain stale evaluated transforms. Do
|
|
2372
|
+
# not present that cache as a measured world bound or evaluated UV set.
|
|
2373
|
+
evaluation_available = obj.type == 'MESH' and obj.visible_get(view_layer=bpy.context.view_layer)
|
|
2147
2374
|
entry = {
|
|
2148
2375
|
"name": obj.name,
|
|
2149
2376
|
"type": obj.type,
|
|
@@ -2153,12 +2380,43 @@ def describe_objects():
|
|
|
2153
2380
|
"rotationEuler": [round(float(value), 6) for value in obj.rotation_euler],
|
|
2154
2381
|
"scale": [round(float(value), 6) for value in obj.scale],
|
|
2155
2382
|
"visible": not (obj.hide_render or obj.hide_viewport),
|
|
2383
|
+
# Render eligibility only: viewport hiding is a different flag, and
|
|
2384
|
+
# this does not test occlusion, clipping, alpha or material output.
|
|
2385
|
+
"renderVisible": obj in render_members and not obj.hide_render and obj.visible_camera,
|
|
2386
|
+
"worldBounds": _object_world_bounds(obj, depsgraph) if evaluation_available else None,
|
|
2387
|
+
"boundsFrame": frame,
|
|
2156
2388
|
"parent": obj.parent.name if obj.parent is not None else None,
|
|
2157
2389
|
}
|
|
2158
2390
|
if obj.type == "MESH":
|
|
2159
2391
|
entry["vertexCount"] = len(obj.data.vertices)
|
|
2160
2392
|
entry["polygonCount"] = len(obj.data.polygons)
|
|
2161
|
-
entry["materialNames"] = [material.name for
|
|
2393
|
+
entry["materialNames"] = [slot.material.name for slot in obj.material_slots if slot.material is not None]
|
|
2394
|
+
entry['uvMaps'] = _uv_map_facts(obj.data, 'mesh-data')
|
|
2395
|
+
if evaluation_available:
|
|
2396
|
+
evaluated = obj.evaluated_get(depsgraph)
|
|
2397
|
+
try:
|
|
2398
|
+
surface = evaluated.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
|
2399
|
+
entry['evaluatedUvMaps'] = _uv_map_facts(surface, 'evaluated-mesh') if surface else []
|
|
2400
|
+
finally:
|
|
2401
|
+
evaluated.to_mesh_clear()
|
|
2402
|
+
if entry['worldBounds'] is None:
|
|
2403
|
+
entry['boundsUnavailable'] = 'empty-or-invalid-bounds'
|
|
2404
|
+
else:
|
|
2405
|
+
entry['boundsUnavailable'] = 'viewport-disabled'
|
|
2406
|
+
entry['evaluatedUvMaps'] = None
|
|
2407
|
+
entry['evaluatedUvMapsUnavailable'] = 'viewport-disabled'
|
|
2408
|
+
if obj.get('deepblend_part_id'):
|
|
2409
|
+
entry['partId'] = obj['deepblend_part_id']
|
|
2410
|
+
entry['parentPartId'] = obj.get('deepblend_parent_part_id') or None
|
|
2411
|
+
entry['sourceMaterialSlots'] = json.loads(obj.get('deepblend_source_material_slots', '[]'))
|
|
2412
|
+
used_slots = {}
|
|
2413
|
+
for polygon in obj.data.polygons:
|
|
2414
|
+
used_slots[polygon.material_index] = used_slots.get(polygon.material_index, 0) + 1
|
|
2415
|
+
entry['materialSlots'] = [
|
|
2416
|
+
{'index': index, 'materialName': slot.material.name if slot.material else None,
|
|
2417
|
+
'materialId': slot.material.get('deepblend_id') if slot.material else None,
|
|
2418
|
+
'usedPolygonCount': used_slots.get(index, 0)}
|
|
2419
|
+
for index, slot in enumerate(obj.material_slots)]
|
|
2162
2420
|
if obj.type == "LIGHT":
|
|
2163
2421
|
entry["lightType"] = obj.data.type
|
|
2164
2422
|
entry["energy"] = round(float(obj.data.energy), 6)
|