Search code examples
javascriptthree.jsaframepdf.js

A-Frame + pdf.js: unable to update canvas texture twice, in VR mode only


I am using A-Frame and pdf.js to create an application to view PDF files in VR. The application works as expected on desktop, including advancing to the next page of the PDF document and re-rendering to a canvas.

When running in a browser in VR (tested with Quest 2), the first attempt renders as expected. However, when rendering a second document page to the canvas, the new image fails to appear unless exiting VR mode, at which point the new texture appears as expected.

I have tried setting the associated material map.needsUpdate repeatedly (in an A-Frame component tick loop) to no effect. While experimenting, I also noticed that if you try to render a new PDF page a second time and then another page a third time (advancing by two pages while in VR mode), when exiting VR mode, only the texture from the second time appears; the data from the third render appears to be lost - I wonder if this is related to the primary issue.

Code for a minimal example is below, and a live version is available at http://stemkoski.net/test/quest-pdf-min.html . When viewing the example in desktop mode, you can advance to the next page by opening the browser JavaScript console and entering testBook.nextPage(); and see that the image changes as expected. You can test in VR mode with a Quest headset; pressing the A button should advance to the next page.

The question is: how can I render pages of a PDF document to the same canvas, multiple times, while in VR mode in A-Frame?

<!DOCTYPE html>
<html>

<head>
    <title>A-Frame + PDF.js</title>
    <script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.6.347/pdf.js"></script>
</head>

<body>
<script>
AFRAME.registerComponent("interactive-pdf", {

    schema:
    {   
        fileName:   {type: 'string', default:"assets/test10.pdf"},
        pageWidth:  {type: 'float',  default: 1200},
        pageHeight: {type: 'float',  default: 1500},
    },

    init: function () 
    {
        this.displayPlane = document.getElementById("plane1");
        this.displayMaterial = this.displayPlane.getObject3D('mesh').material;
        this.canvas = this.displayPlane.getAttribute("material").src
        this.canvas.setAttribute("width", this.data.pageWidth);
        this.canvas.setAttribute("height", this.data.pageHeight);
        this.context = this.canvas.getContext('2d');
        this.textArea = document.querySelector("#textArea");

        this.pdf = null;
        this.currentPage = 1;

        pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.6.347/pdf.worker.js";

        let self = this;
        pdfjsLib.getDocument(this.data.fileName).promise.then( function(pdf) {
            self.pdf = pdf;
            self.render(1);
        });
    },

    render(pageNumber = 1)
    {
        if (!this.pdf)
            return;

        let self = this;

        let text = "rendering page " + pageNumber + " of " + this.pdf.numPages + " on canvas";
        this.textArea.setAttribute("text", "value", text);

        this.pdf.getPage(pageNumber).then( function(page) {

            const pageViewport = page.getViewport({ scale: 2 });
            const context = self.canvas.getContext("2d");
            const renderContext = {
              canvasContext: context,
              viewport: pageViewport
            };

            const renderTask = page.render(renderContext);

            renderTask.promise.then( function() {
                self.displayMaterial.map.needsUpdate = true;
            });    
        });
    },

    nextPage: function()
    {
        this.currentPage++;
        this.render( this.currentPage );
    },

    prevPage: function()
    {
        this.currentPage--;
        this.render( this.currentPage );
    },

    tick: function()
    {
        // this.displayMaterial.map.needsUpdate = true;
    }

});

AFRAME.registerComponent('page-turner', {
  
  init: function () 
  {
      let bookComponent = document.getElementById("interactive-pdf").components["interactive-pdf"];
      this.el.addEventListener('abuttondown', function(event) { bookComponent.nextPage(); } );
  }

});
</script>

<a-scene>
        
    <a-assets>
        <!-- use to display pdf pages -->
        <canvas id="canvas1"></canvas>
    </a-assets>

    <a-camera></a-camera>

    <a-sky color = "#000337"></a-sky>

    <a-plane id="plane1" material="shader: flat; src: #canvas1;" position="0 1 -2"></a-plane>

    <a-entity id="interactive-pdf" position="0 1 -2" 
        interactive-pdf="fileName: assets/test10.pdf; pageWidth: 1224; pageHeight: 1584;">
    </a-entity>
        
    <a-entity id="left-controller" oculus-touch-controls="hand: left"></a-entity>
    <a-entity id="right-controller" oculus-touch-controls="hand: right" page-turner></a-entity>   

    <!-- text area to print debug text -->
    <a-entity id="textArea" position="0 2 -2"
        geometry="primitive: plane;  width: 3; height: auto"
        material="color: #444444;"
        text="anchor: center; color: #8888FF; value: debug text here">
    </a-entity>

</a-scene>

<script>
var testBook = document.getElementById("interactive-pdf").components["interactive-pdf"];
</script>

</body>
</html>

Solution

  • Found an answer here:

    requestAnimationFrame doesn't fire when WebXR is engaged

    Its quite relevant because pdf.js uses requestAnimationFrame by default when rendering the image onto the canvas.

    It can be toggled though - the pages render function has an option intent, which is later on used to determine whether to use requestAnimationFrame.


    The sources You use are slightly different (older?), but if you search for creating the InternalRenderTask in the render function of the ProxyPDFPage - it's all there:

    const internalRenderTask = new InternalRenderTask({
      callback: complete,
      params: {
        canvasContext,
        viewport,
        transform,
        imageLayer,
        background
      },
      objs: this.objs,
      commonObjs: this.commonObjs,
      operatorList: intentState.operatorList,
      pageIndex: this._pageIndex,
      canvasFactory: canvasFactoryInstance,
      webGLContext,
      useRequestAnimationFrame: renderingIntent !== "print",
      pdfBug: this._pdfBug
    });
    

    You can set the intent to print in your renderContext object:

    // 'interactive-pdf'.render()
    const renderContext = {
              canvasContext: context,
              viewport: pageViewport,
              intent: "print"
    };
    const renderTask = page.render(renderContext);
    

    And it seems to be working properly:

    enter image description here

    I'm not sure, if setting the intent to print won't cause undesired effects (it affects other settings as well), so disabling useRequestAnimationFrame in the source does the trick as well.

    Check out the version with the 'print' intent, or the one with the modified pdfjs source code.