text: 
  let
  options = {antialias: false},
  gl = (c.getContext('webgl', options) || c.getContext('experimental-webgl', options)),
  
  R_MAX_VERTS = 1024 * 64, // allow 512k verts max
  R_MAX_LIGHT_V3 = 64,
  
  // Vertex shader source. This translates the model position & rotation and also
  // mixes positions of two buffers for animations.
  R_SOURCE_VS = 
  	'precision highp float;' +
  
  	// Vertex positions, normals and uv coords for the fragment shader
  	'varying vec3 vp,vn;' +
  	'varying vec2 vt;' +
  
  	// Input vertex positions & normals and blend vertex positions & normals
  	'attribute vec3 p,n,p2,n2;' +
  
  	// Input UV coords
  	'attribute vec2 t;' +		
  
  	// Camera position (x, y, z) and aspect ratio (w)
  	'uniform vec4 c;' + 
  
  	// Model position (x, y, z)
  	'uniform vec3 mp;' + 
  
  	// Model rotation (yaw, pitch)
  	'uniform vec2 mr;' + 		
  
  	// Mouse rotation yaw (x), pitch (y)
  	'uniform vec2 m;' +
  
  	// Blend factor between the two vertex positions
  	'uniform float f;' +
  
  	// Generate a rotation Matrix around the x,y,z axis;
  	// Used for model rotation and camera yaw
  	'mat4 rx(float r){' +
  		'return mat4(' +
  			'1,0,0,0,' +
  			'0,cos(r),sin(r),0,' +
  			'0,-sin(r),cos(r),0,' +
  			'0,0,0,1' +
  		');' +
  	'}' +
  
  	'mat4 ry(float r){' +
  		'return mat4(' +
  			'cos(r),0,-sin(r),0,' +
  			'0,1,0,0,' +
  			'sin(r),0,cos(r),0,' +
  			'0,0,0,1' +
  		');' +
  	'}' +
  
  	'mat4 rz(float r){' +
  		'return mat4(' +
  			'cos(r),sin(r),0,0,' +
  			'-sin(r),cos(r),0,0,' +
  			'0,0,1,0,' +
  			'0,0,0,1' +
  		');' +
  	'}' +
  
  	'void main(void){' +
  		// Rotation Matrixes for model rotation
  		'mat4 '+
  			'mry=ry(mr.x),' +
  			'mrz=rz(mr.y);' +
  
  		// Mix vertex positions, rotate and add the model position
  		'vp=(mry*mrz*vec4(mix(p,p2,f),1.)).xyz+mp;' +
  
  		// Mix normals
  		'vn=(mry*mrz*vec4(mix(n,n2,f),1.)).xyz;' +
  
  		// UV coords are handed over to the fragment shader as is
  		'vt=t;' +
  
  		// Final vertex position is transformed by the projection matrix,
  		// rotated around mouse yaw/pitch and offset by the camera position
  		// We use a FOV of 90, so the matrix[0] and [5] are conveniently 1.
  		// (1 / Math.tan((90/180) * Math.PI / 2) === 1)
  		'gl_Position=' +
  			'mat4(' +
  				'1,0,0,0,' +
  				'0,c.w,0,0,' +
  				'0,0,1,1,' +
  				'0,0,-2,0' +
  			')*' + // projection
  			'rx(-m.y)*ry(-m.x)*' +
  			'vec4(vp-c.xyz,1.);' +
  	'}',
  	
  // Fragment shader source. Calculates the lighting, does some cheesy gamma
  // correction and reduces the colors of the final output.
  R_SOURCE_FS = 
  	'precision highp float;' +
  
  	// Vertex positions, normals and uv coords
  	'varying vec3 vp,vn;' +
  	'varying vec2 vt;' +
  
  	'uniform sampler2D s;' +
  
  	// Lights [(x,y,z), [r,g,b], ...]
  	'uniform vec3 l['+R_MAX_LIGHT_V3+'];' +
  
  	'void main(void){' +
  		'gl_FragColor=texture2D(s,vt);' +
  
  		// Debug: no textures
  		// 'gl_FragColor=vec4(1.0,1.0,1.0,1.0);' + 
  
  		// Calculate all lights
  		'vec3 vl;' +
  		'for(int i=0;i<'+R_MAX_LIGHT_V3+';i+=2) {' +
  			'vl+=' +
  				// Angle to normal
  				'max('+ 
  					'dot('+
  						'vn, normalize(l[i]-vp)' +
  					')' +
  				',0.)*' + 
  				'(1./pow(length(l[i]-vp),2.))' + // Inverse distance squared
  				'*l[i+1];' + // Light color/intensity
  		'}' +
  
  		// Debug: full bright lights
  		// 'vl = vec3(2,2,2);' +
  
  		'gl_FragColor.rgb=floor('+
  			'gl_FragColor.rgb*pow(vl,vec3(0.75))'+ // Light, Gamma
  			'*16.0+0.5'+
  		')/16.0;' + // Reduce final output color for some extra dirty looks
  	'}',
  
  // 8 properties per vert [x,y,z, u,v, nx,ny,nz]
  r_buffer = new Float32Array(R_MAX_VERTS*8), 
  r_num_verts = 0,
  
  // 2 vec3 per light [(x,y,z), [r,g,b], ...]
  r_light_buffer = new Float32Array(R_MAX_LIGHT_V3*3), 
  r_num_lights = 0,
  
  // Uniform locations
  r_u_camera,
  r_u_lights,
  r_u_mouse,
  r_u_pos,
  r_u_rotation,
  r_u_frame_mix,
  
  // Vertex attribute location for mixing
  r_va_p2, r_va_n2,
  
  // Texture handles
  r_textures = [],
  
  // Camera position
  r_camera = vec3(0, 0,-50),
  r_camera_pitch = 0.2,
  r_camera_yaw = 0,
  
  // We collect all draw calls in an array and draw them all at once at the end
  // the frame. This way the lights buffer will be completely filled and we
  // only need to set it once for all geometry
  r_draw_calls = [],
  
  r_init = () => {
  	// Create shorthand WebGL function names
  	// let webglShortFunctionNames = {};
  	for (let name in gl) {
  		if (gl[name].length != undefined) {
  			gl[name.match(/(^..|[A-Z]|\d.|v$)/g).join('')] = gl[name];
  			// webglShortFunctionNames[name] = 'gl.' +name.match(/(^..|[A-Z]|\d.|v$)/g).join('');
  		}
  	}
  	// console.log(JSON.stringify(webglShortFunctionNames, null, '\t'));
  
  	let shader_program = gl.createProgram();
  	gl.attachShader(shader_program, r_compile_shader(gl.VERTEX_SHADER, R_SOURCE_VS));
  	gl.attachShader(shader_program, r_compile_shader(gl.FRAGMENT_SHADER, R_SOURCE_FS));
  	gl.linkProgram(shader_program);
  	gl.useProgram(shader_program);
  
  	r_u_camera = gl.getUniformLocation(shader_program, 'c');
  	r_u_lights = gl.getUniformLocation(shader_program, 'l');
  	r_u_mouse = gl.getUniformLocation(shader_program, 'm');
  	r_u_pos = gl.getUniformLocation(shader_program, 'mp');
  	r_u_rotation = gl.getUniformLocation(shader_program, 'mr');
  	r_u_frame_mix = gl.getUniformLocation(shader_program, 'f');
  	
  	gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
  
  	r_vertex_attrib(shader_program, 'p', 3, 8, 0); // position
  	r_vertex_attrib(shader_program, 't', 2, 8, 3); // texture coord
  	r_vertex_attrib(shader_program, 'n', 3, 8, 5); // normals
  
  	r_va_p2 = r_vertex_attrib(shader_program, 'p2', 3, 8, 0); // mix position
  	r_va_n2 = r_vertex_attrib(shader_program, 'n2', 3, 8, 5); // mix normals
  
  	gl.enable(gl.DEPTH_TEST);
  	gl.enable(gl.BLEND);
  	gl.enable(gl.CULL_FACE);
  	gl.viewport(0,0,c.width,c.height);
  },
  
  r_compile_shader = (shader_type, shader_source) => {
  	let shader = gl.createShader(shader_type);
  	gl.shaderSource(shader, shader_source);
  	gl.compileShader(shader);
  	// console.log(gl.getShaderInfoLog(shader));
  	return shader;
  },
  
  r_vertex_attrib = (shader_program, attrib_name, count, vertex_size, offset) => {
  	let location = gl.getAttribLocation(shader_program, attrib_name);
  	gl.enableVertexAttribArray(location);
  	gl.vertexAttribPointer(location, count, gl.FLOAT, false, vertex_size * 4, offset * 4);
  	return location;
  },
  
  r_create_texture = (c) => {
  	let t = {t:gl.createTexture(), c};
  	gl.bindTexture(gl.TEXTURE_2D, t.t);
  	gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
  	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);
  	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
  	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
  	gl.generateMipmap(gl.TEXTURE_2D);
  	r_textures.push(t);
  },
  
  r_prepare_frame = (r,g,b) => {
  	gl.clearColor(r,g,b,1);
  	gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
  
  	r_num_lights = 0;
  	r_light_buffer.fill(0);
  },
  
  r_end_frame = () => {
  	gl.uniform4f(r_u_camera, r_camera.x, r_camera.y, r_camera.z, 16/9);
  	gl.uniform2f(r_u_mouse, r_camera_yaw, r_camera_pitch);
  	gl.uniform3fv(r_u_lights, r_light_buffer);
  
  	let vo = 0,
  		last_texture = -1;
  	for (let c of r_draw_calls) {
  		
  		// c = [x, y, z, yaw, pitch, texture, offset1, offset2, mix, length]
  
  		// Bind new texture only if it changed from the previous one. The map
  		// is sorted by texture indices, so this helps.
  		if (last_texture != c[5]) {
  			last_texture = c[5];
  			gl.bindTexture(gl.TEXTURE_2D, r_textures[last_texture].t);
  		}
  
  		gl.uniform3f(r_u_pos, c[0], c[1], c[2]);
  		gl.uniform2f(r_u_rotation, c[3], c[4]);
  		gl.uniform1f(r_u_frame_mix, c[8]);
  
  		// If we have two different frames, calculate the offset from the
  		// drawArrays call to the mix frame.
  		// Setting the vertexAttribPointer is quite expensive, so we only
  		// do this if we have to; i.e. for animated models.
  		if (vo != (c[7]-c[6])) {
  			vo = (c[7]-c[6]);
  			gl.vertexAttribPointer(r_va_p2, 3, gl.FLOAT, false, 8 * 4, vo*8*4);
  			gl.vertexAttribPointer(r_va_n2, 3, gl.FLOAT, false, 8 * 4, (vo*8+5)*4);
  		}
  		gl.drawArrays(gl.TRIANGLES, c[6], c[9]);
  	}
  
  	// Reset draw calls
  	r_draw_calls = [];
  },
  
  r_draw = (pos, yaw, pitch, texture, f1, f2, mix, num_verts) => {
  	r_draw_calls.push([
  		pos.x, pos.y, pos.z, yaw, pitch,
  		texture, f1, f2, mix, num_verts
  	]);
  },
  
  r_submit_buffer = () => {
  	gl.bufferData(gl.ARRAY_BUFFER, r_buffer.subarray(0, r_num_verts*8), gl.STATIC_DRAW);
  },
  
  r_push_vert = (pos, normal, u, v) => {
  	r_buffer.set([pos.x, pos.y, pos.z, u, v, normal.x, normal.y, normal.z], r_num_verts * 8);
  	r_num_verts++;
  },
  
  r_push_quad = (v0, v1, v2, v3, u, v) => {
  	let n = vec3_face_normal(v0, v1, v2);
  	r_push_vert(v0, n, u, 0);
  	r_push_vert(v1, n, 0, 0);
  	r_push_vert(v2, n, u, v);
  	r_push_vert(v3, n, 0, v);
  	r_push_vert(v2, n, u, v);
  	r_push_vert(v1, n, 0, 0);
  },
  
  r_push_block = (x, y, z, sx, sy, sz, texture) => {
  	let canvas = r_textures[texture].c,
  		index = r_num_verts,
  		tx = sx/canvas.width,
  		ty = sy/canvas.height,
  		tz = sz/canvas.width,
  		
  		// top
  		v0 = vec3(x, y + sy, z),
  		v1 = vec3(x + sx, y + sy, z),
  		v2 = vec3(x, y + sy, z + sz),
  		v3 = vec3(x + sx, y + sy, z + sz),
  
  		// bottom
  		v4 = vec3(x, y, z + sz),
  		v5 = vec3(x + sx, y, z + sz),
  		v6 = vec3(x, y, z),
  		v7 = vec3(x + sx, y, z);
  
  	r_push_quad(v0, v1, v2, v3, tx, tz); // top
  	r_push_quad(v4, v5, v6, v7, tx, tz); // bottom
  	r_push_quad(v2, v3, v4, v5, tx, ty); // front
  	r_push_quad(v1, v0, v7, v6, tx, ty); // back
  	r_push_quad(v3, v1, v5, v7, tz, ty); // right
  	r_push_quad(v0, v2, v6, v4, tz, ty); // left
  	return index;
  },
  
  r_push_light = (pos, intensity, r, g, b) => {
  	// Calculate the distance to the light, fade it out between 768--1024
  	let fade = clamp(
  		scale(
  			vec3_dist(pos, r_camera), 
  			768, 1024, 1, 0
  		),
  		0, 1
  	) * intensity * 10;
  
  	if (fade && r_num_lights < R_MAX_LIGHT_V3/2) {
  		r_light_buffer.set([pos.x, pos.y, pos.z, r*fade, g*fade, b*fade], r_num_lights*6);
  		r_num_lights++;
  	}
  };
  
  