Alright, was a little harder than I expected. I ended up with a LightCuller class. You instantiate it by passing it a list of lights, and then you call it’s start method with a distance (which is only used for pointlights).
It only supports spotlights and pointlights. For spotlights it:
[ul]
[li]Checks to see if the spotlight is in the mesh’s bounding box
[/li][li]Checks to see if the center of the mesh’s bounding box is in the spotlight’s cone
[/li][li]Checks to see if any vertex of the mesh’s bounding box is in the spotlight’s cone
[/li][li]Checks to see if the light’s frustum intersects with any part of the mesh’s bounding box
[/li][/ul]
For pointlights it checks the distance between the pointlight and the mesh’s bounding sphere, and compares it to a completely arbitrary distance you have to manually pass in. Hackish, but enough for my purposes.
It has some error checking. It also displays a progress bar, and can be cancelled by hitting escape, which is kind of good since running this on a big scene is an overnight sort of job.
Hope someone finds it interesting (or at least entertaining; I’m pretty rusty with Python, bad at vector math, and new to Maya/pymel, so the best I can say about my code is that it mostly seems to work).
Note 1: If you run it with debug = True, it’ll spit out a bunch of debugging statements as it goes, but it will NOT actually do any lightlinking. If you run it with debug = False it won’t say anything, but it’ll wipe out all existing lightlinking for the lights you run it on.
Note 2: Works with Maya 2011. No promises about earlier or later versions.
Note 3: In the rather unlikely event that anyone cares, the script is released under under a [Creative Commons Attribution 3.0 License](Creative Commons Attribution 3.0 License).
Note 4: If anyone has any comments or suggestions, I’d love to hear them. As I said, knew to this area.
Part 1 - the LightCuller class:
################################################################
# Link selected lights to objects in light frustum #
# #
# Copyright 2010 Thaumaturgy Studios Ltd #
# http://www.thaumaturgy.co.nz #
# info@thaumaturgy.co.nz #
################################################################
[b]from[/b] pymel[b].[/b]core [b]import[/b] [b]*[/b]
[b]import[/b] maya[b].[/b]cmds [b]as[/b] cmds
[b]import[/b] maya[b].[/b]mel [b]as[/b] mel
[b]import[/b] math
[b]import[/b] pymel[b].[/b]core[b].[/b]datatypes [b]as[/b] dt
[b]import[/b] logging
[b]from[/b] pymel[b].[/b]core[b].[/b]uitypes [b]import[/b] ProgressBar
debug [b]=[/b] [b]False[/b]
logger [b]=[/b] logging[b].[/b]getLogger[b]([/b]"lightCulling"[b])[/b]
[b]if[/b] debug[b]:[/b]
logger[b].[/b]setLevel[b]([/b]logging[b].[/b]DEBUG[b])[/b]
[b]else[/b][b]:[/b]
logger[b].[/b]setLevel[b]([/b]logging[b].[/b]INFO[b])[/b]
[b]class[/b] Plane[b]:[/b]
[b]def[/b] __init__[b]([/b]self[b],[/b] point[b],[/b] normal[b],[/b] name[b]):[/b]
self[b].[/b]normal [b]=[/b] normal # This is the normal vector
self[b].[/b]point [b]=[/b] point # This is a point on the plane
self[b].[/b]name [b]=[/b] name
[b]class[/b] Frustum[b]:[/b]
[b]def[/b] __init__[b]([/b]self[b],[/b] lightPos[b],[/b] lightQ[b],[/b] lightCone[b]):[/b]
theta [b]=[/b] math[b].[/b]radians[b]([/b]lightCone[b])[/b]
self[b].[/b]planes [b]= [][/b]
vectors [b]=[/b] [b]list[/b][b]()[/b]
vectors[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b][b]0[/b][b],[/b][b]0[/b][b],-[/b][b]1[/b][b]).[/b]rotateBy[b]([/b]dt[b].[/b]Quaternion[b]().[/b]setToYAxis[b]([/b]theta[b]) *[/b] dt[b].[/b]Quaternion[b]().[/b]setToXAxis[b]([/b]theta[b]) *[/b] lightQ[b]))[/b]
vectors[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b][b]0[/b][b],[/b][b]0[/b][b],-[/b][b]1[/b][b]).[/b]rotateBy[b]([/b]dt[b].[/b]Quaternion[b]().[/b]setToYAxis[b](-[/b]theta[b]) *[/b] dt[b].[/b]Quaternion[b]().[/b]setToXAxis[b]([/b]theta[b]) *[/b] lightQ[b]))[/b]
vectors[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b][b]0[/b][b],[/b][b]0[/b][b],-[/b][b]1[/b][b]).[/b]rotateBy[b]([/b]dt[b].[/b]Quaternion[b]().[/b]setToYAxis[b]([/b]theta[b]) *[/b] dt[b].[/b]Quaternion[b]().[/b]setToXAxis[b](-[/b]theta[b]) *[/b] lightQ[b]))[/b]
vectors[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b][b]0[/b][b],[/b][b]0[/b][b],-[/b][b]1[/b][b]).[/b]rotateBy[b]([/b]dt[b].[/b]Quaternion[b]().[/b]setToYAxis[b](-[/b]theta[b]) *[/b] dt[b].[/b]Quaternion[b]().[/b]setToXAxis[b](-[/b]theta[b]) *[/b] lightQ[b]))[/b]
self[b].[/b]planes[b].[/b]append[b]([/b]Plane[b]([/b]lightPos[b],[/b]vectors[b][[/b][b]1[/b][b]].[/b]cross[b]([/b]vectors[b][[/b][b]0[/b][b]]).[/b]normal[b](),[/b] "top"[b]))[/b]
self[b].[/b]planes[b].[/b]append[b]([/b]Plane[b]([/b]lightPos[b],[/b]vectors[b][[/b][b]3[/b][b]].[/b]cross[b]([/b]vectors[b][[/b][b]1[/b][b]]).[/b]normal[b](),[/b] "right"[b]))[/b]
self[b].[/b]planes[b].[/b]append[b]([/b]Plane[b]([/b]lightPos[b],[/b]vectors[b][[/b][b]2[/b][b]].[/b]cross[b]([/b]vectors[b][[/b][b]3[/b][b]]).[/b]normal[b](),[/b] "bot"[b]))[/b]
self[b].[/b]planes[b].[/b]append[b]([/b]Plane[b]([/b]lightPos[b],[/b]vectors[b][[/b][b]0[/b][b]].[/b]cross[b]([/b]vectors[b][[/b][b]2[/b][b]]).[/b]normal[b](),[/b] "left"[b]))[/b]
[b]def[/b] checkBox[b]([/b]self[b],[/b] boxCorners[b]):[/b]
[b]for[/b] plane [b]in[/b] self[b].[/b]planes[b]:[/b]
numBehind [b]=[/b] [b]0[/b]
[b]for[/b] corner [b]in[/b] boxCorners[b]:[/b]
[b]if[/b] [b]([/b]corner [b]-[/b] plane[b].[/b]point[b]).[/b]dot[b]([/b]plane[b].[/b]normal[b]) >[/b] [b]0[/b][b]:[/b]
numBehind [b]+=[/b] [b]1[/b]
logger[b].[/b]debug[b]([/b]"Num points behind plane " [b]+[/b] plane[b].[/b]name [b]+[/b] ": " [b]+[/b] [b]str[/b][b]([/b]numBehind[b]))[/b]
[b]if[/b] numBehind [b]==[/b] [b]len[/b][b]([/b]boxCorners[b]):[/b]
[b]return False[/b]
[b]return True[/b]
[b]class[/b] LightCuller[b]:[/b]
[b]def[/b] __init__[b]([/b]self[b],[/b] lightList[b]):[/b]
self[b].[/b]llData [b]= [][/b]
self[b].[/b]lightList [b]=[/b] lightList
self[b].[/b]meshList [b]=[/b] [b]filter[/b][b]([/b]self[b].[/b]checkVis[b],[/b] ls[b]([/b][b]type[/b][b]=[/b]'mesh'[b]))[/b]
[b]if[/b] [b]len[/b][b]([/b]self[b].[/b]meshList[b]) ==[/b] [b]0[/b][b]:[/b]
logger[b].[/b]critical[b]([/b]"You have no unhidden meshes in the scene!"[b])[/b]
sys[b].[/b]exit[b]()[/b]
self[b].[/b]initProgress[b]()[/b]
[b]def[/b] start[b]([/b]self[b],[/b]maxDist[b]=[/b][b]0[/b][b]):[/b]
self[b].[/b]maxDistance [b]=[/b] maxDist
[b]for[/b] light [b]in[/b] self[b].[/b]lightList[b]:[/b]
self[b].[/b]process[b]([/b]light[b])[/b]
[b]if[/b] debug[b]:[/b]
[b]for[/b] light[b],[/b] mesh [b]in[/b] [b]list[/b][b]([/b][b]set[/b][b]([/b]lightc[b].[/b]llData[b])):[/b]
[b]print[/b] light[b].[/b]name[b](),[/b] '->'[b],[/b] mesh[b].[/b]name[b]()[/b]
[b]else[/b][b]:[/b]
lightlink[b]([/b]ual[b]=[/b][b]True[/b][b],[/b] b[b]=[/b][b]True[/b][b],[/b] o[b]=[/b]ls[b]([/b][b]type[/b][b]=[/b]'transform'[b]))[/b] # Breaks all light links
[b]for[/b] light[b],[/b] mesh [b]in[/b] [b]list[/b][b]([/b][b]set[/b][b]([/b]lightc[b].[/b]llData[b])):[/b]
lightlink[b]([/b]l[b]=[/b]light[b],[/b] o[b]=[/b]mesh[b],[/b] m[b]=[/b][b]True[/b][b])[/b]
[b]def[/b] process[b]([/b]self[b],[/b] light[b]):[/b]
lightPos [b]=[/b] light[b].[/b]getTranslation[b]([/b]space[b]=[/b]'world'[b])[/b]
self[b].[/b]startProgress[b]([/b][b]len[/b][b]([/b]self[b].[/b]meshList[b]))[/b]
[b]if[/b] light[b].[/b]getShape[b]().[/b]nodeType[b]() ==[/b] 'spotLight'[b]:[/b]
lightCone [b]=[/b] self[b].[/b]getLightCone[b]([/b]light[b].[/b]getShape[b]())[/b]
lightVec [b]=[/b] dt[b].[/b]Vector[b]([/b][b]0[/b][b],[/b][b]0[/b][b],[/b][b]1[/b][b]).[/b]rotateBy[b]([/b]light[b].[/b]getRotation[b]([/b]space[b]=[/b]'world'[b]))[/b]
lightPos [b]=[/b] light[b].[/b]getTranslation[b]()[/b]
lightQ [b]=[/b] dt[b].[/b]Quaternion[b]([/b]light[b].[/b]getTransformation[b]().[/b]getRotationQuaternion[b]())[/b]
frustum [b]=[/b] Frustum[b]([/b]lightPos[b],[/b] lightQ[b],[/b] lightCone[b])[/b]
[b]for[/b] mesh [b]in[/b] self[b].[/b]meshList[b]:[/b]
logger[b].[/b]debug[b]([/b]"Processing " [b]+[/b] mesh[b].[/b]name[b]())[/b]
box [b]=[/b] mesh[b].[/b]getParent[b]().[/b]getBoundingBox[b]([/b]space[b]=[/b]'world'[b])[/b]
boxCorners [b]=[/b] self[b].[/b]findCorners[b]([/b]box[b])[/b]
self[b].[/b]setProgress[b]([/b]'Processing ' [b]+[/b] light[b].[/b]name[b]() +[/b] ' - ' [b]+[/b] mesh[b].[/b]getParent[b]().[/b]name[b]())[/b]
self[b].[/b]checkCancelled[b]()[/b]
[b]if[/b] light[b].[/b]getShape[b]().[/b]nodeType[b]() ==[/b] 'spotLight'[b]:[/b]
# Does the box contain the cone?
[b]if[/b] box[b].[/b]contains[b]([/b]lightPos[b]):[/b]
logger[b].[/b]debug[b]([/b]light[b].[/b]name[b]() +[/b] " is inside bounding box of " [b]+[/b] mesh[b].[/b]getParent[b]().[/b]name[b]())[/b]
self[b].[/b]llData[b].[/b]append[b](([/b]light[b],[/b]mesh[b].[/b]getParent[b]()))[/b]
# Does the cone contain the center of the box?
[b]elif[/b] lightCone [b]>=[/b] self[b].[/b]calcMinAngle[b]([/b]lightVec[b],[/b] lightPos[b], [[/b]box[b].[/b]center[b]()]):[/b]
logger[b].[/b]debug[b]([/b]light[b].[/b]name[b]() +[/b] "'s cone contains the center point of " [b]+[/b] mesh[b].[/b]getParent[b]().[/b]name[b]())[/b]
self[b].[/b]llData[b].[/b]append[b](([/b]light[b],[/b]mesh[b].[/b]getParent[b]()))[/b]
# Does the cone contain a vertex of the box?
[b]elif[/b] lightCone [b]>=[/b] self[b].[/b]calcMinAngle[b]([/b]lightVec[b],[/b] lightPos[b],[/b] boxCorners[b]):[/b]
logger[b].[/b]debug[b]([/b]light[b].[/b]name[b]() +[/b] "'s cone contains a vertex of " [b]+[/b] mesh[b].[/b]getParent[b]().[/b]name[b]())[/b]
self[b].[/b]llData[b].[/b]append[b](([/b]light[b],[/b]mesh[b].[/b]getParent[b]()))[/b]
# Is the frustrum surrounded by points?
[b]elif[/b] frustum[b].[/b]checkBox[b]([/b]boxCorners[b]):[/b]
logger[b].[/b]debug[b]([/b]light[b].[/b]name[b]() +[/b] "'s frustum is inside " [b]+[/b] mesh[b].[/b]getParent[b]().[/b]name[b]())[/b]
self[b].[/b]llData[b].[/b]append[b](([/b]light[b],[/b]mesh[b].[/b]getParent[b]()))[/b]
[b]else[/b][b]:[/b]
# Is distance outside our check distance?
[b]if[/b] self[b].[/b]maxDistance [b]>= ([/b]lightPos [b]-[/b] box[b].[/b]center[b]()).[/b]length[b]() -[/b] self[b].[/b]calcBoundingRadius[b]([/b]boxCorners[b]):[/b]
logger[b].[/b]debug[b]([/b]light[b].[/b]name[b]() +[/b] "is close enough to " [b]+[/b] mesh[b].[/b]getParent[b]().[/b]name[b]())[/b]
self[b].[/b]llData[b].[/b]append[b](([/b]light[b],[/b]mesh[b].[/b]getParent[b]()))[/b]
self[b].[/b]stepProgress[b]()[/b]
self[b].[/b]close[b]()[/b]
[b]def[/b] findCorners[b]([/b]self[b],[/b]mesh[b]):[/b]
bbox [b]= [][/b]
bbox[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b]mesh[b].[/b][b]min[/b][b]().[/b]x[b],[/b] mesh[b].[/b][b]min[/b][b]().[/b]y[b],[/b] mesh[b].[/b][b]min[/b][b]().[/b]z[b]))[/b]
bbox[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b]mesh[b].[/b][b]max[/b][b]().[/b]x[b],[/b] mesh[b].[/b][b]min[/b][b]().[/b]y[b],[/b] mesh[b].[/b][b]min[/b][b]().[/b]z[b]))[/b]
bbox[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b]mesh[b].[/b][b]min[/b][b]().[/b]x[b],[/b] mesh[b].[/b][b]max[/b][b]().[/b]y[b],[/b] mesh[b].[/b][b]min[/b][b]().[/b]z[b]))[/b]
bbox[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b]mesh[b].[/b][b]max[/b][b]().[/b]x[b],[/b] mesh[b].[/b][b]max[/b][b]().[/b]y[b],[/b] mesh[b].[/b][b]min[/b][b]().[/b]z[b]))[/b]
bbox[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b]mesh[b].[/b][b]min[/b][b]().[/b]x[b],[/b] mesh[b].[/b][b]min[/b][b]().[/b]y[b],[/b] mesh[b].[/b][b]max[/b][b]().[/b]z[b]))[/b]
bbox[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b]mesh[b].[/b][b]max[/b][b]().[/b]x[b],[/b] mesh[b].[/b][b]min[/b][b]().[/b]y[b],[/b] mesh[b].[/b][b]max[/b][b]().[/b]z[b]))[/b]
bbox[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b]mesh[b].[/b][b]min[/b][b]().[/b]x[b],[/b] mesh[b].[/b][b]max[/b][b]().[/b]y[b],[/b] mesh[b].[/b][b]max[/b][b]().[/b]z[b]))[/b]
bbox[b].[/b]append[b]([/b]dt[b].[/b]Vector[b]([/b]mesh[b].[/b][b]max[/b][b]().[/b]x[b],[/b] mesh[b].[/b][b]max[/b][b]().[/b]y[b],[/b] mesh[b].[/b][b]max[/b][b]().[/b]z[b]))[/b]
[b]return[/b] bbox
[b]def[/b] calcMinAngle[b]([/b]self[b],[/b] obj1Vec[b],[/b] obj1Pos[b],[/b] otherObjects[b]):[/b]
smallestAngle [b]=[/b] [b]360[/b]
[b]for[/b] obj2Pos [b]in[/b] otherObjects[b]:[/b]
curAngle [b]=[/b] math[b].[/b]degrees[b]([/b]obj1Vec[b].[/b]angle[b](([/b]obj1Pos [b]-[/b] obj2Pos[b]).[/b]normal[b]()))[/b]
[b]if[/b] curAngle [b]<[/b] smallestAngle[b]:[/b]
smallestAngle [b]=[/b] curAngle
[b]return[/b] smallestAngle
[b]def[/b] calcBoundingRadius[b]([/b]self[b],[/b] bboxPoints[b]):[/b]
maxDist [b]=[/b] [b]0[/b]
[b]for[/b] point1 [b]in[/b] bboxPoints[b]:[/b]
[b]for[/b] point2 [b]in[/b] bboxPoints[b]:[/b]
[b]if[/b] [b]([/b]point1 [b]-[/b] point2[b]).[/b]length[b]() >[/b] maxDist[b]:[/b]
maxDist [b]= ([/b]point1 [b]-[/b] point2[b]).[/b]length[b]()[/b]
[b]return[/b] maxDist[b]/[/b][b]2[/b]
[b]def[/b] getLightCone[b]([/b]self[b],[/b] light[b]):[/b]
[b]if[/b] light[b].[/b]nodeType[b]() ==[/b] 'spotLight'[b]:[/b]
[b]return[/b] light[b].[/b]coneAngle[b].[/b]get[b]()/[/b][b]2[/b] [b]+[/b] light[b].[/b]penumbraAngle[b].[/b]get[b]()/[/b][b]2[/b]
[b]else[/b][b]:[/b]
self[b].[/b]close[b]()[/b]
sys[b].[/b]exit[b]()[/b]
[b]def[/b] checkVis[b]([/b]self[b],[/b] obj[b]):[/b]
[b]if[/b] obj[b].[/b]visibility[b].[/b]get[b]():[/b]
[b]if[/b] obj[b].[/b]getParent[b]():[/b]
[b]return[/b] self[b].[/b]checkVis[b]([/b]obj[b].[/b]getParent[b]())[/b]
[b]else[/b][b]:[/b]
[b]return[/b] obj[b].[/b]visibility[b].[/b]get[b]()[/b]
[b]def[/b] initProgress[b]([/b]self[b]):[/b]
self[b].[/b]gMainProgressBar [b]=[/b] mel[b].[/b][b]eval[/b][b]([/b]'$tmp = $gMainProgressBar'[b])[/b]
[b]def[/b] startProgress[b]([/b]self[b],[/b]length[b]):[/b]
progressBar[b]([/b]self[b].[/b]gMainProgressBar[b],[/b] edit[b]=[/b][b]True[/b][b],[/b] beginProgress[b]=[/b][b]True[/b][b],[/b] isInterruptable[b]=[/b][b]True[/b][b],[/b] status[b]=[/b]'Starting script...'[b],[/b] maxValue[b]=[/b]length[b])[/b]
[b]def[/b] setProgress[b]([/b]self[b],[/b]msg[b]):[/b]
progressBar[b]([/b]self[b].[/b]gMainProgressBar[b],[/b] edit[b]=[/b][b]True[/b][b],[/b] status[b]=[/b]msg[b])[/b]
[b]def[/b] stepProgress[b]([/b]self[b]):[/b]
progressBar[b]([/b]self[b].[/b]gMainProgressBar[b],[/b] edit[b]=[/b][b]True[/b][b],[/b] step[b]=[/b][b]1[/b][b])[/b]
[b]def[/b] checkCancelled[b]([/b]self[b]):[/b]
[b]if[/b] progressBar[b]([/b]self[b].[/b]gMainProgressBar[b],[/b] query[b]=[/b][b]True[/b][b],[/b] isCancelled[b]=[/b][b]True[/b][b]):[/b]
progressBar[b]([/b]self[b].[/b]gMainProgressBar[b],[/b] edit[b]=[/b][b]True[/b][b],[/b] ep[b]=[/b][b]True[/b][b])[/b]
sys[b].[/b]exit[b]()[/b]
[b]def[/b] close[b]([/b]self[b]):[/b]
progressBar[b]([/b]self[b].[/b]gMainProgressBar[b],[/b] edit[b]=[/b][b]True[/b][b],[/b] ep[b]=[/b][b]True[/b][b])[/b]
And part 2 - example usage:
[b]if[/b] [b]len[/b][b]([/b]ls[b]([/b]sl[b]=[/b][b]True[/b][b])) ==[/b] [b]0[/b][b]:[/b]
[b]print[/b] "Please select at least one light!"
sys[b].[/b]exit[b]()[/b]
needDist [b]=[/b] [b]False[/b]
[b]for[/b] light [b]in[/b] ls[b]([/b]sl[b]=[/b][b]True[/b][b]):[/b]
[b]if not[/b] light[b].[/b]getShape[b]().[/b]nodeType[b]()[/b] [b]in[/b] [b][[/b]'pointLight'[b],[/b] 'spotLight'[b]]:[/b]
[b]print[/b] "Please only select pointLight or spotLights! " [b]+[/b] light[b].[/b]name[b]() +[/b] " is not a supported light."
sys[b].[/b]exit[b]()[/b]
[b]if[/b] light[b].[/b]getShape[b]().[/b]nodeType[b]() ==[/b] 'pointLight'[b]:[/b]
needDist [b]=[/b] [b]True[/b]
[b]if[/b] needDist[b]:[/b]
with window[b]([/b]title[b]=[/b]'Pointlight maxDist'[b])[/b] [b]as[/b] win[b]:[/b]
with columnLayout[b]():[/b]
text[b]([/b]label[b]=[/b]'Please enter a maximum distance for pointlight culling:'[b])[/b]
maxDist [b]=[/b] floatField[b]([/b][b]min[/b][b]=[/b][b]0[/b][b],[/b] enterCommand[b]=[/b]'lightc = LightCuller(ls(sl=True)), lightc.start(maxDist.getValue()); deleteUI(win)'[b])[/b]
button[b]([/b] label[b]=[/b]'Set max distance'[b],[/b] command[b]=[/b]'lightc = LightCuller(ls(sl=True)), lightc.start(maxDist.getValue()); deleteUI(win)'[b])[/b]
[b]else[/b][b]:[/b]
lightc [b]=[/b] LightCuller[b]([/b]ls[b]([/b]sl[b]=[/b][b]True[/b][b]))[/b]
lightc[b].[/b]start[b]([/b][b]0.0[/b][b])[/b]
Is it a bad sign now that when I close my eyes I can see vectors?