I'm trying to render a THREE.js scene + some overlaid HTML elements using html2canvas.js. It works most times, but not all the time.
In the failure cases, the HTML elements are rendered (the background, overlays, etc.) but nothing else. The THREE.js scene acts as if it's completely empty, even though it visibly has data in it. I can say that it usually fails for larger models, but only early into the rendering. It does eventually work in all cases, but the larger models take about 30 seconds. It's as if I have to give the buffer some time to stabilize.
html2canvas handles the THREE.js canvas as you would expect--it simply uses drawImage
to draw the THREE.js canvas onto the new canvas that is eventually returned by the library.
Otherwise, I try to ensure that nothing else is busy with the canvas, like in this fiddle: http://jsfiddle.net/TheJim01/k6dto5sk/63/ (js code below)
As you can see, I'm doing quite a bit to try to block the render loop, and perform just one more render when I want to capture the scene. But even all of these precautions don't seem to help.
Is there a better way to fetch the image from the THREE.js canvas? I can potentially do that part manually, then swap out the THREE.js canvas for a fetched image just long enough for html2canvas to do its thing, then swap the THREE.js canvas back in. I'd prefer not to do it that way, so I don't muddy up the DOM, should a user make lots of snapshots (image resources, image resources everywhere...).
Anyway, here's the code. Any ideas or suggestions are welcome. Thanks!
var hostDiv, scene, renderer, camera, root, controls, light, shape, theta, aniLoopId, animating;
function snap() {
animating = false;
cancelAnimationFrame(aniLoopId);
renderer.render(scene, camera);
// html2canvas version:
/*
var element = document.getElementById('scenePlusOverlays');
// the input buttons represent my overlays
html2canvas( element, function(canvas) {
// I'd convert the returned canvas to a PNG
animating = true;
animate();
});
*/
// This is basically what html2canvas does with the THREE.js canvas.
var c = document.getElementById('rendererCanvas');
var toC = document.createElement('canvas');
toC.width = c.width;
toC.height = c.height;
var toCtx = toC.getContext('2d');
toCtx.drawImage(c, 0, 0);
console.log(toC.toDataURL('image/png'));
animating = true;
animate();
}
function addGeometry() {
//var geo = new THREE.BoxGeometry(1, 1, 1);
var geo = new THREE.SphereGeometry(5, 32, 32);
var beo = new THREE.BufferGeometry().fromGeometry(geo);
geo.dispose();
geo = null;
var mat = new THREE.MeshPhongMaterial({color:'red'});
var msh;
var count = 10;
count /= 2;
var i = 20;
var topLayer = new THREE.Object3D();
var zLayer, xLayer, yLayer;
for(var Z = -count; Z < count; Z++){
zLayer = new THREE.Object3D();
for(var X = -count; X < count; X++){
xLayer = new THREE.Object3D();
for(var Y = -count; Y < count; Y++){
yLayer = new THREE.Object3D();
msh = new THREE.Mesh(beo, mat);
yLayer.add(msh);
msh.position.set((X*i)+(i/2), (Y*i)+(i/2), (Z*i)+(i/2));
xLayer.add(yLayer);
}
zLayer.add(xLayer);
}
topLayer.add(zLayer);
}
scene.add(topLayer);
}
var WIDTH = '500';//window.innerWidth,
HEIGHT = '500';//window.innerHeight,
FOV = 35,
NEAR = 0.1,
FAR = 10000;
function init() {
hostDiv = document.getElementById('hostDiv');
document.body.insertBefore(hostDiv, document.body.firstElementChild);
renderer = new THREE.WebGLRenderer({ antialias: true, preserverDrawingBuffer: true });
renderer.setSize(WIDTH, HEIGHT);
renderer.domElement.setAttribute('id', 'rendererCanvas');
hostDiv.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(FOV, WIDTH / HEIGHT, NEAR, FAR);
camera.position.z = 500;
controls = new THREE.TrackballControls(camera, renderer.domElement);
light = new THREE.PointLight(0xffffff, 1, Infinity);
light.position.copy(camera.position);
scene = new THREE.Scene();
scene.add(camera);
scene.add(light);
animating = true;
animate();
}
function animate() {
if(animating){
light.position.copy(camera.position);
aniLoopId = requestAnimationFrame(animate);
}
renderer.render(scene, camera);
controls.update();
}
EDIT: Calling readPixels or toDataURL in the manner described is possible--I had considered a similar method to grab the buffer, but was put off by the amount of asynchronous code required. Something like this:
var global_callback = null;
function snapshot_method() {
global_callback = function(returned_image) {
// do something with the image
}
}
// ...
function render() {
renderer.render();
if(global_callback !== null) {
global_callback(renderer.domElement.toDataURL());
global_callback = null;
}
}