I've created a very naive page that will take a Three.js scene (though this could be any non WebGL <canvas>
animation) and exports individual images (or sequences of images) to then be converted into video.
I'm doing this partially as a way to learn Python, but also the idea of being able to rapidly prototype something in Three.js and then export a massive high res silky smooth video is super attractive to me. In the past I've used screen capture software to grab video, though it's always felt a bit clunky and real time drops in FPS would show through in the final video too.
The process I currently have is as follows:
Javascript:
- Create the WebGL scene and set up a render cycle
- Set canvas width/height to desired dimensions
- Render the scene using
requestAnimationFrame
- Pause the scene's render/update cycle
- Call
toDataURL()
on the canvas element and retrieve a base64 string - POST request to a python script, passing the base64 string (and other things, like target directory to save to and whether a single image or sequence is being captured)
Python:
- Strip the MIME header content-type and decode the base64 string
- Write the string to an image file
- Print / return out a string that signifies successful state if the file was written, otherwise print out the offending error message
import base64, cgi, cgitb, datetime, glob, re, os
cgitb.enable()
#cgitb.enable(display=0, logdir='/tmp')
print "Content-type: text/html"
print
def main():
form = cgi.FieldStorage()
saveLocation = "../httpdocs/export/"
# POST variables
dataURL = form['dataURL'].value
captureSequence = form['captureSequence'].value
folderName = saveLocation + form['folderName'].value
saveImage(dataURL, captureSequence, saveLocation, folderName)
def saveImage(dataURL, captureSequence, saveLocation, folderName):
# strip out MIME content-type (e.g. "data:image/png;base64,")
dataURL = dataURL[dataURL.index(','):]
decodedString = base64.decodestring(dataURL)
if captureSequence == 'true':
# based off http://www.akeric.com/blog/?p=632
currentImages = glob.glob(folderName + "/*.jpg")
# TODO: perhaps filenames shouldnt start at %08d+1 but rather %08d+0?
numList = [0]
if not os.path.exists(folderName):
os.makedirs(folderName)
for img in currentImages:
i = os.path.splitext(img)[0]
try:
num = re.findall('[0-9]+$', i)[0]
numList.append(int(num))
except IndexError:
pass
numList = sorted(numList)
newNum = numList[-1] + 1
saveName = folderName + '/%08d.jpg' % newNum
else:
if not os.path.exists(saveLocation):
os.makedirs(saveLocation)
saveName = saveLocation + datetime.datetime.now().isoformat().replace(':', '.') + '.jpg'
# TODO; rather than returning a simple string, consider returning an object?
try:
output = open(saveName, 'w')
output.write(decodedString)
output.close()
print 'true'
except Exception, e:
print e
if __name__ == '__main__':
main()
Javascript:
- Receive response from Python script and display returned message
- Resume/update render cycle
- (Repeat process for as many frames as desired)
This is something that I would always run locally so there would be no risk of conflicting writes or anything of the sort.
I've done a few quick tests and it seems to work for the most part, if a little slow.
- Am I missing something completely obvious in the way this is being done? How could it be improved? (Especially on the python side of things..)
- Is it inefficient to do an individual ajax call per image? One advantage is that I can just stop / close the tab at any time and it'll have saved all images up until that point. Would there be any benefit to storing all those base64 strings and sending them all at the very end?
- As
requestAnimationFrame
will cap the update cycle to 60fps, is it possible to easily set a lower frame rate? Lets say for some stylistic reason I'd want to update everything at 15fps, would the only option be to usesetTimeout(callback, 1000 / targetFPS)
with the understanding that it will drift over time? Following on from the above, this animation has a var
frame
which is being incremented by1
every update cycle. This var is then used to control varied parts of the animation (e.g, rotating the cube, and being passed to vertex/fragment shaders to manipulate colors and texture UV coordinates).If I wanted to simulate something like 15 fps, would I be correct in needing to increment
frame
by(60 / 15)
instead? Is there a more elegant way to be able to easily switch between capped frame rates?- Finally, are there any techniques that could be adopted to improve the quality of images being rendered? (Thinking out aloud, saving out at double size then reducing them?)
I really hope that makes sense, any insight or advice would be massively appreciated.
Source files: http://cl.ly/3V46120C2d3B0A1o3o25 (Tested on Mac / Chrome stable, WebGL required)