How to handle 2K/4K panorama texture (Tils) uploads without frame spikes

Hey everyone

I’m currently building a Matterport-like 3D viewer where panoramic photos are projected onto a mesh
Everything works fine at 512px faces even 1k faces.

But when I start assigning 2K or 4K face textures, I get a noticeable performance spike (frame drop at the same time or only frame drop) each time a new face texture is uploaded to the GPU.
This happens even though I’ve already optimized the loading pipeline as much as possible:
Using createImageBitmap() for decoding.

Uploading with gl.texImage2D() and renderer.initTexture() on requestAnimationFrame.

Loading faces one by one, not all at once.

Even moved all image loading into a Web Worker (off-main-thread).

!!! Despite all that, the GPU spike still happens the moment the 2K/4K texture is assigned to the shader.
You can literally see in Performance Monitor.

I know that Matterport and similar systems use tiling (splitting each face into smaller sub-images), but implementing that seems tricky since WebGL2 only allows up to 16 active sampler2D uniforms,Sampler2dArray or at least for Device capabilties, while even a single 4K cube face would need hundreds of tiles (24–384 !!).

So my question is:

How do systems like Matterport manage smooth transitions and high-res tiled panos without hitting GPU upload stalls or uniform limits?

I’ve tried atlases but too large, spikes again when updated same issue.

Matterport’s view:

https://discover.matterport.com/space/9gCmRSBt3qa

My current viewer’s performance drop in the moments when switching to high-res textures:

Any insights or best practices on:

Progressive GPU uploads

Async or incremental texture binding

Efficient tile management (beyond the 16 texture limit)

1 Like

it works for me :

anyway it dose not matter what space you could see here any space that opens for you

https://discover.matterport.com/space/gzbxoZ1DVDd

You could checkout three.js virtual tours is much similar to your project, it preloads possible 8k equirectangular 360 images and disposing images that are not. You can imagine this project much like a carousel which you swipe left or right but instead of displaying it in 2d you display it on three.js canvas and you can look around at all direction, and the nodes you click to move from one 360 image to another are the next and previous button in a 2d carousel.

Yeah, I’ve checked out that three.js virtual tour project before,it’s definitely cool and nicely implemented.

However, what I’m building is a bit different from that approach.
That project basically swaps out entire equirectangular panoramas (like a 360° carousel, as you mentioned), but it doesn’t use tiled cube faces or projection onto 3D geometry the way Matterport or mine does.

In my case, each cube face is split into smaller texture tiles that get streamed and assigned dynamically, so the issue I’m facing is specifically about GPU upload stalls when assigning higher-resolution tiles (2K - 4K - 8K per face), not just image swapping.

This is another example of the same approach used:

So while that project is great for full-panorama navigation, it doesn’t really address the performance side of tiled cube textures or texture streaming across many materials :slight_smile: which is the main challenge here.

1 Like

You’re absolutely right, the three.js virtual tours face that limitation, especially noticeable on slower internet connections or low-spec devices. The approach you’re describing could really solve the issue our clients are experiencing.

Right now, the tours often crash due to memory allocation problems, basically, they break when trying to load large, high-resolution images all at once. In real estate, image quality is non-negotiable since clients want the sharpest visuals possible to showcase properties.

That’s why your idea of streaming the image in smaller, virtual pieces instead of loading the entire panorama upfront sounds revolutionary. It could make high-quality real estate virtual tours run much smoother and more efficiently.

GPU texture compression is going to be your best option to prevent stalls during image decoding and upload. createImageBitmap gets the decompression off the main thread, but you still have to upload that large uncompressed image to the GPU. With GPU formats, the texture remains compressed on the GPU, so it’s a much smaller/faster upload. There are various formats available, but KTX2 with Basis Universal compression is the most practical to adopt in three.js. See:

3 Likes

Thanks Don, totally agreed.

KTX2/Basis makes sense, even with createImageBitmap and staggered uploads, the stall clearly happens on the GPU upload, not the decode. So compressed GPU formats seem like the right long-term direction.

I’ve also dug into Matterport’s pipeline (old + new tours) and they still serve regular JPEG tiles. They mainly avoid stalls by going very small on tile size and aggressively reusing textures, so uploads per frame are tiny.

I’m also experimenting with a multi-pass idea - show low-res tiles instantly, then swap in high-res tiles in small steps - plus texture pooling. That should keep frame times stable, but still testing.

So likely a hybrid:
KTX2 / Basis
fine-grained tile streaming
texture pooling
progressive/multi-pass tile upgrades (still experimental)

Also just to clarify,this thread was more meant as a discussion than a direct “how to”, hoping to connect with others doing Matterport-style streaming.

Your answer definitely helps, thanks again.

1 Like