Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Three.js canvas.toDataURL sometimes blank

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;
   }
} 
like image 294
TheJim01 Avatar asked Mar 18 '23 04:03

TheJim01


1 Answers

(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?).

like image 198
caseygrun Avatar answered Apr 25 '23 17:04

caseygrun