ynput / OpenPype

Main OpenPype repository and AYON openpype addon codebase
https://openpype.io
MIT License
280 stars 127 forks source link

OCIO config environment variables distribution #4299

Open jakubjezek001 opened 1 year ago

jakubjezek001 commented 1 year ago

Is your feature request related to a problem? Please describe. At the moment we are having working colorspace distribution with OCIO support, but we are still missing a part where EP, SEQ, SHOT environment variables occasionally used by production are set before a host application is started.

Describe the solution you'd like Pre-lauch hooks woflow should be used which would distill mentioned environment variables from the hierarchy of context and set them, so any OCIO config which is using such env vars will be able to use it. Idea here is to fill those variables if the any of parent above shot is sequence or episode assetType.

Additional context related PR to https://github.com/ynput/OpenPype/pull/4195

[cuID:OP-4724]

BigRoy commented 1 year ago

Could you clarify slightly further?

Does this mean the OCIO config would be pointed to using for example: /path/to/ocio/{EP}/{SHOT}? Does this mean that - with such a template - that ALL shots must have it like that? Or is it in reverse that the EP or SQ can have a custom OCIO env var config?

Or are you referring to it in another way of 'filling those variables'?

ddesmond commented 1 year ago

Im currently using the prehook to load OCIO setups in clarisse if there are overrides on the folder structure level. Setting up either global or per task level overrides works nice. Idealy this would go from content creation, lightning and rendering to compositing tasks. All should load the same OCIO to minimize the posible discrepancy in OCIO versions.

fabiaserra commented 4 months ago

I have this function

def get_hierarchy_env(project_doc, asset_doc, skip_empty=True):
    """Returns an environment dictionary based on the hierarchy of an asset in a project

    The environment dictionary contains keys representing the different levels of the
    visual hierarchy (e.g. "SHOW", "SEASON", "EPISODE", etc.) and their corresponding
    values, if available.

    Args:
        project_doc (dict): A dictionary containing metadata about the project.
        asset_doc (dict): A dictionary containing metadata about the asset.
        skip_empty (bool): Whether to skip env entries that we don't have a value for.

    Returns:
        dict: An environment dictionary with keys "SHOW", "SEASON", "EPISODE", "SEQ",
            "SHOT", and "ASSET_TYPE". The values of the keys are the names of the
            corresponding entities in the hierarchy. If an entity is not present in the
            hierarchy, its corresponding key will not be present or have a value of None
            if 'skip_empty' is set to False.

    """
    visual_hierarchy = [asset_doc]
    current_doc = asset_doc
    project_name = project_doc["name"]
    while True:
        visual_parent_id = current_doc["data"]["visualParent"]
        visual_parent = None
        if visual_parent_id:
            visual_parent = get_asset_by_id(project_name, visual_parent_id)

        if not visual_parent:
            break

        visual_hierarchy.append(visual_parent)
        current_doc = visual_parent

    # Dictionary that maps the SG entity names from SG-leecher to their corresponding
    # environment variables
    sg_to_env_map = {
        "Project": "SHOW",
        "Season": "SEASON",
        "Episode": "EPISODE",
        "Sequence": "SEQ",
        "Shot": "SHOT",
        "Asset": "ASSET_TYPE",
    }

    # We create a default env with None values so when we switch context, we can remove
    # the environment variables that aren't defined
    env = {
        "SHOW": project_doc["data"]["code"],
        "SEASON": None,
        "EPISODE": None,
        "SEQ": None,
        "SHOT": None,
        "SHOTNUM": None,
        "ASSET_TYPE": None,
    }

    # For each entity on the hierarchy, we set its environment variable
    for parent in visual_hierarchy:
        sg_entity_type = parent["data"].get("sgEntityType")
        env_key = sg_to_env_map.get(sg_entity_type)
        if env_key:
            env[env_key] = parent["name"]

    # Fill up SHOTNUM assuming it's the last token part of the SHOT env
    # variable
    if env.get("SHOT"):
        env["SHOTNUM"] = env["SHOT"].split("_")[-1]

    # Remove empty values from env if 'skip_empty' is set to True
    if skip_empty:
        env = {key: value for key, value in env.items() if value is not None}

    return env

that I added on context_tools.py:change_current_context:

