How to optimize large scene(10000x10000px) on zoom

Hello,
I’ve looked for the answer on the forum but couldn’t find anything that solves my issue, so I’m making this thread.

I have a case where my scene has set boundaries 10000x10000px. I have 8 smaller pieces of background which I’m placing next to each other, which makes the full scene composition. There is a case where I need to zoom out the scene to the maximum. My application has a resolution of 1920x1080. The problem lies in performance. Putting these 8 pieces of images makes integrated graphics explode on 60fps, it’s always next to 90% of usage when maximally zoomed out, even tho there’s only 8 images. It’s not stuttering at all, but fans are ultra loud.

The reason, probably, is how it redraws the whole canvas. If I decrease the amount of FPS to 15fps, the amount of GPU usage is reduced to ± 40%. This is still a lot(taking the fact that I’m showing 8 images zoomed out), but much better. I think that under the hood, every frame renders 10000x10000 pixels, which are scaled to FHD.

This moves out to the question - how to deal with that? I tried:

  • decreasing the quality of images and switching them in real-time (high-res to low-res) to help ScaleManager, but it seems it’s rendering the same amount of pixels, but in worse quality
  • to put an image in the background in CSS, but it generates problems with zooming, moving around the map, and changing it
  • to use .pvr with S3TCSRGB, but it’s not improving performance significantly

I have no clue how to deal with that. I can’t decrease fps, the size of a canvas, the zoom amount, or the boundaries of the scene.

You can think about this project as a map of the whole world, where you can click on countries to get information about them. The only animated thing out there will be a pop-up on some capitals(on a very specific event) and a color change on the sprite on hovering over it.

Is there any way to simplify the redraw mechanism or mark specific images as “won’t change, you can modify the previous one”?

Seems this should work if you use 1 image (4000px, 2000px, etc.) for the entire world.

There may be a Phaser.GameObjects.DOMElement method that can help with that.

This is the idea behind the “pause render” plugin. But you still have to render all or nothing.

Seems this should work if you use 1 image (4000px, 2000px, etc.) for the entire world.

How would you write that? Adding 2x6 images and changing visibility on specific zoom, and decreasing boundaries of the scene? It might be problematic since my positioning is based on xy {0.0} point, so I would have to recalculate all positions as well.

There may be a Phaser.GameObjects.DOMElement method that can help with that.

It works; I managed to reduce GPU usage to ± 30%. I tested 2 options:

  1. Add directly via this.dom method
    Problem: The inline styles are so heavy that they make frame drops, also huge problems with styling it
  2. Add a standalone div, change the translate and scale on the update event with cam.dirty check
    Problem: There are issues with camera movement(it’s tearing, it looks like DOM is doing it from point A to B and it’s jumping). If I add some kind of smoothing, then it looks like bg is floating on some kind of water, like buoy on waves. They are just delayed. Dunno how to deal with that. Also, If I resume it on tab change, everything is getting “darker” for a second(alpha channel?), then it works normally

This is the idea behind the “pause render” plugin. But you still have to render all or nothing.

Can’t use that

For example, make a single image for the entire background at size 20%, 2000x2000. Then when the camera is fully zoomed out (0.2), show this image at scale 5, so it’s filling 10000x10000 world pixels. Should be able to keep all your positioning logic, since world coordinates would be the same.

Try scene prerender or camera followupdate event if camera is following.

If you were expecting a mini-map, it might be simpler to create a separate mini-map.png (e.g., 1000x1000px) and mark sprites/game objects on it. If you were trying to implement zoom functionality, please refer to the approach I used below.

In my previous development, I encountered a scenario with a 40,000px x 40,000px map where the window resolution was unrestricted. Below is the general processing logic I used (from an older project, which may differ slightly from the actual code):

  1. Tile size: 40px x 40px
  2. Map dimensions: 1000 tiles x 1000 tiles, resulting in a total map size of 40,000px x 40,000px
  3. Chunking: The map was divided into a 200x200 array of chunks. Each chunk contained a 5x5 grid of tiles (25 tiles total).
  4. Center Mapping: The center of the screen was mapped to determine the current chunk[x][y].
  5. Nearby Chunk Calculation: Logic was implemented to calculate the map tiles within the chunks[x][y] and its surrounding chunks (the visible area, pay attention to screen resolution here).
  6. Non-Nearby Tile Removal: Logic was implemented to remove tiles not belonging to the chunks[x][y] and its surrounding chunks.
  7. Loading & Unloading: Based on the chunk[x][y] determined in step 3:
  • The surrounding map tiles (calculated in step 4) were loaded.
  • Non-surrounding map tiles (determined by step 5) were removed.

Hi there!

You’re absolutely right — when the camera is zoomed out, Phaser still needs to render a huge surface (effectively a 10,000×10,000 area scaled down to your 1920×1080 screen). Even if there are only 8 background images, drawing them separately on each frame can be expensive — especially on integrated GPUs.

