Skip to content

Commit cbfbcbd

Browse files
Merge branch '1.6_maintenance'
2 parents 9a39570 + 386b49f commit cbfbcbd

10 files changed

Lines changed: 690 additions & 0 deletions

File tree

Changes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ Build
9292
1.6.x.x (relative to 1.6.14.2)
9393
=======
9494

95+
Features
96+
--------
97+
98+
- ReflectionConstraint : Added a new node for constraining objects so that they are reflected in the target when viewed through a chosen camera.
99+
95100
Improvements
96101
------------
97102

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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/Constraint.h"
40+
41+
namespace GafferScene
42+
{
43+
44+
class GAFFERSCENE_API ReflectionConstraint : public Constraint
45+
{
46+
47+
public :
48+
49+
explicit ReflectionConstraint( const std::string &name=defaultName<ReflectionConstraint>() );
50+
~ReflectionConstraint() override;
51+
52+
GAFFER_NODE_DECLARE_TYPE( GafferScene::ReflectionConstraint, ReflectionConstraintTypeId, Constraint );
53+
54+
Gaffer::StringPlug *cameraPlug();
55+
const Gaffer::StringPlug *cameraPlug() const;
56+
57+
enum class DistanceMode
58+
{
59+
Camera,
60+
Constant
61+
};
62+
63+
Gaffer::IntPlug *distanceModePlug();
64+
const Gaffer::IntPlug *distanceModePlug() const;
65+
66+
Gaffer::FloatPlug *distancePlug();
67+
const Gaffer::FloatPlug *distancePlug() const;
68+
69+
Gaffer::BoolPlug *aimEnabledPlug();
70+
const Gaffer::BoolPlug *aimEnabledPlug() const;
71+
72+
Gaffer::V3fPlug *aimPlug();
73+
const Gaffer::V3fPlug *aimPlug() const;
74+
75+
Gaffer::V3fPlug *upPlug();
76+
const Gaffer::V3fPlug *upPlug() const;
77+
78+
Gaffer::FloatPlug *twistPlug();
79+
const Gaffer::FloatPlug *twistPlug() const;
80+
81+
protected :
82+
83+
bool affectsConstraint( const Gaffer::Plug *input ) const override;
84+
void hashConstraint( const Gaffer::Context *context, IECore::MurmurHash &h ) const override;
85+
Imath::M44f computeConstraint( const Imath::M44f &fullTargetTransform, const Imath::M44f &fullInputTransform, const Imath::M44f &inputTransform ) const override;
86+
87+
private :
88+
89+
static size_t g_firstPlugIndex;
90+
91+
};
92+
93+
IE_CORE_DECLAREPTR( ReflectionConstraint )
94+
95+
} // namespace GafferScene

include/GafferScene/TypeIds.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ enum TypeId
195195
GafferDisplayDriverTypeId = 120150,
196196
CameraQueryTypeId = 120151,
197197
ClosurePlugTypeId = 120152,
198+
ReflectionConstraintTypeId = 120153,
198199