def change_current_context(asset_doc, task_name, template_key=None):
    """Update active Session to a new task work area.

    This updates the live Session to a different task under asset.

    Args:
        asset_doc (Dict[str, Any]): The asset document to set.
        task_name (str): The task to set under asset.
        template_key (Union[str, None]): Prepared template key to be used for
            workfile template in Anatomy.

    Returns:
        Dict[str, str]: The changed key, values in the current Session.
    """

    changes = compute_session_changes(
        legacy_io.Session,
        asset_doc,
        task_name,
        template_key=template_key
    )

    # Update the Session and environments. Pop from environments all keys with
    # value set to None.
    for key, value in changes.items():
        legacy_io.Session[key] = value
        if value is None:
            os.environ.pop(key, None)
        else:
            os.environ[key] = value

    ### Starts Alkemy-X Override ###
    # Calculate the hierarchy environment and update
    project_doc = get_project(legacy_io.Session["AVALON_PROJECT"])
    hierarchy_env = get_hierarchy_env(project_doc, asset_doc, skip_empty=False)
    for key, value in hierarchy_env.items():
        if value is None:
            os.environ.pop(key, None)
        else:
            os.environ[key] = value
    ### Ends Alkemy-X Override ###

    data = changes.copy()
    # Convert env keys to human readable keys
    data["project_name"] = legacy_io.Session["AVALON_PROJECT"]
    data["asset_name"] = legacy_io.Session["AVALON_ASSET"]
    data["task_name"] = legacy_io.Session["AVALON_TASK"]

    # Emit session change
    emit_event("taskChanged", data)

    return changes

This works perfectly thanks to our customization of the SG leecher that adds the sgEntityType on the assets since without that OP is not really aware of what entity is what. This should be possible to change now with Ayon

jakubjezek001 commented 4 months ago

Thanks, @fabiaserra, for the solid solution. To implement it in Ayon, we need to address situations where a context change through the workfile tool is triggered. This workflow ties into active environment variables.

Another point to consider: We're not just limited to SHOT, SQ, EP as key names. We can use any of the environment variables. Currently, we're using AYON_PROJECT_ROOT_WORK, AYON_FOLDER_PATH, AYON_PROJECT_NAME, AYON_WORKDIR, AYON_HOST_NAME.

But since we are not having variable alternative for AYON_FOLDER_NAME we will need to have some sort of global prelaunch hook which will set it on demand. We might also need to get path only to a folder parent, perhaps for sequential luts so AYON_FOLDER_PARENT_PATH and AYON_FOLDER_PARENT_NAME keys might come useful.

At least that is how I understand it from here

These contexts are usually based on environment variables, but also allows on-the-fly context switching in applications that operate on multiple shots (such as playback tools)

Edit: Any environment variables could be also remapped inside of any ocio.config https://opencolorio.readthedocs.io/en/latest/guides/authoring/overview.html#environment

This means we could have our own environment declaration as such, and use them accordingly:

environment:
  AYON_FOLDER_NAME: $AYON_FOLDER_NAME
  AYON_FOLDER_PARENT_PATH: $AYON_FOLDER_PARENT_PATH
  AYON_FOLDER_PARENT_NAME: $AYON_FOLDER_PARENT_NAME 
  AYON_FOLDER_PATH: $AYON_FOLDER_PATH
  AYON_PROJECT_NAME: $AYON_PROJECT_NAME
  AYON_WORKDIR: $AYON_WORKDIR
  AYON_HOST_NAME: shell
  AYON_PROJECT_ROOT_WORK: $AYON_PROJECT_ROOT_WORK

This will allow us to use search paths following way:

search_path: |
  $AYON_PROJECT_ROOT_WORK/$AYON_FOLDER_PATH/colorspace/$AYON_HOST_NAME/luts:
  $AYON_PROJECT_ROOT_WORK/$AYON_FOLDER_PARENT_PATH/colorspace/luts:
  $AYON_PROJECT_ROOT_WORK/$AYON_PROJECT_NAME/colorspace/luts:
  luts:

And of course even resolve shot related .cc which are stored on project level:

- !<ColorSpace>
    name: srgb8
    family: srgb
    bitdepth: 8ui
    isdata: false
    from_reference: !<GroupTransform>
      children:
        - !<FileTransform> {src: ${AYON_FOLDER_PARENT_NAME}_${AYON_FOLDER_NAME}.cc}
        - !<ColorSpaceTransform> {src: lnh, dst: lg10}
        - !<FileTransform> {src: film_emulation.spi3d, interpolation: linear}
fabiaserra commented 4 months ago

Thanks, @fabiaserra, for the solid solution. To implement it in Ayon, we need to address situations where a context change through the workfile tool is triggered. This workflow ties into active environment variables.

My solution already addresses that too, might be because I also call this function here?

