Speedlines
Generator
During the development period of The Dragon Prince, I developed a tool for generating speedlines without the need for any simulation engine. Here's the result for what I'm about to show you in this page.
How does it work?
First, I'd like to tell you about this node in Nuke called STMap. In case you're not familiar with it here's an explanation from Foundry."The STMap node allows you to move pixels around in an image. STMap uses two channels to figure out where each pixel in the resulting image should come from in the input channels.
The U and V values are the absolute position of the source pixel. The values are normalized to be between 0 and 1, where 0 is the bottom left corner of the input image, and 1 is the top right corner. "
I'm going to work on this shot to simplify the process
Let's start
In Maya, select an edge that you want to emit the tail and convert it to a curve (Modify -> Convert -> Polygon Edges to Curves ).Then select the curve you just made and create snapshots from it (Visualize -> Create Animation Snapshot).
"Animation Snapshot creates copies of animated objects at intervals of a playback range. Animation Snapshot works on NURBS or polygonal geometry"
it's crucial that you set Surface Degree to Cubic.
This will ensure that the faces are evenly spaced out along the vertical axis of the uv.
The following python code will construct gradient and transparency ramps for the lofted mesh.
The code will also set some expressions on these ramps so that they look as if they're following the object.
class Ramp():
ramp = ""
def __init__(self):
ramp = self._createRamp()
self.ramp = ramp
def _createRamp(self):
rampRed = cmds.shadingNode("ramp", asTexture=True )
return rampRed
def setColor(self, indx, color3):
cmds.setAttr(self.ramp + ".colorEntryList[{}].color".format(indx) , color3[0], color3[1], color3[2] ,type="double3")
def setExpression(self, exp):
cmds.expression(string=exp, object=self.ramp, alwaysEvaluate=True, unitConversion="all")
def setType(self, uv):
# uv = int
# 0: V Ramp, 1: U Ramp
cmds.setAttr(self.ramp + ".type", uv);
def setPosition(self, indx, pos):
cmds.setAttr(self.ramp + ".colorEntryList[{}].position".format(indx), pos);
def setInterpolation(self, interp):
# interp = int
# 0: None, 1: Linear
cmds.setAttr(self.ramp + ".interpolation", interp)
class Merge():
layeredTexture = ""
def __init__(self, node1, node2, blendMode):
layeredTexture = self._createLayeredTexture()
self.layeredTexture = layeredTexture
self._Merge(node1, node2)
self._setMergeMode(blendMode)
def _createLayeredTexture(self):
layeredTexture = cmds.shadingNode("layeredTexture", asTexture=True )
return layeredTexture
def _Merge(self, node1, node2):
cmds.connectAttr(node1 + ".outColor", self.layeredTexture + ".inputs[1].color", force=True);
cmds.connectAttr(node2 + ".outColor", self.layeredTexture + ".inputs[2].color", force=True);
def _setMergeMode(self, mode):
cmds.setAttr(self.layeredTexture + ".inputs[1].blendMode", mode)
class SurfaceShader():
surfaceShader = ""
shadingEngine = ""
def __init__(self):
surfaceShader = self._createSurfaceShader()
self.surfaceShader = surfaceShader
shadingEngine = self._createShadingEngine()
self.shadingEngine = shadingEngine
self._connectShadingEngine()
def _createSurfaceShader(self):
surfaceShader = cmds.shadingNode("surfaceShader", asShader=True)
return surfaceShader
def _createShadingEngine(self):
shadingEngine = cmds.shadingNode('shadingEngine', asUtility=True)
return shadingEngine
def _connectShadingEngine(self):
cmds.connectAttr(self.surfaceShader + ".outColor", self.shadingEngine + ".surfaceShader ", force=True);
cmds.connectAttr(self.shadingEngine + ".partition", ":renderPartition.sets", nextAvailable=True)
def connectColor(self, node):
cmds.connectAttr(node + ".outColor", self.surfaceShader + ".outColor", force=True);
def connectTransparancy(self, node):
cmds.connectAttr(node + ".outColor", self.surfaceShader + ".outTransparency", force=True);
def _getHeadExpression():
headExp = "$maxFrame = `playbackOptions -q -max`;\n{}.colorEntryList[1].position = (frame-1)/($maxFrame-1) + 0.0001;"
return headExp
def _getTailExpressions():
tailExp = "$offset = .1;\n{0}.colorEntryList[0].position = {0}.colorEntryList[1].position - $offset;"
return tailExp
def _constructRedRamp():
rampRed = Ramp()
rampRed.setType(0)
rampRed.setColor(0, (0,0,0))
rampRed.setColor(1, (1,0,0))
rampRedHeadExp = _getHeadExpression().format(rampRed.ramp)
rampRedTailExp = _getTailExpressions().format(rampRed.ramp)
rampRed.setExpression(rampRedHeadExp)
rampRed.setExpression(rampRedTailExp)
return rampRed.ramp
def _constructGreenRamp():
rampGreen = Ramp()
rampGreen.setType(1)
rampGreen.setPosition(0, 0)
rampGreen.setPosition(1, 1)
rampGreen.setColor(0, (0,1,0))
rampGreen.setColor(1, (0,0,0))
return rampGreen.ramp
def _constructTransparencyRamp():
rampTransparency = Ramp()
rampTransparency.setType(0)
rampTransparency.setInterpolation(0)
rampTransparency.setColor(0, (0,0,0))
rampTransparency.setColor(1, (1,1,1))
rampTransparency.setColor(2, (1,1,1))
rampTransparency.setPosition(2, 0)
rampTransparencyHeadExp = _getHeadExpression().format(rampTransparency.ramp)
rampTransparencyTailExp = _getTailExpressions().format(rampTransparency.ramp)
rampTransparency.setExpression(rampTransparencyHeadExp)
rampTransparency.setExpression(rampTransparencyTailExp)
return rampTransparency.ramp
redRamp = _constructRedRamp()
greenRamp = _constructGreenRamp()
transparancyRamp = _constructTransparencyRamp()
mergedRamps = Merge(redRamp, greenRamp, 4).layeredTexture
surfaceShader = SurfaceShader()
surfaceShader.connectColor(mergedRamps)
surfaceShader.connectTransparancy(transparancyRamp)
If you assign the shader you should get this.
Now we just use the STMap in Nuke to hook any image.
This tool was used in The Dragon Prince to generate speedlines in some shots.
Speedlines with Houdini
An alternative approach that's more procedural can be done in Houdini.We can scatter some points based on velocity threshold and generate lines using SOP Solver.