Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
testRunner: sudo -E -u testUser
sconsCacheMegabytes: 400
jobs: 4
dependenciesURL: https://github.com/ImageEngine/cortex/releases/download/10.7.0.0a7/cortex-10.7.0.0a7-linux-platform24.tar.gz
dependenciesURL: https://github.com/ImageEngine/cortex/releases/download/10.7.0.0a8/cortex-10.7.0.0a8-linux-platform24.tar.gz
extraBuildArguments: CYCLES_ROOT=""

- name: windows
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main/installDependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

# Determine default archive URL.

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

# Parse command line arguments.

Expand Down
9 changes: 8 additions & 1 deletion Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Features
--------

- Cycles : Updated to version 5.0.0.
- 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.

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

Fixes
-----
Expand All @@ -28,6 +32,9 @@ Fixes
- 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.
- BackgroundMethod : Fixed bug that allowed unwanted background computations to continue when a widget was hidden.
- MeshTessellate : Fixed crashes caused by non-manifold geometry.
- 3Delight : Fixed rendering of linear curves.
- DeleteCurves : Fixed deletion of periodic curves.
- ResamplePrimitiveVariables : Fixed resampling between Vertex and Varying for linear curves.

API
---
Expand Down Expand Up @@ -71,7 +78,7 @@ Build
-----

- Boost : Updated to version 1.85.0.
- Cortex : Updated to version 10.7.0.0a7.
- Cortex : Updated to version 10.7.0.0a8.
- Cycles : Updated to version 5.0.0.
- Embree : Updated to version 4.4.0.
- Imath : Updated to version 3.1.12.
Expand Down
80 changes: 80 additions & 0 deletions include/GafferScene/CurvesInterpolation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above
// copyright notice, this list of conditions and the following
// disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided with
// the distribution.
//
// * Neither the name of John Haddon nor the names of
// any other contributors to this software may be used to endorse or
// promote products derived from this software without specific prior
// written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////

#pragma once

#include "GafferScene/ObjectProcessor.h"

#include "Gaffer/OptionalValuePlug.h"

namespace GafferScene
{

class GAFFERSCENE_API CurvesInterpolation : public ObjectProcessor
{

public :

explicit CurvesInterpolation( const std::string &name=defaultName<CurvesInterpolation>() );
~CurvesInterpolation() override;

GAFFER_NODE_DECLARE_TYPE( GafferScene::CurvesInterpolation, CurvesInterpolationTypeId, ObjectProcessor );

/// Values are from `IECore::StandardCubicBasis`.
Gaffer::OptionalValuePlug *basisPlug();
const Gaffer::OptionalValuePlug *basisPlug() const;

Gaffer::OptionalValuePlug *wrapPlug();
const Gaffer::OptionalValuePlug *wrapPlug() const;

Gaffer::BoolPlug *expandPinnedPlug();
const Gaffer::BoolPlug *expandPinnedPlug() const;

protected :

bool affectsProcessedObject( const Gaffer::Plug *input ) const override;
void hashProcessedObject( const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h ) const override;
IECore::ConstObjectPtr computeProcessedObject( const ScenePath &path, const Gaffer::Context *context, const IECore::Object *inputObject ) const override;

private :

static size_t g_firstPlugIndex;

};

IE_CORE_DECLAREPTR( CurvesInterpolation )

} // namespace GafferScene
1 change: 1 addition & 0 deletions include/GafferScene/TypeIds.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ enum TypeId
CameraQueryTypeId = 120151,
ClosurePlugTypeId = 120152,
ReflectionConstraintTypeId = 120153,
CurvesInterpolationTypeId = 120154,

LastTypeId = 120999
};
Expand Down
152 changes: 152 additions & 0 deletions python/GafferSceneTest/CurvesInterpolationTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
##########################################################################
#
# Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with
# the distribution.
#
# * Neither the name of John Haddon nor the names of
# any other contributors to this software may be used to endorse or
# promote products derived from this software without specific prior
# written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
##########################################################################

import unittest

import imath

import IECore
import IECoreScene

import Gaffer
import GafferScene
import GafferSceneTest

class CurvesInterpolationTest( GafferSceneTest.SceneTestCase ) :

def testPassThrough( self ) :

objectToScene = GafferScene.ObjectToScene()
objectToScene["object"].setValue(
IECoreScene.CurvesPrimitive(
IECore.IntVectorData( [ 4 ] ), IECore.CubicBasisf.catmullRom()
)
)

interpolation = GafferScene.CurvesInterpolation()
interpolation["in"].setInput( objectToScene["out"] )

# No filter

self.assertTrue( interpolation["out"].exists( "/object" ) )
self.assertScenesEqual( interpolation["out"], interpolation["in"] )
self.assertSceneHashesEqual( interpolation["out"], interpolation["in"] )