def prepare_context_environments(data, env_group=None, modules_manager=None):
    """Modify launch environments with context data for launched host.

    Args:
        data (EnvironmentPrepData): Dictionary where result and intermediate
            result will be stored.
    """

    from openpype.pipeline.template_data import get_template_data
    from openpype.pipeline.context_tools import get_hierarchy_env

    # Context environments
    log = data["log"]

    project_doc = data["project_doc"]
    asset_doc = data["asset_doc"]
    task_name = data["task_name"]
    if not project_doc:
        log.info(
            "Skipping context environments preparation."
            " Launch context does not contain required data."
        )
        return

    # Load project specific environments
    project_name = project_doc["name"]
    project_settings = get_project_settings(project_name)
    system_settings = get_system_settings()
    data["project_settings"] = project_settings
    data["system_settings"] = system_settings

    app = data["app"]
    context_env = {
        "AVALON_PROJECT": project_doc["name"],
        "AVALON_APP_NAME": app.full_name
    }
    if asset_doc:
        context_env["AVALON_ASSET"] = asset_doc["name"]
        if task_name:
            context_env["AVALON_TASK"] = task_name

    ### Starts Alkemy-X Override ###
    # Get hierarchy environment variables (i.e., SEASON, SHOW, SEQ...)
    context_env.update(get_hierarchy_env(project_doc, asset_doc))
    ### Ends Alkemy-X Override ###

Another point to consider: We're not just limited to SHOT, SQ, EP as key names. We can use any of the environment variables. Currently, we're using AYON_PROJECT_ROOT_WORK, AYON_FOLDER_PATH, AYON_PROJECT_NAME, AYON_WORKDIR, AYON_HOST_NAME.

That's okay, but I think it's still important to have those env var keys as it's very useful to know at all times the specific folder types and being able to combine them (I have added them on template system too so I can use tokens like {shotnum} or {SEQ} to capitalize the sequence) to fit multiple workflows that require direct access to parent keys by type.

But since we are not having variable alternative for AYON_FOLDER_NAME we will need to have some sort of global prelaunch hook which will set it on demand. We might also need to get path only to a folder parent, perhaps for sequential luts so AYON_FOLDER_PARENT_PATH and AYON_FOLDER_PARENT_NAME keys might come useful.

I personally find that over-complicated and much harder to read than being explicit on using $SHOW, $SHOT, $EP, $SEQ...

This means we could have our own environment declaration as such, and use them accordingly:

environment:
  AYON_FOLDER_NAME: $AYON_FOLDER_NAME
  AYON_FOLDER_PARENT_PATH: $AYON_FOLDER_PARENT_PATH
  AYON_FOLDER_PARENT_NAME: $AYON_FOLDER_PARENT_NAME 
  AYON_FOLDER_PATH: $AYON_FOLDER_PATH
  AYON_PROJECT_NAME: $AYON_PROJECT_NAME
  AYON_WORKDIR: $AYON_WORKDIR
  AYON_HOST_NAME: shell
  AYON_PROJECT_ROOT_WORK: $AYON_PROJECT_ROOT_WORK

The only one you are remapping is AYON_HOST_NAME, there's not much point on redeclaring the ones that point to the same env var that already exists?

This will allow us to use search paths following way:

search_path: |
  $AYON_PROJECT_ROOT_WORK/$AYON_FOLDER_PATH/colorspace/$AYON_HOST_NAME/luts:
  $AYON_PROJECT_ROOT_WORK/$AYON_FOLDER_PARENT_PATH/colorspace/luts:
  $AYON_PROJECT_ROOT_WORK/$AYON_PROJECT_NAME/colorspace/luts:
  luts:

And of course even resolve shot related .cc which are stored on project level:

- !<ColorSpace>
    name: srgb8
    family: srgb
    bitdepth: 8ui
    isdata: false
    from_reference: !<GroupTransform>
      children:
        - !<FileTransform> {src: ${AYON_FOLDER_PARENT_NAME}_${AYON_FOLDER_NAME}.cc}
        - !<ColorSpaceTransform> {src: lnh, dst: lg10}
        - !<FileTransform> {src: film_emulation.spi3d, interpolation: linear}

That's cool but as I said I think we'd still prefer to be explicit in our config with the $EP, $SEQ, $SHOT hierarchies.

jakubjezek001 commented 3 months ago

That's cool but as I said I think we'd still prefer to be explicit in our config with the $EP, $SEQ, $SHOT hierarchies.

The issue with $EP, $SEQ, $SHOT environment variables is that they require a lot of guessing in Ayon. The folder path and parent might work even it it doesn't follow OCIO documentation.

Our clients' experiences suggest that our approach would work for 95% of projects, which typically involve a sequence or shot hierarchy. However, for projects with seasons, episodes, sequences, and shots, we'd need to lay down some more groundwork. It's unclear if the effort is justified for the remaining 5%. It's not clear if these projects use looks from one OCIO config for the whole season or episode. They might use separate configs for each season or episode. If that's the case, this idea could work too.