Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Improvements
- LightEditor : Added column for Arnold 7.4.4.0's new `sampling_mode` parameter.
- ArnoldShader : Moved Arnold 7.4.4.0's new `standard_hair.scattering_mode` parameter to the "Specular" section of the UI.
- ArnoldImager : Added activators for Arnold 7.4.4.0's new `lens_effects` imager parameters.
- RenderMan : Added overscan support.

1.6.7.0 (relative to 1.6.6.1)
=======
Expand Down
99 changes: 99 additions & 0 deletions python/IECoreRenderManTest/RendererTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import os
import time
import unittest
import random

import imath

Expand Down Expand Up @@ -2323,6 +2324,104 @@ def testCamera( self ) :
tolerance = 1e-7
)

def runOverscanTest( self, res, pixelsTop, pixelsBottom, pixelsLeft, pixelsRight ) :

with self.subTest(
res = res,
pixelsTop = pixelsTop, pixelsBottom = pixelsBottom,
pixelsLeft = pixelsLeft, pixelsRight = pixelsRight
) :

renderer = GafferScene.Private.IECoreScenePreview.Renderer.create(
self.renderer,
GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch
)

camera = IECoreScene.Camera()
camera.setResolution( res )
camera.setOverscan( True )

# We've got our own rounding in how renderRegion() is computed - to ensure we
# get the exact pixel amounts of overscan we're requesting, we offset by
# a quarter pixel.
camera.setOverscanTop( ( pixelsTop + 0.25 ) / res[1] )
camera.setOverscanBottom( ( pixelsBottom + 0.25 ) / res[1] )
camera.setOverscanLeft( ( pixelsLeft + 0.25 ) / res[0] )
camera.setOverscanRight( ( pixelsRight + 0.25 ) / res[0] )

self.assertEqual(
camera.renderRegion(),
imath.Box2i(
imath.V2i( -pixelsLeft, -pixelsBottom ),
res + imath.V2i( pixelsRight, pixelsTop )
)
)

renderCam = renderer.camera(
"camera", camera, renderer.attributes( IECore.CompoundObject() )
)

renderer.option( "camera", IECore.StringData( "camera" ) )

renderer.output(
f"test",
IECoreScene.Output(
str( self.temporaryDirectory() / f"test.exr" ),
"exr",
"rgb",
{}
)
)

renderer.render()
del renderer

image = OpenImageIO.ImageBuf( str( self.temporaryDirectory() / f"test.exr" ) )

self.assertEqual( image.spec().width, res[0] + pixelsLeft + pixelsRight )
self.assertEqual( image.spec().height, res[1] + pixelsTop + pixelsBottom )
self.assertEqual( image.spec().x, -pixelsLeft )
self.assertEqual( image.spec().y, -pixelsTop )

self.assertEqual( image.spec().full_width, res[0] )
self.assertEqual( image.spec().full_height, res[1] )
self.assertEqual( image.spec().full_x, 0 )
self.assertEqual( image.spec().full_y, 0 )

@unittest.skipIf( True, "Fuzzing the overscan values is too expensive to run regularly" )
def testFuzzOverscan( self ):
random.seed( 42 )
for i in range( 40 ):
self.runOverscanTest(
imath.V2i( random.randint( 100, 1000 ), random.randint( 100, 1000 ) ),
random.randint( 0, 500 ), random.randint( 0, 500 ),
random.randint( 0, 500 ), random.randint( 0, 500 )
)

# We can more efficiently check the rounding behaviours for much larger images by
# using test images that are one pixel tall or one pixel wide
for i in range( 1000 ):
self.runOverscanTest(
imath.V2i( random.randint( 100, 16000 ), 1 ),
0, 0,
random.randint( 0, 1000 ), random.randint( 0, 1000 )
)

for i in range( 1000 ):
self.runOverscanTest(
imath.V2i( 1, random.randint( 100, 16000 ) ),
random.randint( 0, 1000 ), random.randint( 0, 1000 ),
0, 0
)

def testOverscan( self ):
# Test some simple overscan values, and some that were specifically chosen to
# fail if a naive division without rounding compensation is used.
self.runOverscanTest( imath.V2i( 100, 100 ), 1, 2, 3, 4 )
self.runOverscanTest( imath.V2i( 200, 200 ), 14, 13, 12, 11 )
self.runOverscanTest( imath.V2i( 750, 750 ), 249, 249, 249, 249 )
self.runOverscanTest( imath.V2i( 162, 512 ), 745, 347, 819, 882 )

def __assertParameterEqual( self, paramList, name, data, tolerance = None ) :

p = next( x for x in paramList if x["info"]["name"] == name )
Expand Down
39 changes: 30 additions & 9 deletions src/IECoreRenderMan/Camera.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,31 @@ const RtUString g_projectionHandle( "projection" );
const RtUString g_pxrCamera( "PxrCamera" );
const RtUString g_pxrOrthographic( "PxrOrthographic" );

float divRoundDown( int a, int b )
{
// PRMan is going to perform a ceil rather than a round on these values when it converts to
// integers, so we need to make sure we return a value less than the precise value. There
// are two sources of imprecision here:
// * converting integers to floats
// * performing the division
// If the arguments are less than 16 million, the first is not an issue, since those integers
// are exactly representable. We don't expect images to be that big, so we ignore the first issue.
//
// This just leaves any imprecision created by the division itself. The floating point result
// should be the closest representable float, so a single call to nextafterf to pick the next
// lowest float should ensure that we are always less than the true value, and PRMan's ceil
// should pick the correct integer. ( This is probably also reliant on the fact that when
// PRMan performs the multiplication by resolution, the resolution is also an integer less
// than 16 million. )

return nextafterf(
float( a ) / float( b ),
-std::numeric_limits<float>::infinity()
);
}

} // namespace

/// \todo Overscan, depth of field
Camera::Camera( const std::string &name, const IECoreScene::Camera *camera, Session *session )
: m_session( session )
{
Expand Down Expand Up @@ -130,14 +152,13 @@ Camera::Camera( const std::string &name, const IECoreScene::Camera *camera, Sess
options.SetIntegerArray( Loader::strings().k_Ri_FormatResolution, resolution.getValue(), 2 );
options.SetFloat( Loader::strings().k_Ri_FormatPixelAspectRatio, camera->getPixelAspectRatio() );

Box2f cropWindow = camera->getCropWindow();
if( cropWindow.isEmpty() )
{
/// \todo Would be better if IECoreScene::Camera defaulted to this rather
/// than empty box.
cropWindow = Box2f( V2f( 0 ), V2f( 1 ) );
}
float renderManCropWindow[4] = { cropWindow.min.x, cropWindow.max.x, cropWindow.min.y, cropWindow.max.y };
Imath::Box2i renderRegion = camera->renderRegion();
float renderManCropWindow[4] = {
divRoundDown( renderRegion.min.x, resolution.x ),
divRoundDown( renderRegion.max.x, resolution.x ),
divRoundDown( resolution.y - renderRegion.min.y, resolution.y ),
divRoundDown( resolution.y - renderRegion.max.y, resolution.y )
};
options.SetFloatArray( Loader::strings().k_Ri_CropWindow, renderManCropWindow, 4 );

// Camera
Expand Down
Loading
Loading