Skip to content

Commit afbf31d

Browse files
authored
Merge pull request #6855 from GafferHQ/curvePinning
CurvesPrimitive clients : Support WrapMode enum
2 parents cbfbcbd + 274377f commit afbf31d

37 files changed

Lines changed: 907 additions & 103 deletions

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ jobs:
7777
testRunner: sudo -E -u testUser
7878
sconsCacheMegabytes: 400
7979
jobs: 4
80-
dependenciesURL: https://github.com/ImageEngine/cortex/releases/download/10.7.0.0a7/cortex-10.7.0.0a7-linux-platform24.tar.gz
80+
dependenciesURL: https://github.com/ImageEngine/cortex/releases/download/10.7.0.0a8/cortex-10.7.0.0a8-linux-platform24.tar.gz
8181
extraBuildArguments: CYCLES_ROOT=""
8282

8383
- name: windows

.github/workflows/main/installDependencies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949

5050
# Determine default archive URL.
5151

52-
defaultURL = "https://github.com/GafferHQ/dependencies/releases/download/11.0.0a4/gafferDependencies-11.0.0a4-{platform}-{vfxPlatform}.{extension}"
52+
defaultURL = "https://github.com/ImageEngine/cortex/releases/download/10.7.0.0a8/cortex-10.7.0.0a8-{platform}-{vfxPlatform}.{extension}"
5353

5454
# Parse command line arguments.
5555

Changes.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Features
55
--------
66

77
- Cycles : Updated to version 5.0.0.
8+
- CurvesInterpolation : Added node for modifying CurvesPrimitive `basis` and `wrap`. This includes the ability to convert curves with `Pinned` wrap to `NonPeriodic`, adding the appropriate "phantom" points to maintain curve shape.
89

910
Improvements
1011
------------
@@ -20,6 +21,9 @@ Improvements
2021
- CyclesOptions : Added `cycles:integrator:volume_ray_marching` option.
2122
- LightEditor : Added column for `cycles:visibility:camera` attribute.
2223
- OpenColorIO : Added ACES Studio 2.0 config. The default config is still ACES 1.3, due to RenderMan not supporting ACES 2.0.
24+
- CurvesPrimitive : Added `Pinned` wrap mode in addition to the existing `Periodic` and `NonPeriodic` modes. This conveniently interpolates CatmullRom
25+
and BSpline curves to their endpoints automatically, without manual management of duplicate endpoints or "phantom vertices".
26+
- SceneReader, SceneWriter : Added support for pinned UsdGeomBasisCurves.
2327

2428
Fixes
2529
-----
@@ -28,6 +32,9 @@ Fixes
2832
- UI : Fixed failure to cancel background computations when more than one UI element was waiting for the same result. This could result in the UI becoming unresponsive until the computation was complete.
2933
- BackgroundMethod : Fixed bug that allowed unwanted background computations to continue when a widget was hidden.
3034
- MeshTessellate : Fixed crashes caused by non-manifold geometry.
35+
- 3Delight : Fixed rendering of linear curves.
36+
- DeleteCurves : Fixed deletion of periodic curves.
37+
- ResamplePrimitiveVariables : Fixed resampling between Vertex and Varying for linear curves.
3138

3239
API
3340
---
@@ -71,7 +78,7 @@ Build
7178
-----
7279

