How to run PlayCanvas headless in the modern day

Hey so I’ve seen a bunch of old forum posts about running playcanvas engine headless, and using old tools like playcanvas-mock or headless-gl, but nothing seems to be up to date from what I’ve seen. I’m trying to render and screenshot a single frame.

Currently I’m using puppeteer but it’s relatively slow. I want to avoid running the browser at all if possible, and run the project directly through node or some other runtime engine. I have a framebuffer-screenshot system working currently.

TL;DR
I want to render and store a screenshot of a PlayCanvas scene as fast as possible. Been stuck for some time so direction is much appreciated.

Thanks in advance :slight_smile:

The issue is that you would need to have something that can handle a WebGL canvas and usually, the easiest is to have headless browser on hardware that can handle 3D rendering natively instead of using the Swift software renderer on Chrome.

There’s also things like GitHub - stackgl/headless-gl: 🎃 Windowless WebGL for node.js that I’ve not used before but looks like it could be an alternative to using a headless browser.

Worth noting that the engine examples browser has a build target to create screenshots for thumbnails. https://github.com/playcanvas/engine/blob/main/examples/package.json#L12

1 Like

Cheers, always appreciate your responses yaustar

Unfortunately I think headless-gl is struggling with age as it only supports WebGL 1 plans for webgl 2 support? · Issue #109 · stackgl/headless-gl · GitHub and I believe PlayCanvas is deprecating WebGL 1 in the near future(?). In any case I got a lot of ‘function not implemented’ errors when trying to work with it. Not sure if anyone else has had better success

And from digging into the build target engine/examples/scripts/build-thumbnails.mjs at main · playcanvas/engine · GitHub “This file spawns a pool of puppeteer instances to take screenshots of each example for thumbnail.” so they’re still using puppeteer for doing it. Makes sense for thumbnails that only run on build, but we need it running fast and often.

For context, we’re trying to actively render and save screenshots of user generated characters using a cheap VPS. Works great and takes about 10 seconds per character with puppeteer. But we need to cut that down to <1 second to scale it for our purposes

At some point in the future you might want to look at headless WebGPU as well, that will likely be a lot better supported than WebGL2.

1 Like

Have you broken down which part of the process takes the time?

Eg browser startup, PlayCanvas app startup, render frame etc?

If it’s known what is taking the time, perhaps there are other ways to bring down that time

Eg, could you have the browser and app already loaded and all it has to do is load the custom character asset, render and deliver the file. That bypasses the initial browser spin up time and app load

2 Likes

Huh. You’re absolutely right, we could just run the app once and then communicate with it to render and save new characters as needed…

Bulk of the work is spinning up the environment itself. Rendering characters takes about a second but can be optimized.

Thank you for giving me a new perspective on a solution, I was too hyperfocused in mine!!

Cheers :slight_smile:

1 Like

You may need to cycle the app/browser every now and then to potentially clear memory or whatever but see how that goes for you :slight_smile:

You could also customise the app a bit so you can stop the update and render loop when it’s not in use so you aren’t spinning cycles/using the CPU unnecessarily

1 Like

Yep, so that ended up working fantastically. I do an npm run serve for my actual project, and alongside that host an esm module running express on localhost that manages a puppeteer instance.

Then I just do little post requests to send commands and auto close/open the browser every X screenshots just in case of leaks. Runs in <1s on the cheapest VPS instance available.

Thanks again! And happy holidays :slight_smile:

3 Likes

FYI, and for future readers, there are a few IaaS platforms that offer this. Notably Cloudflare have a scalable solution using browser instrumentation, similar to Puppeteer Browser Rendering · Browser Rendering docs

1 Like

I know you’re interested in rendering, but I did just write this new user manual page which you might find interesting:

As @mvaligursky suggests, headless WebGPU is probably the future for rendering in Node.js, but as of today, there isn’t yet an official/popular NPM package for this. Although something might grow from this one day:

https://dawn.googlesource.com/dawn/+/refs/heads/main/src/dawn/node/

1 Like

Hi Will

Im having a trouble getting the application to start once I configured JSDOM and created the application. The tick will be executed only once when I call app.start(). I followed the manual and called app.start() immediately afterwards.

The reason seems to be that platform.browser and platform.worker are both false, which causes AppBase.makeTick() to not request another animation frame. I think I’m just missing some more setup steps, but I’m not sure which. Could you help me please?

Thanks

Hmm interesting. We use Node to unit test the engine, and at the moment, none of the unit tests execute the main application loop (update/render). So we haven’t tested that before. I know various developers have run the engine in Node before though - I just don’t know if they had to hack anything to get it working. You could maybe try setting platform.environment to browser before creating your AppBase instance perhaps?

1 Like

I got it to work. I had to do a bit more though, these are the steps I took:

As you suggested, I needed to set the platform.evironment to browser, but I also needed to set the flag platform.browser to true, as it doesn’t update itself when setting the environment.

// Before creating the app instance
pc.platform.environment = "browser";
pc.platform.browser = true;

The next problem I ran into was that the methods requestAnimationFrame and cancelAnimationFrame were still not defined. JSDOM provides these methods, but only if I added the option pretendToBeVisual: true to its constructor. Then I needed to set them on the global object afterwards.

this.jsdom = new JSDOM(html, {
    resources: 'usable',          // Allow the engine to load assets
    runScripts: 'dangerously',    // Allow the engine to run scripts
    url: 'http://localhost:3000', // Set the URL of the document
    pretendToBeVisual: true
});

global.requestAnimationFrame = this.jsdom.window.requestAnimationFrame;
global.cancelAnimationFrame = this.jsdom.window.cancelAnimationFrame;

Now I’m able to start the application and listen to the update event :raised_hands:

Thanks for the help

1 Like