diff --git a/Changes.md b/Changes.md index 9b02fbb6bf1..7684f601aec 100644 --- a/Changes.md +++ b/Changes.md @@ -12,6 +12,7 @@ Improvements ------------ - LightTool : Changed spot light and quad light edge tool tip locations so that they follow the cone and edge during drag. +- OSL Expression : Added support for getting an element of a vector typed context variable using `context( variableName, index )` or `context( variableName, index, defaultValue )`. Negative a negative index can be used to get elements starting at the end of the array. Fixes ----- diff --git a/python/GafferOSLTest/OSLExpressionEngineTest.py b/python/GafferOSLTest/OSLExpressionEngineTest.py index b096e249eb6..24401f456dc 100644 --- a/python/GafferOSLTest/OSLExpressionEngineTest.py +++ b/python/GafferOSLTest/OSLExpressionEngineTest.py @@ -221,6 +221,143 @@ def testContextTypes( self ) : self.assertEqual( s["n"]["user"]["s"].getValue(), "non-default" ) self.assertEqual( s["n"]["user"]["b"].getValue(), True ) + def testContextVectorTypes( self ) : + + s = Gaffer.ScriptNode() + s["n"] = Gaffer.Node() + for i in range( 0, 4 ) : + s["n"]["user"]["f" + str( i )] = Gaffer.FloatPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["n"]["user"]["i" + str( i )] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["n"]["user"]["c" + str( i )] = Gaffer.Color3fPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["n"]["user"]["v" + str( i )] = Gaffer.V3fPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["n"]["user"]["s" + str( i )] = Gaffer.StringPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["n"]["user"]["b" + str( i )] = Gaffer.BoolPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + s["e"] = Gaffer.Expression() + s["e"].setExpression( + inspect.cleandoc( + """ + parent.n.user.f0 = context( "f", 0, 1 ); + parent.n.user.i0 = context( "i", 0, 1 ); + parent.n.user.c0 = context( "c", 0, color( 1, 2, 3 ) ); + parent.n.user.v0 = context( "v", 0 ); + parent.n.user.s0 = context( "s", 0, "default0" ); + + parent.n.user.f1 = context( "f", 1, 2 ); + parent.n.user.i1 = context( "i", 1, 2 ); + parent.n.user.c1 = context( "c", 1, color( 4, 5, 6 ) ); + parent.n.user.v1 = context( "v", 1 ); + parent.n.user.s1 = context( "s", 1, "default1" ); + + parent.n.user.f2 = context( "f", 2, 3 ); + parent.n.user.i2 = context( "i", 2, 3 ); + parent.n.user.c2 = context( "c", 2, color( 7, 8, 9 ) ); + parent.n.user.v2 = context( "v", 2 ); + parent.n.user.s2 = context( "s", 2, "default2" ); + + parent.n.user.f3 = context( "f", -1 ); + parent.n.user.i3 = context( "i", -1 ); + parent.n.user.c3 = context( "c", -1 ); + parent.n.user.v3 = context( "v", -1 ); + parent.n.user.s3 = context( "s", -1 ); + """ + ), + "OSL" + ) + + with Gaffer.Context() as c : + + self.assertEqual( s["n"]["user"]["f0"].getValue(), 1 ) + self.assertEqual( s["n"]["user"]["i0"].getValue(), 1 ) + self.assertEqual( s["n"]["user"]["c0"].getValue(), imath.Color3f( 1, 2, 3 ) ) + self.assertEqual( s["n"]["user"]["v0"].getValue(), imath.V3f( 0 ) ) + self.assertEqual( s["n"]["user"]["s0"].getValue(), "default0" ) + + self.assertEqual( s["n"]["user"]["f1"].getValue(), 2 ) + self.assertEqual( s["n"]["user"]["i1"].getValue(), 2 ) + self.assertEqual( s["n"]["user"]["c1"].getValue(), imath.Color3f( 4, 5, 6 ) ) + self.assertEqual( s["n"]["user"]["v1"].getValue(), imath.V3f( 0 ) ) + self.assertEqual( s["n"]["user"]["s1"].getValue(), "default1" ) + + self.assertEqual( s["n"]["user"]["f2"].getValue(), 3 ) + self.assertEqual( s["n"]["user"]["i2"].getValue(), 3 ) + self.assertEqual( s["n"]["user"]["c2"].getValue(), imath.Color3f( 7, 8, 9 ) ) + self.assertEqual( s["n"]["user"]["v2"].getValue(), imath.V3f( 0 ) ) + self.assertEqual( s["n"]["user"]["s2"].getValue(), "default2" ) + + self.assertEqual( s["n"]["user"]["f3"].getValue(), 0 ) + self.assertEqual( s["n"]["user"]["i3"].getValue(), 0 ) + self.assertEqual( s["n"]["user"]["c3"].getValue(), imath.Color3f( 0 ) ) + self.assertEqual( s["n"]["user"]["v3"].getValue(), imath.V3f( 0 ) ) + self.assertEqual( s["n"]["user"]["s3"].getValue(), "" ) + + c["f"] = IECore.FloatVectorData( [ 10, 11 ] ) + c["i"] = IECore.IntVectorData( [ 11, 12 ] ) + c["c"] = IECore.Color3fVectorData( [ imath.Color3f( 10, 11, 12 ), imath.Color3f( 13, 14, 15 ) ] ) + c["v"] = IECore.V3fVectorData( [ imath.V3f( 9, 10, 11 ), imath.V3f( 12, 13, 14 ) ] ) + c["s"] = IECore.StringVectorData( [ "non-default0", "non-default1" ] ) + + self.assertEqual( s["n"]["user"]["f0"].getValue(), 10 ) + self.assertEqual( s["n"]["user"]["i0"].getValue(), 11 ) + self.assertEqual( s["n"]["user"]["c0"].getValue(), imath.Color3f( 10, 11, 12 ) ) + self.assertEqual( s["n"]["user"]["v0"].getValue(), imath.V3f( 9, 10, 11 ) ) + self.assertEqual( s["n"]["user"]["s0"].getValue(), "non-default0" ) + + self.assertEqual( s["n"]["user"]["f1"].getValue(), 11 ) + self.assertEqual( s["n"]["user"]["i1"].getValue(), 12 ) + self.assertEqual( s["n"]["user"]["c1"].getValue(), imath.Color3f( 13, 14, 15 ) ) + self.assertEqual( s["n"]["user"]["v1"].getValue(), imath.V3f( 12, 13, 14 ) ) + self.assertEqual( s["n"]["user"]["s1"].getValue(), "non-default1" ) + + self.assertEqual( s["n"]["user"]["f2"].getValue(), 3 ) + self.assertEqual( s["n"]["user"]["i2"].getValue(), 3 ) + self.assertEqual( s["n"]["user"]["c2"].getValue(), imath.Color3f( 7, 8, 9 ) ) + self.assertEqual( s["n"]["user"]["v2"].getValue(), imath.V3f( 0 ) ) + self.assertEqual( s["n"]["user"]["s2"].getValue(), "default2" ) + + self.assertEqual( s["n"]["user"]["f3"].getValue(), 11 ) + self.assertEqual( s["n"]["user"]["i3"].getValue(), 12 ) + self.assertEqual( s["n"]["user"]["c3"].getValue(), imath.Color3f( 13, 14, 15 ) ) + self.assertEqual( s["n"]["user"]["v3"].getValue(), imath.V3f( 12, 13, 14 ) ) + self.assertEqual( s["n"]["user"]["s3"].getValue(), "non-default1" ) + + def testScenePathContext( self ) : + + s = Gaffer.ScriptNode() + s["n"] = Gaffer.Node() + # OSL can't set a `StringVectorDataPlug`, so we set a boolean plug on a successful string match + s["n"]["user"]["p1"] = Gaffer.StringPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["n"]["user"]["p2"] = Gaffer.StringPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["n"]["user"]["p3"] = Gaffer.StringPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["n"]["user"]["p4"] = Gaffer.StringPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + s["e"] = Gaffer.Expression() + s["e"].setExpression( + inspect.cleandoc( + """ + parent.n.user.p1 = context( "scene:path", 0, "noPath1" ); + parent.n.user.p2 = context( "scene:path", 1, "noPath2" ); + parent.n.user.p3 = context( "scene:path", 2 ); + parent.n.user.p4 = context( "scene:path", 3 ); + """ + ), + "OSL" + ) + + with Gaffer.Context() as c : + + self.assertEqual( s["n"]["user"]["p1"].getValue(), "noPath1" ) + self.assertEqual( s["n"]["user"]["p2"].getValue(), "noPath2" ) + self.assertEqual( s["n"]["user"]["p3"].getValue(), "" ) + self.assertEqual( s["n"]["user"]["p4"].getValue(), "" ) + + c["scene:path"] = IECore.InternedStringVectorData( [ "yellow", "brick", "road" ] ) + + self.assertEqual( s["n"]["user"]["p1"].getValue(), "yellow" ) + self.assertEqual( s["n"]["user"]["p2"].getValue(), "brick" ) + self.assertEqual( s["n"]["user"]["p3"].getValue(), "road" ) + self.assertEqual( s["n"]["user"]["p4"].getValue(), "" ) + def testDefaultExpression( self ) : s = Gaffer.ScriptNode() diff --git a/shaders/GafferOSL/Expression.h b/shaders/GafferOSL/Expression.h index dc29bb10315..9b6c3f9e277 100644 --- a/shaders/GafferOSL/Expression.h +++ b/shaders/GafferOSL/Expression.h @@ -97,4 +97,66 @@ string context( string name ) return context( name, "" ); } +// Vector context variable queries with index + +int context( string name, int index, int defaultValue ) +{ + int result = defaultValue; + getattribute( "gaffer:context", name, index, result ); + return result; +} + +int context( string name, int index ) +{ + return context( name, index, 0 ); +} + +float context( string name, int index, float defaultValue ) +{ + float result = defaultValue; + getattribute( "gaffer:context", name, index, result ); + return result; +} + +float context( string name, int index ) +{ + return context( name, index, 0.0 ); +} + +color context( string name, int index, color defaultValue ) +{ + color result = defaultValue; + getattribute( "gaffer:context", name, index, result ); + return result; +} + +color context( string name, int index ) +{ + return context( name, index, color( 0.0 ) ); +} + +vector context( string name, int index, vector defaultValue ) +{ + vector result = defaultValue; + getattribute( "gaffer:context", name, index, result ); + return result; +} + +vector context( string name, int index ) +{ + return context( name, index, vector( 0.0 ) ); +} + +string context( string name, int index, string defaultValue ) +{ + string result = defaultValue; + getattribute( "gaffer:context", name, index, result ); + return result; +} + +string context( string name, int index ) +{ + return context( name, index, "" ); +} + #endif // GAFFEROSL_EXPRESSION_H diff --git a/src/GafferOSL/OSLExpressionEngine.cpp b/src/GafferOSL/OSLExpressionEngine.cpp index 597b7ad87c6..4c6ce2c1fcb 100644 --- a/src/GafferOSL/OSLExpressionEngine.cpp +++ b/src/GafferOSL/OSLExpressionEngine.cpp @@ -54,8 +54,10 @@ #include "IECoreImage/OpenImageIOAlgo.h" +#include "IECore/DataAlgo.h" #include "IECore/SearchPath.h" #include "IECore/StringAlgo.h" +#include "IECore/TypeTraits.h" #include "OSL/oslcomp.h" #include "OSL/oslexec.h" @@ -102,6 +104,65 @@ struct RenderState // our RenderState. ////////////////////////////////////////////////////////////////////////// + +bool getAttributeInternal( OSL::ShaderGlobals *sg, bool derivatives, ustring object, TypeDesc type, ustring name, std::optional index, void *value ) +{ + const RenderState *renderState = sg ? static_cast( sg->renderstate ) : nullptr; + if( !renderState ) + { + return false; + } + + // TODO - might be nice if there was some way to speed this up by directly querying the type matching + // the TypeDesc, instead of getting as a generic Data? + DataPtr data = renderState->context->getAsData( name.c_str(), nullptr ); + if( !data ) + { + return false; + } + + if( derivatives ) + { + memset( (char*)value + type.size(), 0, 2 * type.size() ); + } + + IECoreImage::OpenImageIOAlgo::DataView dataView( data.get(), /* createUStrings = */ true ); + if( !dataView.data ) + { + if( auto b = runTimeCast( data.get() ) ) + { + // BoolData isn't supported by `DataView` because `OIIO::TypeDesc` doesn't + // have a boolean type. We could work around this in `DataView` by casting to + // `TypeDesc::UCHAR` (along with a `static_assert( sizeof( bool ) == 1`). But that + // wouldn't be round-trippable via `OpenImageIOAlgo::data()`, so it's not clear + // that it would be a good thing in general. Here we don't care about round + // tripping, so we simply perform a conversion ourselves. + const unsigned char c = b->readable(); + return ShadingSystem::convert_value( value, type, &c, TypeDesc::UCHAR ); + } + return false; + } + + int effectiveIndex = 0; + if( index ) + { + effectiveIndex = index.value() < 0 ? dataView.type.arraylen + index.value() : index.value(); + if( effectiveIndex >= dataView.type.arraylen ) + { + return false; + } + dataView.type.arraylen = 0; + } + + return ShadingSystem::convert_value( + value, + type, + (char *)dataView.data + dataView.type.size() * effectiveIndex, + dataView.type + ); +} + + /// \todo Share with OSLRenderer class RendererServices : public OSL::RendererServices @@ -135,47 +196,12 @@ class RendererServices : public OSL::RendererServices bool get_attribute( OSL::ShaderGlobals *sg, bool derivatives, ustring object, TypeDesc type, ustring name, void *value ) override { - const RenderState *renderState = sg ? static_cast( sg->renderstate ) : nullptr; - if( !renderState ) - { - return false; - } - - // TODO - might be nice if there was some way to speed this up by directly querying the type matching - // the TypeDesc, instead of getting as a generic Data? - const DataPtr data = renderState->context->getAsData( name.c_str(), nullptr ); - if( !data ) - { - return false; - } - - if( derivatives ) - { - memset( (char*)value + type.size(), 0, 2 * type.size() ); - } - - IECoreImage::OpenImageIOAlgo::DataView dataView( data.get(), /* createUStrings = */ true ); - if( !dataView.data ) - { - if( auto b = runTimeCast( data.get() ) ) - { - // BoolData isn't supported by `DataView` because `OIIO::TypeDesc` doesn't - // have a boolean type. We could work around this in `DataView` by casting to - // `TypeDesc::UCHAR` (along with a `static_assert( sizeof( bool ) == 1`). But that - // wouldn't be round-trippable via `OpenImageIOAlgo::data()`, so it's not clear - // that it would be a good thing in general. Here we don't care about round - // tripping, so we simply perform a conversion ourselves. - const unsigned char c = b->readable(); - return ShadingSystem::convert_value( value, type, &c, TypeDesc::UCHAR ); - } - return false; - } - return ShadingSystem::convert_value( value, type, dataView.data, dataView.type ); + return getAttributeInternal( sg, derivatives, object, type, name, std::nullopt, value ); } bool get_array_attribute( OSL::ShaderGlobals *sg, bool derivatives, ustring object, TypeDesc type, ustring name, int index, void *value ) override { - return false; + return getAttributeInternal( sg, derivatives, object, type, name, index, value ); } // OSL tries to populate shader parameter values per-object by calling this method.