Skip to content

Commit

Permalink
LightPositionTool : Add tool for placing shadows
Browse files Browse the repository at this point in the history
  • Loading branch information
ericmehl committed Nov 22, 2023
1 parent 9d1b54e commit ef6bade
Show file tree
Hide file tree
Showing 13 changed files with 782 additions and 15 deletions.
1 change: 1 addition & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------
Expand Down
11 changes: 11 additions & 0 deletions doc/source/Interface/ControlsAndShortcuts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###
Expand Down Expand Up @@ -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 ###

Expand Down
112 changes: 112 additions & 0 deletions include/GafferSceneUI/LightPositionTool.h
Original file line number Diff line number Diff line change
@@ -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>() );
~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<Imath::V3f> m_originalTranslation;
mutable std::optional<Imath::Eulerf> 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<Imath::V3f> m_shadowPivot;
std::optional<Imath::V3f> m_shadowPoint;

Gaffer::Signals::ScopedConnection m_contextChangedConnection;

static ToolDescription<LightPositionTool, SceneView> g_toolDescription;
static size_t g_firstPlugIndex;

};

IE_CORE_DECLAREPTR( LightPositionTool )

} // namespace GafferSceneUI
1 change: 1 addition & 0 deletions include/GafferSceneUI/TypeIds.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ enum TypeId
HistoryPathTypeId = 110664,
SetPathTypeId = 110665,
LightToolTypeId = 110666,
LightPositionToolTypeId = 110667,

LastTypeId = 110700
};
Expand Down
72 changes: 72 additions & 0 deletions python/GafferSceneUI/LightPositionToolUI.py
Original file line number Diff line number Diff line change
@@ -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")
1 change: 1 addition & 0 deletions python/GafferSceneUI/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
132 changes: 132 additions & 0 deletions python/GafferSceneUITest/LightPositionToolTest.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions python/GafferSceneUITest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
1 change: 1 addition & 0 deletions python/GafferUI/_StyleSheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions resources/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
'gafferSceneUIScaleTool',
'gafferSceneUITranslateTool',
'gafferSceneUILightTool',
'gafferSceneUILightPositionTool',
]

},
Expand Down
Loading

0 comments on commit ef6bade

Please sign in to comment.