How can I optimise my THREE.JS rendering?

I am currently loading in a “map” and after working out how to do the drawing of said map I am getting some issues with the framerate tanking on a older but fairly beefy machine, the machine has a i7 8th gen cpu and a Radeon WX something GPU, it is a laptop but can run most things fine.
My map consists of 357 floors and 357 ceilings, these are being drawn twice from the same shape geometry as it has a different “texture” which is just colours at the moment, these are meshes created from the shape, so 714 meshes drawn for the floors + ceilings, the walls of the map are individually textured and there are 2287 of them, smaller maps are OK, like with 20 floors and ceilings and 100 walls, but I fear that each Mesh I create is making a draw call, that is quite a lot of draw calls my i7 9th gen with RTX GPU maintains 60fps but that was built for gaming, I am drawing my walls as planes (THREE.PlaneGeometry) and the floors / ceilings as ShapeBufferGeometry, I looked at Frustum culling but it made no difference, I tried making everything invisible using raycasting to only enable things within its rays, made no difference, I looked at merging but I only found that you can merge geometries but that complicates things when all planes and shapes can have a different material, I saw that occlusion culling might be the way forward which is what the hiding objects until the raycaster see’s it was meant to simulate, but that doesn’t seem to help.

Are there any tips or tricks that I can use with THREE.js to reduce the number of draw calls?

1 Like
  1. Frustum culling is done by three out of the box (as long as each mesh has it’s own separate geometry.)
  2. Merging geometries prevents frustum culling, so merge only geometries that are most of the time smaller than the size of the viewport (ex. do not merge terrain into a single, giant mesh.)
  3. Take a look at LOD.
  4. Take a look at InstancedMeshes (if a lot of meshes in the scene use the same geometry + material combination, group them together into an InstacedMesh. That way you can still have plenty of these meshes on the screen, but they all cost just a single draw call.)
  5. For raycasting - consider building a spatial index (ie. “regions” / bigger groups in 3D space), and only raycast against the meshes within the closest spatial regions.
  6. Simply don’t render stuff that is too far away to be visible / readable to the user.
2 Likes

Hi @mjurczyk, thanks for your reply.

  1. OK that explains why it seems not to do anything, yes each mesh has its own geometry.
  2. I cannot merge geometries as every single wall can have different material properties.
  3. Interesting i’ll take a look at LOD.
  4. Well technically in terms of the geometry the planes can be the same if the walls are the same dimensions, so for example if a map is a cube all 4 walls have the same height and width / length, not necessarily the same material, but I guess the InstancedMeshes are good for having say a table with 6 chairs, you can use the IM for the chairs for ex?
  5. I’d like to do “true” occlusion culling I heard it is possible by using WebGL2, but don’t think it is implemented in THREE yet.
  6. That is what I was trying to do with the raycaster, but apart from not doing much for the frame rate tank, a) Objects don’t hide themselves if they are not in the casters intersection so i’d have to go back and make every object (or plane mesh in this instance) invisible, and b) how else other than the raycaster could I detect what is visible?

I am sure I did it in C++ with depth buffering under OGL but I wonder if THREE can do it or some kind of extension?

Instanced meshes can have individual scale, rotation, translation matrices and also color.

You could use methods for detecting shadows with light camera/depth buffer or color coding.

Also I believe there is a GPGPU based ray-caster shader on this forum in Resources section.

Hi @tfoller,
Thanks for your reply,
Can instanced meshes have different textures? As that is what I am looking to do.
I am not enabling shadows, nor actual lighting enabled I wanted to sort each “issue” out before moving onto the next part of the renderer, I am rendering an old DOS game map so the lighting consists of dimming the RGB value of the floor / ceiling and the walls follow suit.

Is there a way to tap into the OpenGL api direct from THREE and do occlusion culling? I haven’t looked into other Web GL libraries so don’t know if others can do it.

Right off the bat - I don’t think so, not w/o using you own custom shader or changing the existing library shader code via onBeforeCompile call and then use uv offsets for each instance inside a texture atlas, passed as instance attribute.

What I meant is you could use same techniques as for creating shadows to detect occlusion, instead of ray-casting, but that would require a custom shader material.

renderer.getContext() returns WebGL (not OpenGL) rendering context that you can use directly, it might interfere with the library though if it does something similar. There is frustum culling in THREE.js, not sure about occlusion culling.

I haven’t looked that much into shaders, I might experiment with mapping textures from an atlas onto the mesh.
Isn’t WebGL based on OpenGL ES?
I might have a look at OC with WebGL, THREE has Frustum culling but that didn’t make any difference, I tried the LOD as suggested but that made no performance difference, I also realised that I was rendering all walls with a height of 0, I removed the mesh creation for these too and it made no difference.
I might try rendering in just plain old OpenGL see if there is a performance difference over the WebGL if I have no luck with WebGL.

Yeah, I meant some features of OpenGL might be missing because it’s ES version.

From my experience with procedural trees, an old notebook (Radeon Vega 6 mobile) can run about 10k simple instanced meshes like cylinders and leaf shapes made out of a dozen vertices at FPS60, that’s the limit. But those are not texturized, only colorized.

