mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
blender plugins update
This commit is contained in:
parent
cd79f0654d
commit
f0918ec760
6 changed files with 129 additions and 58 deletions
|
|
@ -38,7 +38,7 @@ class BlendModelLoader(pype.blender.AssetLoader):
|
||||||
Note:
|
Note:
|
||||||
It is assumed that only 1 matching collection is found.
|
It is assumed that only 1 matching collection is found.
|
||||||
"""
|
"""
|
||||||
for collection in bpy.data.collections:
|
for collection in bpy.context.blend_data.collections:
|
||||||
if collection.name != name:
|
if collection.name != name:
|
||||||
continue
|
continue
|
||||||
if collection.library is None:
|
if collection.library is None:
|
||||||
|
|
@ -52,18 +52,19 @@ class BlendModelLoader(pype.blender.AssetLoader):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _collection_contains_object(collection: bpy.types.Collection, object: bpy.types.Object) -> bool:
|
def _collection_contains_object(
|
||||||
|
collection: bpy.types.Collection, object: bpy.types.Object
|
||||||
|
) -> bool:
|
||||||
"""Check if the collection contains the object."""
|
"""Check if the collection contains the object."""
|
||||||
for obj in collection.objects:
|
for obj in collection.objects:
|
||||||
if obj == object:
|
if obj == object:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def process_asset(self,
|
def process_asset(
|
||||||
context: dict,
|
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||||
name: str,
|
options: Optional[Dict] = None
|
||||||
namespace: Optional[str] = None,
|
) -> Optional[List]:
|
||||||
options: Optional[Dict] = None) -> Optional[List]:
|
|
||||||
"""
|
"""
|
||||||
Arguments:
|
Arguments:
|
||||||
name: Use pre-defined name
|
name: Use pre-defined name
|
||||||
|
|
@ -76,21 +77,27 @@ class BlendModelLoader(pype.blender.AssetLoader):
|
||||||
asset = context["asset"]["name"]
|
asset = context["asset"]["name"]
|
||||||
subset = context["subset"]["name"]
|
subset = context["subset"]["name"]
|
||||||
lib_container = pype.blender.plugin.model_name(asset, subset)
|
lib_container = pype.blender.plugin.model_name(asset, subset)
|
||||||
container_name = pype.blender.plugin.model_name(asset, subset, namespace)
|
container_name = pype.blender.plugin.model_name(
|
||||||
|
asset, subset, namespace
|
||||||
|
)
|
||||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||||
|
|
||||||
with bpy.data.libraries.load(libpath, link=True, relative=relative) as (_, data_to):
|
with bpy.context.blend_data.libraries.load(
|
||||||
|
libpath, link=True, relative=relative
|
||||||
|
) as (_, data_to):
|
||||||
data_to.collections = [lib_container]
|
data_to.collections = [lib_container]
|
||||||
|
|
||||||
scene = bpy.context.scene
|
scene = bpy.context.scene
|
||||||
instance_empty = bpy.data.objects.new(container_name, None)
|
instance_empty = bpy.context.blend_data.objects.new(
|
||||||
|
container_name, None
|
||||||
|
)
|
||||||
if not instance_empty.get("avalon"):
|
if not instance_empty.get("avalon"):
|
||||||
instance_empty["avalon"] = dict()
|
instance_empty["avalon"] = dict()
|
||||||
avalon_info = instance_empty["avalon"]
|
avalon_info = instance_empty["avalon"]
|
||||||
avalon_info.update({"container_name": container_name})
|
avalon_info.update({"container_name": container_name})
|
||||||
scene.collection.objects.link(instance_empty)
|
scene.collection.objects.link(instance_empty)
|
||||||
instance_empty.instance_type = 'COLLECTION'
|
instance_empty.instance_type = 'COLLECTION'
|
||||||
container = bpy.data.collections[lib_container]
|
container = bpy.context.blend_data.collections[lib_container]
|
||||||
container.name = container_name
|
container.name = container_name
|
||||||
instance_empty.instance_collection = container
|
instance_empty.instance_collection = container
|
||||||
container.make_local()
|
container.make_local()
|
||||||
|
|
@ -120,7 +127,9 @@ class BlendModelLoader(pype.blender.AssetLoader):
|
||||||
Warning:
|
Warning:
|
||||||
No nested collections are supported at the moment!
|
No nested collections are supported at the moment!
|
||||||
"""
|
"""
|
||||||
collection = bpy.data.collections.get(container["objectName"])
|
collection = bpy.context.blend_data.collections.get(
|
||||||
|
container["objectName"]
|
||||||
|
)
|
||||||
libpath = Path(api.get_representation_path(representation))
|
libpath = Path(api.get_representation_path(representation))
|
||||||
extension = libpath.suffix.lower()
|
extension = libpath.suffix.lower()
|
||||||
|
|
||||||
|
|
@ -130,14 +139,30 @@ class BlendModelLoader(pype.blender.AssetLoader):
|
||||||
pformat(representation, indent=2),
|
pformat(representation, indent=2),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert collection, f"The asset is not loaded: {container['objectName']}"
|
assert collection, (
|
||||||
assert not (collection.children), "Nested collections are not supported."
|
f"The asset is not loaded: {container['objectName']}"
|
||||||
assert libpath, ("No existing library file found for {container['objectName']}")
|
)
|
||||||
assert libpath.is_file(), f"The file doesn't exist: {libpath}"
|
assert not (collection.children), (
|
||||||
assert extension in pype.blender.plugin.VALID_EXTENSIONS, f"Unsupported file: {libpath}"
|
"Nested collections are not supported."
|
||||||
collection_libpath = self._get_library_from_container(collection).filepath
|
)
|
||||||
normalized_collection_libpath = str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
assert libpath, (
|
||||||
normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve())
|
"No existing library file found for {container['objectName']}"
|
||||||
|
)
|
||||||
|
assert libpath.is_file(), (
|
||||||
|
f"The file doesn't exist: {libpath}"
|
||||||
|
)
|
||||||
|
assert extension in pype.blender.plugin.VALID_EXTENSIONS, (
|
||||||
|
f"Unsupported file: {libpath}"
|
||||||
|
)
|
||||||
|
collection_libpath = (
|
||||||
|
self._get_library_from_container(collection).filepath
|
||||||
|
)
|
||||||
|
normalized_collection_libpath = (
|
||||||
|
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||||
|
)
|
||||||
|
normalized_libpath = (
|
||||||
|
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||||
|
)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||||
normalized_collection_libpath,
|
normalized_collection_libpath,
|
||||||
|
|
@ -155,29 +180,46 @@ class BlendModelLoader(pype.blender.AssetLoader):
|
||||||
# Unlink every object
|
# Unlink every object
|
||||||
collection.objects.unlink(obj)
|
collection.objects.unlink(obj)
|
||||||
remove_obj = True
|
remove_obj = True
|
||||||
for coll in [coll for coll in bpy.data.collections if coll != collection]:
|
for coll in [
|
||||||
if coll.objects and self._collection_contains_object(coll, obj):
|
coll for coll in bpy.context.blend_data.collections
|
||||||
|
if coll != collection
|
||||||
|
]:
|
||||||
|
if (
|
||||||
|
coll.objects and
|
||||||
|
self._collection_contains_object(coll, obj)
|
||||||
|
):
|
||||||
remove_obj = False
|
remove_obj = False
|
||||||
if remove_obj:
|
if remove_obj:
|
||||||
objects_to_remove.add(obj)
|
objects_to_remove.add(obj)
|
||||||
|
|
||||||
for obj in objects_to_remove:
|
for obj in objects_to_remove:
|
||||||
# Only delete objects that are not used elsewhere
|
# Only delete objects that are not used elsewhere
|
||||||
bpy.data.objects.remove(obj)
|
bpy.context.blend_data.objects.remove(obj)
|
||||||
|
|
||||||
instance_empties = [obj for obj in collection.users_dupli_group if obj.name in collection.name]
|
instance_empties = [
|
||||||
|
obj for obj in collection.users_dupli_group
|
||||||
|
if obj.name in collection.name
|
||||||
|
]
|
||||||
if instance_empties:
|
if instance_empties:
|
||||||
instance_empty = instance_empties[0]
|
instance_empty = instance_empties[0]
|
||||||
container_name = instance_empty["avalon"]["container_name"]
|
container_name = instance_empty["avalon"]["container_name"]
|
||||||
|
|
||||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||||
with bpy.data.libraries.load(str(libpath), link=True, relative=relative) as (_, data_to):
|
with bpy.context.blend_data.libraries.load(
|
||||||
|
str(libpath), link=True, relative=relative
|
||||||
|
) as (_, data_to):
|
||||||
data_to.collections = [container_name]
|
data_to.collections = [container_name]
|
||||||
|
|
||||||
new_collection = self._get_lib_collection(container_name, libpath)
|
new_collection = self._get_lib_collection(container_name, libpath)
|
||||||
if new_collection is None:
|
if new_collection is None:
|
||||||
raise ValueError("A matching collection '{container_name}' "
|
raise ValueError(
|
||||||
"should have been found in: {libpath}")
|
"A matching collection '{container_name}' "
|
||||||
|
"should have been found in: {libpath}"
|
||||||
|
)
|
||||||
|
|
||||||
for obj in new_collection.objects:
|
for obj in new_collection.objects:
|
||||||
collection.objects.link(obj)
|
collection.objects.link(obj)
|
||||||
bpy.data.collections.remove(new_collection)
|
bpy.context.blend_data.collections.remove(new_collection)
|
||||||
# Update the representation on the collection
|
# Update the representation on the collection
|
||||||
avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY]
|
avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY]
|
||||||
avalon_prop["representation"] = str(representation["_id"])
|
avalon_prop["representation"] = str(representation["_id"])
|
||||||
|
|
@ -195,10 +237,14 @@ class BlendModelLoader(pype.blender.AssetLoader):
|
||||||
Warning:
|
Warning:
|
||||||
No nested collections are supported at the moment!
|
No nested collections are supported at the moment!
|
||||||
"""
|
"""
|
||||||
collection = bpy.data.collections.get(container["objectName"])
|
collection = bpy.context.blend_data.collections.get(
|
||||||
|
container["objectName"]
|
||||||
|
)
|
||||||
if not collection:
|
if not collection:
|
||||||
return False
|
return False
|
||||||
assert not (collection.children), "Nested collections are not supported."
|
assert not (collection.children), (
|
||||||
|
"Nested collections are not supported."
|
||||||
|
)
|
||||||
instance_parents = list(collection.users_dupli_group)
|
instance_parents = list(collection.users_dupli_group)
|
||||||
instance_objects = list(collection.objects)
|
instance_objects = list(collection.objects)
|
||||||
for obj in instance_objects + instance_parents:
|
for obj in instance_objects + instance_parents:
|
||||||
|
|
@ -224,11 +270,10 @@ class CacheModelLoader(pype.blender.AssetLoader):
|
||||||
icon = "code-fork"
|
icon = "code-fork"
|
||||||
color = "orange"
|
color = "orange"
|
||||||
|
|
||||||
def process_asset(self,
|
def process_asset(
|
||||||
context: dict,
|
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||||
name: str,
|
options: Optional[Dict] = None
|
||||||
namespace: Optional[str] = None,
|
) -> Optional[List]:
|
||||||
options: Optional[Dict] = None) -> Optional[List]:
|
|
||||||
"""
|
"""
|
||||||
Arguments:
|
Arguments:
|
||||||
name: Use pre-defined name
|
name: Use pre-defined name
|
||||||
|
|
@ -243,17 +288,23 @@ class CacheModelLoader(pype.blender.AssetLoader):
|
||||||
asset = context["asset"]["name"]
|
asset = context["asset"]["name"]
|
||||||
subset = context["subset"]["name"]
|
subset = context["subset"]["name"]
|
||||||
# TODO (jasper): evaluate use of namespace which is 'alien' to Blender.
|
# TODO (jasper): evaluate use of namespace which is 'alien' to Blender.
|
||||||
lib_container = container_name = pype.blender.plugin.model_name(asset, subset, namespace)
|
lib_container = container_name = (
|
||||||
|
pype.blender.plugin.model_name(asset, subset, namespace)
|
||||||
|
)
|
||||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||||
|
|
||||||
with bpy.data.libraries.load(libpath, link=True, relative=relative) as (data_from, data_to):
|
with bpy.context.blend_data.libraries.load(
|
||||||
|
libpath, link=True, relative=relative
|
||||||
|
) as (data_from, data_to):
|
||||||
data_to.collections = [lib_container]
|
data_to.collections = [lib_container]
|
||||||
|
|
||||||
scene = bpy.context.scene
|
scene = bpy.context.scene
|
||||||
instance_empty = bpy.data.objects.new(container_name, None)
|
instance_empty = bpy.context.blend_data.objects.new(
|
||||||
|
container_name, None
|
||||||
|
)
|
||||||
scene.collection.objects.link(instance_empty)
|
scene.collection.objects.link(instance_empty)
|
||||||
instance_empty.instance_type = 'COLLECTION'
|
instance_empty.instance_type = 'COLLECTION'
|
||||||
collection = bpy.data.collections[lib_container]
|
collection = bpy.context.blend_data.collections[lib_container]
|
||||||
collection.name = container_name
|
collection.name = container_name
|
||||||
instance_empty.instance_collection = collection
|
instance_empty.instance_collection = collection
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,5 @@ class CollectBlenderCurrentFile(pyblish.api.ContextPlugin):
|
||||||
|
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
"""Inject the current working file"""
|
"""Inject the current working file"""
|
||||||
current_file = bpy.data.filepath
|
current_file = bpy.context.blend_data.filepath
|
||||||
context.data['currentFile'] = current_file
|
context.data['currentFile'] = current_file
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class CollectModel(pyblish.api.ContextPlugin):
|
||||||
representation set. If the representation is set, it is a loaded model
|
representation set. If the representation is set, it is a loaded model
|
||||||
and we don't want to publish it.
|
and we don't want to publish it.
|
||||||
"""
|
"""
|
||||||
for collection in bpy.data.collections:
|
for collection in bpy.context.blend_data.collections:
|
||||||
avalon_prop = collection.get(AVALON_PROPERTY) or dict()
|
avalon_prop = collection.get(AVALON_PROPERTY) or dict()
|
||||||
if (avalon_prop.get('family') == 'model'
|
if (avalon_prop.get('family') == 'model'
|
||||||
and not avalon_prop.get('representation')):
|
and not avalon_prop.get('representation')):
|
||||||
|
|
@ -42,6 +42,7 @@ class CollectModel(pyblish.api.ContextPlugin):
|
||||||
instance = context.create_instance(
|
instance = context.create_instance(
|
||||||
name=name,
|
name=name,
|
||||||
family=family,
|
family=family,
|
||||||
|
families=[family],
|
||||||
subset=subset,
|
subset=subset,
|
||||||
asset=asset,
|
asset=asset,
|
||||||
task=task,
|
task=task,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
from pathlib import Path
|
import os
|
||||||
import avalon.blender.workio
|
import avalon.blender.workio
|
||||||
|
|
||||||
import sonar.api
|
import pype.api
|
||||||
|
|
||||||
|
|
||||||
class ExtractModel(sonar.api.Extractor):
|
class ExtractModel(pype.api.Extractor):
|
||||||
"""Extract as model."""
|
"""Extract as model."""
|
||||||
|
|
||||||
label = "Model"
|
label = "Model"
|
||||||
|
|
@ -14,9 +14,10 @@ class ExtractModel(sonar.api.Extractor):
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
# Define extract output file path
|
# Define extract output file path
|
||||||
stagingdir = Path(self.staging_dir(instance))
|
|
||||||
|
stagingdir = self.staging_dir(instance)
|
||||||
filename = f"{instance.name}.blend"
|
filename = f"{instance.name}.blend"
|
||||||
filepath = str(stagingdir / filename)
|
filepath = os.path.join(stagingdir, filename)
|
||||||
|
|
||||||
# Perform extraction
|
# Perform extraction
|
||||||
self.log.info("Performing extraction..")
|
self.log.info("Performing extraction..")
|
||||||
|
|
@ -24,11 +25,23 @@ class ExtractModel(sonar.api.Extractor):
|
||||||
# Just save the file to a temporary location. At least for now it's no
|
# Just save the file to a temporary location. At least for now it's no
|
||||||
# problem to have (possibly) extra stuff in the file.
|
# problem to have (possibly) extra stuff in the file.
|
||||||
avalon.blender.workio.save_file(filepath, copy=True)
|
avalon.blender.workio.save_file(filepath, copy=True)
|
||||||
|
#
|
||||||
|
# # Store reference for integration
|
||||||
|
# if "files" not in instance.data:
|
||||||
|
# instance.data["files"] = list()
|
||||||
|
#
|
||||||
|
# # instance.data["files"].append(filename)
|
||||||
|
|
||||||
# Store reference for integration
|
if "representations" not in instance.data:
|
||||||
if "files" not in instance.data:
|
instance.data["representations"] = []
|
||||||
instance.data["files"] = list()
|
|
||||||
|
|
||||||
instance.data["files"].append(filename)
|
representation = {
|
||||||
|
'name': 'blend',
|
||||||
|
'ext': 'blend',
|
||||||
|
'files': filename,
|
||||||
|
"stagingDir": stagingdir,
|
||||||
|
}
|
||||||
|
instance.data["representations"].append(representation)
|
||||||
|
|
||||||
self.log.info("Extracted instance '%s' to: %s", instance.name, filepath)
|
|
||||||
|
self.log.info("Extracted instance '%s' to: %s", instance.name, representation)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from typing import List
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
import sonar.blender.action
|
import pype.blender.action
|
||||||
|
|
||||||
|
|
||||||
class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
|
class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
|
||||||
|
|
@ -14,7 +14,7 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
|
||||||
families = ["model"]
|
families = ["model"]
|
||||||
category = "geometry"
|
category = "geometry"
|
||||||
label = "Mesh Has UV's"
|
label = "Mesh Has UV's"
|
||||||
actions = [sonar.blender.action.SelectInvalidAction]
|
actions = [pype.blender.action.SelectInvalidAction]
|
||||||
optional = True
|
optional = True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -34,7 +34,9 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
|
||||||
def get_invalid(cls, instance) -> List:
|
def get_invalid(cls, instance) -> List:
|
||||||
invalid = []
|
invalid = []
|
||||||
# TODO (jasper): only check objects in the collection that will be published?
|
# TODO (jasper): only check objects in the collection that will be published?
|
||||||
for obj in [obj for obj in bpy.data.objects if obj.type == 'MESH']:
|
for obj in [
|
||||||
|
obj for obj in bpy.context.blend_data.objects if obj.type == 'MESH'
|
||||||
|
]:
|
||||||
# Make sure we are in object mode.
|
# Make sure we are in object mode.
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
if not cls.has_uvs(obj):
|
if not cls.has_uvs(obj):
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from typing import List
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
import sonar.blender.action
|
import pype.blender.action
|
||||||
|
|
||||||
|
|
||||||
class ValidateMeshNoNegativeScale(pyblish.api.Validator):
|
class ValidateMeshNoNegativeScale(pyblish.api.Validator):
|
||||||
|
|
@ -13,13 +13,15 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator):
|
||||||
hosts = ["blender"]
|
hosts = ["blender"]
|
||||||
families = ["model"]
|
families = ["model"]
|
||||||
label = "Mesh No Negative Scale"
|
label = "Mesh No Negative Scale"
|
||||||
actions = [sonar.blender.action.SelectInvalidAction]
|
actions = [pype.blender.action.SelectInvalidAction]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_invalid(instance) -> List:
|
def get_invalid(instance) -> List:
|
||||||
invalid = []
|
invalid = []
|
||||||
# TODO (jasper): only check objects in the collection that will be published?
|
# TODO (jasper): only check objects in the collection that will be published?
|
||||||
for obj in [obj for obj in bpy.data.objects if obj.type == 'MESH']:
|
for obj in [
|
||||||
|
obj for obj in bpy.context.blend_data.objects if obj.type == 'MESH'
|
||||||
|
]:
|
||||||
if any(v < 0 for v in obj.scale):
|
if any(v < 0 for v in obj.scale):
|
||||||
invalid.append(obj)
|
invalid.append(obj)
|
||||||
|
|
||||||
|
|
@ -28,4 +30,6 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator):
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
invalid = self.get_invalid(instance)
|
invalid = self.get_invalid(instance)
|
||||||
if invalid:
|
if invalid:
|
||||||
raise RuntimeError(f"Meshes found in instance with negative scale: {invalid}")
|
raise RuntimeError(
|
||||||
|
f"Meshes found in instance with negative scale: {invalid}"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue