Skip to content

Commit

Permalink
3Delight : Support Gaffer spline parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
ericmehl committed Sep 1, 2023
1 parent e317412 commit 29ac7eb
Show file tree
Hide file tree
Showing 6 changed files with 662 additions and 5 deletions.
1 change: 1 addition & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Features
- Added support for VDB volume objects.
- Added support for `vdbVolume` shader.
- Added support for `volumeshader` and `displacementshader` attributes.
- Added support for spline parameters in shaders.

API
---
Expand Down
53 changes: 53 additions & 0 deletions include/IECoreDelight/ShaderNetworkAlgo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//////////////////////////////////////////////////////////////////////////
//
// 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 "IECoreDelight/Export.h"

#include "IECoreScene/ShaderNetwork.h"

namespace IECoreDelight
{

namespace ShaderNetworkAlgo
{

IECOREDELIGHT_API IECoreScene::ShaderNetworkPtr preprocessedNetwork( const IECoreScene::ShaderNetwork *shaderNetwork );

} // namespace ShaderNetworkAlgo

} // namespace IECoreDelight
243 changes: 243 additions & 0 deletions python/IECoreDelightTest/RendererTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
from collections import deque
import shlex
import pathlib
import os
import subprocess

import imath

Expand Down Expand Up @@ -740,6 +742,235 @@ def testShaderAttributes( self ) :
self.assertEqual( volumeShader["Cs"], imath.Color3f( 0.5, 0.5, 0.5 ) )
self.assertEqual( displacementShader["Cs"], imath.Color3f( 1.0, 1.0, 1.0 ) )

def test3DelightSplineParameters( self ) :

# Converting from OSL parameters to Gaffer spline parameters is
# tested in GafferOSLTest.OSLShaderTest

r = GafferScene.Private.IECoreScenePreview.Renderer.create(
"3Delight",
GafferScene.Private.IECoreScenePreview.Renderer.RenderType.SceneDescription,
str( self.temporaryDirectory() / "test.nsi" ),
)

os.environ["OSL_SHADER_PATHS"] += os.pathsep + ( pathlib.Path( __file__ ).parent / "shaders" ).as_posix()

s = self.__compileShader( pathlib.Path( __file__ ).parent / "shaders" / "delightSplineParameters.osl" )

network = IECoreScene.ShaderNetwork(
shaders = {
"splineHandle" : IECoreScene.Shader(
s,
"osl:shader",
{
"floatSpline" : IECore.Splineff(
IECore.CubicBasisf.linear(),
[
( 0, 0.25 ),
( 0, 0.25 ),
( 1, 0.75 ),
( 1, 0.75 ),
]
),
"colorSpline" : IECore.SplinefColor3f(
IECore.CubicBasisf.bSpline(),
[
( 0, imath.Color3f( 0.25 ) ),
( 0, imath.Color3f( 0.25 ) ),
( 0, imath.Color3f( 0.25 ) ),
( 1, imath.Color3f( 0.75 ) ),
( 1, imath.Color3f( 0.75 ) ),
( 1, imath.Color3f( 0.75 ) ),
]
),
"dualInterpolationSpline" : IECore.Splineff(
IECore.CubicBasisf.linear(),
[
( 0, 0.25 ),
( 0, 0.25 ),
( 1, 0.75 ),
( 1, 0.75 ),
]
),
"trimmedFloatSpline" : IECore.Splineff(
IECore.CubicBasisf.catmullRom(),
[
( 0, 0.25 ),
( 0, 0.25 ),
( 1, 0.75 ),
( 1, 0.75 ),
]
),
"mayaSpline" : IECore.Splineff(
IECore.CubicBasisf.linear(),
[
( 0, 0.25 ),
( 0, 0.25 ),
( 1, 0.75 ),
( 1, 0.75 ),
]
),
"inconsistentNameSpline": IECore.Splineff(
IECore.CubicBasisf.bSpline(),
[
( 0, 0.25 ),
( 0, 0.25 ),
( 0, 0.25 ),
( 1, 0.75 ),
( 1, 0.75 ),
( 1, 0.75 ),
]
),
}
),
},
output = "splineHandle"
)

o = r.object(
"testPlane",
IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( -1 ), imath.V2f( 1 ) ) ),
r.attributes( IECore.CompoundObject( { "osl:surface" : network } ) )
)
del o

r.render()
del r

nsi = self.__parseDict( self.temporaryDirectory() / "test.nsi" )

shaders = { k: v for k, v in nsi.items() if nsi[k]["nodeType"] == "shader" }
self.assertEqual( len( shaders ), 1 )
shader = shaders[next( iter( shaders ) )]

# 3Delight gives defaults for linear splines as though they have a multiplicity of 2,
# whereas we expect a multiplicity of 1. These tests mirror 3Delight's convention.
# This results in two extra segments, with the first and last of zero length.
# In practice this seems to give correct results, so we leave it as-is rather than
# adding more edge-case handling.
self.assertEqual( shader["floatSpline_Knots"], [ 0, 0, 0, 1, 1, 1 ] )
self.assertEqual( shader["floatSpline_Floats"], [ 0.25, 0.25, 0.25, 0.75, 0.75, 0.75 ] )
self.assertEqual( shader["floatSpline_Interp"], [ 1, 1, 1, 1, 1, 1 ] )

