-
Notifications
You must be signed in to change notification settings - Fork 124
ExamplesProceduralsHairShader
Ben Toogood edited this page Jun 13, 2013
·
3 revisions
An example of a simple hair system inside a procedural, complete with Marschner based hair shading. Sadly no shadows yet.
It demonstrates a couple of concepts:
- Parameter checksumming to improve interactive performance.
- Use of glsl shaders with textures.
- Use of IECore point distribution functions to seed hairs.
You may need to download:
and set $CORTEX_POINTDISTRIBUTION_TILESET accordingly, if you haven't already one so.
from __future__ import with_statement
from IECore import *
import random
class ShadedFurExample( ParameterisedProcedural ) :
def __init__( self ) :
ParameterisedProcedural.__init__( self )
self.parameters().addParameters(
[
## Connect your own mesh to grow hair on here, if you like.
ObjectParameter(
name = "mesh",
description = "The mesh to grow hair on, it should have well laid out UVs.",
defaultValue = MeshPrimitive.createPlane( Box2f( V2f( -2.0 ), V2f( 2.0 ) ) ),
types = [TypeId.MeshPrimitive]
),
## Add in some controls for the glsl shader parameters.
FloatParameter(
name = "diffuse",
description = "Strength of the 'fake' diffuse shading.",
defaultValue = 0.4,
minValue = 0.0,
maxValue = 1.0
),
FloatParameter(
name = "whiteSpec",
description = "The strength of the shading conrtribution from the 'R' component of the Marschner model.",
defaultValue = 2.5,
minValue = 0.0,
maxValue = 5.0
),
FloatParameter(
name = "backlight",
description = "The strength of the shading conrtribution from the 'TT' component of the Marschner model.",
defaultValue = 0.9,
minValue = 0.0,
maxValue = 5.0
),
FloatParameter(
name = "colouredSpec",
description = "The strength of the shading conrtribution from the 'TRT' component of the Marschner model.",
defaultValue = 1.0,
minValue = 0.0,
maxValue = 5.0
),
## We can take advantage of the MarschnerParameter to add in the controls for the base
## Spec model, which is used to generate the lookup tables for GPU shading.
MarschnerParameter( name = "marschnerParameters" ),
## Add some controls for hair generation.
CompoundParameter(
name = "hair",
members = [
FloatParameter(
name = "length",
description = "The length of the hair, in world units.",
defaultValue = 4.0
),
IntParameter(
name = "count",
description = "The numbe of hairs to generate.",
defaultValue = 5000
),
FloatParameter(
name = "frizz",
description = "The amount of frizz in the hair.",
defaultValue = 0.1,
minValue = 0.0,
maxValue = 1.0
),
FloatParameter(
name = "gravity",
description = "How strong gravity is.",
defaultValue = 1.0,
minValue = -1.0,
maxValue = 1.0
)
]
)
]
)
## Change the default colour for the hair in the marschner model.
self.parameters()["marschnerParameters"]["color"].setTypedValue( Color3f( 0.4, 0.2, 0.05 ) )
## We are going to cache our lookup tables, and the hair, so were
## not needlessly re-calculating things that haven't changed.
self.__hairChecksum = -1
self.__shadingChecksum = -1
def doBound( self, args ) :
## We don't actually know where the hair will be, but
## we do know it can't get any longer than a certain length
## so just expand the bounding box by that amount to be safe.
box = args["mesh"].bound()
box.min -= V3f( args["hair"]["length"].value );
box.max += V3f( args["hair"]["length"].value );
return box
def doRenderState( self, renderer, args ) :
pass
def doRender( self, renderer, args ) :
args["mesh"].render( renderer )
## See if we need to re-calculate the hair. The host layer should be
## keeping track on the update counts of parameters, so we'll use those
## as a checksum to see if anything relevant has been altered. If the
## checksum is different to last time, we need to re-calculate.
hairChecksum = 0
if "updateCount" in self.parameters()["hair"].userData() :
hairChecksum += self.parameters()["hair"].userData()["updateCount"].value
if "updateCount" in self.parameters()["mesh"].userData() :
hairChecksum += self.parameters()["mesh"].userData()["updateCount"].value
if hairChecksum != self.__hairChecksum :
self.hair = self.generateHair( args["mesh"], args["hair"] )
self.__hairChecksum = hairChecksum
## For now, set a very basic constant color on the hair, using the color set in
## the shading model.
self.hair["Cs"] = PrimitiveVariable( PrimitiveVariable.Interpolation.Constant, args["marschnerParameters"]["color"] )
## Using a scoped attribute block to make sure the any changes we make are contained.
## This isn't strictly necessary as the procedural implementation should preserve/restore
## state either side of the entry/exit into each individual procedural.
with AttributeBlock( renderer ) :
if renderer.typeName()=="IECoreGL::Renderer" :
# Set up how our hair will render
self.__setupGLRenderer( renderer )
# Set up our shading
self.__setupGLShader( renderer, args )
else :
renderer.setAttribute( "ri:dice:hair", FloatData( 1 ) )
# Render the hair we calculated earlier
self.hair.render( renderer )
## This simply sets up a couple of attributes to control how things will render
def __setupGLRenderer( self, renderer ):
renderer.setAttribute( "gl:curvesPrimitive:useGLLines", BoolData( True ) )
renderer.setAttribute( "gl:curvesPrimitive:ignoreBasis", BoolData( True ) )
## The draw states prevent the wireframe being drawn on top of the hair
## when selected, which obscures it.
renderer.setAttribute( "gl:primitive:wireframe", BoolData( False ) )
renderer.setAttribute( "gl:primitive:solid", BoolData( True ) )
def __setupGLShader( self, renderer, args ):
## Do the same checksumming we did with the hair for the Marschner parameters
## to allow us to re-use the lookup tables where possible.
shadingChecksum = 0
shadingParam = self.parameters()["marschnerParameters"]
if "updateCount" in shadingParam.userData() :
shadingChecksum = shadingParam.userData()["updateCount"].value
if shadingChecksum != self.__shadingChecksum :
## This will returns us an image, with all of the required channels for
## the lookup. We need to split these up into separate RGBA textures
## so we can send them to the graphics card.
lookup = MarschnerLookupTableOp()( model=args["marschnerParameters"] )
## Currently, there is no easy way to take an ImagePrimitive and set it as
## a shader parameter. There is however an implicit conversion in the GL renderer
## between a CompoundData object with the right hierarchy, and a gl texture.
## So, we need to take the channel data out of the image primitive, and build a
## a compound data representation, and feed that into the appropriate shader parameter.
dw = Box2iData( lookup.dataWindow )
tex1 = CompoundData()
tex2 = CompoundData()
tex3 = CompoundData()
tex1["displayWindow"] = dw
tex1["dataWindow"] = dw
tex2["displayWindow"] = dw
tex2["dataWindow"] = dw
tex3["displayWindow"] = dw
tex3["dataWindow"] = dw
tex1["channels"] = CompoundData()
tex2["channels"] = CompoundData()
tex3["channels"] = CompoundData()
tex1["channels"]["R"] = lookup["MR"].data
tex1["channels"]["G"] = lookup["MTT"].data
tex1["channels"]["B"] = lookup["MTRT"].data
tex1["channels"]["A"] = lookup["cosDiffTheta"].data
tex2["channels"]["R"] = lookup["NTT.r"].data
tex2["channels"]["G"] = lookup["NTT.g"].data
tex2["channels"]["B"] = lookup["NTT.b"].data
tex2["channels"]["A"] = lookup["NR"].data
tex3["channels"]["R"] = lookup["NTRT.r"].data
tex3["channels"]["G"] = lookup["NTRT.g"].data
tex3["channels"]["B"] = lookup["NTRT.b"].data
## Store these so we can re-use them later if we want to save some time
self.__lookup1 = tex1
self.__lookup2 = tex2
self.__lookup3 = tex3
self.__shadingChecksum = shadingChecksum
## We can now feed these image representations to the shader via its parameters
## These names need to match those declared in the vert/frag glsl shader.
shaderArgs = {
"lookupM" : self.__lookup1,
"lookupN" : self.__lookup2,
"lookupNTRT" : self.__lookup3,
"scaleR" : args["whiteSpec"],
"scaleTT" : args["backlight"],
"scaleTRT" : args["colouredSpec"],
"scaleDiffuse" : args["diffuse"],
"diffuseFalloff" : FloatData( 0.4 ),
"diffuseAzimuthFalloff" : FloatData( 0.4 )
}
## You may need to adjust this shader path, depending on your cortex installation.
renderer.shader( "surface", "IECoreGL/5/ieMarschnerHair", shaderArgs )
## A super-simple hair system, that simply grows along the normals,
## with a little gravity and frizz.
def generateHair( self, mesh, args ) :
## The evaluator needs a triangulated mesh
meshTri = TriangulateOp()( input=mesh, throwExceptions=False )
## This returns us a PointsPrimtive, which we are going to use as follicles
seeds = UniformRandomPointDistributionOp()( mesh=meshTri, numPoints=args["count"].value )
meshEvaluator = MeshPrimitiveEvaluator( meshTri )
result = meshEvaluator.createResult()
## These are going to hold the data for our CurvesPrimtive
p = V3fVectorData()
vpc = IntVectorData()
## We don't want the geometric normal in result.normal(), so we find this in advance.
if "N" not in meshTri :
meshTri = MeshNormalsOp()( input=meshTri )
normalsPrimVar = meshTri["N"]
## Make sure our frizz will be based on the same sequence of random numbers, though
## we aren't necessarily sure the follicle seeds will be in the same order...
random.seed( 0 )
## Work out the CVs for a hair for each point, and push them onto the p data array.
for i in range( seeds.numPoints ):
meshEvaluator.closestPoint( seeds["P"].data[i], result )
numCvs = self.__hairCVs( result.point(), result.vectorPrimVar( normalsPrimVar ), args, p )
vpc.append( numCvs )
curves = CurvesPrimitive( vpc, CubicBasisf.catmullRom(), False, p )
curves["constantwidth"] = PrimitiveVariable( PrimitiveVariable.Interpolation.Constant, FloatData( 0.05 ) )
## We need vTangents for the Marschner shading model.
curves = CurveTangentsOp()( input=curves )
return curves
def __hairCVs( self, origin, direction, args, pointsData ):
segments = 10
segLength = args["length"].value/float(segments+1)
growth = direction.normalized()
pointsData.append( origin )
lastP = origin
for i in range(segments+1):
# basic Gravity
growth += V3f( 0.0, -args["gravity"].value * max( 0.0, float(i+1)/float(segments) ), 0.0 )
thisP = lastP + growth.normalized() * segLength
lastP = thisP
# Basic frizz, we set lastP first, so we dont accumulate the drift.
# Note: This will potentially cause us to exceed our bounds...
thisP += V3f( random.random()-0.5, random.random()-0.5, random.random()-0.5 ) * args["frizz"].value
pointsData.append( thisP )
return segments + 2
You can create an instance of this procedural by pasting the above into the Maya Script Editor, and running:
import IECoreMaya
import maya.cmds
p = maya.cmds.createNode( "ieProceduralHolder" )
fp = IECoreMaya.FnProceduralHolder( p )
fp.setParameterised( ShadedFurExample() )
Create a Maya Point light, and turn on lighting with the 7 key.
You can also connect the wordMesh output of any mesh of your choosing to the parm_mesh attribute on the procedural, to grow hair on things other than the very exciting plane.
- Introduction
- General Functionality
- Data Handling
- Parameters and Ops
- Point Primitives
- Mesh Primitives
- Image Primitives
- Procedurals
- Cortex & Python
- Cortex & Maya
- Cortex & Houdini