Headless GL Context for THREE server-side rendering

I’v been trying to get headless-gl to work with three.js, running some gists provided by @bsergean in a discussion about the topic in issue #7085, but with no success so far. Some examples will give an error with an undefined WebGL context such as this:

ubuntu@ubuntu:~/dev/08be90a2f21205062ccc$ xvfb-run -s "-ac -screen 0 1280x1024x24" npm start

> offscreen-sample@1.0.0 start /home/ubuntu/dev/08be90a2f21205062ccc
> coffee offscreen_sample.coffee

parsed ! undefined undefined
undefined
THREE.WebGLRenderer 95
THREE.WebGLRenderer: _canvas.addEventListener is not a function
events.js:183
      throw er; // Unhandled 'error' event
      ^

TypeError: Cannot read property 'getShaderPrecisionFormat' of undefined
    at getMaxPrecision (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/three/build/three.js:14830:13)
    at new WebGLCapabilities (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/three/build/three.js:14859:22)
    at initGLContext (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/three/build/three.js:21926:19)
    at new WebGLRenderer (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/three/build/three.js:21976:3)
    at render (/home/ubuntu/dev/08be90a2f21205062ccc/offscreen_sample.coffee:35:16)
    at exports.PNG.<anonymous> (/home/ubuntu/dev/08be90a2f21205062ccc/offscreen_sample.coffee:136:5)
    at emitOne (events.js:116:13)
    at exports.PNG.emit (events.js:211:7)
    at exports.PNG.<anonymous> (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/pngjs/lib/png.js:37:10)
    at emitOne (events.js:116:13)
    at module.exports.emit (events.js:211:7)
    at module.exports.ParserAsync._complete (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/pngjs/lib/parser-async.js:153:8)
    at emitOne (events.js:116:13)
    at module.exports.emit (events.js:211:7)
    at module.exports.complete (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/pngjs/lib/filter-parse-async.js:19:12)
    at module.exports.Filter._reverseFilterLine (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/pngjs/lib/filter-parse.js:169:10)
    at module.exports.ChunkStream._processRead (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/pngjs/lib/chunkstream.js:174:13)
    at module.exports.ChunkStream._process (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/pngjs/lib/chunkstream.js:193:14)
    at module.exports.ChunkStream.write (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/pngjs/lib/chunkstream.js:61:8)
    at Inflate.<anonymous> (/home/ubuntu/dev/08be90a2f21205062ccc/node_modules/pngjs/lib/parser-async.js:94:9)
    at emitOne (events.js:116:13)
    at Inflate.emit (events.js:211:7)
    at addChunk (_stream_readable.js:263:12)
    at readableAddChunk (_stream_readable.js:250:11)
    at Inflate.Readable.push (_stream_readable.js:208:10)
    at Inflate.Transform.push (_stream_transform.js:147:32)
    at Zlib.callback (zlib.js:474:14)

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! offscreen-sample@1.0.0 start: `coffee offscreen_sample.coffee`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the offscreen-sample@1.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/ubuntu/.npm/_logs/2018-08-30T06_39_19_283Z-debug.log

while others such as this example will return and error when creating a shader such as the following:

ubuntu@ubuntu:~/dev/0d79ce3c7384cf6d1bb6$ xvfb-run -s "-ac -screen 0 1280x1024x24" node_modules/.bin/coffee cmd_antialias.coffee -i test_aliased.png -o out.png
THREE.WebGLRenderer 72
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: ANGLE_instanced_arrays extension not supported.
THREE.WebGLRenderer: OES_element_index_uint extension not supported.
THREE.WebGLRenderer: EXT_texture_filter_anisotropic extension not supported.
THREE.WebGLShader: Shader couldn't compile.
THREE.WebGLShader: gl.getShaderInfoLog() vertex 0:2(1): error: syntax error, unexpected NEW_IDENTIFIER
 1: precision highp float;