self.assertNotIn( "floatSplinePositions", shader )
self.assertNotIn( "floatSplineValues", shader )
self.assertNotIn( "floatSplineBasis", shader )

self.assertEqual( shader["colorSpline_Knots"], [ 0, 0, 0, 1, 1, 1 ] )
self.assertEqual(
shader["colorSpline_Colors"],
[
imath.Color3f( 0.25, 0.25, 0.25 ),
imath.Color3f( 0.25, 0.25, 0.25 ),
imath.Color3f( 0.25, 0.25, 0.25 ),
imath.Color3f( 0.75, 0.75, 0.75 ),
imath.Color3f( 0.75, 0.75, 0.75 ),
imath.Color3f( 0.75, 0.75, 0.75 )
]
)
self.assertEqual( shader["colorSpline_Interp"], [ 3, 3, 3, 3, 3, 3 ] )

self.assertNotIn( "colorSplinePositions", shader )
self.assertNotIn( "colorSplineValues", shader )
self.assertNotIn( "colorSplineBasis", shader )

self.assertEqual( shader["dualInterpolationSpline_Knots"], [ 0, 0, 0, 1, 1, 1 ] )
self.assertEqual( shader["dualInterpolationSpline_Floats"], [ 0.25, 0.25, 0.25, 0.75, 0.75, 0.75 ] )
self.assertEqual( shader["dualInterpolationSpline_Interp"], [ 1, 1, 1, 1, 1, 1 ] )

self.assertNotIn( "dualInterpolationSplinePositions", shader )
self.assertNotIn( "dualInterpolationSplineValues", shader )
self.assertNotIn( "dualInterpolationSplineBasis", shader )

# Monotone cubic splines - tested here consistently with how 3Delight presents defaults -
# have a multiplicity of 1, resulting in a single segment.
self.assertEqual( shader["trimmedFloatSpline_Knots"], [ 0, 0, 1, 1 ] )
self.assertEqual( shader["trimmedFloatSpline_Floats"], [ 0.25, 0.25, 0.75, 0.75 ] )
self.assertEqual( shader["trimmedFloatSpline_Interp"], [ 3, 3, 3, 3 ] )

self.assertNotIn( "trimmedFloatSplinePositions", shader )
self.assertNotIn( "trimmedFloatSplineValues", shader )
self.assertNotIn( "trimmedFloatSplineBasis", shader )

self.assertEqual( shader["mayaSpline_Knots"], [ 0, 0, 0, 1, 1, 1 ] )
self.assertEqual( shader["mayaSpline_Floats"], [ 0.25, 0.25, 0.25, 0.75, 0.75, 0.75 ] )
self.assertEqual( shader["mayaSpline_Interp"], [ 1, 1, 1, 1, 1, 1 ] )

self.assertNotIn( "mayaSplinePositions", shader )
self.assertNotIn( "maysSplineValues", shader )
self.assertNotIn( "mayaSplineBasis", shader )

self.assertEqual( shader["inconsistentNameSpline_chaos"], [ 0, 0, 0, 1, 1, 1 ] )
self.assertEqual( shader["inconsistentNameSpline_moreChaos"], [ 0.25, 0.25, 0.25, 0.75, 0.75, 0.75 ] )
self.assertEqual( shader["inconsistentNameSpline_ahhh"], [ 3, 3, 3, 3, 3, 3 ] )

self.assertNotIn( "inconsistentNameSplinePositions", shader )
self.assertNotIn( "inconsistentNameSplineValues", shader )
self.assertNotIn( "inconsistentNameSplineBasis", shader )

def testGafferSplineParameters( self ) :

r = GafferScene.Private.IECoreScenePreview.Renderer.create(
"3Delight",
GafferScene.Private.IECoreScenePreview.Renderer.RenderType.SceneDescription,
str( self.temporaryDirectory() / "test.nsi" ),
)

network = IECoreScene.ShaderNetwork(
shaders = {
"splineHandle" : IECoreScene.Shader(
"Pattern/ColorSpline",
"osl:shader",
{
"spline" : IECore.SplinefColor3f(
IECore.CubicBasisf.linear(),
[
( 0, imath.Color3f( 1, 0, 0 ) ),
( 0, imath.Color3f( 1, 0, 0 ) ),
( 1, imath.Color3f( 0, 0, 1 ) ),
( 1, imath.Color3f( 0, 0, 1 ) ),
]
),
}
),
"constHandle" : IECoreScene.Shader( "Surface/Constant", "osl:surface", {} )
},
connections = [
( ( "splineHandle", "" ), ( "constHandle", "Cs" ) ),
],
output = "splineHandle"
)

