diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8de8539a4c..7482cd07b6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,45 +30,55 @@ jobs: # and then use `include` to define their settings. name: [ - linux-python3, - linux-python3-debug, - windows-python3, - windows-python3-debug + linux-gcc9, + linux-debug-gcc9, + linux-gcc11, + windows, + windows-debug ] include: - - name: linux-python3 + - name: linux-gcc9 os: ubuntu-20.04 buildType: RELEASE - containerImage: ghcr.io/gafferhq/build/build:2.0.0 + containerImage: ghcr.io/gafferhq/build/build:2.1.2 options: .github/workflows/main/options.posix - dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/7.0.0/gafferDependencies-7.0.0-linux.tar.gz + dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-linux-gcc9.tar.gz tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB publish: true - - name: linux-python3-debug + - name: linux-debug-gcc9 os: ubuntu-20.04 buildType: DEBUG - containerImage: ghcr.io/gafferhq/build/build:2.0.0 + containerImage: ghcr.io/gafferhq/build/build:2.1.2 options: .github/workflows/main/options.posix - dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/7.0.0/gafferDependencies-7.0.0-linux.tar.gz + dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-linux-gcc9.tar.gz tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB publish: false - - name: windows-python3 + - name: linux-gcc11 + os: ubuntu-20.04 + buildType: RELEASE + containerImage: ghcr.io/gafferhq/build/build:3.0.0 + options: .github/workflows/main/options.posix + dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-linux-gcc11.tar.gz + tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB + publish: true + + - name: windows os: windows-2019 buildType: RELEASE options: .github/workflows/main/options.windows - dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/7.0.0/gafferDependencies-7.0.0-windows.zip + dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-windows.zip tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB publish: true - - name: windows-python3-debug + - name: windows-debug os: windows-2019 buildType: RELWITHDEBINFO options: .github/workflows/main/options.windows - dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/7.0.0/gafferDependencies-7.0.0-windows.zip + dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-windows.zip tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB publish: false @@ -76,11 +86,17 @@ jobs: container: ${{ matrix.containerImage }} + env: + # GitHub have moved to running actions on Node20, which prevents them from + # running on CentOS 7. The below allows actions to continue running on Node16 + # until October. + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: ilammy/msvc-dev-cmd@v1.10.0 + - uses: ilammy/msvc-dev-cmd@v1.12.1 with: sdk: 10.0.17763.0 @@ -154,7 +170,7 @@ jobs: - name: Build run: | - scons -j 2 install BUILD_TYPE=${{ matrix.buildType }} OPTIONS=${{ matrix.options }} BUILD_CACHEDIR=sconsCache + scons -j 2 BUILD_TYPE=${{ matrix.buildType }} OPTIONS=${{ matrix.options }} BUILD_CACHEDIR=sconsCache # Copy the config log for use in the "Debug Failures" step, because it # gets clobbered by the `scons test*` call below. cp config.log buildConfig.log @@ -168,10 +184,11 @@ jobs: - name: Build Package run: | + scons install BUILD_TYPE=${{ matrix.buildType }} OPTIONS=${{ matrix.options }} BUILD_CACHEDIR=sconsCache ${{ env.PACKAGE_COMMAND }} ${{ env.CORTEX_BUILD_NAME }}.${{env.PACKAGE_EXTENSION}} ${{ env.CORTEX_BUILD_NAME }} if: matrix.publish - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: ${{ env.CORTEX_BUILD_NAME }} path: ${{ env.CORTEX_BUILD_NAME }}.${{ env.PACKAGE_EXTENSION }} diff --git a/.github/workflows/main/options.posix b/.github/workflows/main/options.posix index 4d9c760461..bb854c9e18 100644 --- a/.github/workflows/main/options.posix +++ b/.github/workflows/main/options.posix @@ -19,10 +19,7 @@ LIBPATH = libs PYTHON = deps + "/bin/python" -if os.path.exists( deps + "/bin/python3" ) : - pythonABIVersion = "3.7m" -else : - pythonABIVersion = "2.7" +pythonABIVersion = "3.10" PYTHON_LINK_FLAGS = "-lpython" + pythonABIVersion diff --git a/.gitignore b/.gitignore index 3fd5a4af2b..554f52231f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ *.lib *.ilk *.exp -.sconsign.dblite +.sconsign*.dblite .sconf_temp .cproject .project diff --git a/Changes b/Changes index cb0389ff8d..6a3527fc18 100644 --- a/Changes +++ b/Changes @@ -1,8 +1,84 @@ -10.5.x.x (relative to 10.5.6.2) +10.5.x.x (relative to 10.5.9.2) ======== + +10.5.9.2 (relative to 10.5.9.1) +======== + +Fixes +----- + +- USDScene : + - Fixed round-tripping of colons in set names. + - Fixed `hash()` to consider animation on ModelAPI extents when hashing the bound. +- ToMayaMeshConverter : Reverted #1386 that no longer locked normals set on the Mesh from the scc to fix issues with Maya incorrectly recomputing normals as Vertex normals when they were originally computed as Face normals. +- CurvesAlgo : Fixed `resamplePrimitiveVariables()` for use with periodic curves. + +10.5.9.1 (relative to 10.5.9.0) +======== + +Fixes +----- + +- OpenImageIOAlgo : Fixed `data()` handling of arrays of type `TypeDesc::CHAR`, `TypeDesc::UCHAR`, `TypeDesc::USHORT`, `TypeDesc::SHORT`, `TypeDesc::UINT` and `TypeDesc::HALF`. Among other things, this fixes the round-tripping of ICC profiles in ImageReader/ImageWriter. + +10.5.9.0 (relative to 10.5.8.0) +======== + +Improvements +------------ + +- USDScene : `hasBound()` and `readBound()` now use `UsdGeomModelAPI` extents hints if they are available. This behaviour can be disabled by setting the `IECOREUSD_USE_MODELAPI_BOUNDS` environment variable to a value of `0`. + +10.5.8.0 (relative to 10.5.7.1) +======== + +Improvements +------------ + +- IECoreUSD::DataAlgo : Added binding for `toUSD()` function. + +Fixes +----- + +- ShaderNetworkAlgo : Fixed crash caused by cyclic connections in `removeUnusedShaders()`. +- ShaderStateComponent : Fixed GL rendering failures caused by unsupported values for texture parameters. +- USDScene : + - Fixed exceptions caused by attempt to write shader parameters with unsupported value types. + - Fixed duplicate loading of `arnold:*` primvars on lights as attributes. These are now only loaded as parameters on the light shader. +- IECoreUSD::DataAlgo : Fixed exceptions thrown by `toUSD()` and `valueTypeName()` when passed datatypes not supported by `dispatch()`. An empty VtValue or SdfValueTypeName is now returned instead. + +Build +----- + +- CI : Updated to GafferHQ/dependencies 8.0.1. + +10.5.7.1 (relative to 10.5.7.0) +======== + +Fixes +----- + +- USDScene : + - Adding mapping of `arnold:*` light parameters to and from the non-standard `primvars:arnold:*` attributes preferred by the `arnold-usd` project. + - Fixed writing of `treatAsPoint` and `treatAsLine` light parameters, which were being written as generic `inputs:*` attributes and not the specific + attributes defined by the SphereLight and CylinderLight schemas. + +10.5.7.0 (relative to 10.5.6.2) +======== + +Features +-------- + +- MeshPrimitive : Added `interpolateBoundary`, `faceVaryingLinearInterpolation` and `triangleSubdivisionRule` properties for controlling the shape of the subdivision limit surface. + +Fixes +----- + +- MeshPrimitive : Removed `interpolation` from topologyHash. The topologyHash should only include things which affect the layout of primitive variables. + 10.5.6.2 (relative to 10.5.6.1) ======== diff --git a/SConstruct b/SConstruct index b4cee075f0..39f6ccbdaa 100644 --- a/SConstruct +++ b/SConstruct @@ -56,7 +56,7 @@ SConsignFile() ieCoreMilestoneVersion = 10 # for announcing major milestones - may contain all of the below ieCoreMajorVersion = 5 # backwards-incompatible changes -ieCoreMinorVersion = 6 # new backwards-compatible features +ieCoreMinorVersion = 9 # new backwards-compatible features ieCorePatchVersion = 2 # bug fixes ieCoreVersionSuffix = "" # used for alpha/beta releases. Example: "a1", "b2", etc. @@ -96,7 +96,7 @@ o.Add( o.Add( "CXXFLAGS", "The extra flags to pass to the C++ compiler during compilation.", - [ "-pipe", "-Wall", "-Wextra" ] if Environment()["PLATFORM"] != "win32" else [ "/permissive-", "/D_USE_MATH_DEFINES", "/Zc:externC-", "/DBOOST_ALL_NO_LIB" ], + [ "-pipe", "-Wall", "-Wextra", "-Wsuggest-override" ] if Environment()["PLATFORM"] != "win32" else [ "/permissive-", "/D_USE_MATH_DEFINES", "/Zc:externC-", "/DBOOST_ALL_NO_LIB" ], ) o.Add( @@ -1469,6 +1469,7 @@ if testEnv["PLATFORM"] == "darwin" : testEnv["ENV"][testEnv["TEST_LIBRARY_PATH_ENV_VAR"]] = os.pathsep.join( [ testEnv["ENV"].get(testEnv["TEST_LIBRARY_PATH_ENV_VAR"], ""), testEnvLibPath ] ) if testEnv["TEST_LIBRARY_PATH_ENV_VAR"] != libraryPathEnvVar : testEnv["ENV"][libraryPathEnvVar] = os.pathsep.join( [ testEnv["ENV"].get(libraryPathEnvVar, ""), testEnvLibPath ] ) +testEnv["ENV"]["IECORE_DLL_DIRECTORIES"] = testEnv["ENV"][libraryPathEnvVar] testEnv["ENV"]["IECORE_OP_PATHS"] = os.path.join( "test", "IECore", "ops" ) c = configureSharedLibrary( testEnv ) diff --git a/contrib/IECoreUSD/src/IECoreUSD/AttributeAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/AttributeAlgo.cpp index 1c368a198c..cb30723eef 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/AttributeAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/AttributeAlgo.cpp @@ -40,6 +40,8 @@ IECORE_PUSH_DEFAULT_VISIBILITY #include "pxr/usd/usdGeom/primvar.h" #include "pxr/usd/usdGeom/primvarsAPI.h" + +#include "pxr/usd/usdLux/lightAPI.h" IECORE_POP_DEFAULT_VISIBILITY using namespace IECoreUSD; @@ -92,6 +94,17 @@ bool IECoreUSD::AttributeAlgo::isCortexAttribute( const pxr::UsdGeomPrimvar &pri } } + // Check for `arnold:*` primvars on lights. These will be loaded as + // parameters in `ShaderAlgo::readLight()`. + + if( + boost::starts_with( primVar.GetPrimvarName().GetString(), "arnold:" ) && + pxr::UsdLuxLightAPI( primVar.GetAttr().GetPrim() ) + ) + { + return false; + } + // Everything else should be loaded as a Cortex attribute. return true; diff --git a/contrib/IECoreUSD/src/IECoreUSD/DataAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/DataAlgo.cpp index 701bb140e8..6b4eca217e 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/DataAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/DataAlgo.cpp @@ -400,7 +400,15 @@ struct VtValueFromData pxr::VtValue IECoreUSD::DataAlgo::toUSD( const IECore::Data *data, bool arrayRequired ) { - return IECore::dispatch( data, VtValueFromData(), arrayRequired ); + try + { + return IECore::dispatch( data, VtValueFromData(), arrayRequired ); + } + catch( const IECore::Exception & ) + { + // Type not supported by `dispatch()`. + return VtValue(); + } } pxr::TfToken IECoreUSD::DataAlgo::role( GeometricData::Interpretation interpretation ) @@ -503,5 +511,13 @@ struct VtValueTypeNameFromData pxr::SdfValueTypeName IECoreUSD::DataAlgo::valueTypeName( const IECore::Data *data ) { - return IECore::dispatch( data, VtValueTypeNameFromData() ); + try + { + return IECore::dispatch( data, VtValueTypeNameFromData() ); + } + catch( const IECore::Exception & ) + { + // Type not supported by `dispatch()`. + return SdfValueTypeName(); + } } diff --git a/contrib/IECoreUSD/src/IECoreUSD/MeshAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/MeshAlgo.cpp index 5703c51e5d..7953b782bf 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/MeshAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/MeshAlgo.cpp @@ -76,8 +76,30 @@ IECore::ObjectPtr readMesh( pxr::UsdGeomMesh &mesh, pxr::UsdTimeCode time, const if( subdivScheme == pxr::UsdGeomTokens->catmullClark ) { - newMesh->setInterpolation( "catmullClark" ); + newMesh->setInterpolation( IECoreScene::MeshPrimitive::interpolationCatmullClark.string() ); } + else if( subdivScheme == pxr::UsdGeomTokens->loop ) + { + newMesh->setInterpolation( IECoreScene::MeshPrimitive::interpolationLoop.string() ); + } + else + { + // For "none", we currently use the default value of "linear". It would probably be preferrable if + // we used the name "none", since this is different from "bilinear", which would indicate that + // subdivision is being requested, but without altering the shape of the limit surface. + } + + pxr::TfToken interpolateBoundary; + mesh.GetInterpolateBoundaryAttr().Get( &interpolateBoundary, time ); + newMesh->setInterpolateBoundary( interpolateBoundary.GetString() ); + + pxr::TfToken faceVaryingLinearInterpolation; + mesh.GetFaceVaryingLinearInterpolationAttr().Get( &faceVaryingLinearInterpolation, time ); + newMesh->setFaceVaryingLinearInterpolation( faceVaryingLinearInterpolation.GetString() ); + + pxr::TfToken triangleSubdivisionRule; + mesh.GetTriangleSubdivisionRuleAttr().Get( &triangleSubdivisionRule, time ); + newMesh->setTriangleSubdivisionRule( triangleSubdivisionRule.GetString() ); // Corners @@ -170,15 +192,23 @@ bool writeMesh( const IECoreScene::MeshPrimitive *mesh, const pxr::UsdStagePtr & // Interpolation - if( mesh->interpolation() == std::string( "catmullClark" ) ) + if( mesh->interpolation() == IECoreScene::MeshPrimitive::interpolationCatmullClark.string() ) { usdMesh.CreateSubdivisionSchemeAttr().Set( pxr::UsdGeomTokens->catmullClark ); } + else if( mesh->interpolation() == IECoreScene::MeshPrimitive::interpolationLoop.string() ) + { + usdMesh.CreateSubdivisionSchemeAttr().Set( pxr::UsdGeomTokens->loop ); + } else { usdMesh.CreateSubdivisionSchemeAttr().Set( pxr::UsdGeomTokens->none ); } + usdMesh.CreateInterpolateBoundaryAttr().Set( pxr::TfToken( mesh->getInterpolateBoundary().string() ), time ); + usdMesh.CreateFaceVaryingLinearInterpolationAttr().Set( pxr::TfToken( mesh->getFaceVaryingLinearInterpolation().string() ), time ); + usdMesh.CreateTriangleSubdivisionRuleAttr().Set( pxr::TfToken( mesh->getTriangleSubdivisionRule().string() ), time ); + // Corners if( mesh->cornerIds()->readable().size() ) diff --git a/contrib/IECoreUSD/src/IECoreUSD/ShaderAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/ShaderAlgo.cpp index a6e1c801d7..e80fa58ab8 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/ShaderAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/ShaderAlgo.cpp @@ -49,6 +49,8 @@ #include "pxr/usd/usd/schemaRegistry.h" #endif +#include "pxr/usd/usdGeom/primvarsAPI.h" + #include "boost/algorithm/string/predicate.hpp" #include "boost/algorithm/string/replace.hpp" #include "boost/pointer_cast.hpp" @@ -84,7 +86,43 @@ std::pair shaderIdAndType( const pxr::UsdShadeConnect return std::make_pair( id, type ); } -void readAdditionalLightParameters( const pxr::UsdPrim &prim, IECore::CompoundDataMap ¶meters ) +bool writeNonStandardLightParameter( const std::string &name, const IECore::Data *value, pxr::UsdShadeConnectableAPI usdShader ) +{ +#if PXR_VERSION >= 2111 + + if( auto sphereLight = pxr::UsdLuxSphereLight( usdShader.GetPrim() ) ) + { + if( name == "treatAsPoint" ) + { + sphereLight.GetTreatAsPointAttr().Set( IECoreUSD::DataAlgo::toUSD( value ) ); + return true; + } + } + else if( auto cylinderLight = pxr::UsdLuxCylinderLight( usdShader.GetPrim() ) ) + { + if( name == "treatAsLine" ) + { + cylinderLight.GetTreatAsLineAttr().Set( IECoreUSD::DataAlgo::toUSD( value ) ); + return true; + } + } + + if( pxr::UsdLuxLightAPI( usdShader.GetPrim() ) ) + { + if( boost::starts_with( name, "arnold:" ) ) + { + const pxr::SdfValueTypeName valueTypeName = IECoreUSD::DataAlgo::valueTypeName( value ); + pxr::UsdGeomPrimvar primVar = pxr::UsdGeomPrimvarsAPI( usdShader.GetPrim() ).CreatePrimvar( pxr::TfToken( name ), valueTypeName ); + primVar.Set( IECoreUSD::DataAlgo::toUSD( value ) ); + return true; + } + } + +#endif + return false; +} + +void readNonStandardLightParameters( const pxr::UsdPrim &prim, IECore::CompoundDataMap ¶meters ) { // Just to keep us on our toes, not all light parameters are stored as UsdShade inputs, // so we have special-case code for loading those here. @@ -101,6 +139,25 @@ void readAdditionalLightParameters( const pxr::UsdPrim &prim, IECore::CompoundDa cylinderLight.GetTreatAsLineAttr().Get( &treatAsLine ); parameters["treatAsLine"] = new IECore::BoolData( treatAsLine ); } + + if( auto light = pxr::UsdLuxLightAPI( prim ) ) + { + pxr::UsdGeomPrimvarsAPI primVarsAPI( prim ); + for( const auto &primVar : primVarsAPI.GetPrimvarsWithAuthoredValues() ) + { + pxr::TfToken name = primVar.GetPrimvarName(); + if( !boost::starts_with( name.GetString(), "arnold:" ) ) + { + continue; + } + + pxr::VtValue value; + if( primVar.Get( &value ) ) + { + parameters[name.GetString()] = IECoreUSD::DataAlgo::fromUSD( value, primVar.GetTypeName() ); + } + } + } #endif } @@ -189,7 +246,7 @@ IECore::InternedString readShaderNetworkWalk( const pxr::SdfPath &anchorPath, co } } - readAdditionalLightParameters( usdShader.GetPrim(), parameters ); + readNonStandardLightParameters( usdShader.GetPrim(), parameters ); IECoreScene::ShaderPtr newShader = new IECoreScene::Shader( shaderName, shaderType, parametersData ); pxr::VtValue metadataValue; @@ -256,14 +313,25 @@ void writeShaderParameterValues( const IECoreScene::Shader *shader, pxr::UsdShad { for( const auto &p : shader->parametersData()->readable() ) { + if( writeNonStandardLightParameter( p.first.string(), p.second.get(), usdShader ) ) + { + continue; + } + const pxr::TfToken usdParameterName = toUSDParameterName( p.first ); pxr::UsdShadeInput input = usdShader.GetInput( usdParameterName ); if( !input ) { - input = usdShader.CreateInput( - toUSDParameterName( p.first ), - IECoreUSD::DataAlgo::valueTypeName( p.second.get() ) - ); + pxr::SdfValueTypeName typeName = IECoreUSD::DataAlgo::valueTypeName( p.second.get() ); + if( !typeName ) + { + IECore::msg( IECore::Msg::Warning, "ShaderAlgo", + boost::format( "Shader parameter `%1%.%2%` has unsupported type `%3%`" ) + % shader->getName() % p.first % p.second->typeName() + ); + continue; + } + input = usdShader.CreateInput( toUSDParameterName( p.first ), typeName ); } if( auto *s = IECore::runTimeCast( p.second.get() ) ) { diff --git a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp index 4521870515..dbd1b624a3 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp @@ -58,6 +58,7 @@ IECORE_PUSH_DEFAULT_VISIBILITY #include "pxr/usd/usdGeom/camera.h" #include "pxr/usd/usdGeom/gprim.h" #include "pxr/usd/usdGeom/metrics.h" +#include "pxr/usd/usdGeom/modelAPI.h" #include "pxr/usd/usdGeom/pointInstancer.h" #include "pxr/usd/usdGeom/primvar.h" #include "pxr/usd/usdGeom/primvarsAPI.h" @@ -130,6 +131,22 @@ SceneInterface::Path fromUSDWithoutPrefix( const pxr::SdfPath &path, size_t pref return result; } +/// \todo USD 24.03 introduces support for UTF8 in prim and property names, +/// (while retaining the requirement that they mustn't start with a digit). But +/// `TfMakeValidIdentifier()` remains unchanged and still doesn't allow unicode +/// characters, so we will need to do something else when we update to 24.03. +/// +/// Note also : This is a one-way transformation, when we really want to be able to +/// round-trip names fully. This would be possible with this proposal : +/// +/// https://github.com/PixarAnimationStudios/OpenUSD-proposals/tree/main/proposals/transcoding_invalid_identifiers +/// +/// A similar proposal means that leading digits and medial hyphens wouldn't +/// need transcoding : +/// +/// https://github.com/NVIDIA-Omniverse/USD-proposals/tree/extended_unicode_identifiers/proposals/extended_unicode_identifiers +/// +/// Hopefully one or both of those come along to save us before too long. pxr::TfToken validName( const std::string &name ) { // `TfMakeValidIdentifier` _almost_ does what we want, but in Gaffer @@ -148,6 +165,37 @@ pxr::TfToken validName( const std::string &name ) } } +// As for `validName()`, but allowing ':' and ensuring that each token +// between ':' is also a valid name. This is necessary to meet the requirements +// of `SdfPath::IsValidNamespacedIdentifier()`. +pxr::TfToken validNamespacedName( const std::string &name ) +{ + std::string result; + + size_t index = 0, size = name.size(); + while( index < size ) + { + const size_t prevIndex = index; + index = name.find( ':', index ); + index = index == std::string::npos ? size : index; + if( result.size() ) + { + result += ":"; + } + result += validName( name.substr( prevIndex, index - prevIndex ) ); + index++; + } + + if( result.size() ) + { + return pxr::TfToken( result ); + } + else + { + return validName( name ); + } +} + template T *reportedCast( const IECore::RunTimeTyped *v, const char *context, const char *name ) { @@ -200,7 +248,7 @@ void writeSetInternal( const pxr::UsdPrim &prim, const pxr::TfToken &name, const continue; } pxr::UsdPrim childPrim = prim.GetStage()->DefinePrim( USDScene::toUSD( *it ) ); - writeSetInternal( childPrim, validName( name ), set.subTree( *it ) ); + writeSetInternal( childPrim, name, set.subTree( *it ) ); it.prune(); // Only visit children of root } return; @@ -214,11 +262,11 @@ void writeSetInternal( const pxr::UsdPrim &prim, const pxr::TfToken &name, const #if PXR_VERSION < 2009 - pxr::UsdCollectionAPI collection = pxr::UsdCollectionAPI::ApplyCollection( prim, validName( name ), pxr::UsdTokens->explicitOnly ); + pxr::UsdCollectionAPI collection = pxr::UsdCollectionAPI::ApplyCollection( prim, validNamespacedName( name ), pxr::UsdTokens->explicitOnly ); #else - pxr::UsdCollectionAPI collection = pxr::UsdCollectionAPI::Apply( prim, validName( name ) ); + pxr::UsdCollectionAPI collection = pxr::UsdCollectionAPI::Apply( prim, validNamespacedName( name ) ); collection.CreateExpansionRuleAttr( pxr::VtValue( pxr::UsdTokens->explicitOnly ) ); #endif @@ -528,6 +576,33 @@ Imath::M44d localTransform( const pxr::UsdPrim &prim, pxr::UsdTimeCode time ) return result; } +static const bool g_useModelAPIBounds = []() -> bool { + const char *c = getenv( "IECOREUSD_USE_MODELAPI_BOUNDS" ); + if( !c ) + { + return true; + } + return strcmp( c, "0" ); +}(); + +pxr::UsdAttribute boundAttribute( const pxr::UsdPrim &prim ) +{ + if( auto boundable = pxr::UsdGeomBoundable( prim ) ) + { + return boundable.GetExtentAttr(); + } + + if( g_useModelAPIBounds ) + { + if( auto modelAPI = pxr::UsdGeomModelAPI( prim ) ) + { + return modelAPI.GetExtentsHintAttr(); + } + } + + return pxr::UsdAttribute(); +} + // Used to assign a unique hash to each USD file. Using a global counter rather than the file name // means that we treat the same file as separate if it is closed and reopened. This means it's not // a problem if USD changes things when a file is reopened. USD appears to not in general guarantee @@ -820,30 +895,32 @@ std::string USDScene::fileName() const Imath::Box3d USDScene::readBound( double time ) const { - pxr::UsdGeomBoundable boundable = pxr::UsdGeomBoundable( m_location->prim ); - if( !boundable ) - { - return Imath::Box3d(); - } - - pxr::UsdAttribute attr = boundable.GetExtentAttr(); + pxr::UsdAttribute attr = boundAttribute( m_location->prim ); if( !attr.IsValid() ) { return Imath::Box3d(); } pxr::VtArray extents; - attr.Get >( &extents, m_root->getTime( time ) ); + attr.Get( &extents, m_root->getTime( time ) ); - if( extents.size() == 2 ) + // When coming from UsdGeomModelAPI, `extents` may contain several bounds, + // on a per-purpose basis. Take the union, since the SceneInterface API only + // has a single bound per location. + Imath::Box3d result; + for( size_t i = 0; i + 1 < extents.size(); i += 2 ) { - return Imath::Box3d( - DataAlgo::fromUSD( extents[0] ), - DataAlgo::fromUSD( extents[1] ) + const Imath::Box3d b( + DataAlgo::fromUSD( extents[i] ), + DataAlgo::fromUSD( extents[i+1] ) ); + if( !b.isEmpty() ) + { + result.extendBy( b ); + } } - return Imath::Box3d(); + return result; } ConstDataPtr USDScene::readTransform( double time ) const @@ -873,14 +950,7 @@ void USDScene::path( SceneInterface::Path &p ) const bool USDScene::hasBound() const { - pxr::UsdGeomBoundable boundable = pxr::UsdGeomBoundable( m_location->prim ); - pxr::UsdAttribute attr; - - if( boundable ) - { - attr = boundable.GetExtentAttr(); - } - + pxr::UsdAttribute attr = boundAttribute( m_location->prim ); return attr.IsValid(); } @@ -1520,11 +1590,11 @@ void USDScene::hash( SceneInterface::HashType hashType, double time, MurmurHash void USDScene::boundHash( double time, IECore::MurmurHash &h ) const { - if( pxr::UsdGeomBoundable boundable = pxr::UsdGeomBoundable( m_location->prim ) ) + if( auto attribute = boundAttribute( m_location->prim ) ) { h.append( m_root->uniqueId() ); appendPrimOrMasterPath( m_location->prim, h ); - if( boundable.GetExtentAttr().ValueMightBeTimeVarying() ) + if( attribute.ValueMightBeTimeVarying() ) { h.append( time ); } diff --git a/contrib/IECoreUSD/src/IECoreUSD/bindings/IEUSDModule.cpp b/contrib/IECoreUSD/src/IECoreUSD/bindings/IEUSDModule.cpp index d98cc17db8..168ed4a5d4 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/bindings/IEUSDModule.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/bindings/IEUSDModule.cpp @@ -96,6 +96,7 @@ BOOST_PYTHON_MODULE( _IECoreUSD ) scope dataAlgoModuleScope( dataAlgoModule ); def( "role", &DataAlgo::role ); + def( "toUSD", (pxr::VtValue (*)( const IECore::Data *, bool ))&DataAlgo::toUSD, ( arg( "data" ), arg( "arrayRequired" ) = false ) ); def( "valueTypeName", &DataAlgo::valueTypeName ); } diff --git a/contrib/IECoreUSD/test/IECoreUSD/DataAlgoTest.py b/contrib/IECoreUSD/test/IECoreUSD/DataAlgoTest.py index 9e5bcc7db0..f594cc9086 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/DataAlgoTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/DataAlgoTest.py @@ -35,70 +35,88 @@ import unittest import imath +import pxr.Sdf + import IECore import IECoreUSD class DataAlgoTest( unittest.TestCase ) : - def testRoleBinding( self ) : - - self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Point ), "Point" ) - self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Vector ), "Vector" ) - self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Normal ), "Normal" ) - self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.UV ), "TextureCoordinate" ) - self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Color ), "Color" ) - self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Rational ), "" ) + def testRoleBinding( self ) : + + self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Point ), "Point" ) + self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Vector ), "Vector" ) + self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Normal ), "Normal" ) + self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.UV ), "TextureCoordinate" ) + self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Color ), "Color" ) + self.assertEqual( IECoreUSD.DataAlgo.role( IECore.GeometricData.Interpretation.Rational ), "" ) + + def testValueTypeNameBinding( self ) : + + v3 = IECore.V3fData( imath.V3f( 0.0 ) ) + v2 = IECore.V2fData( imath.V2f( 0.0 ) ) + + v3.setInterpretation( IECore.GeometricData.Interpretation.Point ) + self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( v3 ).type.typeName, "GfVec3f" ) + + v3.setInterpretation( IECore.GeometricData.Interpretation.Vector ) + self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( v3 ).type.typeName, "GfVec3f" ) + + v3.setInterpretation( IECore.GeometricData.Interpretation.Normal ) + self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( v3 ).type.typeName, "GfVec3f" ) - def testValueTypeNameBinding( self ) : + v2.setInterpretation( IECore.GeometricData.Interpretation.UV ) + self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( v2 ).type.typeName, "GfVec2f" ) - v3 = IECore.V3fData( imath.V3f( 0.0 ) ) - v2 = IECore.V2fData( imath.V2f( 0.0 ) ) + self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( IECore.Color3fData( imath.Color3f( 0.0 ) ) ).type.typeName, "GfVec3f" ) - v3.setInterpretation( IECore.GeometricData.Interpretation.Point ) - self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( v3 ).type.typeName, "GfVec3f" ) + self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( IECore.PathMatcherData() ), pxr.Sdf.ValueTypeName() ) + self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( IECore.CompoundData() ), pxr.Sdf.ValueTypeName() ) - v3.setInterpretation( IECore.GeometricData.Interpretation.Vector ) - self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( v3 ).type.typeName, "GfVec3f" ) + def testToUSDBinding( self ) : - v3.setInterpretation( IECore.GeometricData.Interpretation.Normal ) - self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( v3 ).type.typeName, "GfVec3f" ) + # Note : On the C++ side we are converting to VtValue, but the + # bindings for that convert back to native Python types. - v2.setInterpretation( IECore.GeometricData.Interpretation.UV ) - self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( v2 ).type.typeName, "GfVec2f" ) + for data, value in [ + ( IECore.IntData( 10 ), 10 ), + ( IECore.FloatData( 2.5 ), 2.5 ), + ( IECore.IntVectorData( [ 1, 2, 3 ] ), [ 1, 2, 3 ] ), + ( IECore.PathMatcherData(), None ), + ( IECore.CompoundData(), None ), + ] : + self.assertEqual( IECoreUSD.DataAlgo.toUSD( data ), value ) - self.assertEqual( IECoreUSD.DataAlgo.valueTypeName( IECore.Color3fData( imath.Color3f( 0.0 ) ) ).type.typeName, "GfVec3f" ) + def testToFromInternalName( self ) : - def testToFromInternalName( self ) : + a = "a-name(that-is-bad)" - a = "a-name(that-is-bad)" + b = IECoreUSD.SceneCacheDataAlgo.toInternalName( a ) + self.assertEqual( b, "a_____name____that_____is_____bad___" ) - b = IECoreUSD.SceneCacheDataAlgo.toInternalName( a ) - self.assertEqual( b, "a_____name____that_____is_____bad___" ) + c = IECoreUSD.SceneCacheDataAlgo.fromInternalName( b ) + self.assertEqual( a , c ) - c = IECoreUSD.SceneCacheDataAlgo.fromInternalName( b ) - - self.assertEqual( a , c ) + def testToFromInternalPath( self ) : - def testToFromInternalPath( self ) : + a = ["a-path", "(that)", "(is)", "(bad)"] - a = ["a-path", "(that)", "(is)", "(bad)"] + b = IECoreUSD.SceneCacheDataAlgo.toInternalPath( a ) + self.assertEqual( + b, + [ + "__IECOREUSD_ROOT", + "a_____path", + "____that___", + "____is___", + "____bad___", + ] + ) - b = IECoreUSD.SceneCacheDataAlgo.toInternalPath( a ) - self.assertEqual( - b, - [ - "__IECOREUSD_ROOT", - "a_____path", - "____that___", - "____is___", - "____bad___", - ] - ) + c = IECoreUSD.SceneCacheDataAlgo.fromInternalPath( b ) - c = IECoreUSD.SceneCacheDataAlgo.fromInternalPath( b ) + self.assertEqual( a, c ) - self.assertEqual( a, c ) - if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py index 5bd4576222..11fe8a4abf 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py @@ -843,6 +843,96 @@ def testCanWriteSubD( self ): self.assertEqual(readChild.readObject( 0.0 ).interpolation, "catmullClark") + def testSubdOptions( self ) : + + fileName = os.path.join( self.temporaryDirectory(), "test.usda" ) + resaveFileName = os.path.join( self.temporaryDirectory(), "resave.usda" ) + + # We need a list of all the values from USD we should support. There probably should be a + # more direct way to get this, but I have already wasted far, far too much time trying to + # understand which USD API to use. + dummyStage = pxr.Usd.Stage.CreateInMemory() + dummyMesh = pxr.UsdGeom.Mesh.Define( dummyStage, "/mesh" ) + allowedSubScheme = dummyMesh.GetSubdivisionSchemeAttr().GetMetadata( "allowedTokens" ) + allowedIB = dummyMesh.GetInterpolateBoundaryAttr().GetMetadata( "allowedTokens" ) + allowedFVLI = dummyMesh.GetFaceVaryingLinearInterpolationAttr().GetMetadata( "allowedTokens" ) + allowedTS = dummyMesh.GetTriangleSubdivisionRuleAttr().GetMetadata( "allowedTokens" ) + + del dummyMesh + del dummyStage + + for property, allowed in [ + ( "subdivisionScheme", allowedSubScheme ), + ( "interpolateBoundary", allowedIB ), + ( "faceVaryingLinearInterpolation", allowedFVLI ), + ( "triangleSubdivisionRule", allowedTS ), + + ]: + for value in allowed: + + if property == "subdivisionScheme" and value == "bilinear": + # We know we don't support this + continue + + stage = pxr.Usd.Stage.CreateNew( fileName ) + mesh = pxr.UsdGeom.Mesh.Define( stage, "/mesh" ) + if property == "subdivisionScheme": + mesh.CreateSubdivisionSchemeAttr().Set( value ) + else: + mesh.CreateSubdivisionSchemeAttr().Set( "catmullClark" ) + + if property == "interpolateBoundary": + mesh.CreateInterpolateBoundaryAttr().Set( value, 0.0 ) + + if property == "faceVaryingLinearInterpolation": + mesh.CreateFaceVaryingLinearInterpolationAttr().Set( value, 0.0 ) + + if property == "triangleSubdivisionRule": + mesh.CreateTriangleSubdivisionRuleAttr().Set( value, 0.0 ) + + stage.GetRootLayer().Save() + del stage + + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + + cortexMesh = root.child( "mesh" ).readObject( 0.0 ) + if property == "subdivisionScheme": + if value == "none": + self.assertEqual( cortexMesh.interpolation, "linear" ) + else: + self.assertEqual( cortexMesh.interpolation, value ) + elif property == "interpolateBoundary": + self.assertEqual( cortexMesh.getInterpolateBoundary(), value ) + elif property == "faceVaryingLinearInterpolation": + self.assertEqual( cortexMesh.getFaceVaryingLinearInterpolation(), value ) + elif property == "triangleSubdivisionRule": + self.assertEqual( cortexMesh.getTriangleSubdivisionRule(), value ) + + sceneWrite = IECoreScene.SceneInterface.create( resaveFileName, IECore.IndexedIO.OpenMode.Write ) + root = sceneWrite.createChild( "root" ) + child = root.createChild( "mesh" ) + + child.writeObject ( cortexMesh, 0.0 ) + + del child + del root + del sceneWrite + + rereadFile = pxr.Usd.Stage.Open( resaveFileName ) + rereadMesh = pxr.UsdGeom.Mesh.Get( rereadFile, "/root/mesh" ) + + if property == "subdivisionScheme": + self.assertEqual( rereadMesh.GetSubdivisionSchemeAttr().Get( 0.0 ), value ) + elif property == "interpolateBoundary": + self.assertEqual( rereadMesh.GetInterpolateBoundaryAttr().Get( 0.0 ), value ) + elif property == "faceVaryingLinearInterpolation": + self.assertEqual( rereadMesh.GetFaceVaryingLinearInterpolationAttr().Get( 0.0 ), value ) + elif property == "triangleSubdivisionRule": + self.assertEqual( rereadMesh.GetTriangleSubdivisionRuleAttr().Get( 0.0 ), value ) + + del rereadMesh + del rereadFile + def testCanWriteAnimatedPrimitiveVariable ( self ): fileName = os.path.join( self.temporaryDirectory(), "usd_animated_primvar.usda" ) @@ -3091,6 +3181,42 @@ def testShadersAcrossDifferentScopes( self ): ) ] ) + def testUnsupportedShaderParameterTypes( self ) : + + sourceNetwork = IECoreScene.ShaderNetwork( + shaders = { + "test" : IECoreScene.Shader( + "test", + parameters = { + "unsupported1" : IECore.CompoundData(), + "unsupported2" : IECore.PathMatcherData(), + "supported" : "abc" + } + ) + }, + output = "test" + ) + + fileName = os.path.join( self.temporaryDirectory(), "test.usda" ) + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write ) + + with IECore.CapturingMessageHandler() as mh : + root.createChild( "test" ).writeAttribute( "surface", sourceNetwork, 0 ) + + self.assertEqual( + { m.message for m in mh.messages }, + { + "Shader parameter `test.unsupported1` has unsupported type `CompoundData`", + "Shader parameter `test.unsupported2` has unsupported type `PathMatcherData`", + } + ) + + del root + + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + network = root.child( "test" ).readAttribute( "surface", 0 ) + self.assertEqual( network.outputShader().parameters, IECore.CompoundData( { "supported" : "abc" } ) ) + def testHoudiniVaryingLengthArrayPrimVar( self ) : root = IECoreScene.SceneInterface.create( @@ -4001,5 +4127,191 @@ def testRoundTripArnoldLight( self ) : self.assertIn( "__lights", root.setNames() ) self.assertEqual( root.readSet( "__lights" ), IECore.PathMatcher( [ "/light" ] ) ) + def testArnoldSpecificLightInputs( self ) : + + # The `arnold-usd` project doesn't represent Arnold-specific UsdLux + # extensions as `inputs:arnold:*` attributes as it logically should : + # instead it uses `primvars:arnold:*` attributes. In Cortex/Gaffer we + # wish to use regular `arnold:*` shader parameters rather than primvars, + # so must convert to and from the less logical form in USDScene. + + lightShader = IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( + "RectLight", "light", + parameters = { + "exposure" : 1.0, + "arnold:roundness" : 2.0, + } + ) + }, + output = "light", + ) + + fileName = os.path.join( self.temporaryDirectory(), "test.usda" ) + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write ) + light = root.createChild( "light" ) + light.writeAttribute( "light", lightShader, 0 ) + del root, light + + stage = pxr.Usd.Stage.Open( fileName ) + shadeAPI = pxr.UsdShade.ConnectableAPI( stage.GetPrimAtPath( "/light" ) ) + self.assertTrue( shadeAPI.GetInput( "exposure" ) ) + self.assertFalse( shadeAPI.GetInput( "arnold:roundness" ) ) + primvarsAPI = pxr.UsdGeom.PrimvarsAPI( stage.GetPrimAtPath( "/light" ) ) + self.assertTrue( primvarsAPI.HasPrimvar( "arnold:roundness" ) ) + self.assertEqual( primvarsAPI.GetPrimvar( "arnold:roundness" ).Get(), 2.0 ) + del stage, shadeAPI, primvarsAPI + + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + self.assertEqual( root.child( "light" ).readAttribute( "light", 0 ), lightShader ) + self.assertEqual( root.child( "light" ).attributeNames(), [ "light" ] ) + + def testTreatLightAsPointOrLine( self ) : + + # `treatAsPoint` and `treatAsLine` aren't defined as UsdShade inputs but we store + # them as regular shader parameter, so they need special handling when writing to USD. + + sphereLightShader = IECoreScene.ShaderNetwork( + shaders = { + "sphereLight" : IECoreScene.Shader( + "SphereLight", "light", + parameters = { + "treatAsPoint" : True, + } + ) + }, + output = "sphereLight", + ) + + cylinderLightShader = IECoreScene.ShaderNetwork( + shaders = { + "cylinderLight" : IECoreScene.Shader( + "CylinderLight", "light", + parameters = { + "treatAsLine" : True, + } + ) + }, + output = "cylinderLight", + ) + + fileName = os.path.join( self.temporaryDirectory(), "test.usda" ) + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write ) + root.createChild( "sphereLight" ).writeAttribute( "light", sphereLightShader, 0 ) + root.createChild( "cylinderLight" ).writeAttribute( "light", cylinderLightShader, 0 ) + del root + + stage = pxr.Usd.Stage.Open( fileName ) + self.assertEqual( pxr.UsdLux.SphereLight( stage.GetPrimAtPath( "/sphereLight" ) ).GetTreatAsPointAttr().Get(), True ) + self.assertEqual( pxr.UsdLux.CylinderLight( stage.GetPrimAtPath( "/cylinderLight" ) ).GetTreatAsLineAttr().Get(), True ) + del stage + + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + self.assertEqual( root.child( "sphereLight" ).readAttribute( "light", 0 ), sphereLightShader ) + self.assertEqual( root.child( "cylinderLight" ).readAttribute( "light", 0 ), cylinderLightShader ) + + def testModelBound( self ) : + + fileName = os.path.join( self.temporaryDirectory(), "modelBound.usda" ) + + stage = pxr.Usd.Stage.CreateNew( fileName ) + pxr.UsdGeom.Xform.Define( stage, "/withoutModelAPI" ) + pxr.UsdGeom.Xform.Define( stage, "/withModelAPI" ) + pxr.UsdGeom.Xform.Define( stage, "/withModelAPIAndExtent" ) + + pxr.UsdGeom.ModelAPI.Apply( stage.GetPrimAtPath( "/withModelAPI" ) ) + modelAPI = pxr.UsdGeom.ModelAPI.Apply( stage.GetPrimAtPath( "/withModelAPIAndExtent" ) ) + modelAPI.SetExtentsHint( [ ( 1, 2, 3 ), ( 4, 5, 6 ) ] ) + + stage.GetRootLayer().Save() + del stage + + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + self.assertFalse( root.hasBound() ) + + self.assertFalse( root.child( "withoutModelAPI" ).hasBound() ) + self.assertFalse( root.child( "withModelAPI" ).hasBound() ) + self.assertTrue( root.child( "withModelAPIAndExtent" ).hasBound() ) + self.assertEqual( root.child( "withModelAPIAndExtent" ).readBound( 0 ), imath.Box3d( imath.V3d( 1, 2, 3 ), imath.V3d( 4, 5, 6 ) ) ) + + def testAnimatedModelBound( self ) : + + fileName = os.path.join( self.temporaryDirectory(), "modelBound.usda" ) + + stage = pxr.Usd.Stage.CreateNew( fileName ) + pxr.UsdGeom.Xform.Define( stage, "/model" ) + + pxr.UsdGeom.ModelAPI.Apply( stage.GetPrimAtPath( "/model" ) ) + modelAPI = pxr.UsdGeom.ModelAPI.Apply( stage.GetPrimAtPath( "/model" ) ) + modelAPI.SetExtentsHint( [ ( 1, 2, 3 ), ( 4, 5, 6 ) ], 0 ) + modelAPI.SetExtentsHint( [ ( 2, 3, 4 ), ( 5, 6, 7 ) ], 24 ) + + stage.GetRootLayer().Save() + del stage + + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + self.assertTrue( root.child( "model" ).hasBound() ) + self.assertEqual( root.child( "model" ).readBound( 0 ), imath.Box3d( imath.V3d( 1, 2, 3 ), imath.V3d( 4, 5, 6 ) ) ) + self.assertEqual( root.child( "model" ).readBound( 1 ), imath.Box3d( imath.V3d( 2, 3, 4 ), imath.V3d( 5, 6, 7 ) ) ) + + self.assertNotEqual( + root.child( "model" ).hash( root.HashType.BoundHash, 0 ), + root.child( "model" ).hash( root.HashType.BoundHash, 1 ) + ) + + def testPerPurposeModelBound( self ) : + + fileName = os.path.join( self.temporaryDirectory(), "testPerPurposeModelBound.usda" ) + + stage = pxr.Usd.Stage.CreateNew( fileName ) + pxr.UsdGeom.Xform.Define( stage, "/group" ) + cube = pxr.UsdGeom.Cube.Define( stage, "/group/proxy" ) + cube.CreatePurposeAttr().Set( "proxy" ) + + bboxCache = pxr.UsdGeom.BBoxCache( pxr.Usd.TimeCode( 0 ), [ "default", "render", "proxy", "guide" ] ) + modelAPI = pxr.UsdGeom.ModelAPI.Apply( stage.GetPrimAtPath( "/group" ) ) + modelAPI.SetExtentsHint( modelAPI.ComputeExtentsHint( bboxCache ) ) + + stage.GetRootLayer().Save() + del stage + + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + self.assertFalse( root.hasBound() ) + + self.assertTrue( root.child( "group" ).hasBound() ) + self.assertEqual( root.child( "group" ).readBound( 0 ), imath.Box3d( imath.V3d( -1 ), imath.V3d( 1 ) ) ) + + def testSetNameValidation( self ) : + + fileName = os.path.join( self.temporaryDirectory(), "test.usda" ) + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write ) + + expectedSetNames = { + "a" : "a", + "foo" : "foo", + "foo:includes" : "foo:includes", + "render:test" : "render:test", + "render:test:foo" : "render:test:foo", + "1" : "_1", + "render:2": "render:_2", + "" : "_", + } + + for setIndex, setName in enumerate( expectedSetNames.keys() ) : + root.writeSet( setName, IECore.PathMatcher( [ f"/set{setIndex}Member" ] ) ) + + del root + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + + self.assertEqual( + set( root.setNames() ), + set( expectedSetNames.values() ) | { "__lights", "usd:pointInstancers", "__cameras" } + ) + + for setIndex, setName in enumerate( expectedSetNames.values() ) : + with self.subTest( setName = setName ) : + self.assertEqual( root.readSet( setName ), IECore.PathMatcher( [ f"/set{setIndex}Member" ] ) ) + if __name__ == "__main__": unittest.main() diff --git a/include/IECore/EuclideanToSphericalTransform.h b/include/IECore/EuclideanToSphericalTransform.h index 575c235011..25ccf9bf9f 100644 --- a/include/IECore/EuclideanToSphericalTransform.h +++ b/include/IECore/EuclideanToSphericalTransform.h @@ -62,7 +62,7 @@ class EuclideanToSphericalTransform : public SpaceTransform< F, T > EuclideanToSphericalTransform(); /// Perform the conversion. - virtual T transform( const F &f ); + T transform( const F &f ) override; /// Returns an instance of a class able to perform the inverse conversion InverseType inverse() const; diff --git a/include/IECore/SphericalToEuclideanTransform.h b/include/IECore/SphericalToEuclideanTransform.h index 5de0c0ac60..bb11487909 100644 --- a/include/IECore/SphericalToEuclideanTransform.h +++ b/include/IECore/SphericalToEuclideanTransform.h @@ -62,7 +62,7 @@ class SphericalToEuclideanTransform : public SpaceTransform< F, T > SphericalToEuclideanTransform(); /// Perform the conversion. The x component should be in the range [0,2*M_PI] and the second [0,M_PI] - virtual T transform( const F &f ); + T transform( const F &f ) override; /// Returns an instance of a class able to perform the inverse conversion InverseType inverse() const; diff --git a/include/IECorePython/IECoreBinding.h b/include/IECorePython/IECoreBinding.h index a45803a16c..eb3a4265f0 100644 --- a/include/IECorePython/IECoreBinding.h +++ b/include/IECorePython/IECoreBinding.h @@ -79,9 +79,9 @@ IECOREPYTHON_API std::string repr( T &x ); /// For backwards compatibility with older versions of boost, /// which don't provide boost::python::len -IECOREPYTHON_API inline ssize_t len( const boost::python::object &obj ) +IECOREPYTHON_API inline Py_ssize_t len( const boost::python::object &obj ) { - ssize_t result = PyObject_Length( obj.ptr() ); + Py_ssize_t result = PyObject_Length( obj.ptr() ); if( PyErr_Occurred() ) { diff --git a/include/IECoreScene/MeshPrimitive.h b/include/IECoreScene/MeshPrimitive.h index 7a0aee1803..649b724e91 100644 --- a/include/IECoreScene/MeshPrimitive.h +++ b/include/IECoreScene/MeshPrimitive.h @@ -38,6 +38,7 @@ #include "IECoreScene/Export.h" #include "IECoreScene/Primitive.h" +#include "IECore/InternedString.h" #include "IECore/VectorTypedData.h" namespace IECoreScene @@ -61,12 +62,30 @@ class IECORESCENE_API MeshPrimitive : public Primitive IE_CORE_DECLAREEXTENSIONOBJECT( MeshPrimitive, MeshPrimitiveTypeId, Primitive ); + //! @name Define supported interpolations + /// \todo : In the future, we hope to use InternedStrings whenever we get/set + /// interpolations. + /// \todo : The meaning of "linear" has ended up being somewhat misaligned to + /// what we actually want. The ideal would probably be if "linear" was instead + /// named "none" - indicating that no subdivision is requested, and there was + /// a new value "bilinear", which indicated that the limit surface is simple + /// polygons, but subdivision is still being requested. + ///////////////////////////////////////////////////////////////////////////// + //@{ + static const IECore::InternedString interpolationLinear; + static const IECore::InternedString interpolationCatmullClark; + static const IECore::InternedString interpolationLoop; + //@} + /// Construct a MeshPrimitive with no faces. MeshPrimitive(); /// Construct a MeshPrimitive. The number of faces specified by verticesPerFace->readable()->size(). /// Copies of the IntVectorData objects are taken rather than references to the initial data. MeshPrimitive( IECore::ConstIntVectorDataPtr verticesPerFace, IECore::ConstIntVectorDataPtr vertexIds, - const std::string &interpolation = "linear", IECore::V3fVectorDataPtr p = nullptr ); + const std::string &interpolation = interpolationLinear.string(), IECore::V3fVectorDataPtr p = nullptr ); + + /// Destructor + ~MeshPrimitive() override; //! @name Topology access /// These functions allow access to get and set topology after construction. @@ -102,6 +121,35 @@ class IECORESCENE_API MeshPrimitive : public Primitive void removeCreases(); //@} + //! @name Subdivision options + /// These parameters control various details that affect the shape of the limit surface + ///////////////////////////////////////////////////////////////////////////// + //@{ + + const IECore::InternedString &getInterpolateBoundary() const; + void setInterpolateBoundary( const IECore::InternedString &interpolateBoundary ); + + static const IECore::InternedString interpolateBoundaryNone; + static const IECore::InternedString interpolateBoundaryEdgeOnly; + static const IECore::InternedString interpolateBoundaryEdgeAndCorner; + + const IECore::InternedString &getFaceVaryingLinearInterpolation() const; + void setFaceVaryingLinearInterpolation( const IECore::InternedString &faceVaryingLinearInterpolation ); + + static const IECore::InternedString faceVaryingLinearInterpolationNone; + static const IECore::InternedString faceVaryingLinearInterpolationCornersOnly; + static const IECore::InternedString faceVaryingLinearInterpolationCornersPlus1; + static const IECore::InternedString faceVaryingLinearInterpolationCornersPlus2; + static const IECore::InternedString faceVaryingLinearInterpolationBoundaries; + static const IECore::InternedString faceVaryingLinearInterpolationAll; + + const IECore::InternedString &getTriangleSubdivisionRule() const; + void setTriangleSubdivisionRule( const IECore::InternedString &triangleSubdivisionRule ); + + static const IECore::InternedString triangleSubdivisionRuleCatmullClark; + static const IECore::InternedString triangleSubdivisionRuleSmooth; + //@} + size_t variableSize( PrimitiveVariable::Interpolation interpolation ) const override; /// Render the mesh diff --git a/python/IECore/__init__.py b/python/IECore/__init__.py index c5a30a59c4..414f2df083 100644 --- a/python/IECore/__init__.py +++ b/python/IECore/__init__.py @@ -38,7 +38,7 @@ # # Some parts of the IECore library are defined purely in Python. These are shown below. -import os, sys, ctypes +import os, sys, ctypes, pathlib if os.name == "posix" and os.environ.get( "IECORE_RTLD_GLOBAL", "1" ) == "1" : # Historically, we had problems with cross-module RTTI on Linux, whereby # different Python modules and/or libraries could end up with their own @@ -56,8 +56,15 @@ sys.getdlopenflags() | ctypes.RTLD_GLOBAL ) +if hasattr( os, "add_dll_directory" ) and "IECORE_DLL_DIRECTORIES" in os.environ : + for directory in os.environ.get( "IECORE_DLL_DIRECTORIES" ).split( os.pathsep ) : + directory = pathlib.Path( directory ).resolve() + if directory.is_dir() : + os.add_dll_directory( directory ) + del directory + # Remove pollution of IECore namespace -del os, sys, ctypes +del os, sys, ctypes, pathlib __import__( "imath" ) diff --git a/src/IECoreGL/ShaderStateComponent.cpp b/src/IECoreGL/ShaderStateComponent.cpp index ec014bc406..0fc5f7912b 100644 --- a/src/IECoreGL/ShaderStateComponent.cpp +++ b/src/IECoreGL/ShaderStateComponent.cpp @@ -139,7 +139,14 @@ class ShaderStateComponent::Implementation : public IECore::RefCounted it->second->typeId() == IECore::SplinefColor3fData::staticTypeId() ) { - texture = IECore::runTimeCast( CachedConverter::defaultCachedConverter()->convert( it->second.get() ) ); + try + { + texture = IECore::runTimeCast( CachedConverter::defaultCachedConverter()->convert( it->second.get() ) ); + } + catch( const std::exception &e ) + { + IECore::msg( IECore::Msg::Error, "ShaderStateComponent", e.what() ); + } } else if( it->second->typeId() == IECore::StringData::staticTypeId() ) { diff --git a/src/IECoreImage/OpenImageIOAlgo.cpp b/src/IECoreImage/OpenImageIOAlgo.cpp index c41831b97e..db023f8f3d 100644 --- a/src/IECoreImage/OpenImageIOAlgo.cpp +++ b/src/IECoreImage/OpenImageIOAlgo.cpp @@ -83,6 +83,24 @@ OIIO::TypeDesc extractStringCharPointers( const std::vector &strings, std::ve return type; } +template +DataPtr extractScalarData( const OIIO::ParamValue &value ) +{ + const T *data = static_cast( value.data() ); + if( value.type().arraylen == 0 ) + { + return new TypedData( *data ); + } + else if( value.type().arraylen > 0 ) + { + using DataType = TypedData>; + typename DataType::Ptr result = new DataType; + result->writable().insert( result->writable().end(), data, data + value.type().arraylen ); + return result; + } + return nullptr; +} + } // namespace namespace IECoreImage @@ -566,7 +584,7 @@ IECore::DataPtr data( const OIIO::ParamValue &value ) { if ( type.aggregate == TypeDesc::SCALAR ) { - return new CharData( *static_cast( value.data() ) ); + return extractScalarData( value ); } return nullptr; } @@ -574,7 +592,7 @@ IECore::DataPtr data( const OIIO::ParamValue &value ) { if ( type.aggregate == TypeDesc::SCALAR ) { - return new UCharData( *static_cast( value.data() ) ); + return extractScalarData( value ); } return nullptr; } @@ -606,7 +624,7 @@ IECore::DataPtr data( const OIIO::ParamValue &value ) { if ( type.aggregate == TypeDesc::SCALAR ) { - return new UShortData( *static_cast( value.data() ) ); + return extractScalarData( value ); } return nullptr; } @@ -614,7 +632,7 @@ IECore::DataPtr data( const OIIO::ParamValue &value ) { if ( type.aggregate == TypeDesc::SCALAR ) { - return new ShortData( *static_cast( value.data() ) ); + return extractScalarData( value ); } return nullptr; } @@ -622,14 +640,14 @@ IECore::DataPtr data( const OIIO::ParamValue &value ) { if ( type.aggregate == TypeDesc::SCALAR ) { - const unsigned *typedData = static_cast( value.data() ); if ( type.vecsemantics == TypeDesc::TIMECODE ) { + const unsigned *typedData = static_cast( value.data() ); return new TimeCodeData( Imf::TimeCode( typedData[0], typedData[1] ) ); } else { - return new UIntData( *typedData ); + return extractScalarData( value ); } } return nullptr; @@ -641,17 +659,7 @@ IECore::DataPtr data( const OIIO::ParamValue &value ) { case TypeDesc::SCALAR : { - if( !type.arraylen ) - { - return new IntData( *typedData ); - } - else - { - IntVectorDataPtr vectorData = new IntVectorData(); - vectorData->writable().resize( type.arraylen ); - std::copy( &typedData[0], &typedData[type.arraylen], vectorData->writable().begin() ); - return vectorData; - } + return extractScalarData( value ); } case TypeDesc::VEC2 : { @@ -696,7 +704,7 @@ IECore::DataPtr data( const OIIO::ParamValue &value ) { if ( type.aggregate == TypeDesc::SCALAR ) { - return new HalfData( *static_cast( value.data() ) ); + return extractScalarData( value ); } return nullptr; } @@ -707,17 +715,7 @@ IECore::DataPtr data( const OIIO::ParamValue &value ) { case TypeDesc::SCALAR : { - if( !type.arraylen ) - { - return new FloatData( *typedData ); - } - else - { - FloatVectorDataPtr vectorData = new FloatVectorData(); - vectorData->writable().resize( type.arraylen ); - std::copy( &typedData[0], &typedData[type.arraylen], vectorData->writable().begin() ); - return vectorData; - } + return extractScalarData( value ); } case TypeDesc::VEC2 : { @@ -764,17 +762,7 @@ IECore::DataPtr data( const OIIO::ParamValue &value ) { case TypeDesc::SCALAR : { - if( !type.arraylen ) - { - return new DoubleData( *typedData ); - } - else - { - DoubleVectorDataPtr vectorData = new DoubleVectorData(); - vectorData->writable().resize( type.arraylen ); - std::copy( &typedData[0], &typedData[type.arraylen], vectorData->writable().begin() ); - return vectorData; - } + return extractScalarData( value ); } case TypeDesc::VEC2 : { diff --git a/src/IECoreMaya/ToMayaMeshConverter.cpp b/src/IECoreMaya/ToMayaMeshConverter.cpp index cf31c75187..39fb938e4c 100644 --- a/src/IECoreMaya/ToMayaMeshConverter.cpp +++ b/src/IECoreMaya/ToMayaMeshConverter.cpp @@ -183,10 +183,6 @@ void ToMayaMeshConverter::addUVSet( MFnMesh &fnMesh, const MIntArray &polygonCou bool ToMayaMeshConverter::doConversion( IECore::ConstObjectPtr from, MObject &to, IECore::ConstCompoundObjectPtr operands ) const { - // Note: Normals are not set on the Mesh from the scc as by setting them - // explicitly we are implying they should be locked which is not - // supported, instead we rely on Maya computing the normals everytime - MStatus s; IECoreScene::ConstMeshPrimitivePtr mesh = IECore::runTimeCast( from ); @@ -267,6 +263,79 @@ bool ToMayaMeshConverter::doConversion( IECore::ConstObjectPtr from, MObject &to return false; } + it = mesh->variables.find("N"); + if ( it != mesh->variables.end() ) + { + if (it->second.interpolation == IECoreScene::PrimitiveVariable::FaceVarying ) + { + /// \todo Employ some M*Array converters to simplify this + MVectorArray vertexNormalsArray; + IECore::ConstV3fVectorDataPtr n = IECore::runTimeCast(it->second.data); + if (n) + { + IECoreScene::PrimitiveVariable::IndexedView normalView = IECoreScene::PrimitiveVariable::IndexedView( it->second ); + vertexNormalsArray.setLength( normalView.size() ); + + size_t i = 0; + for(const auto& normal : normalView) + { + vertexNormalsArray[i++] = IECore::convert( normal ); + } + } + else + { + IECore::ConstV3dVectorDataPtr n = IECore::runTimeCast(it->second.data); + if (n) + { + IECoreScene::PrimitiveVariable::IndexedView normalView = IECoreScene::PrimitiveVariable::IndexedView( it->second ); + vertexNormalsArray.setLength( normalView.size() ); + + size_t i = 0; + for(const auto& normal : normalView) + { + vertexNormalsArray[i++] = IECore::convert( normal ); + } + } + else + { + IECore::msg( IECore::Msg::Warning, "ToMayaMeshConverter::doConversion", boost::format( "PrimitiveVariable \"N\" has unsupported type \"%s\"." ) % it->second.data->typeName() ); + } + } + + if ( vertexNormalsArray.length() ) + { + MStatus status; + MItMeshPolygon itPolygon( mObj, &status ); + if( status != MS::kSuccess ) + { + IECore::msg( IECore::Msg::Warning, "ToMayaMeshConverter::doConversion", "Failed to create mesh iterator" ); + } + + unsigned v = 0; + MIntArray vertexIds; + MIntArray faceIds; + + for ( ; !itPolygon.isDone(); itPolygon.next() ) + { + for ( v=0; v < itPolygon.polygonVertexCount(); ++v ) + { + faceIds.append( itPolygon.index() ); + vertexIds.append( itPolygon.vertexIndex( v ) ); + } + } + + if( !fnMesh.setFaceVertexNormals( vertexNormalsArray, faceIds, vertexIds ) ) + { + IECore::msg( IECore::Msg::Warning, "ToMayaMeshConverter::doConversion", "Setting normals failed" ); + } + } + } + else + { + IECore::msg( IECore::Msg::Warning, "ToMayaMeshConverter::doConversion", "PrimitiveVariable \"N\" has unsupported interpolation (expected FaceVarying)." ); + } + } + /// Add UV sets for ( it = mesh->variables.begin(); it != mesh->variables.end(); ++it ) { diff --git a/src/IECorePython/StringAlgoBinding.cpp b/src/IECorePython/StringAlgoBinding.cpp index 84fcaccd17..e56984250c 100644 --- a/src/IECorePython/StringAlgoBinding.cpp +++ b/src/IECorePython/StringAlgoBinding.cpp @@ -73,12 +73,12 @@ list matchPatternPath( const std::string &path, char separator ) struct VariableProviderWrapper : StringAlgo::VariableProvider, wrapper { - virtual int frame() const + int frame() const override { return this->get_override( "frame" )(); } - virtual const std::string &variable( const boost::string_view &name, bool &recurse ) const + const std::string &variable( const boost::string_view &name, bool &recurse ) const override { object result = this->get_override( "variable" )( std::string( name ) ); extract tupleExtractor( result ); diff --git a/src/IECoreScene/CurvesAlgo.cpp b/src/IECoreScene/CurvesAlgo.cpp index 225c9cd175..e0cb51b43c 100644 --- a/src/IECoreScene/CurvesAlgo.cpp +++ b/src/IECoreScene/CurvesAlgo.cpp @@ -200,7 +200,8 @@ struct CurvesUniformToVarying for( size_t i = 0; i < numCurves; ++i, ++srcIt ) { Canceller::check( m_canceller ); - for( size_t j = 0; j < m_curves->numSegments( i ) + 1; ++j ) + const size_t varyingSize = m_curves->variableSize( PrimitiveVariable::Varying, i ); + for( size_t j = 0; j < varyingSize; ++j ) { trg.push_back( *srcIt ); } @@ -241,7 +242,7 @@ struct CurvesVaryingToUniform typename From::ValueType::value_type total = *srcIt; ++srcIt; - size_t varyingSize = m_curves->numSegments( i ) + 1; + const size_t varyingSize = m_curves->variableSize( PrimitiveVariable::Varying, i ); for( size_t j = 1; j < varyingSize; ++j, ++srcIt ) { total += *srcIt; @@ -300,7 +301,9 @@ struct CurvesVertexToVarying Canceller::check( m_canceller ); size_t numSegments = m_curves->numSegments( i ); float step = 1.0f / numSegments; - for( size_t j = 0; j < numSegments + 1; ++j ) + + const size_t varyingSize = m_curves->variableSize( PrimitiveVariable::Varying, i ); + for( size_t j = 0; j < varyingSize; ++j ) { evaluator->pointAtV( i, j * step, evaluatorResult.get() ); trg.push_back( evalPrimVar( evaluatorResult.get(), *primVar ) ); diff --git a/src/IECoreScene/MeshPrimitive.cpp b/src/IECoreScene/MeshPrimitive.cpp index 2a25afe837..7825b617a9 100644 --- a/src/IECoreScene/MeshPrimitive.cpp +++ b/src/IECoreScene/MeshPrimitive.cpp @@ -37,10 +37,13 @@ #include "IECoreScene/PolygonIterator.h" #include "IECoreScene/Renderer.h" +#include "IECore/ClassData.h" #include "IECore/MurmurHash.h" #include "boost/format.hpp" +#include "tbb/spin_rw_mutex.h" + #include #include @@ -62,6 +65,9 @@ IndexedIO::EntryID g_cornerSharpnessesEntry("cornerSharpnesses"); IndexedIO::EntryID g_creaseLengthsEntry("creaseLengths"); IndexedIO::EntryID g_creaseIdsEntry("creaseIds"); IndexedIO::EntryID g_creaseSharpnessesEntry("creaseSharpnesses"); +IndexedIO::EntryID g_interpolateBoundaryEntry("interpolateBoundary"); +IndexedIO::EntryID g_faceVaryingLinearInterpolationEntry("faceVaryingLinearInterpolation"); +IndexedIO::EntryID g_triangleSubdivisionRuleEntry("triangleSubdivisionRule"); const IntVectorData *emptyIntVectorData() { @@ -75,14 +81,50 @@ const FloatVectorData *emptyFloatVectorData() return g_d.get(); } +// \todo : Replace these with actual class members once we can break binary compatibility +struct MeshClassData +{ + IECore::InternedString interpolateBoundary; + IECore::InternedString faceVaryingLinearInterpolation; + IECore::InternedString triangleSubdivisionRule; +}; + +// We intentionally don't clean up this static memory at shutdown, because we can't destruct it until +// every mesh has been destructed, and other static globals ( like the ObjectPool ) might be holding onto meshes. +// There's no real problem with leaking memory when the process is shutting down anyway. +static IECore::ClassData< MeshPrimitive, MeshClassData > *g_classData = new IECore::ClassData< MeshPrimitive, MeshClassData >(); + +// This lock must be held in read mode to read or write properties of individual class data entries, +// and held in write mode to add or remove class data entries +static tbb::spin_rw_mutex *g_classDataMutex = new tbb::spin_rw_mutex(); + } // namespace +const IECore::InternedString MeshPrimitive::interpolationLinear( "linear" ); +const IECore::InternedString MeshPrimitive::interpolationCatmullClark( "catmullClark" ); +const IECore::InternedString MeshPrimitive::interpolationLoop( "loop" ); +const IECore::InternedString MeshPrimitive::interpolateBoundaryNone( "none" ); +const IECore::InternedString MeshPrimitive::interpolateBoundaryEdgeOnly( "edgeOnly" ); +const IECore::InternedString MeshPrimitive::interpolateBoundaryEdgeAndCorner( "edgeAndCorner" ); +const IECore::InternedString MeshPrimitive::faceVaryingLinearInterpolationNone( "none" ); +const IECore::InternedString MeshPrimitive::faceVaryingLinearInterpolationCornersOnly( "cornersOnly" ); +const IECore::InternedString MeshPrimitive::faceVaryingLinearInterpolationCornersPlus1( "cornersPlus1" ); +const IECore::InternedString MeshPrimitive::faceVaryingLinearInterpolationCornersPlus2( "cornersPlus2" ); +const IECore::InternedString MeshPrimitive::faceVaryingLinearInterpolationBoundaries( "boundaries" ); +const IECore::InternedString MeshPrimitive::faceVaryingLinearInterpolationAll( "all" ); +const IECore::InternedString MeshPrimitive::triangleSubdivisionRuleCatmullClark( "catmullClark" ); +const IECore::InternedString MeshPrimitive::triangleSubdivisionRuleSmooth( "smooth" ); + const unsigned int MeshPrimitive::m_ioVersion = 0; IE_CORE_DEFINEOBJECTTYPEDESCRIPTION(MeshPrimitive); MeshPrimitive::MeshPrimitive() : m_verticesPerFace( new IntVectorData ), m_vertexIds( new IntVectorData ), m_numVertices( 0 ), m_interpolation( "linear" ), m_minVerticesPerFace( 0 ), m_maxVerticesPerFace( 0 ) { + { + tbb::spin_rw_mutex::scoped_lock lock( *g_classDataMutex, true ); + g_classData->create( this, { interpolateBoundaryEdgeAndCorner, faceVaryingLinearInterpolationCornersPlus1, triangleSubdivisionRuleCatmullClark } ); + } removeCorners(); removeCreases(); } @@ -90,6 +132,10 @@ MeshPrimitive::MeshPrimitive() MeshPrimitive::MeshPrimitive( ConstIntVectorDataPtr verticesPerFace, ConstIntVectorDataPtr vertexIds, const std::string &interpolation, V3fVectorDataPtr p ) { + { + tbb::spin_rw_mutex::scoped_lock lock( *g_classDataMutex, true ); + g_classData->create( this, { interpolateBoundaryEdgeAndCorner, faceVaryingLinearInterpolationCornersPlus1, triangleSubdivisionRuleCatmullClark } ); + } setTopology( verticesPerFace, vertexIds, interpolation ); if( p ) { @@ -99,6 +145,12 @@ MeshPrimitive::MeshPrimitive( ConstIntVectorDataPtr verticesPerFace, ConstIntVec } } +MeshPrimitive::~MeshPrimitive() +{ + tbb::spin_rw_mutex::scoped_lock lock( *g_classDataMutex, true ); + g_classData->erase( this ); +} + size_t MeshPrimitive::numFaces() const { return m_verticesPerFace->readable().size(); @@ -343,6 +395,42 @@ void MeshPrimitive::removeCreases() m_creaseSharpnesses = emptyFloatVectorData(); } +const IECore::InternedString &MeshPrimitive::getInterpolateBoundary() const +{ + tbb::spin_rw_mutex::scoped_lock lock( *g_classDataMutex, false ); + return (*g_classData)[ this ].interpolateBoundary; +} + +void MeshPrimitive::setInterpolateBoundary( const IECore::InternedString &interpolateBoundary ) +{ + tbb::spin_rw_mutex::scoped_lock lock( *g_classDataMutex, false ); + (*g_classData)[ this ].interpolateBoundary = interpolateBoundary; +} + +const IECore::InternedString &MeshPrimitive::getFaceVaryingLinearInterpolation() const +{ + tbb::spin_rw_mutex::scoped_lock lock( *g_classDataMutex, false ); + return (*g_classData)[ this ].faceVaryingLinearInterpolation; +} + +void MeshPrimitive::setFaceVaryingLinearInterpolation( const IECore::InternedString &faceVaryingLinearInterpolation ) +{ + tbb::spin_rw_mutex::scoped_lock lock( *g_classDataMutex, false ); + (*g_classData)[ this ].faceVaryingLinearInterpolation = faceVaryingLinearInterpolation; +} + +const IECore::InternedString &MeshPrimitive::getTriangleSubdivisionRule() const +{ + tbb::spin_rw_mutex::scoped_lock lock( *g_classDataMutex, false ); + return (*g_classData)[ this ].triangleSubdivisionRule; +} + +void MeshPrimitive::setTriangleSubdivisionRule( const IECore::InternedString &triangleSubdivisionRule ) +{ + tbb::spin_rw_mutex::scoped_lock lock( *g_classDataMutex, false ); + (*g_classData)[ this ].triangleSubdivisionRule = triangleSubdivisionRule; +} + size_t MeshPrimitive::variableSize( PrimitiveVariable::Interpolation interpolation ) const { switch(interpolation) @@ -388,6 +476,9 @@ void MeshPrimitive::copyFrom( const Object *other, IECore::Object::CopyContext * m_creaseLengths = tOther->m_creaseLengths; m_creaseIds = tOther->m_creaseIds; m_creaseSharpnesses = tOther->m_creaseSharpnesses; + setInterpolateBoundary( tOther->getInterpolateBoundary() ); + setFaceVaryingLinearInterpolation( tOther->getFaceVaryingLinearInterpolation() ); + setTriangleSubdivisionRule( tOther->getTriangleSubdivisionRule() ); } void MeshPrimitive::save( IECore::Object::SaveContext *context ) const @@ -416,6 +507,10 @@ void MeshPrimitive::save( IECore::Object::SaveContext *context ) const context->save( m_creaseIds.get(), container.get(), g_creaseIdsEntry ); context->save( m_creaseSharpnesses.get(), container.get(), g_creaseSharpnessesEntry ); } + + container->write( g_interpolateBoundaryEntry, getInterpolateBoundary().string() ); + container->write( g_faceVaryingLinearInterpolationEntry, getFaceVaryingLinearInterpolation().string() ); + container->write( g_triangleSubdivisionRuleEntry, getTriangleSubdivisionRule().string() ); } void MeshPrimitive::load( IECore::Object::LoadContextPtr context ) @@ -454,6 +549,23 @@ void MeshPrimitive::load( IECore::Object::LoadContextPtr context ) { removeCreases(); } + + std::string interpolateBoundary, faceVaryingLinearInterpolation, triangleSubdivisionRule; + if( container->hasEntry( g_interpolateBoundaryEntry ) ) + { + container->read( g_interpolateBoundaryEntry, interpolateBoundary ); + setInterpolateBoundary( interpolateBoundary ); + } + if( container->hasEntry( g_faceVaryingLinearInterpolationEntry ) ) + { + container->read( g_faceVaryingLinearInterpolationEntry, faceVaryingLinearInterpolation ); + setFaceVaryingLinearInterpolation( faceVaryingLinearInterpolation ); + } + if( container->hasEntry( g_triangleSubdivisionRuleEntry ) ) + { + container->read( g_triangleSubdivisionRuleEntry, triangleSubdivisionRule ); + setTriangleSubdivisionRule( triangleSubdivisionRule ); + } } bool MeshPrimitive::isEqualTo( const Object *other ) const @@ -501,6 +613,18 @@ bool MeshPrimitive::isEqualTo( const Object *other ) const { return false; } + if( getInterpolateBoundary() != tOther->getInterpolateBoundary() ) + { + return false; + } + if( getFaceVaryingLinearInterpolation() != tOther->getFaceVaryingLinearInterpolation() ) + { + return false; + } + if( getTriangleSubdivisionRule() != tOther->getTriangleSubdivisionRule() ) + { + return false; + } return true; } @@ -525,13 +649,16 @@ void MeshPrimitive::hash( MurmurHash &h ) const m_creaseLengths->hash( h ); m_creaseIds->hash( h ); m_creaseSharpnesses->hash( h ); + h.append( m_interpolation ); + h.append( getInterpolateBoundary() ); + h.append( getFaceVaryingLinearInterpolation() ); + h.append( getTriangleSubdivisionRule() ); } void MeshPrimitive::topologyHash( MurmurHash &h ) const { m_verticesPerFace->hash( h ); m_vertexIds->hash( h ); - h.append( m_interpolation ); } MeshPrimitivePtr MeshPrimitive::createBox( const Box3f &b ) @@ -677,7 +804,7 @@ MeshPrimitivePtr MeshPrimitive::createPlane( const Box2f &b, const Imath::V2i &d nData->setInterpretation( GeometricData::Normal ); Canceller::check( canceller ); nData->writable().resize( p.size(), V3f( 0, 0, 1 ) ); - + result->variables["N"] = PrimitiveVariable( PrimitiveVariable::Vertex, nData ); return result; diff --git a/src/IECoreScene/ShaderNetworkAlgo.cpp b/src/IECoreScene/ShaderNetworkAlgo.cpp index 3402b7c252..578da65f2c 100644 --- a/src/IECoreScene/ShaderNetworkAlgo.cpp +++ b/src/IECoreScene/ShaderNetworkAlgo.cpp @@ -96,10 +96,12 @@ namespace void visitInputs( const ShaderNetwork *network, InternedString handle, std::unordered_set &visited ) { - visited.insert( handle ); - for( const auto &c : network->inputConnections( handle ) ) + if( visited.insert( handle ).second ) { - visitInputs( network, c.source.shader, visited ); + for( const auto &c : network->inputConnections( handle ) ) + { + visitInputs( network, c.source.shader, visited ); + } } } diff --git a/src/IECoreScene/bindings/MeshPrimitiveBinding.cpp b/src/IECoreScene/bindings/MeshPrimitiveBinding.cpp index b863a87429..0903dccae2 100644 --- a/src/IECoreScene/bindings/MeshPrimitiveBinding.cpp +++ b/src/IECoreScene/bindings/MeshPrimitiveBinding.cpp @@ -95,6 +95,21 @@ MeshPrimitivePtr createSphereWrapper( float radius, float zMin, float zMax, floa return MeshPrimitive::createSphere( radius, zMin, zMax, thetaMax, divisions, canceller ); } +InternedString getInterpolateBoundaryWrapper( const MeshPrimitive &p ) +{ + return p.getInterpolateBoundary(); +} + +InternedString getFaceVaryingLinearInterpolationWrapper( const MeshPrimitive &p ) +{ + return p.getFaceVaryingLinearInterpolation(); +} + +InternedString getTriangleSubdivisionRuleWrapper( const MeshPrimitive &p ) +{ + return p.getTriangleSubdivisionRule(); +} + } // namespace void IECoreSceneModule::bindMeshPrimitive() @@ -122,6 +137,30 @@ void IECoreSceneModule::bindMeshPrimitive() .def( "creaseIds", &creaseIds ) .def( "creaseSharpnesses", &creaseSharpnesses ) .def( "removeCreases", &MeshPrimitive::removeCreases ) + + // Note these are bound as functions, not properties - this is inconsistent with how interpolation is + // bound, but we hope to switch interpolation at some point, see todo above. + .def( "getInterpolateBoundary", &getInterpolateBoundaryWrapper ) + .def( "setInterpolateBoundary", &MeshPrimitive::setInterpolateBoundary ) + .def( "getFaceVaryingLinearInterpolation", &getFaceVaryingLinearInterpolationWrapper ) + .def( "setFaceVaryingLinearInterpolation", &MeshPrimitive::setFaceVaryingLinearInterpolation ) + .def( "getTriangleSubdivisionRule", &getTriangleSubdivisionRuleWrapper ) + .def( "setTriangleSubdivisionRule", &MeshPrimitive::setTriangleSubdivisionRule ) + .def_readonly( "interpolationLinear", MeshPrimitive::interpolationLinear ) + .def_readonly( "interpolationCatmullClark", MeshPrimitive::interpolationCatmullClark ) + .def_readonly( "interpolationLoop", MeshPrimitive::interpolationLoop ) + .def_readonly( "interpolateBoundaryNone", MeshPrimitive::interpolateBoundaryNone ) + .def_readonly( "interpolateBoundaryEdgeOnly", MeshPrimitive::interpolateBoundaryEdgeOnly ) + .def_readonly( "interpolateBoundaryEdgeAndCorner", MeshPrimitive::interpolateBoundaryEdgeAndCorner ) + .def_readonly( "faceVaryingLinearInterpolationNone", MeshPrimitive::faceVaryingLinearInterpolationNone ) + .def_readonly( "faceVaryingLinearInterpolationCornersOnly", MeshPrimitive::faceVaryingLinearInterpolationCornersOnly ) + .def_readonly( "faceVaryingLinearInterpolationCornersPlus1", MeshPrimitive::faceVaryingLinearInterpolationCornersPlus1 ) + .def_readonly( "faceVaryingLinearInterpolationCornersPlus2", MeshPrimitive::faceVaryingLinearInterpolationCornersPlus2 ) + .def_readonly( "faceVaryingLinearInterpolationBoundaries", MeshPrimitive::faceVaryingLinearInterpolationBoundaries ) + .def_readonly( "faceVaryingLinearInterpolationAll", MeshPrimitive::faceVaryingLinearInterpolationAll ) + .def_readonly( "triangleSubdivisionRuleCatmullClark", MeshPrimitive::triangleSubdivisionRuleCatmullClark ) + .def_readonly( "triangleSubdivisionRuleSmooth", MeshPrimitive::triangleSubdivisionRuleSmooth ) + .def( "createBox", &MeshPrimitive::createBox, ( arg_( "bounds" ) ) ).staticmethod( "createBox" ) .def( "createPlane", &createPlaneWrapper, ( arg_( "bounds" ), arg_( "divisions" ) = Imath::V2i( 1 ), arg( "canceller" ) = object() ) ).staticmethod( "createPlane" ) .def( "createSphere", &createSphereWrapper, ( arg_( "radius" ), arg_( "zMin" ) = -1.0f, arg_( "zMax" ) = 1.0f, arg_( "thetaMax" ) = 360.0f, arg_( "divisions" ) = Imath::V2i( 20, 40 ), arg( "canceller" ) = object() ) ).staticmethod( "createSphere" ) diff --git a/test/IECoreImage/ImageReaderTest.py b/test/IECoreImage/ImageReaderTest.py index 8f83acf2fb..199d780397 100644 --- a/test/IECoreImage/ImageReaderTest.py +++ b/test/IECoreImage/ImageReaderTest.py @@ -212,7 +212,7 @@ def testOrientation( self ) : def testIncompleteImage( self ) : r = IECoreImage.ImageReader( os.path.join( "test", "IECoreImage", "data", "exr", "incomplete.exr" ) ) - self.assertRaisesRegex( Exception, "Error reading pixel data from image file", r.read ) + self.assertRaisesRegex( Exception, "Error reading pixel data from image file|.*Some scanline chunks were missing or corrupted", r.read ) def testHeaderToBlindData( self ) : diff --git a/test/IECoreImage/ImageThinnerTest.py b/test/IECoreImage/ImageThinnerTest.py index 9eef5441e1..b9d283b437 100644 --- a/test/IECoreImage/ImageThinnerTest.py +++ b/test/IECoreImage/ImageThinnerTest.py @@ -58,7 +58,7 @@ def test( self ) : iic = ii[c] for j in range( 0, i.channelSize() ) : # the values may not match exactly due to color space conversions reading the tif on disk - self.assertAlmostEqual( ic[j], iic[j], 6 ) + self.assertAlmostEqual( ic[j], iic[j], delta = 0.0001 ) if __name__ == "__main__": unittest.main() diff --git a/test/IECoreImage/data/exr/primitives.exr b/test/IECoreImage/data/exr/primitives.exr index bdd223a8d7..b097ad0ed8 100644 Binary files a/test/IECoreImage/data/exr/primitives.exr and b/test/IECoreImage/data/exr/primitives.exr differ diff --git a/test/IECoreMaya/ParameterisedHolder.py b/test/IECoreMaya/ParameterisedHolder.py index 7fd5b4066e..c40c4579b8 100644 --- a/test/IECoreMaya/ParameterisedHolder.py +++ b/test/IECoreMaya/ParameterisedHolder.py @@ -286,6 +286,7 @@ def testMeshParameterIOProblem( self ) : op = fnOP.getOp() mesh = IECoreScene.MeshPrimitive.createBox( imath.Box3f( imath.V3f( -2, -2, -2 ), imath.V3f( 2, 3, 4 ) ) ) + mesh[ "N" ] = IECoreScene.PrimitiveVariable( mesh[ "N" ].interpolation, mesh[ "N" ].expandedData() ) op.parameters()[ "input" ].setValue( mesh ) fnOP.setNodeValues() @@ -300,13 +301,7 @@ def testMeshParameterIOProblem( self ) : op = fnOP.getOp() mesh2 = op.parameters()["input"].getValue() - self.assertTrue( mesh2.arePrimitiveVariablesValid() ) - # The ToMayaMeshConverter relies on Maya to calculate the normals - # whereas createBox uses indexed normals so we cannot include them - # in the comparison otherwise they will never be the same - del mesh[ "N" ] - del mesh2[ "N" ] self.assertEqual( mesh2, mesh ) def testOpHolder( self ) : diff --git a/test/IECoreMaya/ToMayaMeshConverterTest.py b/test/IECoreMaya/ToMayaMeshConverterTest.py index cfb813b217..245b1f189c 100644 --- a/test/IECoreMaya/ToMayaMeshConverterTest.py +++ b/test/IECoreMaya/ToMayaMeshConverterTest.py @@ -308,9 +308,6 @@ def testNormals( self ) : self.assertAlmostEqual( origNormal[j], normal3f[j], 6 ) self.assertAlmostEqual( origNormal[j], normal3d[j], 6 ) - # normals should always be unlocked when reading from scc - self.assertFalse( any( maya.cmds.polyNormalPerVertex( newSphere+".vtx[*]", query=True, allLocked=True ) ) ) - def testSetMeshInterpolation( self ) : sphere = maya.cmds.polySphere( subdivisionsX=10, subdivisionsY=5, constructionHistory=False ) diff --git a/test/IECoreScene/CurvesAlgoTest.py b/test/IECoreScene/CurvesAlgoTest.py index 5ea99c8625..938fdb9881 100644 --- a/test/IECoreScene/CurvesAlgoTest.py +++ b/test/IECoreScene/CurvesAlgoTest.py @@ -470,6 +470,41 @@ def testBSplineCurvesIndexedFaceVaryingToVarying( self ) : self.assertEqual( p.interpolation, IECoreScene.PrimitiveVariable.Interpolation.Varying ) self.assertEqual( p.data, IECore.FloatVectorData( range( 0, 3 ) ) ) self.assertEqual( p.indices, IECore.IntVectorData( [ 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2 ] ) ) + + def testBSplineCurvesPeriodic( self ) : + # Periodic curves are a corner case that we don't test much, but we should at least make sure that + # we produce valid primvars + + curves = self.curvesBSpline() + for q in [ "d", "e", "h", "i", "varying_Color_V3f", "facevarying_Normal_V3f" ]: + del curves[q] + + curves.setTopology( curves.verticesPerCurve(), curves.basis(), True ) + + p = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, curves["c"].data ) + IECoreScene.CurvesAlgo.resamplePrimitiveVariable(curves, p, IECoreScene.PrimitiveVariable.Interpolation.Varying ) + + self.assertTrue( curves.isPrimitiveVariableValid( p ) ) + + IECoreScene.CurvesAlgo.resamplePrimitiveVariable(curves, p, IECoreScene.PrimitiveVariable.Interpolation.Uniform ) + self.assertEqual( curves["c"], p ) + + p = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.FloatVectorData( [ i for i in range( curves.variableSize( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) ) ] ) ) + + # Work around weird limitation that for resampling Varying -> Vertex the primvar must be stored on curves. + # Note that there may no longer be any reason for this limitation ( CurvesPrimitiveEvaluator looks like + # it should work fine with arbitrary prim vars ), but we're still doing something weird in + # CurvesVaryingToVertex in CurvesAlgo.cpp line 366 + curves["dummy"] = p + IECoreScene.CurvesAlgo.resamplePrimitiveVariable(curves, p, IECoreScene.PrimitiveVariable.Interpolation.Varying ) + + self.assertTrue( curves.isPrimitiveVariableValid( p ) ) + + curves["dummy"] = p + + IECoreScene.CurvesAlgo.resamplePrimitiveVariable(curves, p, IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + self.assertTrue( curves.isPrimitiveVariableValid( p ) ) + # endregion # region catmullrom @@ -930,6 +965,56 @@ def testLinearCurvesFaceVaryingToVarying( self ) : self.assertEqual( p.interpolation, IECoreScene.PrimitiveVariable.Interpolation.Varying ) self.assertEqual( p.data, IECore.FloatVectorData( range( 0, 4 ) ) ) + def testLinearCurvesPeriodic( self ) : + # Periodic curves are a corner case that we don't test much, but we should at least make sure that + # we produce valid primvars + + curves = IECoreScene.CurvesPrimitive( + + IECore.IntVectorData( [ 3, 3 ] ), + IECore.CubicBasisf.linear(), + True, + IECore.V3fVectorData( + [ + imath.V3f( 0, 0, 0 ), + imath.V3f( 0, 1, 0 ), + imath.V3f( 0, 0, 1 ), + imath.V3f( 0, 0, 0 ), + imath.V3f( 1, 0, 0 ), + imath.V3f( 0, 0, 1 ) + ] + ) + ) + + curves["c"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.FloatVectorData( range( 0, 2 ) ) ) + + p = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, curves["c"].data ) + IECoreScene.CurvesAlgo.resamplePrimitiveVariable(curves, p, IECoreScene.PrimitiveVariable.Interpolation.Varying ) + + self.assertTrue( curves.isPrimitiveVariableValid( p ) ) + + IECoreScene.CurvesAlgo.resamplePrimitiveVariable(curves, p, IECoreScene.PrimitiveVariable.Interpolation.Uniform ) + self.assertEqual( curves["c"], p ) + + origP = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.FloatVectorData( [ i for i in range( curves.variableSize( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) ) ] ) ) + p = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, origP.data ) + + # Work around weird limitation that for resampling Varying -> Vertex the primvar must be stored on curves + curves["dummy"] = p + IECoreScene.CurvesAlgo.resamplePrimitiveVariable(curves, p, IECoreScene.PrimitiveVariable.Interpolation.Varying ) + + self.assertTrue( curves.isPrimitiveVariableValid( p ) ) + + # Linear curves are a special case where the data shouldn't actually change when resampling from Vertex + # to Varying + self.assertEqual( p.data, origP.data ) + + curves["dummy"] = p + + IECoreScene.CurvesAlgo.resamplePrimitiveVariable(curves, p, IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + self.assertTrue( curves.isPrimitiveVariableValid( p ) ) + self.assertEqual( p.data, origP.data ) + def testCanSegmentUsingIntegerPrimvar( self ) : curves = self.curvesLinear() diff --git a/test/IECoreScene/MeshPrimitive.py b/test/IECoreScene/MeshPrimitive.py index 7317d4bec7..7ffd78aca1 100644 --- a/test/IECoreScene/MeshPrimitive.py +++ b/test/IECoreScene/MeshPrimitive.py @@ -53,6 +53,9 @@ def test( self ) : self.assertEqual( m.verticesPerFace, IECore.IntVectorData() ) self.assertEqual( m.vertexIds, IECore.IntVectorData() ) self.assertEqual( m.interpolation, "linear" ) + self.assertEqual( m.getInterpolateBoundary(), IECoreScene.MeshPrimitive.interpolateBoundaryEdgeAndCorner ) + self.assertEqual( m.getFaceVaryingLinearInterpolation(), IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus1 ) + self.assertEqual( m.getTriangleSubdivisionRule(), IECoreScene.MeshPrimitive.triangleSubdivisionRuleCatmullClark ) self.assertEqual( m, m.copy() ) self.assertEqual( m.maxVerticesPerFace(), 0 ) @@ -176,8 +179,12 @@ def testSetInterpolation( self ) : m = IECoreScene.MeshPrimitive() self.assertEqual( m.interpolation, "linear" ) + + hashBefore = m.hash() + m.interpolation = "catmullClark" self.assertEqual( m.interpolation, "catmullClark" ) + self.assertNotEqual( m.hash(), hashBefore ) def testEmptyMeshConstructor( self ) : @@ -227,7 +234,7 @@ def testHash( self ) : m.setTopology( IECore.IntVectorData( [ 3 ] ), IECore.IntVectorData( [ 0, 2, 1 ] ), "catmullClark" ) self.assertNotEqual( m.hash(), h ) - self.assertNotEqual( m.topologyHash(), t ) + self.assertEqual( m.topologyHash(), t ) h = m.hash() t = m.topologyHash() @@ -529,6 +536,65 @@ def testSaveAndLoadCorners( self ) : m2 = IECore.Object.load( io, "test" ) self.assertEqual( m, m2 ) + def testSubdivOptions( self ) : + self.assertEqual( IECoreScene.MeshPrimitive.interpolateBoundaryNone, "none" ) + self.assertEqual( IECoreScene.MeshPrimitive.interpolateBoundaryEdgeOnly, "edgeOnly" ) + self.assertEqual( IECoreScene.MeshPrimitive.interpolateBoundaryEdgeAndCorner, "edgeAndCorner" ) + + self.assertEqual( IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationNone, "none" ) + self.assertEqual( IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersOnly, "cornersOnly" ) + self.assertEqual( IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus1, "cornersPlus1" ) + self.assertEqual( IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus2, "cornersPlus2" ) + self.assertEqual( IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationBoundaries, "boundaries" ) + self.assertEqual( IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationAll, "all" ) + + self.assertEqual( IECoreScene.MeshPrimitive.triangleSubdivisionRuleCatmullClark, "catmullClark" ) + self.assertEqual( IECoreScene.MeshPrimitive.triangleSubdivisionRuleSmooth, "smooth" ) + + default = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 1 ) ) ) + m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 1 ) ) ) + self.assertEqual( m.getInterpolateBoundary(), IECoreScene.MeshPrimitive.interpolateBoundaryEdgeAndCorner ) + self.assertEqual( m.getFaceVaryingLinearInterpolation(), IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus1 ) + self.assertEqual( m.getTriangleSubdivisionRule(), IECoreScene.MeshPrimitive.triangleSubdivisionRuleCatmullClark ) + self.assertEqual( m, default ) + self.assertEqual( m.hash(), default.hash() ) + + m.setInterpolateBoundary( IECoreScene.MeshPrimitive.interpolateBoundaryEdgeOnly ) + self.assertNotEqual( m, default ) + self.assertNotEqual( m.hash(), default.hash() ) + self.assertEqual( m.getInterpolateBoundary(), IECoreScene.MeshPrimitive.interpolateBoundaryEdgeOnly ) + m.setInterpolateBoundary( IECoreScene.MeshPrimitive.interpolateBoundaryEdgeAndCorner ) + self.assertEqual( m, default ) + self.assertEqual( m.hash(), default.hash() ) + m.setFaceVaryingLinearInterpolation( IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus2 ) + self.assertEqual( m.getFaceVaryingLinearInterpolation(), IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus2 ) + self.assertNotEqual( m, default ) + self.assertNotEqual( m.hash(), default.hash() ) + m.setFaceVaryingLinearInterpolation( IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus1 ) + self.assertEqual( m, default ) + self.assertEqual( m.hash(), default.hash() ) + m.setTriangleSubdivisionRule( IECoreScene.MeshPrimitive.triangleSubdivisionRuleSmooth ) + self.assertEqual( m.getTriangleSubdivisionRule(), IECoreScene.MeshPrimitive.triangleSubdivisionRuleSmooth ) + self.assertNotEqual( m, default ) + self.assertNotEqual( m.hash(), default.hash() ) + + m.setInterpolateBoundary( IECoreScene.MeshPrimitive.interpolateBoundaryEdgeOnly ) + m.setFaceVaryingLinearInterpolation( IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus2 ) + + mCopy = m.copy() + self.assertEqual( mCopy.getInterpolateBoundary(), IECoreScene.MeshPrimitive.interpolateBoundaryEdgeOnly ) + self.assertEqual( mCopy.getFaceVaryingLinearInterpolation(), IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus2 ) + self.assertEqual( m, mCopy ) + + io = IECore.MemoryIndexedIO( IECore.CharVectorData(), [], IECore.IndexedIO.OpenMode.Append ) + + m.save( io, "test" ) + m2 = IECore.Object.load( io, "test" ) + self.assertEqual( m2.getInterpolateBoundary(), IECoreScene.MeshPrimitive.interpolateBoundaryEdgeOnly ) + self.assertEqual( m2.getFaceVaryingLinearInterpolation(), IECoreScene.MeshPrimitive.faceVaryingLinearInterpolationCornersPlus2 ) + self.assertEqual( m.getTriangleSubdivisionRule(), IECoreScene.MeshPrimitive.triangleSubdivisionRuleSmooth ) + self.assertEqual( m, m2 ) + def tearDown( self ) : for f in ( diff --git a/test/IECoreScene/ShaderNetworkAlgoTest.py b/test/IECoreScene/ShaderNetworkAlgoTest.py index 05daa89a95..92ac2bfae7 100644 --- a/test/IECoreScene/ShaderNetworkAlgoTest.py +++ b/test/IECoreScene/ShaderNetworkAlgoTest.py @@ -85,7 +85,7 @@ def testAddShaders( self ) : def testRemoveUnusedShaders( self ) : - n = IECoreScene.ShaderNetwork( + source = IECoreScene.ShaderNetwork( shaders = { "used1" : IECoreScene.Shader(), "used2" : IECoreScene.Shader(), @@ -103,6 +103,13 @@ def testRemoveUnusedShaders( self ) : output = ( "used3", "" ), ) + n = source.copy() + IECoreScene.ShaderNetworkAlgo.removeUnusedShaders( n ) + self.assertEqual( set( n.shaders().keys() ), { "used1", "used2", "used3" } ) + + # Test a network with a cycle - this is invalid, but we don't want it to crash + n = source.copy() + n.addConnection( ( ( "used3", "out" ), ( "used2", "in2" ) ) ) IECoreScene.ShaderNetworkAlgo.removeUnusedShaders( n ) self.assertEqual( set( n.shaders().keys() ), { "used1", "used2", "used3" } )