2: precision highp int;
3: #define SHADER_NAME ShaderMaterial
4: #define VERTEX_TEXTURES
5: #define GAMMA_FACTOR 2
6: #define MAX_DIR_LIGHTS 0
7: #define MAX_POINT_LIGHTS 0
8: #define MAX_SPOT_LIGHTS 0
9: #define MAX_HEMI_LIGHTS 0
10: #define MAX_SHADOWS 0
11: #define MAX_BONES 1019
12: uniform mat4 modelMatrix;
13: uniform mat4 modelViewMatrix;
14: uniform mat4 projectionMatrix;
15: uniform mat4 viewMatrix;
16: uniform mat3 normalMatrix;
17: uniform vec3 cameraPosition;
18: attribute vec3 position;
19: attribute vec3 normal;
20: attribute vec2 uv;
21: #ifdef USE_COLOR
22: 	attribute vec3 color;
23: #endif
24: #ifdef USE_MORPHTARGETS
25: 	attribute vec3 morphTarget0;
26: 	attribute vec3 morphTarget1;
27: 	attribute vec3 morphTarget2;
28: 	attribute vec3 morphTarget3;
29: 	#ifdef USE_MORPHNORMALS
30: 		attribute vec3 morphNormal0;
31: 		attribute vec3 morphNormal1;
32: 		attribute vec3 morphNormal2;
33: 		attribute vec3 morphNormal3;
34: 	#else
35: 		attribute vec3 morphTarget4;
36: 		attribute vec3 morphTarget5;
37: 		attribute vec3 morphTarget6;
38: 		attribute vec3 morphTarget7;
39: 	#endif
40: #endif
41: #ifdef USE_SKINNING
42: 	attribute vec4 skinIndex;
43: 	attribute vec4 skinWeight;
44: #endif
45: 
46: varying vec2 vUv;
47: 
48: void main() {
49:     vUv = uv;
50:     gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
51: }
52: 
THREE.WebGLShader: Shader couldn't compile.
THREE.WebGLShader: gl.getShaderInfoLog() fragment 0:2(1): error: syntax error, unexpected NEW_IDENTIFIER
 1: precision highp float;
