diff --git a/Changes.md b/Changes.md index ed7a4344a30..e7d31c1c8c1 100644 --- a/Changes.md +++ b/Changes.md @@ -1,11 +1,21 @@ 1.3.x.x (relative to 1.3.7.0) ======= +Features +-------- + +- Viewer : Added "Snapshot To Catalogue" command to the right-click menu of the 3D view. + Fixes ----- - Windows `Scene/OpenGL/Shader` Menu : Removed `\` at the beginning of menu items. +API +--- + +- SceneGadget : Added `snapshotToFile()` method. + 1.3.7.0 (relative to 1.3.6.1) ======= diff --git a/SConstruct b/SConstruct index 86971382166..9a3eb646bd2 100644 --- a/SConstruct +++ b/SConstruct @@ -1105,7 +1105,7 @@ libraries = { "GafferSceneUI" : { "envAppends" : { - "LIBS" : [ "Gaffer", "GafferUI", "GafferImage", "GafferImageUI", "GafferScene", "Iex$IMATH_LIB_SUFFIX", "IECoreGL$CORTEX_LIB_SUFFIX", "IECoreImage$CORTEX_LIB_SUFFIX", "IECoreScene$CORTEX_LIB_SUFFIX" ], + "LIBS" : [ "Gaffer", "GafferUI", "GafferImage", "GafferImageUI", "GafferScene", "Iex$IMATH_LIB_SUFFIX", "IECoreGL$CORTEX_LIB_SUFFIX", "IECoreImage$CORTEX_LIB_SUFFIX", "IECoreScene$CORTEX_LIB_SUFFIX", "OpenImageIO$OIIO_LIB_SUFFIX", "OpenImageIO_Util$OIIO_LIB_SUFFIX" ], }, "pythonEnvAppends" : { "LIBS" : [ "IECoreGL$CORTEX_LIB_SUFFIX", "GafferBindings", "GafferScene", "GafferImage", "GafferUI", "GafferImageUI", "GafferSceneUI", "IECoreScene$CORTEX_LIB_SUFFIX" ], diff --git a/include/GafferSceneUI/Private/OutputBuffer.h b/include/GafferSceneUI/Private/OutputBuffer.h index 9c8b8ef956b..94bba165982 100644 --- a/include/GafferSceneUI/Private/OutputBuffer.h +++ b/include/GafferSceneUI/Private/OutputBuffer.h @@ -44,6 +44,7 @@ #include "IECore/PathMatcher.h" +#include #include namespace GafferSceneUI @@ -81,6 +82,13 @@ class OutputBuffer using BufferChangedSignal = Gaffer::Signals::Signal; BufferChangedSignal &bufferChangedSignal(); + /// See `SceneGadget::snapshotToFile()` for documentation. + void snapshotToFile( + const std::filesystem::path &fileName, + const Imath::Box2f &resolutionGate = Imath::Box2f(), + const IECore::CompoundData *metadata = nullptr + ); + private : class DisplayDriver; diff --git a/include/GafferSceneUI/SceneGadget.h b/include/GafferSceneUI/SceneGadget.h index 26fdbbc1ad3..bba05075bba 100644 --- a/include/GafferSceneUI/SceneGadget.h +++ b/include/GafferSceneUI/SceneGadget.h @@ -53,6 +53,8 @@ #include "IECoreGL/State.h" +#include + namespace GafferSceneUI { @@ -191,6 +193,17 @@ class GAFFERSCENEUI_API SceneGadget : public GafferUI::Gadget /// Implemented to return the name of the object under the mouse. std::string getToolTip( const IECore::LineSegment3f &line ) const override; + /// Saves a snapshot of the current rendered scene. All renderers are supported _except_ + /// the OpenGL renderer. All formats supported by OpenImageIO can be used. The output + /// display window will be set to `resolutionGate` if it is not an empty `Box2f`. + /// All of the supplied metadata will be written, regardless of conflicts with + /// OpenImageIO built-in metadata. + void snapshotToFile( + const std::filesystem::path &fileName, + const Imath::Box2f &resolutionGate = Imath::Box2f(), + const IECore::CompoundData *metadata = nullptr + ) const; + protected : void renderLayer( Layer layer, const GafferUI::Style *style, RenderReason reason ) const override; diff --git a/python/GafferSceneUI/SceneViewUI.py b/python/GafferSceneUI/SceneViewUI.py index 04a5834d69f..fe48cd7c5be 100644 --- a/python/GafferSceneUI/SceneViewUI.py +++ b/python/GafferSceneUI/SceneViewUI.py @@ -36,6 +36,8 @@ ########################################################################## import functools +import datetime +import pathlib import imath @@ -46,6 +48,7 @@ import GafferUI import GafferScene import GafferSceneUI +import GafferImage from ._SceneViewInspector import _SceneViewInspector @@ -1082,6 +1085,98 @@ def __appendClippingPlaneMenuItems( menuDefinition, prefix, view, parentWidget ) } ) +def __snapshotDescription( view ) : + + sceneGadget = view.viewportGadget().getPrimaryChild() + if sceneGadget.getRenderer() == "OpenGL" : + return "Viewport snapshots are only available for rendered (non-OpenGL) previews." + + return "Snapshot viewport and send to catalogue." + +def __snapshotToCatalogue( catalogue, view ) : + + timeStamp = str( datetime.datetime.now() ) + + scriptRoot = view["in"].getInput().ancestor( Gaffer.ScriptNode ) + + fileName = pathlib.Path( + scriptRoot.context().substitute( catalogue["directory"].getValue() ) + ) / ( f"viewerSnapshot-{ timeStamp.replace( ':', '-' ).replace(' ', '_' ) }.exr" ) + + resolutionGate = imath.Box3f() + if isinstance( view, GafferSceneUI.SceneView ) : + resolutionGate = view.resolutionGate() + + metadata = IECore.CompoundData( + { + "gaffer:sourceScene" : view["in"].getInput().relativeName( scriptRoot ), + "gaffer:context:frame" : view["in"].node().getContext().getFrame() + } + ) + + sceneGadget = view.viewportGadget().getPrimaryChild() + sceneGadget.snapshotToFile( fileName, resolutionGate, metadata ) + + image = GafferImage.Catalogue.Image( "Snapshot1", Gaffer.Plug.Direction.In, Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + image["fileName"].setValue( fileName ) + + image["description"].setValue( + "Snapshot of {} at frame {} taken at {}".format( + metadata["gaffer:sourceScene"], + metadata["gaffer:context:frame"], timeStamp[:-6] # Remove trailing microseconds + ) + ) + + catalogue["images"].source().addChild( image ) + catalogue["imageIndex"].source().setValue( len( catalogue["images"].source().children() ) - 1 ) + +def __snapshotCataloguesSubMenu( view, scriptNode ) : + + menuDefinition = IECore.MenuDefinition() + + catalogueList = list( GafferImage.Catalogue.RecursiveRange( scriptNode ) ) + + if len( catalogueList ) == 0 : + menuDefinition.append( + "/No Catalogues Available", + { + "active" : False, + } + ) + + else : + commonDescription = __snapshotDescription( view ) + commonActive = view["renderer"]["name"].getValue() != "OpenGL" + + for c in catalogueList : + + cName = c["name"].getValue() + nName = c["imageIndex"].source().node().relativeName( scriptNode ) + + snapshotActive = ( + commonActive and + not Gaffer.MetadataAlgo.readOnly( c["images"].source() ) and + not Gaffer.MetadataAlgo.readOnly( c["imageIndex"].source() ) + ) + + snapshotDescription = commonDescription + if Gaffer.MetadataAlgo.readOnly( c["images"].source() ) : + snapshotDescription = "\"images\" plug is read-only" + if Gaffer.MetadataAlgo.readOnly( c["imageIndex"].source() ) : + snapshotDescription = "\"imageIndex\" plug is read-only" + + menuDefinition.append( + "/" + ( nName + ( " ({})".format( cName ) if cName else "" ) ), + { + "active" : snapshotActive, + "command" : functools.partial( __snapshotToCatalogue, c, view ), + "description" : snapshotDescription + } + ) + + return menuDefinition + + def __viewContextMenu( viewer, view, menuDefinition ) : if not isinstance( view, GafferSceneUI.SceneView ) : @@ -1089,6 +1184,20 @@ def __viewContextMenu( viewer, view, menuDefinition ) : __appendClippingPlaneMenuItems( menuDefinition, "/Clipping Planes", view, viewer ) + scriptNode = view["in"].getInput().ancestor( Gaffer.ScriptNode ) + + menuDefinition.append( + "/Snapshot to Catalogue", + { + "subMenu" : functools.partial( + __snapshotCataloguesSubMenu, + view, + scriptNode + ) + } + ) + + GafferUI.Viewer.viewContextMenuSignal().connect( __viewContextMenu, scoped = False ) def __plugValueWidgetContextMenu( menuDefinition, plugValueWidget ) : diff --git a/src/GafferSceneUI/OutputBuffer.cpp b/src/GafferSceneUI/OutputBuffer.cpp index 7071966fcbf..cbf5122fcd6 100644 --- a/src/GafferSceneUI/OutputBuffer.cpp +++ b/src/GafferSceneUI/OutputBuffer.cpp @@ -37,6 +37,7 @@ #include "IECoreGL/ShaderLoader.h" #include "IECoreImage/DisplayDriver.h" +#include "IECoreImage/OpenImageIOAlgo.h" #include "OpenEXR/OpenEXRConfig.h" #if OPENEXR_VERSION_MAJOR < 3 @@ -45,6 +46,8 @@ #include "Imath/ImathBoxAlgo.h" #endif +#include "OpenImageIO/imageio.h" + #include "boost/lexical_cast.hpp" #include @@ -462,6 +465,46 @@ void OutputBuffer::dirtyTexture() } } +void OutputBuffer::snapshotToFile( + const std::filesystem::path &fileName, + const Box2f &resolutionGate, + const CompoundData *metadata +) +{ + std::filesystem::create_directories( fileName.parent_path() ); + + std::unique_lock lock( m_bufferReallocationMutex ); + + OIIO::ImageSpec spec( m_dataWindow.size().x + 1, m_dataWindow.size().y + 1, 4, OIIO::TypeDesc::HALF ); + + if( !resolutionGate.isEmpty() ) + { + spec.x = -resolutionGate.min.x; + spec.y = -resolutionGate.min.y; + + spec.full_x = 0; + spec.full_y = 0; + spec.full_width = resolutionGate.size().x; + spec.full_height = resolutionGate.size().y; + } + + const std::vector &rgbaBuffer = m_rgbaBuffer; + + for( const auto &[key, value] : metadata->readable() ) + { + const IECoreImage::OpenImageIOAlgo::DataView dataView( value.get() ); + if( dataView.data ) + { + spec.attribute( key.c_str(), dataView.type, dataView.data ); + } + } + + std::unique_ptr output = OIIO::ImageOutput::create( fileName.c_str() ); + output->open( fileName.c_str(), spec ); + output->write_image( OIIO::TypeDesc::FLOAT, &rgbaBuffer[0] ); + output->close(); +} + ////////////////////////////////////////////////////////////////////////// // DisplayDriver ////////////////////////////////////////////////////////////////////////// diff --git a/src/GafferSceneUI/SceneGadget.cpp b/src/GafferSceneUI/SceneGadget.cpp index 031ef4e25c0..68264f2fc9b 100644 --- a/src/GafferSceneUI/SceneGadget.cpp +++ b/src/GafferSceneUI/SceneGadget.cpp @@ -780,6 +780,20 @@ Imath::Box3f SceneGadget::bound() const return bound( /* selection = */ false ); } +void SceneGadget::snapshotToFile( + const std::filesystem::path &fileName, + const Box2f &resolutionGate, + const CompoundData *metadata +) const +{ + if( !m_outputBuffer ) + { + return; + } + + m_outputBuffer->snapshotToFile( fileName, resolutionGate, metadata ); +} + void SceneGadget::renderLayer( Layer layer, const GafferUI::Style *style, RenderReason reason ) const { assert( layer == m_layer || layer == Layer::MidFront ); diff --git a/src/GafferSceneUIModule/SceneGadgetBinding.cpp b/src/GafferSceneUIModule/SceneGadgetBinding.cpp index a0295a5bded..93a0fda0a07 100644 --- a/src/GafferSceneUIModule/SceneGadgetBinding.cpp +++ b/src/GafferSceneUIModule/SceneGadgetBinding.cpp @@ -179,6 +179,12 @@ Imath::Box3f bound( SceneGadget &g, bool selected, const IECore::PathMatcher *om return g.bound( selected, omitted ); } +void snapshotToFile( SceneGadget &g, const std::filesystem::path &fileName, const Imath::Box2f &resolutionGate, const IECore::CompoundData *metadata ) +{ + ScopedGILRelease gilRelease; + g.snapshotToFile( fileName, resolutionGate, metadata ); +} + } // namespace void GafferSceneUIModule::bindSceneGadget() @@ -214,6 +220,11 @@ void GafferSceneUIModule::bindSceneGadget() .def( "getSelection", &SceneGadget::getSelection, return_value_policy() ) .def( "selectionBound", &selectionBound ) .def( "bound", &bound, ( arg( "selected" ), arg( "omitted" ) = object() ) ) + .def( + "snapshotToFile", + &snapshotToFile, + ( arg( "fileName" ), arg( "resolutionGate" ) = Imath::Box2f(), arg( "metadata" ) = object() ) + ) ; enum_( "State" )