Hope you don’t mind me posting the exercises/examples while learning Pyblish.
First simple example translated to Cinema’s syntax, working well:
"""
Plugin collects polygon objects in the active document and
prints their name.
Informs the user of how many polygon objects are collected.
"""
import pyblish.api
import c4d
class CollectPolyObjects(pyblish.api.ContextPlugin):
"""
Collects polygon objects in active document.
"""
order = pyblish.api.CollectorOrder
def get_objs(self, op, lst):
"""
Returns list with polygon objects.
"""
while op:
if op.GetTypeName() == 'Polygon':
lst.append(op)
self.get_objs(op.GetDown(), lst)
op = op.GetNext()
return lst
def process(self, context):
doc = c4d.documents.GetActiveDocument()
first_obj = doc.GetFirstObject()
if not first_obj:
raise Exception("No objects in the scene!")
objs = self.get_objs(first_obj, [])
if not objs:
raise Exception("No polygon objects in the scene!")
context.data["objs"] = objs
class PrintCollectedObjects(pyblish.api.ContextPlugin):
"""
Prints the collected objects.
"""
order = pyblish.api.CollectorOrder + 0.1
def process(self, context):
if "objs" not in context.data:
raise Exception("No polygon objects collected!")
objs = context.data["objs"]
for obj in objs:
print(obj.GetName())
self.log.info("Collected {} polygon object(s).".format(len(objs)))
# Register Plugins
pyblish.api.register_plugin(CollectPolyObjects)
pyblish.api.register_plugin(PrintCollectedObjects)
Another example…
This example interacts the Cinema 4D objects in a basic level. I’ve added a context, label, information to the user and created an instance to be validated.
"""
Collect all of the subdivision generator objects and checks if are enabled.
If False enables it and passes the validation.
"""
import pyblish.api as api
import c4d
class CollectSDSObjects(api.ContextPlugin):
"""
Collect all of the subdivision generator objects.
"""
order = api.CollectorOrder
label = "Collect SDS Objects"
def get_objs(self, op, name_type, lst):
"""
Returns list with selected objects.
"""
while op:
if op.GetTypeName() == name_type:
lst.append(op)
self.get_objs(op.GetDown(), name_type, lst)
op = op.GetNext()
return lst
def process(self, context):
doc = c4d.documents.GetActiveDocument()
first_obj = doc.GetFirstObject()
if not first_obj:
raise Exception("No objects in the scene!")
objs = self.get_objs(first_obj, 'Subdivision Surface', [])
if not objs:
raise Exception("No SDS objects in the scene!")
instance = context.create_instance("Subdivision Objects")
instance.data["objs"] = objs
instance.data["families"] = ["subdObjs"]
self.log.info("Collected {} SDS object(s).".format(len(objs)))
self.log.info([obj.GetName() for obj in objs])
class ValidateSDSObjects(api.InstancePlugin):
"""
Checks if the SDS objects is enabled. If False enables it.
"""
order = api.ValidatorOrder
families = ["subdObjs"]
label = "Enable SDS Objects"
def process(self, instance):
if "objs" not in instance.data:
raise Exception("No SDS objects in the scene!")
objs = instance.data["objs"]
count = 0
enabled = []
for obj in objs:
if not obj[c4d.ID_BASEOBJECT_GENERATOR_FLAG]:
obj[c4d.ID_BASEOBJECT_GENERATOR_FLAG] = 1
enabled.append(obj)
count += 1
if count > 0:
self.log.warning("Enabled {} SDS object(s).".format(count))
self.log.info([obj.GetName() for obj in enabled])
c4d.EventAdd()
api.register_plugin(CollectSDSObjects)
api.register_plugin(ValidateSDSObjects)
Question:
I understand that targets are probably what I’m looking for if I want to publish for specific groups. E.g. animation team, modelling team, etc
Checked the example that @marcus posted and even though I can see the targets present in the terminal, i can’t see any GUI changes. Perhaps I’m thinking incorrectly about this?
The perhaps simplest way to separate between plug-ins run for various teams is to let each team have a folder of plug-ins to themselves.
c:\animation_plugins\
c:\modeling_plugins\
These could then be e.g. per-project or per-studio if you wanted, and are can be added to the Pyblish path either at application startup, or in any kind of task management system you’ve got to establish an environment for the artist (such as Allzpark!).
Targets is a way for you to do this from within each plug-in. Such that all plug-ins can reside in a single folder, but where you decide which “group” of plug-ins to use upon launching a GUI, e.g. pyblish.api.register_target("modeling")
But the perhaps most flexible method of branching plug-ins is by use of families; such that your Collector picks up “modeling” instances that modelers make, or “animation” instances that animators make. The instance is then tagged with a family, like model which is then matched with any plug-in supporting that family.
This would be the recommended approach, as it doesn’t prevent an artist from making a modeling publish from within a rendering or animation context; it would be entirely dependent on the instance they are making, whether it’s a model or something else. Then it would be up to your tools to either permit/prevent them to actually create such instances.
Thank you very much for your insightful reply.
I will have a play! Seems promising and I’m glad that can be flexible as you describe.
P.S. Allzpark looks fantastic!
I was considering learning Rez or your Bleeding-Rez version anytime in the future and this will make things much much easier.
Thank you very much for your (your team) awesome work!
Another example now regarding extraction:
This will also include an action to “fix the issue”. Edit - As per @tokejepsen important workflow tip below I’ve updated the example.
"""
Extracts the current working document.
"""
import os
from os.path import expanduser
import pyblish
from pyblish import api
import c4d
doc = c4d.documents.GetActiveDocument()
doc_name = doc.GetDocumentName()
class CollectDocument(api.ContextPlugin):
"""
Collect the document data.
"""
order = api.CollectorOrder + 0.1
label = "Collect Document Info"
def process(self, context):
instance = context.create_instance(doc_name, family="scene")
instance.data["doc"] = doc
self.log.info("{} Collected!".format(doc_name))
class SaveSceneAction(api.Action):
"""
Action to let user save the file
"""
label = "Save File"
on = "failed"
icon = "wrench"
def process(self, context, plugin):
file_path = c4d.storage.LoadDialog(title="Save Scene File", flags=c4d.FILESELECT_SAVE,
force_suffix="c4d")
if file_path == "":
raise Exception("You need to save the scene before extraction!")
c4d.documents.SaveDocument(doc, file_path,
c4d.SAVEDOCUMENTFLAGS_CRASHSITUATION | c4d.SAVEDOCUMENTFLAGS_AUTOSAVE,
c4d.FORMAT_C4DEXPORT)
c4d.documents.KillDocument(doc)
c4d.documents.LoadFile(file_path)
c4d.EventAdd()
self.log.info("{0} Saved to {1}".format(doc_name, file_path))
class ValidateScene(api.InstancePlugin):
"""
Checks if scene file has all of the requirement elements before extraction.
"""
order = api.ValidatorOrder
families = ["scene"]
label = "Validate Document Info"
actions = [SaveSceneAction]
def process(self, instance):
def save_scene():
"""
Function to save the scene. Returns True if the scene has been saved and it's OK to continue.
"""
if doc.GetDocumentPath() == "" or doc.GetChanged():
return False
return True
doc_saved = save_scene()
if not doc_saved:
raise Exception("You need to save the scene before extraction!")
class ExtractScene(api.InstancePlugin):
"""
Extract the current working scene.
"""
order = api.ExtractorOrder
families = ["scene"]
label = "Save Scene File"
def process(self, instance):
path = os.path.join(expanduser("~"), doc_name)
result = c4d.documents.SaveDocument(doc, path,
c4d.SAVEDOCUMENTFLAGS_CRASHSITUATION | c4d.SAVEDOCUMENTFLAGS_0,
c4d.FORMAT_C4DEXPORT)
if not result:
raise Exception("Unexpected error! Scene file not saved!")
self.log.info("{0} Saved to {1}".format(doc_name, path))
api.register_plugin(CollectDocument)
api.register_plugin(ValidateScene)
api.register_plugin(ExtractScene)
Hope this helps and please give me a shout if this can be improved
Cheers!
A typical workflow is to have actions on validators. Validators should be quick checks before longer running extractors.
You might find that if you have more extractors, people will be waiting for long running extractors before they can “fix the issue”.
Typically you dont want extractors to raise exceptions. When the publish passes validation, you would expect the publish to go through.
If extractors fail, you probably need to adjust your validators or create a new one.
There is a still an exception raising in the extractor. Is that a safety net?
One thing to look out for is that your validator checks for two things, so when it fails and produces an action for the user, its only a fix for one of those checks. Might be confusing for users, but it depends on how you introduce users to the Pyblish.
Good to encourage users to inspect what the exception is, but equally good to make the validators with a single goal.
yeah, I wasn’t too sure how to tackle this in case the extraction failed when saving the file to the server (where will eventually will be saved). The first validation save would be a local save and the extraction would basically confirm the saved file into the server. Even though I haven’t experience the file failing, there is still the possibility, i think. i.e. Server down or Cinema crashes, etc.
Would you recommend a workaround for this?
Fair enough! That makes sense to me if it takes any ambiguity from the tools.
Hmm, there are various workflow for this, but nothing standardised yet.
If you are just moving files (sorry, not entirely sure I get your plugin flow) around it been, then the integrations step might be more logical. Extractors are typically meant for getting data out of the DCCs, then integrators move the data to its final location.
@AndreAnjos are you still using Pyblish with Cinema4D? How is that going?
I’m investigating integrating Cinema4D with Avalon and part of that is the usage of Pyblish. I was hoping you’d be able to provide some pointer on what might or might not work. And maybe even you would have some Pyblish plug-ins available open-source as a reference.
Aside of that, do you happen to run other Qt interfaces with Pyblish? I noticed PySide interfaces run fine, but somehow there’s no need to call app.exec_() on the QApplication instance.
Yes, we are! I’m based on the animation team, so this has been where the majority and type of tests were made. It’s been received so well, that I’m incorporating to the modelling team. We are a mid-size studio and we are quite new, so there are a few things still being sorted.
Absolutely! I don’t have any open source info at the moment, but can PM you some examples.
Unfortunately, I haven’t used other QT interfaces with Pyblish. I know that Cinema’s interface and QT don’t work well together, which to be honest is expected. Sorry!
But let me know if you want me to do some testing.
I’m also currently testing with R21. I can’t predict any complications, but hey you never know!
Thanks for the details. Would love to see some usage examples, looking forward to that PM.
Plus, if the studio is willing I’d definitely recommend open-sourcing the code. I know some small studios who are keen on getting started with C4D and Pyblish, and having examples out in the open could be what makes them start adopting it. Best case scenario you’ll be getting resources back on that front from the open-source community.
For now there’s no need. I was just wondering if anyone had done that on your end already.
Hi @AndreAnjos, do you have any updates on this? Can you share a working Pyblish integration for Cinema4d? At the moment I’m working for a studio that also uses Cinema4d, together with Maya. Using Pyblish would be super helpful in getting assets from Cinema to Maya.
Any help, updates or code would be greatly appreciated!
Hi @jasperge,
Unfortunately I haven’t released anything public yet as my schedule is absolutely crazy at the moment.
I will send you what I currently have for the animation team and anything you like help with I will do my best.
Are you working with specific format between Cinema and Maya?