From ef6bade9302062702762e46eaf2efbddab74adee Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 13 Nov 2023 17:28:31 -0500 Subject: [PATCH] LightPositionTool : Add tool for placing shadows --- Changes.md | 1 + .../Interface/ControlsAndShortcuts/index.md | 11 + include/GafferSceneUI/LightPositionTool.h | 112 ++++++ include/GafferSceneUI/TypeIds.h | 1 + python/GafferSceneUI/LightPositionToolUI.py | 72 ++++ python/GafferSceneUI/__init__.py | 1 + .../LightPositionToolTest.py | 132 +++++++ python/GafferSceneUITest/__init__.py | 1 + python/GafferUI/_StyleSheet.py | 1 + resources/graphics.py | 1 + resources/graphics.svg | 109 +++++- src/GafferSceneUI/LightPositionTool.cpp | 347 ++++++++++++++++++ src/GafferSceneUIModule/ToolBinding.cpp | 8 +- 13 files changed, 782 insertions(+), 15 deletions(-) create mode 100644 include/GafferSceneUI/LightPositionTool.h create mode 100644 python/GafferSceneUI/LightPositionToolUI.py create mode 100644 python/GafferSceneUITest/LightPositionToolTest.py create mode 100644 src/GafferSceneUI/LightPositionTool.cpp diff --git a/Changes.md b/Changes.md index 26d4bf1a9fa..66ead1b7461 100644 --- a/Changes.md +++ b/Changes.md @@ -9,6 +9,7 @@ Features - RenderPasses : Added a new node for appending render passes to the scene globals. - DeleteRenderPasses : Added a new node for removing render passes from the scene globals. - RenderPassWedge : Added a new node for causing upstream tasks to be dispatched in a range of contexts where the value of the `renderPass` context variable is varied based on the render pass names defined in the `renderPass:names` option. +- LightPositionTool : Added tool to the scene viewer to place shadows. With a light selected, Control + Left Click will set the pivot point used for casting a shadow. Shift + Left Click sets the point to receive the shadow. The light is repositioned to be the same distance from the pivot, along the pivot-shadow point line, and oriented to face the shadow point. Improvements ------------ diff --git a/doc/source/Interface/ControlsAndShortcuts/index.md b/doc/source/Interface/ControlsAndShortcuts/index.md index 2cb36ba0291..26733cc82bb 100644 --- a/doc/source/Interface/ControlsAndShortcuts/index.md +++ b/doc/source/Interface/ControlsAndShortcuts/index.md @@ -223,6 +223,7 @@ Cycle Transform Tool Orientation | {kbd}`O` Scale Tool | {kbd}`R` Camera Tool | {kbd}`T` Crop Window Tool | {kbd}`C` +Light Position Tool | {kbd}`D` Pin to numeric bookmark | {kbd}`1` … {kbd}`9` ### 3D scenes ### @@ -273,6 +274,16 @@ Action | Control or shortcut Adjust, fine precision | Hold {kbd}`Shift` during action Constrain to aspect ratio (Quad lights only) | Hold {kbd}`Ctrl` during action +### Light Position Tool ### + +> Note : +> For the following controls and shortcuts, the Light Position Tool must be active. + +Action | Control or shortcut +----------------------------------------------|-------------------- +Set shadow pivot position | {kbd}`Ctrl` + {{leftClick}} +Set shadow point | {kbd}`Shift` + {{leftClick}} + ### 2D images ### diff --git a/include/GafferSceneUI/LightPositionTool.h b/include/GafferSceneUI/LightPositionTool.h new file mode 100644 index 00000000000..1e3dcadd44b --- /dev/null +++ b/include/GafferSceneUI/LightPositionTool.h @@ -0,0 +1,112 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2023, 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 "GafferSceneUI/Export.h" +#include "GafferSceneUI/TransformTool.h" +#include "GafferSceneUI/TypeIds.h" + +#include "Gaffer/ScriptNode.h" + +namespace GafferSceneUI +{ + +IE_CORE_FORWARDDECLARE( SceneView ); + +class GAFFERSCENEUI_API LightPositionTool : public GafferSceneUI::TransformTool +{ + + public : + + LightPositionTool( SceneView *view, const std::string &name = defaultName() ); + ~LightPositionTool() override; + + GAFFER_NODE_DECLARE_TYPE( GafferSceneUI::LightPositionTool, LightPositionToolTypeId, TransformTool ); + + void position( const Imath::V3f &shadowPivot, const Imath::V3f &shadowPoint ); + + protected : + + bool affectsHandles( const Gaffer::Plug *input ) const override; + void updateHandles( float rasterScale ) override; + + private : + + struct TranslationRotation + { + + TranslationRotation( const Selection &selection ); + + void apply( const Imath::V3f &tranlation, const Imath::Eulerf &rotation ); + + private : + + Imath::V3f updatedRotateValue( const Gaffer::V3fPlug *rotatePlug, const Imath::Eulerf &rotation, Imath::V3f *currentValue = nullptr ) const; + + const Selection &m_selection; + Imath::M44f m_gadgetToTranslationXform; + Imath::M44f m_gadgetToRotationXform; + + mutable std::optional m_originalTranslation; + mutable std::optional m_originalRotation; // Radians + + }; + + void selectionChanged( const TransformTool &tool ); + void plugSet( Gaffer::Plug *plug ); + void plugInputChanged( Gaffer::Plug *Plug ); + void connectToViewContext(); + void contextChanged( const IECore::InternedString &name ); + + bool buttonPress( const GafferUI::ButtonEvent &event ); + + Imath::V3f m_worldCentroid; + Imath::Quatf m_centroidOrientation; + + std::optional m_shadowPivot; + std::optional m_shadowPoint; + + Gaffer::Signals::ScopedConnection m_contextChangedConnection; + + static ToolDescription g_toolDescription; + static size_t g_firstPlugIndex; + +}; + +IE_CORE_DECLAREPTR( LightPositionTool ) + +} // namespace GafferSceneUI \ No newline at end of file diff --git a/include/GafferSceneUI/TypeIds.h b/include/GafferSceneUI/TypeIds.h index 97027e1a8b6..c8058633528 100644 --- a/include/GafferSceneUI/TypeIds.h +++ b/include/GafferSceneUI/TypeIds.h @@ -57,6 +57,7 @@ enum TypeId HistoryPathTypeId = 110664, SetPathTypeId = 110665, LightToolTypeId = 110666, + LightPositionToolTypeId = 110667, LastTypeId = 110700 }; diff --git a/python/GafferSceneUI/LightPositionToolUI.py b/python/GafferSceneUI/LightPositionToolUI.py new file mode 100644 index 00000000000..2396579fb3a --- /dev/null +++ b/python/GafferSceneUI/LightPositionToolUI.py @@ -0,0 +1,72 @@ +########################################################################## +# +# Copyright (c) 2023, 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 Gaffer +import GafferUI +import GafferSceneUI + +Gaffer.Metadata.registerNode( + + GafferSceneUI.LightPositionTool, + + "description", + """ + Tool for placing lights. + """, + + "viewer:shortCut", "D", + "order", 7, + "tool:exclusive", True, + + "nodeToolbar:bottom:type", "GafferUI.StandardNodeToolbar.bottom", + + "toolbarLayout:customWidget:InteractionTipWidget:widgetType", "GafferSceneUI.LightPositionToolUI._InteractionTipWidget", + "toolbarLayout:customWidget:InteractionTipWidget:section", "Bottom", + "toolbarLayout:customWidget:InteractionTipWidget:index", -1, + +) + +class _InteractionTipWidget( GafferUI.Frame ) : + + def __init__( self, tool, **kw ) : + + GafferUI.Frame.__init__( self, borderWidth = 4, **kw ) + + with self : + with GafferUI.ListContainer( orientation = GafferUI.ListContainer.Orientation.Vertical, spacing = 4 ) : + GafferUI.Label( "Control + Left Click to place shadow pivot" ) + GafferUI.Label( "Shift + Left Click to place shadow target" ) + GafferUI.Label( "Deactivate tool to reset shadow and pivot points") diff --git a/python/GafferSceneUI/__init__.py b/python/GafferSceneUI/__init__.py index 952a41374f1..2237ded5ab8 100644 --- a/python/GafferSceneUI/__init__.py +++ b/python/GafferSceneUI/__init__.py @@ -190,6 +190,7 @@ from . import RenderPassesUI from . import DeleteRenderPassesUI from . import RenderPassWedgeUI +from . import LightPositionToolUI # then all the PathPreviewWidgets. note that the order # of import controls the order of display. diff --git a/python/GafferSceneUITest/LightPositionToolTest.py b/python/GafferSceneUITest/LightPositionToolTest.py new file mode 100644 index 00000000000..ce7ad806831 --- /dev/null +++ b/python/GafferSceneUITest/LightPositionToolTest.py @@ -0,0 +1,132 @@ +########################################################################## +# +# Copyright (c) 2023, 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 random +import math + +import imath + +import IECore + +import Gaffer +import GafferUITest +import GafferSceneUI +import GafferSceneTest + +class LightPositionToolTest( GafferUITest.TestCase ) : + + def __shadowSource( self, lightP, shadowPivot, shadowPoint ) : + return ( shadowPivot - shadowPoint ).normalize() * ( lightP - shadowPivot ).length() + shadowPivot + + def testPosition( self ) : + + random.seed( 42 ) + + script = Gaffer.ScriptNode() + script["light"] = GafferSceneTest.TestLight() + + view = GafferSceneUI.SceneView() + view["in"].setInput( script["light"]["out"] ) + GafferSceneUI.ContextAlgo.setSelectedPaths( view.getContext(), IECore.PathMatcher( [ "/light" ] ) ) + + tool = GafferSceneUI.LightPositionTool( view ) + tool["active"].setValue( True ) + + for i in range( 0, 5 ) : + lightP = imath.V3f( random.random() * 10 - 5, random.random() * 10 - 5, random.random() * 10 - 5 ) + shadowPivot = imath.V3f( random.random() * 10 - 5, random.random() * 10 - 5, random.random() * 10 - 5 ) + shadowPoint = imath.V3f( random.random() * 10 - 5, random.random() * 10 - 5, random.random() * 10 - 5 ) + + script["light"]["transform"]["translate"].setValue( lightP ) + tool.handlesTransform() # Trigger `updateHandles()` to update our centroid + + d0 = ( lightP - shadowPivot ).length() + upDir = script["light"]["transform"].matrix().multDirMatrix( imath.V3f( 0, 1, 0 ) ) + + tool.position( shadowPivot, shadowPoint ) + + p = script["light"]["transform"]["translate"].getValue() + + d1 = ( p - shadowPivot ).length() + self.assertAlmostEqual( d0, d1, places = 4 ) + + desiredP = self.__shadowSource( lightP, shadowPivot, shadowPoint ) + + for j in range( 0, 3 ) : + self.assertAlmostEqual( p[j], desiredP[j], places = 4 ) + + desiredO = imath.M44f() + imath.M44f.rotationMatrixWithUpDir( desiredO, imath.V3f( 0, 0, -1 ), shadowPoint - shadowPivot, upDir ) + rotationO = imath.V3f() + desiredO.extractEulerXYZ( rotationO ) + + o = script["light"]["transform"]["rotate"].getValue() + + for j in range( 0, 3 ) : + self.assertAlmostEqual( o[j] % 360, math.degrees( rotationO[j] ) % 360, places = 3 ) + + def testUndo( self ) : + + script = Gaffer.ScriptNode() + script["light"] = GafferSceneTest.TestLight() + + view = GafferSceneUI.SceneView() + view["in"].setInput( script["light"]["out"] ) + GafferSceneUI.ContextAlgo.setSelectedPaths( view.getContext(), IECore.PathMatcher( [ "/light" ] ) ) + + tool = GafferSceneUI.LightPositionTool( view ) + tool["active"].setValue( True ) + + tool.handlesTransform() # Trigger `updateHandles()` to update our centroid + + p = imath.V3f( 0, 0, 0 ) + shadowPivot = imath.V3f( 4, 5, 6 ) + shadowPoint = imath.V3f( 7, 8, 9 ) + tool.position( shadowPivot, shadowPoint ) + + newP = script["light"]["transform"]["translate"].getValue() + shadowSource = self.__shadowSource( p, shadowPivot, shadowPoint ) + for i in range( 0, 3 ) : + self.assertAlmostEqual( newP[i], shadowSource[i], places = 4 ) + + script.undo() + + self.assertEqual( script["light"]["transform"]["translate"].getValue(), imath.V3f( 0, 0, 0 ) ) + + +if __name__ == "__main__" : + unittest.main() \ No newline at end of file diff --git a/python/GafferSceneUITest/__init__.py b/python/GafferSceneUITest/__init__.py index 2b60aa25c8a..2438e69b5e0 100644 --- a/python/GafferSceneUITest/__init__.py +++ b/python/GafferSceneUITest/__init__.py @@ -59,6 +59,7 @@ from .SetMembershipInspectorTest import SetMembershipInspectorTest from .SetEditorTest import SetEditorTest from .LightToolTest import LightToolTest +from .LightPositionToolTest import LightPositionToolTest if __name__ == "__main__": unittest.main() diff --git a/python/GafferUI/_StyleSheet.py b/python/GafferUI/_StyleSheet.py index 00c3a97c6a8..ad49d5d092f 100644 --- a/python/GafferUI/_StyleSheet.py +++ b/python/GafferUI/_StyleSheet.py @@ -1492,6 +1492,7 @@ def styleColor( key ) : #gafferColorInspector, *[gafferClass="GafferSceneUI.TransformToolUI._SelectionWidget"], *[gafferClass="GafferSceneUI.CropWindowToolUI._StatusWidget"], + *[gafferClass="GafferSceneUI.LightPositionToolUI._InteractionTipWidget"], *[gafferClass="GafferUI.EditScopeUI.EditScopePlugValueWidget"] > QFrame, *[gafferClass="GafferSceneUI.InteractiveRenderUI._ViewRenderControlUI"] > QFrame, *[gafferClass="GafferSceneUI._SceneViewInspector"] > QFrame diff --git a/resources/graphics.py b/resources/graphics.py index ec6b7e6c45f..b3a6d64b169 100644 --- a/resources/graphics.py +++ b/resources/graphics.py @@ -195,6 +195,7 @@ 'gafferSceneUIScaleTool', 'gafferSceneUITranslateTool', 'gafferSceneUILightTool', + 'gafferSceneUILightPositionTool', ] }, diff --git a/resources/graphics.svg b/resources/graphics.svg index 2a955965939..409e7110b0c 100644 --- a/resources/graphics.svg +++ b/resources/graphics.svg @@ -277,17 +277,7 @@ inkscape:bbox-nodes="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" - inkscape:deskcolor="#4c4c4c" - showgrid="false" - inkscape:zoom="5.357041" - inkscape:cx="260.68496" - inkscape:cy="2570.2622" - inkscape:window-width="3747" - inkscape:window-height="2126" - inkscape:window-x="82" - inkscape:window-y="-11" - inkscape:window-maximized="1" - inkscape:current-layer="layer1"> + inkscape:deskcolor="#4c4c4c"> + + + + + + + + + + @@ -1599,7 +1639,8 @@ style="font-size:24px;fill:#f4f4f4;fill-opacity:1;stroke-width:1.00000381" id="rect8084-2" />SetEditor + id="flowPara8088-1">SetEditor + + + + + + + + diff --git a/src/GafferSceneUI/LightPositionTool.cpp b/src/GafferSceneUI/LightPositionTool.cpp new file mode 100644 index 00000000000..833ac5ff450 --- /dev/null +++ b/src/GafferSceneUI/LightPositionTool.cpp @@ -0,0 +1,347 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2023, 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. +// +////////////////////////////////////////////////////////////////////////// + +#include "GafferSceneUI/LightPositionTool.h" +#include "GafferSceneUI/SceneView.h" + +#include "GafferSceneUI/ContextAlgo.h" + +#include "Gaffer/MetadataAlgo.h" + +#include "IECore/AngleConversion.h" + +IECORE_PUSH_DEFAULT_VISIBILITY +#include "OpenEXR/OpenEXRConfig.h" +#if OPENEXR_VERSION_MAJOR < 3 +#include "OpenEXR/ImathEuler.h" +#include "OpenEXR/ImathMatrixAlgo.h" +#else +#include "Imath/ImathEuler.h" +#include "Imath/ImathMatrixAlgo.h" +#endif +IECORE_POP_DEFAULT_VISIBILITY + +#include "boost/algorithm/string/predicate.hpp" +#include "boost/bind/bind.hpp" + +#include "fmt/format.h" + +using namespace boost::placeholders; +using namespace Imath; +using namespace Gaffer; +using namespace GafferUI; +using namespace GafferScene; +using namespace GafferSceneUI; + +GAFFER_NODE_DEFINE_TYPE( LightPositionTool ); + +LightPositionTool::ToolDescription LightPositionTool::g_toolDescription; +size_t LightPositionTool::g_firstPlugIndex = 0; + +LightPositionTool::LightPositionTool( SceneView *view, const std::string &name ) : + TransformTool( view, name ), + m_shadowPivot( std::nullopt ), + m_shadowPoint( std::nullopt ) +{ + SceneGadget *sg = runTimeCast( this->view()->viewportGadget()->getPrimaryChild() ); + // We have to insert this before the underlying SelectionTool connections or it starts an object drag. + sg->buttonPressSignal().connectFront( boost::bind( &LightPositionTool::buttonPress, this, ::_2 ) ); + + plugSetSignal().connect( boost::bind( &LightPositionTool::plugSet, this, ::_1 ) ); + + connectToViewContext(); + view->contextChangedSignal().connect( boost::bind( &LightPositionTool::connectToViewContext, this ) ); + + view->plugInputChangedSignal().connect( boost::bind( &LightPositionTool::plugInputChanged, this, ::_1 ) ); + + selectionChangedSignal().connect( boost::bind( &LightPositionTool::selectionChanged, this, ::_1 ) ); + + storeIndexOfNextChild( g_firstPlugIndex ); +} + +LightPositionTool::~LightPositionTool() +{ +} + +void LightPositionTool::position( const V3f &shadowPivot, const V3f &shadowPoint ) +{ + const V3f newCentroid = + ( shadowPivot - shadowPoint ).normalized() * + ( m_worldCentroid - shadowPivot ).length() + + shadowPivot + ; + + const M44f centroidTransform = rotationMatrix( m_worldCentroid - shadowPivot, newCentroid - shadowPivot ); + + UndoScope undoScope( selection().back().editTarget()->ancestor() ); + + for( const auto &s : selection() ) + { + // See `RotateTool::buttonPress()` for a description of why we use this relatively + // elaborate orientation calculation. + Context::Scope scopedContext( s.context() ); + + ScenePlug::ScenePath parentPath( s.path() ); + parentPath.pop_back(); + + const M44f worldParentTransform = s.scene()->fullTransform( parentPath ); + const M44f worldParentTransformInverse = worldParentTransform.inverse(); + const M44f localTransform = s.scene()->transform( s.path() ); + + const M44f worldTransform = s.scene()->fullTransform( s.path() ); + + const V3f offset = ( + ( ( worldTransform.translation() - shadowPivot ) * centroidTransform ) + shadowPivot - + worldParentTransform.translation() - localTransform.translation() + ); + + V3f currentYAxis; + localTransform.multDirMatrix( V3f( 0.f, 1.f, 0.f ), currentYAxis ); + + // Point in the pivot-shadowPoint direction, in local space + const V3f targetZAxis = ( shadowPoint - shadowPivot ) * worldParentTransformInverse; + + M44f orientationMatrix = rotationMatrixWithUpDir( + V3f( 0.f, 0.f, -1.f ), targetZAxis, currentYAxis + ); + + V3f originalRotation; + extractEulerXYZ( localTransform, originalRotation ); + const M44f originalRotationMatrix = M44f().rotate( originalRotation ); + + const M44f relativeMatrix = originalRotationMatrix.inverse() * orientationMatrix; + + V3f relativeRotation; + extractEulerXYZ( relativeMatrix, relativeRotation ); + + TranslationRotation( s ).apply( offset, relativeRotation ); + } +} + +bool LightPositionTool::affectsHandles( const Gaffer::Plug *input ) const +{ + if( TransformTool::affectsHandles( input ) ) + { + return true; + } + + return input == scenePlug()->transformPlug(); +} + +void LightPositionTool::updateHandles( float rasterScale ) +{ + +} + +void LightPositionTool::selectionChanged( const TransformTool &tool ) +{ + if( activePlug()->getValue() ) + { + if( selection().empty() ) + { + return; + } + + m_worldCentroid = V3f( 0 ) * selection().back().orientedTransform( Orientation::World ); + } +} + +void LightPositionTool::plugSet( Plug *plug ) +{ + if( plug == activePlug() && !activePlug()->getValue() ) + { + m_shadowPivot = std::nullopt; + m_shadowPoint = std::nullopt; + } +} + +void LightPositionTool::plugInputChanged( Plug *plug ) +{ + if( activePlug()->getValue() && plug == view()->inPlug() ) + { + m_shadowPivot = std::nullopt; + m_shadowPoint = std::nullopt; + } +} + +bool LightPositionTool::buttonPress( const ButtonEvent &event ) +{ + if( + event.button != ButtonEvent::Left || + !activePlug()->getValue() || + !( + event.modifiers == ButtonEvent::Control || + event.modifiers == ButtonEvent::Shift + ) + ) + { + return false; + } + + if( !selectionEditable() ) + { + return true; + } + + ScenePlug::ScenePath scenePath; + V3f targetPos; + + const SceneGadget *sceneGadget = runTimeCast( view()->viewportGadget()->getPrimaryChild() ); + if( !sceneGadget->objectAt( event.line, scenePath, targetPos ) ) + { + return true; + } + + if( event.modifiers == ButtonEvent::Control ) + { + m_shadowPivot = targetPos * sceneGadget->fullTransform(); + } + else if( event.modifiers == ButtonEvent::Shift ) + { + m_shadowPoint = targetPos * sceneGadget->fullTransform(); + } + + if( !m_shadowPivot || !m_shadowPoint ) + { + return true; + } + + position( m_shadowPivot.value(), m_shadowPoint.value() ); + + return true; +} + + +void LightPositionTool::connectToViewContext() +{ + m_contextChangedConnection = view()->getContext()->changedSignal().connect( boost::bind( &LightPositionTool::contextChanged, this, ::_2 ) ); +} + +void LightPositionTool::contextChanged( const InternedString &name ) +{ + if( ContextAlgo::affectsSelectedPaths( name ) ) + { + m_shadowPivot = std::nullopt; + m_shadowPoint = std::nullopt; + } +} + +////////////////////////////////////////////////////////////////////////// +// LightPositionTool::TranslationRotation +////////////////////////////////////////////////////////////////////////// + +/// \todo These methods are either exactly the same as those in +/// `TranslateTool` and `RotateTool` or very close. Do they belong +/// in a `TransformToolAlgo` or something similar? + +LightPositionTool::TranslationRotation::TranslationRotation( const Selection &selection ) + : m_selection( selection ) +{ + const M44f handleRotationXform = selection.orientedTransform( Orientation::World ); + m_gadgetToRotationXform = handleRotationXform * selection.sceneToTransformSpace(); + + const M44f handleTranslateXform = selection.orientedTransform( Orientation::Parent ); + m_gadgetToTranslationXform = handleTranslateXform * selection.sceneToTransformSpace(); +} + +void LightPositionTool::TranslationRotation::apply( const V3f &translation, const Eulerf &rotation ) +{ + V3fPlug *translatePlug = m_selection.acquireTransformEdit()->translate.get(); + if( !m_originalTranslation ) + { + Context::Scope scopedContext( m_selection.context() ); + m_originalTranslation = translatePlug->getValue(); + } + + V3f offsetInTransformSpace; + m_gadgetToTranslationXform.multDirMatrix( translation, offsetInTransformSpace ); + + V3fPlug *rotatePlug = m_selection.acquireTransformEdit()->rotate.get(); + + const Imath::V3f e = updatedRotateValue( rotatePlug, rotation ); + for( int i = 0; i < 3; ++i ) + { + FloatPlug *pTranslate = translatePlug->getChild( i ); + if( canSetValueOrAddKey( pTranslate ) ) + { + setValueOrAddKey( pTranslate, m_selection.context()->getTime(), (*m_originalTranslation)[i] + offsetInTransformSpace[i] ); + } + + FloatPlug *pRotate = rotatePlug->getChild( i ); + if( canSetValueOrAddKey( pRotate ) ) + { + setValueOrAddKey( pRotate, m_selection.context()->getTime(), e[i] ); + } + } +} + +V3f LightPositionTool::TranslationRotation::updatedRotateValue( const V3fPlug *rotatePlug, const Eulerf &rotation, V3f *currentValue ) const +{ + // Duplicated verbatim from `RotateTool::Rotation::updatedRotateValue()` + + if( !m_originalRotation ) + { + Context::Scope scopedContext( m_selection.context() ); + m_originalRotation = degreesToRadians( rotatePlug->getValue() ); + } + + // Convert the rotation into the space of the + // upstream transform. + Quatf q = rotation.toQuat(); + V3f transformSpaceAxis; + m_gadgetToRotationXform.multDirMatrix( q.axis(), transformSpaceAxis ); + float d = Imath::sign( m_gadgetToRotationXform.determinant() ); + q.setAxisAngle( transformSpaceAxis, q.angle() * d ); + + // Compose it with the original. + + M44f m = q.toMatrix44(); + m.rotate( *m_originalRotation ); + + // Convert to the euler angles closest to + // those we currently have. + + const V3f current = rotatePlug->getValue(); + if( currentValue ) + { + *currentValue = current; + } + + Eulerf e; e.extract( m ); + e.makeNear( degreesToRadians( current ) ); + + return radiansToDegrees( V3f( e ) ); +} diff --git a/src/GafferSceneUIModule/ToolBinding.cpp b/src/GafferSceneUIModule/ToolBinding.cpp index a77d0dac0ce..2689583a49a 100644 --- a/src/GafferSceneUIModule/ToolBinding.cpp +++ b/src/GafferSceneUIModule/ToolBinding.cpp @@ -38,11 +38,12 @@ #include "GafferSceneUI/CameraTool.h" #include "GafferSceneUI/CropWindowTool.h" +#include "GafferSceneUI/LightPositionTool.h" +#include "GafferSceneUI/LightTool.h" #include "GafferSceneUI/RotateTool.h" #include "GafferSceneUI/ScaleTool.h" #include "GafferSceneUI/SceneView.h" #include "GafferSceneUI/SelectionTool.h" -#include "GafferSceneUI/LightTool.h" #include "GafferSceneUI/TransformTool.h" #include "GafferSceneUI/TranslateTool.h" @@ -268,4 +269,9 @@ void GafferSceneUIModule::bindTools() GafferBindings::SignalClass, SelectionChangedSlotCaller>( "SelectionChangedSignal" ); } + GafferBindings::NodeClass( nullptr, no_init ) + .def( init() ) + .def( "position", &LightPositionTool::position ) + ; + }