7380
- Boost : Updated to version 1.85.0.
74-
- Cortex : Updated to version 10.7.0.0a7.
81+
- Cortex : Updated to version 10.7.0.0a8.
7582
- Cycles : Updated to version 5.0.0.
7683
- Embree : Updated to version 4.4.0.
7784
- Imath : Updated to version 3.1.12.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//////////////////////////////////////////////////////////////////////////
2+
//
3+
// Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved.
4+
//
5+
// Redistribution and use in source and binary forms, with or without
6+
// modification, are permitted provided that the following conditions are
7+
// met:
8+
//
9+
// * Redistributions of source code must retain the above
10+
// copyright notice, this list of conditions and the following
11+
// disclaimer.
12+
//
13+
// * Redistributions in binary form must reproduce the above
14+
// copyright notice, this list of conditions and the following
15+
// disclaimer in the documentation and/or other materials provided with
16+
// the distribution.
17+
//
18+
// * Neither the name of John Haddon nor the names of
19+
// any other contributors to this software may be used to endorse or
20+
// promote products derived from this software without specific prior
21+
// written permission.
22+
//
23+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
24+
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
25+
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
26+
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
27+
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
28+
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29+
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30+
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31+
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32+
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33+
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34+
//
35+
//////////////////////////////////////////////////////////////////////////
36+
37+
#pragma once
38+
39+
#include "GafferScene/ObjectProcessor.h"
40+
41+
#include "Gaffer/OptionalValuePlug.h"
42+
43+
namespace GafferScene
44+
{
45+
46+
class GAFFERSCENE_API CurvesInterpolation : public ObjectProcessor
47+
{
48+
49+
public :
50+
51+
explicit CurvesInterpolation( const std::string &name=defaultName<CurvesInterpolation>() );
52+
~CurvesInterpolation() override;
53+
54+
GAFFER_NODE_DECLARE_TYPE( GafferScene::CurvesInterpolation, CurvesInterpolationTypeId, ObjectProcessor );
55+
56+
/// Values are from `IECore::StandardCubicBasis`.
57+
Gaffer::OptionalValuePlug *basisPlug();
58+
const Gaffer::OptionalValuePlug *basisPlug() const;
59+
60+
Gaffer::OptionalValuePlug *wrapPlug();
61+
const Gaffer::OptionalValuePlug *wrapPlug() const;
62+
63+
Gaffer::BoolPlug *expandPinnedPlug();
64+
const Gaffer::BoolPlug *expandPinnedPlug() const;
65+
66+
protected :
67+
68+
bool affectsProcessedObject( const Gaffer::Plug *input ) const override;
69+
void hashProcessedObject( const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h ) const override;
70+
IECore::ConstObjectPtr computeProcessedObject( const ScenePath &path, const Gaffer::Context *context, const IECore::Object *inputObject ) const override;
71+
72+
private :
73+
74+
static size_t g_firstPlugIndex;
75+
76+
};
77+
78+
IE_CORE_DECLAREPTR( CurvesInterpolation )
79+
80+
} // namespace GafferScene

include/GafferScene/TypeIds.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ enum TypeId
196196
CameraQueryTypeId = 120151,
197197
ClosurePlugTypeId = 120152,
198198
ReflectionConstraintTypeId = 120153,
199+
CurvesInterpolationTypeId = 120154,
199200