2: precision highp int;
3: #define SHADER_NAME ShaderMaterial
4: #define MAX_DIR_LIGHTS 0
5: #define MAX_POINT_LIGHTS 0
6: #define MAX_SPOT_LIGHTS 0
7: #define MAX_HEMI_LIGHTS 0
8: #define MAX_SHADOWS 0
9: #define GAMMA_FACTOR 2
10: uniform mat4 viewMatrix;
11: uniform vec3 cameraPosition;
12: 
13: 
14: //
15: // Assembled (de-glslify'ed) from https://github.com/mattdesl/glsl-fxaa
16: //
17: 
18: #ifndef FXAA_REDUCE_MIN
19:     #define FXAA_REDUCE_MIN   (1.0/ 128.0)
20: #endif
21: #ifndef FXAA_REDUCE_MUL
22:     #define FXAA_REDUCE_MUL   (1.0 / 8.0)
23: #endif
24: #ifndef FXAA_SPAN_MAX
25:     #define FXAA_SPAN_MAX     8.0
26: #endif
27: 
28: // optimized version for mobile, where dependent 
29: // texture reads can be a bottleneck
30: vec4 fxaa(sampler2D tex, vec2 fragCoord, vec2 resolution,
31:             vec2 v_rgbNW, vec2 v_rgbNE, 
32:             vec2 v_rgbSW, vec2 v_rgbSE, 
33:             vec2 v_rgbM) {
34:     vec4 color;
35:     vec2 inverseVP = vec2(1.0 / resolution.x, 1.0 / resolution.y);
36:     vec3 rgbNW = texture2D(tex, v_rgbNW).xyz;
37:     vec3 rgbNE = texture2D(tex, v_rgbNE).xyz;
38:     vec3 rgbSW = texture2D(tex, v_rgbSW).xyz;
39:     vec3 rgbSE = texture2D(tex, v_rgbSE).xyz;
40:     vec4 texColor = texture2D(tex, v_rgbM);
41:     vec3 rgbM  = texColor.xyz;
42:     vec3 luma = vec3(0.299, 0.587, 0.114);
43:     float lumaNW = dot(rgbNW, luma);
44:     float lumaNE = dot(rgbNE, luma);
45:     float lumaSW = dot(rgbSW, luma);
46:     float lumaSE = dot(rgbSE, luma);
47:     float lumaM  = dot(rgbM,  luma);
48:     float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));
49:     float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));
50:     
51:     vec2 dir;
52:     dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));
53:     dir.y =  ((lumaNW + lumaSW) - (lumaNE + lumaSE));
54:     
55:     float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) *
56:                           (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN);
57:     
58:     float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);
59:     dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX),
60:               max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),
61:               dir * rcpDirMin)) * inverseVP;
62:     
63:     vec3 rgbA = 0.5 * (
64:         texture2D(tex, fragCoord * inverseVP + dir * (1.0 / 3.0 - 0.5)).xyz +
65:         texture2D(tex, fragCoord * inverseVP + dir * (2.0 / 3.0 - 0.5)).xyz);
66:     vec3 rgbB = rgbA * 0.5 + 0.25 * (
67:         texture2D(tex, fragCoord * inverseVP + dir * -0.5).xyz +
68:         texture2D(tex, fragCoord * inverseVP + dir * 0.5).xyz);
69: 
70:     float lumaB = dot(rgbB, luma);
71:     if ((lumaB < lumaMin) || (lumaB > lumaMax))
72:         color = vec4(rgbA, texColor.a);
73:     else
74:         color = vec4(rgbB, texColor.a);
75:     return color;
76: }
77: 
78: void texcoords(vec2 fragCoord, vec2 resolution,
79:             out vec2 v_rgbNW, out vec2 v_rgbNE,
80:             out vec2 v_rgbSW, out vec2 v_rgbSE,
81:             out vec2 v_rgbM) {
82:     vec2 inverseVP = 1.0 / resolution.xy;
83:     v_rgbNW = (fragCoord + vec2(-1.0, -1.0)) * inverseVP;
84:     v_rgbNE = (fragCoord + vec2(1.0, -1.0)) * inverseVP;
85:     v_rgbSW = (fragCoord + vec2(-1.0, 1.0)) * inverseVP;
86:     v_rgbSE = (fragCoord + vec2(1.0, 1.0)) * inverseVP;
87:     v_rgbM = vec2(fragCoord * inverseVP);
88: }
89: 
90: vec4 apply(sampler2D tex, vec2 fragCoord, vec2 resolution) {
91:     vec2 v_rgbNW;
92:     vec2 v_rgbNE;
93:     vec2 v_rgbSW;
94:     vec2 v_rgbSE;
95:     vec2 v_rgbM;
96: 
97:     // compute the texture coords
98:     texcoords(fragCoord, resolution, 
99:               v_rgbNW, v_rgbNE, v_rgbSW, v_rgbSE, v_rgbM);
100:     
101:     // compute FXAA
102:     return fxaa(tex, fragCoord, resolution, 
103:                 v_rgbNW, v_rgbNE, v_rgbSW, v_rgbSE, v_rgbM);
104: }
105: 
106: uniform vec2 resolution;
107: uniform sampler2D dataTexture;
108: varying vec2 vUv;
109: 
110: void main() {
111:     vec2 fragCoord = vUv * resolution;
112:     gl_FragColor = apply(dataTexture, fragCoord, resolution);
113: }
114: 
THREE.WebGLProgram: shader error:  1286 gl.VALIDATE_STATUS false gl.getProgramInfoLog  0:2(1): error: syntax error, unexpected NEW_IDENTIFIER
 0:2(1): error: syntax error, unexpected NEW_IDENTIFIER

Image written: out.png

Has anyone been able to render a THREE scene server-side?

This might be an incorrect suggestion, but does your server have a GPU to run the shaders on?

@mkarnicki Well it’s running on a VM, this is the glxinfo output for my ubuntu x64 virtual machine:

ubuntu@ubuntu:~$ glxinfo | grep 'version'
server glx version string: 1.4
client glx version string: 1.4
GLX version: 1.4
    Max core profile version: 3.3
    Max compat profile version: 3.0
    Max GLES1 profile version: 1.1
    Max GLES[23] profile version: 3.0
OpenGL core profile version string: 3.3 (Core Profile) Mesa 18.0.5
OpenGL core profile shading language version string: 3.30
OpenGL version string: 3.0 Mesa 18.0.5
OpenGL shading language version string: 1.30
OpenGL ES profile version string: OpenGL ES 3.0 Mesa 18.0.5
OpenGL ES profile shading language version string: OpenGL ES GLSL ES 3.00

As well as the GPU information.

ubuntu@ubuntu:~$ sudo lshw -C display
[sudo] password for ubuntu: 
  *-display               
       description: VGA compatible controller
       product: VirtualBox Graphics Adapter
       vendor: InnoTek Systemberatung GmbH
       physical id: 2
       bus info: pci@0000:00:02.0
       version: 00
       width: 32 bits
       clock: 33MHz
       capabilities: vga_controller rom
       configuration: driver=vboxvideo latency=0
       resources: irq:18 memory:e0000000-e0ffffff memory:c0000-dffff

We had luck with Mesa - software emulation.