Ramp UI hint questions

UI Hints for Ramps

Background

The recently introduced UI Hints have provided a unified and consistent way of describing potentially complex authoring interfaces which has significantly helped in providing consistent interfaces. However, ramps were excluded from the initial proposal, and we’d like to assist in defining a possible unified uihint structure. Ramps are generally defined as a combination of multiple properties:

  • Array of positions
  • Array of values
  • Array of interpolation modes (or single value for global interpolation; normally if the renderer does not support per-knot interpolation)
  • Knot count (optional; some renderers such as RenderMan define a property that holds the ramp size, although generally it can be inferred from the array of positions and/or values)

Currently, there isn’t a unified mechanism to declare that these properties should be presented to the user with a ramp widget; and their relationships to eachother.

Idea and questions

Our initial idea is to define metadata (in the uiHints dictionary) that would be set on one of the properties that make up a ramp, to link it to the other properties and describe the widget.

The following example illustrates setting the hints on the positions array:

#usda 1.0

def Shader "example"
{
    float[] my_ramp_positions = [0.0, 0.1, 0.4, 1.0] (
        uiHints = {
            token rampPositionsPropertyName = "my_ramp_positions"
            token rampValuesPropertyName = "my_ramp_values"
            token rampInterpolationsPropertyName = "my_ramp_interpolations"
            token rampSizePropertyName = "my_ramp_size"
        }
    )

    color3f[] my_ramp_values = [(0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0), (1.0, 1.0, 1.0)] (
        uiHints = {
            bool hidden = 1
        }
    )

    int[] my_ramp_interpolations = [1, 0, 1, 1] (
        uiHints = {
            bool hidden = 1
            dictionary valueLabels = {
                int constant = 0
                int linear = 1
                int bezier = 2
            }
            token[] valueLabelsOrder = ["constant", "linear", "bezier"]
        }
    )

    int my_ramp_size = 4 (
        uiHints = {
            bool hidden = 1
        }
    )
}

The metadata fields are:

  • rampPositionsPropertyName: Name of the property that holds the knot positions.
  • rampValuesPropertyName: Name of the property that holds the knot values.
  • rampInterpolationsPropertyName: Name of the property that holds the knot interpolations.
  • rampSizePropertyName (optional): Name of the property that holds the ramp size (i.e. the knot count).

All properties are expected to be siblings. Properties can have any arbitrary name: the metadata tells the ramp widget where the relevant properties are located so that they can be read and written as the ramp is edited by the user in the UI. This should allow existing scenes to work as-is, with additional metadata helping to tie things together.

The property that defines the ramp metadata becomes the main property. All other properties must be explicitly marked as hidden so as not to show any widget for the companion properties. Naturally, any other metadata, such as display label, would be set in the main property as the others will be hidden.

The type of the values property determines whether the ramp is a color ramp (e.g. color3f[]) or a float ramp (e.g. float[]).

These UI hints would apply equally to both USD Attributes and Shader Properties.

Interpolations

Whether the interpolation is per knot or global does not require a dedicated metadata field. It can be determined based on the topology of the interpolations property: per-knot interpolation if it is an array or global interpolation if it is a scalar.

The selection of interpolations available varies between renderers. The valueLabels metadata would list the possible interpolation modes that the UI will offer to the user. Each of the interpolation modes would be mapped to the actual value that is ultimately set in the referenced interpolations property based on the user selection in the UI.

Although USD would not control what interpolations are available, it could define a list of common ones so that ramp widget implementations can prepare visual representations for them.

Weights

Both Bezier and Hermite curves need weights which would be explicit tangent handles per knot.

Question: We are unsure whether the representation in the data should be interleaved into the knot position structure, or be an additional structure.

Attribute Connections

We believe that connections should be handled using the current system. However, this means that connections per knot would have to be fulfilled by additional multi-input to array node to convert many single float/color values into a single array ready for the shader to comprehend.

Unlike animation splines, we don’t believe there is a requirement for USD value resolution to evaluate values given a point on the ramp, since this is typically handed by the shaders within the renderers.

Conclusion & questions

This is based on our understanding and practical use of ramp style widgets, particularly within shaders. There may well be topics we’ve missed, or considerations we haven’t encountered. We welcome any thoughts or feedback on this, with the aim for us to produce a proposal once we believe we’re not missing any critical information.

When we looked at ramps last year, in addition to the complexity of coordinating the multiple properties, we looked around at several different packages’ notions of ramps and saw alot of variation, and so decided to defer consideration. So the first question I’m really interested in is whether this proposed parameterization seems adoptable/adaptable to the big shading/lighting DCC’s?

In terms of Pixar/RenderMan, the array-attributes encoding works for us - and to your question about weights/tangents, James… PxrRamp doesn’t go there, as catrom is the only higher-order basis it supports, from what I can tell. But I’d suggest a separate array of weights, to avoid the need for defining/depending-on a custom struct on the shading side.

