Skip to content

Commit

Permalink
RendererAlgo : Fix render options used for Capsules
Browse files Browse the repository at this point in the history
  • Loading branch information
johnhaddon committed Sep 25, 2023
1 parent c779007 commit 090b4da
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 8 deletions.
7 changes: 7 additions & 0 deletions include/GafferScene/Capsule.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#pragma once

#include "GafferScene/Private/IECoreScenePreview/Procedural.h"
#include "GafferScene/Private/RendererAlgo.h"
#include "GafferScene/ScenePlug.h"

#include "Gaffer/Context.h"
Expand Down Expand Up @@ -90,6 +91,11 @@ class GAFFERSCENE_API Capsule : public IECoreScenePreview::Procedural
const ScenePlug::ScenePath &root() const;
const Gaffer::Context *context() const;

/// Used to apply the correct render settings to the capsule before rendering it.
/// For internal use only.
void setRenderOptions( const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions );
std::optional<GafferScene::Private::RendererAlgo::RenderOptions> getRenderOptions() const;

private :

void throwIfNoScene() const;
Expand All @@ -106,6 +112,7 @@ class GAFFERSCENE_API Capsule : public IECoreScenePreview::Procedural
const ScenePlug *m_scene;
ScenePlug::ScenePath m_root;
Gaffer::ConstContextPtr m_context;
std::optional<GafferScene::Private::RendererAlgo::RenderOptions> m_renderOptions;

};

Expand Down
176 changes: 176 additions & 0 deletions python/GafferSceneTest/RendererAlgoTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@

import unittest

import imath

import IECore

import Gaffer
Expand Down Expand Up @@ -749,5 +751,179 @@ def assertIncludedObjects( scene, includedPurposes, paths ) :
}
)

def testCapsuleMotionBlur( self ) :

sphere = GafferScene.Sphere()
sphere["type"].setValue( sphere.Type.Primitive )
sphere["expression"] = Gaffer.Expression()
sphere["expression"].setExpression(
'parent["radius"] = context.getFrame() + 1; parent["transform"]["translate"]["x"] = context.getFrame()'
)

group = GafferScene.Group()
group["in"][0].setInput( sphere["out"] )

groupFilter = GafferScene.PathFilter()
groupFilter["paths"].setValue( IECore.StringVectorData( [ "/group" ] ) )

encapsulate = GafferScene.Encapsulate()
encapsulate["in"].setInput( group["out"] )
encapsulate["filter"].setInput( groupFilter["out"] )

camera = GafferScene.Camera()
camera["renderSettingOverrides"]["shutter"]["value"].setValue( imath.V2f( -0.5, 0.5 ) )

parent = GafferScene.Parent()
parent["in"].setInput( encapsulate["out"] )
parent["parent"].setValue( "/" )
parent["in"].setInput( encapsulate["out"] )
parent["children"][0].setInput( camera["out"] )

standardOptions = GafferScene.StandardOptions()
standardOptions["in"].setInput( parent["out"] )
standardOptions["options"]["transformBlur"]["enabled"].setValue( True )
standardOptions["options"]["deformationBlur"]["enabled"].setValue( True )
standardOptions["options"]["shutter"]["enabled"].setValue( True )
standardOptions["options"]["renderCamera"]["enabled"].setValue( True )
standardOptions["options"]["renderCamera"]["value"].setValue( "/camera" )

def assertExpectedMotion( scene ) :

# Render to capture Capsule. This will always contain only a single
# motion sample because procedurals themselves can't have motion samples
# (although their contents can).

renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer(
GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch
)
GafferScene.Private.RendererAlgo.outputObjects(
scene, GafferScene.Private.RendererAlgo.RenderOptions( scene ),
GafferScene.Private.RendererAlgo.RenderSets( scene ), GafferScene.Private.RendererAlgo.LightLinks(),
renderer
)

capsule = renderer.capturedObject( "/group" )
self.assertEqual( len( capsule.capturedSamples() ), 1 )
capsule = capsule.capturedSamples()[0]
self.assertIsInstance( capsule, GafferScene.Capsule )

# Render again to expand contents of Capsule.

capsuleRenderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer(
GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch
)
capsule.render( capsuleRenderer )

# Check transform blur of contents matches what was requested by the scene globals.

motionTimes = list( GafferScene.SceneAlgo.shutter( scene.globals(), scene ) )
sphere = capsuleRenderer.capturedObject( "/sphere" )
if scene.globals()["option:render:transformBlur"].value :
transformTimes = motionTimes
else :
transformTimes = [ Gaffer.Context.current().getFrame() ]

self.assertEqual( len( sphere.capturedTransforms() ), len( transformTimes ) )
self.assertEqual( sphere.capturedTransformTimes(), transformTimes if len( transformTimes ) > 1 else [] )
for index, time in enumerate( transformTimes ) :
with Gaffer.Context() as context :
context.setFrame( time )
self.assertEqual( sphere.capturedTransforms()[index], capsule.scene().transform( "/group/sphere" ) )