199200
LastTypeId = 120999
200201
};
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
import imath
39+
40+
import IECore
41+
42+
import Gaffer
43+
import GafferScene
44+
import GafferSceneTest
45+
46+
class ReflectionConstraintTest( GafferSceneTest.SceneTestCase ) :
47+
48+
def testPosition( self ) :
49+
50+
target = GafferScene.Plane()
51+
target["name"].setValue( "target" )
52+
53+
sphere = GafferScene.Sphere()
54+
camera = GafferScene.Camera()
55+
56+
parent = GafferScene.Parent()
57+
parent["parent"].setValue( "/" )
58+
parent["in"].setInput( target["out"] )
59+
parent["children"][0].setInput( sphere["out"] )
60+
parent["children"][1].setInput( camera["out"] )
61+
62+
sphereFilter = GafferScene.PathFilter()
63+
sphereFilter["paths"].setValue( IECore.StringVectorData( [ "/sphere" ] ) )
64+
65+
constraint = GafferScene.ReflectionConstraint()
66+
constraint["in"].setInput( parent["out"] )
67+
constraint["filter"].setInput( sphereFilter["out"] )
68+
constraint["target"].setValue( "/target" )
69+
constraint["targetMode"].setValue( constraint.TargetMode.UV )
70+
constraint["targetUV"].setValue( imath.V2f( 0.5 ) )
71+
constraint["camera"].setValue( "/camera" )
72+
73+
for cameraTranslation in [
74+
imath.V3f( 0 ),
75+
imath.V3f( 1, 0, 0 ),
76+
imath.V3f( 1, 1, 0 ),
77+
imath.V3f( 10, 2, 0 ),
78+
] :
79+
80+
for distance in ( None, 1.0, 10.0 ) :
81+
82+
with self.subTest( cameraTranslation = cameraTranslation, distance = distance ) :
83+
84+
camera["transform"]["translate"].setValue( cameraTranslation )
85+
86+
if distance is None :
87+
constraint["distanceMode"].setValue( constraint.DistanceMode.Camera )
88+
else :
89+
constraint["distanceMode"].setValue( constraint.DistanceMode.Constant )
90+
constraint["distance"].setValue( distance )
91+
92+
transform = constraint["out"].fullTransform( "/sphere" )
93+
94+
if distance is None :
95+
self.assertEqual( transform.translation(), -cameraTranslation )
96+
else :
97+
self.assertEqual( transform.translation(), -cameraTranslation.normalized() * distance )
98+
99+
self.__assertRotationsEqual( transform, constraint["in"].fullTransform( "/sphere" ) )
100+
101+
def testOrientation( self ) :
102+
103+
target = GafferScene.Plane()
104+
target["name"].setValue( "target" )
105+
106+
sphere = GafferScene.Sphere()
107+
sphere["transform"]["rotate"].setValue( imath.V3f( 90, 0, 0 ) )
108+
camera = GafferScene.Camera()
109+
camera["transform"]["translate"].setValue( imath.V3f( 1, 1, 0 ) )
110+
111+
parent = GafferScene.Parent()
112+
parent["parent"].setValue( "/" )
113+
parent["in"].setInput( target["out"] )
114+
parent["children"][0].setInput( sphere["out"] )
115+
parent["children"][1].setInput( camera["out"] )
116+
117+
sphereFilter = GafferScene.PathFilter()
118+
sphereFilter["paths"].setValue( IECore.StringVectorData( [ "/sphere" ] ) )
119+
120+
constraint = GafferScene.ReflectionConstraint()
121+
constraint["in"].setInput( parent["out"] )
122+
constraint["filter"].setInput( sphereFilter["out"] )
123+
constraint["target"].setValue( "/target" )
124+
constraint["targetMode"].setValue( constraint.TargetMode.UV )
125+
constraint["targetUV"].setValue( imath.V2f( 0.5 ) )
126+
constraint["camera"].setValue( "/camera" )
127+
128+
transform = constraint["out"].fullTransform( "/sphere" )
129+
self.assertEqual( transform.translation(), -camera["transform"]["translate"].getValue() )
130+
self.__assertRotationsEqual( transform, constraint["in"].fullTransform( "/sphere" ) )
131+
132+
constraint["aimEnabled"].setValue( True )
133+
transform = constraint["out"].fullTransform( "/sphere" )
134+
135+
expectedRotation = imath.M44f().rotationMatrixWithUpDir( imath.V3f( 0, 0, -1 ), -transform.translation(), imath.V3f( 0, 1, 0 ) )
136+
self.__assertRotationsEqual( transform, expectedRotation )
137+
138+
def testMissingCamera( self ) :
139+
140+
plane = GafferScene.Plane()
141+
sphere = GafferScene.Sphere()
142+
143+
parent = GafferScene.Parent()
144+
parent["parent"].setValue( "/" )
145+
parent["in"].setInput( plane["out"] )
146+
parent["children"][0].setInput( sphere["out"] )
147+
148+
sphereFilter = GafferScene.PathFilter()
149+
sphereFilter["paths"].setValue( IECore.StringVectorData( [ "/sphere" ] ) )
150+
151+
constraint = GafferScene.ReflectionConstraint()
152+
constraint["in"].setInput( parent["out"] )
153+
constraint["filter"].setInput( sphereFilter["out"] )
154+
constraint["target"].setValue( "/plane" )
155+
constraint["camera"].setValue( "/camera" )
156+
157+
with self.assertRaisesRegex( Gaffer.ProcessException, "Camera \"/camera\" does not exist. Error may be suppressed using `ignoreMissingTarget`." ) :
158+
constraint["out"].fullTransform( "/sphere" )
159+
160+
constraint["ignoreMissingTarget"].setValue( True )
161+
self.assertEqual( constraint["out"].fullTransform( "/sphere" ), constraint["in"].fullTransform( "/sphere" ) )
162+
163+
def __assertRotationsEqual( self, matrix1, matrix2 ) :
164+
165+
rotation1 = imath.Eulerf()
166+
matrix1.extractEulerXYZ( rotation1 )
167+
rotation2 = imath.Eulerf()
168+
matrix2.extractEulerXYZ( rotation2 )
169+
170+
self.assertEqual( rotation1, rotation2 )
171+
172+
if __name__ == "__main__":
173+
unittest.main()

python/GafferSceneTest/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
from .CatalogueSelectTest import CatalogueSelectTest
196196
from .CameraQueryTest import CameraQueryTest
197197
from .GlobalsSanitiserTest import GlobalsSanitiserTest
198+
from .ReflectionConstraintTest import ReflectionConstraintTest
198199

199200
from .IECoreScenePreviewTest import *
200201
from .IECoreGLPreviewTest import *

0 commit comments

Comments
 (0)