My primary suggestion is to migrate the “ramp hints package” from any arbitrarily chosen property, to the prim level. That will make it much easier/faster to identify presence of a ramp on a prim (question: are there any known scenarios where a single shader/prim would want to have more than one ramp?) and importantly, easier to validate the ramp data/setup. And then also, turn it into a sub-dictionary for clarity and grouping. So…

def Shader "example"
(
    uiHints = {
        dictionary ramp = {
            token positionsPropertyName = "my_ramp_positions"
            token valuesPropertyName = "my_ramp_values"
            token interpolationsPropertyName = "my_ramp_interpolations"
            token sizePropertyName = "my_ramp_size"
        }
    }
)
{
...
}

You could also identify a “primary property” in there if it’s useful.

On attribute connections… UsdShade does support multiple connected targets, implemented exactly for building up an array input. There’s a potentially crippling limitation (that does not affect our use case, which is material layering) which is that each source/target can only be used to fill a single element in an array (because connections are listOps, which create unique sets, not actual, general “lists”. So you would need that “array builder” node explicitly in a shading network.

Hi Spiff,

Given shaders can be arbitrarily defined I think it may be risky to define it at the Prim level given, as you question, there may be a shader with multiple ramps. I am also not certain that there is a need to quickly find whether a prim has a ramp shader or not. The typical approach to creating the UI elements for a prim/shader is to iterate its properties, it actually makes it harder to discern if we suddenly need to read from the Prim as well. Similarly this becomes more difficult at the SdrShaderProperty/SdrShader level.

In our example it would be handled simply by looping the properties and handling them individually, when we hit the arbitrary property, this is where the UI element is read and multiple properties hooked up.

On Weights/Tangents, I’ve checked a few different renderers documentation pages and can confirm that I cannot find documentation in the renderers specifying any interpolations other than:
Constant, Linear, Catmull-Rom, Monotone Cubic. Although most documentation pages do not mention the list, so this may not be correct across the board. Certainly intrigued to hear from any other renderers/shader developers as to whether this is required?

Hope that makes our intention and workflow clearer?

Thanks,
James

Additionally, we’d need to support promoting such properties to the “NodeGraph”/“Material” interface, and as such would prefer the data to be stored on the properties, since we can hoist the metadata directly. This means there is a certainty of requiring multiple ramp properties per Prim.

This approach is quite similar to ours. A few notes:

  • Since a shader can have several ramps, I agree that defining the ramp at a prim level is risky, unless there is some disambiguation provision to support several ramp attribs.
  • We have an explicit token for interpolation array vs. scalar, but I agree that this can be deduced from the attrib type
  • We author the metadata on all the attributes that constitute the ramp sturcture. It is a bit excessive/redundant and can lead to inconsistencies, but it allows to quickly find all the relevant pieces when user wants to edit the ramp value.
  • In addition to the interpolation listed above, Karma also has: Bezier, B-spline, and Hermite. But the ramp does not take any additional parameters beyond the positions and values: the final knots and weights are implicitly calculated from the keys array (eg, the Hermite boundary tangents come from the first and last pair of keys).
        def Shader "genericshader1"
        {
            uniform token info:id = "shader"
            int inputs:my_ramp = 3 (
                customData = {
                    string rampbasisattr = "inputs:my_ramp_basis"
                    bool rampbasisisarray = 0
                    string rampcountattr = "inputs:my_ramp"
                    string rampposattr = "inputs:my_ramp_keys"
                    string rampvalueattr = "inputs:my_ramp_values"
                }
            )
            token inputs:my_ramp_basis = "linear" (
                customData = {
                    string rampbasisattr = "inputs:my_ramp_basis"
                    bool rampbasisisarray = 0
                    string rampcountattr = "inputs:my_ramp"
                    string rampposattr = "inputs:my_ramp_keys"
                    string rampvalueattr = "inputs:my_ramp_values"
                }
            )
            float[] inputs:my_ramp_keys = [0, 0.5, 1] (
                customData = {
                    string rampbasisattr = "inputs:my_ramp_basis"
                    bool rampbasisisarray = 0
                    string rampcountattr = "inputs:my_ramp"
                    string rampposattr = "inputs:my_ramp_keys"
                    string rampvalueattr = "inputs:my_ramp_values"
                }
            )
            color3f[] inputs:my_ramp_values = [(0, 0, 0), (0, 1, 0), (1, 1, 1)] (
                customData = {
                    string rampbasisattr = "inputs:my_ramp_basis"
                    bool rampbasisisarray = 0
                    string rampcountattr = "inputs:my_ramp"
                    string rampposattr = "inputs:my_ramp_keys"
                    string rampvalueattr = "inputs:my_ramp_values"
                }
            )
            float outputs:out
        }