OpenPype icon indicating copy to clipboard operation
OpenPype copied to clipboard

Maya: ValidateRenderSettings can fail on float values

Open vonbehr opened this issue 3 years ago • 5 comments

Running version 3.12.2

Describe the bug When validating Vray Render Attibutes that have a float value, the validation can fail because of precision errors.

Example error message: Invalid value 2.200000047683716 set on vraySettings.cmap_gamma. Expecting 2.2

To Reproduce Steps to reproduce the behavior:

  1. Configure project_settings/maya/publish/ValidateRenderSettings/vray_render_attributes with VRaySettingsNode.map_gamma and a value of 2.2.
  2. Create a render scene in Maya.
  3. Create a render family...
  4. ...and try to run validation.

Expected behavior Scene should validate.

Desktop (please complete the following information):

  • OS: Windows
  • Host: Maya 2022
  • Plugin: V-Ray 5.20.02

[cuID:]

vonbehr avatar Sep 21 '22 13:09 vonbehr

The question really would be, for those values - what precision would we like to compare float values with? Then if the value we're comparing with we could do the check like math.abs(value-check_value) < precision so that deviations of e.g. 1e-6 (= 0.000001) are allowed.

The deviation in the problem here is about 4e-8.

BigRoy avatar Sep 21 '22 13:09 BigRoy

Have looked into this a bit more and the abs(value-check_value) < tolerance with tolerance of 1e-6 works perfect.

However, it does leave the question with what we'd want to do with potential other attributes. How do we want to handle Enum attributes? (Do we allow the user to set it by its string value, or only by its index?) How do we want to handle Boolean values? (Do we allow the settings to be "False", "True", "no", "yes", "off", "on", or just 1 or 0?

It's slightly more involved than we'd think because the settings for OpenPype will always store the value as a string.

import six
from maya import cmds


def _convert_value(value, value_type):
    """Convert a value to an explicit python type.
    
    This allows string conversions to boolean from
    "False", "True", "no", "off", "yes" and "1", "0"

    """
    
    if type(value) is value_type:
        # No conversion needed
        return value
    
    # Boolean
    if value_type is bool:
        if isinstance(value, str):
            if value.isnumeric():
                # Consider "0" or "0.0" as False
                return bool(float(value))
            elif value.lower() in {"false", "no", "off"}:
                return False
            else:
                return bool(value)
        else:
            return bool(value)
            
    # Integer, Float, String straight conversion
    if value_type in {int, float, str}:
        return value_type(value)
                
    # Passthrough any other type
    raise NotImplementedError("Unsupport type conversion to: {}".format(value_type))


def compare_attribute(attr, value, tolerance=1e-6):
    """Compare attribute with a value

    It will try to convert the input value to the Maya
    attribute's type. For example a string value from OP settings
    can be interpreted to matched the attribute type.
    Also supports other value types as inputs.

    It will return None if the attributes matches the
    value within the tolerance (float attributes only).
    If the attribute does not match then it will return
    a tuple with the current value and the value you
    can set it to with `maya.cmds.setAttr`
    
    Returns:
        None: If attribute matches value None is returned
        tuple: If attribute does not match value then a
            2-tuple is returned as (current_value, to_set_value)
            The to set value can differ from the input value
            argument because it will be converted to the
            attribute's type.
    
    """
    
    attr_type = cmds.getAttr(attr, type=True)
    attr_value = cmds.getAttr(attr)
    
    # Handle special case enum
    if attr_type == "enum":
        if value == attr_value:
            return 
        
        attr_value_str = cmds.getAttr(attr, asString=True)
        if isinstance(value, six.string_types):
            if str(value) == attr_value_str:
                # matching string value
                return
            node, attr_name = attr.split(".", 1)
            
            # get enum index from string match
            enum_strings = cmds.attributeQuery(attr_name, node=node, listEnum=True)[0].split(":")
            try:
                index = enum_strings.index(value)
            except ValueError:
                # String value does not exist
                if value.isnumeric():
                    value = int(value)
                else:
                    raise ValueError("Enum attribute {} has no value: {} (enums: {})".format(attr, value, enum_strings))
            else:
                return [value, index]
                
        value = int(value)
        return [attr_value_str, value]
            
    
    # Mapping of Maya attribute type to Python type
    python_type = {
        "string": str,
        "bool": bool,
        "long": int,
        "short": int,
        "byte": int,
        "float": float,
        "double": float,
        "doubleAngle": float,
        "doubleLinear": float,
        "time": float
    }[attr_type]
    
    value = _convert_value(value, value_type=python_type)
    if tolerance and python_type is float:
        # Allow to match within absolute tolerance range
        if abs(value - attr_value) < tolerance:
            return
    elif value == attr_value:
        return
        
    # Return current value and to set value and a function
    return [attr_value, value]
        

