Skip to content

Instantly share code, notes, and snippets.

@iandanforth
Last active October 5, 2022 10:57
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save iandanforth/0ed987bfddf8205b8a23 to your computer and use it in GitHub Desktop.
Save iandanforth/0ed987bfddf8205b8a23 to your computer and use it in GitHub Desktop.
Capture WebGL frames to disk

How to capture WebGL/Canvas by piping data over a websocket.

This Gist builds on https://gist.github.com/unconed/4370822 from @unconed.

Instead of the original method which writes to the browsers sandboxed filesystem here we use a websocket connection provided by websocketd to pipe image data to a short python script that writes out the .png files to disk.

Install websocketd

https://github.com/joewalnes/websocketd

After installing it and saving the below code snippets to canvascapture.html and canvascapture.py you will launch the websocket server like this:

websocketd --port=8080 ./canvascapture.py

HTML + JavaScript

The below combines the original raf override from @unconed, the example code from websocketd and the introductory scene code from threejs.org.

From the same directory to which you save the html launch a server and load it in your browser:

python -m SimpleHTTPServer 8000

canvascapture.html

<html>
<head>
	<meta charset=utf-8>
	<title>My first Three.js app</title>
	<style>
		body { margin: 0; }
		canvas { width: 100%; height: 100% }
	</style>
</head>
<body>
	<script src="js/three.min.js"></script>
	<script>

		var ws = new WebSocket('ws://localhost:8080/');
		ws.onopen = function() {
			console.log('CONNECT');
		};

		ws.onclose = function() {
			console.log('DISCONNECT');
		};
		ws.onmessage = function(event) {
			console.log('MESSAGE: ' + event.data);
		};

		var frames = 1000;

		// Request Animation Frame Override
		var raf = window.requestAnimationFrame;
		var next = null;
		var hold = false;
		window.requestAnimationFrame = function rafOverride(callback) {
		  // Find canvas
		  var canvas = document.querySelector('canvas');
		  if (canvas) {
		    // Done capturing?
		    if (frames < 0) {
		      window.requestAnimationFrame = raf;
		      return raf(callback);
		    }

		    // Hold rendering until screenshot is done
		    if (!hold) {
		      hold = true;
		      frames--;
		      setTimeout(function () {
		        callback();
		        capture(canvas, function () {
		          // Resume rendering
		          hold = false;
		          rafOverride(next);
		        });
		      }, 5);
		    }
		    else {
		      next = callback;
		    }
		  }
		  else {
		    // Canvas not created yet?
		    return raf(callback);
		  }
		}

		function capture(canvas, callback) {
		  // Capture image and strip header from string.
		  var image = canvas.toDataURL('image/png').slice(22);

		  // Writing image as msg
		  ws.send(image);

		  setTimeout(function () {
		    // Resume rendering
		    callback();
		  }, 5);
		}

		// Three js scene
		var scene = new THREE.Scene();
		var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

		var renderer = new THREE.WebGLRenderer();
		renderer.setSize( window.innerWidth, window.innerHeight );
		document.body.appendChild( renderer.domElement );

		var geometry = new THREE.BoxGeometry( 1, 1, 1 );
		var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
		var cube = new THREE.Mesh( geometry, material );
		scene.add( cube );

		camera.position.z = 5;

		function render() {
			requestAnimationFrame( render );

			cube.rotation.x += 0.1;
			cube.rotation.y += 0.1;

			renderer.render( scene, camera );
		}
		render();

	</script>

</body>
</html

Python

Copy the below into a file named canvascapture.py and then make it executable

chmod +x canvascapture.py
#!/usr/bin/env python
'''
Writes out data received from websocketd on stdin as 
png frames.
'''

import time
import base64
import binascii
from sys import stdin, stdout

# From http://stackoverflow.com/a/9807138
def decode_base64(data):
    """Decode base64, padding being optional.

    :param data: Base64 data as an ASCII byte string
    :returns: The decoded byte string.

    """
    missing_padding = 4 - len(data) % 4
    if missing_padding:
        data += b'='* missing_padding
    return base64.decodestring(data)

def main():
    '''
    As each frame is received write it to a .png file
    '''

    frames = 0
    while True:

        received_val = stdin.readline()
        # Right pad filenames so they can be sorted
        filename = str(time.time()).ljust(13, "0") + ".png"
        with open(filename, 'wb') as file_handle:
            try:
                file_handle.write(decode_base64(received_val))
                frames += 1
                if frames % 10 == 0:
                    print("Captured %d frames ..." % frames)
                    stdout.flush()
            except binascii.Error:
                # Ignore malformed data urls
                continue

if __name__ == '__main__':
    main()
@joeblew99
Copy link

This does not allow high frame rates ? For example if I wanted 60 fps. Continuous time can't be used, but instead I think delta time is needed.

@knightofelf
Copy link

knightofelf commented Mar 30, 2018

Error!
websocketd --port=8080 test.py
Could not launch process ./test.py (fork/exec ./test.py: %1 is not a valid Win32 application.)

@nathan-sixnines
Copy link

@knightofelf

For windows, try > websocketd --port=8080 python test.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment