From 26b00252fcffb468f651576946fa2b729a9fadab Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Tue, 13 Oct 2026 13:42:49 -0400 Subject: [PATCH 1/2] IECoreUSD : Read all Gaussian splat coefficients Previously we were only reading the number of coefficients that `radiance:sphericalHarmonicsDegree` indicated we should get. In order to preserve data, we now load all of the coefficients possible. --- Changes | 3 ++ .../src/IECoreUSD/GaussianSplatAlgo.cpp | 52 +++++++++++++------ .../IECoreUSD/test/IECoreUSD/USDSceneTest.py | 4 +- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Changes b/Changes index a9d7212301..377d6e0675 100644 --- a/Changes +++ b/Changes @@ -1,7 +1,10 @@ 10.7.x.x (relative to 10.7.3.1) ======== +Improvements +------------ +- USD : All `radiance:sphericalHarmonicsCoefficients` are now loaded for `ParticleField3DGaussianSplat` objects, regardless of the `radiance:sphericalHarmonicsDegree`. 10.7.3.1 (relative to 10.7.3.0) ======== diff --git a/contrib/IECoreUSD/src/IECoreUSD/GaussianSplatAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/GaussianSplatAlgo.cpp index 1b95ea6d5a..4a87997879 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/GaussianSplatAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/GaussianSplatAlgo.cpp @@ -113,33 +113,53 @@ IECore::ObjectPtr readGaussianSplat( pxr::UsdVolParticleField3DGaussianSplat &sp Canceller::check( canceller ); pxr::UsdAttribute degreeAttr = splats.GetRadianceSphericalHarmonicsDegreeAttr(); - auto degreeData = boost::static_pointer_cast( DataAlgo::fromUSD( degreeAttr, time, false ) ); - newPoints->variables["radiance:sphericalHarmonicsDegree"] = PrimitiveVariable( PrimitiveVariable::Interpolation::Constant, degreeData ); - - const int degree = degreeData->readable(); - const int coefficientCount = ( degree + 1 ) * ( degree + 1 ); + PrimitiveAlgo::readPrimitiveVariable( degreeAttr, time, newPoints.get(), "radiance:sphericalHarmonicsDegree", PrimitiveVariable::Interpolation::Constant ); pxr::UsdAttribute coefficientsAttr; splats.UsesFloatRadianceCoefficients( &coefficientsAttr ); - auto allCoefficientsData = runTimeCast( DataAlgo::fromUSD( coefficientsAttr, time ) ); - if( allCoefficientsData && allCoefficientsData->readable().size() >= coefficientCount * newPoints->getNumPoints() ) + if( auto allCoefficientsData = runTimeCast( DataAlgo::fromUSD( coefficientsAttr, time ) ) ) { const std::vector &allCoefficients = allCoefficientsData->readable(); - for( int i = 0; i < coefficientCount; ++i ) + if( allCoefficients.size() % newPoints->getNumPoints() == 0 ) { - Canceller::check( canceller ); - - V3fVectorDataPtr coefficientsData = new V3fVectorData(); - std::vector &coefficients = coefficientsData->writable(); - coefficients.resize( newPoints->getNumPoints() ); - for( size_t j = 0, eJ = newPoints->getNumPoints(); j < eJ; ++j ) + const int coefficientsCount = allCoefficients.size() / newPoints->getNumPoints(); + int degree; + degreeAttr.Get( °ree, time ); + if( coefficientsCount != ( degree + 1 ) * ( degree + 1 ) ) { - coefficients[j] = allCoefficients[j * coefficientCount + i]; + IECore::msg( + IECore::Msg::Warning, + "GaussianSplatAlgo::readGaussianSplat", + "Attribute \"radiance:sphericalHarmonicsCoefficients\" size does not match that expected by \"radiance:sphericalHarmonicsDegree\" at location \"{}\".", + splats.GetPrim().GetPath().GetAsString() + ); } - newPoints->variables[fmt::format( "radiance:sphericalHarmonicsCoefficients:{}", i )] = PrimitiveVariable( PrimitiveVariable::Interpolation::Vertex, coefficientsData ); + for( int i = 0; i < coefficientsCount; ++i ) + { + Canceller::check( canceller ); + + V3fVectorDataPtr coefficientsData = new V3fVectorData(); + std::vector &coefficients = coefficientsData->writable(); + coefficients.resize( newPoints->getNumPoints() ); + for( size_t j = 0, eJ = newPoints->getNumPoints(); j < eJ; ++j ) + { + coefficients[j] = allCoefficients[j * coefficientsCount + i]; + } + + newPoints->variables[fmt::format( "radiance:sphericalHarmonicsCoefficients:{}", i )] = PrimitiveVariable( PrimitiveVariable::Interpolation::Vertex, coefficientsData ); + } + } + else + { + IECore::msg( + IECore::Msg::Warning, + "GaussianSplatAlgo::readGaussianSplat", + "Attribute \"radiance:sphericalHarmonicsCoefficients\" has invalid length at location \"{}\".", + splats.GetPrim().GetPath().GetAsString() + ); } } diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py index 1506d96fae..1e42a1f218 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py @@ -5167,7 +5167,9 @@ def testReadGaussianSplats( self ) : object = root.child( "splats" ) self.assertTrue( object.hasObject() ) - splats = object.readObject( 0.0 ) + with IECore.CapturingMessageHandler() as mh : + splats = object.readObject( 0.0 ) + self.assertEqual( len( mh.messages ), 0 ) self.assertTrue( isinstance( splats, IECoreScene.PointsPrimitive ) ) self.assertEqual( splats.numPoints, 4 ) From ca72db140d09a7dcd84b50884100cf66ef938e00 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 12 Oct 2026 12:35:16 -0400 Subject: [PATCH 2/2] IECoreUSD : Write Gaussian splats --- Changes | 4 +- .../IECoreUSD/src/IECoreUSD/PointsAlgo.cpp | 103 ++++++++++ .../IECoreUSD/test/IECoreUSD/USDSceneTest.py | 191 ++++++++++++++++++ 3 files changed, 297 insertions(+), 1 deletion(-) diff --git a/Changes b/Changes index 377d6e0675..d75f86aaa3 100644 --- a/Changes +++ b/Changes @@ -4,7 +4,9 @@ Improvements ------------ -- USD : All `radiance:sphericalHarmonicsCoefficients` are now loaded for `ParticleField3DGaussianSplat` objects, regardless of the `radiance:sphericalHarmonicsDegree`. +- USD : + - All `radiance:sphericalHarmonicsCoefficients` are now loaded for `ParticleField3DGaussianSplat` objects, regardless of the `radiance:sphericalHarmonicsDegree`. + - Added write support for `ParticleField3DGaussianSplat`. 10.7.3.1 (relative to 10.7.3.0) ======== diff --git a/contrib/IECoreUSD/src/IECoreUSD/PointsAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/PointsAlgo.cpp index 2512f8dc84..98329e1d6a 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/PointsAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/PointsAlgo.cpp @@ -40,8 +40,13 @@ #include "IECore/SimpleTypedData.h" +#include "boost/algorithm/string/predicate.hpp" + IECORE_PUSH_DEFAULT_VISIBILITY #include "pxr/usd/usdGeom/points.h" +#if PXR_VERSION >= 2603 +#include "pxr/usd/usdVol/particleField3DGaussianSplat.h" +#endif IECORE_POP_DEFAULT_VISIBILITY using namespace IECore; @@ -94,6 +99,104 @@ namespace bool writePoints( const IECoreScene::PointsPrimitive *points, const pxr::UsdStagePtr &stage, const pxr::SdfPath &path, pxr::UsdTimeCode time ) { +#if PXR_VERSION >= 2603 + auto type = points->variableData( "type", PrimitiveVariable::Constant ); + if( type && type->readable() == "gaussianSplat" ) + { + auto usdSplats = pxr::UsdVolParticleField3DGaussianSplat::Define( stage, path ); + + int maxCoefficientsIndex = -1; + std::unordered_map coefficientsMap; + + for( const auto &p : points->variables ) + { + if( p.first == "P" ) + { + usdSplats.CreatePositionsAttr().Set( PrimitiveAlgo::toUSDExpanded( p.second ), time ); + } + else if( p.first == "orientations" ) + { + usdSplats.CreateOrientationsAttr().Set( PrimitiveAlgo::toUSDExpanded( p.second ), time ); + } + else if( p.first == "scales" ) + { + usdSplats.CreateScalesAttr().Set( PrimitiveAlgo::toUSDExpanded( p.second ), time ); + } + else if( p.first == "opacities" ) + { + usdSplats.CreateOpacitiesAttr().Set( PrimitiveAlgo::toUSDExpanded( p.second ), time ); + } + else if( p.first == "radiance:sphericalHarmonicsDegree" ) + { + usdSplats.CreateRadianceSphericalHarmonicsDegreeAttr().Set( DataAlgo::toUSD( p.second.data.get() ), time ); + } + else if( p.first == "projectionModeHint" ) + { + usdSplats.CreateProjectionModeHintAttr().Set( DataAlgo::toUSD( p.second.data.get() ), time ); + } + else if( p.first == "sortingModeHint" ) + { + usdSplats.CreateSortingModeHintAttr().Set( DataAlgo::toUSD( p.second.data.get() ), time ); + } + else if( boost::starts_with( p.first, "radiance:sphericalHarmonicsCoefficients:" ) ) + { + try + { + const int coefficientsIndex = std::stoi( p.first.substr( 40 ) ); // 40 = length of `radiance:...` prefix + + if( p.second.data->typeId() != V3fVectorDataTypeId || p.second.interpolation != PrimitiveVariable::Interpolation::Vertex ) + { + IECore::msg( + IECore::Msg::Warning, + "PointsAlgo::writePoints", + "Invalid primitive variable \"{}\" on location \"{}\". Spherical harmonics coefficients must be of type V3fVectorData with Vertex interpolation.", + p.first, path.GetAsString() + ); + continue; + } + + coefficientsMap[coefficientsIndex] = static_cast( p.second.data.get() ); + maxCoefficientsIndex = std::max( maxCoefficientsIndex, coefficientsIndex ); + } + catch( ... ) + { + PrimitiveAlgo::writePrimitiveVariable( p.first, p.second, usdSplats, time ); + } + } + else if( p.first == "type" ) + { + // This is intrinsic to `UsdVolParticleField3DGaussianSplat` + continue; + } + else + { + PrimitiveAlgo::writePrimitiveVariable( p.first, p.second, usdSplats, time ); + } + } + + if( !coefficientsMap.empty() ) + { + const int coefficientsCount = maxCoefficientsIndex + 1; + pxr::VtArray allCoefficients( coefficientsCount * points->getNumPoints() ); + + for( const auto &[i, coefficientsData] : coefficientsMap ) + { + const std::vector &coefficients = coefficientsData->readable(); + + for( size_t j = 0, eJ = points->getNumPoints(); j < eJ; ++j ) + { + allCoefficients[j * coefficientsCount + i] = DataAlgo::toUSD( coefficients[j] ); + } + } + + usdSplats.CreateRadianceSphericalHarmonicsCoefficientsAttr().Set( allCoefficients, time ); + } + + return true; + } + +#endif + auto usdPoints = pxr::UsdGeomPoints::Define( stage, path ); for( const auto &p : points->variables ) { diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py index 1e42a1f218..ebe9b22b60 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py @@ -54,6 +54,7 @@ import pxr.Usd import pxr.UsdGeom import pxr.UsdLux +import pxr.UsdVol if pxr.Usd.GetVersion() < ( 0, 19, 3 ) : pxr.Usd.Attribute.HasAuthoredValue = pxr.Usd.Attribute.HasAuthoredValueOpinion @@ -5193,5 +5194,195 @@ def testReadGaussianSplats( self ) : self.assertEqual( splats[variable].data.getInterpretation(), IECore.GeometricData.Interpretation.None_ ) self.assertEqual( len( splats[variable].data ), 4 ) + @unittest.skipIf( pxr.Usd.GetVersion() < ( 0, 26, 3 ), "ParticleField3DGaussianSplat not available" ) + def testWriteGaussianSplats( self ) : + + fileName = pathlib.Path( self.temporaryDirectory() ) / "usd_splats.usda" + + sceneWrite = IECoreScene.SceneInterface.create( str( fileName ), IECore.IndexedIO.OpenMode.Write ) + root = sceneWrite.createChild( "root" ) + + nPoints = 16 + degree = 2 + + positionsData = IECore.V3fVectorData( [ imath.V3f( i ) for i in range( 0, nPoints ) ] ) + + cortexSplats = IECoreScene.PointsPrimitive( positionsData ) + + typeData = IECore.StringData( "gaussianSplat" ) + cortexSplats["type"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, typeData ) + orientationsData = IECore.QuatfVectorData( [ imath.Quatf( i, imath.V3f( i ) ) for i in range( 0, nPoints ) ] ) + cortexSplats["orientations"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, orientationsData ) + scalesData = IECore.V3fVectorData( [ imath.V3f( i ) for i in range( 0, nPoints ) ] ) + cortexSplats["scales"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, scalesData ) + opacitiesData = IECore.FloatVectorData( [ i / nPoints for i in range( 0, nPoints ) ] ) + cortexSplats["opacities"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, opacitiesData ) + projectionModeHintData = IECore.InternedStringData( "tangential" ) + cortexSplats["projectionModeHint"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, projectionModeHintData ) + sortingModeHintData = IECore.InternedStringData( "cameraDistance" ) + cortexSplats["sortingModeHint"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, sortingModeHintData ) + degreeData = IECore.IntData( degree ) + cortexSplats["radiance:sphericalHarmonicsDegree"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, degreeData ) + nCoefficients = ( degree + 1 ) * ( degree + 1 ) + coefficientsData = [] + for i in range( 0, nCoefficients ) : + coefficientsData.append( IECore.V3fVectorData( [ imath.V3f( j ) for j in range( 0, nPoints ) ] ) ) + cortexSplats[f"radiance:sphericalHarmonicsCoefficients:{i}"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, coefficientsData[i] ) + + displayColorData = IECore.Color3fVectorData( [ imath.Color3f( i ) for i in range( 0, nPoints ) ] ) + cortexSplats["Cs"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, displayColorData ) + + root.writeObject( cortexSplats, 0.0 ) + + del root + del sceneWrite + + # Check USD loading + + usdFile = pxr.Usd.Stage.Open( str( fileName ) ) + usdSplats = pxr.UsdVol.ParticleField3DGaussianSplat.Get( usdFile, "/root" ) + + orientationsAttr = usdSplats.GetOrientationsAttr() + orientations = orientationsAttr.Get( 0 ) + self.assertEqual( list( orientationsData ), [ imath.Quatf( i.GetReal(), imath.V3f( *i.GetImaginary() ) ) for i in orientations ] ) + + scalesAttr = usdSplats.GetScalesAttr() + scales = scalesAttr.Get( 0 ) + self.assertEqual( list( scalesData ), [ imath.V3f( i[0], i[1], i[2] ) for i in scales ] ) + + opacitiesAttr = usdSplats.GetOpacitiesAttr() + opacities = opacitiesAttr.Get( 0 ) + self.assertEqual( list( opacitiesData ), opacities ) + + displayColorPrimVar = usdSplats.GetDisplayColorPrimvar() + self.assertEqual( displayColorPrimVar.GetInterpolation(), "vertex" ) + displayColors = displayColorPrimVar.Get( 0 ) + self.assertEqual( list( displayColorData ), [ imath.Color3f( i[0], i[1], i[2] ) for i in displayColors ] ) + + projectionModeHintAttr = usdSplats.GetProjectionModeHintAttr() + self.assertEqual( projectionModeHintData.value, projectionModeHintAttr.Get( 0 ) ) + sortingModeHintAttr = usdSplats.GetSortingModeHintAttr() + self.assertEqual( sortingModeHintData.value, sortingModeHintAttr.Get( 0 ) ) + + degreeAttr = usdSplats.GetRadianceSphericalHarmonicsDegreeAttr() + self.assertEqual( degree, degreeAttr.Get( 0 ) ) + + desiredCoefficients = [ coefficientsData[j][i] for i in range( 0, nPoints ) for j in range( 0, nCoefficients ) ] + coefficientsAttr = usdSplats.GetRadianceSphericalHarmonicsCoefficientsAttr() + coefficients = coefficientsAttr.Get( 0 ) + self.assertEqual( desiredCoefficients, [ imath.V3f( i[0], i[1], i[2] ) for i in coefficients ] ) + + # Check round trip to Cortex + + sceneRead = IECoreScene.SceneInterface.create( str( fileName ), IECore.IndexedIO.OpenMode.Read ) + self.assertEqual( sceneRead.childNames(), ["root"] ) + readRoot = sceneRead.child( "root" ) + + self.assertTrue( readRoot.hasObject() ) + + readPoints = readRoot.readObject( 0.0 ) + self.assertIsInstance( readPoints, IECoreScene.PointsPrimitive ) + + self.assertEqual( readPoints, cortexSplats ) + + @unittest.skipIf( pxr.Usd.GetVersion() < ( 0, 26, 3 ), "ParticleField3DGaussianSplat not available" ) + def testWriteGaussianSplatsMalformedCoefficients( self ) : + + fileName = pathlib.Path( self.temporaryDirectory() ) / "usd_splats.usda" + + sceneWrite = IECoreScene.SceneInterface.create( str( fileName ), IECore.IndexedIO.OpenMode.Write ) + root = sceneWrite.createChild( "root" ) + + nPoints = 16 + degree = 2 + + positionsData = IECore.V3fVectorData( [ imath.V3f( i ) for i in range( 0, nPoints ) ] ) + + cortexSplats = IECoreScene.PointsPrimitive( positionsData ) + + cortexSplats["type"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.StringData( "gaussianSplat" ) ) + cortexSplats["radiance:sphericalHarmonicsDegree"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntData( degree ) ) + + nCoefficients = ( degree + 1 ) * ( degree + 1 ) + coefficientsOffset = 2 + coefficientsData = [] + for i in list( range( 0, nCoefficients ) ): + coefficientsData.append( IECore.V3fVectorData( [ imath.V3f( j ) for j in range( 0, nPoints ) ] ) ) + cortexSplats[f"radiance:sphericalHarmonicsCoefficients:{i}"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, coefficientsData[i] ) + + extraCoefficientsData = IECore.V3fVectorData( [ imath.V3f( i ) for i in range( 0, nPoints ) ] ) + cortexSplats[f"radiance:sphericalHarmonicsCoefficients:{nCoefficients + coefficientsOffset}"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, extraCoefficientsData ) + + nonNumericCoefficientIndexData = IECore.FloatVectorData( [ i + 10 for i in range( 0, nPoints ) ] ) + cortexSplats["radiance:sphericalHarmonicsCoefficients:string"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, nonNumericCoefficientIndexData ) + + nonV3fCoefficientsData = IECore.FloatVectorData( [ i + 100 for i in range( 0, nPoints ) ] ) + cortexSplats[f"radiance:sphericalHarmonicsCoefficients:{nCoefficients + 10}"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, nonV3fCoefficientsData ) + + with IECore.CapturingMessageHandler() as mh : + root.writeObject( cortexSplats, 0.0 ) + + self.assertEqual( len( mh.messages ), 1 ) + self.assertEqual( + mh.messages[0].message, + f"Invalid primitive variable \"radiance:sphericalHarmonicsCoefficients:{nCoefficients + 10}\" on location \"/root\". Spherical harmonics coefficients must be of type V3fVectorData with Vertex interpolation." + ) + + del root + del sceneWrite + + # Check USD loading + + usdFile = pxr.Usd.Stage.Open( str( fileName ) ) + usdSplats = pxr.UsdVol.ParticleField3DGaussianSplat.Get( usdFile, "/root" ) + + primVars = pxr.UsdGeom.PrimvarsAPI( usdSplats ) + stringCoefficients = primVars.GetPrimvar( "radiance:sphericalHarmonicsCoefficients:string" ).Get( 0 ) + self.assertEqual( list( nonNumericCoefficientIndexData ), stringCoefficients ) + + self.assertFalse( primVars.GetPrimvar( "radiance:sphericalHarmonicsCoefficients:22" ) ) + + coefficientsAttr = usdSplats.GetRadianceSphericalHarmonicsCoefficientsAttr() + usdCoefficients = coefficientsAttr.Get( 0 ) + self.assertEqual( len( usdCoefficients ), ( nCoefficients + coefficientsOffset + 1) * nPoints ) + + desiredCoefficients = [] + for i in range( 0, nPoints ) : + for j in range( 0, nCoefficients ) : + desiredCoefficients.append( coefficientsData[j][i] ) + for j in range( 0, coefficientsOffset ) : + desiredCoefficients.append( imath.V3f( 0.0 ) ) + desiredCoefficients.append( imath.V3f( i ) ) + + self.assertEqual( desiredCoefficients, [ imath.V3f( i[0], i[1], i[2] ) for i in usdCoefficients ] ) + + # Check round trip to Cortex + + sceneRead = IECoreScene.SceneInterface.create( str( fileName ), IECore.IndexedIO.OpenMode.Read ) + self.assertEqual( sceneRead.childNames(), ["root"] ) + readRoot = sceneRead.child( "root" ) + + self.assertTrue( readRoot.hasObject() ) + + with IECore.CapturingMessageHandler() as mh : + readPoints = readRoot.readObject( 0.0 ) + + self.assertIsInstance( readPoints, IECoreScene.PointsPrimitive ) + self.assertEqual( len( mh.messages ), 1 ) + self.assertEqual( + mh.messages[0].message, + "Attribute \"radiance:sphericalHarmonicsCoefficients\" size does not match that expected by \"radiance:sphericalHarmonicsDegree\" at location \"/root\"." + ) + + self.assertEqual( readPoints["radiance:sphericalHarmonicsCoefficients:string"].data, nonNumericCoefficientIndexData ) + + for i in range( 0, nCoefficients ) : + with self.subTest( i = i ) : + self.assertEqual( readPoints[f"radiance:sphericalHarmonicsCoefficients:{i}"].data, coefficientsData[i] ) + for i in range( nCoefficients, nCoefficients + coefficientsOffset ) : + with self.subTest( i = i ) : + self.assertEqual( list( readPoints[f"radiance:sphericalHarmonicsCoefficients:{i}"].data ), [ imath.V3f( 0.0 ) for i in range( 0, nPoints ) ] ) + self.assertEqual( list( readPoints[f"radiance:sphericalHarmonicsCoefficients:{nCoefficients + coefficientsOffset}"].data ), [ imath.V3f( i ) for i in range( 0, nPoints ) ] ) + if __name__ == "__main__": unittest.main()