# Filter, but not matching anything yet

pathFilter = GafferScene.PathFilter()
interpolation["filter"].setInput( pathFilter["out"] )

self.assertTrue( interpolation["out"].exists( "/object" ) )
self.assertScenesEqual( interpolation["out"], interpolation["in"] )
self.assertSceneHashesEqual( interpolation["out"], interpolation["in"] )

# Filter matching something, but node disabled

pathFilter["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) )
interpolation["enabled"].setValue( False )

self.assertTrue( interpolation["out"].exists( "/object" ) )
self.assertScenesEqual( interpolation["out"], interpolation["in"] )
self.assertSceneHashesEqual( interpolation["out"], interpolation["in"] )

# Node enabled, but not doing anything

interpolation["enabled"].setValue( True )

self.assertTrue( interpolation["out"].exists( "/object" ) )
self.assertScenesEqual( interpolation["out"], interpolation["in"] )
self.assertSceneHashesEqual( interpolation["out"], interpolation["in"] )

def testChangeWrap( self ) :

objectToScene = GafferScene.ObjectToScene()
objectToScene["object"].setValue(
IECoreScene.CurvesPrimitive(
IECore.IntVectorData( [ 4 ] ), IECore.CubicBasisf.catmullRom(),
wrap = IECoreScene.CurvesPrimitive.Wrap.Pinned,
p = IECore.V3fVectorData( [ imath.V3f( x ) for x in range( 0, 4 ) ] )
)
)

pathFilter = GafferScene.PathFilter()
pathFilter["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) )

interpolation = GafferScene.CurvesInterpolation()
interpolation["in"].setInput( objectToScene["out"] )
interpolation["filter"].setInput( pathFilter["out"] )

self.assertEqual( interpolation["out"].object( "/object" ).wrap(), IECoreScene.CurvesPrimitive.Wrap.Pinned )

interpolation["wrap"]["enabled"].setValue( True )
interpolation["wrap"]["value"].setValue( IECoreScene.CurvesPrimitive.Wrap.NonPeriodic )

self.assertEqual( interpolation["out"].object( "/object" ).wrap(), IECoreScene.CurvesPrimitive.Wrap.NonPeriodic )
self.assertEqual( interpolation["out"].object( "/object" )["P"], interpolation["in"].object( "/object" )["P"] )

interpolation["expandPinned"].setValue( True )
self.assertEqual( interpolation["out"].object( "/object" ).wrap(), IECoreScene.CurvesPrimitive.Wrap.NonPeriodic )
self.assertEqual(
interpolation["out"].object( "/object" )["P"].data,
IECore.V3fVectorData( [ imath.V3f( x ) for x in range( -1, 5 ) ], IECore.GeometricData.Interpretation.Point )
)

def testChangeBasis( self ) :

objectToScene = GafferScene.ObjectToScene()
objectToScene["object"].setValue(
IECoreScene.CurvesPrimitive(
IECore.IntVectorData( [ 4 ] ), IECore.CubicBasisf.catmullRom(),
p = IECore.V3fVectorData( [ imath.V3f( x ) for x in range( 0, 4 ) ] )
)
)

pathFilter = GafferScene.PathFilter()
pathFilter["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) )

interpolation = GafferScene.CurvesInterpolation()
interpolation["in"].setInput( objectToScene["out"] )
interpolation["filter"].setInput( pathFilter["out"] )

self.assertEqual( interpolation["out"].object( "/object" ).basis(), IECore.CubicBasisf.catmullRom() )

interpolation["basis"]["enabled"].setValue( True )
interpolation["basis"]["value"].setValue( IECore.StandardCubicBasis.BSpline )

self.assertEqual( interpolation["out"].object( "/object" ).basis(), IECore.CubicBasisf.bSpline() )

if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def testMergePrimitivesSimpleCurves( self ) :
self.assertEqual( merged["P"], IECoreScene.PrimitiveVariable( Interpolation.Vertex, IECore.V3fVectorData( curveVerts1 + curveVerts2, IECore.GeometricData.Interpretation.Point ) ) )

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

curves2.setTopology( curves2.verticesPerCurve(), curves2.basis(), True )
Expand Down
1 change: 1 addition & 0 deletions python/GafferSceneTest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
from .CameraQueryTest import CameraQueryTest
from .GlobalsSanitiserTest import GlobalsSanitiserTest
from .ReflectionConstraintTest import ReflectionConstraintTest
from .CurvesInterpolationTest import CurvesInterpolationTest

from .IECoreScenePreviewTest import *
from .IECoreGLPreviewTest import *
Expand Down
Loading
Loading