for attr, value in [
    # bool
    ("defaultArnoldRenderOptions.motion_blur_enable", "off"),
    ("defaultArnoldRenderOptions.motion_blur_enable", "on"),
    ("defaultArnoldRenderOptions.motion_blur_enable", "0.1"),
    ("defaultArnoldRenderOptions.motion_blur_enable", "1.0"),
    ("defaultArnoldRenderOptions.motion_blur_enable", "False"),
    ("defaultArnoldRenderOptions.motion_blur_enable", "True"),
    ("defaultArnoldRenderOptions.motion_blur_enable", "-"),

    # int
    ("defaultArnoldRenderOptions.GIDiffuseSamples", "4"),
    ("defaultArnoldRenderOptions.GIDiffuseSamples", 1),

    # float
    ("defaultArnoldRenderOptions.motion_frames", "2.2"),
    ("defaultArnoldRenderOptions.motion_frames", "2.1"),
    ("defaultArnoldRenderOptions.motion_frames", 3.0),
    ("defaultArnoldRenderOptions.motion_frames", True),

    # enum
    ("defaultArnoldRenderOptions.range_type", "Center On Frame"),
    ("defaultArnoldRenderOptions.range_type", "0"),
    ("defaultArnoldRenderOptions.range_type", 5),
    ("defaultArnoldRenderOptions.range_type", "Start On Frame")
]:
    
    msg = f"Set {attr} to {value}"
    diff = compare_attribute(attr, value)
    if diff:
        current_value, to_set_value = diff
        msg = f"{msg: <70} --> invalid value {current_value} - expecting {value} (to set value: {to_set_value})"
    else:
        current_value = cmds.getAttr(attr)
        msg = f"{msg: <70} --> matches current value {current_value}"
    
    print(msg)

Seems a bit convoluted - but maybe makes most sense from a user perspective - they can just type a value and our logic tries to "auto-match" it.

Example output:

Set defaultArnoldRenderOptions.motion_blur_enable to off               --> invalid value True - expecting off (to set value: False)
Set defaultArnoldRenderOptions.motion_blur_enable to on                --> matches current value True
Set defaultArnoldRenderOptions.motion_blur_enable to 0.1               --> matches current value True
Set defaultArnoldRenderOptions.motion_blur_enable to 1.0               --> matches current value True
Set defaultArnoldRenderOptions.motion_blur_enable to False             --> invalid value True - expecting False (to set value: False)
Set defaultArnoldRenderOptions.motion_blur_enable to True              --> matches current value True
Set defaultArnoldRenderOptions.motion_blur_enable to -                 --> matches current value True
Set defaultArnoldRenderOptions.GIDiffuseSamples to 4                   --> invalid value 1 - expecting 4 (to set value: 4)
Set defaultArnoldRenderOptions.GIDiffuseSamples to 1                   --> matches current value 1
Set defaultArnoldRenderOptions.motion_frames to 2.2                    --> matches current value 2.200000047683716
Set defaultArnoldRenderOptions.motion_frames to 2.1                    --> invalid value 2.200000047683716 - expecting 2.1 (to set value: 2.1)
Set defaultArnoldRenderOptions.motion_frames to 3.0                    --> invalid value 2.200000047683716 - expecting 3.0 (to set value: 3.0)
Set defaultArnoldRenderOptions.motion_frames to True                   --> invalid value 2.200000047683716 - expecting True (to set value: 1.0)
Set defaultArnoldRenderOptions.range_type to Center On Frame           --> invalid value Center On Frame - expecting Center On Frame (to set value: 1)
Set defaultArnoldRenderOptions.range_type to 0                         --> invalid value Start On Frame - expecting 0 (to set value: 0)
Set defaultArnoldRenderOptions.range_type to 5                         --> invalid value Start On Frame - expecting 5 (to set value: 5)
Set defaultArnoldRenderOptions.range_type to Start On Frame            --> matches current value 0

BigRoy avatar Sep 22 '22 15:09 BigRoy

Right now the validator does accept bolleans. In fact you have to use True and False in the Pype settings if you want to validate any attribute that that only has values 0 and 1. Using 0 and 1 in the settings will error at the moment.

vonbehr avatar Sep 22 '22 15:09 vonbehr

In fact you have to use True and False in the Pype settings if you want to validate any attribute that that only has values 0 and 1. Using 0 and 1 in the settings will error at the moment.

Ah of course, it'll currently convert the maya value to str in the comparison too. Actually - that might be the most trivial option to maintain and would remain quite explicit. Similarly we would then set enum attributes as integers too and we should be quite close. Thanks!

BigRoy avatar Sep 22 '22 16:09 BigRoy

Set up a slightly simpler version in a branch here with this commit. It also replaces some logic in a few areas to this logic - which act as great examples. I'm not too happy with the implementation yet - I feel like it could be simpler and less abstract in naming with attribute_diff too.

I haven't made the PR as a draft to OpenPype yet because it's build on top of #3880 and thus would list many more changes than relevant. Once that PR is in I can open a draft easily.

BigRoy avatar Sep 22 '22 21:09 BigRoy