o = r.object(
"testPlane",
IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( -1 ), imath.V2f( 1 ) ) ),
r.attributes( IECore.CompoundObject( { "osl:surface" : network } ) )
)
del o

r.render()
del r

nsi = self.__parseDict( self.temporaryDirectory() / "test.nsi" )

shaders = { k: v for k, v in nsi.items() if nsi[k]["nodeType"] == "shader" }
self.assertEqual( len( shaders ), 1 )
shader = shaders[next( iter( shaders ) )]

self.assertEqual( shader["splinePositions"], [ 0, 0, 0, 1, 1, 1 ] )
self.assertEqual(
shader["splineValues"],
[
imath.Color3f( 1, 0, 0 ),
imath.Color3f( 1, 0, 0 ),
imath.Color3f( 1, 0, 0 ),
imath.Color3f( 0, 0, 1 ),
imath.Color3f( 0, 0, 1 ),
imath.Color3f( 0, 0, 1 )
]
)
self.assertEqual( shader["splineBasis"], "linear" )

# Helper methods used to check that NSI files we write contain what we
# expect. The 3delight API only allows values to be set, not queried,
# so we build a simple dictionary-based node graph for now.
Expand Down Expand Up @@ -908,5 +1139,17 @@ def __assertNotInNSI( self, s, nsi ) :
# Continue search at next position
pos += 1

def __compileShader( self, sourceFileName ) :

outputFileName = self.temporaryDirectory() / pathlib.Path( sourceFileName ).with_suffix( ".oso" ).name

subprocess.check_call(
[ "oslc", "-q" ] +
[ "-I" + p for p in os.environ.get( "OSL_SHADER_PATHS", "" ).split( os.pathsep ) ] +
[ "-o", str( outputFileName ), str( sourceFileName ) ]
)

return outputFileName.with_suffix("").as_posix()

if __name__ == "__main__":
unittest.main()
72 changes: 72 additions & 0 deletions python/IECoreDelightTest/shaders/delightSplineParameters.osl
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.
//
//////////////////////////////////////////////////////////////////////////

shader delightSplineParameters
(

float floatSpline_Knots[] = { 0, 0, 1, 1 } [[ string widget = "null", string related_to_widget = "floatRamp" ]],
float floatSpline_Floats[] = { 0, 0, 1, 1 } [[ string widget = "floatRamp" ]],
int floatSpline_Interp[] = { 1, 1, 1, 1 } [[ string widget = "null", string related_to_widget = "floatRamp" ]],

float colorSpline_Knots[] = { 0, 0, 0, 1, 1, 1 } [[ string widget = "null", string related_to_widget = "colorRamp" ]],
color colorSpline_Colors[] = { 0, 0, 0, 1, 1, 1 } [[ string widget = "colorRamp" ]],
int colorSpline_Interp[] = { 3, 3, 3, 3, 3, 3 } [[ string widget = "null", string related_to_widget = "colorRamp" ]],

float dualInterpolationSpline_Knots[] = { 0, 0, 1, 1 } [[ string widget = "null", string related_to_widget = "floatRamp" ]],
float dualInterpolationSpline_Floats[] = { 0, 0, 1, 1 } [[ string widget = "floatRamp" ]],
string dualInterpolationSpline_Interpolation = "linear" [[ string widget = "null", string related_to_widget = "floatRamp" ]],
int dualInterpolationSpline_Interp[] = { -1 } [[ string widget = "null", string related_to_widget = "floatRamp" ]],

float trimmedFloatSpline_Floats[] = { 0, 1 } [[ string widget = "floatRamp" ]],
float trimmedFloatSpline_Knots[] = { 0, 1 } [[ string widget = "null", string related_to_widget = "floatRamp" ]],
int trimmedFloatSpline_Interp[] = { 3, 3 } [[ string widget = "null", string related_to_widget = "floatRamp" ]],

float mayaSpline_Knots[] = { 0, 0, 1, 1 } [[ string widget = "null", string related_to_widget = "maya_floatRamp" ]],
float mayaSpline_Floats[] = { 0, 0, 1, 1 } [[ string widget = "maya_floatRamp" ]],
int mayaSpline_Interp[] = { 1, 1, 1, 1 } [[ string widget = "null", string related_to_widget = "maya_floatRamp" ]],

float inconsistentNameSpline_chaos[] = { 0, 0, 0, 1, 1, 1 } [[ string widget = "null", string related_to_widget = "floatRamp" ]],
float inconsistentNameSpline_moreChaos[] = { 0, 0, 0, 1, 1, 1 } [[ string widget = "floatRamp" ]],
int inconsistentNameSpline_ahhh[] = { 2, 2, 2, 2 } [[ string widget = "null", string related_to_widget = "floatRamp" ]],

output closure color out = 0
)
{
float t = splineinverse( "bspline", v, colorSpline_Knots );
color c = spline( "bspline", t, colorSpline_Colors );
out = c * emission() + debug( "R", "type", "float", "value", color( c[0] ) );
Ci = out;
}
Loading

0 comments on commit 29ac7eb

Please sign in to comment.