A few suggestions to optimize this:

  1. Make one big sprite instead of multiple.

  2. Use a RenderTexture to pre-render your background (I don’t like it but you can try):

    • Phaser lets you draw images onto a RenderTexture once, and then treat it as a single static sprite. This way, on each frame only one big texture is rendered, instead of 8 separate ones.
    • Example:
      const rt = this.make.renderTexture({ width: 10000, height: 10000 }, true);
      rt.draw([yourSprites...]); // draw once
      
      Now you can remove the individual background sprites and just keep this one.
  3. Avoid making background interactive:

    • Interaction costs extra checks per frame. Instead, calculate mouse/touch world coordinates and act based on them, e.g.:
      const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
      
  4. Pre-scale images:

    • If you’re scaling, consider loading different sprite versions when zoomed out/in. Obviously you can use less details if it is possible. Technically it looks like different scenes for different zoom.
  5. Disable re-rendering when nothing changes.

So, from your description, I would like to combine 1, 3, 5.

  • Separate zoom range into a few parts/scenes (scene1 = zoom 1…2, scene2 = zoom 2…3, …).
  • Use one sprite instead of multiple (usually it helps),
  • Use one sprite/image/background for each zoom scene part (for detail/quality reason),
  • Do not use interactive sprite, watch clicks positions instead.
  • Optionally, switch off scene rendering if it possible.

Note that pausing a scene won’t save any rendering resources, because it’s still visible.

Sorry for not responding, but I was summing up all my results.

I took @OlexandrC recommendation and decreased the background image’s size to 10000x7000px. I also decided to push it into 1 image. Non-crucial elements were moved as divs below and above the scene. As a result, it gave me ± 5% less of the GPU usage.

Then I tried to separate BG into smaller chunks as @JinDaiCN recommended, but for my scenario (where I need to see everything on max zoom) @samme ideas were much better. On max zoom, combining all chunks resulted in the same, if not worse(1-2%), performance compared to the full image. I don’t have problems with many animated things on the map, so I decided to pick the simpler one, which is making 3 images (10000x7000, 5000x3500, 2500x1750) and scaling them to the correct size, while switching on zoom. It works great, zero complaints. The idea with smaller chunks is great for high-density maps, which I will have to make in the future, so thank you for the hint @JinDaiCN !

What is more, I tried to fix the code with an image on the background set as CSS. I can say “prerender” event was a key that made everything work almost smoothly. If you focus, you will notice jiggling, but it doesn’t break your eyes on mid/high-end devices. It’s a little bit worse on the low-end. The flickering game elements were fixed by setting ‘clearBeforeRender’ inside the config. The only problem was FPS drop, which was noticeable on one of the laptops (3050 Ti).

I did tests.
Settings: roundPixels false, antialias: true, pixelArt: false, clearBeforeRender: false, premultipliedAlpha: true, fps limit 60/165, disabled all FX, imageLoadType set to HTMLImageElemenet, resolution: 1920x911. Tests were made on the Ryzen 6800H iGPU(680m), Phaser 3.88.2

Base:
60fps: No BG - 14% GPU Usage
60fps: BG - 32%
165fps: No BG - 24%
165fps: BG - 60-62%
Problems: none

IMG inside the div
60fps: No BG - 14%
60fps: BG - 17%
165fps: No BG - 23%
165fps: BG - 30%
Problems: image flickering(clearBeforeRender: true fixes it), on one laptop frame drops while moving the camera

LOD
60fps: No BG - 14%
60fps: BG - 17%
165fps: No BG - 23%
165fps: BG - 27%
Problems: none

IMG + LOD
60fps: No BG - 14%
60fps: BG - 14%
165fps: No BG - 23%
165fps: BG - 24%
Problems: image-flickering(clearBeforeRender: true fixes it), frame drops on all laptops, BG flickers while switching the image(preload doesn’t help, requesting animation as well)

So I decided to pick LOD as a good balance between optimization and deep tinkering. Combined with powerPreference “low” in config, I can’t hear a fan, so it’s perfect. I thought about further optimization (huge VRAM usage) with .pvr/.ktx textures and, oh man…

The support which comes from the browser/os sucks. On Win11 Chrome I have only s3tc/s3tc_srgb/bptc(fallback), on Linux Chrome I have astc, etc, etc1, s3tc, s3tc_srgb and bptc(fallback). Each of them makes huge files on the drive (4-8x more than normal .webp/.avif file), there are a lot of problems with the tools (.pvr + s3tcrgb looks great in texture packer, but in phaser it’s dark, like it was without brightness). PVRTexTool hasn’t had tools for encoding the stuff since 2020, and NVCompress creates files that make Phaser crash. After fixing everything(in Texture Packer), I managed to get ± 3% of performance on S3TCSRGB, but I decided to resign; it takes too much effort, and the gains are almost negligible.

I’m not sure what else I can do to optimize it. I believe WebGL itself is a bottleneck right now, so I probably have to wait for WebGPU implementation.

TLDR:

  • LOD + IMG BG > LOD > IMG
  • LOD + IMG BG requires a lot of tinkering
  • IMG BG generates a jiggling effect on camera move, which is more visible on low-end devices
  • img via phaser dom is terrible, “transform-origin” switching inside inline styles consumes all gains on performance
  • don’t waste your time on GPU textures if you want to optimize it on devices other than a Windows

Thank you @samme @OlexandrC @JinDaiCn