200201
LastTypeId = 120999
201202
};
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
##########################################################################
2+
#
3+
# Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are
7+
# met:
8+
#
9+
# * Redistributions of source code must retain the above
10+
# copyright notice, this list of conditions and the following
11+
# disclaimer.
12+
#
13+
# * Redistributions in binary form must reproduce the above
14+
# copyright notice, this list of conditions and the following
15+
# disclaimer in the documentation and/or other materials provided with
16+
# the distribution.
17+
#
18+
# * Neither the name of John Haddon nor the names of
19+
# any other contributors to this software may be used to endorse or
20+
# promote products derived from this software without specific prior
21+
# written permission.
22+
#
23+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
24+
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
25+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
26+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
27+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
28+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34+
#
35+
##########################################################################
36+
37+
import unittest
38+
39+
import imath
40+
41+
import IECore
42+
import IECoreScene
43+
44+
import Gaffer
45+
import GafferScene
46+
import GafferSceneTest
47+
48+
class CurvesInterpolationTest( GafferSceneTest.SceneTestCase ) :
49+
50+
def testPassThrough( self ) :
51+
52+
objectToScene = GafferScene.ObjectToScene()
53+
objectToScene["object"].setValue(
54+
IECoreScene.CurvesPrimitive(
55+
IECore.IntVectorData( [ 4 ] ), IECore.CubicBasisf.catmullRom()
56+
)
57+
)
58+
59+
interpolation = GafferScene.CurvesInterpolation()
60+
interpolation["in"].setInput( objectToScene["out"] )
61+
62+
# No filter
63+
64+
self.assertTrue( interpolation["out"].exists( "/object" ) )
65+
self.assertScenesEqual( interpolation["out"], interpolation["in"] )
66+
self.assertSceneHashesEqual( interpolation["out"], interpolation["in"] )
67+
68+
# Filter, but not matching anything yet
69+
70+
pathFilter = GafferScene.PathFilter()
71+
interpolation["filter"].setInput( pathFilter["out"] )
72+
73+
self.assertTrue( interpolation["out"].exists( "/object" ) )
74+
self.assertScenesEqual( interpolation["out"], interpolation["in"] )
75+
self.assertSceneHashesEqual( interpolation["out"], interpolation["in"] )
76+
77+
# Filter matching something, but node disabled
78+
79+
pathFilter["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) )
80+
interpolation["enabled"].setValue( False )
81+
82+
self.assertTrue( interpolation["out"].exists( "/object" ) )
83+
self.assertScenesEqual( interpolation["out"], interpolation["in"] )
84+
self.assertSceneHashesEqual( interpolation["out"], interpolation["in"] )
85+
86+
# Node enabled, but not doing anything
87+
88+
interpolation["enabled"].setValue( True )
89+
90+
self.assertTrue( interpolation["out"].exists( "/object" ) )
91+
self.assertScenesEqual( interpolation["out"], interpolation["in"] )
92+
self.assertSceneHashesEqual( interpolation["out"], interpolation["in"] )
93+
94+
def testChangeWrap( self ) :
95+
96+
objectToScene = GafferScene.ObjectToScene()
97+
objectToScene["object"].setValue(
98+
IECoreScene.CurvesPrimitive(
99+
IECore.IntVectorData( [ 4 ] ), IECore.CubicBasisf.catmullRom(),
100+
wrap = IECoreScene.CurvesPrimitive.Wrap.Pinned,
101+
p = IECore.V3fVectorData( [ imath.V3f( x ) for x in range( 0, 4 ) ] )
102+
)
103+
)
104+
105+
pathFilter = GafferScene.PathFilter()
106+
pathFilter["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) )
107+
108+
interpolation = GafferScene.CurvesInterpolation()
109+
interpolation["in"].setInput( objectToScene["out"] )
110+
interpolation["filter"].setInput( pathFilter["out"] )
111+
112+
self.assertEqual( interpolation["out"].object( "/object" ).wrap(), IECoreScene.CurvesPrimitive.Wrap.Pinned )
113+
114+
interpolation["wrap"]["enabled"].setValue( True )
115+
interpolation["wrap"]["value"].setValue( IECoreScene.CurvesPrimitive.Wrap.NonPeriodic )
116+
117+
self.assertEqual( interpolation["out"].object( "/object" ).wrap(), IECoreScene.CurvesPrimitive.Wrap.NonPeriodic )
118+
self.assertEqual( interpolation["out"].object( "/object" )["P"], interpolation["in"].object( "/object" )["P"] )
119+
120+
interpolation["expandPinned"].setValue( True )
121+
self.assertEqual( interpolation["out"].object( "/object" ).wrap(), IECoreScene.CurvesPrimitive.Wrap.NonPeriodic )
122+
self.assertEqual(
123+
interpolation["out"].object( "/object" )["P"].data,
124+
IECore.V3fVectorData( [ imath.V3f( x ) for x in range( -1, 5 ) ], IECore.GeometricData.Interpretation.Point )
125+
)
126+
127+
def testChangeBasis( self ) :
128+
129+
objectToScene = GafferScene.ObjectToScene()
130+
objectToScene["object"].setValue(
131+
IECoreScene.CurvesPrimitive(
132+
IECore.IntVectorData( [ 4 ] ), IECore.CubicBasisf.catmullRom(),
133+
p = IECore.V3fVectorData( [ imath.V3f( x ) for x in range( 0, 4 ) ] )
134+
)
135+
)
136+
137+
pathFilter = GafferScene.PathFilter()
138+
pathFilter["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) )
139+
140+
interpolation = GafferScene.CurvesInterpolation()
141+
interpolation["in"].setInput( objectToScene["out"] )
142+
interpolation["filter"].setInput( pathFilter["out"] )
143+
144+
self.assertEqual( interpolation["out"].object( "/object" ).basis(), IECore.CubicBasisf.catmullRom() )
145+
146+
interpolation["basis"]["enabled"].setValue( True )
147+
interpolation["basis"]["value"].setValue( IECore.StandardCubicBasis.BSpline )
148+
149+
self.assertEqual( interpolation["out"].object( "/object" ).basis(), IECore.CubicBasisf.bSpline() )
150+
151+
if __name__ == "__main__":
152+
unittest.main()

python/GafferSceneTest/IECoreScenePreviewTest/PrimitiveAlgoTest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ def testMergePrimitivesSimpleCurves( self ) :
274274
self.assertEqual( merged["P"], IECoreScene.PrimitiveVariable( Interpolation.Vertex, IECore.V3fVectorData( curveVerts1 + curveVerts2, IECore.GeometricData.Interpretation.Point ) ) )
275275

276276
curves1.setTopology( curves1.verticesPerCurve(), curves1.basis(), True )
277-
with self.assertRaisesRegex( RuntimeError, "Cannot merge periodic and non-periodic curves" ) :
277+
with self.assertRaisesRegex( RuntimeError, "Cannot merge curves with mismatched wrap" ) :
278278
PrimitiveAlgo.mergePrimitives( [( curves1, imath.M44f() ), ( curves2, imath.M44f() ) ] )
279279

280280
curves2.setTopology( curves2.verticesPerCurve(), curves2.basis(), True )

python/GafferSceneTest/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@
196196
from .CameraQueryTest import CameraQueryTest
197197
from .GlobalsSanitiserTest import GlobalsSanitiserTest
198198
from .ReflectionConstraintTest import ReflectionConstraintTest
199+
from .CurvesInterpolationTest import CurvesInterpolationTest
199200

200201
from .IECoreScenePreviewTest import *
201202
from .IECoreGLPreviewTest import *

0 commit comments

Comments
 (0)