harryjubb
9/9/2015 - 5:05 PM

offscreen rendering with three.js and headless-gl, in coffee-script

offscreen rendering with three.js and headless-gl, in coffee-script

{
  "name": "offscreen-sample",
  "version": "1.0.0",

  "scripts": {
    "start": "coffee offscreen_sample.coffee",
    "compile": "coffee -c -b offscreen_sample.coffee && cat offscreen_sample.js"
  },

  "dependencies": {
    "three": "latest",
    "pngjs": "latest", 
    "gl": "latest"
  },

  "devDependencies": {
    "coffee-script": "latest"
  }
}

# The required node modules
THREE = require('three')
PNG   = require('pngjs').PNG
gl    = require("gl")()
fs    = require('fs')

# Parameters (the missing one is the camera position, see below)
width  = 600
height = 400
path   = 'out.png'
png    = new PNG({ width: width, height: height })

# THREE.js business starts here
scene = new THREE.Scene()

# camera attributes
VIEW_ANGLE = 45
ASPECT = width / height
NEAR = 0.1
FAR  = 100

# set up camera
camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT, NEAR, FAR)

scene.add(camera)
camera.position.set(0, 2, 2)
camera.lookAt(scene.position)

# mock object, not used in our test case, might be problematic for some workflow
canvas = new Object()

# The width / height we set here doesn't matter
renderer = new THREE.WebGLRenderer({
    antialias: true,
    width: 0,
    height: 0,
    canvas: canvas, # This parameter is usually not specified
    context: gl     # Use the headless-gl context for drawing offscreen
})

# add some geometry
geometry = new THREE.BoxGeometry( 1, 1, 1 )

# add a material; it has to be a ShaderMaterial with custom shaders for now 
# this is a work in progress, some related link / issues / discussions
#
# https://github.com/stackgl/headless-gl/issues/26
# https://github.com/mrdoob/three.js/pull/7136
# https://github.com/mrdoob/three.js/issues/7085
material = new THREE.ShaderMaterial()
vec4 = new THREE.Vector4( 1.0, 0.0, 0.0, 1.0 ) # red

material.vertexShader = '''
void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
'''
material.fragmentShader = '''
uniform vec4 solidColor;

void main() {
    gl_FragColor = solidColor;
}
'''
material.uniforms = { solidColor: { type: "v4", value: vec4 } }

# Create the mesh and add it to the scene
cube     = new THREE.Mesh(geometry, material)
scene.add(cube)

# Let's create a render target object where we'll be rendering
rtTexture = new THREE.WebGLRenderTarget(
    width, height, {
        minFilter: THREE.LinearFilter,
        magFilter: THREE.NearestFilter,
        format: THREE.RGBAFormat
})

# render
renderer.render(scene, camera, rtTexture, true)

# read render texture into buffer
gl = renderer.getContext()

# create a pixel buffer of the correct size
pixels = new Uint8Array(4 * width * height)

# read back in the pixel buffer
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels)

# lines are vertically flipped in the FBO / need to unflip them
for j in [0...height]
    for i in [0...width]
        k = j * width + i
        r = pixels[4*k]
        g = pixels[4*k + 1]
        b = pixels[4*k + 2]
        a = pixels[4*k + 3]

        m = (height - j + 1) * width + i
        png.data[4*m]     = r
        png.data[4*m + 1] = g
        png.data[4*m + 2] = b
        png.data[4*m + 3] = a

# Now write the png to disk
stream = fs.createWriteStream(path)
png.pack().pipe stream

stream.on 'close', () ->
    # We're done !!
    console.log("Image written: #{ path }")

Getting the code

git clone https://gist.github.com/6780d7cc0cabb1b4d6c8.git

Executing the code

$ npm install # maybe npm start will take care of it but just in case
$ npm start && open out.png

> offscreen-sample@1.0.0 start /Users/bsergean/src/offscreen_sample
> coffee offscreen_sample.coffee

THREE.WebGLRenderer 71
THREE.WebGLRenderer: TypeError: Object #<Object> has no method 'addEventListener'
THREE.WebGLRenderer: OES_texture_float extension not supported.
THREE.WebGLRenderer: OES_texture_float_linear extension not supported.
THREE.WebGLRenderer: OES_texture_half_float extension not supported.
THREE.WebGLRenderer: OES_texture_half_float_linear extension not supported.
THREE.WebGLRenderer: OES_standard_derivatives extension not supported.
THREE.WebGLRenderer: OES_element_index_uint extension not supported.
THREE.WebGLRenderer: EXT_texture_filter_anisotropic extension not supported.
Image written: out.png

Those warnings are harmless for our test case, but might be problematic for some folks. Support for extension is planned and coming -> https://github.com/stackgl/headless-gl/issues/5

If you are on Linux you will need Xvfb. One way to do it:

$ xvfb-run -s "-ac -screen 0 1280x1024x24” node_modules/.bin/coffee cmd_antialias.coffee -i test_aliased.png -o out.png

More infos here -> https://github.com/stackgl/headless-gl#how-can-headless-gl-be-used-on-a-headless-linux-machine

Inspecting the output

Tada ! You just created an image thanks to OpenGL and many awesome libraries. How cool is that. Now open the output image. On a Mac you can just do that:

open out.png

I can't figure out how to add a .png to a gist. I've updaloaded the very non-impressive image here -> http://imgur.com/Vq4FnN9

Bummer, the sample is in coffee-script

npm run compile

This will compile the .coffee file to javascript and print it in your terminal. It's almost the same as the .coffee.