# Check deformation blur of contents matches what was requested by the scene globals.

if scene.globals()["option:render:deformationBlur"].value :
objectTimes = motionTimes
else :
objectTimes = [ Gaffer.Context.current().getFrame() ]

self.assertEqual( len( sphere.capturedSamples() ), len( objectTimes ) )
self.assertEqual( sphere.capturedSampleTimes(), objectTimes if len( objectTimes ) > 1 else [] )
for index, time in enumerate( objectTimes ) :
with Gaffer.Context() as context :
context.setFrame( time )
self.assertEqual( sphere.capturedSamples()[index].radius, capsule.scene().object( "/group/sphere" ).radius )

for frame in ( 0, 1 ) :
for deformation in ( False, True ) :
for transform in ( False, True ) :
for shutter in ( imath.V2f( -0.25, 0.25 ), imath.V2f( 0, 0.5 ) ) :
for overrideShutter in ( False, True ) :
with self.subTest( frame = frame, deformation = deformation, transform = transform, shutter = shutter, overrideShutter = overrideShutter ) :
standardOptions["options"]["transformBlur"]["value"].setValue( transform )
standardOptions["options"]["deformationBlur"]["value"].setValue( deformation )
standardOptions["options"]["shutter"]["value"].setValue( shutter )
camera["renderSettingOverrides"]["shutter"]["enabled"].setValue( overrideShutter )
with Gaffer.Context() as context :
context.setFrame( frame )
assertExpectedMotion( standardOptions["out"] )

def testCapsulePurposes( self ) :

rootFilter = GafferScene.PathFilter()
rootFilter["paths"].setValue( IECore.StringVectorData( [ "*" ] ) )

cube = GafferScene.Cube()

attributes = GafferScene.CustomAttributes()
attributes["in"].setInput( cube["out"] )
attributes["filter"].setInput( rootFilter["out"] )
attributes["attributes"].addChild( Gaffer.NameValuePlug( "usd:purpose", "${collect:rootName}" ) )

collect = GafferScene.CollectScenes()
collect["in"].setInput( attributes["out"] )
collect["rootNames"].setValue( IECore.StringVectorData( [ "default", "render", "proxy", "guide" ] ) )

group = GafferScene.Group()
group["in"][0].setInput( collect["out"] )

encapsulate = GafferScene.Encapsulate()
encapsulate["in"].setInput( group["out"] )
encapsulate["filter"].setInput( rootFilter["out"] )

standardOptions = GafferScene.StandardOptions()
standardOptions["in"].setInput( encapsulate["out"] )
standardOptions["options"]["includedPurposes"]["enabled"].setValue( True )

for includedPurposes in [
[ "default", "render" ],
[ "default", "proxy" ],
[ "default", "render", "proxy", "guide" ],
[ "default" ],
] :
with self.subTest( includedPurposes = includedPurposes ) :

standardOptions["options"]["includedPurposes"]["value"].setValue( IECore.StringVectorData( includedPurposes ) )

# Render to capture Capsule.

renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer(
GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch
)
GafferScene.Private.RendererAlgo.outputObjects(
standardOptions["out"], GafferScene.Private.RendererAlgo.RenderOptions( standardOptions["out"] ),
GafferScene.Private.RendererAlgo.RenderSets( standardOptions["out"] ), GafferScene.Private.RendererAlgo.LightLinks(),
renderer
)

capsule = renderer.capturedObject( "/group" )
capsule = capsule.capturedSamples()[0]
self.assertIsInstance( capsule, GafferScene.Capsule )

# Render again to expand contents of Capsule.

capsuleRenderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer(
GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch
)
capsule.render( capsuleRenderer )

# Check that only objects with the right purpose have been included.

for purpose in [ "default", "render", "proxy", "guide" ] :
if purpose in includedPurposes :
self.assertIsNotNone( capsuleRenderer.capturedObject( f"/{purpose}/cube" ) )
else :
self.assertIsNone( capsuleRenderer.capturedObject( f"/{purpose}/cube" ) )