Are you sure it’s the number of draw calls that is the culprit here? See here to get an accurate number of draw calls so that you can certainly decide if that is a problem. In my (limited) experience, the biggest resource drain when it comes to Three.js (well, it’s not directly connected to it, but anyway) is to let the animations run even when you don’t need them. In other words, in the case of, say, requestAnimationFrame(), I’ve seen quite a few cases where folks just set their rotation increments to 0 to achieve a “pause” in the animation, but that is ineffective when it comes to resource usage, the right way is to cancelAnimationFrame() altogether. Doing that whenever your scene is static might improve the performance in those moments. Of course, once you resume (or rather, start again the requests) the animation, it will be the same thing, but at least it’s not the whole time.

Chrome’s dev tools panel has some useful ways to check for bottlenecks in performance, so you can look there as well to try to identify the real cause of increased resource usage.

Hi @Yin_Cognyto,
I put the renderstats in and it states on initial load I am doing 727 draw calls, although upon rotating it goes to nearly 2000 not sure why the geometry in view is the same as when rotating apart from 3 meshes that make up the back wall, my update (requestanimationframe) checks for keypresses and updates the stats and renderstats then renders, it also outputs the current camera position (updates div text). When the scene is static it does 60fps, when the camera is moving forward back and left and right it is 60fps, as soon as I start rotating (posted a new thread for this issue) it goes down to 1fps for a few seconds then eventually goes back to 60.
I’ll have a look see if devtools can help at all.

Thanks

Chrome’s Developer Tools can help for sure. It has a Performance tab where you can Record a period of your page usage, then see how much various functions, methods, or processes from your page took to complete. If you manage to figure out the relationships between that info and what you actually do in code in those functions (e.g. various computations, draw calls, repainting the page and applying the CSS, etc.), then you’ll be able to identify what parts of your workflow weight heavier on the system and, if needed, take measures to alleviate bad performance. Assuming that is possible without sacrificing certain features in your “app”.

Other than that, the things that Three.js does under the hood when using various approaches were more or less covered by @mjurczyk and @tfoller as far as the concerns you mentioned.

Anyway, if your FPS is constantly at 60, this generally means things are going well. The impact on the system though (e.g. CPU or GPU usage) varies from case to case.

So the one thing that made a difference was setting the FAR camera value from 1000 to 100, it is a lot smoother now, Frustum culling is working as if I turn it off I go from 270 up to 1700 draw calls, I found I had it disabled initially which is why it was using 700 odd calls, left over from some previous tests.

I don’t think it is doing what it should be as if I walk up to a “wall” which is a single planegeometry mesh with no floor or ceiling in view it states it is using 29 draw calls, but why 29? Nothing else is in frame, so it must be still drawing things outside of the viewable area. Screenshot below of what I am looking at (the planegeometry) vs the stats.

Nice to see you got some improvement. Yeah, more draw calls than expected can be surprising. I’ve read somewhere some very simple example that logically should have taken 1 draw call but instead it took 6 - unfortunately I can’t remember where I’ve read that so I can give it as an example.

Anyway, here, here, here, here and here you can find other interesting and potentially useful information, tests and some nice tools regarding performance when it comes to Three.js in general. Maybe it would be of some use.

Oh, and yeah, adjusting the camera near and far values is among those tips and tricks. Which reminds me that in my current project, my camera near is at 0.00001 and the camera far is at 720000, so at some point in time maybe I should see how performance is affected by that… Fortunately, I don’t have so much stuff to render (in theory) and the impact is not yet substantial, so I can postpone that once the main work is finished.

2 Likes

That shouldn’t be a problem. If you are looking for interactivity, you still have to render at a certain rate. If you can do that, then there is no harm in running the loop, other than draining someone’s battery if you are targeting such devices. Not rendering some frames should not make the other frames render faster.

What do you expect to achieve with occlusion culling? That is a very advanced technique if I understand and it hasn’t been available in webgl. I don’t think it’s the best tool to render a bunch of planes, i think it’s more geared towards not doing expensive shader work. You are probably issuing way too many commands than are necessary.

The slowdown upon rotation, could be due to some objects being rendered for the first time. If you happen to have textures there, each one will be uploaded then. That may be what you’re seeing.

You should also probably remove any Raycasting while debugging rendering performance, since it’s not related to rendering.

Thanks for the links i’ll take a look. The more I add the slower it is!

Hi, thanks for the reply.
I want the renderer to not draw what it cannot see, I am creating the mesh from plane geometry applying a colour to it then sticking it on the scene after positioning it, the are static meshes so not sure how I can issue more commands than are necessary?

I am not using textures yet, just randomising the colour before creating the mesh (the meshes are created only once).
The slowdown seemed to be the FAR distance of the camera because I suspect it is still trying to draw objects / meshes that are not visible.

Raycasting got removed a while ago, no sign of it anywhere in my codebase now.

Thanks

Nah, far still requires threejs and the gpu to do the same amount of work in most cases.

If you can, share a live example of your code setup, there may be things people will not be able to debug through description only. I’ve done my best to read through this thread and the suggestions so far all seem on point. It may be a memory leak, you may be blocking the main thread with something, you could be recursively creating objects such as vector3’s instead of reusing resources. I’m mainly making assumptions as we can’t see your code but your physical setup vs what you are trying to achieve sounds out of balance as to the performance you’re getting.

2 Likes

+1 it’s really hard to debug code without code :slight_smile:

1 Like