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;
}
}
(For the benefit of future Googlers): You need to set preserveDrawingBuffer
to true
when you instantiate WebGLRenderer
:
renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
(original code had this, but there was a typo). This is needed because by default, WebGL doesn't require browsers to save the depth/color buffers after each drawing frame, so if you want to use readPixels
or toDataURL
, you need to explicitly tell the implementation to save the drawing buffer using that flag.
However, there's a performance penalty to doing this; the spec provides some guidance if this is a problem for you:
While it is sometimes desirable to preserve the drawing buffer, it can cause significant performance loss on some platforms. Whenever possible this flag should remain false and other techniques used. Techniques like synchronous drawing buffer access (e.g., calling readPixels or toDataURL in the same function that renders to the drawing buffer) can be used to get the contents of the drawing buffer. If the author needs to render to the same drawing buffer over a series of calls, a Framebuffer Object can be used.
I'm not sure either of those suggestions are feasible in the current version of three.js (r68), but another alternative solution (to call readPixels
or toDataURL
on a copy of the canvas) is discussed on several other questions (Why does my canvas go blank after converting to image? , How do you save an image from a Three.js canvas?).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With