if __name__ == "__main__":
unittest.main()
8 changes: 4 additions & 4 deletions python/GafferSceneUI/EncapsulateUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@
> Note : Encapsulation currently has some limitations
>
> - Motion blur options are taken from the globals at the
> point of Encapsulation, not the downstream globals
> at the point of rendering.
> - Motion blur attributes are not inherited - only
> attributes within the encapsulate hierarchy are
> attributes within the encapsulated hierarchy are
> considered.
> - The `usd:purpose` attribute is not inherited - only
> attributes withing the encapsulated hierarchy are
> considered.
""",

Expand Down
31 changes: 29 additions & 2 deletions src/GafferScene/Capsule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ void Capsule::hash( IECore::MurmurHash &h ) const

Procedural::hash( h );
h.append( m_hash );

if( m_renderOptions )
{
// Hash only what affects our rendering, not everything in
// `RenderOptions::globals`.
h.append( m_renderOptions->transformBlur );
h.append( m_renderOptions->deformationBlur );
h.append( m_renderOptions->shutter );
m_renderOptions->includedPurposes->hash( h );
}
}

void Capsule::copyFrom( const IECore::Object *other, IECore::Object::CopyContext *context )
Expand Down Expand Up @@ -147,9 +157,13 @@ void Capsule::render( IECoreScenePreview::Renderer *renderer ) const
{
throwIfNoScene();
ScenePlug::GlobalScope scope( m_context.get() );
GafferScene::Private::RendererAlgo::RenderOptions renderOptions( m_scene );
std::optional<GafferScene::Private::RendererAlgo::RenderOptions> renderOptions = m_renderOptions;
if( !renderOptions )
{
renderOptions = GafferScene::Private::RendererAlgo::RenderOptions( m_scene );
}
GafferScene::Private::RendererAlgo::RenderSets renderSets( m_scene );
GafferScene::Private::RendererAlgo::outputObjects( m_scene, renderOptions, renderSets, /* lightLinks = */ nullptr, renderer, m_root );
GafferScene::Private::RendererAlgo::outputObjects( m_scene, *renderOptions, renderSets, /* lightLinks = */ nullptr, renderer, m_root );
}

const ScenePlug *Capsule::scene() const
Expand All @@ -170,6 +184,19 @@ const Gaffer::Context *Capsule::context() const
return m_context.get();
}

void Capsule::setRenderOptions( const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions )
{
// This is not pretty, but it allows the capsule to render with the correct
// motion blur and `includedPurposes`, taken from the downstream node being
// rendered rather than from the capsule's own globals.
m_renderOptions = renderOptions;
}

std::optional<GafferScene::Private::RendererAlgo::RenderOptions> Capsule::getRenderOptions() const
{
return m_renderOptions;
}

void Capsule::throwIfNoScene() const
{
if( !m_scene )
Expand Down
18 changes: 16 additions & 2 deletions src/GafferScene/RendererAlgo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

#include "GafferScene/Private/RendererAlgo.h"

#include "GafferScene/Capsule.h"
#include "GafferScene/Private/IECoreScenePreview/Renderer.h"
#include "GafferScene/SceneAlgo.h"
#include "GafferScene/SceneProcessor.h"
Expand Down Expand Up @@ -1119,6 +1120,11 @@ struct LocationOutput

protected :

const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions() const
{
return m_options;
}

bool purposeIncluded() const
{
return m_options.purposeIncluded( m_attributes.get() );
Expand Down Expand Up @@ -1505,12 +1511,20 @@ struct ObjectOutput : public LocationOutput

IECoreScenePreview::Renderer::ObjectInterfacePtr objectInterface;
IECoreScenePreview::Renderer::AttributesInterfacePtr attributesInterface = this->attributesInterface();
if( !sampleTimes.size() )
if( samples.size() == 1 )
{
objectInterface = renderer()->object( name( path ), samples[0].get(), attributesInterface.get() );
ConstObjectPtr sample = samples[0];
if( auto capsule = runTimeCast<const Capsule>( sample.get() ) )
{
CapsulePtr capsuleCopy = capsule->copy();
capsuleCopy->setRenderOptions( renderOptions() );
sample = capsuleCopy;
}
objectInterface = renderer()->object( name( path ), sample.get(), attributesInterface.get() );
}
else
{
assert( sampleTimes.size() == samples.size() );
/// \todo Can we rejig things so this conversion isn't necessary?
vector<const Object *> objectsVector; objectsVector.reserve( samples.size() );
for( const auto &sample : samples )
Expand Down
11 changes: 11 additions & 0 deletions src/GafferSceneModule/HierarchyBinding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ using namespace GafferScene;
namespace
{

object getRenderOptionsWrapper( const Capsule &c )
{
if( auto o = c.getRenderOptions() )
{
return object( *o );
}
return object();
}

ScenePlugPtr scene( const Capsule &c )
{
return const_cast<ScenePlug *>( c.scene() );
Expand Down Expand Up @@ -102,6 +111,8 @@ void GafferSceneModule::bindHierarchy()
.def( "scene", &scene )
.def( "root", &root )
.def( "context", &context )
.def( "setRenderOptions", &Capsule::setRenderOptions )
.def( "getRenderOptions", &getRenderOptionsWrapper )
;

GafferBindings::DependencyNodeClass<Group>()
Expand Down

0 comments on commit 090b4da

Please sign in to comment.